Skip to content

Commit 802691f

Browse files
committed
Add test pipeline generator workflow and bump version to 1.0.0
- Introduced a new GitHub Actions workflow for testing the pipeline generator, including steps for setting up Python 3.10, installing dependencies, and running tests. - Updated the version of the pipeline-generator package from 0.1.0 to 1.0.0 in the uv.lock file. - Refactored import statements and improved code formatting in various files for better readability and consistency. Signed-off-by: Victor Chang <[email protected]>
1 parent 04fd450 commit 802691f

40 files changed

+478
-1694
lines changed

.github/workflows/pr.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,22 @@ jobs:
5555
with:
5656
fail_ci_if_error: false
5757
files: ./coverage.xml
58+
59+
test-pipeline-generator:
60+
runs-on: ubuntu-latest
61+
steps:
62+
- uses: actions/checkout@v2
63+
- name: Set up Python 3.10
64+
uses: actions/setup-python@v2
65+
with:
66+
python-version: "3.10"
67+
- name: Install uv
68+
uses: astral-sh/setup-uv@v6
69+
- name: Install dependencies
70+
working-directory: tools/pipeline-generator
71+
run: |
72+
uv sync
73+
- name: Run tests
74+
working-directory: tools/pipeline-generator
75+
run: |
76+
uv run pytest

monai/deploy/operators/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,22 @@
6868
ModelInfo,
6969
)
7070
from .image_directory_loader_operator import ImageDirectoryLoader
71+
from .image_overlay_writer_operator import ImageOverlayWriter
7172
from .inference_operator import InferenceOperator
7273
from .json_results_writer_operator import JSONResultsWriter
74+
from .llama3_vila_inference_operator import Llama3VILAInferenceOperator
7375
from .monai_bundle_inference_operator import (
7476
BundleConfigNames,
7577
IOMapping,
7678
MonaiBundleInferenceOperator,
7779
)
7880
from .monai_classification_operator import MonaiClassificationOperator
7981
from .monai_seg_inference_operator import InfererType, MonaiSegInferenceOperator
80-
from .nii_data_loader_operator import NiftiDataLoader
8182
from .nifti_directory_loader_operator import NiftiDirectoryLoader
8283
from .nifti_writer_operator import NiftiWriter
84+
from .nii_data_loader_operator import NiftiDataLoader
8385
from .png_converter_operator import PNGConverterOperator
86+
from .prompts_loader_operator import PromptsLoaderOperator
8487
from .publisher_operator import PublisherOperator
8588
from .stl_conversion_operator import STLConversionOperator, STLConverter
86-
from .image_overlay_writer_operator import ImageOverlayWriter
87-
from .prompts_loader_operator import PromptsLoaderOperator
88-
from .llama3_vila_inference_operator import Llama3VILAInferenceOperator
8989
from .vlm_results_writer_operator import VLMResultsWriterOperator

monai/deploy/operators/image_directory_loader_operator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def compute(self, op_input, op_output, context):
142142
def test():
143143
"""Test the ImageDirectoryLoader operator."""
144144
import tempfile
145+
145146
from PIL import Image as PILImageCreate
146147

147148
# Create a temporary directory with test images

