178 lines
7.1 KiB
Markdown
178 lines
7.1 KiB
Markdown
# 接口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_map(int 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) 路径→曲线mask(uint8 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
|
||
│ │
|
||
│ └─ 步骤4:ComfyUI 生发
|
||
│ └─ 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)(需手动在图上画发际线后作为划线图上传)
|