Pillow 图像分割、切片与拼接处理
图像处理不仅包括几何变换、颜色转换、增强滤波与图像标注,还包括一类非常常见的基础操作:图像分割、切片与拼接处理。这类操作主要关注图像在空间上的局部划分与重组,即把一幅完整图像拆分为若干子区域,或者将多个局部图像重新组合为新的整体结果。
在 Pillow 中,这类功能主要依赖 crop() 与 paste() 两个核心接口完成。前者用于从原图中提取局部区域,后者用于将局部图像粘贴到目标画布中。基于这两个基础操作,可以进一步实现规则区域切割、网格分块、横向或纵向拼图,以及简单的全景式拼接思路。
本章围绕 Pillow 中的图像分割、切片与拼接展开,重点说明 crop() 的区域切割、网格分块、paste() 的拼接逻辑,以及横向拼接、纵向拼接和简单全景拼接的实现方式,帮助建立"图像既可以整体处理,也可以按区域拆解与重组"的处理视角。
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) 表示像素坐标,v\mathbf{v}v 表示该位置上的像素值向量。
如果将图像划分为若干局部区域,本质上就是从原始坐标空间中提取子区域:
Isub=I[x1:x2, y1:y2] I_{sub}=I[x_1:x_2,\ y_1:y_2] Isub=I[x1:x2, y1:y2]
若再把这些局部区域重新放置到新的画布中,则形成拼接结果。也就是说,图像分割与拼接并不改变像素值的基本含义,而是改变图像内容在空间上的组织方式。
从处理角度看,分割强调的是"把整体拆开",而拼接强调的是"把局部重新组合"。这种处理在数据预处理、图像分块分析、拼图展示、局部增强以及结果可视化中都非常常见。
2. Pillow 中的核心接口:crop() 与 paste()
Pillow 中与图像切割和拼接最相关的两个基础接口是:
python
from PIL import Image
其中:
crop()用于从原图中提取指定矩形区域;paste()用于把一个图像粘贴到目标图像的指定位置。
下面继续使用 scikit-image 中的示例图像说明这些操作:
python
from PIL import Image
from skimage import data
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style="whitegrid", font="SimHei", rc={"axes.unicode_minus": False})
img = Image.fromarray(data.astronaut()).resize((512, 512))
3. 区域切割:crop()
3.1 原理:从原图中提取局部子区域
crop() 的作用是根据给定的矩形边界,从原图中提取出一个局部区域。若设切割框为:
(left, upper, right, lower) (left,\ upper,\ right,\ lower) (left, upper, right, lower)
则结果图像对应于原图中的这一矩形区域。
这类操作的本质是对原图坐标范围进行裁切,因此它不会主动改变局部像素值,而是保留其中一部分内容。换句话说,crop() 关注的是空间范围的截取,而不是像素值的重新计算。
3.2 Pillow 接口:crop()
python
cropped = img.crop((left, upper, right, lower))
其中四个参数分别表示左边界、上边界、右边界和下边界。
3.3 示例:提取中心区域
python
cropped = img.crop((140, 100, 380, 380))
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
axes[0].imshow(img)
axes[0].set_title("原始图像")
axes[0].axis("off")
axes[1].imshow(cropped)
axes[1].set_title("crop() 切割结果")
axes[1].axis("off")
plt.tight_layout()
plt.show()

