1. 从一张图片开始:Pillow为什么值得认真学
做计算机图形学相关的东西,绕不开图像处理。无论是做纹理贴图、图像滤波、边缘检测,还是做最基础的图片缩放、裁剪、格式转换,几乎每个项目的第一步都是"把图片读进来、改一改、再存出去"。Python生态里负责这件事的主力库就是PIL/Pillow,一套历史非常悠久、API相当稳定、上手成本极低的图像处理工具集。
先说清楚一件事:PIL是原版Python Imaging Library,后来因为开发停滞,社区fork出了一个持续维护的版本,也就是Pillow。现在你装Pillow,import的时候写的仍然是from PIL import ...,因为Pillow继承了原版包名。这个细节坑过不少刚入门的同学——在搜索引擎里查资料,老帖子讲PIL,新文档讲Pillow,其实对应的是同一个东西。
这篇文章适合谁?如果你正在学计算机图形学、做图像处理相关的作业或小项目,或者你只是手里攒了一堆图片想做批处理,都可以读下去。我会从最基础的图像数据结构开始,讲到像素级操作、滤波、几何变换、文字绘制和批量处理,最后再聊聊我实际踩过的坑。全文围绕一个原则:每个操作除了知道怎么写,更要知道为什么这么写。
为什么Pillow值得认真学?因为它是纯Python图像处理的基石。像OpenCV、scikit-image这些更重型的库,底层用C/C++实现,功能固然强大,但很多核心概念在Pillow里反而更容易理解。你手动遍历一遍像素、写一个简单的卷积核,胜过调用一百次现成的filter函数。图形学的底层直觉,就是从这些像素操作里长出来的。
2. 图像在计算机里到底是什么
2.1 一张图片的"最小单位"不是点,是像素
很多人一提到图像就想到"分辨率",比如1920×1080,但真正理解像素是什么的人其实不多。简单说,一张图片就是一块二维数组,数组里的每个元素是一个像素,每个像素记录着这个位置的颜色信息。灰度图是二维的,每个像素只有一个值(亮度);彩色图是三维的,通常是宽×高×3,第三个维度是RGB三个通道。
Pillow里用Image.open()读入图片后,你可以直接通过.size拿到宽高,通过.mode拿到颜色模式。我习惯在一开始就把这两个属性打印出来,因为后面的许多操作(比如创建新图、遍历像素)都依赖它们。
from PIL import Image img = Image.open("demo.jpg") print(img.size) # (1920, 1080) print(img.mode) # RGB这一步看似简单,但它是整个图像处理链路的起点。很多初学者直接对图片做操作,报错了才回头看尺寸和模式,结果浪费了大量时间。养成先探查再动手的习惯,后面能省很多事。
2.2 常见颜色模式:RGB、RGBA、L、P
Pillow支持很多颜色模式,但日常用到的就那几种:
- RGB:三通道彩色图,每个通道0~255,最常见。
- RGBA:比RGB多一个Alpha通道,表示透明度。做图片合成时必须用RGBA,否则透明区域会变成黑色。
- L:灰度图,单通道,0为纯黑,255为纯白。做边缘检测、形态学处理前经常先转成L模式。
- P:调色板模式,像素值是一个索引,指向一个256色的调色板。GIF就是用这种模式。平时很少直接用,但转GIF时会遇到。
模式之间可以转换,用.convert()方法。比如彩色图转灰度:img.convert("L")。这里面有个细节:不是所有模式都能无损互转。RGB转RGBA可以直接加一个全不透明白色通道,但RGBA转RGB会把透明区域合成到背景色上,合成方式不同结果也不一样。默认是直接丢弃Alpha,我建议手动指定背景色再合成,后面会讲。
2.3 像素级读写:一图胜千言的基础操作
拿到图像对象之后,最底层的操作无非两种:读某个像素的颜色,写某个像素的颜色。Pillow里最简单的方式是getpixel()和putpixel(),但逐像素调用这两个方法性能很差,尤其是处理大图时,慢到怀疑人生。
更高效的方式是把整个图像转换成Python列表或NumPy数组。纯Pillow场景下,可以用list(img.getdata())拿到所有像素,注意得到的是一个一维列表,需要自己reshape成宽×高。更推荐的方式是结合NumPy:
import numpy as np from PIL import Image img = Image.open("demo.jpg") arr = np.array(img) # shape: (height, width, 3)做了转数组之后,图像就变成普通的NumPy数组了,你可以用任何数组操作去改它。改完之后再用Image.fromarray(arr)转回Pillow图像对象。这条链路是我平时用得最多的,既保留了Pillow的读写格式能力,又拿到了NumPy的高效计算能力。可以说,Pillow负责"读图和存图",NumPy负责"算图",两者配合才是完整的工作流。
如果你不想引入NumPy,只想用纯Pillow做像素级操作,效率最高的API是img.load()返回的像素访问对象:
pixels = img.load() for y in range(img.height): for x in range(img.width): r, g, b = pixels[x, y] pixels[x, y] = (b, g, r) # 交换通道,制造诡异效果注意pixels[x, y]的索引顺序:第一个下标是x(列),第二个是y(行),这和NumPy数组的arr[y, x]刚好相反。这个坑我至少踩过三次,写循环之前一定想清楚坐标系。
3. 核心功能拆解:从几何变换到像素操纵
3.1 缩放、裁剪、旋转:理解插值和坐标系
几何变换是图像处理里最常用的操作。Pillow提供resize()、crop()、rotate()、transpose()等方法,API本身不复杂,复杂的是背后的概念。
先说resize()。它有两个参数:新尺寸和重采样滤波器。滤波器决定了缩放时如何从原像素插值出新像素。Pillow里常用的有Image.NEAREST(最近邻)、Image.BILINEAR(双线性)、Image.BICUBIC(双三次)、Image.LANCZOS(兰佐斯,质量最高但最慢)。
img.resize((800, 600), Image.LANCZOS)那缩放图片呢,缩小信息和放大信息情况不一样。缩小图片时,应该先做低通滤波(抗锯齿),否则会出现摩尔纹;放大图片时,插值算法决定了边缘的平滑程度。LANCZOS在大多数场景下视觉效果最好,但如果是像素艺术风格的图,用NEAREST反而更合适,因为双线性插值会把硬边糊掉。插值算法的选择本质上是"平滑度"和"锐度"之间的权衡。
再说crop()。它接收一个四元组(left, upper, right, lower),表示左、上、右、下的坐标。注意right和lower是开区间,即不包含边界。一个常见误区:想裁剪一个右下角坐标为(100, 100)的区域,很多人写成(0, 0, 100, 100),结果宽高各少了1像素。原因就是坐标体系从0开始,right=100意味着x取值0~99,宽度其实是100而不是101。恰好很多图形学变换里坐标都是左闭右开,习惯了就好。
rotate()默认逆时针旋转,还会扩展画布以适应旋转后的图像,因此输出尺寸可能会变大,背景用黑色填充。如果想保持尺寸不变,传expand=False(默认就是这个)。旋转角度不是90°倍数时,边缘像素需要插值,同样选滤波器。
3.2 颜色通道操作:拆分、合并、直方图分析
图像的RGB通道可以拆开单独处理,这在做颜色校正、通道交换、伪彩色渲染时非常有用。Pillow的split()方法把图像拆成多个单通道图像,merge()再把它们合回去。
r, g, b = img.split() # 交换R和B通道 img_swapped = Image.merge("RGB", (b, g, r))单通道图像本身也是Image对象,可以单独做滤波或增强。比如想把某个通道整体调亮,可以用point()方法做逐像素映射。这个操作比用循环快得多,因为它内部是C实现的:
# 把红色通道整体提高30个单位(并限制在255以内) r_enhanced = r.point(lambda i: min(i + 30, 255))直方图分析是理解图像颜色分布的核心手段。Image.histogram()返回一个长度为256×通道数的列表,前256个是R通道的统计,然后是G、B。图像偏亮还是偏暗、对比度高低、是否有色偏,看直方图一眼就能判断。做自动白平衡或者对比度拉伸时,第一步都是先算直方图,找到最小和最大亮度值,再映射到0~255的完整区间。
3.3 滤波与卷积:最常见的邻域操作
图像滤波在计算机图形学里几乎是必修课。均值模糊、高斯模糊、边缘检测、锐化,本质上都是卷积操作——用一个小的核矩阵在图像上滑动,每个像素的新值等于它周围邻域像素的加权和。
Pillow内置了ImageFilter模块,一行代码就能应用常见滤波器:
from PIL import ImageFilter img_blur = img.filter(ImageFilter.GaussianBlur(radius=2)) img_edge = img.filter(ImageFilter.FIND_EDGES) img_sharp = img.filter(ImageFilter.SHARPEN)内置滤波器用起来很方便,但如果你理解了原理,完全可以手动实现一个3×3的卷积核,然后通过ImageFilter.Kernel()传入自定义核。比如一个经典的锐化核:
0 -1 0 -1 5 -1 0 -1 0写成代码:
kernel = ImageFilter.Kernel(size=(3, 3), kernel=(0, -1, 0, -1, 5, -1, 0, -1, 0), scale=1) img_sharp_custom = img.filter(kernel)scale参数决定卷积结果除以多少,通常取核元素之和。如果和接近0(比如边缘检测核),要特别小心,因为除以0会导致全图溢出。这一点我在做自定义核的时候栽过跟头,输出图像全白,排查半天才发现是scale没设对。
3.4 图像合成:Alpha混合与遮罩
合成是图像处理里非常高频的需求,比如给图片加水印、把人物抠出来贴到另一张背景上、做渐变叠加。Pillow的Image.blend()做线性混合,Image.composite()做带遮罩的合成。
# 两张图按alpha=0.5的比例混合 blended = Image.blend(img1, img2, alpha=0.5) # 使用mask做区域选择性合成 composited = Image.composite(img1, img2, mask)这里最需要注意的一点:参与合图的图像尺寸、模式必须一致,否则直接报错。我自己经常在合成前加一行断言:
assert img1.size == img2.size == mask.size assert img1.mode == img2.mode == mask.mode很多教程不会提这个细节,但实际项目里90%的合成bug都出在尺寸或模式不一致上。先花一秒钟统一图像属性,能省掉后面半小时的排查。
4. 用Pillow做图形学小实验:一个完整的实操案例
4.1 案例目标:给灰度图添加伪彩色映射
灰度转伪彩色是可视化领域最常见的操作之一,医学影像、热力图、高度图都会用到。核心思路很简单:把灰度值0~255映射到RGB空间的三组颜色插值上。常见做法是分段线性插值,或者直接查一个预设的调色板(colormap)。
下面的代码演示如何用NumPy+Pillow实现一个最简单的"热度图"映射:低灰度用蓝色,中灰度用绿色,高灰度用红色。
import numpy as np from PIL import Image def grayscale_to_heatmap(img_gray): arr = np.array(img_gray.convert("L")).astype(np.float32) # 归一化到0~1 arr = arr / 255.0 h, w = arr.shape rgb = np.zeros((h, w, 3), dtype=np.uint8) # R通道:高值亮,低值暗 rgb[..., 0] = np.clip(arr * 3, 0, 1) * 255 # G通道:中间亮,两头暗(三角波) rgb[..., 1] = np.clip(1 - np.abs(arr * 2 - 1), 0, 1) * 255 # B通道:低值亮,高值暗 rgb[..., 2] = np.clip(1 - arr * 3, 0, 1) * 255 return Image.fromarray(rgb) img = Image.open("grayscale_landscape.jpg") heatmap = grayscale_to_heatmap(img) heatmap.save("heatmap_result.png")这里我用三角波函数生成绿色通道,让它在中灰度附近最亮,两侧渐暗,形成蓝→绿→红的视觉渐变。np.clip非常重要,因为乘3之后的数值可能超过1,直接转uint8会数据溢出,产生不可预测的噪点。
4.2 在图像上绘制文字和几何图形
Pillow自带ImageDraw模块,可以在图像上画点、线、矩形、椭圆、多边形和文字。这在做标注、添加水印、生成图表时非常常用。
from PIL import ImageDraw, ImageFont draw = ImageDraw.Draw(img) # 画一个半透明遮罩 overlay = Image.new("RGBA", img.size, (0, 0, 0, 0)) draw_overlay = ImageDraw.Draw(overlay) draw_overlay.rectangle([50, 50, 400, 200], fill=(255, 0, 0, 128)) img = Image.alpha_composite(img.convert("RGBA"), overlay) # 绘制文字 font = ImageFont.truetype("arial.ttf", 48) draw = ImageDraw.Draw(img) draw.text((60, 70), "Demo", font=font, fill=(255, 255, 255))有两个容易踩的坑:
- text()的坐标是文字左上角锚点,不是基线。要居中放置的话,需要先用
draw.textlength()或font.getbbox()量出文字尺寸,再根据画布尺寸反推坐标。 - JPEG不支持Alpha通道。如果你用
RGBA模式的图像直接保存为JPG,会报错或者丢失透明信息。最好先img.convert("RGB")再保存。
第二个坑我印象特别深,水印做得好好的,保存时突然报OSError: cannot write mode RGBA as JPEG。解决方案就是显式转成RGB模式,丢弃Alpha通道。如果是半透明水印想保留在JPG里,就必须先合成到背景上。
4.3 批量处理:给一整个文件夹的图片加水印
写单个图片的脚本很简单,真正考验代码组织能力的是批量处理。我一般会用一个函数封装单张图片的操作,再用一个循环遍历目录,最后统一保存,这样逻辑清晰,出错也容易定位。
import os from PIL import Image, ImageDraw, ImageFont INPUT_DIR = "photos" OUTPUT_DIR = "watermarked" WATERMARK_TEXT = "Demo Watermark" os.makedirs(OUTPUT_DIR, exist_ok=True) def add_watermark(filepath, output_path): img = Image.open(filepath).convert("RGBA") width, height = img.size # 创建透明水印层 watermark = Image.new("RGBA", img.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(watermark) font = ImageFont.truetype("arial.ttf", max(16, width // 20)) # 先测量文字尺寸,再定位到右下角 bbox = draw.textbbox((0, 0), WATERMARK_TEXT, font=font) text_w = bbox[2] - bbox[0] text_h = bbox[3] - bbox[1] x = width - text_w - 20 y = height - text_h - 20 draw.text((x, y), WATERMARK_TEXT, font=font, fill=(255, 255, 255, 160)) # 合成 result = Image.alpha_composite(img, watermark).convert("RGB") result.save(output_path, quality=90) for filename in os.listdir(INPUT_DIR): if filename.lower().endswith((".jpg", ".jpeg", ".png")): input_path = os.path.join(INPUT_DIR, filename) output_path = os.path.join(OUTPUT_DIR, os.path.splitext(filename)[0] + "_wm.jpg") add_watermark(input_path, output_path) print(f"已处理 {filename}")批量处理的核心心得:每一步都做一次状态检查,而不是最后一次性看结果。比如处理到第50张图时,应该打印一下当前文件信息和输出尺寸,确认没跑偏。我的习惯是在函数里加入一些防御性断言,一旦数据异常立刻终止,而不是带着错误处理完所有图片。因为错误会在第80张才集中爆发,到时候想定位哪一张因为什么原因出的错,会非常痛苦。
4.4 格式转换与压缩:理解JPEG、PNG、WebP的差异
Pillow支持几十种图片格式,但日常用到最多的是JPEG、PNG、WebP和GIF。每种格式的适用场景差异很大:
- JPEG:有损压缩,适合照片类图像,文件小,但不支持透明通道。
- PNG:无损压缩,支持透明,适合截图、图标、图形类图像,文件较大。
- WebP:谷歌推出的格式,有损和损都可,支持透明,压缩率比JPEG更好,但老设备兼容性一般。
- GIF:支持动画,但只有256色,适合简单动画和低色彩需求的图形。
转换格式只要换一下保存后缀即可:
img = Image.open("photo.png") img.save("photo.jpg", quality=85, optimize=True)quality参数是JPEG特有的,范围0~100,数值越大质量越高文件越大。optimize=True会做额外优化,文件更小但保存更慢。批量压缩照片的时候,我通常会先用quality=85跑一遍,肉眼基本看不出区别,文件却可能缩小一半。如果有透明通道又想存JPEG,务必先合并到背景色,否则透明区域直接变黑。
GIF动画的创建稍微复杂点,但也是Pillow的经典应用:
frames = [img1, img2, img3] # 每张图尺寸一致 frames[0].save("animation.gif", save_all=True, append_images=frames[1:], duration=200, loop=0)duration单位是毫秒,表示每帧停留时间;loop=0表示无限循环。制作GIF前一定要统一所有帧的尺寸和模式,否则保存出来的动画会花屏或者严重变形。
5. 工具选型:Pillow vs OpenCV vs scikit-image
5.1 什么时候用Pillow,什么时候换别的
很多同学在Pillow和OpenCV之间纠结。我的观点是:先看任务是什么,再选工具。
Pillow适合的场景:图片格式转换、批量水印/裁剪/缩放、简单的滤镜处理、在图像上绘制文字和图形、制作GIF、做小规模图像预处理。它的优势是轻量、API简洁、零配置安装,而且对图像格式的支持非常全面。你不需要研究色彩空间转换矩阵,不需要理解BGR和RGB的坑,它直接给你最直观的API。
OpenCV适合的场景:实时视频处理、特征点检测与匹配、目标检测、相机标定、复杂形态学操作。它的底层是C++优化过的,性能远高于Pillow,但API更底层,学习曲线陡峭。而且OpenCV读图默认是BGR顺序,和RGB混用时稍不注意就会得到一张蓝红色偏的图,这个坑我到现在还会偶尔触发。
scikit-image则介于两者之间,偏向科学研究,提供了很多经典算法的高质量实现,比如阈值分割、边缘检测、形态学重建。它依赖scipy和NumPy,适合做算法原型验证。
一句话总结:Pillow是瑞士军刀,OpenCV是重型工具车,scikit-image是实验室仪器。日常图片处理用Pillow,性能敏感或者做视觉算法了再考虑OpenCV。
5.2 为什么Pillow适合作为图形学入门的第一选择
图形学初学者最容易犯的错是"一上来就调重型库",觉得自己用了OpenCV就算会了图像处理。但实际上,图像处理的核心概念——像素、通道、卷积、插值、坐标变换——都和具体库无关。Pillow的API设计得很薄,基本把"底层图像操作"直接暴露给用户,没有太多抽象层,反而适合用来理解本质。
拿卷积来说,OpenCV的filter2D一行搞定,但你不看源码永远不会知道卷积核是怎么应用到图像上的。用Pillow手动实现一个卷积核,写两层for循环,处理一张100×100的小图,几毫秒就能跑完。这种"慢的可感知"恰恰是学习过程中最宝贵的体验。你亲眼看到了每个像素如何影响邻域点,看到模糊是怎么回事,看到边缘检测为什么能提取轮廓,这些直觉是任何一行现成API都给不了的。
所以我的建议是:先用Pillow手写一遍这些基础操作,再去用OpenCV释放生产力。顺序不能反。
6. 实操中常见的坑与排查思路
6.1 图像保存后颜色不对
这个问题十有八九出在颜色模式转换上。常见场景:从PNG转JPG时,RGBA通道丢失导致透明区域变成黑色;或者从CMYK模式的图像直接保存为标准RGB格式导致颜色偏暗。解决办法是保存前显式convert("RGB"),不要依赖库的默认行为。
还有一种是NumPy数组和Pillow的dtype不一致导致的颜色错乱。比如NumPy数组是float64,直接Image.fromarray()会报错或者输出全黑,因为Pillow期望的是uint8。转换方式:
arr = arr.astype(np.uint8) img = Image.fromarray(arr)6.2 图片旋转后出现黑边
rotate()默认会把旋转后的区域用黑色填充,并且自动扩展画布。如果你旋转45°后不需要黑边,有两种选择:一是旋转后手动裁剪掉边缘,二是用expand=False只转不扩。但后者会导致图像角落被截掉。更平滑的做法是先将图片放大一点再旋转,最后再裁剪回目标尺寸,这样角落不会被裁掉太多信息。
实际操作中,如果只是想翻转图片(上下或左右),用transpose(Image.FLIP_LEFT_RIGHT)或transpose(Image.FLIP_TOP_BOTTOM),速度远快于rotate(180),也不会产生插值损失。
6.3 大图处理内存暴涨
Pillow加载图片时,会在内存中生成完整的像素数组。一张常见的6000×4000照片,RGB模式下就是6000×4000×3字节,约72MB。听起来不大,但如果同时加载多张图做批处理,内存很快就会吃紧。
处理大图的几个经验:
- 能不全部加载就不全部加载。如果只做格式转换,可以用
Image.open()先打开,再按需load(),Pillow默认是惰性加载的。 - 用
thumbnail()替代resize()做缩略图。前者会保持宽高比,并且只计算需要的尺寸,内存占用和计算量都更小。 - 处理完一张图立刻
del img并调用gc.collect(),尤其在一个循环里连续开图时。
还有一个实用技巧:Pillow可以读取图像的尺寸和基本信息而不加载像素数据。
with Image.open("huge_photo.jpg") as img: print(img.size, img.mode) # 此时尚未解码像素这在批量检查图片属性时非常高效,几千张图几秒钟就能扫完。
6.4 坐标翻转:为什么我的图像上下颠倒了
这个话题在图形学中尤其重要。很多图像格式的坐标系原点在左上角,y轴向下增长,而数学坐标系原点通常在左下角,y轴向上增长。如果你用NumPy做数组操作,经常会遇到图像翻转的问题。
arr = np.array(img) flipped_arr = arr[::-1] # 上下翻转 flipped_img = Image.fromarray(flipped_arr)这种翻转处理在Pillow里等效于img.transpose(Image.FLIP_TOP_BOTTOM)。明白坐标系的方向,理解为什么arr[::-1]是上下翻转而不是左右翻转,是图像处理的基本功。左右翻转是arr[:, ::-1]。如果搞混了,调出来的图永远是镜像的。
6.5 字体渲染缺字或乱码
在Linux服务器上用Pillow画中文文字,经常会遇到乱码或者文字变成方框,原因是系统没有安装相应的字体文件。ImageFont.truetype("arial.ttf", 36)冒号不会报错,但保存出来的图片里文字全是空心方块,因为找不到字体去渲染。
解决办法:先确认系统字体路径,fc-list | grep "SimHei\|Noto",然后指定完整路径:
font = ImageFont.truetype("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", 36)另外,ImageFont.truetype在Windows和Linux上的字体路径完全不同,建议用os.path.exists()先检查路径,再决定是否回退到默认字体。这种与环境强相关的问题,写的时候多一个判断,部署的时候能省一大半心。
7. 给初学者的三个练习建议
如果你刚接触图像处理,光看不练远远不够。我强烈建议按下面的顺序,用Pillow完成三个小练习,每一步都会加深你对图像本质的理解。
第一个练习:写一个"马赛克"滤镜。选定一个块大小(比如10×10像素),把每个块内的像素统一成该块的平均颜色。这需要你理解坐标分块、颜色求平均、逐像素写入这三个环节。完成后你会对"邻域操作"有直观认识。
第二个练习:手动实现一个3×3的高斯模糊核,应用在一张图片上,再和GaussianBlur(radius=1)的结果进行对比。你会发现两者结果不完全一样,因为内置实现可能做了不同的边界处理。探究这个差异的过程,比结果本身更有价值。
第三个练习:做一个"像素风"处理——先把图片缩小到比如64×64,再用最近邻插值放大回原尺寸,观察硬边效果。这能让你真正理解插值的意义:为什么NEAREST会锯齿,为什么LANCZOS会平滑。
这三个练习全部加起来,代码量不超过200行,但做完之后你对Pillow的掌握程度会有质的飞跃。
8. 最后说几句实在话
用Pillow处理图像这几年,我最深的体会是:图像处理不是靠背API学的,而是靠踩坑学的。API能教会你"怎么写",只有踩坑才能教会你"为什么不能这么写"。
比如RGBA转JPEG报错、旋转后黑边、NumPy数组dtype不对导致全黑、中文乱码、坐标左闭右开差1像素……每一个坑都是我看文档永远不会注意到的地方,但它们恰恰是真实项目里最常见的故障点。所以在这篇文章里,我没有只讲Pillow有多好用,而是把我踩过的、身边同事踩过的坑尽量都写了出来。希望你在遇到同样问题的时候,能少走几步弯路。
另外说个实用的小习惯:无论做多简单的图像处理任务,我都会在保存前把中间结果输出一份(比如加了水印的图、转完格式的图),肉眼确认效果无误后再批量处理全部文件。这个习惯救过我很多次,有时候逻辑看着对,输出却是错的。
最后再分享一个收尾技巧:Pillow的Image.open()返回的对象在文件被关闭后依然可以访问像素,但如果你在上下文管理器with外面使用它,某些操作可能会报异常。稳妥的做法是把所有需要的操作都在with块内完成,或者先用img.load()把数据加载到内存再退出上下文。代码写得多,不如代码写得稳。