人臉檢測就是定位一個框,把一張照片中的每一張人臉框起來起來。人臉關鍵點檢測旨在定位特定的面部特徵:例如,眼睛中心,鼻尖。
儘管這種方法對過去而言是成功的,但它也有缺點。關鍵點檢測器通常能優化到由特定的人臉檢測器產生的邊界框的特性(上限)。更新人臉檢測器也需要重新優化關鍵點檢測器。此外,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、代碼試跑使用官方提供的模型和數據集測試姿態估計和對齊效果
import syssys.path.append('../../')import numpy as npimport torchfrom torchvision import transformsfrom matplotlib import pyplot as pltfrom tqdm.notebook import tqdmfrom PIL import Image, ImageOpsimport matplotlib.patches as patchesfrom scipy.spatial.transform import Rotationimport pandas as pdfrom scipy.spatial import distanceimport timeimport osimport mathimport scipy.io as sioimport randomfrom utils.renderer import Rendererfrom utils.image_operations import expand_bbox_rectanglefrom utils.pose_operations import get_posefrom img2pose import img2poseModelfrom model_loader import load_modelfrom data_loader_lmdb import LMDBDataLoaderfrom dataclasses import dataclass
np.set_printoptions(suppress=True)
torch.random.manual_seed(42)np.random.seed(42)
@dataclassclass 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: boolrenderer = 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()])
BBOX_X_FACTOR = 1.1BBOX_Y_FACTOR = 1.1EXPAND_FOREHEAD = 0.3
DEPTH = 18MAX_SIZE = 1400MIN_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"
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.8total_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) 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