基于 Tensorflow 2.x 从零训练 15 点人脸关键点检测模型

Posted 小毕超

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 Tensorflow 2.x 从零训练 15 点人脸关键点检测模型相关的知识,希望对你有一定的参考价值。

一、人脸关键点检测数据集

在计算机视觉人脸计算领域,人脸关键点检测是一个十分重要的区域,可以实现例如一些人脸矫正、表情分析、姿态分析、人脸识别、人脸美颜等方向。

人脸关键点数据集通常有 5点、15点、68点、96点、98点、106点、186点 等,例如通用 Dlib 中的 68 点检测,它将人脸关键点分为脸部关键点和轮廓关键点,脸部关键点包含眉毛、眼睛、鼻子、嘴巴共计51个关键点,轮廓关键点包含17个关键点。

本文基于 kaggleFacial Keypoints Detection 中的数据集进行实践,该数据集包含包括7,049幅训练图像,图像是 96 x 96像素的灰度图像,其中关键点有 15个点,注意数据集有的字段缺失,如果去除字段缺失的数据,实际训练数据只有 2,140 幅训练图像,还包括1,783张测试图片,数据集的效果如下所示:


可以看出,关键点包括眉毛的两端、眼睛的中心和两端、鼻子尖、嘴巴两端和上下嘴唇的中间。

下载数据集

数据集在 kaggle 的官方网址上:

https://www.kaggle.com/c/facial-keypoints-detection

下载前需要进行登录,如果没有 kaggle 账号可以注册一个。


下载解压后,可以看到 training.ziptest.zip 两个文件,分别对应训练集和测试集,解压后数据是以 CSV 的格式进行存放的:


其中 training.csv 中的字段分别表示:

序号字段含义
0left_eye_center_x左眼中心 x 点
1left_eye_center_y左眼中心 y 点
2right_eye_center_x右眼中心 x 点
3right_eye_center_y右眼中心 y 点
4left_eye_inner_corner_x左眼内端 x 点
5left_eye_inner_corner_y左眼内端 y 点
6left_eye_outer_corner_x左眼外端 x 点
7left_eye_outer_corner_y左眼外端 y 点
8right_eye_inner_corner_x右眼内端 x 点
9right_eye_inner_corner_y右眼内端 y 点
10right_eye_outer_corner_x右眼外端 x 点
11right_eye_outer_corner_y右眼外端 y 点
12left_eyebrow_inner_end_x左眉毛内端 x 点
13left_eyebrow_inner_end_y左眉毛内端 y 点
14left_eyebrow_outer_end_x左眉毛外端 x 点
15left_eyebrow_outer_end_y左眉毛外端 y 点
16right_eyebrow_inner_end_x右眉毛内端 x 点
17right_eyebrow_inner_end_y右眉毛内端 y 点
18right_eyebrow_outer_end_x右眉毛外端 x 点
19right_eyebrow_outer_end_y右眉毛外端 y 点
20nose_tip_x鼻尖中心 x 点
21nose_tip_y鼻尖中心 y 点
22mouth_left_corner_x嘴巴左端 x 点
23mouth_left_corner_y嘴巴左端 y 点
24mouth_right_corner_x嘴巴右端 x 点
25mouth_right_corner_y嘴巴右端 y 点
26mouth_center_top_lip_x上嘴唇中心 x 点
27mouth_center_top_lip_y上嘴唇中心 y 点
28mouth_center_bottom_lip_x下嘴唇中心 x 点
29mouth_center_bottom_lip_y下嘴唇中心 y 点
30Image图形像素

由于数据是存放在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 数据集打乱

在喂入数据前还需要对数据进行打乱,使每次都随机数据喂入模型,在 Tensorflowtf.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 从零训练花卉图像识别模型

基于 Tensorflow 2.x 使用 MobileNetV2 微调模型优化训练花卉图像识别模型

基于 Tensorflow 2.x 使用 MobileNetV2 微调模型优化训练花卉图像识别模型