| """ |
| Image processing utilities for the FastAPI application. |
| """ |
|
|
| import base64 |
| import io |
| from typing import Tuple |
| import numpy as np |
| from PIL import Image |
| import cv2 |
|
|
| from app.core.config import settings |
| from app.core.logging import get_logger |
|
|
| logger = get_logger(__name__) |
|
|
|
|
| def decode_base64_image(image_data: str) -> Tuple[np.ndarray, Tuple[int, int]]: |
| """ |
| Decode base64 image data to numpy array. |
| |
| Args: |
| image_data: Base64 encoded image string |
| |
| Returns: |
| Tuple of (image_array, (width, height)) |
| """ |
| try: |
| |
| if image_data.startswith('data:image'): |
| image_data = image_data.split(',')[1] |
| |
| |
| image_bytes = base64.b64decode(image_data) |
| |
| |
| pil_image = Image.open(io.BytesIO(image_bytes)) |
| |
| |
| if pil_image.mode != 'RGB': |
| pil_image = pil_image.convert('RGB') |
| |
| |
| original_dims = pil_image.size |
| |
| |
| image_array = np.array(pil_image) |
| |
| logger.debug(f"Decoded image with shape: {image_array.shape}") |
| |
| return image_array, original_dims |
| |
| except Exception as e: |
| logger.error(f"Failed to decode base64 image: {str(e)}") |
| raise ValueError(f"Invalid image data: {str(e)}") |
|
|
|
|
| def encode_image_to_base64(image: np.ndarray, format: str = "JPEG", quality: int = 95) -> str: |
| """ |
| Encode numpy array image to base64 string. |
| |
| Args: |
| image: Image as numpy array |
| format: Image format (JPEG, PNG, etc.) |
| quality: JPEG quality (1-100) |
| |
| Returns: |
| Base64 encoded image string |
| """ |
| try: |
| |
| if image.dtype != np.uint8: |
| image = (image * 255).astype(np.uint8) |
| |
| pil_image = Image.fromarray(image) |
| |
| |
| buffer = io.BytesIO() |
| save_kwargs = {"format": format} |
| |
| if format.upper() == "JPEG": |
| save_kwargs["quality"] = quality |
| save_kwargs["optimize"] = True |
| |
| pil_image.save(buffer, **save_kwargs) |
| |
| |
| image_bytes = buffer.getvalue() |
| base64_string = base64.b64encode(image_bytes).decode('utf-8') |
| |
| return base64_string |
| |
| except Exception as e: |
| logger.error(f"Failed to encode image to base64: {str(e)}") |
| raise ValueError(f"Image encoding failed: {str(e)}") |
|
|
|
|
| def validate_image_size(image: np.ndarray) -> bool: |
| """ |
| Validate image dimensions. |
| |
| Args: |
| image: Image as numpy array |
| |
| Returns: |
| True if image size is valid |
| """ |
| height, width = image.shape[:2] |
| |
| |
| min_dim = min(width, height) |
| max_dim = max(width, height) |
| |
| if min_dim < 32: |
| return False |
| |
| if max_dim > 4096: |
| return False |
| |
| return True |
|
|
|
|
| def resize_image_if_needed(image: np.ndarray, max_size: int = 1280) -> np.ndarray: |
| """ |
| Resize image if it's too large while maintaining aspect ratio. |
| |
| Args: |
| image: Image as numpy array |
| max_size: Maximum dimension size |
| |
| Returns: |
| Resized image |
| """ |
| height, width = image.shape[:2] |
| |
| if max(height, width) <= max_size: |
| return image |
| |
| |
| if width > height: |
| new_width = max_size |
| new_height = int(height * (max_size / width)) |
| else: |
| new_height = max_size |
| new_width = int(width * (max_size / height)) |
| |
| |
| pil_image = Image.fromarray(image) |
| resized_pil = pil_image.resize((new_width, new_height), Image.Resampling.LANCZOS) |
| |
| return np.array(resized_pil) |
|
|
|
|
| def validate_image_format(image_bytes: bytes) -> bool: |
| """ |
| Validate if the image format is supported. |
| |
| Args: |
| image_bytes: Raw image bytes |
| |
| Returns: |
| True if format is supported |
| """ |
| try: |
| with Image.open(io.BytesIO(image_bytes)) as img: |
| |
| format_lower = img.format.lower() if img.format else "" |
| allowed_formats = {"jpeg", "jpg", "png", "bmp", "tiff", "webp"} |
| return format_lower in allowed_formats |
| except Exception: |
| return False |
|
|
|
|
| def get_image_info(image: np.ndarray) -> dict: |
| """ |
| Get information about an image. |
| |
| Args: |
| image: Image as numpy array |
| |
| Returns: |
| Dictionary with image information |
| """ |
| height, width = image.shape[:2] |
| channels = image.shape[2] if len(image.shape) > 2 else 1 |
| |
| return { |
| "width": width, |
| "height": height, |
| "channels": channels, |
| "dtype": str(image.dtype), |
| "size_mb": image.nbytes / (1024 * 1024) |
| } |
|
|