dlib人脸关键点检测的模型分析与压缩
本文系原创,转载请注明出处~
小喵的博客: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
随机推荐
- http请求报文格式和响应报文格式
转载 出处 超文本传输协议(Hypertext Transfer Protocol,简称HTTP)是应用层协议.HTTP 是一种请求/响应式的协议,即一个客户端与服务器建立连接后,向服务器发送一个请求 ...
- applicaitonContext属性未注入, 请在applicationContext.xml中定义SpringContextHolder.
我用的jeesite框架,在使用定时任务时,出现一些问题,,1.在windows上项目可以在tomcat下,运行2.在linux下项目在tomcatgh下,却出现问题: 15-Dec-2017 15: ...
- ChromeExtension那些事儿
Chrome Extension是什么呢? 简而言之,就是Chrome扩展,它是基于Chrome浏览器的,我们可以理解它为一个独立运行在Chrome浏览器下的APP,当然核心编程语言就是JavaScr ...
- opensslBIO系列之2---BIO结构和BIO相关文件介绍
BIO结构和BIO相关文件介绍 (作者:DragonKing Mail:wzhah@263.net 公布于:http://gdwzh.126.com openssl专业论坛) ...
- Cairo-Dock 系统关机无效
正文 背景 Cairo-Dock 设置为开机自己主动启动后.系统菜单条里的关机选项就无效了,命令行里能够使用命令关机. 搜索过程 这次google找到的结果让我非常失望,于是仅仅好百度了. 在百度贴吧 ...
- 相似QQ对话框上下部分可拖动代码
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content ...
- SSH2——filter过滤器
概述: 过滤器是Servlet2.3以上新添加的一个功能,其技术也是很强大的.通过Filter技术能够对WEBserver的文件进行拦截,从而实现一些特殊的功能. 在JSP开发应用中也是必备的技能之中 ...
- NIO相关基础篇三
转载请注明原创出处,谢谢! 说在前面 上篇NIO相关基础篇二,主要介绍了文件锁.以及比较关键的Selector,本篇继续NIO相关话题内容,主要谈谈一些Linux 网络 I/O模型.零拷贝等一些内容, ...
- J2EE: JCA (Java Connector Architecture) [转]
JCA (J2EE 连接器架构,Java Connector Architecture)是对J2EE标准集的重要补充.因为它注重的是将Java程序连接到非Java程序和软件包中间件的开发.连接器特指基 ...
- 【批处理】shift用法举例
@echo off set sum=0 call :sub sum 1 2 3 4 echo sum=%sum% pause :sub set /a %1=%1+%2 shift /2 if not ...