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
Also, remove import of "itkGDCMImageIO.h" by extracting CharStringToUTF8Converter
  • Loading branch information
PaulHax authored and thewtex committed Aug 13, 2024
1 parent 033fb54 commit 2234a56
Show file tree
Hide file tree
Showing 15 changed files with 660 additions and 457 deletions.
467 changes: 467 additions & 0 deletions packages/dicom/gdcm/CharStringToUTF8Converter.h

Large diffs are not rendered by default.

440 changes: 1 addition & 439 deletions packages/dicom/gdcm/DICOMTagReader.h

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion packages/dicom/gdcm/Tags.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

#include <string>
#include <set>
#include "itkGDCMImageIO.h"

using Tag = gdcm::Tag;
using Tags = std::set<Tag>;
Expand Down
62 changes: 62 additions & 0 deletions packages/dicom/gdcm/TagsOptionParser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*=========================================================================
*
* 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 <optional>
#include "rapidjson/document.h"

#include "Tags.h"

std::optional<const 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
41 changes: 28 additions & 13 deletions packages/dicom/gdcm/image-sets-normalization.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,22 @@
#include "itkMakeUniqueForOverwrite.h"

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

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

const Tags SERIES_GROUP_BY_DEFAULT = Tags{SERIES_UID, FRAME_OF_REFERENCE_UID};
const Tags IMAGE_SET_GROUP_BY_DEFAULT = Tags{STUDY_UID};


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 @@ -529,7 +536,6 @@ using DicomFiles = std::unordered_set<DicomFile, dicomFileHash>;
DicomFiles loadFiles(const std::vector<FileName> &fileNames)
{
DicomFiles dicomFiles;
itk::DICOMTagReader tagReader;
for (const FileName &fileName : fileNames)
{
dicomFiles.insert(DicomFile(fileName));
Expand Down Expand Up @@ -559,20 +565,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 +592,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 +735,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(SERIES_GROUP_BY_DEFAULT);
const std::optional<Tags> imageSetGroupByParse = parseTags(imageSetGroupByOption, pipeline);
const Tags imageSetGroupBy = imageSetGroupByParse.value_or(IMAGE_SET_GROUP_BY_DEFAULT);

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,13 @@ 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 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
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
// Generated file. To retain edits, remove this comment.

import { BinaryFile } from 'itk-wasm'
import { BinaryFile,JsonCompatible } from 'itk-wasm'

interface ImageSetsNormalizationNodeOptions {
/** DICOM files */
files: string[] | File[] | BinaryFile[]

/** 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. */
seriesGroupBy?: JsonCompatible

/** 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. */
imageSetGroupBy?: JsonCompatible

}

export default ImageSetsNormalizationNodeOptions
12 changes: 12 additions & 0 deletions packages/dicom/typescript/src/image-sets-normalization-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ async function imageSetsNormalizationNode(
args.push(value as string)
})
}
if (options.seriesGroupBy) {
const inputCountString = inputs.length.toString()
inputs.push({ type: InterfaceTypes.JsonCompatible, data: options.seriesGroupBy as JsonCompatible })
args.push('--series-group-by', inputCountString)

}
if (options.imageSetGroupBy) {
const inputCountString = inputs.length.toString()
inputs.push({ type: InterfaceTypes.JsonCompatible, data: options.imageSetGroupBy as JsonCompatible })
args.push('--image-set-group-by', inputCountString)

}

const pipelinePath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'pipelines', 'image-sets-normalization')

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
// Generated file. To retain edits, remove this comment.

import { BinaryFile, WorkerPoolFunctionOption } from 'itk-wasm'
import { BinaryFile,JsonCompatible, WorkerPoolFunctionOption } from 'itk-wasm'

interface ImageSetsNormalizationOptions extends WorkerPoolFunctionOption {
/** DICOM files */
files: string[] | File[] | BinaryFile[]

/** 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. */
seriesGroupBy?: JsonCompatible

/** 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. */
imageSetGroupBy?: JsonCompatible

}

export default ImageSetsNormalizationOptions
12 changes: 12 additions & 0 deletions packages/dicom/typescript/src/image-sets-normalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ async function imageSetsNormalization(
args.push(name)
}))
}
if (options.seriesGroupBy) {
const inputCountString = inputs.length.toString()
inputs.push({ type: InterfaceTypes.JsonCompatible, data: options.seriesGroupBy as JsonCompatible })
args.push('--series-group-by', inputCountString)

}
if (options.imageSetGroupBy) {
const inputCountString = inputs.length.toString()
inputs.push({ type: InterfaceTypes.JsonCompatible, data: options.imageSetGroupBy as JsonCompatible })
args.push('--image-set-group-by', inputCountString)

}

const pipelinePath = 'image-sets-normalization'

Expand Down
Loading

0 comments on commit 2234a56

Please sign in to comment.