帧差法顾名思义就是对输入的前后两帧图像做差值,然后检测出两帧图像不同的地方,并且可以实时跟踪运动的目标轮廓。
本设计是基于ZYNQ7010和VIVADO2018.3实现的帧差法运动目标检测,针对运动目标检测算法在传统 PC端上实时性较差的问题,设计了一种基于 ZYNQ 硬件加速的运动目标实时检测系统。将摄像头采集的彩色视频流转换为灰度视频流并进行图像处理来实现运动目标检测,并将检测后的结果与原彩色视频流叠加来显示实时检测结果。
本设计使用到的硬件有ZYNQ7010、768P显示屏、OV5640摄像头这些硬件,在原有的摄像头显示例子上搭建完成,关于显示环境的搭建可以参照前面的文章。
帧差法项目通过pl端两个按键分别控制帧差的阈值和输出图像的类型。按键1控制阈值为40-100递增,按键2控制输出图像类型为原始RGB图像、灰度图像、帧差结果图像以及RGB叠加检测框图像。
这边的项目架构主要是参照了《基于ZYNQ加速的帧差法运动目标检测》这样一篇论文,大家可以去下载看看。然后视频教程的话,我是看了B站up主大磊FPGA的视频,大家也可以去看看。
下图是我的帧差法实现运动目标检测跟踪的整体框图了。大致的原理过程是这样子的,首先OV5640采集图像,将采集到的图像分别送入到vdma0和vdma1中,然后两个vdma缓存不同的帧数,达到帧差的效果;之后将两帧图像送入到自己编写的帧差模块进行差值计算,之后将结果输出到vdma2;最后经过DVI模块将信号送至hdmi显示屏上显示。
下面这张图呢是我参照b站视频里面的,帧差代码也主要完成下面这个框图。将其中一个vdma,比如vdma0中的数据送到fifo进行缓存。然后当vdma1中的数据的user来到时,从fifo中读数据,这样可以对齐信号。因为从fifo中读数据需要耗费两个时钟周期,所以vdma1的数据延迟2clk,之后就是转灰度,做差值,都比较简单。
然后这边需要说一下这个握手的协议。它是有五组信号组成,data是数据信号,user是一帧图像的开始信号,last是一帧图像每一行结束的信号,当valid和ready拉高时数据有效。
首先是信号的定义,有三组信号,分别是vdma0和vdma1的输入,以及输出到vdma2这样的三组信号。
module frame_diff(
input clk,
input rst_n,
//control key
input key0,
input key1,
//data stream from vdma0
input [23:0] s0_axis_tdata,
input s0_axis_tvalid,
output s0_axis_tready,
input s0_axis_tuser,
input s0_axis_tlast,
//data stream from VDMA1
input [23:0] s1_axis_tdata,
input s1_axis_tvalid,
output s1_axis_tready,
input s1_axis_tuser,
input s1_axis_tlast,
//data stream to VDMA2
output reg [23:0] m_axis_tdata,
output reg m_axis_tvalid,
input m_axis_tready,
output reg m_axis_tuser,
output reg m_axis_tlast
);
接着就是信号缓存了,对vdma0来的数据到user来临时就开始存;然后当vdma1的user来临时就开始从fifo中读数据。注意vdma1的数据延迟2clk来同比,具体可以看代码。
//-------------------------------------------------------
// fifo write and read data from vdma0
//-------------------------------------------------------
reg [23:0] data_in;
reg data_wr;
reg data_wr_en;
wire data_alfull;
wire [23:0] data_out;
reg data_rd;
reg data_rd_en;
reg [23:0] s0_axis_tdata_dy1;
reg [23:0] s0_axis_tdata_dy2;
//write data to fifo
assign s1_axis_tready=~data_alfull; //always ready until fifo is full
always @(posedge clk or negedge rst_n) begin
if(~rst_n)begin
data_wr<=1'b0;
data_wr_en<=1'b0;
data_in<=24'd0;
end
else begin
if(s1_axis_tvalid & s1_axis_tready & s1_axis_tuser)begin //when s0_user is come,wr_en is high,make corrsponding
data_wr_en<=1'b1;
end
if(s1_axis_tvalid & s1_axis_tready)begin //begin to write data to fifo
data_wr<=1'b1;
data_in<=s1_axis_tdata;
end
else begin
data_wr<=1'b0;
data_in<=data_in;
end
end
end
//read data from fifo
always @(posedge clk or negedge rst_n) begin
if(~rst_n)begin
data_rd<=1'b0;
data_rd_en<=1'b0;
end
else begin
if(s0_axis_tvalid & s0_axis_tready & s0_axis_tuser)begin//unfull //when fifo is alfull and data from camera is come,rd_en is high
data_rd_en<=1'b1;
end
if(s0_axis_tvalid & s0_axis_tready)begin
data_rd<=1'b1;
end
else begin
data_rd<=1'b0;
end
end
end
//fifo define 24*1024
fifo_generator_0 u_fifo_generator_0 (
.clk (clk), // input wire clk
.srst (~rst_n), // input wire srst
.din (data_in), // input wire [23 : 0] din
.wr_en (data_wr_en & data_wr), // input wire wr_en
.rd_en (data_rd_en & data_rd), // input wire rd_en
.dout (data_out), // output wire [23 : 0] dout
.full (), // output wire full
.almost_full(data_alfull), // output wire almost_full
.empty (), // output wire empty
.data_count () // output wire [10 : 0] data_count
);
//read from fifo cost 2 clk, so data from camera need delay 2 clk too
always @(posedge clk or negedge rst_n) begin
if(~rst_n)begin
s0_axis_tdata_dy1<=24'd0;
s0_axis_tdata_dy2<=24'd0;
end
else begin
s0_axis_tdata_dy1<=s0_axis_tdata;
s0_axis_tdata_dy2<=s0_axis_tdata_dy1;
end
end
然后我这边又定义了两个按键检测和滤波信号,其实就是为了控制输出的阈值大小和图像类型,这部分内容很简单。
//-------------------------------------------------------
// key filter and throshold control and state chose
//-------------------------------------------------------
reg [7:0] Frame_Diff_Throshold;
reg [1:0] state;
wire key0_flag;
wire key1_flag;
key_filter u_key_filter0(
.clk ( clk ),
.rst_n ( rst_n ),
.key_in ( key0 ),
.key_flag ( key0_flag )
);
key_filter u_key_filter1(
.clk ( clk ),
.rst_n ( rst_n ),
.key_in ( key1 ),
.key_flag ( key1_flag )
);
always @(posedge clk or negedge rst_n) begin
if(~rst_n)begin
Frame_Diff_Throshold<=8'd70;
end
else if(key0_flag==1'b1 & Frame_Diff_Throshold<8'd100)begin
Frame_Diff_Throshold<=Frame_Diff_Throshold+4'd10;
end
else if (key0_flag==1'b1 & Frame_Diff_Throshold>=8'd100)begin
Frame_Diff_Throshold<=8'd40;
end
else begin
Frame_Diff_Throshold<=Frame_Diff_Throshold;
end
end
always @(posedge clk or negedge rst_n) begin
if(~rst_n)begin
state<=2'd0;
end
else if(key1_flag==1'b1)begin
state<=state+1'b1;
end
else begin
state<=state;
end
end
对读出的两组信号将RGB图像转换成灰度图像,之后在把两张灰度图像进行帧差,最后注意fifo读用了两个clk、转灰度用了3clk、帧差用了1clk,所以握手信号也要延迟6clk,经过这些操作后,flag信号就是代表了帧差的结果了,接下来只需要提取出这些flag在图像上的范围和边界即可,具体代码如下所示。
更多的关于怎么找到边间画出边界,大磊FPGA里面有很详细的步骤,大家可以看视频,或者看我工程,这里就不叙述了。
//-------------------------------------------------------
// current img data and past img data:RGB888 to RGB565
//-------------------------------------------------------
wire [23:0] s0_data_gray; //data from camera
wire [23:0] s1_data_gray; //data from vdma0
wire [7:0] s0_data_gray1;
wire [7:0] s1_data_gray1;
assign s0_data_gray1=s0_data_gray[23:16];
assign s1_data_gray1=s1_data_gray[23:16];
rgb2gray u_rgb2gray1(
.pclk ( clk ),
.rst_n ( rst_n ),
.rgb_data ( s0_axis_tdata_dy2 ),
.gray_data ( s0_data_gray )
);
rgb2gray u_rgb2gray2(
.pclk ( clk ),
.rst_n ( rst_n ),
.rgb_data ( data_out ),
.gray_data ( s1_data_gray )
);
//-------------------------------------------------------
// frame diff operation
//-------------------------------------------------------
reg frame_diff_flag;
reg [10:0] x_cnt;
reg [10:0] y_cnt;
always @(posedge clk or negedge rst_n) begin
if(~rst_n)begin
frame_diff_flag<=1'b0;
end
else begin
if(s0_data_gray1>s1_data_gray1)begin
if( ((s0_data_gray1-s1_data_gray1) > Frame_Diff_Throshold) && (x_cntd5) )begin
frame_diff_flag<=1'b1;
end
else frame_diff_flag<=1'b0;
end
else begin
if( ((s1_data_gray1-s0_data_gray1) > Frame_Diff_Throshold) && (x_cnt<H_DISP-5'd5) )begin
frame_diff_flag<=1'b1;
end
else frame_diff_flag<=1'b0;
end
end
end
//-------------------------------------------------------
// for signal of s0,delay 6 clk
//-------------------------------------------------------
//fifo:2clk; rgb2gray:3clk; frame diff:1clk
//so 6 clk delay needed
wire s0_axis_tvalid_dy;
wire s0_axis_tuser_dy;
wire s0_axis_tlast_dy;
reg [5:0] s0_axis_tvalid_reg;
reg [5:0] s0_axis_tuser_reg;
reg [5:0] s0_axis_tlast_reg;
assign s0_axis_tvalid_dy=s0_axis_tvalid_reg[5];
assign s0_axis_tuser_dy=s0_axis_tuser_reg[5];
assign s0_axis_tlast_dy=s0_axis_tlast_reg[5];
always @(posedge clk or negedge rst_n) begin
if(~rst_n)begin
s0_axis_tvalid_reg<=6'd0;
s0_axis_tuser_reg<=6'd0;
s0_axis_tlast_reg<=6'd0;
end
else begin
s0_axis_tvalid_reg<={s0_axis_tvalid_reg[4:0],s0_axis_tvalid & s0_axis_tready};
s0_axis_tuser_reg<={s0_axis_tuser_reg[4:0],s0_axis_tvalid & s0_axis_tready & s0_axis_tuser};
s0_axis_tlast_reg<={s0_axis_tlast_reg[4:0],s0_axis_tvalid & s0_axis_tready & s0_axis_tlast};
end
end
vivado工程中的block design就如下图所示了,可以看到例化了3个vdma,然后自己编写了frame diff模块,原理是和上面的架构是相同的。
综合布局布线完成后,我们把它导入到sdk设计中,sdk中c代码如下所示,就很基础,只是初始化3个vdma开启读和写通道就可以了。
#include
#include
#include
#include "xil_types.h"
#include "xil_cache.h"
#include "xparameters.h"
#include "xaxivdma.h"
#include "xaxivdma_i.h"
#include "vdma_api/vdma_api.h"
//宏定义
#define VDMA_ID0 XPAR_AXIVDMA_0_DEVICE_ID
#define VDMA_ID1 XPAR_AXIVDMA_1_DEVICE_ID
#define VDMA_ID2 XPAR_AXIVDMA_2_DEVICE_ID
//frame buffer的起始地址
unsigned int const frame_buffer_addr0 =0x03000000;
unsigned int const frame_buffer_addr1 =0x06000000;
unsigned int const frame_buffer_addr2 =0x09000000;
//驱动实例
XAxiVdma vdma;
int main(void)
{
int i;
int j;
int k;
//配置VDMA
i=run_vdma_frame_buffer(&vdma, VDMA_ID0,1024,768,frame_buffer_addr0,0,0,BOTH);
j=run_vdma_frame_buffer(&vdma, VDMA_ID1,1024,768,frame_buffer_addr1,0,0,BOTH);
k=run_vdma_frame_buffer(&vdma, VDMA_ID2,1024,768,frame_buffer_addr2,0,0,BOTH);
printf("%d\r\n",i);
printf("%d\r\n",j);
printf("%d\r\n",k);
while(1);
return 0;
}
这个工程采用了前后帧差法,完成了单运动目标的检测,是比较基础的实战项目。对于后期想扩展的方向,可以使用背景帧差法来更好的定位目标,或者完成多运动目标的检测和计数。
资源下载链接:
https://download.csdn.net/download/qq_40995480/86880974