iOS OpenCV:详解人脸识别原理(二)

人脸识别

上篇 iOS-OpenCV笔记:实现简单的人脸识别(一)着重介绍了OpenCV的基本知识和在iOS上的编译过程,本篇将通过代码和API了解整个人脸的识别过程。

人脸识别主要分两部分:

我将这两部分的功能分别实现在这两个类下:

  • HVFaceDetectorUtil:负责检测和收集人脸
  • HVFaceRecognizerUitl:负责识别人脸

一、检测人脸

iPhone通过摄像头获取到视频流,对每一帧的图片持续进行检测,来捕捉到图片中人脸的区域。

  1. 首先通过 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
  1. 然后实现 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;
    }
}
  1. 下面我们来详细研究一下获取检测图像列表的关键函数 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值,并用这个值来反映该区域的纹理特征。如下图所示:

原始的LBPg
LBP的改进版本:

原始的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>头文件看到这几个主要的函数的具体使用。

  1. 首先我们创建 HVFaceRecognizerUitl 初始化函数,在函数中创建实例 Ptr<LBPHFaceRecognizer> _faceRecognizer
  2. 创建和实现 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];
    }
}
  1. 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:

OpenCV-iOS-FaceRecDemo

注:Demo不包含opencv2.framework,我手动编译的 Opencv+Contrib 库版本为 3.4.1,大约407MB上传不了,Git上传单个文件只允许<100MB,所以你可以在这个地址下载我编译好的库:Opencv+Contrib-3.4.1,如有遇到问题,请留言。

上一篇: iOS-OpenCV笔记:实现简单的人脸识别(一)

参考资料:
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260