118 lines
5.1 KiB
Python
118 lines
5.1 KiB
Python
"""接口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
|