fix(发际线): 修正不对称刘海导致的模板叠加倾斜
按 MediaPipe 左右对应额头点协调模板延伸距离,保留人脸姿态与头部轮廓钳制,统一预览白线和生发遮罩的几何。补充对齐回归测试并更新贴图顺序测试及实现说明。 验证:13 项相关测试通过;两张用户图片完成前后对比,新图男性 7 型接口与生发图均成功返回。
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user