文章目录
-
- 概要
- 一、为什么先选帧间差分
- 二、算法模块在系统中的位置
- 三、模块内部的基本处理流程
- 四、为什么我没有一开始就做红框
- [五、为什么帧差法在 FPGA 里比想象中更难调](#五、为什么帧差法在 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_*:处理后的输出视频流
这两个输入在功能上并不是随便取的。
- s0_axis:当前视频流
这一路可以理解成当前时刻的图像数据,也就是显示主链路上正在传输的这一帧。
- s1_axis:延迟帧/参考帧
第二路数据不是摄像头再来一路,而是通过帧缓存和 FIFO 形成的"延迟图像"。在模块内部,s1_axis_tdata 会先写入 FIFO,然后在满足条件时读出,形成与 s0_axis 相比较的参考数据。当前代码里确实存在 fifo_data、fifo_wr、fifo_rd、fifo_q、fifo_empty、fifo_full 这些 FIFO 相关逻辑。
- m_axis:算法处理后的输出
处理完成后,模块再把输出视频流送回显示链路。这样,算法调试的结果就能直接在 HDMI 上可视化。
从系统结构上看,这样做的最大好处是:
算法模块本身既不需要关心摄像头,也不需要关心显示器,它只处理 AXI4-Stream 视频流。
三、模块内部的基本处理流程
虽然帧间差分的思路不复杂,但在 FPGA 中实现,还是要把处理链一段一段地搭起来。
- 第二路数据先进入 FIFO
在当前版本代码中,s1_axis_tdata 会先被写入 video_fifo,再由读控制逻辑读出,输出为 fifo_q。这样做的目的,就是在时序上给第二路数据一个缓存和延迟。代码中 video_fifo u_video_fifo 的实例化和 fifo_wr_en / fifo_rd_en 控制逻辑已经把这一层搭起来了。
- 主路数据先打一拍再打第二拍
主路 s0_axis_tdata 在进入后续比较之前,先经过了两级寄存器延迟:
s0_axis_tdata_dly1
s0_axis_tdata_dly2
这一步一开始我没太在意,后来调试才发现,它很重要。因为 AXI4-Stream 视频流不是单纯一拍一拍送数据,中间还带有 tuser、tlast、tvalid 等控制信号,如果主路和第二路数据完全不对齐,后面的差分结果很容易出问题。
- 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 位颜色通道。
- RGB 转灰度 Y
做帧差时,没必要直接对 RGB 三个通道分别比较,最常见的做法是先转成亮度值 Y。
所以当前模块里实例化了两份 RGB888_YCbCr444:
S0_RGB888_YCbCr444
S1_RGB888_YCbCr444
分别对两路 RGB888 数据做转换,输出:
s0_img_y
s1_img_y
这一步之后,问题就从"比较三通道颜色差异"变成了"比较两个亮度值的差异"。
- 帧差判定
真正的帧差判断逻辑非常直接:
如果 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 里比想象中更难调
很多人会觉得,帧差法不就是减法和比较吗,为什么会调这么久?
真正做下来以后,我觉得难点主要不在算法公式,而在于视频流系统的时序和工程细节。
- 数据要对齐
主路数据经过 s0_axis_tdata_dly1、s0_axis_tdata_dly2 延迟,第二路数据经过 FIFO 输出 fifo_q,后面还要经过 RGB 转换和灰度转换。任何一级拍数没对上,都会导致:
bash
差分不稳定
有时候有反应,有时候没反应
甚至画面本身出问题
- 控制信号也要对齐
AXI4-Stream 视频流里,真正决定帧起始、行结束、数据有效的不是 tdata 本身,而是:
bash
tuser
tlast
tvalid
所以后面如果要做坐标计数和框选,就不能只盯着数据,还得让这些控制信号一起对齐。现有一些历史版本代码里,也确实加入了 s0_axis_tuser_dly、s0_axis_tlast_dly、s0_axis_tvalid_dly 这套延迟控制逻辑,以及后续的 x_cnt / y_cnt 坐标统计。
- 阈值要调
差分阈值太小,会把噪声也判成运动;
差分阈值太大,又会把真实的小目标运动滤掉。
所以实际调试时,阈值不是拍脑袋写一个数字就结束,而是要结合:
bash
摄像头噪声
光照变化
背景复杂程度
目标大小和运动速度
慢慢调。
- "会动"不等于"能稳定框出来"
这一点是我后来最深的体会。
运动区域标红能做出来,并不代表红框就能立刻稳定。因为框选不是单点判断,而是要把整帧中所有运动点统计成一个区域,再画成外接矩形。这个过程对时序、噪声和参数都非常敏感。
所以如果一开始就直奔"稳定目标框选",往往很容易把问题搞复杂;反而是先做"纯透传"和"运动区域标红",更容易把整个系统一步步稳住。
六、当前阶段的实验结论
到目前这个阶段,这个项目已经完成了一个很重要的里程碑:
bash
自定义 AXI4-Stream 算法模块已经能成功插入视频链路
双路视频数据已经能在模块内部建立起比较关系
FIFO、RGB565 扩展、灰度转换和帧差逻辑都已经搭起来
运动区域标红已经能够作为算法可视化结果输出
这意味着,当前系统已经不是单纯"能显示视频",而是已经具备了做视频运动检测实验的能力。
当然,这个阶段还不是最终版。因为"区域标红"只能说明哪里发生了变化,并不能很好地表达"目标到底在哪里"。后续如果想更直观地展示结果,就需要进一步做:
bash
坐标计数
运动区域统计
外接矩形框选
甚至更进一步的背景建模或形态学处理
但不管后面怎么扩展,这一阶段的意义都非常大:
至少证明了帧差法已经真正在 FPGA 视频系统里跑起来了。
七、这一步最大的经验
如果让我总结这篇实验最有价值的经验,那就是:算法调试不要一口气把所有功能都塞进去。
我这次调试真正走通的顺序是:
bash
先让视频链路跑通
再插入算法模块做纯透传
再加 FIFO 和双路输入
再加灰度和差分标志
最后才开始做可视化和框选
这个顺序表面上看慢,实际上是最快的。
因为视频算法一旦和 AXI4-Stream、VDMA、显示时序耦合在一起,问题来源会非常多。只有把问题一层层拆开,才能真正知道是哪一层出了问题。