文章目录
- 一、仿射变换
- 什么是仿射变换
- 公式推导
- 插值方法:双线性插值
- 二、透视变换
- 三、代码
- 四、结果
- 原图
- 翻转(上下左右)
- 大小变化
- 绕中心旋转
- 偏移
- opencv结果:
- 缩小
- 透视变换
一、仿射变换
什么是仿射变换
仿射变换也称仿射投影,是指几何中,对一个向量空间进行线性变换并接上一个平移,变换为另一个向量空间。所以,仿射变换其实也就是在讲如何来进行两个向量空间的变换。
对于一幅图像,可以看作很多个坐标的集合,每个坐标可以代表一个向量,由此可以将图像看作向量集合,那么在二维坐标系上:
公式推导
假设存在一个向量空间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⎦⎤=⎣⎡w00w100w01w110c1c21⎦⎤⎣⎡ab1⎦⎤
进一步缩写为:
[
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⎦⎤=W⎣⎡ab1⎦⎤
其中W称为转换矩阵。
在网上有一张很好的总结图:
上图为转换矩阵的转置:
第一步为找矩阵空间,第二步为插值;
插值方法:双线性插值
因为双线性插值需要知道插值点对应的四个原始坐标,所以变换矩阵需要变为逆矩阵,变换矩阵乘以原始坐标点可得到变换之后的坐标点,当前插值点乘以变换矩阵的逆矩阵可得到原始点的坐标,根据双线性插值算法进而可以得到最近四领域的插值。
操作为:
将变换之后的图像(X,Y)进行逆变换映射到原始的坐标
(
x
,
y
)
(x,y)
(x,y);
将
(
x
,
y
)
(x,y)
(x,y)改写为
(
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)=(1−u)∙(1−v)∙(x,y)+(u−1)∙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∙(v−1)∙(x+1,y)+(u∙v)∙(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⎦⎤=⎣⎡w00w100w01w110c1c21⎦⎤⎣⎡ab1⎦⎤
当图像在三维变化的之后需要加上一个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⎦⎤=⎣⎡w00w10w20w01w11w21c1c21⎦⎤⎣⎡ab1⎦⎤
一共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]
⎣⎡310−3103261001⎦⎤;
其中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结果:
缩小
透视变换
更多推荐
OpenCV学习笔记15_仿射变换与透视变换
发布评论