Skip to content

Commit f28e33c

Browse files
committed
PNG converter operator and the DICOM series to image app final update
Signed-off-by: M Q <mingmelvinq@nvidia.com>
1 parent 228d195 commit f28e33c

File tree

4 files changed

+112
-32
lines changed

4 files changed

+112
-32
lines changed

examples/apps/dicom_series_to_image_app/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
# Copyright 2021-2023 MONAI Consortium
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an "AS IS" BASIS,
8+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
# See the License for the specific language governing permissions and
10+
# limitations under the License.
11+
112
import os
213
import sys
314

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1+
# Copyright 2021-2023 MONAI Consortium
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an "AS IS" BASIS,
8+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
# See the License for the specific language governing permissions and
10+
# limitations under the License.
11+
112
from app import App
213

314
if __name__ == "__main__":
4-
App(do_run=True)
15+
App().run()
Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2021 MONAI Consortium
1+
# Copyright 2021-2023 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
@@ -9,27 +9,44 @@
99
# See the License for the specific language governing permissions and
1010
# limitations under the License.
1111

12+
from pathlib import Path
1213

13-
from monai.deploy.core import Application
14+
from monai.deploy.conditions import CountCondition
15+
from monai.deploy.core import AppContext, Application
16+
from monai.deploy.logger import load_env_log_level
1417
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
1518
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
1619
from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
1720
from monai.deploy.operators.png_converter_operator import PNGConverterOperator
1821

1922

2023
class App(Application):
24+
"""This application loads DICOM files, converts them to 3D image, then to PNG files on disk.
25+
26+
This showcases the MONAI Deploy application framework
27+
"""
28+
2129
def compose(self):
22-
study_loader_op = DICOMDataLoaderOperator()
23-
series_selector_op = DICOMSeriesSelectorOperator()
24-
series_to_vol_op = DICOMSeriesToVolumeOperator()
25-
png_converter_op = PNGConverterOperator()
30+
app_context = AppContext({}) # Let it figure out the data paths using well-known env vars etc.
31+
input_dcm_folder = Path(app_context.input_path)
32+
output_folder = Path(app_context.output_path)
33+
print(f"input_dcm_folder: {input_dcm_folder}")
34+
35+
# Set the first operator to run only once by setting the count condition to 1
36+
study_loader_op = DICOMDataLoaderOperator(
37+
self, CountCondition(self, 1), input_folder=input_dcm_folder, name="dcm_loader"
38+
)
39+
series_selector_op = DICOMSeriesSelectorOperator(self, name="series_selector")
40+
series_to_vol_op = DICOMSeriesToVolumeOperator(self, name="series_to_vol")
41+
png_converter_op = PNGConverterOperator(self, output_folder=output_folder, name="png_converter")
2642

27-
self.add_flow(study_loader_op, series_selector_op, {"dicom_study_list": "dicom_study_list"})
43+
# Create the execution DAG by linking operators' named output to named input.
44+
self.add_flow(study_loader_op, series_selector_op, {("dicom_study_list", "dicom_study_list")})
2845
self.add_flow(
29-
series_selector_op, series_to_vol_op, {"study_selected_series_list": "study_selected_series_list"}
46+
series_selector_op, series_to_vol_op, {("study_selected_series_list", "study_selected_series_list")}
3047
)
31-
self.add_flow(series_to_vol_op, png_converter_op, {"image": "image"})
48+
self.add_flow(series_to_vol_op, png_converter_op, {("image", "image")})
3249

3350

3451
if __name__ == "__main__":
35-
App(do_run=True)
52+
App().run()
Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2021 MONAI Consortium
1+
# Copyright 2021-2023 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
@@ -13,39 +13,75 @@
1313
from os import getcwd, makedirs
1414
from os.path import join
1515
from pathlib import Path
16+
from typing import Union
1617

17-
from monai.deploy.core import Operator, OperatorSpec
18+
import numpy as np
19+
20+
from monai.deploy.core import Fragment, Image, Operator, OperatorSpec
1821
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
1922
from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
2023
from monai.deploy.utils.importutil import optional_import
2124

2225
PILImage, _ = optional_import("PIL", name="Image")
2326

2427

25-
# @md.env(pip_packages=["Pillow >= 8.0.0"])
28+
# @md.env(pip_packages=["Pillow >= 8.0.0", "numpy"])
2629
class PNGConverterOperator(Operator):
2730
"""
28-
This operator writes out a 3D Volumetric Image to disk in a slice by slice manner
31+
This operator writes out a 3D Volumetric Image to to a file folder in a slice by slice manner.
32+
33+
Named input:
34+
image: Image object or numpy ndarray
35+
36+
Named output:
37+
None
38+
39+
File output:
40+
Generated PNG image file(s) saved in the provided output folder.
2941
"""
3042

