Hopenet人脸姿态估计

Fine-Grained Head Pose Estimation Without Keypoints-论文解读

代码
论文

  1. Abstract
    传统的头部姿态计算方法是通过人脸上关键点求解二维到三维对应问题,因为对landmark 检测性能的依赖而导致存在不足。论文中提出一种基于多损失卷积神经网络的姿态估计方法。该方法通过联合分块姿态分类和回归,直接根据图像强度预测固有欧拉角(yaw,pitch,roll)。
  2. Introduction
    过去主要是基于discriminative/landmark和参数化外观模型parameterized appearance models方法进行人脸建模。目前,利用深度学习工具直接提取二维人脸关键点的方法因为其对遮挡和极端姿势变化的灵活性和鲁棒性成为面部表情分析的主流方法。在某些情况下头部整个姿态需要被估计,如果头部没有检测到足够多的关键点,并且三维头部模型质量以及变形计算成本高,这时候仅仅靠关键点的方法并不可用。本文提出的新方法正好可以解决这个问题,并且使用卷积神经网络从图像强度估计三维头部姿势具有更高精度。
  3. 相关工作
    在过去文献中,通常采用外观模板模型将测试图像与姿势样本进行比较以及面部探测器阵列方法(face detectors)。最近也有很多方法使用神经网络估计头部姿势,M. Patacchiola and A. Cangelosi.利用卷积神经网络回归损失训练和自适应梯度方法对头部姿态进行估计。A. Kumar, A. Alavi, and R. Chellappa.通过改进GoogleNet架构,学习有效的h-cnn回归器估计无约束人脸关键点和姿态。Hyperface是一个卷积神经网络,可以基于R-CNN的方法和改进AlexNet架构,融合了中间卷积层输出,添加全连接网络实现检测人脸,确定性别,发现landmark并且立即估计头部姿势。而用于人脸分析的All-InOne卷积神经网络增加了微笑、年龄估计和面部识别功能。Chang 等人使用一个简单的CNN回归3D头部姿态,并使用预测头部姿态用于面部对齐。Gu 等人使用VGG网络来回归头部姿态欧拉角,使用递归神经网络利用时间维度来改进姿态预测。这些方法和本文另一个区别就是数据集的处理。本文方法是在大型数据集上进行训练,并在各种外部数据集上测试网络性能,很好地展示了网络的泛化能力,可以更好地衡量模型如何在实际应用中推广。
  4. 方法
     a为复杂场景中姿态检测结果
    b为数据集部分测试结果
    该网络先在一个大的具有精确姿态标注的真实合成数据集上进行测试,在AFLW、AFLW2000、BIWI数据集上显示最新结果,然后在实际情况下准确地预测姿态。与landmark-to-pose方法相比,deep networks不依赖于选择头部模型、landmark检测方法、用于头部模型对齐的点子集或用于对齐2D到3D的优化方法。
    4.1多重损失法:
    在此之前的所有卷积网络预测头部姿态都是使用均方误差损失回归三个欧拉角,但是在大规模综合训练数据上并没有达到最佳效果。所以本文采用三个单独的loss,每个角度一个。每个损失都是一个二进制姿态分类和一个回归组合。每个backbone都可以通过三个全连接层扩展来预测角度,并且这三个全连接层共享网络先前的卷积层。该方法主要思想是使用稳定的softmax层和交叉熵进行分类,通过计算三个欧拉角的交叉熵损失,反向传播三个信号到网络中,计算每个输出角对二进制输出的期望值,获得细粒度预测(fine-grained predictions)然后再增加回归均方误差损失,改善细粒度预测。最后损失表现为两部分组合:
    L = H(y,yˆ)+ α · MSE(y,yˆ)
    其中y是角度的预测值,yˆ是真实值,H代表交叉熵损失。

 ResNet50 architecture with combined Mean Squared Error and Cross Entropy Losses
