FaceSWAP / core /landmarks.py
aditya-rAj19's picture
Fix all listed technical challenges across detection, landmarks, segmentation
f4995df
Raw
History Blame Contribute Delete
3.59 kB
import cv2
import numpy as np
_mp_face_mesh = None
_face_mesh_instance = None
def _get_face_mesh():
global _mp_face_mesh, _face_mesh_instance
if _face_mesh_instance is None:
try:
import mediapipe as mp
_mp_face_mesh = mp.solutions.face_mesh
_face_mesh_instance = _mp_face_mesh.FaceMesh(
static_image_mode=True,
max_num_faces=2,
refine_landmarks=True,
min_detection_confidence=0.5
)
except Exception:
_face_mesh_instance = None
return _face_mesh_instance
def extract_landmarks_468(image: np.ndarray) -> np.ndarray | None:
"""
Extract 468 facial landmarks using MediaPipe Face Mesh.
Returns array of shape (468, 2) with (x, y) pixel coords, or None.
"""
face_mesh = _get_face_mesh()
if face_mesh is None:
return _fallback_landmarks(image)
h, w = image.shape[:2]
rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
results = face_mesh.process(rgb)
if not results.multi_face_landmarks:
return _fallback_landmarks(image)
lm = results.multi_face_landmarks[0].landmark
pts = np.array([[l.x * w, l.y * h] for l in lm], dtype=np.float32)
return pts # shape (468, 2)
def extract_landmarks_68(image: np.ndarray) -> np.ndarray | None:
"""
Extract 68 facial landmarks. Uses the first 68 points from MediaPipe
mapped to dlib-compatible indices.
Returns array of shape (68, 2), or None.
"""
pts468 = extract_landmarks_468(image)
if pts468 is None:
return None
# MediaPipe to dlib 68-point index mapping (approximate)
MP_TO_DLIB_68 = [
162, 234, 93, 58, 172, 136, 149, 148, 152, 377, 378, 365,
397, 288, 323, 454, 389, 71, 63, 105, 66, 107, 336, 296,
334, 293, 301, 168, 197, 5, 4, 75, 97, 2, 326, 305,
33, 160, 158, 133, 153, 144, 362, 385, 387, 263, 373, 380,
61, 39, 37, 0, 267, 269, 291, 405, 314, 17, 84, 181,
78, 82, 13, 312, 308, 317, 14, 87
]
return pts468[MP_TO_DLIB_68]
def _fallback_landmarks(image: np.ndarray) -> np.ndarray | None:
"""
Fallback when MediaPipe is unavailable: use InsightFace 5-point keypoints
(left eye, right eye, nose, left mouth, right mouth) mapped to a sparse
468-point array so downstream callers get real anatomical positions.
Returns None if InsightFace is also unavailable — callers must handle None.
The previous implementation returned random uniform noise inside the face
bounding box, which silently corrupted alignment transforms and quality
scores whenever MediaPipe failed.
"""
from .detector import _get_insightface
app = _get_insightface()
if app is None:
return None
try:
faces = app.get(image)
if not faces:
return None
face = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))
kps = np.asarray(face.kps, dtype=np.float32) # shape (5, 2)
# Map the 5 InsightFace keypoints to their nearest MediaPipe indices so
# _get_key_indices() in aligner.py selects them correctly.
# kps[0] = left eye → MP 33
# kps[1] = right eye → MP 263
# kps[2] = nose tip → MP 1
# kps[3] = left mouth → MP 61
# kps[4] = right mouth→ MP 291
pts = np.zeros((468, 2), dtype=np.float32)
for mp_idx, kp in zip([33, 263, 1, 61, 291], kps):
pts[mp_idx] = kp
return pts
except Exception:
return None