FPGA实现人脸检测

  之前的博客都是基本的图像处理,本篇博客整理一下用 FPGA 实现人脸检测的方法,工程比较有趣。

 

一、肤色提取

  首先我们需要把肤色从外界环境提取出来,在肤色识别算法中,常用的颜色空间为YCbCr,Y 代表亮度,Cb 代表蓝色分量,Cr 代表红色分量。肤色在 YCbCr 空间受亮度信息的影响较小,本算法直接考虑 YCbCr 空间的CbCr 分量,映射为两维独立分布的 CbCr 空间。在 CbCr 空间下,肤色类聚性好,利用人工阈值法将肤色与非肤色区域分开,形成二值图像。

image

  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:硅农(公众号)

posted @ 2020-03-28 17:17  咸鱼IC  阅读(9224)  评论(16)    收藏  举报