视觉里程计主要是通过图像对运动进行估计。一副中等分辨率的图像就是一个维度巨大的矩阵,我们无法对矩阵直接进行估计,其面临的将是海量的计算,因此我们有必要对图像进行特征提取。特征就是图像中比较特别的地方,例如:角点、边缘等等,并且这些角点在相机运动及不同光照下应该保持稳定。
特征点由关键点(keyPoint)与描述子(descriptor)组成,关键点是指二维图像中的坐标,描述子是指方向,大小等关键点的信息描述。时至今日,学者们已经提出了非常多的图像特征,常见的有:Harris,SIFT,SURF,ORB等等。虽然很多特征提取方法精度及鲁棒性很好,但其计算量巨大,明显不适合在当前使用。我们适当降低精度和鲁棒性,选择ORB特征作为图像特征提取方法,其余方法我们不展开介绍,感兴趣的读者自行了解。
ORB特征由FAST关键点和BRIEF描述子构成,我们先来介绍FAST( Features from Accelerated Segments Test )角点的提取。

上图为FAST角点的示意图,其核心思想是检测角点周围的灰度情况,速度非常快。给定一个像素坐标
,将他的亮度记作
,我们设置一个阈值
,然后选取以
为中心,半径为3的圆上的16个像素点。按照下式进行状态计算:
如果有连续的
个点状态为darker或brighter,则认为
为关键点。通常
取12,为了加速计算,通常对1,5,9,13首先进行检测,满足条件后才进行灰度判断。通常检测完成后,还需要再使用非极大值抑制(NMS)再处理一次,避免出现多个角点的情况发生。
FAST角点不具有尺度及方向性。针对尺度问题通常通过构建如下的图像金字塔来解决,在每一层金字塔进行检测,并通过距离阈值进行筛选。ORB中针对方向性采用质心法进行了改进,感兴趣的读者,可以阅读ORB-SLAM v3源码或者高翔的《视觉slam十四讲》。

BRIEF描述子是一种二进制描述子,由0或1构成,其在特征点法的SLAM框架中较为常见。笔者现在从自动驾驶转到了AR方向,也使用光流跟踪取代了描述子匹配,故在此不展开BRIEF的介绍,感兴趣的读者阅读源码即可。

上图为TUM数据集双目鱼眼相机提取的FAST角点可视图。程序编码我们使用openCV作为工具,实现很简单,这里不给出具体代码了。筛选FAST角点经常使用到harris响应,我们在这里再介绍一下harris角点,关注ORB-SLAM的同学可以忽略harris响应了。
harris响应建立在像素附近像素在梯度及幅值上均有较大的变化。基本思想是:选用窗口函数在图像上任意滑动,如果灰度变化较大,则认为有角点的存在。
假设窗口函数为
,
表示图像滑动前的灰度值,
表示图像滑动后的灰度值,
表示窗口平移量,像素在滑动窗口内的变化量为:
对上式进行泰勒展开:
中间矩阵的
分别表示像素在两个方向上的梯度。一般来说,矩阵对应的两个特征值如果都大,则窗口内含有角点;如果一大一小,则是边缘;如果都很小,则为平面。harris角点将特征值计算隐含到了harris响应
当中,可通过harris响应直接对含有角点直接进行判断。
其中
取0.04-0.06。
static void HarrisResponses(const Mat& img, vector<KeyPoint>& pts, int blockSize, float harris_k)
{
//CV_Assert() means if the value in() is false,return a mistake message
CV_Assert( img.type() == CV_8UC1 && blockSize*blockSize <= 2048 );
size_t ptidx, ptsize = pts.size();
const uchar* ptr00 = img.ptr<uchar>(); // get the ptr00 row in image
int step = (int)(img.step/img.elemSize1());// the number of bytes of a element' one channel
int r = blockSize/2; // circle radius R
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;
}
// Harris response function
pts[ptidx].response = ((float)a * b - (float)c * c -
harris_k * ((float)a + b) * ((float)a + b))*scale_sq_sq;
}
}