dlib人脸关键点检测的模型分析与压缩

喵耳朵 喵耳朵     2022-10-09     283

关键词:

本文系原创,转载请注明出处~

小喵的博客:https://www.miaoerduo.com

博客原文(排版更精美):https://www.miaoerduo.com/c/dlib人脸关键点检测的模型分析与压缩.html

github项目:https://github.com/miaoerduo/dlib-face-landmark-compression

 

人脸关键点检测的技术在很多领域上都有应用,首先是人脸识别,常见的人脸算法其实都会有一步,就是把人脸的图像进行对齐,而这个对齐就是通过关键点实现的,因此关于人脸关键点检测的论文也常叫face alignment,也就是人脸对齐。另一方面,对于美颜,2D/3D建模等等也需要一来人脸的关键点技术,而且通常也要求有尽可能多的人脸关键点。

Dlib is a modern C++ toolkit containing machine learning algorithms and tools for creating complex software in C++ to solve real world problems. It is used in both industry and academia in a wide range of domains including robotics, embedded devices, mobile phones, and large high performance computing environments. Dlib‘s open source licensing allows you to use it in any application, free of charge.

Dlib是一个包含了大量的机器学习和复杂软件开发工具的现代C++工具箱,被广泛的用于软件开发等领域。

本篇博客主要研究的就是Dlib中的人脸关键点检测的工具。该工具的方法依据是 One Millisecond Face Alignment with an Ensemble of Regression Trees by Vahid Kazemi and Josephine Sullivan, CVPR 2014 这篇论文,在速度和精度上均达到了极好的效果。

本文的侧重点在于人脸关键点模型的存储结构的分析和模型的压缩策略分析,最终在性能几乎不变的情况下,得到模型的至少10倍的压缩比。项目最终的github地址为:https://github.com/miaoerduo/dlib-face-landmark-compression 欢迎fork、star和pr。

注意:

  1. 本文假定了读者对该论文有一定的了解,可以使用Dlib完成人脸关键点的训练和部署,因此不做论文的相关方法的解释。、
  2. 本文中分析的数据都是Dlib的shape_predictor类的私有成员,这里不得不把他们的修饰符从private改成了public,但文中并没有专门指出。
  3. 本文中所有的代码均在本地的64位操作系统上运行,在变量数据存储的大小描述的时候也均以64位来说明,即使是不同的编译器也会对数据大小造成影响,但这不是本文的重点。
  4. 本文中的数据类型如果不在C++中见到的数据类型,则为下面的typedef的数据类型
typedef char   int8;
typedef short  int16;
typedef int    int32;
typedef long   int64;
typedef float  float32;
typedef double float64;
typedef unsigned char  uint8;
typedef unsigned short uint16;
typedef unsigned int   uint32;
typedef unsigned long  uint64;

Dlib中人脸关键点实现的类是dlib::shape_predictor,源码为:https://github.com/davisking/dlib/blob/master/dlib/image_processing/shape_predictor.h

这里简单的抽取了数据相关的接口定义:

namespace dlib
{
// ----------------------------------------------------------------------------------------
    namespace impl
    {
        struct split_feature
        {
            unsigned long idx1;
            unsigned long idx2;
            float thresh;
        };

        struct regression_tree
        {
            std::vector<split_feature> splits;
            std::vector<matrix<float,0,1> > leaf_values;
        };
    } // end namespace impl

// ----------------------------------------------------------------------------------------
    class shape_predictor
    {
    private:
        matrix<float,0,1> initial_shape;
        std::vector<std::vector<impl::regression_tree> > forests;
        std::vector<std::vector<unsigned long> > anchor_idx; 
        std::vector<std::vector<dlib::vector<float,2> > > deltas;
    };
}

 

下面,我们逐一对每个部分的参数进行分析。

Dlib内置了很多的数据类型,像vector、metrix等等,每种数据类型又可以单独序列化成二进制的数据。对于shape_predictor的序列化,本质上就是不断的调用成员变量数据的序列化方法,由此极大地简化代码,提高了代码的复用率。

inline void serialize (const shape_predictor& item, std::ostream& out)
    {
        int version = 1;
        dlib::serialize(version, out);
        dlib::serialize(item.initial_shape, out);
        dlib::serialize(item.forests, out);
        dlib::serialize(item.anchor_idx, out);
        dlib::serialize(item.deltas, out);
    }

