feat: 拆分接口11/12,新增接口12 final / final v2 精简重绘端点

- 接口11 移除重绘,仅生成 final;接口12 (grow_v2) 负责发际线带 Flux-2 重绘
- 接口12 重绘带改为发际线外推 band_lo_mult~band_hi_mult 倍 push(默认 0.5~1.5),页面可调
- 接口12 同时产出 A 整帧重绘 与 B 局部加发+全脸美颜(beauty_alpha 可调)
- 新增 grow_v2_final(整帧重绘)/ grow_v2_final_v2(B 局部+美颜)端点:仅需图片+发型 ID,其余用固化默认值(color_match 关闭)
- 配套精简测试页 test_interface12_final.html / test_interface12_final_v2.html / test_interface12.html
- 恢复接口11 调试页多频段与换发型可调参数、color_match 默认不勾选
- 删除旧脚本 batch_grow_v2.py / gen_report_hairline_v2.py / test_simple.html

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
xsl
2026-07-16 00:04:36 +08:00
co-authored by Cursor
parent 4001df2c34
commit a208fe88ec
26 changed files with 1332 additions and 728 deletions
+235 -121
View File
@@ -299,26 +299,42 @@ def _pushed_mask(hair_mask, upper, baseline_pts, push_px, rid="",
def _redraw_band_mask(inner_pts, outer_pts, h, w, rid=""):
"""重绘带遮罩:把发际线(①-f 内轮廓 inner_pts)和外推发际线(①-g outer_pts
两条折线的端点连接成闭合多边形,填充得到带状区域,作为重绘 mask。
def _redraw_band_mask(inner_pts, outer_pts, h, w, rid="", upper=None,
lo_mult=0.5, hi_mult=1.5):
"""重绘带遮罩:由发际线(①-f 内轮廓 inner_pts)沿径向外推方向,取
`lo_mult × push` 与 `hi_mult × push` 两条外推线之间的带状区域作为重绘 mask。
inner_pts / outer_pts 是一一对应的有序点列(outer = inner 径向外推 push_px),
闭合环 = inner_pts(正向)+ outer_pts(反向)首尾相接。带只覆盖「现有头发下沿
到外推线」这一段(在头发一侧),正好是发际线交界处需要重绘融合的窄带
inner_pts / outer_pts 是一一对应的有序点列(outer = inner 径向外推 1.0×push_px),
每点的 1.0× 位移向量 = outer - inner;下界线 = inner + lo_mult×位移,
上界线 = inner + hi_mult×位移。闭合环 = 下界线(正向)+ 上界线(反向)首尾相接
lo_mult / hi_mult:外推倍率(相对 hairline_push_cm)。默认 0.5 / 1.5,即带位于
0.5×push ~ 1.5×push 之间(以内轮廓为 0×、原外推线为 1.0×)。
upper:①-a baseline 以上区域布尔掩码。传入时把重绘带与它求交集,只保留 baseline
以上的部分(两侧鬓角落到 baseline 以下的段会被截掉)。
返回 band_bool。
"""
lg = lambda msg: logger.info("[%s] %s", rid, msg) if rid else None
if len(inner_pts) < 2 or len(outer_pts) < 2:
return np.zeros((h, w), dtype=bool)
# 闭合多边形:内轮廓正向 + 外推线反向,端点自然相连
ring = np.vstack([inner_pts.astype(np.int32), outer_pts[::-1].astype(np.int32)])
inner_f = np.asarray(inner_pts, dtype=np.float32)
outer_f = np.asarray(outer_pts, dtype=np.float32)
disp = outer_f - inner_f # 每点 1.0×push 的径向位移向量
lo_line = inner_f + float(lo_mult) * disp # 下界外推线(lo_mult×push
hi_line = inner_f + float(hi_mult) * disp # 上界外推线(hi_mult×push
# 闭合多边形:下界线正向 + 上界线反向,端点自然相连
ring = np.vstack([lo_line.astype(np.int32), hi_line[::-1].astype(np.int32)])
band_u8 = np.zeros((h, w), dtype=np.uint8)
cv2.fillPoly(band_u8, [ring], 255)
band = band_u8 > 0
lg(f"_redraw_band_mask: 内轮廓点={len(inner_pts)} 外推点={len(outer_pts)} "
f"band像素={int(band.sum())}")
raw_px = int(band.sum())
# ①-a baseline 截断:只保留 baseline 以上的重绘带
if upper is not None:
band = band & upper
lg(f"_redraw_band_mask: 内轮廓点={len(inner_pts)} lo_mult={lo_mult} hi_mult={hi_mult} "
f"band像素(截断前)={raw_px} band像素(截断后)={int(band.sum())}")
return band
@@ -436,6 +452,8 @@ def compute_mask(image_bgr, landmarks, seg_model, mask_type, erode_cm, px_per_cm
# 重绘带用原始数据:内轮廓点 + 外推点(供 _redraw_band_mask 连端点成带)
viz["_inner_pts"] = inner_pts
viz["_outer_pts"] = outer_pts
# baseline 以上区域,供重绘带按 ①-a baseline 截断(只留上面)
viz["_upper_mask"] = upper
# 记录 viz 各字段是否非空(长度),便于排查前端取不到图的问题
viz_summary = {k: (len(v) if isinstance(v, str) and v else 0)
for k, v in viz.items() if k.endswith("_base64")}
@@ -804,47 +822,27 @@ 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",
erode_cm=0.6, swap_mode="ext_mask",
edge_erode_px=3,
denoising_strength=0.6, gen_backend="swaphair",
hairgrow_strength=0.75, mb_levels=5,
hairline_push_cm=1.0, hairline_edge="column",
blend_method="multiband", color_match=True,
color_match_strength=1.0, mb_feather_px=1,
transition_band_px=-1, redraw=False,
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0,
comfyui_prompt=None, rid=None):
"""接口11 完整管线。返回可直接进 ok() 的 data dict。未检出人脸抛 NoFaceError。
def _grow_core(image_bgr, hairline_id, *, is_hr, seg_model, erode_cm, swap_mode,
edge_erode_px, denoising_strength, gen_backend, hairgrow_strength,
mb_levels, hairline_push_cm, hairline_edge, blend_method, color_match,
color_match_strength, mb_feather_px, transition_band_px,
inpainting_fill, mask_blur, mask_dilate_scale, rid):
"""接口11 共享核心:遮罩(pushed)→生成→硬贴回→接缝融合,产出 ④ final。
遮罩算法固定为 pushed(发际线外推)。
融合算法 blend_method 默认 multiband(多频段金字塔),可选 seamless(泊松)/
two_stage(泊松→多频段两段式)/feather(羽化)/alpha_gradient(距离变换)。
color_match 默认开启 Reinhard 颜色迁移消除整体色差(对 multiband/feather 有效)
redraw=True 时额外跑一条「发际线带重绘」分支:
重绘区域 = 外推发际线↔内推发际线之间的带(以原内轮廓为中心,向头发/脸各推 push_cm)。
输入图 + 融合基底都用 final(④接缝融合最终图)。两路后端对比:
② swapHair 路(final+band 重绘→final 融合)
③ Flux-2 路(final+band 调 ComfyUI 保色重绘→final 融合)
结果在 steps.redraw_a/redraw_c 单独展示,不替换 final。
comfyui_promptFlux-2 路提示词,None 用默认「补充遮罩区域的头发,加一点美颜」。
inpainting_fill/mask_blur/mask_dilate_scale:透传 change_hair 服务端重绘参数(默认值
=服务端原始硬编码值,未传行为不变)。inpainting_fill=0 保留原图可治"染绿"
rid: 调用方的 request id,用于日志关联。为 None 时自动生成。
不做任何重绘。返回中间产物 dict(供接口11 构造响应、接口12 取 final+重绘带用):
final / swap_result / hard_paste / alpha / mask_bool / mask_viz /
px_per_cm / t_mask / t_swap / t_blend / h / w
未检出人脸抛 NoFaceError
"""
mask_type = "pushed" # 固定:只支持 pushed 遮罩算法
# blend_method 由参数传入(默认 multiband,接口11 可覆盖)
if rid is None:
rid = uuid4().hex[:8]
logger.info("[%s] ===== generate_hairline_grow 开始 =====", rid)
logger.info("[%s] 参数(固定 mask=pushed): erode_cm=%s hairline_push_cm=%s hairline_edge=%r "
"mb_levels=%s seg=%s gen_backend=%s swap_mode=%s blend=%s color_match=%s "
"cm_strength=%s mb_feather_px=%s transition_band_px=%s redraw=%s "
"inpainting_fill=%s mask_blur=%s mask_dilate_scale=%s comfyui_prompt=%r",
logger.info("[%s] _grow_core 参数(固定 mask=pushed): erode_cm=%s hairline_push_cm=%s "
"hairline_edge=%r mb_levels=%s seg=%s gen_backend=%s swap_mode=%s blend=%s "
"color_match=%s cm_strength=%s mb_feather_px=%s transition_band_px=%s "
"inpainting_fill=%s mask_blur=%s mask_dilate_scale=%s",
rid, erode_cm, hairline_push_cm, hairline_edge, mb_levels,
seg_model, gen_backend, swap_mode, blend_method, color_match,
color_match_strength, mb_feather_px, transition_band_px, redraw,
inpainting_fill, mask_blur, mask_dilate_scale, comfyui_prompt)
color_match_strength, mb_feather_px, transition_band_px,
inpainting_fill, mask_blur, mask_dilate_scale)
h, w = image_bgr.shape[:2]
landmarks = detector.detect(image_bgr)
if landmarks is None:
@@ -876,7 +874,7 @@ 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:接缝融合(默认 multiband)
# 步骤4:接缝融合(默认 multiband)→ ④ final
t0 = time.time()
final, alpha = _composite(
image_bgr, swap_result, mask_bool, blend_method, 0, edge_erode_px,
@@ -885,69 +883,54 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo
mb_feather_px=mb_feather_px, transition_band_px=transition_band_px)
t_blend = time.time() - t0
# 步骤5(可选):发际线带重绘分支 —— 开关 redraw=True 时执行,结果单独展示。
# 重绘区域 = 外推发际线↔内推发际线之间的带(以原内轮廓为中心,向头发/脸各推 push_cm)。
# 输入图 + 融合基底都用 final(④接缝融合最终图)。
redraw_viz = {
"redraw_band_overlay_base64": "",
"redraw_a_base64": "",
"redraw_c_base64": "",
return {
"final": final, "swap_result": swap_result, "hard_paste": hard_paste,
"alpha": alpha, "mask_bool": mask_bool, "mask_viz": mask_viz,
"px_per_cm": px_per_cm, "t_mask": t_mask, "t_swap": t_swap, "t_blend": t_blend,
"h": h, "w": w,
}
redraw_info = {"enabled": False}
if redraw:
t0 = time.time()
logger.info("[%s] 步骤5 发际线带重绘 开始", rid)
# ① 算重绘带:发际线(内轮廓)↔外推发际线 两条折线端点相连组成的带
inner_pts = mask_viz.get("_inner_pts")
outer_pts = mask_viz.get("_outer_pts")
push_px = int(round(max(0.0, hairline_push_cm) * px_per_cm))
try:
band_mask = _redraw_band_mask(inner_pts, outer_pts, h, w, rid=rid)
if band_mask.sum() < 30:
raise RuntimeError("重绘带像素过少,可能内轮廓/外推线缺失")
logger.info("[%s] 步骤5 重绘带 push_px=%d band_pixels=%d",
rid, push_px, int(band_mask.sum()))
redraw_viz["redraw_band_overlay_base64"] = _jpg_b64(
_overlay(final, band_mask, (255, 0, 255)))
redraw_info = {"enabled": True, "band_pixels": int(band_mask.sum()),
"push_px": push_px}
except Exception as ex: # noqa: BLE001
logger.exception("[%s] 步骤5 重绘带计算失败,整个重绘跳过", rid)
redraw_info = {"enabled": False, "error": f"band: {ex}"}
# ② swapHair 路:final + band 作 ext_mask 重绘 → final 作基底融合
if redraw_info.get("enabled"):
try:
redraw_a_raw = _call_swap(final, hairline_id, is_hr, band_mask, denoising_strength,
inpainting_fill=inpainting_fill, mask_blur=mask_blur,
mask_dilate_scale=mask_dilate_scale)
final_a, _ = _composite(
final, redraw_a_raw, band_mask, blend_method, 0, edge_erode_px,
color_match=color_match, mb_levels=mb_levels,
color_match_strength=color_match_strength,
mb_feather_px=mb_feather_px, transition_band_px=transition_band_px)
redraw_viz["redraw_a_base64"] = _jpg_b64(final_a)
logger.info("[%s] 步骤5 swapHair路完成", rid)
except Exception as ex: # noqa: BLE001
logger.warning("[%s] 步骤5 swapHair路失败,跳过: %s", rid, ex)
redraw_info["swap_error"] = str(ex)
# ③ Flux-2 路:final + band 调 ComfyUIreference latent 保色,不染绿)→ final 融合
try:
prompt = comfyui_prompt if comfyui_prompt else "补充遮罩区域的头发,加一点美颜"
redraw_c_raw = _call_comfyui(final, band_mask, prompt=prompt)
final_c, _ = _composite(
final, redraw_c_raw, band_mask, blend_method, 0, edge_erode_px,
color_match=color_match, mb_levels=mb_levels,
color_match_strength=color_match_strength,
mb_feather_px=mb_feather_px, transition_band_px=transition_band_px)
redraw_viz["redraw_c_base64"] = _jpg_b64(final_c)
logger.info("[%s] 步骤5 Flux-2路完成", rid)
except Exception as ex: # noqa: BLE001
logger.warning("[%s] 步骤5 Flux-2路失败,跳过: %s", rid, ex)
redraw_info["c_error"] = str(ex)
# ---------------------------------------------------------------------------
# 主入口
# ---------------------------------------------------------------------------
logger.info("[%s] 步骤5 发际线带重绘完成 耗时=%dms", rid, int((time.time()-t0)*1000))
def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segformer",
erode_cm=0.6, swap_mode="ext_mask",
edge_erode_px=3,
denoising_strength=0.6, gen_backend="swaphair",
hairgrow_strength=0.75, mb_levels=5,
hairline_push_cm=1.0, hairline_edge="column",
blend_method="multiband", color_match=True,
color_match_strength=1.0, mb_feather_px=1,
transition_band_px=-1,
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0,
rid=None):
"""接口11 完整管线(**不含重绘**,重绘见接口12 generate_hairline_redraw)。
返回可直接进 ok() 的 data dict。未检出人脸抛 NoFaceError。
遮罩算法固定为 pushed(发际线外推)。
融合算法 blend_method 默认 multiband(多频段金字塔),可选 seamless(泊松)/
two_stage(泊松→多频段两段式)/feather(羽化)/alpha_gradient(距离变换)。
color_match 默认开启 Reinhard 颜色迁移消除整体色差(对 multiband/feather 有效)。
inpainting_fill/mask_blur/mask_dilate_scale:透传 change_hair 服务端换发型重绘参数。
rid: 调用方的 request id,用于日志关联。为 None 时自动生成。
"""
if rid is None:
rid = uuid4().hex[:8]
logger.info("[%s] ===== generate_hairline_grow 开始 =====", rid)
core = _grow_core(
image_bgr, 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, blend_method=blend_method,
color_match=color_match, color_match_strength=color_match_strength,
mb_feather_px=mb_feather_px, transition_band_px=transition_band_px,
inpainting_fill=inpainting_fill, mask_blur=mask_blur,
mask_dilate_scale=mask_dilate_scale, rid=rid)
mask_viz = core["mask_viz"]
alpha = core["alpha"]
w, h = core["w"], core["h"]
data = {
"hairline_id": hairline_id,
@@ -955,7 +938,7 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo
"hairgrow_strength": round(float(hairgrow_strength), 3),
"is_hr": is_hr,
"seg_model": seg_model,
"mask_type": mask_type,
"mask_type": "pushed",
"erode_cm": round(float(erode_cm), 2),
"swap_mode": swap_mode,
"blend_method": blend_method,
@@ -971,16 +954,16 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo
"inpainting_fill": int(inpainting_fill),
"mask_blur": int(mask_blur),
"mask_dilate_scale": round(float(mask_dilate_scale), 3),
"px_per_cm": round(float(px_per_cm), 4),
"px_per_cm": round(float(core["px_per_cm"]), 4),
"erode_px": mask_viz["erode_px"],
"hair_pixels": mask_viz["hair_pixels"],
"closed_pixels": mask_viz["closed_pixels"],
"mask_pixels": mask_viz["mask_pixels"],
"image_size": {"width": w, "height": h},
"timings_ms": {
"mask": int(t_mask * 1000),
"swap": int(t_swap * 1000),
"blend": int(t_blend * 1000),
"mask": int(core["t_mask"] * 1000),
"swap": int(core["t_swap"] * 1000),
"blend": int(core["t_blend"] * 1000),
},
"steps": {
"input_base64": _jpg_b64(image_bgr),
@@ -996,21 +979,152 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo
# 最终遮罩
"mask_overlay_base64": mask_viz["mask_overlay_base64"],
"mask_base64": mask_viz["mask_base64"],
"swap_raw_base64": _jpg_b64(swap_result),
"hard_paste_base64": _jpg_b64(hard_paste),
"swap_raw_base64": _jpg_b64(core["swap_result"]),
"hard_paste_base64": _jpg_b64(core["hard_paste"]),
"alpha_base64": _gray_b64(alpha) if alpha is not None else mask_viz["mask_base64"],
"final_base64": _jpg_b64(final),
# 步骤5(可选):发际线带重绘分支 —— redraw=False 时为空串
"redraw_band_overlay_base64": redraw_viz["redraw_band_overlay_base64"],
"redraw_a_base64": redraw_viz["redraw_a_base64"],
"redraw_c_base64": redraw_viz["redraw_c_base64"],
"final_base64": _jpg_b64(core["final"]),
},
"redraw": redraw_info,
"_rid": rid, # 调用方的 request id,用于日志关联。为 None 时自动生成。
"_rid": rid,
}
# 记录 steps 各图字段是否非空,供排查前端取图问题
steps_summary = {k: (len(v) if isinstance(v, str) and v else 0)
for k, v in data["steps"].items() if k.endswith("_base64")}
logger.info("[%s] 返回 steps 字段长度: %s", rid, steps_summary)
logger.info("[%s] ===== generate_hairline_grow 完成 =====", rid)
return data
def generate_hairline_redraw(image_bgr, hairline_id, is_hr=False, seg_model="segformer",
erode_cm=0.6, swap_mode="ext_mask",
edge_erode_px=3,
denoising_strength=0.6, gen_backend="swaphair",
hairgrow_strength=0.75, mb_levels=5,
hairline_push_cm=1.0, hairline_edge="column",
blend_method="multiband", color_match=True,
color_match_strength=1.0, mb_feather_px=1,
transition_band_px=-1,
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0,
comfyui_prompt=None, beauty_alpha=0.6,
band_lo_mult=0.5, band_hi_mult=1.5, rid=None):
"""接口12 发际线带重绘。内部先跑接口11 核心拿到 ④ final,再取 ⑤-① 发际线重绘带
(外推↔内推之间、经 baseline 截断只留上部)作遮罩,用 Flux-2ComfyUIhair_repaint.json
重绘(band 经 alpha 送进 ComfyUI 决定加发位置,ComfyUI 输出为整帧重绘+美颜图)。
**同时产出两版结果供对比**:
- `redraw_full`ComfyUI 整帧输出(全脸美颜 + 全脸重绘),与手动跑 ComfyUI 一致。
- `redraw_band`:加发只在发际线带、美颜保留全脸。band 内完全用 ComfyUI 重绘(加发),
band 外用 `final` 结构 + 按 `beauty_alpha` 融入 ComfyUI 的全脸美颜。
返回可直接进 ok() 的 data dict。未检出人脸抛 NoFaceError。
comfyui_promptFlux-2 提示词,None 用默认「补充遮罩区域的头发,加一点美颜」。
beauty_alpharedraw_band 版 band 外的全脸美颜融入强度(0=完全保留 final 无美颜,
1=band 外也完全用 ComfyUI 输出≈redraw_full),默认 0.6。
band_lo_mult / band_hi_mult:重绘带外推倍率(相对 hairline_push_cm),带位于
lo×push ~ hi×push 之间(内轮廓=0×、原外推线=1.0×),默认 0.5 / 1.5。
其余参数含义与接口11 相同(用于内部生成 final 与重绘带)。
"""
if rid is None:
rid = uuid4().hex[:8]
logger.info("[%s] ===== generate_hairline_redraw 开始 =====", rid)
core = _grow_core(
image_bgr, 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, blend_method=blend_method,
color_match=color_match, color_match_strength=color_match_strength,
mb_feather_px=mb_feather_px, transition_band_px=transition_band_px,
inpainting_fill=inpainting_fill, mask_blur=mask_blur,
mask_dilate_scale=mask_dilate_scale, rid=rid)
final = core["final"]
mask_viz = core["mask_viz"]
w, h = core["w"], core["h"]
px_per_cm = core["px_per_cm"]
# ① 算重绘带(⑤-①):发际线(内轮廓)↔外推发际线成带,经 baseline 截断只留上部
t0 = time.time()
inner_pts = mask_viz.get("_inner_pts")
outer_pts = mask_viz.get("_outer_pts")
upper_mask = mask_viz.get("_upper_mask")
push_px = int(round(max(0.0, hairline_push_cm) * px_per_cm))
redraw_band_overlay_b64 = ""
redraw_full_b64 = "" # A:ComfyUI 整帧(全脸美颜+全脸重绘)
redraw_band_b64 = "" # B:加发只在发际线带、美颜保留全脸
redraw_info = {"enabled": False}
band_mask = None
try:
band_mask = _redraw_band_mask(inner_pts, outer_pts, h, w, rid=rid, upper=upper_mask,
lo_mult=band_lo_mult, hi_mult=band_hi_mult)
if band_mask.sum() < 30:
raise RuntimeError("重绘带像素过少,可能内轮廓/外推线缺失")
logger.info("[%s] 重绘带 push_px=%d lo_mult=%s hi_mult=%s band_pixels=%d",
rid, push_px, band_lo_mult, band_hi_mult, int(band_mask.sum()))
redraw_band_overlay_b64 = _jpg_b64(_overlay(final, band_mask, (255, 0, 255)))
redraw_info = {"enabled": True, "band_pixels": int(band_mask.sum()), "push_px": push_px,
"band_lo_mult": float(band_lo_mult), "band_hi_mult": float(band_hi_mult)}
except Exception as ex: # noqa: BLE001
logger.exception("[%s] 重绘带计算失败,整个重绘跳过", rid)
redraw_info = {"enabled": False, "error": f"band: {ex}"}
# ② Flux-2 路:final + band 调 ComfyUIhair_repaint.json 整图重绘 + reference latent
# 保色 + ColorMatch + 美颜)。band 经 alpha 送进 ComfyUI 决定加发位置。
beauty_alpha = float(min(max(beauty_alpha, 0.0), 1.0))
if redraw_info.get("enabled"):
try:
prompt = comfyui_prompt if comfyui_prompt else "补充遮罩区域的头发,加一点美颜"
redraw_c_raw = _call_comfyui(final, band_mask, prompt=prompt)
# A:整帧输出(与手动跑 ComfyUI 一致)
redraw_full_b64 = _jpg_b64(redraw_c_raw)
# B:加发只在 band、美颜保留全脸。
# alpha = band 内 1(羽化边缘);band 外 = beauty_alpha。
# band 内完全用 ComfyUI(加发);band 外用 final 结构 + beauty_alpha 融入全脸美颜。
feather_px = max(8, int(round(push_px * 0.6)))
band_a = _feather_alpha(band_mask, "feather", feather_px, 0) # 0..1band 外=0
a = band_a + (1.0 - band_a) * beauty_alpha
a3 = a[:, :, None]
band_mix = (final.astype(np.float32) * (1.0 - a3)
+ redraw_c_raw.astype(np.float32) * a3)
redraw_band_b64 = _jpg_b64(np.clip(band_mix, 0, 255).astype(np.uint8))
redraw_info["beauty_alpha"] = beauty_alpha
logger.info("[%s] Flux-2 重绘完成:redraw_full(整帧) + redraw_band(局部加发+全脸美颜 beauty_alpha=%.2f)",
rid, beauty_alpha)
except Exception as ex: # noqa: BLE001
logger.warning("[%s] Flux-2 路重绘失败,跳过: %s", rid, ex)
redraw_info["c_error"] = str(ex)
t_redraw = time.time() - t0
data = {
"hairline_id": hairline_id,
"blend_method": blend_method,
"hairline_push_cm": round(float(hairline_push_cm), 2),
"comfyui_prompt": comfyui_prompt or "补充遮罩区域的头发,加一点美颜",
"beauty_alpha": beauty_alpha,
"px_per_cm": round(float(px_per_cm), 4),
"mask_pixels": mask_viz["mask_pixels"],
"image_size": {"width": w, "height": h},
"timings_ms": {
"mask": int(core["t_mask"] * 1000),
"swap": int(core["t_swap"] * 1000),
"blend": int(core["t_blend"] * 1000),
"redraw": int(t_redraw * 1000),
},
"steps": {
"input_base64": _jpg_b64(image_bgr),
# 接口11 的 ④ final —— 作为本接口的重绘输入基底
"final_base64": _jpg_b64(final),
# ⑤-① 发际线重绘带(紫,已按 baseline 截断只留上部)
"redraw_band_overlay_base64": redraw_band_overlay_b64,
# A:ComfyUI 整帧重绘+美颜(与手动跑 ComfyUI 一致)
"redraw_full_base64": redraw_full_b64,
# B:加发只在发际线带、美颜保留全脸
"redraw_band_base64": redraw_band_b64,
# 兼容旧字段:指向 A(整帧版)
"redraw_c_base64": redraw_full_b64,
},
"redraw": redraw_info,
"_rid": rid,
}
steps_summary = {k: (len(v) if isinstance(v, str) and v else 0)
for k, v in data["steps"].items() if k.endswith("_base64")}
logger.info("[%s] 返回 steps 字段长度: %s", rid, steps_summary)
logger.info("[%s] ===== generate_hairline_redraw 完成 =====", rid)
return data