Skip to content

Commit 1a8cf86

Browse files
CPBridgeMMelQin
andauthored
Implement highdicom seg operator (#327)
* Implement highdicom seg operator Signed-off-by: Chris Bridge <chrisbridge44@googlemail.com> * Formatting fixes Signed-off-by: Chris Bridge <chrisbridge44@googlemail.com> * Typing fixes Signed-off-by: Chris Bridge <chrisbridge44@googlemail.com> * Update the Spleen App to use HighDicom Seg Writer. The app is still not compatible with monai v0.9.1 as the app testing revealed that to its Invert transform failed resample the predicted image back to input image spacings. Also, the new Seg Writer impl is strict on DICOM attribute VR conformance, and would throw exception when the input DICOM instances have non-conformant attribute VR values. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Fix isort error for ordering of imports Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Update doc strings and comments for seg label and algorithm name and version Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Pin moani==0.9.0 for now as 0.9.1 causes issues. Also pydicom to 2.3.0 as the use of highdicom require pydicom>=2.3.0 Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Updated apps that have multiple segments Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Found the few missing codes, so avoided use of generic "Organ" Signed-off-by: mmelqin <mingmelvinq@nvidia.com> Signed-off-by: Chris Bridge <chrisbridge44@googlemail.com> Signed-off-by: mmelqin <mingmelvinq@nvidia.com> Co-authored-by: mmelqin <mingmelvinq@nvidia.com>
1 parent 2526579 commit 1a8cf86

File tree

8 files changed

+291
-480
lines changed

8 files changed

+291
-480
lines changed

examples/apps/ai_livertumor_seg_app/app.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@
1313

1414
from livertumor_seg_operator import LiverTumorSegOperator
1515

16+
# Required for setting SegmentDescription attributes. Direct import as this is not part of App SDK package.
17+
from pydicom.sr.codedict import codes
18+
1619
from monai.deploy.core import Application, resource
1720
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
18-
from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator
21+
from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator, SegmentDescription
1922
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
2023
from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
2124
from monai.deploy.operators.publisher_operator import PublisherOperator
@@ -46,33 +49,57 @@ def compose(self):
4649
series_selector_op = DICOMSeriesSelectorOperator()
4750
series_to_vol_op = DICOMSeriesToVolumeOperator()
4851
# Model specific inference operator, supporting MONAI transforms.
49-
unetr_seg_op = LiverTumorSegOperator()
52+
liver_tumor_seg_op = LiverTumorSegOperator()
5053

5154
# Create the publisher operator
5255
publisher_op = PublisherOperator()
5356

54-
# Creates DICOM Seg writer with segment label name in a string list
55-
dicom_seg_writer = DICOMSegmentationWriterOperator(
56-
seg_labels=[
57-
"Liver",
58-
"Tumor",
59-
]
60-
)
57+
# Create DICOM Seg writer providing the required segment description for each segment with
58+
# the actual algorithm and the pertinent organ/tissue.
59+
# The segment_label, algorithm_name, and algorithm_version are limited to 64 chars.
60+
# https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
61+
# User can Look up SNOMED CT codes at, e.g.
62+
# https://bioportal.bioontology.org/ontologies/SNOMEDCT
63+
64+
_algorithm_name = "3D segmentation of the liver and tumor from CT image"
65+
_algorithm_family = codes.DCM.ArtificialIntelligence
66+
_algorithm_version = "0.1.0"
67+
68+
segment_descriptions = [
69+
SegmentDescription(
70+
segment_label="Liver",
71+
segmented_property_category=codes.SCT.Organ,
72+
segmented_property_type=codes.SCT.Liver,
73+
algorithm_name=_algorithm_name,
74+
algorithm_family=_algorithm_family,
75+
algorithm_version=_algorithm_version,
76+
),
77+
SegmentDescription(
78+
segment_label="Tumor",
79+
segmented_property_category=codes.SCT.Tumor,
80+
segmented_property_type=codes.SCT.Tumor,
81+
algorithm_name=_algorithm_name,
82+
algorithm_family=_algorithm_family,
83+
algorithm_version=_algorithm_version,
84+
),
85+
]
86+
87+
dicom_seg_writer = DICOMSegmentationWriterOperator(segment_descriptions)
6188
# Create the processing pipeline, by specifying the source and destination operators, and
6289
# ensuring the output from the former matches the input of the latter, in both name and type.
6390
self.add_flow(study_loader_op, series_selector_op, {"dicom_study_list": "dicom_study_list"})
6491
self.add_flow(
6592
series_selector_op, series_to_vol_op, {"study_selected_series_list": "study_selected_series_list"}
6693
)
67-
self.add_flow(series_to_vol_op, unetr_seg_op, {"image": "image"})
94+
self.add_flow(series_to_vol_op, liver_tumor_seg_op, {"image": "image"})
6895
# Add the publishing operator to save the input and seg images for Render Server.
6996
# Note the PublisherOperator has temp impl till a proper rendering module is created.
70-
self.add_flow(unetr_seg_op, publisher_op, {"saved_images_folder": "saved_images_folder"})
97+
self.add_flow(liver_tumor_seg_op, publisher_op, {"saved_images_folder": "saved_images_folder"})
7198
# Note below the dicom_seg_writer requires two inputs, each coming from a source operator.
7299
self.add_flow(
73100
series_selector_op, dicom_seg_writer, {"study_selected_series_list": "study_selected_series_list"}
74101
)
75-
self.add_flow(unetr_seg_op, dicom_seg_writer, {"seg_image": "seg_image"})
102+
self.add_flow(liver_tumor_seg_op, dicom_seg_writer, {"seg_image": "seg_image"})
76103

77104
self._logger.debug(f"End {self.compose.__name__}")
78105

examples/apps/ai_spleen_seg_app/app.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@
1111

1212
import logging
1313

14+
# Required for setting SegmentDescription attributes. Direct import as this is not part of App SDK package.
15+
from pydicom.sr.codedict import codes
16+
1417
from monai.deploy.core import Application, resource
1518
from monai.deploy.core.domain import Image
1619
from monai.deploy.core.io_type import IOType
1720
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
18-
from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator
21+
from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator, SegmentDescription
1922
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
2023
from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
2124
from monai.deploy.operators.monai_bundle_inference_operator import IOMapping, MonaiBundleInferenceOperator
@@ -56,13 +59,29 @@ def compose(self):
5659
# during init to provide the optional packages info, parsed from the bundle, to the packager
5760
# for it to install the packages in the MAP docker image.
5861
# Setting output IOType to DISK only works only for leaf operators, not the case in this example.
62+
#
63+
# Pertinent MONAI Bundle:
64+
# https://github.com/Project-MONAI/model-zoo/tree/dev/models/spleen_ct_segmentation
5965
bundle_spleen_seg_op = MonaiBundleInferenceOperator(
6066
input_mapping=[IOMapping("image", Image, IOType.IN_MEMORY)],
6167
output_mapping=[IOMapping("pred", Image, IOType.IN_MEMORY)],
6268
)
6369

64-
# Create DICOM Seg writer with segment label name in a string list
65-
dicom_seg_writer = DICOMSegmentationWriterOperator(seg_labels=["Spleen"])
70+
# Create DICOM Seg writer providing the required segment description for each segment with
71+
# the actual algorithm and the pertinent organ/tissue. The segment_label, algorithm_name,
72+
# and algorithm_version are of DICOM VR LO type, limited to 64 chars.
73+
# https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
74+
segment_descriptions = [
75+
SegmentDescription(
76+
segment_label="Spleen",
77+
segmented_property_category=codes.SCT.Organ,
78+
segmented_property_type=codes.SCT.Spleen,
79+
algorithm_name="volumetric (3D) segmentation of the spleen from CT image",
80+
algorithm_family=codes.DCM.ArtificialIntelligence,
81+
algorithm_version="0.1.0",
82+
)
83+
]
84+
dicom_seg_writer = DICOMSegmentationWriterOperator(segment_descriptions=segment_descriptions)
6685

6786
# Create the processing pipeline, by specifying the source and destination operators, and
6887
# ensuring the output from the former matches the input of the latter, in both name and type.

examples/apps/ai_unetr_seg_app/app.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111

1212
import logging
1313

14+
# Required for setting SegmentDescription attributes. Direct import as this is not part of App SDK package.
15+
from pydicom.sr.codedict import codes
1416
from unetr_seg_operator import UnetrSegOperator
1517

1618
from monai.deploy.core import Application, resource
1719
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
20+
from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator, SegmentDescription
1821
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
1922
from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
2023
from monai.deploy.operators.publisher_operator import PublisherOperator
@@ -54,6 +57,50 @@ def compose(self):
5457
output_file="stl/multi-organs.stl", keep_largest_connected_component=False
5558
)
5659

