用了OpenCV的AKAZE和ORB算法来追踪给定视频中的目标对象
这段代码是一个简单的视觉追踪系统,使用了OpenCV的AKAZE和ORB算法来追踪给定视频中的目标对象。主要包含一下几个部分:
Tracker
类:这是核心的追踪类,包括下面重要的方法;
-
setFirstFrame
: 这个方法设置了第一帧图像及目标的边界包围框。它先根据给定的边界框,得到掩膜图像,然后提取特征点和特征描述符。 -
process
: 这个方法处理每一帧图像,提取其特征点及描述符,并与第一帧进行匹配、模型拟合,得到当前帧的目标边界包围框。
main
函数:主函数首先进行初始化工作,比如解析命令行参数,创建检测器(AKAZE和ORB)和匹配器,初始化两个Tracker。然后进入一个循环中,处理每一帧图像,每10帧更新一次统计信息,最后打印整体的统计信息。
cpp
#include <opencv2/features2d.hpp> // 导入OpenCV特征检测库
#include <opencv2/videoio.hpp> // 导入OpenCV视频输入输出库
#include <opencv2/imgproc.hpp> // 导入OpenCV图像处理库
#include <opencv2/calib3d.hpp> // 导入OpenCV相机标定和三维重建库
#include <opencv2/highgui.hpp> // 导入OpenCV显示图像的库
#include <vector> // 导入标准模版库中的向量库
#include <iostream> // 导入输入输出流库
#include <iomanip> // 导入输入输出流操作库
#include "stats.h" // 导入统计结构体的定义
#include "utils.h" // 导入绘图和打印功能的工具库
using namespace std; // 使用标准命名空间
using namespace cv; // 使用OpenCV命名空间
// 定义一些常量参数
const double akaze_thresh = 3e-4; // AKAZE检测阈值, 用于定位大约1000个关键点 控制AKAZE算法的特征点检测敏感度。
const double ransac_thresh = 2.5f; // RANSAC内点阈值
const double nn_match_ratio = 0.8f; // 最近邻匹配比率
const int bb_min_inliers = 100; // 绘制边界框的最少内点数
const int stats_update_period = 10; // 每10帧更新一次屏幕上的统计信息
// 定义example命名空间下的Tracker类
namespace example {
class Tracker
{
public:
Tracker(Ptr<Feature2D> _detector, Ptr<DescriptorMatcher> _matcher) :
detector(_detector),
matcher(_matcher)
{}
// 设置视频的第一帧以及目标边界框
void setFirstFrame(const Mat frame, vector<Point2f> bb, string title, Stats& stats);
// 处理视频的每一帧
Mat process(const Mat frame, Stats& stats);
// 获取特征检测器
Ptr<Feature2D> getDetector() {
return detector;
}
protected:
Ptr<Feature2D> detector; // 特征检测器指针
Ptr<DescriptorMatcher> matcher; // 描述子匹配器指针
Mat first_frame, first_desc; // 第一帧和它的描述子
vector<KeyPoint> first_kp; // 第一帧中的关键点
vector<Point2f> object_bb; // 目标的边界框
};
// setFirstFrame 函数实现
void Tracker::setFirstFrame(const Mat frame, vector<Point2f> bb, string title, Stats& stats)
{
// 根据边界框创建掩码
// 使用new关键字动态创建一个Point类型的数组,大小为bb容器的大小
cv::Point *ptMask = new cv::Point[bb.size()];
// 创建指向Point数组的指针数组,准备传递给绘图函数
const Point* ptContain = { &ptMask[0] };
// 计算bb容器大小,并转换为int类型
int iSize = static_cast<int>(bb.size());
// 遍历bb容器中的所有Point2f点
for (size_t i=0; i<bb.size(); i++) {
// 将每个Point2f点转换为Point,并赋值给前面创建的Point数组
ptMask[i].x = static_cast<int>(bb[i].x);
ptMask[i].y = static_cast<int>(bb[i].y);
}
first_frame = frame.clone(); // 克隆帧到first_frame
cv::Mat matMask = cv::Mat::zeros(frame.size(), CV_8UC1); // 创建和第一帧同样大小的掩码Mat
cv::fillPoly(matMask, &ptContain, &iSize, 1, cv::Scalar::all(255)); // 将所选区域填充为255
// 用掩码检测特征点并计算描述子
detector->detectAndCompute(first_frame, matMask, first_kp, first_desc);
stats.keypoints = (int)first_kp.size(); // 更新关键点统计信息
drawBoundingBox(first_frame, bb); // 绘制边界框
putText(first_frame, title, Point(0, 60), FONT_HERSHEY_PLAIN, 5, Scalar::all(0), 4); // 在图像上放置标题
object_bb = bb; // 保存边界框顶点向量
delete[] ptMask; // 删除临时创建的掩码数组
}
// process函数实现
Mat Tracker::process(const Mat frame, Stats& stats)
{
TickMeter tm; // 创建时间计量器
vector<KeyPoint> kp; // 存储关键点
Mat desc; // 存储描述子
tm.start(); // 开始计时
// 检测特征点并计算描述子
detector->detectAndCompute(frame, noArray(), kp, desc);
stats.keypoints = (int)kp.size();//完整图像特征点数
vector< vector<DMatch> > matches; // 存储匹配结果
vector<KeyPoint> matched1, matched2; // 存储匹配的关键点
// 匹配描述子
matcher->knnMatch(first_desc, desc, matches, 2);
for(unsigned i = 0; i < matches.size(); i++) {
// 使用最近邻距离比率过滤错误匹配
if(matches[i][0].distance < nn_match_ratio * matches[i][1].distance) {
// 如果第一对的距离小于第二对的距离乘以一个比例因子(最近邻匹配比率)
// 则将该匹配的关键点push进matched1和matched2
matched1.push_back(first_kp[matches[i][0].queryIdx]); // 将query image的匹配点加到matched1
matched2.push_back(kp[matches[i][0].trainIdx]); // 将train image的匹配点加到matched2
}
}
stats.matches = (int)matched1.size(); // 更新匹配统计信息
Mat inlier_mask, homography; // 内点遮罩和单应性矩阵
vector<KeyPoint> inliers1, inliers2; // 内点关键点
vector<DMatch> inlier_matches; // 内点匹配
// 如果有足够的匹配点,则计算单应性矩阵
if(matched1.size() >= 4) {
homography = findHomography(Points(matched1), Points(matched2),
RANSAC, ransac_thresh, inlier_mask);
}
tm.stop(); // 停止计时
stats.fps = 1. / tm.getTimeSec(); // 计算帧率
// 判断是否有足够的匹配点,以及是否计算出了单应性矩阵
if(matched1.size() < 4 || homography.empty()) {
Mat res;
hconcat(first_frame, frame, res); // 横向拼接两帧
stats.inliers = 0; // 无内点
stats.ratio = 0; // 内点比例
return res;
}
// 遍历所有的已匹配特征点
for(unsigned i = 0; i < matched1.size(); i++) {
// 如果匹配点对应的inlier_mask中的值为真(在RANSAC等算法后表明为内点)
if(inlier_mask.at<uchar>(i)) {
// 获取当前内点向量inliers1的大小,即下一个将要添加的内点索引
int new_i = static_cast<int>(inliers1.size());
// 将被认定为内点的匹配对中的点添加到inliers1和inliers2中
inliers1.push_back(matched1[i]);
inliers2.push_back(matched2[i]);
// 在inlier_matches中添加一个新的DMatch对象,其特征点索引相同并且指向同一对内点
inlier_matches.push_back(DMatch(new_i, new_i, 0));
}
}
stats.inliers = (int)inliers1.size(); // 更新内点统计信息
stats.ratio = stats.inliers * 1.0 / stats.matches; // 计算内点比例
vector<Point2f> new_bb; // 新的边界框点集
perspectiveTransform(object_bb, new_bb, homography); // 变换边界框点集
Mat frame_with_bb = frame.clone(); // 克隆当前帧
// 如果内点足够则绘制边界框,测试中匹配点数较少,所以大多数边界框没有画出
if(stats.inliers >= bb_min_inliers) {
drawBoundingBox(frame_with_bb, new_bb);
}
Mat res;
// 绘制匹配的内点
drawMatches(first_frame, inliers1, frame_with_bb, inliers2,
inlier_matches, res,
Scalar(255, 0, 0), Scalar(255, 0, 0));
return res; // 返回结果
}
// main函数实现
int main(int argc, char **argv)
{
// 命令行解析
CommandLineParser parser(argc, argv, "{@input_path |0|input path can be a camera id, like 0,1,2 or a video filename}");
parser.printMessage();
// 获取视频输入路径
string input_path = parser.get<string>(0);
string video_name = input_path;
VideoCapture video_in;
// 根据输入路径判断是摄像头ID还是视频文件名,并尝试打开视频输入
if ( ( isdigit(input_path[0]) && input_path.size() == 1 ) )
{
int camera_no = input_path[0] - '0';
video_in.open( camera_no );
}
else {
video_in.open(video_name);
}
// 如果打开视频输入失败,则退出
if(!video_in.isOpened()) {
cerr << "Couldn't open " << video_name << endl;
return 1;
}
// 创建统计数据结构,并初始化AKAZE和ORB特征检测器与匹配器
// 定义三个Stats结构体变量,分别存储统计数据
Stats stats, akaze_stats, orb_stats;
// 创建一个AKAZE特征检测器的指针
Ptr<AKAZE> akaze = AKAZE::create();
// 设置AKAZE算法中的检测阈值
akaze->setThreshold(akaze_thresh);
// 创建一个ORB特征检测器的指针
Ptr<ORB> orb = ORB::create();//(Oriented FAST and Rotated BRIEF)是另一种快速且高效的特征点检测和描述子生成的算法。这里没有为ORB设置任何参数,因此它将使用默认参数。
// 创建一个描述符匹配器的指针,使用的是BruteForce-Hamming匹配策略
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("BruteForce-Hamming");
example::Tracker akaze_tracker(akaze, matcher); // 创建AKAZE追踪器
example::Tracker orb_tracker(orb, matcher); // 创建ORB追踪器
Mat frame; // 创建一个Mat用于存储每一帧图像
namedWindow(video_name, WINDOW_NORMAL); // 创建窗口
cout << "\nPress any key to stop the video and select a bounding box" << endl;
// 循环播放视频,直到按任意键停止
while ( waitKey(1) < 1 )
{
video_in >> frame; // 读取一帧
cv::resizeWindow(video_name, frame.size()); // 调整窗口大小
imshow(video_name, frame); // 显示当前帧
}
// 让用户从视频中选择一个边界框
vector<Point2f> bb;
cv::Rect uBox = cv::selectROI(video_name, frame);
// 将选中的边界框坐标转换为Point2f
// 在矩形bb中加入左上角的点
bb.push_back(cv::Point2f(static_cast<float>(uBox.x), static_cast<float>(uBox.y)));
// 在矩形bb中加入右上角的点
bb.push_back(cv::Point2f(static_cast<float>(uBox.x+uBox.width), static_cast<float>(uBox.y)));
// 在矩形bb中加入右下角的点
bb.push_back(cv::Point2f(static_cast<float>(uBox.x+uBox.width), static_cast<float>(uBox.y+uBox.height)));
// 在矩形bb中加入左下角的点
bb.push_back(cv::Point2f(static_cast<float>(uBox.x), static_cast<float>(uBox.y+uBox.height)));
// 使用AKAZE算法设置跟踪器的第一帧
akaze_tracker.setFirstFrame(frame, bb, "AKAZE", stats);
// 使用ORB算法设置跟踪器的第一帧
orb_tracker.setFirstFrame(frame, bb, "ORB", stats);
// 定义AKAZE和ORB算法的统计信息变量
Stats akaze_draw_stats, orb_draw_stats;
// 定义AKAZE和ORB算法的结果图像以及最终显示的垂直拼接图像
Mat akaze_res, orb_res, res_frame;
// 帧计数器
int i = 0;
// 无限循环处理视频帧
for(;;) {
i++; // 每次循环帧计数器增加
// 根据每隔一定帧数更新统计信息的条件判断是否更新
bool update_stats = (i % stats_update_period == 0);
// 读取视频的下一帧
video_in >> frame;
// 如果没有更多的帧则停止
if(frame.empty()) break;
// 使用AKAZE算法处理帧并更新统计信息
akaze_res = akaze_tracker.process(frame, stats);
akaze_stats += stats;//
// 如果需要更新统计信息,则更新AKAZE的统计信息
if(update_stats) {
akaze_draw_stats = stats;
}
// 设置ORB算法最大特征点数为当前统计到的特征点数
orb->setMaxFeatures(stats.keypoints);
// 使用ORB算法处理帧并更新统计信息
orb_res = orb_tracker.process(frame, stats);
orb_stats += stats;
// 如果需要更新统计信息,则更新ORB的统计信息
if(update_stats) {
orb_draw_stats = stats;
}
// 在AKAZE结果上绘制统计信息
drawStatistics(akaze_res, akaze_draw_stats);
// 在ORB结果上绘制统计信息
drawStatistics(orb_res, orb_draw_stats);
// 将AKAZE和ORB的结果上下拼接起来
vconcat(akaze_res, orb_res, res_frame);
// 展示拼接后的结果
cv::imshow(video_name, res_frame);
// 如果按下ESC键则退出循环
if(waitKey(1)==27) break;
}
// 计算AKAZE和ORB算法的平均统计信息
akaze_stats /= i - 1;
orb_stats /= i - 1;
// 打印AKAZE和ORB算法的统计信息
printStatistics("AKAZE", akaze_stats);
printStatistics("ORB", orb_stats);
return 0;
}
cpp
#ifndef STATS_H // 防止重复包含stats.h头文件
#define STATS_H
// Stats结构体定义,用于存储统计信息
struct Stats
{
int matches; // 匹配的特征点对数
int inliers; // 一致性特征点对数(RANSAC算法后的)
double ratio; // inliers/matches的比例
int keypoints; // 检测到的关键点数
double fps; // 处理每帧所用的时间(帧每秒)
// 构造函数,初始化所有统计数据
Stats() : matches(0),
inliers(0),
ratio(0),
keypoints(0),
fps(0.)
{}
// += 操作符重载,用于累加统计数据
Stats& operator+=(const Stats& op) {
matches += op.matches; // 累加匹配的特征点对数
inliers += op.inliers; // 累加一致性特征点对数
ratio += op.ratio; // 累加比例
keypoints += op.keypoints; // 累加关键点数
fps += op.fps; // 累加帧率
return *this; // 返回累加结果
}
// /= 操作符重载,用于计算平均统计数据
Stats& operator/=(int num)
{
matches /= num; // 计算平均匹配的特征点对数
inliers /= num; // 计算平均一致性特征点对数
ratio /= num; // 计算平均比例
keypoints /= num; // 计算平均关键点数
fps /= num; // 计算平均帧率
return *this; // 返回计算平均后的结果
}
};
#endif // STATS_H
这段代码是一个简单的结构体定义文件(stats.h
),它定义了一个Stats
结构,用来保存关键点匹配和跟踪过程中的统计数据。包括匹配点对数、一致性点对数、匹配的比例、关键点数和帧率。结构体内部还提供了简单的重载操作符+=
和/=
,方便地对多个Stats
实例进行数据累加和取平均值的操作。这些统计数据可用于对关键点匹配和对象跟踪算法的性能进行评估和比较。
cpp
#ifndef UTILS_H // 防止重复包含utils.h头文件
#define UTILS_H
#include <opencv2/core.hpp> // 包含OpenCV核心操作的头文件
#include <vector> // 包含标准模板库中的向量容器
#include "stats.h" // 包含Stats结构的定义
using namespace std; // 使用标准命名空间
using namespace cv; // 使用OpenCV命名空间
// 函数声明
void drawBoundingBox(Mat image, vector<Point2f> bb);
void drawStatistics(Mat image, const Stats& stats);
void printStatistics(string name, Stats stats);
vector<Point2f> Points(vector<KeyPoint> keypoints);
Rect2d selectROI(const String &video_name, const Mat &frame);
// 绘制边界框的函数
void drawBoundingBox(Mat image, vector<Point2f> bb)
{
// 遍历边界框的所有点并连线,形成矩形边框
for(unsigned i = 0; i < bb.size() - 1; i++) {
line(image, bb[i], bb[i + 1], Scalar(0, 0, 255), 2); // 用红色线条连接相邻的两个点
}
// 最后将序列的最后一个点与第一个点连线,闭合边界框
line(image, bb[bb.size() - 1], bb[0], Scalar(0, 0, 255), 2);
}
// 绘制统计信息的函数
void drawStatistics(Mat image, const Stats& stats)
{
static const int font = FONT_HERSHEY_PLAIN; // 设置文字字体
stringstream str1, str2, str3, str4; // 创建字符串流,用于拼接统计信息
// 将统计信息格式化后放入字符串流中
str1 << "Matches: " << stats.matches;
str2 << "Inliers: " << stats.inliers;
str3 << "Inlier ratio: " << setprecision(2) << stats.ratio;
str4 << "FPS: " << std::fixed << setprecision(2) << stats.fps;
// 在图像上打印统计信息
putText(image, str1.str(), Point(0, image.rows - 120), font, 2, Scalar::all(255), 3);
putText(image, str2.str(), Point(0, image.rows - 90), font, 2, Scalar::all(255), 3);
putText(image, str3.str(), Point(0, image.rows - 60), font, 2, Scalar::all(255), 3);
putText(image, str4.str(), Point(0, image.rows - 30), font, 2, Scalar::all(255), 3);
}
// 打印统计信息到控制台的函数
void printStatistics(string name, Stats stats)
{
// 输出统计信息标题和分割线
cout << name << endl;
cout << "----------" << endl;
// 输出各项统计信息
cout << "Matches " << stats.matches << endl;
cout << "Inliers " << stats.inliers << endl;
cout << "Inlier ratio " << setprecision(2) << stats.ratio << endl;
cout << "Keypoints " << stats.keypoints << endl;
cout << "FPS " << std::fixed << setprecision(2) << stats.fps << endl;
cout << endl;
}
// 将KeyPoint向量转换为Point2f向量的函数
vector<Point2f> Points(vector<KeyPoint> keypoints)
{
vector<Point2f> res; // 创建保存转换后的点的向量
for(unsigned i = 0; i < keypoints.size(); i++) {
// 将每个关键点的坐标转换为Point2f并加入到结果向量中
res.push_back(keypoints[i].pt);
}
return res; // 返回转换后的点的向量
}
#endif // UTILS_H
这段代码是一个辅助操作的头文件(utils.h
),包含了一些绘图和数据统计的函数。主要作用是辅助在图像中绘制边界框、在图像上打印统计信息以及到控制台打印统计信息,以及将KeyPoint结构体向量转换成普通的Point2f
结构体向量的功能。这些辅助函数通常用于视觉跟踪、特征匹配的算法当中,以便对算法的工作状态及效果进行观察和统计。
为什么要使用掩码矩阵进行关键点检测?
matcher->knnMatch(first_desc, desc, matches, 2);
if(matches[i][0].distance < nn_match_ratio * matches[i][1].distance)
homography = findHomography(Points(matched1), Points(matched2), RANSAC, ransac_thresh, inlier_mask);
在RANSAC算法中,如何确定阈值ransac_thresh的大小?
什么是单应性矩阵
cout << "Matches " << stats.matches << endl; cout << "Inliers " << stats.inliers << endl; cout << "Inlier ratio " << setprecision(2) << stats.ratio << endl; cout << "Keypoints " << stats.keypoints << endl; cout << "FPS " << std::fixed << setprecision(2) << stats.fps << endl; cout << endl;