《Opencv3编程入门》学习笔记—第十一章

news2024/11/24 13:32:17

《Opencv3编程入门》学习笔记

记录一下在学习《Opencv3编程入门》这本书时遇到的问题或重要的知识点。

第十一章 特征检测与匹配

一、SURF特征点检测

太复杂了!全是公式!

(一)SURF算法概览

  • SURF,SpeededUp Rebust Features,加速版的具有鲁棒性的特征算法,是尺度不变特征变换算法(SIFT)的加速版
  • 特点:采用了haar特征以及积分图像的概念,加快了运行时间
  • 应用:计算机视觉的物体识别以及3D重构

(二)SURF算法原理

1、构建Hessian矩阵构造高斯金字塔尺度空间

(1)Hessian matrix:
一个自变量为向量的实值函数的二阶偏导数组成的方框矩阵,假设函数f(z,y),Hessian矩阵H,图像中某个像素点的Hessian矩阵如下:
在这里插入图片描述                  
即每一个像素点都可以求出一个Hessian矩阵
H矩阵判别式为:
在这里插入图片描述
                  
判别式的值是H矩阵的特征值,可以利用判定结果的符号将所有点分类,根据判别式取值正负,来判别该点是或不是极值点。

(2)SURF算法中H矩阵的计算

用图像像素l(x,y)作为函数值f(x,y),选用二阶标准高斯函数作为滤波器,通过特定核间的卷积计算二阶偏导数,计算出H矩阵:
在这里插入图片描述
                 
由于特征点需要具备尺度无关性,所以在进行H矩阵构造前需要进行高斯滤波,滤波后在进行H计算:
在这里插入图片描述
             
L(x,t)是一幅图像在不同解析度下的表示,可以利用高斯核G(t)与图像函数I(x)在点x的卷积实现,其中高斯核G(t)计算公式为:
在这里插入图片描述
                      
其中,g(x)为高斯函数,t为高斯方差。

Herbert Bay提出用近似值替代L(x,t),为平衡准确值与近似值间的误差引入的权值,权值随尺度变化,H矩阵判别式:
在这里插入图片描述

(3)SURF的金字塔
  金字塔图像分很多层,每一层叫做一个octave,每一个octave有几张不同尺度图片。
  Sift算法中,同一个octave层图片尺寸(大小)相同,但尺度(模糊程度)不同,高斯模糊时,sift的高斯模板大小不变,只在不同octave之间改变图片大小
  Surf算法中,图片大小一直不变,同一层octave中不同图片高斯模板尺度不同,不同octave层图片改变高斯模糊尺寸
传统金字塔图片尺寸变化,且反复利用高斯函数对子层进行平滑,而surf算法保持原图像不变只改变滤波器大小,节省了降采样过程,提升了处理速度。

2、利用非极大值抑制初步确定特征点

将经过hessian矩阵处理过的每个像素点与其三维领域的26个点进行大小比较,如果是26个点中的最大/小值,则保留作为初步特征点。
  检测过程中,使用与该尺度层图像解析度对应大小的滤波器进行检测,以3*3滤波器为例,该尺度层图像9个像素点之一的检测特征点与自身尺度层中其余8个点和其上下尺度层中各9个点进行比较。

3、精确定位极值点

采用三维线性插值法得到亚像素级特征点,同时去掉值小于一定阈值的点,筛选出特征较强点。

4、选取特征点的主方向

(1)Sift选取特征点主方向是采用在特征点领域内统计其梯度直方图,取直方图bin值最大的及超过最大bin值80%的方向作为特征点主方向。
(2)Surf中,不统计梯度直方图,而是统计特征点领域内的haar小波特征。即在特征点领域内(如,半径为6s的圆,s为该点所在尺度),统计60度扇形内所有点的水平haar小波特征和垂直haar小波特征总和,haar小波尺寸边长为4s,得到扇形值,然后60度扇形以一定间隔进行旋转,最后将最大值那个扇形的方向作为该特征点主方向。

5、构造surf特征点描述算子

(1)Sift中,在特征点周围取1616领域,并把该领域化为44个小区域,每个小区域统计8个方向梯度,最后得到448=128维向量,该向量作为该点的sift描述子。
(2)Surf中,在特征点周围取一个正方形框,框边长为20s,该框方向即为第四步检测的主方向。然后把框分为16个子区域,每个子区域统计25个像素的水平方向和垂直方向(相对于主方向)的haar小波特征,该小波特征为水平方向值之和,水平方向绝对值之和,垂直方向之和,垂直方向绝对值之和。每个小区域有4个值,所以每个特征点就是16*4=64维向量,相比sift少了一半,会加快匹配速度。
在这里插入图片描述

6、总结

Surf采用hessian矩阵获取图像局部最值稳定,但求主方向阶段太过于依赖局部区域像素的梯度方向,可能主方向不准确,从而导致后面特征点提取及匹配误差。同时金字塔层不够紧密会使尺度有误差影响特征向量提取,所以应取适量层后进行插值。

(三)SURF类相关OpenCV源码剖析

在opencv安装路径下…\opencv\sources\modules\nonfree\include\opencv2\nonfree下的features2d.hpp头文件中,我们可以发现这样两句定义:
(这里不一一解读了,感兴趣的自行查看源码)

typedef SURF SurfFeatureDetector;
typedef SURF SurfDescriptorExtractor;

我们平常使用的SurfFeatureDetector类和SurfDescriptorExtractor类,其实就是SURF类,他们三者等价。

SURF类关系图:
在这里插入图片描述

(四)绘制关键点:drawKeypoints()函数

用于绘制关键点。

void drawKeypoints(const Mat&image, const vector<KeyPoint>& keypoints, Mat& outImage, constScalar& color=Scalar::all(-1), int flags=DrawMatchesFlags::DEFAULT )

第一个参数,const Mat&类型的src,输入图像。
第二个参数,const vector&类型的keypoints,根据源图像得到的特征点,它是一个输出参数。
第三个参数,Mat&类型的outImage,输出图像,其内容取决于第五个参数标识符falgs。
第四个参数,const Scalar&类型的color,关键点的颜色,有默认值Scalar::all(-1)。
第五个参数,int类型的flags,绘制关键点的特征标识符,有默认值DrawMatchesFlags::DEFAULT。可以在如下这个结构体中选取值。

