OpenCV代码精妙之二 FAST特征点检测 巧妙避免大量if

FAST源码分析

FAST 是被广泛使用的一种特征点,由 英国学者 提出,
在SLAM 这样的性能要求高的场合中被大量使用,

顾名思义, FAST的最大特点就是 快速,好像是说能达到 SIFT 速度的100倍左右
具体多少不重要,能快个数量级是肯定的

FAST 具体方法为比较图像中某个点的像素值和周围像素点的差异,
如果 具有一定数量的邻域点 和 中心点(即待判断是否为特征点的像素点) 像素差距超过 既定阈值,
那么 该中心点被判断为 特征点

既然是 比较 像素值, 那么实现时 最直观的方法就是 使用 if , 进行各种比较,
如下是 FAST 作者 在github上给出的代码
这里写图片描述

作者在其原始论文《Machine learning for high-speed corner detection》中给出了 这种代码是怎么来的,即,使用决策树在数据集上进行训练,最后把决策树转换为C语言代码。

这样的代码执行效率非常低,因为 大量的逻辑判断,导致 无法发挥CPU 的流水线 优势,
《深入理解计算机系统》上面阐述过 相关原理, CPU 会 预执行指令,碰到逻辑判断也会如此,所谓的“冒险”,如果预测失败,那么就再返回到 正确的分支上继续执行。

好了, OpenCV 的代码是怎么实现的呢? 看不到大量的if, 当然 OpenCV 顺手把 SSE 的版本也实现了下
抠出来的关键代码如下

	int patternSize = 16;
	Mat img = _img.getMat();
	const int K = patternSize/2, N = patternSize + K + 1;

	int i, j, k, pixel[25];
	makeOffsets2(pixel, (int)img.step, patternSize);

	keypoints.clear();

	threshold = std::min(std::max(threshold, 0), 255);


	// 中心点和周围点的比较情况: 分类成为darker、similar、brighter三种
	uchar threshold_tab[512];
	for( i = -255; i <= 255; i++ )
		threshold_tab[i+255] = (uchar)(i < -threshold ? 1 : i > threshold ? 2 : 0);

	AutoBuffer<uchar> _buf( (img.cols+16) * 3 * (sizeof(int) + sizeof(uchar)) + 128);
	uchar* buf[3];

	// //保存对应角点强度值,否则为0, 强度值一个 uchar 类型就够了
	buf[0] = _buf; buf[1] = buf[0] + img.cols; buf[2] = buf[1] + img.cols;

	//保存角点位置,+1为了存储这一行的角点总数
	int* cpbuf[3];
	cpbuf[0] = (int*)alignPtr(buf[2] + img.cols, sizeof(int)) + 1;
	cpbuf[1] = cpbuf[0] + img.cols + 1;
	cpbuf[2] = cpbuf[1] + img.cols + 1;
	memset(buf[0], 0, img.cols*3);

	// 逐行进行检测
	for(i = 3; i < img.rows-2; i++)
	{
		const uchar* ptr = img.ptr<uchar>(i) + 3;
		uchar* curr = buf[(i - 3)%3];
		int* cornerpos = cpbuf[(i - 3)%3];
		memset(curr, 0, img.cols);
		int ncorners = 0;

		if( i < img.rows - 3 )
		{
			j = 3;

			for( ; j < img.cols - 3; j++, ptr++ )
			{
				int v = ptr[0];
				const uchar* tab = &threshold_tab[0] - v + 255;
				int d = tab[ptr[pixel[0]]] | tab[ptr[pixel[8]]];

				if( d == 0 )
					continue;

				d &= tab[ptr[pixel[2]]] | tab[ptr[pixel[10]]];
				d &= tab[ptr[pixel[4]]] | tab[ptr[pixel[12]]];
				d &= tab[ptr[pixel[6]]] | tab[ptr[pixel[14]]];

				if( d == 0 )
					continue;

				d &= tab[ptr[pixel[1]]] | tab[ptr[pixel[9]]];
				d &= tab[ptr[pixel[3]]] | tab[ptr[pixel[11]]];
				d &= tab[ptr[pixel[5]]] | tab[ptr[pixel[13]]];
				d &= tab[ptr[pixel[7]]] | tab[ptr[pixel[15]]];

				if( d & 1 )      中心值大,周围小的情况
				{
					int vt = v - threshold, count = 0;

					for( k = 0; k < N; k++ ) //且连续一半的像素点灰度差值( v-x > threshold )大于阈值  
					{
						int x = ptr[pixel[k]];
						if(x < vt)
						{
							if( ++count > K )
							{
								cornerpos[ncorners++] = j;
								if(nonmax_suppression)
									curr[j] = (uchar)cornerScore2(ptr, pixel, threshold);
								break;
							}
						}
						else
							count = 0;
					}
				}

				if( d & 2 )
				{
					int vt = v + threshold, count = 0;

					for( k = 0; k < N; k++ )
					{
						int x = ptr[pixel[k]];
						if(x > vt)
						{
							if( ++count > K )
							{
								cornerpos[ncorners++] = j;
								if(nonmax_suppression)
									curr[j] = (uchar)cornerScore2(ptr, pixel, threshold);
								break;
							}
						}
						else
							count = 0;
					}
				}
			} // end of for 列
		}

		cornerpos[-1] = ncorners; 存储第i行上的角点总数量

		if( i == 3 )
			continue;

		const uchar* prev = buf[(i - 4 + 3)%3]; // 第m-1行的角点强度
		const uchar* pprev = buf[(i - 5 + 3)%3];
		cornerpos = cpbuf[(i - 4 + 3)%3]; 存储 第 m-1 行 角点的列位置
		ncorners = cornerpos[-1]; // 其实是第 m-1 行 角点个数

		// 假设当前把第 m 行的点都检测了,如下代码实际上是在处理 第 m-1 行 !!!!, prev是第m-1行的信息
		// pprev是第 m-2 行的信息, curr是第m行的信息,所以插入的特征点的纵坐标为 i-1
		for( k = 0; k < ncorners; k++ )
		{
			j = cornerpos[k];
			int score = prev[j]; 
			if( !nonmax_suppression ||
				(score > prev[j+1] && score > prev[j-1] &&
				score > pprev[j-1] && score > pprev[j] && score > pprev[j+1] &&
				score > curr[j-1] && score > curr[j] && score > curr[j+1]) )
			{
				keypoints.push_back(KeyPoint((float)j, (float)(i-1), 7.f, -1, (float)score));
			}
		}
	} // end of for 行

