OpenCV
开源计算机视觉库
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
AKAZE 和 ORB 平面跟踪

上一篇教程: AKAZE局部特征匹配
下一篇教程: 带代码讲解的单应性基本概念

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

引言

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

算法如下:

  • 在第一帧上检测和描述关键点,手动设置物体边界
  • 对于每一帧:
    1. 检测和描述关键点
    2. 使用暴力匹配器进行匹配
    3. 使用RANSAC估计单应性变换
    4. 过滤所有匹配项中的内点
    5. 将单应性变换应用于边界框以查找物体
    6. 绘制边界框和内点,计算内点比率作为评估指标

数据

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

您可以从 这里 下载我们的示例视频和数据。

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

./planar_tracking blais.mp4

源代码

#include <opencv2/highgui.hpp> // 用于 imshow
#include <vector>
#include <iostream>
#include <iomanip>
#include "stats.h" // Stats 结构体定义
#include "utils.h" // 绘图和打印函数
using namespace std;
using namespace cv;
const double akaze_thresh = 3e-4; // AKAZE 检测阈值,设置为定位大约 1000 个关键点
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 帧更新屏幕上的统计信息
namespace example {
class Tracker
{
public:
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;
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)
{
cv::TickMeter tm;
std::vector<cv::KeyPoint> kp;
cv::Mat desc;
tm.start();
detector->detectAndCompute(frame, cv::noArray(), kp, desc);
stats.keypoints = (int)kp.size();
std::vector<std::vector<cv::DMatch> > matches;
std::vector<cv::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();
cv::Mat inlier_mask, homography;
std::vector<cv::KeyPoint> inliers1, inliers2;
std::vector<cv::DMatch> inlier_matches;
if(matched1.size() >= 4) {
homography = cv::findHomography(cv::Mat(matched1), cv::Mat(matched2),
cv::RANSAC, ransac_thresh, inlier_mask);
}
tm.stop();
stats.fps = 1. / tm.getTimeSec();
if(matched1.size() < 4 || homography.empty()) {
cv::Mat res;
cv::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(cv::DMatch(new_i, new_i, 0));
}
}
stats.inliers = (int)inliers1.size();
stats.ratio = stats.inliers * 1.0 / stats.matches;
std::vector<cv::Point2f> new_bb;
cv::perspectiveTransform(object_bb, new_bb, homography);
cv::Mat frame_with_bb = frame.clone();
if(stats.inliers >= bb_min_inliers) {
drawBoundingBox(frame_with_bb, new_bb);
}
cv::Mat res;
cv::drawMatches(first_frame, inliers1, frame_with_bb, inliers2,
inlier_matches, res,
cv::Scalar(255, 0, 0), cv::Scalar(255, 0, 0));
return res;
}
}
int main(int argc, char **argv)
{
cv::CommandLineParser parser(argc, argv, "{@input_path |0|输入路径可以是摄像头ID,例如0,1,2或视频文件名}");
parser.printMessage();
std::string input_path = parser.get<std::string>(0);
std::string video_name = input_path;
cv::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()) {
std::cerr << "无法打开 " << video_name << std::endl;
return -1; 1;
}
Stats stats, akaze_stats, orb_stats;
cv::Ptr<cv::AKAZE> akaze = cv::AKAZE::create();
akaze->setThreshold(akaze_thresh);
cv::Ptr<cv::ORB> orb = cv::ORB::create();
cv::Ptr<cv::DescriptorMatcher> matcher = cv::DescriptorMatcher::create("BruteForce-Hamming");
example::Tracker akaze_tracker(akaze, matcher);
example::Tracker orb_tracker(orb, matcher);
cv::Mat frame;
cv::namedWindow(video_name, cv::WINDOW_NORMAL);
std::cout << "\n按下任意键停止视频并选择边界框" << std::endl;
while ( cv::waitKey(1) < 1 )
{
video_in >> frame;
cv::resizeWindow(video_name, frame.size());
cv::imshow(video_name, frame);
}
std::vector<cv::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;
cv::Mat akaze_res, orb_res, res_frame;
int i = 0;
for(;;){ // 无限循环,直到break(;;) {
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;
如果 (update_stats) {
akaze_draw_stats = stats;
}
orb->setMaxFeatures(stats.keypoints);
orb_res = orb_tracker.process(frame, stats);
orb_stats += stats;
如果 (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);
如果 (waitKey(1)==27) break; //按ESC键退出
}
akaze_stats /= i - 1;
orb_stats /= i - 1;
printStatistics("AKAZE", akaze_stats);
printStatistics("ORB", orb_stats);
return -1; 0;
}
用于命令行解析。
**定义** utility.hpp:890
用于匹配关键点描述符的类。
**定义** types.hpp:849
n维密集数组类
**定义** mat.hpp:829
CV_NODISCARD_STD Mat clone() const
创建数组及其底层数据的完整副本。
MatSize size
**定义** mat.hpp:2177
static CV_NODISCARD_STD MatExpr zeros(int rows, int cols, int type)
返回指定大小和类型的零数组。
_Tp & at(int i0=0)
返回对指定数组元素的引用。
bool empty() const
如果数组没有元素,则返回true。
_Tp y
点的y坐标
**定义** types.hpp:202
_Tp x
点的x坐标
**定义** types.hpp:201
二维矩形的模板类。
**定义** types.hpp:444
_Tp x
左上角的x坐标
**定义** types.hpp:487
_Tp y
左上角的y坐标
**定义** types.hpp:488
_Tp width
矩形的宽度
**定义** types.hpp:489
_Tp height
矩形的高度
**定义** types.hpp:490
static Scalar_< double > all(double v0)
用于测量经过时间的类。
**定义** utility.hpp:326
void start()
开始计数。
**定义** utility.hpp:335
double getTimeSec() const
返回以秒为单位的经过时间。
**定义** utility.hpp:371
void stop()
停止计数。
**定义** utility.hpp:341
长期跟踪器的基本抽象类。
**定义** tracking.hpp:726
用于从视频文件、图像序列或摄像头捕获视频的类。
**定义** videoio.hpp:766
virtual bool open(const String &filename, int apiPreference=CAP_ANY)
打开视频文件或捕获设备或IP视频流进行视频捕获。
virtual bool isOpened() const
如果视频捕获已初始化,则返回true。
Mat findHomography(InputArray srcPoints, InputArray dstPoints, int method=0, double ransacReprojThreshold=3, OutputArray mask=noArray(), const int maxIters=2000, const double confidence=0.995)
查找两个平面之间的透视变换。
void vconcat(const Mat *src, size_t nsrc, OutputArray dst)
将垂直连接应用于给定的矩阵。
void perspectiveTransform(InputArray src, OutputArray dst, InputArray m)
执行向量的透视矩阵变换。
void hconcat(const Mat *src, size_t nsrc, OutputArray dst)
将水平连接应用于给定的矩阵。
std::shared_ptr< _Tp > Ptr
**定义** cvstd_wrapper.hpp:23
InputOutputArray noArray()
返回一个空的InputArray或OutputArray。
unsigned char uchar
**定义** interface.h:51
#define CV_8UC1
**定义** interface.h:88
void 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)
绘制从两幅图像中找到的关键点匹配。
void imshow(const String &winname, InputArray mat)
在指定的窗口中显示图像。
int waitKey(int delay=0)
等待按下的键。
void namedWindow(const String &winname, int flags=WINDOW_AUTOSIZE)
创建一个窗口。
void resizeWindow(const String &winname, int width, int height)
将窗口大小调整为指定的大小。
Rect selectROI(const String &windowName, InputArray img, bool showCrosshair=true, bool fromCenter=false, bool printNotice=true)
允许用户在给定图像上选择ROI。
void fillPoly(InputOutputArray img, InputArrayOfArrays pts, const Scalar &color, int lineType=LINE_8, int shift=0, Point offset=Point())
填充一个或多个多边形包围的区域。
void putText(InputOutputArray img, const String &text, Point org, int fontFace, double fontScale, Scalar color, int thickness=1, int lineType=LINE_8, bool bottomLeftOrigin=false)
绘制文本字符串。
int main(int argc, char *argv[])
**定义** highgui_qt.cpp:3
**定义** core.hpp:107
STL命名空间。

解释

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 个关键点。

    2. 使用 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_ 倍,则它就是一个匹配。
    3. 使用 _RANSAC_ 估计单应性变换
      homography = findHomography(Points(matched1), Points(matched2),
      cv::RANSAC, ransac_thresh, inlier_mask);
      如果至少有 4 个匹配项,我们可以使用随机抽样一致性来估计图像变换。
    4. 保存内点
      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(cv::DMatch(new_i, new_i, 0));
      }
      }
      由于 _findHomography_ 计算内点,我们只需要保存选择的点和匹配项。
    5. 投影目标边界框

      perspectiveTransform(object_bb, new_bb, homography);

      如果有合理数量的内点,我们可以使用估计的变换来定位目标。

结果

您可以在 YouTube 上观看生成的 视频

_AKAZE_ 统计数据

匹配数 626
内点数 410
内点比率 0.58
关键点数 1117

_ORB_ 统计数据

匹配数 504
内点数 319
内点比率 0.56
关键点数 1112