前言:
相信很多人手機(jī)里都裝了個(gè)“掃描全能王”APP,平時(shí)可以用它來(lái)可以掃描一些證件、文本,確實(shí)很好用,第一次用的時(shí)候確實(shí)感覺(jué)功能很強(qiáng)大啊算法很牛逼啊。但是仔細(xì)一想,其實(shí)這些實(shí)現(xiàn)起來(lái)也是很簡(jiǎn)單的,我想了下,實(shí)現(xiàn)的步驟應(yīng)該就只有下面三個(gè):
將證件輪廓找到
提取證件矩形輪廓四點(diǎn)進(jìn)行透視變換
二值化
知道原理之后,我馬上利用強(qiáng)大的opencv開(kāi)發(fā)一個(gè)類(lèi)似“全能掃描王”掃描工具。
整理一下我們要制作的這個(gè)掃描工具有哪些功能:
圖像的信息區(qū)域的提取與矯正
圖像的二值化
銳化和增強(qiáng)
第二第三點(diǎn)都非常簡(jiǎn)單,那么制作這個(gè)工具的難點(diǎn)完全落在了第一點(diǎn)“ 圖像的信息區(qū)域的提取與矯正”上了。在編碼實(shí)現(xiàn)的過(guò)程中,確實(shí)有很多坑需要踩一踩。
效果圖:
我們先展示一下效果,我們有這么一個(gè)用手機(jī)拍攝的圖片

經(jīng)過(guò)掃描工具一番處理后變成這樣子。也就是說(shuō),我們將原圖中的那個(gè)文件摳了了出來(lái),并且完成矯正。

實(shí)現(xiàn)過(guò)程查閱了大量資料,也看了網(wǎng)上很多類(lèi)似的博客,前輩們實(shí)現(xiàn)過(guò)相類(lèi)似的透視變換的代碼,但是他們的代碼實(shí)現(xiàn)的都不理想,很多圖片根本沒(méi)法檢測(cè)。不過(guò)還是可以從前人的經(jīng)驗(yàn)中獲取到很多好想法的。
代碼實(shí)現(xiàn):
第一步,二值化+高斯濾波+膨脹+canny邊緣提取
一開(kāi)始我是沒(méi)有采取形態(tài)學(xué)處理的,僅僅是二值化+高斯濾波+canny邊緣提取的策略,但是實(shí)際運(yùn)行下效果并不好,原因在于有一些圖片的信息區(qū)域輪廓沒(méi)法閉合,這就導(dǎo)致了信息區(qū)域輪廓沒(méi)法提取。但是加入適當(dāng)?shù)呐蛎浐?,效果就好多了?/p>
Matsrc= imread("1.png");
imshow("src img", src);
Matsource= src.clone();
Matbkup= src.clone();
Matimg= src.clone();
cvtColor(img, img, CV_RGB2GRAY); //二值化
imshow("gray", img);
//equalizeHist(img, img);//imshow("equal", img);
GaussianBlur(img, img, Size(5, 5), 0, 0); //高斯濾波//獲取自定義核Matelement= getStructuringElement(MORPH_RECT, Size(3, 3)); //第一個(gè)參數(shù)MORPH_RECT表示矩形的卷積核,當(dāng)然還可以選擇橢圓形的、交叉型的//膨脹操作
dilate(img, img, element); //實(shí)現(xiàn)過(guò)程中發(fā)現(xiàn),適當(dāng)?shù)呐蛎浐苤匾? imshow("dilate", img);
Canny(img, img, 30, 120, 3); //邊緣提取
imshow("get contour", img);
}
輪廓提取效果如下:

第二步,輪廓查找并篩選
一般情況下,我們提取到的輪廓不會(huì)像上圖那樣的干凈,而是帶有很多干擾項(xiàng)輪廓,如果我們不能很好的剔除這些輪廓,我們根本沒(méi)法找出我們想要的信息區(qū)域。我篩選輪廓的方法很簡(jiǎn)單,就是找出一張圖片中面積最大的那個(gè)輪廓作為我們的信息區(qū)域輪廓,這招真是屢試不爽,因?yàn)楦鶕?jù)我們?nèi)粘=?jīng)驗(yàn),我們對(duì)一張證件或者文件性掃描拍攝,證件區(qū)域占整張圖片的面積肯定是最大的。
vector<vector<Point> > contours;
vector<vector<Point> > f_contours;
std::vector<cv::Point> approx2;
//注意第5個(gè)參數(shù)為CV_RETR_EXTERNAL,只檢索外框 findContours(img, f_contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); //找輪廓//求出面積最大的輪廓int max_area = 0;
int index;
for (int i = 0; i < f_contours.size(); i++)
{
double tmparea = fabs(contourArea(f_contours[i]));
if (tmparea > max_area)
{
index = i;
max_area = tmparea;
}
}
contours.push_back(f_contours[index]);
篩選出我們所需的輪廓

