Skip to content

Commit dacfaa9

Browse files
Add nnUNet segmentation application and dependencies
Signed-off-by: Simone Bendazzoli <[email protected]>
1 parent e7420e0 commit dacfaa9

File tree

8 files changed

+344
-2
lines changed

8 files changed

+344
-2
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2021-2023 MONAI Consortium
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an "AS IS" BASIS,
8+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
# See the License for the specific language governing permissions and
10+
# limitations under the License.
11+
12+
import os
13+
import sys
14+
15+
_current_dir = os.path.abspath(os.path.dirname(__file__))
16+
if sys.path and os.path.abspath(sys.path[0]) != _current_dir:
17+
sys.path.insert(0, _current_dir)
18+
del _current_dir
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2021-2023 MONAI Consortium
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an "AS IS" BASIS,
8+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
# See the License for the specific language governing permissions and
10+
# limitations under the License.
11+
12+
import logging
13+
14+
from app import AISpleennnUNetSegApp
15+
16+
if __name__ == "__main__":
17+
logging.info(f"Begin {__name__}")
18+
AISpleennnUNetSegApp().run()
19+
logging.info(f"End {__name__}")
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Copyright 2021-2023 MONAI Consortium
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an "AS IS" BASIS,
8+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
# See the License for the specific language governing permissions and
10+
# limitations under the License.
11+
12+
import logging
13+
from pathlib import Path
14+
15+
from pydicom.sr.codedict import codes
16+
17+
from monai.deploy.conditions import CountCondition
18+
from monai.deploy.core import AppContext, Application
19+
from monai.deploy.core.domain import Image
20+
from monai.deploy.core.io_type import IOType
21+
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
22+
from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator, SegmentDescription
23+
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
24+
from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
25+
from monai.deploy.operators.monai_bundle_inference_operator import (
26+
BundleConfigNames,
27+
IOMapping,
28+
29+
)
30+
from monai.deploy.operators.monai_nnunet_bundle_inference_operator import MonainnUNetBundleInferenceOperator
31+
from monai.deploy.operators.stl_conversion_operator import STLConversionOperator
32+
33+
34+
# @resource(cpu=1, gpu=1, memory="7Gi")
35+
# pip_packages can be a string that is a path(str) to requirements.txt file or a list of packages.
36+
# The monai pkg is not required by this class, instead by the included operators.
37+
class AISpleennnUNetSegApp(Application):
38+
"""Demonstrates inference with built-in MONAI nnUNet Bundle inference operator with DICOM files as input/output
39+
40+
This application loads a set of DICOM instances, select the appropriate series, converts the series to
41+
3D volume image, performs inference with the built-in MONAI nnUNet Bundle inference operator, including pre-processing
42+
and post-processing, save the segmentation image in a DICOM Seg OID in an instance file, and optionally the
43+
surface mesh in STL format.
44+
45+
Pertinent nnUNet MONAI Bundle:
46+
<Upload to the MONAI Model Zoo>
47+
48+
Execution Time Estimate:
49+
With a Nvidia RTXA600 48GB GPU, for an input DICOM Series of 139 instances, the execution time is around
50+
75 seconds with saving both DICOM Seg and surface mesh STL file.
51+
"""
52+
53+
def __init__(self, *args, **kwargs):
54+
"""Creates an application instance."""
55+
self._logger = logging.getLogger("{}.{}".format(__name__, type(self).__name__))
56+
super().__init__(*args, **kwargs)
57+
58+
def run(self, *args, **kwargs):
59+
# This method calls the base class to run. Can be omitted if simply calling through.
60+
self._logger.info(f"Begin {self.run.__name__}")
61+
super().run(*args, **kwargs)
62+
self._logger.info(f"End {self.run.__name__}")
63+
64+
def compose(self):
65+
"""Creates the app specific operators and chain them up in the processing DAG."""
66+
67+
logging.info(f"Begin {self.compose.__name__}")
68+
69+
# Use Commandline options over environment variables to init context.
70+
app_context: AppContext = Application.init_app_context(self.argv)
71+
app_input_path = Path(app_context.input_path)
72+
app_output_path = Path(app_context.output_path)
73+
74+
# Create the custom operator(s) as well as SDK built-in operator(s).
75+
study_loader_op = DICOMDataLoaderOperator(
76+
self, CountCondition(self, 1), input_folder=app_input_path, name="study_loader_op"
77+
)
78+
series_selector_op = DICOMSeriesSelectorOperator(self, rules=Sample_Rules_Text, name="series_selector_op")
79+
series_to_vol_op = DICOMSeriesToVolumeOperator(self, name="series_to_vol_op")
80+
81+
# Create the inference operator that supports MONAI Bundle and automates the inference.
82+
# The IOMapping labels match the input and prediction keys in the pre and post processing.
83+
# The model_name is optional when the app has only one model.
84+
# The bundle_path argument optionally can be set to an accessible bundle file path in the dev
85+
# environment, so when the app is packaged into a MAP, the operator can complete the bundle parsing
86+
# during init.
87+
88+
config_names = BundleConfigNames(config_names=["inference"]) # Same as the default
89+
90+
bundle_spleen_seg_op = MonainnUNetBundleInferenceOperator(
91+
self,
92+
input_mapping=[IOMapping("image", Image, IOType.IN_MEMORY)],
93+
output_mapping=[IOMapping("pred", Image, IOType.IN_MEMORY)],
94+
app_context=app_context,
95+
bundle_config_names=config_names,
96+
name="nnunet_bundle_spleen_seg_op",
97+
)
98+
99+
# Create DICOM Seg writer providing the required segment description for each segment with
100+
# the actual algorithm and the pertinent organ/tissue. The segment_label, algorithm_name,
101+
# and algorithm_version are of DICOM VR LO type, limited to 64 chars.
102+
# https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
103+
segment_descriptions = [
104+
SegmentDescription(
105+
segment_label="Spleen",
106+
segmented_property_category=codes.SCT.Organ,
107+
segmented_property_type=codes.SCT.Spleen,
108+
algorithm_name="volumetric (3D) segmentation of the spleen from CT image",
109+
algorithm_family=codes.DCM.ArtificialIntelligence,
110+
algorithm_version="0.3.2",
111+
)
112+
]
113+
114+
custom_tags = {"SeriesDescription": "AI generated Seg, not for clinical use."}
115+
116+
dicom_seg_writer = DICOMSegmentationWriterOperator(
117+
self,
118+
segment_descriptions=segment_descriptions,
119+
custom_tags=custom_tags,
120+
output_folder=app_output_path,
121+
name="dicom_seg_writer",
122+
)
123+
124+
# Create the processing pipeline, by specifying the source and destination operators, and
125+
# ensuring the output from the former matches the input of the latter, in both name and type.
126+
self.add_flow(study_loader_op, series_selector_op, {("dicom_study_list", "dicom_study_list")})
127+
self.add_flow(
128+
series_selector_op, series_to_vol_op, {("study_selected_series_list", "study_selected_series_list")}
129+
)
130+
self.add_flow(series_to_vol_op, bundle_spleen_seg_op, {("image", "image")})
131+
# Note below the dicom_seg_writer requires two inputs, each coming from a source operator.
132+
self.add_flow(
133+
series_selector_op, dicom_seg_writer, {("study_selected_series_list", "study_selected_series_list")}
134+
)
135+
self.add_flow(bundle_spleen_seg_op, dicom_seg_writer, {("pred", "seg_image")})
136+
# Create the surface mesh STL conversion operator and add it to the app execution flow, if needed, by
137+
# uncommenting the following couple lines.
138+
stl_conversion_op = STLConversionOperator(
139+
self, output_file=app_output_path.joinpath("stl/spleen.stl"), name="stl_conversion_op"
140+
)
141+
self.add_flow(bundle_spleen_seg_op, stl_conversion_op, {("pred", "image")})
142+
143+
logging.info(f"End {self.compose.__name__}")
144+
145+
146+
# This is a sample series selection rule in JSON, simply selecting CT series.
147+
# If the study has more than 1 CT series, then all of them will be selected.
148+
# Please see more detail in DICOMSeriesSelectorOperator.
149+
Sample_Rules_Text = """
150+
{
151+
"selections": [
152+
{
153+
"name": "CT Series",
154+
"conditions": {
155+
"StudyDescription": "(.*?)",
156+
"Modality": "(?i)CT",
157+
"SeriesDescription": "(.*?)"
158+
}
159+
}
160+
]
161+
}
162+
"""
163+
164+
if __name__ == "__main__":
165+
# Creates the app and test it standalone. When running is this mode, please note the following:
166+
# -m <model file>, for model file path
167+
# -i <DICOM folder>, for input DICOM CT series folder
168+
# -o <output folder>, for the output folder, default $PWD/output
169+
# e.g.
170+
# monai-deploy exec app.py -i input -m model/model.ts
171+
#
172+
# export HOLOSCAN_INPUT_PATH=dcm
173+
# export HOLOSCAN_MODEL_PATH=spleen_model/model.ts
174+
# export HOLOSCAN_OUTPUT_PATH="output"
175+
logging.info(f"Begin {__name__}")
176+
AISpleennnUNetSegApp().run()
177+
logging.info(f"End {__name__}")
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
%YAML 1.2
2+
# SPDX-FileCopyrightText: Copyright (c) 2022-2023 MONAI. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
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+
# http://www.apache.org/licenses/LICENSE-2.0
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+
application:
18+
title: MONAI Deploy App Package - Spleen nnUNet Seg Inference
19+
version: 1.0
20+
inputFormats: ["file"]
21+
outputFormats: ["file"]
22+
23+
resources:
24+
cpu: 1
25+
gpu: 1
26+
memory: 1Gi
27+
gpuMemory: 7Gi
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
scikit-image>=0.17.2
2+
pydicom>=2.3.0
3+
highdicom>=0.18.2
4+
SimpleITK>=2.0.0
5+
Pillow>=8.0.0
6+
numpy-stl>=2.12.0
7+
trimesh>=3.8.11
8+
nibabel>=3.2.1
9+
torch>=1.12.0
10+
monai>=1.0.0
11+
nnunetv2>=2.5.1

