Skip to content

Commit 24960d4

Browse files
committed
Enhanced the code and documentation
Signed-off-by: M Q <mingmelvinq@nvidia.com>
1 parent 60e1854 commit 24960d4

8 files changed

+161
-130
lines changed

examples/apps/simple_imaging_app/__main__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@
22

33
if __name__ == "__main__":
44
App().run()
5-

monai/deploy/operators/dicom_data_loader_operator.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from pathlib import Path
1515
from typing import List
1616

17-
from monai.deploy.core import Operator, OperatorSpec
17+
from monai.deploy.core import Fragment, Operator, OperatorSpec
1818
from monai.deploy.core.domain.dicom_series import DICOMSeries
1919
from monai.deploy.core.domain.dicom_study import DICOMStudy
2020
from monai.deploy.exceptions import ItemNotExistsError
@@ -33,20 +33,23 @@ class DICOMDataLoaderOperator(Operator):
3333
"""This operator loads DICOM studies into memory from a folder containing DICOM instance files.
3434
3535
Input:
36-
Path to the folder containing DICOM instance files, set as argument to the object constructor
36+
Path to the folder containing DICOM instance files, set as argument to the object constructor.
37+
It defaults to the folder, input, in the current working directory.
3738
3839
Output:
3940
A list of DICOMStudy objects in memory, named `dicom_study_list` by default but can be changed
40-
via the object instance attribute, `output_name`.
41+
via the object instance attribute, `output_name`. The name can be omitted when linking it to the
42+
receiver, simply because it is the only output.
4143
"""
4244

4345
DEFAULT_INPUT_FOLDER = Path.cwd() / "input"
4446
DEFAULT_OUTPUT_NAME = "dicom_study_list"
4547

