Pillow 灰度化、二值化与阈值处理
在前面的内容中,已经介绍了图像的基础读写、几何变换以及颜色模式与通道表示。在此基础上,图像处理的重点可以进一步从颜色表达转向结构表达。灰度化、二值化与阈值处理正是这一过程中的基础操作,它们能够将连续的颜色信息逐步压缩为更适合分析轮廓、区域、前景与背景关系的形式。
灰度化、二值化与阈值处理常用于目标区域提取、文档图像处理、前景分离、掩码构造以及图像分割的前期准备。在 Pillow 中,这些操作可以通过 convert()、point() 以及结合 NumPy 的像素级逻辑运算来实现。
本章将围绕灰度图的生成、阈值映射、二值化处理、掩码生成以及简单的伪彩色可视化展开,说明图像如何从连续的颜色表示逐步过渡为更明确的结构表达。整体思路仍然延续前文,即从图像的数学表示出发,再落实到 Pillow 的具体接口与 Notebook 中的可视化实现。
1. 灰度化:从彩色表示到亮度表示
1.1 原理:单通道亮度建模
一幅彩色图像可表示为:
I : ( x , y ) → ( R , G , B ) I: (x, y) \rightarrow (R, G, B) I:(x,y)→(R,G,B)
而灰度化的目标,是将每个像素从三通道颜色向量压缩为单一亮度值:
I g r a y : ( x , y ) → L I_{gray}: (x, y) \rightarrow L Igray:(x,y)→L
其中 L L L 表示亮度强度,通常取值范围为 [ 0 , 255 ] [0, 255] [0,255]。
这意味着灰度图不再保留颜色类别信息,而是强调图像中的明暗变化、边缘过渡与局部结构。
从图像分析角度看,灰度化的意义在于:
- 降低数据维度;
- 消除颜色差异对结构分析的干扰;
- 为阈值分割、边缘检测与区域提取提供更直接的输入。
1.2 Pillow 接口:convert('L')
在 Pillow 中,灰度化最直接的方法是:
python
gray = img.convert('L')
这里的 'L' 表示 8 位灰度模式,即每个像素由单个亮度值构成。前文已经说明,convert() 是 Pillow 中进行图像模式转换的核心入口,而灰度图正是最常见的一种模式转换结果。
1.3 示例:彩色图转灰度图
下面继续使用 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())
gray = img.convert('L')
fig, axes = plt.subplots(1, 2, figsize=(8, 4))
axes[0].imshow(img)
axes[0].set_title("原始图像")
axes[0].axis("off")
axes[1].imshow(gray, cmap='gray')
axes[1].set_title("灰度图像")
axes[1].axis("off")
plt.tight_layout()
plt.show()

