diff --git a/app.py b/app.py index 0d2cc93..b8e9d6d 100644 --- a/app.py +++ b/app.py @@ -714,10 +714,11 @@ async def face_measure_v2( - 发际线类型 `hairline_type`(英文 key) - 顺序 `order`(本期固定 `1..N`,不排序) -> **female 走「换发型」模式**:生发图 `grown_image_base64` 由换发型(change_hair) +> **female 走「换发型」模式**:1..5 的生发图 `grown_image_base64` 由换发型(change_hair) > + Flux-2 整帧重绘(= 接口12 final 管线,整帧美颜+整帧重绘)生成,其余参数用固化默认值。 -> **male 仍走原生发(ComfyUI add_hair)管线**。入参与返回结构不变。 -> female 依赖 change_hair 与 ComfyUI(:8188) 均在跑。 +> **6/7(bigflower/clasicalflower)与 male 一样走原生发(ComfyUI add_hair)管线**。 +> **male 全部走原生发(ComfyUI add_hair)管线**。入参与返回结构不变。 +> female 1..5 依赖 change_hair 与 ComfyUI(:8188) 均在跑。 {_image_fields_desc} @@ -725,15 +726,15 @@ async def face_measure_v2( --- -- **gender**(必填):`male` / `female`。决定返回的贴图集合(female 5 张 / male 4 张)。 +- **gender**(必填):`male` / `female`。决定返回的贴图集合(female 7 张 / male 6 张)。 非法或缺失返回 `1004`。 - **hair_style**(必填):发型序号,**逗号分隔多选**(如 `1,2,3`),最多不超过该性别的预设数量。 - `female`:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave; - `male`:1=ellipse, 2=inverse_arc, 3=m, 4=straight。越界/非法返回 `1007`。 + `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。越界/非法返回 `1007`。 - **beauty_enabled**:本期保留但不生效。 -`hairline_type` 取值:`ellipse` / `flower` / `heart` / `straight` / `wave`(female), -`ellipse` / `m` / `straight` / `inverse_arc`(male)。 +`hairline_type` 取值:`ellipse` / `flower` / `heart` / `straight` / `wave` / `bigflower` / `clasicalflower`(female), +`ellipse` / `m` / `straight` / `inverse_arc` / `heart` / `Softpetal`(male)。 """, responses={ 200: { @@ -772,7 +773,7 @@ async def hair_grow( image_url: Optional[str] = Form(default=None, description="图片 URL"), image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"), gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"), - hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-5 male:1-4"), + hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填),如 1,2,3。female:1-7 male:1-6"), beauty_enabled: bool = Form(default=False, description="是否开启美颜(本期不生效)"), use_mask: bool = Form(default=True, description="是否启用 inpaint 遮罩(测试对比用)。false 时用干净原图生成(空遮罩,不烧模板线)"), prompt: str = Form(default="填充遮罩区域的头发", description="ComfyUI 提示词,会替换工作流节点60的文本"), @@ -784,7 +785,7 @@ async def hair_grow( return err(1004, "gender 必填且只能为 male / female") # 2. hair_style 必填校验(解析逗号分隔,越界 → 1007) - max_styles = {"female": 5, "male": 4}[gender] + max_styles = {"female": 7, "male": 6}[gender] hair_styles = _parse_hair_styles(hair_style, max_styles) if hair_styles is None: return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}") @@ -807,7 +808,7 @@ async def hair_grow( from hairline.service import generate_grow_results_swap items = await run_in_threadpool( generate_grow_results_swap, image, hair_styles, _V2_FINAL_DEFAULTS, - redraw_max_side=redraw_max_side, unet_name=flux_model) + redraw_max_side=redraw_max_side, unet_name=flux_model, prompt=prompt) else: from hairline.service import generate_grow_results items = await run_in_threadpool( @@ -831,6 +832,149 @@ async def hair_grow( return err(1007, f"处理失败:{ex}") +# --------------------------------------------------------------------------- +# 调试接口:接口2 女性生发 分步计时 +# --------------------------------------------------------------------------- +@app.post( + "/api/v1/debug/grow-timing", + summary="调试-接口2女性生发分步计时", + tags=["调试"], + include_in_schema=False, +) +async def debug_grow_timing( + image_file: Optional[UploadFile] = File(default=None), + image_url: Optional[str] = Form(default=None), + image_base64: Optional[str] = Form(default=None), + hair_style: str = Form(default="2", description="发型序号(花瓣=2),逗号分隔多选"), + webui_steps: Optional[int] = Form(default=None, description="swapHair webui img2img 采样步数,None=服务端默认(15),可填10/15/20/25对比"), + redraw_max_side: Optional[int] = Form(default=None, description="ComfyUI重绘分辨率(长边像素)。None=默认896;0=原图不缩;其他如640/768/1024"), + redraw_prompt: Optional[str] = Form(default=None, description="ComfyUI重绘提示词,None=默认'填充遮罩区域的头发'"), +): + """单图跑接口2女性生发,返回每个步骤的耗时 + 结果图,用于定位性能瓶颈。 + + 步骤拆分: + 1. extract_context:人脸关键点检测 + 头发分割 + 发际线几何 + 2. [每个发型] generate_hairline_redraw: + 2a. compute_mask:发际线遮罩计算 + 2b. _call_swap:调 change_hair 换发型(内含 webui SD1.5 推理,远程或本机) + 2c. _composite:接缝融合(多频段/羽化) + 3. [每个发型] _call_local_redraw:调本机 ComfyUI 用 Flux.2 重绘 + """ + import time as _time + from fastapi.concurrency import run_in_threadpool + + 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: + max_styles = 7 + hair_styles = _parse_hair_styles(hair_style, max_styles) + if hair_styles is None: + return err(1007, f"hair_style 必须为 1..{max_styles}") + + t_total0 = _time.perf_counter() + timings = {"total_ms": 0, "extract_context_ms": 0, "per_hairstyle": []} + + # 步骤1: extract_context + t0 = _time.perf_counter() + from hairline.service import extract_context as _ec, _call_local_redraw, _REDRAW_MAX_SIDE # noqa + from face_analysis.hairline_grow import generate_hairline_redraw, NoFaceError # noqa + from face_analysis.head_mask import SEGFORMER_HAIR # noqa + ctx = await run_in_threadpool(_ec, image) + timings["extract_context_ms"] = int((_time.perf_counter() - t0) * 1000) + if ctx is None: + return err(1001, "无法识别人像") + + hair_mask_reuse = (ctx["parse_map"] == SEGFORMER_HAIR) + h, w = image.shape[:2] + eff_side = _REDRAW_MAX_SIDE if redraw_max_side is None else redraw_max_side + redraw_img, hair_mask_redraw = image, hair_mask_reuse + downscale_info = None + if eff_side > 0 and max(h, w) > eff_side: + from hairline.service import _downscale_max_side + redraw_img, _rs = _downscale_max_side(image, eff_side) + _nh, _nw = redraw_img.shape[:2] + if hair_mask_redraw is not None: + hair_mask_redraw = cv2.resize(hair_mask_reuse.astype(np.uint8), (_nw, _nh), + interpolation=cv2.INTER_NEAREST).astype(bool) + downscale_info = {"from": f"{w}x{h}", "to": f"{_nw}x{_nh}", "max_side": eff_side} + + textures_map = None + from hairline.service import get_texture_map, _FEMALE_KEY_TO_CHANG, load_texture_rgba, build_overlay_layer, load_ext_mesh + textures = get_texture_map()["female"] + items = [(s, textures[s - 1]) for s in hair_styles] + + for order, (key, white_path) in items: + hs_t0 = _time.perf_counter() + entry = {"hairline_type": key, "order": order} + chang_id = _FEMALE_KEY_TO_CHANG.get(key) + entry["chang_id"] = chang_id + entry["ok"] = False + entry["error"] = None + entry["grown_b64"] = None + if chang_id is None: + entry["error"] = f"无对应 chang_id" + timings["per_hairstyle"].append(entry) + continue + try: + # 2a/2b/2c: generate_hairline_redraw (内部含 mask+swap+blend) + t0 = _time.perf_counter() + data = await run_in_threadpool( + generate_hairline_redraw, redraw_img, chang_id, + hair_mask=hair_mask_redraw, webui_steps=webui_steps, **_V2_FINAL_DEFAULTS) + t_redraw_pipeline = _time.perf_counter() - t0 + _tm = data.get("timings_ms") or {} + entry["mask_ms"] = _tm.get("mask", 0) + entry["swap_ms"] = _tm.get("swap", 0) + entry["blend_ms"] = _tm.get("blend", 0) + entry["redraw_pipeline_ms"] = int(t_redraw_pipeline * 1000) + + steps = data.get("steps") or {} + final_b64 = steps.get("final_base64") or "" + mask_b64 = steps.get("redraw_band_mask_base64") or "" + if not final_b64 or not mask_b64: + entry["error"] = f"final/遮罩缺失(final={len(final_b64)} mask={len(mask_b64)})" + timings["per_hairstyle"].append(entry) + continue + if final_b64.startswith("data:"): + final_b64 = final_b64.split(",", 1)[1] + if mask_b64.startswith("data:"): + mask_b64 = mask_b64.split(",", 1)[1] + + # 3: ComfyUI 重绘 + t0 = _time.perf_counter() + # max_side: 0 或 None 都让 _call_local_redraw 用默认逻辑(外层已控制分辨率) + _ms = redraw_max_side if redraw_max_side is not None and redraw_max_side > 0 else None + grown_png = await run_in_threadpool( + _call_local_redraw, + base64.b64decode(final_b64), base64.b64decode(mask_b64), + max_side=_ms, prompt=redraw_prompt) + entry["comfyui_redraw_ms"] = int((_time.perf_counter() - t0) * 1000) + if grown_png: + entry["grown_b64"] = "data:image/jpeg;base64," + _png_to_jpg_b64(grown_png) + entry["ok"] = True + else: + entry["error"] = "ComfyUI 重绘返回空" + except NoFaceError: + entry["error"] = "未检出人脸" + except Exception as ex: # noqa: BLE001 + entry["error"] = str(ex)[:150] + entry["hairstyle_total_ms"] = int((_time.perf_counter() - hs_t0) * 1000) + timings["per_hairstyle"].append(entry) + + timings["total_ms"] = int((_time.perf_counter() - t_total0) * 1000) + timings["downscale"] = downscale_info + timings["image_size"] = f"{w}x{h}" + return ok(timings) + except Exception as ex: # noqa: BLE001 + logger.exception("debug/grow-timing 异常") + return err(1007, f"处理失败:{ex}") + + # --------------------------------------------------------------------------- # 接口 7:C 端生发 v2 —— 已弃用(add_hair2.json 用 Klein-9b 大模型,会把常驻的 # Klein-4b/Flux 挤出显存,导致接口2/3/5 耗时抖动;且业务已不再调用)。 @@ -1016,11 +1160,13 @@ async def face_features( --- **入参**(同接口2:先选性别,再多选发型): -- 必填 `gender`(`male`/`female`),决定发型集合(female 5 / male 4)。 +- 必填 `gender`(`male`/`female`),决定发型集合(female 7 / male 6)。 - 必填 `hair_style`(发型序号,逗号分隔如 `1,2,3`),决定返回哪些发际线类型。缺失/越界/非法返回 `1007`。 - `female`:1=ellipse,2=flower,3=heart,4=straight,5=wave;`male`:1=ellipse,2=inverse_arc,3=m,4=straight。 + `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。 - 可选 `use_mask` / `prompt`:同接口2 的生发控制参数(仅 male 路径生效)。 -- 可选 `generate_grow_image`(默认 `true`):是否生成生发效果图(最耗时)。 + 注:生发黑模板固定取 `hairline_texture_black/`(middle 档),即三档叠图分别用各自贴图、但生发目标固定 middle。 +- 可选 `generate_grow_image`(默认 `true`):是否生成生发效果图(ComfyUI 生发,全流程最耗时)。 `false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,大幅降低耗时。 - **生发机制(同接口2,按性别分流)**: `female` 走「换发型 + Flux-2 整帧重绘」(依赖 change_hair:8801 与 ComfyUI:8188); @@ -1101,7 +1247,7 @@ async def hairline_generate( image_url: Optional[str] = Form(default=None, description="图片 URL"), image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"), gender: Optional[str] = Form(default=None, description="性别 male/female(必填)"), - hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填,如 1,2,3)。female:1-5 male:1-4"), + hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填,如 1,2,3)。female:1-7 male:1-6"), use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,仅 male 路径生效)"), prompt: str = Form(default="填充遮罩区域的头发", description="ComfyUI 提示词(同接口2,仅 male 路径生效),会替换工作流节点60的文本"), generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(最耗时)。默认 true 出图;false 时跳过生发,各发型 grown_image 恒为 null,仅返回三档发际线叠图与中心点"), @@ -1112,7 +1258,7 @@ async def hairline_generate( return err(1004, "gender 必填且只能为 male / female") # hair_style 必填(同接口2):解析逗号分隔,缺失/越界/非法 → 1007 - max_styles = {"female": 5, "male": 4}[gender] + max_styles = {"female": 7, "male": 6}[gender] hair_styles = _parse_hair_styles(hair_style, max_styles) if hair_styles is None: return err(1007, f"hair_style 必填且为 1..{max_styles} 的整数(逗号分隔),收到 {hair_style!r}") diff --git a/docs/实现说明.md b/docs/实现说明.md index 9aab141..fe5d120 100644 --- a/docs/实现说明.md +++ b/docs/实现说明.md @@ -68,7 +68,7 @@ 按 `face_ext.obj` 的 UV 把发际线贴图渲染到额头(预览)。生发:黑贴图渲染遮罩 → 调本机 **ComfyUI 8182** 的 `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;male 1=ellipse 2=inverse_arc 3=m 4=straight。 +- `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。female 1..5 走「换发型(change_hair)」+Flux-2 重绘管线;female 6/7 与 male 全部走原生发(ComfyUI add_hair)管线。 ### 接口7 C端生发 v2 `/api/v1/hair/grow-v2`(worker)—— 接口2同款,add_hair2 工作流 - **做什么**:与接口 2 完全一致(正面照 + `gender` + `hair_style` 逗号分隔多选 → N 组预览+生发图)。 diff --git a/docs/接口文档.md b/docs/接口文档.md index f8e770c..c4d990d 100644 --- a/docs/接口文档.md +++ b/docs/接口文档.md @@ -265,7 +265,7 @@ | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | gender | string | **是** | 性别:`male` / `female`。决定使用的发际线贴图集合 | -| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`),最多不超过该性别的预设数。female:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave;male:1=ellipse, 2=inverse_arc, 3=m, 4=straight。越界/非法返回 `1007` | +| 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。越界/非法返回 `1007` | | beauty_enabled | bool | 否 | 生发图是否带美颜效果,默认 false(当前阶段不生效) | | use_mask | bool | 否 | 是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 | | prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发,加一点美颜」,会替换工作流节点 60 的文本 | @@ -278,7 +278,7 @@ |------|------|------| | image_url | string | 发际线曲线**透明 PNG** URL(仅白色发际线曲线,透明底,**不含人物**,需前端叠加原图显示) | | grown_image_url | string | **生发后图片** URL(ComfyUI/Flux「植发 3 个月」效果图,完整人像照片) | -| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`(female),`ellipse`/`m`/`straight`/`inverse_arc`(male) | +| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`/`bigflower`/`clasicalflower`(female),`ellipse`/`m`/`straight`/`inverse_arc`/`heart`/`Softpetal`(male) | | order | int | 排序序号(当前阶段固定 `1..N`,按贴图顺序,暂不计算合适度) | > ⚠️ 生发图由本机 ComfyUI(Flux-2,端口 8182)生成,**一次请求生成指定发型的 1 张、同步返回**。 @@ -405,8 +405,8 @@ | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| -| gender | string | **是** | 性别:`male` / `female`。决定发型集合(female 5 / male 4)。缺失/非法返回 `1004` | -| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`),决定返回哪些发际线类型。female:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave;male:1=ellipse, 2=inverse_arc, 3=m, 4=straight。缺失/越界/非法返回 `1007` | +| gender | string | **是** | 性别:`male` / `female`。决定发型集合(female 7 / male 6)。缺失/非法返回 `1004` | +| 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。缺失/越界/非法返回 `1007` | | use_mask | bool | 否 | 生发是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 | | prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发,加一点美颜」,会替换工作流节点 60 的文本 | | generate_grow_image | bool | 否 | 是否生成生发效果图(ComfyUI 生发,全流程最耗时),默认 `true`。传 `false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,可大幅降低耗时 | @@ -427,7 +427,7 @@ | 字段 | 类型 | 说明 | |------|------|------| -| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`(female),`ellipse`/`m`/`straight`/`inverse_arc`(male) | +| hairline_type | string | 发际线类型 key:`ellipse`/`flower`/`heart`/`straight`/`wave`/`bigflower`/`clasicalflower`(female),`ellipse`/`m`/`straight`/`inverse_arc`/`heart`/`Softpetal`(male) | | image_middle_url | string | middle 档发际线曲线**透明 PNG** URL(仅曲线,透明底,**不含人物**,需叠加原图显示) | | image_high_url | string | high 档发际线曲线**透明 PNG** URL(同上,high 档曲线) | | image_low_url | string | low 档发际线曲线**透明 PNG** URL(同上,low 档曲线) | diff --git a/hairline/service.py b/hairline/service.py index fe16171..7efd260 100644 --- a/hairline/service.py +++ b/hairline/service.py @@ -29,7 +29,8 @@ import logging logger = logging.getLogger("hair.worker") # 接口2 女性发型 key → change_hair hair_id(chang_*)映射:换发型+Flux-2 整帧重绘用。 -# 与接口12 final 的 5 型一一对应。 +# 与接口12 final 的 5 型一一对应。female 6/7(bigflower/clasicalflower)无对应 LoRA, +# 走与男性一致的原生生发(ComfyUI add_hair)管线,故不在本表。 _FEMALE_KEY_TO_CHANG = { "ellipse": "chang_tuoyuan", # 椭圆 "flower": "chang_huaban", # 花瓣 @@ -38,6 +39,16 @@ _FEMALE_KEY_TO_CHANG = { "wave": "chang_bolang", # 波浪 } +# 发际线贴图显式顺序表:决定 hair_style 序号(1-indexed)。 +# 不再依赖文件名字母序——字母序会因新增/重命名文件而错位,破坏现有前端/客户端取值。 +# key 须与 _gender_key 派生结果一致(已去空格):如 "inverse_arc"(源 man_ inverse_arc.png)、 +# "Softpetal"(源 man_Soft petal.png,大写 S 保留)。表外未知 key 兜底排到末尾。 +_HAIRSTYLE_ORDER = { + "female": ["ellipse", "flower", "heart", "straight", "wave", + "bigflower", "clasicalflower"], # 1..7 + "male": ["ellipse", "inverse_arc", "m", "straight", "heart", "Softpetal"], # 1..6 +} + _REPO = os.path.dirname(os.path.dirname(__file__)) _TEXTURE_DIR = os.path.join(_REPO, "hairline_texture") _BLACK_TEXTURE_DIR = os.path.join(_REPO, "hairline_texture_black") @@ -129,10 +140,13 @@ def _gender_key(stem: str): def get_texture_map(level: str = "middle") -> dict: - """扫描指定档位贴图目录建 {gender: [(key, path)]},按 key 排序、按档位缓存。 + """扫描指定档位贴图目录建 {gender: [(key, path)]},按显式顺序表排序、按档位缓存。 level:middle(默认) / high / low,分别对应 hairline_texture[/_high|/_low]。 文件名规范化去空格(如 `man_ inverse_arc.png` → key `inverse_arc`)。 + + 排序依据 _HAIRSTYLE_ORDER:表内 key 按表序、表外未知 key 兜底排到末尾(再按字母序), + 保证新增/重命名文件不会打乱现有 hair_style 序号。 """ if level not in _TEXTURE_DIRS: raise ValueError(f"hairline_level 必须是 middle/high/low,收到 {level!r}") @@ -146,7 +160,9 @@ def get_texture_map(level: str = "middle") -> dict: if gender: mapping[gender].append((key, path)) for g in mapping: - mapping[g].sort(key=lambda kp: kp[0]) + order = _HAIRSTYLE_ORDER.get(g, []) + idx = {k: i for i, k in enumerate(order)} + mapping[g].sort(key=lambda kp: (idx.get(kp[0], len(idx)), kp[0])) _texture_maps[level] = mapping return mapping @@ -210,7 +226,7 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = T unet_name: str | None = None): """指定发际线类型:发际线透明叠图(白线 RGBA) + 生发图(ComfyUI)。 - hair_styles(1-indexed 列表):指定生成哪几张发际线(按贴图排序)。female: 1..5,male: 1..4。 + hair_styles(1-indexed 列表):指定生成哪几张发际线(按贴图排序)。female: 1..7,male: 1..6。 为 None 时生成全部(兼容旧调用)。 use_mask(默认 True):是否启用 inpaint 遮罩,用于测试对比(同接口3)。 False 时用**干净原图 + 空遮罩**送 ComfyUI(不烧黑色模板线)。 @@ -284,15 +300,20 @@ def generate_grow_results(image_bgr: np.ndarray, gender: str, use_mask: bool = T def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | None, redraw_defaults: dict, redraw_max_side: int | None = None, - unet_name: str | None = None): - """接口2 女性专用:发际线透明叠图(同 generate_grow_results)+ 换发型重绘图。 + unet_name: str | None = None, + prompt: str | None = None): + """接口2 女性专用:发际线透明叠图(同 generate_grow_results)+ 生发图。 - grown 图来源(新流程):对每个选中发型把 female key 映射到 change_hair 的 chang_* hair_id, - 调 face_analysis.hairline_grow.generate_hairline_redraw(= 接口12 final 管线,参数用 - redraw_defaults)拿到 ④ final(接缝融合基底)+ ⑤-② 纯红遮罩 PNG,再**后端直接调 - ComfyUI**(0716add-hair-api.json 工作流)完成发际线带重绘,重绘结果作为生发图。 + 生发图来源按发型分两路: + - 换发型(chang_* 表内,1..5):female key → change_hair 的 chang_* hair_id,调 + face_analysis.hairline_grow.generate_hairline_redraw(= 接口12 final 管线,参数用 + redraw_defaults)拿到 ④ final(接缝融合基底)+ ⑤-② 纯红遮罩 PNG,再**后端直接调 + ComfyUI**(0716add-hair-api.json 工作流)完成发际线带重绘。 + - 原生生发(表外,6/7 bigflower/clasicalflower 等):无对应 change_hair LoRA,改走与男性 + 一致的原生生发(ComfyUI add_hair),由 _grow_native_one 完成(黑模板 + inpaint 遮罩)。 overlay 仍是发际线曲线透明层(与 generate_grow_results 完全一致)。 + prompt 仅用于原生生发分支(换发型分支的提示词由 redraw 流程内部固定)。 Returns: list[dict] {"hairline_type","order","overlay","grown_png"(jpg bytes 或 None)}; 无人脸返回 None。单个发型换发型/重绘失败时 grown_png=None,不抛异常。 """ @@ -337,7 +358,10 @@ def generate_grow_results_swap(image_bgr: np.ndarray, hair_styles: list[int] | N grown_png = None chang_id = _FEMALE_KEY_TO_CHANG.get(key) if chang_id is None: - logger.warning("接口2 换发型:female key=%s 无对应 chang_id,跳过生发图", key) + # 无对应 change_hair LoRA(如 bigflower/clasicalflower)→ 走与男性一致的原生生发 + logger.info("接口2 女 key=%s 无 chang_id,走原生生发(ComfyUI add_hair)", key) + grown_png = _grow_native_one(image_bgr, ctx, white_path, + prompt=prompt, unet_name=unet_name) else: try: import time as _t @@ -408,6 +432,32 @@ def _grow_from_texture(image_bgr: np.ndarray, ctx: dict, white_path: str | None, return None +def _grow_native_one(image_bgr: np.ndarray, ctx: dict, white_path: str, + prompt: str | None = None, unet_name: str | None = None): + """对单个发际线做原生生发(ComfyUI add_hair),与男性 generate_grow_results 一致。 + + 接口2 女性新发型(bigflower/clasicalflower 等)无对应 change_hair LoRA,改走此路径: + 黑模板 → build_inpaint_mask → 限边(_prep_comfy_input)→ comfyui.run(front=True)。 + 失败返回 None,不抛异常。结果按限边前原图尺寸放大回原尺寸(仅展示对齐)。 + """ + try: + h, w = image_bgr.shape[:2] + black = load_texture_rgba(_black_texture_path(white_path)) + marked, mask = build_inpaint_mask( + image_bgr, ctx["landmarks"], ctx["parse_map"], ctx["points"], black) + m_s, msk_s, gsc = _prep_comfy_input(marked, mask) + buf = io.BytesIO() + compose_comfy_rgba(m_s, msk_s).save(buf, format="PNG", compress_level=1) + # front=True:接口2 时延敏感,插到 ComfyUI 队列最前 + grown_png = comfyui.run(buf.getvalue(), prompt=prompt, front=True, unet_name=unet_name) + if gsc < 1.0 and grown_png: + grown_png = _upscale_png_to(grown_png, w, h) + return grown_png + except Exception as e: # noqa: BLE001 单张失败不拖垮整请求 + logger.warning("接口2 女原生生发图失败:%s", e) + return None + + def generate_hairline_pngs(image_bgr: np.ndarray, gender: str, hair_styles: list[int], use_mask: bool = True, prompt: str | None = None, diff --git a/hairline_texture/girl_bigflower.png b/hairline_texture/girl_bigflower.png new file mode 100644 index 0000000..61bc284 Binary files /dev/null and b/hairline_texture/girl_bigflower.png differ diff --git a/hairline_texture/girl_clasicalflower.png b/hairline_texture/girl_clasicalflower.png new file mode 100644 index 0000000..0fa08ff Binary files /dev/null and b/hairline_texture/girl_clasicalflower.png differ diff --git a/hairline_texture/girl_ellipse.png b/hairline_texture/girl_ellipse.png index 2f2207a..16f4a5d 100644 Binary files a/hairline_texture/girl_ellipse.png and b/hairline_texture/girl_ellipse.png differ diff --git a/hairline_texture/girl_flower.png b/hairline_texture/girl_flower.png index 0a478b4..47237bb 100644 Binary files a/hairline_texture/girl_flower.png and b/hairline_texture/girl_flower.png differ diff --git a/hairline_texture/girl_heart.png b/hairline_texture/girl_heart.png index 8559fdf..20b4fd4 100644 Binary files a/hairline_texture/girl_heart.png and b/hairline_texture/girl_heart.png differ diff --git a/hairline_texture/girl_straight.png b/hairline_texture/girl_straight.png index f46efa4..a659ddb 100644 Binary files a/hairline_texture/girl_straight.png and b/hairline_texture/girl_straight.png differ diff --git a/hairline_texture/girl_wave.png b/hairline_texture/girl_wave.png index f8adddc..ad9f0ab 100644 Binary files a/hairline_texture/girl_wave.png and b/hairline_texture/girl_wave.png differ diff --git a/hairline_texture/man_ inverse_arc.png b/hairline_texture/man_ inverse_arc.png index 1212535..3b57667 100644 Binary files a/hairline_texture/man_ inverse_arc.png and b/hairline_texture/man_ inverse_arc.png differ diff --git a/hairline_texture/man_Soft petal.png b/hairline_texture/man_Soft petal.png new file mode 100644 index 0000000..e2ed78c Binary files /dev/null and b/hairline_texture/man_Soft petal.png differ diff --git a/hairline_texture/man_ellipse.png b/hairline_texture/man_ellipse.png index b834176..316f33d 100644 Binary files a/hairline_texture/man_ellipse.png and b/hairline_texture/man_ellipse.png differ diff --git a/hairline_texture/man_heart.png b/hairline_texture/man_heart.png new file mode 100644 index 0000000..2e062f2 Binary files /dev/null and b/hairline_texture/man_heart.png differ diff --git a/hairline_texture/man_m.png b/hairline_texture/man_m.png index 70ce13a..5472663 100644 Binary files a/hairline_texture/man_m.png and b/hairline_texture/man_m.png differ diff --git a/hairline_texture/man_straight.png b/hairline_texture/man_straight.png index a5ea402..533ece3 100644 Binary files a/hairline_texture/man_straight.png and b/hairline_texture/man_straight.png differ diff --git a/hairline_texture_black/girl_bigflower.png b/hairline_texture_black/girl_bigflower.png new file mode 100644 index 0000000..3dec2ff Binary files /dev/null and b/hairline_texture_black/girl_bigflower.png differ diff --git a/hairline_texture_black/girl_clasicalflower.png b/hairline_texture_black/girl_clasicalflower.png new file mode 100644 index 0000000..9d732ef Binary files /dev/null and b/hairline_texture_black/girl_clasicalflower.png differ diff --git a/hairline_texture_black/girl_ellipse.png b/hairline_texture_black/girl_ellipse.png index bad192e..6038e0e 100644 Binary files a/hairline_texture_black/girl_ellipse.png and b/hairline_texture_black/girl_ellipse.png differ diff --git a/hairline_texture_black/girl_flower.png b/hairline_texture_black/girl_flower.png index debae8f..20abad7 100644 Binary files a/hairline_texture_black/girl_flower.png and b/hairline_texture_black/girl_flower.png differ diff --git a/hairline_texture_black/girl_heart.png b/hairline_texture_black/girl_heart.png index cd48126..672fc29 100644 Binary files a/hairline_texture_black/girl_heart.png and b/hairline_texture_black/girl_heart.png differ diff --git a/hairline_texture_black/girl_straight.png b/hairline_texture_black/girl_straight.png index 023d366..3d8dd64 100644 Binary files a/hairline_texture_black/girl_straight.png and b/hairline_texture_black/girl_straight.png differ diff --git a/hairline_texture_black/girl_wave.png b/hairline_texture_black/girl_wave.png index 4c2762f..9d5c357 100644 Binary files a/hairline_texture_black/girl_wave.png and b/hairline_texture_black/girl_wave.png differ diff --git a/hairline_texture_black/man_ inverse_arc.png b/hairline_texture_black/man_ inverse_arc.png index 6742d5e..798fba9 100644 Binary files a/hairline_texture_black/man_ inverse_arc.png and b/hairline_texture_black/man_ inverse_arc.png differ diff --git a/hairline_texture_black/man_Soft petal.png b/hairline_texture_black/man_Soft petal.png new file mode 100644 index 0000000..53e456a Binary files /dev/null and b/hairline_texture_black/man_Soft petal.png differ diff --git a/hairline_texture_black/man_ellipse.png b/hairline_texture_black/man_ellipse.png index 367acc9..1825e0a 100644 Binary files a/hairline_texture_black/man_ellipse.png and b/hairline_texture_black/man_ellipse.png differ diff --git a/hairline_texture_black/man_heart.png b/hairline_texture_black/man_heart.png new file mode 100644 index 0000000..9164450 Binary files /dev/null and b/hairline_texture_black/man_heart.png differ diff --git a/hairline_texture_black/man_m.png b/hairline_texture_black/man_m.png index d87b691..795ab07 100644 Binary files a/hairline_texture_black/man_m.png and b/hairline_texture_black/man_m.png differ diff --git a/hairline_texture_black/man_straight.png b/hairline_texture_black/man_straight.png index 632174e..ddae404 100644 Binary files a/hairline_texture_black/man_straight.png and b/hairline_texture_black/man_straight.png differ diff --git a/hairline_texture_high/girl_bigflower.png b/hairline_texture_high/girl_bigflower.png new file mode 100644 index 0000000..50a0dd7 Binary files /dev/null and b/hairline_texture_high/girl_bigflower.png differ diff --git a/hairline_texture_high/girl_clasicalflower.png b/hairline_texture_high/girl_clasicalflower.png new file mode 100644 index 0000000..9167fce Binary files /dev/null and b/hairline_texture_high/girl_clasicalflower.png differ diff --git a/hairline_texture_high/girl_ellipse.png b/hairline_texture_high/girl_ellipse.png index 1abbe93..c9f9b63 100644 Binary files a/hairline_texture_high/girl_ellipse.png and b/hairline_texture_high/girl_ellipse.png differ diff --git a/hairline_texture_high/girl_flower.png b/hairline_texture_high/girl_flower.png index b86cd4f..1d4f757 100644 Binary files a/hairline_texture_high/girl_flower.png and b/hairline_texture_high/girl_flower.png differ diff --git a/hairline_texture_high/girl_heart.png b/hairline_texture_high/girl_heart.png index bd7141d..1e26d4f 100644 Binary files a/hairline_texture_high/girl_heart.png and b/hairline_texture_high/girl_heart.png differ diff --git a/hairline_texture_high/girl_straight.png b/hairline_texture_high/girl_straight.png index 4a8ac8e..57aec80 100644 Binary files a/hairline_texture_high/girl_straight.png and b/hairline_texture_high/girl_straight.png differ diff --git a/hairline_texture_high/girl_wave.png b/hairline_texture_high/girl_wave.png index 2b4a400..117fe9f 100644 Binary files a/hairline_texture_high/girl_wave.png and b/hairline_texture_high/girl_wave.png differ diff --git a/hairline_texture_high/man_ inverse_arc.png b/hairline_texture_high/man_ inverse_arc.png index 28625ef..c9cba4b 100644 Binary files a/hairline_texture_high/man_ inverse_arc.png and b/hairline_texture_high/man_ inverse_arc.png differ diff --git a/hairline_texture_high/man_Soft petal.png b/hairline_texture_high/man_Soft petal.png new file mode 100644 index 0000000..718bd72 Binary files /dev/null and b/hairline_texture_high/man_Soft petal.png differ diff --git a/hairline_texture_high/man_ellipse.png b/hairline_texture_high/man_ellipse.png index c08eb69..905aa14 100644 Binary files a/hairline_texture_high/man_ellipse.png and b/hairline_texture_high/man_ellipse.png differ diff --git a/hairline_texture_high/man_heart.png b/hairline_texture_high/man_heart.png new file mode 100644 index 0000000..3c04f25 Binary files /dev/null and b/hairline_texture_high/man_heart.png differ diff --git a/hairline_texture_high/man_m.png b/hairline_texture_high/man_m.png index 099b1fc..e8ce0de 100644 Binary files a/hairline_texture_high/man_m.png and b/hairline_texture_high/man_m.png differ diff --git a/hairline_texture_high/man_straight.png b/hairline_texture_high/man_straight.png index 7f53ebc..0c542df 100644 Binary files a/hairline_texture_high/man_straight.png and b/hairline_texture_high/man_straight.png differ diff --git a/hairline_texture_low/girl_bigflower.png b/hairline_texture_low/girl_bigflower.png new file mode 100644 index 0000000..149c7e7 Binary files /dev/null and b/hairline_texture_low/girl_bigflower.png differ diff --git a/hairline_texture_low/girl_clasicalflower.png b/hairline_texture_low/girl_clasicalflower.png new file mode 100644 index 0000000..90f2980 Binary files /dev/null and b/hairline_texture_low/girl_clasicalflower.png differ diff --git a/hairline_texture_low/girl_ellipse.png b/hairline_texture_low/girl_ellipse.png index 1274860..024573f 100644 Binary files a/hairline_texture_low/girl_ellipse.png and b/hairline_texture_low/girl_ellipse.png differ diff --git a/hairline_texture_low/girl_flower.png b/hairline_texture_low/girl_flower.png index b1cba19..062cc9d 100644 Binary files a/hairline_texture_low/girl_flower.png and b/hairline_texture_low/girl_flower.png differ diff --git a/hairline_texture_low/girl_heart.png b/hairline_texture_low/girl_heart.png index 4efb3a8..325a817 100644 Binary files a/hairline_texture_low/girl_heart.png and b/hairline_texture_low/girl_heart.png differ diff --git a/hairline_texture_low/girl_straight.png b/hairline_texture_low/girl_straight.png index 7bb84e4..eeda3a8 100644 Binary files a/hairline_texture_low/girl_straight.png and b/hairline_texture_low/girl_straight.png differ diff --git a/hairline_texture_low/girl_wave.png b/hairline_texture_low/girl_wave.png index aed9123..0914255 100644 Binary files a/hairline_texture_low/girl_wave.png and b/hairline_texture_low/girl_wave.png differ diff --git a/hairline_texture_low/man_ inverse_arc.png b/hairline_texture_low/man_ inverse_arc.png index 7575fc0..10f7c17 100644 Binary files a/hairline_texture_low/man_ inverse_arc.png and b/hairline_texture_low/man_ inverse_arc.png differ diff --git a/hairline_texture_low/man_Soft petal.png b/hairline_texture_low/man_Soft petal.png new file mode 100644 index 0000000..dcf0973 Binary files /dev/null and b/hairline_texture_low/man_Soft petal.png differ diff --git a/hairline_texture_low/man_ellipse.png b/hairline_texture_low/man_ellipse.png index 9e21265..40b7531 100644 Binary files a/hairline_texture_low/man_ellipse.png and b/hairline_texture_low/man_ellipse.png differ diff --git a/hairline_texture_low/man_heart.png b/hairline_texture_low/man_heart.png new file mode 100644 index 0000000..e0a0de7 Binary files /dev/null and b/hairline_texture_low/man_heart.png differ diff --git a/hairline_texture_low/man_m.png b/hairline_texture_low/man_m.png index b17ab54..12fc4c0 100644 Binary files a/hairline_texture_low/man_m.png and b/hairline_texture_low/man_m.png differ diff --git a/hairline_texture_low/man_straight.png b/hairline_texture_low/man_straight.png index e06c6a6..a49e34a 100644 Binary files a/hairline_texture_low/man_straight.png and b/hairline_texture_low/man_straight.png differ diff --git a/static/test_interface2.html b/static/test_interface2.html index 28ee1ad..a8b8f0a 100644 --- a/static/test_interface2.html +++ b/static/test_interface2.html @@ -252,12 +252,16 @@ const HAIR_STYLES = { { value:'3', label:'heart 心形' }, { value:'4', label:'straight 直线形' }, { value:'5', label:'wave 波浪形' }, + { value:'6', label:'bigflower 大花瓣型' }, + { value:'7', label:'clasicalflower 古典花瓣型' }, ], male: [ { value:'1', label:'ellipse 椭圆形' }, { value:'2', label:'inverse_arc 倒弧形' }, { value:'3', label:'m M形' }, { value:'4', label:'straight 直线形' }, + { value:'5', label:'heart 桃心形' }, + { value:'6', label:'Softpetal 柔和花瓣形' }, ] }; diff --git a/static/test_interface5.html b/static/test_interface5.html index bf4d2d1..08b6f3d 100644 --- a/static/test_interface5.html +++ b/static/test_interface5.html @@ -101,7 +101,7 @@