RobHess的SIFT代码解析步骤四

平台:win10 x64 +VS 2015专业版 +opencv-2.4.11 + gtk_-bundle_2.24.10_win32

主要参考:1.代码:RobHess的SIFT源码

2.书:王永明 王贵锦 《图像局部不变性特征与描述》

 

SIFT四步骤和特征匹配及筛选:

步骤一:建立尺度空间,即建立高斯差分(DoG)金字塔dog_pyr

步骤二:在尺度空间中检测极值点,并进行精确定位和筛选创建默认大小的内存存储器

步骤三:特征点方向赋值,完成此步骤后,每个特征点有三个信息:位置、尺度、方向

步骤四:计算特征描述子

SIFT后特征匹配:KD树+BBF算法

SIFT后特征匹配后错误点筛选:RANSAC算法

 

 

步骤四:计算特征描述子

问题及解答:

 

(1)问题描述:如何确定子采样区域?思路是什么?

答:SIFI 描述子h(x, y, θ)是对特征点附近邻域内高斯图像梯度统计结果的一种表示,它是一个三维的阵列,但通常将它表示成一个矢量。矢量是通过对三维阵列按一定规律进行排列得到的。特征描述子与特征点所在的尺度有关,因此,对梯度的求取应在特征点对应的高斯图像上进行。将特征点附近邻域划分成Bp X Bp个子区域,每个子区域的尺寸为mσ个像元,其中,m=3,Bp=4。σ为特征点的尺度值。考虑到实际计算时,需要采用双线性插值,计算的图像区域为mσ(Bp+ 1)。如果再考虑旋转的因素,那么,实际计算的图像区域应大mσ(Bp+ 1)√2。

 

 参考书P134及图6-7

 

 

(2)问题描述:如何生成描述子?思路是什么?

答:

步骤一:旋转图像至主方向

为了保证特征矢量具有旋转不变性,需要以特征点为中心,将特征点附近邻域内(mσ(Bp+ 1)√2 x mσ(Bp+ 1)√2)图像梯度的位置和方向旋转一个方向角θ,即将原图像x轴转到与主方向相同的方向。旋转公式如下:

  在特征点附近邻域图像梯度的位置和方向旋转后,再以特征点为中心,在旋转后的图像中取一个mσBp x mσBp大小的图像区域。并将它等间隔划分成Bp X Bp个子区域,每个间隔为mσ像元。

 

步骤二:生成特征向量
  在每子区域内计算8个方向的梯度方向直方图,绘制每个梯度方向的累加值,形成一个种子点。与求特征点主方向时有所不同,此时,每个子区域的梯度方向直方图将0° ~360°划分为8个方向范围,每个范围为45°,这样,每个种子点共有8个方向的梯度强度信息。由于存在4X4(Bp X Bp)个子区域,所以,共有4X4X8=128个数据,最终形成128维的SIFT特征矢量。同样,对于特征矢量需要进行高斯加权处理,加权采用方差为mσBp/2的标准高斯函数,其中距离为各点相对于特征点的距离。使用高斯权重的是为了防止位置微小的变化给特征向量带来很大的改变,并且给远离特征点的点赋予较小的权重,以防止错误的匹配。

 参考书P134及图6-8和图6-9

 

(3)问题描述:对于旋转之后的点进行三线性插值,具体是怎样操作的?思路是什么?

答:

参看:

三线性插值:

1)SIFT算法:特征描述子:https://www.cnblogs.com/JiePro/p/sift_4.html

2)描述向量:https://blog.csdn.net/zddblog/article/details/7521424

3)4.3 三线性插值:https://www.sun11.me/blog/2016/sift-implementation-in-matlab/

4) 为什么进行三线性插值?https://blog.csdn.net/fzthao/article/details/62424271

5)我与插值萍水相逢续(3): 三线性插值(Trilinear Interpolation)原理及使用:https://blog.csdn.net/Xyc19930930/article/details/77806504

6)三线性插值(三维线性插值):https://blog.csdn.net/webzhuce/article/details/86585489

7)线性插值&双线性插值&三线性插值:https://blog.csdn.net/sinat_39372048/article/details/81361038

 

(4)问题描述:如何归一化特征向量?

答:为了去除光照变化的影响,需对上述生成的特征向量进行归一化处理,在归一化处理后,对特征矢量大于0.2的要进行截断处理,即大于0.2的值只取0.2,然后重新进行一次归一化处理,其目的是为了提高鉴别性。

 

