5.25. 查找色块

阈值处理把捕获的帧变成了一张二值掩膜:每个像素要么通过阈值测试,要么不通过。这回答了应用关心的颜色是否出现在场景中,却没有回答出现在哪里——掩膜只是一片由 1 和 0 构成的海洋。下一步就是色块检测:遍历掩膜,找出由通过测试的像素构成的连通区域,并把每个区域作为一个对象返回,这个对象带有位置、大小、朝向以及应用可以据此采取行动的其他属性。

find_blobs() 是完成这一步的主力方法,也是进入 image 模块结果对象世界最常用的入口。跟踪一个彩色小球、沿着地面上画的一条线行进、统计热成像传感器看到了多少个亮点、判断一个蓝色 LED 是亮还是灭——同一个调用就能覆盖所有这些场景。输入会变化(阈值、搜索的区域、施加在结果上的过滤器),但调用模式始终相同。

5.25.1. 基本调用

find_blobs 接受一个阈值列表,并返回一个色块结果对象列表:

thresholds = [(30, 100, 15, 127, 15, 127)]  # LAB threshold for red
blobs = img.find_blobs(thresholds)

for b in blobs:
    img.draw_rectangle(b.rect, color=(255, 0, 0))
    img.draw_cross(b.cx, b.cy, color=(255, 0, 0))

每个阈值元组的形式与传给 binary() 的阈值相同——对于 RGB565 图像是六个条目 (l_lo, l_hi, a_lo, a_hi, b_lo, b_hi)(边界以 LAB 表示),对于灰度图像是两个条目 (lo, hi)。单次调用最多可提供 32 个阈值,这正是 find_blobs() 如此灵活的原因:红、绿、蓝信标可以同时被跟踪,每个阈值各自向返回列表贡献它的色块,而每个色块的 code 属性标识它匹配的是哪个阈值。

上面的 draw_rectangle()draw_cross() 调用为 IDE 预览标注了捕获的帧。色块结果本身就带有 b.rect(4 元组形式的边界框)以及 b.cx / b.cy(整数质心),因此把检测结果绘制回帧中只需两次方法调用。

5.25.2. 结果包含的内容

每个 Blob 都是一个属性元组,把检测器测得的关于该区域的所有信息打包在一起。这些属性分为四组。

边界框与质心组——xywhrectcxcycxfcyf——描述色块的位置。rect 是绘制方法所期望的 (x, y, w, h) 4 元组;cxcy 是以整数像素坐标表示的质心;cxfcyf 是以亚像素浮点坐标表示的质心,当上游标定关心小数位置时很有用。

形状描述符组——pixelsareadensityperimeterroundnesselongationcompactnessrotation——描述色块的外形。pixels 是通过测试的像素数;area 是轴对齐边界框的面积(w * h);density 是两者之比,对于实心矩形它趋近于 1.0,对于细长的对角笔画则趋向于 0.0roundnesscompactness 都从不同的几何视角来衡量色块有多圆(roundness 来自二阶矩,compactness 来自周长与面积之比);为方便起见,elongation1.0 - roundnessrotation 是主轴朝向(以弧度表示),它在细长色块上最为准确,而在近乎圆形的色块上会变得嘈杂(朝向不明确的轴没有明确定义的方向)。

阈值与合并元数据组——codecount——标识匹配的是哪个阈值,以及有多少个源色块被合并进了返回的这个色块中。code 是一个 32 位位图,每匹配一个阈值就置一位(单个阈值得到 code == 1;合并后的多色色块可以置多位);除非 merge=True 把多个检测合并成了一个,否则 count1

角点组——cornersmin_corners——给出色块的旋转几何信息。corners 是从色块轮廓中取出的 (x, y) 极值构成的 4 元组,从左上角开始按顺时针排序;min_corners 是包围色块的最小面积旋转矩形的角点 4 元组。最小面积矩形是紧贴拟合,而轴对齐的 rect 则是与像素网格对齐的宽松拟合。两者都有用处,取决于下游环节需要的是带朝向的框还是普通的框。