避免大量 if 的核心原理就在于, 利用了 实际需要比较的都是像素数据, 即整数, 范围为0-255,
通过使用 算术运算,判断 结果的正负 ,间接实现了 像素的 大小比较!
看到这样的代码,不得不 佩服 作者功力之深厚!!!

要比较出两个数的大小是否超过某个阈值,那么 需要将这两个数字相减,
然后 将 差 和阈值进行比较

OpenCV 的取巧之处在于 差 和 阈值的比较方法很独特,
先是利用 先验信息 形成了一个 临时数组threshold_tab, 该数组的计算方法如下

	uchar threshold_tab[512];
	for( i = -255; i <= 255; i++ )
		threshold_tab[i+255] = (uchar)(i < -threshold ? 1 : i > threshold ? 2 : 0);

计算结果如下图所示
这里写图片描述

将某两个像素 进行 比较的 方法如下
先获取 第一个像素像素值,假定为 v1
另一个像素值大小为 v2
const uchar* tab = &threshold_tab[0] - v1 + 255;
int d = tab[v2]

d的值 就能表明 v1 和 v2 的差异是否超过 特定阈值!
更妙的地方是 , 如果 v1 和 v2 的差异如果 超过阈值, 那么d 要么是1, 要么是2
分别 对应 v1 > v2+ th 和 v2 > v1 + th
如果 在 阈值范围内, 那么 d的值 为0

具体内存寻址 如 下图所示(检测阈值为10的情况)
在这里插入图片描述

而 1 和 2 的共同点时 二进制 表示中 都有 一位 为1
因此, FAST 中 大量的像素差异比较 可以 转换为 位运算!
所以代码中有大量这样的代码
d &= tab[ptr[pixel[1]]] | tab[ptr[pixel[9]]];

即在获取 比较结果
比如

				int d = tab[ptr[pixel[0]]] | tab[ptr[pixel[8]]];

				if( d == 0 )
					continue;

即表明 ptr[pixel[0]] 和 ptr[pixel[8]] 都和 当前像素值比较接近!
因为 此时 或运算 两边都是0

这样, 避免了大量 if
厉害了, OpenCV !

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值