Skip to content

BUG: Fix missing ROI slices when RT Struct contains sub-pixel contours#117

Open
brianmanderson wants to merge 3 commits into
open-vv:masterfrom
brianmanderson:master
Open

BUG: Fix missing ROI slices when RT Struct contains sub-pixel contours#117
brianmanderson wants to merge 3 commits into
open-vv:masterfrom
brianmanderson:master

Conversation

@brianmanderson

Copy link
Copy Markdown

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-Left from 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-Left
at Z = 130.6:

Contour Points XY extent
Main lung outline 346 94.05 mm
Small exclusion #1 4 0.80 mm
Small exclusion #2 4 0.12 mm (1/8 of a voxel)

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() (in
common/clitkDicomRTStruct2ImageFilter.cxx) historically:

  1. Appended every contour from every Z slice into one global vtkPolyData.
  2. Extruded that whole mesh in -Z by mSpacing[2].
  3. Rasterised it with vtkPolyDataToImageStencil and SetTolerance(0).

The combination of (a) cross-slice global mesh, (b) zero-tolerance
stencil, and (c) a near-degenerate input polygon caused
vtkPolyDataToImageStencil to mark the entire mask slice for that Z as
empty, rather than producing the expected lung with a tiny invisible
sub-pixel hole.

A separate, hard-coded +0.5 mm Z offset in DicomRT_Contour::Read()
(introduced in commits a1acb6a4 / fa3343f1, 2017, "Debug RTStruct
offset 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:

  • Group contours by destination mask slice index in Z.
  • For each group, build a polydata containing only that slice's contours,
    translate it by −spacing/2 so the slab will straddle the destination
    voxel centre, then extrude by +spacing and stencil into the mask.
  • Use VTK's default vtkPolyDataToImageStencil tolerance (no more
    SetTolerance(0)) — boundary voxels become deterministic.
  • The even-odd rule still produces holes for nested contours within a
    single Z plane, so the small exclusions are treated correctly.

Side cleanups in the same commit:

  • Remove the hard-coded +0.5 mm Z offset (no longer needed with the
    centred slab, and incorrect for non-3 mm slice spacing).
  • Fix a pre-existing copy-paste bug in
    DicomRT_Contour::UpdateDicomItem() where Y was overwritten with Z
    and Z was never written back to the output buffer.
  • Replace the strict assert(p[2] == mZ) planarity check (a no-op in
    release builds, but flooded stderr in debug) with a tolerant warning
    that snaps off-plane points onto the contour plane.
  • Add DicomRT_ROI::GetListOfContours() so the filter can iterate
    contours 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 Before fix After fix
    87 5003 5003
    88 0 4420
    89 3724 3724

    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-Left contour 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 mm Z hack, fix
    the Y/Z copy-paste bug, soften the planarity assert.
  • common/clitkDicomRT_ROI.h — add GetListOfContours().

brianmanderson and others added 3 commits May 14, 2026 12:37
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
@brianmanderson

Copy link
Copy Markdown
Author

Providing some instructions on where to find the data that I originally identified this bug with below:

Data source

The reproducer dataset is patient LUNG1-002 from the public
NSCLC-Radiomics collection on The Cancer Imaging Archive (TCIA):

  • Collection page: https://www.cancerimagingarchive.net/collection/nsclc-radiomics/
  • License: Creative Commons Attribution-NonCommercial 3.0 (CC BY-NC 3.0).
    Free to redistribute and modify for non-commercial use with attribution.
  • Citation:

    Aerts, H. J. W. L., Wee, L., Rios Velazquez, E., Leijenaar, R. T. H.,
    Parmar, C., Grossmann, P., Carvalho, S., Bussink, J., Monshouwer, R.,
    Haibe-Kains, B., Rietveld, D., Hoebers, F., Rietbergen, M. M., Leemans,
    C. R., Dekker, A., Quackenbush, J., Gillies, R. J., Lambin, P. (2019).
    Data From NSCLC-Radiomics (Version 4) [Data set]. The Cancer Imaging
    Archive. https://doi.org/10.7937/K9/TCIA.2015.PF0M9REI

How to download just LUNG1-002

The whole collection is several hundred GB, but a single patient is ~50 MB.

  1. From the collection page above, click Search/DownloadNBIA
    Search
    . (NBIA is TCIA's image registry; the link normally opens
    https://nbia.cancerimagingarchive.net/nbia-search/ with the collection
    pre-filtered.)
  2. In the search panel set Subject ID = LUNG1-002 and run the search.
  3. Tick the row for the patient and click Add to Cart → Download.
  4. The download is delivered through the NBIA Data Retriever desktop
    tool (Windows / macOS / Linux installers at
    https://wiki.cancerimagingarchive.net/x/2QKPBQ ). Point it at the
    manifest the cart download produced; it pulls one folder containing
    the CT series and the RT Struct.

Alternatively, the same patient can be fetched programmatically via the
TCIA REST API; see
https://wiki.cancerimagingarchive.net/x/fILTB for the endpoints.

Files needed to reproduce

After download the patient directory contains a CT series (~110 axial
slices) plus one DICOM-RT Structure Set. The bug is on ROI Lung-Left,
contour at Z = 130.6 mm (axial slice 88 of the series). No other
files from the collection are required.

@brianmanderson

Copy link
Copy Markdown
Author

Absolutely love vv and I hope this is helpful for the project!

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.

1 participant