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