Pillow:图片批量处理与编辑

Python 办公自动化专题 · Python图像处理在办公自动化中的实战应用

专题:Python 自动化办公系统学习

关键词:Python, 自动化办公, Pillow, PIL, 图片处理, 批量处理, 图像编辑, 水印, Python自动化

一、Pillow概述

Pillow 是 Python 生态中最著名的图像处理库,也是 PIL(Python Imaging Library)的现代化分支和继承者。PIL 最初由 Fredrik Lundh 开发,自 2009 年发布 1.1.7 版本后就停止了维护。Alex Clark 和社区在 PIL 的基础上创建了 Pillow,它不仅完全兼容 PIL 的 API,还持续增加了对新格式的支持和功能增强,成为 Python 图像处理的事实标准库。

Pillow 的设计哲学是"简单易用但功能强大"。它支持超过 100 种图像文件格式的读写,包括常见的 JPEG、PNG、GIF、BMP、TIFF、WebP,以及专业领域的格式如 PPM、PCX、ICO、PSD 等。在日常办公自动化中,它能够完美胜任批量图片格式转换、尺寸调整、水印添加、压缩优化等任务,是每个 Python 自动化工程师的必备工具。

Pillow 的核心模块可分为五个主要部分:Image 模块是最核心的类,负责图像的打开、创建、保存和信息获取;ImageDraw 模块提供基本的 2D 绘图功能,可以绘制直线、矩形、椭圆、多边形以及文字;ImageFilter 模块封装了丰富的滤镜效果,包括模糊、锐化、边缘增强等;ImageFont 模块用于加载字体文件以支持文字渲染;ImageEnhance 模块则专门用于调整图像的色彩、亮度、对比度和锐度。这些模块组合起来几乎可以覆盖日常办公中遇到的所有图像处理需求。

核心优势:Pillow 是纯 Python 实现,无需系统级依赖,通过 pip 一键安装即可使用。API 设计优雅简洁,学习成本极低,适合快速集成到自动化脚本中。

安装与版本选择

Pillow 的安装极为简单,通过 pip 命令即可完成。需要注意的是,旧版的 PIL 库已不再维护,切勿使用 pip install PIL。在 Python 3.x 环境下,建议安装最新的稳定版本以获得最佳的格式支持和性能表现。Pillow 与常见的 Python Web 框架(如 Flask、Django)和数据处理库(如 NumPy、OpenCV)均有良好的兼容性。

# 安装 Pillow(推荐 Python 3.8+) pip install Pillow # 验证安装 python -c "from PIL import Image; print(Image.__version__)" # 升级到最新版本 pip install --upgrade Pillow

核心模块一览

理解 Pillow 的模块结构是深入学习的第一步。以下代码展示了如何导入并了解各个核心模块的基本功能。

from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageEnhance from PIL import ImageChops, ImageOps # 查看支持的格式(Pillow 10.x 支持 100+ 格式) print("支持的打开格式:", Image.OPEN) print("支持的保存格式:", Image.SAVE) # 创建一个简单的纯色图像(RGB 模式) img = Image.new('RGB', (200, 100), '#2e7d32') img.show() # 调用系统默认图片查看器

二、图片打开与保存

图像文件的打开与保存是任何图像处理操作的起点和终点。Pillow 的 Image.open() 方法非常智能,它能够根据文件头自动识别图片格式,无需用户指定扩展名。这意味着即使文件扩展名被错误修改,Pillow 仍能正确读取图片内容。此外,Image.open() 默认采用惰性加载(lazy loading)策略,只有在真正需要像素数据时才将图像加载到内存中,这种设计在处理大量图片时能显著降低内存占用。

保存图片时,save() 方法会根据文件扩展名自动选择对应的编码器。如果需要在不同格式之间转换,只需更改保存时的扩展名即可。Pillow 提供了丰富的保存参数,例如 JPEG 格式的 quality 参数控制压缩质量(1-100,数值越大质量越高),PNG 格式的 compress_level 参数控制压缩级别(0-9)。对于需要保留透明背景的场景,必须选择支持 alpha 通道的格式(如 PNG、WebP、GIF),否则透明区域会被填充为默认的背景色。

在实际办公场景中,经常需要获取图片的元信息,包括尺寸(宽高)、颜色模式(RGB/RGBA/CMYK/L 等)、文件大小、DPI 信息等。这些信息对于后续的批量处理决策至关重要——例如,只有知道图片的原始尺寸,才能计算合适的缩放比例;只有了解颜色模式,才能确定是否需要先进行模式转换。

基本打开与保存操作

from PIL import Image # 打开图片(自动识别格式,惰性加载) img = Image.open('example.jpg') # 获取图片基本信息 print(f"格式: {img.format}") print(f"尺寸: {img.size}") # (宽度, 高度) print(f"宽: {img.width}, 高: {img.height}") print(f"颜色模式: {img.mode}") # RGB, RGBA, L(灰度), CMYK等 print(f"DPI: {img.info.get('dpi', '未设置')}") # 格式转换:JPEG → PNG img.save('output.png') # 格式转换时设置质量参数 img.save('compressed.jpg', quality=85, optimize=True)

创建缩略图

