from __future__ import annotations from typing import Tuple import numpy as np _DST_5PTS_112 = np.array( [ [38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041], ], dtype=np.float32, ) def align_face_5pts(img_rgb: np.ndarray, landmarks5: np.ndarray, out_size: Tuple[int, int] = (112, 112)) -> np.ndarray: """Return aligned RGB image (H,W,3) uint8.""" import cv2 if img_rgb is None or img_rgb.ndim != 3 or img_rgb.shape[2] != 3: raise ValueError("img_rgb must be HxWx3") lmk = np.asarray(landmarks5, dtype=np.float32) if lmk.shape != (5, 2): raise ValueError("landmarks5 must be shape (5,2)") dst = _DST_5PTS_112.copy() if out_size != (112, 112): sx = out_size[0] / 112.0 sy = out_size[1] / 112.0 dst[:, 0] *= sx dst[:, 1] *= sy M, inliers = cv2.estimateAffinePartial2D(lmk, dst, method=cv2.LMEDS) if M is None or not np.isfinite(M).all(): raise ValueError("estimateAffinePartial2D failed") w, h = out_size aligned = cv2.warpAffine(img_rgb, M, (w, h), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=0) return aligned