Skip to content

Commit a1d683d

Browse files
authored
Add domain classes for study selected series and selection operator (#200)
* Add domain classes for study selected series and selection operator Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Added flag to select first one or all matched series. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Added support for StudySelectedSeries in consumer operator, though not in inference yet. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Change main() to test(), and Path to str Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Updated Jupyter Notebook and example apps to use the new operator APIs. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Fix style check complaints Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Addressed review comments on quieting style checker warnings. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * More files needing per_file_ignores Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Fixed more style checker errors Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Adding a space in comment line to quiet the checker. 1 file reformatted, 113 files left unchanged Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Last few style checker fixes? Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Fixing checker complaint. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * checker complaints. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * checker complaints. Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Checker complaints Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Why does Python think None is a type, and not treat it as null? Signed-off-by: mmelqin <mingmelvinq@nvidia.com> * Well, last one? Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
1 parent 9a5ca3f commit a1d683d

File tree

15 files changed

+842
-245
lines changed

15 files changed

+842
-245
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/core/domain/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919
DICOMStudy
2020
DICOMSeries
2121
DICOMSOPInstance
22+
SelectedSeries
23+
StudySelectedSeries
2224
"""
2325

2426
from .datapath import DataPath, NamedDataPath
2527
from .dicom_series import DICOMSeries
28+
from .dicom_series_selection import SelectedSeries, StudySelectedSeries
2629
from .dicom_sop_instance import DICOMSOPInstance
2730
from .dicom_study import DICOMStudy
2831
from .domain import Domain

monai/deploy/core/domain/dicom_series.py

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -50,70 +50,85 @@ def add_sop_instance(self, sop_instance):
5050
def get_sop_instances(self):
5151
return self._sop_instances
5252

53+
# Properties named after DICOM Series module attribute keywords
54+
# There are two required (Type 1) attrbutes for a series:
55+
# Keyword: SeriesInstanceUID, Tag: (0020,000E)
56+
# Keyword: Modality, Tag: (0008,0060)
57+
#
5358
@property
54-
def series_date(self):
59+
def SeriesInstanceUID(self):
60+
return self._series_instance_uid
61+
62+
@SeriesInstanceUID.setter
63+
def SeriesInstanceUID(self, val):
64+
self._series_instance_uid = val
65+
66+
@property
67+
def SeriesDate(self):
5568
return getattr(self, "_series_date", None)
5669

57-
@series_date.setter
58-
def series_date(self, val):
70+
@SeriesDate.setter
71+
def SeriesDate(self, val):
5972
self._series_date = val
6073

6174
@property
62-
def series_time(self):
75+
def SeriesTime(self):
6376
return getattr(self, "_series_time", None)
6477

65-
@series_time.setter
66-
def series_time(self, val):
78+
@SeriesTime.setter
79+
def SeriesTime(self, val):
6780
self._series_time = val
6881

6982
@property
70-
def modality(self):
83+
def Modality(self):
7184
return getattr(self, "_modality", None)
7285

73-
@modality.setter
74-
def modality(self, val):
86+
@Modality.setter
87+
def Modality(self, val):
7588
self._modality = val
7689

7790
@property
78-
def series_description(self):
91+
def SeriesDescription(self):
7992
return getattr(self, "_series_description", None)
8093

81-
@series_description.setter
82-
def series_description(self, val):
94+
@SeriesDescription.setter
95+
def SeriesDescription(self, val):
8396
self._series_description = val
8497

8598
@property
86-
def body_part_examined(self):
99+
def BodyPartExamined(self):
87100
return getattr(self, "_body_part_examined", None)
88101

89-
@body_part_examined.setter
90-
def body_part_examined(self, val):
102+
@BodyPartExamined.setter
103+
def BodyPartExamined(self, val):
91104
self._body_part_examined = val
92105

93106
@property
94-
def patient_position(self):
107+
def PatientPosition(self):
95108
return getattr(self, "_patient_position", None)
96109

97-
@patient_position.setter
98-
def patient_position(self, val):
110+
@PatientPosition.setter
111+
def PatientPosition(self, val):
99112
self._patient_position = val
100113

101114
@property
102-
def series_number(self):
115+
def SeriesNumber(self):
103116
return getattr(self, "_series_number", None)
104117

105-
@series_number.setter
106-
def series_number(self, val):
118+
@SeriesNumber.setter
119+
def SeriesNumber(self, val):
107120
self._series_number = val
108121

109122
@property
110-
def laterality(self):
111-
return getattr(self, "_laterality", None)
123+
def Laterality(self):
124+
return getattr(self, "_Laterality", None)
112125

113-
@laterality.setter
114-
def laterality(self, val):
126+
@Laterality.setter
127+
def Laterality(self, val):
115128
self._laterality = val
116129

130+
# Derived properties based on image module attributes
131+
#
117132
@property
118133
def row_pixel_spacing(self):
119134
return getattr(self, "_row_pixel_spacing", None)
@@ -179,28 +194,28 @@ def nifti_affine_transform(self, val):
179194
self._nifti_affine_transform = val
180195

181196
def __str__(self):
182-
result = "---------------" + "\n"
197+
result = "\n---------------" + "\n"
183198

184199
series_instance_uid_attr = "Series Instance UID: " + self._series_instance_uid + "\n"
185200
result += series_instance_uid_attr
186201

187202
num_sop_instances = "Num SOP Instances: " + str(len(self._sop_instances)) + "\n"
188203
result += num_sop_instances
189204

190-
if self.series_date is not None:
191-
series_date_attr = "Series Date: " + self.series_date + "\n"
205+
if self.SeriesDate is not None:
206+
series_date_attr = "Series Date: " + self.SeriesDate + "\n"
192207
result += series_date_attr
193208

194-
if self.series_time is not None:
195-
series_time_attr = "Series Time: " + self.series_time + "\n"
209+
if self.SeriesTime is not None:
210+
series_time_attr = "Series Time: " + self.SeriesTime + "\n"
196211
result += series_time_attr
197212

198-
if self.modality is not None:
199-
modality_attr = "Modality: " + self.modality + "\n"
213+
if self.Modality is not None:
214+
modality_attr = "Modality: " + self.Modality + "\n"
200215
result += modality_attr
201216

202-
if self.series_description is not None:
203-
series_desc_attr = "Series Description: " + self.series_description + "\n"
217+
if self.SeriesDescription is not None:
218+
series_desc_attr = "Series Description: " + self.SeriesDescription + "\n"
204219
result += series_desc_attr
205220

206221
if self.row_pixel_spacing is not None:

0 commit comments

Comments
 (0)