基于 Tensorflow 2.x 从零训练 15 点人脸关键点检测模型
Posted 小毕超
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 Tensorflow 2.x 从零训练 15 点人脸关键点检测模型相关的知识,希望对你有一定的参考价值。
一、人脸关键点检测数据集
在计算机视觉人脸计算领域,人脸关键点检测是一个十分重要的区域,可以实现例如一些人脸矫正、表情分析、姿态分析、人脸识别、人脸美颜等方向。
人脸关键点数据集通常有 5点、15点、68点、96点、98点、106点、186点
等,例如通用 Dlib
中的 68 点检测,它将人脸关键点分为脸部关键点和轮廓关键点,脸部关键点包含眉毛、眼睛、鼻子、嘴巴共计51
个关键点,轮廓关键点包含17
个关键点。
本文基于 kaggle
的 Facial Keypoints Detection
中的数据集进行实践,该数据集包含包括7,049
幅训练图像,图像是 96 x 96
像素的灰度图像,其中关键点有 15
个点,注意数据集有的字段缺失,如果去除字段缺失的数据,实际训练数据只有 2,140
幅训练图像,还包括1,783
张测试图片,数据集的效果如下所示:
可以看出,关键点包括眉毛的两端、眼睛的中心和两端、鼻子尖、嘴巴两端和上下嘴唇的中间。
下载数据集
数据集在 kaggle
的官方网址上:
下载前需要进行登录,如果没有 kaggle
账号可以注册一个。
下载解压后,可以看到 training.zip
和 test.zip
两个文件,分别对应训练集和测试集,解压后数据是以 CSV
的格式进行存放的:
其中 training.csv
中的字段分别表示:
序号 | 字段 | 含义 |
---|---|---|
0 | left_eye_center_x | 左眼中心 x 点 |
1 | left_eye_center_y | 左眼中心 y 点 |
2 | right_eye_center_x | 右眼中心 x 点 |
3 | right_eye_center_y | 右眼中心 y 点 |
4 | left_eye_inner_corner_x | 左眼内端 x 点 |
5 | left_eye_inner_corner_y | 左眼内端 y 点 |
6 | left_eye_outer_corner_x | 左眼外端 x 点 |
7 | left_eye_outer_corner_y | 左眼外端 y 点 |
8 | right_eye_inner_corner_x | 右眼内端 x 点 |
9 | right_eye_inner_corner_y | 右眼内端 y 点 |
10 | right_eye_outer_corner_x | 右眼外端 x 点 |
11 | right_eye_outer_corner_y | 右眼外端 y 点 |
12 | left_eyebrow_inner_end_x | 左眉毛内端 x 点 |
13 | left_eyebrow_inner_end_y | 左眉毛内端 y 点 |
14 | left_eyebrow_outer_end_x | 左眉毛外端 x 点 |
15 | left_eyebrow_outer_end_y | 左眉毛外端 y 点 |
16 | right_eyebrow_inner_end_x | 右眉毛内端 x 点 |
17 | right_eyebrow_inner_end_y | 右眉毛内端 y 点 |
18 | right_eyebrow_outer_end_x | 右眉毛外端 x 点 |
19 | right_eyebrow_outer_end_y | 右眉毛外端 y 点 |
20 | nose_tip_x | 鼻尖中心 x 点 |
21 | nose_tip_y | 鼻尖中心 y 点 |
22 | mouth_left_corner_x | 嘴巴左端 x 点 |
23 | mouth_left_corner_y | 嘴巴左端 y 点 |
24 | mouth_right_corner_x | 嘴巴右端 x 点 |
25 | mouth_right_corner_y | 嘴巴右端 y 点 |
26 | mouth_center_top_lip_x | 上嘴唇中心 x 点 |
27 | mouth_center_top_lip_y | 上嘴唇中心 y 点 |
28 | mouth_center_bottom_lip_x | 下嘴唇中心 x 点 |
29 | mouth_center_bottom_lip_y | 下嘴唇中心 y 点 |
30 | Image | 图形像素 |
由于数据是存放在CSV
中,可以借助 pandas
工具对数据进行解析,如果没有安装 pandas
工具,可以通过下面指令安装:
pip3 install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple
下面程序通过 pandas
解析 CSV
文件,并将图片转为 numpy
数组,通过 matplotlib
可视化工具查看,其中具体的解释都写在了注释中:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
def main():
csv_path = './data/training.csv'
# 读取 CSV 文件
train_df = pd.read_csv(csv_path)
# 查看数据框,并列出数据集的头部。
train_df.info()
# 丢弃有缺失数据的样本
train_df = train_df.dropna()
# 获取图片信息,并转为 numpy 结构
x_train = train_df['Image'].apply(lambda img: np.fromstring(img, sep=' '))
x_train = np.vstack(x_train)
# 重新修改形状
x_train = x_train.reshape((-1, 96, 96, 1))
# 去除最后一列的 Image
cols = train_df.columns[:-1]
y_train = train_df[cols].values
print('训练集 shape: ', x_train.shape)
print('训练集label shape: ', y_train.shape)
plt.figure(figsize=(10, 10))
for p in range(2):
data = x_train[(p * 9):(p * 9 + 9)]
label = y_train[(p * 9):(p * 9 + 9)]
plt.clf()
for i in range(9):
plt.subplot(3, 3, i + 1)
img = data[i].reshape(96, 96, 1)
plt.imshow(img, cmap='gray')
# 画关键点
l = label[i]
# 从 1 开始,每次走 2 步,j-1,j 就是当前点的坐标
for j in range(1, 31, 2):
plt.plot(l[j - 1], l[j], 'ro', markersize=4)
plt.show()
if __name__ == '__main__':
main()
运行之后,可以看到如下效果图:
下面我们基于该数据集进行建模,训练一个自己的关键点检测模型。
二、构建模型
1. 数据集预处理
1.1 灰度图像转3维度图像
由于自己构建模型,初始值比较随机,loss
收敛起来没那么快,又因为该数据集的训练数据比较少,因此模型这里,我们基于 MobileNetV2
作为基础模型,并使用 ImageNet
上的权重作为初始值,可以让 loss
快速收敛,但 MobileNetV2
是基于 RGB
彩色三通道进行构建的,因此在训练时,需要将灰度图像转为 3
维形式,这里可以借助 PIL
工具的 Image.convert('RGB')
进行实现,转换程序如下所示,这里我分出了 80%
的训练数据,20%
的验证数据
import numpy as np
import pandas as pd
from PIL import Image
def toRgbImg(img):
img = np.fromstring(img, sep=' ').astype(np.uint8).reshape(96, 96)
img = Image.fromarray(img).convert('RGB')
return img
def main():
csv_path = './data/training.csv'
# 读取 CSV 文件
train_df = pd.read_csv(csv_path)
# 查看数据框,并列出数据集的头部。
train_df.info()
# 丢弃有缺失数据的样本
train_df = train_df.dropna()
# 获取图片信息,并转为 numpy 结构
# x_train = train_df['Image'].apply(lambda img: np.fromstring(img, sep=' '))
x_train = train_df['Image'].apply(toRgbImg)
x_train = np.vstack(x_train)
# 重新修改形状
x_train = x_train.reshape((-1, 96, 96, 3))
# 去除最后一列的 Image
cols = train_df.columns[:-1]
y_train = train_df[cols].values
# 使用 80% 的数据训练,20% 的数据进行验证
size = int(x_train.shape[0] * 0.8)
print(size)
x_val = x_train[size:]
y_val = y_train[size:]
x_train = x_train[:size]
y_train = y_train[:size]
print("数据集信息")
print(x_train.shape)
print(y_train.shape)
print(x_val.shape)
print(y_val.shape)
if __name__ == '__main__':
main()
运行后可以看到如下日志:
1.2 y_train 标签转为比例点
针对于 y_train
标签 ,是表示着图像上真实关键点的坐标,直接让模型回归真实点的话,会出现浮动大的情况,因此这里将真实点转为相对于图像的比例点,也就是直接除于图像的大小,得到一个相对点的位置,相对点的位置都在 0 - 1
区间:
# 去除最后一列的 Image, 将y值缩放到[0,1]区间
cols = train_df.columns[:-1]
y_train = train_df[cols].values / 96.0
1.3 数据集打乱
在喂入数据前还需要对数据进行打乱,使每次都随机数据喂入模型,在 Tensorflow
中 tf.data
都已经帮我们完成了这些逻辑:
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
val_dataset = tf.data.Dataset.from_tensor_slices((x_val, y_val))
随机打乱,并根据 batch
分批:
SHUFFLE_BUFFER_SIZE = 100
train_dataset = train_dataset.shuffle(SHUFFLE_BUFFER_SIZE).batch(batch_size=batch_size)
val_dataset = val_dataset.shuffle(SHUFFLE_BUFFER_SIZE).batch(batch_size=batch_size)
最后,完整的数据处理方法如下:
def toRgbImg(img):
img = np.fromstring(img, sep=' ').astype(np.uint8).reshape(96, 96)
img = Image.fromarray(img).convert('RGB')
return img
# 处理数据集
def getData(csv_path, img_width, img_height, dim, batch_size):
train_df = pd.read_csv(csv_path)
# 丢弃有缺失数据的样本
train_df = train_df.dropna()
# 获取图片信息,并转为 numpy 结构
x_train = train_df['Image'].apply(toRgbImg)
x_train = np.vstack(x_train)
# 重新修改形状
x_train = x_train.reshape((-1, img_width, img_height, dim))
# 去除最后一列的 Image, 将y值缩放到[0,1]区间
cols = train_df.columns[:-1]
y_train = train_df[cols].values / 96.0
# 使用 80% 的数据训练,20% 的数据进行验证
size = int(x_train.shape[0] * 0.8)
x_val = x_train[size:]
y_val = y_train[size:]
x_train = x_train[:size]
y_train = y_train[:size]
# 加载为数据集
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
val_dataset = tf.data.Dataset.from_tensor_slices((x_val, y_val))
SHUFFLE_BUFFER_SIZE = 100
train_dataset = train_dataset.shuffle(SHUFFLE_BUFFER_SIZE).batch(batch_size=batch_size)
val_dataset = val_dataset.shuffle(SHUFFLE_BUFFER_SIZE).batch(batch_size=batch_size)
return train_dataset, val_dataset
1.4 数据增强
上面提到实际训练的数据集并不多,因此有必要通过数据增强扩充数据,数据增强这里就做一个随机对比度的改变,这样不会影响关键点的位置,增强部分放在模型的Sequential
中,这样在训练时有增强的效果,在预测或评估时增强又会被禁用:
# 数据增强
data_augmentation = tf.keras.Sequential([
# 随机对比度改变
keras.layers.RandomContrast(0.3)
])
2. 构建模型
上面提到我们基于 MobileNetV2
作为基础模型,并进行迁移学习,这里需要去除 MobileNetV2
的分类器层,最后添加我们自己的全链接层,最后给到一个 30
的输出,其输出的顺序就表示训练数据集中去除Image
列后剩下的一一对应,就是这 15
个关键点的 x,y
比例数据, loss 函数,使用均方差来计算,整体模型结构如下:
下面使用 keras
构建模型结构:
import tensorflow as tf
import tensorflow.keras as keras
# 定义模型类
class Model():
# 初始化结构
def __init__(self, checkpoint_path, log_path, model_path, img_width, img_height, dim):
# checkpoint 权重保存地址
self.checkpoint_path = checkpoint_path
# 训练日志保存地址
self.log_path = log_path
# 训练模型保存地址:
self.model_path = model_path
# 图片大小信息
self.img_width = img_width
self.img_height = img_height
self.dim = dim
# 数据统一大小并归一处理
resize_and_rescale = tf.keras.Sequential([
keras.layers.Resizing(img_width, img_height),
keras.layers.Rescaling(1. / 255)
])
# 数据增强
data_augmentation = tf.keras.Sequential([
# 对比度
keras.layers.RandomContrast(0.3)
])
# MobileNetV2 模型结构
mobienet = keras.applications.MobileNetV2(weights='imagenet', include_top=False, input_shape=(img_width, img_height, dim))
# 初始化模型结构
self.model = keras.Sequential([
resize_and_rescale,
data_augmentation,
mobienet,
keras.layers.Flatten(),
keras.layers.Dense(1024,
kernel_initializer=keras.initializers.truncated_normal(stddev=0.05),
kernel_regularizer=keras.regularizers.l2(0.001),
activation='relu'),
keras.layers.Dropout(0.1),
keras.layers.Dense(256,
kernel_initializer=keras.initializers.truncated_normal(stddev=0.05),
kernel_regularizer=keras.regularizers.l2(0.001),
activation='relu'),
keras.layers.Dense(30)
])
# 编译模型
def compile(self):
# 输出模型摘要
self.model.build(input_shape=(None, self.img_width, self.img_height, self.dim))
self.model.summary()
# 定义训练模式, loss 使用均方差
self.model.compile(optimizer='adam',
loss='mean_squared_error',
metrics=['mae'])
# 训练模型
def train(self, train_ds, val_ds, epochs):
# tensorboard 训练日志收集
tensorboard = keras.callbacks.TensorBoard(log_dir=self.log_path)
# 训练过程保存 Checkpoint 权重
model_checkpoint = keras.callbacks.ModelCheckpoint(self.checkpoint_path, monitor='val_loss', verbose=0,
save_best_only=True, save_weights_only=True, mode='auto',
period=3)
# 填充数据,迭代训练
self.model.fit(
train_ds, # 训练集
validation_data=val_ds, # 验证集
epochs=epochs, # 迭代周期
verbose=2, # 训练过程的日志信息显示,一个epoch输出一行记录
callbacks=[tensorboard, model_checkpoint]
)
# 保存训练模型
self.model.save(self.model_path)
def evaluate(self, val_ds):
# 评估模型
test_loss, test_acc = self.model.evaluate(val_ds)
return test_loss, test_acc
处理数据集,使用 80%
的图像进行训练,20%
的图像进行验证,并将将数据喂入模型训练:
import numpy as np
import pandas as pd
from PIL import Image
import tensorflow as tf
def toRgbImg(img):
img = np.fromstring(img, sep=' ').astype(np.uint8).reshape(96, 96)
img = Image.fromarray(img).convert('RGB')
return img
# 处理数据集
def getData(csv_path, img_width, img_height, dim, batch_size):
train_df = pd.read_csv(csv_path)
# 丢弃有缺失数据的样本
train_df = train_df.dropna()
# 获取图片信息,并转为 numpy 结构
x_train = train_df['Image'].apply(toRgbImg)
x_train = np.vstack(x_train)
# 重新修改形状
x_train = x_train.reshape((-1, img_width, img_height, dim))
# 去除最后一列的 Image, 将y值缩放到[0,1]区间
cols = train_df.columns[:-1]
y_train = train_df[cols].values / 96.0
# 使用 80% 的数据训练,20% 的数据进行验证
size = int(x_train.shape[0] * 0.8)
x_val = x_train[size:]
y_val = y_train[size:]
x_train = x_train[:size]
y_train = y_train[:size]
# 加载为数据集
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
val_dataset = tf.data.Dataset.from_tensor_slices((x_val, y_val))
SHUFFLE_BUFFER_SIZE = 100
train_dataset = train_dataset.shuffle(SHUFFLE_BUFFER_SIZE).batch(batch_size=batch_size)
val_dataset = val_dataset.shuffle(SHUFFLE_BUFFER_SIZE).batch(batch_size=batch_size)
return train_dataset, val_dataset
def main():
train_csv_path = './data/training.csv'
img_width = 96
img_height = 96
dim = 3
batch_size = 20
epochs = 100
checkpoint_path = './checkout/'
log_path = './log'
model_path = './model/model_mobie.h5'
# 加载数据集
train_ds, val_ds = getData(train_csv_path, img_width, img_height, dim, batch_size)
# 构建模型
model = Model(checkpoint_path, log_path, model_path, img_width, img_height, dim)
# 编译模型
model.compile()
# 训练模型
model.train(train_ds, val_ds, epochs)
# 评估模型
test_loss, test_acc = model.evaluate(val_ds)
print(test_loss, test_acc)
if __name__ == '__main__':
main()
运行后可以看到打印的网络结构:
从训练日志中,可以看到 loss
一直在减小:
训练结束后可以看到在评估集上的评估结果,最终 loss
降到了 0.00175
结合 tensorboard
中可视化的损失看下迭代的曲线:
tensorboard --logdir=log/train
三、模型预测
训练后会在 model
下生成 model.h5
模型,下面可以直接加载该模型进行预测,这里换成对数据集的 test.csv
进行解析,同样将灰度图转为 3
维后输入到模型:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow import keras
from PIL import Image
def toRgbImg(img):
img = np.fromstring(img, sep=' ').astype(np.uint8).reshape(96, 96)
img = Image.fromarray(img).以上是关于基于 Tensorflow 2.x 从零训练 15 点人脸关键点检测模型的主要内容,如果未能解决你的问题,请参考以下文章
基于 Tensorflow 2.x 从零训练 15 点人脸关键点检测模型
基于 Tensorflow 2.x 从零训练花卉图像识别模型
基于 Tensorflow 2.x 从零训练花卉图像识别模型
基于 Tensorflow 2.x 从零训练花卉图像识别模型