| """ |
| Preprocessing — decode, resize, color-convert. |
| |
| Uses cores.vision for all image operations — no duplicated decode, |
| resize, or format-sniffing logic. Preserves the original bytes so |
| metadata / forensics providers can use them. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
| from typing import Optional |
|
|
| import numpy as np |
|
|
| from cores.vision import bytes_to_numpy, resize_with_aspect, sniff_format |
|
|
|
|
| @dataclass |
| class PreprocessedImage: |
| """Output of preprocessing — the canonical image object passed downstream.""" |
| image: np.ndarray |
| width: int |
| height: int |
| channels: int = 3 |
| source: str = "" |
| resized: bool = False |
| original_bytes: Optional[bytes] = None |
| original_format: Optional[str] = None |
|
|
|
|
| class ImagePreprocessor: |
| """Decodes + resizes inbound images.""" |
|
|
| def __init__(self, max_dim: int = 1024) -> None: |
| self._max_dim = max_dim |
|
|
| def from_bytes(self, data: bytes, source: str = "bytes") -> PreprocessedImage: |
| img = bytes_to_numpy(data) |
| fmt = sniff_format(data) |
| return self._finalize(img, source, original_bytes=data, original_format=fmt) |
|
|
| def from_url(self, url: str, timeout: int = 15) -> PreprocessedImage: |
| from cores.vision import url_to_bytes |
| data = url_to_bytes(url, timeout=timeout) |
| img = bytes_to_numpy(data) |
| fmt = sniff_format(data) |
| return self._finalize(img, "url", original_bytes=data, original_format=fmt) |
|
|
| def from_numpy(self, img: np.ndarray, source: str = "in_memory") -> PreprocessedImage: |
| return self._finalize(img, source) |
|
|
| def _finalize( |
| self, |
| img: np.ndarray, |
| source: str, |
| original_bytes: Optional[bytes] = None, |
| original_format: Optional[str] = None, |
| ) -> PreprocessedImage: |
| |
| if img.ndim == 2: |
| import cv2 |
| img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) |
| elif img.shape[2] == 4: |
| import cv2 |
| img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) |
|
|
| resized = False |
| h, w = img.shape[:2] |
| if max(h, w) > self._max_dim: |
| img = resize_with_aspect(img, max_dim=self._max_dim) |
| resized = True |
|
|
| h, w = img.shape[:2] |
| return PreprocessedImage( |
| image=img, |
| width=w, |
| height=h, |
| channels=img.shape[2] if img.ndim == 3 else 1, |
| source=source, |
| resized=resized, |
| original_bytes=original_bytes, |
| original_format=original_format, |
| ) |
|
|