Skip to content

Commit a1eb2d0

Browse files
committed
Updated Jupyter Notebook and example apps to use the new operator APIs.
Signed-off-by: mmelqin <[email protected]>
1 parent 8285415 commit a1eb2d0

File tree

9 files changed

+97
-51
lines changed

9 files changed

+97
-51
lines changed

examples/apps/ai_livertumor_seg_app/app.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,19 @@ def compose(self):
6161
# Create the processing pipeline, by specifying the upstream and downstream operators, and
6262
# ensuring the output from the former matches the input of the latter, in both name and type.
6363
self.add_flow(study_loader_op, series_selector_op, {"dicom_study_list": "dicom_study_list"})
64-
self.add_flow(series_selector_op, series_to_vol_op, {"dicom_series": "dicom_series"})
64+
self.add_flow(
65+
series_selector_op, series_to_vol_op, {"study_selected_series_list": "study_selected_series_list"}
66+
)
6567
self.add_flow(series_to_vol_op, unetr_seg_op, {"image": "image"})
6668
# Add the publishing operator to save the input and seg images for Render Server.
6769
# Note the PublisherOperator has temp impl till a proper rendering module is created.
6870
self.add_flow(unetr_seg_op, publisher_op, {"saved_images_folder": "saved_images_folder"})
6971
# Note below the dicom_seg_writer requires two inputs, each coming from a upstream operator.
7072
# Also note that the DICOMSegmentationWriterOperator may throw exception with some inputs.
7173
# Bug has been created to track the issue.
72-
self.add_flow(series_selector_op, dicom_seg_writer, {"dicom_series": "dicom_series"})
74+
self.add_flow(
75+
series_selector_op, dicom_seg_writer, {"study_selected_series_list": "study_selected_series_list"}
76+
)
7377
self.add_flow(unetr_seg_op, dicom_seg_writer, {"seg_image": "seg_image"})
7478

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

examples/apps/ai_spleen_seg_app/app.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ def compose(self):
3939
"""Creates the app specific operators and chain them up in the processing DAG."""
4040

4141
self._logger.debug(f"Begin {self.compose.__name__}")
42+
4243
# Creates the custom operator(s) as well as SDK built-in operator(s).
4344
study_loader_op = DICOMDataLoaderOperator()
44-
series_selector_op = DICOMSeriesSelectorOperator()
45+
series_selector_op = DICOMSeriesSelectorOperator(Sample_Rules_Text)
4546
series_to_vol_op = DICOMSeriesToVolumeOperator()
4647
# Model specific inference operator, supporting MONAI transforms.
4748
spleen_seg_op = SpleenSegOperator()
@@ -51,15 +52,37 @@ def compose(self):
5152
# Create the processing pipeline, by specifying the upstream and downstream operators, and
5253
# ensuring the output from the former matches the input of the latter, in both name and type.
5354
self.add_flow(study_loader_op, series_selector_op, {"dicom_study_list": "dicom_study_list"})
54-
self.add_flow(series_selector_op, series_to_vol_op, {"dicom_series": "dicom_series"})
55+
self.add_flow(
56+
series_selector_op, series_to_vol_op, {"study_selected_series_list": "study_selected_series_list"}
57+
)
5558
self.add_flow(series_to_vol_op, spleen_seg_op, {"image": "image"})
5659
# Note below the dicom_seg_writer requires two inputs, each coming from a upstream operator.
57-
self.add_flow(series_selector_op, dicom_seg_writer, {"dicom_series": "dicom_series"})
60+
self.add_flow(
61+
series_selector_op, dicom_seg_writer, {"study_selected_series_list": "study_selected_series_list"}
62+
)
5863
self.add_flow(spleen_seg_op, dicom_seg_writer, {"seg_image": "seg_image"})
5964

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

6267

68+
# This is a sample series selection rule in JSON, simply selecting CT series.
69+
# If the study has more than 1 CT series, then all of them will be selected.
70+
# Please see more detail in DICOMSeriesSelectorOperator.
71+
Sample_Rules_Text = """
72+
{
73+
"selections": [
74+
{
75+
"name": "CT Series",
76+
"conditions": {
77+
"StudyDescription": "(.*?)",
78+
"Modality": "(?i)CT",
79+
"SeriesDescription": "(.*?)"
80+
}
81+
}
82+
]
83+
}
84+
"""
85+
6386
if __name__ == "__main__":
6487
# Creates the app and test it standalone. When running is this mode, please note the following:
6588
# -m <model file>, for model file path

