传统物体检测

1. 特征
特征(Feature) 描述了一个物体的特性,而对于图像来说,这一切都归结为强度和强度的梯度,以及这些特征如何捕捉物体的颜色和形状。
哪些特征更重要则取决于待检测对象的外形。在大多数应用中都需要综合使用这些特征,才能获得最佳效果。
2. 模板匹配
图像中可以得到的最简单的特征是颜色的值。例如,给出一辆汽车的图像,假设现在想检测一张新的测试图像的某些区域是否包含一辆车应该怎么做?利用已知的汽车图像,就能轻松地发现汽车图像和测试区域之间的区别,可以发现下图中的这种区别很大。
这意味着把对应区域的颜色值,统计所有的区别并将其与阈值进行比较。也可以计算汽车图像和测试区域之间的相关性并检验这种相关性是否足够高-------这种通用的方法称为模板匹配,将已知的图像作为模板或模型,并将其与测试图像中的区域进行匹配。
模板匹配在较为有限的场景中有效,该方法对于检测外观变化不大的物体效果较好,但由于现实世界的大多数物体在外形、方向、大小等方面都不同,因此这种技术的效果并不好。举个例子,假设模板是左边标记为“Template”的汽车映像。如果其他汽车(A - D)位于图像的某个位置,使用模板匹配,哪一个是匹配的?答案是只有 B,虽然 C 在外形和方向上是一模一样的。
3. 颜色直方图
从上面模板匹配方法可知,模板匹配需要依靠特定顺序排列的原始颜色值,而它们在真实世界中的变化较大,这种技术的效果并不好。因此,需要采用一些对外观改变较为稳定的变换。
有一种通过计算图像中颜色值直方图的变换,当使用已知物体的直方图与测试图像的区域比较时,有类似颜色分布的区域将显示为高度匹配。这种方式摆脱了对物体结构的依赖
,也就是说,对于特定的像素排列顺序不再那么敏感。因此,在不同朝向或方向上略有不同的物体仍然可以被匹配到。


也可以通过对直方图进行归一化来适应直方图之间的差异和不同的图像大小。但需要注意的是,现在完全依靠颜色值的分布进行匹配也可能匹配到某些没有目标物体的区域。


下图为两幅完全不同图像的颜色直方图特征。第一个图像是一辆红色的车,第二个是一辆蓝色的车。红色轿车的颜色直方图显示在第一行,蓝色轿车的颜色直方图显示在下面的第二行中。在这里,可以看到每个RGB通道有
8
8
8 个 bin。如果有必要,我们可以仅根据直方图的差异来区分这两幅图像。正如预期的那样,红色车的图像在 R Histogram 1
(红色通道)中比蓝色车的 R Histogram 2
中有更大的 bin 值强度。相比之下,在 B Histogram 2
(蓝色通道)中,蓝色车比红色车的 B Histogram 1
特征具有更大的 bin 值强度。
通过颜色的强度和范围来区分图像可以帮助我们区分汽车图像和非汽车图像:
然而,无论是直接应用原始颜色还是通过它们的直方图,仍然不能解决物体种类相同但颜色不同的问题。也就是说,依然无法识别同种物体在颜色上的差异。以下图为例,这个图像的颜色值在 RGB 颜色空间是这样分布的:
将红色、绿色和蓝色强度作为三个坐标轴。在本例中,红色和蓝色汽车的像素聚集成两个较为独立的组。这时虽然我们可以想办法通过 RGB 值区分不同组,然而,当引入更多颜色时问题马上就变得复杂得多。
那么从这些颜色值中,能否获得汽车共有的属性,并利用这些属性将汽车与图像的其他内容进行区分?
一种想法是,与背景相比,汽车颜色的饱和度更高,而一般而言,背景饱和度较低,看上去更苍白一些。于是现在来看看这张图的 HSV 空间像素分布:
在这种图像上的汽车像素似乎都在同一饱和度值的平面上。这就比较说得通了,无论汽车的颜色或色相怎样,在饱和度域内,汽车的外观往往会更加突出。
不过这并不适用于所有图像。需要做的是,取一批不同的图像,并研究它们的像素值在各种色彩空间内的分布方式(RGB/HSV/LUV)。然后,需要验证在该色彩空间内的某些维度中汽车是否能与其他物体区分开来。可以多尝试一些不同的色彩空间,看看哪个空间能提供最有用的特征。
4. 空间分拣
在前面我们看到,模板匹配并不是查找车辆的特别鲁棒的方法,除非确切地知道目标对象的样子。然而,在搜索汽车时,原始像素值仍然是非常有用的。
虽然包含全分辨率图像的三个颜色通道可能很麻烦,但可以在图像上执行空间分拣(spatial binning),并仍然保留足够的信息来帮助查找车辆。
在下面的例子可以看到,即使是在
32
∗
32
32 * 32
32∗32 像素的分辨率下,汽车本身仍然可以被肉眼清晰地识别出来,这意味着在这个分辨率下,相关的特征仍然被保存下来。
5. 梯度特征
到目前为止一直在对颜色值进行操作和转换,但是颜色值只能表示对象外观的某一部分。当你有一类颜色可能不同的对象时,梯度或边缘等结构的顺序可能给出更稳定的特征。实际上,某点上特定方向的梯度可以捕捉到一些大致的形状。
我们来看一些简单形状。下图是一个三角形及其对应的梯度图像。如果梯度划分为一些小网格并将每个小网格排列成一个行向量就能得到三角形的特征。类似地,如果有一个圆,也能得到另一种不同的特征。
理想情况下,一个形状的特征应有足够的灵活性以适应不同的方向和大小,不过这个特征也应该具有独特性用来区分不同形状。直接使用梯度值的一个问题是,会使这些特征对于方向、大小过于敏感。
6. HOG 特征
来看一个更真实的图像。下图是一辆汽车的 64 ∗ 64 64*64 64∗64 像素图像,现计算每个像素的梯度幅值和方向。我们不会逐像素使用这些梯度,而是统计每个小的单元格中的梯度。假设每个单元格大小为 8×8 像素,在每个单元格内有神奇的事情会发生。


