iOS上用OpenCV寫識別試紙

demo 向,邏輯示例,場景測試不全。

久聞 opencv 的大名,想要了解一下。于是我司每周挑戰(zhàn)做題的內(nèi)容,讓大家試了一下這個。

過程遇到很多困難,網(wǎng)上流傳的接入教程不是比較久,就是以 python 語言為準。而我們可能更多偏向用 C++ 語言的比較方便,有些時候要去了解 python 代碼,然后轉(zhuǎn)寫成 C++。

C++ 又會遇到 namespace 的問題,因為不太有這方面的經(jīng)驗。遇到幾次導入命名空間不對的問題。

計算機如何理解圖片,如何做識別?

Corgi3.png

左邊是人眼看到的,是3只狗。

右邊是計算機看到的一種數(shù)據(jù)模擬。

當你需要計算機識別出來3只狗,說到底,就是用一系列合適的數(shù)學方法,找到數(shù)字規(guī)律,得到需要的部分。

如果你數(shù)學不好,就有種淪為調(diào)參工程師的錯覺(事實)

經(jīng)過系列過濾之后,你可能就換到了你要的線條部分了。

Filter.png

原圖和識別后

勉強識別出來,但是很容易受到背景的噪聲污染,如果是那種虎皮桌子的當背景,就完全識別不出來了。

origin.jpg

然后我想要識別出來是以試紙集中區(qū)域的部分。

after.png

部署 OpenCV

Cocoapods 就能搞定了。并不像網(wǎng)上說的,你需要處理一堆引用bug,手動解決xxx。

除了非常慢,甚至網(wǎng)絡鏈接失敗。

pod 'OpenCV', '~> 4.1.0'

在等待的過程中,發(fā)現(xiàn)了一篇非常有意思的 OpenCV 在 pod 時都做了什么的分析,讓人長見識。糖炒小蝦 - I have a pod, I have a cartha

模仿

主要參考這2個給予靈感,提取桌面圖像提取 ppt 。非常有意思,對吧!

靠著 ppt 的例子,基本能全程運行起來。但桌面圖像基本只有思路分析,但也讓人學習了很多思想。

邊緣檢測算子嘗試

這是唯一難點,怎么樣把我需要的部分檢測出來。

opencv 常用的 canny 算子,是邊緣識別的首選項。
但實際測試,發(fā)現(xiàn)由于試紙?zhí)?,且顏色比較復雜,太容易被背景融入進去,導致識別不準確。

因為試紙會有帶花的,白色,帶字的,甚至綠色待,整體邊界不一致,導致 canny 算子識別出來的圖結(jié)果非常差。形態(tài)被割裂的很厲害,連接處不清晰。

類似這樣的(找不到自己的圖了,1個月前寫的...):

sobel.png

當了2小時調(diào)參工程師之后,我放棄了這個思路。

無意中,發(fā)現(xiàn)邊緣直方圖法補全,sobel 算子能提供幫助。力用sobel算子,可以得到相對完整的一個矩形區(qū)域。

步驟

一:縮減尺寸

為了加快識別計算時間。最后得到坐標后,還原用到原圖裁剪。

cv::Mat shrinkPic;
cv::pyrDown(cvImage, shrinkPic);

二:灰度圖

grey.png

幾乎所有的識別,都會用灰度圖。為什么呢?我查了資料一句話就是:降維計算。

我們拿1個象素來說,如果只表示黑和白,那么0和1即可。

如果是 RBG 那么3個信道的值分別是256,3種組合的數(shù)量級就是:256 * 256 * 256 = 1600w+多種組合

如果加上 alpha 信道,那么一個象素的可能組合達到40億。對于計算機來說,一張1024 x 1024的圖,從這個數(shù)據(jù)里找規(guī)律,計算量是非常大的。

但如果是灰度圖,那么只有 0-255 的灰度值,那么計算量下了很多倍。

cv::cvtColor(shrinkPic, greyPic, cv::COLOR_RGBA2GRAY);

