# 旷视五接口 — 实现说明(总) > 把原先分散的「各接口技术方案 + 开发任务书 + 系统架构 + 网关任务书」合并成这一份**简要总览**。 > 对外 API 契约以 [`接口文档.md`](接口文档.md) 为唯一权威;原始需求见 [`旷视具体需求.md`](旷视具体需求.md); > 离线模型清单见 [`../OFFLINE_ASSETS.md`](../OFFLINE_ASSETS.md)。 --- ## 1. 架构 两台机器、一个仓库: ``` 客户端 ──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 + 工具路由 ``` - **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`。 网关本机实现的接口(不转发 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)。 ### base64 → URL 映射(网关落盘改写,递归进数组、可空保留 null) | 接口 | worker 字段(内部) | 对外字段 | |------|--------------------|----------| | 1 | `annotated_image_base64` | `annotated_image_url` | | 6 | `annotated_image_base64` | `annotated_image_url` | | 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` | | 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`,自动覆盖嵌套/新增字段。 > **图片格式**:接口1 标注图含透明用 **PNG**;接口2/3/5 是不透明照片用 **JPG**(小很多,~9×)。 > 网关落盘按内容嗅探扩展名(PNG 头→`.png`,否则 `.jpg`)。 ### 错误码 `1001` 无法识别人像 | `1002` 分辨率过低 | `1003` 非正面 | `1004` gender 必填/非法(接口2/5/7)| `1006` >1MB | `1007` 图片参数错误(0或多个)/未预期异常 | `1008` 格式不支持。 --- ## 2. 各接口实现简述 ### 接口1 四庭七眼测量 `/api/v1/face/measure`(worker) - **做什么**:正面照 → 四庭(顶/上/中/下庭) + 七眼(眼宽/脸宽/间距) 的 cm 与占比、5 个关键点坐标、一张透明底标注 PNG。 - **怎么实现**(`face_analysis/`):MediaPipe Face Mesh 468+虹膜点 → solvePnP 姿态校验(非正面 1003) → 虹膜直径法定标(px→cm) → **眉心以下实测**;**眉心以上**用 BiSeNet 头发分割取真实发际线/头顶(方案B), 失败回退比例推算(方案A,`hairline_source` 透出)。标注图 numpy 向量化渐变线 + 思源黑体。返回 `annotated_image_base64`。 - 门槛可配:`MIN_SHORT_SIDE`/`MIN_LONG_SIDE`(默认600/800)、姿态阈值 `FRONTAL_*_THR`(默认30°)。 ### 接口6 四庭七眼测量 v2 `/api/v1/face/measure-v2`(worker)—— 接口1 的去顶庭变体 - **做什么**:基于接口 1,**去顶庭**:不画头顶横线、不返回顶庭数据(`four_courts` 仅上/中/下庭,`landmarks` 无 `hair_top`,`face_total_height_cm` 为三庭之和)。 - **与接口1 的标注差异**(`create_annotated_image(variant="v6")`):①竖线范围改为发际线→下巴尖;②不画人头最左/最右端线(仅七眼 6 点 5 段,接口1 为 8 线 7 段);③左侧只标上/中/下庭。箭头/虚线/字体等与接口1 一致。 - **怎么实现**:与接口 1 共用 `_face_measure_impl(variant="v6")`;v6 时标注走变体分支、数据由 app.py 边界删顶庭字段并重算三庭比例。 - **网关改动**:新增路由 `POST /api/v1/face/measure-v2`,转发到 worker 同路径;base64→URL 改写无需改动。 ### 接口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 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 重绘管线(`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 组预览+生发图)。 - **与接口 2 的唯一区别**:ComfyUI 工作流使用 `add_hair2.json`(Flux-2 Klein 9b),输入/遮罩节点同为 26, SaveImage 输出节点为 75(自动检测)。其他参数、响应结构、错误码**完全相同**。 - **网关改动**:在 `gateway/app.py` 新增路由 `POST /api/v1/hair/grow-v2`,转发到 worker 同路径即可(盲转发, base64→URL 改写逻辑无需改动,数组内图片字段已覆盖)。详见 [`网关待改动.md`](网关待改动.md)。 ### 接口3 B端生发 `/api/v1/hair/grow-b`(worker)—— 马克笔发际线 - **做什么**:医生用马克笔在额头画好发际线,**只传这一张划线图** → 检测线 → 生发图。输出 `hair_growth_image_url` + `hairline_type="custom"`。 - **怎么实现**:检测算法源自 headmark——**黑帽响应图 + 鬓角锚点(MediaPipe 21/251) + Dijkstra 最小路径**(scikit-image), 比全局阈值鲁棒;路径平均响应过低→拒识(1001)。检测路径建遮罩,划线图原样送 ComfyUI(提示词清除黑线)。 ### 接口4 用户特征 `/api/v1/face/features`(**网关本机**) - **做什么**:照片 → 几十项面部特征(脸型/眉形/肤色/三庭五眼/四季色彩季型/量感/基因风格/性别…)。`data.features` 是 JSON 字符串。 - **怎么实现**(`gateway/`,逻辑参考 worker `face_features.py` / `/home/xsl/fuyan`):调**火山方舟 豆包视觉模型** `doubao-seed-1-6-vision`(OpenAI 兼容,base64 data URI 喂图),解析 JSON + 映射 6 个英文优先字段并保留全部中文。 无人脸→1001。**唯一调外网的接口**:网关需可达 `ark.cn-beijing.volces.com`,API Key 走网关配置(不入 git)。 ### 接口5 发际线PNG生成 `/api/v1/hairline/generate`(worker) - **做什么**:入参同接口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)取中点。**纯 CPU 可跑**(BiSeNet 单核 <1s),网关直接 import 本机实现(`_if99_lock` 单路排队);worker 侧保留同路径接口供测试页直连。部署依赖清单见 `网关待改动.md` §9。 - 测试页:`static/test_interface99.html`(canvas 可视化 + JSON,worker 本机直连)。 --- ## 3. 部署 / 环境要点 **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,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 兼容)。 - 不装 torch/mediapipe/opencv。配置 `gateway/config.json`(不入 git):`workers` 列表、`shared_password`、 `ark` 的 api_key/base_url/model、`public_base_url`、超时(**生发接口慢,`request_timeout_seconds` 调大 ≥120s**)。 - 托管 `/static/annotations/`(落盘的图),定期清理。 --- > 维护:本文为简要总览;字段以 `接口文档.md` 为准。各接口更细的算法推导可查 git 历史中已合并的旧技术方案文档。