5.5. 区域与掩膜

默认情况下,image 模块中的每个操作都会处理其源图像的每一个像素。这是最容易描述的行为,当算法的工作确实需要覆盖整个帧时——例如均匀的颜色校正、全局直方图、用于传输的编码处理——这种行为也是正确的。但实际上大多数算法只需要关注其中的一部分。监视彩色标记的色块跟踪器只关心标记可能出现的那部分场景,而不是它身后的墙。形态学清理处理只有在前一阶段标记为候选的像素上运行才是安全的。人脸检测器可能只在较粗略的检测器已经缩小的边界框内运行。image 模块通过两种将操作限定到像素子集的机制来支持这类工作:矩形的感兴趣区域和二值掩膜。它们可以自由组合,几乎每个会处理像素的方法都接受其中之一——或两者——作为关键字参数。

5.5.1. 感兴趣区域

感兴趣区域是由坐标页中介绍的 (x, y, w, h) 四元组所命名的一个像素矩形。该接口上大约有三十个方法接受 roi 关键字参数;当提供该参数时,操作仅在该矩形内的像素上运行,而保持图像的其余部分不受影响。当 roiNone 或被省略时,操作会在整幅图像上运行——这与传入 roi=(0, 0, width, height) 的效果相同。

在代码中,该关键字与操作所接受的其他参数并列放置:

# Compute a histogram over a centred crop of the image.
h = img.get_histogram(roi=(64, 64, 128, 128))

ROI 带来的第一个好处是误报控制。只查看桌面的颜色跟踪器永远不会被路过的衬衫触发;只在定义的工作区域内运行的边缘检测器永远不会报告摄像头支架本身的边缘。将搜索区域缩小到算法真正关心的那部分场景,是流水线能为自身可靠性所做的最廉价的改进。

它们带来的第二个好处是由粗到精的流水线。检测结果对象——一个 blob、一个 rect、一个 apriltag 等等——以与 roi 所接受的相同的 (x, y, w, h) 四元组形式暴露其边界框。因此,粗略的第一阶段可以返回一个边界框,该框直接放入下一阶段的 roi,第二阶段便在更窄的区域上运行。每一次渐进的收窄既加快了下一阶段的速度,也使其结果更可靠,因为搜索空间已经被过滤过了。

5.5.2. 二值掩膜

当感兴趣的区域是轴对齐时,矩形是合适的形式。当它不是时——例如曲线区域、非凸区域,或某个较早阶段归类为“匹配”的那些像素——就必须告知操作将自身限定到任意的像素模式上。实现这一点的机制是二值掩膜:一个独立的、与源图像尺寸相同的 Image,用作逐像素的开/关开关。掩膜中非零的像素表示“包含对应的源像素”;零像素表示“不处理该源像素”。

掩膜通常是 BINARY 图像——正是为此目的而存在的每像素一比特的格式——但任何单通道图像都可以使用,因为使用方会将任何非零值视为开启。

滤波、阈值和算术方法都接受 mask 关键字参数。每种方法的形式都相同:一个单独分配的、与源图像尺寸相同的二值图像,传入即可。

ROI 与掩膜可以组合。同时传入两者,操作就只在既位于 ROI 内 在掩膜中开启的像素上运行。这两种机制为应用代码提供了相互独立的调节手段——一个用于矩形的感兴趣区域,一个用于其中任意的模式——而不会让任一形式继承另一形式的约束。

一个表示图像的小网格。一个 虚线矩形画在网格的 中上部,标注出 ROI:只有该矩形内的像素 会被考虑。在 ROI 内, 一组大致呈圆形的填充单元格 标注出掩膜:只有这些填充的单元格 才会被真正修改。其余的 单元格被轻微着色,以表示它们 不受影响。

ROI 将操作限定到一个轴对齐的矩形。掩膜则进一步将其收窄到任意的像素模式。两者可以组合:只有既位于 ROI 内 在掩膜中开启的像素才会被修改。

5.5.3. 构建掩膜

有三个 Image 方法通过将所选区域之外的像素清零,就地构建常见的掩膜几何形状:

每个方法都接受 (x, y, w, h)(用于矩形和椭圆)或 (x, y, radius)(用于圆形)。不带参数调用它们中的任何一个,都会将几何形状居中并将其尺寸调整为填满图像,当目标是一个除了四角什么也不遮挡的简单全图椭圆或圆形时,应用就会采用这种形式。

mask = image.Image(img.width(), img.height(), image.BINARY)
mask.clear()              # start from all zeros
mask.mask_ellipse()       # centred, full-size oval

有意思的掩膜很少仅由 mask_* 方法生成。它们来自流水线的较早阶段:阈值处理产生一幅二值图像,其非零像素标记出匹配项,正是适合送入下一阶段 mask= 参数的形式。形态学清理处理在不改变其形式的情况下细化该掩膜。任何最终成为单通道图像的东西本身都是有效的掩膜。

5.5.4. 操作如何修改图像

在前几页的每个代码片段中都能看到一种模式——操作返回同一个 img 以便链式调用——值得明确地提取出来,这样每当引入新方法时就不必再重述一遍。Image 接口上出现了三类方法,每一类对源图像的处理方式各不相同:

  • 操作型方法就地修改源的像素,并返回同一图像以便链式调用。绘制、算术、阈值和滤波这几类方法都以这种方式运作。img.gaussian(1) 会模糊 img 并返回同一个 img;重新赋值——img = img.gaussian(1)——无害但没有必要。

  • 转换型方法默认与操作型方法一样就地运作,但它们接受 copy=Truecopy_to_fb=True,以便在需要保留源图像时分配一幅单独的结果图像。格式转换和几何复制是这一类的主要成员。

  • 检查型方法读取像素并返回一个结果对象——检测到的特征列表、直方图、一组统计数据——而完全不修改源图像。

这种三分法在整个接口上是一致的。知道一个方法属于哪一类,就能告诉应用对一次调用应该预期什么:源的像素是否会完好保留、是否会分配一幅单独的结果图像,以及返回值是源本身还是别的东西。