Pillow 图像几何变换与仿射操作
几何变换是图像处理中最基础且最常用的一类操作,在数据预处理、样本增强、图像对齐以及视觉建模等任务中均扮演核心角色。该类操作通过对像素坐标进行系统性映射,改变图像在空间中的结构表达,而不直接修改像素的语义内容。
在 Pillow 中,几何变换主要通过裁剪、翻转、旋转、缩放以及更一般形式的仿射变换来实现。本章围绕这些操作,从坐标模型、实现方式与可视化结果三个层面,对其原理与实践进行说明。
1. 几何变换的坐标表示
一幅二维数字图像可以表示为定义在规则网格上的函数:
I:(x,y)→v,v∈Rc I: (x, y) \rightarrow \mathbf{v}, \quad \mathbf{v} \in \mathbb{R}^c I:(x,y)→v,v∈Rc
其中 (x,y)(x, y)(x,y) 为像素坐标,ccc 为通道数。
几何变换通过构造坐标映射函数 TTT,在新的坐标系下重建图像:
I′(x′,y′)=I(T−1(x′,y′)) I'(x', y') = I(T^{-1}(x', y')) I′(x′,y′)=I(T−1(x′,y′))
Pillow 在实现几何变换时采用反向映射策略,即在输出图像坐标系中查找原图像的对应位置,从而避免像素空洞问题。
2. 图像裁剪
2.1 原理:区域裁剪与坐标范围
裁剪通过限定坐标范围提取原图像的局部区域。该操作不涉及插值计算,其结果直接来自原始像素数据。
2.2 Pillow 接口:Image.crop()
python
from PIL import Image # Pillow 的核心图像对象与处理接口
from skimage import data # scikit-image 自带示例数据集(便于复现实验)
from IPython.display import display # 在 Notebook / Jupyter 环境中显示图像
img = Image.fromarray(data.astronaut()) # 将 skimage 返回的 NumPy 数组转换为 PIL.Image 对象
box = (100, 100, 400, 400) # 裁剪区域 (left, upper, right, lower),右/下边界为"开区间"语义
cropped = img.crop(box) # 按 box 指定的矩形区域裁剪,返回新的 Image 对象(不修改原图)
display(cropped) # 在 Notebook 中直接渲染裁剪后的结果

2.3 可视化:裁剪前后对比
python
import matplotlib.pyplot as plt # Matplotlib:绘图与可视化
import seaborn as sns # Seaborn:设置更美观的默认主题(这里主要用于风格)
sns.set_theme(style="whitegrid", font="SimHei", rc={"axes.unicode_minus": False})
# style="whitegrid":白底网格风格(对比更清晰)
# font="SimHei":中文字体,避免标题中文乱码
# axes.unicode_minus=False:避免负号显示为方块(某些中文字体环境常见问题)
fig, axes = plt.subplots(1, 2, figsize=(10, 4)) # 创建 1 行 2 列子图,画布大小 10×4 英寸
axes[0].imshow(img) # 左图显示原图(PIL.Image 可被 imshow 直接接受)
axes[0].set_title("原始图像") # 设置左图标题
axes[1].imshow(cropped) # 右图显示裁剪结果
axes[1].set_title("裁剪后图像") # 设置右图标题
for ax in axes:
ax.axis("off") # 关闭坐标轴刻度与边框,让图像展示更干净
plt.tight_layout() # 自动调整子图间距,避免标题/图像重叠
plt.show() # 显示整张图