struct DrawMatchesFlags
{
    enum
    {
        DEFAULT = 0, // Output image matrix will be created (Mat::create),
                     // i.e. existing memory of output image may be reused.
                     // Two source images, matches, and single keypoints
                     // will be drawn.
                     // For each keypoint, only the center point will be
                     // drawn (without a circle around the keypoint with the
                     // keypoint size and orientation).
        DRAW_OVER_OUTIMG = 1, // Output image matrix will not be
                       // created (using Mat::create). Matches will be drawn
                       // on existing content of output image.
        NOT_DRAW_SINGLE_POINTS = 2, // Single keypoints will not be drawn.
        DRAW_RICH_KEYPOINTS = 4 // For each keypoint, the circle around
                       // keypoint with keypoint size and orientation will
                       // be drawn.
    };
};

(五)KeyPoint类

KeyPoint类是一个为特征点检测而生的数据结构,用于表示特征点

class KeyPoint
{
	Point2f pt;   //坐标
	float size;    //特征点领域直径
	float angle;  //特征点方向,值为[0,360),负值表示不使用
	float response;
	int octave;   //特征点所在图像金字塔的组
	int class_id;  //用于聚类的id
}

(六)示例程序:SURF特征点检测

步骤
1、使用FeatureDetector接口来发现感兴趣点
2、使用SurfFeatureDetector以及其函数detect来实现检测过程
3、使用函数drawKeypoints绘制检测到的关键点

示例代码

//-----------------------------------【程序说明】----------------------------------------------
//		【SURF特征点检测】

//----------------------------------------------------------------------------------------------

//-----------------------------------【头文件包含部分】---------------------------------------
//		描述:包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include "opencv2/core/core.hpp"
#include "opencv2/features2d/features2d.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/nonfree/nonfree.hpp"
#include <iostream>


//-----------------------------------【命名空间声明部分】--------------------------------------
//          描述:包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------
using namespace cv;

//-----------------------------------【全局函数声明部分】--------------------------------------
//          描述:全局函数的声明
//-----------------------------------------------------------------------------------------------
static void ShowHelpText( );//输出帮助文字

//-----------------------------------【main( )函数】--------------------------------------------
//   描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//-----------------------------------------------------------------------------------------------
int main( int argc, char** argv )
{
	//【0】改变console字体颜色    
	system("color 2F");    

	//【0】显示帮助文字  
	ShowHelpText( );  

	//【1】载入源图片并显示
	Mat srcImage1 = imread("D://lili/Desktop/jpg/opencv/4.jpg", 1 );
	Mat srcImage2 = imread("D://lili/Desktop/jpg/opencv/5.jpg", 1 );
	if( !srcImage1.data || !srcImage2.data )//检测是否读取成功
	{ printf("读取图片错误,请确定目录下是否有imread函数指定名称的图片存在~! \n"); return false; } 
	imshow("原始图1",srcImage1);
	imshow("原始图2",srcImage2);

	//【2】定义需要用到的变量和类
	int minHessian = 400;//定义SURF中的hessian阈值特征点检测算子
	SurfFeatureDetector detector( minHessian );//定义一个SurfFeatureDetector(SURF) 特征检测类对象
	std::vector<KeyPoint> keypoints_1, keypoints_2;//vector模板类是能够存放任意类型的动态数组,能够增加和压缩数据

	//【3】调用detect函数检测出SURF特征关键点,保存在vector容器中
	detector.detect( srcImage1, keypoints_1 );
	detector.detect( srcImage2, keypoints_2 );

	//【4】绘制特征关键点
	Mat img_keypoints_1; Mat img_keypoints_2;
	drawKeypoints( srcImage1, keypoints_1, img_keypoints_1, Scalar::all(-1), DrawMatchesFlags::DEFAULT );
	drawKeypoints( srcImage2, keypoints_2, img_keypoints_2, Scalar::all(-1), DrawMatchesFlags::DEFAULT );

	//【5】显示效果图
	imshow("特征点检测效果图1", img_keypoints_1 );
	imshow("特征点检测效果图2", img_keypoints_2 );

	waitKey(0);
	return 0;
}


//-----------------------------------【ShowHelpText( )函数】----------------------------------
//          描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{ 
	//输出一些帮助信息  
	printf("\n\n\n\t欢迎来到【SURF特征点检测】示例程序~\n\n");    
	printf("\t当前使用的OpenCV版本为 OpenCV "CV_VERSION);  
	printf( "\n\n\t按键操作说明: \n\n"     
				"\t\t键盘按键任意键- 退出程序\n\n"
				"\n\n\t\t\t\t\t\t\t\t by浅墨\n\n\n");  

}

运行效果
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、SURF特征提取

在SURF进行特征点描述主要是drawMatches方法和BruteForceMatcher类的运用。

(一)绘制匹配点:drawMatches()函数

用于绘制出相匹配的两个图像的关键点

drawMatches (
	InputArray img1,
	const std::vector< KeyPoint > & keypoints1,
	InputArray img2,
	const std::vector< KeyPoint > & keypoints2,
	const std::vector< DMatch > & matches1to2,
	InputOutputArray outImg,
	const Scalar & matchColor = Scalar::all(-1),
	const Scalar & singlePointColor = Scalar::all(-1),
	const std::vector< char > & matchesMask = std::vector< char >(),
	DrawMatchesFlags flags = DrawMatchesFlags::DEFAULT
)

第一个参数是第一个源图像,
第二个参数是第一个源图像的关键点,
第三个参数是第二个源图像,
第四个参数是第二个源图像的关键点,
第五个参数是从第一张图像匹配到第二张图像,
第六个参数是输出图像。它的内容取决于定义在输出图像中绘制的内容的标志值,
第七个参数是匹配的颜色(线和连接的关键点),
第八个参数是单个关键点(圆圈)的颜色,表示关键点不匹配,
第九个参数是确定绘制哪些匹配项的掩码。如果掩码为空,则绘制所有匹配项。
第十个参数是标志设置绘图功能。可以在如下DrawMatchesFlags结构体中选取值

