上篇 iOS-OpenCV筆記:實(shí)現(xiàn)簡(jiǎn)單的人臉識(shí)別(一)著重介紹了OpenCV的基本知識(shí)和在iOS上的編譯過程泊交,本篇將通過代碼和API了解整個(gè)人臉的識(shí)別過程市框。
人臉識(shí)別主要分兩部分:
我將這兩部分的功能分別實(shí)現(xiàn)在這兩個(gè)類下:
-
HVFaceDetectorUtil
:負(fù)責(zé)檢測(cè)和收集人臉 -
HVFaceRecognizerUitl
:負(fù)責(zé)識(shí)別人臉
一哈踱、檢測(cè)人臉
iPhone通過攝像頭獲取到視頻流望抽,對(duì)每一幀的圖片持續(xù)進(jìn)行檢測(cè)溪窒,來捕捉到圖片中人臉的區(qū)域夯接。
- 首先通過
HVFaceDetectorUtil
類的初始化獲取CvVideoCamera *videoCamera
屬性的實(shí)例闲擦,并設(shè)置代理慢味,再加載工程中的訓(xùn)練好的 HaarCascade xml 文件,創(chuàng)建人臉和眼睛檢測(cè)的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;
//加載項(xiàng)目中訓(xùn)練好的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常用來做人臉檢測(cè)佛致,其實(shí)它可以檢測(cè)任何對(duì)象贮缕。
-
OpenCV 項(xiàng)目源碼中有很多訓(xùn)練好的Haar分類器,它們?cè)?/oenncv/data/haarcascades 文件夾路徑中可以找到如下:
Haar Cascade list
- 然后實(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í)間超過 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 );
//開啟時(shí)間計(jì)時(shí)器
t = (double)cvGetTickCount();
//決定每次遍歷分類器后尺度會(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);
//通過檢測(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;
//畫正方形
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è)到的眼睛畫圓
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;
}
}
- 下面我們來詳細(xì)研究一下獲取檢測(cè)圖像列表的關(guān)鍵函數(shù)
detectMultiScale
,以及它所需傳入的參數(shù)定義:
//通過檢測(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類型的圖像矩陣,待檢測(cè)圖片诲祸,一般為灰度圖像浊吏,加快檢測(cè)速度而昨。
- @param objects:包含所有被檢測(cè)出的圖像的矩形列表,這些矩形可能部分位于原始圖像之外找田。
- @param scaleFactor:指定每次遍歷分類器后每張圖像尺度的縮放大小歌憨。
- @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è)可能圖像的最大范圍。超過該范圍的圖像被忽略籽懦。如果“maxSize == minSize”則視為同一個(gè)范圍于个。
二、識(shí)別人臉
上篇介紹過 OpenCV 自帶了三個(gè)人臉識(shí)別算法:Eigenfaces暮顺,F(xiàn)isherfaces 和LBPH(局部二值模式直方圖)厅篓。
下面我們看一下它們的關(guān)系:
Eigenfaces,F(xiàn)isherfaces 繼承自 BasicFaceRecognizer捶码,
BasicFaceRecognizer 再繼承自 FaceRecognizer羽氮,
而 LBPH 直接繼承自 FaceRecognizer,
cv::Algorithm 是這些算法的抽象基類惫恼。
區(qū)別:
- Eigenfaces档押,F(xiàn)isherfaces 直接使用所有的像素來進(jìn)行人臉識(shí)別,而 LBPH 采用的是提取局部特征祈纯。
- Eigenfaces令宿,F(xiàn)isherfaces 為了獲取良好的識(shí)別率,至少每個(gè)人需要8張左右的圖像來訓(xùn)練腕窥。
- LBPH 可以根據(jù)用戶的輸入自動(dòng)更新粒没,而不需要在每添加一個(gè)人或糾正一次出錯(cuò)的判斷的時(shí)候都要重新進(jìn)行一次徹底的訓(xùn)練
1. LBP理論基礎(chǔ)
Local Binary Patterns 的基本思想是通過比較每個(gè)像素與其鄰域來總結(jié)圖像中的局部結(jié)構(gòu)。以一個(gè)像素為中心簇爆,并對(duì)其鄰居進(jìn)行限制癞松。如果中心像素的強(qiáng)度大于等于其鄰居,那么用1表示它入蛆,否則用0表示响蓉。就像每個(gè)像素一樣,你最終會(huì)得到一個(gè)二進(jìn)制數(shù)安寺。
因此厕妖,對(duì)于8個(gè)周圍的像素首尼,最終會(huì)有2 ^ 8個(gè)可能的組合挑庶,稱為局部二進(jìn)制模式或有時(shí)稱為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è)值來反映該區(qū)域的紋理特征凳枝。如下圖所示:
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算子:
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的過程示意圖,圖中算子下方的數(shù)字表示該算子對(duì)應(yīng)的LBP值甫贯,圖中所示的8種LBP模式吼鳞,經(jīng)過旋轉(zhuǎn)不變的處理,最終得到的具有旋轉(zhuǎn)不變性的LBP值為15叫搁。也就是說赔桌,圖中的8種LBP模式對(duì)應(yīng)的旋轉(zhuǎn)不變的LBP碼值都是00001111。
二·、使用LBPH識(shí)別人臉
LBPH 繼承自 FaceRecognizer惨奕,F(xiàn)aceRecognizer 實(shí)際是通過生成本地的 model.xml 文件進(jìn)行 read雪位, write,update梨撞,predict雹洗。我們可以在#import <opencv2/face.hpp>
頭文件看到這幾個(gè)主要的函數(shù)的具體使用香罐。
- 首先我們創(chuàng)建
HVFaceRecognizerUitl
初始化函數(shù),在函數(shù)中創(chuàng)建實(shí)例Ptr<LBPHFaceRecognizer> _faceRecognizer
- 創(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)簽,通過對(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];
}
}
- 將
HVFaceRecognizerUitl
實(shí)現(xiàn)在識(shí)別人臉的視圖HVFaceRecViewController
氮凝,通過按鈕對(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:
注:Demo不包含opencv2.framework淋淀,我手動(dòng)編譯的 Opencv+Contrib 庫版本為 3.4.1,大約407MB上傳不了覆醇,Git上傳單個(gè)文件只允許<100MB朵纷,所以你可以在這個(gè)地址下載我編譯好的庫:Opencv+Contrib-3.4.1,如有遇到問題永脓,請(qǐng)留言袍辞。
上一篇: iOS-OpenCV筆記:實(shí)現(xiàn)簡(jiǎn)單的人臉識(shí)別(一)