Skip to content

Commit 016add6

Browse files
committed
Enabled model factory and migrated multi-AI app
Signed-off-by: M Q <mingmelvinq@nvidia.com>
1 parent 9675245 commit 016add6

File tree

18 files changed

+460
-280
lines changed

18 files changed

+460
-280
lines changed

examples/apps/ai_livertumor_seg_app/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def compose(self):
6161
series_selector_op = DICOMSeriesSelectorOperator(self, rules=Sample_Rules_Text, name="series_selector_op")
6262
series_to_vol_op = DICOMSeriesToVolumeOperator(self, name="series_to_vol_op")
6363
# Model specific inference operator, supporting MONAI transforms.
64-
liver_tumor_seg_op = LiverTumorSegOperator(self, model_path=model_path, name="seg_op")
64+
liver_tumor_seg_op = LiverTumorSegOperator(self, app_context=app_context, model_path=model_path, name="seg_op")
6565
# self, model_path=model_path, output_folder=app_output_path, name="seg_op"
6666
# )
6767

examples/apps/ai_livertumor_seg_app/livertumor_seg_operator.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import logging
1313
from pathlib import Path
1414

15-
from monai.deploy.core import ConditionType, Fragment, Operator, OperatorSpec
15+
from monai.deploy.core import AppContext, ConditionType, Fragment, Operator, OperatorSpec
1616
from monai.deploy.operators.monai_seg_inference_operator import InMemImageReader, MonaiSegInferenceOperator
1717
from monai.transforms import ( # SaveImaged,
1818
Activationsd,
@@ -57,7 +57,13 @@ class LiverTumorSegOperator(Operator):
5757
DEFAULT_OUTPUT_FOLDER = Path.cwd() / "output/saved_images_folder"
5858

5959
def __init__(
60-
self, frament: Fragment, *args, model_path: Path, output_folder: Path = DEFAULT_OUTPUT_FOLDER, **kwargs
60+
self,
61+
frament: Fragment,
62+
*args,
63+
app_context: AppContext,
64+
model_path: Path,
65+
output_folder: Path = DEFAULT_OUTPUT_FOLDER,
66+
**kwargs,
6167
):
6268
self.logger = logging.getLogger("{}.{}".format(__name__, type(self).__name__))
6369
self._input_dataset_key = "image"
@@ -67,11 +73,13 @@ def __init__(
6773
self.output_folder = output_folder
6874
self.output_folder.mkdir(parents=True, exist_ok=True)
6975
self.fragement = frament # Cache and later pass the Fragment/Application to contained operator(s)
76+
self.app_context = app_context
7077
self.input_name_image = "image"
7178
self.output_name_seg = "seg_image"
7279
self.output_name_saved_images_folder = "saved_images_folder"
7380

74-
self.fragement = frament # Cache and later pass the Fragment/Application to contained operator(s)
81+
self.fragement = frament
82+
7583
super().__init__(frament, *args, **kwargs)
7684

7785
def setup(self, spec: OperatorSpec):
@@ -109,6 +117,7 @@ def compute(self, op_input, op_output, context):
109117
pre_transforms=pre_transforms,
110118
post_transforms=post_transforms,
111119
overlap=0.6,
120+
app_context=self.app_context,
112121
model_name="",
113122
model_path=self.model_path,
114123
)
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
# from app import App
1+
import logging
22

3-
# if __name__ == "__main__":
4-
# App(do_run=True)
3+
from app import App
4+
5+
from monai.deploy.logger import load_env_log_level
6+
7+
load_env_log_level()
8+
logging.info(f"Begin {__name__}")
9+
App.run()
10+
logging.info(f"End {__name__}")

examples/apps/ai_multi_ai_app/app.py

Lines changed: 255 additions & 219 deletions
Large diffs are not rendered by default.

examples/apps/ai_pancrea_seg_app/app.py renamed to examples/apps/ai_pancreas_seg_app/app.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ class AIPancreasSegApp(Application):
4949
https://github.com/Project-MONAI/model-zoo/tree/dev/models/pancreas_ct_dints_segmentation
5050
5151
Execution Time Estimate:
52-
With a Nvidia GV100 32GB GPU, for a input of 200 DICOM instances, execution time is around 90 seconds, and
53-
for 500 instance 180 seconds.
52+
With a Nvidia GV100 32GB GPU, the execution time is around 80 seconds for an input DICOM series of 204 instances,
53+
and 160 second for a series of 515 instances.
5454
"""
5555

5656
def __init__(self, *args, **kwargs):
@@ -94,6 +94,7 @@ def compose(self):
9494
self,
9595
input_mapping=[IOMapping("image", Image, IOType.IN_MEMORY)],
9696
output_mapping=[IOMapping("pred", Image, IOType.IN_MEMORY)],
97+
app_context=app_context,
9798
bundle_config_names=config_names,
9899
bundle_path=model_path,
99100
name="bundle_seg_op",
@@ -103,15 +104,27 @@ def compose(self):
103104
# the actual algorithm and the pertinent organ/tissue. The segment_label, algorithm_name,
104105
# and algorithm_version are of DICOM VR LO type, limited to 64 chars.
105106
# https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
107+
_algorithm_name = "Pancreas CT DiNTS segmentation from CT image"
108+
_algorithm_family = codes.DCM.ArtificialIntelligence
109+
_algorithm_version = "0.3.8"
110+
106111
segment_descriptions = [
107112
SegmentDescription(
108113
segment_label="Pancreas",
109114
segmented_property_category=codes.SCT.Organ,
110115
segmented_property_type=codes.SCT.Pancreas,
111-
algorithm_name="volumetric (3D) segmentation of the pancreas from CT image",
112-
algorithm_family=codes.DCM.ArtificialIntelligence,
113-
algorithm_version="0.3.0",
114-
)
116+
algorithm_name=_algorithm_name,
117+
algorithm_family=_algorithm_family,
118+
algorithm_version=_algorithm_version,
119+
),
120+
SegmentDescription(
121+
segment_label="Tumor",
122+
segmented_property_category=codes.SCT.Tumor,
123+
segmented_property_type=codes.SCT.Tumor,
124+
algorithm_name=_algorithm_name,
125+
algorithm_family=_algorithm_family,
126+
algorithm_version=_algorithm_version,
127+
),
115128
]
116129

117130
custom_tags = {"SeriesDescription": "AI generated Seg for research use only. Not for clinical use."}

examples/apps/ai_spleen_seg_app/app.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ class AISpleenSegApp(Application):
4747
https://github.com/Project-MONAI/model-zoo/tree/dev/models/spleen_ct_segmentation
4848
4949
Execution Time Estimate:
50-
With a Nvidia GV100 32GB GPU, for a input of 500 DICOM instances, execution time is around 25 seconds.
50+
With a Nvidia GV100 32GB GPU, for an input DICOM Series of 515 instances, the execution time is around
51+
25 seconds with saving both DICOM Seg and surface mesh STL file, and 15 seconds with DICOM Seg only.
5152
"""
5253

5354
def __init__(self, *args, **kwargs):
@@ -83,15 +84,15 @@ def compose(self):
8384
# The model_name is optional when the app has only one model.
8485
# The bundle_path argument optionally can be set to an accessible bundle file path in the dev
8586
# environment, so when the app is packaged into a MAP, the operator can complete the bundle parsing
86-
# during init to provide the optional packages info, parsed from the bundle, to the packager
87-
# for it to install the packages in the MAP docker image.
87+
# during init.
8888

8989
config_names = BundleConfigNames(config_names=["inference"]) # Same as the default
9090

9191
bundle_spleen_seg_op = MonaiBundleInferenceOperator(
9292
self,
9393
input_mapping=[IOMapping("image", Image, IOType.IN_MEMORY)],
9494
output_mapping=[IOMapping("pred", Image, IOType.IN_MEMORY)],
95+
app_context=app_context,
9596
bundle_config_names=config_names,
9697
bundle_path=model_path,
9798
name="bundle_spleen_seg_op",
@@ -108,7 +109,7 @@ def compose(self):
108109
segmented_property_type=codes.SCT.Spleen,
109110
algorithm_name="volumetric (3D) segmentation of the spleen from CT image",
110111
algorithm_family=codes.DCM.ArtificialIntelligence,
111-
algorithm_version="0.1.0",
112+
algorithm_version="0.3.2",
112113
)
113114
]
114115

