""" utils/image.py — Tiện ích xử lý ảnh: decode, encode, annotate, crop face """ import base64 import cv2 import numpy as np def decode_image(file_bytes: bytes) -> np.ndarray | None: """Giải mã bytes ảnh thành numpy array (BGR).""" nparr = np.frombuffer(file_bytes, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) return img def image_to_base64(img: np.ndarray) -> str: """Chuyển numpy array thành chuỗi base64 JPEG.""" _, buffer = cv2.imencode(".jpg", img) return base64.b64encode(buffer).decode("utf-8") def image_to_bytes(img: np.ndarray) -> bytes: """Chuyển numpy array thành JPEG bytes.""" _, buffer = cv2.imencode(".jpg", img) return buffer.tobytes() def annotate_frame(frame: np.ndarray, bbox, label: str, color: tuple) -> None: """Vẽ bounding box + nhãn lên frame.""" x1, y1, x2, y2 = [int(v) for v in bbox] cv2.rectangle(frame, (x1, y1), (x2, y2), color, 3) cv2.putText( frame, label, (x1, max(y1 - 10, 15)), cv2.FONT_HERSHEY_SIMPLEX, 0.65, color, 2, ) def crop_face(img: np.ndarray, bbox, padding: int = 20) -> tuple[np.ndarray, dict]: """ Cắt khuôn mặt từ ảnh gốc với padding. Returns: face_crop: numpy array coords: dict với x1, y1, x2, y2 sau khi clamp """ h, w = img.shape[:2] x1 = int(max(0, bbox[0] - padding)) y1 = int(max(0, bbox[1] - padding)) x2 = int(min(w, bbox[2] + padding)) y2 = int(min(h, bbox[3] + padding)) face_crop = img[y1:y2, x1:x2] return face_crop, {"x1": x1, "y1": y1, "x2": x2, "y2": y2} def get_image_dimensions(img: np.ndarray) -> tuple[int, int]: """Trả về (width, height) của ảnh.""" h, w = img.shape[:2] return w, h