5万字用纯C语言从零开始实现人脸检测

文章目录

前言

读者你好! 这是我第一次写博客,也是我第一次对图像处理和人脸检测进行研究,所以这篇文章会有许多错误,请读者多多包含。但正如标题所述,是用C从读取二进制图像文件开始,到基本的图像处理,再到实现人脸检测(传统VJ算法),过程中并不需要下载任何第三方库。这篇文章记录了作者一整个的学习流程。且文中会毫无保留的展示所有功能的实现,和其相应的代码。
注意:1.每个知识点间的代码都是相互独立的,可直接复制进行测试运行。

一. BMP文件的处理

1.1 为什么选择BMP文件

BMP文件是未经压缩的文件,它保存了原图的所有的像素信息,文件体积最大,文件所包含的信息也最多,BMP的文件结构相对与JPG和PNG简单很多,易于处理和学习。(网络上一般以JPG格式的图片为主,所以使用时要进行格式转换,建议用电脑自带的画图工具转换。有些格式转换工具可能会加入一些其它的字节信息,破坏其原本结构)

1.2 BMP文件结构的介绍

5万字用纯C语言从零开始实现人脸检测
1.)bmp文件的组成
bmp文件 = bmp头部(bmp文件头(14字节)+位图信息头(40字节))+位图数据 (bmp文件中的数据一个字节为一组,正向写入倒向读出)

2.)bmp文件头(14字节)
a.(2)424D---->‘BM’,表示这是Windows支持的位图格式。
b.(4)B6EA0500---->0050EAB6---->整个文件的大小(正向写入倒向读出)
c.(2)0000---->保留段,通常为0
d.(2)0000---->保留段,通常为0
e.(4)36000000---->从文件头到位图数据需偏移的字节数,即整个bmp头部的大小(54字节)

2.)位图信息头(40字节)
a.(4)28000000---->00000028---->位图信息头的大小(40)
b.(4)E9000000---->000000E9---->图片宽度(正向写入倒向读出)
c.(4)A0010000---->000001A0 ---->图片高度(正向写入倒向读出)
d.(2)0100->0001---->颜色平面数,即调色盘数,恒等于1
d.(2)2000->0020---->图片颜色的位数(32)
f.(4)00000000---->图像压缩比,不压缩则为
g.(4)00000000---->图像的大小,因为使用BI_RGB,所以设置为0
h.(4)00000000---->水平分辨率,缺省为0
i.(4)00000000---->垂直分辨率,缺省为0
j.(4)00000000---->说明本位图实际使用调色盘的颜色索引数,0表示全部
k.(4)00000000---->说明本位图重要调色盘的颜色索引数,0表示全都重要

1.3 BMP文件的读取和保存

代码说明与注意事项:
1. 代码中有详细的注释,核心就是C语言对二进制文件的读取和写入,但由于结构体对齐问题的存在,所以我选择最保险的,一条一条读取和写入的方式。
2. 虽然目前只能读取BMP文件,但还是保留了JPG的接口空位,后续可对其功能进行拓展。
3. BMP有32位24位16位8位,该程序只对前两个进行了处理
4. 计算机读取能被4整除大小的二进制文件时速度较快,所以读取24位图像的二进制文件时,每一行都会有补零操作。(每个图像每行补零的字节个数是固定的,且最多补3个字节)

计算要补零的字节个数的公式
( 3 ⋅ w + k ) mod ⁡ 4 = 0 \left(3\cdot w+k\right)\operatorname{mod}4=0 (3⋅w+k)mod4=0
w:是图像的宽度
k:要补几个字节零
mod:取余

#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<math.h>
#include<string.h>

// 获取文件的后缀名
char* GetFlieExta(char* filename)
{
    int fileLen = strlen(filename);
    int exLen = 0;
    char fileExta[255];
    memset(fileExta, 0, sizeof(fileExta));

    for (int i = fileLen-1; i > 0; i--)
        if (filename[i] == '.'){
            exLen = fileLen - i;
            break;
        }
    strncpy(fileExta, filename + fileLen - exLen, exLen);
    return fileExta;
}


// BGRA颜色结构体
typedef struct tagBGRA
{
	unsigned char blue;          // 该颜色的蓝色分量  (值范围为0-255)
	unsigned char green;         // 该颜色的绿色分量  (值范围为0-255)
	unsigned char red;           // 该颜色的红色分量  (值范围为0-255)
	unsigned char transparency;  // 透明度,在bmp中是保留值,无实际效果
}BGRA, * PBGRA;

// 图像结构体
typedef struct tagIMAGE
{
	unsigned int w;
	unsigned int h;
    BGRA* color;
}IMAGE, * PIMAGE;


// BMP文件的处理

// BMP文件头结构体
typedef struct tagBITMAP_HEAD_INFO
{
    /* bmp文件头的信息,有#的是重点!!*/
    // bmp文件头
    unsigned short  bfType;             // 0x424D,即BM字符串,表明是bmp格式文件
    unsigned int    bfSize;             // ###总的bmp文件大小 以字节为单位     
    unsigned short  bfReserved1;        // 保留,必须设置为0                     
    unsigned short  bfReserved2;        // 保留,必须设置为0 
    unsigned int    bfOffBits;          // ###总的bmp头部的大小(包括位图信息头),即到像素数据的偏移  
    // 位图信息头
    unsigned int    biSize;             // 位图信息头的大小
    unsigned int    biWidth;            // ###图像的宽  
    unsigned int    biHeight;           // ###图像的高  
    unsigned short  biPlanes;           // 颜色平面数,即调色盘数,恒等于1 
    unsigned short  biBitCount;         // ###图片颜色的位数,一般为32
    unsigned int    biCompression;      // 说明图象数据压缩的类型,0为不压缩
    unsigned int    biSizeImage;        // 像素数据所占大小,因为使用BI_RGB,所以设置为0
    unsigned int    biXPelsPerMeter;    // 说明水平分辨率,缺省为0
    unsigned int    biYPelsPerMeter;    // 说明垂直分辨率,缺省为0
    unsigned int    biClrUsed;          // 说明本位图实际使用调色盘的颜色索引数,0表示全部
    unsigned int    biClrImportant;     // 说明本位图重要调色盘的颜色索引数,0表示全都重要
}BITMAP_HEAD_INFO,*PBITMAP_HEAD_INFO;

// 加载BMP图片
IMAGE Image_bmp_load(char* filename)
{
    IMAGE im;
    BITMAP_HEAD_INFO bmpHeadInfo;
    FILE* fp;

    if ((fp = fopen(filename, "rb")) == NULL)
        printf("打开%s文件失败!\n", filename);
    
    // 读取bmp头部
    // bmp文件头
    fread(&bmpHeadInfo.bfType, 1, sizeof(bmpHeadInfo.bfType), fp);
    fread(&bmpHeadInfo.bfSize, 1, sizeof(bmpHeadInfo.bfSize), fp);
    fread(&bmpHeadInfo.bfReserved1, 1, sizeof(bmpHeadInfo.bfReserved1), fp);
    fread(&bmpHeadInfo.bfReserved2, 1, sizeof(bmpHeadInfo.bfReserved2), fp);
    fread(&bmpHeadInfo.bfOffBits, 1, sizeof(bmpHeadInfo.bfOffBits), fp);
    // 位图信息头
    fread(&bmpHeadInfo.biSize, 1, sizeof(bmpHeadInfo.biSize), fp);
    fread(&bmpHeadInfo.biWidth, 1, sizeof(bmpHeadInfo.biWidth), fp);
    fread(&bmpHeadInfo.biHeight, 1, sizeof(bmpHeadInfo.biHeight), fp);
    fread(&bmpHeadInfo.biPlanes, 1, sizeof(bmpHeadInfo.biPlanes), fp);
    fread(&bmpHeadInfo.biBitCount, 1, sizeof(bmpHeadInfo.biBitCount), fp);
    fread(&bmpHeadInfo.biCompression, 1, sizeof(bmpHeadInfo.biCompression), fp);
    fread(&bmpHeadInfo.biSizeImage, 1, sizeof(bmpHeadInfo.biSizeImage), fp);
    fread(&bmpHeadInfo.biXPelsPerMeter, 1, sizeof(bmpHeadInfo.biXPelsPerMeter), fp);
    fread(&bmpHeadInfo.biYPelsPerMeter, 1, sizeof(bmpHeadInfo.biYPelsPerMeter), fp);
    fread(&bmpHeadInfo.biClrUsed, 1, sizeof(bmpHeadInfo.biClrUsed), fp);
    fread(&bmpHeadInfo.biClrImportant, 1, sizeof(bmpHeadInfo.biClrImportant), fp);


    // 读取bmp位图数据
    BGRA* bgra = (BGRA*)malloc(sizeof(BGRA) * (bmpHeadInfo.biWidth * bmpHeadInfo.biHeight));
    fseek(fp, bmpHeadInfo.bfOffBits, SEEK_SET);

    if (bmpHeadInfo.biBitCount == 32)
    {
       for (unsigned int i = 0; i < bmpHeadInfo.biWidth * bmpHeadInfo.biHeight; i++)
            fread(&bgra[i], 1, sizeof(BGRA), fp);
    }
    else if (bmpHeadInfo.biBitCount == 24)
    {
		// 计算每行补几个字节零
         int k = 4 * (3 * bmpHeadInfo.biWidth / 4 + 1) - 3 * bmpHeadInfo.biWidth;
        for (unsigned int i = 0; i < bmpHeadInfo.biWidth * bmpHeadInfo.biHeight; i++)
        {
            if (k != 4 && (ftell(fp)- 54 + k )% (3 * bmpHeadInfo.biWidth + k)==0)
                fseek(fp, ftell(fp) + k, SEEK_SET);

            fread(&bgra[i].blue, 1, sizeof(unsigned char), fp);
            fread(&bgra[i].green, 1, sizeof(unsigned char), fp);
            fread(&bgra[i].red, 1, sizeof(unsigned char), fp);
            bgra[i].transparency = (unsigned char)0xFF;
        }
    }

    im.color = bgra;
    im.w = bmpHeadInfo.biWidth;
    im.h = bmpHeadInfo.biHeight;

    fclose(fp);
    return im;
}

