feat: 新增mask区域重绘功能 + 羽化贴回优化 + 发丝增强
新增功能: - inpaint_mask.py: mask区域重绘服务(enhance/pure_inpaint两种模式) 严格只在mask区做SD inpainting,发丝自然化 - /inpaint 页面 + /api/inpaint 接口:画板手绘mask + 提示词编辑 + denoising可调 - enhance_hair.webui_img2img: 新增denoising_strength参数(原硬编码0.35) 羽化贴回优化(hair_swap_manual.py 步骤⑥): - feather_px(羽化范围)+ feather_alpha(羽化强度)拆分为两个独立参数 - 改为只羽化边缘(mask内部保持硬切,仅边缘带渐变) - 新增黑底羽化对比图(纯黑背景凸显边缘过渡) 其他改动: - OSS/COS密钥脱敏:改为可选import,未设环境变量时不崩溃 - manual页面:发型列表改为本次训练的8个(含hair_flow) - enhance二次增强:重绘区改为「原图头发∪手绘mask」并外扩10%+减发际线带3cm - 发型清单文档更新:179→180个可用发型(+hair_flow) 涉及文件: - 新增: inpaint_mask.py, inpaint.html - 修改: app.py, manual.html, hair_swap_manual.py, enhance_hair.py - 修改: oss_module.py, upload_oss.py, cos_module.py(密钥脱敏) - 文档: HAIRSTYLES_AVAILABLE.md, hairstyles_available.csv
This commit is contained in:
@@ -20,8 +20,11 @@ class COS_object():
|
||||
token = None # 如果使用永久密钥不需要填入 token,如果使用临时密钥需要填入,临时密钥生成和使用指引参见 https://cloud.tencent.com/document/product/436/14048
|
||||
self.scheme = 'https' # 指定使用 http/https 协议来访问 COS,默认为 https,可不填
|
||||
self.BucketName= os.getenv('COS_BUCKET', '<your-cos-bucket>')
|
||||
for param in (secret_id, secret_key, self.region, self.BucketName):
|
||||
assert '<' not in param, '请设置环境变量 COS_SECRET_ID / COS_SECRET_KEY / COS_REGION / COS_BUCKET'
|
||||
# 密钥未设置时标记不可用(不抛异常)
|
||||
if '<' in secret_id or '<' in secret_key:
|
||||
print('[COS] ⚠️ 未设置 COS 密钥环境变量,COS 上传功能不可用')
|
||||
self.client = None
|
||||
return
|
||||
config = CosConfig(Region=self.region, SecretId=secret_id, SecretKey=secret_key, Token=token, Scheme=self.scheme)
|
||||
self.client = CosS3Client(config)
|
||||
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import time
|
||||
|
||||
import oss2
|
||||
import os
|
||||
|
||||
try:
|
||||
import oss2
|
||||
_OSS2_AVAILABLE = True
|
||||
except Exception as _e:
|
||||
print(f'[OSS] ⚠️ oss2 导入失败({_e}),OSS 上传功能不可用')
|
||||
_OSS2_AVAILABLE = False
|
||||
|
||||
class OSS_object():
|
||||
def __init__(self):
|
||||
if not _OSS2_AVAILABLE:
|
||||
print('[OSS] oss2 不可用,OSS 功能关闭')
|
||||
self.bucket = None
|
||||
return
|
||||
access_key_id = os.getenv('OSS_TEST_ACCESS_KEY_ID', '<your-access-key-id>')
|
||||
access_key_secret = os.getenv('OSS_TEST_ACCESS_KEY_SECRET', '<your-access-key-secret>')
|
||||
bucket_name = os.getenv('OSS_TEST_BUCKET', '<your-bucket-name>')
|
||||
endpoint = os.getenv('OSS_TEST_ENDPOINT', '<your-endpoint>')
|
||||
for param in (access_key_id, access_key_secret, bucket_name, endpoint):
|
||||
assert '<' not in param, '请设置环境变量 OSS_TEST_ACCESS_KEY_ID / OSS_TEST_ACCESS_KEY_SECRET / OSS_TEST_BUCKET / OSS_TEST_ENDPOINT'
|
||||
|
||||
# 密钥未设置时标记不可用(不抛异常,避免服务启动/换发型时崩溃)
|
||||
if '<' in access_key_id or '<' in access_key_secret:
|
||||
print('[OSS] ⚠️ 未设置 OSS 密钥环境变量,OSS 上传功能不可用(本地测试用 base64 返回即可)')
|
||||
self.bucket = None
|
||||
return
|
||||
self.bucket = oss2.Bucket(oss2.Auth(access_key_id, access_key_secret), endpoint, bucket_name)
|
||||
|
||||
def upload_file(self, file, target_name):
|
||||
|
||||
@@ -36,7 +36,8 @@ def hair_swap_manual(origin_img, hand_mask, hair_id, hairstyle_process, landmark
|
||||
denoising_strength=0.6, # 重绘强度(唯一能透传到webui的SD参数)
|
||||
# ===== 贴回/融合(真正生效)=====
|
||||
blend_dilate=(5, 5), # 步骤⑥ strict mask贴回时mask膨胀核
|
||||
feather_px=0, # ★ strict贴回边缘羽化像素(0=无羽化硬边缘,>0=高斯模糊边缘)
|
||||
feather_px=0, # ★ strict贴回边缘羽化范围(像素,边缘带宽度,0=无羽化)
|
||||
feather_alpha=1.0, # ★ 羽化强度(0~1,0=硬切,1=最大羽化,控制边缘过渡柔和度)
|
||||
seamless_dilate=(9, 9), # 步骤⑦ 泊松融合mask膨胀核
|
||||
# ===== enhance 二次增强重绘 =====
|
||||
enhance=False, # 步骤⑧ 是否对结果再做一次低强度SD重绘(让发丝更清晰)
|
||||
@@ -230,10 +231,20 @@ def hair_swap_manual(origin_img, hand_mask, hair_id, hairstyle_process, landmark
|
||||
result_strict_hard[mask_back == 0] = origin_img[mask_back == 0]
|
||||
|
||||
if feather_px and feather_px > 0:
|
||||
# ★ 只羽化边缘:mask 内部保持硬切(255),只有边缘带做渐变
|
||||
# 1. 对 mask_back 做高斯模糊(得到边缘的渐变)
|
||||
k = max(1, int(feather_px)) * 2 + 1 # 高斯核必须是奇数
|
||||
mask_back_blur = cv2.GaussianBlur(mask_back, (k, k), 0)
|
||||
mask_blend = (mask_back_blur.astype(np.float32) / 255)[..., None]
|
||||
feather_info = f",边缘羽化{feather_px}px"
|
||||
mask_blur = cv2.GaussianBlur(mask_back, (k, k), 0)
|
||||
# 2. 取 mask 内部区域(离边缘 > feather_px 的部分),强制为255(不羽化)
|
||||
erode_kernel = np.ones((k, k), np.uint8)
|
||||
mask_inner = cv2.erode(mask_back, erode_kernel)
|
||||
# 3. 合成:内部硬切(255) + 边缘用模糊值
|
||||
mask_blend_mask = np.maximum(mask_inner, mask_blur).astype(np.float32)
|
||||
# 4. ★ feather_alpha 控制羽化强度:在「全硬切(原mask)」和「边缘渐变(mask_blend_mask)」之间插值
|
||||
# alpha=0 → 完全硬切(无羽化),alpha=1 → 最大羽化
|
||||
mask_blend_mask = mask_back.astype(np.float32) * (1 - feather_alpha) + mask_blend_mask * feather_alpha
|
||||
mask_blend = (mask_blend_mask / 255)[..., None]
|
||||
feather_info = f",边缘羽化{feather_px}px 强度{feather_alpha:.2f}"
|
||||
else:
|
||||
mask_blend = hard_blend
|
||||
result_strict = (sd_result_back.astype(np.float32) * mask_blend +
|
||||
@@ -241,17 +252,38 @@ def hair_swap_manual(origin_img, hand_mask, hair_id, hairstyle_process, landmark
|
||||
result_strict = np.clip(result_strict, 0, 255).astype(np.uint8)
|
||||
result_strict[mask_back == 0] = origin_img[mask_back == 0]
|
||||
|
||||
# ★ 羽化对比图:左=硬边缘(feather=0) 右=当前羽化,裁剪mask边界附近放大
|
||||
# ★ 羽化对比图(黑底版):纯黑背景 + 贴回图,凸显羽化边缘过渡
|
||||
# 用黑色背景而非原图,避免原图干扰,纯粹对比羽化效果
|
||||
feather_compare = None
|
||||
feather_compare_full = None
|
||||
if feather_px and feather_px > 0:
|
||||
# 计算 mask_blend(用于把贴回图叠加到黑底上)
|
||||
ys, xs = np.where(mask_back > 10)
|
||||
if len(ys) > 0:
|
||||
x1 = max(0, xs.min()-20); x2 = min(origin_img.shape[1], xs.max()+20)
|
||||
y1 = max(0, ys.min()-20); y2 = min(origin_img.shape[0], ys.max()+20)
|
||||
hard_crop = result_strict_hard[y1:y2, x1:x2]
|
||||
blur_crop = result_strict[y1:y2, x1:x2]
|
||||
sep = np.full((hard_crop.shape[0], 3, 3), 128, dtype=np.uint8)
|
||||
# 黑底版:贴回图 * mask_blend + 黑色(0) * (1-mask_blend)
|
||||
# result_strict_hard 已是「贴回图+原图」混合,这里重新算黑底版
|
||||
black_bg = np.zeros_like(origin_img)
|
||||
# 无羽化版(黑底)
|
||||
hard_on_black = (sd_result_back.astype(np.float32) * hard_blend +
|
||||
black_bg.astype(np.float32) * (1 - hard_blend))
|
||||
hard_on_black = np.clip(hard_on_black, 0, 255).astype(np.uint8)
|
||||
hard_on_black[mask_back == 0] = 0 # mask外纯黑
|
||||
# 有羽化版(黑底)
|
||||
blur_on_black = (sd_result_back.astype(np.float32) * mask_blend +
|
||||
black_bg.astype(np.float32) * (1 - mask_blend))
|
||||
blur_on_black = np.clip(blur_on_black, 0, 255).astype(np.uint8)
|
||||
blur_on_black[mask_back == 0] = 0 # mask外纯黑
|
||||
|
||||
# 局部放大版(裁剪mask边界附近)
|
||||
x1 = max(0, xs.min()-30); x2 = min(origin_img.shape[1], xs.max()+30)
|
||||
y1 = max(0, ys.min()-30); y2 = min(origin_img.shape[0], ys.max()+30)
|
||||
hard_crop = hard_on_black[y1:y2, x1:x2]
|
||||
blur_crop = blur_on_black[y1:y2, x1:x2]
|
||||
sep = np.full((hard_crop.shape[0], 3, 3), 60, dtype=np.uint8) # 深灰分隔线
|
||||
feather_compare = np.hstack([hard_crop, sep, blur_crop])
|
||||
# 完整图版(不裁剪)
|
||||
sep_full = np.full((origin_img.shape[0], 3, 3), 60, dtype=np.uint8)
|
||||
feather_compare_full = np.hstack([hard_on_black, sep_full, blur_on_black])
|
||||
|
||||
result = result_strict if strict_mask else result_full
|
||||
|
||||
@@ -262,7 +294,9 @@ def hair_swap_manual(origin_img, hand_mask, hair_id, hairstyle_process, landmark
|
||||
{"label": f"当前选用({'严格mask' if strict_mask else '整框覆盖'})", "b64": _enc(result)},
|
||||
]
|
||||
if feather_compare is not None:
|
||||
step6_images.append({"label": f"★ 羽化对比(左=硬边 右=羽化{feather_px}px)", "b64": _enc(feather_compare)})
|
||||
step6_images.append({"label": f"★ 黑底羽化对比·边缘(左=无羽化 右=羽化{feather_px}pxα{feather_alpha:.1f})", "b64": _enc(feather_compare)})
|
||||
if feather_compare_full is not None:
|
||||
step6_images.append({"label": f"★ 黑底羽化对比·完整(左=无羽化 右=羽化{feather_px}pxα{feather_alpha:.1f})", "b64": _enc(feather_compare_full)})
|
||||
steps.append({
|
||||
"title": "⑥ 贴回原图",
|
||||
"desc": f"把SD结果逆warpAffine贴回用户原图。可调:strict_mask={strict_mask}, blend_dilate={bd}{feather_info}。整框覆盖=整个裁剪框覆盖原图;严格mask=只在mask区域融合,mask外保留原图",
|
||||
@@ -390,7 +424,7 @@ def hair_swap_manual(origin_img, hand_mask, hair_id, hairstyle_process, landmark
|
||||
"strict_mask": strict_mask, "seamless_blend": seamless_blend,
|
||||
"is_hr": is_hr, "dilate_kernel": list(dilate_kernel),
|
||||
"denoising_strength": denoising_strength,
|
||||
"blend_dilate": list(blend_dilate), "feather_px": feather_px,
|
||||
"blend_dilate": list(blend_dilate), "feather_px": feather_px, "feather_alpha": feather_alpha,
|
||||
"seamless_dilate": list(seamless_dilate),
|
||||
"enhance": enhance, "enhance_denoising": enhance_denoising,
|
||||
"mask_area_ratio_pct": round(mask_area_ratio, 1),
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""mask 区域重绘(inpainting)服务
|
||||
|
||||
两种模式(底层都是 webui img2img inpainting,严格只在mask区重绘):
|
||||
1. enhance: 用 enhance_hair.webui_img2img(默认提示词=发丝增强,denoising默认0.35)
|
||||
2. pure_inpaint: 纯SD inpainting(默认提示词=自然头发,denoising默认0.5)
|
||||
|
||||
严格只重绘 mask 区域:inpainting_fill=1(mask外保留原图)
|
||||
"""
|
||||
import cv2
|
||||
import numpy as np
|
||||
from utils import enhance_hair
|
||||
|
||||
|
||||
def inpaint_mask(img, mask, prompt="", denoising_strength=0.5, mode="enhance"):
|
||||
"""在 mask 区域做 SD inpainting 重绘。
|
||||
|
||||
参数:
|
||||
img: 输入图 BGR
|
||||
mask: 灰度mask,白色(255)=重绘区,与img同尺寸
|
||||
prompt: 提示词(空则用模式默认值)
|
||||
denoising_strength: 重绘强度 0~1
|
||||
mode: "enhance" 或 "pure_inpaint"
|
||||
|
||||
返回:
|
||||
result: 重绘后的图(mask区已重绘,其余保留原图)
|
||||
info: 本次参数说明
|
||||
"""
|
||||
h, w = img.shape[:2]
|
||||
|
||||
# 归一化 mask(保证 0/255,尺寸一致)
|
||||
if mask.shape[:2] != (h, w):
|
||||
mask = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST)
|
||||
if mask.ndim == 3:
|
||||
mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
|
||||
_, mask_bin = cv2.threshold(mask, 30, 255, cv2.THRESH_BINARY)
|
||||
|
||||
# 模式默认值
|
||||
if not prompt:
|
||||
if mode == "enhance":
|
||||
prompt = "high quality, detailed natural hair strands, realistic hair texture, sharp focus, photorealistic"
|
||||
else:
|
||||
prompt = "natural realistic hair, soft hair strands, photorealistic, high quality, detailed"
|
||||
|
||||
# 统计 mask 区域占比
|
||||
mask_ratio = float((mask_bin > 0).sum()) / mask_bin.size * 100
|
||||
|
||||
# 调用 enhance_hair.webui_img2img(底层是 inpainting,inpainting_fill=1 严格只在mask区重绘)
|
||||
result = enhance_hair.webui_img2img(
|
||||
img, mask_bin, prompt=prompt,
|
||||
denoising_strength=denoising_strength
|
||||
)
|
||||
|
||||
# webui 可能调整尺寸(调整到8的倍数),resize 回原图尺寸
|
||||
if result.shape[:2] != (h, w):
|
||||
result = cv2.resize(result, (w, h), interpolation=cv2.INTER_LANCZOS4)
|
||||
|
||||
# 双重保险:强制 mask 外区域用原图(虽然 inpainting_fill=1 已保证,再加一层确保严格)
|
||||
mask_3c = (mask_bin > 0)[..., None]
|
||||
result = np.where(mask_3c, result, img).astype(np.uint8)
|
||||
|
||||
info = (f"模式={mode},提示词=\"{prompt[:50]}...\",denoising={denoising_strength},"
|
||||
f"mask区域={mask_ratio:.1f}%。严格只重绘mask区,其余保留原图。")
|
||||
return result, mask_bin, info
|
||||
@@ -1,17 +1,28 @@
|
||||
import time
|
||||
|
||||
import oss2
|
||||
import os
|
||||
|
||||
try:
|
||||
import oss2
|
||||
_OSS2_AVAILABLE = True
|
||||
except Exception as _e:
|
||||
print(f'[OSS upload_oss] ⚠️ oss2 导入失败({_e}),OSS 上传功能不可用')
|
||||
_OSS2_AVAILABLE = False
|
||||
|
||||
class OSS_object():
|
||||
def __init__(self):
|
||||
if not _OSS2_AVAILABLE:
|
||||
print('[OSS upload_oss] oss2 不可用,OSS 功能关闭')
|
||||
self.bucket = None
|
||||
return
|
||||
access_key_id = os.getenv('OSS_TEST_ACCESS_KEY_ID', '<your-access-key-id>')
|
||||
access_key_secret = os.getenv('OSS_TEST_ACCESS_KEY_SECRET', '<your-access-key-secret>')
|
||||
bucket_name = os.getenv('OSS_TEST_BUCKET', '<your-bucket-name>')
|
||||
endpoint = os.getenv('OSS_TEST_ENDPOINT', '<your-endpoint>')
|
||||
for param in (access_key_id, access_key_secret, bucket_name, endpoint):
|
||||
assert '<' not in param, '请设置环境变量 OSS_TEST_ACCESS_KEY_ID / OSS_TEST_ACCESS_KEY_SECRET / OSS_TEST_BUCKET / OSS_TEST_ENDPOINT'
|
||||
|
||||
# 密钥未设置时标记不可用(不抛异常)
|
||||
if '<' in access_key_id or '<' in access_key_secret:
|
||||
print('[OSS upload_oss] ⚠️ 未设置 OSS 密钥环境变量,OSS 上传功能不可用')
|
||||
self.bucket = None
|
||||
return
|
||||
self.bucket = oss2.Bucket(oss2.Auth(access_key_id, access_key_secret), endpoint, bucket_name)
|
||||
|
||||
def upload_file(self, file, target_name):
|
||||
|
||||
Reference in New Issue
Block a user