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.
This commit is contained in:
Siddharth Kumar Sah
2026-03-26 16:01:56 +08:00
parent 8ee4d7b2fb
commit f15102c632
3 changed files with 58 additions and 50 deletions
+5
View File
@@ -3,6 +3,7 @@ import { writeFile } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { blurFaces } from "@stirling-image/ai"; import { blurFaces } from "@stirling-image/ai";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { autoOrient } from "../../lib/auto-orient.js";
import { validateImageBuffer } from "../../lib/file-validation.js"; import { validateImageBuffer } from "../../lib/file-validation.js";
import { createWorkspace } from "../../lib/workspace.js"; import { createWorkspace } from "../../lib/workspace.js";
import { updateSingleFileProgress } from "../progress.js"; import { updateSingleFileProgress } from "../progress.js";
@@ -52,6 +53,10 @@ export function registerBlurFaces(app: FastifyInstance) {
try { try {
const settings = settingsRaw ? JSON.parse(settingsRaw) : {}; const settings = settingsRaw ? JSON.parse(settingsRaw) : {};
// Auto-orient to fix EXIF rotation before face detection
fileBuffer = await autoOrient(fileBuffer);
const jobId = randomUUID(); const jobId = randomUUID();
const workspacePath = await createWorkspace(jobId); const workspacePath = await createWorkspace(jobId);
@@ -69,11 +69,6 @@ export function BlurFacesSettings() {
</div> </div>
</div> </div>
{/* Info */}
<p className="text-[10px] text-muted-foreground">
Uses MediaPipe for face detection. Automatically detects and blurs all faces in the image.
</p>
{/* Error */} {/* Error */}
{error && <p className="text-xs text-red-500">{error}</p>} {error && <p className="text-xs text-red-500">{error}</p>}
@@ -91,7 +86,6 @@ export function BlurFacesSettings() {
active={processing} active={processing}
phase={progress.phase === "idle" ? "uploading" : progress.phase} phase={progress.phase === "idle" ? "uploading" : progress.phase}
label="Blurring faces" label="Blurring faces"
stage={progress.stage}
percent={progress.percent} percent={progress.percent}
elapsed={progress.elapsed} elapsed={progress.elapsed}
/> />
+53 -44
View File
@@ -1,4 +1,4 @@
"""Face detection and blurring using MediaPipe.""" """Face detection and blurring using OpenCV."""
import sys import sys
import json import json
@@ -17,70 +17,79 @@ def main():
sensitivity = settings.get("sensitivity", 0.5) sensitivity = settings.get("sensitivity", 0.5)
try: try:
emit_progress(10, "Loading face detection model") emit_progress(10, "Preparing")
from PIL import Image, ImageFilter from PIL import Image, ImageFilter
img = Image.open(input_path).convert("RGB") img = Image.open(input_path).convert("RGB")
try: try:
import mediapipe as mp import cv2
import numpy as np import numpy as np
emit_progress(20, "Model ready") emit_progress(20, "Ready")
mp_face = mp.solutions.face_detection # Load Haar cascade for face detection
haar_path = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
face_cascade = cv2.CascadeClassifier(haar_path)
with mp_face.FaceDetection( # Convert to grayscale for detection
min_detection_confidence=sensitivity img_array = np.array(img)
) as detector: gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)
img_array = np.array(img)
emit_progress(25, "Scanning for faces")
results = detector.process(img_array)
faces = [] # Map sensitivity (0.1-0.9) to minNeighbors (8-2)
emit_progress(50, f"Found {len(results.detections or [])} faces") # Higher sensitivity = fewer required neighbors = more detections
if results.detections: min_neighbors = max(2, int(8 - sensitivity * 7))
for i, detection in enumerate(results.detections):
bbox = detection.location_data.relative_bounding_box
x = int(bbox.xmin * img.width)
y = int(bbox.ymin * img.height)
w = int(bbox.width * img.width)
h = int(bbox.height * img.height)
# Add some padding around the face emit_progress(25, "Scanning for faces")
pad = int(max(w, h) * 0.1) faces_detected = face_cascade.detectMultiScale(
x1 = max(0, x - pad) gray,
y1 = max(0, y - pad) scaleFactor=1.1,
x2 = min(img.width, x + w + pad) minNeighbors=min_neighbors,
y2 = min(img.height, y + h + pad) minSize=(30, 30),
)
face_region = img.crop((x1, y1, x2, y2)) faces = []
blurred = face_region.filter( num_faces = len(faces_detected)
ImageFilter.GaussianBlur(blur_radius) emit_progress(50, f"Found {num_faces} face{'s' if num_faces != 1 else ''}")
)
img.paste(blurred, (x1, y1))
faces.append({"x": x, "y": y, "w": w, "h": h})
emit_progress(50 + int((i + 1) / max(len(results.detections), 1) * 40), f"Blurring face {i + 1} of {len(results.detections)}")
emit_progress(95, "Saving result") if num_faces > 0:
img.save(output_path) for i, (x, y, w, h) in enumerate(faces_detected):
print( # Add padding around the face
json.dumps( pad = int(max(w, h) * 0.1)
{ x1 = max(0, x - pad)
"success": True, y1 = max(0, y - pad)
"facesDetected": len(faces), x2 = min(img.width, x + w + pad)
"faces": faces, 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: except ImportError:
# MediaPipe not available — report error clearly
print( print(
json.dumps( json.dumps(
{ {
"success": False, "success": False,
"error": "Face detection requires the mediapipe package. Install with: pip install mediapipe", "error": "Face detection requires OpenCV. Install with: pip install opencv-python-headless",
} }
) )
) )