iOS圖像識別

iOS經過攝像頭動態識別圖像

前言

目前的計算機圖像識別,透過現象看本質,主要分爲兩大類:git

  1. 基於規則運算的圖像識別,例如顏色形狀等特徵匹配。
  2. 基於統計的圖像識別,例如機器學習自動提取特徵,並經過級聯多特徵匹配。
  3. 場景:特徵匹配方法適合固定的場景或物體識別,機器學習方法適合大量具備共同特徵的場景或物體識別。
  4. 優劣:不管從識別率,準確度,仍是適應多變場景來說,機器學習都是優於特徵匹配方法的,前提你有大量的數據來訓練分類器。若是是僅僅是識別特定場景、物體或者形狀,使用模板匹配方法更簡單更易於實現。

本文目標,實如今iOS客戶端,經過攝像頭髮現並標記目標。github

效果圖

方案選擇

iOS 客戶端快速實現圖像識別的兩種方案:數組

開源庫 公司 方案說明
TensorFlow Google AlphaGo 打敗世界圍棋冠軍,人工智能大火,谷歌 2016 年開源了其用來製做 AlphaGo 的深度學習系統 Tensorflow,並且 Tensorflow 支持了 iOS,Android 等移動端。
OpenCV Intel OpenCV 於 1999 年由 Intel 創建的,跨平臺的開源計算機視覺庫,主要由 C 和 C++ 代碼構成,有 Python、Ruby、MATLAB 等語言的接口,支持 iOS,Android 等移動設備。

TensorFlow && OpenCV框架

TensorFlow && OpenCV

結論,雖然都是開源庫,TensorFlow 側重點偏向於機器學習,OpenCV 偏向於圖像處理。從推出時間,代碼迭代,資料的豐富度,以及前輩已經給踩平的坑來說,本文選擇 OpenCV 實現。機器學習

OpenCV 中經常使用圖像識別的方法對比

方法名稱 適用場景 示例
模板匹配 適合固定的場景、物體或特定形狀的圖片識別 1. 某公司的 Logo 圖標,假設圖標是固定的;
2. 適用於某個圖片是另一張大圖的一部分的場景;
3. 例如五角星形狀固定,可轉換爲邊框匹配。
特徵點檢測 適合標記兩幅圖片中相同的特徵點 1. 有相同部分的照片拼接,視頻運動追蹤;
2. 例如全景圖片的拼接,長圖的拼接;
3. 監控視頻中的目標跟蹤。
機器學習 適合識別某類有多種狀態的場景或物體識別 1. 人臉識別、人眼識別,身體識別等等;
2. 支付寶掃福,福字有成千上萬種寫法。

備註:基於機器學習訓練分類器用來分類的方法,依賴於訓練數據,給機器提供大量包含目標的正確數據和不包含目標的錯誤背景數據,讓機器來總結提取特徵,適合識別某類有多種狀態的場景或物體識別。async

集成 OpenCV

iOS項目集成OpenCV,主要有兩種方法:ide

  1. OpenCV官網下載 opencv2.framework 框架,拖入便可,導入依賴的庫,具體集成方法見個人另一篇文章iOS集成OpenCV函數

  2. 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;
}
複製代碼

分類器識別圖像效果圖

分類器識別圖像效果圖

OpenCV 其餘圖像處理

  1. 原圖像
  2. 直方圖均衡化
  3. 圖像二值化
  4. 攝像頭預覽
  5. 灰度圖
  6. 輪廓圖

OpenCV處理圖像

若是您以爲有所幫助,請在 GitHub OOBDemo 上賞個Star ⭐️,您的鼓勵是我前進的動力。

相關文章
相關標籤/搜索