Descriptor 描述子运算 HLS代码解析(ⅰ)

作者:阿白叔 发布时间: 2025-11-17 阅读量:4 评论数:0

引言

该系列依旧是ORB-SLAM3移植到zynq平台的系列代码解析。

接着上一课,FAST角点检测我们已经基本讲完了,后续有什么不理解的可以先问问AI,如果不好解决的问题可以评论区留言。

从本章节开始,一改以前的风格。如果您是按顺序看到这里,那说明你的对HLS代码的书写风格已经有所熟悉了,所以之后的代码以注释为主,针对部分需要理解的处理方式,会额外进行解释。

再次提示,源代码来自于Github上ZYNQ-SLAM的一个开源项目。文章中时常会少掉很多行重复或者功能原理类似的代码,为了你阅读的连贯性,请下载源代码以便于参照的去学习。

让我们正式开始Descriptor 描述子运算加速的学习。

bit_pattern_31_数组

bit_pattern_31_ 是 ORB 描述子的预定义采样点对偏移表,数组维度为 [256][4],对应 256 组采样点对的相对坐标(dx1, dy1, dx2, dy2)。

其核心作用是:以图像特征点为原点,通过数组中的坐标偏移计算 256 对采样点的实际位置,对比每对点的灰度值生成 0/1 二进制位,最终拼接成 256 位 ORB 描述子。该数组是算法设计者优化后的固定配置,封装在底层特征提取模块中,无需用户修改,直接为 SLAM 的特征匹配(通过汉明距离计算相似度)提供 “可比对的特征标识”。

可以简单理解为,他是描述子运算中固定的参数。

比如我们的y = k x ; 这个固定参数就相当于这个方程中的 k 。

Descriptors 函数

void Descriptors(

        hls::stream<ap_uint<336> > &dataPackStreamIn,

        hls::stream<ap_axiu<32,1,1,1> > &descriptor_out,

        uint16_t img_width,

        uint16_t img_height,

        ap_uint<8> img_level

        )

定义输入流输出流。

输入流的位宽数据分配如下图:

bafecc78029e3dd750c96e32b15f682dbe8d3da8 

 

输出流位宽分配如下:

11cb383fa60d0eee33bd0a8a9af3dff05ffe5e4f 

开头提到的 bit_pattern_31_ 这个数组是每次运算都会使用到的,在硬件上我们可以把它存放到Rom中,以便于快速读取。

#pragma HLS RESOURCE variable=bit_pattern_31_ core=ROM_1P

core=ROM_1P表示 “单端口 ROM”:只有一个读端口,每次只能读取一个数据。由于采样模式的读取是按顺序或流水线方式进行(函数中循环访问 256 组数据),单端口已足够满足需求,且比多端口 ROM 更节省资源。

#pragma HLS ARRAY_PARTITION variable=bit_pattern_31_ complete dim=2

对数组bit_pattern_31_的第二维进行完全分区(数组拆分优化)。

背景:bit_pattern_31_是[256][4]的二维数组(256 组,每组 4 个元素),未分区时硬件会将其作为连续存储的块,访问不同元素可能存在冲突(同一时钟周期只能访问一个元素)。

生成 ORB 描述子时,每次需要读取一组 4 个偏移量(dx1, dy1, dx2, dy2),完全分区后可一次性并行读取 4 个值,无需等待,提升循环(compute_loop)的执行速度。

unsigned char src_buf0[31][31];

#pragma HLS ARRAY_PARTITION variable=src_buf0 complete dim=1

#pragma HLS ARRAY_PARTITION variable=src_buf0 complete dim=2

    unsigned char src_buf1[31][31];

#pragma HLS ARRAY_PARTITION variable=src_buf1 complete dim=1

#pragma HLS ARRAY_PARTITION variable=src_buf1 complete dim=2

 

