Skip to content

Commit

Permalink
WIP feat(image-sets-normalization): add group by options
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulHax committed Apr 4, 2024
1 parent 35db8a3 commit f324962
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 13 deletions.
61 changes: 61 additions & 0 deletions packages/dicom/gdcm/TagsOptionParser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*=========================================================================
*
* 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.
*
*=========================================================================*/
#ifndef TAGS_OPTION_PARSER_H
#define TAGS_OPTION_PARSER_H

#include "rapidjson/document.h"

#include "Tags.h"

std::optional<Tags> parseTags(itk::wasm::InputTextStream &tagsToRead, itk::wasm::Pipeline &pipeline)
{
if (tagsToRead.GetPointer() == nullptr)
{
return std::nullopt;
}

rapidjson::Document inputTagsDocument;
const std::string inputTagsString((std::istreambuf_iterator<char>(tagsToRead.Get())),
std::istreambuf_iterator<char>());
if (inputTagsDocument.Parse(inputTagsString.c_str()).HasParseError())
{
CLI::Error err("Runtime error", "Could not parse input tags JSON.", 1);
pipeline.exit(err);
return std::nullopt;
}
if (!inputTagsDocument.HasMember("tags"))
{
CLI::Error err("Runtime error", "Input tags does not have expected \"tags\" member", 1);
pipeline.exit(err);
return std::nullopt;
}

const rapidjson::Value &inputTagsArray = inputTagsDocument["tags"];

Tags tags;
for (rapidjson::Value::ConstValueIterator itr = inputTagsArray.Begin(); itr != inputTagsArray.End(); ++itr)
{
const std::string tagString(itr->GetString());
Tag tag;
tag.ReadFromPipeSeparatedString(tagString.c_str());
tags.insert(tag);
}
return tags;
}

#endif // TAGS_OPTION_PARSER_H
34 changes: 23 additions & 11 deletions packages/dicom/gdcm/image-sets-normalization.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,18 @@
#include "itkMakeUniqueForOverwrite.h"

#include "itkPipeline.h"
#include "itkInputTextStream.h"
#include "itkOutputTextStream.h"

#include "DICOMTagReader.h"
#include "Tags.h"
#include "TagsOptionParser.h"
#include "SortSpatially.h"

