5.9. 算术运算

上一节中的绘图系列是将内容绘制图像。算术系列则是将两幅图像合并为第三幅 —— 把它们的像素值相加、彼此相减、在每个位置取最小值或最大值。正是这一小组逐像素的算术运算,构成了帧差分、背景减除、曝光叠加以及其他若干经典模式的基础。

Image 类上的算术系列足够精简,可以一次性列举完:

  • add() —— 逐像素 self + other,截断到该格式的最大值。

  • sub() —— 逐像素 self - other,在下端截断到 0

  • rsub() —— 逐像素 other - self,截断到 0(与 sub 算术相同,只是操作数顺序相反)。

  • min() —— 取两个值的逐像素最小值。

  • max() —— 逐像素最大值。

  • difference() —— 逐像素 |self - other|,即绝对差。

外加两个相关的单图像运算:

  • invert() —— 将每个像素替换为 255 - pixel(或该格式对应的最大值)。

  • negate() —— invert() 的别名。

顶部有两条水平渐变条, 代表源图像 A 和 B —— A 从左到右由暗变亮,B 从左到右由亮变暗。 下方是五条渐变条, 代表对 A 和 B 应用每种成对 运算的结果:A.add(B) 显示为均匀的白色,因为每个 位置相加后都超过 255 并被截断; A.sub(B) 在左半部分为零, 向右逐渐变亮; A.difference(B) 呈 V 形,两端 明亮、中间暗淡; A.min(B) 两端暗、中间 较亮;A.max(B) 两端 明亮、中间为灰色。

两条源渐变 A 和 B,以及对它们应用每种成对运算的结果。每种运算都逐位置进行 —— 结果中任一位置所显示的内容,仅取决于该位置上的两个源像素。

5.9.1. 两种操作数形式

每个双图像方法的第二个操作数都接受以下任一形式:

  • 另一幅尺寸相同的 Image。算术运算逐位置进行 —— (x, y) 处的结果,是对两幅图像 (x, y) 处的源像素施加该运算的结果。

  • 一个标量值 —— 灰度用整数,RGB565 用 (r, g, b) 元组。同一标量应用于每个位置。

当应用程序想把每个像素平移一个常数量时,标量形式很有用。img.add(40) 将整幅图像提亮 40;img.sub((20, 20, 20)) 将每个像素的每个通道压暗 20;img.max(50) 将任何低于 50 的像素抬升到 50,其余保持不变 —— 这类运算能把接近全黑的传感器底噪变成平坦的暗灰,供后续阶段处理。

5.9.2. 截断

在每一种运算中,像素值都保持在该格式的取值范围内。对于 8 位通道,这意味着 0 —— 255:任何本应溢出超过 255 的值都会被截断回 255,任何本应低于 0 的值都会被截断到 0。不存在回绕。

这种选择在实践中很重要。add 提亮像素时,绝不会在明亮端因运算本会溢出而产生突然变暗的伪影;sub 压暗像素时,也绝不会在暗端因本会下溢而产生突然变亮的伪影。结果在视觉上保持有意义,代价是在饱和的极端处损失一些信息。

截断也正是 subrsub 返回结果彼此不同的原因。img_a.sub(img_b) 给出 a 中比 b 更亮的部分,其余处处为零;img_a.rsub(img_b) 给出 b 中比 a 更亮的部分。两者对单向变化检测都有用 —— 如果应用程序只关心变亮的像素,或只关心变暗的像素 —— 但二者都无法捕获两帧之间的全部变化。

5.9.3. 差分运算

对于双向变化检测,应当选用的运算是 difference(),它在每个位置计算 |self - other| —— 即无符号的绝对差。无论朝哪个方向发生变化的每个像素,都会在结果中显示为非零值,其幅值与该位置的变化量成正比。

正是这一特性 —— 恰好在两幅图像不一致之处为非零 —— 使 difference 成为逐帧变化检测的主力。启动时存储的参考帧与一次新捕获,经过 difference 运算,会产生一幅图像,其非零像素标记出场景中发生移动或亮度变化的每个位置。

5.9.4. 用掩码限定范围

所有算术方法都接受在区域与掩码一页中介绍的 mask 关键字参数。当传入掩码时,运算仅在掩码非零的位置进行;其余各处目标图像保持不变。

这种组合体现在两种模式中。第一种是将运算约束到已知区域:例如,仅在检测到的标记的边界框内将两帧相加。第二种是逐块构建一幅合成帧 —— 在前景掩码内对一系列帧取 min,在互补掩码内对同一系列取 max —— 就是这类模式。

5.9.5. 就地运算并保留输入

所有算术方法都遵循前面确立的运算约定:每个方法都就地修改源图像,并返回同一图像以便链式调用。调用之后源图像的像素就消失了 —— 被该运算针对作为第二操作数传入的内容所得的结果所替换。

当应用程序需要保留两个输入时,安全的做法是先复制其中之一:

diff = current.copy()       # leaves current intact
diff.difference(reference)  # diff now holds the absolute difference

这种模式 —— 先复制,再运算 —— 是任何帧差分流水线的支柱,因为参考帧必须在比较中存留下来,才能在下一捕获帧上重复使用。

有了六种合并运算、两种单图像运算、一种绝对差主力运算,以及用于限定范围的掩码关键字,逐像素算术工具集涵盖了经典机器视觉所需的亮度与通道组合。表面上看类似算术的其余工具,是逐位而非逐值地工作的。