Files
hair/hairline/constants.py
T
xsl a3fddbbc34 fix(发际线): 修正不对称刘海导致的模板叠加倾斜
按 MediaPipe 左右对应额头点协调模板延伸距离,保留人脸姿态与头部轮廓钳制,统一预览白线和生发遮罩的几何。补充对齐回归测试并更新贴图顺序测试及实现说明。

验证:13 项相关测试通过;两张用户图片完成前后对比,新图男性 7 型接口与生发图均成功返回。
2026-11-01 15:14:29 +08:00

178 lines
7.8 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.
"""Shared constants for the hairline-extension pipeline.
These define the topology of the extended mesh and must stay in sync with
the C++ side (see sdk/ExtensionConstants.h). If you change anything here,
regenerate face_ext.obj and update the C++ header.
"""
from __future__ import annotations
# Number of MediaPipe FaceMesh landmarks (no iris refinement).
N_MP = 468
# Anchors along the upper boundary of the MediaPipe face mesh,
# ordered left-to-right when viewing the face frontally.
# Each anchor will get a paired hairline sample directly "above" it.
#
# Verify visually with scripts/show_anchors.py before locking these in.
MP_TOP_ANCHORS: list[int] = [
127, 234, 162, 21, 54, 103, 67, 109, 10,
338, 297, 332, 284, 251, 389, 356, 454,
]
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)
# [468+N .. 468+2N) : hairline row
N_EXT = 2 * N_ANCHORS # 34
N_TOTAL = N_MP + N_EXT # 502
MIDDLE_START = N_MP # 468
HAIRLINE_START = N_MP + N_ANCHORS # 485
# Saggital head-curvature radius (in MediaPipe normalized-Y units),
# expressed as a fraction of face height. The head's mid-line cross
# section is treated locally as a circular arc; for a hairline / middle
# vertex located dy=(y_hair - y_anchor) above an MP top anchor (dy < 0
# since hairline y < anchor y) we compute its Z by
#
# z_hair = z_anchor + dy² / (2 R), R = HEAD_ARC_RADIUS_FRAC × face_h
#
# This always pushes the added vertex BACKWARD (toward +z in MP / face.obj
# convention, i.e. toward the back of the head), matching the actual
# anatomy. See README for the derivation.
#
# Smaller fraction = more pronounced backward bulge. 0.30 produces a
# moderate offset (~0.024 in normalized z for a 0.08-y hairline lift on
# a typical face).
HEAD_ARC_RADIUS_FRAC = 0.30
# Extra lift applied to the detected hairline along the face-up direction,
# expressed as a fraction of the MP face height. The 2D hairline detector
# stops at the hair-skin boundary (start of the visible hair); the mesh
# ribbon's top row should sit at the crown of the head instead, so the
# texture-overlay band can cover the whole forehead → crown region.
#
# 0.06 ≈ moves the hairline row up by 6% of face height, which on the
# reference photos lands the top row just above the visible hairline and
# below the crown — empirically tuned with the /preview slider and locked
# in as the project default. Bump to 0.10..0.15 for taller foreheads /
# higher crowns; drop to 0.03 to keep the ribbon hugging the hair-skin
# boundary.
HAIRLINE_CROWN_LIFT_FRAC = 0.06
# Pure-geometry hairline offset for the /preview 502-point pipeline.
#
# When placing the hairline row WITHOUT hair detection (works for bald /
# with-hair / hat — all head types), each anchor is offset upward along
# face-up by GEOMETRIC_HAIRLINE_OFFSET_FRAC × face_h in normalised
# image space. The sagittal-arc model then derives Z.
#
# 0.25 × face_h ≈ 0.12 normalised on a typical face (face_h ≈ 0.48),
# giving dz ≈ 0.05 — matches the real hairline distance observed on
# reference photos where hair detection succeeds.
GEOMETRIC_HAIRLINE_OFFSET_FRAC = 0.25
# UV layout for the 34 forehead-extension vertices.
#
# The texture (imgs/texture0.png, 512×512) is laid out with the original
# MediaPipe face skin in V_raw ≈ 0.00..0.77 (image y ≈ 117..511) and a
# horizontal stack of 5 hairline-design arcs at the TOP of the image
# (image y ≈ 30..170, i.e. V_raw ≈ 0.67..0.94). Those 5 arcs are the
# content that the extension strip is supposed to display: hairline row
# samples the topmost arc (blue), middle row samples the bottom arc
# (orange), and the 3 arcs in between fall out automatically because the
# ribbon triangle interpolates V linearly between the two rows.
#
# V conventions
# -------------
# uv_template.py / Three.js (with texture.flipY=true) treat V_raw=1 as
# the TOP of the image (image y=0) and V_raw=0 as the BOTTOM.
#
# U conventions — IMPORTANT
# -------------------------
# The 17 MP_TOP_ANCHORS in face.obj have NON-uniform u (≈ 0.00 at the
# temples, ≈ 0.50 at the forehead center, ≈ 1.00 at the other temple).
# Each ribbon triangle (anchor[i] → middle[i] → anchor[i+1] etc.) is a
# vertical column in UV space ONLY when the middle/hairline vertex
# inherits its U from the corresponding anchor. If we used a uniform
# 0.05..0.95 U for the strip the columns would slant relative to the
# anchor U values, warping the texture's 5 horizontal arcs into
# zig-zags. So `extension_uv_for` takes `anchor_u` and copies it.
UV_MIDDLE_DV = 0.110 # middle 行 V_raw 相对该列 anchor V_raw 上移这么多
UV_HAIRLINE_DV = 0.220 # hairline 行 V_raw 相对该列 anchor V_raw 上移这么多
# 为什么是"相对 anchor 平行偏移"而不是固定常数:
#
# face.obj 中 17 个 MP_TOP_ANCHORS 的 V_raw 是**非均匀弧形** (额头中央 MP 10
# = 0.7724, 太阳穴 MP 127 = 0.4668, 横跨 0.30 V 单位)。 如果 middle/hairline
# 用固定常数 V_raw (例如 0.82 / 0.998), 那每个 ribbon quad 的 V 跨度
# (= middle.V − anchor[i].V) 在 17 列之间差异巨大 (中央列 0.05, 两端列 0.35,
# 差了 7 倍)。 贴图最底部的弧线 (V_raw ≈ 0.76) 正好落在 V 跨度大的列上 →
# 被拉伸成粗大色块, 而顶部弧线 (V_raw ≈ 0.93) 落在 V 跨度小的列上 → 被压
# 缩成细线。 这就是"最下面那条线特别粗、上面 4 根都细"的根因。
#
# 把 middle/hairline 的 V 也设成"anchor V + 固定 Δ", 每列的 V 跨度变成恒定
# 的 Δm / (Δh − Δm), ribbon 在贴图上是上下都跟随 anchor 弧度的弯月形带,
# 贴图 5 条弧线在 mesh 上粗细均匀。
#
# 硬约束:
# 1. Δm > 0 且 Δm < Δh (顺序保持 anchor < middle < hairline, 防 V 反向)
# 2. anchor.V_max + Δh ≤ 1.0 (即 Δh ≤ 1 − 0.7724 = 0.2276)
# 否则中央列 hairline V 溢出, 采到贴图边缘的抗锯齿像素。
# 当前 Δh = 0.220 留 ≈ 0.008 V 单位 buffer; Δm = Δh / 2 让上下两段等宽。
def extension_uv_for(row: int, anchor_u: float, anchor_v: float) -> tuple[float, float]:
"""Return (u, v_raw) UV for an extension vertex.
row: 0 = middle, 1 = hairline.
anchor_u: U of the corresponding MP anchor (copy verbatim → ribbon column
is vertical in UV space).
anchor_v: V_raw of the corresponding MP anchor (we add a constant Δ to
it → ribbon row stays parallel to anchor row in UV space, so
each quad has the same V span and texture arcs render at the
same thickness across all 17 columns).
"""
dv = UV_MIDDLE_DV if row == 0 else UV_HAIRLINE_DV
return (anchor_u, anchor_v + dv)
# Face-parsing class indices for the jonathandinu/face-parsing
# SegFormer model (matches CelebAMask-HQ labels):
PARSE_BG = 0
PARSE_SKIN = 1
PARSE_NOSE = 2
PARSE_EYE_G = 3
PARSE_L_EYE = 4
PARSE_R_EYE = 5
PARSE_L_BROW = 6
PARSE_R_BROW = 7
PARSE_L_EAR = 8
PARSE_R_EAR = 9
PARSE_MOUTH = 10
PARSE_U_LIP = 11
PARSE_L_LIP = 12
PARSE_HAIR = 13
PARSE_HAT = 14
PARSE_EAR_R = 15
PARSE_NECK_L = 16
PARSE_NECK = 17
PARSE_CLOTH = 18
# 内网/离线:指向本地权重目录(transformers from_pretrained 支持本地路径)。
# 在线 id 为 "jonathandinu/face-parsing",权重已放到 hairline/models/face-parsing/。
import os as _os
HF_FACE_PARSER_MODEL = _os.path.join(
_os.path.dirname(_os.path.abspath(__file__)), "models", "face-parsing"
)