FPGA实战:帧间差分法实现移动目标检测与目标框定

📅 发布时间:2026/9/10 11:39:28
FPGA实战:帧间差分法实现移动目标检测与目标框定
简介面向实时视频监控与智能交通等场景基于FPGA帧间差分法的移动目标检测与框定完整工程方案包含Vivado源码与配套脚本。方案通过连续两帧像素差分、阈值判断与连通域分析提取运动区域并以FPGA并行流水线加速处理适配高帧率、低延迟应用相比背景差分法它对光照变化更稳健存储开销也更小。压缩包共2000个文件约216MB涵盖416个v、69个vhdl、34个vhd、20个vh等Verilog/VHDL源码48个xdc和1个ucf约束文件32个xci等IP配置以及大量sh、tcl、do、bat工程与仿真脚本便于直接打开工程、综合布线并仿真验证bit/bin等文件还可用于上板测试。已有110人学习/下载资源包含仿真运行脚本、时序报告和完整目录结构可帮助接触者快速上手适合FPGA开发者、监控系统研究人员学习参考。1. FPGA帧间差分法做移动目标检测先解决“能不能实时”的问题做视频监控或工业视觉的人第一反应是用背景建模或光流法但到了FPGA上帧间差分法反而是最容易跑通、也最容易被低估的方案。它不需要训练模型不需要存背景帧甚至不需要浮点运算只要把两帧灰度图的像素差值和阈值比较就能得到运动区域的二值掩码。而“框定”这个动作才是把差分结果变成真正可用信息的核心——它要求硬件在几毫秒内完成连通域分析画出边界框再叠加到输出视频上。这篇文章直接回答三个问题差分和框定在FPGA里怎么写、怎么调参数、怎么避开那些让人熬夜的坑。适合已经能跑通Hello World级的FPGA开发但第一次做图像处理项目的工程师。2. 帧间差分法的数学原理与FPGA并行化切分2.1 差分、阈值、形态学三个算子的硬件映射帧间差分法的公式极其简单D(x,y) |I_t(x,y) - I_{t-1}(x,y)|然后对D做阈值T大于T的像素判为前景。这个公式在CPU上是一个双重for循环在FPGA上却可以映射成三级流水线像素时钟驱动下前一级读出当前帧数据后一级读出上一帧数据两者相减后经过比较器输出二值信号。真正需要设计的是上一帧数据从哪来。最直接的办法是用双帧缓冲一帧写入DDR3一帧从DDR3读出。但这样延迟至少是一帧时间而且DDR带宽要同时满足写、读、和显示器刷新很容易成为瓶颈。另一种常见做法是只存上一帧的灰度图用FPGA内部的BRAM或URAM做环形缓冲代价是分辨率受限。以1080p灰度图为例一帧约2MB而中端FPGA如Artix-7 200T的BRAM总量不到2MB需要外挂DDR。阈值T不是一个全局常量。光照变化会导致整体灰度漂移常见做法是用当前帧的平均亮度做自适应阈值每行统计灰度累加值行结束时求出均值再乘个系数作为下一行的阈值。这个系数一般在1.5到2.5之间可以在运行时通过AXI-Lite寄存器修改。形态学操作是差分后必加的步骤。因为差分得到的二值图有很多孤立噪点先做一次3x3腐蚀去掉单像素噪声再做一次3x3膨胀把断裂的轮廓连起来。在FPGA里3x3窗口用两行LineBuffer加三个寄存器组实现延迟只有两个行周期。注意腐蚀和膨胀是组合逻辑可以用同一个窗口计算单元复用只要改变比较逻辑——腐蚀取窗口最小值膨胀取最大值。2.2 行缓存与帧缓存用DDR3还是用BRAM这是第一个要拍板的设计决策。我的经验是如果目标分辨率小于等于720p帧率30fps且FPGA内部BRAM够大优先用BRAM做双帧缓冲。原因很简单——延迟低实现简单不需要DDR控制器。但BRAM方案有个隐藏问题上一帧和当前帧的读写端口冲突。例如当前帧写入BRAM的同时还要读出上一帧数据来做差分这需要双端口RAM而且两个端口分别接写地址和读地址。用DDR3的话建议采用“写一帧读一帧”的策略当前帧写入DDR的同时从另一块地址读出上一帧。这样可以把DDR带宽分成两个方向。以1080p30为例一帧2MB30fps就是60MB/s写入再加上60MB/s读出总共120MB/sDDR3的带宽轻松满足。但要注意DDR控制器的效率尽量用突发长度大的AXI接口每次读写至少64字节不要做单像素随机访问。帧缓冲的存储格式也影响资源。如果直接存RGB888一帧1920x1080需要约6.2MBDDR浪费一半以上带宽。常见做法是先把RGB转成灰度Y通道只存8bit灰度这样一帧只需要2MB而且差分运算只需要灰度值。RGB转灰度的系数固定Y 0.299R 0.587G 0.114BFPGA里用三个乘法器加两个加法器系数以定点数表示比如0.299用9bit近似为0x4C0.2988。2.3 流水线结构从灰度到二值图的最小时延整个图像处理链的流水线设计直接决定系统延迟。我常用的流水线分为五级第一级像素输入与RGB转灰度。第二级当前帧写入行缓存同时从上一帧行缓存读出差分。第三级差分结果与自适应阈值比较输出二值图。第四级3x3中值滤波去除椒盐噪声。第五级腐蚀膨胀形态学处理输出干净的前景掩码。每一级之间用FIFO或寄存器打拍保证数据流连续。这里有一个关键点上一帧的读取必须比当前帧写入晚一拍因为差分需要的是“当前帧像素”和“上一帧对应位置像素”同时有效。由于两帧数据来自不同的存储介质BRAM或DDR这个同步要在读数据返回后做对齐。简单做法是把上一帧数据也存入一个行缓冲和当前帧像素以同样的时钟节奏进入差分模块。用Xilinx Vitis HLS写会更省事但纯Verilog更容易控制时序。下面这段代码是差分模块的核心用AXI-Stream接口输入两路灰度流输出二值流module frame_diff #( parameter WIDTH 8, parameter THRESHOLD 32 )( input clk, input rst_n, input [WIDTH-1:0] cur_pixel, input [WIDTH-1:0] prev_pixel, input pixel_valid, output reg diff_out, output reg diff_valid ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin diff_out 0; diff_valid 0; end else if (pixel_valid) begin diff_out (cur_pixel prev_pixel) ? (cur_pixel - prev_pixel THRESHOLD) : (prev_pixel - cur_pixel THRESHOLD); diff_valid 1; end else begin diff_valid 0; end end endmodule这段代码把绝对差的计算转换成两个条件比较避免使用乘法和除法。THRESHOLD是编译期参数实际运行时可以用寄存器替代做成axis_register接口动态调整。diff_valid信号是单帧有效标志后面接的形态学模块必须用这个信号做流控否则会产生像素错位。3. 在Vivado里实现FPGA帧间差分模块的完整步骤3.1 创建IP核与引脚约束如果你的设计基于Xilinx平台建议用Vivado的IP Integrator或者纯RTL工程。首先建立工程选择芯片型号比如Artix-7 xc7a200tfbg484。然后添加两个IP一个Video Timing ControllerVTC用来生成同步信号一个Frame Buffer IP用来管理DDR读写。Frame Buffer IP在Vivado里叫Video Frame Buffer它支持AXI4-Stream输入输出内部自动处理行同步、场同步和奇偶场问题。引脚约束需要特别注意摄像头输入引脚的电平标准。如果是LVDS接口要用set_property IOSTANDARD LVDS_25如果是并行RGB接口则用set_property IOSTANDARD LVCMOS33。时钟方面像素时钟要单独约束一般用create_generated_clock从MMCM输出派生。这里有一个容易踩的坑如果你使用MIPI CSI-2接口的摄像头需要额外的MIPI D-PHY IP核将串行数据转成并行像素流。FPGA实现MIPI接收是另一个话题但在这个项目里建议先用开发板自带的HDMI输入或并行摄像头接口把精力放在差分算法上。等算法调通再换MIPI也不迟。3.2 Verilog代码差分阈值膨胀腐蚀差分模块上面已经给了。现在补上3x3形态学模块。这个模块需要两行缓存每行缓存宽度是图像宽度深度为1字节。用FPGA的Shift Register IPrambasedshiftreg可以节省LUT。下面给出一个完整的腐蚀模块膨胀模块只需把最小值改成最大值module erode_3x3 #( parameter WIDTH 8, parameter IMG_W 1920 )( input clk, input rst_n, input [WIDTH-1:0] pixel_in, input valid_in, output reg [WIDTH-1:0] pixel_out, output reg valid_out ); // 两行延迟线 reg [WIDTH-1:0] line_buf [0:IMG_W-1]; // 实际工程里用BRAM实现 reg [WIDTH-1:0] row0_col0, row0_col1, row0_col2; reg [WIDTH-1:0] row1_col0, row1_col1, row1_col2; reg [WIDTH-1:0] row2_col0, row2_col1, row2_col2; reg [WIDTH-1:0] shift_0, shift_1; // 行延迟 always (posedge clk) begin if (valid_in) begin shift_0 pixel_in; line_buf[0] shift_0; // 第一行延迟 end end // 窗口像素提取简化示意 always (posedge clk) begin if (valid_in) begin row0_col2 shift_0; row0_col1 row0_col2; row0_col0 row0_col1; row1_col2 line_buf[0]; row1_col1 row1_col2; row1_col0 row1_col1; row2_col2 line_buf[1]; row2_col1 row2_col2; row2_col0 row2_col1; end end // 3x3最小值 reg [WIDTH-1:0] min_val; always (*) begin min_val row0_col0; if (row0_col1 min_val) min_val row0_col1; if (row0_col2 min_val) min_val row0_col2; if (row1_col0 min_val) min_val row1_col0; if (row1_col1 min_val) min_val row1_col1; if (row1_col2 min_val) min_val row1_col2; if (row2_col0 min_val) min_val row2_col0; if (row2_col1 min_val) min_val row2_col1; if (row2_col2 min_val) min_val row2_col2; end always (posedge clk) begin if (valid_in) begin pixel_out min_val; valid_out 1; end else begin valid_out 0; end end endmodule注意上面的代码是行为级示意实际中line_buf必须用rambasedshiftreg或者Block RAM的原语例化不能直接写数组否则综合会变成大量寄存器。另外窗口像素的提取时序需要对齐——row0_col2是当前像素row0_col1是左侧像素以此类推。valid_out要等窗口填满后才拉高否则前两行的数据是无效的。形态学之后二值图就可以用于连通域标记了。但在此之前建议加一个中值滤波因为差分噪点往往是单像素的白点或黑点中值滤波的3x3窗口和形态学窗口可以复用只需要额外比较逻辑。3.3 用AXI-Stream接口对接摄像头与HDMI显示整个系统的数据通路是摄像头像素流 → 帧差分模块 → 形态学模块 → 连通域标记 → OSD叠加 → HDMI输出。为了让这些模块互联用AXI-Stream是最好的选择。每个模块输入输出都是tvalid/tready/tdata/tlast信号tlast表示行结束这样模块之间可以自动反压。这里提供一个实用的AXI-Stream写法的差分模块接口module diff_axis #( parameter DATA_WIDTH 8 )( input clk, input rst_n, input [DATA_WIDTH-1:0] s_axis_cur_tdata, input s_axis_cur_tvalid, output s_axis_cur_tready, input [DATA_WIDTH-1:0] s_axis_prev_tdata, input s_axis_prev_tvalid, output s_axis_prev_tready, output [DATA_WIDTH-1:0] m_axis_tdata, output m_axis_tvalid, input m_axis_tready );tready的处理是关键。如果下游模块比如形态学处理速度慢tready被拉低当前模块必须暂停读取上游数据同时保持内部状态。简单做法是用一个FIFO缓冲输入FIFO的空满信号控制tready。这样即使下游反压也不会丢帧。如果不用FIFO那么tready必须同时看两路输入的tvalid和下游的m_axis_tready逻辑上稍有差错就会死锁。HDMI输出端的OSD叠加是框定显示的最后一步。OSD模块本质上是一个简单画框电路当像素坐标落在某个矩形框内时将像素值强制设为特定颜色。这个电路不需要存储只需要比较器。比较器的输入是当前像素的行列计数器和四个边界寄存器。为了不让画框覆盖画面内容可以只在边框的四个边宽范围内改变颜色而且通常用异或方式产生高亮效果。4. 目标框定连通域标记与边界框提取4.1 连通域标记的FPGA实现策略二值图出来后移动目标是一堆白色像素块。要画框需要知道每个目标的外接矩形范围。最简单的方法是连通域标记Connected Component Labeling传统算法用两遍扫描加等价类合并在CPU上容易实现但在FPGA上直接做两遍扫描需要存储整个标签图和一个大的等价类表资源消耗大。工程上更常用的是单遍扫描加游程编码。把每行二值图转成若干段“连续白像素”的游程Run每个游程记为(行号, 起始列, 结束列)。然后只对相邻行的游程做重叠判断如果重叠就把它们归为同一个目标并更新目标的边界框。这个做法避免了逐像素标注数据量从像素级降到区域级非常适合硬件。游程编码在FPGA里实现很方便用一个计数器统计当前连续白像素的起始列当遇到白像素变黑或行结束时输出一个游程包。游程包通过一个FIFO送给合并模块。合并模块维护一个目标列表每个目标有min_row, max_row, min_col, max_col四个寄存器。处理一帧图像时目标列表需要上一帧的遗留状态吗不需要。帧间差分法天然会把静止目标消除掉所以每帧的目标列表从零开始上一帧的数据在帧结束时清空。这也意味着帧间目标关联不是差分法本身能完成的需要后面接卡尔曼滤波或IOU匹配。4.2 游程编码合并与边界框更新合并逻辑的伪代码描述实际用状态机实现读取当前行的一个游程(y, col_start, col_end)。读取上一行已保存的游程列表检查是否有游程的col_start col_end gap且col_end col_start - gap。这里的gap是容错间隙防止目标因形态学处理后出现细微断裂。如果匹配到多个上一行游程说明两个目标在当前行合并了实际多目标粘连时发生则将这些目标合并为一个更新外接框为所有匹配目标的并集。如果没有匹配说明新目标出现创建新目标。一行结束后所有被匹配过的目标标记为“活跃”未被匹配的目标可能在本帧消失但不立即删除而是等下一行继续尝试匹配——因为在行间目标可能由于运动产生1-2行的间断。边界框更新简单来说min_row min(min_row, y)max_row max(max_row, y)min_col min(min_col, col_start)max_col max(max_col, col_end)。如果用两个补码比较器加两个多路选择器就能完成不消耗太多逻辑。游程合并的硬件结构是两个FIFO一个存当前行游程一个存上一行游程。上一行游程在每帧开始时清空每处理完一行把当前行游程FIFO的内容整体搬到上一行FIFO。注意容量问题1080p图像一行最多约有960个游程黑白交替每个游程用64bit存储行号用11bit起止列各用11bit再加标记位一行FIFO深度设为1024就够。4.3 框定结果叠加到视频流的OSD电路连通域标记模块输出的是目标列表每个目标四个边界值。但列表是动态变化的数量不确定怎么传给输出端的OSD模块常见做法是把目标列表写入BRAM最多支持N个目标比如16个每个目标占24字节。帧同步信号vsync到来时清空BRAM随后连通域模块每发现一个新目标就写入空闲槽位。OSD模块在输出每一行时需要同时查所有16个目标的边界寄存器判断当前像素是否落在任一框内。这个判断用16组比较器并行做每组四个比较器即64个比较器代价很小。为了降低BRAM读写冲突推荐用双端口RAM一个端口写目标列表另一个端口读。但OSD模块每行要读16个目标的边界可以采用“行内缓存”的办法——在每行开始时把16个目标的上下左右边界全部读出锁存到寄存器组里后面像素比较只读寄存器不再访问BRAM。OSD输出颜色可以用目标ID区分也可以用统一的高亮绿色。现实项目中框的颜色往往要可配置所以OSD模块内部也有一组颜色寄存器通过AXI-Lite接口由CPU或软核设置。边框宽度默认是2像素可以配置成1到4像素。对于大目标边框宽度太小会不明显太大又会遮住目标内部细节一般设置成3像素。5. 参数调优与工程排错阈值、帧率、资源消耗5.1 三组必调参数差分阈值、形态学核大小、帧间隔差分阈值是影响检测灵敏度的首要参数。阈值太低背景噪声会被误判为运动目标阈值太高真实目标尤其是与背景颜色接近的人或车会被滤除。我通常的做法是让阈值可在线调节先设一个初值比如30/255然后观察ILA采样的二值图直方图。如果前景像素占比超过5%说明阈值过低如果一帧几乎没有前景说明阈值过高。注意差分的灰度是绝对值所以阈值范围是0到255。形态学核大小决定了轮廓恢复能力。3x3适合低速目标行人、车辆如果目标速度快帧间位移大目标会变成两个重影此时应该把阈值调大并把膨胀核加大到5x5。但核越大硬件资源开销呈平方增长5x5需要四行缓存相当于四个行缓冲BRAM对1080p来说就是4x1920x8bit约60Kbit还能接受。千万不要在资源紧张的芯片上盲目用7x7除非你用的是高云或安路的大资源FPGA。帧间隔是另一个被忽略的参数。标准视频流是逐帧差分即当前帧和上一帧做差。但如果目标移动很慢比如监控画面里的行人相邻两帧的位移可能只有1-2像素差分响应很弱。常见的做法是每隔N帧做一次差分比如隔3帧让目标位移更明显。在FPGA里实现帧间隔很简单不需要增加存储只需要给帧缓冲的写地址加一个偏移写第t帧时读第t-N帧。这个N可以通过寄存器配置但要注意N太大会导致运动目标丢失通常N取2到5。5.2 常见坑DDR带宽不足、行同步丢失、二值图闪烁DDR带宽不足的典型症状是画面撕裂、帧率下降、实时性变差。因为帧缓冲读写DDR是频繁的如果DDR同时又被OSD或其他任务占用就有了竞争。解决思路有三个一是把帧缓冲的位宽和突发长度调大比如AXI数据位宽设为128bit突发长度设为16二是只在帧消隐期做DDR刷新减少随机访问三是将上一帧灰度图缓存到FPGA内部的BRAM中差分时读BRAM不读DDR这需要分辨率不要超过BRAM容量。行同步丢失表现为图像花屏或错位原因是差分模块的两个输入流行对齐不一致。比如当前帧从摄像头来上一帧从DDR回来DDR的读延迟是变化的导致上一帧的第一行数据到达时当前帧已经输出了好几行。解决办法是在差分模块前面加一个行同步FIFO用上一帧数据的tlast信号来对齐两路流当两路流的tlast都到来时才允许下一行开始。具体可以用一个“等待两路valid”的状态机来做。二值图闪烁是指静止背景下目标区域边缘不断出现随机噪点导致框定结果抖动。原因往往是摄像头噪声和阈值临界。此时不是加大阈值这样会丢真实目标而是给差分结果加上“滞后比较”当前像素差值大于高阈值T_high则为前景小于低阈值T_low则为背景中间状态保持上一帧的结论。这就是滞回比较器在FPGA里只需要两个比较器和一个D触发器却能显著抑制边缘闪烁。5.3 用ILA和Vivado逻辑分析器验证时序调参不能靠猜必须看波形。Vivado的ILAIntegrated Logic Analyzer是调试利器。在差分模块的输出端口挂上ILA触发条件设为diff_valid上升沿捕获数据宽度建议至少64bit深度选1024或2048。观察点有三个第一差分模块两路输入的tvalid是否持续拉高有没有周期性停顿。如果停顿说明上游FIFO或DDR读带宽不够需要优化读策略。第二diff_out输出的二值图是否存在整行全0或全1的情况如果整行全1说明当前帧和上一帧明暗差异极大可能是摄像头曝光变化需要检查曝光控制。第三形态学模块输出的valid_out与输入valid_in之间是否恰好延迟两行加若干像素如果延迟抖动说明行缓存FIFO的读写指针有错位。还有一个重要验证手段是用SystemVerilog的断言在仿真中验证差分模块的时序关系。断言可以写成当valid_in拉高时prev_pixel与cur_pixel必须是同一坐标的像素。在仿真里加入随机延迟后断言能立刻暴露出对齐问题。6. 进阶结合卡尔曼滤波做多目标跟踪框定帧间差分法本身没有“记忆”它只知道当前帧哪里在动不知道这个目标上一帧在哪也无法在目标短暂消失或遮挡后保持框定。要让框定结果稳定低成本的方案是给每个目标框中心点接一个卡尔曼滤波预测下一帧位置再用预测位置和当前帧检测框做关联。卡尔曼滤波在FPGA里实现不会用矩阵库而是把状态量拆成标量运算。以目标中心的x坐标为例状态向量为x [px, vx]观测值为检测框中心x。预测方程为px px vx * dtvx vx。更新方程需要计算卡尔曼增益虽然公式里有除法和乘法但可以用定点数预先计算增益系数并在运行中近似。如果硬件资源紧张可以退化成最简单的α-β滤波器预测位置xp x_prev v*dt更新位置x_new xp α * (x_measured - xp)速度更新v_new v β * (x_measured - xp) / dt。α和β取0.6和0.2效果已经足够好。关联逻辑用IOU交并比匹配。FPGA里计算两个矩形IOU只需要比较器和乘法器。匹配策略采用贪心法对于当前帧的每个检测框找上一帧所有目标框中IOU最大的那个如果IOU大于0.3就认为是同一目标否则作为新目标创建跟踪器。上一帧的跟踪器如果连续3帧没有被匹配就删除。这套机制用状态机实现每帧处理时间在1ms以内不阻塞流水线。框定显示时也可以用卡尔曼滤波的预测框代替原始检测框这样可以消除框的抖动。做法很简单OSD模块画框时用的边界值不是连通域模块直接输出的原始值而是卡尔曼滤波更新后的值。滤波后的框中心更平滑宽高也可以做平滑w_new w_prev γ * (w_measured - w_prev)γ取0.3。在FPGA上把卡尔曼滤波接进流水线需要注意时序连通域标记模块在帧末尾才能输出全部目标框而卡尔曼更新必须等这些框全部产生因此整个跟踪模块工作在“帧间”而不是“像素级”。设计时把跟踪模块放在DDR帧缓冲之后与OSD模块之间用AXI-Lite寄存器接口或BRAM邮箱传递目标列表即可。这样帧间差分负责“看见运动”卡尔曼滤波负责“记住运动”两者结合才算一个完整的移动目标检测和框定系统。本文还有配套的精品资源点击获取