最近下了幾個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 app。git
根據OpenCV入門筆記(七) 文字區域的提取中提供的思路,我實現了OC版本的代碼,經過測試,清晰的文字截圖識別沒有問題,可是在複雜的拍照場景中幾乎沒法識別任何內容,例以下圖github
這張是相機拍攝的屏幕上的文字,有清晰的豎紋及屏幕反光,在該算法下,最終的框選區域是整個圖片,沒法識別文字區域,說明這個處理流程仍是不完善的,咱們先來看一下他的處理過程:objective-c
根據前面獲得的識別結果,咱們大體能夠猜想問題出在了第二步,因爲豎紋影響將所有文字區域連城一片,致使整圖被框選。那麼在第二步中都作了哪些操做呢?算法
實際上上面的流程一共作了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。