1. 为什么是Pillow图像处理入门的瑞士军刀1.1 Pillow到底解决了什么问题做Python开发的人只要跟图片打过交道几乎都用过Pillow。这个库在Python生态里的地位相当于美工手里的Photoshop、厨师手里的菜刀——不是它功能最强大而是它开箱即用、覆盖面广日常能碰到的图像处理需求它基本都能接住。Pillow的前身是PILPython Imaging Library这个库早在上世纪90年代就存在了但后来年久失修对Python 3的支持一直跟不上。Pillow是PIL的友好分支API基本保持一致但持续维护、支持新版本Python还加入了大量新特性。所以现在大家说“用PIL”实际上装的都是Pillow导入语句仍然写作from PIL import Image。它到底能解决什么问题简单说就是图像的读取、保存、格式转换、缩放裁剪、颜色调整、滤镜特效、绘制文字图形、批量处理以及GIF动图、WebP、EXIF信息读取这些进阶操作。比如你想把一个目录下几百张图片全部压缩成WebP格式或者给图片批量加水印再或者从一张证件照里提取人脸区域裁出来Pillow都可以几行代码搞定。有人可能会问OpenCV不是也能做这些吗确实能但OpenCV的重点在计算机视觉算法比如人脸检测、目标跟踪、图像配准它的图像读写反而不是强项而且安装体积大、依赖多。Pillow的场景定位更像是“图像文件处理工具”比OpenCV轻量得多语法也友好得多。适合把Pillow当成第一梯队的工具来学。1.2 Pillow与OpenCV、scikit-image的定位差异我经常看到初学者纠结该学Pillow还是OpenCV甚至有人在论坛上问“我用Pillow做图像处理是不是不够专业”。这种焦虑完全没必要。三个库解决的是不同层面的问题我用一张表把它们的差异说清楚对比维度PillowOpenCVscikit-image核心强项图像IO、格式转换、绘制、批量处理计算机视觉算法、视频流处理科学计算图像算法典型场景压缩图片、加水印、缩略图、格式转换人脸识别、目标检测、摄像头实时处理图像分割、特征提取、滤波研究依赖复杂度低pip直接装较高部分功能需要额外库依赖NumPy、SciPy学习曲线平缓较陡中等像素级操作弱需转NumPy强原生支持强原生支持从这张表能看出来Pillow更像一个“图片文件处理工具”OpenCV则是“视觉算法引擎”。日常自动化办公、爬虫数据清洗、Web后端处理用户上传图片Pillow是最合适的。你如果要做人脸识别、车牌识别、视频分析那再去学OpenCV不迟。两个不是替代关系是互补关系。另外还有一个库叫scikit-image它跟SciPy是一套生态的函数风格跟OpenCV有些类似更偏科研和算法复现。我的建议是普通开发者把Pillow吃透用到的时候再按需学OpenCV科研人员再加一个scikit-image。不用一上来就全铺开。1.3 Pillow的高频应用场景结合我这几年实际用下来的经验Pillow最常见的应用场景大概是这些图片格式转换与压缩把PNG转JPG、转WebP给图片降体积批量处理尤其实用。批量添加水印给摄影师作品、电商图、公众号头图批量打上版权信息。生成缩略图网站上传图片后自动生成不同尺寸的预览图。图片裁剪与几何变换按指定区域裁图、旋转、镜像。颜色调整增强对比度、饱和度、亮度做老照片修复前的预处理。验证码生成爬虫模拟登录时虽然现在用得少了但老一代爬虫脚本里Pillow是标配。图像数据集预处理做深度学习前用Pillow把图片统一尺寸、去EXIF信息。GIF动图拆帧与合成把一个动图拆解成逐帧图片或者反过来把多张图合成动图。这些场景每一个都不复杂但组合起来非常能打。接下来我就从环境搭建开始把Pillow的核心功能一次讲透。2. 环境准备与核心概念2.1 安装与版本确认安装Pillow很简单一行命令的事pip install pillow如果你的环境里有多个Python版本或者用了虚拟环境注意先激活对应环境再安装。国内网络环境下如果pip下载慢可以加国内镜像源pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后在Python中确认一下版本号避免跟老项目里残留的PIL产生混淆import PIL print(PIL.__version__) from PIL import Image print(Image.__name__)正常情况下会输出类似10.4.0这样的版本号。这里有一个我自己踩过的坑如果你以前装过PIL再装Pillow的时候Python会把两个包都识别出来导入时优先找的是PIL目录但API基本一致一般问题不大。不过为了干净建议在新虚拟环境里装不要跟旧的环境混着用。Pillow的依赖很少主要依赖就是Python本身。这在部署到服务器时非常友好不像OpenCV那样动不动就报缺库、缺DLL。如果安装中途报错绝大多数情况是pip版本太低先执行pip install --upgrade pip再装一遍。2.2 图像模式与像素访问Pillow里最常用的一个类是Image打开一张图片就是Image.open()。但在深入之前先把几个核心概念说清楚。第一个概念是“图像模式”。Pillow支持多种颜色模式最常用的是这么几种模式说明通道数L灰度图每个像素一个0-255的亮度值1RGB真彩色红绿蓝三通道3RGBARGB加Alpha透明通道4CMYK印刷四色模式4P调色板模式像素值映射到颜色表1YCbCr视频、JPEG常用的颜色空间3不同模式之间可以直接转换比如img.convert(RGB)可以把RGBA图转成RGB把P模式转成RGB。后续做JPEG保存时RGBA必须转RGB才能保存。第二个概念是“坐标系”。Pillow的坐标系原点在图片左上角x轴向右递增y轴向下递增单位是像素。比如一张宽800、高600的图左上角是(0, 0)右下角是(799, 599)。所有裁剪、绘制、像素访问都基于这个坐标系这个概念不搞清楚裁剪出来的图永远歪。第三个概念是“像素访问”。最直接的方式是用getpixel和putpixelimg Image.open(photo.jpg) point img.getpixel((100, 100)) print(point) # 输出(红, 绿, 蓝)或灰度值但这两个方法性能非常差逐像素循环处理一张几百万像素的图能把CPU跑冒烟。如果要做像素级操作正确的做法是转成NumPy数组import numpy as np arr np.array(img) # 转成numpy数组 print(arr.shape) # (高度, 宽度, 通道数) arr arr[:, :, ::-1] # 通道反转RGB变BGR img2 Image.fromarray(arr) # 再转回Image对象这条转NumPy的思路在实际工程里用得非常多几乎是我写Pillow代码的基础操作。原因很简单NumPy的向量化运算比Python循环快几个数量级配合Pillow的Image.fromarray既能享受NumPy的计算能力又能借助Pillow的IO和保存能力。2.3 读取、保存与格式转换先说读取。Image.open()是惰性加载的也就是说打开文件时不会立刻把全部像素读入内存而是先读取文件头信息真正需要像素时才加载。这个设计对处理大图非常友好你可以先用它快速获取宽、高、模式、格式等元数据再决定要不要加载。from PIL import Image img Image.open(sample.png) print(img.size) # (1920, 1080) print(img.mode) # RGB 或 RGBA print(img.format) # PNG # 真正加载像素后后续操作才会触发解码 img.load()再说保存。img.save()支持格式由文件扩展名决定但也可以通过参数显式指定img.save(output.jpg, JPEG, quality85, optimizeTrue)JPEG有损压缩的quality参数控制质量范围1-95默认75。日常压缩图片时我一般用85既保留了不错的视觉质量体积也能压缩到比较理想的程度。optimizeTrue会额外做一次优化让文件更小一点但保存时间会加长适合离线批量处理。格式转换是Pillow最常见也最简单的操作。比如把PNG转成JPEGpng_img Image.open(logo.png) rgb_img png_img.convert(RGB) rgb_img.save(logo.jpg, quality90)这里有一个经典坑PNG可能带透明通道直接save成JPEG会报错必须先用convert(RGB)把透明通道去掉否则保存时会出现类似于cannot write mode RGBA as JPEG的报错。保存WebP格式也很常用WebP比JPEG体积更小适合网页使用img.save(output.webp, WEBP, quality80)注意WebP支持透明通道。如果你的Pillow版本比较旧保存WebP可能会提示缺少WebP支持升级Pillow到最新版基本就解决了。3. 动手实战Pillow核心功能逐个击破3.1 缩放、裁剪、旋转与镜像图片缩放有两个方法resize()和thumbnail()很多人容易搞混。resize()是直接指定目标宽高不管原始比例所以图片可能被拉伸变形img Image.open(photo.jpg) resized img.resize((200, 100)) # 强制改成200x100可能变形thumbnail()则保持图片宽高比自动缩放到指定尺寸范围内适合生成缩略图img.thumbnail((200, 200)) # 在200x200范围内等比缩小 print(img.size) # 实际结果可能是 (200, 150) 或 (150, 200)注意thumbnail()是原地修改不会返回新图resize()则返回新图。这个区别刚学Pillow时很容易犯迷糊。裁剪用crop()参数是左上角和右下角的坐标元组box (50, 50, 400, 300) # (left, upper, right, lower) cropped img.crop(box)这里有个小技巧right和lower坐标是不包含的。比如(0, 0, 100, 100)实际裁剪了100x100的矩形区域等价于取0到99的像素。边界算错一格通常肉眼看不出来但在像素级处理时会出问题。旋转用rotate()最容易被忽略的是expand参数rotated img.rotate(45, expandTrue, fillcolor(255, 255, 255))不加expandTrue时旋转后的图片尺寸不变内容会被裁切掉加了之后画布会自动扩展完整保留旋转后的内容露出的背景可以用fillcolor指定颜色。镜像翻转用transpose()这个方法比rotate(90)更精确因为它是像素级别的映射不做插值不会有质量损失flip_left_right img.transpose(Image.FLIP_LEFT_RIGHT) # 左右翻转 flip_top_bottom img.transpose(Image.FLIP_TOP_BOTTOM) # 上下翻转 rot90 img.transpose(Image.ROTATE_90) # 逆时针旋转90度实际做数据增强、图片预处理时这些几何操作会组合使用。比如我写过一个数据增强脚本对每张图片随机做0到3次90度旋转加一次水平翻转几行代码就把训练集扩充了好几个批次。3.2 颜色处理通道分离、点运算与增强颜色处理是Pillow比较有意思的部分它的API设计得很直观像一个个小积木可以搭出很多效果。分离和合并通道用split()和merge()r, g, b img.split() merged Image.merge(RGB, (r, g, b))通道分离有个很好玩的用途交换通道产生伪彩色效果。比如把红蓝通道对调原本蓝色的天空会变成橙红色这在艺术创意处理中经常能玩出花。红外摄影、遥感伪彩色合成原理上也都是通道的重新组合。point()方法可以对每个像素做映射变换类似于给每个像素值套一个函数# 把图片亮度取反相当于反色 inverted img.point(lambda x: 255 - x) # 增加亮度 brighter img.point(lambda x: min(x 30, 255))point()内部用查表实现速度很快适合做全局像素变换。很多网上的“Pillow亮度调整代码”其实就是在point()里加一个偏移量。如果想调整的东西更多、更精细直接用ImageEnhance模块它能调整亮度、对比度、锐度、色彩饱和度四大类参考下面的写法from PIL import ImageEnhance enhancer ImageEnhance.Contrast(img) contrast_img enhancer.enhance(1.5) # 对比度提升50% enhancer ImageEnhance.Brightness(img) bright_img enhancer.enhance(1.2) # 亮度提升20% enhancer ImageEnhance.Sharpness(img) sharp_img enhancer.enhance(2.0) # 锐度增强 enhancer ImageEnhance.Color(img) color_img enhancer.enhance(1.3) # 色彩饱和度提升30%enhance参数小于1时减弱属性大于1时增强。这套API内部其实是通过Image.blend来实现的底层逻辑是原图和一个特殊版本图像的混合所以性能也不错。老照片修复、图像增强等场景里我通常先ImageEnhance.Color把饱和度提一点再用Contrast增加层次感最后用Sharpness做锐化三步操作配合使用效果比单纯做一个滤镜好很多。3.3 滤镜与图像特效Pillow自带一个ImageFilter模块里面有十几种内置滤镜从基础的模糊、锐化到轮廓提取、边缘增强都有。from PIL import ImageFilter blur img.filter(ImageFilter.BLUR) # 普通模糊 gaussian img.filter(ImageFilter.GaussianBlur(2)) # 高斯模糊半径2 edges img.filter(ImageFilter.FIND_EDGES) # 边缘提取 contour img.filter(ImageFilter.CONTOUR) # 轮廓提取 sharpen img.filter(ImageFilter.SHARPEN) # 锐化这里面GaussianBlur最常用比如做背景虚化、给图片做毛玻璃效果。FIND_EDGES则能提取图片的轮廓信息配合反色可以做出类似素描线稿的效果。举个例子把边缘提取结果反色之后叠加到原图上可以做出简单的“手绘风”from PIL import Image, ImageOps, ImageFilter img Image.open(photo.jpg).convert(L) # 转灰度 edges img.filter(ImageFilter.FIND_EDGES) edges ImageOps.invert(edges) # 反色白色背景黑色线条 # 也可以把边缘图柔化后再和原图混合 blurred img.filter(ImageFilter.GaussianBlur(1)) result Image.blend(blurred, edges, 0.5)这种玩法在创意视觉、海报设计里很实用。做完这些滤镜操作输出结果时建议用PNG保存避免JPEG压缩二次损失细节。有一点值得注意Pillow的滤镜都是预置好的无法像OpenCV那样自定义卷积核。如果要做任意卷积滤波还是得转成NumPy自己实现或者交给OpenCV。Pillow适合快速出效果不太适合做算法研究。3.4 合成与透明抠图拼图实战图像合成包括blend、composite、paste三个方法它们的区别值得好好说说。blend要求两张图尺寸完全一致直接按权重混合img1 Image.open(bg.jpg) img2 Image.open(fg.jpg) blended Image.blend(img1, img2, alpha0.3)alpha取0到1之间0表示完全显示img11表示完全显示img2。这个功能像“多张照片淡入淡出”的过渡效果做幻灯片切换动画时很常用。composite可以根据mask掩码决定每个像素取哪张图的颜色composited Image.composite(img1, img2, mask)日常用得最多的是paste把一张图贴到另一张图上。最经典的应用就是给图片加Logo水印或者贴图background Image.open(bg.jpg).convert(RGBA) logo Image.open(logo.png).convert(RGBA) # logo带透明通道 background.paste(logo, (100, 100), logo) # 第三个参数是mask这里第三个参数传logo表示用logo的Alpha通道作为透明掩码。有些新手直接把透明PNG贴上去结果透明区域变成了黑色色块原因就是没传mask参数。把背景和贴图都转成RGBA模式是个好习惯否则在合成时容易出现颜色异常。处理完成后如果想要JPEG输出记得再转回RGB。3.5 批量处理与自动化Pillow的批量处理能力是我工作中用到最多的功能之一。最典型的场景一个文件夹里几百张照片要全部压缩、重命名、加水印。from pathlib import Path from PIL import Image, ImageDraw, ImageFont input_dir Path(photos) output_dir Path(compressed) output_dir.mkdir(exist_okTrue) for path in input_dir.glob(*.jpg): img Image.open(path) img.thumbnail((1280, 1280)) # 最长边不超过1280 # 加版权水印 draw ImageDraw.Draw(img) font ImageFont.truetype(Arial.ttf, 24) draw.text((20, img.height - 50), Copyright 2024, fill(255, 255, 255), fontfont) img.convert(RGB).save(output_dir / path.name, quality85)这段代码里有一个细节值得注意Image.open打开的是同一个Path对象处理完成后文件可能还被Pillow持有句柄如果后续要删除原文件需要先img.close()或者让img对象离开作用域否则在Windows上会报文件被占用。批量处理时还容易遇到一个跟手机有关的坑手机拍摄的JPEG图片通常带有EXIF方向信息。在电脑上打开是正的但在某些不读取EXIF的软件里就是横着的。解决办法是使用ImageOps.exif_transposefrom PIL import ImageOps img Image.open(phone_photo.jpg) img ImageOps.exif_transpose(img) # 自动按照EXIF方向修正这个函数会读取EXIF中的方向标记并实际旋转像素处理完后保存的图片在任何软件里都是正的。我做批量图片压缩时每次都会先执行这一步算是经验之谈。4. 进阶主题内存操作与实用技巧4.1 在内存中处理BytesIO实战很多场景下图片不是存在硬盘上的而是从网络抓下来或者从数据库读取出来的二进制数据。这时候不用先存成临时文件直接用BytesIO在内存中处理。import requests from io import BytesIO from PIL import Image resp requests.get(https://example.com/image.jpg) img Image.open(BytesIO(resp.content)) # 处理完后直接保存成新文件 img.convert(RGB).save(downloaded.jpg, quality90)反过来也可以把处理好的图片保存到BytesIO在上传云存储或传给第三方接口时非常方便from io import BytesIO buffer BytesIO() img.save(buffer, formatJPEG, quality85) data buffer.getvalue() # 二进制的JPEG数据 # 可以直接作为 requests.post 的 files 参数 files {file: (photo.jpg, data, image/jpeg)}这种方式比先落盘再读取少了一次磁盘IO在高并发场景下能节省大量时间。我在爬虫项目里抓图、做图片压缩接口时都用的这套逻辑。有一点需要注意BytesIO用完最好调用buffer.close()或者用with语句管理否则会一直占用内存。另外在从BytesIO打开Image后如果原始BytesIO对象被垃圾回收Image对象仍然能正常操作不用担心。4.2 图片加文字与中文乱码用Pillow在图片上写字是高频需求比如加水印、评分卡、海报文案。核心API是ImageDraw.text但坑很多最大的坑就是中文字体。from PIL import Image, ImageDraw, ImageFont img Image.open(bg.jpg) draw ImageDraw.Draw(img) # 指定字体文件路径否则默认字体不支持中文 font ImageFont.truetype(PingFang.ttc, 36) draw.text((50, 50), 你好世界, fill(255, 255, 255), fontfont)Pillow自带一个默认位图字体不支持中文直接写中文会变成一堆方框。解决方法是ImageFont.truetype()指定一个支持中文的字体文件。不同系统的字体路径不同WindowsC:\Windows\Fonts\msyh.ttc微软雅黑或simhei.ttf黑体macOS/System/Library/Fonts/PingFang.ttc或/System/Library/Fonts/STHeiti Light.ttcLinux/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc文泉驿正黑我建议在代码里做一次字体路径探测或者干脆把字体文件复制到项目目录里统一引用这样部署到服务器之后不会因为字体缺失而出问题。设置文字位置时如果想让文字居中需要先获取文字宽度和高度bbox draw.textbbox((0, 0), 沙雕水印, fontfont) text_w bbox[2] - bbox[0] text_h bbox[3] - bbox[1] # 把文字放在图片中间 x (img.width - text_w) // 2 y (img.height - text_h) // 2textbbox返回的是文字实际占据的边界框比用textlength只算宽度更准确。文字垂直居中时特别注意中文的ascent和descent差异直接把字体高度除以2往往偏上。4.3 绘制图形与制作圆角图片ImageDraw不仅能写字还能画矩形、线条、椭圆、多边形做简单的图像标注工具或者给图片加边框都靠它。from PIL import Image, ImageDraw draw ImageDraw.Draw(img) draw.rectangle([50, 50, 200, 150], outline(255, 0, 0), width3) draw.line([(0, 0), (img.width, img.height)], fill(0, 255, 0), width2) draw.ellipse([100, 100, 300, 300], fill(0, 0, 255), outline(255, 255, 255))图形绘制有个小技巧先画在一个透明图层上再叠加到原图这样方便统一调整透明度或撤销操作。圆角图片是很多UI场景的需求。Pillow没有直接提供圆角函数但可以用mask加paste实现from PIL import Image, ImageDraw def rounded_corner_img(img, radius30): mask Image.new(L, img.size, 0) draw ImageDraw.Draw(mask) draw.rounded_rectangle([0, 0, img.width, img.height], radiusradius, fill255) output Image.new(RGBA, img.size, (0, 0, 0, 0)) output.paste(img, (0, 0), mask) return output这段代码先创建一个灰度mask圆角内填充白色圆角外是黑色然后用mask作为透明通道把原图贴到透明画布上四个角就自然变成透明了。用来生成头像、卡片、弹窗背景都很好用。4.4 GIF动图处理与图像序列GIF是Pillow进阶功能里比较有意思的一块。它可以读取GIF的每一帧逐帧处理再保存成一个新动图。比如给GIF加水印、降低帧率、抽帧。from PIL import Image, ImageSequence img Image.open(animation.gif) frames [] for frame in ImageSequence.Iterator(img): # 把每一帧都转成RGBA方便处理 frame_rgba frame.convert(RGBA) frames.append(frame_rgba) # 保存成新的GIF frames[0].save( output.gif, save_allTrue, append_imagesframes[1:], duration100, # 每帧显示间隔单位毫秒 loop0 # 0表示无限循环 )保存GIF时有两个坑。第一个是颜色数量控制GIF最多256色如果原图颜色太丰富保存时会自动抖动处理图片质量会下降。第二个是duration参数如果设置太小动画会飞速播放设置太大又变得卡顿一般100到200毫秒比较自然。另外GIF的透明处理比较麻烦某些帧可能不带Alpha通道转成RGBA时要注意。处理完再转回P模式保存时透明区域可能变成黑色这时候需要手动指定透明颜色frames[0].save(output.gif, save_allTrue, append_imagesframes[1:], transparency0)看GIF帧数可以用img.n_frames在拆分GIF做逐帧分析时能省不少事。4.5 处理超大图片的内存控制Pillow在读取超大图时内存占用会很高。比如一张6000x4000的RGB照片解码后大约需要72MB内存这还没算后续处理过程中的临时拷贝。如果图片更大或者运行环境内存本来就紧张就需要做一些控制。好在Image.open是惰性的不会立刻加载全部像素。但一旦调用resize、filter等操作Pillow就必须把像素解码到内存中。一个实用的优化思路是draft()方法img Image.open(huge.jpg) img.draft(RGB, (1024, 1024)) # 告诉Pillow只需要解码到大约1024x1024尺寸draft()会尽量只解码需要的数据对JPEG这种有渐进解码支持的格式效果比较明显。但要注意draft()不一定生效需要看具体格式而且必须在加载像素之前调用。另一个更普遍的做法是先看宽高再决定处理策略如果图片太大先降采样再处理。比如先按比例缩到宽2000以内再做滤镜操作处理速度和内存占用都会好很多。Pillow的resize在缩小图片时默认使用LANCZOS重采样质量比较高但速度稍慢简单缩略图用NEAREST或BILINEAR更快。如果遇到比内存还大的极端场景比如几亿像素的遥感影像Pillow就不太适合了那需要用imageio、tifffile、或者像GDAL这样的专业库。Pillow适合处理“日常大图”不适合处理“超大规模遥感数据”。5. 常见问题与排查技巧5.1 高频报错与解决方案Pillow用多了之后会发现报错基本就围绕那么几个原因。我把这几年最常见的问题整理成了表格报错信息出现原因解决办法OSError: cannot identify image file文件不是有效图片或者格式扩展名与实际内容不符用十六进制查看文件头确认真实格式Cannot write mode RGBA as JPEGRGBA图直接保存为JPEG先convert(RGB)再保存OSError: image file is truncated图片文件不完整或被截断ImageFile.LOAD_TRUNCATED_IMAGES True或重新下载文件DecompressionBombError图片尺寸超过Pillow默认安全阈值调大Image.MAX_IMAGE_PIXELS或分批处理UnicodeDecodeError中文文件名或路径编码问题用pathlib.Path处理路径不做字符串拼接AttributeError: module PIL has no attribute Image导入方式错误或安装包名冲突用from PIL import Image不要用import PIL其中image file is truncated在爬虫场景最常见。从网上抓下来的图片经常因为网络原因只下载了一半Pillow打开就会报这个错。最简单的处理方式是在打开前检查文件大小只有一个很小的值比如几百字节就直接判为无效文件不进入后续流程。如果偏要处理不完整的JPEG可以加这句from PIL import ImageFile ImageFile.LOAD_TRUNCATED_IMAGES True但这样只能修复部分文件还可能出现像素残缺。做正规项目时我一般不建议这么干宁可把坏图丢掉。5.2 性能优化与批量加速Pillow处理速度的问题归根结底是两个像素级操作太慢、大量小图操作太慢。像素级操作慢解决方案是转NumPy。前面提到过np.array(img)和Image.fromarray(arr)这对组合。举例来说你要给所有RGB像素做一次阈值分割用getpixel循环可能要几秒用NumPy直接向量化运算连一毫秒都用不到。大量小图操作慢要考虑并发。Pillow在处理图片时某些操作会释放GIL比如编解码部分但纯Python层面的处理还是受GIL限制。如果面对几百张甚至上千张图片最简单的方式是用multiprocessing或者concurrent.futures.ProcessPoolExecutor把不同图片分给不同进程处理。from concurrent.futures import ProcessPoolExecutor from pathlib import Path from PIL import Image def compress_one(path: Path): img Image.open(path) img.convert(RGB).save(path.with_suffix(.compressed.jpg), quality80) return path.name if __name__ __main__: paths list(Path(photos).glob(*.jpg)) with ProcessPoolExecutor(max_workers4) as pool: result list(pool.map(compress_one, paths))我这里用ProcessPoolExecutor而不是ThreadPoolExecutor是因为在Windows上Pillow的多线程提升有限而多进程可以完美利用多核CPU。注意进程池代码要放在if __name__ __main__里面否则会报递归错误。还有一个隐蔽的性能问题Image.open时不加载像素但如果调用img.size、img.mode这些属性也会触发文件头的读取。如果同一个文件被反复open可以考虑把img.copy()缓存起来用。不过大多数场景下这不是瓶颈真正耗时的大头是编解码和滤镜操作。5.3 实战中容易被忽略的几个细节最后分享几个我每次写Pillow代码都会注意的细节都是血泪教训换来的。第一个细节处理完图片要关文件。Image.open返回的对象持有文件句柄在Windows上如果不显式关闭后面删除、重命名文件都会报错。最好的做法是用with语句with Image.open(photo.jpg) as img: img.thumbnail((800, 800)) img.save(thumb.jpg)Image对象支持上下文管理器退出时自动关闭。第二个细节在Web后端保存用户上传的图片时不要直接信任文件扩展名。攻击者可以上传一个恶意文件伪装成jpg如果服务器直接处理轻则保存失败重则可能造成安全问题。安全做法是用Pillow重新打开一次再保存让Pillow重新编码图片顺带去掉原图里可能隐藏的恶意代码。from PIL import Image img Image.open(upload_file) # 验证能否被Pillow识别 img.load() img.thumbnail((2000, 2000)) # 顺便限制尺寸 # 然后保存成自己指定的文件名和格式这样处理后输出的一定是标准的JPEG或PNG文件。第三个细节给图片做压缩或尺寸调整时最好先做一次色彩模式标准化。很多手机拍的图片是RGB但某些截图工具生成的是P模式或CMYK统一转成RGB再处理可以避免很多隐性问题。第四个细节保存文件名不要用中文或者至少要处理好编码。在Linux服务器上中文文件名问题不大但在Windows上和某些云存储SDK里容易翻车。我习惯用时间戳加UUID的方式生成文件名安全又省事。写在最后说回Pillow这个库它在我做过的项目里出现的频率比OpenCV高得多。爬虫抓图要归它管后端接口处理上传图要归它管制作数据集要归它管连给博客图片压缩体积这种事我也习惯写个几行的脚本交给它。它的API简单到不需要翻文档但真要抠起来又能在细节里挖出不少门道。我个人在实际使用中的体会是Pillow不是一门“需要专门学习”的技术而是那种“用到的时候自然就学会了”的工具。如果非要给一个学习路径的建议我建议你把本文第二、第三部分的示例代码全部手敲一遍然后找一个真实的批量处理场景练手——比如把自己手机里的两千张照片批量压缩一遍再给它们加上日期水印。这一趟下来Pillow的八成功能你都实操过了。最后再分享一个小技巧遇到Pillow不知道怎么写的功能不急着查官方文档先在交互式环境里用help(Image)看一遍所有属性和方法很多灵感和解决方案就是这么冒出来的。工具的价值从来不在于功能列表有多长而在于你真正需要的时候正好知道用它的哪一招。Pillow就是这样一把趁手的瑞士军刀希望你也能用好它。