第一部分 设计概述
1.1 设计目的
作品基于赛方提供的PYNQ-Z2开发板,采用软硬件协同方案,搭建基于DMA的图像数据缓存传输系统。在此基础上,自主设计基于AXI4-stream接口的图像处理IP核,从而构建高实时性的运动目标检测系统。
在我们的设计中,侧重PL部分数字逻辑开发,对传统的帧差法进行改进,通过对缓存背景帧的累加压缩和重建展开达到节省逻辑资源的优化目标。
1.2 应用领域
本系统消耗较小资源,可以应用在如自动触发门禁、设备智能检测、远程婴幼儿监护、家庭防盗等场景,有效降低成本,也可以使得有限逻辑资源的PL部分留有余地去做其他处理。
1.3 主要技术特点
检测方法上采用了帧间差分法,其具有运算简单,资源消耗少,易于实时检测的特点。
在整个设计中,需要缓存前一帧作为背景帧以便与后一帧相减得到绝对值。本设计结合FPGA自身的并行流水特性,对传统的帧间差分法进行改进,将前一帧从640×480×8bit的灰度图像通过2×2的窗口累加压缩为320×240×10bit的灰度图像作为缓存帧,在帧差过程中再重建展开用于运算,节省缓存帧图像使用的BRAM。帧差得到的运动区域,与完整的现帧图像结合获得红色运动区域,并通过蓝色矩形包围盒捕捉。
而依照此方法类推,可以使用4×4乃至更大的矩阵累加压缩,在少量降低精度不对检测产生较大影响的同时,可以大大减少缓存图像的BRAM,从而使得PL部分可以腾出逻辑资源用于其他处理。
1.4 关键性能指标
Utilization(型号:xc7z020clg400-1):
电路连接图(外接摄像头和HDMI):
效果图(红色是运动轨迹,蓝色是矩形包围盒):
第二部分 系统组成及功能说明
2.1 整体介绍
2.1.1 系统架构
采集OV7725摄像头数据,通过 Video In AXI4-Stream转换为stream数据流。
随后经过基于AXI4-stream接口的图像处理IP核,其流水线处理实现了灰度化、中值滤波、累加压缩、缓存背景帧、帧差、阈值化、重建展开、包围盒捕捉以及重叠输出到DMA写通道,DMA写通道将图像数据搬运缓存到DDR,通过中断实现三缓存机制。
于此同时,DMA读通道始终读取三缓存中写通道正在写的帧的前一帧,再通过AXI4-Stream to Video Out转换为行场视频信号输出到HDMI驱动IP,最后在外接显示器上呈现图像处理效果。
2.1.2 Vivado Block Design
下图是vivado的block design。
2.2 各模块介绍
2.2.1 图像处理IP核
2.2.1.1 图像处理简述
基于AXI4-stream接口的图像处理IP核,其流水线处理实现了灰度化、中值滤波、累加压缩、缓存背景帧、帧差、阈值化、重建展开、包围盒捕捉以及重叠输出,流水化作业通过模块输入数据握手信号作为ce使能控制,输出端由一个小型同步fifo缓冲。
2.2.1.2 IP核内部架构
由于输入数据是不连续的,对流水线使用ce使能控制。
使用一个很小的同步fifo(位宽为24,深度为32)来缓冲流水线输出数据,另外利用fifo的满空信号对stream接口握手信号控制。
2.2.1.3 详细过程
下面介绍图像处理的详细过程以及代码。
(1)AXI4-Stream接口
在2.2.1.2中阐释了接口的设计,下面是读写握手信号部分代码。
assign rnext = s00_axis_tready && s00_axis_tvalid;
assign wnext = m00_axis_tvalid && m00_axis_tready;
always @(posedge m00_axis_aclk)
begin
if(!m00_axis_aresetn)
m00_axis_tvalid<=1'b0;
else if(signal_empty)
m00_axis_tvalid<=1'b0;
else if( signal_almost_empty && wnext )
m00_axis_tvalid<=1'b0;
else if( (!signal_empty) && (!m00_axis_tvalid) && m00_axis_tready )
m00_axis_tvalid<=1'b1;
else
m00_axis_tvalid<=1'b1;
end
always @(posedge m00_axis_aclk)
begin
if(!m00_axis_aresetn)
s00_axis_tready<=1'b0;
else if(signal_full)
s00_axis_tready<=1'b0;
else if( signal_almost_full && rnext )
s00_axis_tready<=1'b0;
else if( (!signal_full) && (!s00_axis_tready) && s00_axis_tvalid )
s00_axis_tready<=1'b1;
else
s00_axis_tready<=1'b1;
end
(2) 灰度化
为了便于之后的图像处理,我们首先把RGB888格式的数据转换为灰度值。
通常,通过以下公式转换:
Gray=0.299*Red+0.587*Green+0.114*Blue
式子中 Red、Green、Blue分别为每个像素点红色、绿色、蓝色的量化值。Gray为转换后得到的灰度值.
再利用二进制移位性质,将浮点运算近似化简为定点运算,然后分割为两级流水,化简公式如下:
(Gray<<8)=[(Red<<6)+(Red<<3)+(Red<<2)+Red]
+[(Green<<7)+(Green<<4)+(Green<<2)+Green]
+[(Blue<<4)+(Blue<<3)+(Blue<<2)+1]
下面是灰度转换部分代码:
assign red = rgb[23:16];
assign green = rgb[15:8];
assign blue = rgb[7:0];
assign gray_w = gray_r + gray_g + gray_b;
always@(posedge clk)
begin
if(!rst)
begin
gray_r <= 0;
gray_g <= 0;
gray_b <= 0;
end
else if(ce)
begin
gray_r <= (red <<6) + (red <<3) + (red <<2) + red;
gray_g <= (green<<7) + (green<<4) + (green<<2) + green;
gray_b <= (blue <<4) + (blue <<3) + (blue <<2) + 1;
end
else
begin
gray_r <= gray_r;
gray_g <= gray_g;
gray_b <= gray_b;
end
end
always@(posedge clk)
begin
if(!rst)
gray <= 0;
else if(ce)
gray <= gray_w[15:8];
else
gray <= gray;
end
(3) 中值滤波
为了保证图像质量,我们使用中值滤波来去除椒盐噪声,也就是求得 3*3 像素阵列的中间值.通过上图所示,我们用三级流水实现。
第一级:将3*3像素阵列划分为三份,每3个元素排序,得到相应的最大值、中间值、最小值。
第二级:将第一级得到的三个最大值排序得到其中的最小值,三个最小值排序得到其中的最大值,三个中间值排序得到其中的中间值。
第三级:将第二级得到的三个值排序得到中间值。
下面是三个数据排序部分代码:
always@(posedge clk)
begin
if(!rst)
begin
max_data <= 0;
mid_data <= 0;
min_data <= 0;
end
else if(ce)
begin
if(data1 >= data2 && data1 >= data3)
max_data <= data1;
else if(data2 >= data1 && data2 >= data3)
max_data <= data2;
else
max_data <= data3;
if((data1 >= data2 && data1 <= data3) || (data1 >= data3 && data1 <= data2))
mid_data <= data1;
else if((data2 >= data1 && data2 <= data3) || (data2 >= data3 && data2 <= data1))
mid_data <= data2;
else
mid_data <= data3;
if(data1 <= data2 && data1 <= data3)
min_data <= data1;
else if(data2 <= data1 && data2 <= data3)
min_data <= data2;
else
min_data <= data3;
end
else
begin
max_data <= max_data;
mid_data <= mid_data;
min_data <= min_data;
end
end
(4)累加压缩
为了减小缓存帧大小,我们对原来的灰度图像累加压缩,如上图所示.通过两级流水累加,对2*2像素阵列压缩.
效果如下图所示:
下面是累加压缩部分代码:
always @(posedge clk)
begin
if(!rst)
begin
av_buffer1<=0;
av_buffer2<=0;
average_value<=0;
end
else if(ce)
begin
av_buffer1<=data11+data12;
av_buffer2<=data21+data22;
average_value<=av_buffer1+av_buffer2;
end
else
begin
av_buffer1<=av_buffer1;
av_buffer2<=av_buffer2;
average_value<=average_value;
end
end
但需要注意的一点是,在我们的设计中,输出数据被下一级模块采样时序是与在中值滤波时完全不同。因为在一个同样长度的周期中,中值滤波模块输入1280个数据,输出1280个数据,累加压缩模块则是输入1280个数据,输出320个数据。
下图是两者采样时序的比较:
在实际设计中,我们使用计数器来实现累加压缩下一级采样,计数器从0到1279循环计数,采样使能在未与流水线使能ce相与前为(cnt[0]==0) && (cnt<640)。
(5)缓存背景帧
这里使用vivado的FIFO Generator生成基于block ram的同步fifo,write width为10,write depth为131072,使用了36个bram。
下面是缓存背景帧的部分代码,需要注意的是,在第一帧存入fifo时,fifo读使能无效,只有从第二帧开始,读使能才可能有效。这时背景帧存入的数据始终保持在320×240×10bit,每写入一个数据,就读出一个数据用作后续处理。
always @(posedge clk)
begin
if(!rst)
backframe_rd_valid<=0;
else if( ce && (bf_cnt==76799) )
backframe_rd_valid<=1;
else
backframe_rd_valid<=backframe_rd_valid;
end
fifo_backframe unit_fifo_backframe
(
.clk(clk),
.srst(!rst),
.din(average_value),
.wr_en( (cnt[0]==0) && (cnt<640) && ce ),
.rd_en( (cnt[0]==0) && (cnt<640) && ce && backframe_rd_valid),
.dout(back_pixel),
.data_count()
);
(6)重建展开
当前帧与缓存帧都经过累加压缩后的数据流,我们将两者展开,这将为后续帧差以及运动区域附加在原本完整图像上打下基础。
如下图所示,将压缩后的图像进行展开,实质上是像素数据流操作。
下面是进行上述操作的代码。
assign rd = (cnt>=640) && ce;
always @(posedge clk)
begin
if(!rst)
wr<=0;
else
wr<=(cnt<640) && ce;
end
always @(posedge clk)
begin
if(!rst)
cnt<=0;
else if( ce && (cnt==1279) )
cnt<=0;
else if( ce && (cnt!=1279) )
cnt<=cnt+1;
else
cnt<=cnt;
end
fifo_block_av_gen unit_fifo_block_av_gen
(
.clk(clk),
.srst(!rst),
.din(value_generated),
.wr_en(wr),
.rd_en(rd),
.dout(fifo_gen),
.data_count()
);
fifo_block_av_gen unit_fifo_block_bf_gen
(
.clk(clk),
.srst(!rst),
.din(bf_generated),
.wr_en(wr),
.rd_en(rd),
.dout(fifo_gen_bf),
.data_count()
);
always @(posedge clk)
begin
if(!rst)
value_generated<=0;
else if( (cnt[0]==0) && (cnt<640) && ce )
value_generated<=average_value;
else if( (cnt[0]==1) && (cnt<640) && ce )
value_generated<=value_generated;
else if( (cnt>=640) && ce )
value_generated<=fifo_gen;
else
value_generated<=value_generated;
end
always @(posedge clk)
begin
if(!rst)
bf_generated<=0;
else if( (cnt[0]==0) && (cnt<640) && ce )
bf_generated<=back_pixel;
else if( (cnt[0]==1) && (cnt<640) && ce )
bf_generated<=bf_generated;
else if( (cnt>=640) && ce )
bf_generated<=fifo_gen_bf;
else
bf_generated<=bf_generated;
end
这里对代码进行阐释,首先,我们设定一个循环周期,在这一周期内,我们输入压缩帧的第一行数据(共计320),输出完整帧的两行数据(640×2,共计1280)。
我们使用一个从0到1279循环计数的计数器来控制时序操作(下面提到的fifo大小为10bit*1024):
① 当计数器在0到639中,并且此值为奇数,那么读取压缩帧的一个像素输出,同时也写入fifo暂存。
② 当计数器在0到639中,并且此值为偶数,那么将之前读取的数据保持并输出,同时也写入fifo暂存。
③ 当计数器在639到1279中,那么逐个读取暂存在fifo中的一行共计640个数据并输出。
另外,写入fifo由于数据向后延一拍,那么写使能向后延一拍。
(7)帧差
输入为前一帧和后一帧的累加压缩像素数据流,将每一个对应的像素相减并取绝对值输出.
下面是帧差部分代码。
always @(posedge clk)
begin
if(!rst)
begin
sub_data1<=0;
sub_data2<=0;
end
else if(ce)
begin
sub_data1<=value_generated;
sub_data2<=bf_generated;
end
else
begin
sub_data1<=sub_data1;
sub_data2<=sub_data2;
end
end
always @(posedge clk)
begin
if(!rst)
abs_data<=0;
else if(ce)
begin
if(sub_data1>sub_data2)
abs_data<=sub_data1-sub_data2;
else
abs_data<=sub_data2-sub_data1;
end
else
abs_data<=abs_data;
end
(8)包围盒设计
本设计目标是对运动目标进行捕捉,所以我们通过读取阈值后的像素数据流,确定矩形包围盒的坐标,
需要设计矩形包围盒。
首先,我们需要一个行计数器(0479),一个列计数器(0639)来对读取到的单个像素定位。
这一部分代码如下:
reg [9:0] cnt_x;
reg [8:0] cnt_y;
always @(posedge clk)
begin
if(!rst)
cnt_x<=0;
else if(cnt_x<639 && ce_buf)
cnt_x<=cnt_x+1;
else if(cnt_x==639 && ce_buf)
cnt_x<=0;
else
cnt_x<=cnt_x;
end
always @(posedge clk)
begin
if(!rst)
cnt_y<=0;
else if(cnt_y<479 && ce_buf && cnt_x==639)
cnt_y<=cnt_y+1;
else if(cnt_y==479 && ce_buf && cnt_x==639)
cnt_y<=0;
else
cnt_y<=cnt_y;
end
需要注意的是,当前帧像素数据输入过程中检测包围盒时,当检测这一帧后,已经进入下一帧的处理了。那我们可以将前一帧检测的包围盒附加在后一帧上,也就是说,包围盒有一帧的延迟。
下面是两组包围盒坐标代码设计:
一组坐标保存上一帧运动区域,在每一帧结束后刷新一次。
always @(posedge clk)
begin
if(!rst)
begin
x1_cs<=0;
x2_cs<=0;
y1_cs<=0;
y2_cs<=0;
end
else if(cnt_y==479 && ce_buf && cnt_x==639)
begin
x1_cs<=x1_ns;
x2_cs<=x2_ns;
y1_cs<=y1_ns;
y2_cs<=y2_ns;
end
else
begin
x1_cs<=x1_cs;
x2_cs<=x2_cs;
y1_cs<=y1_cs;
y2_cs<=y2_cs;
end
end
另外一组坐标用于当前帧检测,处于改变过程中。x1检测横坐标最小值,x2检测横坐标最小值,y1检测纵坐标最小值,y2检测纵坐标最大值。
always @(posedge clk)
begin
if(!rst)
x1_ns<=0;
else if(cnt_y==479 && ce_buf && cnt_x==639)
x1_ns<=639;
else if(cnt_x<x1_ns && (abs_data>70) && ce_buf && (cnt_x>5) && (cnt_x<634))
x1_ns<=cnt_x;
else
x1_ns<=x1_ns;
end
always @(posedge clk)
begin
if(!rst)
x2_ns<=0;
else if(cnt_y==479 && ce_buf && cnt_x==639)
x2_ns<=0;
else if(cnt_x>x2_ns && (abs_data>70) && ce_buf && (cnt_x>5) && (cnt_x<634))
x2_ns<=cnt_x;
else
x2_ns<=x2_ns;
end
always @(posedge clk)
begin
if(!rst)
y1_ns<=0;
else if(cnt_y==479 && ce_buf && cnt_x==639)
y1_ns<=469;
else if(cnt_y<y1_ns && (abs_data>70) && ce_buf && (cnt_y>5) && (cnt_y<474))
y1_ns<=cnt_y;
else
y1_ns<=y1_ns;
end
always @(posedge clk)
begin
if(!rst)
y2_ns<=0;
else if(cnt_y==479 && ce_buf && cnt_x==639)
y2_ns<=0;
else if(cnt_y>y2_ns && (abs_data>70) && ce_buf && (cnt_y>5) && (cnt_y<474) )
y2_ns<=cnt_y;
else
y2_ns<=y2_ns;
end