第三步,找出這個(gè)四邊形輪廓的四個(gè)頂點(diǎn)
因?yàn)槲覀冃枰喞乃膫€(gè)頂點(diǎn)坐標(biāo)來(lái)實(shí)現(xiàn)透視變換,現(xiàn)在的問(wèn)題來(lái)了:我們?cè)趺蠢眠@個(gè)四邊形輪廓的點(diǎn)集來(lái)找出初四邊形的四個(gè)頂點(diǎn)?
這真是一個(gè)大難題!
一開(kāi)始我的想法是這樣子的:直接從四邊形點(diǎn)集中篩選出四個(gè)定點(diǎn)(比如x坐標(biāo)最大的那個(gè)坐標(biāo)肯定是四邊形右上角坐標(biāo)或者右下角坐標(biāo),x坐標(biāo)最小的那個(gè)坐標(biāo)肯定是左上角或者下角的那個(gè)坐標(biāo),如此類(lèi)推),但是這種想法實(shí)現(xiàn)起來(lái)是很有問(wèn)題的而因?yàn)樗苁芟抻谒倪呅蔚淖藨B(tài),所以一個(gè)思路一直沒(méi)法進(jìn)行下去。如果大家有僅依賴(lài)四邊形點(diǎn)集就能找出四邊形的四個(gè)頂點(diǎn)坐標(biāo)的方法,請(qǐng)告訴我,我們一同探討。
所以我切換了另外一個(gè)思路:基于直線交點(diǎn)的思路。我們首先使用霍夫變換找出四邊形的邊,然后求兩兩直線的交點(diǎn)不就是四邊形的定點(diǎn)嗎?的確是這樣子的,但是實(shí)際操作起來(lái)也是問(wèn)題多多啊。
最大的問(wèn)題就是,我們?cè)趺幢WC我們使用霍夫變換找到的直線剛好就是形成四邊形的四條直線?
所以我們就必須不斷地去改變霍夫變換的參數(shù),不斷迭代,來(lái)求出一個(gè)可以形成四邊形的直線情況。
那什么情況的直線我們不能接受?
兩兩直線過(guò)于接近我們排除
兩兩直線沒(méi)有交點(diǎn)我們排除
檢測(cè)出來(lái)的直線數(shù)目不是4條我們排除
如果找到了滿(mǎn)足條件的四條直線,我們就可以去計(jì)算他們的交點(diǎn)了。算法如下:
cv::Point2fcomputeIntersect(cv::Vec4i a, cv::Vec4i b)
{
int x1 = a[0], y1 = a[1], x2 = a[2], y2 = a[3];
int x3 = b[0], y3 = b[1], x4 = b[2], y4 = b[3];
if (float d = ((float)(x1 - x2) * (y3 - y4)) - ((y1 - y2) * (x3 - x4)))
{
cv::Point2f pt;
pt.x = ((x1*y2 - y1*x2) * (x3 - x4) - (x1 - x2) * (x3*y4 - y3*x4)) / d;
pt.y = ((x1*y2 - y1*x2) * (y3 - y4) - (y1 - y2) * (x3*y4 - y3*x4)) / d;
return pt;
}
elsereturn cv::Point2f(-1, -1);
}
計(jì)算出四個(gè)交點(diǎn)后,我們不能完全信任他們就是我們要找的四個(gè)頂點(diǎn),所以繼續(xù)篩選:
如果兩兩定點(diǎn)的距離過(guò)近,我們排除
bool IsGoodPoints = true;
//保證點(diǎn)與點(diǎn)的距離足夠大以排除錯(cuò)誤點(diǎn)for (int i = 0; i < corners.size(); i++)
{
for (int j = i + 1; j < corners.size(); j++)
{
int distance = sqrt((corners[i].x - corners[j].x)*(corners[i].x - corners[j].x) + (corners[i].y - corners[j].y)*(corners[i].y - corners[j].y));
if (distance < 5)
{
IsGoodPoints = false;
}
}
}
if (!IsGoodPoints) continue;
如果這四個(gè)點(diǎn)構(gòu)成不了四邊形我們排除
cv::approxPolyDP(cv::Mat(corners), approx, cv::arcLength(cv::Mat(corners), true) * 0.02, true);
if (lines.size() == 4 && corners.size() == 4 && approx.size() == 4)
{
flag = 1;
break;
}
如果都通過(guò)以上篩選條件的,我們就可以認(rèn)為他們就是我們找的那四個(gè)頂點(diǎn),這時(shí)我們就可以停止迭代,進(jìn)行頂點(diǎn)排序,即確定這四個(gè)頂點(diǎn)哪個(gè)是左上角點(diǎn),哪個(gè)又是右下點(diǎn)。
算法如下:
boolx_sort(const Point2f & m1, const Point2f & m2){
return m1.x < m2.x;
}
//確定四個(gè)點(diǎn)的中心線voidsortCorners(std::vector<cv::Point2f>& corners,
cv::Point2f center){
std::vector<cv::Point2f> top, bot;
vector<Point2f> backup = corners;
sort(corners, x_sort); //注意先按x的大小給4個(gè)點(diǎn)排序for (int i = 0; i < corners.size(); i++)
{
if (corners[i].y < center.y && top.size() < 2) //這里的小于2是為了避免三個(gè)頂點(diǎn)都在top的情況
top.push_back(corners[i]);
else
bot.push_back(corners[i]);
}
corners.clear();
if (top.size() == 2 && bot.size() == 2)
{
//cout << "log" << endl;
cv::Point2f tl = top[0].x > top[1].x ? top[1] : top[0];
cv::Point2f tr = top[0].x > top[1].x ? top[0] : top[1];
cv::Point2f bl = bot[0].x > bot[1].x ? bot[1] : bot[0];
cv::Point2f br = bot[0].x > bot[1].x ? bot[0] : bot[1];
corners.push_back(tl);
corners.push_back(tr);
corners.push_back(br);
corners.push_back(bl);
}
else
{
corners = backup;
}
}
第四步,四點(diǎn)法透射變換
我們拿到原圖信息區(qū)域四邊形的四個(gè)頂點(diǎn),現(xiàn)在我們還需要變換后圖像的四個(gè)頂點(diǎn)才可以實(shí)現(xiàn)投射變換。
求變換后四個(gè)頂點(diǎn)坐標(biāo)前我們還需要做的一件事就是,確定變換后的圖像尺寸。第一種方法就是人工指定,比如我直接規(guī)定好變換后的圖片大小是bbb*aaa。第二種方法就是,通過(guò)計(jì)算確定信息區(qū)域的尺寸,也就是說(shuō),信息區(qū)域有多大,我們變換后的圖像就有多大。
既然我們知道了四邊形的四個(gè)頂點(diǎn)了,那么我們可以直接求兩點(diǎn)的距離來(lái)確定四邊形的長(zhǎng)寬。變換后的圖像高度寬度可以這么確定:
int g_dst_hight; //最終圖像的高度int g_dst_width; //最終圖像的寬度voidCalcDstSize(const vector<cv::Point2f>& corners){
int h1 = sqrt((corners[0].x - corners[3].x)*(corners[0].x - corners[3].x) + (corners[0].y - corners[3].y)*(corners[0].y - corners[3].y));
int h2 = sqrt((corners[1].x - corners[2].x)*(corners[1].x - corners[2].x) + (corners[1].y - corners[2].y)*(corners[1].y - corners[2].y));
g_dst_hight = MAX(h1, h2);
int w1 = sqrt((corners[0].x - corners[1].x)*(corners[0].x - corners[1].x) + (corners[0].y - corners[1].y)*(corners[0].y - corners[1].y));
int w2 = sqrt((corners[2].x - corners[3].x)*(corners[2].x - corners[3].x) + (corners[2].y - corners[3].y)*(corners[2].y - corners[3].y));
g_dst_width = MAX(w1, w2);
}
透射變換:
cv::Mat quad = cv::Mat::zeros(g_dst_hight, g_dst_width, CV_8UC3);
std::vector<cv::Point2f> quad_pts;
quad_pts.push_back(cv::Point2f(0, 0));
quad_pts.push_back(cv::Point2f(quad.cols, 0));
quad_pts.push_back(cv::Point2f(quad.cols, quad.rows));
quad_pts.push_back(cv::Point2f(0, quad.rows));
cv::Mat transmtx = cv::getPerspectiveTransform(corners, quad_pts);
cv::warpPerspective(source, quad, transmtx, quad.size());
所有關(guān)鍵步驟都已經(jīng)說(shuō)明完畢,運(yùn)行一下代碼,看看效果。

再拍一些其他圖片,看看處理效果




試一下帶有干擾背景的圖像,效果還是不錯(cuò)的

額外效果:二值化
有些時(shí)候還需要將一些文本或者證件弄成掃描模樣,那我們就加入二值化實(shí)現(xiàn)該效果。
Mat local,gray;
cvtColor(quad, gray, CV_RGB2GRAY);
int blockSize = 25;
int constValue = 10;
adaptiveThreshold(gray, local, 255, CV_ADAPTIVE_THRESH_MEAN_C, CV_THRESH_BINARY, blockSize, constValue);
imshow("二值化", local);
二值化效果挺好的文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-481475.html


源碼下載:
OpenCV實(shí)現(xiàn)“全能掃描王”的圖像矯正功能文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-481475.html
到了這里,關(guān)于OpenCV實(shí)現(xiàn)“全能掃描王”的圖像矯正功能的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!