Skip to content

BUG: Fix RTSS contour-to-surface conversion with volume-based approach - #304

Open
melandur wants to merge 1 commit into
SlicerRt:masterfrom
melandur:fix/rtss-contour-visualization
Open

BUG: Fix RTSS contour-to-surface conversion with volume-based approach#304
melandur wants to merge 1 commit into
SlicerRt:masterfrom
melandur:fix/rtss-contour-visualization

Conversation

@melandur

@melandur melandur commented Feb 13, 2026

Copy link
Copy Markdown

Summary

  • Replaces the linear interpolation volume filling in vtkPlanarContourToClosedSurfaceConversionRule::Convert with a nearest-neighbor approach combined with Gaussian low-pass filtering before marching cubes surface extraction
  • Fixes vertical sheet artifacts, contour overshooting/stretching in Z, and blocky 2D cross-sections when loading DICOM RT Structure Sets
  • Preserves small and single-slice contours by using per-group/per-pixel overlap checks and an additive-only (max) volume smoothing merge

Changes

Volume filling (Step 4)

  • Per-group Z-spacing: connected contour groups extend to meet overlapping neighbors; disconnected/isolated groups get minimal (1 voxel) extension
  • Per-pixel overlap check: only pixels overlapping with an adjacent group receive full Z-extension; spatially disconnected pixels stay thin

Volume smoothing (Step 5)

  • Gaussian low-pass filter applied to the binary volume before surface extraction
  • max(smoothed, original) merge ensures the filter only adds material (fills concavities) and never erodes small contours

Surface extraction (Step 6)

  • Marching cubes on the smoothed volume with minimal mesh cleanup
  • No post-hoc mesh smoothing — surface quality comes from the volume low-pass filter

Test plan

  • Load DICOM RTSS with multiple contour structures of varying sizes
  • Verify large multi-slice structures render as connected 3D surfaces without vertical sheet artifacts
  • Verify small/single-slice contours are preserved and not stretched across multiple slices
  • Verify 2D slice views show clean filled contours without holes or jagged artifacts
  • Verify spatially disconnected contours with the same label are treated independently

Made with Cursor

Add vtkPlanarContourToClosedSurfaceByRasterizationConversionRule as an
alternative to the existing triangulation-based conversion. This rule
rasterizes each contour into a 2D binary slice, builds a 3D volume with
per-group nearest-neighbor fill, applies a Gaussian low-pass filter
(additive-only to preserve small contours), and extracts a closed surface
using marching cubes.

