基于FPGA的人脸检测及人数统计

基于FPGA的人脸检测及人数统计

  • 基于FPGA的人脸检测及人数统计
    • 基于Matlab的人脸检测及人数统计
    • 基于FPGA的人脸检测
      • 抱歉,有 ~~钱~~ 内存就是可以为所欲为
      • 数据流
      • 模块设计
    • 人数统计:mybwlabel
    • 最后的展示

基于FPGA的人脸检测及人数统计

第一次写博客,新手上路,请多关照哈。

闲话就不多说了。这次做数电大作业,笔者主要参考了下面三篇博文(顺便感谢一下这位创作者)。但代码也不是照搬就能用的,笔者对其进行了改进与创新。详情见后文。

基于MATLAB的人脸检测

基于FPGA的人脸检测(1)

基于FPGA的人脸检测(2)

本次实验所用到的软硬件环境如下:

1、VIVADO2017.4软件环境

2、EGO1开发板,板载 Xilinx Artix-7 系列 XC7A35T-1CSG324C FPGA
(FPGA是什么?Field Programmable Gate Array,“现场可编程逻辑门阵列”,说人话,即可以编程的硬件,用于编程的语言叫硬件描述语言(HDL),verilog语言就是一种HDL,就是本次实验使用的语言)

3、MATLABR2020b(用于验证实验结果的正确性)、pythonIDE(用于将图片转为位图数据以及将FPGA回传的位图数据转为图片)、DevC++(用于验证算法的可行性)、VSCode(用于写代码,vivado写代码的界面太不好用了…)

4、串口调试助手(用于上位机的数据发送与接收,与开发板采用UART串口通信)

这次试验干了些什么呢?简要概括,就是通过上位机向FPGA发送一张图片的信息,然后FPGA识别出图片里有多少个后将信息显示在EGO1开发板的七段数码管上,并将缩略图显示在VGA上,等图片处理好后,可以将处理后的黑白图片回传给上位机。

基于Matlab的人脸检测及人数统计

工欲善其事,必先利其器。Matlab是一个强大的工具,据说除了生孩子啥都能干。强大的库函数是它的力量源泉。

clc;
clear all;
close;
image = imread('near1024x768.bmp');

%------------------------原图----------------------------

subplot(2,2,1)
image_red = image(:,:,1);
image_green = image(:,:,2);
image_blue = image(:,:,3);
[ROW,COL] = size(image_red);
figure(1)
imshow(image);
title('原图','Color','black','FontSize',14);

%------------------------肤色检测----------------------------
YCbCr = rgb2ycbcr(image); %将RGB格式转为YCbCr格式

Y = YCbCr(:,:,1);
Cb = YCbCr(:,:,2);
Cr = YCbCr(:,:,3);
pic_gray = zeros(ROW,COL);

for i = 1:ROW
    for j = 1:COL
        if(Cb(i,j) > 77 && Cb(i,j) < 127 && Cr(i,j) > 133 && Cr(i,j) < 173)
            pic_gray(i,j) = 255;
        else
            pic_gray(i,j) = 0;
        end
    end
end

subplot(2,2,2)
imshow(pic_gray);
title('肤色检测图','Color','black','FontSize',14);

%------------------------腐蚀----------------------------
subplot(2,2,3)
erodesize = 41;
se = strel('square',erodesize);

erode = imerode(pic_gray,se); 

imshow(erode);
strerode = sprintf('腐蚀(卷积核大小:%d)\n',erodesize);
title(strerode,'Color','black','FontSize',14);

%------------------------膨胀----------------------------
subplot(2,2,4)
imdilatesize = 51;
se1 = strel('square',imdilatesize);

image_1 = imdilate(erode,se1);

imshow(image_1);


%------------------计数连通区域--------------------
image_label = im2bw(image_1);

[labeled,number]=bwlabel(image_label,4);

strnumber = sprintf('人数:%d',number);

%-----------------添加最后的标题---------------------
strimdilate = sprintf('再膨胀(卷积核大小:%d)\n%s',imdilatesize,strnumber);
title(strimdilate,'Color','black','FontSize',14 );

