Spaces:
Sleeping
Sleeping
| """ | |
| Face Extractor β OpenCV-based face detection and frame sampling | |
| """ | |
| import cv2 | |
| import numpy as np | |
| from pathlib import Path | |
| from typing import List, Tuple, Optional | |
| class FaceExtractor: | |
| """ | |
| Extracts and crops face regions from video frames. | |
| Uses OpenCV's Haar cascade (no extra dependencies). | |
| """ | |
| def __init__(self): | |
| cascade_path = cv2.data.haarcascades + "haarcascade_frontalface_default.xml" | |
| self.detector = cv2.CascadeClassifier(cascade_path) | |
| if self.detector.empty(): | |
| raise RuntimeError("Failed to load Haar cascade classifier.") | |
| # ββ Frame sampling ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def sample_frames( | |
| self, | |
| video_path: str, | |
| max_frames: int = 32, | |
| sample_fps: float = 2.0, | |
| ) -> List[Tuple[int, np.ndarray]]: | |
| """ | |
| Returns a list of (frame_index, BGR_frame) tuples. | |
| """ | |
| cap = cv2.VideoCapture(str(video_path)) | |
| if not cap.isOpened(): | |
| raise ValueError(f"Cannot open video: {video_path}") | |
| video_fps = cap.get(cv2.CAP_PROP_FPS) or 25.0 | |
| total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | |
| interval = max(1, int(video_fps / sample_fps)) | |
| frames: List[Tuple[int, np.ndarray]] = [] | |
| idx = 0 | |
| while cap.isOpened() and len(frames) < max_frames: | |
| ret, frame = cap.read() | |
| if not ret: | |
| break | |
| if idx % interval == 0: | |
| frames.append((idx, frame)) | |
| idx += 1 | |
| cap.release() | |
| return frames | |
| # ββ Face crop βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def crop_face( | |
| self, | |
| frame: np.ndarray, | |
| target_size: Tuple[int, int] = (224, 224), | |
| padding_ratio: float = 0.25, | |
| ) -> Optional[np.ndarray]: | |
| """ | |
| Detects the largest face and returns a padded RGB crop. | |
| Returns None if no face detected (caller should decide what to do). | |
| """ | |
| gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) | |
| faces = self.detector.detectMultiScale( | |
| gray, scaleFactor=1.1, minNeighbors=4, minSize=(48, 48) | |
| ) | |
| h, w = frame.shape[:2] | |
| if len(faces) == 0: | |
| # Fallback: centre-crop as square | |
| size = min(h, w) | |
| y0 = (h - size) // 2 | |
| x0 = (w - size) // 2 | |
| crop = frame[y0 : y0 + size, x0 : x0 + size] | |
| else: | |
| # Largest face | |
| fx, fy, fw, fh = max(faces, key=lambda r: r[2] * r[3]) | |
| pad = int(max(fw, fh) * padding_ratio) | |
| x1 = max(0, fx - pad) | |
| y1 = max(0, fy - pad) | |
| x2 = min(w, fx + fw + pad) | |
| y2 = min(h, fy + fh + pad) | |
| crop = frame[y1:y2, x1:x2] | |
| if crop.size == 0: | |
| return None | |
| crop = cv2.resize(crop, target_size, interpolation=cv2.INTER_AREA) | |
| crop = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB) | |
| return crop | |
| # ββ Full pipeline ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def extract( | |
| self, | |
| video_path: str, | |
| max_frames: int = 32, | |
| sample_fps: float = 2.0, | |
| ) -> List[dict]: | |
| """ | |
| Returns list of dicts: {frame_idx, face_rgb (H,W,3 uint8)} | |
| """ | |
| raw_frames = self.sample_frames(video_path, max_frames, sample_fps) | |
| results = [] | |
| for fidx, frame in raw_frames: | |
| face = self.crop_face(frame) | |
| if face is not None: | |
| results.append({"frame_idx": fidx, "face": face}) | |
| return results | |