OpenCV学习笔记15_仿射变换与透视变换

一、仿射变换

什么是仿射变换

仿射变换也称仿射投影,是指几何中,对一个向量空间进行线性变换并接上一个平移,变换为另一个向量空间。所以,仿射变换其实也就是在讲如何来进行两个向量空间的变换。

对于一幅图像,可以看作很多个坐标的集合,每个坐标可以代表一个向量,由此可以将图像看作向量集合,那么在二维坐标系上:

公式推导

假设存在一个向量空间V:

V = ( a , b ) V=\left(a,b\right) V=(a,b)
存在另一个空间U:

U = ( x , y ) U=\left(x,y\right) U=(x,y)
从空间V转换到空间U可以表示为:
U = w V + C U=wV+C U=wV+C
具体可以展开为:
x = w 00 a + w 01 b + c 1 x=w_{00}a+w_{01}b+c_1 x=w00a+w01b+c1
y = w 10 a + w 11 b + c 2 y=w_{10}a+w_{11}b+c_2 y=w10a+w11b+c2
转换为矩阵形式则有:
[ x y 1 ] = [ w 00 w 01 c 1 w 10 w 11 c 2 0 0 1 ] [ a b 1 ] \left[\begin{array}{l} x \\ y \\ 1 \end{array}\right]=\left[\begin{array}{ccc} w_{00} & w_{01} & c_{1} \\ w_{10} & w_{11} & c_{2} \\ 0 & 0 & 1 \end{array}\right]\left[\begin{array}{l} a \\ b \\ 1 \end{array}\right] xy1=w00w100w01w110c1c21ab1
进一步缩写为:
[ x y 1 ] = W [ a b 1 ] \left[\begin{array}{l} x \\ y \\ 1\end{array}\right]=W\left[\begin{array}{l} a \\ b \\ 1 \end{array}\right] xy1=Wab1
其中W称为转换矩阵
在网上有一张很好的总结图:在这里插入图片描述
上图为转换矩阵转置

第一步为找矩阵空间,第二步为插值;

插值方法:双线性插值

因为双线性插值需要知道插值点对应的四个原始坐标,所以变换矩阵需要变为逆矩阵,变换矩阵乘以原始坐标点可得到变换之后的坐标点,当前插值点乘以变换矩阵的逆矩阵可得到原始点的坐标,根据双线性插值算法进而可以得到最近四领域的插值。
操作为:
将变换之后的图像(X,Y)进行逆变换映射到原始的坐标 ( x , y ) (x,y) (x,y);
( x , y ) (x,y) (xy)改写为 ( x ′ + u , y ′ + v ) (x^\prime+u,y^\prime+v) (x+u,y+v)的形式,表示将x与y中的整数和小数分开表示,uv分别代表小数部分。
根据权重比率的思想得到计算公式:
( X , Y ) = ( 1 − u ) ∙ ( 1 − v ) ∙ ( x , y ) + ( u − 1 ) ∙ v ∙ ( x , y + 1 ) (X,Y)=(1-u)\bullet(1-v)\bullet(x,y)+(u-1)\bullet v\bullet(x,y+1) (X,Y)=(1u)(1v)(x,y)+(u1)v(x,y+1) + u ∙ ( v − 1 ) ∙ ( x + 1 , y ) + ( u ∙ v ) ∙ ( x + 1 , y + 1 ) +u\bullet(v-1)\bullet(x+1,y)+(u\bullet v)\bullet(x+1,y+1) +u(v1)(x+1,y)+(uv)(x+1,y+1)

结合图例:
在这里插入图片描述
注意这是笛卡尔坐标系,图像坐标系需要改变一下转换矩阵。
公式的推导很简单,不详细的说了,多个变换可以结合使用。

OpenCV的函数为:

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

参数解释:

InputArray src,输入变换前图像
OutputArray dst,输出图像
InputArray M, 仿射变换矩阵
Size dsize,输出的图像大小
int flags = INTER_LINEAR,插值方式,默认为双线性插值
int borderMode = BORDER_CONSTANT, 边界像素模式
const Scalar& borderValue = Scalar();边缘默认值

