C# 中的 Haar 特征对象检测






4.97/5 (133投票s)
描述了在 2001 年如何通过一些巧妙的想法实现实时人脸检测。
目录
引言
2001 年,Viola 和 Jones 提出了第一个实时对象检测框架。这个框架在 2001 年的硬件上能够实现实时运行,部分应用于人脸检测。而大家熟知的成果——人脸检测现在几乎是市面上所有数码相机和手机的标配功能。即使这些设备可能并不直接使用他们的方法,但人脸检测设备的普及程度肯定受到了他们工作的影响。
现在,来到了这个框架的一个更有趣的地方。要在大型场景中定位一个对象,该算法只需使用滑动窗口进行详尽搜索,采用不同的尺寸、纵横比和位置。为什么这样的方法能如此高效呢?
而这正是作者贡献发挥作用的地方。
本文旨在向读者介绍 Viola-Jones 对象检测框架,并指导其在 Accord.NET Framework 中的实现。提供了一个示例应用程序,供感兴趣的读者尝试图像检测,并了解如何使用该框架进行此操作。
背景
Paul Viola 和 Michael Jones 的贡献是三方面的。首先,他们专注于创建一种基于若干弱分类器的组合的分类器,这些分类器基于极其简单的特征来检测人脸。其次,他们修改了当时标准的分类器组合算法,生成了即使花费一些时间才能实际检测到图像中的人脸,但能极其快速地拒绝不包含人脸的区域的分类器。第三,他们使用了一种精妙的图像表示方法,可以有效地预先计算出运行其分类器所需的所有昂贵操作。
简易特征
大多数时候,当我们要创建一个分类器时,必须决定考虑哪些特征。特征是某个特性,它将为决策过程带来足够的信息,以便分类器能够做出决策。例如,假设我们正在尝试创建一个分类器来区分一个人是否超重。直接选择的特征将是人的身高和体重。在这种情况下,发色等特征就不会是非常有用的特征。
所以,我们回到 Viola-Jones 分类器选择的特征。下面显示的特征是 Haar 类矩形特征。虽然不明显,但它们代表了图像中两个或多个相邻矩形区域的强度(灰度)差异。
例如,考虑将其中一个特征放置在图像上,如下面的 Lena Söderberg 的图像所示。该特征的值将是该矩形白色区域所有强度像素的总和,加上蓝色区域所有像素的总和,然后计算它们的差值。希望通过序列右侧的图像,可以清楚地看出为什么这些矩形特征在检测人脸方面会很有效。由于人脸阴影的均匀性,某些特征似乎与人脸非常匹配。
上图也给出了搜索算法工作方式的 ধারণা。它从一个大(或小)窗口开始,并对图像进行详尽搜索(即,通过将窗口向右移动一些点,并在行的末尾向下移动)。扫描完成后,它会缩小(或增大)该窗口,重复此过程。
注意力级联
如果检测器不够快,这种方案很可能无法实时运行。关键在于检测器在拒绝没有前景的窗口方面非常快速。因此,它可以快速确定某个区域不包含人脸。当它对某个区域不太确定时,它会花费更多时间尝试检查它是否是人脸。当它最终放弃拒绝时,它只能得出结论:它是人脸。
那么,检测器是如何做到这一点的呢?
它通过使用注意力级联来实现。级联是一种组合分类器的方式,其中一个给定的分类器只有在它之前的所有分类器都已处理后才会被处理。在级联中,只有当目标对象未被前一个检测器拒绝时,才允许其在级联中继续。
Viola-Jones 方法使用的分类方案实际上是增强分类器的级联。级联中的每个阶段本身都是一个强分类器,因为通过以某种方式组合一系列弱分类器,它可以获得非常高的拒绝率。
弱分类器是其性能仅比随机猜测好一点的分类器。这意味着它比抛硬币并决定图像中是否存在某个对象要好一点点。然而,通过将许多弱分类器的决策组合成一个加权决策,可以构建一个强分类器。这种组合多个弱学习器以形成强学习器的过程称为提升。例如,可以使用 AdaBoost 学习算法的许多变体来学习这样的分类器。
在 Viola 和 Jones 提出的方法中,每个弱分类器最多只能依赖一个 Haar 特征。有趣的是,这就提供了一个未被讲述的问题的解决方案:Viola 和 Jones 已经为其算法申请了专利。因此,如果要在商业上使用它,您将需要从作者那里获得许可,可能需要支付费用。为了扩展检测器,OpenCV Haar 特征检测器的原始实现者 Rainer Lienhart 博士提出添加两种新类型的特征并将每个弱学习器转化为一棵树。这个后一个技巧,除了有助于分类之外,也足以摆脱原始方法的专利保护。
好了,到目前为止,我们有了一个潜在的快速拒绝误报的分类系统。但是,请记住,为了完全扫描场景,该分类器必须在图像的多个缩放区域上运行。计算强度差异也将非常耗时(想象一下为每个特征反复求矩形区域的总和,并在每次缩放时重新计算)。如何才能让它更快呢?
积分图像表示
缓存。这通常是我们编码时每天都在进行的优化。就像将变量的输出从循环中缓存出来,而不是每次都重新计算一样。我认为大多数人都熟悉这个概念。
使 Haar 检测实用的想法 no different。与其为每次缩放的每个特征重新计算矩形的总和,不如一开始就计算所有总和,并将它们保存以备将来计算。这可以通过为正在处理的帧构建一个求和面积表来完成,也称为计算其积分图像表示。
这个想法是计算图像中所有可能的矩形区域。幸运的是,这可以使用递归公式在对图像的单次遍历中完成。
或者,简单地说,
在积分图像中,可以使用仅 4 次数组访问来计算图像中任何矩形区域的面积。下面的图片可能有助于说明这一点。
蓝色矩阵代表原始图像,而紫色矩阵代表积分变换后的图像。如果我们计算第一个图像中的阴影区域,我们将不得不单独对所有像素求和,大约进行 6 次内存访问得到 20 的结果。使用积分图像,只需要一次访问(但仅因为我们在边界上)。如果我们不在边界上,无论区域大小如何,最多只需要 4 次数组访问;有效地将计算复杂度从O(n) 降低到O(1)。检索右侧图像中阴影区域的总和只需要两次减法和一次加法,如下面的等式所示:
源代码
最后是源代码!让我们从展示这个应用程序主要类的类图开始。
如果有点难看,我很抱歉,但我尽量让它尽可能紧凑,以便大致适合 640px 以下。您可以点击它以获得更大的版本,或查看 Accord.NET Framework 网站上最新的版本。
好了,首先是第一个。前面(在引言中)解释的详尽搜索发生在 HaarObjectDetector
中。这是主要的检测对象类。它的构造函数接受一个 HaarClassifier 作为参数,该参数将在对象检测过程中使用。HaarObjectDetector
的作用就是用滑动窗口扫描图像,必要时进行重新定位和缩放,然后调用 HaarClassifier
来检查当前区域是否存在人脸。
另一方面,分类器完全由一个 HaarCascade
对象及其当前工作比例确定。我忘了说,但在搜索过程中窗口并不需要真正缩放。Haar 特征被缩放,这效率要高得多。
那么,继续。HaarCascade
包含一系列阶段,这些阶段应按顺序进行评估。一旦级联中的某个阶段拒绝了该窗口,分类器就会停止并返回 false。通过查看 HaarClassifier
如何遍历级联,可以最好地看到这一点。
/// <summary>
/// Detects the presence of an object in a given window.
/// </summary>
///
public bool Compute(IntegralImage2 image, Rectangle rectangle)
{
int x = rectangle.X;
int y = rectangle.Y;
int w = rectangle.Width;
int h = rectangle.Height;
double mean = image.GetSum(x, y, w, h) * invArea;
double factor = image.GetSum2(x, y, w, h) * invArea - (mean * mean);
factor = (factor >= 0) ? Math.Sqrt(factor) : 1;
// For each classification stage in the cascade
foreach (HaarCascadeStage stage in cascade.Stages)
{
// Check if the stage has rejected the image
if (stage.Classify(image, x, y, factor) == false)
{
return false; // The image has been rejected.
}
}
// If the object has gone all stages and has not
// been rejected, the object has been detected.
return true; // The image has been detected.
}
就这样。现在轮到 HaarCascadeStage
对象的 Classify
方法了。请记住,每个阶段都包含一系列决策树。我们所要做的就是处理各个决策树,并检查它是否高于决策阈值。
/// <summary>
/// Classifies an image as having the searched object or not.
/// </summary>
public bool Classify(IntegralImage2 image, int x, int y, double factor)
{
double value = 0;
// For each feature in the feature tree of the current stage,
foreach (HaarFeatureNode[] tree in Trees)
{
int current = 0;
do
{
// Get the feature node from the tree
HaarFeatureNode node = tree[current];
// Evaluate the node's feature
double sum = node.Feature.GetSum(image, x, y);
// And increase the value accumulator
if (sum < node.Threshold * factor)
{
value += node.LeftValue;
current = node.LeftNodeIndex;
}
else
{
value += node.RightValue;
current = node.RightNodeIndex;
}
} while (current > 0);
}
// After we have evaluated the output for the
// current stage, we will check if the value
// is still lesser than the stage threshold.
if (value < this.Threshold)
{
// If it is, the stage has rejected the current
// image and it doesn't contains our object.
return false;
}
else
{
// The stage has accepted the current image
return true;
}
}
好了,如果我没有严重错误地解释什么,应该就是这样了。树中的每个决策节点包含一个特征,而一个特征可能包含两个或三个矩形。这些特征也可能倾斜,但我将克制解释倾斜特征,这只会增加复杂性,而且文章似乎已经太长了!最好是直接讲如何使用代码。
使用代码
使用代码相当简单。该框架已经提供了一些默认的 HaarCascade
定义,可以作为可实例化类使用(无需 *.xml 文件)。创建检测器可以这样做:
// First we create a classifier cascade
HaarCascade cascade = new FaceHaarCascade();
// Then we feed this cascade into a detector
var detector = new HaarObjectDetector(cascade, 30);
顺便说一句,值得注意的是,这些定义已经通过一个方便命名的类生成器 HaarCascadeWriter
从 OpenCV 的 *.xml 定义文件中自动创建的。但是,要编写某些内容,定义肯定必须首先加载到框架中。因此,该框架可以使用标准的 .NET 序列化打开 OpenCV 的定义文件。
现在已经创建了检测器,我们可以通过调用以下命令处理图像:
// Process frame to detect objects
Rectangle[] objects = detector.ProcessFrame(picture);
然后我们就可以在原始图片上标记那些矩形了:
// Create a rectangles marker to draw some rectangles around the faces
RectanglesMarker marker = new RectanglesMarker(objects, Color.Fuchsia);
// Applies the marker to the picture
pictureBox1.Image = marker.Apply(picture);
结果可以在样本应用程序中看到,该应用程序可在文章顶部下载。希望您觉得它有趣!
有趣的点
首先,也是最重要的,让我强调一下:部分代码是基于 Masakazu Ohtsuka 在其Marilena 项目中对出色的 ActionScript 实现的算法理解而编写的。因此,该项目的某些部分遵循与 Marilena 相同的 BSD 许可证,并且也双重授权于 LGPL。
关于样本应用程序的一些说明:样本应用程序正在处理一张 600 x 597 的图片。在我的 2008 年 Core 2 Duo 笔记本电脑上,使用并行处理检测图片中的所有五张人脸大约需要 400 毫秒。如果检测仅限于一张人脸,从较大的窗口尺寸开始,时间会下降到大约 10 毫秒。在典型应用程序中,在运行检测器之前,图像会被缩放到小得多的尺寸,从而实现更快的检测时间。
顺便说一句;本文对该方法进行了相当简化的描述。例如,Haar 特征之所以得名,是有原因的。它们基于 Haar 小波基函数,该函数由 PAPAGEORGIOU、OREN 和 POGGIO 等人在 1998 年使用。我也没有讨论倾斜的 Haar 特征,但可以通过在处理开始之前计算一个特殊的倾斜积分图像来计算它们(完整框架版本对它们提供了完整的支持)。另一个没有讨论的是学习这种分类器。然而,我不会对为本文实现一个可演示的版本抱太大希望。学习一个增强分类器级联可能需要几周的时间。
最后;我意识到 Haar 级联检测可能已经被讨论过很多次了,甚至在这里 CodeProject 上也讨论过。其他文章也涉及人脸检测。然而,这里展示的不是移植,也不是对原生库的封装。这是一个纯 C# 应用程序。而且,使用已有的东西只是一半的乐趣。
结论
在本文中,我们回顾了人脸检测中最基本的方法之一。该方法是流行的 Viola & Jones 方法的一个变体,基于矩形 Haar 类特征,如 Viola & Jones (2001) 所述,并由 Lienhart 进一步改进。本文提供的代码是 Accord.NET Framework 的一部分,该框架用于构建 .NET 中的科学计算应用程序,并支持机器学习和机器视觉方法,如本文所介绍的。如果您感到好奇,请查看 Accord.NET 机器学习框架项目页面。我保证您不会失望 。
参考文献
- Viola and Jones, "Rapid object detection using a boosted cascade of simple features", Computer Vision and Pattern Recognition, 2001
- Lienhart, R. and Maydt, J., "An extended set of Haar-like features for rapid object detection", ICIP02, pp. I: 900–903, 2002
- Papageorgiou, Oren and Poggio, "A general framework for object detection", International Conference on Computer Vision, 1998.
- Masakazu Ohtsuka, "Project Marilena", SPARK project. Available from: http://www.libspark.org/wiki/mash/Marilena
历史
- 2012 年 8 月 16 日:提交初稿
- 2014 年 12 月 2 日:更新项目链接