二维特征框架——AKAZE 和 ORB 平面跟踪 OpenCV v4.8.0

上一个教程AKAZE 局部特征匹配

下一个教程用代码解释同源性的基本概念

原作者Fedor Morozov
兼容性OpenCV >= 3.0

简介

在本教程中,我们将比较 AKAZEORB 本地特征,使用它们来查找视频帧之间的匹配点并跟踪物体运动。

算法如下:

  • 检测并描述第一帧的关键点,手动设置物体边界
  • 接下来的每一帧
    1. 检测和描述关键点
    2. 使用暴力匹配器进行匹配
    3. 使用 RANSAC 估算同构变换
    4. 从所有匹配结果中过滤异常值
    5. 对边界框应用同构变换以找到对象
    6. 绘制边界框和离群值,计算离群值比率作为评估指标
      在这里插入图片描述

数据

要进行跟踪,我们需要视频和第一帧上的物体位置。

您可以从此处下载示例视频和数据。

要运行代码,您必须指定输入(摄像机 ID 或视频文件)。然后,用鼠标选择一个边界框,按任意键开始跟踪

./planar_tracking blais.mp4

源代码

#include <opencv2/features2d.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/calib3d.hpp>
#include <opencv2/highgui.hpp> //for imshow
#include <vector>
#include <iostream>
#include <iomanip>
#include "stats.h" //  统计结构定义
#include "utils.h" // 绘图和打印函数
using namespace std;
using namespace cv;
const double akaze_thresh = 3e-4; // AKAZE 检测阈值设置为定位约 1000 个关键点
const double ransac_thresh = 2.5f; // RANSAC inlier threshold
const double nn_match_ratio = 0.8f; // 最近邻匹配率
const int bb_min_inliers = 100; // 绘制边界框的最小离群值个数
const int stats_update_period = 10; // 屏幕上的统计数据每 10 帧更新一次
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;
};
void Tracker::setFirstFrame(const Mat frame, vector<Point2f> bb, string title, Stats& stats)
{
 cv::Point *ptMask = new cv::Point[bb.size()];
 const Point* ptContain = { &ptMask[0] };
 int iSize = static_cast<int>(bb.size());
 for (size_t i=0; i<bb.size(); i++) {
 ptMask[i].x = static_cast<int>(bb[i].x);
 ptMask[i].y = static_cast<int>(bb[i].y);
 }
 first_frame = frame.clone();
 cv::Mat matMask = cv::Mat::zeros(frame.size(), CV_8UC1);
 cv::fillPoly(matMask, &ptContain, &iSize, 1, cv::Scalar::all(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;
}
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) {
 matched1.push_back(first_kp[matches[i][0].queryIdx]);
 matched2.push_back( kp[matches[i][0].trainIdx]);
 }
 }
 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++) {
 if(inlier_mask.at<uchar>(i)) {
 int new_i = static_cast<int>(inliers1.size());
 inliers1.push_back(matched1[i]);
 inliers2.push_back(matched2[i]);
 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;
}
}
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;
 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;
 }
 Stats stats, akaze_stats, orb_stats;
 Ptr<AKAZE> akaze = AKAZE::create();
 akaze->setThreshold(akaze_thresh);
 Ptr<ORB> orb = ORB::create();
 Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("BruteForce-Hamming");
 example::Tracker akaze_tracker(akaze, matcher);
 example::Tracker orb_tracker(orb, matcher);
 Mat frame;
 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);
 bb.push_back(cv::Point2f(static_cast<float>(uBox.x), static_cast<float>(uBox.y)));
 bb.push_back(cv::Point2f(static_cast<float>(uBox.x+uBox.width), static_cast<float>(uBox.y)));
 bb.push_back(cv::Point2f(static_cast<float>(uBox.x+uBox.width), static_cast<float>(uBox.y+uBox.height)));
 bb.push_back(cv::Point2f(static_cast<float>(uBox.x), static_cast<float>(uBox.y+uBox.height)));
 akaze_tracker.setFirstFrame(frame, bb, "AKAZE", stats);
 orb_tracker.setFirstFrame(frame, bb, "ORB", stats);
 Stats akaze_draw_stats, orb_draw_stats;
 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_res = akaze_tracker.process(frame, stats);
 akaze_stats += stats;
 if(update_stats) {
 akaze_draw_stats = stats;
 }
 orb->setMaxFeatures(stats.keypoints);
 orb_res = orb_tracker.process(frame, stats);
 orb_stats += stats;
 if(update_stats) {
 orb_draw_stats = stats;
 }
 drawStatistics(akaze_res, akaze_draw_stats);
 drawStatistics(orb_res, orb_draw_stats);
 vconcat(akaze_res, orb_res, res_frame);
 cv::imshow(video_name, res_frame);
 if(waitKey(1)==27) break; //按 ESC 键退出
 }
 akaze_stats /= i - 1;
 orb_stats /= i - 1;
 printStatistics("AKAZE", akaze_stats);
 printStatistics("ORB", orb_stats);
 return 0;
}

说明

Tracker类

该类使用给定的特征检测器和描述符匹配器实现上文所述的算法。

  • 设置第一帧
void Tracker::setFirstFrame(const Mat frame, vector<Point2f> bb, string title, Stats& stats)
{
 first_frame = frame.clone()(*detector)(first_frame, noArray(), 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;
}

我们计算并存储第一帧的关键点和描述符,为输出做好准备。

我们需要保存检测到的关键点数量,以确保两个检测器找到的关键点数量大致相同。

  • 处理帧
  1. 定位关键点并计算描述符
(*detector)(frame, noArray(), kp, desc)

要找到帧间的匹配点,我们必须先找到关键点。

在本教程中,检测器的设置是在每帧上找到约 1000 个关键点。

  1. 使用 2-nn 匹配器查找对应关系
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) {
 matched1.push_back( first_kp[matches[i][0].queryIdx]);
 matched2.push_back( kp[matches[i][0].trainIdx])}
}

如果最接近的匹配比第二个最接近的匹配更接近 nn_match_ratio,那么它就是匹配的。

  1. 使用 RANSAC 估算同源性变换
homography = findHomography(Points(matched1), Points(matched2)、
 RANSAC, ransac_thresh, inlier_mask);

如果至少有 4 个匹配点,我们就可以使用随机样本共识来估计图像变换。

  1. 保存异常值
for(unsigned i = 0; i < matched1.size(); i++) {
 if(inlier_mask.at<uchar>(i)) {
 int new_i = static_cast<int>(inliers1.size());
 inliers1.push_back(matched1[i]);
 inliers2.push_back(matched2[i]);
 inlier_matches.push_back(DMatch(new_i, new_i, 0))}
}

由于 findHomography 会计算离群值,因此我们只需保存选定的点和匹配值。

  1. 项目对象边界框
perspectiveTransform(object_bb, new_bb, homography)

如果有合理数量的离群值,我们就可以使用估计变换来定位对象。

结果

您可以在 youtube 上观看结果视频。

AKAZE 统计数据:

Matches 626
Inliers 410
Inlier ratio 0.58
Keypoints 1117

ORB 统计数据:

Matches 504
Inliers 319
Inlier ratio 0.56
Keypoints 1112
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值