引言
该系列依旧是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
)定义输入流输出流。
输入流的位宽数据分配如下图:
输出流位宽分配如下:
开头提到的 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 描述子生成的流水线中缓冲采样点坐标数据,平衡硬件逻辑的读写时序,避免数据冲突,确保并行处理的高效性。
重构数据类型
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硬件加速项目系列

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