Skip to content

BLOCKED BY VTK MERGE FIRST DO NOT MERGE feat(segmentation): labelmap edge projection over MIP for generic planar viewports#2784

Open
sedghi wants to merge 4 commits into
mainfrom
cornerstoneimproveMIPSegPerf
Open

BLOCKED BY VTK MERGE FIRST DO NOT MERGE feat(segmentation): labelmap edge projection over MIP for generic planar viewports#2784
sedghi wants to merge 4 commits into
mainfrom
cornerstoneimproveMIPSegPerf

Conversation

@sedghi

@sedghi sedghi commented Jul 5, 2026

Copy link
Copy Markdown
Member

Context

Labelmap edge projection over MIP only worked on legacy volume viewports; generic planar viewports had no working labelmap-over-MIP path at all. This PR adds that capability, with a new labelmapMIPGeneric example.

The generic-viewport slab label outline relies on the slab-projected label outline added to the vtk.js ImageResliceMapper in Kitware/vtk-js#3541 (which also carries a fix that keeps mapper fragment blocks compatible with render-pass postambles - early returns skipped the OIT weight write and composited the outlines white - and a slab-loop exit that keeps huge slab thicknesses proportional to the in-volume march). Until that vtk.js change is released, this PR is blocked; running the example requires a linked vtk.js build containing that PR.

The editing-performance and legacy-path fixes that do not depend on vtk.js were split out into #2797, which can merge independently.

Changes & Results

Generic planar viewports (new capability + example labelmapMIPGeneric)

  • The labelmap mounts as a separate overlay display set and now inherits the source display set's slabThickness, so labels project across the same slab as the MIP instead of reflecting only the current slice.
  • Segmentation overlays keep the transfer functions assigned by the segmentation styling; previously the data-presentation update reapplied a VOI grayscale ramp over the labelmap colors.

Editing performance in the generic render path

  • The generic volume-slice render path defers volume-modified repaints of slab-projecting overlays to a 240 ms trailing debounce (a slab-projecting overlay re-marches the whole slab per fragment, far too expensive to repeat for every brush-stroke event), and no longer invalidates the full 3D texture per modification - brush edits upload only the modified slices through the existing streaming-texture partial update path.
  • perf(segmentation): defer projection-heavy renders and merge only modified slices during labelmap edits #2797 complements this on the tools side by deferring the segmentation-render trigger itself for projection-heavy viewports.

Measured on the example (PET volume, Apple Silicon): brush strokes cost ~1.3 ms of main thread per pointer move, the projection viewport refreshes ~240 ms after the stroke ends, and rotating the generic MIP viewport with a whole-volume slab (50000) plus outline overlay renders at ~30 ms/frame.

