OpenCV 是一個開源的計算機視覺和機器學習庫。它包含成千上萬優化過的算法,爲各類計算機視覺應用提供了一個通用工具包。根據這個項目的關於頁面,OpenCV 已被普遍運用在各類項目上,從谷歌街景的圖片拼接,到交互藝術展覽的技術實現中,都有 OpenCV 的身影。html
OpenCV 起始於 1999 年 Intel 的一個內部研究項目。從那時起,它的開發就一直很活躍。進化到如今,它已支持如 OpenCL 和 OpenGL 的多種現代技術,也支持如 iOS 和 Android 等多種平臺。ios
1999 年,半條命發 布後大紅大熱。Intel 奔騰 3 處理器是當時最高級的 CPU,400-500 MHZ 的時鐘頻率已被認爲是至關快。2006 年 OpenCV 1.0 版本發佈的時候,當時主流 CPU 的性能也只和 iPhone 5 的 A6 處理器至關。儘管計算機視覺從傳統上被認爲是計算密集型應用,但咱們的移動設備性能已明顯地超出可以執行有用的計算機視覺任務的閾值,帶着攝像頭的移動設 備能夠在計算機視覺平臺上大有所爲。git
在本文中,我會從一個 iOS 開發者的視角概述一下 OpenCV,並介紹一點基礎的類和概念。隨後,會講到一些如何集成 OpenCV 到你的 iOS 項目中以及 Objective-C++ 的基礎知識。最後,咱們會看一個 demo 項目,看看如何在 iOS 設備上使用 OpenCV 實現人臉檢測與人臉識別。github
OpenCV 的 API 是 C++ 的。它由不一樣的模塊組成,這些模塊中包含範圍極爲普遍的各類方法,從底層的圖像顏色空間轉換到高層的機器學習工具。Ss算法
使用 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::以保持一致性。安全
下面是在官方文檔中列出的最重要的模塊。網絡
OpenCV 包含幾百個類。爲簡便起見,咱們只看幾個基礎的類和操做,進一步閱讀請參考所有文檔。過一遍這幾個核心類應該足以對這個庫的機理產生一些感受認識。數據結構
cv::Mat是 OpenCV 的核心數據結構,用來表示任意 N 維矩陣。由於圖像只是 2 維矩陣的一個特殊場景,因此也是使用cv::Mat來表示的。也就是說,cv::Mat將是你在 OpenCV 中用到最多的類。app
一個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 位浮點數)。
Algorithm是 OpenCV 中實現的不少算法的抽象基類,包括將在咱們的 demo 工程中用到的FaceRecognizer。它提供的 API 與蘋果的 Core Image 框架中的CIFilter有些類似之處。建立一個Algorithm的時候使用算法的名字來調用Algorithm::create(),而且能夠經過get()和set()方法來獲取和設置各個參數,這有點像是鍵值編碼。另外,Algorithm從底層就支持從/向 XML 或 YAML 文件加載/保存參數的功能。
You have three options to integrate OpenCV into your iOS project:
集成 OpenCV 到你的工程中有三種方法:
如前面所說,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 寫的這篇教程。
如今,咱們對 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 = [CIDetector detectorOfType:CIDetectorTypeFace context:context options:@{CIDetectorAccuracy: CIDetectorAccuracyHigh}]; NSArray *faces = [faceDetector featuresInImage:image];
從該圖片中檢測到的每一張面孔都在數組faces中保存着一個CIFaceFeature實例。這個實例中保存着這張面孔的所處的位置和寬高,除此以外,眼睛和嘴的位置也是可選的。
另外一方面,OpenCV 也提供了一套物體檢測功能,通過訓練後可以檢測出任何你須要的物體。該庫爲多個場景自帶了能夠直接拿來用的檢測參數,如人臉、眼睛、嘴、身體、上半身、下 半身和笑臉。檢測引擎由一些很是簡單的檢測器的級聯組成。這些檢測器被稱爲 Haar 特徵檢測器,它們各自具備不一樣的尺度和權重。在訓練階段,決策樹會經過已知的正確和錯誤的圖片進行優化。關於訓練與檢測過程的詳情可參考此原始論文。當正確的特徵級聯及其尺度與權重經過訓練確立之後,這些參數就可被加載並初始化級聯分類器了:
// 正面人臉檢測器訓練參數的文件路徑 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);
這些參數文件可在 OpenCV 發行包裏的data/haarcascades文件夾中找到。
在使用所須要的參數對人臉檢測器進行初始化後,就能夠用它進行人臉檢測了:
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) );
檢測過程當中,已訓練好的分類器會用不一樣的尺度遍歷輸入圖像的每個像素,以檢測不一樣大小的人臉。參數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 = [FJFaceRecognizer new]; fr->_faceClassifier = createLBPHFaceRecognizer(); fr->_faceClassifier->load(path.UTF8String); return fr; }
預測函數能夠像下面這樣實現:
- (NSString *)predict:(UIImage*)img confidence:(double *)confidence { cv::Mat src = [img cvMatRepresentationGray]; int label; 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]; 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標籤的轉換。咱們還得如 OpenCV 的FaceRecognizer::updateAPI所指望的那樣,把咱們的參數放到std::vector實例中去。
如此「預測,得到反饋,更新循環」,就是文獻上所說的監督式學習。
OpenCV 是一個強大而用途普遍的庫,覆蓋了不少現現在仍在活躍的研究領域。想在一篇文章中給出詳細的使用說明只會是讓人徒勞的事情。所以,本文僅意在從較高層次對 OpenCV 庫作一個概述。同時,還試圖就如何集成 OpenCV 庫到你的 iOS 工程中給出一些實用建議,並經過一我的臉識別的例子來向你展現如何在一個真正的項目中使用 OpenCV。若是你以爲 OpenCV 對你的項目有用, OpenCV 的官方文檔寫得很是好很是詳細,請繼續前行,創造出下一個偉大的 app!
@phenmod,新奇有趣事務愛好者,心中常念「坐忘」,的中二青年。