opencv的sift特征匹配图形

概述

         该文章是介绍在c/c++,使用opencv的扩展库(opencv_contrib)sift 中的特征提取来实现根据目标图片的提取。

效果演示

  • 演示素材
    匹配测的测试素材

  • 演示效果1(在all_hero.png 中 找到js.png的区域,并绘制出矩形框)
    测试颜色1

  • 演示效果2(在all_hero.png 中 找到zx.png的区域,并绘制出矩形框)
    测试演示2

  • 演示效果3(在all_hero.png 中 找到kerma.png的区域,并绘制出矩形框)

测试演示3

实现步骤

  1. 包含外部库的头文件
#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
  1. 提取匹配图片和被目标图片的目标特征(特征描述子)
std::string path1 = "E:/code/cmake/opencvFeatureTest/CMakeProject1/res/kerma.png";
std::string path2 = "E:/code/cmake/opencvFeatureTest/CMakeProject1/res/all_hero.png";
cv::Mat src1 = cv::imread(path1);
cv::Mat src2 = cv::imread(path2);
imshow("all_hero", src2);
Ptr<cv::SIFT> sift = cv::SIFT::create();
std::vector<cv::KeyPoint> keypoints1, keypoints2;	// 关键点
cv::Mat descriptors1, descriptors2;					// 描述符
#if 1
	// 进行特征提取
	sift->detectAndCompute(src1, cv::noArray(), keypoints1, descriptors1);
	sift->detectAndCompute(src2, cv::noArray(), keypoints2, descriptors2);
	std::cout << "PNG 1 KeyPoints count: " << keypoints1.size() << std::endl;
	std::cout << "PNG 2 KeyPoints count: " << keypoints2.size() << std::endl;

	// 保存模板文件的的特征点到文件
	FeaturesConfig config("E:\\code\\cmake\\opencvFeatureTest\\CMakeProject1\\FeaturesConfig");
	config.saveFeatures("zx", keypoints1, descriptors1);
#else
	sift->detectAndCompute(src2, cv::noArray(), keypoints2, descriptors2);
	// 读取模板文件的的特征点到上述的特征点 及 描述符
	FeaturesConfig config("E:\\code\\cmake\\opencvFeatureTest\\CMakeProject1\\FeaturesConfig");
	config.loadFeatures("E:\\code\\cmake\\opencvFeatureTest\\CMakeProject1\\FeaturesConfig\\zx_features_config.bin", keypoints1, descriptors1);
#endif
  1. 根据提取的匹配图片和被目标图片的特征,进行目标匹配
// 进行特征匹配 (使用FLANN匹配器 速度比暴力匹配快)
#if 0	// FlannBasedMatcher
	cv::Ptr<cv::DescriptorMatcher> matcher = cv::DescriptorMatcher::create(cv::DescriptorMatcher::FLANNBASED);
#else
	cv::Ptr<cv::DescriptorMatcher> matcher = BFMatcher::create(NORM_L2);
#endif
	std::vector<cv::DMatch> matches; // 匹配结果
	matcher->match(descriptors1, descriptors2, matches);
if (1)
{
	// 绘制匹配结果
	cv::Mat img_matches;
	cv::drawMatches(src1, keypoints1, src2, keypoints2, matches, img_matches);
	cv::imshow("match", img_matches);
}
  1. 对匹配的特征值进一步过滤,减少匹配数量
