Spaces:
Sleeping
Sleeping
| """ | |
| Face detection using OpenCV DNN (Res10 SSD). | |
| The model is automatically downloaded to ~/.cache/face_detector/ on first use. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import urllib.request | |
| from pathlib import Path | |
| from typing import List, Tuple | |
| import cv2 | |
| import numpy as np | |
| CACHE_DIR = Path.home() / ".cache" / "face_detector" | |
| PROTO_URL = "https://raw.githubusercontent.com/opencv/opencv/master/samples/dnn/face_detector/deploy.prototxt" | |
| WEIGHTS_URL = "https://github.com/opencv/opencv_3rdparty/raw/dnn_samples_face_detector_20170830/res10_300x300_ssd_iter_140000.caffemodel" | |
| PROTO_PATH = CACHE_DIR / "deploy.prototxt" | |
| WEIGHTS_PATH = CACHE_DIR / "res10_300x300_ssd_iter_140000.caffemodel" | |
| def _download_if_missing(url: str, path: Path) -> None: | |
| if path.exists(): | |
| return | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| print(f"[face_detector] Downloading {path.name}…") | |
| urllib.request.urlretrieve(url, path) | |
| class FaceDetector: | |
| """Detects faces in an image and returns bounding boxes + crops.""" | |
| def __init__(self, confidence_threshold: float = 0.7) -> None: | |
| self.threshold = confidence_threshold | |
| _download_if_missing(PROTO_URL, PROTO_PATH) | |
| _download_if_missing(WEIGHTS_URL, WEIGHTS_PATH) | |
| self.net = cv2.dnn.readNetFromCaffe( | |
| str(PROTO_PATH), str(WEIGHTS_PATH) | |
| ) | |
| def detect( | |
| self, image: np.ndarray | |
| ) -> List[Tuple[int, int, int, int]]: | |
| """ | |
| Args: | |
| image: BGR numpy array (H, W, 3) | |
| Returns: | |
| List of (x1, y1, x2, y2) boxes, sorted left-to-right | |
| """ | |
| h, w = image.shape[:2] | |
| blob = cv2.dnn.blobFromImage( | |
| cv2.resize(image, (300, 300)), 1.0, | |
| (300, 300), (104.0, 177.0, 123.0) | |
| ) | |
| self.net.setInput(blob) | |
| detections = self.net.forward() | |
| boxes: List[Tuple[int, int, int, int]] = [] | |
| for i in range(detections.shape[2]): | |
| conf = float(detections[0, 0, i, 2]) | |
| if conf < self.threshold: | |
| continue | |
| box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) | |
| x1, y1, x2, y2 = box.astype(int) | |
| x1, y1 = max(0, x1), max(0, y1) | |
| x2, y2 = min(w, x2), min(h, y2) | |
| if x2 > x1 and y2 > y1: | |
| boxes.append((x1, y1, x2, y2)) | |
| return sorted(boxes, key=lambda b: b[0]) | |
| def crop_faces( | |
| self, image: np.ndarray, padding: float = 0.10 | |
| ) -> Tuple[List[np.ndarray], List[Tuple[int, int, int, int]]]: | |
| """ | |
| Returns (crops, boxes) where crops are RGB face crops with optional padding. | |
| """ | |
| h, w = image.shape[:2] | |
| boxes = self.detect(image) | |
| crops = [] | |
| for x1, y1, x2, y2 in boxes: | |
| pad_x = int((x2 - x1) * padding) | |
| pad_y = int((y2 - y1) * padding) | |
| cx1, cy1 = max(0, x1 - pad_x), max(0, y1 - pad_y) | |
| cx2, cy2 = min(w, x2 + pad_x), min(h, y2 + pad_y) | |
| crop = image[cy1:cy2, cx1:cx2] | |
| crop_rgb = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB) | |
| crops.append(crop_rgb) | |
| return crops, boxes | |