Vivado HLS中源图像写回方法及two pass连通分量标记算法实现问题咨询
看起来你在实现两遍连通分量标记时遇到了两个核心问题:3x3窗口无法复用前一次的标记结果,以及不知道如何在HLS中写回源图像。我来一步步帮你解决:
一、解决窗口处理忽略前一次结果的问题
你遇到的本质问题是没有维护已更新的邻域状态——连通分量标记需要依赖左侧、左上、上方已经完成标记的像素结果,而不是原始图像的像素。如果只传递原始的3x3窗口数据,算法自然无法获取之前的标记,导致结果不延续。
具体解决方案:
用行缓存(Line Buffer)保存前两行的标记结果
连通分量标记的3x3窗口需要上一行的已标记数据,因此需要用移位寄存器或者自定义行缓存来存储前两行的处理结果,而不是原始图像数据。Vivado HLS提供了ap_shift_reg可以方便实现行缓存。维护当前行的左侧标记寄存器
当前行中,窗口左侧的像素是上一个处理完成的像素,需要用一个寄存器实时保存这个最新的标记值,供下一个窗口使用。组装包含已更新邻域的窗口数据
调用标记算法函数时,不能直接传原始3x3窗口,要把左侧、左上、上方的标记结果替换进去,确保算法能拿到正确的邻域标记。
代码示例片段:
#include "ap_int.h" #include "ap_shift_reg.h" #define IMG_WIDTH 1920 #define IMG_HEIGHT 1080 #define LABEL_BITS 8 typedef ap_uint<LABEL_BITS> label_t; typedef ap_uint<1> binary_pixel_t; // 行缓存:存储前两行的标记结果 ap_shift_reg<label_t, IMG_WIDTH> line_buffer[2]; // 当前行已处理的左侧标记 label_t current_left_label; // 连通标记算法函数(这里简化示意) label_t label_algorithm(label_t window[3][3]) { // 实现你的连通判断逻辑,返回当前像素的标记 label_t result = 0; // ... 你的算法代码 ... return result; } void two_pass_cc_labeling(binary_pixel_t img_in[IMG_HEIGHT][IMG_WIDTH], label_t img_out[IMG_HEIGHT][IMG_WIDTH]) { // 初始化行缓存和左侧标记 line_buffer[0].shift(0); line_buffer[1].shift(0); current_left_label = 0; for (int y = 0; y < IMG_HEIGHT; y++) { current_left_label = 0; // 每行开头左侧无有效像素 for (int x = 0; x < IMG_WIDTH; x++) { // 组装包含已更新邻域的3x3窗口 label_t window[3][3] = { // 上一行的标记(从行缓存读取) {line_buffer[1].read(IMG_WIDTH - 1 - x), line_buffer[1].read(IMG_WIDTH - 2 - x), line_buffer[1].read(IMG_WIDTH - 3 - x)}, // 当前行左侧是已处理的标记,当前像素先填原始值 {current_left_label, (label_t)img_in[y][x], 0}, {0, 0, 0} }; // 补充窗口其他位置的原始像素(如果需要) // ... // 调用标记算法得到当前像素的标记 label_t current_label = label_algorithm(window); // 更新状态:写入当前行缓存,更新左侧标记 line_buffer[0].shift(current_label); current_left_label = current_label; // 写入输出图像 img_out[y][x] = current_label; } // 行切换:将当前行缓存移到上一行缓存 line_buffer[1] = line_buffer[0]; line_buffer[0].shift(0); } // 第二遍处理:合并等价对(这里省略,需要维护等价表并遍历图像替换标记) }
二、在Vivado HLS中写回源图像
Vivado HLS中默认输入端口是只读的,要写回源图像可以通过以下几种方式实现:
1. 将图像声明为双向端口(inout)
直接把图像数组参数声明为inout类型,这样既能读取原始像素,又能将标记结果写回同一个数组。注意要保证数据位宽匹配:如果原始像素是1bit,但标记是多bit,需要把数组的位宽改成标记的位宽(比如ap_uint<8>),或者用位拼接存储原始像素和标记。
示例:
void cc_labeling_inout(ap_uint<8> img_inout[IMG_HEIGHT][IMG_WIDTH]) { for (int y = 0; y < IMG_HEIGHT; y++) { for (int x = 0; x < IMG_WIDTH; x++) { // 读取原始像素(比如取最低位) binary_pixel_t raw_pixel = img_inout[y][x][0]; // 处理得到标记 label_t current_label = ...; // 写回:将标记存入高7位,保留原始像素在最低位(或直接覆盖) img_inout[y][x] = (current_label << 1) | raw_pixel; } } }
2. 映射输入输出到同一内存空间
如果你的HLS函数设计为分离的输入和输出端口,可以在顶层IP集成时,将输入和输出的AXI接口映射到同一个外部内存(比如BRAM或DDR)地址空间。这样处理完成后,输出的标记数据会自动覆盖源图像的原始数据。
这种方式的好处是保持HLS函数的输入输出分离,更符合硬件设计的模块化原则,同时实现写回效果。
3. 内部缓存读写
先将整个源图像读取到HLS内部的缓存(比如BRAM数组),在内部缓存上直接修改处理,完成后再将缓存数据写回外部内存,覆盖源图像。
示例:
void cc_labeling_internal_buffer(ap_uint<1> *img_in, ap_uint<8> *img_out, int width, int height) { // 读取源图像到内部缓存 ap_uint<1> internal_raw[IMG_HEIGHT][IMG_WIDTH]; ap_uint<8> internal_label[IMG_HEIGHT][IMG_WIDTH]; for (int i = 0; i < height * width; i++) { internal_raw[i/width][i%width] = img_in[i]; } // 执行连通分量标记处理,直接修改internal_label // ... 处理逻辑 ... // 写回外部内存(如果img_out和img_in指向同一地址,就会覆盖源图像) for (int i = 0; i < height * width; i++) { img_out[i] = internal_label[i/width][i%width]; } }
额外注意事项
- 优化流水线:在Vivado HLS中,记得给循环添加
#pragma HLS pipeline II=1指令,同时对行缓存或内部数组使用#pragma HLS array_partition来提升并行性,避免数据依赖导致的性能瓶颈。 - 两遍算法的等价表:第一遍处理时需要维护等价关系表,第二遍遍历图像合并标记,这部分也需要确保能读取到第一遍的标记结果,同样可以用内部缓存或行缓存实现。
内容的提问来源于stack exchange,提问作者A.k.

