Skip to content

fix (tools): freehand roi in volume viewport for oblique data#2744

Open
arul-trenser wants to merge 3 commits into
cornerstonejs:mainfrom
arul-trenser:fix-freehand-mpr-layout-oblique
Open

fix (tools): freehand roi in volume viewport for oblique data#2744
arul-trenser wants to merge 3 commits into
cornerstonejs:mainfrom
arul-trenser:fix-freehand-mpr-layout-oblique

Conversation

@arul-trenser

@arul-trenser arul-trenser commented May 26, 2026

Copy link
Copy Markdown
Contributor

Context

This PR resolves an issue where the freehand ROI tool was not functioning correctly within volume viewports displaying oblique data. The PR is incorporated by FlyWheel.io.

Changes & Results

⚠️⚠️ This change adapts the implementation from cornerstonejs/cornerstone3D#2619 with minor modifications:

  • Removed _isObliqueView check: Eliminated the restrictive conditional check for oblique views.
  • Standardized Stats Calculation: Modified the logic to consistently calculate ROI statistics using the robust approach previously reserved only for oblique views, ensuring consistency across viewports.

Before:

PR-before.mp4

After:

PR-after.mp4

Testing

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: Windows 11
  • "Node version: 22.19.0
  • "Browser: 141.0.7390.123

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved planar freehand ROI measurement for oblique contours by removing axis-alignment failures and using effective voxel spacing.
  • Improvements

    • Refined ROI voxel/statistics sampling by rasterizing ROI polygons in canvas space, deriving an appropriate sampling step, and sampling voxels from canvas coordinates (with deduplication).
  • New Features

    • Added reusable utilities for canvas-space ROI polygon rasterization and canvas-driven voxel sampling, exposed for external use.
  • Tests

    • Added Jest coverage for the ROI polygon-to-pixel rasterization iterator, including edge cases and fractional sampling.

@claude claude 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.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@arul-trenser

arul-trenser commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

@wayfarer3130 This PR replaces #2619. While we are able to draw the freehand ROI in the oblique view, the stats calculation for non-acquisition viewports in MPR mode was incorrect. This PR fixes those issues as well. Kindly review the changes.

Thanks!

Comment thread packages/tools/src/utilities/math/polyline/getSubPixelSpacingAndXYDirections.ts Outdated
Comment thread packages/tools/src/tools/annotation/PlanarFreehandROITool.ts Outdated
[canvasMaxXPadded, cy] as Types.Point2
);

