深入探討C++ OpenCV如何實(shí)現(xiàn)圖像矯正
剛進(jìn)入實(shí)驗(yàn)室導(dǎo)師就交給我一個(gè)任務(wù),就是讓我設(shè)計(jì)算法給圖像進(jìn)行矯正。哎呀,我不太會(huì)圖像這塊啊,不過(guò)還是接下來(lái)了,硬著頭皮開(kāi)干吧!
那什么是圖像的矯正呢?舉個(gè)例子就好明白了。
我的好朋友小明給我拍了這幾張照片,因?yàn)樗呐恼占夹g(shù)不咋地,照片都拍得歪歪扭扭的,比如下面這些照片:
發(fā)票
文本
這些圖片讓人看得真不舒服!看個(gè)圖片還要歪脖子看,實(shí)在是太煩人了!我叫小明幫我掃描一下一本教科書(shū),小明把每一頁(yè)書(shū)都拍成上面的文本那樣了。好氣啊那該怎么辦呢?一頁(yè)一頁(yè)用PS來(lái)處理?1000頁(yè)的矯正啊,當(dāng)然交給計(jì)算機(jī)去做!
真的,對(duì)于圖像矯正的問(wèn)題,在圖像處理領(lǐng)域還真得多,比如人民幣的矯正、文本的矯正、車(chē)牌的矯正、身份證矯正等等。這些都是因?yàn)榕臄z者總不可能100%正確地拍攝好圖片,這就要求我們通過(guò)后期的圖像處理技術(shù)將圖片還原好,才能進(jìn)一步做后面的處理,比如數(shù)字分割啊數(shù)字識(shí)別啊,不然歪歪扭扭的文字?jǐn)?shù)字,想識(shí)別出來(lái)估計(jì)就很難了。
上面幾個(gè)圖,我們?cè)谌粘I钪杏龅降目刹簧?,因?yàn)榕臄z時(shí)拍的不好,導(dǎo)致拍出來(lái)的圖片歪歪扭扭的,很不自然,那么我們能不能把這些圖片盡可能地矯正過(guò)來(lái)呢?
OpenCV告訴我們,沒(méi)問(wèn)題!工具我給你,算法你自己設(shè)計(jì)!
比如圖一該怎么做?那就涉及到了圖像的矯正和感興趣區(qū)域提取兩大技術(shù)了。
總的來(lái)說(shuō),要進(jìn)行進(jìn)行圖像矯正,至少有以下幾項(xiàng)知識(shí)儲(chǔ)備:
- 輪廓提取技術(shù)
- 霍夫變換知識(shí)
- ROI感興趣區(qū)域知識(shí)
下面以發(fā)票矯正、文本矯正為例,一步步剖析如何實(shí)現(xiàn)圖像矯正。
比如我們要矯正這張圖片,思路應(yīng)該是怎么樣?
首先分析這張圖的特點(diǎn)。
在這張圖里,物體有一定的傾斜角度,但是角度不大;背景是黑色的,而且物體邊緣應(yīng)該比較明顯。
沒(méi)錯(cuò),我們就抓住邊緣比較明顯來(lái)做文章!我們是不是可以先把輪廓找出來(lái)(找出來(lái)的輪廓當(dāng)然就是一個(gè)大大的矩形),然后用矩形去包圍它,得到他的旋轉(zhuǎn)角度,然后根據(jù)得到的角度進(jìn)行旋轉(zhuǎn),那樣不就可以實(shí)現(xiàn)矯正了嗎!
再詳細(xì)地總結(jié)處理步驟:
- 圖片灰度化
- 閾值二值化
- 檢測(cè)輪廓
- 尋找輪廓的包圍矩陣,并且獲取角度
- 根據(jù)角度進(jìn)行旋轉(zhuǎn)矯正
- 對(duì)旋轉(zhuǎn)后的圖像進(jìn)行輪廓提取
- 對(duì)輪廓內(nèi)的圖像區(qū)域摳出來(lái),成為一張獨(dú)立圖像
我把該矯正算法命名為基于輪廓提取的矯正算法,因?yàn)槠潢P(guān)鍵技術(shù)就是通過(guò)輪廓來(lái)獲取旋轉(zhuǎn)角度。
#include "opencv2/imgproc.hpp" #include "opencv2/highgui.hpp" #include <iostream> using namespace cv; using namespace std; //第一個(gè)參數(shù):輸入圖片名稱(chēng);第二個(gè)參數(shù):輸出圖片名稱(chēng) void GetContoursPic(const char* pSrcFileName, const char* pDstFileName) { Mat srcImg = imread(pSrcFileName); imshow("原始圖", srcImg); Mat gray, binImg; //灰度化 cvtColor(srcImg, gray, COLOR_RGB2GRAY); imshow("灰度圖", gray); //二值化 threshold(gray, binImg, 100, 200, CV_THRESH_BINARY); imshow("二值化", binImg); vector<vector<Point> > contours; vector<Rect> boundRect(contours.size()); //注意第5個(gè)參數(shù)為CV_RETR_EXTERNAL,只檢索外框 findContours(binImg, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); //找輪廓 cout << contours.size() << endl; for (int i = 0; i < contours.size(); i++) { //需要獲取的坐標(biāo) CvPoint2D32f rectpoint[4]; CvBox2D rect =minAreaRect(Mat(contours[i])); cvBoxPoints(rect, rectpoint); //獲取4個(gè)頂點(diǎn)坐標(biāo) //與水平線(xiàn)的角度 float angle = rect.angle; cout << angle << endl; int line1 = sqrt((rectpoint[1].y - rectpoint[0].y)*(rectpoint[1].y - rectpoint[0].y) + (rectpoint[1].x - rectpoint[0].x)*(rectpoint[1].x - rectpoint[0].x)); int line2 = sqrt((rectpoint[3].y - rectpoint[0].y)*(rectpoint[3].y - rectpoint[0].y) + (rectpoint[3].x - rectpoint[0].x)*(rectpoint[3].x - rectpoint[0].x)); //rectangle(binImg, rectpoint[0], rectpoint[3], Scalar(255), 2); //面積太小的直接pass if (line1 * line2 < 600) { continue; } //為了讓正方形橫著放,所以旋轉(zhuǎn)角度是不一樣的。豎放的,給他加90度,翻過(guò)來(lái) if (line1 > line2) { angle = 90 + angle; } //新建一個(gè)感興趣的區(qū)域圖,大小跟原圖一樣大 Mat RoiSrcImg(srcImg.rows, srcImg.cols, CV_8UC3); //注意這里必須選CV_8UC3 RoiSrcImg.setTo(0); //顏色都設(shè)置為黑色 //imshow("新建的ROI", RoiSrcImg); //對(duì)得到的輪廓填充一下 drawContours(binImg, contours, -1, Scalar(255),CV_FILLED); //摳圖到RoiSrcImg srcImg.copyTo(RoiSrcImg, binImg); //再顯示一下看看,除了感興趣的區(qū)域,其他部分都是黑色的了 namedWindow("RoiSrcImg", 1); imshow("RoiSrcImg", RoiSrcImg); //創(chuàng)建一個(gè)旋轉(zhuǎn)后的圖像 Mat RatationedImg(RoiSrcImg.rows, RoiSrcImg.cols, CV_8UC1); RatationedImg.setTo(0); //對(duì)RoiSrcImg進(jìn)行旋轉(zhuǎn) Point2f center = rect.center; //中心點(diǎn) Mat M2 = getRotationMatrix2D(center, angle, 1);//計(jì)算旋轉(zhuǎn)加縮放的變換矩陣 warpAffine(RoiSrcImg, RatationedImg, M2, RoiSrcImg.size(),1, 0, Scalar(0));//仿射變換 imshow("旋轉(zhuǎn)之后", RatationedImg); imwrite("r.jpg", RatationedImg); //將矯正后的圖片保存下來(lái) } #if 1 //對(duì)ROI區(qū)域進(jìn)行摳圖 //對(duì)旋轉(zhuǎn)后的圖片進(jìn)行輪廓提取 vector<vector<Point> > contours2; Mat raw = imread("r.jpg"); Mat SecondFindImg; //SecondFindImg.setTo(0); cvtColor(raw, SecondFindImg, COLOR_BGR2GRAY); //灰度化 threshold(SecondFindImg, SecondFindImg, 80, 200, CV_THRESH_BINARY); findContours(SecondFindImg, contours2, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); //cout << "sec contour:" << contours2.size() << endl; for (int j = 0; j < contours2.size(); j++) { //這時(shí)候其實(shí)就是一個(gè)長(zhǎng)方形了,所以獲取rect Rect rect = boundingRect(Mat(contours2[j])); //面積太小的輪廓直接pass,通過(guò)設(shè)置過(guò)濾面積大小,可以保證只拿到外框 if (rect.area() < 600) { continue; } Mat dstImg = raw(rect); imshow("dst", dstImg); imwrite(pDstFileName, dstImg); } #endif } void main() { GetContoursPic("6.jpg", "FinalImage.jpg"); waitKey(); }
效果如下:
原始圖
傾斜矯正之后
最后把目標(biāo)區(qū)域摳出來(lái),成為單獨(dú)的照片。
上面的算法可以很好的處理發(fā)票的傾斜矯正,那文本矯正可以嗎?我趕緊試了一下,結(jié)果是失敗的。
原圖
算法矯正后,還是原樣,矯正失敗。
認(rèn)真分析一下,還是很容易看出文本矯正失敗的原因的。
原因就在于,發(fā)票圖像他們有明顯的的邊界輪廓,而文本圖像沒(méi)有。文本圖像的背景是白色的,所以我們沒(méi)有辦法像人民幣發(fā)票那類(lèi)有明顯邊界的矩形物體那樣,提取出輪廓并旋轉(zhuǎn)矯正。
經(jīng)過(guò)深入分析可以看出,雖然文本類(lèi)圖像沒(méi)有明顯的邊緣輪廓,但是他們有一個(gè)很重要的特征,那就是每一行文字都是呈現(xiàn)一條直線(xiàn)形狀,而且這些直線(xiàn)都是平行的!
對(duì)于這種情況,我想到了另一種方法:基于直線(xiàn)探測(cè)的矯正算法。
首先介紹一下我的算法思路:
- 用霍夫線(xiàn)變換探測(cè)出圖像中的所有直線(xiàn)
- 計(jì)算出每條直線(xiàn)的傾斜角,求他們的平均值
- 根據(jù)傾斜角旋轉(zhuǎn)矯正
- 最后根據(jù)文本尺寸裁剪圖片
然后給出OpenCV的實(shí)現(xiàn)算法:
#include "opencv2/imgproc.hpp" #include "opencv2/highgui.hpp" #include <iostream> using namespace cv; using namespace std; #define ERROR 1234 //度數(shù)轉(zhuǎn)換 double DegreeTrans(double theta) { double res = theta / CV_PI * 180; return res; } //逆時(shí)針旋轉(zhuǎn)圖像degree角度(原尺寸) void rotateImage(Mat src, Mat& img_rotate, double degree) { //旋轉(zhuǎn)中心為圖像中心 Point2f center; center.x = float(src.cols / 2.0); center.y = float(src.rows / 2.0); int length = 0; length = sqrt(src.cols*src.cols + src.rows*src.rows); //計(jì)算二維旋轉(zhuǎn)的仿射變換矩陣 Mat M = getRotationMatrix2D(center, degree, 1); warpAffine(src, img_rotate, M, Size(length, length), 1, 0, Scalar(255,255,255));//仿射變換,背景色填充為白色 } //通過(guò)霍夫變換計(jì)算角度 double CalcDegree(const Mat &srcImage, Mat &dst) { Mat midImage, dstImage; Canny(srcImage, midImage, 50, 200, 3); cvtColor(midImage, dstImage, CV_GRAY2BGR); //通過(guò)霍夫變換檢測(cè)直線(xiàn) vector<Vec2f> lines; HoughLines(midImage, lines, 1, CV_PI / 180, 300, 0, 0);//第5個(gè)參數(shù)就是閾值,閾值越大,檢測(cè)精度越高 //cout << lines.size() << endl; //由于圖像不同,閾值不好設(shè)定,因?yàn)殚撝翟O(shè)定過(guò)高導(dǎo)致無(wú)法檢測(cè)直線(xiàn),閾值過(guò)低直線(xiàn)太多,速度很慢 //所以根據(jù)閾值由大到小設(shè)置了三個(gè)閾值,如果經(jīng)過(guò)大量試驗(yàn)后,可以固定一個(gè)適合的閾值。 if (!lines.size()) { HoughLines(midImage, lines, 1, CV_PI / 180, 200, 0, 0); } //cout << lines.size() << endl; if (!lines.size()) { HoughLines(midImage, lines, 1, CV_PI / 180, 150, 0, 0); } //cout << lines.size() << endl; if (!lines.size()) { cout << "沒(méi)有檢測(cè)到直線(xiàn)!" << endl; return ERROR; } float sum = 0; //依次畫(huà)出每條線(xiàn)段 for (size_t i = 0; i < lines.size(); i++) { float rho = lines[i][0]; float theta = lines[i][1]; Point pt1, pt2; //cout << theta << endl; double a = cos(theta), b = sin(theta); double x0 = a*rho, y0 = b*rho; pt1.x = cvRound(x0 + 1000 * (-b)); pt1.y = cvRound(y0 + 1000 * (a)); pt2.x = cvRound(x0 - 1000 * (-b)); pt2.y = cvRound(y0 - 1000 * (a)); //只選角度最小的作為旋轉(zhuǎn)角度 sum += theta; line(dstImage, pt1, pt2, Scalar(55, 100, 195), 1, LINE_AA); //Scalar函數(shù)用于調(diào)節(jié)線(xiàn)段顏色 imshow("直線(xiàn)探測(cè)效果圖", dstImage); } float average = sum / lines.size(); //對(duì)所有角度求平均,這樣做旋轉(zhuǎn)效果會(huì)更好 cout << "average theta:" << average << endl; double angle = DegreeTrans(average) - 90; rotateImage(dstImage, dst, angle); //imshow("直線(xiàn)探測(cè)效果圖2", dstImage); return angle; } void ImageRecify(const char* pInFileName, const char* pOutFileName) { double degree; Mat src = imread(pInFileName); imshow("原始圖", src); Mat dst; //傾斜角度矯正 degree = CalcDegree(src,dst); if (degree == ERROR) { cout << "矯正失??!" << endl; return; } rotateImage(src, dst, degree); cout << "angle:" << degree << endl; imshow("旋轉(zhuǎn)調(diào)整后", dst); Mat resulyImage = dst(Rect(0, 0, dst.cols, 500)); //根據(jù)先驗(yàn)知識(shí),估計(jì)好文本的長(zhǎng)寬,再裁剪下來(lái) imshow("裁剪之后", resulyImage); imwrite("recified.jpg", resulyImage); } int main() { ImageRecify("correct2.jpg", "FinalImage.jpg"); waitKey(); return 0; }
看看效果。這是原始圖
直線(xiàn)探測(cè)的效果。
矯正之后的效果。
我們發(fā)現(xiàn)矯正之后的圖像有較多留白,影響觀看,所以需要進(jìn)一步裁剪,保留文字區(qū)域。
趕緊再試多一張。
原始圖
直線(xiàn)探測(cè)
矯正效果
進(jìn)一步裁剪
可以看出,基于直線(xiàn)探測(cè)的矯正算法在文本處理上效果真的很不錯(cuò)!
最后總結(jié)一下兩個(gè)算法的應(yīng)用場(chǎng)景:
- 基于輪廓提取的矯正算法更適用于車(chē)牌、身份證、人民幣、書(shū)本、發(fā)票一類(lèi)矩形形狀而且邊界明顯的物體矯正。
- 基于直線(xiàn)探測(cè)的矯正算法更適用于文本類(lèi)的矯正。
以上就是深入探討C++ OpenCV如何實(shí)現(xiàn)圖像矯正的詳細(xì)內(nèi)容,更多關(guān)于OpenCV圖像矯正的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C++中類(lèi)型推斷(auto和decltype)的使用
在C++11之前,每個(gè)數(shù)據(jù)類(lèi)型都需要在編譯時(shí)顯示聲明,在運(yùn)行時(shí)限制表達(dá)式的值,但在C++的新版本之后,引入了 auto 和 decltype等關(guān)鍵字,本文就來(lái)介紹一下C++中類(lèi)型推斷(auto和decltype)的使用,感興趣的可以了解一下2023-12-12利用C語(yǔ)言實(shí)現(xiàn)猜數(shù)字游戲
這篇文章主要為大家詳細(xì)介紹了利用C語(yǔ)言實(shí)現(xiàn)猜數(shù)字游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-02-02Qt QFtp客戶(hù)端實(shí)現(xiàn)上傳下載文件
本文主要介紹了Qt QFtp客戶(hù)端實(shí)現(xiàn)上傳下載文件,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07???????C語(yǔ)言實(shí)現(xiàn)單鏈表基本操作方法
這篇文章主要介紹了???????C語(yǔ)言實(shí)現(xiàn)單鏈表基本操作方法,文章圍繞主題展開(kāi)詳細(xì)介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-05-05