前言

传统而言,基于特征点匹配的图像配准步骤如下:

  • step01:提取关键点
  • step02:计算描述子
  • step03:关键点匹配,通过描述子的相似性匹配
  • step04:计算得到变换矩阵,实现图像的配准

1、特征点提取

(1)GFTT算法提取特征点

A.What(什么是GFTT)

GFTT算法是对Harris的改进:一是针对特征点聚集问题,使用局部区域最大值解决;二是针对不均匀分布问题,使用限制两个特征点之间最短具体解决

B.GFTT的优势

  • 稳定性好:GTFF 算法检测到的角点相对更加稳定,不容易出现聚簇现象,且角点信息的丢失和位置偏移也相对较少
  • 参数可调节:通过设置相关参数,如最大角点数目、角点可接受的最小特征值、角点之间的最小距离等,可以根据具体需求和图像特点来优化特征点的提取效果

C.How(如何使用GFTT算法提取图像特征点)

int GetKpsByGTFF(
	const cv::Mat& imSrcGray, 
	std::vector<cv::KeyPoint>& vkps)
{
	//对参数进行检测
	if(imSrcGray.empty() )
		return -1; 
	
	if(imSrcGray.channels()!=1)
		return -2; 

	if(!vkps.empty())
		vkps.clear(); 

	//提取图像特征点
	cv::Ptr<cv::GFTTDetector> ptrGFTT = 
		cv::GFTTDetector::create(
							//关键点的最大数量
							500, 
							//角点的最小特征值
							0.01, 
							//角点之间允许的最短距离
							15); 
	//调用算法探测图像上的特征点
	ptrGFTT->detect(imSrcGray, vkps); 
	if(vkps.size()==0) return -3; 
	else return 1;
											
}

(2)FAST算法提取特征点

A.What(什么是FAST角点)

以某个点为中心做一个圆,根据圆上的像素值判断该 点是否为关键点。由于不需要作复杂的梯度计算,该方法提取关键点的效率很高,在实时性要求高的条件下使用很广

B.FAST角点的强度值

计算中心点像素与 认定的连续圆弧上的像素的差值,然后将这些差值的绝对值累加,就能得到角点强度

C.How(如何使用FAST算法提取图像特征点)

int GetKpsByFAST(
	const cv::Mat& imSrcGray, 
	std::vector<cv::KeyPoint>& vkps)
{
	//对参数进行检测
	if(imSrcGray.empty() )
		return -1; 

	if(imSrcGray.channels()!=1)
		return -2; 

	if(!vkps.empty())
		vkps.clear(); 

	//使用FAST算法提取特征点
	//65表关键点强度,FAST角点强度值(response)
	cv::Ptr<cv::FastFeatureDetector> ptrFAST = 
		cv::FastFeatureDetector::create(65);
	ptrFAST->detect(imgSrcGray, vkps);
	if(vkps.size()==0)
		return -3;
	else
		return 1;
}

(3)BRISK算法提取特征点

A.What

基于FAST的多尺度快速检测器。首先下采样构建图像金字塔,然后对金字塔所有图像应用FAST特征检测。只有是局部最大值的像素才可能成 为关键点。这个条件满足后,比较这个点与上下两层的相邻像素的评分;如果它的评分在尺度上 也更高,那么就认为它是一个兴趣点。

B.How(如何使用BRISK算法提取特征点)

int GetKpsByBRISK(
	const cv::Mat& imSrcGray, 
	std::vector<cv::KeyPoint>& vkps)
{
	//对参数进行检测
	if(imSrcGray.empty() )
		return -1; 
	
	if(imSrcGray.channels()!=1)
		return -2; 

	if(!vkps.empty())
		vkps.clear(); 
		
	//使用BRISK算法提取图像特征点
	//60表示FAST角点的强度值必须达到60,
	//才有可能被认为是关键点;5表示金字塔层数
	cv::Ptr<cv::BRISK> ptrBRISK = 
		cv::BRISK::create(60, 5); 
	ptrBRISK->detect(imSrcGray, vkps);
	
	if (vkps.size() == 0) 	
		return -3;
	else 	
		return 1;
}

(4)ORB算法提取特征点

