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