网络结构如图所示,输入图像通过ResNet50主干网络得到特征,然后通过三个全连接层FC,作为yaw、pitch和roll的分类输入。在代码中交叉熵(CrossEntropyLoss)包含了softmax,分类时从-99到99,以3为间隔,共67个值,66个间隔,作为离散的分类,对这些使用交叉熵计算损失。再将softmax归一化后的特征作为概率和对应类别相乘求和计算期望,和实际角度计算MSE损失。
yaw, pitch, roll = model(images) # Forward pass,得到特征
loss_yaw = criterion(yaw, label_yaw) #Cross entropy loss 交叉熵分类损失,对应类别为0-65
yaw_predicted = softmax(yaw) # MSE loss 回归损失
离散型随机变量的一切可能的取值(idx_tensor)与对应的概率(yaw_predicted)乘积之和称为该离散型随机变量的数学期望:
yaw_predicted = torch.sum(yaw_predicted * idx_tensor, 1) * 3 – 99

预测值的总的期望和真实值计算回归损失:
loss_reg_yaw = reg_criterion(yaw_predicted, label_yaw_cont)
4.2 数据集处理:
首先使用AFLW2000数据集作为首选测试集,其次,使用设备记录不同受试者不同头部姿态视频,收集为BIWI数据集,将三维模型拟合到每个个体点云上,跟踪头部旋转生成姿态标注。采用300W-LP数据集综合扩展数据训练landmark检测模型,所有的图像都被扭曲改变脸部姿态。
4.3 低分辨率的影响:
在目前进行的对低分辨率对广泛使用的地标探测器和最先进的探测器的影响研究中,认为低分辨率会降低landmark的检测性能,因为在估计关键点时需要访问在低分辨率下消失的特征。除此之外对于远处的头部姿态估计还没有什么研究进展,如何使用深度学习讨论低分辨率头部姿态也还处于空白。直接根据图像强度预测姿态的深度网络提供了一个很好的候选方法,可以通过智能修改网络或增加训练数据建立鲁棒性。本文提出的方法就提高对低分辨率图像的鲁棒性,通过随即下采样和上采样增加数据,使网络学习不同分辨率,同时还通过模糊图像增加数据。
5. 实验结果
实验对不同的姿态估计数据集和landmark检测数据集进行了测试。结果表明,当分辨率较低时,即使landmark探测器是最先进的,使用深度网络的整体姿态方法也要优于landmark-to-pose方法。在AFLW2000和BIWI数据集上评估了论文方法,完成细粒度的姿态估计任务,并与使用两种不同的地标探测器FAN和Dlib以及groundtruth landmarks(仅适用于AFLW2000)从地标估计的姿态进行了比较。AFLW2000图像很小,在脸部周围裁剪。对于BIWI,运行了一个更快的R-CNN人脸检测器,它在更广泛的人脸数据集上进行训练,并部署在Docker容器中。最后还从AFLW2000的groundtruth landmarks中提取姿态。除此之外,还运行了3DDFA,它通过卷积神经网络将三维人脸模型直接拟合到RGB图像中。3DDFA的主要任务是使用一个密集的三维模型来对齐面部的标志点。
本文提出的方法大大缩小了RGBD方法和ResNet50之间的差距。Pitch估计仍然落后,部分原因是在300W-LP数据集中缺少大量的极端pitch示例。预计猜测是可以通过增加可用数据缩小差距。
6. 结论
本文证明了一个多损失深度网络可以直接准确并且鲁棒地从图像强度中预测头部姿态。而且这种网络优于使用最先进的路标检测方法的landmark-to-pose方法。同时还证明了所提出的方法在数据集上是通用的,并且在检测路标时,更优于将头部姿态作为子目标的网络。在分辨低的情况下,landmark-to-pose性能效果不佳,但如果适当增加训练数据,多损失网络更具有鲁棒性。
附录代码:测试指定文件目录下图片中人脸姿态欧拉角估计,输出三个角度

import sys, os, argparse
from functools import partial

import numpy as np
import cv2
import matplotlib.pyplot as plt

import math
import torch
import torch.nn as nn
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import transforms
import torch.backends.cudnn as cudnn
import torchvision
import torch.nn.functional as F
from PIL import Image
import matplotlib.image as mp
from skimage import img_as_ubyte

import hopenet

from torch.backends import cudnn
from torchvision import transforms
import utils


