所谓帧差法,就是比较相邻两帧图像的像素数据有没有差异,并对差异的大小做一个规定阈值,若超出规定阈值,即判定有运动物体经过,并且通过算法绘制矩形框对运动物体进行标定,借此来实现运动目标的检测,该实验缺点就是只能对单个目标进行框定。
整个实验大致包含如下几个注意点:
1、选取单个分量进行前一帧与当前帧的差异比较,在本实验中选取的是Y分量(亮度分量)进行比较,因此需要对传输的RGB数据做RGB转YCbCr的转换,这一块在我的另一篇文章《FPGA图像处理_色彩空间转换(含源码)》中讲到过,只不过那一篇里是用vsync和hsync作为同步信号,而在这一篇里是用AXI4-Stream的tuser信号作为同步信号,稍作修改即可,至于为什么可以用tuser信号作为同步信号,可以去查看我的另一篇文章《【ZYNQ】IP核_关于视频IP核的详细介绍》有讲到。
2、帧差法运动目标检测模块是以自定义IP核的形式添加打到摄像头+LCD屏幕显示的实验中,这样简化了操作,关于如何封装自定义IP核,在我的另一篇文章《【ZYNQ】自定义IP核的封装》有讲到。
3、时序问题,做这个实验的关键就是一定要注意延时的问题,既然是做图像的差异对比,那么对比的对象一定要一致,比如通过FIFO对前一帧数据进行缓冲,当当前帧的第一个像素到来时,读取前一帧图像的第一个像素数据,这样就保证了二者的一致性。
4、检测边框该如何绘制,这个只能通过代码去琢磨其中奥妙,代码我就放这:
`timescale 1ns / 1ps
module ip_movings(
// input clk ,
// input rst_n ,
//AXI4_Stream Slave接口0,来自摄像头,获取当前帧数据
input s0_axis_aclk ,
input s0_axis_aresetn ,
input [23:0] s0_axis_tdata ,
input s0_axis_tvalid ,
output s0_axis_tready ,
input s0_axis_tuser , //tuser == start of frame(SOF)
input s0_axis_tlast , //tlast == end of line(EOL)
//AXI4_Stream Slave接口1,来自VDMA_0,获取前一帧数据
input s1_axis_aclk ,
input s1_axis_aresetn ,
input [23:0] s1_axis_tdata ,
input s1_axis_tvalid ,
output s1_axis_tready ,
input s1_axis_tuser ,
input s1_axis_tlast ,
//AXIS4_Stream Master接口,输出到VDMA_1
input m_axis_aclk ,
input m_axis_aresetn ,
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
);
localparam [7:0] Diff_Threshold = 8'd100; //帧差阈值
localparam [10:0] IMG_HDISP = 11'd1920; //图像分辨率
localparam [10:0] IMG_VDISP = 11'd1080;
//*************************************************************************
//将VDMA的图像保存到FIFO中
reg [23:0] fifo_data ;
reg fifo_wr ;
reg fifo_wr_en ;
wire fifo_full ;
reg fifo_rd ;
reg fifo_rd_en ;
wire [23:0] fifo_q ;
//FIFO不满时,持续从VDMA读取data
assign s1_axis_tready = ~fifo_full;
//写状态机
localparam W_IDLE = 2'b01 ;
localparam W_FIFO = 2'b10 ;
reg [1:0] write_state ;
reg [1:0] next_write_state ;
reg [3:0] wcnt ;
always @ (posedge s1_axis_aclk or negedge s1_axis_aresetn) begin
if(!s1_axis_aresetn)
write_state <= W_IDLE ;
else
write_state <= next_write_state ;
end
always @ (*) begin
case(write_state)
W_IDLE: begin
if(wcnt == 4'd8)
next_write_state <= W_FIFO;
else
next_write_state <= W_IDLE;
end
W_FIFO:
next_write_state <= W_FIFO;
default:
next_write_state <= W_IDLE;
endcase
end
always @ (posedge s1_axis_aclk or negedge s1_axis_aresetn) begin
if(!s1_axis_aresetn)
wcnt <= 4'd0 ;
else if(write_state == W_IDLE)
wcnt <= wcnt + 1'b1;
else
wcnt <= 4'd0 ;
end
always @ (posedge s1_axis_aclk or negedge s1_axis_aresetn)begin
if(!s1_axis_aresetn) begin
fifo_wr_en <= 1'b0 ;
fifo_wr <= 1'b0 ;
fifo_data <= 24'b0 ;
end
else begin
if(write_state == W_FIFO) begin
//检测到SOF(tuser)信号拉高后才打开写FIFO使能,保证FIFO中写入的第一个数据为SOF
if(s1_axis_tvalid & s1_axis_tready & s1_axis_tuser)
fifo_wr_en <= 1'b1 ; //确保前一帧的数据第一个像素点与当前帧的第一个像素点数据对齐
if(s1_axis_tvalid & s1_axis_tready) begin
fifo_wr <= 1'b1 ;
fifo_data <= s1_axis_tdata ;
end
else begin
fifo_wr <= 1'b0 ;
fifo_data <= fifo_data ;
end
end
else begin
fifo_wr_en <= 1'b0 ;
fifo_wr <= 1'b0 ;
fifo_data <= fifo_data ;
end
end
end
fifo_generator_0 fifo_generator_inst (
.clk(s1_axis_aclk), // input wire clk
.rst(~s1_axis_aresetn), // input wire rst
.din(fifo_data), // input wire [23 : 0] din
.wr_en(fifo_wr_en & fifo_wr), // input wire wr_en
.rd_en(fifo_rd_en & fifo_rd), // input wire rd_en
.dout(fifo_q), // output wire [23 : 0] dout
.full(), // output wire full
.almost_full(fifo_full), // output wire almost_full
.empty() // output wire empty
);
//读状态机
reg [1:0] read_state ;
reg [1:0] next_read_state ;
reg [3:0] rcnt ;
localparam R_IDLE = 2'b01 ;
localparam R_FIFO = 2'b10 ;
always @ (posedge s0_axis_aclk or negedge s0_axis_aresetn) begin
if(!s0_axis_aresetn)
read_state <= R_IDLE ;
else
read_state <= next_read_state ;
end
always @(*) begin
case(read_state)
R_IDLE: begin
if(rcnt == 8'd8)
next_read_state <= R_FIFO;
else
next_read_state <= R_IDLE;
end
R_FIFO:
next_read_state <= R_FIFO; //一直处于读的状态
default:
next_read_state <= R_IDLE;
endcase
end
always @ (posedge s0_axis_aclk or negedge s0_axis_aresetn) begin
if(!s0_axis_aresetn)
rcnt <= 4'd0 ;
else if(read_state == R_IDLE)
rcnt <= rcnt+ 1'b1;
else
rcnt <= 4'd0 ;
end
//************************************************************************
//摄像头输入像素的同时,从FIFO中读出VDMA像素,以进行运算
//需要注意的是FIFO的read mode有两种模式:Standard FIFO,数据会落后于读信号一个周期;还有一种是FWFT模式,预先取出一个数据,读信号有效时,相应的数据也有效
always @ (posedge s0_axis_aclk or negedge s0_axis_aresetn) begin
if(!s0_axis_aresetn) begin
fifo_rd_en <= 1'b0 ;
fifo_rd <= 1'b0 ;
end
else begin
if(read_state == R_FIFO) begin
//等待FIFO中缓存一定量的数据之后,检测摄像头输入SOF之后才打开读FIFO使能,使得与FIFO中读取的VDMA视频数据同步
if(s0_axis_tvalid & s0_axis_tready & s0_axis_tuser & fifo_full)
fifo_rd_en <= 1'b1 ;
//此处FIFO读信号相对于摄像头输入视频延迟了一个时钟周期
if(s0_axis_tvalid & s0_axis_tready) //当握手信号在上升沿完成时,当前帧的数据开始传输,而FIFO的读使能落后一个时钟,数据读出来又落后读使能一个时钟,所以
fifo_rd <= 1'b1 ; //前一帧的数据落后于当前帧2个时钟周期
else
fifo_rd <= 1'b0 ;
end
else begin
fifo_rd_en <= 1'b0 ;
fifo_rd <= 1'b0 ;
end
end
end
//*******************************************消耗掉两个时钟周期
//为了与FIFO钟读出的数据同步,将摄像头输入视频也延迟两个时钟周期(简单通过两个寄存器级联)
reg [23:0] s0_axis_tdata_delay1;
reg [23:0] s0_axis_tdata_delay2;
always @ (posedge s0_axis_aclk or negedge s0_axis_aresetn) begin
if(!s0_axis_aresetn) begin
s0_axis_tdata_delay1 <= 24'd0 ;
s0_axis_tdata_delay2 <= 24'd0 ;
end
else begin
s0_axis_tdata_delay1 <= s0_axis_tdata ;
s0_axis_tdata_delay2 <= s0_axis_tdata_delay1 ;
end
end
//*******************************************消耗掉三个时钟周期
//将RGB格式数据转化成YCbCr数据,但是只取Y分量,因此是灰度图像
wire [7:0] s0_img_y ;//来自摄像头
wire [7:0] s1_img_y ;//来自VDMA
rgb_ycbcr s0_rgb_ycbcr(
.clk(s0_axis_aclk) ,
.rst_n(s0_axis_aresetn) ,
.in_img_red(s0_axis_tdata_delay2[23:16]) ,
.in_img_green(s0_axis_tdata_delay2[15:8]) ,
.in_img_blue(s0_axis_tdata_delay2[7:0]) ,
.out_img_Y(s0_img_y) ,
.out_img_Cb() ,
.out_img_Cr()
);
rgb_ycbcr s1_rgb_ycbcr(
.clk(s1_axis_aclk) ,
.rst_n(s1_axis_aresetn) ,
.in_img_red(fifo_q[23:16]) ,
.in_img_green(fifo_q[15:8]) ,
.in_img_blue(fifo_q[7:0]) ,
.out_img_Y(s1_img_y) ,
.out_img_Cb() ,
.out_img_Cr()
);
//******************************************消耗一个时钟周期
//帧差运算
//帧差标志位,为1表示两帧数据差别超过阈值
reg frame_difference_flag ;
always @ (posedge s0_axis_aclk or negedge s0_axis_aresetn) begin
if(!s0_axis_aresetn) begin
frame_difference_flag <= 1'b0 ;
end
else begin
if(s0_img_y > s1_img_y) begin
if(s0_img_y - s1_img_y > Diff_Threshold) //灰度大于阈值
frame_difference_flag <= 1'b1 ;
else
frame_difference_flag <= 1'b0 ;
end
else begin
if(s1_img_y - s0_img_y > Diff_Threshold) //灰度大于阈值
frame_difference_flag <= 1'b1 ;
else
frame_difference_flag <= 1'b0 ;
end
end
end
//*****************************************************延迟六个时钟周期
//将摄像头的输入的同步信号延迟六个时钟周期,与数据同步
wire s0_axis_tuser_dly ;
wire s0_axis_tlast_dly ;
wire s0_axis_tvalid_dly ;
reg [5:0] s0_axis_tuser_reg ;
reg [5:0] s0_axis_tlast_reg ;
reg [5:0] s0_axis_tvalid_reg ;
always @ (posedge s0_axis_aclk or negedge s0_axis_aresetn) begin
if(!s0_axis_aresetn) begin
s0_axis_tuser_reg <= 6'd0 ;
s0_axis_tlast_reg <= 6'd0 ;
s0_axis_tvalid_reg <= 6'd0 ;
end
else begin
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};
s0_axis_tvalid_reg <= {s0_axis_tvalid_reg[4:0],s0_axis_tvalid & s0_axis_tready} ;
end
end
assign s0_axis_tuser_dly = s0_axis_tuser_reg[5] ;
assign s0_axis_tlast_dly = s0_axis_tlast_reg[5] ;
assign s0_axis_tvalid_dly = s0_axis_tvalid_reg[5];
//*****************************************************************
//计算帧差后的像素坐标
reg [10:0] x_cnt;
reg [10:0] y_cnt;
always @ (posedge s0_axis_aclk or negedge s0_axis_aresetn) begin
if(!s0_axis_aresetn) begin
x_cnt <= 11'd0 ;
y_cnt <= 11'd0 ;
end
else if(s0_axis_tvalid_dly) begin
if(s0_axis_tlast_dly) begin
x_cnt <= 11'd0 ;
y_cnt <= y_cnt+1'b1;
end
else if(s0_axis_tuser_dly) begin
x_cnt <= 11'd0;
y_cnt <= 11'd0;
end
else begin
x_cnt <= x_cnt + 1'b1;
end
end
end
//******************************************************
//求出运动目标的最大矩形边框
/**********************
down_reg
起始点 -》-----------------------------------
\ \
\ \
\ \
right_reg \ \ left_reg
\ \
\ \
------------------------------------
up_reg
**********************/
reg [10:0] up_reg ;
reg [10:0] down_reg;
reg [10:0] left_reg;
reg [10:0] right_reg;
reg flag_reg;
always @(posedge s0_axis_aclk or negedge s0_axis_aresetn) begin
if(!s0_axis_aresetn) begin
up_reg <= IMG_VDISP;
down_reg<= 11'd0 ;
left_reg<= IMG_HDISP;
right_reg<= 11'd0 ;
end
else if(s0_axis_tuser_dly) begin
up_reg <= IMG_VDISP;
down_reg<= 11'd0 ;
left_reg<= IMG_HDISP;
right_reg<= 11'd0 ;
end
else if(s0_axis_tvalid_dly & frame_difference_flag) begin
flag_reg <= 1'b1 ;
if(x_cnt<left_reg)
left_reg<= x_cnt ;
else
left_reg<= left_reg ;
if(x_cnt>right_reg)
right_reg<= x_cnt ;
else
right_reg<= right_reg ;
if(y_cnt<up_reg)
up_reg <= y_cnt ;
else
up_reg <= up_reg ;
if(y_cnt>down_reg)
down_reg <= y_cnt ;
else
down_reg <= down_reg ;
end
end
reg [10:0] rectangular_up ;
reg [10:0] rectangular_down ;
reg [10:0] rectangular_left ;
reg [10:0] rectangular_right ;
reg rectangular_flag ;
always @ (posedge s0_axis_aclk or negedge s0_axis_aresetn) begin
if(!s0_axis_aresetn) begin
rectangular_up <= 11'd0 ;
rectangular_down<= 11'd0 ;
rectangular_left<= 11'd0 ;
rectangular_right<= 11'd0 ;
rectangular_flag <= 1'b0 ;
end
else if((x_cnt == IMG_HDISP-1) && (y_cnt == IMG_VDISP-1)) begin
rectangular_up <= up_reg ;
rectangular_down<= down_reg;
rectangular_left<= left_reg;
rectangular_right<= right_reg;
rectangular_flag <= flag_reg;
end
end
//**********************************************************
//绘制矩形框
//计算摄像头输入图像的像素坐标
reg [10:0] s0_x_cnt;
reg [10:0] s0_y_cnt;
always @ (posedge s0_axis_aclk or negedge s0_axis_aresetn) begin
if(!s0_axis_aresetn) begin
s0_x_cnt <= 11'd0 ;
s0_y_cnt <= 11'd0 ;
end
else if(s0_axis_tvalid) begin
if(s0_axis_tlast) begin
s0_x_cnt <= 11'd0 ;
s0_y_cnt <= y_cnt+1'b1 ;
end
else if(s0_axis_tuser) begin
s0_x_cnt <= 11'd0 ;
s0_y_cnt <= 11'd0 ;
end
else begin
s0_x_cnt <= s0_x_cnt + 1'b1;
end
end
end
reg boarder_flag; //标志着像素点位于方框上
always @(posedge s0_axis_aclk or negedge s0_axis_aresetn) begin
if(!s0_axis_aresetn) begin
boarder_flag <= 1'b0 ;
end
else begin
if(rectangular_flag) begin //检测到运动目标
if((s0_x_cnt>rectangular_left) && (s0_x_cnt<rectangular_right) && ((s0_y_cnt == rectangular_up) || (s0_y_cnt == rectangular_down))) begin
boarder_flag <= 1'b1 ;
end
else if((s0_y_cnt>rectangular_up) && (s0_y_cnt<rectangular_down) && ((s0_x_cnt == rectangular_left) || (s0_x_cnt == rectangular_right))) begin
boarder_flag <= 1'b1 ;
end
else begin
boarder_flag <= 1'b0 ;
end
end
else begin
boarder_flag <= 1'b0 ;
end
end
end
//输出给摄像头输入的Ready信号
assign s0_axis_tready = m_axis_tready;
//给AXI4_Stream Master接口赋值
always @(posedge s0_axis_aclk or negedge s0_axis_aresetn) begin
if(!s0_axis_aresetn) begin
m_axis_tvalid <= 1'd0 ;
m_axis_tuser <= 1'd0 ;
m_axis_tlast <= 1'd0 ;
m_axis_tdata <= 24'd0 ;
end
else begin
m_axis_tvalid <= s0_axis_tvalid ;
m_axis_tuser <= s0_axis_tuser ;
m_axis_tlast <= s0_axis_tlast ;
m_axis_tdata <= boarder_flag ? 24'hff_00_00: s0_axis_tdata ;
end
end
endmodule