☰
FPGA实时图像处理实战:帧间差分算法在Quartus与Vivado中的实现
2026/10/20 16:02:20 网站建设 项目流程

最近被一个项目倒逼着把帧间差分算法从PC端搬到了FPGA上,前前后后折腾了三周,同时用到了Quartus和Vivado两套平台。做下来最大的感受是:用FPGA做实时图像处理,真正的门槛不是算法本身,而是如何把串行的软件思维翻译成并行的硬件流水线。这篇东西不准备讲高深理论,就把基于FPGA的Quartus与Vivado实时图像处理技术怎么落地这件事拆开,从帧间差分法的原理、RTL结构、两家平台的配置差异,再到我在调试里遇到的坑,一一说清楚。如果你正打算做运动物体检测、目标追踪、高速视觉定位这类需求,这篇文章应该能帮你少走不少弯路。

1. 为什么最终选帧间差分法,又为什么是FPGA

1.1 帧间差分到底在算什么

帧间差分法的核心思路非常直白:把相邻两帧图像的像素值逐点相减,取绝对值,得到的差值如果超过某个阈值,就认为这个像素属于"当前帧中的运动区域"。

用数学表达就是:

D_n(x,y) = |I_n(x,y) - I_{n-1}(x,y)| B_n(x,y) = 1, 如果 D_n(x,y) > T B_n(x,y) = 0, 否则

其中 I_n 是当前帧灰度图,I_{n-1} 是上一帧灰度图,T 是检测阈值。B 是二值化结果,1 表示前景运动像素,0 表示背景。

这个算法最突出的优点是完全不依赖背景建模,也没有训练过程。不管是静止镜头下的行人走动,还是传送带上的零件移动,只要相邻两帧存在像素级差异,它就能把变化的区域捡出来。它的缺点也很明显:运动物体内部颜色一致的区域会因为帧差被抵消而出现空洞;物体如果完全停在画面里不动,下一帧开始就检测不到了。但对于"检测目标是否存在、大概在什么位置"这个层面的需求,帧间差分法是用最少的资源换运动信息的最佳切入点。

实际写 RTL 时,我不建议一上来就做背景差分、光流之类的高复杂度算法。这些算法迭代到FPGA上需要大量DSP和Block RAM,排错难度成倍上升。帧间差分法用到的运算只有减法、绝对值、比较,全部可以用简单组合逻辑或两级流水线实现,非常符合FPGA的并行计算模型。

1.2 CPU方案和FPGA方案的分水岭是延迟

我最早用OpenCV在工控机上跑背景差分做运动目标检测。视频流通过USB采集,分辨率640x480,帧率60fps,程序里要做灰度转换、高斯滤波、背景差分、形态学开闭、连通域标注、质心计算。跑起来之后CPU占用接近90%,端到端延迟约30到40毫秒。

这个延迟在静态场景里看起来问题不大,但接上机械臂或云台跟踪系统后就很致命。视觉给控制系统的位置信息每延迟一帧,运动目标就可能已经跑出一段距离,控制系统只能靠预测算法补差值,调参调到怀疑人生。

FPGA方案的核心优势是"边进边算"。像素从摄像头进入FPGA后,经过灰度转换、帧差、形态学、坐标统计,最终从串口输出目标坐标,整个过程是严格流水线化的。以640x480@60fps为例,像素时钟约25MHz,一帧像素流持续16.6ms,坐标结果可以在帧结束后的几个像素周期内就更新出来,端到端延迟非常稳定,基本等于固定的几级流水线延迟加一帧缓冲时间。

当然,FPGA不是万能的。如果应用需要跑大量神经网络、目标分类、复杂跟踪逻辑,FPGA的开发复杂度会指数级上升。这时候比较合理的做法是FPGA做前级运动区域检测和坐标提取,后级在CPU/DSP上做识别和决策。这也是我目前维护的架构。

1.3 什么样的项目适合用这个方案

不是所有运动检测项目都应该上FPGA。我的经验是,满足下面任意两条,FPGA方案的性价比就会变得很高:

  • 视频分辨率在720p以内,帧率高于30fps,实时性要求严格。
  • 延迟要求不能超过几个毫秒,甚至要绑定到硬实时控制环。
  • 部署环境空间有限,不适合放一台工控机。
  • 功耗有严格要求,比如电池供电的便携检测设备。
  • 需要同时处理多路视频源,CPU方案扩展成本太高。