thumbnail() 方法是 Pillow 中非常实用的函数,它会在保持原始宽高比的前提下,将图片缩小到指定尺寸范围内,非常适合批量生成缩略图的场景。

# 生成缩略图(保持宽高比) img = Image.open('large_photo.jpg') img.thumbnail((300, 200)) # 等比缩放,不超过 300x200 img.save('thumbnail.jpg', quality=90) print(f"缩略图尺寸: {img.size}") # 批量生成缩略图(遍历目录) import os from pathlib import Path src_dir = Path('./photos') dst_dir = Path('./thumbnails') dst_dir.mkdir(exist_ok=True) for f in src_dir.glob('*.jpg'): img = Image.open(f) img.thumbnail((400, 400)) img.save(dst_dir / f.name, quality=85) print(f"已处理: {f.name} → {img.size}")

格式自动识别与安全打开

from PIL import Image, UnidentifiedImageError # 安全打开图片(避免损坏文件导致崩溃) def safe_open_image(filepath): try: with Image.open(filepath) as img: img.verify() # 验证文件完整性但不加载像素 return Image.open(filepath) # 重新打开用于实际操作 except (UnidentifiedImageError, IOError) as e: print(f"无法识别或打开: {filepath} - {e}") return None

三、几何变换

几何变换是图像处理中最基础也最常用的操作之一,包括缩放(resize)、裁剪(crop)、旋转(rotate)、翻转(transpose)和透视变换。Pillow 对每种变换都提供了简洁而强大的 API,能够满足办公自动化中绝大部分图片几何处理需求。理解不同的重采样算法(resampling filter)对于获得高质量的变换结果至关重要——不同的算法在速度和画质之间有不同的取舍。

缩放操作是日常工作中最频繁使用的功能,无论是将产品图片统一尺寸、缩小高清照片以节省存储空间,还是将用户头像裁剪为固定尺寸。Pillow 的 resize() 方法可以指定目标尺寸和重采样算法,而 thumbnail() 方法则自动保持宽高比。裁剪操作通过 crop() 方法实现,它接受一个四元组 (left, top, right, bottom) 定义裁剪区域。旋转操作支持任意角度旋转,但需要注意旋转后图片尺寸可能发生变化——默认会扩展画布以容纳完整图片,也可以选择裁剪到原始尺寸。

在实际应用中,几何变换需要配合数学计算才能满足复杂的业务需求。例如,将图片居中裁剪为正方形时,需要先计算原始图片的中心点,再根据所需边长确定裁剪区域;将图片旋转后填充特定背景色时,需要考虑到旋转角度对画布尺寸的影响。掌握这些技巧后,可以轻松应对商品图统一处理、用户头像生成、图片自动校正等常见的办公自动化场景。

缩放与裁剪

from PIL import Image img = Image.open('product.jpg') print(f"原始尺寸: {img.size}") # 1. 直接缩放到指定尺寸(可能变形) resized = img.resize((800, 600)) resized.save('resized_fixed.jpg') # 2. 使用高质量算法缩放 resized_hq = img.resize((800, 600), Image.LANCZOS) # LANCZOS 是最高质量的重采样 # 3. 居中裁剪为正方形 width, height = img.size min_side = min(width, height) left = (width - min_side) // 2 top = (height - min_side) // 2 right = left + min_side bottom = top + min_side cropped = img.crop((left, top, right, bottom)) cropped.save('cropped_square.jpg') # 4. 裁剪指定区域 box = (100, 50, 500, 400) # (左, 上, 右, 下) region = img.crop(box) region.save('region.jpg')

旋转与翻转

# 旋转操作 img = Image.open('photo.jpg') # 顺时针旋转 90 度 rotated_90 = img.rotate(90, expand=True, fillcolor='white') # 旋转任意角度,并指定背景填充色 rotated_45 = img.rotate(45, expand=True, fillcolor='white') # 翻转(镜像) flipped_h = img.transpose(Image.FLIP_LEFT_RIGHT) # 水平翻转 flipped_v = img.transpose(Image.FLIP_TOP_BOTTOM) # 垂直翻转 # 固定角度旋转(更高效) transposed = img.transpose(Image.ROTATE_90) # ROTATE_180, ROTATE_270 # 批量自动校正(根据EXIF方向信息) from PIL import ImageOps corrected = ImageOps.exif_transpose(img) if corrected: corrected.save('corrected.jpg')

透视变换

透视变换(perspective transform)可以将图片从一个四边形映射到另一个四边形,常用于文档扫描校正、广告牌图像替换等场景。

from PIL import Image img = Image.open('document.jpg') width, height = img.size # 定义源四边形的四个顶点 src_points = [(0, 0), (width, 0), (width, height), (0, height)] # 定义目标四边形的四个顶点(透视变形效果) dst_points = [(50, 0), (width-50, 0), (width, height), (0, height)] # 计算透视变换矩阵并应用 coeffs = Image._get_perspective_coeffs(src_points, dst_points) transformed = img.transform(img.size, Image.PERSPECTIVE, coeffs, Image.BICUBIC) transformed.save('perspective.jpg')

四、颜色与滤镜

