天天看点

写给程序员的机器学习入门 (十二) - 脸部关键点检测

在前几篇文章中我们看到了怎样检测图片上的物体,例如人脸,那么把实现人脸识别的时候是不是可以把图片中的人脸截取出来再交给识别人脸的模型呢?下面的流程是可行的,但因为人脸的范围不够准确,截取出来的人脸并不在图片的正中心,对于识别人脸的模型来说,数据质量不够好就会导致识别的效果打折。

这一篇文章会介绍如何使用机器学习检测脸部关键点 (眼睛鼻子嘴巴的位置),检测图片上的人脸以后,再检测脸部关键点,然后基于脸部关键点来调整人脸范围,再根据调整后的人脸范围截取人脸并交给后面的模型,即可提升数据质量改善识别效果。

写给程序员的机器学习入门 (十二) - 脸部关键点检测

脸部关键点检测模型其实就是普通的 CNN 模型,在第八篇文章中已经介绍过🤒,第八篇文章中,输入是图片,输出是分类 (例如动物的分类,或者验证码中的字母分类)。而这一篇文章输入同样是图片,输出则是各个脸部关键点的坐标:

写给程序员的机器学习入门 (十二) - 脸部关键点检测

我们会让模型输出五个关键点 (左眼中心,右眼中心,鼻尖,嘴巴左边角,嘴巴右边角) 的 x 坐标与 y 坐标,合计一共 10 个输出。

模型输出的坐标值范围会落在 -1 ~ 1 之间,这是把图片左上角视为 -1,-1,右下角视为 1,1 以后正规化的坐标值。不使用绝对值的原因是机器学习模型并不适合处理较大的值,并且使用相对坐标可以让处理不同大小图片的逻辑更加简单。你可能会问为什么不像前一篇介绍的 YOLO 一样,让坐标值范围落在 0 ~ 1 之间,这是因为下面会使用仿射变换来增加人脸样本,而仿射变换要求相对坐标在 -1 ~ 1 之间,让坐标值范围落在 -1 ~ 1 之间可以省掉转换的步骤。

准备数据集是机器学习中最头疼的部分,一般来说我们需要上百度搜索人脸的图片,然后一张一张的截取,再手动标记各个器官的位置,但这样太苦累了😭。这篇还是像之前的文章一样,从网上找一个现成的数据集来训练,偷个懒🤗。

使用的数据集:

https://www.kaggle.com/drgilermo/face-images-with-marked-landmark-points

下载回来以后可以看到以下的文件:

<code>face_images.npz</code> 是使用 zip 压缩后的 numpy 数据转储文件,把文件名改为 <code>face_images.zip</code> 以后再解压缩即可得到 <code>face_images.npy</code> 文件。

之后再执行 python 命令行,输入以下代码加载数据内容:

可以看到数据包含了 7049 张 96x96 的黑白人脸图片。

再输入以下代码保存第一章图片:

这就是提取出来的图片:

写给程序员的机器学习入门 (十二) - 脸部关键点检测

对应以下的坐标,坐标的值可以在 <code>facial_keypoints.csv</code> 中找到:

各个坐标对应 csv 中的字段如下:

左眼中心点的 x 坐标: left_eye_center_x

左眼中心点的 y 坐标: left_eye_center_y

右眼中心点的 x 坐标: right_eye_center_x

右眼中心点的 y 坐标: right_eye_center_y

鼻尖的 x 坐标: nose_tip_x

鼻尖的 y 坐标: nose_tip_y

嘴巴左边角的 x 坐标: mouth_left_corner_x

嘴巴左边角的 y 坐标: mouth_left_corner_y

嘴巴右边角的 x 坐标: mouth_right_corner_x

嘴巴右边角的 y 坐标: mouth_right_corner_y

csv 中还有更多的坐标但我们只使用这些🤒。

接下来定义一个在图片上标记关键点的函数:

再使用这个函数标记图片即可得到:

写给程序员的机器学习入门 (十二) - 脸部关键点检测

