iOS OpenCV:詳解人臉識(shí)別原理(二)

人臉識(shí)別

上篇 iOS-OpenCV筆記:實(shí)現(xiàn)簡(jiǎn)單的人臉識(shí)別(一)著重介紹了OpenCV的基本知識(shí)和在iOS上的編譯過(guò)程,本篇將通過(guò)代碼和API了解整個(gè)人臉的識(shí)別過(guò)程。

人臉識(shí)別主要分兩部分:

我將這兩部分的功能分別實(shí)現(xiàn)在這兩個(gè)類(lèi)下:

  • HVFaceDetectorUtil:負(fù)責(zé)檢測(cè)和收集人臉
  • HVFaceRecognizerUitl:負(fù)責(zé)識(shí)別人臉

一、檢測(cè)人臉

iPhone通過(guò)攝像頭獲取到視頻流,對(duì)每一幀的圖片持續(xù)進(jìn)行檢測(cè),來(lái)捕捉到圖片中人臉的區(qū)域。

  1. 首先通過(guò) HVFaceDetectorUtil 類(lèi)的初始化獲取 CvVideoCamera *videoCamera 屬性的實(shí)例,并設(shè)置代理,再加載工程中的訓(xùn)練好的 HaarCascade xml 文件,創(chuàng)建人臉和眼睛檢測(cè)的Haar分類(lèi)器:
#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;
               
        //加載項(xiàng)目中訓(xùn)練好的Haar分類(lèi)器
        //正面臉部Haar分類(lèi)器
        NSString *faceCascadePath = [[NSBundle mainBundle]
                                     pathForResource:@"haarcascade_frontalface_alt2"
                                     ofType:@"xml"];
        
        _faceDetector.load([faceCascadePath UTF8String]);
        
        //眼睛部位Haar分類(lèi)器
        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常用來(lái)做人臉檢測(cè),其實(shí)它可以檢測(cè)任何對(duì)象。
  • OpenCV 項(xiàng)目源碼中有很多訓(xùn)練好的Haar分類(lèi)器,它們?cè)?/oenncv/data/haarcascades 文件夾路徑中可以找到如下:


    Haar Cascade list
  1. 然后實(shí)現(xiàn) CvVideoCamera *videoCamera 的代理函數(shù)- (void)processImage:(cv::Mat&)image,對(duì)每一幀的圖片進(jìn)行檢測(cè):
  • 攝像頭的幀率被設(shè)置為30幀每秒,實(shí)現(xiàn)的 processImage 函數(shù)將每秒被調(diào)用30次。
  • 因?yàn)橐掷m(xù)不斷地檢測(cè)人臉,所以在這個(gè)函數(shù)里實(shí)現(xiàn)人臉的檢測(cè)。
  • 要注意的是,如果對(duì)某一幀進(jìn)行人臉檢測(cè)的時(shí)間超過(guò) 1/30 秒,就會(huì)產(chǎn)生掉幀現(xiàn)象。
#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;
    //劃線顏色數(shù)組
    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 );
    
    //將圖片轉(zhuǎn)成灰度圖
    cvtColor( img, gray, cv::COLOR_BGR2GRAY );
    
    ////修改圖片尺寸,壓縮成小圖
    resize( gray, smallImg, smallImg.size(), 0, 0, cv::INTER_LINEAR );
    //直方圖均衡化︰ 在低光照條件下的人臉檢測(cè)是不可靠的,所以我們應(yīng)該執(zhí)行直方圖均衡化
    equalizeHist( smallImg, smallImg );
    
    //開(kāi)啟時(shí)間計(jì)時(shí)器
    t = (double)cvGetTickCount();
    
    //決定每次遍歷分類(lèi)器后尺度會(huì)變大多少倍
    double scalingFactor = 1.1;
    
    //指定一個(gè)符合條件的人臉區(qū)域應(yīng)該有多少個(gè)符合條件的鄰居像素才被認(rèn)為是一個(gè)可能的人臉區(qū)域,
    //擁有少于 minNeighbors 個(gè)符合條件的鄰居像素的人臉區(qū)域會(huì)被拒絕掉。
    int minNeighbors = 2;
    
    //設(shè)定檢測(cè)人臉區(qū)域范圍的最小值
    cv::Size minSize(30,30);
    //設(shè)定檢測(cè)人臉區(qū)域范圍的最大值
    cv::Size maxSize(280,280);
    
    //通過(guò)檢測(cè)輸入不同大小的圖像,獲取被檢測(cè)到的圖像列表 
    //圖像對(duì)象會(huì)作為一個(gè)矩形列表返回:self->_faceRects。
    self->_faceDetector.detectMultiScale(smallImg, self->_faceRects,
                                         scalingFactor, minNeighbors, 0,
                                         minSize,maxSize);
    
    //計(jì)算檢測(cè)所花費(fèi)的時(shí)間
    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;
        
        //畫(huà)正方形
        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());
        
        //檢測(cè)眼睛
        self->_eyesDetector.detectMultiScale( smallImgROI,
                                             nestedObjects,
                                             1.1, 2, 0,
                                             cv::Size(1, 1) );
        
        //將檢測(cè)到的眼睛畫(huà)圓
        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. 下面我們來(lái)詳細(xì)研究一下獲取檢測(cè)圖像列表的關(guān)鍵函數(shù) detectMultiScale ,以及它所需傳入的參數(shù)定義:
