文章目录

  • 4.7 检测脸部元素的层次结构

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); // Blueconst 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);}}
}

唷!那个辅助方法真的很长。我想起了一个非常古老的故事,讲的是一个苏格兰传教士和他的教众移民到新斯科舍省。他为这次漫长的海上航行准备了一系列的布道,当船从阿伯丁启航时,他逐渐形成了一个有趣的观点:“第十七,朋友们,我们遇到了很大的困难……”
[我是新斯科舍省人,我的祖先中有苏格兰人。]

###返回到第四章目录###
###返回到书籍目录###

4.7 检测脸部元素的层次结构相关推荐

  1. 4.8 对齐和混合脸部元素

    文章目录 4.8 对齐和混合脸部元素 4.8 对齐和混合脸部元素 我们应用程序的其他功能在Face类的实现中.创建一个新文件Face.cpp.记住,脸有一个种类,图像数据的矩阵,眼睛中心和鼻尖的坐标. ...

  2. 元素的层次结构和HTML文档结构

    1. HTML元素是HTML文档的重要组成部分,一个HTML文档由大量的元素组成 HTML中的所有内容结构,都是靠元素组织到页面中的 2. HTML元素由起始标记 属性 元素内容 结束标记组成 书写格 ...

  3. jQuery下实现检测指定元素加载完毕

    检测元素出现方法. 虽然是基于 jQuery 的,但是代码很简洁,可以修改成纯js版的. 文本 jQuery.fn.wait = function (func, times, interval) {v ...

  4. 三坐标检测之元素的测针半径补偿

    1.测点半径补偿 在接触式坐标测量中,一般采用球型探针,当被测轮廓面还处于未知的情况下,探针红宝石球与工件表面接触点也是未知的,但由于两者之间是点接触,所以红宝石球心的位置是唯一的,然后在这个球心唯一 ...

  5. 用10行代码检测脸部情绪

    引言 面部表情展示人类内心的情感.它们帮助我们识别一个人是愤怒.悲伤.快乐还是正常.医学研究人员也使用面部情绪来检测和了解一个人的心理健康. 人工智能在识别一个人的情绪方面可以发挥很大的作用.在卷积神 ...

  6. html 滚动条停止事件,CSS scroll-snap滚动事件停止及元素位置检测实现

    一.Scroll Snap是前端必备技能 CSS Scroll Snap是个非常好用的特性,可以让网页容器滚动停止的时候,无需任何JS代码的参与,浏览器可以自动平滑定位到指定元素的指定位置.类似幻灯片 ...

  7. arraylist如何检测某一元素是否为空_我们应该如何理解Java集合框架的关键知识点?...

    以下介绍经常使用的集合类,这里不介绍集合类的使用方法,只介绍每个集合类的用途和特点,然后通过比较相关集合类的不同特点来让我们更深入的了解它们. Collection接口 Collection是最基本的 ...

  8. jq 检测元素内html变化,jq 监听 textarea 元素内容变化的方法

    在前台是可以限制 textarea 元素内输入内容的长度的,当然是用 jquery 代码实现起来是最简单,方便的,JQ脚本通过对 textarea 元素内容变化的检测来判断内容是否超出指定的长度. j ...

  9. js 中如何检测元素是否存在 - setInterval 方式

    js 中使用 setInterval 方式暴力检测元素是否存在 <!DOCTYPE html> <html lang="en"> <head>& ...

最新文章

  1. 比起睡觉,我更喜欢刷巨详细的Java枚举类,这是来自猿人的自觉呀
  2. kafka的topic命名技巧
  3. linux环境编程 学习,学习linux环境高级编程首先学习的是文件的操作。因为有.pdf...
  4. 修改anaconda中conda和pip的源为清华源
  5. React Portals的使用
  6. python listdir报错_Python常见十六个错误集合,你知道那些?
  7. 使用镜像数据库减轻报表的负载
  8. baidu 地图 鼠标移上显示标签 鼠标离开隐藏标签
  9. linux给进程加速,Linux 利用并行进程加速命令执行
  10. 人生第一次被迫转行!实现月薪16K!勤能补拙是良训,一分耕耘一分才
  11. wpf之样式属性、事件、触发器
  12. 关于城市智慧道路建设的思考
  13. k2路由器改无线打印服务器,修改斐讯K2、K3路由器的无线密码【图解】
  14. 终于找到可转载的摄影基础知识贴了
  15. (转)如何将Sklearn数据集Bunch格式转换为Pandas数据集DataFrame?
  16. 计算机仿真塞曼效应实验报告,塞曼效应实验报告[完整版].doc
  17. Docker部署服务(二)上传镜像至Habor
  18. allegro差分信号走线_Cadence差分线走线规则
  19. 响应式布局之微软商城部分开发
  20. office web apps安装部署 Win 2008 安装

热门文章

  1. 使用Jil序列化JSON提升Asp.net web api 性能
  2. nginx upstream中长连接池的维护
  3. linux下kil命令l,linux/centos下使用kill命令的使用教程方法
  4. Java实现 word.excel等文档在线预览
  5. 【图像配准】图像配准基础知识:入门知识、点云基础、图像配准的概念、基础和分类
  6. 复用浏览器之跳过扫码登录
  7. 驱动给我带来的麻烦,我在OpenGpu上论坛发的两个求助帖(让我百思不得其解呀。),都是因为驱动的原因。。...
  8. 上传多组带标题的图片
  9. matlab腔内光子寿命,光子在腔内的平均寿命
  10. 两个led并联和一个电阻串联两个灯不能同时亮问题