Skip to content

Commit 28a78a0

Browse files
committed
Refactor operator imports and enhance pipeline generator functionality
- Removed deprecated operators from the MONAI Deploy SDK and updated the import paths in the application template to reflect the new structure. - Introduced new operators such as GenericDirectoryScanner and ImageFileLoader for improved file handling. - Enhanced the NiftiDataLoader to handle various dimensionalities correctly and added logging for unexpected shapes. - Updated the pipeline generator to include new operators and refined the requirements for dependencies in the configuration files. - Added comprehensive tests for the new operators and updated existing tests to ensure functionality and correctness. Signed-off-by: Victor Chang <[email protected]>
1 parent 3a3a37d commit 28a78a0

20 files changed

+171
-40
lines changed

monai/deploy/operators/__init__.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,18 @@
2121
DICOMSeriesToVolumeOperator
2222
DICOMTextSRWriterOperator
2323
EquipmentInfo
24-
GenericDirectoryScanner
25-
ImageFileLoader
26-
ImageOverlayWriter
2724
InferenceOperator
2825
InfererType
2926
IOMapping
30-
JSONResultsWriter
31-
Llama3VILAInferenceOperator
3227
ModelInfo
3328
MonaiBundleInferenceOperator
34-
MonaiClassificationOperator
3529
MonaiSegInferenceOperator
3630
NiftiDataLoader
37-
NiftiWriter
3831
PNGConverterOperator
39-
PromptsLoaderOperator
4032
PublisherOperator
4133
SegmentDescription
4234
STLConversionOperator
4335
STLConverter
44-
VLMResultsWriterOperator
4536
"""
4637

4738
# If needed, can choose to expose some or all of Holoscan SDK built-in operators.
@@ -67,24 +58,21 @@
6758
EquipmentInfo,
6859
ModelInfo,
6960
)
70-
from .generic_directory_scanner_operator import GenericDirectoryScanner
71-
from .image_file_loader_operator import ImageFileLoader
72-
from .image_overlay_writer_operator import ImageOverlayWriter
61+
7362
from .inference_operator import InferenceOperator
74-
from .json_results_writer_operator import JSONResultsWriter
75-
from .llama3_vila_inference_operator import Llama3VILAInferenceOperator
63+
7664
from .monai_bundle_inference_operator import (
7765
BundleConfigNames,
7866
IOMapping,
7967
MonaiBundleInferenceOperator,
8068
)
81-
from .monai_classification_operator import MonaiClassificationOperator
69+
8270
from .monai_seg_inference_operator import InfererType, MonaiSegInferenceOperator
8371

84-
from .nifti_writer_operator import NiftiWriter
72+
8573
from .nii_data_loader_operator import NiftiDataLoader
8674
from .png_converter_operator import PNGConverterOperator
87-
from .prompts_loader_operator import PromptsLoaderOperator
75+
8876
from .publisher_operator import PublisherOperator
8977
from .stl_conversion_operator import STLConversionOperator, STLConverter
90-
from .vlm_results_writer_operator import VLMResultsWriterOperator
78+

monai/deploy/operators/nii_data_loader_operator.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,22 @@ def convert_and_save(self, nii_path):
8080
image_reader = SimpleITK.ImageFileReader()
8181
image_reader.SetFileName(str(nii_path))
8282
image = image_reader.Execute()
83-
image_np = np.transpose(SimpleITK.GetArrayFromImage(image), [2, 1, 0])
83+
image_np = SimpleITK.GetArrayFromImage(image)
84+
85+
# Handle different dimensionalities properly
86+
if image_np.ndim == 3:
87+
# Standard 3D volume: transpose from (z, y, x) to (x, y, z)
88+
image_np = np.transpose(image_np, [2, 1, 0])
89+
elif image_np.ndim == 4:
90+
# 4D volume with channels: (c, z, y, x) to (c, x, y, z)
91+
image_np = np.transpose(image_np, [0, 3, 2, 1])
92+
elif image_np.ndim == 2:
93+
# 2D slice: transpose from (y, x) to (x, y)
94+
image_np = np.transpose(image_np, [1, 0])
95+
else:
96+
# 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+
8499
return image_np
85100

86101

tools/pipeline-generator/pipeline_generator/config/config.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,27 @@ endpoints:
4343
- model_id: "MONAI/Llama3-VILA-M3-3B"
4444
input_type: "custom"
4545
output_type: "custom"
46+
dependencies:
47+
- transformers>=4.44.0
48+
- torch>=2.0.0
49+
- Pillow>=8.0.0
50+
- PyYAML>=6.0
4651
- model_id: "MONAI/Llama3-VILA-M3-8B"
4752
input_type: "custom"
4853
output_type: "custom"
54+
dependencies:
55+
- transformers>=4.44.0
56+
- torch>=2.0.0
57+
- Pillow>=8.0.0
58+
- PyYAML>=6.0
4959
- model_id: "MONAI/Llama3-VILA-M3-13B"
5060
input_type: "custom"
5161
output_type: "custom"
62+
dependencies:
63+
- transformers>=4.44.0
64+
- torch>=2.0.0
65+
- Pillow>=8.0.0
66+
- PyYAML>=6.0
5267

5368
additional_models:
5469
- model_id: "LGAI-EXAONE/EXAONEPath"

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,14 +446,74 @@ def _copy_additional_files(self, output_dir: Path, context: Dict[str, Any]) -> N
446446
output_dir: Output directory
447447
context: Template context
448448
"""
449-
# No need for custom operators anymore - using SDK operators
449+
# Copy needed operators to generated application
450+
self._copy_operators(output_dir, context)
450451

