Pillow 图像算术运算与通道计算
图像处理不仅包括几何变换、颜色空间转换、滤波增强与图层合成,还包括一类非常基础且重要的操作:图像算术运算与通道计算。这类操作不再关注图像在空间中的位置映射,也不以透明叠加为核心,而是直接把图像看作由像素值构成的数值矩阵,对这些矩阵执行逐像素、逐通道的运算。
在 Pillow 中,这类功能主要由 ImageChops 模块提供。它支持图像之间的差异比较、加法、减法、亮值选择、暗值选择以及反相等操作,适用于变化检测、结果比较、局部响应分析、通道级处理和基础图像增强等场景。
本章围绕 Pillow 中的 ImageChops 模块展开,重点说明 difference()、add()、subtract()、lighter()、darker()、invert() 等常用操作的基本原理、接口用法与可视化效果,帮助建立"图像不仅是图层对象,也是数值矩阵"的处理视角。
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 表示该位置上的像素值向量,ccc 表示通道数。
当两幅图像尺寸一致、模式兼容时,可以对它们在每个像素位置进行逐点运算。其一般形式可以写为:
Iout(x,y)=F(I1(x,y),I2(x,y)) I_{out}(x,y)=F(I_1(x,y),I_2(x,y)) Iout(x,y)=F(I1(x,y),I2(x,y))
其中 FFF 表示某种像素级映射规则。例如,加法运算对应像素值叠加,减法运算对应像素值相减,差异运算对应绝对差值,亮值与暗值选择对应逐像素极值运算,而反相则对应单幅图像的亮暗互补变换。
因此,图像算术运算的核心并不是图层覆盖关系,而是像素值本身的数值关系。
2. Pillow 的通道运算模块:ImageChops
Pillow 提供了专门的图像通道运算模块:
python
from PIL import Image, ImageChops
其中常见方法包括:
ImageChops.difference():计算两幅图像的绝对差值;ImageChops.add():计算两幅图像逐像素加法;ImageChops.subtract():计算两幅图像逐像素减法;ImageChops.lighter():逐像素取较亮值;ImageChops.darker():逐像素取较暗值;ImageChops.invert():对图像进行反相处理。
下面继续使用 scikit-image 中的示例图像说明这些操作:
python
from PIL import Image, ImageChops
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})
img1 = Image.fromarray(data.astronaut()).resize((256, 256))
img2 = Image.fromarray(data.chelsea()).resize((256, 256))
3. 差异运算:difference()
3.1 原理:逐像素绝对差值
difference() 用于计算两幅图像在每个像素位置上的绝对差异,其形式可写为:
Idiff(x,y)=∣I1(x,y)−I2(x,y)∣ I_{diff}(x,y)=|I_1(x,y)-I_2(x,y)| Idiff(x,y)=∣I1(x,y)−I2(x,y)∣
这意味着:
- 若两幅图像在某位置像素值接近,则结果较暗;
- 若两幅图像在某位置差异较大,则结果较亮。
因此,差异图本质上刻画的是两幅图像在像素层面的不一致程度,常用于图像变化检测、处理前后结果比较、图像对齐误差观察以及简单的运动区域分析。
3.2 Pillow 接口:difference()
python
diff_img = ImageChops.difference(img1, img2)
3.3 示例:两幅图像的差异可视化
python
diff_img = ImageChops.difference(img1, img2)
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(img1)
axes[0].set_title("图像 1")
axes[0].axis("off")
axes[1].imshow(img2)
axes[1].set_title("图像 2")
axes[1].axis("off")
axes[2].imshow(diff_img)
axes[2].set_title("difference() 差异图")
axes[2].axis("off")
plt.tight_layout()
plt.show()

差异图并不强调"融合效果",而是直接显示像素值差异的强弱分布。
3.4 示例:原图与旋转版本的差异
python
rotated = img1.rotate(5)
diff_rotated = ImageChops.difference(img1, rotated)
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(img1)
axes[0].set_title("原始图像")
axes[0].axis("off")
axes[1].imshow(rotated)
axes[1].set_title("旋转后图像")
axes[1].axis("off")
axes[2].imshow(diff_rotated)
axes[2].set_title("差异结果")
axes[2].axis("off")
plt.tight_layout()
plt.show()