(5)问题描述:RobHess的SIFT源码如何实现步骤四,大概思路是这样的?

答:5.1)代码及说明:

/*步骤四:计算特征描述子*/

//计算特征点序列中每个特征点的特征描述子向量

compute_descriptors( features, gauss_pyr, descr_width, descr_hist_bins );

 

(5.2)compute_descriptors代码及说明:

生成特征描述子

/*计算特征点序列中每个特征点的特征描述子向量

参数:

features:特征点序列

gauss_pyr:高斯金字塔图像组

d:计算方向直方图时,将特征点附近划分为d*d个区域,每个区域生成一个直方图

n:每个方向直方图的bin个数*/

static void compute_descriptors( CvSeq* features, IplImage*** gauss_pyr, int d, int n)

{

    struct feature* feat;

    struct detection_data* ddata;

    double*** hist;//d*d*n的三维直方图数组

    int i, k = features->total;//特征点的个数

 

    //遍历特征点序列中的特征点

    for( i = 0; i < k; i++ )

    {

        //调用宏,获取序列features中的第i个元素,并强制转换为struct feature类型

        feat = CV_GET_SEQ_ELEM( struct feature, features, i );

        //调用宏feat_detection_data来提取参数feat中的feature_data成员并转换为detection_data类型的指针

        ddata = feat_detection_data( feat );

        //计算特征点附近区域的方向直方图,此直方图在计算特征描述子中要用到,返回值是一个d*d*n的三维数组

        hist = descr_hist( gauss_pyr[ddata->octv][ddata->intvl], ddata->r,

            ddata->c, feat->ori, ddata->scl_octv, d, n );

        //将某特征点的方向直方图转换为特征描述子向量,对特征描述子归一化并将所有元素转化为整型,存入特征点feat中

        hist_to_descr( hist, d, n, feat );

        //释放特征点的方向直方图

        release_descr_hist( &hist, d );

    }

}

 

问题:hist = descr_hist( gauss_pyr[ddata->octv][ddata->intvl], ddata->r,ddata->c, feat->ori, ddata->scl_octv, d, n );中feat->ori为特征点的方向, 而ddata->scl_octv为特征点所在组的尺度而不是feat->scl特征点的尺度?

书P133 未解决。

 

(5.2.1)descr_hist代码及说明: 

答:

/*计算特征点附近区域的方向直方图,此直方图在计算特征描述子中要用到,返回值是一个d*d*n的三维数组

参数:

img:图像指针

r:特征点所在的行

c:特征点所在的列

ori:特征点的主方向

scl:特征点的尺度//特征点所在组的尺度

d:计算方向直方图时,将特征点附近划分为d*d个区域,每个区域生成一个直方图,默认d为4

n:每个直方图中bin的个数

返回值:double类型的三维数组,即一个d*d的二维数组,数组中每个元素是一个有n个bin的直方图数组*/

static double*** descr_hist( IplImage* img, int r, int c, double ori,

                            double scl, int d, int n )

