资讯详情

FPGA图像处理实战:SAD模板匹配算法实现实时目标跟踪

📅 2026/9/9 10:40:45 | 华诺云谱 👁 阅读
FPGA图像处理实战:SAD模板匹配算法实现实时目标跟踪
做图像处理这几年凡是我在电脑上跑得好好的算法一旦琢磨着往嵌入式端搬几乎都要脱一层皮。去年接了一个运动目标实时跟踪的活要求在分辨率为640×480的灰度视频流里锁住一个已知目标帧率不能低于30fps。一开始我图省事直接拿ARM核跑软件模板匹配结果目标稍微动快一点搜索窗口一拉大CPU占用率直接飙到90%还频频丢帧。后来把目光转向FPGA用SAD模板匹配算法把整个计算链路搬到了硬件上才算真正把这个问题解决掉。这篇东西就是把我在这个项目里的完整思路整理一遍从为什么选SAD、为什么用FPGA到硬件架构、Verilog实现、调试踩坑一次说清楚。如果你正准备入门FPGA图像处理或者已经被实时目标跟踪的算力需求折磨过这篇文章应该能帮你省下不少弯路。这里讲的全是实际工程里能落地的做法不是拿个Demo跑通就算完事的那种。1. 为什么是SAD为什么是FPGA——选型背后的真实考量1.1 目标跟踪任务拆解先搞清楚要算什么任何目标跟踪算法底层都逃不开一个基本操作拿已知目标的特征去当前帧里找最相似的位置。模板匹配就是最朴素的一种思路——提前保存一张目标图像模板在待搜索的图像区域里逐点滑动用某种相似度度量来判断哪个位置和目标最像。这个流程拆开就是三步在搜索窗口内把模板覆盖到每一个候选位置计算模板与当前窗口内容的相似度选出相似度最高的位置作为目标在当前帧的坐标。听起来不复杂但问题出在计算量上。假设模板尺寸是16×16搜索窗口是100×100一个像素位置就要做256次差值运算整个搜索窗口就是100×100×256约256万次计算。一秒钟30帧那就是7680万次。这还只是灰度图、单目标的情况。在通用处理器上这种纯数据并行的运算效率并不高因为每个计算单元都在做重复的乘加或比较而CPU的核数和指令流水线是有限的。1.2 相似度度量对比SAD为什么最省硬件资源相似度度量方法有很多常用的包括SAD绝对差值和、SSD差方和、NCC归一化互相关。我拿一张表直接对比一下指标SAD绝对差值和SSD差方和NCC归一化互相关计算公式Σ|I-T|Σ(I-T)²Σ(I-T)² / sqrt(ΣI²·ΣT²)乘法器需求无有大量FPGA资源占用极低中高对照明变化敏感度较敏感较敏感不敏感硬件实现难度低中高在FPGA上做实时处理第一原则就是尽量少用DSP乘法器因为乘法器资源是有限的而且用多了布局布线压力也大。SAD计算只需要做减法、取绝对值、累加这三个操作在FPGA里全部可以用LUT和进位链完成根本用不到DSP块。这对于一片入门级FPGA来说是非常友好的。提示如果你的应用场景光照变化剧烈SAD的精度会打折扣。工业现场固定光源下SAD完全够用但如果是在户外自然光下做跟踪就要考虑在SAD之前加一个简单的对比度归一化预处理或者换用NCC。工程上是先能满足实时性再考虑精度。1.3 为什么FPGA而不是DSP或GPU我自己做过对比评估。DSP比如TI的C6000系列在图像处理里很常见优点是开发流程像嵌入式软件复杂算法好移植但瓶颈在数据搬运上——图像数据要从内存搬进L2缓存计算完再搬出来当数据量一上来DSP的瓶颈往往不是算力而是访存带宽。GPU呢算力是强但功耗和成本对很多设备来说比较尴尬而且驱动和实时性也不好把控。FPGA的优势在于它能把取数据→算SAD→比较最小值这个过程变成一条硬件流水线图像像素从传感器流进来结果坐标直接流出中间不需要频繁访问DDR延迟是微秒级的。举个例子你就明白了。CPU像是只有一个厨师虽然厨艺高超但只能一道一道做菜GPU像是一大群厨师能做很多菜但光召集和管理这群厨师就要耗不少电FPGA像是直接铺了一条传送带每个工位就干一件事食材流动的过程里菜就做好了。对于SAD这种大量重复、高度并行的运算传送带模式的效率是最高的。2. SAD算法的数学原理与硬件化改造思路2.1 从公式到硬件SAD怎么算边界在哪SAD的数学定义很直白SAD(u, v) Σ(i0→N-1)Σ(j0→M-1) |I(ui, vj) - T(i, j)|其中I是当前帧的像素灰度T是模板图像N×M是模板尺寸(u,v)是模板在搜索图像中的偏移位置。SAD值越小说明模板和当前窗口内容越匹配。硬件实现时需要先确定几个关键参数灰度位宽一般取8bit0~255覆盖256级灰度对大多数可见光图像足够模板尺寸8×8到32×32之间比较常见。模板太小特征不够匹配容易误判模板太大计算量成平方增长。我这个项目里用的16×16是实时性和鲁棒性折中后的选择SAD累加值的位宽8bit灰度、16×16模板最大SAD值 16×16×255 65280需要16bit的累加器来存。2.2 软件循环 vs 硬件流水两种思维的差异在软件里写SAD最直观的是三重循环外层遍历搜索窗口的行和列内层遍历模板的每一个像素。但在FPGA里这种循环靠状态机大量复用计算单元要么性能不够要么状态机复杂到想砸键盘。更优雅的做法是空间换时间把模板的每一个像素对应到一个独立的计算单元上。一个时钟周期内256个绝对差值运算器对应16×16模板同时工作计算结果通过一棵加法树汇总得到当前候选位置的SAD值。下一个时钟周期窗口滑动一个像素新的256个像素值进入计算单元得到下一个位置的SAD值。整个计算过程是流水的每个周期都能输出一个候选位置的SAD数据的吞吐率是软件循环望尘莫及的。2.3 加法树与流水线寄存怎样保证时序收敛256个绝对差值要累加成一个SAD值如果直接串行累加组合逻辑路径会非常长时序根本收敛不了。正确做法是搭一棵加法树——第一层128个加法器把256个数两两相加第二层64个加法器加第一层的结果以此类推最后一级2个加法器得到最终结果。每一层之间插上流水线寄存器整个树深度是log2(256) 8级。这样单级的组合逻辑延迟很小时钟频率可以拉得很高。要注意的是插了流水线寄存器后从数据输入到SAD结果出来会有固定的时钟周期延迟latency。这个延迟在后面的比较器和状态机里必须算进去否则数据对的乱七八糟。这一点看起来不起眼实际调试时坑得我花了大半天。3. 顶层架构设计一帧图像从进来到坐标出去3.1 系统模块划分与数据流整个系统的顶层架构我这样划分模块功能说明图像采集接口接收摄像头/传感器的像素数据可以是MIPI、DVP或自定义并行接口灰度转换RGB转Y灰度如果是彩色输入需要做灰度输入可跳过行缓存模块缓存N行图像数据生成滑动窗口用BRAM或移位寄存器实现SAD计算阵列并行计算256个绝对差值并求和核心计算单元最小值比较模块扫描搜索窗口记录SAD最小值位置相当于目标坐标搜索模板存储存储模板图像数据用BRAM或寄存器阵列坐标输出/叠加显示输出目标坐标可叠加方框方便观察验证数据流是这样的摄像头像素按行扫描顺序逐像素流入经过灰度转换后写入行缓存。当写入的图像行数达到模板高度后行缓存里就有一个滑动的16×16窗口了。窗口每向右滑动一个像素就触发一次SAD计算。扫完一整行窗口下移一行继续。等整帧图像扫描完毕SAD值最小的那个位置就是目标模板在当前帧的最佳匹配位置。3.2 从像素流到滑动窗口行缓存的设计思路行缓存是整个SAD硬件化的地基。常见的实现方式有两种移位寄存器方式一个深度为图像宽度、位宽为8bit的寄存器链。像素流入时整个链逐级移动窗口数据天然对齐。优点是逻辑简单但不适合大图像因为宽度640的移位寄存器会消耗大量寄存器资源。BRAM交替写读方式用多个BRAM分别缓存每一行配合复杂的读地址控制生成窗口。资源利用更高效但控制逻辑复杂需要处理写地址和读地址的同步问题。我这个项目里用的是移位寄存器方式。为啥因为640×480的分辨率不算大16行的移位寄存器算下来资源消耗还在可接受范围内而且实现起来最不容易出错。Zynq或者Artix-7这类芯片完全扛得住。如果你的分辨率上到1920×1080那还是老老实实上BRAM方案吧。3.3 SAD计算阵列与搜索策略一次扫描还是分块扫描搜索策略上有个决策点一帧图像来了是全图扫描还是只在上一帧目标附近的小范围搜索全图扫描的优点是绝对不会漏掉目标即使目标瞬间大范围移动也能追上缺点是搜索范围大一帧里要比较几万个候选位置。小范围搜索速度快得多但目标如果运动过快直接跑出搜索窗口那就跟丢了。我的做法是二者折中默认在上一帧目标坐标周围一个80×80的邻域内搜索同时并行记录整帧扫描时的SAD最小值。如果小邻域内没有找到置信度足够高的匹配最小SAD值超过阈值就用全图最小值的结果顶上。这样既保证了常规情况下的高帧率又能应对目标快速移动的情况后面在卡尔曼滤波扩展里还可以进一步优化。4. 核心模块的Verilog实现三百行代码把SAD落地4.1 行缓存与滑动窗口生成行缓存模块的核心思路是来一个像素就往寄存器链里推进一步。当流水线填满16行×16列后窗口数据就开始流动了。这里我给出一个简化的行缓存生成窗口的Verilog框架// 行缓存生成16x16滑动窗口 module line_buffer_16x16 #( parameter DW 8, parameter IMG_W 640, parameter WIN_H 16, parameter WIN_W 16 )( input wire clk, input wire rst_n, input wire pix_valid, input wire [DW-1:0] pix_data, output wire [DW-1:0] win_data [0:WIN_H*WIN_W-1], output reg win_valid ); // 使用16个深度为640的移位寄存器行 reg [DW-1:0] line_buf [0:WIN_H-1] [0:IMG_W-1]; ...真正工程里我不会把窗口数据做成一个二维数组端口那样综合起来非常慢。更好的做法是例化16个独立的行缓存模块每个模块存一行再用generate循环把每行的窗口列数据抽出来拼接成256个8bit信号送给SAD计算阵列。4.2 SAD计算阵列的流水线实现SAD计算阵列是整个设计的心脏我用了三级流水线来实现第一级并行计算256个 |I-T| 绝对差值第二级16个16输入加法器分别求和得到16行各自的和第三级把16个部分和累加得到最终的SAD值。16×16模板的SAD核心计算代码如下// SAD计算阵列16x16模板三级流水线 module sad_16x16 ( input wire clk, input wire rst_n, input wire [7:0] img_data [0:255], // 当前窗口像素 input wire [7:0] tpl_data [0:255], // 模板像素 output reg [15:0] sad_value, output reg sad_valid ); // 第一级流水线绝对差值 reg [7:0] abs_diff [0:255]; always (posedge clk) begin for (int i 0; i 256; i) begin abs_diff[i] (img_data[i] tpl_data[i]) ? (img_data[i] - tpl_data[i]) : (tpl_data[i] - img_data[i]); end end // 第二级行内16个像素累加产生16个部分和 reg [11:0] row_sum [0:15]; always (posedge clk) begin for (int r 0; r 16; r) begin row_sum[r] abs_diff[r*160] abs_diff[r*161] ... abs_diff[r*1615]; end end // 第三级16行部分和累加 always (posedge clk) begin sad_value row_sum[0] row_sum[1] ... row_sum[15]; sad_valid pixel_valid_delay; end endmodule这段代码里有一个细节值得拎出来讲绝对差值不要用abs()函数而是用比较器加选择器。因为综合工具对abs()的处理方式不可控有时候会生成不必要的大减法器和额外逻辑。在FPGA上比较后二选一才是最省资源的做法。4.3 最小值搜索与坐标锁定逻辑每一个候选位置都会产生一个SAD值但最终只需要最小的那个。这个逻辑写成硬件也很直接用一个寄存器保存当前最小SAD值每来一个新SAD值就和它比较如果更小就刷新最小值同时把当前的扫描坐标锁存到输出寄存器里。这里有一个容易犯的错误比较器的使能时机。滑动窗口往前移动时并不是每个周期都有有效的SAD结果因为流水线有latency而且图像边缘位置的窗口数据是不完整的。我在设计里加了一个search_enable信号只有当窗口完整地落在搜索区域内时比较器才被使能避免了边缘区域产生一堆垃圾坐标。5. 调试过程中踩过的坑时序、资源与滞后问题5.1 流水线latency引发的坐标错位这是我调这个系统时遇到的第一个硬骨头。SAD计算阵列有三级流水线也就是说从窗口数据采入到SAD结果输出整整延迟了3个时钟周期。一开始我没注意到这个延迟坐标计数器在像素输入的第N个周期就自增了但SAD结果对应的是N-3个周期那个窗口的位置。表现就是输出的目标坐标总是偏的而且目标运动越快偏移越大。排查链路是这样的我先用Testbench构造一张带已知目标的测试图目标放在固定位置然后观察输出的坐标是否和预设位置一致。结果发现输出坐标比预设位置小了3个像素。又检查了一遍窗口生成逻辑确认窗口数据没问题最后把矛头指向流水线延迟。解决方法是把像素计数器也打3拍让坐标和SAD结果对齐。注意这类时序对齐问题Debug时最好的工具是仿真波形里同时拉出像素有效信号、窗口有效信号、SAD结果有效信号和坐标计数器四个信号放在一起对比一眼就能看出数据对没对上。5.2 资源不够时的并行度取舍我最初设计时想提高吞吐率在基础版SAD单元旁边又加了一套并行处理逻辑想让两个候选位置同时计算。结果综合完一看资源报告LUT消耗超了50%触发器也快爆了。换了一片更大的FPGA成本又不允许。后来做了两件事解决了问题一是把模板缓存从寄存器阵列改成BRAM一个16×16模板只需要几个BRAM比几百个寄存器省太多二是把搜索步长从1像素改成2像素相当于分辨率降低一半但搜索范围不变计算量降为原来的四分之一。当然步长变大后精度会有损失。我的处理方式是先粗搜索定位到大致区域再在这个区域附近做一次步长为1的精细搜索这样精度和资源就平衡了。5.3 模板更新策略静态模板为什么会跟丢静态模板在目标姿态、光照变化不大的时候表现稳定但一旦目标开始旋转或者光照变了匹配分数会急剧恶化。我实测过目标转动超过15度SAD值就会翻倍超过阈值直接判定跟丢。解决思路是加入模板更新。最简单的做法是每帧匹配到最优位置后把当前帧这个位置的窗口数据以一定比例混合进旧模板T_new α * T_old (1-α) * I_bestα取0.7~0.9之间比较常见。α越大模板更新越保守对噪点的适应能力差但稳定α越小模板更新越快容易跟上目标变化但也容易被相似背景带跑。还有一种更稳健的做法只有当当前帧的最小SAD值低于某个阈值时才更新模板如果SAD值偏大说明目标可能被遮挡了这帧就不要更新模板否则会把遮挡物的特征学进模板里。5.4 边界效应的处理边界问题也是我在工程里踩过的坑。当滑动窗口靠近图像边缘时有一部分窗口区域超出了图像范围这些位置的SAD值是没有意义的。如果不处理它们会充斥在比较器里甚至可能因为局部无效像素恰好和模板很像而干扰判定结果。处理办法非常简单粗暴直接把边缘区域的计算使能信号拉低。在窗口中心坐标离图像边缘小于搜索半径时不产生有效SAD值也不参与最小值比较。这样虽然牺牲了图像边缘的搜索能力但换来的是结果的可靠性。如果你的场景要求边缘也能搜到目标那只能做像素复制或镜像填充但这会让计算量明显上升一般场景不值得。6. 验证方法与测试结果怎么证明这个系统真的能用6.1 仿真验证用Testbench把图片喂给FPGA纯硬件上板调试效率太低我习惯先用ModelSim或Vivado Simulator做功能仿真。方法是用Python读一张带目标的灰度图转成十六进制文本文件Testbench里用$readmemh把像素数据按时钟节拍喂给设计模型。仿真结束后再写个小脚本把输出的坐标位置标回原图确认匹配点是否准。我用的测试序列是一个在室内走廊里走动的人目标模板是在起始帧人上半身的一个16×16区域。仿真结果看只要目标没有被大面积遮挡SAD最小值对应的位置和人工标注位置偏差在2~3个像素以内这个精度对目标跟踪来说是够用的。全图单次扫描的SAD计算耗时约640×480307200个时钟周期在100MHz时钟下约3.07ms远小于一帧33ms的预算。6.2 上板实测资源占用和帧率数据我用的是一块Xilinx Artix-7系列的板子具体型号是XC7A35T属于入门级FPGA。综合实现后的资源占用如下资源类型消耗量占芯片比例LUT8921约42%FF触发器6845约16%BRAM6个约20%DSP48E100%看到DSP用了0我特别满意——整个SAD计算没有任何乘法全是逻辑运算把DSP资源留给其他算法模块用了。板级实测系统运行在100MHz摄像头的输入是12MHz像素时钟一帧640×480的处理时间大约在3.2ms即使算上模板更新和坐标输出叠加的额外开销整体帧率也能跑到45fps以上远超我30fps的需求。6.3 后续还可以怎么扩展卡尔曼滤波、多尺度和多目标这个系统做出来之后我又在它基础上扩展了几个方向后面你也可以顺着这些方向继续深入卡尔曼滤波现在输出的目标坐标是逐帧独立计算的会有抖动。接一个卡尔曼滤波器做轨迹平滑同时用预测值缩小搜索范围能够进一步提升鲁棒性和帧率。卡尔曼在FPGA上实现不复杂主要就是几个矩阵乘法原理上和一维维纳滤波类似不会引入太大资源开销。多尺度匹配目标由远及近时大小会变化固定16×16模板会失效。可以构建图像金字塔在不同分辨率上各做一次SAD选择SAD值最小的那个尺度和位置。代价是计算量成倍增加可以考虑只在目标附近的局部区域多尺度匹配。多目标跟踪SAD计算阵列本质上是可以分时复用的如果目标数量不多可以依次用不同模板做匹配或者并行放两套模板存储轮流查。资源有限时可以在搜索策略上做文章比如把画面分区每区管一个目标减少同时计算的压力。整个项目做下来我最深的体会是FPGA图像处理并不神秘关键是想清楚数据流怎么走和哪些计算能并行。SAD算法恰好是一个能完美适配FPGA特性的算法——计算规则简单、数据并行度高、不需要乘法器很适合作为FPGA图像处理入门的第一个完整项目。你在自己动手的时候也建议先用仿真验证好窗口生成和流水线时序再上板调试能省掉非常多排查问题的精力。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。