FAST角点检测 HLS代码解析(ⅲ)

作者:阿白叔 发布时间: 2025-10-24 阅读量:9 评论数:0

引言

本小节是FAST角点检测 HLS移植的最核心的代码,希望对这方面感兴趣的朋友可以仔细理解这篇的内容。

xFfastProc函数

这个函数是FAST角点检测中的核心逻辑,重中之重。

负责从滑动窗口像素中判定 “是否为角点” 并计算角点分数;首行的 C++ 模板参数则是为了适配不同硬件配置(如并行度、窗口大小),实现代码复用与灵活配置。

template<int NPC,int WORDWIDTH,int DEPTH, int WIN_SZ, int WIN_SZ_SQ>

void xFfastProc(

        unsigned char OutputValues[XF_NPIXPERCYCLE(NPC)],

        unsigned char src_buf[WIN_SZ][XF_NPIXPERCYCLE(NPC)+(WIN_SZ-1)],

        ap_uint<8> win_size,

        uchar_t threshold,XFPTNAME(DEPTH) &pack_corners)

{

#pragma HLS INLINE

 

    uchar_t kx = 0, ix = 0;

 

    //XF_SNAME(WORDWIDTH) tbuf_temp;

    unsigned char tbuf_temp = 0;

 

    ////////////////////////////////////////////////

    // Main code goes here

    // Bresenham's circle score computation

    short int flag_d[(1 << XF_BITSHIFT(NPC))][NUM] = {0}, flag_val[(1 << XF_BITSHIFT(NPC))][NUM] = {0};

 

#pragma HLS ARRAY_PARTITION variable=flag_val dim=1

#pragma HLS ARRAY_PARTITION variable=flag_d dim=1

 

    for (ap_uint<4> i = 0; i < 1; i++)

    {

#pragma HLS LOOP_TRIPCOUNT MAX=1

#pragma HLS LOOP_FLATTEN off

#pragma HLS PIPELINE II=1

        // Compute the intensity difference between the candidate pixel and pixels on the Bresenham's circle

        flag_d[i][0]  = src_buf[3][3+i] - src_buf[0][3+i]; //tbuf4[3+i] - tbuf1[3+i];

        flag_d[i][1]  = src_buf[3][3+i] - src_buf[0][4+i]; //tbuf4[3+i] - tbuf1[4+i];

        flag_d[i][2]  = src_buf[3][3+i] - src_buf[1][5+i]; //tbuf4[3+i] - tbuf2[5+i];

        flag_d[i][3]  = src_buf[3][3+i] - src_buf[2][6+i]; //tbuf4[3+i] - tbuf3[6+i];

        flag_d[i][4]  = src_buf[3][3+i] - src_buf[3][6+i]; //tbuf4[3+i] - tbuf4[6+i];

        flag_d[i][5]  = src_buf[3][3+i] - src_buf[4][6+i]; //tbuf4[3+i] - tbuf5[6+i];

        flag_d[i][6]  = src_buf[3][3+i] - src_buf[5][5+i]; //tbuf4[3+i] - tbuf6[5+i];

        flag_d[i][7]  = src_buf[3][3+i] - src_buf[6][4+i]; //tbuf4[3+i] - tbuf7[4+i];

        flag_d[i][8]  = src_buf[3][3+i] - src_buf[6][3+i]; //tbuf4[3+i] - tbuf7[3+i];

        flag_d[i][9]  = src_buf[3][3+i] - src_buf[6][2+i]; //tbuf4[3+i] - tbuf7[2+i];

        flag_d[i][10] = src_buf[3][3+i] - src_buf[5][1+i]; //tbuf4[3+i] - tbuf6[1+i];

        flag_d[i][11] = src_buf[3][3+i] - src_buf[4][0+i]; //tbuf4[3+i] - tbuf5[0+i];

        flag_d[i][12] = src_buf[3][3+i] - src_buf[3][0+i]; //tbuf4[3+i] - tbuf4[0+i];

        flag_d[i][13] = src_buf[3][3+i] - src_buf[2][0+i]; //tbuf4[3+i] - tbuf3[0+i];

        flag_d[i][14] = src_buf[3][3+i] - src_buf[1][1+i]; //tbuf4[3+i] - tbuf2[1+i];

        flag_d[i][15] = src_buf[3][3+i] - src_buf[0][2+i]; //tbuf4[3+i] - tbuf1[2+i];

        // Repeating the first 9 values

        flag_d[i][16] = flag_d[i][0];

        flag_d[i][17] = flag_d[i][1];

        flag_d[i][18] = flag_d[i][2];

        flag_d[i][19] = flag_d[i][3];

        flag_d[i][20] = flag_d[i][4];

        flag_d[i][21] = flag_d[i][5];

        flag_d[i][22] = flag_d[i][6];

        flag_d[i][23] = flag_d[i][7];

        flag_d[i][24] = flag_d[i][8];

 

        // Classification of pixels on the Bresenham's circle into brighter, darker or similar w.r.t.

        // the candidate pixel

        for (ap_uint<4> j = 0; j < 8; j++)

        {

#pragma HLS unroll

            if (flag_d[i][j] > _threshold)

                flag_val[i][j] = 1;

            else if (flag_d[i][j] < -_threshold)

                flag_val[i][j] = 2;

            else

                flag_val[i][j] = 0;

 

            if (flag_d[i][j+8] > _threshold)

                flag_val[i][j+8] = 1;

            else if (flag_d[i][j+8] < -_threshold)

                flag_val[i][j+8] = 2;

            else

                flag_val[i][j+8] = 0;

            // Repeating the first 9 values

            flag_val[i][j+PSize] = flag_val[i][j];

        }

        flag_val[i][PSize/2+PSize] = flag_val[i][PSize/2];

        flag_d[i][PSize/2+PSize] = flag_d[i][PSize/2];

 

        // Bresenham's circle score computation complete

 

        // Decision making for corners

        uchar_t core=0;

        uchar_t iscorner=0;

        uchar_t count=1;

        for (ap_uint<5> c=1; c < PSize+PSize/2+1; c++)

        {

#pragma HLS LOOP_TRIPCOUNT MAX=25

#pragma HLS UNROLL

            if ((flag_val[i][c-1] == flag_val[i][c]) && flag_val[i][c]>0)

            {

                count++;

                if (count > 12)

                {

                    iscorner = 1; // Candidate pixel is a corner

                }

            }

            else

            {

                count=1;

            }

        } // Corner position computation complete

        // NMS Score Computation

        if(iscorner)

        {

            xFCoreScore(flag_d[i],_threshold, &core);

            pack_corners.range(ix+7,ix) = 255;

        }

        else

            pack_corners.range(ix+7,ix) = 0;

        ix+=8;

        // Pack the 8-bit score values into 64-bit words

        tbuf_temp = core;   // Set bits in a range of positions.

        kx += 8;

    }

 

    OutputValues[0] = tbuf_temp;//array[(WIN_SZ_SQ)>>1];

    return;

}

 

