图像特征描述子之ORB_orb描述子-程序员宅基地

技术标签: image  特征提取  计算机视觉  图像处理  ORB  

原文站点:https://senitco.github.io/2017/07/09/image-feature-orb/

  ORB(Oriented FAST and Rotated BRIEF)算法是对FAST特征点检测和BRIEF特征描述子的一种结合,在原有的基础上做了改进与优化,使得ORB特征具备多种局部不变性,并为实时计算提供了可能。

特征点检测

  ORB首先利用FAST算法检测特征点,然后计算每个特征点的Harris角点响应值,从中筛选出 N 个最大的特征点,Harris角点的响应函数如下:

R=detMα(traceM)2

相关内容已在博文 FAST角点检测Harris角点检测分别做了详细的介绍。
FAST检测特征点不具备尺度不变性,可以像SIFT特征一样,借助尺度空间理论构建图像高斯金字塔,然后在每一层金字塔图像上检测角点,以实现尺度不变性。对于旋转不变性,原论文中提出了一种利用图像矩(几何矩),在半径为r的邻域内求取灰度质心的方法,从特征点到灰度质心的向量,定义为该特征点的主方向。图像矩定义如下:

mpq=Σx,yxpyqI(x,y),x,y[r,r]

I(x,y) 表示像素灰度值,0阶矩 m00 即图像邻域窗口内所有像素的灰度和, m10 m01 分别相对 x 和相对 y 的一阶矩,因此图像局部邻域的中心矩或者质心可定义为
C=(m10m00,m01m00)

特征点与质心形成的向量与 X 轴的夹角定义为特征点的主方向
θ=arctan(m01,m10)

特征点描述

  ORB采用BRIEF作为特征描述方法,BRIEF虽然速度优势明显,但也存在一些缺陷,例如不具备尺度不变性和旋转不变性,对噪声敏感。尺度不变性的问题在利用FAST检测特征点时,通过构建高斯金字塔得以解决。BRIEF中采用 9×9 的高斯卷积核进行滤波降噪,可以在一定程度上缓解噪声敏感问题;ORB中利用积分图像,在 31×31 的Patch中选取随机点对,并以选取的随机点为中心,在 5×5 的窗口内计算灰度平均值(灰度和),比较随机点对的邻域灰度均值,进行二进制编码,而不是仅仅由两个随机点对的像素值决定编码结果,可以有效地解决噪声问题。
  至于旋转不变性问题,可利用FAST特征点检测时求取的主方向,旋转特征点邻域,但旋转整个Patch再提取BRIEF特征描述子的计算代价较大,因此,ORB采用了一种更高效的方式,在每个特征点邻域Patch内,先选取256对随机点,将其进行旋转,然后做判决编码为二进制串。n个点对构成矩阵 S

S=[x1 y1x2y2x2ny2n]

旋转矩阵 Rθ

Rθ=[cosθ sinθsinθcosθ]

旋转后的坐标矩阵为
Sθ=RθS

描述子的区分性

  通过上述方法得到的特征描述子具有旋转不变性,称为steered BRIEF(sBRIEF),但匹配效果却不如原始BRIEF算法,因为可区分性减弱了。特征描述子的一个要求就是要尽可能地表达特征点的独特性,便于区分不同的特征点。如下图所示,为几种特征描述子的均值分布,横轴为均值与0.5之间的距离,纵轴为相应均值下特征点的统计数量。可以看出,BRIEF描述子所有比特位的均值接近于0.5,且方差很大;方差越大表明可区分性越好。不同特征点的描述子表现出较大的差异性,不易造成无匹配。但steered BRIEF进行了坐标旋转,损失了这个特性,导致可区分性减弱,相关性变强,不利于匹配。


