|
| 1 | +"""DICOM Image Resizing Service |
| 2 | +
|
| 3 | +Provides resizing functionality for DICOM images while maintaining aspect ratio. |
| 4 | +""" |
| 5 | + |
| 6 | +import logging |
| 7 | +import os |
| 8 | + |
| 9 | +import numpy as np |
| 10 | +from PIL import Image |
| 11 | +from pydicom import Dataset |
| 12 | + |
| 13 | +logger = logging.getLogger(__name__) |
| 14 | + |
| 15 | + |
| 16 | +class ImageResizer: |
| 17 | + def __init__(self, thumbnail_size: int | None = None): |
| 18 | + self.thumbnail_size = ( |
| 19 | + thumbnail_size if thumbnail_size is not None else int(os.getenv("DICOM_THUMBNAIL_SIZE", "400")) |
| 20 | + ) |
| 21 | + |
| 22 | + def _calculate_thumbnail_dimensions(self, original_cols: int, original_rows: int) -> tuple[int, int]: |
| 23 | + aspect_ratio = original_cols / original_rows |
| 24 | + if original_cols > original_rows: |
| 25 | + new_cols = self.thumbnail_size |
| 26 | + new_rows = int(self.thumbnail_size / aspect_ratio) |
| 27 | + else: |
| 28 | + new_rows = self.thumbnail_size |
| 29 | + new_cols = int(self.thumbnail_size * aspect_ratio) |
| 30 | + return new_cols, new_rows |
| 31 | + |
| 32 | + def _to_pil_image(self, pixel_array: np.ndarray, bits_allocated: int) -> tuple[Image.Image, dict]: |
| 33 | + normalization_info = {} |
| 34 | + |
| 35 | + if bits_allocated == 16: |
| 36 | + pixel_min = pixel_array.min() |
| 37 | + pixel_max = pixel_array.max() |
| 38 | + normalization_info = {"pixel_min": pixel_min, "pixel_max": pixel_max} |
| 39 | + |
| 40 | + if pixel_max > pixel_min: |
| 41 | + pixel_array_8bit = ((pixel_array - pixel_min) / (pixel_max - pixel_min) * 255).astype(np.uint8) |
| 42 | + else: |
| 43 | + # Handle uniform images (all same value) |
| 44 | + pixel_array_8bit = np.zeros_like(pixel_array, dtype=np.uint8) |
| 45 | + img = Image.fromarray(pixel_array_8bit, mode="L") |
| 46 | + else: |
| 47 | + img = Image.fromarray(pixel_array, mode="L") |
| 48 | + |
| 49 | + return img, normalization_info |
| 50 | + |
| 51 | + def _from_pil_image(self, img: Image.Image, bits_allocated: int, normalization_info: dict) -> np.ndarray: |
| 52 | + resized_array = np.array(img) |
| 53 | + |
| 54 | + # For 16-bit images, scale back to 16-bit range |
| 55 | + if bits_allocated == 16: |
| 56 | + pixel_min = normalization_info.get("pixel_min", 0) |
| 57 | + pixel_max = normalization_info.get("pixel_max", 0) |
| 58 | + |
| 59 | + if pixel_max > pixel_min: |
| 60 | + resized_array = (resized_array.astype(np.float32) / 255 * (pixel_max - pixel_min) + pixel_min).astype( |
| 61 | + np.uint16 |
| 62 | + ) |
| 63 | + else: |
| 64 | + # Keep uniform image as is |
| 65 | + resized_array = resized_array.astype(np.uint16) |
| 66 | + |
| 67 | + return resized_array |
| 68 | + |
| 69 | + def resize(self, ds: Dataset) -> Dataset: |
| 70 | + original_rows = ds.Rows |
| 71 | + original_cols = ds.Columns |
| 72 | + |
| 73 | + # Skip if already smaller than thumbnail size |
| 74 | + if original_rows <= self.thumbnail_size and original_cols <= self.thumbnail_size: |
| 75 | + logger.info( |
| 76 | + f"Image {original_cols}x{original_rows} already smaller than {self.thumbnail_size}, skipping resize" |
| 77 | + ) |
| 78 | + return ds |
| 79 | + |
| 80 | + # Calculate new dimensions |
| 81 | + new_cols, new_rows = self._calculate_thumbnail_dimensions(original_cols, original_rows) |
| 82 | + logger.info(f"Resizing from {original_cols}x{original_rows} to {new_cols}x{new_rows}") |
| 83 | + |
| 84 | + # Convert DICOM to PIL Image |
| 85 | + pixel_array = ds.pixel_array |
| 86 | + img, normalization_info = self._to_pil_image(pixel_array, ds.BitsAllocated) |
| 87 | + |
| 88 | + # Resize |
| 89 | + img_resized = img.resize((new_cols, new_rows), Image.Resampling.LANCZOS) |
| 90 | + |
| 91 | + # Convert back to DICOM pixel data |
| 92 | + resized_array = self._from_pil_image(img_resized, ds.BitsAllocated, normalization_info) |
| 93 | + |
| 94 | + # Update dataset |
| 95 | + ds.PixelData = resized_array.tobytes() |
| 96 | + ds.Rows = new_rows |
| 97 | + ds.Columns = new_cols |
| 98 | + |
| 99 | + return ds |
0 commit comments