Files
hair/docs/接口3-B端生发-技术实现方案.md
T
xslandClaude Opus 4.8 bf76923591 refactor(接口3): 简化为只需划线图一张,去掉 original + best_hairline
按需求方意见——B端只需上传一张已画好发际线的图,用不着原图:
- 入参去掉 original_image_*,只保留 marked_image_*(三选一)
- 输出去掉 best_hairline_image_url,只返回 hair_growth_image_url + hairline_type
- ComfyUI 输入图改用 marked 划线图原样(add_hair.json 本就是"画了线的照片",
  提示词清除黑线);检测路径只用于建遮罩,不再重画干净线/不需对齐原图
- service.generate_grow_b 签名改 (marked_bgr) 单参
- 同步文档:接口文档/接口3技术方案/网关映射表(去掉接口3 best_hairline 行)
- 测试更新:grow-b 只传 marked,断言无 best_hairline 字段,44全绿

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:28:13 +08:00

92 lines
5.1 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.
# 接口 3:B 端生发 — 技术实现方案(马克笔发际线检测 + 生发)
> 在 **高性能 workerGPU 机)** 实现,与接口 1/2 同机。对外经网关代理。
> B 端:医生在患者额头**用马克笔画出规划的发际线**,拍照上传。系统**检测这条手绘线**,
> 据此生成生发图。检测算法移植自 `/home/xsl/headmark` 的调研结论(黑帽 + Dijkstra)。
---
## 0. 契约(对齐 `接口文档.md` 接口3,不变)
`POST /api/v1/hair/grow-b`
| 输入 | 说明 |
|------|------|
| `marked_image_*` | 已用马克笔标注发际线的图,三选一,必填。**只需这一张**(划线图即用户照片+手绘线,不需要原图) |
| 输出 data | 决策 |
|-----------|------|
| `hair_growth_image_url` | **生发后图片**ComfyUIworker 返回 `hair_growth_image_base64` |
| `hairline_type` | 固定 **`"custom"`**(手绘定制) |
> 落盘改 URL 由网关做(架构同接口1/2)。
---
## 1. 马克笔发际线检测(核心,源自 headmark 调研)
headmark `docs/detection_research.md` 结论:全局灰度阈值不可用(笔迹平均灰度反而高于阈值、
与皮肤阴影分布重叠);推荐 **黑帽响应图 + 端点锚定 Dijkstra 最小路径**,实测误差 ≤0.5px(GT锚点)。
本项目用 **MediaPipe 锚点**(非 GT)实测平均 3.2px、中位 0px —— 对生成遮罩足够(线会膨胀成带)。
```
detect_marker_hairline(marked_bgr, landmarks, parse_map):
[1] ROI = forehead_upper_region(landmarks) ∩ head_silhouette(parse_map) # 复用接口2 mask.py
[2] 黑帽响应 bh = MORPH_BLACKHAT(gray, ksize=max(15,int(w*0.025)|1))ROI 外置 0
[3] 锚点 = MediaPipe 21(左鬓角)/251(右鬓角),各自小窗口(≈w*3%)内吸附到 bh 最大处
[4] 代价 cost = bh.max()-bh+1ROI 外设 1e6
path = skimage.graph.route_through_array(cost, 左锚, 右锚, fully_connected, geometric)
[5] 拒识:path 平均 bh 响应 < 阈值(可调) → None(上层返回 1001 "未检测到发际线划线"
return path # (N,2) row,col
```
- 依赖:**`scikit-image==0.24.0`**。⚠️ 0.25+ 强依赖 numpy≥2,会顶掉 mediapipe 的 numpy<2 →
mediapipe/SegFormer 全崩。**必须锁 0.24.x**。
- 复用接口2`forehead_upper_region` / `head_silhouette``hairline/mask.py`)、SegFormer / MediaPipe 单例。
## 2. 遮罩(检测路径只用来建遮罩;ComfyUI 输入图 = 划线图原样)
- **遮罩**path → 画成 curve_mask → 复用接口2 `mask_from_curve`ROI ∩ 曲线以上 → 闭合)
得到"发际线以上闭合区域"。
- **ComfyUI 输入图**:直接用 **marked 划线图原样**(已含医生手绘线;`add_hair.json` 节点26
本来就是"画了线的照片",提示词会清除黑线再生发)。**不需要原图、不重画线**。
- 合成 RGBARGB=marked 划线图,alpha=255mask(透明=重绘区)。复用 `compose_comfy_rgba`
## 3. 生发(复用接口2 ComfyUI 客户端)
`hairline/comfyui.run(rgba_png)` → 跑 `add_hair.json`(Flux-2)→ 生发图 PNG。同步。
## 4. worker handler`/api/v1/hair/grow-b`
```
1. marked 三选一取图(复用 resolve_image_bytes+ 校验(大小/解码/分辨率)。只需这一张。
2. landmarks(MediaPipe)+parse(SegFormer) → detect_marker_hairline
- 无人脸 → 1001;未检测到画线 → 1001 "未检测到发际线划线"
3. 遮罩(mask_from_curve) + marked原样 → RGBA → comfyui.run → 生发图
4. return ok({ hair_growth_image_base64: 生发图, hairline_type: "custom" })
异常 → 1007;重活 run_in_threadpool。
```
## 5. 开发步骤
| 阶段 | 内容 | 验证 |
|------|------|------|
| **M1 检测** | `hairline/marker_detect.py`(黑帽+锚点+Dijkstra+拒识) | headmark test_image:检测线贴合真值;无线图被拒识 |
| **M2 遮罩** | path→遮罩(复用 mask_from_curve) + marked原样 RGBA 合成 | 目视:遮罩贴合发际线 |
| **M3 接 app** | grow-b 真实实现(仅 marked) + 输出字段 + 1001 | curlgrown 合法PNG/type=custom;无线→1001 |
| **M4 测试** | 检测/mask 单测 + mock-ComfyUI 集成 + 真机冒烟 | pytest 绿;真机出生发图 |
## 6. 风险
1. **锚点偏差/路径端点偏移**MediaPipe 21/251 吸附后仍可能在鬓角端有偏移(实测 max~42px,少数点)。
膨胀成带 + 遮罩闭合可吸收;必要时改进吸附窗口或端点截断。
2. **没画线/画线极浅**:靠拒识阈值(路径平均黑帽响应)兜底,阈值需在更多真实图上标定。
3. **医生手绘线毛刺/杂线**ComfyUI 输入图用 marked 原样(含手绘线),提示词会清除黑线;
检测出的干净 path 只用于建遮罩。若手绘过乱影响生成,可改为在 marked 上重画干净检测线(备选)。
4. **抬头纹/眉毛/发丝干扰**:黑帽 + ROI + Dijkstra 平滑已大幅抑制(调研验证抬头纹零干扰),极端情况可在代价图抑制头发区域。
---
> **文档版本**: v1.0 **创建日期**: 2026-06-15 检测来源: headmark(黑帽+Dijkstra)|
> 生发: 复用接口2 ComfyUI(add_hair.json) 运行位置: worker(GPU) + 本机 ComfyUI(8182)