但,对于移动端等应用场景,需要模型占用尽可能少的存储空间,这样一来,这些标准的存储方式就会造成数据的很大程度的冗余。我们的任务就是一点点的减少这些冗余,只存有用的数据。

一、常量部分

首先,我们需要知道一些常量的数据。这些数据完成了对模型的描述。

变量名数据类型作用
version uint64 记录模型版本号
cascade_depth uint64 回归树的级数
num_trees_per_cascade_level uint64 每一级中的树的个数
tree_depth uint64 树的深度
feature_pool_size uint64 特征池的大小
landmark_num uint64 特征点的数目
quantization_num uint64 量化的级数
prune_thresh float32 剪枝的阈值

 

二、初始形状 initial_shape

matrix<float,0,1> initial_shape; 表示的是初始化人脸关键点的坐标,存储类型是float型,个数为 landmark_num * 2 (不要忘了一个点是两个数组成 :P)。

三、锚点 anchor_idx

std::vector<std::vector<unsigned long> > anchor_idx; 是一个二维的数组,存放的是landmark点的下标。在常见的68点和192点的任务中,使用一个uint8就可以存放下标,而这里使用的是unsigned long,显然过于冗余,这里可以简化成uint8存储。这个二维数组的大小为 cascade_depth * feature_pool_size 。每一级回归树使用一套锚点。

四、deltas

std::vector<std::vector<dlib::vector<float,2> > > deltas;和anchor_idx类似,是一个二维数组,不同的是,数组的每个值都是dlib::vector<float,2>的结构。这个数组的大小为 cascade_depth * feature_pool_size * 2 ,存放的内容是float数值。考虑到这里的参数量很少,没有压缩的必要,这里我们直接存储原数据。

五、森林 forests

这部分是模型参数量最大的部分,一个模型大概2/3的存储都耗在了这个地方。这里才是我们压缩的重点!

std::vector<std::vector<impl::regression_tree> > forests;一个shape_predictor中,有cascade_depth级,每一级有num_trees_per_cascade_level棵树。对于每棵树,它主要存放了两个部分的数据:分割的阈值splits和叶子的值leaf_values。为了便于阅读,再把数据结构的定义附上。

namespace dlib
{
    namespace impl
    {
        struct split_feature
        {
            unsigned long idx1;
            unsigned long idx2;
            float thresh;
        };

        struct regression_tree
        {
            std::vector<split_feature> splits;
            std::vector<matrix<float,0,1> > leaf_values;
        };
    } // end namespace impl
}

5.1 splits

splits存放的数据是阈值和特征像素值的下标,这个下标的范围是[0, feature_pool_size),在通常情况下,feature_pool_size不会太大,论文中最大也就设到了2000。这里我们可以使用一个uint16来存储。thresh就直接存储。对于一棵树,树的深度为tree_depth,则有 2^tree_depth - 1 个split_node。(这里认为只有根节点的树深度为0)。

5.2 leaf_values

std::vector<matrix<float,0,1> > leaf_values;对于深度为tree_depth的树,有 2^tree_depth 个叶子节点。对于每个叶子节点,需要存储整个关键点的偏移量,也就是说每个节点存放了 landmark_num * 2 个float的数值。那么这部分的参数量到底有多大呢?

举个例子,在cascade_num为10,num_trees_per_cascade_level为500,tree_depth为5,landmark_num为68的时候。leaf_values的值有cascade_num * num_trees_per_cascade_level * (2 ^ tree_depth) * landmark_num * 2 = 21760000 = 20.8M 的参数量,由于使用float存储,通常一个float是4个字节,因此总的存储量达到了逆天的80MB!远大于其他的参数的总和。

那么如何才能有效的降低这部分的存储量呢?

这就要要用到传说中的模型压缩三件套:剪枝,量化编码

5.2.1 参数分布分析

首先笔者统计了参数的分布,大致的情况是这样的,(具体的结果找不到了)。

叶子节点里的参数的范围在[-0.11, 0.11]之间,其中[-0.0001, 0.0001]的参数占了50%以上。说明模型中有大量的十分接近0的数字。

5.2.2 剪枝

剪枝的策略十分粗暴,选择一个剪枝的阈值prune_thresh,将模小于阈值的数全部置0。

5.2.3 量化