从结果可以看到,灰度化之后图像不再依赖颜色差异,而是通过亮度变化来表达面部轮廓、衣物纹理和背景层次。这种表达方式虽然损失了色彩信息,但更便于后续进行阈值分析与结构提取。
1.4 NumPy 视角:数组维度变化
灰度化之后,图像的数组形状也会随之变化:
python
import numpy as np
rgb_arr = np.array(img)
gray_arr = np.array(gray)
print(rgb_arr.shape)
print(gray_arr.shape)
输出示例:
python
(512, 512, 3)
(512, 512)
这说明:
- 原始 RGB 图像在数组中包含三个通道;
- 灰度图只保留二维亮度矩阵。
这一点与前文关于"图像模式决定数组结构"的讨论完全一致,只不过这里进一步将颜色表达压缩为了结构更简洁的单通道形式。
2. 阈值处理:从连续灰度到离散判别
2.1 原理:阈值映射
灰度图虽然已经简化为单通道表示,但每个像素仍然取连续的 0--255 数值。若希望更明确地区分"前景"和"背景",可以进一步引入阈值函数,将灰度值映射为离散类别。
最简单的阈值映射可写为:
B ( x , y ) = { 255 , L ( x , y ) ≥ t 0 , L ( x , y ) < t B(x, y)= \begin{cases} 255, & L(x, y) \ge t \\ 0, & L(x, y) < t \end{cases} B(x,y)={255,0,L(x,y)≥tL(x,y)<t
其中:
- L ( x , y ) L(x, y) L(x,y) 为灰度值;
- t t t 为设定阈值;
- 输出结果仅保留两类取值:0 与 255。
从数学意义上看,阈值处理是一种像素级判别函数。它将连续强度空间压缩为离散标签空间,因此非常适合用于区域分离、掩码生成和简单分割任务。
2.2 Pillow 接口:point()
Pillow 提供了 point() 方法对像素逐点映射。对于阈值操作,可以写成:
python
binary = gray.point(lambda p: 255 if p >= 128 else 0)
这里:
p表示单个像素值;- 当像素值不小于阈值 128 时,映射为白色 255;
- 否则映射为黑色 0。
这种方式非常适合对灰度图进行快速规则映射。
2.3 示例:固定阈值二值化
python
binary = gray.point(lambda p: 255 if p >= 128 else 0)
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(img)
axes[0].set_title("原始图像")
axes[0].axis("off")
axes[1].imshow(gray, cmap='gray')
axes[1].set_title("灰度图")
axes[1].axis("off")
axes[2].imshow(binary, cmap='gray')
axes[2].set_title("二值图(阈值=128)")
axes[2].axis("off")
plt.tight_layout()
plt.show()

二值化之后,图像中的亮区域与暗区域被清晰分离。尽管这种划分较为粗糙,但已经可以用于很多简单的结构提取任务,例如高亮区域筛选、文本背景分离、物体粗略轮廓定位等。
3. 二值化:从阈值判别到结构分割
3.1 原理:前景与背景的两类表达
二值图可以视为一种最简单的分割结果。此时图像不再表示连续亮度,而是只表示两类状态:
- 0:通常对应背景;
- 255:通常对应前景。
因此,二值图在逻辑上非常接近"结构掩码"。
它虽然不能表达复杂的纹理与层次,但可以高效突出:
- 轮廓边界;
- 文本与背景;
- 明亮目标区域;
- 简单形状的存在与否。
3.2 示例:不同阈值的影响
阈值的选择会直接影响分割结果。下面对比几个不同阈值下的二值化效果:
python
thresholds = [64, 128, 192]
fig, axes = plt.subplots(1, 4, figsize=(14, 4))
axes[0].imshow(gray, cmap='gray')
axes[0].set_title("灰度图")
axes[0].axis("off")
for ax, t in zip(axes[1:], thresholds):
binary_t = gray.point(lambda p, th=t: 255 if p >= th else 0)
ax.imshow(binary_t, cmap='gray')
ax.set_title(f"阈值={t}")
ax.axis("off")
plt.tight_layout()
plt.show()

从结果中可以观察到:
- 阈值较低时,更多区域会被归入前景;
- 阈值较高时,只有更亮的区域会保留下来;
- 阈值设定不同,本质上对应不同的结构筛选标准。
这说明二值化并不是单纯的"黑白转换",而是基于判别规则的结构提取过程。
3.3 Pillow 中更规范的二值模式:'1'
如果希望生成真正意义上的单比特图像,还可以进一步转换为模式 '1':
python
binary_1bit = gray.point(lambda p: 255 if p >= 128 else 0).convert('1')
print(binary_1bit.mode)
输出结果:
txt
1
这里的 '1' 表示每个像素仅占 1 bit。
与 L 模式下的 0/255 灰度图相比,它在存储层面更加紧凑,常用于文档扫描、黑白图稿和简单掩码保存。
4. 掩码生成:阈值结果作为结构选择器
4.1 原理:Mask 的含义
在图像处理中,Mask(掩码)通常是一幅与原图大小一致的单通道图像,用于表示哪些区域"被选中"。
从功能上看,掩码并不直接描述颜色,而是描述像素位置是否满足某种条件。
若记掩码为 M ( x , y ) M(x, y) M(x,y),则它可以表示为:
M ( x , y ) = { 1 , 该位置满足条件 0 , 该位置不满足条件 M(x, y)= \begin{cases} 1, & \text{该位置满足条件} \\ 0, & \text{该位置不满足条件} \end{cases} M(x,y)={1,0,该位置满足条件该位置不满足条件
在实际中,掩码往往由阈值处理得到。也就是说,二值图本身就可以直接作为掩码使用。
4.2 示例:基于亮度阈值生成掩码
python
mask = gray.point(lambda p: 255 if p >= 150 else 0)
plt.figure(figsize=(5, 5))
plt.imshow(mask, cmap='gray')
plt.title("亮度掩码")
plt.axis("off")
plt.show()

这张掩码图中,较亮区域被标记为白色,较暗区域则被压制为黑色。
4.3 示例:使用掩码筛选原图区域
在 Pillow 中,可以借助 Image.composite() 用掩码选择保留原图中的部分区域:
python
from PIL import Image
black_bg = Image.new("RGB", img.size, (0, 0, 0))
masked_result = Image.composite(img, black_bg, mask)
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].imshow(mask, cmap='gray')
axes[0].set_title("Mask")
axes[0].axis("off")
axes[1].imshow(masked_result)
axes[1].set_title("掩码筛选结果")
axes[1].axis("off")
plt.tight_layout()
plt.show()

这里的处理逻辑是:
- 掩码为白色的位置,从原图中取值;
- 掩码为黑色的位置,从黑色背景中取值。
因此,掩码实际上起到了"结构选择器"的作用。
这种机制在目标提取、前景保留、区域增强和图像合成中都非常常见。
5. 阈值处理中的注意事项
5.1 灰度化并不等于结构分割
灰度图虽然减少了颜色维度,但仍然保留了完整的亮度连续变化。
因此,灰度化只是阈值分割的前置步骤,而不是分割结果本身。
5.2 阈值选择会强烈影响结果
固定阈值方法实现简单,但对光照变化、背景复杂度和图像对比度较为敏感。
同一阈值在不同图像上可能产生完全不同的结果,因此在实际应用中应结合图像内容进行调整。
5.3 二值图更适合离散标签场景
与前一篇几何变换中提到的插值方法选择类似,离散类别图像通常更适合使用最近邻思想来处理,而不宜引入模糊插值。标签图、分割掩码和二值结果本质上都属于离散值图像,这一点与前文关于 NEAREST 更适合掩码类图像的讨论是一致的。
6. 简单伪彩色:让灰度结果更易观察
6.1 原理:灰度值到颜色映射
严格来说,灰度图只包含亮度,不包含颜色类别。
但在可视化时,我们常常希望让不同强度区间更容易区分,于是可以使用伪彩色映射,即:
L ( x , y ) → ( R , G , B ) L(x, y) \rightarrow (R, G, B) L(x,y)→(R,G,B)
这并不是恢复原始颜色,而只是将单通道数值映射为某种颜色表,用于增强可视化辨识度。
6.2 示例:使用 Matplotlib 伪彩色显示
虽然 Pillow 本身不提供丰富的 colormap 体系,但在 Notebook 中可以直接配合 Matplotlib 展示:
python
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(gray, cmap='gray')
axes[0].set_title("灰度图")
axes[0].axis("off")
axes[1].imshow(gray, cmap='viridis')
axes[1].set_title("伪彩色:viridis")
axes[1].axis("off")
axes[2].imshow(gray, cmap='magma')
axes[2].set_title("伪彩色:magma")
axes[2].axis("off")
plt.tight_layout()
plt.show()

这类伪彩色并不会改变图像的结构信息,但能让亮度差异在视觉上更容易被观察到。
因此,在分析热区、亮度分布或局部对比差异时,伪彩色是一种非常实用的辅助表达方式。
7. NumPy 视角:阈值处理的数组表达
为了更直接地理解阈值处理,也可以将灰度图转换为 NumPy 数组后进行布尔运算:
python
gray_arr = np.array(gray)
binary_arr = np.where(gray_arr >= 128, 255, 0).astype(np.uint8)
print(gray_arr.shape)
print(binary_arr.shape)
print(binary_arr.dtype)
输出示例:
python
(512, 512)
(512, 512)
uint8
然后再转回 Pillow 图像:
python
binary_from_np = Image.fromarray(binary_arr)
plt.figure(figsize=(5, 5))
plt.imshow(binary_from_np, cmap='gray')
plt.title("NumPy 阈值结果")
plt.axis("off")
plt.show()

这种方式与前文中 Pillow 和 NumPy 的互转思路完全一致:Pillow 提供图像对象与基础接口,NumPy 提供更灵活的数组级逻辑运算,两者结合后可以构建更复杂的像素处理流程。
8. 从灰度化到掩码提取的完整流程
下面给出一个更完整的示例,将灰度化、阈值处理与掩码应用串联起来:
python
from PIL import Image
from skimage import data
import matplotlib.pyplot as plt
import numpy as np
# 1. 读取图像
img = Image.fromarray(data.astronaut())
# 2. 灰度化
gray = img.convert('L')
# 3. 阈值二值化
mask = gray.point(lambda p: 255 if p >= 150 else 0)
# 4. 用掩码提取亮区域
black_bg = Image.new("RGB", img.size, (0, 0, 0))
result = Image.composite(img, black_bg, mask)
# 5. 可视化
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
axes[0].imshow(img)
axes[0].set_title("原始图像")
axes[0].axis("off")
axes[1].imshow(gray, cmap='gray')
axes[1].set_title("灰度图")
axes[1].axis("off")
axes[2].imshow(mask, cmap='gray')
axes[2].set_title("阈值掩码")
axes[2].axis("off")
axes[3].imshow(result)
axes[3].set_title("掩码提取结果")
axes[3].axis("off")
plt.tight_layout()
plt.show()

这个流程很好地展示了本章的主线:
- 先通过灰度化去掉颜色冗余;
- 再通过阈值处理建立结构判别规则;
- 最后将二值结果作为掩码,用于区域筛选与前景提取。
这正是从"颜色表示"向"结构提取"过渡的典型过程。
9. 总结
本章围绕 Pillow 中的灰度化、二值化与阈值处理,对灰度转换、阈值映射、二值分割、掩码生成以及伪彩色可视化的实现方式与基本原理进行了系统说明。
通过这些操作可以看到:
- 灰度化本质上是将彩色图像压缩为单通道亮度表示;
- 阈值处理是对连续灰度值进行离散判别;
- 二值图可以作为结构掩码使用;
- 掩码不仅能够表示区域选择,还可以参与图像合成与目标提取;
- 伪彩色则提供了更直观的灰度结果可视化方式。
结合 Pillow 的像素映射接口与 NumPy 的数组表示,可以更直观地理解图像如何从颜色表达过渡到结构表达。这些操作构成了图像预处理、区域提取与简单分割任务的重要基础,也是进一步理解图像增强、滤波处理与更复杂视觉分析流程的前提。