基于tensorflow2.x从零训练15点人脸关键点检测模型(代码片段)

小毕超 小毕超     2023-01-09     245

关键词:

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

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

人脸关键点数据集通常有 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).基于tensorflow2.x从零训练15点人脸关键点检测模型(代码片段)

一、人脸关键点检测数据集在计算机视觉人脸计算领域,人脸关键点检测是一个十分重要的区域,可以实现例如一些人脸矫正、表情分析、姿态分析、人脸识别、人脸美颜等方向。人脸关键点数据集通常有5点、15点、68点... 查看详情

基于tensorflow2.x从零训练花卉图像识别模型(代码片段)

一、数据集准备本篇文章使用数千张花卉照片作为数据集,共分为5个分类:雏菊(daisy)、蒲公英(dandelion)、玫瑰(roses)、向日葵(sunflowers)、郁金香(tulips),数据集下载... 查看详情

基于tensorflow2.x从零训练花卉图像识别模型(代码片段)

一、数据集准备本篇文章使用数千张花卉照片作为数据集,共分为5个分类:雏菊(daisy)、蒲公英(dandelion)、玫瑰(roses)、向日葵(sunflowers)、郁金香(tulips),数据集下载... 查看详情

基于tensorflow2.x从零训练花卉图像识别模型(代码片段)

一、数据集准备本篇文章使用数千张花卉照片作为数据集,共分为5个分类:雏菊(daisy)、蒲公英(dandelion)、玫瑰(roses)、向日葵(sunflowers)、郁金香(tulips),数据集下载... 查看详情

强化学习基于tensorflow2.x的ppo2(离散动作情况)训练cartpole-v1(代码片段)

算法流程代码importmatplotlib.pyplotaspltimporttensorflowastfimportnumpyasnpimportgymimportcopydefbuild_actor_network(state_dim,action_dim):model=tf.keras.Sequential([tf.keras.layers.Dense(units=128, 查看详情

基于tensorflow2.x低阶api搭建神经网络模型并训练及解决梯度爆炸与消失方法实践(代码片段)

1.低阶API神经网络模型1.1.关于tf.Module关于Tensorflow2.x,最令我觉得有意思的功能就是tf.function和AutoGraph了.他们可以把Python风格的代码转为效率更好的Tensorflow计算图。TensorFlow2.0主要使用的是动态计算图和Autograph。Autograph机... 查看详情

基于tensorflow2.x低阶api搭建神经网络模型并训练及解决梯度爆炸与消失方法实践(代码片段)

1.低阶API神经网络模型1.1.关于tf.Module关于Tensorflow2.x,最令我觉得有意思的功能就是tf.function和AutoGraph了.他们可以把Python风格的代码转为效率更好的Tensorflow计算图。TensorFlow2.0主要使用的是动态计算图和Autograph。Autograph机... 查看详情

基于tensorflow2.x低阶api搭建神经网络模型并训练及解决梯度爆炸与消失方法实践(代码片段)

1.低阶API神经网络模型1.1.关于tf.Module关于Tensorflow2.x,最令我觉得有意思的功能就是tf.function和AutoGraph了.他们可以把Python风格的代码转为效率更好的Tensorflow计算图。TensorFlow2.0主要使用的是动态计算图和Autograph。Autograph机... 查看详情

基于tensorflow2.x使用mobilenetv2微调模型优化训练花卉图像识别模型(代码片段)

一、模型微调上篇文章通过搭建三层卷积模型,训练了花卉图像识别模型,最后经验证集验证后准确率大约为75%,本篇文章继续对该数据集进行处理优化,以提高识别的准确度。在本篇文章中不再过多的介绍数据... 查看详情

基于tensorflow2.x使用mobilenetv2微调模型优化训练花卉图像识别模型(代码片段)

一、模型微调上篇文章通过搭建三层卷积模型,训练了花卉图像识别模型,最后经验证集验证后准确率大约为75%,本篇文章继续对该数据集进行处理优化,以提高识别的准确度。在本篇文章中不再过多的介绍数据... 查看详情

人脸识别系列之人脸检测--训练基于肤色特征的检测

前言:基于特征的方法是利用人脸的先验知识导出的规则进行人脸检测。一般来说,常用的特征包括人脸和人脸器官典型的边缘和形状特征(如人脸轮廓、虹膜轮廓、嘴唇轮廓等)、纹理特征(纹理是在图上表现为灰度或颜色分... 查看详情

人脸识别系列之人脸检测--训练基于肤色特征的检测

前言:基于特征的方法是利用人脸的先验知识导出的规则进行人脸检测。一般来说,常用的特征包括人脸和人脸器官典型的边缘和形状特征(如人脸轮廓、虹膜轮廓、嘴唇轮廓等)、纹理特征(纹理是在图上表现为灰度或颜色分... 查看详情

如何从零开始构建人脸识别系统?

】如何从零开始构建人脸识别系统?【英文标题】:HowtobuildaFacerecognitionsystemfromscratch?【发布时间】:2019-08-1222:22:34【问题描述】:我正在为人脸识别系统构建一个原型,在编写算法时,我有几个问题。算法:收集一对(A(i),P(i),N(... 查看详情

基于tensorflow2.x使用mobilenetv2微调模型优化训练花卉图像识别模型(代码片段)

一、模型微调上篇文章通过搭建三层卷积模型,训练了花卉图像识别模型,最后经验证集验证后准确率大约为75%,本篇文章继续对该数据集进行处理优化,以提高识别的准确度。在本篇文章中不再过多的介绍数据... 查看详情

基于opencv的人脸采集训练及识别应用

摘要  本文主要基于Python展现人脸识别的应用过程和方法,从人脸采集,人脸模型训练以及人脸识别应用讨论这三个过程简单是如何共同构成我们日常中人脸识别技术应用的,提高我们队人脸识别技术及应用的认识。通过采集... 查看详情

基于tensorflow2.x实现多层卷积神经网络,实践mnist手写数字识别(代码片段)

一、MNIST数据集上篇文章中使用了Tensorflow2.x搭建了对层的BP神经网络,经过训练后发现准确率只有96.8%对于单环境的图片识别场景来说,还是有点偏低,本文使用多层的卷积代替BP网络中的隐藏层对模型进行优化。下面... 查看详情

基于tensorflow2.x实现多层卷积神经网络,实践mnist手写数字识别(代码片段)

一、MNIST数据集上篇文章中使用了Tensorflow2.x搭建了对层的BP神经网络,经过训练后发现准确率只有96.8%对于单环境的图片识别场景来说,还是有点偏低,本文使用多层的卷积代替BP网络中的隐藏层对模型进行优化。下面... 查看详情

基于tensorflow2.x实现多层卷积神经网络,实践fashionmnist服装图像识别(代码片段)

一、FashionMNIST服装数据集FashionMNIST数据集,该数据集包含10个类别的70000个灰度图像。大小统一是28x28的长宽,其中60000张作为训练数据,10000张作为测试数据,该数据集已被封装在了tf.keras.datasets工具包下,数... 查看详情