量化的过程,首先获取数据中的最小值和最大值,记为:leaf_min_value 和 leaf_max_value。之后根据量化的级数 quantization_num,计算出每一级的步长:quantization_precision = (leaf_max_value - leaf_min_value) / quantization_num。之后对于任意数值x,那么它最终为 x / quantization_precision 进行四舍五入的结果。这样就可以把float的数字转换成整形来表示。量化级数越高,则量化之后的值损失就越小。

5.3.3 编码

如果我们不做任何的编码操作,直接存储量化之后的结果,也是可以一定程度上进行模型的压缩的。比如使用256级量化,则量化的结果使用一个uint8就可以存储,从而把存储量降为原来的1/4。但是这样有两个问题:1,依赖量化的级数;2,存储量减少不大。

在信息论中有个信息熵的概念。为了验证存储上的可以再优化,这里选择了一个68点的模型,经过256级量化之后,计算出信息熵(信息熵的计算请查阅其他的资料),其数值为1.53313,也就是说,理想情况下,一个数值只需要不到2 bits就可以存储了。如果不编码则需要8 bits。压缩比为 1.53313 / 8 = 19.2 %,前者仅为后者的1/5不到!

这里,我采用的是经典的huffman编码,使用了github上的 https://github.com/ningke/huffman-codes 项目中的代码,感谢作者的贡献!

原项目中只能对char类型的数据进行编码,因此这里也做了相应的修改,以适应于int类型的编码,同时删除了一些用不到的函数。

使用huffman对上述的256级的数值进行编码,最终的每个数字的平均长度为1.75313,已经很接近理想情况。

使用huffman编码时,同时需要将码表进行储存,这部分细节较为繁琐,读者可以自行阅读源码。

 

至此,Dlib的模型的分析和压缩就全部介绍完了。对代码感兴趣的同学可以在:https://github.com/miaoerduo/dlib-face-landmark-compression ,也就是我的github上clone到最新的代码,代码我目前也在不断的测试,如果有问题,也会及时更新的。

在本地的实验中,原模型的大小为127M,压缩之后只有5.9M,且性能几乎不变(这里prune_thresh设为0.0001, quantization_num设为256,quantization_num设置越大,则精度越接近原模型,同时prune_thresh的大小很多时候是没有用的)。

 

马上就要毕业了,希望写博客的习惯能够一直保持下去。

最后,再一次,希望小喵能和大家一起学习和进步~~

dlib库的68特征原理人脸关键点检测原理

