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

小喵的博客: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 long   int64;
typedef float       float32;
typedef double      float64;
typedef unsigned char       uint8;
typedef unsigned short      uint16;
typedef unsigned int        uint32;
typedef unsigned long 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,,> > leaf_values;
};
} // end namespace impl // ----------------------------------------------------------------------------------------
class shape_predictor
{
private:
matrix<float,,> 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,> > > deltas;
};
}

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

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

inline void serialize (const shape_predictor& item, std::ostream& out)
{
int version = ;
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,,> > 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人脸关键点检测的模型分析与压缩的更多相关文章

  1. opencv+python+dlib人脸关键点检测、实时检测

    安装的是anaconde3.python3.7.3,3.7环境安装dlib太麻烦, 在anaconde3中新建环境python3.6.8, 在3.6环境下安装dlib-19.6.1-cp36-cp36 ...

  2. 机器学习进阶-人脸关键点检测 1.dlib.get_frontal_face_detector(构建人脸框位置检测器) 2.dlib.shape_predictor(绘制人脸关键点检测器) 3.cv2.convexHull(获得凸包位置信息)

    1.dlib.get_frontal_face_detector()  # 获得人脸框位置的检测器, detector(gray, 1) gray表示灰度图, 2.dlib.shape_predict ...

  3. OpenCV实战:人脸关键点检测(FaceMark)

    Summary:利用OpenCV中的LBF算法进行人脸关键点检测(Facial Landmark Detection) Author:    Amusi Date:       2018-03-20 ...

  4. OpenCV Facial Landmark Detection 人脸关键点检测

    Opencv-Facial-Landmark-Detection 利用OpenCV中的LBF算法进行人脸关键点检测(Facial Landmark Detection) Note: OpenCV3.4 ...

  5. 用keras实现人脸关键点检测(2)

    上一个代码只能实现小数据的读取与训练,在大数据训练的情况下.会造内存紧张,于是我根据keras的官方文档,对上一个代码进行了改进. 用keras实现人脸关键点检测 数据集:https://pan.ba ...

  6. keras实现简单CNN人脸关键点检测

    用keras实现人脸关键点检测 改良版:http://www.cnblogs.com/ansang/p/8583122.html 第一步:准备好需要的库 tensorflow  1.4.0 h5py ...

  7. Opencv与dlib联合进行人脸关键点检测与识别

    前言 依赖库:opencv 2.4.9 /dlib 19.0/libfacedetection 本篇不记录如何配置,重点在实现上.使用libfacedetection实现人脸区域检测,联合dlib标记 ...

  8. Facial landmark detection - 人脸关键点检测

    Facial landmark detection  (Facial keypoints detection) OpenSourceLibrary: DLib Project Home:  http: ...

  9. 级联MobileNet-V2实现CelebA人脸关键点检测(转)

    https://blog.csdn.net/u011995719/article/details/79435615

随机推荐

  1. 倒水问题 (FillUVa 10603) 隐式图

    题意:本题的题意是给你三个杯子,第一二个杯子是空的,第三个杯子装满水,要求是量出一定容量d升的水.若是得不到d升的水,那就让某一个杯子里面的水达到d',使得d'尽量接近d升. 解题思路:本题是给出初始 ...

  2. 翻煎饼 Stacks of Flapjacks

    题意:本题意为煎饼排序,大的放在上面,小的放在下面(此题输入是从上到下输入的),为煎饼排序是通过一系列的"翻转"动作来完成的.翻转动作就是将一个小铲插到一叠煎饼中的某两个煎饼之间, ...

  3. 如何在BIOS里设置定时关机?

    如何在BIOS里设置定时关机? 通过CMOS设置实现定时开机的设置过程如下: 首先进入"CMOS SETUP"程序(大多数主板是在计算机启动时按DEL键进入): 然后将光条移到&q ...

  4. SAP ABAP编程 Table Control动态隐藏列

    在SAP DIALOG设计中,有时候须要动态的隐藏某些列,以下是方法. ***数据定义 CONTROLS: table_control TYPE TABLEVIEW USING SCREEN 0100 ...

  5. as 与 is

    在存储过程(PROCEDURE)和函数(FUNCTION)中没有区别:在视图(VIEW)中只能用AS:在游标(CURSOR)中只能用IS.

  6. jstl常用语句

    1.select框中if选中,下面的语句实现从后台给过来一个category实体,如果category的categoryType为指定的值,则选中. <select class="fo ...

  7. gunicorn geventworker 解析

    在前面的文章曾介绍过gunicorn的syncworker,本文介绍其中一种asyncworker:GeventWorker.类图如下:   可见GeventWorker重载了init_process ...

  8. java String,StringBuffer和StringBulder学习笔记

    1.String:不可改变的Unicode字符序列. 池化思想,把需要共享的数据放在池中,用一个存储区域来存放一些公用资源以减少存储空间的开销. 在String类中,以字面值创建时,回到java方法空 ...

  9. 用Inferno代替React开发高性能响应式WEB应用

    什么是Inferno Inferno可以看做是React的另一个精简.高性能实现.它的使用方式跟React基本相同,无论是JSX语法.组件的建立.组件的生命周期,还是与Redux或Mobx的配合.路由 ...

  10. Android查缺补漏(View篇)--自定义 View 中 wrap_content 无效的解决方案

    自定义 View 中 wrap_content 无效的解决方案 做过自定义 View 的童鞋都会发现,直接继承 View 的自定义控件需要重写 onMeasure() 方法,并设置 wrap_cont ...