Files
hair/docs/接口2-C端生发-技术实现方案.md
T
xsl db32fa12e5 feat(接口2): 移植head3d发际线管线 + 接口2实现方案文档
- 从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张发际线贴图入库
2026-06-14 16:59:39 +08:00

247 lines
14 KiB
Markdown
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.
# 接口 2:C 端生发 — 技术实现方案(第一步:发际线遮罩渲染)
> 在 **高性能 worker(GPU 机)** 上实现,与接口 1 同机。对外接口经网关代理(见 [`系统架构-网关与高性能后端.md`](系统架构-网关与高性能后端.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 indexMapbuild_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 移植后需要修改的集成点
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")`
2. **`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`
3. **相对导入**:模块用 `from . import constants`,已加 `hairline/__init__.py`,作为包导入即可(`from hairline.extract_hairline import run`)。
4. **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.objUV(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 步骤
```python
# 伪代码
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_rawV=1 对应贴图顶部),转贴图像素 y 要 `(1 - v)`,与 head3d Three.js `texture.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/`SegFormerconfig + 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. 风险与待办
1. **新贴图 UV 是否与 texture0 完全一致**:本方案假设 9 张贴图沿用 head3d 的顶部条带 UV 布局(已肉眼确认曲线在顶部)。M2 渲染若位置偏移,核对贴图内容所在的 V 区间与 `UV_MIDDLE_DV/UV_HAIRLINE_DV`
2. **接缝/锯齿**:逐三角形 warp 的共享边接缝,M2 跑通后按 §4.3 优化。
3. **歪头/非正面**head3d 矢状-arc 假设近正脸,大角度发际线贴合差。可复用接口 1 的 solvePnP 做前置姿态校验(可选)。
4. **秃头/高发际线/刘海**:SegFormer 找不到头发时射线回退几何外推,曲线可能偏高;valid_hairline 标记可用于提示。
5. **排序**:本期 order=1..N。后续排序需定义依据(脸型/额型匹配度)。
6. **真正的生发(文生图)**:本期只出遮罩/预览。下一步把预览图/曲线作为 ControlNet/inpaint 输入接文生图模型,再替换 `image_url` 为真实生发图。
---
> **文档版本**: v1.0 **创建日期**: 2026-06-14 算法来源: head3d502 点 mesh + UV)| 运行位置: worker(GPU)
> **本期产出**: 发际线曲线叠加预览图(非最终生发图)