std::string getLabelFromTag(const gdcm::Tag &tag, const gdcm::DataSet &dataSet)
{
if (tag.IsPrivateCreator()) {
if (tag.IsPrivateCreator())
{
return tag.PrintAsContinuousUpperCaseString();
}
std::string strowner;
Expand Down Expand Up @@ -559,20 +562,19 @@ bool compareTags(const gdcm::DataSet &tagsA, const gdcm::DataSet &tagsB, const T
return true;
}

bool isSameVolume(const gdcm::DataSet &tagsA, const gdcm::DataSet &tagsB)
bool isSameVolume(const gdcm::DataSet &tagsA, const gdcm::DataSet &tagsB, const Tags &criteria)
{
const Tags criteria = {SERIES_UID, FRAME_OF_REFERENCE_UID};
return compareTags(tagsA, tagsB, criteria);
}

Volumes groupByVolume(const DicomFiles &dicomFiles)
Volumes groupByVolume(const DicomFiles &dicomFiles, const Tags &criteria = {SERIES_UID, FRAME_OF_REFERENCE_UID})
{
Volumes volumes;
for (const DicomFile &dicomFile : dicomFiles)
{
const auto candidate = dicomFile.dataSet;
auto matchingVolume = std::find_if(volumes.begin(), volumes.end(), [&candidate](const Volume &volume)
{ return isSameVolume(volume.begin()->dataSet, candidate); });
auto matchingVolume = std::find_if(volumes.begin(), volumes.end(), [&candidate, &criteria](const Volume &volume)
{ return isSameVolume(volume.begin()->dataSet, candidate, criteria); });

if (matchingVolume != volumes.end())
{
Expand All @@ -587,16 +589,16 @@ Volumes groupByVolume(const DicomFiles &dicomFiles)
return volumes;
}

ImageSets groupByImageSet(const Volumes &volumes)
ImageSets groupByImageSet(const Volumes &volumes, const Tags &imageSetCriteria = {STUDY_UID})
{
ImageSets imageSets;
for (const Volume &volume : volumes)
{
const gdcm::DataSet volumeDataSet = volume.begin()->dataSet;
auto matchingImageSet = std::find_if(imageSets.begin(), imageSets.end(), [&volumeDataSet](const Volumes &volumes)
auto matchingImageSet = std::find_if(imageSets.begin(), imageSets.end(), [&volumeDataSet, &imageSetCriteria](const Volumes &volumes)
{
const gdcm::DataSet imageSetDataSet = volumes.begin()->begin()->dataSet;
return compareTags(volumeDataSet, imageSetDataSet, {STUDY_UID}); });
return compareTags(volumeDataSet, imageSetDataSet, imageSetCriteria); });
if (matchingImageSet != imageSets.end())
{
matchingImageSet->push_back(volume);
Expand Down Expand Up @@ -730,15 +732,25 @@ int main(int argc, char *argv[])
std::vector<std::string> files;
pipeline.add_option("--files", files, "DICOM files")->required()->check(CLI::ExistingFile)->type_size(1, -1)->type_name("INPUT_BINARY_FILE");

itk::wasm::InputTextStream seriesGroupByOption;
pipeline.add_option("--series-group-by", seriesGroupByOption, "Create series so that all instances in a series share these tags. Option is a JSON object with a \"tags\" array. Example tag: \"0008|103e\". If not provided, defaults to Series UID and Frame Of Reference UID tags.")->type_name("INPUT_JSON");
itk::wasm::InputTextStream imageSetGroupByOption;
pipeline.add_option("--image-set-group-by", imageSetGroupByOption, "Create image sets so that all series in a set share these tags. Option is a JSON object with a \"tags\" array. Example tag: \"0008|103e\". If not provided, defaults to Study UID tag.")->type_name("INPUT_JSON");

itk::wasm::OutputTextStream imageSetsOutput;
pipeline.add_option("image-sets", imageSetsOutput, "Image sets JSON")->required()->type_name("OUTPUT_JSON");

ITK_WASM_PARSE(pipeline);

const std::optional<Tags> seriesGroupByParse = parseTags(seriesGroupByOption, pipeline);
const Tags seriesGroupBy = seriesGroupByParse.value_or(Tags{SERIES_UID, FRAME_OF_REFERENCE_UID});
const std::optional<Tags> imageSetGroupByParse = parseTags(imageSetGroupByOption, pipeline);
const Tags imageSetGroupBy = imageSetGroupByParse.value_or(Tags{STUDY_UID});

const DicomFiles dicomFiles = loadFiles(files);
Volumes volumes = groupByVolume(dicomFiles);
Volumes volumes = groupByVolume(dicomFiles, seriesGroupBy);
volumes = sortSpatially(volumes);
const ImageSets imageSets = groupByImageSet(volumes);
const ImageSets imageSets = groupByImageSet(volumes, imageSetGroupBy);

rapidjson::Document imageSetsJson = toJson(imageSets);
rapidjson::StringBuffer stringBuffer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@

async def image_sets_normalization_async(
files: List[os.PathLike] = [],
series_group_by: Optional[Any] = None,
image_set_group_by: Optional[Any] = None,
) -> Any:
"""Group DICOM files into image sets
:param files: DICOM files
:type files: os.PathLike
:param series_group_by: Create series so that all instances in a series share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Series UID and Frame Of Reference UID tags.
:type series_group_by: Any
:param image_set_group_by: Create image sets so that all series in a set share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Study UID tag.
:type image_set_group_by: Any
:return: Image sets JSON
:rtype: Any
"""
Expand All @@ -33,6 +41,10 @@ async def image_sets_normalization_async(
kwargs = {}
if files is not None:
kwargs["files"] = to_js(BinaryFile(files))
if series_group_by is not None:
kwargs["seriesGroupBy"] = to_js(series_group_by)
if image_set_group_by is not None:
kwargs["imageSetGroupBy"] = to_js(image_set_group_by)

outputs = await js_module.imageSetsNormalization(webWorker=web_worker, noCopy=True, **kwargs)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@

def image_sets_normalization(
files: List[os.PathLike] = [],
series_group_by: Optional[Any] = None,
image_set_group_by: Optional[Any] = None,
) -> Any:
"""Group DICOM files into image sets
:param files: DICOM files
:type files: os.PathLike
:param series_group_by: Create series so that all instances in a series share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Series UID and Frame Of Reference UID tags.
:type series_group_by: Any
:param image_set_group_by: Create image sets so that all series in a set share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Study UID tag.
:type image_set_group_by: Any
:return: Image sets JSON
:rtype: Any
"""
Expand Down Expand Up @@ -55,6 +63,18 @@ def image_sets_normalization(
pipeline_inputs.append(PipelineInput(InterfaceTypes.BinaryFile, BinaryFile(value)))
args.append(input_file)

if series_group_by is not None:
pipeline_inputs.append(PipelineInput(InterfaceTypes.JsonCompatible, series_group_by))
args.append('--series-group-by')
args.append(str(input_count))
input_count += 1

if image_set_group_by is not None:
pipeline_inputs.append(PipelineInput(InterfaceTypes.JsonCompatible, image_set_group_by))
args.append('--image-set-group-by')
args.append(str(input_count))
input_count += 1


outputs = _pipeline.run(args, pipeline_outputs, pipeline_inputs)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def test_ct():
sorted_files = pick_files(image_sets[0])
assert_equal(sorted_files, ct_series)


def test_mr():
assert mr_series[0].exists()
out_of_order = [
Expand All @@ -67,6 +68,14 @@ def test_mr():
assert_equal(sorted_files, mr_series)


def test_series_group_by_option():
assert mr_series[0].exists()
group_by_tags = {"tags": ["0008|0018"]} # SOP Instance UID
image_sets = image_sets_normalization(mr_series, series_group_by=group_by_tags)
assert image_sets
assert len(image_sets) == len(mr_series)


def test_two_series():
files = [
orientation_series[1],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,23 @@

def image_sets_normalization(
files: List[os.PathLike] = [],
series_group_by: Optional[Any] = None,
image_set_group_by: Optional[Any] = None,
) -> Any:
"""Group DICOM files into image sets
:param files: DICOM files
:type files: os.PathLike
:param series_group_by: Create series so that all instances in a series share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Series UID and Frame Of Reference UID tags.
:type series_group_by: Any
:param image_set_group_by: Create image sets so that all series in a set share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Study UID tag.
:type image_set_group_by: Any
:return: Image sets JSON
:rtype: Any
"""
func = environment_dispatch("itkwasm_dicom", "image_sets_normalization")
output = func(files=files)
output = func(files=files, series_group_by=series_group_by, image_set_group_by=image_set_group_by)
return output
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,23 @@

async def image_sets_normalization_async(
files: List[os.PathLike] = [],
series_group_by: Optional[Any] = None,
image_set_group_by: Optional[Any] = None,
) -> Any:
"""Group DICOM files into image sets
:param files: DICOM files
:type files: os.PathLike
:param series_group_by: Create series so that all instances in a series share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Series UID and Frame Of Reference UID tags.
:type series_group_by: Any
:param image_set_group_by: Create image sets so that all series in a set share these tags. Option is a JSON object with a "tags" array. Example tag: "0008|103e". If not provided, defaults to Study UID tag.
:type image_set_group_by: Any
:return: Image sets JSON
:rtype: Any
"""
func = environment_dispatch("itkwasm_dicom", "image_sets_normalization_async")
output = await func(files=files)
output = await func(files=files, series_group_by=series_group_by, image_set_group_by=image_set_group_by)
return output

0 comments on commit f324962

Please sign in to comment.