Skip to content

Commit 84af3e1

Browse files
committed
Migrated stl writer and the liver tumor seg example.
Signed-off-by: M Q <mingmelvinq@nvidia.com>
1 parent ccaf1de commit 84af3e1

File tree

4 files changed

+111
-48
lines changed

4 files changed

+111
-48
lines changed

examples/apps/ai_livertumor_seg_app/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
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+
112
import os
213
import sys
314

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
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+
112
from app import AILiverTumorApp
213

314
if __name__ == "__main__":
4-
AILiverTumorApp(do_run=True)
15+
AILiverTumorApp().run()

examples/apps/ai_livertumor_seg_app/app.py

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2021 MONAI Consortium
1+
# Copyright 2021-2023 MONAI Consortium
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -10,18 +10,23 @@
1010
# limitations under the License.
1111

1212
import logging
13+
from pathlib import Path
1314

1415
from livertumor_seg_operator import LiverTumorSegOperator
1516

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

19-
from monai.deploy.core import Application, resource
20+
from monai.deploy.conditions import CountCondition
21+
from monai.deploy.core import AppContext, Application
22+
from monai.deploy.logger import load_env_log_level
2023
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
2124
from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator, SegmentDescription
2225
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
2326
from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
24-
from monai.deploy.operators.publisher_operator import PublisherOperator
27+
from monai.deploy.operators.stl_conversion_operator import STLConversionOperator
28+
29+
# from monai.deploy.operators.publisher_operator import PublisherOperator
2530