帧间差分法在这个场景里的定位是"低延迟、高稳定性的运动区域感知层"。它不负责识别你是人还是车,也不负责目标ID跟踪,它只负责把运动区域从背景中分离出来,并且给出准确的质心和外接矩形。这套逻辑如果跑在FPGA上,后续再叠加其他模块(比如模板匹配、卡尔曼预测)会非常顺手。

2. 帧间差分算法的FPGA硬件拆解与RTL映射

2.1 完整数据通路:像素流进来,坐标出去

FPGA侧的运动追踪系统,数据通路大致是这样的:

摄像头或HDMI解码器输出原始数字视频信号(包含像素时钟、行同步、帧有效标志),经过色彩空间转换得到8bit灰度图;灰度图同时进入两个方向:一路直接送入差分模块的当前帧输入,一路写入帧缓存延迟一拍;帧缓存读出的上一帧像素与当前帧像素做差分、阈值比较,得到二值化运动掩膜;掩膜经过形态学滤波滤除噪声,再进入统计模块累计前景像素数量、x坐标加权和、y坐标加权和;帧结束时做除法得到质心坐标,打包成UART或者寄存器数据输出。

文字画出来就是:

sensor_in -> gray -> frame_diff -> binary -> morph_filter -> centroid_stat -> coord_out

这里最容易做错的设计决策是"在哪里插入帧缓存"。很多人会把帧缓存放在差分模块之后,先把当前帧二值化完存起来,下一帧再读出来做差分。这种方案有两个问题:一是二值化后的运动掩膜直接从像素特性变成了区域特性,帧与帧之间的对应关系容易被形态学运算破坏;二是需要同时维护两套二值图缓存,BRAM消耗翻倍。正确做法是存储原始灰度图,差分计算放在缓存读出之后。这样形态学、统计模块只处理当前帧的结果,不会把上一帧的中间状态卷进来。

2.2 帧缓存设计:Ping-Pong Buffer还是外挂DDR

帧间差分需要一个最重要的东西:上一帧图像。这个"上一帧"存在哪里,直接决定整个工程的复杂度和性能上限。

如果分辨率不高,比如640x480或800x600,灰度图一帧分别约307KB和480KB。现在中端FPGA芯片内部的Block RAM总量大约在几百KB到几MB不等,完全可以塞下两帧,做成Ping-Pong Buffer。

Ping-Pong的思路是准备两块RAM,当前帧写入RAM A,同时从RAM B读取上一帧;下一帧到来时交换,当前帧写入RAM B,同时从RAM A读取。每帧数据只被写入一次、读出一到两次,读写端口可以分在两个时钟域,天然避免了同一bank同时读写造成的带宽冲突。实现时用双端口RAM,写端口接当前帧像素流,读端口接差分模块。帧有效信号的上升沿作为切换触发,切换逻辑要有一次同步打拍,避免在像素流中段突然切换导致读出撕裂数据。

如果视频到了720p或者1080p,一帧灰度图约0.66MB到2.07MB,FPGA片内BRAM往往不够用,就必须接DDR颗粒。DDR方案在带宽上足够,但工程复杂度会明显上一个台阶:需要DDR控制器、地址管理、帧指针切换、缓存行策略。实际使用720p60灰度视频时,写入带宽约58MB/s,读出上一帧也约58MB/s,总带宽不到120MB/s,中低端DDR3完全绰绰有余。反而是地址连续性和帧切换逻辑容易出问题,尤其要确保在帧消隐期完成指针跳变,否则画面会出现撕裂。

2.3 差分、阈值、形态学的Verilog实现

差分单元非常简单。下面这段代码是典型的时序逻辑实现,两级流水线:第一拍计算绝对差值,第二拍把差值与阈值比较输出二值结果。

reg [7:0] diff_reg; reg [7:0] bin_reg; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin diff_reg <= 8'd0; bin_reg <= 8'd0; end else if (frame_valid) begin diff_reg <= (cur_pixel >= pre_pixel) ? (cur_pixel - pre_pixel) : (pre_pixel - cur_pixel); end end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin bin_reg <= 8'd0; end else if (frame_valid) begin bin_reg <= (diff_reg > threshold) ? 8'hFF : 8'h00; end end

代码很简单,但有两个容易忽略的点。

第一,阈值寄存器最好做成可配置寄存器,通过AXI-Lite或Avalon-MM总线由上层CPU动态改写。固定阈值在室内恒定光源下能用,但一遇到窗帘光影、灯管闪烁,检测结果会剧烈跳变。我后面会单独说动态阈值的处理。