三:sobel 算子補全形態(tài)

sobel_lh.png
cv::Mat grabX, grabY;
cv::Sobel(greyPic, grabX, CV_32F, 1, 0);
cv::Sobel(greyPic, grabY, CV_32F, 0, 1);
cv::subtract(grabX, grabY, sobPic);
cv::convertScaleAbs(sobPic, sobPic);

四:增加對比度

因為補全的部分,雖然是完整的矩形形態(tài),但是邊緣還是相對弱,如果直接二值化,容易補全形態(tài)丟失。

cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(25, 25));
cv::morphologyEx(sobPic, enhancePic, cv::MORPH_CLOSE, kernel);

五:去噪和二值化

上一步,過濾了一部分偏小的顏色之后,還是會有不屬于識別物的噪點存在。我們做一下過濾,最后轉(zhuǎn)化成二值化的圖。

二值化就是全圖只前0和1。那么計算機識別速度又加快上百倍了。

threshold.png
cv::blur(sobPic, threshPic, cv::Size(5,5));
cv::threshold(threshPic, threshPic, 30, 255, cv::THRESH_BINARY);

六:找到最小外接矩形中最大的一個得到坐標

rect.png

識別物和噪點區(qū)域,可能被識別成一個數(shù)組,交還給你,你需要找到面積最大的一個,那么就是我們的識別目標

// 找出輪廓區(qū)域
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours(threshPic, contours, hierarchy, cv::RETR_CCOMP, cv::CHAIN_APPROX_SIMPLE);
    
// 求所有形狀的最小外接矩形中最大的一個
cv::RotatedRect box;
for( int i = 0; i < contours.size(); i++ ){
    cv::RotatedRect rect = cv::minAreaRect( cv::Mat(contours[i]) );
    if (box.size.width < rect.size.width) {
        box = rect;
    }
}

七:剪裁未縮小后的圖,使用放射變換

完整示例

我展示的代碼,缺了對 UIIImage 做處理,在參考鏈接最后一條。

如果不處理,你拍到的圖可能和 OpenCV 拿到的 Mat 圖 orientation 不一致。

#import "OpenCVWrapper.h"

#import <opencv2/imgproc/imgproc_c.h>
#import <opencv2/imgproc/imgproc.hpp>

#import <opencv2/imgcodecs/ios.h>//MatToUIImage、MatToUIImage用到
#import <opencv2/imgproc.hpp>//cv::域名下的東西會用到
#import <opencv2/highgui.hpp>


+ (UIImage *)change:(UIImage *)image {
    cv::Mat cvImage;
    UIImageToMat(image, cvImage);
    if (cvImage.empty()) {
        return nil;
    }
    
    cv::Mat shrinkPic;
    cv::pyrDown(cvImage, shrinkPic);
    
    int shrinkCount = (image.size.width / 500);
    int multi = 2;
    if (shrinkCount > 1) {
        shrinkCount = shrinkCount / 2;
        multi = pow(2, shrinkCount + 1);
        for (int i = 0; i < shrinkCount; i++) {
            cv::pyrDown(shrinkPic, shrinkPic);
        }
    }
    
    cv::Mat greyPic, sobPic,enhancePic, threshPic;

    cv::cvtColor(shrinkPic, greyPic, cv::COLOR_RGBA2GRAY);
    
    // 邊緣直方圖法,采用sobel算子提取邊緣線,然后水平,垂直分別做直方圖
    cv::Mat grabX, grabY;
    cv::Sobel(greyPic, grabX, CV_32F, 1, 0);
    cv::Sobel(greyPic, grabY, CV_32F, 0, 1);
    cv::subtract(grabX, grabY, sobPic);
    cv::convertScaleAbs(sobPic, sobPic);
    
    // 填充空白區(qū)域,增強對比度
    cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(25, 25));
    cv::morphologyEx(sobPic, enhancePic, cv::MORPH_CLOSE, kernel);
    
    // 去除噪聲
    cv::blur(sobPic, threshPic, cv::Size(5,5));
    cv::threshold(threshPic, threshPic, 30, 255, cv::THRESH_BINARY);//90
    