monai/deploy/operators/image_overlay_writer_operator.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
- filename: base name (stem) for output file
2222
"""
2323

24+
import logging
2425
from pathlib import Path
2526
from typing import Optional, Tuple
26-
import logging
2727

2828
import numpy as np
2929

@@ -78,9 +78,7 @@ def _to_hwc_uint8(self, image) -> np.ndarray:
7878
else:
7979
arr = np.asarray(image)
8080
if arr.ndim != 3 or arr.shape[2] not in (3, 4):
81-
raise ValueError(
82-
f"Expected HWC image with 3 or 4 channels, got shape {arr.shape}"
83-
)
81+
raise ValueError(f"Expected HWC image with 3 or 4 channels, got shape {arr.shape}")
8482
# Drop alpha if present
8583
if arr.shape[2] == 4:
8684
arr = arr[..., :3]
@@ -105,17 +103,14 @@ def _to_mask_uint8(self, pred) -> np.ndarray:
105103
return arr
106104

107105
@staticmethod
108-
def _blend_overlay(
109-
img: np.ndarray, mask_u8: np.ndarray, alpha: float, color: Tuple[int, int, int]
110-
) -> np.ndarray:
106+
def _blend_overlay(img: np.ndarray, mask_u8: np.ndarray, alpha: float, color: Tuple[int, int, int]) -> np.ndarray:
111107
# img: HWC uint8, mask_u8: HW uint8
112108
mask = (mask_u8 > 0).astype(np.float32)[..., None]
113109
color_img = np.zeros_like(img, dtype=np.uint8)
114110
color_img[..., 0] = color[0]
115111
color_img[..., 1] = color[1]
116112
color_img[..., 2] = color[2]
117113
blended = (
118-
img.astype(np.float32) * (1.0 - alpha * mask)
119-
+ color_img.astype(np.float32) * (alpha * mask)
114+
img.astype(np.float32) * (1.0 - alpha * mask) + color_img.astype(np.float32) * (alpha * mask)
120115
).astype(np.uint8)
121116
return blended

monai/deploy/operators/json_results_writer_operator.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,11 @@ def _process_prediction(self, pred: Any, filename: str) -> Dict[str, Any]:
128128
}
129129
else:
130130
# Generic classification
131-
result["probabilities"] = {
132-
f"class_{i}": float(pred_data[i]) for i in range(len(pred_data))
133-
}
131+
result["probabilities"] = {f"class_{i}": float(pred_data[i]) for i in range(len(pred_data))}
134132

135133
# Add predicted class
136134
max_idx = int(np.argmax(pred_data))
137-
result["predicted_class"] = list(result["probabilities"].keys())[
138-
max_idx
139-
]
135+
result["predicted_class"] = list(result["probabilities"].keys())[max_idx]
140136
result["confidence"] = float(pred_data[max_idx])
141137

142138
elif pred_data.ndim == 2: # 2D array (batch of predictions)
@@ -172,14 +168,13 @@ def _print_classification_summary(self, result: Dict[str, Any]):
172168
for class_name, prob in probs.items():
173169
print(f" {class_name}: {prob:.4f}")
174170
if "predicted_class" in result:
175-
print(
176-
f" Predicted: {result['predicted_class']} (confidence: {result['confidence']:.4f})"
177-
)
171+
print(f" Predicted: {result['predicted_class']} (confidence: {result['confidence']:.4f})")
178172

179173

180174
def test():
181175
"""Test the JSONResultsWriter operator."""
182176
import tempfile
177+
183178
import numpy as np
184179

185180
with tempfile.TemporaryDirectory() as temp_dir:

monai/deploy/operators/llama3_vila_inference_operator.py

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,7 @@ def _load_model(self):
116116
config = AutoConfig.from_pretrained(self.model_path)
117117

118118
# Load tokenizer
119-
self.tokenizer = AutoTokenizer.from_pretrained(
120-
self.model_path / "llm", use_fast=False
121-
)
119+
self.tokenizer = AutoTokenizer.from_pretrained(self.model_path / "llm", use_fast=False)
122120

123121
# For LLaVA-style models, we typically need to handle image processing
124122
# and model loading in a specific way. For now, we'll create a simplified
@@ -156,17 +154,15 @@ def _preprocess_image(self, image: Image) -> torch.Tensor:
156154
# For now, we'll just convert to tensor
157155
return torch.from_numpy(image_array).float()
158156

159-
def _generate_response(
160-
self, image_tensor: torch.Tensor, prompt: str, generation_params: Dict[str, Any]
161-
) -> str:
157+
def _generate_response(self, image_tensor: torch.Tensor, prompt: str, generation_params: Dict[str, Any]) -> str:
162158
"""Generate text response from the model."""
163159
if self._mock_mode:
164160
# Mock response based on common medical VQA patterns
165161
mock_responses = {
166-
"what is this image showing": "This medical image shows anatomical structures with various tissue densities and contrast patterns.",
167-
"summarize key findings": "Key findings include: 1) Normal anatomical structures visible, 2) No obvious pathological changes detected, 3) Image quality is adequate for assessment.",
168-
"is there a focal lesion": "No focal lesion is identified in the visible field of view.",
169-
"describe the image": "This appears to be a medical imaging study showing cross-sectional anatomy with good tissue contrast.",
162+
"what is this image showing": "This medical image shows anatomical structures with various tissue densities and contrast patterns.", # noqa: B950
163+
"summarize key findings": "Key findings include: 1) Normal anatomical structures visible, 2) No obvious pathological changes detected, 3) Image quality is adequate for assessment.", # noqa: B950
164+
"is there a focal lesion": "No focal lesion is identified in the visible field of view.", # noqa: B950
165+
"describe the image": "This appears to be a medical imaging study showing cross-sectional anatomy with good tissue contrast.", # noqa: B950
170166
}
171167

172168
# Find best matching response
@@ -176,7 +172,7 @@ def _generate_response(
176172
return response
177173

178174
# Default response
179-
return f"Analysis of the medical image based on the prompt: '{prompt}'. [Mock response - actual model not loaded]"
175+
return f"Analysis of the medical image based on the prompt: {prompt!r}. [Mock response - actual model not loaded]"
180176

181177
# In a real implementation, you would:
182178
# 1. Tokenize the prompt
@@ -189,8 +185,8 @@ def _create_json_result(
189185
self,
190186
text_response: str,
191187
request_id: str,
192-
prompt: str = None,
193-
image_metadata: Dict = None,
188+
prompt: Optional[str] = None,
189+
image_metadata: Optional[Dict] = None,
194190
) -> Dict[str, Any]:
195191
"""Create a JSON result from the text response."""
196192
result = {
@@ -276,44 +272,30 @@ def compute(self, op_input, op_output, context):
276272
request_id = op_input.receive("request_id")
277273
generation_params = op_input.receive("generation_params")
278274

279-
self._logger.info(
280-
f"Processing request {request_id} with output type '{output_type}'"
281-
)
275+
self._logger.info(f"Processing request {request_id} with output type {output_type!r}")
282276

283277
try:
284278
# Preprocess image
285279
image_tensor = self._preprocess_image(image)
286280

287281
# Generate text response
288-
text_response = self._generate_response(
289-
image_tensor, prompt, generation_params
290-
)
282+
text_response = self._generate_response(image_tensor, prompt, generation_params)
291283

292284
# Get image metadata if available
293-
image_metadata = (
294-
image.metadata()
295-
if hasattr(image, "metadata") and callable(image.metadata)
296-
else None
297-
)
285+
image_metadata = image.metadata() if hasattr(image, "metadata") and callable(image.metadata) else None
298286

299287
# Create result based on output type
300288
if output_type == "json":
301-
result = self._create_json_result(
302-
text_response, request_id, prompt, image_metadata
303-
)
289+
result = self._create_json_result(text_response, request_id, prompt, image_metadata)
304290
elif output_type == "image":
305291
# For now, just return the original image
306292
# In future, this could generate new images
307293
result = image
308294
elif output_type == "image_overlay":
309295
result = self._create_image_overlay(image, text_response)
310296
else:
311-
self._logger.warning(
312-
f"Unknown output type: {output_type}, defaulting to json"
313-
)
314-
result = self._create_json_result(
315-
text_response, request_id, prompt, image_metadata
316-
)
297+
self._logger.warning(f"Unknown output type: {output_type}, defaulting to json")
298+
result = self._create_json_result(text_response, request_id, prompt, image_metadata)
317299

318300
# Emit outputs
319301
op_output.emit(result, "result")
@@ -335,3 +317,4 @@ def compute(self, op_input, op_output, context):
335317
op_output.emit(error_result, "result")
336318
op_output.emit(output_type, "output_type")
337319
op_output.emit(request_id, "request_id")
320+
raise e from None

0 commit comments

Comments
 (0)