目前的計算機圖像識別,透過現象看本質,主要分爲兩大類:git
大量的數據
來訓練分類器。若是是僅僅是識別特定場景、物體或者形狀,使用模板匹配方法更簡單更易於實現。本文目標,實如今iOS客戶端,經過攝像頭髮現並標記目標。github
iOS 客戶端快速實現圖像識別的兩種方案:數組
開源庫 | 公司 | 方案說明 |
---|---|---|
TensorFlow | AlphaGo 打敗世界圍棋冠軍,人工智能大火,谷歌 2016 年開源了其用來製做 AlphaGo 的深度學習系統 Tensorflow,並且 Tensorflow 支持了 iOS,Android 等移動端。 | |
OpenCV | Intel | OpenCV 於 1999 年由 Intel 創建的,跨平臺的開源計算機視覺庫,主要由 C 和 C++ 代碼構成,有 Python、Ruby、MATLAB 等語言的接口,支持 iOS,Android 等移動設備。 |
TensorFlow && OpenCV框架
結論,雖然都是開源庫,TensorFlow 側重點偏向於機器學習,OpenCV 偏向於圖像處理。從推出時間,代碼迭代,資料的豐富度,以及前輩已經給踩平的坑來說,本文選擇 OpenCV 實現。機器學習
方法名稱 | 適用場景 | 示例 |
---|---|---|
模板匹配 | 適合固定的場景、物體或特定形狀的圖片識別 | 1. 某公司的 Logo 圖標,假設圖標是固定的; 2. 適用於某個圖片是另一張大圖的一部分的場景; 3. 例如五角星形狀固定,可轉換爲邊框匹配。 |
特徵點檢測 | 適合標記兩幅圖片中相同的特徵點 | 1. 有相同部分的照片拼接,視頻運動追蹤; 2. 例如全景圖片的拼接,長圖的拼接; 3. 監控視頻中的目標跟蹤。 |
機器學習 | 適合識別某類有多種狀態的場景或物體識別 | 1. 人臉識別、人眼識別,身體識別等等; 2. 支付寶掃福,福字有成千上萬種寫法。 |
備註:基於機器學習訓練分類器用來分類的方法,依賴於訓練數據,給機器提供大量包含目標的正確數據和不包含目標的錯誤背景數據,讓機器來總結提取特徵,適合識別某類有多種狀態的場景或物體識別。async
iOS項目集成OpenCV,主要有兩種方法:ide
從OpenCV官網下載 opencv2.framework 框架,拖入便可,導入依賴的庫,具體集成方法見個人另一篇文章iOS集成OpenCV。函數
CocoaPods 方式集成,Pod 文件中配置 pod 'OpenCV'
,此方法簡單,推薦。學習
首先要作的確定是從 iPhone 攝像頭獲取視頻幀,從輸出視頻流代理AVCaptureVideoDataOutputSampleBufferDelegate
的代理方法中獲取視頻幀。優化
#pragma mark - 獲取視頻幀,處理視頻
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;
複製代碼
而後,須要將視頻幀轉換爲 OpenCV 可以用的 cv::Mat 矩陣,OpenCV 運算是以矩陣 Mat 爲基礎的。涉及大量 CPU 運算,須要將視頻幀對象高效轉換爲 OpenCV 可以使用矩陣 cv::Mat,不然手機發燙嚴重。
模板匹配法不須要顏色,經過設置相機輸出格式是YpCbCr
格式,直接從內存讀取灰度圖像,減小 CPU 運算。
/** * 高效將視頻流轉換爲 Mat 圖像矩陣 * Efficiently convert video streams to Mat image matrices @param sampleBuffer 視頻流(video stream) @return OpenCV 可用的圖像矩陣(OpenCV available image matrix) */
+(cv::Mat)bufferToGrayMat:(CMSampleBufferRef) sampleBuffer{
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
OSType format = CVPixelBufferGetPixelFormatType(pixelBuffer);
if (format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
OOBLog(@"Only YUV is supported"); // Y 是亮度,UV 是顏色
return cv::Mat();
}
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
void *baseaddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
CGFloat width = CVPixelBufferGetWidth(pixelBuffer);
videoRenderWidth = width; // 保存渲染寬度
CGFloat colCount = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
if (width != colCount) {
width = colCount; // 若是有字節對齊
}
CGFloat height = CVPixelBufferGetHeight(pixelBuffer);
cv::Mat mat(height, width, CV_8UC1, baseaddress, 0);
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
return mat;
}
複製代碼
若是相機須要設置爲BGRA
格式,這時候須要用其餘方法獲取,若是是須要獲取灰度圖像,不推薦此方法,CPU 佔有率較前一種高的多。
其中旋轉 90 度和鏡像翻轉,根據獲取的圖像是否須要,可經過設置攝像頭輸出避免,前置和後置攝像頭設置不一樣。
///MARK: - 將CMSampleBufferRef轉爲cv::Mat
+(cv::Mat)bufferToMat:(CMSampleBufferRef) sampleBuffer{
CVImageBufferRef imgBuf = CMSampleBufferGetImageBuffer(sampleBuffer);
//鎖定內存
CVPixelBufferLockBaseAddress(imgBuf, 0);
// get the address to the image data
void *imgBufAddr = CVPixelBufferGetBaseAddress(imgBuf);
// get image properties
int w = (int)CVPixelBufferGetWidth(imgBuf);
int h = (int)CVPixelBufferGetHeight(imgBuf);
// create the cv mat
cv::Mat mat(h, w, CV_8UC4, imgBufAddr, 0);
//轉換爲灰度圖像
cv::Mat edges;
cv::cvtColor(mat, edges, CV_BGR2GRAY);
//旋轉90度
cv::Mat transMat;
cv::transpose(mat, transMat);
//翻轉,1是x方向,0是y方向,-1位Both
cv::Mat flipMat;
cv::flip(transMat, flipMat, 1);
CVPixelBufferUnlockBaseAddress(imgBuf, 0);
return flipMat;
}
複製代碼
最後,視頻幀矩陣與模板矩陣對比,此時獲取了模板 UIImage 的矩陣 templateMat 和視頻幀的矩陣 flipMat,只須要用 OpenCV 的函數對比便可。
/** 對比兩個圖像是否有相同區域 @return 有爲Yes */
-(BOOL)compareInput:(cv::Mat) inputMat templateMat:(cv::Mat)tmpMat{
int result_rows = inputMat.rows - tmpMat.rows + 1;
int result_cols = inputMat.cols - tmpMat.cols + 1;
cv::Mat resultMat = cv::Mat(result_cols,result_rows,CV_32FC1);
cv::matchTemplate(inputMat, tmpMat, resultMat, cv::TM_CCOEFF_NORMED);
double minVal, maxVal;
cv::Point minLoc, maxLoc, matchLoc;
cv::minMaxLoc( resultMat, &minVal, &maxVal, &minLoc, &maxLoc, cv::Mat());
// matchLoc = maxLoc;
// NSLog(@"min==%f,max==%f",minVal,maxVal);
dispatch_async(dispatch_get_main_queue(), ^{
self.similarLevelLabel.text = [NSString stringWithFormat:@"類似度:%.2f",maxVal];
});
if (maxVal > 0.7) {
//有類似位置,返回類似位置的第一個點
currentLoc = maxLoc;
return YES;
}else{
return NO;
}
}
複製代碼
此時,咱們已經對比兩個圖像的類似度了,其中maxVal越大表示匹配度越高,1爲徹底匹配,通常想要匹配準確,須要大於0.7。
但此時咱們發現一個問題,咱們攝像頭離圖像太遠或者太近,都沒法識別,只有在特定的距離纔可以識別。
這是由於模板匹配法,只是死板
的拿模板圖像去和攝像頭讀取的圖像進行比較,放大縮小都不行。
咱們作些優化,按照圖像金字塔
的方法,將模板進行動態的放大縮小,只要可以匹配,說明圖像就是同樣的,這樣攝像頭前進後退都可以識別。咱們將識別出的位置和大小保存在數組中,用矩形方框來標記位置。至於怎麼標記,就不細說了,方法不少。
/** * 對比兩個圖像是否有相同區域 * Compare whether two images have the same area @param inputMat 縮放後的視頻圖像矩陣(Scaled video image matrix) @param tmpMat 待識別的目標圖像矩陣(Target image matrix to be identified) @param scale 視頻縮放比例(video scaling) @param similarValue 設置的對比類似度閾值(set contrast similarity threshold) @param videoFillWidth 視頻圖像字節補齊寬度(Video image byte fill width) @return 對比結果,包含目標座標,類似度(comparison result, including target coordinates, similarity) */
+(NSDictionary *)compareInput:(Mat) inputMat templateMat:(Mat)tmpMat VideoScale:(CGFloat)scale SimilarValue:(CGFloat)similarValue VideoFillWidth:(CGFloat)videoFillWidth{
// 將待比較的圖像縮放至視頻寬度的 20% 至 50%
NSArray *tmpArray = @[@(0.2),@(0.3),@(0.4),@(0.5)];
int currentTmpWidth = 0; // 匹配的模板圖像寬度
int currentTmpHeight = 0; // 匹配的模板圖像高度
double maxVal = 0; // 類似度
cv::Point maxLoc; // 匹配的位置
for (NSNumber *tmpNum in tmpArray) {
CGFloat tmpScale = tmpNum.floatValue;
// 待比較圖像寬度,將待比較圖像寬度縮放至視頻圖像的一半左右
int tmpCols = inputMat.cols * tmpScale;
// 待比較圖像高度,保持寬高比
int tmpRows = (tmpCols * tmpMat.rows) / tmpMat.cols;
// 縮放後的圖像
Mat tmpReMat;
cv::Size tmpReSize = cv::Size(tmpCols,tmpRows);
resize(tmpMat, tmpReMat, tmpReSize);
// 比較結果
int result_rows = inputMat.rows - tmpReMat.rows + 1;
int result_cols = inputMat.cols - tmpReMat.cols + 1;
if (result_rows < 0 || result_cols < 0) {
break;
}
Mat resultMat = Mat(result_cols,result_rows,CV_32FC1);
matchTemplate(inputMat, tmpReMat, resultMat, TM_CCOEFF_NORMED);
double minVal_temp, maxVal_temp;
cv::Point minLoc_temp, maxLoc_temp, matchLoc_temp;
minMaxLoc( resultMat, &minVal_temp, &maxVal_temp, &minLoc_temp, &maxLoc_temp, Mat());
maxVal = maxVal_temp;
if (maxVal >= similarValue) {
maxLoc = maxLoc_temp;
currentTmpWidth = tmpCols;
currentTmpHeight = tmpRows;
break;
}
}
if (maxVal >= similarValue) {
// 目標圖像按照縮放比例恢復
CGFloat zoomScale = 1.0 / scale;
CGRect rectF = CGRectMake(maxLoc.x * zoomScale, maxLoc.y * zoomScale, currentTmpWidth * zoomScale, currentTmpHeight * zoomScale);
NSDictionary *tempDict = @{kTargetRect:NSStringFromCGRect(rectF),
kSimilarValue:@(maxVal),
kVideoFillWidth:@(videoFillWidth)};
return tempDict;
}else{
NSDictionary *tempDict = @{kTargetRect:NSStringFromCGRect(CGRectZero),
kSimilarValue:@(maxVal),
kVideoFillWidth:@(videoFillWidth)};
return tempDict;
}
}
複製代碼
機器學習方法適合批量提取大量圖片的特徵,並且若是樣本不標準或者有錯誤,也會致使分類器識別正確率下降。
訓練分類器的方法,請自行 google 查找,本篇再也不詳細說明。
大量的訓練數據如何獲取,例如人臉分類器須要的正樣本人臉圖像幾千張,負樣本須要爲正樣本的3倍左右。個人解決思路爲從攝像頭錄製待識別物體,從視頻幀中生成 PNG 格式的正樣本,再拍攝不包含待識別物體的背景,仍舊從視頻中自動生成 PNG 格式的負樣本,最後對圖片進行縮放統一。
訓練完成後會通常會生成一個 XML 格式的文件,咱們加載這個 XML 文件,就能夠用其來識別物體了,這裏咱們使用 OpenCV 官方庫中人眼識別庫haarcascade_eye_tree_eyeglasses.xml
,咱們從GitHub上面下載開源庫 OpenCV 的源代碼,目前最新版本爲 4.1.0,分類器在 OpenCV 項目的 /data 目錄下的文件夾中,XML 格式文件就是。
加載訓練好的分類器文件須要用到加載器,咱們定義一個加載器屬性對象:
cv::CascadeClassifier icon_cascade;//分類器
複製代碼
加載器加載 XML 文件,加載成功返回 YES。
//加載訓練文件
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"haarcascade_eye_tree_eyeglasses.xml" ofType:nil];
cv::String fileName = [bundlePath cStringUsingEncoding:NSUTF8StringEncoding];
BOOL isSuccessLoadFile = icon_cascade.load(fileName);
isSuccessLoadXml = isSuccessLoadFile;
if (isSuccessLoadFile) {
NSLog(@"Load success.......");
}else{
NSLog(@"Load failed......");
}
複製代碼
咱們是從攝像頭獲取圖像,仍需把視頻幀轉換爲 OpenCV 可以使用的cv::Mat
矩陣格式,按照上面已知的方法轉換,假設咱們已經獲取了視頻幀轉換好的灰度圖像矩陣cv::Mat imgMat
,那咱們用OpenCV的API接口來識別視頻幀,並把識別出的位置轉換爲 Frame 存在數組中返回,咱們能夠隨意使用這些 Frame 來標記識別出的位置。
//獲取計算出的標記的位置,保存在數組中
-(NSArray *)getTagRectInLayer:(cv::Mat) inputMat{
if (inputMat.empty()) {
return nil;
}
//圖像均衡化
cv::equalizeHist(inputMat, inputMat);
//定義向量,存儲識別出的位置
std::vector<cv::Rect> glassess;
//分類器識別
icon_cascade.detectMultiScale(inputMat, glassess, 1.1, 3, 0);
//轉換爲Frame,保存在數組中
NSMutableArray *marr = [NSMutableArray arrayWithCapacity:glassess.size()];
for (NSInteger i = 0; i < glassess.size(); i++) {
CGRect rect = CGRectMake(glassess[i].x, glassess[i].y, glassess[i].width,glassess[i].height);
NSValue *value = [NSValue valueWithCGRect:rect];
[marr addObject:value];
}
return marr.copy;
}
複製代碼
分類器識別圖像效果圖
若是您以爲有所幫助,請在 GitHub OOBDemo 上賞個Star ⭐️,您的鼓勵是我前進的動力。