struct DrawMatchesFlags
{
    enum
    {
        DEFAULT = 0, // Output image matrix will be created (Mat::create),
                     // i.e. existing memory of output image may be reused.
                     // Two source images, matches, and single keypoints
                     // will be drawn.
                     // For each keypoint, only the center point will be
                     // drawn (without a circle around the keypoint with the
                     // keypoint size and orientation).
        DRAW_OVER_OUTIMG = 1, // Output image matrix will not be
                       // created (using Mat::create). Matches will be drawn
                       // on existing content of output image.
        NOT_DRAW_SINGLE_POINTS = 2, // Single keypoints will not be drawn.
        DRAW_RICH_KEYPOINTS = 4 // For each keypoint, the circle around
                       // keypoint with keypoint size and orientation will
                       // be drawn.
    };
};

(二)BruteForceMatcher类源码分析

在…\opencv\sources\modules\legacy\include\opencv2\legacy\legacy.hpp路径下(感兴趣的自行查看)

(三)示例程序:SURF特征提取

这个示例程序中,我们利用SurfDescriptorExtractor类进行特征向量的相关计算。

程序利用了SURF特征的特征描述办法,其操作封装在类SurfFeatureDetector中,利用类内的detect函数可以检测出SURF特征的关键点,保存在vector容器中。第二步利用SurfDescriptorExtractor类进行特征向量的相关计算。将之前的vector变量变成向量矩阵形式保存在Mat中。最后强行匹配两幅图像的特征向量,利用了类BruteForceMatcher中的函数match。

程序的核心思想是:

  • 使用 DescriptorExtractor 接口来寻找关键点对应的特征向量。
  • 使用 SurfDescriptorExtractor 以及它的函数 compute 来完成特定的计算。
  • 使用 BruteForceMatcher 来匹配特征向量。
  • 使用函数 drawMatches 来绘制检测到的匹配点。

示例代码

#include "opencv2/core/core.hpp"
#include "opencv2/features2d/features2d.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <opencv2\nonfree\nonfree.hpp>
#include <opencv2\legacy\legacy.hpp> 
#include <iostream>
using namespace cv;
using namespace std;

//-----------------------------------【main( )函数】--------------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//-----------------------------------------------------------------------------------------------
int main(  )
{
    //【1】载入素材图
    Mat srcImage1 = imread("D://lili/Desktop/jpg/opencv/2.jpg",1);
    Mat srcImage2 = imread("D://lili/Desktop/jpg/opencv/16.jpg",1);
    if( !srcImage1.data || !srcImage2.data )
    { printf("读取图片错误,请确定目录下是否有imread函数指定的图片存在~! \n"); return false; }
    imshow("原始图1", srcImage1);
    imshow("原始图2", srcImage2);

    //【2】使用SURF算子检测关键点
    int minHessian = 100;//SURF算法中的hessian阈值
    SurfFeatureDetector detector( minHessian );//定义一个SurfFeatureDetector(SURF) 特征检测类对象
    //Ptr<SurfFeatureDetector> detector=SurfFeatureDetector::create( minHessian );//定义一个SurfFeatureDetector(SURF) 特征检测类对象
    std::vector<KeyPoint> keyPoint1, keyPoints2;//vector模板类,存放任意类型的动态数组

    //【3】调用detect函数检测出SURF特征关键点,保存在vector容器中
    detector.detect( srcImage1, keyPoint1 );
    detector.detect( srcImage2, keyPoints2 );

    //【4】计算描述符(特征向量)
	SurfDescriptorExtractor extractor;
    //Ptr<SurfDescriptorExtractor> extractor=SurfDescriptorExtractor::create();
    Mat descriptors1, descriptors2;
    extractor.compute( srcImage1, keyPoint1, descriptors1 );
    extractor.compute( srcImage2, keyPoints2, descriptors2 );

    //【5】使用BruteForce进行匹配
    // 实例化一个匹配器
    BFMatcher matcher(NORM_L1);
    std::vector< DMatch > matches;
    //匹配两幅图中的描述子(descriptors)
    matcher.match( descriptors1, descriptors2, matches );

    //【6】绘制从两个图像中匹配出的关键点
    Mat imgMatches;
    drawMatches( srcImage1, keyPoint1, srcImage2, keyPoints2, matches, imgMatches );//进行绘制

    //【7】显示效果图
    imshow("匹配图", imgMatches );

    waitKey(0);
    return 0;
}

运行效果
在这里插入图片描述

三、使用FLANN进行特征点匹配

使用FlannBasedMatcher接口以及函数FLANN(),实现快速高效匹配(FLANN)

(一)FlannBasedMatcher类的简单分析

class CV_EXPORTS_W FlannBasedMatcher : public DescriptorMatcher{
	//......
}

发现:FlannBasedMatcher也是继承自DescriptorMatcher,并且同样主要使用来自DescriptorMatcher类的match方法进行匹配。

(二)找到最佳匹配:DescriptorMatcher::match方法

DescriptorMatcher:.match()函数从每个描述符查询集中找到最佳匹配,有两个版本的源码,下面用注释对其进行讲解。
在这里插入图片描述

(三)示例程序:使用FLANN进行特征点匹配

示例代码

#include<opencv2\features2d\features2d.hpp>
#include<opencv2\core\core.hpp>
#include<highgui\highgui.hpp>
#include<opencv2\nonfree\nonfree.hpp>
#include<opencv2\legacy\legacy.hpp>
#include<iostream>

using namespace cv;
using namespace std;
 
