OpenCV 4.12.0
開源計算機視覺
載入中...
搜尋中...
無匹配項
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;
}
保護:
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)
{
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;
}
如果陣列沒有元素,則返回 true。
int64_t int64
用於匹配關鍵點描述符的類。
定義 types.hpp:849
n 維密集陣列類
定義 mat.hpp:830
CV_NODISCARD_STD Mat clone() const
建立陣列及其底層資料的完整副本。
MatSize size
定義 mat.hpp:2187
static CV_NODISCARD_STD MatExpr zeros(int rows, int cols, int type)
返回指定大小和型別的全零陣列。
_Tp & at(int i0=0)
返回指定陣列元素的引用。
cv::getTickFrequency
double getTickFrequency()
_Tp y
點的 y 座標
定義 types.hpp:202
_Tp x
點的 x 座標
定義 types.hpp:201
2D 矩形的模板類。
定義 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:729
用於從影片檔案、影像序列或攝像機捕獲影片的類。
Definition videoio.hpp:772
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
Definition cvstd_wrapper.hpp:23
InputOutputArray noArray()
template<typename _Tp , int m, int n>
I.at<uchar>(y, x) = saturate_cast<uchar>(r);
uchar
unsigned char uchar
#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),
      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(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