4648
# For now, need to have the input folder as an instance attribute, set on init, because the
47-
# compute function does not get file I/O path in the context. Enhancement has been requested.
49+
# compute function does not get file I/O path in the execution context. Enhancement requested.
4850
def __init__(
4951
self,
52+
fragment: Fragment,
5053
*args,
5154
input_folder: Path = DEFAULT_INPUT_FOLDER,
5255
output_name: str = DEFAULT_OUTPUT_NAME,
@@ -56,6 +59,7 @@ def __init__(
5659
"""Creates an instance of this class
5760
5861
Args:
62+
fragment (Fragment): An instance of the Application class which is derived from Fragment.
5963
input_folder (Path): Folder containing DICOM instance files to load from.
6064
Defaults to `input` in the current working directory.
6165
output_name (str): The name for the output, which is list of DICOMStudy objects.
@@ -67,13 +71,16 @@ def __init__(
6771
self._must_load = must_load
6872
self.input_path = input_folder
6973
self.index = 0
70-
self.output_name = output_name.strip() if output_name and len(output_name.strip()) > 0 else DEFAULT_OUTPUT_NAME
74+
self.output_name = (
75+
output_name.strip()
76+
if output_name and len(output_name.strip()) > 0
77+
else DICOMDataLoaderOperator.DEFAULT_OUTPUT_NAME
78+
)
7179

72-
super().__init__(*args, **kwargs)
80+
super().__init__(fragment, *args, **kwargs)
7381

7482
def setup(self, spec: OperatorSpec):
7583
spec.output(self.output_name)
76-
# spec.param("input_path", Path("."))
7784

7885
def compute(self, op_input, op_output, context):
7986
"""Performs computation for this operator and handlesI/O."""
@@ -88,7 +95,7 @@ def compute(self, op_input, op_output, context):
8895
# MQ
8996

9097
dicom_study_list = self.load_data_to_studies(input_path)
91-
op_output.emit(dicom_study_list, "dicom_study_list")
98+
op_output.emit(dicom_study_list, self.output_name)
9299

93100
def load_data_to_studies(self, input_path: Path):
94101
"""Load DICOM data from files into DICOMStudy objects in a list.
@@ -314,7 +321,7 @@ def test():
314321
current_file_dir = Path(__file__).parent.resolve()
315322
data_path = current_file_dir.joinpath("../../../inputs/spleen_ct/dcm")
316323

317-
loader = DICOMDataLoaderOperator()
324+
loader = DICOMDataLoaderOperator(Fragment())
318325
study_list = loader.load_data_to_studies(data_path.absolute())
319326

320327
for study in study_list:
@@ -363,7 +370,7 @@ def test():
363370
except ItemNotExistsError as ex:
364371
print(f"Test passed: exception when no studies loaded & must_load flag is True: {ex}")
365372

366-
relaxed_loader = DICOMDataLoaderOperator(must_load=False)
373+
relaxed_loader = DICOMDataLoaderOperator(Fragment(), must_load=False)
367374
study_list = relaxed_loader.load_data_to_studies(non_dcm_dir)
368375
print(f"Test passed: {len(study_list)} study loaded and is OK when must_load flag is False.")
369376

monai/deploy/operators/dicom_encapsulated_pdf_writer_operator.py

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@
1111

1212
import logging
1313
import os
14-
from ast import Bytes
1514
from io import BytesIO
1615
from pathlib import Path
17-
from typing import Dict, List, Optional, Union
16+
from typing import Dict, Optional, Union
1817

1918
from monai.deploy.utils.importutil import optional_import
2019

@@ -27,24 +26,36 @@
2726
Sequence, _ = optional_import("pydicom.sequence", name="Sequence")
2827
PdfReader, _ = optional_import("PyPDF2", name="PdfReader")
2928

30-
from monai.deploy.core import Operator, OperatorSpec
29+
from monai.deploy.core import ConditionType, Fragment, Operator, OperatorSpec
3130
from monai.deploy.core.domain.dicom_series import DICOMSeries
3231
from monai.deploy.core.domain.dicom_series_selection import StudySelectedSeries
33-
from monai.deploy.exceptions import ItemNotExistsError
3432
from monai.deploy.operators.dicom_utils import EquipmentInfo, ModelInfo, save_dcm_file, write_common_modules
3533
from monai.deploy.utils.version import get_sdk_semver
3634

3735

3836
# @md.env(pip_packages=["pydicom >= 1.4.2", "PyPDF2 >= 2.11.1", "monai"])
3937
class DICOMEncapsulatedPDFWriterOperator(Operator):
38+
"""Class to write DICOM Encapsulated PDF Instance with provided PDF bytes in memory.
39+
40+
Named inputs:
41+
pdf_bytes: Bytes of the the PDF content.
42+
study_selected_series_list: Optional, DICOM series for copying metadata from.
43+
44+
Named output:
45+
None
46+
47+
File output:
48+
Generaed DICOM instance file in the provided output folder.
49+
"""
50+
4051
# File extension for the generated DICOM Part 10 file.
4152
DCM_EXTENSION = ".dcm"
4253
# The default output folder for saveing the generated DICOM instance file.
43-
# DEFAULT_OUTPUT_FOLDER = Path(os.path.join(os.path.dirname(__file__))) / "output"
4454
DEFAULT_OUTPUT_FOLDER = Path(os.getcwd()) / "output"
4555

4656
def __init__(
4757
self,
58+
fragment: Fragment,
4859
*args,
4960
output_folder: Union[str, Path],
5061
copy_tags: bool,
@@ -56,6 +67,7 @@ def __init__(
5667
"""Class to write DICOM Encapsulated PDF Instance with PDF bytes in memory or in a file.
5768
5869
Args:
70+
fragment (Fragment): An instance of the Application class which is derived from Fragment.
5971
output_folder (str or Path): The folder for saving the generated DICOM instance file.
6072
copy_tags (bool): True for copying DICOM attributes from a provided DICOMSeries.
6173
If True and no DICOMSeries obj provided, runtime exception is thrown.
@@ -81,6 +93,8 @@ def __init__(
8193
self.model_info = model_info if model_info else ModelInfo()
8294
self.equipment_info = equipment_info if equipment_info else EquipmentInfo()
8395
self.custom_tags = custom_tags
96+
self.input_name_bytes = "pdf_bytes"
97+
self.input_name_dcm_series = "study_selected_series_list"
8498

8599
# Set own Modality and SOP Class UID
86100
# Modality, e.g.,
@@ -101,7 +115,7 @@ def __init__(
101115
except Exception:
102116
self.software_version_number = ""
103117
self.operators_name = f"AI Algorithm {self.model_info.name}"
104-
super().__init__(*args, **kwargs)
118+
super().__init__(fragment, *args, **kwargs)
105119

106120
def setup(self, spec: OperatorSpec):
107121
"""Set up the named input(s), and output(s) if applicable.
@@ -112,8 +126,8 @@ def setup(self, spec: OperatorSpec):
112126
spec (OperatorSpec): The Operator specification for inputs and outputs etc.
113127
"""
114128

115-
spec.input("pdf_bytes")
116-
spec.input("study_selected_series_list")
129+
spec.input(self.input_name_bytes)
130+
spec.input(self.input_name_dcm_series).condition(ConditionType.NONE) # Optional input
117131

118132
def compute(self, op_input, op_output, context):
119133
"""Performs computation for this operator and handles I/O.
@@ -133,24 +147,13 @@ def compute(self, op_input, op_output, context):
133147

134148
# Gets the input, prepares the output folder, and then delegates the processing.
135149
pdf_bytes: bytes = b""
136-
try:
137-
pdf_bytes = op_input.receive("pdf_bytes")
138-
except ItemNotExistsError:
139-
# try:
140-
# file_path = op_input.receive("pdf_file")
141-
# except ItemNotExistsError:
142-
# raise ValueError("None of the named inputs can be found.") from None
143-
# # Read file, and if exception, let it bubble up
144-
# with open(file_path.path, "rb") as f:
145-
# pdf_bytes = f.read().strip()
146-
pass
147-
150+
pdf_bytes = op_input.receive(self.input_name_bytes)
148151
if not pdf_bytes or not len(pdf_bytes.strip()):
149152
raise IOError("Input is read but blank.")
150153

151154
try:
152-
study_selected_series_list = op_input.receive("study_selected_series_list")
153-
except ItemNotExistsError:
155+
study_selected_series_list = op_input.receive(self.input_name_dcm_series)
156+
except Exception:
154157
study_selected_series_list = None
155158

156159
dicom_series = None # It can be None if not to copy_tags.
@@ -245,24 +248,27 @@ def _is_pdf_bytes(self, content: bytes):
245248
return True
246249

247250

248-
def test():
251+
def test(test_copy_tags: bool = True):
249252
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
250253
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
251254

252255
current_file_dir = Path(__file__).parent.resolve()
253256
dcm_folder = current_file_dir.joinpath("../../../inputs/livertumor_ct/dcm/1-CT_series_liver_tumor_from_nii014")
254257
pdf_file = current_file_dir.joinpath("../../../inputs/pdf/TestPDF.pdf")
255-
out_path = "output_pdf_op"
258+
out_path = Path("output_pdf_op").absolute()
256259
pdf_bytes = b"Not PDF bytes."
257-
test_copy_tags = False
258260

259-
loader = DICOMDataLoaderOperator()
260-
series_selector = DICOMSeriesSelectorOperator()
261+
fragment = Fragment()
262+
loader = DICOMDataLoaderOperator(fragment, name="loader_op")
263+
series_selector = DICOMSeriesSelectorOperator(fragment, name="selector_op")
261264
sr_writer = DICOMEncapsulatedPDFWriterOperator(
265+
fragment,
266+
output_folder=out_path,
262267
copy_tags=test_copy_tags,
263268
model_info=None,
264269
equipment_info=EquipmentInfo(),
265270
custom_tags={"SeriesDescription": "Report from AI algorithm. Not for clinical use."},
271+
name="writer_op",
266272
)
267273

268274
# Testing with the main entry functions
@@ -284,8 +290,9 @@ def test():
284290
with open(pdf_file, "rb") as f:
285291
pdf_bytes = f.read()
286292

287-
sr_writer.write(pdf_bytes, dicom_series, Path(out_path).absolute())
293+
sr_writer.write(pdf_bytes, dicom_series, out_path)
288294

289295

290296
if __name__ == "__main__":
291-
test()
297+
test(test_copy_tags=True)
298+
test(test_copy_tags=False)

monai/deploy/operators/dicom_series_selector_operator.py

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,23 @@
1313
import numbers
1414
import re
1515
from json import loads as json_loads
16-
from typing import Dict, List, Text
16+
from typing import List
1717

18-
from monai.deploy.core import Operator, OperatorSpec # ExecutionContext, InputContext, IOType, Operator, OutputContext
18+
from monai.deploy.core import ConditionType, Fragment, Operator, OperatorSpec
1919
from monai.deploy.core.domain.dicom_series import DICOMSeries
2020
from monai.deploy.core.domain.dicom_series_selection import SelectedSeries, StudySelectedSeries
2121
from monai.deploy.core.domain.dicom_study import DICOMStudy
22-
from monai.deploy.exceptions import ItemNotExistsError
2322

2423

25-
# @md.input("dicom_study_list", List[DICOMStudy], IOType.IN_MEMORY)
26-
# @md.input("selection_rules", Dict, IOType.IN_MEMORY) # This overrides the rules in the instance.
27-
# @md.output("study_selected_series_list", List[StudySelectedSeries], IOType.IN_MEMORY)
2824
class DICOMSeriesSelectorOperator(Operator):
2925
"""This operator selects a list of DICOM Series in a DICOM Study for a given set of selection rules.
3026
27+
Named input:
28+
dicom_study_list: A list of DICOMStudy objects.
29+
30+
Named output:
31+
study_selected_series_list: A list of StudySelectedSeries objects. Downstream receiver optional.
32+
3133
This class can be considered a base class, and a derived class can override the 'filer' function to with
3234
custom logics.
3335
@@ -67,50 +69,41 @@ class DICOMSeriesSelectorOperator(Operator):
6769
}
6870
"""
6971

70-
def __init__(self, *args, rules_json_str: str = "", all_matched: bool = False, **kwargs) -> None:
72+
def __init__(
73+
self, fragment: Fragment, *args, rules_json_str: str = "", all_matched: bool = False, **kwargs
74+
) -> None:
7175
"""Instantiate an instance.
7276
7377
Args:
78+
fragment (Fragment): An instance of the Application class which is derived from Fragment.
7479
rules (Text): Selection rules in JSON string.
7580
all_matched (bool): Gets all matched series in a study. Defaults to False for first match only.
7681
"""
7782

7883
# rules: Text = "", all_matched: bool = False,
7984

8085
# Delay loading the rules as JSON string till compute time.
81-
self._rules_json_str = rules_json_str # rules if rules and rules.strip() else None
86+
self._rules_json_str = rules_json_str
8287
self._all_matched = all_matched # all_matched
88+
self.input_name_study_list = "dicom_study_list"
89+
self.output_name_selected_series = "study_selected_series_list"
8390

84-
super().__init__(*args, **kwargs)
91+
super().__init__(fragment, *args, **kwargs)
8592

8693
def setup(self, spec: OperatorSpec):
87-
spec.input("dicom_study_list")
88-
spec.output("study_selected_series_list")
94+
spec.input(self.input_name_study_list)
95+
spec.output(self.output_name_selected_series).condition(ConditionType.NONE) # Receiver optional
8996

9097
# Can use the config file to alter the selection rules per app run
9198
# spec.param("selection_rules")
9299

93100
def compute(self, op_input, op_output, context):
94101
"""Performs computation for this operator."""
95102

96-
dicom_study_list = None
97-
selection_rules = None
98-
try:
99-
dicom_study_list = op_input.receive("dicom_study_list") # op_input.get("dicom_study_list")
100-
except ItemNotExistsError as ex:
101-
logging.exception(f"Failed to find input 'dicom_study_list', {ex}")
102-
raise
103-
104-
try:
105-
selection_rules = self._rules_json_str # static for now # op_input.receive("selection_rules")
106-
except ItemNotExistsError:
107-
# OK for not providing selection rules.
108-
pass
109-
110-
if not selection_rules:
111-
selection_rules = self._load_rules() if self._rules_json_str else None
103+
dicom_study_list = op_input.receive(self.input_name_study_list)
104+
selection_rules = self._load_rules() if self._rules_json_str else None
112105
study_selected_series = self.filter(selection_rules, dicom_study_list, self._all_matched)
113-
op_output.emit(study_selected_series, "study_selected_series_list")
106+
op_output.emit(study_selected_series, self.output_name_selected_series)
114107

115108
def filter(self, selection_rules, dicom_study_list, all_matched: bool = False) -> List[StudySelectedSeries]:
116109
"""Selects the series with the given matching rules.
@@ -304,11 +297,12 @@ def test():
304297
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
305298

306299
current_file_dir = Path(__file__).parent.resolve()
307-
data_path = current_file_dir.joinpath("../../../inputs/spleen_ct/dcm")
300+
data_path = current_file_dir.joinpath("../../../inputs/spleen_ct/dcm").absolute()
308301

309-
loader = DICOMDataLoaderOperator()
310-
study_list = loader.load_data_to_studies(data_path.absolute())
311-
selector = DICOMSeriesSelectorOperator()
302+
fragment = Fragment()
303+
loader = DICOMDataLoaderOperator(fragment, name="loader_op")
304+
selector = DICOMSeriesSelectorOperator(fragment, name="selector_op")
305+
study_list = loader.load_data_to_studies(data_path)
312306
sample_selection_rule = json_loads(Sample_Rules_Text)
313307
print(f"Selection rules in JSON:\n{sample_selection_rule}")
314308
study_selected_seriee_list = selector.filter(sample_selection_rule, study_list)

0 commit comments

Comments
 (0)