// 保存BMP图片
void Image_bmp_save(char* filename,IMAGE im)
{
    FILE* fp = fopen(filename, "wb");

    unsigned short  bfType = 0x4D42;                // 0x424D,即BM字符串,表明是bmp格式文件
    unsigned int    bfSize = im.w * im.h * 4 + 54;  // ###总的bmp文件大小 以字节为单位     
    unsigned short  bfReserved1 = 0;                // 保留,必须设置为0                     
    unsigned short  bfReserved2 = 0;                // 保留,必须设置为0 
    unsigned int    bfOffBits = 54;                 // ###总的bmp头部的大小(包括位图信息头),即到像素数据的偏移  
    unsigned int    biSize = 40;                    // 位图信息头的大小
    unsigned int    biWidth = im.w;                 // ###图像的宽  
    unsigned int    biHeight = im.h;                // ###图像的高  
    unsigned short  biPlanes = 1;                   // 颜色平面数,即调色盘数,恒等于1 
    unsigned short  biBitCount = 32;                // ###图片颜色的位数,一般为32
    unsigned int    biCompression = 0;              // 说明图象数据压缩的类型,0为不压缩
    unsigned int    biSizeImage = 0;                // 像素数据所占大小,因为使用BI_RGB,所以设置为0
    unsigned int    biXPelsPerMeter = 0;            // 说明水平分辨率,缺省为0
    unsigned int    biYPelsPerMeter = 0;            // 说明垂直分辨率,缺省为0
    unsigned int    biClrUsed = 0;                  // 说明本位图实际使用调色盘的颜色索引数,0表示全部
    unsigned int    biClrImportant = 0;             // 说明本位图重要调色盘的颜色索引数,0表示全都重要

    fwrite(&bfType, 2, 1, fp);
    fwrite(&bfSize, 4, 1, fp);
    fwrite(&bfReserved1, 2, 1, fp);
    fwrite(&bfReserved2, 2, 1, fp);
    fwrite(&bfOffBits, 4, 1, fp);
    fwrite(&biSize, 4, 1, fp);
    fwrite(&biWidth, 4, 1, fp);
    fwrite(&biHeight, 4, 1, fp);
    fwrite(&biPlanes, 2, 1, fp);
    fwrite(&biBitCount, 2, 1, fp);
    fwrite(&biCompression, 4, 1, fp);
    fwrite(&biSizeImage, 4, 1, fp);
    fwrite(&biXPelsPerMeter, 4, 1, fp);
    fwrite(&biYPelsPerMeter, 4, 1, fp);
    fwrite(&biClrUsed, 4, 1, fp);
    fwrite(&biClrImportant, 4, 1, fp);

    fwrite(im.color, sizeof(BGRA) * im.w * im.h, 1, fp);

    fclose(fp);
}


// JPG结构体
// typedef struct tagJPG
// 加载JPG图片
// IMAGE Image_jpg_load(char* filename)
// 保存JPG图片
// void Image_jpg_save(char* filename, IMAGE im)

// 加载图片
IMAGE Image_load(char* filename)
{
    IMAGE im;
    char* fileEx= GetFlieExta(filename);

    if (strcmp(fileEx, ".bmp") == 0)
        im = Image_bmp_load(filename);
    // else if (strcmp(fileEx, ".jpg") == 0)
        // im = Image_jpg_load(filename);

    return im;
}

// 保存图片
void Image_save(char* filename, IMAGE im)
{
    char* fileEx = GetFlieExta(filename);

    if (strcmp(fileEx, ".bmp") == 0)
        Image_bmp_save(filename, im);
    // else if (strcmp(fileEx, ".jpg") == 0)
        // im = Image_jpg_save(filename);
}

// 查看图片
void Image_show(char* filename)
{
    system(filename);
}

// 释放图像结构体
void Image_free(IMAGE im)
{
    free(im.color);
}

// 代码测试
/*
	IMAGE image1 = Image_load("123.bmp");	
	Image_save("01.bmp", image1);
	Image_free(image1);
	Image_show("01.bmp");

*/

二. 图片的形状变换

2.1 图像一维坐标与二维坐标的转换

注意:一维坐标与二维坐标的转换是这篇文章最最基础的核心知识点,贯穿全文的所有代码,一定要理解。图片的核心数据是位图信息头中的RGB值,它可以看作一个巨大的一维数组。图片的第一个像素位于左下角,从左到右,从上到下依次排列。但在图像处理中二维坐标更易处理。所以可将一维数组的下标看作一维坐标,并在需要时转化成二维坐标。

一维坐标转二维坐标
x = i w x=\frac{i}{w} x=wi​
y = i m o d    w y=i\mod{w} y=imodw
二维坐标转一维坐标
i = x + y ∗ w i=x+y*w i=x+y∗w
除法是整除,mod是取余, i i i是一维数组下标, w w w是图片的宽度

2.2 图像的任意大小的缩放

2.2.1 最近邻插值法(不推荐使用)

5万字用纯C语言从零开始实现人脸检测

算法核心:1. 通过新图的坐标计算原图的坐标,逆向映射。
2. 通过原图的坐标计算与该点最邻近的点坐标,原图坐标(0.3,0.3)与它最
邻近的点的坐标为(0,0),即新图坐标中该点的RGB等于原图(0,0)点的RGB。即该点RGB等于最近点的RGB

通过新图的坐标计算原图的坐标
o l d X = n e w X ∗ o l d W n e w W oldX=newX*\frac{oldW}{newW} oldX=newX∗newWoldW​      o l d Y = n e w Y ∗ o l d H n e w H oldY=newY*\frac{oldH}{newH} oldY=newY∗newHoldH​

// 缩放图片(最近邻插值法)(部分区域有明显的锯齿状,不推荐使用)
IMAGE Transform_shape_nearest(IMAGE im, unsigned int newWidth, unsigned int newHeight)
{
    // 算法核心:通过新图的坐标计算原图的坐标

    float fx, fy;    // f->former  原来(原图坐标)
    float dx, dy;    // d->Decimal 小数(原图坐标的小数部分)

    BGRA* bgra = (BGRA*)malloc(sizeof(BGRA) * newWidth * newHeight);

    // 遍历整张新图
    for (unsigned int i = 0; i < newWidth * newHeight; i++)
    {
        // 通过新图的坐标计算原图的坐标
        fx = (i % newWidth) * ((float)im.w / newWidth);
        fy = (i / newWidth) * ((float)im.h / newHeight);

        // 计算原图坐标的小数部分
        dx = fx - (int)fx;
        dy = fy - (int)fy;

        fx = (dx <= 0.5 ? (int)fx : (int)fx + 1);
        fy = (dy <= 0.5 ? (int)fy : (int)fy + 1);
    
        unsigned int k = fx + fy * im.w; // 注意乘的是原图的w

        if (k >= im.w * im.h)  // 判断k是否越界
            k = im.w * im.h - 1;

        bgra[i].blue = im.color[k].blue;
        bgra[i].green = im.color[k].green;
        bgra[i].red = im.color[k].red;
        bgra[i].transparency = 255;
    }


    free(im.color);
    im.color = bgra;
    im.w = newWidth;
    im.h = newHeight;
    return im;
}
// 代码测试
/*
	IMAGE image1 = Image_load("123.bmp");	
	
	image1 = Transform_shape_nearest(image1, 1200, 600)
	
	Image_free(image1);
	Image_show("01.bmp");
*/

2.2.1 双线性插值法(推荐使用)

5万字用纯C语言从零开始实现人脸检测
算法核心:1. 通过新图的坐标计算原图的坐标,,逆向映射。
2. 通过坐标值,按权重分配像素颜色值,离谁近谁权重大

计算(0.4,0)
(0.4,0)blue = (0,0)blue ∗ * ∗ (1-0.4) + (1,0)blue ∗ * ∗ 0.4
计算(0.4,1)
(0.4,1)blue = (0,1)blue ∗ * ∗ (1-0.4) + (1,1)blue ∗ * ∗ 0.4
计算(0.4,0.6)
(0.4,0.6)blue = (0.4,0)blue ∗ * ∗ (1-0.6) + (0.4,1)blue ∗ * ∗ 0.6

令四个角上的点的一维坐标为k1,k2,k3,k4。所求点的小数部分为dx,dy可得
c o l o r = ( k 1 ⋅ ( 1 − d x ) + k 2 ⋅ d x ) ⋅ ( 1 − d y ) + ( k 3 ⋅ ( 1 − d x ) + k 4 ⋅ d x ) ⋅ d y color=\left(k_{1}\cdot\left(1-dx\right)+k_{2}\cdot dx\right)\cdot\left(1-dy\right)+\left(k_{3}\cdot\left(1-dx\right)+k_{4}\cdot dx\right)\cdot dy color=(k1​⋅(1−dx)+k2​⋅dx)⋅(1−dy)+(k3​⋅(1−dx)+k4​⋅dx)⋅dy

// 缩放图片(双线性插值法)(推荐使用)
IMAGE Transform_shape_linear(IMAGE im, unsigned int newWidth, unsigned int newHeight)
{
    float fx, fy, dx, dy;
    int k1, k2, k3, k4;

    BGRA* bgra = (BGRA*)malloc(sizeof(BGRA) * newWidth * newHeight);

    for (unsigned int i = 0; i < newWidth * newHeight; i++)
    {
        // 通过新图的坐标计算原图的坐标
        fx = (i % newWidth) * ((float)im.w / newWidth);
        fy = (i / newWidth) * ((float)im.h / newHeight);

        dx = fx - (int)fx;
        dy = fy - (int)fy;

        fx = (int)fx;
        fy = (int)fy;

        // 分别计算四个角上点的坐标
        k1 = fx + fy * im.w;
        k2 = fx + 1 + fy * im.w;
        k3 = fx + (fy + 1) * im.w;
        k4 = fx + 1 + (fy + 1) * im.w;

        // 判断是否越界
        if (k1 >= im.w * im.h)
            k1 = im.w * im.h - 1;
        if (k2 >= im.w * im.h)
            k2 = im.w * im.h - 1;
        if (k3 >= im.w * im.h)
            k3 = im.w * im.h - 1;
        if (k4 >= im.w * im.h)
            k4 = im.w * im.h - 1;

        bgra[i].blue = (im.color[k1].blue * (1 - dx) + im.color[k2].blue * dx) * (1 - dy) + (im.color[k3].blue * (1 - dx) + im.color[k4].blue * dx) * dy;
        bgra[i].green = (im.color[k1].green * (1 - dx) + im.color[k2].green * dx) * (1 - dy) + (im.color[k3].green * (1 - dx) + im.color[k4].green * dx) * dy;
        bgra[i].red = (im.color[k1].red * (1 - dx) + im.color[k2].red * dx) * (1 - dy) + (im.color[k3].red * (1 - dx) + im.color[k4].red * dx) * dy;
        bgra[i].transparency = 255;
    }

    free(im.color);
    im.color = bgra;
    im.w = (int)newWidth;
    im.h = (int)newHeight;
    return im;
}

5万字用纯C语言从零开始实现人脸检测
左:最近邻(有明显锯齿),右:双线性

2.3 图像的任意角度的旋转

5万字用纯C语言从零开始实现人脸检测

  1. 由矩阵旋转可得旋转后的坐标与旋转前坐标的转换

x ′ ′ = x ′ cos ⁡ θ + y ′ sin ⁡ θ x''=x'\cos\theta+y'\sin\theta x′′=x′cosθ+y′sinθ       y ′ ′ = y ′ cos ⁡ θ − x ′ sin ⁡ θ y''=y'\cos\theta-x'\sin\theta y′′=y′cosθ−x′sinθ

  1. 由图可知,旋转后的图片离第一象限, x x x轴偏移了 b x bx bx, y y y轴偏移了 b y by by,所以公式调整为

x ′ ′ = x ′ cos ⁡ θ + y ′ sin ⁡ θ + b x x''=x'\cos\theta+y'\sin\theta+bx x′′=x′cosθ+y′sinθ+bx      y ′ ′ = y ′ cos ⁡ θ − x ′ sin ⁡ θ + b y y''=y'\cos\theta-x'\sin\theta+by y′′=y′cosθ−x′sinθ+by

  1. 实现的核心:通过原图坐标计算新图坐标,向前映射。
  2. 注意旋转后的图片大小也会改变
  3. 旋转后的图片会有许多空白区域,最后要对这些空白区域进行填充
