Pillow图像处理实战:从基础操作到批量处理全攻略
2026/9/24 21:16:36 网站建设 项目流程

1. 为什么是Pillow:图像处理入门的瑞士军刀

1.1 Pillow到底解决了什么问题

做Python开发的人,只要跟图片打过交道,几乎都用过Pillow。这个库在Python生态里的地位,相当于美工手里的Photoshop、厨师手里的菜刀——不是它功能最强大,而是它开箱即用、覆盖面广,日常能碰到的图像处理需求,它基本都能接住。

Pillow的前身是PIL(Python 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的亮度值1
RGB真彩色,红绿蓝三通道3
RGBARGB加Alpha透明通道4
CMYK印刷四色模式4
P调色板模式,像素值映射到颜色表1
YCbCr视频、JPEG常用的颜色空间3

不同模式之间可以直接转换,比如img.convert('RGB')可以把RGBA图转成RGB,把P模式转成RGB。后续做JPEG保存时,RGBA必须转RGB才能保存。

第二个概念是“坐标系”。Pillow的坐标系原点在图片左上角,x轴向右递增,y轴向下递增,单位是像素。比如一张宽800、高600的图,左上角是(0, 0),右下角是(799, 599)。所有裁剪、绘制、像素访问都基于这个坐标系,这个概念不搞清楚,裁剪出来的图永远歪。

第三个概念是“像素访问”。最直接的方式是用getpixelputpixel

img = 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', quality=85, optimize=True)

JPEG有损压缩的quality参数控制质量,范围1-95,默认75。日常压缩图片时我一般用85,既保留了不错的视觉质量,体积也能压缩到比较理想的程度。optimize=True会额外做一次优化,让文件更小一点,但保存时间会加长,适合离线批量处理。

格式转换是Pillow最常见也最简单的操作。比如把PNG转成JPEG:

png_img = Image.open('logo.png') rgb_img = png_img.convert('RGB') rgb_img.save('logo.jpg', quality=90)

这里有一个经典坑:PNG可能带透明通道,直接save成JPEG会报错,必须先用convert('RGB')把透明通道去掉,否则保存时会出现类似于cannot write mode RGBA as JPEG的报错。

保存WebP格式也很常用,WebP比JPEG体积更小,适合网页使用:

img.save('output.webp', 'WEBP', quality=80)

注意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, expand=True, fillcolor=(255, 255, 255))

不加expand=True时,旋转后的图片尺寸不变,内容会被裁切掉;加了之后,画布会自动扩展,完整保留旋转后的内容,露出的背景可以用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 合成与透明:抠图拼图实战

图像合成包括blendcompositepaste三个方法,它们的区别值得好好说说。

blend要求两张图尺寸完全一致,直接按权重混合:

img1 = Image.open('bg.jpg') img2 = Image.open('fg.jpg') blended = Image.blend(img1, img2, alpha=0.3)

alpha取0到1之间,0表示完全显示img1,1表示完全显示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_ok=True) 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), font=font) img.convert('RGB').save(output_dir / path.name, quality=85)

这段代码里有一个细节值得注意:Image.open打开的是同一个Path对象,处理完成后文件可能还被Pillow持有句柄,如果后续要删除原文件,需要先img.close()或者让img对象离开作用域,否则在Windows上会报文件被占用。

批量处理时还容易遇到一个跟手机有关的坑:手机拍摄的JPEG图片通常带有EXIF方向信息。在电脑上打开是正的,但在某些不读取EXIF的软件里就是横着的。解决办法是使用ImageOps.exif_transpose

from 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', quality=90)

反过来,也可以把处理好的图片保存到BytesIO,在上传云存储或传给第三方接口时非常方便:

from io import BytesIO buffer = BytesIO() img.save(buffer, format='JPEG', quality=85) 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), font=font)

Pillow自带一个默认位图字体,不支持中文,直接写中文会变成一堆方框。解决方法是ImageFont.truetype()指定一个支持中文的字体文件。不同系统的字体路径不同:

  • Windows:C:\Windows\Fonts\msyh.ttc(微软雅黑)或simhei.ttf(黑体)
  • macOS:/System/Library/Fonts/PingFang.ttc/System/Library/Fonts/STHeiti Light.ttc
  • Linux:/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc(文泉驿正黑)

我建议在代码里做一次字体路径探测,或者干脆把字体文件复制到项目目录里统一引用,这样部署到服务器之后不会因为字体缺失而出问题。

