前言:
😊😊😊欢迎来到本博客😊😊😊
🌟🌟🌟 本专栏主要结合OpenCV和C++来实现一些基本的图像处理算法并详细解释各参数含义,适用于平时学习、工作快速查询等,随时更新。
😊😊😊 具体食用方式:可以点击本专栏【OpenCV快速查找(更新中)】–>搜索你要查询的算子名称或相关知识点,或者通过这篇博客👉通俗易懂OpenCV(C++版)详细教程——OpenCV函数快速查找(不断更新中)]查阅你想知道的知识,即可食用。
🎁🎁🎁支持:如果觉得博主的文章还不错或者您用得到的话,可以悄悄关注一下博主哈,如果三连收藏支持就更好啦!这就是给予我最大的支持!😙😙😙
文章目录
- 学习目标
- 一、基础知识
- 二、仿射变换
- 2.1 平移
- 2.2 放大和缩小
- 2.3 旋转
- 2.4 计算仿射矩阵
- 2.5 插值算法
- 2.6 代码实现
- 三、 总结
学习目标
- 熟悉仿射变换中平移、放大和缩小、旋转等基本操作
- 熟悉如何计算仿射矩阵
- 了解常见插值算法
一、基础知识
首先,对几何变换做个简单了解。打开任意一个图像编辑器,一般可以有对图像进行放大、缩小、旋转等操作,这类操作改变了原图中各区域的空间关系。对于这类操作,通常称为图像的几何变换。
一般而言,完成一张图像的几何变换需要两个独立的算法:首先,需要一个算法实现空间坐标变换,用它描述每个像素如何从初始位置移动到终止位置;其次,还需要一个插值算法完成输出图像的每个像素的灰度值。
本章节就围绕这两个独立的算法进行介绍,从而实现图像的几何变换。几何变换主要包括三种:仿射变换、投影变换和极坐标变换。
二、仿射变换
二维空间坐标的仿射变换可以这样表示:
为了更简洁地表达此式,在原坐标的基础上,引入第三个数值为1的坐标,这种表示方法称为齐次坐标,这样就可以用简单的矩阵乘法来表示仿射变换:
通常称A
为仿射变换矩阵,因为它的最后一行均为(0,0,1)。
为方便起见,在讨论过程中会省略最后一行。下面将详细介绍的仿射变换类型:平移、缩放、旋转。
2.1 平移
平移是最简单的仿射变换,如下图矩形所示,假设将空间坐标(x,y)
,其中0≤x≤110,0≤y≤110,先沿x轴正方向平移40,再沿y轴正方向平移50;或者反过来,先沿y轴正方向平移,再沿x轴正方向平移,平移后的坐标为(x',y')
,即(x',y')=(x+40,y+50)
。
关于平移,假设任意空间坐标(x,y)
先沿x轴平移tx
,再沿y轴平移ty
,则最后得到的坐标为 (x',y')=(x+tx ,y+ty)
。用矩阵形式表示该平移变换过程如下:
其中,若tx>0
,则表示沿x轴正方向移动;tx<0
,则表示沿x轴负方向移动;ty亦是。
2.2 放大和缩小
放大和缩小不是指在物理空间中某一个物体的放大和缩小。二维空间坐标(x,y)
以(0,0)为中心在水平方向上缩放sx
倍,在垂直方向上缩放sy
倍,指的是变换后的坐标位置离(0,0)的水平距离变为原坐标离位置中心点的水平距离的sx
倍,垂直距离变为原坐标离中心点的垂直距离的sy
倍。
根据以上定义,(x,y)
以(0,0)为中心缩放变换后的坐标为(x',y')
,即(x',y')=(sx*x,sy*y)
,显然,变换
后的坐标位置离中心点的水平距离由|x|
缩放为|sx*x|
,垂直距离由|y|
缩放为|sy*y|
。
若sx>1
,则表示在水平方向上放大,就是离中心点的水平距离增大了;反之,在水平方向上缩小。同样,若sy>1
,则表示在垂直方向上放大;反之,在垂直方向上缩小。
通常令sx =sy
,即常说的等比例缩放。例如:(-10,10)以(0,0)为中心放大两倍,则坐标变换为(-20,20)。缩放变换也可用矩阵形式来表示:
如下图所示:
以上介绍的是以原点(0,0)为中心的缩放变换,那么(x,y)以任意一点(x0,y0)为中心在水平方向上缩放sx
倍,在垂直方向上缩放sy
倍,则缩放后的坐标为(x',y')
,即(x',y') =(x0 +sx*(x-x0),y0 +sy*(y-y0))
。
显然,缩放后的坐标位置离中心点的水平距离变为原来的sx
倍,离中心点的垂直距离变为原来的sy
倍。可以将该变换过程理解为先将原点平移到中心点,再以原点为中心进行缩放,然后移回坐标原点。用矩阵形式可以表示为:
这里显示了齐次坐标的优势,以任意一点为中心的缩放仿射变换矩阵是平移矩阵和以(0,0)为中心的缩放仿射变换矩阵组合相乘而得到的。
我们通过一个实际案例来深入理解:上图中矩形右下角坐标(40,50)以(20,20)为中心同比例缩小2倍。(40,50)离(20,20)的水平距离为20,垂直距离为30,同比例缩小2倍,则变换后的坐标位置离(40,50)的水平距离应为10,垂直距离应为15,即变换后的坐标为(30,35),用矩阵表示该计算过程为:
2.3 旋转
除了坐标的平移、缩放,还有一种常用的坐标变换,即旋转。如下图所示,图1显示的是(x,y)
绕(0,0)顺时针旋转α(α>0)
的结果;图2显示的是(x,y)
绕(0,0)逆时针旋转α的结果。
对于顺时针后的坐标(x',y')
:
其中p代表(x,y)
到中心点(0,0)的距离,则:
化简得:
那么,用矩阵表示为:
对于逆时针后的坐标(x',y')
:
化简得:
那么,用矩阵表示为:
从得到的两个旋转仿射矩阵可得逆时针旋转α和顺时针旋转-α是一样的,用程序实现时,实现其中的一种就可以了。
以上讨论的旋转变换是以(0,0)为中心进行旋转的,如果(x,y)
绕任意一点(x0,y0)
逆时针旋转α,则首先将原点移到旋转中心,然后绕原点旋转,最后移回坐标原点,即:
需要注意的是,以上解决的是已知坐标及其仿射变换矩阵,从而计算出变换后的坐标。
那么,反过来考虑一个问题,如何通过已知坐标及其对应的经过某种仿射变换后的坐标,从而计算出它们之间的仿射变换矩阵?
2.4 计算仿射矩阵
对于空间变换的仿射矩阵有两种计算方式,分别是方程组法和矩阵相乘法。
(1) 方程组法
仿射变换矩阵有六个未知数,所以需要三组对应位置坐标,构造出由六个方程组成的方程组即可解六个未知数。
举例:如果(0,0) 、(200,0) 、(0,200)这三个坐标通过某仿射变换矩阵A分别转换为(0,0) 、(100,0) 、(0,100),则可利用这三组对应坐标构造出六个方程,求解出A。
对于C++的API函数getAffineTransform()
输入参数有两种方式,第一种方式是将原位置坐标和对应的变换后的坐标分别保存在Point2f
数组中,代码如下:
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
using namespace std;
using namespace cv;
int main() {
Point2f src[] = { Point2f(0,0),Point2f(200,0), Point2f(0,200) };
Point2f dst[] = { Point2f(0,0),Point2f(100,0), Point2f(0,100) };
Mat A = getAffineTransform(src,dst);
cout << A<<endl;
return 0;
}
返回值A仍然是2行3列的矩阵,指的是仿射变换矩阵的前两行。需要注意的是,数据类型是CV_64F
而不是CV_32F
。
第二种方式是将原位置坐标和对应的变换后的坐标保存在Mat
中,每一行代表一个坐标,数据类型必须是CV_32F
,否则会报错,代码如下:
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
using namespace std;
using namespace cv;
int main() {
Mat src = (Mat_<float>(3, 2) << 0, 0, 200, 0, 0, 200);
Mat dst = (Mat_<float>(3, 2) << 0, 0, 100, 0, 0, 100);
Mat A = getAffineTransform(src, dst);
cout << A << endl;
return 0;
}
(2) 矩阵相乘法
使用矩阵相乘法计算仿射矩阵,前提是需要知道基本仿射变换步骤,即如果(x,y)先缩放再平移,则变换后的矩阵形式为:
以上仿射变换矩阵是由平移矩阵乘以缩放矩阵得到的。需要注意的是,虽然先缩放再平移,但是仿射变换矩阵是平移仿射矩阵乘以缩放仿射矩阵,而不是缩放仿射矩阵乘以平移仿射矩阵,即等式右边的运算是从右向左进行的。
在2.2 OpenCV之矩阵运算详解(全)中已经提到,在OpenCV中是通过“*”
运算符或者gemm
函数来实现矩阵的乘法的,代码如下:
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
using namespace std;
using namespace cv;
int main() {
Mat src = (Mat_<float>(3, 3) << 0.5, 0, 0, 0, 0.5, 0, 0, 0, 1);//缩放矩阵
Mat dst = (Mat_<float>(3, 3) << 1, 0, 100, 0,1, 200, 0, 0, 1);//平移矩阵
Mat A;
gemm(src,dst,1.0,Mat(),0,A,0);
cout << A << endl;
return 0;
}
类似的,如果以(x0,y0)为中心进行缩放变换,然后逆时针旋转α
,则仿射变换矩阵为:
即:
若还需平移,则只需将结果左乘一个平移仿射矩阵即可。以上是以(x0,y0)为中心先进行缩放,然后逆时针旋转α的;反过来,如果先逆时针旋转α
再进行缩放处理,则仿射变换矩阵为:
从得到的两个仿射变换矩阵可以看出,如果是等比例缩放的,即sx =sy
,则两个仿射变换矩阵是相等的。对于这种等比例缩放的仿射变换,OpenCV提供了函数:
getRotationMatrix2D(center,angle,scale)
其中参数center
:变换中心点的坐标;
scale
:等比例缩放的系数;
angle
:逆时针旋转的角度。
虽然这里angle
称为逆时针,但是如果angle
是负数,则相当于顺时针了。需要注意的是:angle
是以角度为单位,而不是以弧度为单位的。
举例:计算以坐标点(50,50)为中心逆时针旋转45°的仿射变换矩阵。
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
using namespace std;
using namespace cv;
int main() {
Mat A = getRotationMatrix2D(Point2f(50, 50), 45, 0.5);
cout << A << endl;
return 0;
}
返回值是一个2×3的Mat
,数据类型是CV_64F
。
知道空间坐标变换中的仿射变换后,接下来需要考虑如何将其运用到图像的几何变换中,就需要利用以下提到的插值算法了。
2.5 插值算法
(1) 最近邻插值法
已知坐标点(x,y),令[x]代表x的整数部分,[y]代表y的整数部分,如果x>0且y>0,即在第一象限,显然(x,y)的四个相邻整数坐标为([x],[y])、([x]+1,[y])、([x],[y]+1)、([x]+1,[y]+1)
;如果在第二象限,即x<0且y>0,则(x,y)的四个相邻整数坐标为([x],[y])、([x]-1,[y])、([x],[y]+1)、([x]-1,[y]+1)
;其他两个象限类似。
举例:(2.3,2.7)的四个相邻整数坐标分别为(2,2)、(3,2)、(2,3)、(3,3),离它最近的是(2,3),则函数 fI 在(2.3,2.7)处的函数值等于 fI 在(2,3)处的值,即 fI (2.3,2.7)= fI (2,3)。
使用最近邻插值法完成图像几何变换,输出图像会出现锯齿状外观,对图像放大处理的效果会更明显。为了得到更好的效果,应使用更多的信息,而不仅仅使用最近像素的灰度值,常用的方法是双线性插值和三次样条插值。
(2) 双线性插值法
对于双线性插值,以(x,y)落在第一象限为例,如下图所示,其他象限类似,处理过程可以分为以下三个步骤。
第一步
|x-[x]|
是点(x,y)
和([x],[y])
的水平距离,|[x]+1-x|
是点(x,y)
和([x]+1,[y])
的水平距离,显然0<|x-[x]|<1
,0<|[x]+1-x|<1
且|x-[x]|+|[x]+1-x|=1
。为了方便,记a=|x-[x]|
,如下图所示,通过以下线性关系估计 fI 在(x,[y])
处的值:
第二步
和第一步类似,|x-[x]|
是点(x,y)
和([x],[y]+1)
的水平距离,|[x]+1-x|
是点(x,y)
和([x]+1,[y]+1)
的水平距离,如下图所示,通过以下线性关系估计fI 在(x,[y]+1)
处的值处的值:
第三步
通过第一步和第二步分别得到了fI 在(x,[y]+1)
和(x,[y])
处的函数值,(x,y)
和(x,[y])
的垂直距离|y-[y]|
,(x,y)
和(x,[y]+1)
处的垂直距离为|[y]+1-y|
,如下图所示,通过以下线性关系估计fI 在(x,y)
处的函数值:
令b=|y-[y]|
:
这样对于非整数坐标处的函数值,就可以利用它的邻域的四个整数坐标处的函数值进行插值计算而得到。如果先进行两次垂直方向上的插值,然后再进行水平方向上的插值,得到的结果是一样的。
从双线性插值法的公式可以看出,fI 是一个二阶的函数。有时为了得到更好的拟合值,需要高阶的插值函数,如三次样条插值、Legendre中心函数和sin(axs)
函数,高阶插值常使用二维离散卷积运算来实现 。关于二维离散卷积将在后期会详细介绍。
2.6 代码实现
关于几何变换OpenCV提供了几个方便简洁的函数。
(1) 缩放
前面已经详细介绍了关于图像几何变换的两个重要关键点:空间坐标变换和插值方法,在已知仿射变换矩阵的基础上,C++提供了warpAffine()
函数。
使用函数warpAffine()
对图像进行缩放,需要先创建缩放仿射矩阵。为了使用更方便,对于图像的缩放,OpenCV还提供了另一个函数:
void resise(InputArayy src, OuputArray dst, Size dsize, double fx=0, double fy=0, int iterpolation=INTER_ LINEAR )
src
:输入图像矩阵;
dst
:输出图像矩阵;
dsize
:二元元组(宽,高),输出图像大小;
fx
:在水平方向的缩放比例,默认0;
fy
:在垂直方向的缩放比例,默认0;
interpolation
:插值方法:INTE_NEAREST
, INTE_LINEAR
(默认)。
这样在对图像矩阵进行缩放时,就不需要先创建缩放仿射矩阵,然后再使用函数warpAffine()
了,在本质上resize
的参数fx
和fy
也相当于构建了缩放仿射矩阵。下面分别使用这两个函数进行图像缩放:
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
using namespace std;
using namespace cv;
int main() {
Mat image = imread("D:/VSCodeFile/OpenCV_CSDN/image/logo.jpeg", IMREAD_COLOR);
if (!image.data)
{
cout << "Image is not exist!";
}
/*第一种方式:利用warpAffine()进行缩放*/
Mat dst1;
Mat A = (Mat_<float>(2, 3) << 2, 0, 0, 0, 2, 0);//缩放矩阵
warpAffine(image,dst1,A,Size(2*image.cols,2*image.rows));
/*第二种方式:利用resize()进行缩放*/
Mat dst2;
resize(image,dst2, Size(image.cols*2, image.rows *2),2,2);
imshow("src", image);
imshow("warpAffine", dst1);
imshow("resize", dst2);
waitKey(0);
return 0;
}
对图像进行缩放处理时,从代码量来说,使用resize
会更方便。
(2) 旋转
在OpenCV中,定义了rotate()
函数来实现图像矩阵顺时针旋转90°、180°、270°,函数如下:
rotate(InputArray src,OutputArray dst,int rotateCode)
src
:输入图像矩阵(单、多通道均可);
dst
:输出图像矩阵;
rotateCode
:ROTATE_90_CLOCKWISE
:顺时针旋转90°;ROTATE_180
:顺时针旋转180°;ROTATE_90_COUNTERCLOCKWISE
:顺时针旋转270°;
注意:虽然是图像矩阵的旋转,但该函数不需要利用仿射变换来完成这类旋转,只是行列的互换,类似于矩阵的转置操作,所以该函数声明在头文件opencv2/core.hpp
中。
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
using namespace std;
using namespace cv;
int main() {
//旋转函数rotate
Mat image = imread("D:/VSCodeFile/OpenCV_CSDN/image/logo.jpeg", IMREAD_COLOR);
Mat dst_90;
Mat dst_180;
Mat dst_270;
imshow("src", image);//原图显示
rotate(image,dst_90,ROTATE_90_CLOCKWISE);
imshow("dst_90", dst_90);//顺时针90°
rotate(image, dst_180, ROTATE_180);
imshow("dst_180", dst_180);//顺时针180°
rotate(image, dst_270, ROTATE_90_COUNTERCLOCKWISE);
imshow("dst_270", dst_270);//顺时针270°
waitKey(0);
return 0;
}
三、 总结
最后,长话短说,大家看完就好好动手实践一下,切记不能三分钟热度、三天打鱼,两天晒网。OpenCV是学习图像处理理论知识比较好的一个途径,大家也可以自己尝试写写博客,来记录大家平时学习的进度,可以和网上众多学者一起交流、探讨,有什么问题希望大家可以积极评论交流,我也会及时更新,来督促自己学习进度。希望大家觉得不错的可以点赞、关注、收藏。