第二,差分模块输出了二值图,里面往往有大片孤立噪声点和小连通碎片。如果直接把二值图送入质心统计,坐标会漂得很厉害。通常要加一个3x3形态学滤波器:先用一次腐蚀去掉孤立噪点,再用一次膨胀恢复目标边缘。形态学滤波需要缓存两行像素,一般用两个FIFO或移位寄存器做行延迟,例如:

// 假设 pixel_line0 是当前行,pixel_line1 是上一行,pixel_line2 是上上行 wire [8:0] win_center = {pixel_line0[1], pixel_line1[1], pixel_line2[1]};

3x3窗口的所有像素都到位后,腐蚀用AND,膨胀用OR。如果资源紧张,可以把腐蚀膨胀合并成一次"先OR后AND"的近似形态学,对噪声抑制也有效,但我建议标准做法不要省,FPGA扇出代价并不高。

3. Quartus与Vivado两套工具链下的实现对照

3.1 Quartus环境下搭建视频处理工程骨架

先说Quartus生态。使用Quartus的FPGA,视频链路最常见的接口标准是Avalon-ST。它以包的格式传输视频流,每个像素包含data、valid(数据有效)、sop(包头)、eop(包尾)信号。整帧数据可以看成一个包,帧有效期间valid拉高,sop在第一个像素拉高,eop在最后一个像素拉高。

用Platform Designer(旧称QSys)搭系统时,视频源、灰度转换、差分模块、统计模块、显示输出都会作为不同的Streaming组件连接起来。视频输入IP输出Avalon-ST信号后,自己写的差分模块只需要按同样的信号格式接入即可。关键是把差分模块设计成"透明转换器":输入Avalon-ST视频流,输出Avalon-ST视频流,内部把灰度流替换成二值图流,其余控制信号全部透传。

在Quartus环境里,我强烈建议把灰度转换、形态学、质心统计都拆成独立模块,每个模块只处理Avalon-ST接口的四个核心信号。这样在SignalTap II Logic Analyzer里抓波形时会非常清晰。SignalTap可以实时观测内部信号,但有一个坑:如果你没有把信号标记为keep,综合器很可能因为逻辑冗余把一些中间节点优化掉,导致SignalTap里看不到你想抓的差分结果。解决方式是在Verilog中声明信号时加上综合属性,比如Quartus下用(* keep *) wire diff_result;。

3.2 Vivado环境下搭建AXI4-Stream视频链路

Vivado生态下,视频链路几乎都是AXI4-Stream格式。AXI4-Stream的TDATA可以配置成不同字节数,视频常用的是RGB888(24bit)和灰度Y8(8bit)。相比Avalon-ST,AXI4-Stream多了TKEEP、TLAST、TUSER等信号,其中TLAST对应图像行尾,TUSER可以携带帧同步信息。

用Vivado搭工程时,我一般走Block Design流程:Video In to AXI4-Stream IP负责把并行视频信号转成AXI Stream;VDMA(Video Direct Memory Access)负责把视频流写入DDR和从DDR读出;差分模块做成一个AXI4-Stream插入节点,挂在VDMA读通道之后。

这里有一个非常关键的工具链差异:Vivado的AXI4-Stream IP在握手逻辑上比Quartus的Avalon-ST更严格。AXI4-Stream有TREADY信号,发送方和接收方需要通过握手完成数据传输。自己写差分模块时,如果只是简单地把TVALID透传,忽略了TREADY,在背靠背像素时会出现丢数据。正确做法是:差分模块内部用FIFO缓存输入数据,只有在TREADY拉高时才输出结果,或者干脆把差分模块设置为AXI4-Stream从端口,由上游控制节拍。我第一次迁移时没注意,结果帧差图里每隔几行就出现一条错位,排查了很久。

Vivado里调试用ILA(集成逻辑分析仪)。ILA的采样时钟要和视频像素时钟保持一致,否则看到的波形会让人完全摸不着头脑。另外XDC约束里必须给视频时钟创建约束,常用create_clock命令,如果来自外部晶振还要加set_property PACKAGE_PIN。

3.3 两套工具链的约束、IP和调试差异清单

为了给两平台都做过的朋友一个快速对照,我把关键差异整理成表格:

对比项Quartus平台Vivado平台
视频流接口Avalon-STAXI4-Stream
工程入口Platform Designer(QSys)Block Design
引脚约束文件.qsf.xdc
时钟生成IPALTPLL / PLLClocking Wizard
片上逻辑分析SignalTap IIILA(集成逻辑分析仪)
寄存器配置总线Avalon-MMAXI-Lite
复位习惯通常异步复位同步释放常模板 reset_pulse / Processor System Reset
IP封装平台Quartus IP CatalogVivado IP Catalog / Package IP

