最近參加了大創項目,題目涉及到計算機視覺,學姐發了個修正圖像的博客連接,因而打算用這個題目入門OpenCV。c++
照片中的PPT區域老是沿着x,y,z三個軸都有傾斜(以下圖),要想把照片翻轉到平行位置,須要進行透視變換,而透視變換須要同一像素點變換先後的座標。由此能夠想到,提取矩形區域四個角的座標做爲變換前的座標,變換後的座標能夠設爲照片的四個角落,通過投影變換,矩形區域將會翻轉並充滿圖像。
git
所以咱們要解決的問題變爲:提取矩形的四個角落、進行透視變換。github
矩形的檢測主要是提取邊緣,PPT顯示部分的亮度一般高於周圍環境,咱們能夠將圖片閾值化,將PPT部分與周圍環境明顯的分別開來,這對後邊的邊緣檢測很是有幫助。數組
檢測矩形並提取座標須要對圖像進行預處理、邊緣檢測、提取輪廓、檢測凸包、角點檢測。app
因爲手機拍攝的照片像素可能會很高,爲了加快處理速度,咱們首先縮小圖片,這裏縮小了4倍。函數
pyrDown(srcPic, shrinkedPic); //減少尺寸 加快運算速度 pyrDown(shrinkedPic, shrinkedPic);
cvtColor(shrinkedPic, greyPic, COLOR_BGR2GRAY); //轉化爲灰度圖
medianBlur(greyPic, greyPic, 7); //中值濾波
threshold(greyPic, binPic, 80, 255, THRESH_BINARY); //閾值化爲二值圖片
此時圖片已經變成了這個樣子:
ui
可見PPT部分已經與環境分離開來。spa
Canny(binPic, cannyPic, cannyThr, cannyThr*FACTOR); //Canny邊緣檢測
這裏 cannyThr = 200, FACTOR = 2.5
可能因爲邊緣特徵過於明顯,係數在100-600範圍(具體數字可能有出入,反正範圍很是大)內產生的效果幾乎相同。code
vector<vector<Point>> contours; //儲存輪廓 vector<Vec4i> hierarchy; findContours(cannyPic, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE); //獲取輪廓
findContour
函數原型以下:orm
CV_EXPORTS_W void findContours( InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset = Point());
檢測到的輪廓都存在contours
裏,每一個輪廓保存爲一個vector<Point>
hierarchy
爲可選的輸出向量,包括圖像的拓撲信息,這裏能夠選擇不用。
咱們能夠反覆調用drawContours
函數將輪廓畫出
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
函數原型:
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
爲線條的粗細, contourIdx
爲負數時畫出全部輪廓
這裏要注意的是在繪製輪廓前要提早爲輸出矩陣分配空間,不然會出現如下錯誤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的矩形已經成爲了圖片的主要部分,接下來的思路是提取面積最大的輪廓,獲得矩形輪廓。
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
用來計算輪廓的面積approxPolyDP
的做用是用多邊形包圍輪廓,能夠獲得嚴格的矩形,有助於找到角點
畫出矩形,一樣注意要提早爲Mat
分配空間
Mat polyPic = Mat::zeros(shrinkedPic.size(), CV_8UC3); drawContours(polyPic, polyContours, maxArea, Scalar(0,0,255/*rand() & 255, rand() & 255, rand() & 255*/), 2);
如圖,接下來咱們只需提取到四個角的座標
vector<int> hull; convexHull(polyContours[maxArea], hull, false); //檢測該輪廓的凸包
convexHull
函數原型
CV_EXPORTS_W void convexHull( InputArray points, OutputArray hull, bool clockwise = false, bool returnPoints = true );
hull
爲輸出參數, clockwise
決定凸包順逆時針方向, returnPoints
爲真時返回凸包的各個點,不然返回各點的指數 hull
能夠爲vector<int>
類型,此時返回的是凸包點在原圖中的下標索引
咱們能夠把點和多邊形添加到原圖中查看效果
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);
如今咱們已經比較準確地得到了須要的點,下面就要利用這些點進行座標映射。
投影變換須要像素在兩個座標系中的座標一一對應,雖然咱們已經有了四個座標,但尚未區分它們的位置。
新建兩個數組
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
儲存的是變換後各點的座標,依次爲左上,右上,右下, 左下srcPoints
儲存的是上面獲得的四個角的座標
下面對獲得的四個點進行處理
for (int i = 0; i < 4; i++){ polyContours[maxArea][i] = Point2f(polyContours[maxArea][i].x * 4, polyContours[maxArea][i].y * 4); //恢復座標到原圖 } //對四個點進行排序 分出左上 右上 右下 左下 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]; }
即先對四個點的x座標進行冒泡排序分出左右,再根據兩對座標的y值比較分出上下
(筆者試圖經過凸包的順逆時針順序以及凸包點與原點的距離來活得位置信息,卻均以失敗了結)
座標變換須要矩陣運算,OpenCV中給咱們提供了getPerspectiveTransform
函數用來獲得矩陣
Mat transMat = getPerspectiveTransform(srcPoints, dstPoints); //獲得變換矩陣
接下來進行座標變換,網上查到的步驟都是經過perspectiveTransform
函數變換,但嘗試屢次都出現了報錯,Google了好長時間才知道原來這個函數的傳入輸入輸出參數均爲點集,咱們這個場景用起來比較麻煩。
而warpPerspective
函數能夠直接傳入輸入Mat
類型數據,比較方便
warpPerspective(srcPic, outPic, transMat, srcPic.size()); //進行座標變換
參數分別爲輸入輸出圖像、變換矩陣、大小。
座標變換後就獲得了咱們要的最終圖像。
咱們利用了屏幕亮度較高的特色,經過二值化突出輪廓提取座標,進行透視變換。
但侷限性在於,若是矩形的亮度與背景相差不大,就很難用這種方法檢測到輪廓。