Files

688 lines
29 KiB
Python

import torch
import numpy as np
from PIL import Image, ImageOps
import cv2
import re
class MaskBoxCropNodeV2:
"""
根据mask裁剪图像并调整大小
"""
CATEGORY = "AFL/Mask"
DESCRIPTION = "根据mask裁剪图像区域并调整大小"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"resize_mode": (["NaN", "lanczos", "nearest-exact", "bilinear", "bicubic"], {"default": "lanczos"}),
},
"optional": {
"Box_grow_factor": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 5.0, "step": 0.05, "tooltip": "裁剪区域的扩展倍数,1.0表示不扩展,大于1.0表示按比例扩大"}),
"megapixels": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1, "tooltip": "目标图像的百万像素数,以1024*1024为1百万像素基准"}),
"divisible_by": ("INT", {"default": 8, "min": 1, "max": 1024, "step": 1, "tooltip": "目标分辨率必须被此数字整除"}),
"ratio": (["auto", "1:1", "4:3", "3:4", "16:9", "9:16"], {"default": "auto", "tooltip": "裁剪比例模式,auto为自动检测最接近比例"}),
"startup_threshold": ("FLOAT", {"default": 0.4, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "当mask的box面积与输入图像的面积占比达到此阈值时,跳过ratio和box_grow_factor判断"}),
"fill_color": ("STRING", {"default": "#FFFFFF", "tooltip": "边界超出时的填充颜色,支持hex格式(#FFFFFF/#FFF)或颜色名称(red/blue/green等)"}),
}
}
RETURN_TYPES = ("IMAGE", "CROPBOX", "MASK")
RETURN_NAMES = ("cropped_image", "crop_box", "cropped_mask")
FUNCTION = "crop_and_resize"
def _tensor_to_pil(self, tensor):
"""将ComfyUI的tensor转换为PIL图像"""
# 确保输入tensor是正确的数据类型和形状
if tensor.dtype != torch.float32:
tensor = tensor.float()
# 处理不同的tensor形状
if len(tensor.shape) == 4:
# 标准的4D tensor (batch, height, width, channels)
img_np = np.clip(tensor[0].cpu().numpy() * 255, 0, 255).astype(np.uint8)
elif len(tensor.shape) == 3:
# 3D tensor (height, width, channels)
img_np = np.clip(tensor.cpu().numpy() * 255, 0, 255).astype(np.uint8)
else:
# 其他情况,尝试处理
img_np = np.clip(tensor.cpu().numpy() * 255, 0, 255).astype(np.uint8)
return Image.fromarray(img_np)
def _tensor_to_pil_mask(self, mask):
"""将ComfyUI的mask tensor转换为PIL图像"""
# 确保输入mask是正确的数据类型
if mask.dtype != torch.float32:
mask = mask.float()
# 处理不同的mask形状
if len(mask.shape) == 4:
# 标准的4D mask tensor (batch, height, width, channels)
mask_np = np.clip(mask[0].cpu().numpy() * 255, 0, 255).astype(np.uint8)
elif len(mask.shape) == 3:
# 3D mask tensor (batch, height, width) 或 (height, width, channels)
if mask.shape[0] == 1: # (1, height, width)
mask_np = np.clip(mask[0].cpu().numpy() * 255, 0, 255).astype(np.uint8)
else: # (height, width, channels) 或其他情况
mask_np = np.clip(mask.cpu().numpy() * 255, 0, 255).astype(np.uint8)
elif len(mask.shape) == 2:
# 2D mask tensor (height, width)
mask_np = np.clip(mask.cpu().numpy() * 255, 0, 255).astype(np.uint8)
else:
# 其他情况,尝试处理
mask_np = np.clip(mask.cpu().numpy() * 255, 0, 255).astype(np.uint8)
# 确保mask_np是2D数组
if len(mask_np.shape) > 2:
# 如果是3D数组,取第一个通道
mask_np = mask_np[:, :, 0] if mask_np.shape[2] > 1 else mask_np[:, :, 0]
return Image.fromarray(mask_np, mode='L')
def _pil_to_tensor(self, pil_image):
"""将PIL图像转换为ComfyUI的tensor"""
img_np = np.array(pil_image).astype(np.float32) / 255.0
return torch.from_numpy(img_np)[None,]
def _pil_to_mask(self, pil_mask):
"""将PIL mask转换为ComfyUI的mask tensor"""
mask_np = np.array(pil_mask).astype(np.float32) / 255.0
return torch.from_numpy(mask_np)[None,]
def _find_best_aspect_ratio(self, width, height):
"""根据输入宽高找到最接近的预设宽高比"""
# 预设的宽高比列表 (width, height)
aspect_ratios = [(1, 1), (4, 3), (3, 4), (16, 9), (9, 16)]
# 计算输入尺寸的宽高比
input_ratio = width / height
# 找出最接近的宽高比
best_ratio = aspect_ratios[0]
min_diff = float('inf')
for ratio in aspect_ratios:
ratio_value = ratio[0] / ratio[1]
diff = abs(input_ratio - ratio_value)
if diff < min_diff:
min_diff = diff
best_ratio = ratio
return best_ratio
def _hex_to_rgb(self, hex_color):
"""将hex颜色值或颜色名称转换为RGB元组"""
# 基础颜色名称映射
color_names = {
'white': (255, 255, 255),
'black': (0, 0, 0),
'red': (255, 0, 0),
'green': (0, 128, 0),
'blue': (0, 0, 255),
'yellow': (255, 255, 0),
'cyan': (0, 255, 255),
'magenta': (255, 0, 255),
'orange': (255, 165, 0),
'pink': (255, 192, 203),
'purple': (128, 0, 128),
'gray': (128, 128, 128),
'grey': (128, 128, 128),
}
# 清理输入并转小写
hex_color = hex_color.strip().lower()
# 检查是否是颜色名称
if hex_color in color_names:
return color_names[hex_color]
# 移除#前缀
hex_color = hex_color.lstrip('#')
# 支持3位和6位hex格式
if len(hex_color) == 3:
hex_color = ''.join([c*2 for c in hex_color])
# 验证hex格式
if not re.match('^[0-9a-f]{6}$', hex_color):
# 无效格式,返回白色
return (255, 255, 255)
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
return (r, g, b)
def _calculate_target_dimensions(self, megapixels, aspect_ratio, divisible_by=1):
"""根据百万像素数、宽高比和可整除要求计算目标尺寸"""
# 1024*1024 = 约1百万像素
total_pixels = megapixels * 1024 * 1024
# 根据宽高比计算目标宽度和高度
width_ratio, height_ratio = aspect_ratio
aspect_ratio_value = width_ratio / height_ratio
target_height = int((total_pixels / aspect_ratio_value) ** 0.5)
target_width = int(target_height * aspect_ratio_value)
# 确保尺寸能被指定数字整除
if divisible_by > 1:
# 计算最接近但不小于原尺寸的可整数值
target_width = ((target_width + divisible_by - 1) // divisible_by) * divisible_by
target_height = ((target_height + divisible_by - 1) // divisible_by) * divisible_by
elif divisible_by == 1:
# 保持原有逻辑,确保尺寸为偶数
target_width = target_width + 1 if target_width % 2 != 0 else target_width
target_height = target_height + 1 if target_height % 2 != 0 else target_height
return (target_width, target_height)
def _process_single_image(self, pil_image, pil_mask, resize_mode, Box_grow_factor, megapixels, divisible_by, ratio, startup_threshold, original_width, original_height, fill_color=(255, 255, 255)):
"""处理单张图片的裁剪和缩放"""
# 确保mask是二值图像
pil_mask = pil_mask.convert('L')
# 获取mask的边界框
bbox = pil_mask.getbbox()
if bbox is None:
# 如果没有找到mask,返回整个图像
bbox = (0, 0, pil_image.width, pil_image.height)
# 紧密贴合mask边缘计算裁剪区域
x1, y1, x2, y2 = bbox
# 计算边界框的宽度和高度
bbox_width = x2 - x1
bbox_height = y2 - y1
# 计算mask的box面积与输入图像面积的比例
image_area = pil_image.width * pil_image.height
bbox_area = bbox_width * bbox_height
area_ratio = bbox_area / image_area
# 判断是否需要跳过ratio和box_grow_factor判断
skip_ratio_and_grow = area_ratio >= startup_threshold
if skip_ratio_and_grow:
# 当mask的box面积占比超过阈值时,不启动ratio判断,也不启用box_grow_factor判断,返回整个原始图像
crop_x1, crop_y1, crop_x2, crop_y2 = 0, 0, pil_image.width, pil_image.height
# 使用原始图像的宽高比
best_aspect_ratio = (pil_image.width, pil_image.height)
else:
# 找到宽高比
if ratio != "auto":
# 使用指定的比例
width_ratio, height_ratio = map(int, ratio.split(":"))
best_aspect_ratio = (width_ratio, height_ratio)
else:
# 找到最接近的预设宽高比
best_aspect_ratio = self._find_best_aspect_ratio(bbox_width, bbox_height)
width_ratio, height_ratio = best_aspect_ratio
# 根据宽高比和Box_grow_factor计算目标尺寸
if width_ratio >= height_ratio: # 横向或正方形
base_size = max(bbox_width, bbox_height * (width_ratio / height_ratio))
else: # 纵向
base_size = max(bbox_height, bbox_width * (height_ratio / width_ratio))
target_size = int(base_size * Box_grow_factor)
# 计算中心点
center_x = (x1 + x2) // 2
center_y = (y1 + y2) // 2
# 计算裁剪区域边界
if width_ratio >= height_ratio: # 横向或正方形
half_width = target_size // 2
half_height = int(half_width * (height_ratio / width_ratio))
else: # 纵向
half_height = target_size // 2
half_width = int(half_height * (width_ratio / height_ratio))
crop_x1 = center_x - half_width
crop_y1 = center_y - half_height
crop_x2 = center_x + half_width
crop_y2 = center_y + half_height
# 处理边界超出图像的情况
pad_left = max(0, -crop_x1)
pad_top = max(0, -crop_y1)
pad_right = max(0, crop_x2 - pil_image.width)
pad_bottom = max(0, crop_y2 - pil_image.height)
# 如果需要padding,则对图像和mask都进行padding
if pad_left > 0 or pad_top > 0 or pad_right > 0 or pad_bottom > 0:
pil_image = ImageOps.expand(pil_image, (pad_left, pad_top, pad_right, pad_bottom), fill=fill_color)
pil_mask = ImageOps.expand(pil_mask, (pad_left, pad_top, pad_right, pad_bottom), fill=0)
# 调整坐标
crop_x1 += pad_left
crop_y1 += pad_top
crop_x2 += pad_left
crop_y2 += pad_top
# 裁剪区域
crop_box = (crop_x1, crop_y1, crop_x2, crop_y2)
cropped_image = pil_image.crop(crop_box)
cropped_mask = pil_mask.crop(crop_box)
# 根据resize_mode决定是否进行缩放
if resize_mode == "NaN":
# 不执行缩放,但应用divisible_by要求
resized_image = cropped_image
resized_mask = cropped_mask
# 如果divisible_by大于1,确保尺寸可被整除
if divisible_by > 1:
width, height = cropped_image.size
# 计算新的尺寸,确保可被divisible_by整除
new_width = ((width + divisible_by - 1) // divisible_by) * divisible_by
new_height = ((height + divisible_by - 1) // divisible_by) * divisible_by
# 如果尺寸发生变化,进行缩放
if new_width != width or new_height != height:
# 使用lanczos算法进行质量较好的缩放
resized_image = cropped_image.resize((new_width, new_height), Image.LANCZOS)
resized_mask = cropped_mask.resize((new_width, new_height), Image.LANCZOS)
else:
# 计算目标尺寸
target_dimensions = self._calculate_target_dimensions(megapixels, best_aspect_ratio, divisible_by)
# 获取重采样过滤器
resample_filter = {
"lanczos": Image.LANCZOS,
"nearest-exact": Image.NEAREST,
"bilinear": Image.BILINEAR,
"bicubic": Image.BICUBIC
}.get(resize_mode, Image.LANCZOS)
# 进行缩放
resized_image = cropped_image.resize(target_dimensions, resample_filter)
resized_mask = cropped_mask.resize(target_dimensions, resample_filter)
# 返回crop_box信息以便还原使用
crop_info = {
"original_coords": crop_box,
"padded_size": (pil_image.width, pil_image.height),
"original_image_size": (original_width, original_height), # width, height
"pad_info": (pad_left, pad_top, pad_right, pad_bottom),
"fill_color": fill_color
}
return resized_image, resized_mask, crop_info
def crop_and_resize(self, image, mask, resize_mode, Box_grow_factor=1.0, megapixels=1.0, divisible_by=1, ratio="auto", startup_threshold=0.4, fill_color="#FFFFFF"):
image_batch_size = image.shape[0]
mask_batch_size = mask.shape[0] if len(mask.shape) == 3 else 1
# 取image和mask中较大的batch_size
batch_size = max(image_batch_size, mask_batch_size)
original_width = image.shape[2]
original_height = image.shape[1]
# 转换hex颜色为RGB元组
fill_color_rgb = self._hex_to_rgb(fill_color)
output_images = []
output_masks = []
crop_infos = []
for i in range(batch_size):
# 获取当前批次的图像,如果image只有1张则复用
single_image = image[i] if i < image_batch_size else image[0]
# 获取当前批次的mask,如果mask只有1张则复用
if len(mask.shape) == 3:
single_mask = mask[i] if i < mask_batch_size else mask[0]
else:
single_mask = mask
# 转换为PIL图像
img_np = np.clip(single_image.cpu().numpy() * 255, 0, 255).astype(np.uint8)
pil_image = Image.fromarray(img_np)
mask_np = np.clip(single_mask.cpu().numpy() * 255, 0, 255).astype(np.uint8)
if len(mask_np.shape) > 2:
mask_np = mask_np[:, :, 0]
pil_mask = Image.fromarray(mask_np, mode='L')
# 处理单张图片
resized_image, resized_mask, crop_info = self._process_single_image(
pil_image, pil_mask, resize_mode, Box_grow_factor,
megapixels, divisible_by, ratio, startup_threshold,
original_width, original_height, fill_color_rgb
)
# 转换回tensor
img_tensor = np.array(resized_image).astype(np.float32) / 255.0
mask_tensor = np.array(resized_mask).astype(np.float32) / 255.0
output_images.append(img_tensor)
output_masks.append(mask_tensor)
crop_infos.append(crop_info)
# 堆叠为批次tensor
output_image = torch.from_numpy(np.stack(output_images, axis=0))
output_mask = torch.from_numpy(np.stack(output_masks, axis=0))
# 返回crop_box信息列表以便还原使用
crop_info_batch = {
"batch_size": batch_size,
"crop_infos": crop_infos
}
return (output_image, crop_info_batch, output_mask)
class ImageRestoreNodeV2:
"""
将处理后的图像粘贴回原来的图像中
"""
CATEGORY = "AFL/Mask"
DESCRIPTION = "将处理后的图像粘贴回原图"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"original_image": ("IMAGE",),
"processed_image": ("IMAGE",),
"crop_box": ("CROPBOX",),
"blur_amount": ("INT", {"default": 0, "min": 0, "max": 500, "step": 1, "tooltip": "边缘羽化值,对mask边缘或bbox边缘应用高斯模糊"}),
"mask_expand": ("INT", {"default": 0, "min": -500, "max": 500, "step": 1, "tooltip": "遮罩扩展值,正值扩展,负值收缩"}),
},
"optional": {
"mask": ("MASK",),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("restored_image",)
FUNCTION = "restore_image"
def _tensor_to_pil(self, tensor):
"""将ComfyUI的tensor转换为PIL图像"""
# 确保输入tensor是正确的数据类型和形状
if tensor.dtype != torch.float32:
tensor = tensor.float()
# 处理不同的tensor形状
if len(tensor.shape) == 4:
# 标准的4D tensor (batch, height, width, channels)
img_np = np.clip(tensor[0].cpu().numpy() * 255, 0, 255).astype(np.uint8)
elif len(tensor.shape) == 3:
# 3D tensor (height, width, channels)
img_np = np.clip(tensor.cpu().numpy() * 255, 0, 255).astype(np.uint8)
else:
# 其他情况,尝试处理
img_np = np.clip(tensor.cpu().numpy() * 255, 0, 255).astype(np.uint8)
return Image.fromarray(img_np)
def _tensor_to_pil_mask(self, mask):
"""将ComfyUI的mask tensor转换为PIL图像"""
# 确保输入mask是正确的数据类型
if mask.dtype != torch.float32:
mask = mask.float()
# 处理不同的mask形状
if len(mask.shape) == 4:
# 标准的4D mask tensor (batch, height, width, channels)
mask_np = np.clip(mask[0].cpu().numpy() * 255, 0, 255).astype(np.uint8)
elif len(mask.shape) == 3:
# 3D mask tensor (batch, height, width) 或 (height, width, channels)
if mask.shape[0] == 1: # (1, height, width)
mask_np = np.clip(mask[0].cpu().numpy() * 255, 0, 255).astype(np.uint8)
else: # (height, width, channels) 或其他情况
mask_np = np.clip(mask.cpu().numpy() * 255, 0, 255).astype(np.uint8)
elif len(mask.shape) == 2:
# 2D mask tensor (height, width)
mask_np = np.clip(mask.cpu().numpy() * 255, 0, 255).astype(np.uint8)
else:
# 其他情况,尝试处理
mask_np = np.clip(mask.cpu().numpy() * 255, 0, 255).astype(np.uint8)
# 确保mask_np是2D数组
if len(mask_np.shape) > 2:
# 如果是3D数组,取第一个通道
mask_np = mask_np[:, :, 0] if mask_np.shape[2] > 1 else mask_np[:, :, 0]
return Image.fromarray(mask_np, mode='L')
def _pil_to_tensor(self, pil_image):
"""将PIL图像转换为ComfyUI的tensor"""
img_np = np.array(pil_image).astype(np.float32) / 255.0
return torch.from_numpy(img_np)[None,]
def _restore_single_image(self, original_pil, processed_pil, crop_info, blur_amount, mask_expand, single_mask=None):
"""处理单张图片的还原"""
# 获取裁剪信息
original_coords = crop_info["original_coords"]
padded_size = crop_info["padded_size"]
original_image_size = crop_info["original_image_size"]
pad_info = crop_info["pad_info"]
fill_color = crop_info.get("fill_color", (255, 255, 255)) # 兼容旧格式
pad_left, pad_top, pad_right, pad_bottom = pad_info
# 调整processed_image大小以匹配裁剪区域
crop_width = original_coords[2] - original_coords[0]
crop_height = original_coords[3] - original_coords[1]
resized_processed = processed_pil.resize((crop_width, crop_height), Image.LANCZOS)
# 创建一个与填充后图像相同大小的图像副本
restored_image = original_pil.copy()
if padded_size != (original_image_size[0], original_image_size[1]):
# 如果之前进行了padding,我们需要创建一个填充后的图像
restored_image = Image.new("RGB", padded_size, fill_color)
# 粘贴原始图像的有效区域
orig_region = (
pad_left,
pad_top,
pad_left + original_image_size[0],
pad_top + original_image_size[1]
)
restored_image.paste(original_pil, orig_region)
# 保存原始填充后图像用于混合
padded_original = restored_image.copy()
# 根据是否有mask决定粘贴方式
if single_mask is not None:
# 有mask:只在mask区域粘贴处理后的图像
restored_image = self._apply_mask_blend(
restored_image,
resized_processed,
padded_original,
original_coords,
single_mask,
blur_amount,
mask_expand
)
else:
# 无mask:粘贴整个bbox区域,使用边缘模糊
restored_image.paste(resized_processed, original_coords[:2])
if blur_amount > 0 or mask_expand != 0:
restored_image = self._apply_bbox_edge_blur(
restored_image,
padded_original,
original_coords,
blur_amount,
mask_expand
)
# 移除padding回到原始尺寸
if pad_left > 0 or pad_top > 0 or pad_right > 0 or pad_bottom > 0:
restored_image = restored_image.crop((
pad_left,
pad_top,
pad_left + original_image_size[0],
pad_top + original_image_size[1]
))
return restored_image
def restore_image(self, original_image, processed_image, crop_box, blur_amount, mask_expand, mask=None):
batch_size = original_image.shape[0]
# 检查是否为批次格式的crop_box
if "batch_size" in crop_box:
crop_infos = crop_box["crop_infos"]
else:
# 兼容旧格式(单张图片)
crop_infos = [crop_box] * batch_size
output_images = []
for i in range(batch_size):
# 获取当前批次的图像
single_original = original_image[i]
single_processed = processed_image[i]
crop_info = crop_infos[i] if i < len(crop_infos) else crop_infos[0]
# 转换为PIL图像
orig_np = np.clip(single_original.cpu().numpy() * 255, 0, 255).astype(np.uint8)
original_pil = Image.fromarray(orig_np)
proc_np = np.clip(single_processed.cpu().numpy() * 255, 0, 255).astype(np.uint8)
processed_pil = Image.fromarray(proc_np)
# 获取当前批次的mask(如果有)
single_mask = None
if mask is not None:
if len(mask.shape) == 3:
single_mask = mask[i] if i < mask.shape[0] else mask[0]
else:
single_mask = mask
# 处理单张图片
restored_image = self._restore_single_image(
original_pil, processed_pil, crop_info,
blur_amount, mask_expand, single_mask
)
# 转换回numpy
img_tensor = np.array(restored_image).astype(np.float32) / 255.0
output_images.append(img_tensor)
# 堆叠为批次tensor
output_image = torch.from_numpy(np.stack(output_images, axis=0))
return (output_image,)
def _apply_mask_blend(self, restored_image, resized_processed, original_image, crop_coords, input_mask, blur_amount, mask_expand):
"""使用mask混合图像,只在mask区域粘贴处理后的图像
blur_amount: 对mask边缘应用高斯模糊羽化
mask_expand: 遮罩扩展值,正值扩展(dilate),负值收缩(erode)
input_mask: 单个mask tensor (height, width) 或 (1, height, width)
"""
restored_np = np.array(restored_image)
processed_np = np.array(resized_processed)
original_np = np.array(original_image)
x1, y1, x2, y2 = crop_coords
crop_width = x2 - x1
crop_height = y2 - y1
# 将输入mask转换为numpy数组
if torch.is_tensor(input_mask):
mask_np = np.clip(input_mask.cpu().numpy() * 255, 0, 255).astype(np.uint8)
if len(mask_np.shape) > 2:
mask_np = mask_np[0] if mask_np.shape[0] == 1 else mask_np[:, :, 0]
else:
mask_np = input_mask
# 转换为PIL并调整大小
pil_mask = Image.fromarray(mask_np, mode='L')
resized_mask = pil_mask.resize((crop_width, crop_height), Image.LANCZOS)
mask_np = np.array(resized_mask)
# 对mask进行扩展或收缩
if mask_expand != 0:
abs_expand = abs(mask_expand)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (abs_expand * 2 + 1, abs_expand * 2 + 1))
if mask_expand > 0:
# 正值:扩展(膨胀)
mask_np = cv2.dilate(mask_np, kernel, iterations=1)
else:
# 负值:收缩(腐蚀)
mask_np = cv2.erode(mask_np, kernel, iterations=1)
# 对mask边缘应用高斯模糊羽化
if blur_amount > 0:
kernel_size = blur_amount * 2 + 1
mask_np = cv2.GaussianBlur(mask_np, (kernel_size, kernel_size), 0)
# 归一化到0-1范围
mask_float = mask_np.astype(np.float32) / 255.0
# 扩展mask维度以匹配图像通道
mask_3ch = np.stack([mask_float] * 3, axis=-1)
# 提取原图在crop区域的部分
original_crop = original_np[y1:y2, x1:x2]
# 在crop区域内混合:mask白色区域使用processed,黑色区域使用original
blended_crop = (processed_np * mask_3ch + original_crop * (1 - mask_3ch)).astype(np.uint8)
# 将混合结果放回原图
restored_np[y1:y2, x1:x2] = blended_crop
return Image.fromarray(restored_np)
def _apply_bbox_edge_blur(self, restored_image, original_image, crop_coords, blur_amount, mask_expand):
"""对bbox边缘应用高斯模糊过渡
blur_amount: 高斯模糊的半径
mask_expand: 遮罩扩展值,正值扩展(dilate),负值收缩(erode)
"""
restored_np = np.array(restored_image)
original_np = np.array(original_image)
x1, y1, x2, y2 = crop_coords
img_h, img_w = restored_np.shape[:2]
# 创建bbox形状的mask,内部为白色
bbox_mask = np.zeros((img_h, img_w), dtype=np.uint8)
bbox_mask[y1:y2, x1:x2] = 255
# 对mask进行扩展或收缩
if mask_expand != 0:
abs_expand = abs(mask_expand)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (abs_expand * 2 + 1, abs_expand * 2 + 1))
if mask_expand > 0:
# 正值:扩展(膨胀)
bbox_mask = cv2.dilate(bbox_mask, kernel, iterations=1)
else:
# 负值:收缩(腐蚀)
bbox_mask = cv2.erode(bbox_mask, kernel, iterations=1)
# 对mask应用高斯模糊,创建边缘羽化
if blur_amount > 0:
kernel_size = blur_amount * 2 + 1
bbox_mask = cv2.GaussianBlur(bbox_mask, (kernel_size, kernel_size), 0)
# 归一化到0-1范围
mask_float = bbox_mask.astype(np.float32) / 255.0
mask_3ch = np.stack([mask_float] * 3, axis=-1)
# 混合:mask白色区域使用restored,黑色区域使用original
result_np = (restored_np * mask_3ch + original_np * (1 - mask_3ch)).astype(np.uint8)
return Image.fromarray(result_np)
# 节点映射
NODE_CLASS_MAPPINGS = {
"AFL2:MaskBoxCropNodeV2": MaskBoxCropNodeV2,
"AFL2:ImageRestoreNodeV2": ImageRestoreNodeV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AFL2:MaskBoxCropNodeV2": "AFL Target box cropV2",
"AFL2:ImageRestoreNodeV2": "AFL Target restoreV2"
}