这次实验的最关键最关键的部分在于,如何将Matlab里的三个库函数移植到FPGA上。这三个库函数分别是:

  • imerode, 对图像进行腐蚀运算
  • imdilate, 对图像进行膨胀运算
  • bwlabel, 对图像进行计数连通域,可以返回连通区域的个数

以下是关于这三个函数的详细介绍,笔者就不赘述了。
图像腐蚀

Matlab图像形态学处理

bwlabel函数(二值图像中元素标记)

单看代码还是很抽象,放两张Matlab生成的实验结果,就能很快说明很多问题了。

基于FPGA的人脸检测及人数统计_第1张图片

基于FPGA的人脸检测及人数统计_第2张图片
实验的原理并不复杂。首先将原图从RGB格式转为YCbCr格式,因为YCbCr格式中Y是亮度信息、Cr表示红色信息、Cb表示绿色信息。转换成此格式的原因是为了将亮度相关的信息取消掉,进而利用红色分量和绿色分量判断出哪里是肤色。经过数据分析,肤色的分量处于下面的范围:

Cb(i,j) > 77 && Cb(i,j) < 127 && Cr(i,j) > 133 && Cr(i,j) < 173

其次,对图像进行腐蚀运算。

erodesize = 41;
se = strel('square',erodesize); %生成一个41x41的正方形

erode = imerode(pic_gray,se);

注意到imerode函数有两个参数,一个是灰度图,另一个就是用来运算的卷积核。这里的卷积核可以理解为一个小正方形,小正方形的大小是一个重要的参数(也就是上面的erodesize),决定了腐蚀后的图像形态。卷积核大,则腐蚀后剩余的白色区域小、少;卷积核小,则腐蚀后剩余的白色区域大、多。用到腐蚀函数是因为,我们要去除图像中的噪声干扰

那为什么之后还要用到膨胀函数呢?这是因为,膨胀函数可以填补腐蚀后图像白色区域之间的缝隙,减少误算。举个例子,第二张图里由于存在黑色的眼镜,人脸经过腐蚀后并不是完整的一块白色,而是分成了五块但经过膨胀后,又连为一体了。

经过多次实验,笔者发现膨胀核略大于腐蚀核,图像的处理效果比较好,结果较为准确。

基于FPGA的人脸检测

用Matlab验证了实验的可行性,只是万里长征走完了第一步。须知硬件描述语言verilog和Matlab语言是不一样的,是没有得天独厚的那么多库函数的,只能将其算法一步步的移植到硬件上。好在我们只需要移植三个函数,而前两个,imerode和imdilate的原理几乎相同,看样子是十分简单的。可实际操作起来就是另一回事情了。

抱歉,有 内存就是可以为所欲为

笔者很快来到了实验的第一个瓶颈:内存问题。笔者立志像那篇博客一样,处理1024x768的分辨率的图片。但不算不知道,一算吓一跳。我们平时使用的图片通常是24位RGB图,也就是一个像素点用24位二进制数表示,这样的话,一张1024x768的位图大小是1024x768x24 = 18874368bit = 18432kbit = 18Mbit.
(这里要啰嗦一句MbMB的区别:由于计算机上的存储是以字节(Byte)为单位的,1Byte = 8Bit,所以这个18Mb的图片在电脑上显示大小的是18/8 = 2.25MB )

而我们的FPGA有多大的内存呢?它只有2Mb的内存,是的,只有2Mb.即便加上板载的一块2Mb的SRAM,笔者的手头也仅仅有4Mb,和18Mb的图片根本不在一个数量级。图片连存都存不下,又何谈图片处理呢?

那篇博客是如何处理的呢?仔细查看了下介绍,发现它用到了DDR3内存条,而查阅资料才知道,DDR3的内存空间动辄1GB!此外,用FPGA控制DDR3的存取需要用verilog语言编写复杂的DDR3控制模块,对我这样的新手来说是史诗级难度。巧妇难为无米之炊!所以只能望洋兴叹、坐以待毙了?

数据流

