Skip to content

Commit a102298

Browse files
authored
Merge pull request #536 from QIICR/Ft_itk2label
Add direct ITK to DICOM Labelmap conversion
2 parents 8b161f9 + 830bdd8 commit a102298

13 files changed

Lines changed: 754 additions & 50 deletions

CMakeExternals/DCMTK.cmake

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ if(NOT DEFINED DCMTK_DIR AND NOT ${CMAKE_PROJECT_NAME}_USE_SYSTEM_${proj})
4747

4848
ExternalProject_SetIfNotDefined(
4949
${proj}_GIT_TAG
50-
# DCMTK 3.7.0 ++ (December 2025).
50+
# DCMTK 3.7.0 ++ (May 8th, 2026).
5151
# This is DCMTK version with some extra fixes on segmentations and parametric maps,
52-
# which have been released a few days after DCMTK 3.7.0.
53-
"3e85b37444107e93550167c2284e64b4881b0fcb"
52+
# which have been released a few days after DCMTK 3.7.0, as well as performance
53+
# optimizations for segmentation/parametric map. It also includes a simpler
54+
# mechanism to add external modules to the DCMTK CMake build.
55+
"2dd54ca1c28100b820ccf1383a0948e889246f96"
5456
QUIET
5557
)
5658

apps/seg/Testing/CMakeLists.txt

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,68 @@ dcmqi_add_test(
3030
--outputDICOM ${MODULE_TEMP_DIR}/liver.dcm
3131
)
3232

33+
dcmqi_add_test(
34+
NAME ${itk2dcm}_makeSEG_labelmap
35+
MODULE_NAME ${MODULE_NAME}
36+
COMMAND $<TARGET_FILE:${itk2dcm}>
37+
--inputMetadata ${CMAKE_SOURCE_DIR}/doc/examples/seg-example.json
38+
--inputImageList ${BASELINE}/liver_seg.nrrd
39+
--inputDICOMDirectory ${DICOM_DIR}
40+
--outputDICOM ${MODULE_TEMP_DIR}/liver-labelmap-direct.dcm
41+
--segmentationType labelmap
42+
)
43+
44+
# Multi-segment labelmap from two non-overlapping single-segment ITK inputs.
45+
# Verifies that segment numbering for foreground starts at 1 (regression guard
46+
# for the bug where the first foreground segment got number 0 and collided
47+
# with the implicit background pixel value).
48+
dcmqi_add_test(
49+
NAME ${itk2dcm}_makeSEG_labelmap_multi
50+
MODULE_NAME ${MODULE_NAME}
51+
COMMAND $<TARGET_FILE:${itk2dcm}>
52+
--inputMetadata ${CMAKE_SOURCE_DIR}/doc/examples/seg-example_liver_spine.json
53+
--inputImageList ${BASELINE}/liver_seg.nrrd,${BASELINE}/spine_seg.nrrd
54+
--inputDICOMList ${DICOM_DIR}/01.dcm,${DICOM_DIR}/02.dcm,${DICOM_DIR}/03.dcm
55+
--outputDICOM ${MODULE_TEMP_DIR}/liver_spine-labelmap.dcm
56+
--segmentationType labelmap
57+
)
58+
59+
# Same as above but exercises --useLabelIDAsSegmentNumber together with
60+
# --segmentationType labelmap.
61+
dcmqi_add_test(
62+
NAME ${itk2dcm}_makeSEG_labelmap_multi_labelID
63+
MODULE_NAME ${MODULE_NAME}
64+
COMMAND $<TARGET_FILE:${itk2dcm}>
65+
--inputMetadata ${CMAKE_SOURCE_DIR}/doc/examples/seg-example_liver_spine.json
66+
--inputImageList ${BASELINE}/liver_seg.nrrd,${BASELINE}/spine_seg.nrrd
67+
--inputDICOMList ${DICOM_DIR}/01.dcm,${DICOM_DIR}/02.dcm,${DICOM_DIR}/03.dcm
68+
--outputDICOM ${MODULE_TEMP_DIR}/liver_spine-labelmap-labelID.dcm
69+
--segmentationType labelmap
70+
--useLabelIDAsSegmentNumber
71+
)
72+
73+
# Overlap rejection: liver+spine+heart input. Of the three segments only
74+
# liver and heart actually overlap (522 pixels, verified by pixel-level
75+
# intersection of the input NRRDs); spine does not overlap with either.
76+
# In labelmap mode the converter must abort because labelmaps cannot
77+
# represent overlapping segments — each pixel value can encode only one
78+
# segment number. The expected error is
79+
# "Cannot write labelmap SEG due to overlapping segments at slice ..."
80+
# Reuses the existing 3-segment metadata so no new JSON file is needed.
81+
# This is the ITK->labelmap counterpart of partial_overlaps_to_labelmap,
82+
# which exercises the same rejection in the DICOM->labelmap path.
83+
dcmqi_add_test(
84+
NAME ${itk2dcm}_makeSEG_labelmap_overlap
85+
MODULE_NAME ${MODULE_NAME}
86+
COMMAND $<TARGET_FILE:${itk2dcm}>
87+
--inputMetadata ${CMAKE_SOURCE_DIR}/doc/examples/seg-example_multiple_segments.json
88+
--inputImageList ${BASELINE}/liver_seg.nrrd,${BASELINE}/spine_seg.nrrd,${BASELINE}/heart_seg.nrrd
89+
--inputDICOMList ${DICOM_DIR}/01.dcm,${DICOM_DIR}/02.dcm,${DICOM_DIR}/03.dcm
90+
--outputDICOM ${MODULE_TEMP_DIR}/liver_spine_heart-labelmap.dcm
91+
--segmentationType labelmap
92+
)
93+
set_tests_properties(${itk2dcm}_makeSEG_labelmap_overlap PROPERTIES WILL_FAIL TRUE)
94+
3395