二、透视变换

要理解透视变换之前需要理解什么是透视:

在绘画上需要在二维的纸上,画出三维的物体,欺骗人的眼睛;
归结起来就是四个字,“近大远小”
在这里插入图片描述
在这里插入图片描述

仿射变换是透视变换的特例,区别在于
仿射变换保持了二维图形的“平直性”(直线经过变换之后依然是直线)和“平行性”(二维图形之间的相对位置关系保持不变,平行线依然是平行线,且直线上点的位置顺序不变)。

透射变换将图像映射到三维空间中,再反向映射到二维平面。
我们知道,确定一个二维空间至少需要3个点(0,x,y),确定一个三维空间至少要四个点(0,x,y,z),一个三维的物体从不同的角度看能看到不同的二维图像,根据这个理论,一个二维的图像实际上是一个三维的物体在某种透视下产生的,前面讲到:
[ x y 1 ] = [ w 00 w 01 c 1 w 10 w 11 c 2 0 0 1 ] [ a b 1 ] \left[\begin{array}{l} x \\ y \\ 1 \end{array}\right]=\left[\begin{array}{ccc} w_{00} & w_{01} & c_{1} \\ w_{10} & w_{11} & c_{2} \\ 0 & 0 & 1 \end{array}\right]\left[\begin{array}{l} a \\ b \\ 1 \end{array}\right] xy1=w00w100w01w110c1c21ab1
当图像在三维变化的之后需要加上一个z变量(假设变换前二维空间称为z=1空间):
[ x y z ] = [ w 00 w 01 c 1 w 10 w 11 c 2 w 20 w 21 1 ] [ a b 1 ] \left[\begin{array}{l} x \\ y \\ z \end{array}\right]=\left[\begin{array}{ccc} w_{00} & w_{01} & c_{1} \\ w_{10} & w_{11} & c_{2} \\ w_{20} & w_{21} & 1 \end{array}\right]\left[\begin{array}{l} a \\ b \\ 1 \end{array}\right] xyz=w00w10w20w01w11w21c1c21ab1

一共8个未知数,需要8个方程组:
(因为在三维空间中,z坐标可以用x与y坐标线性表出)
Z = m x + n y Z=mx+ny Z=mx+ny
那么在z=1空间就表示为
1 = q x + d y 1=qx+dy 1=qx+dy

所以每个独立点只能对应两个方程组:
由于:
x = w 00 a + w 01 b + c 1 x=w_{00}a+w_{01}b+c_1 x=w00a+w01b+c1
y = w 10 a + w 11 b + c 2 y=w_{10}a+w_{11}b+c_2 y=w10a+w11b+c2
则有

z = w 20 a + w 21 b + c 3 z=w_{20}a+w_{21}b+c_3 z=w20a+w21b+c3
Z = m x + n y Z=mx+ny Z=mx+ny
当前(x,y)是Z=z时的二维空间坐标,下一步需要换算成Z=1时候的坐标:
则有
Z z = 1 \frac{Z}{z}=1 zZ=1
所以
Z z = m x + n y z = 1 \frac{Z}{z}=\frac{mx+ny}{z}=1 zZ=zmx+ny=1

X = x z X=\frac{x}{z} X=zx
Y = y z Y=\frac{y}{z} Y=zy
最后

X = w 00 a + w 01 b + c 1 w 20 a + w 21 b + c 3 X=\frac{w_{00}a+w_{01}b+c_1}{w_{20}a+w_{21}b+c_3} X=w20a+w21b+c3w00a+w01b+c1
Y = w 10 a + w 11 b + c 2 w 20 a + w 21 b + c 3 Y=\frac{w_{10}a+w_{11}b+c_2}{w_{20}a+w_{21}b+c_3} Y=w20a+w21b+c3w10a+w11b+c2
对应四个点就能解出所有未知数:

