您好,登錄后才能下訂單哦!
這篇文章主要介紹了怎么利用OpenCV提取圖像中的矩形區(qū)域,具有一定借鑒價(jià)值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
分析問題
照片中的PPT區(qū)域總是沿著x,y,z三個(gè)軸都有傾斜(如下圖),要想把照片翻轉(zhuǎn)到平行位置,需要進(jìn)行透視變換,而透視變換需要同一像素點(diǎn)變換前后的坐標(biāo)。由此可以想到,提取矩形區(qū)域四個(gè)角的坐標(biāo)作為變換前的坐標(biāo),變換后的坐標(biāo)可以設(shè)為照片的四個(gè)角落,經(jīng)過投影變換,矩形區(qū)域?qū)?huì)翻轉(zhuǎn)并充滿圖像。
因此我們要解決的問題變?yōu)椋禾崛【匦蔚乃膫€(gè)角落、進(jìn)行透視變換。
提取矩形角落坐標(biāo)
矩形的檢測主要是提取邊緣,PPT顯示部分的亮度通常高于周圍環(huán)境,我們可以將圖片閾值化,將PPT部分與周圍環(huán)境明顯的分別開來,這對后邊的邊緣檢測非常有幫助。
檢測矩形并提取坐標(biāo)需要對圖像進(jìn)行預(yù)處理、邊緣檢測、提取輪廓、檢測凸包、角點(diǎn)檢測。
預(yù)處理
由于手機(jī)拍攝的照片像素可能會(huì)很高,為了加快處理速度,我們首先縮小圖片,這里縮小了4倍。
pyrDown(srcPic, shrinkedPic); //減小尺寸 加快運(yùn)算速度 pyrDown(shrinkedPic, shrinkedPic);
轉(zhuǎn)化為灰度圖
cvtColor(shrinkedPic, greyPic, COLOR_BGR2GRAY); //轉(zhuǎn)化為灰度圖
中值濾波
medianBlur(greyPic, greyPic, 7); //中值濾波
轉(zhuǎn)為二值圖片
threshold(greyPic, binPic, 80, 255, THRESH_BINARY); //閾值化為二值圖片
此時(shí)圖片已經(jīng)變成了這個(gè)樣子:
可見PPT部分已經(jīng)與環(huán)境分離開來。
邊緣檢測與輪廓處理
進(jìn)行Canny邊緣檢測
Canny(binPic, cannyPic, cannyThr, cannyThr*FACTOR); //Canny邊緣檢測
這里 cannyThr = 200, FACTOR = 2.5
可能由于邊緣特征過于明顯,系數(shù)在100-600范圍(具體數(shù)字可能有出入,反正范圍非常大)內(nèi)產(chǎn)生的效果幾乎相同。
提取輪廓
vector<vector<Point>> contours; //儲(chǔ)存輪廓 vector<Vec4i> hierarchy; findContours(cannyPic, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE); //獲取輪廓
findContour
函數(shù)原型如下:
CV_EXPORTS_W void findContours( InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset = Point());
檢測到的輪廓都存在contours
里,每個(gè)輪廓保存為一個(gè)vector<Point>
hierarchy
為可選的輸出向量,包括圖像的拓?fù)湫畔?,這里可以選擇不用。
我們可以反復(fù)調(diào)用drawContours
函數(shù)將輪廓畫出
linePic = Mat::zeros(cannyPic.rows, cannyPic.cols, CV_8UC3); for (int index = 0; index < contours.size(); index++){ drawContours(linePic, contours, index, Scalar(rand() & 255, rand() & 255, rand() & 255), 1, 8/*, hierarchy*/); }
drawContours
函數(shù)原型:
CV_EXPORTS_W void drawContours( InputOutputArray image, InputArrayOfArrays contours, int contourIdx, const Scalar& color, int thickness = 1, int lineType = LINE_8, InputArray hierarchy = noArray(), int maxLevel = INT_MAX, Point offset = Point() );
作用是將contours
中的第contourIdx
條輪廓用color
顏色繪制到image
中,thickness
為線條的粗細(xì), contourIdx
為負(fù)數(shù)時(shí)畫出所有輪廓
這里要注意的是在繪制輪廓前要提前為輸出矩陣分配空間,否則會(huì)出現(xiàn)以下錯(cuò)誤
OpenCV(3.4.1) Error: Assertion failed (size.width>0 && size.height>0) in cv::imshow, file C:\build\master_winpack-build-win64-vc15\opencv\modules\highgui\src\window.cpp, line 356
提取面積最大的輪廓并用多邊形將輪廓包圍
從上面的輪廓圖中看出,PPT的矩形已經(jīng)成為了圖片的主要部分,接下來的思路是提取面積最大的輪廓,得到矩形輪廓。
vector<vector<Point>> polyContours(contours.size()); int maxArea = 0; for (int index = 0; index < contours.size(); index++){ if (contourArea(contours[index]) > contourArea(contours[maxArea])) maxArea = index; approxPolyDP(contours[index], polyContours[index], 10, true); }
contourArea
用來計(jì)算輪廓的面積approxPolyDP
的作用是用多邊形包圍輪廓,可以得到嚴(yán)格的矩形,有助于找到角點(diǎn)
畫出矩形,同樣注意要提前為Mat
分配空間
Mat polyPic = Mat::zeros(shrinkedPic.size(), CV_8UC3); drawContours(polyPic, polyContours, maxArea, Scalar(0,0,255/*rand() & 255, rand() & 255, rand() & 255*/), 2);
如圖,接下來我們只需提取到四個(gè)角的坐標(biāo)
尋找凸包
vector<int> hull; convexHull(polyContours[maxArea], hull, false); //檢測該輪廓的凸包
convexHull
函數(shù)原型
CV_EXPORTS_W void convexHull( InputArray points, OutputArray hull, bool clockwise = false, bool returnPoints = true );
hull
為輸出參數(shù), clockwise
決定凸包順逆時(shí)針方向, returnPoints
為真時(shí)返回凸包的各個(gè)點(diǎn),否則返回各點(diǎn)的指數(shù) hull
可以為vector<int>
類型,此時(shí)返回的是凸包點(diǎn)在原圖中的下標(biāo)索引
我們可以把點(diǎn)和多邊形添加到原圖中查看效果
for (int i = 0; i < hull.size(); ++i){ circle(polyPic, polyContours[maxArea][i], 10, Scalar(rand() & 255, rand() & 255, rand() & 255), 3); } addWeighted(polyPic, 0.5, shrinkedPic, 0.5, 0, shrinkedPic);
現(xiàn)在我們已經(jīng)比較準(zhǔn)確地獲得了需要的點(diǎn),下面就要利用這些點(diǎn)進(jìn)行坐標(biāo)映射。
投影變換
投影變換需要像素在兩個(gè)坐標(biāo)系中的坐標(biāo)一一對應(yīng),雖然我們已經(jīng)有了四個(gè)坐標(biāo),但還沒有區(qū)分它們的位置。
新建兩個(gè)數(shù)組
Point2f srcPoints[4], dstPoints[4]; dstPoints[0] = Point2f(0, 0); dstPoints[1] = Point2f(srcPic.cols, 0); dstPoints[2] = Point2f(srcPic.cols, srcPic.rows); dstPoints[3] = Point2f(0, srcPic.rows);
dstPoints
儲(chǔ)存的是變換后各點(diǎn)的坐標(biāo),依次為左上,右上,右下, 左下
srcPoints
儲(chǔ)存的是上面得到的四個(gè)角的坐標(biāo)
下面對得到的四個(gè)點(diǎn)進(jìn)行處理
for (int i = 0; i < 4; i++){ polyContours[maxArea][i] = Point2f(polyContours[maxArea][i].x * 4, polyContours[maxArea][i].y * 4); //恢復(fù)坐標(biāo)到原圖 } //對四個(gè)點(diǎn)進(jìn)行排序 分出左上 右上 右下 左下 bool sorted = false; int n = 4; while (!sorted){ for (int i = 1; i < n; i++){ sorted = true; if (polyContours[maxArea][i-1].x > polyContours[maxArea][i].x){ swap(polyContours[maxArea][i-1], polyContours[maxArea][i]); sorted = false; } } n--; } if (polyContours[maxArea][0].y < polyContours[maxArea][1].y){ srcPoints[0] = polyContours[maxArea][0]; srcPoints[3] = polyContours[maxArea][1]; } else{ srcPoints[0] = polyContours[maxArea][1]; srcPoints[3] = polyContours[maxArea][0]; } if (polyContours[maxArea][9].y < polyContours[maxArea][10].y){ srcPoints[1] = polyContours[maxArea][2]; srcPoints[2] = polyContours[maxArea][3]; } else{ srcPoints[1] = polyContours[maxArea][3]; srcPoints[2] = polyContours[maxArea][2]; }
即先對四個(gè)點(diǎn)的x坐標(biāo)進(jìn)行冒泡排序分出左右,再根據(jù)兩對坐標(biāo)的y值比較分出上下
(筆者試圖通過凸包的順逆時(shí)針順序以及凸包點(diǎn)與原點(diǎn)的距離來活得位置信息,卻均以失敗告終)
坐標(biāo)變換需要矩陣運(yùn)算,OpenCV中給我們提供了getPerspectiveTransform
函數(shù)用來得到矩陣
Mat transMat = getPerspectiveTransform(srcPoints, dstPoints); //得到變換矩陣
接下來進(jìn)行坐標(biāo)變換,網(wǎng)上查到的步驟都是通過perspectiveTransform
函數(shù)變換,但嘗試多次都出現(xiàn)了報(bào)錯(cuò),Google了好長時(shí)間才知道原來這個(gè)函數(shù)的傳入輸入輸出參數(shù)均為點(diǎn)集,我們這個(gè)場景用起來比較麻煩。
而warpPerspective
函數(shù)可以直接傳入輸入Mat
類型數(shù)據(jù),比較方便
warpPerspective(srcPic, outPic, transMat, srcPic.size()); //進(jìn)行坐標(biāo)變換
參數(shù)分別為輸入輸出圖像、變換矩陣、大小。
坐標(biāo)變換后就得到了我們要的最終圖像。
感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“怎么利用OpenCV提取圖像中的矩形區(qū)域”這篇文章對大家有幫助,同時(shí)也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識(shí)等著你來學(xué)習(xí)!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。