//FLANN对高维数据较快
int main(int argc,char** argv)
{
	//【1】载入源图片
	Mat img_1, img_2;
	img_1 = imread("D://lili/Desktop/jpg/opencv/2.jpg");
	img_2 = imread("D://lili/Desktop/jpg/opencv/16.jpg");
	if (img_1.empty() || img_2.empty()){printf("加载图片失败\n");return -1;}
 
	//【2】利用SURF检测器检测的关键点
	int minHessian = 400;

	SURF detector(minHessian);
	std::vector<KeyPoint>keypoints_1,keypoints_2;
	Mat descriptor1, descriptor2;
	//检测关键点并计算描述符
	detector.detect(img_1,keypoints_1);
	detector.detect(img_2,keypoints_2);
 
	//【3】计算描述符(特征向量)
	SURF extractor;
	Mat descriptors_1,descriptors_2;
	extractor.compute(img_1,keypoints_1,descriptors_1);
	extractor.compute(img_2,keypoints_2,descriptors_2);

	//【4】采用FLANN算法匹配描述符向量
	FlannBasedMatcher matcher;
	std::vector<DMatch> matches;
	matcher.match(descriptors_1,descriptors_2,matches);
	double max_dist = 0;double min_dist = 100;

	//【5】快速计算关键点之间的最大和最小距离
	for(int i = 0;i < descriptors_1.rows;i++){
		double dist = matches[i].distance;
		if(dist < min_dist) min_dist = dist;
		if(dist < max_dist) max_dist = dist;
	}
	//输出距离信息
	printf(">最大距离(Max dist): %f \n",max_dist);
	printf(">最小距离(Min dist): %f \n",min_dist);

	//【6】存下符合条件的匹配结果(即其距离小于2*min_dist的),使用radiusMatch同样可行
	std::vector<DMatch> good_matches;
	for(int i = 0;i < descriptors_1.rows;i++){
		if(matches[i].distance < 2*min_dist){
			good_matches.push_back(matches[i]);
		}
	}

	//【7】绘制出符合条件的匹配点
	Mat img_matches;
	drawMatches(img_1,keypoints_1,img_2,keypoints_2,good_matches,img_matches,Scalar::all(-1),Scalar::all(-1),vector<char>(),DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);

	//【8】输出相关匹配点信息
	for(int i = 0;i < good_matches.size();i++){
		printf(">符合条件的匹配点 [%d] 特征点1:%d -- 特征点2: %d \n",i,good_matches[i].queryIdx,good_matches[i].trainIdx);
	}

	//【9】显示效果图
	imshow("匹配效果图",img_matches);

	waitKey(0);
	return 0;
}

运行效果
在这里插入图片描述

(四)综合示例程序:FLANN结合SURF进行关键点的描述和匹配

【程序运行出现错误,代码不太理解,有待进一步学习】
【需要调用摄像头,暂时无法实现】
示例代码

//------------------【FLANN结合SURF进行关键点的描述和匹配】----------------------
 
#include<opencv2\features2d\features2d.hpp>
#include<opencv2\core\core.hpp>
#include<highgui\highgui.hpp>
#include<opencv2\nonfree\nonfree.hpp>
#include<opencv2\legacy\legacy.hpp>
#include<iostream>

using namespace cv;
using namespace std;
 
int main()
{
	//【1】载入原图
	Mat srcImage = imread("D://lili/Desktop/jpg/opencv/2.jpg");
	imshow("【原图】", srcImage);     
 
	//【2】对BGR空间的图像直接进行计算很费时间,所以,需要转换为灰度图
	Mat srcGrayImage;
	cvtColor(srcImage, srcGrayImage, CV_BGR2GRAY);
 
	//【3】首先对两幅图像检测SURF关键点、提取测试图像描述符
	vector<KeyPoint> keyPoint1;
	Mat dstImage1, dstImage2;
	int minHessian = 80;
	SURF surf(minHessian);
	//Ptr<SURF> surf = SURF::create(80);
	surf.detect(srcGrayImage, keyPoint1);
	Mat descriImage1;
	surf.compute(srcGrayImage, keyPoint1, descriImage1);
 
	//【4】先对原图的描述子进行保留-------邻近匹配
	//FlannBasedMatcher FLMatcher;
	//因为FlannBasedMatcher类的成员函数add()的参数是一个vector<Mat>的容器,所以先定义一个这样的变量,并将原图的描述子放入容器中
	//vector<Mat> g_vdescriImage1(1, descriImage1);
	//调用FlannBasedMatcher类的成员函数add,将原图的描述子放在FlannBasedMatcher的对象FLMatcher中
	//FLMatcher.add(g_vdescriImage1);c
	//FLMatcher.train();
 
	//【4】进行基于描述符的-------暴力匹配
	BFMatcher matcher;
	//因为FlannBasedMatcher类的成员函数add()的参数是一个vector<Mat>的容器,所以先定义一个这样的变量,并将原图的描述子放入容器中
	vector<Mat> g_vdescriImage1(1, descriImage1);
	//调用FlannBasedMatcher类的成员函数add,将原图的描述子放在FlannBasedMatcher的对象FLMatcher中
	matcher.add(g_vdescriImage1);
	matcher.train();
 
	VideoCapture capture;
	capture.open(0);
 
	Mat frameImage, frameGrayImage;
	while (waitKey(1) != 27)
	{
		capture >> frameImage;
 
		//<1>为了提高计算效率,将图像转换为灰度图像
		cvtColor(frameImage, frameGrayImage, CV_BGR2GRAY);
 
		//<2>检测S关键点、提取测试图像描述符
		vector<KeyPoint> keyPoints2;
		surf.detect(frameGrayImage, keyPoints2);
		Mat descriImage2;
		surf.compute(frameGrayImage, keyPoints2, descriImage2);
 
		//<3>将之前得到的原图的描述子和现在得到的描述子进行匹配(匹配训练和测试描述符)
		//成员函数knnMatch的参数是二维的DMatch向量,所以首先定义一个该容器的向量
		vector<vector<DMatch>> knnDMatches;
		
		//<4>用之前已经存放原图描述子的对象来计算------邻近匹配
		//FLMatcher.knnMatch(descriImage2, knnDMatches, 2);
 
		//<4>用之前已经存放原图描述子的对象来计算------暴力匹配
		matcher.knnMatch(descriImage2, knnDMatches, 2);
		//<5>根据劳氏算法,采集优秀的匹配点
		vector<DMatch> goodMatches;
		for (size_t i = 0; i < knnDMatches.size(); i++)
		{
			if (knnDMatches[i][0].distance < 0.6 * knnDMatches[i][1].distance)
			{
				goodMatches.push_back(knnDMatches[i][0]);
			}
		}
		//<6>绘制匹配点并显示窗口
		Mat dstImage;
		drawMatches(frameImage, keyPoints2, srcImage, keyPoint1, goodMatches, dstImage);
 
		imshow("【结果图】", dstImage);
	}
 
	return 0;
}

运行效果
在这里插入图片描述
在这里插入图片描述

(五)综合示例程序:SIFT配合暴力匹配进行关键点描述和提取

