diff --git a/app.py b/app.py index 940b396..82c27db 100644 --- a/app.py +++ b/app.py @@ -1297,19 +1297,15 @@ async def hairline_grow( 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(闭合区域) | pushed(发际线外推)(默认 eroded)"), - erode_cm: float = Form(default=1.2, description="遮罩外缘朝中心151内缩距离(厘米,同接口9),默认 1.2"), - hairline_push_cm: float = Form(default=1.0, description="发际线外推距离(厘米,往头发方向推进;仅 mask_type=pushed 生效),默认 1.0"), - hairline_edge: str = Form(default="column", description="发际线提取方式:column(逐列最低点) | contour(形态学轮廓)(仅 pushed 生效),默认 column"), - 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(泊松无缝) | multiband(多频段金字塔)(默认 feather)"), - feather_px: int = Form(default=15, description="羽化/渐变过渡宽度(像素),默认 15"), + erode_cm: float = Form(default=0.6, description="baseline 参考内缩距离(厘米),默认 0.6"), + hairline_push_cm: float = Form(default=1.0, description="发际线外推距离(厘米,往头发方向推进),默认 1.0"), + hairline_edge: str = Form(default="column", description="发际线提取方式:column | contour,默认 column"), + swap_mode: str = Form(default="ext_mask", description="换发型取图模式:ext_mask | as_is(默认 ext_mask)"), edge_erode_px: int = Form(default=3, description="贴图前遮罩内缩像素(防边缘露皮/光晕),默认 3"), denoising_strength: float = Form(default=0.6, description="换发型 webui 重绘强度(越大生发越激进),默认 0.6"), - color_match: bool = Form(default=False, description="融合前对生成图做 Reinhard 颜色校正(消除整体色差,seamless 下自动跳过),默认 false"), - mb_levels: int = Form(default=5, description="multiband 多频段金字塔层数(2~6,越大低频色差抹得越宽,仅 multiband 生效),默认 5"), + mb_levels: int = Form(default=5, description="多频段金字塔层数(2~6,越大低频色差抹得越宽),默认 5"), ): - """接口11:发际线生发 + 分步可视化""" + """接口11:发际线生发 + 分步可视化。遮罩固定 pushed、融合固定 multiband。""" raw, e = await resolve_image_bytes(image_file, image_url, image_base64) if e is not None: return e @@ -1323,14 +1319,16 @@ async def hairline_grow( from face_analysis.hairline_grow import generate_hairline_grow, NoFaceError, SwapError from uuid import uuid4 as _uuid4 rid = _uuid4().hex[:8] - logger.info("[%s] 接口11 收到请求: mask_type=%s hairline_push_cm=%s hairline_edge=%s", - rid, mask_type, hairline_push_cm, hairline_edge) + logger.info("[%s] 接口11 收到请求: hairline_push_cm=%s hairline_edge=%s mb_levels=%s", + rid, hairline_push_cm, hairline_edge, mb_levels) 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, color_match, mb_levels, - hairline_push_cm=hairline_push_cm, hairline_edge=hairline_edge, rid=rid) + generate_hairline_grow, image, hairline_id, + is_hr=is_hr, seg_model=seg_model, erode_cm=erode_cm, swap_mode=swap_mode, + edge_erode_px=edge_erode_px, denoising_strength=denoising_strength, + gen_backend=gen_backend, hairgrow_strength=hairgrow_strength, + mb_levels=mb_levels, hairline_push_cm=hairline_push_cm, + hairline_edge=hairline_edge, rid=rid) except NoFaceError: return err(1001, "无法识别人像") except SwapError as se: @@ -1348,17 +1346,15 @@ async def hairline_grow( @app.post( "/api/v1/hairline/grow_v2", - summary="接口12 发际线生发(固定金字塔融合,仅返回最终图)", + summary="接口12 发际线生发(固定 pushed 遮罩 + 多频段融合,仅返回最终图)", tags=["生发"], description=f""" -接口11 的固定参数精简版,适合生产直调。与接口11 共用同一管线,区别仅在于: +接口11 的固定参数精简版,适合生产直调。与接口11 共用同一管线,遮罩固定 pushed(发际线外推), +融合固定 multiband(多频段金字塔)。本接口固定 `erode_cm=0.6`、`mb_levels=5` 不暴露, +**只返回 `final_base64`**(最终合成图),不附带分步可视化。 -- **固定** `blend_method=multiband`(多频段金字塔融合)、`mb_levels=5`、`erode_cm=0.6` - (外缘朝中心151 内缩 0.6cm)。这三项不可调,故本接口不暴露。 -- **返回值精简**:只返回 `final_base64`(最终合成图),不再附带接口11 的分步可视化。 - -其余参数(hairline_id、seg_model、gen_backend、is_hr、denoising_strength、color_match、 -edge_erode_px 等)仍保留为可选 Form,调用方可按需覆盖,未传则用接口11 同款默认值。 +其余参数(hairline_id、seg_model、gen_backend、is_hr、denoising_strength、edge_erode_px、 +hairline_push_cm、hairline_edge 等)保留为可选 Form,调用方可按需覆盖。 {_image_fields_desc} """, @@ -1372,16 +1368,13 @@ async def hairline_grow_v2( 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(闭合区域) | pushed(发际线外推)(默认 eroded)"), - swap_mode: str = Form(default="ext_mask", description="换发型取图模式:ext_mask(改造换发型用接口9遮罩) | as_is(不改换发型,贴回再裁)(默认 ext_mask)"), - feather_px: int = Form(default=15, description="羽化/渐变过渡宽度(像素,本接口固定 multiband 故不生效,仅留作兼容)"), + swap_mode: str = Form(default="ext_mask", description="换发型取图模式:ext_mask | as_is(默认 ext_mask)"), edge_erode_px: int = Form(default=3, description="贴图前遮罩内缩像素(防边缘露皮/光晕),默认 3"), denoising_strength: float = Form(default=0.6, description="换发型 webui 重绘强度(越大生发越激进),默认 0.6"), - color_match: bool = Form(default=False, description="融合前对生成图做 Reinhard 颜色校正(消除整体色差),默认 false"), - hairline_push_cm: float = Form(default=1.0, description="发际线外推距离(厘米,仅 mask_type=pushed 生效),默认 1.0"), - hairline_edge: str = Form(default="column", description="发际线提取方式:column | contour(仅 pushed 生效),默认 column"), + hairline_push_cm: float = Form(default=1.0, description="发际线外推距离(厘米),默认 1.0"), + hairline_edge: str = Form(default="column", description="发际线提取方式:column | contour,默认 column"), ): - """接口12:发际线生发(固定 multiband/mb_levels=5/erode_cm=0.6,仅返回最终图)。""" + """接口12:发际线生发(固定 pushed 遮罩 + multiband 融合,仅返回最终图)。""" raw, e = await resolve_image_bytes(image_file, image_url, image_base64) if e is not None: return e @@ -1394,12 +1387,13 @@ async def hairline_grow_v2( from fastapi.concurrency import run_in_threadpool from face_analysis.hairline_grow import generate_hairline_grow, NoFaceError, SwapError try: - # 固定三项:blend_method=multiband、mb_levels=5、erode_cm=0.6 + # 固定:mask_type=pushed、blend_method=multiband、erode_cm=0.6、mb_levels=5 data = await run_in_threadpool( - generate_hairline_grow, image, hairline_id, is_hr, seg_model, - mask_type, 0.6, swap_mode, "multiband", feather_px, edge_erode_px, - denoising_strength, gen_backend, hairgrow_strength, color_match, 5, - hairline_push_cm=hairline_push_cm, hairline_edge=hairline_edge) + generate_hairline_grow, image, hairline_id, + is_hr=is_hr, seg_model=seg_model, erode_cm=0.6, swap_mode=swap_mode, + edge_erode_px=edge_erode_px, denoising_strength=denoising_strength, + gen_backend=gen_backend, hairgrow_strength=hairgrow_strength, + mb_levels=5, hairline_push_cm=hairline_push_cm, hairline_edge=hairline_edge) except NoFaceError: return err(1001, "无法识别人像") except SwapError as se: diff --git a/docs/发际线生发遮罩算法_pushed模式.md b/docs/发际线生发遮罩算法_pushed模式.md index 9b4d0ea..5f133ad 100644 --- a/docs/发际线生发遮罩算法_pushed模式.md +++ b/docs/发际线生发遮罩算法_pushed模式.md @@ -1,11 +1,14 @@ # 发际线生发遮罩算法(pushed 模式) -> 对应接口11 `/api/v1/hairline/grow`、接口12 `/api/v1/hairline/grow_v2`,`mask_type=pushed`。 +> 对应接口11 `/api/v1/hairline/grow`、接口12 `/api/v1/hairline/grow_v2`。 +> 遮罩算法固定为 pushed,融合算法固定为 multiband(已移除其他选项)。 > 代码:`face_analysis/hairline_grow.py`(`_extract_hairline` / `_pushed_mask` / `compute_mask`)。 ## 概述 -pushed 模式是发际线生发的默认遮罩算法(接口11/12 的 `mask_type` 三选一:`eroded` / `closed` / `pushed`,当前只用 pushed)。它从头发分割结果中提取「头发/皮肤交界线」(发际线),以眉心为圆心逐点径向外推一段距离,与 baseline 组成闭合区域作为最终遮罩。这样遮罩顶部会覆盖现有头发下沿一小段,贴回生发结果时顶部与真头发重叠、过渡自然。 +pushed 是发际线生发的**唯一**遮罩算法,multiband(多频段金字塔)是**唯一**融合算法。它从头发分割结果中提取「头发/皮肤交界线」(发际线),以眉心为圆心逐点径向外推一段距离,与 baseline 组成闭合区域作为最终遮罩。这样遮罩顶部会覆盖现有头发下沿一小段,贴回生发结果时顶部与真头发重叠、过渡自然。 + +> 接口12 `/api/v1/hairline/grow_v2` 只需传 `image` + `hairline_id`,遮罩和融合全部固定,无需任何算法选择参数。 ## 算法流程(5 步) @@ -54,15 +57,18 @@ segformer(默认)或 bisenet 得到的头发二值掩码。 ## 关键参数 +遮罩算法(pushed)和融合算法(multiband)已固定,接口不再暴露选择参数。可调的只有: + | 参数 | 默认 | 说明 | |---|---|---| -| `mask_type` | `eroded`(接口默认)/ `pushed`(当前推荐) | pushed 走上述流程;eroded/closed 走旧的 top_fill→closed/eroded 流程 | | `hairline_push_cm` | 1.0 | 内轮廓径向外推距离(厘米),= push_px / px_per_cm。`px_per_cm` 由虹膜直径标定 | -| `hairline_edge` | `column` | 兼容保留的入参;新版内轮廓提取(轮廓+内侧判定)不再按它分支,取值不影响结果 | +| `hairline_edge` | `column` | 兼容保留的入参;内轮廓提取(轮廓+内侧判定)不再按它分支,取值不影响结果 | +| `mb_levels` | 5 | 多频段金字塔层数(2~6,越大低频色差抹得越宽)| +| `erode_cm` | 0.6(接口12 固定)| baseline 参考内缩距离,对 pushed 影响很小 | -## 与旧模式(eroded/closed)的区别 +## 与旧模式(eroded/closed,已移除)的区别 -| | eroded/closed | pushed(当前) | +| | eroded/closed(已移除) | pushed(当前唯一) | |---|---|---| | 遮罩来源 | top_fill(头发向下填充含额头)外缘内缩 | 头发内轮廓线 径向外推成带 | | 截断方式 | 用 baseline 组上半区 upper | 内侧判定 + 下颌 y 截断(不再用 baseline) | diff --git a/face_analysis/hairline_grow.py b/face_analysis/hairline_grow.py index 8764425..0e483b5 100644 --- a/face_analysis/hairline_grow.py +++ b/face_analysis/hairline_grow.py @@ -64,16 +64,14 @@ DEFAULTS = { "gen_backend": "swaphair", # swaphair(换发型LoRA) | hairgrow(区域生发inpaint) "is_hr": False, "seg_model": "segformer", # bisenet | segformer - "mask_type": "eroded", # eroded | closed - "erode_cm": 1.2, + "hairline_push_cm": 1.0, # 发际线径向外推距离(厘米) + "hairline_edge": "column", # column(逐列下沿) "swap_mode": "ext_mask", # ext_mask | as_is(仅 swaphair) "denoising_strength": 0.6, # 仅 swaphair "hairgrow_strength": 0.75, # 仅 hairgrow - "blend_method": "feather", # feather | alpha_gradient | seamless | multiband - "feather_px": 15, "edge_erode_px": 3, - "color_match": False, # True 时对生成图做 Reinhard 颜色校正(seamless 下自动跳过) "mb_levels": 5, # multiband 金字塔层数(2~6,越大色差抹得越宽) + "erode_cm": 0.6, # 接口12 固定值(pushed 模式下仅用于 baseline 截断参考,影响很小) } @@ -662,21 +660,25 @@ def _composite(orig, swap_result, mask_bool, blend_method, feather_px, edge_erod # --------------------------------------------------------------------------- def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segformer", - mask_type="eroded", erode_cm=1.2, swap_mode="ext_mask", - blend_method="feather", feather_px=15, edge_erode_px=3, + erode_cm=0.6, swap_mode="ext_mask", + edge_erode_px=3, denoising_strength=0.6, gen_backend="swaphair", - hairgrow_strength=0.75, color_match=False, mb_levels=5, - hairline_push_cm=0.0, hairline_edge="column", rid=None): + hairgrow_strength=0.75, mb_levels=5, + hairline_push_cm=1.0, hairline_edge="column", rid=None): """接口11 完整管线。返回可直接进 ok() 的 data dict。未检出人脸抛 NoFaceError。 - rid: 调用方的 request id,用于日志关联。为 None 时自动生成。 + 遮罩算法固定为 pushed(发际线外推),融合算法固定为 multiband(多频段金字塔), + 不再支持其他选项。rid: 调用方的 request id,用于日志关联。为 None 时自动生成。 """ + mask_type = "pushed" # 固定:只支持 pushed 遮罩算法 + blend_method = "multiband" # 固定:只支持 multiband 融合 if rid is None: rid = uuid4().hex[:8] logger.info("[%s] ===== generate_hairline_grow 开始 =====", rid) - logger.info("[%s] 参数: mask_type=%r erode_cm=%s blend=%s hairline_push_cm=%s hairline_edge=%r " - "seg=%s gen_backend=%s swap_mode=%s", rid, mask_type, erode_cm, blend_method, - hairline_push_cm, hairline_edge, seg_model, gen_backend, swap_mode) + logger.info("[%s] 参数(固定 mask=pushed blend=multiband): erode_cm=%s hairline_push_cm=%s " + "hairline_edge=%r mb_levels=%s seg=%s gen_backend=%s swap_mode=%s", + rid, erode_cm, hairline_push_cm, hairline_edge, mb_levels, + seg_model, gen_backend, swap_mode) h, w = image_bgr.shape[:2] landmarks = detector.detect(image_bgr) if landmarks is None: @@ -685,7 +687,7 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo px_per_cm = estimate_scale_factor(landmarks, w, h) logger.info("[%s] 人脸检出 px_per_cm=%.3f 图尺寸=%dx%d", rid, px_per_cm, w, h) - # 步骤1:接口9 遮罩 + # 步骤1:接口9 遮罩(固定 pushed) t0 = time.time() mask_bool, mask_viz = compute_mask( image_bgr, landmarks, seg_model, mask_type, erode_cm, px_per_cm, @@ -706,11 +708,11 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo hard_paste = image_bgr.copy() hard_paste[mask_bool] = swap_result[mask_bool] - # 步骤4:接缝融合 + # 步骤4:接缝融合(固定 multiband) t0 = time.time() final, alpha = _composite( - image_bgr, swap_result, mask_bool, blend_method, feather_px, edge_erode_px, - color_match=color_match, mb_levels=mb_levels) + image_bgr, swap_result, mask_bool, blend_method, 0, edge_erode_px, + color_match=False, mb_levels=mb_levels) t_blend = time.time() - t0 data = { @@ -723,9 +725,7 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo "erode_cm": round(float(erode_cm), 2), "swap_mode": swap_mode, "blend_method": blend_method, - "feather_px": int(feather_px), "edge_erode_px": int(edge_erode_px), - "color_match": bool(color_match) and blend_method != "seamless", "mb_levels": int(mb_levels), "hairline_push_cm": round(float(hairline_push_cm), 2), "hairline_edge": hairline_edge, diff --git a/static/test_interface11.html b/static/test_interface11.html index d0dc06f..fb7cc21 100644 --- a/static/test_interface11.html +++ b/static/test_interface11.html @@ -113,32 +113,23 @@