Skip to content

Commit f324962

Browse files
committed
WIP feat(image-sets-normalization): add group by options
1 parent 35db8a3 commit f324962

File tree

7 files changed

+143
-13
lines changed

7 files changed

+143
-13
lines changed
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*=========================================================================
2+
*
3+
* Copyright NumFOCUS
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0.txt
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*=========================================================================*/
18+
#ifndef TAGS_OPTION_PARSER_H
19+
#define TAGS_OPTION_PARSER_H
20+
21+
#include "rapidjson/document.h"
22+
23+
#include "Tags.h"
24+
25+
std::optional<Tags> parseTags(itk::wasm::InputTextStream &tagsToRead, itk::wasm::Pipeline &pipeline)
26+
{
27+
if (tagsToRead.GetPointer() == nullptr)
28+
{
29+
return std::nullopt;
30+
}
31+
32+
rapidjson::Document inputTagsDocument;
33+
const std::string inputTagsString((std::istreambuf_iterator<char>(tagsToRead.Get())),
34+
std::istreambuf_iterator<char>());
35+
if (inputTagsDocument.Parse(inputTagsString.c_str()).HasParseError())
36+
{
37+
CLI::Error err("Runtime error", "Could not parse input tags JSON.", 1);
38+
pipeline.exit(err);
39+
return std::nullopt;
40+
}
41+
if (!inputTagsDocument.HasMember("tags"))
42+
{
43+
CLI::Error err("Runtime error", "Input tags does not have expected \"tags\" member", 1);
44+
pipeline.exit(err);
45+
return std::nullopt;
46+
}
47+
48+
const rapidjson::Value &inputTagsArray = inputTagsDocument["tags"];
49+
50+
Tags tags;
51+
for (rapidjson::Value::ConstValueIterator itr = inputTagsArray.Begin(); itr != inputTagsArray.End(); ++itr)
52+
{
53+
const std::string tagString(itr->GetString());
54+
Tag tag;
55+
tag.ReadFromPipeSeparatedString(tagString.c_str());
56+
tags.insert(tag);
57+
}
58+
return tags;
59+
}
60+
61+
#endif // TAGS_OPTION_PARSER_H

packages/dicom/gdcm/image-sets-normalization.cxx

+23-11
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,18 @@
4343
#include "itkMakeUniqueForOverwrite.h"
4444

4545
#include "itkPipeline.h"
46+
#include "itkInputTextStream.h"
4647
#include "itkOutputTextStream.h"
4748

4849
#include "DICOMTagReader.h"
4950
#include "Tags.h"
51+
#include "TagsOptionParser.h"
5052
#include "SortSpatially.h"
5153

