先说结论:用FPGA做SAD模板匹配目标跟踪,核心不是“会不会写SAD”,而是“怎么把SAD算得足够快、足够省资源、时序还能收敛”。我最早是在一个实时视频处理项目里接触到这个需求的,当时要在1080p@60fps的输入流里框住一个运动目标,CPU和GPU的方案要么延迟太高,要么功耗和成本压不住,最后把目光落在了FPGA上。折腾了小半年,从算法仿真到板级调通,期间踩了不少坑,也总结出一些我觉得比较有价值的设计思路,这里完整记录下来。
1. SAD算法原理与FPGA方案选型:为什么是SAD,为什么是FPGA
1.1 SAD的数学本质与计算特征
SAD的全称是Sum of Absolute Differences,即绝对差值和。它的数学表达式非常简洁:
[ SAD(x, y) = \sum_{i=0}^{M-1} \sum_{j=0}^{N-1} |I(x+i, y+j) - T(i, j)| ]
其中,T是M×N大小的模板图像,I是搜索区域中同样大小的候选块,SAD值越小,说明候选块和模板越相似。目标跟踪的任务,就是在下一帧图像的搜索区域内,遍历所有可能的候选位置,找到SAD值最小的那个点,作为目标的新位置。
这个算法在软件里实现非常直白:两层循环遍历搜索区域,每个位置再做一次M×N的减法、取绝对值、累加。但正是这种“直白”带来了巨大的计算量。假设模板大小是32×32,搜索区域是100×100,那意味着要计算(100-32+1)² ≈ 4761个候选位置,每个位置要做1024次减法、1024次取绝对值和1024次累加,总计约1500万次操作——这是单帧单目标的计算量。要是目标数量更多、搜索区域更大、帧率要求更高,计算量会迅速膨胀到难以接受的程度。
所以SAD算法的特征非常鲜明:计算密集、数据并行度极高、控制逻辑简单。这恰恰是FPGA最擅长处理的运算类型。FPGA的并行计算能力在这里体现得淋漓尽致——它不像CPU那样一个时钟周期只能执行有限条指令,而是可以通过硬件描述语言把成百上千个计算单元同时铺开,让所有候选位置的SAD计算同时进行。
1.2 FPGA方案与CPU、GPU的边界对比
很多人在项目立项时会纠结一个问题:现有方案那么多,为什么非得用FPGA?我自己做过的对比评估大致是这样的:
| 维度 | CPU方案 | GPU方案 | FPGA方案 |
|---|---|---|---|
| 峰值算力 | 中低 | 极高 | 中高(但高度定制) |
| 功耗 | 高 | 极高 | 较低 |
| 延迟 | 毫秒级(受OS调度影响) | 毫秒级(PCIe传输+驱动开销) | 微秒级(纯硬件流水) |
| 开发周期 | 短 | 中等 | 长 |
| 确定性 | 差(受系统负载影响) | 中等 | 极好(时钟级确定) |
| 环境适应性 | 差(需要整机) | 差(体积大) | 好(可单板集成) |
CPU方案的优势在于开发效率,OpenCV里一行matchTemplate就能搞定,但对实时视频流来说,这个“一行代码”背后是每秒上亿次的内存访问和计算,帧率一高就吃力。GPU方案算力充足,但PCIe传输延迟和数据拷贝开销对“目标跟踪”这种对实时性极其敏感的应用来说,有时候反而是致命伤。
FPGA方案的突出优势体现在三个方面:第一是低延迟,数据从传感器进入FPGA后可以直接在流水线上处理,不需要经过内存拷贝和操作系统调度,延迟能做到微秒级;第二是高确定性,每个时钟周期做什么都是固定的,不存在CPU那种“有时候快有时候慢”的问题;第三是低功耗,整颗FPGA芯片的功耗通常只有几瓦到几十瓦,远低于GPU动辄几百瓦的功耗,这在嵌入式场景(无人机、机器人、工业检测设备)里是硬性指标。
1.3 什么样的目标跟踪场景适合用SAD+FPGA
SAD算法本身相对“朴素”,它没有特征点提取、没有机器学习模型,但它有一个突出的优点:对纹理清晰、形变小的刚性目标非常有效。比如工业流水线上检测工件位置、无人机锁定地面车辆、机器人视觉伺服追踪标记物等场景,目标外观在连续帧之间变化不大,SAD算法配合合适的搜索策略完全能胜任。
另一个选择FPGA+SAD组合的重要理由是可定制性和接口灵活性。很多视觉系统不只做目标跟踪,还要同时处理多路视频输入、控制云台或机械臂、输出触发信号等。FPGA可以在同一颗芯片里把这些功能全部集成,用硬件逻辑实现传感器接口(MIPI、LVDS、SDI)、图像预处理(滤波、增强)、SAD计算、结果输出和控制信号生成。这种“一站式”集成能力,是纯软件方案很难比的。
2. SAD算法硬件化的核心思路:流水线设计、并行度规划与资源权衡
2.1 从软件到硬件的思维转换
把SAD算法从C语言搬到FPGA,第一步要完成的是思维模型的转换。软件是“顺序执行”的思维:先读数据、再做计算、再写结果,每一步都在一个“大循环”里完成。硬件则是“空间展开”的思维:数据从一个模块流向另一个模块,所有模块同时工作,就像工厂里的流水线——不同的零件在不同工位同时被加工,而不是一个工人依次加工完所有零件。
具体到SAD算法,硬件化设计要考虑三个核心问题:数据怎么存、计算怎么做、结果怎么比。这三个问题环环相扣,任何一个没想清楚,后面的综合实现都会出问题。
2.2 模板数据的存储与复用策略
模板数据是整个SAD计算的基准,它在跟踪过程中是基本固定的(或周期性更新)。我的做法是把模板数据放在FPGA的片上Block RAM(BRAM)里,而不是外部DDR。这个选择的理由很直接:BRAM的访问延迟固定(一般1-2个时钟周期),而且可以做到多端口并行读取;DDR虽然容量大,但访问延迟高、带宽受总线调度影响大,如果模板数据在计算过程中被反复读取,DDR会产生大量突发访问,严重影响吞吐率。
模板大小需要精心权衡。我在项目中用的是24×24的模板,BRAM占用大约是24×24×8bit = 4608bit,约4.5Kb,对现代FPGA动辄几Mb的BRAM资源来说微不足道。如果模板切得太大(比如64×64),一方面BRAM消耗会显著上升,另一方面并行计算单元的规模也会膨胀;如果模板太小(比如8×8),跟踪的鲁棒性又会下降,容易被噪声干扰或目标局部纹理欺骗。24×24是我在工程实践里认为比较折中的选择。
2.3 滑动窗口与像素流式的搜索区域复用
SAD计算最直观的实现方式是“整块比较”:每算一个候选位置,从DDR里读出一个和模板一样大的候选块,然后做逐像素比较。但这种方式存在两个问题:一是DDR访问量巨大,每个候选位置都要读取M×N个像素,计算量还乘以了2,带宽很快就会耗尽;二是数据复用率低,相邻候选块之间有大量重叠区域,这些像素被反复从DDR读出来,非常浪费。
更聪明的做法是用滑动窗口的思想,配合FPGA的“行缓冲”(Line Buffer)来构建数据流。核心逻辑是这样的:
- 在FPGA内部维护若干行像素缓存(行数取决于模板高度),数据从传感器或DDR输入后,按行逐像素填充这些缓存;
- 每当新的像素进入,整个窗口就向右滑动一列;窗口到达行的末尾时,换到下一行继续滑动;
- 这样,每个输入像素只需要从外部存储读一次,但它会被用于计算多个候选位置——被完整地复用了。
具体来说,我用的搜索区域是64×64,模板是24×24,那么候选位置总共有(64-24+1)² = 1681个。如果不用滑动窗口,每个候选位置都要读576个像素,总计约96万次像素读取;用滑动窗口后,整个搜索区域只需要4096次像素读取(64×64),数据访问量下降了99%以上。这个差距是决定性的。
2.4 并行度规划:全并行、行并行还是部分并行
接下来是SAD计算阵列的设计。这里有个基本的资源-性能权衡:并行度越高、计算速度越快,但消耗的DSP(数字信号处理单元)和LUT(查找表)资源也越多。我总结下来有三种典型的并行架构:
第一种是全并行架构。把M×N个绝对差计算单元全部展开,一个时钟周期算完一个候选位置的完整SAD值。24×24模板就需要576个减法器、576个绝对值计算单元和一个大型加法树。这种方案延迟最低,但资源消耗非常大,而且加法树的级数(需要11级左右)会拉长组合逻辑路径,时序收敛比较难。除非模板非常小,否则我不建议第一版就这么干。
第二种是行并行架构。以模板的一行为单位,并行计算一个候选块中每一行的SAD部分和,然后用加法树把这M个行和加起来。同样以24×24为例,需要24个减法器和24个绝对值单元,加法树的规模比全并行小一个数量级。这个方案在资源和速度之间比较平衡,适合模板中等的场景。
第三种是部分并行架构。只例化K个计算单元,每个时钟周期处理K个像素的绝对差,通过多周期累加完成整个候选块的计算。比如24×24的模板,如果K=24,就需要24个时钟周期算完一个候选位置。这种方案资源最省,但吞吐率最低,适合对实时性要求不高的场景。
我的做法是选择“行并行”和“部分并行”的组合:每个时钟周期并行处理一行(24个像素),24个时钟周期算完一个候选位置。配合像素流的滑动窗口,整个搜索区域的1681个候选位置可以在大约1681×24个时钟周期内扫描完。在200MHz的时钟下,这个耗时大约200微秒,远低于一帧视频的可用时间预算(约16.6ms@60fps)。
2.5 搜索区域的遍历策略与提前终止机制
全搜索(Full Search)是最暴力的做法——所有候选位置都算一遍,然后取最小值。它的优点是实现简单、结果最优,缺点是计算量最大。在实际项目中,我一般会根据移*动目标的运动特性做优化。目标的运动在相邻两帧之间是有连续性的,不太可能瞬间跳到很远的位置。所以一个非常实用的优化是搜索中心预测 + 有限搜索范围:用上一帧的目标位置作为中心,只在其周围一定范围内(比如左右各32像素、上下各32像素)计算SAD值,而不是在全图范围内遍历。这样既保证了跟踪的实时性,又不牺牲过多的准确度。
另一个技巧是提前终止(Early Termination)。在扫描一个候选位置的过程中,如果累加的部分SAD值已经超过了当前已经找到的最小SAD值,那这个候选位置就不可能是最优解了,可以直接终止计算,跳到下一个候选位置。这个技巧在跟踪场景下尤其有效,因为目标在连续帧中的位置变化通常不大,第一个或前几个候选位置往往就能找到不错的SAD值,后续大量候选位置在计算过程中就会被提前淘汰。我在实测中发现,加上提前终止后,搜索区域的平均计算量可以减少30%-50%,具体效果取决于目标的运动速度和搜索区域的初始化。
3. 核心模块的RTL设计与实现细节
3.1 顶层架构与模块划分
从顶层来看,我把整个系统划分为以下几个模块:
顶层模块 (sad_tracker_top) ├── 像素输入接口 (pixel_rx) ├── 行缓冲控制器 (line_buffer_ctrl) ├── 模板存储模块 (template_mem) ├── SAD计算阵列 (sad_array) ├── 最小值搜索模块 (min_search) ├── 跟踪状态机 (tracker_fsm) └── 结果输出接口 (result_tx)这里我特别强调一下“模块边界清晰”的重要性。FPGA开发和软件开发的共同点是——模块化设计能显著降低调试难度。每个模块都有明确的输入输出接口,信号命名规范统一,这对后续的仿真验证和板级调试都很有帮助。
3.2 行缓冲控制器的设计要点
行缓冲是SAD计算的关键前置模块,它的设计质量直接决定数据流的顺畅程度。我的实现思路是:
// 以24行行缓冲为例,每行缓存64个像素(搜索区域宽度) // 使用移位寄存器实现,每来一个像素,所有行的数据同步右移 reg [7:0] line_buf [0:23][0:63]; always @(posedge clk) begin if (pixel_valid) begin for (int i = 0; i < 24; i = i + 1) begin for (int j = 63; j > 0; j = j - 1) begin line_buf[i][j] <= line_buf[i][j-1]; end end // 新像素进入第一行 line_buf[0][0] <= pixel_data; // 其余行从上一行的对应位置移入 for (int i = 1; i < 24; i = i + 1) begin line_buf[i][0] <= line_buf[i-1][63]; // 这里需要仔细设计行间衔接 end end end实际设计时要特别注意行间的衔接逻辑。上面的代码只是一个简化示意,真实场景中行缓冲的数据移动需要配合像素坐标和行列计数器精确控制,否则会出现错位问题。
行缓冲的存储资源消耗也不容忽视。24×64×8bit = 12288bit ≈ 12Kb,在小规模FPGA上这个是完全可以接受的。如果搜索区域更大(比如128×128),就需要考虑用BRAM替代寄存器来实现行缓冲,以节省寄存器资源。
3.3 SAD计算阵列的RTL实现
SAD计算阵列是整个设计的心脏。在行并行方案下,它的核心逻辑是:
module sad_row_calc #( parameter TEMPLATE_WIDTH = 24, parameter DATA_WIDTH = 8 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] search_pixels [0:TEMPLATE_WIDTH-1], // 搜索区域当前行的24个像素 input wire [DATA_WIDTH-1:0] template_pixels [0:TEMPLATE_WIDTH-1], // 模板对应行的24个像素 output wire [DATA_WIDTH+5:0] sad_row_sum // 13bit,24个8bit数累加最大值为24*255 ); // 第一级:并行减法并取绝对值 wire [DATA_WIDTH-1:0] abs_diff [0:TEMPLATE_WIDTH-1]; generate genvar i; for (i = 0; i < TEMPLATE_WIDTH; i = i + 1) begin : abs_diff_gen assign abs_diff[i] = (search_pixels[i] > template_pixels[i]) ? (search_pixels[i] - template_pixels[i]) : (template_pixels[i] - search_pixels[i]); end endgenerate // 第二级:加法树(24输入加法树,5级) // 这里使用树形结构逐级两两相加,减少组合逻辑深度 // 实际实现时可以用流水线寄存器切分,提高时钟频率 reg [DATA_WIDTH+5:0] sum_stage1 [0:11]; reg [DATA_WIDTH+6:0] sum_stage2 [0:5]; reg [DATA_WIDTH+7:0] sum_stage3 [0:2]; reg [DATA_WIDTH+8:0] sum_stage4 [0:1]; reg [DATA_WIDTH+9:0] sum_stage5; // 每个时钟周期完成一级加法,最终结果延迟5个时钟周期 // ... endmodule这段代码里有两个值得展开说的设计决策:
第一是绝对值计算。FPGA里没有现成的绝对值指令,但可以巧妙地利用比较器加减法器来实现。我的写法是:先比较两个数的大小,然后用大的减小的。这样虽然消耗两个减法器的资源(实际只有一个是有效输出),但逻辑路径短、时序好。另一种常见的做法是使用补码特性做条件取反,但那样会增加额外的异或门开销,对比下来并不划算。
第二是加法树的流水线切分。24个8bit数求和,如果一次性用组合逻辑完成,会产生很深的加法链(大约4-5级加法的组合延迟),在200MHz以上时钟下极难收敛。我的做法是将加法树分5级流水线:第一级把24个数分成12对相加,第二级把12个结果分成6对相加,依此类推,最后得到1个结果。每级之间用寄存器切分,这样每级组合逻辑只有一条简单加法,时钟频率可以显著提高。
3.4 最小值搜索模块:在线比较与索引追踪
最小值搜索模块的功能是在所有候选位置的SAD值中找出最小值,并记录其坐标。这个模块和SAD计算阵列直接相连,采用了“在线比较”的策略:
module min_search #( parameter DATA_WIDTH = 16, parameter COORD_WIDTH = 8 )( input wire clk, input wire rst_n, input wire sad_valid, // SAD结果有效信号 input wire [DATA_WIDTH-1:0] sad_value, input wire [COORD_WIDTH-1:0] pos_x, input wire [COORD_WIDTH-1:0] pos_y, output reg [DATA_WIDTH-1:0] min_sad, output reg [COORD_WIDTH-1:0] min_x, output reg [COORD_WIDTH-1:0] min_y ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin min_sad <= {DATA_WIDTH{1'b1}}; // 初始化为最大值 min_x <= 0; min_y <= 0; end else if (sad_valid) begin if (sad_value < min_sad) begin min_sad <= sad_value; min_x <= pos_x; min_y <= pos_y; end end end endmodule这里有个容易踩坑的小细节:min_sad的初始值必须设为最大值(全1),而不是0。我见过不少初学者在这里用<= 0初始化,导致最小值搜索永远停留在初始值,整个模块输出的目标位置永远是(0,0)。
另外一个实用技巧是:当多个候选位置的SAD值相同时,可以优先保留先出现的那个(坐标更小的),这样在目标跟踪中能避免目标位置的抖动。这个规则在模块里就体现了——只有严格小于才更新,等于时不更新。
3.5 跟踪状态机的设计与模板更新策略
整个跟踪过程由一个有限状态机(FSM)控制,我的状态划分是这样的:
- IDLE:等待启动信号,收到后进入TEMPLATE_INIT状态;
- TEMPLATE_INIT:从搜索区域中心位置提取模板数据,存入模板存储模块;
- SEARCH:执行SAD搜索,扫描整个搜索区域的所有候选位置;
- RESULT_OUT:输出最小SAD值对应的坐标,作为目标的当前位置;
- TEMPLATE_UPDATE:根据新位置从当前帧提取新的模板,更新模板存储模块;
- WAIT_NEXT_FRAME:等待下一帧开始信号,进入SEARCH状态。
其中模板更新策略是目标跟踪效果好坏的关键。如果完全不更新模板,目标外观一旦发生变化(光照变化、目标旋转、尺度变化),跟踪就会逐渐漂移最终丢失;如果每帧都全量更新模板,又可能出现“模板污染”问题——一旦某一帧跟踪结果有偏差,模板会被错误地刷新,之后的偏差会越来越大,最终导致跟丢。
我的做法是条件更新:只有当当前帧的最小SAD值低于阈值时,才用新的搜索结果更新模板;如果SAD值较大,说明当前帧的目标匹配置信度不高,保留原模板,等待后续帧重新确认。阈值的选择需要通过实验标定,我建议设为模板总像素数的某个比例,比如10%-20%。例如24×24模板,SAD总阈值可以设置在5000左右(24×24×255×0.1 ≈ 14688,实际可以更宽松一些),这个数值需要结合具体应用场景调整。
4. 时序约束、资源优化与实测结果分析
4.1 时序收敛的实战调整:加法树、扇出与关键路径
FPGA设计从功能仿真通过到板级跑通之间,最大的拦路虎几乎都是时序问题。我最初在200MHz时钟下综合,时序报告一片通红,关键路径集中在SAD计算阵列的加法树上。排查后发现,主要问题出在三个方面:
第一个是加法树的组合逻辑过深。虽然我按5级流水线切分了24输入加法树,但每一级的加法器位宽不同,从8bit一直扩展到13bit、14bit、15bit,级联起来组合延迟依然偏大。我的解决方法是使用Vivado中的retiming选项,并手动在加法树的关键节点插入额外的流水线寄存器,把加法树拆得更细。
第二个是模板数据扇出过大。模板存储模块输出的24个像素,需要同时广播到24个SAD计算单元,总扇出达到576个负载,导致布线拥塞和延迟增加。解决办法是复制模板存储——在FPGA里例化多份相同的模板BRAM,每个计算单元只连接其中的一份,这样扇出被显著降低。代价是BRAM资源增加,但在模板不大时完全划算。
第三个是行缓冲的输出使能逻辑复杂。行缓冲模块里为了处理行间衔接,生成的控制信号有大量组合逻辑,导致路径延迟超标。我后来简化了控制逻辑,改用预先计算的地址偏移配合计数器来控制,减少了组合逻辑层级,时序问题迎刃而解。
4.2 资源占用与功耗评估
在我的实际项目中,使用的FPGA是Xilinx Artix-7系列的XC7A100T,最终的资源占用情况如下:
| 资源类型 | 使用量 | 总量 | 占用率 |
|---|---|---|---|
| LUT | 12,634 | 63,400 | 19.9% |
| Flip-Flop | 8,209 | 126,800 | 6.5% |
| BRAM(36Kb) | 12 | 135 | 8.9% |
| DSP48E1 | 0 | 240 | 0% |
这里有个值得注意的点:整个SAD计算阵列没有使用DSP单元。因为绝对差和加法树都可以用LUT和进位链实现,对于8bit精度的小规模计算,LUT实现反而更灵活,不会因为DSP资源布局问题导致布线拥塞。当然,如果模板宽度更大、并行度更高,用DSP做多路加法也是可行的优化方向。
功耗方面,在200MHz时钟下,整颗FPGA的实测功耗大约是2.3W,其中动态功耗约1.8W、静态功耗约0.5W。这个功耗水平在嵌入式视觉系统里非常友好,A78的芯片配个普通的散热片就完全够了。
4.3 实测性能与跟踪效果
整个系统的实测表现是在1080p@60fps的视频流中,对单个刚性目标的跟踪延迟约230微秒(从输入像素到输出目标坐标),其中SAD搜索过程占约200微秒,其余为流水线延迟和状态机切换开销。在目标以中等速度运动、外观变化不大的场景下,跟踪稳定不丢帧。
我特别关注的一个指标是“搜索区域的扫描效率”。在加入提前终止机制后,实际的平均计算量约为原始全搜索的55%左右,搜索性能提升明显。不过在目标快速运动或者被暂时遮挡的场景下,提前终止的收益会下降,因为最优候选位置可能出现在搜索区域的边缘,导致大部分候选位置都需要完整计算。
5. 系统调试与验证过程中的关键问题记录
5.1 功能仿真正确但板级结果错误的排查方法
这是FPGA开发中最让人头疼的问题之一:仿真一切正常,上了板子就是不对。我在这个项目里遇到了一个典型的案例——输出的目标坐标偶尔会跳变到搜索区域边缘,并且SAD值异常大。
排查过程是这样的:先用ILA逻辑分析仪抓取SAD计算阵列的输出,发现某些时钟周期出现了全0的SAD值,而正常情况下应该有个非零的基础值。顺着数据流往前查,发现行缓冲模块在某些特定坐标时会输出全0数据。
最终定位到问题根源是行缓冲的行间数据衔接逻辑。我在处理“新一行的第一个像素进入时,上一行的最后一个像素如何移入下一行”这个细节时,边界条件写错了,导致在第24、48等特定列位置,行缓冲内出现了一行全0数据。修改了行间衔接的地址控制逻辑后,问题彻底消失。
这个经历给我的教训是:行缓冲这类与坐标强相关的模块,边界条件的仿真用例必须覆盖行首、行尾、首个像素、最后一个像素等极端情况,否则仿真很难发现隐患。
5.2 模板更新中的“死锁”现象
另一个值得一提的问题是模板更新逻辑的一个隐蔽Bug。我在设计模板更新策略时,要求“只有当SAD值低于阈值时才更新模板”。这个逻辑本身没有问题,但有一个边界情况没考虑到:如果目标刚进入画面时,搜索区域中心位置并不完全是目标(比如目标只占了一半搜索区域),初始模板本身就“不干净”,之后每帧的SAD值都可能超过阈值,导致模板永远不会更新——跟踪器失去了自我修正能力。
解决方法是增加了“强制更新”机制:如果连续超过30帧SAD值都超过阈值,就认为模板严重过时,强制用当前帧的最小SAD位置提取新模板。这个机制在很大程度上改善了跟踪的鲁棒性,特别是在目标外观随着视角变化而缓慢演变的场景中。
5.3 PCIe/HDMI等接口集成时的注意事项
在实际部署中,FPGA很少单独工作,总要接各种接口。我在集成HDMI输入输出时遇到的问题是:HDMI的像素时钟和FPGA内部的SAD计算时钟不同域,需要做跨时钟域处理。最简单的做法是用FIFO做异步缓冲,把HDMI像素流先存入FIFO,再用SAD计算时钟域读取。这块属于成熟技术,但有一点容易忽略——FIFO的深度一定要按照最大突发长度(Burst Length)来计算,而不是平均速率。我当时按平均速率算的FIFO深度,结果在画面快速切换时出现了数据溢出丢像素,目标跟踪短暂失效。
这个问题最后通过增大FIFO深度解决了,另外我还在输入端加了一个像素有效信号(de)的同步处理,彻底消除了亚稳态风险。
5.4 三帧对齐与流水线延迟补偿
跟踪系统里有个细节容易被软件工程师忽略,但硬件工程师必须面对:输入数据、处理结果和输出坐标之间是有流水线延迟的。比如我们在第N帧的第1000个像素输入后,才开始计算SAD,但计算结果可能要到第N帧的第2000个像素输入时才能出来。如果直接把结果用于控制云台或标注显示,会出现“控制滞后”现象。
我的做法是在跟踪状态机里记录一个“流水线延迟计数器”,计算出从搜索区域起始像素到SAD结果输出的精确时钟周期数。在输出目标坐标时,把目标位置从“搜索起始时刻的坐标系”换算到“当前时刻的坐标系”,也就是加上一个预测位移量。这个位移量可以用目标的历史运动速度来估计(即卡尔曼滤波的雏形,如果做平滑跟踪,可以接一个简单的卡尔曼滤波器做后处理)。
6. 从工程角度对SAD硬件化的综合评判与优化展望
6.1 当前方案的优缺点复盘
如果让我用一句话评价FPGA+SAD做目标跟踪这个技术路线,我会说:在小规模、高实时性、低功耗的嵌入式视觉场景中,它是一个极具竞争力的选择;但在模板匹配精度要求极高、目标外观复杂多变的场景中,它存在明显的天花板。
优点方面不再重复,前面已经阐述得比较充分。局限方面主要有两个:
一是SAD本身是像素级的相似度度量,对光照变化、目标形变、尺度变化非常敏感。同一个目标,如果角度偏了5度、光照暗了两档,SAD值就会急剧上升,跟踪稳定性大打折扣。
二是FPGA的开发周期和调试成本确实比软件高一个量级。RTL仿真、时序收敛、板级调试,每一步都需要专业技能。但一旦架构确定、模块复用起来,后期的增量开发效率还是不错的。
6.2 多尺度模板匹配与多目标扩展思路
针对SAD对尺度变化敏感的问题,一个实用的扩展方向是“多尺度模板匹配”:在FPGA中例化多套不同尺寸的模板(比如16×16、24×24、32×32),每套模板并行计算SAD,最后在搜索模块里取所有尺度的最小值。这样可以在目标靠近/远离摄像头时保持稳定的跟踪。资源代价是模板存储和计算阵列都翻倍,但在中等规模的FPGA上仍然可以接受。
多目标跟踪的扩展相对复杂,核心瓶颈在于SAD计算阵列需要为每个目标分别分配一套计算资源。如果是两个目标,就把计算阵列例化两份;如果目标数量动态变化,需要引入任务调度的状态机,在多个目标之间时分复用计算阵列。后者在逻辑上更复杂,但资源效率更高。
6.3 结合卡尔曼滤波与光流法的性能增强路径
SAD跟踪输出的坐标往往带有一定的噪声和抖动,特别是在目标运动不规律或图像有噪声时。我的建议是在SAD结果后续接卡尔曼滤波,对目标位置做平滑和预测。卡尔曼滤波器在FPGA上的实现不算复杂——核心就是一个状态预测方程和一个更新方程,涉及几次矩阵乘法和除法(可以用移位近似替代)。加上卡尔曼滤波后,跟踪的稳定性会有明显提升,实测中目标位置的抖动幅度大约能降低60%-70%。
如果还想进一步增强鲁棒性,可以考虑在SAD基础上引入“光流法”做局部修正:先用SAD锁定粗略位置,再用LK光流法在局部窗口内计算目标的精确位移。这种“粗定位+精修正”的组合策略在FPGA上是可以流水线实现的,但复杂度较高,适合在有经验的团队里推进。
最后还是分享一个我个人的体会:做FPGA图像处理,最大的成就感不是看代码综合通过、板子跑通的那一刻,而是当你通过硬件架构设计,把一个在通用处理器上需要几毫秒才能完成的运算压缩到几百微秒,并且功耗只有几瓦的时候,你真正感受到了“用空间换时间”这句硬件设计名言的份量。硬件思维和软件思维是很不一样的,但一旦跨过那道坎,你会发现FPGA世界里的每一个时钟周期都充满确定性,每一个逻辑门都在执行明确的任务,这种掌控感是其他开发方式很难给的。如果这篇文章能帮你少走几步弯路,那这些时间花得就值了。