Skip to content

Commit

Permalink
WIP: add native tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jadh4v committed Jul 8, 2024
1 parent 742ae9e commit f4ca151
Show file tree
Hide file tree
Showing 3 changed files with 348 additions and 17 deletions.
116 changes: 100 additions & 16 deletions packages/dicom/dcmtk/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,24 +63,32 @@ target_link_libraries(read-overlapping-segmentation PUBLIC ${ITK_LIBRARIES} dcmq
add_executable(write-segmentation write-segmentation.cxx)
target_link_libraries(write-segmentation PUBLIC ${ITK_LIBRARIES} dcmqi)

add_executable(write-multi-segmentation write-multi-segmentation.cxx)
target_link_libraries(write-multi-segmentation PUBLIC ${ITK_LIBRARIES} dcmqi)

add_executable(write-overlapping-segmentation write-overlapping-segmentation.cxx)
target_link_libraries(write-overlapping-segmentation PUBLIC ${ITK_LIBRARIES} dcmqi)

add_test(
NAME read-segmentation_help
COMMAND read-segmentation --help
)
set(TEMP_DIR ${CMAKE_BINARY_DIR}/Testing/Temporary)
set(BASELINE ${dcmqi_lib_SOURCE_DIR}/data/segmentations)
set(JSON_DIR ${dcmqi_lib_SOURCE_DIR}/doc/examples)
set(DICOM_DIR ${BASELINE}/ct-3slice)
set(MODULE_TEMP_DIR ${TEMP_DIR}/seg)
make_directory(${MODULE_TEMP_DIR})

add_test(
NAME read-overlapping-segmentation_help
COMMAND read-overlapping-segmentation --help
)
set(dcm2itk read-segmentation)
set(itk2dcm write-segmentation)

add_test(
NAME write-segmentation_help
COMMAND write-segmentation --help
)

add_test(
NAME write-multi-segmentation_help
COMMAND write-multi-segmentation --help
)

add_test(
NAME write-overlapping-segmentation_help
COMMAND write-overlapping-segmentation --help
Expand All @@ -89,20 +97,96 @@ add_test(
add_test(
NAME write-segmentation_makeSEG
COMMAND write-segmentation
${dcmqi_lib_SOURCE_DIR}/data/segmentations/liver_seg.nrrd
${dcmqi_lib_SOURCE_DIR}/doc/examples/seg-example.json
${BASELINE}/liver_seg.nrrd
${JSON_DIR}/seg-example.json
${CMAKE_CURRENT_SOURCE_DIR}/../test/data/output/write-read-overlapping-segmentation-output_makeSEG.dcm
--ref-dicom-series ${dcmqi_lib_SOURCE_DIR}/data/segmentations/ct-3slice/01.dcm
${dcmqi_lib_SOURCE_DIR}/data/segmentations/ct-3slice/02.dcm
${dcmqi_lib_SOURCE_DIR}/data/segmentations/ct-3slice/03.dcm
)
)

add_test(
NAME write-read-overlapping-segmentation_makeSEG
COMMAND write-overlapping-segmentation
${dcmqi_lib_SOURCE_DIR}/data/segmentations/partial_overlaps-combined.nrrd
add_test(
NAME write-multi-segmentation_makeSEG_merged_segment_files_from_partial_overlap
COMMAND write-multi-segmentation
${dcmqi_lib_SOURCE_DIR}/doc/examples/seg-example_partial_overlaps.json
${CMAKE_CURRENT_SOURCE_DIR}/../test/data/output/partial_overlaps.dcm
--ref-dicom-series ${dcmqi_lib_SOURCE_DIR}/data/segmentations/ct-3slice/01.dcm ${dcmqi_lib_SOURCE_DIR}/data/segmentations/ct-3slice/02.dcm ${dcmqi_lib_SOURCE_DIR}/data/segmentations/ct-3slice/03.dcm
--seg-images ${BASELINE}/partial_overlaps-1.nrrd ${BASELINE}/partial_overlaps-2.nrrd ${BASELINE}/partial_overlaps-3.nrrd
-l
)
)

add_test(
NAME write-multi-segmentation_makeSEG_multiple_segment_files
COMMAND write-multi-segmentation
${dcmqi_lib_SOURCE_DIR}/doc/examples/seg-example_multiple_segments.json
${MODULE_TEMP_DIR}/liver_heart_seg.dcm
--ref-dicom-series ${DICOM_DIR}/01.dcm ${DICOM_DIR}/02.dcm ${DICOM_DIR}/03.dcm
--seg-images ${BASELINE}/liver_seg.nrrd ${BASELINE}/spine_seg.nrrd ${BASELINE}/heart_seg.nrrd
)

add_test(
NAME write-multi-segmentation_makeSEG_multiple_segment_files_reordered
COMMAND write-multi-segmentation
${CMAKE_SOURCE_DIR}/doc/examples/seg-example_multiple_segments_reordered.json
${MODULE_TEMP_DIR}/liver_heart_seg_reordered.dcm
--ref-dicom-series ${DICOM_DIR}/01.dcm ${DICOM_DIR}/02.dcm ${DICOM_DIR}/03.dcm
--seg-images ${BASELINE}/heart_seg.nrrd ${BASELINE}/liver_seg.nrrd ${BASELINE}/spine_seg.nrrd
)

add_test(
NAME read-segmentation_help
COMMAND read-segmentation --help
)

add_test(
NAME read-overlapping-segmentation_help
COMMAND read-overlapping-segmentation --help
)

add_test(
NAME read-segmentation_makeNRRD
COMMAND
read-segmentation
${dcmqi_lib_SOURCE_DIR}/data/segmentations/liver.dcm
${MODULE_TEMP_DIR}/makeNRRD-1.nrrd
${MODULE_TEMP_DIR}/makeNRRD-1.json
)

add_test(
NAME ${dcm2itk}_makeNRRD_merged_segment_files_from_partial_overlaps
COMMAND
${dcm2itk}
${BASELINE}/partial_overlaps.dcm
${MODULE_TEMP_DIR}/makeNRRD_merged_segment_files_from_partial_overlaps.nrrd
${MODULE_TEMP_DIR}/makeNRRD_merged_segment_files_from_partial_overlaps.json
)

set(TEST_SEG_SIZES 24x38x3 23x38x3)

foreach(seg_size ${TEST_SEG_SIZES})

add_test(
NAME ${itk2dcm}_makeSEG_${seg_size}
COMMAND ${itk2dcm}
${BASELINE}/${seg_size}/nrrd/label.nrrd
${dcmqi_lib_SOURCE_DIR}/doc/examples/seg-example.json
${MODULE_TEMP_DIR}/${seg_size}_seg.dcm
--ref-dicom-series
${BASELINE}/${seg_size}/image/IMG0001.dcm
${BASELINE}/${seg_size}/image/IMG0002.dcm
${BASELINE}/${seg_size}/image/IMG0003.dcm
)

add_test(
NAME ${dcm2itk}_makeNRRD_${seg_size}
COMMAND ${dcm2itk}
${MODULE_TEMP_DIR}/${seg_size}_seg.dcm
${MODULE_TEMP_DIR}/${seg_size}.nrrd
${MODULE_TEMP_DIR}/${seg_size}.json
)

set_tests_properties(${dcm2itk}_makeNRRD_${seg_size}
PROPERTIES DEPENDS ${itk2dcm}_makeSEG_${seg_size}
)

endforeach()
247 changes: 247 additions & 0 deletions packages/dicom/dcmtk/write-multi-segmentation.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/*=========================================================================
* Copyright NumFOCUS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*=========================================================================*/

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

// DCMTK includes
#include "dcmtk/oflog/configrt.h"

#include "itkPipeline.h"
#include "itkInputImage.h"
#include "itkOutputBinaryStream.h"
#include "itkSupportInputImageTypes.h"
#include "itkSmartPointer.h"
#include "itkInputTextStream.h"

typedef dcmqi::Helper helper;
constexpr unsigned int Dimension = 3;
using PixelType = short;
using ScalarImageType = itk::Image<PixelType, Dimension>;

int runPipeline(
itk::wasm::InputTextStream& metaInfoStream,
const std::vector<std::string> & dicomImageFiles,
const std::vector<std::string> & segImageFiles,
const std::string & outputDicomFile,
const bool skipEmptySlices,
const bool useLabelIDAsSegmentNumber)
{
const std::string metaInfo((std::istreambuf_iterator<char>(metaInfoStream.Get())),
std::istreambuf_iterator<char>());

// Pipeline code goes here
if(dicomImageFiles.empty()){
cerr << "Error: No input DICOM files specified!" << endl;
return EXIT_FAILURE;
}

std::vector<ScalarImageType::Pointer> segmentations;
for(size_t segFileNumber = 0; segFileNumber < segImageFiles.size(); ++segFileNumber)
{
ShortReaderType::Pointer reader = ShortReaderType::New();
reader->SetFileName(segImageFiles[segFileNumber]);
reader->Update();
cout << "Loaded segmentation from " << segImageFiles[segFileNumber] << endl;

ShortImageType::Pointer labelImage = reader->GetOutput();
segmentations.push_back(labelImage);

ShortImageType::SizeType ref_size, cmp_size;
ref_size = segmentations[0]->GetLargestPossibleRegion().GetSize();
cmp_size = labelImage->GetLargestPossibleRegion().GetSize();
if(ref_size[0] != cmp_size[0] || ref_size[1] != cmp_size[1])
{
cerr << "Error: In-plane dimensions of segmentations are inconsistent!" << endl;
cerr << ref_size << " vs " << cmp_size << endl;
return EXIT_FAILURE;
}
}
// itk::Indent indent;
// img->Print(std::cout, indent);

#if !defined(NDEBUG) || defined(_DEBUG)
// Display DCMTK debug, warning, and error logs in the console
// For some reason, this code has no effect if it is called too early (e.g., directly after PARSE_ARGS)
// therefore we call it here.
dcmtk::log4cplus::BasicConfigurator::doConfigure();
#endif

if(!helper::pathsExist(dicomImageFiles))
{
return EXIT_FAILURE;
}

std::vector<DcmDataset*> dcmDatasets = helper::loadDatasets(dicomImageFiles);

if(dcmDatasets.empty())
{
cerr << "Error: no DICOM could be loaded from the specified list/directory" << endl;
return EXIT_FAILURE;
}

/*
ifstream metainfoStream(metaInfoFile.c_str(), ios_base::binary);
std::string metadata( (std::istreambuf_iterator<char>(metainfoStream) ),
(std::istreambuf_iterator<char>()));
*/

Json::Value metaRoot;
istringstream metainfoisstream(metaInfo);
metainfoisstream >> metaRoot;

if(metaRoot.isMember("segmentAttributes"))
{
if(metaRoot["segmentAttributes"].size() != segImageFiles.size())
{
cerr << "Error: number of items in the \"segmentAttributes\" metadata array should match the number of input segmentation files!" << endl;
cerr << "segmentAttributes has: " << metaRoot["segmentAttributes"].size() << " items, the are " << segImageFiles.size() << " input segmentation files!" << endl;
return EXIT_FAILURE;
}
}

if(metaRoot.isMember("segmentAttributesFileMapping"))
{
if(metaRoot["segmentAttributesFileMapping"].size() != metaRoot["segmentAttributes"].size())
{
cerr << "Number of files in segmentAttributesFileMapping should match the number of entries in segmentAttributes!" << endl;
return EXIT_FAILURE;
}
// otherwise, re-order the entries in the segmentAtrributes list to match the order of files in segmentAttributesFileMapping
Json::Value reorderedSegmentAttributes;
vector<int> fileOrder(segImageFiles.size());
fill(fileOrder.begin(), fileOrder.end(), -1);
vector<ShortImageType::Pointer> segmentationsReordered(segImageFiles.size());
for(size_t filePosition=0;filePosition<segImageFiles.size();filePosition++)
{
for(size_t mappingPosition=0;mappingPosition<segImageFiles.size();mappingPosition++)
{
string mappingItem = metaRoot["segmentAttributesFileMapping"][static_cast<int>(mappingPosition)].asCString();
size_t foundPos = segImageFiles[filePosition].rfind(mappingItem);
if(foundPos != std::string::npos)
{
fileOrder[filePosition] = mappingPosition;
break;
}
}
if(fileOrder[filePosition] == -1)
{
cerr << "Failed to map " << segImageFiles[filePosition] << " from the segmentAttributesFileMapping attribute to an input file name!" << endl;
return EXIT_FAILURE;
}
}
cout << "Order of input ITK images updated as shown below based on the segmentAttributesFileMapping attribute:" << endl;
for(size_t i=0;i<segImageFiles.size();i++)
{
cout << " image " << i << " moved to position " << fileOrder[i] << endl;
segmentationsReordered[fileOrder[i]] = segmentations[i];
}
segmentations = segmentationsReordered;
}

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

if (result == NULL)
{
std::cerr << "ERROR: Conversion failed." << std::endl;
return EXIT_FAILURE;
}
else
{
DcmFileFormat segdocFF(result);
bool compress = false;
if (compress)
{
CHECK_COND(segdocFF.saveFile(outputDicomFile.c_str(), EXS_DeflatedLittleEndianExplicit));
}
else
{
CHECK_COND(segdocFF.saveFile(outputDicomFile.c_str(), EXS_LittleEndianExplicit));
}

std::cout << "Saved segmentation as " << outputDicomFile << endl;
}

for (size_t i = 0; i < dcmDatasets.size(); i++)
{
delete dcmDatasets[i];
}

if (result != NULL)
{
delete result;
}
return EXIT_SUCCESS;
}
catch (int e)
{
std::cerr << "Fatal error encountered." << std::endl;
}
return EXIT_SUCCESS;
}

int main(int argc, char * argv[])
{
itk::wasm::Pipeline pipeline("write-segmentation", "Write DICOM segmentation object", argc, argv);

//itk::wasm::InputImage<ScalarImageType> inputImage;
//pipeline.add_option("seg-image", inputImage, "dicom segmentation object as an image")->required()->type_name("INPUT_IMAGE");

itk::wasm::InputTextStream metaInfo;
pipeline.add_option("meta-info", metaInfo, "JSON file containing the meta-information that describes" \
"the measurements to be encoded. See DCMQI documentation for details.")->required()->type_name("INPUT_JSON");

std::string outputDicomFile;
pipeline.add_option("output-dicom-file", outputDicomFile, "File name of the DICOM SEG object that will store the" \
"result of conversion.")->required()->type_name("OUTPUT_BINARY_FILE");

std::vector<std::string> refDicomSeriesFiles;
pipeline.add_option("-r,--ref-dicom-series", refDicomSeriesFiles, "List of DICOM files that correspond to the original." \
"image that was segmented.")->required()->check(CLI::ExistingFile)->expected(1,-1)->type_name("INPUT_BINARY_FILE");

std::vector<std::string> segImageFiles;
pipeline.add_option("-i,--seg-images", segImageFiles, "List of input segmentation images." \
"image that was segmented.")->required()->check(CLI::ExistingFile)->expected(1,-1)->type_name("INPUT_BINARY_FILE");

bool skipEmptySlices{true};
pipeline.add_flag("-s,--skip-empty-slices", skipEmptySlices, "Skip empty slices while encoding segmentation image." \
"By default, empty slices will not be encoded, resulting in a smaller output file size.");

bool useLabelIDAsSegmentNumber{false};
pipeline.add_flag("-l,--use-labelid-as-segmentnumber", useLabelIDAsSegmentNumber, "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.");

ITK_WASM_PARSE(pipeline);

runPipeline(
metaInfo,
refDicomSeriesFiles,
segImageFiles,
outputDicomFile,
skipEmptySlices,
useLabelIDAsSegmentNumber
);
}
Loading

0 comments on commit f4ca151

Please sign in to comment.