接口10:头部外缘膨胀带遮罩 + 分步可视化

新增 POST /api/v1/head/band(worker + 网关代理)与测试页 test_interface10.html:
- 先和接口9 一样得到内缩后的基准遮罩(含额头的闭合区域外缘朝151内缩 erode_cm、
  底线不动,默认1.2cm),在此基础上取外轮廓线,去掉贴着底部分割线的那一段。
- 把外轮廓线膨胀成带子(总宽 dilate_cm,默认2cm;半径=总宽/2,虹膜标定换算)。
- 裁到分割线以上(不越过底线)。BiSeNet/SegFormer 两套并排对比。
- 两个可调参数 erode_cm + dilate_cm(页面数字框+滑块+localStorage)。
- 复用 head_mask 的构件,无新依赖;纯新增,不改动既有接口。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
xsl
2026-07-07 23:32:45 +08:00
co-authored by Claude Opus 4.8
parent 9774997035
commit 3eb60bddc5
4 changed files with 502 additions and 0 deletions
+86
View File
@@ -1146,6 +1146,92 @@ async def head_mask(
return err(1007, f"处理失败:{ex}")
# ---------------------------------------------------------------------------
# 接口 10:头部外缘膨胀带遮罩
# ---------------------------------------------------------------------------
@app.post(
"/api/v1/head/band",
summary="接口10 头部外缘膨胀带遮罩",
tags=["人脸分析"],
description=f"""
先和接口9 一样得到**内缩后的基准遮罩**(含额头的闭合区域外缘朝151内缩 erode_cm、底线不动,默认1.2cm),
在此基础上生成一条沿头部外缘的膨胀带遮罩:
1. 取基准遮罩的**外轮廓线**,去掉贴着底部分割线的那一段(只留头发/头部外缘弧线)。
2. 把外轮廓线膨胀成带子(**总宽 dilate_cm,默认 2cm,可调**;半径=总宽/2,虹膜标定换算像素)。
3. 裁到分割线以上(不越过底线)。
{_image_fields_desc}
返回 `data` 内含 `steps_common`(关键点/分割线、上半区)与 `bisenet` / `segformer`
两组结果(各含 `base_mask` / `contour` / `band_overlay` / `mask`)。经网关时所有
`*_base64` 图片字段会被落盘改写为 `*_url`。
""",
responses={
200: {
"description": "成功",
"content": {
"application/json": {
"example": {
"code": 0,
"message": "success",
"request_id": "mock-request-id",
"data": {
"px_per_cm": 48.12,
"erode_cm": 1.2,
"erode_px": 58,
"dilate_cm": 2.0,
"dilate_radius_px": 48,
"image_size": {"width": 1080, "height": 1440},
"steps_common": {"landmarks_baseline_url": SAMPLE_IMAGE_URL,
"upper_region_url": SAMPLE_IMAGE_URL},
"bisenet": {"base_pixels": 96000, "band_pixels": 42000,
"base_mask_url": SAMPLE_IMAGE_URL,
"contour_url": SAMPLE_IMAGE_URL,
"band_overlay_url": SAMPLE_IMAGE_URL,
"mask_url": SAMPLE_IMAGE_URL},
"segformer": {"base_pixels": 97000, "band_pixels": 43000,
"base_mask_url": SAMPLE_IMAGE_URL,
"contour_url": SAMPLE_IMAGE_URL,
"band_overlay_url": SAMPLE_IMAGE_URL,
"mask_url": SAMPLE_IMAGE_URL},
},
}
}
},
},
},
)
async def head_band(
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, 前缀)"),
erode_cm: float = Form(default=1.2, description="基准遮罩外缘朝151内缩距离(厘米,同接口9),默认 1.2"),
dilate_cm: float = Form(default=2.0, description="外轮廓线膨胀后带子总宽(厘米),默认 2.0"),
):
"""接口10:头部外缘膨胀带遮罩 + 分步可视化"""
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.head_band import generate_head_band, NoFaceError
try:
data = await run_in_threadpool(generate_head_band, image, erode_cm, dilate_cm)
except NoFaceError:
return err(1001, "无法识别人像")
return ok(data)
except Exception as ex: # noqa: BLE001
logger.exception("接口10 处理异常")
return err(1007, f"处理失败:{ex}")
# ---------------------------------------------------------------------------
# 健康检查
# ---------------------------------------------------------------------------