我还没有学过C++语言,但有一天偶尔看到它的iostream,启发了我。须知,我们的数据是以数据流的形式进入FPGA的,因此,并没有必要使用那么多的内存空间。打个比方,好比我们要检测长江某个节点一年的水质变化情况,那么我们在它的那个节点检测一年流过的水流就可以了,并没有必要像核废水那样,把长江一年的巨量的流量都储存到罐子里,再一个个去检验。也就是说,数据像水流一样,流过,留下有用的信息后就没有用了。根据这个思路,具体到本次实验,对我们来说有用的信息是什么呢?

1.经过处理后的图像数据。由于彩色图像经过处理后,转化为黑白二值图像,也就是说一个像素点只需要1个bit就可以表示。需要占用1024*768 bit = 768kb的内存。

2.彩色缩略图。由于笔者希望将结果展示在VGA上,因此少不了原图的展示,上面已经提到,我们没有那么多空间储存1024x768的24位RGB图。考虑到VGA的分辨率是640x480、
开发板只能输出12位RGB的图以及内存的限制,笔者最终决定将1024x768的24位RGB图转化为256x192的12位RGB缩略图存储到内存里,需要占用 25619212/1024 = 576kb的内存。

3.检测的人数结果。这个占用的内存就是一个小小的寄存器,不需要格外考虑它的内存问题。

综上,我们至少需要768+576 = 1344kb的内存用来存储检测结果,小于FPGA自带的1800kbBRAM空间。内存问题就这样解决了。

模块设计

接下来就要考虑如何设计模块了。为了便于说明问题,先上几张最后的成果图。

首先是各个模块的层级关系
基于FPGA的人脸检测及人数统计_第3张图片

基于FPGA的人脸检测及人数统计_第4张图片
这是top_0_demo的电路原理图
基于FPGA的人脸检测及人数统计_第5张图片
把top0打开的电路原理图
基于FPGA的人脸检测及人数统计_第6张图片
在核心的几个模块处放大
基于FPGA的人脸检测及人数统计_第7张图片
top模块:数据流(rx_data)以比特流(1位)的形式从串口进入,先通过uart_top模块将其变为字节流(8位)的形式,再通过top模块里的状态机(FSM)实现移位寄存器的功能,将字节流转化为24位的数据流,进行下一步处理;FSM同时对数据进行取样,每16个像素取一次样,取每个像素各8位RGB分量的高4位,一共12位存入BRAM,产生256x192的12位RGB位图。上面那眼花缭乱的一堆线路大部分都是FSM。

1位比特流
8位字节流
24位
12位
24位
24位
1位
1位
1位
1位
12位
1位
rx_data
uart_top
FSM
data_stream
BRAM0,576kb
rgb2ycbcr
face_test
myerode
myimdilate
mydwlabel
BRAM1,768kb
VGA显示模块
  • rgb2ycbcr:将24位RGB转为24位YCbCr
  • face_test:检测该像素点的YCbCr信息是否符合肤色,如果是,则输出1,如果不是,则输出0.因此,24位的数据流变为了1位的数据流。
  • myerode:图像腐蚀
  • myimdilate:图像膨胀
  • mydwlabel:计数连通域

基于FPGA的人脸检测及人数统计_第8张图片

注意到上面模块里有输入的pi_flag和输出的po_flag。在数据流经的各个模块的过程中,flag很重要。这个flag可以理解为采样的时间。上游的模块处理好数据后,就给下游一个flag信号脉冲,从而使数据处理有条不紊。

想清楚这些问题后,就可以开始编写myerode模块了。若卷积核的大小为size,则myerode里使用一块位宽为size,深度为1024的内存,用于储存流过的信息。这是因为在腐蚀图像的时候,原图与腐蚀后的图像构成映射关系。腐蚀后的图像上某一个位置的点的数值,取决于这个原图上这个对应位置周围的sizexsize个点的数值,是多对一的关系。因此,myerode模块所做的,就是把输入的这幅拉成1维的细丝图像数据流,先在内存里恢复为2维,再将其拉成1维输出到下一个模块。768x1024的内存肯定能干这个问题,但我们希望使用的内存越小越好,sizex1024的内存就够了。假设下面这张图是输入的数据,那么数据流是它从第一行向右到头、再从第二行向右逐个扫描输出的。数据流先填满空的size*1024的内存后,每个地址的size位数据开始进行移位寄存器的操作,也就是说新的数据会把最上面的最旧的数据挤出去。整个过程等同于图像从下往上运动,逐步用自己的不同位置填充这块内存。运动是相对的,这个过程也相当于用这块内存对图像进行了由上到下的扫描。

