iOS-OpenCV笔记:详解人脸识别原理(二)
上篇 iOS-OpenCV笔记:实现简单的人脸识别(一)着重介绍了OpenCV的基本知识和在iOS上的编译过程,本篇将通过代码和API了解整个人脸的识别过程。
人脸识别主要分两部分:
我将这两部分的功能分别实现在这两个类下:
-
HVFaceDetectorUtil
:负责检测和收集人脸 -
HVFaceRecognizerUitl
:负责识别人脸
一、检测人脸
iPhone通过摄像头获取到视频流,对每一帧的图片持续进行检测,来捕捉到图片中人脸的区域。
- 首先通过
HVFaceDetectorUtil
类的初始化获取CvVideoCamera *videoCamera
属性的实例,并设置代理,再加载工程中的训练好的 HaarCascade xml 文件,创建人脸和眼睛检测的Haar分类器:
#ifdef __cplusplus
#import <opencv2/opencv.hpp>
#endif
@interface HVFaceDetectorUtil()<CvVideoCameraDelegate>
{
cv::CascadeClassifier _faceDetector;
cv::CascadeClassifier _eyesDetector;
std::vector<cv::Rect> _faceRects;
std::vector<cv::Mat> _faceImgs;
}
@property (nonatomic, retain) CvVideoCamera *videoCamera;
@property (nonatomic, assign) CGFloat scale;
@end
@implementation HVFaceDetectorUtil
- (instancetype)initWithParentView:(UIImageView *)parentView scale:(CGFloat)scale
{
self = [super init];
if (self) {
_videoCamera = [[CvVideoCamera alloc] initWithParentView:parentView];
_videoCamera.defaultAVCaptureDevicePosition = AVCaptureDevicePositionBack;
_videoCamera.defaultAVCaptureSessionPreset = AVCaptureSessionPreset640x480;
_videoCamera.defaultAVCaptureVideoOrientation = AVCaptureVideoOrientationPortrait;
_videoCamera.defaultFPS = 30;
_videoCamera.grayscaleMode = NO;
_videoCamera.delegate = self;
_scale = scale;
//加载项目中训练好的Haar分类器
//正面脸部Haar分类器
NSString *faceCascadePath = [[NSBundle mainBundle]
pathForResource:@"haarcascade_frontalface_alt2"
ofType:@"xml"];
_faceDetector.load([faceCascadePath UTF8String]);
//眼睛部位Haar分类器
NSString *eyesCascadePath = [[NSBundle mainBundle]
pathForResource:@"haarcascade_eye_tree_eyeglasses"
ofType:@"xml"];
_eyesDetector.load([eyesCascadePath UTF8String]);
}
return self;
}
- (void)startCapture
{
[self.videoCamera start];
}
- (void)stopCapture
{
[self.videoCamera stop];
}
- Haar Cascade常用来做人脸检测,其实它可以检测任何对象。
-
OpenCV 项目源码中有很多训练好的Haar分类器,它们在 /oenncv/data/haarcascades 文件夹路径中可以找到如下:
Haar Cascade list
- 然后实现
CvVideoCamera *videoCamera
的代理函数- (void)processImage:(cv::Mat&)image
,对每一帧的图片进行检测:
- 摄像头的帧率被设置为30帧每秒,实现的 processImage 函数将每秒被调用30次。
- 因为要持续不断地检测人脸,所以在这个函数里实现人脸的检测。
- 要注意的是,如果对某一帧进行人脸检测的时间超过 1/30 秒,就会产生掉帧现象。
#pragma mark - Protocol CvVideoCameraDelegate
- (void)processImage:(cv::Mat &)image {
// Do some OpenCV stuff with the image
[self detectAndDrawFacesOn:image scale:self.scale];
}
- (void)detectAndDrawFacesOn:(cv::Mat&)img scale:(double) scale
{
int i = 0;
double t = 0;
//划线颜色数组
const static cv::Scalar colors[] = { CV_RGB(0,0,255),
CV_RGB(0,128,255),
CV_RGB(0,255,255),
CV_RGB(0,255,0),
CV_RGB(255,128,0),
CV_RGB(255,255,0),
CV_RGB(255,0,0),
CV_RGB(255,0,255)} ;
cv::Mat gray, smallImg( cvRound (img.rows/scale), cvRound(img.cols/scale), CV_8UC1 );
//将图片转成灰度图
cvtColor( img, gray, cv::COLOR_BGR2GRAY );
////修改图片尺寸,压缩成小图
resize( gray, smallImg, smallImg.size(), 0, 0, cv::INTER_LINEAR );
//直方图均衡化︰ 在低光照条件下的人脸检测是不可靠的,所以我们应该执行直方图均衡化
equalizeHist( smallImg, smallImg );
//开启时间计时器
t = (double)cvGetTickCount();
//决定每次遍历分类器后尺度会变大多少倍
double scalingFactor = 1.1;
//指定一个符合条件的人脸区域应该有多少个符合条件的邻居像素才被认为是一个可能的人脸区域,
//拥有少于 minNeighbors 个符合条件的邻居像素的人脸区域会被拒绝掉。
int minNeighbors = 2;
//设定检测人脸区域范围的最小值
cv::Size minSize(30,30);
//设定检测人脸区域范围的最大值
cv::Size maxSize(280,280);
//通过检测输入不同大小的图像,获取被检测到的图像列表
//图像对象会作为一个矩形列表返回:self->_faceRects。
self->_faceDetector.detectMultiScale(smallImg, self->_faceRects,
scalingFactor, minNeighbors, 0,
minSize,maxSize);
//计算检测所花费的时间
t = (double)cvGetTickCount() - t;
// printf( "detection time = %g ms\n", t/((double)cvGetTickFrequency()*1000.) );
std::vector<cv::Mat> faceImages;
for( std::vector<cv::Rect>::const_iterator r = _faceRects.begin(); r != _faceRects.end(); r++, i++ )
{
cv::Mat smallImgROI;
cv::Point center;
cv::Scalar color = colors[i%8];
std::vector<cv::Rect> nestedObjects;
//画正方形
rectangle(img,
cvPoint(cvRound(r->x*scale), cvRound(r->y*scale)),
cvPoint(cvRound((r->x + r->width-1)*scale), cvRound((r->y + r->height-1)*scale)),
color, 1, 8, 0);
//eye detection is pretty low accuracy
if(self->_eyesDetector.empty())
continue;
smallImgROI = smallImg(*r);
faceImages.push_back(smallImgROI.clone());
//检测眼睛
self->_eyesDetector.detectMultiScale( smallImgROI,
nestedObjects,
1.1, 2, 0,
cv::Size(1, 1) );
//将检测到的眼睛画圆
for( std::vector<cv::Rect>::const_iterator nr = nestedObjects.begin(); nr != nestedObjects.end(); nr++ )
{
center.x = cvRound((r->x + nr->x + nr->width*0.5)*scale);
center.y = cvRound((r->y + nr->y + nr->height*0.5)*scale);
int radius = cvRound((nr->width + nr->height)*0.25*scale);
circle( img, center, radius, color, 1, 8, 0 );
}
}
@synchronized(self) {
self->_faceImgs = faceImages;
}
}
- 下面我们来详细研究一下获取检测图像列表的关键函数
detectMultiScale
,以及它所需传入的参数定义:
//通过检测输入不同大小的图像,获取被检测到的图像列表
//检测出的对象会作为一个矩形列表返回:objects。
CV_WRAP void detectMultiScale( InputArray image,
CV_OUT std::vector<Rect>& objects,
double scaleFactor = 1.1,
int minNeighbors = 3, int flags = 0,
Size minSize = Size(),
Size maxSize = Size() );
- @param image:CV_8U类型的图像矩阵,待检测图片,一般为灰度图像,加快检测速度。
- @param objects:包含所有被检测出的图像的矩形列表,这些矩形可能部分位于原始图像之外。
- @param scaleFactor:指定每次遍历分类器后每张图像尺度的缩放大小。
- @param minNeighbors:指定符合条件的图像区域应该有多少个符合条件的相邻像素,才被认为是一个可能的图像区域。
- @param flags:参数 flags 是 OpenCV 1.x 版本 API 的遗留物,应该始终把它设置为 0。
- @param minSize: 检测可能图像的最小范围。小于该范围的图像会被忽略。
- @param maxSize:检测可能图像的最大范围。超过该范围的图像被忽略。如果“maxSize == minSize”则视为同一个范围。
二、识别人脸
上篇介绍过 OpenCV 自带了三个人脸识别算法:Eigenfaces,Fisherfaces 和LBPH(局部二值模式直方图)。
下面我们看一下它们的关系:
Eigenfaces,Fisherfaces 继承自 BasicFaceRecognizer,
BasicFaceRecognizer 再继承自 FaceRecognizer,
而 LBPH 直接继承自 FaceRecognizer,
cv::Algorithm 是这些算法的抽象基类。
3种算法关系图
区别:
- Eigenfaces,Fisherfaces 直接使用所有的像素来进行人脸识别,而 LBPH 采用的是提取局部特征。
- Eigenfaces,Fisherfaces 为了获取良好的识别率,至少每个人需要8张左右的图像来训练。
- LBPH 可以根据用户的输入自动更新,而不需要在每添加一个人或纠正一次出错的判断的时候都要重新进行一次彻底的训练
1. LBP理论基础
Local Binary Patterns 的基本思想是通过比较每个像素与其邻域来总结图像中的局部结构。以一个像素为中心,并对其邻居进行限制。如果中心像素的强度大于等于其邻居,那么用1表示它,否则用0表示。就像每个像素一样,你最终会得到一个二进制数。
因此,对于8个周围的像素,最终会有2 ^ 8个可能的组合,称为局部二进制模式或有时称为LBP代码。
原始的LBP算子定义为一个固定的3×3邻域,邻域内的8个点经比较可产生8位二进制数(通常转换为十进制数即LBP码,共256种),即得到该邻域中心像素点的LBP值,并用这个值来反映该区域的纹理特征。如下图所示:
原始的LBPgLBP的改进版本:
原始的LBP提出后,研究人员不断对其提出了各种改进和优化。
1.1 圆形LBP算子
基本的 LBP算子的最大缺陷在于它只覆盖了一个固定半径范围内的小区域,这显然不能满足不同尺寸和频率纹理的需要。为了适应不同尺度的纹理特征,Ojala等对LBP算子进行了改进,将3×3邻域扩展到任意邻域,并用圆形邻域代替了正方形邻域,改进后的LBP算子允许在半径为R的圆形邻域内有任意多个像素点,从而得到了诸如半径为R的圆形区域内含有P个采样点的LBP算子,OpenCV中正是使用圆形LBP算子,下图示意了圆形LBP算子:
圆形LBP算子1.2 旋转不变模式
从LBP的定义可以看出,LBP算子是灰度不变的,但却不是旋转不变的,图像的旋转就会得到不同的LBP值。Maenpaa等人又将LBP算子进行了扩展,提出了具有旋转不变性的LBP算子,即不断旋转圆形邻域得到一系列初始定义的LBP值,取其最小值作为该邻域的LBP值。下图给出了求取旋转不变LBP的过程示意图,图中算子下方的数字表示该算子对应的LBP值,图中所示的8种LBP模式,经过旋转不变的处理,最终得到的具有旋转不变性的LBP值为15。也就是说,图中的8种LBP模式对应的旋转不变的LBP码值都是00001111。
旋转不变LBP 根据定义,LBP算子对单调灰度变换具有健壮性。我们可以通过查看人工修改图像的LBP图像来轻松验证这一点: lbp_yale.jpg二·、使用LBPH识别人脸
LBPH 继承自 FaceRecognizer,FaceRecognizer 实际是通过生成本地的 model.xml 文件进行 read, write,update,predict。我们可以在#import <opencv2/face.hpp>
头文件看到这几个主要的函数的具体使用。
- 首先我们创建
HVFaceRecognizerUitl
初始化函数,在函数中创建实例Ptr<LBPHFaceRecognizer> _faceRecognizer
- 创建和实现 read, write,update,predict 函数。
具体代码实现如下:
#ifdef __cplusplus
#import <opencv2/opencv.hpp>
#import <opencv2/face.hpp>
#endif
using namespace cv;
using namespace face;
@interface HVFaceRecognizerUtil()
{
Ptr<LBPHFaceRecognizer> _faceRecognizer;
}
@property (nonatomic,strong) NSMutableDictionary *labelsDic;
@end
@implementation HVFaceRecognizerUtil
+ (HVFaceRecognizerUtil *)faceRecWithFile:(NSString *)path
{
//OpenCV 3.X 之后的版本创建 LBPH 的实例由旧的方法
createLBPHFaceRecognizer() 改为:
LBPHFaceRecognizer::create()。
HVFaceRecognizerUtil *faceRec = [HVFaceRecognizerUtil new];
faceRec->_faceRecognizer = LBPHFaceRecognizer::create();
NSFileManager *fm = [NSFileManager defaultManager];
if (path && [fm fileExistsAtPath:path isDirectory:nil]) {
[faceRec readFaceRecParamatersFromFile:path];
}else
{
faceRec.labelsDic = [[NSMutableDictionary alloc]init];
NSLog(@"could not load paramaters file: %@", path);
}
return faceRec;
}
#pragma mark - FaceRec read/write
- (BOOL)readFaceRecParamatersFromFile:(NSString *)path
{
self->_faceRecognizer->read(path.UTF8String);
NSDictionary *unarchiverNames = [NSKeyedUnarchiver
unarchiveObjectWithFile:[path stringByAppendingString:@".names"]];
self.labelsDic = [NSMutableDictionary dictionaryWithDictionary:unarchiverNames];
return YES;
}
- (BOOL)writeFaceRecParamatersToFile:(NSString *)path
{
self->_faceRecognizer->write(path.UTF8String);
[NSKeyedArchiver archiveRootObject:self.labelsDic toFile:[path stringByAppendingString:@".names"]];
return YES;
}
#pragma mark - FaceRec predict/update
//根据脸部图片的灰度图匹配出对应的标签,通过对应的标签获取人名
- (NSString *)predict:(UIImage *)image confidence:(double *)confidence
{
//原图转成灰度图
cv::Mat src = [UIImage cvMatGrayFromUIImage:image];
int label;
//@param src:样本图像得到一个预测。
//@param label:给定的图像标记预测的标签。
//@param confidence:预测的置信度(例如距离)。
self->_faceRecognizer->predict(src, label, *confidence);
//返回标签对应的人名
return self.labelsDic[@(label)];
}
- (void)updateFace:(UIImage *)faceImg name:(NSString *)name
{
//原图转成灰度图
cv::Mat src = [UIImage cvMatGrayFromUIImage:faceImg];
NSSet *keys = [self.labelsDic keysOfEntriesPassingTest:^BOOL(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
return [name isEqual:obj];
}];
NSInteger label;
if (keys.count) {
label = [[keys anyObject] integerValue];
}else
{
label = self.labelsDic.allKeys.count;
[self.labelsDic setObject:name forKey:@(label)];
}
std::vector<Mat> newImages = std::vector<cv::Mat>();;
std::vector<int> newLabels = std::vector<int>();
newImages.push_back(src);
newLabels.push_back((int)label);
_faceRecognizer->update(newImages, newLabels);
[self labels];
}
- (NSArray *)labels
{
cv::Mat labels = _faceRecognizer->getLabels();
if (labels.total() == 0) {
return @[];
}
else {
NSMutableArray *mutableArray = [NSMutableArray array];
for (MatConstIterator_<int> itr = labels.begin<int>(); itr != labels.end<int>(); ++itr ) {
int lbl = *itr;
[mutableArray addObject:@(lbl)];
}
return [NSArray arrayWithArray:mutableArray];
}
}
- 将
HVFaceRecognizerUitl
实现在识别人脸的视图HVFaceRecViewController
,通过按钮对识别的结果确认和修正:
@interface HVFaceRecViewController ()
@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
@property (weak, nonatomic) IBOutlet UILabel *confidenceLabel;
@property (weak, nonatomic) IBOutlet UIImageView *inputImageView;
@property (nonatomic, strong) HVFaceRecognizerUtil *faceModel;
@end
@implementation HVFaceRecViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_inputImageView.image = _inputImage;
NSString *modelPath = [self faceModelFilePath];
self.faceModel = [HVFaceRecognizerUtil faceRecWithFile:modelPath];
if (_faceModel.labels.count == 0) {
[_faceModel updateFace:_inputImage name:@"朱茵"];
}
double confidence;
NSString *name = [_faceModel predict:_inputImage confidence:&confidence];
_nameLabel.text = name;
_confidenceLabel.text = [@(confidence) stringValue];
}
- (NSString *)faceModelFilePath {
NSString *modelPath = [NSString pathFromFlieName:@"face-model.xml"];
NSLog(@">>> modelPath[face-model.xml] = %@ ",modelPath);
return modelPath;
}
- (IBAction)didTapCorrect:(id)sender {
//Positive feedback for the correct prediction
[_faceModel updateFace:_inputImage name:_nameLabel.text];
[_faceModel writeFaceRecParamatersToFile:[self faceModelFilePath]];
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)didTapWrong:(id)sender {
//Update our face model with the new person
// NSString *name = [@"Person " stringByAppendingFormat:@"%lu", (unsigned long)_faceModel.labels.count];
NSString *name = @"至尊宝";
[_faceModel updateFace:_inputImage name:name];
[_faceModel writeFaceRecParamatersToFile:[self faceModelFilePath]];
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
详细代码已上传到我的GitHub:
注:Demo不包含opencv2.framework,我手动编译的 Opencv+Contrib 库版本为 3.4.1,大约407MB上传不了,Git上传单个文件只允许<100MB,所以你可以在这个地址下载我编译好的库:Opencv+Contrib-3.4.1,如有遇到问题,请留言。
上一篇: iOS-OpenCV笔记:实现简单的人脸识别(一)