可以计算单元格内 64 64 64 个梯度的直方图,梯度样本按照方向被分布到直方图的 9 个柱 (orientation bins) 中然后求和。样本规模不大的时候通常不需要很高的精度,所得到的直方图看起来大概是下面中间图这样。
还有个更好的可视化方法,就是在把各个梯度按照方向分类放置,从而得到右图中所示的各方向矢量大小不同的星形。最长矢量的方向代表这个单元格的主梯度方向。



注意,这个直方图并不是对每个方向梯度数量进行求和,需要计算的是所有像素梯度的矢量和。因此,梯度幅值越大,对该方向对应的直方图柱的贡献就越大,从而减少了例如噪声引入的随机梯度影响。换句话说,图像中的每个像素根据其梯度方向,对直方图中对应方向的柱进行投票。但是每一票的强度/权重,取决于该像素梯度的幅值。当对所有单元格投票后,开始看到原始结构轮廓的出现,正如此前采用更简单的形状演示的那样。这个轮廓可以用作给定形状的特征,被称为梯度方向直方图 或 HoG 特征。
这种方法的主要优势在于,可以在形状发生小幅变化同时保持形状特征的独特性。可以通过调整各个参数来改变特征对于小幅变化的适应程度。这些参数包括 柱的数量
、单元格格数
、单元格大小
、以及是否在单元格之间增加重叠部分
等。在实际应用中,我们可以使用许多其他增强功能,包括对每个单元格内的梯度幅值进行归一化处理。
7. 特征融合
检测目标时并非只能使用一种特征,基于颜色和基于形状的特征可以综合使用,因为在捕获的目标信息中里这两者互为补充。实际上,特征越多,设计出的检测系统更为稳定。
不过,这些特征需要小心地使用。例如,假设我们使用矢量化的 HSV 值作为一个输入特征,它包括 a a a 个元素;然后使用 HoG 作为另一个特征,它包括 b b b 个元素。要合并这两种特征,最简单办法把两个矢量连起来,将 HSV 和 HoG 合并为一个长度为 a + b a + b a+b 个元素的向量。