裁剪操作仅改变图像的空间范围,不引入数值近似误差。
2.4 NumPy 视角:数组切片裁剪
python
import numpy as np # NumPy:数组表示与切片操作
arr = np.array(img) # 将 PIL.Image 转为 NumPy 数组,形状通常为 (H, W, C)
cropped_np = arr[100:400, 100:400] # NumPy 切片裁剪:先行(y/H)后列(x/W),等价于 box=(100,100,400,400)
从数组角度看,裁剪等价于二维切片操作,具有较低的计算成本。
3. 图像翻转
3.1 原理:坐标反射模型
翻转属于坐标反射变换,不涉及连续插值。
- 水平翻转(左右翻转):
x′=W−1−x,y′=y x' = W - 1 - x,\quad y' = y x′=W−1−x,y′=y
- 垂直翻转(上下翻转):
x′=x,y′=H−1−y x' = x,\quad y' = H - 1 - y x′=x,y′=H−1−y
其中 WWW、HHH 分别表示图像宽度与高度。
3.2 Pillow 接口:Image.transpose()
python
flip_lr = img.transpose(Image.FLIP_LEFT_RIGHT) # 左右翻转(沿 y 轴镜像),像素值不变,仅重排位置
flip_tb = img.transpose(Image.FLIP_TOP_BOTTOM) # 上下翻转(沿 x 轴镜像),同样不做插值
3.3 可视化:水平/垂直翻转对比
python
fig, axes = plt.subplots(1, 3, figsize=(12, 4)) # 创建 1×3 子图:原图/水平翻转/垂直翻转
axes[0].imshow(img) # 原图
axes[0].set_title("原始图像")
axes[1].imshow(flip_lr) # 水平翻转结果
axes[1].set_title("水平翻转")
axes[2].imshow(flip_tb) # 垂直翻转结果
axes[2].set_title("垂直翻转")
for ax in axes:
ax.axis("off") # 隐藏坐标轴,突出图像内容
plt.tight_layout() # 自动布局
plt.show() # 显示图像对比

