Skip to content

Depth Pixel Spacing Bug for Oblique Cases (DSTVO) - #588

Open
bluna301 wants to merge 2 commits into
Project-MONAI:mainfrom
bluna301:bl/depth_pixel_spacing_bug
Open

bluna301 wants to merge 2 commits into
Project-MONAI:mainfrom
bluna301:bl/depth_pixel_spacing_bug

Conversation

@bluna301

@bluna301 bluna301 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Bug

A discrepancy was identified between DICOM SR volume values written by one of our internal MAP pipelines and calculated volumes extracted from DICOM SEG metadata (produced by the same pipeline). This is despite DICE being > 0.999 for MONAI Bundle NIfTI vs. MAP DICOM SEG.

Further investigation showed a discrepancy in the depth pixel spacing (DPS) produced by the DICOMSeriesToVolumeOperator vs. DPS extracted from the DICOM SEG:

Case ID IOP Oblique Angle SR Volume SEG Volume Volume RVE% DSTVO DPS SEG DPS DPS Error % NIfTI - SEG DICE
956_001_pre [1.0000, 0.0000, 0.0000, 0.0000, 0.0000, -1.0000] 0.00° 109.300 mL 109.270 mL 0.027% 4.000004 mm 4.000000 mm 0.0001% 0.9999
956_001_post [1.0000, 0.0000, 0.0000, 0.0000, 0.0000, -1.0000] 0.00° 215.800 mL 215.768 mL 0.015% 4.000004 mm 4.000000 mm 0.0001% 0.9999
956_002_post [0.9935, 0.0901, 0.0699, 0.0681, 0.0233, -0.9974] 5.34° 192.200 mL 193.902 mL 0.878% 3.965520 mm 4.000000 mm 0.8620% 0.9999
956_003_pre [1.0000, 0.0000, 0.0000, 0.0000, 0.0322, -0.9995] 1.85° 133.400 mL 133.574 mL 0.130% 3.995841 mm 4.000000 mm 0.1040% 0.9999
956_003_post [1.0000, 0.0000, 0.0000, 0.0000, 0.0322, -0.9995] 1.85° 197.800 mL 197.966 mL 0.084% 3.995841 mm 4.000000 mm 0.1040% 0.9998
956_004_pre [0.9992, -0.0402, 0.0000, 0.0000, 0.0000, -1.0000] 2.30° 251.800 mL 252.218 mL 0.166% 3.993552 mm 4.000000 mm 0.1612% 0.9998
956_004_post [0.9992, -0.0402, 0.0000, 0.0000, 0.0000, -1.0000] 2.30° 335.500 mL 336.011 mL 0.152% 3.993552 mm 4.000000 mm 0.1612% 0.9997
956_006_pre [1.0000, 0.0000, 0.0000, 0.0000, 0.0000, -1.0000] 0.00° 73.100 mL 73.126 mL 0.036% 4.000000 mm 4.000000 mm 0.0000% 0.9998
956_006_post [1.0000, 0.0000, 0.0000, 0.0000, 0.0000, -1.0000] 0.00° 87.400 mL 87.361 mL 0.045% 4.000000 mm 4.000000 mm 0.0000% 0.9999

DPS seemed to be underestimated specifically for oblique acquisitions.

Investigation

