7.1 KiB
7.1 KiB
接口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 提示词,默认"补充遮罩区域的头发,加一点美颜" |
返回
{
"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)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k))
bh = cv2.morphologyEx(gray, cv2.MORPH_BLACKHAT, kernel)
黑帽 = 闭运算 − 原图,响应"比局部邻域暗的细结构"(即马克笔线条),对抬头纹/眉毛/发丝鲁棒。
5.3 Dijkstra 最小代价路径
- ROI 限定:额头上部 ∩ 头部轮廓(排除背景)
- 锚点:左鬓角(21) / 右鬓角(251) MediaPipe 关键点
- 代价图:
cost = (bh.max() - bh) + 1.0,ROI外设 1e6 - 路径:
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
- 测试图片:image/girl_img/girl13.jpg(需手动在图上画发际线后作为划线图上传)