451452
# Generate requirements.txt
452453
self._generate_requirements(output_dir, context)
453454

454455
# Generate README.md
455456
self._generate_readme(output_dir, context)
456457

458+
def _copy_operators(self, output_dir: Path, context: Dict[str, Any]) -> None:
459+
"""Copy needed operators to the generated application.
460+
461+
Args:
462+
output_dir: Output directory
463+
context: Template context
464+
"""
465+
import shutil
466+
467+
# Map operator usage based on context
468+
needed_operators = []
469+
470+
input_type = context.get('input_type', '')
471+
output_type = context.get('output_type', '')
472+
task = context.get('task', '').lower()
473+
474+
# Determine which operators are needed based on the application type
475+
if input_type == "image":
476+
needed_operators.extend([
477+
'generic_directory_scanner_operator.py',
478+
'image_file_loader_operator.py'
479+
])
480+
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+
])
486+
elif input_type == "nifti":
487+
needed_operators.append('generic_directory_scanner_operator.py')
488+
489+
if output_type == "json":
490+
needed_operators.append('json_results_writer_operator.py')
491+
elif output_type == "image_overlay":
492+
needed_operators.append('image_overlay_writer_operator.py')
493+
elif output_type == "nifti":
494+
needed_operators.append('nifti_writer_operator.py')
495+
496+
if "classification" in task and input_type == "image":
497+
needed_operators.append('monai_classification_operator.py')
498+
499+
# Remove duplicates
500+
needed_operators = list(set(needed_operators))
501+
502+
if needed_operators:
503+
# Get the operators directory in templates
504+
operators_dir = Path(__file__).parent.parent / "templates" / "operators"
505+
506+
logger.info(f"Copying {len(needed_operators)} operators to generated application")
507+
508+
for operator_file in needed_operators:
509+
src_path = operators_dir / operator_file
510+
if src_path.exists():
511+
dst_path = output_dir / operator_file
512+
shutil.copy2(src_path, dst_path)
513+
logger.debug(f"Copied operator: {operator_file}")
514+
else:
515+
logger.warning(f"Operator file not found: {src_path}")
516+
457517
def _generate_requirements(self, output_dir: Path, context: Dict[str, Any]) -> None:
458518
"""Generate requirements.txt file.
459519

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,33 +43,33 @@ from monai.deploy.operators.stl_conversion_operator import STLConversionOperator
4343

4444
{% endif %}
4545
{% elif input_type == "image" %}
46-
from monai.deploy.operators.generic_directory_scanner_operator import GenericDirectoryScanner
47-
from monai.deploy.operators.image_file_loader_operator import ImageFileLoader
46+
from generic_directory_scanner_operator import GenericDirectoryScanner
47+
from image_file_loader_operator import ImageFileLoader
4848

4949
{% elif input_type == "custom" %}
50-
from monai.deploy.operators.llama3_vila_inference_operator import Llama3VILAInferenceOperator
50+
from llama3_vila_inference_operator import Llama3VILAInferenceOperator
5151

5252
# Custom operators for vision-language models
53-
from monai.deploy.operators.prompts_loader_operator import PromptsLoaderOperator
54-
from monai.deploy.operators.vlm_results_writer_operator import VLMResultsWriterOperator
53+
from prompts_loader_operator import PromptsLoaderOperator
54+
from vlm_results_writer_operator import VLMResultsWriterOperator
5555

5656
{% else %}
57-
from monai.deploy.operators.generic_directory_scanner_operator import GenericDirectoryScanner
57+
from generic_directory_scanner_operator import GenericDirectoryScanner
5858
from monai.deploy.operators.nii_data_loader_operator import NiftiDataLoader
5959

6060
{% endif %}
6161
{% if output_type == "json" %}
62-
from monai.deploy.operators.json_results_writer_operator import JSONResultsWriter
62+
from json_results_writer_operator import JSONResultsWriter
6363

6464
{% elif output_type == "image_overlay" %}
65-
from monai.deploy.operators.image_overlay_writer_operator import ImageOverlayWriter
65+
from image_overlay_writer_operator import ImageOverlayWriter
6666

67-
{% elif not use_dicom %}
68-
from monai.deploy.operators.nifti_writer_operator import NiftiWriter
67+
{% elif not use_dicom and input_type != "custom" %}
68+
from nifti_writer_operator import NiftiWriter
6969

7070
{% endif %}
7171
{% if "classification" in task.lower() and input_type == "image" %}
72-
from monai.deploy.operators.monai_classification_operator import MonaiClassificationOperator
72+
from monai_classification_operator import MonaiClassificationOperator
7373

7474
{% elif not (input_type == "custom" and output_type == "custom") %}
7575
from monai.deploy.operators.monai_bundle_inference_operator import (

monai/deploy/operators/generic_directory_scanner_operator.py renamed to tools/pipeline-generator/pipeline_generator/templates/operators/generic_directory_scanner_operator.py

File renamed without changes.

monai/deploy/operators/image_file_loader_operator.py renamed to tools/pipeline-generator/pipeline_generator/templates/operators/image_file_loader_operator.py

File renamed without changes.

monai/deploy/operators/image_overlay_writer_operator.py renamed to tools/pipeline-generator/pipeline_generator/templates/operators/image_overlay_writer_operator.py

File renamed without changes.

monai/deploy/operators/json_results_writer_operator.py renamed to tools/pipeline-generator/pipeline_generator/templates/operators/json_results_writer_operator.py

File renamed without changes.

monai/deploy/operators/llama3_vila_inference_operator.py renamed to tools/pipeline-generator/pipeline_generator/templates/operators/llama3_vila_inference_operator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
AutoTokenizer, _ = optional_import("transformers", name="AutoTokenizer")
2626

2727
PILImage, _ = optional_import("PIL", name="Image")
28-
ImageDraw, _ = optional_import("PIL", name="ImageDraw")
29-
ImageFont, _ = optional_import("PIL", name="ImageFont")
28+
ImageDraw, _ = optional_import("PIL.ImageDraw")
29+
ImageFont, _ = optional_import("PIL.ImageFont")
3030

3131

3232
class Llama3VILAInferenceOperator(Operator):

0 commit comments

Comments
 (0)