mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
131 lines
4.7 KiB
Python
131 lines
4.7 KiB
Python
"""Face landmark detection using MediaPipe FaceMesh for passport photo positioning."""
|
|||
|
|
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] # unused but kept for bridge.ts compatibility
|
||
|
|
settings = json.loads(sys.argv[3]) if len(sys.argv) > 3 else {}
|
||
|
|
|
||
|
|
try:
|
||
|
|
emit_progress(10, "Loading image")
|
||
|
|
from PIL import Image
|
||
|
|
|
||
|
|
img = Image.open(input_path).convert("RGB")
|
||
|
|
iw, ih = img.size
|
||
|
|
|
||
|
|
try:
|
||
|
|
import mediapipe as mp
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
emit_progress(20, "Initializing face mesh")
|
||
|
|
|
||
|
|
img_array = np.array(img)
|
||
|
|
|
||
|
|
mp_face_mesh = mp.solutions.face_mesh
|
||
|
|
face_mesh = mp_face_mesh.FaceMesh(
|
||
|
|
static_image_mode=True,
|
||
|
|
max_num_faces=1,
|
||
|
|
refine_landmarks=True,
|
||
|
|
min_detection_confidence=0.5,
|
||
|
|
)
|
||
|
|
|
||
|
|
emit_progress(30, "Detecting face landmarks")
|
||
|
|
results = face_mesh.process(img_array)
|
||
|
|
face_mesh.close()
|
||
|
|
|
||
|
|
if not results.multi_face_landmarks:
|
||
|
|
print(json.dumps({
|
||
|
|
"success": True,
|
||
|
|
"faceDetected": False,
|
||
|
|
"landmarks": None,
|
||
|
|
}))
|
||
|
|
return
|
||
|
|
|
||
|
|
emit_progress(60, "Extracting key points")
|
||
|
|
face_lm = results.multi_face_landmarks[0]
|
||
|
|
lms = face_lm.landmark
|
||
|
|
|
||
|
|
# Left eye center (average of key eye landmarks)
|
||
|
|
left_eye_indices = [33, 133, 159, 145, 160, 144, 158, 153]
|
||
|
|
left_eye_x = sum(lms[i].x for i in left_eye_indices) / len(left_eye_indices)
|
||
|
|
left_eye_y = sum(lms[i].y for i in left_eye_indices) / len(left_eye_indices)
|
||
|
|
|
||
|
|
# Right eye center
|
||
|
|
right_eye_indices = [362, 263, 386, 374, 385, 373, 387, 380]
|
||
|
|
right_eye_x = sum(lms[i].x for i in right_eye_indices) / len(right_eye_indices)
|
||
|
|
right_eye_y = sum(lms[i].y for i in right_eye_indices) / len(right_eye_indices)
|
||
|
|
|
||
|
|
# Eye center (midpoint between both eyes)
|
||
|
|
eye_center_x = (left_eye_x + right_eye_x) / 2
|
||
|
|
eye_center_y = (left_eye_y + right_eye_y) / 2
|
||
|
|
|
||
|
|
# Chin bottom (landmark 152)
|
||
|
|
chin_x = lms[152].x
|
||
|
|
chin_y = lms[152].y
|
||
|
|
|
||
|
|
# Forehead top (landmark 10)
|
||
|
|
forehead_x = lms[10].x
|
||
|
|
forehead_y = lms[10].y
|
||
|
|
|
||
|
|
# Nose tip (landmark 1)
|
||
|
|
nose_x = lms[1].x
|
||
|
|
nose_y = lms[1].y
|
||
|
|
|
||
|
|
# Estimate crown position
|
||
|
|
# The crown is above the forehead. Using anthropometric data:
|
||
|
|
# forehead-to-chin distance is roughly 85-90% of crown-to-chin.
|
||
|
|
# So crown is about 12-15% above forehead relative to chin-forehead distance.
|
||
|
|
forehead_chin_dist = chin_y - forehead_y
|
||
|
|
crown_y = forehead_y - (forehead_chin_dist * 0.15)
|
||
|
|
crown_x = forehead_x
|
||
|
|
|
||
|
|
# Face center X (average of nose, eye center)
|
||
|
|
face_center_x = (nose_x + eye_center_x) / 2
|
||
|
|
|
||
|
|
emit_progress(90, "Done")
|
||
|
|
|
||
|
|
print(json.dumps({
|
||
|
|
"success": True,
|
||
|
|
"faceDetected": True,
|
||
|
|
"landmarks": {
|
||
|
|
"leftEye": {"x": round(left_eye_x, 6), "y": round(left_eye_y, 6)},
|
||
|
|
"rightEye": {"x": round(right_eye_x, 6), "y": round(right_eye_y, 6)},
|
||
|
|
"eyeCenter": {"x": round(eye_center_x, 6), "y": round(eye_center_y, 6)},
|
||
|
|
"chin": {"x": round(chin_x, 6), "y": round(chin_y, 6)},
|
||
|
|
"forehead": {"x": round(forehead_x, 6), "y": round(forehead_y, 6)},
|
||
|
|
"crown": {"x": round(crown_x, 6), "y": round(crown_y, 6)},
|
||
|
|
"nose": {"x": round(nose_x, 6), "y": round(nose_y, 6)},
|
||
|
|
"faceCenterX": round(face_center_x, 6),
|
||
|
|
},
|
||
|
|
"imageWidth": iw,
|
||
|
|
"imageHeight": ih,
|
||
|
|
}))
|
||
|
|
|
||
|
|
except ImportError:
|
||
|
|
print(json.dumps({
|
||
|
|
"success": False,
|
||
|
|
"error": "Face landmark detection requires MediaPipe. Install with: pip install mediapipe",
|
||
|
|
}))
|
||
|
|
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()
|