def parse_args():
    """Parse input arguments."""
    parser = argparse.ArgumentParser(description='Head pose estimation using the Hopenet network.')
    parser.add_argument('--gpu', dest='gpu_id', help='GPU device id to use [0]', default=-1, type=int)
    parser.add_argument('--snapshot', dest='snapshot', help='Path of model snapshot.',
                        default='I:/GLJ/aliment/hopenet/output/snapshots/hopenet_robust_alpha1.pkl', type=str)
    args = parser.parse_args()
    return args


if __name__ == '__main__':
    args = parse_args()

    gpu = args.gpu_id

    if gpu >= 0:
        cudnn.enabled = True

    # ResNet50 structure
    model = hopenet.Hopenet(torchvision.models.resnet.Bottleneck, [3, 4, 6, 3], 66)  # 载入模型

    print('Loading snapshot.')
    if gpu >= 0:
        saved_state_dict = torch.load(args.snapshot)
    else:
        saved_state_dict = torch.load(args.snapshot, map_location=torch.device('cpu'))  # 无GPU的话,载入模型参数到CPU中
    model.load_state_dict(saved_state_dict)

    transformations = transforms.Compose([transforms.Resize(224),
                                          transforms.CenterCrop(224), transforms.ToTensor(),
                                          transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])

    if gpu >= 0:
        model.cuda(gpu)

    model.eval()  # Change model to 'eval' mode (BN uses moving mean/var).
    total = 0

    idx_tensor = [idx for idx in range(66)]
    idx_tensor = torch.FloatTensor(idx_tensor)
    if gpu >= 0:
        idx_tensor = idx_tensor.cuda(gpu)

    yaw_error = .0
    pitch_error = .0
    roll_error = .0
    l1loss = torch.nn.L1Loss(size_average=False)
    # img_path = '7.jpg'
    # frame = cv2.imread(img_path, 1)
    # cv2_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    img_path = './Pose_input/'
    out_path = './Pose_output/'

    filelist = os.listdir(img_path)
    total_num = len(filelist)
    for i in range(total_num):
        # cv2_img = cv2.imread(os.path.join(img_path))
        cv2_img = img_path + str(i + 1) + '.jpg'
        cv2_img = mp.imread(cv2_img)
        img = cv2.cvtColor(cv2_img, cv2.COLOR_BGR2RGB)
        img = Image.fromarray(img)

        img = transformations(img)  # Transform
        img_shape = img.size()
        img = img.view(1, img_shape[0], img_shape[1], img_shape[2])  # NCHW
        # img = torch.tensor(img)
        if gpu >= 0:
            img = img.cuda(gpu)

        yaw, pitch, roll = model(img)  # 得到特征 Forward pass

        _, yaw_bpred = torch.max(yaw.data, 1)
        _, pitch_bpred = torch.max(pitch.data, 1)
        _, roll_bpred = torch.max(roll.data, 1)

        # 特征归一化
        yaw_predicted = utils.softmax_temperature(yaw.data, 1)
        pitch_predicted = utils.softmax_temperature(pitch.data, 1)
        roll_predicted = utils.softmax_temperature(roll.data, 1)

        # 得到期望,将期望变换到连续值的度数
        yaw_predicted = torch.sum(yaw_predicted * idx_tensor, 1).cpu() * 3 - 99
        pitch_predicted = torch.sum(pitch_predicted * idx_tensor, 1).cpu() * 3 - 99
        roll_predicted = torch.sum(roll_predicted * idx_tensor, 1).cpu() * 3 - 99

        rows = cv2_img.shape[0]
        cols = cv2_img.shape[1]
        cv2_img = utils.draw_axis(cv2_img, yaw_predicted, pitch_predicted, roll_predicted, tdx=rows / 2,
                                  tdy=cols / 2, size=150)
        print(yaw_predicted.item(), pitch_predicted.item(), roll_predicted.item())
        # out_path = 'I:/GLJ/aliment/hopenet/output/Pose_output/'
        # cv2.imwrite('{}.png'.format(os.path.splitext(out_path)[0]), frame)
        out_img = out_path + str(i + 1) + '.png'
        result_img = Image.fromarray(cv2_img)
        result_img.save(out_img)
        # cv2.imwrite('{}.png'.format(os.path.splitext(out_path)[0]), cv2_img)

跑代码 一些坑
知乎解读

  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值