OpenCV 是一個開源的計算機(jī)視覺和機(jī)器學(xué)習(xí)庫。它包含成千上萬優(yōu)化過的算法,為各種計算機(jī)視覺應(yīng)用提供了一個通用工具包。根據(jù)這個項(xiàng)目的關(guān)于頁面,OpenCV 已被廣泛運(yùn)用在各種項(xiàng)目上,從谷歌街景的圖片拼接,到交互藝術(shù)展覽的技術(shù)實(shí)現(xiàn)中,都有 OpenCV 的身影。
OpenCV 起始于 1999 年 Intel 的一個內(nèi)部研究項(xiàng)目。從那時起,它的開發(fā)就一直很活躍。進(jìn)化到現(xiàn)在,它已支持如 OpenCL 和 OpenGL 等現(xiàn)代技術(shù),也支持如 iOS 和 Android 等平臺。
1999 年,半條命發(fā)布后大紅大熱。Intel 奔騰 3 處理器是當(dāng)時最高級的 CPU,400-500 MHZ 的時鐘頻率已被認(rèn)為是相當(dāng)快。2006 年 OpenCV 1.0 版本發(fā)布的時候,當(dāng)時主流 CPU 的性能也只和 iPhone 5 的 A6 處理器相當(dāng)。盡管計算機(jī)視覺從傳統(tǒng)上被認(rèn)為是計算密集型應(yīng)用,但我們的移動設(shè)備性能已明顯地超出能夠執(zhí)行有用的計算機(jī)視覺任務(wù)的閾值,帶著攝像頭的移動設(shè)備可以在計算機(jī)視覺平臺上大有所為。
在本文中,我會從一個 iOS 開發(fā)者的視角概述一下 OpenCV,并介紹一點(diǎn)基礎(chǔ)的類和概念。隨后,會講到如何集成 OpenCV 到你的 iOS 項(xiàng)目中以及一些 Objective-C++ 基礎(chǔ)知識。最后,我們會看一個 demo 項(xiàng)目,看看如何在 iOS 設(shè)備上使用 OpenCV 實(shí)現(xiàn)人臉檢測與人臉識別。
OpenCV 的 API 是 C++ 的。它由不同的模塊組成,這些模塊中包含范圍極為廣泛的各種方法,從底層的圖像顏色空間轉(zhuǎn)換到高層的機(jī)器學(xué)習(xí)工具。
使用 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++ 代碼,從而避免掉很多讓人頭痛的問題,比如直接改文件后綴名會因?yàn)樵阱e誤的文件中引用了一個 C++ 頭文件而產(chǎn)生難以追蹤的編譯錯誤。
OpenCV 聲明了命名空間 cv
,因此 OpenCV 的類的前面會有個 cv::
前綴,就像 cv::Mat
、 cv::Algorithm
等等。你也可以在 .mm
文件中使用 using namespace cv
來避免在一堆類名前使用 cv::
前綴。但是,在某些類名前你必須使用命名空間前綴,比如 cv::Rect
和 cv::Point
,因?yàn)樗鼈儠x在 MacTypes.h
中的 Rect
和 Point
相沖突。盡管這只是個人偏好問題,我還是偏向在任何地方都使用 cv::
以保持一致性。
下面是在官方文檔中列出的最重要的模塊。
Mat
和其他模塊需要的基本函數(shù)。OpenCV 包含幾百個類。為簡便起見,我們只看幾個基礎(chǔ)的類和操作,進(jìn)一步閱讀請參考全部文檔。過一遍這幾個核心類應(yīng)該足以對這個庫的機(jī)理產(chǎn)生一些感覺認(rèn)識。
cv::Mat
cv::Mat
是 OpenCV 的核心數(shù)據(jù)結(jié)構(gòu),用來表示任意 N 維矩陣。因?yàn)閳D像只是 2 維矩陣的一個特殊場景,所以也是使用 cv::Mat
來表示的。也就是說,cv::Mat
將是你在 OpenCV 中用到最多的類。
一個 cv::Mat
實(shí)例的作用就像是圖像數(shù)據(jù)的頭,其中包含著描述圖像格式的信息。圖像數(shù)據(jù)只是被引用,并能為多個 cv::Mat
實(shí)例共享。OpenCV 使用類似于 ARC 的引用計數(shù)方法,以保證當(dāng)最后一個來自 cv::Mat
的引用也消失的時候,圖像數(shù)據(jù)會被釋放。圖像數(shù)據(jù)本身是圖像連續(xù)的行的數(shù)組 (對 N 維矩陣來說,這個數(shù)據(jù)是由連續(xù)的 N-1 維數(shù)據(jù)組成的數(shù)組)。使用 step[]
數(shù)組中包含的值,圖像的任一像素地址都可通過下面的指針運(yùn)算得到:
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 位浮點(diǎn)數(shù))。
cv::Algorithm
Algorithm
是 OpenCV 中實(shí)現(xiàn)的很多算法的抽象基類,包括將在我們的 demo 工程中用到的 FaceRecognizer
。它提供的 API 與蘋果的 Core Image 框架中的 CIFilter
有些相似之處。創(chuàng)建一個 Algorithm
的時候使用算法的名字來調(diào)用 Algorithm::create()
,并且可以通過 get()
和 set()
方法來獲取和設(shè)置各個參數(shù),這有點(diǎn)像是鍵值編碼。另外,Algorithm
從底層就支持從/向 XML 或 YAML 文件加載/保存參數(shù)的功能。
集成 OpenCV 到你的工程中有三種方法:
pod "OpenCV"
。如前面所說,OpenCV 是一個 C++ 的 API,因此不能直接在 Swift 和 Objective-C 代碼中使用,但能在 Objective-C++ 文件中使用。
Objective-C++ 是 Objective-C 和 C++ 的混合物,讓你可以在 Objective-C 類中使用 C++ 對象。clang 編譯器會把所有后綴名為 .mm
的文件都當(dāng)做是 Objective-C++。一般來說,它會如你所期望的那樣運(yùn)行,但還是有一些使用 Objective-C++ 的注意事項(xiàng)。內(nèi)存管理是你最應(yīng)該格外注意的點(diǎn),因?yàn)?ARC 只對 Objective-C 對象有效。當(dāng)你使用一個 C++ 對象作為類屬性的時候,其唯一有效的屬性就是 assign
。因此,你的 dealloc
函數(shù)應(yīng)確保 C++ 對象被正確地釋放了。
第二重要的點(diǎn)就是,如果你在 Objective-C++ 頭文件中引入了 C++ 頭文件,當(dāng)你在工程中使用該 Objective-C++ 文件的時候就泄露了 C++ 的依賴。任何引入你的 Objective-C++ 類的 Objective-C 類也會引入該 C++ 類,因此該 Objective-C 文件也要被聲明為 Objective-C++ 的文件。這會像森林大火一樣在工程中迅速蔓延。所以,應(yīng)該把你引入 C++ 文件的地方都用 #ifdef __cplusplus
包起來,并且只要可能,就盡量只在 .mm
實(shí)現(xiàn)文件中引入 C++ 頭文件。
要獲得更多如何混用 C++ 和 Objective-C 的細(xì)節(jié),請查看 Matt Galloway 寫的這篇教程。
現(xiàn)在,我們對 OpenCV 及如何把它集成到我們的應(yīng)用中有了大概認(rèn)識,那讓我們來做一個小 demo 應(yīng)用:從 iPhone 的攝像頭獲取視頻流,對它持續(xù)進(jìn)行人臉檢測,并在屏幕上標(biāo)出來。當(dāng)用戶點(diǎn)擊一個臉孔時,應(yīng)用會嘗試識別這個人。如果識別結(jié)果正確,用戶必須點(diǎn)擊 “Correct”。如果識別錯誤,用戶必須選擇正確的人名來糾正錯誤。我們的人臉識別器就會從錯誤中學(xué)習(xí),變得越來越好。
http://wiki.jikexueyuan.com/project/objc/images/21-43.jpg" alt="" />
本 demo 應(yīng)用的源碼可從 GitHub 獲得。
OpenCV 的 highgui 模塊中有個類,CvVideoCamera
,它把 iPhone 的攝像機(jī)抽象出來,讓我們的 app 通過一個代理函數(shù) - (void)processImage:(cv::Mat&)image
來獲得視頻流。CvVideoCamera
實(shí)例可像下面這樣進(jìn)行設(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 幀每秒, 我們實(shí)現(xiàn)的 processImage
函數(shù)將每秒被調(diào)用 30 次。因?yàn)槲覀兊?app 要持續(xù)不斷地檢測人臉,所以我們應(yīng)該在這個函數(shù)里實(shí)現(xiàn)人臉的檢測。要注意的是,如果對某一幀進(jìn)行人臉檢測的時間超過 1/30 秒,就會產(chǎn)生掉幀現(xiàn)象。
其實(shí)你并不需要使用 OpenCV 來做人臉檢測,因?yàn)?Core Image 已經(jīng)提供了 CIDetector
類。用它來做人臉檢測已經(jīng)相當(dāng)好了,并且它已經(jīng)被優(yōu)化過,使用起來也很容易:
CIDetector *faceDetector = [CIDetector detectorOfType:CIDetectorTypeFace context:context options:@{CIDetectorAccuracy: CIDetectorAccuracyHigh}];
NSArray *faces = [faceDetector featuresInImage:image];
從該圖片中檢測到的每一張面孔都在數(shù)組 faces
中保存著一個 CIFaceFeature
實(shí)例。這個實(shí)例中保存著這張面孔的所處的位置和寬高,除此之外,眼睛和嘴的位置也是可選的。
另一方面,OpenCV 也提供了一套物體檢測功能,經(jīng)過訓(xùn)練后能夠檢測出任何你需要的物體。該庫為多個場景自帶了可以直接拿來用的檢測參數(shù),如人臉、眼睛、嘴、身體、上半身、下半身和笑臉。檢測引擎由一些非常簡單的檢測器的級聯(lián)組成。這些檢測器被稱為 Haar 特征檢測器,它們各自具有不同的尺度和權(quán)重。在訓(xùn)練階段,決策樹會通過已知的正確和錯誤的圖片進(jìn)行優(yōu)化。關(guān)于訓(xùn)練與檢測過程的詳情可參考此原始論文。當(dāng)正確的特征級聯(lián)及其尺度與權(quán)重通過訓(xùn)練確立以后,這些參數(shù)就可被加載并初始化級聯(lián)分類器了:
// 正面人臉檢測器訓(xù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ù)對人臉檢測器進(jìn)行初始化后,就可以用它進(jìn)行人臉檢測了:
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, flags
cv::Size(30, 30) );
檢測過程中,已訓(xùn)練好的分類器會用不同的尺度遍歷輸入圖像的每一個像素,以檢測不同大小的人臉。參數(shù) scalingFactor
決定每次遍歷分類器后尺度會變大多少倍。參數(shù) minNeighbors
指定一個符合條件的人臉區(qū)域應(yīng)該有多少個符合條件的鄰居像素才被認(rèn)為是一個可能的人臉區(qū)域;如果一個符合條件的人臉區(qū)域只移動了一個像素就不再觸發(fā)分類器,那么這個區(qū)域非??赡懿⒉皇俏覀兿胍慕Y(jié)果。擁有少于 minNeighbors
個符合條件的鄰居像素的人臉區(qū)域會被拒絕掉。如果 minNeighbors
被設(shè)置為 0,所有可能的人臉區(qū)域都會被返回回來。參數(shù) flags
是 OpenCV 1.x 版本 API 的遺留物,應(yīng)該始終把它設(shè)置為 0。最后,參數(shù) minimumSize
指定我們所尋找的人臉區(qū)域大小的最小值。faceRects
向量中將會包含對 img
進(jìn)行人臉識別獲得的所有人臉區(qū)域。識別的人臉圖像可以通過 cv::Mat
的 ()
運(yùn)算符提取出來,調(diào)用方式很簡單:cv::Mat faceImg = img(aFaceRect)
。
不管是使用 CIDetector
還是 OpenCV 的 CascadeClassifier
,只要我們獲得了至少一個人臉區(qū)域,我們就可以對圖像中的人進(jìn)行識別了。
OpenCV 自帶了三個人臉識別算法:Eigenfaces,F(xiàn)isherfaces 和局部二值模式直方圖 (LBPH)。如果你想知道它們的工作原理及相互之間的區(qū)別,請閱讀 OpenCV 的詳細(xì)文檔。
針對于我們的 demo app,我們將采用 LBPH 算法。因?yàn)樗鼤鶕?jù)用戶的輸入自動更新,而不需要在每添加一個人或糾正一次出錯的判斷的時候都要重新進(jìn)行一次徹底的訓(xùn)練。
要使用 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 實(shí)例:
+ (FJFaceRecognizer *)faceRecognizerWithFile:(NSString *)path {
FJFaceRecognizer *fr = [FJFaceRecognizer new];
fr->_faceClassifier = createLBPHFaceRecognizer();
fr->_faceClassifier->load(path.UTF8String);
return fr;
}
預(yù)測函數(shù)可以像下面這樣實(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)換本身倒是相當(dāng)簡單直接:使用 CGBitmapContextCreate
創(chuàng)建一個指向 cv::Image
中的 data
指針?biāo)赶虻臄?shù)據(jù)的 CGContextRef
。當(dāng)我們在此圖形上下文中繪制此 UIImage
的時候,cv::Image
的 data
指針?biāo)妇褪撬枰臄?shù)據(jù)。更有趣的是,我們能對一個 Objective-C 類創(chuàng)建一個 Objective-C++ 的類別,并且確實(shí)管用。
另外,OpenCV 的人臉識別器僅支持整數(shù)標(biāo)簽,但是我們想使用人的名字作標(biāo)簽,所以我們得通過一個 NSArray
屬性來對二者實(shí)現(xiàn)簡單的轉(zhuǎn)換。
一旦識別器給了我們一個識別出來的標(biāo)簽,我們把此標(biāo)簽給用戶看,這時候就需要用戶給識別器一個反饋。用戶可以選擇,“是的,識別正確”,也可以選擇,“不,這是 Y,不是 X”。在這兩種情況下,我們都可以通過人臉圖像和正確的標(biāo)簽來更新 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
標(biāo)簽的轉(zhuǎn)換。我們還得如 OpenCV 的 FaceRecognizer::update
API所期望的那樣,把我們的參數(shù)放到 std::vector
實(shí)例中去。
如此“預(yù)測,獲得反饋,更新循環(huán)”,就是文獻(xiàn)上所說的監(jiān)督式學(xué)習(xí)。
OpenCV 是一個強(qiáng)大而用途廣泛的庫,覆蓋了很多現(xiàn)如今仍在活躍的研究領(lǐng)域。想在一篇文章中給出詳細(xì)的使用說明只會是讓人徒勞的事情。因此,本文僅意在從較高層次對 OpenCV 庫做一個概述。同時,還試圖就如何集成 OpenCV 庫到你的 iOS 工程中給出一些實(shí)用建議,并通過一個人臉識別的例子來向你展示如何在一個真正的項(xiàng)目中使用 OpenCV。如果你覺得 OpenCV 對你的項(xiàng)目有用, OpenCV 的官方文檔寫得非常好非常詳細(xì),請繼續(xù)前行,創(chuàng)造出下一個偉大的 app!