如果只是做一个项目,选哪个平台都行,关键看团队熟悉哪个生态。如果是要做长期产品,你需要考虑FPGA供货稳定性和配套IP库。拿我自己来说,Quartus侧工程偏向工业视觉类应用,Vivado侧工程偏向视频接口高速采集和边缘处理。两套平台的Verilog逻辑核心其实可以共用,只要把接口适配层剥离开,算法模块几乎原封不动就能搬过去。

迁移时一定要留意复位逻辑。Quartus默认很多IP是异步复位,而Vivado很多AXI IP是高电平有效复位,且对复位释放时序有要求。我遇到过在Quartus正常跑的模块,搬到Vivado后上电不工作,查了半天发现是复位极性相反,差分模块一直没有使能。

4. 从二值掩膜到运动目标精准追踪

4.1 连通域太复杂,先用投影统计做单目标质心

二值化后得到的运动掩膜,最常见的使用方式就是算质心和外接矩形。完整连通域标记在FPGA上能做,但逻辑量相当大,要维护相当多的状态,不适合作为第一版方案。

如果应用场景是单运动目标,或者视频画面里主要目标只有一个,一个非常高效的做法是行列投影统计:在整帧有效期间,逐像素累加三个量:

  • sum_fg:前景像素总数
  • sum_x:所有前景像素的x坐标之和
  • sum_y:所有前景像素的y坐标之和

帧结束时用除法得到质心坐标:

X_center = sum_x / sum_fg Y_center = sum_y / sum_fg

这个逻辑在FPGA上就是两个乘加器加一个除法器。除法器可以用Xilinx/Finilix IP,也可以自己用移位减法实现,如果坐标精度要求不高,甚至可以右移几位做近似除法。实际工程里我用的除法模块是流水线式的,每帧调用一次,延迟可以忽略。

需要注意:如果画面里有多个分离目标,投影统计会把所有目标的坐标加权平均成一个中间点,导致追踪目标"跳来跳去"。这时可以把画面分割成若干子区域,每个区域单独统计,再在顶层做一个简单的分区域优先级判断。也可以先做一次膨胀将所有靠近的目标连成一块,再把它们当单目标处理。

4.2 形态学滤波对质心稳定性的影响

只看二值图直接算质心,质心很容易被边缘的孤立噪点带偏。我实测过一个对比:没有形态学滤波时,一个静止的视频测试画面的质心噪声标准差大约在2.5像素;加了3x3中值滤波和3x3膨胀后,噪声标准差降到0.7像素左右。这个数值在视觉定位场景中非常关键,因为后级机械臂会直接吃掉这个坐标抖动的代价。

形态学滤波放在差分模块之后、统计模块之前,数据流不会变复杂。唯一注意的是时序:3x3滤波会引入约两行加两个像素的延迟,需要在坐标输出时写清楚你输出的坐标到底对应哪一帧的几何位置。如果是高精度定位,应该在统计模块里把坐标计算时机与原始像素坐标对齐,否则会有几行的固定偏差。

4.3 坐标平滑和通信输出

就算做了形态学滤波,二值化阈值在目标边缘抖动仍会导致质心小范围波动。FPGA端做一阶低通滤波是最划算的:

x_filt_reg <= (alpha * x_center) + ((1 - alpha) * x_filt_reg)

alpha推荐0.6到0.8,目标移动快就取小一点,目标慢就取大一点。更好的做法是二阶预测滤波,但一阶已经能解决大部分抖动问题。这个alpha其实也应该做成寄存器,方便后级控制器根据实际工况调整。

最后一步是输出。坐标一般通过UART、以太网或者GPIF总线发给外部控制器。FPGA内部通常要做一个寄存器组,映射很多实时参数,比如当前质心x、质心y、目标面积、帧计数、阈值、alpha。用AXI-Lite或Avalon-MM总线挂到处理器上,软件可以直接读取这些寄存器用于决策。UART输出则建议在坐标帧头加上magic number和CRC校验,避免传输覆盖。

5. 实测效果、参数选取与踩坑复盘

5.1 我测出来的延迟和资源占用

在项目X中,我用一块主流中端FPGA芯片跑640x480@60fps视频,像素时钟24MHz。整条链路从像素输入到坐标寄存器更新,占用的逻辑单元约4K个LUT,Block RAM约30KB(主要用于两帧片内缓存),DSP单元基本没用。系统上电初始化完成后,从视频帧有效信号到坐标输出稳定的时间约为两帧周期加若干时钟周期,也就是大约34ms里包含两帧间隔加少量流水线延迟。