例子
四个顶点: a ( 0 , 0 ) , b ( 2 , 0 ) , c ( 0 , 2 ) , d ( 2 , 2 ) a(0,0),b(2,0),c(0,2),d(2,2) a(0,0),b(2,0),c(0,2),d(2,2)在z=1时候为正方形;
转换坐标: A ( 0 , 0 ) , B ( 2 , 0 ) , C ( 0 , 1 ) , D ( 1 , 2 ) A(0,0),B(2,0),C(0,1),D(1,2) A(0,0),B(2,0),C(0,1),D(1,2)在z=1时候是一个不规则四边形;
通过计算得到转换矩阵为: [ 1 3 0 0 0 2 3 0 − 1 3 1 6 1 ] \left[\begin{matrix}\frac{1}{3}&0&0\\0&\frac{2}{3}&0\\-\frac{1}{3}&\frac{1}{6}&1\\\end{matrix}\right] 3103103261001;
其中z(也就是空间映射之后)的图像为
在这里插入图片描述

OpenCV中的函数为:

CV_EXPORTS_W void warpPerspective( InputArray src, OutputArray dst,
                                   InputArray M, Size dsize,
                                   int flags = INTER_LINEAR,
                                   int borderMode = BORDER_CONSTANT,
                                   const Scalar& borderValue = Scalar());

其中InputArray M 为变换矩阵,需要配合使用getPerspectiveTransform函数得到;

三、代码

#include <opencv.hpp>
using namespace std;
using namespace cv;

#define UP_2_DOWN 0
#define LEFT_2_RIGHT 1
#define UP_LEFT_2_DOWN_RIGHT 2

//翻转
void turn(Mat src , Mat &dst, int type = 0);

//大小变换
void re_size(Mat src , Mat &dst , double x_d,double y_d);

//旋转变化
void revolve(Mat src, Mat &dst, double theta,double x_d, double y_d);

//偏移变换
void Offset(Mat src, Mat &dst, double k_x, double k_y, double x_d, double y_d);

//双线性插值
void Bilinear(Mat src , Mat &dst, Mat_<double> &dst_coordinate);




void main()
{
    
    
	Mat src =  imread("test.jpg");
	Mat src_2 = imread("warp.jpg");
	Mat dst,dst_2;
	Mat dst_turn(src.rows,src.cols,CV_8UC3);
	Mat dst_size(src.rows, src.cols, CV_8UC3);
	Mat dst_revolve(src.rows, src.cols, CV_8UC3);
	Mat dst_offset(src.rows, src.cols, CV_8UC3);
翻转
//	turn(src, dst_turn,UP_LEFT_2_DOWN_RIGHT);
//
大小变换
//	re_size(src, dst_size, 0.8, 0.5);
//
旋转变换
//	revolve(src, dst_revolve, 60, 0.5, 0.5);
//
偏移变换(不能全为一)
//	Offset(src,dst_offset,1,0,0.5,0.5);
//

	//opencv中的函数实现
	Point2f p1[3] = {
    
    Point2f(0,0),Point2f(0,src.rows),Point2f(src.cols,0)};
	Point2f p2[3] = {
    
     Point2f(0,0),Point2f(0,src.rows/3),Point2f(src.cols/3,0) };
// 	Point2f *p2 = new Point2f;
// 	p2[0] = Point2f(0, 0);
// 	p2[1] = Point2f(0, src.rows / 3);
// 	p2[2] = Point2f(src.cols/3,0);
	Mat M1 = getAffineTransform(p1,p2);
	warpAffine(src,dst,M1,Size(src.cols, src.rows));


	//透视变换
	Point2f p3[4] = {
    
     Point2f(97,2198),Point2f(658,1341),Point2f(1803,2899),Point2f(2050,1946) };

	for (size_t i = 0; i < 4; i++)
	{
    
    
		circle(src_2, p3[i],20, Scalar(0, 255, 0),FILLED);
	}


	Point2f p4[4] = {
    
     Point2f(0,0),Point2f(1600,-40),Point2f(0,1780),Point2f(1600,1750) };
	Mat M2 = getPerspectiveTransform(p3, p4);
	warpPerspective(src_2, dst_2, M2,Size(1500,1700));



	imshow("src", src);
	imshow("dst_turn", dst_turn);
	imshow("dst_size", dst_size);
	imshow("dst_offset", dst_offset);
	imshow("dst_revolve", dst_revolve);
	imshow("dst", dst);
	imshow("dst_2", dst_2);
	waitKey(0);





}

