Skip to content

Commit

Permalink
Implement highdicom seg operator (#327)
Browse files Browse the repository at this point in the history
* Implement highdicom seg operator

Signed-off-by: Chris Bridge <[email protected]>

* Formatting fixes

Signed-off-by: Chris Bridge <[email protected]>

* Typing fixes

Signed-off-by: Chris Bridge <[email protected]>

* 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 <[email protected]>

* Fix isort error for ordering of imports

Signed-off-by: mmelqin <[email protected]>

* Update doc strings and comments for seg label and algorithm name and version

Signed-off-by: mmelqin <[email protected]>

* 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 <[email protected]>

* Updated apps that have multiple segments

Signed-off-by: mmelqin <[email protected]>

* Found the few missing codes, so avoided use of generic "Organ"

Signed-off-by: mmelqin <[email protected]>

Signed-off-by: Chris Bridge <[email protected]>
Signed-off-by: mmelqin <[email protected]>
Co-authored-by: mmelqin <[email protected]>
  • Loading branch information
CPBridge and MMelQin authored Aug 18, 2022
1 parent 2526579 commit 1a8cf86
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 480 deletions.
51 changes: 39 additions & 12 deletions examples/apps/ai_livertumor_seg_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@

from livertumor_seg_operator import LiverTumorSegOperator

# Required for setting SegmentDescription attributes. Direct import as this is not part of App SDK package.
from pydicom.sr.codedict import codes

from monai.deploy.core import Application, resource
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator
from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator, SegmentDescription
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
from monai.deploy.operators.publisher_operator import PublisherOperator
Expand Down Expand Up @@ -46,33 +49,57 @@ def compose(self):
series_selector_op = DICOMSeriesSelectorOperator()
series_to_vol_op = DICOMSeriesToVolumeOperator()
# Model specific inference operator, supporting MONAI transforms.
unetr_seg_op = LiverTumorSegOperator()
liver_tumor_seg_op = LiverTumorSegOperator()

# Create the publisher operator
publisher_op = PublisherOperator()

# Creates DICOM Seg writer with segment label name in a string list
dicom_seg_writer = DICOMSegmentationWriterOperator(
seg_labels=[
"Liver",
"Tumor",
]
)
# Create DICOM Seg writer providing the required segment description for each segment with
# the actual algorithm and the pertinent organ/tissue.
# The segment_label, algorithm_name, and algorithm_version are limited to 64 chars.
# https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
# User can Look up SNOMED CT codes at, e.g.
# https://bioportal.bioontology.org/ontologies/SNOMEDCT

_algorithm_name = "3D segmentation of the liver and tumor from CT image"
_algorithm_family = codes.DCM.ArtificialIntelligence
_algorithm_version = "0.1.0"

segment_descriptions = [
SegmentDescription(
segment_label="Liver",
segmented_property_category=codes.SCT.Organ,
segmented_property_type=codes.SCT.Liver,
algorithm_name=_algorithm_name,
algorithm_family=_algorithm_family,
algorithm_version=_algorithm_version,
),
SegmentDescription(
segment_label="Tumor",
segmented_property_category=codes.SCT.Tumor,
segmented_property_type=codes.SCT.Tumor,
algorithm_name=_algorithm_name,
algorithm_family=_algorithm_family,
algorithm_version=_algorithm_version,
),
]

dicom_seg_writer = DICOMSegmentationWriterOperator(segment_descriptions)
# Create the processing pipeline, by specifying the source and destination operators, and
# ensuring the output from the former matches the input of the latter, in both name and type.
self.add_flow(study_loader_op, series_selector_op, {"dicom_study_list": "dicom_study_list"})
self.add_flow(
series_selector_op, series_to_vol_op, {"study_selected_series_list": "study_selected_series_list"}
)
self.add_flow(series_to_vol_op, unetr_seg_op, {"image": "image"})
self.add_flow(series_to_vol_op, liver_tumor_seg_op, {"image": "image"})
# Add the publishing operator to save the input and seg images for Render Server.
# Note the PublisherOperator has temp impl till a proper rendering module is created.
self.add_flow(unetr_seg_op, publisher_op, {"saved_images_folder": "saved_images_folder"})
self.add_flow(liver_tumor_seg_op, publisher_op, {"saved_images_folder": "saved_images_folder"})
# Note below the dicom_seg_writer requires two inputs, each coming from a source operator.
self.add_flow(
series_selector_op, dicom_seg_writer, {"study_selected_series_list": "study_selected_series_list"}
)
self.add_flow(unetr_seg_op, dicom_seg_writer, {"seg_image": "seg_image"})
self.add_flow(liver_tumor_seg_op, dicom_seg_writer, {"seg_image": "seg_image"})

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

Expand Down
25 changes: 22 additions & 3 deletions examples/apps/ai_spleen_seg_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@

import logging

# Required for setting SegmentDescription attributes. Direct import as this is not part of App SDK package.
from pydicom.sr.codedict import codes

from monai.deploy.core import Application, resource
from monai.deploy.core.domain import Image
from monai.deploy.core.io_type import IOType
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator
from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator, SegmentDescription
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
from monai.deploy.operators.monai_bundle_inference_operator import IOMapping, MonaiBundleInferenceOperator
Expand Down Expand Up @@ -56,13 +59,29 @@ def compose(self):
# during init to provide the optional packages info, parsed from the bundle, to the packager
# for it to install the packages in the MAP docker image.
# Setting output IOType to DISK only works only for leaf operators, not the case in this example.
#
# Pertinent MONAI Bundle:
# https://github.com/Project-MONAI/model-zoo/tree/dev/models/spleen_ct_segmentation
bundle_spleen_seg_op = MonaiBundleInferenceOperator(
input_mapping=[IOMapping("image", Image, IOType.IN_MEMORY)],
output_mapping=[IOMapping("pred", Image, IOType.IN_MEMORY)],
)

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

# Create the processing pipeline, by specifying the source and destination operators, and
# ensuring the output from the former matches the input of the latter, in both name and type.
Expand Down
53 changes: 53 additions & 0 deletions examples/apps/ai_unetr_seg_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@

import logging

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

from monai.deploy.core import Application, resource
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator, SegmentDescription
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
from monai.deploy.operators.publisher_operator import PublisherOperator
Expand Down Expand Up @@ -54,6 +57,50 @@ def compose(self):
output_file="stl/multi-organs.stl", keep_largest_connected_component=False
)