通过这种方式,可以非常直接地从原图中提取感兴趣区域。它适合用于目标区域截取、局部分析与后续分块处理。
4. 多区域切片:按规则位置进行分割
4.1 原理:将图像拆分为多个局部块
如果对图像执行多次 crop(),就可以把一幅完整图像切分为多个子区域。最简单的方式,是按固定坐标范围依次提取不同区域。
从空间划分角度看,这种操作可以理解为把原图定义域划分为若干互不重叠或按规则排列的子区域,再分别提取每一个子区域对应的图像内容。若以四分块为例,则可以写成:
I1=I[0:W2, 0:H2],I2=I[W2:W, 0:H2]I3=I[0:W2, H2:H],I4=I[W2:W, H2:H] I_1=I[0:\tfrac{W}{2},\ 0:\tfrac{H}{2}],\quad I_2=I[\tfrac{W}{2}:W,\ 0:\tfrac{H}{2}] \\ I_3=I[0:\tfrac{W}{2},\ \tfrac{H}{2}:H],\quad I_4=I[\tfrac{W}{2}:W,\ \tfrac{H}{2}:H] I1=I[0:2W, 0:2H],I2=I[2W:W, 0:2H]I3=I[0:2W, 2H:H],I4=I[2W:W, 2H:H]
其中 WWW 和 HHH 分别表示图像宽度与高度。
4.2 示例:将图像切成四块
python
w, h = img.size
blocks = [
img.crop((0, 0, w//2, h//2)),
img.crop((w//2, 0, w, h//2)),
img.crop((0, h//2, w//2, h)),
img.crop((w//2, h//2, w, h)),
]
fig, axes = plt.subplots(2, 2, figsize=(8, 8))
for ax, block, title in zip(
axes.ravel(),
blocks,
["左上", "右上", "左下", "右下"]
):
ax.imshow(block)
ax.set_title(title)
ax.axis("off")
plt.tight_layout()
plt.show()

这种规则切片方式适合图像局部分析、多区域结果展示、分块式预处理以及拼图数据准备。它的本质仍然是把完整图像按空间坐标切成若干可独立处理的子块。
5. 网格分块处理
5.1 原理:把图像按固定网格均匀划分
相比简单的四分切割,更常见的做法是将图像按规则网格分成若干小块。例如,把一幅图像切成 m × n 个小块,本质上就是按照固定步长在宽度和高度方向循环提取区域。
若图像尺寸为 (W,H)(W,H)(W,H),总列数为 nnn、总行数为 mmm,则每个网格块的宽高可写为:
wtile=Wn,htile=Hm w_{tile}=\frac{W}{n},\qquad h_{tile}=\frac{H}{m} wtile=nW,htile=mH
第 (r,c)(r,c)(r,c) 个网格块对应的切割区域可以表示为:
Ir,c=I[c⋅wtile:(c+1)⋅wtile, r⋅htile:(r+1)⋅htile] I_{r,c}=I[c\cdot w_{tile}:(c+1)\cdot w_{tile},\ r\cdot h_{tile}:(r+1)\cdot h_{tile}] Ir,c=I[c⋅wtile:(c+1)⋅wtile, r⋅htile:(r+1)⋅htile]
这说明网格分块本质上是把二维图像空间映射为一个更高层的离散网格索引系统。
5.2 示例:将图像切为 4×4 网格
python
rows, cols = 4, 4
w, h = img.size
tile_w, tile_h = w // cols, h // rows
tiles = []
for r in range(rows):
for c in range(cols):
left = c * tile_w
upper = r * tile_h
right = left + tile_w
lower = upper + tile_h
tile = img.crop((left, upper, right, lower))
tiles.append(tile)
5.3 可视化:展示部分网格块
python
# 可视化:在每个图块中心添加白色半透明编号
fig, axes = plt.subplots(4, 4, figsize=(8, 8))
for ax, tile, idx in zip(axes.ravel(), tiles, range(1, 17)):
ax.imshow(tile)
ax.text(
0.5, 0.5, str(idx),
transform=ax.transAxes,
ha="center", va="center",
fontsize=36,
color="white",
alpha=0.65
)
ax.axis("off")
plt.tight_layout()
plt.show()

网格分块常用于图像切片、局部特征分析、拼图处理以及批量区域增强等任务。它也是将大图拆解为小图的典型思路。
6. 拼接处理:paste()
6.1 原理:把局部图像放回目标画布
与 crop() 相对应,paste() 用于把一个图像粘贴到目标图像中的指定位置。若目标画布记为 IcanvasI_{canvas}Icanvas,被粘贴图像记为 IpatchI_{patch}Ipatch,其左上角放置位置为 (x0,y0)(x_0,y_0)(x0,y0),则拼接结果可以写为:
Iout(x,y)={Ipatch(x−x0, y−y0),(x,y)∈ΩpasteIcanvas(x,y),otherwise I_{out}(x,y)= \begin{cases} I_{patch}(x-x_0,\ y-y_0), & (x,y)\in \Omega_{paste} \\ I_{canvas}(x,y), & \text{otherwise} \end{cases} Iout(x,y)={Ipatch(x−x0, y−y0),Icanvas(x,y),(x,y)∈Ωpasteotherwise
其中 Ωpaste\Omega_{paste}Ωpaste 表示被粘贴图像在目标画布中所覆盖的区域。
因此,paste() 的作用并不是对两幅图像做加权混合,而是将源图像区域直接写入目标图像的指定位置。从空间角度看,它相当于把一个局部图块重新映射到新的画布坐标中。
6.2 Pillow 接口:paste()
python
canvas.paste(patch, (x, y))
其中 (x, y) 为粘贴的左上角坐标。
6.3 示例:将切下来的区域粘贴到新画布中
python
patch = img.crop((140, 100, 380, 380))
canvas = Image.new("RGB", (512, 512), "white")
canvas.paste(patch, (136, 136))
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
axes[0].imshow(patch)
axes[0].set_title("切割区域")
axes[0].axis("off")
axes[1].imshow(canvas)
axes[1].set_title("paste() 拼接结果")
axes[1].axis("off")
plt.tight_layout()
plt.show()

这种方式能够把局部图像独立展示到新的背景中,也可以作为更复杂拼接操作的基础。
7. 横向拼图
7.1 原理:按水平方向依次拼接多幅图像
如果有多幅尺寸一致或已经统一尺寸的图像,可以创建一张更宽的新画布,再按顺序把它们从左到右依次粘贴。若有 nnn 幅宽度分别为 w1,w2,...,wnw_1,w_2,\dots,w_nw1,w2,...,wn 的图像,高度统一为 HHH,则目标画布宽度为:
W=∑i=1nwi W=\sum_{i=1}^{n} w_i W=i=1∑nwi
第 kkk 幅图像的粘贴起点可写为:
xk=∑i=1k−1wi,yk=0 x_k=\sum_{i=1}^{k-1} w_i,\qquad y_k=0 xk=i=1∑k−1wi,yk=0
于是横向拼接本质上是在同一高度基线上,对各个子图按宽度累加的方式依次排列。
7.2 示例:横向拼接两幅图像
python
img1 = Image.fromarray(data.astronaut()).resize((256, 256))
img2 = Image.fromarray(data.chelsea()).resize((256, 256))
canvas = Image.new("RGB", (img1.width + img2.width, 256), "white")
canvas.paste(img1, (0, 0))
canvas.paste(img2, (img1.width, 0))
plt.figure(figsize=(10, 5))
plt.imshow(canvas)
plt.axis("off")
plt.show()

这种横向拼图方式常用于多图结果对比、前后处理效果展示以及图像样本并排可视化。``
8. 纵向拼图
8.1 原理:按垂直方向依次拼接多幅图像
与横向拼图类似,若沿垂直方向排列图像,只需创建一张更高的新画布,再从上到下依次粘贴。若有 nnn 幅高度分别为 h1,h2,...,hnh_1,h_2,\dots,h_nh1,h2,...,hn 的图像,宽度统一为 WWW,则目标画布高度为:
H=∑i=1nhi H=\sum_{i=1}^{n} h_i H=i=1∑nhi
第 kkk 幅图像的粘贴起点可写为:
xk=0,yk=∑i=1k−1hi x_k=0,\qquad y_k=\sum_{i=1}^{k-1} h_i xk=0,yk=i=1∑k−1hi
因此,纵向拼接的本质是在同一垂直列中,按高度累加依次放置各个子图。
8.2 示例:纵向拼接两幅图像
python
img1 = Image.fromarray(data.astronaut()).resize((256, 256))
img2 = Image.fromarray(data.rocket()).resize((256, 256))
canvas = Image.new("RGB", (256, img1.height + img2.height), "white")
canvas.paste(img1, (0, 0))
canvas.paste(img2, (0, img1.height))
plt.figure(figsize=(5, 10))
plt.imshow(canvas)
plt.axis("off")
plt.show()

纵向拼接适合流程展示、长图说明以及分步骤结果对比。
9. 网格拼图
9.1 原理:把多幅图像组织成二维布局
如果需要同时展示多幅图像,可以进一步把它们按网格形式拼接到一个二维画布中。设每个子图尺寸统一为 (w,h)(w,h)(w,h),总行数为 RRR,总列数为 CCC,则目标画布尺寸为:
W=C⋅w,H=R⋅h W=C\cdot w,\qquad H=R\cdot h W=C⋅w,H=R⋅h
若第 (r,c)(r,c)(r,c) 个位置上的子图从 0 开始计数,则其左上角坐标为:
x=c⋅w,y=r⋅h x=c\cdot w,\qquad y=r\cdot h x=c⋅w,y=r⋅h
因此,网格拼图可以看作把一组图像按照二维离散索引映射到统一画布中的规则坐标位置。
9.2 示例:构建 2×2 拼图
python
imgs = [
Image.fromarray(data.astronaut()).resize((256, 256)),
Image.fromarray(data.chelsea()).resize((256, 256)),
Image.fromarray(data.coffee()).resize((256, 256)),
Image.fromarray(data.rocket()).resize((256, 256)),
]
canvas = Image.new("RGB", (512, 512), "white")
positions = [(0, 0), (256, 0), (0, 256), (256, 256)]
for im, pos in zip(imgs, positions):
canvas.paste(im, pos)
plt.figure(figsize=(8, 8))
plt.imshow(canvas)
plt.axis("off")
plt.show()

这种方式非常适合做样本集合展示、多图比较和教学示例拼版。
10. 从切片到重组:打乱并重拼
10.1 原理:先切割,再改变排列顺序
图像切片不仅可以用于分析,也可以用于重组。最典型的做法是先把图像按网格切开,再按照新的顺序重新拼接,形成类似拼图重排的结果。
若原始切片集合记为:
{T1,T2,...,Tn} \{T_1,T_2,\dots,T_n\} {T1,T2,...,Tn}
重组后的排列可看作在这些切片上施加一个置换映射:
π:{1,2,...,n}→{1,2,...,n} \pi:\{1,2,\dots,n\}\rightarrow \{1,2,\dots,n\} π:{1,2,...,n}→{1,2,...,n}
则第 kkk 个新位置上的图像块为:
Tk′=Tπ(k) T'{k}=T{\pi(k)} Tk′=Tπ(k)
这说明,切片重组的核心并不是改变图像块内部内容,而是改变它们在整体中的空间排列关系。
10.2 示例:打乱 4×4 网格顺序后重新拼接
python
import random
# 读取图像
img = Image.fromarray(data.astronaut()).resize((512, 512))
# 切成 4×4 网格,并保留原始编号
rows, cols = 4, 4
w, h = img.size
tile_w, tile_h = w // cols, h // rows
tiles = []
original_id = 1
for r in range(rows):
for c in range(cols):
tile = img.crop((c * tile_w, r * tile_h, (c + 1) * tile_w, (r + 1) * tile_h))
tiles.append((original_id, tile))
original_id += 1
# 使用固定随机种子打乱顺序
seed = 42
rng = random.Random(seed)
rng.shuffle(tiles)
# 重新拼接
canvas = Image.new("RGB", img.size, "white")
for idx, (_, tile) in enumerate(tiles):
r = idx // cols
c = idx % cols
canvas.paste(tile, (c * tile_w, r * tile_h))
# 显示拼接结果,并在每个图块中心标注"原始编号"
fig, ax = plt.subplots(figsize=(6, 6))
ax.imshow(canvas)
for idx, (orig_id, _) in enumerate(tiles):
r = idx // cols
c = idx % cols
# 当前拼接位置对应图块中心
x = c * tile_w + tile_w / 2
y = r * tile_h + tile_h / 2
ax.text(
x, y, str(orig_id),
ha="center", va="center",
fontsize=36,
color="white",
alpha=0.65
)
ax.axis("off")
plt.tight_layout()
plt.show()

这种思路可用于拼图游戏原型、图像块重组实验以及局部区域随机重排增强。
11. NumPy 视角:切片与拼接的数组表达
前面的系列中已经多次说明,Pillow 图像与 NumPy 数组可以无缝互转。从数组角度看,图像切片与拼接实际上更加直观。
例如,切割图像区域可以写成:
python
import numpy as np
arr = np.array(img)
patch = arr[100:300, 150:350]
print(arr.shape)
print(patch.shape)
输出示例为:
python
(512, 512, 3)
(200, 200, 3)
这里的切片操作与 crop() 的思想是一致的,只不过 NumPy 使用的是数组索引方式。
若要重新转回 Pillow 图像:
python
patch_img = Image.fromarray(patch)
因此,Pillow 提供了更直观的图像对象接口,而 NumPy 提供了更底层的数组表达方式。两者结合后,可以更方便地理解图像分割与拼接在本质上就是空间区域的提取与重组。
12. 总结
本章围绕 Pillow 中的图像分割、切片与拼接处理,对 crop() 的区域切割、网格分块、paste() 拼接、横向纵向拼图以及简单全景拼接思路进行了系统说明。
通过这些操作可以看到:
crop()本质上是从原图中提取局部空间区域;- 规则切片与网格分块能够把完整图像拆分为多个可独立处理的子块;
paste()提供了将局部图像重新放置到目标画布中的基础能力;- 横向、纵向与网格拼图体现了图像在空间布局上的重组方式;
结合 Pillow 的图像对象接口与 NumPy 的数组切片方式,可以更直观地理解图像如何从整体变为局部区域,再从局部区域重新组合为新的整体结果。这些操作构成了图像分块分析、拼图展示、局部处理与基础拼接任务中的重要基础。
使用的是数组索引方式。