A.What

基于FAST的多尺度带方向的特征检测算法。

  • 特征点方向:每个被检测的兴趣点总是关联了一个方向。在FAST的中心点的圆形区域内计算重心,中心与重心的组成的向量即为该关键电的方向。

B.How(如何使用ORB算法提取特征点)

int GetKpsByORB(
	const cv::Mat& imSrcGray, 
	std::vector<cv::KeyPoint>& vkps)
{
	//对参数进行检测
	if(imSrcGray.empty() )
		return -1; 
	
	if(imSrcGray.channels()!=1)
		return -2; 

	if(!vkps.empty())
		vkps.clear(); 
	
	//使用ORB算法对图像进行特征点提取
	//500:特征点数量;1.2:金字塔的缩放尺度;8:金字塔层数
	cv::Ptr<cv::ORB> ptrORB = 
		cv::ORB::create(500, 1.2, 8);
	ptrORB->detect(imSrcGray, vkps);
	if (vkps.size() == 0)
		return -3;
	else
		return 1;
}

2、描述子

(1)What

A图像的某一点和B图像的某一点如果对应现实世界的同一点,那么它们就应该是匹配的。这也是特征点匹配的目的

从数学的角度分析:要进行特征点的匹配,就应该考虑两个关键点的相似性,因此,如何衡量两个关键点的相似性成为了我们要考虑的首要问题

(2)How(如何衡量两个特征点相似性)

A.周围像素的特征

计算该特征点指定区域的均值、方差、梯度等,用作衡量标准。但单个的值很容易受到环境的印象,具有不稳定性,在工程实践中不会采用这些特征

B.周围像素的描述子

使用一些专门为特征点设计的描述子(可以理解为一个向量),这些描述子能够捕捉到角点的独特特性,通过比较描述子的差异来衡量相似性。在工程实践中一般采用特征点的描述子来进行特征点的匹配

C.基于深度学习的方法

利用训练好的卷积神经网络模型提取角点的深层特征,然后比较这些特征的相似度

(3)SIFT描述子实现关键点匹配

A.原理

step01:首先选取关键点周围16×16像素点,将其分为4×4大小的子区域,总共16个子区域。
step02:然后对每个子区域统计八个方向的梯度方向直方图,每个子区域用这8个统计结果进行描述。
step03:最后得到8×16 = 128 个数据作为描述符向量。

B.How(如何得到SIFT关键点和描述子)

#include <opencv2/features2d/features2d.hpp>
int GetKpsAndDescriptorBySIFT(
	const cv::Mat& imSrc,
	std::vector<cv::KeyPoint>& vkps,
	cv::Mat& matDescriptor)
{
	//对输入的参数进行检查
	if (imSrc.empty())
		return -1;
	
	if (imSrc.channels() != 1)
		return - 2;
	
	if (!vkps.empty())
		vkps.clear();
	
	//提取关键点
	cv::Ptr<cv::SIFT> ptrSIFT=cv::SIFT::create(500,3);
	ptrSIFT->detect(imSrc, vkps);
	if (vkps.size() == 0)
		return -3;
		
	//计算关键点的描述子
	ptrSIFT->compute(imSrc, vkps, matDescriptor);
	if (matDescriptor.empty())
		return -4;
	return 1;
}

(4)BRIEF描述子实现关键点匹配

A.原理

采用BRIEF算法提取关键点的描述子

B.BRIEF算法计算描述子

  • 为减少噪声干扰,先对图像进行高斯滤波(方差为2,高斯窗口为9x9)
  • 以特征点为中心,取s×s的邻域窗口,在窗口内随机选取一对点,比较二者像素的大小,进行二进制赋值:若p(x) > p(y),则赋值为1;若p(x) ≤ p(y),则赋值为0,其中p(x)、p(y)分别是随机点x=(u1,v1)、y=(u2,v2)的像素值。
  • 在窗口中随机选取n对随机点(一般n=256),重复步骤2的二进制赋值,形成一个二进制编码,即特征描述子。

C.How(如何得到ORB关键点和描述子)

