1. 为什么用FPGA做目标跟踪:SAD算法的硬件化价值
做FPGA图像处理这些年,经常被问到一个问题:目标跟踪用软件做不就行了?OpenCV里一个matchTemplate函数就能搞定的事情,何必折腾硬件?这个问题得从应用场景和实时性说起。
SAD(Sum of Absolute Differences,绝对差值和)模板匹配算法,核心思路是拿一个已知的目标模板,在视频帧的搜索区域内逐像素滑动,计算每个位置模板与对应图像块的相似程度,差异最小的位置就是目标当前所在位置。算法本身不复杂,数学表达也很直白:
SAD(x, y) = Σ|I(x+i, y+j) - T(i, j)|
其中I是当前帧图像,T是模板,i、j遍历模板的行列。
但问题就出在这个"逐像素滑动"上。假设一帧图像是1920x1080,模板是64x64,搜索范围如果在整帧或者一个较大区域,计算量是百万级别甚至千万级别。CPU处理一帧可能就要几十毫秒到上百毫秒,而FPGA擅长的事情恰恰就是这种大量重复、可并行、数据流密集的计算。
我见过不少工程师一上来就直接拿HLS(High-Level Synthesis)写SAD,或者用纯Verilog硬憋实现,最后都遇到过时序收敛困难、资源爆表、或者调试起来无从下手的问题。这篇我就把我实际跑通的一套方案完整拆开讲:从算法怎么映射到硬件架构,到搜索策略怎么选,再到目标跟踪状态机怎么设计,最后是仿真和板上调试的踩坑记录。整套方案我是在Xilinx Artix-7平台上验证的,逻辑资源占用不算高,核心逻辑大概用了不到15K个LUT,跑在150MHz时钟下,对640x480@60fps的灰度视频流能做到实时处理。
适合看这篇内容的人有两类:一是刚接触FPGA图像处理、想找一个完整项目练手的同学,二是已经在做视频处理但想了解模板匹配类算法怎么在硬件上落地的工程师。我不会只贴一堆代码,而是尽量把每个设计决策背后的"为什么"讲清楚,这样你换成其他平台、其他分辨率,也能自己调整。
2. SAD算法到硬件架构的映射:一个反直觉的设计思路
2.1 为什么不是算完一整帧再处理
软件实现SAD很自然:先把一整帧图像存下来,然后两层循环遍历搜索区域,每个位置再套两层循环算模板内的SAD值。很直观,四层循环。但这个思路直接搬到FPGA里就是灾难,因为DDR带宽、片上存储、流水线节奏全都对不上。
FPGA图像处理的核心思路是流式处理(streaming)。像素数据从一个模块流向另一个模块,像流水线一样,每来一个像素就处理一个像素,尽量不在中间停下来等一整帧数据凑齐。SAD算法要做到流式处理,就需要重新审视数据依赖关系:模板T是固定的,可以常驻在寄存器里;图像I则是按行扫描顺序逐个到达的像素流。
这里有个反直觉的地方。软件里"搜索区域"是一个平面的概念,但硬件里图像是光栅扫描序到达的,一行一行从左到右从上到下。想让SAD窗口在图像上"滑动",实际上做的就是行缓存(line buffer)加上移位寄存器阵列,把二维窗口的像素同时准备好。
2.2 模板数据与图像数据的组织方式
真实项目中,模板一般是从第一帧里手动框选的,比如框一个目标区域,把这个区域的灰度数据存成模板。模板大小我常用32x32或者64x64,大小对资源占用影响很大,后面会细算。
模板数据在FPGA里的存储方式有讲究。最简单粗暴的做法是例化一个二维寄存器数组,模板多大就铺多大。32x32的模板就是1024个8-bit寄存器,64x64就是4096个寄存器。这个开销对Artix-7这种中等规模的芯片来说完全可以接受,但如果模板再大,比如128x128,就得考虑用BRAM分布式存储,代价是每个时钟周期只能读有限个像素,并行度下降,SAD计算吞吐率就会受影响。
我的推荐是:模板小于等于64x64,直接用寄存器阵列,把模板数据打平,让所有像素在同一拍内并行参与计算。模板数据在初始化阶段从外部接口(例如UART、SD卡或片上ROM)写入,之后就只读,不涉及动态更新问题。
图像数据这边,按行缓存的思想组织。假设窗口大小是W x W(我这里以32x32为例),图像宽度是IMG_W,那么需要例化W-1个行缓存,每个行缓存的深度是IMG_W。Vertically,每来一个新像素,所有行缓存里的数据整体下移一格,W行的数据同时就绪。水平方向再用一组W x W的寄存器阵列接收这W行数据的滑动窗口内容。
每一拍,也就是每个像素时钟,SAD计算单元拿到的是一个完整的、与模板同尺寸的图像块,这W平方个像素值和模板的W平方个像素值做绝对差求和。这一步在硬件上完全可以用并行减法器、绝对值电路和加法树在几个时钟周期内算完,而软件里这个过程等价于最内层的那两层循环。
2.3 归一化与灰度数据位宽的选择
SAD对光照变化比较敏感。如果目标变暗或者变亮,原始像素值整体偏移,绝对差就会变大,可能导致明明同一个目标却匹配不上。实际项目中我做了个简化版的归一化处理:对图像块和模板分别做均值剔除,也就是先把块内每个像素减去这个块的平均灰度,再计算SAD。这个操作在硬件里会增加不少加法器,因为每个窗口位置都要重新算均值。
如果项目对资源要求苛刻、光照环境又比较稳定,可以不加归一化。但如果目标会经过阴影区域或者光照会变化,还是建议加。我做过实测,同样场景下加归一化能把跟踪丢失率从30%左右压到5%以内,差价是大约多消耗2-3K个LUT和几个DSP,性价比很高。
灰度位宽方面,8-bit是底线。如果你用的是10-bit或12-bit的RAW图像传感器,建议先把数据截断到8-bit再做SAD。一来资源省,二来对匹配精度影响很小,三来系统简单好调试。我试过把12-bit全部保留,做出来的绝对值差电路面积大了一圈,效果却没有本质提升。
3. 核心计算模块的流水线设计:从像素进来到SAD值输出
3.1 三级流水线结构拆解
SAD计算单元是整个系统的数据通路核心。它接收来自窗口寄存器的W x W个图像像素和W x W个模板像素,输出一个SAD值。我采用的是一套三级流水线结构:
第一级,逐像素减法取绝对值。每个位置做一个abs(a - b)运算,这一级有W平方个减法器和W平方个绝对值逻辑,完全并行。如果W=32,就是1024路并行减法,这个规模FPGA完全扛得住。
第二级,加法树求和。把第一级的W平方个结果逐层两两相加。32x32的窗口需要10级加法树,64x64需要12级。加法树可以用LUT来实现,也可以用DSP48。如果芯片DSP资源充裕,用DSP48做加法器能省LUT,但要注意DSP48的级联延迟和布局布线约束。
第三级,结果寄存。加法树算出的SAD值打一拍寄存,然后送给后级的比较模块。
三级流水线意味着从像素进入窗口到对应位置的SAD值出来,一共有固定几拍的延迟。这个延迟在系统设计时必须算清楚,否则后级的状态机对不上数据的节奏。
3.2 窗口滑动机制与边界处理
硬件里的窗口滑动,本质上就是移位寄存器的节奏。每来一个有效像素,窗口寄存器阵列中每个寄存器把值传给右邻,新像素进入最左列。配合行缓存,这个二维"数据立方"就在图像上从左到右、从上到下平滑移动。
这里有个细节必须处理:边界。窗口滑到图像右边界或者底边界的时候,窗口有一部分会超出图像范围,没有有效像素。处理方案无非三种:补零、边缘复制、或者直接不输出该位置的SAD值。目标跟踪场景里目标一般不会贴着图像边缘出现,所以我选择了最简单的不输出无效位置,节省逻辑也省了后续比较器的判断复杂度。代价是搜索范围最外侧W/2个像素宽度的带状区域覆盖不到。如果一定要全覆盖,可以在图像周围做一圈像素填充,效果会好一些但逻辑量会上来。
3.3 帧同步与行同步的握手信号
图像数据一般伴随着三种同步信号:帧有效(frame valid)、行有效(line valid)、像素有效(data valid)。SAD模块必须严格遵循这三个信号的节奏。行缓存写入使能就是line valid && data valid,窗口移位同样只在有效像素到来时执行,否则会错位。
我踩过的一个坑是在某些测试平台上,行与行之间有空闲周期(行消隐),刚开始没处理这个,导致窗口里的行数据错行。正确的做法是line valid的下降沿把所有行缓存的写指针清零,窗口寄存器在无效周期保持不变。这样可以保证每行开始时,数据立方里各行都对应图像里同一列范围,逻辑上才正确。
再一个值得注意的点是模板初始化时机。模板数据必须在帧有效信号到来之前就全部就绪,否则第一帧的部分窗口会拿全零模板去匹配,产生一堆假的目标位置。系统上电后可以有个专门的初始化状态,等模板加载完成再启动跟踪流程,这样最稳妥。
4. 搜索策略与目标跟踪状态机的整体设计
4.1 全搜索 vs 稀疏搜索:实时性从哪来
SAD算法本身只解决"一个位置上像不像"的问题。目标跟踪要回答的是"目标在哪里",需要在搜索区域内找到SAD值最小的位置。最简单的思路是穷举搜索,也叫全搜索。搜索区域内的每个候选位置都算一遍SAD,然后取最小值。这个思路在软件里很常见,硬件里也能做,但要看搜索范围和参数之间的权衡。
全搜索的区域大小和窗口大小、图像大小直接挂钩。如果搜索区域是128x128,窗口是32x32,那么候选位置数量是(128-32+1)^2,大约9409个位置。每个位置算一次SAD需要1024次绝对值加法,算下来一个目标搜索就要接近千万次运算。虽然FPGA可以流水化连续做,但延迟会比较高,资源集中在比较器网络上,而且对帧率会造成压力。要做到实时处理,必须控制搜索范围。
工程上常用的折中方案是稀疏搜索。基于一个合理的假设:视频帧之间的时间间隔很短(比如16ms一帧),目标在相邻帧间的位移是有限的。也就是说,上一帧目标在位置A,下一帧目标大概率出现在A附近的一个小邻域内,比如±16或±32像素。那我就只在这个邻域里做全搜索,搜索范围大幅缩小,实时性自然就上来了。
我实际用的参数是:搜索邻域±16像素,窗口32x32。候选位置数从9000多降到1089个。SAD计算延迟可能会引入几拍的流水延迟,但整体处理时间已经远小于帧间隔,余量很充足。
4.2 跟踪状态机的状态迁移设计
目标跟踪不只是一个计算问题,更是一个时序逻辑问题。系统需要在视频流里持续运行,必须有一个跟踪状态机来管理"初始化-跟踪-丢失-重新初始化"的完整生命周期。
状态机我设计了四个状态,具体迁移条件和动作看下面这个表:
| 状态 | 进入条件 | 核心动作 | 退出条件 |
|---|---|---|---|
| IDLE | 系统复位 | 等待模板加载和外同步信号 | 模板加载完成 && 收到一帧有效图像 |
| SEARCH | 从IDLE进入 | 在搜索区域内输出SAD值,找全局最小值 | 找到最小SAD值且置信度达标 → TRACKING;最小SAD仍太大 → 回到IDLE重新初始化 |
| TRACKING | SEARCH完成后进入 | 以上一帧目标位置为中心,在±16像素邻域内搜索;计算SAD最小值和次小值比值做置信度 | 置信度低于阈值 → LOST;正常 → 继续跟踪并输出目标坐标 |
| LOST | TRACKING置信度过低 | 全图或者更大范围内搜索目标,尝试重新捕获 | 重新找到目标 → TRACKING;多次未找到 → 回到IDLE |
TRACKING状态里有个细节容易忽略:目标每帧位置都会更新,所以搜索中心也在移动。目标跑出搜索范围的风险是真实存在的,尤其是快速运动的物体。我的做法是保留两档搜索模式,跟踪稳定时用±16邻域,如果连续多帧置信度都在下降,就自动切换到±48的大范围搜索模式,不是直接判定丢失。实测下来目标快速转身或者短时间遮挡后重新出现,恢复率提升了不少。
4.3 置信度判断:单靠SAD值够不够
SAD值是一个绝对量,目标大小、光照、背景复杂度都会影响它的数值区间。单纯设定"SAD低于多少就认为匹配成功"很容易误判。我用的置信度判据有两个:
第一个是绝对阈值。SAD值必须低于T_abs,这个阈值一般通过若干帧的统计确定。当目标正常跟踪时,我记录SAD值的移动平均,取均值的1.5到2倍作为动态阈值上限。
第二个是次小值/最小值比值。在全搜索区域内,把SAD值排序,找到最小值d_min和次小值d_second(注意次小值的位置不能紧挨着最小值位置,否则可能属于同一个目标)。如果d_second / d_min > 1.3到1.5,说明最小值的匹配显著优秀,可信度高;如果这个比值接近1,说明搜索区域内有好几个位置长得都差不多,匹配结果不可信,应该判为丢失。
这个比例阈值我在不同类型视频上调试过多次,效果在大多数场景下都稳定。特别提示:目标纹理过于简单(比如纯色、无纹理区域)时,SAD值本身就体现不出差异性,次小值/最小值比值会趋近于1,这种情况不管怎么调阈值都难有本质改善,应该考虑换算法(比如NCC,归一化互相关),不过那就是另一个话题了。
5. 系统集成与平台的完整数据流:从摄像头到显示
5.1 完整系统组成
一个完整的FPGA目标跟踪系统不只是SAD模块,它还包括图像采集、预处理、叠加显示、以及和外部处理器通信等部分。我这里列一下我使用的整体架构:
- 图像输入:OV5640摄像头,YUV422格式转灰度,分辨率640x480,输出60fps
- 图像预处理:灰度转换、简单的3x3中值滤波降噪。中值滤波一般配一个小的行缓存,与SAD模块共用延迟链路的思路,可以复用行缓存资源
- SAD模板匹配模块:前文所述的窗口阵列、三级流水线SAD计算、比较器、置信度判断
- 目标状态机:管理跟踪生命周期,输出目标坐标
- 叠加显示:在HDMI或VGA输出上,用目标坐标数据画一个框,直观显示跟踪结果
- 外部通信:把目标坐标和跟踪状态通过UART/PCIe等接口上报给上位机,方便远程监控或者后续接云台控制
整个数据通路保持流式,摄像头像素经过预处理进入SAD模块,同时旁路一路经过叠加显示模块直接输出到屏幕。SAD模块算出的目标位置更新到显示模块的寄存器里,屏幕上的框位置自然跟着目标的移动而移动。
5.2 与STM32H743 FMC通信的集成方式
相关热搜里提到了STM32H743通过FMC与FPGA通信。这种异构架构在实际产品里很常见:FPGA负责实时视频处理,ARM负责上层逻辑、控制和人机交互。STM32H743的FMC总线本质上是并行存储器接口,可以映射到FPGA内部的双口RAM或者寄存器组,通信方式很直观。
我用过一种简单可行的做法:FPGA内部例化一个双口BRAM,一端挂在FMC总线的从设备接口上,另一端由跟踪状态机写入目标坐标和状态标志。STM32通过FMC读这个地址空间,就能拿到跟踪结果;也可以反过来写配置寄存器,比如更新模板数据、调整阈值、切换搜索模式等。这样分工明确,FPGA处理数据流,ARM做决策和展示。
跟FMC时序对齐的细节还是要强调一下:FMC读写的建立时间、保持时间、总线位宽、字节使能等参数,都要和FPGA侧的时序约束对上。建议先在FPGA里用逻辑分析仪抓一轮FMC读写波形,确认地址译码和读写操作时序没问题了再接上层应用代码,否则你很难分辨是链路问题还是上层逻辑问题。
5.3 Vivado工程要点与时序收敛
我在Vivado里的工程设置有几个关键点,比较容易踩坑:
时钟方面。图像处理链路最好使用独立的像素时钟域(比如OV5640的PCLK),SAD模块和显示模块都在这个时钟域下工作。FMC和UART再各自自己的时钟域,跨时钟域的地方用FIFO或者寄存器打拍同步。用异步FIFO做跨时钟域最稳妥,Xilinx的FIFO IP核配置比较简单,需要注意读侧和写侧的位宽可以不一致。不同位宽是因为读写速率差异,但转移的数据内容要保持一致,比如灰度像素8位,但读取侧可以是16位等。
时序约束方面。像素时钟如果是25MHz到75MHz之间,常规约束问题不大。但要把SAD模块的路径单独关注,因为窗口寄存器阵列很大,fanout很高,不加约束的话布局布线可能把关键路径拉得很长。建议给窗口阵列的输入加寄存器层,打一拍再进入减法器,可以显著改善fmax。我实际优化后,在Artix-7上SAD数据通路跑到了约180MHz,留出较大余量。
资源方面。32x32模板+SAD计算大致需要:行缓存占用BRAM若干块、窗口寄存器阵列+模板寄存器约2K个寄存器组、加法树用LUT大约8K,DSP约40到60个,整体资源在Artix-7 35T上大约占40%多。如果模板换成64x64,加法树面积大约翻4倍,行缓存深度不变但数量翻倍,资源会明显吃紧,需要仔细评估。
6. 实测数据、精度评估与调参经验
6.1 不同模板大小和搜索范围下的性能对比
我在室内视频、室外道路、车载拍摄三组数据上做了比较,下面是实际记录的数据(640x480@60fps,灰度):
| 测试场景 | 模板大小 | 搜索范围 | 正确跟踪率 | 平均处理时延(帧周期占比) | 资源占用(LUT) |
|---|---|---|---|---|---|
| 室内桌面物体 | 16x16 | ±16 | 86% | 约15% | 4.2K |
| 室内桌面物体 | 32x32 | ±16 | 95% | 约30% | 8.1K |
| 室外行人 | 32x32 | ±16 | 91% | 约30% | 8.3K |
| 室外行人 | 64x64 | ±32 | 96% | 约60% | 24.5K |
| 车载快速目标 | 32x32 | ±48 | 81% | 约50% | 8.6K |
结论很清楚:模板太小,辨识度差;模板太大,资源占用高、时延长。32x32是个不错的中间点。大规模搜索对快速目标有帮助,但实时性代价明显,需要和帧率做权衡。正确跟踪率指的是目标被连续正确框选超过200帧的测试序列比例,比如目标快速移动、短时遮挡、背景光照变化这些情况。
6.2 调参的三个核心经验
第一个经验是置信度阈值不是实测出来的,是统计出来的。先在场景A里跑一段,记录正常跟踪时SAD最小值的分布区间,以及目标丢失时SAD最小值的分布区间,然后取两者之间的分界值作为初始阈值。别拍脑袋定一个数字就开始用,不同摄像头、不同光照下,SAD的数值范围差得很大。
第二个经验是次小值/最小值比值比绝对阈值更可靠。绝对阈值受光照和噪声影响波动大,比值判据更接近一个"形状相似度"概念,具备更强的环境适应性。
第三个经验是跟踪丢失后不要立即放弃。我遇到的很多"丢失"其实只是目标被遮挡了三四帧。丢了之后判断一次,如果LOST状态里SAD_min又变得很小,说明目标重新出现了,应该继续锁定而不是重新初始化。否则每遮挡一次就重新框选一次模板,会让用户体验变得非常糟。
6.3 时序收敛与逻辑分析仪调试记录
板上调试阶段我用了几种手段。第一是ILA(集成逻辑分析仪),抓窗口像素、行缓存输出、SAD输出变化、比较器跳变等信号,判断流水线节奏是否正确。ILA在调试初期很有价值,但注意ILA会占用大量的BRAM和布线资源,条件允许的话调试完毕就去掉,不要留在最终版本里。
第二是Vivado的VIO,虚拟IO,可以在线修改置信度阈值、搜索范围大小、模板更新指令等整型参数。有了VIO,调参不用反复综合,直接改寄存器值看效果。这个工具我几乎每个图像处理项目都会用,强烈建议你掌握。
还有一个调试技巧:不要一上来就盯SAD模块的输出是否正确,而是先验证行缓存和窗口传输链路的时序。可以在测试模式下输入特定方向的斜纹图案(比如从左上到右下逐步递增的灰阶),观察窗口阵列里每行数据的相对位置是否和预期一致。如果这个验证通过了,SAD计算模块只要单独用测试向量验证一遍,系统的正确性就比较有把握了。
7. 资源优化与进阶扩展方向
7.1 资源紧张时的降配方案
如果你的目标平台逻辑资源比较紧张,有几个方向可以压缩。一是模板缩小,比如从32x32降到24x24或16x16,虽然模板匹配精度有所下降,但如果目标纹理比较丰富且场景变化不剧烈,效果能接受。二是把绝对差求和改成"差分符号"的粗粒度匹配,先粗筛掉大部分明显不匹配的候选位置,再对少量候选做完整SAD计算,两段式流水线能省不少加法树资源。三是用DSP48替代LUT做加法树,如果芯片的DSP资源富余的话。
7.2 多目标跟踪的可能性
SAD模板匹配天然是一个模板对应一个目标。要做多目标跟踪,思路一般是例化多个SAD匹配引擎,每个引擎负责一个模板,引擎之间做目标位置去重。比如两辆车重叠或者靠近时,两个引擎可能匹配到同一个目标,需要一个仲裁模块来处理冲突。资源开销基本是线性增加的,做4到8个目标在Artix-7级别的芯片上是可以考虑的。
7.3 升级到NCC或相关算法的思路
SAD的弱点在前面提到过:对光照变化敏感。如果项目长期跑在室内固定光照下,SAD完全够用。但户外场景光照总在变,升级到NCC(归一化互相关)是个自然的演进路线。NCC的计算里包含均值、方差、协方差,硬件实现会多一些乘除法器,但FPGA里乘法用DSP做并不算太难。如果你做视觉的项目,可以考虑在SAD模块基础上扩展一个NCC模式。接口设计时可以把窗口阵列和行缓存共享,只是把后段计算替换掉,这样资源增加不多,功能覆盖却宽很多。
模板的在线更新也需要考虑。固定模板在目标外观变化较大时容易失效,适当的策略是每隔N帧用当前帧跟踪窗口内的图像块替换模板,或者与旧模板做加权平均。这个功能会增加少量BRAM开销和一点控制逻辑,但对长期跟踪稳定性提升非常明显。我做室外行人的那次测试,用了在线更新模板后,跟踪成功率从91%提升到97%,效果立竿见影。当然在线更新也有风险:如果某帧匹配错误,错误区域被更新进模板,错误会被延续放大,所以更新必须仅在置信度高的时候进行。
8. 写在最后的工程建议
项目做到后面,越发觉得FPGA图像处理项目的核心其实是"权衡"。SAD算法的硬件实现,难点不在于SAD本身,而在于整个系统层面的节奏设计:数据流的同步、延迟的对齐、资源与时序的平衡、状态机与实时性的配合。把这些想清楚了,SAD只是整个链路上一个相对简单的计算节点。
给正在做或者准备做类似项目的人几条实际建议:
模板匹配里的"模板"从哪来很关键。我建议先在PC端用Python或MATLAB预演一遍算法流程,确认你的应用场景下SAD+搜索策略能work,再移植到FPGA。硬件实现之前先用软件扫一遍参数空间,能省掉大量上板调试时间。这不算多此一举,因为很多问题其实是算法层面的问题而不是硬件层面的。
另一个是仿真环境要建好。Vivado的仿真虽然慢,但SAD模块本身规模不大,把窗口激励和模板激励喂进去,检查SAD输出是否符合手算结果,这一步最好在综合前完成。我写过一个小的Python脚本生成随机窗口激励值,然后把期望SAD值导出成coe文件或文本,直接喂给testbench比对输出。基本一次能把加法树和流水线逻辑bug扫干净,比上板之后用ILA盲猜高效太多。
最后是关于调试心态的。FPGA图像处理链路长、信号多,一旦出问题很难一眼定位。但只要你把数据流画清楚,把同步信号跟对,把每个模块的有效信号、数据信号、帧同步信号理清楚,绝大多数问题都能在几分钟内定位。耐心、有条理、按层级排查,这是FPGA工程师最值钱的能力。