99# See the License for the specific language governing permissions and
1010# limitations under the License.
1111
12+ import json
1213import logging
14+ import os
1315from pathlib import Path
16+ from typing import List , Union
1417
1518# Required for setting SegmentDescription attributes. Direct import as this is not part of App SDK package.
1619from pydicom .sr .codedict import codes
17- from reporter_operator import ExecutionStatusReporterOperator
1820
1921from monai .deploy .conditions import CountCondition
2022from monai .deploy .core import AppContext , Application
3335)
3436from monai .deploy .operators .stl_conversion_operator import STLConversionOperator
3537
38+ from .results_message import (
39+ AggregatedResults ,
40+ AlgorithmClass ,
41+ DetailedResult ,
42+ MeasurementResult ,
43+ Results ,
44+ )
45+
3646
37- # @resource(cpu=1, gpu=1, memory="7Gi")
38- # pip_packages can be a string that is a path(str) to requirements.txt file or a list of packages.
39- # The monai pkg is not required by this class, instead by the included operators.
4047class AISpleenSegApp (Application ):
4148 """Demonstrates inference with built-in MONAI Bundle inference operator with DICOM files as input/output
4249
@@ -57,13 +64,68 @@ def __init__(self, *args, status_callback=None, **kwargs):
5764 """Creates an application instance."""
5865 self ._logger = logging .getLogger ("{}.{}" .format (__name__ , type (self ).__name__ ))
5966 self ._status_callback = status_callback
67+ self ._app_input_path = None # to be set in compose
68+ self ._app_output_path = None # to be set in compose
6069 super ().__init__ (* args , ** kwargs )
6170
71+ def _get_files_in_folder (self , folder_path : Union [str , Path ]) -> List [str ]:
72+ """Traverses a folder and returns a list of full paths of all files.
73+
74+ Args:
75+ folder_path (Union[str, Path]): The path to the folder to traverse.
76+
77+ Returns:
78+ List[str]: A list of absolute paths to the files in the folder.
79+ """
80+ if not os .path .isdir (folder_path ):
81+ self ._logger .warning (f"Output folder '{ folder_path } ' not found, returning empty file list." )
82+ return []
83+
84+ file_paths = []
85+ for root , _ , files in os .walk (folder_path ):
86+ for file in files :
87+ full_path = os .path .abspath (os .path .join (root , file ))
88+ file_paths .append (full_path )
89+ return file_paths
90+
6291 def run (self , * args , ** kwargs ):
6392 # This method calls the base class to run. Can be omitted if simply calling through.
6493 self ._logger .info (f"Begin { self .run .__name__ } " )
65- # The try...except block is removed as the reporter operator will handle status reporting.
66- super ().run (* args , ** kwargs )
94+ try :
95+ super ().run (* args , ** kwargs )
96+
97+ if self ._status_callback :
98+ # Create the results object using the Pydantic models
99+ ai_results = Results (
100+ aggregated_results = AggregatedResults (
101+ name = "Spleen Segmentation" ,
102+ algorithm_class = {AlgorithmClass .MEASUREMENT },
103+ ),
104+ detailed_results = {
105+ "Spleen Segmentation" : DetailedResult (
106+ measurement = MeasurementResult (
107+ measurements_text = "Spleen segmentation completed successfully." ,
108+ )
109+ )
110+ },
111+ )
112+
113+ output_files = self ._get_files_in_folder (self ._app_output_path )
114+
115+ callback_msg_dict = {
116+ "run_success" : True ,
117+ "output_files" : output_files ,
118+ "error_message" : None ,
119+ "error_code" : None ,
120+ "result" : ai_results .model_dump_json (),
121+ }
122+ self ._status_callback (json .dumps (callback_msg_dict ))
123+
124+ except Exception as e :
125+ self ._logger .error (f"Error in { self .run .__name__ } : { e } " )
126+ # Let the caller to handle and report the error
127+ raise e
128+
67129 self ._logger .info (f"End { self .run .__name__ } " )
68130
69131 def compose (self ):
@@ -73,12 +135,12 @@ def compose(self):
73135
74136 # Use Commandline options over environment variables to init context.
75137 app_context : AppContext = Application .init_app_context (self .argv )
76- app_input_path = Path (app_context .input_path )
77- app_output_path = Path (app_context .output_path )
138+ self . _app_input_path = Path (app_context .input_path )
139+ self . _app_output_path = Path (app_context .output_path )
78140
79141 # Create the custom operator(s) as well as SDK built-in operator(s).
80142 study_loader_op = DICOMDataLoaderOperator (
81- self , CountCondition (self , 1 ), input_folder = app_input_path , name = "study_loader_op"
143+ self , CountCondition (self , 1 ), input_folder = self . _app_input_path , name = "study_loader_op"
82144 )
83145 series_selector_op = DICOMSeriesSelectorOperator (self , rules = Sample_Rules_Text , name = "series_selector_op" )
84146 series_to_vol_op = DICOMSeriesToVolumeOperator (self , name = "series_to_vol_op" )
@@ -122,11 +184,11 @@ def compose(self):
122184 self ,
123185 segment_descriptions = segment_descriptions ,
124186 custom_tags = custom_tags ,
125- output_folder = app_output_path ,
187+ output_folder = self . _app_output_path ,
126188 name = "dicom_seg_writer" ,
127189 )
128190
129- reporter_op = ExecutionStatusReporterOperator (self , status_callback = self ._status_callback )
191+ # reporter_op = ExecutionStatusReporterOperator(self, status_callback=self._status_callback)
130192
131193 # Create the processing pipeline, by specifying the source and destination operators, and
132194 # ensuring the output from the former matches the input of the latter, in both name and type.
@@ -143,13 +205,13 @@ def compose(self):
143205 # Create the surface mesh STL conversion operator and add it to the app execution flow, if needed, by
144206 # uncommenting the following couple lines.
145207 stl_conversion_op = STLConversionOperator (
146- self , output_file = app_output_path .joinpath ("stl/spleen.stl" ), name = "stl_conversion_op"
208+ self , output_file = self . _app_output_path .joinpath ("stl/spleen.stl" ), name = "stl_conversion_op"
147209 )
148210 self .add_flow (bundle_spleen_seg_op , stl_conversion_op , {("pred" , "image" )})
149211
150212 # Connect the reporter operator to the end of the pipeline.
151213 # It will be triggered after the DICOM SEG file is written.
152- self .add_flow (stl_conversion_op , reporter_op , {("stl_bytes" , "data" )})
214+ # self.add_flow(stl_conversion_op, reporter_op, {("stl_bytes", "data")})
153215
154216 logging .info (f"End { self .compose .__name__ } " )
155217
0 commit comments