14
14
import json
15
15
import logging
16
16
import os
17
+ from pathlib import Path
17
18
from random import randint
18
19
from typing import List , Optional , Union
19
20
33
34
import monai .deploy .core as md
34
35
from monai .deploy .core import DataPath , ExecutionContext , Image , InputContext , IOType , Operator , OutputContext
35
36
from monai .deploy .core .domain .dicom_series import DICOMSeries
36
- from monai .deploy .operators .dicom_data_loader_operator import DICOMDataLoaderOperator
37
- from monai .deploy .operators .dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
37
+ from monai .deploy .core .domain .dicom_series_selection import SelectedSeries , StudySelectedSeries
38
38
39
39
40
40
@md .input ("seg_image" , Image , IOType .IN_MEMORY )
41
- @md .input ("dicom_series" , DICOMSeries , IOType .IN_MEMORY )
41
+ @md .input (
42
+ "study_selected_series_list" , List [StudySelectedSeries ], IOType .IN_MEMORY
43
+ ) # "dicom_series", DICOMSeries, IOType.IN_MEMORY)
42
44
@md .output ("dicom_seg_instance" , DataPath , IOType .DISK )
43
45
@md .env (pip_packages = ["pydicom >= 1.4.2" , "SimpleITK >= 2.0.0" ])
44
46
class DICOMSegmentationWriterOperator (Operator ):
@@ -48,7 +50,7 @@ class DICOMSegmentationWriterOperator(Operator):
48
50
49
51
# Supported input image format, based on extension.
50
52
SUPPORTED_EXTENSIONS = [".nii" , ".nii.gz" , ".mhd" ]
51
- # DICOM instance file extension. Case insentiive in string comaprision .
53
+ # DICOM instance file extension. Case insensitive in string comparison .
52
54
DCM_EXTENSION = ".dcm"
53
55
# Suffix to add to file name to indicate DICOM Seg dcm file.
54
56
DICOMSEG_SUFFIX = "-DICOMSEG"
@@ -79,53 +81,94 @@ def __init__(self, seg_labels: Optional[Union[List[str], str]] = None, *args, **
79
81
if isinstance (seg_labels , str ):
80
82
self ._seg_labels = [seg_labels ]
81
83
elif isinstance (seg_labels , list ):
84
+ self ._seg_labels = []
82
85
for label in seg_labels :
83
- if not isinstance (label , str ):
86
+ if isinstance (label , str ) or isinstance (label , int ):
87
+ self ._seg_labels .append (label )
88
+ else :
84
89
raise ValueError (f"List of strings expected, but contains { label } of type { type (label )} ." )
85
- self ._seg_labels = seg_labels
86
- else :
87
- raise ValueError ("List of strings expected." )
88
90
89
91
def compute (self , op_input : InputContext , op_output : OutputContext , context : ExecutionContext ):
90
- dicom_series = op_input .get ("dicom_series" )
92
+ """Performs computation for this operator and handles I/O.
93
+
94
+ For now, only a single segmentation image object or file is supported and the selected DICOM
95
+ series for inference is required, because the DICOM Seg IOD needs to refer to original instance.
96
+ When there are multiple selected series in the input, the first series' containing study will
97
+ be used for retrieving DICOM Study module attributes, e.g. StudyInstanceUID.
98
+
99
+ Raises:
100
+ FileNotFoundError: When image object not in the input, and segmentation image file not found either.
101
+ ValueError: Neither image object nor image file's folder is in the input, or no selected series.
102
+ """
91
103
92
- # Get the seg image in numpy
104
+ # Gets the input, prepares the output folder, and then delegates the processing.
105
+ study_selected_series_list = op_input .get ("study_selected_series_list" )
106
+ if not study_selected_series_list or len (study_selected_series_list ) < 1 :
107
+ raise ValueError ("Missing input, list of 'StudySelectedSeries'." )
108
+ for study_selected_series in study_selected_series_list :
109
+ if not isinstance (study_selected_series , StudySelectedSeries ):
110
+ raise ValueError ("Element in input is not expected type, 'StudySelectedSeries'." )
111
+
112
+ seg_image = op_input .get ("seg_image" )
113
+ # In case the Image object is not in the input, and input is the seg image file folder path.
114
+ if not isinstance (seg_image , Image ):
115
+ if isinstance (seg_image , DataPath ):
116
+ input_path = op_input .get ("segmentation_image" ).path
117
+ seg_image , _ = self .select_input_file (input_path )
118
+ else :
119
+ raise ValueError ("Input 'seg_image' is not Image or DataPath." )
120
+
121
+ output_dir = op_output .get ().path
122
+ output_dir .mkdir (parents = True , exist_ok = True )
123
+
124
+ self .process_images (seg_image_numpy , dicom_series , output_path )
125
+
126
+ def process_images (
127
+ self , image : Union [Image , Path ], study_selected_series_list : StudySelectedSeries , output_dir : Path
128
+ ):
129
+ """ """
130
+ # Get the seg image in numpy, and if the image is passed in as object, need to fake a input path.
93
131
seg_image_numpy = None
94
132
input_path = "dicom_seg"
95
- seg_image = op_input .get ("seg_image" )
96
- if isinstance (seg_image , Image ):
97
- seg_image_numpy = seg_image .asnumpy ()
98
- elif isinstance (seg_image , DataPath ):
99
- input_path = op_input .get ("segmentation_image" ).path
100
- input_path , _ = self .select_input_file (input_path )
133
+
134
+ if isinstance (image , Image ):
135
+ seg_image_numpy = image .asnumpy ()
136
+ elif isinstance (image , Path ):
137
+ input_path = image # It is expected that this is the image file path.
101
138
seg_image_numpy = self ._image_file_to_numpy (input_path )
102
139
else :
103
- # What else
104
- raise ValueError ("seg_image is not Image or DataPath" )
140
+ raise ValueError ("'image' is not an Image object or a supported image file." )
105
141
106
- # Create the output path for created DICOM Seg instance
107
- output_dir = op_output .get ().path
108
- output_dir .mkdir (parents = True , exist_ok = True )
142
+ # The output DICOM Seg instance file name is based on the actual or made-up input image file name.
109
143
output_filename = "{0}{1}{2}" .format (
110
144
os .path .splitext (os .path .basename (input_path ))[0 ],
111
145
DICOMSegmentationWriterOperator .DICOMSEG_SUFFIX ,
112
146
DICOMSegmentationWriterOperator .DCM_EXTENSION ,
113
147
)
114
- output_path = os . path . join ( output_dir , output_filename )
148
+ output_path = output_dir / output_filename
115
149
116
- self .create_dicom_seg (seg_image_numpy , dicom_series , output_path )
150
+ # Pick DICOM Series that was used as input for getting the seg image.
151
+ # For now, first one in the list.
152
+ for study_selected_series in study_selected_series_list :
153
+ if not isinstance (study_selected_series , StudySelectedSeries ):
154
+ raise ValueError ("Element in input is not expected type, 'StudySelectedSeries'." )
155
+ selected_series = study_selected_series .selected_series [0 ]
156
+ dicom_series = selected_series .series
157
+ self .create_dicom_seg (seg_image_numpy , dicom_series , output_path )
158
+ break
117
159
118
- def create_dicom_seg (self , image , dicom_series : DICOMSeries , file_path : str ):
119
- dicom_dataset_list = [ i . get_native_sop_instance () for i in dicom_series . get_sop_instances ()]
160
+ def create_dicom_seg (self , image : Image , dicom_series : DICOMSeries , file_path : Path ):
161
+ file_path . parent . absolute (). mkdir ( parents = True , exist_ok = True )
120
162
163
+ dicom_dataset_list = [i .get_native_sop_instance () for i in dicom_series .get_sop_instances ()]
121
164
# DICOM Seg creation
122
165
self ._seg_writer = DICOMSegWriter ()
123
166
try :
124
- self ._seg_writer .write (image , dicom_dataset_list , file_path , self ._seg_labels )
167
+ self ._seg_writer .write (image , dicom_dataset_list , file_path . name , self ._seg_labels )
125
168
# TODO: get a class to encapsulate the seg label information.
126
169
127
170
# Test reading back
128
- _ = self ._read_from_dcm (file_path )
171
+ _ = self ._read_from_dcm (file_path . name )
129
172
except Exception as ex :
130
173
print ("DICOMSeg creation failed. Error:\n {}" .format (ex ))
131
174
raise
@@ -193,7 +236,7 @@ def _get_label_list(self, stringfied_list_of_labels: str = ""):
193
236
"""
194
237
195
238
# Use json.loads as a convenience method to convert string to list of strings
196
- assert isinstance (stringfied_list_of_labels , str ), "Expected stringfiled list pf labels."
239
+ assert isinstance (stringfied_list_of_labels , str ), "Expected stringfied list pf labels."
197
240
198
241
label_list = ["default-label" ] # Use this as default if empty string
199
242
if stringfied_list_of_labels :
@@ -212,7 +255,7 @@ def write(self, seg_img, input_ds, outfile, seg_labels):
212
255
"""Write DICOM Segmentation object for the segmentation image
213
256
214
257
Args:
215
- seg_img (numpy array): numpy array of the segmentatin image.
258
+ seg_img (numpy array): numpy array of the segmentation image.
216
259
input_ds (list): list of Pydicom datasets of the original DICOM instances.
217
260
outfile (str): path for the output DICOM instance file.
218
261
seg_labels: list of labels for the segments
@@ -571,7 +614,7 @@ def segslice_from_mhd(dcm_output, seg_img, input_ds, num_labels):
571
614
img_slice , label , out_frame_counter , safe_get (input_ds [img_slice ], 0x00200032 )
572
615
)
573
616
)
574
- seg_slice = np .zeros ((1 , seg_img .shape [1 ], seg_img .shape [2 ]), dtype = np . bool )
617
+ seg_slice = np .zeros ((1 , seg_img .shape [1 ], seg_img .shape [2 ]), dtype = bool )
575
618
576
619
seg_slice [np .expand_dims (seg_img [img_slice , ...] == label , 0 )] = 1
577
620
@@ -584,7 +627,7 @@ def segslice_from_mhd(dcm_output, seg_img, input_ds, num_labels):
584
627
585
628
dcm_output .add_new (0x52009230 , "SQ" , out_frames ) # PerFrameFunctionalGroupsSequence
586
629
dcm_output .NumberOfFrames = out_frame_counter
587
- dcm_output .PixelData = np .packbits (np .flip (np .reshape (out_pixels .astype (np . bool ), (- 1 , 8 )), 1 )).tostring ()
630
+ dcm_output .PixelData = np .packbits (np .flip (np .reshape (out_pixels .astype (bool ), (- 1 , 8 )), 1 )).tobytes ()
588
631
589
632
dcm_output .get (0x00081115 )[0 ].add_new (0x0008114A , "SQ" , referenceInstances ) # ReferencedInstanceSequence
590
633
@@ -616,24 +659,36 @@ def segslice_from_mhd(dcm_output, seg_img, input_ds, num_labels):
616
659
617
660
618
661
def test ():
619
- data_path = "/home/mqin/src/monai-deploy-app-sdk/examples/apps/ai_spleen_seg_app/input"
620
- out_path = "/home/mqin/src/monai-deploy-app-sdk/examples/apps/ai_spleen_seg_app/output/dcm_seg_test.dcm"
662
+ from monai .deploy .operators .dicom_data_loader_operator import DICOMDataLoaderOperator
663
+ from monai .deploy .operators .dicom_series_selector_operator import DICOMSeriesSelectorOperator
664
+ from monai .deploy .operators .dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
665
+
666
+ data_path = "../../../examples/ai_spleen_seg_data/dcm"
667
+ out_path = "../../../examples/output_seg_op/dcm_seg_test.dcm"
621
668
622
669
files = []
623
670
loader = DICOMDataLoaderOperator ()
624
- loader ._list_files (data_path , files )
625
- study_list = loader ._load_data (files )
671
+ series_selector = DICOMSeriesSelectorOperator ()
672
+ dcm_to_volume_op = DICOMSeriesToVolumeOperator ()
673
+ seg_writer = DICOMSegmentationWriterOperator ()
674
+
675
+ # Testing with more granular functions
676
+ study_list = loader .load_data_to_studies (Path (data_path ).absolute ())
626
677
series = study_list [0 ].get_all_series ()[0 ]
627
678
628
- dcm_to_volume_op = DICOMSeriesToVolumeOperator ()
629
679
dcm_to_volume_op .prepare_series (series )
630
680
voxels = dcm_to_volume_op .generate_voxel_data (series )
631
681
metadata = dcm_to_volume_op .create_metadata (series )
632
682
image = dcm_to_volume_op .create_volumetric_image (voxels , metadata )
633
683
image_numpy = image .asnumpy ()
634
684
635
- seg_writer = DICOMSegmentationWriterOperator ()
636
- seg_writer .create_dicom_seg (image_numpy , series , out_path )
685
+ seg_writer .create_dicom_seg (image_numpy , series , Path (out_path ).absolute ())
686
+
687
+ # Testing with the main entry functions
688
+ study_list = loader .load_data_to_studies (Path (data_path ).absolute ())
689
+ _ , study_selected_series_list = series_selector .filter (None , study_list )
690
+ image = dcm_to_volume_op .convert_to_image (study_selected_series_list )
691
+ seg_writer .process_images (image , study_selected_series_list , Path (out_path ).parent .absolute ())
637
692
638
693
639
694
if __name__ == "__main__" :
0 commit comments