|
|
""" |
|
|
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) |
|
|
} |
|
|
|