""" Image loader supporting JPEG, PNG, and DICOM formats. """ import numpy as np from pathlib import Path from typing import Tuple, Optional try: import pydicom HAS_PYDICOM = True except ImportError: HAS_PYDICOM = False try: from PIL import Image HAS_PIL = True except ImportError: HAS_PIL = False try: import cv2 HAS_CV2 = True except ImportError: HAS_CV2 = False def load_xray(path: str) -> Tuple[np.ndarray, Optional[np.ndarray]]: """ Load an X-ray image from file. Supports: JPEG, PNG, BMP, DICOM (.dcm) Args: path: Path to the image file Returns: img_u8: Grayscale image as uint8 array (H, W) spacing_mm: Pixel spacing [sx, sy] in mm, or None if not available """ path = Path(path) suffix = path.suffix.lower() # DICOM if suffix in ['.dcm', '.dicom']: if not HAS_PYDICOM: raise ImportError("pydicom is required for DICOM files. Install with: pip install pydicom") return _load_dicom(str(path)) # Standard image formats if suffix in ['.jpg', '.jpeg', '.png', '.bmp', '.tif', '.tiff']: return _load_standard_image(str(path)) # Try to load as standard image anyway try: return _load_standard_image(str(path)) except Exception as e: raise ValueError(f"Could not load image: {path}. Error: {e}") def _load_dicom(path: str) -> Tuple[np.ndarray, Optional[np.ndarray]]: """Load DICOM file.""" ds = pydicom.dcmread(path) arr = ds.pixel_array.astype(np.float32) # Apply modality LUT if present if hasattr(ds, 'RescaleSlope') and hasattr(ds, 'RescaleIntercept'): arr = arr * ds.RescaleSlope + ds.RescaleIntercept # Normalize to 0-255 arr = arr - arr.min() if arr.max() > 0: arr = arr / arr.max() img_u8 = (arr * 255).astype(np.uint8) # Get pixel spacing spacing_mm = None if hasattr(ds, 'PixelSpacing'): # PixelSpacing is [row_spacing, col_spacing] in mm sy, sx = [float(x) for x in ds.PixelSpacing] spacing_mm = np.array([sx, sy], dtype=np.float32) elif hasattr(ds, 'ImagerPixelSpacing'): sy, sx = [float(x) for x in ds.ImagerPixelSpacing] spacing_mm = np.array([sx, sy], dtype=np.float32) return img_u8, spacing_mm def _load_standard_image(path: str) -> Tuple[np.ndarray, Optional[np.ndarray]]: """Load standard image format (JPEG, PNG, etc.).""" if HAS_CV2: img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: raise ValueError(f"Could not read image: {path}") return img.astype(np.uint8), None elif HAS_PIL: img = Image.open(path).convert('L') # Convert to grayscale return np.array(img, dtype=np.uint8), None else: raise ImportError("Either opencv-python or Pillow is required. Install with: pip install opencv-python") def load_xray_rgb(path: str) -> Tuple[np.ndarray, Optional[np.ndarray]]: """ Load X-ray as RGB (for models that expect 3-channel input). Returns: img_rgb: RGB image as uint8 array (H, W, 3) spacing_mm: Pixel spacing or None """ img_gray, spacing = load_xray(path) # Convert grayscale to RGB by stacking img_rgb = np.stack([img_gray, img_gray, img_gray], axis=-1) return img_rgb, spacing