// 图像的任意角度的旋转
IMAGE Transform_shape_whirl(IMAGE im, float angle)
{
    // 转角度换成弧度
    angle = 3.141592 * angle / 180;

    float cosnum = (float)cos(angle);   
    float sinnum = (float)sin(angle);

    // 计算原图的四个角的坐标
    int fx1 = 0;
    int fy1 = 0;
    int fx2 = im.w - 1;
    int fy2 = 0;
    int fx3 = 0;
    int fy3 = im.h - 1;
    int fx4 = im.w - 1;
    int fy4 = im.h - 1;

    // 计算旋转后的图像四个角的坐标
    int nx1 = 0;
    int ny1 = 0;
    int nx2 = (int)(fx2 * cosnum + fy2 * sinnum);
    int ny2 = (int)(fy2 * cosnum - fx2 * sinnum);
    int nx3 = (int)(fx3 * cosnum + fy3 * sinnum);
    int ny3 = (int)(fy3 * cosnum - fx3 * sinnum);
    int nx4 = (int)(fx4 * cosnum + fy4 * sinnum);
    int ny4 = (int)(fy4 * cosnum - fx4 * sinnum);

    // 计算旋转后的图像的宽和高
    unsigned int width = abs(max(max(nx1, nx2), max(nx3, nx4))) + abs(min(min(nx1, nx2), min(nx3, nx4))) + 1;
    unsigned int hight = abs(max(max(ny1, ny2), max(ny3, ny4))) + abs(min(min(ny1, ny2), min(ny3, ny4))) + 1;

    // 计算旋转后的图像到第一象限的位置偏移
    int bx = abs(min(min(nx1, nx2), min(nx3, nx4)));
    int by = abs(min(min(ny2, ny3), ny4));

    // 申请并初始化内存空间
    BGRA* bgra = (BGRA*)calloc(width * hight, sizeof(BGRA));
    
    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        // 注意(int)放的位置,不能随便移动,因为y可能为负的小数 
        int k = ((i % im.w) * cosnum + (i / im.w) * sinnum + bx) + (int)((i / im.w) * cosnum - (i % im.w) * sinnum + by) * width;

        // 判断是否越界
        if (k >= width * hight)
            k = width * hight - 1;

        bgra[k].blue = im.color[i].blue;
        bgra[k].green = im.color[i].green;
        bgra[k].red = im.color[i].red;
        bgra[k].transparency = 255;
    }

    // 用邻近的像素填充空白区域
    for (unsigned int i = 0; i < width * hight; i++)
    {
        if (bgra[i].transparency != 255 && bgra[i + 1].transparency == 255)
        {
            bgra[i].blue = bgra[i - 1].blue;
            bgra[i].green = bgra[i - 1].green;
            bgra[i].red = bgra[i - 1].red;
            bgra[i].transparency = 255;
        }
    }

    free(im.color);
    im.color = bgra;
    im.w = width;
    im.h = hight;
    return im;
}

5万字用纯C语言从零开始实现人脸检测

2.4 图像的镜像翻转

图像的镜像翻转分为水平翻转和垂直翻转
水平翻转是 y y y坐标不变, x x x坐标翻转
垂直翻转是 x x x坐标不变, y y y坐标翻转

#define UPTURN_MODE_HORIZONTAL 0    // 水平翻转
#define UPTURN_MODE_VERTICAL 1      // 垂直翻转
// 图像的镜像翻转
IMAGE Transform_shape_upturn(IMAGE im, int upturn_mode)
{
    BGRA* bgra = (BGRA*)malloc(sizeof(BGRA) * im.w * im.h);

    if(upturn_mode == UPTURN_MODE_HORIZONTAL)
        for (unsigned int i = 0; i < im.w * im.h; i++)
            bgra[i] = im.color[(im.w - 1 - (i % im.w)) + i / im.w * im.w];  // 水平翻转是y坐标不变,x坐标翻转
    else if(upturn_mode == UPTURN_MODE_VERTICAL)
        for (unsigned int i = 0; i < im.w * im.h; i++)
            bgra[i] = im.color[(i % im.w) + (im.h - 1 - i / im.w ) * im.w]; // 垂直翻转是x坐标不变,y坐标翻转

    free(im.color);
    im.color = bgra;
    return im;
}

5万字用纯C语言从零开始实现人脸检测

5万字用纯C语言从零开始实现人脸检测

三. 图片的颜色预处理

图片的颜色预处理中部分算法比较简单所以不做过多的讲解,直接看公式即可明白。

3.1 灰度图(4种算法)

3.1.1 加权法(推荐使用)

c o l o r = b l u e ⋅ 144 + g r e e n ⋅ 587 + r e d ⋅ 299 1000 color=\frac{blue\cdot144+green\cdot587+red\cdot299}{1000} color=1000blue⋅144+green⋅587+red⋅299​

3.1.2 最值法

c o l o r = m a x ( b l u e , g r e e n , r e d ) color=max(blue,green,red) color=max(blue,green,red)

3.1.2 均值法

c o l o r = b l u e + g r e e n + r e d 3 color=\frac{blue+green+red}{3} color=3blue+green+red​

3.1.2 分量法

c o l o r = ( r e d / g r e e n / b l u e ) color=(red/green/blue) color=(red/green/blue)

#define GRAY_MODE_WEIGHT 1           // 加权法(推荐使用)
#define GRAY_MODE_BEST 2             // 最值法
#define GRAY_MODE_AVERAGE 3          // 均值法
#define GRAY_MODE_PART_RED 4         // 分量法_RED
#define GRAY_MODE_PART_GREEN 5       // 分量法_GREEN
#define GRAY_MODE_PART_BLUE 6        // 分量法_BLUE

// 彩色图转灰度图
void Transform_color_grayscale(IMAGE im, int grayscale_mode)
{
    int color = 0;
    switch (grayscale_mode)
    {
    case GRAY_MODE_WEIGHT:
    {
        for (unsigned int i = 0; i < im.w * im.h; i++)
        {
            color = (im.color[i].blue * 114 + im.color[i].green * 587 + im.color[i].red * 299) / 1000;
            im.color[i].blue = color;
            im.color[i].green = color;
            im.color[i].red = color;
        }
        break;
    }

    case GRAY_MODE_BEST:
    {
        for (unsigned int i = 0; i < im.w * im.h; i++)
        {
            color = im.color[i].blue;
            if (color < im.color[i].red)
                color = im.color[i].red;
            if (color < im.color[i].green)
                color = im.color[i].green;
            im.color[i].blue = color;
            im.color[i].green = color;
            im.color[i].red = color;
        }
        break;
    }

    case GRAY_MODE_AVERAGE:
    {
        for (unsigned int i = 0; i < im.w * im.h; i++)
        {
            color = (im.color[i].blue + im.color[i].green + im.color[i].red) / 3;
            im.color[i].blue = color;
            im.color[i].green = color;
            im.color[i].red = color;
        }
        break;
    }

    case GRAY_MODE_PART_RED:
    {
        for (unsigned int i = 0; i < im.w * im.h; i++)
        {
            im.color[i].blue = im.color[i].red;
            im.color[i].green = im.color[i].red;
        }
        break;
    }

    case GRAY_MODE_PART_GREEN:
    {
        for (unsigned int i = 0; i < im.w * im.h; i++)
        {
            im.color[i].blue = im.color[i].green;
            im.color[i].red = im.color[i].green;
        }
        break;
    }

    case GRAY_MODE_PART_BLUE:
    {
        for (unsigned int i = 0; i < im.w * im.h; i++)
        {
            im.color[i].green = im.color[i].blue;
            im.color[i].red = im.color[i].blue;
        }
        break;
    }

    }

}

5万字用纯C语言从零开始实现人脸检测
5万字用纯C语言从零开始实现人脸检测
5万字用纯C语言从零开始实现人脸检测
5万字用纯C语言从零开始实现人脸检测
图片顺序:加权法,最值法,均值法,分量法_RED

3.2 二值图

二值图是指由只由黑白两种颜色的像素组成的图片,二值图的质量会直接影响到提取到的图片特征的是否有效,而决定二值图的质量的核心就是阈值的计算,像素值大于等于阈值则取255,小于阈值则取0。

3.2.1 自定义阈值法

// 二值图(自定义阈值法)
void Transform_color_BW_DIY(IMAGE im, unsigned char Threshold)
{
    unsigned char color = 0;
    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        // 先转换成灰度图
        color = (im.color[i].blue * 114 + im.color[i].green * 587 + im.color[i].red * 299) / 1000;
        if (color >= Threshold) // Threshold的值在不同的图片中是不同的
            color = 255;
        else
            color = 0;

        im.color[i].blue = color;
        im.color[i].green = color;
        im.color[i].red = color;
    }
}

3.2.2 最大类间方差法(大津法OTSU)

大津法OTSU是假设阈值将图像分成背景和前景两部分。计算背景和前景之间的类间方差,类间方差越大说明背景和前景之间的差别越大,二值图的效果越好。大津法OSTU,适用双峰直方图。当图像的整体颜色差别不大时,不推荐使用。

类间方差计算公式
g = w 前 1 − w 前 ⋅ ( u 前 − u 总 ) 2 g=\frac{w_{前}}{1-w_{前}}\cdot\left(u_{前}-u_{总}\right)^{2} g=1−w前​w前​​⋅(u前​−u总​)2
w 前 w_{前} w前​:前景像素数的占比
u 前 u_{前} u前​:前景的平局灰度
u 总 u_{总} u总​:图像的平局灰度
g g g:类间方差

整理可得
g = n 前 n 总 1 − n 前 n 总 ⋅ ( ∑ i = T i = 255 n i ⋅ i n 总 − ∑ i = 0 i = 255 n i ⋅ i n 总 ) 2 g=\frac{\frac{n_{前}}{n_{总}}}{1-\frac{n_{前}}{n_{总}}}\cdot\left(\frac{\sum_{i=T}^{i=255}n_{i}\cdot i}{n_{总}}-\frac{\sum_{i=0}^{i=255}n_{i}\cdot i}{n_{总}}\right)^{2} g=1−n总​n前​​n总​n前​​​⋅(n总​∑i=Ti=255​ni​⋅i​−n总​∑i=0i=255​ni​⋅i​)2
n 前 n_{前} n前​:前景的像素数
n 总 n_{总} n总​:总的像素数
n i n_{i} ni​:对应像素颜色的像素数

这里计算平局灰度,读者可能有点不理解,举个例子:一组7人每人10元,二组6人每人8元,三组11人每人5元,求计算平均每人几元。


7 ∗ 10 + 6 ∗ 8 + 11 ∗ 5 7 + 6 + 11 = 7.2 \frac{7*10+6*8+11*5}{7+6+11}=7.2 7+6+117∗10+6∗8+11∗5​=7.2

