iOS开发OpenCviOS 图像处理

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

2018-03-26  本文已影响293人  Harveyhhw
人脸识别

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

人脸识别主要分两部分:

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

一、检测人脸

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];
}
  1. 然后实现 CvVideoCamera *videoCamera 的代理函数- (void)processImage:(cv::Mat&)image,对每一帧的图片进行检测:
#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() );

二、识别人脸

上篇介绍过 OpenCV 自带了三个人脸识别算法:Eigenfaces,Fisherfaces 和LBPH(局部二值模式直方图)。

下面我们看一下它们的关系:

Eigenfaces,Fisherfaces 继承自 BasicFaceRecognizer,
BasicFaceRecognizer 再继承自 FaceRecognizer,
而 LBPH 直接继承自 FaceRecognizer,
cv::Algorithm 是这些算法的抽象基类。


3种算法关系图
区别:

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笔记:实现简单的人脸识别(一)

参考资料:
上一篇下一篇

猜你喜欢

热点阅读