设置文字位置时,如果想让文字居中,需要先获取文字宽度和高度:

bbox = draw.textbbox((0, 0), '沙雕水印', font=font) text_w = bbox[2] - bbox[0] text_h = bbox[3] - bbox[1] # 把文字放在图片中间 x = (img.width - text_w) // 2 y = (img.height - text_h) // 2

textbbox返回的是文字实际占据的边界框,比用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), width=3) draw.line([(0, 0), (img.width, img.height)], fill=(0, 255, 0), width=2) draw.ellipse([100, 100, 300, 300], fill=(0, 0, 255), outline=(255, 255, 255))

图形绘制有个小技巧:先画在一个透明图层上,再叠加到原图,这样方便统一调整透明度或撤销操作。

圆角图片是很多UI场景的需求。Pillow没有直接提供圆角函数,但可以用maskpaste实现:

from PIL import Image, ImageDraw def rounded_corner_img(img, radius=30): mask = Image.new('L', img.size, 0) draw = ImageDraw.Draw(mask) draw.rounded_rectangle([0, 0, img.width, img.height], radius=radius, fill=255) 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_all=True, append_images=frames[1:], duration=100, # 每帧显示间隔,单位毫秒 loop=0 # 0表示无限循环 )

保存GIF时有两个坑。第一个是颜色数量控制,GIF最多256色,如果原图颜色太丰富,保存时会自动抖动处理,图片质量会下降。第二个是duration参数,如果设置太小,动画会飞速播放;设置太大,又变得卡顿,一般100到200毫秒比较自然。

另外GIF的透明处理比较麻烦,某些帧可能不带Alpha通道,转成RGBA时要注意。处理完再转回P模式保存时,透明区域可能变成黑色,这时候需要手动指定透明颜色:

frames[0].save('output.gif', save_all=True, append_images=frames[1:], transparency=0)

看GIF帧数可以用img.n_frames,在拆分GIF做逐帧分析时能省不少事。

4.5 处理超大图片的内存控制

Pillow在读取超大图时内存占用会很高。比如一张6000x4000的RGB照片,解码后大约需要72MB内存,这还没算后续处理过程中的临时拷贝。如果图片更大,或者运行环境内存本来就紧张,就需要做一些控制。

好在Image.open是惰性的,不会立刻加载全部像素。但一旦调用resizefilter等操作,Pillow就必须把像素解码到内存中。一个实用的优化思路是draft()方法:

img = Image.open('huge.jpg') img.draft('RGB', (1024, 1024)) # 告诉Pillow只需要解码到大约1024x1024尺寸

draft()会尽量只解码需要的数据,对JPEG这种有渐进解码支持的格式效果比较明显。但要注意,draft()不一定生效,需要看具体格式,而且必须在加载像素之前调用。

另一个更普遍的做法是先看宽高再决定处理策略:如果图片太大,先降采样再处理。比如先按比例缩到宽2000以内再做滤镜操作,处理速度和内存占用都会好很多。Pillow的resize在缩小图片时默认使用LANCZOS重采样,质量比较高,但速度稍慢;简单缩略图用NEARESTBILINEAR更快。

如果遇到比内存还大的极端场景,比如几亿像素的遥感影像,Pillow就不太适合了,那需要用imageiotifffile、或者像GDAL这样的专业库。Pillow适合处理“日常大图”,不适合处理“超大规模遥感数据”。

5. 常见问题与排查技巧

5.1 高频报错与解决方案

Pillow用多了之后,会发现报错基本就围绕那么几个原因。我把这几年最常见的问题整理成了表格:

报错信息出现原因解决办法
OSError: cannot identify image file文件不是有效图片,或者格式扩展名与实际内容不符用十六进制查看文件头,确认真实格式
Cannot write mode RGBA as JPEGRGBA图直接保存为JPEGconvert('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'), quality=80) return path.name if __name__ == '__main__': paths = list(Path('photos').glob('*.jpg')) with ProcessPoolExecutor(max_workers=4) as pool: result = list(pool.map(compress_one, paths))

我这里用ProcessPoolExecutor而不是ThreadPoolExecutor,是因为在Windows上Pillow的多线程提升有限,而多进程可以完美利用多核CPU。注意进程池代码要放在if __name__ == '__main__'里面,否则会报递归错误。

还有一个隐蔽的性能问题:Image.open时不加载像素,但如果调用img.sizeimg.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就是这样一把趁手的瑞士军刀,希望你也能用好它。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询