Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
35 changes: 35 additions & 0 deletions MuseTalk_project/3DMM/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 基于3DMM点云的TFG评价指标

在talking face generation领域,正确地评估生成的视频中人物动作、神态等身份信息对于提高生成质量有重要帮助,更有助于对于不同方法间的公平比较。传统的图像评价指标如psnr、ssim等并不能直接准确表示人物信息,可能受非人物的因素影响(如图像背景等)。对此,我们提出了一种全新的评价指标,通过抽取视频中的人脸点云信息,将生成的视频与Grand Truth进行对比,完成生成质量的评估。

### 评价方法

Deep3DFaceRecon 是一种使用CNN对人脸进行3D重建的方法,在重建速度、精确度、鲁棒性方面有了很大提升。它不仅能够完成对人脸的识别和提取关键点,还能够获取人物表情、动作等特征,用特征向量加以表示。![image-20241222024034296](C:\Users\14879\AppData\Roaming\Typora\typora-user-images\image-20241222024034296.png)

使用Deep3DFaceRecon方法抽取视频中人脸的关键点信息,得到 lm68 点云集。将生成视频与GT的关键点按照以下方式进行比较,进行标准化后根据MSE计算评价指标:
$$
\log(\sum_{i=1}^{68}\lambda_i[(x_i-\hat{x_i})^2+(y_i-\hat{y_i})^2]+m_i)
$$
其中,$(x,y)$ 代表点云的坐标,$\lambda_i$ 表示权重系数,可以据此对目标人物脸部的不同部位(如唇部、眼睛等)进行加权评估,以突出不同部位的渲染效果。$m_i$ 为偏移量。在下面的评测中,唇部位置 $\lambda _i=1.2$ 其余位置 $\lambda _i =1.0$ 。$m_i = 5$。

### 如何使用

1. 通过 ``pip install -r requirements`` 安装对应环境
2. 将需要对比的视频放置根目录,更改eval.py中的默认路径
3. 运行eval.py

### 评测结果

按照以上指标对测试集视频进行评测,结果如下:

| 视频 | MuseTalk(30s) | MuseTalk(half) |
| ------- | ------------- | -------------- |
| Jae-in | 3.5845 | 3.5606 |
| Lieu | 4.4873 | 4.3448 |
| Macron | 5.0920 | 5.0923 |
| May | 4.4909 | 4.5529 |
| Obama | 4.3296 | 4.7277 |
| Obama1 | 4.8002 | 4.9230 |
| Obama2 | 4.5330 | 4.6687 |
| Shaheen | 3.8962 | 4.1104 |

1 change: 1 addition & 0 deletions MuseTalk_project/3DMM/deep_3drecon/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .reconstructor import *
Binary file not shown.
Binary file not shown.
116 changes: 116 additions & 0 deletions MuseTalk_project/3DMM/deep_3drecon/data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""This package includes all the modules related to data loading and preprocessing

To add a custom dataset class called 'dummy', you need to add a file called 'dummy_dataset.py' and define a subclass 'DummyDataset' inherited from BaseDataset.
You need to implement four functions:
-- <__init__>: initialize the class, first call BaseDataset.__init__(self, opt).
-- <__len__>: return the size of dataset.
-- <__getitem__>: get a data point from data loader.
-- <modify_commandline_options>: (optionally) add dataset-specific options and set default options.

Now you can use the dataset class by specifying flag '--dataset_mode dummy'.
See our template dataset class 'template_dataset.py' for more details.
"""
import numpy as np
import importlib
import torch.utils.data
from data.base_dataset import BaseDataset


def find_dataset_using_name(dataset_name):
"""Import the module "data/[dataset_name]_dataset.py".