这种方式可以直观观察几何变换对图像内容带来的局部变化。
4. 加法运算:add()
4.1 原理:逐像素相加
add() 对两幅图像执行逐像素加法,可写为:
Iadd(x,y)=I1(x,y)+I2(x,y) I_{add}(x,y)=I_1(x,y)+I_2(x,y) Iadd(x,y)=I1(x,y)+I2(x,y)
由于像素值通常限制在一定范围内,Pillow 提供了 scale 与 offset 参数,用于控制输出:
Iout(x,y)=I1(x,y)+I2(x,y)scale+offset I_{out}(x,y)=\frac{I_1(x,y)+I_2(x,y)}{\text{scale}}+\text{offset} Iout(x,y)=scaleI1(x,y)+I2(x,y)+offset
其中,scale 用于缩放结果,offset 用于整体偏移输出值。因此,add() 不只是简单叠加,也可以通过缩放构造更平衡的数值组合。
4.2 Pillow 接口:add()
python
add_img = ImageChops.add(img1, img2, scale=2.0, offset=0)
4.3 示例:加法运算效果
python
add_img = ImageChops.add(img1, img2, scale=2.0)
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(img1)
axes[0].set_title("图像 1")
axes[0].axis("off")
axes[1].imshow(img2)
axes[1].set_title("图像 2")
axes[1].axis("off")
axes[2].imshow(add_img)
axes[2].set_title("add(scale=2.0)")
axes[2].axis("off")
plt.tight_layout()
plt.show()

这里设置 scale=2.0,可以避免直接相加导致的亮度溢出,使结果更容易观察。
从实际用途看,add() 常用于合并两幅响应图、构造简单的图像增强结果,或者对不同处理中间结果进行数值叠加。不过如果不合理控制 scale,很容易导致高亮区域饱和。
5. 减法运算:subtract()
5.1 原理:逐像素相减
subtract() 对两幅图像执行逐像素减法,其形式可写为:
Isub(x,y)=I1(x,y)−I2(x,y) I_{sub}(x,y)=I_1(x,y)-I_2(x,y) Isub(x,y)=I1(x,y)−I2(x,y)
Pillow 同样支持:
Iout(x,y)=I1(x,y)−I2(x,y)scale+offset I_{out}(x,y)=\frac{I_1(x,y)-I_2(x,y)}{\text{scale}}+\text{offset} Iout(x,y)=scaleI1(x,y)−I2(x,y)+offset
- 与
difference()不同,减法是有方向的 。也就是说,I_1-I_2与I_2-I_1的结果通常不同,因此它更适合表示某一幅图像相对于另一幅图像"多出了什么"或"减去了什么"。
5.2 Pillow 接口:subtract()
python
sub_img = ImageChops.subtract(img1, img2, scale=1.0, offset=0)
5.3 示例:减法结果可视化
python
sub_img = ImageChops.subtract(img1, img2)
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(img1)
axes[0].set_title("图像 1")
axes[0].axis("off")
axes[1].imshow(img2)
axes[1].set_title("图像 2")
axes[1].axis("off")
axes[2].imshow(sub_img)
axes[2].set_title("subtract()")
axes[2].axis("off")
plt.tight_layout()
plt.show()

5.4 示例:原图减去模糊图,观察细节分量
python
from PIL import ImageFilter
blurred = img1.filter(ImageFilter.GaussianBlur(radius=2))
detail_part = ImageChops.subtract(img1, blurred, offset=128)
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(img1)
axes[0].set_title("原始图像")
axes[0].axis("off")
axes[1].imshow(blurred)
axes[1].set_title("模糊图像")
axes[1].axis("off")
axes[2].imshow(detail_part)
axes[2].set_title("原图 - 模糊图")
axes[2].axis("off")
plt.tight_layout()
plt.show()

