iOS利用OpenCV 實現文字行區域提取的嘗試

一些探索

最近下了幾個OCR的App(好比白描),發現能夠選中圖片中的文字行逐行轉成文字,以爲頗有意思(固然想用要花錢啦),想着本身研究一下實現原理,google以後,發現了兩個庫,一個是OpenCV,在機器視覺方面應用普遍,圖像分析必備利器。另外一個是Tesseract,谷歌開源的文字識別框架,iOS端gali8編譯了一個Tesseract-OCR-iOS的庫可使用,可是集成過程不是很愉快,Tesseract-OCR-iOS使用的Tesseract 3.3版本,而Tesseract已經更新到4.0,因此字庫不匹配問題搞的很煩,並且利用官方提供的訓練字庫識別效果不好,想要實現高準確率的識別效果須要自行進行字庫訓練,至關繁瑣,而且工做量巨大,在完成demo以後就放棄使用了。接着,我又Google了一番,獲得的答案是ABBYY是業界中文OCR識別效果最好的,其次是百度,因而我又點開了白描,在關於頁裏看到了這個 html

額,好吧,那我就研究下他是如何把選中的文字和百度OCR的結果進行對應的,等等,讓我先抓個包看看。ios

這是…座標?百度666,好像沒啥好研究的了,不過出於好奇仍是想知道使用openCV是如何作到把文字區域進行框選的,因此接下來咱們就看看如何在iOS上使用OpenCV實現圖片中的文字框選。c++

着手實現

首先,須要去OpenCV官網下載iOS的framework,下載好後拖入新建的工程中便可,因爲OpenCV庫是使用C++編寫,因此swift沒法直接使用,須要使用OC作橋接,須要使用swift的同窗能夠看下這篇文章Using OpenCV in an iOS appgit

根據OpenCV入門筆記(七) 文字區域的提取中提供的思路,我實現了OC版本的代碼,經過測試,清晰的文字截圖識別沒有問題,可是在複雜的拍照場景中幾乎沒法識別任何內容,例以下圖github

這張是相機拍攝的屏幕上的文字,有清晰的豎紋及屏幕反光,在該算法下,最終的框選區域是整個圖片,沒法識別文字區域,說明這個處理流程仍是不完善的,咱們先來看一下他的處理過程objective-c

  1. 將圖片轉爲灰度圖
  2. 形態學變換的預處理,獲得能夠查找矩形的圖片
  3. 查找和篩選文字區域
  4. 用綠線畫出這些找到的輪廓

根據前面獲得的識別結果,咱們大體能夠猜想問題出在了第二步,因爲豎紋影響將所有文字區域連城一片,致使整圖被框選。那麼在第二步中都作了哪些操做呢?算法

實際上上面的流程一共作了4步操做,二值化->膨脹->腐蝕->再膨脹,這個流程對於正常的白底文本截圖的識別沒有問題,一但圖片中出現了噪點,噪點在第一次膨脹的以後被放大,對整個圖像產生不可逆的污染,咱們先來看一下二值化後的圖像swift

文字仍是很清晰的,可是豎紋同樣明顯,接着第二步膨脹,看下會怎樣bash

一片白,不用往下看了吧。app

既然如此,就須要咱們修改一下在第二步的處理流程了,在反轉圖像(由黑白變爲白黑)以前,須要對圖像進行降噪處理,由於OpenCV是對亮點進行操做,在黑白圖像中降噪更容易處理(去除雜亂黑點),降噪使用的方法仍然是上面的膨脹和腐蝕法

//第一次二值化,轉爲黑白圖片
cv::Mat binary;  			    
cv::adaptiveThreshold(gray,binary,255,cv::ADAPTIVE_THRESH_GAUSSIAN_C,cv::THRESH_BINARY,31,10);

//在第二次二值化以前 爲了去除噪點 作了兩次膨脹腐蝕

//膨脹一次
cv::Mat dilateelement = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(4,2));
cv::Mat dilate1;
dilate(binary, dilate1, dilateelement);
    
//輕度腐蝕一次,去除噪點
cv::Mat element3 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(4,4));
cv::Mat erode11;
erode(dilate1, erode11, element3);
    
//第二次膨脹
cv::Mat dilateelement12 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5,1));
cv::Mat dilate12;
dilate(erode11, dilate12, dilateelement12);

//輕度腐蝕一次,去除噪點
cv::Mat element12 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5,1));
cv::Mat erode12;
erode(dilate12, erode12, element12);

複製代碼

看一下通過兩次降噪以後的圖像是怎麼樣的

豎紋基本上不見了,仍然還有一部分黑點,可是已經不影響後面的識別了,這裏降噪只能適度,過分處理可能會使文字部分丟失。