【需要调用摄像头,暂时无法实现】
示例代码

#include<opencv2\features2d\features2d.hpp>
#include<opencv2\core\core.hpp>
#include<highgui\highgui.hpp>
#include<opencv2\nonfree\nonfree.hpp>
#include<opencv2\legacy\legacy.hpp>
#include<iostream>

using namespace cv;
using namespace std;
 
int main()
{
	Mat srcImage = imread("D://lili/Desktop/jpg/opencv/2.jpg");
	imshow("【原图】", srcImage);
 
	//对BGR空间的图像直接进行计算很费时间,所以,需要转换为灰度图
	Mat srcGrayImage;
	cvtColor(srcImage, srcGrayImage, CV_BGR2GRAY);
 
	//首先对两幅图像进行特征点的检测和描述子的计算
	vector<KeyPoint> keyPoint1;
	//这里用SURF会更加快
	SIFT surf(2000);
	surf.detect(srcGrayImage, keyPoint1);
	Mat descriImage1;
	surf.compute(srcGrayImage, keyPoint1, descriImage1);
 
	//先对原图的描述子进行保留
	BFMatcher FLMatcher;
	//因为FlannBasedMatcher类的成员函数add()的参数是一个vector<Mat>的容器,所以先定义一个这样的变量,并将原图的描述子放入容器中
	vector<Mat> g_vdescriImage1(1, descriImage1);
	/*g_vdescriImage1.push_back(descriImage1);*/
	//调用FlannBasedMatcher类的成员函数add,将原图的描述子放在FlannBasedMatcher的对象FLMatcher中
	FLMatcher.add(g_vdescriImage1);
	//...........................................................
	FLMatcher.train();
 
	VideoCapture capture;
	capture.open(0);
 
	Mat frameImage, frameGrayImage;
	while (waitKey(1) != 27)
	{
		capture >> frameImage;
 
		//为了提高计算效率,将图像转换为灰度图像
		cvtColor(frameImage, frameGrayImage, CV_BGR2GRAY);
 
		//计算特征点和描述子
		vector<KeyPoint> keyPoints2;
		surf.detect(frameGrayImage, keyPoints2);
		Mat descriImage2;
		surf.compute(frameGrayImage, keyPoints2, descriImage2);
 
		//将之前得到的原图的描述子和现在得到的描述子进行匹配
		//成员函数knnMatch的参数是二维的DMatch向量,所以首先定义一个该容器的向量
		vector<vector<DMatch>> knnDMatches;
		//用之前已经存放原图描述子的对象来计算
		FLMatcher.knnMatch(descriImage2, knnDMatches, 2);
 
		//采集优秀的匹配点
		vector<DMatch> goodMatches;
		for (size_t i = 0; i < knnDMatches.size(); i++)
		{
			//........................................................................
			if (knnDMatches[i][0].distance < 0.6 * knnDMatches[i][1].distance)
			{
				goodMatches.push_back(knnDMatches[i][0]);
			}
		}
 
		Mat dstImage;
		drawMatches(frameImage, keyPoints2, srcImage, keyPoint1, goodMatches, dstImage);
 
		imshow("【结果图】", dstImage);
	}
 
	return 0;
}

运行效果
在这里插入图片描述

四、寻找已知物体

寻找已知物体:在Flann特征匹配的基础上,还可以进一步利用Homography映射找出已知物体。具体分为两个步骤
(1)使用函数findHomography寻找匹配上的关键点的变换
(2)使用函数perspectiveTransform来映射点

(一)寻找透视变换:findHomography()函数

寻找透视变换:findHomography()函数----找到并返回原图像和目标图像之间的透视变换H

Mat findHomography(
    InputArray srcPoints,   //原平面上的对应点
    InputArray dstPoints,   //目标平面上的对应点
    int method=0,//用于计算单应矩阵的方法(默认0;CV_RANSAC---基于RANSAC的鲁棒性方法;CV_LMEDS---最小中值鲁棒性方法)
    double ransacReprojThreshold=3,//(默认3)处理点对为内围层时,允许重投影误差的最大值
    OutputArray mask=noArray()//可选参数
);

(二)进行透视矩阵变换:perspectiveTransform()函数

进行透视矩阵变换:perspectiveTransform()函数—进行向量透视矩阵变换

void perspectiveTransform(
    InputArray src,   //输入图像
    OutputArray dst,  //目标图像
    InputArray m     //变换矩阵
);

(三)示例程序:寻找已知物体

示例代码

//-------------------【寻找已知物体】---------------
#include<opencv2\features2d\features2d.hpp>
#include<opencv2\core\core.hpp>
#include<highgui\highgui.hpp>
#include<opencv2\nonfree\nonfree.hpp>
#include<opencv2\legacy\legacy.hpp>
#include<iostream>

using namespace cv;
using namespace std;
 
