接口9:头发遮罩生成 + 分步可视化
新增 POST /api/v1/head/mask(worker + 网关代理)与测试页 test_interface9.html:
- MediaPipe 关键点连成额头分割线(21,68,104,69,108,151,337,299,333,298,251,
左端21/右端251 水平延伸到图片边缘),分割线以上为上半区。
- 头发分割 BiSeNet 与 SegFormer 两套并排对比;每列从最顶端头发向下填充到分割线,
得到含额头的闭合区域(不从发际线割断)。
- 外缘朝中心点151内缩 erode_cm(默认1.2cm,页面可调,虹膜标定换算像素)、底线不动。
- 复用现有 detector/hair_segmenter/SegFormer 单例(只读推理),无新依赖;纯新增,
不改动既有接口。
顺带修复接口2 遗留测试 test_grow_female_returns_5:hair_style 自 cb1989c 起必填,
补上 hair_style=1,2,3,4,5。全套 42 passed。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1061,6 +1061,91 @@ async def hairline_generate(
|
||||
return err(1007, f"处理失败:{ex}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 接口 9:头发遮罩生成
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.post(
|
||||
"/api/v1/head/mask",
|
||||
summary="接口9 头发遮罩生成",
|
||||
tags=["人脸分析"],
|
||||
description=f"""
|
||||
输入一张含人头的照片,生成头发遮罩,并返回**每一步的可视化图**(供对比调试):
|
||||
|
||||
1. MediaPipe 关键点 → 底部分割线(关键点 21,68,104,69,108,151,337,299,333,298,251 的连线,
|
||||
21 水平延伸到最左边、251 延伸到最右边)。
|
||||
2. 分割线以上为「上半区」。
|
||||
3. 头发分割(**BiSeNet** 与 **SegFormer** 两套);从每列最顶端头发向下填充到分割线,
|
||||
得到**含额头**的闭合区域(头发+额头,不割断)。
|
||||
4. 外缘朝中心点 151 内缩 **erode_cm(默认 1.2cm,可调)**(虹膜标定换算像素)、底线不动 → 最终遮罩。
|
||||
|
||||
{_image_fields_desc}
|
||||
|
||||
返回 `data` 内含 `steps_common`(关键点/分割线、上半区)与 `bisenet` / `segformer`
|
||||
两组结果(各含 `hair_mask` / `closed_region` / `final_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,
|
||||
"image_size": {"width": 1080, "height": 1440},
|
||||
"steps_common": {"landmarks_baseline_url": SAMPLE_IMAGE_URL,
|
||||
"upper_region_url": SAMPLE_IMAGE_URL},
|
||||
"bisenet": {"hair_pixels": 123456, "closed_pixels": 110000, "mask_pixels": 98765,
|
||||
"hair_mask_url": SAMPLE_IMAGE_URL,
|
||||
"closed_region_url": SAMPLE_IMAGE_URL,
|
||||
"final_overlay_url": SAMPLE_IMAGE_URL,
|
||||
"mask_url": SAMPLE_IMAGE_URL},
|
||||
"segformer": {"hair_pixels": 130000, "closed_pixels": 112000, "mask_pixels": 99000,
|
||||
"hair_mask_url": SAMPLE_IMAGE_URL,
|
||||
"closed_region_url": SAMPLE_IMAGE_URL,
|
||||
"final_overlay_url": SAMPLE_IMAGE_URL,
|
||||
"mask_url": SAMPLE_IMAGE_URL},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
async def head_mask(
|
||||
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内缩的距离(厘米),默认 1.2"),
|
||||
):
|
||||
"""接口9:头发遮罩生成 + 分步可视化"""
|
||||
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_mask import generate_head_mask, NoFaceError
|
||||
try:
|
||||
data = await run_in_threadpool(generate_head_mask, image, erode_cm)
|
||||
except NoFaceError:
|
||||
return err(1001, "无法识别人像")
|
||||
return ok(data)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
logger.exception("接口9 处理异常")
|
||||
return err(1007, f"处理失败:{ex}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 健康检查
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user