BUG: Fix missing ROI slices when RT Struct contains sub-pixel contours#117
BUG: Fix missing ROI slices when RT Struct contains sub-pixel contours#117brianmanderson wants to merge 3 commits into
Conversation
DicomRTStruct2ImageFilter used a global extrude+stencil with SetTolerance(0), which lost entire mask slices when the input contained degenerate sub-pixel contours (LUNG1-002 slice 88 with a 0.07x0.12 mm exclusion - 1/8 of a voxel). Replaced with per-slice rasterisation; the slab is centred on the destination voxel so the +0.5 mm Z hack is no longer needed. Also fixes a pre-existing copy-paste bug in UpdateDicomItem (Y was clobbered with Z, Z was never written), and softens the strict p[2] == mZ planarity assert to a tolerant warning that snaps off-plane points to the contour plane. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…69c8 Fix missing ROI slices in RT Struct rasterisation
|
Providing some instructions on where to find the data that I originally identified this bug with below: Data sourceThe reproducer dataset is patient LUNG1-002 from the public
How to download just LUNG1-002The whole collection is several hundred GB, but a single patient is ~50 MB.
Alternatively, the same patient can be fetched programmatically via the Files needed to reproduceAfter download the patient directory contains a CT series (~110 axial |
|
Absolutely love vv and I hope this is helpful for the project! |
Problem
When opening a DICOM RT Struct via the ROI Manager tool, an entire axial
slice of the ROI can be missing from the overlay even though the contour
exists in the file (and renders correctly in other treatment-planning
systems such as Eclipse and Pinnacle, and in the coronal/sagittal
projections within vv itself).
The same ROI displays correctly on all neighbouring slices, so the gap
is easy to spot when stepping through axial views.
Reproducer
Reproducible on patient LUNG1-002 from the public
NSCLC-Radiomics collection
on TCIA, using ROI
Lung-Leftfrom the supplied RT Struct.CT geometry: 512×512×111, spacing 0.977×0.977×3 mm, origin
(-250.112, -250.112, -133.4).
The bug manifests at axial slice 88 (Z = 130.6 mm).
The RT Struct stores three closed-planar contour items for
Lung-Leftat Z = 130.6:
Other multi-contour slices in the same ROI (84, 92, 93) display fine —
only slice 88 fails, because contour #2 is dramatically smaller than
any other contour in the dataset.
Root cause
clitk::DicomRTStruct2ImageFilter::Update()(incommon/clitkDicomRTStruct2ImageFilter.cxx) historically:vtkPolyData.-ZbymSpacing[2].vtkPolyDataToImageStencilandSetTolerance(0).The combination of (a) cross-slice global mesh, (b) zero-tolerance
stencil, and (c) a near-degenerate input polygon caused
vtkPolyDataToImageStencilto mark the entire mask slice for that Z asempty, rather than producing the expected lung with a tiny invisible
sub-pixel hole.
A separate, hard-coded
+0.5 mmZ offset inDicomRT_Contour::Read()(introduced in commits
a1acb6a4/fa3343f1, 2017, "Debug RTStructoffset in z direction with vtk6") had been added to push voxel centres
off the slab boundary in the extrusion pipeline. It is half the slice
thickness for 1 mm slices but only 17 % of it for 3 mm slices — fragile
in either direction.
Fix
Replace the global extrude-then-stencil with per-slice rasterisation:
translate it by
−spacing/2so the slab will straddle the destinationvoxel centre, then extrude by
+spacingand stencil into the mask.vtkPolyDataToImageStenciltolerance (no moreSetTolerance(0)) — boundary voxels become deterministic.single Z plane, so the small exclusions are treated correctly.
Side cleanups in the same commit:
+0.5 mmZ offset (no longer needed with thecentred slab, and incorrect for non-3 mm slice spacing).
DicomRT_Contour::UpdateDicomItem()where Y was overwritten with Zand Z was never written back to the output buffer.
assert(p[2] == mZ)planarity check (a no-op inrelease builds, but flooded stderr in debug) with a tolerant warning
that snaps off-plane points onto the contour plane.
DicomRT_ROI::GetListOfContours()so the filter can iteratecontours by Z.
Verification
All three CI matrix builds (ubuntu-22.04, windows-2022, macos-14)
pass.
On LUNG1-002
Lung-Left, mask voxel counts:Slice-88 count fits between its neighbours, consistent with the lung
tapering toward the apex.
Stepped through axial slices 80–96 in the rebuilt vv binary: the
Lung-Leftcontour now renders continuously, including on slice 88.Coronal and sagittal projections at Z = 130.6 mm look identical to
before the change (those views never relied on the broken axial mask).
Spot-checked one single-contour-per-slice RT Struct as a regression
control: per-slice path produces the same mask the global path used to
produce.
Files changed
common/clitkDicomRTStruct2ImageFilter.cxx— per-slice rasterisation.common/clitkDicomRT_Contour.cxx— drop the±0.5 mmZ hack, fixthe Y/Z copy-paste bug, soften the planarity assert.
common/clitkDicomRT_ROI.h— addGetListOfContours().