1. 程式人生 > >郭雲飛的專欄

郭雲飛的專欄

一、介紹

假如有兩張人物圖片,我們的目標是要確認這兩張圖片中的人物是否是同一個人。如果人來判斷,這太簡單了。但是讓計算機來完成這個功能就困難重重。一種可行的方法是:

  1. 分別找出兩張圖片中的特徵點
  2. 描述這些特徵點的屬性,
  3. 比較這兩張圖片的特徵點的屬性。如果有足夠多的特徵點具有相同的屬性,那麼就可以認為兩張圖片中的人物就是同一個人。

ORB(Oriented FAST and Rotated BRIEF)就是一種特徵提取並描述的方法。ORB是由Ethan Rublee, Vincent Rabaud, Kurt Konolige以及Gary R.Bradski在2011年提出,論文名稱為。

ORB分兩部分,即特徵點提取和特徵點描述。特徵提取是由FAST(

Features from Accelerated Segment Test)演算法發展來的,特徵點描述是根據BRIEF(Binary Robust Independent Elementary Features)特徵描述演算法改進的。ORB特徵是將FAST特徵點的檢測方法與BRIEF特徵描述子結合起來,並在它們原來的基礎上做了改進與優化。據說ORB演算法的速度是sift的100倍,是surf的10倍。

二、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):