{

    double*** hist;//d*d*n的三维直方图数组

    double cos_t, sin_t, hist_width, exp_denom, r_rot, c_rot, grad_mag,

        grad_ori, w, rbin, cbin, obin, bins_per_rad, PI2 = 2.0 * CV_PI;

    int radius, i, j;

 

    //为直方图数组分配空间

    hist = calloc( d, sizeof( double** ) );//为第一维分配空间

    for( i = 0; i < d; i++ )

    {

        hist[i] = calloc( d, sizeof( double* ) );//为第二维分配空间

        for( j = 0; j < d; j++ )

            hist[i][j] = calloc( n, sizeof( double ) );//为第三维分配空间

    }

    //为了保证特征描述子具有旋转不变性,要以特征点为中心,在附近邻域内旋转θ角,即旋转为特征点的方向

    cos_t = cos( ori );

    sin_t = sin( ori );

    bins_per_rad = n / PI2;

  //乘以8/2Π,8为直方图有8个bin,2Π为角度取值范围的长度,把方向的索引归于0~8.

    exp_denom = d * d * 0.5; //4*4*0.5

    //计算特征描述子过程中,特征点周围的d*d个区域中,每个区域的宽度为m*σ个像素,

        SIFT_DESCR_SCL_FCTR即m的默认值,σ为特征点的尺度,m=3,书P133

    hist_width = SIFT_DESCR_SCL_FCTR * scl;

    //考虑到要进行双线性插值,每个区域的宽度应为:SIFT_DESCR_SCL_FCTR * scl * ( d + 1.0 )

    //在考虑到旋转因素,每个区域的宽度应为:SIFT_DESCR_SCL_FCTR * scl * ( d + 1.0 ) * sqrt(2)

    //所以搜索的半径是:SIFT_DESCR_SCL_FCTR * scl * ( d + 1.0 ) * sqrt(2) / 2

    radius = hist_width * sqrt(2) * ( d + 1.0 ) * 0.5 + 0.5;

    //遍历每个区域的像素

    for( i = -radius; i <= radius; i++ )

        for( j = -radius; j <= radius; j++ )

        {

            //坐标旋转为主方向

            c_rot = ( j * cos_t - i * sin_t ) / hist_width;

            r_rot = ( j * sin_t + i * cos_t ) / hist_width;

            rbin = r_rot + d / 2 - 0.5;

            cbin = c_rot + d / 2 - 0.5;

            if( rbin > -1.0  &&  rbin < d  &&  cbin > -1.0  &&  cbin < d )

                if( calc_grad_mag_ori( img, r + i, c + j, &grad_mag, &grad_ori ))

                {

                    grad_ori -= ori;

                    while( grad_ori < 0.0 )

                        grad_ori += PI2;

                    while( grad_ori >= PI2 )

                        grad_ori -= PI2;  //把梯度方向归入[0,2Π]

 

                    obin = grad_ori * bins_per_rad; //梯度方向*8/2Π归到[0,8]

                    w = exp( -(c_rot * c_rot + r_rot * r_rot) / exp_denom ); 

      //exp( -(c_rot ^2 + r_rot^2) / d*d*0.5)=exp( -(c_rot ^2 + r_rot^2) / (2*(0.5d)^2))

                    interp_hist_entry( hist, rbin, cbin, obin, grad_mag * w, d, n );

                }

        }

        return hist;

}

 

问题1:所需的图像区域半径radius怎么计算?

答:将关键点附近的区域划分为d*d(Lowe建议d=4)个子区域,每个子区域作为一个种子点,每个种子点有8个方向。考虑到实际计算时,需要采用三线性插值,所需图像窗口边长为3x3xσ_oct x(d+1)  。在考虑到旋转因素(方便下一步将坐标轴旋转到关键点的方向),如下图所示,实际计算所需的图像区域半径为:

参看博客:https://blog.csdn.net/lingyunxianhe/article/details/79063547 4.1.1、确定计算描述子所需的区域

 

问题2:将坐标轴旋转为关键点的方向,以确保旋转不变性。坐标怎么计算?

答:

 将邻域内的采样点分配到对应的子区域内,将子区域内的梯度值分配到8个方向上,计算其权值。

旋转后的采样点 落在子区域的下标为

参看博客:https://blog.csdn.net/lingyunxianhe/article/details/79063547 4.1.2、坐标轴旋转至主方向 和 4.1.3、梯度直方图的生成

 

 

(5.2.1.1) calc_grad_mag_ori代码及说明:

答:参看RobHess的SIFT代码解析步骤三(5.2.2.1)

 

(5.2.1.2)interp_hist_entry代码及说明:

答:

//将一个条目插入到形成特征描述符的方向直方图数组中,该条目分配最多8个方向,对于每个维度每个方向乘以(1-d) 的权重,其中d是以bin测量的bin中心值距离。

//hist为方向直方图的三维数组

//rbin子行

//cbin子列

//obin子方向

//mag权重

//d方向直方图的宽度

//n每个直方图中bin的个数

//双线性插值,此处网上未统一,本人理解是三线性插值

static void interp_hist_entry( double*** hist, double rbin, double cbin,

                              double obin, double mag, int d, int n )

