前言
最近參加了大創項目,題目涉及到計算機視覺,學姐發了個修正圖像的博客鏈接,于是打算用這個題目入門OpenCV。
分析問題
照片中的PPT區域總是沿著x,y,z三個軸都有傾斜(如下圖),要想把照片翻轉到平行位置,需要進行透視變換,而透視變換需要同一像素點變換前后的坐標。由此可以想到,提取矩形區域四個角的坐標作為變換前的坐標,變換后的坐標可以設為照片的四個角落,經過投影變換,矩形區域將會翻轉并充滿圖像。
因此我們要解決的問題變為:提取矩形的四個角落、進行透視變換。
提取矩形角落坐標
矩形的檢測主要是提取邊緣,PPT顯示部分的亮度通常高于周圍環境,我們可以將圖片閾值化,將PPT部分與周圍環境明顯的分別開來,這對后邊的邊緣檢測非常有幫助。
檢測矩形并提取坐標需要對圖像進行預處理、邊緣檢測、提取輪廓、檢測凸包、角點檢測。
預處理
由于手機拍攝的照片像素可能會很高,為了加快處理速度,我們首先縮小圖片,這里縮小了4倍。
1
2
|
pyrDown(srcPic, shrinkedPic); / / 減小尺寸 加快運算速度 pyrDown(shrinkedPic, shrinkedPic); |
轉化為灰度圖
1
|
cvtColor(shrinkedPic, greyPic, COLOR_BGR2GRAY); / / 轉化為灰度圖 |
中值濾波
1
|
medianBlur(greyPic, greyPic, 7 ); / / 中值濾波 |
轉為二值圖片
1
|
threshold(greyPic, binPic, 80 , 255 , THRESH_BINARY); / / 閾值化為二值圖片 |
此時圖片已經變成了這個樣子:
可見PPT部分已經與環境分離開來。
邊緣檢測與輪廓處理
進行Canny邊緣檢測
1
|
Canny(binPic, cannyPic, cannyThr, cannyThr * FACTOR); / / Canny邊緣檢測 |
這里 cannyThr = 200, FACTOR = 2.5
可能由于邊緣特征過于明顯,系數在100-600范圍(具體數字可能有出入,反正范圍非常大)內產生的效果幾乎相同。
提取輪廓
1
2
3
4
|
vector<vector<Point>> contours; / / 儲存輪廓 vector<Vec4i> hierarchy; findContours(cannyPic, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE); / / 獲取輪廓 |
findContour
函數原型如下:
1
2
3
|
CV_EXPORTS_W void findContours( InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset = Point()); |
檢測到的輪廓都存在contours
里,每個輪廓保存為一個vector<Point>
hierarchy
為可選的輸出向量,包括圖像的拓撲信息,這里可以選擇不用。
我們可以反復調用drawContours
函數將輪廓畫出
1
2
3
4
|
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
函數原型:
1
2
3
4
5
|
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的矩形已經成為了圖片的主要部分,接下來的思路是提取面積最大的輪廓,得到矩形輪廓。
1
2
3
4
5
6
7
|
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
分配空間
1
2
|
Mat polyPic = Mat::zeros(shrinkedPic.size(), CV_8UC3); drawContours(polyPic, polyContours, maxArea, Scalar( 0 , 0 , 255 / * rand() & 255 , rand() & 255 , rand() & 255 * / ), 2 ); |
如圖,接下來我們只需提取到四個角的坐標
尋找凸包
1
2
|
vector< int > hull; convexHull(polyContours[maxArea], hull, false); / / 檢測該輪廓的凸包 |
convexHull
函數原型
1
2
|
CV_EXPORTS_W void convexHull( InputArray points, OutputArray hull, bool clockwise = false, bool returnPoints = true ); |
hull
為輸出參數, clockwise
決定凸包順逆時針方向, returnPoints
為真時返回凸包的各個點,否則返回各點的指數
hull
可以為vector<int>
類型,此時返回的是凸包點在原圖中的下標索引
我們可以把點和多邊形添加到原圖中查看效果
1
2
3
4
|
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); |
現在我們已經比較準確地獲得了需要的點,下面就要利用這些點進行坐標映射。
投影變換
投影變換需要像素在兩個坐標系中的坐標一一對應,雖然我們已經有了四個坐標,但還沒有區分它們的位置。
新建兩個數組
1
2
3
4
5
|
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
儲存的是上面得到的四個角的坐標
下面對得到的四個點進行處理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
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
函數用來得到矩陣
1
|
Mat transMat = getPerspectiveTransform(srcPoints, dstPoints); / / 得到變換矩陣 |
接下來進行坐標變換,網上查到的步驟都是通過perspectiveTransform
函數變換,但嘗試多次都出現了報錯,Google了好長時間才知道原來這個函數的傳入輸入輸出參數均為點集,我們這個場景用起來比較麻煩。
而warpPerspective
函數可以直接傳入輸入Mat
類型數據,比較方便
1
|
warpPerspective(srcPic, outPic, transMat, srcPic.size()); / / 進行坐標變換 |
參數分別為輸入輸出圖像、變換矩陣、大小。
坐標變換后就得到了我們要的最終圖像。
總結
我們利用了屏幕亮度較高的特點,通過二值化突出輪廓提取坐標,進行透視變換。
但局限性在于,如果矩形的亮度與背景相差不大,就很難用這種方法檢測到輪廓。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://segmentfault.com/a/1190000013925648