基于FPGA的人脸检测及人数统计_第9张图片
还有一个细节问题:myerode输出数据的时间相比输入的时间会有所滞后。换句话说,就是myerode不能在数据刚进来的时候就输出数据。这是因为它得等待那sizexsize的矩阵在内存中成形。只有成形后,才能输出第一个处理后的像素点。此外,对于边缘位置的周围无法形成sizexsize正方形的点,该位置一致输出0。以上使用状态机实现。原理不难,但实操中有些细节一开始没考虑好,导致仿真调试失败了上百次才成功。

myimdilate和myerode是一堆双胞胎,因此不再赘述。

人数统计:mybwlabel

一张黑白二值图像,如何计算它的连通域的数目?刚来到这个问题时,笔者一无所知,不知道从何下手。问了数学专业的同学,同学说可以用深度搜索算法,给笔者发了一段C++代码。代码如下:

#include
using namespace std;
int a[1000][1000]={0};
int flag[1000][1000]={0};
int dx[4]={0,-1,0,1};
int dy[4]={1,0,-1,0}''
void deepfirst(a,b){
    flag[a][b]=1;
    for(int i=0;i<=3;i++){
        int tempa=a+dx[i];
        int tempb=b+dx[j];
        if(flag[tempa][tempb]==1){
            continue;
        }
        else{
            deepfirst(tempa,tempb);
        }
    }
}
int main(){
    \\input a[m][n]
    sumnum=0;
    for(int i=1;i<=m;i++){
        for(int j=1;j<=n;j++){
            if(a[i][j]=0){
                continue;
            }
            else{
                if(flag[i][j]=1){
                    continue;
                }
                else{//还没搜索过的1
                    deepfirst(i,j);
                    sumnum++;
                }
            }
        }
    }

}

注意:此flag非彼flag。上面的flag是时序用途。这里的flag是一个矩阵,用来插flag,是空间用途。
这个算法可以描述为:先对图像进行扫描,如果找到一个1,那么计数器加1,在它对应的flag矩阵位置上插上flag,然后就在它上下左右找,找到的话也插上flag,找不到就结束。有flag的地方不再寻找。

这个方法的确挺巧妙。但是要移植到FPGA上的话,有两个问题。

  1. 内存问题。这个其实也不是太大的问题。也就是需要额外的一块flag内存,768k,我们支付得起。
  2. 递归问题。verilog语言能用递归吗?!

递归问题确实是一个问题。回想笔者在C程序设计上浅尝辄止的了解到的递归在计算机上实现的本质:程序是以堆栈的形式运行的。使用递归的时候,就需要额外申请动态的内存,等递归结束的条件满足的时候,就可以逐步释放。笔者认识到(以笔者目前的知识水平),不可能用verilog实现递归。verilog不允许模块自己调用自己,因为考虑到现实情况,在硬件上运行的模块只能是有限个,内存只能是有限的、静态的。

在这里,我们遇到了第二个重要的瓶颈。笔者苦苦思索两天后,想出了一个并不完美,但可以实现的算法。代码如下。

#include 

void scan(int a[], int *num){
	
	int scan_state = 0;
	int j;
	for (j=0; j<8; j++){  //遍历那一行的每一位,从低位开始 
		if (a[j] != 0 && scan_state==0){ 

			(*num)++;
			scan_state = 1;
			
		}else if (a[j] != 0 && scan_state==1){  
		
			scan_state = 1;
			
		}else {  //如果遇到0 
		
			scan_state = 0;

		}

	}
	
}