代码解析:

 

复用功能实现,模板参数

template<int NPC,int WORDWIDTH,int DEPTH, int WIN_SZ, int WIN_SZ_SQ>

 

 C++ 泛型编程的模板定义,核心作用是让函数支持不同的硬件参数配置,无需重复编写多个函数。下面解释一下各个参数的作用:

 

NPC:每周期处理的像素数(Number of Pixels per Cycle),控制硬件并行度。

作用:适配不同的吞吐量需求(并行度越高,每周期处理像素越多,帧率越高)

例:NPC=1(每周期 1 像素)、NPC=4(每周期 4 像素)。

 

WORDWIDTH:数据总线位宽,对应输入 / 输出数据的位宽(如像素数据、打包后的角点信息)。

作用:匹配 FPGA 的总线宽度,避免位宽浪费或数据截断。

例:WORDWIDTH=8(单像素 8 位灰度)、WORDWIDTH=64(打包 8 个 8 位像素)。

 

DEPTH:输出角点信息的打包深度,控制pack_corners(输出角点打包数据)的位宽。

作用:适配下游模块的接收位宽(如 64 位、128 位)。

例:DEPTH=64(64 位打包输出)、DEPTH=128(128 位打包输出)

 

WIN_SZ:滑动窗口大小(FAST 角点检测的窗口尺寸,如 7x7、15x15)。

作用:适配不同尺度的角点检测(大窗口检测大尺度角点,小窗口检测小尺度角点)。

