Skip to content

Commit ed8b7c2

Browse files
committed
Refactor operator imports and enhance code clarity
- Updated operator imports in the MONAI Deploy SDK to streamline the structure and improve readability. - Refined the NiftiDataLoader to ensure proper handling of various dimensionalities and added logging for unexpected shapes. - Enhanced the pipeline generator to include new operators and improved the handling of output types. - Cleaned up whitespace and formatting inconsistencies across multiple files for better code clarity. - Removed deprecated test files related to GenericDirectoryScanner and VLM operators to maintain a clean codebase. Signed-off-by: Victor Chang <[email protected]>
1 parent 28a78a0 commit ed8b7c2

File tree

13 files changed

+195
-1219
lines changed

13 files changed

+195
-1219
lines changed

monai/deploy/operators/__init__.py

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2021-2025 MONAI Consortium
1+
# Copyright 2021-2022 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
@@ -22,57 +22,33 @@
2222
DICOMTextSRWriterOperator
2323
EquipmentInfo
2424
InferenceOperator
25-
InfererType
2625
IOMapping
2726
ModelInfo
2827
MonaiBundleInferenceOperator
2928
MonaiSegInferenceOperator
30-
NiftiDataLoader
3129
PNGConverterOperator
3230
PublisherOperator
33-
SegmentDescription
3431
STLConversionOperator
3532
STLConverter
33+
NiftiDataLoader
3634
"""
3735

3836
# If needed, can choose to expose some or all of Holoscan SDK built-in operators.
3937
# from holoscan.operators import *
40-
from holoscan.operators import (
41-
PingRxOp,
42-
PingTxOp,
43-
VideoStreamRecorderOp,
44-
VideoStreamReplayerOp,
45-
)
38+
from holoscan.operators import PingRxOp, PingTxOp, VideoStreamRecorderOp, VideoStreamReplayerOp
4639

4740
from .clara_viz_operator import ClaraVizOperator
4841
from .dicom_data_loader_operator import DICOMDataLoaderOperator
4942
from .dicom_encapsulated_pdf_writer_operator import DICOMEncapsulatedPDFWriterOperator
50-
from .dicom_seg_writer_operator import (
51-
DICOMSegmentationWriterOperator,
52-
SegmentDescription,
53-
)
43+
from .dicom_seg_writer_operator import DICOMSegmentationWriterOperator
5444
from .dicom_series_selector_operator import DICOMSeriesSelectorOperator
5545
from .dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
56-
from .dicom_text_sr_writer_operator import (
57-
DICOMTextSRWriterOperator,
58-
EquipmentInfo,
59-
ModelInfo,
60-
)
61-
46+
from .dicom_text_sr_writer_operator import DICOMTextSRWriterOperator
47+
from .dicom_utils import EquipmentInfo, ModelInfo, random_with_n_digits, save_dcm_file, write_common_modules
6248
from .inference_operator import InferenceOperator
63-
64-
from .monai_bundle_inference_operator import (
65-
BundleConfigNames,
66-
IOMapping,
67-
MonaiBundleInferenceOperator,
68-
)
69-
70-
from .monai_seg_inference_operator import InfererType, MonaiSegInferenceOperator
71-
72-
49+
from .monai_bundle_inference_operator import BundleConfigNames, IOMapping, MonaiBundleInferenceOperator
50+
from .monai_seg_inference_operator import MonaiSegInferenceOperator
7351
from .nii_data_loader_operator import NiftiDataLoader
7452
from .png_converter_operator import PNGConverterOperator
75-
7653
from .publisher_operator import PublisherOperator
7754
from .stl_conversion_operator import STLConversionOperator, STLConverter
78-

monai/deploy/operators/nii_data_loader_operator.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def convert_and_save(self, nii_path):
8181
image_reader.SetFileName(str(nii_path))
8282
image = image_reader.Execute()
8383
image_np = SimpleITK.GetArrayFromImage(image)
84-
84+
8585
# Handle different dimensionalities properly
8686
if image_np.ndim == 3:
8787
# Standard 3D volume: transpose from (z, y, x) to (x, y, z)
@@ -94,8 +94,10 @@ def convert_and_save(self, nii_path):
9494
image_np = np.transpose(image_np, [1, 0])
9595
else:
9696
# For other dimensions, log a warning and return as-is
97-
self._logger.warning(f"Unexpected {image_np.ndim}D NIfTI file shape {image_np.shape} from {nii_path}, returning without transpose")
98-
97+
self._logger.warning(
98+
f"Unexpected {image_np.ndim}D NIfTI file shape {image_np.shape} from {nii_path}, returning without transpose"
99+
)
100+
99101
return image_np
100102

101103

tools/pipeline-generator/pipeline_generator/cli/run.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,39 +28,39 @@
2828

2929
def _validate_results(output_dir: Path) -> tuple[bool, str]:
3030
"""Validate that the application actually generated results.
31-
31+
3232
Args:
3333
output_dir: Path to the output directory
34-
34+
3535
Returns:
3636
Tuple of (success, message) where success is True if validation passed
3737
"""
3838
if not output_dir.exists():
3939
return False, f"Output directory does not exist: {output_dir}"
40-
40+
4141
# Check if any files were generated in the output directory
4242
output_files = list(output_dir.rglob("*"))
4343
result_files = [f for f in output_files if f.is_file()]
44-
44+
4545
if not result_files:
4646
return False, f"No result files generated in {output_dir}"
47-
47+
4848
# Count different types of output files
49-
json_files = [f for f in result_files if f.suffix.lower() == '.json']
50-
nifti_files = [f for f in result_files if f.suffix.lower() in ['.nii', '.gz']]
51-
image_files = [f for f in result_files if f.suffix.lower() in ['.png', '.jpg', '.jpeg', '.tiff']]
49+
json_files = [f for f in result_files if f.suffix.lower() == ".json"]
50+
nifti_files = [f for f in result_files if f.suffix.lower() in [".nii", ".gz"]]
51+
image_files = [f for f in result_files if f.suffix.lower() in [".png", ".jpg", ".jpeg", ".tiff"]]
5252
other_files = [f for f in result_files if f not in json_files + nifti_files + image_files]
53-
53+
5454
file_summary = []
5555
if json_files:
5656
file_summary.append(f"{len(json_files)} JSON files")
5757
if nifti_files:
58-
file_summary.append(f"{len(nifti_files)} NIfTI files")
58+
file_summary.append(f"{len(nifti_files)} NIfTI files")
5959
if image_files:
6060
file_summary.append(f"{len(image_files)} image files")
6161
if other_files:
6262
file_summary.append(f"{len(other_files)} other files")
63-
63+
6464
summary = ", ".join(file_summary) if file_summary else f"{len(result_files)} files"
6565
return True, f"Generated {summary}"
6666

tools/pipeline-generator/pipeline_generator/generator/app_generator.py

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"""Generate MONAI Deploy applications from MONAI Bundles."""
1313

1414
import logging
15+
import re
1516
from pathlib import Path
1617
from typing import Any, Dict, Optional
1718

@@ -20,8 +21,6 @@
2021
from ..config.settings import Settings, load_config
2122
from .bundle_downloader import BundleDownloader
2223

23-
import re
24-
2524
logger = logging.getLogger(__name__)
2625

2726

@@ -463,48 +462,43 @@ def _copy_operators(self, output_dir: Path, context: Dict[str, Any]) -> None:
463462
context: Template context
464463
"""
465464
import shutil
466-
465+
467466
# Map operator usage based on context
468467
needed_operators = []
469-
470-
input_type = context.get('input_type', '')
471-
output_type = context.get('output_type', '')
472-
task = context.get('task', '').lower()
473-
468+
469+
input_type = context.get("input_type", "")
470+
output_type = context.get("output_type", "")
471+
task = context.get("task", "").lower()
472+
474473
# Determine which operators are needed based on the application type
475474
if input_type == "image":
476-
needed_operators.extend([
477-
'generic_directory_scanner_operator.py',
478-
'image_file_loader_operator.py'
479-
])
475+
needed_operators.extend(["generic_directory_scanner_operator.py", "image_file_loader_operator.py"])
480476
elif input_type == "custom":
481-
needed_operators.extend([
482-
'llama3_vila_inference_operator.py',
483-
'prompts_loader_operator.py',
484-
'vlm_results_writer_operator.py'
485-
])
477+
needed_operators.extend(
478+
["llama3_vila_inference_operator.py", "prompts_loader_operator.py", "vlm_results_writer_operator.py"]
479+
)
486480
elif input_type == "nifti":
487-
needed_operators.append('generic_directory_scanner_operator.py')
488-
481+
needed_operators.append("generic_directory_scanner_operator.py")
482+
489483
if output_type == "json":
490-
needed_operators.append('json_results_writer_operator.py')
484+
needed_operators.append("json_results_writer_operator.py")
491485
elif output_type == "image_overlay":
492-
needed_operators.append('image_overlay_writer_operator.py')
486+
needed_operators.append("image_overlay_writer_operator.py")
493487
elif output_type == "nifti":
494-
needed_operators.append('nifti_writer_operator.py')
495-
488+
needed_operators.append("nifti_writer_operator.py")
489+
496490
if "classification" in task and input_type == "image":
497-
needed_operators.append('monai_classification_operator.py')
498-
491+
needed_operators.append("monai_classification_operator.py")
492+
499493
# Remove duplicates
500494
needed_operators = list(set(needed_operators))
501-
495+
502496
if needed_operators:
503497
# Get the operators directory in templates
504498
operators_dir = Path(__file__).parent.parent / "templates" / "operators"
505-
499+
506500
logger.info(f"Copying {len(needed_operators)} operators to generated application")
507-
501+
508502
for operator_file in needed_operators:
509503
src_path = operators_dir / operator_file
510504
if src_path.exists():

