7.2. 机器学习带来了什么改变

image 模块带有少数几个遗留的检测方法——用于 Haar 级联人脸检测的 find_features(),用于固定瞳孔查找的 find_eye(),用于梯度方向汇总的 find_hog(),以及用于任意关键点的 find_keypoints()find_lbp() 路径。它们全都仍可使用;但它们全都已被机器学习流水线所取代。

7.2.1. 经典的二分:手工设计的汇总,学习得到的决策

一条经典的视觉流水线是分两步的。第一步将原始像素转化为一组精简的数字,这些数字经过精心挑选,用来汇总图像中的内容——不是像素值本身,而是对哪些图案在哪里出现的一种更简短的描述。第二步取这份汇总并做出决策:是不是人脸、是这个物体还是那个、是同一个目标还是不同目标。

这种二分之所以重要,是因为这两步出自不同的作者。第一步由人编写。有人坐下来,判定两个特定矩形之间的亮度差是对眼睛区域的一个良好汇总,判定网格中每个单元格内的主导边缘方向是对站立者轮廓的一个良好汇总,判定每个像素周围的明暗图案是对局部纹理的一个良好汇总。这些选择中的每一个都是一个手写的算法——经过编写、调试并发表。上文列出的遗留方法全都是这一类已成为标准工具的汇总方法:

  • find_features() 通过把若干矩形内部的亮度加总并比较各总和,来汇总图像中的一个窗口。之所以选取这些矩形布局,是因为人脸会呈现出可靠的明暗对比:眉毛对脸颊、眼窝对额头、鼻子对周围皮肤。

  • find_hog() 通过遍历由小单元格组成的网格、并记录每个单元格中占主导的边缘方向,来汇总一张图像。之所以选取这种网格,是因为无论衣着或光照如何,站立者的轮廓都会产生一种可辨识的边缘方向图案。

  • find_lbp() 通过编码每个像素周围的像素中哪些更亮、哪些更暗,来汇总该像素的邻域。之所以选取这种编码,是因为这些“比某像素亮 / 比某像素暗”的图案能够独立于整体光照地捕捉表面纹理。

  • find_keypoints() 在图像中查找角点,并以一种在角点旋转时保持不变的方式描述每个角点周围的区域。之所以选取这种角点加旋转不变的方案,是因为当从不同角度观察同一场景时,相同的角点会再次出现。

一旦某个汇总被手工编写出来,在其之上的一个小型学习步骤就能把这些数字组合成一个决策。人脸检测算法在矩形差值汇总之上加装了一个学习步骤,用带标签的人脸和非人脸图像对其进行训练,以学习哪些差值组合标志着一张人脸。边缘方向汇总则被送入一个在带标签的人物和非人物图像上训练的学习步骤。角点描述符则被送入一个匹配步骤,由它学习赋予每个角点多少权重。这些第二步中的每一个都是一个学习算法——以现代标准衡量是个小型算法,但仍是一个学习算法。

真正重要的是贡献的二分。人贡献了汇总。机器学习了组合。要增加一个新目标,就意味着要编写一个新的汇总。

7.2.2. 神经网络改变了什么

神经网络抹去了这种二分。网络靠前的层做的正是过去手写算法所做的汇总工作——检测边缘、角点、有方向的条形、纹理,恰恰是上文列出的遗留方法各自调校去检测的那些东西——但它们不是手写的。它们是从决策步骤所依据的同一份训练数据中学习得到的,在一次同时调整网络两半部分的训练过程中完成。更深的层做的则是过去手写汇总之上那个小型学习步骤所做的组合工作,同样是学习得到的,也在同一次训练过程中完成。

由谁来设计什么,这一点发生了彻底的变化:

  • 人设计输入——给定尺寸和格式的采集帧。

  • 人设计输出——结果张量的布局(分类时每个类别一个分数,检测时一组框,地标检测时一个关键点网格)。

  • 人提供带标签的训练数据——足够多的目标样本以及足够多的非目标样本,让训练过程有可供学习的素材。

输入与输出之间的一切都由训练过程生成。不再有单独的汇总编写步骤。靠前的层之所以稳定成边缘和纹理检测器,并非因为有人那样写它们,而是因为边缘和纹理检测正是让网络的预测与标签相符的关键。更深的层出于同样的原因稳定成形状和物体检测器。两半部分一起训练,这使得每一层产生的汇总能够恰好是下一层所需要的汇总——而不是手写流水线不得不将就的那些通用汇总。

7.2.3. 与 image 模块组合使用

神经网络流水线仍然通过相同的传感器 API 进行采集,通过相同的 draw_rectangle()draw_circle() 原语绘制结果,并通过相同的 (x, y, w, h) ROI 来限定工作范围。一条典型的流水线会采集一帧,可选地用 find_blobs() 之类的经典检测器找出一个粗略目标、并将其边界框作为 ROI 传给推理,运行推理,再将返回的检测结果标注回原始帧。经典原语是底层基础;网络则是中间新增的步骤。