Pillow 图像几何变换与仿射操作

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′1abcdef001xy1 \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 表示与可视化结果,可以直观理解几何变换对图像空间结构的影响。这些操作构成了图像预处理与数据增强的重要基础。

相关推荐
珺毅同学1 天前
YOLO生成预测json标签迁移问题
python·yolo·json
骑士雄师1 天前
18.4 长期记忆可修改版
python
~小先生~1 天前
Python从入门到放弃(一)
开发语言·python
天佑木枫1 天前
第2天:变量与数据类型 —— 让程序记住信息
python
Dust-Chasing1 天前
Claude Code源码剖析 - Claude Code 上下文压缩机制
人工智能·python·ai
Cloud_Shy6181 天前
解读《Effective Python 3rd Edition》:从练气到老魔(第五章 Item 33 - 35)
开发语言·人工智能·笔记·python·学习方法
abcy0712131 天前
python pandas csv异步后台清洗前端优先返回成功信息
前端·python·pandas
颜酱1 天前
LangChain使用RAG 入门:让大模型读懂你的私有文档
python·langchain
天天进步20151 天前
Python全栈项目--校园智能宿舍管理系统
开发语言·python
测试员周周1 天前
【AI测试智能体-面试】AI测试面试60题(附回答思路)
人工智能·python·功能测试·测试工具·单元测试·自动化·测试用例