圖像的幾何變換是在不改變圖像內容的前提下對圖像像素的進行空間幾何變換,主要包括了圖像的平移變換、鏡像變換、縮放和旋轉等。本文首先介紹了圖像幾何變換的一些基本概念,然后再OpenCV2下實現了圖像的平移變換、鏡像變換、縮放以及旋轉,最后介紹幾何的組合變換(平移+縮放+旋轉)。
1.幾何變換的基本概念
1.1 坐標映射關系
圖像的幾何變換改變了像素的空間位置,建立一種原圖像像素與變換后圖像像素之間的映射關系,通過這種映射關系能夠實現下面兩種計算:
- 原圖像任意像素計算該像素在變換后圖像的坐標位置
- 變換后圖像的任意像素在原圖像的坐標位置
對於第一種計算,只要給出原圖像上的任意像素坐標,都能通過對應的映射關系獲得到該像素在變換后圖像的坐標位置。將這種輸入圖像坐標映射到輸出的過程稱為“向前映射”。反過來,知道任意變換后圖像上的像素坐標,計算其在原圖像的像素坐標,將輸出圖像映射到輸入的過程稱為“向后映射”。但是,在使用向前映射處理幾何變換時卻有一些不足,通常會產生兩個問題:映射不完全,映射重疊
- 映射不完全
輸入圖像的像素總數小於輸出圖像,這樣輸出圖像中的一些像素找不到在原圖像中的映射。
上圖只有(0,0),(0,2),(2,0),(2,2)四個坐標根據映射關系在原圖像中找到了相對應的像素,其余的12個坐標沒有有效值。 - 映射重疊
根據映射關系,輸入圖像的多個像素映射到輸出圖像的同一個像素上。
上圖左上角的四個像素(0,0),(0,1),(1,0),(1,1)都會映射到輸出圖像的(0,0)上,那么(0,0)究竟取那個像素值呢?
要解決上述兩個問題可以使用“向后映射”,使用輸出圖像的坐標反過來推算改坐標對應於原圖像中的坐標位置。這樣,輸出圖像的每個像素都可以通過映射關系在原圖像找到唯一對應的像素,而不會出現映射不完全和映射重疊。所以,一般使用向后映射來處理圖像的幾何變換。從上面也可以看出,向前映射之所以會出現問題,主要是由於圖像像素的總數發生了變化,也就是圖像的大小改變了。在一些圖像大小不會發生變化的變換中,向前映射還是很有效的。
1.2.插值算法
對於數字圖像而言,像素的坐標是離散型非負整數,但是在進行變換的過程中有可能產生浮點坐標值。例如,原圖像坐標(9,9)在縮小一倍時會變成(4.5,4.5),這顯然是一個無效的坐標。插值算法就是用來處理這些浮點坐標的。常見的插值算法有最鄰近插值法、雙線性插值法,二次立方插值法,三次立方插值法等。本文主要介紹最鄰近插值和雙線性插值,其他一些高階的插值算法,以后再做研究。
- 最鄰近插值
也被稱為零階插值法,最簡單插值算法,當然效果也是最差的。它的思想相當簡單,就是四舍五入,浮點坐標的像素值等於距離該點最近的輸入圖像的像素值。
上面的代碼可以求得(x,y)的最鄰近插值坐標(u,v)。
最鄰近插值幾乎沒有多余的運算,速度相當快。但是這種鄰近取值的方法是很粗糙的,會造成圖像的馬賽克、鋸齒等現象。 - 雙線性插值
它的插值效果比最鄰近插值要好很多,相應的計算速度也要慢上不少。雙線性插值的主要思想是計算出浮點坐標像素近似值。那么要如何計算浮點坐標的近似值呢。一個浮點坐標必定會被四個整數坐標所包圍,將這個四個整數坐標的像素值按照一定的比例混合就可以求出浮點坐標的像素值。混合比例為距離浮點坐標的距離。
假設要求坐標為(2.4,3)的像素值P,該點在(2,3)和(3,3)之間,如下圖u和v分別是距離浮點坐標最近兩個整數坐標像素在浮點坐標像素所占的比例
P(2.4,3) = u * P(2,3) + v * P(3,3),混合的比例是以距離為依據的,那么u = 0.4,v = 0.6。
上面是只在一條直線的插值,稱為線性插值。雙線性插值就是分別在X軸和Y軸做線性插值運算。
下面利用三次的線性插值進行雙線性插值運算
(2.4,3)的像素值 F1 = m * T1 + (1 – m) * T2
(2.4,4)的像素值 F2 = m * T3 + (1 – m ) * T4
(2.4,3.5)的像素值 F = n * F1 + (1 – n) * F2
這樣就可以求得浮點坐標(2.4,3.5)的像素值了。
求浮點坐標像素F,設該浮點坐標周圍的4個像素值分別為T1,T2,T3,T4,並且浮點坐標距離其左上角的橫坐標的差為m,縱坐標的差為n。
F1 = m * T1 + (1 – m) * T2
F2 = m * T3 + (1 – m) *T4
F = n * F1 + (1 – n) * F2
上面就是雙線性插值的基本公式,可以看出,計算每個像素像素值需要進行6次浮點運算。而且,由於浮點坐標有4個坐標近似求得,如果這個四個坐標的像素值差別較大,插值后,會使得圖像在顏色分界較為明顯的地方變得比較模糊。
2.圖像平移
圖像的平移變換就是將圖像所有的像素坐標分別加上指定的水平偏移量和垂直偏移量。平移變換根據是否改變圖像大小分為兩種
左邊平移圖像的大小發生了,在保證圖像平移的同時,也保存了完整的圖像信息。右邊的平移圖像大小沒有變化,故圖像右下角的部分被截除了。
2.1平移變換原理
設dx為水平偏移量,dy為垂直偏移量,(x0,y0)為原圖像坐標,(x,y)為變換后圖像坐標,則平移變換的坐標映射為
這是向前映射,即將原圖像的坐標映射到變換后的圖像上。
其逆變換為 ,向后映射,即將變換后的圖像坐標映射到原圖像上。在圖像的幾何變換中,一般使用向后映射。
2.2 基於OpenCV的實現
圖像的平移變換實現還是很簡單的,這里不再贅述.
平移后圖像的大小不變
void GeometricTrans::translateTransform(cv::Mat const& src, cv::Mat& dst, int dx, int dy) { CV_Assert(src.depth() == CV_8U); const int rows = src.rows; const int cols = src.cols; dst.create(rows, cols, src.type()); Vec3b *p; for (int i = 0; i < rows; i++) { p = dst.ptr<Vec3b>(i); for (int j = 0; j < cols; j++) { //平移后坐標映射到原圖像 int x = j - dx; int y = i - dy; //保證映射后的坐標在原圖像范圍內 if (x >= 0 && y >= 0 && x < cols && y < rows) p[j] = src.ptr<Vec3b>(y)[x]; } } }
平移后圖像的大小變化
void GeometricTrans::translateTransformSize(cv::Mat const& src, cv::Mat& dst, int dx, int dy) { CV_Assert(src.depth() == CV_8U); const int rows = src.rows + abs(dy); //輸出圖像的大小 const int cols = src.cols + abs(dx); dst.create(rows, cols, src.type()); Vec3b *p; for (int i = 0; i < rows; i++) { p = dst.ptr<Vec3b>(i); for (int j = 0; j < cols; j++) { int x = j - dx; int y = i - dy; if (x >= 0 && y >= 0 && x < src.cols && y < src.rows) p[j] = src.ptr<Vec3b>(y)[x]; } } }
ps:這里圖像變換的代碼以三通道圖像為例,單通道的於此類似,代碼中沒有做處理。
3.圖像的鏡像變換
圖像的鏡像變換分為兩種:水平鏡像和垂直鏡像。水平鏡像以圖像垂直中線為軸,將圖像的像素進行對換,也就是將圖像的左半部和右半部對調。垂直鏡像則是以圖像的水平中線為軸,將圖像的上半部分和下班部分對調。效果如下:
3.1變換原理
設圖像的寬度為width,長度為height。(x,y)為變換后的坐標,(x0,y0)為原圖像的坐標
3.2基於OpenCV的實現
水平鏡像的實現
void GeometricTrans::hMirrorTrans(const Mat &src, Mat &dst) { CV_Assert(src.depth() == CV_8U); dst.create(src.rows, src.cols, src.type()); int rows = src.rows; int cols = src.cols; switch (src.channels()) { case 1: const uchar *origal; uchar *p; for (int i = 0; i < rows; i++){ origal = src.ptr<uchar>(i); p = dst.ptr<uchar>(i); for (int j = 0; j < cols; j++){ p[j] = origal[cols - 1 - j]; } } break; case 3: const Vec3b *origal3; Vec3b *p3; for (int i = 0; i < rows; i++) { origal3 = src.ptr<Vec3b>(i); p3 = dst.ptr<Vec3b>(i); for(int j = 0; j < cols; j++){ p3[j] = origal3[cols - 1 - j]; } } break; default: break; } }
分別對三通道圖像和單通道圖像做了處理,由於比較類似以后的代碼只處理三通道圖像,不再做特別說明。
在水平鏡像變換時,遍歷了整個圖像,然后根據映射關系對每個像素都做了處理。實際上,水平鏡像變換就是將圖像坐標的列換到右邊,右邊的列換到左邊,是可以以列為單位做變換的。同樣垂直鏡像變換也如此,可以以行為單位進行變換。
垂直鏡像變換
void GeometricTrans::vMirrorTrans(const Mat &src, Mat &dst) { CV_Assert(src.depth() == CV_8U); dst.create(src.rows, src.cols, src.type()); int rows = src.rows; for (int i = 0; i < rows; i++) src.row(rows - i - 1).copyTo(dst.row(i)); }
src.row(rows - i - 1).copyTo(dst.row(i));
上面一行代碼是變換的核心代碼,從原圖像中取出第i行,並將其復制到目標圖像。
頂不住了啊,寫理論部分太痛苦了啊,明天繼續幾何變換的后續幾種:轉置、縮放、旋轉以及組合變換。