如果将这个向量看作一个简单的柱状图,你可能注意到在颜色特征和梯度特征的幅值不同,这是因为它们代表的绝对数量不一样。归一化步骤可以防止某一个特征在后续处理中占主导地位。另外,有时候某种类型的单元数量可能比另外一种类型多,这可能不会造成问题,不过最好看一下,组合的特征向量中是否存在冗余。例如,可以应用决策树分析各种特征的相对重要性并抛弃作用不大的特征。
8. 搭建分类器
现在,已经学会了如何从一张图像中提取合适的特征,下面要做的就是如何使用这些特征来检测汽车。一个经典的方法是,首先设计一个能区分汽车图像与非汽车图像的分类器,然后在每一帧图像中逐个区域运行这个分类器,被分类器认为是汽车的区域就是我们所需的检测结果。要想使这种方法有效运行,必须通过训练分类器使它能够区分图像中的汽车与非汽车物体。
8.1 数据标注
训练任何分类器都需要有标签数据,而且是大量的有标签数据。如果想要区分的两个类别是图像中的汽车和非汽车物体,因此同时需要这两种类别的数据。如果只有视频本身,就需要裁剪出这些区域并按比例调节为统一的图像大小。
理想情况下,需要使用平衡的数据集,其中汽车和非汽车图像的数量应大致相等,否则,分类器有可能试图将所有对象都预测为数据量较大的类别。
有一些处理不平衡数据集的技巧:例如,可从数据量较小的类别中复制一些样本以平衡数量。对于车辆分类,如果没有足够的非汽车图像,可以简单地从视频中截取更多非汽车的物体。
一旦有了足够大的数据集时,需要将其分成两个子集:训练集
和测试集
。顾名思义,在训练分类器时将只使用训练集中的图像,然后使用测试集来检验分类器对于新样本的识别能力。为了避免数据中任何可能的顺序影响,在将数据集划分为训练集和测试集时,需要随机抽样或者打乱数据集。分别对于训练集和测试集而言,也应该尽可能保证两个子集内汽车和非汽车图像数量的平衡。
这些预处理可能看起来工作量很大,但是如果机器学习算法的输入的是垃圾,那么输出的一定也是垃圾。因此,对于提供给算法的数据需要特别小心。
8.2 数据预处理
数据准备步骤有:
- 避免因数据排序而出现的问题:
∙ \bullet ∙ 数据的随机洗牌; - 估计模型对新数据的泛化:
∙ \bullet ∙ 将数据拆分为训练集和测试集; - 避免您的算法简单地将所有内容分类为属于多数类:
∙ \bullet ∙ 准备一个平衡的数据集,即,具有的正负示例一样多,或在多类别情况下,每个类的案例数量大致相同; - 避免单个特征或特征集主导分类器的响应:
∙ \bullet ∙ 特征归一化,通常为零均值和单位方差。
8.3 训练分类器
训练阶段大致包括:
1.从训练集中提取每个样本的特征;
2.将这些特征向量与对应的标签一起提供给训练算法。训练算法初始化一个模型;
3.使用特征向量和标签调整模型的参数,通常这一步会涉及迭代过程。
迭代时,每次向分类器提供一个或多个样本,分类器预测其标签。这些预测标签与真值之间的误差被用作修改模型参数的依据。当这一误差低于某个阈值时,或迭代次数已达上限,可以认为该模型已经经过了充分训练。
测试集上的误差通常要比在训练集上的大,这两种误差通常都会随着对模型的进一步训练而降低。然而必须小心的一点是,如果一直训练模型,在超过某个临界点后,训练误差可能会继续降低,而测试误差反而开始上升,这种情况称为过拟合-----该模型会很好地拟合训练数据,但是却无法对新数据进行泛化。
还有一点,就是分类器的选择。需要进行一些实验才能知道,哪种分类器对于给定的问题更有效。支持向量机对 HoG 特征很有效,不过可以自由选择最终使用的分类器,甚至可以综合使用多种分类器。
8.4 交叉验证
交叉验证法 基本思想就是将原始数据(dataset)进行分组,一部分做为训练集来训练模型,另一部分做为测试集来评价模型。
为什么用交叉验证法?
- 交叉验证用于评估模型的预测性能,尤其是训练好的模型在新数据上的表现,可以在一定程度上减小过拟合;
- 可以从有限的数据中获取尽可能多的有效信息。
一种比较经典的交叉验证法叫做 k-折交叉验证(k-fold cross validation)
。
如果
K
=
5
K=5
K=5,那么我们利用五折交叉验证的步骤就是:
3. 将所有数据集分成
5
5
5 份;
4. 取其中一份做测试集,用其他四份做训练集训练模型,之后计算该模型在测试集上的
M
S
E
i
MSE_i
MSEi
5. 将
5
5
5 次的
M
S
E
i
MSE_i
MSEi 取平均得到最后的
M
S
E
MSE
MSE。
C
V
=
1
k
∑
i
=
1
k
M
S
E
i
\mathrm{CV}=\frac{1}{k} \sum_{i=1}^{k} \mathrm{MSE}_{i}
CV=k1i=1∑kMSEi
9. 滑动窗口
现在已经决定好从每幅图像中提取哪些特征并且已经采用有标签记数据训练了分类器,下一步是使用一种搜索目标方法。
我们现在可以考虑图像局部或子区域并在每个子区域上运行分类器,查看它是否包含要检测的物体。接下来要做的是,部署一种滑窗技术。需要以一定步长进行窗口滑动,遍历整个图像,并在每个窗口提取对分类器训练的相同特征。这时需要运行分类器,在每个窗口中给出预测。
要实现滑动窗口搜索,需要决定:
- 想要搜索的窗口大小;
- 想要开始和停止搜索的图像位置;
- 希望窗口重叠的程度。
现在尝试一个例子,看看在特定的图像大小,窗口大小和重叠情况下,会搜索多少个窗口:假设有一个
256
∗
256
256*256
256∗256 像素的图像,您想要搜索大小为
128
∗
128
128*128
128∗128 像素的窗口,每个窗口在垂直和水平维度上相邻窗口之间的重叠都为
50
50%
50。滑动窗口搜索将如下图所示:
所以在这个例子中,总共搜寻
9
9
9 个窗口。
9.1 多尺度窗口
一般来说不会知道正在搜索的物体在图像中的大小,所以多尺度搜索是很好的解决方法。一种合理的做法是,在目标可能出现的地方预设一个最小和最大尺寸,也设置一个两者间的中间尺度来进行搜索。要小心的是,这样一来,搜索窗口的总数量会迅速增加,这意味着算法运行会变慢。
我们此时的目的在于寻找车辆,因此,最好将搜索区域局限在车辆可能出现的位置上。此外,对于目标尺寸越远的车辆看起来越小。因此,小尺度搜索可以局限在图像中一条狭长的条状区域中。