if (!intersections || intersections.length === 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This entire logic seems like it should be repeated for every volume type, just the actual method used to create the AABB intersections would differ depending on the actual object. That would then handle oblique ellipse etc.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actually, it would be the overall intersections iterator that would need to be generated. You could then put it onto the stats calculator or the voxel manager as helper methods. That way the storage of thigns like the set of points iterated over could be standardized, and decided whether it is required or not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Separated

Comment thread packages/tools/src/tools/annotation/PlanarFreehandROITool.ts Outdated
Comment thread packages/tools/src/tools/annotation/PlanarFreehandROITool.ts Outdated

@wayfarer3130 wayfarer3130 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Generally really like this change, but would like it to be made a general solution for any area measurement, in terms of the intersectionIterator

Comment thread packages/tools/src/tools/annotation/PlanarFreehandROITool.ts Outdated
@arul-trenser arul-trenser force-pushed the fix-freehand-mpr-layout-oblique branch from 4ab923e to 8a9563e Compare June 23, 2026 06:32
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 78795d45-8af4-4be6-b3b7-fc3d6488fd31

📥 Commits

Reviewing files that changed from the base of the PR and between 9f34282 and ac9768c.

📒 Files selected for processing (1)
  • packages/tools/src/utilities/sampleVoxelsFromCanvas.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/tools/src/utilities/sampleVoxelsFromCanvas.ts

📝 Walkthrough

Walkthrough

The PR adds oblique-volume spacing support and changes closed freehand ROI stats sampling to use canvas-space rasterization before projecting samples back to voxel space.

Changes

Oblique Volume Freehand ROI Stats

Layer / File(s) Summary
Oblique spacing calculation
packages/tools/src/utilities/math/polyline/getSubPixelSpacingAndXYDirections.ts, packages/tools/src/utilities/math/polyline/computeEffectiveVoxelSpacing.ts, packages/tools/src/utilities/math/polyline/index.ts
getSubPixelSpacingAndXYDirections now computes effective spacing for oblique volume directions, normalizes viewUp, and re-exports the spacing helper through the polyline utilities index.
Canvas intersection and voxel sampling
packages/tools/src/utilities/math/polyline/getIntersectionIterator.ts, packages/tools/src/utilities/sampleVoxelsFromCanvas.ts, packages/tools/src/index.ts, packages/tools/src/utilities/math/polyline/index.ts, packages/tools/test/utilities/getIntersectionIterator.jest.js
Canvas-space ROI rasterization and voxel sampling helpers are added, publicly exported, and covered by rasterization tests.
Freehand ROI stats integration
packages/tools/src/tools/annotation/PlanarFreehandROITool.ts
Closed freehand ROI stats now use canvas-space scanline intersections and sampled voxel points in the cached stats path.

Sequence Diagram(s)

sequenceDiagram
  participant FreehandROITool
  participant getIntersectionIterator
  participant sampleVoxelsFromCanvas
  participant viewport
  participant imageData
  FreehandROITool->>getIntersectionIterator: build canvas-space ROI samples
  FreehandROITool->>sampleVoxelsFromCanvas: sample voxels from iterator output
  sampleVoxelsFromCanvas->>viewport: canvasToWorld and worldToIndex
  sampleVoxelsFromCanvas->>imageData: bounds-check sampled voxels
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #5889 – [Bug] Freehand ROI not renders in Volume Viewport for Oblique data: The PR changes oblique spacing and closed freehand ROI sampling in the same volume-viewport rendering path described by the issue.
  • #2646 – The PR replaces the closed freehand ROI sampling approach used by the same stats path discussed in the issue.

Possibly related PRs

Suggested reviewers

  • wayfarer3130

Poem

🐰 I hopped through oblique space today,
With canvas pixels leading the way.
No more thorns in the voxel trail,
Just tidy stats and a happy tail.
Hooray for ROIs that render bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is descriptive and matches the main fix for oblique freehand ROI rendering in volume viewports.
Description check ✅ Passed The PR follows the template with Context, Changes & Results, Testing, and completed checklist items.
Linked Issues check ✅ Passed The code changes address #5889 by handling oblique volume viewports and fixing ROI statistics and rendering flow.
Out of Scope Changes check ✅ Passed All changes support the oblique freehand ROI fix or its tests; no unrelated edits are apparent.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 2

🧹 Nitpick comments (1)
packages/tools/src/tools/annotation/PlanarFreehandROITool.ts (1)

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

Reuse sample for the stats callback.

The object passed to statsCallback duplicates sample field-for-field; pass sample directly to avoid the redundant allocation and keep the two in sync.

♻️ Proposed tweak
           pointsInShape.push(sample);

-          this.configuration.statsCalculator.statsCallback({
-            value: value as number,
-            pointLPS: worldPoint as Types.Point3,
-            pointIJK: ijkPoint,
-          });
+          this.configuration.statsCalculator.statsCallback(sample);
🤖 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/tools/annotation/PlanarFreehandROITool.ts` around lines
1016 - 1028, The code in the statsCallback call creates a duplicate object with
the exact same structure as the sample variable that was just defined. Replace
the object literal passed to this.configuration.statsCalculator.statsCallback()
with the sample variable directly to eliminate the redundant allocation and
maintain consistency between the two uses.
🤖 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/src/tools/annotation/PlanarFreehandROITool.ts`:
- Around line 974-1028: The loop iterating through canvas pixels from xEnter to
xExit uses a step size of 1, which undersamples voxels when zoomed out since
each canvas pixel can span multiple voxels in world space. Calculate the
voxel-to-canvas spacing by transforming two adjacent canvas points (e.g., cx and
cx+1) to world space and then to index space, then determine the IJK distance
between them. If this distance is greater than 1 voxel, reduce the canvas step
size or add sub-sampling to ensure all voxels within each canvas pixel are
visited. Modify the loop increment in the for statement and/or add logic to
handle fractional canvas-pixel steps so that voxel sampling density remains
independent of zoom level while preserving the oblique-view correctness.

In
`@packages/tools/src/utilities/math/polyline/getSubPixelSpacingAndXYDirections.ts`:
- Around line 99-107: The _computeEffectiveSpacing function requires a
normalized direction vector according to its JSDoc, but viewUp is being passed
raw without normalization when computing ySpacing and yDir. Apply the same
normalization pattern that is already used for viewRight at line 52 to normalize
viewUp before passing it to _computeEffectiveSpacing in the ySpacing computation
block, and use the normalized viewUp vector for the yDir assignment as well to
ensure consistent unit-length behavior in the spacing calculations.

---

Nitpick comments:
In `@packages/tools/src/tools/annotation/PlanarFreehandROITool.ts`:
- Around line 1016-1028: The code in the statsCallback call creates a duplicate
object with the exact same structure as the sample variable that was just
defined. Replace the object literal passed to
this.configuration.statsCalculator.statsCallback() with the sample variable
directly to eliminate the redundant allocation and maintain consistency between
the two uses.
🪄 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: 29fbc263-4437-43c7-bee6-e7c1eda0470f

📥 Commits

Reviewing files that changed from the base of the PR and between 7b60dbf and 8a9563e.

📒 Files selected for processing (2)
  • packages/tools/src/tools/annotation/PlanarFreehandROITool.ts
  • packages/tools/src/utilities/math/polyline/getSubPixelSpacingAndXYDirections.ts

Comment thread packages/tools/src/tools/annotation/PlanarFreehandROITool.ts Outdated
Comment thread packages/tools/src/utilities/math/polyline/getSubPixelSpacingAndXYDirections.ts Outdated

@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: 2

🤖 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/src/utilities/math/polyline/getIntersectionIterator.ts`:
- Around line 44-49: The scanline sampling in getIntersectionIterator is
snapping intersection bounds to integer pixels too early, which can skip valid
samples when canvasStep is fractional. Update the loop that computes xEnter and
xExit from intersections so it preserves the raw intersection span and only
aligns iteration to the sampling step, ensuring narrow spans still yield points.
Keep the fix localized to getIntersectionIterator and its intersection-pair
traversal logic.

In `@packages/tools/src/utilities/sampleVoxelsFromCanvas.ts`:
- Line 82: The voxel sampling logic in sampleVoxelsFromCanvas is reading
voxelManager from viewport.getImageData(), which can mismatch the selected
target image. Update the code to use the voxel manager from the passed-in
imageData for the current targetId throughout the sampling flow, and ensure any
IJK-to-value lookups in sampleVoxelsFromCanvas are paired with that target’s
voxel manager rather than the viewport’s current image data.
🪄 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: ebccfcff-25a2-41a5-8b63-df3f798054cb

📥 Commits

Reviewing files that changed from the base of the PR and between 8a9563e and c9429b5.

📒 Files selected for processing (8)
  • packages/tools/src/index.ts
  • packages/tools/src/tools/annotation/PlanarFreehandROITool.ts
  • packages/tools/src/utilities/math/polyline/computeEffectiveVoxelSpacing.ts
  • packages/tools/src/utilities/math/polyline/getIntersectionIterator.ts
  • packages/tools/src/utilities/math/polyline/getSubPixelSpacingAndXYDirections.ts
  • packages/tools/src/utilities/math/polyline/index.ts
  • packages/tools/src/utilities/sampleVoxelsFromCanvas.ts
  • packages/tools/test/utilities/getIntersectionIterator.jest.js
✅ Files skipped from review due to trivial changes (3)
  • packages/tools/src/utilities/math/polyline/index.ts
  • packages/tools/test/utilities/getIntersectionIterator.jest.js
  • packages/tools/src/utilities/math/polyline/computeEffectiveVoxelSpacing.ts

Comment thread packages/tools/src/utilities/sampleVoxelsFromCanvas.ts Outdated

@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

🤖 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/src/utilities/sampleVoxelsFromCanvas.ts`:
- Around line 71-77: The sampleVoxelsFromCanvas helper now requires voxelManager
but the PlanarFreehandROITool call site is still not passing it, which can leave
getAtIJKPoint unusable. Update the sampleVoxelsFromCanvas signature to type
voxelManager explicitly, then thread the same voxelManager argument through the
PlanarFreehandROITool invocation so the helper always receives a valid manager.
🪄 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: b89359fe-48bd-4244-bcaa-d1427caa6d26

📥 Commits

Reviewing files that changed from the base of the PR and between c9429b5 and 30dbc71.

📒 Files selected for processing (3)
  • packages/tools/src/utilities/math/polyline/getIntersectionIterator.ts
  • packages/tools/src/utilities/sampleVoxelsFromCanvas.ts
  • packages/tools/test/utilities/getIntersectionIterator.jest.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/tools/test/utilities/getIntersectionIterator.jest.js
  • packages/tools/src/utilities/math/polyline/getIntersectionIterator.ts

Comment thread packages/tools/src/utilities/sampleVoxelsFromCanvas.ts Outdated
@arul-trenser arul-trenser force-pushed the fix-freehand-mpr-layout-oblique branch from 30dbc71 to 9f34282 Compare June 24, 2026 12:12

@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.

♻️ Duplicate comments (1)
packages/tools/src/tools/annotation/PlanarFreehandROITool.ts (1)

945-951: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

De-duplicate rounded IJK samples before updating stats.

sampleVoxelsFromCanvas invokes statsCallback for every canvas sample after rounding to IJK, so sub-pixel sampling or zoomed-in views can count the same voxel multiple times. That makes ROI stats viewport/zoom weighted instead of voxel weighted; filter by pointIJK before calling the stats calculator and before storing pointsInShape.

🐛 Possible call-site fix
-    const pointsInShape = sampleVoxelsFromCanvas({
+    const pointsInShape = [];
+    const visitedIJK = new Set<string>();
+
+    sampleVoxelsFromCanvas({
       iterator: pixelIterator,
       imageData,
       viewport,
       voxelManager,
-      statsCallback: this.configuration.statsCalculator.statsCallback,
+      statsCallback: (sample) => {
+        const key = sample.pointIJK.join(',');
+
+        if (visitedIJK.has(key)) {
+          return;
+        }
+
+        visitedIJK.add(key);
+        pointsInShape.push(sample);
+        this.configuration.statsCalculator.statsCallback(sample);
+      },
     });
🤖 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/tools/annotation/PlanarFreehandROITool.ts` around lines
945 - 951, The ROI sampling in PlanarFreehandROITool is counting duplicate
rounded IJK voxels because sampleVoxelsFromCanvas emits stats for every canvas
sample. Update the call site around pointsInShape so duplicates are filtered by
pointIJK before invoking this.configuration.statsCalculator.statsCallback and
before saving pointsInShape, keeping stats voxel-weighted rather than
viewport-weighted.
🤖 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.

Duplicate comments:
In `@packages/tools/src/tools/annotation/PlanarFreehandROITool.ts`:
- Around line 945-951: The ROI sampling in PlanarFreehandROITool is counting
duplicate rounded IJK voxels because sampleVoxelsFromCanvas emits stats for
every canvas sample. Update the call site around pointsInShape so duplicates are
filtered by pointIJK before invoking
this.configuration.statsCalculator.statsCallback and before saving
pointsInShape, keeping stats voxel-weighted rather than viewport-weighted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ef5c4512-87f7-4807-9f96-817fd0269f1a

📥 Commits

Reviewing files that changed from the base of the PR and between 30dbc71 and 9f34282.

📒 Files selected for processing (8)
  • packages/tools/src/index.ts
  • packages/tools/src/tools/annotation/PlanarFreehandROITool.ts
  • packages/tools/src/utilities/math/polyline/computeEffectiveVoxelSpacing.ts
  • packages/tools/src/utilities/math/polyline/getIntersectionIterator.ts
  • packages/tools/src/utilities/math/polyline/getSubPixelSpacingAndXYDirections.ts
  • packages/tools/src/utilities/math/polyline/index.ts
  • packages/tools/src/utilities/sampleVoxelsFromCanvas.ts
  • packages/tools/test/utilities/getIntersectionIterator.jest.js
✅ Files skipped from review due to trivial changes (1)
  • packages/tools/src/utilities/math/polyline/index.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/tools/src/utilities/math/polyline/computeEffectiveVoxelSpacing.ts
  • packages/tools/src/index.ts
  • packages/tools/src/utilities/sampleVoxelsFromCanvas.ts
  • packages/tools/test/utilities/getIntersectionIterator.jest.js
  • packages/tools/src/utilities/math/polyline/getIntersectionIterator.ts
  • packages/tools/src/utilities/math/polyline/getSubPixelSpacingAndXYDirections.ts

@arul-trenser

Copy link
Copy Markdown
Contributor Author

@wayfarer3130 I've updated the code based on your feedback. Could you please take another look when you have a moment?

Thanks!

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.

[Bug] Freehand ROI not renders in Volume Viewport for Oblique data

2 participants