十、圖像的幾何變換——平移、鏡像、縮放、旋轉、仿射變換 OpenCV2:圖像的幾何變換,平移、鏡像、縮放、旋轉(1) OpenCV2:圖像的幾何變換,平移、鏡像、縮放、旋轉(2) 數字圖像處理筆記與

1.幾何變換的基本概念

  圖像幾何變換又稱爲圖像空間變換,它將一副圖像中的座標位置映射到另外一幅圖像中的新座標位置。咱們學習幾何變換就是肯定這種空間映射關係,以及映射過程當中的變化參數。圖像的幾何變換改變了像素的空間位置,創建一種原圖像像素與變換後圖像像素之間的映射關係,經過這種映射關係可以實現下面兩種計算:html

  1. 原圖像任意像素計算該像素在變換後圖像的座標位置
  2. 變換後圖像的任意像素在原圖像的座標位置

對於第一種計算,只要給出原圖像上的任意像素座標,都能經過對應的映射關係得到到該像素在變換後圖像的座標位置。將這種輸入圖像座標映射到輸出的過程稱爲「向前映射」。反過來,知道任意變換後圖像上的像素座標,計算其在原圖像的像素座標,將輸出圖像映射到輸入的過程稱爲「向後映射」。可是,在使用向前映射處理幾何變換時卻有一些不足,一般會產生兩個問題:映射不徹底,映射重疊ios

  1. 映射不徹底 
    輸入圖像的像素總數小於輸出圖像,這樣輸出圖像中的一些像素找不到在原圖像中的映射。 
    image 
    上圖只有(0,0),(0,2),(2,0),(2,2)四個座標根據映射關係在原圖像中找到了相對應的像素,其他的12個座標沒有有效值。
  2. 映射重疊 
    根據映射關係,輸入圖像的多個像素映射到輸出圖像的同一個像素上。 
    image 
    上圖左上角的四個像素(0,0),(0,1),(1,0),(1,1)都會映射到輸出圖像的(0,0)上,那麼(0,0)究竟取那個像素值呢?

要解決上述兩個問題可使用「向後映射」,使用輸出圖像的座標反過來推算改座標對應於原圖像中的座標位置。這樣,輸出圖像的每一個像素均可以經過映射關係在原圖像找到惟一對應的像素,而不會出現映射不徹底和映射重疊。因此,通常使用向後映射來處理圖像的幾何變換。從上面也能夠看出,向前映射之因此會出現問題,主要是因爲圖像像素的總數發生了變化,也就是圖像的大小改變了。在一些圖像大小不會發生變化的變換中,向前映射仍是頗有效的。web

2.圖像平移

圖像的平移變換就是將圖像全部的像素座標分別加上指定的水平偏移量和垂直偏移量。平移變換根據是否改變圖像大小分爲兩種,直接丟棄或者經過加目標圖像尺寸的方法使圖像可以包含這些點。算法

2.1平移變換原理

假設原來的像素的位置座標爲(x0,y0),通過平移量(△x,△y)後,座標變爲(x1,y1),以下所示:函數

                     

用數學式子表示能夠表示爲:post

x1 = x0 + △x,性能

 y1 = y0 + △y;學習

用矩陣表示爲:優化

                  

原本使用二維矩陣就能夠了的,可是爲了適應像素、拓展適應性,這裏使用三維的向量。ui

式子中,矩陣:

                

稱爲平移變換矩陣(因子),△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:這裏圖像變換的代碼以三通道圖像爲例,單通道的於此相似,代碼中沒有作處理。

示例:

#include "stdafx.h"

#include <iostream>
#include <opencv2\core\core.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>

using namespace std;
using namespace cv;

void 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 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];
		}
	}
}


int main()
{
	Mat srcImage, dstImage0, dstImage1, dstImage2;
	int xOffset, yOffset;		//x和y方向的平移量
	srcImage = imread("111.jpg");
	if (!srcImage.data)
	{
		cout << "讀入圖片錯誤!" << endl;
		return -1;
	}

	cout << "請輸入x方向和y方向的平移量:";
	cin >> xOffset >> yOffset;
	int rowNumber = srcImage.rows;
	int colNumber = srcImage.cols;

	translateTransform(srcImage, dstImage0, xOffset, yOffset);
	translateTransformSize(srcImage, dstImage1, xOffset, yOffset);

	imshow("原圖像", srcImage);
	imshow("不丟棄平移後的圖像", dstImage0);
	imshow("丟棄平移後的圖像", dstImage1);
	waitKey();
	return 0;
}

