上一节我们分析了人脸检测的原理,以及简化了示例代码。在检测过程中,我们主要使用了opencv的级联分类器CascadeClassifier,而它提供的detectMultiScale方法能够检测出一张图片中的一个或多个人脸,并用矩形标识出人脸位置。结果如下图所示
在检测过程当中,我们并没有训练模型,而是使用opencv提供给我们训练好的模型。事实上,opencv提供给我们很多模型。我们可以根据这些模型做相应的检测。
我们可以根据这些模型,套用上次代码做相应不同的检测,比如根据名字,上图中红色框框起的模型应该是用来检测眼睛部位的。但是我们不能直接套用上次代码,原因是双眼检测是在人脸检测的基础上进行的,也就是说,检测某人双眼位置之前,我们需要先检测出该人的人脸部位,继而在人脸范围内寻找双眼部位,这样比较合理。
代码应该如下所示;
// face1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
void detectAndDraw(Mat& img, CascadeClassifier& cascade,
CascadeClassifier& nestedCascade,
double scale, bool tryflip);
string cascadeName;
string nestedCascadeName;
int main(int argc, const char** argv)
{
string inputName;
bool tryflip;
double scale;
CommandLineParser parser(argc, argv,
"{help h||}"
"{cascade|data/haarcascades/haarcascade_frontalface_alt2.xml|}"
//双眼检测模型
"{nested-cascade|data/haarcascades/haarcascade_eye.xml|}"
"{scale|1|}{try-flip||}{@filename|D:/试验/test/smile.jpg|}"
);
if (parser.has("help"))
{
return 0;
}
//解析参数得到人脸检测模型路径
cascadeName = parser.get<string>("cascade");
//解析参数得到眼检测模型路径
nestedCascadeName = parser.get<string>("nested-cascade");
//开始解析其他参数
scale = parser.get<double>("scale");
if (scale < 1)
scale = 1;
tryflip = parser.has("try-flip");
//解析输入文件名(就是我们想要检测的图像路径)
inputName = parser.get<string>("@filename");
if (!parser.check())
{
parser.printErrors();
return 0;
}
//构造分类器用于检测脸部
cv::CascadeClassifier cascade(cascadeName);
//根据参数inputName加载被检测图像
Mat image = imread(inputName);
Mat gray;
//转灰度图便于检测
cvtColor(image, gray, cv::COLOR_BGRA2GRAY);
vector<Rect> faces;
//检测人脸
cascade.detectMultiScale(gray,faces);
//构造“内层”的分类器,该分类器用于检测双眼
cv::CascadeClassifier nestedCascade(nestedCascadeName);
for (int i = 0; i < faces.size(); i++)
{
//得到本人人脸检测范围,我们需要在这个范围内进行内层检测(双眼定位)
Rect r = faces[i];
//绘制人脸矩形
cv::rectangle(image,r,Scalar(255, 0, 0));
//定义一个矩形数组,用于标识检测到的双眼
vector<Rect> faces2;
//检测双眼矩形
nestedCascade.detectMultiScale(image(r), faces2);
for (int j = 0; j < faces2.size(); j++)
{
Mat image2 = image(r);
cv::rectangle(image2, faces2[j], Scalar(0, 255, 0));
}
}
//显示标记好的图像
imshow("检测人脸", image);
waitKey();
return 0;
}
结果如下图所示
让我们看看还有什么模型
上面红框标记的好像是检测笑脸。笑脸跟双眼一样也是在脸部检测的基础上,再来看微笑有无出现,所以可以套用上述代码。事实上我们可以利用这个模型,来检测出人脸微笑的程度。但是这个检测是个动态的概念,就是说,对人脸笑颜动态展开,会比较敏感。
接下来我们有一个视频,人再逐渐展开微笑时,视频两边红色条块随着男人微笑程度而逐渐明亮
效果视频
代码如下
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
void detectAndDraw(Mat& img, CascadeClassifier& cascade,
CascadeClassifier& nestedCascade);
string cascadeName;
string nestedCascadeName;
VideoWriter writer("output.mp4", CV_FOURCC('m', 'p', '4', 'v'), 10, Size(700, 350));
int main(int argc, const char** argv)
{
VideoCapture capture;
Mat frame, image;
string inputName;
bool tryflip;
CascadeClassifier cascade, nestedCascade;
double scale;
cv::CommandLineParser parser(argc, argv,
"{help h||}{scale|1|}"
"{cascade|data/haarcascades/haarcascade_frontalface_alt.xml|}"
"{smile-cascade|data/haarcascades/haarcascade_smile.xml|}"
"{try-flip||}{@input|D:/试验/test/smile.mp4|}");
//以下参数解析
if (parser.has("help"))
{
return 0;
}
cascadeName = samples::findFile(parser.get<string>("cascade"));
nestedCascadeName = samples::findFile(parser.get<string>("smile-cascade"));
inputName = parser.get<string>("@input");
if (!parser.check())
{
return 1;
}
if (!cascade.load(cascadeName))
{
cerr << "错误:不能加载人脸检测模型" << endl;
return -1;
}
if (!nestedCascade.load(nestedCascadeName))
{
cerr << "错误:不能加载内层检测模型" << endl;
return -1;
}
if (inputName.empty() )
{
cerr << "错误:被检测图像路径为空" << endl;
return -1;
}
//跟据参数“inputName”打开相应视频文件,并读取一帧图像
capture.open(inputName);
capture.read(frame);
//循环读取视频每帧图像,并进行检测处理
while (!frame.empty())
{
//检测处理以及微笑程度显示
detectAndDraw(frame, cascade, nestedCascade);
capture.read(frame);
}
writer.release();
return 0;
}
//检测处理以及微笑程度显示
void detectAndDraw(Mat& img, CascadeClassifier& cascade,
CascadeClassifier& nestedCascade)
{
//定义矩形数组存放人脸检测结果
vector<Rect> faces;
Mat gray;
//将原图转换为灰度图方便进一步处理
cvtColor(img, gray, COLOR_BGR2GRAY);
//检测人脸
cascade.detectMultiScale(img, faces,
1.1, 2, 0|CASCADE_SCALE_IMAGE);
//对每个人脸检测结果进行处理(进一步检测微笑程度)
Mat imgOut;
for (size_t i = 0; i < faces.size(); i++)
{
//得到本人人脸检测范围,我们需要在这个范围内进行内层检测(微笑密度)
Rect r = faces[i];
Mat imgROI = img(r);
//用于存储内层检测结果
vector<Rect> nestedObjects;
//绘制人脸矩形
rectangle(img,r,Scalar(255,0,0));
//在矩形范围内检测微笑密度
nestedCascade.detectMultiScale(imgROI, nestedObjects,
1.1, 0, 0 | CASCADE_SCALE_IMAGE);
// 微笑密度和内层检测的结果大小有关(成正比)
const int smile_neighbors = (int)nestedObjects.size();
//结合前帧图像数据,经过换算得到真正的微笑密度
//注意:以下两个变量为静态变量,循环调用此函数,它们的值不会重新初始化。
static int max_neighbors = -1;
static int min_neighbors = -1;
if (min_neighbors == -1) min_neighbors = smile_neighbors;
max_neighbors = MAX(max_neighbors, smile_neighbors);
float intensityZeroOne = ((float)smile_neighbors - min_neighbors) / (max_neighbors - min_neighbors + 1);
//根据微笑密度换算“红色”深度
Scalar color = Scalar(0,0,(float)255 * intensityZeroOne);
//绘制左右两边矩形,显示微笑程度
rectangle(img, Point(0, 0), Point(img.cols / 10, img.rows), color, -1);
rectangle(img, Point(img.cols*9/10, 0), Point(img.cols , img.rows), color, -1);
}
resize(img, imgOut,Size(700, 350));
writer.write(imgOut);
}
事实你还可以依此原理玩出其他花样,比如用微笑触发烟花,植入手机,用微笑触发拍照,植入电子锁,用微笑打开日记,等等
最后值得一提的是,实际上opencv只是实现了经典的算法,但很多时候并不精确,还需要我们根据工作需要改进。