// 二值图(大津法OSTU,适用双峰直方图。当图像的整体颜色差别不大时,不推荐使用)
void Transform_color_BW_OSTU(IMAGE im)
{
    // 公式:g = w0 / (1 - w0) * (u0 - u)* (u0 - u) 当g最大时取到阈值T
    int colorMap[256] = { 0 };
    float w0 = 0; // 前景像素数的占比
    unsigned int u0 = 0; // 前景的平局灰度(灰度值*其对应的素数个数)的累加/前景的像素个数
    unsigned int u = 0;  // 图像的平局灰度(灰度值*其对应的素数个数)的累加/总的像素个数
    float g = 0;  // 方差
    unsigned char T = 0;    // 阈值

    // 创建灰度直方图
    for (unsigned int i = 0; i < im.w * im.h; i++)
        colorMap[im.color[i].blue] += 1;
  
    for (int i = 0; i < 256; i++)
         u += colorMap[i] * i;  // u暂时计算累加
    u /= (im.h * im.w);

    // 遍历 0-255 寻找合适的阈值 
    for(unsigned int m = 0 ; m < 256; m++)
    {
        for (int n = m; n < 256; n++)
        {
            w0 += colorMap[n]; // w0暂时计算,保存前景的所有像素个数
            u0 += colorMap[n] * n; // u0暂时计算累加
        }
        u0 /= w0;
        w0 /= (im.h * im.w);
        
        if((w0 / (1 - w0) * (u0 - u)* (u0 - u)) > g)
            g = w0 / (1 - w0) * (u0 - u)* (u0 - u), T = m;
    }
 Transform_color_BW_DIY(im, T);
}

5万字用纯C语言从零开始实现人脸检测

3.2.3 三角法TRIANGLE

5万字用纯C语言从零开始实现人脸检测

如图,在灰度直方图中,找到最低点和最高点,计算出这两点的函数(黄色直线),分别计算最低点和最高点之间的点到黄色直线的距离 d d d,当 d d d取到最大时,取到 T T T阈值三角法TRIANGLE,适用单峰直方图。当图像的整体颜色差别不大时,不推荐使用

计算公式
        d = ∣ A x 0 + B y 0 + C ∣ A 2 + B 2 d=\frac{\left|Ax_{0}+By_{0}+C\right|}{\sqrt{A^{2}+B^{2}}} d=A2+B2 ​∣Ax0​+By0​+C∣​        y = k x + b y=kx+b y=kx+b
联立可得
d = ∣ − k x 0 + y 0 − b ∣ 1 + k 2 d=\frac{\left|-kx_{0}+y_{0}-b\right|}{\sqrt{1+k^{2}}} d=1+k2 ​∣−kx0​+y0​−b∣​

// 二值图(三角法TRIANGLE,适用单峰直方图。当图像的整体颜色差别不大时,不推荐使用)
void Transform_color_BW_TRIANGLE(IMAGE im)
{
    int colorMap[256] = { 0 };
    unsigned char minColor = 0;
    unsigned int minCount = 0;
    unsigned char maxColor = 0;
    unsigned int maxCount = 0;
    int d = 0;  // 最短距离
    unsigned char T = 0; // 阈值

    // 创建灰度直方图
    for (unsigned int i = 0; i < im.w * im.h; i++)
        colorMap[im.color[i].blue] += 1;

    for (int i = 0; i < 256; i++)
    {
        if (im.color[i].blue < minColor)
            minColor = im.color[i].blue, minCount = colorMap[im.color[i].blue];
        if (im.color[i].blue > maxColor)
            maxColor = im.color[i].blue, maxCount = colorMap[im.color[i].blue];
    }

    float k = ((float)maxCount - minCount) / ((float)maxColor - minColor);
    float b = maxCount - k * maxColor;
   
    // 遍历寻找最近距离 
    for (unsigned int n = minColor; n <= maxColor; n++)
        if (abs((int)(-k * n + colorMap[n] - b)) / sqrt((double)(1 + k * k)) > b)
            b = abs((int)(-k * n + colorMap[n] - b)) / sqrt((double)(1 + k * k)), T = n;

    Transform_color_BW_DIY(im, T);
}

5万字用纯C语言从零开始实现人脸检测
识别出了部分云彩

3.2.4 自适应阈值法

自适应阈值法是一种,类卷积的操作(卷积会在下文详细讲解),其核心是选取一定大小的区域(区域的边长为奇数),用这区域的像素平均值来作为这一个区域的阈值。自适应阈值法,当图片线条多且密时,不推荐使用

// 二值图(自适应阈值法,areaSize=25较合适,当图片线条多且密时,不推荐使用)
IMAGE Transform_color_BW_Adaptive(IMAGE im, int areaSize)
{
    // areaSize为区域的大小,区域越大,效果图的细节越好,areaSize=25较合适
    BGRA* bgra = (BGRA*)malloc(sizeof(BGRA) * im.w * im.h);
    int* p = (int*)malloc(sizeof(int) * areaSize); // p->position 位置坐标
    int k = (int)(sqrt((double)areaSize)) / 2;  // 重合区域边长的一半

    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        // 计算与卷积和对应重合区域的坐标
        int t = 0; // 记录p的下标
        for (int n = k; n >= -k; n--)
            for (int m = -k; m <= k; m++)
            {
                p[t] = ((i % im.w) + m) + (i / im.w + n) * im.w;
                t++;
            }

        // 判断是否越界
        for (int j = 0; j < areaSize; j++)
            if (p[j] < 0 || p[j] >= im.w * im.h)
                p[j] = i;

        unsigned int color = 0;
        for (int j = 0; j < areaSize; j++)
            color += im.color[p[j]].blue;
        color /= areaSize;

        if (im.color[i].blue >= color)
            bgra[i].blue = 255;
        else
            bgra[i].blue = 0;

        bgra[i].green = bgra[i].blue;
        bgra[i].red = bgra[i].blue;
    }

    free(p);
    free(im.color);
    im.color = bgra;
    return im;
}

处理山
5万字用纯C语言从零开始实现人脸检测
处理人脸
5万字用纯C语言从零开始实现人脸检测

3.2.5 拓展:用二值图表示灰度变化

核心思想是通过像素的疏密来表示灰度的变化,其实现的代码和自适应阈值法的代码极为相识

// 二值图(用二值图表示灰度变化,areaSize=25较合适)
void Transform_color_BW_grayscale(IMAGE im, int areaSize)
{
	// areaSize为区域的大小,区域越大,效果图的细节越好,areaSize=25较合适
    int* p = (int*)malloc(sizeof(int) * areaSize); // p->position 位置坐标
    int k = (int)(sqrt((double)areaSize)) / 2;  // 重合区域边长的一半

    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        // 计算与卷积和对应重合区域的坐标
        int t = 0; // 记录p的下标
        for (int n = k; n >= -k; n--)
            for (int m = -k; m <= k; m++)
            {
                p[t] = ((i % im.w) + m) + (i / im.w + n) * im.w;
                t++;
            }

        // 判断是否越界
        for (int j = 0; j < areaSize; j++)
            if (p[j] < 0 || p[j] >= im.w * im.h)
                p[j] = i;

        unsigned int color = 0;
        for (int j = 0; j < areaSize; j++)
            color += im.color[p[j]].blue;
        color /= areaSize;

        if (im.color[i].blue >= color)
            im.color[i].blue = 255;
        else
            im.color[i].blue = 0;

        im.color[i].green = im.color[i].blue;
        im.color[i].red = im.color[i].blue;
    }

    free(p);
}

5万字用纯C语言从零开始实现人脸检测
这是用彩色图转的看起来效果较好,也可以用灰度图

3.3 反色

c o l o r ′ ′ = 255 − c o l o r ′ color'' = 255-color' color′′=255−color′

// 反色
void Transform_color_opposite(IMAGE im)    
{
    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        im.color[i].green = 255 - im.color[i].green;
        im.color[i].blue = 255 - im.color[i].blue;
        im.color[i].red = 255 - im.color[i].red;
    }

}

3.4 直方图均衡化

5万字用纯C语言从零开始实现人脸检测

1. 直方图均衡化:是一种增强图像对比度的方法,即让亮的地方更亮,暗的地方更暗,主要用于图像整体颜色偏淡,图像整体太亮,图像整体太暗,等图像整体变化度不够的情况。由图可知,其主要思想是将一副图像的颜色直方图分布变成近似均匀分布,从而增强图像的对比度。
2. 直方图均衡化核心公式

c o l o r = ( R G B max ⁡ − R G B min ⁡ ) N ∑ i = 0 k n i color=\frac{\left(RGB\max-RGB\min\right)}{N}\sum_{i=0}^{k}ni color=N(RGBmax−RGBmin)​i=0∑k​ni
R G B max ⁡ RGB\max RGBmax是:最大的RGB值一般为255
R G B min ⁡ RGB\min RGBmin是:最小的RGB值一般为0
N是:总的像素数一般为 w ∗ h w*h w∗h
k k k 是:color的RGB值
∑ i = 0 k n i \sum_{i=0}^{k}ni ∑i=0k​ni 是:所有小于color的RGB值的像素的个数之和

整理后的公式

c o l o r = 255 − 0 w ⋅ h ∑ i = 0 k n i color=\frac{255-0}{w\cdot h}\sum_{i=0}^{k}ni color=w⋅h255−0​i=0∑k​ni

3.两种不同的直方图均衡化(实现代码差不多,但效果区别很大)

// 直方图均衡化(分步计算,效果更加柔和)
void Transform_color_Histogram_part(IMAGE im)
{
    // 公式:均衡后的颜色值=(最大颜色位255-最小颜色位0)*小于等于该颜色值的像素数量的累加/图片总的像素数

    int Accumulate = 0;             // 保存累加的值
    unsigned char color = 0;                  // 保存颜色的值
    int allBlue[256] = { 0 };       // 保存蓝色直方图
    int allGreen[256] = { 0 };      // 保存绿色直方图
    int allRed[256] = { 0 };        // 保存红色直方图

    // 数组下标等于RBG值极大的简化了计算
    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        allBlue[im.color[i].blue] += 1;
        allGreen[im.color[i].green] += 1;
        allRed[im.color[i].red] += 1;
    }

    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        //blue
        for (int j = 0; j <= im.color[i].blue; j++) // 累加计算
            Accumulate += allBlue[j];
        color = (255 - 0) * Accumulate / (im.w * im.h);
        im.color[i].blue = color;
        Accumulate = 0;

        //green
        for (int j = 0; j <= im.color[i].green; j++)// 累加计算
            Accumulate += allGreen[j];
        color = (255 - 0) * Accumulate / (im.w * im.h);
        im.color[i].green = color;
        Accumulate = 0;

        //red
        for (int j = 0; j <= im.color[i].red; j++)// 累加计算
            Accumulate += allRed[j];
        color = (255 - 0) * Accumulate / (im.w * im.h);
        im.color[i].red = color;
        Accumulate = 0;
    }
}

// 直方图均衡化(整体计算,效果更加尖锐)
void Transform_color_Histogram_all(IMAGE im)
{    
    int Accumulate = 0;         // 保存累加的值
    unsigned char color = 0;              // 保存颜色的值
    int allColor[256] = { 0 };  // 保存所有颜色直方图

    // 数组下标等于RBG值极大的简化了计算
    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        allColor[im.color[i].blue] += 1;
        allColor[im.color[i].green] += 1;
        allColor[im.color[i].red] += 1;
    }

    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        //blue
        for (int j = 0; j <= im.color[i].blue; j++)// 累加计算
            Accumulate += allColor[j];
        color = (255 - 0) * Accumulate / (im.w * im.h);
        im.color[i].blue = color;
        Accumulate = 0;

        //green
        for (int j = 0; j <= im.color[i].green; j++)// 累加计算
            Accumulate += allColor[j];
        color = (255 - 0) * Accumulate / (im.w * im.h);
        im.color[i].green = color;
        Accumulate = 0;

        //red
        for (int j = 0; j <= im.color[i].red; j++)// 累加计算
            Accumulate += allColor[j];
        color = (255 - 0) * Accumulate / (im.w * im.h);
        im.color[i].red = color;
        Accumulate = 0;
    }
}