examples/apps/ai_unetr_seg_app/app.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,19 @@ def compose(self):
7171
# Create the processing pipeline, by specifying the upstream and downstream operators, and
7272
# ensuring the output from the former matches the input of the latter, in both name and type.
7373
self.add_flow(study_loader_op, series_selector_op, {"dicom_study_list": "dicom_study_list"})
74-
self.add_flow(series_selector_op, series_to_vol_op, {"dicom_series": "dicom_series"})
74+
self.add_flow(
75+
series_selector_op, series_to_vol_op, {"study_selected_series_list": "study_selected_series_list"}
76+
)
7577
self.add_flow(series_to_vol_op, unetr_seg_op, {"image": "image"})
7678
# Add the publishing operator to save the input and seg images for Render Server.
7779
# Note the PublisherOperator has temp impl till a proper rendering module is created.
7880
self.add_flow(unetr_seg_op, publisher_op, {"saved_images_folder": "saved_images_folder"})
7981
# Note below the dicom_seg_writer requires two inputs, each coming from a upstream operator.
8082
# Also note that the DICOMSegmentationWriterOperator may throw exception with some inputs.
8183
# Bug has been created to track the issue.
82-
self.add_flow(series_selector_op, dicom_seg_writer, {"dicom_series": "dicom_series"})
84+
self.add_flow(
85+
series_selector_op, dicom_seg_writer, {"study_selected_series_list": "study_selected_series_list"}
86+
)
8387
self.add_flow(unetr_seg_op, dicom_seg_writer, {"seg_image": "seg_image"})
8488

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

examples/apps/dicom_series_to_image_app/app.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ def compose(self):
2525
png_converter_op = PNGConverterOperator()
2626

2727
self.add_flow(study_loader_op, series_selector_op, {"dicom_study_list": "dicom_study_list"})
28-
self.add_flow(series_selector_op, series_to_vol_op, {"dicom_series": "dicom_series"})
28+
self.add_flow(
29+
series_selector_op, series_to_vol_op, {"study_selected_series_list": "study_selected_series_list"}
30+
)
2931
self.add_flow(series_to_vol_op, png_converter_op, {"image": "image"})
3032

3133

monai/deploy/operators/dicom_data_loader_operator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
4040

4141
input_path = op_input.get().path
4242
dicom_study_list = self.load_data_to_studies(input_path)
43-
op_output.set(dicom_study_list)
43+
op_output.set(dicom_study_list, "dicom_study_list")
4444

4545
def load_data_to_studies(self, input_path: Path) -> List[DICOMStudy]:
4646
"""Load DICOM data from files into DICOMStudy objects in a list.

monai/deploy/operators/dicom_seg_writer_operator.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@
3838

3939

4040
@md.input("seg_image", Image, IOType.IN_MEMORY)
41-
@md.input(
42-
"study_selected_series_list", List[StudySelectedSeries], IOType.IN_MEMORY
43-
) # "dicom_series", DICOMSeries, IOType.IN_MEMORY)
41+
@md.input("study_selected_series_list", List[StudySelectedSeries], IOType.IN_MEMORY)
4442
@md.output("dicom_seg_instance", DataPath, IOType.DISK)
4543
@md.env(pip_packages=["pydicom >= 1.4.2", "SimpleITK >= 2.0.0"])
4644
class DICOMSegmentationWriterOperator(Operator):

monai/deploy/operators/dicom_series_selector_operator.py

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525

2626
@md.input("dicom_study_list", List[DICOMStudy], IOType.IN_MEMORY)
2727
@md.input("selection_rules", Dict, IOType.IN_MEMORY) # This overides the rules in the instance.
28-
@md.output("dicom_series", List[DICOMSeries], IOType.IN_MEMORY)
2928
@md.output("study_selected_series_list", List[StudySelectedSeries], IOType.IN_MEMORY)
3029
class DICOMSeriesSelectorOperator(Operator):
3130
"""This operator selects a list of DICOM Series in a DICOM Study for a given set of selection rules.
@@ -84,22 +83,27 @@ def __init__(self, rules: Text = None, all_matched: bool = False, *args, **kwarg
8483

8584
def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):
8685
"""Performs computation for this operator."""
86+
87+
dicom_study_list = None
88+
selection_rules = None
8789
try:
8890
dicom_study_list = op_input.get("dicom_study_list")
91+
except ItemNotExistsError as ex:
92+
logging.exception(f"Failed to find input 'dicom_study_list', {ex}")
93+
raise
94+
95+
try:
8996
selection_rules = op_input.get("selection_rules")
90-
if not selection_rules:
91-
selection_rules = self._load_rules() if self._rules_json_str else None
92-
dicom_series_list, study_selected_series = self.filter(selection_rules, dicom_study_list, self._all_matched)
93-
op_output.set(dicom_series_list, "dicom_series")
94-
op_output.set(
95-
study_selected_series,
96-
)
9797
except ItemNotExistsError:
98+
# OK for not providing selection rules.
9899
pass
99100