//    return MatToUIImage(threshPic);
    
    // 找出輪廓區(qū)域
    std::vector<std::vector<cv::Point>> contours;
    std::vector<cv::Vec4i> hierarchy;
    cv::findContours(threshPic, contours, hierarchy, cv::RETR_CCOMP, cv::CHAIN_APPROX_SIMPLE);
    
    // 求所有形狀的最小外接矩形中最大的一個
    cv::RotatedRect box;
    for( int i = 0; i < contours.size(); i++ ){
        cv::RotatedRect rect = cv::minAreaRect( cv::Mat(contours[i]) );
        if (box.size.width < rect.size.width) {
            box = rect;
        }
    }
    
    {
        // 畫出來矩形和4個點, 供調(diào)試。此部分代碼可以不要
        cv::Mat drawing = cv::Mat::zeros(threshPic.rows, threshPic.cols, CV_8UC3);
        cv::Scalar color = cv::Scalar( rand() & 255, rand() & 255, rand() & 255 );
        cv::Point2f rect_points[4];
        box.points( rect_points );
        for ( int j = 0; j < 4; j++ )
        {
            line( drawing, rect_points[j], rect_points[(j+1)%4], color );
            circle(drawing, rect_points[j], 10, color, 2);
        }
//        return MatToUIImage(drawing);
    }
    
    // 仿射變換
    cv::Point2f corners[4], canvas[4], tmp[4];
    
    // 固定輸出尺寸,可以由外部傳入
    cv::Size real_size = cv::Size(500, 40);
    
    canvas[0] = cv::Point2f(0, 0);
    canvas[1] = cv::Point2f(real_size.width, 0);
    canvas[2] = cv::Point2f(real_size.width, real_size.height);
    canvas[3] = cv::Point2f(0, real_size.height);
    
    box.points( tmp );
    
    bool sorted = false;
    int n = 4;
    while (!sorted){
        for (int i = 1; i < n; i++){
            sorted = true;
            if (tmp[i-1].x > tmp[i].x){
                swap(tmp[i-1], tmp[i]);
                sorted = false;
            }
        }
        n--;
    }
    if (tmp[0].y < tmp[1].y){
        corners[0] = tmp[0];
        corners[3] = tmp[1];
    }
    else{
        corners[0] = tmp[1];
        corners[3] = tmp[0];
    }
    
    if (tmp[2].y < tmp[3].y){
        corners[1] = tmp[2];
        corners[2] = tmp[3];
    }
    else{
        corners[1] = tmp[3];
        corners[2] = tmp[2];
    }
    for (int i = 0; i < 4; i++){
        corners[i] = cv::Point2f(corners[i].x * multi, corners[i].y * multi); //恢復坐標到原圖
    }
    
    cv::Mat result;
    cv::Mat M = cv::getPerspectiveTransform(corners, canvas);
    cv::warpPerspective(cvImage, result, M, real_size);
    return MatToUIImage(result);
}

參考

糖炒小蝦 - I have a pod, I have a cartha

Adit Deshpande - A Beginner's Guide To Understanding Convolutional Neural Networks

奧卡姆剃刀 - 數(shù)字圖像的壓縮與恢復

達聞西 - 利用OpenCV檢測圖像中的長方形畫布或紙張并提取圖像內(nèi)容

才才才 - 利用OpenCV提取圖像中的矩形區(qū)域(PPT屏幕等)

傻傻小蘿卜 - OpenCV(iOS)的邊緣檢測和Canny算子

太一吾魚水- OpenCV矩形檢測

迭代自己 - 使用 Python 和 OpenCV 檢測圖像中的物體并將物體裁剪下來

OpenCV - Creating Bounding rotated boxes and ellipses for contours

@qt6hy - iOS で opencv を使う。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容