Files
hair/face_analysis/face_ratios.py
T

107 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 .calibration import normalized_to_pixel
from .special_points import detect_special_points
override = to_landmark_list(landmarks)
data = detect_special_points(image, landmarks_override=override)
points = data["points"]
height, width = image.shape[:2]
x, y = normalized_to_pixel(override.landmark[454], width, height)
points["face_edge_454"] = {"x": round(x), "y": round(y)}
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}