5万字用纯C语言从零开始实现人脸检测
分步计算,效果更加柔和
5万字用纯C语言从零开始实现人脸检测
整体计算,效果更加尖锐

注意:上文写二值图时介绍的大津法和三角法,都在图像的整体颜色差别不大时处理效果不佳,而直方均衡化就抑制这问题。

四. 图像的卷积操作

4.1 卷积核

  1. 卷积核是图像处理时,给定输入图像,输入图像中一个小区域中像素加权平均后成为输出图像中的每个对应像素,其中权值由一个函数定义,这个函数称为卷积核。通俗的讲就是一个一维或二维数组,但注意卷积核一般是 3 ∗ 3 3*3 3∗3, 5 ∗ 5 5*5 5∗5, 7 ∗ 7 7*7 7∗7 奇数大小的。卷积核中的数加起来一般为0。 灵活运用不同的卷积核可以实现很多功能。
    5万字用纯C语言从零开始实现人脸检测
  2. 卷积核的操作和计算方法,如图所示, 3 ∗ 3 3*3 3∗3的卷积核在 w ∗ h w*h w∗h的 6 ∗ 6 6*6 6∗6图像上进行移动,将卷积核与图像重合部分的数,分别相乘再相加,即为卷积核中心的值(黄色框区域)当该值小于0时,一般会取绝对值。
  3. 不与卷积核重合的部分,即超出图像边界的部分的值一般为设为0,或等于卷积核中心的值。
  4. 类卷积操作,指并没有用到卷积核,但操作方法与卷积操作类似

4.1.1 自定义卷积操作

卷积操作的代码实现具有很高的重复性,所以先写一个自定义卷积操作(代码与上文二值图的自定义阈值法的实现相类似)可以极大的缩减代码。类卷积操作的代码实现还是要手写

// 判断像素值的范围
unsigned char Tool_RBG(int BRRA)
{
    if (BRRA > 255)
        return (unsigned char)255;
    else if (BRRA < 0)
        return (unsigned char)0;
    else
        return (unsigned char)BRRA;
}

// 卷积操作(自定义)
IMAGE Kernels_use_DIY(IMAGE im, double* kernels, int areaSize, int modulus)
{
	// kernels卷积核
	// areaSize区域的大小
	// modulus最后乘的系数
	
    BGRA* bgra = (BGRA*)malloc(sizeof(BGRA) * im.w * im.h);
    int* p = (int*)malloc(sizeof(int) * areaSize); // p->position 位置坐标
    int k = (int)(sqrt((double)areaSize)) / 2;  // 重合区域边长的一半

    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        // 计算与卷积和对应重合区域的坐标
        int t = 0; // 记录p的下标
        for(int n = k; n >= -k; n--)
            for (int m = -k; m <= k; m++)
                p[t] = ((i % im.w) + m) + (i / im.w + n) * im.w, t++;
                
        // 判断是否越界
        for (int j = 0; j < areaSize; j++) 
            if (p[j] < 0 || p[j] >= im.w * im.h)
                p[j] = i;

		// 相乘相加
        int blue = 0, green = 0, red = 0;
        for (int j = 0; j < areaSize; j++)
        {
            blue += im.color[p[j]].blue * kernels[j];
            green += im.color[p[j]].green * kernels[j];
            red += im.color[p[j]].red * kernels[j];
        }
        
        bgra[i].blue = Tool_RBG(blue * modulus);
        bgra[i].green = Tool_RBG(green * modulus);
        bgra[i].red = Tool_RBG(red * modulus);
    }

    free(p);
    free(im.color);
    im.color = bgra;
    return im;
}

4.2 滤波

图像滤波即在尽量保留图像细节特征的条件下对目标图像的噪声进行抑制。

4.2.1 中值滤波

中值滤波主要用于处理脉冲噪声(椒盐噪声),它是属于随机噪声,即在图像上随机出现的黑点或白点。中值滤波,是将卷积区域内的值按顺序排列,并取中值,作为该点的像素。

//中值滤波
IMAGE Wavefiltering_Median(IMAGE im)
{
    BGRA* bgra = (BGRA*)malloc(sizeof(BGRA) * im.w * im.h);

    for (int unsigned i = 0; i < im.w * im.h; i++)
    {
        // 与卷积和对应重合区域的坐标
        int p[9] =   // p->position 位置坐标
        {
            i + im.w - 1,i + im.w,i + im.w + 1,
            i - 1,i,i + 1,
            i - im.w - 1,i - im.w,i - im.w + 1
        };

        // 判断是否越界
        for (int j = 0; j < 9; j++)
            if (p[j] < 0 || p[j] >= im.w * im.h)
                p[j] = i;

        // 取颜色
        int color[9] =
        {
            im.color[p[0]].blue, im.color[p[1]].blue, im.color[p[2]].blue,
            im.color[p[3]].blue, im.color[p[4]].blue, im.color[p[5]].blue,
            im.color[p[6]].blue, im.color[p[7]].blue, im.color[p[8]].blue
        };

        for (int n = 0; n < 9; n++)  // 对颜色进行排序
            for (int m = n; m < 9; m++)
                if (color[n] > color[m])   // 异或交换不能用等于号
                {
                    color[n] ^= color[m];
                    color[m] ^= color[n];
                    color[n] ^= color[m];
                }

        bgra[i].blue = color[4];
        bgra[i].green = color[4];
        bgra[i].red = color[4];
    }

    free(im.color);
    im.color = bgra;
    return im;
}

滤波前后比较
5万字用纯C语言从零开始实现人脸检测
5万字用纯C语言从零开始实现人脸检测

4.2.2 高斯滤波

高斯噪声是指它的概率密度函数服从高斯分布(即正态分布)的一类噪声。常见的高斯噪声包括起伏噪声、宇宙噪声、热噪声和散粒噪声等等。

高斯滤波公式
1 16 [ 1 2 1 2 4 2 1 2 1 ] \frac{1}{16}\begin{bmatrix} 1 & 2 & 1\\\\ 2 & 4 & 2\\\\ 1 & 2 & 1\\\\ \end{bmatrix} 161​⎣⎢⎢⎢⎢⎢⎢⎡​121​242​121​⎦⎥⎥⎥⎥⎥⎥⎤​

//高斯滤波卷积核
double KERNELS_Wave_Gauss[9] =
{
    1, 2, 1,
    2, 4, 2,
    1, 2 ,1
};

//高斯滤波
IMAGE Wavefiltering_Gauss(IMAGE im)
{
    im = Kernels_use_DIY(im, KERNELS_Wave_Gauss, 9, 1.0 / 16);
    return im;
}

效果和中值滤波差不多
5万字用纯C语言从零开始实现人脸检测

4.2.3 低通滤波

低通滤波是增强图像的低频成分,阻挡高频成分,使图像变得平滑与模糊。

// 低通滤波卷积核 LP1
double KERNELS_Wave_LowPass_LP1[9] =
{
    1 / 9.0, 1 / 9.0, 1 / 9.0,
    1 / 9.0, 1 / 9.0, 1 / 9.0,
    1 / 9.0, 1 / 9.0, 1 / 9.0
};

// 低通滤波卷积核 LP2
double KERNELS_Wave_LowPass_LP2[9] =
{
    1 / 10.0, 1 / 10.0, 1 / 10.0,
    1 / 10.0, 1 / 5.0, 1 / 10.0,
    1 / 10.0, 1 / 10.0, 1 / 10.0
};

// 低通滤波卷积核 LP3
double KERNELS_Wave_LowPass_LP3[9] =
{
    1 / 16.0, 1 / 8.0, 1 / 16.0,
    1 / 8.0, 1 / 4.0, 1 / 8.0,
    1 / 16.0, 1 / 8.0, 1 / 16.0
};


// 低通滤波
IMAGE Wavefiltering_LowPass(IMAGE im, double* kernels)
{
    im = Kernels_use_DIY(im, kernels, 9, 1);
    return im;
}

LP1
5万字用纯C语言从零开始实现人脸检测
LP2
5万字用纯C语言从零开始实现人脸检测
LP3
5万字用纯C语言从零开始实现人脸检测

4.2.4 高通滤波

高通滤波是增强图像的高频成分,阻挡低频成分,使图像变得锐化与清晰。

// 高通滤波卷积核 HP1
double KERNELS_Wave_HighPass_HP1[9] =
{
    -1, -1, -1,
    -1, 9, -1,
    -1, -1 ,-1
};

// 高通滤波卷积核 HP2
double KERNELS_Wave_HighPass_HP2[9] =
{
    0, -1, 0,
   -1, 5, -1,
    0, -1 ,0
};

// 高通滤波卷积核 HP3
double KERNELS_Wave_HighPass_HP3[9] =
{
    1, -2, 1,
   -2, 5, -2,
    1, -2 ,1
};

// 高通滤波
IMAGE Wavefiltering_HighPass(IMAGE im, double* kernels)
{
    im = Kernels_use_DIY(im, kernels, 9, 1);
    return im;
}

HP1
5万字用纯C语言从零开始实现人脸检测
HP2
5万字用纯C语言从零开始实现人脸检测
HP3
5万字用纯C语言从零开始实现人脸检测

4.2.5 均值滤波

和低通滤波差不多,都是使图像变得平滑与模糊。

// 均值滤波卷积核
double KERNELS_Wave_Average[25] =
{
  1, 1, 1, 1, 1,
  1, 1, 1, 1, 1,
  1, 1, 1, 1, 1,
  1, 1, 1, 1, 1,
  1, 1, 1, 1, 1
};

// 均值滤波
IMAGE Wavefiltering_Average(IMAGE im)
{
    im = Kernels_use_DIY(im, KERNELS_Wave_Average, 25, 1.0 / 25);
    return im;
}

5万字用纯C语言从零开始实现人脸检测

4.3 边缘检测

边缘检测就是找出图像中像素亮度发生剧烈变化的区域,通常这些区域表现出就是图像的轮廓。

4.3.1 差分边缘检测

差分边缘检测有三个卷积核,分别用于检测水平方向,垂直方向,垂直和水平方向。

// 差分垂直边缘检测卷积核
double KERNELS_Edge_difference_vertical[9] =
{
    0, 0, 0,
   -1, 1, 0,
    0, 0, 0
};

// 差分水平边缘检测卷积核
double KERNELS_Edge_difference_horizontal[9] =
{
    0,-1, 0,
    0, 1, 0,
    0, 0, 0
};

// 差分垂直和水平边缘检测卷积核
double KERNELS_Edge_difference_VH[9] =
{
   -1, 0, 0,
    0, 1, 0,
    0, 0, 0
};


// 差分边缘检测
IMAGE Edge_detection_difference(IMAGE im, double* kernels)
{
    im = Kernels_use_DIY(im, kernels, 9, 1);
    return im;
}

差分垂直和水平边缘检测
5万字用纯C语言从零开始实现人脸检测

4.3.2 Sobel边缘检测

差分边缘检测有两个卷积核,分别对应XY方向,而该点的像素值为

