基于 OpenCV 的人脸识别
一點背景知識
OpenCV 是一個開源的計算機視覺和機器學習庫。它包含成千上萬優(yōu)化過的算法,為各種計算機視覺應(yīng)用提供了一個通用工具包。根據(jù)這個項目的關(guān)于頁面,OpenCV 已被廣泛運用在各種項目上,從谷歌街景的圖片拼接,到交互藝術(shù)展覽的技術(shù)實現(xiàn)中,都有 OpenCV 的身影。
OpenCV 起始于 1999 年 Intel 的一個內(nèi)部研究項目。從那時起,它的開發(fā)就一直很活躍。進化到現(xiàn)在,它已支持如 OpenCL 和 OpenGL 等現(xiàn)代技術(shù),也支持如 iOS 和 Android 等平臺。
1999 年,半條命發(fā)布后大紅大熱。Intel 奔騰 3 處理器是當時最高級的 CPU,400-500 MHZ 的時鐘頻率已被認為是相當快。2006 年 OpenCV 1.0 版本發(fā)布的時候,當時主流 CPU 的性能也只和 iPhone 5 的 A6 處理器相當。盡管計算機視覺從傳統(tǒng)上被認為是計算密集型應(yīng)用,但我們的移動設(shè)備性能已明顯地超出能夠執(zhí)行有用的計算機視覺任務(wù)的閾值,帶著攝像頭的移動設(shè)備可以在計算機視覺平臺上大有所為。
在本文中,我會從一個 iOS 開發(fā)者的視角概述一下 OpenCV,并介紹一點基礎(chǔ)的類和概念。隨后,會講到如何集成 OpenCV 到你的 iOS 項目中以及一些 Objective-C++ 基礎(chǔ)知識。最后,我們會看一個 demo 項目,看看如何在 iOS 設(shè)備上使用 OpenCV 實現(xiàn)人臉檢測與人臉識別。
OpenCV 概述
概念
OpenCV 的 API 是 C++ 的。它由不同的模塊組成,這些模塊中包含范圍極為廣泛的各種方法,從底層的圖像顏色空間轉(zhuǎn)換到高層的機器學習工具。
使用 C++ API 并不是絕大多數(shù) iOS 開發(fā)者每天都做的事,你需要使用 Objective-C++ 文件來調(diào)用 OpenCV 的函數(shù)。 也就是說,你不能在 Swift 或者 Objective-C 語言內(nèi)調(diào)用 OpenCV 的函數(shù)。 這篇 OpenCV 的?iOS 教程告訴你只要把所有用到 OpenCV 的類的文件后綴名改為?.mm?就行了,包括視圖控制器類也是如此。這么干或許能行得通,卻不是什么好主意。正確的方式是給所有你要在 app 中使用到的 OpenCV 功能寫一層 Objective-C++ 封裝。這些 Objective-C++ 封裝把 OpenCV 的 C++ API 轉(zhuǎn)化為安全的 Objective-C API,以方便地在所有 Objective-C 類中使用。走封裝的路子,你的工程中就可以只在這些封裝中調(diào)用 C++ 代碼,從而避免掉很多讓人頭痛的問題,比如直接改文件后綴名會因為在錯誤的文件中引用了一個 C++ 頭文件而產(chǎn)生難以追蹤的編譯錯誤。
OpenCV 聲明了命名空間?cv,因此 OpenCV 的類的前面會有個?cv::?前綴,就像?cv::Mat、?cv::Algorithm?等等。你也可以在?.mm?文件中使用?using namespace cv?來避免在一堆類名前使用?cv::?前綴。但是,在某些類名前你必須使用命名空間前綴,比如?cv::Rect?和?cv::Point,因為它們會跟定義在?MacTypes.h?中的?Rect?和?Point?相沖突。盡管這只是個人偏好問題,我還是偏向在任何地方都使用?cv::?以保持一致性。
模塊
下面是在官方文檔中列出的最重要的模塊。
- core:簡潔的核心模塊,定義了基本的數(shù)據(jù)結(jié)構(gòu),包括稠密多維數(shù)組?Mat?和其他模塊需要的基本函數(shù)。
- imgproc:圖像處理模塊,包括線性和非線性圖像濾波、幾何圖像轉(zhuǎn)換 (縮放、仿射與透視變換、一般性基于表的重映射)、顏色空間轉(zhuǎn)換、直方圖等等。
- video:視頻分析模塊,包括運動估計、背景消除、物體跟蹤算法。
- calib3d:包括基本的多視角幾何算法、單體和立體相機的標定、對象姿態(tài)估計、雙目立體匹配算法和元素的三維重建。
- features2d:包含了顯著特征檢測算法、描述算子和算子匹配算法。
- objdetect:物體檢測和一些預定義的物體的檢測 (如人臉、眼睛、杯子、人、汽車等)。
- ml:多種機器學習算法,如 K 均值、支持向量機和神經(jīng)網(wǎng)絡(luò)。
- highgui:一個簡單易用的接口,提供視頻捕捉、圖像和視頻編碼等功能,還有簡單的 UI 接口 (iOS 上可用的僅是其一個子集)。
- gpu:OpenCV 中不同模塊的 GPU 加速算法 (iOS 上不可用)。
- ocl:使用 OpenCL 實現(xiàn)的通用算法 (iOS 上不可用)。
- 一些其它輔助模塊,如 Python 綁定和用戶貢獻的算法。
基礎(chǔ)類和操作
OpenCV 包含幾百個類。為簡便起見,我們只看幾個基礎(chǔ)的類和操作,進一步閱讀請參考全部文檔。過一遍這幾個核心類應(yīng)該足以對這個庫的機理產(chǎn)生一些感覺認識。
cv::Mat
cv::Mat?是 OpenCV 的核心數(shù)據(jù)結(jié)構(gòu),用來表示任意 N 維矩陣。因為圖像只是 2 維矩陣的一個特殊場景,所以也是使用?cv::Mat來表示的。也就是說,cv::Mat?將是你在 OpenCV 中用到最多的類。
一個?cv::Mat?實例的作用就像是圖像數(shù)據(jù)的頭,其中包含著描述圖像格式的信息。圖像數(shù)據(jù)只是被引用,并能為多個?cv::Mat?實例共享。OpenCV 使用類似于 ARC 的引用計數(shù)方法,以保證當最后一個來自?cv::Mat?的引用也消失的時候,圖像數(shù)據(jù)會被釋放。圖像數(shù)據(jù)本身是圖像連續(xù)的行的數(shù)組 (對 N 維矩陣來說,這個數(shù)據(jù)是由連續(xù)的 N-1 維數(shù)據(jù)組成的數(shù)組)。使用?step[]?數(shù)組中包含的值,圖像的任一像素地址都可通過下面的指針運算得到:
uchar *pixelPtr = cvMat.data + rowIndex * cvMat.step[0] + colIndex * cvMat.step[1]每個像素的數(shù)據(jù)格式可以通過?type()?方法獲得。除了常用的每通道 8 位無符號整數(shù)的灰度圖 (1 通道,CV_8UC1) 和彩色圖 (3 通道,CV_8UC3),OpenCV 還支持很多不常用的格式,例如?CV_16SC3?(每像素 3 通道,每通道使用 16 位有符號整數(shù)),甚至CV_64FC4?(每像素 4 通道,每通道使用 64 位浮點數(shù))。
cv::Algorithm
Algorithm?是 OpenCV 中實現(xiàn)的很多算法的抽象基類,包括將在我們的 demo 工程中用到的?FaceRecognizer。它提供的 API 與蘋果的 Core Image 框架中的?CIFilter?有些相似之處。創(chuàng)建一個?Algorithm?的時候使用算法的名字來調(diào)用Algorithm::create(),并且可以通過?get()?和?set()方法來獲取和設(shè)置各個參數(shù),這有點像是鍵值編碼。另外,Algorithm從底層就支持從/向 XML 或 YAML 文件加載/保存參數(shù)的功能。
在 iOS 上使用 OpenCV
添加 OpenCV 到你的工程中
集成 OpenCV 到你的工程中有三種方法:
- 使用 CocoaPods 就好:?pod "OpenCV"。
- 下載官方?iOS 框架發(fā)行包,并把它添加到工程里。
- 從?GitHub?拉下代碼,并根據(jù)教程自己編譯 OpenCV 庫。
Objective-C++
如前面所說,OpenCV 是一個 C++ 的 API,因此不能直接在 Swift 和 Objective-C 代碼中使用,但能在 Objective-C++ 文件中使用。
Objective-C++ 是 Objective-C 和 C++ 的混合物,讓你可以在 Objective-C 類中使用 C++ 對象。clang 編譯器會把所有后綴名為?.mm?的文件都當做是 Objective-C++。一般來說,它會如你所期望的那樣運行,但還是有一些使用 Objective-C++ 的注意事項。內(nèi)存管理是你最應(yīng)該格外注意的點,因為 ARC 只對 Objective-C 對象有效。當你使用一個 C++ 對象作為類屬性的時候,其唯一有效的屬性就是?assign。因此,你的?dealloc?函數(shù)應(yīng)確保 C++ 對象被正確地釋放了。
第二重要的點就是,如果你在 Objective-C++ 頭文件中引入了 C++ 頭文件,當你在工程中使用該 Objective-C++ 文件的時候就泄露了 C++ 的依賴。任何引入你的 Objective-C++ 類的 Objective-C 類也會引入該 C++ 類,因此該 Objective-C 文件也要被聲明為 Objective-C++ 的文件。這會像森林大火一樣在工程中迅速蔓延。所以,應(yīng)該把你引入 C++ 文件的地方都用?#ifdef __cplusplus?包起來,并且只要可能,就盡量只在?.mm?實現(xiàn)文件中引入 C++ 頭文件。
要獲得更多如何混用 C++ 和 Objective-C 的細節(jié),請查看?Matt Galloway?寫的這篇教程。
Demo:人臉檢測與識別
現(xiàn)在,我們對 OpenCV 及如何把它集成到我們的應(yīng)用中有了大概認識,那讓我們來做一個小 demo 應(yīng)用:從 iPhone 的攝像頭獲取視頻流,對它持續(xù)進行人臉檢測,并在屏幕上標出來。當用戶點擊一個臉孔時,應(yīng)用會嘗試識別這個人。如果識別結(jié)果正確,用戶必須點擊 “Correct”。如果識別錯誤,用戶必須選擇正確的人名來糾正錯誤。我們的人臉識別器就會從錯誤中學習,變得越來越好。
本 demo 應(yīng)用的源碼可從?GitHub?獲得。
視頻拍攝
OpenCV 的 highgui 模塊中有個類,CvVideoCamera,它把 iPhone 的攝像機抽象出來,讓我們的 app 通過一個代理函數(shù)?- (void)processImage:(cv::Mat&)image?來獲得視頻流。CvVideoCamera?實例可像下面這樣進行設(shè)置:
CvVideoCamera *videoCamera = [[CvVideoCamera alloc] initWithParentView:view]; videoCamera.defaultAVCaptureDevicePosition = AVCaptureDevicePositionFront; videoCamera.defaultAVCaptureSessionPreset = AVCaptureSessionPreset640x480; videoCamera.defaultAVCaptureVideoOrientation = AVCaptureVideoOrientationPortrait; videoCamera.defaultFPS = 30; videoCamera.grayscaleMode = NO; videoCamera.delegate = self;攝像頭的幀率被設(shè)置為 30 幀每秒, 我們實現(xiàn)的?processImage?函數(shù)將每秒被調(diào)用 30 次。因為我們的 app 要持續(xù)不斷地檢測人臉,所以我們應(yīng)該在這個函數(shù)里實現(xiàn)人臉的檢測。要注意的是,如果對某一幀進行人臉檢測的時間超過 1/30 秒,就會產(chǎn)生掉幀現(xiàn)象。
人臉檢測
其實你并不需要使用 OpenCV 來做人臉檢測,因為 Core Image 已經(jīng)提供了?CIDetector?類。用它來做人臉檢測已經(jīng)相當好了,并且它已經(jīng)被優(yōu)化過,使用起來也很容易:
CIDetector *faceDetector = [CIDetector detectorOfType:CIDetectorTypeFace context:context options:@{CIDetectorAccuracy: CIDetectorAccuracyHigh}];NSArray *faces = [faceDetector featuresInImage:image];從該圖片中檢測到的每一張面孔都在數(shù)組?faces?中保存著一個?CIFaceFeature?實例。這個實例中保存著這張面孔的所處的位置和寬高,除此之外,眼睛和嘴的位置也是可選的。
另一方面,OpenCV 也提供了一套物體檢測功能,經(jīng)過訓練后能夠檢測出任何你需要的物體。該庫為多個場景自帶了可以直接拿來用的檢測參數(shù),如人臉、眼睛、嘴、身體、上半身、下半身和笑臉。檢測引擎由一些非常簡單的檢測器的級聯(lián)組成。這些檢測器被稱為 Haar 特征檢測器,它們各自具有不同的尺度和權(quán)重。在訓練階段,決策樹會通過已知的正確和錯誤的圖片進行優(yōu)化。關(guān)于訓練與檢測過程的詳情可參考此原始論文。當正確的特征級聯(lián)及其尺度與權(quán)重通過訓練確立以后,這些參數(shù)就可被加載并初始化級聯(lián)分類器了:
// 正面人臉檢測器訓練參數(shù)的文件路徑 NSString *faceCascadePath = [[NSBundle mainBundle] pathForResource:@"haarcascade_frontalface_alt2" ofType:@"xml"];const CFIndex CASCADE_NAME_LEN = 2048; char *CASCADE_NAME = (char *) malloc(CASCADE_NAME_LEN); CFStringGetFileSystemRepresentation( (CFStringRef)faceCascadePath, CASCADE_NAME, CASCADE_NAME_LEN);CascadeClassifier faceDetector; faceDetector.load(CASCADE_NAME);這些參數(shù)文件可在 OpenCV 發(fā)行包里的?data/haarcascades?文件夾中找到。
在使用所需要的參數(shù)對人臉檢測器進行初始化后,就可以用它進行人臉檢測了:
cv::Mat img; vector<cv::Rect> faceRects; double scalingFactor = 1.1; int minNeighbors = 2; int flags = 0; cv::Size minimumSize(30,30); faceDetector.detectMultiScale(img, faceRects, scalingFactor, minNeighbors, flagscv::Size(30, 30) );檢測過程中,已訓練好的分類器會用不同的尺度遍歷輸入圖像的每一個像素,以檢測不同大小的人臉。參數(shù)?scalingFactor?決定每次遍歷分類器后尺度會變大多少倍。參數(shù)?minNeighbors?指定一個符合條件的人臉區(qū)域應(yīng)該有多少個符合條件的鄰居像素才被認為是一個可能的人臉區(qū)域;如果一個符合條件的人臉區(qū)域只移動了一個像素就不再觸發(fā)分類器,那么這個區(qū)域非常可能并不是我們想要的結(jié)果。擁有少于?minNeighbors?個符合條件的鄰居像素的人臉區(qū)域會被拒絕掉。如果?minNeighbors?被設(shè)置為 0,所有可能的人臉區(qū)域都會被返回回來。參數(shù)?flags?是 OpenCV 1.x 版本 API 的遺留物,應(yīng)該始終把它設(shè)置為 0。最后,參數(shù)?minimumSize指定我們所尋找的人臉區(qū)域大小的最小值。faceRects?向量中將會包含對?img?進行人臉識別獲得的所有人臉區(qū)域。識別的人臉圖像可以通過?cv::Mat?的?()?運算符提取出來,調(diào)用方式很簡單:cv::Mat faceImg = img(aFaceRect)。
不管是使用?CIDetector?還是 OpenCV 的?CascadeClassifier,只要我們獲得了至少一個人臉區(qū)域,我們就可以對圖像中的人進行識別了。
人臉識別
OpenCV 自帶了三個人臉識別算法:Eigenfaces,Fisherfaces 和局部二值模式直方圖 (LBPH)。如果你想知道它們的工作原理及相互之間的區(qū)別,請閱讀 OpenCV 的詳細文檔。
針對于我們的 demo app,我們將采用 LBPH 算法。因為它會根據(jù)用戶的輸入自動更新,而不需要在每添加一個人或糾正一次出錯的判斷的時候都要重新進行一次徹底的訓練。
要使用 LBPH 識別器,我們也用 Objective-C++ 把它封裝起來。這個封裝中暴露以下函數(shù):
+ (FJFaceRecognizer *)faceRecognizerWithFile:(NSString *)path; - (NSString *)predict:(UIImage*)img confidence:(double *)confidence; - (void)updateWithFace:(UIImage *)img name:(NSString *)name;像下面這樣用工廠方法來創(chuàng)建一個 LBPH 實例:
+ (FJFaceRecognizer *)faceRecognizerWithFile:(NSString *)path {FJFaceRecognizer *fr = [FJFaceRecognizer new];fr->_faceClassifier = createLBPHFaceRecognizer();fr->_faceClassifier->load(path.UTF8String);return fr; }預測函數(shù)可以像下面這樣實現(xiàn):
- (NSString *)predict:(UIImage*)img confidence:(double *)confidence {cv::Mat src = [img cvMatRepresentationGray];int label;self->_faceClassifier->predict(src, label, *confidence);return _labelsArray[label]; }請注意,我們要使用一個類別方法把?UIImage?轉(zhuǎn)化為?cv::Mat。此轉(zhuǎn)換本身倒是相當簡單直接:使用?CGBitmapContextCreate創(chuàng)建一個指向?cv::Image?中的?data?指針所指向的數(shù)據(jù)的?CGContextRef。當我們在此圖形上下文中繪制此?UIImage?的時候,cv::Image?的?data?指針所指就是所需要的數(shù)據(jù)。更有趣的是,我們能對一個 Objective-C 類創(chuàng)建一個 Objective-C++ 的類別,并且確實管用。
另外,OpenCV 的人臉識別器僅支持整數(shù)標簽,但是我們想使用人的名字作標簽,所以我們得通過一個?NSArray?屬性來對二者實現(xiàn)簡單的轉(zhuǎn)換。
一旦識別器給了我們一個識別出來的標簽,我們把此標簽給用戶看,這時候就需要用戶給識別器一個反饋。用戶可以選擇,“是的,識別正確”,也可以選擇,“不,這是 Y,不是 X”。在這兩種情況下,我們都可以通過人臉圖像和正確的標簽來更新 LBPH 模型,以提高未來識別的性能。使用用戶的反饋來更新人臉識別器的方式如下:
- (void)updateWithFace:(UIImage *)img name:(NSString *)name {cv::Mat src = [img cvMatRepresentationGray];NSInteger label = [_labelsArray indexOfObject:name];if (label == NSNotFound) {[_labelsArray addObject:name];label = [_labelsArray indexOfObject:name];}vector<cv::Mat> images = vector<cv::Mat>();images.push_back(src);vector<int> labels = vector<int>();labels.push_back((int)label);self->_faceClassifier->update(images, labels); }這里,我們又做了一次了從?UIImage?到?cv::Mat、int?到?NSString?標簽的轉(zhuǎn)換。我們還得如 OpenCV 的FaceRecognizer::update?API所期望的那樣,把我們的參數(shù)放到?std::vector?實例中去。
如此“預測,獲得反饋,更新循環(huán)”,就是文獻上所說的監(jiān)督式學習。
結(jié)論
OpenCV 是一個強大而用途廣泛的庫,覆蓋了很多現(xiàn)如今仍在活躍的研究領(lǐng)域。想在一篇文章中給出詳細的使用說明只會是讓人徒勞的事情。因此,本文僅意在從較高層次對 OpenCV 庫做一個概述。同時,還試圖就如何集成 OpenCV 庫到你的 iOS 工程中給出一些實用建議,并通過一個人臉識別的例子來向你展示如何在一個真正的項目中使用 OpenCV。如果你覺得 OpenCV 對你的項目有用, OpenCV 的官方文檔寫得非常好非常詳細,請繼續(xù)前行,創(chuàng)造出下一個偉大的 app!
話題 #21 下的更多文章
原文?Face Recognition with OpenCV
總結(jié)
以上是生活随笔為你收集整理的基于 OpenCV 的人脸识别的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GPU 加速下的图像视觉
- 下一篇: GPU 加速下的图像处理