import os import torch import numpy as np from PIL import ImageOps, Image def pil_to_tensor(image_pil): i = ImageOps.exif_transpose(image_pil) image = i.convert("RGB") image = np.array(image).astype(np.float32) / 255.0 image_tensor = torch.from_numpy(image)[None,] return image_tensor def tensor_to_pil(image_tensor): image = image_tensor[0] i = 255. * image.cpu().numpy() image_pil = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)).convert("RGB") return image_pil def tensor_to_np(image_tensor): image = image_tensor[0] i = 255. * image.cpu().numpy() image_np = np.clip(i, 0, 255).astype(np.uint8) return image_np def np_to_tensor(image_np): image = image_np.astype(np.float32) / 255.0 image_tensor = torch.from_numpy(image)[None,] return image_tensor def load_images_from_folder(folder, ): """从目录读取图片""" images = [] files = os.listdir(folder) png_files = [f for f in files if f.endswith('.png')] # png_files.sort(key=lambda x: int(x.split('_')[1].split('.')[0])) for filename in png_files: img = Image.open(os.path.join(folder, filename)).convert('RGB') # img = img.resize((width, height)) images.append(pil_to_tensor(img)) return torch.cat(images, dim=0) import sys from functools import reduce FLOAT = ("FLOAT", {"default": 1, "min": -sys.float_info.max, "max": sys.float_info.max, "step": 0.01}) BOOLEAN = ("BOOLEAN", {"default": True}) BOOLEAN_FALSE = ("BOOLEAN", {"default": False}) INT = ("INT", {"default": 1, "min": -sys.maxsize, "max": sys.maxsize, "step": 1}) STRING = ("STRING", {"default": ""}) STRING_ML = ("STRING", {"multiline": True, "default": ""}) STRING_WIDGET = ("STRING", {"forceInput": True}) JSON_WIDGET = ("JSON", {"forceInput": True}) METADATA_RAW = ("METADATA_RAW", {"forceInput": True}) class AnyType(str): """A special class that is always equal in not equal comparisons. Credit to pythongosssss""" def __eq__(self, _) -> bool: return True def __ne__(self, __value: object) -> bool: return False any = AnyType("*") class FormatData: """ 一个用于格式化图像或掩码数据的基础类,支持将不同类型的图像或掩码数据转换为指定的数据类型和形状。 可以通过FormatImage或FormatMask函数快速切换需要的格式化目标类型和形状。pil作为目标时,shape、dtype参数无效 """ valid_types = (torch.Tensor, np.ndarray, Image.Image) valid_shapes = () def __init__(self): pass @classmethod def to_np(cls, data, shape=None, dtype=None): data = cls._format_data(data, target_type=np.ndarray, shape=shape, dtype=dtype) return data @classmethod def to_torch(cls, data, shape=None, dtype=None): data = cls._format_data(data, target_type=torch.Tensor, shape=shape, dtype=dtype) return data @classmethod def _format_data(cls, data, target_type, shape=None, dtype=None): if not isinstance(data, cls.valid_types): raise TypeError(f"data must be either a numpy.ndarray, a torch.Tensor, a tensorflow.Tensor or a PIL.Image.Image. Got {type(data)} instead.") instance = cls() if target_type is np.ndarray: if isinstance(data, np.ndarray): data = instance.convert_dtype(data, dtype) elif isinstance(data, torch.Tensor): data = data.detach().cpu().numpy() data = instance.convert_dtype(data, dtype) elif isinstance(data, Image.Image): data = ImageOps.exif_transpose(data) data = np.array(data) data = instance.convert_dtype(data, dtype) data = instance._adjust_shape(data, shape) elif target_type is torch.Tensor: if isinstance(data, torch.Tensor): data = data.cpu() data = instance._adjust_shape(data, shape) data = instance.convert_dtype(data, dtype) elif isinstance(data, np.ndarray): data = torch.from_numpy(data) data = instance._adjust_shape(data, shape) data = instance.convert_dtype(data, dtype) elif isinstance(data, Image.Image): data = ImageOps.exif_transpose(data) data = data.convert("RGB") data = np.array(data) data = torch.from_numpy(data)[None,] data = instance._adjust_shape(data, shape) data = instance.convert_dtype(data, dtype) elif target_type is Image.Image: images_list = [] if isinstance(data, Image.Image): images_list.append(ImageOps.exif_transpose(data)) # 调整握持方向 else: for img in data if len(data.shape) > 3 or (len(data.shape) == 3 and data.shape[-1] not in [1, 3, 4]) else [data]: if isinstance(img, np.ndarray): img_np = instance.convert_dtype(img, np.uint8) elif isinstance(img, torch.Tensor): img_np = instance.convert_dtype(img.detach().cpu().numpy(), np.uint8) images_list.append(Image.fromarray(img_np)) # 不需要调用 instance._adjust_shape 方法,也不需要处理 dtype if dtype is not None or shape is not None: images_list = [img.convert('RGB') for img in images_list] data = images_list else: raise TypeError(f"Unsupported target type: {target_type}") return data @staticmethod def convert_dtype(data, dtype): """ 根据目标数据类型转换数据,并确保值在有效范围内。 """ # 如果 dtype 为 None,则不转换 if dtype is None: return data # 确定当前数据类型 if isinstance(data, np.ndarray): current_dtype = data.dtype module = np elif isinstance(data, torch.Tensor): current_dtype = data.dtype module = torch else: raise TypeError("Unsupported Current Object type conversion dtype.") # 根据目标类型进行转换 if dtype == module.uint8: # 根据目标类型进行转换 if dtype == module.uint8: min_val = module.min(data) max_val = module.max(data) if current_dtype in [module.float16, module.float32, module.float64]: if 0 <= min_val <= max_val <= 1: data = FormatData._clip_value(data * 255, 0, 255, module.uint8) elif -1 <= min_val <= max_val <= 2: data = FormatData._clip_value((data.clip(0, 1) * 255), 0, 255, module.uint8) elif 0 <= min_val <= max_val <= 255: data = data.astype(module.uint8) if module is np else data.to(torch.uint8) else: data = FormatData._clip_value(data, 0, 255, module.uint8) elif current_dtype == module.bool_ if hasattr(module,"bool_") else module.bool: data = data.astype(module.uint8) * 255 if module is np else data.to(torch.uint8) elif current_dtype == module.uint8: pass else: raise ValueError("Unsupported dtype for conversion to uint8.") elif dtype in [module.float16, module.float32, module.float64]: min_val = module.min(data) max_val = module.max(data) if current_dtype == module.uint8: data = data / 255.0 if module is np else data.float() / 255.0 elif current_dtype in [module.float16, module.float32, module.float64]: if min_val < 0 or max_val > 1: data = FormatData._clip_value(data, 0, 1, dtype) elif current_dtype == module.bool: data = data.astype(module.float32) * 1.0 if module is np else data.to(torch.float32) * 1.0 else: raise ValueError("Unsupported dtype for conversion to float.") elif dtype == module.bool: if current_dtype in [module.uint8, module.float16, module.float32, module.float64]: data = data > 0 if module is np else data > 0 elif current_dtype == module.bool: pass else: raise ValueError("Unsupported dtype for conversion to bool.") else: data = data.astype(dtype) if module is np else data.to(dtype) return data @staticmethod def _clip_value(data, min_val, max_val, dtype): """根据数据类型剪切张量/数组的值并将其转换为指定的 dtype.""" if isinstance(data, np.ndarray): return np.clip(data, min_val, max_val).astype(dtype) elif isinstance(data, torch.Tensor): return torch.clamp(data, min_val, max_val).to(dtype) def _transpose(data, axes): """ 根据输入张量的类型选择合适的转置方法。 参数: - data: 输入的图像或掩码数据。 - axes: 转置轴。 """ if isinstance(data, np.ndarray): return data.transpose(axes) elif isinstance(data, torch.Tensor): return data.permute(axes) else: raise TypeError(f"Unsupported tensor type: {type(data)}") def _adjust_shape(self, data, shape=None): """ 调整图像或掩码数据的形状。 参数: - data: 输入的图像或掩码数据。 - shape: 目标形状。 """ raise NotImplementedError("Subclasses should implement this method.") def check_data(data): if isinstance(data, np.ndarray): return np.ndarray, data.dtype elif isinstance(data, torch.Tensor): return torch.Tensor, data.dtype elif isinstance(data, Image.Image): return Image.Image, data.mode else: return None, None class FormatImage(FormatData): """ 一个用于格式化图像数据的类,支持将不同类型的图像数据转换为指定的数据类型和形状。 """ valid_shapes = ['hwc', 'chw', 'bhwc', 'bchw'] @classmethod def _adjust_shape(cls, image, shape='bhwc'): """ 调整图像数据的形状。 参数: - image: 输入的图像数据。 - shape: 目标形状。 """ return cls.convert_shape(image, shape) @staticmethod def check_shape(image): """ 检查图像数据的形状,并返回一个描述形状的字符串和高度、宽度的元组。 参数: - image: 输入的图像数据,可以是 numpy 数组、torch 张量、tensorflow 张量或 PIL 图像。 返回: - str: 描述输入数据形状的字符串。 - tuple: 包含高度和宽度的元组。 """ if not isinstance(image, FormatData.valid_types): raise TypeError(f"Unsupported image type: {type(image)}") if isinstance(image, Image.Image): # PIL 图像转换为 numpy 数组,并获取宽度和高度 width, height = image.size return 'wh', (height, width) shape = image.shape ndim = len(shape) # 假设最后一个维度是颜色通道 channels = shape[-1] if ndim > 2 else 1 if ndim == 2: return 'hw', (shape[0], shape[1]) elif ndim == 3: if channels in [1, 3, 4]: return 'hwc', (shape[0], shape[1]) else: return 'bhw', (shape[1], shape[2]) elif ndim == 4: if channels in [1, 3, 4]: return 'bhwc', (shape[1], shape[2]) else: return 'bchw', (shape[2], shape[3]) else: raise ValueError(f"Unsupported number of dimensions: {ndim}") @staticmethod def convert_shape(image, target_shape='bhwc'): """ 调整图像数据的形状。 参数: - image: 输入的图像数据。 - target_shape: 目标形状。 返回: - 调整后的图像数据。 """ if target_shape is None: target_shape = 'bhwc' if target_shape not in FormatImage.valid_shapes: raise ValueError(f"Invalid shape: {target_shape}. Supported shapes are {FormatImage.valid_shapes}.") src_shape, _ = FormatImage.check_shape(image) if src_shape == "bhw": src_shape = "chw" # 假定调用者确定输入的一定是image而不是mask,就把第一个维度看做c而不是b # 定义从输入形状到目标形状的映射逻辑 if src_shape == 'hwc' and target_shape == 'hwc': pass # 已经是 hwc 形状 elif src_shape == 'hwc' and target_shape == 'chw': image = FormatImage._transpose(image, (2, 0, 1)) # 转置到 chw 形状 elif src_shape == 'hwc' and target_shape == 'bhwc': image = image[None, ...] # 增加批次 elif src_shape == 'hwc' and target_shape == 'bchw': image = FormatImage._transpose(image, (2, 0, 1))[None, ...] # 转置到 chw 形状然后增加批次 elif src_shape == 'chw' and target_shape == 'hwc': image = FormatImage._transpose(image, (1, 2, 0)) # 转置到 hwc 形状 elif src_shape == 'chw' and target_shape == 'chw': pass # 已经是 chw 形状 elif src_shape == 'chw' and target_shape == 'bhwc': image = FormatImage._transpose(image, (1, 2, 0))[None, ...] # 转置到 hwc 形状然后增加批次 elif src_shape == 'chw' and target_shape == 'bchw': image = image[None, ...] # 增加批次 elif src_shape == 'bhwc' and target_shape == 'hwc': image = image.squeeze(0) # 去掉批次 elif src_shape == 'bhwc' and target_shape == 'chw': image = FormatImage._transpose(image, (0, 3, 1, 2)).squeeze(0) # 转置到 chw 形状并去掉批次 elif src_shape == 'bhwc' and target_shape == 'bhwc': pass # 已经是 bhwc 形状 elif src_shape == 'bhwc' and target_shape == 'bchw': image = FormatImage._transpose(image, (0, 3, 1, 2)) # 转置到 bchw 形状 elif src_shape == 'bchw' and target_shape == 'hwc': image = FormatImage._transpose(image, (0, 2, 3, 1)).squeeze(0) # 转置到 hwc 形状并去掉批次 elif src_shape == 'bchw' and target_shape == 'chw': image = image.squeeze(0) # 去掉批次 elif src_shape == 'bchw' and target_shape == 'bhwc': image = FormatImage._transpose(image, (0, 2, 3, 1)) # 转置到 bhwc 形状 elif src_shape == 'bchw' and target_shape == 'bchw': pass # 已经是 bchw 形状 elif src_shape == 'hw' and target_shape == 'hwc': image = image[..., None] # 增加末位颜色通道 elif src_shape == 'hw' and target_shape == 'chw': image = image[None, ...] # 增加首位颜色通道 elif src_shape == 'hw' and target_shape == 'bhwc': image = image[None, ..., None] # 首尾增加批次和颜色通道 elif src_shape == 'hw' and target_shape == 'bchw': image = image[None, None, ...] # 前两位增加批次和颜色通道 else: raise ValueError(f"Unsupported conversion from {src_shape} to {target_shape}") return image class FormatMask(FormatData): """ 一个用于格式化掩码数据的类,支持将不同类型的掩码数据转换为指定的数据类型和形状。 """ valid_shapes = ['hw', 'bhw', 'bhwc', 'bchw'] @classmethod def _adjust_shape(cls, mask, shape='bhw'): """ 调整掩码数据的形状。 参数: - mask: 输入的掩码数据。 - shape: 目标形状。 """ return cls.convert_shape(mask, shape) @staticmethod def check_shape(mask): """ 检查掩码数据的形状,并返回一个描述形状的字符串。 参数: - mask: 输入的掩码数据。 返回: - str: 描述输入数据形状的字符串。 """ if not isinstance(mask, FormatData.valid_types): raise TypeError(f"Unsupported mask type: {type(mask)}") if isinstance(mask, Image.Image): # PIL 图像转换为 numpy 数组 mask = np.array(mask) shape = mask.shape ndim = len(shape) # 假设最后一个维度是颜色通道 channels = mask.shape[-1] if ndim == 2: return 'hw', shape elif ndim == 3: if channels in [1, 3, 4]: return 'hwc', shape else: return 'bhw', shape elif ndim == 4: if channels in [1, 3, 4]: return 'bhwc', shape else: return 'bchw', shape else: raise ValueError(f"Unsupported number of dimensions: {ndim}") @staticmethod def convert_shape(mask, target_shape='bhw'): """ 调整掩码数据的形状。 参数: - mask: 输入的掩码数据。 - target_shape: 目标形状。 返回: - 调整后的掩码数据。 """ if target_shape is None: target_shape = 'bhw' if target_shape not in FormatMask.valid_shapes: raise ValueError(f"Invalid shape: {target_shape}. Supported Mask shapes are {FormatMask.valid_shapes}.") src_shape, _ = FormatMask.check_shape(mask) # 定义从输入形状到目标形状的映射逻辑 if src_shape == 'hw' and target_shape == 'hw': pass # 已经是 hw 形状 elif src_shape == 'hw' and target_shape == 'bhw': mask = mask[None, ...] # 增加批次 elif src_shape == 'hw' and target_shape == 'bhwc': mask = mask[None, ..., None] # 首尾增加批次和颜色通道 elif src_shape == 'hw' and target_shape == 'bchw': mask = mask[None, None, ...] # 增加前两位批次和颜色通道 elif src_shape == 'bhw' and target_shape == 'hw': mask = mask.squeeze(0) # 去掉首位批次 elif src_shape == 'bhw' and target_shape == 'bhw': pass # 已经是 bhw 形状 elif src_shape == 'bhw' and target_shape == 'bhwc': mask = mask[..., None] # 增加颜色通道 elif src_shape == 'bhw' and target_shape == 'bchw': mask = mask[..., None] # 增加颜色通道 mask = FormatData._transpose(mask, (0, 3, 1, 2)) # 转置成 bchw 形状 elif src_shape == 'hwc' and target_shape == 'hw': mask = mask.squeeze(-1) # 去掉末位颜色通道 elif src_shape == 'hwc' and target_shape == 'bhw': mask = mask.squeeze(-1)[None, ...] # 去掉末位颜色通道并增加批次通道 elif src_shape == 'hwc' and target_shape == 'bhwc': mask = mask[None, ...] # 增加批次 elif src_shape == 'hwc' and target_shape == 'bchw': mask = FormatData._transpose(mask, (2, 0, 1))[None, ...] # 转置并增加批次 elif src_shape == 'bhwc' and target_shape == 'hw': mask = mask.squeeze(-1).squeeze(0) # 去掉末位颜色通道和首位批次 elif src_shape == 'bhwc' and target_shape == 'bhw': mask = mask.squeeze(-1) # 去掉末位颜色通道 elif src_shape == 'bhwc' and target_shape == 'bhwc': pass # 已经是 bhwc 形状 elif src_shape == 'bhwc' and target_shape == 'bchw': mask = FormatData._transpose(mask, (0, 3, 1, 2)) # 转置成 bchw 形状 elif src_shape == 'bchw' and target_shape == 'hw': mask = mask.squeeze(0).squeeze(0) # 去掉前两位批次和颜色通道 elif src_shape == 'bchw' and target_shape == 'bhw': mask = mask.squeeze(1) # 去掉第二位颜色通道 elif src_shape == 'bchw' and target_shape == 'bhwc': mask = FormatData._transpose(mask, (0, 2, 3, 1)) # 转置成 bhwc 形状 elif src_shape == 'bchw' and target_shape == 'bchw': pass # 已经是 bchw 形状 else: raise ValueError(f"Unsupported conversion from {src_shape} to {target_shape}") return mask import torch.nn.functional as F from comfy.utils import lanczos, bislerp # 导入 comfy.utils 模块 """当输入图像是PIL的还需要继续debug""" class Resize: valid_methods = ["just_resize", "crop_center", "crop_topleft", "fill_center", "fill_topleft", "align_center", "align_topleft"] valid_resamples = ['nearest-exact', 'bilinear', 'area', 'bicubic', 'lanczos'] @staticmethod def _format_data(data): """格式化图像或掩码为bchw形状的torch张量""" original_class, original_dtype = FormatData.check_data(data) is_mask = False original_shape_type = None original_shape = None if isinstance(data, Image.Image): is_mask = False original_shape_type = "wh" original_shape = data.size # data = FormatData._format_data(data, original_class, shape="bchw") return data, is_mask, original_shape_type, original_shape, original_dtype original_shape = data.shape original_dtype = data.dtype ndim = len(data.shape) # 检查是否为mask类型 if ndim == 2 or (ndim == 3 and data.shape[-1] not in [1, 3, 4]) or data.dtype == torch.bool: # 格式化掩码 is_mask = True original_shape_type, _ = FormatMask.check_shape(data) data = FormatMask.to_torch(data, shape="bchw") # 将布尔类型转为uint8 if original_dtype == np.bool_: data = data.astype(np.uint8) if original_dtype == torch.bool: data = data.to(torch.uint8) else: original_shape_type, _ = FormatImage.check_shape(data) if not isinstance(original_class, torch.Tensor) or ndim != 4 or data.shape[1] not in [1, 3, 4]: # 格式化图像 data = FormatImage.to_torch(data, shape="bchw") return data, is_mask, original_shape_type, original_shape, original_dtype @staticmethod def _check_resample_method(resample_method): """检查插值方法是否有效""" valid_methods = ['nearest-exact', 'bilinear', 'area', 'bicubic', 'lanczos', 'bislerp'] # 添加 bislerp 方法 if resample_method.lower() not in valid_methods: raise ValueError(f"无效的插值方法: {resample_method}. 有效的插值方法包括: {valid_methods}.") @staticmethod def _resize(data, width, height, resample_method="bicubic"): """使用torch和torchvision库调整图像大小""" Resize._check_resample_method(resample_method) # 选择插值方法 if resample_method.lower() == "lanczos": resized_data = lanczos(data, width, height) elif resample_method.lower() == "bislerp": resized_data = bislerp(data, width, height) else: # 对于其他方法,使用 torch 的 interpolate 函数 resized_data = torch.nn.functional.interpolate(data, size=(height, width), mode=resample_method) return resized_data @staticmethod def _restore_shape_and_dtype(data, is_mask, original_shape_type, original_shape, original_dtype): """ 恢复图像或掩码的原始形状和数据类型。 :param data: 调整大小并填充后的图像或掩码数据。 :param is_mask: 是否是掩码。 :param original_shape_type: 原始形状的字符串标识,如 'hw', 'bhw', 'hwc', 'bchw', 'bhwc'。 :param original_shape: 原始数据的维度数。 :param original_dtype: 原始数据类型。 :return: 恢复形状和数据类型的图像或掩码数据。 """ # 根据 original_dtype 校正像素值 if isinstance(original_dtype, torch.dtype): data = FormatImage.to_torch(data, original_shape_type, original_dtype) if not is_mask else FormatMask.to_torch(data, original_shape_type, original_dtype) if original_dtype == torch.uint8: data = torch.clamp(data, 0, 255) else: data = torch.clamp(data, 0, 1) elif isinstance(original_dtype, np.dtype): data = FormatImage.to_np(data, original_shape_type, original_dtype) if not is_mask else FormatMask.to_np(data, original_shape_type, original_dtype) if original_dtype == np.uint8: data = np.clip(data, 0, 255) else: data = np.clip(data, 0, 1) """ # 根据原始形状恢复数据 if original_shape_type == 'hw': # 单通道掩码,形状为 (h, w) data = data.squeeze(0).squeeze(0) elif original_shape_type == 'bhw': # 批量单通道掩码,形状为 (b, h, w) data = data.squeeze(1) elif original_shape_type == 'hwc': # 单张图像,形状为 (h, w, c) data = data.squeeze(0).permute(1, 2, 0) elif original_shape_type == 'bchw': # 批量图像,形状为 (b, c, h, w) data = data elif original_shape_type == 'bhwc': # 批量图像,形状为 (b, h, w, c) data = data.permute(0, 2, 3, 1) # 恢复数据类型 if isinstance(original_dtype, torch.dtype): data = data.type(original_dtype) elif isinstance(original_dtype, np.dtype): data = data.astype(original_dtype) elif isinstance(original_dtype, tf.DType): data = tf.cast(data, original_dtype) """ return data @staticmethod def just_resize(data, width, height, resample_method="bicubic"): """直接调整图像大小""" data, is_mask, original_shape_type, original_shape, original_dtype = Resize._format_data(data) Resize._check_resample_method(resample_method) # 调整图像大小 resized_data = Resize._resize(data, width, height, resample_method) # 恢复原始形状和数据类型 resized_data = Resize._restore_shape_and_dtype(resized_data, is_mask, original_shape_type, original_shape, original_dtype) return resized_data @staticmethod def crop_topleft(data, width, height, resample_method="bicubic"): """ 从图像的左上角开始裁剪,并调整大小到指定的宽度和高度。 :param data: 输入的图像数据,形状为 (b, c, h, w) 的 Tensor。 :param width: 目标宽度。 :param height: 目标高度。 :param resample_method: 重采样方法,默认为 "bicubic"。 :return: 裁剪并调整大小后的图像数据,形状为 (b, c, height, width) 的 Tensor。 """ data, is_mask, original_shape_type, original_shape, original_dtype = Resize._format_data(data) Resize._check_resample_method(resample_method) # 获取原始图像的尺寸 old_width = data.shape[3] old_height = data.shape[2] old_aspect = old_width / old_height new_aspect = width / height # 计算裁剪的宽度和高度 if old_aspect > new_aspect: # 宽度大于目标宽高比,需要裁剪宽度 crop_width = int(old_height * new_aspect) cropped_data = data[:, :, :, :crop_width] elif old_aspect < new_aspect: # 高度大于目标宽高比,需要裁剪高度 crop_height = int(old_width / new_aspect) cropped_data = data[:, :, :crop_height, :] else: # 宽高比相同,不需要裁剪 cropped_data = data # 调整图像大小 resized_data = Resize._resize(cropped_data, width, height, resample_method) # 恢复原始形状和数据类型 resized_data = Resize._restore_shape_and_dtype(resized_data, is_mask, original_shape_type, original_shape, original_dtype) return resized_data @staticmethod def crop_bottomleft(data, width, height, resample_method="bicubic"): """ 从图像的左下角开始裁剪,并调整大小到指定的宽度和高度。 :param data: 输入的图像数据,形状为 (b, c, h, w) 的 Tensor。 :param width: 目标宽度。 :param height: 目标高度。 :param resample_method: 重采样方法,默认为 "bicubic"。 :return: 裁剪并调整大小后的图像数据,形状为 (b, c, height, width) 的 Tensor。 """ data, is_mask, original_shape_type, original_shape, original_dtype = Resize._format_data(data) Resize._check_resample_method(resample_method) # 获取原始图像的尺寸 old_width = data.shape[3] old_height = data.shape[2] old_aspect = old_width / old_height new_aspect = width / height # 计算裁剪的宽度和高度 if old_aspect > new_aspect: # 宽度大于目标宽高比,需要裁剪宽度 crop_width = int(old_height * new_aspect) # 从左边界往右裁剪 crop_width 个像素 cropped_data = data[:, :, :, :crop_width] elif old_aspect < new_aspect: # 高度大于目标宽高比,需要裁剪高度 crop_height = int(old_width / new_aspect) # 从底部边界往上裁剪 crop_height 个像素 cropped_data = data[:, :, crop_height:, :] else: # 宽高比相同,不需要裁剪 cropped_data = data # 调整图像大小 resized_data = Resize._resize(cropped_data, width, height, resample_method) # 恢复原始形状和数据类型 resized_data = Resize._restore_shape_and_dtype(resized_data, is_mask, original_shape_type, original_shape, original_dtype) return resized_data @staticmethod def crop_center(data, width, height, resample_method="bicubic"): """先根据目标宽高比裁剪中心部分,再调整大小""" data, is_mask, original_shape_type, original_shape, original_dtype = Resize._format_data(data) Resize._check_resample_method(resample_method) # 获取原始图像的尺寸 old_width = data.shape[3] old_height = data.shape[2] old_aspect = old_width / old_height new_aspect = width / height # 计算裁剪的起始位置 x = 0 y = 0 if old_aspect > new_aspect: # 宽度大于目标宽高比,需要裁剪两侧 x = round((old_width - old_width * (new_aspect / old_aspect)) / 2) elif old_aspect < new_aspect: # 高度大于目标宽高比,需要裁剪上下 y = round((old_height - old_height * (old_aspect / new_aspect)) / 2) # 裁剪中心部分 cropped_data = data[:, :, y:old_height - y, x:old_width - x] # 调整图像大小 resized_data = Resize._resize(cropped_data, width, height, resample_method) # 恢复原始形状和数据类型 resized_data = Resize._restore_shape_and_dtype(resized_data, is_mask, original_shape_type, original_shape, original_dtype) return resized_data @staticmethod def fill_topleft(data, width, height, resample_method="bicubic"): """ 调整大小并将图像填充到与目标左上角对齐的位置。 """ data, is_mask, original_shape_type, original_shape, original_dtype = Resize._format_data(data) Resize._check_resample_method(resample_method) # 计算缩放因子 scale_factor_w = width / data.shape[3] scale_factor_h = height / data.shape[2] scale_factor = min(scale_factor_w, scale_factor_h) # 计算调整后的尺寸 resized_width = int(data.shape[3] * scale_factor) resized_height = int(data.shape[2] * scale_factor) # 调整图像大小 resized_data = Resize._resize(data, resized_width, resized_height, resample_method) # 创建填充后的图像 filled_data = resized_data.clone() # 计算需要填充的尺寸 pad_left = 0 pad_top = 0 pad_right = max(width - resized_data.shape[3], 0) pad_bottom = max(height - resized_data.shape[2], 0) # 使用 F.pad 进行填充 padding_params = [pad_left, pad_right, pad_top, pad_bottom] # [left, right, top, bottom] filled_data = F.pad(filled_data, padding_params, mode='constant', value=0) # 恢复原始形状和数据类型 filled_data = Resize._restore_shape_and_dtype(filled_data, is_mask, original_shape_type, original_shape, original_dtype) return filled_data @staticmethod def fill_center(data, width, height, resample_method="bicubic"): """调整大小并填充至目标中心""" data, is_mask, original_shape_type, original_shape, original_dtype = Resize._format_data(data) Resize._check_resample_method(resample_method) # 计算缩放因子 scale_factor_w = width / data.shape[3] scale_factor_h = height / data.shape[2] scale_factor = min(scale_factor_w, scale_factor_h) # 计算调整后的尺寸 resized_width = int(data.shape[3] * scale_factor) resized_height = int(data.shape[2] * scale_factor) # 调整图像大小 resized_data = Resize._resize(data, resized_width, resized_height, resample_method) # 创建填充后的图像 filled_data = resized_data.clone() # 计算需要填充的尺寸 pad_left = (width - resized_data.shape[3]) // 2 pad_top = (height - resized_data.shape[2]) // 2 # 如果宽度不够,则在左右两侧添加零填充 if width > resized_data.shape[3]: left_pad = torch.zeros((data.shape[0], data.shape[1], resized_data.shape[2], pad_left), dtype=data.dtype) right_pad = torch.zeros((data.shape[0], data.shape[1], resized_data.shape[2], width - resized_data.shape[3] - pad_left), dtype=data.dtype) filled_data = torch.cat((left_pad, filled_data, right_pad), dim=3) # 如果高度不够,则在上下两侧添加零填充 if height > resized_data.shape[2]: top_pad = torch.zeros((data.shape[0], data.shape[1], pad_top, filled_data.shape[3]), dtype=data.dtype) bottom_pad = torch.zeros((data.shape[0], data.shape[1], height - resized_data.shape[2] - pad_top, filled_data.shape[3]), dtype=data.dtype) filled_data = torch.cat((top_pad, filled_data, bottom_pad), dim=2) # 恢复原始形状和数据类型 filled_data = Resize._restore_shape_and_dtype(filled_data, is_mask, original_shape_type, original_shape, original_dtype) return filled_data @staticmethod def align_topleft(data, width, height, resample_method=None): """ 在原图右侧和下方进行填充以达到目标宽高,如果原图宽或高大于目标宽高,则执行裁剪。 :param data: 输入的图像数据,形状为 (b, c, h, w) 的 Tensor。 :param resample_method: 可选参数,目前未使用。 """ data, is_mask, original_shape_type, original_shape, original_dtype = Resize._format_data(data) current_height, current_width = data.shape[2], data.shape[3] # 如果原图宽或高大于目标宽高,则执行裁剪 if current_width > width: data = data[:, :, :, :max(width, 1)] if current_height > height: data = data[:, :, :max(height, 1), :] current_height, current_width = data.shape[2], data.shape[3] pad_right = max(width - current_width, 0) pad_bottom = max(height - current_height, 0) # 计算填充参数 padding_params = [0, pad_right, 0, pad_bottom] # [left, right, top, bottom] # 使用 F.pad 进行填充 data = F.pad(data, padding_params, mode='constant', value=0) aligned_data = Resize._restore_shape_and_dtype(data, is_mask, original_shape_type, original_shape, original_dtype) return aligned_data @staticmethod def align_center(data, width, height, resample_method=None): """ 在原图像四周进行填充以达到目标宽高,如果原图宽或高大于目标宽高,则执行裁剪。 :param data: 输入的图像数据,形状为 (b, c, h, w) 的 Tensor。 :param width: 目标宽度。 :param height: 目标高度。 :param resample_method: 可选参数,目前未使用。 :return: 填充后的图像数据,形状为 (b, c, height, width) 的 Tensor。 """ data, is_mask, original_shape_type, original_shape, original_dtype = Resize._format_data(data) current_height, current_width = data.shape[2], data.shape[3] # 如果原图宽或高大于目标宽高,则执行裁剪 if current_width > width: # 计算从两侧均匀裁剪的起始位置 start_col = (current_width - width) // 2 end_col = start_col + width data = data[:, :, :, start_col:end_col] if current_height > height: # 计算从两侧均匀裁剪的起始位置 start_row = (current_height - height) // 2 end_row = start_row + height data = data[:, :, start_row:end_row, :] current_height, current_width = data.shape[2], data.shape[3] # 计算需要填充的尺寸 pad_left = max((width - current_width) // 2, 0) pad_right = max(width - current_width - pad_left, 0) pad_top = max((height - current_height) // 2, 0) pad_bottom = max(height - current_height - pad_top, 0) # 计算填充参数 padding_params = [pad_left, pad_right, pad_top, pad_bottom] # 使用 F.pad 进行填充 data = F.pad(data, padding_params, mode='constant', value=0) aligned_data = Resize._restore_shape_and_dtype(data, is_mask, original_shape_type, original_shape, original_dtype) return aligned_data # 创建模型简化映射字典和访问器 class SimpMapAccessor: def __init__(self, data_structure): self.name = "SimpMapAccessor" is_dict = self._extract_is_dict(data_structure.copy()) self._mapping = self._prepare_mapping(data_structure.copy(), is_dict) self._accessor = self._create_accessor(is_dict) def _extract_is_dict(self, data_structure): is_dict = False if 'is_dict' in data_structure: is_dict = data_structure.pop('is_dict') else: print(f"{self.name} No 'is_dict' found in data structure, using default False.") return is_dict def _prepare_mapping(self, data_structure, is_dict): orig_get = [] simp_get = [] if 'custom_map' in data_structure: custom_map = data_structure.pop('custom_map') for simp_key, orig_key in custom_map.items(): simp_get.append(simp_key) orig_get.append(orig_key) self._build_map(data_structure, orig_get, "", simp_get, "", is_dict) return dict(zip(simp_get, orig_get)) def _build_map(self, data, orig_get, prefix_orig, simp_get, prefix_simp, is_dict=False): for key, value in data.items(): if isinstance(value, dict): new_simp = f"{prefix_simp}{self._simplify_obj_name(simp_get, key)}" new_orig = f"{prefix_orig},{key}" if prefix_orig else f"{key}" self._build_map(value, orig_get, new_orig, simp_get, new_simp, is_dict) else: simple_key = self._simplify_attr_name(key) simp_get.append(f"{prefix_simp}{simple_key}") orig_get.append(f"{prefix_orig},{key}" if prefix_orig else f"{key}") def _simplify_obj_name(self, simp_key, key): simplified = "" index = 0 while True: if index < len(key): char = key[index] temp_simplified = simplified + char if temp_simplified not in simp_key: simplified = temp_simplified break index += 1 else: raise ValueError(f"Could not find unique simplified name for key {key}.") return f'{simplified}_' def _simplify_attr_name(self, key): if '_' in key and key.rfind('_') != len(key) - 1: return key.rsplit('_', 1)[-1] else: return key def _create_accessor(self, is_dict): if is_dict: return self._create_dict_accessor else: return self._create_object_accessor def _create_object_accessor(self, parts): def object_accessor(r): try: return reduce(getattr, parts.split(','), r) except AttributeError: print(f"Failed to access attribute path: {'.'.join(parts.split(','))}") return None return object_accessor def _create_dict_accessor(self, parts): def dict_accessor(r): try: for part in parts.split(','): part = part.strip("['").strip("']") if isinstance(r, dict) and part in r: r = r[part] else: print(f"Key '{part}' not found in dictionary.") return None return r except KeyError: print(f"Failed to access key path: {'.'.join(parts.split(','))}") return None return dict_accessor # 张量类型转换 def to_torch(tensor, dtype): if isinstance(tensor, np.ndarray): return torch.from_numpy(tensor).to(dtype) else: return tensor.to(dtype) def to_np(tensor, dtype): if isinstance(tensor, torch.Tensor): return tensor.cpu().numpy().astype(dtype) else: return tensor.astype(dtype) def align_properties(target_tensor, reference_tensor): """ 验证并转换输入对象的张量类型、设备和数据类型。 参数: target_tensor: 第一个张量对象,用于确定目标类型、设备和数据类型。 reference_tensor: 第二个张量对象,将被转换为与目标一致。 返回: Tuple[Any, Any]: 验证并转换后的两个对象。 其他说明: TensorFlow 的张量在设计上不直接支持设备的概念,而是通过设备上下文(如 tf.device)来管理设备。因此,在 to_tf 函数中不需要也不应该直接处理设备参数。TensorFlow 会自动管理张量的设备分配,通常不需要手动指定设备。 """ # 判断 target_tensor 的类型并进行相应处理 if isinstance(target_tensor, torch.Tensor): target_device = target_tensor.device target_dtype = target_tensor.dtype convert_func = to_torch elif isinstance(target_tensor, np.ndarray): target_device = None target_dtype = target_tensor.dtype convert_func = to_np else: raise TypeError("target_tensor must be either a numpy.ndarray, a torch.Tensor or a tensorflow.Tensor.") # 判断 reference_tensor 的类型并进行相应处理 if not isinstance(reference_tensor, (torch.Tensor, np.ndarray,)): raise TypeError("reference_tensor must be either a numpy.ndarray, a torch.Tensor or a tensorflow.Tensor.") # 根据 target_tensor 的类型调整 reference_tensor reference_tensor = convert_func(reference_tensor, target_dtype) # 对于 PyTorch 张量,还需要对设备进行对齐 if isinstance(target_tensor, torch.Tensor) and isinstance(reference_tensor, torch.Tensor): reference_tensor = reference_tensor.to(device=target_device) return target_tensor, reference_tensor def get_shape(tensor): """ 获取张量的形状。 参数: tensor: 输入张量,可以是 numpy.ndarray、torch.Tensor、tf.Tensor 或 PIL.Image.Image。 返回: tuple: 张量的形状。 """ if isinstance(tensor, Image.Image): return tensor.size[::-1] elif isinstance(tensor, np.ndarray): return tensor.shape elif isinstance(tensor, torch.Tensor): return tensor.shape def pad_tensor_shape(tensor, target_shape): """ 对张量进行填充以匹配目标形状。 参数: tensor: 输入张量,可以是 numpy.ndarray、torch.Tensor、tf.Tensor 或 PIL.Image.Image。 target_shape (tuple): 目标形状。 返回: 同类型张量: 对齐后的张量。 """ if isinstance(tensor, Image.Image): # PIL 图像的填充 current_shape = get_shape(tensor) if current_shape == target_shape: return tensor padded_image = Image.new('RGB', (target_shape[0], target_shape[1])) padded_image.paste(tensor, (0, 0)) return padded_image elif isinstance(tensor, np.ndarray): # Numpy 数组的填充 current_shape = get_shape(tensor) if current_shape == target_shape: return tensor pad_width = [(0, target_shape[i] - current_shape[i]) for i in range(len(current_shape))] return np.pad(tensor, pad_width, mode='constant') elif isinstance(tensor, torch.Tensor): # PyTorch 张量的填充 current_shape = get_shape(tensor) if current_shape == target_shape: return tensor pad_width = [0 for _ in range(len(current_shape) * 2)] for i in range(len(current_shape)): if current_shape[i] < target_shape[i]: pad_width[i * 2 + 1] = target_shape[i] - current_shape[i] # padding right/bottom return F.pad(tensor, pad_width) def align_shapes(tensor1, tensor2): """ 对齐两个张量的形状,使得它们在所有维度上具有相同的大小。 如果一个张量在某个维度上的大小小于另一个张量,则该函数会在必要的维度上扩展该张量以匹配另一个张量的大小。 参数: tensor1: 第一个张量,可以是 numpy.ndarray、torch.Tensor、tf.Tensor 或 PIL.Image.Image。 tensor2: 第二个张量,可以是 numpy.ndarray、torch.Tensor、tf.Tensor 或 PIL.Image.Image。 返回: tuple: 包含对齐后的两个张量 (aligned_tensor1, aligned_tensor2)。 """ # 确保输入张量的类型为 numpy.ndarray、torch.Tensor、tf.Tensor 或 PIL.Image.Image if not (isinstance(tensor1, (np.ndarray, torch.Tensor, Image.Image))): raise ValueError("Unsupported type for tensor1.") if not (isinstance(tensor2, (np.ndarray, torch.Tensor, Image.Image))): raise ValueError("Unsupported type for tensor2.") # 获取两个张量的形状 shape1 = get_shape(tensor1) shape2 = get_shape(tensor2) # 检查维度数量是否相同 if len(shape1) != len(shape2): raise ValueError("The number of dimensions must be the same for both tensors.") # 计算两个张量的最大形状 max_shape = tuple(max(s1, s2) for s1, s2 in zip(shape1, shape2)) # 对齐张量1 aligned_tensor1 = pad_tensor_shape(tensor1, max_shape) # 对齐张量2 aligned_tensor2 = pad_tensor_shape(tensor2, max_shape) return aligned_tensor1, aligned_tensor2 def align_batch(tensor1, tensor2, mode='pad'): """ 对齐两个带有 batch 维度的张量,使得它们的 batch 数量相同。 如果一个张量的 batch 数量小于另一个张量,则该函数会在必要的维度上扩展该张量以匹配另一个张量的 batch 数量(pad模式), 或者截断较长的张量以匹配较短的张量的 batch 数量(truncate模式)。 参数: tensor1 (torch.Tensor): 第一个张量,形状为 (batch_size1, ...) tensor2 (torch.Tensor): 第二个张量,形状为 (batch_size2, ...) mode (str): 对齐模式,支持 'pad' 和 'truncate',默认为 'pad'。 返回: tuple: 包含对齐后的两个张量 (aligned_tensor1, aligned_tensor2)。 """ # 获取两个张量的 batch 维度大小 batch_size1 = tensor1.shape[0] batch_size2 = tensor2.shape[0] # 计算最大/最小 batch 维度大小 max_batch_size = max(batch_size1, batch_size2) min_batch_size = min(batch_size1, batch_size2) # 根据模式选择对齐方式 if mode == 'pad': # 对齐张量1 if batch_size1 < max_batch_size: aligned_tensor1 = pad_tensor_shape(tensor1, (max_batch_size,) + tensor1.shape[1:]) else: aligned_tensor1 = tensor1 # 对齐张量2 if batch_size2 < max_batch_size: aligned_tensor2 = pad_tensor_shape(tensor2, (max_batch_size,) + tensor2.shape[1:]) else: aligned_tensor2 = tensor2 elif mode == 'truncate': # 截断张量1 if batch_size1 > min_batch_size: aligned_tensor1 = tensor1[:min_batch_size] else: aligned_tensor1 = tensor1 # 截断张量2 if batch_size2 > min_batch_size: aligned_tensor2 = tensor2[:min_batch_size] else: aligned_tensor2 = tensor2 else: raise ValueError(f"Unsupported alignment mode '{mode}'. Supported modes are 'pad' and 'truncate'.") return aligned_tensor1, aligned_tensor2 def batch_dimension_to_list(batch_item, keep_shape=True): if keep_shape: item_list = [torch.unsqueeze(i, dim=0) for i in batch_item.clone()] # list中的元素从(b,)转为(1,),形状不变 else: item_list = [i.squeeze() for i in batch_item.clone()] # list中的元素从(b,)转为(1,),形状改变,失去batch维度 return item_list def list_to_batch_dimension(item_list, mode="concat"): if not isinstance(item_list, list): return item_list all_tensors = all(isinstance(item, torch.Tensor) for item in item_list) new_list = [] if all_tensors: if mode == "concat": # 对于 concat 模式,直接连接张量,不需要填充 return torch.cat(item_list, dim=0) elif mode == "stack": # 对于 stack 模式,需要确保所有张量的形状完全相同 max_shape = tuple(max(dim) for dim in zip(*[tensor.shape for tensor in item_list])) for tensor in item_list: padding = [] for dim, max_dim in zip(tensor.shape, max_shape): padding.append((0, max_dim - dim)) padding = sum(padding[::-1], ()) new_tensor = torch.nn.functional.pad(tensor, padding) new_list.append(new_tensor) return torch.stack(new_list, dim=0) else: raise ValueError(f"Invalid mode. Must be either 'concat' or 'stack'.") else: element_types = set(type(item) for item in item_list) if len(element_types) > 1: raise ValueError("When not all elements are tensors, the non-tensor elements should be of the same type.") elif element_types == {int} or element_types == {float}: return torch.tensor(item_list) elif element_types == {np.ndarray}: max_length = 0 for arr in item_list: max_length = max(max_length, len(arr)) for arr in item_list: padding_length = max_length - len(arr) if padding_length > 0: padding_array = np.zeros(padding_length, dtype=arr.dtype) new_arr = np.concatenate((arr, padding_array)) new_list.append(new_arr) else: new_list.append(arr) return np.stack(new_list) else: raise ValueError("Unsupported non-tensor element types.") ''' 以下是格式化颜色(Color)的方法 ''' def normalize_color(color): """ 将颜色转换为标准的整数格式。 参数: - color: 颜色,可以是 (R, G, B) 或 (R, G, B, A) 的浮点数形式, 十六进制字符串(例如 '#RRGGBB' 或 '#AARRGGBB'),或者 (R, G, B) 的整数形式。 返回: - color: 转换后的颜色,格式为 (R, G, B) 或 (R, G, B, A) 的整数形式。 """ if isinstance(color, tuple): # 处理 (R, G, B) 或 (R, G, B, A) 形式的颜色 if all(isinstance(c, int) and 0 <= c <= 255 for c in color): # 整数格式 return color elif all(isinstance(c, (int, float)) and 0 <= c <= 1 for c in color): # 浮点数格式 color = tuple(int(round(float(c) * 255)) for c in color) # 确保所有值都是浮点数 return color else: raise ValueError("Invalid color value range.") elif isinstance(color, str): # 处理十六进制颜色字符串 if color.startswith('#'): color = color[1:] # 移除开头的 '#' if len(color) == 6: # RGB color = tuple(int(color[i:i+2], 16) for i in (0, 2, 4)) elif len(color) == 8: # RGBA color = tuple(int(color[i:i+2], 16) for i in (0, 2, 4, 6)) else: raise ValueError("Invalid hexadecimal color format.") else: raise TypeError("Unsupported color type.") return color def format_float_color(color): color = normalize_color(color) return tuple(c / 255.0 for c in color) def format_int_color(color): return normalize_color(color) ''' 以下是一些数学(Math)算法 ''' import math def gcd(a, b): """计算最大公约数""" while b: a, b = b, a % b return a def simplest_ratio(a, b): """计算a、b的的最简比。即通过计算a、b的最大公约数,使其互质""" divisor = gcd(a, b) return a // divisor, b // divisor def closest_multiple(value, multiple=32): """计算最接近的整倍数。比较上下两个整数值的方法避免了浮点运算精度和性能可能出现的问题。""" if multiple == 0: raise ValueError("Multiple cannot be zero.") if not isinstance(value, (int, float)) or not isinstance(multiple, int): raise TypeError("Both value and multiple must be numbers.") lower = (value // multiple) * multiple # 向下取整 upper = ((value + multiple - 1) // multiple) * multiple # 向上取整 # 找到最接近的值 if upper - value < value - lower: return upper else: return lower def best_step(target, base=640, step_unit=64, max_steps=6): """ 根据给定的目标值、基数、步长单位和最大步数,计算最接近目标值的步数及其对应的步长值。 参数: target (int): 目标值。 base (int): 基数值,默认为640。 step_unit (int): 步长单位,默认为64。 max_steps (int): 最大步数,默认为6。 返回: tuple: 包含三个元素的元组: best_steps (int): 最佳步数。 best_step_value (int): 对应的最佳步长值。 min_offset (int): 实际值与目标值之间的最小偏移量。 """ # 计算目标值和基数之间的差值 diff = target - base # 计算差值除以步长单位的商(可以是小数) total_steps = diff / step_unit # 在最大步数到 2(最小步数)之间,找到使实际值与目标值之间距离最小的步数和步长值 best_result = None min_distance = float('inf') best_steps = None for steps in range(2, max_steps + 1): # 计算候选步长值 candidate_value = round(total_steps / steps) * step_unit # 计算候选步长值与目标值之间的距离 distance = target - (base + candidate_value * steps) # 如果当前距离更小,则更新最佳结果 if abs(distance) < abs(min_distance): min_distance = distance best_result = (steps, candidate_value) best_steps = steps elif abs(distance) == abs(min_distance): # 如果距离相同,则取较大的步数 if steps > best_steps: best_result = (steps, candidate_value) best_steps = steps # 解析最佳结果 best_steps, best_step_value = best_result # 计算实际值与目标值之间的偏移量 min_offset = base + best_step_value * best_steps - target return best_steps, best_step_value, min_offset def short_side_to(width, height, target_short_side=640, multiple=32): """ 根据给定的宽度和高度,按比例缩放以使短边等于目标短边长度,并确保最终的长边长度是特定整数倍数。 参数: width (int): 图像的宽度。 height (int): 图像的高度。 target_short_side (int): 目标短边长度,默认为 640。 multiple (int): 长边长度需要是此数的整数倍,默认为 32。 返回: tuple: 包含目标宽度和高度的元组 (target_width, target_height)。 """ # 确定短边和长边 short_side = min(width, height) long_side = max(width, height) # 计算缩放因子 scale_factor = target_short_side / short_side # 缩放长边 scaled_long_side = int(long_side * scale_factor) # 确保长边是 multiple 的倍数 target_long_side = int(np.floor(scaled_long_side / multiple) * multiple) # 根据长边确定目标宽度和高度 if width == short_side: target_width = target_short_side target_height = target_long_side else: target_width = target_long_side target_height = target_short_side return target_width, target_height def calc_target_size_by_aspect(source_width, source_height, target_resolution, aspect_ratio=None, factor=1): """ 根据给定的分辨率(像素数量)和宽高比计算目标尺寸。 :param source_width: 原始图像宽度 :param source_height: 原始图像高度 :param target_resolution: 目标分辨率 (像素数量) :param aspect_ratio: 字符串形式的宽高比,例如 "16:9";如果为 None,则使用原始宽高比 :param factor: 目标尺寸的宽度和高度应该是该值的整数倍,默认为 1,最大不超过 1024 :return: 目标宽度和高度 """ # 校验 aspect_ratio 是否合法 if aspect_ratio is not None: if not isinstance(aspect_ratio, str) or ':' not in aspect_ratio: raise ValueError("Aspect ratio must be a string containing a colon (:) and two integers.") ratio_parts = aspect_ratio.split(':') if len(ratio_parts) != 2 or not all(part.replace('.', '', 1).isdigit() for part in ratio_parts): raise ValueError("Aspect ratio must be in the format 'long_side:short_side'.") # 将字符串转换为浮点数并四舍五入为整数 long_side = int(round(float(ratio_parts[0]))) short_side = int(round(float(ratio_parts[1]))) # 确保 long_side 是较大的那个数,short_side 是较小的那个数 if long_side < short_side: long_side, short_side = short_side, long_side # 计算 long_side 和 short_side 的最简比 # long_side, short_side = simplest_ratio(long_side, short_side) # 限制 long_side / short_side 的比例 if long_side / short_side > 1000: raise ValueError("The aspect ratio cannot exceed 1000:1.") else: # 使用原始图像的宽高比 long_side, short_side = max(source_width, source_height), min(source_width, source_height) aspect_ratio = f"{long_side}:{short_side}" # 确保 long_side 和 short_side 互质 long_side, short_side = simplest_ratio(long_side, short_side) # 计算 unit_length adjusted_resolution = int(round(target_resolution / (factor ** 2))) unit_length = (adjusted_resolution / (long_side * short_side)) ** 0.5 # 如果指定了倍数,则确保单位长度是该倍数的整数倍 if factor < 1 or factor > 1024 or not isinstance(factor, int): raise ValueError("Factor must be a positive integer between 1 and 1024.") # 计算目标长边和短边 target_long_side = int(round(long_side * unit_length)) * factor target_short_side = int(round(short_side * unit_length)) * factor # 确保目标宽度和高度至少为 factor target_long_side = max(target_long_side, factor) target_short_side = max(target_short_side, factor) # 根据原始图像的方向确定目标宽度和高度 if source_width >= source_height: target_width = target_long_side target_height = target_short_side else: target_width = target_short_side target_height = target_long_side return target_width, target_height """ 验证输入数据的类型和形状,并返回原始数据类型和拆包后的数据。 参数: data: 输入数据,形状为 (n, 2) 或 (2,) 的张量、numpy 数组、列表或元组。 name: 输入数据的名称,用于错误信息。 返回: data_x: 拆包后的 X 坐标。 data_y: 拆包后的 Y 坐标。 original_type: 原始数据类型。 original_ndim: 原始数据的维度。 """ original_type = None if isinstance(data, (list, tuple)): # 转换为 numpy 数组 data = np.array(data) original_type = type(data) elif isinstance(data, np.ndarray): data = data original_type = np.ndarray elif isinstance(data, torch.Tensor): data = data original_type = torch.Tensor else: raise ValueError(f"{name} 必须是 torch 张量、numpy 数组、列表或元组") # 检查数据的维度和形状 if data.ndim != 2 or data.shape[1] != 2: raise ValueError(f"{name} 必须是形状为 (n, 2) 或 (2,) 的张量、numpy 数组、列表或元组") elif data.ndim == 2 and data.shape[1] == 2: original_ndim = 2 elif data.ndim == 1 and data.shape[0] == 2: original_ndim = 1 # 返回拆包后的数据和原始数据类型 return data[..., 0], data[..., 1], original_type, original_ndim