基于 FPGA 的帧间差分运动检测

文章目录

概要

上一篇我把整个 ZYNQ 视频链路从 OV5640 摄像头采集、AXI4-Stream 视频流、VDMA 帧缓存到 HDMI 显示的搭建过程梳理了一遍。视频链路跑通以后,接下来的工作就不再是"能不能显示",而是"能不能在这条视频链路中插入算法模块,并且让它稳定工作"。这篇文章就重点记录我在这个项目中做的第一个视频算法实验:帧间差分运动检测

我一开始的想法很简单:

把当前帧和延迟的一帧进行比较,哪里变化大,就说明哪里可能有运动目标。这个思路本身并不复杂,但真正落到 FPGA 视频流系统里,问题会立刻变得具体起来:

bash 复制代码
两帧数据怎么拿到?
两帧怎么对齐?
AXI4-Stream 接口怎么插进现有视频链路?
为什么一上来做红框经常黑屏、花屏或者没反应?
为什么"先做纯透传"比"直接上完整算法"更靠谱?

这篇文章就按我实际调试的顺序,把这件事完整复盘一下。

一、为什么先选帧间差分

在做基于 FPGA 的视频算法时,帧间差分几乎是最常见的入门方案之一。原因很现实:

第一,它实现简单。

只需要拿两帧做比较,不需要复杂的训练模型,也不需要特别大的资源开销。

第二,它适合流式处理。

视频流本身就是按像素逐点输入的,而帧差法本质上只是在同一位置比较"前后两帧的亮度差",很符合 FPGA 的流水线处理方式。

第三,它适合作为算法 IP 插入现有视频链路。

我现在的系统已经有:

bash 复制代码
摄像头视频输入
AXI4-Stream 视频通道
DDR 帧缓存
HDMI 显示输出

这意味着我不需要从零搭一个算法平台,只需要在 AXI4-Stream 链路中间插入自己的算法模块即可。

所以,这个实验的目标并不是一开始就做成很完美的目标检测,而是先完成一个能在 FPGA 视频链路中工作的运动检测原型。

二、算法模块在系统中的位置

在当前工程里,我插入的自定义模块叫做 AXI_VIP_Frame_Difference。从接口上看,它是一个典型的双输入、单输出 AXI4-Stream 处理模块:

bash 复制代码
s0_axis_*:主输入视频流
s1_axis_*:第二路输入视频流
m_axis_*:处理后的输出视频流

这两个输入在功能上并不是随便取的。

  1. s0_axis:当前视频流

这一路可以理解成当前时刻的图像数据,也就是显示主链路上正在传输的这一帧。

  1. s1_axis:延迟帧/参考帧

第二路数据不是摄像头再来一路,而是通过帧缓存和 FIFO 形成的"延迟图像"。在模块内部,s1_axis_tdata 会先写入 FIFO,然后在满足条件时读出,形成与 s0_axis 相比较的参考数据。当前代码里确实存在 fifo_data、fifo_wr、fifo_rd、fifo_q、fifo_empty、fifo_full 这些 FIFO 相关逻辑。

  1. m_axis:算法处理后的输出

处理完成后,模块再把输出视频流送回显示链路。这样,算法调试的结果就能直接在 HDMI 上可视化。

从系统结构上看,这样做的最大好处是:

算法模块本身既不需要关心摄像头,也不需要关心显示器,它只处理 AXI4-Stream 视频流。

三、模块内部的基本处理流程

虽然帧间差分的思路不复杂,但在 FPGA 中实现,还是要把处理链一段一段地搭起来。

  1. 第二路数据先进入 FIFO

在当前版本代码中,s1_axis_tdata 会先被写入 video_fifo,再由读控制逻辑读出,输出为 fifo_q。这样做的目的,就是在时序上给第二路数据一个缓存和延迟。代码中 video_fifo u_video_fifo 的实例化和 fifo_wr_en / fifo_rd_en 控制逻辑已经把这一层搭起来了。

  1. 主路数据先打一拍再打第二拍

主路 s0_axis_tdata 在进入后续比较之前,先经过了两级寄存器延迟:

s0_axis_tdata_dly1

s0_axis_tdata_dly2

这一步一开始我没太在意,后来调试才发现,它很重要。因为 AXI4-Stream 视频流不是单纯一拍一拍送数据,中间还带有 tuser、tlast、tvalid 等控制信号,如果主路和第二路数据完全不对齐,后面的差分结果很容易出问题。

  1. RGB565 扩展为 RGB888