这个结果可以近似看作原图中被模糊过程削弱掉的局部高频细节。这里加入 offset=128,是为了让正负差异在可视化时更加明显。
6. 亮值选择:lighter()
6.1 原理:逐像素取较大值
lighter() 在两幅图像的对应位置逐像素取较亮值,可表示为:
Ilight(x,y)=max(I1(x,y),I2(x,y)) I_{light}(x,y)=\max(I_1(x,y),I_2(x,y)) Ilight(x,y)=max(I1(x,y),I2(x,y))
这类操作不做平均,也不做差值,而是直接保留两个输入中更亮的那个结果。
6.2 Pillow 接口:lighter()
python
lighter_img = ImageChops.lighter(img1, img2)
6.3 示例:亮值选择结果
python
lighter_img = ImageChops.lighter(img1, img2)
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(img1)
axes[0].set_title("图像 1")
axes[0].axis("off")
axes[1].imshow(img2)
axes[1].set_title("图像 2")
axes[1].axis("off")
axes[2].imshow(lighter_img)
axes[2].set_title("lighter()")
axes[2].axis("off")
plt.tight_layout()
plt.show()

这种方法适合保留两幅图像中的高亮区域,或者构造亮响应优先的结果。从本质上看,它是一种逐像素极大值合成。
7. 暗值选择:darker()
7.1 原理:逐像素取较小值
与 lighter() 相对,darker() 会在对应位置保留较暗值:
Idark(x,y)=min(I1(x,y),I2(x,y)) I_{dark}(x,y)=\min(I_1(x,y),I_2(x,y)) Idark(x,y)=min(I1(x,y),I2(x,y))
它本质上是一种逐像素极小值运算。
7.2 Pillow 接口:darker()
python
darker_img = ImageChops.darker(img1, img2)
7.3 示例:暗值选择结果
python
darker_img = ImageChops.darker(img1, img2)
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(img1)
axes[0].set_title("图像 1")
axes[0].axis("off")
axes[1].imshow(img2)
axes[1].set_title("图像 2")
axes[1].axis("off")
axes[2].imshow(darker_img)
axes[2].set_title("darker()")
axes[2].axis("off")
plt.tight_layout()
plt.show()

它常用于保留较暗区域、构造极小值组合结果,或者对局部暗部特征进行简单比较。与 lighter() 一样,它也是一种典型的逐像素极值选择操作。
8. 反相运算:invert()
8.1 原理:亮暗互补变换
invert() 用于对图像进行反相处理。对于 8 位图像,其形式可写为:
Iinv(x,y)=255−I(x,y) I_{inv}(x,y)=255-I(x,y) Iinv(x,y)=255−I(x,y)
也就是说:
- 原本亮的区域会变暗;
- 原本暗的区域会变亮。
这是一种非常直接的像素互补运算。
8.2 Pillow 接口:invert()
python
invert_img = ImageChops.invert(img1)
8.3 示例:彩色图像反相
python
invert_img = ImageChops.invert(img1)
fig, axes = plt.subplots(1, 2, figsize=(8, 4))
axes[0].imshow(img1)
axes[0].set_title("原始图像")
axes[0].axis("off")
axes[1].imshow(invert_img)
axes[1].set_title("invert()")
axes[1].axis("off")
plt.tight_layout()
plt.show()

8.4 示例:灰度图像反相
python
gray = img1.convert("L")
gray_invert = ImageChops.invert(gray)
fig, axes = plt.subplots(1, 2, figsize=(8, 4))
axes[0].imshow(gray, cmap="gray")
axes[0].set_title("灰度图")
axes[0].axis("off")
axes[1].imshow(gray_invert, cmap="gray")
axes[1].set_title("灰度反相图")
axes[1].axis("off")
plt.tight_layout()
plt.show()

