本节摘要:Image 对象是 Pillow 一切操作的载体——它封装了一张图的像素数据与元信息。本节解剖 Image 对象的属性(size/mode/format/info)与常用方法(copy/convert/thumbnail/getdata/putdata),并讲清"操作返回新对象 vs 原地修改"这个关键区别。
阅读完本节,你应当能够:
Image 对象可以想象成一张画布:画布上的颜料是像素数据,画布角落贴的标签是元信息(尺寸、模式、格式)。Pillow 的一切操作,要么在改颜料(像素),要么在看标签(属性)。
from PIL import Image img = Image.open("photo.jpg") # 标签(属性) print(img.size) # (1920, 1080) 宽高像素数 print(img.mode) # RGB 颜色模式 print(img.format) # JPEG 文件格式 print(img.info) # 附加信息字典(如 EXIF、dpi) # 画布(像素数据) data = img.getdata() # 一个可迭代的像素序列 print(len(list(data))) # 像素总数 = 1920*1080
这个"画布+标签"的心智模型贯穿整个 Pillow 学习——先看标签了解情况,再动画布做处理。
| 属性 | 含义 | 示例 |
|---|---|---|
| size | 尺寸 (宽, 高) | (1920, 1080) |
| mode | 颜色模式 | RGB/RGBA/L/1/CMYK |
| format | 文件格式 | JPEG/PNG/GIF |
| info | 附加信息字典 | dpi、exif、comment |
| width / height | 单独取宽高 | 1920 / 1080 |
| palette | 调色板(P 模式) | 调色板数据 |
💡 关键直觉:mode 决定了每个像素的数据结构。RGB 每像素 3 字节(R/G/B),RGBA 每像素 4 字节(多 Alpha 透明度),L 每像素 1 字节(灰度)。想明白这个,后面的通道操作就顺了。
这是初学者最容易踩的坑——Pillow 的方法绝大多数返回新对象,少数原地修改:
# 返回新对象(原图不变) img2 = img.convert("L") # 转灰度,img 仍是 RGB img3 = img.resize((500, 500)) # 缩放,img 不变 img4 = img.crop((0, 0, 100, 100)) # 裁剪,img 不变 # 原地修改(img 本身变了) img.thumbnail((300, 300)) # 缩略图,原地改
⚠️ 常见坑:把"返回新对象"当成"原地修改",结果处理完发现原图没变。判断方法很简单——看方法有没有返回值:
img2 = img.convert(...)有返回值就是新对象;img.thumbnail(...)没有赋值就是原地改。不确定时,查文档或先 copy 一份再操作。
original = img.copy() # 完全独立的副本 # 之后怎么折腾 img 都不影响 original
copy 是浅复制还是深复制?对 Image 对象而言,copy 创建像素数据的独立副本,两个对象互不影响。在不确定操作是否原地修改时,先 copy 是稳妥习惯。
img_gray = img.convert("L") # RGB → 灰度 img_rgba = img.convert("RGBA") # 加透明通道 img_rgb = img.convert("RGB") # 去透明通道 img_1 = img.convert("1") # 二值化(黑或白)
convert 内部处理颜色映射,是跨模式操作的必经之路。比如要叠加半透明水印,先 convert("RGBA") 确保有 Alpha 通道。
# 读取全部像素 pixels = list(img.getdata()) # 每个元素依模式而定:RGB 是 (r,g,b),L 是单个数值 # 修改后写回 new_pixels = [(255 - r, 255 - g, 255 - b) for r, g, b in pixels] # 反色 img.putdata(new_pixels)
getdata/putdata 适合需要逐像素处理的场景(如反色、阈值),但速度慢——大数据量时优先用 ImageOps 或 numpy 配合(见后续章节)。
from PIL import Image with Image.open("photo.jpg") as img: # 看标签 print(f"尺寸: {img.size}, 模式: {img.mode}, 格式: {img.format}") print(f"信息: {list(img.info.keys())}") # 复制一份安全操作 work = img.copy() # 转灰度并保存 gray = work.convert("L") gray.save("gray.jpg", quality=90) # 读取像素验证 pix = list(gray.getdata()) print(f"灰度像素数: {len(pix)}, 首个像素值: {pix[0]}")
输出会让你直观看到:灰度图每个像素只是一个 0-255 的数值,RGB 图则是三元组。模式一变,像素结构就变——这是图像处理的底层规律。
img.info 里藏着很多有用信息,值得单独讲讲:
with Image.open("photo.jpg") as img: info = img.info # 常见键(不同格式键不同) if "dpi" in info: # 分辨率(打印相关) print("DPI:", info["dpi"]) if "icc_profile" in info: # 色彩配置(颜色准确性) print("带 ICC:", len(info["icc_profile"]), "字节") if "exif" in info: # 拍摄信息(相机/时间) print("带 EXIF 信息")
info 的常见用途:
| 信息 | 用途 |
|---|---|
| dpi | 打印尺寸换算、扫描分辨率 |
| icc_profile | 色彩管理(见 3.3) |
| exif | 拍摄元数据、版权信息 |
| comment | 注释文本 |
| duration(GIF) | 帧间间隔 |
⚠️ 常见坑:info 不是所有格式都有、键也不统一。读之前先
if key in info判断,别直接info["dpi"]——不存在会抛 KeyError。
除了 open,Image 对象还有三种常见创建途径:
from PIL import Image # 1. 新建空白图 blank = Image.new("RGB", (400, 300), "white") # 2. 纯色图 red = Image.new("RGB", (100, 100), (255, 0, 0)) # 3. 从像素数据创建 Image.frombytes("RGB", (2, 2), b"\xff\x00\x00" * 4) # 2x2 红色 # 4. 从 numpy 数组创建(第五章详述) import numpy as np arr = np.zeros((100, 100, 3), dtype=np.uint8) arr[..., 0] = 255 from_numpy = Image.fromarray(arr)
新建图最常用:Image.new("RGB", size, color)——生成纯色背景、拼图画布、测试图都靠它。第一章 1.4 的四宫格实验就是用它做画布的。
💡 关键直觉:Image.new 是"画布",Image.open 是"读档"。做合成、拼图、生成图时,先 new 一张画布,再把内容 paste 上去——这是 Pillow 合成类任务的标准起手式。
大图处理时,Image 对象的内存管理值得注意:
# 大图(如 8000x6000)占内存 = 宽*高*每像素字节 # RGB 8000*6000*3 ≈ 144 MB——单张就上 GB 级内存压力 # 省内存技巧: # 1. 用 with 及时释放 with Image.open("huge.jpg") as img: thumb = img.thumbnail((1000, 1000)) # 只保留小结果 # 2. 不需要原图就立即释放引用 img = None # 或 del img # 3. 分批处理,别把所有 Image 装进列表
内存公式:占用 ≈ 宽 × 高 × 每像素字节(RGB=3, RGBA=4, L=1)。理解这个公式,你就能估算任意图片的内存需求,提前规划处理策略。
除了前面提到的,Image 对象的方法全家福值得通览一遍,建立"有什么工具"的认知:
| 方法 | 作用 | 章节 |
|---|---|---|
| copy / convert | 复制 / 转模式 | 本节 |
| crop / resize / rotate | 几何操作 | 1.4、2.6 |
| thumbnail / transpose | 缩略 / 翻转 | 1.4 |
| filter | 应用滤镜 | 2.2 |
| point / split / merge | 像素 / 通道 | 2.1、2.4 |
| paste / composite | 合成 | 1.4、2.3 |
| getdata / putdata / load | 像素读写 | 3.1 |
| getexif / getpalette | 元数据 | 3.4 |
| histogram | 直方图 | 4.3 |
| seek / tell | 序列帧 | 2.5 |
学习策略:不用背方法名,知道"有这么个工具、管什么、在哪一章"就够——用到时回查。真正要熟练的是"什么时候用什么"的判断力。
问:为什么我改了 img 却没效果?
多半是"改了副本"。检查是否把新对象赋值给了新变量但继续用旧变量,或方法返回新对象却忘了接收。确认方法是否原地修改是第一步。
问:convert 和 copy 都要用吗?
不同目的:copy 是"独立副本"(保护原图),convert 是"转换模式"。想"转模式且不污染原图"就 img.copy().convert("L");不关心原图就直接 convert。
问:Image 对象可以保存为任意格式吗?
格式受模式支持限制(见 1.2 格式表)。存不进去时先转模式,或查该格式支持的模式。
问:怎么判断两张图是否相同?
谨慎:比较像素最直接但不简单——list(img1.getdata()) == list(img2.getdata()) 全量比较慢且占用内存大;先比 size/mode,再抽样比像素。生产环境常用"计算感知哈希"(差异哈希/感知哈希),本书 4.3 有思路。
把本节知识做成一个小工具——"图片信息速查器",批量输出图片的核心信息:
from PIL import Image import os def image_report(path): with Image.open(path) as img: w, h = img.size mode = img.mode fmt = img.format # 估算内存占用 bytes_per_pixel = {"1": 0.125, "L": 1, "P": 1, "RGB": 3, "RGBA": 4, "CMYK": 4}.get(mode, 1) mem_mb = w * h * bytes_per_pixel / (1024 * 1024) return { "文件": os.path.basename(path), "尺寸": f"{w}x{h}", "模式": mode, "格式": fmt, "内存估算": f"{mem_mb:.1f} MB", } # 批量查看 for fname in ["photo.jpg", "logo.png", "anim.gif"]: if os.path.exists(fname): r = image_report(fname) print(" | ".join(f"{k}:{v}" for k, v in r.items()))
这个工具的输出会同时包含"标签"(size/mode/format)和"画布估算"(内存)——把本节的画布+标签模型用到了实处。在批量处理前跑一遍,能提前发现"这张图多大、会不会爆内存"。
理解"不该怎么用",和"该怎么用"同样重要:
| 误用 | 后果 | 正确做法 |
|---|---|---|
| 在循环里反复 getdata() | 极慢 | 一次取、或 numpy |
| 忘接收 convert 返回值 | 原图没变 | img = img.convert(...) |
| 大图对象不释放 | 内存暴涨 | with / del |
| 直接对 RGBA 做 RGB 操作 | 报错/丢透明 | 先确认 mode |
| 误以为所有方法都原地改 | 结果不符合预期 | 看方法是否返回新对象 |
误用的根源:没建立"Image 对象 = 数据 + 元信息"的清晰心智。每遇到一个"结果不对",先问自己三个问题:改的是原图还是副本?方法返回了什么?模式对了吗?
Image 对象除了保存为文件,还能序列化成多种形式(跨进程/网络传输):
from PIL import Image import io, pickle img = Image.open("photo.jpg") # 1. 序列化成 bytes(网络传输) buf = io.BytesIO() img.save(buf, format="PNG") data = buf.getvalue() img_back = Image.open(io.BytesIO(data)) # 2. 保留元数据的 pickle(进程间传递) blob = pickle.dumps(img) # 3. 转 numpy(跨库传递,见第五章) import numpy as np arr = np.array(img) print(f"bytes 长度: {len(data)}, pickle 长度: {len(blob)}")
序列化的三种用途:io.BytesIO(网络/Base64)、pickle(进程间)、numpy(跨库计算)。理解"Image 可以变成 bytes/数组",就理解了它为什么能无缝融入各种系统——Image 是"图像数据的万能容器"。
把 Image 对象放在一个完整"管线"中理解它的核心地位:
from PIL import Image import io def image_pipeline(raw_bytes, steps): """通用的图片处理管线:bytes 进,bytes 出""" img = Image.open(io.BytesIO(raw_bytes)) # 1. 反序列化 # 2. 按步骤执行处理(每步输入输出都是 Image) for name, fn in steps: img = fn(img) print(f"步骤 [{name}]: {img.size} {img.mode}") # 3. 序列化输出 buf = io.BytesIO() img.save(buf, format="WEBP", quality=85) return buf.getvalue() # 定义处理步骤 from PIL import ImageOps steps = [ ("转方向", ImageOps.exif_transpose), ("统一尺寸", lambda im: ImageOps.fit(im, (800, 600))), ("转灰度", lambda im: im.convert("L").convert("RGB")), ] with open("photo.jpg", "rb") as f: result = image_pipeline(f.read(), steps) print(f"管线输出: {len(result)} bytes")
管线思维的价值:把"一个 Image 处理任务"拆成"步骤列表",每步是 Image → Image 的函数——可组合、可复用、可调试。这是本书第四章批量处理、第五章生态集成的共同架构思想。理解"Image 是管线的血液",就理解了 Pillow 的模块化本质。
初学者常混淆"Image 对象"和"文件对象",理清关系很重要:
| 对象 | 是什么 | 何时存在 |
|---|---|---|
| Image 对象 | 内存中的图像数据 | open/copy/new 后 |
| 文件句柄 | 磁盘文件的访问通道 | open() 返回的 |
| BytesIO | 内存中的"伪文件" | 读写内存数据 |
# 文件句柄(Pillow 之外的概念) f = open("photo.jpg", "rb") # 文件句柄 img = Image.open(f) # Image 对象(引用同一句柄) f.close() # 关闭句柄后 img 可能受影响! # 更安全:用 with 同时管理 with open("photo.jpg", "rb") as f: img = Image.open(f) img.load() # 立即解码,句柄关闭无影响
关键点:open 返回的 Image 是"惰性"的,依赖底层文件。用 with open + load 组合,或直接 Image.open(path) + img.copy(),让 Image 独立于文件存在。理解"Image 依赖文件直到 load",是避免"文件被关闭后图片报错"的关键。
做大量图片处理时,Image 对象的生命周期管理决定程序稳定性:
from PIL import Image import gc def process_many(paths): results = [] for path in paths: # 方案 A(推荐):with + 只保留结果 with Image.open(path) as img: small = img.copy() small.thumbnail((200, 200)) results.append(small) # 只保留缩略图 # 走出 with,img 与文件句柄都释放 # 方案 B:显式释放 img2 = Image.open(path) data = img2.copy() img2.close() # 显式关闭 del img2 # 删除引用 # 全部处理完,释放结果 results.clear() gc.collect() return len(results) print(process_many([f"img_{i}.jpg" for i in range(10)]))
生命周期管理三原则:
"打开-处理-释放"的节奏感,是长时间运行的图片服务(如 Web worker)不崩内存的关键——Image 对象像水,端着不撒手就会溢出。
Image 对象对文件的依赖常被忽视,主动解耦能避免很多坑:
from PIL import Image import io # 场景:打开文件后想立刻删除/移动文件 # 问题:open 惰性加载,文件删除后 Image 可能失效 with Image.open("temp.jpg") as img: # 方法 1:copy 解耦(像素已加载,独立于文件) independent = img.copy() # 方法 2:转 bytes 解耦(网络传输、缓存) buf = io.BytesIO() independent.save(buf, format="PNG") data = buf.getvalue() # 方法 3:load 强制加载 with Image.open("temp.jpg") as img: img.load() # 像素进内存,文件可安全删除 # 之后即使文件没了,img 仍可用
解耦的三种方式:copy(像素独立)、bytes(完全序列化)、load(强制加载)。理解"open 惰性依赖文件",你就知道为什么"文件被占用/删除了图片报错"——处理前先 load 或 copy,让 Image 独立于文件。
Image 对象的选择直接影响性能,几个实用规律:
from PIL import Image import time # 1. copy 的代价:大图 copy 是深拷贝,耗时耗内存 img = Image.open("photo.jpg") t0 = time.time() img_copy = img.copy() print(f"copy 耗时: {time.time()-t0:.3f}s") # 2. 模式影响速度:L 模式处理比 RGB 快 3 倍 rgb = Image.open("photo.jpg").convert("RGB") gray = rgb.convert("L") t0 = time.time() rgb.filter(ImageFilter.GaussianBlur(3)) t_rgb = time.time() - t0 t0 = time.time() gray.filter(ImageFilter.GaussianBlur(3)) t_gray = time.time() - t0 print(f"RGB 模糊: {t_rgb:.3f}s, L 模糊: {t_gray:.3f}s") # 3. 尺寸影响:缩小 1 半,处理快 4 倍 small = rgb.resize((rgb.width//2, rgb.height//2)) t0 = time.time() small.filter(ImageFilter.GaussianBlur(3)) print(f"半尺寸模糊: {time.time()-t0:.3f}s")
性能规律:
"能小就小、能简就简"——这八个字是 Pillow 性能优化的总纲,配合"先测量"(见 4.4),你能把任何 Pillow 程序调到合理速度。

认识了 Image 对象,接下来动真格的——裁剪、缩放、旋转、翻转,把图片"玩起来"。