1,了解iBUG 300-W数据集,该数据集是用于训练形状预测器的通用数据集,该预测器用于定位人脸的特定位置(即面部标志)。
2,训练自己的自定义dlib形状预测器,从而生成一个可以平衡速度,准确性和模型大小的模型。
3,最后,我们将形状预测器进行测试并将其应用于一组输入的图像/视频流,这表明我们的形状预测器能够实时运行。
https://ibug.doc.ic.ac.uk/resources/300-W/
http://dlib.net/files/data/ibug_300W_large_face_landmark_dataset.tar.gz
$ tar -xvf ibug_300W_large_face_landmark_dataset.tar.gz
top:边界框的起始y坐标。
left:边界框的起始x坐标。
width:边框的宽度。
height:边框的高度。
需要在训练形状预测器之前设置选项(即超参数):
1.tree_depth:
每棵回归树的深度,默认值为4,论文中为5,建议值范围为[2, 8]。
每棵回归树中总共有2^tree_depth的叶子数,即最底部的那一层中所有节点才为叶子,那么该整棵树的叶子数便为2^tree_depth。
2.nu:
建议值范围为 float(0, 1],默认为0.1。
接近1的值将使我们的模型更接近训练数据,但可能导致过度拟合。
接近0的值将有助于我们的模型推广;但是需要注意泛化能力的话,nu越接近0 那么则需要的训练数据就越多。
也就是值越大也更加fit训练样本数据,但也越容易可能过拟合。
3.cascade_depth:
回归树的级数,也即级联的级数。默认值为10,建议值范围为 [6, 18]。对accuracy和模型大小都有影响。
拥有的级联越多,模型可能就越准确,但是输出大小也越大。
4.num_trees_per_cascade_level
每层级联中所包含的树数目,默认值为500,即级联中的每一层级都默认包含500颗树。
5.oversampling_amount
对训练样本进行随机变形拓增样本的数量。默认值为20,建议值范围为[0,50]。
6.feature_pool_size
每层级联中从图片中随机取样的像素个数,这些像素作为训练回归树的特征池。
默认值为400,论文中设置了最大到2000。
为了降低复杂度才会设置这个值,值越大应该越精确,但是也会更耗时。
7.num_test_splits
生成回归树时,每个节点随机生成的分裂个数。
分析训练/测试XML数据集文件以获取仅眼睛的界标坐标
"""
命令行
python parse_xml.py --input ibug_300W_large_face_landmark_dataset/labels_ibug_300W_train.xml --output ibug_300W_large_face_landmark_dataset/labels_ibug_300W_train_eyes.xml
python parse_xml.py --input ibug_300W_large_face_landmark_dataset/labels_ibug_300W_test.xml --output ibug_300W_large_face_landmark_dataset/labels_ibug_300W_test_eyes.xml
"""
import argparse
import re
# 构造参数解析器并解析参数
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--input", required=False, default="./ibug_300W_large_face_landmark_dataset/labels_ibug_300W_train.xml", help="path to iBug 300-W data split XML file")
ap.add_argument("-t", "--output", required=False, default="./ibug_300W_large_face_landmark_dataset/labels_ibug_300W_train_eyes.xml", help="path output data split XML file")
args = vars(ap.parse_args())
"""
在iBUG 300-W数据集中,每个(x,y)坐标都映射到特定的面部特征(例如,眼睛,嘴巴,鼻子等)
为了仅在眼睛上训练dlib形状预测器 ,我们必须首先定义属于眼睛的整数索引。
左眼特征点的索引值:36到42。
右眼特征点的索引值:43到48。
"""
LANDMARKS = set(list(range(36, 48)))
#为了轻松地从XML文件中解析出眼睛的位置,我们可以利用正则表达式来确定任何给定行上是否存在“ part”元素
PART = re.compile("part name='[0-9]+'")
# 加载原始XML文件的内容并打开输出文件以进行写入
print("[INFO] parsing data split XML file...")
rows = open(args["input"]).read().strip().split("\n")
output = open(args["output"], "w")
"""
在循环内部,我们执行以下任务:
通过正则表达式匹配确定当前行是否包含part元素。
如果它不包含part元素,则将该行写回到文件中。
如果它确实包含part元素,则需要进一步解析它。
在这里,我们从文件中提取名称属性。
然后检查名称是否存在于我们要训练形状预测器进行本地化的地标中。
如果是这样,我们会将行写回到磁盘上(否则我们将忽略特定名称,因为它不是我们要本地化的地标)。
通过关闭输出XML文件来结束脚本。
"""
# 循环遍历数据拆分文件的行
for row in rows:
#检查当前行是否具有我们感兴趣的面部标志的(x,y)坐标
parts = re.findall(PART, row)
#如果没有与面部标志的(x,y)坐标有关的信息,我们可以将当前行写到磁盘上,而无需进行进一步修改
if len(parts) == 0:
output.write("{}\n".format(row))
#确实包含part元素,则需要进一步解析它。
else:
# 从行中解析出属性名称
attr = "name='"
i = row.find(attr)
# print("i",i) # 比如 12,该值为索引值,也即为 字符串"name='"的头字符"n" 在这一行句子中的索引值
j = row.find("'", i + len(attr) + 1) #i + len(attr) + 1:比如 12+6+1=19,那么从索引第19个字符开始查找"字符串'"
# print("j",j) # 比如 20,该值为索引值,也即为 字符串"'" 在这一行句子中的索引值
name = int(row[i + len(attr):j]) ## row[i + len(attr):j]:比如 row[12 + 6:20],取出 name='00',即为 0
# print("name",name) # 比如 0,取出 name='00',即为 0
# # 如果面部界标名称存在于我们的索引范围内,请将其写入我们的输出文件
if name in LANDMARKS:
output.write("{}\n".format(row))
# 关闭输出文件
output.close()
接受解析的XML文件以使用dlib训练我们的形状预测器
# 命令行参数:python train_shape_predictor.py --training ibug_300W_large_face_landmark_dataset/labels_ibug_300W_train_eyes.xml --model eye_predictor.dat
import multiprocessing
import argparse
import dlib
# 构造参数解析器并解析参数
ap = argparse.ArgumentParser()
ap.add_argument("-t", "--training", required=False, default="./ibug_300W_large_face_landmark_dataset/labels_ibug_300W_train_eyes.xml", help="path to input training XML file")
#路径序列化dlib形状预测器模型:eye_predictor.dat
ap.add_argument("-m", "--model", required=False, default="./eye_predictor.dat", help="path serialized dlib shape predictor model")
args = vars(ap.parse_args())
# 掌握dlib形状预测器的默认选项
print("[INFO] 设置形状预测器选项...")
options = dlib.shape_predictor_training_options()
"""
需要在训练形状预测器之前设置选项(即超参数):
1.tree_depth:
每棵回归树的深度,默认值为4,论文中为5,建议值范围为[2, 8]。
每棵回归树中总共有2^tree_depth的叶子数,即最底部的那一层中所有节点才为叶子,那么该整棵树的叶子数便为2^tree_depth。
2.nu:
建议值范围为 float(0, 1],默认为0.1。
接近1的值将使我们的模型更接近训练数据,但可能导致过度拟合。
接近0的值将有助于我们的模型推广;但是需要注意泛化能力的话,nu越接近0 那么则需要的训练数据就越多。
也就是值越大也更加fit训练样本数据,但也越容易可能过拟合。
3.cascade_depth:
回归树的级数,也即级联的级数。默认值为10,建议值范围为 [6, 18]。对accuracy和模型大小都有影响。
拥有的级联越多,模型可能就越准确,但是输出大小也越大。
4.num_trees_per_cascade_level
每层级联中所包含的树数目,默认值为500,即级联中的每一层级都默认包含500颗树。
5.oversampling_amount
对训练样本进行随机变形拓增样本的数量。默认值为20,建议值范围为[0,50]。
6.feature_pool_size
每层级联中从图片中随机取样的像素个数,这些像素作为训练回归树的特征池。
默认值为400,论文中设置了最大到2000。
为了降低复杂度才会设置这个值,值越大应该越精确,但是也会更耗时。
7.num_test_splits
生成回归树时,每个节点随机生成的分裂个数。
"""
#定义每棵回归树的深度:
# 每棵树中总共有2^tree_depth叶;
# 较小的tree_depth值将更快但精度较低,而较大的值将生成更深,更准确的树,但在进行预测时运行得慢得多
options.tree_depth = 4
#范围为[0,1]的正则化参数,用于帮助我们的模型泛化
# 值接近1将使我们的模型更适合训练数据,但可能导致过度拟合;
# 接近0的值将有助于我们的模型泛化,但将要求我们拥有约1000个数据点的训练数据(即需要更多的训练数据才能真正实现模型泛化)
options.nu = 0.1
#用于训练形状预测器的级联数量:
# 该参数对模型的准确性和输出大小均产生重大影响;
# 您拥有的级联越多,模型可能就越准确,但是输出大小也越大
options.cascade_depth = 15
#每个级联中用于为随机树生成特征的像素数
# 较大的像素值将使形状预测器更准确,但速度较慢;
# 如果速度不是问题,则使用较大的值;
# 否则,对于资源受限/嵌入式设备,使用较小的值
options.feature_pool_size = 400
#训练时会在每个级联中选择最佳功能
# 该值越大,训练所需的时间就越长,但是(可能)您的模型越准确
options.num_test_splits = 50
#在训练形状预测器时控制“抖动”(即数据增强)的数量
# 应用所提供的随机变形数量,从而执行正则化并提高我们模型进行泛化的能力
options.oversampling_amount = 5
# 应用的转换抖动量-dlib docs建议的值范围为[0,0.5]
options.oversampling_translation_jitter = 0.1
# 告诉dlib形状预测变量详细,并打印出我们模型训练的状态消息
options.be_verbose = True
# 训练时要使用的线程/ CPU核心数
# 我们将此值默认为系统上可用核心的数量,但是如果您愿意,可以在此处提供一个整数值
options.num_threads = multiprocessing.cpu_count()
# 将我们的训练选项记录到终端
print("[INFO] 形状预测器选项:")
#shape_predictor_training_options(be_verbose=1, cascade_depth=15, tree_depth=4, num_trees_per_cascade_level=500,
# nu=0.1, oversampling_amount=5, oversampling_translation_jitter=0.1, feature_pool_size=400, lambda_param=0.1,
# num_test_splits=50, feature_pool_region_padding=0, random_seed=, num_threads=8, landmark_relative_padding_mode=1)
print(options)
# 训练形状预测器
print("[INFO] 训练形状预测器...")
dlib.train_shape_predictor(args["training"], args["model"], options)
计算自定义形状预测器的平均误差(MAE)
# 命令行参数:
# python evaluate_shape_predictor.py --predictor eye_predictor.dat --xml ibug_300W_large_face_landmark_dataset/labels_ibug_300W_train_eyes.xml
# python evaluate_shape_predictor.py --predictor eye_predictor.dat --xml ibug_300W_large_face_landmark_dataset/labels_ibug_300W_test_eyes.xml
import argparse
import dlib
# 构造参数解析器并解析参数
ap = argparse.ArgumentParser()
#路径序列化dlib形状预测器模型:eye_predictor.dat
ap.add_argument("-p", "--predictor", required=False, default="./eye_predictor.dat", help="训练好的dlib形状预测器模型的路径")
# ap.add_argument("-x", "--xml", required=False, default="./ibug_300W_large_face_landmark_dataset/labels_ibug_300W_test_eyes.xml",help="输入 training/testing XML文件的路径")
ap.add_argument("-x", "--xml", required=False, default="./ibug_300W_large_face_landmark_dataset/labels_ibug_300W_train_eyes.xml",help="输入 training/testing XML文件的路径")
args = vars(ap.parse_args())
# 计算所提供数据分割的误差并将其显示在屏幕上
print("[INFO] 评估形状预测器 evaluating shape predictor...")
error = dlib.test_shape_predictor(args["xml"], args["predictor"])
#测试集(labels_ibug_300W_test_eyes.xml)的error: 7.402431623435015
#训练集(labels_ibug_300W_train_eyes.xml):error: 3.659473224958382
print("[INFO] error: {}".format(error))
使用我们的自定义dlib形状预测器执行形状预测,该预测器经过训练只能识别眼睛界标
# 命令行参数:python predict_eyes.py --shape-predictor eye_predictor.dat
from imutils.video import VideoStream
from imutils import face_utils
import argparse
import imutils
import time
import dlib
import cv2
# 构造参数解析器并解析参数
ap = argparse.ArgumentParser()
#路径序列化dlib形状预测器模型:eye_predictor.dat
ap.add_argument("-p", "--shape-predictor", required=False, default="./eye_predictor.dat", help="path to facial landmark predictor")
args = vars(ap.parse_args())
# 初始化dlib的面部检测器(基于HOG),然后加载我们训练有素的形状预测器
print("[INFO] 加载面部界标预测器 loading facial landmark predictor...")
#获取正面人脸检测器
detector = dlib.get_frontal_face_detector()
#眼睛形状预测器:eye_predictor.dat
predictor = dlib.shape_predictor(args["shape_predictor"])
# 初始化视频流,并允许摄像头传感器预热
print("[INFO] 相机传感器预热...")
vs = VideoStream(src=0).start()
time.sleep(2.0)
# 循环播放视频流中的帧
while True:
# 从视频流中抓取帧然后
frame = vs.read()
# 将其调整为最大宽度为400像素
frame = imutils.resize(frame, width=400)
# 将其转换为灰度
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 在灰度图中检测人脸
rects = detector(gray, 0)
# 循环遍历所检测出来的人脸坐标
for rect in rects:
# 将dlib矩形转换为OpenCV边界框的位置坐标
(x, y, w, h) = face_utils.rect_to_bb(rect)
# 在脸部周围绘制边界框:cv2.rectangle(frame, (startX, startY), (endX, endY), color, 2)
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
#使用我们的自定义dlib形状预测器(眼睛形状预测器eye_predictor.dat)预测眼睛的位置坐标信息(一共为左右眼加起来的12个[x,y]特征点的坐标信息)
shape = predictor(gray, rect)
# print("shape1:",shape) # 比如 <dlib.full_object_detection object at 0x000002355B32F030>
#将预测的眼睛的位置坐标信息(一共为左右眼加起来的12个[x,y]特征点的坐标信息) 转换为易于解析的NumPy数组
shape = face_utils.shape_to_np(shape)
print("shape2:",shape) #比如 [[181 179] 。。。 [214 183]] 一共为左右眼加起来的12个[x,y]特征点的坐标信息
# 把形状预测器模型(眼睛形状预测器eye_predictor.dat)预测的眼睛的位置坐标信息中的(x,y)坐标 一个个循环地将它们绘制在图像上
for (sX, sY) in shape:
# 比如遍历“[[181 179] 。。。 [214 183]]”列表中的每个 小列表(眼睛每个特征点的位置坐标信息中的(x,y)坐标 )
# print("(sX, sY)",(sX, sY))
cv2.circle(frame, (sX, sY), 1, (0, 0, 255), -1)
# 显示图像
cv2.imshow("Frame", frame)
key = cv2.waitKey(1) & 0xFF
#如果按下“ q”键,则退出循环
if key == ord("q"):
break
# 做一些清理
cv2.destroyAllWindows()
vs.stop()