3496
# ------------------------------------------------------------------------------
3597

@@ -115,6 +177,16 @@ if(EXISTS ${DCIODVFY_EXECUTABLE})
115177
TEST_DEPENDS
116178
${itk2dcm}_makeSEG_multiple_segment_files_reordered
117179
)
180+
# Note: dciodvfy is intentionally not run on the labelmap outputs produced
181+
# by ${itk2dcm}_makeSEG_labelmap{,_multi,_multi_labelID} or by the
182+
# bin2labelsegimage labelmap producers. All labelmap segmentations emitted
183+
# by dcmqi today fail dciodvfy with
184+
# Error - Missing attribute Type 2C Conditional Element=<PatientOrientation>
185+
# Module=<GeneralImage>
186+
# which is a pre-existing compliance gap shared by every labelmap-emitting
187+
# path (Itk2DicomConverter and Bin2Label). Once Patient Orientation is
188+
# populated for labelmap output, add dciodvfy invocations for
189+
# liver-labelmap-direct.dcm and liver_spine-labelmap.dcm here.
118190
else()
119191
message(STATUS "Skipping test '${itk2dcm}_dciodvfy': dciodvfy executable not found")
120192
endif()
@@ -147,6 +219,39 @@ dcmqi_add_test(
147219
${itk2dcm}_makeSEG
148220
)
149221