//通過(guò)檢測(cè)輸入不同大小的圖像,獲取被檢測(cè)到的圖像列表 
//檢測(cè)出的對(duì)象會(huì)作為一個(gè)矩形列表返回: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類(lèi)型的圖像矩陣,待檢測(cè)圖片,一般為灰度圖像,加快檢測(cè)速度。
  • @param objects:包含所有被檢測(cè)出的圖像的矩形列表,這些矩形可能部分位于原始圖像之外。
  • @param scaleFactor:指定每次遍歷分類(lèi)器后每張圖像尺度的縮放大小。
  • @param minNeighbors:指定符合條件的圖像區(qū)域應(yīng)該有多少個(gè)符合條件的相鄰像素,才被認(rèn)為是一個(gè)可能的圖像區(qū)域。
  • @param flags:參數(shù) flags 是 OpenCV 1.x 版本 API 的遺留物,應(yīng)該始終把它設(shè)置為 0。
  • @param minSize: 檢測(cè)可能圖像的最小范圍。小于該范圍的圖像會(huì)被忽略。
  • @param maxSize:檢測(cè)可能圖像的最大范圍。超過(guò)該范圍的圖像被忽略。如果“maxSize == minSize”則視為同一個(gè)范圍。

二、識(shí)別人臉

上篇介紹過(guò) OpenCV 自帶了三個(gè)人臉識(shí)別算法:Eigenfaces,F(xiàn)isherfaces 和LBPH(局部二值模式直方圖)。

下面我們看一下它們的關(guān)系:

Eigenfaces,F(xiàn)isherfaces 繼承自 BasicFaceRecognizer,
BasicFaceRecognizer 再繼承自 FaceRecognizer,
而 LBPH 直接繼承自 FaceRecognizer,
cv::Algorithm 是這些算法的抽象基類(lèi)。


3種算法關(guān)系圖
區(qū)別:
  • Eigenfaces,F(xiàn)isherfaces 直接使用所有的像素來(lái)進(jìn)行人臉識(shí)別,而 LBPH 采用的是提取局部特征。
  • Eigenfaces,F(xiàn)isherfaces 為了獲取良好的識(shí)別率,至少每個(gè)人需要8張左右的圖像來(lái)訓(xùn)練。
  • LBPH 可以根據(jù)用戶的輸入自動(dòng)更新,而不需要在每添加一個(gè)人或糾正一次出錯(cuò)的判斷的時(shí)候都要重新進(jìn)行一次徹底的訓(xùn)練

1. LBP理論基礎(chǔ)

Local Binary Patterns 的基本思想是通過(guò)比較每個(gè)像素與其鄰域來(lái)總結(jié)圖像中的局部結(jié)構(gòu)。以一個(gè)像素為中心,并對(duì)其鄰居進(jìn)行限制。如果中心像素的強(qiáng)度大于等于其鄰居,那么用1表示它,否則用0表示。就像每個(gè)像素一樣,你最終會(huì)得到一個(gè)二進(jìn)制數(shù)。
因此,對(duì)于8個(gè)周?chē)南袼?,最終會(huì)有2 ^ 8個(gè)可能的組合,稱(chēng)為局部二進(jìn)制模式或有時(shí)稱(chēng)為L(zhǎng)BP代碼。

原始的LBP算子定義為一個(gè)固定的3×3鄰域,鄰域內(nèi)的8個(gè)點(diǎn)經(jīng)比較可產(chǎn)生8位二進(jìn)制數(shù)(通常轉(zhuǎn)換為十進(jìn)制數(shù)即LBP碼,共256種),即得到該鄰域中心像素點(diǎn)的LBP值,并用這個(gè)值來(lái)反映該區(qū)域的紋理特征。如下圖所示:

原始的LBPg
LBP的改進(jìn)版本:

原始的LBP提出后,研究人員不斷對(duì)其提出了各種改進(jìn)和優(yōu)化。

1.1 圓形LBP算子