颜色调整和滤镜效果是提升图片视觉质量的重要手段。Pillow 提供了从基础的颜色模式转换到高级的滤镜特效的完整工具链。颜色模式转换是许多图像处理操作的前置步骤——例如,在进行图像识别前通常需要将 RGB 图像转换为灰度模式,在保存透明背景图片时需要确保为 RGBA 模式,而 CMYK 模式则是印刷输出的标准格式。Pillow 的 convert() 方法可以轻松在这些模式之间切换。

ImageEnhance 模块提供了四种颜色调整工具:亮度(Brightness)、对比度(Contrast)、锐度(Sharpness)和色彩饱和度(Color)。每个工具都通过一个简单的因子参数来控制调整强度——1.0 表示保持原样,小于 1.0 减弱效果,大于 1.0 增强效果。这种设计使得代码写起来非常直观且易于理解。在实际办公场景中,批量调整图片亮度和对比度是常见的需求,例如在制作产品展示图时统一亮度和色彩风格。

滤镜方面,Pillow 的 ImageFilter 模块内置了十多种常用滤镜,包括模糊(BLUR、GaussianBlur、BoxBlur)、轮廓(CONTOUR、FIND_EDGES)、浮雕(EMBOSS)、锐化(SHARPEN、UnsharpMask)、平滑(SMOOTH、SMOOTH_MORE)等。对于更专业的处理需求,可以通过 Kernel 类创建自定义卷积核,实现定制化的滤镜效果。需要注意的是,某些滤镜(如高斯模糊)的强度可以通过参数调节,而另一些(如 BLUR、CONTOUR)则是固定效果的快捷方式。

颜色模式转换

from PIL import Image img = Image.open('photo.jpg') # RGB → 灰度图 gray = img.convert('L') # L = Luminance(亮度) gray.save('grayscale.jpg') # RGB → RGBA(添加透明通道) rgba = img.convert('RGBA') rgba.save('with_alpha.png') # RGB → CMYK(印刷模式) cmyk = img.convert('CMYK') cmyk.save('for_print.tif') # 逐像素点操作(反色效果) inverted = Image.eval(img, lambda x: 255 - x) inverted.save('inverted.jpg')

色彩增强与调整

from PIL import Image, ImageEnhance img = Image.open('dark_photo.jpg') # 亮度增强(因子 > 1.0 提高亮度) enhancer = ImageEnhance.Brightness(img) brighter = enhancer.enhance(1.5) # 亮度提高 50% brighter.save('brighter.jpg') # 对比度增强 enhancer = ImageEnhance.Contrast(img) more_contrast = enhancer.enhance(1.3) more_contrast.save('more_contrast.jpg') # 锐度增强 enhancer = ImageEnhance.Sharpness(img) sharper = enhancer.enhance(2.0) # 锐度提高一倍 # 色彩饱和度调整 enhancer = ImageEnhance.Color(img) vivid = enhancer.enhance(1.5) # 色彩饱和度提高 50% # 批量统一调整(多张照片统一风格) import os for f in os.listdir('./photos'): if f.lower().endswith(('.jpg', '.png')): img = Image.open(os.path.join('./photos', f)) img = ImageEnhance.Brightness(img).enhance(1.1) img = ImageEnhance.Contrast(img).enhance(1.2) img.save(os.path.join('./output', f))

内置滤镜与自定义卷积核

from PIL import Image, ImageFilter img = Image.open('photo.jpg') # 内置滤镜 blurred = img.filter(ImageFilter.BLUR) # 模糊 gaussian = img.filter(ImageFilter.GaussianBlur(5)) # 高斯模糊(可调半径) sharpened = img.filter(ImageFilter.SHARPEN) # 锐化 edges = img.filter(ImageFilter.FIND_EDGES) # 边缘检测 emboss = img.filter(ImageFilter.EMBOSS) # 浮雕效果 smooth = img.filter(ImageFilter.SMOOTH) # 平滑 contour = img.filter(ImageFilter.CONTOUR) # 轮廓 # 自定义卷积核(锐化核) from PIL.ImageFilter import Kernel sharpen_kernel = Kernel((3, 3), [ 0, -1, 0, -1, 5, -1, 0, -1, 0 ], scale=1, offset=0) custom_sharp = img.filter(sharpen_kernel) custom_sharp.save('custom_sharp.jpg')

五、绘图与文字

Pillow 的 ImageDraw 模块提供了在图片上绘制各种图形和文字的能力,这在制作图表、添加标注、生成证书、创建社交图片等场景中非常实用。绘图功能支持绘制直线(line)、矩形(rectangle)、椭圆(ellipse)、圆弧(arc)、弦(chord)、饼图(pieslice)、多边形(polygon)和任意点(point)。每种绘图函数都支持自定义轮廓颜色、填充颜色、线条宽度等参数。

文字绘制是办公自动化中最常用的功能之一——从简单的图片加标题、标注日期,到复杂的证书生成、海报制作,都离不开文字渲染。Pillow 使用 ImageFont 模块加载字体文件,TrueType 字体(.ttf)和 OpenType 字体(.otf)均得到支持。渲染中文文本时,需要使用支持中文的字体文件,否则会出现乱码或方块。Windows 系统通常使用 "msyh.ttc"(微软雅黑)或 "simsun.ttc"(宋体),Linux 系统可以使用 "NotoSansCJK" 系列字体。

