Files
yichengup-ComfyUI_SwiftCut/py/image.py
T
2025-09-02 14:51:51 +00:00

473 lines
20 KiB
Python

import torch
import numpy as np
from PIL import Image
# 导入ComfyUI工具函数
def pil2tensor(image):
"""PIL图像转tensor"""
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
def tensor2pil(image):
"""tensor转PIL图像"""
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
def create_crop_mask(original_width, original_height, left, top, right, bottom, invert_mask=False):
"""创建裁剪遮罩,可选择遮罩方向
Args:
original_width: 原始图像宽度
original_height: 原始图像高度
left: 左边裁剪像素
top: 上边裁剪像素
right: 右边裁剪像素
bottom: 下边裁剪像素
invert_mask: 是否反向遮罩
- False: 保留区域为白色(1.0),裁剪区域为黑色(0.0)
- True: 保留区域为黑色(0.0),裁剪区域为白色(1.0)
"""
# 计算裁剪后的尺寸
new_width = original_width - left - right
new_height = original_height - top - bottom
# 确保尺寸有效
if new_width <= 0 or new_height <= 0:
raise ValueError(f"裁剪后尺寸无效: {new_width}x{new_height}")
# 根据反向设置决定默认颜色
default_color = 255 if invert_mask else 0
fill_color = 0 if invert_mask else 255
# 创建PIL遮罩图像(使用原始尺寸)
mask_image = Image.new('L', (original_width, original_height), default_color)
# 在保留区域绘制填充色矩形
# 保留区域是 (left, top) 到 (left + new_width, top + new_height)
mask_image.paste(fill_color, (left, top, left + new_width, top + new_height))
# 转换为numpy数组并归一化到0.0-1.0
mask_np = np.array(mask_image).astype(np.float32) / 255.0
# 转换为torch tensor并添加批次维度
mask_tensor = torch.from_numpy(mask_np).unsqueeze(0) # [1, height, width]
return mask_tensor
class YCImagePushPullLens:
"""推拉镜头效果节点 - 从图像中心逐渐放大或缩小,输出多帧
支持单张图片生成多帧,也支持多帧图片分别处理"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"frames": ("INT", {"default": 30, "min": 2, "max": 120, "step": 1}),
"start_crop_ratio": ("FLOAT", {"default": 0.3, "min": 0.1, "max": 1.0, "step": 0.05}),
"middle_crop_ratio": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.05}),
"end_crop_ratio": ("FLOAT", {"default": 0.8, "min": 0.1, "max": 1.0, "step": 0.05}),
"middle_frame": ("INT", {"default": 15, "min": 1, "max": 120, "step": 1}),
"input_mode": (["single_to_multi", "multi_to_multi"], {"default": "single_to_multi"}),
}
}
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("frames", "masks")
FUNCTION = "push_pull_lens"
CATEGORY = "SwiftCut/Image"
def push_pull_lens(self, image, frames, start_crop_ratio, middle_crop_ratio, end_crop_ratio, middle_frame, input_mode):
"""推拉镜头处理 - 支持单张图片生成多帧和多帧图片分别处理"""
ret_images = []
ret_masks = []
if input_mode == "single_to_multi":
# 原有功能:单张图片生成多帧
return self._single_to_multi(image, frames, start_crop_ratio, middle_crop_ratio, end_crop_ratio, middle_frame)
else:
# 新功能:多帧图片分别处理
return self._multi_to_multi(image, start_crop_ratio, middle_crop_ratio, end_crop_ratio, middle_frame)
def _single_to_multi(self, image, frames, start_crop_ratio, middle_crop_ratio, end_crop_ratio, middle_frame):
"""原有功能:单张图片生成多帧"""
ret_images = []
ret_masks = []
# 处理batch
for img in image:
# 转换为PIL图像
pil_img = tensor2pil(torch.unsqueeze(img, 0))
# 获取原始图像尺寸
original_width, original_height = pil_img.size
# 计算每帧的裁切系数
crop_ratios = self._calculate_crop_ratios(frames, start_crop_ratio, middle_crop_ratio, end_crop_ratio, middle_frame)
# 为每一帧生成裁切图像
for frame_idx, crop_ratio in enumerate(crop_ratios):
# 计算当前帧的裁切参数(从中心裁切)
crop_width = int(original_width * crop_ratio)
crop_height = int(original_height * crop_ratio)
# 计算裁切区域的左上角坐标(保持居中)
left = (original_width - crop_width) // 2
top = (original_height - crop_height) // 2
right = original_width - (left + crop_width) # 右边剩余的像素数量
bottom = original_height - (top + crop_height) # 下边剩余的像素数量
# 执行裁切
cropped_pil = pil_img.crop((left, top, left + crop_width, top + crop_height))
# 调整到原始尺寸(保持宽高比)
result_pil = cropped_pil.resize((original_width, original_height), Image.Resampling.LANCZOS)
# 转换回tensor
result_tensor = pil2tensor(result_pil)
ret_images.append(result_tensor)
# 创建对应的遮罩(显示裁切区域)
mask_tensor = create_crop_mask(original_width, original_height, left, top, right, bottom, False)
ret_masks.append(mask_tensor)
# 返回结果
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0))
def _multi_to_multi(self, image, start_crop_ratio, middle_crop_ratio, end_crop_ratio, middle_frame):
"""新功能:多帧图片分别处理,每帧应用不同的裁切比例"""
ret_images = []
ret_masks = []
# 获取输入帧数
input_frames = image.shape[0]
# 计算每帧的裁切系数
crop_ratios = self._calculate_crop_ratios(input_frames, start_crop_ratio, middle_crop_ratio, end_crop_ratio, middle_frame)
# 为每一帧生成裁切图像
for frame_idx in range(input_frames):
# 获取当前帧
current_frame = image[frame_idx:frame_idx+1] # 保持批次维度
# 转换为PIL图像
pil_img = tensor2pil(current_frame)
# 获取原始图像尺寸
original_width, original_height = pil_img.size
# 获取当前帧的裁切系数
crop_ratio = crop_ratios[frame_idx]
# 计算当前帧的裁切参数(从中心裁切)
crop_width = int(original_width * crop_ratio)
crop_height = int(original_height * crop_ratio)
# 计算裁切区域的左上角坐标(保持居中)
left = (original_width - crop_width) // 2
top = (original_height - crop_height) // 2
right = original_width - (left + crop_width) # 右边剩余的像素数量
bottom = original_height - (top + crop_height) # 下边剩余的像素数量
# 执行裁切
cropped_pil = pil_img.crop((left, top, left + crop_width, top + crop_height))
# 调整到原始尺寸(保持宽高比)
result_pil = cropped_pil.resize((original_width, original_height), Image.Resampling.LANCZOS)
# 转换回tensor
result_tensor = pil2tensor(result_pil)
ret_images.append(result_tensor)
# 创建对应的遮罩(显示裁切区域)
mask_tensor = create_crop_mask(original_width, original_height, left, top, right, bottom, False)
ret_masks.append(mask_tensor)
# 返回结果
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0))
def _calculate_crop_ratios(self, frames, start_ratio, middle_ratio, end_ratio, middle_frame):
"""计算每帧的裁切系数 - 支持三段式变化"""
ratios = []
# 确保中间帧数不超过总帧数
middle_frame = min(middle_frame, frames - 1)
# 计算每段的帧数
first_segment_frames = middle_frame
second_segment_frames = frames - middle_frame
# 第一段:从起始比例到中间比例
for i in range(first_segment_frames):
if first_segment_frames > 1:
progress = i / (first_segment_frames - 1)
current_ratio = start_ratio + (middle_ratio - start_ratio) * progress
else:
current_ratio = start_ratio
ratios.append(current_ratio)
# 第二段:从中间比例到结束比例
for i in range(second_segment_frames):
if second_segment_frames > 1:
progress = i / (second_segment_frames - 1)
current_ratio = middle_ratio + (end_ratio - middle_ratio) * progress
else:
current_ratio = middle_ratio
ratios.append(current_ratio)
return ratios
class YCImageOverlayBlend:
"""多帧图片叠加混合节点 - 将多帧图片与底色叠加,裁剪系数转换为透明程度"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"frames": ("IMAGE",), # 来自推拉镜头节点的多帧图片
"background_color": ("STRING", {"default": "#000000", "multiline": False}),
"start_alpha_ratio": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.05}),
"middle_alpha_ratio": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05}),
"end_alpha_ratio": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 1.0, "step": 0.05}),
"middle_frame": ("INT", {"default": 15, "min": 1, "max": 120, "step": 1}),
"blend_mode": (["normal", "multiply", "screen", "overlay", "soft_light"], {"default": "normal"}),
}
}
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("blended_frames", "alpha_masks")
FUNCTION = "overlay_blend"
CATEGORY = "SwiftCut/Image"
def overlay_blend(self, frames, background_color, start_alpha_ratio, middle_alpha_ratio, end_alpha_ratio, middle_frame, blend_mode):
"""多帧图片叠加混合处理"""
ret_images = []
ret_masks = []
# 解析背景颜色
bg_color = self._parse_color(background_color)
# 计算每帧的透明系数
alpha_ratios = self._calculate_alpha_ratios(frames.shape[0], start_alpha_ratio, middle_alpha_ratio, end_alpha_ratio, middle_frame)
# 为每一帧生成叠加混合图像
for frame_idx, alpha_ratio in enumerate(alpha_ratios):
# 获取当前帧
current_frame = frames[frame_idx:frame_idx+1] # 保持批次维度
# 创建背景图像
bg_tensor = self._create_background_tensor(current_frame.shape, bg_color)
# 应用透明度和混合模式
blended_tensor = self._apply_blend(current_frame, bg_tensor, alpha_ratio, blend_mode)
ret_images.append(blended_tensor)
# 创建对应的透明度遮罩
alpha_mask = self._create_alpha_mask(current_frame.shape, alpha_ratio)
ret_masks.append(alpha_mask)
# 返回结果
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0))
def _parse_color(self, color_str):
"""解析颜色字符串为RGB值"""
if color_str.startswith('#'):
# 十六进制颜色
color_str = color_str[1:]
if len(color_str) == 6:
r = int(color_str[0:2], 16) / 255.0
g = int(color_str[2:4], 16) / 255.0
b = int(color_str[4:6], 16) / 255.0
return (r, g, b)
# 默认黑色
return (0.0, 0.0, 0.0)
def _create_background_tensor(self, frame_shape, bg_color):
"""创建背景tensor"""
batch_size, channels, height, width = frame_shape
bg_tensor = torch.zeros(frame_shape, dtype=torch.float32)
# 设置RGB通道
for c in range(min(3, channels)):
bg_tensor[:, c, :, :] = bg_color[c]
return bg_tensor
def _apply_blend(self, foreground, background, alpha, blend_mode):
"""应用混合模式和透明度"""
# 应用透明度
blended = foreground * alpha + background * (1 - alpha)
# 应用混合模式
if blend_mode == "multiply":
blended = foreground * background
elif blend_mode == "screen":
blended = 1 - (1 - foreground) * (1 - background)
elif blend_mode == "overlay":
# 简化版overlay混合
blended = torch.where(background < 0.5,
2 * foreground * background,
1 - 2 * (1 - foreground) * (1 - background))
elif blend_mode == "soft_light":
# 简化版soft light混合
blended = torch.where(background < 0.5,
background * (2 * foreground - 1),
background * (2 * foreground - 1) + 2 * background * (1 - foreground))
return blended
def _create_alpha_mask(self, frame_shape, alpha_ratio):
"""创建透明度遮罩"""
batch_size, channels, height, width = frame_shape
mask = torch.full((1, height, width), alpha_ratio, dtype=torch.float32)
return mask
def _calculate_alpha_ratios(self, frames, start_alpha, middle_alpha, end_alpha, middle_frame):
"""计算每帧的透明系数 - 支持三段式变化"""
ratios = []
# 确保中间帧数不超过总帧数
middle_frame = min(middle_frame, frames - 1)
# 计算每段的帧数
first_segment_frames = middle_frame
second_segment_frames = frames - middle_frame
# 第一段:从起始透明度到中间透明度
for i in range(first_segment_frames):
if first_segment_frames > 1:
progress = i / (first_segment_frames - 1)
current_alpha = start_alpha + (middle_alpha - start_alpha) * progress
else:
current_alpha = start_alpha
ratios.append(current_alpha)
# 第二段:从中间透明度到结束透明度
for i in range(second_segment_frames):
if second_segment_frames > 1:
progress = i / (second_segment_frames - 1)
current_alpha = middle_alpha + (end_alpha - middle_alpha) * progress
else:
current_alpha = middle_alpha
ratios.append(current_alpha)
return ratios
class YCImageBatchBlend:
"""多批次图像混合节点 - 支持BCHW格式的多批次图像混合"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image1": ("IMAGE",), # 第一组图像批次
"image2": ("IMAGE",), # 第二组图像批次
"blend_factor": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"blend_mode": (["normal", "multiply", "screen", "overlay", "soft_light", "add", "subtract"], {"default": "normal"}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("blended_images",)
FUNCTION = "batch_blend"
CATEGORY = "SwiftCut/Image"
def batch_blend(self, image1, image2, blend_factor, blend_mode):
"""多批次图像混合处理"""
# 确保两个图像批次具有相同的帧数
frames1 = image1.shape[0]
frames2 = image2.shape[0]
# 如果帧数不同,取较小的帧数
min_frames = min(frames1, frames2)
# 截取相同帧数进行混合
img1 = image1[:min_frames]
img2 = image2[:min_frames]
# 确保通道数匹配
channels1 = img1.shape[1]
channels2 = img2.shape[1]
# 如果通道数不同,统一到3通道
if channels1 != channels2:
if channels1 == 4: # RGBA
img1 = img1[:, :3, :, :] # 只取RGB通道
elif channels1 == 1: # 灰度
img1 = img1.repeat(1, 3, 1, 1) # 复制到3通道
if channels2 == 4: # RGBA
img2 = img2[:, :3, :, :] # 只取RGB通道
elif channels2 == 1: # 灰度
img2 = img2.repeat(1, 3, 1, 1) # 复制到3通道
# 应用混合模式
if blend_mode == "normal":
blended = self._normal_blend(img1, img2, blend_factor)
elif blend_mode == "multiply":
blended = self._multiply_blend(img1, img2)
elif blend_mode == "screen":
blended = self._screen_blend(img1, img2)
elif blend_mode == "overlay":
blended = self._overlay_blend(img1, img2)
elif blend_mode == "soft_light":
blended = self._soft_light_blend(img1, img2)
elif blend_mode == "add":
blended = self._add_blend(img1, img2)
elif blend_mode == "subtract":
blended = self._subtract_blend(img1, img2)
else:
blended = self._normal_blend(img1, img2, blend_factor)
return (blended,)
def _normal_blend(self, img1, img2, factor):
"""标准混合模式"""
return img1 * (1 - factor) + img2 * factor
def _multiply_blend(self, img1, img2):
"""正片叠底混合"""
return img1 * img2
def _screen_blend(self, img1, img2):
"""滤色混合"""
return 1 - (1 - img1) * (1 - img2)
def _overlay_blend(self, img1, img2):
"""叠加混合"""
# 使用img1作为混合层,img2作为基底层
return torch.where(img2 < 0.5,
2 * img1 * img2,
1 - 2 * (1 - img1) * (1 - img2))
def _soft_light_blend(self, img1, img2):
"""柔光混合"""
# 使用img1作为混合层,img2作为基底层
return torch.where(img2 < 0.5,
img2 * (2 * img1 - 1),
img2 * (2 * img1 - 1) + 2 * img2 * (1 - img1))
def _add_blend(self, img1, img2):
"""加法混合"""
return torch.clamp(img1 + img2, 0, 1)
def _subtract_blend(self, img1, img2):
"""减法混合"""
return torch.clamp(img1 - img2, 0, 1)
NODE_CLASS_MAPPINGS = {
"YCImagePushPullLens": YCImagePushPullLens,
"YCImageOverlayBlend": YCImageOverlayBlend,
"YCImageBatchBlend": YCImageBatchBlend,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"YCImagePushPullLens": "Image Push Pull Lens",
"YCImageOverlayBlend": "Image Overlay Blend",
"YCImageBatchBlend": "Image Batch Blend",
}