c o l o r = x 2 + y 2 color=\sqrt{x^{2}+y^{2}} color=x2+y2

// Sobel X边缘检测卷积核
double KERNELS_Edge_Sobel_X[9] =
{
    -1, 0, 1,
   - 2, 0, 2,
    -1, 0, 1
};

// Sobel Y边缘检测卷积核
double KERNELS_Edge_Sobel_Y[9] =
{
   -1, -2, -1,
    0, 0, 0,
    1, 2, 1
};

// Sobel边缘检测
IMAGE Kernels_use_Edge_Sobel(IMAGE im, double* kernels1, double* kernels2)
{
    BGRA* bgra = (BGRA*)malloc(sizeof(BGRA) * im.w * im.h);

    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        // 与卷积和对应重合区域的坐标
        int p[9] =   // p->position 位置坐标
        {
            i + im.w - 1,i + im.w,i + im.w + 1,
            i - 1,i,i + 1,
            i - im.w - 1,i - im.w,i - im.w + 1
        };

        for (int j = 0; j < 9; j++) // 判断是否越界
            if (p[j] < 0 || p[j] >= im.w * im.h)
                p[j] = i;


        unsigned char color1 = Tool_RBG(im.color[p[0]].blue * kernels1[0] + im.color[p[1]].blue * kernels1[1] + im.color[p[2]].blue * kernels1[2] + im.color[p[3]].blue * kernels1[3] + im.color[p[4]].blue * kernels1[4] + im.color[p[5]].blue * kernels1[5] + im.color[p[6]].blue * kernels1[6] + im.color[p[7]].blue * kernels1[7] + im.color[p[8]].blue * kernels1[8]);

        if (kernels2 != NULL)
        {
            unsigned char  color2 = Tool_RBG(im.color[p[0]].blue * kernels2[0] + im.color[p[1]].blue * kernels2[1] + im.color[p[2]].blue * kernels2[2] + im.color[p[3]].blue * kernels2[3] + im.color[p[4]].blue * kernels2[4] + im.color[p[5]].blue * kernels2[5] + im.color[p[6]].blue * kernels2[6] + im.color[p[7]].blue * kernels2[7] + im.color[p[8]].blue * kernels2[8]);
            color1 = Tool_RBG(sqrt(color1 * color1 + color2 * color2));
        }

        bgra[i].blue = color1;
        bgra[i].green = color1;
        bgra[i].red = color1;
    }

    free(im.color);
    im.color = bgra;
    return im;
}

X方向
5万字用纯C语言从零开始实现人脸检测
Y方向
5万字用纯C语言从零开始实现人脸检测
XY方向
5万字用纯C语言从零开始实现人脸检测

4.3.3 Laplace边缘检测

Laplace边缘检测的最大特点是它的边缘检测与方向无关。

// Laplace边缘检测卷积核 LAP1
double KERNELS_Edge_Laplace_LAP1[9] =
{
    0, 1, 0,
    1, -4, 1,
    0, 1, 0
};

// Laplace边缘检测卷积核 LAP2
double KERNELS_Edge_Laplace_LAP2[9] =
{
   -1, -1, -1,
   -1, 8, -1,
   -1, -1, -1
};

// Laplace边缘检测卷积核 LAP3
double KERNELS_Edge_Laplace_LAP3[9] =
{
   -1, -1, -1,
   -1, 9, -1,
   -1, -1, -1
};

// Laplace边缘检测卷积核 LAP4
double KERNELS_Edge_Laplace_LAP4[9] =
{
    1, -2, 1,
   -2, 8, -2,
    1, -2, 1
};


// Laplace边缘检测 
IMAGE Edge_detection_Laplace(IMAGE im, double* kernels)
{
    im = Kernels_use_DIY(im, kernels, 9, 1);
    return im;
}

LAP1
5万字用纯C语言从零开始实现人脸检测
LAP2
5万字用纯C语言从零开始实现人脸检测
LAP3
5万字用纯C语言从零开始实现人脸检测
LAP4
5万字用纯C语言从零开始实现人脸检测

4.4 形态学图像处理

  1. 形态学图像处理包括,图形腐蚀,膨胀,开闭运算等,一般都是对二值图进行处理。且二值图是黑底白字(可通过反色转换)
  2. 形态学图像处理中的用的卷积核种类很多,可根据实际自己设计

4.4.1 腐蚀

  1. 图像腐蚀是将图像中的高亮区域即白色部分进行缩减细化,其运行结果图比原图的高亮区域更小。即图像轮廓变细了
  2. 腐蚀用的卷积核,用的是十字形
// 腐蚀卷积核
double KERNELS_Morphology_Erosion_cross[9] =
{
    0, 1, 0,
    1, 1, 1,
    0, 1, 0
};
  1. 腐蚀卷积核的使用规则,除去卷积核的中心,将其它8个数与图像上对应的数相乘再相加,如果结果小于255乘卷积核中8个数的和的积,那么就将卷积核的中心所对应的图像上的数变为0 。(即边上四个1中有一个所对应的数是0就成立)
  2. 腐蚀用的公式

∑ n = 1 8 k n ⋅ p n < 255 ⋅ ∑ n = 1 8 k n \sum_{n=1}^{8}kn\cdot pn<255\cdot\sum_{n=1}^{8}kn n=1∑8​kn⋅pn<255⋅n=1∑8​kn
kn是:卷积核中的数
pn是:卷积核所对应的图像上的数
注意:这八个数字不包含卷积核的中心

十字形腐蚀卷积核的公式
( k 1 ⋅ p 1 + k 2 ⋅ p 2 + k 3 ⋅ p 3 + k 4 ⋅ p 4 ) < 255 ⋅ ( k 1 + k 2 + k 3 + k 4 ) \left(k_{1}\cdot p_{1}+k_{2}\cdot p_{2}+k_{3}\cdot p_{3}+k_{4}\cdot p_{4}\right)<255\cdot\left(k_{1}+k_{2}+k_{3}+k_{4}\right) (k1​⋅p1​+k2​⋅p2​+k3​⋅p3​+k4​⋅p4​)<255⋅(k1​+k2​+k3​+k4​)
注意:这里是方便读者理解,但实际代码实现中为了代码的通用性,是用原公式实现的。

// 腐蚀
IMAGE Morphology_Erosion(IMAGE im, double* kernels)
{
    BGRA* bgra = (BGRA*)malloc(sizeof(BGRA) * im.w * im.h);

    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        // 与卷积和对应重合区域的坐标
        int p[9] =   // p->position 位置坐标
        {
            i + im.w - 1,i + im.w,i + im.w + 1,
            i - 1,i,i + 1,
            i - im.w - 1,i - im.w,i - im.w + 1
        };

        for (int j = 0; j < 9; j++) // 判断是否越界
            if (p[j] < 0 || p[j] >= im.w * im.h)
                p[j] = i;

        // 判断是否腐蚀(式子很长,但简单)
        if ((im.color[i].blue == 255) && (im.color[p[0]].blue * kernels[0] + im.color[p[1]].blue * kernels[1] + im.color[p[2]].blue * kernels[2] + im.color[p[3]].blue * kernels[3] + im.color[p[5]].blue * kernels[5] + im.color[p[6]].blue * kernels[6] + im.color[p[7]].blue * kernels[7] + im.color[p[8]].blue * kernels[8]) < 255 * (kernels[0] + kernels[1] + kernels[2] + kernels[3] + kernels[5] + kernels[6] + kernels[7] + kernels[8]))
        {
            bgra[i].blue = 0;
            bgra[i].green = 0;
            bgra[i].red = 0;
        }
        else
        {
            bgra[i].blue = im.color[i].blue;
            bgra[i].green = im.color[i].green;
            bgra[i].red = im.color[i].red;
        }
    }

    free(im.color);
    im.color = bgra;
    return im;
}

腐蚀前后效果比较
5万字用纯C语言从零开始实现人脸检测
5万字用纯C语言从零开始实现人脸检测

4.4.2 膨胀

  1. 图像膨胀是将图像中的高亮区域即白色部分进行扩张添加像素值,其运行结果图比原图的高亮区域更大。即图像轮廓变粗了
  2. 膨胀用的卷积核与腐蚀用的一样,都是十字形。
// 膨胀卷积核
double KERNELS_Morphology_Dilation_cross[9] =
{
    0, 1, 0,
    1, 1, 1,
    0, 1, 0
};
  1. 膨胀卷积核的使用规则,除去卷积核的中心,将其它8个数与图像上对应的数相乘再相加,如果结果大于等于255( 即边上四个1中有一个所对应的数是1就成立)
  2. 膨胀用的公式

∑ n = 1 8 k n ⋅ p n > = 255 \sum_{n=1}^{8}kn\cdot pn>=255 n=1∑8​kn⋅pn>=255
kn是:卷积核中的数
pn是:卷积核所对应的图像上的数
注意:这八个数字不包含卷积核的中心

十字形膨胀卷积核的公式
( k 1 ⋅ p 1 + k 2 ⋅ p 2 + k 3 ⋅ p 3 + k 4 ⋅ p 4 ) > = 255 \left(k_{1}\cdot p_{1}+k_{2}\cdot p_{2}+k_{3}\cdot p_{3}+k_{4}\cdot p_{4}\right)>=255 (k1​⋅p1​+k2​⋅p2​+k3​⋅p3​+k4​⋅p4​)>=255
注意:这里是方便读者理解,但实际代码实现中为了代码的通用性,是用原公式实现的。

// 膨胀
IMAGE Morphology_Dilation(IMAGE im, double* kernels)
{
    BGRA* bgra = (BGRA*)malloc(sizeof(BGRA) * im.w * im.h);

    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        // 与卷积和对应重合区域的坐标
        int p[9] =   // p->position 位置坐标
        {
            i + im.w - 1,i + im.w,i + im.w + 1,
            i - 1,i,i + 1,
            i - im.w - 1,i - im.w,i - im.w + 1
        };

        for (int j = 0; j < 9; j++) // 判断是否越界
            if (p[j] < 0 || p[j] >= im.w * im.h)
                p[j] = i;

        // 判断是否膨胀(式子很长,但简单)
        if ((im.color[i].blue == 0) && (im.color[p[0]].blue * kernels[0] + im.color[p[1]].blue * kernels[1] + im.color[p[2]].blue * kernels[2] + im.color[p[3]].blue * kernels[3] + im.color[p[5]].blue * kernels[5] + im.color[p[6]].blue * kernels[6] + im.color[p[7]].blue * kernels[7] + im.color[p[8]].blue * kernels[8]) >= 255)
        {
            bgra[i].blue = 255;
            bgra[i].green = 255;
            bgra[i].red = 255;
        }
        else
        {
            bgra[i].blue = im.color[i].blue;
            bgra[i].green = im.color[i].green;
            bgra[i].red = im.color[i].red;
        }
    }

    free(im.color);
    im.color = bgra;
    return im;
}

5万字用纯C语言从零开始实现人脸检测
5万字用纯C语言从零开始实现人脸检测

4.4.3 开闭运算

1. 开运算:先腐蚀后膨胀
2. 闭运算:先膨胀后腐蚀
一般一张图片会经过多次的腐蚀和膨胀处理,但第一次是腐蚀操作还是膨胀操作,会起到决定性的用,要认真选择。

五. 池化

