FPGA实现人脸检测
之前的博客都是基本的图像处理,本篇博客整理一下用 FPGA 实现人脸检测的方法,工程比较有趣。
一、肤色提取
首先我们需要把肤色从外界环境提取出来,在肤色识别算法中,常用的颜色空间为YCbCr,Y 代表亮度,Cb 代表蓝色分量,Cr 代表红色分量。肤色在 YCbCr 空间受亮度信息的影响较小,本算法直接考虑 YCbCr 空间的CbCr 分量,映射为两维独立分布的 CbCr 空间。在 CbCr 空间下,肤色类聚性好,利用人工阈值法将肤色与非肤色区域分开,形成二值图像。

RGB 转 YCbCr的实现参照之前博客《FPGA实现图像灰度转换(2):RGB转YCbCr转Gray》。
根据经验,对肤色进行提取的条件场用如下不等式:
77 < Cb < 127,133 < Cr < 173.
代码基于 RGB_YCbCr_Gray,在最后本该输出灰度数据时,修改为输出肤色数据:
always @(posedge clk or negedge rst_n) begin if(!rst_n) begin face_data <= 'h0; end else if( (Cb2 > 77) && (Cb2 < 127) && (Cr2 > 133) && (Cr2 < 173) ) begin face_data <= 16'hffff; end else begin face_data <= 'h0; end end
用一副图片试试,原图如下所示:

肤色提取后结果如下所示:

完整代码如下所示:
//**************************************************************************
// *** 名称 : RGB565_face.v
// *** 作者 : xianyu_FPGA
// *** 博客 : https://www.cnblogs.com/xianyufpga/
// *** 日期 : 2020年3月
// *** 描述 : RGB565转YCbCr444,再根据肤色范围进行二值化
//--------------------------------------------------------------------------
// Y = 0.299*R + 0.587*G + 0.114*B
// Cb = 0.586*(B-Y) + 128 = -0.172*R - 0.339*G + 0.511*B + 128
// Cr = 0.713*(R-Y) + 128 = 0.511*R - 0.428*G - 0.083*B + 128
// --->
// Y = ( 77*R + 150*G + 29*B) >> 8
// Cb = (-43*R - 85*G + 128*B) >> 8 + 128
// Cr = (128*R - 107*G - 21*B) >> 8 + 128
// --->
// Y = ( 77*R + 150*G + 29*B) >> 8
// Cb = (-43*R - 85*G + 128*B + 32768) >> 8
// Cr = (128*R - 107*G - 21*B + 32768) >> 8
//**************************************************************************
module RGB565_face
//========================< 端口 >==========================================
(
input wire clk , //时钟
input wire rst_n , //复位
//input ---------------------------------------------
input wire RGB_hsync , //RGB行同步
input wire RGB_vsync , //RGB场同步
input wire [15:0] RGB_data , //RGB数据
input wire RGB_de , //RGB数据使能
//output --------------------------------------------
output wire face_hsync , //face行同步
output wire face_vsync , //face场同步
output reg [ 7:0] face_data , //face数据
output wire face_de //face数据使能
);
//========================< 信号 >==========================================
wire [ 7:0] R0, G0, B0 ;
reg [15:0] R1, G1, B1 ;
reg [15:0] R2, G2, B2 ;
reg [15:0] R3, G3, B3 ;
reg [15:0] Y1, Cb1, Cr1 ;
reg [ 7:0] Y2, Cb2, Cr2 ;
reg [ 3:0] RGB_de_r ;
reg [ 3:0] RGB_hsync_r ;
reg [ 3:0] RGB_vsync_r ;
//==========================================================================
//== RGB565转RGB888
//==========================================================================
assign R0 = {RGB_data[15:11],RGB_data[13:11]};
assign G0 = {RGB_data[10: 5],RGB_data[ 6: 5]};
assign B0 = {RGB_data[ 4: 0],RGB_data[ 2: 0]};
//==========================================================================
//== RGB888转YCbCr
//==========================================================================
//clk 1
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
{R1,G1,B1} <= {16'd0, 16'd0, 16'd0};
{R2,G2,B2} <= {16'd0, 16'd0, 16'd0};
{R3,G3,B3} <= {16'd0, 16'd0, 16'd0};
end
else begin
{R1,G1,B1} <= { {R0 * 16'd77}, {G0 * 16'd150}, {B0 * 16'd29 } };
{R2,G2,B2} <= { {R0 * 16'd43}, {G0 * 16'd85}, {B0 * 16'd128} };
{R3,G3,B3} <= { {R0 * 16'd128}, {G0 * 16'd107}, {B0 * 16'd21 } };
end
end
//clk 2
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
Y1 <= 16'd0;
Cb1 <= 16'd0;
Cr1 <= 16'd0;
end
else begin
Y1 <= R1 + G1 + B1;
Cb1 <= B2 - R2 - G2 + 16'd32768; //128扩大256倍
Cr1 <= R3 - G3 - B3 + 16'd32768; //128扩大256倍
end
end
//clk 3,除以256即右移8位,即取高8位
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n)begin
Y2 <= 8'd0;
Cb2 <= 8'd0;
Cr2 <= 8'd0;
end
else begin
Y2 <= Y1[15:8];
Cb2 <= Cb1[15:8];
Cr2 <= Cr1[15:8];
end
end
//clk 根据肤色范围进行二值化
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
face_data <= 8'h0;
end
else if( (Cb2 > 8'd77) && (Cb2 < 8'd127) && (Cr2 > 8'd133) && (Cr2 < 8'd173) ) begin
face_data <= 8'hff;
end
else begin
face_data <= 8'h0;
end
end
//==========================================================================
//== 信号同步
//==========================================================================
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
RGB_de_r <= 4'b0;
RGB_hsync_r <= 4'b0;
RGB_vsync_r <= 4'b0;
end
else begin
RGB_de_r <= {RGB_de_r[2:0], RGB_de};
RGB_hsync_r <= {RGB_hsync_r[2:0], RGB_hsync};
RGB_vsync_r <= {RGB_vsync_r[2:0], RGB_vsync};
end
end
assign face_de = RGB_de_r[3];
assign face_hsync = RGB_hsync_r[3];
assign face_vsync = RGB_vsync_r[3];
endmodule
二、滤波处理
图片还好,如果是摄像头数据会有很多噪声,针对噪声,我们可以用之前整理过的中值滤波、高斯滤波等处理。
此外人脸内部还会有些黑点,包括人脸外的环境可能有些地方也会被误检测为人脸,造成实验失败,因此可以加入形态学处理:腐蚀、膨胀、开运算、闭运算等,这些之前都整理过,不展开说了。
三、人脸框选
现在我们要用一个框将人脸框住,达到人脸检测的目的。
//**************************************************************************
// *** 名称 : img_box.v
// *** 作者 : xianyu_FPGA
// *** 博客 : https://www.cnblogs.com/xianyufpga/
// *** 日期 : 2020年3月
// *** 描述 : 将目标框住后输出
//**************************************************************************
module img_box
//========================< 参数 >==========================================
#(
parameter H_DISP = 12'd480 , //图像宽度
parameter V_DISP = 12'd272 //图像高度
)
//========================< 端口 >==========================================
(
input wire clk , //时钟
input wire rst_n , //复位
//RGB -----------------------------------------------
input wire RGB_hsync , //RGB行同步
input wire RGB_vsync , //RGB场同步
input wire [15:0] RGB_data , //RGB数据
input wire RGB_de , //RGB数据使能
//face ----------------------------------------------
input wire face_hsync , //face行同步
input wire face_vsync , //face场同步
input wire [ 7:0] face_data , //face数据
input wire face_de , //face数据使能
//key -----------------------------------------------
input wire [ 1:0] key_vld , //消抖后的按键值
//DISP ----------------------------------------------
output reg DISP_hsync , //最终显示的行同步
output reg DISP_vsync , //最终显示的场同步
output reg [15:0] DISP_data , //最终显示的数据
output reg DISP_de //最终显示的数据使能
);
//========================< 信号 >==========================================
reg face_vsync_r ;
wire pos_vsync ;
wire neg_vsync ;
//---------------------------------------------------
reg [11:0] face_x ;
wire add_face_x ;
wire end_face_x ;
reg [11:0] face_y ;
wire add_face_y ;
wire end_face_y ;
//---------------------------------------------------
reg [11:0] x_min ;
reg [11:0] x_max ;
reg [11:0] y_min ;
reg [11:0] y_max ;
reg [11:0] x_min_r ;
reg [11:0] x_max_r ;
reg [11:0] y_min_r ;
reg [11:0] y_max_r ;
//---------------------------------------------------
reg [11:0] RGB_x ;
wire add_RGB_x ;
wire end_RGB_x ;
reg [11:0] RGB_y ;
wire add_RGB_y ;
wire end_RGB_y ;
//---------------------------------------------------
reg mode ;
//==========================================================================
//== 帧开始和结束标志
//==========================================================================
always @(posedge clk) begin
face_vsync_r <= face_vsync;
end
assign pos_vsync = face_vsync && ~face_vsync_r;
assign neg_vsync = ~face_vsync && face_vsync_r;
//==========================================================================
//== 肤色图像的行列划分
//==========================================================================
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
face_x <= 12'd0;
else if(add_face_x) begin
if(end_face_x)
face_x <= 12'd0;
else
face_x <= face_x + 12'd1;
end
end
assign add_face_x = face_de;
assign end_face_x = add_face_x && face_x== H_DISP-12'd1;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
face_y <= 12'd0;
else if(add_face_y) begin
if(end_face_y)
face_y <= 12'd0;
else
face_y <= face_y + 12'd1;
end
end
assign add_face_y = end_face_x;
assign end_face_y = add_face_y && face_y== V_DISP-12'd1;
//==========================================================================
//== 帧运行:人脸框选
//==========================================================================
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
x_min <= H_DISP;
end
else if(pos_vsync) begin
x_min <= H_DISP;
end
else if(face_data==8'hff && x_min > face_x && face_de) begin
x_min <= face_x;
end
end
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
x_max <= 'd0;
end
else if(pos_vsync) begin
x_max <= 'd0;
end
else if(face_data==8'hff && x_max < face_x && face_de) begin
x_max <= face_x;
end
end
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
y_min <= V_DISP;
end
else if(pos_vsync) begin
y_min <= V_DISP;
end
else if(face_data==8'hff && y_min > face_y && face_de) begin
y_min <= face_y;
end
end
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
y_max <= 'd0;
end
else if(pos_vsync) begin
y_max <= 'd0;
end
else if(face_data==8'hff && y_max < face_y && face_de) begin
y_max <= face_y;
end
end
//==========================================================================
//== 帧结束:保存坐标值
//==========================================================================
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
x_min_r <= 'd0;
x_max_r <= 'd0;
y_min_r <= 'd0;
y_max_r <= 'd0;
end
else if(neg_vsync) begin
x_min_r <= x_min;
x_max_r <= x_max;
y_min_r <= y_min;
y_max_r <= y_max;
end
end
//==========================================================================
//== 原图的行列划分
//==========================================================================
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
RGB_x <= 12'd0;
else if(add_RGB_x) begin
if(end_RGB_x)
RGB_x <= 12'd0;
else
RGB_x <= RGB_x + 12'd1;
end
end
assign add_RGB_x = RGB_de;
assign end_RGB_x = add_RGB_x && RGB_x== H_DISP-12'd1;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
RGB_y <= 12'd0;
else if(add_RGB_y) begin
if(end_RGB_y)
RGB_y <= 12'd0;
else
RGB_y <= RGB_y + 12'd1;
end
end
assign add_RGB_y = end_RGB_x;
assign end_RGB_y = add_RGB_y && RGB_y== V_DISP-12'd1;
//==========================================================================
//== 按键切换不同显示效果
//==========================================================================
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
mode <= 1'b0;
end
else if(key_vld[0]) begin
mode <= 1'b1;
end
else if(key_vld[1]) begin
mode <= 1'b0;
end
end
//==========================================================================
//== 最终数据输出:包围盒+图像
//==========================================================================
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
DISP_hsync <= 1'b0;
DISP_vsync <= 1'b0;
DISP_data <= 16'b0;
DISP_de <= 1'b0;
end
//--------------------------------------------------- 输出包围盒+原图
else if(mode==1'b0) begin
DISP_hsync <= RGB_hsync;
DISP_vsync <= RGB_vsync;
DISP_de <= RGB_de;
if((RGB_y >= y_min_r-1 && RGB_y <= y_min_r+1) && RGB_x >= x_min_r && RGB_x <= x_max_r) begin
DISP_data <= 16'b11111_000000_00000;
end
else if((RGB_y >= y_max_r-1 && RGB_y <= y_max_r+1) && RGB_x >= x_min_r && RGB_x <= x_max_r) begin
DISP_data <= 16'b11111_000000_00000;
end
else if((RGB_x >= x_min_r-1 && RGB_x <= x_min_r+1) && RGB_y >= y_min_r && RGB_y <= y_max_r) begin
DISP_data <= 16'b11111_000000_00000;
end
else if((RGB_x >= x_max_r-1 && RGB_x <= x_max_r+1) && RGB_y >= y_min_r && RGB_y <= y_max_r) begin
DISP_data <= 16'b11111_000000_00000;
end
else begin
DISP_data <= RGB_data;
end
end
//--------------------------------------------------- 输出包围盒+肤色
else if(mode==1'b1) begin
DISP_vsync <= face_vsync;
DISP_de <= face_de;
DISP_hsync <= face_hsync;
if((face_y >= y_min_r-1 && face_y <= y_min_r+1) && face_x >= x_min_r && face_x <= x_max_r) begin
DISP_data <= 16'b11111_000000_00000;
end
else if((face_y >= y_max_r-1 && face_y <= y_max_r+1) && face_x >= x_min_r && face_x <= x_max_r) begin
DISP_data <= 16'b11111_000000_00000;
end
else if((face_x >= x_min_r-1 && face_x <= x_min_r+1) && face_y >= y_min_r && face_y <= y_max_r) begin
DISP_data <= 16'b11111_000000_00000;
end
else if((face_x >= x_max_r-1 && face_x <= x_max_r+1) && face_y >= y_min_r && face_y <= y_max_r) begin
DISP_data <= 16'b11111_000000_00000;
end
else begin
DISP_data <= {face_data[7:3],face_data[7:2],face_data[7:3]};
end
end
end
endmodule
x 和 y为图像的实时坐标值,TFT_x 和 TFT_y 为 TFT_driver 生成的坐标值,这两个是不一样的。如果二者一样,最后的图像会有偏移。框的四个顶点坐标代码挺有意思,一开始很难理解,带几个数去看看就明白了,这段代码挺巧妙的,也挺简洁的。总体的思想和直方图拉伸很像,分两帧来处理,第一帧得到顶点坐标,当前帧的输出则实时的使用这个顶点坐标,因为两帧图像的差别很小,所以这么做比较方便。
要注意的是每次扫描一帧后,顶点坐标要变回初始值,否则会出错,这点在图片的处理上体会不到什么,感觉不出bug,但是移植到摄像头视频数据时,不变回初始值就会有问题。这里的时序挺有意思,一开始我以为要打拍什么的,后面发现其实得到的是一个坐标值,坐标值本身在一帧结束到下一帧结束这段时间里是固定的,没必要打拍,但是要寄存住,和直方图拉伸一样。
最终输出的结果是一个绿色的矩形框,非矩形框区域则输出原始视频数据,效果如下所示:

板卡坏了哈,本来颜色很好的。
四、基于 OV7670 的人脸检测工程
算法方面直接移植即可,注意的是,OV7670摄像头很差劲,噪声很多,需要进行一定的滤波处理,否则效果很差劲。
我一开始直接移植写好的图像版本的主要代码,结果基本没有效果,以为是程序出错,检查了半天没找到毛病。后面将显示改成“框+二值图”,终于发现了屏幕上全是椒盐噪声,难怪没法成功,而如果用 OV7725 或 OV5640 等摄像头,这样的问题应该没这么严重。设备不给力,算法来使劲,我在肤色提取后连续进行了三次中值滤波去噪,然后用了一次腐蚀算法,最终的效果才勉强成功。
视频演示如下所示,不想上镜,人脸改成手来测试:
由于摄像头太差,加上板卡出现问题,颜色失真,导致看起来不漂亮,但总的结果还是对的。
五、基于OV7725的人脸检测工程
回学校修好了板卡,换了 OV7725 摄像头,效果好多了。
参考资料:
[1]OpenS Lee:FPGA开源工作室(公众号)
[2]NingHechuan:硅农(公众号)

浙公网安备 33010602011771号