在輸入框輸入200,200後結果爲

 

 

3.圖像的鏡像變換

圖像的鏡像變換分爲兩種:水平鏡像和垂直鏡像。水平鏡像以圖像垂直中線爲軸,將圖像的像素進行對換,也就是將圖像的左半部和右半部對調。垂直鏡像則是以圖像的水平中線爲軸,將圖像的上半部分和下班部分對調。

3.1變換原理

    1. 水平變換

image向前映射 
其逆變換爲 
image向後映射

      2.垂直鏡像變換 
image
其逆變換爲 
image

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));
}
複製代碼

上面一行代碼是變換的核心代碼,從原圖像中取出第i行,並將其複製到目標圖像。

 示例:

#include "stdafx.h"

#include <iostream>
#include <opencv2\core\core.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>

using namespace std;
using namespace cv;
void 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 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));
}
int main()
{
	Mat srcImage, dstImage, dstImage1;;
	srcImage = imread("111.jpg");
	if (!srcImage.data)
	{
		cout << "讀入圖片錯誤!" << endl;
		return -1;
	}
	hMirrorTrans(srcImage, dstImage);
	vMirrorTrans(srcImage, dstImage1);
	imshow("原圖像", srcImage);
	imshow("水平鏡像後的圖像", dstImage);
	imshow("垂直鏡像後的圖像", dstImage1);
	waitKey();
	return 0;
}

 程序運行結果以下:

 

 

 

3.圖像縮放

圖像的縮放指的是將圖像的尺寸變小或變大的過程,也就是減小或增長原圖像數據的像素個數。簡單來講,就是經過增長或刪除像素點來改變圖像的尺寸。當圖像縮小時,圖像會變得更加清晰,當圖像放大時,圖像的質量會有所降低,所以須要進行插值處理。

3.1 縮放原理

設水平縮放係數爲sx,垂直縮放係數爲sy,(x0,y0)爲縮放前座標,(x,y)爲縮放後坐標,其縮放的座標映射關係: CodeCogsEqn

矩陣表示的形式爲:

CodeCogsEqn (3)

這是向前映射,在縮放的過程改變了圖像的大小,使用向前映射會出現映射重疊和映射不徹底的問題,因此這裏更關心的是向後映射,也就是輸出圖像經過向後映射關係找到其在原圖像中對應的像素。

向後映射關係:

image

 

3.2基於OpenCV的縮放實現

  在用前一篇文章講到利用resize函數的進行圖像的縮放操做,函數的原型爲:

resize( InputArray src, OutputArray dst,Size dsize, double fx=0, double fy=0,int interpolation=INTER_LINEAR );

這裏固然能夠用resize進行縮放,可是爲了更好的理解縮放原理,這裏利用向後映射進行圖像縮放,過程爲:

首先進行計算新圖像的大小,在這裏設newWidth和newHeight分別表示新圖像的寬度和高度,width和height表示原始圖像的寬度和高度,

在圖像縮放的時首先須要計算縮放後圖像的大小,設newWidth,newHeight爲縮放後的圖像的寬和高,width,height爲原圖像的寬度和高度,那麼有:

image

而後再進行枚舉新圖像每一個像素的座標,經過向後映射計算出該像素映射在原始圖像的座標位置,再進行獲取該像素的值。

根據上面公式可知,縮放後圖像的寬和高用原圖像寬和高和縮放因子相乘便可。

int rows = static_cast<int>(src.rows * xRatio + 0.5);
int cols = static_cast<int>(src.cols * yRatio + 0.5);

須要注意的是,在進行後向映射的過程當中可能會產生浮點數座標,可是數字圖像是以離散型整數存儲數據的,因此沒法獲得浮點數座標對應的像素值,這裏就須要進行插值算法計算座標是浮點型的像素值。這裏使用最鄰近插值和雙線性插值來處理。