100-
def filter(
101-
self, selection_rules, dicom_study_list, all_matched: bool = False
102-
) -> Tuple[List[SelectedSeries], List[StudySelectedSeries]]:
101+
if not selection_rules:
102+
selection_rules = self._load_rules() if self._rules_json_str else None
103+
study_selected_series = self.filter(selection_rules, dicom_study_list, self._all_matched)
104+
op_output.set(study_selected_series, "study_selected_series_list")
105+
106+
def filter(self, selection_rules, dicom_study_list, all_matched: bool = False) -> List[StudySelectedSeries]:
103107
"""Selects the series with the given matching rules.
104108
105109
If rules object is None, all series will be returned with series instance UID as the selection name.
@@ -115,15 +119,14 @@ def filter(
115119
all_matched (bool): Gets all matched series in a study. Defaults to False for first match only.
116120
117121
Returns:
118-
list: A list of all selected series of type SelectedSeries.
119122
list: A list of objects of type StudySelectedSeries.
120123
121124
Raises:
122125
ValueError: If the selection_rules object does not contain "selections" attribute.
123126
"""
124127

125128
if not dicom_study_list or len(dicom_study_list) < 1:
126-
return [], []
129+
return []
127130

128131
if not selection_rules:
129132
# Return all series if no selection rules are supplied
@@ -135,7 +138,6 @@ def filter(
135138
if not selections:
136139
raise ValueError('Expected "selections" not found in the rules.')
137140

138-
selected_series_list = [] # List of all selected DICOMSeries objects
139141
study_selected_series_list = [] # List of StudySelectedSeries objects
140142

141143
for study in dicom_study_list:
@@ -153,30 +155,26 @@ def filter(
153155
# Select only the first series that matches the conditions, list of one
154156
series_list = self._select_series(conditions, study, all_matched)
155157
if series_list and len(series_list) > 0:
156-
selected_series_list.extend(x for x in series_list) # Add each single one
157158
for series in series_list:
158159
selected_series = SelectedSeries(selection_name, series)
159160
study_selected_series.add_selected_series(selected_series)
160161

161162
if len(study_selected_series.selected_series) > 0:
162163
study_selected_series_list.append(study_selected_series)
163164

164-
return selected_series_list, study_selected_series_list
165+
return study_selected_series_list
165166

166167
def _load_rules(self):
167168
return json_loads(self._rules_json_str) if self._rules_json_str else None
168169

169-
def _select_all_series(
170-
self, dicom_study_list: List[DICOMStudy]
171-
) -> Tuple[List[SelectedSeries], List[StudySelectedSeries]]:
170+
def _select_all_series(self, dicom_study_list: List[DICOMStudy]) -> List[StudySelectedSeries]:
172171
"""Select all series in studies
173172
174173
Returns:
175174
list: list of DICOMSeries objects
176175
list: list of StudySelectedSeries objects
177176
"""
178177

179-
series_list = []
180178
study_selected_series_list = []
181179
for study in dicom_study_list:
182180
logging.info(f"Working on study, instance UID: {study.StudyInstanceUID}")
@@ -187,9 +185,8 @@ def _select_all_series(
187185
print(f"Working on series, instance UID: {str(series.SeriesInstanceUID)}")
188186
selected_series = SelectedSeries(None, series) # No selection name is known or given
189187
study_selected_series.add_selected_series(selected_series)
190-
series_list.append(series)
191188
study_selected_series_list.append(study_selected_series)
192-
return series_list, study_selected_series_list
189+
return study_selected_series_list
193190

194191
def _select_series(self, attributes: dict, study: DICOMStudy, all_matched=False) -> List[DICOMSeries]:
195192
"""Finds series whose attributes match the given attributes.

monai/deploy/operators/monai_seg_inference_operator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ def _convert_dicom_metadata_datatype(self, metadata: Dict):
143143
return metadata
144144

145145
# Try to convert data type for the well knowned attributes. Add more as needed.
146-
if metadata.get("series_instance_uid", None):
146+
if metadata.get("SeriesInstanceUID", None):
147147
try:
148-
metadata["series_instance_uid"] = str(metadata["series_instance_uid"])
148+
metadata["SeriesInstanceUID"] = str(metadata["SeriesInstanceUID"])
149149
except Exception:
150150
pass
151151
if metadata.get("row_pixel_spacing", None):
@@ -186,7 +186,7 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
186186
# Need to try to convert the data type of a few metadata attributes.
187187
input_img_metadata = self._convert_dicom_metadata_datatype(input_image.metadata())
188188
# Need to give a name to the image as in-mem Image obj has no name.
189-
img_name = str(input_img_metadata.get("series_instance_uid", "Img_in_context"))
189+
img_name = str(input_img_metadata.get("SeriesInstanceUID", "Img_in_context"))
190190

191191
pre_transforms: Compose = self._pre_transform
192192
post_transforms: Compose = self._post_transforms

0 commit comments

Comments
 (0)