本文系原创,转载请注明出处~
小喵的博客: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。
注意:
- 本文假定了读者对该论文有一定的了解,可以使用Dlib完成人脸关键点的训练和部署,因此不做论文的相关方法的解释。、
- 本文中分析的数据都是Dlib的shape_predictor类的私有成员,这里不得不把他们的修饰符从private改成了public,但文中并没有专门指出。
- 本文中所有的代码均在本地的64位操作系统上运行,在变量数据存储的大小描述的时候也均以64位来说明,即使是不同的编译器也会对数据大小造成影响,但这不是本文的重点。
- 本文中的数据类型如果不在C++中见到的数据类型,则为下面的typedef的数据类型
typedef char int8;
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人脸关键点检测的模型分析与压缩的更多相关文章
-
opencv+python+dlib人脸关键点检测、实时检测
安装的是anaconde3.python3.7.3,3.7环境安装dlib太麻烦, 在anaconde3中新建环境python3.6.8, 在3.6环境下安装dlib-19.6.1-cp36-cp36 ...
-
机器学习进阶-人脸关键点检测 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 ...
-
OpenCV实战:人脸关键点检测(FaceMark)
Summary:利用OpenCV中的LBF算法进行人脸关键点检测(Facial Landmark Detection) Author: Amusi Date: 2018-03-20 ...
-
OpenCV Facial Landmark Detection 人脸关键点检测
Opencv-Facial-Landmark-Detection 利用OpenCV中的LBF算法进行人脸关键点检测(Facial Landmark Detection) Note: OpenCV3.4 ...
-
用keras实现人脸关键点检测(2)
上一个代码只能实现小数据的读取与训练,在大数据训练的情况下.会造内存紧张,于是我根据keras的官方文档,对上一个代码进行了改进. 用keras实现人脸关键点检测 数据集:https://pan.ba ...
-
keras实现简单CNN人脸关键点检测
用keras实现人脸关键点检测 改良版:http://www.cnblogs.com/ansang/p/8583122.html 第一步:准备好需要的库 tensorflow 1.4.0 h5py ...
-
Opencv与dlib联合进行人脸关键点检测与识别
前言 依赖库:opencv 2.4.9 /dlib 19.0/libfacedetection 本篇不记录如何配置,重点在实现上.使用libfacedetection实现人脸区域检测,联合dlib标记 ...
-
Facial landmark detection - 人脸关键点检测
Facial landmark detection (Facial keypoints detection) OpenSourceLibrary: DLib Project Home: http: ...
-
级联MobileNet-V2实现CelebA人脸关键点检测(转)
https://blog.csdn.net/u011995719/article/details/79435615
随机推荐
-
揭秘史上最完美一步到位的搭建Andoriod开发环境
Windows环境下Android开发环境搭建虽然不难而且网上资料众多,但是众多资料如出一折 忽略了很多细节,最终还是没能达到满意效果. 基本步骤如下:JDK安装.环境变量配置.Eclipse下载.A ...
-
APP One Link ,android and ios qrcode merge as One QRCode and one short link
Adroid and ios qrcode merge as One QRCode and one short link is publish , the web site is www.appone ...
-
jQuery鼠标悬停内容动画切换效果
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
-
UML系列01之 UML和绘图工具Visio介绍
概要 UML,全称是Unified Modeling Language,中文是"统一建模语言".通俗点说,UML是一种创建模型的语言.UML是在开发阶段,说明,可视化,构建和书写一 ...
-
wget进行整站下载
wget加上参数之后,即可成为相当强大的下载工具. wget -r -p -np -k http://xxx.com/abc/ -r, --recursive(递归) specif ...
-
oracle-odu小试牛刀--恢复drop表的数据
现在进入oracle12c时代:普遍用的oracle版本为10g以上.在oracle10g之后提供了一个回收的机制.所以恢复drop表的数据以及表很容易.当然需要打开回收机制以及是归档模式. ...
-
Laravel 4 Blade模板引擎
http://my.oschina.net/5say/blog/201290 模板输出 基本输出 1 <!-- app/views/example.blade.php --> 2 < ...
-
Duanxx的Design abroad: C++矩阵运算库Eigen 概要
一.概要 这两天想起来要做神经网络的作业了,要求用C++完毕神经网络的算法. 摆在面前的第一个问题就是,神经网络算法中大量用到了矩阵运算.可是C++不像matlab那样对矩阵运算有非常好的支持.本来准 ...
-
P2P直播承载平台与CDN直播承载平台比较
收看软件不一样:CDN直播收看无需安装第三方收看软件,一般操作系统已带播放器软件:P2P直播收看需要安装厂家自己的播放器软件,每家P2P的软件不兼容,收看者要装多套软件才能收看不同内容. 收看人数不一 ...
-
webstorm下的sass自动编译和移动端自适应实践
1.安装Ruby 2.安装sass 3.webstorm配置file watcher 4.移动端自适应 1.安装Ruby 安装Ruby,有多种方式,打开官网下载 因为,使用的是window选择Ruby ...