本文主要是介绍[转]【OpenCV入门教程之十八】OpenCV仿射变换 SURF特征点描述合辑,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
本篇文章中,我们一起探讨了OpenCV中仿射变换和SURF特征点描述相关的知识点,主要一起了解OpenCV中仿射变换相关的函数warpAffine和getRotationMatrix2D,SURF算法在OpenCV中进一步的体现与应用。此博文一共有两个配套的麻雀虽小但五脏俱全的示例程序,其经过浅墨详细注释过的代码都在文中贴出,且文章最后提供了综合示例程序的下载。 依然是先看看示例程序截图: 


一、仿射变换
1.1 初识仿射变换
仿射变换(Affine Transformation或 Affine Map),又称仿射映射,是指在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间的过程。它保持了二维图形的“平直性”(即:直线经过变换之后依然是直线)和“平行性”(即:二维图形之间的相对位置关系保持不变,平行线依然是平行线,且直线上点的位置顺序不变)。
一个任意的仿射变换都能表示为乘以一个矩阵(线性变换)接着再加上一个向量(平移)的形式。
那么, 我们能够用仿射变换来表示如下三种常见的变换形式:
- 旋转,rotation (线性变换)
- 平移,translation(向量加)
- 缩放,scale(线性变换)
如果进行更深层次的理解,仿射变换代表的是两幅图之间的一种映射关系。
而我们通常使用2 x 3的矩阵来表示仿射变换。

考虑到我们要使用矩阵 A 和 B 对二维向量
做变换, 所以也能表示为下列形式:
或者 
即: 
仿射变换(Affine Transformation或 Affine Map),又称仿射映射,是指在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间的过程。它保持了二维图形的“平直性”(即:直线经过变换之后依然是直线)和“平行性”(即:二维图形之间的相对位置关系保持不变,平行线依然是平行线,且直线上点的位置顺序不变)。
一个任意的仿射变换都能表示为乘以一个矩阵(线性变换)接着再加上一个向量(平移)的形式。
那么, 我们能够用仿射变换来表示如下三种常见的变换形式:
- 旋转,rotation (线性变换)
- 平移,translation(向量加)
- 缩放,scale(线性变换)
如果进行更深层次的理解,仿射变换代表的是两幅图之间的一种映射关系。
而我们通常使用2 x 3的矩阵来表示仿射变换。
考虑到我们要使用矩阵 A 和 B 对二维向量 做变换, 所以也能表示为下列形式:
或者
即:
1. 2 仿射变换的求法
我们知道,仿射变换表示的就是两幅图片之间的一种联系 . 关于这种联系的信息大致可从以下两种场景获得:
- <1>已知 X和T,而且我们知道他们是有联系的. 接下来我们的工作就是求出矩阵 M
- <2>已知 M和X,要想求得 T. 我们只要应用算式
即可. 对于这种联系的信息可以用矩阵 M 清晰的表达 (即给出明确的2×3矩阵) 或者也可以用两幅图片点之间几何关系来表达。
我们形象地说明一下,因为矩阵 M 联系着两幅图片, 我们就以其表示两图中各三点直接的联系为例。
我们知道,仿射变换表示的就是两幅图片之间的一种联系 . 关于这种联系的信息大致可从以下两种场景获得:
- <1>已知 X和T,而且我们知道他们是有联系的. 接下来我们的工作就是求出矩阵 M
- <2>已知 M和X,要想求得 T. 我们只要应用算式
即可. 对于这种联系的信息可以用矩阵 M 清晰的表达 (即给出明确的2×3矩阵) 或者也可以用两幅图片点之间几何关系来表达。
见下图:

其中,点1, 2 和 3 (在图一中形成一个三角形) 与图二中三个点是一一映射的关系, 且他们仍然形成三角形, 但形状已经和之前不一样了。我们能通过这样两组三点求出仿射变换 (可以选择自己喜欢的点), 接着就可以把仿射变换应用到图像中去。
见下图:
其中,点1, 2 和 3 (在图一中形成一个三角形) 与图二中三个点是一一映射的关系, 且他们仍然形成三角形, 但形状已经和之前不一样了。我们能通过这样两组三点求出仿射变换 (可以选择自己喜欢的点), 接着就可以把仿射变换应用到图像中去。
1.3 仿射变换相关的函数使用
OpenCV仿射变换相关的函数一般涉及到warpAffine和getRotationMatrix2D这两个:
- 使用OpenCV函数warpAffine 来实现一些简单的重映射.
- 使用OpenCV函数getRotationMatrix2D 来获得旋转矩阵。
下面分别对其进行讲解。
- 使用OpenCV函数warpAffine 来实现一些简单的重映射.
- 使用OpenCV函数getRotationMatrix2D 来获得旋转矩阵。
1.3.1 warpAffine函数详解
warpAffine函数的作用是依据如下式子,对图像做仿射变换。