在灰度图中,反相效果通常更直观,因此很适合观察亮暗结构之间的互补关系。
9. 多种通道运算的统一对比
为了更直观地比较不同运算方式的作用,可以将几类常见结果统一展示:
python
diff_img = ImageChops.difference(img1, img2)
add_img = ImageChops.add(img1, img2, scale=2.0)
sub_img = ImageChops.subtract(img1, img2)
lighter_img = ImageChops.lighter(img1, img2)
darker_img = ImageChops.darker(img1, img2)
invert_img = ImageChops.invert(img1)
fig, axes = plt.subplots(2, 3, figsize=(12, 8))
images = [diff_img, add_img, sub_img, lighter_img, darker_img, invert_img]
titles = ["difference", "add", "subtract", "lighter", "darker", "invert"]
for ax, im, title in zip(axes.ravel(), images, titles):
ax.imshow(im, cmap="gray" if getattr(im, "mode", "") == "L" else None)
ax.set_title(title)
ax.axis("off")
plt.tight_layout()
plt.show()

通过这一组结果可以看出,difference() 强调的是差异强度,add() 强调的是数值叠加,subtract() 强调的是有方向的变化,lighter() 与 darker() 强调的是极值选择,而 invert() 强调的是亮暗互补。
10. 构建一个简单的变化分析流程
下面给出一个更完整的示例,将模糊、差异运算与反相组合起来,用于观察图像处理前后的变化:
python
from PIL import Image, ImageChops, ImageFilter
from skimage import data
import matplotlib.pyplot as plt
img = Image.fromarray(data.astronaut()).resize((256, 256))
# 1. 构造一个模糊版本
blurred = img.filter(ImageFilter.GaussianBlur(radius=2))
# 2. 计算差异
diff_img = ImageChops.difference(img, blurred)
# 3. 对差异图做反相,便于观察互补效果
diff_invert = ImageChops.invert(diff_img)
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
axes[0].imshow(img)
axes[0].set_title("原始图像")
axes[0].axis("off")
axes[1].imshow(blurred)
axes[1].set_title("模糊图像")
axes[1].axis("off")
axes[2].imshow(diff_img)
axes[2].set_title("差异图")
axes[2].axis("off")
axes[3].imshow(diff_invert)
axes[3].set_title("差异图反相")
axes[3].axis("off")
plt.tight_layout()
plt.show()

这个流程体现了图像算术运算在结果比较中的一个典型用途:先生成不同处理版本,再通过差异运算观察变化,最后通过反相增强显示上的可读性。
11. 使用 ImageChops 时的注意事项
11.1 输入图像尺寸应一致
多数运算都要求两幅图像具有相同尺寸。若尺寸不同,应先统一大小:
python
img1 = img1.resize((256, 256))
img2 = img2.resize((256, 256))
11.2 图像模式尽量一致
为了避免不必要的问题,两幅输入图像最好具有相同模式。例如都转换为 RGB:
python
img1 = img1.convert("RGB")
img2 = img2.convert("RGB")
11.3 add() 与 subtract() 要注意结果范围
像素值通常限制在 0~255 范围内,因此在加减运算中,合理使用 scale 与 offset 很重要,否则容易出现过亮、过暗或层次丢失的问题。
11.4 difference() 更适合做变化比较
如果目标是观察两幅图像哪里不同,通常优先使用 difference(),因为它比有方向的减法更直观、更适合可视化。
12. 总结
本章围绕 Pillow 中的 ImageChops 模块,对 difference()、add()、subtract()、lighter()、darker()、invert() 等常见图像算术运算与通道计算方式进行了系统说明。
通过这些操作可以看到:
difference()本质上是逐像素绝对差值,适合用于变化分析;add()与subtract()表示逐像素加减运算,强调像素值之间的数值关系;lighter()与darker()分别对应逐像素极大值与极小值选择;invert()则实现亮暗互补的反相变换;ImageChops提供了从图像对象层面理解像素矩阵计算的简洁入口。
结合这些操作,可以更直观地理解图像在 Pillow 中不仅可以被视为可显示、可合成的视觉对象,也可以被视为能够直接参与数值运算的像素矩阵。这些方法构成了图像比较、变化检测、通道分析与基础图像处理流程中的重要组成部分。