Troubleshooting¶
Most problems with RTStructs are silent: the file is valid, it opens in a viewer, and it describes the wrong anatomy. This page is organised by what you see.
Tip
Before anything else, check your version. Eight distinct silent wrong-data bugs were fixed between 2.1.1 and 2.4.0.
import rt_utils; print(rt_utils.__version__)
The ROI is rotated, flipped or transposed¶
Almost always a mask that was not in (Rows, Columns, Slices) order.
Masks are indexed the way pixel_array is read. A mask from a NIfTI file, or
from a tool with its own conventions, probably is not. See
Mask layout.
For NIfTI, use nifti_to_mask(), not a transpose. The
correct rearrangement differs from file to file.
Check it before writing anything:
k = mask.sum(axis=(0, 1)).argmax()
plt.imshow(rtstruct.series_data[k].pixel_array, cmap="gray")
plt.contour(mask[:, :, k], colors="magenta")
If that overlay is wrong, the structure set will be wrong in exactly the same way.
The ROI is on the wrong slices¶
The mask’s axis 2 was not in the series’ spatial order.
series_data is sorted by position along the slice normal. That is not the
order of a directory listing, and not InstanceNumber order, which descends
in plenty of real acquisitions. Build the volume from series_data:
volume = np.stack([s.pixel_array for s in rtstruct.series_data], axis=-1)
Or the whole structure is offset by a constant number of slices.
That is the signature of a reversed slice axis: an ROI at slice k appearing
at N - 1 - k, so the offset changes with series length. Try
mask[:, :, ::-1], and check with the overlay above.
The ROI comes back empty¶
get_roi_mask_by_name returns all zeros, or raises:
None of this ROI's N contour(s) could be matched to a slice of the input series
The series is not the one the structures were drawn on. Each contour names
its image by ReferencedSOPInstanceUID, and none of those images is in the
series you supplied.
Or the images were re-written after the structures were made.
Anonymisation tools regenerate SOPInstanceUIDs, which breaks the link while
leaving the geometry intact. For that case:
mask = rtstruct.get_roi_mask_by_name("GTV", match_by="position")
Read the caveats in How contours find their slice first. Position matching always succeeds, including when it should not.
Note
Before 2.3.0 this returned an array of zeros with no error, indistinguishable from a structure genuinely absent from the patient.
An open contour comes back as a solid blob¶
Fixed in 2.3.0. ContourGeometricType was ignored on read, so an
OPEN_PLANAR path (a brachytherapy channel, a catheter, a vessel centreline)
had the entire area between its two ends filled in. A C-shaped channel of 65
voxels came back as a solid 579.
Upgrade. Closed contours were never affected, which is why organ structures always looked right.
Mask must have the same number of layers as the input series¶
The mask’s slice count does not match the series.
If the number is a whole multiple of the real one, twice or three times,
the directory contains copies of the series, in a backup folder or a nested
export. Fixed in 2.3.0, where copies sharing a SOPInstanceUID are collapsed.
Otherwise the directory holds more than one series, or files that are not part of the stack. Point RT-Utils at a directory containing exactly the one series.
No DICOM Images found in input path¶
The path holds no images, or none RT-Utils recognises as such.
Files without ImageOrientationPatient are skipped: secondary captures,
screenshots, dose reports, structure sets. Those carry no geometry to contour
against.
For files written without the standard preamble or file meta:
RTStructBuilder.create_new(dicom_series_path="./series", force=True)
Note
Before 2.3.0, whether a file was an image was decided by decoding its pixels, so a compressed transfer syntax with no handler installed made a perfectly good series vanish behind this message. Nothing in RT-Utils reads pixel values; presence of the element is now enough.
Mask must have the same in-plane dimensions as the input series¶
The mask is sized for a different series, or is transposed on a non-square
one. The expected shape is (Rows, Columns), the pixel_array layout.
Warning
On a square series this check cannot catch a transpose: 512 × 512
transposed is still 512 × 512. It guards against the wrong series, not the
wrong orientation.
The structure is slightly too small everywhere¶
Half a voxel on every side, which is a large fraction of a small structure: a 3 × 3 voxel region exports at about 44 % of its true area.
That was the default before 2.0.0. Upgrade, or pass the current default explicitly:
rtstruct.add_roi(mask=mask, name="seed", contour_mode="voxel_edge")
See Contour placement.
A single voxel comes back as four, or five¶
As four: a contour tracing voxel boundaries, rasterised with an inclusive fill. Fixed in 2.0.0; contours are now sampled at voxel centres.
As five: a point contour drawn as a radius-1 circle. Fixed in 1.4.0; a
POINT now sets exactly the voxel it names.
Contours are at positions no slice occupies¶
Fixed in 2.3.0. Coordinates came from the first slice’s position plus
index × average_spacing. On an unevenly spaced series (a scout image in the
folder, a varying increment, a partly culled stack) every slice between the
first and the last was written somewhere no slice is. A stack at z = 0, 2, 4,
20 put the z = 2 contour at z = 6.67.
The first and last slices always land correctly, which is what makes this easy to miss in a viewer.
Nothing is printed any more¶
Changed in 2.4.0, deliberately. Diagnostics go to module loggers now, not to stdout. To see them:
import logging
logging.basicConfig(level=logging.INFO)
Or just the library:
logging.getLogger("rt_utils").setLevel(logging.INFO)
A viewer will not draw holes¶
Use use_pin_hole=True when writing. See
Contour placement.
Known limits¶
- A structure set spanning several series.
series_datais one stack andget_roi_mask_by_namereturns one array, so only the part of a structure set belonging to the series you supplied can be read. Load the structure set once per series:for series_path in series_paths: rtstruct = RTStructBuilder.create_from( series_path, "structures.dcm", warn_only=True ) for name in rtstruct.get_roi_names(): try: masks[(series_path, name)] = rtstruct.get_roi_mask_by_name(name) except Exception: pass # this ROI was not drawn on this series
Do not reach for
match_by="position"here. Overlapping series would attract each other’s contours.- A volume stored in a single file.
Some SPECT and enhanced multi-frame objects hold the whole volume in one file. RT-Utils assumes one file per slice.
- Contours outside the imaged volume.
A couch, an applicator or a bolus contoured beyond the image extent has nowhere to rasterise to.
- Copying structures between sets for different series.
Geometry drawn on one series has to be resampled onto another, which RT-Utils does not do.
Still stuck¶
Open an issue at github.com/qurit/rt-utils/issues. What makes a report actionable:
rt_utils.__version__,pydicom.__version__and your Python version;the series’
Rows,Columns,PixelSpacing,ImageOrientationPatientand the number of slices;whether the series is square, and whether the slice spacing is even;
a short script that reproduces it, ideally with a synthetic series rather than patient data.