Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for saving segments in a single file #464

Merged
merged 29 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
52de374
ENH: write all segments to the same file
fedorov Jan 5, 2023
65b9418
ENH: adding parameterization for merged segments
fedorov Jan 25, 2023
ae1cab1
BUG: add segments to existing segmentation
fedorov Jan 27, 2023
980ccd3
STYLE: remove blanks per codefactor
fedorov Jan 27, 2023
0b6e726
ENH: fix tests
fedorov Jan 27, 2023
cbad6aa
BUG: fix overwrite of segment metadata
fedorov Feb 4, 2023
2daf29c
fix metadata init with mergedSegments on
fedorov Feb 20, 2023
0a4f0a1
WIP: use ImportImageFilter + iterators to copy frame
fedorov Apr 7, 2023
3f85903
WIP: Introduce OverlapUtil
michaelonken Oct 17, 2023
d9dc34f
WIP: Fully integrated code producing first results.
michaelonken Oct 25, 2023
57cd7ef
Better error handling.
michaelonken Oct 25, 2023
ca703ba
Fix Image Origin bug and speed up processing.
michaelonken Oct 26, 2023
921b947
Error handling.
michaelonken Oct 27, 2023
ce07b70
WIP: Fix assignment of frames to NRRD, speedup.
michaelonken Oct 31, 2023
3c3df73
Merge latest code from 'master'.
michaelonken Nov 1, 2023
1f136ca
Merge branch 'master' into merged-segments
michaelonken Nov 1, 2023
d9fce9d
Fix accidentially commited typo.
michaelonken Nov 1, 2023
8c32931
Cleanups, robustness, documentation.
michaelonken Nov 2, 2023
5761fc5
Refactored converter classes.
michaelonken Nov 2, 2023
52a8e92
Merge branch 'master' into merged-segments
fedorov Nov 3, 2023
d677992
Fixed tests and bug, minor changes.
michaelonken Nov 7, 2023
137f309
Try to fix Linux docker build.
michaelonken Nov 7, 2023
4fbcfe3
New method for finding frame sorting coordinate.
michaelonken Nov 9, 2023
549d24e
Return result image by image to save memory.
michaelonken Nov 9, 2023
9ee7d5b
Try to fix compile error on Windows.
michaelonken Nov 9, 2023
0a070f2
Another fix for windows build errors.
michaelonken Nov 9, 2023
35e761f
Replace use of deprecated jsoncpp class.
michaelonken Nov 10, 2023
c1f09ef
Fix bug when creating JSON output for merges.
michaelonken Nov 10, 2023
25d8165
Added test for merging segments into NRRD files.
michaelonken Nov 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 [email protected] -g"
$(TMP)/dockcross bash -c "export PATH=/work/build/node-v12.19.1-linux-x64/bin:$$PATH && sudo npm install [email protected] -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