资讯动态

FPGA实时目标跟踪:基于SAD模板匹配的从RTL到板卡实现全解析

发布时间:2026/9/8 13:52:17 来源:尧图企业网站定制
把基于FPGA的SAD模板匹配目标跟踪方案从算法验证到RTL实现、再到板卡实测完整走了一遍过程中踩了不少坑也沉淀了不少心得。这篇文章就把整个设计过程拆开聊一聊——FPGA、SAD、模板匹配、目标跟踪这四个关键词如何组合成一个可落地的实时系统以及每一环我们在选型和实现上是怎么权衡的。先一句话说清楚这个项目解决什么问题给定一段视频流在第一帧框选一个目标区域作为模板后续每一帧在上一帧目标位置附近的搜索区域内用模板逐点滑动匹配通过SAD值找出最相似的位置输出目标的新坐标。用模板匹配做目标跟踪在工业检测、无人机视觉、智能监控这些场景非常常见而FPGA做这件事的核心价值在于极低的处理延迟和完全可控的实时性——图像不需要传回上位机再等结果板卡内部就能完成采集、匹配、坐标输出的完整闭环。这篇文章适合有基础FPGA开发经验、想往图像处理方向深入的同学也适合正在规划实时跟踪方案、需要评估SAD这条路线可行性的工程师。如果你是刚接触FPGA开头几节涉及的概念比较多可以先跳着看如果你已经把视频采集和显示通路跑通了那直接跳到算法核心架构和调试部分那里有能直接抄作业的细节。1. 模板匹配在FPGA上落地前先想清楚这几个问题1.1 目标跟踪的算法选型为什么偏偏是SAD目标跟踪的算法候选池其实挺大的。软件上跑的话光流法、均值漂移、卡尔曼滤波加检测、甚至深度学习目标跟踪都能做各有各的适用范围。但一旦限定在FPGA上做实时处理选择范围立刻缩小了很多。一开始我也在SAD、SSD、NCC这三者之间犹豫过。先给结论在FPGA这种以并行计算和流水线为核心优势的平台上SADSum of Absolute Differences绝对差值和是性价比最高的匹配准则。它的计算公式长这样SAD Σ|I_template(i,j) - I_search(xi, yj)|对搜索区域里的每一个候选位置把模板图和对应位置的像素逐个做差、取绝对值、累加求和。整个计算过程只有减法、绝对值、加法三种运算。没有乘法、没有除法、没有开方、没有浮点。对比一下其他两个方案就有感觉了SSD虽然也是纯加法结构但平方项意味着每个像素对都要额外消耗DSP切片或者大查找表NCC就更麻烦均值、方差、归一化除法任何一个在硬件上都是“重武器”。FPGA上做一个整数除法通常要排几十级流水线延迟在实时视频这种对延迟敏感的场景里能用加减法解决的事情尽量不要引入额外开销。SAD另一个被很多人忽略的优势是数据位宽极好控制。图像是8 bit灰度图两个像素相减的结果范围是-255到255取绝对值之后就是0到255多路累加之后按模板尺寸扩展位宽即可。整个过程天然定点不需要做任何浮点对齐和舍入处理。在硬件上这意味着你可以把数据通路做得非常干净时钟频率能拉得更高逻辑资源也省下一大截。1.2 SAD的计算流程与数据流拆解把SAD算法放到目标跟踪的框架里看完整流程是这样的。第一帧由用户或者上位机选定目标把目标区域的灰度数据裁剪出来存成模板从第二帧开始以上一帧目标中心为基准划定一个搜索区域模板在这个区域内从左上角开始逐行、逐列滑动每滑动到一个位置就计算一次SAD值遍历完整个搜索区域后SAD值最小的位置就是当前帧目标最可能出现的地方。用伪代码表达就是这样for dy -R to R: for dx -R to R: sad 0 for j 0 to N-1: for i 0 to N-1: sad abs(template[j][i] - search_frame[y0 dy j][x0 dx i]) if sad min_sad: min_sad sad best_dx dx best_dy dy注意这个计算量的结构很有意思。搜索区域大小是(2R1)^2模板大小是N^2所以一帧的总计算量约等于(2R1)^2 × N^2次减法、绝对值和累加。这里面的优化空间非常大——候选位置之间是天然独立的可以并行每个候选位置内部的N^2个像素对也是天然独立的同样可以并行。两级并行度叠加起来正是FPGA最擅长处理的模式。这也是后来整个RTL架构设计的原点。2. 系统整体架构与关键参数测算2.1 顶层架构从像素流到目标坐标整个系统在板卡上的数据通路可以分成采集、处理、显示三段。以我验证用的720p60输入为例大致是这样一条链路HDMI或MIPI摄像头输入 → Bayer/RGB转灰度 → 搜索区域行缓存 → SAD计算阵列 → 最小值搜索与坐标更新 → 目标区域OSD叠加 → HDMI输出前端灰度化很简单无论是RGB转灰度还是Bayer插值后转灰度本质上都是加权求和用几个乘加器就能搞定。真正决定系统性能的是搜索区域行缓存和SAD计算阵列这两块。我需要说明一个设计选择这里没有用完整的帧缓存。很多第一次做图像算法的同学习惯把整帧图存入DDR再慢慢处理这样逻辑简单但延迟高、带宽压力大而且BRAM消耗很大。实际做下来行缓存方案要优雅得多——只需要缓存搜索区域对应高度的若干行数据数据边到边运算延迟只有几十行的量级BRAM占用也小一个数量级。后面的SAD计算阵列我采用的是“多路并行候选位置单路像素流水线”的结构。也就是同时让多个候选位置的计算单元工作每个单元内部串行处理模板像素最后通过比较器树选出最小值。这种折中方案在资源消耗和实时性之间取得了很好的平衡。2.2 关键参数测算模板尺寸、搜索范围与并行度参数怎么定不能拍脑袋。拿一个典型场景算一遍你就知道每一步该怎么取舍了。假设输入是720p60灰度图模板大小16×16搜索范围±32像素。那么搜索区域候选位置数 (2×32 1)^2 4225个每个候选位置需要计算16×16 256个像素对的绝对差累加单帧总计算量 4225 × 256 ≈ 108万次运算60fps下每秒约6490万次运算这个量级对FPGA来说压力不大。以150MHz工作频率为例即使完全串行执行每个时钟周期完成一次像素对运算150MHz每秒可以处理1.5亿次已经覆盖需求。但实际系统里我们还要考虑像素数据到达的节奏以及搜索窗口建起来的时间所以需要一定的并行度。我最终选择的是4路候选位置并行。每个SAD单元每时钟周期处理一个像素对4路就是每周期处理4个像素对那么一个候选位置需要256个周期完成4路并行下每秒可以完成约234万个候选位置的计算远大于4225×60≈25万个候选位置/秒的需求。也就是说计算资源有约9倍的余量这为后面模板更新、多目标扩展留了空间。模板尺寸的选择也要结合目标实际大小。16×16在720p画面中大概对应一个中等偏小的目标如果目标只有几个像素大SAD匹配会因为缺乏纹理信息而失效反过来如果模板太大目标一旦发生微小形变或者旋转匹配误差也会急剧增大。一般经验是模板尺寸覆盖目标主体面积的60%以上并且包含一定的纹理细节。这个比例在大多数场景下都能取得较好的效果。2.3 资源评估动手前先算清BRAM和LUT资源评估是判断方案可行性的关键一关。直接说数字。如果采用行缓存方案搜索区域需要缓存的高度是搜索范围上下的扩展加上模板高度的一半左右。以±32搜索范围、16×16模板为例需要缓存(32 16 32) 80行数据每行宽度1280像素、8 bit灰度。总存储量80 × 1280 × 8 819200 bit ≈ 100KBXilinx 7系列单片BRAM是36Kb所以大约需要23片BRAM。对比整帧缓存方案一帧720p灰度是1280×720×8 ≈ 7.2Mb需要200片BRAM差距非常明显。这里行缓存的优势就体现出来了。LUT的消耗主要来自SAD计算阵列。单路SAD单元实现256次累加有多种方式最简单的方案是每个时钟周期处理一个像素对用1个减法器、1个取绝对值逻辑、1个累加器总共大概几十个LUT如果要在一个周期内算出16个像素对的累加就要用到16输入加法树LUT消耗会涨到几百个。4路并行、每路单像素流水线再加上最小值比较器树总体LUT消耗可以控制在3000以内在主流FPGA上非常轻松。3. RTL核心模块实现与流水线设计3.1 搜索区域缓存行缓存与窗口滑动的设计在前面的架构里搜索区域缓存是系统的数据源头设计好坏直接影响后续模块的时序和资源。这里我采用了一个典型的双端口BRAM行缓存结构写端口持续接收实时视频流读端口按窗口需求输出搜索区域数据。实现上行缓存的地址管理用一个按行号回绕的计数器即可。每一行数据写入时地址从0递增到行宽-1然后换到下一行起始地址继续写。因为搜索区域窗口总是逐行向下滑动更新窗口时只需要丢弃最旧的一行、把新到达的一行写入缓存窗口内其他数据不变。这样BRAM的读写带宽需求只有两倍像素时钟非常充裕。// 搜索区域行缓存模块缓存高度为 SEARCH_ROWS module search_area_cache #( parameter DATA_WIDTH 8, parameter LINE_WIDTH 1280, parameter ROWS 80, parameter ADDR_WIDTH 16 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] din, input wire wr_en, input wire [ROWS_LOG-1:0] wr_row, input wire [LINE_ADDR_WIDTH-1:0] wr_col, input wire [LINE_ADDR_WIDTH-1:0] rd_col, output wire [DATA_WIDTH-1:0] dout ); // 使用BRAM原语或综合推断 reg [DATA_WIDTH-1:0] mem [ROWS * LINE_WIDTH - 1 : 0]; wire [ADDR_WIDTH-1:0] wr_addr wr_row * LINE_WIDTH wr_col; wire [ADDR_WIDTH-1:0] rd_addr rd_row * LINE_WIDTH rd_col; always (posedge clk) begin if (wr_en) mem[wr_addr] din; dout mem[rd_addr]; end endmodule实际调试中有一个很容易忽略的细节BRAM读操作有1拍延迟如果后续SAD计算单元需要严格同步读取模板和搜索区域像素必须在读地址侧也做一拍对齐否则会出现数据错位一拍的奇怪bug。这个坑我在联调阶段排查了很久。3.2 SAD计算单元核心流水线的实现单个SAD计算单元是整个系统的心脏。它的输入是模板像素和搜索区域像素输出是一个候选位置的累计SAD值。我采用的微架构是单像素流水的累加结构每时钟周期处理一个像素对模板按行扫描顺序输出。// 单路SAD计算单元用于计算一个候选位置的绝对差值和 module sad_unit #( parameter DATA_WIDTH 8, parameter BLOCK_SIZE 16, parameter TOTAL_PIXELS 256, parameter CNT_WIDTH 9 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] template_pixel, input wire [DATA_WIDTH-1:0] search_pixel, input wire pixel_valid, output reg [15:0] sad_value, output reg result_valid ); reg [CNT_WIDTH-1:0] cnt; wire [DATA_WIDTH-1:0] abs_diff (template_pixel search_pixel) ? (template_pixel - search_pixel) : (search_pixel - template_pixel); always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; sad_value 16d0; result_valid 1b0; end else if (pixel_valid) begin if (cnt TOTAL_PIXELS - 1) begin sad_value sad_value abs_diff; result_valid 1b1; cnt 0; end else begin sad_value sad_value abs_diff; result_valid 1b0; cnt cnt 1b1; end end end endmodule这套逻辑看着简单但有几个边界条件需要格外小心。首先是清零时机当前候选位置算完最后一个像素后累加器里还残留着当前结果下一个候选位置开始后必须彻底清零后再累加。这里我用计数器的回绕天然实现了这个功能等于TOTAL_PIXELS - 1时输出result_valid下一个时钟周期cnt回0并开始累加新值。其次如果匹配块做了边界对齐处理比如模板正好落在图像边缘附近要确保搜索区域缓存读出来的数据是对齐过的否则SAD值会算出一堆无意义的噪声。3.3 最小SAD搜索与目标位置输出多个SAD单元并行计算出候选位置的SAD值之后还需要一个模块找出最小值及其对应的坐标偏移量。这个模块本质上是一个带数据有效信号的流水线比较器。// 最小值选择与坐标更新逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin min_sad 16hFFFF; best_dx signed_offset_rst; best_dy signed_offset_rst; end else if (sad_valid) begin if (sad_value min_sad) begin min_sad sad_value; best_dx current_dx; best_dy current_dy; end end end这里有个小技巧值得分享不要用“小于等于”作为更新条件而要用严格的“小于”。因为SAD匹配在一个平坦区域可能出现多个并列最小值如果用小于等于目标坐标会在多个极值点之间反复横跳表现在画面上就是跟踪框抖动。严格小于则会让最小值保持第一次出现的位置抖动明显改善。如果希望跟踪更平滑还可以对最终输出坐标做一个IIR滤波比如new_pos old_pos × (1-α) match_pos × αα取0.6到0.8之间效果都不错。4. 时序收敛与资源优化实战4.1 时序约束与关键路径分析写完RTL可能很快但真正让工程跑上150MHz才是考验。720p60的像素时钟是74.25MHz如果做双倍速处理或者预留余量主时钟通常要跑到150MHz以上。这个频率下最容易出现时序违例的地方往往就在SAD加法树和行缓存读地址计算这两处。加法树的本质是组合逻辑的多级求和位宽越大、级数越深关键路径越长。16输入加法树大概有4到5级LUT串联在150MHz下是比较危险的。解决办法是插入额外的流水线寄存器用一拍延迟换时序收敛。我对SAD模块内部的处理是第一级算绝对值差第二级做2路加法第三级做4路加法四级之后得到最终累加结果再进入下一级比较器。这样把组合逻辑拆成了4段每段大概只有2到3个LUT深度时序余量非常充裕。Vivado里的具体做法是先写完整的时序约束文件给主时钟和所有生成时钟建立约束create_clock -period 6.667 -name clk_main [get_ports clk_p] create_clock -period 13.468 -name clk_pixel [get_pins clkgen/clk_pixel]然后跑综合实现打开Report Timing Summary看WNS。如果WNS为负用Timing Report里的关键路径高亮基本都能立刻定位到是哪一级流水线太深。4.2 资源优化把BRAM和LUT用在刀刃上资源利用率是FPGA方案能不能落地的硬指标。我这里分享三个实战中发现很有效的优化手段。第一行缓存地址计算可以全部改为增量寄存器避免每次都用乘法和加法做实时地址换算。乘法器在地址生成这种每周期都要执行的逻辑里很浪费LUT而且容易成为关键路径。改成行号递增、列号回绕的计数器地址在一个周期内就能稳定输出。第二模板数据不要存成完整的N×N二维数组。16×16模板需要256个寄存器虽然不多但如果多路并行会膨胀。更高效的做法是把模板按行展开存入分布式RAM用读地址索引的方式输出这样模板存储只占用很少的LUT而且与搜索区域行缓存的读写天然解耦。第三如果SAD并行路数较多可以考虑让多路SAD单元分时共享同一个绝对值差计算模块。比如4路并行但每路处理速度只有像素时钟的一半就能用2个abs计算模块为4路服务。这种时分复用策略在资源紧张时非常实用代价是控制逻辑稍微复杂一点。5. 系统联调、实测效果与问题排查5.1 在Vivado里搭建硬件调试环境RTL仿真通过只是第一步真正上板调试才是考验。我的调试环境分了三个层次首先是Vivado自带的仿真平台验证SAD计算单元的功能正确性然后通过ILAIntegrated Logic Analyzer抓FPGA内部信号观察实际视频流下的SAD输出和坐标更新是否正常最后是整机画面测试在显示器上直接看跟踪框是否贴合目标。整合ILA的时候有一个非常重要的经验不要一次性把所有信号都抓ILA的存储深度有限抓太多信号会导致采样深度不够。我习惯的做法是先抓SAD输出和最小值比较器的状态确认匹配逻辑工作正常然后再加抓坐标输出和OSD叠加的调试信号分两步定位问题。举个例子我在第一次上板时发现跟踪框完全不动怀疑是坐标更新逻辑的问题。用ILA抓了best_dx和best_dy发现这两个信号一直是复位值但min_sad已经更新了。顺着排查发现是在多路并行SAD单元汇总时sad_valid信号的时序对齐出了问题——其中一路的result_valid比另一路早了1拍导致比较器在无效数据上做了更新。这个问题在仿真里很难发现因为仿真激励太规整而真实视频流里不同搜索区域的时序差异被放大了。5.2 实测效果与SAD的天然局限实测下来这套系统在模板匹配目标跟踪上的表现可以用“能干但别太贪心”来概括。简单场景——目标亮度稳定、运动平缓、无遮挡的情况下帧率稳定、延迟低、坐标输出平滑跟踪效果完全可用。但SAD这个算法本身有一些天然短板必须在项目规划阶段就想清楚。最典型的是光照变化问题。一旦环境光照整体变化所有像素的灰度值都会发生偏移SAD值会整体增大但最小值对应的位置可能没有变。这种情况影响不大。怕的是局部光照变化比如目标上半部分被阴影遮挡、下半部分被强光照射这时模板内每个像素的灰度偏移方向不一致SAD最小位置就可能偏移到完全不相关的地方。快速运动也是一个问题。目标在相邻两帧之间位移超过搜索范围时搜索区域内找不到正确位置SAD最小值会落在某个噪声峰值上目标瞬间丢失。解决思路有两条一是扩大搜索范围但这会增加计算量二是引入运动预测比如用卡尔曼滤波预测目标下一帧的大概位置再以预测位置为搜索中心能用同样的搜索范围覆盖更快的运动。这也是FPGA实现目标跟踪时很常见的增强手段。5.3 常见问题与排查速查表现象可能原因排查思路跟踪框漂移到背景目标上目标和背景纹理过于相似SAD最小值不唯一增大模板尺寸给搜索位置加距离惩罚项目标跟丢后无法自动找回目标位移超过搜索范围引入运动预测或扩大搜索范围增加目标再检测逻辑跟踪框在目标静止时抖动多个候选位置的SAD值接近比较器频繁切换更新条件改为严格“小于”输出坐标加IIR平滑帧率不达标、画面卡顿SAD计算单元吞吐不足增加并行路数精简流水线降低模板和搜索区域尺寸ILA抓到的坐标值有跳变sad_valid对齐时序有误差检查多路SAD单元的result_valid是否严格同步BRAM资源占用过高搜索区域缓存高度设置过大压缩搜索范围改用更高效的存储编码这里提一个很多教程不会讲的小细节如果你在测试中发现跟踪框偶尔往左上方偏移而且偏移量刚好等于模板宽度一半那大概率是ROI生成逻辑的取整方式出了问题。我在实现模板裁剪时最初用了向下取整导致模板中心比真实目标中心偏了半个像素的整数倍。后来统一改成四舍五入取整问题就消失了。这种边界问题在仿真里因为坐标都是整数写得比较规范极其不容易暴露但在真实视频流里几乎必然出现。联调阶段我还有一个感触很深的点FPGA图像处理项目“先让图像通路跑通再谈算法优化”是铁律。我最初试图一次性把采集、缓存、SAD、跟踪、叠加全部调通结果出了问题完全不知道是哪个环节引起的。后来改成先灰度输出测试、再OSD叠加测试、再SAD计算功能验证、最后才跑完整跟踪流程每一步都有可见的中间结果定位问题快得多。这个习惯后来成了我做所有图像项目的标准流程——永远把系统拆成可见模块逐个点亮。文章讲到这核心设计思路和实现细节基本都覆盖了。最后再分享一点个人长期做FPGA图像处理项目的体会算法选型和硬件架构是互相绑定的。SAD这种算法看似简单但它和FPGA的契合度非常高背后那句“能用简单加法解决就绝不上重武器”的优化原则在硬件工程师眼里比算法本身的复杂度更重要。后续如果你想扩展多目标跟踪现在这套并行SAD单元的结构完全可以按目标数成倍扩展搜索区域行缓存也天然支持多目标的并发读取如果想挑战更高难度的跟踪场景可以在SAD的基础上叠加卡尔曼滤波做运动预测或者用分层搜索策略先粗后细地缩小范围。这些扩展路径都是这套基础架构未来可以继续延伸的方向。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价