- 从head3d复制发际线检测管线到 hairline/ 包:MediaPipe Tasks + SegFormer分割 + 17锚点射线检测 + 502点mesh(face_ext.obj)+UV - 复制模型:face_landmarker.task(3.7MB)、SegFormer config/preprocessor (model.safetensors 340MB 单独下载中) - 新增 docs/接口2-C端生发-技术实现方案.md:第一步=发际线曲线叠加预览图, 新增gender必填参数,按性别贴图数量输出(female5/male4),hairline_type英文key, 服务端cv2逐三角形warp渲染器(head3d只有浏览器端Three.js渲染) - 接口文档.md 接口2章节同步:gender参数、输出语义、错误码说明 - hairline_texture/ 9张发际线贴图入库
14 KiB
接口 2:C 端生发 — 技术实现方案(第一步:发际线遮罩渲染)
在 高性能 worker(GPU 机) 上实现,与接口 1 同机。对外接口经网关代理(见
系统架构-网关与高性能后端.md)。 发际线检测算法移植自 head3d 项目(已实现 502 点 mesh + UV 贴图方案)。
0. 本期范围(第一步)
接口 2 输入用户正面照 + 性别,按性别对应的发际线类型贴图,逐张把发际线曲线渲染到照片上,输出多张「叠加了建议发际线的预览图」,按固定顺序返回。
- 本期只做「渲染遮罩/预览图」,不做真正的文生图生发(那是后续步骤)。当前
image_url返回的是「原照片 + 发际线曲线叠加图」。 - 排序
order本期不计算,按贴图顺序1..N。
1. 与现接口文档的差异(接口 2 需同步更新 接口文档.md)
| 项 | 现状 | 本期改为 |
|---|---|---|
| 输入参数 | beauty_enabled |
新增必填 gender(male/female);beauty_enabled 保留但本期不生效 |
输出 results[] 数量 |
Mock 2 个 | = 该性别的贴图数量(female 5 张 / male 4 张) |
results[].image_url |
生发后图片 | 本期 = 发际线曲线叠加在原照片上的预览图 |
results[].hairline_type |
中文(花瓣形…) | 英文 key(flower/wave/heart/ellipse/straight/m/inverse_arc) |
results[].order |
排序 | 本期固定 1..N(不排序) |
| 错误码 1004(性别判断异常) | 待确认 | gender 改为必填入参 → 不再自动判别性别;1004 仅在 gender 非法值时使用(或弃用) |
⚠️ 这是接口 2 的有意契约变更(加入参 + 改输出语义),需在
接口文档.md接口 2 章节同步。其余 4 个接口契约不变。
gender → 贴图集合
hairline_texture/ 目录下贴图(512×512 RGBA,白色发际线曲线在顶部 UV 条带):
| gender | 贴图文件 | hairline_type (key) |
|---|---|---|
| female | girl_ellipse.png |
ellipse |
| female | girl_flower.png |
flower |
| female | girl_heart.png |
heart |
| female | girl_straight.png |
straight |
| female | girl_wave.png |
wave |
| male | man_ellipse.png |
ellipse |
| male | man_m.png |
m |
| male | man_straight.png |
straight |
| male | man_ inverse_arc.png |
inverse_arc |
注意
man_ inverse_arc.png文件名里有个空格,代码里按gender + '_' + key生成文件名时需保留/清洗一致。建议启动时扫描目录建立{gender: [(key, path)]}映射,而不是硬编码文件名,并把文件名规范化(去空格)。
2. 已从 head3d 复制到本项目的文件
全部放在 hairline/ 包下(已复制,agent 直接用):
hairline/
├── __init__.py
├── constants.py # 17 锚点、UV 偏移、分割类别、矢状-arc 常量、HF 模型 id
├── obj_io.py # OBJ 读写
├── face_landmarks.py # MediaPipe Tasks FaceLandmarker 封装(用 face_landmarker.task)
├── face_parsing.py # SegFormer 人脸分割封装
├── hairline_2d.py # 射线检测发际线 2D + 平滑 + 回退
├── lift_3d.py # 2D→3D 矢状-arc 提升 + 中间行 + assemble 502 点
├── extract_hairline.py # 主管线(image → 502 点),可复用 run()
├── _index_map_data.py # 468→OBJ indexMap(build_extended_obj 用,本期渲染不需要)
├── _mediapipe_subprocess.py# WSL 下子进程跑 MediaPipe 的兜底(可选)
├── models/
│ ├── face_landmarker.task # MediaPipe 模型(3.7MB,已复制)
│ └── face-parsing/ # SegFormer 权重(离线,已下载,见 OFFLINE_ASSETS.md)
│ ├── config.json
│ ├── preprocessor_config.json
│ └── model.safetensors
├── mesh/
│ ├── face_ext.obj # 502 点扩展 mesh + UV + 三角面(渲染器读这个)
│ └── face.obj # 原始 468 点 mesh(参考/重生成用)
└── reference/
├── texture0.png # head3d 原 5 弧线贴图(核对 UV 用)
└── uv_template.png # UV 布局参考
发际线类型贴图在仓库根目录 hairline_texture/(用户提供,9 张)。
2.1 移植后需要修改的集成点
face_landmarks.py的DEFAULT_MODEL_PATH:原逻辑是dirname(dirname(__file__))/models/...(head3d 里模块在python/子目录)。现在模块在hairline/根,该路径会指向hair/models/,而模型在hairline/models/。改为os.path.join(os.path.dirname(__file__), "models", "face_landmarker.task")。face_parsing.py离线加载:C.HF_FACE_PARSER_MODEL当前是 HF 在线 id"jonathandinu/face-parsing"。内网/离线改为本地目录:把constants.py的HF_FACE_PARSER_MODEL指向hairline/models/face-parsing的绝对路径(from_pretrained支持本地目录);或设HF_HUB_OFFLINE=1。- 相对导入:模块用
from . import constants,已加hairline/__init__.py,作为包导入即可(from hairline.extract_hairline import run)。 - GPU:
FaceParser(device="cuda"),worker 有 GPU。
3. 算法管线(整体)
输入: 用户正面照 + gender
│
▼
[A] head3d 管线(复用 hairline.extract_hairline 的步骤)
- MediaPipe 468 点(face_landmarker.task)
- SegFormer 人脸分割 → parse_map
- 17 锚点射线检测发际线 → 17 个 2D 点 → 平滑
- 矢状-arc 提升 → 502 点(归一化 x,y,z)
│ 失败处理:无人脸→1001
▼
[B] 投影到图像像素
- 502 点的 (x,y) × (W,H) → 502 个 2D 图像坐标
- 读 face_ext.obj:UV(502) + 扩展三角面(涉及顶点 ≥468 的 64 个三角形)
▼
[C] 逐张贴图渲染(新写的服务端渲染器,本方案核心)
for 每个该性别的发际线贴图 t:
- 对每个扩展三角形:src=UV→贴图像素, dst=投影 2D 坐标 → cv2 仿射 warp
- 累积成一张 RGBA 曲线层(贴图 alpha 控制曲线/透明)
- 把曲线层 alpha 合成到原照片上 → 预览图
▼
[D] 输出
results[] = N 个 {image(预览图), hairline_type(key), order=1..N}
worker 侧每张图以 base64 返回(见 §6)
4. 渲染器(新代码,本期重点)★
head3d 把贴图渲染到照片是浏览器 Three.js 做的(/preview ortho overlay),没有服务端实现。本期新写一个 OpenCV 逐三角形仿射 warp 渲染器,无需 OpenGL 离屏上下文,确定性好、部署简单。
4.1 原理
face_ext.obj 的 502 顶点里:
[0..467]MediaPipe 点,其中 17 个MP_TOP_ANCHORS是发际线 ribbon 的下边沿;[468..484]中间行、[485..501]发际线行,是 ribbon 的中、上两行。
这 34 个新点 + 17 个锚点之间连成 64 个三角形(ribbon),它们的 UV 落在贴图顶部条带(V_raw≈0.67..0.94,正是发际线曲线所在)。所以只要把这 64 个三角形按 UV→图像坐标 warp,就能把贴图里的发际线曲线贴到照片的额头/发际线区域。
4.2 步骤
# 伪代码
def render_hairline_overlay(photo_bgr, points502_norm, ext_faces, uv502, texture_rgba):
H, W = photo_bgr.shape[:2]
# 502 点投影到图像像素
img_xy = points502_norm[:, :2] * [W, H] # (502, 2)
TW, TH = texture_rgba.shape[1], texture_rgba.shape[0] # 512, 512
overlay = np.zeros((H, W, 4), np.float32) # 累积曲线层 RGBA
for (i, j, k) in ext_faces: # 仅扩展三角形(顶点含 ≥468)
dst = img_xy[[i, j, k]].astype(np.float32) # 图像坐标
# UV → 贴图像素。注意 flipY:贴图 y = (1 - v_raw) * TH
src = np.array([[uv502[v][0]*TW, (1-uv502[v][1])*TH] for v in (i,j,k)], np.float32)
M = cv2.getAffineTransform(src, dst)
warped = cv2.warpAffine(texture_rgba, M, (W, H), flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT, borderValue=(0,0,0,0))
# 三角形掩码,避免覆盖整张 warp 结果
tri_mask = np.zeros((H, W), np.uint8)
cv2.fillConvexPoly(tri_mask, dst.astype(np.int32), 255)
sel = tri_mask > 0
overlay[sel] = warped[sel] # 逐三角形写入(相邻共享边,覆盖等价)
# alpha 合成到原照片
a = overlay[:, :, 3:4] / 255.0
out = photo_bgr.astype(np.float32)
out = out * (1 - a) + overlay[:, :, :3][..., ::-1] * a # RGBA→BGR 注意通道序
return out.astype(np.uint8)
4.3 注意点
- 通道序:贴图是 RGBA,照片 OpenCV 是 BGR,合成时注意 R/B 调换。
- flipY:face_ext.obj 的 UV 是 V_raw(V=1 对应贴图顶部),转贴图像素 y 要
(1 - v),与 head3d Three.jstexture.flipY=true一致。 - 只 warp 扩展三角形:从 face_ext.obj 筛出顶点索引含 ≥468 的面(约 64 个)。不要 warp 整脸。
- 抗锯齿/接缝:逐三角形
fillConvexPoly掩码可能在共享边留 1px 缝。可对tri_mask略膨胀,或最后对 overlay alpha 做轻微羽化。先跑通看效果再优化。 - 裁剪到额头:曲线层只在 ribbon 区域有内容(贴图其余透明),天然不会画到脸下半部。
5. 依赖
worker 已有(接口 1):opencv-python、numpy、Pillow、torch(CUDA)。接口 2 新增:
mediapipe>=0.10 # Tasks Vision FaceLandmarker(注意与接口1的 solutions API 可共存)
transformers>=4.40 # SegFormer 人脸分割
# torch/torchvision 已由接口1引入(worker CUDA 版)
⚠️ 两套人脸分割模型:接口 1 用 BiSeNet(
79999_iter.pth),接口 2 用 head3d 的 SegFormer(jonathandinu/face-parsing)。两者并存,显存/内存够(worker 32G+GPU)。后续可评估是否统一为一个分割模型,本期先各用各的,不强行合并。⚠️ MediaPipe API 差异:接口 1 用
mp.solutions.face_mesh(468 点 + 虹膜 refine),接口 2 用mp.tasks.vision.FaceLandmarker(读.task文件)。同一个 mediapipe 包都支持,但版本需兼容两者(建议先用一个版本把两接口都跑通)。
6. worker 集成(接口 2 handler)
在 app.py 替换 /api/v1/hair/grow 的 Mock:
1. 解析图片(三选一)+ 读 gender(必填,male/female;非法→1004 或 1008 参数错误)
2. 校验(大小/解码/分辨率,同接口1)
3. 跑 hairline.extract_hairline 的步骤拿 502 点(无人脸→1001)
4. 按 gender 取贴图集合(启动时扫描 hairline_texture/ 建映射)
5. for 每张贴图: render_hairline_overlay → PNG
6. results[] = [{image_base64, hairline_type, order}], 逐张 base64
7. return ok({"results": results})
- 拆分架构:worker 返回
results[].image_base64,不落盘不拼 URL。网关把每个image_base64落盘改写成image_url(架构文档 §9 的映射表需支持数组里的图片字段results[].image)。 - 模型单例:
FaceLandmarker和FaceParser在模块加载时初始化一次,避免每请求重建。face_ext.obj 的 UV/faces 也只解析一次缓存。
7. 离线资产(内网部署)
接口 2 新增需要随项目带入内网的模型(已下载,登记到 OFFLINE_ASSETS.md):
hairline/models/face_landmarker.task(MediaPipe,~3.7MB)hairline/models/face-parsing/(SegFormer:config + preprocessor + model.safetensors)
SegFormer 加载方式改本地路径后,内网无需联网(见 §2.1)。
8. 开发步骤与验证(agent 执行)
| 阶段 | 内容 | 验证 |
|---|---|---|
| M0 跑通管线 | 修好集成点(§2.1),用一张人像跑 hairline.extract_hairline.run() 得 502 点 JSON |
502 点、valid_hairline 有 true |
| M1 解析 mesh | 读 face_ext.obj 拿 UV + 扩展三角面(顶点≥468 的面),缓存 | 打印扩展面数(~64)、502 个 UV |
| M2 渲染器 | 实现 render_hairline_overlay,对 1 张贴图渲染 |
输出预览图,目视:发际线曲线贴在额头正确位置、跟随脸 |
| M3 全量 + 性别 | 扫描 hairline_texture/ 建 gender→贴图映射,按性别渲染 N 张 |
female 出 5 张、male 出 4 张,hairline_type 对 |
| M4 接 app.py | handler + gender 必填 + base64 返回 | curl 验证 results 数量/字段;无人脸→1001;缺 gender→报错 |
| M5 网关映射 | 网关支持 results[].image_base64→image_url(网关任务书侧) |
端到端经网关返回 image_url,公网可访问 |
M2 是关键里程碑:渲染器对齐效果好不好,决定整个接口可用性,先用几张测试人像目视确认贴合。
9. 风险与待办
- 新贴图 UV 是否与 texture0 完全一致:本方案假设 9 张贴图沿用 head3d 的顶部条带 UV 布局(已肉眼确认曲线在顶部)。M2 渲染若位置偏移,核对贴图内容所在的 V 区间与
UV_MIDDLE_DV/UV_HAIRLINE_DV。 - 接缝/锯齿:逐三角形 warp 的共享边接缝,M2 跑通后按 §4.3 优化。
- 歪头/非正面:head3d 矢状-arc 假设近正脸,大角度发际线贴合差。可复用接口 1 的 solvePnP 做前置姿态校验(可选)。
- 秃头/高发际线/刘海:SegFormer 找不到头发时射线回退几何外推,曲线可能偏高;valid_hairline 标记可用于提示。
- 排序:本期 order=1..N。后续排序需定义依据(脸型/额型匹配度)。
- 真正的生发(文生图):本期只出遮罩/预览。下一步把预览图/曲线作为 ControlNet/inpaint 输入接文生图模型,再替换
image_url为真实生发图。
文档版本: v1.0 | 创建日期: 2026-06-14 | 算法来源: head3d(502 点 mesh + UV)| 运行位置: worker(GPU) 本期产出: 发际线曲线叠加预览图(非最终生发图)