轉自:https://www.cnblogs.com/skyfsm/p/6902524.html
剛進入實驗室導師就交給我一個任務,就是讓我設計算法給圖像進行矯正。哎呀,我不太會圖像這塊啊,不過還是接下來了,硬着頭皮開幹吧!
那什麼是圖像的矯正呢?舉個例子就好明白了。
我的好朋友小明給我拍了這幾張照片,因爲他的拍照技術不咋地,照片都拍得歪歪扭扭的,比如下面這些照片:
人民幣
發票
文本
這些圖片讓人看得真不舒服!看個圖片還要歪脖子看,實在是太煩人了!我叫小明幫我掃描一下一本教科書,小明把每一頁書都拍成上面的文本那樣了。好氣啊那該怎麼辦呢?一頁一頁用PS來處理?1000頁的矯正啊,當然交給計算機去做!
真的,對於圖像矯正的問題,在圖像處理領域還真得多,比如人民幣的矯正、文本的矯正、車牌的矯正、身份證矯正等等。這些都是因爲拍攝者總不可能100%正確地拍攝好圖片,這就要求我們通過後期的圖像處理技術將圖片還原好,才能進一步做後面的處理,比如數字分割啊數字識別啊,不然歪歪扭扭的文字數字,想識別出來估計就很難了。
上面幾個圖,我們在日常生活中遇到的可不少,因爲拍攝時拍的不好,導致拍出來的圖片歪歪扭扭的,很不自然,那麼我們能不能把這些圖片儘可能地矯正過來呢?
OpenCV告訴我們,沒問題!工具我給你,算法你自己設計!
比如圖一,我要想將人民幣矯正,並且把人民幣整個摳出來保存,該怎麼做?那就涉及到了圖像的矯正和感興趣區域提取兩大技術了。
總的來說,要進行進行圖像矯正,至少有以下幾項知識儲備:
- 輪廓提取技術
- 霍夫變換知識
- ROI感興趣區域知識
下面以人民幣矯正、發票矯正、文本矯正爲例,一步步剖析如何實現圖像矯正。
首先分析如何矯正人民幣。
比如我們要矯正這張人民幣,思路應該是怎麼樣?
首先分析這張圖的特點。
在這張圖裏,人民幣有一定的傾斜角度,但是角度不大;人民幣的背景是黑色的,而且人民幣的邊緣應該比較明顯。
沒錯,我們就抓住人民幣的的邊緣比較明顯來做文章!我們是不是可以先把人民幣的輪廓找出來(找出來的輪廓當然就是一個大大的矩形),然後用矩形去包圍它,得到他的旋轉角度,然後根據得到的角度進行旋轉,那樣不就可以實現矯正了嗎!
再詳細地總結處理步驟:
- 圖片灰度化
- 閾值二值化
- 檢測輪廓
- 尋找輪廓的包圍矩陣,並且獲取角度
- 根據角度進行旋轉矯正
- 對旋轉後的圖像進行輪廓提取
- 對輪廓內的圖像區域摳出來,成爲一張獨立圖像
我把該矯正算法命名爲基於輪廓提取的矯正算法,因爲其關鍵技術就是通過輪廓來獲取旋轉角度。
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;
//第一個參數:輸入圖片名稱;第二個參數:輸出圖片名稱
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);
<span class="hljs-built_in">vector</span><<span class="hljs-built_in">vector</span><Point> > contours;
<span class="hljs-built_in">vector</span><Rect> boundRect(contours.size());
<span class="hljs-comment">//注意第5個參數爲CV_RETR_EXTERNAL,只檢索外框 </span>
findContours(binImg, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); <span class="hljs-comment">//找輪廓</span>
<span class="hljs-built_in">cout</span> << contours.size() << <span class="hljs-built_in">endl</span>;
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i < contours.size(); i++)
{
<span class="hljs-comment">//需要獲取的座標 </span>
CvPoint2D32f rectpoint[<span class="hljs-number">4</span>];
CvBox2D rect =minAreaRect(Mat(contours[i]));
cvBoxPoints(rect, rectpoint); <span class="hljs-comment">//獲取4個頂點座標 </span>
<span class="hljs-comment">//與水平線的角度 </span>
<span class="hljs-keyword">float</span> angle = rect.angle;
<span class="hljs-built_in">cout</span> << angle << <span class="hljs-built_in">endl</span>;
<span class="hljs-keyword">int</span> line1 = <span class="hljs-built_in">sqrt</span>((rectpoint[<span class="hljs-number">1</span>].y - rectpoint[<span class="hljs-number">0</span>].y)*(rectpoint[<span class="hljs-number">1</span>].y - rectpoint[<span class="hljs-number">0</span>].y) + (rectpoint[<span class="hljs-number">1</span>].x - rectpoint[<span class="hljs-number">0</span>].x)*(rectpoint[<span class="hljs-number">1</span>].x - rectpoint[<span class="hljs-number">0</span>].x));
<span class="hljs-keyword">int</span> line2 = <span class="hljs-built_in">sqrt</span>((rectpoint[<span class="hljs-number">3</span>].y - rectpoint[<span class="hljs-number">0</span>].y)*(rectpoint[<span class="hljs-number">3</span>].y - rectpoint[<span class="hljs-number">0</span>].y) + (rectpoint[<span class="hljs-number">3</span>].x - rectpoint[<span class="hljs-number">0</span>].x)*(rectpoint[<span class="hljs-number">3</span>].x - rectpoint[<span class="hljs-number">0</span>].x));
<span class="hljs-comment">//rectangle(binImg, rectpoint[0], rectpoint[3], Scalar(255), 2);</span>
<span class="hljs-comment">//面積太小的直接pass</span>
<span class="hljs-keyword">if</span> (line1 * line2 < <span class="hljs-number">600</span>)
{
<span class="hljs-keyword">continue</span>;
}
<span class="hljs-comment">//爲了讓正方形橫着放,所以旋轉角度是不一樣的。豎放的,給他加90度,翻過來 </span>
<span class="hljs-keyword">if</span> (line1 > line2)
{
angle = <span class="hljs-number">90</span> + angle;
}
<span class="hljs-comment">//新建一個感興趣的區域圖,大小跟原圖一樣大 </span>
<span class="hljs-function">Mat <span class="hljs-title">RoiSrcImg</span><span class="hljs-params">(srcImg.rows, srcImg.cols, CV_8UC3)</span></span>; <span class="hljs-comment">//注意這裏必須選CV_8UC3</span>
RoiSrcImg.setTo(<span class="hljs-number">0</span>); <span class="hljs-comment">//顏色都設置爲黑色 </span>
<span class="hljs-comment">//imshow("新建的ROI", RoiSrcImg);</span>
<span class="hljs-comment">//對得到的輪廓填充一下 </span>
drawContours(binImg, contours, <span class="hljs-number">-1</span>, Scalar(<span class="hljs-number">255</span>),CV_FILLED);
<span class="hljs-comment">//摳圖到RoiSrcImg</span>
srcImg.copyTo(RoiSrcImg, binImg);
<span class="hljs-comment">//再顯示一下看看,除了感興趣的區域,其他部分都是黑色的了 </span>
namedWindow(<span class="hljs-string">"RoiSrcImg"</span>, <span class="hljs-number">1</span>);
imshow(<span class="hljs-string">"RoiSrcImg"</span>, RoiSrcImg);
<span class="hljs-comment">//創建一個旋轉後的圖像 </span>
<span class="hljs-function">Mat <span class="hljs-title">RatationedImg</span><span class="hljs-params">(RoiSrcImg.rows, RoiSrcImg.cols, CV_8UC1)</span></span>;
RatationedImg.setTo(<span class="hljs-number">0</span>);
<span class="hljs-comment">//對RoiSrcImg進行旋轉 </span>
Point2f center = rect.center; <span class="hljs-comment">//中心點 </span>
Mat M2 = getRotationMatrix2D(center, angle, <span class="hljs-number">1</span>);<span class="hljs-comment">//計算旋轉加縮放的變換矩陣 </span>
warpAffine(RoiSrcImg, RatationedImg, M2, RoiSrcImg.size(),<span class="hljs-number">1</span>, <span class="hljs-number">0</span>, Scalar(<span class="hljs-number">0</span>));<span class="hljs-comment">//仿射變換 </span>
imshow(<span class="hljs-string">"旋轉之後"</span>, RatationedImg);
imwrite(<span class="hljs-string">"r.jpg"</span>, RatationedImg); <span class="hljs-comment">//將矯正後的圖片保存下來</span>
}
//對ROI區域進行摳圖
<span class="hljs-comment">//對旋轉後的圖片進行輪廓提取 </span>
<span class="hljs-built_in">vector</span><<span class="hljs-built_in">vector</span><Point> > contours2;
Mat raw = imread(<span class="hljs-string">"r.jpg"</span>);
Mat SecondFindImg;
<span class="hljs-comment">//SecondFindImg.setTo(0);</span>
cvtColor(raw, SecondFindImg, COLOR_BGR2GRAY); <span class="hljs-comment">//灰度化 </span>
threshold(SecondFindImg, SecondFindImg, <span class="hljs-number">80</span>, <span class="hljs-number">200</span>, CV_THRESH_BINARY);
findContours(SecondFindImg, contours2, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
<span class="hljs-comment">//cout << "sec contour:" << contours2.size() << endl;</span>
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> j = <span class="hljs-number">0</span>; j < contours2.size(); j++)
{
<span class="hljs-comment">//這時候其實就是一個長方形了,所以獲取rect </span>
Rect rect = boundingRect(Mat(contours2[j]));
<span class="hljs-comment">//面積太小的輪廓直接pass,通過設置過濾面積大小,可以保證只拿到外框</span>
<span class="hljs-keyword">if</span> (rect.area() < <span class="hljs-number">600</span>)
{
<span class="hljs-keyword">continue</span>;
}
Mat dstImg = raw(rect);
imshow(<span class="hljs-string">"dst"</span>, dstImg);
imwrite(pDstFileName, dstImg);
}
}
void main()
{
GetContoursPic(“6.jpg”, “FinalImage.jpg”);
waitKey();
}
效果依次如下:
原始圖
二值化圖
掩膜mask是這樣的
旋轉矯正之後
將人民幣區域摳出來
該算法的效果還是很不錯的!那趕緊試試其他圖片,我把傾斜的發票圖像拿去試試。
原始圖
傾斜矯正之後
最後把目標區域摳出來,成爲單獨的照片。
上面的算法可以很好的處理人民幣和發票兩種情況的傾斜矯正,那文本矯正可以嗎?我趕緊試了一下,結果是失敗的。
原圖
算法矯正後,還是原樣,矯正失敗。
認真分析一下,還是很容易看出文本矯正失敗的原因的。
原因就在於,人民幣圖像和發票圖像他們有明顯的的邊界輪廓,而文本圖像沒有。文本圖像的背景是白色的,所以我們沒有辦法像人民幣發票那類有明顯邊界的矩形物體那樣,提取出輪廓並旋轉矯正。
經過深入分析可以看出,雖然文本類圖像沒有明顯的邊緣輪廓,但是他們有一個很重要的特徵,那就是每一行文字都是呈現一條直線形狀,而且這些直線都是平行的!
對於這種情況,我想到了另一種方法:基於直線探測的矯正算法。
首先介紹一下我的算法思路:
- 用霍夫線變換探測出圖像中的所有直線
- 計算出每條直線的傾斜角,求他們的平均值
- 根據傾斜角旋轉矯正
- 最後根據文本尺寸裁剪圖片
然後給出OpenCV的實現算法:
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;
//度數轉換
double DegreeTrans(double theta)
{
double res = theta / CV_PI * 180;
return res;
}
//逆時針旋轉圖像degree角度(原尺寸)
void rotateImage(Mat src, Mat& img_rotate, double degree)
{
//旋轉中心爲圖像中心
Point2f center;
center.x = float(src.cols / 2.0);
center.y = float(src.rows / 2.0);
int length = 0;
length = sqrt(src.colssrc.cols + src.rowssrc.rows);
//計算二維旋轉的仿射變換矩陣
Mat M = getRotationMatrix2D(center, degree, 1);
warpAffine(src, img_rotate, M, Size(length, length), 1, 0, Scalar(255,255,255));//仿射變換,背景色填充爲白色
}
//通過霍夫變換計算角度
double CalcDegree(const Mat &srcImage, Mat &dst)
{
Mat midImage, dstImage;
Canny(srcImage, midImage, <span class="hljs-number">50</span>, <span class="hljs-number">200</span>, <span class="hljs-number">3</span>);
cvtColor(midImage, dstImage, CV_GRAY2BGR);
<span class="hljs-comment">//通過霍夫變換檢測直線</span>
<span class="hljs-built_in">vector</span><Vec2f> lines;
HoughLines(midImage, lines, <span class="hljs-number">1</span>, CV_PI / <span class="hljs-number">180</span>, <span class="hljs-number">300</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);<span class="hljs-comment">//第5個參數就是閾值,閾值越大,檢測精度越高</span>
<span class="hljs-comment">//cout << lines.size() << endl;</span>
<span class="hljs-comment">//由於圖像不同,閾值不好設定,因爲閾值設定過高導致無法檢測直線,閾值過低直線太多,速度很慢</span>
<span class="hljs-comment">//所以根據閾值由大到小設置了三個閾值,如果經過大量試驗後,可以固定一個適合的閾值。</span>
<span class="hljs-keyword">if</span> (!lines.size())
{
HoughLines(midImage, lines, <span class="hljs-number">1</span>, CV_PI / <span class="hljs-number">180</span>, <span class="hljs-number">200</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
}
<span class="hljs-comment">//cout << lines.size() << endl;</span>
<span class="hljs-keyword">if</span> (!lines.size())
{
HoughLines(midImage, lines, <span class="hljs-number">1</span>, CV_PI / <span class="hljs-number">180</span>, <span class="hljs-number">150</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
}
<span class="hljs-comment">//cout << lines.size() << endl;</span>
<span class="hljs-keyword">if</span> (!lines.size())
{
<span class="hljs-built_in">cout</span> << <span class="hljs-string">"沒有檢測到直線!"</span> << <span class="hljs-built_in">endl</span>;
<span class="hljs-keyword">return</span> ERROR;
}
<span class="hljs-keyword">float</span> sum = <span class="hljs-number">0</span>;
<span class="hljs-comment">//依次畫出每條線段</span>
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">size_t</span> i = <span class="hljs-number">0</span>; i < lines.size(); i++)
{
<span class="hljs-keyword">float</span> rho = lines[i][<span class="hljs-number">0</span>];
<span class="hljs-keyword">float</span> theta = lines[i][<span class="hljs-number">1</span>];
Point pt1, pt2;
<span class="hljs-comment">//cout << theta << endl;</span>
<span class="hljs-keyword">double</span> a = <span class="hljs-built_in">cos</span>(theta), b = <span class="hljs-built_in">sin</span>(theta);
<span class="hljs-keyword">double</span> x0 = a*rho, y0 = b*rho;
pt1.x = cvRound(x0 + <span class="hljs-number">1000</span> * (-b));
pt1.y = cvRound(y0 + <span class="hljs-number">1000</span> * (a));
pt2.x = cvRound(x0 - <span class="hljs-number">1000</span> * (-b));
pt2.y = cvRound(y0 - <span class="hljs-number">1000</span> * (a));
<span class="hljs-comment">//只選角度最小的作爲旋轉角度</span>
sum += theta;
line(dstImage, pt1, pt2, Scalar(<span class="hljs-number">55</span>, <span class="hljs-number">100</span>, <span class="hljs-number">195</span>), <span class="hljs-number">1</span>, LINE_AA); <span class="hljs-comment">//Scalar函數用於調節線段顏色</span>
imshow(<span class="hljs-string">"直線探測效果圖"</span>, dstImage);
}
<span class="hljs-keyword">float</span> average = sum / lines.size(); <span class="hljs-comment">//對所有角度求平均,這樣做旋轉效果會更好</span>
<span class="hljs-built_in">cout</span> << <span class="hljs-string">"average theta:"</span> << average << <span class="hljs-built_in">endl</span>;
<span class="hljs-keyword">double</span> angle = DegreeTrans(average) - <span class="hljs-number">90</span>;
rotateImage(dstImage, dst, angle);
<span class="hljs-comment">//imshow("直線探測效果圖2", dstImage);</span>
<span class="hljs-keyword">return</span> 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(“旋轉調整後”, dst);
Mat resulyImage = dst(Rect(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, dst.cols, <span class="hljs-number">500</span>)); <span class="hljs-comment">//根據先驗知識,估計好文本的長寬,再裁剪下來</span>
imshow(<span class="hljs-string">"裁剪之後"</span>, resulyImage);
imwrite(<span class="hljs-string">"recified.jpg"</span>, resulyImage);
}
int main()
{
ImageRecify(“correct2.jpg”, “FinalImage.jpg”);
waitKey();
return 0;
}
看看效果。這是原始圖
直線探測的效果。
矯正之後的效果。
我們發現矯正之後的圖像有較多留白,影響觀看,所以需要進一步裁剪,保留文字區域。
趕緊再試多一張。
原始圖
直線探測
矯正效果
進一步裁剪
可以看出,基於直線探測的矯正算法在文本處理上效果真的很不錯!
最後總結一下兩個算法的應用場景:
-
基於輪廓提取的矯正算法更適用於車牌、身份證、人民幣、書本、發票一類矩形形狀而且邊界明顯的物體矯正。
-
基於直線探測的矯正算法更適用於文本類的矯正。