//翻转
void turn(Mat src, Mat &dst, int type)
{
    
    
	Mat_<double> dst_coordinate;
	//对称变换
	if (type == UP_2_DOWN)
	{
    
    
		 dst_coordinate = (Mat_<double>(3, 3) << 1.0, 0.0, 0.0, 0.0, -1.0, 1.0*(src.rows - 1), 0.0, 0.0, 1);
	}
	
	if (type == LEFT_2_RIGHT)
	{
    
    
		dst_coordinate = (Mat_<double>(3, 3) << -1.0, 0.0, 1.0*(src.cols - 1), 0.0, 1.0,0.0, 0.0, 0.0,1);
	}

	if (type == UP_LEFT_2_DOWN_RIGHT)
	{
    
    
		dst_coordinate = (Mat_<double>(3, 3) << -1.0, 0.0, 1.0*(src.cols - 1), 0.0, -1.0, 1.0*(src.rows - 1), 0.0, 0.0,1);
	}

	/*
	1.0,  0.0, 0.0
	0.0, -1.0, src.rows - 1
	0.0,  0.0, 0.0	
	*/
	//双线性插值

	Bilinear(src, dst, dst_coordinate);

}

//大小变化
void re_size(Mat src, Mat & dst, double x_d,  double y_d)
{
    
    

	Mat_<double> dst_coordinate = (Mat_<double>(3, 3) << x_d, 0, 0, 0, y_d, 0, 0, 0, 1);


	Bilinear(src, dst, dst_coordinate);

}

//图像旋转(先旋转,再变化大小)
void revolve(Mat src, Mat & dst, double theta_, double x_d, double y_d)
{
    
    
	double theta = (double)(theta_ / 180.0)*CV_PI;
#if 0
	//第一种方法
	/*
	分为两步:
	1、直接带公式旋转变换
	2、直接带公式大小变换
	3、合为一体
	
	*/
	Mat_<double> dst_coordinate_1 = (Mat_<double>(3, 3) << x_d, 0, x_d*0.5*src.cols, 0, y_d, y_d*0.5*src.rows, 0, 0, 1);
	Mat_<double> dst_coordinate_2 = (Mat_<double>(3, 3) <<
		cos(theta), -sin(theta), ((1.0 - cos(theta))*0.5*(src.cols - 1) + 0.5*(src.rows - 1.0)*sin(theta)),
		sin(theta), cos(theta), ((1.0 - cos(theta))*0.5*(src.rows - 1) - 0.5*(src.cols - 1.0)*sin(theta)),
		0, 0, 1);
	Mat_<double> dst_coordinate = dst_coordinate_2*dst_coordinate_1;
#endif


	//坐标原点变化到中点;
    Mat_<double> dst_coordinate_1 = (Mat_<double>(3, 3) << 1, 0, 0.5*src.cols , 0, 1, 0.5*src.rows, 0, 0, 1);

	//旋转变化
	Mat_<double> dst_coordinate_2 = (Mat_<double>(3, 3) <<cos(theta), -sin(theta), 0,sin(theta), cos(theta), 0,0, 0, 1);
	//还原坐标系到原坐标系
	Mat_<double> dst_coordinate_3 = (Mat_<double>(3, 3) << 1, 0, -0.5*src.cols, 0, 1, -0.5*src.rows, 0, 0, 1);

	//图像大小变换
	Mat_<double> dst_coordinate_4 = (Mat_<double>(3, 3) << x_d, 0, 0, 0, y_d, 0, 0, 0, 1);

	//第一部分:
	/*
	第一步:坐标系变化,将坐标系下移(针对图像坐标系)
	第二步:旋转变化(针对图像坐标系)
	第三步:还原坐标系
	*/
	Mat_<double> dst_coordinate_a = dst_coordinate_1*dst_coordinate_2*dst_coordinate_3;

	//第二部分:
	/*
	第一步:坐标系变化,将坐标系下移(针对图像坐标系)
	第二步:大小变化(针对图像坐标系)
	第三步:还原坐标系
	*/
	Mat_<double> dst_coordinate_b = dst_coordinate_1*dst_coordinate_4*dst_coordinate_3;

	//第三部分:
	/*
	
	将两个操作相乘合为一个操作

	*/
	Mat_<double> dst_coordinate = dst_coordinate_a*dst_coordinate_b;

	//最后双线性插值
	Bilinear(src, dst, dst_coordinate);

}