基本的 LBP算子的最大缺陷在于它只覆蓋了一個(gè)固定半徑范圍內(nèi)的小區(qū)域,這顯然不能滿足不同尺寸和頻率紋理的需要。為了適應(yīng)不同尺度的紋理特征,Ojala等對(duì)LBP算子進(jìn)行了改進(jìn),將3×3鄰域擴(kuò)展到任意鄰域,并用圓形鄰域代替了正方形鄰域,改進(jìn)后的LBP算子允許在半徑為R的圓形鄰域內(nèi)有任意多個(gè)像素點(diǎn),從而得到了諸如半徑為R的圓形區(qū)域內(nèi)含有P個(gè)采樣點(diǎn)的LBP算子,OpenCV中正是使用圓形LBP算子,下圖示意了圓形LBP算子:

圓形LBP算子
1.2 旋轉(zhuǎn)不變模式

從LBP的定義可以看出,LBP算子是灰度不變的,但卻不是旋轉(zhuǎn)不變的,圖像的旋轉(zhuǎn)就會(huì)得到不同的LBP值。Maenpaa等人又將LBP算子進(jìn)行了擴(kuò)展,提出了具有旋轉(zhuǎn)不變性的LBP算子,即不斷旋轉(zhuǎn)圓形鄰域得到一系列初始定義的LBP值,取其最小值作為該鄰域的LBP值。下圖給出了求取旋轉(zhuǎn)不變LBP的過(guò)程示意圖,圖中算子下方的數(shù)字表示該算子對(duì)應(yīng)的LBP值,圖中所示的8種LBP模式,經(jīng)過(guò)旋轉(zhuǎn)不變的處理,最終得到的具有旋轉(zhuǎn)不變性的LBP值為15。也就是說(shuō),圖中的8種LBP模式對(duì)應(yīng)的旋轉(zhuǎn)不變的LBP碼值都是00001111。

旋轉(zhuǎn)不變LBP

根據(jù)定義,LBP算子對(duì)單調(diào)灰度變換具有健壯性。我們可以通過(guò)查看人工修改圖像的LBP圖像來(lái)輕松驗(yàn)證這一點(diǎn):
lbp_yale.jpg

二·、使用LBPH識(shí)別人臉

LBPH 繼承自 FaceRecognizer,F(xiàn)aceRecognizer 實(shí)際是通過(guò)生成本地的 model.xml 文件進(jìn)行 read, write,update,predict。我們可以在#import <opencv2/face.hpp>頭文件看到這幾個(gè)主要的函數(shù)的具體使用。

  1. 首先我們創(chuàng)建 HVFaceRecognizerUitl 初始化函數(shù),在函數(shù)中創(chuàng)建實(shí)例 Ptr<LBPHFaceRecognizer> _faceRecognizer
  2. 創(chuàng)建和實(shí)現(xiàn) read, write,update,predict 函數(shù)。
    具體代碼實(shí)現(xiàn)如下:
#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 之后的版本創(chuàng)建 LBPH 的實(shí)例由舊的方法
    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
//根據(jù)臉部圖片的灰度圖匹配出對(duì)應(yīng)的標(biāo)簽,通過(guò)對(duì)應(yīng)的標(biāo)簽獲取人名
- (NSString *)predict:(UIImage *)image confidence:(double *)confidence
{
    //原圖轉(zhuǎn)成灰度圖
    cv::Mat src = [UIImage cvMatGrayFromUIImage:image];
    int label;
    
    //@param src:樣本圖像得到一個(gè)預(yù)測(cè)。
    //@param label:給定的圖像標(biāo)記預(yù)測(cè)的標(biāo)簽。
    //@param confidence:預(yù)測(cè)的置信度(例如距離)。
    self->_faceRecognizer->predict(src, label, *confidence);
    
    //返回標(biāo)簽對(duì)應(yīng)的人名
    return self.labelsDic[@(label)];
}

- (void)updateFace:(UIImage *)faceImg name:(NSString *)name
{
    //原圖轉(zhuǎn)成灰度圖
    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 實(shí)現(xiàn)在識(shí)別人臉的視圖 HVFaceRecViewController,通過(guò)按鈕對(duì)識(shí)別的結(jié)果確認(rèn)和修正:
@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];
}
詳細(xì)代碼已上傳到我的GitHub:

OpenCV-iOS-FaceRecDemo

注:Demo不包含opencv2.framework,我手動(dòng)編譯的 Opencv+Contrib 庫(kù)版本為 3.4.1,大約407MB上傳不了,Git上傳單個(gè)文件只允許<100MB,所以你可以在這個(gè)地址下載我編譯好的庫(kù):Opencv+Contrib-3.4.1,如有遇到問(wèn)題,請(qǐng)留言。

上一篇: iOS-OpenCV筆記:實(shí)現(xiàn)簡(jiǎn)單的人臉識(shí)別(一)

參考資料:
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容