拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FPGA与SAD模板匹配:嵌入式实时目标跟踪的实现方案

前阵子我接了一个实时视频小项目要求在一台没有GPU的嵌入式平台上做目标跟踪。一开始想得很简单模板匹配嘛拿OpenCV在ARM上滑窗算一遍SAD就行。结果真跑起来才发现到了720p分辨率、目标窗口32×32还要按30fps实时处理ARM单核直接吃紧。后来我干脆把匹配引擎整体挪到FPGA里做外面只留坐标输出整个链路才算真正跑稳。这篇文章就围绕“FPGA SAD模板匹配”这套组合展开把方案选型、模块划分、SAD引擎硬件实现、帧间跟踪逻辑、仿真验证和踩坑记录都摊开讲。适合正在做FPGA图像处理、目标跟踪或者想把手上的软件算法往硬件迁移的工程师参考。FPGA新手也不用担心我会把从公式到电路的基本逻辑讲清楚你照着这个框架做第一版踩的坑会少很多。1. 为什么是FPGASAD方案选型背后的老实话1.1 模板匹配在目标跟踪里的定位先说目标跟踪。很多人一听到“目标跟踪”就想到卡尔曼滤波、相关滤波甚至是深度学习目标检测。但在这个项目里需求其实收敛得很小第一帧我手动或由检测模块给出一个目标框后续每一帧都要告诉上位机“目标现在在哪个像素位置”。这种场景下模板匹配是最直接、最稳定的起点尤其是目标本身纹理较明显、尺度变化不大时SAD模板匹配足以撑起一套有效的跟踪闭环。模板匹配做的事可以理解成“拼图找位置”我手里有一小块模板图例如32×32的目标灰度图然后把这个小图贴到当前帧的大图上贴着滑过每一个可能的位置计算模板和当前位置下图像块的相似度。最像的那个位置就是目标当前所在位置。SAD就是衡量“像不像”的指标之一。在工程落地时真正的问题不是算法原理而是计算量。以720p图像为例若模板是32×32逐点全图搜索会产生上百万个候选位置每个位置还要做1024次像素级运算。这个计算体量放在CPU上单帧耗时轻松超过几十毫秒实时性自然无从谈起。1.2 SAD为什么比NCC、SSD更适配FPGA图像相似度指标有很多SAD、SSD、NCC都常见。但放进FPGA再看差异就非常明显。SAD全称是Sum of Absolute Differences公式很简洁把模板与搜索图像块逐像素相减取绝对值再全部加总。整个运算只需要减法、取绝对值和加法没有任何乘法更没有除法。FPGA做这类运算时可以摆出大量并行减法器中间用加法树逐级合并逻辑资源消耗非常可控。SSD虽然只比SAD多了一步平方但平方意味着乘法8bit像素平方后数据位宽需要扩展到16bit甚至更宽。加法树中所有中间结果都会变宽不仅更占LUT时序也容易绷紧。而且SAD和SSD的排序结果基本一致既然目标只是找最小值没必要多花资源做平方。NCC归一化互相关对光照鲁棒性确实更好但它的公式里带均值、方差和除法硬件里做除法是一件很吃亏的事。除非目标区域光照变化极其剧烈否则我不建议第一版就在FPGA里硬磕NCC。真遇到光照变化更实用的做法是后面我会提到的“边缘图SAD”或“均值预补偿”保留SAD的硬件优势同时规避光照敏感问题。相似度指标主要运算FPGA硬件代价光照鲁棒性SAD减法、取绝对值、加法低无乘法一般SSD减法、平方、加法中高需要乘法器一般NCC均值、方差、除法高流水级数长较好1.3 FPGA赢在“计算空间展开”CPU是串行执行指令的即使是多核每个SAD候选位置之间的数据依赖性也让它很难把所有候选位置同时展开。FPGA完全不同它是空间计算你可以在芯片里同时放几十路、几百路流水让不同搜索位置的运算同时进行。举个例子模板宽度是32那么我可以把当前窗口一行的32个像素和模板一行的32个像素在同一拍里全部送入32个减法器并行算出32个绝对差。下一拍通过加法树把32个结果归并成一个行和。如果再把模板的32行展开一个SAD候选点理论上只需要几拍流水延迟就能出结果而非CPU那样做1024次循环。但全并行也不是无代价的。32×32全展开意味着1024路绝对差电路同时存在这在入门级FPGA上资源会比较紧张。实际工程一般会取折中方案行方向并行度拉满列方向通过累加器多次复用或者同时处理两三个候选SAD。这套思路在后面的SAD引擎实现里会展开细讲。2. 系统级架构从视频流到目标坐标2.1 板卡级数据链路怎么搭实际项目的FPGA板卡一般包含CMOS摄像头或HDMI输入、DDR3/DDR4颗粒、HDMI输出接口。图像数据从传感器进来后通常不是直接给SAD引擎用的中间需要经过几个标准环节。我的数据链路是这样的摄像头输出RAW Bayer或RGB格式先做去马赛克和色彩校正然后转成灰度图。SAD用单通道灰度就足够三通道彩色既浪费BRAM也浪费DDR带宽。灰度转换公式用经典的Y 0.299R 0.587G 0.114BFPGA里固定系数后变成Y (77R 150G 29B) 8乘法和移位就能实现。灰度流随后兵分两路一路进显示通路直接叠加目标框后上屏另一路写入DDR帧存用于后续SAD引擎回读。之所以要经过DDR是因为SAD搜索需要随机访问搜索区域内的任意像素而行流是顺序的不能保证模板和当前搜索窗口在时间上对齐。把整帧先存入DDR再让SAD引擎按搜索轨迹发读请求是最稳妥的做法。这里也提醒一句如果摄像头分辨率低例如640×480其实可以不用DDR用片上Block RAM缓存两帧都行。但既然项目要求720p且后续要放大分辨率DDR就绕不开了。整个系统按功能划分成几个明确的模块图像接收与预处理MIPI/HDMI接收、灰度转换、目标框叠加。DDR存储与仲裁写帧缓存、SAD引擎读请求、显示回读。模板管理单元模板初始化、模板RAM读写、模板更新。SAD匹配引擎核心计算阵列产生最佳匹配坐标。跟踪控制器生成搜索区域ROI、判决是否跟丢、维护帧间状态。显示叠加模块把跟踪结果坐标画框输出。2.2 模板放在哪搜索图又放在哪模板图尺寸固定常见16×16到64×64。以32×32模板、8bit灰度为例数据量只有1KB完全适合放在FPGA内部RAM里。一般用BRAM实现双口RAM一个口给初始化或更新写入另一个口给SAD引擎连续读出。条件允许时最好把每一行模板单独放到一个BRAM block中这样SAD引擎可以同时读取32行数据避免逐个像素串行读取造成等待。搜索图的数据体量则大得多。如果每次都要从DDR把一整张搜索ROI读出来存进BRAM再开始匹配读带宽会白白浪费。更高效的办法是SAD引擎只维护一个“滑动窗口缓存”窗口宽度等于模板宽度高度等于模板高度每次向右移动一列时只需补充最右侧新进入的像素列最左侧一列直接丢弃。为支撑这个滑动窗口FPGA内部需要一组行缓存。若模板大小为T×T行缓存数量是T每个行缓存的深度等于ROI一行像素数。理论上1行720p也就1280×8bit约10Kb32个行缓存总容量大约320Kb很多中端FPGA都能承受。当然如果ROI取得更大可以先在DDR里只锁定一个较小的矩形搜索区把该区域搬进BRAM后再做行缓存这样资源会更宽裕。2.3 搜索区域策略全图搜索不现实许多人第一次写模板匹配时都是天真地全图滑窗。这个方案在FPGA里同样不现实原因不是算法不能做而是资源和速度会成倍恶化。我们按下表粗略算笔账模板尺寸搜索ROI候选点数每候选点节拍数150MHz下估算耗时32×32128×1289409约33约2ms32×32256×25650625约33约11ms64×64256×25637249约65约16ms同样模板尺寸ROI从128×128扩到256×256候选点从9409跳到50625计算量涨了5倍以上。所以目标跟踪场景下一定要利用上一帧的位置下一帧不再全图搜索只在上一帧目标位置附近裁剪一个搜索区域例如原坐标向上下左右各扩展64像素作为本次SAD的ROI。这是模板匹配能追上实时帧率的核心前提。ROI选多大取决于目标最大运动速度和帧率。假设帧率30fps目标每帧最多移动20像素ROI余量给到两倍以上就很安全。扩大ROI虽然能提升捕获快速目标的概率但也会引入更多局部极值匹配稳定性未必更好。除此之外还有一个细节ROI在图像边界处会自动缩小因为搜索窗口不能越过图像边缘。跟踪控制器在设定ROI时要把坐标钳位在合法范围内否则SAD引擎读DDR时会出现越界地址轻则匹配结果异常重则导致AXI总线挂死。3. SAD引擎核心窗口滑动与加法树实现3.1 滑动窗口的数据流行缓存是关键SAD引擎每到一个新位置就要拿到以该位置为左上角的T×T图像块。如果每个候选位置都从DDR重新读一遍DDR带宽会被读爆。滑动窗口的优势在于窗口向右移动一列时T行像素中每行只有最右侧的一个像素是新数据其余T×(T-1)个像素全部是上一拍刚用过的旧数据。用行缓存实现这个流程时我维护T个行缓存每个行缓存对应ROI中的一行。当前窗口最右侧像素进入行缓存后控制器从缓存中取出T行、每行T个像素组成一个与模板同尺寸的并行数据块送入计算阵列。窗口每右移一列判断是否到了一行末尾如果到了末尾则回到行首并让下一行像素进入缓存同时丢弃最旧的行数据。实际编码中行缓存可以用FPGA的移位寄存器资源或BRAM实现。如果把ROI先完整锁存在片上RAM里也可以给每行建一个双口RAM用读地址控制窗口位置。我更推荐后者因为ROI不大时实现简单调试时也能直接看到窗口地址变化规律。3.2 绝对差计算与加法树流水SAD计算阵列是引擎的心脏。每个像素对做一个绝对值减法这一步完全没有跨行依赖可以全部并行。假设我选择行并行度T32同一拍产生32个绝对差值。下面这行代码是核心逻辑genvar i; generate for (i 0; i 32; i i 1) begin : gen_abs_diff wire [7:0] diff (search_pixel[i] tpl_pixel[i]) ? (search_pixel[i] - tpl_pixel[i]) : (tpl_pixel[i] - search_pixel[i]); // diff 送入下一级加法树 abs_diff[i] diff; end endgenerate这里要注意取绝对值如果用三元判断综合后通常是一个比较器加两个减法器加选择器逻辑层级还好。但如果像素位宽再高也可以考虑直接利用减法器的借位信号做条件选择。综合工具通常能优化好不必过度手工优化。得到32个绝对差值后接着要做加法树。最简单的写法是always块里for循环顺序累加但综合后会变成一个31级深的长链加法器时序必然爆炸。正确做法是做成树形归并每两个数相加后打一拍逐级减少加法器数量。32个数只需5级加法器第1级16个加法器把第2i和第2i1个数相加 第2级8个加法器 第3级4个 第4级2个 第5级1个得到行和。每级之间都插入寄存器形成完整流水线。这样处理后的时钟频率能跑得比较高我在150MHz下没有遇到时序收敛问题。若在更低端的芯片上跑还可以把行并行度从32降到16代价是行和需要两次累加。加法树算出的只是当前模板行的行和。一个SAD候选点需要把T行的行和再相加这部分我会用一个单独的累加器按行数循环累加。每算完新的一行把行和累加进sad_acc等模板所有行处理完毕后把sad_acc与当前最佳值best_sad比较。若更小则更新最佳值和对应坐标。3.3 状态机与搜索顺序SAD引擎的搜索过程由一个简单的状态机控制分为几个阶段IDLE等待帧开始、ROI加载、逐行扫描、结果输出。搜索顺序不必和像素扫描顺序完全一致但按行优先扫描最容易实现也方便维护行缓存地址。我用的状态转移大致是这样帧同步来到后引擎进入MATCH状态从ROI左上角开始计算一个候选点的SAD然后窗口右移一列继续下一个候选点。当窗口到达ROI右边界时回到ROI最左侧并下移一行。直到遍历完ROI内所有位置引擎拉高done信号输出best_x和best_y回到IDLE等待下一帧。这里有一个工程上常见的坑窗口在每行开始位置时行缓存需要预填充T个像素状态机如果处理不好预取时机会导致第一列候选点的数据不完整。我最早调试时匹配结果整体向右偏了十几像素查了一整天才发现是预取逻辑少了一个时钟周期。建议在仿真阶段就对每一行第一个候选点的数据有效性做断言检查不要等到上板后再看视频流找问题。由于每个候选点需要按模板行数循环多次引擎输出的SAD并不是每周期一个。如果帧率要求高可以把引擎改成多实例并行让两个或四个候选点同时进入不同计算单元它们互不干扰最终比较单元统一取最小。这种“多个计算阵列 一个裁决逻辑”的结构是FPGA上扩展SAD吞吐量的标准做法。4. 跟踪逻辑别让匹配停留在单帧4.1 帧间ROI跟随与坐标处理SAD引擎本身只回答“这一帧里目标在哪”它不会管上一帧在哪。把单帧匹配变成连续跟踪要靠外部跟踪控制器把状态传下去。最简单的策略是上一帧匹配到的坐标记作last_x和last_y下一帧的搜索ROI中心就是(last_x, last_y)ROI半宽设置为目标运动最大像素距离加上模板自身一半的宽度。比如目标最大每帧移动15像素模板32×32ROI只需从上一帧坐标往外扩50像素左右128×128足够应付大部分运动场景。如果目标是突然变速或高速运动ROI不够大就会跟丢。我建议写一个自适应ROI模块当连续多帧匹配得分都很理想时说明目标运动平稳ROI可以适当减小以节省计算时间当匹配得分突然变差或者目标位置跳变距离很大时立刻把ROI放大到原来的两倍并重新搜索。这个逻辑有点像网络协议里的拥塞窗口控制实际效果很稳。坐标输出时还要考虑整条处理链路的延迟。SAD引擎匹配的是DDR里的历史帧而不是当前正在显示的帧所以坐标输出必须打上对应的帧号或时间戳。如果直接把坐标叠加到当前显示帧上目标框会明显滞后一拍。工程上一般把DDR读回的帧和SAD结果坐标绑定在显示通路中按帧号对齐后再叠加这样观感才正常。4.2 模板更新不更新会漂乱更新会丢固定模板永远匹配不到形变后的目标。打个比方目标从远处走近尺度会变大一个固定的32×32模板不可能一直很好地描述它。因此跟踪过程中必须周期性更新模板。但模板更新是一个双刃剑。如果更新得太激进一旦匹配位置偏差了几个像素模板会把背景也学进去慢慢漂移到背景纹理上。如果完全锁住不更新目标一旦改变姿态或光照SAD值会持续变大最终跟丢。我的做法是双保险。第一匹配成功后从当前帧最佳位置周围裁剪比原模板稍小的中心区域作为“候选更新模板”比如原模板32×32我裁中间24×24。只取中心区域可以有效防止边框上的背景像素进入模板。第二不是直接把候选模板覆盖旧模板而是做加权融合new_tpl (1 - alpha) * old_tpl alpha * candidatealpha取0.1到0.3之间。硬件里这个公式需要一次性读旧模板和候选模板的全部像素然后并行做乘加运算。alpha预先转成定点小数例如0.25这样乘法退化成移位降低成本。不管用哪种策略都不要每帧都更新模板。我建议连续5到10帧中只选匹配效果最好的那一帧来做模板刷新并且刷新前校验最佳SAD值确实低于阈值。这样即使某一帧匹配抖动严重也不会立刻污染模板。4.3 SAD阈值、遮挡与丢失判定纯SAD结果的绝对值大小和图像内容强相关不能用同一个固定阈值判断所有场景。一个简单的归一化方法是把SAD除以模板像素的能量或方差。模板越平坦哪怕完全不匹配SAD也可能很小模板纹理越强匹配良好时SAD才能压得很低。实际项目里我是在跟踪控制器里维护一个动态阈值模板刚初始化时强制做几帧匹配记录匹配良好时的SAD平均值再留出1.5~2倍余量作为丢失阈值。当当前帧最佳SAD超过这个阈值时不输出新的跟踪位置也不更新模板连续超过N帧则向外部上报“目标丢失”。目标被短暂遮挡时最怕的就是把遮挡物当成新目标。所以一旦SAD超阈值进入“疑似遮挡”状态我让ROI暂时保持在丢失前的位置同时略微扩大ROI观察后续帧能否恢复匹配。这样做能抗住一两秒的短时遮挡直到目标真的离开该区域再宣告丢失。5. 仿真、上板验证与踩坑记录5.1 怎么搭一个有效的仿真测试环境FPGA算法工程不能等上板再验证标准做法是先做仿真。第一步不是仿真整个SAD引擎而是先写一个行为级参考模型在PC端用Python或MATLAB读图像序列用同样的SAD流程逐帧计算最佳位置把这组坐标存成文本作为FPGA输出的“金标准”。第二步生成FPGA仿真激励。做法很简单把连续几帧灰度图像转换成HEX或BMP序列Testbench按像素时钟把数据喂给图像接收接口。SAD引擎跑完后用$display打印最佳坐标再用脚本比对FPGA结果与PC参考结果是否一致。我踩过的第一个坑是仿真激励里图像的坐标系统和Verilog数组索引不一致。图像左上角是(0,0)但FPGA内部为了DDR对齐有时会预留几行几列做同步消隐导致仿真输出的坐标始终比参考坐标偏移一个固定量。解决方法是统一在Testbench入口就把行场同步对应的blank区域去掉只让有效像素进入SAD引擎。如果想让仿真更贴近真实可以往图像里注入高斯噪声、模拟曝光跳变甚至让目标在序列中间被遮挡几帧。这样能在上板前就验证模板更新和丢失判定逻辑是否正确。5.2 资源估算与实测结果以720p30fps、模板32×32、ROI128×128为例我在一块中端Artix-7级别FPGA上实测SAD引擎加上周围图像链路逻辑LUT消耗大概在20K左右FF约15KBRAM主要消耗在行缓存和帧缓冲控制约占总量三到四成。如果你的目标是低端型号例如资源更少的Cyclone IV或国产同等级芯片建议把模板并行度降为16并且ROI控制在96×96以内。这样逻辑量能降到10K LUT上下性能依然足够720p下实时搜索。时序方面需要重点约束的是DDR读数据接口到SAD计算阵列的组合逻辑路径。DDR读出的像素数据经过跨时钟域FIFO后直接送进绝对差逻辑如果这一级组合路径过长很容易出现时序违例。我在关键路径上插了两级寄存器后150MHz时序余量就非常健康了。真正实测时SAD引擎处理一帧搜索ROI的时间大约是2ms到3ms远小于33ms的帧周期。这意味着系统还有大量资源余量可以做后续拓展例如把模板增大到64×64或者增加一路并行SAD引擎以支持多目标跟踪。5.3 实战中常见的坑位速查现象原因解决思路输出坐标整体偏移固定像素行缓存预填充时序不对窗口数据滞后检查每行起始位置的数据有效标志增加预取节拍对齐SAD值很小但目标框乱跳搜索区域纹理太弱如纯色墙初始化前计算模板方差纹理不足时拒绝启动跟踪目标一加速就丢失ROI设置过小根据历史帧运动速度估算最大位移动态扩大ROI跑几秒后模板漂到背景上模板更新误学了背景改为中心区域更新降低更新频率增加SAD阈值校验上板后偶发坐标跳变DDR读数据未对齐或跨时钟域处理不到位检查异步FIFO深度确认地址对齐必要时增加CRC校验静态时序分析通不过加法树长链或读数据到abs逻辑过深加法树逐级打拍关键路径插寄存器流水我印象最深的是目标被短暂遮挡后“认新不认旧”的问题。刚开始我以为只要SAD值够小就是匹配上了结果目标从遮挡物后面出来时框却钉在了遮挡物上。后来加入“疑似遮挡状态”和“动态阈值判决”后系统才真正具备抗遮挡能力。这类逻辑问题在仿真里注入遮挡帧就能提前暴露千万别只拿理想视频测。另外一个容易被忽略的坑是图像过亮或过暗时边缘对比度下降像素差全部趋于0SAD区分度很低。这种情况下即使是正确位置SAD也不一定是最小值因为整片区域到处都很“像”。我在预处理通路里额外加了一个sobel边缘计算选项启用后SAD引擎匹配的对象从灰度图变成边缘图光照变化的影响明显小很多。付出的代价是预处理阶段多一组卷积计算但对FPGA来说sobel 3×3几乎不占资源。如果项目对光照鲁棒性有硬要求建议第一版就把边缘图SAD考虑进去。这套项目做完后我最大的体会是算法选型和硬件实现常常是两套评价体系。SAD在软件算法里显得朴素但在FPGA里它的规则运算、无乘法、可并行特性让它变成一种几乎为硬件而生的匹配算法。目标跟踪的真正难点也不止是“找到最像的位置”而是处理好模板生命周期里遇到的各种现实问题。把帧间ROI、模板更新、遮挡判定这些外围逻辑做扎实比单纯把SAD计算单元做得更快更有价值。希望这篇实战分享能帮你少走几步弯路也欢迎你按自己的平台和场景调整参数把这套框架跑起来之后再回头看很多细节会比文字描述更清晰。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门