import os import random import math from PIL import Image, ImageDraw,ImageOps import torch from typing import Union import numpy as np from pathlib import Path # from torchvision import transforms # from .tensor_pillow_loading import pillow_image_from_path # from .tensor_pillow_conversion import pillow_image_from_tensor def pillow_image_from_path(image_path: str) -> Image.Image: """ 从指定路径加载图像为 Pillow 图像。 :param image_path: 图像文件的路径 :return: 加载后的 Pillow 图像 """ image = Image.open(image_path) # print(f"图像 {image_path} 已从文件加载为 Pillow 图像。 | Image {image_path} has been loaded from file as a Pillow image.") return image def pillow_image_from_tensor(tensor: torch.Tensor): # 先将张量移到 CPU 并转换为 numpy 数组 numpy_array = tensor.cpu().numpy() # 确保数组的维度和数据类型正确 if numpy_array.ndim == 4: # 如果是批量数据,取第一个图像 numpy_array = numpy_array[0] # 调整维度顺序,从 (C, H, W) 到 (H, W, C) if numpy_array.shape[0] in [3, 4]: numpy_array = np.transpose(numpy_array, (1, 2, 0)) # 将数据类型转换为 uint8 if numpy_array.dtype != np.uint8: # 如果数据范围是 [0, 1],将其转换为 [0, 255] if numpy_array.max() <= 1.0: numpy_array = (numpy_array * 255).astype(np.uint8) else: numpy_array = numpy_array.astype(np.uint8) # 创建 Pillow 图像 image = Image.fromarray(numpy_array) return image def pillow_image_get_shape(image:Image.Image)-> tuple[int,int]: width, height = image.size return tuple(width, height) def pillow_image_to_gray(image:Image.Image)->Image.Image: return image.convert('L') def pillow_image_to_pillow_mask(image:Image.Image, channel:str): """ 将 Pillow 图像转换为 Pillow 掩码。 :param pillow_image: 输入的 Pillow 图像对象 :param channel: 选择的通道,字符串类型,可选值为 'red', 'green', 'blue', 'alpha' :return: 转换后的 Pillow 掩码图像对象 """ channels = ["red", "green", "blue", "alpha"] if channel not in channels: raise ValueError(f"无效的通道名称 '{channel}',请选择 'red', 'green', 'blue' 或 'alpha'。") # 将 Pillow 图像转换为 numpy 数组 image_array = np.array(image) # 根据通道名称确定索引 channel_index = channels.index(channel) if len(image_array.shape) == 3: # 彩色图像 mask_array = image_array[:, :, channel_index] elif len(image_array.shape) == 2: # 灰度图像 if channel != 'alpha': mask_array = image_array else: raise ValueError("灰度图像没有 alpha 通道。") else: raise ValueError("输入的图像数组维度不符合要求。") # 将 numpy 数组转换回 Pillow 图像 mask = Image.fromarray(mask_array) return mask def pillow_image_from_input(input: Union[str, torch.Tensor,Image.Image], is_mask: bool = False) -> Image.Image: """ 从图像路径或图像张量加载Pillow图像或遮罩。 Args: input (Union[str, torch.Tensor]): 输入可以是图像/遮罩的文件路径(字符串), 或者是一个图像/遮罩张量(torch.Tensor)。 is_mask (bool): 如果为True,则表示加载的是遮罩;否则为图像。默认值为False。 Returns: Image.Image: 加载后的Pillow图像或遮罩对象。 Raises: ValueError: 如果输入既不是字符串也不是torch.Tensor类型。 FileNotFoundError: 如果输入是字符串但文件不存在。 """ if isinstance(input, str): image = pillow_image_from_path(input) if is_mask: image = pillow_image_to_gray(image) return image if isinstance(input, torch.Tensor): image =pillow_image_from_tensor(input) if is_mask: image = pillow_image_to_gray(image) return image if isinstance(input, Image.Image): image =input if is_mask: image = pillow_image_to_gray(image) return image def create_shape_mask(shape: str, width: int, height: int) -> Image.Image: """ 创建指定形状的遮罩 Args: shape (str): 形状名称,如 "square", "triangle", "circle", "heart", "star", "diamond" width (int): 遮罩的宽度 height (int): 遮罩的高度 Returns: Image.Image: 创建好的遮罩图像 """ mask = Image.new('L', (width, height), 0) draw = ImageDraw.Draw(mask) if shape == "square": draw.rectangle((0, 0, width, height), fill=255) elif shape == "triangle": draw.polygon([(0, height), (width // 2, 0), (width, height)], fill=255) elif shape == "circle": draw.ellipse((0, 0, width, height), fill=255) elif shape == "heart": points = [] for t in range(0, 360, 10): rad = math.radians(t) x_coord = 16 * math.sin(rad) ** 3 y_coord = -(13 * math.cos(rad) - 5 * math.cos(2 * rad) - 2 * math.cos(3 * rad) - math.cos(4 * rad)) x_scaled = int(x_coord * width / 32 + width / 2) y_scaled = int(y_coord * height / 32 + height / 2) points.append((x_scaled, y_scaled)) draw.polygon(points, fill=255) elif shape == "star": outer_radius = min(width, height) // 2 inner_radius = outer_radius // 2 center_x = width // 2 center_y = height // 2 points = [] for i in range(5): angle1 = 2 * math.pi * i / 5 angle2 = 2 * math.pi * (i + 0.5) / 5 x1 = center_x + outer_radius * math.cos(angle1) y1 = center_y + outer_radius * math.sin(angle1) x2 = center_x + inner_radius * math.cos(angle2) y2 = center_y + inner_radius * math.sin(angle2) points.extend([(x1, y1), (x2, y2)]) draw.polygon(points, fill=255) elif shape == "diamond": draw.polygon([(width // 2, 0), (width, height // 2), (width // 2, height), (0, height // 2)], fill=255) return mask def get_shard_dimensions(x: int, y: int, shard_size: int, width: int, height: int) -> tuple: """ 计算单个碎片的实际尺寸 Args: x (int): 碎片的起始 x 坐标 y (int): 碎片的起始 y 坐标 shard_size (int): 碎片的大小 width (int): 原始图片的宽度 height (int): 原始图片的高度 Returns: tuple: 碎片的实际宽度和高度 """ actual_width = min(shard_size, width - x) actual_height = min(shard_size, height - y) return actual_width, actual_height def extract_shard_image(original_img: Image.Image, x: int, y: int, actual_width: int, actual_height: int) -> Image.Image: """ 从原始图片中提取单个碎片图像 Args: original_img (Image.Image): 原始图片 x (int): 碎片的起始 x 坐标 y (int): 碎片的起始 y 坐标 actual_width (int): 碎片的实际宽度 actual_height (int): 碎片的实际高度 Returns: Image.Image: 提取的碎片图像 """ return original_img.crop((x, y, x + actual_width, y + actual_height)) def apply_shard_mask(shard_img: Image.Image, shard_shape: str, shard_mask_image: Union[str, torch.Tensor], actual_width: int, actual_height: int) -> Image.Image: """ 为碎片图像应用遮罩形状和遮罩图像 Args: shard_img (Image.Image): 碎片图像 shard_shape (str): 碎片的形状 shard_mask_image (Union[str, torch.Tensor]): 碎片的遮罩图像 actual_width (int): 碎片的实际宽度 actual_height (int): 碎片的实际高度 Returns: Image.Image: 应用遮罩后的碎片图像 """ if shard_shape: mask = create_shape_mask(shard_shape, actual_width, actual_height) shard_img.putalpha(mask) if shard_mask_image is not None: # Boolean value of Tensor with more than one value is ambiguous mask = pillow_image_from_input(shard_mask_image, is_mask=True) mask = mask.resize((actual_width, actual_height)) shard_img.putalpha(mask) return shard_img def generate_shard_movement_params(speed: int, rotation: int, frame_count: int, fade_start: int) -> dict: """ 生成单个碎片的运动参数 Args: speed (int): 碎片的运动速度 rotation (int): 碎片的旋转速度 frame_count (int): 总帧数 fade_start (int): 碎片开始消失的帧数 Returns: dict: 碎片的运动参数 """ angle = random.uniform(0, 2 * math.pi) shard_speed = random.uniform(0.5, 1.5) * speed rot_speed = random.uniform(-1, 1) * rotation fade = random.randint(frame_count // 2, frame_count - 5) if max(0, fade_start) < 1 else random.randint(max(0, fade_start - 5), min(frame_count - 1, fade_start + 5)) return { 'angle': angle, 'speed': shard_speed, 'rotation': 0, 'rotation_speed': rot_speed, 'fade_start': fade, 'opacity': 1.0 } def create_shard_info(shard_img: Image.Image, x: int, y: int, speed: int, rotation: int, frame_count: int, fade_start: int) -> dict: """ 创建单个碎片的完整信息 Args: shard_img (Image.Image): 碎片图像 x (int): 碎片的起始 x 坐标 y (int): 碎片的起始 y 坐标 speed (int): 碎片的运动速度 rotation (int): 碎片的旋转速度 frame_count (int): 总帧数 fade_start (int): 碎片开始消失的帧数 Returns: dict: 碎片的完整信息 """ movement_params = generate_shard_movement_params(speed, rotation, frame_count, fade_start) return { 'img': shard_img, 'x': x, 'y': y, 'width': shard_img.width, 'height': shard_img.height, **movement_params } def update_shard_position_and_rotation(shard: dict, frame: int): """ 更新单个碎片的位置和旋转 Args: shard (dict): 碎片的完整信息 frame (int): 当前帧数 """ if frame > 0: shard['x'] += math.cos(shard['angle']) * shard['speed'] shard['y'] += math.sin(shard['angle']) * shard['speed'] shard['rotation'] += shard['rotation_speed'] def update_shard_opacity(shard: dict, frame: int): """ 更新单个碎片的透明度 Args: shard (dict): 碎片的完整信息 frame (int): 当前帧数 """ if frame >= shard['fade_start']: shard['opacity'] = max(0, shard['opacity'] - 0.1) def rotate_and_apply_opacity(shard: dict) -> Image.Image: """ 旋转碎片图像并应用透明度 Args: shard (dict): 碎片的完整信息 Returns: Image.Image: 旋转并应用透明度后的碎片图像 """ rotated_shard = shard['img'].rotate(shard['rotation'], expand=True) if shard['opacity'] < 1.0: alpha = rotated_shard.split()[3] alpha = alpha.point(lambda p: p * shard['opacity']) rotated_shard.putalpha(alpha) return rotated_shard def text_padd(text: str, action: str, length: int, filled: str = ' ') -> str: """ 对字符串进行填充操作 Args: text (str): 要填充的字符串 action (str): 填充方向,'padd-start' 或 'padd-end' length (int): 填充后的总长度 filled (str): 填充字符,默认为空格 Returns: str: 填充后的字符串 """ res = text if 'padd-start' in action: res = text.rjust(length, filled) elif 'padd-end' in action: res = text.ljust(length, filled) return res # 编写一个函数,将 false,true转为 bool。忽略大小写。支持传入 字符或者布尔。添加函数提示和注释。 # 支持 enable,disable,1,0 # 如果输入为None,第二个参数为默认值,支持设置默认值。 # 如果输入为None或者空字符,第二个参数为默认值,支持设置默认值。 # 函数注释由中文/英文 双语展示 def to_bool(value: str | bool | None, default: bool = False) -> bool: """ 此函数用于将输入值转换为布尔类型。 支持布尔值输入,也支持 'true'、'false'、'enable'、'disable'、'1'、'0' 字符串输入(忽略大小写)。 若输入为 None 或者空字符串,则返回默认值。 参数: value (str | bool | None): 要转换为布尔值的输入,可以是字符串、布尔值或 None。 default (bool): 当输入为 None 或者空字符串时返回的默认值,默认为 False。 返回: bool: 转换后的布尔值。 异常: ValueError: 若输入既不是有效的布尔值,也不是支持的字符串,会抛出此异常。 This function is used to convert the input value to a boolean type. It supports boolean input and string inputs such as 'true', 'false', 'enable', 'disable', '1', '0' (case-insensitive). If the input is None or an empty string, the default value will be returned. Parameters: value (str | bool | None): The input to be converted to a boolean value, which can be a string, a boolean, or None. default (bool): The default value to be returned when the input is None or an empty string, with a default of False. Returns: bool: The converted boolean value. Exceptions: ValueError: If the input is neither a valid boolean value nor a supported string, this exception will be thrown. """ if value is None or (isinstance(value, str) and value.strip() == ""): return default if isinstance(value, bool): return value if isinstance(value, str): # 去除字符串首尾空白并转换为小写 # Remove leading and trailing whitespace from the string and convert it to lowercase value = value.strip().lower() if value in ['true', 'enable', '1']: return True elif value in ['false', 'disable', '0']: return False raise ValueError(f"无法将 '{value}' 转换为布尔值。 Unable to convert '{value}' to a boolean value.") # def tensor_list_to_batch(tensor_list:list[ torch.Tensor]): # """ # 将张量列表转换为一个批次(batch)。 # 参数: # tensor_list (list): 张量列表。 # 返回: # torch.Tensor: 转换后的批次张量。 # """ # return torch.stack(tensor_list, dim=0) # def pillow_image_to_tensor(image: Image.Image): # # 步骤 1: 将 PIL 图像对象转换为 numpy 数组 # # PIL 图像对象在内存中的存储格式不便于直接进行深度学习计算 # # 使用 np.array() 函数将其转换为 numpy 数组,数组的形状通常为 (高度, 宽度, 通道数) # image_np = np.array(image) # # 步骤 2: 将 numpy 数组转换为 PyTorch 张量 # # 使用 torch.tensor() 函数将 numpy 数组转换为 PyTorch 张量 # # 同时指定数据类型为 torch.float32,这是深度学习中常用的数据类型 # image_tensor = torch.tensor(image_np, dtype=torch.float32) # # 步骤 3: 调整张量的维度 # # 在 PyTorch 中,图像张量的常见形状是 (通道数, 高度, 宽度) # # 而 numpy 数组的形状是 (高度, 宽度, 通道数),所以需要调整维度 # # 使用 permute() 函数对维度进行重新排列,参数 (2, 0, 1) 表示将原数组的第 2 维放到第 0 位,第 0 维放到第 1 位,第 1 维放到第 2 位 # image_tensor = image_tensor.permute(2, 0, 1) # # 步骤 4: 归一化像素值 # # 在深度学习中,通常需要将像素值归一化到 [0, 1] 范围 # # 因为图像的像素值通常在 [0, 255] 之间,所以将每个像素值除以 255 来实现归一化 # image_tensor = image_tensor / 255 # # 步骤 5: 移除可能存在的批量维度为 1 的情况 # # 如果张量的维度是 4(通常表示 (批量大小, 通道数, 高度, 宽度)) # # 并且批量大小为 1,那么使用 squeeze(0) 方法移除这个批量维度 # if image_tensor.ndim == 4 and image_tensor.shape[0] == 1: # # print(f"[shatter] 当前张量维度为 {image_tensor.dim()},自动调整为3维。") # image_tensor = image_tensor.squeeze(0) # # if image_tensor.dim() < 4: # # print(f"[shatter] 当前张量维度为 {image_tensor.dim()},自动调整为4维。") # # # 自动调整维度 # # while image_tensor.dim() < 4: # # image_tensor = image_tensor.unsqueeze(0) # return image_tensor def pillow_image_to_tensor(image: Image.Image) -> torch.Tensor: # 步骤 1: 将 PIL 图像对象转换为 numpy 数组 # PIL 图像对象在内存中的存储格式不便于直接进行深度学习计算 # 使用 np.array() 函数将其转换为 numpy 数组,数组的形状通常为 (高度, 宽度, 通道数) np_array = np.array(image) # 步骤 2: 将 numpy 数组转换为 PyTorch 张量 tensor = torch.from_numpy(np_array).permute(2, 0, 1).float() / 255.0 # 转换为 (C, H, W) 形状并归一化到 [0, 1] return tensor def tensor_list_to_batch(tensor_list: list[torch.Tensor]) -> torch.Tensor: """ 将张量列表转换为一个批次(batch)。 参数: tensor_list (list): 张量列表。 返回: torch.Tensor: 转换后的批次张量。 """ # 检查列表是否为空 if not tensor_list: raise ValueError("输入的张量列表为空。") # 检查所有张量的形状是否一致 first_shape = tensor_list[0].shape for tensor in tensor_list: if tensor.shape != first_shape: raise ValueError("张量列表中的所有张量形状必须一致。") # 将张量列表堆叠成一个批次张量 batch_tensor = torch.stack(tensor_list) return batch_tensor # def pillow_images_to_tensor(images: list[Image.Image]) -> torch.Tensor: # """ # 将Pillow图像列表转换为图像张量。 # 参数: # images (list[Image.Image]): 包含Pillow图像对象的列表。 # 返回: # torch.Tensor: 形状为 (N, C, H, W) 的图像张量,其中 N 是图像数量,C 是通道数,H 是高度,W 是宽度。 # """ # transform = transforms.Compose([ # transforms.ToTensor() # ]) # tensors = [transform(img) for img in images] # return torch.stack(tensors) def tensor_common_upscale(tensor: torch.Tensor, height: int, width: int, mode: str = "bilinear", align_corners: bool = False) -> torch.Tensor: """ 自定义的上采样函数,实现图像的上采样操作。 Custom upsampling function to perform image upsampling. 参数: :param tensor: 输入的图像张量 The input image tensor :param height: 目标高度 The target height :param width: 目标宽度 The target width :param mode: 上采样模式,默认为双线性插值 Upsampling mode, default is bilinear interpolation :param align_corners: 是否对齐角点,默认为 False Whether to align corners, default is False 返回: :return: 上采样后的图像张量 The upsampled image tensor """ # 调整张量形状为 (B, C, H, W) if tensor.dim() == 3: tensor = tensor.unsqueeze(0) # 使用 PyTorch 的 interpolate 函数进行上采样 upsampled = torch.nn.functional.interpolate(tensor, size=(height, width), mode=mode, align_corners=align_corners) # 移除批量维度(如果只有一个样本) if upsampled.shape[0] == 1: upsampled = upsampled.squeeze(0) return upsampled def tensor_image_and_mask_from_path_or_pillow(image_input: str | Image.Image) -> tuple[torch.Tensor, torch.Tensor, bool]: """ 处理单张图像,将其转换为 RGB 图像张量和掩码张量。 Process a single image and convert it into an RGB image tensor and a mask tensor. 参数: :param image_input: 图像文件的路径或者 PIL.Image.Image 对象。 The path of the image file or a PIL.Image.Image object. 返回: :return: 处理后的图像张量、掩码张量和是否有非空掩码的标志。 The processed image tensor, mask tensor, and a flag indicating whether there is a non-empty mask. """ if isinstance(image_input, str): # 如果输入是字符串,当作文件路径处理 i = Image.open(image_input) elif isinstance(image_input, Image.Image): # 如果输入是 PIL.Image.Image 对象,直接使用 i = image_input else: raise ValueError("输入必须是字符串类型的文件路径或者 PIL.Image.Image 对象。") i = ImageOps.exif_transpose(i) # # 转换为 RGB 模式 # image = i.convert("RGB") # 转换为 RGBA 模式 image = i.convert("RGBA") # 将图像转换为 numpy 数组并归一化 image = np.array(image).astype(np.float32) / 255.0 # 转换为 PyTorch 张量并调整形状 image = torch.from_numpy(image)[None, ] # 检查图像是否有透明度通道 has_non_empty_mask = False if 'A' in i.getbands(): # 获取透明度通道并转换为 numpy 数组,然后归一化 mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 # 转换为 PyTorch 张量并取反 mask = 1. - torch.from_numpy(mask) has_non_empty_mask = True else: # 如果没有透明度通道,创建全零掩码 mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") return image, mask, has_non_empty_mask def tensor_image_and_mask_batch_from_list(images: list[torch.Tensor], masks: list[torch.Tensor], has_non_empty_mask: bool) -> tuple[torch.Tensor, torch.Tensor, int]: """ 根据图像和掩码列表处理并拼接图像和掩码张量。 Process and concatenate image and mask tensors based on the image and mask lists. 参数: :param images: 图像张量列表 A list of image tensors :param masks: 掩码张量列表 A list of mask tensors :param has_non_empty_mask: 是否有非空掩码的标志 A flag indicating whether there is a non-empty mask 返回: :return: 拼接后的图像张量、掩码张量和图像数量 The concatenated image tensor, mask tensor, and the number of images """ if len(images) == 1: return (images[0], masks[0], 1) elif len(images) > 1: # 获取第一张图像的张量 image1 = images[0] mask1 = None # 拼接图像张量 for image2 in images[1:]: if image1.shape[1:] != image2.shape[1:]: # 如果图像形状不一致,进行上采样 image2 = tensor_common_upscale(image2.movedim(-1, 1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1, -1) image1 = torch.cat((image1, image2), dim=0) # 拼接掩码张量 for mask2 in masks[1:]: if has_non_empty_mask: if image1.shape[1:3] != mask2.shape: # 如果掩码形状与图像不一致,进行插值 mask2 = torch.nn.functional.interpolate(mask2.unsqueeze(0).unsqueeze(0), size=(image1.shape[2], image1.shape[1]), mode='bilinear', align_corners=False) mask2 = mask2.squeeze(0) else: mask2 = mask2.unsqueeze(0) else: mask2 = mask2.unsqueeze(0) if mask1 is None: mask1 = mask2 else: mask1 = torch.cat((mask1, mask2), dim=0) return (image1, mask1, len(images)) else: return (torch.tensor([]), torch.tensor([]), 0) def create_shatter_effect(params: dict): """ 创建图片破碎效果并保存序列帧 参数: params (dict): 包含所有配置参数的字典 如果传入 shards_area_mask 碎化粒子只在该区域初始化 如果传入 save_image 则保存图片,否则不保存图片 """ input_image_path = params.get('input_image_path') # print(f"input_image_path type: {type(input_image_path)}, value: {input_image_path}") # 添加调试输出 # 其他代码保持不变 input_image_path = params.get('input_image_path') output_folder = params.get('output_folder') frame_count = params.get('frame_count', 20) shard_size = params.get('shard_size', 30) speed = params.get('speed', 10) rotation = params.get('rotation', 5) fade_start = params.get('fade_start', 0) name_prefix = params.get('name_prefix', "frame_") name_id_length = params.get('name_id_length', 3) frame_start_index = params.get('frame_start_index', 1) shard_shape = params.get('shard_shape', None) shard_mask_image = params.get('shard_mask_image', None) shards_area_mask = params.get('shards_area_mask', None) save_image = to_bool(params.get('save_image', True)) show_image = to_bool(params.get('show_image', False)) frame_count,shard_size=tuple([int(item) for item in [frame_count,shard_size]]) fade_start,name_id_length,frame_start_index=tuple([int(item) for item in [fade_start,name_id_length,frame_start_index]]) speed,rotation=tuple([float(item) for item in [speed,rotation]]) # name_prefix=tuple([int(item) for item in [name_prefix]]) # 创建输出文件夹 if save_image: os.makedirs(output_folder, exist_ok=True) # 加载原始图片 original_img = pillow_image_from_input(input_image_path, False) width, height = original_img.size # 转换为RGBA模式以便处理透明度 original_img = original_img.convert("RGBA") # 创建碎片网格 # cols = math.ceil(width / shard_size) # rows = math.ceil(height / shard_size) # shards_area_mask = pillow_image_from_input(shards_area_mask, is_mask=True) # if shards_area_mask.getpixel((x, y)) != 0: # 为每个碎片生成随机运动参数 # shards = [] # for row in range(rows): # for col in range(cols): # x = col * shard_size # y = row * shard_size # actual_width, actual_height = get_shard_dimensions(x, y, shard_size, width, height) # if actual_width <= 0 or actual_height <= 0: # continue # shard_img = extract_shard_image(original_img, x, y, actual_width, actual_height) # shard_img = apply_shard_mask(shard_img, shard_shape, shard_mask_image, actual_width, actual_height) # shard_info = create_shard_info(shard_img, x, y, speed, rotation, frame_count, fade_start) # shards.append(shard_info) shards = [] if shards_area_mask is not None: shards_area_mask = pillow_image_from_input(shards_area_mask, is_mask=True) # 遍历原始图像的每个位置 for y in range(0, height, shard_size): for x in range(0, width, shard_size): # 检查遮罩图像对应位置是否为有效位置 if shards_area_mask is not None and shards_area_mask.getpixel((x, y)) == 0: continue # 计算碎片的实际尺寸 actual_width, actual_height = get_shard_dimensions(x, y, shard_size, width, height) if actual_width <= 0 or actual_height <= 0: continue # 从原始图片中提取单个碎片图像 shard_img = extract_shard_image(original_img, x, y, actual_width, actual_height) # 为碎片图像应用遮罩形状和遮罩图像 shard_img = apply_shard_mask(shard_img, shard_shape, shard_mask_image, actual_width, actual_height) # 生成单个碎片的运动参数 movement_params = generate_shard_movement_params(speed, rotation, frame_count, fade_start) # 创建单个碎片的完整信息 shard_info = create_shard_info(shard_img, x, y, speed, rotation, frame_count, fade_start) shards.append(shard_info) filelist:list[str]=[] image_list = [] # 生成每一帧 for frame in range(frame_count): # 创建透明背景 frame_img = Image.new("RGBA", (width, height), (0, 0, 0, 0)) draw = ImageDraw.Draw(frame_img) # 绘制所有碎片 for shard in shards: update_shard_position_and_rotation(shard, frame) update_shard_opacity(shard, frame) rotated_shard = rotate_and_apply_opacity(shard) draw_x = shard['x'] - (rotated_shard.width - shard['width']) / 2 draw_y = shard['y'] - (rotated_shard.height - shard['height']) / 2 frame_img.paste(rotated_shard, (int(draw_x), int(draw_y)), rotated_shard) # get file path name_id = text_padd(str(frame + frame_start_index), 'padd-start', name_id_length, '0') filepath = str(Path(f"{output_folder}/{name_prefix}{name_id}.png").as_posix()) filelist.append(filepath) print(f"已生成帧 {frame + 1}/{frame_count}") # 保存帧 if save_image: frame_img.save(filepath) if show_image: image_list.append(frame_img) images=[] masks=[] has_non_empty_mask = False if show_image: for img in image_list: image,mask,has_mask = tensor_image_and_mask_from_path_or_pillow(img) images.append(image) masks.append(mask) if has_mask: has_non_empty_mask = True images,masks,has_non_empty_mask=tensor_image_and_mask_batch_from_list(images, masks, has_non_empty_mask) return (filelist,images,masks,has_non_empty_mask)