【Python图像处理】5 Pillow图像处理与格式转换
摘要:本文详细介绍Pillow库的使用方法,包括图像的读取、写入、格式转换、基本操作和高级功能。Pillow是Python中最流行的图像处理库之一,提供了丰富的图像操作功能和广泛的格式支持。文章通过大量综合性代码示例,演示Pillow的各种应用场景,并介绍如何使用GPT-5.4辅助编写Pillow代码。由于国内无法访问OpenAI官网,因此使用国内镜像站可以注册使用GPT-5.4最新模型。注册入口:AIGCBAR镜像站。请广大读者遵守法律法规,切勿翻墙访问境外网站,使用国内合法镜像站即可满足学习需求。
5.1 Pillow库概述
5.1.1 Pillow简介与特点
Pillow是Python图像库(PIL,Python Imaging Library)的现代分支和继承者。PIL最初由Fredrik Lundh于1995年开发,是Python最早的图像处理库之一。由于PIL的开发在2009年后停滞,Alex Clark等人创建了Pillow项目,在保持向后兼容的同时持续维护和开发新功能。如今,Pillow已经成为Python生态系统中最流行的图像处理库之一,每月下载量超过数千万次。
Pillow的设计理念是简洁易用,它提供了直观的API和丰富的图像处理功能。与OpenCV相比,Pillow更专注于图像的基本操作和格式处理,而不是计算机视觉算法。Pillow特别适合以下场景:图像文件的读取和写入、图像格式转换、基本图像操作(裁剪、缩放、旋转)、图像增强(滤镜、色彩调整)、图像绘制(添加文字、图形)、图像元数据处理(EXIF信息)。
Pillow支持超过30种图像格式,包括常见的JPEG、PNG、GIF、BMP、TIFF、WebP,以及一些专业格式如ICO、PPM、SGI等。Pillow对各种格式的读写进行了优化,可以根据需要选择不同的压缩参数和质量设置。对于GIF格式,Pillow支持动画GIF的读取和创建;对于PNG格式,支持透明通道和各种压缩级别;对于JPEG格式,支持质量参数和渐进式编码。
5.1.2 Pillow与OpenCV的比较
Pillow和OpenCV是Python图像处理领域两个最重要的库,它们各有特点和适用场景。以下表格对两者进行了详细比较。
| 特性 | Pillow | OpenCV |
|---|---|---|
| 主要用途 | 图像处理、格式转换 | 计算机视觉、图像处理 |
| 核心语言 | Python/C | C++ |
| 图像表示 | PIL.Image对象 | NumPy数组 |
| 格式支持 | 30+种格式 | 常见格式 |
| 学习曲线 | 简单易学 | 相对复杂 |
| 性能 | 中等 | 高 |
| 计算机视觉 | 有限支持 | 全面支持 |
| 深度学习 | 需要转换 | 直接支持 |
| 文档质量 | 优秀 | 良好 |
在实际项目中,通常会同时使用这两个库,各取所长。例如,使用Pillow进行图像读取和格式转换,使用OpenCV进行复杂的图像处理和计算机视觉任务。两个库之间的转换非常简单,Pillow的Image对象可以转换为NumPy数组,反之亦然。
5.2 图像读取与写入
5.2.1 基本读写操作
Pillow提供了简洁的API进行图像的读取和写入。Image.open()函数用于读取图像文件,返回一个Image对象。Image.save()方法用于将Image对象保存为文件,可以根据文件扩展名自动识别格式。以下代码展示了Pillow的基本读写操作。
""" Pillow图像读写操作详解 演示各种图像格式的读取和写入方法 兼容Python 3.13 """from PIL import Image, ImageOps, ExifTags import numpy as np from typing import Optional, Tuple, List, Dict, Any import os import io classPillowImageIO:""" Pillow图像输入输出操作类 提供图像读取、写入、格式转换等功能 """ SUPPORTED_FORMATS ={'read':['JPEG','PNG','GIF','BMP','TIFF','WebP','PPM','PNG','ICO','SGI','TGA','Palm','PCX','XBM'],'write':['JPEG','PNG','GIF','BMP','TIFF','WebP','PPM','ICO','TGA','PCX']}def__init__(self):"""初始化图像IO操作器"""passdefopen_image(self, file_path:str)-> Optional[Image.Image]:""" 打开图像文件 参数: file_path: 图像文件路径 返回: Image对象,失败返回None """try: image = Image.open(file_path)return image except Exception as e:print(f"无法打开图像: {e}")returnNonedefopen_from_bytes(self, data:bytes)-> Optional[Image.Image]:""" 从字节数据打开图像 参数: data: 图像字节数据 返回: Image对象 """try:return Image.open(io.BytesIO(data))except Exception as e:print(f"无法解析图像数据: {e}")returnNonedefsave_image(self, image: Image.Image, file_path:str,format: Optional[str]=None,**kwargs)->bool:""" 保存图像文件 参数: image: Image对象 file_path: 输出路径 format: 输出格式(自动检测如果为None) **kwargs: 格式特定参数 返回: 是否保存成功 """try:# 确保输出目录存在 output_dir = os.path.dirname(file_path)if output_dir andnot os.path.exists(output_dir): os.makedirs(output_dir, exist_ok=True) image.save(file_path,format=format,**kwargs)returnTrueexcept Exception as e:print(f"保存图像失败: {e}")returnFalsedefsave_jpeg(self, image: Image.Image, file_path:str, quality:int=95, progressive:bool=False, optimize:bool=False)->bool:""" 保存为JPEG格式 参数: image: Image对象 file_path: 输出路径 quality: 质量(1-100) progressive: 是否渐进式 optimize: 是否优化 返回: 是否保存成功 """# 转换为RGB模式(JPEG不支持透明通道)if image.mode in('RGBA','P'): image = image.convert('RGB')return self.save_image( image, file_path, quality=quality, progressive=progressive, optimize=optimize )defsave_png(self, image: Image.Image, file_path:str, compress_level:int=6, optimize:bool=False)->bool:""" 保存为PNG格式 参数: image: Image对象 file_path: 输出路径 compress_level: 压缩级别(0-9) optimize: 是否优化 返回: 是否保存成功 """return self.save_image( image, file_path, compress_level=compress_level, optimize=optimize )defsave_webp(self, image: Image.Image, file_path:str, quality:int=80, lossless:bool=False)->bool:""" 保存为WebP格式 参数: image: Image对象 file_path: 输出路径 quality: 质量(1-100) lossless: 是否无损压缩 返回: 是否保存成功 """return self.save_image( image, file_path, quality=quality, lossless=lossless )defsave_tiff(self, image: Image.Image, file_path:str, compression:str='tiff_deflate')->bool:""" 保存为TIFF格式 参数: image: Image对象 file_path: 输出路径 compression: 压缩方法 返回: 是否保存成功 """return self.save_image( image, file_path, compression=compression )defconvert_format(self, input_path:str, output_path:str, output_format: Optional[str]=None,**kwargs)->bool:""" 转换图像格式 参数: input_path: 输入路径 output_path: 输出路径 output_format: 输出格式 **kwargs: 格式参数 返回: 是否转换成功 """ image = self.open_image(input_path)if image isNone:returnFalsereturn self.save_image(image, output_path,format=output_format,**kwargs)defbatch_convert(self, input_dir:str, output_dir:str, output_format:str='PNG', input_extensions: List[str]=None)-> Dict[str, Any]:""" 批量转换图像格式 参数: input_dir: 输入目录 output_dir: 输出目录 output_format: 输出格式 input_extensions: 输入文件扩展名列表 返回: 转换结果统计 """if input_extensions isNone: input_extensions =['.jpg','.jpeg','.png','.bmp','.gif']# 确保输出目录存在ifnot os.path.exists(output_dir): os.makedirs(output_dir, exist_ok=True) results ={'total':0,'success':0,'failed':0,'failed_files':[]}# 遍历输入目录中的文件for filename in os.listdir(input_dir): file_ext = os.path.splitext(filename)[1].lower()if file_ext in input_extensions: input_file = os.path.join(input_dir, filename) file_stem = os.path.splitext(filename)[0] output_file = os.path.join(output_dir,f'{file_stem}.{output_format.lower()}') results['total']+=1if self.convert_format(input_file, output_file): results['success']+=1else: results['failed']+=1 results['failed_files'].append(filename)return results defget_image_info(self, file_path:str)-> Dict[str, Any]:""" 获取图像信息 参数: file_path: 图像文件路径 返回: 图像信息字典 """ image = self.open_image(file_path)if image isNone:return{'success':False,'error':'无法打开图像'} info ={'success':True,'file_path': file_path,'format': image.format,'mode': image.mode,'size': image.size,'width': image.width,'height': image.height,'bits': image.bits ifhasattr(image,'bits')elseNone,'is_animated':getattr(image,'is_animated',False),'n_frames':getattr(image,'n_frames',1)}# 获取EXIF信息 exif = self.get_exif_data(image)if exif: info['exif']= exif return info defget_exif_data(self, image: Image.Image)-> Optional[Dict[str, Any]]:""" 获取EXIF元数据 参数: image: Image对象 返回: EXIF数据字典 """try: exif_data = image._getexif()if exif_data isNone:returnNone result ={}for tag_id, value in exif_data.items(): tag = ExifTags.TAGS.get(tag_id, tag_id) result[tag]= value return result except Exception:returnNonedefcreate_test_image(self, width:int=640, height:int=480, mode:str='RGB', pattern:str='gradient')-> Image.Image:""" 创建测试图像 参数: width: 宽度 height: 高度 mode: 颜色模式 pattern: 图案类型 返回: 测试图像 """if pattern =='gradient':# 创建渐变图像 r = np.linspace(0,255, width, dtype=np.uint8) g = np.linspace(0,255, height, dtype=np.uint8) r_grid, g_grid = np.meshgrid(r, g) b = np.full((height, width),128, dtype=np.uint8) arr = np.stack([r_grid, g_grid, b], axis=2)return Image.fromarray(arr, mode='RGB')elif pattern =='checkerboard':# 创建棋盘格 block_size =50 arr = np.zeros((height, width,3), dtype=np.uint8)for y inrange(0, height, block_size *2):for x inrange(0, width, block_size *2): arr[y:y+block_size, x:x+block_size]=[255,255,255] arr[y+block_size:y+block_size*2, x+block_size:x+block_size*2]=[255,255,255]return Image.fromarray(arr, mode='RGB')elif pattern =='noise':# 随机噪声 arr = np.random.randint(0,256,(height, width,3), dtype=np.uint8)return Image.fromarray(arr, mode='RGB')else:return Image.new(mode,(width, height), color='white')defdemonstrate_image_io():""" 演示图像读写操作 """ io_handler = PillowImageIO()print("Pillow图像读写演示")print("="*50)# 创建测试图像 test_image = io_handler.create_test_image(640,480,'RGB','gradient')print(f"创建测试图像: {test_image.size}, 模式: {test_image.mode}")# 保存为不同格式 - 使用当前目录下的测试文件夹 current_dir = os.path.dirname(os.path.abspath(__file__)) test_dir = os.path.join(current_dir,"pillow_test")# 确保测试目录存在ifnot os.path.exists(test_dir): os.makedirs(test_dir, exist_ok=True)# JPEG jpeg_path = os.path.join(test_dir,"test.jpg") io_handler.save_jpeg(test_image, jpeg_path, quality=95)print(f"保存JPEG: {os.path.getsize(jpeg_path)/1024:.1f} KB")# PNG png_path = os.path.join(test_dir,"test.png") io_handler.save_png(test_image, png_path, compress_level=6)print(f"保存PNG: {os.path.getsize(png_path)/1024:.1f} KB")# WebP webp_path = os.path.join(test_dir,"test.webp") io_handler.save_webp(test_image, webp_path, quality=80)print(f"保存WebP: {os.path.getsize(webp_path)/1024:.1f} KB")# 获取图像信息 info = io_handler.get_image_info(jpeg_path)print(f"\n图像信息:")print(f" 格式: {info['format']}")print(f" 尺寸: {info['size']}")print(f" 模式: {info['mode']}")return test_image if __name__ =="__main__": image = demonstrate_image_io()print("\n图像读写演示完成")5.2.2 图像模式与转换
Pillow支持多种图像模式,每种模式对应不同的颜色空间和像素表示方式。理解图像模式对于正确处理图像非常重要。以下表格列出了Pillow支持的主要图像模式。
| 模式 | 说明 | 通道数 | 每像素位数 |
|---|---|---|---|
| 1 | 二值图像 | 1 | 1 |
| L | 灰度图像 | 1 | 8 |
| P | 调色板图像 | 1 | 8 |
| RGB | 真彩色 | 3 | 24 |
| RGBA | 带透明通道的真彩色 | 4 | 32 |
| CMYK | 印刷四色 | 4 | 32 |
| YCbCr | 视频颜色空间 | 3 | 24 |
| LAB | CIE Lab颜色空间 | 3 | 24 |
| HSV | 色相饱和度明度 | 3 | 24 |
| I | 32位整数灰度 | 1 | 32 |
| F | 32位浮点灰度 | 1 | 32 |
以下代码展示了图像模式转换的各种操作。
""" Pillow图像模式转换详解 演示各种颜色模式之间的转换 兼容Python 3.13 """from PIL import Image import numpy as np from typing import Optional, Tuple, List classImageModeConverter:""" 图像模式转换类 """def__init__(self, image: Image.Image):""" 初始化模式转换器 参数: image: 输入图像 """ self.image = image self.original_mode = image.mode defto_grayscale(self, method:str='standard')-> Image.Image:""" 转换为灰度图像 参数: method: 转换方法 'standard': 标准转换 'luminance': 亮度转换 'desaturation': 去饱和 'average': 平均值 返回: 灰度图像 """if method =='standard':return self.image.convert('L')# 转换为RGB rgb = self.image.convert('RGB') arr = np.array(rgb, dtype=np.float64)if method =='luminance':# 使用亮度公式 gray =0.299* arr[:,:,0]+0.587* arr[:,:,1]+0.114* arr[:,:,2]elif method =='desaturation':# 去饱和 gray = np.max(arr, axis=2)/2+ np.min(arr, axis=2)/2elif method =='average':# 平均值 gray = np.mean(arr, axis=2)else: gray = np.mean(arr, axis=2)return Image.fromarray(gray.astype(np.uint8), mode='L')defto_rgb(self)-> Image.Image:""" 转换为RGB模式 返回: RGB图像 """return self.image.convert('RGB')defto_rgba(self, background_color: Tuple[int,int,int]=(255,255,255))-> Image.Image:""" 转换为RGBA模式 参数: background_color: 背景色(用于无透明通道的图像) 返回: RGBA图像 """if self.image.mode =='RGBA':return self.image.copy()if self.image.mode =='P':# 调色板模式可能有透明通道return self.image.convert('RGBA')if self.image.mode =='L':# 灰度图转RGBA rgb = self.image.convert('RGB')return rgb.convert('RGBA')return self.image.convert('RGBA')defto_binary(self, threshold:int=128)-> Image.Image:""" 转换为二值图像 参数: threshold: 阈值 返回: 二值图像 """ gray = self.image.convert('L')return gray.point(lambda x:255if x > threshold else0, mode='1')defto_cmyk(self)-> Image.Image:""" 转换为CMYK模式 返回: CMYK图像 """ rgb = self.image.convert('RGB')return rgb.convert('CMYK')defto_palette(self, palette:str='WEB', colors:int=256)-> Image.Image:""" 转换为调色板模式 参数: palette: 调色板类型 colors: 颜色数量 返回: 调色板图像 """ rgb = self.image.convert('RGB')if palette =='WEB':return rgb.convert('P', palette=Image.Palette.WEB)elif palette =='ADAPTIVE':return rgb.convert('P', palette=Image.Palette.ADAPTIVE, colors=colors)else:return rgb.convert('P')defto_hsv(self)-> Image.Image:""" 转换为HSV模式 返回: HSV图像 """return self.image.convert('HSV')defto_lab(self)-> Image.Image:""" 转换为LAB模式 返回: LAB图像 """return self.image.convert('LAB')defseparate_channels(self)-> List[Image.Image]:""" 分离颜色通道 返回: 通道图像列表 """returnlist(self.image.split())defmerge_channels(self, channels: List[Image.Image], mode:str)-> Image.Image:""" 合并颜色通道 参数: channels: 通道图像列表 mode: 输出模式 返回: 合并后的图像 """return Image.merge(mode, channels)defswap_channels(self, order: Tuple[int,...])-> Image.Image:""" 交换通道顺序 参数: order: 新的通道顺序 返回: 交换后的图像 """ channels = self.separate_channels() reordered =[channels[i]for i in order]return Image.merge(self.image.mode, reordered)defextract_channel(self, channel:int)-> Image.Image:""" 提取单个通道 参数: channel: 通道索引 返回: 单通道图像 """ channels = self.separate_channels()return channels[channel]defset_channel(self, channel_index:int, channel_image: Image.Image)-> Image.Image:""" 设置单个通道 参数: channel_index: 通道索引 channel_image: 通道图像 返回: 修改后的图像 """ channels = self.separate_channels() channels[channel_index]= channel_image return Image.merge(self.image.mode, channels)defget_mode_info(self)->dict:""" 获取当前模式信息 返回: 模式信息字典 """return{'mode': self.image.mode,'bands': self.image.getbands(),'bits': self.image.bits ifhasattr(self.image,'bits')else8,'size': self.image.size }defdemonstrate_mode_conversion():""" 演示模式转换 """# 创建测试图像 image = Image.new('RGB',(200,200), color=(255,128,64)) converter = ImageModeConverter(image)print("图像模式转换演示")print("="*50)# 原始模式信息 info = converter.get_mode_info()print(f"原始模式: {info['mode']}")print(f"通道: {info['bands']}")# 转换为灰度 gray = converter.to_grayscale()print(f"\n灰度模式: {gray.mode}")# 转换为RGBA rgba = converter.to_rgba()print(f"RGBA模式: {rgba.mode}")# 转换为二值 binary = converter.to_binary(128)print(f"二值模式: {binary.mode}")# 转换为CMYK cmyk = converter.to_cmyk()print(f"CMYK模式: {cmyk.mode}")# 分离通道 channels = converter.separate_channels()print(f"\n分离通道: {[ch.mode for ch in channels]}")# 不同灰度转换方法比较print("\n灰度转换方法比较:")for method in['standard','luminance','desaturation','average']: gray = converter.to_grayscale(method) arr = np.array(gray)print(f" {method}: 均值={arr.mean():.2f}")return{'original': image,'gray': gray,'rgba': rgba,'binary': binary }if __name__ =="__main__": results = demonstrate_mode_conversion()print("\n模式转换演示完成")5.3 图像基本操作
5.3.1 几何变换
Pillow提供了丰富的几何变换功能,包括裁剪、缩放、旋转、翻转等操作。这些操作都返回新的Image对象,不会修改原始图像。以下代码展示了各种几何变换操作。
""" Pillow图像几何变换详解 演示裁剪、缩放、旋转、翻转等操作 兼容Python 3.13 """from PIL import Image, ImageOps import numpy as np from typing import Tuple, Optional, List import math classPillowGeometry:""" Pillow几何变换类 """def__init__(self, image: Image.Image):""" 初始化几何变换器 参数: image: 输入图像 """ self.image = image self.width, self.height = image.size defcrop(self, box: Tuple[int,int,int,int])-> Image.Image:""" 裁剪图像 参数: box: 裁剪区域 (left, upper, right, lower) 返回: 裁剪后的图像 """return self.image.crop(box)defcrop_center(self, width:int, height:int)-> Image.Image:""" 中心裁剪 参数: width: 裁剪宽度 height: 裁剪高度 返回: 裁剪后的图像 """ left =(self.width - width)//2 upper =(self.height - height)//2 right = left + width lower = upper + height return self.image.crop((left, upper, right, lower))defcrop_to_aspect_ratio(self, ratio:float, mode:str='center')-> Image.Image:""" 按宽高比裁剪 参数: ratio: 目标宽高比 (width/height) mode: 裁剪模式 ('center', 'top', 'bottom', 'left', 'right') 返回: 裁剪后的图像 """ current_ratio = self.width / self.height ifabs(current_ratio - ratio)<0.01:return self.image.copy()if current_ratio > ratio:# 当前图像更宽,裁剪宽度 new_width =int(self.height * ratio)if mode =='center': left =(self.width - new_width)//2elif mode =='left': left =0elif mode =='right': left = self.width - new_width else: left =(self.width - new_width)//2return self.image.crop((left,0, left + new_width, self.height))else:# 当前图像更高,裁剪高度 new_height =int(self.width / ratio)if mode =='center': upper =(self.height - new_height)//2elif mode =='top': upper =0elif mode =='bottom': upper = self.height - new_height else: upper =(self.height - new_height)//2return self.image.crop((0, upper, self.width, upper + new_height))defresize(self, size: Tuple[int,int], resample:int= Image.Resampling.LANCZOS)-> Image.Image:""" 调整图像大小 参数: size: 目标尺寸 (width, height) resample: 重采样方法 返回: 调整后的图像 """return self.image.resize(size, resample=resample)defresize_to_width(self, width:int, resample:int= Image.Resampling.LANCZOS)-> Image.Image:""" 按宽度缩放(保持宽高比) 参数: width: 目标宽度 resample: 重采样方法 返回: 缩放后的图像 """ ratio = width / self.width height =int(self.height * ratio)return self.image.resize((width, height), resample=resample)defresize_to_height(self, height:int, resample:int= Image.Resampling.LANCZOS)-> Image.Image:""" 按高度缩放(保持宽高比) 参数: height: 目标高度 resample: 重采样方法 返回: 缩放后的图像 """ ratio = height / self.height width =int(self.width * ratio)return self.image.resize((width, height), resample=resample)defresize_contain(self, size: Tuple[int,int], background_color: Tuple[int,...]=(255,255,255), resample:int= Image.Resampling.LANCZOS)-> Image.Image:""" 缩放并包含在指定尺寸内(保持宽高比,不足部分填充) 参数: size: 目标尺寸 (width, height) background_color: 背景色 resample: 重采样方法 返回: 缩放后的图像 """return ImageOps.fit(self.image, size, method=resample, bleed=0.0, centering=(0.5,0.5))defresize_cover(self, size: Tuple[int,int], resample:int= Image.Resampling.LANCZOS)-> Image.Image:""" 缩放并覆盖指定尺寸(保持宽高比,可能裁剪) 参数: size: 目标尺寸 (width, height) resample: 重采样方法 返回: 缩放后的图像 """ ratio =max(size[0]/ self.width, size[1]/ self.height) new_size =(int(self.width * ratio),int(self.height * ratio)) resized = self.image.resize(new_size, resample=resample)# 居中裁剪 left =(resized.width - size[0])//2 upper =(resized.height - size[1])//2return resized.crop((left, upper, left + size[0], upper + size[1]))defrotate(self, angle:float, expand:bool=False, fillcolor: Optional[Tuple[int,...]]=None)-> Image.Image:""" 旋转图像 参数: angle: 旋转角度(逆时针为正) expand: 是否扩展画布 fillcolor: 填充色 返回: 旋转后的图像 """return self.image.rotate(angle, expand=expand, fillcolor=fillcolor)defrotate_90(self)-> Image.Image:""" 旋转90度 返回: 旋转后的图像 """return self.image.rotate(90, expand=True)defrotate_180(self)-> Image.Image:""" 旋转180度 返回: 旋转后的图像 """return self.image.rotate(180)defrotate_270(self)-> Image.Image:""" 旋转270度 返回: 旋转后的图像 """return self.image.rotate(270, expand=True)defflip_horizontal(self)-> Image.Image:""" 水平翻转 返回: 翻转后的图像 """return ImageOps.mirror(self.image)defflip_vertical(self)-> Image.Image:""" 垂直翻转 返回: 翻转后的图像 """return ImageOps.flip(self.image)deftranspose(self, method:int)-> Image.Image:""" 图像转置 参数: method: 转置方法 Image.Transpose.FLIP_LEFT_RIGHT Image.Transpose.FLIP_TOP_BOTTOM Image.Transpose.ROTATE_90 Image.Transpose.ROTATE_180 Image.Transpose.ROTATE_270 Image.Transpose.TRANSPOSE Image.Transpose.TRANSVERSE 返回: 转置后的图像 """return self.image.transpose(method)deftransform_affine(self, matrix: Tuple[float,...], size: Optional[Tuple[int,int]]=None)-> Image.Image:""" 仿射变换 参数: matrix: 变换矩阵(6个元素) size: 输出尺寸 返回: 变换后的图像 """if size isNone: size = self.image.size return self.image.transform( size, Image.Transform.AFFINE, matrix, resample=Image.Resampling.BICUBIC )deftransform_perspective(self, coeffs: Tuple[float,...], size: Optional[Tuple[int,int]]=None)-> Image.Image:""" 透视变换 参数: coeffs: 变换系数(8个元素) size: 输出尺寸 返回: 变换后的图像 """if size isNone: size = self.image.size return self.image.transform( size, Image.Transform.PERSPECTIVE, coeffs, resample=Image.Resampling.BICUBIC )deftransform_quad(self, quad: Tuple[Tuple[int,int],...], size: Tuple[int,int])-> Image.Image:""" 四边形变换 参数: quad: 目标四边形的四个顶点 size: 输出尺寸 返回: 变换后的图像 """# 计算透视变换系数# 这里简化处理,实际需要计算变换矩阵return self.image.transform( size, Image.Transform.QUAD, quad, resample=Image.Resampling.BICUBIC )defcreate_thumbnail(self, size: Tuple[int,int])-> Image.Image:""" 创建缩略图 参数: size: 最大尺寸 (width, height) 返回: 缩略图 """ img_copy = self.image.copy() img_copy.thumbnail(size, resample=Image.Resampling.LANCZOS)return img_copy defdemonstrate_geometry():""" 演示几何变换 """# 创建测试图像 image = Image.new('RGB',(400,300), color=(100,150,200))# 添加一些标记from PIL import ImageDraw draw = ImageDraw.Draw(image) draw.rectangle([50,50,150,100], fill=(255,0,0)) draw.ellipse([200,100,350,200], fill=(0,255,0)) draw.text((100,200),"Test Image", fill=(255,255,255)) geometry = PillowGeometry(image)print("Pillow几何变换演示")print("="*50)# 裁剪 cropped = geometry.crop((50,50,200,150))print(f"裁剪: {image.size} -> {cropped.size}")# 中心裁剪 center_cropped = geometry.crop_center(200,150)print(f"中心裁剪: {image.size} -> {center_cropped.size}")# 缩放 resized = geometry.resize((200,150))print(f"缩放: {image.size} -> {resized.size}")# 按宽度缩放 width_scaled = geometry.resize_to_width(200)print(f"按宽度缩放: {image.size} -> {width_scaled.size}")# 旋转 rotated = geometry.rotate(45, expand=True)print(f"旋转45度: {rotated.size}")# 翻转 h_flipped = geometry.flip_horizontal() v_flipped = geometry.flip_vertical()print(f"水平翻转: {h_flipped.size}")print(f"垂直翻转: {v_flipped.size}")# 缩略图 thumbnail = geometry.create_thumbnail((100,100))print(f"缩略图: {thumbnail.size}")return{'original': image,'cropped': cropped,'resized': resized,'rotated': rotated,'thumbnail': thumbnail }if __name__ =="__main__": results = demonstrate_geometry()print("\n几何变换演示完成")5.3.2 图像增强
Pillow提供了多种图像增强功能,包括色彩调整、对比度调整、锐化、模糊等。这些功能主要通过ImageEnhance模块和ImageFilter模块实现。以下代码展示了各种图像增强操作。
""" Pillow图像增强详解 演示色彩调整、滤镜应用等增强操作 兼容Python 3.13 """from PIL import Image, ImageEnhance, ImageFilter, ImageOps import numpy as np from typing import Tuple, Optional, List classPillowEnhance:""" Pillow图像增强类 """def__init__(self, image: Image.Image):""" 初始化图像增强器 参数: image: 输入图像 """ self.image = image.convert('RGB')defadjust_brightness(self, factor:float)-> Image.Image:""" 调整亮度 参数: factor: 亮度因子(1.0为原始亮度) 返回: 调整后的图像 """ enhancer = ImageEnhance.Brightness(self.image)return enhancer.enhance(factor)defadjust_contrast(self, factor:float)-> Image.Image:""" 调整对比度 参数: factor: 对比度因子(1.0为原始对比度) 返回: 调整后的图像 """ enhancer = ImageEnhance.Contrast(self.image)return enhancer.enhance(factor)defadjust_color(self, factor:float)-> Image.Image:""" 调整色彩饱和度 参数: factor: 饱和度因子(1.0为原始饱和度) 返回: 调整后的图像 """ enhancer = ImageEnhance.Color(self.image)return enhancer.enhance(factor)defadjust_sharpness(self, factor:float)-> Image.Image:""" 调整锐度 参数: factor: 锐度因子(1.0为原始锐度) 返回: 调整后的图像 """ enhancer = ImageEnhance.Sharpness(self.image)return enhancer.enhance(factor)defauto_contrast(self, cutoff:float=0)-> Image.Image:""" 自动对比度调整 参数: cutoff: 截断百分比 返回: 调整后的图像 """return ImageOps.autocontrast(self.image, cutoff=cutoff)defequalize(self)-> Image.Image:""" 直方图均衡化 返回: 均衡化后的图像 """return ImageOps.equalize(self.image)definvert(self)-> Image.Image:""" 反色 返回: 反色后的图像 """return ImageOps.invert(self.image)defposterize(self, bits:int=3)-> Image.Image:""" 色调分离 参数: bits: 每通道保留的位数 返回: 处理后的图像 """return ImageOps.posterize(self.image, bits)defsolarize(self, threshold:int=128)-> Image.Image:""" 曝光效果 参数: threshold: 阈值 返回: 处理后的图像 """return ImageOps.solarize(self.image, threshold=threshold)defapply_filter(self, filter_type:str,**kwargs)-> Image.Image:""" 应用滤镜 参数: filter_type: 滤镜类型 **kwargs: 滤镜参数 返回: 滤镜处理后的图像 """ filters ={'blur': ImageFilter.BLUR,'contour': ImageFilter.CONTOUR,'detail': ImageFilter.DETAIL,'edge_enhance': ImageFilter.EDGE_ENHANCE,'edge_enhance_more': ImageFilter.EDGE_ENHANCE_MORE,'emboss': ImageFilter.EMBOSS,'find_edges': ImageFilter.FIND_EDGES,'sharpen': ImageFilter.SHARPEN,'smooth': ImageFilter.SMOOTH,'smooth_more': ImageFilter.SMOOTH_MORE,'unsharp_mask': ImageFilter.UnsharpMask( radius=kwargs.get('radius',2), percent=kwargs.get('percent',150), threshold=kwargs.get('threshold',3))}if filter_type in filters:return self.image.filter(filters[filter_type])return self.image.copy()defgaussian_blur(self, radius:int=2)-> Image.Image:""" 高斯模糊 参数: radius: 模糊半径 返回: 模糊后的图像 """return self.image.filter(ImageFilter.GaussianBlur(radius=radius))defbox_blur(self, radius:int=2)-> Image.Image:""" 盒式模糊 参数: radius: 模糊半径 返回: 模糊后的图像 """return self.image.filter(ImageFilter.BoxBlur(radius=radius))defmedian_filter(self, size:int=3)-> Image.Image:""" 中值滤波 参数: size: 滤波器大小 返回: 滤波后的图像 """return self.image.filter(ImageFilter.MedianFilter(size=size))defmin_filter(self, size:int=3)-> Image.Image:""" 最小值滤波 参数: size: 滤波器大小 返回: 滤波后的图像 """return self.image.filter(ImageFilter.MinFilter(size=size))defmax_filter(self, size:int=3)-> Image.Image:""" 最大值滤波 参数: size: 滤波器大小 返回: 滤波后的图像 """return self.image.filter(ImageFilter.MaxFilter(size=size))defmode_filter(self, size:int=3)-> Image.Image:""" 众数滤波 参数: size: 滤波器大小 返回: 滤波后的图像 """return self.image.filter(ImageFilter.ModeFilter(size=size))defrank_filter(self, size:int=3, rank:int=4)-> Image.Image:""" 秩滤波 参数: size: 滤波器大小 rank: 秩 返回: 滤波后的图像 """return self.image.filter(ImageFilter.RankFilter(size=size, rank=rank))defcustom_kernel(self, kernel: List[List[float]], scale:float=None)-> Image.Image:""" 自定义卷积核 参数: kernel: 卷积核 scale: 缩放因子 返回: 卷积后的图像 """# 展平卷积核 flat_kernel =[item for row in kernel for item in row]if scale isNone: scale =sum(flat_kernel)if scale ==0: scale =1 kernel_size =(len(kernel[0]),len(kernel))return self.image.filter( ImageFilter.Kernel(kernel_size, flat_kernel, scale=scale))defcolorize(self, black:str='black', white:str='white', mid: Optional[str]=None)-> Image.Image:""" 灰度图着色 参数: black: 暗部颜色 white: 亮部颜色 mid: 中间调颜色 返回: 着色后的图像 """ gray = self.image.convert('L')return ImageOps.colorize(gray, black=black, white=white, mid=mid)defadd_border(self, border:int=10, color:str='black')-> Image.Image:""" 添加边框 参数: border: 边框宽度 color: 边框颜色 返回: 添加边框后的图像 """return ImageOps.expand(self.image, border=border, fill=color)defcrop_border(self, border:int=10)-> Image.Image:""" 裁剪边框 参数: border: 边框宽度 返回: 裁剪后的图像 """return ImageOps.crop(self.image, border=border)deffit(self, size: Tuple[int,int], method:int= Image.Resampling.LANCZOS, bleed:float=0.0, centering: Tuple[float,float]=(0.5,0.5))-> Image.Image:""" 适配尺寸 参数: size: 目标尺寸 method: 重采样方法 bleed: 边缘去除比例 centering: 居中方式 返回: 适配后的图像 """return ImageOps.fit(self.image, size, method=method, bleed=bleed, centering=centering)defpad(self, size: Tuple[int,int], color:str='black', centering: Tuple[float,float]=(0.5,0.5))-> Image.Image:""" 填充到指定尺寸 参数: size: 目标尺寸 color: 填充颜色 centering: 居中方式 返回: 填充后的图像 """return ImageOps.pad(self.image, size, color=color, centering=centering)defdemonstrate_enhancement():""" 演示图像增强 """# 创建测试图像 image = Image.new('RGB',(400,300), color=(100,150,200))from PIL import ImageDraw draw = ImageDraw.Draw(image) draw.rectangle([50,50,150,100], fill=(255,0,0)) draw.ellipse([200,100,350,200], fill=(0,255,0)) enhance = PillowEnhance(image)print("Pillow图像增强演示")print("="*50)# 亮度调整 bright = enhance.adjust_brightness(1.5) dark = enhance.adjust_brightness(0.5)print(f"亮度调整: 1.5x 和 0.5x")# 对比度调整 high_contrast = enhance.adjust_contrast(1.5) low_contrast = enhance.adjust_contrast(0.5)print(f"对比度调整: 1.5x 和 0.5x")# 饱和度调整 high_color = enhance.adjust_color(1.5) low_color = enhance.adjust_color(0.5)print(f"饱和度调整: 1.5x 和 0.5x")# 滤镜效果 blurred = enhance.gaussian_blur(5) sharpened = enhance.apply_filter('sharpen') embossed = enhance.apply_filter('emboss')print(f"滤镜: 高斯模糊、锐化、浮雕")# 自动增强 auto_contrast = enhance.auto_contrast() equalized = enhance.equalize()print(f"自动增强: 自动对比度、直方图均衡化")# 特殊效果 inverted = enhance.invert() posterized = enhance.posterize(2) solarized = enhance.solarize(128)print(f"特殊效果: 反色、色调分离、曝光")return{'original': image,'bright': bright,'dark': dark,'blurred': blurred,'sharpened': sharpened,'embossed': embossed,'inverted': inverted }if __name__ =="__main__": results = demonstrate_enhancement()print("\n图像增强演示完成")5.4 图像绘制与文字处理
5.4.1 图像绘制
Pillow的ImageDraw模块提供了在图像上绘制各种图形的功能,包括直线、矩形、圆形、椭圆、多边形、弧线等。这些功能对于创建标注图像、添加水印、生成图表等应用非常有用。以下代码展示了各种绘制操作。
""" Pillow图像绘制详解 演示各种图形和文字的绘制方法 兼容Python 3.13 """from PIL import Image, ImageDraw, ImageFont import numpy as np from typing import Tuple, List, Optional, Union from pathlib import Path classPillowDrawing:""" Pillow图像绘制类 """def__init__(self, image: Optional[Image.Image]=None, size: Tuple[int,int]=(640,480), background: Union[str, Tuple[int,...]]='white'):""" 初始化绘制器 参数: image: 输入图像(如果为None则创建新图像) size: 新图像尺寸 background: 背景色 """if image isnotNone: self.image = image.copy()else: self.image = Image.new('RGB', size, background) self.draw = ImageDraw.Draw(self.image) self.width, self.height = self.image.size defget_image(self)-> Image.Image:""" 获取当前图像 返回: Image对象 """return self.image defdraw_line(self, start: Tuple[int,int], end: Tuple[int,int], color: Union[str, Tuple[int,...]]='black', width:int=1)->None:""" 绘制直线 参数: start: 起点 end: 终点 color: 颜色 width: 线宽 """ self.draw.line([start, end], fill=color, width=width)defdraw_lines(self, points: List[Tuple[int,int]], color: Union[str, Tuple[int,...]]='black', width:int=1, closed:bool=False)->None:""" 绘制折线 参数: points: 点列表 color: 颜色 width: 线宽 closed: 是否闭合 """ self.draw.line(points, fill=color, width=width, joint=None)if closed andlen(points)>2: self.draw.line([points[-1], points[0]], fill=color, width=width)defdraw_rectangle(self, box: Tuple[int,int,int,int], outline: Optional[Union[str, Tuple[int,...]]]='black', fill: Optional[Union[str, Tuple[int,...]]]=None, width:int=1)->None:""" 绘制矩形 参数: box: 矩形区域 (left, upper, right, lower) outline: 边框颜色 fill: 填充颜色 width: 边框宽度 """ self.draw.rectangle(box, outline=outline, fill=fill, width=width)defdraw_rounded_rectangle(self, box: Tuple[int,int,int,int], radius:int=10, outline: Optional[Union[str, Tuple[int,...]]]='black', fill: Optional[Union[str, Tuple[int,...]]]=None, width:int=1)->None:""" 绘制圆角矩形 参数: box: 矩形区域 radius: 圆角半径 outline: 边框颜色 fill: 填充颜色 width: 边框宽度 """ self.draw.rounded_rectangle(box, radius=radius, outline=outline, fill=fill, width=width)defdraw_ellipse(self, box: Tuple[int,int,int,int], outline: Optional[Union[str, Tuple[int,...]]]='black', fill: Optional[Union[str, Tuple[int,...]]]=None, width:int=1)->None:""" 绘制椭圆 参数: box: 边界框 outline: 边框颜色 fill: 填充颜色 width: 边框宽度 """ self.draw.ellipse(box, outline=outline, fill=fill, width=width)defdraw_circle(self, center: Tuple[int,int], radius:int, outline: Optional[Union[str, Tuple[int,...]]]='black', fill: Optional[Union[str, Tuple[int,...]]]=None, width:int=1)->None:""" 绘制圆形 参数: center: 圆心 radius: 半径 outline: 边框颜色 fill: 填充颜色 width: 边框宽度 """ x, y = center box =(x - radius, y - radius, x + radius, y + radius) self.draw.ellipse(box, outline=outline, fill=fill, width=width)defdraw_arc(self, box: Tuple[int,int,int,int], start:float, end:float, color: Union[str, Tuple[int,...]]='black', width:int=1)->None:""" 绘制弧线 参数: box: 边界框 start: 起始角度 end: 结束角度 color: 颜色 width: 线宽 """ self.draw.arc(box, start=start, end=end, fill=color, width=width)defdraw_chord(self, box: Tuple[int,int,int,int], start:float, end:float, outline: Optional[Union[str, Tuple[int,...]]]='black', fill: Optional[Union[str, Tuple[int,...]]]=None, width:int=1)->None:""" 绘制弦 参数: box: 边界框 start: 起始角度 end: 结束角度 outline: 边框颜色 fill: 填充颜色 width: 边框宽度 """ self.draw.chord(box, start=start, end=end, outline=outline, fill=fill, width=width)defdraw_pieslice(self, box: Tuple[int,int,int,int], start:float, end:float, outline: Optional[Union[str, Tuple[int,...]]]='black', fill: Optional[Union[str, Tuple[int,...]]]=None, width:int=1)->None:""" 绘制扇形 参数: box: 边界框 start: 起始角度 end: 结束角度 outline: 边框颜色 fill: 填充颜色 width: 边框宽度 """ self.draw.pieslice(box, start=start, end=end, outline=outline, fill=fill, width=width)defdraw_polygon(self, points: List[Tuple[int,int]], outline: Optional[Union[str, Tuple[int,...]]]='black', fill: Optional[Union[str, Tuple[int,...]]]=None, width:int=1)->None:""" 绘制多边形 参数: points: 顶点列表 outline: 边框颜色 fill: 填充颜色 width: 边框宽度 """ self.draw.polygon(points, outline=outline, fill=fill)defdraw_point(self, position: Tuple[int,int], color: Union[str, Tuple[int,...]]='black')->None:""" 绘制点 参数: position: 位置 color: 颜色 """ self.draw.point(position, fill=color)defdraw_points(self, points: List[Tuple[int,int]], color: Union[str, Tuple[int,...]]='black')->None:""" 绘制多个点 参数: points: 点列表 color: 颜色 """for point in points: self.draw.point(point, fill=color)defdraw_text(self, position: Tuple[int,int], text:str, color: Union[str, Tuple[int,...]]='black', font: Optional[ImageFont.FreeTypeFont]=None)->None:""" 绘制文字 参数: position: 位置 text: 文字内容 color: 颜色 font: 字体对象 """ self.draw.text(position, text, fill=color, font=font)defdraw_text_with_box(self, position: Tuple[int,int], text:str, color: Union[str, Tuple[int,...]]='black', font: Optional[ImageFont.FreeTypeFont]=None, box_color: Optional[Union[str, Tuple[int,...]]]=None, padding:int=5)->None:""" 绘制带背景框的文字 参数: position: 位置 text: 文字内容 color: 文字颜色 font: 字体对象 box_color: 背景框颜色 padding: 内边距 """# 获取文字边界框if font: bbox = font.getbbox(text)else: bbox = self.draw.textbbox((0,0), text) text_width = bbox[2]- bbox[0] text_height = bbox[3]- bbox[1]# 绘制背景框if box_color: box =( position[0]- padding, position[1]- padding, position[0]+ text_width + padding, position[1]+ text_height + padding ) self.draw.rectangle(box, fill=box_color)# 绘制文字 self.draw.text(position, text, fill=color, font=font)defdraw_text_centered(self, text:str, color: Union[str, Tuple[int,...]]='black', font: Optional[ImageFont.FreeTypeFont]=None)->None:""" 绘制居中文字 参数: text: 文字内容 color: 颜色 font: 字体对象 """# 获取文字边界框if font: bbox = font.getbbox(text)else: bbox = self.draw.textbbox((0,0), text) text_width = bbox[2]- bbox[0] text_height = bbox[3]- bbox[1]# 计算居中位置 x =(self.width - text_width)//2 y =(self.height - text_height)//2 self.draw.text((x, y), text, fill=color, font=font)defdraw_cross(self, center: Tuple[int,int], size:int=10, color: Union[str, Tuple[int,...]]='red', width:int=2)->None:""" 绘制十字标记 参数: center: 中心点 size: 大小 color: 颜色 width: 线宽 """ x, y = center self.draw.line([(x - size, y),(x + size, y)], fill=color, width=width) self.draw.line([(x, y - size),(x, y + size)], fill=color, width=width)defdraw_arrow(self, start: Tuple[int,int], end: Tuple[int,int], color: Union[str, Tuple[int,...]]='black', width:int=2, head_size:int=10)->None:""" 绘制箭头 参数: start: 起点 end: 终点 color: 颜色 width: 线宽 head_size: 箭头大小 """# 绘制线段 self.draw.line([start, end], fill=color, width=width)# 计算箭头方向import math dx = end[0]- start[0] dy = end[1]- start[1] length = math.sqrt(dx * dx + dy * dy)if length ==0:return# 单位向量 ux = dx / length uy = dy / length # 垂直向量 px =-uy py = ux # 箭头顶点 arrow_points =[ end,(int(end[0]- head_size * ux + head_size * px *0.5),int(end[1]- head_size * uy + head_size * py *0.5)),(int(end[0]- head_size * ux - head_size * px *0.5),int(end[1]- head_size * uy - head_size * py *0.5))] self.draw.polygon(arrow_points, fill=color)defdraw_grid(self, spacing:int=50, color: Union[str, Tuple[int,...]]=(200,200,200), width:int=1)->None:""" 绘制网格 参数: spacing: 网格间距 color: 颜色 width: 线宽 """# 垂直线for x inrange(0, self.width, spacing): self.draw.line([(x,0),(x, self.height)], fill=color, width=width)# 水平线for y inrange(0, self.height, spacing): self.draw.line([(0, y),(self.width, y)], fill=color, width=width)defdraw_checkerboard(self, square_size:int=50, color1: Union[str, Tuple[int,...]]='white', color2: Union[str, Tuple[int,...]]='gray')->None:""" 绘制棋盘格背景 参数: square_size: 方块大小 color1: 颜色1 color2: 颜色2 """for y inrange(0, self.height, square_size):for x inrange(0, self.width, square_size): color = color1 if((x // square_size)+(y // square_size))%2==0else color2 self.draw.rectangle([x, y, x + square_size, y + square_size], fill=color )defclear(self, color: Union[str, Tuple[int,...]]='white')->None:""" 清空画布 参数: color: 背景色 """ self.draw.rectangle([0,0, self.width, self.height], fill=color)classFontManager:""" 字体管理类 """def__init__(self):"""初始化字体管理器""" self.fonts ={}defload_font(self, name:str, font_path:str, size:int=16)-> ImageFont.FreeTypeFont:""" 加载字体 参数: name: 字体名称 font_path: 字体文件路径 size: 字体大小 返回: 字体对象 """try: font = ImageFont.truetype(font_path, size) self.fonts[name]= font return font except Exception as e:print(f"加载字体失败: {e}")return ImageFont.load_default()defget_font(self, name:str)-> Optional[ImageFont.FreeTypeFont]:""" 获取字体 参数: name: 字体名称 返回: 字体对象 """return self.fonts.get(name)defget_default_font(self, size:int=16)-> ImageFont.FreeTypeFont:""" 获取默认字体 参数: size: 字体大小 返回: 字体对象 """try:return ImageFont.truetype("arial.ttf", size)except Exception:return ImageFont.load_default()defdemonstrate_drawing():""" 演示图像绘制 """# 创建绘制器 drawer = PillowDrawing(size=(800,600), background='white')print("Pillow图像绘制演示")print("="*50)# 绘制网格 drawer.draw_grid(50,(230,230,230))print("绘制网格")# 绘制基本图形 drawer.draw_rectangle((50,50,200,150), outline='blue', fill='lightblue', width=2)print("绘制矩形") drawer.draw_circle((350,100),50, outline='red', fill='pink', width=2)print("绘制圆形") drawer.draw_ellipse((450,50,650,150), outline='green', fill='lightgreen', width=2)print("绘制椭圆")# 绘制多边形 points =[(100,250),(150,200),(200,250),(175,300),(125,300)] drawer.draw_polygon(points, outline='purple', fill='lavender')print("绘制多边形")# 绘制弧线和扇形 drawer.draw_arc((300,200,450,350),0,180,'orange',3)print("绘制弧线") drawer.draw_pieslice((500,200,650,350),0,270, outline='brown', fill='tan')print("绘制扇形")# 绘制线条和箭头 drawer.draw_line((50,400),(200,400),'black',2) drawer.draw_arrow((250,400),(400,400),'darkblue',2,15)print("绘制线条和箭头")# 绘制十字标记 drawer.draw_cross((500,400),20,'red',3)print("绘制十字标记")# 绘制文字 font_manager = FontManager() default_font = font_manager.get_default_font(20) drawer.draw_text((50,500),"Hello, Pillow!",'darkblue', default_font) drawer.draw_text_with_box((50,540),"Text with Background",'white', default_font,'darkgreen',5) drawer.draw_text_centered("Centered Text",'purple', default_font)print("绘制文字")# 绘制圆角矩形 drawer.draw_rounded_rectangle((550,400,750,550),20, outline='teal', fill='lightcyan', width=2)print("绘制圆角矩形")return drawer.get_image()if __name__ =="__main__": image = demonstrate_drawing()print("\n图像绘制演示完成")5.5 本章小结
本章详细介绍了Pillow库的使用方法,包括图像的读取、写入、格式转换、几何变换、图像增强和图像绘制等功能。Pillow是Python图像处理的重要工具,它提供了简洁易用的API和丰富的功能支持。
Pillow与OpenCV各有优势,在实际项目中通常会结合使用。Pillow适合处理图像格式转换、基本操作和图像绘制等任务,而OpenCV更适合复杂的计算机视觉任务。两个库之间的转换非常简单,可以通过NumPy数组实现无缝衔接。
下一章将介绍图像色彩空间转换与通道操作,深入讲解RGB、HSV、LAB等色彩空间的原理和应用。
GPT-5.4辅助编程提示词:
我需要使用Pillow处理一批图像,请帮我编写完整的Python代码: 需求描述: 1. 读取指定目录下的所有图像 2. 对每张图像进行以下处理: - 自动调整方向(根据EXIF信息) - 缩放到指定最大尺寸(保持宽高比) - 添加水印(文字水印,位置可配置) - 调整亮度和对比度 - 添加边框 3. 支持输出为JPEG/PNG/WebP格式 4. 生成处理报告 技术要求: - 使用Pillow库 - 支持批量处理 - 兼容Python 3.13 - 提供命令行接口 - 完善的错误处理