文本排版是文字绘制的进阶技巧,主要包括文本对齐(左对齐、居中对齐、右对齐)、文本换行处理、多行文本绘制以及文本尺寸计算。Pillow 的 textbbox() 方法可以精确计算文本的边界框,结合图片尺寸即可实现文本居中显示。对于需要换行的长文本,需要开发者自行实现换行逻辑——根据 textbbox() 测量每行宽度,在超出目标宽度时进行截断。掌握这些技巧后,可以轻松实现自动生成证书、制作分享卡片、批量添加水印等办公自动化任务。

基本绘图操作

from PIL import Image, ImageDraw # 创建空白画布 img = Image.new('RGB', (500, 400), 'white') draw = ImageDraw.Draw(img) # 绘制直线 draw.line([(50, 50), (450, 50)], fill='red', width=3) # 绘制矩形 draw.rectangle([(100, 100), (400, 200)], outline='blue', width=2) # 绘制填充矩形 draw.rectangle([(100, 230), (400, 280)], fill='#2e7d32') # 绘制圆形 draw.ellipse([(200, 300), (300, 380)], outline='green', width=2) # 绘制多边形(三角形) draw.polygon([(250, 310), (220, 370), (280, 370)], fill='orange') img.save('shapes.png')

文字绘制(支持中文)

from PIL import Image, ImageDraw, ImageFont img = Image.new('RGB', (600, 300), '#f0f4f8') draw = ImageDraw.Draw(img) # 加载中文字体(根据系统选择合适路径) try: font_large = ImageFont.truetype('msyh.ttc', 48) # Windows 微软雅黑 font_small = ImageFont.truetype('simsun.ttc', 24) # 宋体 except IOError: font_large = ImageFont.load_default() # 回退到默认字体 font_small = ImageFont.load_default() # 绘制标题文本(居中) title = "Pillow 图片处理学习笔记" bbox = draw.textbbox((0, 0), title, font=font_large) tw = bbox[2] - bbox[0] # 文本宽度 x = (img.width - tw) // 2 draw.text((x, 40), title, fill='#2e7d32', font=font_large) # 绘制副标题 subtitle = "Python 办公自动化 · 图像处理实战" draw.text((180, 120), subtitle, fill='#666666', font=font_small) # 绘制日期 from datetime import datetime date_str = datetime.now().strftime("%Y年%m月%d日") draw.text((220, 180), date_str, fill='#999999', font=font_small) img.save('text_demo.png')

自动换行长文本

def draw_multiline_text(draw, text, x, y, max_width, font, fill='black', line_spacing=5): """自动换行绘制文本""" lines = [] for char in text: if not lines: lines.append(char) else: trial = lines[-1] + char bbox = draw.textbbox((0, 0), trial, font=font) if bbox[2] - bbox[0] > max_width: lines.append(char) else: lines[-1] = trial for i, line in enumerate(lines): draw.text((x, y + i * (font.size + line_spacing)), line, fill=fill, font=font) # 使用示例 text = "Pillow是Python图像处理的标准库,支持100多种图片格式的读写操作。" draw_multiline_text(draw, text, 50, 250, 400, font_small)

六、水印与批注

在办公自动化中,批量添加水印是最常见的需求之一——无论是保护公司图片版权、添加产品标识、还是给照片加上时间日期标记。Pillow 提供了灵活的水印方案,支持文字水印和图片水印两种形式。文字水印通过 ImageDraw 的 text() 方法实现,图片水印(如 Logo)则通过 Image.paste() 方法将水印图片粘贴到目标图片上。水印添加的核心技术点在于位置计算和透明度控制。

位置计算需要综合考虑水印自身尺寸和目标图片尺寸,通常需要支持多种对齐方式,如左上角、右下角、居中,以及平铺(tile)覆盖整个图片。在批量处理时,每张图片的尺寸可能不同,位置参数需要动态计算。透明度控制则通过 RGBA 模式实现——水印图片的 alpha 通道值决定了其透明程度。对于文字水印,可以先创建一个带有透明背景的文本图像,然后通过调整其透明度再与原始图片叠加。

批注功能则更多地服务于信息记录和归档场景——例如在照片上添加拍摄日期、GPS 坐标、拍摄参数(光圈、快门速度等),或者在产品图片上添加尺寸标签、价格标签。这些信息可以批量从 EXIF 数据或数据库记录中读取,然后自动化地绘制到对应的图片上。结合本章节学到的位置计算和样式定制技巧,可以构建一个功能完善的批量批注系统。

添加文字水印

from PIL import Image, ImageDraw, ImageFont def add_text_watermark(img, text, position='bottom-right', font_size=36, opacity=128, color='white'): """添加文字水印,支持位置和透明度控制""" overlay = Image.new('RGBA', img.size, (255, 255, 255, 0)) draw = ImageDraw.Draw(overlay) try: font = ImageFont.truetype('msyh.ttc', font_size) except IOError: font = ImageFont.load_default() bbox = draw.textbbox((0, 0), text, font=font) tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] # 位置计算 positions = { 'top-left': (20, 20), 'top-right': (img.width - tw - 20, 20), 'bottom-left': (20, img.height - th - 20), 'bottom-right': (img.width - tw - 20, img.height - th - 20), 'center': ((img.width - tw) // 2, (img.height - th) // 2), } pos = positions.get(position, positions['bottom-right']) draw.text(pos, text, fill=(255, 255, 255, opacity), font=font) return Image.alpha_composite(img.convert('RGBA'), overlay).convert('RGB') # 使用 img = Image.open('photo.jpg') watermarked = add_text_watermark(img, '© 佼艾科技', position='bottom-right', opacity=100) watermarked.save('watermarked.jpg', quality=95)