int GetKpsAndDescriptorByORB(
	const cv::Mat& imSrc,
	std::vector<cv::KeyPoint>& vkps,
	cv::Mat& matDescriptor)
{
	//对输入的参数进行检查
	if (imSrc.empty())
		return -1;

	if (imSrc.channels() != 1)
		return - 2;
	
	if (!vkps.empty())
		vkps.clear();
	
	//提取ORB关键点
	cv::Ptr<cv::Feature2D> ptrORB = 
		cv::ORB::create(500, 1.2, 8);
	ptrORB->detect(imSrc, vkps);
	if (vkps.size() == 0)
		return -3;
		
	//计算关键点的描述子
	ptrORB->compute(imSrc, vkps, matDescriptor);
	if (matDescriptor.empty())
		return -4;
	return 1;
}

(5)GFTT和BRIEF提取关键点和描述子

GFTT算法提取关键点、BRIEF提取关键点的描述子:

int GetKpsAndDescriptorByGFTT(
	const cv::Mat& imSrc,
	std::vector<cv::KeyPoint>& vkps,
	cv::Mat& matDescriptor)
{

	//对输入的参数进行检查
	if (imSrc.empty())
		return -1;
	
	if (imSrc.channels() != 1)
		return -2;
	
	if (!vkps.empty())
		vkps.clear();
	
	//通过GFTT算法得到图像关键点
	cv::Ptr<cv::GFTTDetector> ptrGFTT =
		 cv::GFTTDetector::create();
	ptrGFTT->detect(imSrc, vkps);
	cv::Ptr<cv::ORB> ptrORB = cv::ORB::create();
	
	//计算关键点的BRIEF描述子
	ptrORB->compute(imSrc, vkps, matDescriptor);
	if (matDescriptor.empty())
		return -4;
	else
		return 1;
}

3、特征匹配

(1)What(什么是特征匹配)

两幅图像上的特征点,实现物理世界空间点的跟踪

(2)Which(有哪些特征匹配的方法)

A.暴力匹配

首先在第一幅图像中选取一个特征点,然后依次与第二幅图像中的特征点进行描述符的距离测试,最后返回距离最近的特征点作为匹配结果

使用GFTT提取关键点、BRIEF获取描述子、暴力算法实现匹配:

cv::Mat matDesLft, matDesRgt;
GetKpsAndDescriptorByGFTT(imLftGray, 
						  vkpsLft, 
						  matDesLft);
						  
GetKpsAndDescriptorByGFTT(imRgtGray, 
					      vkpsRgt, 
					      matDesRgt);
					      
//特征匹配
cv::Ptr<cv::DescriptorMatcher> matcher = 
	cv::DescriptorMatcher::create("BruteForce");
std::vector<cv::DMatch> vMatches;
matcher->match(matDesLft, matDesRgt, vMatches);
//匹配点筛选:当匹配距离大于两倍的最小距离时,判定为匹配有误
auto min_max = 
	std::minmax_element(vMatches.begin(), vMatches.end());
double dMinVal = min_max.first->distance;
double dMaxVal = min_max.second->distance;
std::vector<cv::DMatch> vGoodMatches;
for (int i = 0; i < matDesLft.rows; ++i)
{
	if (vMatches[i].distance<=std::max(2*dMinVal,30.0))
		vGoodMatches.push_back(vMatches[i]);
}
//绘制匹配的结果到outImg中
cv::Mat outImg;
cv::drawMatches(imLft, 
			    vkpsLft, 
			    imRgt, 
			    vkpsRgt, 
			    vGoodMatches,
			    outImg);
	

注意:cv::DescriptorMatcher::create(“BruteForce”)参数可选如下:
- `BruteForce` (it uses L2 )
- `BruteForce-L1`
- `BruteForce-Hamming`
- `BruteForce-Hamming(2)`
- `FlannBased`

B.最近邻匹配和FLANN匹配

对于图像 A 中的一个特征点,在图像 B 中找到与其距离最近的特征点作为匹配点。为了提高准确性,通常会采用最近邻距离与次近邻距离的比值来筛选匹配点

FLANN算法中,使用kd-trees进行向量相似度的匹配