int main()
{
	Mat srcImage1 = imread("D://lili/Desktop/jpg/opencv/2.jpg");
	Mat srcImage2 = imread("D://lili/Desktop/jpg/opencv/16.jpg");
	imshow("【原图1】", srcImage1);
	imshow("【原图2】", srcImage2);
 
	Mat grayImage1, grayImage2;
	cvtColor(srcImage1, grayImage1, CV_BGR2GRAY);
	cvtColor(srcImage2, grayImage2, CV_BGR2GRAY);
 
	//首先对两幅图像进行特征点的检测
	//先准备参数
	vector<KeyPoint> g_vKeyPoint1;
	vector<KeyPoint> g_vKeyPoint2;
	int minHessian = 400;
	SurfFeatureDetector surf(minHessian);
	surf.detect(grayImage1, g_vKeyPoint1);
	surf.detect(grayImage2, g_vKeyPoint2);
 
	//利用得到的特征点计算特征描述子
	//目的:对得到的每个特征点进行特征描述,整合到Mat类型的矩阵中(计算结果是Mat类型的)
	//该得到的结果矩阵的行数就是特征点的个数,因为是对每个点进行描述,所以每行都会有一个描述的字子向量,共同构成Mat矩阵
	Mat descriImage1, descriImage2;
	surf.compute(grayImage1, g_vKeyPoint1, descriImage1);
	surf.compute(grayImage2, g_vKeyPoint2, descriImage2);
 
	//正式开始在两幅图像中进行匹配
	//先得到一个匹配向量
	FlannBasedMatcher FLMatcher;
	vector<DMatch> g_vMatches;
	//g_vMatches就是得到的匹配向量
	FLMatcher.match(descriImage1, descriImage2, g_vMatches);
 
	//用找最大最小值的方式找到 两幅图像中匹配的点的距离的最大值和最小值
	//这里的 keyPoint1.size() 和 descriImage1.rows是一样的值,因为descriImage1的行数就是检测到的特征点的个数
	double minDistance = g_vMatches[0].distance, maxDistance = g_vMatches[0].distance;
	for (size_t i = 0; i < g_vKeyPoint1.size(); i++)
	{
		double currDistance = g_vMatches[i].distance;
		if (currDistance < minDistance)
			minDistance = currDistance;
		if (currDistance > maxDistance)
			maxDistance = currDistance;
	}
 
	//定义一个新的变量,用来存储 通过距离检测后  通过阀值的点
	vector<DMatch> newMatches;
	for (size_t i = 0; i < g_vKeyPoint1.size(); i++)
	{
		if (g_vMatches[i].distance < 2 * minDistance)
			newMatches.push_back(g_vMatches[i]);
	}
 
	//用绘制函数对匹配向量进行绘制
	Mat dstImage;
	drawMatches(srcImage1, g_vKeyPoint1, srcImage2, g_vKeyPoint2, newMatches, dstImage
		, Scalar(theRNG().uniform(0, 255), theRNG().uniform(0, 255), theRNG().uniform(0, 255))
		, Scalar(theRNG().uniform(0, 255), theRNG().uniform(0, 255), theRNG().uniform(0, 255)), Mat(), 2);
 
	imshow("【特征提取后的图像】", dstImage);
 
	//=================================正式开始寻找已知物体============================
	//为了调用 得到H矩阵findHomography函数,所以需要得到 匹配点所对应的特征点   然后作为参数传递给计算H矩阵的函数
	//所以首先是进行 匹配点和对应的特征点的转换步骤
	//将得到的点放入新的容器中,所以需要定义新的容器
	vector<Point2f> g_vSrcPoint2f1;
	vector<Point2f> g_vSrcPoint2f2;
	for (size_t i = 0; i < newMatches.size(); i++)
	{
		g_vSrcPoint2f1.push_back(g_vKeyPoint1[newMatches[i].queryIdx].pt);
		g_vSrcPoint2f2.push_back(g_vKeyPoint2[newMatches[i].trainIdx].pt);
	}
 
	//将得到的对应的特征点  计算H矩阵
	Mat H = findHomography(g_vSrcPoint2f1, g_vSrcPoint2f2, 0);
 
	//用得到的H矩阵  来进行透视矩阵变换  用到的是perspectiveTransform函数
	//vector<Point2f> g_vCorners1(4);
	//vector<Point2f> g_vCorners2(4);
	//g_vCorners1[0] = Point2f(0, 0);
	//g_vCorners1[1] = Point2f((float)srcImage1.cols, 0);
	//g_vCorners1[2] = Point2f((float)srcImage1.cols, (float)srcImage1.rows);
	//g_vCorners1[3] = Point2f(0, (float)srcImage1.rows);
 
	//perspectiveTransform(g_vCorners1, g_vCorners2, H);
 
	//在得到的两幅图像的合成图中绘制检测到的物体的直线
	//line(dstImage, (Point)g_vCorners2[0] + Point(srcImage1.cols, 0), (Point)g_vCorners2[1] + Point(srcImage1.cols, 0)
	//	, Scalar(theRNG().uniform(0, 255), theRNG().uniform(0, 255), theRNG().uniform(0, 255)), 2);
	//line(dstImage, (Point)g_vCorners2[1] + Point(srcImage1.cols, 0), (Point)g_vCorners2[2] + Point(srcImage1.cols, 0)
	//	, Scalar(theRNG().uniform(0, 255), theRNG().uniform(0, 255), theRNG().uniform(0, 255)), 2);
	//line(dstImage, (Point)g_vCorners2[2] + Point(srcImage1.cols, 0), (Point)g_vCorners2[3] + Point(srcImage1.cols, 0)
	//	, Scalar(theRNG().uniform(0, 255), theRNG().uniform(0, 255), theRNG().uniform(0, 255)), 2);
	//line(dstImage, (Point)g_vCorners2[3] + Point(srcImage1.cols, 0), (Point)g_vCorners2[0] + Point(srcImage1.cols, 0)
	//	, Scalar(theRNG().uniform(0, 255), theRNG().uniform(0, 255), theRNG().uniform(0, 255)), 2);
 
	//imshow("【检测物体后的图像】", dstImage);
 
	//进行角点检测
	//开始进行强角点检测
	//先配置需要的函数参数
	vector<Point2f> dstPoint2f1;
	goodFeaturesToTrack(grayImage1, dstPoint2f1, 200, 0.01, 10, Mat(), 3);
	vector<Point2f> dstPoint2f2(dstPoint2f1.size());
	//进行透视变换
	perspectiveTransform(dstPoint2f1, dstPoint2f2, H);
 
	//在计算得到的点中寻找最小包围矩形
	//rectPoint变量中得到了矩形的四个顶点坐标
	RotatedRect rectPoint = minAreaRect(dstPoint2f2);
	//定义一个存储以上四个点的坐标的变量
	Point2f fourPoint2f[4];
	//将rectPoint变量中存储的坐标值放到 fourPoint的数组中
	rectPoint.points(fourPoint2f);
 
	//根据得到的四个点的坐标  绘制矩形
	for (int i = 0; i < 3; i++)
	{
		line(srcImage2, fourPoint2f[i], fourPoint2f[i + 1]
			, Scalar(theRNG().uniform(0, 255), theRNG().uniform(0, 255), theRNG().uniform(0, 255)), 3);
	}
	line(srcImage2, fourPoint2f[0], fourPoint2f[3]
		, Scalar(theRNG().uniform(0, 255), theRNG().uniform(0, 255), theRNG().uniform(0, 255)), 3);
 
	imshow("【检测到的物体】", srcImage2);
 
	waitKey(0);
 
	return 0;
}

运行效果
在这里插入图片描述

