在OpenCV2:圖像的幾何變換,平移、鏡像、縮放、旋轉(1)主要介紹了圖像變換中的向前映射、向後映射、處理變換過程當中浮點座標像素值的插值算法,而且基於OpenCV2實現了兩個簡單的幾何變換:平移和鏡像變換。本文主要稍微複雜點的兩個幾何變換:縮放和旋轉。html
1.圖像縮放算法
圖像的縮放主要用於改變圖像的大小,縮放後圖像的圖像的寬度和高度會發生變化。水平縮放係數,控制圖像寬度的縮放,其值爲1,則圖像的寬度不變;垂直縮放係數控制圖像高度的縮放,其值爲1,則圖像的高度不變。若是水平縮放係數和垂直縮放係數不相等,那麼縮放後圖像的寬度和高度的比例會發生變化,會使圖像變形。要保持圖像寬度和高度的比例不發生變化,就須要水平縮放係數和垂直縮放係數相等。工具
左邊的圖像水平縮放係數和垂直縮放係數都是0.5;右邊的圖像的水平縮放係數爲1,垂直縮放係數爲0.5,縮放後圖像寬度和高度比例發生變化,圖像變形。學習
設水平縮放係數爲sx,垂直縮放係數爲sy,(x0,y0)爲縮放前座標,(x,y)爲縮放後坐標,其縮放的座標映射關係: spa
矩陣表示的形式爲:3d
這是向前映射,在縮放的過程改變了圖像的大小,使用向前映射會出現映射重疊和映射不徹底的問題,因此這裏更關心的是向後映射,也就是輸出圖像經過向後映射關係找到其在原圖像中對應的像素。code
向後映射關係:htm
在圖像縮放的時首先須要計算縮放後圖像的大小,設newWidth,newHeight爲縮放後的圖像的寬和高,width,height爲原圖像的寬度和高度,那麼有:blog
而後遍歷縮放後的圖像,根據向後映射關係計算出縮放的像素在原圖像中像素的位置,若是獲得的浮點座標,就須要使用插值算法取得近似的像素值。圖片
根據上面公式可知,縮放後圖像的寬和高用原圖像寬和高和縮放因子相乘便可。
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; }
因爲使用四個像素進行計算,在邊界的時候,會有不存在的像素,這裏把在圖像的右下角、最後一行、最後一列三種特殊情形分別處理。
圖像的旋轉就是讓圖像按照某一點旋轉指定的角度。圖像旋轉後不會變形,可是其垂直對稱抽和水平對稱軸都會發生改變,旋轉後圖像的座標和原圖像座標之間的關係已不能經過簡單的加減乘法獲得,而須要經過一系列的複雜運算。並且圖像在旋轉後其寬度和高度都會發生變化,其座標原點會發生變化。
圖像所用的座標系不是經常使用的笛卡爾,其左上角是其座標原點,X軸沿着水平方向向右,Y軸沿着豎直方向向下。而在旋轉的過程通常使用旋轉中心爲座標原點的笛卡爾座標系,因此圖像旋轉的第一步就是座標系的變換。設旋轉中心爲(x0,y0),(x’,y’)是旋轉後的座標,(x,y)是旋轉後的座標,則座標變換以下:
矩陣表示爲:
在最終的實現中,經常使用到的是有縮放後的圖像經過映射關係找到其座標在原圖像中的相應位置,這就須要上述映射的逆變換
座標系變換到以旋轉中心爲原點後,接下來就要對圖像的座標進行變換。
上圖所示,將座標(x0,y0)順時針方向旋轉a,獲得(x1,y1)。
旋轉前有:
旋轉a後有:
矩陣的表示形式:
其逆變換:
因爲在旋轉的時候是以旋轉中心爲座標原點的,旋轉結束後還須要將座標原點移到圖像左上角,也就是還要進行一次變換。這裏須要注意的是,旋轉中心的座標(x0,y0)實在以原圖像的左上角爲座標原點的座標系中獲得,而在旋轉後因爲圖像的寬和高發生了變化,也就致使了旋轉後圖像的座標原點和旋轉前的發生了變換。
上邊兩圖,能夠清晰的看到,旋轉先後圖像的左上角,也就是座標原點發生了變換。
在求圖像旋轉後左上角的座標前,先來看看旋轉後圖像的寬和高。從上圖能夠看出,旋轉後圖像的寬和高與原圖像的四個角旋轉後的位置有關。
設top爲旋轉後最高點的縱座標,down爲旋轉後最低點的縱座標,left爲旋轉後最左邊點的橫座標,right爲旋轉後最右邊點的橫座標。
旋轉後的寬和高爲newWidth,newHeight,則可獲得下面的關係:
也就很容易的得出旋轉後圖像左上角座標(left,top)(以旋轉中心爲原點的座標系)
故在旋轉完成後要將座標系轉換爲以圖像的左上角爲座標原點,可由下面變換關係獲得:
矩陣表示:
其逆變換:
綜合以上,也就是說原圖像的像素座標要通過三次的座標變換:
能夠獲得下面的旋轉公式:(x’,y’)旋轉後的座標,(x,y)原座標,(x0,y0)旋轉中心,a旋轉的角度(順時針)
這種由輸入圖像經過映射獲得輸出圖像的座標,是向前映射。經常使用的向後映射是其逆運算
獲得了上述的旋轉公式,實現起來就不是很困難了.
首先計算四個角的旋轉後坐標(以旋轉中心爲座標原點)
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]; } }
這使用的插值方法是最鄰近插值,雙線性插值的實現方法和圖像縮放相似,再也不贅述。
使用上述算法進行圖像旋轉,會發現不論使用圖像內的那個位置做爲旋轉的中心,最後獲得的結果都是同樣的。這是由於,不一樣位置做爲旋轉中心,旋轉後圖像的大小都是同樣,所不一樣的只是其位置。而在最後的一次變換中(圖像旋轉用了三次座標變換),統一的把座標原點移到了旋轉後圖像的左上角。這就至關於對圖像作了一次平移,把其位置也挪到了一塊兒,最後旋轉獲得的圖像也就同樣了。
若是,在旋轉結束後把座標原點不是移到旋轉後圖像的左上角,而是原圖像的左上角,會是怎麼一個情形呢?
就像上圖,圖像的部分區域會被截掉。固然,這時旋轉中心不一樣的話最終獲得的圖像也就不一樣了,被截掉的部分不相同。
組合變換就是把多種幾何放在一塊兒進行。上面推導了旋轉的變換公式,那麼組合變換也就不是很困難了,無非是多了幾個矩陣相乘。比較常見的組合變換:縮放+旋轉+平移,下面以此爲例推導下組合變換的公式。
分別獲得了三個變換矩陣,按照縮放、平移、旋轉的順序組合起來
組合變換的時候要注意順序,畢竟矩陣的左乘和右乘是不同的。
到如今,寫的最長的一篇文章。和上一篇放到一塊兒弄了差很少快一個周了,算法的實現除了旋轉有個幾回挫折外,其他的幾種變換都很順利。耗時間的主要是畫圖和數學公式,畫圖一直沒有找到合適的工具,不了了之;數學公式特地花了一天的時間學了Latex,而且找了些把Tex轉換爲HTML的工具,可是效果都不是很好,仍是粘貼圖片。沒有圖,有些東西只靠文字確實很難說的清楚,抽空得學習學習MATLAB畫圖了。