"""Framed Ft and Zy measurements shared by interface 42 and the ratio preview.""" import math FT_POINTS = ("hair_line_right_right", "hair_line_left_left") ZY_POINTS = ("face_edge_234", "face_edge_454") class PortraitQualityError(ValueError): """An input that must be rejected before feature analysis.""" def __init__(self, code, message): super().__init__(message) self.code = code def prepare_feature_portrait(image_bytes): """Check the source image before applying interface 52 framing.""" import cv2 import numpy as np from face_analysis.pose import PITCH_THRESHOLD, estimate_head_pose from hairline.face_landmarks import get_landmarker, to_landmark_list from hairline.framing import decode_portrait, normalize_portrait image = decode_portrait(image_bytes) if image is None: raise PortraitQualityError(1008, "图片格式不支持(仅 JPG / PNG)") landmarks = get_landmarker().detect( cv2.cvtColor(image, cv2.COLOR_BGR2RGB), include_iris=True ) if landmarks is None: raise PortraitQualityError(1001, "无法识别人像,请上传包含完整人脸的照片") h, w = image.shape[:2] face_xy = landmarks[:468, :2] * [w, h] if not np.isfinite(face_xy).all() or ( (face_xy <= 0).any() or (face_xy >= [w - 1, h - 1]).any() ): raise PortraitQualityError(1003, "人脸不完整,请保留完整的脸部轮廓") pose = estimate_head_pose(to_landmark_list(landmarks), w, h) # In the camera coordinate system, positive pitch points the face down. if pose is not None and pose[1] > PITCH_THRESHOLD: raise PortraitQualityError(1003, "俯拍角度过大,请将相机调整至与脸部平视") try: image, landmarks, _ = normalize_portrait(image, landmarks) except ValueError as exc: raise PortraitQualityError(1003, "人脸过于贴近边缘,无法扶正裁切") from exc return image, landmarks def detect_framed_ratio_points(image): """Use the interface 52 crop and return the endpoints in its coordinate space.""" from hairline.framing import prepare_portrait image, landmarks, framing = prepare_portrait(image) data = _detect_ratio_points(image, landmarks) data.update(framing=framing, coordinate_space="processed_image") return image, data def _detect_ratio_points(image, landmarks): from hairline.face_landmarks import to_landmark_list from .special_points import detect_special_points data = detect_special_points(image, landmarks_override=to_landmark_list(landmarks)) points = data["points"] data["ft_original_points"] = {key: points[key] for key in FT_POINTS} a, b = (points[key] for key in FT_POINTS) if a is not None and b is not None: middle_y = (a["y"] + b["y"]) / 2 points[FT_POINTS[0]] = {**a, "y": middle_y} points[FT_POINTS[1]] = {**b, "y": middle_y} data["ft_alignment"] = "horizontal_at_mean_y" return data def analyze_ratio_features(image, landmarks) -> dict[str, bool | None]: """Return both labels from the same measurement of the processed image.""" from .special_points import NoFaceError, NonFrontalError matched = None try: data = _detect_ratio_points(image, landmarks) except (NoFaceError, NonFrontalError): pass else: ft_left, ft_right, zy_left, zy_right = ( data["points"].get(key) for key in (*FT_POINTS, *ZY_POINTS) ) if all(point is not None for point in (ft_left, ft_right, zy_left, zy_right)): ft = abs(ft_left["x"] - ft_right["x"]) zy = math.hypot(zy_left["x"] - zy_right["x"], zy_left["y"] - zy_right["y"]) if ft > 0 and zy > 0: matched = ft / zy < 0.8 return {"zygoma_expansion": matched, "temple_hollowing": matched}