tools/pipeline-generator/pipeline_generator/templates/app.py.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ from vlm_results_writer_operator import VLMResultsWriterOperator
5555

5656
{% else %}
5757
from generic_directory_scanner_operator import GenericDirectoryScanner
58+
5859
from monai.deploy.operators.nii_data_loader_operator import NiftiDataLoader
5960

6061
{% endif %}

tools/pipeline-generator/pipeline_generator/templates/operators/generic_directory_scanner_operator.py

Lines changed: 23 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818

1919
class GenericDirectoryScanner(Operator):
2020
"""Scan a directory for files matching specified extensions and emit file paths one by one.
21-
21+
2222
This operator provides a generic way to iterate through files in a directory,
2323
emitting one file path at a time. It can be chained with file-specific loaders
2424
to create flexible data loading pipelines.
25-
25+
2626
Named Outputs:
2727
file_path: Path to the current file being processed
2828
filename: Name of the current file (without extension)
@@ -51,10 +51,10 @@ def __init__(
5151
"""
5252
self._logger = logging.getLogger("{}.{}".format(__name__, type(self).__name__))
5353
self._input_folder = Path(input_folder)
54-
self._file_extensions = [ext if ext.startswith('.') else f'.{ext}' for ext in file_extensions]
54+
self._file_extensions = [ext if ext.startswith(".") else f".{ext}" for ext in file_extensions]
5555
self._recursive = bool(recursive)
5656
self._case_sensitive = bool(case_sensitive)
57-
57+
5858
# State tracking
5959
self._files = []
6060
self._current_index = 0
@@ -64,69 +64,65 @@ def __init__(
6464
def _find_files(self) -> List[Path]:
6565
"""Find all files matching the specified extensions."""
6666
files = []
67-
67+
6868
# Normalize extensions for comparison
6969
if not self._case_sensitive:
7070
extensions = [ext.lower() for ext in self._file_extensions]
7171
else:
7272
extensions = self._file_extensions
73-
73+
7474
# Choose search method based on recursive flag
7575
if self._recursive:
7676
search_pattern = "**/*"
7777
search_method = self._input_folder.rglob
7878
else:
7979
search_pattern = "*"
8080
search_method = self._input_folder.glob
81-
81+
8282
# Find all files and filter by extension
8383
for file_path in search_method(search_pattern):
8484
if file_path.is_file():
8585
# Skip hidden files (starting with .) to avoid macOS metadata files like ._file.nii.gz
86-
if file_path.name.startswith('.'):
86+
if file_path.name.startswith("."):
8787
continue
88-
88+
8989
# Handle compound extensions like .nii.gz by checking if filename ends with any extension
9090
filename = file_path.name
9191
if not self._case_sensitive:
9292
filename = filename.lower()
93-
93+
9494
# Check if filename ends with any of the specified extensions
9595
for ext in extensions:
9696
if filename.endswith(ext):
9797
files.append(file_path)
9898
break # Only add once even if multiple extensions match
99-
99+
100100
# Sort files for consistent ordering
101101
files.sort()
102102
return files
103103

104104
def setup(self, spec: OperatorSpec):
105105
"""Define the operator outputs."""
106106
spec.output("file_path")
107-
spec.output("filename")
107+
spec.output("filename")
108108
spec.output("file_index").condition(ConditionType.NONE)
109109
spec.output("total_files").condition(ConditionType.NONE)
110110

111111
# Pre-initialize the files list
112112
if not self._input_folder.is_dir():
113113
raise ValueError(f"Input folder {self._input_folder} is not a directory")
114-
114+
115115
self._files = self._find_files()
116116
self._current_index = 0
117117

118118
if not self._files:
119-
self._logger.warning(
120-
f"No files found in {self._input_folder} with extensions {self._file_extensions}"
121-
)
119+
self._logger.warning(f"No files found in {self._input_folder} with extensions {self._file_extensions}")
122120
else:
123-
self._logger.info(
124-
f"Found {len(self._files)} files to process with extensions {self._file_extensions}"
125-
)
121+
self._logger.info(f"Found {len(self._files)} files to process with extensions {self._file_extensions}")
126122

127123
def compute(self, op_input, op_output, context):
128124
"""Emit the next file path."""
129-
125+
130126
# Check if we have more files to process
131127
if self._current_index >= len(self._files):
132128
# No more files to process
@@ -144,9 +140,7 @@ def compute(self, op_input, op_output, context):
144140
op_output.emit(self._current_index, "file_index")
145141
op_output.emit(len(self._files), "total_files")
146142

147-
self._logger.info(
148-
f"Emitted file: {file_path.name} ({self._current_index + 1}/{len(self._files)})"
149-
)
143+
self._logger.info(f"Emitted file: {file_path.name} ({self._current_index + 1}/{len(self._files)})")
150144

151145
except Exception as e:
152146
self._logger.error(f"Failed to process file {file_path}: {e}")
@@ -162,16 +156,13 @@ def test():
162156
# Create a temporary directory with test files
163157
with tempfile.TemporaryDirectory() as temp_dir:
164158
temp_path = Path(temp_dir)
165-
159+
166160
# Create test files with different extensions
167-
test_files = [
168-
"test1.jpg", "test2.png", "test3.nii", "test4.nii.gz",
169-
"test5.txt", "test6.jpeg"
170-
]
171-
161+
test_files = ["test1.jpg", "test2.png", "test3.nii", "test4.nii.gz", "test5.txt", "test6.jpeg"]
162+
172163
for filename in test_files:
173164
(temp_path / filename).touch()
174-
165+
175166
# Create a subdirectory with more files
176167
sub_dir = temp_path / "subdir"
177168
sub_dir.mkdir()
@@ -181,14 +172,12 @@ def test():
181172
# Test the operator with image extensions
182173
fragment = Fragment()
183174
scanner = GenericDirectoryScanner(
184-
fragment,
185-
input_folder=temp_path,
186-
file_extensions=['.jpg', '.jpeg', '.png'],
187-
recursive=True
175+
fragment, input_folder=temp_path, file_extensions=[".jpg", ".jpeg", ".png"], recursive=True
188176
)
189177

190178
# Simulate setup
191179
from monai.deploy.core import OperatorSpec
180+
192181
spec = OperatorSpec()
193182
scanner.setup(spec)
194183

0 commit comments

Comments
 (0)