作完二值化反轉以後是上面這個樣子的,接下來再對圖片作膨脹->腐蝕->膨脹處理

//二值化 第二次二值化將黑白圖像反轉 文字變亮
cv::Mat binary2;  
cv::adaptiveThreshold(erode12,binary2,255,cv::ADAPTIVE_THRESH_GAUSSIAN_C,cv::THRESH_BINARY_INV,17,10);

//橫向膨脹拉伸 文字連片造成亮條
cv::Mat dilateelement21 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(60,1));
cv::Mat dilate21;
dilate(binary2, dilate21, dilateelement21);

//腐蝕一次,去掉細節,表格線等。這裏去掉的是豎直的線
cv::Mat erodeelement21 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(30,1));
cv::Mat erode21;
erode(dilate21, erode21, erodeelement21);

//再次膨脹,讓輪廓明顯一些
cv::Mat dilateelement22 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5,1));
cv::Mat dilate22;
dilate(erode21, dilate22, dilateelement22);
複製代碼

處理的結果圖以下:

最終的框選效果

固然調試過程當中不止用了這一張圖片,畢竟結果要有必定的普適性,下面是其餘幾種狀況下的識別結果

好了,下面貼一下整個過程的源碼

+ (UIImage *)detect:(UIImage *) image {
    
    cv::Mat img;
    img = [self cvMatFromUIImage:image];
    
    //1.轉化成灰度圖
    cv::Mat gray;
    cvtColor(bigImg, gray, cv::COLOR_BGR2GRAY);
    
    //2.形態學變換的預處理,獲得能夠查找矩形的輪廓
    cv::Mat dilation = [self preprocess:gray];
    
    //3.查找和篩選文字區域
    std::vector<cv::RotatedRect> rects = [self findTextRegion:dilation];
    
    //4.用線畫出這些找到的輪廓
    for (int i = 0; i < rects.size(); i++) {
        cv::Point2f P[4];
        cv::RotatedRect rect = rects[i];
        rect.points(P);
        for (int j = 0; j <= 3; j++) {
            cv::line(bigImg, P[j], P[(j + 1) % 4], cv::Scalar(0,0,255),2);
        }
    }
    
    return [self UIImageFromCVMat:bigImg];
}

+ (cv::Mat) preprocess:(cv::Mat)gray {
    
    //第一次二值化,轉爲黑白圖片
    cv::Mat binary; 				  
    cv::adaptiveThreshold(gray, binary, 255,cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, 31, 10);
    
    //在第二次二值化以前 爲了去除噪點 作了兩次膨脹腐蝕,OpenCV是對亮點進行操做,在黑白圖像中降噪更容易處理(去除雜亂黑點)
    
    //膨脹一次
    cv::Mat dilateelement = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(4,2));
    cv::Mat dilate1;
    dilate(binary, dilate1, dilateelement);
    
    //輕度腐蝕一次,去除噪點
    cv::Mat element3 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(4,4));
    cv::Mat erode11;
    erode(dilate1, erode11, element3);
    
    //第二次膨脹
    cv::Mat dilateelement12 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5,1));
    cv::Mat dilate12;
    dilate(erode11, dilate12, dilateelement12);
    
    //輕度腐蝕一次,去除噪點
    cv::Mat element12 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5,1));
    cv::Mat erode12;
    erode(dilate12, erode12, element12);
    
    //////////////////////////////////////////////////////////
    //二值化 第二次二值化將黑白圖像反轉 文字變亮
    cv::Mat binary2;
    cv::adaptiveThreshold(erode12, binary2, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY_INV, 17, 10);
    
    //橫向膨脹拉伸 文字連片造成亮條
    cv::Mat dilateelement21 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(60,1));
    cv::Mat dilate21;
    dilate(binary2, dilate21, dilateelement21);

    //腐蝕一次,去掉細節,表格線等。這裏去掉的是豎直的線
    cv::Mat erodeelement21 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(30,1));
    cv::Mat erode21;
    erode(dilate21, erode21, erodeelement21);

    //再次膨脹,讓輪廓明顯一些
    cv::Mat dilateelement22 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5,1));
    cv::Mat dilate22;
    dilate(erode21, dilate22, dilateelement22);

    return dilate22;
}

