# 发际线生发遮罩算法(pushed 模式) > 对应接口11 `/api/v1/hairline/grow`、接口12 `/api/v1/hairline/grow_v2`。 > 遮罩算法固定为 pushed;融合算法默认 multiband(多频段金字塔),接口11 可切换 seamless/two_stage/feather。 > 代码:`face_analysis/hairline_grow.py`(`_extract_hairline` / `_pushed_mask` / `compute_mask` / `_composite`)。 ## 概述 pushed 是发际线生发的**唯一**遮罩算法。融合算法默认 multiband(多频段金字塔),接口11 暴露 `blend_method` 可切换为 seamless(泊松)/two_stage(泊松→多频段两段式)/feather(羽化),便于对比调优。它从头发分割结果中提取「头发/皮肤交界线」(发际线),以眉心为圆心逐点径向外推一段距离,与 baseline 组成闭合区域作为最终遮罩。这样遮罩顶部会覆盖现有头发下沿一小段,贴回生发结果时顶部与真头发重叠、过渡自然。 > 接口12 `/api/v1/hairline/grow_v2` 只需传 `image` + `hairline_id`,遮罩和融合全部固定,无需任何算法选择参数。 ## 算法流程(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` 现在来自修正后的整条内轮廓,额头弧已延伸到两侧鬓角,额头遮罩宽度不再被截短。 ## 关键参数 遮罩算法(pushed)固定。融合算法接口11 通过 `blend_method` 可切换(默认 multiband),其余融合参数均可调: | 参数 | 默认 | 说明 | |---|---|---| | `hairline_push_cm` | 1.0 | 内轮廓径向外推距离(厘米),= push_px / px_per_cm。`px_per_cm` 由虹膜直径标定 | | `hairline_edge` | `column` | 兼容保留的入参;内轮廓提取(轮廓+内侧判定)不再按它分支,取值不影响结果 | | `mb_levels` | 5 | 多频段金字塔层数(2~6,越大低频色差抹得越宽)| | `blend_method` | `multiband` | 接缝融合:multiband(多频段金字塔) / seamless(泊松) / two_stage(泊松→多频段,大色差) / feather(羽化) / alpha_gradient。接口12 固定 multiband | | `color_match` | `true` | 融合前 Reinhard 颜色迁移消除整体色差(multiband/feather/alpha_gradient 生效;seamless/two_stage 自带调色故跳过)| | `color_match_strength` | 1.0 | 颜色迁移强度(0~1,<1 只迁移部分,防 Reinhard 过度改色)| | `mb_feather_px` | 1 | 多频段最细层掩码轻羽化像素(0=不羽化),消除发丝边缘锯齿 | | `transition_band_px` | -1 | keep-region 过渡带边距(-1=自动按层数 `2**n`;>=0 用绝对像素与层数解耦)| | `edge_erode_px` | 3 | 贴图前遮罩内缩像素(防边缘露皮/光晕)| | `erode_cm` | 0.6(接口12 固定)| baseline 参考内缩距离,对 pushed 影响很小 | | `redraw` | `false` | 发际线带重绘开关:开启后用 final(④融合图)在「外推线↔发际线」带重绘,swapHair/Flux-2 两路对比,结果单独展示(不替换 final)| | `inpainting_fill` | 1 | change_hair 重绘填充:0=保留原图(治染绿) / 1=填充噪声(默认) / 2=纯色 / 3=潜变量噪声 | | `mask_blur` | 11 | change_hair 遮罩边缘模糊像素(越大颜色越易从边缘渗透)| | `mask_dilate_scale` | 1.0 | change_hair 遮罩膨胀核缩放(1.0=原始,<1 收缩防越界)| | `comfyui_prompt` | `null` | redraw Flux-2 路提示词,null 用默认「补充遮罩区域的头发,加一点美颜」| > 接口12 `/api/v1/hairline/grow_v2` 只需传 `image` + `hairline_id`,遮罩和融合全部用默认值(multiband + color_match=true),不暴露算法选择参数。 ### 融合方法选择建议 - **multiband**(默认):常规首选。低频抹色差、高频保发丝。需配合 `color_match=true` 消除整体色差。 - **two_stage**:生成图与原图色差大时用。先泊松克隆统一色调,再多频段贴细节,兼顾调色与保发丝。比纯 seamless 更不易溢色。 - **seamless**:纯泊松梯度域调和,色调统一干净,但可能整体改色/边缘溢色。 - **feather / alpha_gradient**:单层 alpha 过渡,最轻量,但过渡带内色差不会被抹平,仅适合色差极小的场景。 ## 发际线带重绘(redraw,接口11 可选) `redraw=true` 时,在主流程(④接缝融合 final)之后额外跑一条重绘分支,结果单独展示(`steps.redraw_a` / `redraw_c`),**不替换** final。 **重绘区域** = ①-g 外推发际线(`outer_pts`)与 ①-f 发际线(`inner_pts`)两条折线端点相连组成的带状闭合区域(宽度 ≈ `hairline_push_cm`,只覆盖发际线交界处)。 **两路后端对比**(输入图 + 融合基底都用 final): - **swapHair 路**(`redraw_a`):final + 带遮罩调 change_hair → final 走 multiband 融合 - **Flux-2 路**(`redraw_c`):final + 带遮罩调 ComfyUI(`hair_repaint.json` 工作流)→ final 走 multiband 融合。Flux-2 经 reference latent + ColorMatch 双重保色,**不易染绿** > `inpainting_fill` / `mask_blur` / `mask_dilate_scale` 透传 change_hair 服务端(仅影响 swapHair 路)。`comfyui_prompt` 仅影响 Flux-2 路。 > 两路独立容错:任一路失败只跳过该路,不影响另一路和主 final。 > ⚠️ Flux-2 路需 ComfyUI(8188)在跑;swapHair 路需 change_hair(8801)在跑。 ## 与旧模式(eroded/closed,已移除)的区别 | | eroded/closed(已移除) | pushed(当前唯一) | |---|---|---| | 遮罩来源 | top_fill(头发向下填充含额头)外缘内缩 | 头发内轮廓线 径向外推成带 | | 截断方式 | 用 baseline 组上半区 upper | 内侧判定 + 下颌 y 截断(不再用 baseline) | | 遮罩形状 | 整个额头闭合区域 | 沿内轮廓的环脸带(额头弧+两侧,压住现有头发 push_cm) | ## 调试 - 调试页:`http://:8187/static/test_interface11_debug.html`(带前后端日志面板、下载日志按钮) - 后端日志:`/home/xsl/hair/log/hairline_grow.log`(按 `[rid]` 关联一次请求),下载接口 `/api/v1/debug/hairline_log?rid=&tail=500` - 可视化步骤:①-a baseline / ①-b upper / ①-c 头发分割 / ①-f 交界线 / ①-g 外推+遮罩 / 最终遮罩 / 生成 / 贴回 / 融合