无需人脸检测,即可实时,6自由度3维人脸姿态估计方法 | 代码刚开源

2021-01-05 09:59:14 浏览数 (1)

导读

论文:《img2pose: Face Alignment and Detection via 6DoF, Face Pose stimation》

链接:https://arxiv.org/abs/2012.07791

代码:http://github.com/vitoralbiero/img2pose

本文提出了实时、六自由度(6DoF)、三维人脸姿态估计,无需人脸检测或关键点定位。我们发现估计人脸的6自由度刚性变换比人脸关键点检测更简单,人脸关键点检测通常用于三维人脸对齐。

摘要

我们提出了实时、六自由度(6DoF)、三维人脸姿态估计,无需人脸检测或关键点定位。我们发现估计人脸的6自由度刚性变换比人脸关键点检测更简单,人脸关键点检测通常用于三维人脸对齐。此外,6DoF提供了比人脸框标签更多的信息。我们利用这些观察结果做出了多种贡献:(a)我们描述了一个容易训练、高效、基于Faster R-CNN的模型,该模型回归照片中所有面孔的6DoF姿态,而不需要进行初步的人脸检测。(b)我们解释在训练和评估我们的模型时,如何转换输入照片和任意作物之间的姿态并保持一致。(c)最后,我们展示了人脸姿态如何取代检测边界框训练标签。在AFLW2000-3D和BIWI上的测试表明,我们的方法运行在实时和性能优于状态(SotA)人脸姿态估计器。值得注意的是,我们的方法在更宽的人脸检测基准上也超过了类似的SotA模型,尽管没有在包围框标签上进行优化。

1、简介

人脸检测就是定位一个框,把一张照片中的每一张人脸框起来起来。人脸关键点检测旨在定位特定的面部特征:例如,眼睛中心,鼻尖。

尽管这种方法对过去而言是成功的,但它也有缺点。关键点检测器通常能优化到由特定的人脸检测器产生的边界框的特性(上限)。更新人脸检测器也需要重新优化关键点检测器。此外,SotA 的检测和姿态估计需要巨大的计算量。最后一点,对于定位标准的68个关键点而言,小脸是非常难做到的。

为了解决上述的问题,我们重点关注了下面的内容:

关注点1:估计6个姿态自由度比检测关键点更容易

关注点2:6个姿态自由度标签捕捉的不仅仅是框的位置。

贡献:

  • 我们提出了一种直接对图像中所有人脸进行6自由度三维人脸姿态估计的新方法,而不需要进行人脸检测
  • 我们介绍了一种有效的姿态转换方法,以保持估计和真实位姿的一致性,在图像和它的特别推荐之间
  • 我们展示了生成的3D姿态估计如何被转换成精确的2D边界框,能作为附带产物,以最小的计算开销。

我们的模型使用了一个小的、快速的ResNet-18 作为 backbone,并在WIDER FACE 训练集上使用弱监督和人工注释的ground-truth姿态标签进行训练。

2、Related work

主要涉及 Face detection

Face alignment and pose estimation

这里不详细介绍了,文章介绍了一些相关工作的最新进展。

之前的人脸识别综述中有涉及到一些相关的,感兴趣的可以参看下面的链接

2020人脸识别最新进展综述,参考文献近400篇 | 附下载

3、本文提出的方法

给定一张图片 I,估计图片中每个人脸的6 个自由度。

其中的含义:

(rx, ry, rz) 表示 Euler angles – roll, pitch, yaw 旋转

(tx, ty, tz)是三维人脸变换

3.1. Our img2pose network

网络采用 图 4 的 基于 Faster R-CNN 的二阶段方法。第一阶段采用的是特征金字塔的 RPN 网络,用于定位在图片可能存在人脸的位置。

与标准的RPN loss不同(采用ground-truth 边界框),我们对边界框进行投影,采用方程 2 获得6个姿态自由度的ground-truth 姿态标签。能获得更好人脸区域一致性。

K是内参矩阵

R和t分别为由h得到的三维旋转矩阵和平移向量均值

是一个表示三维面形曲面上n个三维点的矩阵。

最后,

是二维点从三维投影到图像上的矩阵表示。

其他地方与 Faster R-CNN类似。

