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′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() # 展示结果 ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/099ad283124447a98168c01dd876e104.png) 仿射变换在保持直线和平行关系的同时,对图像整体结构进行线性调整。 *** ** * ** *** ### 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() # 展示增强对比 ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/eb1cf5fea0c3493980409638484b5e13.png) *** ** * ** *** ### 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 表示与可视化结果,可以直观理解几何变换对图像空间结构的影响。这些操作构成了图像预处理与数据增强的重要基础。

相关推荐
xzl041 小时前
小智服务器:设备的各种MCP消息、初始化响应、工具列表和工具调用响应
java·网络·python
喵手2 小时前
Python爬虫零基础入门【第四章:解析与清洗·第3节】文本清洗:去空格、去噪、金额/日期/单位标准化!
爬虫·python·python爬虫实战·文本清洗·python爬虫工程化实战·python爬虫零基础入门·去空格去噪
喵手2 小时前
Python爬虫零基础入门【第四章:解析与清洗·第1节】BeautifulSoup 入门:从 HTML 提取结构化字段!
爬虫·python·beautifulsoup·爬虫实战·python爬虫工程化实战·零基础python爬虫教学·beautifulsoup入门
应用市场2 小时前
CNN池化层深度解析:从原理到PyTorch实现
人工智能·pytorch·python
小北方城市网2 小时前
微服务接口熔断降级与限流实战:保障系统高可用
java·spring boot·python·rabbitmq·java-rabbitmq·数据库架构
2401_841495642 小时前
【强化学习】DQN 改进算法
人工智能·python·深度学习·强化学习·dqn·double dqn·dueling dqn
幸福清风2 小时前
【Python】实战记录:从零搭建 Django + Vue 全栈应用 —— 用户认证篇
vue.js·python·django
qunaa01012 小时前
基于YOLO11-CSP-EDLAN的软夹持器夹持状态检测方法研究
python