save code
This commit is contained in:
+281
-35
@@ -35,6 +35,7 @@ from face_analysis.detector import detector
|
||||
from face_analysis.calibration import estimate_scale_factor
|
||||
from face_analysis.head_mask import (
|
||||
NoFaceError,
|
||||
BASELINE_IDX,
|
||||
_baseline_points,
|
||||
_upper_region_mask,
|
||||
_bisenet_hair_mask,
|
||||
@@ -60,6 +61,10 @@ SWAP_URL = os.getenv("SWAP_HAIR_URL", "http://127.0.0.1:8801/api/swapHair/v1")
|
||||
HAIRGROW_URL = os.getenv("HAIR_GROW_URL", "http://127.0.0.1:8801/api/hairGrow/v1")
|
||||
SWAP_TIMEOUT = float(os.getenv("SWAP_HAIR_TIMEOUT", "300"))
|
||||
|
||||
# 多频段融合最细层羽化:羽化最细 FEATHER_LAYERS 层(每层核尺寸按尺度放大)。
|
||||
# 只羽最细1层效果极弱(其拉普拉斯系数幅度小),羽化 3 层才能明显软化发丝边缘锯齿。
|
||||
FEATHER_LAYERS = 3
|
||||
|
||||
DEFAULTS = {
|
||||
"gen_backend": "swaphair", # swaphair(换发型LoRA) | hairgrow(区域生发inpaint)
|
||||
"is_hr": False,
|
||||
@@ -294,6 +299,29 @@ 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。
|
||||
|
||||
inner_pts / outer_pts 是一一对应的有序点列(outer = inner 径向外推 push_px),
|
||||
故闭合环 = inner_pts(正向)+ outer_pts(反向)首尾相接。带只覆盖「现有头发下沿
|
||||
到外推线」这一段(在头发一侧),正好是发际线交界处需要重绘融合的窄带。
|
||||
|
||||
返回 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)])
|
||||
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())}")
|
||||
return band
|
||||
|
||||
|
||||
def compute_mask(image_bgr, landmarks, seg_model, mask_type, erode_cm, px_per_cm,
|
||||
hairline_push_cm=0.0, hairline_edge="column", rid=""):
|
||||
"""算出布尔遮罩 + 可视化。
|
||||
@@ -334,7 +362,9 @@ def compute_mask(image_bgr, landmarks, seg_model, mask_type, erode_cm, px_per_cm
|
||||
if mask_type == "pushed":
|
||||
push_px = int(round(max(0.0, hairline_push_cm) * px_per_cm))
|
||||
# 圆心 = 151 点(眉心)完整坐标,内侧判定与径向外推共用
|
||||
center = baseline_pts[5] if len(baseline_pts) > 5 else None
|
||||
# 按值查 151 在 BASELINE_IDX 中的位置,避免列表变动后索引错位(曾硬编码 [5])
|
||||
_idx151 = BASELINE_IDX.index(151) if 151 in BASELINE_IDX else -1
|
||||
center = baseline_pts[_idx151] if _idx151 >= 0 else None
|
||||
# 下颌截断线:下巴关键点 152 的 y(内轮廓两侧向下画到这里为止)
|
||||
try:
|
||||
chin_y = int(round(landmarks.landmark[152].y * h))
|
||||
@@ -394,15 +424,18 @@ def compute_mask(image_bgr, landmarks, seg_model, mask_type, erode_cm, px_per_cm
|
||||
# ①-g 外推:圆心红点(151) + 内轮廓(绿)+ 外推线(青)+ 遮罩(红半透明)
|
||||
ps_img = _draw_polyline(image_bgr.copy(), inner_pts, (0, 255, 0), 2)
|
||||
ps_img = _draw_polyline(ps_img, outer_pts, (0, 255, 255), 3)
|
||||
# 画圆心(151 点)红点,标示径向外推的中心
|
||||
if baseline_pts is not None and len(baseline_pts) > 5:
|
||||
cx151, cy151 = baseline_pts[5]
|
||||
# 画圆心(151 点)红点,标示径向外推的中心(_idx151 上方已按值查到)
|
||||
if center is not None:
|
||||
cx151, cy151 = center
|
||||
cv2.circle(ps_img, (cx151, cy151), 6, (0, 0, 255), -1, cv2.LINE_AA)
|
||||
cv2.putText(ps_img, "151", (cx151 + 8, cy151 - 8),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 1, cv2.LINE_AA)
|
||||
ps_img = _overlay(ps_img, mask_bool, (0, 0, 255), 0.3)
|
||||
viz["pushed_overlay_base64"] = _jpg_b64(ps_img)
|
||||
viz["push_px"] = push_px
|
||||
# 重绘带用原始数据:内轮廓点 + 外推点(供 _redraw_band_mask 连端点成带)
|
||||
viz["_inner_pts"] = inner_pts
|
||||
viz["_outer_pts"] = outer_pts
|
||||
# 记录 viz 各字段是否非空(长度),便于排查前端取不到图的问题
|
||||
viz_summary = {k: (len(v) if isinstance(v, str) and v else 0)
|
||||
for k, v in viz.items() if k.endswith("_base64")}
|
||||
@@ -410,15 +443,32 @@ def compute_mask(image_bgr, landmarks, seg_model, mask_type, erode_cm, px_per_cm
|
||||
return mask_bool, viz
|
||||
|
||||
|
||||
def _segment_hair(image_bgr, seg_model, landmarks, w, h):
|
||||
"""对任意图(如 hard_paste 重绘结果)重跑头发分割,返回 bool 掩码。
|
||||
|
||||
与 compute_mask 内部用的同一个 seg_model 逻辑(bisenet 需 landmarks,
|
||||
segformer 不需要),保证第1步(原图头发)与第2步(重绘后头发)分割口径一致。
|
||||
"""
|
||||
if seg_model == "bisenet":
|
||||
return _bisenet_hair_mask(image_bgr, landmarks, w, h)
|
||||
elif seg_model == "segformer":
|
||||
return _segformer_hair_mask(image_bgr)
|
||||
else:
|
||||
raise ValueError(f"未知 seg_model: {seg_model}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 步骤2:调 change_hair 换发型
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _call_swap(image_bgr, hairline_id, is_hr, ext_mask_bool, denoising_strength):
|
||||
def _call_swap(image_bgr, hairline_id, is_hr, ext_mask_bool, denoising_strength,
|
||||
inpainting_fill=1, mask_blur=11, mask_dilate_scale=1.0):
|
||||
"""调 change_hair /api/swapHair/v1,返回与输入同分辨率同对齐的换发型结果(BGR)。
|
||||
|
||||
ext_mask_bool 非 None 时作为 ext_mask 传入(swap_mode=ext_mask)。
|
||||
denoising_strength:webui img2img 重绘强度(越大生发越激进),透传给换发型。
|
||||
inpainting_fill / mask_blur / mask_dilate_scale:服务端重绘参数(透传给 change_hair,
|
||||
默认值=服务端原始硬编码值,未传时行为不变)。详见 change_hair 文档。
|
||||
"""
|
||||
import requests
|
||||
|
||||
@@ -430,6 +480,9 @@ def _call_swap(image_bgr, hairline_id, is_hr, ext_mask_bool, denoising_strength)
|
||||
"user_img_path": "data:image/jpeg;base64," + base64.b64encode(ibuf.tobytes()).decode(),
|
||||
"output_format": "base64",
|
||||
"denoising_strength": float(denoising_strength),
|
||||
"inpainting_fill": int(inpainting_fill),
|
||||
"mask_blur": int(mask_blur),
|
||||
"mask_dilate_scale": float(mask_dilate_scale),
|
||||
}
|
||||
if ext_mask_bool is not None:
|
||||
mbuf = cv2.imencode(".png", (ext_mask_bool.astype(np.uint8)) * 255)[1]
|
||||
@@ -500,25 +553,60 @@ def _call_hairgrow(image_bgr, mask_bool, strength):
|
||||
return result
|
||||
|
||||
|
||||
_REPAINT_WORKFLOW = os.path.join(os.path.dirname(os.path.dirname(__file__)), "hair_repaint.json")
|
||||
|
||||
|
||||
def _call_comfyui(image_bgr, mask_bool, prompt=None):
|
||||
"""调本机 ComfyUI 的 Flux-2 inpaint 工作流(hair_repaint.json),返回与输入同分辨率的 BGR。
|
||||
|
||||
与 swapHair 的区别:ComfyUI 把「原图 VAE 编码作 reference latent + ColorMatch」双重保色,
|
||||
天生不易染色;提示词自由可调(中文)。mask 经 RGBA alpha 通道传入(透明=重绘区)。
|
||||
ComfyUI 不在线时抛 SwapError(由调用方捕获降级)。prompt=None 用工作流内置默认提示词。
|
||||
"""
|
||||
import io
|
||||
from hairline.mask import compose_comfy_rgba
|
||||
from hairline.comfyui import run as comfyui_run, ping
|
||||
|
||||
if not ping():
|
||||
raise SwapError("ComfyUI 不可达(http://127.0.0.1:8188),redraw Flux-2 路跳过")
|
||||
mask_u8 = (mask_bool.astype(np.uint8)) * 255
|
||||
rgba_img = compose_comfy_rgba(image_bgr, mask_u8) # alpha=255-mask:透明=重绘区
|
||||
buf = io.BytesIO()
|
||||
rgba_img.save(buf, format="PNG")
|
||||
png_bytes = comfyui_run(buf.getvalue(), prompt=prompt, workflow_path=_REPAINT_WORKFLOW)
|
||||
result = cv2.imdecode(np.frombuffer(png_bytes, np.uint8), cv2.IMREAD_COLOR)
|
||||
if result is None:
|
||||
raise SwapError("ComfyUI 结果解码失败")
|
||||
if result.shape[:2] != image_bgr.shape[:2]:
|
||||
result = cv2.resize(result, (image_bgr.shape[1], image_bgr.shape[0]),
|
||||
interpolation=cv2.INTER_LANCZOS4)
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 步骤3+4:按遮罩贴回 + 接缝融合
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _color_match_to_orig(swap_result, orig, mask_bool):
|
||||
def _color_match_to_orig(swap_result, orig, mask_bool, strength=1.0):
|
||||
"""在 mask_bool 区域内做 Reinhard 颜色迁移:逐通道把 swap_result 的均值/方差对齐 orig。
|
||||
|
||||
strength 控制迁移强度:1.0=完全对齐到 orig(原行为),<1.0 只迁移部分,
|
||||
防止 Reinhard 在某些图上过度改色(如把生成发色整体拉向皮肤色)。
|
||||
遮罩外保持 swap_result 原样(不会越界污染)。返回 uint8 BGR。
|
||||
"""
|
||||
m = mask_bool.astype(bool)
|
||||
out = swap_result.astype(np.float32).copy()
|
||||
src_f = swap_result.astype(np.float32)
|
||||
out = src_f.copy()
|
||||
if m.sum() < 30:
|
||||
return swap_result.copy()
|
||||
strength = float(min(max(strength, 0.0), 1.0))
|
||||
for c in range(3):
|
||||
src_pix = swap_result[..., c][m].astype(np.float32)
|
||||
dst_pix = orig[..., c][m].astype(np.float32)
|
||||
s_mean, s_std = src_pix.mean(), src_pix.std() + 1e-6
|
||||
d_mean, d_std = dst_pix.mean(), dst_pix.std() + 1e-6
|
||||
out[..., c] = (out[..., c] - s_mean) * (d_std / s_std) + d_mean
|
||||
aligned = (out[..., c] - s_mean) * (d_std / s_std) + d_mean
|
||||
out[..., c] = src_f[..., c] * (1.0 - strength) + aligned * strength
|
||||
return np.clip(out, 0, 255).astype(np.uint8)
|
||||
|
||||
|
||||
@@ -553,10 +641,17 @@ def _multiband_alpha(mask_bool, edge_erode_px):
|
||||
return m
|
||||
|
||||
|
||||
def _multiband_blend(orig, swap_result, mask_bool, levels, edge_erode_px):
|
||||
def _multiband_blend(orig, swap_result, mask_bool, levels, edge_erode_px,
|
||||
feather_px=1, transition_band_px=-1):
|
||||
"""多频段(拉普拉斯金字塔)融合:低频用宽窗抹色差,高频用窄窗保发丝。
|
||||
|
||||
levels:金字塔层数(2~6),越大则低频色差在越宽范围被抹平。
|
||||
feather_px:最细若干层掩码轻羽化像素(0=不羽化,保持硬二值)。羽化最细 FEATHER_LAYERS
|
||||
层(核尺寸按层尺度放大),消除发丝边缘 1px 硬切锯齿;粗层仍保持二值(否则粗层会
|
||||
把整图混色)。注意:这是消除锯齿的微调,幅度有限(边界 Δ 约 1~3/255),
|
||||
不要指望它做大范围过渡——那是 mb_levels/transition_band_px 的事。
|
||||
transition_band_px:keep-region 外缘边距。-1=自动按层数 2**n(旧行为);
|
||||
>=0 则用绝对像素,使过渡带宽度与金字塔层数解耦。
|
||||
返回 uint8 BGR。
|
||||
"""
|
||||
m = _multiband_alpha(mask_bool, edge_erode_px)
|
||||
@@ -601,6 +696,21 @@ def _multiband_blend(orig, swap_result, mask_bool, levels, edge_erode_px):
|
||||
lb = lap_pyr(swap_result, n)
|
||||
ma = mask_pyr(m, n)
|
||||
|
||||
# 最细层(reversed 后末元素 = 全分辨率原始二值掩码)及其下若干层轻羽化,
|
||||
# 消除发丝边缘 1px 硬切锯齿。注意:多频段融合中各层都贡献边界过渡,但最细层的
|
||||
# 拉普拉斯系数幅度最小,只羽化它效果很弱(实测边界 Δ 仅 ~0.25/255)。因此对最细
|
||||
# FEATHER_LAYERS 层都做按尺度放大的羽化(越细的层核越大),才能明显软化边缘。
|
||||
# 粗层(低频)仍保持二值,否则会把整图混色,违反多频段融合的二值掩码前提。
|
||||
fp = int(max(0, feather_px))
|
||||
if fp > 0:
|
||||
for li in range(1, FEATHER_LAYERS + 1):
|
||||
idx = -li
|
||||
if abs(idx) > len(ma):
|
||||
break
|
||||
scale = 2 ** (li - 1)
|
||||
ksz = fp * 2 * scale + 1
|
||||
ma[idx] = cv2.GaussianBlur(ma[idx], (ksz, ksz), sigmaX=fp * scale / 2.0)
|
||||
|
||||
merged = []
|
||||
for a, b, mk in zip(la, lb, ma):
|
||||
m3 = mk[:, :, None]
|
||||
@@ -618,33 +728,68 @@ def _multiband_blend(orig, swap_result, mask_bool, levels, edge_erode_px):
|
||||
# 这条带正是 mb_levels 要控制的东西。若像旧实现那样用原始硬二值遮罩钳回,
|
||||
# 过渡带会被整条抹掉(实测 levels 2↔6 边界差恒为 0),mb_levels 形同虚设。
|
||||
# 故按层数膨胀出一个外缘 keep 区:keep 内允许过渡,keep 外才强制还原原图。
|
||||
margin = 2 ** n # n=2→4px … n=6→64px,与粗层掩码的自然扩散宽度匹配
|
||||
if transition_band_px is not None and transition_band_px >= 0:
|
||||
margin = int(transition_band_px) # 与金字塔层数解耦,用绝对像素
|
||||
else:
|
||||
margin = 2 ** n # n=2→4px … n=6→64px,与粗层掩码的自然扩散宽度匹配
|
||||
margin = max(0, margin)
|
||||
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * margin + 1, 2 * margin + 1))
|
||||
keep = cv2.dilate(mask_bool.astype(np.uint8), k).astype(bool)
|
||||
out[~keep] = orig[~keep]
|
||||
return out
|
||||
|
||||
|
||||
def _seamless_clone(orig, swap_result, mask_bool, edge_erode_px):
|
||||
"""泊松无缝克隆(cv2.seamlessClone NORMAL_CLONE):梯度域调和整体色调。
|
||||
|
||||
返回调色后的整帧 uint8 BGR;掩码过小(<10px)时返回原图。
|
||||
供 seamless 分支与 two_stage 两段式融合的第一段复用。
|
||||
"""
|
||||
m = mask_bool.astype(np.uint8)
|
||||
if edge_erode_px > 0:
|
||||
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * edge_erode_px + 1,) * 2)
|
||||
m = cv2.erode(m, k)
|
||||
if m.sum() < 10:
|
||||
return orig.copy()
|
||||
ys, xs = np.where(m > 0)
|
||||
center = (int((xs.min() + xs.max()) / 2), int((ys.min() + ys.max()) / 2))
|
||||
return cv2.seamlessClone(swap_result, orig, m * 255, center, cv2.NORMAL_CLONE)
|
||||
|
||||
|
||||
def _composite(orig, swap_result, mask_bool, blend_method, feather_px, edge_erode_px,
|
||||
color_match=False, mb_levels=5):
|
||||
"""把 swap_result 按遮罩贴回 orig,返回 (final_bgr, alpha_float or None)。"""
|
||||
color_match=False, mb_levels=5, color_match_strength=1.0,
|
||||
mb_feather_px=1, transition_band_px=-1):
|
||||
"""把 swap_result 按遮罩贴回 orig,返回 (final_bgr, alpha_float or None)。
|
||||
|
||||
blend_method:
|
||||
- multiband : 多频段金字塔融合(默认)
|
||||
- seamless : 泊松无缝克隆(梯度域调色,自带色彩调和,故跳过 color_match)
|
||||
- two_stage : 先 seamless 统一整体色调,再 multiband 贴发丝细节(大色差场景)
|
||||
- feather/alpha_gradient : 单层 alpha 过渡
|
||||
"""
|
||||
# seamless / two_stage 自带梯度域色彩调和,不叠 Reinhard 颜色迁移
|
||||
if blend_method == "seamless":
|
||||
m = mask_bool.astype(np.uint8)
|
||||
if edge_erode_px > 0:
|
||||
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * edge_erode_px + 1,) * 2)
|
||||
m = cv2.erode(m, k)
|
||||
if m.sum() < 10:
|
||||
return orig.copy(), None
|
||||
ys, xs = np.where(m > 0)
|
||||
center = (int((xs.min() + xs.max()) / 2), int((ys.min() + ys.max()) / 2))
|
||||
final = cv2.seamlessClone(swap_result, orig, m * 255, center, cv2.NORMAL_CLONE)
|
||||
final = _seamless_clone(orig, swap_result, mask_bool, edge_erode_px)
|
||||
return final, None
|
||||
|
||||
# 颜色校正前置(seamless 自带色彩调和,已在上面提前返回;其余分支在此生效)
|
||||
src = _color_match_to_orig(swap_result, orig, mask_bool) if color_match else swap_result
|
||||
if blend_method == "two_stage":
|
||||
# 第一段:seamless 把整体色调拉平(生成图色调对齐到原图)
|
||||
harmonized = _seamless_clone(orig, swap_result, mask_bool, edge_erode_px)
|
||||
# 第二段:对调色后的结果再做 multiband 贴发丝细节(不加 color_match,避免重复改色)
|
||||
final = _multiband_blend(orig, harmonized, mask_bool, mb_levels, edge_erode_px,
|
||||
feather_px=mb_feather_px,
|
||||
transition_band_px=transition_band_px)
|
||||
alpha = (_multiband_alpha(mask_bool, edge_erode_px).astype(np.float32)) / 255.0
|
||||
return final, alpha
|
||||
|
||||
# multiband / feather / alpha_gradient:先做 Reinhard 颜色迁移消除整体色差
|
||||
src = (_color_match_to_orig(swap_result, orig, mask_bool, color_match_strength)
|
||||
if color_match else swap_result)
|
||||
|
||||
if blend_method == "multiband":
|
||||
final = _multiband_blend(orig, src, mask_bool, mb_levels, edge_erode_px)
|
||||
final = _multiband_blend(orig, src, mask_bool, mb_levels, edge_erode_px,
|
||||
feather_px=mb_feather_px,
|
||||
transition_band_px=transition_band_px)
|
||||
# 可视化用:用多频段的二值掩码做一层 alpha 标记(展示实际合成区)
|
||||
alpha = (_multiband_alpha(mask_bool, edge_erode_px).astype(np.float32)) / 255.0
|
||||
return final, alpha
|
||||
@@ -664,21 +809,42 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo
|
||||
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", rid=None):
|
||||
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。
|
||||
|
||||
遮罩算法固定为 pushed(发际线外推),融合算法固定为 multiband(多频段金字塔),
|
||||
不再支持其他选项。rid: 调用方的 request id,用于日志关联。为 None 时自动生成。
|
||||
遮罩算法固定为 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 时自动生成。
|
||||
"""
|
||||
mask_type = "pushed" # 固定:只支持 pushed 遮罩算法
|
||||
blend_method = "multiband" # 固定:只支持 multiband 融合
|
||||
# blend_method 由参数传入(默认 multiband,接口11 可覆盖)
|
||||
if rid is None:
|
||||
rid = uuid4().hex[:8]
|
||||
logger.info("[%s] ===== generate_hairline_grow 开始 =====", rid)
|
||||
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",
|
||||
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",
|
||||
rid, erode_cm, hairline_push_cm, hairline_edge, mb_levels,
|
||||
seg_model, gen_backend, swap_mode)
|
||||
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)
|
||||
h, w = image_bgr.shape[:2]
|
||||
landmarks = detector.detect(image_bgr)
|
||||
if landmarks is None:
|
||||
@@ -701,20 +867,88 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo
|
||||
swap_result = _call_hairgrow(image_bgr, mask_bool, hairgrow_strength)
|
||||
else:
|
||||
ext_mask = mask_bool if swap_mode == "ext_mask" else None
|
||||
swap_result = _call_swap(image_bgr, hairline_id, is_hr, ext_mask, denoising_strength)
|
||||
swap_result = _call_swap(image_bgr, hairline_id, is_hr, ext_mask, denoising_strength,
|
||||
inpainting_fill=inpainting_fill, mask_blur=mask_blur,
|
||||
mask_dilate_scale=mask_dilate_scale)
|
||||
t_swap = time.time() - t0
|
||||
|
||||
# 步骤3:严格按遮罩硬贴回(无融合,用于对比)
|
||||
hard_paste = image_bgr.copy()
|
||||
hard_paste[mask_bool] = swap_result[mask_bool]
|
||||
|
||||
# 步骤4:接缝融合(固定 multiband)
|
||||
# 步骤4:接缝融合(默认 multiband)
|
||||
t0 = time.time()
|
||||
final, alpha = _composite(
|
||||
image_bgr, swap_result, mask_bool, blend_method, 0, edge_erode_px,
|
||||
color_match=False, mb_levels=mb_levels)
|
||||
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)
|
||||
t_blend = time.time() - t0
|
||||
|
||||
# 步骤5(可选):发际线带重绘分支 —— 开关 redraw=True 时执行,结果单独展示。
|
||||
# 重绘区域 = 外推发际线↔内推发际线之间的带(以原内轮廓为中心,向头发/脸各推 push_cm)。
|
||||
# 输入图 + 融合基底都用 final(④接缝融合最终图)。
|
||||
redraw_viz = {
|
||||
"redraw_band_overlay_base64": "",
|
||||
"redraw_a_base64": "",
|
||||
"redraw_c_base64": "",
|
||||
}
|
||||
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))
|
||||
|
||||
data = {
|
||||
"hairline_id": hairline_id,
|
||||
"gen_backend": gen_backend,
|
||||
@@ -730,6 +964,13 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo
|
||||
"hairline_push_cm": round(float(hairline_push_cm), 2),
|
||||
"hairline_edge": hairline_edge,
|
||||
"denoising_strength": round(float(denoising_strength), 3),
|
||||
"color_match": bool(color_match),
|
||||
"color_match_strength": round(float(color_match_strength), 3),
|
||||
"mb_feather_px": int(mb_feather_px),
|
||||
"transition_band_px": int(transition_band_px),
|
||||
"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),
|
||||
"erode_px": mask_viz["erode_px"],
|
||||
"hair_pixels": mask_viz["hair_pixels"],
|
||||
@@ -759,8 +1000,13 @@ def generate_hairline_grow(image_bgr, hairline_id, is_hr=False, seg_model="segfo
|
||||
"hard_paste_base64": _jpg_b64(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"],
|
||||
},
|
||||
"_rid": rid, # 调试用:返回本次请求的日志关联 id
|
||||
"redraw": redraw_info,
|
||||
"_rid": rid, # 调用方的 request id,用于日志关联。为 None 时自动生成。
|
||||
}
|
||||
# 记录 steps 各图字段是否非空,供排查前端取图问题
|
||||
steps_summary = {k: (len(v) if isinstance(v, str) and v else 0)
|
||||
|
||||
Reference in New Issue
Block a user