sBRIEF-rBRIEF.jpg

  为了解决steered BRIEF可区分性降低的问题,ORB使用了一种基于学习的方法来选择一定数量的随机点对。首先建立一个大约300k特征点的数据集(特征点来源于PASCAL2006中的图像),对每个特征点,考虑其 31×31 的邻域Patch,为了去除噪声的干扰,选择 5×5 的子窗口的灰度均值代替单个像素的灰度,这样每个Patch内就有 N=(315+1)×(315+1)=27×27=729 个子窗口,从中随机选取2个非重复的子窗口,一共有 M=C2N 中方法。这样,每个特征点便可提取出一个长度为 M 的二进制串,所有特征点可构成一个 300k×M 的二进制矩阵 Q ,矩阵中每个元素取值为0或1。现在需要从 M 个点对中选取256个相关性最小、可区分性最大的点对,作为最终的二进制编码。筛选方法如下:
- 对矩阵 Q 的每一列求取均值,并根据均值与0.5之间的距离从小到大的顺序,依次对所有列向量进行重新排序,得到矩阵 T
- 将 T 中的第一列向量放到结果矩阵 R
- 取出 T 中的下一列向量,计算其与矩阵 R 中所有列向量的相关性,如果相关系数小于给定阈值,则将 T 中的该列向量移至矩阵 R 中,否则丢弃
- 循环执行上一步,直到 R 中有256个列向量;如果遍历 T 中所有列, R <script type="math/tex" id="MathJax-Element-39">R</script>中向量列数还不满256,则增大阈值,重复以上步骤。

这样,最后得到的就是相关性最小的256对随机点,该方法称为rBRIEF。

Experiment & Result

OpenCV实现ORB特征检测与描述

#include <opencv2/core/core.hpp> 
#include <opencv2/highgui/highgui.hpp> 
#include <opencv2/imgproc/imgproc.hpp> 
#include <opencv2/features2d/features2d.hpp>

using namespace cv;

int main(int argc, char** argv) 
{ 
    Mat img_1 = imread("box.png"); 
    Mat img_2 = imread("box_in_scene.png");

    // -- Step 1: Detect the keypoints using STAR Detector 
    std::vector<KeyPoint> keypoints_1,keypoints_2; 
    ORB orb; 
    orb.detect(img_1, keypoints_1); 
    orb.detect(img_2, keypoints_2);

    // -- Stpe 2: Calculate descriptors (feature vectors) 
    Mat descriptors_1, descriptors_2; 
    orb.compute(img_1, keypoints_1, descriptors_1); 
    orb.compute(img_2, keypoints_2, descriptors_2);

    //-- Step 3: Matching descriptor vectors with a brute force matcher 
    BFMatcher matcher(NORM_HAMMING); 
    std::vector<DMatch> mathces; 
    matcher.match(descriptors_1, descriptors_2, mathces); 
    // -- dwaw matches 
    Mat img_mathes; 
    drawMatches(img_1, keypoints_1, img_2, keypoints_2, mathces, img_mathes); 
    // -- show 
    imshow("Mathces", img_mathes);

    waitKey(0); 
    return 0; 
}

result.png

OpenCV中ORB算法的部分源码实现

//计算Harris角点响应  
static void HarrisResponses(const Mat& img, vector<KeyPoint>& pts, int blockSize, float harris_k)  
{  
    CV_Assert( img.type() == CV_8UC1 && blockSize*blockSize <= 2048 );  

    size_t ptidx, ptsize = pts.size();  

    const uchar* ptr00 = img.ptr<uchar>();  
    int step = (int)(img.step/img.elemSize1());  
    int r = blockSize/2;  

    float scale = (1 << 2) * blockSize * 255.0f;  
    scale = 1.0f / scale;  
    float scale_sq_sq = scale * scale * scale * scale;  

    AutoBuffer<int> ofsbuf(blockSize*blockSize);  
    int* ofs = ofsbuf;  
    for( int i = 0; i < blockSize; i++ )  
        for( int j = 0; j < blockSize; j++ )  
            ofs[i*blockSize + j] = (int)(i*step + j);  

    for( ptidx = 0; ptidx < ptsize; ptidx++ )  
    {  
        int x0 = cvRound(pts[ptidx].pt.x - r);  
        int y0 = cvRound(pts[ptidx].pt.y - r);  

        const uchar* ptr0 = ptr00 + y0*step + x0;  
        int a = 0, b = 0, c = 0;  

        for( int k = 0; k < blockSize*blockSize; k++ )  
        {  
            const uchar* ptr = ptr0 + ofs[k];  
            int Ix = (ptr[1] - ptr[-1])*2 + (ptr[-step+1] - ptr[-step-1]) + (ptr[step+1] - ptr[step-1]);  
            int Iy = (ptr[step] - ptr[-step])*2 + (ptr[step-1] - ptr[-step-1]) + (ptr[step+1] - ptr[-step+1]);  
            a += Ix*Ix;  
            b += Iy*Iy;  
            c += Ix*Iy;  
        }  
        pts[ptidx].response = ((float)a * b - (float)c * c -  harris_k * ((float)a + b) * ((float)a + b))*scale_sq_sq;  
    }  
}  