+ (std::vector<cv::RotatedRect>) findTextRegion:(cv::Mat) img {
    
    std::vector<cv::RotatedRect> rects;
    std::vector<int> heights;
    //1.查找輪廓
    std::vector<std::vector<cv::Point> > contours;
    std::vector<cv::Vec4i> hierarchy;
    cv::Mat m = img.clone();
    cv::findContours(img,contours,hierarchy,
                     cv::RETR_EXTERNAL,cv::CHAIN_APPROX_SIMPLE,cv::Point(0,0));
    //2.篩選那些面積小的
    for (int i = 0; i < contours.size(); i++) {
        //計算當前輪廓的面積
        double area = cv::contourArea(contours[i]);
        //面積小於1000的所有篩選掉
        if (area < 1000)
            continue;
        //輪廓近似,做用較小,approxPolyDP函數有待研究
        double epsilon = 0.001*arcLength(contours[i], true);
        cv::Mat approx;
        approxPolyDP(contours[i], approx, epsilon, true);
        
        //找到最小矩形,該矩形可能有方向
        cv::RotatedRect rect = minAreaRect(contours[i]);
        
        //計算高和寬
        int m_width = rect.boundingRect().width;
        int m_height = rect.boundingRect().height;
        
        //篩選那些太細的矩形,留下扁的
        if (m_height > m_width * 1.2)
            continue;
        //過濾很扁的
        if (m_height < 20)
            continue;
        heights.push_back(m_height);
        //符合條件的rect添加到rects集合中
        rects.push_back(rect);
    }
    
    return rects;
}
複製代碼

這裏還有幾個cv::Mat 與 UIImage相互轉換的方法一併提供

//從UIImage對象轉換爲4通道的Mat,便是原圖的Mat
+ (cv::Mat)cvMatFromUIImage:(UIImage *)image
{
    CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);
    CGFloat cols = image.size.width;
    CGFloat rows = image.size.height;
    
    cv::Mat cvMat(rows, cols, CV_8UC4); // 8 bits per component, 4 channels (color channels + alpha)
    
    CGContextRef contextRef = CGBitmapContextCreate(cvMat.data,
                                                    cols,
                                                    rows,
                                                    8,
                                                    cvMat.step[0],
                                                    colorSpace,
                                                    kCGImageAlphaNoneSkipLast |
                                                    kCGBitmapByteOrderDefault);
    
    CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);
    CGContextRelease(contextRef);
    
    return cvMat;
}

//從UIImage轉換單通道的Mat,即灰度值
+ (cv::Mat)cvMatGrayFromUIImage:(UIImage *)image
{
    CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);
    CGFloat cols = image.size.width;
    CGFloat rows = image.size.height;
    
    cv::Mat cvMat(rows, cols, CV_8UC1); // 8 bits per component, 1 channels
    
    CGContextRef contextRef = CGBitmapContextCreate(cvMat.data,
                                                    cols,
                                                    rows,
                                                    8,
                                                    cvMat.step[0],
                                                    colorSpace,
                                                    kCGImageAlphaNoneSkipLast |
                                                    kCGBitmapByteOrderDefault);
    
    CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);
    CGContextRelease(contextRef);
    
    return cvMat;
}

//將Mat轉換爲UIImage
+ (UIImage *)UIImageFromCVMat:(cv::Mat)cvMat
{
    NSData *data = [NSData dataWithBytes:cvMat.data length:cvMat.elemSize()*cvMat.total()];
    CGColorSpaceRef colorSpace;
    
    if (cvMat.elemSize() == 1) {
        colorSpace = CGColorSpaceCreateDeviceGray();
    } else {
        colorSpace = CGColorSpaceCreateDeviceRGB();
    }
    
    CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
    
    // Creating CGImage from cv::Mat
    CGImageRef imageRef = CGImageCreate(cvMat.cols,
                                        cvMat.rows,
                                        8,
                                        8 * cvMat.elemSize(),
                                        cvMat.step[0],                            
                                        colorSpace,
                                        kCGImageAlphaNone|kCGBitmapByteOrderDefault,
                                        provider,
                                        NULL,
                                        false,
                                        kCGRenderingIntentDefault
                                        );
    
    
    // Getting UIImage from CGImage
    UIImage *finalImage = [UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);
    CGDataProviderRelease(provider);
    CGColorSpaceRelease(colorSpace);
    
    return finalImage;
}

複製代碼

結語

調試是一個反覆修改流程、修改參數的過程,至於爲何是這樣的流程和參數都是不斷嘗試以後,經過主觀感覺獲得的結果,有興趣的小夥伴能夠本身修改下參數看看效果,若是有更好的方案歡迎你來和我交流探討,還有,若是真的要運用到項目中,這個方案仍是不完善的,好比黑底白字就沒辦法識別,因此還須要加入邏輯判斷,進行不一樣的處理,我這裏只是提供一個思路。最後附上demo地址因爲openCV框架很大,須要自行下載加入工程,pod文件也沒有上傳,請自行pod install,最後...歡迎Star。

其餘參考內容

OpenCV處理拍照表格(一)

相關文章
相關標籤/搜索