...ls/80441556shape_predictor_68_face_landmarks.dat是已经训练好的人脸关键点检测器。dlib_face_recognition_resnet_model_v1.dat是训练好的ResNet人脸识别模型。(说明:ResNet是何凯明在微软的时候提出的深度残差网络,获得了ImageNet2015冠军,通过让网... 查看详情

基于tensorflow2.x从零训练15点人脸关键点检测模型(代码片段)

一、人脸关键点检测数据集在计算机视觉人脸计算领域,人脸关键点检测是一个十分重要的区域,可以实现例如一些人脸矫正、表情分析、姿态分析、人脸识别、人脸美颜等方向。人脸关键点数据集通常有5点、15点、68点... 查看详情

图片人脸检测——dlib版

上几篇给大家讲了OpenCV的图片人脸检测,而本文给大家带来的是比OpenCV更加精准的图片人脸检测Dlib库。点击查看往期:《图片人脸检测——OpenCV版(二)》《视频人脸检测——OpenCV版(三)》dlib与OpenCV对比识别精准... 查看详情

人脸识别完整项目实战:完整项目案例运行演示

一、前言本文是《人脸识别完整项目实战》系列博文第1部分,第一节《完整项目运行演示》,本章内容系统介绍:人脸系统核心功能的运行演示。本内容已经录制成视频课程,详见网易云课堂。整个《人脸识别完整项目实战》... 查看详情

dlib代码解读人脸关键点检测器的训练

1.源代码先给出测试的结果,关键点并不是特别准,原因是训练样本数据量太少。以下给出完整的人脸关键点检测器训练代码。详细的代码解读请看第二部分。/*faceLandmarksTrain.cppfunction:借助dlib训练自己的人脸关键点检测器(参考dl... 查看详情

dlib+opencv库实现疲劳检测(代码片段)

文章目录1.关键点检测2.算法实现的核心点3.算法实现(1)人脸的关键点集合(2)加载人脸检测库和人脸关键点检测库(3)绘制人脸检测的框(4)对检测之后的人脸关键点坐标进行转换(5)... 查看详情

如何线上部署用python基于dlib写的人脸识别算法

python使用dlib进行人脸检测与人脸关键点标记Dlib简介:首先给大家介绍一下DlibDlib是一个跨平台的C++公共库,除了线程支持,网络支持,提供测试以及大量工具等等优点,Dlib还是一个强大的机器学习的C++库,包含了许多机器学习... 查看详情

opencv+openvino实现人脸landmarks实时检测(代码片段)

...模型基于自定义的卷积神经网络实现,取35个人脸各部位关键点。人脸检测模型使用OpenCVDNN模块人脸检测的tensorflow量化8位模型opencv_face_detector_uint8.pb权重文件opencv_face_detector.pbtxt配 查看详情

opencv+dlib实现疲劳检测(代码片段)

目录(1)锁定眼睛部分关键点(2)EAR(2)设置参数(3)对视频的每一帧进行判断本文基于OpenCV并利用dlib工具包,通过实时计算EAR值来统计眨眼次数实现了疲劳检测。步骤:首先对对检测到的人脸进行关键点定位并锁定眼睛部分的关键点... 查看详情

视频人脸检测——dlib版(代码片段)

往期目录视频人脸检测——Dlib版(六)OpenCV添加中文(五)图片人脸检测——Dlib版(四)视频人脸检测——OpenCV版(三)图片人脸检测——OpenCV版(二)OpenCV环境搭建(一)更多更新,欢迎访问我的github:https://github.com/vipstone/... 查看详情

基于tensorflow2.x从零训练15点人脸关键点检测模型(代码片段)

一、人脸关键点检测数据集在计算机视觉人脸计算领域,人脸关键点检测是一个十分重要的区域,可以实现例如一些人脸矫正、表情分析、姿态分析、人脸识别、人脸美颜等方向。人脸关键点数据集通常有5点、15点、68点... 查看详情

基于tensorflow2.x从零训练15点人脸关键点检测模型(代码片段)

一、人脸关键点检测数据集在计算机视觉人脸计算领域,人脸关键点检测是一个十分重要的区域,可以实现例如一些人脸矫正、表情分析、姿态分析、人脸识别、人脸美颜等方向。人脸关键点数据集通常有5点、15点、68点... 查看详情

加载人像检测模型的代码是

很多人都认为人脸识别是一项非常难以实现的工作,看到名字就害怕,然后心怀忐忑到网上一搜,看到网上N页的教程立马就放弃了。这些人里包括曾经的我自己。其实如果如果你不是非要深究其中的原理,只是要实现这一工作... 查看详情

MTCNN 与 DLIB 相比如何进行人脸检测?

】MTCNN与DLIB相比如何进行人脸检测?【英文标题】:HowdoesMTCNNperformvsDLIBforfacedetection?【发布时间】:2018-06-0910:18:46【问题描述】:我看到MTCNN被推荐,但没有看到DLIB和MTCNN的直接比较。我认为既然MTCNN使用神经网络,它可能更适... 查看详情

pytorch深度学习实战|基于resnet的人脸关键点检测

人脸关键点检测指的是用于标定人脸五官和轮廓位置的一系列特征点的检测,是对于人脸形状的稀疏表示。关键点的精确定位可以为后续应用提供十分丰富的信息。因此,人脸关键点检测是人脸分析领域的基础技术之一... 查看详情

人脸检测进阶:使用dlibopencv和python检测面部标记(代码片段)

...象的ROI),形状预测器尝试沿形状定位感兴趣的关键点。在人脸标记的背景下,我们的目标是使用形状预 查看详情

Dlib cuda 人脸检测 .dat 模型在 GTX 1650 上崩溃,而在许多其他 gpu 设备上表现良好

】Dlibcuda人脸检测.dat模型在GTX1650上崩溃,而在许多其他gpu设备上表现良好【英文标题】:Dlibcudafacedetection.datmodelcrashonGTX1650whilebeinggoodonmanyothergpudevices【发布时间】:2021-01-0708:14:58【问题描述】:我得到了与下面相同的Dockerfile和... 查看详情

基于dlib的人脸检测(68关键点)(代码片段)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档文章目目录文章目录前言一、背景(1)环境搭建(2)下载开源数据集二、具体实现效果展示:效果展示:总结前言imutils... 查看详情