feat: 新增52号发际线接口并限制GPU worker来源IP

保留5号历史三档实现,新增中下庭高度定位、扶正裁切、矩形叠加与30案例对比页。

worker仅允许本机和配置白名单访问;网关不增加IP限制。空图片URL字段改写限定为52号,保持旧接口兼容。同步接口、裁切与部署文档。

验证:113项测试通过;1项历史数值基线失败,在未修改的a3fddbb版本复现。浏览器验证5/52及30案例对比页通过。
This commit is contained in:
xsl
2026-11-04 10:20:29 +08:00
parent a3fddbbc34
commit 5b2b73f5d5
35 changed files with 2826 additions and 26 deletions
+18 -5
View File
@@ -13,21 +13,22 @@
```
客户端 ──HTTPS──> 外网网关(gateway/) ──HTTP(X-Internal-Token)──> worker(GPU 机, app.py)
│ 薄代理 + 落盘改URL │ 跑算法(本地模型/ComfyUI/change_hair)
└ 接口4(豆包)/99(特殊点) 本机实现(不转发) └ 接口1/2/3/5/6/7/9/10/11/12 + 工具路由
└ 接口4(豆包)/99(特殊点) 本机实现(不转发) └ 接口1/2/3/5/52/6/7/9/10/11/12 + 工具路由
```
- **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/*`(免鉴权调试)。
`/api/v1/redraw`(直接 ComfyUI 重绘)、`/api/v1/debug/*`(免Token调试,仍受IP限制)。
所有动态路径先校验直连来源IP:允许本机与网关`49.232.51.171`,其余返回HTTP403/code1010;`/static/`除外。配置见[`接口IP白名单.md`](接口IP白名单.md),网关侧不增加IP限制。
- **网关**(`gateway/`):薄反向代理,健康检查/派发/鉴权/把 worker 的 `*_base64` 落盘改成 `*_url`。
网关本机实现的接口(不转发 worker):**接口4**(调外网豆包)与**接口99**(纯 CPU 特殊点检测,
直接 import 同仓库 `face_analysis/special_points`,`_if99_lock` 单路排队)。
- **图片三选一**:所有接口图片入参 `image_file`/`image_url`/`image_base64` 严格三选一(接口3 是 `marked_image_*`)。
- **响应信封**:`{code, message, request_id, data}`;业务错误用 `code`(HTTP 一律 200)。
- **响应信封**:`{code, message, request_id, data}`;业务错误用 `code`(HTTP 200);IP拒绝为HTTP403/code1010,Token错误为HTTP401/code1009,依赖健康异常为HTTP503。
### base64 → URL 映射(网关落盘改写,递归进数组、可空保留 null)
### base64 → URL 映射(网关落盘改写,递归进数组)
| 接口 | worker 字段(内部) | 对外字段 |
|------|--------------------|----------|
@@ -36,6 +37,7 @@
| 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` |
| 52 | `hairline_images[].image_{middle,high,low}_base64` / `grown_image_base64`、`input_image_base64` / `original_image_base64` | 同名 `*_url`,空图片也改为`*_url: null` |
| 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` |
@@ -43,13 +45,14 @@
| 4 | (网关本机产出,无图片字段,`features` 为 JSON 字符串) | — |
> 实现建议:递归遍历 data,凡 key 以 `_base64` 结尾就落盘改 `_url`,自动覆盖嵌套/新增字段。
> 兼容约定:接口52显式开启空图片字段改名;接口2/3/5等旧接口仍保留空图片的`*_base64: null`字段名。有图片时均落盘为`*_url`。
> **图片格式**:接口1 标注图含透明用 **PNG**;接口2/3/5 是不透明照片用 **JPG**(小很多,~9×)。
> 网关落盘按内容嗅探扩展名(PNG 头→`.png`,否则 `.jpg`)。
### 错误码
`1001` 无法识别人像 | `1002` 分辨率过低 | `1003` 非正面 | `1004` gender 必填/非法(接口2/5/7)|
`1006` >1MB | `1007` 图片参数错误(0或多个)/未预期异常 | `1008` 格式不支持。
`1006` >1MB | `1007` 图片参数错误(0或多个)/未预期异常 | `1008` 格式不支持 | `1009` Token鉴权失败 | `1010` worker来源IP不允许或白名单配置无效。
---
@@ -99,6 +102,15 @@
- **做什么**:入参同接口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。
### 接口52 发际线PNG生成 `/api/v1/hairline/generate-52`(worker)
- **预处理**:`hairline/framing.py`先扶正9→152,再按高(3÷0.9)L、宽2.5L(3:4)做原像素裁切,人物相对原3L规则缩小约10%。L为9→152距离;眉心位置沿用已扶正参考图比例。长图的标准窗口不足时,求原图内最大的3:4窗口,通过可行区域约束保留全部人脸关键点,并尽量贴近目标眉心位置;无法保留人脸时回退仅扶正。其余图片空间不足时仍仅扶正、去除旋转空角。参考图与参数由`scripts/calibrate_hairline_framing.py`生成。公开接口为兼容旧调用默认关闭预处理,`normalize_image=true`开启;规则页显式开启,对比调试页默认开启。返回当前底图、原图及双向坐标矩阵。
- **目标高度**:`上庭=(中庭+下庭)/2`,用当前底图测量的未取整像素坐标(眉心9、鼻翼下缘94、下巴152)算目标中心。测量失败时用发际线管线同编号点计算并标注来源。
- **模板定位**:原有502点/贴图生成形状,`hairline/height_rule.py` 将每种 middle 曲线沿面部向上方向平移、校准中心 Y,保留曲线形状与倾斜。此规则仅接口52使用,接口2保持原逻辑。
- **生发**:移动后的预览曲线直接生成黑线和重绘遮罩,再送ComfyUI,避免预览与黑模板位置不一致。
- **裁切叠加预览**:测试页新增“裁切发际线叠加”。`static/hairline_reference_box.js`共享同一个162/389/9/152参考框几何,按原图像素裁切middle透明图层,再叠加到处理后人像;四栏顺序为原图、middle、裁切发际线叠加、生发图。
- **兼容**:high/low不再生成,图片与中心点字段保留为null。网关`gateway/forward.py`仅为接口52开启空图片`*_base64`→`*_url: null`,旧接口空字段名保持原样。部署52路由时同步该改写;网关不增加IP限制。`face_measure`为当前底图测量;新增`height_rule`与各项`height_placement`说明设计目标与实际误差。
- **测试**:规则页`static/test_interface52.html`,对比页`static/test_interface52_compare.html`;运行`scripts/compare_hairline_height.py`默认保存全部30张×男女各7型,共420组对比。每人一行,顺序为未裁切原图、单张旧规则图、全部新规则样式,支持位置对照、额头特写、同图叠线和上传新图。新图可叠加162/389两侧、底边经过9、高度为9→152距离70%的参考框。原图与叠图按需加载。
### 接口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` 两组各四张图。
@@ -126,6 +138,7 @@
## 3. 部署 / 环境要点
**worker(GPU 机,本机 = gpu_worker 角色)**
- 启动使用`./run_worker.sh`或`hair-worker.service`,均显式传`--no-proxy-headers`以保留直连来源IP。部署时复制`ip_access.example.json`为`ip_access.json`并核对网关IP;运行配置不入Git,改动即时生效。旧启动方式也应补上此参数,详见[`接口IP白名单.md`](接口IP白名单.md)。
- 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 推理)。
+119
View File
@@ -0,0 +1,119 @@
# 接口52上传图片裁切与扶正规划
状态:2026-11-04 在原3L规则上将人物显示缩小约10%,裁切高度改为(3÷0.9)L≈3.3333L、宽度2.5L,已接入接口52与对比页。参考图先扶正后重新标定,以下旧预检数值仅为历史记录,当前标准以 `hairline/reference/framing_profile.json` 为准。
## 接口拆分
- 接口5已按11月2日前最新提交`a3fddbb`恢复:`POST /api/v1/hairline/generate`,返回middle/high/low三档,测试页`/static/test_interface5.html`。
- 当前新规则完整迁至接口52:`POST /api/v1/hairline/generate-52`,测试页`/static/test_interface52.html`,30案例对比`/static/test_interface52_compare.html`。接口52保留迁移前所有参数和返回字段,页面显式开启`normalize_image=true`。
- 原`test_interface5_compare.html`书签跳转至52号对比页;公开5号与52号的处理路由、生成函数和网关转发均独立。
## 已确定的要求
- 仅用于接口52与当前旧/新规则对比页。
- 先扶正:变换后的9号点与152号点保持同一X坐标,9在上、152在下。
- 原图空间够时按目标人脸比例和位置裁切;标准窗口不足时,偏长图片在保留人脸的前提下裁为3:4,其余仍仅扶正。3:4无法保留完整人脸时不强裁。
- 输出保留实际像素尺寸,不为了凑参考图尺寸而放大小图。
- 当前展示:全部30个案例,每个案例一行,未裁切原图放最前,旧规则图仅一张,新规则男女各7型横排,人物图片完整显示。选择新样式时旧图同步对应形状;失败案例仍保留原图并显示原因。
## 已扶正标准(当前生效)
- 标准图:`static/framing_reference.png`,786×1048,3:4。
- 两点X均为385.18783691;9号Y为360.65437127,152号Y为725.82510480。
- 相对位置:9号点 x=49.00608612%,y=34.41358504%。
- 当前裁切高/L=3÷0.9≈3.3333、宽/L=2.5(3:4),L为9→152长度。参考图自身测得的宽/L=2.15241783589、高/L=2.86989044785作为原始标定记录保留;眉心在画面中的位置比例继续使用参考图数值。
- 校准旋转约0.25962484°,变换后横向误差约5.7e-14像素。参数取自扶正并去空角后的图,不再使用未扶正原图的比例。
- 重标定脚本:`./venv/bin/python scripts/calibrate_hairline_framing.py <参考图路径>`。
- 使用同一套原始检测点作刚体变换与后续测量,避免二次检测抖动破坏轴线竖直。
- 公开接口新增`normalize_image`默认false,旧请求保持原图尺寸和坐标;显式true开启预处理。规则页主动传true,对比调试页默认开启。响应明确返回`coordinate_space`、当前底图和坐标变换。
## 原始参考构图(旧测量)
[用户指定参考图](https://raw.githubusercontent.com/feastfuyan/yonghe-badcase-0917/main/misc/20261103_user.jpg)
测量采用当前发际线管线的MediaPipe FaceLandmarker:
| 指标 | 测量值 |
|---|---|
| 画面 | 792×1056,3:4 |
| 9号点 | (388.93, 364.62) |
| 152号点 | (387.27, 729.79) |
| 9→152长度L | 365.17像素 |
| 面部轴线距竖直方向 | 约0.26° |
| 9号点画面相对位置 | x约49.1%,y约34.5% |
| 152号点画面相对高度 | y约69.1% |
| 画面宽度/L | 约2.16885 |
| 画面高度/L | 约2.89180 |
参考图接近竖直。以9号点为定位锚点,用9→152长度作为缩放无关的构图标尺。这里的L仅为眉心到下巴的距离,不是头顶到下巴的全头高度。
## 实施流程
1. 读取上传原图、处理EXIF方向,再检测人脸关键点。
2. 用9→152方向计算二维旋转,使转换后两点竖直。不改变脸部比例。
3. 按本人的L算目标裁切尺寸:高(3÷0.9)L、宽2.5L;令9号点位于裁切画面约(49.0061%,34.4136%)处。高度取最接近的4的倍数,宽=高×3/4,不改变原图像素尺度。
4. 将裁切窗口四角反算回原图,四角均在有效图片区域内才执行标准构图。采用一次仿射变换合并旋转与裁切,降低重复插值。
5. 标准窗口空间不足时,偏长图片采用原图内最大的3:4有效窗口,约束所有人脸关键点在框内,并尽量靠近目标眉心位置。若无法保留完整人脸则仅扶正并给出原因。其余图片仍仅扶正,最小去除旋转空角。
6. 后续测量、分割、旧/新发际线和70%参考框使用同一个处理后底图与坐标系。保留原图及原图/处理后坐标转换关系,防止叠图错位。
7. 页面展示原图、处理后图、旋转角、是否标准裁切、跳过原因与前后尺寸。旧/新规则对比使用同一处理后底图,避免构图变化混入高度规则差异。
## 当前:长图与矩形发际线预览
- 长图即使放不下标准人脸占比窗口,也尝试在原图有效区域裁出最大的3:4窗口;通过可行区域约束保留468个人脸关键点,并尽量贴近参考眉心位置。无法完整保留人脸时回退仅扶正,响应说明原因。
- 案例7从1080×1920裁为1062×1416,保持9→152竖直。长图7、20、21、22、50、51的男女各7型已更新,84组均成功;对比页30张、378组结果统一使用当前规则。
- 规则页展示原图、middle、裁切发际线叠加、生发图。新增预览使用共享矩形几何裁切透明发际线,矩形外清空透明,底图仍是完整处理后人像。
## 人物缩小10%的验证(长图规则加入前)
- 裁切高=(3÷0.9)L、宽=2.5L,使同样展示尺寸下的人物线性尺寸约为原3L规则的90%。
- 案例1由828×1104改为918×1224,人脸相对大小约90.20%,差异来自原像素窗口取整;底图、发际线和实际生发图均为918×1224。
- 对比页保留全部30张。重新计算了1、3、42、43、44、45;其余48组结果验证新窗口仍超出有效范围,且只扶正后的矩阵和尺寸完全一致,复用原定位图并更新裁切参数。
- 当前生成378组对比,43、44、45的男女组仍显示原图及定位失败原因。布局、眉心比例和9→152扶正方向保持一致。
## 历史:3L规则的五个案例验证
2026-11-04 按3L规则重跑男女各7型,共70组,失败0组。保留原始像素,不缩放人脸;实际尺寸如下。
| 案例 | 原尺寸 | 处理后尺寸 | 实际处理 |
|---|---|---|---|
| 1 | 1088×1406 | 828×1104 | 按3L规则裁切 |
| 2 | 1088×1406 | 1042×1346 | 空间不足,仅扶正并去除旋转空角 |
| 3 | 1088×1406 | 810×1080 | 按3L规则裁切 |
| 4 | 1088×1406 | 1042×1346 | 空间不足,仅扶正并去除旋转空角 |
| 5 | 1088×1367 | 1071×1346 | 空间不足,仅扶正并去除旋转空角 |
## 历史几何预检(未扶正参考图)
以下为修改前的预检记录,当前生效尺寸见上表。
| 案例 | 原尺寸 | 所需裁切尺寸(约) | 预计处理 |
|---|---|---|---|
| 1 | 1088×1406 | 797×1063 | 可按参考构图裁切并扶正 |
| 2 | 1088×1406 | 1442×1923 | 空间不足,仅扶正 |
| 3 | 1088×1406 | 782×1042 | 可按参考构图裁切并扶正 |
| 4 | 1088×1406 | 1465×1953 | 空间不足,仅扶正 |
| 5 | 1088×1367 | 1463×1950 | 空间不足,仅扶正 |
## 历史:3L规则的全部30张验证
- 按3L规则运行全部30张、男女各7型,共60次请求;55组请求成功,生成385组旧/新规则对比。27张男女均成功,案例43额外生成男性7型。
- 案例43女性:模板在面部中轴处没有有效曲线;案例44、45男女:扶正后无法保留9/152参考点。三个案例均在对应行显示完整原图和未生成组合的原因,不隐藏失败案例。
- 页面顺序为未裁切原图、旧规则、全部新规则;案例筛选包括失败图片,重跑按钮运行完整30张。
- 页面筛选、旧图切换、末尾图片加载及移动端横向浏览均通过;3L调参后再次检查全部输出尺寸与混合成功/失败案例的筛选。
## 验收重点
- 处理后的9、152号点X差不超过1像素,保证9在152上方。
- 标准裁切成功时,画面比例及两点位置接近参考构图,保留实际像素,不放大小图。
- 空间不足的案例明确提示,仍完成扶正。
- 旧/新规则叠图、参考框与底图坐标一致,原图坐标可通过逆变换还原。
- 全部30案例均可查看,成功案例显示男女各7型,失败案例显示原图与原因;沿用每人一行、单张旧规则图的展示。
## 2026-11-04 提交前 Review 与验证
- 接口5的请求处理函数、三档PNG生成函数与`a3fddbb`逐项AST比较一致,测试页逐字节一致;新高度规则、扶正裁切和矩形叠加均在接口52。
- 修复网关空图片字段改名影响旧接口的兼容性问题:仅接口52开启`*_base64: null`→`*_url: null`,接口2/3/5等旧接口保留原字段名;已增加递归改写和真实转发函数的回归测试。
- GPU worker所有动态接口启用直连IP白名单,本机和网关`49.232.51.171`可访问;网关侧不启用IP限制。配置与部署见[接口IP白名单](接口IP白名单.md)。
- `./venv/bin/pytest -q`:113项通过、1项已有失败。`tests/test_pipeline.py::test_numeric_regression`期望全脸总高24.01cm,当前实测22.47cm;从未修改的`a3fddbb`导出代码、夹具后独立运行,同样失败。此次保留历史断言,未通过修改基线掩盖差异。
- 浏览器实测:接口5返回middle/high/low且保持868×1166;接口52长图案例7返回1062×1416、`status=aspect_crop`,四栏图和裁切边框正常;对比页显示30个案例、378组结果,旧书签可跳转52,未出现页面脚本错误。定位验证均关闭ComfyUI生发,本轮未重复运行耗时的真实生发。
- 批量结果与上传图位于被Git忽略的`static/annotations/`及`tests/output/`,不随提交分发;新环境执行`scripts/compare_hairline_height.py`重建。420是计划组合数,378为当前成功结果,失败组及原因保留在报告中。
+29
View File
@@ -0,0 +1,29 @@
# GPU worker 接口 IP白名单
仅GPU worker使用项目根目录的`ip_access.json`,不存在时仅允许本机。网关不启用IP限制。配置每次请求重新读取,修改后立即生效。现有`X-Internal-Token`鉴权继续有效。
```json
{
"allowed_ips": ["49.232.51.171"]
}
```
当前已将网关IP `49.232.51.171`加入白名单;本机回环地址和网卡自身地址自动允许,其余来源返回HTTP403、`code=1010`。支持IPv4、IPv6及CIDR,不自动允许整个内网。
worker的所有动态路径统一保护,包括5号、52号、其他业务接口、调试接口、代理接口、健康检查和文档接口。`/static/`下的测试页和图片可正常访问,但页面发出的API请求仍需通过IP限制。
启动GPU worker时使用`uvicorn ... --no-proxy-headers`,保留真实连接来源。`run_worker.sh`与`hair-worker.service`已配置该选项。来源被服务器提前改写、地址格式错误或白名单配置无效时拒绝访问。
判断依据是直接连接worker的IP,忽略`X-Forwarded-For`、`X-Real-IP`和`Forwarded`,这些请求头不能用于绕过限制。由网关转发的请求按网关IP判断,网关后面的最终用户无需加入worker白名单。网关保持原有鉴权和转发流程,无需部署此配置。
`ip_access.json`不入Git;配置格式参考`ip_access.example.json`。本机网卡地址在请求时自动获取。
## 部署与维护
首次部署复制`ip_access.example.json`为`ip_access.json`,示例已包含网关`49.232.51.171`;已有配置不覆盖。该操作仅在GPU worker执行,网关服务和启动参数保持原样。
修改`allowed_ips`后保存为有效JSON即可生效,无需重启;建议写入同目录临时文件再原子替换,避免编辑过程中出现半份JSON。文件不存在时仅允许本机,内容损坏时动态接口一律拒绝。更新程序时保留本机配置文件。
IP检查先于Token检查。白名单内请求仍需原来的`X-Internal-Token`;`/health`和`/api/v1/debug/*`等原本免Token的路径仍需通过IP检查。静态页可打开不代表该来源可以调用API。
验证命令:`./venv/bin/pytest tests/test_ip_access.py -q`,覆盖本机、精确白名单、CIDR、IPv6、转发头伪造、热更新、全接口拦截与网关不受限制。
+156 -3
View File
@@ -20,11 +20,14 @@
| 3 B 端生发 | POST | `/api/v1/hair/grow-b` |
| 4 用户特征 | POST | `/api/v1/face/features` |
| 5 发际线 PNG 生成 | POST | `/api/v1/hairline/generate` |
| 52 发际线新规则与裁切 | POST | `/api/v1/hairline/generate-52` |
---
## 通用约定
GPU worker的所有动态接口均启用IP限制:允许本机及`ip_access.json`白名单(当前为网关IP `49.232.51.171`),其他来源返回HTTP403、code=1010;静态页面与图片除外。按直接连接worker的IP判断,网关不启用此限制,已有鉴权继续有效。
以下约定适用于全部 5 个接口(最终以联调为准)。
### 请求
@@ -35,7 +38,7 @@
| 请求方式 | `POST` |
| 图片传参方式 | 三选一,三种都支持:① `multipart/form-data` 上传文件、② 图片 URL、③ base64 字符串 |
| 字符编码 | UTF-8 |
| 坐标基准 | 所有返回的坐标均以**原图像素**为基准(原点为图片左上角,x 向右、y 向下) |
| 坐标基准 | 默认以**原图像素**为基准;接口52开启预处理时使用响应中的处理后底图坐标(原点为左上角,x向右、y向下) |
图片传参字段(三者**严格互斥**,必须且只能传其一;传了 0 个或 ≥2 个均报错):
@@ -81,12 +84,13 @@
| 1001 | 无法识别人像 | 图片中未检测到人脸 |
| 1002 | 人像分辨率过低 | 低于最低分辨率要求 |
| 1003 | 角度问题,非正面照 | 非正面 / 角度过大 |
| 1004 | gender 必填/非法 | 接口2/5 的 `gender` 缺失或非 `male`/`female` |
| 1004 | gender 必填/非法 | 接口2/5/52/7 的 `gender` 缺失或非 `male`/`female` |
| 1005 | 检测到多张人脸 | 默认仅支持单人,检测到 2 人或以上时返回 |
| 1006 | 文件超出大小限制 | 单文件超过 1 MB |
| 1007 | 图片参数错误 | file / url / base64 未传,或同时传了多个(三者严格互斥) |
| 1008 | 图片格式不支持 | 非 JPG / PNG |
| 1009 | 未授权 | 缺少或错误的 `X-Internal-Token`(`/api/*` 路径鉴权) |
| 1010 | 来源IP不允许 | 仅GPU worker:直连IP不在本机/白名单内,或来源/白名单配置无效;HTTP403 |
---
@@ -518,6 +522,154 @@
---
## 接口 52:发际线 PNG 生成接口
**说明**:入参同接口2(先选性别、再多选发型)。对每个选中发型返回一张 `middle` 设计叠图与生发图。**目标上庭 =(中庭 + 下庭)÷2**,从眉心向上定位;所有形状的中点对齐同一目标。`high` / `low` 已停用,旧字段保留为 `null`。
**请求**:`POST /api/v1/hairline/generate-52`
**IP限制**:GPU worker的所有动态接口统一仅允许本机回环地址、本机网卡地址及白名单;目前白名单含网关IP `49.232.51.171`,其他直连来源需加入`ip_access.json`的`allowed_ips`。该文件支持单个IP与CIDR,修改后立即生效;拒绝时返回HTTP403、code=1010。网关不启用此限制,已有鉴权仍保留。详见[接口IP白名单](接口IP白名单.md)。
### 图片扶正与标准构图
显式传`normalize_image=true`时,先扶正9→152,再按**高(3÷0.9)×L≈3.3333×L、宽2.5×L(3:4)**裁切,L为9→152的直线像素距离。相对原3L规则,裁切框宽高均扩大约11.11%,同样显示尺寸下人物缩小约10%。高度取最接近的4的倍数,宽度取高度的3/4。眉心定位沿用已扶正参考图:9号点相对位置约(49.0061%,34.4136%)。参考图仍为786×1048,两点X均为385.18784。
目标窗口完全在原图有效区域内才进行标准裁切。标准窗口不足时,偏长图片(宽/高<3/4)采用原图有效范围内最大的3:4窗口,尽量贴近眉心目标位置并保留完整人脸,`status=aspect_crop`;人脸放不进3:4窗口时回退仅扶正并返回原因。其余图片空间不足时仍仅扶正、去除旋转空角。不放大小图。**所有叠图与坐标使用响应中的处理后底图 `input_image_*`**,调用方应使用该底图叠加;`normalize_image=false` 可关闭此预处理。
`framing`包含处理前后尺寸、旋转角、是否标准裁切、跳过原因、`source_to_processed`/`processed_to_source`仿射矩阵。`status`区分`reference_crop`(标准窗口)、`aspect_crop`(长图3:4裁切)和`rotation_only`(仅扶正);`standard_crop=false`时仍可能有`aspect_crop=true`。`aspect_crop_skip_reason`说明长图无法保留人脸时的回退原因。`original_image_*`保留EXIF方向修正后的上传图,原图坐标可通过逆矩阵还原。
### 输入
图片参数见「通用约定 → 图片传参字段」。专属参数:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| gender | string | **是** | 性别:`male` / `female`。决定发型集合(female 7 / male 7)。缺失/非法返回 `1004` |
| 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` |
| use_mask | bool | 否 | 生发是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 |
| prompt | string | 否 | ComfyUI 提示词,默认「填充遮罩区域的头发,人物增加一点磨皮」,会替换工作流节点 60 的文本 |
| normalize_image | bool | 否 | 新增可选开关,默认false:保留原图尺寸与坐标。传true开启扶正和裁切;规则测试页显式传true,对比调试页默认开启 |
| generate_grow_image | bool | 否 | 是否生成生发效果图(ComfyUI 生发,全流程最耗时),默认 `true`。传 `false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回设计叠图、中心点与高度规则数据,可大幅降低耗时 |
> 生发划线与遮罩直接取新位置的 middle 曲线;旧规则的头发边界与模板用于保留形状和倾斜,不决定新中心高度。计算使用未取整的像素高度,与四庭测量采用同一 Y 方向;测量主流程失败时用发际线管线的同编号关键点计算,`measurement_source` 标明来源。目标越出图片或无法对齐时返回 1007,不静默回退旧规则。
### 高度规则与对比
- **参数与字段**:原有8个请求参数、响应外层结构和所有旧返回字段均保留。`use_mask=true`、默认prompt、`generate_grow_image=true`保持原值。新功能只新增可选参数和返回字段;旧请求未传`normalize_image`时保留原图坐标,high/low按已约定保留null,worker仍返回`*_base64`,网关仍返回对应`*_url`。
- `height_rule`:`middle_height_px`、`lower_height_px`、`target_upper_height_px`、`target_upper_height_cm`(可空)、`target_center`、`brow_center`、`nose_bottom`、`chin_tip`、`measurement_source`。
- 每项 `height_placement`:`old_center`、`new_center`、`shift_x_px`、`shift_y_px`(正=下移)、`target_error_px`。浮点坐标保留精度,旧中心点字段继续返回取整像素。
- `face_measure` 为处理后底图的实际测量值,未被设计目标覆盖。
- `guide_landmarks`:9、150、152、162、251、389 号 MediaPipe 点的图片像素坐标,供对比页绘制参考框。参考框左/右边分别经过162/389,底边经过9,高度为9→152距离的70%;两侧均固定平行9→152。
- 规则页:`/static/test_interface52.html`;旧/新规则对比:`/static/test_interface52_compare.html`。
- 规则页每个发型依次展示未裁切原图、middle、裁切发际线叠加、生发图。“裁切发际线叠加”由页面按`guide_landmarks`生成:在相同处理后底图上叠加矩形内的middle PNG,矩形外保持透明;左/右边过162/389,底边过9,高度0.7×9→152,左右边平行9→152。
- 对比调试接口:`POST /api/v1/debug/hairline-height-compare`,图片三选一、gender、hair_style;默认只出旧/新曲线,可选 `generate_grow_image=true` 额外验证新规则生发。
- 测试清单:`tests/fixtures/hairline_height_cases.json`(用户指定30张);批量运行:`./venv/bin/python scripts/compare_hairline_height.py`,默认全部30张、男女各7型,共420组。每人一行,未裁切原图放在最前,后接一张可切换样式的旧规则图和全部新规则样式。可用 `--cases 1,2,3,4,5` 指定部分案例。
### 输出(data)
| 字段 | 类型 | 说明 |
|------|------|------|
| hairline_images | object[] | **选中发型**列表,**数量 = 所选发型数**,元素见下表 |
| best_hairline_center_point | object \| null | **首个选中发型**的 **middle 档**发际线曲线「面部中间点」坐标,当前底图像素:`{ "x": number, "y": number }` |
| high_hairline_center_point | object \| null | 已停用,固定 `null` |
| low_hairline_center_point | object \| null | 已停用,固定 `null` |
| input_image_url | string | 当前底图,发际线叠图、参考点和测量值均使用该图的坐标系 |
| original_image_url | string | EXIF方向修正后的上传原图 |
| width / height | int | 当前底图的实际像素宽 / 高 |
| coordinate_space | string | 开启预处理时为 `processed_image`,关闭时为 `original_image` |
| framing | object \| null | 裁切状态、旋转角、前后尺寸、轴线误差及双向仿射矩阵;关闭预处理时为 `null` |
| height_rule | object | 中庭/下庭、目标上庭高度、目标中心及测量来源,详见“高度规则与对比” |
| guide_landmarks | object | 9、150、152、162、251、389 号点的当前底图像素坐标;参考框实际使用9、152、162、389 |
| face_measure | object \| null | **复用接口1**的四庭七眼测量**数值**(不含标注图)。独立流程,测量失败(无人脸/非正面/分割失败)时为 `null`,不影响发际线主结果。字段结构见下表 |
`hairline_images` 元素:
| 字段 | 类型 | 说明 |
|------|------|------|
| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`/`bigflower`/`clasicalflower`(female),`ellipse`/`m`/`straight`/`inverse_arc`/`heart`/`Softpetal`/`wave`(male) |
| image_middle_url | string | middle 档发际线曲线**透明 PNG** URL(仅曲线,透明底,**不含人物**,需叠加 `input_image_url` 显示) |
| image_high_url | null | 已停用,保留字段为 `null` |
| image_low_url | null | 已停用,保留字段为 `null` |
| grown_image_url | string \| null | **生发后图片** URL(ComfyUI「植发」效果图,完整人像照片,生发失败或 `generate_grow_image=false` 时为 `null`) |
| order | int | 发型序号(= 传入的 hair_style 值) |
| height_placement | object | 旧/新中心、XY移动距离、目标误差,均为未取整的像素值 |
> worker 侧返回 `image_middle_base64` / `image_high_base64` / `image_low_base64` / `grown_image_base64`,网关落盘后改写为上表对应的 `*_url`。
>
> 💡 `image_middle_url` 为透明底 PNG,前端需用绝对定位叠加到 `input_image_url` 上显示(参考[测试页](https://hair.xiangsilian.com/static/test_interface52.html)的 `.img-stack` 叠加结构)。`grown_image_url` 是完整人像照片,直接显示即可。worker返回上述图片的 `*_base64`,网关改写为 `*_url`。
`face_measure` 元素(与[接口1](#接口-1四庭七眼测量标注接口)的 `data` 同构,**不含** `annotated_image_*` 标注图字段):
| 字段 | 类型 | 说明 |
|------|------|------|
| face_total_height_cm | number | 全脸总高度(cm)= 四庭之和 |
| four_courts | object | 四庭数据(顶/上/中/下庭 cm + 占比 ratios),结构同接口1 |
| seven_eyes | object | 七眼数据(眼宽/脸宽/两眼间距 cm + 占比 ratios + eye1~eye7 从左到右 7 段宽度),结构同接口1 |
| landmarks | object | 5 个纵向关键点像素坐标(hair_top/hairline/brow_center/nose_bottom/chin_tip),结构同接口1 |
| hairline_source | string | 发际线来源:`segmentation`(真实分割)/ `estimated`(比例估算)/ `corrected`(发际线贴近头顶、分割不可靠,已按 0.95×中庭反推修正) |
| head_pose | object | 头部姿态角度(yaw/pitch/roll,单位:度) |
| left_position | object | MediaPipe 21 号关键点坐标(左脸定位点),当前底图像素:`{ "x": int, "y": int }` |
| right_position | object | MediaPipe 251 号关键点坐标(右脸定位点,与 21 号镜像),当前底图像素:`{ "x": int, "y": int }` |
> `eye1`~`eye7` 为从左到右共 7 段宽度,eye1=左耳外段、eye7=右耳外段,某侧耳朵不可见时对应段为 `null`。详见接口1说明。
### 网关响应结构示例(normalize_image=false)
此例未开启裁切,坐标基于原图;`face_measure=null`时仍可按发际线关键点计算目标。网关对接口52的所有空图片字段均返回`*_url: null`。worker直连时图片字段名对应为`*_base64`,图片内容为base64。
```json
{
"code": 0,
"message": "success",
"request_id": "mock-request-id",
"data": {
"hairline_images": [
{
"hairline_type": "ellipse",
"image_middle_url": "https://hair.xiangsilian.com/static/annotations/mid1.png",
"image_high_url": null,
"image_low_url": null,
"grown_image_url": "https://hair.xiangsilian.com/static/annotations/grown1.jpg",
"order": 1,
"height_placement": {
"old_center": {"x": 540.0, "y": 450.0},
"new_center": {"x": 540.0, "y": 430.0},
"shift_x_px": 0.0, "shift_y_px": -20.0, "target_error_px": 0.0
}
}
],
"best_hairline_center_point": {"x": 540, "y": 430},
"high_hairline_center_point": null,
"low_hairline_center_point": null,
"input_image_url": "https://hair.xiangsilian.com/static/annotations/input.jpg",
"original_image_url": "https://hair.xiangsilian.com/static/annotations/original.jpg",
"width": 1080, "height": 1440,
"coordinate_space": "original_image",
"framing": null,
"height_rule": {
"name": "court_average", "formula": "上庭 = (中庭 + 下庭) / 2",
"measurement_source": "hairline_landmarks",
"brow_center": {"x": 540.0, "y": 740.0},
"nose_bottom": {"x": 540.0, "y": 1050.0},
"chin_tip": {"x": 540.0, "y": 1360.0},
"middle_height_px": 310.0, "lower_height_px": 310.0,
"target_upper_height_px": 310.0, "target_upper_height_cm": null,
"target_center": {"x": 540.0, "y": 430.0}
},
"guide_landmarks": {
"9": {"x": 540.0, "y": 740.0}, "150": {"x": 470.0, "y": 1340.0},
"152": {"x": 540.0, "y": 1360.0}, "162": {"x": 300.0, "y": 680.0},
"251": {"x": 770.0, "y": 630.0}, "389": {"x": 780.0, "y": 680.0}
},
"face_measure": null
}
}
```
开启`normalize_image=true`后,`coordinate_space=processed_image`,`framing`包含前述裁切状态和坐标变换,所有发际线图片须叠加到`input_image_url`。原始图片仅用于展示和坐标回溯。
---
## 接口 9:头发遮罩生成接口
**说明**:输入一张含人头的照片,生成头发遮罩,并返回**每一步的可视化图**(供对比调试)。
@@ -702,6 +854,7 @@
| 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 三档发际线叠图 + 生发图 + 最合适发际线面部中间点坐标 |
| 52 发际线新规则 | `/api/v1/hairline/generate-52` | 用户照片 + gender + hair_style(多选) | 每个选中发型的新规则 middle 设计叠图 + 生发图 + 目标中心坐标;high/low 旧字段为 null |
| 9 头发遮罩 | `/api/v1/head/mask` | 用户照片 | 头发遮罩 + 分步可视化(BiSeNet/SegFormer 双路) |
| 10 外缘膨胀带 | `/api/v1/head/band` | 用户照片 | 头部外缘带状遮罩 + 分步可视化 |
| 11 发际线生发 | `/api/v1/hairline/grow` | 用户照片 + hairline_id | 压低发际线图 + 全步骤可视化(不含重绘) |
@@ -716,7 +869,7 @@
- 图片传参:**文件 / URL / base64 三种都支持,严格互斥**(必须且只能传一个,base64 需带 `data:image/...;base64,` 前缀)。
- 默认单人;检测到 2~3 人报错提示客户端;单文件 **≤ 1 MB**;格式仅 **JPG / PNG**。
- 所有坐标基准:**原图像素**。
- 坐标基准:默认**原图像素**;接口52开启预处理时使用响应中的**处理后底图像素**。
- 接口 2:提供「美颜」开关 `beauty_enabled`。
- 接口 3:仅传**划线图**(`marked_image_*` 三选一),需返回发际线类型(`custom`)。
- 接口 4:`data` 返回**一个 JSON 字符串**,字段不固定,可随时调整。