From 5311c8d7c7c124b26142ffb9f04a4401bf9a50b7 Mon Sep 17 00:00:00 2001 From: xsl Date: Wed, 17 Jun 2026 23:01:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8E=A5=E5=8F=A32/3):=20=E5=8A=A0=20use?= =?UTF-8?q?=5Fmask=20=E5=BC=80=E5=85=B3=E7=94=A8=E4=BA=8E=E9=81=AE?= =?UTF-8?q?=E7=BD=A9=E5=AF=B9=E6=AF=94=EF=BC=9B=E7=BD=91=E5=85=B3=E5=9B=9E?= =?UTF-8?q?=E9=80=80=E7=BA=AF=E9=80=8F=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 接口3 generate_grow_b 增加 use_mask(默认 true):false 跳过检测、 直接送划线图(空遮罩) - 接口2 generate_grow_results 增加 use_mask:false 用干净原图+空遮罩, 只跑一次 ComfyUI、N 项复用 - app.py 接口2/3 增加 use_mask form 参数并透传 - gateway/app.py 回退为纯透传(移除 OpenAPI schema 注入,请求体原样转发) - docs 补 use_mask 说明 Co-Authored-By: Claude Opus 4.8 --- app.py | 3 +- docs/接口文档.md | 1 + gateway/app.py | 71 ++------------------------------------------- hairline/service.py | 40 ++++++++++++++++++------- 4 files changed, 35 insertions(+), 80 deletions(-) diff --git a/app.py b/app.py index 3cd54a2..5739c8b 100644 --- a/app.py +++ b/app.py @@ -503,6 +503,7 @@ async def hair_grow( image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"), gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"), beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"), + use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"), ): # 1. gender 必填校验(非法/缺失 → 1004) if gender not in ("male", "female"): @@ -529,7 +530,7 @@ async def hair_grow( from hairline.service import generate_grow_results # 预览 + 生发(ComfyUI) 都是阻塞且较慢,放线程池避免卡住事件循环 - items = await run_in_threadpool(generate_grow_results, image, gender) + items = await run_in_threadpool(generate_grow_results, image, gender, use_mask) if items is None: return err(1001, "无法识别人像") diff --git a/docs/接口文档.md b/docs/接口文档.md index b704e90..7769375 100644 --- a/docs/接口文档.md +++ b/docs/接口文档.md @@ -192,6 +192,7 @@ |------|------|------|------| | gender | string | **是** | 性别:`male` / `female`。决定使用的发际线贴图集合 | | beauty_enabled | bool | 否 | 生发图是否带美颜效果,默认 false(当前阶段不生效) | +| use_mask | bool | 否 | 是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比;此时 N 张结果共用同一张生发图 | ### 输出(data) diff --git a/gateway/app.py b/gateway/app.py index faa0e05..bc19f79 100644 --- a/gateway/app.py +++ b/gateway/app.py @@ -214,8 +214,7 @@ def _proxy(request: Request, path: str): return proxy_request(request, path) -# 声明各接口的 form 参数 → 注入 OpenAPI schema(实际转发直接读 Request body,不在此解析)。 -# 每项字段:type(file/string/boolean)、description,可选 required(默认否)/default。 +# 声明各接口的 form 参数用于 OpenAPI schema(实际转发直接读 Request) _MEASURE_FORMS = { "image_file": {"type": "file", "description": "上传图片文件(JPG/PNG,≤ 1 MB)"}, "image_url": {"type": "string", "description": "图片 URL"}, @@ -224,31 +223,13 @@ _MEASURE_FORMS = { _GROW_FORMS = { **_MEASURE_FORMS, - "gender": {"type": "string", "description": "性别 male/female(必填)", "required": True}, - "beauty_enabled": {"type": "boolean", "description": "是否开启美颜(本期不生效)", "default": False}, + "beauty_enabled": {"type": "boolean", "description": "是否开启美颜效果"}, } _GROW_B_FORMS = { - "marked_image_file": {"type": "file", "description": "划线图片文件(JPG/PNG,≤ 1 MB)"}, + "marked_image_file": {"type": "file", "description": "划线图片文件"}, "marked_image_url": {"type": "string", "description": "划线图片 URL"}, "marked_image_base64": {"type": "string", "description": "划线图片 base64"}, - "use_mask": {"type": "boolean", - "description": "是否启用自动检测遮罩,默认 true;false 跳过检测直接送图(空遮罩),测试对比用", - "default": True}, -} - -_HAIRLINE_FORMS = { - **_MEASURE_FORMS, - "gender": {"type": "string", "description": "性别 male/female(必填)", "required": True}, -} - -# 路由(path, method) → form 参数表,供 custom_openapi 注入 requestBody。 -# 接口4(/api/v1/face/features) 在网关本地用 Form() 声明,FastAPI 自动生成 schema,不在此列。 -_ROUTE_FORMS = { - ("/api/v1/face/measure", "post"): _MEASURE_FORMS, - ("/api/v1/hair/grow", "post"): _GROW_FORMS, - ("/api/v1/hair/grow-b", "post"): _GROW_B_FORMS, - ("/api/v1/hairline/generate", "post"): _HAIRLINE_FORMS, } @@ -339,49 +320,3 @@ async def face_features( async def hairline_generate(request: Request): """接口5:发际线PNG生成""" return await _proxy(request, "/api/v1/hairline/generate") - - -# --------------------------------------------------------------------------- -# 自定义 OpenAPI:代理路由用 Request 透传、不声明 Form,FastAPI 默认生成不出入参。 -# 这里把 _ROUTE_FORMS 注入各路由的 multipart/form-data requestBody,使 /docs 入参完整。 -# --------------------------------------------------------------------------- - - -def _form_schema(forms: dict) -> dict: - """form 参数表 → multipart/form-data 的 JSON Schema。""" - props, required = {}, [] - for name, spec in forms.items(): - prop = ({"type": "string", "format": "binary"} if spec["type"] == "file" - else {"type": spec["type"]}) - if spec.get("description"): - prop["description"] = spec["description"] - if "default" in spec: - prop["default"] = spec["default"] - props[name] = prop - if spec.get("required"): - required.append(name) - schema = {"type": "object", "properties": props} - if required: - schema["required"] = required - return schema - - -def custom_openapi(): - if app.openapi_schema: - return app.openapi_schema - from fastapi.openapi.utils import get_openapi - - schema = get_openapi(title=app.title, version=app.version, - description=app.description, routes=app.routes) - for (path, method), forms in _ROUTE_FORMS.items(): - op = schema.get("paths", {}).get(path, {}).get(method) - if op is not None: - op["requestBody"] = { - "required": True, - "content": {"multipart/form-data": {"schema": _form_schema(forms)}}, - } - app.openapi_schema = schema - return schema - - -app.openapi = custom_openapi diff --git a/hairline/service.py b/hairline/service.py index 9315aab..f901792 100644 --- a/hairline/service.py +++ b/hairline/service.py @@ -128,9 +128,12 @@ def generate_previews(image_bgr: np.ndarray, gender: str): return results -def generate_grow_results(image_bgr: np.ndarray, gender: str): +def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = True): """该性别全部发际线:预览图(白线) + 生发图(ComfyUI)。同步、串行。 + use_mask(默认 True):是否启用 inpaint 遮罩,用于测试对比(同接口3)。 + False 时用**干净原图 + 空遮罩**送 ComfyUI(不烧黑色模板线),与模板无关, + 故只跑一次 ComfyUI、N 项复用同一张生发图;预览图(白线)仍按各模板生成。 Returns: list[dict] {"hairline_type","order","image_bgr"(预览), "grown_png"(bytes 或 None)}。 无人脸返回 None。某张 ComfyUI 失败时该项 grown_png=None,不影响其余。 """ @@ -140,21 +143,36 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str): if ctx is None: return None uv, ext_faces = load_ext_mesh() + + # 禁用遮罩:干净原图 + 空遮罩,与模板无关 → 只跑一次 ComfyUI,下面 N 项复用 + shared_grown = None + if not use_mask: + try: + h, w = image_bgr.shape[:2] + buf = io.BytesIO() + compose_comfy_rgba(image_bgr, np.zeros((h, w), np.uint8)).save(buf, format="PNG") + shared_grown = comfyui.run(buf.getvalue()) + except Exception as e: # noqa: BLE001 + logger.warning("接口2 生发图失败(无遮罩):%s", e) + results = [] for order, (key, white_path) in enumerate(get_texture_map()[gender], start=1): white = load_texture_rgba(white_path) preview = render_hairline_overlay(image_bgr, ctx["points"], ext_faces, uv, white) - grown_png = None - try: - black = load_texture_rgba(_black_texture_path(white_path)) - marked, mask = build_inpaint_mask( - image_bgr, ctx["landmarks"], ctx["parse_map"], ctx["points"], black) - buf = io.BytesIO() - compose_comfy_rgba(marked, mask).save(buf, format="PNG") - grown_png = comfyui.run(buf.getvalue()) - except Exception as e: # noqa: BLE001 单张失败不拖垮整请求 - logger.warning("接口2 生发图失败 type=%s:%s", key, e) + if not use_mask: + grown_png = shared_grown + else: + grown_png = None + try: + black = load_texture_rgba(_black_texture_path(white_path)) + marked, mask = build_inpaint_mask( + image_bgr, ctx["landmarks"], ctx["parse_map"], ctx["points"], black) + buf = io.BytesIO() + compose_comfy_rgba(marked, mask).save(buf, format="PNG") + grown_png = comfyui.run(buf.getvalue()) + except Exception as e: # noqa: BLE001 单张失败不拖垮整请求 + logger.warning("接口2 生发图失败 type=%s:%s", key, e) results.append({"hairline_type": key, "order": order, "image_bgr": preview, "grown_png": grown_png})