接口11:发际线生发(接口9遮罩 + change_hair换发型/区域生发 + 按遮罩羽化贴回)+ 分步可视化测试页

- 端点 POST /api/v1/hairline/grow(app.py,纯新增,不影响接口1-5)
- 编排模块 face_analysis/hairline_grow.py:复用接口9 遮罩 → HTTP 调 change_hair(8801) → 按遮罩贴回
- 双生成后端 gen_backend:swaphair(换发型LoRA) / hairgrow(区域生发inpaint)
- swap_mode:ext_mask(接口9遮罩作换发型遮罩) / as_is;融合 feather/alpha_gradient/seamless
- 参数全在测试页可调;可视化按算法文档4步:最终遮罩→生成全帧→严格贴回→接缝融合
- 附启停脚本 scripts/restart_if11_backends.sh、算法文档、测试图

注:change_hair 侧 swapHair 的 ext_mask/denoising_strength 改造在 change_hair 仓库,向后兼容。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
xsl
2026-07-09 22:06:17 +08:00
co-authored by Claude Opus 4.8
parent 3eb60bddc5
commit 0ddfa83743
6 changed files with 883 additions and 0 deletions
+73
View File
@@ -1232,6 +1232,79 @@ async def head_band(
return err(1007, f"处理失败:{ex}")
# ---------------------------------------------------------------------------
# 接口 11:发际线生发(接口9 遮罩 + change_hair 换发型 + 按遮罩羽化贴回)
# ---------------------------------------------------------------------------
@app.post(
"/api/v1/hairline/grow",
summary="接口11 发际线生发",
tags=["生发"],
description=f"""
输入一张**发际线较高 / 头发稀少**的正脸图 + **发际线类型 ID**= change_hair 的 `hair_id`
如 `chang_tuoyuan`/`chang_bolang`/`chang_zhixian`/`chang_xinxing`/`chang_huaban`),
输出同一个人、同一发型、按该发际线类型压低发际线后的图片,并返回**每一步可视化**。
管线(见 `docs/发际线增强算法.md`):
1. 用接口9 算法算头发遮罩(含额头闭合区域,外缘内缩 `erode_cm`)。`seg_model` 选
BiSeNet/SegFormer`mask_type` 选 eroded(内缩)/closed(闭合区域)。
2. 调 change_hair 换发型服务生成该发际线类型图(返回图已与原图同分辨率同对齐)。
`swap_mode=ext_mask`:把接口9 遮罩作为 `ext_mask` 传给换发型,webui 精确重绘该区域
(忠于算法文档);`swap_mode=as_is`:不改换发型,贴回时再裁到接口9 遮罩。
3. 严格按接口9 遮罩把生成图贴回原图(遮罩外=原图不动)。
4. 接缝融合:`blend_method` 选 feather(高斯羽化)/alpha_gradient(距离变换内渐变)/
seamless(泊松无缝克隆)`feather_px`、`edge_erode_px` 控制过渡细节。
{_image_fields_desc}
返回 `data.steps` 含 `input`/`baseline`/`hair_mask`/`mask_overlay`/`mask`/`swap_raw`/
`alpha`/`final` 各步图(`*_base64`,经网关改写为 `*_url`)。
""",
)
async def hairline_grow(
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, 前缀)"),
hairline_id: str = Form(..., description="发际线类型 ID= change_hair hair_id,如 chang_tuoyuan"),
gen_backend: str = Form(default="swaphair", description="生成后端:swaphair(换发型LoRA) | hairgrow(区域生发inpaint,压低发际线)(默认 swaphair)"),
hairgrow_strength: float = Form(default=0.75, description="区域生发强度(仅 hairgrow 后端),默认 0.75"),
is_hr: bool = Form(default=False, description="高清模式(换发型输出 1152×1536,否则 576×768"),
seg_model: str = Form(default="segformer", description="头发分割模型:bisenet | segformer(默认 segformer"),
mask_type: str = Form(default="eroded", description="遮罩类型:eroded(内缩) | closed(闭合区域)(默认 eroded"),
erode_cm: float = Form(default=1.2, description="遮罩外缘朝中心151内缩距离(厘米,同接口9),默认 1.2"),
swap_mode: str = Form(default="ext_mask", description="换发型取图模式:ext_mask(改造换发型用接口9遮罩) | as_is(不改换发型,贴回再裁)(默认 ext_mask)"),
blend_method: str = Form(default="feather", description="接缝融合:feather(高斯羽化) | alpha_gradient(距离渐变) | seamless(泊松无缝)(默认 feather"),
feather_px: int = Form(default=15, description="羽化/渐变过渡宽度(像素),默认 15"),
edge_erode_px: int = Form(default=3, description="贴图前遮罩内缩像素(防边缘露皮/光晕),默认 3"),
denoising_strength: float = Form(default=0.6, description="换发型 webui 重绘强度(越大生发越激进),默认 0.6"),
):
"""接口11:发际线生发 + 分步可视化"""
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.hairline_grow import generate_hairline_grow, NoFaceError, SwapError
try:
data = await run_in_threadpool(
generate_hairline_grow, image, hairline_id, is_hr, seg_model,
mask_type, erode_cm, swap_mode, blend_method, feather_px, edge_erode_px,
denoising_strength, gen_backend, hairgrow_strength)
except NoFaceError:
return err(1001, "无法识别人像")
except SwapError as se:
return err(1007, f"换发型失败:{se}")
return ok(data)
except Exception as ex: # noqa: BLE001
logger.exception("接口11 处理异常")
return err(1007, f"处理失败:{ex}")
# ---------------------------------------------------------------------------
# 健康检查
# ---------------------------------------------------------------------------