添加图片水印(Logo)

def add_image_watermark(bg_img, watermark_path, position='bottom-right', scale=0.15, opacity=0.6): """添加图片水印(支持缩放和透明度)""" bg = bg_img.convert('RGBA') wm = Image.open(watermark_path).convert('RGBA') # 按比例缩放水印 wm_width = int(bg.width * scale) wm_height = int(wm.height * (wm_width / wm.width)) wm = wm.resize((wm_width, wm_height), Image.LANCZOS) # 调整透明度 r, g, b, a = wm.split() a = a.point(lambda x: int(x * opacity)) wm = Image.merge('RGBA', (r, g, b, a)) # 位置计算 positions = { 'top-left': (20, 20), 'top-right': (bg.width - wm_width - 20, 20), 'bottom-left': (20, bg.height - wm_height - 20), 'bottom-right': (bg.width - wm_width - 20, bg.height - wm_height - 20), } pos = positions.get(position, positions['bottom-right']) bg.paste(wm, pos, wm) return bg.convert('RGB') # 平铺水印覆盖全图 bg = Image.open('photo.jpg').convert('RGBA') wm = Image.open('logo.png').convert('RGBA') wm = wm.resize((100, 100)) for x in range(0, bg.width, 150): for y in range(0, bg.height, 150): bg.paste(wm, (x, y), wm) bg.save('tiled_watermark.jpg')

批量添加时间日期批注

import os from pathlib import Path from PIL import Image, ImageDraw, ImageFont from PIL.ExifTags import TAGS def get_exif_datetime(img): """从EXIF中提取拍摄时间""" exif = img._getexif() if not exif: return "未知时间" for tag_id, value in exif.items(): tag = TAGS.get(tag_id, tag_id) if tag == 'DateTimeOriginal': return value return "未知时间" # 批量给照片添加拍摄日期 src_dir = Path('./photos') dst_dir = Path('./annotated') dst_dir.mkdir(exist_ok=True) font = ImageFont.truetype('msyh.ttc', 24) for f in src_dir.glob('*.jpg'): img = Image.open(f).convert('RGBA') date_str = get_exif_datetime(img) draw = ImageDraw.Draw(img) # 右下角添加日期 bbox = draw.textbbox((0, 0), date_str, font=font) tw, th = bbox[2]-bbox[0], bbox[3]-bbox[1] x, y = img.width - tw - 15, img.height - th - 15 draw.text((x, y), date_str, fill=(255, 255, 255, 200), font=font) img.convert('RGB').save(dst_dir / f.name, quality=95) print(f"已批注: {f.name}")

七、批量处理

批量处理是办公自动化的核心价值所在。当需要处理成百上千张图片时,手动操作既耗时又容易出错,而 Python + Pillow 的组合可以在几秒钟内完成相同的工作。批量处理的典型模式包括:遍历指定目录下的所有图片文件、对每张图片执行相同的处理操作、将结果保存到输出目录。这种"文件遍历 + 图像处理 + 文件保存"的三段式结构是批量处理脚本的通用模板。

性能优化是批量处理中不可忽视的环节。对于 CPU 密集型的图像处理任务,多线程(multithreading)可以有效提升处理速度,尤其适合 I/O 密集型的格式转换操作。Python 的 concurrent.futures 模块提供了 ThreadPoolExecutor 和 ProcessPoolExecutor 两种并行方案——前者适合 I/O 密集型任务,后者适合 CPU 密集型任务。在实际使用中,ProcessPoolExecutor 对 Pillow 操作通常能带来 2-4 倍的加速效果(取决于 CPU 核心数)。

除了性能优化,完善的批量处理脚本还需要考虑以下几个方面:进度显示(tqdm 库或自制的进度条)、错误处理(单个文件处理失败不应影响后续处理)、日志记录(记录处理成功和失败的文件列表)、增量处理(只处理修改过的文件)以及可配置性(通过命令行参数或配置文件控制处理参数)。将这些要素整合到脚本中,可以构建出生产级别的批量图像处理工具。

基础批量处理

import os from pathlib import Path from PIL import Image def batch_convert_to_png(src_dir, dst_dir): """批量转换 JPEG 为 PNG""" src = Path(src_dir) dst = Path(dst_dir) dst.mkdir(parents=True, exist_ok=True) succeeded = 0 failed = 0 for f in src.glob('*.*'): if f.suffix.lower() not in ('.jpg', '.jpeg', '.tif', '.bmp'): continue try: with Image.open(f) as img: output_path = dst / f"{f.stem}.png" img.save(output_path, 'PNG') succeeded += 1 except Exception as e: print(f"处理失败 [{f.name}]: {e}") failed += 1 print(f"处理完成!成功: {succeeded}, 失败: {failed}") # 执行批量处理 batch_convert_to_png('./raw_photos', './converted')

