鍍金池/ 教程/ iOS/ 基于 OpenCV 的人臉識別
與四軸無人機的通訊
在沙盒中編寫腳本
結構體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學
NSString 與 Unicode
代碼簽名探析
測試
架構
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應用數(shù)據腳本化
一個完整的 Core Data 應用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網絡應用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應式 Android 應用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調試:案例學習
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學習的一代人
視頻
Playground 快速原型制作
Omni 內部
同步數(shù)據
設計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據模型和模型對象
Core Data
字符串本地化
View Controller 轉場
照片框架
響應式視圖
Square Register 中的擴張
DTrace
基礎集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設計的藝術
導航應用
線程安全類的設計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務
自定義 Collection View 布局
測試 View Controllers
訪談
收據驗證
數(shù)據同步
自定義 ViewController 容器轉場
游戲
調試核對清單
View Controller 容器
學無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術:Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構建 iOS 應用
Android 中的 SQLite 數(shù)據庫支持
Fetch 請求
導入大數(shù)據集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標簽
同步案例學習
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉字符串
相機工作原理
Build 過程

基于 OpenCV 的人臉識別

一點背景知識

OpenCV 是一個開源的計算機視覺和機器學習庫。它包含成千上萬優(yōu)化過的算法,為各種計算機視覺應用提供了一個通用工具包。根據這個項目的關于頁面,OpenCV 已被廣泛運用在各種項目上,從谷歌街景的圖片拼接,到交互藝術展覽的技術實現(xiàn)中,都有 OpenCV 的身影。

OpenCV 起始于 1999 年 Intel 的一個內部研究項目。從那時起,它的開發(fā)就一直很活躍。進化到現(xiàn)在,它已支持如 OpenCL 和 OpenGL 等現(xiàn)代技術,也支持如 iOS 和 Android 等平臺。

1999 年,半條命發(fā)布后大紅大熱。Intel 奔騰 3 處理器是當時最高級的 CPU,400-500 MHZ 的時鐘頻率已被認為是相當快。2006 年 OpenCV 1.0 版本發(fā)布的時候,當時主流 CPU 的性能也只和 iPhone 5 的 A6 處理器相當。盡管計算機視覺從傳統(tǒng)上被認為是計算密集型應用,但我們的移動設備性能已明顯地超出能夠執(zhí)行有用的計算機視覺任務的閾值,帶著攝像頭的移動設備可以在計算機視覺平臺上大有所為。

在本文中,我會從一個 iOS 開發(fā)者的視角概述一下 OpenCV,并介紹一點基礎的類和概念。隨后,會講到如何集成 OpenCV 到你的 iOS 項目中以及一些 Objective-C++ 基礎知識。最后,我們會看一個 demo 項目,看看如何在 iOS 設備上使用 OpenCV 實現(xiàn)人臉檢測與人臉識別。

OpenCV 概述

概念

OpenCV 的 API 是 C++ 的。它由不同的模塊組成,這些模塊中包含范圍極為廣泛的各種方法,從底層的圖像顏色空間轉換到高層的機器學習工具。

使用 C++ API 并不是絕大多數(shù) iOS 開發(fā)者每天都做的事,你需要使用 Objective-C++ 文件來調用 OpenCV 的函數(shù)。 也就是說,你不能在 Swift 或者 Objective-C 語言內調用 OpenCV 的函數(shù)。 這篇 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::Rectcv::Point,因為它們會跟定義在 MacTypes.h 中的 RectPoint 相沖突。盡管這只是個人偏好問題,我還是偏向在任何地方都使用 cv:: 以保持一致性。

模塊

下面是在官方文檔中列出的最重要的模塊。

  • core:簡潔的核心模塊,定義了基本的數(shù)據結構,包括稠密多維數(shù)組 Mat 和其他模塊需要的基本函數(shù)。
  • imgproc:圖像處理模塊,包括線性和非線性圖像濾波、幾何圖像轉換 (縮放、仿射與透視變換、一般性基于表的重映射)、顏色空間轉換、直方圖等等。
  • video:視頻分析模塊,包括運動估計、背景消除、物體跟蹤算法。
  • calib3d:包括基本的多視角幾何算法、單體和立體相機的標定、對象姿態(tài)估計、雙目立體匹配算法和元素的三維重建。
  • features2d:包含了顯著特征檢測算法、描述算子和算子匹配算法。
  • objdetect:物體檢測和一些預定義的物體的檢測 (如人臉、眼睛、杯子、人、汽車等)。
  • ml:多種機器學習算法,如 K 均值、支持向量機和神經網絡。
  • highgui:一個簡單易用的接口,提供視頻捕捉、圖像和視頻編碼等功能,還有簡單的 UI 接口 (iOS 上可用的僅是其一個子集)。
  • gpu:OpenCV 中不同模塊的 GPU 加速算法 (iOS 上不可用)。
  • ocl:使用 OpenCL 實現(xiàn)的通用算法 (iOS 上不可用)。
  • 一些其它輔助模塊,如 Python 綁定和用戶貢獻的算法。

