特征点的描述
ORB选择了BRIEF作为特征描述方法,但是我们知道BRIEF是没有旋转不变性的,所以我们需要给BRIEF加上旋转不变性,把这种方法称为“Steer BREIF”。对于任何一个特征点来说,它的BRIEF描述子是一个长度为 n
的二值码串,这个二值串是由特征点周围 n 个点对( 2n 个点)生成的,现在我们将这 2n 个点 (xi,yi),i=1,2,⋯,2n 组成一个矩阵 S
Calonder建议为每个块的旋转和投影集合分别计算BRIEF描述子,但代价昂贵。ORB中采用了一个更有效的方法:使用邻域方向 θ
和对应的旋转矩阵 Rθ ,构建 S 的一个校正版本 Sθ
其中
而 θ
即我们在1.2中为特征点求得的主方向。
实际上,我们可以把角度离散化,即把360度分为12份,每一份是30度,然后我们对这个12个角度分别求得一个 Sθ
,这样我们就创建了一个查找表,对于每一个 θ ,我们只需查表即可快速得到它的点对的集合 Sθ。
1.3 解决描述子的区分性
BRIEF令人惊喜的特性之一是:对于 n
维的二值串的每个比特征位,所有特征点在该位上的值都满足一个均值接近于0.5,而方差很大的高斯分布。方差越大,说明区分性越强,那么不同特征点的描述子就表现出来越大差异性,对匹配来说不容易误配。但是当我们把BRIEF沿着特征点的方向调整为Steered BRIEF时,均值就漂移到一个更加分散式的模式。可以理解为有方向性的角点关键点对二值串则展现了一个更加均衡的表现。而且论文中提到经过PCA对各个特征向量进行分析,得知Steered BRIEF的方差很小,判别性小,各个成分之间相关性较大。
为了减少Steered BRIEF方差的亏损,并减少二进制码串之间的相关性,ORB使用了一种学习的方法来选择一个较小的点对集合。方法如下:
首先建立一个大约300k关键点的测试集,这些关键点来自于PASCAL2006集中的图像。
对于这300k个关键点中的每一个特征点,考虑它的 31×31
的邻域,我们将在这个邻域内找一些点对。不同于BRIEF中要先对这个Patch内的点做平滑,再用以Patch中心为原点的高斯分布选择点对的方法。ORB为了去除某些噪声点的干扰,选择了一个 5×5 大小的区域的平均灰度来代替原来一个单点的灰度,这里 5×5 区域内图像平均灰度的计算可以用积分图的方法。我们知道 31×31 的Patch里共有 N=(31−5+1)×(31−5+1) 个这种子窗口,那么我们要 N 个子窗口中选择2个子窗口的话,共有 C2N 种方法。所以,对于300k中的每一个特征点,我们都可以从它的 31×31 大小的邻域中提取出一个很长的二进制串,长度为 M=C2N ,表示为那么当300k个关键点全部进行上面的提取之后,我们就得到了一个 300k×M
的矩阵,矩阵中的每个元素值为0或1。
对该矩阵的每个列向量,也就是每个点对在300k个特征点上的测试结果,计算其均值。把所有的列向量按均值进行重新排序。排好后,组成了一个向量 T
, T的每一个元素都是一个列向量。
进行贪婪搜索:从 T
中把排在第一的那个列放到 R 中, T 中就没有这个点对了测试结果了。然后把 T 中的排下一个的列与 R 中的所有元素比较,计算它们的相关性,旭果相关超过了某一事先设定好的阈值,就扔了它,否则就把它放到 R 里面。重复上面的步骤,只到 R 中有256个列向量为止。如果把 T全找完也,也没有找到256个,那么,可以把相关的阈值调高一些,再重试一遍。
这样,我们就得到了256个点对。上面这个过程我们称它为rBRIEF。
opencv 计算BRIEF描述子函数如下:
const float factorPI = (float)(CV_PI/180.f);
static void computeOrbDescriptor(const KeyPoint& kpt,
const Mat& img, const Point* pattern,
uchar* desc)
{
float angle = (float)kpt.angle*factorPI;
float a = (float)cos(angle), b = (float)sin(angle);
const uchar* center = &img.at<uchar>(cvRound(kpt.pt.y), cvRound(kpt.pt.x));
const int step = (int)img.step;
#define GET_VALUE(idx) \
center[cvRound(pattern[idx].x*b + pattern[idx].y*a)*step + \
cvRound(pattern[idx].x*a - pattern[idx].y*b)]
if((cvRound(kpt.pt.y)==129)&&(cvRound(kpt.pt.x)==1178))
{
for(int k=0;k<3;k++)
{
printf("pattern_x=%d\n",(pattern[k].x));
printf("pattern_y=%d\n",(pattern[k].y));
printf("angle=%f\n",angle);
printf("y=%d\n",(cvRound(pattern[k].x*b + pattern[k].y*a)));
printf("x=%d\n",(cvRound(pattern[k].x*a - pattern[k].y*b)));
int y=cvRound(pattern[k].x*b + pattern[k].y*a);//y=x*sin+y*cos;//逆时针旋转
int x=cvRound(pattern[k].x*a - pattern[k].y*b);//x=x*cos-y*sin;
坐标系是以关键点坐标为圆心,以图像坐标的x,y坐标系,即y向下,x向右。
y对应row,x对应column
const uchar* center2 = &img.at<uchar>(y+129,x+1178);
printf("center2=%d\n",int(*center2));
printf("k=%d GET_VALUE(k)=%d\n",k,(GET_VALUE(k)));
}
int cnt=0;
for(int k=0;k<512;k++)
{
int y=cvRound(pattern[k].x*b + pattern[k].y*a);
int x=cvRound(pattern[k].x*a - pattern[k].y*b);
const uchar* center2 = &img.at<uchar>(y+129,x+1178);
if(int(*center2)==(GET_VALUE(k)))
{
cnt=cnt+1;
if(cnt==512)
printf("brief is sucess.\n");
}
}
}//if
for (int i = 0; i < 32; ++i, pattern += 16)
{
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;
}
#undef GET_VALUE
}
首先,要计算旋转后的坐标,去原图像中取对应的点,要看坐标系是什么的,是逆时针还是顺时针旋转,这里面一般都是逆时针旋转,而且brief选取的点是坐标取整进行取值的,一般是要进行双线性插值,比如sift就是;然后,Hls里面利用lineBufer 缓存31行数据,对应取数据;剩下的就是按照brief的计算方式取计算就可以了,最后,通过计算旋转后的点对应取值,并且按照pattern中的256个点对,比较提取32个byte的描述子。
HLS实现相比FPGA用verilog实现开发效率高了不知多少倍,至少5倍以上,但是其利用的资源相对比verilog的要多些,但没多的那么离谱,如果离谱了,那就是你hls里面的代码没有优化到位。