5254
std::string getLabelFromTag(const gdcm::Tag &tag, const gdcm::DataSet &dataSet)
5355
{
54-
if (tag.IsPrivateCreator()) {
56+
if (tag.IsPrivateCreator())
57+
{
5558
return tag.PrintAsContinuousUpperCaseString();
5659
}
5760
std::string strowner;
@@ -559,20 +562,19 @@ bool compareTags(const gdcm::DataSet &tagsA, const gdcm::DataSet &tagsB, const T
559562
return true;
560563
}
561564

562-
bool isSameVolume(const gdcm::DataSet &tagsA, const gdcm::DataSet &tagsB)
565+
bool isSameVolume(const gdcm::DataSet &tagsA, const gdcm::DataSet &tagsB, const Tags &criteria)
563566
{
564-
const Tags criteria = {SERIES_UID, FRAME_OF_REFERENCE_UID};
565567
return compareTags(tagsA, tagsB, criteria);
566568
}
567569

568-
Volumes groupByVolume(const DicomFiles &dicomFiles)
570+
Volumes groupByVolume(const DicomFiles &dicomFiles, const Tags &criteria = {SERIES_UID, FRAME_OF_REFERENCE_UID})
569571
{
570572
Volumes volumes;
571573
for (const DicomFile &dicomFile : dicomFiles)
572574
{
573575
const auto candidate = dicomFile.dataSet;
574-
auto matchingVolume = std::find_if(volumes.begin(), volumes.end(), [&candidate](const Volume &volume)
575-
{ return isSameVolume(volume.begin()->dataSet, candidate); });
576+
auto matchingVolume = std::find_if(volumes.begin(), volumes.end(), [&candidate, &criteria](const Volume &volume)
577+
{ return isSameVolume(volume.begin()->dataSet, candidate, criteria); });
576578

577579
if (matchingVolume != volumes.end())
578580
{
@@ -587,16 +589,16 @@ Volumes groupByVolume(const DicomFiles &dicomFiles)
587589
return volumes;
588590
}
589591

590-
ImageSets groupByImageSet(const Volumes &volumes)
592+
ImageSets groupByImageSet(const Volumes &volumes, const Tags &imageSetCriteria = {STUDY_UID})
591593
{
592594
ImageSets imageSets;
593595
for (const Volume &volume : volumes)
594596
{
595597
const gdcm::DataSet volumeDataSet = volume.begin()->dataSet;
596-
auto matchingImageSet = std::find_if(imageSets.begin(), imageSets.end(), [&volumeDataSet](const Volumes &volumes)
598+
auto matchingImageSet = std::find_if(imageSets.begin(), imageSets.end(), [&volumeDataSet, &imageSetCriteria](const Volumes &volumes)
597599
{
598600
const gdcm::DataSet imageSetDataSet = volumes.begin()->begin()->dataSet;
599-
return compareTags(volumeDataSet, imageSetDataSet, {STUDY_UID}); });
601+
return compareTags(volumeDataSet, imageSetDataSet, imageSetCriteria); });
600602
if (matchingImageSet != imageSets.end())
601603
{
602604
matchingImageSet->push_back(volume);
@@ -730,15 +732,25 @@ int main(int argc, char *argv[])
730732
std::vector<std::string> files;
731733
pipeline.add_option("--files", files, "DICOM files")->required()->check(CLI::ExistingFile)->type_size(1, -1)->type_name("INPUT_BINARY_FILE");
732734

735+
itk::wasm::InputTextStream seriesGroupByOption;
736+
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");
737+
itk::wasm::InputTextStream imageSetGroupByOption;
738+
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");
739+
733740
itk::wasm::OutputTextStream imageSetsOutput;
734741
pipeline.add_option("image-sets", imageSetsOutput, "Image sets JSON")->required()->type_name("OUTPUT_JSON");
735742

736743
ITK_WASM_PARSE(pipeline);
737744

745+
const std::optional<Tags> seriesGroupByParse = parseTags(seriesGroupByOption, pipeline);
746+
const Tags seriesGroupBy = seriesGroupByParse.value_or(Tags{SERIES_UID, FRAME_OF_REFERENCE_UID});
747+
const std::optional<Tags> imageSetGroupByParse = parseTags(imageSetGroupByOption, pipeline);
748+
const Tags imageSetGroupBy = imageSetGroupByParse.value_or(Tags{STUDY_UID});
749+
738750
const DicomFiles dicomFiles = loadFiles(files);
739-
Volumes volumes = groupByVolume(dicomFiles);
751+
Volumes volumes = groupByVolume(dicomFiles, seriesGroupBy);
740752
volumes = sortSpatially(volumes);
741-
const ImageSets imageSets = groupByImageSet(volumes);
753+
const ImageSets imageSets = groupByImageSet(volumes, imageSetGroupBy);
742754

743755
rapidjson::Document imageSetsJson = toJson(imageSets);
744756
rapidjson::StringBuffer stringBuffer;

packages/dicom/python/itkwasm-dicom-emscripten/itkwasm_dicom_emscripten/image_sets_normalization_async.py

+12
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,20 @@
1818

1919
async def image_sets_normalization_async(
2020
files: List[os.PathLike] = [],
21+
series_group_by: Optional[Any] = None,
22+
image_set_group_by: Optional[Any] = None,
2123
) -> Any:
2224
"""Group DICOM files into image sets
2325
2426
:param files: DICOM files
2527
:type files: os.PathLike
2628
29+
: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.
30+
:type series_group_by: Any
31+
32+
: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.
33+
:type image_set_group_by: Any
34+
2735
:return: Image sets JSON
2836
:rtype: Any
2937
"""
@@ -33,6 +41,10 @@ async def image_sets_normalization_async(
3341
kwargs = {}
3442
if files is not None:
3543
kwargs["files"] = to_js(BinaryFile(files))
44+
if series_group_by is not None:
45+
kwargs["seriesGroupBy"] = to_js(series_group_by)
46+
if image_set_group_by is not None:
47+
kwargs["imageSetGroupBy"] = to_js(image_set_group_by)
3648

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

packages/dicom/python/itkwasm-dicom-wasi/itkwasm_dicom_wasi/image_sets_normalization.py

+20
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,20 @@
1818

1919
def image_sets_normalization(
2020
files: List[os.PathLike] = [],
21+
series_group_by: Optional[Any] = None,
22+
image_set_group_by: Optional[Any] = None,
2123
) -> Any:
2224
"""Group DICOM files into image sets
2325
2426
:param files: DICOM files
2527
:type files: os.PathLike
2628
29+
: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.
30+
:type series_group_by: Any
31+
32+
: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.
33+
:type image_set_group_by: Any
34+
2735
:return: Image sets JSON
2836
:rtype: Any
2937
"""
@@ -55,6 +63,18 @@ def image_sets_normalization(
5563
pipeline_inputs.append(PipelineInput(InterfaceTypes.BinaryFile, BinaryFile(value)))
5664
args.append(input_file)
5765

66+
if series_group_by is not None:
67+
pipeline_inputs.append(PipelineInput(InterfaceTypes.JsonCompatible, series_group_by))
68+
args.append('--series-group-by')
69+
args.append(str(input_count))
70+
input_count += 1
71+
72+
if image_set_group_by is not None:
73+
pipeline_inputs.append(PipelineInput(InterfaceTypes.JsonCompatible, image_set_group_by))
74+
args.append('--image-set-group-by')
75+
args.append(str(input_count))
76+
input_count += 1
77+
5878

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

packages/dicom/python/itkwasm-dicom-wasi/tests/test_image_sets_normalization.py

+9
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def test_ct():
5252
sorted_files = pick_files(image_sets[0])
5353
assert_equal(sorted_files, ct_series)
5454

55+
5556
def test_mr():
5657
assert mr_series[0].exists()
5758
out_of_order = [
@@ -67,6 +68,14 @@ def test_mr():
6768
assert_equal(sorted_files, mr_series)
6869

6970

71+
def test_series_group_by_option():
72+
assert mr_series[0].exists()
73+
group_by_tags = {"tags": ["0008|0018"]} # SOP Instance UID
74+
image_sets = image_sets_normalization(mr_series, series_group_by=group_by_tags)
75+
assert image_sets
76+
assert len(image_sets) == len(mr_series)
77+
78+
7079
def test_two_series():
7180
files = [
7281
orientation_series[1],

packages/dicom/python/itkwasm-dicom/itkwasm_dicom/image_sets_normalization.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,23 @@
1010

1111
def image_sets_normalization(
1212
files: List[os.PathLike] = [],
13+
series_group_by: Optional[Any] = None,
14+
image_set_group_by: Optional[Any] = None,
1315
) -> Any:
1416
"""Group DICOM files into image sets
1517
1618
:param files: DICOM files
1719
:type files: os.PathLike
1820
21+
: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.
22+
:type series_group_by: Any
23+
24+
: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.
25+
:type image_set_group_by: Any
26+
1927
:return: Image sets JSON
2028
:rtype: Any
2129
"""
2230
func = environment_dispatch("itkwasm_dicom", "image_sets_normalization")
23-
output = func(files=files)
31+
output = func(files=files, series_group_by=series_group_by, image_set_group_by=image_set_group_by)
2432
return output

packages/dicom/python/itkwasm-dicom/itkwasm_dicom/image_sets_normalization_async.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,23 @@
1010

1111
async def image_sets_normalization_async(
1212
files: List[os.PathLike] = [],
13+
series_group_by: Optional[Any] = None,
14+
image_set_group_by: Optional[Any] = None,
1315
) -> Any:
1416
"""Group DICOM files into image sets
1517
1618
:param files: DICOM files
1719
:type files: os.PathLike
1820
21+
: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.
22+
:type series_group_by: Any
23+
24+
: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.
25+
:type image_set_group_by: Any
26+
1927
:return: Image sets JSON
2028
:rtype: Any
2129
"""
2230
func = environment_dispatch("itkwasm_dicom", "image_sets_normalization_async")
23-
output = await func(files=files)
31+
output = await func(files=files, series_group_by=series_group_by, image_set_group_by=image_set_group_by)
2432
return output

0 commit comments

Comments
 (0)