翻转操作保持像素值不变,仅改变像素在空间中的排列顺序。
3.4 NumPy 视角:步进切片翻转
python
flip_lr_np = arr[:, ::-1, :] # 水平翻转:对宽度维 W 反向步进(::-1),行(y)与通道(c)不变
flip_tb_np = arr[::-1, :, :] # 垂直翻转:对高度维 H 反向步进(::-1),列(x)与通道(c)不变
4. 图像旋转
4.1 原理:二维旋转与插值
二维旋转可以表示为线性变换:
x′y′\]\[cosθ−sinθsinθcosθ\]\[xy\] \\begin{bmatrix} x' \\\\ y' \\end{bmatrix} \\begin{bmatrix} \\cos\\theta \& -\\sin\\theta \\\\ \\sin\\theta \& \\cos\\theta \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\end{bmatrix} \[x′y′\]\[cosθsinθ−sinθcosθ\]\[xy
旋转后像素坐标通常不再位于整数网格上,因此需要通过插值方法计算新像素值。
4.2 Pillow 接口:Image.rotate()
python
rotated = img.rotate(45) # 旋转 45°,默认以图像中心为旋转中心;输出尺寸默认不变(可能裁切边缘)
rotated_expand = img.rotate(45, expand=True) # expand=True:自动扩大画布以容纳完整旋转后的图像(常见于数据增强)
4.3 可视化:expand=True 的画布差异
python
fig, axes = plt.subplots(1, 3, figsize=(12, 4)) # 创建 1×3 子图:原图/旋转不扩展/旋转并扩展
axes[0].imshow(img) # 原始图像
axes[0].set_title("原始图像")
axes[1].imshow(rotated) # rotate(45):画布不变,边缘可能被裁掉
axes[1].set_title("rotate(45)")
axes[2].imshow(rotated_expand) # rotate(45, expand=True):画布变大,内容尽量保留
axes[2].set_title("rotate(45, expand=True)")
for ax in axes:
ax.axis("off") # 去掉坐标轴
plt.tight_layout() # 自动布局
plt.show() # 展示对比结果

当启用 expand=True 时,画布尺寸会随旋转角度自动调整,以完整保留图像内容。
5. 图像缩放
5.1 原理:尺度变换与重采样
缩放通过对坐标轴进行线性变换实现:
x′=sxx,y′=syy x' = s_x x,\quad y' = s_y y x′=sxx,y′=syy
当缩放比例不为整数时,新像素值由插值算法估计。
5.2 Pillow 接口:Image.resize()
python
methods = {
"NEAREST": Image.NEAREST, # 最近邻:最快、锯齿明显,适合标签/掩码(mask)这类离散值图像
"BILINEAR": Image.BILINEAR, # 双线性:速度与平滑度折中,常用于一般图像缩放
"BICUBIC": Image.BICUBIC # 双三次:更平滑、细节更好但更慢,常用于高质量缩放
}
5.3 可视化:缩放-插值方法对比
python
fig, axes = plt.subplots(1, 3, figsize=(12, 4)) # 创建 1×3 子图用于对比不同插值结果
for ax, (name, method) in zip(axes, methods.items()): # 同步遍历子图与插值方法
resized = img.resize((256, 256), resample=method) # resize:指定目标尺寸 (W, H),resample 控制插值策略
ax.imshow(resized) # 显示缩放结果
ax.set_title(name) # 标注插值方法名称
ax.axis("off") # 关闭坐标轴
plt.tight_layout() # 自动布局
plt.show() # 展示对比

不同插值方式在平滑性与边缘保留方面表现不同,应根据应用需求进行选择。
6. 仿射变换
6.1 原理:齐次坐标与仿射矩阵
仿射变换使用齐次坐标表示为:
x′y′1\]\[abcdef001\]\[xy1\] \\begin{bmatrix} x' \\\\ y' \\\\ 1 \\end{bmatrix} \\begin{bmatrix} a \& b \& c \\\\ d \& e \& f \\\\ 0 \& 0 \& 1 \\end{bmatrix} \\begin{bmatrix} x \\\\ y \\\\ 1 \\end{bmatrix} x′y′1 ad0be0cf1 xy1 该模型可以统一描述平移、旋转、缩放与剪切等几何操作。 #### 6.2 Pillow 接口:Image.transform(mode=AFFINE) ```python matrix = (1, 0.3, 0, 0.2, 1, 0) # Pillow 的 AFFINE 使用 6 参数矩阵 (a, b, c, d, e, f),对应: # x' = a*x + b*y + c # y' = d*x + e*y + f # 这里 b=0.3、d=0.2 引入剪切(shear),c=f=0 表示不平移 affine = img.transform( img.size, # 输出图像大小(这里保持与原图一致) Image.AFFINE, # 指定变换类型为仿射变换 matrix, # 6 参数仿射矩阵 resample=Image.BICUBIC # 重采样方式:双三次插值,减弱锯齿并提升观感 ) ``` #### 6.3 可视化:仿射变换效果对比 ```python fig, axes = plt.subplots(1, 2, figsize=(10, 4)) # 创建 1×2 子图:原图 vs 仿射结果 axes[0].imshow(img) # 原图 axes[0].set_title("原始图像") axes[1].imshow(affine) # 仿射变换后的图像 axes[1].set_title("仿射变换结果") for ax in axes: ax.axis("off") # 隐藏坐标轴 plt.tight_layout() # 自动布局 plt.show() # 展示结果 ```  仿射变换在保持直线和平行关系的同时,对图像整体结构进行线性调整。 *** ** * ** *** ### 7. 批量几何变换:数据增强示例 ```python def geometric_augmentations(img): # 将多种几何变换打包返回,便于统一管理与可视化对比 return { "original": img, # 原始图像(基准) "rotated": img.rotate(30), # 旋转 30°(默认中心旋转,可能发生裁切) "flipped": img.transpose(Image.FLIP_LEFT_RIGHT), # 水平翻转(左右镜像) "scaled": img.resize((224, 224)) # 缩放到固定输入尺寸(常用于深度学习模型输入) } augmented = geometric_augmentations(img) # 获取增强结果字典:name -> Image fig, axes = plt.subplots(1, 4, figsize=(16, 4)) # 创建 1×4 子图用于展示四种版本 for ax, (name, im) in zip(axes, augmented.items()): # 同步遍历子图与增强图像 ax.imshow(im) # 显示当前增强结果 ax.set_title(name) # 用 key 作为标题 ax.axis("off") # 关闭坐标轴 plt.tight_layout() # 自动布局 plt.show() # 展示增强对比 ```  *** ** * ** *** ### 8. NumPy 表示:维度变化与数组形状 ```python arr = np.array(img) # 原图转为 NumPy 数组,通常形状为 (H, W, C) arr_rotated = np.array(rotated_expand) # 旋转并扩展后的图像转数组(H/W 可能变化) print(arr.shape) # 打印原图数组形状:验证高度、宽度、通道数 print(arr_rotated.shape) # 打印旋转后数组形状:expand=True 往往导致 H/W 变大 ``` > (512, 512, 3) > > (726, 726, 3) 几何变换主要影响数组的空间维度,通道维度保持不变。 *** ** * ** *** ### 9. 总结 本章围绕 Pillow 提供的几何变换接口,对裁剪、翻转、旋转、缩放以及仿射变换的实现方式与数学背景进行了系统说明。通过结合 NumPy 表示与可视化结果,可以直观理解几何变换对图像空间结构的影响。这些操作构成了图像预处理与数据增强的重要基础。