9.2 Hog子采样窗口搜索(Hog Sub-sampling Window Search)
现在来探索一种更有效的滑动窗口方法,这种方法允许我们只需要提取一次 Hog 特征。
对于一组预先确定的窗口大小(由scale参数定义),find_cars只需要提取hog特征一次,然后可以进行次采样以获得其所有覆盖窗口。每个窗口都由一个影响窗口大小的缩放因子定义。可以在图像的不同区域设置比例因子(例如,靠近地平线处小,中心处大)。
10. Multiple Detections & False Positives
现在已经可以进行图像搜索和特定物体检测,但是现在的分类器并不完美,在某些情况下,同一辆汽车会被多次重复检测到,甚至在没有汽车时却提示检测到了汽车-------这些被称为 重复(duplicate)
和 误报(false positive)
,需要将它们滤除。采用合适的方式处理重复检测并排除误报,就能定位车辆在道路上的具体位置。
但是,判断哪里没有车辆也同样重要。如果误报没有被筛选出去,会导致车辆在正常情况下采取紧急刹车等动作,从而会造成事故。
为了避免撞到其他汽车,需要尽可能准确地估计检测到的车辆的位置和大小。这意味着,对同一辆汽车无论进行了单次或重复检测都需要严格确定每辆汽车边界,这些边界框最终会用于路径规划或运动控制算法,从而与其他车辆保持一定距离。
10.1 热图(heat-map)
这里是使用上述算法得到的六个连续帧,展示了分类器报告 positive 检测的所有边界框。可以看到,两辆车都存在重叠检测,在其中两帧中,在左边的护栏上发现了 false positive 检测。现在将从这些检测构建一个热图,以便组合重叠检测并删除 false positive。
要制作热图,只需为分类器报告 positive 检测的窗口内的所有像素添加
heat
(+=1)。上述图像的热图是这样的:
11. 总结
现在来总结一下整体的跟踪流程:
- 首先,需要决定使用哪些特征。这需要进行一些实验才能确定哪些组合的效果最佳;
- 接下来,需要选择并训练分类器。线性支持向量机或许是同时获得理想速度和精度的最佳选择,但也可以尽可能大胆尝试其他分类器;
- 然后,在每帧视频中,使用滑动窗口技术来搜索图像。当分类器返回正确检测时需要记录该窗口的位置。不过要记住,尽量减少搜索窗口的数量,比如,不需要在天空和树梢上搜索汽车;
- 可能会在重叠的窗口或者在不同尺度下中检测到同一辆车。对于重复检测,需要将车辆位置定位到所有重叠窗口的质心;
- 需要过滤掉 false positive 物体,也就是在前一帧中出现 却没有在下一帧中出现的车辆。
一旦拥有了一个高置信度的检测,就可以记录汽车的中心是如何逐帧移动的,并最终预测出它在后续每帧中出现的位置。
11. 传统 vs 深度学习方式
前面的识别内容,还可以采用深度神经网络的方法来解决。那么,使用深度神经网络进行图像检测,与传统计算机视觉进行比较,两者之间的区别在哪?
在之间的步骤中,我们手动提取颜色和梯度特征,这个过程也被称为特征工程
。然后训练一个分类器并搜索图像中的对象。
采用深度神经网络,所有这些任务在一步之内就可以完成。在训练深度神经网络的过程中,可以得到一些最优的特征集来定位要搜索的物体,甚至可以训练一个网络来定位目标,无论目标在哪个位置。也就是说,之前手动做的一系列任务,实际上都可以由神经网络自动完成。
用于对象检测的传统计算机视觉管道通常由单独的处理阶段组成,例如,特征提取、空间采样和分类。这些阶段可以应用不同的算法和技术,并且每个阶段的参数通常由手动调整。
相比之下,为对象检测设计的深度神经网络可以使用复杂的互连架构来执行这些任务,其中阶段并不明显。神经网络的较低层,即更接近输入数据的层,通常执行一些等效的特征提取,而较高的层可以同时定位和分类以产生检测。
手动特征提取在图像搜索目标等工作量会很大,但是神经网络就像一个黑盒子,很难直观地理解它的内部工作原理。在这里了解到的更传统的方法,可以对神经网络的内部流程有个大致概念。
映射颜色空间代码
import cv2
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def plot3d(pixels, colors_rgb,
axis_labels=list("RGB"), axis_limits=((0, 255), (0, 255), (0, 255))):
"""Plot pixels in 3D."""
# Create figure and 3D axes
fig = plt.figure(figsize=(8, 8))
ax = Axes3D(fig)
# Set axis limits
ax.set_xlim(*axis_limits[0])
ax.set_ylim(*axis_limits[1])
ax.set_zlim(*axis_limits[2])
# Set axis labels and sizes
ax.tick_params(axis='both', which='major', labelsize=14, pad=8)
ax.set_xlabel(axis_labels[0], fontsize=16, labelpad=16)
ax.set_ylabel(axis_labels[1], fontsize=16, labelpad=16)
ax.set_zlabel(axis_labels[2], fontsize=16, labelpad=16)
# Plot pixel values with colors given in colors_rgb
ax.scatter(
pixels[:, :, 0].ravel(),
pixels[:, :, 1].ravel(),
pixels[:, :, 2].ravel(),
c=colors_rgb.reshape((-1, 3)), edgecolors='none')
return ax # return Axes3D object for further manipulation
# Read a color image
img = cv2.imread("img.png")
# Select a small fraction of pixels to plot by subsampling it
scale = max(img.shape[0], img.shape[1], 64) / 64 # at most 64 rows and columns
img_small = cv2.resize(img, (np.int(img.shape[1] / scale), np.int(img.shape[0] / scale)), interpolation=cv2.INTER_NEAREST)
# Convert subsampled image to desired color space(s)
img_small_RGB = cv2.cvtColor(img_small, cv2.COLOR_BGR2RGB) # OpenCV uses BGR, matplotlib likes RGB
img_small_HSV = cv2.cvtColor(img_small, cv2.COLOR_BGR2HSV)
img_small_rgb = img_small_RGB / 255. # scaled to [0, 1], only for plotting
# Plot and show
plot3d(img_small_RGB, img_small_rgb)
plt.show()
plot3d(img_small_HSV, img_small_rgb, axis_labels=list("HSV"))
plt.show()
执行该代码,可以得到与上面相似的效果: