Skip to content

Commit 6f2f58c

Browse files
committed
WIP feat(image-sets-normalization): add group by options
Also, remove import of "itkGDCMImageIO.h" by extracting CharStringToUTF8Converter
1 parent 35db8a3 commit 6f2f58c

15 files changed

+660
-457
lines changed

packages/dicom/gdcm/CharStringToUTF8Converter.h

+467
Large diffs are not rendered by default.

packages/dicom/gdcm/DICOMTagReader.h

+1-439
Large diffs are not rendered by default.

packages/dicom/gdcm/Tags.h

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
#include <string>
2222
#include <set>
23-
#include "itkGDCMImageIO.h"
2423

2524
using Tag = gdcm::Tag;
2625
using Tags = std::set<Tag>;
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 <optional>
22+
#include "rapidjson/document.h"
23+
24+
#include "Tags.h"
25+
26+
std::optional<const Tags> parseTags(itk::wasm::InputTextStream &tagsToRead, itk::wasm::Pipeline &pipeline)
27+
{
28+
if (tagsToRead.GetPointer() == nullptr)
29+
{
30+
return std::nullopt;
31+
}
32+
33+
rapidjson::Document inputTagsDocument;
34+
const std::string inputTagsString((std::istreambuf_iterator<char>(tagsToRead.Get())),
35+
std::istreambuf_iterator<char>());
36+
if (inputTagsDocument.Parse(inputTagsString.c_str()).HasParseError())
37+
{
38+
CLI::Error err("Runtime error", "Could not parse input tags JSON.", 1);
39+
pipeline.exit(err);
40+
return std::nullopt;
41+
}
42+
if (!inputTagsDocument.HasMember("tags"))
43+
{
44+
CLI::Error err("Runtime error", "Input tags does not have expected \"tags\" member", 1);
45+
pipeline.exit(err);
46+
return std::nullopt;
47+
}
48+
49+
const rapidjson::Value &inputTagsArray = inputTagsDocument["tags"];
50+
51+
Tags tags;
52+
for (rapidjson::Value::ConstValueIterator itr = inputTagsArray.Begin(); itr != inputTagsArray.End(); ++itr)
53+
{
54+
const std::string tagString(itr->GetString());
55+
Tag tag;
56+
tag.ReadFromPipeSeparatedString(tagString.c_str());
57+
tags.insert(tag);
58+
}
59+
return tags;
60+
}
61+
62+
#endif // TAGS_OPTION_PARSER_H

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

+28-13
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,22 @@
4343
#include "itkMakeUniqueForOverwrite.h"
4444

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

48-
#include "DICOMTagReader.h"
49+
#include "CharStringToUTF8Converter.h"
4950
#include "Tags.h"
51+
#include "TagsOptionParser.h"
5052
#include "SortSpatially.h"
5153

