774 lines
29 KiB
Python
774 lines
29 KiB
Python
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)
|
|
|