ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FPGA运动目标检测:基于OV7670与帧差法的实时硬件实现

2026/9/10 10:43:20 拓冰建站 浏览量
FPGA运动目标检测:基于OV7670与帧差法的实时硬件实现 简介面向FPGA学习者和电子类毕业设计学生这套压缩包以小梅哥AC620开发板为核心搭配TFT液晶显示与OV7670/OV7725摄像头实现基于帧差法的实时运动目标检测。工程提供完整Quartus项目源码包含Verilog与TDF设计文件、仿真激励、引脚约束、编译报告及演示图片/视频可帮助理清从图像采集、帧差运算到TFT显示输出的完整链路。资源共2000个文件虽然以工程编译中间文件cdb、hdb、rdb等为主但核心源码与文档清晰可辨v/tdf为硬件设计、txt为说明注释、mif用于存储器初始化、sof为FPGA配置文件另有m脚本与mp4实操演示辅助学习。压缩包约212.54MB已有1207人学习下载目录结构清晰适合需要快速搭建FPGA视频处理平台、进行运动检测实验或作为毕设参考的开发者省去从零搭建工程的繁琐过程也可直接对照源码修改参数便于二次开发和功能扩展。1. 拿着OV7670做运动检测为什么最后都得回到FPGA如果你在STM32F407上做过OV7670采集大概会记得那个尴尬DMA搬运一帧320x240的RGB565数据到内存再做一次帧差帧率直接掉到15fpsCPU占用拉满风扇都开始转。换成AC620上的FPGA之后同样用OV7670像素数据在PCLK节拍下逐点流过逻辑帧差运算被拆成流水线整帧延迟只比行扫描多几个时钟TFT上可以实时显示运动目标。这就是这套小梅哥AC620 TFT OV7670/OV7725运动目标检测工程的价值摄像头配置、像素同步、帧缓存、差分算法、TFT显示全部用Verilog一条链路打通配套ModelSim仿真和上板工程适合作为毕设框架也适合想搞懂图像处理硬件化的工程师用来做算法落地验证。2. 硬件链路与像素时序AC620 与 OV7670/OV7725 的 DVP 对接2.1 选型差异OV7670 不带 FIFOOV7725 带 FIFO 时的工程区别OV7670和OV7725都是30万像素COMS摄像头输出接口都是DVP但工程中两者处理方式不一样。OV7670不带FIFOPCLK和行场同步完全由FPGA实时接收这意味着FPGA必须严格跟上每一拍像素OV7725有带FIFO的版本可以用帧突发方式读出MCU侧更友好但FPGA侧其实没必要依赖它的FIFO直接按DVP时序读取即可反而可以减少FIFO控制的复杂度。AC620开发板上有OV7670/OV7725二合一的摄像头接口引脚分配固定常见接法是PCLK - FPGA全局时钟引脚VSYNC - 普通IO需要做电平同步HREF - 普通IOD[7:0] - 8位数据引脚注意DVP信号从摄像头出来后没有经过时序约束直接用PCLK采样时要保证数据比PCLK边沿提前足够时间建立。我一般会把PCLK做一次240MHz以下时钟域的同步打拍再把数据延迟一拍匹配避免在高速模式下采集到亚稳态数据。AC620上TFT彩屏通常是320x240分辨率RGB565接口因此OV7670/OV7725也配置为QVGA分辨率输出这样一帧数据量是320x240x16bitBRAM可以完整放下做帧差也无需行缓存拼接。2.2 SCCB 寄存器配置OV7670 初始化序列与读写时序SCCB是兼容I2C的串行配置总线OV7670有上百个寄存器但运动检测工程中真正关键的就几个输出格式、分辨率、PCLK分频、时序偏移。下面是一套常见的OV7670 QVGA RGB565初始化配置表实际应用中可以根据TFT偏色情况调整寄存器地址写入值作用0x120x80软件复位0x120x00恢复正常输出0x400x10RGB565输出0x110x01PCLK分频系数0x1E0x27水平窗口高字节0x1F0x02水平窗口低字节0x170x13HREF起始高0x180x01HREF结束低0x320xB6行长配置0x190x02VSYNC配置0x1A0x7C垂直同步偏移这里0x1E/0x1F和0x17/0x18组合决定有效像素区域不能照搬全部初始化序列需要配合上电时序分步写入。常见做法是先复位摄像头等待至少5ms然后写0x120x80再等待5ms最后按顺序写其余寄存器。SCCB每次写操作都是“起始位 从机地址 寄存器地址 数据 停止位”FPGA中可以用一个状态机复现这个时序。// sccb_cfg.v - 简易SCCB配置状态机单次写入 module sccb_cfg ( input wire clk_50m, input wire rst_n, output reg sio_c, // SCCB时钟 output reg sio_d, // SCCB数据 output reg idle ); // 状态定义 localparam IDLE 3d0; localparam START 3d1; localparam ADDR 3d2; localparam REGADR 3d3; localparam DATA 3d4; localparam stop 3d5; reg [3:0] bit_cnt; reg [15:0] clk_cnt; // 写入数据由外部传入这里以固定寄存器为例 reg [7:0] reg_addr 8h12; reg [7:0] reg_data 8h80; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin sio_c 1b1; sio_d 1b1; state IDLE; end else begin // 实际工程中需要按节拍切换状态生成START、ACK、STOP end end endmodule这段代码只给出框架不是完整逻辑因为SCCB时序的关键是每一拍sio_c的翻转和sio_d的建立保持。实际工程里建议直接用三段式状态机IDLE下产生起始位发送8位从机地址0x42等ACK再发寄存器地址和数据。配置OV7725时从机地址通常是0x42寄存器地址空间和OV7670大体兼容但PCLK分频和窗口偏移需要查数据手册重新填不能直接复用OV7670的值。2.3 像素拼接与 TFT 彩屏时序匹配OV7670在RGB565模式下每个像素通过两个PCLK周期输出先高字节后低字节。如果直接用8位数据去做帧差会得到错误结果。正确做法是拉高HREF后用两个PCLK收集一个16位像素。我一般用4个周期的移位寄存器第一个周期存高8位第二个周期拼成pixel同时产生px_valid。这样后续的差分模块只需要关心px_valid不用再管字节拆分。// pixel_stitch.v - RGB565像素拼接 module pixel_stitch ( input wire pclk, input wire rst_n, input wire href, input wire [7:0] din, output reg [15:0] pixel, output reg px_valid ); reg byte_flag; always (posedge pclk or negedge rst_n) begin if (!rst_n) begin byte_flag 1b0; pixel 16d0; px_valid 1b0; end else if (href) begin if (!byte_flag) begin pixel[15:8] din; byte_flag 1b1; px_valid 1b0; end else begin pixel[7:0] din; byte_flag 1b0; px_valid 1b1; // 每两个PCLK出一个RGB565像素 end end else begin byte_flag 1b0; px_valid 1b0; end end endmodule逻辑说明byte_flag指示当前接收的是高字节还是低字节。href有效期间第一个PCLK写入高字节第二个PCLK写入低字节并拉高px_valid。注意px_valid只持续一个PCLK后续帧差模块用这个信号作为写使能和计算使能。TFT彩屏侧需要另一个时钟域。常见做法是用PLL产生25MHz像素时钟FPGA在时钟上升沿输出RGB数据和行场同步信号。OV7670的PCLK在QVGA模式下可能超过24MHz如果直接拿PCLK驱动TFT会因为时序不匹配产生闪烁。稳妥方案是摄像头采集域用PCLK显示域用PLL产生的固定时钟中间用异步FIFO做缓存。这套工程里直接使用TFT的DE模式即只给DE、HSYNC、VSYNC和RGB数据TFT内部自动生成显示时序。注意AC620板上的TFT彩屏多数是RGB565接口背光引脚需要单独拉高否则屏幕不亮。3. 帧差法在 FPGA 上的并行实现3.1 帧差法公式和硬件化思路帧差法是最经济的运动目标检测算法表达式为D(x,y) |I_t(x,y) - I_{t-1}(x,y)|当D超过阈值时判定该像素属于运动目标。这个运算在FPGA上非常适合流水线化因为每个像素的计算互不依赖。难点不在减法而在“如何保存上一帧图像并保证当前像素和上一帧像素在同一个时钟周期对齐”。最直接的做法是用双端口RAM保存一帧数据。但要区分“当前帧写入”和“上一帧读取”不能把当前帧刚写进去的值立刻当成上一帧。我采用的方案是双bank缓存两个同样大小的RAM一个写当前帧另一个读上一帧帧同步信号到来时交换bank角色。这样既避免了阻塞写入也省去了帧结束后的搬移时间。3.2 双 bank 帧差分模块下面给出一个可综合的帧差核输入RGB565像素输出差分后的二值化标记。代码使用两个16位RAM每个大小320x240可以用AC620上的FPGA Block RAM直接映射。// frame_diff_top.v - 双bank帧差模块 module frame_diff_top #( parameter WIDTH 320, parameter HEIGHT 240, parameter PIX_WIDTH 16, parameter THRESHOLD 16d25 // 运动阈值根据环境调整 )( input wire clk, input wire rst_n, input wire frame_begin, // 帧同步脉冲 input wire [PIX_WIDTH-1:0] cur_pix, // 当前帧RGB565 input wire cur_valid, output reg diff_valid, output reg motion_pix // 1表示运动像素 ); localparam PIX_NUM WIDTH * HEIGHT; reg [15:0] waddr; reg [15:0] raddr; reg [PIX_WIDTH-1:0] ram0 [0:PIX_NUM-1]; reg [PIX_WIDTH-1:0] ram1 [0:PIX_NUM-1]; reg bank_w; // 当前写bank reg bank_r; // 当前读bank // 帧切换交换读写bank always (posedge clk or negedge rst_n) begin if (!rst_n) begin bank_w 1b0; bank_r 1b1; end else if (frame_begin) begin bank_w ~bank_w; bank_r ~bank_r; end end // 写当前帧 always (posedge clk) begin if (cur_valid) begin if (bank_w 1b0) ram0[waddr] cur_pix; else ram1[waddr] cur_pix; waddr waddr 1b1; end end // 读上一帧raddr延迟waddr一拍确保与当前像素对齐 always (posedge clk) begin raddr waddr; end wire [PIX_WIDTH-1:0] prev_pix; assign prev_pix (bank_r 1b0) ? ram0[raddr] : ram1[raddr]; // 计算灰度差再做阈值比较 reg [8:0] diff_r; reg [8:0] diff_g; reg [8:0] diff_b; always (posedge clk or negedge rst_n) begin if (!rst_n) begin diff_r 9d0; diff_g 9d0; diff_b 9d0; motion_pix 1b0; diff_valid 1b0; end else if (cur_valid) begin // 分别计算RGB各通道绝对差 diff_r (cur_pix[15:11] prev_pix[15:11]) ? {5d0, cur_pix[15:11] - prev_pix[15:11]} : {5d0, prev_pix[15:11] - cur_pix[15:11]}; diff_g (cur_pix[10:5] prev_pix[10:5]) ? {4d0, cur_pix[10:5] - prev_pix[10:5]} : {4d0, prev_pix[10:5] - cur_pix[10:5]}; diff_b (cur_pix[4:0] prev_pix[4:0]) ? {5d0, cur_pix[4:0] - prev_pix[4:0]} : {5d0, prev_pix[4:0] - cur_pix[4:0]}; // 三个通道差值之和超过阈值判为运动像素 if ((diff_r diff_g diff_b) THRESHOLD) motion_pix 1b1; else motion_pix 1b0; diff_valid 1b1; end else begin diff_valid 1b0; motion_pix 1b0; end end endmodule逻辑说明frame_begin来临时交换读写bank使当前新帧数据写入空闲RAM上一帧数据从另一个RAM读出。waddr随着cur_valid累加raddr延迟一拍因此prev_pix正是在当前像素在RAM中的前一帧内容。差分计算没有转灰度直接求R、G、B三个分量的绝对差之和这样能保留颜色变化信息对阴影下的运动目标更敏感。THRESHOLD25是我在室内日光灯下的起点值如果场景光线波动大需要提高到40以上否则整个背景会被判定为运动。3.3 二值化显示与运动区域叠加得到motion_pix后可以有两种显示方式。一种是直接把运动像素显示为红色背景显示为原始图像适合观察算法效果另一种是把差分结果二值化白底黑目标适合做后续框选。我推荐在调试阶段用第一种因为能同时观察到原始画面和运动区域容易判断是误检还是漏检。在TFT侧做叠加时注意时钟域帧差模块输出在PCLK域TFT显示在显示时钟域需要把motion_pix同步到显示时钟域。一个轻量级做法是写一个同步FIFO深度512写入PCLK域的有效像素读出显示域。由于QVGATFT每帧需要约307200个像素而OV7670实际输出大约25000个有效像素因为存在消隐区FIFO深度根据有效行计算我一般用1024深度避免溢出。使用表格总结THRESHOLD参数影响阈值背景噪声运动目标完整性适用场景10明显噪点目标完整光线稳定室内25少量噪点轮廓清晰一般教室或办公环境50无噪点目标边缘断续有阳光直射的窗边100无噪点只能检测高反差目标室外强光阈值不能设置成固定值就完事后面第5章会讲自适应调整。中间过程里如果diff_valid拉高但TFT上没有任何红色像素优先检查bank交换信号是否在帧消隐期到来而不是检查阈值。4. 从 ModelSim 仿真到 AC620 上板调试4.1 仿真激励用两帧不同图案验证差分逻辑直接上板调帧差容易出问题因为看不到RAM里的数据。ModelSim仿真时可以先用两张固定图案代替真实摄像头数据验证diff_valid和motion_pix的时序关系。下面是一个testbench片段模拟背景和带运动物体的前景。// tb_frame_diff.v - 帧差仿真激励 timescale 1ns/1ps module tb_frame_diff; reg clk; reg rst_n; reg frame_begin; reg [15:0] cur_pix; reg cur_valid; wire diff_valid; wire motion_pix; frame_diff_top dut( .clk(clk), .rst_n(rst_n), .frame_begin(frame_begin), .cur_pix(cur_pix), .cur_valid(cur_valid), .diff_valid(diff_valid), .motion_pix(motion_pix) ); initial begin clk 0; forever #10 clk ~clk; end integer i; initial begin rst_n 0; frame_begin 0; cur_valid 0; cur_pix 0; #20 rst_n 1; #20 frame_begin 1; #20 frame_begin 0; // 写入第一帧全部像素颜色相同 for (i 0; i 320*240; i i 1) begin (posedge clk); cur_valid 1; cur_pix 16h0F80; // 绿色背景 end (posedge clk); cur_valid 0; #100; // 第二帧部分区域像素变化 frame_begin 1; #20 frame_begin 0; for (i 0; i 320*240; i i 1) begin (posedge clk); cur_valid 1; if (i 320*100 i 320*120) cur_pix 16hF800; // 红色方块模拟运动目标 else cur_pix 16h0F80; end (posedge clk); cur_valid 0; #100; $stop; end endmodule参数说明cur_pix使用绿色0x0F80和红色0xF800因为帧差模块按RGB通道差分红绿差异会直接产生高差值。仿真时注意观察frame_begin后第一个像素的raddr是否等于0然后看motion_pix是否只在红色方块区域拉高。如果整个画面都变白说明bank交换时机太早导致第二帧部分数据写入了仍然在读的bank需要把frame_begin延迟到第一帧最后一个有效像素之后。在AC620工程的top.v里frame_begin通常由VSYNC下降沿产生但要注意VSYNC和第一个有效像素之间有约15行消隐时间所以应该用VSYNC的延迟版本作为bank交换信号而不是直接用原始VSYNC。4.2 SignalTap II 观测真实摄像头时序AC620使用Quartus自带的SignalTap II是排查上板问题的利器。我在调试时会在frame_diff_top里挂三组信号waddr[15:0]、raddr[15:0]、cur_valid采样时钟用PCLK采样深度4K。上板后如果看到waddr在帧中间跳跃说明前面的pixel_stitch拼接有问题可能是href极性反了。OV7670的HREF高电平有效但某些寄存器配置后HREF极性会翻转这时数据错位会造成图像左右颠倒或颜色通道互换。常见的上板现象和原因如下表现象可能原因检查方法TFT全白或花屏显示时钟域不同步或FIFO溢出降低OV7670 PCLK分频或增大FIFO深度图像偏绿偏紫RGB565高低字节拼接反了检查pixel_stitch中的字节赋值顺序帧差全白bank交换在帧中触发用SignalTap观察frame_begin是否跟VSYNC对齐运动目标有拖影阈值过低背景微变被检测提高THRESHOLD或加入形态学滤波TFT闪烁但不花缺少帧同步锁相用PLL固定TFT像素时钟避免摄像头PCLK漂移4.3 去掉OV7670改用OV7725时的注意事项把OV7670替换为OV7725时不要以为寄存器初始化和像素时序完全相同。OV7725的PCLK极性、数据格式寄存器地址和OV7670有差异常见做法是直接使用OV7725数据手册里的RGB565初始化表并把pixel_stitch中HREF有效电平调整好。AC620工程通常提供两个摄像头接口的引脚复用但FPGA内部逻辑需要根据摄像头类型通过宏定义或跳线选择。如果出现只输出上半屏画面多半是垂直窗口设置错误检查OV7725寄存器0x0C、0x0D和0x0E、0x0F。SignalTap另一个有效用法是抓取prev_pix和cur_pix的差值在波形里手动对比两个值。如果发现prev_pix恒定为0说明读bank始终是未写过的空RAM检查bank_r初值是否设置为~bank_w而不是在frame_begin时直接置反。5. 进阶运动目标框选与阈值自适应5.1 用行列投影快速生成目标框很多人做完帧差后只是把运动像素标红但毕设答辩时需要画一个方框圈住目标。不做连通域标记也能得到方框方法是前景像素行列投影统计每一行的motion_pix数量得到垂直分布再统计每一列的motion_pix数量得到水平分布。找到第一个和最后一个超过设定行/列阈值的坐标就得到一个矩形包围盒。在FPGA上实现时可以准备两个计数器数组一个长度320对应列一个长度240对应行。每个像素差分完成后如果motion_pix1就把对应的列计数器和行计数器加1。帧结束时扫描计数器生成box_left、box_right、box_top、box_bottom四个寄存器。下一帧在TFT上叠加绿色方框时只需要比较当前像素的横纵坐标是否落在box范围内。// box_projection.v - 行列投影法生成包围盒 reg [7:0] col_cnt [0:319]; // 每列前景像素数 reg [7:0] row_cnt [0:239]; // 每行前景像素数 reg [8:0] x_pos; reg [8:0] y_pos; always (posedge clk) begin if (frame_begin) begin for (i 0; i 320; i i 1) col_cnt[i] 8d0; for (i 0; i 240; i i 1) row_cnt[i] 8d0; box_left 9d320; box_right 9d0; box_top 9d240; box_bottom 9d0; end else if (motion_pix diff_valid) begin col_cnt[x_pos] col_cnt[x_pos] 1b1; row_cnt[y_pos] row_cnt[y_pos] 1b1; end end这段代码里的x_pos和y_pos需要在像素有效期间同步累加注意行结束时要清零x_pos。投影法对单个目标效果很好多目标时方框会连成一个大的包围区域因为行列计数被多个目标叠加。如果必须分离多个目标需要做连通域标记但那样会占用大量RAM和状态机资源在AC620上不划算。5.2 阈值自适应用背景均方差动态调整固定阈值在光照变化明显时会失效。简单的自适应方案是统计当前帧与上一帧全图绝对差的和求平均得到mean_diff再用THRESHOLD mean_diff * 3作为下一帧的阈值。这样当环境光轻微变化时全图差分会整体抬高阈值也跟随抬高避免整个画面被误判成运动目标。实现时可以在帧差模块旁加一个累加器把每个像素的diff_r diff_g diff_b累加帧结束时除以总像素数。除法在FPGA里直接用移位近似因为乘以3相当于(mean*3)可以用(sum / PIX_NUM) * 3但除法周期长我一般用sum 17近似sum / 131072再右移两位逼近乘以3误差可接受。需要注意的是阈值更新要在帧消隐期完成新阈值从下一帧第一行开始生效不能在当前帧中间切换。最后还有一个实用技巧OV7670/OV7725的自动曝光会使画面亮度缓慢变化帧差法对亮度变化非常敏感。如果阈值自适应后依然有面积闪烁可以把RGB565先转成灰度再用当前像素与上一帧灰度差除以当前灰度值得到一个比值阈值。这样运动检测对光照梯度变化不再敏感代价是每个像素多一次除法但在AC620上用查找表实现除法也可以跑实时。把这两个技巧加进工程运动目标检测的稳定性会明显提升。本文还有配套的精品资源点击获取