From 63bd2653862ba90e291b08cc1bf185bce592bf22 Mon Sep 17 00:00:00 2001 From: xsl Date: Sun, 6 Sep 2026 22:57:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8E=A5=E5=8F=A399):=20=E4=BA=BA?= =?UTF-8?q?=E8=84=B8=E7=89=B9=E6=AE=8A=E7=82=B9=E6=A3=80=E6=B5=8B=20+=20?= =?UTF-8?q?=E7=BD=91=E5=85=B3=E8=BD=AC=E5=8F=91=E8=B7=AF=E7=94=B1=EF=BC=9B?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=90=8C=E6=AD=A5=E8=87=B3=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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部署步骤) --- CLAUDE.md | 124 ++++++++------- app.py | 64 ++++++++ docs/实现说明.md | 85 ++++++---- docs/接口文档.md | 210 +++++++++++++++++++++++-- docs/网关待改动.md | 78 +++++++++- face_analysis/special_points.py | 166 ++++++++++++++++++++ gateway/app.py | 6 + static/test_interface99.html | 266 ++++++++++++++++++++++++++++++++ 8 files changed, 906 insertions(+), 93 deletions(-) create mode 100644 face_analysis/special_points.py create mode 100644 static/test_interface99.html diff --git a/CLAUDE.md b/CLAUDE.md index e02f00e..9c4a61d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,80 +4,100 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Overview -"旷视五接口" — a FastAPI facial analysis API service providing 5 endpoints for face measurement, hair growth simulation (C-end and B-end), user feature extraction, and hairline PNG generation. Currently a **Mock v1**: all endpoints return hardcoded sample data; real ML logic is pending. +旷视接口服务 — FastAPI 人像分析/生发服务,**一个仓库、两种部署角色**: -- **Base URL (prod)**: `https://hair.xiangsilian.com` -- **Swagger docs**: `https://hair.xiangsilian.com/docs` -- **Internal**: uvicorn on `127.0.0.1:8000` behind nginx reverse proxy +- **worker**(`app.py` + `face_analysis/` + `hairline/`):跑真实算法(本地模型 + ComfyUI + change_hair),监听 `0.0.0.0:8187`。**本机(GPU 机)即 worker 角色**,不跑网关。 +- **网关**(`gateway/`):外网机器上的薄反向代理,负责鉴权/健康检查/派发,并把 worker 返回的 `*_base64` 落盘改写为 `*_url`。接口4(豆包)在网关本机实现,不转发 worker。 + +> 历史包袱:`app.py` 的 FastAPI `description` 和 `docs/接口文档.md` 部分示例仍写「Mock 第一版」字样,实际全部接口已是真实算法实现。**架构以 `docs/实现说明.md` 为准,契约以 `docs/接口文档.md` 为准。** ## Commands ```bash -# Development server -./venv/bin/uvicorn app:app --host 127.0.0.1 --port 8000 --reload +# ---- worker(本机)---- +./run_worker.sh # 后台启动(0.0.0.0:8187,日志 worker.log) +FOREGROUND=1 ./run_worker.sh # 前台调试 +./stop_worker.sh # 停止 +sudo systemctl restart hair-worker # systemd 方式(生产) +journalctl -u hair-worker -f -# Or via the start script -./start.sh +# ---- 依赖服务(worker 之前要先起)---- +sudo systemctl start comfyui # ComfyUI 127.0.0.1:8188(Flux-2 生图) +cd /home/ubuntu/change_hair && bash start_all.sh start # 换发型三件套 8801/32678/57860 -# Production (systemd) -sudo systemctl restart hair -sudo systemctl status hair -journalctl -u hair -f - -# Install dependencies (uses Tencent Cloud PyPI mirror) -./venv/bin/pip install -r requirements.txt +# ---- 测试 ---- +./venv/bin/pytest tests/ # 单元测试(conftest 会准备 fixtures) +./venv/bin/python -m hairline.comfyui tests/output/comfy_input.png # 手动跑一次生发工作流 ``` +依赖装在 `./venv`(Python 3.10,torch 2.7.1+cu128,RTX 4090 CUDA 可用)。`requirements.txt` 里的 torch 2.2.2/5090 注释是旧机器的记录,本机 venv 实际是 cu128 版。 + ## Architecture -**Single-file app** — `app.py` (~555 lines) contains the entire server: FastAPI app creation, Pydantic models, all 5 endpoint handlers, and health check routes. There are no separate routers, services, or middleware modules. +### 接口一览(worker 侧路由,`app.py`) -### Endpoints +| 接口 | 路径 | 实现位置 | 说明 | +|------|------|----------|------| +| 1 | `POST /api/v1/face/measure` | `face_analysis/` | 四庭七眼测量 + 标注 PNG | +| 6 | `POST /api/v1/face/measure-v2` | 同上(`variant="v6"`) | 接口1 去顶庭变体 | +| 2 | `POST /api/v1/hair/grow` | `hairline/service.py` | C端生发。**female 1..5** 走 change_hair swapHair + Flux-2 整帧重绘(接口12 final 管线);**female 6/7 与 male 全部**走 ComfyUI `add_hair.json` inpaint | +| 7 | `POST /api/v1/hair/grow-v2`(hidden/deprecated) | 同上 | 同接口2,用 `add_hair2.json` 工作流 | +| 3 | `POST /api/v1/hair/grow-b` | `hairline/marker_detect.py` | B端马克笔划线生发(黑帽响应 + Dijkstra) | +| 4 | `POST /api/v1/face/features` | worker 有实现;生产走**网关本机**(豆包) | 用户特征 | +| 5 | `POST /api/v1/hairline/generate` | `hairline/service.py` | 发际线 PNG 三档(middle/high/low)+ 生发 | +| 9 | `POST /api/v1/head/mask` | `face_analysis/head_mask.py` | 头发遮罩 + 分步可视化(BiSeNet/SegFormer 双路) | +| 10 | `POST /api/v1/head/band` | `face_analysis/head_band.py` | 头部外缘膨胀带遮罩 | +| 11 | `POST /api/v1/hairline/grow` | `face_analysis/hairline_grow.py` | 发际线生发(接口9 遮罩 + change_hair + 接缝融合),不含重绘 | +| 12 | `POST /api/v1/hairline/grow_v2` | 同上 | 接口11 final + 发际线重绘带 → Flux-2 保色重绘 | +| 12f | `POST /api/v1/hairline/grow_v2_final` | 同上(`_V2_FINAL_DEFAULTS`) | 精简版:仅图片 + hairline_id,取整帧重绘 | +| 12f2 | `POST /api/v1/hairline/grow_v2_final_v2` | 同上 | 同上,取 B 局部加发+全脸美颜 | +| 99 | `POST /api/v1/face/special-points` | `face_analysis/special_points.py` | 固定编号关键点 + 发际线特殊点(射线求交头发 mask)像素坐标 | +| 代理 | `POST /api/swapHair/v1`、`/hairColor/v2` | httpx 转发 127.0.0.1:8801 | 解决测试页 CORS | +| 工具 | `POST /api/v1/redraw` | `hairline/redraw.py` | 直接 ComfyUI 重绘(图+遮罩+prompt) | +| 调试 | `POST /api/v1/debug/grow-timing`、`GET /api/v1/debug/hairline_log` | — | 免鉴权,分步计时/下载日志 | -| Method | Path | Purpose | -|--------|------|---------| -| POST | `/api/v1/face/measure` | 四庭七眼 measurement + annotated PNG | -| POST | `/api/v1/hair/grow` | C-end hair growth (user-facing) | -| POST | `/api/v1/hair/grow-b` | B-end hair growth (doctor/operator, with marked image) | -| POST | `/api/v1/face/features` | User facial feature analysis | -| POST | `/api/v1/hairline/generate` | Hairline PNG generation | -| GET | `/health` | Health check (excluded from OpenAPI schema) | -| GET | `/` | Service info (excluded from OpenAPI schema) | +配套测试页在 `static/test_interface{1..12}*.html`,经 `/static` 挂载。 -### Image input convention (all endpoints) +### 关键模式 -Every endpoint accepts images via **exactly one** of three mutually exclusive fields: -- `image_file` — multipart file upload (≤ 1 MB) -- `image_url` — HTTP/HTTPS URL string -- `image_base64` — base64 string with `data:image/...;base64,` prefix +- **统一信封**:`ok(data)` / `err(code, message)` 构造 `{code, message, request_id, data}`;业务错误 HTTP 一律 200。错误码:1001 无人像 / 1002 分辨率低 / 1003 非正面 / 1004 gender 非法 / 1005 多人 / 1006 >1MB / 1007 参数或处理失败 / 1008 格式不支持。 +- **图片三选一**:每个接口 `image_file`(multipart) / `image_url` / `image_base64`(带 `data:` 前缀)严格互斥,由 `resolve_image_bytes()` 统一处理(0 个或多个 → 1007)。接口3 是 `marked_image_*`。 +- **阻塞算法进线程池**:所有推理 handler 用 `run_in_threadpool(...)`,避免卡事件循环。 +- **内网鉴权**:HTTP 中间件校验 `/api/*` 的 `X-Internal-Token` 头,密码来自 `worker_config.json` 的 `accept_passwords`(或环境变量 `WORKER_ACCEPT_PASSWORDS`)。豁免前缀见 `_AUTH_EXEMPT`(health/docs/static/debug/redraw/swapHair/hairColor)。 +- **发型序号映射**:`hairline/service.py` 的 `_HAIRSTYLE_ORDER` 显式定义 1..7 顺序(不靠文件名字母序);female 1..5 → change_hair `chang_*` 映射在 `_FEMALE_KEY_TO_CHANG`。 +- **接口12 固化参数**:`_V2_FINAL_DEFAULTS`(app.py)是生产默认值(push=0.8cm / two_stage / color_match=关 等),接口2 female 重绘也复用它。 -Endpoint 3 (B-end) has **two** sets of image params: `marked_image_*` (the marked-up image) and `original_image_*` (the original photo). Each set follows the same three-way exclusive rule independently. +### 外部依赖(worker 视角) -### Unified response structure +| 依赖 | 地址 | 用途 | 配置 | +|------|------|------|------| +| ComfyUI | `http://127.0.0.1:8188`(`COMFYUI_URL`) | Flux-2 生图/重绘,走 HTTP API(`hairline/comfyui.py`) | Basic Auth `admin`,密码:`COMFYUI_PASSWORD` → `worker_config.json.comfyui_password` → `password.txt` | +| change_hair | `http://127.0.0.1:8801`(`SWAP_HAIR_URL`/`HAIR_GROW_URL`/`CHANGE_HAIR_BASE`) | 换发型(SD WebUI + LoRA)、hairGrow | 服务目录 `/home/ubuntu/change_hair`,`start_all.sh` 启停 | -```json -{"code": 0, "message": "success", "request_id": "mock-request-id", "data": {...}} -``` +ComfyUI 工作流 JSON 在仓库根:`add_hair.json`(接口2/3/5 生发)、`add_hair2.json`(接口7)、`0716add-hair-api.json`(接口12/2女 重绘)、`hair_repaint.json`。节点约定:**26** 输入图 / **6** seed / **60** 提示词 / **16** unet(.gguf 自动切 UnetLoaderGGUF)/ **61** CLIP(4b↔9b 自动配套)。 -Error codes: 1001–1008 (see `app.py` docstring or `docs/接口文档.md`). +### 性能/显存相关的坑 -### Key code patterns +- 三个生发接口共用**同一提示词字符串**以命中 ComfyUI 的 CLIP 文本条件缓存——prompt 变了会触发 Flux(7.7G)+qwen CLIP(3.9G) 互相挤显存重载(每次 +4s)。 +- 接口2 女性重绘管线默认把输入长边压到 `REDRAW_MAX_SIDE`(env,默认 1024)再放大回原尺寸;`redraw_max_side=0` 表示不缩图。 +- `comfyui.run(front=True)` 可插队(接口2 对时延敏感);超时 `COMFY_TIMEOUT`(默认 600s)。 -- `ok(data)` / `err(code, msg)` — helper functions at module level that build the standard response envelope -- `ImageJsonBody` — Pydantic model for JSON-based image params (URL/base64); form-upload endpoints use FastAPI `Form()` / `File()` parameters directly -- `SAMPLE_IMAGE_URL` — `https://hair.xiangsilian.com/static/sample.jpg`, returned as the image URL in all mock responses -- Static files (`/static/*`) are mounted via `app.mount("/static", StaticFiles(directory="static"), name="static")` -- All 5 POST endpoints use `multipart/form-data` with both `File` and `Form` fields — there are no JSON-body POST endpoints despite `ImageJsonBody` being defined (it exists for the OpenAPI schema but the actual handlers use `Form()` for all params) +## Deployment(本机 = gpu_worker) -### Deployment +systemd 单元(仓库根有副本,实装在 `/etc/systemd/system/`): -- **Process manager**: systemd (`hair.service` → `ExecStart` runs uvicorn directly from venv) -- **Reverse proxy**: nginx (`nginx/hair.conf`) proxies port 80 → `127.0.0.1:8000` for `hair.xiangsilian.com` -- **Python**: venv at `./venv`, dependencies in `requirements.txt` -- **PyPI mirror**: Tencent Cloud (`mirrors.cloud.tencent.com`), configured in `pip.conf` +| 服务 | 端口 | 说明 | +|------|------|------| +| `comfyui.service` | 127.0.0.1:8188 | `ComfyUI/venv` 的 `main.py --cache-classic --fast` | +| `hair-worker.service` | 0.0.0.0:8187 | 本仓库 `app:app`(Wants comfyui + change_hair-hair) | +| `change_hair-{webui,photo,hair}.service` | 57860 / 32678 / 8801 | conda envs `sdwebui`/`py310`/`my_hair`,依赖顺序 webui→photo→hair | -### Documentation +网关机另行部署 `gateway/`(config:`workers` 列表 + `shared_password` + `ark` key),见 `gateway/config.example.json` 与 `docs/网关待改动.md`。 -- `docs/接口文档.md` — complete API documentation (inputs, outputs, examples, error codes) -- `docs/旷视具体需求.md` — original requirements spec (converted from `.docx`) -- The FastAPI app's `description` docstring in `app.py` is an additional source of API docs visible in Swagger UI +## Documentation + +- `docs/接口文档.md` — 对外 API 契约(唯一权威) +- `docs/实现说明.md` — 架构总览 + 各接口实现简述 + 部署要点 +- `docs/发际线增强算法.md`、`docs/发际线生发遮罩算法_pushed模式.md` — 接口11/12 算法推导 +- `docs/gpu-worker改动说明.md`、`docs/网关待改动.md` — worker↔网关 协同变更记录 +- `OFFLINE_ASSETS.md` — 离线模型权重/字体清单 diff --git a/app.py b/app.py index 6214427..5f381ea 100644 --- a/app.py +++ b/app.py @@ -1370,6 +1370,70 @@ async def head_band( return err(1007, f"处理失败:{ex}") +# --------------------------------------------------------------------------- +# 接口 99:人脸特殊点检测(固定关键点 + 发际线特殊点) +# --------------------------------------------------------------------------- + +@app.post( + "/api/v1/face/special-points", + summary="接口99 人脸特殊点检测", + tags=["人脸分析"], + description=f""" +输入一张人脸照片,返回 MediaPipe Face Mesh 一组**固定编号特殊点**的像素坐标, +以及 5 个**发际线特殊点**(射线与 BiSeNet 头发分割发际线的交点,见 hairline_grow 同款分割)。 + +**字段命名**:`<组英文名>_<点ID>`,如 `eyebrow_107`、`mouth_17`;发际线点用给定字段名 +(`hair_line_center` 等)。坐标为原图像素坐标(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 沿 a→b 方向逐像素步进,首次进入头发 mask 处即交点; +双射线的点取两交点中点(仅一条命中返回该点,均未命中为 null): +- `hair_line_center`:151→10 +- `hair_line_right`:mid(108→109, 69→67) +- `hair_line_right_right`:mid(104→103, 68→54) +- `hair_line_left`:mid(337→338, 299→297) +- `hair_line_left_left`:mid(333→332, 298→284) + +{_image_fields_desc} +""", +) +async def face_special_points( + image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG)"), + image_url: Optional[str] = Form(default=None, description="图片 URL"), + image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"), +): + """接口99:固定关键点 + 发际线特殊点,像素坐标 JSON 返回。""" + raw, e = await resolve_image_bytes(image_file, image_url, image_base64) + if e is not None: + return e + + image = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR) + if image is None: + return err(1008, "图片格式不支持(仅 JPG / PNG)") + + try: + from fastapi.concurrency import run_in_threadpool + from face_analysis.special_points import detect_special_points, NoFaceError, NonFrontalError + try: + data = await run_in_threadpool(detect_special_points, image) + except NoFaceError: + return err(1001, "无法识别人像") + except NonFrontalError: + return err(1003, "角度问题,请上传正面照") + return ok(data) + except Exception as ex: # noqa: BLE001 + logger.exception("接口99 处理异常") + return err(1007, f"处理失败:{ex}") + + # --------------------------------------------------------------------------- # 接口 11:发际线生发(接口9 遮罩 + change_hair 换发型 + 按遮罩羽化贴回) # --------------------------------------------------------------------------- diff --git a/docs/实现说明.md b/docs/实现说明.md index 3df782e..0f130b9 100644 --- a/docs/实现说明.md +++ b/docs/实现说明.md @@ -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 组预览+生发图)。 @@ -85,37 +90,63 @@ ### 接口4 用户特征 `/api/v1/face/features`(**网关本机**) - **做什么**:照片 → 几十项面部特征(脸型/眉形/肤色/三庭五眼/四季色彩季型/量感/基因风格/性别…)。`data.features` 是 JSON 字符串。 - **怎么实现**(`gateway/`,逻辑参考 worker `face_features.py` / `/home/xsl/fuyan`):调**火山方舟 豆包视觉模型** - `doubao-seed-2-0-lite-260428`(OpenAI 兼容,base64 data URI 喂图),解析眉形/年龄/动静/性别/基因风格 5 项。 - **`face_shape`(脸型)改为本机 `face/face_shape_classifier.py`(MediaPipe)计算并覆盖豆包结果**。 - 无人脸→1001。网关需可达 `ark.cn-beijing.volces.com`,API Key 走网关配置(不入 git)。 - ⚠️ 因此**网关机不再是纯轻量代理**,需额外安装 `mediapipe`/`opencv-python`/`numpy<2`(见 `requirements.txt`)。 + `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)取中点。无图片字段,网关纯盲转发。 +- 测试页:`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 兼容)。 -- ⚠️ 接口4 `face_shape` 改本机 MediaPipe 计算后,网关机**也需要装** `mediapipe`/`opencv-python`/`numpy<2` - (不再是"网关不装 torch/mediapipe/opencv",只是仍不需要 torch/transformers/scikit-image 等重依赖)。 -- 配置 `gateway/config.json`(不入 git):`workers` 列表、`shared_password`、 +- 很轻: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/`(落盘的图),永久保留,不自动清理。 +- 托管 `/static/annotations/`(落盘的图),定期清理。 --- diff --git a/docs/接口文档.md b/docs/接口文档.md index 72be069..f6c34da 100644 --- a/docs/接口文档.md +++ b/docs/接口文档.md @@ -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`。 > @@ -352,7 +355,7 @@ ## 接口 4:用户特征接口 -**说明**:输入用户照片,由**火山方舟 豆包视觉模型**(`doubao-seed-2-0-lite-260428`)分析,输出一大批面部特征。 +**说明**:输入用户照片,由**火山方舟 豆包视觉模型**(`doubao-seed-1-6-vision`)分析,输出一大批面部特征。 **请求**:`POST /api/v1/face/features` @@ -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 不固定**,需按合适度**排序**。 - 英文字段命名由本文档约定(见各接口表格)。 diff --git a/docs/网关待改动.md b/docs/网关待改动.md index 9136956..47eb0ad 100644 --- a/docs/网关待改动.md +++ b/docs/网关待改动.md @@ -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`。 diff --git a/face_analysis/special_points.py b/face_analysis/special_points.py new file mode 100644 index 0000000..ee14005 --- /dev/null +++ b/face_analysis/special_points.py @@ -0,0 +1,166 @@ +"""接口99:人脸特殊点检测。 + +返回 MediaPipe Face Mesh 关键点中一组固定编号的像素坐标(英文组名_点ID 为字段名), +外加 5 个发际线特殊点:从指定关键点沿指定方向射线步进,与 BiSeNet 头发分割 mask +首次相交处即发际线点(参考接口1 方案B 的发际线定位方式,见 hair_segmenter.py)。 + +组与编号(需求方给定): +- 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 两射线时取中点,仅一条命中则返回该点,均未命中为 null): +- 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) # 与 right_right 镜像对称 +""" +import math + +import numpy as np + +# 固定关键点组:组英文名 → MediaPipe 点 ID 列表(顺序即需求方给定顺序) +GROUPS = { + "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], +} + +# 发际线点定义:字段名 → 参与计算的 (起点ID, 方向点ID) 射线组;单射线直接取交点, +# 双射线取两交点中点。 +HAIRLINE_RAYS = { + "hair_line_center": [(151, 10)], + "hair_line_right": [(108, 109), (69, 67)], + "hair_line_right_right": [(104, 103), (68, 54)], + "hair_line_left": [(337, 338), (299, 297)], + "hair_line_left_left": [(333, 332), (298, 284)], +} + + +class NoFaceError(Exception): + """未检出人脸。""" + + +class NonFrontalError(Exception): + """非正面照(复用接口1 姿态阈值)。""" + + +def ray_hairline_intersection(p1, p2, hair_mask): + """从 p1 沿 p1→p2 方向逐像素步进,返回首次进入头发 mask 的整数像素坐标。 + + p1/p2: (x, y) 像素坐标(float)。起点本身已在头发内时直接返回起点(已到发际线)。 + 步进直到出图或步数上限(3×图对角线,防极端死循环);未命中返回 None。 + """ + mask = np.asarray(hair_mask).astype(bool) + if mask.size == 0: + return None + h, w = mask.shape + x0, y0 = float(p1[0]), float(p1[1]) + dx, dy = float(p2[0]) - x0, float(p2[1]) - y0 + length = math.hypot(dx, dy) + if length < 1e-6: + return None + ux, uy = dx / length, dy / length + max_steps = int(3 * math.hypot(h, w)) + + def _hit(x, y): + xi, yi = int(round(x)), int(round(y)) + if 0 <= xi < w and 0 <= yi < h: + return mask[yi, xi] + return False + + x, y = x0, y0 + for _ in range(max_steps): + if _hit(x, y): + return int(round(x)), int(round(y)) + x += ux + y += uy + if not (0 <= int(round(x)) < w and 0 <= int(round(y)) < h): + return None # 已步出图像,方向上无头发 + return None + + +def detect_special_points(image_bgr): + """主入口:图片 → {字段名: {"x","y"} 或 None} + 元信息。 + + Returns: {"points": {...}, "image_size": {"width","height"}, + "hairline_source": "segmentation"|"unavailable"} + 无人脸抛 NoFaceError;非正面抛 NonFrontalError。 + """ + from face_analysis.detector import detector + from face_analysis.calibration import normalized_to_pixel + from face_analysis.pose import check_frontal_face + + h, w = image_bgr.shape[:2] + landmarks = detector.detect(image_bgr) + if landmarks is None: + raise NoFaceError("未检出人脸") + if not check_frontal_face(landmarks, w, h): + raise NonFrontalError("非正面照") + + lm = landmarks.landmark + px = {i: normalized_to_pixel(lm[i], w, h) for i in range(len(lm))} + + points = {} + for group, ids in GROUPS.items(): + for idx in ids: + x, y = px[idx] + points[f"{group}_{idx}"] = {"x": int(round(x)), "y": int(round(y))} + + # 头发分割(与接口1 相同:按人脸框裁剪后再分割,脸外背景不干扰) + hair_mask = None + try: + from face_analysis.hair_segmenter import get_segmenter + face_box = (min(p[0] for p in px.values()), min(p[1] for p in px.values()), + max(p[0] for p in px.values()), max(p[1] for p in px.values())) + hair_mask, _ = get_segmenter().segment_hair_and_ears( + image_bgr, face_box=face_box) + except Exception: # noqa: BLE001 分割不可用 → 发际线点置 null,不阻塞关键点 + hair_mask = None + + for name, rays in HAIRLINE_RAYS.items(): + hits = [] + if hair_mask is not None: + for a, b in rays: + hits.append(ray_hairline_intersection(px[a], px[b], hair_mask)) + found = [p for p in hits if p is not None] + if not found: + points[name] = None + elif len(found) == 1: + points[name] = {"x": found[0][0], "y": found[0][1]} + else: + points[name] = { + "x": int(round(sum(p[0] for p in found) / len(found))), + "y": int(round(sum(p[1] for p in found) / len(found))), + } + + return { + "points": points, + "image_size": {"width": w, "height": h}, + "hairline_source": "segmentation" if hair_mask is not None else "unavailable", + } + + +if __name__ == "__main__": + import sys + import cv2 + import json + + path = sys.argv[1] if len(sys.argv) > 1 else "tests/fixtures/frontal.jpg" + img = cv2.imread(path) + if img is None: + print(f"无法读取图片: {path}") + sys.exit(1) + try: + out = detect_special_points(img) + except (NoFaceError, NonFrontalError) as e: + print(f"检测失败: {e}") + sys.exit(1) + print(json.dumps(out, ensure_ascii=False, indent=2)) diff --git a/gateway/app.py b/gateway/app.py index dc3fb13..a974996 100644 --- a/gateway/app.py +++ b/gateway/app.py @@ -586,3 +586,9 @@ async def head_mask(request: Request): async def head_band(request: Request): """接口10:头部外缘膨胀带遮罩 + 分步可视化""" return await _proxy(request, "/api/v1/head/band") + + +@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") diff --git a/static/test_interface99.html b/static/test_interface99.html new file mode 100644 index 0000000..80481d1 --- /dev/null +++ b/static/test_interface99.html @@ -0,0 +1,266 @@ + + + + + +接口99 — 人脸特殊点检测 测试页 + + + +
+

接口99 — 人脸特殊点检测

+
POST /api/v1/face/special-points · 固定编号 MediaPipe 关键点 + 5 个发际线特殊点(射线与头发分割 mask 交点),返回原图像素坐标
+ +
+
+ + + +
+
支持 JPG / PNG;单人正面照效果最佳。发际线点依赖头发分割,秃顶/遮挡时对应字段为 null。
+
+
+ + +
+ + + + + +