Files
hair/docs/iface3_implementation.md
T
2026-07-18 15:30:31 +08:00

178 lines
7.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端生发 — 实现文档
> 文档日期:2026-07-18
---
## 一、接口概述
**接口3** 是 B端(医生/操作端)生发接口。医生在用户照片上手动用马克笔画出发际线后,只需上传这一张划线图,系统自动检测划线 → 生成遮罩 → 送 ComfyUI 生发,返回「植发3个月」效果图。
**与接口2 的核心区别**
| 特性 | 接口2(C端生发) | 接口3(B端生发) |
|------|----------------|----------------|
| 输入 | 原始照片 | 划线图(含手绘线) |
| 发际线来源 | 系统按发型模板自动生成 | 医生手绘标注 |
| 发型类型 | ellipse/flower/heart/straight/wave | custom(自定义) |
| 中间步骤 | extract_context + swapHair + ComfyUI重绘 | 划线检测 + 遮罩 + ComfyUI生发 |
| 是否调 change_hair | 是(女性流程) | 否 |
| ComfyUI 工作流 | 0716add-hair-api.json(重绘) | add_hair.json(生发) |
| 典型耗时 | ~11s | ~6-8s |
---
## 二、接口定义
### 路由
```
POST /api/v1/hair/grow-b
```
### 入参
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `marked_image_file` | UploadFile | 三选一 | 划线图片文件(JPG/PNG) |
| `marked_image_url` | str | 三选一 | 划线图片 URL |
| `marked_image_base64` | str | 三选一 | 划线图片 base64 |
| `use_mask` | bool | 否(默认True) | 是否自动检测划线并建遮罩。False时跳过检测,直接送划线图 |
| `prompt` | str | 否 | ComfyUI 提示词,默认"补充遮罩区域的头发,加一点美颜" |
### 返回
```json
{
"code": 0,
"message": "success",
"data": {
"hair_growth_image_base64": "iVBORw0KGgo...(生发图 JPG base64",
"hairline_type": "custom"
}
}
```
错误码:
- `1001`: 无法识别人像 / 未检测到发际线划线
- `1007`: 处理失败
- `1008`: 图片格式不支持
---
## 三、完整调用链
```
POST /api/v1/hair/grow-b
├─ app.py hair_grow_b() [app.py:929]
│ ├─ resolve_image_bytes() → marked_raw 解析图片(file/url/base64三选一)
│ ├─ cv2.imdecode → marked_bgr 解码为 BGR
│ └─ run_in_threadpool(generate_grow_b, ...)
├─ service.py generate_grow_b(marked_bgr, use_mask, prompt) [service.py:381]
│ │
│ ├─ 步骤1:人脸检测 + 头发分割(仅 use_mask=True 时)
│ │ ├─ get_landmarker().detect(rgb) MediaPipe 478点人脸检测
│ │ │ → landmarks(无人脸返回 no_face
│ │ ├─ get_parser().parse(rgb) SegFormer 面部分割(CPU ~0.9s
│ │ │ → parse_mapint label map
│ │ │
│ ├─ 步骤2:手绘发际线检测(仅 use_mask=True 时)
│ │ ├─ detect_marker_hairline(marked_bgr, landmarks, parse_map)
│ │ │ │ [marker_detect.py:41]
│ │ │ ├─ forehead_upper_region(landmarks) 额头上部 ROI
│ │ │ ├─ head_silhouette(parse_map) 头部轮廓 ROI
│ │ │ ├─ _blackhat(gray) 黑帽变换(响应比邻域暗的细结构)
│ │ │ ├─ _snap_anchor(bh, 左鬓角21) 左锚点吸附
│ │ │ ├─ _snap_anchor(bh, 右鬓角251) 右锚点吸附
│ │ │ ├─ route_through_array(cost, 左, 右) Dijkstra最小代价路径
│ │ │ └→ path (N,2) row,col(拒识返回 None → no_line
│ │ │
│ │ ├─ path_to_curve_mask(path) 路径→曲线maskuint8 0/255
│ │ └─ mask_from_curve(curve_mask, landmarks, parse_map)
│ │ │ [mask.py]
│ │ ├─ _above_curve_region(curve_mask) 曲线以上区域
│ │ ├─ cv2.morphologyEx(闭运算) 填洞
│ │ ├─ 最大连通域
│ │ └─ 高斯羽化 → mask (uint8 0-255)
│ │
│ ├─ 步骤3:合成 RGBA PNG
│ │ ├─ compose_comfy_rgba(marked_bgr, mask) RGB=原图,alpha=255×(1-mask)
│ │ └─ PNG 编码 → rgba_png_bytes
│ │
│ └─ 步骤4ComfyUI 生发
│ └─ comfyui.run(rgba_png_bytes, prompt) [comfyui.py:87]
│ ├─ 上传图片到 ComfyUI /upload/image
│ ├─ 加载工作流 add_hair.json
│ ├─ 替换节点26输入图 + 节点6随机seed + 节点60提示词
│ ├─ POST /prompt 提交工作流
│ ├─ 轮询 /history/{prompt_id}(间隔0.2s
│ └─ GET /view 取回输出 PNG → grown_png
└─ 返回 {"grown_png": bytes, "status": "ok"}
```
---
## 四、用到的模型和外部服务
| 模型/服务 | 用途 | 位置 | 设备 |
|----------|------|------|------|
| **FaceLandmarker** (MediaPipe) | 478点人脸检测 | hairline/face_landmarks.py | CPU |
| **FaceParser** (SegFormer) | 面部分割(hair/skin/... | hairline/face_parsing.py | CPU (5090不兼容cu121) |
| **ComfyUI** (Flux-2) | 生发图生成 | hairline/comfyui.py → :8188 | GPU |
**注意**:接口3 **不调用** change_hair 服务(:8801),不需要 swapHair。这是它与接口2女性流程的关键区别。
---
## 五、核心算法:手绘发际线检测
### 5.1 为什么不用简单阈值?
手绘马克笔线条的灰度值与皮肤阴影、抬头纹等重叠,全局阈值无法区分。采用**黑帽变换 + Dijkstra最小路径**方案。
### 5.2 黑帽变换(Black Hat
```python
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k))
bh = cv2.morphologyEx(gray, cv2.MORPH_BLACKHAT, kernel)
```
黑帽 = 闭运算 − 原图,响应"比局部邻域暗的细结构"(即马克笔线条),对抬头纹/眉毛/发丝鲁棒。
### 5.3 Dijkstra 最小代价路径
1. **ROI 限定**:额头上部 ∩ 头部轮廓(排除背景)
2. **锚点**:左鬓角(21) / 右鬓角(251) MediaPipe 关键点
3. **代价图**`cost = (bh.max() - bh) + 1.0`ROI外设 1e6
4. **路径**`route_through_array(cost, 左锚, 右锚)` — skimage 的 Dijkstra 实现
### 5.4 拒识机制
路径平均黑帽响应 < 8.0 → 判定"未画线",返回 `no_line`
---
## 六、与接口1、接口2 的对比
| 维度 | 接口1 | 接口2 | 接口3 |
|------|-------|-------|-------|
| 功能 | 四庭七眼测量 | C端生发(5种发际线) | B端生发(手绘线) |
| 路由 | /api/v1/face/measure | /api/v1/hair/grow | /api/v1/hair/grow-b |
| 输入 | 正面照 | 正面照 | 划线图 |
| MediaPipe | ✅ | ✅ | ✅ |
| SegFormer | ✅ | ✅ | ✅ |
| change_hair | ❌ | ✅(女性) | ❌ |
| ComfyUI | ❌ | ✅(Flux-2重绘) | ✅(Flux-2生发) |
| 典型耗时 | ~2s | ~11s | ~6-8s |
| ComfyUI工作流 | — | 0716add-hair-api.json | add_hair.json |
---
## 七、测试
- **测试页面**[static/test_interface3.html](file:///home/ubuntu/hair/static/test_interface3.html)
- **测试图片**[image/girl_img/girl13.jpg](file:///home/ubuntu/hair/image/girl_img/girl13.jpg)(需手动在图上画发际线后作为划线图上传)