Skip to content

Commit 913b392

Browse files
authored
Add MONAI Bundle inference operator and update example app (#303)
* WIP MONAI Bundle inference operator. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Further simplified the code requiring model name only for bundle. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Check in the bundle operator and multipl model support. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Added changes for bundle operator and app. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Fix styling check errors. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Fix flake8 check error. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Correct all MyPy type checking errors and improve inline doc. The base inference operator needs to adjust the function signatures due to the expanded use of types resulting from the intro if bundle inference operator. The new version of MyPy also seems to be stricter on types. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * MyPy on Git is complaining things that are not found in local checking. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Update per review comments. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Rebased for the requirements.txt file. Not req'ed for docs/requirements but for consistency. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Add the notebook for creating app with MONAI Bundle inference operator. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Updating the notebook descriptions. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Update and make consistent the user guides Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
1 parent 971a567 commit 913b392

18 files changed

+1717
-187
lines changed

docs/requirements.txt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Sphinx==4.1.2
22
sphinx-autobuild==2021.3.14
33
myst-parser==0.15.2
4-
numpy==1.21 # CVE-2021-33430
4+
numpy==1.21.2 # CVE-2021-33430
55
matplotlib==3.3.4
66
ipywidgets==7.6.4
77
pandas==1.1.5
@@ -19,10 +19,13 @@ docutils==0.16 # 0.17 causes error. https://github.com/executablebooks/MyST-Par
1919
pydata_sphinx_theme==0.6.3
2020
sphinxemoji==0.1.8
2121
scipy
22-
scikit-image
22+
scikit-image>=0.17.2
2323
plotly
24-
nibabel
25-
monai
24+
nibabel>=3.2.1
25+
monai>=0.9.0
26+
torch>=1.10.0
27+
numpy-stl>=2.12.0
28+
trimesh>=3.8.11
2629
pydicom
2730
sphinx-autodoc-typehints==1.12.0
2831
sphinxcontrib-applehelp==1.0.2
@@ -31,4 +34,4 @@ sphinxcontrib-htmlhelp==2.0.0
3134
sphinxcontrib-jsmath==1.0.1
3235
sphinxcontrib-qthelp==1.0.3
3336
sphinxcontrib-serializinghtml==1.1.5
34-
sphinxcontrib-mermaid==0.7.1
37+
sphinxcontrib-mermaid==0.7.1

docs/source/getting_started/examples.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
<https://github.com/Project-MONAI/monai-deploy-app-sdk/tree/main/examples/apps> has example apps that you can see.
66

77
- ai_spleen_seg_app
8+
- ai_livertumor_seg_app
89
- ai_unetr_seg_app
910
- dicom_series_to_image_app
1011
- mednist_classifier_monaideploy
1112
- simple_imaging_app
13+
- deply_app_on_aarch64

docs/source/getting_started/tutorials/03_segmentation_app.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jupyter-lab
1515
```
1616

1717
## Executing from Jupyter Notebook
18-
18+
Please note that the example code used in the Jupyter Notebook is based on an earlier version of the segmentation application, hence not the same as the latest source code on Github, e.g. not using MONAI Bundle inference operator.
1919
```{toctree}
2020
:maxdepth: 4
2121
@@ -43,7 +43,7 @@ jupyter-lab
4343
```
4444

4545
## Executing from Shell
46-
46+
Please note that this part of the example uses the latest application source code on Github, as well as the corresponding test data.
4747
```bash
4848
# Clone the github project (the latest version of main branch only)
4949
git clone --branch main --depth 1 https://github.com/Project-MONAI/monai-deploy-app-sdk.git
@@ -53,17 +53,18 @@ cd monai-deploy-app-sdk
5353
# Install monai-deploy-app-sdk package
5454
pip install monai-deploy-app-sdk
5555

56-
# Download/Extract ai_spleen_seg_data zip file from https://drive.google.com/file/d/1GC_N8YQk_mOWN02oOzAU_2YDmNRWk--n/view?usp=sharing
56+
# Download/Extract ai_spleen_bundle_data zip file from https://drive.google.com/file/d/1cJq0iQh_yzYIxVElSlVa141aEmHZADJh/view?usp=sharing
5757

58-
# Download ai_spleen_seg_data.zip
58+
# Download ai_spleen_bundle_data.zip
5959
pip install gdown
60-
gdown https://drive.google.com/uc?id=1GC_N8YQk_mOWN02oOzAU_2YDmNRWk--n
60+
gdown https://drive.google.com/uc?id=1cJq0iQh_yzYIxVElSlVa141aEmHZADJh
6161

62-
# After downloading ai_spleen_seg_data.zip from the web browser or using gdown,
63-
unzip -o ai_spleen_seg_data_updated_1203.zip
62+
# After downloading ai_spleen_bundle_data.zip from the web browser or using gdown,
63+
unzip -o ai_spleen_bundle_data.zip
6464

65-
# Install necessary packages from the app
66-
pip install monai pydicom SimpleITK Pillow nibabel
65+
# Install necessary packages from the app; note that numpy-stl and trimesh are only
66+
# needed if the application uses the STL Conversion Operator
67+
pip install monai pydicom SimpleITK Pillow nibabel scikit-image numpy-stl trimesh
6768

6869
# Local execution of the app
6970
python examples/apps/ai_spleen_seg_app/app.py -i dcm/ -o output -m model.ts

docs/source/getting_started/tutorials/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
03_segmentation_app
1010
04_mis_tutorial
1111
05_full_tutorial
12+
06_monai_bundle_app
1213
```

examples/apps/ai_livertumor_seg_app/livertumor_seg_operator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
9494
pre_transforms,
9595
post_transforms,
9696
overlap=0.6,
97+
model_name="",
9798
)
9899

99100
# Setting the keys used in the dictironary based transforms may change.

examples/apps/ai_spleen_seg_app/app.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2021 MONAI Consortium
1+
# Copyright 2021-2022 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
@@ -11,14 +11,16 @@
1111

1212
import logging
1313

14-
from spleen_seg_operator import SpleenSegOperator
15-
1614
from monai.deploy.core import Application, resource
15+
from monai.deploy.core.domain import Image
16+
from monai.deploy.core.io_type import IOType
1717
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
1818
from monai.deploy.operators.dicom_seg_writer_operator import DICOMSegmentationWriterOperator
1919
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
2020
from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
21-
from monai.deploy.operators.stl_conversion_operator import STLConversionOperator
21+
from monai.deploy.operators.monai_bundle_inference_operator import IOMapping, MonaiBundleInferenceOperator
22+
23+
# from monai.deploy.operators.stl_conversion_operator import STLConversionOperator # import as needed.
2224

2325

2426
@resource(cpu=1, gpu=1, memory="7Gi")
@@ -32,42 +34,54 @@ def __init__(self, *args, **kwargs):
3234

3335
def run(self, *args, **kwargs):
3436
# This method calls the base class to run. Can be omitted if simply calling through.
35-
self._logger.debug(f"Begin {self.run.__name__}")
37+
self._logger.info(f"Begin {self.run.__name__}")
3638
super().run(*args, **kwargs)
37-
self._logger.debug(f"End {self.run.__name__}")
39+
self._logger.info(f"End {self.run.__name__}")
3840

3941
def compose(self):
4042
"""Creates the app specific operators and chain them up in the processing DAG."""
4143

42-
self._logger.debug(f"Begin {self.compose.__name__}")
44+
logging.info(f"Begin {self.compose.__name__}")
4345

44-
# Creates the custom operator(s) as well as SDK built-in operator(s).
46+
# Create the custom operator(s) as well as SDK built-in operator(s).
4547
study_loader_op = DICOMDataLoaderOperator()
4648
series_selector_op = DICOMSeriesSelectorOperator(Sample_Rules_Text)
4749
series_to_vol_op = DICOMSeriesToVolumeOperator()
48-
# Model specific inference operator, supporting MONAI transforms.
49-
spleen_seg_op = SpleenSegOperator()
50+
51+
# Create the inference operator that supports MONAI Bundle and automates the inference.
52+
# The IOMapping labels match the input and prediction keys in the pre and post processing.
53+
# The model_name is optional when the app has only one model.
54+
# The bundle_path argument optionally can be set to an accessible bundle file path in the dev
55+
# environment, so when the app is packaged into a MAP, the operator can complete the bundle parsing
56+
# during init to provide the optional packages info, parsed from the bundle, to the packager
57+
# for it to install the packages in the MAP docker image.
58+
# Setting output IOType to DISK only works only for leaf operators, not the case in this example.
59+
bundle_spleen_seg_op = MonaiBundleInferenceOperator(
60+
input_mapping=[IOMapping("image", Image, IOType.IN_MEMORY)],
61+
output_mapping=[IOMapping("pred", Image, IOType.IN_MEMORY)],
62+
)
63+
5064
# Create DICOM Seg writer with segment label name in a string list
5165
dicom_seg_writer = DICOMSegmentationWriterOperator(seg_labels=["Spleen"])
52-
# Create the surface mesh STL conversion operator
53-
stl_conversion_op = STLConversionOperator(output_file="stl/spleen.stl")
5466

5567
# Create the processing pipeline, by specifying the upstream and downstream operators, and
5668
# ensuring the output from the former matches the input of the latter, in both name and type.
5769
self.add_flow(study_loader_op, series_selector_op, {"dicom_study_list": "dicom_study_list"})
5870
self.add_flow(
5971
series_selector_op, series_to_vol_op, {"study_selected_series_list": "study_selected_series_list"}
6072
)
61-
self.add_flow(series_to_vol_op, spleen_seg_op, {"image": "image"})
73+
self.add_flow(series_to_vol_op, bundle_spleen_seg_op, {"image": "image"})
6274
# Note below the dicom_seg_writer requires two inputs, each coming from a upstream operator.
6375
self.add_flow(
6476
series_selector_op, dicom_seg_writer, {"study_selected_series_list": "study_selected_series_list"}
6577
)
66-
self.add_flow(spleen_seg_op, dicom_seg_writer, {"seg_image": "seg_image"})
67-
# Add the STL conversion operator as another leaf operator taking as input the seg image.
68-
self.add_flow(spleen_seg_op, stl_conversion_op, {"seg_image": "image"})
78+
self.add_flow(bundle_spleen_seg_op, dicom_seg_writer, {"pred": "seg_image"})
79+
# Create the surface mesh STL conversion operator and add it to the app execution flow, if needed, by
80+
# uncommenting the following couple lines.
81+
# stl_conversion_op = STLConversionOperator(output_file="stl/spleen.stl")
82+
# self.add_flow(bundle_spleen_seg_op, stl_conversion_op, {"pred": "image"})
6983

70-
self._logger.debug(f"End {self.compose.__name__}")
84+
logging.info(f"End {self.compose.__name__}")
7185

7286

7387
# This is a sample series selection rule in JSON, simply selecting CT series.
@@ -94,8 +108,7 @@ def compose(self):
94108
# -i <DICOM folder>, for input DICOM CT series folder
95109
# -o <output folder>, for the output folder, default $PWD/output
96110
# e.g.
97-
# python3 app.py -i input -m model/model.ts
111+
# monai-deploy exec app.py -i input -m model/model.ts
98112
#
99113
logging.basicConfig(level=logging.DEBUG)
100-
app_instance = AISpleenSegApp() # Optional params' defaults are fine.
101-
app_instance.run()
114+
app_instance = AISpleenSegApp(do_run=True)

examples/apps/ai_spleen_seg_app/spleen_seg_operator.py

Lines changed: 0 additions & 117 deletions
This file was deleted.

monai/deploy/operators/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
.. autosummary::
1313
:toctree: _autosummary
1414
15+
BundleConfigNames
1516
ClaraVizOperator
1617
DICOMDataLoaderOperator
1718
DICOMSegmentationWriterOperator
1819
DICOMSeriesSelectorOperator
1920
DICOMSeriesToVolumeOperator
2021
DICOMTextSRWriterOperator
2122
InferenceOperator
23+
IOMapping
24+
MonaiBundleInferenceOperator
2225
MonaiSegInferenceOperator
2326
PNGConverterOperator
2427
PublisherOperator
@@ -33,6 +36,7 @@
3336
from .dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
3437
from .dicom_text_sr_writer_operator import DICOMTextSRWriterOperator, EquipmentInfo, ModelInfo
3538
from .inference_operator import InferenceOperator
39+
from .monai_bundle_inference_operator import BundleConfigNames, IOMapping, MonaiBundleInferenceOperator
3640
from .monai_seg_inference_operator import MonaiSegInferenceOperator
3741
from .png_converter_operator import PNGConverterOperator
3842
from .publisher_operator import PublisherOperator

monai/deploy/operators/inference_operator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# limitations under the License.
1111

1212
from abc import abstractmethod
13-
from typing import Any, Union
13+
from typing import Any, Dict, Tuple, Union
1414

1515
from monai.deploy.core import ExecutionContext, Image, InputContext, Operator, OutputContext
1616

@@ -27,7 +27,7 @@ def __init__(self, *args, **kwargs):
2727
super().__init__()
2828

2929
@abstractmethod
30-
def pre_process(self, data: Any) -> Union[Image, Any]:
30+
def pre_process(self, data: Any, *args, **kwargs) -> Union[Image, Any, Tuple[Any, ...], Dict[Any, Any]]:
3131
"""Transforms input before being used for predicting on a model.
3232
3333
This method must be overridden by a derived class.
@@ -50,7 +50,7 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
5050
pass
5151

5252
@abstractmethod
53-
def predict(self, data: Any) -> Union[Image, Any]:
53+
def predict(self, data: Any, *args, **kwargs) -> Union[Image, Any, Tuple[Any, ...], Dict[Any, Any]]:
5454
"""Predicts results using the models(s) with input tensors.
5555
5656
This method must be overridden by a derived class.
@@ -61,7 +61,7 @@ def predict(self, data: Any) -> Union[Image, Any]:
6161
raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
6262

6363
@abstractmethod
64-
def post_process(self, data: Any) -> Union[Image, Any]:
64+
def post_process(self, data: Any, *args, **kwargs) -> Union[Image, Any, Tuple[Any, ...], Dict[Any, Any]]:
6565
"""Transform the prediction results from the model(s).
6666
6767
This method must be overridden by a derived class.

0 commit comments

Comments
 (0)