我以前研究过一段时间的OpenMV的源码,当时的功力太浅,看不大懂,现在又重新的翻出来看。
先确定代码在哪里,OpenMV是在资源受限的情况下执行视觉算法,所以里面的很多写法都是高效的,被优化过的,这也是我读的一个原因。
第一先读第一个文件,是一个角点的快速查找算法。图像基础的操作都被封装在了我现在展示的这个文件里面。
什么是角点?
角点通常被定义为两条边的交点,或者说,角点的局部邻域应该具有两个不同区域的不同方向的边界。角点检测(Corner Detection)是计算机视觉系统中获取图像特征的一种方法,广泛应用于运动检测、图像匹配、视频跟踪、三维重建和目标识别等,也可称为特征点检测。
这张图可以说是非常的简单明了。
角点的基本算法:选取一个局部窗口,将这个窗口沿着各个方向移动,计算移动前后窗口内像素的差异的多少进而判断窗口对应的区域是否是角点。
这个就是数学上的描述
我们寻找的角点就是去滑动判断。
这个就是我说的常见操作被打包的地方
它提供了低级图像处理操作的定义和函数。像素格式,基本图像统计,滤波,边缘检测,形状检测,条码识读等。
它针对嵌入式设备和微控制器设计,侧重效率和代码体积小。
使用定点数代替浮点数。直接对原始像素缓冲区进行操作。
支持常见的图像格式,像BMP,PPM,JPEG等。以及基本的机器视觉功能,比如模板匹配,QR码识读。
有在图像上绘制基本形状,线条,文字等的函数。
在可用时使用DMA和SIMD指令做硬件加速。不可用时回退到C实现。
使用C语言编写,但可以通过FFI在更高级语言如MicroPython或Arduino中使用。
OpenMV开源开发,作为他们的机器视觉相机模块的一部分。但可以独立使用。
这个是要看的算法的函数
这个算法现在讨论的很少,我就简单的说下:自适应通用角点检测(Adaptive and Generic Accelerated Segment Test,AGAST)算法,该算法是对FAST算法的一种改进主要提升了速度与亮度变化下的鲁棒性,但没有解决尺度不变性。
现在接着看代码。上面的代码里面大体是实现了:
init5_8_pattern() 函数:
初始化像素横竖方向偏移量,用于后续快速访问周围像素。
agast58_detect() 函数:
使用5x5个像素组成的模板匹配算法扫描图像,找到角点。
像素值大于或小于中心点像素值的偏移量编码成一个64位的特征码。
如果匹配了角点模板,记录下角点坐标。
agast58_score() 函数:
使用二分法找到最佳的阈值,进一步提高角点质量。
周围5x5像素值和该阈值进行比较,如果匹配角点模板,说明是角点。
不断调整阈值,找到使得匹配模板的像素数最多的阈值。
nonmax_suppression() 函数:
应用非极大值抑制进一步提炼角点。
抑制掉像素梯度较小的不明显角点。
alloc_keypoint() 函数:
将检测到的角点包装成 keypoints 数据结构。
第一次见这种写法
该变量是用于储存图像像素的偏移量,用于快速访问像素周围的像素灰度值。
s_offset0 表示相对于当前像素的左上方像素的偏移量。
具体来说:
s_offset0 表示相对于(x,y)的(x-1, y-1)像素
s_offset1 表示相对于(x,y)的(x-1, y)像素
s_offset2 表示相对于(x,y)的(x, y-1)像素
以此类推
通过预先计算好这些固定的偏移量,就可以通过 指针偏移 的方式,快速获取周围像素的值,而不需要每次都计算坐标关系,从而提高效率。所以,这个 s_offset0 变量就是一个优化手段,用来加速周围像素访问。
我们看第一个函数的签名,有个*,这里就要写一下C语言的知识了。
先说这个函数的作用-agast58_detect() 是AGAST算法中用于检测角点的主要函数。
它的主要功能是:
在输入图像img上,使用一个5x5像素模板滑动扫描。
将中心像素与周围像素进行比较,大于或小于阈值b的编码成一个特征码。
如果特征码与角点的模板匹配,则记录该像素为角点候选。
所有检测到的角点候选保存在 corner_t 结构体数组 corners 中。
num_corners 为输出参数,用于返回检测到的角点总数。
roi 参数用于指定只检测图像的某个区域。
其中corner_t结构体包含了每个检测到角点的x,y坐标和score明显性分数。
agast检测依赖于一个经过优化的像素访问顺序以及二值比较来实现高效运算。
在C语言中,函数名前面的*代表该函数返回一个指针类型。
对于agast58_detect这个函数:
返回值的类型是corner_t*
,是一个指向corner_t结构体的指针。
这个指针指向一个动态分配的数组,用于存储检测到的所有角点。
加上*的原因:
返回一个指针,函数可以返回一个数组或对象,不仅仅是一个scalar值。
指针访问内存速度快,不需要拷贝整个数组。
函数执行结束后,指针变量还可以被外部代码访问,相当于函数可以修改外部变量。
把返回数组的内存管理交给调用者,函数执行完就可以释放内部内存,不用维护资源。
总结一下:
*表示返回一个指针
可以返回动态数组/对象
提高效率,不拷贝大数组
指针可修改外部变量
内存管理交给调用者
程序的实现里面大量的使用了指针的偏移,基本思想是:
直接通过指针运算获取相邻像素,而不用每次计算坐标。
预先计算好偏移量,例如左上角像素的偏移量是 -1行 -1列。
将这些固定偏移量存储在变量中,比如s_offset0。
在访问像素时,直接基于指针偏移这个固定的值,这样就跳过了坐标计算。
例如,当前指针指向像素 (x, y):
uint8_t *imgPtr = &img[y * width + x];
获取左上角像素,不用偏移:
uint8_t leftUp = img[ (y-1) * width + (x-1) ]; // 需要计算坐标
使用偏移:
int offset0 = -width - 1; // 预先计算偏移量
uint8_t leftUp = imgPtr[offset0]; // 基于指针偏移
通过指针偏移,避免每次获取相邻像素时重复计算偏移量,这样可以明显减少计算量,从而加速像素访问。
再看这个函数,这个alloc_keypoint()函数是用于分配和初始化一个关键点结构kp_t的。
它做了以下几件事:
使用xalloc0()在堆上分配一个kp_t结构的内存,并初始化为0。
将传入的x,y坐标及score分数存入kp_t中。
注释里提到必须将描述子descriptor数组初始化为0。这里通过xalloc0()预先设置为0实现。
返回这个kp_t指针。
这样调用者就可以拿到一个堆上分配的、坐标与分数填充了、描述子初始化为0的关键点结构kp_t。
需要注意的是:
必须初始化描述子数组,后续的特征描述算子会填充描述子。
使用xalloc0()而不是malloc,可以自动初始化内存为0。
返回 kp_t 指针,调用者可以进一步访问关键点数据。
综上,这是一个辅助函数,用于根据坐标分数快速创建一个关键点结构.
它自己又重写了一次这个malloc的函数,xalloc0()是一种自定义的内存分配函数,与malloc()类似,但是有一些额外的功能:
当size为0时,直接返回NULL,而不报错。这与malloc的行为不同。
使用gc_alloc在堆上分配内存,这是MaixPy特有的 gc 堆内存分配函数。
分配成功后用memset清零内存。这是xalloc0的关键功能之一。
如果分配失败,调用xalloc_fail导致程序异常。
返回清零后的内存指针。
这样使用xalloc0比malloc好在:
SIZE为0时不会错误。
自动清零内存,不需再memset。
与MaixPy的GC堆内存管理兼容。
出错时终止程序,不需要额外判断返回NULL情况。
这个init5_8_pattern()函数是用于初始化图像像素的8方向偏移量,这是AGAST算法的一个优化。
它的作用是:
接收图像的宽度width作为参数。
如果当前宽度与已保存的s_width相同,直接返回,不再初始化,避免重复计算。
如果宽度变了,更新s_width为新宽度。
计算8个方向相对当前像素点的偏移量:
s_offset0 (-1, -1) 左上角
s_offset1 (-1, 0) 正上方
...
s_offset7 (-1, 1) 左下角
偏移量根据图像宽度width调整,即乘以width。
这样,在后续检测角点时,可以直接用这些预计算偏移访问周围像素,不需要每次都计算坐标偏移,加速了像素访问。通过一次初始化,减少重复计算,从而提升检测效率。充分利用了图像具有固定尺寸的特点。
这个agast_detect()函数实现了AGAST角点检测的完整流程:
初始化5x5窗口的偏移量init5_8_pattern()
调用agast58_detect()函数进行角点检测,返回检测到的角点数组
对每一个角点调用agast58_score()计算明显性分数
进行非极大值抑制nonmax_suppression(),过滤掉弱角点
将剩下的角点保存在输出的keypoints数组中
释放临时的角点内存fb_free()
所以这个函数将角点检测、评分和滤波三个阶段包装起来,实现了一个完整的AGAST角点检测器。
它接受输入图像,检测参数(threshold),区域等,最终输出经过优化的高质量角点集。
其实还没有完全说完,这个函数很长:
大概是这样的
初始化循环边界 - xsizeB, ysizeB 定义了只在图像有效区域内循环
分配内存 - 使用 fb_alloc 在内存池中分配 corner_t 数组
双重循环 - 外层循环 y 方向,内层循环 x 方向扫描每个像素
像素比较 - 在 homogeneous 和 structured 两个标签下,使用偏移像素比较中心像素,判断是否匹配角点模式
记录角点 - 如果匹配就记录下角点坐标到 corners 数组
返回结果 - 将检测到的角点数量赋值给 num_corners,并返回角点数组
里面的循环做的这个事情比较多。
同样下头的还有一个函数, agast58_score() 函数主要实现了A-GAST算法中使用二分法搜索最佳阈值的步骤。
主要流程是:
初始化阈值的上下限 bmin、bmax。
不断循环,将当前阈值 b 代入角点模式比较。
如果匹配了角点模式,则把 b 作为新的下限 bmin。
如果不匹配角点模式,则把 b 作为新的上限 bmax。
通过二分不断逼近使得角点模式匹配的像素数最多的阈值。
当上下限只差1时,返回最佳阈值 bmin。
关键点:
使用二分搜索提升角点明显性。
像素比较使用偏移访问提速。
通过不断调整阈值 b 寻找最佳角点模式匹配。
返回最佳阈值,作为该像素角点的分数。
看下这个检测算法里面的这个句子,
循环遍历所有检测到的角点 corners
计算每个角点的像素指针 - 通过角点的 x,y 坐标,计算在图像像素数组中的偏移量
调用 agast58_score() 并传入像素指针和阈值threshold
agast58_score() 将返回0-255范围的分数,记录在 corner[i].score 中
最后每个角点除了有坐标x,y之外,还有一个分数score表示角点的明显程度。
所以这个过程对每一个角点候选运行了二分搜索,找到了最佳的阈值,作为该点的分数。分数高的角点匹配度更好,更明显,更稳定,这样后续就可以基于分数进行非极大值抑制来过滤掉弱角点。这种为每个角点单独密集计算的方式也是AGAST算法区别于FAST算法的主要特点之一。
这个非极大值抑制(non-maximum suppression)函数的作用是去除重复或边缘响应较弱的非极大值角点,只保留每个局部区域响应最强的角点。
主要步骤是:
计算每一行的起始角点索引,用于快速查找上下行角点。
对每个角点,检查其上下左右4邻域是否存在更高分数的角点。
如果存在,则抑制该角点,不将其录入最终角点集。
只保留每个局部区域分数最高的角点。
检查内存是否足够,不足则试图释放内存使能继续录入角点。
将抑制后的角点保存到输出数组中。
这通过只保留局部最大值点,去除了边缘响应较弱的重复角点,提升了角点质量。
AGAST算法相比FAST算法加入了这个非极大值抑制步骤,可以有效提升角点的重复性和分布均匀性。
里面有一个这样的句子,是初始化 row_start 数组,row_start 数组用来记录每一行的第一个角点在 corners 数组中的索引。
具体地:
row_start 的大小是图像总行数+1,即 last_row + 1
使用 -1 来表示该行没有检测到角点
初始化所有值为 -1,表示刚开始还没有任何角点
后面在检测到角点时会记录:
row_start[角点所在行] = 角点在corners数组中的索引
所以row_start[y] = x 表示:
第y行的第一个角点在corners数组中的索引为x
这样初始化row_start为-1VeryAmerican,在后续的非极大值抑制中,就可以通过row_start数组快速获取上下行的角点信息,从而高效实现非极大值抑制。
真复杂啊,操。
联系客服