opencv的sift特征匹配图形
概述
该文章是介绍在c/c++,使用opencv的扩展库(opencv_contrib)sift 中的特征提取来实现根据目标图片的提取。
效果演示
- 演示素材

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

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

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

实现步骤
- 包含外部库的头文件
#include <opencv2/opencv.hpp>
#include <opencv2/features2d.hpp>
- 提取匹配图片和被目标图片的目标特征(特征描述子)
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
- 根据提取的匹配图片和被目标图片的特征,进行目标匹配
// 进行特征匹配 (使用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);
}
- 对匹配的特征值进一步过滤,减少匹配数量
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);
- 根据上述的匹配的特征点,进行单应性矩阵计算
// 计算单应性矩阵
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);
- 根据匹配出的单应性矩阵绘制匹配框,即演示效果

特征点的存储到文件
- 特征文件的保存类(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)

浙公网安备 33010602011771号