161 lines
4.8 KiB
Python
161 lines
4.8 KiB
Python
"""
|
|
Face detector using OpenCV Haar Cascades.
|
|
|
|
This module provides face detection functionality optimized for
|
|
privacy blur in video editing workflows.
|
|
"""
|
|
|
|
import os
|
|
from typing import List, Tuple, Optional
|
|
import numpy as np
|
|
|
|
|
|
class FaceDetector:
|
|
"""
|
|
Face detector using OpenCV Haar Cascades.
|
|
|
|
Optimized for privacy blur use case:
|
|
- Detects frontal faces
|
|
- Configurable detection sensitivity
|
|
- Generates feathered masks for smooth blur edges
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
scale_factor: float = 1.1,
|
|
min_neighbors: int = 5,
|
|
min_size: Tuple[int, int] = (30, 30),
|
|
):
|
|
"""
|
|
Initialize the face detector.
|
|
|
|
Args:
|
|
scale_factor: Image pyramid scale factor
|
|
min_neighbors: Minimum neighbors for detection
|
|
min_size: Minimum face size in pixels
|
|
"""
|
|
self.scale_factor = scale_factor
|
|
self.min_neighbors = min_neighbors
|
|
self.min_size = min_size
|
|
self._classifier = None
|
|
|
|
@property
|
|
def classifier(self):
|
|
"""Lazy-load the Haar cascade classifier."""
|
|
if self._classifier is None:
|
|
import cv2
|
|
|
|
# Use haarcascade for frontal face detection
|
|
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
|
|
|
|
if not os.path.exists(cascade_path):
|
|
raise RuntimeError(f"Haar cascade not found: {cascade_path}")
|
|
|
|
self._classifier = cv2.CascadeClassifier(cascade_path)
|
|
|
|
return self._classifier
|
|
|
|
def detect(self, frame: np.ndarray) -> List[Tuple[int, int, int, int]]:
|
|
"""
|
|
Detect faces in a frame.
|
|
|
|
Args:
|
|
frame: BGR image as numpy array
|
|
|
|
Returns:
|
|
List of face bounding boxes as (x, y, width, height)
|
|
"""
|
|
import cv2
|
|
|
|
# Convert to grayscale for detection
|
|
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
|
|
# Detect faces
|
|
faces = self.classifier.detectMultiScale(
|
|
gray,
|
|
scaleFactor=self.scale_factor,
|
|
minNeighbors=self.min_neighbors,
|
|
minSize=self.min_size,
|
|
flags=cv2.CASCADE_SCALE_IMAGE,
|
|
)
|
|
|
|
# Convert to list of tuples
|
|
return [tuple(face) for face in faces]
|
|
|
|
def generate_mask(
|
|
self,
|
|
frame_shape: Tuple[int, int, int],
|
|
detections: List[Tuple[int, int, int, int]],
|
|
mask_scale: float = 1.5,
|
|
feather_radius: int = 20,
|
|
) -> np.ndarray:
|
|
"""
|
|
Generate a mask image from face detections.
|
|
|
|
Args:
|
|
frame_shape: Shape of the original frame (height, width, channels)
|
|
detections: List of face bounding boxes
|
|
mask_scale: Scale factor for mask region (1.0 = exact bounding box)
|
|
feather_radius: Radius for edge feathering
|
|
|
|
Returns:
|
|
Grayscale mask image (white = blur, black = keep)
|
|
"""
|
|
import cv2
|
|
|
|
height, width = frame_shape[:2]
|
|
mask = np.zeros((height, width), dtype=np.uint8)
|
|
|
|
for (x, y, w, h) in detections:
|
|
# Scale the bounding box
|
|
center_x = x + w // 2
|
|
center_y = y + h // 2
|
|
|
|
scaled_w = int(w * mask_scale)
|
|
scaled_h = int(h * mask_scale)
|
|
|
|
# Calculate scaled bounding box
|
|
x1 = max(0, center_x - scaled_w // 2)
|
|
y1 = max(0, center_y - scaled_h // 2)
|
|
x2 = min(width, center_x + scaled_w // 2)
|
|
y2 = min(height, center_y + scaled_h // 2)
|
|
|
|
# Draw ellipse for more natural face shape
|
|
cv2.ellipse(
|
|
mask,
|
|
(center_x, center_y),
|
|
(scaled_w // 2, scaled_h // 2),
|
|
0, # angle
|
|
0, 360, # arc
|
|
255, # color (white)
|
|
-1, # filled
|
|
)
|
|
|
|
# Apply Gaussian blur for feathering
|
|
if feather_radius > 0 and len(detections) > 0:
|
|
# Ensure kernel size is odd
|
|
kernel_size = feather_radius * 2 + 1
|
|
mask = cv2.GaussianBlur(mask, (kernel_size, kernel_size), 0)
|
|
|
|
return mask
|
|
|
|
|
|
def detect_faces_batch(
|
|
frames: List[np.ndarray],
|
|
detector: Optional[FaceDetector] = None,
|
|
) -> List[List[Tuple[int, int, int, int]]]:
|
|
"""
|
|
Detect faces in multiple frames.
|
|
|
|
Args:
|
|
frames: List of BGR images
|
|
detector: Optional detector instance (creates one if not provided)
|
|
|
|
Returns:
|
|
List of detection lists, one per frame
|
|
"""
|
|
if detector is None:
|
|
detector = FaceDetector()
|
|
|
|
return [detector.detect(frame) for frame in frames]
|