54+
const Tags SERIES_GROUP_BY_DEFAULT = Tags{SERIES_UID, FRAME_OF_REFERENCE_UID};
55+
const Tags IMAGE_SET_GROUP_BY_DEFAULT = Tags{STUDY_UID};
56+
57+
5258
std::string getLabelFromTag(const gdcm::Tag &tag, const gdcm::DataSet &dataSet)
5359
{
54-
if (tag.IsPrivateCreator()) {
60+
if (tag.IsPrivateCreator())
61+
{
5562
return tag.PrintAsContinuousUpperCaseString();
5663
}
5764
std::string strowner;
@@ -529,7 +536,6 @@ using DicomFiles = std::unordered_set<DicomFile, dicomFileHash>;
529536
DicomFiles loadFiles(const std::vector<FileName> &fileNames)
530537
{
531538
DicomFiles dicomFiles;
532-
itk::DICOMTagReader tagReader;
533539
for (const FileName &fileName : fileNames)
534540
{
535541
dicomFiles.insert(DicomFile(fileName));
@@ -559,20 +565,19 @@ bool compareTags(const gdcm::DataSet &tagsA, const gdcm::DataSet &tagsB, const T
559565
return true;
560566
}
561567

562-
bool isSameVolume(const gdcm::DataSet &tagsA, const gdcm::DataSet &tagsB)
568+
bool isSameVolume(const gdcm::DataSet &tagsA, const gdcm::DataSet &tagsB, const Tags &criteria)
563569
{
564-
const Tags criteria = {SERIES_UID, FRAME_OF_REFERENCE_UID};
565570
return compareTags(tagsA, tagsB, criteria);
566571
}
567572

568-
Volumes groupByVolume(const DicomFiles &dicomFiles)
573+
Volumes groupByVolume(const DicomFiles &dicomFiles, const Tags &criteria = {SERIES_UID, FRAME_OF_REFERENCE_UID})
569574
{
570575
Volumes volumes;
571576
for (const DicomFile &dicomFile : dicomFiles)
572577
{
573578
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); });
579+
auto matchingVolume = std::find_if(volumes.begin(), volumes.end(), [&candidate, &criteria](const Volume &volume)
580+
{ return isSameVolume(volume.begin()->dataSet, candidate, criteria); });
576581

577582
if (matchingVolume != volumes.end())
578583
{
@@ -587,16 +592,16 @@ Volumes groupByVolume(const DicomFiles &dicomFiles)
587592
return volumes;
588593
}
589594

590-
ImageSets groupByImageSet(const Volumes &volumes)
595+
ImageSets groupByImageSet(const Volumes &volumes, const Tags &imageSetCriteria = {STUDY_UID})
591596
{
592597
ImageSets imageSets;
593598
for (const Volume &volume : volumes)
594599
{
595600
const gdcm::DataSet volumeDataSet = volume.begin()->dataSet;
596-
auto matchingImageSet = std::find_if(imageSets.begin(), imageSets.end(), [&volumeDataSet](const Volumes &volumes)
601+
auto matchingImageSet = std::find_if(imageSets.begin(), imageSets.end(), [&volumeDataSet, &imageSetCriteria](const Volumes &volumes)
597602
{
598603
const gdcm::DataSet imageSetDataSet = volumes.begin()->begin()->dataSet;
599-
return compareTags(volumeDataSet, imageSetDataSet, {STUDY_UID}); });
604+
return compareTags(volumeDataSet, imageSetDataSet, imageSetCriteria); });
600605
if (matchingImageSet != imageSets.end())
601606
{
602607
matchingImageSet->push_back(volume);
@@ -730,15 +735,25 @@ int main(int argc, char *argv[])
730735
std::vector<std::string> files;
731736
pipeline.add_option("--files", files, "DICOM files")->required()->check(CLI::ExistingFile)->type_size(1, -1)->type_name("INPUT_BINARY_FILE");
732737

738+
itk::wasm::InputTextStream seriesGroupByOption;
739+
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");
740+
itk::wasm::InputTextStream imageSetGroupByOption;
741+
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");
742+
733743
itk::wasm::OutputTextStream imageSetsOutput;
734744
pipeline.add_option("image-sets", imageSetsOutput, "Image sets JSON")->required()->type_name("OUTPUT_JSON");
735745

736746
ITK_WASM_PARSE(pipeline);
737747

748+
const std::optional<Tags> seriesGroupByParse = parseTags(seriesGroupByOption, pipeline);
749+
const Tags seriesGroupBy = seriesGroupByParse.value_or(SERIES_GROUP_BY_DEFAULT);
750+
const std::optional<Tags> imageSetGroupByParse = parseTags(imageSetGroupByOption, pipeline);
751+
const Tags imageSetGroupBy = imageSetGroupByParse.value_or(IMAGE_SET_GROUP_BY_DEFAULT);
752+
738753
const DicomFiles dicomFiles = loadFiles(files);
739-
Volumes volumes = groupByVolume(dicomFiles);
754+
Volumes volumes = groupByVolume(dicomFiles, seriesGroupBy);
740755
volumes = sortSpatially(volumes);
741-
const ImageSets imageSets = groupByImageSet(volumes);
756+
const ImageSets imageSets = groupByImageSet(volumes, imageSetGroupBy);
742757

743758
rapidjson::Document imageSetsJson = toJson(imageSets);
744759
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

+8
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,13 @@ 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 len(image_sets) == len(mr_series)
76+
77+
7078
def test_two_series():
7179
files = [
7280
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
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
// Generated file. To retain edits, remove this comment.
22

3-
import { BinaryFile } from 'itk-wasm'
3+
import { BinaryFile,JsonCompatible } from 'itk-wasm'
44

55
interface ImageSetsNormalizationNodeOptions {
66
/** DICOM files */
77
files: string[] | File[] | BinaryFile[]
88

9+
/** 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. */
10+
seriesGroupBy?: JsonCompatible
11+
12+
/** 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. */
13+
imageSetGroupBy?: JsonCompatible
14+
915
}
1016

1117
export default ImageSetsNormalizationNodeOptions

packages/dicom/typescript/src/image-sets-normalization-node.ts

+12
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ async function imageSetsNormalizationNode(
5353
args.push(value as string)
5454
})
5555
}
56+
if (options.seriesGroupBy) {
57+
const inputCountString = inputs.length.toString()
58+
inputs.push({ type: InterfaceTypes.JsonCompatible, data: options.seriesGroupBy as JsonCompatible })
59+
args.push('--series-group-by', inputCountString)
60+
61+
}
62+
if (options.imageSetGroupBy) {
63+
const inputCountString = inputs.length.toString()
64+
inputs.push({ type: InterfaceTypes.JsonCompatible, data: options.imageSetGroupBy as JsonCompatible })
65+
args.push('--image-set-group-by', inputCountString)
66+
67+
}
5668

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

Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
// Generated file. To retain edits, remove this comment.
22

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

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

9+
/** 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. */
10+
seriesGroupBy?: JsonCompatible
11+
12+
/** 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. */
13+
imageSetGroupBy?: JsonCompatible
14+
915
}
1016

1117
export default ImageSetsNormalizationOptions

packages/dicom/typescript/src/image-sets-normalization.ts

+12
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ async function imageSetsNormalization(
6060
args.push(name)
6161
}))
6262
}
63+
if (options.seriesGroupBy) {
64+
const inputCountString = inputs.length.toString()
65+
inputs.push({ type: InterfaceTypes.JsonCompatible, data: options.seriesGroupBy as JsonCompatible })
66+
args.push('--series-group-by', inputCountString)
67+
68+
}
69+
if (options.imageSetGroupBy) {
70+
const inputCountString = inputs.length.toString()
71+
inputs.push({ type: InterfaceTypes.JsonCompatible, data: options.imageSetGroupBy as JsonCompatible })
72+
args.push('--image-set-group-by', inputCountString)
73+
74+
}
6375

6476
const pipelinePath = 'image-sets-normalization'
6577

0 commit comments

Comments
 (0)