在这里插入图片描述

五、ORB特征提取

(一)ORB算法概述

ORB是brief算法的改进版。ORB算法综合性能在各种测评里相较于其他特征提取算法是最好的。

(二)相关概念认知

1、关于Brief描述子

主要思路:在特征点附近随机选取若干点对,将这些点对的灰度值的大小,组合成一个二进制串,并将这个二进制串作为该特征点的特征描述子
Brief优点在于速度,缺点在于:不具备旋转不变性;对噪声敏感;不具备尺寸不变性。
为了解决上述缺点中的1和2提出了一种新概念:ORB算法。

2、关于尺寸不变性

ORB没有试图解决尺寸不变性,一般应用在实时视频处理中,可以通过跟踪还有一些启发式的策略来解决尺寸不变性的问题。

3、关于计算速度

经统计,ORB算法的执行速度是SIFT的100倍,是SURF的10倍

(三)ORB类相关源码简单分析

源码路径:…\opencv\buld\include\opencv2\features2d\features2d.hpp
(感兴趣的自行查看)
可以发现ORB,OrbFeatureDetector,OrbDescriptorExtractor这三个类是完全等价的。而且ORB类同样继承自Feature2D类

class CV_EXPORTS_W ORB : public Feature2D{
	//......
}

类关系图
在这里插入图片描述

(四)示例程序:ORB算法描述与匹配

ORB的关键点和描述符的提取,采用摄像头获取待检测图像,使用FLANN-LSH进行匹配。
【需要调用摄像头,暂时无法实现】
示例代码

//------------------【ORB】--------------------------
 
#include<opencv2/opencv.hpp>
#include<iostream>
#include<vector>
 
using namespace cv;
using namespace std;
 
int main()
{
	Mat srcImage = imread("D://lili/Desktop/jpg/opencv/2.jpg");
	imshow("【原图】", srcImage);
 
	//对BGR空间的图像直接进行计算很费时间,所以,需要转换为灰度图
	Mat srcGrayImage;
	cvtColor(srcImage, srcGrayImage, CV_BGR2GRAY);
 
	//首先对两幅图像进行特征点的检测和描述子的计算
	vector<KeyPoint> keyPoint1;
	int minHessian = 400;
	OrbFeatureDetector orb(minHessian);

	//调用detect函数检测出特征关键点,保存在vector中
	orb.detect(srcGrayImage, keyPoint1);
	Mat descriImage1;
	//计算描述符(特征向量)
	orb.compute(srcGrayImage, keyPoint1, descriImage1);
 
	//基于FLANN的描述符对象匹配
	flann::Index flannIndex(descriImage1, flann::LshIndexParams(12, 20, 2), cvflann::FLANN_DIST_HAMMING);
	
	//初始化视屏采集对象
	VideoCapture capture;
	capture.open(0);
	capture.set(CV_CAP_PROP_FRAME_WIDTH, 360);//设置采集视频的宽度
	capture.set(CV_CAP_PROP_FRAME_HEIGHT, 900);//设置采集视频的高度
 
	Mat frameImage, frameGrayImage;
 
	while (waitKey(1) != 27)
	{
		capture >> frameImage;
 
		//为了提高计算效率,将图像转换为灰度图像
		cvtColor(frameImage, frameGrayImage, CV_BGR2GRAY);
 
		//计算特征点和描述子
		vector<KeyPoint> keyPoints2;
		orb.detect(frameGrayImage, keyPoints2);
		Mat descriImage2;
		orb.compute(frameGrayImage, keyPoints2, descriImage2);
 
		//匹配和测试描述符,获取两个最临近的描述符
		Mat matchIndex(descriImage2.rows, 2, CV_32SC1);
		Mat matchDistance(descriImage2.rows, 2, CV_32SC1);
 
		//调用k邻近算法
		flannIndex.knnSearch(descriImage2, matchIndex, matchDistance, 2, flann::SearchParams());
 
 
		//采集优秀的匹配点(根据劳氏算法)
		vector<DMatch> goodMatches;
		for (int i = 0; i < matchDistance.rows; i++)
		{
			//........................................................................
			if (matchDistance.at<float>(i, 0) < 0.6 * matchDistance.at<float>(i, 1))
			{
				DMatch midDMatch(i, matchIndex.at<int>(i, 0), matchDistance.at<float>(i, 0));
				goodMatches.push_back(midDMatch);
			}
		}
 
		Mat dstImage;
		drawMatches(frameImage, keyPoints2, srcImage, keyPoint1, goodMatches, dstImage);
 
		imshow("【结果图】", dstImage);
	}
 
	return 0;
}

运行效果
【没有摄像头看不了效果】
书上运行效果
在这里插入图片描述

《Opencv3编程入门》学习笔记到这里就结束啦!完结撒花
在这里插入图片描述
静候下一个学习系列吧!喜欢的可以关注伍六琪,持续更新包括但不限于以下内容:计算机视觉,图像处理,深度学习,计算机相关小技巧,各种软件安装包插件使用教程破解教程…
大家一起共同进步,共同学习吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/703088.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于matlab使用点要素匹配在杂乱场景中检测对象(附源码)

一、前言 此示例演示如何在给定对象的参考图像的情况下检测杂乱场景中的特定对象。 此示例提供了一种基于查找参考和目标图像之间的点对应关系来检测特定对象的算法。它可以检测物体&#xff0c;尽管刻度变化或面内旋转。它对少量的面外旋转和遮挡也很鲁棒。这种对象检测方法…

Ansible练习

部署ansible练习 开始之前先使用student用户登录 登录命令&#xff1a;ssh studentworkstation 在workstation上运行lab deploy-review start命令&#xff0c;此脚本将确保受管主机在网络上访问。 然后开始验证控制节点上是否安装了ansible软件包&#xff0c;在运行anisble -…

Shell脚本编程教程

Shell脚本编程 1.Shell脚本语言的基本结构 1.1 Shell脚本的用途&#xff1a; 自动化常用命令执行系统管理和故障排除创建简单的应用程序处理文本或文件 1.2 Shell脚本基本结构&#xff1a; ​ Shell脚本编程&#xff1a;是基于过程式&#xff0c;解释执行的语言 编程语言…

【C#】简单聊下Framework框架下的事务