一次色块检测,叠加在一张二值阈值掩膜上演示。左侧面板显示一个倾斜的椭圆形掩膜,由通过测试的像素构成。右侧面板显示同一张掩膜,并标注了围绕它绘制的轴对齐边界框、中央用十字标记的质心、一个以椭圆真实角度紧贴它的虚线最小面积旋转矩形,以及一条穿过质心、沿椭圆长轴方向延伸的主轴线。

色块携带轴对齐边界框(rectxywh)、质心(cxcy 或亚像素的 cxfcyf)、最小面积旋转矩形(min_corners 加上 rotation),以及由下文模块级辅助函数计算的可选主/次轴线。

5.25.4. 合并重叠的色块

merge=True 对结果列表进行后处理,把边界矩形相互重叠的色块合并起来。其自然用途是检测这样一个目标:由于镜面高光、阴影线条或物体上光照不均,摄像头把它的颜色看成了多个经阈值处理的区域:一个红色小球可能返回为三四个小的红色色块,而这些色块合在一起才勾勒出整个球。使用 merge=True 后,这三个色块合并成一个大色块,rect 覆盖它们的并集,code 是被合并色块各 code 的按位或(因此多色合并能识别出哪些颜色作了贡献),而 count 报告有多少个源色块被合并。

margin 在进行重叠测试之前扩大或缩小边界矩形。设 margin=2 时,边界矩形彼此相距在 2 像素以内的色块仍会合并;设 margin=-2 时,只有边界矩形至少重叠 2 像素的色块才合并。自然的调参方式是:正 margin 用于处理被阈值拆成相邻碎片的色块;负 margin 用于把紧密成组但相互独立的物体保持分开。

merge_cb 在每对候选合并发生之前运行。回调接收这两个色块,返回 True 表示允许合并,False 表示阻止合并。这是用来复核几何规则会漏掉的合并的合适工具——例如,拒绝合并两个 rotation 角度相差超过某阈值的色块,或拒绝把一个小色块并入一个大得多的色块(如果那个小色块只是斑点噪声的话)。

5.25.5. 投影直方图

x_hist_bins_maxy_hist_bins_max 为每个色块附加可选的投影直方图。投影直方图是沿某一轴方向上通过测试的像素的计数:X 轴直方图统计色块边界框内每一列通过测试的像素,Y 轴直方图则按行统计。两者默认都为零——除非提供了非零的 max,否则不会计算这些直方图,因为否则它们会给每次检测都增加额外的工作量。

计算出来后,这些直方图提供了一个廉价的一维信号,应用可以在其上做进一步分析:检测色块内一条竖直条纹的位置、找出双色目标的分界点、统计长轴方向上出现了多少个间隙。它们以每个 Blob 上的 x_hist_binsy_hist_bins 属性的形式填充。

5.25.6. 额外的几何辅助函数

还有少数几个进一步的几何度量以模块级函数的形式存在,它们接受一个色块并返回所请求的度量值:

5.25.7. 自动学习阈值

色块检测器的好坏完全取决于运行它所用的阈值,而找到目标颜色合适阈值这件工作本身就是一个问题。有两种常见模式可以减少这部分工作。

第一种是在 IDE 中交互式选取:捕获一帧,在目标颜色的一个样例周围拖出一个矩形,让 IDE 的 阈值编辑器 报告它所看到的 LAB 边界。这些边界放入脚本中作为 find_blobs() 的阈值,检测器便准备就绪。

第二种是程序化自动学习:在摄像头上运行的标定例程捕获一帧,对一块已知含有目标的色块取直方图(带 roi=get_histogram()),并用 get_percentile() 从直方图上读出该色块的取值范围。第 5 百分位设定每个通道的下界,第 95 百分位设定其上界,从而忽略两端零星的离群像素。在 RGB565 图像上,一次百分位调用就能一并报告全部三个 LAB 通道,因此这两次调用便产生了 find_blobs() 所期望的六个数字:

h = img.get_histogram(roi=patch)
lo = h.get_percentile(0.05)
hi = h.get_percentile(0.95)
threshold = (lo.l_value, hi.l_value,
             lo.a_value, hi.a_value,
             lo.b_value, hi.b_value)