{

    double d_r, d_c, d_o, v_r, v_c, v_o;

    double** row, * h;

    int r0, c0, o0, rb, cb, ob, r, c, o;

 

    r0 = cvFloor( rbin ); //向下取整

    c0 = cvFloor( cbin );

    o0 = cvFloor( obin );

    d_r = rbin - r0; //小数余项

    d_c = cbin - c0;

    d_o = obin - o0;

//这里也可以看出前面rbin,cbin为何减0.5,这样原点上这点d_r,d_c均为0.5,即原点上这点的方向将被平均分配叠加在它周围4个直方图上面。

    for( r = 0; r <= 1; r++ ) //沿着行方向不超过1个单位长度

    {

        rb = r0 + r;

        if( rb >= 0  &&  rb < d ) //如果此刻还在真正的描述子区间内

        {

            v_r = mag * ( ( r == 0 )? 1.0 - d_r : d_r ); //对近邻两行的贡献因子

            row = hist[rb]; //第rb行上的hist

            for( c = 0; c <= 1; c++ ) //沿着列方向不超过1个单位长度

            {

                cb = c0 + c;

                if( cb >= 0  &&  cb < d ) //如果此刻还在真正的描述子区间内

                {

                    v_c = v_r * ( ( c == 0 )? 1.0 - d_c : d_c ); //对近邻两列的贡献因子

                    h = row[cb]; //h表示第rb行cb列上的hist

                    for( o = 0; o <= 1; o++ ) //沿着直方图方向不超过1个单位长度

                    {

                        ob = ( o0 + o ) % n; //n=8,8个柱子

                        v_o = v_c * ( ( o == 0 )? 1.0 - d_o : d_o ); //对近邻两个方向的贡献因子

                        h[ob] += v_o;

                    }

                }

            }

        }

    }

}

三线性插值:

插值计算每个种子点八个方向的梯度。

采样点在子区域中的下标(x'',y'')                              (图中蓝色窗口内红色点)线性插值,计算其对每个种子点的贡献。如图中的红色点,落在第0行和第1行之间,对这两行都有贡献。对第0行第3列种子点的贡献因子为dr,对第1行第3列的贡献因子为1-dr,同理,对邻近两列的贡献因子为dc和1-dc,对邻近两个方向的贡献因子为do和1-do。则最终累加在每个方向上的梯度大小为:


其中k,m,n为0(像素点超出了对要插值区间的四个邻近子区间所在范围)或为1(像素点处在对要插值区间的四个邻近子区间之一所在范围)。

 

参看博客:https://blog.csdn.net/lingyunxianhe/article/details/79063547 4.2.1、描述子生成总括

 

 

(5.2.2)hist_to_descr代码及说明: 

答:

/*将某特征点的方向直方图转换为特征描述子向量,对特征描述子归一化并将所有元素转化为整型,存入指定特征点中

参数:

hist:d*d*n的三维直方图数组

d:计算方向直方图时,将特征点附近划分为d*d个区域,每个区域生成一个直方图

n:每个直方图的bin个数

feat:特征点指针,将计算好的特征描述子存入其中*/

static void hist_to_descr( double*** hist, int d, int n, struct feature* feat )

{                               

    int int_val, i, r, c, o, k = 0;

 

    //遍历d*d*n的三维直方图数组,将其中的所有数据(一般是128个)都存入feat结构的descr成员中

    for( r = 0; r < d; r++ )

        for( c = 0; c < d; c++ )

            for( o = 0; o < n; o++ )

                feat->descr[k++] = hist[r][c][o];

 

    feat->d = k;//特征描述子的维数,一般是128

    //归一化特征点的特征描述子,即将特征描述子数组中每个元素除以特征描述子的模

    normalize_descr( feat );

 

    //遍历特征描述子向量,将超过阈值SIFT_DESCR_MAG_THR的元素强行赋值SIFT_DESCR_MAG_THR

    for( i = 0; i < k; i++ )

        if( feat->descr[i] > SIFT_DESCR_MAG_THR )

            feat->descr[i] = SIFT_DESCR_MAG_THR;

    //再次归一化特征描述子向量,两次归一化,目的为了提高特征的鉴别性

    normalize_descr( feat );

    //遍历特征描述子向量,每个元素乘以系数SIFT_INT_DESCR_FCTR来变为整型,并且最大值不能超过255

    for( i = 0; i < k; i++ )

    {

        int_val = SIFT_INT_DESCR_FCTR * feat->descr[i];

        feat->descr[i] = MIN( 255, int_val );

    }

}

 

(5.2.2.1)normalize_descr代码及说明: 

答:

/*归一化特征点的特征描述子,即将特征描述子数组中每个元素除以特征描述子的模*/

static void normalize_descr( struct feature* feat )

{

    double cur, len_inv, len_sq = 0.0;

    int i, d = feat->d;//特征描述子的维数

 

    //求特征描述子的模

    for( i = 0; i < d; i++ )

    {

        cur = feat->descr[i];

        len_sq += cur*cur;

    }

    len_inv = 1.0 / sqrt( len_sq ); //sqrt( len_sq )为特征描述子的模

    //特征描述子中每个元素除以特征描述子的模,完成归一化

    for( i = 0; i < d; i++ )

        feat->descr[i] *= len_inv;

}

 

(5.2.3)release_descr_hist代码及说明:

答:

/*释放计算特征描述子过程中用到的方向直方图的内存空间

参数:

hist:方向直方图的指针,是一个d*d*n的三维直方图数组

d:直方图数组前两维的维数

*/

static void release_descr_hist( double**** hist, int d )

{

    int i, j;

    for( i = 0; i < d; i++)

    {

        for( j = 0; j < d; j++ )

            free( (*hist)[i][j] );//释放第三维的内存

        free( (*hist)[i] );//释放第二维的内存

    }

    free( *hist );//释放第一维的内存

    *hist = NULL;

}

 

 

SIFT四个步骤完成后,需要对特征点排序,并释放空间

(1)问题描述:RobHess的SIFT源码如何实现,大概思路是这样的?

答:1.1)代码及说明:

//按特征点尺度的降序排列序列中的元素的顺序,feature_cmp是自定义的比较函数
cvSeqSort( features, (CvCmpFunc)feature_cmp, NULL );
//将CvSeq类型的特征点序列features转换为通用的struct feature类型的数组feat
n = features->total;//特征点个数
*feat = calloc( n, sizeof(struct feature) );//分配控件
//将序列features中的元素拷贝到数组feat中,返回数组指针给feat
*feat = cvCvtSeqToArray( features, *feat, CV_WHOLE_SEQ );
//释放特征点数组feat中所有特征点的feature_data成员,因为此成员中的数据在检测完特征
//点后就没用了
for( i = 0; i < n; i++ )
{
free( (*feat)[i].feature_data );
(*feat)[i].feature_data = NULL;
}
//释放各种临时数据的存储空间
cvReleaseMemStorage( &storage );
cvReleaseImage( &init_img );
release_pyr( &gauss_pyr, octvs, intvls + 3 );
release_pyr( &dog_pyr, octvs, intvls + 2 );
//返回检测到的特征点的个数
return n;

(1.1.1)feature_cmp代码及说明:

答:

/*比较函数,将特征点按尺度的降序排列,用在序列排序函数CvSeqSort中

参数:

feat1:第一个特征点的指针

feat2:第二个特征点的指针

param:用户自定义参数,这里不使用

返回值:如果feat1的尺度大于feat2的尺度,返回1;否则返回-1;若相等返回0(好像反了)*/

static int feature_cmp( void* feat1, void* feat2, void* param )

{

    //将输入的参数强制转换为struct feature类型的指针

    struct feature* f1 = (struct feature*) feat1;

    struct feature* f2 = (struct feature*) feat2;

 

    //比较两个特征点的尺度值

    if( f1->scl < f2->scl )

        return 1;

    if( f1->scl > f2->scl )

        return -1;

    return 0;

}

 

(1.1.2)cvSeqSort函数说明:

答: 函数原型:void cvSeqSort( CvSeq* seq, CvCmpFunc func, void* userdata=NULL );

功能:函数 cvSeqSort 使用特定的标准对序列进行排序。
seq待排序的序列
func比较函数,按照元素间的大小关系返回负数,零,正数(见:上面的声明和下面的例子) --相关函数为 C 运行时库中的 qsort, 后者(qsort)不使用参数userdata.
userdata传递给比较函数的用户参数;有些情况下,可避免全局变量的使用
参考:cvSeqSort函数详细说明——https://blog.csdn.net/c20081052/article/details/38037553

 

(1.1.3)cvCvtSeqToArray函数说明:

答:函数原型:void* cvCvtSeqToArray(const CvSeq* Seq, void* elements, slice=CV_WHOLE_SEQ)

函数功能:将整个序列或子序列复制到指定的缓冲区, 并将指针返回到缓冲区(连续的内存中)

seq//序列

elements//目的数组指针,指针指向的空间必须足够大,它应该是指向数据的指针

slice//切片,要复制到数组的序列部分

 

(1.1.4)release_pyr代码及说明:

答:

/*释放金字塔图像组的存储空间

参数:

pyr:金字塔图像组的指针

octvs:金字塔的组数

n:每一组的图像数*/

static void release_pyr( IplImage**** pyr, int octvs, int n )

{

    int i, j;

    for( i = 0; i < octvs; i++ )

    {

        for( j = 0; j < n; j++ )

            cvReleaseImage( &(*pyr)[i][j] );//释放每个图像

        free( (*pyr)[i] );//释放每个组

    }

    free( *pyr );//释放金字塔

    *pyr = NULL;

}

 

posted on 2019-06-03 17:32  Alliswell_WP  阅读(1754)  评论(0编辑  收藏  举报

导航