多线程加速处理

from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path from PIL import Image import os def process_single_image(filepath, output_dir, max_size=(1920, 1080)): """单张图片处理函数""" try: with Image.open(filepath) as img: # 等比缩放 img.thumbnail(max_size, Image.LANCZOS) # 转 sRGB 并优化压缩 if img.mode == 'RGBA': img = img.convert('RGB') output_path = Path(output_dir) / Path(filepath).name img.save(output_path, 'JPEG', quality=85, optimize=True) return (True, Path(filepath).name) except Exception as e: return (False, f"{Path(filepath).name}: {e}") def batch_process_multithreaded(src_dir, dst_dir, max_workers=4): """多线程批量处理图片""" src, dst = Path(src_dir), Path(dst_dir) dst.mkdir(parents=True, exist_ok=True) files = [f for f in src.glob('*') if f.suffix.lower() in ('.jpg', '.jpeg', '.png')] print(f"共发现 {len(files)} 张图片,使用 {max_workers} 个线程处理...") with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = {executor.submit(process_single_image, f, dst): f for f in files} done = 0 for future in as_completed(futures): success, msg = future.result() done += 1 status = "✓" if success else "✗" print(f"[{done}/{len(files)}] {status} {msg}") batch_process_multithreaded('./photos', './processed', max_workers=8)

带进度显示的批量处理 + 重命名

from tqdm import tqdm # pip install tqdm 进度条库 def batch_resize_with_rename(src_dir, dst_dir, prefix="processed_", target_size=(800, 800)): src, dst = Path(src_dir), Path(dst_dir) dst.mkdir(exist_ok=True) images = [f for f in src.glob('*.jpg')] for i, f in enumerate(tqdm(images, desc="处理中")): with Image.open(f) as img: img.thumbnail(target_size, Image.LANCZOS) new_name = f"{prefix}{i+1:04d}{f.suffix}" img.save(dst / new_name, quality=90) print(f"处理完成!输出目录: {dst}")

八、高级应用

除了基础的图像处理操作,Pillow 还支持一系列高级应用功能,包括图片拼接与合成、GIF 动画创建、图片压缩优化、透明背景处理以及 EXIF 信息读取管理。这些功能在实际工作中非常实用,能够满足更复杂的业务场景需求。

图片拼接是将多张图片组合成一张大图的过程,常见于商品展示图(白底图拼接)、长截图制作、全景图合成等场景。Pillow 实现拼接的基本思路是:先计算最终画布的尺寸,创建一张空白大图,然后通过 paste() 方法将各张图片粘贴到指定位置。根据排列方式的不同,可以分为水平拼接、垂直拼接、网格拼接和自由布局拼接。对于尺寸不一致的图片,拼接前需要进行统一尺寸或对齐处理。

GIF 创建是另一个非常实用的功能——无论是制作简单的产品展示动画、演示操作步骤,还是生成数据可视化 GIF 图。Pillow 支持从多帧图像创建 GIF 动画,并且可以控制每帧的持续时间(duration)、循环次数(loop)和透明度(transparency)。对于需要逐帧优化的高级场景,还可以结合其他模块实现帧间过渡效果、文字动画等。图片压缩优化则在 Web 性能优化中扮演着重要角色——在保证视觉质量的前提下尽可能减小文件体积,对于提升网站加载速度至关重要。

图片拼接与合成

from PIL import Image from pathlib import Path def horizontal_concat(image_paths, spacing=10, bg_color='white'): """水平拼接多张图片""" images = [Image.open(p) for p in image_paths] # 统一高度(按最高图片缩放) max_h = max(img.height for img in images) resized = [] for img in images: ratio = max_h / img.height new_w = int(img.width * ratio) resized.append(img.resize((new_w, max_h), Image.LANCZOS)) total_w = sum(img.width for img in resized) + spacing * (len(resized) - 1) canvas = Image.new('RGB', (total_w, max_h), bg_color) x_offset = 0 for img in resized: canvas.paste(img, (x_offset, 0)) x_offset += img.width + spacing return canvas def concat_grid(image_paths, cols=3, spacing=10): """网格拼接多张图片""" images = [Image.open(p) for p in image_paths] # 统一所有图片尺寸为 300x300 thumbnails = [] for img in images: thumb = img.copy() thumb.thumbnail((300, 300), Image.LANCZOS) thumbnails.append(thumb) w, h = 300, 300 rows = (len(thumbnails) + cols - 1) // cols canvas_w = cols * w + (cols - 1) * spacing canvas_h = rows * h + (rows - 1) * spacing canvas = Image.new('RGB', (canvas_w, canvas_h), 'white') for idx, thumb in enumerate(thumbnails): row, col = divmod(idx, cols) x = col * (w + spacing) y = row * (h + spacing) canvas.paste(thumb, (x, y)) return canvas # 使用示例 # result = horizontal_concat(['img1.jpg', 'img2.jpg', 'img3.jpg']) # result.save('concat_horizontal.jpg')

创建 GIF 动画

