文章目录
4.7 检测脸部元素的层次结构
在我们的脸部识别算法中,我们拒绝猫脸和人脸相交的情况.原因是猫脸的级联比人脸的级联会产生更多的假阳性.因此,如果一个区域被检测为同时是人脸和猫脸,那么事实上可能是人脸.为了方便我们检查脸部相交的情况,我们写一个工具函数,intersects.在一个新的头文件GeomUtils.h中声明这个方法,代码如下:
#ifndef GeomUtils_hpp
#define GeomUtils_hpp
#include <opencv2/core.hpp>
namespace GeomUtils {
bool intersects(const cv::Rect &rect0, const cv::Rect &rect1);
}
#endif /* GeomUtils_hpp */
两个矩形相交时,一个矩形的一个角必定会在另一个矩形之中.创建另一个文件GeomUtils.cpp,其中intersects函数的实现如下:
bool GeomUtils::intersects(const cv::Rect &rect0, const cv::Rect &rect1) {
return rect0.x < rect1.x + rect1.width && rect0.x + rect0.width > rect1.x && rect0.y < rect1.y + rect1.height && rect0.y + rect0.height > rect1.y;
}
现在,让我们创建FaceDetector.cpp文件,实现FaceDetector类.该文件开头定义了宏EQUAlLIZE(src,dst),如果打开了WITH_CLAHE宏,宏的内容为cv::equalizeHist函数,如果没有打开,则内容为:cv::CLAHE::apply方法.代码如下:
#include "FaceDetector.hpp"
#include <opencv2/imgproc.hpp>
#include "GeomUtils.hpp"
#ifdef WITH_CLAHE
#define EQUALIZE(src, dst) clahe->apply(src, dst)
#else
#define EQUALIZE(src, dst) cv::equalizeHist(src, dst)
#endif
我们的脸部检测算法使用了很多常量,我们将在靠近文件头的位置声明他们,在这里我们可以很方便地浏览和修改.对于级联分类器,我们使用下面类型的常量:
- 比例因子:该比率表示搜索级别之间的比例变化。例如,如果比例因子是1.4,分类器可能会搜索140×140像素的人脸,然后搜索100×100像素的人脸,依此类推。
- 最小邻域:如果这个值大于零,分类器就会将这个相交检测结果合并到一个邻域中。如果交叉点较少,则邻域内的结果被拒绝。
- 最小尺寸:这是分类器搜索的最小尺寸。我们将最小的人脸尺寸表示为整个图像尺寸的比例,最小的眼睛尺寸表示为人脸尺寸的比例。
我们为人脸,人眼和猫脸,定义不同的级联分类器,如下面代码所示:
const double DETECT_HUMAN_FACE_SCALE_FACTOR = 1.4;
const int DETECT_HUMAN_FACE_MIN_NEIGHBORS = 4;
const int DETECT_HUMAN_FACE_RELATIVE_MIN_SIZE_IN_IMAGE = 0.25;
const double DETECT_HUMAN_EYE_SCALE_FACTOR = 1.2;
const int DETECT_HUMAN_EYE_MIN_NEIGHBORS = 2;
const int DETECT_HUMAN_EYE_RELATIVE_MIN_SIZE_IN_FACE = 0.1;
const double DETECT_CAT_FACE_SCALE_FACTOR = 1.4;
const int DETECT_CAT_FACE_MIN_NEIGHBORS = 6;
const int DETECT_CAT_FACE_RELATIVE_MIN_SIZE_IN_IMAGE = 0.2;
其它的常量代表了眼睛和鼻子在脸部的典型布局.我们将这些布局值表示为脸或眼睛的宽度或高度的比例,我们将对猫和人使用不同的值。定义如下:
//人眼中,眼睛中心的相对位置
const double ESTIMATE_HUMAN_EYE_CENTER_RELATIVE_X_IN_EYE = 0.5;
const double ESTIMATE_HUMAN_EYE_CENTER_RELATIVE_Y_IN_EYE = 0.65;
//人脸中,左眼中心的相对位置
const double ESTIMATE_HUMAN_LEFT_EYE_CENTER_RELATIVE_X_IN_FACE =0.3;
//人脸中,右眼中心的相对位置
const double ESTIMATE_HUMAN_RIGHT_EYE_CENTER_RELATIVE_X_IN_FACE =1.0 - ESTIMATE_HUMAN_LEFT_EYE_CENTER_RELATIVE_X_IN_FACE;
const double ESTIMATE_HUMAN_EYE_CENTER_RELATIVE_Y_IN_FACE = 0.4;
//人脸中,鼻子的相对长度
const double ESTIMATE_HUMAN_NOSE_RELATIVE_LENGTH_IN_FACE = 0.2;
const double ESTIMATE_CAT_LEFT_EYE_CENTER_RELATIVE_X_IN_FACE =0.25;
const double ESTIMATE_CAT_RIGHT_EYE_CENTER_RELATIVE_X_IN_FACE = 1.0 - ESTIMATE_CAT_LEFT_EYE_CENTER_RELATIVE_X_IN_FACE;
const double ESTIMATE_CAT_EYE_CENTER_RELATIVE_Y_IN_FACE = 0.4;
//猫脸中,鼻尖的相对位置.
const double ESTIMATE_CAT_NOSE_TIP_RELATIVE_X_IN_FACE = 0.5;
const double ESTIMATE_CAT_NOSE_TIP_RELATIVE_Y_IN_FACE = 0.75;
最后,下边的常量描述了BGR模式下的颜色值,以及我们想要绘制的在检测到的脸部,眼睛和鼻子周围的圆圈的半径:
const cv::Scalar DRAW_HUMAN_FACE_COLOR(0, 255, 255); // Yellow
const cv::Scalar DRAW_CAT_FACE_COLOR(255, 255, 255); // White
const cv::Scalar DRAW_LEFT_EYE_COLOR(0, 0, 255); // Red
const cv::Scalar DRAW_RIGHT_EYE_COLOR(0, 255, 0); // Green
const cv::Scalar DRAW_NOSE_COLOR(255, 0, 0); // Blue
const int DRAW_RADIUS = 4;
还记得FaceDetector的构造函数接收4个级联文件的路径作为参数.他初始化了人脸,猫脸,人左眼,人右眼的级联分类器.如果设置了’WITH_CLAHE’宏的话,构造函数还初始化了一个CLAHE算法,下面是代码:
FaceDetector::FaceDetector(const std::string &humanFaceCascadePath, const std::string &catFaceCascadePath, const std::string &humanLeftEyeCascadePath,const std::string &humanRightEyeCascadePath) : humanFaceClassifier(humanFaceCascadePath) , catFaceClassifier(catFaceCascadePath) , humanLeftEyeClassifier(humanLeftEyeCascadePath) , humanRightEyeClassifier(humanRightEyeCascadePath)
#ifdef WITH_CLAHE
, clahe(cv::createCLAHE())
#endif
{ }
现在,让我们考虑’detect’函数的实现.实现的代码很长,所以我们将把它分成三部分。首先,我们从结果的向量中清除前面的所有内容,然后调整图像的大小并均衡化图片。均衡化是在一个辅助方法’equalize’中实现的,我们稍后将对此进行研究。下面是’detect’方法实现的开始:
void FaceDetector::detect(cv::Mat &image, std::vector<Face> &faces, double resizeFactor, bool draw) {
faces.clear();
if (resizeFactor == 1.0) {
equalize(image);
}
else {
cv::resize(image, resizedImage, cv::Size(), resizeFactor,resizeFactor, cv::INTER_AREA);
equalize(resizedImage);
}
其次,该方法使用两个级联分类器在调整大小的均衡图像中找到人脸和猫脸的矩形边界。作为这一步的一部分,我们根据我们定义的常数比例计算出以像素为单位的最小人脸尺寸。矩形存储在向量中,如下代码所示:
// Detect human faces.
std::vector<cv::Rect> humanFaceRects;
int detectHumanFaceMinWidth = MIN(image.cols, image.rows) * DETECT_HUMAN_FACE_RELATIVE_MIN_SIZE_IN_IMAGE;
cv::Size detectHumanFaceMinSize(detectHumanFaceMinWidth,detectHumanFaceMinWidth);
humanFaceClassifier.detectMultiScale(equalizedImage,humanFaceRects, DETECT_HUMAN_FACE_SCALE_FACTOR, DETECT_HUMAN_FACE_MIN_NEIGHBORS, 0, detectHumanFaceMinSize);
// Detect cat faces.
std::vector<cv::Rect> catFaceRects; int detectCatFaceMinWidth = MIN(image.cols, image.rows) * DETECT_CAT_FACE_RELATIVE_MIN_SIZE_IN_IMAGE;
cv::Size detectCatFaceMinSize(detectCatFaceMinWidth,detectCatFaceMinWidth); catFaceClassifier.detectMultiScale(equalizedImage, catFaceRects,DETECT_CAT_FACE_SCALE_FACTOR, DETECT_CAT_FACE_MIN_NEIGHBORS,0, detectCatFaceMinSize);
第三,我们遍历矩形,丢弃与人脸相交的猫脸,并将剩余的项传递给detectInnerComponents辅助方法。每次调用辅助方法时,它构造一个Face对象并将其添加到结果的向量中。下面是相关的循环的代码:
equalize辅助方法执行灰度转换(如果图像还不是灰度),并根据equalize宏应用标准的均衡化方法或CLAHE算法。下面是该方法的实现:
void FaceDetector::equalize(const cv::Mat &image) {
switch (image.channels()) {
case 4:
cv::cvtColor(image, equalizedImage, cv::COLOR_BGRA2GRAY);
EQUALIZE(equalizedImage, equalizedImage);
break;
case 3:
cv::cvtColor(image, equalizedImage, cv::COLOR_BGR2GRAY);
EQUALIZE(equalizedImage, equalizedImage);
break;
default:
// Assume the image is already grayscale.
EQUALIZE(image, equalizedImage);
break;
}
}
detectInnerComponents辅助方法方法很长,因此我们将把它分为八个部分。(如果这似乎很多块,要记住,这仅仅是2 ^ 3或1 < < 3。)首先,我们将定义局部变量来表示人脸子矩阵和眼鼻坐标。人脸子矩阵引用(而不是复制)人脸区域中的图像数据。以下是detectInnerComponents的开始:
void FaceDetector::detectInnerComponents(const cv::Mat &image, std::vector<Face> &faces, double resizeFactor, bool draw, Species species, cv::Rect faceRect) {
cv::Range rowRange(faceRect.y, faceRect.y + faceRect.height);
cv::Range colRange(faceRect.x, faceRect.x + faceRect.width);
bool isHuman = (species == Human);
cv::Mat equalizedFaceMat(equalizedImage, rowRange, colRange);
cv::Rect leftEyeRect;
cv::Rect rightEyeRect;
cv::Point2f leftEyeCenter;
cv::Point2f rightEyeCenter;
cv::Point2f noseTip;
```
如果脸是人脸,我们使用级联分类器来搜索人脸左半部分的左眼。如果分类器无法检测到眼睛,我们就会对眼睛在脸部位置进行大致估计。以下是相关代码:
```
if (isHuman) {
int faceWidth = equalizedFaceMat.cols;
int halfFaceWidth = faceWidth / 2;
int eyeMinWidth = faceWidth * DETECT_HUMAN_EYE_RELATIVE_MIN_SIZE_IN_FACE; cv::Size eyeMinSize(eyeMinWidth, eyeMinWidth);
// Try to detect the left eye.
std::vector<cv::Rect> leftEyeRects;
humanLeftEyeClassifier.detectMultiScale( equalizedFaceMat.colRange(0, halfFaceWidth), leftEyeRects, DETECT_HUMAN_EYE_SCALE_FACTOR, DETECT_HUMAN_EYE_MIN_NEIGHBORS, 0, eyeMinSize);
if (leftEyeRects.size() > 0) {
leftEyeRect = leftEyeRects[0];
leftEyeCenter.x = leftEyeRect.x + ESTIMATE_HUMAN_EYE_CENTER_RELATIVE_X_IN_EYE * leftEyeRect.width;
leftEyeCenter.y = leftEyeRect.y + ESTIMATE_HUMAN_EYE_CENTER_RELATIVE_Y_IN_EYE * leftEyeRect.height;
}
else { // Assume the left eye is in a typical location for a human.
leftEyeCenter.x = ESTIMATE_HUMAN_LEFT_EYE_CENTER_RELATIVE_X_IN_FACE * faceRect.width;
leftEyeCenter.y = ESTIMATE_HUMAN_EYE_CENTER_RELATIVE_Y_IN_FACE * faceRect.height;
}
对于右眼,我们采用相同的方法,只是这次搜索脸的右半部分并使用不同的级联分类器。我们必须调整检测结果相对于整张脸的原点,而不是右半脸的原点。下面是所有用于检测或粗略地估计右眼坐标的代码:
// Try to detect the right eye.
std::vector<cv::Rect> rightEyeRects;
humanRightEyeClassifier.detectMultiScale(equalizedFaceMat.colRange(halfFaceWidth, faceWidth), rightEyeRects, DETECT_HUMAN_EYE_SCALE_FACTOR,DETECT_HUMAN_EYE_MIN_NEIGHBORS, 0, eyeMinSize);
if (rightEyeRects.size() > 0) {
rightEyeRect = rightEyeRects[0];
// Adjust the right eye rect to be relative to the whole
// face.
rightEyeRect.x += halfFaceWidth;
rightEyeCenter.x = rightEyeRect.x + ESTIMATE_HUMAN_EYE_CENTER_RELATIVE_X_IN_EYE * rightEyeRect.width;
rightEyeCenter.y = rightEyeRect.y + ESTIMATE_HUMAN_EYE_CENTER_RELATIVE_Y_IN_EYE * rightEyeRect.height;
}
else {
// Assume the right eye is in a typical location for a
// human.
rightEyeCenter.x = ESTIMATE_HUMAN_RIGHT_EYE_CENTER_RELATIVE_X_IN_FACE * faceRect.width;
rightEyeCenter.y = ESTIMATE_HUMAN_EYE_CENTER_RELATIVE_Y_IN_FACE * faceRect.height;
}
由于我们没有一个级联来检测鼻子,我们必须对它的坐标做一个粗略的估计。然而,我们可以利用眼睛的检测结果,这可能告诉我们,脸是倾斜的。如果是这种情况,鼻子也会倾斜,鼻尖不会水平居中在脸的矩形。相反,我们假设如果我们找到眼睛之间的线段,到它的中点,然后沿着垂直线段向下,我们会到达鼻尖。这个假设并不完美,因为它没有考虑透视,但它为稍微倾斜的脸提供了一个有用的调整。以下是相关代码:
// Assume the nose is in a typical location for a human.
// Consider the location of the eyes.
cv::Point2f eyeDiff = rightEyeCenter - leftEyeCenter;
cv::Point2f centerBetweenEyes = leftEyeCenter + 0.5 * eyeDiff;
cv::Point2f noseNormal = cv::Point2f(-eyeDiff.y, eyeDiff.x) / sqrt(pow(eyeDiff.x, 2.0) + pow(eyeDiff.y, 2.0));
double noseLength = ESTIMATE_HUMAN_NOSE_RELATIVE_LENGTH_IN_FACE * faceRect.height;
noseTip = centerBetweenEyes + noseNormal * noseLength;
}
对于猫,我们没有级联分类器来检测鼻子和眼睛.因此,我们总是对未知进行粗略估计,代码如下:
else {
// I haz kitteh! The face is a cat.
// Assume the eyes and nose are in typical locations for a // cat.
leftEyeCenter.x = ESTIMATE_CAT_LEFT_EYE_CENTER_RELATIVE_X_IN_FACE * faceRect.width;
leftEyeCenter.y = ESTIMATE_CAT_EYE_CENTER_RELATIVE_Y_IN_FACE * faceRect.height;
rightEyeCenter.x = ESTIMATE_CAT_RIGHT_EYE_CENTER_RELATIVE_X_IN_FACE * faceRect.width;
rightEyeCenter.y = ESTIMATE_CAT_EYE_CENTER_RELATIVE_Y_IN_FACE * faceRect.height;
noseTip.x = ESTIMATE_CAT_NOSE_TIP_RELATIVE_X_IN_FACE * faceRect.width;
noseTip.y = ESTIMATE_CAT_NOSE_TIP_RELATIVE_Y_IN_FACE *faceRect.height;
}
在这个阶段,我们已经有了眼睛和鼻子在调整了大小的脸部子矩阵中的坐标。让我们将坐标恢复到原始的比例,如下面的代码所示:
// Restore everything to the original scale.
faceRect.x /= resizeFactor;
faceRect.y /= resizeFactor;
faceRect.width /= resizeFactor;
faceRect.height /= resizeFactor;
rowRange.start /= resizeFactor;
rowRange.end /= resizeFactor;
colRange.start /= resizeFactor;
colRange.end /= resizeFactor;
cv::Mat faceMat(image, rowRange, colRange);
leftEyeRect.x /= resizeFactor;
leftEyeRect.y /= resizeFactor;
leftEyeRect.width /= resizeFactor;
leftEyeRect.height /= resizeFactor;
rightEyeRect.x /= resizeFactor;
rightEyeRect.y /= resizeFactor;
rightEyeRect.width /= resizeFactor;
rightEyeRect.height /= resizeFactor;
leftEyeCenter /= resizeFactor;
rightEyeCenter /= resizeFactor;
noseTip /= resizeFactor;
现在,使用原始尺度下的face子矩阵,创建一个新的face对象,并将其添加到结果向量中:
faces.push_back(Face(species, faceMat, leftEyeCenter, rightEyeCenter, noseTip));
Face构造函数复制子矩阵,所以现在我们可以在不影响Face的情况下绘制原始图像。由于face子矩阵的原点与完整图像的原点不同,为了绘制函数的目的,我们必须调整眼睛和鼻子的坐标。下面是相关的代码,完成了detectInnerComponents方法的实现:
if (draw) {
cv::rectangle(image, faceRect.tl(), faceRect.br(), isHuman ? DRAW_HUMAN_FACE_COLOR : DRAW_CAT_FACE_COLOR);
cv::circle(image, faceRect.tl() + cv::Point(leftEyeCenter),DRAW_RADIUS, DRAW_LEFT_EYE_COLOR);
cv::circle(image, faceRect.tl() + cv::Point(rightEyeCenter),DRAW_RADIUS, DRAW_RIGHT_EYE_COLOR);
cv::circle(image, faceRect.tl() + cv::Point(noseTip),DRAW_RADIUS, DRAW_NOSE_COLOR);
if (leftEyeRect.width > 0) {
cv::rectangle(image, faceRect.tl() + leftEyeRect.tl(), faceRect.tl() + leftEyeRect.br(), DRAW_LEFT_EYE_COLOR);
}
if (rightEyeRect.width > 0) {
cv::rectangle(image, faceRect.tl() + rightEyeRect.tl(),faceRect.tl() + rightEyeRect.br(), DRAW_RIGHT_EYE_COLOR);
}
}
}
唷!那个辅助方法真的很长。我想起了一个非常古老的故事,讲的是一个苏格兰传教士和他的教众移民到新斯科舍省。他为这次漫长的海上航行准备了一系列的布道,当船从阿伯丁启航时,他逐渐形成了一个有趣的观点:“第十七,朋友们,我们遇到了很大的困难……”
[我是新斯科舍省人,我的祖先中有苏格兰人。]