同理指定两个buf 能并行处理。dim = 1 指定行,dim = 2 指定列。行列一起才能实现灰度值缓冲的每个点都能完全被访问,不受干扰。

 

static hls::stream<ap_uint<5> > res_x0;

#pragma HLS STREAM variable=res_x0 depth=8 dim=1

    static hls::stream<ap_uint<5> > res_y0;

#pragma HLS STREAM variable=res_y0 depth=8 dim=1

    static hls::stream<ap_uint<5> > res_x1;

#pragma HLS STREAM variable=res_x1 depth=8 dim=1

    static hls::stream<ap_uint<5> > res_y1;

#pragma HLS STREAM variable=res_y1 depth=8 dim=1

这些代码定义了 4 个静态流式缓冲通道,并通过 HLS 指令配置了其硬件属性,核心作用是在 ORB 描述子生成的流水线中缓冲采样点坐标数据,平衡硬件逻辑的读写时序,避免数据冲突,确保并行处理的高效性。

 

重构数据类型

6e511b023f6d0616608240f8905d0e246e41afe3 

c17754cc6f7567a8644494f16c78c0961189109c 

for(int ss = 0;ss<img_height*(img_width+15);ss++)

    {

        ap_uint<336> datapack = dataPackStreamIn.read();

        ap_uint<248> winVal = datapack.range(247,0);

        m_10_int = datapack.range(271,248);

        m_01_int = datapack.range(295,272);

        posX = datapack.range(307,296);

        posY = datapack.range(319,308);

        validFlag = datapack.range(327,320);

        score = datapack.range(335,328);

        shift_reg.range(287,256) = (int)posX;

        shift_reg.range(319,288) = (int)posY;

        shift_reg.range(351,320) = (int)score;

        read_row_loop:

        for(ap_uint<6> k = 0;k<31;k++)

        {

#pragma HLS UNROLL

#pragma HLS LOOP_TRIPCOUNT MAX=31

            for(ap_uint<8> s = 0;s<31;s++)

            {

#pragma HLS UNROLL

                if(k!=30)

                {

                    src_buf0[s][k] = src_buf0[s][k+1];

                    src_buf1[s][k] = src_buf1[s][k+1];

                }

                else

                {

                    src_buf0[s][30] = (unsigned char)winVal.range(s*8+7,s*8);

                    src_buf1[s][30] = (unsigned char)winVal.range(s*8+7,s*8);

                }

            }

        }

遍历输入数据流中所有可能的特征点数据包。循环次数img_height*(img_width+15)是根据图像尺寸估算的 “最大特征点数量上限”,其中 “+15” 是为了覆盖图像边缘的特征点(31×31 邻域可能超出图像边界,额外预留 15 列像素的缓冲空间),确保不遗漏任何有效特征点。

 

ap_uint<336> datapack = dataPackStreamIn.read(); // 读取336位数据包

// 从数据包中拆分出各字段

ap_uint<248> winVal = datapack.range(247,0);     // 特征点周围31×31邻域的灰度值

m_10_int = datapack.range(271,248);              // 一阶矩m₁₀(用于计算主方向)

m_01_int = datapack.range(295,272);              // 一阶矩m₀₁(用于计算主方向)

posX = datapack.range(307,296);                  // 特征点X坐标

posY = datapack.range(319,308);                  // 特征点Y坐标

validFlag = datapack.range(327,320);             // 特征点是否有效(0=无效)

score = datapack.range(335,328);                 // 特征点响应分数(如FAST角点强度)

 

 shift_reg.range(287,256) = (int)posX;

 shift_reg.range(319,288) = (int)posY;

 shift_reg.range(351,320) = (int)score;

  提前将特征点的元数据(坐标、分数)存入shift_reg寄存器,后续生成 256 位描述子后,直接在此寄存器中拼接,最终一次性输出 “描述子 + 元数据” 的完整信息。这里将 12 位坐标和 8 位分数扩展为 32 位,是为了适配输出接口的 32 位数据宽度(AXI-Stream 协议),避免输出时的位拼接逻辑复杂化。

 

for(ap_uint<6> k = 0;k<31;k++)

        {

#pragma HLS UNROLL

#pragma HLS LOOP_TRIPCOUNT MAX=31

            for(ap_uint<8> s = 0;s<31;s++)

            {

#pragma HLS UNROLL

                if(k!=30)

                {

                    src_buf0[s][k] = src_buf0[s][k+1];

                    src_buf1[s][k] = src_buf1[s][k+1];

                }

                else

                {

                    src_buf0[s][30] = (unsigned char)winVal.range(s*8+7,s*8);

                    src_buf1[s][30] = (unsigned char)winVal.range(s*8+7,s*8);

                }

            }

        }

31*31 邻域缓冲区的更新!

 

if(validFlag != 0)  // 仅处理有效特征点(非0表示有效,过滤噪声或低响应点)

{

    m_10 = m_10_int;  // 将整数形式的一阶矩m₁₀转换为定点数,用于后续角度计算

    m_01 = m_01_int;  // 将整数形式的一阶矩m₀₁转换为定点数,用于后续角度计算

    // 计算特征点主方向角度(弧度):通过灰度矩的反正切得到纹理主导方向,为旋转不变性做准备

    ap_fixed<14,3> angle = hls::atan2(m_01, m_10);

    

    compute_loop:  // 生成256位ORB描述子的主循环(256次迭代对应256个二进制位)

    for(unsigned int i =0;i<256;i++)

    {

#pragma HLS PIPELINE  // 流水线优化:使循环迭代并行重叠执行,256次迭代仅需~256个时钟周期(而非串行延迟)

        // 步骤1:计算采样点在31×31邻域缓冲区中的绝对坐标

        // 采样模式bit_pattern_31_[i]存储相对偏移,+15将其转换为0~30的绝对索引(缓冲区中心在(15,15))

        y0_res = bit_pattern_31_[i][1] + 15;  // 第一采样点Y坐标(绝对索引)

        x0_res = bit_pattern_31_[i][0] + 15;  // 第一采样点X坐标(绝对索引)

        y1_res = bit_pattern_31_[i][3] + 15;  // 第二采样点Y坐标(绝对索引)

        x1_res = bit_pattern_31_[i][2] + 15;  // 第二采样点X坐标(绝对索引)

        

        // 步骤2:通过流缓冲坐标(平衡流水线时序)

        res_y0.write(y0_res);  // 将第一采样点Y坐标写入流(FIFO缓冲)

        res_x0.write(x0_res);  // 将第一采样点X坐标写入流

        res_y1.write(y1_res);  // 将第二采样点Y坐标写入流

        res_x1.write(x1_res);  // 将第二采样点X坐标写入流

        // 从流中读取坐标:流的缓冲作用避免坐标计算与灰度读取的时序冲突(硬件延迟适配)

        y0 = res_y0.read();

        x0 = res_x0.read();

        y1 = res_y1.read();

        x1 = res_x1.read();

        

        // 步骤3:读取采样点的灰度值(缓冲区已通过数组全分区优化,支持并行访问)

        t0 = src_buf0[y0][x0];  // 第一采样点的灰度值

        t1 = src_buf1[y1][x1];  // 第二采样点的灰度值

        

        // 步骤4:对比灰度值生成描述子位:t0 < t1则该位为1,否则为0,累计256位形成原始描述子

        descriptor.range(i,i) = t0 < t1;

    }

}

对有效特征点,计算其纹理主方向(为旋转不变性打基础),并基于固定采样模式生成 256 位原始 ORB 描述子。

 

ap_int<8> times = (ap_int<8>)(angle / unitAngle);  // 计算主方向所属的量化等级

ap_int<8> bitsToShift = times << 3;  // 每个等级对应8位描述子移位(256/32=8)

if(angle - (times * unitAngle) > halfUnitAngle)  // 四舍五入调整:若角度偏差超过半阈值,多移8位

    bitsToShift += 8;

 

for (unsigned int i = 0; i < 256; i++)

#pragma HLS PIPELINE  // 流水线优化:256位移位在256个时钟周期内完成

    shift_reg[i] = descriptor[(i + bitsToShift) & 0x000000ff];  // 循环移位(&0xff确保在256位内循环)

 

output_loop:

for (unsigned int i = 0; i < 11; i++)  // 352位数据分11次输出(11×32=352)

{

#pragma HLS PIPELINE  // 流水线输出,每个周期输出32位

    ap_axiu<32,1,1,1> outVal;  // AXI-Stream格式数据(32位数据+控制信号)

    ap_uint<32> temp = shift_reg.range((i<<5)+31,i<<5);  // 每次截取32位(i<<5 = i×32)

    outVal.data = temp;  // 32位数据载荷

    outVal.keep = 0xf;  // 数据有效掩码(4字节全有效)

    // 最后一个数据包且是最后一个特征点时,置位last信号(标识传输结束)

    if(posX  4095 && posY  4095 && i==10)

        outVal.last = 1;

    else

        outVal.last = 0;

    descriptor_out.write(outVal);  // 写入输出流

}

把处理好的数据写入输出流,输出给下一个环节使用。

lineProcess函数

// 模板参数说明:
// ROWS/COLS:图像的行数/列数(配置参数)
// DEPTH:像素数据深度(如8位、16位等)
// NPC:每个周期处理的像素数(Xilinx常用宏,如XF_NPPC8表示8像素/周期)
// WORDWIDTH:数据总线宽度(与NPC和DEPTH相关,如8像素×8位=64位)
// TC:循环迭代次数提示(用于HLS性能分析)
// WIN_SZ:滑动窗口的尺寸(如3×3窗口中的3)
// WIN_SZ_SQ:滑动窗口尺寸的平方(如3×3=9,用于缓存大小计算)

template<int ROWS, int COLS, int DEPTH, int NPC, int WORDWIDTH, int TC, int WIN_SZ, int WIN_SZ_SQ>void lineProcess(hls::stream<ap_uint<8> > & _src_mat,                  
// 输入图像数据流(8位像素)
		hls::stream<ap_uint<88> > &dataPackStreamIn,             // 输入数据打包流(88位)
		hls::stream<ap_uint<336> > &dataPackStreamOut,            // 输出数据打包流(336位)
		XF_SNAME(WORDWIDTH) buf[WIN_SZ][(COLS >> XF_BITSHIFT(NPC))],  // 图像行缓冲区(存储窗口内的行数据)
		XF_PTNAME(DEPTH) src_buf[WIN_SZ][XF_NPIXPERCYCLE(NPC)+(WIN_SZ-1)],  // 窗口像素缓存(用于滑动窗口操作)
		uint16_t img_width,                                       // 图像实际宽度
		uint16_t img_height,                                      // 图像实际高度
		ap_uint<13> row_ind[WIN_SZ],                              // 窗口内行索引数组(标记当前窗口包含的行)
		ap_uint<13> row,                                          // 当前处理的行号
		ap_uint<8> win_size)                                      // 滑动窗口的实际尺寸(与WIN_SZ一致,用于边界处理)
{
#pragma HLS INLINE  // 函数内联优化:将该函数代码嵌入调用处,减少函数调用开销

	// 窗口行数据复制缓冲区:用于临时存储窗口内的一行数据,供并行处理
	XF_SNAME(WORDWIDTH) buf_cop[WIN_SZ];#pragma HLS ARRAY_PARTITION variable=buf_cop complete dim=1  // 数组完全分区:将buf_cop按行完全拆分,支持并行访问

	// 计算每个周期处理的像素数(NPC:Number of Pixels Per Cycle)
	// XF_NPIXPERCYCLE是Xilinx HLS图像库宏,根据NPC参数返回像素数(如XF_NPPC8对应8)
	uint16_t npc = XF_NPIXPERCYCLE(NPC);
	uint16_t col_loop_var = 0;  // 列循环边界调整变量(用于处理窗口边缘)
	ap_uint<248> winVal = 0;    // 窗口像素值打包变量(248位=31×8位,对应31个8位像素)
	ap_uint<88> dataIn;         // 输入打包数据临时变量

	// 确定列循环的边界补偿值:
	// 当每个周期处理1个像素(npc=1)时,补偿值为窗口尺寸的一半(WIN_SZ>>1),用于覆盖窗口边缘
	// 其他情况下(如npc=8),补偿值为1,简化边缘处理
	if(npc == 1)
	{
		col_loop_var = (WIN_SZ>>1);
	}
	else
	{
		col_loop_var = 1;
	}

	// 初始化窗口像素缓存src_buf:将所有元素清零
	// 双重循环均使用unroll优化,实现并行初始化,提高启动速度
	for(int extract_px=0;extract_px<WIN_SZ;extract_px++)
	{#pragma HLS LOOP_TRIPCOUNT min=WIN_SZ max=WIN_SZ  // 告知HLS循环迭代次数范围,辅助优化#pragma HLS unroll  // 循环展开:将循环体复制WIN_SZ次,并行执行
		for(int ext_copy=0; ext_copy<npc + WIN_SZ - 1; ext_copy++)
		{#pragma HLS unroll  // 内层循环展开:并行初始化每个窗口位置的像素
			src_buf[extract_px][ext_copy] = 0;
		}
	}

	// 列方向主循环:处理图像的每一列(按周期粒度,非像素粒度)
	// 循环范围:图像列数(按周期折算)+ 补偿值,确保窗口完全覆盖边缘
	Col_Loop:
	for(ap_uint<13> col = 0; col < ((img_width)>>XF_BITSHIFT(NPC)) + col_loop_var; col++)
	{#pragma HLS LOOP_TRIPCOUNT min=TC max=TC  // 告知HLS循环迭代次数范围(TC为模板参数)#pragma HLS pipeline  // 流水线优化:每个迭代周期启动一次,提高吞吐量(目标II=1)#pragma HLS LOOP_FLATTEN OFF  // 禁止循环合并:避免与其他循环合并,保证流水线稳定性

		// 读取输入图像数据到行缓冲区buf:
		// 仅当当前行在有效图像范围内(row < img_height),且当前列在有效列范围内时读取
		// row_ind[win_size-1]是窗口内最后一行的索引,确保数据存入窗口对应的行缓冲区
		if(row < img_height && col < (img_width>>XF_BITSHIFT(NPC)))
			buf[row_ind[win_size-1]][col] = _src_mat.read(); // 从输入流读取8位像素数据

		// 处理非8像素/周期(NPC != XF_NPPC8)的情况(8像素/周期的逻辑未实现)
		if(NPC == XF_NPPC8)
		{
			// 预留8像素/周期的处理逻辑
		}
		else
		{
			// 阶段1:将行缓冲区buf的数据复制到临时缓冲区buf_cop,处理行方向边界
			for(int copy_buf_var=0;copy_buf_var<WIN_SZ;copy_buf_var++)
			{#pragma HLS LOOP_TRIPCOUNT min=WIN_SZ max=WIN_SZ#pragma HLS UNROLL  // 并行复制窗口内的每一行
				// 边界处理:当当前行超过图像高度(row > img_height-1)时,
				// 使用窗口内前几行的数据填充(模拟边缘扩展,避免越界)
				if(	(row >(img_height-1)) && (copy_buf_var>(win_size-1-(row-(img_height-1)))))
				{
					// 超出图像高度时,复用窗口内上方的行数据(如镜像填充)
					buf_cop[copy_buf_var] = buf[(row_ind[win_size-1-(row-(img_height-1))])][col];
				}
				else
				{
					// 正常情况下,直接复制窗口内对应行的数据
					buf_cop[copy_buf_var] = buf[(row_ind[copy_buf_var])][col];
				}
			}

			// 阶段2:提取像素到窗口缓存src_buf,并打包到winVal
			for(int extract_px=0;extract_px<WIN_SZ;extract_px++)
			{#pragma HLS LOOP_TRIPCOUNT min=WIN_SZ max=WIN_SZ#pragma HLS UNROLL  // 并行处理窗口内的每一行
				if(col < img_width)  // 列在有效范围内
				{
					// 将当前列的像素存入src_buf的窗口尾部(win_size-1位置)
					src_buf[extract_px][win_size-1] = buf_cop[extract_px];
					// 将像素值打包到winVal(每个像素占8位,按行索引偏移)
					winVal.range((extract_px<<3)+7, extract_px<<3) = buf_cop[extract_px];
				}
				else  // 列超出有效范围(边缘处理)
				{
					// 复用前一列的像素值(列方向边缘填充)
					src_buf[extract_px][win_size-1] = src_buf[extract_px][win_size-2];
					winVal.range((extract_px<<3)+7, extract_px<<3) = src_buf[extract_px][win_size-2];
				}
			}

			// 阶段3:处理输入数据打包流dataPackStreamIn
			if(img_height-1 == row-15 && img_width-1 == col-15)
			{
				// 特殊位置处理(可能是图像右下角偏移15的位置):
				// 读取输入流后,修改特定字段为最大值(4095=12位最大值,255=8位最大值)
				dataIn = dataPackStreamIn.read();
				dataIn.range(59,48) = 4095;  // 12位字段置为最大值
				dataIn.range(71,60) = 4095;  // 12位字段置为最大值
				dataIn.range(79,72) = 255;   // 8位字段置为最大值
			}
			else if(col >= (WIN_SZ>>1))
			{
				// 当列索引超过窗口尺寸的一半时,读取有效输入流(窗口已完全进入图像区域)
				dataIn = dataPackStreamIn.read();
			}
			else
			{
				// 窗口未完全进入图像区域时,输入流数据置0
				dataIn = 0;
			}

			// 阶段4:打包输出数据并写入输出流
			ap_uint<336> temp;  // 336位=248位(winVal)+88位(dataIn)
			temp.range(247,0) = winVal;       // 低248位存储窗口像素数据
			temp.range(335,248) = dataIn;     // 高88位存储输入打包数据
			dataPackStreamOut.write(temp);    // 写入输出流

			// 阶段5:更新窗口缓存src_buf,实现列方向滑动(左移操作)
			for(int wrap_buf=0;wrap_buf<WIN_SZ;wrap_buf++)
			{#pragma HLS UNROLL  // 并行处理窗口内的每一行#pragma HLS LOOP_TRIPCOUNT min=WIN_SZ max=WIN_SZ
				for(int col_warp=0; col_warp<WIN_SZ-1;col_warp++)
				{#pragma HLS UNROLL  // 并行处理每行内的列移位#pragma HLS LOOP_TRIPCOUNT min=WIN_SZ max=WIN_SZ
					if(col == 0)
					{
						// 第一列时,用窗口尾部数据初始化所有位置(窗口初始化)
						src_buf[wrap_buf][col_warp] = src_buf[wrap_buf][win_size-1];
					}
					else
					{
						// 非第一列时,列数据左移一位(滑动窗口向右移动一个位置)
						src_buf[wrap_buf][col_warp] = src_buf[wrap_buf][col_warp+1];
					}
				}
			}
		}
	} // Col_Loop结束}

ORB-SLAM3硬件加速项目系列

image-dtx3-2vpd.png

点击图片中的 #slam移植系列 获取全部文章。

评论