因为当前链路中统一使用的是 16 位 RGB565 格式,而后面的灰度转换模块 RGB888_YCbCr444 需要 24 位 RGB888 输入,所以在算法模块内部做了格式扩展。

当前代码里:

s0_axis_tdata_dly2 会被拆成 s0_r_8、s0_g_8、s0_b_8

fifo_q 会被拆成 s1_r_8、s1_g_8、s1_b_8

这一步可以理解成:

外部链路统一 16 位,模块内部为了做算法运算再恢复成 24 位颜色通道。

  1. RGB 转灰度 Y

做帧差时,没必要直接对 RGB 三个通道分别比较,最常见的做法是先转成亮度值 Y。

所以当前模块里实例化了两份 RGB888_YCbCr444:

S0_RGB888_YCbCr444

S1_RGB888_YCbCr444

分别对两路 RGB888 数据做转换,输出:

s0_img_y

s1_img_y

这一步之后,问题就从"比较三通道颜色差异"变成了"比较两个亮度值的差异"。

  1. 帧差判定

真正的帧差判断逻辑非常直接:

如果 s0_img_y > s1_img_y,就判断差值是否大于阈值

否则就判断 s1_img_y - s0_img_y 是否大于阈值

大于阈值则 frame_difference_flag = 1

否则置 0

在当前代码中,这一段已经写成了 frame_difference_flag 的 always 逻辑,而且阈值通过 Diff_Threshold 参数控制。

也就是说,这一阶段的算法核心其实就是:

bash 复制代码
两帧灰度值作差,大于阈值就认为该像素处发生了运动。

四、为什么我没有一开始就做红框

这是这次调试中最关键的经验之一。

一开始,我其实很自然地想:

既然能检测运动,那就直接框起来。

但实际做起来发现,红框版本经常出问题。原因不在于"框"这个结果本身,而在于:

bash 复制代码
两路数据要先对齐
差分结果要先稳定
控制信号和像素坐标也要对齐
只要其中任何一层没对上,框就会乱飘、误检、漏检,甚至直接黑屏

所以后来我的策略变成了:

先做"纯透传"

先让这个自定义算法模块只做一件事:

把 s0_axis 原样送到 m_axis。

也就是说:

bash 复制代码
不做 FIFO
不做灰度
不做差分
不做框选

只验证:

bash 复制代码
这个模块插进系统后,视频流还能不能正常过
AXI4-Stream 接口接法有没有问题

从后面的调试结果看,这一步非常有必要。因为纯透传能显示,就说明至少:

bash 复制代码
模块壳子没问题
AXI4-Stream 上下游连接没问题
不是"模块一插进去就坏"

再做"运动区域标红"

在确认纯透传没问题之后,我没有立刻恢复红框,而是先做了一个更简单的可视化版本:

bash 复制代码
如果 frame_difference_flag = 1
就把当前输出像素改成红色
否则仍然输出原图

这一版对应的输出逻辑,在现有代码历史版本里就能看到:if(frame_difference_flag) m_axis_tdata <= 16'hF800; else m_axis_tdata <= s0_axis_tdata;。

这一步的好处非常明显:

1、能立刻看见差分结果是否真的起作用

你一动,画面哪里变红,哪里就是算法判定为"有运动"的区域。

2、比红框版本稳很多

因为它不依赖坐标统计,不依赖外接矩形,不依赖框选逻辑,只依赖单个像素的差分判断。

3、能把问题拆小

如果运动区域标红已经能工作,那么后续框选的问题就不是"差分没做出来",而是"框选逻辑没处理好"。

这一步后来事实证明是非常关键的。因为到了这个阶段,系统已经能做到:

bash 复制代码
视频正常显示
插入算法模块后仍能显示
移动物体时,运动区域能实时变红

这就说明帧差法本身是已经跑起来的。

五、为什么帧差法在 FPGA 里比想象中更难调

很多人会觉得,帧差法不就是减法和比较吗,为什么会调这么久?

真正做下来以后,我觉得难点主要不在算法公式,而在于视频流系统的时序和工程细节。

  1. 数据要对齐

