在OpenCV2:圖像的幾何變換,平移、鏡像、縮放、旋轉(1)主要介紹了圖像變換中的向前映射、向后映射、處理變換過程中浮點坐標像素值的插值算法,並且基於OpenCV2實現了兩個簡單的幾何變換:平移和鏡像變換。本文主要稍微復雜點的兩個幾何變換:縮放和旋轉。
1.圖像縮放
圖像的縮放主要用於改變圖像的大小,縮放后圖像的圖像的寬度和高度會發生變化。水平縮放系數,控制圖像寬度的縮放,其值為1,則圖像的寬度不變;垂直縮放系數控制圖像高度的縮放,其值為1,則圖像的高度不變。如果水平縮放系數和垂直縮放系數不相等,那么縮放后圖像的寬度和高度的比例會發生變化,會使圖像變形。要保持圖像寬度和高度的比例不發生變化,就需要水平縮放系數和垂直縮放系數相等。
左邊的圖像水平縮放系數和垂直縮放系數都是0.5;右邊的圖像的水平縮放系數為1,垂直縮放系數為0.5,縮放后圖像寬度和高度比例發生變化,圖像變形。
1.1 縮放原理
設水平縮放系數為sx,垂直縮放系數為sy,(x0,y0)為縮放前坐標,(x,y)為縮放后坐標,其縮放的坐標映射關系:
矩陣表示的形式為:
這是向前映射,在縮放的過程改變了圖像的大小,使用向前映射會出現映射重疊和映射不完全的問題,所以這里更關心的是向后映射,也就是輸出圖像通過向后映射關系找到其在原圖像中對應的像素。
向后映射關系:
1.2基於OpenCV的縮放實現
在圖像縮放的時首先需要計算縮放后圖像的大小,設newWidth,newHeight為縮放后的圖像的寬和高,width,height為原圖像的寬度和高度,那么有:
然后遍歷縮放后的圖像,根據向后映射關系計算出縮放的像素在原圖像中像素的位置,如果得到的浮點坐標,就需要使用插值算法取得近似的像素值。
根據上面公式可知,縮放后圖像的寬和高用原圖像寬和高和縮放因子相乘即可。
int rows = static_cast<int>(src.rows * xRatio + 0.5); int cols = static_cast<int>(src.cols * yRatio + 0.5);
在向后映射時有可能得到浮點坐標,這里使用最鄰近插值和雙線性插值來處理。
最鄰近插值
for (int i = 0; i < rows; i++){ int row = static_cast<int>(i / xRatio + 0.5); if (row >= src.rows) row--; origin = src.ptr<uchar>(row); p = dst.ptr<uchar>(i); for (int j = 0; j < cols; j++){ int col = static_cast<int>(j / yRatio + 0.5); if (col >= src.cols) col--; p[j] = origin[col]; } }
最鄰近插值只需要對浮點坐標“四舍五入”運算。但是在四舍五入的時候有可能使得到的結果超過原圖像的邊界(只會比邊界大1),所以要進行下修正。
雙線性插值
雙線性插值的精度要比最鄰近插值好很多,相對的其計算量也要大的多。雙線性插值使用浮點坐標周圍四個像素的值按照一定的比例混合近似得到浮點坐標的像素值。
設浮點坐標F,其周圍的四個整數坐標別為T1,T2,T3,T4,並且F和其左上角的整數坐標的縱軸差的絕對值為n,橫軸差的絕對值為n。據上一篇文章分析可得浮點坐標F的像素值T,有下面公式計算得到:
F1為([F.y],F.x),F2為([F.y]+1,F.x)。具體的參見OpenCV2:圖像的幾何變換,平移、鏡像、縮放、旋轉(1).
在實現的時候首先要根據浮點坐標計算出其周圍的四個整數坐標
double row = i / xRatio; double col = j / yRatio; int lRow = static_cast<int>(row); int nRow = lRow + 1; int lCol = static_cast<int>(col); int rCol = lCol + 1; double u = row - lRow; double v = col - lCol;
縮放放后圖像的坐標(i,j),根絕向后映射關系找到其在原圖像中對應的坐標(i / xRatio,j / yRatio),接着找到改坐標周圍的四個整數坐標(lcol,lRow),(lCol,nrow),
(rCol,lRow),(rCo1,nRow)。下面根據雙線性插值公式得到浮點坐標的像素值
//坐標在圖像的右下角 if ((row >= src.rows - 1) && (col >= src.cols - 1)) { lastRow = src.ptr<Vec3b>(lRow); p[j] = lastRow[lCol]; } //最后一行 else if (row >= src.rows - 1) { lastRow = src.ptr<Vec3b>(lRow); p[j] = v * lastRow[lCol] + (1 - v) * lastRow[rCol]; } //最后一列 else if (col >= src.cols - 1){ lastRow = src.ptr<Vec3b>(lRow); nextRow = src.ptr<Vec3b>(nRow); p[j] = u * lastRow[lCol] + (1 - u) * nextRow[lCol]; } else { lastRow = src.ptr<Vec3b>(lRow); nextRow = src.ptr<Vec3b>(nRow); Vec3b f1 = v * lastRow[lCol] + (1 - v) * lastRow[rCol]; Vec3b f2 = v * nextRow[lCol] + (1 - v) * lastRow[rCol]; p[j] = u * f1 + (1 - u) * f2; }
由於使用四個像素進行計算,在邊界的時候,會有不存在的像素,這里把在圖像的右下角、最后一行、最后一列三種特殊情形分別處理。
2.圖像旋轉
2.1旋轉原理
圖像的旋轉就是讓圖像按照某一點旋轉指定的角度。圖像旋轉后不會變形,但是其垂直對稱抽和水平對稱軸都會發生改變,旋轉后圖像的坐標和原圖像坐標之間的關系已不能通過簡單的加減乘法得到,而需要通過一系列的復雜運算。而且圖像在旋轉后其寬度和高度都會發生變化,其坐標原點會發生變化。
圖像所用的坐標系不是常用的笛卡爾,其左上角是其坐標原點,X軸沿着水平方向向右,Y軸沿着豎直方向向下。而在旋轉的過程一般使用旋轉中心為坐標原點的笛卡爾坐標系,所以圖像旋轉的第一步就是坐標系的變換。設旋轉中心為(x0,y0),(x’,y’)是旋轉后的坐標,(x,y)是旋轉后的坐標,則坐標變換如下:
矩陣表示為:
在最終的實現中,常用到的是有縮放后的圖像通過映射關系找到其坐標在原圖像中的相應位置,這就需要上述映射的逆變換
坐標系變換到以旋轉中心為原點后,接下來就要對圖像的坐標進行變換。
上圖所示,將坐標(x0,y0)順時針方向旋轉a,得到(x1,y1)。
旋轉前有:
旋轉a后有:
矩陣的表示形式:
其逆變換:
由於在旋轉的時候是以旋轉中心為坐標原點的,旋轉結束后還需要將坐標原點移到圖像左上角,也就是還要進行一次變換。這里需要注意的是,旋轉中心的坐標(x0,y0)實在以原圖像的左上角為坐標原點的坐標系中得到,而在旋轉后由於圖像的寬和高發生了變化,也就導致了旋轉后圖像的坐標原點和旋轉前的發生了變換。
上邊兩圖,可以清晰的看到,旋轉前后圖像的左上角,也就是坐標原點發生了變換。
在求圖像旋轉后左上角的坐標前,先來看看旋轉后圖像的寬和高。從上圖可以看出,旋轉后圖像的寬和高與原圖像的四個角旋轉后的位置有關。
設top為旋轉后最高點的縱坐標,down為旋轉后最低點的縱坐標,left為旋轉后最左邊點的橫坐標,right為旋轉后最右邊點的橫坐標。
旋轉后的寬和高為newWidth,newHeight,則可得到下面的關系:
也就很容易的得出旋轉后圖像左上角坐標(left,top)(以旋轉中心為原點的坐標系)
故在旋轉完成后要將坐標系轉換為以圖像的左上角為坐標原點,可由下面變換關系得到:
矩陣表示:
其逆變換:
綜合以上,也就是說原圖像的像素坐標要經過三次的坐標變換:
- 將坐標原點由圖像的左上角變換到旋轉中心
- 以旋轉中心為原點,圖像旋轉角度a
- 旋轉結束后,將坐標原點變換到旋轉后圖像的左上角
可以得到下面的旋轉公式:(x’,y’)旋轉后的坐標,(x,y)原坐標,(x0,y0)旋轉中心,a旋轉的角度(順時針)
這種由輸入圖像通過映射得到輸出圖像的坐標,是向前映射。常用的向后映射是其逆運算
2.2基於OpenCV的實現
得到了上述的旋轉公式,實現起來就不是很困難了.
首先計算四個角的旋轉后坐標(以旋轉中心為坐標原點)
const double cosAngle = cos(angle); const double sinAngle = sin(angle); //原圖像四個角的坐標變為以旋轉中心的坐標系 Point2d leftTop(-center.x, center.y); //(0,0) Point2d rightTop(src.cols - center.x,center.y); // (width,0) Point2d leftBottom(-center.x, -src.rows + center.y); //(0,height) Point2d rightBottom(src.cols - center.x, -src.rows + center.y); // (width,height) //以center為中心旋轉后四個角的坐標 Point2d transLeftTop, transRightTop, transLeftBottom, transRightBottom; transLeftTop = coordinates(leftTop, angle); transRightTop = coordinates(rightTop, angle); transLeftBottom = coordinates(leftBottom, angle); transRightBottom = coordinates(rightBottom, angle);
需要注意的是要將原圖像四個角的坐標變為以旋轉中心為坐標原點的坐標系坐標。然后通過旋轉變換公式
由於旋轉角度的不同旋轉后四個角的位置和其在原圖像的位置是不相同的,也就是說原圖像的左上角在旋轉后不一定是旋轉后圖像的左上角,有可能是右下角。所以在計算旋轉后圖像的寬度就不能使用原圖右上角旋轉后的橫坐標減去原圖像左下角旋轉后的橫坐標,高度也是如此。(在查找資料時發現,大部分都是使用這種方式計算的圖像的寬度和高度)。
//計算旋轉后圖像的width,height double left = min({ transLeftTop.x, transRightTop.x, transLeftBottom.x, transRightBottom.x }); double right = max({ transLeftTop.x, transRightTop.x, transLeftBottom.x, transRightBottom.x }); double top = max({ transLeftTop.y, transRightTop.y, transLeftBottom.y, transRightBottom.y }); double down = min({ transLeftTop.y, transRightTop.y, transLeftBottom.y, transRightBottom.y }); int width = static_cast<int>(abs(left - right) + 0.5); int height = static_cast<int>(abs(top - down) + 0.5);
計算旋轉圖像的寬度,可以使用四個角旋轉后最右邊點的橫坐標減去最左邊點的橫坐標;高度時最上邊點的縱坐標減去最下邊點的縱坐標。
然后,就可以使用最終的那個旋轉公式,處理圖像的每一個像素了。
const double num1 = -abs(left) * cosAngle - abs(top) * sinAngle + center.x; const double num2 = abs(left) * sinAngle - abs(top) * cosAngle + center.y; Vec3b *p; for (int i = 0; i < height; i++) { p = dst.ptr<Vec3b>(i); for (int j = 0; j < width; j++) { //坐標變換 int x = static_cast<int>(j * cosAngle + i * sinAngle + num1 + 0.5 ); int y = static_cast<int>(-j * sinAngle + i * cosAngle + num2 + 0.5 ); if (x >= 0 && y >= 0 && x < src.cols && y < src.rows) p[j] = src.ptr<Vec3b>(y)[x]; } }
這使用的插值方法是最鄰近插值,雙線性插值的實現方法和圖像縮放類似,不再贅述。
使用上述算法進行圖像旋轉,會發現不論使用圖像內的那個位置作為旋轉的中心,最后得到的結果都是一樣的。這是因為,不同位置作為旋轉中心,旋轉后圖像的大小都是一樣,所不同的只是其位置。而在最后的一次變換中(圖像旋轉用了三次坐標變換),統一的把坐標原點移到了旋轉后圖像的左上角。這就相當於對圖像做了一次平移,把其位置也挪到了一起,最后旋轉得到的圖像也就一樣了。
如果,在旋轉結束后把坐標原點不是移到旋轉后圖像的左上角,而是原圖像的左上角,會是怎么一個情形呢?
就像上圖,圖像的部分區域會被截掉。當然,這時旋轉中心不同的話最終得到的圖像也就不同了,被截掉的部分不相同。
3.組合變換
組合變換就是把多種幾何放在一起進行。上面推導了旋轉的變換公式,那么組合變換也就不是很困難了,無非是多了幾個矩陣相乘。比較常見的組合變換:縮放+旋轉+平移,下面以此為例推導下組合變換的公式。
- 縮放
設(x0,y0)是縮放后的坐標,(x,y)是縮放前的坐標,sx,sy為縮放因子 - 平移
設(x0,y0)是平移后的坐標,(x,y)是平移前的坐標,dx,dy為偏移量 - 旋轉
設(x0,y0)是旋轉后坐標,(x,y)是旋轉前坐標,(m,n)是旋轉中心,a是旋轉的角度,(left,top)是旋轉后圖像的左上角坐標
分別得到了三個變換矩陣,按照縮放、平移、旋轉的順序組合起來
組合變換的時候要注意順序,畢竟矩陣的左乘和右乘是不一樣的。
4.最后
到現在,寫的最長的一篇文章。和上一篇放到一起弄了差不多快一個周了,算法的實現除了旋轉有個幾次挫折外,其余的幾種變換都很順利。耗時間的主要是畫圖和數學公式,畫圖一直沒有找到合適的工具,不了了之;數學公式特意花了一天的時間學了Latex,並且找了些把Tex轉換為HTML的工具,但是效果都不是很好,還是粘貼圖片。沒有圖,有些東西只靠文字確實很難說的清楚,抽空得學習學習MATLAB畫圖了。