60+
# Create DICOM Seg writer providing the required segment description for each segment with
61+
# the actual algorithm and the pertinent organ/tissue.
62+
# The segment_label, algorithm_name, and algorithm_version are limited to 64 chars.
63+
# https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
64+
65+
_algorithm_name = "3D multi-organ segmentation from CT image"
66+
_algorithm_family = codes.DCM.ArtificialIntelligence
67+
_algorithm_version = "0.1.0"
68+
69+
# List of (Segment name, [Code menaing str]), not including background which is value of 0.
70+
# User must provide correct codes, which can be looked at, e.g.
71+
# https://bioportal.bioontology.org/ontologies/SNOMEDCT
72+
# Alternatively, consult the concept and code dictionaries in PyDicom
73+
74+
organs = [
75+
("Spleen",),
76+
("Right Kidney", "Kidney"),
77+
("Left Kideny", "Kidney"),
78+
("Gallbladder",),
79+
("Esophagus",),
80+
("Liver",),
81+
("Stomach",),
82+
("Aorta",),
83+
("Inferior vena cava", "InferiorVenaCava"),
84+
("Portal and Splenic Veins", "SplenicVein"),
85+
("Pancreas",),
86+
("Right adrenal gland", "AdrenalGland"),
87+
("Left adrenal gland", "AdrenalGland"),
88+
]
89+
90+
segment_descriptions = [
91+
SegmentDescription(
92+
segment_label=organ[0],
93+
segmented_property_category=codes.SCT.Organ,
94+
segmented_property_type=codes.SCT.__getattr__(organ[1] if len(organ) > 1 else organ[0]),
95+
algorithm_name=_algorithm_name,
96+
algorithm_family=_algorithm_family,
97+
algorithm_version=_algorithm_version,
98+
)
99+
for organ in organs
100+
]
101+
102+
dicom_seg_writer = DICOMSegmentationWriterOperator(segment_descriptions)
103+
57104
# Create the processing pipeline, by specifying the source and destination operators, and
58105
# ensuring the output from the former matches the input of the latter, in both name and type.
59106
self.add_flow(study_loader_op, series_selector_op, {"dicom_study_list": "dicom_study_list"})
@@ -67,6 +114,12 @@ def compose(self):
67114
# Note the PublisherOperator has temp impl till a proper rendering module is created.
68115
self.add_flow(unetr_seg_op, publisher_op, {"saved_images_folder": "saved_images_folder"})
69116

117+
# Note below the dicom_seg_writer requires two inputs, each coming from a source operator.
118+
self.add_flow(
119+
series_selector_op, dicom_seg_writer, {"study_selected_series_list": "study_selected_series_list"}
120+
)
121+
self.add_flow(unetr_seg_op, dicom_seg_writer, {"seg_image": "seg_image"})
122+
70123
self._logger.debug(f"End {self.compose.__name__}")
71124

72125

0 commit comments

Comments
 (0)