In the file, the class called DatasetNameDataset() will
be instantiated. It has to be a subclass of BaseDataset,
and it is case-insensitive.
"""
dataset_filename = "data." + dataset_name + "_dataset"
datasetlib = importlib.import_module(dataset_filename)

dataset = None
target_dataset_name = dataset_name.replace('_', '') + 'dataset'
for name, cls in datasetlib.__dict__.items():
if name.lower() == target_dataset_name.lower() \
and issubclass(cls, BaseDataset):
dataset = cls

if dataset is None:
raise NotImplementedError("In %s.py, there should be a subclass of BaseDataset with class name that matches %s in lowercase." % (dataset_filename, target_dataset_name))

return dataset


def get_option_setter(dataset_name):
"""Return the static method <modify_commandline_options> of the dataset class."""
dataset_class = find_dataset_using_name(dataset_name)
return dataset_class.modify_commandline_options


def create_dataset(opt, rank=0):
"""Create a dataset given the option.

This function wraps the class CustomDatasetDataLoader.
This is the main interface between this package and 'train.py'/'test.py'

Example:
>>> from data import create_dataset
>>> dataset = create_dataset(opt)
"""
data_loader = CustomDatasetDataLoader(opt, rank=rank)
dataset = data_loader.load_data()
return dataset

class CustomDatasetDataLoader():
"""Wrapper class of Dataset class that performs multi-threaded data loading"""

def __init__(self, opt, rank=0):
"""Initialize this class

Step 1: create a dataset instance given the name [dataset_mode]
Step 2: create a multi-threaded data loader.
"""
self.opt = opt
dataset_class = find_dataset_using_name(opt.dataset_mode)
self.dataset = dataset_class(opt)
self.sampler = None
print("rank %d %s dataset [%s] was created" % (rank, self.dataset.name, type(self.dataset).__name__))
if opt.use_ddp and opt.isTrain:
world_size = opt.world_size
self.sampler = torch.utils.data.distributed.DistributedSampler(
self.dataset,
num_replicas=world_size,
rank=rank,
shuffle=not opt.serial_batches
)
self.dataloader = torch.utils.data.DataLoader(
self.dataset,
sampler=self.sampler,
num_workers=int(opt.num_threads / world_size),
batch_size=int(opt.batch_size / world_size),
drop_last=True)
else:
self.dataloader = torch.utils.data.DataLoader(
self.dataset,
batch_size=opt.batch_size,
shuffle=(not opt.serial_batches) and opt.isTrain,
num_workers=int(opt.num_threads),
drop_last=True
)

def set_epoch(self, epoch):
self.dataset.current_epoch = epoch
if self.sampler is not None:
self.sampler.set_epoch(epoch)

def load_data(self):
return self

def __len__(self):
"""Return the number of data in the dataset"""
return min(len(self.dataset), self.opt.max_dataset_size)

def __iter__(self):
"""Return a batch of data"""
for i, data in enumerate(self.dataloader):
if i * self.opt.batch_size >= self.opt.max_dataset_size:
break
yield data
Binary file not shown.
Binary file not shown.
125 changes: 125 additions & 0 deletions MuseTalk_project/3DMM/deep_3drecon/data/base_dataset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""This module implements an abstract base class (ABC) 'BaseDataset' for datasets.

It also includes common transformation functions (e.g., get_transform, __scale_width), which can be later used in subclasses.
"""
import random
import numpy as np
import torch.utils.data as data
from PIL import Image
import torchvision.transforms as transforms
from abc import ABC, abstractmethod


class BaseDataset(data.Dataset, ABC):
"""This class is an abstract base class (ABC) for datasets.

To create a subclass, you need to implement the following four functions:
-- <__init__>: initialize the class, first call BaseDataset.__init__(self, opt).
-- <__len__>: return the size of dataset.
-- <__getitem__>: get a data point.
-- <modify_commandline_options>: (optionally) add dataset-specific options and set default options.
"""

def __init__(self, opt):
"""Initialize the class; save the options in the class

Parameters:
opt (Option class)-- stores all the experiment flags; needs to be a subclass of BaseOptions
"""
self.opt = opt
# self.root = opt.dataroot
self.current_epoch = 0

