mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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.
115 lines
3.6 KiB
Python
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()
|