基礎類和操作

OpenCV 包含幾百個類。為簡便起見,我們只看幾個基礎的類和操作,進一步閱讀請參考全部文檔。過一遍這幾個核心類應該足以對這個庫的機理產生一些感覺認識。

cv::Mat

cv::Mat 是 OpenCV 的核心數(shù)據結構,用來表示任意 N 維矩陣。因為圖像只是 2 維矩陣的一個特殊場景,所以也是使用 cv::Mat 來表示的。也就是說,cv::Mat 將是你在 OpenCV 中用到最多的類。

一個 cv::Mat 實例的作用就像是圖像數(shù)據的頭,其中包含著描述圖像格式的信息。圖像數(shù)據只是被引用,并能為多個 cv::Mat 實例共享。OpenCV 使用類似于 ARC 的引用計數(shù)方法,以保證當最后一個來自 cv::Mat 的引用也消失的時候,圖像數(shù)據會被釋放。圖像數(shù)據本身是圖像連續(xù)的行的數(shù)組 (對 N 維矩陣來說,這個數(shù)據是由連續(xù)的 N-1 維數(shù)據組成的數(shù)組)。使用 step[] 數(shù)組中包含的值,圖像的任一像素地址都可通過下面的指針運算得到:

uchar *pixelPtr = cvMat.data + rowIndex * cvMat.step[0] + colIndex * cvMat.step[1]

每個像素的數(shù)據格式可以通過 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 的時候使用算法的名字來調用 Algorithm::create(),并且可以通過 get()set()方法來獲取和設置各個參數(shù),這有點像是鍵值編碼。另外,Algorithm 從底層就支持從/向 XML 或 YAML 文件加載/保存參數(shù)的功能。

在 iOS 上使用 OpenCV

添加 OpenCV 到你的工程中

集成 OpenCV 到你的工程中有三種方法:

  • 使用 CocoaPods 就好: pod "OpenCV"。
  • 下載官方 iOS 框架發(fā)行包,并把它添加到工程里。
  • 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 函數(shù)應確保 C++ 對象被正確地釋放了。

第二重要的點就是,如果你在 Objective-C++ 頭文件中引入了 C++ 頭文件,當你在工程中使用該 Objective-C++ 文件的時候就泄露了 C++ 的依賴。任何引入你的 Objective-C++ 類的 Objective-C 類也會引入該 C++ 類,因此該 Objective-C 文件也要被聲明為 Objective-C++ 的文件。這會像森林大火一樣在工程中迅速蔓延。所以,應該把你引入 C++ 文件的地方都用 #ifdef __cplusplus 包起來,并且只要可能,就盡量只在 .mm 實現(xiàn)文件中引入 C++ 頭文件。

要獲得更多如何混用 C++ 和 Objective-C 的細節(jié),請查看 Matt Galloway 寫的這篇教程。

Demo:人臉檢測與識別

現(xiàn)在,我們對 OpenCV 及如何把它集成到我們的應用中有了大概認識,那讓我們來做一個小 demo 應用:從 iPhone 的攝像頭獲取視頻流,對它持續(xù)進行人臉檢測,并在屏幕上標出來。當用戶點擊一個臉孔時,應用會嘗試識別這個人。如果識別結果正確,用戶必須點擊 “Correct”。如果識別錯誤,用戶必須選擇正確的人名來糾正錯誤。我們的人臉識別器就會從錯誤中學習,變得越來越好。

http://wiki.jikexueyuan.com/project/objc/images/21-43.jpg" alt="" />

本 demo 應用的源碼可從 GitHub 獲得。

視頻拍攝

