Skip to content

Commit

Permalink
Use Label IDs as Segment Numbers.
Browse files Browse the repository at this point in the history
When converting from ITK to DICOM, allow to use Label IDs as Segment
Numbers so that even if reading from various NRRD inputs, the Segments
in the DICOM file will still use the Label IDs from the NRRD files.

This is particularly helpful for testing since it allows a roundtrip test
from DICOM -> multiple NRRD files using potentially multiple segments
each -> back to DICOM. If no mapping to the Label IDs takes place, the
converter will use ascending Segment Numbers, so the order will depend
on the read order, which again usually depends on the order in the JSON
meta information.

This sorting behavior is disabled by default. It is available as
cli option (--sortByLabelID) which is handed to the library through the
Itk2DicomConverter::itkimage2dcmSegmentation() call.
  • Loading branch information
michaelonken committed Dec 8, 2023
1 parent cee108c commit 08a5ebd
Show file tree
Hide file tree
Showing 10 changed files with 354 additions and 19 deletions.
86 changes: 85 additions & 1 deletion apps/seg/Testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,40 @@ dcmqi_add_test(
--outputDICOM ${MODULE_TEMP_DIR}/liver.dcm
)


# ------------------------------------------------------------------------------

# Creates a DICOM segmentation with 5 segments, partially overlapping:
# - Segments GREEN:
# - overlaps with ORANGE on same frame
# - overlaps with PURPLE on next frame
# - Segment ORANGE:
# - overlaps with GREEN on same frame
# - overlaps with PURPLE on next frame
# - Segment PURPLE:
# - overlaps with GREEN on previous frame
# - overlaps with ORANGE on previous frame
# - SEGMENT LIGHT_BLUE:
# - no overlaps at all
# - SEGMENT DARK_BLUE:
# - no overlaps at all
# This will produce 3 groups (i.e NRRD files):
# - Group 1: GREEN, LIGHT_BLUE, DARK_BLUE
# - Group 2: ORANGE
# - Group 3: PURPLE
dcmqi_add_test(
NAME ${itk2dcm}_makeSEG_merged_segment_files_from_partial_overlap
MODULE_NAME ${MODULE_NAME}
COMMAND $<TARGET_FILE:${itk2dcm}>
--inputMetadata ${CMAKE_SOURCE_DIR}/doc/examples/seg-example_partial_overlaps.json
--inputImageList ${BASELINE}/partial_overlaps-1.nrrd,${BASELINE}/partial_overlaps-2.nrrd,${BASELINE}/partial_overlaps-3.nrrd
--inputDICOMList ${DICOM_DIR}/01.dcm,${DICOM_DIR}/02.dcm,${DICOM_DIR}/03.dcm
--outputDICOM ${MODULE_TEMP_DIR}/partial_overlaps.dcm
--sortByLabelID
)

# ------------------------------------------------------------------------------

# Creates a DICOM segmentation file that has 3 segments:
# - segment for liver (DICOM Segment Number 1)
# - segment for spine (DICOM Segment Number 2)
Expand All @@ -54,7 +88,6 @@ dcmqi_add_test(
--outputDICOM ${MODULE_TEMP_DIR}/liver_heart_seg_reordered.dcm
)


find_program(DCIODVFY_EXECUTABLE dciodvfy)

if(EXISTS ${DCIODVFY_EXECUTABLE})
Expand Down Expand Up @@ -144,6 +177,8 @@ dcmqi_add_test(
${itk2dcm}_makeSEG_multiple_segment_files_reordered
)

# ------------------------------------------------------------------------------

# Reads a DICOM segmentation file that has 3 segments (liver, spine, heart - in this order).
# Heart and liver segments overlap.
# The goal is to export these segments to NRRD+JSON. Since the liver and heart segments overlap,
Expand Down Expand Up @@ -176,6 +211,55 @@ dcmqi_add_test(
${dcm2itk}_makeNRRD_merged_segment_file
)

# ------------------------------------------------------------------------------


# Reads a DICOM segmentation file (partial_overlaps.dcm) that has 5 segments,
# - Segments GREEN:
# - overlaps with ORANGE on same frame
# - overlaps with PURPLE on next frame
# - Segment ORANGE:
# - overlaps with GREEN on same frame
# - overlaps with PURPLE on next frame
# - Segment PURPLE:
# - overlaps with GREEN on previous frame
# - overlaps with ORANGE on previous frame
# - SEGMENT LIGHT_BLUE:
# - no overlaps at all
# - SEGMENT DARK_BLUE:
# - no overlaps at all
# This will produce 3 groups (i.e NRRD files):
# - Group 1: GREEN, LIGHT_BLUE, DARK_BLUE
# - Group 2: ORANGE
# - Group 3: PURPLE

