第一步:按性别把发际线类型贴图渲染到照片,输出 N 张发际线叠加预览图。 - hairline/render.py: 解析 face_ext.obj(502 UV + 64 ribbon扩展面) + OpenCV 逐三角 仿射 warp 渲染器;关键修复——face_ext.obj 是 OBJ序,用 INDEX_MAP_468 把 MP序 502点重排成 OBJ序后再投影,否则 ribbon 会错贴到中脸 - hairline/service.py: FaceLandmarker+SegFormer 单例 + 性别贴图映射(扫描去空格) + generate_previews 管线(female5/male4) - 集成点修复: face_landmarks DEFAULT_MODEL_PATH 改 hairline/models/; constants HF_FACE_PARSER_MODEL 改本地路径(离线) - app.py: /api/v1/hair/grow 接真实实现,gender 必填(非法→1004),返回 results[].image_base64(不落盘),校验/鉴权同接口1;lifespan 预热接口2单例; 补 logging.basicConfig - 依赖: transformers==4.45.2;SegFormer 权重走 hf-mirror 下载(见 OFFLINE_ASSETS) - 测试: tests/test_hairline.py(mesh/重排/贴图映射) + test_api 接口2用例,31 全绿 注:SegFormer 受 5090/torch 限制走 CPU(~2.5s/张),换 cu128 可 SEG_DEVICE=cuda。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
171 lines
7.5 KiB
Python
171 lines
7.5 KiB
Python
"""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
|
||
|
||
# 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"
|
||
)
|