FEATURED · 精选文章

FPGA实现SAD模板匹配:从算法到Verilog的实时目标跟踪实战

发布时间 / 2026/9/8 18:53:28
来源 / 创域科博编辑部
栏目 / 资讯中心
FPGA实现SAD模板匹配:从算法到Verilog的实时目标跟踪实战 1. 从图像数据流到目标坐标SAD算法的硬件友好性拆解做FPGA图像处理这几年我最大的体会是很多在软件里随手就能写的算法搬到硬件上完全是另一回事。SAD模板匹配恰好是少有的天生适合FPGA的算法之一这也是为什么很多实时目标跟踪项目的第一版原型都会选它。SADSum of Absolute Differences绝对差值和的原理简单到一句话就能说清拿一个已知的目标模板在视频帧的搜索区域内逐个位置滑动计算模板与对应图像块的像素差值绝对值之和和最小的位置就是目标最可能出现的地方。听起来就是一个双重循环加累加器的事在CPU上写起来确实也就十几行代码。但放到实时视频处理场景里事情就没那么轻松了。以1080p60fps的输入为例一帧图像大约有200万个像素留给每帧的处理时间只有16.7毫秒。如果搜索区域是100×100像素模板是32×32像素暴力遍历需要计算的候选位置接近一万个每个位置要做1024次减法、取绝对值和累加。这个计算量在通用处理器上做软件优化能跑到实时但CPU占用率基本就满了。而在FPGA上SAD的计算结构可以被完全并行化和流水线化同样的工作量占用资源很少还能把整条图像处理链路的时间压到微秒级。不过这里要提醒一句SAD在FPGA上好实现不等于随便写写就能跑。实际工程里最麻烦的往往不是算法本身而是数据怎么喂进来、结果怎么送出去、时序怎么收敛。所以这篇文章我会从算法公式出发一路讲到具体的Verilog代码结构、乒乓缓存设计、时序约束技巧最后聊一聊实测性能和踩过的坑。先看SAD的数学表达。假设模板T的大小是M×N搜索图像I中的候选块与模板的SAD值为SAD(x, y) Σ(i0 to M-1) Σ(j0 to N-1) |I(xi, yj) - T(i, j)|其中(x, y)是候选块左上角在搜索区域中的坐标。遍历整个搜索区域后取SAD值最小的位置作为跟踪结果。这个公式没有任何复杂的非线性运算只有减法、绝对值、加法三种操作映射到FPGA上就是减法器、绝对值逻辑和加法树。整个计算链路的延迟只有几个时钟周期吞吐量极高。1.1 为什么模板匹配在target跟踪场景仍不过时现在深度学习目标检测很火动不动就是YOLO、Transformer似乎传统方法已经被淘汰了。但实际做工程项目你会发现很多场景根本用不上那么重的方案。第一FPGA上跑神经网络推理尤其是视频流实时处理资源开销非常大。一个稍微像样点的卷积网络即使做了量化也要消耗大量的DSP和BRAM资源逻辑资源和功耗也跟着上去。而SAD模板匹配只用到加法器和比较器几乎不碰DSP资源逻辑资源消耗也很小在入门级芯片上就能跑得很轻松。第二当目标是预先知道的小尺寸刚体比如零件、标记点、特定纹理区域模板匹配的精度和稳定性完全够用。目标跟踪不等于一定要做语义级别的理解很多工业场景只需要知道目标在画面里的像素坐标就够了。第三模板匹配算法是确定性的行为完全可预测这对通过安全认证或功能验证非常有帮助。而深度学习模型的行为很难做到逐帧可解释。我做过一个高速运动目标跟踪的Demo目标是一个快速移动的小球背景相对干净。用SAD模板匹配在100MHz时钟下处理720p视频单帧匹配耗时不到1毫秒跟踪稳定没有任何卡顿感。这要是用软件跑单帧耗时至少翻十倍还得面临操作系统调度带来的延迟抖动。2. 硬件架构设计从顶层框图到数据通路FPGA工程和软件工程最大的区别在于你得先把数据的流动路径想清楚再谈计算逻辑。SAD模板匹配的整体架构我习惯这样划分图像缓存与数据流控制模块、SAD计算阵列并行比较器阵列、最小值和坐标提取模块、以及输出与显示控制接口。每个模块各司其职组合成一条完整的流水线。顶层数据通路的逻辑是这样的视频输入可以是摄像头接口也可以是测试用的测试图案发生器先把像素数据写入行缓存行缓存将数据组织成模板大小的图像块然后送入SAD计算单元。计算单元对搜索区域内的所有候选位置并行计算SAD值或者按行流水计算得到一个SAD值序列送入最小SAD追踪模块。该模块不断比较当前SAD值和历史最小值最终输出最小SAD值对应的坐标。2.1 搜索窗口的行缓存设计别小看这块BufferSAD计算需要同时访问模板和图像中的邻域像素。如果直接从外部存储器读取每个候选块的像素访问次数会爆炸。以32×32模板、100×100搜索区域为例暴力读取的像素数大约是32×32×100×100 ≈ 1000万次这在实时系统里是不可接受的。正确的做法是用行缓存Line Buffer在FPGA内部构建一个滑动窗口。经典方案是利用FPGA内部的BRAM实现N行缓存N等于模板高度比如32行每来一个新像素窗口整体右移一格当窗口到达行尾时换到下一行。这样任意时刻窗口内都保存了当前位置周围32×32的图像块数据复用率接近100%。行缓存的实现有一些细节值得注意用移位寄存器还是用BRAM模板小的话比如8×8可以用移位寄存器实现延迟小、逻辑简单模板大的话16×16以上建议用BRAM实现多行循环缓存节省寄存器资源。建议用两级缓存第一级缓存原始图像的行数据第二级缓存窗口内的数据块。这样可以把图像输入速率和计算速率解耦。行缓存深度要留余量。如果图像宽度是1920缓存深度至少是1920但实际工程中我习惯留到2048因为有些视频源会有行消隐和同步信号控制逻辑更省心。实际代码里行缓存的经典写法是用一个二维数组 写指针循环覆盖。以16行缓存、每行1280像素、8bit灰度为例reg [7:0] line_buffer [15:0][1279:0]; reg [9:0] write_addr; reg [3:0] write_line; always (posedge clk) begin if (data_valid) begin line_buffer[write_line][write_addr] pixel_in; if (write_addr 1279) begin write_addr 0; write_line write_line 1b1; end else begin write_addr write_addr 1b1; end end end这段代码维护了16行、每行1280像素的滑动窗口。窗口的读地址根据当前像素坐标动态生成取出的16×16数据块直接对接后续的SAD计算阵列。2.2 SAD计算阵列并行度与资源的天平SAD计算阵列是整个设计的核心。一次完整的SAD计算要对模板和图像块的每一个像素做减法、取绝对值、累加。在FPGA上这个操作可以按像素级并行展开。以16×16模板为例理想情况下我可以生成256个减法器和256个绝对值电路然后通过一个加法树把256个结果累加起来。这样只用一个时钟周期就能完成一个候选位置的SAD计算。但256路并行意味着大约256个DSP48E1或大量的LUT对资源紧张的芯片不现实。实际工程中需要在并行度和资源占用之间取平衡。我常用的设计是把一行的16个像素做并行计算然后按行累加16个像素并行做减法、绝对值、部分和消耗16个减法器和16个加法器每一行的部分和在一个时钟周期内完成16行的部分和再串行累加得到完整的SAD值。这样只需要大约16路的并行计算资源代价是单个候选位置需要16个时钟周期才能出结果。不过这个速度对大多数跟踪场景已经够用了。下面给出16×16模板、行并行实现的核心Verilog框架// 假设win_data和template_data是16 bit位宽的16个像素 // 实际使用中应展开为16路减法器 wire [8:0] diff [0:15]; wire [9:0] abs_diff [0:15]; genvar i; generate for (i 0; i 16; i i 1) begin: sad_cell assign diff[i] {1b0, win_data[i]} - {1b0, template_data[i]}; assign abs_diff[i] diff[i][8] ? (~diff[i] 1b1) : diff[i]; end endgenerate wire [13:0] row_partial_sum; assign row_partial_sum abs_diff[0] abs_diff[1] ... abs_diff[15];每一行得到一个14bit的部分和之后在16行的尺度上做累加。这个加法树可以做成pipeline结构每级插一拍寄存器避免组合逻辑过长导致时序不收敛。我实测过一个8×8模板、64路像素级并行加法树的设计在Artix-7系列上跑到200MHz没有任何问题。如果做16×16模板全并行256路频率会降到150MHz左右但对720p视频来说仍然绰绰有余。2.3 最小SAD追踪比大小也有讲究SAD计算阵列不断产生候选位置的SAD值目标跟踪需要从中选出最小的那个以及对应的坐标。这个模块看似简单其实有一个容易忽略的性能瓶颈。如果你在每个时钟周期都做一个当前SAD 历史最小SAD的比较那整个比较链会变得很长。比较器是有延迟的尤其是当历史最小值需要反馈到下一拍时组合逻辑回路很可能成为时序瓶颈。更好的做法是流水线化比较过程把SAD值和坐标写进一个FIFO由专门的最小值搜索状态机来处理。状态机每次从FIFO取一个SAD值和当前最小值比较同时记录坐标。FIFO可以平滑计算阵列的输出节奏让最小值搜索不阻塞SAD计算。另一个工程细节是不要只记最小SAD值还得把对应的搜索位置坐标存下来。坐标需要跟着SAD值一起进FIFO或者通过一个与SAD计算阵列同步的计数器来生成。我习惯用后者搜索位置由行列计数器直接编码当SAD值被判定为新的最小值时把当前计数器值锁存到输出寄存器。下面是这个核心比较逻辑的示意always (posedge clk or negedge rst_n) begin if (!rst_n) begin min_sad 16hFFFF; best_x 0; best_y 0; end else if (sad_valid) begin if (sad_value min_sad) begin min_sad sad_value; best_x curr_x; best_y curr_y; end end end注意一个坑如果搜索区域里有多个候选位置SAD值相同到底取哪一个这在模板匹配里是个真实存在的问题尤其当背景有大片平坦区域时。我的做法是取先出现的即只在严格小于时更新这样实现最简单行为也确定。如果你需要取中心位置之类的策略就得再加一个计数器记录候选位置与搜索区域中心的距离并在SAD值相等时做二次比较。3. 模板更新机制与多目标扩展从Demo到工程化的距离能跑通单目标跟踪只是第一步。真正落地的目标跟踪系统几乎都会遇到两个问题目标外观变化了怎么办画面里有多个目标怎么办先说模板更新。固定模板在目标发生尺度变化、旋转、光照变化时匹配效果会迅速恶化。比如跟踪一辆正在转弯的车模板还是直着时的正面照转过30度角之后SAD值就会暴涨跟踪框很容易飘走。最朴素也最有效的方案是每N帧用当前跟踪结果区域的图像更新模板。这样做的好处是能持续跟随目标外观变化坏处是跟踪错误一旦发生错误会被累积放大也就是所谓的模板漂移。为了解决漂移问题工程上常用的折中方案是保留初始模板第一帧手工框选或检测得到的目标每次匹配时同时计算当前模板和初始模板的SAD当当前模板的SAD迅速变差时回退到初始模板重新匹配。这在FPGA上的实现成本很低只需要双端口RAM存两份模板比较逻辑稍作修改即可。而它带来的稳定性提升非常明显尤其适合目标被短暂遮挡或姿态突变的场景。如果你用的是Zynq这类SoC FPGA更聪明的做法是把模板更新放到ARM核上做比如每50帧用当前跟踪框的图像重新计算一次模板做一些简单的滤波比如新旧模板加权平均然后通过AXI-Lite接口把新模板写入FPGA侧的模板RAM。FPGA只管高速匹配ARM管决策和更新分工非常合理。再说多目标扩展。SAD算法天然支持多目标扩展思路有两种时分复用一个SAD计算阵列轮流加载多个目标的模板输出多个最小SAD值和坐标。缺点是当目标数量增多时整体帧率会线性下降。空间并行复制多份SAD计算阵列每份配一个模板同时计算多个目标的位置。缺点是资源占用会成倍增加。如果做4个目标的跟踪4套16×16模板的SAD阵列中等规模的FPGA芯片能扛住。我实际做过的项目里用空间并行方案在一块中等规模的Kintex-7上同时跟踪4个目标处理1080p60fps输入资源占用大约在45%左右时序收敛良好约180MHz。如果目标数量再多建议还是考虑用DSP软件方案来卸载部分计算。毕竟FPGA也不是万能的关键还是选对计算引擎。4. 系统集成与实时性分析当SAD匹配撞上视频流算法模块单独验证没问题不代表放到整个视频链路里也能跑。SAD模板匹配的目标跟踪一般嵌在视频采集 → 预处理 → 目标匹配 → 结果叠加 → 显示输出的完整系统中。这里每个环节都会影响SAD模块的输入数据质量以及整个系统的实时性。4.1 输入预处理灰度转换与降噪SAD算法本身是基于灰度图像的所以彩色视频进来必须先做RGB转灰度。如果摄像头输出的是RAW Bayer格式那还得先做去马赛克再转灰度。这些预处理在FPGA上做很快但要注意预处理延迟要计算进整个流水线——如果你的预处理要缓存好几行才能出结果那SAD模块收到第一帧完整数据的时间会往后拖。降噪方面如果画面噪声较大SAD匹配的稳定性会很差。我的经验是加一个简单的3×3中值滤波或者高斯滤波作为预处理。中值滤波在FPGA上的经典实现是行缓存 排序网络3×3窗口的9个像素排序需要3个比较器级联资源开销不大效果立竿见影。但注意滤波会使边缘略微模糊如果目标本身是很精细的纹理结构模板匹配精度可能会轻微下降这一点要在实际项目中权衡。4.2 帧存调度DDR3/DDR4带宽与时序跟踪是在视频帧上撒搜索窗口可能涉及对整帧图像的随机访问。如果要把搜索区域扩展到全图或者需要大范围的搜索比如目标快速运动就需要把整帧图像存入外部DDR内存。这时DDR读写带宽就成为主要瓶颈。以1080p、8bit灰度、60fps为例写入帧存带宽约124MB/s读取搜索窗口数据的带宽取决于搜索策略。如果搜索区域覆盖整个画面每个候选位置都要从DDR读一个模板大小的图像块带宽需求会爆炸根本跑不动。所以全图搜索的SAD跟踪在实际工程中很少见大多数情况是基于上一帧目标位置的小范围搜索——比如只搜上一帧位置周围64×64的区域。这样DDR读取带宽可以控制在一个可接受的范围。我做过一个测试上一帧位置周围64×64搜索、16×16模板720p60fps输入DDR3-1600单片16bitDDR带宽占用大约25%余量充足。如果把搜索范围扩大到128×128带宽占用直接飙到80%以上系统的其他部分比如显示叠加就会开始卡顿。4.3 流水线设计的时序约束要点FPGA设计跑到系统集成阶段最让人头疼的就是时序收敛。SAD计算阵列动辄几百级的组合逻辑如果不加流水线寄存器时序必然出问题。我的做法是在每个SAD计算级之间插入一级寄存器形成3~5级流水线减法和绝对值合并在同一级流水内完成减少组合逻辑深度加法树每级加法插一拍寄存器用乒乓寄存器加法器结构实现。时序约束文件XDC里建议给SAD计算阵列单独定义时钟组并对跨时钟域信号做异步FIFO隔离。视频输入时钟和系统时钟不一致是很常见的情况比如摄像头给一个24MHz像素时钟系统主频跑100MHz中间必须通过异步FIFO转换。我实测过一个设计因为忽略了跨时钟域约束导致跟踪坐标偶尔跳变排查了半天才发现是亚稳态问题。下面给出一段简化版的XDC约束示例create_clock -period 10.000 -name sys_clk [get_ports clk] create_clock -period 41.667 -name pix_clk [get_ports pix_clk] set_clock_groups -asynchronous -group [get_clocks sys_clk] -group [get_clocks pix_clk]5. 实测调优SAD阈值、搜索策略与性能数据设计做完之后真正决定能不能用的是调优阶段。SAD模板匹配有非常多的旋钮可以调不同场景下的最优参数差异很大。分享几个我实际调过的经验。5.1 SAD阈值与遮挡判定的配合SAD最小值本身不能直接说明匹配有多可靠。如果目标跑出画面外或者被完全遮挡SAD计算还是会给你一个最小的数值但那个数值通常比正常跟踪时高不少。所以工程上必须设置一个SAD阈值当最小SAD值超过阈值时判定为跟踪丢失或目标被遮挡此时可以切换到等待模式或者启动搜索模式扩大搜索区域重新寻找。阈值的标定方法我建议这样先让系统对着目标正常跟踪50~100帧记录SAD值的统计分布最大值、均值、标准差然后设定阈值为均值加3~4倍标准差。在FPGA上做这个统计不方便一般是在仿真阶段用软件算好再固化到寄存器里。如果想做自适应阈值可以把统计逻辑放在Zynq的ARM端每N帧回调一次更新阈值寄存器。5.2 多尺度搜索大目标变小目标怎么办SAD模板匹配对尺度变化非常敏感。目标在画面里变大变小SAD值都会迅速增大。解决这个问题的一个实用方案是多尺度模板池预先生成目标在不同尺度下的多个模板比如1.0倍、0.85倍、0.7倍每一帧对每个尺度都做一次SAD匹配取所有尺度中最小的SAD值及其坐标作为输出。这个方案在FPGA上的实现开销是乘法器资源BRAM容量。如果模板数量不多3~5个尺度资源成本还能接受。我的实测感受是多尺度匹配对跟踪稳定性的提升是肉眼可见的尤其是在俯视相机比如无人机视角场景中目标大小随高度变化非常明显。不过要注意多尺度匹配的计算时间会成倍增加。对于实时系统建议优先做时间维度的并行优化也就是把不同尺度的匹配任务分配到不同的计算阵列上而不是串行执行。5.3 实测性能数据参考最后给出一组典型的实测数据硬件平台是Xilinx Artix-7 XC7A200T工作时钟150MHz图像分辨率1280×720灰度8bit参数配置数值模板大小32 × 32搜索区域64 × 64以上一帧目标位置为中心候选位置数1089单候选位置SAD计算周期32个时钟周期单帧匹配总耗时约232微秒资源占用LUT/FF/BRAM/DSP12500/9800/20/0整体帧率60fps232微秒的匹配耗时意味着即使搜索区域进一步扩大依然有充足的性能余量。如果你用更高端的芯片比如Kintex-7或Zynq UltraScale跑更大的模板和更大的搜索区域也毫无压力。所以SAD模板匹配在FPGA上做目标跟踪最大的优势不是能不能做而是做了之后还剩多少资源给其他功能。这在实际产品设计中往往比单纯的算法效果更具决策价值。6. 踩坑实录跨时钟域、资源竞争与坐标抖动调试阶段总是会遇到一些让人抓狂的问题。分享几个我踩过的坑希望能帮你少走弯路。6.1 异步FIFO没处理好坐标偶尔跳变的症状是跟踪基本正常但每隔几秒钟目标坐标会突然跳出去几十个像素然后下一帧又跳回来。一开始怀疑是算法问题查了半天逻辑仿真完全正常。后来用ILA抓实时信号发现是像素时钟域和系统时钟域之间的异步FIFO读数据时出现了亚稳态导致偶尔读到错误的像素值SAD计算结果当然就错了。解决办法是给异步FIFO加上摆同步gray code指针同步并且预留两级寄存器同步后再判断FIFO空满状态。这个坑提醒我FPGA里最贵的时序成本往往不是算法本身而是接口转换。6.2 模板RAM和图像缓存的BRAM资源打架之前在一个设计中模板最多支持64个预留给多目标每个模板32×32×8bit 8KB总共512KB直接吃了半片Artix-7的BRAM。结果SAD计算阵列里头行缓存已经用了不少BRAM布局布线时资源告警甚至出现时序违例。解决方案有两个方向把模板存储在DDR里运行时通过AXI DMA按需读取到FPGA的BRAM缓存中。这样BRAM只需要保存当前用的几个模板即可压缩模板位宽。灰度8bit降到6bit或者5bitSAD精度略降但对跟踪稳定性影响不大。曾做过一次测试5bit量化模板下SAD值整体偏小但最小坐标基本不变。6.3 目标高速运动时跟踪框滞后目标移动速度太快搜索窗口跟不上目标直接跑出搜索范围。症状是跟踪框一开始还行目标加速后就丢掉然后到处乱跑。这不是算法算得慢而是搜索策略的问题。解决思路是引入运动预测。最简单的预测方法是记录前两帧目标坐标用差分近似速度预测当前帧的搜索中心。公式上就是predict_x last_x (last_x - prev_x)predict_y last_y (last_y - prev_y)然后把搜索窗口中心放在predict坐标上而不是上一帧坐标。这个改动在FPGA上几乎没有成本两个减法器加两个加法器。但效果非常显著目标以高速直线运动时跟踪成功率几乎达到100%。如果还想更精细一些可以上卡尔曼滤波不过卡尔曼在FPGA上实现涉及矩阵运算资源消耗大不少。我的建议是先用差分预测效果不够再考虑卡尔曼。很多场景下差分预测已经足够。另外有一个容易忽略的细节预测坐标不要直接作为跟踪输出而是作为搜索中心。最终输出仍然用SAD匹配到的坐标。这样即使预测偏了只要目标还在搜索窗口内依然能正确捕获。7. 用FPGA做SAD跟踪的几点经验总结如果让我用一个词概括FPGA上的SAD模板匹配目标跟踪我会选划算。算法逻辑简单、资源消耗可控、实时性极强是FPGA图像处理入门到进阶之间非常优质的一个练习项目。它可以锻炼行缓存设计、流水线架构、并行计算阵列、跨时钟域处理这些基本功而这些能力在更复杂的FPGA视频处理项目里都是通用的。再说几个个人体会最深刻的点。其一不要一上来就追求全并行。SAD的并行度可以根据芯片资源灵活调整先用行并行、串行累加的方式跑通整个链路再去优化并行度是更稳妥的路径。全并行方案在资源利用率上并不一定更优反而会引入更复杂的布线拥塞和时序收敛问题调试成本陡增。其二数据流的组织比计算本身更重要。很多FPGA初学者把精力都花在SAD计算逻辑上却忽略了行缓存、FIFO、坐标计数器这些数据管道。实际上视频处理系统的性能瓶颈十有八九出在数据搬运上而不是计算上。行缓存设计是否合理、跨时钟域FIFO是否可靠直接决定了整个系统的稳定性。其三善用Zynq这种SoC平台做任务分工。单纯的FPGA实现适合追求极致实时性和确定性要求的场景但一旦涉及复杂的模板更新策略、多目标管理、甚至和上层控制系统交互ARM核的加入会让系统架构清爽很多。SAD计算在PL侧做管理和决策在PS侧做这是Zynq平台上目标跟踪项目最经典的架构之一。最后留一个可以继续深入的优化方向当前做的是灰度图像的SAD匹配如果把RGB三通道的SAD分别计算再加权合并可以在彩色特征明显的场景下大幅提升匹配精度。实现上只需把单通道SAD阵列复制三份或者做时分复用资源增加约2倍但对特定目标的跟踪效果提升明显。以后有空我会把这个扩展的实现细节单独整理出来这次就先聊到这里。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