from PIL import Image import os def create_gif_from_images(image_paths, output_path, duration=500, loop=0): """从图片列表创建 GIF 动画 duration: 每帧持续时间(毫秒) loop: 循环次数(0 = 无限循环) """ frames = [] for path in image_paths: img = Image.open(path).convert('P') # GIF 需要调色板模式 frames.append(img) # 第一帧作为基础,其余帧附加 frames[0].save( output_path, save_all=True, append_images=frames[1:], duration=duration, loop=loop, optimize=True ) print(f"GIF 已创建: {output_path} ({len(frames)} 帧)") def create_text_animation_gif(text, output_path): """创建文字动画 GIF""" from PIL import ImageDraw, ImageFont frames = [] colors = ['#e74c3c', '#2ecc71', '#3498db', '#f39c12', '#9b59b6'] for color in colors: frame = Image.new('RGB', (400, 200), 'white') draw = ImageDraw.Draw(frame) font = ImageFont.truetype('msyh.ttc', 36) bbox = draw.textbbox((0, 0), text, font=font) x = (frame.width - (bbox[2]-bbox[0])) // 2 y = (frame.height - (bbox[3]-bbox[1])) // 2 draw.text((x, y), text, fill=color, font=font) frames.append(frame) frames[0].save(output_path, save_all=True, append_images=frames[1:], duration=800, loop=0) create_text_animation_gif("Hello Pillow!", 'animation.gif')

图片压缩优化

def optimize_image(input_path, output_path, target_size_kb=200, min_quality=50, max_quality=95): """自适应质量压缩到目标文件大小""" img = Image.open(input_path) # 先缩放到合理尺寸(限制最长边) max_dim = 1920 if img.width > max_dim or img.height > max_dim: img.thumbnail((max_dim, max_dim), Image.LANCZOS) # 二分法搜索最优质量 low, high = min_quality, max_quality best_quality = max_quality best_path = output_path while low <= high: mid = (low + high) // 2 img.save('_temp.jpg', quality=mid, optimize=True) size_kb = os.path.getsize('_temp.jpg') / 1024 if size_kb <= target_size_kb: best_quality = mid low = mid + 1 else: high = mid - 1 img.save(output_path, quality=best_quality, optimize=True) final_size = os.path.getsize(output_path) / 1024 os.remove('_temp.jpg') print(f"压缩完成: {input_path} → {output_path}") print(f"质量={best_quality}, 大小={final_size:.1f}KB") return final_size # 读取和修改 EXIF 信息 from PIL.ExifTags import TAGS, GPSTAGS def read_exif(image_path): img = Image.open(image_path) exif = img._getexif() if not exif: return {} info = {} for tag_id, value in exif.items(): tag = TAGS.get(tag_id, tag_id) info[tag] = value return info # exif_info = read_exif('photo.jpg') # for k, v in exif_info.items(): # print(f"{k}: {v}")

九、实战案例

理论学习的最终目的是解决实际问题。下面通过四个典型的办公自动化实战案例,展示如何将 Pillow 的各项功能组合起来,构建完整的图像处理解决方案。每个案例都从真实业务需求出发,涵盖了完整的代码实现和关键要点说明。

案例一:批量转换 RAW 格式照片为 JPEG,这是摄影爱好者和电商从业者的刚需。通过遍历相机存储卡或文件夹中的 RAW 文件(如 CR2、NEF、ARW 等系),利用 Pillow 的格式兼容能力转换为通用的 JPEG 格式,同时可以根据需要调整尺寸、优化压缩参数。案例二:批量添加公司水印,保护企业图片资产,支持平铺水印和单角水印两种模式,并自动根据图片尺寸调整水印大小。案例三:商品图自动压缩优化,确保每个商品图片在保持视觉质量的同时达到最小的文件体积,提升网站加载速度。案例四:证件照自动生成,从一张生活照自动裁剪生成符合各种规格的证件照,包括尺寸调整、背景替换、格式转换等。

这些案例的共同点在于:它们都遵循"输入遍历 → 逐图处理 → 输出保存"的批处理模式,同时加入了完善的错误处理和进度反馈机制。通过组合前面各章节的知识点——几何变换、颜色调整、水印添加、压缩优化等——可以构建出功能完善的自动化图片处理流水线。开发者可以根据自己的业务需求,在这些案例的基础上进行修改和扩展。

案例一:批量转换 RAW 格式为 JPG

from PIL import Image from pathlib import Path from concurrent.futures import ProcessPoolExecutor, as_completed RAW_EXTENSIONS = ('.cr2', '.nef', '.arw', '.dng', '.orf', '.rw2') def convert_raw_to_jpg(raw_path, output_dir, max_size=(3840, 2160), quality=92): try: with Image.open(raw_path) as img: # 转换为 sRGB if img.mode != 'RGB': img = img.convert('RGB') # 缩放到 4K 以内 img.thumbnail(max_size, Image.LANCZOS) # 输出 JPG output_path = Path(output_dir) / f"{Path(raw_path).stem}.jpg" img.save(output_path, 'JPEG', quality=quality, optimize=True) size_kb = output_path.stat().st_size / 1024 return (True, Path(raw_path).name, size_kb) except Exception as e: return (False, Path(raw_path).name, str(e)) def batch_convert_raw(src_dir="./raw", dst_dir="./jpg"): src, dst = Path(src_dir), Path(dst_dir) dst.mkdir(parents=True, exist_ok=True) raw_files = [f for ext in RAW_EXTENSIONS for f in src.glob(f"*{ext}")] raw_files += [f for ext in RAW_EXTENSIONS for f in src.glob(f"*{ext.upper()}")] print(f"发现 {len(raw_files)} 个 RAW 文件,开始转换...") with ProcessPoolExecutor() as executor: futures = {executor.submit(convert_raw_to_jpg, f, dst): f for f in raw_files} for future in as_completed(futures): success, name, info = future.result() if success: print(f"✓ {name} → {info:.1f}KB") else: print(f"✗ {name}: {info}") # batch_convert_raw('./camera_raw', './converted_jpg')