dcmqi_add_test(
NAME ${dcm2itk}_makeNRRD_merged_segment_files_from_partial_overlaps
MODULE_NAME ${MODULE_NAME}
COMMAND ${SEM_LAUNCH_COMMAND} $<TARGET_FILE:${dcm2itk}Test>
--compare ${BASELINE}/partial_overlaps-1.nrrd ${MODULE_TEMP_DIR}/makeNRRD_merged_segment_files_from_partial_overlaps-1.nrrd
--compare ${BASELINE}/partial_overlaps-2.nrrd ${MODULE_TEMP_DIR}/makeNRRD_merged_segment_files_from_partial_overlaps-2.nrrd
${dcm2itk}Test
--inputDICOM ${MODULE_TEMP_DIR}/partial_overlaps.dcm
--outputDirectory ${MODULE_TEMP_DIR}
--prefix makeNRRD_merged_segment_files_from_partial_overlaps
--mergeSegments
TEST_DEPENDS
${itk2dcm}_makeSEG_multiple_segment_files
)

dcmqi_add_test(
NAME ${dcm2itk}_makeNRRD_merged_segment_files_from_partial_overlaps_JSON
MODULE_NAME ${MODULE_NAME}
COMMAND python ${CMAKE_SOURCE_DIR}/util/comparejson.py
${CMAKE_SOURCE_DIR}/doc/examples/seg-example_partial_overlaps.json
${MODULE_TEMP_DIR}/makeNRRD_merged_segment_files_from_partial_overlaps-meta.json
TEST_DEPENDS
${dcm2itk}_makeNRRD_merged_segment_files_from_partial_overlaps
)

# ------------------------------------------------------------------------------

dcmqi_add_test(
NAME seg_meta_roundtrip
MODULE_NAME ${MODULE_NAME}
Expand Down
2 changes: 1 addition & 1 deletion apps/seg/itkimage2segimage.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ int main(int argc, char *argv[])
}

