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 都是一个属性元组,把检测器测得的关于该区域的所有信息打包在一起。这些属性分为四组。
边界框与质心组——x、y、w、h、rect、cx、cy、cxf、cyf——描述色块的位置。rect 是绘制方法所期望的 (x, y, w, h) 4 元组;cx 和 cy 是以整数像素坐标表示的质心;cxf 和 cyf 是以亚像素浮点坐标表示的质心,当上游标定关心小数位置时很有用。
形状描述符组——pixels、area、density、perimeter、roundness、elongation、compactness、rotation——描述色块的外形。pixels 是通过测试的像素数;area 是轴对齐边界框的面积(w * h);density 是两者之比,对于实心矩形它趋近于 1.0,对于细长的对角笔画则趋向于 0.0。roundness 和 compactness 都从不同的几何视角来衡量色块有多圆(roundness 来自二阶矩,compactness 来自周长与面积之比);为方便起见,elongation 即 1.0 - roundness。rotation 是主轴朝向(以弧度表示),它在细长色块上最为准确,而在近乎圆形的色块上会变得嘈杂(朝向不明确的轴没有明确定义的方向)。
阈值与合并元数据组——code、count——标识匹配的是哪个阈值,以及有多少个源色块被合并进了返回的这个色块中。code 是一个 32 位位图,每匹配一个阈值就置一位(单个阈值得到 code == 1;合并后的多色色块可以置多位);除非 merge=True 把多个检测合并成了一个,否则 count 为 1。
角点组——corners、min_corners——给出色块的旋转几何信息。corners 是从色块轮廓中取出的 (x, y) 极值构成的 4 元组,从左上角开始按顺时针排序;min_corners 是包围色块的最小面积旋转矩形的角点 4 元组。最小面积矩形是紧贴拟合,而轴对齐的 rect 则是与像素网格对齐的宽松拟合。两者都有用处,取决于下游环节需要的是带朝向的框还是普通的框。
色块携带轴对齐边界框(rect、x、y、w、h)、质心(cx、cy 或亚像素的 cxf、cyf)、最小面积旋转矩形(min_corners 加上 rotation),以及由下文模块级辅助函数计算的可选主/次轴线。¶
5.25.3. 过滤搜索¶
捕获的帧中通常会包含一些像素,它们因应用所关心对象以外的原因而匹配了阈值:镜面高光、远处的背景物体、恰好落在 LAB 范围内的图像噪声像素。find_blobs() 的关键字参数就是第一道防线。
roi 把搜索限制在帧的某个区域内,方式与其他所有 image 模块方法一样。若应用知道目标只可能出现在视场的下半部分,便可传入 roi=(0, h//2, w, h//2) 并忽略上方的一切;省下的时间会回报到帧率上。
area_threshold 和 pixels_threshold 都用于过滤掉小到不值得关注的色块。area_threshold 丢弃边界框面积像素数小于该值的色块(适合过滤零散噪声);pixels_threshold 丢弃通过测试的像素数小于该值的色块(适合过滤那些面积大但稀疏的色块,比如一处经过阈值处理的点画图案,零星地有一两个像素在各处匹配)。两者的默认值都是 10;对于一个横跨几厘米的前景目标,把它们调高到几百便能甩掉每一粒小噪声。
x_stride 和 y_stride 设定扫描器在寻找起始追踪色块时所采取的像素步长。步长不是追踪分辨率——追踪始终以单像素的细致程度沿着真实的色块边界走——但它控制扫描找到起始像素的速度。当已知色块较大时(距摄像头一英尺、拳头大小的彩色目标,轻松横跨上百像素),x_stride=4, y_stride=4 能把扫描时间缩短为十六分之一,而在检测上几乎没有实际损失。当色块较小时(远处的 LED 信标,仅横跨几像素),步长必须保持在 1,以免完全跨过它们。invert 翻转阈值测试:匹配变为不匹配,例程返回的是未通过像素构成的色块。
threshold_cb 是一个 Python 回调,在阈值处理之后、最终结果列表构建之前对每个色块调用。回调接收该色块,返回 True 表示保留,False 表示丢弃。这里正是对关键字参数未直接暴露的属性施加任意 Python 级过滤器的地方——比如最小密度、特定的旋转范围、合并后自定义的 code 位组合。关键字参数是原生代码中的过滤器,运行得很快;回调在 Python 中运行,较慢,但在能表达的内容上不受限制。
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_max 和 y_hist_bins_max 为每个色块附加可选的投影直方图。投影直方图是沿某一轴方向上通过测试的像素的计数:X 轴直方图统计色块边界框内每一列通过测试的像素,Y 轴直方图则按行统计。两者默认都为零——除非提供了非零的 max,否则不会计算这些直方图,因为否则它们会给每次检测都增加额外的工作量。
计算出来后,这些直方图提供了一个廉价的一维信号,应用可以在其上做进一步分析:检测色块内一条竖直条纹的位置、找出双色目标的分界点、统计长轴方向上出现了多少个间隙。它们以每个 Blob 上的 x_hist_bins 和 y_hist_bins 属性的形式填充。
5.25.6. 额外的几何辅助函数¶
还有少数几个进一步的几何度量以模块级函数的形式存在,它们接受一个色块并返回所请求的度量值:
image.get_solidity()返回色块的实心度——像素数除以凸包的面积。实心填充的区域接近1.0;带有凹陷的色块(马蹄形、张开手指的手)则远低于此值。image.get_convexity()返回凸度——凸包周长除以色块周长。完全凸的色块为1.0;锯齿状或有缺口的色块则更低。image.get_major_axis_line()和image.get_minor_axis_line()返回沿色块主轴和次轴的Line对象,它们由旋转后的最小面积矩形推导而来。image.get_enclosing_circle()返回一个包围色块的Circle——当下游环节想要一个圆来绘制或进行比对时很有用。image.get_enclosed_ellipse()返回一个内接于色块最小面积矩形的椭圆的 5 元组(cx, cy, rx, ry, rotation)。这些值可直接送入draw_ellipse()。
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)