郭雲飛的專欄
一、介紹
假如有兩張人物圖片,我們的目標是要確認這兩張圖片中的人物是否是同一個人。如果人來判斷,這太簡單了。但是讓計算機來完成這個功能就困難重重。一種可行的方法是:
- 分別找出兩張圖片中的特徵點
- 描述這些特徵點的屬性,
- 比較這兩張圖片的特徵點的屬性。如果有足夠多的特徵點具有相同的屬性,那麼就可以認為兩張圖片中的人物就是同一個人。
ORB(Oriented FAST and Rotated BRIEF)就是一種特徵提取並描述的方法。ORB是由Ethan Rublee, Vincent Rabaud, Kurt Konolige以及Gary R.Bradski在2011年提出,論文名稱為。
ORB分兩部分,即特徵點提取和特徵點描述。特徵提取是由FAST(
二、Oriented FAST(oFast)特徵點的提取
oFast就是在使用FAST提取特徵點之後,給其定義一個該特徵點的放向,並以此來實現該特徵點的旋轉不變形。
2.1、粗提取
影象的特徵點可以簡單的理解為影象中比較顯著顯著的點,如輪廓點,較暗區域中的亮點,較亮區域中的暗點等。
FAST的核心思想是找出那些卓爾不群的點。即拿一個點跟它周圍的點比較,如果它和其中大部分的點都不一樣,就人物它是一個特徵點。
如上圖,假設影象中的一點P,及其一個鄰域。右半拉是放大的圖,每個小方格代表一個畫素,方格內的顏色只是為了便於區分,不代表該畫素點的顏色。判斷該點是不是特徵點的方法是,以P為圓心畫一個半徑為3pixel的圓(周長為16pixel)。圓周上如果有連續n個畫素點的灰度值比P點的灰度值大或者小(需事先設定一個閾值T),則認為P為特徵點。一般n設定為12。
為了加快特徵點的提取,快速排除非特徵點,首先檢測1、9、5、13位置上的灰度值,如果P是特徵點,那麼這四個位置上有3個或3個以上的的畫素值都大於或者小於P
2.2、使用ID3決策樹,將特徵點圓周上的16個畫素輸入決策樹中,以此來篩選出最優的FAST特徵點。
2.3、使用非極大值抑制演算法去除臨近位置多個特徵點的。具體:為每一個特徵點計算出其響應大小(特徵點P和其周圍16個特徵點偏差的絕對值和)。在比較臨近的特徵點中,保留響應值較大的特徵點,刪除其餘的特徵點。
2.4、特徵點的尺度不變性
建立金字塔,來實現特徵點的多尺度不變性。設定一個比例因子scaleFactor(opencv預設為1.2)和金字塔的層數nlevels(pencv預設為8)。將原影象按比例因子縮小成nlevels幅影象。縮放後的影象為:I’= I/scaleFactork(k=1,2,…, nlevels)。nlevels幅不同比例的影象提取特徵點總和作為這幅影象的oFAST特徵點。
2.5、特徵點的旋轉不變形
oFast用矩(moment)法來確定FAST特徵點的方向。即計算特徵點以r為半徑範圍內的質心,特徵點座標到質心形成一個向量作為該特徵點的方向。矩定義如下:
三、Rotated BRIEF(rBRIEF)特徵點的描述
3.1、BRIEF演算法
BRIEF演算法計算出來的是一個二進位制串的特徵描述符。它是在一個特徵點的鄰域內,選擇n對畫素點pi、qi(i=1,2,…,n)。然後比較每個點對的灰度值的大小。如果I(pi)> I(qi),則生成二進位制串中的1,否則為0。所有的點對都進行比較,則生成長度為n的二進位制串。一般n取128、256或512(opencv預設為256)。
另外,為了增加特徵描述符的抗噪性,演算法需要先對影象進行高斯平滑處理。在ORB演算法中,在這個地方進行了改進,在使用高斯函式進行平滑後又用了其他操作,使其更加的具有抗噪性。具體方法下面將會描述。
在特徵點SxS的區域內選取點對的方法,BRIEF論文中測試了5種方法:
- 在影象塊內平均取樣;
- p和q都符合(0,S2/25)的高斯分佈;
- p符合(0,S2/25)的高斯分佈,而q符合(0,S2/100)的高斯分佈;
- 在空間量化極座標下的離散位置隨機取樣;
- 把p固定為(0,0),q在周圍平均取樣。
3.2、rBRIEF演算法
3.2.1、steered BRIEF(旋轉不變性改進):
在使用oFast演算法計算出的特徵點中包括了特徵點的方向角度。假設原始的BRIEF演算法在特徵點SxS(一般S取31)鄰域內選取n對點集。
經過旋轉角度θ旋轉,得到新的點對:
在新的點集位置上比較點對的大小形成二進位制串的描述符。這裡需要注意的是,在使用oFast演算法是在不同的尺度上提取的特徵點。因此,在使用BRIEF特徵描述時,要將影象轉換到相應的尺度影象上,然後在尺度影象上的特徵點處取SxS鄰域,然後選擇點對並旋轉,得到二進位制串描述符。
3.2.2、rBRIEF-改進特徵點描述子的相關性
使用steeredBRIEF方法得到的特徵描述子具有旋轉不變性,但是卻在另外一個性質上不如原始的BRIEF演算法,即描述符的可區分性(相關性)。為了解決描述子的可區分性和相關性的問題,ORB論文中沒有使用原始BRIEF演算法中選取點對時的5種方法中的任意一種,而是使用統計學習的方法來重新選擇點對集合。
對每個特徵點選取31x31領域,每個領域選擇5x5的平均灰度值代替原來單個畫素值進行比對,因此可以得到N=(31-5+1)x(31-5+1) = 729個可以比對的子視窗(patch),可以使用積分影象加快求取5x5鄰域灰度平均值的速度。一共有M = 1+2+3+...+N = 265356種點對組合,也就是一個長度為M的01字串。顯然M遠大於256,我們得篩選。
篩選方法如下:
- 重組所有點以及對應的初始二值串得到矩陣O,行數為提取得到的點數,每行是每個點對應的初始二值描述子
- 對重組後的矩陣O,按照每列均值與0.5的絕對差從小到大排序,得到矩陣T
- 貪心選擇:把T中第一列放進矩陣R(一開始為空)中,並從T中移除依次選擇T的每列,與R中所有的列進行比較,如果相似度超過一定閾值,忽略,進行下一列,否則放進R中,並從T中移除重複以上過程直到選擇256個列,這樣每個特徵點就有256個0,1組成的描述子。如果不足256個,則降低閾值直到滿足256就可,R即為最終特徵描述矩陣。
三、特徵點匹配
這部分是另外一個話題了。ORB演算法最大的特點就是計算速度快 。這得益於使用FAST檢測特徵點,FAST的檢測速度正如它的名字一樣是出了名的快。再就是是使用BRIEF演算法計算描述子,該描述子特有的2進位制串的表現形式不僅節約了儲存空間,而且大大縮短了匹配的時間。
例如特徵點A、B的描述子如下。
A:10101011
B:10101010
設定一個閾值,比如80%。當A和B的描述子的相似度大於90%時,我們判斷A,B是相同的特徵點,即這2個點匹配成功。在這個例子中A,B只有最後一位不同,相似度為87.5%,大於80%。則A和B是匹配的。
將A和B進行異或操作就可以輕鬆計算出A和B的相似度。而異或操作可以藉助硬體完成,具有很高的效率,加快了匹配的速度。
四、OpenCV實驗(OpenCV3.0以上版本,包含contrib模組)
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <dirent.h>
#include <unistd.h>
#include <vector>
#include <sstream>
#include <fstream>
#include <sys/io.h>
#include <sys/times.h>
#include <iomanip>
#include <tuple>
#include <cstdlib>
using namespace std;
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/stitching.hpp"
#include "opencv2/xfeatures2d/nonfree.hpp"
using namespace cv;
#define ENABLE_LOG
bool PreapreImg(vector<Mat> &imgs);
bool Match(vector<cv::detail::MatchesInfo> &pairwise_matches,
const vector<cv::detail::ImageFeatures> &features,
const cv::String matcher_type = "homography",
const int range_width = -1,
const bool try_cuda = false,
const double match_conf = 0.3f);
void demo();
int main(int argc, char** argv)
{
cout << "# STA ##############################" << endl;
cout << "\n" << endl;
int64 app_start_time = getTickCount();
demo();
cout << "\n" << endl;
cout << "# END ############################## Time: "
<< ((getTickCount() - app_start_time) / getTickFrequency())
<< " sec" << endl;
return 0;
}
void demo()
{
vector<Mat> imgs;
PreapreImg(imgs);
// define feature finder
Ptr<cv::detail::FeaturesFinder> finder =
cv::makePtr<cv::detail::OrbFeaturesFinder>();
// detect features
int num_images = static_cast<int>(imgs.size());
vector<cv::detail::ImageFeatures> features(num_images);
for (int i = 0; i < num_images; i++) {
(*finder)(imgs[i], features[i]);
features[i].img_idx = i;
#ifdef ENABLE_LOG
cout << ">> features number: " << setw(4) << features[i].img_idx
<< setw(5) << static_cast<int>(features[i].keypoints.size())
<< endl;
Mat tmp;
cv::drawKeypoints(imgs[i], features[i].keypoints, tmp);
stringstream ss;
ss << i;
cv::imwrite(("./img" + string(ss.str()) + "_keypoints.jpg").c_str(), tmp);
#endif
}
// Frees unused memory allocated before if there is any
finder->collectGarbage();
// Pairwise matching
vector<cv::detail::MatchesInfo> pairwise_matches;
Match(pairwise_matches, features);
#ifdef ENABLE_LOG
cout << ">> pairwise matches: "
<< setw(5) << static_cast<int>(pairwise_matches.size())
<< endl;
cout << ">> Saving matches graph..." << endl;
ofstream f("./matchGraph.txt");
vector<cv::String> img_names;
for (int i = 0; i < num_images; i++) {
stringstream ss; ss << i;
img_names.push_back(ss.str());
}
f << matchesGraphAsString(img_names, pairwise_matches, 1.0f);
cout << ">> Saving matches graph OK. Position: ./matchGraph.txt" << endl;
Mat tmp;
cv::drawMatches(imgs[0], features[0].keypoints,
imgs[1], features[1].keypoints,
pairwise_matches[1].matches,
tmp);
cv::imwrite("./matches0_1.jpg", tmp);
#endif
}
bool PreapreImg(vector<Mat> &imgs)
{
Mat image0 = imread("./0.jpg", IMREAD_GRAYSCALE);
Mat image1 = imread("./1.jpg", IMREAD_GRAYSCALE);
imgs.push_back(image0);
imgs.push_back(image1);
// Check if have enough images
int num_images = static_cast<int>(imgs.size());
if (num_images < 2)
{
cout << ">> error. num_images < 2" << endl;
return false;
}
#ifdef ENABLE_LOG
for (int i = 0; i < num_images; i++) {
cout << ">> image " << setw(2) << i << ": "
<< setw(5) << imgs[i].rows
<< setw(5) << imgs[i].cols
<< setw(5) << imgs[i].channels()
<< endl;
}
#endif
return true;
}
/************************************************
* There are 3 kinds of feature matchers offered by "matchers.hpp"
*/
bool Match(vector<cv::detail::MatchesInfo> &pairwise_matches,
const vector<cv::detail::ImageFeatures> &features,
const cv::String matcher_type = "homography",
const int range_width = -1,
const bool try_cuda = false,
const double match_conf = 0.3f)
{
Ptr<cv::detail::FeaturesMatcher> matcher;
if (matcher_type == "affine")
{
bool full_affine = false;
int num_matches_thresh1 = 6;
matcher = makePtr<cv::detail::AffineBestOf2NearestMatcher>(
full_affine, try_cuda, match_conf, num_matches_thresh1);
}
else if (matcher_type == "homography")
{
int num_matches_thresh1 = 6;
int num_matches_thresh2 = 6;
if (range_width == -1)
matcher = makePtr<cv::detail::BestOf2NearestMatcher>(
try_cuda, match_conf, num_matches_thresh1, num_matches_thresh2);
else
matcher = makePtr<cv::detail::BestOf2NearestRangeMatcher>(
range_width, try_cuda, match_conf, num_matches_thresh1, num_matches_thresh2);
}
(*matcher)(features, pairwise_matches);
matcher->collectGarbage();
return true;
}
實驗程式碼:https://code.csdn.net/guoyunfei20/orb.git
輸入影象1:
輸入影象2:
影象1的ORB特徵點位置:
影象2的ORB特徵點位置:
利用cv::detail::BestOf2NearestMatcher匹配演算法得到的能匹配上的特徵點(影象0 -> 影象1):