31-
# The default output folder for saveing the generated DICOM instance file.
32-
# DEFAULT_OUTPUT_FOLDER = Path(os.path.join(os.path.dirname(__file__))) / "output"
43+
# The default output folder for saving the generated DICOM instance file.
3344
DEFAULT_OUTPUT_FOLDER = Path(getcwd()) / "output"
3445

35-
DEFAULT_OUTPUT_FOLDER = Path(getcwd()) / "output"
46+
def __init__(
47+
self,
48+
fragment: Fragment,
49+
*args,
50+
output_folder: Union[str, Path],
51+
**kwargs,
52+
):
53+
"""Class to write out a 3D Volumetric Image to a file folder in a slice by slice manner.
54+
55+
Args:
56+
fragment (Fragment): An instance of the Application class which is derived from Fragment.
57+
output_folder (str or Path): The folder for saving the generated DICOM instance file.
58+
"""
59+
60+
self.output_folder = output_folder if output_folder else PNGConverterOperator.DEFAULT_OUTPUT_FOLDER
61+
self.input_name_image = "image"
62+
# Need to call the base class constructor last
63+
super().__init__(fragment, *args, **kwargs)
64+
65+
def setup(self, spec: OperatorSpec):
66+
spec.input(self.input_name_image)
3667

3768
def compute(self, op_input, op_output, context):
38-
input_path = op_input.receive("image")
39-
# output_dir = op_output.get().path
40-
output_dir.mkdir(parents=True, exist_ok=True)
41-
self.convert_and_save(input_path, output_dir)
69+
input_image = op_input.receive(self.input_name_image)
70+
self.output_folder.mkdir(parents=True, exist_ok=True)
71+
self.convert_and_save(input_image, self.output_folder)
4272

4373
def convert_and_save(self, image, path):
4474
"""
4575
extracts the slices in originally acquired direction (often axial)
46-
and saves then in PNG format slice by slice in the specified directory
76+
and saves them in PNG format slice by slice in the specified directory
4777
"""
48-
image_data = image.asnumpy()
78+
79+
if isinstance(image, Image):
80+
image_data = image.asnumpy()
81+
elif isinstance(image, np.ndarray):
82+
image_data = image
83+
else:
84+
raise ValueError(f"Input is not Image or ndarray, {type(image)}.")
4985
image_shape = image_data.shape
5086

5187
num_images = image_shape[0]
@@ -62,31 +98,36 @@ def main():
6298
from pathlib import Path
6399

64100
current_file_dir = Path(__file__).parent.resolve()
65-
data_path = current_file_dir.joinpath("../../../examples/ai_spleen_seg_data/dcm")
66-
out_path = "png-output"
101+
data_path = current_file_dir.joinpath("../../../inputs/spleen_ct/dcm")
102+
out_path = "output_png"
67103
makedirs(out_path, exist_ok=True)
68104

69105
files = []
70-
loader = DICOMDataLoaderOperator()
106+
fragment = Fragment()
107+
loader = DICOMDataLoaderOperator(fragment, name="dcm_loader")
71108
loader._list_files(
72109
data_path,
73110
files,
74111
)
75112
study_list = loader._load_data(files)
76113
series = study_list[0].get_all_series()[0]
77114

78-
op1 = DICOMSeriesToVolumeOperator()
115+
print(f"The loaded series object properties:\n{series}")
116+
117+
op1 = DICOMSeriesToVolumeOperator(fragment, name="series_to_vol")
79118
op1.prepare_series(series)
80119
voxels = op1.generate_voxel_data(series)
81120
metadata = op1.create_metadata(series)
82121
image = op1.create_volumetric_image(voxels, metadata)
83122

84-
op2 = PNGConverterOperator()
85-
op2.convert_and_save(image, out_path)
86-
87-
print(f"The loaded series object properties:\n{series}")
88123
print(f"The converted Image object metadata:\n{metadata}")
89124

125+
op2 = PNGConverterOperator(fragment, output_folder=out_path, name="png_converter")
126+
# Not mocking the operator context, so bypassing compute
127+
op2.convert_and_save(image, op2.output_folder)
128+
129+
print(f"The converted PNG files are in: {out_path}")
130+
90131

91132
if __name__ == "__main__":
92133
main()

0 commit comments

Comments
 (0)