5.9. 算术运算¶
上一节中的绘图系列是将内容绘制进图像。算术系列则是将两幅图像合并为第三幅 —— 把它们的像素值相加、彼此相减、在每个位置取最小值或最大值。正是这一小组逐像素的算术运算,构成了帧差分、背景减除、曝光叠加以及其他若干经典模式的基础。
Image 类上的算术系列足够精简,可以一次性列举完:
add()—— 逐像素self + other,截断到该格式的最大值。sub()—— 逐像素self - other,在下端截断到0。rsub()—— 逐像素other - self,截断到0(与sub算术相同,只是操作数顺序相反)。min()—— 取两个值的逐像素最小值。max()—— 逐像素最大值。difference()—— 逐像素|self - other|,即绝对差。
外加两个相关的单图像运算:
两条源渐变 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 压暗像素时,也绝不会在暗端因本会下溢而产生突然变亮的伪影。结果在视觉上保持有意义,代价是在饱和的极端处损失一些信息。
截断也正是 sub 和 rsub 返回结果彼此不同的原因。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
这种模式 —— 先复制,再运算 —— 是任何帧差分流水线的支柱,因为参考帧必须在比较中存留下来,才能在下一捕获帧上重复使用。
有了六种合并运算、两种单图像运算、一种绝对差主力运算,以及用于限定范围的掩码关键字,逐像素算术工具集涵盖了经典机器视觉所需的亮度与通道组合。表面上看类似算术的其余工具,是逐位而非逐值地工作的。