5.1 池化的介绍

1.池化操作可以进行数据压缩和参数压缩,减少过拟合提高所提取特征的鲁棒性,同时提高运算速度。池化操作一般分为三种,MAX,AVERAGE,MAX,其中MAX是最常用的。

5.2 MAX池化的原理

看图,一目了然
5万字用纯C语言从零开始实现人脸检测

5.3 池化的实现

//池化
IMAGE Pooling(IMAGE im, int lenght)
{
	// lenght池化区域的边长
    unsigned int width = im.w / lenght;
    unsigned int hight = im.h / lenght;
    BGRA* bgra = (BGRA*)malloc(sizeof(BGRA) * width * hight);
    int* p = (int*)malloc(sizeof(int) * lenght * lenght); // p->position 位置坐标
    unsigned char maxColor = 0;  // 保存区域内的最大颜色值
    int k = 0; // 记录实际循环的次数,作为新图的坐标

    for (unsigned int i = 0; i < im.w * im.h; i += lenght)
    {
        // 计算与卷积和对应重合区域的坐标
        int t = 0; // 记录p的下标
        for (int n = 0; n < lenght; n++)
            for (int m = 0; m < lenght; m++)
                p[t] = ((i % im.w) + m) + (i / im.w + n) * im.w, t++;

        if (p[lenght * lenght - 1] >= im.w * im.h) // 判断上边界
            break;
        else if (i / im.w != 0 && (i / im.w) % lenght != 0) // 判断到了中间行
        {
            i += (lenght - 1) * im.w;
            continue;
        }
        else if ((p[lenght * lenght - 1] / im.w) - (p[0] / im.w) + 1 != lenght)  // 判断右边界
        {
            i = i / im.w * im.w + im.w * lenght - lenght;
            continue;
        }
        else
        {       
            maxColor = im.color[p[0]].blue;     // 计算最大颜色值
            for (int j = 0; j < lenght * lenght; j++)
                if (im.color[p[j]].blue > maxColor)
                    maxColor = im.color[p[j]].blue;

            bgra[k].blue = maxColor, bgra[k].green = maxColor, bgra[k].red = maxColor, k++;
        }
    }

    free(p);
    free(im.color);
    im.color = bgra;
    im.w = width;
    im.h = hight;
    return im;
}

池化前后的比较
5万字用纯C语言从零开始实现人脸检测
用的是 2 ∗ 2 2*2 2∗2的大小的卷积核,进行池化操作,处理后图片缩小为原图的 1 4 \frac{1}{4} 41​
5万字用纯C语言从零开始实现人脸检测

五. VJ算法的介绍与分析

VJ(Viola-Jones)算法于2001年的CVPR上提出。该算法一般是被用来检测正面的人脸图像,而对于侧脸图像的检测能力较弱。即便如此它也是人脸检测算法中是非常经典的存在。作者实在能力有限,只是根据自己的理解去尽可能的实现,所以与真正的VJ算法,有较大不同

六. 积分图

6.1 积分图的介绍

积分图是统计二值图中各个区域像素个数的矩阵数组,使用积分图可以非常快速的计算出任意矩形区域内的像素个数,极大的简化了计算。
5万字用纯C语言从零开始实现人脸检测
积分图的统计规则是:统计原点到图上任意一点所组成的矩形区域中的像素个数,所以积分图数组的大小等于图片的象数个数。如图中的( a = > b a=>b a=>b)区域,( a = > c a=>c a=>c)区域,( a = > f a=>f a=>f)区域,( a = > e a=>e a=>e)区域,( a = > d a=>d a=>d)区域。

计算( f = > d f=>d f=>d)区域中的像素个数
( f = > d ) = ( a = > d ) − ( a = > c ) − ( a = > e ) + ( a = > b ) (f=>d)=(a=>d)-(a=>c)-(a=>e)+(a=>b) (f=>d)=(a=>d)−(a=>c)−(a=>e)+(a=>b)

6.2 积分图的生成

  1. 图像在经过卷积操作和二值化操作后,会变成“黑底白字”,但为了方便人眼观察和积分图处理,会进行反色处理使其变成“白底黑字”
  2. 下面这段代码很巧妙,注意理解。
// 积分图结构体
typedef struct tagIGIMAGE
{
    unsigned int w;
    unsigned int h;
    int* date;
}IGIMAGE, *PIGIMAGE;

// 获得积分图(在此之前要保证图片是“白底黑字”)
IGIMAGE IntegralImage_get(IMAGE im)
{
    IGIMAGE IGmap;
    int* array = (int*)malloc(sizeof(int) * im.w * im.h);

    int k = 0; // 用于统计每一行的像素个数
    for (unsigned int i = 0; i < im.w * im.h; i++)
    {
        if (i % im.w == 0) // 判断左边界
            k = 0;

        if (im.color[i].blue == 0)// 判断是否有像素
            k++;

        if (i / im.w == 0)  // 统计第一行的数据,这是基础
            array[i] = k;
        else
            array[i] = array[i - im.w] + k;
    }

    IGmap.date = array;
    IGmap.w = im.w;
    IGmap.h = im.h;
    return IGmap;
}

6.3 积分图的计算

// 计算积分区域像素个数
int IntegralImage_count(IGIMAGE IGmap, int rightTop, int leftBottom)
{
    int a1, a2, a3, a4;
    a1 = leftBottom;
    a2 = (rightTop % IGmap.w) + (leftBottom / IGmap.w ) * IGmap.w;
    a3 = (leftBottom % IGmap.w) + (rightTop / IGmap.w) * IGmap.w;
    a4 = rightTop;

    //  判断是否越界
    if (a1 < 0)
        a1 = 0;
    if (a2 < 0)
        a2 = 0;
    if (a3 < 0)
        a3 = 0;
    if (a3 > IGmap.w * IGmap.h - 1)
        a3 = a4;

    // 计算区域中的像素数
    return IGmap.date[a4] - IGmap.date[a3] - IGmap.date[a2] + IGmap.date[a1];
}

// 释放积分图结构体
void IntegralImage_free(IGIMAGE IGimage)
{
    free(IGimage.date);
}

七. 类哈尔特征

6.1 哈尔特征的介绍

5万字用纯C语言从零开始实现人脸检测

哈尔特征(Haar-like features) 是用于物体识别的一种数字图像特征。哈尔特征模板内有白色和黑色两种矩形,其反映了图像的亮暗变化情况。即人脸图像不同区域的像素分部情况。例如,眼睛和眉毛区域的像素多且密集,所以是黑色矩形,脸颊区域的像素少且稀疏,所以是白色矩形。

6.2 类哈尔特征的选取

作者在这项目中并没有严格按哈尔特征模板的定义去识别图片,而是自己定义了一些类似于哈尔特征的模板去简化操作的流程。会在下文单分支决策树分类器的构建中详细讲解

七. 级联分类器(单分支决策树分类器)

5万字用纯C语言从零开始实现人脸检测

7.2 单分支决策树分类器的介绍

简单的讲就是一连串的if判断语句,快速过滤掉不是人脸的宽口,最后满足所有if条件的即为人脸。只能用正态分布的去简单的计算正确的概率。

7.3 单分支决策树分类器的构建

5万字用纯C语言从零开始实现人脸检测
作者将要检测的人脸的区域分成25份,根据人脸不同部位的像素分布,制定相应的规则,很简单就不过多讲解了,请自行看代码。(读者也可自己添加)