函数原型如下:
- 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。
- 第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,需和源图片有一样的尺寸和类型。
- 第三个参数,InputArray类型的M,2×3的变换矩阵。
- 第四个参数,Size类型的dsize,表示输出图像的尺寸。
- 第五个参数,int类型的flags,插值方法的标识符。此参数有默认值INTER_LINEAR(线性插值),可选的插值方式如下:
- INTER_NEAREST - 最近邻插值
- INTER_LINEAR - 线性插值(默认值)
- INTER_AREA - 区域插值
- INTER_CUBIC –三次样条插值
- INTER_LANCZOS4 -Lanczos插值
- CV_WARP_FILL_OUTLIERS - 填充所有输出图像的象素。如果部分象素落在输入图像的边界外,那么它们的值设定为 fillval.
- CV_WARP_INVERSE_MAP –表示M为输出图像到输入图像的反变换,即 。因此可以直接用来做象素插值。否则, warpAffine函数从M矩阵得到反变换。
- 第六个参数,int类型的borderMode,边界像素模式,默认值为BORDER_CONSTANT。
- 第七个参数,const Scalar&类型的borderValue,在恒定的边界情况下取的值,默认值为Scalar(),即0。
warpAffine函数的作用是依据如下式子,对图像做仿射变换。
函数原型如下:
- 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。
- 第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,需和源图片有一样的尺寸和类型。
- 第三个参数,InputArray类型的M,2×3的变换矩阵。
- 第四个参数,Size类型的dsize,表示输出图像的尺寸。
- 第五个参数,int类型的flags,插值方法的标识符。此参数有默认值INTER_LINEAR(线性插值),可选的插值方式如下:
- INTER_NEAREST - 最近邻插值
- INTER_LINEAR - 线性插值(默认值)
- INTER_AREA - 区域插值
- INTER_CUBIC –三次样条插值
- INTER_LANCZOS4 -Lanczos插值
- CV_WARP_FILL_OUTLIERS - 填充所有输出图像的象素。如果部分象素落在输入图像的边界外,那么它们的值设定为 fillval.
- CV_WARP_INVERSE_MAP –表示M为输出图像到输入图像的反变换,即 。因此可以直接用来做象素插值。否则, warpAffine函数从M矩阵得到反变换。
- 第六个参数,int类型的borderMode,边界像素模式,默认值为BORDER_CONSTANT。
- 第七个参数,const Scalar&类型的borderValue,在恒定的边界情况下取的值,默认值为Scalar(),即0。
另外提一点,我们的WarpAffine函数与一个叫做cvGetQuadrangleSubPix( )的函数类似,但是不完全相同。 WarpAffine要求输入和输出图像具有同样的数据类型,有更大的资源开销(因此对小图像不太合适)而且输出图像的部分可以保留不变。而 cvGetQuadrangleSubPix 可以精确地从8位图像中提取四边形到浮点数缓存区中,具有比较小的系统开销,而且总是全部改变输出图像的内容。
另外提一点,我们的WarpAffine函数与一个叫做cvGetQuadrangleSubPix( )的函数类似,但是不完全相同。 WarpAffine要求输入和输出图像具有同样的数据类型,有更大的资源开销(因此对小图像不太合适)而且输出图像的部分可以保留不变。而 cvGetQuadrangleSubPix 可以精确地从8位图像中提取四边形到浮点数缓存区中,具有比较小的系统开销,而且总是全部改变输出图像的内容。
1.3.2 getRotationMatrix2D
计算二维旋转变换矩阵。变换会将旋转中心映射到它自身。
- 第一个参数,Point2f类型的center,表示源图像的旋转中心。
- 第二个参数,double类型的angle,旋转角度。角度为正值表示向逆时针旋转(坐标原点是左上角)。
- 第三个参数,double类型的scale,缩放系数。
此函数计算以下矩阵:

其中:

计算二维旋转变换矩阵。变换会将旋转中心映射到它自身。
- 第一个参数,Point2f类型的center,表示源图像的旋转中心。
- 第二个参数,double类型的angle,旋转角度。角度为正值表示向逆时针旋转(坐标原点是左上角)。
- 第三个参数,double类型的scale,缩放系数。
此函数计算以下矩阵:
其中:
1.4 仿射变换相关核心函数在OpenCV中的实现源代码
这个部分贴出OpenCV中本节相关函数的源码实现细节,来给想了解实现细节的小伙伴们参考。浅墨暂时不在源码的细节上挖深作详细注释。
这个部分贴出OpenCV中本节相关函数的源码实现细节,来给想了解实现细节的小伙伴们参考。浅墨暂时不在源码的细节上挖深作详细注释。
1.4.1 OpenCV2.X中warpAffine函数源代码
首先,是warpAffine函数的实现源码。
1.4.2 OpenCV2.X中getRotationMatrix2D函数源代码
1.5 OpenCV仿射变换示例程序
看完上面的讲解和函数线吗实现,下面是一个以warpAffine和getRotationMatrix2D函数为核心的对图像进行仿射变换的示例程序。
效果图:



看完上面的讲解和函数线吗实现,下面是一个以warpAffine和getRotationMatrix2D函数为核心的对图像进行仿射变换的示例程序。
效果图:
二、SURF特征点描述
通过上一篇文章SURF相关内容的讲解,我们已经对SURF算法有了一定的理解。SURF算法为每个检测到的特征定义了位置和尺度,尺度值可用于定义围绕特征点的窗口大小,不论物体的尺度在窗口是什么样的,都将包含相同的视觉信息,这些信息用于表示特征点以使得他们与众不同。
在特征匹配中,特征描述子通常是用于N维向量,在光照不变以及少许透视变形的情况下很理想。另外,优质的描述子可以通过简单的距离测量进行比较,比如欧氏距离。因此,他们在特征匹配算法中,用处是很大的。
在OpenCV中,使用SURF进行特征点描述主要是drawMatches方法和BruteForceMatcher类的运用,让我们一起来认识他们。
通过上一篇文章SURF相关内容的讲解,我们已经对SURF算法有了一定的理解。SURF算法为每个检测到的特征定义了位置和尺度,尺度值可用于定义围绕特征点的窗口大小,不论物体的尺度在窗口是什么样的,都将包含相同的视觉信息,这些信息用于表示特征点以使得他们与众不同。
在特征匹配中,特征描述子通常是用于N维向量,在光照不变以及少许透视变形的情况下很理想。另外,优质的描述子可以通过简单的距离测量进行比较,比如欧氏距离。因此,他们在特征匹配算法中,用处是很大的。
在OpenCV中,使用SURF进行特征点描述主要是drawMatches方法和BruteForceMatcher类的运用,让我们一起来认识他们。
2.1 drawMatches函数详解
drawMatches用于绘制出相匹配的两个图像的关键点,它有如下两个版本的C++函数原型:
除了第五个参数matches1to2和第九个参数matchesMask有细微的差别以外,两个版本的基本上相同。
- 第一个参数,const Mat&类型的img1,第一幅源图像。
- 第二个参数,const vector<KeyPoint>&类型的keypoints1,根据第一幅源图像得到的特征点,它是一个输出参数。
- 第三个参数,const Mat&类型的img2,第二幅源图像。
- 第四个参数,const vector<KeyPoint>&类型的keypoints2,根据第二幅源图像得到的特征点,它是一个输出参数。
- 第五个参数,matches1to2,第一幅图像到第二幅图像的匹配点,即表示每一个图1中的特征点都在图2中有一一对应的点、
- 第六个参数,Mat&类型的outImg,输出图像,其内容取决于第五个参数标识符falgs。
- 第七个参数,const Scalar&类型的matchColor,匹配的输出颜色,即线和关键点的颜色。它有默认值Scalar::all(-1),表示颜色是随机生成的。
- 第八个参数,const Scalar&类型的singlePointColor,单一特征点的颜色,它也有表示随机生成颜色的默认值Scalar::all(-1)。
- 第九个参数,matchesMask,确定哪些匹配是会绘制出来的掩膜,如果掩膜为空,表示所有匹配都进行绘制。
- 第十个参数,int类型的flags,特征绘制的标识符,有默认值DrawMatchesFlags::DEFAULT。可以在如下这个DrawMatchesFlags结构体中选取值:
drawMatches用于绘制出相匹配的两个图像的关键点,它有如下两个版本的C++函数原型:
除了第五个参数matches1to2和第九个参数matchesMask有细微的差别以外,两个版本的基本上相同。
- 第一个参数,const Mat&类型的img1,第一幅源图像。
- 第二个参数,const vector<KeyPoint>&类型的keypoints1,根据第一幅源图像得到的特征点,它是一个输出参数。
- 第三个参数,const Mat&类型的img2,第二幅源图像。
- 第四个参数,const vector<KeyPoint>&类型的keypoints2,根据第二幅源图像得到的特征点,它是一个输出参数。
- 第五个参数,matches1to2,第一幅图像到第二幅图像的匹配点,即表示每一个图1中的特征点都在图2中有一一对应的点、
- 第六个参数,Mat&类型的outImg,输出图像,其内容取决于第五个参数标识符falgs。
- 第七个参数,const Scalar&类型的matchColor,匹配的输出颜色,即线和关键点的颜色。它有默认值Scalar::all(-1),表示颜色是随机生成的。
- 第八个参数,const Scalar&类型的singlePointColor,单一特征点的颜色,它也有表示随机生成颜色的默认值Scalar::all(-1)。
- 第九个参数,matchesMask,确定哪些匹配是会绘制出来的掩膜,如果掩膜为空,表示所有匹配都进行绘制。
- 第十个参数,int类型的flags,特征绘制的标识符,有默认值DrawMatchesFlags::DEFAULT。可以在如下这个DrawMatchesFlags结构体中选取值:
2.2 OpenCV2.X中drawMatches函数源代码
在D:\Program Files(x86)\opencv\sources\modules\features2d\src\draw.cpp路径下,有drawMatches函数两个版本的源码。在这里贴出OpenCV中本其源码实现细节,来给想了解实现细节的小伙伴们参考。
在D:\Program Files(x86)\opencv\sources\modules\features2d\src\draw.cpp路径下,有drawMatches函数两个版本的源码。在这里贴出OpenCV中本其源码实现细节,来给想了解实现细节的小伙伴们参考。
BruteForceMatcher类源码分析
接下来,我们看看本文示例程序中会用到的BruteForceMatcher类的源码分析。在D:\Program Files(x86)\opencv\sources\modules\legacy\include\opencv2\legacy\legacy.hpp路径下,可以找到BruteForceMatcher类的定义。
其公共继承自BFMatcher类。而BFMatcher类位于D:\Program Files(x86)\opencv\sources\modules\features2d\include\opencv2\features2d\features2d.hpp
路径之下,我们一起看看其代码:
发现公共其继承自DescriptorMatcher类,再次进行溯源,在D:\Program Files(x86)\opencv\sources\modules\features2d\include\opencv2\features2d\features2d.hpp路径下找到DescriptorMatcher类的定义。
可以发现,DescriptorMatcher类和之前我们讲到FeatureDetector 类和DescriptorExtractor类一样,都是继承自他们“德高望重的祖先”Algorithm基类的。
而我们用BruteForceMatcher类时用到最多的match方法,是它从DescriptorMatcher类那里的“拿来主义”。定义如下:
接下来,我们看看本文示例程序中会用到的BruteForceMatcher类的源码分析。在D:\Program Files(x86)\opencv\sources\modules\legacy\include\opencv2\legacy\legacy.hpp路径下,可以找到BruteForceMatcher类的定义。
其公共继承自BFMatcher类。而BFMatcher类位于D:\Program Files(x86)\opencv\sources\modules\features2d\include\opencv2\features2d\features2d.hpp
路径之下,我们一起看看其代码:
发现公共其继承自DescriptorMatcher类,再次进行溯源,在D:\Program Files(x86)\opencv\sources\modules\features2d\include\opencv2\features2d\features2d.hpp路径下找到DescriptorMatcher类的定义。
可以发现,DescriptorMatcher类和之前我们讲到FeatureDetector 类和DescriptorExtractor类一样,都是继承自他们“德高望重的祖先”Algorithm基类的。
而我们用BruteForceMatcher类时用到最多的match方法,是它从DescriptorMatcher类那里的“拿来主义”。定义如下:
2.3 SURF特征匹配示例程序
这个示例程序中,我们利用SurfDescriptorExtractor类进行特征向量的相关计算。
程序利用了SURF特征的特征描述办法,其操作封装在类SurfFeatureDetector中,利用类内的detect函数可以检测出SURF特征的关键点,保存在vector容器中。第二步利用SurfDescriptorExtractor类进行特征向量的相关计算。将之前的vector变量变成向量矩阵形式保存在Mat中。最后强行匹配两幅图像的特征向量,利用了类BruteForceMatcher中的函数match。
程序的核心思想是:
- 使用 DescriptorExtractor 接口来寻找关键点对应的特征向量。
- 使用 SurfDescriptorExtractor 以及它的函数 compute 来完成特定的计算。
- 使用 BruteForceMatcher 来匹配特征向量。
- 使用函数 drawMatches 来绘制检测到的匹配点。
关键点讲解:OpenCV2引入了一个通用类,用于提取不同的特征点描述子,计算如下:
这里的结果为一个Mat矩阵,它的行数与特征点向量中元素个数是一致的。每行都是一个N维描述子的向量,比如SURF算法默认的描述子维度为64,该向量描绘了特征点周围的强度样式。两个特征点越相似,他们的特征向量也越靠近。这些描述子在图像匹配中尤其有用,如我们想匹配同一个场景中的两幅图像。首先,我们检测每幅图像中的特征,然后提取他们的描述子。第一幅图像中的每一个特征描述子向量都会与第二幅图中的描述子进行比较,得分最高的一对描述子,也就是两个向量的距离最近)将被视为那个特征的最佳匹配。该过程对于第一幅图像中的所有特征进行重复,这便是BruteForceMatcher中实行的最基本的策略。相关代码如下:
BruteForceMatcher是由DescriptorMatcher派生出来的一个类,而DescriptorMatcher定义了不同的匹配策略的共同接口。调用match方法后,在其第三个参数输出一个cv::DMatch向量。于是我们定义一个std::vector<DMatch>类型的matches。
调用match方法之后,我们便可以使用drawMatches方法对匹配到的点进行绘制,并最终显示出来。相关代码如下:
核心部分讲解完毕,下面我们一起来看看详细注释的示例程序的完全体。
看看运行效果图。
两幅原始图:

效果图:

这个示例程序中,我们利用SurfDescriptorExtractor类进行特征向量的相关计算。
程序利用了SURF特征的特征描述办法,其操作封装在类SurfFeatureDetector中,利用类内的detect函数可以检测出SURF特征的关键点,保存在vector容器中。第二步利用SurfDescriptorExtractor类进行特征向量的相关计算。将之前的vector变量变成向量矩阵形式保存在Mat中。最后强行匹配两幅图像的特征向量,利用了类BruteForceMatcher中的函数match。
程序的核心思想是:
- 使用 DescriptorExtractor 接口来寻找关键点对应的特征向量。
- 使用 SurfDescriptorExtractor 以及它的函数 compute 来完成特定的计算。
- 使用 BruteForceMatcher 来匹配特征向量。
- 使用函数 drawMatches 来绘制检测到的匹配点。
关键点讲解:OpenCV2引入了一个通用类,用于提取不同的特征点描述子,计算如下:
这里的结果为一个Mat矩阵,它的行数与特征点向量中元素个数是一致的。每行都是一个N维描述子的向量,比如SURF算法默认的描述子维度为64,该向量描绘了特征点周围的强度样式。两个特征点越相似,他们的特征向量也越靠近。这些描述子在图像匹配中尤其有用,如我们想匹配同一个场景中的两幅图像。首先,我们检测每幅图像中的特征,然后提取他们的描述子。第一幅图像中的每一个特征描述子向量都会与第二幅图中的描述子进行比较,得分最高的一对描述子,也就是两个向量的距离最近)将被视为那个特征的最佳匹配。该过程对于第一幅图像中的所有特征进行重复,这便是BruteForceMatcher中实行的最基本的策略。相关代码如下:
BruteForceMatcher是由DescriptorMatcher派生出来的一个类,而DescriptorMatcher定义了不同的匹配策略的共同接口。调用match方法后,在其第三个参数输出一个cv::DMatch向量。于是我们定义一个std::vector<DMatch>类型的matches。
调用match方法之后,我们便可以使用drawMatches方法对匹配到的点进行绘制,并最终显示出来。相关代码如下:
核心部分讲解完毕,下面我们一起来看看详细注释的示例程序的完全体。
看看运行效果图。
两幅原始图:
效果图:
这篇关于[转]【OpenCV入门教程之十八】OpenCV仿射变换 SURF特征点描述合辑的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!