Our MAP pipeline pulls depth_pixel_spacing (calculated in DSTVO) from metadata (DSTVO's create_metadata method)

After analysis of DSTVO's current approach, DPS is systematically underestimated for oblique acquisitions.

Currently, DPS is derived by taking the Euclidean norm of the difference between the first_pixel_on_slice_normal vectors of the first two SOP instances. That vector is not a point in space, so the norm is not a distance. The resulting spacing is correct only when ImageOrientationPatient is not oblique. For any oblique orientation, DPS is always too small.

Inside prepare_series, each slice stores an element-wise product:

slice_normal[0] = cosines[1] * cosines[5] - cosines[2] * cosines[4]
slice_normal[1] = cosines[2] * cosines[3] - cosines[0] * cosines[5]
slice_normal[2] = cosines[0] * cosines[4] - cosines[1] * cosines[3]
...
i = 0
while i < 3:
    point[i] = slice_normal[i] * slice_position[i]
    i += 1
distance += point[0] + point[1] + point[2]
series._sop_instances[slice_index].distance = distance
series._sop_instances[slice_index].first_pixel_on_slice_normal = point

and later consumes it as if it were a coordinate:

p1 = series._sop_instances[0].first_pixel_on_slice_normal
p2 = series._sop_instances[1].first_pixel_on_slice_normal
depth_pixel_spacing = (
    (p1[0] - p2[0]) * (p1[0] - p2[0])
    + (p1[1] - p2[1]) * (p1[1] - p2[1])
    + (p1[2] - p2[2]) * (p1[2] - p2[2])
)
depth_pixel_spacing = math.sqrt(depth_pixel_spacing)
series.depth_pixel_spacing = depth_pixel_spacing

sqrt(a² + b² + c²) is only a length when a, b, c are displacements along three mutually orthogonal axes. Here a, b, and c are the three component differences p1[i] - p2[i], which are three addends of a single sum, not displacements along three orthogonal axes. Combining them under Pythagoras rather than adding them is a category error. The real DPS is |a + b + c|.

Fix

The signed position along the normal for each slice has already been calculated as distance. series._sop_instances have already been sorted by distance, so taking the difference between two consecutive slice's signed positions should give the depth pixel spacing:

d1 = series._sop_instances[0].distance
d2 = series._sop_instances[1].distance
depth_pixel_spacing = abs(d2 - d1)

Examples

Non-oblique. IOP = [1, 0, 0, 0, 1, 0] gives n = (0, 0, 1). Two slices 4 mm apart:

IPP first_pixel_on_slice_normal distance
slice 0 (-150, -150, 10.0) (0, 0, 10.0) 10.0
slice 1 (-150, -150, 14.0) (0, 0, 14.0) 14.0

Norm of the difference: sqrt(0² + 0² + 4²) = 4.0. Difference of distances: abs(14.0 - 10.0) = 4.0. The two agree, because n zeroes two of the three components and sqrt(x²) = |x| for a single surviving term. Every non-oblique axial, coronal and sagittal series would produce the correct DPS, and thus the failure in calculation logic would be silent.

Oblique. IOP = [1, 0, 0, 0, 0.8, -0.6] gives n = (0, 0.6, 0.8). Same 4 mm spacing, so the second slice is offset by 4n = (0, 2.4, 3.2):

IPP first_pixel_on_slice_normal distance
slice 0 (-150, -150.0, 10.0) (0, -90.00, 8.00) -82.0
slice 1 (-150, -147.6, 13.2) (0, -88.56, 10.56) -78.0

Norm of the difference: sqrt(0² + 1.44² + 2.56²) = 2.937. Difference of distances: abs(-78.0 - -82.0) = 4.0.

Two components are now live instead of one, so the norm splits the 4 mm across them and takes the hypotenuse instead of the sum. This results in a constant underestimating of the DPS in oblique cases.

Post-Fix Comparison

After the fix, the DPS is now correct for the oblique cases; DPS Error % and Volume RVE % are now consistently minuscule as expected. All cases have DPS = 4 mm, which aligns with the SpacingBetweenSlices metadata on each DICOM series.

Case ID IOP Oblique Angle SR Volume SEG Volume Volume RVE% DSTVO DPS SEG DPS DPS Error % NIfTI - SEG DICE
956_001_pre [1.0000, 0.0000, 0.0000, 0.0000, 0.0000, -1.0000] 0.00° 109.300 mL 109.273 mL 0.025% 4.000004 mm 4.000000 mm 0.0001% 0.9999
956_001_post [1.0000, 0.0000, 0.0000, 0.0000, 0.0000, -1.0000] 0.00° 215.800 mL 215.768 mL 0.015% 4.000004 mm 4.000000 mm 0.0001% 0.9999
956_002_post [0.9935, 0.0901, 0.0699, 0.0681, 0.0233, -0.9974] 5.34° 193.900 mL 193.902 mL 0.001% 3.999998 mm 4.000000 mm 0.0000% 0.9999
956_003_pre [1.0000, 0.0000, 0.0000, 0.0000, 0.0322, -0.9995] 1.85° 133.600 mL 133.577 mL 0.017% 3.999997 mm 4.000000 mm 0.0001% 0.9999
956_003_post [1.0000, 0.0000, 0.0000, 0.0000, 0.0322, -0.9995] 1.85° 198.000 mL 197.969 mL 0.016% 3.999997 mm 4.000000 mm 0.0001% 0.9999
956_004_pre [0.9992, -0.0402, 0.0000, 0.0000, 0.0000, -1.0000] 2.30° 252.200 mL 252.218 mL 0.007% 4.000001 mm 4.000000 mm 0.0000% 0.9998
956_004_post [0.9992, -0.0402, 0.0000, 0.0000, 0.0000, -1.0000] 2.30° 336.000 mL 336.008 mL 0.002% 4.000001 mm 4.000000 mm 0.0000% 0.9997
956_006_pre [1.0000, 0.0000, 0.0000, 0.0000, 0.0000, -1.0000] 0.00° 73.100 mL 73.126 mL 0.036% 4.000000 mm 4.000000 mm 0.0000% 0.9998
956_006_post [1.0000, 0.0000, 0.0000, 0.0000, 0.0000, -1.0000] 0.00° 87.400 mL 87.361 mL 0.045% 4.000000 mm 4.000000 mm 0.0000% 0.9999

Summary by CodeRabbit

  • Bug Fixes

    • Improved depth pixel spacing calculations when converting DICOM series into volumes, enabling more accurate spatial measurements.
  • Chores

    • Updated copyright information.
    • Removed unused code.

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: de3d63d7-3823-440f-a92b-2b7cde8ae894

📥 Commits

Reviewing files that changed from the base of the PR and between dae4f92 and 095a6e3.

📒 Files selected for processing (1)
  • monai/deploy/operators/dicom_series_to_volume_operator.py

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


Walkthrough

The DICOM series-to-volume operator updates its copyright year, removes unused setup code, and calculates depth pixel spacing from the first two slice-normal projections.

Changes

DICOM spacing update

Layer / File(s) Summary
Depth pixel spacing calculation
monai/deploy/operators/dicom_series_to_volume_operator.py
prepare_series calculates depth pixel spacing from the absolute difference between the first two slices’ .distance values. The calculation logs the projections and result. The unused math import and redundant initialization are removed. The copyright year is updated.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 095a6

Depth spacing is now calculated from consecutive slice projection distances, correcting oblique-series spacing without unresolved merge-readiness risk.

Suggested reviewers: mmelqin

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: fixing the depth pixel spacing bug for oblique cases in DICOM series-to-volume processing.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files.
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.
✨ 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.

Signed-off-by: bluna301 <luna.bryanr@gmail.com>
Signed-off-by: bluna301 <luna.bryanr@gmail.com>
@sonarqubecloud

sonarqubecloud Bot commented Sep 3, 2026

Copy link
Copy Markdown

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.

3 participants