Files
hair/hairline/height_rule.py
T

118 lines
5.1 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.
"""接口52设计高度:目标上庭为眉心到下巴垂直距离的 45%。"""
from __future__ import annotations
import cv2
import numpy as np
from .mask import mask_from_curve
def height_target(landmarks, width, height, reference=None):
"""Use unrounded measurement coordinates; fall back to the same MP indices.
Distances use image Y, matching face_analysis.measure's court heights.
No rounded centimetre values or existing-hairline estimates enter the rule.
"""
scale = np.array([width, height], dtype=np.float64)
if reference is None:
brow, nose, chin = landmarks[[9, 94, 152], :2] * scale
source = "hairline_landmarks"
px_per_cm = None
else:
brow = np.asarray(reference["brow_center"], dtype=np.float64)
nose = np.asarray(reference["nose_bottom"], dtype=np.float64)
chin = np.asarray(reference["chin_tip"], dtype=np.float64)
source = "face_measure"
px_per_cm = reference.get("px_per_cm")
if not np.isfinite([brow, nose, chin]).all() or not brow[1] < nose[1] < chin[1]:
raise ValueError("眉心、鼻翼下缘与下巴坐标异常,无法计算目标上庭高度")
middle, lower = float(nose[1] - brow[1]), float(chin[1] - nose[1])
upper = (middle + lower) * 0.45
target_y = float(brow[1] - upper)
if not (0 <= brow[0] < width and 1 <= target_y < height - 1):
raise ValueError("按中下庭高度计算的目标发际线超出图片范围,请使用完整正面照")
def pt(p):
return {"x": float(p[0]), "y": float(p[1])}
return {
"name": "court_average",
"formula": "上庭 = (中庭 + 下庭) × 0.45",
"measurement_source": source,
"brow_center": pt(brow), "nose_bottom": pt(nose), "chin_tip": pt(chin),
"middle_height_px": middle, "lower_height_px": lower,
"target_upper_height_px": upper,
"target_upper_height_cm": upper / px_per_cm if px_per_cm else None,
"target_center": {"x": float(brow[0]), "y": target_y},
}
def clean_curve(overlay):
"""Keep the white design line; discard opaque black texture-border pixels."""
out = np.zeros_like(overlay, dtype=np.float32)
out[:, :, :3] = 255
out[:, :, 3] = overlay[:, :, 3] * (overlay[:, :, :3].mean(axis=2) > 128)
return out
def curve_center(overlay, center_x):
"""Alpha-weighted centre of the line at the facial midline, in image pixels."""
h, w = overlay.shape[:2]
column = int(round(center_x))
if not 0 <= column < w:
raise ValueError("面部中轴超出图片范围")
alpha = overlay[:, column, 3].astype(np.float64)
alpha[alpha <= 1] = 0
if not alpha.any():
# Rasterized endpoints may leave a 1px gap, but distant sideburns
# must never be silently substituted for a missing central curve.
columns = np.flatnonzero((overlay[:, :, 3] > 1).any(axis=0))
if not columns.size or np.min(np.abs(columns - column)) > 2:
raise ValueError("发际线模板在面部中轴处没有有效曲线")
column = int(columns[np.argmin(np.abs(columns - column))])
alpha = overlay[:, column, 3].astype(np.float64)
alpha[alpha <= 1] = 0
return {"x": float(center_x), "y": float(np.dot(np.arange(h), alpha) / alpha.sum())}
def place_curve(overlay, target, landmarks):
"""Rigidly shift a rendered template along face-up to meet the target Y.
Keeping the curve rigid preserves roll and the template's shape. Pixel Y
remains the height convention, even for a rolled face. A short correction
compensates for the curve's slope as the local-axis shift also changes X.
"""
curve = clean_curve(overlay)
h, w = curve.shape[:2]
center_x, target_y = target["x"], target["y"]
before = curve_center(curve, center_x)
up = (landmarks[10, :2] - landmarks[152, :2]) * [w, h]
if abs(up[1]) < 1e-6:
raise ValueError("无法确定人脸向上方向")
x_per_y = float(np.clip(up[0] / up[1], -1.0, 1.0))
dy = target_y - before["y"]
moved = curve
for _ in range(8):
transform = np.array([[1, 0, dy * x_per_y], [0, 1, dy]], np.float32)
moved = cv2.warpAffine(curve, transform, (w, h), flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
center = curve_center(moved, center_x)
residual = target_y - center["y"]
if abs(residual) <= 0.15:
break
dy += residual
if abs(center["y"] - target_y) > 1.0:
raise ValueError("发际线中心无法对齐目标高度,请检查照片姿态与裁剪范围")
return moved, {
"old_center": before, "new_center": center,
"shift_x_px": float(dy * x_per_y), "shift_y_px": float(dy),
"target_error_px": float(abs(center["y"] - target_y)),
}
def inpaint_from_curve(photo, landmarks, parse_map, overlay):
"""The preview line is also the exact boundary used for the black line/mask."""
alpha = np.clip(overlay[:, :, 3:4] / 255.0, 0, 1)
marked = np.clip(photo * (1.0 - alpha), 0, 255).astype(np.uint8)
curve = (overlay[:, :, 3] > 40).astype(np.uint8) * 255
mask = mask_from_curve(curve, landmarks, parse_map)
return marked, mask