examples/apps/ai_unetr_seg_app/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def compose(self):
6161
series_selector_op = DICOMSeriesSelectorOperator(self, rules=Sample_Rules_Text, name="series_selector_op")
6262
series_to_vol_op = DICOMSeriesToVolumeOperator(self, name="series_to_vol_op")
6363
# Model specific inference operator, supporting MONAI transforms.
64-
seg_op = UnetrSegOperator(self, model_path=model_path, name="seg_op")
64+
seg_op = UnetrSegOperator(self, app_context=app_context, model_path=model_path, name="seg_op")
6565

6666
# Create the surface mesh STL conversion operator, for all segments
6767
stl_conversion_op = STLConversionOperator(

examples/apps/ai_unetr_seg_app/unetr_seg_operator.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from numpy import uint8
1616

17-
from monai.deploy.core import ConditionType, Fragment, Operator, OperatorSpec
17+
from monai.deploy.core import AppContext, ConditionType, Fragment, Operator, OperatorSpec
1818
from monai.deploy.operators.monai_seg_inference_operator import InMemImageReader, MonaiSegInferenceOperator
1919
from monai.transforms import (
2020
Activationsd,
@@ -55,7 +55,13 @@ class UnetrSegOperator(Operator):
5555
DEFAULT_OUTPUT_FOLDER = Path.cwd() / "output/saved_images_folder"
5656

5757
def __init__(
58-
self, frament: Fragment, *args, model_path: Path, output_folder: Path = DEFAULT_OUTPUT_FOLDER, **kwargs
58+
self,
59+
frament: Fragment,
60+
*args,
61+
app_context: AppContext,
62+
model_path: Path,
63+
output_folder: Path = DEFAULT_OUTPUT_FOLDER,
64+
**kwargs,
5965
):
6066
self.logger = logging.getLogger("{}.{}".format(__name__, type(self).__name__))
6167

@@ -66,11 +72,11 @@ def __init__(
6672
self.output_folder = output_folder
6773
self.output_folder.mkdir(parents=True, exist_ok=True)
6874
self.fragement = frament # Cache and later pass the Fragment/Application to contained operator(s)
75+
self.app_context = app_context
6976
self.input_name_image = "image"
7077
self.output_name_seg = "seg_image"
7178
self.output_name_saved_images_folder = "saved_images_folder"
7279

73-
self.fragement = frament # Cache and later pass the Fragment/Application to contained operator(s)
7480
super().__init__(frament, *args, **kwargs)
7581

7682
def setup(self, spec: OperatorSpec):
@@ -105,6 +111,7 @@ def compute(self, op_input, op_output, context):
105111
pre_transforms=pre_transforms,
106112
post_transforms=post_transforms,
107113
overlap=0.5,
114+
app_context=self.app_context,
108115
model_path=self.model_path,
109116
)
110117

examples/apps/breast_density_classifer_app/app.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,10 @@ def compose(self):
4646
study_loader_op = DICOMDataLoaderOperator(
4747
self, CountCondition(self, 1), input_folder=app_input_path, name="study_loader_op"
4848
)
49-
series_selector_op = DICOMSeriesSelectorOperator(
50-
self, rules_json_str=Sample_Rules_Text, name="series_selector_op"
51-
)
49+
series_selector_op = DICOMSeriesSelectorOperator(self, rules=Sample_Rules_Text, name="series_selector_op")
5250
series_to_vol_op = DICOMSeriesToVolumeOperator(self, name="series_to_vol_op")
5351
classifier_op = ClassifierOperator(
54-
self, output_folder=app_output_path, model_path=model_path, name="classifier_op"
52+
self, app_context=app_context, output_folder=app_output_path, model_path=model_path, name="classifier_op"
5553
)
5654
sr_writer_op = DICOMTextSRWriterOperator(
5755
self,

examples/apps/breast_density_classifer_app/breast_density_classifier_operator.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import torch
77

88
from monai.data import DataLoader, Dataset
9-
from monai.deploy.core import ConditionType, Fragment, Image, Operator, OperatorSpec
9+
from monai.deploy.core import AppContext, ConditionType, Fragment, Image, Operator, OperatorSpec
1010
from monai.deploy.operators.monai_seg_inference_operator import InMemImageReader
1111
from monai.transforms import (
1212
Activations,
@@ -42,6 +42,7 @@ def __init__(
4242
frament: Fragment,
4343
*args,
4444
model_name: Optional[str] = "",
45+
app_context: AppContext,
4546
model_path: Path = MODEL_LOCAL_PATH,
4647
output_folder: Path = DEFAULT_OUTPUT_FOLDER,
4748
**kwargs,
@@ -73,9 +74,34 @@ def __init__(
7374
# Need the path to load the models when they are not loaded in the execution context
7475
self.model_path = model_path
7576

76-
# This needs to be at the end of the constructor.
77+
# Use AppContect object for getting the loaded models
78+
self.app_context = app_context
79+
80+
self.model = self._get_model(self.app_context, self.model_path, self._model_name)
81+
7782
super().__init__(frament, *args, **kwargs)
7883

84+
def _get_model(self, app_context: AppContext, model_path: Path, model_name: str):
85+
"""Load the model with the given name from context or model path
86+
87+
Args:
88+
app_context (AppContext): The application context object holding the model(s)
89+
model_path (Path): The path to the model file, as a backup to load model directly
90+
model_name (str): The name of the model, when multiples are loaded in the context
91+
"""
92+
93+
if app_context.models:
94+
# `app_context.models.get(model_name)` returns a model instance if exists.
95+
# If model_name is not specified and only one model exists, it returns that model.
96+
model = app_context.models.get(model_name)
97+
else:
98+
model = torch.jit.load(
99+
ClassifierOperator.MODEL_LOCAL_PATH,
100+
map_location=torch.device("cuda" if torch.cuda.is_available() else "cpu"),
101+
)
102+
103+
return model
104+
79105
def setup(self, spec: OperatorSpec):
80106
"""Set up the operator named input and named output, both are in-memory objects."""
81107

examples/apps/mednist_classifier_monaideploy/mednist_classifier_monaideploy.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
from pathlib import Path
1515
from typing import Optional
1616

17+
import torch
18+
1719
from monai.deploy.conditions import CountCondition
1820
from monai.deploy.core import AppContext, Application, ConditionType, Fragment, Image, Operator, OperatorSpec
1921
from monai.deploy.operators.dicom_text_sr_writer_operator import DICOMTextSRWriterOperator, EquipmentInfo, ModelInfo
@@ -99,6 +101,7 @@ def __init__(
99101
self,
100102
frament: Fragment,
101103
*args,
104+
app_context: AppContext,
102105
model_name: Optional[str] = "",
103106
model_path: Path = MODEL_LOCAL_PATH,
104107
output_folder: Path = DEFAULT_OUTPUT_FOLDER,
@@ -130,10 +133,33 @@ def __init__(
130133
self._model_name = model_name.strip() if isinstance(model_name, str) else ""
131134
# Need the path to load the models when they are not loaded in the execution context
132135
self.model_path = model_path
136+
self.app_context = app_context
137+
self.model = self._get_model(self.app_context, self.model_path, self._model_name)
133138

134139
# This needs to be at the end of the constructor.
135140
super().__init__(frament, *args, **kwargs)
136141

142+
def _get_model(self, app_context: AppContext, model_path: Path, model_name: str):
143+
"""Load the model with the given name from context or model path
144+
145+
Args:
146+
app_context (AppContext): The application context object holding the model(s)
147+
model_path (Path): The path to the model file, as a backup to load model directly
148+
model_name (str): The name of the model, when multiples are loaded in the context
149+
"""
150+
151+
if app_context.models:
152+
# `app_context.models.get(model_name)` returns a model instance if exists.
153+
# If model_name is not specified and only one model exists, it returns that model.
154+
model = app_context.models.get(model_name)
155+
else:
156+
model = torch.jit.load(
157+
MedNISTClassifierOperator.MODEL_LOCAL_PATH,
158+
map_location=torch.device("cuda" if torch.cuda.is_available() else "cpu"),
159+
)
160+
161+
return model
162+
137163
def setup(self, spec: OperatorSpec):
138164
"""Set up the operator named input and named output, both are in-memory objects."""
139165

@@ -157,12 +183,8 @@ def compute(self, op_input, op_output, context):
157183
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
158184
image_tensor = image_tensor.to(device)
159185

160-
# Need to get the model from context, when it is re-implemented, and for now, load it directly here.
161-
# model = context.models.get() # get a TorchScriptModel object
162-
model = torch.jit.load(self.model_path, map_location=device)
163-
164186
with torch.no_grad():
165-
outputs = model(image_tensor)
187+
outputs = self.model(image_tensor)
166188

167189
_, output_classes = outputs.max(dim=1)
168190

@@ -189,7 +211,7 @@ def compose(self):
189211
model_path = Path(app_context.model_path)
190212
load_pil_op = LoadPILOperator(self, CountCondition(self, 1), input_folder=app_input_path, name="pil_loader_op")
191213
classifier_op = MedNISTClassifierOperator(
192-
self, output_folder=app_output_path, model_path=model_path, name="classifier_op"
214+
self, app_context=app_context, output_folder=app_output_path, model_path=model_path, name="classifier_op"
193215
)
194216

195217
my_model_info = ModelInfo("MONAI WG Trainer", "MEDNIST Classifier", "0.1", "xyz")

monai/deploy/core/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,17 @@
3838
from .runtime_env import RuntimeEnv
3939

4040
# from .resource import resource
41+
42+
43+
# Create function to add to the Application class
44+
def load_models(modle_path: str):
45+
"""_summary_
46+
47+
Args:
48+
modle_path (str): _description_
49+
"""
50+
51+
return ModelFactory.create(modle_path)
52+
53+
54+
Application.load_models = load_models

0 commit comments

Comments
 (0)