例:WIN_SZ=7(7x7 窗口)、WIN_SZ=15(15x15 窗口)

 

WIN_SZ_SQ:滑动窗口大小的平方(WIN_SZ * WIN_SZ),用于快速定位窗口中心像素。

作用:避免函数内重复计算(如 7x7 窗口的中心索引是(7*7-1)/2=24,直接用WIN_SZ_SQ推导更高效)。

例:WIN_SZ=7时,WIN_SZ_SQ=49。

 

 

模板的核心价值:泛型适配。比如要实现 “每周期 4 像素、15x15 窗口” 的 FAST 检测,只需调用xFfastProc<4, 32, 64, 15, 225>(...);要改为 “每周期 1 像素、7x7 窗口”,只需修改模板参数,无需修改函数内部逻辑,极大提升代码复用性和硬件配置灵活性。

 

问题:有了WIN_SZ 参数,还需要WIN_SZ_SQ干什么?填错了会有什么后果?

1. WIN_SZ_SQ的核心作用:硬件效率与逻辑简化,它并非多余参数,而是针对FPGA硬件特性的优化设计。一方面,硬件中乘法运算延迟远高于加法(如16位乘法需3-5个时钟周期,加法仅1个),WIN_SZ_SQ作为模板参数,编译时就确定为“窗口大小(WIN_SZ)的平方”,无需实时计算,直接用常量值避免延迟;另一方面,它能简化窗口中心像素索引推导(如7x7窗口中心索引为(49-1)/2,用WIN_SZ_SQ=49直接计算,无需重复写“WIN_SZ*WIN_SZ”,代码更简洁且避免歧义)。

 

2. 填错WIN_SZ_SQ的后果:直接破坏角点检测逻辑, 若填错,会引发两类关键问题:一是轻微错误(如WIN_SZ=7却填48),中心像素索引推导错误(本该24变成23),导致后续“中心像素与周围采样点的灰度差”计算基准错误,最终漏检真实角点或误检非角点;二是严重错误(如WIN_SZ=7填100),索引超出窗口像素范围(7x7仅49个像素,索引却到49),触发硬件内存越界,可能导致整个SLAM特征提取流水线停滞。

 