// 单分支决策树分类器
double Classifier_decisionStump(IGIMAGE IGmap, int rightTop, int leftBottom)
{   
    // 计算所判定区域的宽和高
    int areaW = (rightTop % IGmap.w) - (leftBottom % IGmap.w);
    int areaH = (rightTop / IGmap.w) - (leftBottom / IGmap.w);

    int x0 = leftBottom % IGmap.w;
    int y0 = leftBottom / IGmap.w;
    int x1 = rightTop % IGmap.w;
    int y1 = rightTop / IGmap.w;

    // 计算25个区域的像素个数
    int w_all = IntegralImage_count(IGmap, rightTop, leftBottom);

    int w_1 = IntegralImage_count(IGmap, (x1 - areaW * 4 / 5) + y1 * IGmap.w, x0 + (y1 - areaH / 5) * IGmap.w);
    int w_2 = IntegralImage_count(IGmap, (x1 - areaW * 3 / 5) + y1 * IGmap.w, (x1 - areaW * 4 / 5) + (y1 - areaH / 5) * IGmap.w);
    int w_3 = IntegralImage_count(IGmap, (x1 - areaW * 2 / 5) + y1 * IGmap.w, (x1 - areaW * 3 / 5) + (y1 - areaH / 5) * IGmap.w);
    int w_4 = IntegralImage_count(IGmap, (x1 - areaW / 5) + y1 * IGmap.w, (x1 - areaW * 2 / 5) + (y1 - areaH / 5) * IGmap.w);
    int w_5 = IntegralImage_count(IGmap, x1 + y1 * IGmap.w, (x1 - areaW * 1 / 5) + (y1 - areaH / 5) * IGmap.w);

    int w_6 = IntegralImage_count(IGmap, (x1 - areaW * 4 / 5) + (y1 - areaH / 5) * IGmap.w, x0 + (y1 - areaH * 2 / 5) * IGmap.w);
    int w_7 = IntegralImage_count(IGmap, (x1 - areaW * 3 / 5) + (y1 - areaH / 5) * IGmap.w, (x1 - areaW * 4 / 5) + (y1 - areaH * 2 / 5) * IGmap.w);
    int w_8 = IntegralImage_count(IGmap, (x1 - areaW * 2 / 5) + (y1 - areaH / 5) * IGmap.w, (x1 - areaW * 3 / 5) + (y1 - areaH * 2 / 5) * IGmap.w);
    int w_9 = IntegralImage_count(IGmap, (x1 - areaW / 5) + (y1 - areaH / 5) * IGmap.w, (x1 - areaW * 2 / 5) + (y1 - areaH * 2 / 5) * IGmap.w);
    int w_10 = IntegralImage_count(IGmap, x1 + (y1 - areaH / 5) * IGmap.w, (x1 - areaW / 5) + (y1 - areaH * 2 / 5) * IGmap.w);

    int w_11 = IntegralImage_count(IGmap, (x1 - areaW * 4 / 5) + (y1 - areaH * 2 / 5) * IGmap.w, x0 + (y1 - areaH * 3 / 5) * IGmap.w);
    int w_12 = IntegralImage_count(IGmap, (x1 - areaW * 3 / 5) + (y1 - areaH * 2 / 5) * IGmap.w, (x1 - areaW * 4 / 5) + (y1 - areaH * 3 / 5) * IGmap.w);
    int w_13 = IntegralImage_count(IGmap, (x1 - areaW * 2 / 5) + (y1 - areaH * 2 / 5) * IGmap.w, (x1 - areaW * 3 / 5) + (y1 - areaH * 3 / 5) * IGmap.w);
    int w_14 = IntegralImage_count(IGmap, (x1 - areaW / 5) + (y1 - areaH * 2 / 5) * IGmap.w, (x1 - areaW * 2 / 5) + (y1 - areaH * 3 / 5) * IGmap.w);
    int w_15 = IntegralImage_count(IGmap, x1 + (y1 - areaH * 2 / 5) * IGmap.w, (x1 - areaW / 5) + (y1 - areaH * 3 / 5) * IGmap.w);

    int w_16 = IntegralImage_count(IGmap, (x1 - areaW * 4 / 5) + (y1 - areaH * 3 / 5) * IGmap.w, x0 + (y1 - areaH * 4 / 5) * IGmap.w);
    int w_17 = IntegralImage_count(IGmap, (x1 - areaW * 3 / 5) + (y1 - areaH * 3 / 5) * IGmap.w, (x1 - areaW * 4 / 5) + (y1 - areaH * 4 / 5) * IGmap.w);
    int w_18 = IntegralImage_count(IGmap, (x1 - areaW * 2 / 5) + (y1 - areaH * 3 / 5) * IGmap.w, (x1 - areaW * 3 / 5) + (y1 - areaH * 4 / 5) * IGmap.w);
    int w_19 = IntegralImage_count(IGmap, (x1 - areaW / 5) + (y1 - areaH * 3 / 5) * IGmap.w, (x1 - areaW * 2 / 5) + (y1 - areaH * 4 / 5) * IGmap.w);
    int w_20 = IntegralImage_count(IGmap, x1 + (y1 - areaH * 3 / 5) * IGmap.w, (x1 - areaW / 5) + (y1 - areaH * 4 / 5) * IGmap.w);

    int w_21 = IntegralImage_count(IGmap, (x1 - areaW * 4 / 5) + (y1 - areaH * 4 / 5) * IGmap.w, x0 + y0 * IGmap.w);
    int w_22 = IntegralImage_count(IGmap, (x1 - areaW * 3 / 5) + (y1 - areaH * 4 / 5) * IGmap.w, (x1 - areaW * 4 / 5) + y0 * IGmap.w);
    int w_23 = IntegralImage_count(IGmap, (x1 - areaW * 2 / 5) + (y1 - areaH * 4 / 5) * IGmap.w, (x1 - areaW * 3 / 5) + y0 * IGmap.w);
    int w_24 = IntegralImage_count(IGmap, (x1 - areaW / 5) + (y1 - areaH * 4 / 5) * IGmap.w, (x1 - areaW * 2 / 5) + y0 * IGmap.w);
    int w_25 = IntegralImage_count(IGmap, x1 + (y1 - areaH * 4 / 5) * IGmap.w, (x1 - areaW / 5) + y0 * IGmap.w);

    // 判断是否为人脸
    if ((double)w_all / (areaW * areaH) < 0.19)
        return 1;
    if ((double)(w_1 + w_2 + w_6 + w_7) / (w_3 + w_8) < 2.6 || (double)(w_4 + w_5 + w_9 + w_10) / (w_3 + w_8) < 2.6)
        return 1;
    if ((double)(w_13 + w_18) / (w_11  + w_16 ) < 1 || (double)(w_13 + w_18) / ( w_15 + w_20) < 1)
        return 1;
    if ((double)(w_1 + w_2 + w_6 + w_7) / (w_11 + w_12 + w_16 + w_17) < 1.3 || (double)(w_4 + w_5 + w_9 + w_10) / (w_14 + w_15 + w_19 + w_20) < 1.3)
        return 1;
    if ((double)(w_1 + w_2 + w_3 + w_4 + w_5 + w_6 + w_7 + w_8 + w_9 + w_10) / (w_16 + w_17 + w_18 + w_19 + w_20 + w_21 + w_22 + w_23 + w_24 + w_25) > 2)
        return 1;
    if ((double)(w_1 + w_2 + w_6 + w_7 + w_4 + w_5 + w_9 + w_10 + w_13 + w_17 + w_18 + w_19 + w_23) / w_all < 0.6)
        return 1;

    double PCT_1 = (double)min(w_1 + w_2 + w_6 + w_7 + w_11 + w_12 + w_16 + w_17 + w_21 + w_22, w_4 + w_5 + w_9 + w_10 + w_14 + w_15 + w_19 + w_20 + w_24 + w_25) / max(w_1 + w_2 + w_6 + w_7 + w_11 + w_12 + w_16 + w_17 + w_21 + w_22, w_4 + w_5 + w_9 + w_10 + w_14 + w_15 + w_19 + w_20 + w_24 + w_25);
    PCT_1 = exp(-3.125 * (PCT_1 - 1) * (PCT_1 - 1)) * 100;

    double PCT_2 = (double)min(w_1 + w_2 + w_6 + w_7, w_4 + w_5 + w_9 + w_10) / max(w_1 + w_2 + w_6 + w_7, w_4 + w_5 + w_9 + w_10);
    PCT_2 = exp(-3.125 * (PCT_1 - 1) * (PCT_1 - 1)) * 100;

    double PCT_3 = (double)min(w_16 + w_21, w_20 + w_25) / max(w_16 + w_21, w_20 + w_25);
    PCT_3 = exp(-3.125 * (PCT_3 - 1) * (PCT_3 - 1)) * 100;

    // 计算总的概率
    double PCT_all = (PCT_1 + PCT_2 + PCT_3) / 3;

    if (PCT_all > 60)
        return PCT_all;
}

八. 多尺寸滑动窗口技术

8.1 多尺寸滑动窗口技术的介绍

5万字用纯C语言从零开始实现人脸检测

因为人脸可能出现在图片中的任何位置,所以我们用不同大小的滑动窗口以等距步长在整幅图像上滑动,并对每一个滑动窗口做人脸检测。

8.2 多尺寸滑动窗口技术的实现

// 人脸数据结构体
typedef struct tagFACEDATE
{
    int rightTop;
    int leftBottom;
    double confidence;
}FACEDATE;


//滑动窗口区域(训练用)
FACEDATE MoveWindowArea(IMAGE im, IGIMAGE IGmap)
{
    FACEDATE maxFaceDate = { 0 };       // 保存概率最大的人脸区域
    double confidence = 0;              // 置信度
    int minSide = min(im.w, im.h) / 3;  // 最小区域
    int daltaSide = 5;                  // 区域每次的增加量
    int k = 0;                          // faceDate结构体数组的下标
    
    // 窗口区域的取值范围
    for (int i = 0; i <= (min(im.w, im.h) - minSide) / daltaSide - 1; i++)
    {
        int rightTop = (minSide + i * daltaSide) * (im.w + 1);
        int leftBottom = 0;

        while (rightTop != im.w * im.h - 1)
        {
            if ((rightTop + 1) % im.w == 0)
            {
                rightTop += minSide + i * daltaSide;
                leftBottom += minSide + i * daltaSide;
            }
            else
            {
                rightTop += 1;
                leftBottom += 1;
            }

            if ((confidence = Classifier_decisionStump(IGmap, rightTop, leftBottom)) > 1 && confidence > maxFaceDate.confidence)
            {
                maxFaceDate.confidence = confidence;
                maxFaceDate.rightTop = rightTop;
                maxFaceDate.leftBottom = leftBottom;
            }
            
        }
    }

    return maxFaceDate;
 }

九. 运行项目

9.1 画出人脸框

// 画出人框
void Image_draw(IMAGE im ,FACEDATE faceDate)
{
    //画出人脸框
    for (unsigned int i = faceDate.leftBottom / im.w; i <= faceDate.rightTop / im.w; i++) {
        for (unsigned int j = faceDate.leftBottom % im.w; j <= faceDate.rightTop % im.w; j++) {
            if (i == faceDate.leftBottom / im.w || i == faceDate.rightTop / im.w || j == faceDate.leftBottom % im.w || j == faceDate.rightTop % im.w) {
                im.color[j + i * im.w].blue = 0;
                im.color[j + i * im.w].green = 0;
                im.color[j + i * im.w].red = 200;
            }
        }
    }
}

9.2 项目运行结果

int main()
{
	char loadFilename[300] = "C://Users//asus//Desktop//Image02//ren4.bmp";
	char saveFilename[300] = "C://Users//asus//Desktop//Image02//ren4.bmp";

	// 用于处理
	IMAGE image1 = Image_load(loadFilename);
	// 用于保存
	IMAGE image2= Image_load(loadFilename);
	// 灰度图
	Transform_color_grayscale(image1, GRAY_MODE_WEIGHT);
	// 均值滤波
	image1 = Wavefiltering_Average(image1);
	// 二值图加边缘检测
	image1 = Transform_color_BW_Adaptive(image1, 25);
	// 积分图
	IGIMAGE IGmap1 = IntegralImage_get(image1);
	// 滑动窗口
	FACEDATE faceDate1 = MoveWindowArea(image1, IGmap1);
	// 画出人脸框
	Image_draw(image2, faceDate1);
	// 保存图片
	Image_save(saveFilename, image2);
	// 释放积分图
	IntegralImage_free(IGmap1);
	// 释放图片资源 
	Image_free(image1);
	Image_free(image2);
	Image_show(saveFilename);
	return 0;
}

5万字用纯C语言从零开始实现人脸检测

十. 项目中遇到的问题

10.1 malloc也有初始化功能?

BGRA* bgra = (BGRA*)malloc(sizeof(BGRA) * 100 * 100);
for(int i=0; i<100* 100; i++)
{
	printf("%x ",bgra[i].blue);
	printf("%x ",bgra[i].green);
	printf("%x ",bgra[i].red);
	printf("%x\n",bgra[i].transparency);
}
// 结果为 cd cd cd cd cd cd.....
// 你可以用它生成图片

10.2 池化操作具体怎么使用?

作者认为池化操作与VJ算法的契合度并没有那么高,池化后的图片特征变的明显,但坐标被压缩,在池化后的图片中找寻人脸更加简单,但后续计算坐标困难(项目中并未使用)。

10.3 二值图的自适应阈值法也有边缘检测的功能?

作者在项目运行结果的代码中边缘检测用的是二值图的自适应阈值法,其效果和精度都好于边缘检测算法。

10.4 类哈尔特征怎么定义和计算?

文章中类哈尔特征的选取和计算,花费了作者大量的时间和精力,更改也及其麻烦。这也直接影响到最终人脸检测的正确度。

十一. 项目总结

  1. 由于个人能力有限,所以对一些相关算法的实现,进行了一定程度上的修改,代码中也有许多警告,但运行是没为问题的。
  2. 项目整体的人脸识别准确率和效率都不高。

十二. 结语

  1. 再简单的算法用数学去描述都是复杂的,作者的数学学不好,所以没有写出具体算法的数学原理和推导过程。
  2. 学得越多越知道自己知识有多匮乏,这篇文章的内容也越写越多,但受限与篇幅和作者的能力,每个知识点只能以这种简短的形式呈现。
  3. 代码进行了多次迭代,写博客的过程中也对其进行了三次重构(但人也没了),大幅提高了代码的可读性,可扩展性,和可维护性。
  4. 所有代码都在这篇文章中了,完整源码惨不忍睹就不拿出来献丑了。

不知道有多少人会看到这篇文章,也不知道有多少人会看到这里。最后再说几句吧。其实写这篇文章主要是写给自己看的,知识点有点多,怕自己忘记。所以一股脑整理了这一路下来的所学的所有东西。但一直坚持走下来真的很难,特别是学到一半发现要从头再来一遍的时候。。。。。。。。
好了就写这么多吧,最后希望这篇文章能帮助到你。

作者:墨尘_MO
时间:2021年10月3日

上一篇:CF 1425B Blue and Red of Our Faculty!


下一篇:go切片去重