2631
# This is a sample series selection rule in JSON, simply selecting CT series.
2732
# If the study has more than 1 CT series, then all of them will be selected.
@@ -45,7 +50,7 @@
4550
"""
4651

4752

48-
@resource(cpu=1, gpu=1, memory="7Gi")
53+
# @resource(cpu=1, gpu=1, memory="7Gi")
4954
# pip_packages can be a string that is a path(str) to requirements.txt file or a list of packages.
5055
# The MONAI pkg is not required by this class, instead by the included operators.
5156
class AILiverTumorApp(Application):
@@ -64,16 +69,27 @@ def run(self, *args, **kwargs):
6469
def compose(self):
6570
"""Creates the app specific operators and chain them up in the processing DAG."""
6671

67-
self._logger.debug(f"Begin {self.compose.__name__}")
72+
self._logger.info(f"Begin {self.compose.__name__}")
73+
app_context = AppContext({}) # Let it figure out all the attributes without overriding
74+
app_input_path = Path(app_context.input_path)
75+
app_output_path = Path(app_context.output_path)
76+
model_path = Path(app_context.model_path)
77+
78+
self._logger.info(f"App input and output path: {app_input_path}, {app_output_path}")
79+
6880
# Creates the custom operator(s) as well as SDK built-in operator(s).
69-
study_loader_op = DICOMDataLoaderOperator()
70-
series_selector_op = DICOMSeriesSelectorOperator(rules=Sample_Rules_Text)
71-
series_to_vol_op = DICOMSeriesToVolumeOperator()
81+
study_loader_op = DICOMDataLoaderOperator(
82+
self, CountCondition(self, 1), input_folder=app_input_path, name="dcm_loader_op"
83+
)
84+
series_selector_op = DICOMSeriesSelectorOperator(self, rules=Sample_Rules_Text, name="series_selector_op")
85+
series_to_vol_op = DICOMSeriesToVolumeOperator(self, name="series_to_vol_op")
7286
# Model specific inference operator, supporting MONAI transforms.
73-
liver_tumor_seg_op = LiverTumorSegOperator()
87+
liver_tumor_seg_op = LiverTumorSegOperator(
88+
self, model_path=model_path, output_folder=app_output_path, name="seg_op"
89+
)
7490

7591
# Create the publisher operator
76-
publisher_op = PublisherOperator()
92+
stl_op = STLConversionOperator(self, output_file=app_output_path.joinpath("stl/mesh.stl"), name="stl_op")
7793

7894
# Create DICOM Seg writer providing the required segment description for each segment with
7995
# the actual algorithm and the pertinent organ/tissue.
@@ -105,24 +121,27 @@ def compose(self):
105121
),
106122
]
107123

108-
dicom_seg_writer = DICOMSegmentationWriterOperator(segment_descriptions)
124+
dicom_seg_writer = DICOMSegmentationWriterOperator(
125+
self, segment_descriptions=segment_descriptions, output_folder=app_output_path, name="dcm_seg_writer_op"
126+
)
109127
# Create the processing pipeline, by specifying the source and destination operators, and
110128
# ensuring the output from the former matches the input of the latter, in both name and type.
111-
self.add_flow(study_loader_op, series_selector_op, {"dicom_study_list": "dicom_study_list"})
129+
self.add_flow(study_loader_op, series_selector_op, {("dicom_study_list", "dicom_study_list")})
112130
self.add_flow(
113-
series_selector_op, series_to_vol_op, {"study_selected_series_list": "study_selected_series_list"}
131+
series_selector_op, series_to_vol_op, {("study_selected_series_list", "study_selected_series_list")}
114132
)
115-
self.add_flow(series_to_vol_op, liver_tumor_seg_op, {"image": "image"})
116-
# Add the publishing operator to save the input and seg images for Render Server.
117-
# Note the PublisherOperator has temp impl till a proper rendering module is created.
118-
self.add_flow(liver_tumor_seg_op, publisher_op, {"saved_images_folder": "saved_images_folder"})
133+
self.add_flow(series_to_vol_op, liver_tumor_seg_op, {("image", "image")})
134+
119135
# Note below the dicom_seg_writer requires two inputs, each coming from a source operator.
120136
self.add_flow(
121-
series_selector_op, dicom_seg_writer, {"study_selected_series_list": "study_selected_series_list"}
137+
series_selector_op, dicom_seg_writer, {("study_selected_series_list", "study_selected_series_list")}
122138
)
123-
self.add_flow(liver_tumor_seg_op, dicom_seg_writer, {"seg_image": "seg_image"})
139+
self.add_flow(liver_tumor_seg_op, dicom_seg_writer, {("seg_image", "seg_image")})
140+
141+
# Add the stl mesh operator to save the mesh in stl format.
142+
self.add_flow(liver_tumor_seg_op, stl_op, {("seg_image", "image")})
124143

125-
self._logger.debug(f"End {self.compose.__name__}")
144+
self._logger.info(f"End {self.compose.__name__}")
126145

127146

128147
if __name__ == "__main__":
@@ -134,5 +153,4 @@ def compose(self):
134153
# python3 app.py -i input -m model/model.ts
135154
#
136155
logging.basicConfig(level=logging.DEBUG)
137-
app_instance = AILiverTumorApp() # Optional params' defaults are fine.
138-
app_instance.run()
156+
AILiverTumorApp().run()

examples/apps/ai_livertumor_seg_app/livertumor_seg_operator.py

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2021 MONAI Consortium
1+
# Copyright 2021-2023 MONAI Consortium
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -10,11 +10,11 @@
1010
# limitations under the License.
1111

1212
import logging
13+
from pathlib import Path
1314

1415
from numpy import uint8
1516

16-
import monai.deploy.core as md
17-
from monai.deploy.core import DataPath, ExecutionContext, Image, InputContext, IOType, Operator, OutputContext
17+
from monai.deploy.core import ConditionType, Fragment, Operator, OperatorSpec
1818
from monai.deploy.operators.monai_seg_inference_operator import InMemImageReader, MonaiSegInferenceOperator
1919
from monai.transforms import (
2020
Activationsd,
@@ -30,10 +30,10 @@
3030
)
3131

3232

33-
@md.input("image", Image, IOType.IN_MEMORY)
34-
@md.output("seg_image", Image, IOType.IN_MEMORY)
35-
@md.output("saved_images_folder", DataPath, IOType.DISK)
36-
@md.env(pip_packages=["monai>=1.0.0", "torch>=1.5", "numpy>=1.21", "nibabel"])
33+
# @md.input("image", Image, IOType.IN_MEMORY)
34+
# @md.output("seg_image", Image, IOType.IN_MEMORY)
35+
# @md.output("saved_images_folder", DataPath, IOType.DISK)
36+
# @md.env(pip_packages=["monai>=1.0.0", "torch>=1.5", "numpy>=1.21", "nibabel"])
3737
class LiverTumorSegOperator(Operator):
3838
"""Performs liver and tumor segmentation using a DL model with an image converted from a DICOM CT series.
3939
@@ -49,27 +49,48 @@ class LiverTumorSegOperator(Operator):
4949
Note that the App SDK InMemImageReader, derived from MONAI ImageReader, is passed to LoadImaged.
5050
This derived reader is needed to parse the in memory image object, and return the expected data structure.
5151
Loading of the model, and predicting using in-proc PyTorch inference is done by MonaiSegInferenceOperator.
52+
53+
Named Input:
54+
image: Image object.
55+
56+
Named Outputs:
57+
seg_image: Image object of the segmentation object.
58+
saved_images_folder: Path to the folder with intermediate image output, not requiring a downstream receiver.
5259
"""
5360

54-
def __init__(self):
61+
DEFAULT_OUTPUT_FOLDER = Path.cwd() / "saved_images_folder"
62+
63+
def __init__(self, frament: Fragment, *args, model_path: Path, output_folder: Path = None, **kwargs):
5564
self.logger = logging.getLogger("{}.{}".format(__name__, type(self).__name__))
56-
super().__init__()
5765
self._input_dataset_key = "image"
5866
self._pred_dataset_key = "pred"
5967

60-
def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):
61-
input_image = op_input.get("image")
68+
self.model_path = model_path
69+
self.output_folder = output_folder if output_folder else LiverTumorSegOperator.DEFAULT_OUTPUT_FOLDER
70+
self.output_folder.mkdir(parents=True, exist_ok=True)
71+
self.fragement = frament # Cache and later pass the Fragment/Application to contained operator(s)
72+
self.input_name_image = "image"
73+
self.output_name_seg = "seg_image"
74+
75+
self.fragement = frament # Cache and later pass the Fragment/Application to contained operator(s)
76+
super().__init__(frament, *args, **kwargs)
77+
78+
def setup(self, spec: OperatorSpec):
79+
spec.input(self.input_name_image)
80+
spec.output(self.output_name_seg)
81+
82+
def compute(self, op_input, op_output, context):
83+
input_image = op_input.receive(self.input_name_image)
6284
if not input_image:
6385
raise ValueError("Input image is not found.")
6486

6587
# Get the output path from the execution context for saving file(s) to app output.
66-
# Without using this path, operator would be saving files to its designated path, e.g.
67-
# $PWD/.monai_workdir/operators/6048d75a-5de1-45b9-8bd1-2252f88827f2/0/output
68-
op_output_folder_name = DataPath("saved_images_folder")
69-
op_output.set(op_output_folder_name, "saved_images_folder")
70-
op_output_folder_path = op_output.get("saved_images_folder").path
71-
op_output_folder_path.mkdir(parents=True, exist_ok=True)
72-
print(f"Operator output folder path: {op_output_folder_path}")
88+
# Without using this path, operator would be saving files to its designated path
89+
# op_output_folder_name = "saved_images_folder"
90+
# op_output.set(op_output_folder_name, "saved_images_folder")
91+
# op_output_folder_path = Path(op_output_folder_name) # op_output.get("saved_images_folder").path
92+
# op_output_folder_path.mkdir(parents=True, exist_ok=True)
93+
# print(f"Operator output folder path: {op_output_folder_path}")
7394

7495
# This operator gets an in-memory Image object, so a specialized ImageReader is needed.
7596
_reader = InMemImageReader(input_image)
@@ -78,28 +99,30 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
7899
# They are both saved in the same subfolder of the application output folder, with names
79100
# distinguished by postfix. They can also be save in different subfolder if need be.
80101
# These images files can then be packaged for rendering.
81-
pre_transforms = self.pre_process(_reader, op_output_folder_path)
82-
post_transforms = self.post_process(pre_transforms, op_output_folder_path)
102+
pre_transforms = self.pre_process(_reader, str(self.output_folder))
103+
post_transforms = self.post_process(pre_transforms, str(self.output_folder))
83104

84105
# Delegates inference and saving output to the built-in operator.
85106
infer_operator = MonaiSegInferenceOperator(
86-
(
107+
self.fragement,
108+
roi_size=(
87109
160,
88110
160,
89111
160,
90112
),
91-
pre_transforms,
92-
post_transforms,
113+
pre_transforms=pre_transforms,
114+
post_transforms=post_transforms,
93115
overlap=0.6,
94116
model_name="",
117+
model_path=self.model_path,
95118
)
96119

97120
# Setting the keys used in the dictironary based transforms may change.
98121
infer_operator.input_dataset_key = self._input_dataset_key
99122
infer_operator.pred_dataset_key = self._pred_dataset_key
100123

101-
# Now let the built-in operator handles the work with the I/O spec and execution context.
102-
infer_operator.compute(op_input, op_output, context)
124+
# Now let the built-in operator handle the work with the I/O spec and execution context.
125+
op_output.emit(infer_operator.compute_impl(input_image, context), self.output_name_seg)
103126

104127
def pre_process(self, img_reader, out_dir: str = "./input_images") -> Compose:
105128
"""Composes transforms for preprocessing input before predicting on a model."""

0 commit comments

Comments
 (0)