feat(接口99): 人脸特殊点检测 + 网关转发路由;文档同步至当前代码

- worker: POST /api/v1/face/special-points,60 个固定编号关键点(6组)
  + 5 个发际线特殊点(沿指定关键点方向射线步进,与 BiSeNet 头发 mask 首次相交;
  双射线取中点,left_left=mid(333→332, 298→284) 与右侧镜像)
- 网关: 同路径盲转发路由(调用频率低,维持转发不占网关依赖;
  响应无 *_base64 字段,改写逻辑天然跳过)
- 测试页 static/test_interface99.html:分组着色小点 + 点ID标注(可开关)
  + 发际线点大圆标签 + JSON 展示
- 文档: CLAUDE.md 重写至当前架构;实现说明/接口文档补接口 9~12/99 契约;
  网关待改动补 §7(9/10已加) §8(11/12待加) §9(接口99部署步骤)
This commit is contained in:
xsl
2026-09-06 22:57:51 +08:00
parent 3e4fc758ea
commit b215f81ca3
8 changed files with 900 additions and 83 deletions
+53 -18
View File
@@ -12,13 +12,15 @@
```
客户端 ──HTTPS──> 外网网关(gateway/) ──HTTP(X-Internal-Token)──> worker(GPU 机, app.py)
│ 薄代理 + 落盘改URL │ 跑算法(本地模型/ComfyUI)
└ 接口4 本机直接调豆包(不转发) └ 接口1/2/3/5/6/7
│ 薄代理 + 落盘改URL │ 跑算法(本地模型/ComfyUI/change_hair)
└ 接口4 本机直接调豆包(不转发) └ 接口1/2/3/5/6/7/9/10/11/12/99 + 工具路由
```
- **worker**(`app.py` + `face_analysis/` + `hairline/`):跑真正的算法,**纯本地、无外网依赖**。
对 `/api/*` 校验 `X-Internal-Token`(密码在 `worker_config.json` 的 `accept_passwords`);
`/health` 模型就绪才返回 200。监听 **8187**(`./start.sh` 控制开关,`./run_worker.sh` 热重载)。
- **worker**(`app.py` + `face_analysis/` + `hairline/`):跑真正的算法,除 ComfyUI/change_hair 走本机
回环外无外网依赖。对 `/api/*` 校验 `X-Internal-Token`(密码在 `worker_config.json` 的 `accept_passwords`);
`/health` 模型就绪才返回 200。监听 **8187**(systemd `hair-worker.service`,也可 `./run_worker.sh` 手动起)。
另有 worker 本机的工具路由:`/api/swapHair/v1`、`/hairColor/v2`(CORS 代理转发 change_hair:8801)、
`/api/v1/redraw`(直接 ComfyUI 重绘)、`/api/v1/debug/*`(免鉴权调试)。
- **网关**(`gateway/`):薄反向代理,健康检查/派发/鉴权/把 worker 的 `*_base64` 落盘改成 `*_url`。
唯一例外是**接口4 在网关本机直接实现**(调外网豆包,不转发 worker)。
- **图片三选一**:所有接口图片入参 `image_file`/`image_url`/`image_base64` 严格三选一(接口3 是 `marked_image_*`)。
@@ -33,7 +35,10 @@
| 2 | `results[].image_base64` / `results[].grown_image_base64`(可空) | `results[].image_url` / `results[].grown_image_url` |
| 3 | `hair_growth_image_base64`(可空) | `hair_growth_image_url` |
| 5 | `hairline_images[].image_{middle,high,low}_base64` / `grown_image_base64`(可空) | `hairline_images[].image_{middle,high,low}_url` / `grown_image_url` |
| 7 | `results[].image_base64` / `results[].grown_image_base64`(可空) | `results[].image_url` / `results[].grown_image_url` |
| 9 | `steps_common.*_base64`、`bisenet|segformer.{hair,closed_region,final_overlay,mask}_mask_base64` 等 | 同名 `*_url` |
| 10 | `steps_common.*_base64`、`bisenet|segformer.{base_mask,contour,band_overlay,mask}_base64` 等 | 同名 `*_url` |
| 11 | `steps.{input,baseline,hair_mask,mask_overlay,mask,swap_raw,alpha,final}_base64` | `steps.*_url` |
| 12 | `steps.{input,final,redraw_band_overlay,redraw_full,redraw_band,redraw_c}_base64` | `steps.*_url` |
| 4 | (网关本机产出,无图片字段,`features` 为 JSON 字符串) | — |
> 实现建议:递归遍历 data,凡 key 以 `_base64` 结尾就落盘改 `_url`,自动覆盖嵌套/新增字段。
@@ -47,7 +52,7 @@
---
## 2. 五个接口实现简述
## 2. 各接口实现简述
### 接口1 四庭七眼测量 `/api/v1/face/measure`(worker)
- **做什么**:正面照 → 四庭(顶/上/中/下庭) + 七眼(眼宽/脸宽/间距) 的 cm 与占比、5 个关键点坐标、一张透明底标注 PNG。
@@ -65,10 +70,10 @@
### 接口2 C端生发 `/api/v1/hair/grow`(worker)—— 预览 + 生发图
- **做什么**:正面照 + `gender`(必填) + `hair_style`(必填,逗号分隔多选,如 `1,2,3`) → 指定发际线类型 **N 组**:**预览图**(发际线叠在照片上) + **生发后图**(植发3个月效果)。
- **怎么实现**(`hairline/`):移植 head3d——MediaPipe(Tasks) + SegFormer 分割 + 17 锚点射线检测 → 502 点 mesh,
按 `face_ext.obj` 的 UV 把发际线贴图渲染到额头(预览)。生发:黑贴图渲染遮罩 → 调本机 **ComfyUI 8182** 的
按 `face_ext.obj` 的 UV 把发际线贴图渲染到额头(预览)。原生生发:黑贴图渲染遮罩 → 调本机 **ComfyUI 8188** 的
`add_hair.json`(Flux-2) 出图。**关键坑**:obj 是重排序,需 `INDEX_MAP_468` 把 MP 序→OBJ 序。
返回 `results[].image_base64` + `grown_image_base64`。
- `hair_style` 映射:female 1=ellipse 2=flower 3=heart 4=straight 5=wave 6=bigflower 7=clasicalflower;male 1=ellipse 2=inverse_arc 3=m 4=straight 5=heart 6=Softpetal 7=wave。female 1..5 走「换发型(change_hair)」+Flux-2 重绘管线;female 6/7 与 male 全部走原生发(ComfyUI add_hair)管线。
- `hair_style` 映射:female 1=ellipse 2=flower 3=heart 4=straight 5=wave 6=bigflower 7=clasicalflower;male 1=ellipse 2=inverse_arc 3=m 4=straight 5=heart 6=Softpetal 7=wave。female 1..5 走「换发型(change_hair)」+Flux-2 重绘管线(`generate_grow_results_swap`,复用接口12 final 的 `_V2_FINAL_DEFAULTS`,长边默认压到 `REDRAW_MAX_SIDE`=1024 再放大回原尺寸);female 6/7 与 male 全部走原生发(ComfyUI add_hair)管线。可选参数 `flux_model`(切换 Flux unet,.gguf 自动配 UnetLoaderGGUF + 配套 qwen CLIP)、`redraw_max_side`(0=不缩图)。
### 接口7 C端生发 v2 `/api/v1/hair/grow-v2`(worker)—— 接口2同款,add_hair2 工作流
- **做什么**:与接口 2 完全一致(正面照 + `gender` + `hair_style` 逗号分隔多选 → N 组预览+生发图)。
@@ -92,20 +97,50 @@
- **做什么**:入参同接口2(`gender` + 多选 `hair_style` 必填)。对每个选中发型 → `middle`/`high`/`low` 三档发际线叠图 + 生发图 + 首个选中发型的面部中间点坐标。
- **怎么实现**:复用接口2 的 502 点渲染管线,三档分别用 `hairline_texture[/_high|/_low]` 同名贴图渲染叠图;生发同接口2(ComfyUI inpaint),**黑模板固定取 `hairline_texture_black/`(middle)**,每发型 1 张生发图。`best_hairline_center_point`=眉心 x × 首个选中发型 middle 档发际线 y。
### 接口9 头发遮罩 `/api/v1/head/mask`(worker)
- **做什么**:人像照 → 头发遮罩 + 全流程分步可视化。
- **怎么实现**(`face_analysis/head_mask.py`):MediaPipe 底部分割线(21/68/104/69/108/151/337/299/333/298/251 连线,21/251 水平延伸)→ 线以上为上半区 → BiSeNet 与 SegFormer 双路头发分割、逐列向下填充到分割线得闭合区域(头发+额头)→ 外缘朝 151 内缩 `erode_cm`(默认1.2cm,虹膜标定) → 最终遮罩。返回 `steps_common` + `bisenet`/`segformer` 两组各四张图。
### 接口10 头部外缘膨胀带 `/api/v1/head/band`(worker)
- **做什么**:在接口9 基准遮罩基础上,沿头部外缘生成一条膨胀带遮罩。
- **怎么实现**(`face_analysis/head_band.py`):取基准遮罩外轮廓、去掉贴底分割线段 → 膨胀成总宽 `dilate_cm`(默认2cm) 的带子 → 裁到分割线以上。返回 `base_mask`/`contour`/`band_overlay`/`mask` 分步图。
### 接口11 发际线生发 `/api/v1/hairline/grow`(worker)—— 不含重绘
- **做什么**:发际线偏高/头发稀少正脸图 + `hairline_id`(= change_hair `hair_id`,如 `chang_tuoyuan`)→ 压低发际线图 + 全步骤可视化。
- **怎么实现**(`face_analysis/hairline_grow.py`,算法见 `发际线增强算法.md`):① 接口9 遮罩(`seg_model` 选 bisenet/segformer)→ ② 调 change_hair 生成该发际线类型图(`swap_mode=ext_mask` 把遮罩传给 webui 精确重绘 / `as_is` 用其内部遮罩;`gen_backend` 可换 hairgrow 区域生发 inpaint)→ ③ 严格按遮罩贴回 → ④ 接缝融合 `blend_method`(multiband 默认 / seamless / two_stage / feather / alpha_gradient,可选 Reinhard `color_match`)。返回 `steps.{input,baseline,hair_mask,mask_overlay,mask,swap_raw,alpha,final}`。
### 接口12 发际线带重绘 `/api/v1/hairline/grow_v2[_final[_v2]]`(worker)
- **做什么**:接口11 的下游重绘阶段——先跑接口11 拿 ④final,再取发际线重绘带(外推 `band_lo_mult×push`~`band_hi_mult×push`,默认 0.5×~1.5×)作遮罩,用 **Flux-2(ComfyUI,`0716add-hair-api.json`,reference-latent 保色)** 重绘,产出两版:`redraw_full`(整帧美颜+整帧重绘)与 `redraw_band`(band 内加发、band 外 final 结构 + `beauty_alpha` 融入全脸美颜)。
- **三个变体**:`grow_v2` 全参数暴露(接口11 参数 + `comfyui_prompt`/`beauty_alpha`/`band_lo_mult`/`band_hi_mult`);`grow_v2_final` / `grow_v2_final_v2` 精简生产版——仅图片 + `hairline_id`,其余固化 `_V2_FINAL_DEFAULTS`(push=0.8cm / two_stage / color_match=关 / beauty_alpha=0.6 等),两者后端计算完全一致(同一次输出含 A/B 两版),差异只是测试页展示哪版。接口2 女性 1..5 的生发图即复用此管线。
- ⚠️ 依赖 ComfyUI(:8188);重绘失败在 `data.redraw.c_error` 报告。
### 接口99 人脸特殊点 `/api/v1/face/special-points`(worker)
- **做什么**:人脸照 → 60 个固定编号 MediaPipe 关键点(6 组:eyebrow/eye/mouth/nose_midline/cheek/face_edge)+ 5 个发际线特殊点,原图像素坐标 JSON(字段 `<组名>_<点ID>`,如 `eyebrow_107`)。
- **怎么实现**(`face_analysis/special_points.py`):MediaPipe 单例取关键点;发际线点=接口1 方案B 的 BiSeNet 头发 mask 上沿指定关键点方向(如 151→10)射线步进取首个头发像素,双射线的(right/right_right/left/left_left)取中点。无图片字段,网关纯盲转发。
- 测试页:`static/test_interface99.html`(canvas 可视化 + JSON,worker 本机直连)。
---
## 3. 部署 / 环境要点
**worker(GPU 机)**
- Python **3.12**(系统 3.13 无 mediapipe/torch wheel);venv 在 `./venv`,依赖 `requirements.txt`。
- `numpy<2`(1.26.4),`scikit-image==0.24.0`(**别升 0.25+,会顶 numpy≥2 顶崩 mediapipe**)。
- ⚠️ 本机 **RTX 5090(sm_120)**,pinned `torch 2.2.2(cu121)` 只到 sm_90 → GPU 算子报 "no kernel image",
代码已自动**回退 CPU**(BiSeNet/SegFormer CPU 推理可用)。要用 5090 GPU 需换 torch cu128(≥2.7)。
**worker(GPU 机,本机 = gpu_worker 角色)**
- Python **3.10.12**(本机 venv 在 `./venv`);`numpy<2`(1.26.4),`scikit-image==0.24.0`
(**别升 0.25+,会顶 numpy≥2 顶崩 mediapipe**)。
- 本机 **RTX 4090**:venv 实装 `torch 2.7.1+cu128`,CUDA 可用(BiSeNet/SegFormer GPU 推理)。
`requirements.txt` 里 pinned 的 `torch 2.2.2(cu121)` 及 5090 注释是旧机(sm_120 无 cu121 kernel、
自动回退 CPU)的记录,本机以 venv 实际版本为准。
- 模型权重/字体见 [`../OFFLINE_ASSETS.md`](../OFFLINE_ASSETS.md);BiSeNet/SegFormer/face_landmarker.task 本地。
- 生发接口依赖本机 **ComfyUI(8188)**(Flux-2,它自带支持 5090 的 torch);worker 只调其 HTTP API,不跑 Flux。
ComfyUI 开了 **HTTP Basic Auth**(user `admin` + 密码);密码放 `password.txt`(不入 git) /
`worker_config.json.comfyui_password` / 环境变量 `COMFYUI_PASSWORD`,URL 用 `COMFYUI_URL`。
- `worker_config.json`(不入 git):`accept_passwords`(鉴权) + 鉴权头 `X-Internal-Token`。
- 生发接口依赖本机 **ComfyUI(8188)**(Flux-2,systemd `comfyui.service`,`--cache-classic --fast`);
worker 只调其 HTTP API,不跑 Flux。ComfyUI 开了 **HTTP Basic Auth**(user `admin` + 密码);
密码放 `password.txt`(不入 git) / `worker_config.json.comfyui_password` / 环境变量 `COMFYUI_PASSWORD`,
URL 用 `COMFYUI_URL`。默认模型 flux-2-klein-9b Q4_K_M GGUF(接口2 `flux_model` 参数可切)。
- 换发型依赖本机 **change_hair**(`/home/ubuntu/change_hair`,三进程 webui:57860(sdwebui) →
photo_service:32678(py310/kohya) → hair_service_sd:8801(my_hair),`start_all.sh start|stop|status`,
也有 systemd `change_hair-{webui,photo,hair}.service`)。不跑时接口2女1..5、接口11/12、
swapHair/hairColor 代理不可用。
- `worker_config.json`(不入 git):`accept_passwords`(鉴权) + `comfyui_password`;鉴权头 `X-Internal-Token`。
- systemd 单元:`hair-worker.service`(8187) / `comfyui.service`(8188),仓库根有副本;手动启停用
`./run_worker.sh` / `./stop_worker.sh`。
**网关机**
- 很轻:FastAPI/uvicorn/httpx + **接口4 的 `volcengine-python-sdk[ark]`**(或直接 httpx 调,OpenAI 兼容)。
+196 -12
View File
@@ -267,8 +267,10 @@
| gender | string | **是** | 性别:`male` / `female`。决定使用的发际线贴图集合 |
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`),最多不超过该性别的预设数。female:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave, 6=bigflower, 7=clasicalflower;male:1=ellipse, 2=inverse_arc, 3=m, 4=straight, 5=heart, 6=Softpetal, 7=wave。越界/非法返回 `1007` |
| beauty_enabled | bool | 否 | 生发图是否带美颜效果,默认 false(当前阶段不生效) |
| use_mask | bool | 否 | 是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 |
| prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发,加一点美颜」,会替换工作流节点 60 的文本 |
| use_mask | bool | 否 | 是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比。仅对原生发(male / female 6/7)管线生效 |
| prompt | string | 否 | ComfyUI 提示词,默认「填充遮罩区域的头发」,会替换工作流节点 60 的文本。仅对原生发管线生效(female 1..5 换发型分支的提示词内部固定) |
| flux_model | string | 否 | Flux 模型文件名(如 `flux-2-klein-9b-Q5_K_M.gguf`)。缺省用工作流默认(9B-Q4_K_M);`.gguf` 自动切 UnetLoaderGGUF 并同步配套 qwen CLIP |
| redraw_max_side | int | 否 | female 1..5 重绘管线送模型前的长边上限像素。缺省=服务端默认(`REDRAW_MAX_SIDE`,当前 1024);`0`=不缩图(原图直送,更慢) |
### 输出(data)
@@ -281,7 +283,8 @@
| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`/`bigflower`/`clasicalflower`(female),`ellipse`/`m`/`straight`/`inverse_arc`/`heart`/`Softpetal`/`wave`(male) |
| order | int | 排序序号(当前阶段固定 `1..N`,按贴图顺序,暂不计算合适度) |
> ⚠️ 生发图由本机 ComfyUI(Flux-2,端口 8182)生成,**一次请求生成指定发型的 1 张、同步返回**。
> ⚠️ 生发图由本机 ComfyUI(Flux-2,端口 8188)生成,**一次请求生成指定发型的 1 张、同步返回**;
> female 1..5 的生发图另依赖 change_hair 换发型服务(:8801)。
> worker 侧返回 `image_base64` / `grown_image_base64`,
> 网关落盘后改写为上表的 `image_url` / `grown_image_url`。
>
@@ -515,16 +518,197 @@
---
## 接口 9:头发遮罩生成接口
**说明**:输入一张含人头的照片,生成头发遮罩,并返回**每一步的可视化图**(供对比调试)。
**请求**:`POST /api/v1/head/mask`
### 输入
图片参数见「通用约定 → 图片传参字段」。专属参数:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| erode_cm | float | 否 | 外缘朝中心(鼻梁 151 点)内缩的距离(厘米),默认 1.2 |
### 处理流程
1. MediaPipe 关键点 → 底部分割线(关键点 21,68,104,69,108,151,337,299,333,298,251 的连线,21 水平延伸到最左、251 延伸到最右)。
2. 分割线以上为「上半区」。
3. 头发分割(**BiSeNet** 与 **SegFormer** 两套);从每列最顶端头发向下填充到分割线,得到**含额头**的闭合区域(头发+额头,不割断)。
4. 外缘朝中心点 151 内缩 `erode_cm`(虹膜标定换算像素)、底线不动 → 最终遮罩。
### 输出(data)
| 字段 | 类型 | 说明 |
|------|------|------|
| px_per_cm | float | 虹膜标定出的像素/厘米 |
| erode_cm / erode_px | float / int | 内缩距离(厘米 / 像素) |
| image_size | object | `{width, height}` 原图尺寸 |
| steps_common | object | `landmarks_baseline_*`(关键点/分割线)、`upper_region_*`(上半区) |
| bisenet / segformer | object | 两套分割模型各自的结果:`hair_mask_*`(原始头发分割)、`closed_region_*`(含额头闭合区域)、`final_overlay_*`(最终遮罩叠加可视化)、`mask_*`(最终遮罩 PNG)及 `hair_pixels`/`closed_pixels`/`mask_pixels` 像素计数 |
> worker 返回 `*_base64`(data URI),经网关改写为 `*_url`。
---
## 接口 10:头部外缘膨胀带遮罩接口
**说明**:先和接口9 一样得到内缩后的基准遮罩,再沿头部外缘生成一条膨胀带遮罩。
**请求**:`POST /api/v1/head/band`
### 输入
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| erode_cm | float | 否 | 基准遮罩外缘朝 151 内缩距离(厘米,同接口9),默认 1.2 |
| dilate_cm | float | 否 | 外轮廓线膨胀后带子总宽(厘米),默认 2.0 |
### 处理流程
1. 取基准遮罩的**外轮廓线**,去掉贴着底部分割线的那一段(只留头发/头部外缘弧线)。
2. 把外轮廓线膨胀成带子(总宽 `dilate_cm`;半径=总宽/2,虹膜标定换算像素)。
3. 裁到分割线以上(不越过底线)。
### 输出(data)
结构同接口9,但每组结果为 `base_mask_*`(基准遮罩)/ `contour_*`(外轮廓线)/ `band_overlay_*`(带子叠加可视化)/ `mask_*`(最终带状遮罩)及 `base_pixels`/`band_pixels` 计数,另有 `dilate_cm`/`dilate_radius_px`。
---
## 接口 11:发际线生发接口(不含重绘)
**说明**:输入一张**发际线较高 / 头发稀少**的正脸图 + 发际线类型 ID,输出同一个人、同一发型、按该类型压低发际线后的图片,并返回**每一步可视化**。重绘版见接口12。
**请求**:`POST /api/v1/hairline/grow`
### 输入
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| hairline_id | string | **是** | 发际线类型 ID(= change_hair `hair_id`):`chang_tuoyuan` / `chang_huaban` / `chang_xinxing` / `chang_zhixian` / `chang_bolang` |
| gen_backend | string | 否 | 生成后端:`swaphair`(换发型 LoRA,默认)|`hairgrow`(区域生发 inpaint) |
| hairgrow_strength | float | 否 | 区域生发强度(仅 hairgrow 后端),默认 0.75 |
| is_hr | bool | 否 | 高清模式(换发型输出 1152×1536,否则 576×768),默认 false |
| seg_model | string | 否 | 头发分割模型:`bisenet` | `segformer`(默认) |
| erode_cm | float | 否 | baseline 参考内缩距离(厘米),默认 0.6 |
| hairline_push_cm | float | 否 | 发际线外推距离(厘米,往头发方向推进),默认 1.0 |
| hairline_edge | string | 否 | 发际线提取方式:`column`(默认)|`contour` |
| swap_mode | string | 否 | 换发型取图模式:`ext_mask`(默认,把接口9 遮罩传给 webui 精确重绘)|`as_is`(用内部遮罩,贴回时再裁) |
| edge_erode_px | int | 否 | 贴图前遮罩内缩像素(防边缘露皮/光晕),默认 3 |
| denoising_strength | float | 否 | 换发型 webui 重绘强度(越大生发越激进),默认 0.6 |
| mb_levels | int | 否 | 多频段金字塔层数(2~6),默认 5 |
| blend_method | string | 否 | 接缝融合:`multiband`(默认)|`seamless`(泊松)|`two_stage`(泊松→多频段)|`feather`(高斯羽化)|`alpha_gradient`(距离渐变) |
| color_match | bool | 否 | 融合前 Reinhard 颜色迁移消除整体色差,默认 true |
| color_match_strength | float | 否 | 颜色迁移强度(0~1),默认 1.0 |
| mb_feather_px | int | 否 | 多频段最细层掩码轻羽化像素,默认 1 |
| transition_band_px | int | 否 | keep-region 过渡带边距(-1=自动按层数 2^n),默认 -1 |
| inpainting_fill | int | 否 | change_hair 服务端重绘填充:0=保留原图 | 1=噪声(默认)| 2=纯色 | 3=潜变量噪声 |
| mask_blur | int | 否 | change_hair 服务端遮罩边缘模糊像素,默认 11 |
| mask_dilate_scale | float | 否 | change_hair 服务端遮罩膨胀缩放(<1 收缩防越界),默认 1.0 |
### 输出(data)
`steps`:`input`(原图)/ `baseline`(底部分割线)/ `hair_mask`(头发分割)/ `mask_overlay`(遮罩叠加)/ `mask`(遮罩)/ `swap_raw`(换发型原始输出)/ `alpha`(融合权重)/ `final`(最终结果)各步图(`*_base64`,经网关改写为 `*_url`),另有 `timings_ms` 分步耗时。
> ⚠️ `swaphair` 后端依赖 change_hair(:8801)在跑;`hairgrow` 后端依赖 ComfyUI(:8188)。
---
## 接口 12:发际线带重绘接口
**说明**:接口11 的**下游重绘阶段**——内部先跑接口11 核心管线拿 ④ 接缝融合最终图(final),再取发际线重绘带(外推 `band_lo_mult×push` ~ `band_hi_mult×push` 之间、经 baseline 截断只留上部的带状区域)作遮罩,用 **Flux-2(ComfyUI,reference-latent 保色重绘)** 重绘。
**同时产出两版结果**:
- `redraw_full`:ComfyUI 整帧输出(全脸美颜 + 全脸重绘)。
- `redraw_band`:加发只在发际线带内、band 外保留 final 结构并按 `beauty_alpha` 融入全脸美颜。
**请求**:`POST /api/v1/hairline/grow_v2`
### 输入
接口11 的全部参数(见上表)再叠加:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| comfyui_prompt | string | 否 | Flux-2 重绘提示词,缺省「填充遮罩区域的头发」 |
| beauty_alpha | float | 否 | redraw_band 版 band 外的全脸美颜融入强度(0=band 外纯用 final,1≈整帧版),默认 0.6 |
| band_lo_mult | float | 否 | 重绘带外推倍率下限(相对 hairline_push_cm),默认 0.5 |
| band_hi_mult | float | 否 | 重绘带外推倍率上限(相对 hairline_push_cm),默认 1.5 |
### 输出(data)
`steps`:`input` / `final`(重绘输入基底)/ `redraw_band_overlay`(重绘带可视化)/ `redraw_full`(A 整帧)/ `redraw_band`(B 局部+美颜)/ `redraw_c`(兼容旧字段,=redraw_full);重绘异常在 `redraw.c_error` 报告。
### 精简变体(生产推荐)
| 路径 | 说明 |
|------|------|
| `POST /api/v1/hairline/grow_v2_final` | 仅需图片 + `hairline_id`,其余参数固化为调优默认值(push=0.8cm / two_stage / color_match=关 等),**最终取整帧重绘 `redraw_full`** |
| `POST /api/v1/hairline/grow_v2_final_v2` | 参数同上,**最终取 B 局部加发+全脸美颜 `redraw_band`** |
> ⚠️ 依赖 ComfyUI(:8188)与 change_hair(:8801)均在跑。接口2 女性 1..5 的生发图即复用 `grow_v2_final` 管线。
---
## 接口 99:人脸特殊点检测接口
**说明**:输入一张人脸照片,返回一组**固定编号 MediaPipe Face Mesh 关键点**的像素坐标,外加 5 个**发际线特殊点**(射线与头发分割发际线的交点)。
**请求**:`POST /api/v1/face/special-points`
### 输入
图片参数见「通用约定 → 图片传参字段」,无专属参数。要求单人正面照(非正面返回 `1003`)。
### 输出(data)
`points`:字段名 = `<组英文名>_<点ID>`,值为原图像素坐标 `{x, y}`(int);发际线点不可得(秃顶/分割失败)时为 null。
| 组 | 字段前缀 | 点 ID |
|----|----------|-------|
| 眉毛 | `eyebrow` | 107 66 105 63 70 336 296 334 293 300 |
| 眼睛 | `eye` | 189 222 224 113 226 413 442 444 342 446 |
| 嘴 | `mouth` | 0 37 40 67 91 84 17 405 375 409 269 |
| 中线鼻 | `nose_midline` | 9 168 197 5 4 194 278 48 437 217 |
| 面颊 | `cheek` | 101 117 123 187 205 50 330 346 352 280 425 |
| 边线 | `face_edge` | 162 234 58 136 149 152 378 365 288 401 366 389 251 |
**发际线特殊点**(沿关键点 a→b 方向射线步进,首次进入 BiSeNet 头发 mask 处为交点;双射线取中点,仅一条命中返回该点):
| 字段 | 计算 |
|------|------|
| `hair_line_center` | 151→10 方向交点 |
| `hair_line_right` | mid(108→109 交点A, 69→67 交点B) |
| `hair_line_right_right` | mid(104→103 交点A, 68→54 交点B) |
| `hair_line_left` | mid(337→338 交点A, 299→297 交点B) |
| `hair_line_left_left` | mid(333→332 交点A, 298→284 交点B) |
另有 `image_size`(宽高)与 `hairline_source`(`segmentation`=发际线点已算出 / `unavailable`=分割不可用,发际线点为 null)。
---
## 汇总:输入输出一览
| 接口 | 输入 | 主要输出 |
|------|------|----------|
| 1 四庭七眼测量 | 用户照片 | 标注 PNG(无人物)+ 四庭/七眼厘米数值与坐标 |
| 6 四庭七眼测量 v2 | 用户照片 | 同接口1,复刻实现 |
| 2 C 端生发 | 用户照片 | 生发后图片 + 指定发际线预览(单/多张) |
| 3 B 端生发 | 划线图片 | 最合适发际线图片 + 生发后图片 |
| 4 用户特征 | 用户照片 | 6 个用户特征字段(脸形/眉形/年龄/动静/性别/基因风格) |
| 5 发际线 PNG | 用户照片 + gender + hair_style(多选) | 每个选中发型 middle/high/low 三档发际线叠图 + 生发图 + 最合适发际线面部中间点坐标 |
| 接口 | 路径 | 输入 | 主要输出 |
|------|------|------|----------|
| 1 四庭七眼测量 | `/api/v1/face/measure` | 用户照片 | 标注 PNG(无人物)+ 四庭/七眼厘米数值与坐标 |
| 6 四庭七眼测量 v2 | `/api/v1/face/measure-v2` | 用户照片 | 同接口1,去顶庭变体 |
| 2 C 端生发 | `/api/v1/hair/grow` | 用户照片 + gender + hair_style(多选) | 每发型预览 PNG + 生发图(female 1..5=换发型+重绘,其余=原生生发) |
| 7 C 端生发 v2 | `/api/v1/hair/grow-v2` | 同接口2 | 同接口2(add_hair2 工作流,已弃用隐藏) |
| 3 B 端生发 | `/api/v1/hair/grow-b` | 划线图片 | 检测到的发际线 + 生发后图片 |
| 4 用户特征 | `/api/v1/face/features` | 用户照片 | 6 个用户特征字段(脸形/眉形/年龄/动静/性别/基因风格),网关本机实现 |
| 5 发际线 PNG | `/api/v1/hairline/generate` | 用户照片 + gender + hair_style(多选) | 每个选中发型 middle/high/low 三档发际线叠图 + 生发图 + 最合适发际线面部中间点坐标 |
| 9 头发遮罩 | `/api/v1/head/mask` | 用户照片 | 头发遮罩 + 分步可视化(BiSeNet/SegFormer 双路) |
| 10 外缘膨胀带 | `/api/v1/head/band` | 用户照片 | 头部外缘带状遮罩 + 分步可视化 |
| 11 发际线生发 | `/api/v1/hairline/grow` | 用户照片 + hairline_id | 压低发际线图 + 全步骤可视化(不含重绘) |
| 12 发际线带重绘 | `/api/v1/hairline/grow_v2` | 同接口11 + 重绘带参数 | final 基底 + Flux-2 保色重绘(整帧/局部两版);精简版 `grow_v2_final[_v2]` 仅需图片+hairline_id |
| 99 人脸特殊点 | `/api/v1/face/special-points` | 用户照片 | 60 个固定编号关键点 + 5 个发际线特殊点像素坐标 JSON |
> 另有 worker 本机工具路由(不在对外契约内):`/api/swapHair/v1`、`/hairColor/v2`(CORS 代理 change_hair)、`/api/v1/redraw`(直接 ComfyUI 重绘)、`/api/v1/debug/*`。
---
@@ -534,7 +718,7 @@
- 默认单人;检测到 2~3 人报错提示客户端;单文件 **≤ 1 MB**;格式仅 **JPG / PNG**。
- 所有坐标基准:**原图像素**。
- 接口 2:提供「美颜」开关 `beauty_enabled`。
- 接口 3:需同时上传原图,需返回发际线类型。
- 接口 3:仅传**划线图**(`marked_image_*` 三选一),需返回发际线类型(`custom`)。
- 接口 4:`data` 返回**一个 JSON 字符串**,字段不固定,可随时调整。
- 接口 5:发际线 PNG 数量 **N 不固定**,需按合适度**排序**。
- 英文字段命名由本文档约定(见各接口表格)。
+77 -1
View File
@@ -70,7 +70,7 @@ async def hair_grow_v2(request: Request):
|------|------|------|------|
| image_file / image_url / image_base64 | — | **三选一** | 用户正面照 |
| gender | string | **是** | `male` / `female` |
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`)。female: 1–5,male: 1–4 |
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`)。female / male 均为 1–7 |
| beauty_enabled | bool | 否 | 美颜开关(本期不生效) |
| use_mask | bool | 否 | 默认 `true`,`false` 跳过遮罩 |
| prompt | string | 否 | ComfyUI 提示词 |
@@ -135,6 +135,82 @@ async def face_measure_v2(request: Request):
---
## 7. ✅【已完成】接口9/10 头部遮罩(`/api/v1/head/mask`、`/api/v1/head/band`)
`gateway/app.py` 已新增两条盲转发路由(同接口6 的写法):
```python
@app.post("/api/v1/head/mask", tags=["人脸分析"])
@app.post("/api/v1/head/band", tags=["人脸分析"])
```
- 入参:图片三选一 + `erode_cm`(接口10 另有 `dilate_cm`)。
- 出参:`steps_common.*_base64` 与 `bisenet`/`segformer` 组内的 `*_base64` 字段——现有递归
`rewrite_base64_to_url` 已覆盖(嵌套 dict/数组),无需额外改动。
- 两个接口响应较慢(双分割模型跑两遍),注意 `request_timeout_seconds`。
---
## 8. 🔲【新增】接口11/12 发际线生发/带重绘(`/api/v1/hairline/grow*`)
worker 侧已上线 4 条路由,**网关尚无对应转发路由**(当前仅测试页直连 worker)。如需对外提供,参考接口9 加法:
```python
@app.post("/api/v1/hairline/grow", tags=["生发"]) # 接口11
@app.post("/api/v1/hairline/grow_v2", tags=["生发"]) # 接口12 全参数
@app.post("/api/v1/hairline/grow_v2_final", tags=["生发"]) # 接口12 final 精简版
@app.post("/api/v1/hairline/grow_v2_final_v2", tags=["生发"])
```
- 入参:图片三选一 + `hairline_id`(接口12 final 两变体仅需这两个)。接口11/12 全参数版还有
20+ 个调优 Form 参数,网关盲转发即可,无需逐个声明。
- 出参:`data.steps.*_base64`(input/final/redraw_full/redraw_band 等各步图)→ 现有递归改写覆盖。
- ⚠️ 依赖 worker 机的 change_hair(:8801) 与 ComfyUI(:8188) 都在跑;单请求 10s+,超时建议 ≥120s。
- 契约细节见 [`接口文档.md`](接口文档.md) 接口11/12 章节。
---
## 9. ✅【新增】接口99 人脸特殊点(`/api/v1/face/special-points`)
**背景**:worker 侧新增接口99——固定编号 MediaPipe 关键点(6 组 60 点)+ 5 个发际线特殊点,
返回原图像素坐标 JSON(无图片字段)。
**网关代码已在本仓库 `gateway/app.py` 改好**(同接口1 的盲转发写法,pull 后重启即生效):
```python
@app.post("/api/v1/face/special-points", tags=["人脸分析"])
async def face_special_points(request: Request):
"""接口99:人脸特殊点检测(固定关键点 + 发际线特殊点像素坐标)"""
return await _proxy(request, "/api/v1/face/special-points")
```
**网关机部署/调试步骤**:
```bash
cd /path/to/hair && git pull # 拉取本分支
sudo systemctl restart hair-gateway # 或项目实际的网关服务名(uvicorn gateway.app:app)
# 验证路由已注册(应能在输出里看到 /api/v1/face/special-points)
curl -s http://127.0.0.1:8080/openapi.json | python3 -m json.tool | grep special-points
# 冒烟:经网关调一次(multipart 传图,无需 X-Internal-Token,网关会注入)
curl -s -X POST http://127.0.0.1:8080/api/v1/face/special-points \
-F "image_file=@/tmp/face.jpg" | python3 -m json.tool | head -30
```
**无需额外改动**:
- 请求:multipart/form-data,仅图片三选一参数,盲转发即可。
- 响应:`data.points` 全是坐标(`{"x":..,"y":..}`),**没有任何 `*_base64` 字段**,
现有 `rewrite_base64_to_url` 递归改写天然跳过,原样返回。
- 性能:纯本地推理(MediaPipe + BiSeNet,秒级),不依赖 ComfyUI/change_hair,
`request_timeout_seconds` 无需调大。
**注意**:
- worker 侧测试页 `static/test_interface99.html` 是 worker 本机直连的(带 token 头),
经网关调试请用 curl 或网关 `/docs`(网关只盲转发 `/api/*`,不代理 worker 的 `/static`)。
- 契约详见 [`接口文档.md`](接口文档.md) 接口99 章节;worker 错误码 1001(无人脸)/1003(非正面)
/1007/1008 原样透传。
---
## 已经做好、无需再动的
- **接口4 在网关本机实现**(调豆包,不转发 worker)——已完成;config 里配 `ark`。