3.3插值算法

0、什麼叫插值

數學的數值分析領域中,內插或稱插值(英語:interpolation)是一種經過已知的、離散的數據點,在範圍內推求新數據點的過程或方法。

    一組離散數據點在一個外延的插值。曲線中實際已知數據點是紅色的;鏈接它們的藍色曲線即爲插值。在一個函數裏面,自變量是離散有間隔的,插值就是往自變量的間隔之間插入新的自變量,而後求解新的自變量函數值。

      常見的插值算法有最鄰近插值法、雙線性插值法,雙三次插值法等。雙三次插值法因爲計算量較大,這裏不作詳細講解,有興趣的能夠看參考資料中的實現opencv中經常使用的三種插值算法

一、最鄰近插值

最近鄰域是三種插值之中最簡單的一種,原理就是選取距離插入的像素點(x+u, y+v)【注:x,y爲整數, u,v爲小數】最近的一個像素點,用它的像素點的灰度值代替插入的像素點。

image

 
void nearestIntertoplation(cv::Mat& src, cv::Mat& dst, const int rows, const int cols)
{
	//比例尺
	const double scale_row = static_cast<double>(src.rows) / rows;
	const double scale_col = static_cast<double>(src.rows) / cols;

	//擴展src到dst
	dst = cv::Mat(rows, cols, src.type());
	assert(src.channels() == 1 && dst.channels() == 1);

	for (int i = 0; i < rows; ++i)//dst的行
		for (int j = 0; j < cols; ++j)//dst的列
		{
			//求插值的四個點
			double y = (i + 0.5) * scale_row + 0.5;
			double x = (j + 0.5) * scale_col + 0.5;
			int x1 = static_cast<int>(x);//col對應x
			if (x1 >= (src.cols - 2)) x1 = src.cols - 2;//防止越界
			int x2 = x1 + 1;
			int y1 = static_cast<int>(y);//row對應y
			if (y1 >= (src.rows - 2))  y1 = src.rows - 2;
			int y2 = y1 + 1;
			//根據目標圖像的像素點(浮點座標)找到原始圖像中的4個像素點,取距離該像素點最近的一個原始像素值做爲該點的值。
			assert(0 < x2 && x2 < src.cols && 0 < y2 &&  y2 < src.rows);
			std::vector<double> dist(4);
			dist[0] = distance(x, y, x1, y1);
			dist[1] = distance(x, y, x2, y1);
			dist[2] = distance(x, y, x1, y2);
			dist[3] = distance(x, y, x2, y2);

			int min_val = dist[0];
			int min_index = 0;
			for (int i = 1; i < dist.size(); ++i)
				if (min_val > dist[i])
				{
					min_val = dist[i];
					min_index = i;
				}

			switch (min_index)
			{
			case 0:
				dst.at<uchar>(i, j) = src.at<uchar>(y1, x1);
				break;
			case 1:
				dst.at<uchar>(i, j) = src.at<uchar>(y1, x2);
				break;
			case 2:
				dst.at<uchar>(i, j) = src.at<uchar>(y2, x1);
				break;
			case 3:
				dst.at<uchar>(i, j) = src.at<uchar>(y2, x2);
				break;
			default:
				assert(false);
			}
		}
}

double distance(const double x1, const double y1, const double x2, const double y2)//兩點之間距離,這裏用歐式距離
{
	return (x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2);//只需比較大小,返回距離平方便可
}

最鄰近插值只須要對浮點座標「四捨五入」運算。可是在四捨五入的時候有可能使獲得的結果超過原圖像的邊界(只會比邊界大1),因此要進行下修正。

最鄰近插值幾乎沒有多餘的運算,速度至關快。可是這種鄰近取值的方法是很粗糙的,會形成圖像的馬賽克、鋸齒等現象。

雙線性插值

雙線性插值的精度要比最鄰近插值好不少,相對的其計算量也要大的多。雙線性插值的主要思想是計算出浮點座標像素近似值。那麼要如何計算浮點座標的近似值呢。一個浮點座標一定會被四個整數座標所包圍,將這個四個整數座標的像素值按照必定的比例混合就能夠求出浮點座標的像素值。混合比例爲距離浮點座標的距離。 雙線性插值使用浮點座標周圍四個像素的值按照必定的比例混合近似獲得浮點座標的像素值。