if (1)
{
	// 筛选匹配的结果 保留较好的匹配 
	// 计算最小距离 用于筛选阈值
	double min_dist=1000, max_dist = 0;
	for (int i = 0; i < descriptors1.rows; i++)
	{
		double dist= matches[i].distance;
		if(dist<min_dist) min_dist = dist;
			if (dist > max_dist) max_dist = dist;
	}
	std::cout << "最小距离:" << min_dist << ",最大距离:" << max_dist << std::endl;

	// 保留较好的匹配
	std::vector<cv::DMatch> good_matches;
	for (int i = 0; i < descriptors1.rows; i++)
	{
		if (matches[i].distance <= 3 * min_dist)
		{
			good_matches.push_back(matches[i]);
		}
	}
}

	std::cout<<"筛选后的匹配结果数量:" << good_matches.size() << std::endl;
	// 绘制筛选后的匹配结果
	cv::Mat math_img;
	drawMatches(src1,keypoints1,src2,keypoints2,good_matches,math_img
	, Scalar::all(-1), Scalar::all(-1),
	std::vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
	cv::imshow("match", math_img);
  1. 根据上述的匹配的特征点,进行单应性矩阵计算
// 计算单应性矩阵
cv::Mat H=findHomography(points1, points2, RANSAC);

// 定义模板图像的边界(目标区域,假设整个img1都是目标)
std::vector<Point2f> corners(4);
corners[0] = Point2f(0, 0);
corners[1]=Point2f(src1.cols, 0);
corners[2] = Point2f(src1.cols, src1.rows);
corners[3] = Point2f(0, src1.rows);

// 将模板边界框 映射到场景图中 得到目标区域
std::vector<Point2f> scene_corners(4);
perspectiveTransform(corners, scene_corners, H);

// 绘制目标区域
cv::Mat resule = src2.clone();
for (int i = 0; i < 4; i++) {
	line(resule, scene_corners[i], scene_corners[(i + 1) % 4], Scalar(0, 255, 0), 2);
}
imshow("result", resule);
  1. 根据匹配出的单应性矩阵绘制匹配框,即演示效果 匹配测的测试素材

特征点的存储到文件

  1. 特征文件的保存类(FeaturesConfig)声明
#ifndef FEATUERS_CONFIG_H
#define FEATUERS_CONFIG_H
#include <fstream>
#include <vector>
#include "opencv2/opencv.hpp"

// 特征点保存配置文件(.bin)
#define FEATURE_CONFIG_FILE "_features_config.bin"

class FeaturesConfig
{
public:
	FeaturesConfig(std::string dirPath);
	~FeaturesConfig();
	/// <summary>
	/// 读取特征点配置文件
	/// </summary>
	/// <param name="filename">文件名</param>
	/// <param name="keypoints">特征点</param>
	/// <param name="descriptors">特征描述符</param>
	/// <returns></returns>
	bool saveFeatures(const std::string& filename,
		const std::vector<cv::KeyPoint>& keypoints,
		const cv::Mat& descriptors);
	/// <summary>
	/// 读取特征点配置文件
	/// </summary>
	/// <param name="filename">文件名</param>
	/// <param name="keypoints">特征点</param>
	/// <param name="descriptors">特征描述符</param>
	/// <returns></returns>
	bool loadFeatures(const std::string& filename,
        		std::vector<cv::KeyPoint>& keypoints,
        		cv::Mat& descriptors);

private:
	std::string m_dirPath;	// 设置特征文件夹的路径

private:
	/// <summary>
	/// 判断文件夹是否存在
	/// </summary>
	/// <param name="dirPath"></param>
	/// <returns></returns>
	bool foldedrExists(const std::string& dirPath);

	/// <summary>
	/// 创建文件夹(跨平台)
	/// </summary>
	/// <param name="path"></param>
	/// <returns></returns>
	bool createFolder(const std::string& path);

	/// <summary>
	/// // 检查并创建文件夹(支持单级目录)
	/// </summary>
	/// <param name="path"></param>
	/// <returns></returns>
	bool checkAndCreateFolder(const std::string& path);

	/// <summary>
	/// 判断字符是否为路径分隔符
	/// </summary>
	/// <param name="c"></param>
	/// <returns></returns>
	bool isPathSeparator(char c);

	/// <summary>
	/// 拼接文件夹路径和文件名(自动补充分隔符)
	/// </summary>
	/// <param name="folder"></param>
	/// <param name="filename"></param>
	/// <returns></returns>
	std::string combinePath(const std::string& folder, const std::string& filename);
};
#endif // !FEATUERS_CONFIG_H
```c++

2. 特征文件的保存类(FeaturesConfig)实现

```c++
#include "features_config.h"

#include<direct.h> // 用于Windows系统的目录操作
// 跨平台处理 stat 结构体和函数
#ifdef _WIN32
#include <sys/stat.h>  // Windows 下包含 _stat 声明
#else
#include <sys/stat.h>  // 类 Unix 系统的 stat
#endif

FeaturesConfig::FeaturesConfig(std::string dirPath): m_dirPath(dirPath)
{
	checkAndCreateFolder(dirPath);
}

FeaturesConfig::~FeaturesConfig()
{
}


bool FeaturesConfig::saveFeatures(const std::string& filename, const std::vector<cv::KeyPoint>& keypoints, const cv::Mat& descriptors)
{
	std::string path=combinePath(m_dirPath,filename);
	path.append(FEATURE_CONFIG_FILE);

	std::ofstream file(path, std::ios::binary);
	if (!file.is_open()) {
		std::cerr << "无法打开文件: " << path << std::endl;
		return false;
	}

	// 1. 存储关键点数量
	size_t kp_count = keypoints.size();
	file.write(reinterpret_cast<const char*>(&kp_count), sizeof(kp_count));

	// 2. 存储每个关键点信息(keyPoint的成员:angle,class_id,ctave, pt.x, pt.y, response, size)
	for (const auto& kp : keypoints)
	{
		file.write(reinterpret_cast<const char*>(&kp.angle), sizeof(kp.angle));
 		file.write(reinterpret_cast<const char*>(&kp.class_id), sizeof(kp.class_id));
			 file.write(reinterpret_cast<const char*>(&kp.octave), sizeof(kp.octave));
			 file.write(reinterpret_cast<const char*>(&kp.pt.x), sizeof(kp.pt.x));
			 file.write(reinterpret_cast<const char*>(&kp.pt.y), sizeof(kp.pt.y));
		file.write(reinterpret_cast<const char*>(&kp.response), sizeof(kp.response));
		file.write(reinterpret_cast<const char*>(&kp.size), sizeof(kp.size));
	}

	// 3. 存储描存储描述符矩阵(Mat 的rows, clos,type ,以及数据)
	int rows=descriptors.rows;
	int cols = descriptors.cols;
	int type = descriptors.type();
	file.write(reinterpret_cast<const char*>(&rows), sizeof(rows));
	file.write(reinterpret_cast<const char*>(&cols), sizeof(cols));
	file.write(reinterpret_cast<const char*>(&type), sizeof(type));

	if(!descriptors.empty())
		file.write(reinterpret_cast<const char*>(descriptors.data), rows*cols * descriptors.elemSize());

	file.close();
	return true;
}

bool FeaturesConfig::loadFeatures(const std::string& filename, std::vector<cv::KeyPoint>& keypoints, cv::Mat& descriptors)
{
	std::ifstream file(filename, std::ios::binary);
	if (!file.is_open()) {
		std::cerr<<"无法打开特征文件:"<<filename<<std::endl;
		return false;
	}

	keypoints.clear();
	descriptors.release();

	// 1. 读取关键点数量
	size_t kp_cout;
	file.read(reinterpret_cast<char*>(&kp_cout), sizeof(kp_cout));

	// 2.读取每个关键点信息
	for (int i = 0; i < kp_cout; ++i) {
		cv::KeyPoint kp;
		file.read(reinterpret_cast<char*>(&kp.angle), sizeof(kp.angle));
		file.read(reinterpret_cast<char*>(&kp.class_id), sizeof(kp.class_id));
		file.read(reinterpret_cast<char*>(&kp.octave), sizeof(kp.octave));
		file.read(reinterpret_cast<char*>(&kp.pt.x), sizeof(kp.pt.x));
		file.read(reinterpret_cast<char*>(&kp.pt.y), sizeof(kp.pt.y));
		file.read(reinterpret_cast<char*>(&kp.response), sizeof(kp.response));
		file.read(reinterpret_cast<char*>(&kp.size), sizeof(kp.size));
		keypoints.push_back(kp);
	}

	// 3. 读取描述符矩阵
	int row, col, type;
	file.read(reinterpret_cast<char*>(&row), sizeof(row));
	file.read(reinterpret_cast<char*>(&col), sizeof(col));
	file.read(reinterpret_cast<char*>(&type), sizeof(type));
	if (row > 0 && col > 0)
	{
		descriptors = cv::Mat(row, col, type);
		file.read(reinterpret_cast<char*>(descriptors.data), row * col * descriptors.elemSize());
	}
	file.close();
	return true;
}
bool FeaturesConfig::foldedrExists(const std::string& dirPath)
{
#ifdef _WIN32
	struct _stat64i32 info;  // Windows 64位环境下的结构体(匹配 _stat 返回类型)
	int statResult = _stat(dirPath.c_str(), &info);  // Windows 专用 _stat 函数
#else
	struct stat info;  // 类 Unix 系统的 stat 结构体
	int statResult = stat(path.c_str(), &info);   // 类 Unix 系统的 stat 函数
#endif

	// 检查调用是否成功,且是否为目录
	if (statResult == 0) {
#ifdef _WIN32
		return (info.st_mode & _S_IFDIR) != 0;  // Windows 目录标志位
#else
		return (info.st_mode & S_IFDIR) != 0;   // 类 Unix 目录标志位
#endif
	}
	return false;
}

bool FeaturesConfig::createFolder(const std::string& path)
{
#ifdef _WIN32
	// Windows 下创建目录(_mkdir 返回 0 表示成功)
	int result = _mkdir(path.c_str());
#else
	// 类 Unix 下创建目录(权限 0755,返回 0 表示成功)
	int result = mkdir(path.c_str(), 0755);
#endif
	return result == 0;
}

bool FeaturesConfig::checkAndCreateFolder(const std::string& path)
{
	if (foldedrExists(path)) {
		std::cout << "文件夹已存在: " << path << std::endl;
		return true;
	}
	if (createFolder(path)) {
		std::cout << "文件夹创建成功: " << path << std::endl;
		return true;
	}
	else {
		std::cerr << "文件夹创建失败: " << path << std::endl;
		return false;
	}
}

// 判断字符是否为路径分隔符
inline bool FeaturesConfig::isPathSeparator(char c) {
#ifdef _WIN32
    return c == '/' || c == '\\';  // Windows 支持 / 和 \
    		#else
    return c == '/';               // 类 Unix 只支持 /
#endif
}

// 拼接文件夹路径和文件名(自动补充分隔符)

inline std::string FeaturesConfig::combinePath(const std::string& folder, const std::string& filename) {
    if (folder.empty()) {
        return filename;  // 文件夹为空,直接返回文件名
    }
    if (filename.empty()) {
        return folder;    // 文件名为空,直接返回文件夹
    }

    // 检查文件夹末尾是否已有分隔符
    if (isPathSeparator(folder.back())) {
        return folder + filename;  // 已有分隔符,直接拼接
    }
    else {
#ifdef _WIN32
        return folder + "\\" + filename;  // Windows 补 \
        			#else
        return folder + "/" + filename;   // 类 Unix 补 /
#endif
    }
}
```c++

# 相似度匹配(不准确,找到位置后,都是0.1左右,优化后也才0.4),正常来说找到并绘制出框,相似度至少也是0.7以上
```c++
// 计算相似度 ,即计算匹配点占整个关键点的比例 匹配好的数量点/总特征点的数量(取两张图较少的一方)、
#if 0
	int total_keypoints = std::min(keypoints1.size(), keypoints2.size());
	double match_ratio = (double)good_matches.size() / total_keypoints;
#else
	int total_keypoints = keypoints1.size();
	double match_ratio = (double)good_matches.size() / total_keypoints;
#endif
	std::cout << "Match ratio: " << match_ratio << std::endl;
```c++
# 总结
上述实现了基于1张图的特征点,从原图中可以找到对应位置。同时可把特征点提取到文件并打上标签,就可实现对应图能显示对应的标签文字信息等。但是不足的是,相似度不高。而且特征提取不同的图像大小提取的速率不一样,上面要匹配的图(zx.png,js.png,kerma.png)基本一张图要测试要40ms,但是all_hreo.png的特征提取要200多ms。可能换种时时识别的特征提取速率上会好点

# 资源下载
1. 编译好的扩展库下载(v4.1.30)
[链接](https://blog.csdn.net/2504_93486213/article/details/154292907?sharetype=blogdetail&sharerId=154292907&sharerefer=PC&sharesource=2504_93486213&spm=1011.2480.3001.8118)
posted @ 2025-11-09 13:01  test_lqf  阅读(27)  评论(0)    收藏  举报