NDK 开发实战 - 实时人脸检测和识别
关于人脸检测和识别,应用的范围是非常广的,其实之前的《NDK开发前奏 - 实现支付宝人脸识别功能》 也有提到,只是那时并未具体的去分析算法和实现原理,这里笔者打算一步一步来分析和实现人脸识别,首先我们得要明确人脸检测和人脸识别是两个不同的概念,人脸检测是检测有人脸,人脸识别是匹配你是你,他们所采用的算法也是不一样的,这篇文章是基于人脸检测来实现人脸识别。我们先来看下已经实现了的人脸检测效果:
人脸马赛克大家可以忽略,并不是这里要关注的内容,是因为长得太帅了怕大家嫉妒。马赛克效果实现大家可以参考《图形图像处理 - 手写 QQ 说说图片处理效果》。首先我们不妨来思考一下,要实现像支付宝的人脸识别和员工刷脸签到等等,这样的一些应用功能,我们需要用到哪些知识?需要经过哪些步骤呢?其实这里分为两步,第一步是样本数据采集,第二步是检测和匹配。不过我们得先来看几个概念:均值,标准差,协方差矩阵,特征值,特征向量,PCA降维。
1. 均值,标准差,协方差矩阵
这几个都是概率论中的概念,我们随便举一个例子来算下即可,假设我的 Mat 数据如下:
Mat src = (Mat_<int>(3, 3) << 50, 50, 50, 60, 60, 60, 70, 70, 70);
均值:[60]
均值标准差:[8.164965809277252]
标准差协方差矩阵:[200, 200, 200; 200, 200, 200; 200, 200, 200]
协方差矩阵2. 特征值,特征向量
关于特征值与特征向量这是线性代数的概念,还是老套路拿个例子过来算下,能算出来就可以了,同时大家也可以参考这篇文章:
https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors
3. PCA降维
人脸识别肯定需要采集人脸样本,也就是相机采集过来的 Mat 数据,那么大量的数据怎么处理呢?这里就需要 PCA 降维了:
- 把原始数据中每个样本用一个向量表示,然后把所有样本组合起来构成一个矩阵。当然了,为了避免样本的单位的影响,样本集需要标准化。
- 求该矩阵的协方差矩阵
- 求步骤2中得到的协方差矩阵的特征值和特征向量。
- 将求出的特征向量按照特征值的大小进行组合形成一个映射矩阵,并根据指定的 PCA 保留的特征个数取出映射矩阵的前n行或者前n列作为最终的映射矩阵。
- 用步骤4的映射矩阵对原始数据进行映射,达到数据降维的目的。
4. 样本训练
接下来就是代码层面的东西了,代码是很简单的就那么几句话,但关键其实还是在于理解里面的原理:
FaceDetection_trainingPattern(JNIEnv *env, jobject instance) {
// 训练样本,这一步是在数据采集做的
// train it
vector<Mat> faces;
vector<int> labels;
// 样本比较少
for (int i = 1; i <= 5; ++i) {
for (int j = 1; j <= 5; ++j) {
Mat face = imread(format("/storage/emulated/0/s%d/%d.pgm", i, j), 0);
if (face.empty()) {
LOGE("face mat is empty");
continue;
}
// 确保大小一致
resize(face, face, Size(128, 128));
faces.push_back(face);
labels.push_back(i);
}
}
for (int i = 1; i <= 8; ++i) {
Mat face = imread(format("/storage/emulated/0/face_%d.png", i), 0);
if (face.empty()) {
LOGE("face mat is empty");
continue;
}
resize(face, face, Size(128, 128));
faces.push_back(face);
labels.push_back(11);
}
// 训练方法
Ptr<BasicFaceRecognizer> model = EigenFaceRecognizer::create();
// 采集了八张,同一个人 label 一样
model->train(faces, labels);
// 训练样本是 xml ,本地
model->save("/storage/emulated/0/face_darren_pattern.xml");// 存的是处理的特征数据
LOGE("樣本訓練成功");
}
4. 匹配识别
FaceDetection_faceDetection(JNIEnv *env, jobject instance,
jlong nativeObj) {
Mat *src = reinterpret_cast<Mat *>(nativeObj);
int width = src->rows;
int height = src->cols;
Mat grayMat;
// 2. 转成灰度图,提升运算速度,灰度图所对应的 CV_8UC1 单颜色通道,信息量少 0-255 1u
cvtColor(*src, grayMat, COLOR_BGRA2GRAY);
// 4. 检测人脸,这是个大问题
// 参数 1.1 会采取上采样和降采样 ,缩放比例
// 参数 3 检测多少次
// 参数 Size(width / 2, height / 2) 最小脸的大小
std::vector<Rect> faces;
cascadeClassifier.detectMultiScale(grayMat, faces, 1.1, 3, 0, Size(width / 2, height / 2));
if (faces.size() != 1) {
mosaicFace(*src);
return;
}
// 把脸框出来
Rect faceRect = faces[0];
rectangle(*src, faceRect, Scalar(255, 0, 0, 255), 4, LINE_AA);
// 不断检测,录入 10 张,张张嘴巴,眨眨眼睛 ,保证准确率
// 还需要注意一点,确保人脸大小一致,reSize(128,128) ,确保收集到的人脸眼睛尽量在一条线上
// 与服务端进行比对,是不是我
// 用一个计数器,这里我们做及时的
Mat face = (*src)(faceRect).clone();
resize(face, face, Size(128, 128));
cvtColor(face, face, COLOR_BGRA2GRAY);
// 直方均衡,harr 检测人脸
int label = model->predict(face);
// 训练的时候存的是 11
if (label == 11) {
// 识别到了自己
LOGE("识别到了自己");
putText(*src, "Darren", Point(faceRect.x + 20, faceRect.y - 20),
HersheyFonts::FONT_HERSHEY_COMPLEX, 1, Scalar(255, 0, 0, 255),2,LINE_AA);
} else {
// 不是自己
LOGE("不是自己");
putText(*src, "UnKnow", Point(faceRect.x + 20, faceRect.y - 20),
HersheyFonts::FONT_HERSHEY_COMPLEX, 1, Scalar(255, 0, 0, 255),2,LINE_AA);
}
// 速度, 准确率, 人脸尽量正常
mosaicFace((*src)(faceRect));
}
实时人脸识别
视频地址:https://pan.baidu.com/s/1lF92ev7_SqVNRNMih9eHkQ
视频密码:jc4k