# Create DICOM Seg writer providing the required segment description for each segment with
# the actual algorithm and the pertinent organ/tissue.
# The segment_label, algorithm_name, and algorithm_version are limited to 64 chars.
# https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html

_algorithm_name = "3D multi-organ segmentation from CT image"
_algorithm_family = codes.DCM.ArtificialIntelligence
_algorithm_version = "0.1.0"

# List of (Segment name, [Code menaing str]), not including background which is value of 0.
# User must provide correct codes, which can be looked at, e.g.
# https://bioportal.bioontology.org/ontologies/SNOMEDCT
# Alternatively, consult the concept and code dictionaries in PyDicom

organs = [
("Spleen",),
("Right Kidney", "Kidney"),
("Left Kideny", "Kidney"),
("Gallbladder",),
("Esophagus",),
("Liver",),
("Stomach",),
("Aorta",),
("Inferior vena cava", "InferiorVenaCava"),
("Portal and Splenic Veins", "SplenicVein"),
("Pancreas",),
("Right adrenal gland", "AdrenalGland"),
("Left adrenal gland", "AdrenalGland"),
]

segment_descriptions = [
SegmentDescription(
segment_label=organ[0],
segmented_property_category=codes.SCT.Organ,
segmented_property_type=codes.SCT.__getattr__(organ[1] if len(organ) > 1 else organ[0]),
algorithm_name=_algorithm_name,
algorithm_family=_algorithm_family,
algorithm_version=_algorithm_version,
)
for organ in organs
]

dicom_seg_writer = DICOMSegmentationWriterOperator(segment_descriptions)

# Create the processing pipeline, by specifying the source and destination operators, and
# ensuring the output from the former matches the input of the latter, in both name and type.
self.add_flow(study_loader_op, series_selector_op, {"dicom_study_list": "dicom_study_list"})
Expand All @@ -67,6 +114,12 @@ def compose(self):
# Note the PublisherOperator has temp impl till a proper rendering module is created.
self.add_flow(unetr_seg_op, publisher_op, {"saved_images_folder": "saved_images_folder"})

# Note below the dicom_seg_writer requires two inputs, each coming from a source operator.
self.add_flow(
series_selector_op, dicom_seg_writer, {"study_selected_series_list": "study_selected_series_list"}
)
self.add_flow(unetr_seg_op, dicom_seg_writer, {"seg_image": "seg_image"})

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


Expand Down
Loading

0 comments on commit 1a8cf86

Please sign in to comment.