int bwlabel(int (*a)[8]){
	
	int tempAnd[8];  //上下两行与运算的结果 
	int num = 0;
	int num1 = 0, numAnd = 0;
	
	int i,k;
	
	scan(a[0], &num);  //第一行 
	printf("num = %d\n",num);
	
	for (i=0; i<8-1; i++){
		
		
		for (k=0; k<8; k++){
			tempAnd[k] = a[i][k] && a[i+1][k];
			//printf("%d, ",tempAnd[k]);
		}
		//printf("\n");
		scan(a[i+1], &num1);
		scan(tempAnd, &numAnd);

		num +=  num1 - numAnd;
		
		printf("num1 = %d ",num1);
		printf("numAnd = %d ",numAnd);
		printf("num = %d\n",num);

		num1 = 0;
		numAnd = 0;
	}
	
	return num;
}




int main()
{
	int a[8][8] =
	{
		{0, 0, 0, 0, 0, 0, 0, 0,}, 
		{0, 0, 0, 0, 0, 0, 0, 0,}, 
		{0, 0, 0, 0, 1, 1, 1, 0,}, 
		{0, 0, 0, 0, 1, 1, 1, 0,}, 
		{0, 0, 0, 0, 1, 1, 1, 0,}, 
		{0, 0, 0, 0, 0, 0, 0, 0,}, 
		{0, 1, 0, 0, 0, 0, 0, 0,}, 
		{0, 0, 0, 0, 0, 0, 0, 0,}, 
		
	}; 
	
	int number_a;
	
	number_a = bwlabel(a);
	printf("number_a = %d\n",number_a);
	
	return 0;
}

输出结果:

num = 0
num1 = 0 numAnd = 0 num = 0
num1 = 1 numAnd = 0 num = 1
num1 = 1 numAnd = 1 num = 1
num1 = 1 numAnd = 1 num = 1
num1 = 0 numAnd = 0 num = 1
num1 = 1 numAnd = 0 num = 2
num1 = 0 numAnd = 0 num = 2
number_a = 2  //一共有两个连通区域

它的原理是:对矩阵逐行扫描,计算第一行有几段1的连续序列,存入num。扫描第二行时,先计算第二行有几段1的连续序列,存入num1,再计算第二行和第一行进行按位与的运算(&)后这一行数据有几段1的连续序列,存入numAnd。之后执行

num +=  num1 - numAnd;
num1 = 0;
numAnd = 0;

简而言之,可以看做我们把图像切成了一行一行的线段的数据。如果上一行与下一行&后为0,说明这两行毫无瓜葛,计数可以直接将上一行的线段数与下一行的线段数相加;如果上一行与下一行&后有1,说明上一行与下一行之间的1有连着的地方,所以我们要减去多余的numAnd。

FPGA表示很高兴,因为它最擅长做按位运算了。

具体在mybwlabel中实现的时候,这个计数其实用不着状态机。我们可以监测一行数据流的上升沿(posedge,也就是从0到1),一行数据流有几个上升沿,就说明有几段1的数据。

别高兴太早。前面说过它是不完美的。说它不完美,是因为只有当1的区域内部没有值为0的空洞的时候,它才能够正确计数。但是这似乎还是可以接受的。为什么?因为myimdilate模块对图像膨胀后可以帮我们填好那些空洞

最后的展示

实验记录
基于FPGA的人脸检测及人数统计_第10张图片
基于FPGA的人脸检测及人数统计_第11张图片
上面数码管左边显示的是人数;右边是腐蚀核和膨胀和的大小笔者最终设定了四种卷积核的大小。设定不同大小是因为近处的照片和远处的照片的噪声大小不同。近处的人脸需要使用大核,远处的人脸需要使用小核,就像焦距的大小一样。

序号 腐蚀核大小 膨胀核大小
00 11 21
01 21 31
10 31 41
11 41 51

从FPGA回传到上位机的两张图像
基于FPGA的人脸检测及人数统计_第12张图片
基于FPGA的人脸检测及人数统计_第13张图片
可以看到,FPGA处理的结果和Matlab处理的结果完全一致。

你可能感兴趣的:(fpga)