//计算FAST角点的主方向  
static float IC_Angle(const Mat& image, const int half_k, Point2f pt, const vector<int> & u_max)  
{  
    int m_01 = 0, m_10 = 0;  

    const uchar* center = &image.at<uchar> (cvRound(pt.y), cvRound(pt.x));  

    // Treat the center line differently, v=0  
    for (int u = -half_k; u <= half_k; ++u)  
        m_10 += u * center[u];  

    // Go line by line in the circular patch  
    int step = (int)image.step1();  
    for (int v = 1; v <= half_k; ++v)  
    {  
        // Proceed over the two lines  
        int v_sum = 0;  
        int d = u_max[v];  
        for (int u = -d; u <= d; ++u)  
        {  
            int val_plus = center[u + v*step], val_minus = center[u - v*step];  
            v_sum += (val_plus - val_minus);  
            m_10 += u * (val_plus + val_minus);  
        }  
        m_01 += v * v_sum;  
    }  

    return fastAtan2((float)m_01, (float)m_10);  
}  


//计算ORB特征描述子
static void computeOrbDescriptor(const KeyPoint& kpt, const Mat& img, const Point* pattern, uchar* desc, int dsize, int WTA_K)  
{  
    float angle = kpt.angle;   
    //angle = cvFloor(angle/12)*12.f;  
    angle *= (float)(CV_PI/180.f);  
    float a = (float)cos(angle), b = (float)sin(angle);  

    const uchar* center = &img.at<uchar>(cvRound(kpt.pt.y), cvRound(kpt.pt.x));  
    int step = (int)img.step;  

#if 1  
    #define GET_VALUE(idx) \       //取旋转后一个像素点的值  
        center[cvRound(pattern[idx].x*b + pattern[idx].y*a)*step + \  
               cvRound(pattern[idx].x*a - pattern[idx].y*b)]  
#else  
    float x, y;  
    int ix, iy;  
    #define GET_VALUE(idx) \ //取旋转后一个像素点,插值法  
        (x = pattern[idx].x*a - pattern[idx].y*b, \  
        y = pattern[idx].x*b + pattern[idx].y*a, \  
        ix = cvFloor(x), iy = cvFloor(y), \  
        x -= ix, y -= iy, \  
        cvRound(center[iy*step + ix]*(1-x)*(1-y) + center[(iy+1)*step + ix]*(1-x)*y + \  
                center[iy*step + ix+1]*x*(1-y) + center[(iy+1)*step + ix+1]*x*y))  
#endif  

    if( WTA_K == 2 )  
    {  
        for (int i = 0; i < dsize; ++i, pattern += 16)//每个特征描述子长度为32个字节  
        {  
            int t0, t1, val;  
            t0 = GET_VALUE(0); t1 = GET_VALUE(1);  
            val = t0 < t1;  
            t0 = GET_VALUE(2); t1 = GET_VALUE(3);  
            val |= (t0 < t1) << 1;  
            t0 = GET_VALUE(4); t1 = GET_VALUE(5);  
            val |= (t0 < t1) << 2;  
            t0 = GET_VALUE(6); t1 = GET_VALUE(7);  
            val |= (t0 < t1) << 3;  
            t0 = GET_VALUE(8); t1 = GET_VALUE(9);  
            val |= (t0 < t1) << 4;  
            t0 = GET_VALUE(10); t1 = GET_VALUE(11);  
            val |= (t0 < t1) << 5;  
            t0 = GET_VALUE(12); t1 = GET_VALUE(13);  
            val |= (t0 < t1) << 6;  
            t0 = GET_VALUE(14); t1 = GET_VALUE(15);  
            val |= (t0 < t1) << 7;  

            desc[i] = (uchar)val;  
        }  
    }  
    else if( WTA_K == 3 )  
    {  
        for (int i = 0; i < dsize; ++i, pattern += 12)  
        {  
            int t0, t1, t2, val;  
            t0 = GET_VALUE(0); t1 = GET_VALUE(1); t2 = GET_VALUE(2);  
            val = t2 > t1 ? (t2 > t0 ? 2 : 0) : (t1 > t0);  

            t0 = GET_VALUE(3); t1 = GET_VALUE(4); t2 = GET_VALUE(5);  
            val |= (t2 > t1 ? (t2 > t0 ? 2 : 0) : (t1 > t0)) << 2;  

            t0 = GET_VALUE(6); t1 = GET_VALUE(7); t2 = GET_VALUE(8);  
            val |= (t2 > t1 ? (t2 > t0 ? 2 : 0) : (t1 > t0)) << 4;  

            t0 = GET_VALUE(9); t1 = GET_VALUE(10); t2 = GET_VALUE(11);  
            val |= (t2 > t1 ? (t2 > t0 ? 2 : 0) : (t1 > t0)) << 6;  

            desc[i] = (uchar)val;  
        }  
    }  
    else if( WTA_K == 4 )  
    {  
        for (int i = 0; i < dsize; ++i, pattern += 16)  
        {  
            int t0, t1, t2, t3, u, v, k, val;  
            t0 = GET_VALUE(0); t1 = GET_VALUE(1);  
            t2 = GET_VALUE(2); t3 = GET_VALUE(3);  
            u = 0, v = 2;  
            if( t1 > t0 ) t0 = t1, u = 1;  
            if( t3 > t2 ) t2 = t3, v = 3;  
            k = t0 > t2 ? u : v;  
            val = k;  

            t0 = GET_VALUE(4); t1 = GET_VALUE(5);  
            t2 = GET_VALUE(6); t3 = GET_VALUE(7);  
            u = 0, v = 2;  
            if( t1 > t0 ) t0 = t1, u = 1;  
            if( t3 > t2 ) t2 = t3, v = 3;  
            k = t0 > t2 ? u : v;  
            val |= k << 2;  

            t0 = GET_VALUE(8); t1 = GET_VALUE(9);  
            t2 = GET_VALUE(10); t3 = GET_VALUE(11);  
            u = 0, v = 2;  
            if( t1 > t0 ) t0 = t1, u = 1;  
            if( t3 > t2 ) t2 = t3, v = 3;  
            k = t0 > t2 ? u : v;  
            val |= k << 4;  

            t0 = GET_VALUE(12); t1 = GET_VALUE(13);  
            t2 = GET_VALUE(14); t3 = GET_VALUE(15);  
            u = 0, v = 2;  
            if( t1 > t0 ) t0 = t1, u = 1;  
            if( t3 > t2 ) t2 = t3, v = 3;  
            k = t0 > t2 ? u : v;  
            val |= k << 6;  

            desc[i] = (uchar)val;  
        }  
    }  
    else  
        CV_Error( CV_StsBadSize, "Wrong WTA_K. It can be only 2, 3 or 4." );  

    #undef GET_VALUE  
}  

reference

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Zachary_Co/article/details/78872012

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf

推荐文章

热门文章

相关标签