首先看看線性插值

 
 
 

下面經過一個例子進行理解:

假設要求座標爲(2.4,3)的像素值P,該點在(2,3)和(3,3)之間,以下圖 
image 

u和v分別是距離浮點座標最近兩個整數座標像素在浮點座標像素所佔的比例 

P(2.4,3) = u * P(2,3) + v * P(3,3),混合的比例是以距離爲依據的,那麼u = 0.4,v = 0.6。 

接下來看看二維中的雙線性插值

 

首先在x方向上面線性插值,獲得R二、R1

而後以R2,R1在y方向上面再次線性插值

 

一樣,經過一個實例進行理解

進行雙線性插值運算 
image
(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個座標近似求得,若是這個四個座標的像素值差異較大,插值後,會使得圖像在顏色分界較爲明顯的地方變得比較模糊。

OpenCV實現以下:

void bilinearIntertpolatioin(cv::Mat& src, cv::Mat& dst, const int rows, const int cols)
{
    //比例尺
    const double scale_row = static_cast<double>(src.rows) / rows;
    const double scale_col = static_cast<double>(src.rows) / cols;

    //擴展src到dst
    dst = cv::Mat(rows, cols, src.type());
    assert(src.channels() == 1 && dst.channels() == 1);
    
    for(int i = 0; i < rows; ++i)//dst的行
        for (int j = 0; j < cols; ++j)//dst的列
        {
            //求插值的四個點
            double y = (i + 0.5) * scale_row + 0.5;
            double x = (j + 0.5) * scale_col + 0.5;
            int x1 = static_cast<int>(x);//col對應x
            if (x1 >= (src.cols - 2)) x1 = src.cols - 2;//防止越界
            int x2 = x1 + 1;
            int y1 = static_cast<int>(y);//row對應y
            if (y1 >= (src.rows - 2))  y1 = src.rows - 2;
            int y2 = y1 + 1;

            assert(0 < x2 && x2 < src.cols && 0 < y2 &&  y2 < src.rows);
            //插值公式,參考維基百科矩陣相乘的公式https://zh.wikipedia.org/wiki/%E5%8F%8C%E7%BA%BF%E6%80%A7%E6%8F%92%E5%80%BC
    
            cv::Matx12d matx = { x2 - x, x - x1 };
            cv::Matx22d matf = { static_cast<double>(src.at<uchar>(y1, x1)), static_cast<double>(src.at<uchar>(y2, x1)),
                                 static_cast<double>(src.at<uchar>(y1, x2)), static_cast<double>(src.at<uchar>(y2, x2)) };
            cv::Matx21d maty = {
                y2 - y,
                y - y1
            };

            auto  val = (matx * matf * maty);
            dst.at<uchar>(i, j) = val(0,0);
        }

}

 3.3示例

  這裏用resize、最近鄰域插值和雙線性插值(這裏給出的兩種實現都是基於灰度圖的)

#include "stdafx.h"
#include<opencv2/opencv.hpp>
#include<opencv2\highgui\highgui.hpp>
#include<cassert>


double distance(const double x1, const double y1, const double x2, const double y2)//兩點之間距離,這裏用歐式距離
{
	return (x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2);//只需比較大小,返回距離平方便可
}
void nearestIntertoplation(cv::Mat& src, cv::Mat& dst, const int rows, const int cols)
{
	//比例尺
	const double scale_row = static_cast<double>(src.rows) / rows;
	const double scale_col = static_cast<double>(src.rows) / cols;

	//擴展src到dst
	dst = cv::Mat(rows, cols, src.type());
	assert(src.channels() == 1 && dst.channels() == 1);

	for (int i = 0; i < rows; ++i)//dst的行
		for (int j = 0; j < cols; ++j)//dst的列
		{
			//求插值的四個點
			double y = (i + 0.5) * scale_row + 0.5;
			double x = (j + 0.5) * scale_col + 0.5;
			int x1 = static_cast<int>(x);//col對應x
			if (x1 >= (src.cols - 2)) x1 = src.cols - 2;//防止越界
			int x2 = x1 + 1;
			int y1 = static_cast<int>(y);//row對應y
			if (y1 >= (src.rows - 2))  y1 = src.rows - 2;
			int y2 = y1 + 1;
			//根據目標圖像的像素點(浮點座標)找到原始圖像中的4個像素點,取距離該像素點最近的一個原始像素值做爲該點的值。
			assert(0 < x2 && x2 < src.cols && 0 < y2 &&  y2 < src.rows);
			std::vector<double> dist(4);
			dist[0] = distance(x, y, x1, y1);
			dist[1] = distance(x, y, x2, y1);
			dist[2] = distance(x, y, x1, y2);
			dist[3] = distance(x, y, x2, y2);

			int min_val = dist[0];
			int min_index = 0;
			for (int i = 1; i < dist.size(); ++i)
				if (min_val > dist[i])
				{
					min_val = dist[i];
					min_index = i;
				}

			switch (min_index)
			{
			case 0:
				dst.at<uchar>(i, j) = src.at<uchar>(y1, x1);
				break;
			case 1:
				dst.at<uchar>(i, j) = src.at<uchar>(y1, x2);
				break;
			case 2:
				dst.at<uchar>(i, j) = src.at<uchar>(y2, x1);
				break;
			case 3:
				dst.at<uchar>(i, j) = src.at<uchar>(y2, x2);
				break;
			default:
				assert(false);
			}
		}
}



void bilinearIntertpolatioin(cv::Mat& src, cv::Mat& dst, const int rows, const int cols)
{
	//比例尺
	const double scale_row = static_cast<double>(src.rows) / rows;
	const double scale_col = static_cast<double>(src.rows) / cols;

	//擴展src到dst
	dst = cv::Mat(rows, cols, src.type());
	assert(src.channels() == 1 && dst.channels() == 1);

	for (int i = 0; i < rows; ++i)//dst的行
		for (int j = 0; j < cols; ++j)//dst的列
		{
			//求插值的四個點
			double y = (i + 0.5) * scale_row + 0.5;
			double x = (j + 0.5) * scale_col + 0.5;
			int x1 = static_cast<int>(x);//col對應x
			if (x1 >= (src.cols - 2)) x1 = src.cols - 2;//防止越界
			int x2 = x1 + 1;
			int y1 = static_cast<int>(y);//row對應y
			if (y1 >= (src.rows - 2))  y1 = src.rows - 2;
			int y2 = y1 + 1;

			assert(0 < x2 && x2 < src.cols && 0 < y2 &&  y2 < src.rows);
			//插值公式,參考維基百科矩陣相乘的公式https://zh.wikipedia.org/wiki/%E5%8F%8C%E7%BA%BF%E6%80%A7%E6%8F%92%E5%80%BC

			cv::Matx12d matx = { x2 - x, x - x1 };
			cv::Matx22d matf = { static_cast<double>(src.at<uchar>(y1, x1)), static_cast<double>(src.at<uchar>(y2, x1)),
								 static_cast<double>(src.at<uchar>(y1, x2)), static_cast<double>(src.at<uchar>(y2, x2)) };
			cv::Matx21d maty = {
				y2 - y,
				y - y1
			};

			auto  val = (matx * matf * maty);
			dst.at<uchar>(i, j) = val(0, 0);
		}

}
int main()
{
	cv::Mat img = cv::imread("111.jpg", 0);
	if (img.empty()) return -1;
	cv::Mat dst,dst1,dst2;
	nearestIntertoplation(img, dst, 600, 600);
	bilinearIntertpolatioin(img, dst1, 600, 600);
	resize(img, dst2, dst1.size());
	cv::imshow("img", img);
	cv::imshow("最鄰近插值法", dst);
	cv::imshow("雙線性插值", dst1);
	cv::imshow("resize插值", dst2);
	cv::waitKey(0);
	return 0;
	return 0;
}//main

  

 

 

 

4.圖像旋轉

4.1旋轉原理

圖像的旋轉就是讓圖像按照某一點旋轉指定的角度。圖像旋轉後不會變形,可是其垂直對稱抽和水平對稱軸都會發生改變,旋轉後圖像的座標和原圖像座標之間的關係已不能經過簡單的加減乘法獲得,而須要經過一系列的複雜運算。並且圖像在旋轉後其寬度和高度都會發生變化,其座標原點會發生變化。

圖像所用的座標系不是經常使用的笛卡爾,其左上角是其座標原點,X軸沿着水平方向向右,Y軸沿着豎直方向向下。而在旋轉的過程通常使用旋轉中心爲座標原點的笛卡爾座標系,因此圖像旋轉的第一步就是座標系的變換。設旋轉中心爲(x0,y0),(x’,y’)是旋轉後的座標,(x,y)是旋轉後的座標,則座標變換以下:

image

矩陣表示爲:

image

在最終的實現中,經常使用到的是有縮放後的圖像經過映射關係找到其座標在原圖像中的相應位置,這就須要上述映射的逆變換

image

座標系變換到以旋轉中心爲原點後,接下來就要對圖像的座標進行變換。

image

上圖所示,將座標(x0,y0)順時針方向旋轉a,獲得(x1,y1)。

旋轉前有:

image

旋轉a後有:

image

矩陣的表示形式:

image

其逆變換:

image

因爲在旋轉的時候是以旋轉中心爲座標原點的,旋轉結束後還須要將座標原點移到圖像左上角,也就是還要進行一次變換。這裏須要注意的是,旋轉中心的座標(x0,y0)實在以原圖像的左上角爲座標原點的座標系中獲得,而在旋轉後因爲圖像的寬和高發生了變化,也就致使了旋轉後圖像的座標原點和旋轉前的發生了變換。

imageimage

上邊兩圖,能夠清晰的看到,旋轉先後圖像的左上角,也就是座標原點發生了變換。

在求圖像旋轉後左上角的座標前,先來看看旋轉後圖像的寬和高。從上圖能夠看出,旋轉後圖像的寬和高與原圖像的四個角旋轉後的位置有關。

設top爲旋轉後最高點的縱座標,down爲旋轉後最低點的縱座標,left爲旋轉後最左邊點的橫座標,right爲旋轉後最右邊點的橫座標。

旋轉後的寬和高爲newWidth,newHeight,則可獲得下面的關係:

image

也就很容易的得出旋轉後圖像左上角座標(left,top)(以旋轉中心爲原點的座標系)

故在旋轉完成後要將座標系轉換爲以圖像的左上角爲座標原點,可由下面變換關係獲得:

image

矩陣表示:

image

其逆變換:

image

綜合以上,也就是說原圖像的像素座標要通過三次的座標變換:

  1. 將座標原點由圖像的左上角變換到旋轉中心
  2. 以旋轉中心爲原點,圖像旋轉角度a
  3. 旋轉結束後,將座標原點變換到旋轉後圖像的左上角

能夠獲得下面的旋轉公式:(x’,y’)旋轉後的座標,(x,y)原座標,(x0,y0)旋轉中心,a旋轉的角度(順時針)

image

這種由輸入圖像經過映射獲得輸出圖像的座標,是向前映射。經常使用的向後映射是其逆運算

image

 

 

 

 

 

 

4.2基於OpenCV的實現

獲得了上述的旋轉公式,實現起來就不是很困難了.

Mat nearestNeighRotate(cv::Mat img, float angle)
{
	int len = (int)(sqrtf(pow(img.rows, 2) + pow(img.cols, 2)) + 0.5);

	Mat retMat = Mat::zeros(len, len, CV_8UC3);
	float anglePI = angle * CV_PI / 180;
	int xSm, ySm;

	for (int i = 0; i < retMat.rows; i++)
		for (int j = 0; j < retMat.cols; j++)
		{
			xSm = (int)((i - retMat.rows / 2)*cos(anglePI) - (j - retMat.cols / 2)*sin(anglePI) + 0.5);
			ySm = (int)((i - retMat.rows / 2)*sin(anglePI) + (j - retMat.cols / 2)*cos(anglePI) + 0.5);
			xSm += img.rows / 2;
			ySm += img.cols / 2;

			if (xSm >= img.rows || ySm >= img.cols || xSm <= 0 || ySm <= 0) {
				retMat.at<Vec3b>(i, j) = Vec3b(0, 0);
			}
			else {
				retMat.at<Vec3b>(i, j) = img.at<Vec3b>(xSm, ySm);
			}
		}

	return retMat;
}

示例

#include "stdafx.h"
#include<opencv2/opencv.hpp>
#include<opencv2\highgui\highgui.hpp>
#include<cassert>

using namespace std;
using namespace cv;


Mat nearestNeighRotate(cv::Mat img, float angle)
{
	int len = (int)(sqrtf(pow(img.rows, 2) + pow(img.cols, 2)) + 0.5);

	Mat retMat = Mat::zeros(len, len, CV_8UC3);
	float anglePI = angle * CV_PI / 180;
	int xSm, ySm;

	for (int i = 0; i < retMat.rows; i++)
		for (int j = 0; j < retMat.cols; j++)
		{
			xSm = (int)((i - retMat.rows / 2)*cos(anglePI) - (j - retMat.cols / 2)*sin(anglePI) + 0.5);
			ySm = (int)((i - retMat.rows / 2)*sin(anglePI) + (j - retMat.cols / 2)*cos(anglePI) + 0.5);
			xSm += img.rows / 2;
			ySm += img.cols / 2;

			if (xSm >= img.rows || ySm >= img.cols || xSm <= 0 || ySm <= 0) {
				retMat.at<Vec3b>(i, j) = Vec3b(0, 0);
			}
			else {
				retMat.at<Vec3b>(i, j) = img.at<Vec3b>(xSm, ySm);
			}
		}

	return retMat;
}

int main()
{
	Mat img = imread("111.jpg");
	Mat retImg;
	retImg = nearestNeighRotate(img, 45.f);
	imshow("img", img);
	imshow("nearNeigh", retImg);

	waitKey();
	cvDestroyAllWindows();
	return 0;
}

  運行結果以下:

 

 

2.3 仿射變換

  咱們除了本身寫相關函數外,OpenCV還提供了對應的仿射變換的API接口函數warpAffine,仿射變換是指在向量空間中進行一次線性變換(乘以一個矩陣)並加上一個平移(加上一個向量),變換爲另外一個向量空間的過程。在有限維的狀況下,每一個仿射變換能夠由一個矩陣A和一個向量b給出,它能夠寫做A和一個附加的列b。一個仿射變換對應於一個矩陣和一個向量的乘法,而仿射變換的複合對應於普通的矩陣乘法,只要加入一個額外的行到矩陣的底下,這一行所有是0除了最右邊是一個1,而列向量的底下要加上一個1. 

 實際上,仿射變換表明的是兩幅圖之間的關係,咱們一般使用2x3矩陣來表示仿射變換以下: 

這裏寫圖片描述

考慮到咱們要使用矩陣A和B對二維向量這裏寫圖片描述作變換,因此也能表示爲下列形式: 


這裏寫圖片描述 或 這裏寫圖片描述 

獲得以下結果: 


這裏寫圖片描述

應用圖像仿射變換矩陣,能夠獲得大部分的幾何變換結果,例如以前提到的平移變換等,根據平移變換矩陣能夠很容易的獲得實現平移功能的仿射變換矩陣,以下所示:

 

 

對於圖像縮放來講,設水平方向的縮放因子爲a,垂直方向縮放因子爲b,則用仿射矩陣實現圖縮放功能的仿射矩陣爲:

 

 

而對於圖像旋轉來講,設旋轉角度爲θ,利用仿射變換實現圖像旋轉操做的仿射矩陣爲:

 

而對於較爲特殊的斜切變換,一樣的,設斜切的角度爲θ,則仿射矩陣爲:

須要注意的是,在OpenCV中使用仿射變換函數時,一般會先計算一個仿射變換矩陣,以此來得到仿射變換矩陣,爲了實現這個功能,經常使用getRotationMatrix2D()函數用來計算二維旋轉矩陣,這個變換會將旋轉中心映射到它自身。這裏給出它的函數聲明: 

Mat getRotationMatrix2D( Point2f center, double angle, double scale );

  這個函數中有三個參數,第一個參數是Point2f類型的center,也就是原圖像的旋轉中心;第二個參數是double 類型的angle,也就是咱們說的旋轉角度,值得一提的是,當angle的值爲正時,表示的是逆時針旋轉,當angle的值爲負時,表示的是順時針旋轉。第三個參數scale表示的是縮放係數,在這個函數計算的是下面這個矩陣:

其中

 

獲得仿射變換矩陣後,便可調用仿射函數,仿射映射函數聲明爲:

void cv::warpAffine     (  
        InputArray      src,
        OutputArray     dst,
        InputArray      M,
        Size    dsize,
        int     flags = INTER_LINEAR, int borderMode = BORDER_CONSTANT, const Scalar & borderValue = Scalar() )

參數解釋 
. src: 輸入圖像 
. dst: 輸出圖像,尺寸由dsize指定,圖像類型與原圖像一致 
. M: 2X3的變換矩陣 
. dsize: 指定圖像輸出尺寸 
. flags: 插值算法標識符,有默認值INTER_LINEAR,若是插值算法爲WARP_INVERSE_MAP, warpAffine函數使用以下矩陣進行圖像轉換 
經常使用的插值算法以下:

 

. borderMode: 邊界像素模式,有默認值BORDER_CONSTANT 
. borderValue: 邊界取值,有默認值Scalar()即0

 

 示例:

 

#include "stdafx.h"
#include<opencv2/opencv.hpp>
#include<opencv2\highgui\highgui.hpp>
#include<cassert>
#include <iostream>
#include <opencv2\core\core.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>

using namespace std;
using namespace cv;

int main()
{
	Mat srcImage, dstImage;
	srcImage = imread("111.jpg");
	if (!srcImage.data)
	{
		cout << "讀入圖片有誤!" << endl;
		return -1;
	}
	imshow("原圖像", srcImage);

	dstImage.create(srcImage.size(), srcImage.type());
	double degree;
	cout << "請輸入旋轉角度:";
	cin >> degree;

	double a = sin(degree * CV_PI / 180);
	double b = cos(degree * CV_PI / 180);

	int width = srcImage.cols;
	int height = srcImage.rows;

	int rotate_width = int(height * fabs(a) + width * fabs(b));
	int rotate_height = int(width * fabs(a) + height * fabs(b));

	Point center = Point(srcImage.cols / 2, srcImage.rows / 2);

	Mat map_matrix = getRotationMatrix2D(center, degree, 1.0);
	map_matrix.at<double>(0, 2) += (rotate_width - width) / 2;     // 修改座標偏移
	map_matrix.at<double>(1, 2) += (rotate_height - height) / 2;   // 修改座標偏移

	warpAffine(srcImage, dstImage, map_matrix, { rotate_width, rotate_height }, CV_INTER_CUBIC);

	imshow("旋轉後的圖像", dstImage);


	waitKey();
	return 0;
}

 

 

 至此,圖像的幾何變換基本就完畢了,這裏仍是推薦使用OpenCV官方提供的函數API來進行基本的幾何變換,可是對於學習來講,知道其原理仍是十分重要的,因此這裏參考其餘人的博客資料集合了一篇完整的解讀。但願可以對你有點幫助。

 

 

 

 

 

參考資料

數字圖像處理與機器視覺Visual C與Matlab實現

幾何圖像變換

OpenCV2:圖像的幾何變換,平移、鏡像、縮放、旋轉(1)

OpenCV2:圖像的幾何變換,平移、鏡像、縮放、旋轉(2)

數字圖像處理筆記與體會(三)——圖像的幾何變換

【OpenCV圖像處理】4、圖像的幾何變換(上)

【OpenCV圖像處理】5、圖像的幾何變換(下)

OpenCV中resize函數五種插值算法的實現過程

OpenCV ——雙線性插值(Bilinear interpolation)

雙線性插值算法進行圖像縮放及性能效果優化

雙線性插值原理及其實現--基於OpenCV實現

實現opencv中經常使用的三種插值算法

opencv學習(三十五)之仿射變換warpAffine

相關文章
相關標籤/搜索