Files
SnapOtter/packages/ai/python/detect_faces.py
T
Siddharth Kumar Sah f15102c632 fix(blur-faces): switch from MediaPipe to OpenCV and auto-orient images
Fix face detection failure caused by MediaPipe 0.10.33 removing the
mp.solutions API. Replace with OpenCV Haar cascade which works reliably
in headless Docker. Add autoOrient() call before detection to handle
EXIF-rotated phone photos. Remove technical jargon from UI.
2026-03-26 16:01:56 +08:00

115 lines
3.6 KiB
Python

"""Face detection and blurring using OpenCV."""
import sys
import json
def emit_progress(percent, stage):
"""Emit structured progress to stderr for bridge.ts to capture."""
print(json.dumps({"progress": percent, "stage": stage}), file=sys.stderr, flush=True)
def main():
input_path = sys.argv[1]
output_path = sys.argv[2]
settings = json.loads(sys.argv[3]) if len(sys.argv) > 3 else {}
blur_radius = settings.get("blurRadius", 30)
sensitivity = settings.get("sensitivity", 0.5)
try:
emit_progress(10, "Preparing")
from PIL import Image, ImageFilter
img = Image.open(input_path).convert("RGB")
try:
import cv2
import numpy as np
emit_progress(20, "Ready")
# Load Haar cascade for face detection
haar_path = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
face_cascade = cv2.CascadeClassifier(haar_path)
# Convert to grayscale for detection
img_array = np.array(img)
gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)
# Map sensitivity (0.1-0.9) to minNeighbors (8-2)
# Higher sensitivity = fewer required neighbors = more detections
min_neighbors = max(2, int(8 - sensitivity * 7))
emit_progress(25, "Scanning for faces")
faces_detected = face_cascade.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=min_neighbors,
minSize=(30, 30),
)
faces = []
num_faces = len(faces_detected)
emit_progress(50, f"Found {num_faces} face{'s' if num_faces != 1 else ''}")
if num_faces > 0:
for i, (x, y, w, h) in enumerate(faces_detected):
# Add padding around the face
pad = int(max(w, h) * 0.1)
x1 = max(0, x - pad)
y1 = max(0, y - pad)
x2 = min(img.width, x + w + pad)
y2 = min(img.height, y + h + pad)
face_region = img.crop((x1, y1, x2, y2))
blurred = face_region.filter(
ImageFilter.GaussianBlur(blur_radius)
)
img.paste(blurred, (x1, y1))
faces.append({"x": int(x), "y": int(y), "w": int(w), "h": int(h)})
emit_progress(
50 + int((i + 1) / num_faces * 40),
f"Blurring face {i + 1} of {num_faces}",
)
emit_progress(95, "Saving result")
img.save(output_path)
print(
json.dumps(
{
"success": True,
"facesDetected": len(faces),
"faces": faces,
}
)
)
except ImportError:
print(
json.dumps(
{
"success": False,
"error": "Face detection requires OpenCV. Install with: pip install opencv-python-headless",
}
)
)
sys.exit(1)
except ImportError:
print(
json.dumps(
{
"success": False,
"error": "Pillow is not installed. Install with: pip install Pillow",
}
)
)
sys.exit(1)
except Exception as e:
print(json.dumps({"success": False, "error": str(e)}))
sys.exit(1)
if __name__ == "__main__":
main()