我们的img2pose的第二阶段从每个proposal 中提取具有感兴趣区域(ROI)池化的特征,然后将它们传递给两个不同的头部:一个标准的人脸/非人脸分类器和一个新颖的6自由度人脸姿态回归器

3.2. Pose label conversion

简单地说,算法1有两个步骤。首先,在第2-3行,我们调整姿势。这一步直观地调整相机来查看整个图像,而不仅仅是一个裁剪。然后,在步骤4-8中,我们转换焦点,根据焦点位置的差异调整裁剪和图像之间的姿态。最后,我们返回一个相对于图像本身 Kimg 的6自由度姿态。

3.3. Training losses

我们同时训练了人脸/非人脸分类器的头部和人脸姿态回归器。对于每个proposal,模型采用以下多任务损失L。

(1)Face classification loss.

使用标准二进制交叉熵损失(cross-entropy loss)

(2)Face pose loss

这一损失直接比较了6自由度人脸姿态估计与其ground-truth

(3)Calibration point loss

这是一种获取估计姿态精度的额外手段,我们考虑在图像中投影的3D脸形点的二维位置

4、应用细节

img2pose network

我们提出了一种新的六自由度人脸姿态估计和对齐方法,它不依赖于首先运行人脸检测器或定位人脸标志。据我们所知,我们是第一个提出这种多面、直接的方法的人。我们提出了一种新的姿态转换算法,以保持在不同图像中对同一人脸的位姿估计的一致性。我们证明了通过估计的三维人脸姿态可以产生人脸框,从而实现了作为姿态估计的副产品的人脸检测。大量的实验证明了我们的img2pose对于人脸姿态估计和人脸检测的有效性。

作为一个类,人脸作为姿态和检测的结合提供了很好的机会:面孔有明确的外观统计,可以依赖于准确的姿态估计。然而,脸并不是可以采用这种方法的唯一类别;在其他领域,例如零售[25],通过应用类似的直接姿态估计步骤来替代目标和关键点检测,也可以获得同样的精度提高。

5、代码试跑

使用官方提供的模型和数据集测试姿态估计和对齐效果

代码语言:javascript复制
import sys
sys.path.append('../../')
import numpy as np
import torch
from torchvision import transforms
from matplotlib import pyplot as plt
from tqdm.notebook import tqdm
from PIL import Image, ImageOps
import matplotlib.patches as patches
from scipy.spatial.transform import Rotation
import pandas as pd
from scipy.spatial import distance
import time
import os
import math
import scipy.io as sio
import random
from utils.renderer import Renderer
from utils.image_operations import expand_bbox_rectangle
from utils.pose_operations import get_pose
from img2pose import img2poseModel
from model_loader import load_model
from data_loader_lmdb import LMDBDataLoader
from dataclasses import dataclass

np.set_printoptions(suppress=True)

torch.random.manual_seed(42)
np.random.seed(42)

@dataclass
class Config:
    batch_size: int
    pin_memory: bool
    workers: int
    pose_mean: np.array
    pose_stddev: np.array
    noise_augmentation: bool
    contrast_augmentation: bool
    threed_68_points: str
    distributed: bool
renderer = Renderer(
    vertices_path="pose_references/vertices_trans.npy", 
    triangles_path="pose_references/triangles.npy"
)

threed_points = np.load('pose_references/reference_3d_68_points_trans.npy')

transform = transforms.Compose([transforms.ToTensor()])

# bounding box customization
# how much to expand in the width
BBOX_X_FACTOR = 1.1
# how much to expand in height
BBOX_Y_FACTOR = 1.1
# how much to expand the forehead
EXPAND_FOREHEAD = 0.3

DEPTH = 18
MAX_SIZE = 1400
MIN_SIZE = 600

POSE_MEAN = "models/WIDER_train_pose_mean_v1.npy"
POSE_STDDEV = "models/WIDER_train_pose_stddev_v1.npy"
MODEL_PATH = "models/img2pose_v1.pth"

pose_mean = np.load(POSE_MEAN)
pose_stddev = np.load(POSE_STDDEV)