try {
DcmDataset* result = dcmqi::Itk2DicomConverter::itkimage2dcmSegmentation(dcmDatasets, segmentations, metadata, skipEmptySlices);
DcmDataset* result = dcmqi::Itk2DicomConverter::itkimage2dcmSegmentation(dcmDatasets, segmentations, metadata, skipEmptySlices, sortByLabelID);

if (result == NULL){
std::cerr << "ERROR: Conversion failed." << std::endl;
Expand Down
9 changes: 9 additions & 0 deletions apps/seg/itkimage2segimage.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@
<description>Skip empty slices while encoding segmentation image. By default, empty slices will not be encoded, resulting in a smaller output file size.</description>
</boolean>

<boolean>
<name>sortByLabelID</name>
<label>Sort by label ID </label>
<channel>output</channel>
<longflag>sortByLabelID</longflag>
<default>false</default>
<description>Use label IDs from ITK images as Segment Numbers in DICOM. Only works if label IDs are consecutively numbered starting from 1.</description>
</boolean>

<boolean>
<name>verbose</name>
<label>Verbose</label>
Expand Down
Binary file added data/segmentations/partial_overlaps-1.nrrd
Binary file not shown.
Binary file added data/segmentations/partial_overlaps-2.nrrd
Binary file not shown.
Binary file added data/segmentations/partial_overlaps-3.nrrd
Binary file not shown.
Binary file added data/segmentations/partial_overlaps.dcm
Binary file not shown.
114 changes: 114 additions & 0 deletions doc/examples/seg-example_partial_overlaps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{
"BodyPartExamined" : "",
"ClinicalTrialCoordinatingCenterName" : "QIICR",
"ClinicalTrialSeriesID" : "1",
"ClinicalTrialTimePointID" : "1",
"ContentCreatorName" : "Slicer",
"InstanceNumber" : "1",
"SeriesDescription" : "Segmentation",
"SeriesNumber" : "100",
"segmentAttributes" :
[
[
{
"SegmentAlgorithmType" : "MANUAL",
"SegmentDescription" : "Tissue",
"SegmentLabel" : "GREEN",
"SegmentedPropertyCategoryCodeSequence" :
{
"CodeMeaning" : "Tissue",
"CodeValue" : "85756007",
"CodingSchemeDesignator" : "SCT"
},
"SegmentedPropertyTypeCodeSequence" :
{
"CodeMeaning" : "Tissue",
"CodeValue" : "85756007",
"CodingSchemeDesignator" : "SCT"
},
"labelID" : 1,
"recommendedDisplayRGBValue" : [ 128, 174, 128 ]
},
{
"SegmentAlgorithmType" : "MANUAL",
"SegmentDescription" : "Morphologically Altered Structure",
"SegmentLabel" : "LIGHT_BLUE",
"SegmentedPropertyCategoryCodeSequence" :
{
"CodeMeaning" : "Morphologically Altered Structure",
"CodeValue" : "49755003",
"CodingSchemeDesignator" : "SCT"
},
"SegmentedPropertyTypeCodeSequence" :
{
"CodeMeaning" : "Edema",
"CodeValue" : "79654002",
"CodingSchemeDesignator" : "SCT"
},
"labelID" : 4,
"recommendedDisplayRGBValue" : [ 140, 224, 228 ]
},
{
"SegmentAlgorithmType" : "MANUAL",
"SegmentDescription" : "Tissue",
"SegmentLabel" : "DARK_BLUE",
"SegmentedPropertyCategoryCodeSequence" :
{
"CodeMeaning" : "Tissue",
"CodeValue" : "85756007",
"CodingSchemeDesignator" : "SCT"
},
"SegmentedPropertyTypeCodeSequence" :
{
"CodeMeaning" : "Vein",
"CodeValue" : "29092000",
"CodingSchemeDesignator" : "SCT"
},
"labelID" : 5,
"recommendedDisplayRGBValue" : [ 0, 151, 206 ]
}
],
[
{
"SegmentAlgorithmType" : "MANUAL",
"SegmentDescription" : "Tissue",
"SegmentLabel" : "ORANGE",
"SegmentedPropertyCategoryCodeSequence" :
{
"CodeMeaning" : "Tissue",
"CodeValue" : "85756007",
"CodingSchemeDesignator" : "SCT"
},
"SegmentedPropertyTypeCodeSequence" :
{
"CodeMeaning" : "Artery",
"CodeValue" : "51114001",
"CodingSchemeDesignator" : "SCT"
},
"labelID" : 2,
"recommendedDisplayRGBValue" : [ 216, 101, 79 ]
}
],
[
{
"SegmentAlgorithmType" : "MANUAL",
"SegmentDescription" : "Tissue",
"SegmentLabel" : "PURPLE",
"SegmentedPropertyCategoryCodeSequence" :
{
"CodeMeaning" : "Tissue",
"CodeValue" : "85756007",
"CodingSchemeDesignator" : "SCT"
},
"SegmentedPropertyTypeCodeSequence" :
{
"CodeMeaning" : "Capillary",
"CodeValue" : "20982000",
"CodingSchemeDesignator" : "SCT"
},
"labelID" : 3,
"recommendedDisplayRGBValue" : [ 183, 156, 220 ]
}
]
]
}
17 changes: 16 additions & 1 deletion include/dcmqi/Itk2DicomConverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,27 @@ namespace dcmqi {
* @param segmentations A vector of itk images to be converted.
* @param metaData A string containing the metadata to be used for the DICOM Segmentation object.
* @param skipEmptySlices A boolean indicating whether to skip empty slices during the conversion.
* @param sortByLabelID A boolean indicating whether to sort the segments by label ID.
* In this case, the label IDs are used as DICOM segment numbers. This only works
* if the label IDs start at 1 and numbered monotonically without gaps. The processing
* order of label IDs are not relevant, i.e. they can occur in any order int he input.
* If n labels are not assigned uniquely to label IDs 1..n in the input, the
* conversion will fail.
* If this is set to false (default), the segment numbers are assigned in the order of the
* labels that are being converted, i.e. the first label will receive the Segment
* Number 1, the second label will receive the Segment Number 2, etc.
* @return A pointer to the resulting DICOM Segmentation object.
*/
static DcmDataset* itkimage2dcmSegmentation(vector<DcmDataset*> dcmDatasets,
vector<ShortImageType::Pointer> segmentations,
const string &metaData,
bool skipEmptySlices=true);
bool skipEmptySlices=true,
bool sortByLabelID=false);

protected:

static void sortByLabel(DcmDataset* dset, map<Uint16, Uint16> segNum2Label);

};

}
Expand Down
Loading

0 comments on commit 08a5ebd

Please sign in to comment.