222+
dcmqi_add_test(
223+
NAME ${dcm2itk}_makeNRRD_from_labelmap_direct
224+
MODULE_NAME ${MODULE_NAME}
225+
COMMAND $<TARGET_FILE:${dcm2itk}Test>
226+
--compare ${BASELINE}/liver_seg.nrrd
227+
${MODULE_TEMP_DIR}/makeNRRD_labelmap_direct-1.nrrd
228+
${dcm2itk}Test
229+
--inputDICOM ${MODULE_TEMP_DIR}/liver-labelmap-direct.dcm
230+
--outputDirectory ${MODULE_TEMP_DIR}
231+
--outputType nrrd
232+
--prefix makeNRRD_labelmap_direct
233+
TEST_DEPENDS
234+
${itk2dcm}_makeSEG_labelmap
235+
)
236+
237+
# Roundtrip: multi-segment labelmap -> NRRD, compared against the merged
238+
# liver+spine baseline (two non-overlapping segments collapse into a single
239+
# output ITK image with labels 0/1/2).
240+
dcmqi_add_test(
241+
NAME ${dcm2itk}_makeNRRD_from_labelmap_multi
242+
MODULE_NAME ${MODULE_NAME}
243+
COMMAND $<TARGET_FILE:${dcm2itk}Test>
244+
--compare ${BASELINE}/liver_spine_seg.nrrd
245+
${MODULE_TEMP_DIR}/makeNRRD_labelmap_multi-1.nrrd
246+
${dcm2itk}Test
247+
--inputDICOM ${MODULE_TEMP_DIR}/liver_spine-labelmap.dcm
248+
--outputDirectory ${MODULE_TEMP_DIR}
249+
--outputType nrrd
250+
--prefix makeNRRD_labelmap_multi
251+
TEST_DEPENDS
252+
${itk2dcm}_makeSEG_labelmap_multi
253+
)
254+
150255
dcmqi_add_test(
151256
NAME ${dcm2itk}_makeNRRD_multiple_segment_files
152257
MODULE_NAME ${MODULE_NAME}

apps/seg/itkimage2segimage.cxx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ int main(int argc, char *argv[])
8989
}
9090
}
9191

92+
bool outputLabelMap = false;
93+
if (segmentationType == "binary")
94+
outputLabelMap = false;
95+
else if (segmentationType == "labelmap")
96+
outputLabelMap = true;
97+
else {
98+
cerr << "Error: --segmentationType must be either 'binary' or 'labelmap'" << endl;
99+
return EXIT_FAILURE;
100+
}
101+
92102
if(metaRoot.isMember("segmentAttributesFileMapping")){
93103
if(metaRoot["segmentAttributesFileMapping"].size() != metaRoot["segmentAttributes"].size()){
94104
cerr << "Number of files in segmentAttributesFileMapping should match the number of entries in segmentAttributes!" << endl;
@@ -128,7 +138,8 @@ int main(int argc, char *argv[])
128138
skipEmptySlices,
129139
useLabelIDAsSegmentNumber,
130140
referencesGeometryCheck,
131-
!noDicomValueChecks);
141+
!noDicomValueChecks,
142+
outputLabelMap);
132143