OpenCV 的 highgui 模塊中有個類,CvVideoCamera,它把 iPhone 的攝像機抽象出來,讓我們的 app 通過一個代理函數(shù) - (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 幀每秒, 我們實現(xiàn)的 processImage 函數(shù)將每秒被調用 30 次。因為我們的 app 要持續(xù)不斷地檢測人臉,所以我們應該在這個函數(shù)里實現(xiàn)人臉的檢測。要注意的是,如果對某一幀進行人臉檢測的時間超過 1/30 秒,就會產生掉幀現(xiàn)象。

人臉檢測

其實你并不需要使用 OpenCV 來做人臉檢測,因為 Core Image 已經提供了 CIDetector 類。用它來做人臉檢測已經相當好了,并且它已經被優(yōu)化過,使用起來也很容易:

CIDetector *faceDetector = [CIDetector detectorOfType:CIDetectorTypeFace context:context options:@{CIDetectorAccuracy: CIDetectorAccuracyHigh}];

NSArray *faces = [faceDetector featuresInImage:image];

從該圖片中檢測到的每一張面孔都在數(shù)組 faces 中保存著一個 CIFaceFeature 實例。這個實例中保存著這張面孔的所處的位置和寬高,除此之外,眼睛和嘴的位置也是可選的。

另一方面,OpenCV 也提供了一套物體檢測功能,經過訓練后能夠檢測出任何你需要的物體。該庫為多個場景自帶了可以直接拿來用的檢測參數(shù),如人臉、眼睛、嘴、身體、上半身、下半身和笑臉。檢測引擎由一些非常簡單的檢測器的級聯(lián)組成。這些檢測器被稱為 Haar 特征檢測器,它們各自具有不同的尺度和權重。在訓練階段,決策樹會通過已知的正確和錯誤的圖片進行優(yōu)化。關于訓練與檢測過程的詳情可參考此原始論文。當正確的特征級聯(liá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, flags
                              cv::Size(30, 30) );

檢測過程中,已訓練好的分類器會用不同的尺度遍歷輸入圖像的每一個像素,以檢測不同大小的人臉。參數(shù) scalingFactor 決定每次遍歷分類器后尺度會變大多少倍。參數(shù) minNeighbors 指定一個符合條件的人臉區(qū)域應該有多少個符合條件的鄰居像素才被認為是一個可能的人臉區(qū)域;如果一個符合條件的人臉區(qū)域只移動了一個像素就不再觸發(fā)分類器,那么這個區(qū)域非??赡懿⒉皇俏覀兿胍慕Y果。擁有少于 minNeighbors 個符合條件的鄰居像素的人臉區(qū)域會被拒絕掉。如果 minNeighbors 被設置為 0,所有可能的人臉區(qū)域都會被返回回來。參數(shù) flags 是 OpenCV 1.x 版本 API 的遺留物,應該始終把它設置為 0。最后,參數(shù) minimumSize 指定我們所尋找的人臉區(qū)域大小的最小值。faceRects 向量中將會包含對 img 進行人臉識別獲得的所有人臉區(qū)域。識別的人臉圖像可以通過 cv::Mat() 運算符提取出來,調用方式很簡單:cv::Mat faceImg = img(aFaceRect)。

不管是使用 CIDetector 還是 OpenCV 的 CascadeClassifier,只要我們獲得了至少一個人臉區(qū)域,我們就可以對圖像中的人進行識別了。

人臉識別

OpenCV 自帶了三個人臉識別算法:Eigenfaces,F(xiàn)isherfaces 和局部二值模式直方圖 (LBPH)。如果你想知道它們的工作原理及相互之間的區(qū)別,請閱讀 OpenCV 的詳細文檔。

針對于我們的 demo app,我們將采用 LBPH 算法。因為它會根據用戶的輸入自動更新,而不需要在每添加一個人或糾正一次出錯的判斷的時候都要重新進行一次徹底的訓練。

要使用 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 轉化為 cv::Mat。此轉換本身倒是相當簡單直接:使用 CGBitmapContextCreate 創(chuàng)建一個指向 cv::Image 中的 data 指針所指向的數(shù)據的 CGContextRef。當我們在此圖形上下文中繪制此 UIImage 的時候,cv::Imagedata 指針所指就是所需要的數(shù)據。更有趣的是,我們能對一個 Objective-C 類創(chuàng)建一個 Objective-C++ 的類別,并且確實管用。

另外,OpenCV 的人臉識別器僅支持整數(shù)標簽,但是我們想使用人的名字作標簽,所以我們得通過一個 NSArray 屬性來對二者實現(xià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);
}

這里,我們又做了一次了從 UIImagecv::MatintNSString 標簽的轉換。我們還得如 OpenCV 的 FaceRecognizer::update API所期望的那樣,把我們的參數(shù)放到 std::vector 實例中去。

如此“預測,獲得反饋,更新循環(huán)”,就是文獻上所說的監(jiān)督式學習。

結論

OpenCV 是一個強大而用途廣泛的庫,覆蓋了很多現(xiàn)如今仍在活躍的研究領域。想在一篇文章中給出詳細的使用說明只會是讓人徒勞的事情。因此,本文僅意在從較高層次對 OpenCV 庫做一個概述。同時,還試圖就如何集成 OpenCV 庫到你的 iOS 工程中給出一些實用建議,并通過一個人臉識別的例子來向你展示如何在一個真正的項目中使用 OpenCV。如果你覺得 OpenCV 對你的項目有用, OpenCV 的官方文檔寫得非常好非常詳細,請繼續(xù)前行,創(chuàng)造出下一個偉大的 app!

上一篇:XPC下一篇:Core Data 概述