框架用的多了&#xff0c;之前版本的事务都忘记了。本次简单聊下.net framework 4.8框架下本身的事务 目录 1、SqlClient2、TransactionScope3、引用 1、SqlClient 在 C# 中&#xff0c;使用 using 块可以方便地实现对资源的自动释放&#xff0c;但它不适用于实现事务处理。为…

Redis实践篇(二)优惠卷秒杀 一人一单、分布锁

目录 全局ID生成器​编辑 实现优惠卷下单 优惠卷超卖问题 乐观锁 一人一单 分布式锁 分布锁的实现 基于Redis的分布锁 Redis的Lua脚本 再次改进Redis的分布锁 基于Redis的分布锁优化 Redisson分布式框架 引入依赖 Redisson可重入锁原理 Redisson分布锁原理​编辑 全局I…

基于Hadoop的疫情信息分析与可视化研究——包含大屏可视化及预测算法

需要本项目的全套环境、代码、文档、资源、数据和部署调试的私信博主&#xff01;&#xff01;&#xff01; 本研究基于中国新冠疫情2020-01-11至2022-12-20的全国整体数据进行疫情大数据分析&#xff0c;通过对历史的数据进行大数据分析&#xff0c;可以有效的掌握过去疫情数据…

4、JAVA 嵌套for循环 while do-while

1 嵌套for循环 1.1 概述 存在至少2层for循环,根据外层的条件&#xff0c;判断里层能否执行 如果能执行&#xff0c;就把里层代码都循环完毕后&#xff0c;再继续判断是否执行外层循环的下一次循环 1.2 嵌套for形式 1.3 练习&#xff1a;嵌套for循环入门案例 创建包: cn.tedu…

Jupyter notebook安装运行(详解)

目录 Jupyter notebook 概念 官方文档 特点 使用Anaconda安装 使用pip安装 运行Jupyter Notebook 指定端口启动 Jupyter notebook修改主目录 Jupyter notebook 概念 Jupyter Notebook是基于网页的用于交互计算的应用程序。其可被应用于全过程计算&#xff1a;开发…

Dlib —— 对图片进行人脸检测(附源码)

效果 代码 Vs2017下使用Dlib检测人脸&#xff0c;并通过OpenCv将结果绘制出来。&#xff08;由于Dlib库已编译好&#xff0c;Vs工程环境自行搭建&#xff0c;OPenCv环境参考本人之前的专栏文章&#xff09; #include <iostream>#include <dlib/image_processing/fron…

生产环境使用HBase,你必须知道的最佳实践

需要关注的一些最佳实践经验。 **Schema设计七大原则 ** 1&#xff09;每个region的大小应该控制在10G到50G之间&#xff1b; 2&#xff09;一个表最好保持在 50到100个 region的规模&#xff1b; 3&#xff09;每个cell最大不应该超过10MB&#xff0c;如果超过&#xff0c;…

一次性供应商是否可以创建采购信息记录?

近期有读者提出这个问题。我的第一反应就是&#xff0c;为什么自己不试一下呢&#xff1f;如果不能&#xff0c;系统应该会有提示。不过反正我也好久没写了&#xff0c;找篇素材测试下&#xff0c;写写也好。 自行测试一下&#xff0c;在事务ME11中创建信息记录&#xff0c;选择…

基于java的助农在线商城的设计与实现(源码+展示视频+文档+报告)

电商助农受到了广泛的关注&#xff0c;已成为新时期农产品销售的主要发展路径。推进“互联网农业”的深入发展&#xff0c;是促进农业现代化发展的关键。助农在线商城基于Springboot框架Mysql数据库实现&#xff0c;以Jdk1.8Tomcat8为开发环境&#xff0c;实现一个基于Java开发…

【离群点检测算法】离群点|异常值 检测算法——局部离群因子LOF算法

every blog every motto: You can do more than you think. https://blog.csdn.net/weixin_39190382?typeblog 0. 前言 离群点检测&#xff0c;理解起来也比较容易。 同学都考70分&#xff0c;你也考70分&#xff0c;可以。 同学都考90分&#xff0c;你考70分&#xff0c;不…

算法笔记——排序算法

&#x1f44c;&#xff0c;begin&#xff1a; 排序算法很重要&#xff0c;它可以使数据按照一定的规律进行排序&#xff0c;各个语言的代码都有自己的排序函数&#xff0c;那么排序到底有哪几种方法&#xff0c;✌&#xff0c;如下&#xff1a; 按照效率分类如上图&#xff1a…

为什么要提前报考CSPM项目管理专业人员能力评价

2021年10月&#xff0c;中共中央、国务院发布的《国家标准化发展纲要》明确提出构建多层次从业人员培养培训体系&#xff0c;开展专业人才培养培训和国家质量基础设施综合教育。建立健全人才的职业能力评价和激励机制。由中国标准化协会&#xff08;CAS&#xff09;组织开展的项…

《移动互联网技术》第八章 消息与服务:掌握不同类型广播监听方式,以及创建通知的方法

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

k8s中kubectl陈述式资源管理

陈述式管理资源的方法 1&#xff0c;陈述时资源管理集群资源的唯一入口是通过相应的方法调用的apiserver的接口 2&#xff0c;kubectl 是官方的ctl命令&#xff0c;用于与 apiserver 进行通信&#xff0c;将用户在命令行输入的命令&#xff0c;组织并转化为 apiserver 能识别…

基于Python+MySQL所写的智慧校园考试系统设计

点击以下链接获取源码资源&#xff1a; https://download.csdn.net/download/qq_64505944/87971718?spm1001.2014.3001.5503 《智慧校园考试系统》程序使用说明 在虚拟环境下输入命令“python manage.py runserver”启动项目&#xff0c;然后&#xff0c;访问“http://127.0.…

java之static关键字

本文是根据沉默王二前辈所发的一篇博客中所学到的 教妹学Java(二十六)&#xff1a;static 关键字解析_java中static_沉默王二的博客-CSDN博客 1&#xff1a;static关键字比较难以理解&#xff0c;借用沉默王二前辈的一句话就是&#xff1a;方便在没有创建对象的情况下进行调用…

SpirngBoot测试

一、依赖 <spring-boot.version>2.4.2</spring-boot.version> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>…