int ImgFeatureMatchFLANN(
	const cv::Mat& matDesLft,
	const cv::Mat& matDesRgt,
	std::vector<cv::DMatch>& vGoodMatches)
	
{
	if (matDesLft.empty() || matDesRgt.empty())
		return -1;
	
	if (!vGoodMatches.empty())
		vGoodMatches.clear();
	
	cv::Ptr<cv::FlannBasedMatcher> matcher = 
		cv::FlannBasedMatcher::create();
	matcher->match(matDesLft,matDesRgt,vMatches);
	if(vMatches.empty())
		return -2;
	
	//匹配点筛选:当匹配距离大于两倍的最小距离时,判定为匹配有误
	auto min_max =
		 std::minmax_element(vMatches.begin(), 
		 					 vMatches.end());
	double dMinVal = min_max.first->distance;
	double dMaxVal = min_max.second->distance;
	std::vector<cv::DMatch> vGoodMatches;
	for (int i = 0; i < matDesLft.rows; ++i)
	{
		if (vMatches[i].distance <= std::max(2*dMinVal,30.0))
			vGoodMatches.push_back(vMatches[i]);
	}
}

D.RANSAC匹配

用于去除错误的匹配点对。通过随机选择一组匹配点来估计变换模型,然后根据该模型计算其他匹配点的误差,反复迭代,找到最优的模型和匹配点对

注意:一般特征点匹配之后求单映性矩阵时使用RANSAC算法提高单映矩阵的精度

E.双向匹配

不仅从图像 A 到图像 B 进行匹配,还从图像 B 到图像 A 进行匹配,只有双向都匹配成功的点对才被认为是有效的匹配点

4、图像配准

(1)What(什么是图像配准)

通过找到图像之间的空间变换关系,并对其中一幅或多幅图像进行相应的变换,从而实现图像间的精确匹配和重叠

(2)How(如何进行图像配准)

  • 首先,分别提取两幅图像的关键点和描述子

    const char* strLftPth = 
    	"XX\\data\\aloeL.jpg";
    const char* strRgtPth =
    	"XX\\data\\aloeR.jpg";
    cv::Mat imLft = cv::imread(strLftPth);
    cv::Mat imRgt = cv::imread(strRgtPth);
    cv::Mat imLftGray, imRgtGray;
    cv::cvtColor(imLft, 
    			imLftGray, 
    			cv::COLOR_RGB2GRAY);
    			
    cv::cvtColor(imRgt, 
    			imRgtGray, 
    			cv::COLOR_RGB2GRAY);
    			
    //特征点提取
    std::vector<cv::KeyPoint> vkpsLft;
    std::vector<cv::KeyPoint> vkpsRgt;
    cv::Mat matDesLft, matDesRgt;
    GetKpsAndDescriptorBySIFT(imLftGray, 
    						  vkpsLft, 
    						  matDesLft);
    						  
    GetKpsAndDescriptorBySIFT(imRgtGray, 
    						  vkpsRgt, 
    						  matDesRgt);
    
  • 其次,使用特征匹配算法得到特征点的匹配关系

    std::vector<cv::DMatch> vGoodMatches;
    ImgFeatureMatchFLANN(matDesLft, 
    					 matDesRgt, 
    					 vGoodMatches);
    
    
  • 然后,根据匹配关系计算两幅图像之间的变换矩阵

    // 筛选好的匹配点
    std::vector<cv::Point2f> vPtsLft, vPtsRgt;
    for (size_t i = 0; i < vGoodMatches.size(); i++) 
    {
    	vPtsLft.push_back(
    		vkpsLft[vGoodMatches[i].queryIdx].pt
    	);
    	
    	vPtsRgt.push_back(
    		vkpsRgt[vGoodMatches[i].trainIdx].pt
    	);
    }
    cv::Mat T = cv::findHomography(vPtsRgt, vPtsLft);
    
    
  • 最后,根据求得的变换矩阵对第二幅图像进行变换,实现两幅图像的对齐

    cv::Mat imAlgin;
    cv::warpPerspective(imRgtGray, 
    					imAlgin, 
    					T, 
    					imLftGray.size());
    

注意:案例中使用的是求解单应矩阵,针对同一个平面的两幅图像效果比较好

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