133144
if (result == NULL){
134145
std::cerr << "ERROR: Conversion failed." << std::endl;

apps/seg/itkimage2segimage.xml

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,21 +74,22 @@
7474
</integer>
7575

7676
<boolean>
77-
<name>useLabelIDAsSegmentNumber</name>
78-
<label>Use label ID as Segment Number</label>
79-
<channel>output</channel>
80-
<longflag>useLabelIDAsSegmentNumber</longflag>
77+
<name>noDicomValueChecks</name>
78+
<label>No DICOM Value Check</label>
79+
<channel>input</channel>
80+
<longflag>noDicomValueChecks</longflag>
8181
<default>false</default>
82-
<description>Use label IDs from ITK images as Segment Numbers in DICOM. Only works if label IDs are consecutively numbered starting from 1, otherwise conversion will fail.</description>
82+
<description>Bypass DICOM value checks when writing segmentation file. This can be useful for
83+
debugging or when taking over attributes from non-compliant DICOM image files.</description>
8384
</boolean>
8485

8586
<boolean>
86-
<name>noDicomValueChecks</name>
87-
<label>No DICOM Value Check</label>
87+
<name>useLabelIDAsSegmentNumber</name>
88+
<label>Use label ID as Segment Number</label>
8889
<channel>output</channel>
89-
<longflag>noDicomValueChecks</longflag>
90+
<longflag>useLabelIDAsSegmentNumber</longflag>
9091
<default>false</default>
91-
<description>Bypass DICOM value checks when writing segmentation file. This can be useful for debugging or when taking over attributes from non-compliant DICOM image files.</description>
92+
<description>Use label IDs from ITK images as Segment Numbers in DICOM. Only works if label IDs are consecutively numbered starting from 1, otherwise conversion will fail.</description>
9293
</boolean>
9394

9495
<boolean>
@@ -100,6 +101,17 @@
100101
<description>Display more verbose output, useful for troubleshooting.</description>
101102
</boolean>
102103

104+
<string-enumeration>
105+
<name>segmentationType</name>
106+
<label>DICOM Segmentation Type</label>
107+
<channel>output</channel>
108+
<longflag>segmentationType</longflag>
109+
<default>binary</default>
110+
<element>binary</element>
111+
<element>labelmap</element>
112+
<description>Type of DICOM SEG object to create. Use binary for classic 1-bit segments (default), or labelmap for direct labelmap SEG output.</description>
113+
</string-enumeration>
114+
103115
<string-enumeration>
104116
<name>compress</name>
105117
<label>Compress PixelData</label>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"@schema": "https://raw.githubusercontent.com/qiicr/dcmqi/master/doc/schemas/seg-schema.json#",
3+
4+
"ContentCreatorName": "Doe^John",
5+
"ClinicalTrialSeriesID": "Session1",
6+
"ClinicalTrialTimePointID": "1",
7+
"ClinicalTrialCoordinatingCenterName": "BWH",
8+
"SeriesDescription": "Segmentation",
9+
"SeriesNumber": "300",
10+
"InstanceNumber": "1",
11+
12+
"segmentAttributes": [
13+
[
14+
{
15+
"labelID": 1,
16+
"SegmentDescription": "Liver Segmentation",
17+
"SegmentLabel": "Liver",
18+
"SegmentedPropertyCategoryCodeSequence": {
19+
"CodeValue": "85756007",
20+
"CodingSchemeDesignator": "SCT",
21+
"CodeMeaning": "Tissue"
22+
},
23+
"SegmentedPropertyTypeCodeSequence": {
24+
"CodeValue": "10200004",
25+
"CodingSchemeDesignator": "SCT",
26+
"CodeMeaning": "Liver"
27+
},
28+
"SegmentAlgorithmType": "SEMIAUTOMATIC",
29+
"SegmentAlgorithmName": "SlicerEditor",
30+
"recommendedDisplayRGBValue": [
31+
220,
32+
129,
33+
101
34+
]
35+
}
36+
],
37+
[
38+
{
39+
"labelID": 2,
40+
"SegmentDescription": "Anatomical Structure",
41+
"SegmentLabel": "Thoracic spine",
42+
"SegmentedPropertyTypeCodeSequence": {
43+
"CodeMeaning": "Thoracic spine",
44+
"CodingSchemeDesignator": "SCT",
45+
"CodeValue": "122495006"
46+
},
47+
"SegmentedPropertyCategoryCodeSequence": {
48+
"CodeMeaning": "Anatomical Structure",
49+
"CodingSchemeDesignator": "SCT",
50+
"CodeValue": "123037004"
51+
},
52+
"SegmentAlgorithmType": "MANUAL",
53+
"recommendedDisplayRGBValue": [
54+
226,
55+
202,
56+
134
57+
]
58+
}
59+
]
60+
]
61+
}