The conversion cost is set to 1000 (higher than the existing rule's 700)
so the triangulation approach remains the default. The rasterization
approach can be useful for complex or highly variable contour data where
direct triangulation produces artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
@melandur
melandur force-pushed the fix/rtss-contour-visualization branch from d1bb423 to b1c4b8c Compare February 13, 2026 16:32
@cpinter

cpinter commented Feb 14, 2026

Copy link
Copy Markdown
Member

After a very quick review I have the following comments:

  • This is not a bug but a feature (ENH)
  • The proposed change does not replace nor fixes anything but adds an alternative conversion path (do not use the words "fix" and "replace" in the description)
  • From the description only I'd have said that this change should not be integrated, but seeing from the code that it is actually an additional conversion rule with a higher cost than the default (and thoroughly tested) one, I'm not against it
  • Please include as many and relevant comparison schreenshots as possible, otherwise it is hard to judge how this is better

Thank you very much!

@lassoan

lassoan commented Feb 14, 2026

Copy link
Copy Markdown
Member

Yes, please show screenshots of how it converts SlicerRT sample data sets and some other data sets. Highlight differences (good and bad) compared to the current method. Thank you!

@melandur

Copy link
Copy Markdown
Author

Visual Comparison: Standard Triangulation vs Rasterization Rule

Below are side-by-side comparisons across 4 different DICOM-RT cases, showing both the existing triangulation rule (cost: 700) and the new rasterization rule (cost: 1000). Each case shows the planar contour input, the 3D closed surface output, and 2D slice overlays from multiple viewpoints.

Legend:

  • Standard = existing vtkPlanarContourToClosedSurfaceConversionRule (triangulation-based)
  • Rasterization = new vtkPlanarContourToClosedSurfaceByRasterizationConversionRule (volume-based)

Case 1 — Large multi-segment structure (Position 1)

View Standard (triangulation) Rasterization (new)
2D contours case_1_pos_1_2d_standard case_1_pos_1_2d_new
3D surface case_1_pos_1_3d_standard case_1_pos_1_3d_new

Notes:

  • The triangulation rule produces a smoother 3D surface but shows contour fill bleeding into adjacent slices in the coronal and sagittal views. The rasterization rule confines each segment cleanly to its actual slice extent, producing a more voxelized but geometrically accurate surface.

Case 1 — Large multi-segment structure (Position 2)

View Standard (triangulation) Rasterization (new)
2D contours case_1_pos_2_2d_standard case_1_pos_2_2d_new
3D surface case_1_pos_2_3d_standard case_1_pos_2_3d_new

Notes:

  • Same case scrolled to a different axial position. The triangulation rule shows smooth surfaces but with visible overshooting in the coronal view where disconnected contour parts get bridged. The rasterization rule keeps each contour group independent, avoiding inter-group stretching.

Case 2 — Smaller structure with fewer slices

View Standard (triangulation) Rasterization (new)
2D contours case_2_pos_1_2d_standard case_2_pos_1_2d_new
3D surface case_2_pos_1_3d_standard case_2_pos_1_3d_new

Notes:

  • With fewer slices, the triangulation rule still produces a reasonable surface but with some inter-slice bridging artifacts. The rasterization rule produces a compact, voxelized representation that stays faithful to the contour boundaries on each slice.

Case 3 — Elongated multi-segment structure

View Standard (triangulation) Rasterization (new)
2D contours case_3_pos_1_2d_standard case_3_pos_1_2d_new
3D surface case_3_pos_1_3d_standard case_3_pos_1_3d_new

Notes:

  • The triangulation rule stretches contours vertically between distant slices, creating elongated sheet-like artifacts in the 3D view. The rasterization rule treats each contour group independently, so the elongated structure is built slice-by-slice without artificial vertical bridging.

Case 4 — Small single-segment structure (few slices)

View Standard (triangulation) Rasterization (new)
2D contours case_4_pos_1_2d_standard case_4_pos_1_2d_new
3D surface case_4_pos_1_3d_standard case_4_pos_1_3d_new

Notes:

  • A small structure spanning only a few slices. The triangulation rule produces a smooth closed surface with visible contour line artifacts on the mesh. The rasterization rule generates a compact voxelized surface that accurately represents the small extent without stretching or overshooting.

Summary of Differences

Aspect Standard (triangulation) Rasterization (new)
Surface quality Smooth inter-slice interpolation Voxelized/blocky appearance with Gaussian smoothing
Artifact handling Can produce vertical sheet artifacts and overshooting between disconnected contour groups No inter-slice triangulation artifacts; disconnected groups treated independently
Small contours May stretch single-slice contours across multiple slices Single-slice contours preserved as thin discs (1 voxel Z-extension)
Approach Direct triangulation between contour slices (lossless) Rasterize to binary volume, Gaussian low-pass filter, marching cubes extraction

@lassoan

lassoan commented Feb 16, 2026

Copy link
Copy Markdown
Member

How do you decide which contours to connect?

@lassoan

lassoan commented Feb 16, 2026

Copy link
Copy Markdown
Member

How do you handle internal holes? Do you support:

  • keyhole technique (classic)
  • odd/even count of nested contours
  • cw/ccw polygon winding?

@melandur

Copy link
Copy Markdown
Author

How do you decide which contours to connect?

The rasterization does not explicitly connect contours between slices (like the standard), instead it uses a volume-based approach to determine connectivity:

  1. Z-grouping by proximity: Contours are grouped by Z-value (0.05mm tolerance). All contours on the same slice are rasterized together into a single 2D binary fill.
  2. Per-pixel XY overlap between adjacent groups: For each pair of consecutive Z-groups, the algorithm checks whether their rasterized fills share any common pixels in XY. This determines whether two adjacent slices are connected:
  • Connected (XY overlap exists): The contour fill extends halfway toward the neighboring group in Z, so they meet in the middle and marching cubes produces a continuous surface.

  • Disconnected (no XY overlap): The contour fill extends only 1 voxel in Z, keeping it as a thin disc. This prevents unrelated contour groups from being artificially bridged.

Per-pixel refinement: Even within a connected pair, individual pixels that don't overlap with the adjacent group's fill only get 1-voxel extension. This prevents parts of a contour that are spatially isolated from stretching, even if other parts of the same slice are connected.

The surface itself emerges implicitly from marching cubes on the resulting 3D binary volume — there's no explicit contour-to-contour matching or triangulation.

How do you handle internal holes? Do you support:

keyhole technique (classic)
Fully supported. If the input data contains keyhole contours (a thin bridge connecting an inner and outer contour into a single polygon), the rasterization handles it. The keyhole bridge is typically sub-pixel width, so the scanline fill produces the same result as if the inner and outer contours were separate polygons -- the hole is preserved.

odd/even count of nested contours
Fully supported. An outer contour fills, a nested inner contour creates a hole, a third nested contour fills again, etc. The algorithm doesn't need to know how many levels of nesting exist, it just counts boundary crossings.

cw/ccw polygon winding?
Fully supported. A hole is created regardless of whether the inner contour is CW or CCW relative to the outer contour. CW and CCW contours produce identical rasterized output. No winding detection or correction is performed, it just counts boundary crossings.

@lassoan

lassoan commented Feb 16, 2026

Copy link
Copy Markdown
Member

cw/ccw polygon winding?
Fully supported. A hole is created regardless of whether the inner contour is CW or CCW relative to the outer contour. CW and CCW contours produce identical rasterized output. No winding detection or correction is performed, it just counts boundary crossings.

"A hole is created regardless of whether the inner contour is CW or CCW relative to the outer contour" means that it is actually not supported.

CW/CCW is more robust to numerical errors than odd/even counting. Numerical errors are not theoretical but very frequently occur: vtkContourTriangulator does just odd/even counting and in all complex segmentations (for example, those that are created by thresholding a volume) there are dozens of errors in every segmentations. In practice, both techniques are used, but as far as I remember, the DICOM standard only allows odd/even counting.

It is OK to have limitations, but they need to be clearly documented.

The rasterization does not explicitly connect contours between slices (like the standard), instead it uses a volume-based approach to determine connectivity

It means it does not follow the DICOM standard then. Again, it is OK to have limitations, but they need to be clearly documented.

DICOM standard describes contour groups, so in theory you can encode in RTSTRUCT which contours needs to be connected to each other, but we have not implemented it because in the data sets that we got contour groups were not used. Do the data sets you have contain contour groups?


What software created the contours that you are working with? It would be useful to document that for a certain software it is recommended to use the rasterization based conversion.

Another general comment I have is to remove the closed surface generation step. The conversion you implemented converts from parallel contours to binary labelmap with reasonable sophistication. However the labelmap to closed surface conversion is very basic, has several issues (very inefficient, hardcoded Gaussian smoothing stdev, not optimal contouring filter), and if the segment is further edited then it is not nice that we need to convert back to binary labelmap again. It may be better to update your conversion rule to only include parallel contours to binary labelmap conversion and if you think your approach has some advantages compared to https://github.com/Slicer/Slicer/blob/main/Libs/vtkSegmentationCore/vtkBinaryLabelmapToClosedSurfaceConversionRule.cxx then add new alternative conversion rule for that, too. The only reason I can think of why a combine rule can be useful is if you have hundreds of overlapping structures then dropping the intermediate binary labelmap representation can save memory (are your data sets like that?); but for robust slice rendering you may need the binary labelmap representation anyway.

As a very high-level comment, the effort we put into improving supporting parallel contours should be limited (focused on relieving specific pressure points instead of trying to implement general solutions), as segmentations today are generated by AI models and they are all labelmap based. Our efforts should be rather spent on improving support of DICOM SEG instead of the inherently limited, fragile, and complex DICOM RTSTRUCT.

@melandur

Copy link
Copy Markdown
Author

Thank you for the detailed and constructive feedback. The effort is appreciated.


CW/CCW winding?
"A hole is created regardless of whether the inner contour is CW or CCW relative to the outer contour" means that it is actually not supported. CW/CCW is more robust to numerical errors than odd/even counting.

You're right, I should not have framed the even-odd behavior as "fully supported."


It means it does not follow the DICOM standard then. Again, it is OK to have limitations, but they need to be clearly documented.

Acknowledged. The rasterization approach does not use DICOM contour groups to determine inter-slice connectivity. Instead, it infers connectivity spatially via per-pixel XY overlap between adjacent Z-slices. This deviation from the standard needs to be documented


Do the data sets you have contain contour groups?

  • Only standard fields were used
    (ContourGeometricType, NumberOfContourPoints, ContourData, ContourImageSequence, ContourNumber)

  • All contours are CLOSED_PLANAR type

  • All ROIs use ROIGenerationAlgorithm = AUTOMATIC

The contours are stored as flat sequences per ROI without any inter-contour connectivity information.


What software created the contours that you are working with?

The contours were generated by PyRaDiSe, an open-source Python package for DICOM-RT-based auto-segmentation pipeline construction and DICOM-RT data conversion. The RTSS files are created from AI-generated segmentation masks.

The generated RTSS files work correctly with treatment planning systems (TPS) and render properly in Weasis DICOM Viewer, which if I'm not mistaken, uses a pure slice-based 2D approach rather than 3D surface reconstruction.


Remove the closed surface generation step [...] It may be better to update your conversion rule to only include parallel contours to binary labelmap conversion

That sound reasonable to me.


The effort we put into improving supporting parallel contours should be limited [...] segmentations today are generated by AI models and they are all labelmap based.

Fully agreed, I'm not a fan of RTSS as a format either. Unfortunately, RTSS remains widely used in clinical radiotherapy workflows, and clinicians do pull up SlicerRT to review structures. When the artifacts from the 3D visualization appears broken in the 2d view, it undermines confidence in the segmentation regardless of whether the underlying data is correct. This contribution is intended as a focused, narrow alternative for that current situation, not a general solution or replacement for the existing rule.

@cpinter

cpinter commented Mar 3, 2026

Copy link
Copy Markdown
Member

I have not had time yet to fully process the details but only to see the screenshots. Please note, that these problems you see in the 2D views
image
are visualization artifacts that appear only in 2D slice views. If you create the binary labelmap representation from the closed surface then show that representation (Segmentations module / Display / Advanced) then they do not appear.

So other than these it is not obvious to me how the proposed algorithm is an improvement, as the closed surfaces in 3D generally look better to me (reality is never block-like, the interpolation that the current approach does makes the resulting closed surface more faithful to the clinical reality based on the limited planar contours representation of DICOM RTSS).
Again I haven't yet processed the rest of the conversation but wanted to bring attention to this visualization issue.

@ferdymercury

Copy link
Copy Markdown
Contributor

Thanks a lot Michael for this PR!

The conversion you implemented converts from parallel contours to binary labelmap with reasonable sophistication.

I am very interested since it aligns very well with this question of mine https://discourse.slicer.org/t/vtkplanarcontourtobinarylabelmapconversionrule/47507/1

Reading through

Please note, that these problems you see in the 2D views
are visualization artifacts that appear only in 2D slice views. If you create the binary labelmap representation from the closed surface then show that representation (Segmentations module / Display / Advanced) then they do not appear.

plus

When the artifacts from the 3D visualization appears broken in the 2d view, it undermines confidence in the segmentation regardless of whether the underlying data is correct.

I can only share the observation that the frequency of appearance of these artefacts is indeed worrying when loading RTstructs: most people and radiotherapists only look at the 2D slices, not at the 3D view.
From my point of view, my suggestion for a short-term solution would be to switch the standard rule for RTstructs to planar-contour-to-ribbon-to-binary-labelmap, rather than planar-to-closed-surface as of now. That way we'd see much less artefacts.

So other than these it is not obvious to me how the proposed algorithm is an improvement, as the closed surfaces in 3D generally look better to me (reality is never block-like, the interpolation that the current approach does makes the resulting closed surface more faithful to the clinical reality based on the limited planar contours representation of DICOM RTSS).

I guess there is a perspective conflict / compromise here. The current 3D viewing looks better in SlicerRT, whereas the 2D viewing looks better in this PR (SlicerRT has lots of artefacts). From my point of view, I prefer 2D without artefacts and pixelated blocky 3D surfaces rather than perfect 3D smoothy surfaces and artefact-full 2D contours. But I understand that depending on the intended audience one or the other might be beneficial.

Maybe... a dropdown button in the DICOM import Dialog to use one or the other rule as default when importing could make everyone happy?

@cpinter

cpinter commented Jul 6, 2026

Copy link
Copy Markdown
Member

I didn't get an answer about the artifacts, so they may or may not be the vtkCutter issue we discussed recently with @ferdymercury
If so, a very simple solution would be to switch to labelmap display by default in 2D and keep closed surface in 3D.

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.

4 participants