3. 避错关键:从源头杜绝手动填写错误, 最佳方式是“自动推导+编译时检查”:要么用宏定义(如#define WIN_SZ 7,#define WIN_SZ_SQ (WIN_SZ*WIN_SZ)),调用函数时直接用宏,只需填对WIN_SZ即可;要么在函数内加静态断言(static_assert(WIN_SZ_SQ==WIN_SZ*WIN_SZ, "填错提示")),若填错编译时直接报错,避免错误流入硬件开发后续环节。 

 

提炼出一个方法论--“高频复用的固定运算结果,优先定义为常量“。

 

库知识补充

xFfastProc 函数使用了Xilinx FPGA 的图像处理加速库(类似 Xilinx 提供的 XFOpenCV,专为 FPGA 硬件优化的图像处理工具集)。

 

 

库宏解释

XF_NPIXPERCYCLE(NPC):这是库定义的宏,作用是 “根据NPC的值,返回每时钟周期需要处理的像素数量”。

比如NPC=1(每周期处理 1 个像素),宏返回 1;NPC=4(每周期处理 4 个像素),宏返回 4。简单说,它就是NPC的 “别名”,用于明确代码中 “这个数字代表每周期像素数”

 

XF_PTNAME(DEPTH):库定义的类型宏,根据DEPTH(位宽)返回对应的硬件数据类型。

 

比如DEPTH=64,宏返回ap_uint<64>(Xilinx 定义的 64 位无符号整数类型,适合 FPGA 硬件);DEPTH=8,返回ap_uint<8>。作用是让pack_corners的位宽能随DEPTH灵活变化。

 

XF_BITSHIFT(NPC):库定义的宏,作用是 “计算NPC对应的二进制移位次数”。

比如NPC=4(二进制 100),移位次数是 2(因为 2^2=4);NPC=1,移位次数是 0(2^0=1)。1 << XF_BITSHIFT(NPC)等价于NPC,这里用移位是为了更直观地表示 “并行度是 2 的整数次幂”(硬件并行度通常设计为 2 的倍数,方便拆分)

 

更多相关的库,具体还是参考UG1399文档。这份文档已经更新了”简体中文“的最新版本。

针对XfopenCV库的介绍,参考文档Xilinx OpenCV User Guide (UG1233)

 

硬件优化指令

#pragma HLS INLINE

将函数代码嵌入调用处,避免模块间通信延迟,适合流水线设计。

 

#pragma HLS ARRAY_PARTITION variable=flag_val dim=1

#pragma HLS ARRAY_PARTITION variable=flag_d dim=1

按第一维拆分数组,让fpga能并行访问多个像素的数组元素。

 

核心代码

第一步:计算bresenham圆内采样点的灰度差

#pragma HLS PIPELINE II=1

        // Compute the intensity difference between the candidate pixel and pixels on the Bresenham's circle

        flag_d[i][0]  = src_buf[3][3+i] - src_buf[0][3+i]; //tbuf4[3+i] - tbuf1[3+i];

        flag_d[i][1]  = src_buf[3][3+i] - src_buf[0][4+i]; //tbuf4[3+i] - tbuf1[4+i];

        flag_d[i][2]  = src_buf[3][3+i] - src_buf[1][5+i]; //tbuf4[3+i] - tbuf2[5+i];

        flag_d[i][3]  = src_buf[3][3+i] - src_buf[2][6+i]; //tbuf4[3+i] - tbuf3[6+i];

        flag_d[i][4]  = src_buf[3][3+i] - src_buf[3][6+i]; //tbuf4[3+i] - tbuf4[6+i];

        flag_d[i][5]  = src_buf[3][3+i] - src_buf[4][6+i]; //tbuf4[3+i] - tbuf5[6+i];

        flag_d[i][6]  = src_buf[3][3+i] - src_buf[5][5+i]; //tbuf4[3+i] - tbuf6[5+i];

        flag_d[i][7]  = src_buf[3][3+i] - src_buf[6][4+i]; //tbuf4[3+i] - tbuf7[4+i];

        flag_d[i][8]  = src_buf[3][3+i] - src_buf[6][3+i]; //tbuf4[3+i] - tbuf7[3+i];

        flag_d[i][9]  = src_buf[3][3+i] - src_buf[6][2+i]; //tbuf4[3+i] - tbuf7[2+i];

        flag_d[i][10] = src_buf[3][3+i] - src_buf[5][1+i]; //tbuf4[3+i] - tbuf6[1+i];

        flag_d[i][11] = src_buf[3][3+i] - src_buf[4][0+i]; //tbuf4[3+i] - tbuf5[0+i];

        flag_d[i][12] = src_buf[3][3+i] - src_buf[3][0+i]; //tbuf4[3+i] - tbuf4[0+i];

        flag_d[i][13] = src_buf[3][3+i] - src_buf[2][0+i]; //tbuf4[3+i] - tbuf3[0+i];

        flag_d[i][14] = src_buf[3][3+i] - src_buf[1][1+i]; //tbuf4[3+i] - tbuf2[1+i];

        flag_d[i][15] = src_buf[3][3+i] - src_buf[0][2+i]; //tbuf4[3+i] - tbuf1[2+i];

为避免浮点运算(不适合 FPGA),FAST 算法用 Bresenham 圆(整数坐标)近似圆形。对于 7x7 窗口,圆半径为 3,16 个采样点的坐标是预计算的固定值(如(0,3+i)、(0,4+i)等),代码直接引用这些坐标访问src_buf中的像素。

若flag_d[i][j] > 0:中心像素比第j个采样点亮;

若flag_d[i][j] < 0:中心像素比第j个采样点暗;

这些差值直接反映 “中心与周围的灰度突变程度”

 

第二步:重复灰度差数据,处理圆形的连续性

// Repeating the first 9 values

        flag_d[i][16] = flag_d[i][0];

        flag_d[i][17] = flag_d[i][1];

        flag_d[i][18] = flag_d[i][2];

        flag_d[i][19] = flag_d[i][3];

        flag_d[i][20] = flag_d[i][4];

        flag_d[i][21] = flag_d[i][5];

        flag_d[i][22] = flag_d[i][6];

        flag_d[i][23] = flag_d[i][7];

        flag_d[i][24] = flag_d[i][8];

16 个采样点在圆形上是环形排列(第 16 个点的下一个是第 1 个点)。检测 “连续 12 个点” 时,可能出现 “跨边界” 的连续(如第 14、15、1、2... 点)。通过重复前 9 个值,将环形转为线性数组(共 25 个元素),循环检测时无需处理边界绕回,硬件实现更简单。

 

第三步:灰度差分类,标记状态

// Classification of pixels on the Bresenham's circle into brighter, darker or similar w.r.t.

        // the candidate pixel

        for (ap_uint<4> j = 0; j < 8; j++)

        {

#pragma HLS unroll

            if (flag_d[i][j] > _threshold)

                flag_val[i][j] = 1;

            else if (flag_d[i][j] < -_threshold)

                flag_val[i][j] = 2;

            else

                flag_val[i][j] = 0;

 

            if (flag_d[i][j+8] > _threshold)

                flag_val[i][j+8] = 1;

            else if (flag_d[i][j+8] < -_threshold)

                flag_val[i][j+8] = 2;

            else

                flag_val[i][j+8] = 0;

            // Repeating the first 9 values

            flag_val[i][j+PSize] = flag_val[i][j];

        }

        flag_val[i][PSize/2+PSize] = flag_val[i][PSize/2];

        flag_d[i][PSize/2+PSize] = flag_d[i][PSize/2];

 

将连续的灰度差(-255~255)转换为离散状态(0/1/2),降低后续 “连续相同点” 的检测复杂度。硬件比较整数(0/1/2)比比较灰度差(如 - 123 和 - 145)快得多(1 周期完成)。

 

阈值_threshold的作用:控制 “差异显著” 的标准(如_threshold=10,只有灰度差超过 ±10 才被视为 “显著”)。阈值越小,检测到的角点越多,但噪声也越多;阈值越大,角点越少但更稳定。

 

#pragma HLS unroll将 8 次循环展开为 8 个并行操作,原本需要 8 个周期的分类,现在 1 个周期完成,大幅降低延迟。

 

第四步:角点判定

// Decision making for corners

        uchar_t core=0;

        uchar_t iscorner=0;

        uchar_t count=1;

        for (ap_uint<5> c=1; c < PSize+PSize/2+1; c++)

        {

#pragma HLS LOOP_TRIPCOUNT MAX=25

#pragma HLS UNROLL

            if ((flag_val[i][c-1] == flag_val[i][c]) && flag_val[i][c]>0)

            {

                count++;

                if (count > 12)

                {

                    iscorner = 1; // Candidate pixel is a corner

                }

            }

            else

            {

                count=1;

            }

        }

 

核心判定规则:若圆形上有连续 12 个采样点同属 “亮于中心”(状态 1)或 “暗于中心”(状态 2),说明中心像素处于 “灰度突变区域”,符合角点特征。

 

硬件层:通过计数器count累加连续相同状态的数量,超过 12 则置位iscorner。循环展开后,多个c值的比较可并行进行(如一次比较 8 个c),检测延迟压缩到 1-2 个周期。

 

第五步:角点分数计算,打包输出

if(iscorner)

        {

            xFCoreScore(flag_d[i],_threshold, &core);

            pack_corners.range(ix+7,ix) = 255;

        }

        else

            pack_corners.range(ix+7,ix) = 0;

        ix+=8;

        // Pack the 8-bit score values into 64-bit words

        tbuf_temp = core;   // Set bits in a range of positions.

        kx += 8;

    }

 

分数计算主要调用上一节的模块,详细原理看之前的文章。

 

打包逻辑:

pack_cornersXF_PTNAME(DEPTH)类型存储有效性标志(每 8 位对应 1 个像素,255 表示有效),下游模块可快速过滤非角点。

OutputValues:存储角点分数(非角点为 0),数组大小由NPC决定(每周期输出NPC个分数),匹配 FPGA 输出总线宽度。

 

总结:

算法层面,严格遵循 “采样→差异判定→连续检测→分数计算” 的 FAST 流程,确保角点检测的准确性;

硬件层面,通过模板参数适配并行度、数组分块实现并行访问、流水线 / 循环展开降低延迟,确保实时处理。

ORB-SLAM3硬件加速项目系列

image-dtx3.png

点击文章下方标签获取整个系列更新文章!

文章doc版链接【金山文档 | WPS云文档】 FAST角点检测 HLS代码解析

评论