include/dcmqi/Bin2Label.h

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,22 @@ class DcmBinToLabelConverter
151151
static OFCondition copyCommonModules(DcmSegmentation* src, DcmSegmentation* dest);
152152
OFCondition createFramesWithMetadata(DcmSegmentation* src);
153153
OFCondition copySegments(DcmSegmentation* src, DcmSegmentation* dest);
154-
OFBool checkCIELabColorsPresent();
154+
155+
/** Collect Recommended Display CIELab Value of each input segment into
156+
* m_cielabColors so it can later be used to build the Palette Color LUT.
157+
* Only relevant when the conversion target color model is PALETTE; for
158+
* MONOCHROME2 output the call is a no-op and returns OFTrue.
159+
*
160+
* In PALETTE mode every input segment must either provide a Recommended
161+
* Display CIELab Value Macro or m_convFlags.m_forcePalette must be set so
162+
* that random colors are generated for missing segments. Otherwise this
163+
* method clears m_cielabColors and returns OFFalse.
164+
*
165+
* @return OFTrue on success or no-op (non-PALETTE mode), OFFalse if a
166+
* required color is missing and m_forcePalette is not set, or on
167+
* memory allocation failure.
168+
*/
169+
OFBool ensureCIELabColorsPresent();
155170
OFCondition createPaletteColorLUT();
156171
OFCondition loadInput();
157172
OFCondition addSourceSegmentationToDerivationImageFG(DcmSegmentation* src, DcmSegmentation* dest);
@@ -221,6 +236,24 @@ class DcmBinToLabelConverter
221236

222237
OFCondition createFrameContentFG(Uint32 outputFrameNum, OFVector<OverlapUtil::LogicalFrame>::iterator logicalFrame, FGFrameContent*& frameContent);
223238

239+
/** Check whether any frame in the output segmentation contains pixel value 0.
240+
* If so, add a background segment with Segment Number 0 using Property Type
241+
* Code (DCM, 125040, "Background").
242+
* The alternative would be to use Pixel Padding Value which is also
243+
* foreseen for Labelmaps but is expected, for now, that the background
244+
* segment is better supported by consuming applications.
245+
*
246+
* Implementation delegates the frame scan and segment creation to
247+
* ConverterBase::addBackgroundSegmentIfNeeded(); this wrapper additionally
248+
* prepends the matching black entry to m_cielabColors when the output
249+
* color model is PALETTE so that pixel value 0 maps to black in the
250+
* Palette Color LUT (the per-segment CIELab macro is not written in
251+
* PALETTE mode, per Sup 243).
252+
*
253+
* @return EC_Normal if successful or no background segment needed, error otherwise
254+
*/
255+
OFCondition addBackgroundSegmentIfNeeded();
256+
224257
private:
225258

226259
// Disable copy constructor and assignment operator
@@ -268,6 +301,41 @@ class DcmBinToLabelConverter
268301
return OFTrue;
269302
}
270303

304+
/** Prepend a color entry at index 0, shifting existing entries right.
305+
* After this call m_numSegments is incremented by 1 and the values
306+
* (L, a, b) occupy index 0; previous index i is now at i+1. Used to
307+
* align the Palette Color LUT with a newly added background segment
308+
* (Segment Number 0) so that pixel value 0 maps to the prepended
309+
* color in the LUT.
310+
* @param L L* component, DICOM-encoded (0..65535 maps to 0..100).
311+
* @param a a* component, DICOM-encoded (0..65535 maps to -128..127).
312+
* @param b b* component, DICOM-encoded (0..65535 maps to -128..127).
313+
* @return OFTrue on success, OFFalse on memory allocation failure.
314+
*/
315+
OFBool prepend(Uint16 L, Uint16 a, Uint16 b)
316+
{
317+
size_t newSize = m_numSegments + 1;
318+
Uint16* newL = new Uint16[newSize];
319+
Uint16* newA = new Uint16[newSize];
320+
Uint16* newB = new Uint16[newSize];
321+
if (!newL || !newA || !newB)
322+
{
323+
delete[] newL; delete[] newA; delete[] newB;
324+
return OFFalse;
325+
}
326+
newL[0] = L; newA[0] = a; newB[0] = b;
327+
for (size_t i = 0; i < m_numSegments; i++)
328+
{
329+
newL[i + 1] = m_L[i];
330+
newA[i + 1] = m_a[i];
331+
newB[i + 1] = m_b[i];
332+
}
333+
delete[] m_L; delete[] m_a; delete[] m_b;
334+
m_L = newL; m_a = newA; m_b = newB;
335+
m_numSegments = newSize;
336+
return OFTrue;
337+
}
338+
271339
~CIELabColor()
272340
{
273341
clear();

0 commit comments

Comments
 (0)