主路数据经过 s0_axis_tdata_dly1、s0_axis_tdata_dly2 延迟,第二路数据经过 FIFO 输出 fifo_q,后面还要经过 RGB 转换和灰度转换。任何一级拍数没对上,都会导致:

bash 复制代码
差分不稳定
有时候有反应,有时候没反应
甚至画面本身出问题
  1. 控制信号也要对齐

AXI4-Stream 视频流里,真正决定帧起始、行结束、数据有效的不是 tdata 本身,而是:

bash 复制代码
tuser
tlast
tvalid

所以后面如果要做坐标计数和框选,就不能只盯着数据,还得让这些控制信号一起对齐。现有一些历史版本代码里,也确实加入了 s0_axis_tuser_dly、s0_axis_tlast_dly、s0_axis_tvalid_dly 这套延迟控制逻辑,以及后续的 x_cnt / y_cnt 坐标统计。

  1. 阈值要调

差分阈值太小,会把噪声也判成运动;

差分阈值太大,又会把真实的小目标运动滤掉。

所以实际调试时,阈值不是拍脑袋写一个数字就结束,而是要结合:

bash 复制代码
摄像头噪声
光照变化
背景复杂程度
目标大小和运动速度

慢慢调。

  1. "会动"不等于"能稳定框出来"

这一点是我后来最深的体会。

运动区域标红能做出来,并不代表红框就能立刻稳定。因为框选不是单点判断,而是要把整帧中所有运动点统计成一个区域,再画成外接矩形。这个过程对时序、噪声和参数都非常敏感。

所以如果一开始就直奔"稳定目标框选",往往很容易把问题搞复杂;反而是先做"纯透传"和"运动区域标红",更容易把整个系统一步步稳住。

六、当前阶段的实验结论

到目前这个阶段,这个项目已经完成了一个很重要的里程碑:

bash 复制代码
自定义 AXI4-Stream 算法模块已经能成功插入视频链路
双路视频数据已经能在模块内部建立起比较关系
FIFO、RGB565 扩展、灰度转换和帧差逻辑都已经搭起来
运动区域标红已经能够作为算法可视化结果输出

这意味着,当前系统已经不是单纯"能显示视频",而是已经具备了做视频运动检测实验的能力。

当然,这个阶段还不是最终版。因为"区域标红"只能说明哪里发生了变化,并不能很好地表达"目标到底在哪里"。后续如果想更直观地展示结果,就需要进一步做:

bash 复制代码
坐标计数
运动区域统计
外接矩形框选
甚至更进一步的背景建模或形态学处理

但不管后面怎么扩展,这一阶段的意义都非常大:

至少证明了帧差法已经真正在 FPGA 视频系统里跑起来了。

七、这一步最大的经验

如果让我总结这篇实验最有价值的经验,那就是:算法调试不要一口气把所有功能都塞进去。

我这次调试真正走通的顺序是:

bash 复制代码
先让视频链路跑通
再插入算法模块做纯透传
再加 FIFO 和双路输入
再加灰度和差分标志
最后才开始做可视化和框选

这个顺序表面上看慢,实际上是最快的。

因为视频算法一旦和 AXI4-Stream、VDMA、显示时序耦合在一起,问题来源会非常多。只有把问题一层层拆开,才能真正知道是哪一层出了问题。

相关推荐
前端双越老师2 小时前
为什么说 OpenClaw 应该装在自己的电脑上
人工智能·agent·全栈
小陈工2 小时前
2026年4月5日技术资讯洞察:AI商业模式变革、知识管理革命与开源生态反击
开发语言·人工智能·python·安全·oracle·开源
QYR-分析2 小时前
MPPT控制器行业解析:技术迭代与市场机遇前瞻
大数据·人工智能
A尘埃2 小时前
深度学习之卷积神经网络CNN(卷积+池化)
人工智能·深度学习·cnn
pzx_0012 小时前
【Pytorch】nn.Embedding函数详解
人工智能·pytorch·embedding
老成说AI2 小时前
SOUNDVIEW视频翻译:SHARK吸尘器如何靠TIKTOK打破高客单魔咒?
人工智能·跨境电商·tiktok·soundview
ByteX2 小时前
AI Coding
人工智能
fei_sun2 小时前
时序逻辑电路设计基础
fpga开发
jiajia_lisa2 小时前
科技暖民心,通行更便捷——车牌识别赋能民生出行
大数据·人工智能