Files
hair/docs/发际线生发遮罩算法_pushed模式.md
T
2026-07-11 23:14:07 +08:00

76 lines
5.7 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.
# 发际线生发遮罩算法(pushed 模式)
> 对应接口11 `/api/v1/hairline/grow`、接口12 `/api/v1/hairline/grow_v2``mask_type=pushed`。
> 代码:`face_analysis/hairline_grow.py``_extract_hairline` / `_pushed_mask` / `compute_mask`)。
## 概述
pushed 模式是发际线生发的默认遮罩算法(接口11/12 的 `mask_type` 三选一:`eroded` / `closed` / `pushed`,当前只用 pushed)。它从头发分割结果中提取「头发/皮肤交界线」(发际线),以眉心为圆心逐点径向外推一段距离,与 baseline 组成闭合区域作为最终遮罩。这样遮罩顶部会覆盖现有头发下沿一小段,贴回生发结果时顶部与真头发重叠、过渡自然。
## 算法流程(5 步)
```
①-a baseline 分割线 ← 眉骨/glabella 关键点折线(含 151 中心点)
①-b 上半区 upper ← baseline 以上的区域(裁剪范围)
①-c 头发分割 hair_mask ← segformer/bisenet 的原始头发像素
①-f 头发内轮廓线 ← 取头发轮廓中朝脸一侧的那段(额头弧+两侧到下颌),有序折线
①-g 径向外推 + 成带 ← 以 151 点为圆心把内轮廓逐点向外推 push_px,内轮廓↔外推线之间的带 = 最终遮罩
```
> ①-d(填充到基线 top_fill)、①-e(闭合区域 closed)是旧 eroded/closed 模式的中间产物,pushed 模式不走这条流程,前端不展示。
### ①-a baseline 分割线
MediaPipe 人脸关键点 `[21,68,104,69,108,151,337,299,333,298,251]` 连成折线(左端 21 → 中心 151 → 右端 251),再向左右边缘水平延长。151 点(glabella/眉心)是后续径向外推的圆心。代码 `_baseline_points` / `_draw_baseline`
### ①-b 上半区 upper
baseline 折线以上的多边形区域(`_upper_region_mask`)。作为后续裁剪范围,保证遮罩不越界到下半脸。
### ①-c 头发分割 hair_mask
segformer(默认)或 bisenet 得到的头发二值掩码。
### ①-f 头发内轮廓线(核心改动)
代码 `_extract_hairline`。目标是提取「头发区域朝脸一侧的内轮廓线」:额头弧 + 左右两侧鬓角/脸颊边界,一直向下到下颌,是一条**有序折线**(不再是逐列一个 y 的数组,因为两侧近乎竖直、一个 x 对多个 y)。
1. **取头发轮廓**`hair_mask` 最大连通域,`cv2.findContours(RETR_EXTERNAL, CHAIN_APPROX_NONE)` 取稠密、保序的外轮廓点。
2. **内侧判定**:轮廓同时含「朝背景的外侧剪影」和「朝脸的内轮廓」。对每个轮廓点,朝脸中心 151 方向采样 `sample_px`(≈0.4cm)像素,落点若是**非头发**像素 → 该点朝向脸(内轮廓);否则是外侧剪影,丢弃。
3. **取最长连续内侧段**:内轮廓点在闭合轮廓上本是一段连续弧,先做 1D 环形闭运算填掉判定抖动的小缝,再取最长连续 True 段并保序。
4. **下颌截断**:丢掉 y > `chin_y`(下巴关键点 152 的 y)的点,把两侧末端截到下颌一带 → 得到「环脸」内轮廓弧。
**关键点**:不再用 baseline 做水平截断、也不再逐列取下沿;截断改为「朝脸内侧」判定 + 下颌 y 截断,因此能同时拿到额头弧和两侧竖直边界。
### ①-g 径向外推 + 闭合区域(最终遮罩)
代码 `_pushed_mask`
1. **逐点径向外推**:圆心 = 151 点 (cx, cy)。对内轮廓上每个点 (x, y),沿「从圆心指向它」的单位向量 `(ux, uy)` **向外**(远离脸中心 = 推进现有头发)外推 `push_px`,得到外推线(黄线)点 `(x + ux·push_px, y + uy·push_px)``push_px = hairline_push_cm × px_per_cm`(默认 1cm)。
2. **逐列归并**:只取外推线中落在 baseline 以上的点,逐列取最靠上的 y 作为遮罩顶界 `pushed_y[x]`;空列线性插值填补。两侧鬓角落到 baseline 以下的段落自然被排除。
3. **与 baseline 组闭合区域**:逐列从 `pushed_y[x]` 填充到 `baseline_y[x]`(仅 `pushed_y < baseline_y` 的列),`& upper` 去越界、`_largest_cc` 保留最大连通域。
最终遮罩 = **外推发际线(①-g 黄线)与 baseline 分割线(①-a)组成的闭合区域**:顶界=外推发际线(覆盖现有头发约 push_cm),底界=baseline。与旧逻辑一致,区别只是 `pushed_y` 现在来自修正后的整条内轮廓,额头弧已延伸到两侧鬓角,额头遮罩宽度不再被截短。
## 关键参数
| 参数 | 默认 | 说明 |
|---|---|---|
| `mask_type` | `eroded`(接口默认)/ `pushed`(当前推荐) | pushed 走上述流程;eroded/closed 走旧的 top_fill→closed/eroded 流程 |
| `hairline_push_cm` | 1.0 | 内轮廓径向外推距离(厘米),= push_px / px_per_cm。`px_per_cm` 由虹膜直径标定 |
| `hairline_edge` | `column` | 兼容保留的入参;新版内轮廓提取(轮廓+内侧判定)不再按它分支,取值不影响结果 |
## 与旧模式(eroded/closed)的区别
| | eroded/closed | pushed(当前) |
|---|---|---|
| 遮罩来源 | top_fill(头发向下填充含额头)外缘内缩 | 头发内轮廓线 径向外推成带 |
| 截断方式 | 用 baseline 组上半区 upper | 内侧判定 + 下颌 y 截断(不再用 baseline |
| 遮罩形状 | 整个额头闭合区域 | 沿内轮廓的环脸带(额头弧+两侧,压住现有头发 push_cm) |
## 调试
- 调试页:`http://<host>:8187/static/test_interface11_debug.html`(带前后端日志面板、下载日志按钮)
- 后端日志:`/home/xsl/hair/log/hairline_grow.log`(按 `[rid]` 关联一次请求),下载接口 `/api/v1/debug/hairline_log?rid=<id>&tail=500`
- 可视化步骤:①-a baseline / ①-b upper / ①-c 头发分割 / ①-f 交界线 / ①-g 外推+遮罩 / 最终遮罩 / 生成 / 贴回 / 融合