monai/deploy/operators/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
IOMapping
2626
ModelInfo
2727
MonaiBundleInferenceOperator
28+
MonainnUNetBundleInferenceOperator
2829
MonaiSegInferenceOperator
2930
PNGConverterOperator
3031
PublisherOperator
@@ -47,6 +48,7 @@
4748
from .dicom_utils import EquipmentInfo, ModelInfo, random_with_n_digits, save_dcm_file, write_common_modules
4849
from .inference_operator import InferenceOperator
4950
from .monai_bundle_inference_operator import BundleConfigNames, IOMapping, MonaiBundleInferenceOperator
51+
from .monai_nnunet_bundle_inference_operator import MonainnUNetBundleInferenceOperator
5052
from .monai_seg_inference_operator import MonaiSegInferenceOperator
5153
from .nii_data_loader_operator import NiftiDataLoader
5254
from .png_converter_operator import PNGConverterOperator

monai/deploy/operators/monai_bundle_inference_operator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def get_bundle_config(bundle_path, config_names):
6060
Gets the configuration parser from the specified Torchscript bundle file path.
6161
"""
6262

63-
bundle_suffixes = (".json", ".yaml", "yml") # The only supported file ext(s)
63+
bundle_suffixes = (".json", ".yaml", ".yml") # The only supported file ext(s)
6464
config_folder = "extra"
6565

6666
def _read_from_archive(archive, root_name: str, config_name: str, do_search=True):
@@ -90,7 +90,7 @@ def _read_from_archive(archive, root_name: str, config_name: str, do_search=True
9090
name_list = archive.namelist()
9191
for suffix in bundle_suffixes:
9292
for n in name_list:
93-
if (f"{config_name}{suffix}").casefold in n.casefold():
93+
if (f"{config_name}{suffix}").casefold() in n.casefold():
9494
logging.debug(f"Trying to read content of config {config_name!r} from {n!r}.")
9595
content_text = archive.read(n)
9696
break
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Copyright 2002 MONAI Consortium
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an "AS IS" BASIS,
8+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
# See the License for the specific language governing permissions and
10+
# limitations under the License.
11+
12+
from monai.deploy.operators.monai_bundle_inference_operator import MonaiBundleInferenceOperator, get_bundle_config
13+
from monai.deploy.utils.importutil import optional_import
14+
from typing import Any, Dict, Tuple, Union
15+
from monai.deploy.core import Image
16+
from pathlib import Path
17+
MONAI_UTILS = "monai.utils"
18+
nibabel, _ = optional_import("nibabel", "3.2.1")
19+
torch, _ = optional_import("torch", "1.10.2")
20+
21+
NdarrayOrTensor, _ = optional_import("monai.config", name="NdarrayOrTensor")
22+
MetaTensor, _ = optional_import("monai.data.meta_tensor", name="MetaTensor")
23+
PostFix, _ = optional_import("monai.utils.enums", name="PostFix") # For the default meta_key_postfix
24+
first, _ = optional_import("monai.utils.misc", name="first")
25+
ensure_tuple, _ = optional_import(MONAI_UTILS, name="ensure_tuple")
26+
convert_to_dst_type, _ = optional_import(MONAI_UTILS, name="convert_to_dst_type")
27+
Key, _ = optional_import(MONAI_UTILS, name="ImageMetaKey")
28+
MetaKeys, _ = optional_import(MONAI_UTILS, name="MetaKeys")
29+
SpaceKeys, _ = optional_import(MONAI_UTILS, name="SpaceKeys")
30+
Compose_, _ = optional_import("monai.transforms", name="Compose")
31+
ConfigParser_, _ = optional_import("monai.bundle", name="ConfigParser")
32+
MapTransform_, _ = optional_import("monai.transforms", name="MapTransform")
33+
SimpleInferer, _ = optional_import("monai.inferers", name="SimpleInferer")
34+
35+
Compose: Any = Compose_
36+
MapTransform: Any = MapTransform_
37+
ConfigParser: Any = ConfigParser_
38+
__all__ = ["MonainnUNetBundleInferenceOperator"]
39+
40+
41+
class MonainnUNetBundleInferenceOperator(MonaiBundleInferenceOperator):
42+
"""
43+
A specialized operator for performing inference using the MONAI nnUNet bundle.
44+
This operator extends the `MonaiBundleInferenceOperator` to support nnUNet-specific
45+
configurations and prediction logic. It initializes the nnUNet predictor and provides
46+
a method for performing inference on input data.
47+
48+
Attributes
49+
----------
50+
_nnunet_predictor : torch.nn.Module
51+
The nnUNet predictor module used for inference.
52+
53+
Methods
54+
-------
55+
_init_config(config_names)
56+
Initializes the configuration for the nnUNet bundle, including parsing the bundle
57+
configuration and setting up the nnUNet predictor.
58+
predict(data, *args, **kwargs)
59+
Performs inference on the input data using the nnUNet predictor.
60+
"""
61+
62+
def __init__(
63+
self,
64+
*args,
65+
**kwargs,
66+
):
67+
68+
69+
super().__init__(*args, **kwargs)
70+
71+
self._nnunet_predictor : torch.nn.Module = None
72+
73+
74+
def _init_config(self, config_names):
75+
76+
super()._init_config(config_names)
77+
parser = get_bundle_config(str(self._bundle_path), config_names)
78+
parser['bundle_root'] = str(Path(self._bundle_path).parent.parent.parent)
79+
self._parser = parser
80+
81+
self._nnunet_predictor = parser.get_parsed_content("network_def")
82+
83+
def predict(self, data: Any, *args, **kwargs) -> Union[Image, Any, Tuple[Any, ...], Dict[Any, Any]]:
84+
"""Predicts output using the inferer."""
85+
86+
self._nnunet_predictor.predictor.network = self._model_network
87+
#os.environ['nnUNet_def_n_proc'] = "1"
88+
return self._nnunet_predictor(torch.unsqueeze(data, 0))

0 commit comments

Comments
 (0)