img2pose_model = img2poseModel(
    DEPTH, MIN_SIZE, MAX_SIZE, 
    pose_mean=pose_mean, pose_stddev=pose_stddev,
    threed_68_points=threed_points,    
    bbox_x_factor=BBOX_X_FACTOR,
    bbox_y_factor=BBOX_Y_FACTOR,
    expand_forehead=EXPAND_FOREHEAD,
)
load_model(img2pose_model.fpn_model, MODEL_PATH, cpu_mode=str(img2pose_model.device) == "cpu", model_only=True)
img2pose_model.evaluate()


LMDB_FILE = "datasets/lmdb/WIDER_val_annotations.lmdb"

# load images that were already compressed from json list to lmdb file
lmdb_data_loader = LMDBDataLoader(
    Config(
        batch_size=1,
        pin_memory=True,
        workers=1,
        pose_mean=pose_mean,
        pose_stddev=pose_stddev,
        noise_augmentation=False,
        contrast_augmentation=False,
        threed_68_points='pose_references/reference_3d_68_points_trans.npy',
        distributed=False
    ),
    LMDB_FILE,
    train=True,
)

threshold = 0.8
total_imgs = 20

data_iter = iter(lmdb_data_loader)

for j in tqdm(range(total_imgs)):
    torch_img, target = next(data_iter)
    target = target[0]
        
    bboxes = []
    scores = []
    poses = []
    
    img = torch_img[0]
    img = img.squeeze()
    img = transforms.ToPILImage()(img).convert("RGB")
    ori_img = img.copy()
    

    run_img = img.copy()

    w, h = img.size

    min_size = min(w, h)
    max_size = max(w, h)
    
    # run on the original image size
    img2pose_model.fpn_model.module.set_max_min_size(max_size, min_size)

    res = img2pose_model.predict([transform(run_img)])

    res = res[0]

    for i in range(len(res["scores"])):
        if res["scores"][i] > threshold:
            bboxes.append(res["boxes"].cpu().numpy()[i].astype('int'))
            scores.append(res["scores"].cpu().numpy()[i].astype('float'))
            poses.append(res["dofs"].cpu().numpy()[i].astype('float'))
                
    (w, h) = img.size
    image_intrinsics = np.array([[w   h, 0, w // 2], [0, w   h, h // 2], [0, 0, 1]])
    
    plt.figure(figsize=(16, 16))    
    
    poses = np.asarray(poses)
    bboxes = np.asarray(bboxes)
    scores = np.asarray(scores)
    print(poses)
    if np.ndim(bboxes) == 1 and len(bboxes) > 0:
        bboxes = bboxes[np.newaxis, :]
        poses = poses[np.newaxis, :]        
        
    if len(bboxes) != 0:
        ranked = np.argsort(poses[:, 5])[::-1]
        poses = poses[ranked]
        bboxes = bboxes[ranked]
        scores = scores[ranked]

        for i in range(len(scores)):
            if scores[i] > threshold:
                bbox = bboxes[i]

                pose_pred = poses[i]
                pose_pred = np.asarray(pose_pred.squeeze())        

                trans_vertices = renderer.transform_vertices(img, [pose_pred])
                img = renderer.render(img, trans_vertices, alpha=1)  
                plt.gca().add_patch(patches.Rectangle((bbox[0], bbox[1]), bbox[2] - bbox[0], bbox[3] - bbox[1],linewidth=3,edgecolor='b',facecolor='none'))            
                img = Image.fromarray(img)

        plt.imshow(img)        
        plt.show() 

输入图片:

对齐后的效果:

更多的细节可以参看论文和官方的开源代码。公式推导等附录有一些介绍。

如果文章对你有所帮助,请给一波三连(关注,点赞,在看),感谢

链接:https://arxiv.org/abs/2012.07791

代码:http://github.com/vitoralbiero/img2pose

代码语言:javascript复制
下载1:何恺明顶会分享
在「AI算法与图像处理」公众号后台回复:何恺明,即可下载。总共有6份PDF,涉及 ResNet、Mask RCNN等经典工作的总结分析
下载2:leetcode 开源书
在「AI算法与图像处理」公众号后台回复:leetcode,即可下载。每题都 runtime beats 100% 的开源好书,你值得拥有!

下载3 CVPR2020
在「AI算法与图像处理」公众号后台回复:CVPR2020,即可下载1467篇CVPR 2020论文个人微信(如果没有备注不拉群!)请注明:地区 学校/企业 研究方向 昵称

觉得不错就点亮在看吧

0 人点赞