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:
+235
-121
@@ -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_prompt:Flux-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 调 ComfyUI(reference 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-2(ComfyUI,hair_repaint.json)
|
||||
重绘(band 经 alpha 送进 ComfyUI 决定加发位置,ComfyUI 输出为整帧重绘+美颜图)。
|
||||
|
||||
**同时产出两版结果供对比**:
|
||||
- `redraw_full`:ComfyUI 整帧输出(全脸美颜 + 全脸重绘),与手动跑 ComfyUI 一致。
|
||||
- `redraw_band`:加发只在发际线带、美颜保留全脸。band 内完全用 ComfyUI 重绘(加发),
|
||||
band 外用 `final` 结构 + 按 `beauty_alpha` 融入 ComfyUI 的全脸美颜。
|
||||
|
||||
返回可直接进 ok() 的 data dict。未检出人脸抛 NoFaceError。
|
||||
|
||||
comfyui_prompt:Flux-2 提示词,None 用默认「补充遮罩区域的头发,加一点美颜」。
|
||||
beauty_alpha:redraw_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 调 ComfyUI(hair_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..1,band 外=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
|
||||
|
||||
Reference in New Issue
Block a user