2026-03-22 04:31:49 +08:00
|
|
|
"""Face detection and blurring using MediaPipe."""
|
|
|
|
|
import sys
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
|
2026-03-23 01:38:19 +08:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-03-22 04:31:49 +08:00
|
|
|
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:
|
2026-03-23 01:38:19 +08:00
|
|
|
emit_progress(10, "Loading face detection model")
|
2026-03-22 04:31:49 +08:00
|
|
|
from PIL import Image, ImageFilter
|
|
|
|
|
|
|
|
|
|
img = Image.open(input_path).convert("RGB")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
import mediapipe as mp
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
2026-03-23 01:38:19 +08:00
|
|
|
emit_progress(20, "Model ready")
|
|
|
|
|
|
2026-03-22 04:31:49 +08:00
|
|
|
mp_face = mp.solutions.face_detection
|
|
|
|
|
|
|
|
|
|
with mp_face.FaceDetection(
|
|
|
|
|
min_detection_confidence=sensitivity
|
|
|
|
|
) as detector:
|
|
|
|
|
img_array = np.array(img)
|
2026-03-23 01:38:19 +08:00
|
|
|
emit_progress(25, "Scanning for faces")
|
2026-03-22 04:31:49 +08:00
|
|
|
results = detector.process(img_array)
|
|
|
|
|
|
|
|
|
|
faces = []
|
2026-03-23 01:38:19 +08:00
|
|
|
emit_progress(50, f"Found {len(results.detections or [])} faces")
|
2026-03-22 04:31:49 +08:00
|
|
|
if results.detections:
|
2026-03-23 01:38:19 +08:00
|
|
|
for i, detection in enumerate(results.detections):
|
2026-03-22 04:31:49 +08:00
|
|
|
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
|
|
|
|
|
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": x, "y": y, "w": w, "h": h})
|
2026-03-23 01:38:19 +08:00
|
|
|
emit_progress(50 + int((i + 1) / max(len(results.detections), 1) * 40), f"Blurring face {i + 1} of {len(results.detections)}")
|
2026-03-22 04:31:49 +08:00
|
|
|
|
2026-03-23 01:38:19 +08:00
|
|
|
emit_progress(95, "Saving result")
|
2026-03-22 04:31:49 +08:00
|
|
|
img.save(output_path)
|
|
|
|
|
print(
|
|
|
|
|
json.dumps(
|
|
|
|
|
{
|
|
|
|
|
"success": True,
|
|
|
|
|
"facesDetected": len(faces),
|
|
|
|
|
"faces": faces,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
except ImportError:
|
2026-03-22 19:28:57 +08:00
|
|
|
# MediaPipe not available — report error clearly
|
2026-03-22 04:31:49 +08:00
|
|
|
print(
|
|
|
|
|
json.dumps(
|
|
|
|
|
{
|
2026-03-22 19:28:57 +08:00
|
|
|
"success": False,
|
|
|
|
|
"error": "Face detection requires the mediapipe package. Install with: pip install mediapipe",
|
2026-03-22 04:31:49 +08:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-03-22 19:28:57 +08:00
|
|
|
sys.exit(1)
|
2026-03-22 04:31:49 +08:00
|
|
|
|
|
|
|
|
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()
|