Spaces:
Runtime error
Runtime error
from typing import Dict, Optional, Tuple, Type | |
from pathlib import Path | |
import uuid | |
import tempfile | |
import numpy as np | |
import pydicom | |
from PIL import Image | |
from pydantic import BaseModel, Field | |
from langchain_core.callbacks import AsyncCallbackManagerForToolRun, CallbackManagerForToolRun | |
from langchain_core.tools import BaseTool | |
class DicomProcessorInput(BaseModel): | |
"""Input schema for the DICOM Processor Tool.""" | |
dicom_path: str = Field(..., description="Path to the DICOM file") | |
window_center: Optional[float] = Field( | |
None, description="Window center for contrast adjustment" | |
) | |
window_width: Optional[float] = Field(None, description="Window width for contrast adjustment") | |
class DicomProcessorTool(BaseTool): | |
"""Tool for processing DICOM files and converting them to PNG images.""" | |
name: str = "dicom_processor" | |
description: str = ( | |
"Processes DICOM medical image files and converts them to standard image format. " | |
"No tool supports dicom natively, so this tool is used to convert dicom to png. " | |
"Handles window/level adjustments and proper scaling. " | |
"Input: Path to DICOM file and optional window/level parameters. " | |
"Output: Path to processed image file and DICOM metadata." | |
) | |
args_schema: Type[BaseModel] = DicomProcessorInput | |
temp_dir: Path = None | |
def __init__(self, temp_dir: Optional[str] = None): | |
"""Initialize the DICOM processor tool.""" | |
super().__init__() | |
self.temp_dir = Path(temp_dir if temp_dir else tempfile.mkdtemp()) | |
self.temp_dir.mkdir(exist_ok=True) | |
def _apply_windowing(self, img: np.ndarray, center: float, width: float) -> np.ndarray: | |
"""Apply window/level adjustment to the image.""" | |
img_min = center - width // 2 | |
img_max = center + width // 2 | |
img = np.clip(img, img_min, img_max) | |
img = ((img - img_min) / (width) * 255).astype(np.uint8) | |
return img | |
def _process_dicom( | |
self, | |
dicom_path: str, | |
window_center: Optional[float] = None, | |
window_width: Optional[float] = None, | |
) -> Tuple[np.ndarray, Dict]: | |
"""Process DICOM file and extract metadata.""" | |
dcm = pydicom.dcmread(dicom_path) | |
img = dcm.pixel_array.astype(float) | |
# Apply manufacturer's recommended windowing if available and not overridden | |
if window_center is None and hasattr(dcm, "WindowCenter"): | |
window_center = dcm.WindowCenter | |
if isinstance(window_center, list): | |
window_center = window_center[0] | |
if window_width is None and hasattr(dcm, "WindowWidth"): | |
window_width = dcm.WindowWidth | |
if isinstance(window_width, list): | |
window_width = window_width[0] | |
# Apply rescale slope/intercept if available | |
if hasattr(dcm, "RescaleSlope") and hasattr(dcm, "RescaleIntercept"): | |
img = img * dcm.RescaleSlope + dcm.RescaleIntercept | |
# Apply windowing if parameters are available | |
if window_center is not None and window_width is not None: | |
img = self._apply_windowing(img, window_center, window_width) | |
else: | |
img = ((img - img.min()) / (img.max() - img.min()) * 255).astype(np.uint8) | |
metadata = { | |
"PatientID": getattr(dcm, "PatientID", None), | |
"StudyDate": getattr(dcm, "StudyDate", None), | |
"Modality": getattr(dcm, "Modality", None), | |
"PixelSpacing": getattr(dcm, "PixelSpacing", None), | |
"WindowCenter": window_center, | |
"WindowWidth": window_width, | |
"ImageOrientation": getattr(dcm, "ImageOrientationPatient", None), | |
"ImagePosition": getattr(dcm, "ImagePositionPatient", None), | |
"BitsStored": getattr(dcm, "BitsStored", None), | |
} | |
return img, metadata | |
def _run( | |
self, | |
dicom_path: str, | |
window_center: Optional[float] = None, | |
window_width: Optional[float] = None, | |
run_manager: Optional[CallbackManagerForToolRun] = None, | |
) -> Tuple[Dict[str, str], Dict]: | |
"""Process DICOM file and save as viewable image. | |
Args: | |
dicom_path: Path to input DICOM file | |
window_center: Optional center value for windowing | |
window_width: Optional width value for windowing | |
run_manager: Optional callback manager | |
Returns: | |
Tuple[Dict, Dict]: Output dictionary with processed image path and metadata dictionary | |
""" | |
try: | |
# Process DICOM and save as PNG | |
img_array, metadata = self._process_dicom(dicom_path, window_center, window_width) | |
output_path = self.temp_dir / f"processed_dicom_{uuid.uuid4().hex[:8]}.png" | |
Image.fromarray(img_array).save(output_path) | |
output = { | |
"image_path": str(output_path), | |
} | |
metadata.update( | |
{ | |
"original_path": dicom_path, | |
"output_path": str(output_path), | |
"analysis_status": "completed", | |
} | |
) | |
return output, metadata | |
except Exception as e: | |
return ( | |
{"error": str(e)}, | |
{ | |
"dicom_path": dicom_path, | |
"analysis_status": "failed", | |
"error_details": str(e), | |
}, | |
) | |
async def _arun( | |
self, | |
dicom_path: str, | |
window_center: Optional[float] = None, | |
window_width: Optional[float] = None, | |
run_manager: Optional[AsyncCallbackManagerForToolRun] = None, | |
) -> Tuple[Dict[str, str], Dict]: | |
"""Async version of _run.""" | |
return self._run(dicom_path, window_center, window_width) | |