fix(发际线): 修正不对称刘海导致的模板叠加倾斜

按 MediaPipe 左右对应额头点协调模板延伸距离,保留人脸姿态与头部轮廓钳制,统一预览白线和生发遮罩的几何。补充对齐回归测试并更新贴图顺序测试及实现说明。

验证:13 项相关测试通过;两张用户图片完成前后对比,新图男性 7 型接口与生发图均成功返回。
This commit is contained in:
xsl
2026-11-01 15:14:29 +08:00
parent 8554850d55
commit a3fddbbc34
6 changed files with 145 additions and 6 deletions
+7
View File
@@ -20,6 +20,13 @@ MP_TOP_ANCHORS: list[int] = [
]
N_ANCHORS = len(MP_TOP_ANCHORS) # 17
# Anatomical left/right pairs, in MediaPipe indices. The anchor list is
# NOT mirror-ordered at its ends: 127 pairs with 356, 234 with 454.
MP_TOP_ANCHOR_PAIRS = [
(127, 356), (234, 454), (162, 389), (21, 251),
(54, 284), (103, 332), (67, 297), (109, 338),
]
# Vertex ID layout in the extended array of length 468 + 2*N_ANCHORS.
# [0 .. 468) : MediaPipe canonical landmarks
# [468 .. 468+N) : middle row (between MP boundary and hairline)
+40
View File
@@ -124,6 +124,46 @@ def build_hairline_mask(parse_map: np.ndarray) -> np.ndarray:
return build_hair_mask(parse_map)
def align_hairline_for_template(
landmarks_norm: np.ndarray,
hairline_norm: np.ndarray,
valid: np.ndarray,
image_shape: tuple[int, int],
) -> np.ndarray:
"""Keep a designed template aligned with the face rather than the fringe.
Uneven bangs can make the detected hair boundary much higher on one
side, shearing an otherwise balanced template. Average the upward
extension of each anatomical anchor pair, measured in image pixels.
Keep the face anchors and the perpendicular component unchanged so
facial roll/perspective remain visible; do not level the curve in the
image. Missing ray hits retain their fallback and never influence a
valid partner. Call silhouette clamping afterwards to bound the result.
This is for template placement only, not measurement of the real hairline.
"""
h, w = image_shape
scale = np.array([w, h], dtype=np.float64)
up = face_up_vector(landmarks_norm) * scale
length = np.linalg.norm(up)
if length < 1e-9:
return hairline_norm.copy()
up /= length
anchors = landmarks_norm[C.MP_TOP_ANCHORS, :2] * scale
points = hairline_norm * scale
distances = (points - anchors) @ up
indices = {mp_idx: i for i, mp_idx in enumerate(C.MP_TOP_ANCHORS)}
for left, right in C.MP_TOP_ANCHOR_PAIRS:
i, j = indices[left], indices[right]
if not (valid[i] and valid[j]):
continue
mean_distance = (distances[i] + distances[j]) / 2.0
for k in (i, j):
points[k] += (mean_distance - distances[k]) * up
return (points / scale).astype(np.float32)
def smooth_hairline(
hairline_norm: np.ndarray,
valid: np.ndarray,
+9 -4
View File
@@ -15,7 +15,8 @@ from . import comfyui
from .face_landmarks import FaceLandmarker
from .face_parsing import FaceParser
from .hairline_2d import (
smooth_hairline, sample_hairline_clamped,
smooth_hairline, sample_hairline, align_hairline_for_template,
clamp_hairline_to_silhouette,
)
from .lift_3d import lift_hairline_to_3d, build_middle_row, assemble_full
from .render import load_ext_mesh, load_texture_rgba, render_hairline_overlay, build_overlay_layer
@@ -180,17 +181,21 @@ def extract_502(image_bgr: np.ndarray):
def extract_context(image_bgr: np.ndarray):
"""照片(BGR) → {landmarks, parse_map, points, valid}。无人脸返回 None。
发际线几何检测固定用 `sample_hairline_clamped`(射线检测 + 头部轮廓钳制):
发际线几何使用射线检测 + 人脸对应点对齐 + 头部轮廓钳制:
左右刘海高低不同时,协调对应锚点的延伸距离,避免把设计模板拉歪;
保留人脸本身的倾斜。预览白线与生发黑线/遮罩共用同一套 points。
短发/剃光头照片(如 man_test.jpg)中间锚点检测失效时,纯射线检测的固定 fallback
偏移会把点顶到头部轮廓外面的背景,产生"发际线贴到头部外面"的视觉 bug;钳制兜底后
fallback 点不会再跑出头部轮廓,正常长发照片结果与旧行为一致。
fallback 点不会再跑出头部轮廓。
"""
rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
landmarks = get_landmarker().detect(rgb)
if landmarks is None:
return None
parse_map = get_parser().parse(rgb)
hairline_2d, valid = sample_hairline_clamped(landmarks, parse_map)
hairline_2d, valid = sample_hairline(landmarks, parse_map)
hairline_2d = align_hairline_for_template(landmarks, hairline_2d, valid, parse_map.shape)
hairline_2d = clamp_hairline_to_silhouette(hairline_2d, parse_map)
hairline_2d = smooth_hairline(hairline_2d, valid)
hairline_3d = lift_hairline_to_3d(landmarks, hairline_2d)
middle_3d = build_middle_row(landmarks, hairline_3d)