你也许用过 Image.open()、.resize()、.save() 三板斧处理图片,觉得 Pillow 就是个”裁剪缩放工具”。但在真实项目里,批量加水印、格式自适应压缩、通道级像素操作、图像差异比对——这些需求几乎每周都会冒出来。如果每次都去搜 Stack Overflow,不如花 20 分钟把 Pillow 的工程级用法串一遍。
Pillow(PIL Fork)是 Python 生态里下载量最高的图像处理库,PyPI 周下载量超过 4000 万。但它被严重低估了——很多人只用到了它 10% 的能力,剩下的 90% 能帮你解决这些真实问题:
这篇文章不会重写一遍官方教程,而是从工程实践出发,把 Pillow 里最容易被忽略但最值钱的能力拆清楚。
SVG(Scalable Vector Graphics,可缩放矢量图形)是一种用代码描述图形的格式。它无限放大不失真、体积小、易于修改,特别适合技术文章中的流程图、结构图和对比图。
先划清能力边界,避免拿 Pillow 干它不擅长的事。

互补工具一览:
torchvision.transformsreportlab(PDF)、matplotlib(科学图表)opencv-python + pytesseractpillow-heif / pillow-avif-plugin
pip install Pillow
from PIL import Image
# 打开、查看属性、转换、保存
with Image.open("photo.jpg") as im:
print(im.format) # JPEG
print(im.size) # (1920, 1080)
print(im.mode) # RGB
# 裁剪:左上角 200×200
cropped = im.crop((0, 0, 200, 200))
# 等比缩放到最大 300px
thumbnail = im.copy()
thumbnail.thumbnail((300, 300))
# 灰度转换 + 保存
gray = im.convert("L")
gray.save("output.png")
三个关键点: 1. Image.open() 返回的是惰性对象,不会立刻把整张图读进内存 2. thumbnail() 是原地操作,不返回新对象 3. crop() 的 box 是 (left, upper, right, lower),不是 (x, y, w, h)
resize、crop、rotate 看似简单,但工程里踩坑最多的也是它们。
# resize:强制指定目标尺寸,不保持宽高比
resized = im.resize((800, 600), Image.Resampling.LANCZOS)
# thumbnail:保持宽高比,就地修改,不超过指定值
thumb = im.copy()
thumb.thumbnail((800, 800)) # 宽高都 ≤ 800
区别总结:

from PIL import Image
# 放大优先用 LANCZOS(质量最好)
up = im.resize((1920, 1080), Image.Resampling.LANCZOS)
# 缩小到图标尺寸可以用 BICUBIC
icon = im.resize((64, 64), Image.Resampling.BICUBIC)
# 像素风格可以用 NEAREST(最近邻,速度最快)
pixel_art = im.resize((48, 48), Image.Resampling.NEAREST)
重采样滤镜选择指南:

# rotate:任意角度旋转(逆时针)
rotated = im.rotate(15, expand=True, fillcolor="#FFFFFF")
# expand=True 会自动扩展画布,避免裁掉角落
# fillcolor 填充旋转后露出的空白区域
# transpose:90° 倍数旋转和翻转(更快,推荐)
flipped_h = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) # 水平翻转
flipped_v = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM) # 垂直翻转
rot_90 = im.transpose(Image.Transpose.ROTATE_90) # 顺时针 90°
rot_180 = im.transpose(Image.Transpose.ROTATE_180) # 180°
经验法则:90° 倍数旋转永远用 transpose,不要用 rotate——更快、像素更精确、不会引入插值误差。
这是 Pillow 区别于”图片查看器”的关键能力。
from PIL import Image
im = Image.open("photo.jpg")
# RGB → 灰度
gray = im.convert("L")
# RGB → RGBA(加透明通道)
rgba = im.convert("RGBA")
# RGB → CMYK(印刷用)
cmyk = im.convert("CMYK")
# 带抖动的调色板量化(用于 GIF 等格式)
palletized = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
r, g, b = im.split() # 分离三个通道(每个都是单通道 L 模式)
merged = Image.merge("RGB", (r, g, b)) # 重组
# 实战:只保留红色通道,其他置零
only_red = Image.merge("RGB", (r, Image.new("L", im.size, 0), Image.new("L", im.size, 0)))
# 实战:交换红蓝通道(类似一些老照片的色偏效果)
swapped = Image.merge("RGB", (b, g, r))
from PIL import Image
background = Image.new("RGBA", (400, 400), "#1A1A2E")
foreground = Image.new("RGBA", (200, 200), "#E94560")
# 创建渐变蒙版(中心不透明,边缘透明)
mask = Image.new("L", (200, 200), 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.ellipse([0, 0, 200, 200], fill=255)
# composite:按蒙版合成两张图
result = Image.composite(foreground, background.crop((100, 100, 300, 300)), mask)
background.paste(result, (100, 100))
Image.composite() 是实现渐变边缘、羽化效果的利器,比直接 paste 精细得多。
这两个模块不在 from PIL import Image 里,但工程价值极高。
from PIL import Image, ImageOps
im = Image.open("photo.jpg")
# 等比缩放并居中填充(完美适配头像/封面尺寸)
padded = ImageOps.pad(im, (800, 800), color="#000000")
# 居中裁剪到指定尺寸(不管原图比例)
cropped = ImageOps.fit(im, (800, 800), Image.Resampling.LANCZOS)
# 灰度 + 自动对比度(比手动调参更稳健)
auto_contrast = ImageOps.autocontrast(ImageOps.grayscale(im))
# 反色
inverted = ImageOps.invert(ImageOps.grayscale(im))
# 展开(给图片加边框)
expanded = ImageOps.expand(im, border=20, fill="#FFFFFF")
pad vs fit 的区别:

from PIL import Image, ImageChops
before = Image.open("before.png")
after = Image.open("after.png")
# 逐像素差值(高亮变化区域)
diff = ImageChops.difference(before, after)
diff.save("diff.png")
# 判断两张图是否完全相同
bbox = ImageChops.difference(before, after).getbbox()
if bbox is None:
print("完全相同")
else:
print(f"差异区域: {bbox}")
# 加法/减法混合
blended = ImageChops.add(before, after, scale=2.0, offset=0)
实际应用:UI 自动化测试中,截取页面截图与基线图做差异比对,getbbox() 返回 None 就代表渲染完全一致。
批量处理图片时,不同来源的图片质量参差不齐,需要统一校正。
from PIL import Image, ImageFilter
im = Image.open("photo.jpg")
# 高斯模糊(可控半径)
blurred = im.filter(ImageFilter.GaussianBlur(radius=2))
# USM 锐化(比 SHARPEN 更精细)
sharpened = im.filter(ImageFilter.UnsharpMask(
radius=2, # 模糊半径
percent=150, # 锐化强度
threshold=3 # 只锐化差异超过此阈值的像素
))
# 边缘检测
edges = im.filter(ImageFilter.FIND_EDGES)
# 浮雕效果
embossed = im.filter(ImageFilter.EMBOSS)
# 中值滤波(去椒盐噪点)
denoised = im.filter(ImageFilter.MedianFilter(size=3))
from PIL import Image, ImageEnhance
im = Image.open("photo.jpg")
# 对比度增强(factor > 1 增强,< 1 减弱)
enhancer = ImageEnhance.Contrast(im)
contrasted = enhancer.enhance(1.3)
# 亮度调节
bright = ImageEnhance.Brightness(im).enhance(1.1)
# 饱和度调节
vivid = ImageEnhance.Color(im).enhance(1.2)
# 锐度调节
sharp = ImageEnhance.Sharpness(im).enhance(1.5)
# 链式组合:对比度 + 亮度 + 饱和度一步到位
final = ImageEnhance.Contrast(im).enhance(1.2)
final = ImageEnhance.Brightness(final).enhance(1.1)
final = ImageEnhance.Color(final).enhance(1.15)
ImageDraw 是 Pillow 里最容易被低估的模块,它可以直接在图像上绘制几何图形和文字,覆盖大多数标注、水印、装饰需求。
from PIL import Image, ImageDraw, ImageFont
im = Image.new("RGB", (600, 300), "#FAFAFA")
draw = ImageDraw.Draw(im)
# 矩形(支持圆角)
draw.rounded_rectangle(
[20, 20, 280, 130], radius=12,
outline="#3366CC", width=2, fill="#E8F0FE"
)
# 椭圆
draw.ellipse([320, 20, 580, 130], fill="#4CAF50", outline="#388E3C", width=2)
# 带箭头的线
draw.line([(20, 170), (580, 170)], fill="#333333", width=3)
# 文字(尽量加载 truetype 字体,默认字体很小且不美观)
try:
font_large = ImageFont.truetype("arial.ttf", 28)
font_small = ImageFont.truetype("arial.ttf", 18)
except OSError:
font_large = ImageFont.load_default()
font_small = font_large
draw.text((40, 40), "Pillow 12.2", fill="#1A237E", font=font_large)
draw.text((40, 80), "Engineering Guide", fill="#555555", font=font_small)
draw.text((370, 60), "●", fill="#FFFFFF", font=font_large)
im.save("annotated.png")
rounded_rectangle 是 Pillow 8.2+ 新增的,支持圆角矩形,做卡片式 UI 标注非常好用。
同样的图像内容,保存参数不同,文件体积可能差 5 倍。
from PIL import Image
im = Image.open("photo.jpg")
# JPEG:照片首选
im.save("web.jpg", quality=85, optimize=True, progressive=True)
# quality: 75-85 是质量和体积的最佳平衡点
# optimize: 启用 Huffman 编码优化,通常减少 5-10% 体积
# progressive: 渐进式加载,网页体验更好
# PNG:截图/图标/透明图
im.save("screenshot.png", optimize=True)
# RGBA 模式自动保存为带透明通道的 PNG
rgba = im.convert("RGBA")
rgba.save("transparent.png")
# WebP:兼顾质量和体积
im.save("web.webp", quality=80, lossless=False)
# lossless=True 时质量参数无效
# GIF:带调色板量化
gif = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
gif.save("animation.gif", save_all=True, append_images=[frame2, frame3], duration=200, loop=0)
格式选择速查:

把上面的能力串成一条完整的流水线——这正是 Pillow 在工程中真正的价值所在。
"""
批量图片处理流水线:统一尺寸 + 水印 + 自适应压缩 + 元数据清理
"""
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageFilter
INPUT_DIR = Path("./photos")
OUTPUT_DIR = Path("./output")
TARGET_SIZE = (1200, 630) # 社交媒体封面尺寸
WATERMARK_TEXT = "© Tech Series"
def create_watermark(width: int, height: int) -> Image.Image:
"""创建透明水印图层"""
wm = Image.new("RGBA", (width, 100), (0, 0, 0, 0))
draw = ImageDraw.Draw(wm)
try:
font = ImageFont.truetype("arial.ttf", 20)
except OSError:
font = ImageFont.load_default()
# 文字居中
bbox = draw.textbbox((0, 0), WATERMARK_TEXT, font=font)
text_w = bbox[2] - bbox[0]
x = (width - text_w) // 2
draw.text((x, 20), WATERMARK_TEXT, fill=(255, 255, 255, 120), font=font)
return wm
def process_one(src: Path, dst: Path) -> None:
"""处理单张图片"""
with Image.open(src) as im:
# 1. 确保是 RGB 模式(CMYK、RGBA 等统一转换)
if im.mode != "RGB":
im = im.convert("RGB")
# 2. 居中裁剪到目标尺寸
im = ImageOps.fit(im, TARGET_SIZE, Image.Resampling.LANCZOS)
# 3. 自动对比度校正
im = ImageOps.autocontrast(im, cutoff=2)
# 4. 轻微 USM 锐化(补偿缩放带来的模糊)
im = im.filter(ImageFilter.UnsharpMask(radius=1, percent=120, threshold=2))
# 5. 加水印
wm = create_watermark(TARGET_SIZE[0], TARGET_SIZE[1])
rgba = im.convert("RGBA")
rgba.paste(wm, (0, TARGET_SIZE[1] - 100), wm)
im = rgba.convert("RGB")
# 6. 保存为 WebP(如果原图是照片场景)或 JPEG
suffix = src.suffix.lower()
if suffix in (".png", ".gif"):
im.save(dst.with_suffix(".webp"), quality=80)
else:
im.save(dst.with_suffix(".jpg"), quality=85, optimize=True, progressive=True)
def main():
OUTPUT_DIR.mkdir(exist_ok=True)
photos = list(INPUT_DIR.glob("*.[jp][pn]g")) + list(INPUT_DIR.glob("*.webp"))
if not photos:
print("未找到图片文件")
return
for p in photos:
dst = OUTPUT_DIR / p.name
try:
process_one(p, dst)
print(f"✓ {p.name}")
except Exception as e:
print(f"✗ {p.name}: {e}")
print(f"处理完成:{len(photos)} 张")
if __name__ == "__main__":
main()

这条流水线覆盖了真实项目中最常见的需求:
ImageOps.fit 居中裁剪,不变形autocontrast 补偿过暗/过亮的输入Image.open() 是惰性加载im = Image.open("photo.jpg") # 此时只读取了文件头
im.size # 此时才真正加载像素数据
# 如果文件在 open 和实际读取之间被删除/修改,会抛异常
对策:需要确保文件稳定时,尽早触发加载(读取 .size 或 .load())。
crop() 的坐标是绝对坐标,不是偏移量# 错误:以为 (x, y, width, height)
wrong = im.crop((100, 100, 300, 200)) # 200×100 区域
# 正确:(left, upper, right, lower)
right = im.crop((100, 100, 400, 300)) # 300×200 区域
# 错误:RGBA 图像直接 paste,透明区域变成黑色
background.paste(foreground_rgba, (x, y))
# 正确:传 foreground 自身作为 mask
background.paste(foreground_rgba, (x, y), foreground_rgba)
thumbnail() 是原地修改thumb = im # thumb 和 im 指向同一对象
thumb.thumbnail((100, 100)) # im 也被修改了!
对策:先 .copy() 再调 thumbnail()。
# 一张 10000×10000 RGB 图 ≈ 300MB 内存
# 如果同时处理 10 张,直接 3GB 起步
对策: - 用 Image.open() 的惰性加载特性,只保留文件句柄 - 处理完立刻 del 释放 - 超大图用 Image.tile 分块处理或 reduce() 先降采样
rgba = Image.new("RGBA", (100, 100), (255, 0, 0, 128))
rgba.save("test.jpg") # 会自动丢弃 alpha 通道,不会报错
对策:保存 JPEG 前显式 .convert("RGB"),确保行为可控。
Pillow 不是 Photoshop,但它是最轻量的 Python 图像自动化工具。这些场景它最合适:

