Skip to content

Commit

Permalink
Merge pull request #464 from fedorov/merged-segments
Browse files Browse the repository at this point in the history
Add support for merging non-overlapping segments and saving in a single file
  • Loading branch information
fedorov authored Nov 16, 2023
2 parents 9e733b4 + 25d8165 commit a67c6c7
Show file tree
Hide file tree
Showing 22 changed files with 2,242 additions and 435 deletions.
1 change: 1 addition & 0 deletions CMakeExternals/DCMTK.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ if(NOT DEFINED DCMTK_DIR AND NOT ${CMAKE_PROJECT_NAME}_USE_SYSTEM_${proj})
-DDCMTK_ENABLE_BUILTIN_DICTIONARY:BOOL=ON
-DDCMTK_ENABLE_PRIVATE_TAGS:BOOL=ON
-DDCMTK_COMPILE_WIN32_MULTITHREADED_DLL:BOOL=ON
-DDCMTK_ENABLE_STL:BOOL=ON
DEPENDS
${${proj}_DEPENDENCIES}
)
Expand Down
39 changes: 37 additions & 2 deletions apps/seg/Testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ dcmqi_add_test(
--outputDICOM ${MODULE_TEMP_DIR}/liver.dcm
)

# Creates a DICOM segmentation file that has 3 segments:
# - segment for liver (DICOM Segment Number 1)
# - segment for spine (DICOM Segment Number 2)
# - segment for heart (DICOM Segment Number 3)
dcmqi_add_test(
NAME ${itk2dcm}_makeSEG_multiple_segment_files
MODULE_NAME ${MODULE_NAME}
Expand All @@ -50,6 +54,7 @@ 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 @@ -139,6 +144,38 @@ 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,
# one output file will contain 2 segments (liver and spine) and the other will contain only
# the heart. This will be reflected in the JSON metadata files with two entries in the outer
# array (one for each NRRD file), one with 2 segments (liver,spine) and one with 1 segment (heart).
dcmqi_add_test(
NAME ${dcm2itk}_makeNRRD_merged_segment_file
MODULE_NAME ${MODULE_NAME}
COMMAND ${SEM_LAUNCH_COMMAND} $<TARGET_FILE:${dcm2itk}Test>
--compare ${BASELINE}/liver_spine_seg.nrrd ${MODULE_TEMP_DIR}/makeNRRD_merged_segment_file-1.nrrd
--compare ${BASELINE}/heart_seg.nrrd ${MODULE_TEMP_DIR}/makeNRRD_merged_segment_file-2.nrrd
${dcm2itk}Test
--inputDICOM ${MODULE_TEMP_DIR}/liver_heart_seg.dcm
--outputDirectory ${MODULE_TEMP_DIR}
--prefix makeNRRD_merged_segment_file
--mergeSegments
TEST_DEPENDS
${itk2dcm}_makeSEG_multiple_segment_files
)

# Compare expected JSON output coming from makeNRRD_merged_segment_file test.
dcmqi_add_test(
NAME ${dcm2itk}_makeNRRD_merged_segment_file_JSON
MODULE_NAME ${MODULE_NAME}
COMMAND python ${CMAKE_SOURCE_DIR}/util/comparejson.py
${CMAKE_SOURCE_DIR}/doc/examples/seg-example_multiple_segments_merged.json
${MODULE_TEMP_DIR}/makeNRRD_merged_segment_file-meta.json
TEST_DEPENDS
${dcm2itk}_makeNRRD_merged_segment_file
)

dcmqi_add_test(
NAME seg_meta_roundtrip
MODULE_NAME ${MODULE_NAME}
Expand All @@ -160,8 +197,6 @@ dcmqi_add_test(
${dcm2itk}_makeNRRD_multiple_segment_files
)



set(TEST_SEG_SIZES 24x38x3 23x38x3)

foreach(seg_size ${TEST_SEG_SIZES})
Expand Down
4 changes: 2 additions & 2 deletions apps/seg/itkimage2segimage.cxx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// CLP includes
#include "dcmqi/Itk2DicomConverter.h"
#include "itkimage2segimageCLP.h"

// DCMQI includes
#undef HAVE_SSTREAM // Avoid redefinition warning
#include "dcmqi/ImageSEGConverter.h"
#include "dcmqi/internal/VersionConfigure.h"

// DCMTK includes
Expand Down Expand Up @@ -122,7 +122,7 @@ int main(int argc, char *argv[])
}

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

if (result == NULL){
std::cerr << "ERROR: Conversion failed." << std::endl;
Expand Down
51 changes: 37 additions & 14 deletions apps/seg/segimage2itkimage.cxx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
// CLP includes
#include "segimage2itkimageCLP.h"
#include <itkSmartPointer.h>

// DCMQI includes
#undef HAVE_SSTREAM // Avoid redefinition warning
#include "dcmqi/ImageSEGConverter.h"
#include "dcmqi/Dicom2ItkConverter.h"
#include "dcmqi/internal/VersionConfigure.h"

// DCMTK includes
#include <dcmtk/oflog/configrt.h>

typedef dcmqi::Helper helper;
typedef itk::ImageFileWriter<ShortImageType> WriterType;


int main(int argc, char *argv[])
Expand All @@ -23,6 +25,11 @@ int main(int argc, char *argv[])
dcmtk::log4cplus::BasicConfigurator::doConfigure();
}

if (mergeSegments && outputType != "nrrd") {
std::cerr << "ERROR: mergeSegments option is only supported when output format is NRRD!" << std::endl;
return EXIT_FAILURE;
}

if(helper::isUndefinedOrPathDoesNotExist(inputSEGFileName, "Input DICOM file")
|| helper::isUndefinedOrPathDoesNotExist(outputDirName, "Output directory"))
return EXIT_FAILURE;
Expand All @@ -34,31 +41,47 @@ int main(int argc, char *argv[])
DcmDataset* dataset = sliceFF.getDataset();

try {
pair <map<unsigned,ShortImageType::Pointer>, string> result = dcmqi::ImageSEGConverter::dcmSegmentation2itkimage(dataset);
dcmqi::Dicom2ItkConverter converter;
std::string metaInfo;
OFCondition result = converter.dcmSegmentation2itkimage(dataset, metaInfo, mergeSegments);
if (result.bad())
{
std::cerr << "ERROR: Failed to convert DICOM SEG to ITK image: " << result.text() << std::endl;
return EXIT_FAILURE;
}

string outputPrefix = prefix.empty() ? "" : prefix + "-";

string fileExtension = dcmqi::Helper::getFileExtensionFromType(outputType);

for(map<unsigned,ShortImageType::Pointer>::const_iterator sI=result.first.begin();sI!=result.first.end();++sI){
typedef itk::ImageFileWriter<ShortImageType> WriterType;
itk::SmartPointer<ShortImageType> itkImage = converter.begin();
size_t fileIndex = 1;
while (itkImage)
{
stringstream imageFileNameSStream;

imageFileNameSStream << outputDirName << "/" << outputPrefix << sI->first << fileExtension;

WriterType::Pointer writer = WriterType::New();
writer->SetFileName(imageFileNameSStream.str().c_str());
writer->SetInput(sI->second);
writer->SetUseCompression(1);
writer->Update();
cout << "Writing itk image to " << outputDirName << "/" << outputPrefix << fileIndex << fileExtension;
imageFileNameSStream << outputDirName << "/" << outputPrefix << fileIndex << fileExtension;

try {
WriterType::Pointer writer = WriterType::New();
writer->SetFileName(imageFileNameSStream.str().c_str());
writer->SetInput(itkImage);
writer->SetUseCompression(1);
writer->Update();
cout << " ... done" << endl;
} catch (itk::ExceptionObject & error) {
std::cerr << "fatal ITK error: " << error << std::endl;
return EXIT_FAILURE;
}
itkImage = converter.next();
fileIndex++;
}

stringstream jsonOutput;
jsonOutput << outputDirName << "/" << outputPrefix << "meta.json";

ofstream outputFile;
outputFile.open(jsonOutput.str().c_str());
outputFile << result.second;
outputFile << metaInfo;
outputFile.close();

return EXIT_SUCCESS;
Expand Down
9 changes: 9 additions & 0 deletions apps/seg/segimage2itkimage.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@
<description>Display more verbose output, useful for troubleshooting.</description>
</boolean>

<boolean>
<name>mergeSegments</name>
<label>Merge segments</label>
<channel>input</channel>
<longflag>mergeSegments</longflag>
<default>false</default>
<description>Save all segments into a single file. When segments are non-overlapping, output is a single 3D file. If overlapping, single 4D following conventions of 3D Slicer segmentations format. Only supported when the output is NRRD for now.</description>
</boolean>

</parameters>

</executable>
Binary file added data/segmentations/liver_spine_seg.nrrd
Binary file not shown.
68 changes: 68 additions & 0 deletions doc/examples/seg-example_multiple_segments_merged.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"BodyPartExamined" : "",
"ClinicalTrialCoordinatingCenterName" : "BWH",
"ClinicalTrialSeriesID" : "Session1",
"ClinicalTrialTimePointID" : "1",
"ContentCreatorName" : "Doe^John",
"InstanceNumber" : "1",
"SeriesDescription" : "Segmentation",
"SeriesNumber" : "300",
"segmentAttributes" : [
[
{
"SegmentAlgorithmName" : "SlicerEditor",
"SegmentAlgorithmType" : "SEMIAUTOMATIC",
"SegmentDescription" : "Liver Segmentation",
"SegmentLabel" : "Liver",
"SegmentedPropertyCategoryCodeSequence" : {
"CodeMeaning" : "Tissue",
"CodeValue" : "85756007",
"CodingSchemeDesignator" : "SCT"
},
"SegmentedPropertyTypeCodeSequence" : {
"CodeMeaning" : "Liver",
"CodeValue" : "10200004",
"CodingSchemeDesignator" : "SCT"
},
"labelID" : 1,
"recommendedDisplayRGBValue" : [ 220, 129, 101 ]
},
{
"SegmentAlgorithmType" : "MANUAL",
"SegmentDescription" : "Anatomical Structure",
"SegmentLabel" : "Thoracic spine",
"SegmentedPropertyCategoryCodeSequence" : {
"CodeMeaning" : "Anatomical Structure",
"CodeValue" : "123037004",
"CodingSchemeDesignator" : "SCT"
},
"SegmentedPropertyTypeCodeSequence" : {
"CodeMeaning" : "Thoracic spine",
"CodeValue" : "122495006",
"CodingSchemeDesignator" : "SCT"
},
"labelID" : 2,
"recommendedDisplayRGBValue" : [ 226, 202, 134 ]
}
],
[
{
"SegmentAlgorithmType" : "MANUAL",
"SegmentDescription" : "Anatomical Structure",
"SegmentLabel" : "Heart",
"SegmentedPropertyCategoryCodeSequence" : {
"CodeMeaning" : "Anatomical Structure",
"CodeValue" : "123037004",
"CodingSchemeDesignator" : "SCT"
},
"SegmentedPropertyTypeCodeSequence" : {
"CodeMeaning" : "Heart",
"CodeValue" : "80891009",
"CodingSchemeDesignator" : "SCT"
},
"labelID" : 3,
"recommendedDisplayRGBValue" : [ 206, 110, 84 ]
}
]
]
}
12 changes: 7 additions & 5 deletions docker/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,19 @@ prereq.build_dicom3tools: prereq.pull_dockcross
&& ( $(TMP)/dockcross make -j$(grep -c processor /proc/cpuinfo) World > /dev/null 2>&1 )\
|| echo "Reusing "

# Download and install "ajv"
# Download and install "ajv"
prereq.install_npm_packages: prereq.pull_dockcross
mkdir -p $(ROOT_DIR)/$(BUILD_DIR)
echo "Root dir: $(ROOT_DIR)"
echo "Build dir: $(BUILD_DIR)"
# If needed, download node
cd $(ROOT_DIR)/$(BUILD_DIR) && \
[ ! -e node-v6.9.5-linux-x64 ] && \
wget --no-check-certificate https://nodejs.org/dist/v6.9.5/node-v6.9.5-linux-x64.tar.xz && \
tar xf node-v6.9.5-linux-x64.tar.xz || true
[ ! -e node-v12.19.1-linux-x64 ] && \
wget --no-check-certificate https://nodejs.org/dist/v12.19.1/node-v12.19.1-linux-x64.tar.xz && \
tar xf node-v12.19.1-linux-x64.tar.xz || true
# Install tools required to run DCMQI "doc" tests
cd $(ROOT_DIR) && \
$(TMP)/dockcross bash -c "export PATH=/work/build/node-v6.9.5-linux-x64/bin:$$PATH && sudo npm install ajv-cli@3.3.0 -g"
$(TMP)/dockcross bash -c "export PATH=/work/build/node-v12.19.1-linux-x64/bin:$$PATH && sudo npm install ajv-cli@3.3.0 -g"

# Configure, build and package
dcmqi.generate_package: prereq.pull_dockcross
Expand Down
21 changes: 20 additions & 1 deletion include/dcmqi/ConverterBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
using namespace std;

typedef short ShortPixelType;
typedef unsigned char CharPixelType;
typedef itk::Image<ShortPixelType, 3> ShortImageType;
typedef itk::Image<CharPixelType, 3> CharImageType;
typedef itk::ImageFileReader<ShortImageType> ShortReaderType;

namespace dcmqi {
Expand Down Expand Up @@ -219,6 +221,21 @@ namespace dcmqi {
sort(originDistances.begin(), originDistances.end());

sliceSpacing = fabs(originDistances[0]-originDistances[1]);
if (sliceSpacing == 0)
{
cout << "Slice spacing is zero, trying to get/use it from DICOM file instead" << endl;
// Get Slice Spacing as defined in Pixel Measures FG
OFBool isPerFrame;
FGPixelMeasures *pixelMeasures = OFstatic_cast(FGPixelMeasures*,
fgInterface.get(0, DcmFGTypes::EFG_PIXELMEASURES, isPerFrame));
if(!pixelMeasures){
cerr << "Canont get Slice Spacing, Pixel Measures FG is missing!" << endl;
return EXIT_FAILURE;
}
if(pixelMeasures->getSpacingBetweenSlices(sliceSpacing,0).good()){
cout << "Using Slice Spacing (from DICOM file): " << sliceSpacing << endl;
}
}

// for(size_t i=1;i<originDistances.size(); i++){
// float dist1 = fabs(originDistances[i-1]-originDistances[i]);
Expand All @@ -232,7 +249,6 @@ namespace dcmqi {
if(it->second>1)
overlappingFramesCnt++;
}

cout << "Total frames with unique IPP: " << originDistances.size() << endl;
cout << "Total overlapping frames: " << overlappingFramesCnt << endl;
}
Expand All @@ -244,6 +260,9 @@ namespace dcmqi {
sliceSpacing = 1.0;
}
cout << "Origin: " << imageOrigin << endl;
cout << "Slice extent: " << sliceExtent << endl;
cout << "Slice spacing: " << sliceSpacing << endl;


return 0;
}
Expand Down
Loading

0 comments on commit a67c6c7

Please sign in to comment.