图像特征描述子之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

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法