//偏移变换
void Offset(Mat src, Mat & dst, double k_x, double k_y, double x_d, double y_d)
{
    
    
	//k_x为水平偏移距离与原图像rows的比值
	//k_y为垂直偏移距离与原图像cols的比值
	Mat_<double> dst_coordinate = (Mat_<double>(3, 3) << 1, k_x, 0, k_y, 1, 0, 0, 0, 1);

	Mat_<double> re_size = (Mat_<double>(3, 3) << x_d, 0, 0, 0, y_d, 0, 0, 0, 1);
	//与大小变换组合
	Mat_<double> coordinate = re_size*dst_coordinate;

	//最后双线性插值
	Bilinear(src, dst, coordinate);


}



//双线性插值
void Bilinear(Mat src, Mat &dst , Mat_<double> &dst_coordinate)
{
    
    
	//需要映射插值,所以取逆  
	Mat_<double> coordinate ;
	cv::invert(dst_coordinate, coordinate, DECOMP_LU);
	for (int i = 0; i < src.rows; i++)
	{
    
    
		for (int j = 0; j < src.cols; j++)
		{
    
    
			Mat Variable_dst = (Mat_<double>(3, 1) << j, i, 1);//注意Mat_模板类的定义为(rows,cols) 3行1列
			Mat Variable_src = coordinate * Variable_dst;

			double cols = Variable_src.at<double>(0, 0);
			double rows = Variable_src.at<double>(1, 0);//注意at也是(rows,cols) 

			int x1, x2, y1, y2;
			x1 = floor(cols);
			x2 = ceil(cols);
			y1 = floor(rows);
			y2 = ceil(rows);

			double w1 = (double)cols - x1;
			double w2 = (double)rows - y1;


			if (cols >=0 && rows >= 0 && cols <=src.cols-1 && rows <= src.rows-1)
			{
    
    

				//双线性插值											
				dst.at<Vec3b>(i, j)[0] = w1*w2*src.at<Vec3b>(y1, x1)[0]
					+(1-w1)*w2*src.at<Vec3b>(y2,x1)[0]
					+w1*(1-w2)*src.at<Vec3b>(y1, x2)[0] 
					+ (1 - w1)*(1 - w2)*src.at<Vec3b>(y2, x2)[0];
		
				dst.at<Vec3b>(i, j)[1] = w1*w2*src.at<Vec3b>(y1, x1)[1]
					+ (1 - w1)*w2*src.at<Vec3b>(y2, x1)[1]
					+ w1*(1 - w2)*src.at<Vec3b>(y1, x2)[1]
					+ (1 - w1)*(1 - w2)*src.at<Vec3b>(y2, x2)[1];
				dst.at<Vec3b>(i, j)[2] = w1*w2*src.at<Vec3b>(y1, x1)[2]
					+ (1 - w1)*w2*src.at<Vec3b>(y2, x1)[2]
					+ w1*(1 - w2)*src.at<Vec3b>(y1, x2)[2]
					+ (1 - w1)*(1 - w2)*src.at<Vec3b>(y2, x2)[2];
			}

		}


	}


}

四、结果

原图

在这里插入图片描述

翻转(上下左右)

在这里插入图片描述

大小变化

在这里插入图片描述

绕中心旋转

在这里插入图片描述

偏移

在这里插入图片描述

opencv结果:

缩小

在这里插入图片描述

透视变换

在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_40595787/article/details/121555358