""" 文件处理工具模块 提供文件夹图片加载、智能命名、图片配对等功能 """ import os import uuid import time from itertools import product from pathlib import Path from typing import List, Tuple, Optional, NamedTuple from PIL import Image # 支持的图片格式 SUPPORTED_IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.webp', '.bmp', '.gif'} class ImageInfo(NamedTuple): """图片信息结构""" image: Image.Image filename: str # 不含扩展名的文件名 extension: str # 扩展名(如 .png) source_path: str # 原始文件路径 def load_images_from_folder( folder_path: str, recursive: bool = False ) -> List[ImageInfo]: """ 从文件夹加载所有图片 Args: folder_path: 文件夹路径 recursive: 是否递归加载子文件夹 Returns: ImageInfo 列表,包含图片和元数据 Raises: ValueError: 文件夹不存在或为空 Example: >>> images = load_images_from_folder("D:/images") >>> for info in images: ... print(f"{info.filename}: {info.image.size}") """ folder_path = folder_path.strip() if not folder_path: return [] path = Path(folder_path) if not path.exists(): raise ValueError(f"文件夹不存在: {folder_path}") if not path.is_dir(): raise ValueError(f"路径不是文件夹: {folder_path}") images = [] # 获取文件列表 if recursive: files = list(path.rglob("*")) else: files = list(path.iterdir()) # 按文件名排序,确保顺序一致 files = sorted(files, key=lambda x: x.name.lower()) for file_path in files: if not file_path.is_file(): continue ext = file_path.suffix.lower() if ext not in SUPPORTED_IMAGE_EXTENSIONS: continue try: img = Image.open(file_path) img.load() # 确保图片完全加载 # 转换为 RGB 模式 if img.mode != 'RGB': img = img.convert('RGB') images.append(ImageInfo( image=img, filename=file_path.stem, extension=ext, source_path=str(file_path) )) except Exception as e: print(f"警告: 无法加载图片 {file_path}: {e}") continue return images def pair_images_indexed( *image_lists: List[ImageInfo] ) -> List[Tuple[ImageInfo, ...]]: """ 1:1 索引配对 按索引位置配对多个图片列表,以最短列表长度为准。 Args: *image_lists: 多个 ImageInfo 列表 Returns: 配对后的元组列表 Example: >>> list_a = [a1, a2, a3] >>> list_b = [b1, b2, b3] >>> pairs = pair_images_indexed(list_a, list_b) >>> # [(a1, b1), (a2, b2), (a3, b3)] """ if not image_lists: return [] # 过滤空列表 non_empty_lists = [lst for lst in image_lists if lst] if not non_empty_lists: return [] # 使用 zip 进行索引配对(以最短列表为准) return list(zip(*non_empty_lists)) def pair_images_cartesian( *image_lists: List[ImageInfo] ) -> List[Tuple[ImageInfo, ...]]: """ 笛卡尔积配对 生成多个图片列表的所有组合。 Args: *image_lists: 多个 ImageInfo 列表 Returns: 配对后的元组列表 Example: >>> list_a = [a1, a2] >>> list_b = [b1, b2] >>> pairs = pair_images_cartesian(list_a, list_b) >>> # [(a1, b1), (a1, b2), (a2, b1), (a2, b2)] """ if not image_lists: return [] # 过滤空列表 non_empty_lists = [lst for lst in image_lists if lst] if not non_empty_lists: return [] # 使用 itertools.product 生成笛卡尔积 return list(product(*non_empty_lists)) def generate_output_filename( source_images: List[ImageInfo], batch_index: int, output_folder: str, extension: str = ".png", task_id: Optional[str] = None ) -> str: """ 生成智能输出文件名 基于源图片文件名生成输出文件名,使用任务ID和时间戳确保并发安全。 Args: source_images: 源图片信息列表 batch_index: 批次索引(从 0 开始) output_folder: 输出文件夹路径 extension: 输出文件扩展名 task_id: 任务唯一标识符(用于并发场景) Returns: 完整的输出文件路径 Example: >>> # 单图片: hello.png -> hello_task0_12345_000.png >>> # 多图片: hello.png + ref.png -> hello_ref_task0_12345_000.png >>> # 并发安全:每个任务有唯一的 task_id 和时间戳 """ # 构建基础文件名 if len(source_images) == 1: base_name = source_images[0].filename else: # 多个源图片,组合文件名 names = [info.filename for info in source_images] base_name = "_".join(names) # 确保输出文件夹存在 output_path = Path(output_folder) output_path.mkdir(parents=True, exist_ok=True) # 生成唯一性标识 if task_id is None: # 如果没有提供 task_id,使用 UUID 前8位 task_id = str(uuid.uuid4())[:8] # 使用时间戳(毫秒级)增加唯一性 timestamp = int(time.time() * 1000) % 100000 # 精确到毫秒的后5位 # 生成文件名:基础名_任务ID_时间戳_批次索引 filename = f"{base_name}_{task_id}_{timestamp:05d}_{batch_index:03d}{extension}" full_path = output_path / filename # 极小概率的冲突处理 counter = 1 while full_path.exists(): filename = f"{base_name}_{task_id}_{timestamp:05d}_{batch_index:03d}_{counter}{extension}" full_path = output_path / filename counter += 1 return str(full_path) def generate_batch_output_filenames( source_images: List[ImageInfo], count: int, output_folder: str, extension: str = ".png", task_id: Optional[str] = None ) -> List[str]: """ 批量生成输出文件名 Args: source_images: 源图片信息列表 count: 需要生成的文件名数量 output_folder: 输出文件夹路径 extension: 输出文件扩展名 task_id: 任务唯一标识符(用于并发场景) Returns: 输出文件路径列表 """ filenames = [] for i in range(count): filename = generate_output_filename( source_images=source_images, batch_index=i, output_folder=output_folder, extension=extension, task_id=task_id ) filenames.append(filename) return filenames def save_image( image: Image.Image, output_path: str, quality: int = 95 ) -> str: """ 保存图片到指定路径 Args: image: PIL Image 对象 output_path: 输出文件路径 quality: JPEG 质量(仅对 JPEG 格式有效) Returns: 实际保存的文件路径 """ # 确保目录存在 output_dir = Path(output_path).parent output_dir.mkdir(parents=True, exist_ok=True) # 根据扩展名选择保存参数 ext = Path(output_path).suffix.lower() if ext in {'.jpg', '.jpeg'}: # 转换为 RGB(JPEG 不支持 alpha 通道) if image.mode != 'RGB': image = image.convert('RGB') image.save(output_path, quality=quality) elif ext == '.png': image.save(output_path) elif ext == '.webp': image.save(output_path, quality=quality) else: image.save(output_path) return output_path def get_folder_image_count(folder_path: str) -> int: """ 获取文件夹中的图片数量(不加载图片) Args: folder_path: 文件夹路径 Returns: 图片数量 """ folder_path = folder_path.strip() if not folder_path: return 0 path = Path(folder_path) if not path.exists() or not path.is_dir(): return 0 count = 0 for file_path in path.iterdir(): if file_path.is_file() and file_path.suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS: count += 1 return count