@staticmethod
def modify_commandline_options(parser, is_train):
"""Add new dataset-specific options, and rewrite default values for existing options.

Parameters:
parser -- original option parser
is_train (bool) -- whether training phase or test phase. You can use this flag to add training-specific or test-specific options.

Returns:
the modified parser.
"""
return parser

@abstractmethod
def __len__(self):
"""Return the total number of images in the dataset."""
return 0

@abstractmethod
def __getitem__(self, index):
"""Return a data point and its metadata information.

Parameters:
index - - a random integer for data indexing

Returns:
a dictionary of data with their names. It ususally contains the data itself and its metadata information.
"""
pass


def get_transform(grayscale=False):
transform_list = []
if grayscale:
transform_list.append(transforms.Grayscale(1))
transform_list += [transforms.ToTensor()]
return transforms.Compose(transform_list)

def get_affine_mat(opt, size):
shift_x, shift_y, scale, rot_angle, flip = 0., 0., 1., 0., False
w, h = size

if 'shift' in opt.preprocess:
shift_pixs = int(opt.shift_pixs)
shift_x = random.randint(-shift_pixs, shift_pixs)
shift_y = random.randint(-shift_pixs, shift_pixs)
if 'scale' in opt.preprocess:
scale = 1 + opt.scale_delta * (2 * random.random() - 1)
if 'rot' in opt.preprocess:
rot_angle = opt.rot_angle * (2 * random.random() - 1)
rot_rad = -rot_angle * np.pi/180
if 'flip' in opt.preprocess:
flip = random.random() > 0.5

shift_to_origin = np.array([1, 0, -w//2, 0, 1, -h//2, 0, 0, 1]).reshape([3, 3])
flip_mat = np.array([-1 if flip else 1, 0, 0, 0, 1, 0, 0, 0, 1]).reshape([3, 3])
shift_mat = np.array([1, 0, shift_x, 0, 1, shift_y, 0, 0, 1]).reshape([3, 3])
rot_mat = np.array([np.cos(rot_rad), np.sin(rot_rad), 0, -np.sin(rot_rad), np.cos(rot_rad), 0, 0, 0, 1]).reshape([3, 3])
scale_mat = np.array([scale, 0, 0, 0, scale, 0, 0, 0, 1]).reshape([3, 3])
shift_to_center = np.array([1, 0, w//2, 0, 1, h//2, 0, 0, 1]).reshape([3, 3])

affine = shift_to_center @ scale_mat @ rot_mat @ shift_mat @ flip_mat @ shift_to_origin
affine_inv = np.linalg.inv(affine)
return affine, affine_inv, flip

def apply_img_affine(img, affine_inv, method=Image.BICUBIC):
return img.transform(img.size, Image.AFFINE, data=affine_inv.flatten()[:6], resample=Image.BICUBIC)

def apply_lm_affine(landmark, affine, flip, size):
_, h = size
lm = landmark.copy()
lm[:, 1] = h - 1 - lm[:, 1]
lm = np.concatenate((lm, np.ones([lm.shape[0], 1])), -1)
lm = lm @ np.transpose(affine)
lm[:, :2] = lm[:, :2] / lm[:, 2:]
lm = lm[:, :2]
lm[:, 1] = h - 1 - lm[:, 1]
if flip:
lm_ = lm.copy()
lm_[:17] = lm[16::-1]
lm_[17:22] = lm[26:21:-1]
lm_[22:27] = lm[21:16:-1]
lm_[31:36] = lm[35:30:-1]
lm_[36:40] = lm[45:41:-1]
lm_[40:42] = lm[47:45:-1]
lm_[42:46] = lm[39:35:-1]
lm_[46:48] = lm[41:39:-1]
lm_[48:55] = lm[54:47:-1]
lm_[55:60] = lm[59:54:-1]
lm_[60:65] = lm[64:59:-1]
lm_[65:68] = lm[67:64:-1]
lm = lm_
return lm
Loading