这里需要说清楚延迟的计算口径:帧间差分法天然需要等待下一帧像素进来才能和上一帧做比较,因此坐标输出至少滞后一帧。但这里的"滞后一帧"不是CPU方案里那种队列缓冲造成的延迟,而是算法本身的参考帧关系,等价于你在每个像素时钟边沿都能得到当前像素和历史像素的差分结果,帧结束得到统计结果。对于一个60fps的视频,帧间差分法输出坐标的延迟大约是16.6ms加上几个微秒,如果你的控制系统能接受这个延迟,那就非常合适。

换成720p60灰度视频后,片内BRAM不够用,我改为DDR3帧缓存。实测DDR读写带宽占总带宽约22%,延迟相对VGA档位增加不到0.3ms,主要增加在DDR读出到差分模块之间的数据路径上。

5.2 阈值T应该固定还是动态

阈值T是帧间差分法里最让人头疼的参数。T设得太大,运动缓慢的目标会漏检;T设得太小,噪声和光照变化会被当成运动目标。

最简单的固定阈值适合场景完全不变的室内环境。我在可控光照的测试台上用固定阈值16到24(灰度0-255)效果不错。但如果现场有窗户、灯光渐变、反光背景,固定阈值就很容易失效。

动态阈值我推荐一个工程上稳定的土办法:统计当前帧帧差值的局部直方图,然后取一个分位点作为阈值。FPGA里做直方图需要大量片上存储,不过你可以只统计帧差均值,然后按"均值 + 固定偏移"设定阈值:

T = min(T_max, mean_frame_diff + offset)

其中mean_frame_diff用一个小累加器实现,在每帧消隐期除以总像素数得到。优势是当整体光照变化导致帧差均值升高时,阈值会跟随升高,避免整片误检;当目标运动较弱时,均值低,阈值也跟着降低,有利于保留慢速目标。

我实际用下来,这个方案的误检率比我最初固定阈值低很多,尤其是窗外光线慢慢变化时效果很明显。不过如果目标靠近镜头,运动幅度大,均值会被目标本身拉高,阈值也会升高,可能导致目标内部像素被判定为背景。所以动态阈值的上限要严格限制,再配合后面的前景占比保护。

5.3 高频问题和解决思路

调试过程中,我踩过的坑大致有五类:

第一,画面出现横向撕裂。这个通常不是算法问题,而是帧缓存乒乓切换和视频消隐没有对齐。我自己的修复方法是:以帧有效信号下降沿为乒乓切换点,并且切换后丢弃前几个像素。如果把切换放在帧有效上升沿,写入当前帧和读出上一帧会发生在同一像素时刻,极易冲突。

第二,快速移动目标出现"重影"或拖尾。原因是帧间差分的运动边界实际上是上一帧和当前帧位置之间的差值带,目标速度越快,差分的带越宽。对策是后续形态学用较小核,并把质心计算改为只统计当前帧差分边缘的外侧像素,或者适当缩小差分区域。

第三,全局光线突变时整帧误检。处理方式是统计前景像素比例,如果某帧前景比例超过60%,判定为光照突变,整帧数据丢弃,输出值保持上一帧。这是最简单有效的保护逻辑。

第四,SignalTap或ILA抓不到内部信号。Quartus里给关键信号加keep属性,Vivado里在Block Design中把需要观测的信号引出到顶层,再接入ILA的探针。如果信号被综合成了无名字的寄存器,抓波形会非常痛苦。

第五,两套工具综合同一段差分代码,时序表现不同。Quartus和Vivado的综合器对组合逻辑的优化策略差别很大,纯组合的绝对值减法在Vivado里经常被推断成进位链组合,导致跨时钟域时时序紧张。解决办法是不写宽位组合减法,强制分两级寄存器做减法和比较,这样两边综合结果都稳定。

最后再分享一个个人体会。做这类FPGA图像处理项目,不要一上来就点IP、跑综合。先用C/Python按同样的算法顺序算一遍,把中间结果、阈值范围、坐标期望值都记下来;再在RTL仿真里按同样的输入序列比对,仿真通过后再上板。如果不做这一步,上板后在逻辑分析仪里看到一堆波形,你根本分不清是算法参数问题还是时序问题。这次做的帧间差分追踪项目,前期的仿真验证帮我省掉了至少两天的调试时间,这个流程现在已经成为我做所有图像处理工程的固定动作了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询