"""Geometry — bounding boxes, cropping, resizing.""" from __future__ import annotations from dataclasses import dataclass from typing import Tuple import cv2 import numpy as np @dataclass class BBox: """Axis-aligned bounding box.""" x: int y: int w: int h: int def to_dict(self) -> dict: return {"x": self.x, "y": self.y, "w": self.w, "h": self.h} @property def area(self) -> int: return self.w * self.h def to_face_recognition_tuple(self) -> Tuple[int, int, int, int]: """Convert to (top, right, bottom, left) tuple used by face_recognition.""" return (self.y, self.x + self.w, self.y + self.h, self.x) def crop_region(img: np.ndarray, bbox: BBox, margin: float = 0.0) -> np.ndarray: """Crop a region with optional fractional margin. Clamps to image bounds.""" dx = int(bbox.w * margin) dy = int(bbox.h * margin) x0 = max(0, bbox.x - dx) y0 = max(0, bbox.y - dy) x1 = min(img.shape[1], bbox.x + bbox.w + dx) y1 = min(img.shape[0], bbox.y + bbox.h + dy) return img[y0:y1, x0:x1] def resize_with_aspect(img: np.ndarray, max_dim: int = 1024) -> np.ndarray: """Resize so the longest side is at most max_dim, preserving aspect.""" h, w = img.shape[:2] if max(h, w) <= max_dim: return img scale = max_dim / max(h, w) return cv2.resize(img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA) def clamp_box(bbox: BBox, width: int, height: int) -> BBox: """Clamp a bounding box to image bounds.""" x = max(0, min(bbox.x, width - 1)) y = max(0, min(bbox.y, height - 1)) x2 = max(0, min(bbox.x + bbox.w, width)) y2 = max(0, min(bbox.y + bbox.h, height)) return BBox(x, y, max(0, x2 - x), max(0, y2 - y)) def boxes_iou(a: BBox, b: BBox) -> float: """Intersection-over-Union between two bounding boxes.""" x1 = max(a.x, b.x) y1 = max(a.y, b.y) x2 = min(a.x + a.w, b.x + b.w) y2 = min(a.y + a.h, b.y + b.h) inter = max(0, x2 - x1) * max(0, y2 - y1) union = a.area + b.area - inter return inter / union if union > 0 else 0.0