案例二:批量添加公司水印

from PIL import Image, ImageDraw, ImageFont from pathlib import Path def batch_add_watermark(input_dir, output_dir, watermark_text, font_path='msyh.ttc'): """批量添加公司水印""" src, dst = Path(input_dir), Path(output_dir) dst.mkdir(exist_ok=True) font = ImageFont.truetype(font_path, 48) image_files = [f for f in src.glob('*') if f.suffix.lower() in ('.jpg', '.png', '.webp')] for f in image_files: img = Image.open(f).convert('RGBA') # 创建水印层 txt_layer = Image.new('RGBA', img.size, (255,255,255,0)) draw = ImageDraw.Draw(txt_layer) # 在四个角各显示一个水印 bbox = draw.textbbox((0,0), watermark_text, font=font) tw, th = bbox[2]-bbox[0], bbox[3]-bbox[1] padding = 30 positions = [ (padding, padding), (img.width - tw - padding, padding), (padding, img.height - th - padding), (img.width - tw - padding, img.height - th - padding) ] for pos in positions: draw.text(pos, watermark_text, font=font, fill=(255,255,255,80)) # 合并水印层和原图 result = Image.alpha_composite(img, txt_layer).convert('RGB') result.save(dst / f.name, quality=95) print(f"水印已添加: {f.name}") # batch_add_watermark('./products', './watermarked', '© 佼艾科技 www.shjiaoai.com')

案例三:商品图自动压缩

def auto_compress_product_images(src_dir, dst_dir, max_width=1200, target_quality=80): """商品图自动压缩(限制尺寸 + JPEG 优化)""" src, dst = Path(src_dir), Path(dst_dir) dst.mkdir(exist_ok=True) total_saved = 0 for f in src.glob('*'): if f.suffix.lower() not in ('.jpg', '.jpeg', '.png', '.webp'): continue original_size = f.stat().st_size img = Image.open(f) # 等比缩放宽度 if img.width > max_width: ratio = max_width / img.width new_h = int(img.height * ratio) img = img.resize((max_width, new_h), Image.LANCZOS) # 转 JPEG 并压缩输出 if img.mode != 'RGB': img = img.convert('RGB') output_path = dst / f"{f.stem}.jpg" img.save(output_path, 'JPEG', quality=target_quality, optimize=True) new_size = output_path.stat().st_size saved = original_size - new_size total_saved += saved print(f"{f.name}: {original_size//1024}KB → {new_size//1024}KB (节省 {saved//1024}KB)") print(f"\n总计节省: {total_saved//1024}KB ({total_saved/1024/1024:.1f}MB)")

案例四:证件照自动生成

def auto_id_photo(input_path, output_path, size=(413, 579), bg_color='#ffffff'): """从照片自动生成标准证件照 默认尺寸: 413x579(1寸照,35x49mm,300dpi) """ img = Image.open(input_path) # 先裁剪为 3:4 比例(标准证件照比例) target_ratio = size[0] / size[1] # 3:4 w, h = img.size current_ratio = w / h if current_ratio > target_ratio: # 图片偏宽,裁剪左右 new_w = int(h * target_ratio) left = (w - new_w) // 2 img = img.crop((left, 0, left + new_w, h)) else: # 图片偏高,裁剪上下 new_h = int(w / target_ratio) top = (h - new_h) // 3 # 偏上裁剪(头部在上方) img = img.crop((0, top, w, top + new_h)) # 缩放到目标尺寸 img = img.resize(size, Image.LANCZOS) # 换背景色(白色/蓝色/红色) if img.mode != 'RGBA': img = img.convert('RGBA') # 简单的颜色替换(实际生产需更精细的抠图) data = img.getdata() new_data = [] bg_rgb = Image.new('RGB', (1, 1), bg_color).getpixel((0, 0)) for item in data: # 如果像素接近白色(背景),替换为目标颜色 if item[0] > 200 and item[1] > 200 and item[2] > 200: new_data.append((bg_rgb[0], bg_rgb[1], bg_rgb[2], 255)) else: new_data.append(item) img.putdata(new_data) # 设置 DPI 并保存 img.convert('RGB').save(output_path, dpi=(300, 300), quality=95) print(f"证件照已生成: {output_path} ({size[0]}x{size[1]})") # auto_id_photo('portrait.jpg', 'id_photo.jpg', bg_color='#ffffff') # auto_id_photo('portrait.jpg', 'id_photo_blue.jpg', bg_color='#2b7bc8')