仔细观察 csv 中的坐标值,你可能会发现里面的坐标大多都是很接近的,例如左眼中心点的 x 坐标大部分都落在 65 ~ 75 之间。这是因为数据中的人脸图片都是经过处理的,占比和位置比较标准。如果我们直接拿这个数据集来训练,那么模型只会输出学习过的区间的值,这是再拿一张占比和位置不标准的人脸图片给模型,模型就会输出错误的坐标。

解决这个问题我们可以随机旋转移动缩放人脸以增加数据量,在第十篇文章我们学到怎样用仿射变换来提取图片中的某个区域并缩放到固定的大小,仿射变换还可以用来实现旋转移动和缩放,批量计算时的效率非常高。

写给程序员的机器学习入门 (十二) - 脸部关键点检测

首先我们需要以下的变量:

弧度,范围是 -π ~ π,对应 -180°~ 180°

缩放比例,1 代表 100%

横向移动量:范围是 -1 ~ 1,把图片中心视为 0,左边视为 -1,右边视为 1

纵向移动量:范围是 -1 ~ 1,把图片中心视为 0,左边视为 -1,右边视为 1

根据这些变量生成仿射变换参数的公式如下:

写给程序员的机器学习入门 (十二) - 脸部关键点检测

需要注意的是仿射变换参数用于转换 目标坐标 到 来源坐标,在处理图片的时候可以根据目标像素找到来源像素,然后设置来源像素的值到目标像素的值实现各种变形操作。上述的参数只能用于处理图片,如果我们想计算变形以后的图片对应的坐标,我们还需要一个转换 来源坐标 到 目标坐标 的仿射变换参数,计算相反的仿射变换参数的公式如下:

写给程序员的机器学习入门 (十二) - 脸部关键点检测

翻译到代码如下:

变形后的人脸样本如下,背景添加了随机颜色让模型更难作弊,具体代码参考后面的 <code>prepare</code> 函数吧😰:

写给程序员的机器学习入门 (十二) - 脸部关键点检测

完整代码的时间到了🥳,结构跟前面的文章一样,分为 <code>prepare</code>, <code>train</code>, <code>eval</code> 三步。

文章中的 Resnet 模型直接引用了 torchvision 中的实现,结构在第八篇文章已经介绍过,为了支持黑白图片修改了 <code>conv1</code> 使得入通道数变为 1。

另外一个细节是部分关键点的数据是缺损的,例如只有左眼和右眼的坐标,但是没有鼻子和嘴巴的坐标,缺损数据在读取的时候会变为 <code>nan</code>,所以代码中计算损失和正确率的时候会排除掉值为 <code>nan</code> 的数据,这样即使同一数据的部分坐标缺损模型仍然可以学习没有缺损的坐标。

把代码保存到 <code>face_landmark.py</code>,然后按以下的文件夹结构放代码和数据集:

dataset

face-images-with-marked-landmark-points

face_images.npy

facial_keypoints.csv

face_landmark.py

再执行以下命令即可开始训练:

最终训练结果如下:

坐标的偏差大约是 <code>(1 - 0.976565406219812) * 2</code> 即相对图片长宽的 4.68% 左右🤔。

再执行以下命令即可使用训练好的模型:

以下是部分识别结果😤:

写给程序员的机器学习入门 (十二) - 脸部关键点检测
写给程序员的机器学习入门 (十二) - 脸部关键点检测
写给程序员的机器学习入门 (十二) - 脸部关键点检测

有一定误差,但是用来调整脸部范围是足够了。此外,训练出来的模型检测出来的鼻尖坐标会稍微偏下,这是因为训练数据中的大部分都是鼻子比较高的白人😡,我们看到新闻里面说人脸识别模型对于黑人识别率很低也是因为样本中绝大部分都是白人,数据的偏向会直接影响模型的检测结果🤒。

这篇比较简单,下一篇将会介绍人脸识别模型,到时会像这篇最开始给出的图片一样结合三个模型实现。

最后骂一句博客园的傻逼验证码,这种验证码只是拿来恶心用户,对安全没啥实质性的帮助🤬。