Testing

  • pnpm run example labelmapMIPGeneric (requires linked vtk.js with feat(ImageResliceMapper): add LABELMAP_EDGE_PROJECTION_BLEND Kitware/vtk-js#3541): click "Add Labelmap Representation" - the MIP viewport should show outlines in the segment colors (not white). Draw on the left viewport with the brush: drawing stays responsive, and the MIP outline updates shortly after the stroke ends. Rotate the MIP viewport with the mouse wheel. Delete and re-add the representation.

Checklist

PR

  • My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • "OS: macOS (Darwin 25.5.0)"
  • "Node version: 22.22.2"
  • "Browser: Chrome for Testing (latest)"

…wports

Adds the labelmapMIPGeneric example and makes LABELMAP_EDGE_PROJECTION_BLEND
work on generic planar viewports: the labelmap mounts as an overlay display
set that inherits the source display set's slab thickness, and segmentation
overlays keep their labelmap transfer functions instead of having a VOI
grayscale ramp reapplied over them.

Editing performance: segmentation-data modifications no longer re-render
projection-heavy viewports live. triggerSegmentationRenderForModified renders
cheap viewports immediately and defers viewports that re-march a slab per
fragment (legacy edge-projection blend or generic slab presentations) until
the modification stream goes quiet (240ms trailing). The generic volume-slice
render path applies the same deferral to volume-modified repaints of slab
overlays and drops its full-texture invalidation so brush edits upload only
the modified slices.

Legacy independent-components path fixes: the combined actor is re-added
under the render plan's representationUID (the plan tore it down otherwise),
restore removes the injected CPU scalars and label-outline property state,
the data-modified merge now touches only modifiedSlicesToUse with direct
per-slice copies instead of a full-volume per-voxel loop, and the debounced
listener is removed with removeEventListenerDebounced (the plain removal
leaked the wrapper, leaving zombie merges after delete/re-add cycles).

The generic slab label outline requires vtk.js with the slab-projected label
outline (Kitware/vtk-js#3541).
@cursor

cursor Bot commented Jul 5, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds debounced re-rendering for projection-heavy segmentation overlays, updates planar volume rendering to batch overlay refreshes and preserve overlay VOI behavior, propagates slab-thickness metadata to labelmap overlays, refines labelmap scalar restoration and partial-slice updates, and adds a labelmap-over-MIP example.

Changes

Segmentation overlay projection rendering

Layer / File(s) Summary
Debounced render trigger core
packages/tools/src/stateManagement/segmentation/SegmentationRenderingEngine.ts
Adds a per-viewport timer registry, a projection-heavy viewport classifier, and triggerSegmentationRenderForModified, which immediately renders cheap viewports and debounces projection-heavy ones with a 240ms delay.
Event listener wiring to debounced trigger
packages/tools/src/eventListeners/segmentation/segmentationDataModifiedEventListener.ts
Switches the segmentation data-modified listener to call triggerSegmentationRenderForModified and documents the high-frequency update path.
Vtk volume slice render path batching and presentation
packages/core/src/RenderingEngine/GenericViewport/Planar/VtkVolumeSliceRenderPath.ts, packages/core/src/RenderingEngine/GenericViewport/Planar/planarRuntimeTypes.ts
Adds isSegmentationOverlay, batches IMAGE_VOLUME_MODIFIED renders for projecting overlays with a deferred timer, clears pending timers on cleanup, and skips defaultVOIRange for segmentation overlays during presentation updates.
Slab-thickness propagation to labelmap overlays
packages/tools/src/tools/displayTools/Labelmap/labelmapRenderPlan/planarGenericVolumeLabelmap.ts
Extends planar viewport presentation contracts with getSourceDataId and slabThickness, then passes the source viewport’s slab thickness into labelmap overlay presentation when present.
Independent-components labelmap scalar restore and partial update
packages/tools/src/tools/displayTools/Labelmap/addVolumesAsIndependentComponents.ts
Captures and restores shared image-data scalars and label-outline actor state, updates only modified slices in the combined segmentation array, uses debounced listener removal, and restores the original base-volume state on teardown.
Labelmap-over-MIP example demo
packages/tools/examples/labelmapMIPGeneric/index.ts
Adds a standalone example with two planar viewports, a streaming volume, MIP presentation, mock labelmap segmentation styling, and toolbar actions to add or remove the labelmap representation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested reviewers: wayfarer3130

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title identifies the main feature and uses a valid feat(segmentation) prefix.
Description check ✅ Passed The description matches the template with context, changes, testing, and checklist sections completed.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cornerstoneimproveMIPSegPerf

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/tools/src/stateManagement/segmentation/SegmentationRenderingEngine.ts (1)

304-304: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Magic delay duplicated across two files.

DEFERRED_SEGMENTATION_RENDER_DELAY_MS (240ms) here duplicates PROJECTING_OVERLAY_RENDER_DELAY_MS in VtkVolumeSliceRenderPath.ts. Extracting a single shared constant would avoid the two debounce windows drifting apart if one is tuned later.

Also applies to: 304-304

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/tools/src/stateManagement/segmentation/SegmentationRenderingEngine.ts`
at line 304, The segmentation render delay is duplicated as a magic value, so
the debounce windows can drift apart if one is changed later. Extract a single
shared constant for the 240ms delay and use it in both
`DEFERRED_SEGMENTATION_RENDER_DELAY_MS` and
`PROJECTING_OVERLAY_RENDER_DELAY_MS`, updating `SegmentationRenderingEngine` and
`VtkVolumeSliceRenderPath` to reference the same symbol.
packages/core/src/RenderingEngine/GenericViewport/Planar/VtkVolumeSliceRenderPath.ts (1)

101-135: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Debounce keys off rendering.dataPresentation, which is undefined until the first updateDataPresentation call.

isProjectingOverlay reads rendering.dataPresentation?.slabThickness, but dataPresentation is initialized to undefined at line 97 and only populated by a later updateDataPresentation call. Any IMAGE_VOLUME_MODIFIED events that fire before the first presentation update will bypass the debounce and render immediately, even for what will become a projecting overlay. This is a narrow startup-ordering edge case with a safe fallback (an extra immediate render), not a correctness break.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/core/src/RenderingEngine/GenericViewport/Planar/VtkVolumeSliceRenderPath.ts`
around lines 101 - 135, The debounce decision in VtkVolumeSliceRenderPath is
reading rendering.dataPresentation before it is guaranteed to be initialized, so
early IMAGE_VOLUME_MODIFIED events can skip the intended delayed render path.
Update the event handler to base isProjectingOverlay on a value that is
available before the first updateDataPresentation call, or defer the debounce
decision until the presentation state has been set. Use the existing
subscribeToVolumeEvents callback, renderings’s dataPresentation state, and the
deferredProjectionRenderTimer logic to keep the projecting-overlay behavior
consistent during startup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/tools/examples/labelmapMIPGeneric/index.ts`:
- Around line 73-100: The "Add Labelmap Representation" handler currently always
calls addLabelmapRepresentationToViewport, which can create duplicates for the
same viewportId2 and segmentationId on repeated clicks. Update the onClick logic
to first check segmentation.state.getSegmentationRepresentations(viewportId2)
for an existing Labelmap representation matching segmentationId, and only call
addLabelmapRepresentationToViewport when none is present, similar to the guard
already used in the delete handler.

---

Nitpick comments:
In
`@packages/core/src/RenderingEngine/GenericViewport/Planar/VtkVolumeSliceRenderPath.ts`:
- Around line 101-135: The debounce decision in VtkVolumeSliceRenderPath is
reading rendering.dataPresentation before it is guaranteed to be initialized, so
early IMAGE_VOLUME_MODIFIED events can skip the intended delayed render path.
Update the event handler to base isProjectingOverlay on a value that is
available before the first updateDataPresentation call, or defer the debounce
decision until the presentation state has been set. Use the existing
subscribeToVolumeEvents callback, renderings’s dataPresentation state, and the
deferredProjectionRenderTimer logic to keep the projecting-overlay behavior
consistent during startup.

In
`@packages/tools/src/stateManagement/segmentation/SegmentationRenderingEngine.ts`:
- Line 304: The segmentation render delay is duplicated as a magic value, so the
debounce windows can drift apart if one is changed later. Extract a single
shared constant for the 240ms delay and use it in both
`DEFERRED_SEGMENTATION_RENDER_DELAY_MS` and
`PROJECTING_OVERLAY_RENDER_DELAY_MS`, updating `SegmentationRenderingEngine` and
`VtkVolumeSliceRenderPath` to reference the same symbol.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b61f7c1c-c2a6-4f86-86f7-e100a7b77f6f

📥 Commits

Reviewing files that changed from the base of the PR and between e3f8b2b and 36279f7.

📒 Files selected for processing (7)
  • packages/core/src/RenderingEngine/GenericViewport/Planar/VtkVolumeSliceRenderPath.ts
  • packages/core/src/RenderingEngine/GenericViewport/Planar/planarRuntimeTypes.ts
  • packages/tools/examples/labelmapMIPGeneric/index.ts
  • packages/tools/src/eventListeners/segmentation/segmentationDataModifiedEventListener.ts
  • packages/tools/src/stateManagement/segmentation/SegmentationRenderingEngine.ts
  • packages/tools/src/tools/displayTools/Labelmap/addVolumesAsIndependentComponents.ts
  • packages/tools/src/tools/displayTools/Labelmap/labelmapRenderPlan/planarGenericVolumeLabelmap.ts

Comment on lines +73 to +100
addButtonToToolbar({
title: 'Add Labelmap Representation',
onClick: () => {
segmentation.addLabelmapRepresentationToViewport(viewportId2, [
{
segmentationId,
config: {
blendMode: BlendModes.LABELMAP_EDGE_PROJECTION_BLEND,
},
},
]);
},
});

addButtonToToolbar({
title: 'Delete Labelmap Representation',
onClick: () => {
const representations =
segmentation.state.getSegmentationRepresentations(viewportId2);

if (representations && representations.length > 0) {
segmentation.removeSegmentationRepresentation(viewportId2, {
segmentationId,
type: SegmentationRepresentations.Labelmap,
});
}
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard "Add Labelmap Representation" against re-adding an existing representation.

The delete handler checks getSegmentationRepresentations(viewportId2) before removing, but the add handler unconditionally calls addLabelmapRepresentationToViewport even if a representation already exists for viewportId2/segmentationId, risking duplicate representations or errors on repeated clicks.

🔧 Proposed fix
 addButtonToToolbar({
   title: 'Add Labelmap Representation',
   onClick: () => {
+    const representations =
+      segmentation.state.getSegmentationRepresentations(viewportId2);
+    if (representations && representations.length > 0) {
+      return;
+    }
     segmentation.addLabelmapRepresentationToViewport(viewportId2, [
       {
         segmentationId,
         config: {
           blendMode: BlendModes.LABELMAP_EDGE_PROJECTION_BLEND,
         },
       },
     ]);
   },
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tools/examples/labelmapMIPGeneric/index.ts` around lines 73 - 100,
The "Add Labelmap Representation" handler currently always calls
addLabelmapRepresentationToViewport, which can create duplicates for the same
viewportId2 and segmentationId on repeated clicks. Update the onClick logic to
first check segmentation.state.getSegmentationRepresentations(viewportId2) for
an existing Labelmap representation matching segmentationId, and only call
addLabelmapRepresentationToViewport when none is present, similar to the guard
already used in the delete handler.

@sedghi

sedghi commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

@salimkanoun also speed improvements

@salimkanoun

Copy link
Copy Markdown
Contributor

Amazing, thanks!

sedghi added 2 commits July 8, 2026 23:09
The merge with main made Viewport.flip protected, so casting IViewport to
Partial<IVolumeViewport> now fails TS2352 (protected-vs-public flip overlap).
Use the same minimal structural cast already used for the planar-method probe
below it.

Claude-Session: https://claude.ai/code/session_01NJNAWVWWtguh3aA8w1R9mp
@sedghi sedghi changed the title feat(segmentation): labelmap edge projection over MIP for generic viewports feat(segmentation): labelmap edge projection over MIP for generic planar viewports Jul 10, 2026
@sedghi sedghi changed the title feat(segmentation): labelmap edge projection over MIP for generic planar viewports BLOCKED BY VTK MERGE FIRST DO NOT MERGE feat(segmentation): labelmap edge projection over MIP for generic planar viewports Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants