Initial snapshot: hand motion pipeline (Dyn-HaMR + dex-retargeting + SPIDER)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 14:13:49 +08:00
commit 7e4ef6f98b
7002 changed files with 1452249 additions and 0 deletions
@@ -0,0 +1,42 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .builder import DATASETS, PIPELINES, build_dataloader, build_dataset
from .dataset_info import DatasetInfo
from .pipelines import Compose
from .samplers import DistributedSampler
from .datasets import ( # isort:skip
AnimalATRWDataset, AnimalFlyDataset, AnimalHorse10Dataset,
AnimalLocustDataset, AnimalMacaqueDataset, AnimalPoseDataset,
AnimalZebraDataset, Body3DH36MDataset, BottomUpAicDataset,
BottomUpCocoDataset, BottomUpCocoWholeBodyDataset,
BottomUpCrowdPoseDataset, BottomUpMhpDataset, DeepFashionDataset,
Face300WDataset, FaceAFLWDataset, FaceCocoWholeBodyDataset,
FaceCOFWDataset, FaceWFLWDataset, FreiHandDataset,
HandCocoWholeBodyDataset, InterHand2DDataset, InterHand3DDataset,
MeshAdversarialDataset, MeshH36MDataset, MeshMixDataset, MoshDataset,
OneHand10KDataset, PanopticDataset, TopDownAicDataset, TopDownCocoDataset,
TopDownCocoWholeBodyDataset, TopDownCrowdPoseDataset,
TopDownFreiHandDataset, TopDownH36MDataset, TopDownJhmdbDataset,
TopDownMhpDataset, TopDownMpiiDataset, TopDownMpiiTrbDataset,
TopDownOCHumanDataset, TopDownOneHand10KDataset, TopDownPanopticDataset,
TopDownPoseTrack18Dataset, TopDownPoseTrack18VideoDataset)
__all__ = [
'TopDownCocoDataset', 'BottomUpCocoDataset', 'BottomUpMhpDataset',
'BottomUpAicDataset', 'BottomUpCocoWholeBodyDataset', 'TopDownMpiiDataset',
'TopDownMpiiTrbDataset', 'OneHand10KDataset', 'PanopticDataset',
'HandCocoWholeBodyDataset', 'FreiHandDataset', 'InterHand2DDataset',
'InterHand3DDataset', 'TopDownOCHumanDataset', 'TopDownAicDataset',
'TopDownCocoWholeBodyDataset', 'MeshH36MDataset', 'MeshMixDataset',
'MoshDataset', 'MeshAdversarialDataset', 'TopDownCrowdPoseDataset',
'BottomUpCrowdPoseDataset', 'TopDownFreiHandDataset',
'TopDownOneHand10KDataset', 'TopDownPanopticDataset',
'TopDownPoseTrack18Dataset', 'TopDownJhmdbDataset', 'TopDownMhpDataset',
'DeepFashionDataset', 'Face300WDataset', 'FaceAFLWDataset',
'FaceWFLWDataset', 'FaceCOFWDataset', 'FaceCocoWholeBodyDataset',
'Body3DH36MDataset', 'AnimalHorse10Dataset', 'AnimalMacaqueDataset',
'AnimalFlyDataset', 'AnimalLocustDataset', 'AnimalZebraDataset',
'AnimalATRWDataset', 'AnimalPoseDataset', 'TopDownH36MDataset',
'TopDownPoseTrack18VideoDataset', 'build_dataloader', 'build_dataset',
'Compose', 'DistributedSampler', 'DATASETS', 'PIPELINES', 'DatasetInfo'
]
@@ -0,0 +1,162 @@
# Copyright (c) OpenMMLab. All rights reserved.
import copy
import platform
import random
from functools import partial
import numpy as np
from mmcv.parallel import collate
from mmcv.runner import get_dist_info
from mmcv.utils import Registry, build_from_cfg, is_seq_of
from mmcv.utils.parrots_wrapper import _get_dataloader
from torch.utils.data.dataset import ConcatDataset
from .samplers import DistributedSampler
if platform.system() != 'Windows':
# https://github.com/pytorch/pytorch/issues/973
import resource
rlimit = resource.getrlimit(resource.RLIMIT_NOFILE)
base_soft_limit = rlimit[0]
hard_limit = rlimit[1]
soft_limit = min(max(4096, base_soft_limit), hard_limit)
resource.setrlimit(resource.RLIMIT_NOFILE, (soft_limit, hard_limit))
DATASETS = Registry('dataset')
PIPELINES = Registry('pipeline')
def _concat_dataset(cfg, default_args=None):
types = cfg['type']
ann_files = cfg['ann_file']
img_prefixes = cfg.get('img_prefix', None)
dataset_infos = cfg.get('dataset_info', None)
num_joints = cfg['data_cfg'].get('num_joints', None)
dataset_channel = cfg['data_cfg'].get('dataset_channel', None)
datasets = []
num_dset = len(ann_files)
for i in range(num_dset):
cfg_copy = copy.deepcopy(cfg)
cfg_copy['ann_file'] = ann_files[i]
if isinstance(types, (list, tuple)):
cfg_copy['type'] = types[i]
if isinstance(img_prefixes, (list, tuple)):
cfg_copy['img_prefix'] = img_prefixes[i]
if isinstance(dataset_infos, (list, tuple)):
cfg_copy['dataset_info'] = dataset_infos[i]
if isinstance(num_joints, (list, tuple)):
cfg_copy['data_cfg']['num_joints'] = num_joints[i]
if is_seq_of(dataset_channel, list):
cfg_copy['data_cfg']['dataset_channel'] = dataset_channel[i]
datasets.append(build_dataset(cfg_copy, default_args))
return ConcatDataset(datasets)
def build_dataset(cfg, default_args=None):
"""Build a dataset from config dict.
Args:
cfg (dict): Config dict. It should at least contain the key "type".
default_args (dict, optional): Default initialization arguments.
Default: None.
Returns:
Dataset: The constructed dataset.
"""
from .dataset_wrappers import RepeatDataset
if isinstance(cfg, (list, tuple)):
dataset = ConcatDataset([build_dataset(c, default_args) for c in cfg])
elif cfg['type'] == 'ConcatDataset':
dataset = ConcatDataset(
[build_dataset(c, default_args) for c in cfg['datasets']])
elif cfg['type'] == 'RepeatDataset':
dataset = RepeatDataset(
build_dataset(cfg['dataset'], default_args), cfg['times'])
elif isinstance(cfg.get('ann_file'), (list, tuple)):
dataset = _concat_dataset(cfg, default_args)
else:
dataset = build_from_cfg(cfg, DATASETS, default_args)
return dataset
def build_dataloader(dataset,
samples_per_gpu,
workers_per_gpu,
num_gpus=1,
dist=True,
shuffle=True,
seed=None,
drop_last=True,
pin_memory=True,
**kwargs):
"""Build PyTorch DataLoader.
In distributed training, each GPU/process has a dataloader.
In non-distributed training, there is only one dataloader for all GPUs.
Args:
dataset (Dataset): A PyTorch dataset.
samples_per_gpu (int): Number of training samples on each GPU, i.e.,
batch size of each GPU.
workers_per_gpu (int): How many subprocesses to use for data loading
for each GPU.
num_gpus (int): Number of GPUs. Only used in non-distributed training.
dist (bool): Distributed training/test or not. Default: True.
shuffle (bool): Whether to shuffle the data at every epoch.
Default: True.
drop_last (bool): Whether to drop the last incomplete batch in epoch.
Default: True
pin_memory (bool): Whether to use pin_memory in DataLoader.
Default: True
kwargs: any keyword argument to be used to initialize DataLoader
Returns:
DataLoader: A PyTorch dataloader.
"""
rank, world_size = get_dist_info()
if dist:
sampler = DistributedSampler(
dataset, world_size, rank, shuffle=shuffle, seed=seed)
shuffle = False
batch_size = samples_per_gpu
num_workers = workers_per_gpu
else:
sampler = None
batch_size = num_gpus * samples_per_gpu
num_workers = num_gpus * workers_per_gpu
init_fn = partial(
worker_init_fn, num_workers=num_workers, rank=rank,
seed=seed) if seed is not None else None
_, DataLoader = _get_dataloader()
data_loader = DataLoader(
dataset,
batch_size=batch_size,
sampler=sampler,
num_workers=num_workers,
collate_fn=partial(collate, samples_per_gpu=samples_per_gpu),
pin_memory=pin_memory,
shuffle=shuffle,
worker_init_fn=init_fn,
drop_last=drop_last,
**kwargs)
return data_loader
def worker_init_fn(worker_id, num_workers, rank, seed):
"""Init the random seed for various workers."""
# The seed of each worker equals to
# num_worker * rank + worker_id + user_seed
worker_seed = num_workers * rank + worker_id + seed
np.random.seed(worker_seed)
random.seed(worker_seed)
@@ -0,0 +1,104 @@
# Copyright (c) OpenMMLab. All rights reserved.
import numpy as np
class DatasetInfo:
def __init__(self, dataset_info):
self._dataset_info = dataset_info
self.dataset_name = self._dataset_info['dataset_name']
self.paper_info = self._dataset_info['paper_info']
self.keypoint_info = self._dataset_info['keypoint_info']
self.skeleton_info = self._dataset_info['skeleton_info']
self.joint_weights = np.array(
self._dataset_info['joint_weights'], dtype=np.float32)[:, None]
self.sigmas = np.array(self._dataset_info['sigmas'])
self._parse_keypoint_info()
self._parse_skeleton_info()
def _parse_skeleton_info(self):
"""Parse skeleton information.
- link_num (int): number of links.
- skeleton (list((2,))): list of links (id).
- skeleton_name (list((2,))): list of links (name).
- pose_link_color (np.ndarray): the color of the link for
visualization.
"""
self.link_num = len(self.skeleton_info.keys())
self.pose_link_color = []
self.skeleton_name = []
self.skeleton = []
for skid in self.skeleton_info.keys():
link = self.skeleton_info[skid]['link']
self.skeleton_name.append(link)
self.skeleton.append([
self.keypoint_name2id[link[0]], self.keypoint_name2id[link[1]]
])
self.pose_link_color.append(self.skeleton_info[skid].get(
'color', [255, 128, 0]))
self.pose_link_color = np.array(self.pose_link_color)
def _parse_keypoint_info(self):
"""Parse keypoint information.
- keypoint_num (int): number of keypoints.
- keypoint_id2name (dict): mapping keypoint id to keypoint name.
- keypoint_name2id (dict): mapping keypoint name to keypoint id.
- upper_body_ids (list): a list of keypoints that belong to the
upper body.
- lower_body_ids (list): a list of keypoints that belong to the
lower body.
- flip_index (list): list of flip index (id)
- flip_pairs (list((2,))): list of flip pairs (id)
- flip_index_name (list): list of flip index (name)
- flip_pairs_name (list((2,))): list of flip pairs (name)
- pose_kpt_color (np.ndarray): the color of the keypoint for
visualization.
"""
self.keypoint_num = len(self.keypoint_info.keys())
self.keypoint_id2name = {}
self.keypoint_name2id = {}
self.pose_kpt_color = []
self.upper_body_ids = []
self.lower_body_ids = []
self.flip_index_name = []
self.flip_pairs_name = []
for kid in self.keypoint_info.keys():
keypoint_name = self.keypoint_info[kid]['name']
self.keypoint_id2name[kid] = keypoint_name
self.keypoint_name2id[keypoint_name] = kid
self.pose_kpt_color.append(self.keypoint_info[kid].get(
'color', [255, 128, 0]))
type = self.keypoint_info[kid].get('type', '')
if type == 'upper':
self.upper_body_ids.append(kid)
elif type == 'lower':
self.lower_body_ids.append(kid)
else:
pass
swap_keypoint = self.keypoint_info[kid].get('swap', '')
if swap_keypoint == keypoint_name or swap_keypoint == '':
self.flip_index_name.append(keypoint_name)
else:
self.flip_index_name.append(swap_keypoint)
if [swap_keypoint, keypoint_name] not in self.flip_pairs_name:
self.flip_pairs_name.append([keypoint_name, swap_keypoint])
self.flip_pairs = [[
self.keypoint_name2id[pair[0]], self.keypoint_name2id[pair[1]]
] for pair in self.flip_pairs_name]
self.flip_index = [
self.keypoint_name2id[name] for name in self.flip_index_name
]
self.pose_kpt_color = np.array(self.pose_kpt_color)
@@ -0,0 +1,31 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .builder import DATASETS
@DATASETS.register_module()
class RepeatDataset:
"""A wrapper of repeated dataset.
The length of repeated dataset will be `times` larger than the original
dataset. This is useful when the data loading time is long but the dataset
is small. Using RepeatDataset can reduce the data loading time between
epochs.
Args:
dataset (:obj:`Dataset`): The dataset to be repeated.
times (int): Repeat times.
"""
def __init__(self, dataset, times):
self.dataset = dataset
self.times = times
self._ori_len = len(self.dataset)
def __getitem__(self, idx):
"""Get data."""
return self.dataset[idx % self._ori_len]
def __len__(self):
"""Length after repetition."""
return self.times * self._ori_len
@@ -0,0 +1,45 @@
# Copyright (c) OpenMMLab. All rights reserved.
from ...deprecated import (TopDownFreiHandDataset, TopDownOneHand10KDataset,
TopDownPanopticDataset)
from .animal import (AnimalATRWDataset, AnimalFlyDataset, AnimalHorse10Dataset,
AnimalLocustDataset, AnimalMacaqueDataset,
AnimalPoseDataset, AnimalZebraDataset)
from .body3d import Body3DH36MDataset, Body3DMviewDirectPanopticDataset
from .bottom_up import (BottomUpAicDataset, BottomUpCocoDataset,
BottomUpCocoWholeBodyDataset, BottomUpCrowdPoseDataset,
BottomUpMhpDataset)
from .face import (Face300WDataset, FaceAFLWDataset, FaceCocoWholeBodyDataset,
FaceCOFWDataset, FaceWFLWDataset)
from .fashion import DeepFashionDataset
from .hand import (FreiHandDataset, HandCocoWholeBodyDataset,
InterHand2DDataset, InterHand3DDataset, OneHand10KDataset,
PanopticDataset)
from .mesh import (MeshAdversarialDataset, MeshH36MDataset, MeshMixDataset,
MoshDataset)
from .top_down import (TopDownAicDataset, TopDownCocoDataset,
TopDownCocoWholeBodyDataset, TopDownCrowdPoseDataset,
TopDownH36MDataset, TopDownHalpeDataset,
TopDownJhmdbDataset, TopDownMhpDataset,
TopDownMpiiDataset, TopDownMpiiTrbDataset,
TopDownOCHumanDataset, TopDownPoseTrack18Dataset,
TopDownPoseTrack18VideoDataset)
__all__ = [
'TopDownCocoDataset', 'BottomUpCocoDataset', 'BottomUpMhpDataset',
'BottomUpAicDataset', 'BottomUpCocoWholeBodyDataset', 'TopDownMpiiDataset',
'TopDownMpiiTrbDataset', 'OneHand10KDataset', 'PanopticDataset',
'HandCocoWholeBodyDataset', 'FreiHandDataset', 'InterHand2DDataset',
'InterHand3DDataset', 'TopDownOCHumanDataset', 'TopDownAicDataset',
'TopDownCocoWholeBodyDataset', 'MeshH36MDataset', 'MeshMixDataset',
'MoshDataset', 'MeshAdversarialDataset', 'TopDownCrowdPoseDataset',
'BottomUpCrowdPoseDataset', 'TopDownFreiHandDataset',
'TopDownOneHand10KDataset', 'TopDownPanopticDataset',
'TopDownPoseTrack18Dataset', 'TopDownJhmdbDataset', 'TopDownMhpDataset',
'DeepFashionDataset', 'Face300WDataset', 'FaceAFLWDataset',
'FaceWFLWDataset', 'FaceCOFWDataset', 'FaceCocoWholeBodyDataset',
'Body3DH36MDataset', 'AnimalHorse10Dataset', 'AnimalMacaqueDataset',
'AnimalFlyDataset', 'AnimalLocustDataset', 'AnimalZebraDataset',
'AnimalATRWDataset', 'AnimalPoseDataset', 'TopDownH36MDataset',
'TopDownHalpeDataset', 'TopDownPoseTrack18VideoDataset',
'Body3DMviewDirectPanopticDataset'
]
@@ -0,0 +1,15 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .animal_ap10k_dataset import AnimalAP10KDataset
from .animal_atrw_dataset import AnimalATRWDataset
from .animal_fly_dataset import AnimalFlyDataset
from .animal_horse10_dataset import AnimalHorse10Dataset
from .animal_locust_dataset import AnimalLocustDataset
from .animal_macaque_dataset import AnimalMacaqueDataset
from .animal_pose_dataset import AnimalPoseDataset
from .animal_zebra_dataset import AnimalZebraDataset
__all__ = [
'AnimalHorse10Dataset', 'AnimalMacaqueDataset', 'AnimalFlyDataset',
'AnimalLocustDataset', 'AnimalZebraDataset', 'AnimalATRWDataset',
'AnimalPoseDataset', 'AnimalAP10KDataset'
]
@@ -0,0 +1,367 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict, defaultdict
import json_tricks as json
import numpy as np
from mmcv import Config, deprecated_api_warning
from xtcocotools.cocoeval import COCOeval
from ....core.post_processing import oks_nms, soft_oks_nms
from ...builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class AnimalAP10KDataset(Kpt2dSviewRgbImgTopDownDataset):
"""AP-10K dataset for animal pose estimation.
"AP-10K: A Benchmark for Animal Pose Estimation in the Wild"
Neurips Dataset Track'2021.
More details can be found in the `paper
<https://arxiv.org/abs/2108.12617>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
AP-10K keypoint indexes::
0: 'L_Eye',
1: 'R_Eye',
2: 'Nose',
3: 'Neck',
4: 'root of tail',
5: 'L_Shoulder',
6: 'L_Elbow',
7: 'L_F_Paw',
8: 'R_Shoulder',
9: 'R_Elbow',
10: 'R_F_Paw,
11: 'L_Hip',
12: 'L_Knee',
13: 'L_B_Paw',
14: 'R_Hip',
15: 'R_Knee',
16: 'R_B_Paw'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/ap10k.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
self.use_nms = data_cfg.get('use_nms', True)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.ann_info['use_different_joint_weights'] = False
self.db, self.id2Cat = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
assert self.use_gt_bbox
gt_db, id2Cat = self._load_coco_keypoint_annotations()
return gt_db, id2Cat
def _load_coco_keypoint_annotations(self):
"""Ground truth bbox and keypoints."""
gt_db, id2Cat = [], dict()
for img_id in self.img_ids:
db_tmp, id2Cat_tmp = self._load_coco_keypoint_annotation_kernel(
img_id)
gt_db.extend(db_tmp)
id2Cat.update({img_id: id2Cat_tmp})
return gt_db, id2Cat
def _load_coco_keypoint_annotation_kernel(self, img_id):
"""load annotation from COCOAPI.
Note:
bbox:[x1, y1, w, h]
Args:
img_id: coco image id
Returns:
dict: db entry
"""
img_ann = self.coco.loadImgs(img_id)[0]
width = img_ann['width']
height = img_ann['height']
num_joints = self.ann_info['num_joints']
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
# sanitize bboxes
valid_objs = []
for obj in objs:
if 'bbox' not in obj:
continue
x, y, w, h = obj['bbox']
x1 = max(0, x)
y1 = max(0, y)
x2 = min(width - 1, x1 + max(0, w - 1))
y2 = min(height - 1, y1 + max(0, h - 1))
if ('area' not in obj or obj['area'] > 0) and x2 > x1 and y2 > y1:
obj['clean_bbox'] = [x1, y1, x2 - x1, y2 - y1]
valid_objs.append(obj)
objs = valid_objs
bbox_id = 0
rec = []
id2Cat = []
for obj in objs:
if 'keypoints' not in obj:
continue
if max(obj['keypoints']) == 0:
continue
if 'num_keypoints' in obj and obj['num_keypoints'] == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
center, scale = self._xywh2cs(*obj['clean_bbox'][:4])
image_file = osp.join(self.img_prefix, self.id2name[img_id])
rec.append({
'image_file': image_file,
'center': center,
'scale': scale,
'bbox': obj['clean_bbox'][:4],
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox_score': 1,
'bbox_id': bbox_id
})
category = obj['category_id']
id2Cat.append({
'image_file': image_file,
'bbox_id': bbox_id,
'category': category,
})
bbox_id = bbox_id + 1
return rec, id2Cat
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='mAP', **kwargs):
"""Evaluate coco keypoint results. The pose prediction results will be
saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['data/coco/val2017\
/000000393226.jpg']
- heatmap (np.ndarray[N, K, H, W]): model output heatmap
- bbox_id (list(int)).
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed. Defaults: 'mAP'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['mAP']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = defaultdict(list)
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
cat = self.id2Cat[image_id][bbox_ids[i]]['category']
kpts[image_id].append({
'keypoints': preds[i],
'center': boxes[i][0:2],
'scale': boxes[i][2:4],
'area': boxes[i][4],
'score': boxes[i][5],
'image_id': image_id,
'bbox_id': bbox_ids[i],
'category': cat
})
kpts = self._sort_and_unique_bboxes(kpts)
# rescoring and oks nms
num_joints = self.ann_info['num_joints']
vis_thr = self.vis_thr
oks_thr = self.oks_thr
valid_kpts = []
for image_id in kpts.keys():
img_kpts = kpts[image_id]
for n_p in img_kpts:
box_score = n_p['score']
kpt_score = 0
valid_num = 0
for n_jt in range(0, num_joints):
t_s = n_p['keypoints'][n_jt][2]
if t_s > vis_thr:
kpt_score = kpt_score + t_s
valid_num = valid_num + 1
if valid_num != 0:
kpt_score = kpt_score / valid_num
# rescoring
n_p['score'] = kpt_score * box_score
if self.use_nms:
nms = soft_oks_nms if self.soft_nms else oks_nms
keep = nms(list(img_kpts), oks_thr, sigmas=self.sigmas)
valid_kpts.append([img_kpts[_keep] for _keep in keep])
else:
valid_kpts.append(img_kpts)
self._write_coco_keypoint_results(valid_kpts, res_file)
info_str = self._do_python_keypoint_eval(res_file)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
def _write_coco_keypoint_results(self, keypoints, res_file):
"""Write results into a json file."""
data_pack = [{
'cat_id': self._class_to_coco_ind[cls],
'cls_ind': cls_ind,
'cls': cls,
'ann_type': 'keypoints',
'keypoints': keypoints
} for cls_ind, cls in enumerate(self.classes)
if not cls == '__background__']
results = self._coco_keypoint_results_one_category_kernel(data_pack[0])
with open(res_file, 'w') as f:
json.dump(results, f, sort_keys=True, indent=4)
def _coco_keypoint_results_one_category_kernel(self, data_pack):
"""Get coco keypoint results."""
keypoints = data_pack['keypoints']
cat_results = []
for img_kpts in keypoints:
if len(img_kpts) == 0:
continue
_key_points = np.array(
[img_kpt['keypoints'] for img_kpt in img_kpts])
key_points = _key_points.reshape(-1,
self.ann_info['num_joints'] * 3)
result = [{
'image_id': img_kpt['image_id'],
'category_id': img_kpt['category'],
'keypoints': key_point.tolist(),
'score': float(img_kpt['score']),
'center': img_kpt['center'].tolist(),
'scale': img_kpt['scale'].tolist()
} for img_kpt, key_point in zip(img_kpts, key_points)]
cat_results.extend(result)
return cat_results
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
coco_det = self.coco.loadRes(res_file)
coco_eval = COCOeval(self.coco, coco_det, 'keypoints', self.sigmas)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
stats_names = [
'AP', 'AP .5', 'AP .75', 'AP (M)', 'AP (L)', 'AR', 'AR .5',
'AR .75', 'AR (M)', 'AR (L)'
]
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
def _sort_and_unique_bboxes(self, kpts, key='bbox_id'):
"""sort kpts and remove the repeated ones."""
for img_id, persons in kpts.items():
num = len(persons)
kpts[img_id] = sorted(kpts[img_id], key=lambda x: x[key])
for i in range(num - 1, 0, -1):
if kpts[img_id][i][key] == kpts[img_id][i - 1][key]:
del kpts[img_id][i]
return kpts
@@ -0,0 +1,353 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict, defaultdict
import json_tricks as json
import numpy as np
from mmcv import Config, deprecated_api_warning
from xtcocotools.cocoeval import COCOeval
from ....core.post_processing import oks_nms, soft_oks_nms
from ...builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class AnimalATRWDataset(Kpt2dSviewRgbImgTopDownDataset):
"""ATRW dataset for animal pose estimation.
"ATRW: A Benchmark for Amur Tiger Re-identification in the Wild"
ACM MM'2020.
More details can be found in the `paper
<https://arxiv.org/abs/1906.05586>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
ATRW keypoint indexes::
0: "left_ear",
1: "right_ear",
2: "nose",
3: "right_shoulder",
4: "right_front_paw",
5: "left_shoulder",
6: "left_front_paw",
7: "right_hip",
8: "right_knee",
9: "right_back_paw",
10: "left_hip",
11: "left_knee",
12: "left_back_paw",
13: "tail",
14: "center"
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/atrw.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
self.use_nms = data_cfg.get('use_nms', True)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
assert self.use_gt_bbox
gt_db = self._load_coco_keypoint_annotations()
return gt_db
def _load_coco_keypoint_annotations(self):
"""Ground truth bbox and keypoints."""
gt_db = []
for img_id in self.img_ids:
gt_db.extend(self._load_coco_keypoint_annotation_kernel(img_id))
return gt_db
def _load_coco_keypoint_annotation_kernel(self, img_id):
"""load annotation from COCOAPI.
Note:
bbox:[x1, y1, w, h]
Args:
img_id: coco image id
Returns:
dict: db entry
"""
img_ann = self.coco.loadImgs(img_id)[0]
width = img_ann['width']
height = img_ann['height']
num_joints = self.ann_info['num_joints']
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
# sanitize bboxes
valid_objs = []
for obj in objs:
if 'bbox' not in obj:
continue
x, y, w, h = obj['bbox']
x1 = max(0, x)
y1 = max(0, y)
x2 = min(width - 1, x1 + max(0, w - 1))
y2 = min(height - 1, y1 + max(0, h - 1))
if ('area' not in obj or obj['area'] > 0) and x2 > x1 and y2 > y1:
obj['clean_bbox'] = [x1, y1, x2 - x1, y2 - y1]
valid_objs.append(obj)
objs = valid_objs
bbox_id = 0
rec = []
for obj in objs:
if 'keypoints' not in obj:
continue
if max(obj['keypoints']) == 0:
continue
if 'num_keypoints' in obj and obj['num_keypoints'] == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
center, scale = self._xywh2cs(*obj['clean_bbox'][:4], padding=1.0)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
rec.append({
'image_file': image_file,
'center': center,
'scale': scale,
'bbox': obj['clean_bbox'][:4],
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
return rec
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='mAP', **kwargs):
"""Evaluate coco keypoint results. The pose prediction results will be
saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['data/coco/val2017\
/000000393226.jpg']
- heatmap (np.ndarray[N, K, H, W]): model output heatmap
- bbox_id (list(int)).
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed. Defaults: 'mAP'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['mAP']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = defaultdict(list)
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts[image_id].append({
'keypoints': preds[i],
'center': boxes[i][0:2],
'scale': boxes[i][2:4],
'area': boxes[i][4],
'score': boxes[i][5],
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
# rescoring and oks nms
num_joints = self.ann_info['num_joints']
vis_thr = self.vis_thr
oks_thr = self.oks_thr
valid_kpts = []
for image_id in kpts.keys():
img_kpts = kpts[image_id]
for n_p in img_kpts:
box_score = n_p['score']
kpt_score = 0
valid_num = 0
for n_jt in range(0, num_joints):
t_s = n_p['keypoints'][n_jt][2]
if t_s > vis_thr:
kpt_score = kpt_score + t_s
valid_num = valid_num + 1
if valid_num != 0:
kpt_score = kpt_score / valid_num
# rescoring
n_p['score'] = kpt_score * box_score
if self.use_nms:
nms = soft_oks_nms if self.soft_nms else oks_nms
keep = nms(list(img_kpts), oks_thr, sigmas=self.sigmas)
valid_kpts.append([img_kpts[_keep] for _keep in keep])
else:
valid_kpts.append(img_kpts)
self._write_coco_keypoint_results(valid_kpts, res_file)
info_str = self._do_python_keypoint_eval(res_file)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
def _write_coco_keypoint_results(self, keypoints, res_file):
"""Write results into a json file."""
data_pack = [{
'cat_id': self._class_to_coco_ind[cls],
'cls_ind': cls_ind,
'cls': cls,
'ann_type': 'keypoints',
'keypoints': keypoints
} for cls_ind, cls in enumerate(self.classes)
if not cls == '__background__']
results = self._coco_keypoint_results_one_category_kernel(data_pack[0])
with open(res_file, 'w') as f:
json.dump(results, f, sort_keys=True, indent=4)
def _coco_keypoint_results_one_category_kernel(self, data_pack):
"""Get coco keypoint results."""
cat_id = data_pack['cat_id']
keypoints = data_pack['keypoints']
cat_results = []
for img_kpts in keypoints:
if len(img_kpts) == 0:
continue
_key_points = np.array(
[img_kpt['keypoints'] for img_kpt in img_kpts])
key_points = _key_points.reshape(-1,
self.ann_info['num_joints'] * 3)
result = [{
'image_id': img_kpt['image_id'],
'category_id': cat_id,
'keypoints': key_point.tolist(),
'score': float(img_kpt['score']),
'center': img_kpt['center'].tolist(),
'scale': img_kpt['scale'].tolist()
} for img_kpt, key_point in zip(img_kpts, key_points)]
cat_results.extend(result)
return cat_results
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
coco_det = self.coco.loadRes(res_file)
coco_eval = COCOeval(self.coco, coco_det, 'keypoints', self.sigmas)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
stats_names = [
'AP', 'AP .5', 'AP .75', 'AP (M)', 'AP (L)', 'AR', 'AR .5',
'AR .75', 'AR (M)', 'AR (L)'
]
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
def _sort_and_unique_bboxes(self, kpts, key='bbox_id'):
"""sort kpts and remove the repeated ones."""
for img_id, persons in kpts.items():
num = len(persons)
kpts[img_id] = sorted(kpts[img_id], key=lambda x: x[key])
for i in range(num - 1, 0, -1):
if kpts[img_id][i][key] == kpts[img_id][i - 1][key]:
del kpts[img_id][i]
return kpts
@@ -0,0 +1,16 @@
# Copyright (c) OpenMMLab. All rights reserved.
from abc import ABCMeta
from torch.utils.data import Dataset
class AnimalBaseDataset(Dataset, metaclass=ABCMeta):
"""This class has been deprecated and replaced by
Kpt2dSviewRgbImgTopDownDataset."""
def __init__(self, *args, **kwargs):
raise (ImportError(
'AnimalBaseDataset has been replaced by '
'Kpt2dSviewRgbImgTopDownDataset,'
'check https://github.com/open-mmlab/mmpose/pull/663 for details.')
)
@@ -0,0 +1,215 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from ...builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class AnimalFlyDataset(Kpt2dSviewRgbImgTopDownDataset):
"""AnimalFlyDataset for animal pose estimation.
"Fast animal pose estimation using deep neural networks"
Nature methods'2019. More details can be found in the `paper
<https://www.biorxiv.org/content/biorxiv/\
early/2018/05/25/331181.full.pdf>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
Vinegar Fly keypoint indexes::
0: "head",
1: "eyeL",
2: "eyeR",
3: "neck",
4: "thorax",
5: "abdomen",
6: "forelegR1",
7: "forelegR2",
8: "forelegR3",
9: "forelegR4",
10: "midlegR1",
11: "midlegR2",
12: "midlegR3",
13: "midlegR4",
14: "hindlegR1",
15: "hindlegR2",
16: "hindlegR3",
17: "hindlegR4",
18: "forelegL1",
19: "forelegL2",
20: "forelegL3",
21: "forelegL4",
22: "midlegL1",
23: "midlegL2",
24: "midlegL3",
25: "midlegL4",
26: "hindlegL1",
27: "hindlegL2",
28: "hindlegL3",
29: "hindlegL4",
30: "wingL",
31: "wingR"
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/fly.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
# the ori image is 192x192
center, scale = self._xywh2cs(0, 0, 192, 192, 0.8)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCK', **kwargs):
"""Evaluate Fly keypoint results. The pose prediction results will be
saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['Test/source/0.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str): Path of directory to save the results.
metric (str | list[str]): Metric to be performed.
Options: 'PCK', 'AUC', 'EPE'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCK', 'AUC', 'EPE']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,220 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from ...builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class AnimalHorse10Dataset(Kpt2dSviewRgbImgTopDownDataset):
"""AnimalHorse10Dataset for animal pose estimation.
"Pretraining boosts out-of-domain robustness for pose estimation"
WACV'2021. More details can be found in the `paper
<https://arxiv.org/pdf/1909.11229.pdf>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
Horse-10 keypoint indexes::
0: 'Nose',
1: 'Eye',
2: 'Nearknee',
3: 'Nearfrontfetlock',
4: 'Nearfrontfoot',
5: 'Offknee',
6: 'Offfrontfetlock',
7: 'Offfrontfoot',
8: 'Shoulder',
9: 'Midshoulder',
10: 'Elbow',
11: 'Girth',
12: 'Wither',
13: 'Nearhindhock',
14: 'Nearhindfetlock',
15: 'Nearhindfoot',
16: 'Hip',
17: 'Stifle',
18: 'Offhindhock',
19: 'Offhindfetlock',
20: 'Offhindfoot',
21: 'Ischium'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/horse10.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
# use 1.25 padded bbox as input
center, scale = self._xywh2cs(*obj['bbox'][:4], 1.25)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
def _get_normalize_factor(self, gts):
"""Get inter-ocular distance as the normalize factor, measured as the
Euclidean distance between the outer corners of the eyes.
Args:
gts (np.ndarray[N, K, 2]): Groundtruth keypoint location.
Returns:
np.ndarray[N, 2]: normalized factor
"""
interocular = np.linalg.norm(
gts[:, 0, :] - gts[:, 1, :], axis=1, keepdims=True)
return np.tile(interocular, [1, 2])
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCK', **kwargs):
"""Evaluate horse-10 keypoint results. The pose prediction results will
be saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['Test/source/0.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'PCK', 'NME'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCK', 'NME']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,218 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from ...builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class AnimalLocustDataset(Kpt2dSviewRgbImgTopDownDataset):
"""AnimalLocustDataset for animal pose estimation.
"DeepPoseKit, a software toolkit for fast and robust animal
pose estimation using deep learning" Elife'2019.
More details can be found in the paper.
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
Desert Locust keypoint indexes::
0: "head",
1: "neck",
2: "thorax",
3: "abdomen1",
4: "abdomen2",
5: "anttipL",
6: "antbaseL",
7: "eyeL",
8: "forelegL1",
9: "forelegL2",
10: "forelegL3",
11: "forelegL4",
12: "midlegL1",
13: "midlegL2",
14: "midlegL3",
15: "midlegL4",
16: "hindlegL1",
17: "hindlegL2",
18: "hindlegL3",
19: "hindlegL4",
20: "anttipR",
21: "antbaseR",
22: "eyeR",
23: "forelegR1",
24: "forelegR2",
25: "forelegR3",
26: "forelegR4",
27: "midlegR1",
28: "midlegR2",
29: "midlegR3",
30: "midlegR4",
31: "hindlegR1",
32: "hindlegR2",
33: "hindlegR3",
34: "hindlegR4"
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/locust.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
# the ori image is 160x160
center, scale = self._xywh2cs(0, 0, 160, 160, 0.8)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCK', **kwargs):
"""Evaluate Fly keypoint results. The pose prediction results will be
saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['Test/source/0.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'PCK', 'AUC', 'EPE'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCK', 'AUC', 'EPE']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,355 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict, defaultdict
import json_tricks as json
import numpy as np
from mmcv import Config, deprecated_api_warning
from xtcocotools.cocoeval import COCOeval
from ....core.post_processing import oks_nms, soft_oks_nms
from ...builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class AnimalMacaqueDataset(Kpt2dSviewRgbImgTopDownDataset):
"""MacaquePose dataset for animal pose estimation.
"MacaquePose: A novel ‘in the wild’ macaque monkey pose dataset
for markerless motion capture" bioRxiv'2020.
More details can be found in the `paper
<https://www.biorxiv.org/content/10.1101/2020.07.30.229989v1>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
Macaque keypoint indexes::
0: 'nose',
1: 'left_eye',
2: 'right_eye',
3: 'left_ear',
4: 'right_ear',
5: 'left_shoulder',
6: 'right_shoulder',
7: 'left_elbow',
8: 'right_elbow',
9: 'left_wrist',
10: 'right_wrist',
11: 'left_hip',
12: 'right_hip',
13: 'left_knee',
14: 'right_knee',
15: 'left_ankle',
16: 'right_ankle'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/macaque.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
self.use_nms = data_cfg.get('use_nms', True)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
assert self.use_gt_bbox
gt_db = self._load_coco_keypoint_annotations()
return gt_db
def _load_coco_keypoint_annotations(self):
"""Ground truth bbox and keypoints."""
gt_db = []
for img_id in self.img_ids:
gt_db.extend(self._load_coco_keypoint_annotation_kernel(img_id))
return gt_db
def _load_coco_keypoint_annotation_kernel(self, img_id):
"""load annotation from COCOAPI.
Note:
bbox:[x1, y1, w, h]
Args:
img_id: coco image id
Returns:
dict: db entry
"""
img_ann = self.coco.loadImgs(img_id)[0]
width = img_ann['width']
height = img_ann['height']
num_joints = self.ann_info['num_joints']
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
# sanitize bboxes
valid_objs = []
for obj in objs:
if 'bbox' not in obj:
continue
x, y, w, h = obj['bbox']
x1 = max(0, x)
y1 = max(0, y)
x2 = min(width - 1, x1 + max(0, w - 1))
y2 = min(height - 1, y1 + max(0, h - 1))
if ('area' not in obj or obj['area'] > 0) and x2 > x1 and y2 > y1:
obj['clean_bbox'] = [x1, y1, x2 - x1, y2 - y1]
valid_objs.append(obj)
objs = valid_objs
bbox_id = 0
rec = []
for obj in objs:
if 'keypoints' not in obj:
continue
if max(obj['keypoints']) == 0:
continue
if 'num_keypoints' in obj and obj['num_keypoints'] == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
center, scale = self._xywh2cs(*obj['clean_bbox'][:4])
image_file = osp.join(self.img_prefix, self.id2name[img_id])
rec.append({
'image_file': image_file,
'center': center,
'scale': scale,
'bbox': obj['clean_bbox'][:4],
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
return rec
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='mAP', **kwargs):
"""Evaluate coco keypoint results. The pose prediction results will be
saved in ``${res_folder}/result_keypoints.json``.
Note:
batch_size: N
num_keypoints: K
heatmap height: H
heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['data/coco/val2017\
/000000393226.jpg']
- heatmap (np.ndarray[N, K, H, W]): model output heatmap
- bbox_id (list(int)).
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed. Defaults: 'mAP'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['mAP']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = defaultdict(list)
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts[image_id].append({
'keypoints': preds[i],
'center': boxes[i][0:2],
'scale': boxes[i][2:4],
'area': boxes[i][4],
'score': boxes[i][5],
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
# rescoring and oks nms
num_joints = self.ann_info['num_joints']
vis_thr = self.vis_thr
oks_thr = self.oks_thr
valid_kpts = []
for image_id in kpts.keys():
img_kpts = kpts[image_id]
for n_p in img_kpts:
box_score = n_p['score']
kpt_score = 0
valid_num = 0
for n_jt in range(0, num_joints):
t_s = n_p['keypoints'][n_jt][2]
if t_s > vis_thr:
kpt_score = kpt_score + t_s
valid_num = valid_num + 1
if valid_num != 0:
kpt_score = kpt_score / valid_num
# rescoring
n_p['score'] = kpt_score * box_score
if self.use_nms:
nms = soft_oks_nms if self.soft_nms else oks_nms
keep = nms(list(img_kpts), oks_thr, sigmas=self.sigmas)
valid_kpts.append([img_kpts[_keep] for _keep in keep])
else:
valid_kpts.append(img_kpts)
self._write_coco_keypoint_results(valid_kpts, res_file)
info_str = self._do_python_keypoint_eval(res_file)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
def _write_coco_keypoint_results(self, keypoints, res_file):
"""Write results into a json file."""
data_pack = [{
'cat_id': self._class_to_coco_ind[cls],
'cls_ind': cls_ind,
'cls': cls,
'ann_type': 'keypoints',
'keypoints': keypoints
} for cls_ind, cls in enumerate(self.classes)
if not cls == '__background__']
results = self._coco_keypoint_results_one_category_kernel(data_pack[0])
with open(res_file, 'w') as f:
json.dump(results, f, sort_keys=True, indent=4)
def _coco_keypoint_results_one_category_kernel(self, data_pack):
"""Get coco keypoint results."""
cat_id = data_pack['cat_id']
keypoints = data_pack['keypoints']
cat_results = []
for img_kpts in keypoints:
if len(img_kpts) == 0:
continue
_key_points = np.array(
[img_kpt['keypoints'] for img_kpt in img_kpts])
key_points = _key_points.reshape(-1,
self.ann_info['num_joints'] * 3)
result = [{
'image_id': img_kpt['image_id'],
'category_id': cat_id,
'keypoints': key_point.tolist(),
'score': float(img_kpt['score']),
'center': img_kpt['center'].tolist(),
'scale': img_kpt['scale'].tolist()
} for img_kpt, key_point in zip(img_kpts, key_points)]
cat_results.extend(result)
return cat_results
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
coco_det = self.coco.loadRes(res_file)
coco_eval = COCOeval(self.coco, coco_det, 'keypoints', self.sigmas)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
stats_names = [
'AP', 'AP .5', 'AP .75', 'AP (M)', 'AP (L)', 'AR', 'AR .5',
'AR .75', 'AR (M)', 'AR (L)'
]
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
def _sort_and_unique_bboxes(self, kpts, key='bbox_id'):
"""sort kpts and remove the repeated ones."""
for img_id, persons in kpts.items():
num = len(persons)
kpts[img_id] = sorted(kpts[img_id], key=lambda x: x[key])
for i in range(num - 1, 0, -1):
if kpts[img_id][i][key] == kpts[img_id][i - 1][key]:
del kpts[img_id][i]
return kpts
@@ -0,0 +1,359 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict, defaultdict
import json_tricks as json
import numpy as np
from mmcv import Config, deprecated_api_warning
from xtcocotools.cocoeval import COCOeval
from ....core.post_processing import oks_nms, soft_oks_nms
from ...builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class AnimalPoseDataset(Kpt2dSviewRgbImgTopDownDataset):
"""Animal-Pose dataset for animal pose estimation.
"Cross-domain Adaptation For Animal Pose Estimation" ICCV'2019
More details can be found in the `paper
<https://arxiv.org/abs/1908.05806>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
Animal-Pose keypoint indexes::
0: 'L_Eye',
1: 'R_Eye',
2: 'L_EarBase',
3: 'R_EarBase',
4: 'Nose',
5: 'Throat',
6: 'TailBase',
7: 'Withers',
8: 'L_F_Elbow',
9: 'R_F_Elbow',
10: 'L_B_Elbow',
11: 'R_B_Elbow',
12: 'L_F_Knee',
13: 'R_F_Knee',
14: 'L_B_Knee',
15: 'R_B_Knee',
16: 'L_F_Paw',
17: 'R_F_Paw',
18: 'L_B_Paw',
19: 'R_B_Paw'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/animalpose.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
self.use_nms = data_cfg.get('use_nms', True)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
assert self.use_gt_bbox
gt_db = self._load_coco_keypoint_annotations()
return gt_db
def _load_coco_keypoint_annotations(self):
"""Ground truth bbox and keypoints."""
gt_db = []
for img_id in self.img_ids:
gt_db.extend(self._load_coco_keypoint_annotation_kernel(img_id))
return gt_db
def _load_coco_keypoint_annotation_kernel(self, img_id):
"""load annotation from COCOAPI.
Note:
bbox:[x1, y1, w, h]
Args:
img_id: coco image id
Returns:
dict: db entry
"""
img_ann = self.coco.loadImgs(img_id)[0]
width = img_ann['width']
height = img_ann['height']
num_joints = self.ann_info['num_joints']
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
# sanitize bboxes
valid_objs = []
for obj in objs:
if 'bbox' not in obj:
continue
x, y, w, h = obj['bbox']
x1 = max(0, x)
y1 = max(0, y)
x2 = min(width - 1, x1 + max(0, w - 1))
y2 = min(height - 1, y1 + max(0, h - 1))
if ('area' not in obj or obj['area'] > 0) and x2 > x1 and y2 > y1:
obj['clean_bbox'] = [x1, y1, x2 - x1, y2 - y1]
valid_objs.append(obj)
objs = valid_objs
bbox_id = 0
rec = []
for obj in objs:
if 'keypoints' not in obj:
continue
if max(obj['keypoints']) == 0:
continue
if 'num_keypoints' in obj and obj['num_keypoints'] == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
center, scale = self._xywh2cs(*obj['clean_bbox'][:4])
image_file = osp.join(self.img_prefix, self.id2name[img_id])
rec.append({
'image_file': image_file,
'center': center,
'scale': scale,
'bbox': obj['clean_bbox'][:4],
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
return rec
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='mAP', **kwargs):
"""Evaluate coco keypoint results. The pose prediction results will be
saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['data/coco/val2017\
/000000393226.jpg']
- heatmap (np.ndarray[N, K, H, W]): model output heatmap
- bbox_id (list(int)).
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed. Defaults: 'mAP'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['mAP']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = defaultdict(list)
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts[image_id].append({
'keypoints': preds[i],
'center': boxes[i][0:2],
'scale': boxes[i][2:4],
'area': boxes[i][4],
'score': boxes[i][5],
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
# rescoring and oks nms
num_joints = self.ann_info['num_joints']
vis_thr = self.vis_thr
oks_thr = self.oks_thr
valid_kpts = []
for image_id in kpts.keys():
img_kpts = kpts[image_id]
for n_p in img_kpts:
box_score = n_p['score']
kpt_score = 0
valid_num = 0
for n_jt in range(0, num_joints):
t_s = n_p['keypoints'][n_jt][2]
if t_s > vis_thr:
kpt_score = kpt_score + t_s
valid_num = valid_num + 1
if valid_num != 0:
kpt_score = kpt_score / valid_num
# rescoring
n_p['score'] = kpt_score * box_score
if self.use_nms:
nms = soft_oks_nms if self.soft_nms else oks_nms
keep = nms(list(img_kpts), oks_thr, sigmas=self.sigmas)
valid_kpts.append([img_kpts[_keep] for _keep in keep])
else:
valid_kpts.append(img_kpts)
self._write_coco_keypoint_results(valid_kpts, res_file)
info_str = self._do_python_keypoint_eval(res_file)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
def _write_coco_keypoint_results(self, keypoints, res_file):
"""Write results into a json file."""
data_pack = [{
'cat_id': self._class_to_coco_ind[cls],
'cls_ind': cls_ind,
'cls': cls,
'ann_type': 'keypoints',
'keypoints': keypoints
} for cls_ind, cls in enumerate(self.classes)
if not cls == '__background__']
results = self._coco_keypoint_results_one_category_kernel(data_pack[0])
with open(res_file, 'w') as f:
json.dump(results, f, sort_keys=True, indent=4)
def _coco_keypoint_results_one_category_kernel(self, data_pack):
"""Get coco keypoint results."""
cat_id = data_pack['cat_id']
keypoints = data_pack['keypoints']
cat_results = []
for img_kpts in keypoints:
if len(img_kpts) == 0:
continue
_key_points = np.array(
[img_kpt['keypoints'] for img_kpt in img_kpts])
key_points = _key_points.reshape(-1,
self.ann_info['num_joints'] * 3)
result = [{
'image_id': img_kpt['image_id'],
'category_id': cat_id,
'keypoints': key_point.tolist(),
'score': float(img_kpt['score']),
'center': img_kpt['center'].tolist(),
'scale': img_kpt['scale'].tolist()
} for img_kpt, key_point in zip(img_kpts, key_points)]
cat_results.extend(result)
return cat_results
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
coco_det = self.coco.loadRes(res_file)
coco_eval = COCOeval(self.coco, coco_det, 'keypoints', self.sigmas)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
stats_names = [
'AP', 'AP .5', 'AP .75', 'AP (M)', 'AP (L)', 'AR', 'AR .5',
'AR .75', 'AR (M)', 'AR (L)'
]
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
def _sort_and_unique_bboxes(self, kpts, key='bbox_id'):
"""sort kpts and remove the repeated ones."""
for img_id, persons in kpts.items():
num = len(persons)
kpts[img_id] = sorted(kpts[img_id], key=lambda x: x[key])
for i in range(num - 1, 0, -1):
if kpts[img_id][i][key] == kpts[img_id][i - 1][key]:
del kpts[img_id][i]
return kpts
@@ -0,0 +1,193 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from ...builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class AnimalZebraDataset(Kpt2dSviewRgbImgTopDownDataset):
"""AnimalZebraDataset for animal pose estimation.
"DeepPoseKit, a software toolkit for fast and robust animal
pose estimation using deep learning" Elife'2019.
More details can be found in the paper.
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
Desert Locust keypoint indexes::
0: "snout",
1: "head",
2: "neck",
3: "forelegL1",
4: "forelegR1",
5: "hindlegL1",
6: "hindlegR1",
7: "tailbase",
8: "tailtip"
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/zebra.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
# the ori image is 160x160
center, scale = self._xywh2cs(0, 0, 160, 160, 0.8)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCK', **kwargs):
"""Evaluate Fly keypoint results. The pose prediction results will be
saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['Test/source/0.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'PCK', 'AUC', 'EPE'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCK', 'AUC', 'EPE']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,17 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .kpt_2d_sview_rgb_img_bottom_up_dataset import \
Kpt2dSviewRgbImgBottomUpDataset
from .kpt_2d_sview_rgb_img_top_down_dataset import \
Kpt2dSviewRgbImgTopDownDataset
from .kpt_2d_sview_rgb_vid_top_down_dataset import \
Kpt2dSviewRgbVidTopDownDataset
from .kpt_3d_mview_rgb_img_direct_dataset import Kpt3dMviewRgbImgDirectDataset
from .kpt_3d_sview_kpt_2d_dataset import Kpt3dSviewKpt2dDataset
from .kpt_3d_sview_rgb_img_top_down_dataset import \
Kpt3dSviewRgbImgTopDownDataset
__all__ = [
'Kpt3dMviewRgbImgDirectDataset', 'Kpt2dSviewRgbImgTopDownDataset',
'Kpt3dSviewRgbImgTopDownDataset', 'Kpt2dSviewRgbImgBottomUpDataset',
'Kpt3dSviewKpt2dDataset', 'Kpt2dSviewRgbVidTopDownDataset'
]
@@ -0,0 +1,188 @@
# Copyright (c) OpenMMLab. All rights reserved.
import copy
from abc import ABCMeta, abstractmethod
import numpy as np
import xtcocotools
from torch.utils.data import Dataset
from xtcocotools.coco import COCO
from mmpose.datasets import DatasetInfo
from mmpose.datasets.pipelines import Compose
class Kpt2dSviewRgbImgBottomUpDataset(Dataset, metaclass=ABCMeta):
"""Base class for bottom-up datasets.
All datasets should subclass it.
All subclasses should overwrite:
Methods:`_get_single`
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
coco_style (bool): Whether the annotation json is coco-style.
Default: True
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
coco_style=True,
test_mode=False):
self.image_info = {}
self.ann_info = {}
self.ann_file = ann_file
self.img_prefix = img_prefix
self.pipeline = pipeline
self.test_mode = test_mode
# bottom-up
self.base_size = data_cfg['base_size']
self.base_sigma = data_cfg['base_sigma']
self.int_sigma = False
self.ann_info['image_size'] = np.array(data_cfg['image_size'])
self.ann_info['heatmap_size'] = np.array(data_cfg['heatmap_size'])
self.ann_info['num_joints'] = data_cfg['num_joints']
self.ann_info['num_scales'] = data_cfg['num_scales']
self.ann_info['scale_aware_sigma'] = data_cfg['scale_aware_sigma']
self.ann_info['inference_channel'] = data_cfg['inference_channel']
self.ann_info['dataset_channel'] = data_cfg['dataset_channel']
self.use_nms = data_cfg.get('use_nms', False)
self.soft_nms = data_cfg.get('soft_nms', True)
self.oks_thr = data_cfg.get('oks_thr', 0.9)
if dataset_info is None:
raise ValueError(
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.')
dataset_info = DatasetInfo(dataset_info)
assert self.ann_info['num_joints'] == dataset_info.keypoint_num
self.ann_info['flip_pairs'] = dataset_info.flip_pairs
self.ann_info['flip_index'] = dataset_info.flip_index
self.ann_info['upper_body_ids'] = dataset_info.upper_body_ids
self.ann_info['lower_body_ids'] = dataset_info.lower_body_ids
self.ann_info['joint_weights'] = dataset_info.joint_weights
self.ann_info['skeleton'] = dataset_info.skeleton
self.sigmas = dataset_info.sigmas
self.dataset_name = dataset_info.dataset_name
if coco_style:
self.coco = COCO(ann_file)
if 'categories' in self.coco.dataset:
cats = [
cat['name']
for cat in self.coco.loadCats(self.coco.getCatIds())
]
self.classes = ['__background__'] + cats
self.num_classes = len(self.classes)
self._class_to_ind = dict(
zip(self.classes, range(self.num_classes)))
self._class_to_coco_ind = dict(
zip(cats, self.coco.getCatIds()))
self._coco_ind_to_class_ind = dict(
(self._class_to_coco_ind[cls], self._class_to_ind[cls])
for cls in self.classes[1:])
self.img_ids = self.coco.getImgIds()
if not test_mode:
self.img_ids = [
img_id for img_id in self.img_ids if
len(self.coco.getAnnIds(imgIds=img_id, iscrowd=None)) > 0
]
self.num_images = len(self.img_ids)
self.id2name, self.name2id = self._get_mapping_id_name(
self.coco.imgs)
self.pipeline = Compose(self.pipeline)
@staticmethod
def _get_mapping_id_name(imgs):
"""
Args:
imgs (dict): dict of image info.
Returns:
tuple: Image name & id mapping dicts.
- id2name (dict): Mapping image id to name.
- name2id (dict): Mapping image name to id.
"""
id2name = {}
name2id = {}
for image_id, image in imgs.items():
file_name = image['file_name']
id2name[image_id] = file_name
name2id[file_name] = image_id
return id2name, name2id
def _get_mask(self, anno, idx):
"""Get ignore masks to mask out losses."""
coco = self.coco
img_info = coco.loadImgs(self.img_ids[idx])[0]
m = np.zeros((img_info['height'], img_info['width']), dtype=np.float32)
for obj in anno:
if 'segmentation' in obj:
if obj['iscrowd']:
rle = xtcocotools.mask.frPyObjects(obj['segmentation'],
img_info['height'],
img_info['width'])
m += xtcocotools.mask.decode(rle)
elif obj['num_keypoints'] == 0:
rles = xtcocotools.mask.frPyObjects(
obj['segmentation'], img_info['height'],
img_info['width'])
for rle in rles:
m += xtcocotools.mask.decode(rle)
return m < 0.5
@abstractmethod
def _get_single(self, idx):
"""Get anno for a single image."""
raise NotImplementedError
@abstractmethod
def evaluate(self, results, *args, **kwargs):
"""Evaluate keypoint results."""
def prepare_train_img(self, idx):
"""Prepare image for training given the index."""
results = copy.deepcopy(self._get_single(idx))
results['ann_info'] = self.ann_info
return self.pipeline(results)
def prepare_test_img(self, idx):
"""Prepare image for testing given the index."""
results = copy.deepcopy(self._get_single(idx))
results['ann_info'] = self.ann_info
return self.pipeline(results)
def __len__(self):
"""Get dataset length."""
return len(self.img_ids)
def __getitem__(self, idx):
"""Get the sample for either training or testing given index."""
if self.test_mode:
return self.prepare_test_img(idx)
return self.prepare_train_img(idx)
@@ -0,0 +1,287 @@
# Copyright (c) OpenMMLab. All rights reserved.
import copy
from abc import ABCMeta, abstractmethod
import json_tricks as json
import numpy as np
from torch.utils.data import Dataset
from xtcocotools.coco import COCO
from mmpose.core.evaluation.top_down_eval import (keypoint_auc, keypoint_epe,
keypoint_nme,
keypoint_pck_accuracy)
from mmpose.datasets import DatasetInfo
from mmpose.datasets.pipelines import Compose
class Kpt2dSviewRgbImgTopDownDataset(Dataset, metaclass=ABCMeta):
"""Base class for keypoint 2D top-down pose estimation with single-view RGB
image as the input.
All fashion datasets should subclass it.
All subclasses should overwrite:
Methods:`_get_db`, 'evaluate'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
coco_style (bool): Whether the annotation json is coco-style.
Default: True
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
coco_style=True,
test_mode=False):
self.image_info = {}
self.ann_info = {}
self.ann_file = ann_file
self.img_prefix = img_prefix
self.pipeline = pipeline
self.test_mode = test_mode
self.ann_info['image_size'] = np.array(data_cfg['image_size'])
self.ann_info['heatmap_size'] = np.array(data_cfg['heatmap_size'])
self.ann_info['num_joints'] = data_cfg['num_joints']
self.ann_info['inference_channel'] = data_cfg['inference_channel']
self.ann_info['num_output_channels'] = data_cfg['num_output_channels']
self.ann_info['dataset_channel'] = data_cfg['dataset_channel']
self.ann_info['max_num_joints'] = data_cfg.get('max_num_joints', None)
self.ann_info['dataset_idx'] = data_cfg.get('dataset_idx', 0)
self.ann_info['use_different_joint_weights'] = data_cfg.get(
'use_different_joint_weights', False)
if dataset_info is None:
raise ValueError(
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.')
dataset_info = DatasetInfo(dataset_info)
assert self.ann_info['num_joints'] == dataset_info.keypoint_num
self.ann_info['flip_pairs'] = dataset_info.flip_pairs
self.ann_info['flip_index'] = dataset_info.flip_index
self.ann_info['upper_body_ids'] = dataset_info.upper_body_ids
self.ann_info['lower_body_ids'] = dataset_info.lower_body_ids
self.ann_info['joint_weights'] = dataset_info.joint_weights
self.ann_info['skeleton'] = dataset_info.skeleton
self.sigmas = dataset_info.sigmas
self.dataset_name = dataset_info.dataset_name
if coco_style:
self.coco = COCO(ann_file)
if 'categories' in self.coco.dataset:
cats = [
cat['name']
for cat in self.coco.loadCats(self.coco.getCatIds())
]
self.classes = ['__background__'] + cats
self.num_classes = len(self.classes)
self._class_to_ind = dict(
zip(self.classes, range(self.num_classes)))
self._class_to_coco_ind = dict(
zip(cats, self.coco.getCatIds()))
self._coco_ind_to_class_ind = dict(
(self._class_to_coco_ind[cls], self._class_to_ind[cls])
for cls in self.classes[1:])
self.img_ids = self.coco.getImgIds()
self.num_images = len(self.img_ids)
self.id2name, self.name2id = self._get_mapping_id_name(
self.coco.imgs)
self.db = []
self.pipeline = Compose(self.pipeline)
@staticmethod
def _get_mapping_id_name(imgs):
"""
Args:
imgs (dict): dict of image info.
Returns:
tuple: Image name & id mapping dicts.
- id2name (dict): Mapping image id to name.
- name2id (dict): Mapping image name to id.
"""
id2name = {}
name2id = {}
for image_id, image in imgs.items():
file_name = image['file_name']
id2name[image_id] = file_name
name2id[file_name] = image_id
return id2name, name2id
def _xywh2cs(self, x, y, w, h, padding=1.25):
"""This encodes bbox(x,y,w,h) into (center, scale)
Args:
x, y, w, h (float): left, top, width and height
padding (float): bounding box padding factor
Returns:
center (np.ndarray[float32](2,)): center of the bbox (x, y).
scale (np.ndarray[float32](2,)): scale of the bbox w & h.
"""
aspect_ratio = self.ann_info['image_size'][0] / self.ann_info[
'image_size'][1]
center = np.array([x + w * 0.5, y + h * 0.5], dtype=np.float32)
if (not self.test_mode) and np.random.rand() < 0.3:
center += 0.4 * (np.random.rand(2) - 0.5) * [w, h]
if w > aspect_ratio * h:
h = w * 1.0 / aspect_ratio
elif w < aspect_ratio * h:
w = h * aspect_ratio
# pixel std is 200.0
scale = np.array([w / 200.0, h / 200.0], dtype=np.float32)
# padding to include proper amount of context
scale = scale * padding
return center, scale
def _get_normalize_factor(self, gts, *args, **kwargs):
"""Get the normalize factor. generally inter-ocular distance measured
as the Euclidean distance between the outer corners of the eyes is
used. This function should be overrode, to measure NME.
Args:
gts (np.ndarray[N, K, 2]): Groundtruth keypoint location.
Returns:
np.ndarray[N, 2]: normalized factor
"""
return np.ones([gts.shape[0], 2], dtype=np.float32)
@abstractmethod
def _get_db(self):
"""Load dataset."""
raise NotImplementedError
@abstractmethod
def evaluate(self, results, *args, **kwargs):
"""Evaluate keypoint results."""
@staticmethod
def _write_keypoint_results(keypoints, res_file):
"""Write results into a json file."""
with open(res_file, 'w') as f:
json.dump(keypoints, f, sort_keys=True, indent=4)
def _report_metric(self,
res_file,
metrics,
pck_thr=0.2,
pckh_thr=0.7,
auc_nor=30):
"""Keypoint evaluation.
Args:
res_file (str): Json file stored prediction results.
metrics (str | list[str]): Metric to be performed.
Options: 'PCK', 'PCKh', 'AUC', 'EPE', 'NME'.
pck_thr (float): PCK threshold, default as 0.2.
pckh_thr (float): PCKh threshold, default as 0.7.
auc_nor (float): AUC normalization factor, default as 30 pixel.
Returns:
List: Evaluation results for evaluation metric.
"""
info_str = []
with open(res_file, 'r') as fin:
preds = json.load(fin)
assert len(preds) == len(self.db)
outputs = []
gts = []
masks = []
box_sizes = []
threshold_bbox = []
threshold_head_box = []
for pred, item in zip(preds, self.db):
outputs.append(np.array(pred['keypoints'])[:, :-1])
gts.append(np.array(item['joints_3d'])[:, :-1])
masks.append((np.array(item['joints_3d_visible'])[:, 0]) > 0)
if 'PCK' in metrics:
bbox = np.array(item['bbox'])
bbox_thr = np.max(bbox[2:])
threshold_bbox.append(np.array([bbox_thr, bbox_thr]))
if 'PCKh' in metrics:
head_box_thr = item['head_size']
threshold_head_box.append(
np.array([head_box_thr, head_box_thr]))
box_sizes.append(item.get('box_size', 1))
outputs = np.array(outputs)
gts = np.array(gts)
masks = np.array(masks)
threshold_bbox = np.array(threshold_bbox)
threshold_head_box = np.array(threshold_head_box)
box_sizes = np.array(box_sizes).reshape([-1, 1])
if 'PCK' in metrics:
_, pck, _ = keypoint_pck_accuracy(outputs, gts, masks, pck_thr,
threshold_bbox)
info_str.append(('PCK', pck))
if 'PCKh' in metrics:
_, pckh, _ = keypoint_pck_accuracy(outputs, gts, masks, pckh_thr,
threshold_head_box)
info_str.append(('PCKh', pckh))
if 'AUC' in metrics:
info_str.append(('AUC', keypoint_auc(outputs, gts, masks,
auc_nor)))
if 'EPE' in metrics:
info_str.append(('EPE', keypoint_epe(outputs, gts, masks)))
if 'NME' in metrics:
normalize_factor = self._get_normalize_factor(
gts=gts, box_sizes=box_sizes)
info_str.append(
('NME', keypoint_nme(outputs, gts, masks, normalize_factor)))
return info_str
def __len__(self):
"""Get the size of the dataset."""
return len(self.db)
def __getitem__(self, idx):
"""Get the sample given index."""
results = copy.deepcopy(self.db[idx])
results['ann_info'] = self.ann_info
return self.pipeline(results)
def _sort_and_unique_bboxes(self, kpts, key='bbox_id'):
"""sort kpts and remove the repeated ones."""
kpts = sorted(kpts, key=lambda x: x[key])
num = len(kpts)
for i in range(num - 1, 0, -1):
if kpts[i][key] == kpts[i - 1][key]:
del kpts[i]
return kpts
@@ -0,0 +1,200 @@
# Copyright (c) OpenMMLab. All rights reserved.
import copy
from abc import ABCMeta, abstractmethod
import numpy as np
from torch.utils.data import Dataset
from xtcocotools.coco import COCO
from mmpose.datasets import DatasetInfo
from mmpose.datasets.pipelines import Compose
class Kpt2dSviewRgbVidTopDownDataset(Dataset, metaclass=ABCMeta):
"""Base class for keypoint 2D top-down pose estimation with single-view RGB
video as the input.
All fashion datasets should subclass it.
All subclasses should overwrite:
Methods:`_get_db`, 'evaluate'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where videos/images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
coco_style (bool): Whether the annotation json is coco-style.
Default: True
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
coco_style=True,
test_mode=False):
self.image_info = {}
self.ann_info = {}
self.ann_file = ann_file
self.img_prefix = img_prefix
self.pipeline = pipeline
self.test_mode = test_mode
self.ann_info['image_size'] = np.array(data_cfg['image_size'])
self.ann_info['heatmap_size'] = np.array(data_cfg['heatmap_size'])
self.ann_info['num_joints'] = data_cfg['num_joints']
self.ann_info['inference_channel'] = data_cfg['inference_channel']
self.ann_info['num_output_channels'] = data_cfg['num_output_channels']
self.ann_info['dataset_channel'] = data_cfg['dataset_channel']
self.ann_info['use_different_joint_weights'] = data_cfg.get(
'use_different_joint_weights', False)
if dataset_info is None:
raise ValueError(
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.')
dataset_info = DatasetInfo(dataset_info)
assert self.ann_info['num_joints'] == dataset_info.keypoint_num
self.ann_info['flip_pairs'] = dataset_info.flip_pairs
self.ann_info['flip_index'] = dataset_info.flip_index
self.ann_info['upper_body_ids'] = dataset_info.upper_body_ids
self.ann_info['lower_body_ids'] = dataset_info.lower_body_ids
self.ann_info['joint_weights'] = dataset_info.joint_weights
self.ann_info['skeleton'] = dataset_info.skeleton
self.sigmas = dataset_info.sigmas
self.dataset_name = dataset_info.dataset_name
if coco_style:
self.coco = COCO(ann_file)
if 'categories' in self.coco.dataset:
cats = [
cat['name']
for cat in self.coco.loadCats(self.coco.getCatIds())
]
self.classes = ['__background__'] + cats
self.num_classes = len(self.classes)
self._class_to_ind = dict(
zip(self.classes, range(self.num_classes)))
self._class_to_coco_ind = dict(
zip(cats, self.coco.getCatIds()))
self._coco_ind_to_class_ind = dict(
(self._class_to_coco_ind[cls], self._class_to_ind[cls])
for cls in self.classes[1:])
self.img_ids = self.coco.getImgIds()
self.num_images = len(self.img_ids)
self.id2name, self.name2id = self._get_mapping_id_name(
self.coco.imgs)
self.db = []
self.pipeline = Compose(self.pipeline)
@staticmethod
def _get_mapping_id_name(imgs):
"""
Args:
imgs (dict): dict of image info.
Returns:
tuple: Image name & id mapping dicts.
- id2name (dict): Mapping image id to name.
- name2id (dict): Mapping image name to id.
"""
id2name = {}
name2id = {}
for image_id, image in imgs.items():
file_name = image['file_name']
id2name[image_id] = file_name
name2id[file_name] = image_id
return id2name, name2id
def _xywh2cs(self, x, y, w, h, padding=1.25):
"""This encodes bbox(x,y,w,h) into (center, scale)
Args:
x, y, w, h (float): left, top, width and height
padding (float): bounding box padding factor
Returns:
center (np.ndarray[float32](2,)): center of the bbox (x, y).
scale (np.ndarray[float32](2,)): scale of the bbox w & h.
"""
aspect_ratio = self.ann_info['image_size'][0] / self.ann_info[
'image_size'][1]
center = np.array([x + w * 0.5, y + h * 0.5], dtype=np.float32)
if (not self.test_mode) and np.random.rand() < 0.3:
center += 0.4 * (np.random.rand(2) - 0.5) * [w, h]
if w > aspect_ratio * h:
h = w * 1.0 / aspect_ratio
elif w < aspect_ratio * h:
w = h * aspect_ratio
# pixel std is 200.0
scale = np.array([w / 200.0, h / 200.0], dtype=np.float32)
# padding to include proper amount of context
scale = scale * padding
return center, scale
@abstractmethod
def _get_db(self):
"""Load dataset."""
@abstractmethod
def evaluate(self, results, *args, **kwargs):
"""Evaluate keypoint results."""
@staticmethod
@abstractmethod
def _write_keypoint_results(keypoint_results, gt_folder, pred_folder):
"""Write results into a json file."""
@abstractmethod
def _do_keypoint_eval(self, gt_folder, pred_folder):
"""Keypoint evaluation.
Args:
gt_folder (str): The folder of the json files storing
ground truth keypoint annotations.
pred_folder (str): The folder of the json files storing
prediction results.
Returns:
List: Evaluation results for evaluation metric.
"""
def __len__(self):
"""Get the size of the dataset."""
return len(self.db)
def __getitem__(self, idx):
"""Get the sample given index."""
results = copy.deepcopy(self.db[idx])
results['ann_info'] = self.ann_info
return self.pipeline(results)
def _sort_and_unique_bboxes(self, kpts, key='bbox_id'):
"""sort kpts and remove the repeated ones."""
for img_id, persons in kpts.items():
num = len(persons)
kpts[img_id] = sorted(kpts[img_id], key=lambda x: x[key])
for i in range(num - 1, 0, -1):
if kpts[img_id][i][key] == kpts[img_id][i - 1][key]:
del kpts[img_id][i]
return kpts
@@ -0,0 +1,143 @@
# Copyright (c) OpenMMLab. All rights reserved.
import copy
from abc import ABCMeta, abstractmethod
import json_tricks as json
import numpy as np
from torch.utils.data import Dataset
from mmpose.datasets import DatasetInfo
from mmpose.datasets.pipelines import Compose
class Kpt3dMviewRgbImgDirectDataset(Dataset, metaclass=ABCMeta):
"""Base class for keypoint 3D top-down pose estimation with multi-view RGB
images as the input.
All subclasses should overwrite:
Methods:`_get_db`, 'evaluate'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
self.image_info = {}
self.ann_info = {}
self.ann_file = ann_file
self.img_prefix = img_prefix
self.pipeline = pipeline
self.test_mode = test_mode
self.ann_info['image_size'] = np.array(data_cfg['image_size'])
self.ann_info['heatmap_size'] = np.array(data_cfg['heatmap_size'])
self.ann_info['num_joints'] = data_cfg['num_joints']
self.ann_info['space_size'] = data_cfg['space_size']
self.ann_info['space_center'] = data_cfg['space_center']
self.ann_info['cube_size'] = data_cfg['cube_size']
self.ann_info['scale_aware_sigma'] = data_cfg.get(
'scale_aware_sigma', False)
if dataset_info is None:
raise ValueError(
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.')
dataset_info = DatasetInfo(dataset_info)
assert self.ann_info['num_joints'] <= dataset_info.keypoint_num
self.ann_info['flip_pairs'] = dataset_info.flip_pairs
self.ann_info['num_scales'] = 1
self.ann_info['flip_index'] = dataset_info.flip_index
self.ann_info['upper_body_ids'] = dataset_info.upper_body_ids
self.ann_info['lower_body_ids'] = dataset_info.lower_body_ids
self.ann_info['joint_weights'] = dataset_info.joint_weights
self.ann_info['skeleton'] = dataset_info.skeleton
self.sigmas = dataset_info.sigmas
self.dataset_name = dataset_info.dataset_name
self.load_config(data_cfg)
self.db = []
self.pipeline = Compose(self.pipeline)
def load_config(self, data_cfg):
"""Initialize dataset attributes according to the config.
Override this method to set dataset specific attributes.
"""
self.num_joints = data_cfg['num_joints']
self.num_cameras = data_cfg['num_cameras']
self.seq_frame_interval = data_cfg.get('seq_frame_interval', 1)
self.subset = data_cfg.get('subset', 'train')
self.need_2d_label = data_cfg.get('need_2d_label', False)
self.need_camera_param = True
@staticmethod
def _get_mapping_id_name(imgs):
"""
Args:
imgs (dict): dict of image info.
Returns:
tuple: Image name & id mapping dicts.
- id2name (dict): Mapping image id to name.
- name2id (dict): Mapping image name to id.
"""
id2name = {}
name2id = {}
for image_id, image in imgs.items():
file_name = image['file_name']
id2name[image_id] = file_name
name2id[file_name] = image_id
return id2name, name2id
@abstractmethod
def _get_db(self):
"""Load dataset."""
raise NotImplementedError
@abstractmethod
def evaluate(self, results, *args, **kwargs):
"""Evaluate keypoint results."""
@staticmethod
def _write_keypoint_results(keypoints, res_file):
"""Write results into a json file."""
with open(res_file, 'w') as f:
json.dump(keypoints, f, sort_keys=True, indent=4)
def __len__(self):
"""Get the size of the dataset."""
return len(self.db) // self.num_cameras
def __getitem__(self, idx):
"""Get the sample given index."""
results = {}
# return self.pipeline(results)
for c in range(self.num_cameras):
result = copy.deepcopy(self.db[self.num_cameras * idx + c])
result['ann_info'] = self.ann_info
results[c] = result
return self.pipeline(results)
@@ -0,0 +1,226 @@
# Copyright (c) OpenMMLab. All rights reserved.
import copy
from abc import ABCMeta, abstractmethod
import numpy as np
from torch.utils.data import Dataset
from mmpose.datasets import DatasetInfo
from mmpose.datasets.pipelines import Compose
class Kpt3dSviewKpt2dDataset(Dataset, metaclass=ABCMeta):
"""Base class for 3D human pose datasets.
Subclasses should consider overwriting following methods:
- load_config
- load_annotations
- build_sample_indices
- evaluate
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
- num_joints: Number of joints.
- seq_len: Number of frames in a sequence. Default: 1.
- seq_frame_interval: Extract frames from the video at certain
intervals. Default: 1.
- causal: If set to True, the rightmost input frame will be the
target frame. Otherwise, the middle input frame will be the
target frame. Default: True.
- temporal_padding: Whether to pad the video so that poses will be
predicted for every frame in the video. Default: False
- subset: Reduce dataset size by fraction. Default: 1.
- need_2d_label: Whether need 2D joint labels or not.
Default: False.
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
self.ann_file = ann_file
self.img_prefix = img_prefix
self.data_cfg = copy.deepcopy(data_cfg)
self.pipeline = pipeline
self.test_mode = test_mode
self.ann_info = {}
if dataset_info is None:
raise ValueError(
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.')
dataset_info = DatasetInfo(dataset_info)
self.load_config(self.data_cfg)
self.ann_info['num_joints'] = data_cfg['num_joints']
assert self.ann_info['num_joints'] == dataset_info.keypoint_num
self.ann_info['flip_pairs'] = dataset_info.flip_pairs
self.ann_info['upper_body_ids'] = dataset_info.upper_body_ids
self.ann_info['lower_body_ids'] = dataset_info.lower_body_ids
self.ann_info['joint_weights'] = dataset_info.joint_weights
self.ann_info['skeleton'] = dataset_info.skeleton
self.sigmas = dataset_info.sigmas
self.dataset_name = dataset_info.dataset_name
self.data_info = self.load_annotations()
self.sample_indices = self.build_sample_indices()
self.pipeline = Compose(pipeline)
self.name2id = {
name: i
for i, name in enumerate(self.data_info['imgnames'])
}
def load_config(self, data_cfg):
"""Initialize dataset attributes according to the config.
Override this method to set dataset specific attributes.
"""
self.num_joints = data_cfg['num_joints']
self.seq_len = data_cfg.get('seq_len', 1)
self.seq_frame_interval = data_cfg.get('seq_frame_interval', 1)
self.causal = data_cfg.get('causal', True)
self.temporal_padding = data_cfg.get('temporal_padding', False)
self.subset = data_cfg.get('subset', 1)
self.need_2d_label = data_cfg.get('need_2d_label', False)
self.need_camera_param = False
def load_annotations(self):
"""Load data annotation."""
data = np.load(self.ann_file)
# get image info
_imgnames = data['imgname']
num_imgs = len(_imgnames)
num_joints = self.ann_info['num_joints']
if 'scale' in data:
_scales = data['scale'].astype(np.float32)
else:
_scales = np.zeros(num_imgs, dtype=np.float32)
if 'center' in data:
_centers = data['center'].astype(np.float32)
else:
_centers = np.zeros((num_imgs, 2), dtype=np.float32)
# get 3D pose
if 'S' in data.keys():
_joints_3d = data['S'].astype(np.float32)
else:
_joints_3d = np.zeros((num_imgs, num_joints, 4), dtype=np.float32)
# get 2D pose
if 'part' in data.keys():
_joints_2d = data['part'].astype(np.float32)
else:
_joints_2d = np.zeros((num_imgs, num_joints, 3), dtype=np.float32)
data_info = {
'imgnames': _imgnames,
'joints_3d': _joints_3d,
'joints_2d': _joints_2d,
'scales': _scales,
'centers': _centers,
}
return data_info
def build_sample_indices(self):
"""Build sample indices.
The default method creates sample indices that each sample is a single
frame (i.e. seq_len=1). Override this method in the subclass to define
how frames are sampled to form data samples.
Outputs:
sample_indices [list(tuple)]: the frame indices of each sample.
For a sample, all frames will be treated as an input sequence,
and the ground-truth pose of the last frame will be the target.
"""
sample_indices = []
if self.seq_len == 1:
num_imgs = len(self.ann_info['imgnames'])
sample_indices = [(idx, ) for idx in range(num_imgs)]
else:
raise NotImplementedError('Multi-frame data sample unsupported!')
return sample_indices
@abstractmethod
def evaluate(self, results, *args, **kwargs):
"""Evaluate keypoint results."""
def prepare_data(self, idx):
"""Get data sample."""
data = self.data_info
frame_ids = self.sample_indices[idx]
assert len(frame_ids) == self.seq_len
# get the 3D/2D pose sequence
_joints_3d = data['joints_3d'][frame_ids]
_joints_2d = data['joints_2d'][frame_ids]
# get the image info
_imgnames = data['imgnames'][frame_ids]
_centers = data['centers'][frame_ids]
_scales = data['scales'][frame_ids]
if _scales.ndim == 1:
_scales = np.stack([_scales, _scales], axis=1)
target_idx = -1 if self.causal else int(self.seq_len) // 2
results = {
'input_2d': _joints_2d[:, :, :2],
'input_2d_visible': _joints_2d[:, :, -1:],
'input_3d': _joints_3d[:, :, :3],
'input_3d_visible': _joints_3d[:, :, -1:],
'target': _joints_3d[target_idx, :, :3],
'target_visible': _joints_3d[target_idx, :, -1:],
'image_paths': _imgnames,
'target_image_path': _imgnames[target_idx],
'scales': _scales,
'centers': _centers,
}
if self.need_2d_label:
results['target_2d'] = _joints_2d[target_idx, :, :2]
if self.need_camera_param:
_cam_param = self.get_camera_param(_imgnames[0])
results['camera_param'] = _cam_param
# get image size from camera parameters
if 'w' in _cam_param and 'h' in _cam_param:
results['image_width'] = _cam_param['w']
results['image_height'] = _cam_param['h']
return results
def __len__(self):
"""Get the size of the dataset."""
return len(self.sample_indices)
def __getitem__(self, idx):
"""Get a sample with given index."""
results = copy.deepcopy(self.prepare_data(idx))
results['ann_info'] = self.ann_info
return self.pipeline(results)
def get_camera_param(self, imgname):
"""Get camera parameters of a frame by its image name."""
raise NotImplementedError
@@ -0,0 +1,256 @@
# Copyright (c) OpenMMLab. All rights reserved.
import copy
from abc import ABCMeta, abstractmethod
import json_tricks as json
import numpy as np
from torch.utils.data import Dataset
from xtcocotools.coco import COCO
from mmpose.datasets import DatasetInfo
from mmpose.datasets.pipelines import Compose
class Kpt3dSviewRgbImgTopDownDataset(Dataset, metaclass=ABCMeta):
"""Base class for keypoint 3D top-down pose estimation with single-view RGB
image as the input.
All fashion datasets should subclass it.
All subclasses should overwrite:
Methods:`_get_db`, 'evaluate'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
coco_style (bool): Whether the annotation json is coco-style.
Default: True
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
coco_style=True,
test_mode=False):
self.image_info = {}
self.ann_info = {}
self.ann_file = ann_file
self.img_prefix = img_prefix
self.pipeline = pipeline
self.test_mode = test_mode
self.ann_info['image_size'] = np.array(data_cfg['image_size'])
self.ann_info['heatmap_size'] = np.array(data_cfg['heatmap_size'])
self.ann_info['num_joints'] = data_cfg['num_joints']
self.ann_info['inference_channel'] = data_cfg['inference_channel']
self.ann_info['num_output_channels'] = data_cfg['num_output_channels']
self.ann_info['dataset_channel'] = data_cfg['dataset_channel']
if dataset_info is None:
raise ValueError(
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.')
dataset_info = DatasetInfo(dataset_info)
assert self.ann_info['num_joints'] == dataset_info.keypoint_num
self.ann_info['flip_pairs'] = dataset_info.flip_pairs
self.ann_info['flip_index'] = dataset_info.flip_index
self.ann_info['upper_body_ids'] = dataset_info.upper_body_ids
self.ann_info['lower_body_ids'] = dataset_info.lower_body_ids
self.ann_info['joint_weights'] = dataset_info.joint_weights
self.ann_info['skeleton'] = dataset_info.skeleton
self.sigmas = dataset_info.sigmas
self.dataset_name = dataset_info.dataset_name
if coco_style:
self.coco = COCO(ann_file)
if 'categories' in self.coco.dataset:
cats = [
cat['name']
for cat in self.coco.loadCats(self.coco.getCatIds())
]
self.classes = ['__background__'] + cats
self.num_classes = len(self.classes)
self._class_to_ind = dict(
zip(self.classes, range(self.num_classes)))
self._class_to_coco_ind = dict(
zip(cats, self.coco.getCatIds()))
self._coco_ind_to_class_ind = dict(
(self._class_to_coco_ind[cls], self._class_to_ind[cls])
for cls in self.classes[1:])
self.img_ids = self.coco.getImgIds()
self.num_images = len(self.img_ids)
self.id2name, self.name2id = self._get_mapping_id_name(
self.coco.imgs)
self.db = []
self.pipeline = Compose(self.pipeline)
@staticmethod
def _cam2pixel(cam_coord, f, c):
"""Transform the joints from their camera coordinates to their pixel
coordinates.
Note:
N: number of joints
Args:
cam_coord (ndarray[N, 3]): 3D joints coordinates
in the camera coordinate system
f (ndarray[2]): focal length of x and y axis
c (ndarray[2]): principal point of x and y axis
Returns:
img_coord (ndarray[N, 3]): the coordinates (x, y, 0)
in the image plane.
"""
x = cam_coord[:, 0] / (cam_coord[:, 2] + 1e-8) * f[0] + c[0]
y = cam_coord[:, 1] / (cam_coord[:, 2] + 1e-8) * f[1] + c[1]
z = np.zeros_like(x)
img_coord = np.concatenate((x[:, None], y[:, None], z[:, None]), 1)
return img_coord
@staticmethod
def _world2cam(world_coord, R, T):
"""Transform the joints from their world coordinates to their camera
coordinates.
Note:
N: number of joints
Args:
world_coord (ndarray[3, N]): 3D joints coordinates
in the world coordinate system
R (ndarray[3, 3]): camera rotation matrix
T (ndarray[3, 1]): camera position (x, y, z)
Returns:
cam_coord (ndarray[3, N]): 3D joints coordinates
in the camera coordinate system
"""
cam_coord = np.dot(R, world_coord - T)
return cam_coord
@staticmethod
def _pixel2cam(pixel_coord, f, c):
"""Transform the joints from their pixel coordinates to their camera
coordinates.
Note:
N: number of joints
Args:
pixel_coord (ndarray[N, 3]): 3D joints coordinates
in the pixel coordinate system
f (ndarray[2]): focal length of x and y axis
c (ndarray[2]): principal point of x and y axis
Returns:
cam_coord (ndarray[N, 3]): 3D joints coordinates
in the camera coordinate system
"""
x = (pixel_coord[:, 0] - c[0]) / f[0] * pixel_coord[:, 2]
y = (pixel_coord[:, 1] - c[1]) / f[1] * pixel_coord[:, 2]
z = pixel_coord[:, 2]
cam_coord = np.concatenate((x[:, None], y[:, None], z[:, None]), 1)
return cam_coord
@staticmethod
def _get_mapping_id_name(imgs):
"""
Args:
imgs (dict): dict of image info.
Returns:
tuple: Image name & id mapping dicts.
- id2name (dict): Mapping image id to name.
- name2id (dict): Mapping image name to id.
"""
id2name = {}
name2id = {}
for image_id, image in imgs.items():
file_name = image['file_name']
id2name[image_id] = file_name
name2id[file_name] = image_id
return id2name, name2id
def _xywh2cs(self, x, y, w, h, padding=1.25):
"""This encodes bbox(x,y,w,h) into (center, scale)
Args:
x, y, w, h (float): left, top, width and height
padding (float): bounding box padding factor
Returns:
center (np.ndarray[float32](2,)): center of the bbox (x, y).
scale (np.ndarray[float32](2,)): scale of the bbox w & h.
"""
aspect_ratio = self.ann_info['image_size'][0] / self.ann_info[
'image_size'][1]
center = np.array([x + w * 0.5, y + h * 0.5], dtype=np.float32)
if (not self.test_mode) and np.random.rand() < 0.3:
center += 0.4 * (np.random.rand(2) - 0.5) * [w, h]
if w > aspect_ratio * h:
h = w * 1.0 / aspect_ratio
elif w < aspect_ratio * h:
w = h * aspect_ratio
# pixel std is 200.0
scale = np.array([w / 200.0, h / 200.0], dtype=np.float32)
# padding to include proper amount of context
scale = scale * padding
return center, scale
@abstractmethod
def _get_db(self):
"""Load dataset."""
raise NotImplementedError
@abstractmethod
def evaluate(self, results, *args, **kwargs):
"""Evaluate keypoint results."""
@staticmethod
def _write_keypoint_results(keypoints, res_file):
"""Write results into a json file."""
with open(res_file, 'w') as f:
json.dump(keypoints, f, sort_keys=True, indent=4)
def __len__(self):
"""Get the size of the dataset."""
return len(self.db)
def __getitem__(self, idx):
"""Get the sample given index."""
results = copy.deepcopy(self.db[idx])
results['ann_info'] = self.ann_info
return self.pipeline(results)
def _sort_and_unique_bboxes(self, kpts, key='bbox_id'):
"""sort kpts and remove the repeated ones."""
kpts = sorted(kpts, key=lambda x: x[key])
num = len(kpts)
for i in range(num - 1, 0, -1):
if kpts[i][key] == kpts[i - 1][key]:
del kpts[i]
return kpts
@@ -0,0 +1,11 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .body3d_h36m_dataset import Body3DH36MDataset
from .body3d_mpi_inf_3dhp_dataset import Body3DMpiInf3dhpDataset
from .body3d_mview_direct_panoptic_dataset import \
Body3DMviewDirectPanopticDataset
from .body3d_semi_supervision_dataset import Body3DSemiSupervisionDataset
__all__ = [
'Body3DH36MDataset', 'Body3DSemiSupervisionDataset',
'Body3DMpiInf3dhpDataset', 'Body3DMviewDirectPanopticDataset'
]
@@ -0,0 +1,16 @@
# Copyright (c) OpenMMLab. All rights reserved.
from abc import ABCMeta
from torch.utils.data import Dataset
class Body3DBaseDataset(Dataset, metaclass=ABCMeta):
"""This class has been deprecated and replaced by
Kpt3dSviewKpt2dDataset."""
def __init__(self, *args, **kwargs):
raise (ImportError(
'Body3DBaseDataset has been replaced by '
'Kpt3dSviewKpt2dDataset'
'check https://github.com/open-mmlab/mmpose/pull/663 for details.')
)
@@ -0,0 +1,343 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict, defaultdict
import mmcv
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.core.evaluation import keypoint_mpjpe
from mmpose.datasets.datasets.base import Kpt3dSviewKpt2dDataset
from ...builder import DATASETS
@DATASETS.register_module()
class Body3DH36MDataset(Kpt3dSviewKpt2dDataset):
"""Human3.6M dataset for 3D human pose estimation.
"Human3.6M: Large Scale Datasets and Predictive Methods for 3D Human
Sensing in Natural Environments", TPAMI`2014.
More details can be found in the `paper
<http://vision.imar.ro/human3.6m/pami-h36m.pdf>`__.
Human3.6M keypoint indexes::
0: 'root (pelvis)',
1: 'right_hip',
2: 'right_knee',
3: 'right_foot',
4: 'left_hip',
5: 'left_knee',
6: 'left_foot',
7: 'spine',
8: 'thorax',
9: 'neck_base',
10: 'head',
11: 'left_shoulder',
12: 'left_elbow',
13: 'left_wrist',
14: 'right_shoulder',
15: 'right_elbow',
16: 'right_wrist'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
JOINT_NAMES = [
'Root', 'RHip', 'RKnee', 'RFoot', 'LHip', 'LKnee', 'LFoot', 'Spine',
'Thorax', 'NeckBase', 'Head', 'LShoulder', 'LElbow', 'LWrist',
'RShoulder', 'RElbow', 'RWrist'
]
# 2D joint source options:
# "gt": from the annotation file
# "detection": from a detection result file of 2D keypoint
# "pipeline": will be generate by the pipeline
SUPPORTED_JOINT_2D_SRC = {'gt', 'detection', 'pipeline'}
# metric
ALLOWED_METRICS = {'mpjpe', 'p-mpjpe', 'n-mpjpe'}
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/h36m.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
def load_config(self, data_cfg):
super().load_config(data_cfg)
# h36m specific attributes
self.joint_2d_src = data_cfg.get('joint_2d_src', 'gt')
if self.joint_2d_src not in self.SUPPORTED_JOINT_2D_SRC:
raise ValueError(
f'Unsupported joint_2d_src "{self.joint_2d_src}". '
f'Supported options are {self.SUPPORTED_JOINT_2D_SRC}')
self.joint_2d_det_file = data_cfg.get('joint_2d_det_file', None)
self.need_camera_param = data_cfg.get('need_camera_param', False)
if self.need_camera_param:
assert 'camera_param_file' in data_cfg
self.camera_param = self._load_camera_param(
data_cfg['camera_param_file'])
# h36m specific annotation info
ann_info = {}
ann_info['use_different_joint_weights'] = False
# action filter
actions = data_cfg.get('actions', '_all_')
self.actions = set(
actions if isinstance(actions, (list, tuple)) else [actions])
# subject filter
subjects = data_cfg.get('subjects', '_all_')
self.subjects = set(
subjects if isinstance(subjects, (list, tuple)) else [subjects])
self.ann_info.update(ann_info)
def load_annotations(self):
data_info = super().load_annotations()
# get 2D joints
if self.joint_2d_src == 'gt':
data_info['joints_2d'] = data_info['joints_2d']
elif self.joint_2d_src == 'detection':
data_info['joints_2d'] = self._load_joint_2d_detection(
self.joint_2d_det_file)
assert data_info['joints_2d'].shape[0] == data_info[
'joints_3d'].shape[0]
assert data_info['joints_2d'].shape[2] == 3
elif self.joint_2d_src == 'pipeline':
# joint_2d will be generated in the pipeline
pass
else:
raise NotImplementedError(
f'Unhandled joint_2d_src option {self.joint_2d_src}')
return data_info
@staticmethod
def _parse_h36m_imgname(imgname):
"""Parse imgname to get information of subject, action and camera.
A typical h36m image filename is like:
S1_Directions_1.54138969_000001.jpg
"""
subj, rest = osp.basename(imgname).split('_', 1)
action, rest = rest.split('.', 1)
camera, rest = rest.split('_', 1)
return subj, action, camera
def build_sample_indices(self):
"""Split original videos into sequences and build frame indices.
This method overrides the default one in the base class.
"""
# Group frames into videos. Assume that self.data_info is
# chronological.
video_frames = defaultdict(list)
for idx, imgname in enumerate(self.data_info['imgnames']):
subj, action, camera = self._parse_h36m_imgname(imgname)
if '_all_' not in self.actions and action not in self.actions:
continue
if '_all_' not in self.subjects and subj not in self.subjects:
continue
video_frames[(subj, action, camera)].append(idx)
# build sample indices
sample_indices = []
_len = (self.seq_len - 1) * self.seq_frame_interval + 1
_step = self.seq_frame_interval
for _, _indices in sorted(video_frames.items()):
n_frame = len(_indices)
if self.temporal_padding:
# Pad the sequence so that every frame in the sequence will be
# predicted.
if self.causal:
frames_left = self.seq_len - 1
frames_right = 0
else:
frames_left = (self.seq_len - 1) // 2
frames_right = frames_left
for i in range(n_frame):
pad_left = max(0, frames_left - i // _step)
pad_right = max(0,
frames_right - (n_frame - 1 - i) // _step)
start = max(i % _step, i - frames_left * _step)
end = min(n_frame - (n_frame - 1 - i) % _step,
i + frames_right * _step + 1)
sample_indices.append([_indices[0]] * pad_left +
_indices[start:end:_step] +
[_indices[-1]] * pad_right)
else:
seqs_from_video = [
_indices[i:(i + _len):_step]
for i in range(0, n_frame - _len + 1)
]
sample_indices.extend(seqs_from_video)
# reduce dataset size if self.subset < 1
assert 0 < self.subset <= 1
subset_size = int(len(sample_indices) * self.subset)
start = np.random.randint(0, len(sample_indices) - subset_size + 1)
end = start + subset_size
return sample_indices[start:end]
def _load_joint_2d_detection(self, det_file):
""""Load 2D joint detection results from file."""
joints_2d = np.load(det_file).astype(np.float32)
return joints_2d
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='mpjpe', **kwargs):
metrics = metric if isinstance(metric, list) else [metric]
for _metric in metrics:
if _metric not in self.ALLOWED_METRICS:
raise ValueError(
f'Unsupported metric "{_metric}" for human3.6 dataset.'
f'Supported metrics are {self.ALLOWED_METRICS}')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
image_paths = result['target_image_paths']
batch_size = len(image_paths)
for i in range(batch_size):
target_id = self.name2id[image_paths[i]]
kpts.append({
'keypoints': preds[i],
'target_id': target_id,
})
mmcv.dump(kpts, res_file)
name_value_tuples = []
for _metric in metrics:
if _metric == 'mpjpe':
_nv_tuples = self._report_mpjpe(kpts)
elif _metric == 'p-mpjpe':
_nv_tuples = self._report_mpjpe(kpts, mode='p-mpjpe')
elif _metric == 'n-mpjpe':
_nv_tuples = self._report_mpjpe(kpts, mode='n-mpjpe')
else:
raise NotImplementedError
name_value_tuples.extend(_nv_tuples)
if tmp_folder is not None:
tmp_folder.cleanup()
return OrderedDict(name_value_tuples)
def _report_mpjpe(self, keypoint_results, mode='mpjpe'):
"""Cauculate mean per joint position error (MPJPE) or its variants like
P-MPJPE or N-MPJPE.
Args:
keypoint_results (list): Keypoint predictions. See
'Body3DH36MDataset.evaluate' for details.
mode (str): Specify mpjpe variants. Supported options are:
- ``'mpjpe'``: Standard MPJPE.
- ``'p-mpjpe'``: MPJPE after aligning prediction to groundtruth
via a rigid transformation (scale, rotation and
translation).
- ``'n-mpjpe'``: MPJPE after aligning prediction to groundtruth
in scale only.
"""
preds = []
gts = []
masks = []
action_category_indices = defaultdict(list)
for idx, result in enumerate(keypoint_results):
pred = result['keypoints']
target_id = result['target_id']
gt, gt_visible = np.split(
self.data_info['joints_3d'][target_id], [3], axis=-1)
preds.append(pred)
gts.append(gt)
masks.append(gt_visible)
action = self._parse_h36m_imgname(
self.data_info['imgnames'][target_id])[1]
action_category = action.split('_')[0]
action_category_indices[action_category].append(idx)
preds = np.stack(preds)
gts = np.stack(gts)
masks = np.stack(masks).squeeze(-1) > 0
err_name = mode.upper()
if mode == 'mpjpe':
alignment = 'none'
elif mode == 'p-mpjpe':
alignment = 'procrustes'
elif mode == 'n-mpjpe':
alignment = 'scale'
else:
raise ValueError(f'Invalid mode: {mode}')
error = keypoint_mpjpe(preds, gts, masks, alignment)
name_value_tuples = [(err_name, error)]
for action_category, indices in action_category_indices.items():
_error = keypoint_mpjpe(preds[indices], gts[indices],
masks[indices])
name_value_tuples.append((f'{err_name}_{action_category}', _error))
return name_value_tuples
def _load_camera_param(self, camera_param_file):
"""Load camera parameters from file."""
return mmcv.load(camera_param_file)
def get_camera_param(self, imgname):
"""Get camera parameters of a frame by its image name."""
assert hasattr(self, 'camera_param')
subj, _, camera = self._parse_h36m_imgname(imgname)
return self.camera_param[(subj, camera)]
@@ -0,0 +1,417 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict, defaultdict
import mmcv
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.core.evaluation import (keypoint_3d_auc, keypoint_3d_pck,
keypoint_mpjpe)
from mmpose.datasets.datasets.base import Kpt3dSviewKpt2dDataset
from ...builder import DATASETS
@DATASETS.register_module()
class Body3DMpiInf3dhpDataset(Kpt3dSviewKpt2dDataset):
"""MPI-INF-3DHP dataset for 3D human pose estimation.
"Monocular 3D Human Pose Estimation In The Wild Using Improved CNN
Supervision", 3DV'2017.
More details can be found in the `paper
<https://arxiv.org/pdf/1611.09813>`__.
MPI-INF-3DHP keypoint indexes:
0: 'head_top',
1: 'neck',
2: 'right_shoulder',
3: 'right_elbow',
4: 'right_wrist',
5: 'left_shoulder;,
6: 'left_elbow',
7: 'left_wrist',
8: 'right_hip',
9: 'right_knee',
10: 'right_ankle',
11: 'left_hip',
12: 'left_knee',
13: 'left_ankle',
14: 'root (pelvis)',
15: 'spine',
16: 'head'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): Data configurations. Please refer to the docstring of
Body3DBaseDataset for common data attributes. Here are MPI-INF-3DHP
specific attributes.
- joint_2d_src: 2D joint source. Options include:
"gt": from the annotation file
"detection": from a detection result file of 2D keypoint
"pipeline": will be generate by the pipeline
Default: "gt".
- joint_2d_det_file: Path to the detection result file of 2D
keypoint. Only used when joint_2d_src == "detection".
- need_camera_param: Whether need camera parameters or not.
Default: False.
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
JOINT_NAMES = [
'HeadTop', 'Neck', 'RShoulder', 'RElbow', 'RWrist', 'LShoulder',
'LElbow', 'LWrist', 'RHip', 'RKnee', 'RAnkle', 'LHip', 'LKnee',
'LAnkle', 'Root', 'Spine', 'Head'
]
# 2D joint source options:
# "gt": from the annotation file
# "detection": from a detection result file of 2D keypoint
# "pipeline": will be generate by the pipeline
SUPPORTED_JOINT_2D_SRC = {'gt', 'detection', 'pipeline'}
# metric
ALLOWED_METRICS = {
'mpjpe', 'p-mpjpe', '3dpck', 'p-3dpck', '3dauc', 'p-3dauc'
}
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/mpi_inf_3dhp.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
def load_config(self, data_cfg):
super().load_config(data_cfg)
# mpi-inf-3dhp specific attributes
self.joint_2d_src = data_cfg.get('joint_2d_src', 'gt')
if self.joint_2d_src not in self.SUPPORTED_JOINT_2D_SRC:
raise ValueError(
f'Unsupported joint_2d_src "{self.joint_2d_src}". '
f'Supported options are {self.SUPPORTED_JOINT_2D_SRC}')
self.joint_2d_det_file = data_cfg.get('joint_2d_det_file', None)
self.need_camera_param = data_cfg.get('need_camera_param', False)
if self.need_camera_param:
assert 'camera_param_file' in data_cfg
self.camera_param = self._load_camera_param(
data_cfg['camera_param_file'])
# mpi-inf-3dhp specific annotation info
ann_info = {}
ann_info['use_different_joint_weights'] = False
self.ann_info.update(ann_info)
def load_annotations(self):
data_info = super().load_annotations()
# get 2D joints
if self.joint_2d_src == 'gt':
data_info['joints_2d'] = data_info['joints_2d']
elif self.joint_2d_src == 'detection':
data_info['joints_2d'] = self._load_joint_2d_detection(
self.joint_2d_det_file)
assert data_info['joints_2d'].shape[0] == data_info[
'joints_3d'].shape[0]
assert data_info['joints_2d'].shape[2] == 3
elif self.joint_2d_src == 'pipeline':
# joint_2d will be generated in the pipeline
pass
else:
raise NotImplementedError(
f'Unhandled joint_2d_src option {self.joint_2d_src}')
return data_info
@staticmethod
def _parse_mpi_inf_3dhp_imgname(imgname):
"""Parse imgname to get information of subject, sequence and camera.
A typical mpi-inf-3dhp training image filename is like:
S1_Seq1_Cam0_000001.jpg. A typical mpi-inf-3dhp testing image filename
is like: TS1_000001.jpg
"""
if imgname[0] == 'S':
subj, rest = imgname.split('_', 1)
seq, rest = rest.split('_', 1)
camera, rest = rest.split('_', 1)
return subj, seq, camera
else:
subj, rest = imgname.split('_', 1)
return subj, None, None
def build_sample_indices(self):
"""Split original videos into sequences and build frame indices.
This method overrides the default one in the base class.
"""
# Group frames into videos. Assume that self.data_info is
# chronological.
video_frames = defaultdict(list)
for idx, imgname in enumerate(self.data_info['imgnames']):
subj, seq, camera = self._parse_mpi_inf_3dhp_imgname(imgname)
if seq is not None:
video_frames[(subj, seq, camera)].append(idx)
else:
video_frames[subj].append(idx)
# build sample indices
sample_indices = []
_len = (self.seq_len - 1) * self.seq_frame_interval + 1
_step = self.seq_frame_interval
for _, _indices in sorted(video_frames.items()):
n_frame = len(_indices)
if self.temporal_padding:
# Pad the sequence so that every frame in the sequence will be
# predicted.
if self.causal:
frames_left = self.seq_len - 1
frames_right = 0
else:
frames_left = (self.seq_len - 1) // 2
frames_right = frames_left
for i in range(n_frame):
pad_left = max(0, frames_left - i // _step)
pad_right = max(0,
frames_right - (n_frame - 1 - i) // _step)
start = max(i % _step, i - frames_left * _step)
end = min(n_frame - (n_frame - 1 - i) % _step,
i + frames_right * _step + 1)
sample_indices.append([_indices[0]] * pad_left +
_indices[start:end:_step] +
[_indices[-1]] * pad_right)
else:
seqs_from_video = [
_indices[i:(i + _len):_step]
for i in range(0, n_frame - _len + 1)
]
sample_indices.extend(seqs_from_video)
# reduce dataset size if self.subset < 1
assert 0 < self.subset <= 1
subset_size = int(len(sample_indices) * self.subset)
start = np.random.randint(0, len(sample_indices) - subset_size + 1)
end = start + subset_size
return sample_indices[start:end]
def _load_joint_2d_detection(self, det_file):
""""Load 2D joint detection results from file."""
joints_2d = np.load(det_file).astype(np.float32)
return joints_2d
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='mpjpe', **kwargs):
metrics = metric if isinstance(metric, list) else [metric]
for _metric in metrics:
if _metric not in self.ALLOWED_METRICS:
raise ValueError(
f'Unsupported metric "{_metric}" for mpi-inf-3dhp dataset.'
f'Supported metrics are {self.ALLOWED_METRICS}')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
image_paths = result['target_image_paths']
batch_size = len(image_paths)
for i in range(batch_size):
target_id = self.name2id[image_paths[i]]
kpts.append({
'keypoints': preds[i],
'target_id': target_id,
})
mmcv.dump(kpts, res_file)
name_value_tuples = []
for _metric in metrics:
if _metric == 'mpjpe':
_nv_tuples = self._report_mpjpe(kpts)
elif _metric == 'p-mpjpe':
_nv_tuples = self._report_mpjpe(kpts, mode='p-mpjpe')
elif _metric == '3dpck':
_nv_tuples = self._report_3d_pck(kpts)
elif _metric == 'p-3dpck':
_nv_tuples = self._report_3d_pck(kpts, mode='p-3dpck')
elif _metric == '3dauc':
_nv_tuples = self._report_3d_auc(kpts)
elif _metric == 'p-3dauc':
_nv_tuples = self._report_3d_auc(kpts, mode='p-3dauc')
else:
raise NotImplementedError
name_value_tuples.extend(_nv_tuples)
if tmp_folder is not None:
tmp_folder.cleanup()
return OrderedDict(name_value_tuples)
def _report_mpjpe(self, keypoint_results, mode='mpjpe'):
"""Cauculate mean per joint position error (MPJPE) or its variants
P-MPJPE.
Args:
keypoint_results (list): Keypoint predictions. See
'Body3DMpiInf3dhpDataset.evaluate' for details.
mode (str): Specify mpjpe variants. Supported options are:
- ``'mpjpe'``: Standard MPJPE.
- ``'p-mpjpe'``: MPJPE after aligning prediction to groundtruth
via a rigid transformation (scale, rotation and
translation).
"""
preds = []
gts = []
for idx, result in enumerate(keypoint_results):
pred = result['keypoints']
target_id = result['target_id']
gt, gt_visible = np.split(
self.data_info['joints_3d'][target_id], [3], axis=-1)
preds.append(pred)
gts.append(gt)
preds = np.stack(preds)
gts = np.stack(gts)
masks = np.ones_like(gts[:, :, 0], dtype=bool)
err_name = mode.upper()
if mode == 'mpjpe':
alignment = 'none'
elif mode == 'p-mpjpe':
alignment = 'procrustes'
else:
raise ValueError(f'Invalid mode: {mode}')
error = keypoint_mpjpe(preds, gts, masks, alignment)
name_value_tuples = [(err_name, error)]
return name_value_tuples
def _report_3d_pck(self, keypoint_results, mode='3dpck'):
"""Cauculate Percentage of Correct Keypoints (3DPCK) w. or w/o
Procrustes alignment.
Args:
keypoint_results (list): Keypoint predictions. See
'Body3DMpiInf3dhpDataset.evaluate' for details.
mode (str): Specify mpjpe variants. Supported options are:
- ``'3dpck'``: Standard 3DPCK.
- ``'p-3dpck'``: 3DPCK after aligning prediction to groundtruth
via a rigid transformation (scale, rotation and
translation).
"""
preds = []
gts = []
for idx, result in enumerate(keypoint_results):
pred = result['keypoints']
target_id = result['target_id']
gt, gt_visible = np.split(
self.data_info['joints_3d'][target_id], [3], axis=-1)
preds.append(pred)
gts.append(gt)
preds = np.stack(preds)
gts = np.stack(gts)
masks = np.ones_like(gts[:, :, 0], dtype=bool)
err_name = mode.upper()
if mode == '3dpck':
alignment = 'none'
elif mode == 'p-3dpck':
alignment = 'procrustes'
else:
raise ValueError(f'Invalid mode: {mode}')
error = keypoint_3d_pck(preds, gts, masks, alignment)
name_value_tuples = [(err_name, error)]
return name_value_tuples
def _report_3d_auc(self, keypoint_results, mode='3dauc'):
"""Cauculate the Area Under the Curve (AUC) computed for a range of
3DPCK thresholds.
Args:
keypoint_results (list): Keypoint predictions. See
'Body3DMpiInf3dhpDataset.evaluate' for details.
mode (str): Specify mpjpe variants. Supported options are:
- ``'3dauc'``: Standard 3DAUC.
- ``'p-3dauc'``: 3DAUC after aligning prediction to
groundtruth via a rigid transformation (scale, rotation and
translation).
"""
preds = []
gts = []
for idx, result in enumerate(keypoint_results):
pred = result['keypoints']
target_id = result['target_id']
gt, gt_visible = np.split(
self.data_info['joints_3d'][target_id], [3], axis=-1)
preds.append(pred)
gts.append(gt)
preds = np.stack(preds)
gts = np.stack(gts)
masks = np.ones_like(gts[:, :, 0], dtype=bool)
err_name = mode.upper()
if mode == '3dauc':
alignment = 'none'
elif mode == 'p-3dauc':
alignment = 'procrustes'
else:
raise ValueError(f'Invalid mode: {mode}')
error = keypoint_3d_auc(preds, gts, masks, alignment)
name_value_tuples = [(err_name, error)]
return name_value_tuples
def _load_camera_param(self, camear_param_file):
"""Load camera parameters from file."""
return mmcv.load(camear_param_file)
def get_camera_param(self, imgname):
"""Get camera parameters of a frame by its image name."""
assert hasattr(self, 'camera_param')
return self.camera_param[imgname[:-11]]
@@ -0,0 +1,493 @@
# Copyright (c) OpenMMLab. All rights reserved.
import copy
import glob
import json
import os.path as osp
import pickle
import tempfile
import warnings
from collections import OrderedDict
import mmcv
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.core.camera import SimpleCamera
from mmpose.datasets.builder import DATASETS
from mmpose.datasets.datasets.base import Kpt3dMviewRgbImgDirectDataset
@DATASETS.register_module()
class Body3DMviewDirectPanopticDataset(Kpt3dMviewRgbImgDirectDataset):
"""Panoptic dataset for direct multi-view human pose estimation.
`Panoptic Studio: A Massively Multiview System for Social Motion
Capture' ICCV'2015
More details can be found in the `paper
<https://openaccess.thecvf.com/content_iccv_2015/papers/
Joo_Panoptic_Studio_A_ICCV_2015_paper.pdf>`__ .
The dataset loads both 2D and 3D annotations as well as camera parameters.
Panoptic keypoint indexes::
'neck': 0,
'nose': 1,
'mid-hip': 2,
'l-shoulder': 3,
'l-elbow': 4,
'l-wrist': 5,
'l-hip': 6,
'l-knee': 7,
'l-ankle': 8,
'r-shoulder': 9,
'r-elbow': 10,
'r-wrist': 11,
'r-hip': 12,
'r-knee': 13,
'r-ankle': 14,
'l-eye': 15,
'l-ear': 16,
'r-eye': 17,
'r-ear': 18,
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
ALLOWED_METRICS = {'mpjpe', 'mAP'}
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/panoptic_body3d.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.load_config(data_cfg)
self.ann_info['use_different_joint_weights'] = False
if ann_file is None:
self.db_file = osp.join(
img_prefix, f'group_{self.subset}_cam{self.num_cameras}.pkl')
else:
self.db_file = ann_file
if osp.exists(self.db_file):
with open(self.db_file, 'rb') as f:
info = pickle.load(f)
assert info['sequence_list'] == self.seq_list
assert info['interval'] == self.seq_frame_interval
assert info['cam_list'] == self.cam_list
self.db = info['db']
else:
self.db = self._get_db()
info = {
'sequence_list': self.seq_list,
'interval': self.seq_frame_interval,
'cam_list': self.cam_list,
'db': self.db
}
with open(self.db_file, 'wb') as f:
pickle.dump(info, f)
self.db_size = len(self.db)
print(f'=> load {len(self.db)} samples')
def load_config(self, data_cfg):
"""Initialize dataset attributes according to the config.
Override this method to set dataset specific attributes.
"""
self.num_joints = data_cfg['num_joints']
assert self.num_joints <= 19
self.seq_list = data_cfg['seq_list']
self.cam_list = data_cfg['cam_list']
self.num_cameras = data_cfg['num_cameras']
assert self.num_cameras == len(self.cam_list)
self.seq_frame_interval = data_cfg.get('seq_frame_interval', 1)
self.subset = data_cfg.get('subset', 'train')
self.need_camera_param = True
self.root_id = data_cfg.get('root_id', 0)
self.max_persons = data_cfg.get('max_num', 10)
def _get_scale(self, raw_image_size):
heatmap_size = self.ann_info['heatmap_size']
image_size = self.ann_info['image_size']
assert heatmap_size[0][0] / heatmap_size[0][1] \
== image_size[0] / image_size[1]
w, h = raw_image_size
w_resized, h_resized = image_size
if w / w_resized < h / h_resized:
w_pad = h / h_resized * w_resized
h_pad = h
else:
w_pad = w
h_pad = w / w_resized * h_resized
scale = np.array([w_pad, h_pad], dtype=np.float32)
return scale
def _get_cam(self, seq):
"""Get camera parameters.
Args:
seq (str): Sequence name.
Returns: Camera parameters.
"""
cam_file = osp.join(self.img_prefix, seq,
'calibration_{:s}.json'.format(seq))
with open(cam_file) as cfile:
calib = json.load(cfile)
M = np.array([[1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]])
cameras = {}
for cam in calib['cameras']:
if (cam['panel'], cam['node']) in self.cam_list:
sel_cam = {}
R_w2c = np.array(cam['R']).dot(M)
T_w2c = np.array(cam['t']).reshape((3, 1)) * 10.0 # cm to mm
R_c2w = R_w2c.T
T_c2w = -R_w2c.T @ T_w2c
sel_cam['R'] = R_c2w.tolist()
sel_cam['T'] = T_c2w.tolist()
sel_cam['K'] = cam['K'][:2]
distCoef = cam['distCoef']
sel_cam['k'] = [distCoef[0], distCoef[1], distCoef[4]]
sel_cam['p'] = [distCoef[2], distCoef[3]]
cameras[(cam['panel'], cam['node'])] = sel_cam
return cameras
def _get_db(self):
"""Get dataset base.
Returns:
dict: the dataset base (2D and 3D information)
"""
width = 1920
height = 1080
db = []
sample_id = 0
for seq in self.seq_list:
cameras = self._get_cam(seq)
curr_anno = osp.join(self.img_prefix, seq,
'hdPose3d_stage1_coco19')
anno_files = sorted(glob.iglob('{:s}/*.json'.format(curr_anno)))
print(f'load sequence: {seq}', flush=True)
for i, file in enumerate(anno_files):
if i % self.seq_frame_interval == 0:
with open(file) as dfile:
bodies = json.load(dfile)['bodies']
if len(bodies) == 0:
continue
for k, cam_param in cameras.items():
single_view_camera = SimpleCamera(cam_param)
postfix = osp.basename(file).replace('body3DScene', '')
prefix = '{:02d}_{:02d}'.format(k[0], k[1])
image_file = osp.join(seq, 'hdImgs', prefix,
prefix + postfix)
image_file = image_file.replace('json', 'jpg')
all_poses_3d = np.zeros(
(self.max_persons, self.num_joints, 3),
dtype=np.float32)
all_poses_vis_3d = np.zeros(
(self.max_persons, self.num_joints, 3),
dtype=np.float32)
all_roots_3d = np.zeros((self.max_persons, 3),
dtype=np.float32)
all_poses = np.zeros(
(self.max_persons, self.num_joints, 3),
dtype=np.float32)
cnt = 0
person_ids = -np.ones(self.max_persons, dtype=np.int)
for body in bodies:
if cnt >= self.max_persons:
break
pose3d = np.array(body['joints19']).reshape(
(-1, 4))
pose3d = pose3d[:self.num_joints]
joints_vis = pose3d[:, -1] > 0.1
if not joints_vis[self.root_id]:
continue
# Coordinate transformation
M = np.array([[1.0, 0.0, 0.0], [0.0, 0.0, -1.0],
[0.0, 1.0, 0.0]])
pose3d[:, 0:3] = pose3d[:, 0:3].dot(M) * 10.0
all_poses_3d[cnt] = pose3d[:, :3]
all_roots_3d[cnt] = pose3d[self.root_id, :3]
all_poses_vis_3d[cnt] = np.repeat(
np.reshape(joints_vis, (-1, 1)), 3, axis=1)
pose2d = np.zeros((pose3d.shape[0], 3))
# get pose_2d from pose_3d
pose2d[:, :2] = single_view_camera.world_to_pixel(
pose3d[:, :3])
x_check = np.bitwise_and(pose2d[:, 0] >= 0,
pose2d[:, 0] <= width - 1)
y_check = np.bitwise_and(
pose2d[:, 1] >= 0, pose2d[:, 1] <= height - 1)
check = np.bitwise_and(x_check, y_check)
joints_vis[np.logical_not(check)] = 0
pose2d[:, -1] = joints_vis
all_poses[cnt] = pose2d
person_ids[cnt] = body['id']
cnt += 1
if cnt > 0:
db.append({
'image_file':
osp.join(self.img_prefix, image_file),
'joints_3d':
all_poses_3d,
'person_ids':
person_ids,
'joints_3d_visible':
all_poses_vis_3d,
'joints': [all_poses],
'roots_3d':
all_roots_3d,
'camera':
cam_param,
'num_persons':
cnt,
'sample_id':
sample_id,
'center':
np.array((width / 2, height / 2),
dtype=np.float32),
'scale':
self._get_scale((width, height))
})
sample_id += 1
return db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='mpjpe', **kwargs):
"""
Args:
results (list[dict]): Testing results containing the following
items:
- pose_3d (np.ndarray): predicted 3D human pose
- sample_id (np.ndarray): sample id of a frame.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Defaults: 'mpjpe'.
**kwargs:
Returns:
"""
pose_3ds = np.concatenate([result['pose_3d'] for result in results],
axis=0)
sample_ids = []
for result in results:
sample_ids.extend(result['sample_id'])
_results = [
dict(sample_id=sample_id, pose_3d=pose_3d)
for (sample_id, pose_3d) in zip(sample_ids, pose_3ds)
]
_results = self._sort_and_unique_outputs(_results, key='sample_id')
metrics = metric if isinstance(metric, list) else [metric]
for _metric in metrics:
if _metric not in self.ALLOWED_METRICS:
raise ValueError(
f'Unsupported metric "{_metric}"'
f'Supported metrics are {self.ALLOWED_METRICS}')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
mmcv.dump(_results, res_file)
eval_list = []
gt_num = self.db_size // self.num_cameras
assert len(
_results) == gt_num, f'number mismatch: {len(_results)}, {gt_num}'
total_gt = 0
for i in range(gt_num):
index = self.num_cameras * i
db_rec = copy.deepcopy(self.db[index])
joints_3d = db_rec['joints_3d']
joints_3d_vis = db_rec['joints_3d_visible']
if joints_3d_vis.sum() < 1:
continue
pred = _results[i]['pose_3d'].copy()
pred = pred[pred[:, 0, 3] >= 0]
for pose in pred:
mpjpes = []
for (gt, gt_vis) in zip(joints_3d, joints_3d_vis):
vis = gt_vis[:, 0] > 0
if vis.sum() < 1:
break
mpjpe = np.mean(
np.sqrt(
np.sum((pose[vis, 0:3] - gt[vis])**2, axis=-1)))
mpjpes.append(mpjpe)
min_gt = np.argmin(mpjpes)
min_mpjpe = np.min(mpjpes)
score = pose[0, 4]
eval_list.append({
'mpjpe': float(min_mpjpe),
'score': float(score),
'gt_id': int(total_gt + min_gt)
})
total_gt += (joints_3d_vis[:, :, 0].sum(-1) >= 1).sum()
mpjpe_threshold = np.arange(25, 155, 25)
aps = []
ars = []
for t in mpjpe_threshold:
ap, ar = self._eval_list_to_ap(eval_list, total_gt, t)
aps.append(ap)
ars.append(ar)
name_value_tuples = []
for _metric in metrics:
if _metric == 'mpjpe':
stats_names = ['RECALL 500mm', 'MPJPE 500mm']
info_str = list(
zip(stats_names, [
self._eval_list_to_recall(eval_list, total_gt),
self._eval_list_to_mpjpe(eval_list)
]))
elif _metric == 'mAP':
stats_names = [
'AP 25', 'AP 50', 'AP 75', 'AP 100', 'AP 125', 'AP 150',
'mAP', 'AR 25', 'AR 50', 'AR 75', 'AR 100', 'AR 125',
'AR 150', 'mAR'
]
mAP = np.array(aps).mean()
mAR = np.array(ars).mean()
info_str = list(zip(stats_names, aps + [mAP] + ars + [mAR]))
else:
raise NotImplementedError
name_value_tuples.extend(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return OrderedDict(name_value_tuples)
@staticmethod
def _eval_list_to_ap(eval_list, total_gt, threshold):
"""Get Average Precision (AP) and Average Recall at a certain
threshold."""
eval_list.sort(key=lambda k: k['score'], reverse=True)
total_num = len(eval_list)
tp = np.zeros(total_num)
fp = np.zeros(total_num)
gt_det = []
for i, item in enumerate(eval_list):
if item['mpjpe'] < threshold and item['gt_id'] not in gt_det:
tp[i] = 1
gt_det.append(item['gt_id'])
else:
fp[i] = 1
tp = np.cumsum(tp)
fp = np.cumsum(fp)
recall = tp / (total_gt + 1e-5)
precise = tp / (tp + fp + 1e-5)
for n in range(total_num - 2, -1, -1):
precise[n] = max(precise[n], precise[n + 1])
precise = np.concatenate(([0], precise, [0]))
recall = np.concatenate(([0], recall, [1]))
index = np.where(recall[1:] != recall[:-1])[0]
ap = np.sum((recall[index + 1] - recall[index]) * precise[index + 1])
return ap, recall[-2]
@staticmethod
def _eval_list_to_mpjpe(eval_list, threshold=500):
"""Get MPJPE within a certain threshold."""
eval_list.sort(key=lambda k: k['score'], reverse=True)
gt_det = []
mpjpes = []
for i, item in enumerate(eval_list):
if item['mpjpe'] < threshold and item['gt_id'] not in gt_det:
mpjpes.append(item['mpjpe'])
gt_det.append(item['gt_id'])
return np.mean(mpjpes) if len(mpjpes) > 0 else np.inf
@staticmethod
def _eval_list_to_recall(eval_list, total_gt, threshold=500):
"""Get Recall at a certain threshold."""
gt_ids = [e['gt_id'] for e in eval_list if e['mpjpe'] < threshold]
return len(np.unique(gt_ids)) / total_gt
def __getitem__(self, idx):
"""Get the sample given index."""
results = {}
for c in range(self.num_cameras):
result = copy.deepcopy(self.db[self.num_cameras * idx + c])
result['ann_info'] = self.ann_info
width = 1920
height = 1080
result['mask'] = [np.ones((height, width), dtype=np.float32)]
results[c] = result
return self.pipeline(results)
@staticmethod
def _sort_and_unique_outputs(outputs, key='sample_id'):
"""sort outputs and remove the repeated ones."""
outputs = sorted(outputs, key=lambda x: x[key])
num_outputs = len(outputs)
for i in range(num_outputs - 1, 0, -1):
if outputs[i][key] == outputs[i - 1][key]:
del outputs[i]
return outputs
@@ -0,0 +1,41 @@
# Copyright (c) OpenMMLab. All rights reserved.
import numpy as np
from torch.utils.data import Dataset
from mmpose.datasets.builder import DATASETS, build_dataset
@DATASETS.register_module()
class Body3DSemiSupervisionDataset(Dataset):
"""Mix Dataset for semi-supervised training in 3D human pose estimation
task.
The dataset combines data from two datasets (a labeled one and an unlabeled
one) and return a dict containing data from two datasets.
Args:
labeled_dataset (Dataset): Dataset with 3D keypoint annotations.
unlabeled_dataset (Dataset): Dataset without 3D keypoint annotations.
"""
def __init__(self, labeled_dataset, unlabeled_dataset):
super().__init__()
self.labeled_dataset = build_dataset(labeled_dataset)
self.unlabeled_dataset = build_dataset(unlabeled_dataset)
self.length = len(self.unlabeled_dataset)
def __len__(self):
"""Get the size of the dataset."""
return self.length
def __getitem__(self, i):
"""Given index, get the data from unlabeled dataset and randomly sample
an item from labeled dataset.
Return a dict containing data from labeled and unlabeled dataset.
"""
data = self.unlabeled_dataset[i]
rand_ind = np.random.randint(0, len(self.labeled_dataset))
labeled_data = self.labeled_dataset[rand_ind]
data.update(labeled_data)
return data
@@ -0,0 +1,11 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .bottom_up_aic import BottomUpAicDataset
from .bottom_up_coco import BottomUpCocoDataset
from .bottom_up_coco_wholebody import BottomUpCocoWholeBodyDataset
from .bottom_up_crowdpose import BottomUpCrowdPoseDataset
from .bottom_up_mhp import BottomUpMhpDataset
__all__ = [
'BottomUpCocoDataset', 'BottomUpCrowdPoseDataset', 'BottomUpMhpDataset',
'BottomUpAicDataset', 'BottomUpCocoWholeBodyDataset'
]
@@ -0,0 +1,105 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
import json_tricks as json
from mmcv import Config
from xtcocotools.cocoeval import COCOeval
from mmpose.datasets.builder import DATASETS
from .bottom_up_coco import BottomUpCocoDataset
@DATASETS.register_module()
class BottomUpAicDataset(BottomUpCocoDataset):
"""Aic dataset for bottom-up pose estimation.
"AI Challenger : A Large-scale Dataset for Going Deeper
in Image Understanding", arXiv'2017.
More details can be found in the `paper
<https://arxiv.org/abs/1711.06475>`__
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
AIC keypoint indexes::
0: "right_shoulder",
1: "right_elbow",
2: "right_wrist",
3: "left_shoulder",
4: "left_elbow",
5: "left_wrist",
6: "right_hip",
7: "right_knee",
8: "right_ankle",
9: "left_hip",
10: "left_knee",
11: "left_ankle",
12: "head_top",
13: "neck"
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/aic.py')
dataset_info = cfg._cfg_dict['dataset_info']
super(BottomUpCocoDataset, self).__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
print(f'=> num_images: {self.num_images}')
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
stats_names = [
'AP', 'AP .5', 'AP .75', 'AP (M)', 'AP (L)', 'AR', 'AR .5',
'AR .75', 'AR (M)', 'AR (L)'
]
with open(res_file, 'r') as file:
res_json = json.load(file)
if not res_json:
info_str = list(zip(stats_names, [
0,
] * len(stats_names)))
return info_str
coco_det = self.coco.loadRes(res_file)
coco_eval = COCOeval(
self.coco, coco_det, 'keypoints', self.sigmas, use_area=False)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
@@ -0,0 +1,14 @@
# Copyright (c) OpenMMLab. All rights reserved.
from torch.utils.data import Dataset
class BottomUpBaseDataset(Dataset):
"""This class has been deprecated and replaced by
Kpt2dSviewRgbImgBottomUpDataset."""
def __init__(self, *args, **kwargs):
raise (ImportError(
'BottomUpBaseDataset has been replaced by '
'Kpt2dSviewRgbImgBottomUpDataset,'
'check https://github.com/open-mmlab/mmpose/pull/663 for details.')
)
@@ -0,0 +1,305 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict, defaultdict
import json_tricks as json
import numpy as np
from mmcv import Config, deprecated_api_warning
from xtcocotools.cocoeval import COCOeval
from mmpose.core.post_processing import oks_nms, soft_oks_nms
from mmpose.datasets.builder import DATASETS
from mmpose.datasets.datasets.base import Kpt2dSviewRgbImgBottomUpDataset
@DATASETS.register_module()
class BottomUpCocoDataset(Kpt2dSviewRgbImgBottomUpDataset):
"""COCO dataset for bottom-up pose estimation.
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
COCO keypoint indexes::
0: 'nose',
1: 'left_eye',
2: 'right_eye',
3: 'left_ear',
4: 'right_ear',
5: 'left_shoulder',
6: 'right_shoulder',
7: 'left_elbow',
8: 'right_elbow',
9: 'left_wrist',
10: 'right_wrist',
11: 'left_hip',
12: 'right_hip',
13: 'left_knee',
14: 'right_knee',
15: 'left_ankle',
16: 'right_ankle'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/coco.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
print(f'=> num_images: {self.num_images}')
def _get_single(self, idx):
"""Get anno for a single image.
Args:
idx (int): image idx
Returns:
dict: info for model training
"""
coco = self.coco
img_id = self.img_ids[idx]
ann_ids = coco.getAnnIds(imgIds=img_id)
anno = coco.loadAnns(ann_ids)
mask = self._get_mask(anno, idx)
anno = [
obj.copy() for obj in anno
if obj['iscrowd'] == 0 or obj['num_keypoints'] > 0
]
joints = self._get_joints(anno)
mask_list = [mask.copy() for _ in range(self.ann_info['num_scales'])]
joints_list = [
joints.copy() for _ in range(self.ann_info['num_scales'])
]
db_rec = {}
db_rec['dataset'] = self.dataset_name
db_rec['image_file'] = osp.join(self.img_prefix, self.id2name[img_id])
db_rec['mask'] = mask_list
db_rec['joints'] = joints_list
return db_rec
def _get_joints(self, anno):
"""Get joints for all people in an image."""
num_people = len(anno)
if self.ann_info['scale_aware_sigma']:
joints = np.zeros((num_people, self.ann_info['num_joints'], 4),
dtype=np.float32)
else:
joints = np.zeros((num_people, self.ann_info['num_joints'], 3),
dtype=np.float32)
for i, obj in enumerate(anno):
joints[i, :, :3] = \
np.array(obj['keypoints']).reshape([-1, 3])
if self.ann_info['scale_aware_sigma']:
# get person box
box = obj['bbox']
size = max(box[2], box[3])
sigma = size / self.base_size * self.base_sigma
if self.int_sigma:
sigma = int(np.ceil(sigma))
assert sigma > 0, sigma
joints[i, :, 3] = sigma
return joints
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='mAP', **kwargs):
"""Evaluate coco keypoint results. The pose prediction results will be
saved in ``${res_folder}/result_keypoints.json``.
Note:
- num_people: P
- num_keypoints: K
Args:
results (list[dict]): Testing results containing the following
items:
- preds (list[np.ndarray(P, K, 3+tag_num)]): \
Pose predictions for all people in images.
- scores (list[P]): List of person scores.
- image_path (list[str]): For example, ['coco/images/\
val2017/000000397133.jpg']
- heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed. Defaults: 'mAP'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['mAP']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
preds = []
scores = []
image_paths = []
for result in results:
preds.append(result['preds'])
scores.append(result['scores'])
image_paths.append(result['image_paths'][0])
kpts = defaultdict(list)
# iterate over images
for idx, _preds in enumerate(preds):
str_image_path = image_paths[idx]
image_id = self.name2id[osp.basename(str_image_path)]
# iterate over people
for idx_person, kpt in enumerate(_preds):
# use bbox area
area = (np.max(kpt[:, 0]) - np.min(kpt[:, 0])) * (
np.max(kpt[:, 1]) - np.min(kpt[:, 1]))
kpts[image_id].append({
'keypoints': kpt[:, 0:3],
'score': scores[idx][idx_person],
'tags': kpt[:, 3],
'image_id': image_id,
'area': area,
})
valid_kpts = []
for img in kpts.keys():
img_kpts = kpts[img]
if self.use_nms:
nms = soft_oks_nms if self.soft_nms else oks_nms
keep = nms(img_kpts, self.oks_thr, sigmas=self.sigmas)
valid_kpts.append([img_kpts[_keep] for _keep in keep])
else:
valid_kpts.append(img_kpts)
self._write_coco_keypoint_results(valid_kpts, res_file)
info_str = self._do_python_keypoint_eval(res_file)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
def _write_coco_keypoint_results(self, keypoints, res_file):
"""Write results into a json file."""
data_pack = [{
'cat_id': self._class_to_coco_ind[cls],
'cls_ind': cls_ind,
'cls': cls,
'ann_type': 'keypoints',
'keypoints': keypoints
} for cls_ind, cls in enumerate(self.classes)
if not cls == '__background__']
results = self._coco_keypoint_results_one_category_kernel(data_pack[0])
with open(res_file, 'w') as f:
json.dump(results, f, sort_keys=True, indent=4)
def _coco_keypoint_results_one_category_kernel(self, data_pack):
"""Get coco keypoint results."""
cat_id = data_pack['cat_id']
keypoints = data_pack['keypoints']
cat_results = []
for img_kpts in keypoints:
if len(img_kpts) == 0:
continue
_key_points = np.array(
[img_kpt['keypoints'] for img_kpt in img_kpts])
key_points = _key_points.reshape(-1,
self.ann_info['num_joints'] * 3)
for img_kpt, key_point in zip(img_kpts, key_points):
kpt = key_point.reshape((self.ann_info['num_joints'], 3))
left_top = np.amin(kpt, axis=0)
right_bottom = np.amax(kpt, axis=0)
w = right_bottom[0] - left_top[0]
h = right_bottom[1] - left_top[1]
cat_results.append({
'image_id': img_kpt['image_id'],
'category_id': cat_id,
'keypoints': key_point.tolist(),
'score': img_kpt['score'],
'bbox': [left_top[0], left_top[1], w, h]
})
return cat_results
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
stats_names = [
'AP', 'AP .5', 'AP .75', 'AP (M)', 'AP (L)', 'AR', 'AR .5',
'AR .75', 'AR (M)', 'AR (L)'
]
with open(res_file, 'r') as file:
res_json = json.load(file)
if not res_json:
info_str = list(zip(stats_names, [
0,
] * len(stats_names)))
return info_str
coco_det = self.coco.loadRes(res_file)
coco_eval = COCOeval(self.coco, coco_det, 'keypoints', self.sigmas)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
@@ -0,0 +1,238 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
import numpy as np
from mmcv import Config
from xtcocotools.cocoeval import COCOeval
from mmpose.datasets.builder import DATASETS
from .bottom_up_coco import BottomUpCocoDataset
@DATASETS.register_module()
class BottomUpCocoWholeBodyDataset(BottomUpCocoDataset):
"""CocoWholeBodyDataset dataset for bottom-up pose estimation.
`Whole-Body Human Pose Estimation in the Wild', ECCV'2020.
More details can be found in the `paper
<https://arxiv.org/abs/2007.11858>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
In total, we have 133 keypoints for wholebody pose estimation.
COCO-WholeBody keypoint indexes::
0-16: 17 body keypoints,
17-22: 6 foot keypoints,
23-90: 68 face keypoints,
91-132: 42 hand keypoints
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/coco_wholebody.py')
dataset_info = cfg._cfg_dict['dataset_info']
super(BottomUpCocoDataset, self).__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.body_num = 17
self.foot_num = 6
self.face_num = 68
self.left_hand_num = 21
self.right_hand_num = 21
print(f'=> num_images: {self.num_images}')
def _get_joints(self, anno):
"""Get joints for all people in an image."""
num_people = len(anno)
if self.ann_info['scale_aware_sigma']:
joints = np.zeros((num_people, self.ann_info['num_joints'], 4),
dtype=np.float32)
else:
joints = np.zeros((num_people, self.ann_info['num_joints'], 3),
dtype=np.float32)
for i, obj in enumerate(anno):
keypoints = np.array(obj['keypoints'] + obj['foot_kpts'] +
obj['face_kpts'] + obj['lefthand_kpts'] +
obj['righthand_kpts']).reshape(-1, 3)
joints[i, :self.ann_info['num_joints'], :3] = keypoints
if self.ann_info['scale_aware_sigma']:
# get person box
box = obj['bbox']
size = max(box[2], box[3])
sigma = size / self.base_size * self.base_sigma
if self.int_sigma:
sigma = int(np.ceil(sigma))
assert sigma > 0, sigma
joints[i, :, 3] = sigma
return joints
def _coco_keypoint_results_one_category_kernel(self, data_pack):
"""Get coco keypoint results."""
cat_id = data_pack['cat_id']
keypoints = data_pack['keypoints']
cat_results = []
for img_kpts in keypoints:
if len(img_kpts) == 0:
continue
_key_points = np.array(
[img_kpt['keypoints'] for img_kpt in img_kpts])
key_points = _key_points.reshape(-1,
self.ann_info['num_joints'] * 3)
cuts = np.cumsum([
0, self.body_num, self.foot_num, self.face_num,
self.left_hand_num, self.right_hand_num
]) * 3
for img_kpt, key_point in zip(img_kpts, key_points):
kpt = key_point.reshape((self.ann_info['num_joints'], 3))
left_top = np.amin(kpt, axis=0)
right_bottom = np.amax(kpt, axis=0)
w = right_bottom[0] - left_top[0]
h = right_bottom[1] - left_top[1]
cat_results.append({
'image_id':
img_kpt['image_id'],
'category_id':
cat_id,
'keypoints':
key_point[cuts[0]:cuts[1]].tolist(),
'foot_kpts':
key_point[cuts[1]:cuts[2]].tolist(),
'face_kpts':
key_point[cuts[2]:cuts[3]].tolist(),
'lefthand_kpts':
key_point[cuts[3]:cuts[4]].tolist(),
'righthand_kpts':
key_point[cuts[4]:cuts[5]].tolist(),
'score':
img_kpt['score'],
'bbox': [left_top[0], left_top[1], w, h]
})
return cat_results
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
coco_det = self.coco.loadRes(res_file)
cuts = np.cumsum([
0, self.body_num, self.foot_num, self.face_num, self.left_hand_num,
self.right_hand_num
])
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_body',
self.sigmas[cuts[0]:cuts[1]],
use_area=True)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_foot',
self.sigmas[cuts[1]:cuts[2]],
use_area=True)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_face',
self.sigmas[cuts[2]:cuts[3]],
use_area=True)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_lefthand',
self.sigmas[cuts[3]:cuts[4]],
use_area=True)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_righthand',
self.sigmas[cuts[4]:cuts[5]],
use_area=True)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_wholebody',
self.sigmas,
use_area=True)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
stats_names = [
'AP', 'AP .5', 'AP .75', 'AP (M)', 'AP (L)', 'AR', 'AR .5',
'AR .75', 'AR (M)', 'AR (L)'
]
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
@@ -0,0 +1,109 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
import json_tricks as json
from mmcv import Config
from xtcocotools.cocoeval import COCOeval
from mmpose.datasets.builder import DATASETS
from .bottom_up_coco import BottomUpCocoDataset
@DATASETS.register_module()
class BottomUpCrowdPoseDataset(BottomUpCocoDataset):
"""CrowdPose dataset for bottom-up pose estimation.
"CrowdPose: Efficient Crowded Scenes Pose Estimation and
A New Benchmark", CVPR'2019.
More details can be found in the `paper
<https://arxiv.org/abs/1812.00324>`__.
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
CrowdPose keypoint indexes::
0: 'left_shoulder',
1: 'right_shoulder',
2: 'left_elbow',
3: 'right_elbow',
4: 'left_wrist',
5: 'right_wrist',
6: 'left_hip',
7: 'right_hip',
8: 'left_knee',
9: 'right_knee',
10: 'left_ankle',
11: 'right_ankle',
12: 'top_head',
13: 'neck'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/crowdpose.py')
dataset_info = cfg._cfg_dict['dataset_info']
super(BottomUpCocoDataset, self).__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
print(f'=> num_images: {self.num_images}')
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
stats_names = [
'AP', 'AP .5', 'AP .75', 'AR', 'AR .5', 'AR .75', 'AP(E)', 'AP(M)',
'AP(H)'
]
with open(res_file, 'r') as file:
res_json = json.load(file)
if not res_json:
info_str = list(zip(stats_names, [
0,
] * len(stats_names)))
return info_str
coco_det = self.coco.loadRes(res_file)
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_crowd',
self.sigmas,
use_area=False)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
@@ -0,0 +1,108 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
import json_tricks as json
from mmcv import Config
from xtcocotools.cocoeval import COCOeval
from mmpose.datasets.builder import DATASETS
from .bottom_up_coco import BottomUpCocoDataset
@DATASETS.register_module()
class BottomUpMhpDataset(BottomUpCocoDataset):
"""MHPv2.0 dataset for top-down pose estimation.
"Understanding Humans in Crowded Scenes: Deep Nested Adversarial
Learning and A New Benchmark for Multi-Human Parsing", ACM MM'2018.
More details can be found in the `paper
<https://arxiv.org/abs/1804.03287>`__
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
MHP keypoint indexes::
0: "right ankle",
1: "right knee",
2: "right hip",
3: "left hip",
4: "left knee",
5: "left ankle",
6: "pelvis",
7: "thorax",
8: "upper neck",
9: "head top",
10: "right wrist",
11: "right elbow",
12: "right shoulder",
13: "left shoulder",
14: "left elbow",
15: "left wrist",
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/mhp.py')
dataset_info = cfg._cfg_dict['dataset_info']
super(BottomUpCocoDataset, self).__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
print(f'=> num_images: {self.num_images}')
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
stats_names = [
'AP', 'AP .5', 'AP .75', 'AP (M)', 'AP (L)', 'AR', 'AR .5',
'AR .75', 'AR (M)', 'AR (L)'
]
with open(res_file, 'r') as file:
res_json = json.load(file)
if not res_json:
info_str = list(zip(stats_names, [
0,
] * len(stats_names)))
return info_str
coco_det = self.coco.loadRes(res_file)
coco_eval = COCOeval(
self.coco, coco_det, 'keypoints', self.sigmas, use_area=False)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
@@ -0,0 +1,11 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .face_300w_dataset import Face300WDataset
from .face_aflw_dataset import FaceAFLWDataset
from .face_coco_wholebody_dataset import FaceCocoWholeBodyDataset
from .face_cofw_dataset import FaceCOFWDataset
from .face_wflw_dataset import FaceWFLWDataset
__all__ = [
'Face300WDataset', 'FaceAFLWDataset', 'FaceWFLWDataset', 'FaceCOFWDataset',
'FaceCocoWholeBodyDataset'
]
@@ -0,0 +1,199 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.datasets.builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class Face300WDataset(Kpt2dSviewRgbImgTopDownDataset):
"""Face300W dataset for top-down face keypoint localization.
"300 faces In-the-wild challenge: Database and results",
Image and Vision Computing (IMAVIS) 2019.
The dataset loads raw images and apply specified transforms
to return a dict containing the image tensors and other information.
The landmark annotations follow the 68 points mark-up. The definition
can be found in `https://ibug.doc.ic.ac.uk/resources/300-W/`.
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/300w.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
if 'center' in obj and 'scale' in obj:
center = np.array(obj['center'])
scale = np.array([obj['scale'], obj['scale']]) * 1.25
else:
center, scale = self._xywh2cs(*obj['bbox'][:4], 1.25)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
def _get_normalize_factor(self, gts, *args, **kwargs):
"""Get inter-ocular distance as the normalize factor, measured as the
Euclidean distance between the outer corners of the eyes.
Args:
gts (np.ndarray[N, K, 2]): Groundtruth keypoint location.
Returns:
np.ndarray[N, 2]: normalized factor
"""
interocular = np.linalg.norm(
gts[:, 36, :] - gts[:, 45, :], axis=1, keepdims=True)
return np.tile(interocular, [1, 2])
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='NME', **kwargs):
"""Evaluate freihand keypoint results. The pose prediction results will
be saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[1,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[1,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_path (list[str]): For example, ['300W/ibug/\
image_018.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'NME'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['NME']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,205 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.datasets.builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class FaceAFLWDataset(Kpt2dSviewRgbImgTopDownDataset):
"""Face AFLW dataset for top-down face keypoint localization.
"Annotated Facial Landmarks in the Wild: A Large-scale,
Real-world Database for Facial Landmark Localization".
In Proc. First IEEE International Workshop on Benchmarking
Facial Image Analysis Technologies, 2011.
The dataset loads raw images and apply specified transforms
to return a dict containing the image tensors and other information.
The landmark annotations follow the 19 points mark-up. The definition
can be found in `https://www.tugraz.at/institute/icg/research`
`/team-bischof/lrs/downloads/aflw/`
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/aflw.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if self.test_mode:
# 'box_size' is used as normalization factor
assert 'box_size' in obj
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
if 'center' in obj and 'scale' in obj:
center = np.array(obj['center'])
scale = np.array([obj['scale'], obj['scale']]) * 1.25
else:
center, scale = self._xywh2cs(*obj['bbox'][:4], 1.25)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'box_size': obj['box_size'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
def _get_normalize_factor(self, box_sizes, *args, **kwargs):
"""Get normalize factor for evaluation.
Args:
box_sizes (np.ndarray[N, 1]): box size
Returns:
np.ndarray[N, 2]: normalized factor
"""
return np.tile(box_sizes, [1, 2])
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='NME', **kwargs):
"""Evaluate freihand keypoint results. The pose prediction results will
be saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[1,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[1,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_path (list[str]): For example, ['aflw/images/flickr/ \
0/image00002.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'NME'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['NME']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,16 @@
# Copyright (c) OpenMMLab. All rights reserved.
from abc import ABCMeta
from torch.utils.data import Dataset
class FaceBaseDataset(Dataset, metaclass=ABCMeta):
"""This class has been deprecated and replaced by
Kpt2dSviewRgbImgTopDownDataset."""
def __init__(self, *args, **kwargs):
raise (ImportError(
'FaceBaseDataset has been replaced by '
'Kpt2dSviewRgbImgTopDownDataset,'
'check https://github.com/open-mmlab/mmpose/pull/663 for details.')
)
@@ -0,0 +1,198 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.datasets.builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class FaceCocoWholeBodyDataset(Kpt2dSviewRgbImgTopDownDataset):
"""CocoWholeBodyDataset for face keypoint localization.
`Whole-Body Human Pose Estimation in the Wild', ECCV'2020.
More details can be found in the `paper
<https://arxiv.org/abs/2007.11858>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
The face landmark annotations follow the 68 points mark-up.
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/'
'coco_wholebody_face.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if obj['face_valid'] and max(obj['face_kpts']) > 0:
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3),
dtype=np.float32)
keypoints = np.array(obj['face_kpts']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
center, scale = self._xywh2cs(*obj['face_box'][:4], 1.25)
image_file = osp.join(self.img_prefix,
self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['face_box'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
def _get_normalize_factor(self, gts, *args, **kwargs):
"""Get inter-ocular distance as the normalize factor, measured as the
Euclidean distance between the outer corners of the eyes.
Args:
gts (np.ndarray[N, K, 2]): Groundtruth keypoint location.
Returns:
np.ndarray[N, 2]: normalized factor
"""
interocular = np.linalg.norm(
gts[:, 36, :] - gts[:, 45, :], axis=1, keepdims=True)
return np.tile(interocular, [1, 2])
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='NME', **kwargs):
"""Evaluate COCO-WholeBody Face keypoint results. The pose prediction
results will be saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[1,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[1,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_path (list[str]): For example, ['coco/train2017/\
000000000009.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'NME'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['NME']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,198 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.datasets.builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class FaceCOFWDataset(Kpt2dSviewRgbImgTopDownDataset):
"""Face COFW dataset for top-down face keypoint localization.
"Robust face landmark estimation under occlusion", ICCV'2013.
The dataset loads raw images and apply specified transforms
to return a dict containing the image tensors and other information.
The landmark annotations follow the 29 points mark-up. The definition
can be found in `http://www.vision.caltech.edu/xpburgos/ICCV13/`.
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/cofw.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
if 'center' in obj and 'scale' in obj:
center = np.array(obj['center'])
scale = np.array([obj['scale'], obj['scale']]) * 1.25
else:
center, scale = self._xywh2cs(*obj['bbox'][:4], 1.25)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
def _get_normalize_factor(self, gts, *args, **kwargs):
"""Get normalize factor for evaluation.
Args:
gts (np.ndarray[N, K, 2]): Groundtruth keypoint location.
Returns:
np.ndarray[N, 2]: normalized factor
"""
interocular = np.linalg.norm(
gts[:, 8, :] - gts[:, 9, :], axis=1, keepdims=True)
return np.tile(interocular, [1, 2])
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='NME', **kwargs):
"""Evaluate freihand keypoint results. The pose prediction results will
be saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[1,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[1,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_path (list[str]): For example, ['cofw/images/\
000001.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'NME'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['NME']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,199 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.datasets.builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class FaceWFLWDataset(Kpt2dSviewRgbImgTopDownDataset):
"""Face WFLW dataset for top-down face keypoint localization.
"Look at Boundary: A Boundary-Aware Face Alignment Algorithm",
CVPR'2018.
The dataset loads raw images and apply specified transforms
to return a dict containing the image tensors and other information.
The landmark annotations follow the 98 points mark-up. The definition
can be found in `https://wywu.github.io/projects/LAB/WFLW.html`.
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/wflw.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
if 'center' in obj and 'scale' in obj:
center = np.array(obj['center'])
scale = np.array([obj['scale'], obj['scale']]) * 1.25
else:
center, scale = self._xywh2cs(*obj['bbox'][:4], 1.25)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
def _get_normalize_factor(self, gts, *args, **kwargs):
"""Get normalize factor for evaluation.
Args:
gts (np.ndarray[N, K, 2]): Groundtruth keypoint location.
Returns:
np.ndarray[N, 2]: normalized factor
"""
interocular = np.linalg.norm(
gts[:, 60, :] - gts[:, 72, :], axis=1, keepdims=True)
return np.tile(interocular, [1, 2])
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='NME', **kwargs):
"""Evaluate freihand keypoint results. The pose prediction results will
be saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[1,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[1,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_path (list[str]): For example, ['wflw/images/\
0--Parade/0_Parade_marchingband_1_1015.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'NME'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['NME']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,4 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .deepfashion_dataset import DeepFashionDataset
__all__ = ['DeepFashionDataset']
@@ -0,0 +1,225 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.datasets.builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class DeepFashionDataset(Kpt2dSviewRgbImgTopDownDataset):
"""DeepFashion dataset (full-body clothes) for fashion landmark detection.
"DeepFashion: Powering Robust Clothes Recognition
and Retrieval with Rich Annotations", CVPR'2016.
"Fashion Landmark Detection in the Wild", ECCV'2016.
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
The dataset contains 3 categories for full-body, upper-body and lower-body.
Fashion landmark indexes for upper-body clothes::
0: 'left collar',
1: 'right collar',
2: 'left sleeve',
3: 'right sleeve',
4: 'left hem',
5: 'right hem'
Fashion landmark indexes for lower-body clothes::
0: 'left waistline',
1: 'right waistline',
2: 'left hem',
3: 'right hem'
Fashion landmark indexes for full-body clothes::
0: 'left collar',
1: 'right collar',
2: 'left sleeve',
3: 'right sleeve',
4: 'left waistline',
5: 'right waistline',
6: 'left hem',
7: 'right hem'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
subset='',
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
if subset != '':
warnings.warn(
'subset is deprecated.'
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
if subset == 'upper':
cfg = Config.fromfile(
'configs/_base_/datasets/deepfashion_upper.py')
dataset_info = cfg._cfg_dict['dataset_info']
elif subset == 'lower':
cfg = Config.fromfile(
'configs/_base_/datasets/deepfashion_lower.py')
dataset_info = cfg._cfg_dict['dataset_info']
elif subset == 'full':
cfg = Config.fromfile(
'configs/_base_/datasets/deepfashion_full.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
# use 1.25bbox as input
center, scale = self._xywh2cs(*obj['bbox'][:4], 1.25)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCK', **kwargs):
"""Evaluate freihand keypoint results. The pose prediction results will
be saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['img_00000001.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'PCK', 'AUC', 'EPE'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCK', 'AUC', 'EPE']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,16 @@
# Copyright (c) OpenMMLab. All rights reserved.
from abc import ABCMeta
from torch.utils.data import Dataset
class FashionBaseDataset(Dataset, metaclass=ABCMeta):
"""This class has been deprecated and replaced by
Kpt2dSviewRgbImgTopDownDataset."""
def __init__(self, *args, **kwargs):
raise (ImportError(
'FashionBaseDataset has been replaced by '
'Kpt2dSviewRgbImgTopDownDataset,'
'check https://github.com/open-mmlab/mmpose/pull/663 for details.')
)
@@ -0,0 +1,14 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .freihand_dataset import FreiHandDataset
from .hand_coco_wholebody_dataset import HandCocoWholeBodyDataset
from .interhand2d_dataset import InterHand2DDataset
from .interhand3d_dataset import InterHand3DDataset
from .onehand10k_dataset import OneHand10KDataset
from .panoptic_hand2d_dataset import PanopticDataset
from .rhd2d_dataset import Rhd2DDataset
__all__ = [
'FreiHandDataset', 'InterHand2DDataset', 'InterHand3DDataset',
'OneHand10KDataset', 'PanopticDataset', 'Rhd2DDataset',
'HandCocoWholeBodyDataset'
]
@@ -0,0 +1,205 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.datasets.builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class FreiHandDataset(Kpt2dSviewRgbImgTopDownDataset):
"""FreiHand dataset for top-down hand pose estimation.
"FreiHAND: A Dataset for Markerless Capture of Hand Pose
and Shape from Single RGB Images", ICCV'2019.
More details can be found in the `paper
<https://arxiv.org/pdf/1909.04349.pdf>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
FreiHand keypoint indexes::
0: 'wrist',
1: 'thumb1',
2: 'thumb2',
3: 'thumb3',
4: 'thumb4',
5: 'forefinger1',
6: 'forefinger2',
7: 'forefinger3',
8: 'forefinger4',
9: 'middle_finger1',
10: 'middle_finger2',
11: 'middle_finger3',
12: 'middle_finger4',
13: 'ring_finger1',
14: 'ring_finger2',
15: 'ring_finger3',
16: 'ring_finger4',
17: 'pinky_finger1',
18: 'pinky_finger2',
19: 'pinky_finger3',
20: 'pinky_finger4'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/freihand2d.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
# the ori image is 224x224
center, scale = self._xywh2cs(0, 0, 224, 224, 0.8)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCK', **kwargs):
"""Evaluate freihand keypoint results. The pose prediction results will
be saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['training/rgb/\
00031426.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'PCK', 'AUC', 'EPE'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCK', 'AUC', 'EPE']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,16 @@
# Copyright (c) OpenMMLab. All rights reserved.
from abc import ABCMeta
from torch.utils.data import Dataset
class HandBaseDataset(Dataset, metaclass=ABCMeta):
"""This class has been deprecated and replaced by
Kpt2dSviewRgbImgTopDownDataset."""
def __init__(self, *args, **kwargs):
raise (ImportError(
'HandBaseDataset has been replaced by '
'Kpt2dSviewRgbImgTopDownDataset,'
'check https://github.com/open-mmlab/mmpose/pull/663 for details.')
)
@@ -0,0 +1,211 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.datasets.builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class HandCocoWholeBodyDataset(Kpt2dSviewRgbImgTopDownDataset):
"""CocoWholeBodyDataset for top-down hand pose estimation.
"Whole-Body Human Pose Estimation in the Wild", ECCV'2020.
More details can be found in the `paper
<https://arxiv.org/abs/2007.11858>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
COCO-WholeBody Hand keypoint indexes::
0: 'wrist',
1: 'thumb1',
2: 'thumb2',
3: 'thumb3',
4: 'thumb4',
5: 'forefinger1',
6: 'forefinger2',
7: 'forefinger3',
8: 'forefinger4',
9: 'middle_finger1',
10: 'middle_finger2',
11: 'middle_finger3',
12: 'middle_finger4',
13: 'ring_finger1',
14: 'ring_finger2',
15: 'ring_finger3',
16: 'ring_finger4',
17: 'pinky_finger1',
18: 'pinky_finger2',
19: 'pinky_finger3',
20: 'pinky_finger4'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile(
'configs/_base_/datasets/coco_wholebody_hand.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
for type in ['left', 'right']:
if obj[f'{type}hand_valid'] and max(
obj[f'{type}hand_kpts']) > 0:
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3),
dtype=np.float32)
keypoints = np.array(obj[f'{type}hand_kpts']).reshape(
-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(
1, keypoints[:, 2:3])
# use 1.25 padded bbox as input
center, scale = self._xywh2cs(
*obj[f'{type}hand_box'][:4], 1.25)
image_file = osp.join(self.img_prefix,
self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj[f'{type}hand_box'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCK', **kwargs):
"""Evaluate COCO-WholeBody Hand keypoint results. The pose prediction
results will be saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['Test/source/0.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'PCK', 'AUC', 'EPE'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCK', 'AUC', 'EPE']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,306 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import json_tricks as json
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.datasets.builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class InterHand2DDataset(Kpt2dSviewRgbImgTopDownDataset):
"""InterHand2.6M 2D dataset for top-down hand pose estimation.
"InterHand2.6M: A Dataset and Baseline for 3D Interacting Hand Pose
Estimation from a Single RGB Image", ECCV'2020.
More details can be found in the `paper
<https://arxiv.org/pdf/2008.09309.pdf>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
InterHand2.6M keypoint indexes::
0: 'thumb4',
1: 'thumb3',
2: 'thumb2',
3: 'thumb1',
4: 'forefinger4',
5: 'forefinger3',
6: 'forefinger2',
7: 'forefinger1',
8: 'middle_finger4',
9: 'middle_finger3',
10: 'middle_finger2',
11: 'middle_finger1',
12: 'ring_finger4',
13: 'ring_finger3',
14: 'ring_finger2',
15: 'ring_finger1',
16: 'pinky_finger4',
17: 'pinky_finger3',
18: 'pinky_finger2',
19: 'pinky_finger1',
20: 'wrist'
Args:
ann_file (str): Path to the annotation file.
camera_file (str): Path to the camera file.
joint_file (str): Path to the joint file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (str): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
camera_file,
joint_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/interhand2d.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.camera_file = camera_file
self.joint_file = joint_file
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
@staticmethod
def _cam2pixel(cam_coord, f, c):
"""Transform the joints from their camera coordinates to their pixel
coordinates.
Note:
- N: number of joints
Args:
cam_coord (ndarray[N, 3]): 3D joints coordinates
in the camera coordinate system
f (ndarray[2]): focal length of x and y axis
c (ndarray[2]): principal point of x and y axis
Returns:
img_coord (ndarray[N, 3]): the coordinates (x, y, 0)
in the image plane.
"""
x = cam_coord[:, 0] / (cam_coord[:, 2] + 1e-8) * f[0] + c[0]
y = cam_coord[:, 1] / (cam_coord[:, 2] + 1e-8) * f[1] + c[1]
z = np.zeros_like(x)
img_coord = np.concatenate((x[:, None], y[:, None], z[:, None]), 1)
return img_coord
@staticmethod
def _world2cam(world_coord, R, T):
"""Transform the joints from their world coordinates to their camera
coordinates.
Note:
- N: number of joints
Args:
world_coord (ndarray[3, N]): 3D joints coordinates
in the world coordinate system
R (ndarray[3, 3]): camera rotation matrix
T (ndarray[3]): camera position (x, y, z)
Returns:
cam_coord (ndarray[3, N]): 3D joints coordinates
in the camera coordinate system
"""
cam_coord = np.dot(R, world_coord - T)
return cam_coord
def _get_db(self):
"""Load dataset.
Adapted from 'https://github.com/facebookresearch/InterHand2.6M/'
'blob/master/data/InterHand2.6M/dataset.py'
Copyright (c) FaceBook Research, under CC-BY-NC 4.0 license.
"""
with open(self.camera_file, 'r') as f:
cameras = json.load(f)
with open(self.joint_file, 'r') as f:
joints = json.load(f)
gt_db = []
bbox_id = 0
for img_id in self.img_ids:
num_joints = self.ann_info['num_joints']
ann_id = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
ann = self.coco.loadAnns(ann_id)[0]
img = self.coco.loadImgs(img_id)[0]
capture_id = str(img['capture'])
camera_name = img['camera']
frame_idx = str(img['frame_idx'])
image_file = osp.join(self.img_prefix, self.id2name[img_id])
camera_pos, camera_rot = np.array(
cameras[capture_id]['campos'][camera_name],
dtype=np.float32), np.array(
cameras[capture_id]['camrot'][camera_name],
dtype=np.float32)
focal, principal_pt = np.array(
cameras[capture_id]['focal'][camera_name],
dtype=np.float32), np.array(
cameras[capture_id]['princpt'][camera_name],
dtype=np.float32)
joint_world = np.array(
joints[capture_id][frame_idx]['world_coord'], dtype=np.float32)
joint_cam = self._world2cam(
joint_world.transpose(1, 0), camera_rot,
camera_pos.reshape(3, 1)).transpose(1, 0)
joint_img = self._cam2pixel(joint_cam, focal, principal_pt)[:, :2]
joint_img = joint_img.reshape(2, -1, 2)
joint_valid = np.array(
ann['joint_valid'], dtype=np.float32).reshape(2, -1)
# if root is not valid -> root-relative 3D pose is also not valid.
# Therefore, mark all joints as invalid
for hand in range(2):
joint_valid[hand, :] *= joint_valid[hand][-1]
if np.sum(joint_valid[hand, :]) > 11:
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3),
dtype=np.float32)
joints_3d[:, :2] = joint_img[hand, :, :]
joints_3d_visible[:, :2] = np.minimum(
1, joint_valid[hand, :].reshape(-1, 1))
# use the tightest bbox enclosing all keypoints as bbox
bbox = [img['width'], img['height'], 0, 0]
for i in range(num_joints):
if joints_3d_visible[i][0]:
bbox[0] = min(bbox[0], joints_3d[i][0])
bbox[1] = min(bbox[1], joints_3d[i][1])
bbox[2] = max(bbox[2], joints_3d[i][0])
bbox[3] = max(bbox[3], joints_3d[i][1])
bbox[2] -= bbox[0]
bbox[3] -= bbox[1]
# use 1.5bbox as input
center, scale = self._xywh2cs(*bbox, 1.5)
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': bbox,
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCK', **kwargs):
"""Evaluate interhand2d keypoint results. The pose prediction results
will be saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['Capture12/\
0390_dh_touchROM/cam410209/image62434.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'PCK', 'AUC', 'EPE'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCK', 'AUC', 'EPE']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,505 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import json_tricks as json
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.core.evaluation.top_down_eval import keypoint_epe
from mmpose.datasets.builder import DATASETS
from ..base import Kpt3dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class InterHand3DDataset(Kpt3dSviewRgbImgTopDownDataset):
"""InterHand2.6M 3D dataset for top-down hand pose estimation.
"InterHand2.6M: A Dataset and Baseline for 3D Interacting Hand Pose
Estimation from a Single RGB Image", ECCV'2020.
More details can be found in the `paper
<https://arxiv.org/pdf/2008.09309.pdf>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
InterHand2.6M keypoint indexes::
0: 'r_thumb4',
1: 'r_thumb3',
2: 'r_thumb2',
3: 'r_thumb1',
4: 'r_index4',
5: 'r_index3',
6: 'r_index2',
7: 'r_index1',
8: 'r_middle4',
9: 'r_middle3',
10: 'r_middle2',
11: 'r_middle1',
12: 'r_ring4',
13: 'r_ring3',
14: 'r_ring2',
15: 'r_ring1',
16: 'r_pinky4',
17: 'r_pinky3',
18: 'r_pinky2',
19: 'r_pinky1',
20: 'r_wrist',
21: 'l_thumb4',
22: 'l_thumb3',
23: 'l_thumb2',
24: 'l_thumb1',
25: 'l_index4',
26: 'l_index3',
27: 'l_index2',
28: 'l_index1',
29: 'l_middle4',
30: 'l_middle3',
31: 'l_middle2',
32: 'l_middle1',
33: 'l_ring4',
34: 'l_ring3',
35: 'l_ring2',
36: 'l_ring1',
37: 'l_pinky4',
38: 'l_pinky3',
39: 'l_pinky2',
40: 'l_pinky1',
41: 'l_wrist'
Args:
ann_file (str): Path to the annotation file.
camera_file (str): Path to the camera file.
joint_file (str): Path to the joint file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
use_gt_root_depth (bool): Using the ground truth depth of the wrist
or given depth from rootnet_result_file.
rootnet_result_file (str): Path to the wrist depth file.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (str): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
camera_file,
joint_file,
img_prefix,
data_cfg,
pipeline,
use_gt_root_depth=True,
rootnet_result_file=None,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/interhand3d.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['heatmap3d_depth_bound'] = data_cfg[
'heatmap3d_depth_bound']
self.ann_info['heatmap_size_root'] = data_cfg['heatmap_size_root']
self.ann_info['root_depth_bound'] = data_cfg['root_depth_bound']
self.ann_info['use_different_joint_weights'] = False
self.camera_file = camera_file
self.joint_file = joint_file
self.use_gt_root_depth = use_gt_root_depth
if not self.use_gt_root_depth:
assert rootnet_result_file is not None
self.rootnet_result_file = rootnet_result_file
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
@staticmethod
def _encode_handtype(hand_type):
if hand_type == 'right':
return np.array([1, 0], dtype=np.float32)
elif hand_type == 'left':
return np.array([0, 1], dtype=np.float32)
elif hand_type == 'interacting':
return np.array([1, 1], dtype=np.float32)
else:
assert 0, f'Not support hand type: {hand_type}'
def _get_db(self):
"""Load dataset.
Adapted from 'https://github.com/facebookresearch/InterHand2.6M/'
'blob/master/data/InterHand2.6M/dataset.py'
Copyright (c) FaceBook Research, under CC-BY-NC 4.0 license.
"""
with open(self.camera_file, 'r') as f:
cameras = json.load(f)
with open(self.joint_file, 'r') as f:
joints = json.load(f)
if not self.use_gt_root_depth:
rootnet_result = {}
with open(self.rootnet_result_file, 'r') as f:
rootnet_annot = json.load(f)
for i in range(len(rootnet_annot)):
rootnet_result[str(
rootnet_annot[i]['annot_id'])] = rootnet_annot[i]
gt_db = []
bbox_id = 0
for img_id in self.img_ids:
num_joints = self.ann_info['num_joints']
ann_id = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
ann = self.coco.loadAnns(ann_id)[0]
img = self.coco.loadImgs(img_id)[0]
capture_id = str(img['capture'])
camera_name = img['camera']
frame_idx = str(img['frame_idx'])
image_file = osp.join(self.img_prefix, self.id2name[img_id])
camera_pos = np.array(
cameras[capture_id]['campos'][camera_name], dtype=np.float32)
camera_rot = np.array(
cameras[capture_id]['camrot'][camera_name], dtype=np.float32)
focal = np.array(
cameras[capture_id]['focal'][camera_name], dtype=np.float32)
principal_pt = np.array(
cameras[capture_id]['princpt'][camera_name], dtype=np.float32)
joint_world = np.array(
joints[capture_id][frame_idx]['world_coord'], dtype=np.float32)
joint_cam = self._world2cam(
joint_world.transpose(1, 0), camera_rot,
camera_pos.reshape(3, 1)).transpose(1, 0)
joint_img = self._cam2pixel(joint_cam, focal, principal_pt)[:, :2]
joint_valid = np.array(
ann['joint_valid'], dtype=np.float32).flatten()
hand_type = self._encode_handtype(ann['hand_type'])
hand_type_valid = ann['hand_type_valid']
if self.use_gt_root_depth:
bbox = np.array(ann['bbox'], dtype=np.float32)
# extend the bbox to include some context
center, scale = self._xywh2cs(*bbox, 1.25)
abs_depth = [joint_cam[20, 2], joint_cam[41, 2]]
else:
rootnet_ann_data = rootnet_result[str(ann_id[0])]
bbox = np.array(rootnet_ann_data['bbox'], dtype=np.float32)
# the bboxes have been extended
center, scale = self._xywh2cs(*bbox, 1.0)
abs_depth = rootnet_ann_data['abs_depth']
# 41: 'l_wrist', left hand root
# 20: 'r_wrist', right hand root
rel_root_depth = joint_cam[41, 2] - joint_cam[20, 2]
# if root is not valid, root-relative 3D depth is also invalid.
rel_root_valid = joint_valid[20] * joint_valid[41]
# if root is not valid -> root-relative 3D pose is also not valid.
# Therefore, mark all joints as invalid
joint_valid[:20] *= joint_valid[20]
joint_valid[21:] *= joint_valid[41]
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d[:, :2] = joint_img
joints_3d[:21, 2] = joint_cam[:21, 2] - joint_cam[20, 2]
joints_3d[21:, 2] = joint_cam[21:, 2] - joint_cam[41, 2]
joints_3d_visible[...] = np.minimum(1, joint_valid.reshape(-1, 1))
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'hand_type': hand_type,
'hand_type_valid': hand_type_valid,
'rel_root_depth': rel_root_depth,
'rel_root_valid': rel_root_valid,
'abs_depth': abs_depth,
'joints_cam': joint_cam,
'focal': focal,
'princpt': principal_pt,
'dataset': self.dataset_name,
'bbox': bbox,
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='MPJPE', **kwargs):
"""Evaluate interhand2d keypoint results. The pose prediction results
will be saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- hand_type (np.ndarray[N, 4]): The first two dimensions are \
hand type, scores is the last two dimensions.
- rel_root_depth (np.ndarray[N]): The relative depth of left \
wrist and right wrist.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['Capture6/\
0012_aokay_upright/cam410061/image4996.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'MRRPE', 'MPJPE', 'Handedness_acc'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['MRRPE', 'MPJPE', 'Handedness_acc']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result.get('preds')
if preds is None and 'MPJPE' in metrics:
raise KeyError('metric MPJPE is not supported')
hand_type = result.get('hand_type')
if hand_type is None and 'Handedness_acc' in metrics:
raise KeyError('metric Handedness_acc is not supported')
rel_root_depth = result.get('rel_root_depth')
if rel_root_depth is None and 'MRRPE' in metrics:
raise KeyError('metric MRRPE is not supported')
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpt = {
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
}
if preds is not None:
kpt['keypoints'] = preds[i, :, :3].tolist()
if hand_type is not None:
kpt['hand_type'] = hand_type[i][0:2].tolist()
kpt['hand_type_score'] = hand_type[i][2:4].tolist()
if rel_root_depth is not None:
kpt['rel_root_depth'] = float(rel_root_depth[i])
kpts.append(kpt)
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@staticmethod
def _get_accuracy(outputs, gts, masks):
"""Get accuracy of multi-label classification.
Note:
- batch_size: N
- label_num: C
Args:
outputs (np.array[N, C]): predicted multi-label.
gts (np.array[N, C]): Groundtruth muti-label.
masks (np.array[N, ]): masked outputs will be ignored for
accuracy calculation.
Returns:
float: mean accuracy
"""
acc = (outputs == gts).all(axis=1)
return np.mean(acc[masks])
def _report_metric(self, res_file, metrics):
"""Keypoint evaluation.
Args:
res_file (str): Json file stored prediction results.
metrics (str | list[str]): Metric to be performed.
Options: 'MRRPE', 'MPJPE', 'Handedness_acc'.
Returns:
list: Evaluation results for evaluation metric.
"""
info_str = []
with open(res_file, 'r') as fin:
preds = json.load(fin)
assert len(preds) == len(self.db)
gts_rel_root = []
preds_rel_root = []
rel_root_masks = []
gts_joint_coord_cam = []
preds_joint_coord_cam = []
single_masks = []
interacting_masks = []
all_masks = []
gts_hand_type = []
preds_hand_type = []
hand_type_masks = []
for pred, item in zip(preds, self.db):
# mrrpe
if 'MRRPE' in metrics:
if item['hand_type'].all() and item['joints_3d_visible'][
20, 0] and item['joints_3d_visible'][41, 0]:
rel_root_masks.append(True)
pred_left_root_img = np.array(
pred['keypoints'][41], dtype=np.float32)[None, :]
pred_left_root_img[:, 2] += item['abs_depth'][0] + pred[
'rel_root_depth']
pred_left_root_cam = self._pixel2cam(
pred_left_root_img, item['focal'], item['princpt'])
pred_right_root_img = np.array(
pred['keypoints'][20], dtype=np.float32)[None, :]
pred_right_root_img[:, 2] += item['abs_depth'][0]
pred_right_root_cam = self._pixel2cam(
pred_right_root_img, item['focal'], item['princpt'])
preds_rel_root.append(pred_left_root_cam -
pred_right_root_cam)
gts_rel_root.append(
[item['joints_cam'][41] - item['joints_cam'][20]])
else:
rel_root_masks.append(False)
preds_rel_root.append([[0., 0., 0.]])
gts_rel_root.append([[0., 0., 0.]])
if 'MPJPE' in metrics:
pred_joint_coord_img = np.array(
pred['keypoints'], dtype=np.float32)
gt_joint_coord_cam = item['joints_cam'].copy()
pred_joint_coord_img[:21, 2] += item['abs_depth'][0]
pred_joint_coord_img[21:, 2] += item['abs_depth'][1]
pred_joint_coord_cam = self._pixel2cam(pred_joint_coord_img,
item['focal'],
item['princpt'])
pred_joint_coord_cam[:21] -= pred_joint_coord_cam[20]
pred_joint_coord_cam[21:] -= pred_joint_coord_cam[41]
gt_joint_coord_cam[:21] -= gt_joint_coord_cam[20]
gt_joint_coord_cam[21:] -= gt_joint_coord_cam[41]
preds_joint_coord_cam.append(pred_joint_coord_cam)
gts_joint_coord_cam.append(gt_joint_coord_cam)
mask = (np.array(item['joints_3d_visible'])[:, 0]) > 0
if item['hand_type'].all():
single_masks.append(
np.zeros(self.ann_info['num_joints'], dtype=bool))
interacting_masks.append(mask)
all_masks.append(mask)
else:
single_masks.append(mask)
interacting_masks.append(
np.zeros(self.ann_info['num_joints'], dtype=bool))
all_masks.append(mask)
if 'Handedness_acc' in metrics:
pred_hand_type = np.array(pred['hand_type'], dtype=int)
preds_hand_type.append(pred_hand_type)
gts_hand_type.append(item['hand_type'])
hand_type_masks.append(item['hand_type_valid'] > 0)
gts_rel_root = np.array(gts_rel_root, dtype=np.float32)
preds_rel_root = np.array(preds_rel_root, dtype=np.float32)
rel_root_masks = np.array(rel_root_masks, dtype=bool)[:, None]
gts_joint_coord_cam = np.array(gts_joint_coord_cam, dtype=np.float32)
preds_joint_coord_cam = np.array(
preds_joint_coord_cam, dtype=np.float32)
single_masks = np.array(single_masks, dtype=bool)
interacting_masks = np.array(interacting_masks, dtype=bool)
all_masks = np.array(all_masks, dtype=bool)
gts_hand_type = np.array(gts_hand_type, dtype=int)
preds_hand_type = np.array(preds_hand_type, dtype=int)
hand_type_masks = np.array(hand_type_masks, dtype=bool)
if 'MRRPE' in metrics:
info_str.append(('MRRPE',
keypoint_epe(preds_rel_root, gts_rel_root,
rel_root_masks)))
if 'MPJPE' in metrics:
info_str.append(('MPJPE_all',
keypoint_epe(preds_joint_coord_cam,
gts_joint_coord_cam, all_masks)))
info_str.append(('MPJPE_single',
keypoint_epe(preds_joint_coord_cam,
gts_joint_coord_cam, single_masks)))
info_str.append(
('MPJPE_interacting',
keypoint_epe(preds_joint_coord_cam, gts_joint_coord_cam,
interacting_masks)))
if 'Handedness_acc' in metrics:
info_str.append(('Handedness_acc',
self._get_accuracy(preds_hand_type, gts_hand_type,
hand_type_masks)))
return info_str
@@ -0,0 +1,205 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.datasets.builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class OneHand10KDataset(Kpt2dSviewRgbImgTopDownDataset):
"""OneHand10K dataset for top-down hand pose estimation.
"Mask-pose Cascaded CNN for 2D Hand Pose Estimation from
Single Color Images", TCSVT'2019.
More details can be found in the `paper
<https://www.yangangwang.com/papers/WANG-MCC-2018-10.pdf>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
OneHand10K keypoint indexes::
0: 'wrist',
1: 'thumb1',
2: 'thumb2',
3: 'thumb3',
4: 'thumb4',
5: 'forefinger1',
6: 'forefinger2',
7: 'forefinger3',
8: 'forefinger4',
9: 'middle_finger1',
10: 'middle_finger2',
11: 'middle_finger3',
12: 'middle_finger4',
13: 'ring_finger1',
14: 'ring_finger2',
15: 'ring_finger3',
16: 'ring_finger4',
17: 'pinky_finger1',
18: 'pinky_finger2',
19: 'pinky_finger3',
20: 'pinky_finger4'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/onehand10k.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
# use 1.25 padded bbox as input
center, scale = self._xywh2cs(*obj['bbox'][:4], 1.25)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCK', **kwargs):
"""Evaluate onehand10k keypoint results. The pose prediction results
will be saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['Test/source/0.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'PCK', 'AUC', 'EPE'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCK', 'AUC', 'EPE']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,208 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.datasets.builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class PanopticDataset(Kpt2dSviewRgbImgTopDownDataset):
"""Panoptic dataset for top-down hand pose estimation.
"Hand Keypoint Detection in Single Images using Multiview
Bootstrapping", CVPR'2017.
More details can be found in the `paper
<https://arxiv.org/abs/1704.07809>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
Panoptic keypoint indexes::
0: 'wrist',
1: 'thumb1',
2: 'thumb2',
3: 'thumb3',
4: 'thumb4',
5: 'forefinger1',
6: 'forefinger2',
7: 'forefinger3',
8: 'forefinger4',
9: 'middle_finger1',
10: 'middle_finger2',
11: 'middle_finger3',
12: 'middle_finger4',
13: 'ring_finger1',
14: 'ring_finger2',
15: 'ring_finger3',
16: 'ring_finger4',
17: 'pinky_finger1',
18: 'pinky_finger2',
19: 'pinky_finger3',
20: 'pinky_finger4'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/panoptic_hand2d.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
# The bbox is the tightest bbox enclosing keypoints.
# The paper uses 2.2 bbox as the input, while
# we use 1.76 (2.2 * 0.8) bbox as the input.
center, scale = self._xywh2cs(*obj['bbox'][:4], 1.76)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'head_size': obj['head_size'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCKh', **kwargs):
"""Evaluate panoptic keypoint results. The pose prediction results will
be saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['hand_labels/\
manual_test/000648952_02_l.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'PCKh', 'AUC', 'EPE'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCKh', 'AUC', 'EPE']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,205 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.datasets.builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class Rhd2DDataset(Kpt2dSviewRgbImgTopDownDataset):
"""Rendered Handpose Dataset for top-down hand pose estimation.
"Learning to Estimate 3D Hand Pose from Single RGB Images",
ICCV'2017.
More details can be found in the `paper
<https://arxiv.org/pdf/1705.01389.pdf>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
Rhd keypoint indexes::
0: 'wrist',
1: 'thumb1',
2: 'thumb2',
3: 'thumb3',
4: 'thumb4',
5: 'forefinger1',
6: 'forefinger2',
7: 'forefinger3',
8: 'forefinger4',
9: 'middle_finger1',
10: 'middle_finger2',
11: 'middle_finger3',
12: 'middle_finger4',
13: 'ring_finger1',
14: 'ring_finger2',
15: 'ring_finger3',
16: 'ring_finger4',
17: 'pinky_finger1',
18: 'pinky_finger2',
19: 'pinky_finger3',
20: 'pinky_finger4'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/rhd2d.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
# the ori image is 224x224
center, scale = self._xywh2cs(*obj['bbox'][:4], padding=1.25)
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCK', **kwargs):
"""Evaluate rhd keypoint results. The pose prediction results will be
saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1], area, score]
- image_paths (list[str]): For example,
['training/rgb/00031426.jpg']
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'PCK', 'AUC', 'EPE'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCK', 'AUC', 'EPE']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@@ -0,0 +1,10 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .mesh_adv_dataset import MeshAdversarialDataset
from .mesh_h36m_dataset import MeshH36MDataset
from .mesh_mix_dataset import MeshMixDataset
from .mosh_dataset import MoshDataset
__all__ = [
'MeshH36MDataset', 'MoshDataset', 'MeshMixDataset',
'MeshAdversarialDataset'
]
@@ -0,0 +1,43 @@
# Copyright (c) OpenMMLab. All rights reserved.
import numpy as np
from torch.utils.data import Dataset
from mmpose.datasets.builder import DATASETS, build_dataset
@DATASETS.register_module()
class MeshAdversarialDataset(Dataset):
"""Mix Dataset for the adversarial training in 3D human mesh estimation
task.
The dataset combines data from two datasets and
return a dict containing data from two datasets.
Args:
train_dataset (Dataset): Dataset for 3D human mesh estimation.
adversarial_dataset (Dataset): Dataset for adversarial learning,
provides real SMPL parameters.
"""
def __init__(self, train_dataset, adversarial_dataset):
super().__init__()
self.train_dataset = build_dataset(train_dataset)
self.adversarial_dataset = build_dataset(adversarial_dataset)
self.length = len(self.train_dataset)
def __len__(self):
"""Get the size of the dataset."""
return self.length
def __getitem__(self, i):
"""Given index, get the data from train dataset and randomly sample an
item from adversarial dataset.
Return a dict containing data from train and adversarial dataset.
"""
data = self.train_dataset[i]
ind_adv = np.random.randint(
low=0, high=len(self.adversarial_dataset), dtype=int)
data.update(self.adversarial_dataset[ind_adv %
len(self.adversarial_dataset)])
return data
@@ -0,0 +1,155 @@
# Copyright (c) OpenMMLab. All rights reserved.
import copy as cp
import os
from abc import ABCMeta
import numpy as np
from torch.utils.data import Dataset
from mmpose.datasets.pipelines import Compose
class MeshBaseDataset(Dataset, metaclass=ABCMeta):
"""Base dataset for 3D human mesh estimation task. In 3D humamesh
estimation task, all datasets share this BaseDataset for training and have
their own evaluate function.
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
This dataset can only be used for training.
For evaluation, subclass should write an extra evaluate function.
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
test_mode=False):
self.image_info = {}
self.ann_info = {}
self.ann_file = ann_file
self.img_prefix = img_prefix
self.pipeline = pipeline
self.test_mode = test_mode
self.ann_info['image_size'] = np.array(data_cfg['image_size'])
self.ann_info['iuv_size'] = np.array(data_cfg['iuv_size'])
self.ann_info['num_joints'] = data_cfg['num_joints']
self.ann_info['flip_pairs'] = None
self.db = []
self.pipeline = Compose(self.pipeline)
# flip_pairs
# For all mesh dataset, we use 24 joints as CMR and SPIN.
self.ann_info['flip_pairs'] = [[0, 5], [1, 4], [2, 3], [6, 11],
[7, 10], [8, 9], [20, 21], [22, 23]]
self.ann_info['use_different_joint_weights'] = False
assert self.ann_info['num_joints'] == 24
self.ann_info['joint_weights'] = np.ones([24, 1], dtype=np.float32)
self.ann_info['uv_type'] = data_cfg['uv_type']
self.ann_info['use_IUV'] = data_cfg['use_IUV']
uv_type = self.ann_info['uv_type']
self.iuv_prefix = os.path.join(self.img_prefix, f'{uv_type}_IUV_gt')
self.db = self._get_db(ann_file)
def _get_db(self, ann_file):
"""Load dataset."""
data = np.load(ann_file)
tmpl = dict(
image_file=None,
center=None,
scale=None,
rotation=0,
joints_2d=None,
joints_2d_visible=None,
joints_3d=None,
joints_3d_visible=None,
gender=None,
pose=None,
beta=None,
has_smpl=0,
iuv_file=None,
has_iuv=0)
gt_db = []
_imgnames = data['imgname']
_scales = data['scale'].astype(np.float32)
_centers = data['center'].astype(np.float32)
dataset_len = len(_imgnames)
# Get 2D keypoints
if 'part' in data.keys():
_keypoints = data['part'].astype(np.float32)
else:
_keypoints = np.zeros((dataset_len, 24, 3), dtype=np.float32)
# Get gt 3D joints, if available
if 'S' in data.keys():
_joints_3d = data['S'].astype(np.float32)
else:
_joints_3d = np.zeros((dataset_len, 24, 4), dtype=np.float32)
# Get gt SMPL parameters, if available
if 'pose' in data.keys() and 'shape' in data.keys():
_poses = data['pose'].astype(np.float32)
_betas = data['shape'].astype(np.float32)
has_smpl = 1
else:
_poses = np.zeros((dataset_len, 72), dtype=np.float32)
_betas = np.zeros((dataset_len, 10), dtype=np.float32)
has_smpl = 0
# Get gender data, if available
if 'gender' in data.keys():
_genders = data['gender']
_genders = np.array([str(g) != 'm' for g in _genders]).astype(int)
else:
_genders = -1 * np.ones(dataset_len).astype(int)
# Get IUV image, if available
if 'iuv_names' in data.keys():
_iuv_names = data['iuv_names']
has_iuv = has_smpl
else:
_iuv_names = [''] * dataset_len
has_iuv = 0
for i in range(len(_imgnames)):
newitem = cp.deepcopy(tmpl)
newitem['image_file'] = os.path.join(self.img_prefix, _imgnames[i])
newitem['scale'] = np.array([_scales[i], _scales[i]])
newitem['center'] = _centers[i]
newitem['joints_2d'] = _keypoints[i, :, :2]
newitem['joints_2d_visible'] = _keypoints[i, :, -1][:, None]
newitem['joints_3d'] = _joints_3d[i, :, :3]
newitem['joints_3d_visible'] = _joints_3d[i, :, -1][:, None]
newitem['pose'] = _poses[i]
newitem['beta'] = _betas[i]
newitem['has_smpl'] = has_smpl
newitem['gender'] = _genders[i]
newitem['iuv_file'] = os.path.join(self.iuv_prefix, _iuv_names[i])
newitem['has_iuv'] = has_iuv
gt_db.append(newitem)
return gt_db
def __len__(self, ):
"""Get the size of the dataset."""
return len(self.db)
def __getitem__(self, idx):
"""Get the sample given index."""
results = cp.deepcopy(self.db[idx])
results['ann_info'] = self.ann_info
return self.pipeline(results)
@@ -0,0 +1,101 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os
from collections import OrderedDict
import json_tricks as json
import numpy as np
from mmpose.core.evaluation import keypoint_mpjpe
from mmpose.datasets.builder import DATASETS
from .mesh_base_dataset import MeshBaseDataset
@DATASETS.register_module()
class MeshH36MDataset(MeshBaseDataset):
"""Human3.6M Dataset for 3D human mesh estimation. It inherits all function
from MeshBaseDataset and has its own evaluate function.
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def evaluate(self, outputs, res_folder, metric='joint_error', logger=None):
"""Evaluate 3D keypoint results."""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['joint_error']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
res_file = os.path.join(res_folder, 'result_keypoints.json')
kpts = []
for out in outputs:
for (keypoints, image_path) in zip(out['keypoints_3d'],
out['image_path']):
kpts.append({
'keypoints': keypoints.tolist(),
'image': image_path,
})
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file)
name_value = OrderedDict(info_str)
return name_value
@staticmethod
def _write_keypoint_results(keypoints, res_file):
"""Write results into a json file."""
with open(res_file, 'w') as f:
json.dump(keypoints, f, sort_keys=True, indent=4)
def _report_metric(self, res_file):
"""Keypoint evaluation.
Report mean per joint position error (MPJPE) and mean per joint
position error after rigid alignment (MPJPE-PA)
"""
with open(res_file, 'r') as fin:
preds = json.load(fin)
assert len(preds) == len(self.db)
pred_joints_3d = [pred['keypoints'] for pred in preds]
gt_joints_3d = [item['joints_3d'] for item in self.db]
gt_joints_visible = [item['joints_3d_visible'] for item in self.db]
pred_joints_3d = np.array(pred_joints_3d)
gt_joints_3d = np.array(gt_joints_3d)
gt_joints_visible = np.array(gt_joints_visible)
# we only evaluate on 14 lsp joints
joint_mapper = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 18]
pred_joints_3d = pred_joints_3d[:, joint_mapper, :]
pred_pelvis = (pred_joints_3d[:, 2] + pred_joints_3d[:, 3]) / 2
pred_joints_3d = pred_joints_3d - pred_pelvis[:, None, :]
gt_joints_3d = gt_joints_3d[:, joint_mapper, :]
gt_pelvis = (gt_joints_3d[:, 2] + gt_joints_3d[:, 3]) / 2
gt_joints_3d = gt_joints_3d - gt_pelvis[:, None, :]
gt_joints_visible = gt_joints_visible[:, joint_mapper, 0] > 0
mpjpe = keypoint_mpjpe(pred_joints_3d, gt_joints_3d, gt_joints_visible)
mpjpe_pa = keypoint_mpjpe(
pred_joints_3d,
gt_joints_3d,
gt_joints_visible,
alignment='procrustes')
info_str = []
info_str.append(('MPJPE', mpjpe * 1000))
info_str.append(('MPJPE-PA', mpjpe_pa * 1000))
return info_str
@@ -0,0 +1,73 @@
# Copyright (c) OpenMMLab. All rights reserved.
from abc import ABCMeta
import numpy as np
from torch.utils.data import ConcatDataset, Dataset, WeightedRandomSampler
from mmpose.datasets.builder import DATASETS
from .mesh_base_dataset import MeshBaseDataset
@DATASETS.register_module()
class MeshMixDataset(Dataset, metaclass=ABCMeta):
"""Mix Dataset for 3D human mesh estimation.
The dataset combines data from multiple datasets (MeshBaseDataset) and
sample the data from different datasets with the provided proportions.
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
Args:
configs (list): List of configs for multiple datasets.
partition (list): Sample proportion of multiple datasets. The length
of partition should be same with that of configs. The elements
of it should be non-negative and is not necessary summing up to
one.
Example:
>>> from mmpose.datasets import MeshMixDataset
>>> data_cfg = dict(
>>> image_size=[256, 256],
>>> iuv_size=[64, 64],
>>> num_joints=24,
>>> use_IUV=True,
>>> uv_type='BF')
>>>
>>> mix_dataset = MeshMixDataset(
>>> configs=[
>>> dict(
>>> ann_file='tests/data/h36m/test_h36m.npz',
>>> img_prefix='tests/data/h36m',
>>> data_cfg=data_cfg,
>>> pipeline=[]),
>>> dict(
>>> ann_file='tests/data/h36m/test_h36m.npz',
>>> img_prefix='tests/data/h36m',
>>> data_cfg=data_cfg,
>>> pipeline=[]),
>>> ],
>>> partition=[0.6, 0.4])
"""
def __init__(self, configs, partition):
"""Load data from multiple datasets."""
assert min(partition) >= 0
datasets = [MeshBaseDataset(**cfg) for cfg in configs]
self.dataset = ConcatDataset(datasets)
self.length = max(len(ds) for ds in datasets)
weights = [
np.ones(len(ds)) * p / len(ds)
for (p, ds) in zip(partition, datasets)
]
weights = np.concatenate(weights, axis=0)
self.sampler = WeightedRandomSampler(weights, 1)
def __len__(self):
"""Get the size of the dataset."""
return self.length
def __getitem__(self, idx):
"""Given index, sample the data from multiple datasets with the given
proportion."""
idx_new = list(self.sampler)[0]
return self.dataset[idx_new]
@@ -0,0 +1,68 @@
# Copyright (c) OpenMMLab. All rights reserved.
import copy as cp
from abc import ABCMeta
import numpy as np
from torch.utils.data import Dataset
from mmpose.datasets.builder import DATASETS
from mmpose.datasets.pipelines import Compose
@DATASETS.register_module()
class MoshDataset(Dataset, metaclass=ABCMeta):
"""Mosh Dataset for the adversarial training in 3D human mesh estimation
task.
The dataset return a dict containing real-world SMPL parameters.
Args:
ann_file (str): Path to the annotation file.
pipeline (list[dict | callable]): A sequence of data transforms.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self, ann_file, pipeline, test_mode=False):
self.ann_file = ann_file
self.pipeline = pipeline
self.test_mode = test_mode
self.db = self._get_db(ann_file)
self.pipeline = Compose(self.pipeline)
@staticmethod
def _get_db(ann_file):
"""Load dataset."""
data = np.load(ann_file)
_betas = data['shape'].astype(np.float32)
_poses = data['pose'].astype(np.float32)
tmpl = dict(
pose=None,
beta=None,
)
gt_db = []
dataset_len = len(_betas)
for i in range(dataset_len):
newitem = cp.deepcopy(tmpl)
newitem['pose'] = _poses[i]
newitem['beta'] = _betas[i]
gt_db.append(newitem)
return gt_db
def __len__(self, ):
"""Get the size of the dataset."""
return len(self.db)
def __getitem__(self, idx):
"""Get the sample given index."""
item = cp.deepcopy(self.db[idx])
trivial, pose, beta = \
np.zeros(3, dtype=np.float32), item['pose'], item['beta']
results = {
'mosh_theta':
np.concatenate((trivial, pose, beta), axis=0).astype(np.float32)
}
return self.pipeline(results)
@@ -0,0 +1,30 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .topdown_aic_dataset import TopDownAicDataset
from .topdown_coco_dataset import TopDownCocoDataset
from .topdown_coco_wholebody_dataset import TopDownCocoWholeBodyDataset
from .topdown_crowdpose_dataset import TopDownCrowdPoseDataset
from .topdown_h36m_dataset import TopDownH36MDataset
from .topdown_halpe_dataset import TopDownHalpeDataset
from .topdown_jhmdb_dataset import TopDownJhmdbDataset
from .topdown_mhp_dataset import TopDownMhpDataset
from .topdown_mpii_dataset import TopDownMpiiDataset
from .topdown_mpii_trb_dataset import TopDownMpiiTrbDataset
from .topdown_ochuman_dataset import TopDownOCHumanDataset
from .topdown_posetrack18_dataset import TopDownPoseTrack18Dataset
from .topdown_posetrack18_video_dataset import TopDownPoseTrack18VideoDataset
__all__ = [
'TopDownAicDataset',
'TopDownCocoDataset',
'TopDownCocoWholeBodyDataset',
'TopDownCrowdPoseDataset',
'TopDownMpiiDataset',
'TopDownMpiiTrbDataset',
'TopDownOCHumanDataset',
'TopDownPoseTrack18Dataset',
'TopDownJhmdbDataset',
'TopDownMhpDataset',
'TopDownH36MDataset',
'TopDownHalpeDataset',
'TopDownPoseTrack18VideoDataset',
]
@@ -0,0 +1,112 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
from mmcv import Config
from xtcocotools.cocoeval import COCOeval
from ...builder import DATASETS
from .topdown_coco_dataset import TopDownCocoDataset
@DATASETS.register_module()
class TopDownAicDataset(TopDownCocoDataset):
"""AicDataset dataset for top-down pose estimation.
"AI Challenger : A Large-scale Dataset for Going Deeper
in Image Understanding", arXiv'2017.
More details can be found in the `paper
<https://arxiv.org/abs/1711.06475>`__
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
AIC keypoint indexes::
0: "right_shoulder",
1: "right_elbow",
2: "right_wrist",
3: "left_shoulder",
4: "left_elbow",
5: "left_wrist",
6: "right_hip",
7: "right_knee",
8: "right_ankle",
9: "left_hip",
10: "left_knee",
11: "left_ankle",
12: "head_top",
13: "neck"
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/aic.py')
dataset_info = cfg._cfg_dict['dataset_info']
super(TopDownCocoDataset, self).__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
self.use_nms = data_cfg.get('use_nms', True)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
assert self.use_gt_bbox
gt_db = self._load_coco_keypoint_annotations()
return gt_db
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
coco_det = self.coco.loadRes(res_file)
coco_eval = COCOeval(
self.coco, coco_det, 'keypoints', self.sigmas, use_area=False)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
stats_names = [
'AP', 'AP .5', 'AP .75', 'AP (M)', 'AP (L)', 'AR', 'AR .5',
'AR .75', 'AR (M)', 'AR (L)'
]
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
@@ -0,0 +1,16 @@
# Copyright (c) OpenMMLab. All rights reserved.
from abc import ABCMeta
from torch.utils.data import Dataset
class TopDownBaseDataset(Dataset, metaclass=ABCMeta):
"""This class has been deprecated and replaced by
Kpt2dSviewRgbImgTopDownDataset."""
def __init__(self, *args, **kwargs):
raise (ImportError(
'TopDownBaseDataset has been replaced by '
'Kpt2dSviewRgbImgTopDownDataset,'
'check https://github.com/open-mmlab/mmpose/pull/663 for details.')
)
@@ -0,0 +1,405 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict, defaultdict
import json_tricks as json
import numpy as np
from mmcv import Config, deprecated_api_warning
from xtcocotools.cocoeval import COCOeval
from ....core.post_processing import oks_nms, soft_oks_nms
from ...builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class TopDownCocoDataset(Kpt2dSviewRgbImgTopDownDataset):
"""CocoDataset dataset for top-down pose estimation.
"Microsoft COCO: Common Objects in Context", ECCV'2014.
More details can be found in the `paper
<https://arxiv.org/abs/1405.0312>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
COCO keypoint indexes::
0: 'nose',
1: 'left_eye',
2: 'right_eye',
3: 'left_ear',
4: 'right_ear',
5: 'left_shoulder',
6: 'right_shoulder',
7: 'left_elbow',
8: 'right_elbow',
9: 'left_wrist',
10: 'right_wrist',
11: 'left_hip',
12: 'right_hip',
13: 'left_knee',
14: 'right_knee',
15: 'left_ankle',
16: 'right_ankle'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/coco.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
self.use_nms = data_cfg.get('use_nms', True)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
if (not self.test_mode) or self.use_gt_bbox:
# use ground truth bbox
gt_db = self._load_coco_keypoint_annotations()
else:
# use bbox from detection
gt_db = self._load_coco_person_detection_results()
return gt_db
def _load_coco_keypoint_annotations(self):
"""Ground truth bbox and keypoints."""
gt_db = []
for img_id in self.img_ids:
gt_db.extend(self._load_coco_keypoint_annotation_kernel(img_id))
return gt_db
def _load_coco_keypoint_annotation_kernel(self, img_id):
"""load annotation from COCOAPI.
Note:
bbox:[x1, y1, w, h]
Args:
img_id: coco image id
Returns:
dict: db entry
"""
img_ann = self.coco.loadImgs(img_id)[0]
width = img_ann['width']
height = img_ann['height']
num_joints = self.ann_info['num_joints']
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
# sanitize bboxes
valid_objs = []
for obj in objs:
if 'bbox' not in obj:
continue
x, y, w, h = obj['bbox']
x1 = max(0, x)
y1 = max(0, y)
x2 = min(width - 1, x1 + max(0, w - 1))
y2 = min(height - 1, y1 + max(0, h - 1))
if ('area' not in obj or obj['area'] > 0) and x2 > x1 and y2 > y1:
obj['clean_bbox'] = [x1, y1, x2 - x1, y2 - y1]
valid_objs.append(obj)
objs = valid_objs
bbox_id = 0
rec = []
for obj in objs:
if 'keypoints' not in obj:
continue
if max(obj['keypoints']) == 0:
continue
if 'num_keypoints' in obj and obj['num_keypoints'] == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
center, scale = self._xywh2cs(*obj['clean_bbox'][:4])
image_file = osp.join(self.img_prefix, self.id2name[img_id])
rec.append({
'image_file': image_file,
'center': center,
'scale': scale,
'bbox': obj['clean_bbox'][:4],
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
return rec
def _load_coco_person_detection_results(self):
"""Load coco person detection results."""
num_joints = self.ann_info['num_joints']
all_boxes = None
with open(self.bbox_file, 'r') as f:
all_boxes = json.load(f)
if not all_boxes:
raise ValueError('=> Load %s fail!' % self.bbox_file)
print(f'=> Total boxes: {len(all_boxes)}')
kpt_db = []
bbox_id = 0
for det_res in all_boxes:
if det_res['category_id'] != 1:
continue
image_file = osp.join(self.img_prefix,
self.id2name[det_res['image_id']])
box = det_res['bbox']
score = det_res['score']
if score < self.det_bbox_thr:
continue
center, scale = self._xywh2cs(*box[:4])
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.ones((num_joints, 3), dtype=np.float32)
kpt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'bbox': box[:4],
'bbox_score': score,
'dataset': self.dataset_name,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
print(f'=> Total boxes after filter '
f'low score@{self.det_bbox_thr}: {bbox_id}')
return kpt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='mAP', **kwargs):
"""Evaluate coco keypoint results. The pose prediction results will be
saved in ``${res_folder}/result_keypoints.json``.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['data/coco/val2017\
/000000393226.jpg']
- heatmap (np.ndarray[N, K, H, W]): model output heatmap
- bbox_id (list(int)).
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed. Defaults: 'mAP'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['mAP']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = defaultdict(list)
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts[image_id].append({
'keypoints': preds[i],
'center': boxes[i][0:2],
'scale': boxes[i][2:4],
'area': boxes[i][4],
'score': boxes[i][5],
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
# rescoring and oks nms
num_joints = self.ann_info['num_joints']
vis_thr = self.vis_thr
oks_thr = self.oks_thr
valid_kpts = []
for image_id in kpts.keys():
img_kpts = kpts[image_id]
for n_p in img_kpts:
box_score = n_p['score']
kpt_score = 0
valid_num = 0
for n_jt in range(0, num_joints):
t_s = n_p['keypoints'][n_jt][2]
if t_s > vis_thr:
kpt_score = kpt_score + t_s
valid_num = valid_num + 1
if valid_num != 0:
kpt_score = kpt_score / valid_num
# rescoring
n_p['score'] = kpt_score * box_score
if self.use_nms:
nms = soft_oks_nms if self.soft_nms else oks_nms
keep = nms(img_kpts, oks_thr, sigmas=self.sigmas)
valid_kpts.append([img_kpts[_keep] for _keep in keep])
else:
valid_kpts.append(img_kpts)
self._write_coco_keypoint_results(valid_kpts, res_file)
info_str = self._do_python_keypoint_eval(res_file)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
def _write_coco_keypoint_results(self, keypoints, res_file):
"""Write results into a json file."""
data_pack = [{
'cat_id': self._class_to_coco_ind[cls],
'cls_ind': cls_ind,
'cls': cls,
'ann_type': 'keypoints',
'keypoints': keypoints
} for cls_ind, cls in enumerate(self.classes)
if not cls == '__background__']
results = self._coco_keypoint_results_one_category_kernel(data_pack[0])
with open(res_file, 'w') as f:
json.dump(results, f, sort_keys=True, indent=4)
def _coco_keypoint_results_one_category_kernel(self, data_pack):
"""Get coco keypoint results."""
cat_id = data_pack['cat_id']
keypoints = data_pack['keypoints']
cat_results = []
for img_kpts in keypoints:
if len(img_kpts) == 0:
continue
_key_points = np.array(
[img_kpt['keypoints'] for img_kpt in img_kpts])
key_points = _key_points.reshape(-1,
self.ann_info['num_joints'] * 3)
result = [{
'image_id': img_kpt['image_id'],
'category_id': cat_id,
'keypoints': key_point.tolist(),
'score': float(img_kpt['score']),
'center': img_kpt['center'].tolist(),
'scale': img_kpt['scale'].tolist()
} for img_kpt, key_point in zip(img_kpts, key_points)]
cat_results.extend(result)
return cat_results
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
coco_det = self.coco.loadRes(res_file)
coco_eval = COCOeval(self.coco, coco_det, 'keypoints', self.sigmas)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
stats_names = [
'AP', 'AP .5', 'AP .75', 'AP (M)', 'AP (L)', 'AR', 'AR .5',
'AR .75', 'AR (M)', 'AR (L)'
]
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
def _sort_and_unique_bboxes(self, kpts, key='bbox_id'):
"""sort kpts and remove the repeated ones."""
for img_id, persons in kpts.items():
num = len(persons)
kpts[img_id] = sorted(kpts[img_id], key=lambda x: x[key])
for i in range(num - 1, 0, -1):
if kpts[img_id][i][key] == kpts[img_id][i - 1][key]:
del kpts[img_id][i]
return kpts
@@ -0,0 +1,274 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os
import warnings
import numpy as np
from mmcv import Config
from xtcocotools.cocoeval import COCOeval
from ...builder import DATASETS
from .topdown_coco_dataset import TopDownCocoDataset
@DATASETS.register_module()
class TopDownCocoWholeBodyDataset(TopDownCocoDataset):
"""CocoWholeBodyDataset dataset for top-down pose estimation.
"Whole-Body Human Pose Estimation in the Wild", ECCV'2020.
More details can be found in the `paper
<https://arxiv.org/abs/2007.11858>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
COCO-WholeBody keypoint indexes::
0-16: 17 body keypoints,
17-22: 6 foot keypoints,
23-90: 68 face keypoints,
91-132: 42 hand keypoints
In total, we have 133 keypoints for wholebody pose estimation.
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/coco_wholebody.py')
dataset_info = cfg._cfg_dict['dataset_info']
super(TopDownCocoDataset, self).__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
self.use_nms = data_cfg.get('use_nms', True)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.body_num = 17
self.foot_num = 6
self.face_num = 68
self.left_hand_num = 21
self.right_hand_num = 21
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _load_coco_keypoint_annotation_kernel(self, img_id):
"""load annotation from COCOAPI.
Note:
bbox:[x1, y1, w, h]
Args:
img_id: coco image id
Returns:
dict: db entry
"""
img_ann = self.coco.loadImgs(img_id)[0]
width = img_ann['width']
height = img_ann['height']
num_joints = self.ann_info['num_joints']
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
# sanitize bboxes
valid_objs = []
for obj in objs:
if 'bbox' not in obj:
continue
x, y, w, h = obj['bbox']
x1 = max(0, x)
y1 = max(0, y)
x2 = min(width - 1, x1 + max(0, w - 1))
y2 = min(height - 1, y1 + max(0, h - 1))
if ('area' not in obj or obj['area'] > 0) and x2 > x1 and y2 > y1:
obj['clean_bbox'] = [x1, y1, x2 - x1, y2 - y1]
valid_objs.append(obj)
objs = valid_objs
rec = []
bbox_id = 0
for obj in objs:
if 'keypoints' not in obj:
continue
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints'] + obj['foot_kpts'] +
obj['face_kpts'] + obj['lefthand_kpts'] +
obj['righthand_kpts']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3] > 0)
center, scale = self._xywh2cs(*obj['clean_bbox'][:4])
image_file = os.path.join(self.img_prefix, self.id2name[img_id])
rec.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
return rec
def _coco_keypoint_results_one_category_kernel(self, data_pack):
"""Get coco keypoint results."""
cat_id = data_pack['cat_id']
keypoints = data_pack['keypoints']
cat_results = []
for img_kpts in keypoints:
if len(img_kpts) == 0:
continue
_key_points = np.array(
[img_kpt['keypoints'] for img_kpt in img_kpts])
key_points = _key_points.reshape(-1,
self.ann_info['num_joints'] * 3)
cuts = np.cumsum([
0, self.body_num, self.foot_num, self.face_num,
self.left_hand_num, self.right_hand_num
]) * 3
result = [{
'image_id': img_kpt['image_id'],
'category_id': cat_id,
'keypoints': key_point[cuts[0]:cuts[1]].tolist(),
'foot_kpts': key_point[cuts[1]:cuts[2]].tolist(),
'face_kpts': key_point[cuts[2]:cuts[3]].tolist(),
'lefthand_kpts': key_point[cuts[3]:cuts[4]].tolist(),
'righthand_kpts': key_point[cuts[4]:cuts[5]].tolist(),
'score': float(img_kpt['score']),
'center': img_kpt['center'].tolist(),
'scale': img_kpt['scale'].tolist()
} for img_kpt, key_point in zip(img_kpts, key_points)]
cat_results.extend(result)
return cat_results
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
coco_det = self.coco.loadRes(res_file)
cuts = np.cumsum([
0, self.body_num, self.foot_num, self.face_num, self.left_hand_num,
self.right_hand_num
])
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_body',
self.sigmas[cuts[0]:cuts[1]],
use_area=True)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_foot',
self.sigmas[cuts[1]:cuts[2]],
use_area=True)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_face',
self.sigmas[cuts[2]:cuts[3]],
use_area=True)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_lefthand',
self.sigmas[cuts[3]:cuts[4]],
use_area=True)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_righthand',
self.sigmas[cuts[4]:cuts[5]],
use_area=True)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_wholebody',
self.sigmas,
use_area=True)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
stats_names = [
'AP', 'AP .5', 'AP .75', 'AP (M)', 'AP (L)', 'AR', 'AR .5',
'AR .75', 'AR (M)', 'AR (L)'
]
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
@@ -0,0 +1,110 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
from mmcv import Config
from xtcocotools.cocoeval import COCOeval
from ...builder import DATASETS
from .topdown_coco_dataset import TopDownCocoDataset
@DATASETS.register_module()
class TopDownCrowdPoseDataset(TopDownCocoDataset):
"""CrowdPoseDataset dataset for top-down pose estimation.
"CrowdPose: Efficient Crowded Scenes Pose Estimation and
A New Benchmark", CVPR'2019.
More details can be found in the `paper
<https://arxiv.org/abs/1812.00324>`__.
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
CrowdPose keypoint indexes::
0: 'left_shoulder',
1: 'right_shoulder',
2: 'left_elbow',
3: 'right_elbow',
4: 'left_wrist',
5: 'right_wrist',
6: 'left_hip',
7: 'right_hip',
8: 'left_knee',
9: 'right_knee',
10: 'left_ankle',
11: 'right_ankle',
12: 'top_head',
13: 'neck'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/crowdpose.py')
dataset_info = cfg._cfg_dict['dataset_info']
super(TopDownCocoDataset, self).__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
self.use_nms = data_cfg.get('use_nms', True)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
coco_det = self.coco.loadRes(res_file)
coco_eval = COCOeval(
self.coco,
coco_det,
'keypoints_crowd',
self.sigmas,
use_area=False)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
stats_names = [
'AP', 'AP .5', 'AP .75', 'AR', 'AR .5', 'AR .75', 'AP(E)', 'AP(M)',
'AP(H)'
]
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
@@ -0,0 +1,206 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import json_tricks as json
import numpy as np
from mmcv import Config, deprecated_api_warning
from ...builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class TopDownH36MDataset(Kpt2dSviewRgbImgTopDownDataset):
"""Human3.6M dataset for top-down 2D pose estimation.
"Human3.6M: Large Scale Datasets and Predictive Methods for 3D Human
Sensing in Natural Environments", TPAMI`2014.
More details can be found in the `paper
<http://vision.imar.ro/human3.6m/pami-h36m.pdf>`__.
Human3.6M keypoint indexes::
0: 'root (pelvis)',
1: 'right_hip',
2: 'right_knee',
3: 'right_foot',
4: 'left_hip',
5: 'left_knee',
6: 'left_foot',
7: 'spine',
8: 'thorax',
9: 'neck_base',
10: 'head',
11: 'left_shoulder',
12: 'left_elbow',
13: 'left_wrist',
14: 'right_shoulder',
15: 'right_elbow',
16: 'right_wrist'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/h36m.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
gt_db = []
bbox_id = 0
num_joints = self.ann_info['num_joints']
for img_id in self.img_ids:
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
for obj in objs:
if max(obj['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
# use 1.25 padded bbox as input
center, scale = self._xywh2cs(*obj['bbox'][:4])
image_file = osp.join(self.img_prefix, self.id2name[img_id])
gt_db.append({
'image_file': image_file,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox': obj['bbox'],
'bbox_score': 1,
'bbox_id': bbox_id
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCK', **kwargs):
"""Evaluate human3.6m 2d keypoint results. The pose prediction results
will be saved in `${res_folder}/result_keypoints.json`.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0],
scale[1],area, score]
- image_paths (list[str]): For example, ['data/coco/val2017
/000000393226.jpg']
- heatmap (np.ndarray[N, K, H, W]): model output heatmap
- bbox_id (list(int)).
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed. Defaults: 'PCK'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCK', 'EPE']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@staticmethod
def _write_keypoint_results(keypoints, res_file):
"""Write results into a json file."""
with open(res_file, 'w') as f:
json.dump(keypoints, f, sort_keys=True, indent=4)
@@ -0,0 +1,77 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
from mmcv import Config
from ...builder import DATASETS
from .topdown_coco_dataset import TopDownCocoDataset
@DATASETS.register_module()
class TopDownHalpeDataset(TopDownCocoDataset):
"""HalpeDataset for top-down pose estimation.
'https://github.com/Fang-Haoshu/Halpe-FullBody'
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
Halpe keypoint indexes::
0-19: 20 body keypoints,
20-25: 6 foot keypoints,
26-93: 68 face keypoints,
94-135: 42 hand keypoints
In total, we have 136 keypoints for wholebody pose estimation.
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/halpe.py')
dataset_info = cfg._cfg_dict['dataset_info']
super(TopDownCocoDataset, self).__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
self.use_nms = data_cfg.get('use_nms', True)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.ann_info['use_different_joint_weights'] = False
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
@@ -0,0 +1,361 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import json_tricks as json
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.core.evaluation.top_down_eval import keypoint_pck_accuracy
from ...builder import DATASETS
from .topdown_coco_dataset import TopDownCocoDataset
@DATASETS.register_module()
class TopDownJhmdbDataset(TopDownCocoDataset):
"""JhmdbDataset dataset for top-down pose estimation.
"Towards understanding action recognition", ICCV'2013.
More details can be found in the `paper
<https://openaccess.thecvf.com/content_iccv_2013/papers/\
Jhuang_Towards_Understanding_Action_2013_ICCV_paper.pdf>`__
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
sub-JHMDB keypoint indexes::
0: "neck",
1: "belly",
2: "head",
3: "right_shoulder",
4: "left_shoulder",
5: "right_hip",
6: "left_hip",
7: "right_elbow",
8: "left_elbow",
9: "right_knee",
10: "left_knee",
11: "right_wrist",
12: "left_wrist",
13: "right_ankle",
14: "left_ankle"
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/jhmdb.py')
dataset_info = cfg._cfg_dict['dataset_info']
super(TopDownCocoDataset, self).__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
assert self.use_gt_bbox
gt_db = self._load_coco_keypoint_annotations()
return gt_db
def _load_coco_keypoint_annotation_kernel(self, img_id):
"""load annotation from COCOAPI.
Note:
bbox:[x1, y1, w, h]
Args:
img_id: coco image id
Returns:
dict: db entry
"""
img_ann = self.coco.loadImgs(img_id)[0]
width = img_ann['width']
height = img_ann['height']
num_joints = self.ann_info['num_joints']
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
# sanitize bboxes
valid_objs = []
for obj in objs:
if 'bbox' not in obj:
continue
x, y, w, h = obj['bbox']
# JHMDB uses matlab format, index is 1-based,
# we should first convert to 0-based index
x -= 1
y -= 1
x1 = max(0, x)
y1 = max(0, y)
x2 = min(width - 1, x1 + max(0, w - 1))
y2 = min(height - 1, y1 + max(0, h - 1))
if ('area' not in obj or obj['area'] > 0) and x2 > x1 and y2 > y1:
obj['clean_bbox'] = [x1, y1, x2 - x1, y2 - y1]
valid_objs.append(obj)
objs = valid_objs
rec = []
bbox_id = 0
for obj in objs:
if 'keypoints' not in obj:
continue
if max(obj['keypoints']) == 0:
continue
if 'num_keypoints' in obj and obj['num_keypoints'] == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
# JHMDB uses matlab format, index is 1-based,
# we should first convert to 0-based index
joints_3d[:, :2] = keypoints[:, :2] - 1
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
center, scale = self._xywh2cs(*obj['clean_bbox'][:4])
image_file = osp.join(self.img_prefix, self.id2name[img_id])
rec.append({
'image_file': image_file,
'center': center,
'scale': scale,
'bbox': obj['clean_bbox'][:4],
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox_score': 1,
'bbox_id': f'{img_id}_{bbox_id:03}'
})
bbox_id = bbox_id + 1
return rec
def _write_keypoint_results(self, keypoints, res_file):
"""Write results into a json file."""
with open(res_file, 'w') as f:
json.dump(keypoints, f, sort_keys=True, indent=4)
def _report_metric(self, res_file, metrics, pck_thr=0.2):
"""Keypoint evaluation.
Args:
res_file (str): Json file stored prediction results.
metrics (str | list[str]): Metric to be performed.
Options: 'PCK', 'PCKh', 'AUC', 'EPE'.
pck_thr (float): PCK threshold, default as 0.2.
pckh_thr (float): PCKh threshold, default as 0.7.
auc_nor (float): AUC normalization factor, default as 30 pixel.
Returns:
List: Evaluation results for evaluation metric.
"""
info_str = []
with open(res_file, 'r') as fin:
preds = json.load(fin)
assert len(preds) == len(self.db)
outputs = []
gts = []
masks = []
threshold_bbox = []
threshold_torso = []
for pred, item in zip(preds, self.db):
outputs.append(np.array(pred['keypoints'])[:, :-1])
gts.append(np.array(item['joints_3d'])[:, :-1])
masks.append((np.array(item['joints_3d_visible'])[:, 0]) > 0)
if 'PCK' in metrics:
bbox = np.array(item['bbox'])
bbox_thr = np.max(bbox[2:])
threshold_bbox.append(np.array([bbox_thr, bbox_thr]))
if 'tPCK' in metrics:
torso_thr = np.linalg.norm(item['joints_3d'][4, :2] -
item['joints_3d'][5, :2])
if torso_thr < 1:
torso_thr = np.linalg.norm(
np.array(pred['keypoints'])[4, :2] -
np.array(pred['keypoints'])[5, :2])
warnings.warn('Torso Size < 1.')
threshold_torso.append(np.array([torso_thr, torso_thr]))
outputs = np.array(outputs)
gts = np.array(gts)
masks = np.array(masks)
threshold_bbox = np.array(threshold_bbox)
threshold_torso = np.array(threshold_torso)
if 'PCK' in metrics:
pck_p, pck, _ = keypoint_pck_accuracy(outputs, gts, masks, pck_thr,
threshold_bbox)
stats_names = [
'Head PCK', 'Sho PCK', 'Elb PCK', 'Wri PCK', 'Hip PCK',
'Knee PCK', 'Ank PCK', 'Mean PCK'
]
stats = [
pck_p[2], 0.5 * pck_p[3] + 0.5 * pck_p[4],
0.5 * pck_p[7] + 0.5 * pck_p[8],
0.5 * pck_p[11] + 0.5 * pck_p[12],
0.5 * pck_p[5] + 0.5 * pck_p[6],
0.5 * pck_p[9] + 0.5 * pck_p[10],
0.5 * pck_p[13] + 0.5 * pck_p[14], pck
]
info_str.extend(list(zip(stats_names, stats)))
if 'tPCK' in metrics:
pck_p, pck, _ = keypoint_pck_accuracy(outputs, gts, masks, pck_thr,
threshold_torso)
stats_names = [
'Head tPCK', 'Sho tPCK', 'Elb tPCK', 'Wri tPCK', 'Hip tPCK',
'Knee tPCK', 'Ank tPCK', 'Mean tPCK'
]
stats = [
pck_p[2], 0.5 * pck_p[3] + 0.5 * pck_p[4],
0.5 * pck_p[7] + 0.5 * pck_p[8],
0.5 * pck_p[11] + 0.5 * pck_p[12],
0.5 * pck_p[5] + 0.5 * pck_p[6],
0.5 * pck_p[9] + 0.5 * pck_p[10],
0.5 * pck_p[13] + 0.5 * pck_p[14], pck
]
info_str.extend(list(zip(stats_names, stats)))
return info_str
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCK', **kwargs):
"""Evaluate onehand10k keypoint results. The pose prediction results
will be saved in `${res_folder}/result_keypoints.json`.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_path (list[str])
- output_heatmap (np.ndarray[N, K, H, W]): model outputs.
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed.
Options: 'PCK', 'tPCK'.
PCK means normalized by the bounding boxes, while tPCK
means normalized by the torso size.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCK', 'tPCK']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
# convert 0-based index to 1-based index,
# and get the first two dimensions.
preds[..., :2] += 1.0
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts.append({
'keypoints': preds[i],
'center': boxes[i][0:2],
'scale': boxes[i][2:4],
'area': boxes[i][4],
'score': boxes[i][5],
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file, metrics)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
def _sort_and_unique_bboxes(self, kpts, key='bbox_id'):
"""sort kpts and remove the repeated ones."""
kpts = sorted(kpts, key=lambda x: x[key])
num = len(kpts)
for i in range(num - 1, 0, -1):
if kpts[i][key] == kpts[i - 1][key]:
del kpts[i]
return kpts
@@ -0,0 +1,125 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
from mmcv import Config
from xtcocotools.cocoeval import COCOeval
from ...builder import DATASETS
from .topdown_coco_dataset import TopDownCocoDataset
@DATASETS.register_module()
class TopDownMhpDataset(TopDownCocoDataset):
"""MHPv2.0 dataset for top-down pose estimation.
"Understanding Humans in Crowded Scenes: Deep Nested Adversarial
Learning and A New Benchmark for Multi-Human Parsing", ACM MM'2018.
More details can be found in the `paper
<https://arxiv.org/abs/1804.03287>`__
Note that, the evaluation metric used here is mAP (adapted from COCO),
which may be different from the official evaluation codes.
'https://github.com/ZhaoJ9014/Multi-Human-Parsing/tree/master/'
'Evaluation/Multi-Human-Pose'
Please be cautious if you use the results in papers.
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
MHP keypoint indexes::
0: "right ankle",
1: "right knee",
2: "right hip",
3: "left hip",
4: "left knee",
5: "left ankle",
6: "pelvis",
7: "thorax",
8: "upper neck",
9: "head top",
10: "right wrist",
11: "right elbow",
12: "right shoulder",
13: "left shoulder",
14: "left elbow",
15: "left wrist",
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/mhp.py')
dataset_info = cfg._cfg_dict['dataset_info']
super(TopDownCocoDataset, self).__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
if 'image_thr' in data_cfg:
warnings.warn(
'image_thr is deprecated, '
'please use det_bbox_thr instead', DeprecationWarning)
self.det_bbox_thr = data_cfg['image_thr']
self.use_nms = data_cfg.get('use_nms', True)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
assert self.use_gt_bbox
gt_db = self._load_coco_keypoint_annotations()
return gt_db
def _do_python_keypoint_eval(self, res_file):
"""Keypoint evaluation using COCOAPI."""
coco_det = self.coco.loadRes(res_file)
coco_eval = COCOeval(
self.coco, coco_det, 'keypoints', self.sigmas, use_area=False)
coco_eval.params.useSegm = None
coco_eval.evaluate()
coco_eval.accumulate()
coco_eval.summarize()
stats_names = [
'AP', 'AP .5', 'AP .75', 'AP (M)', 'AP (L)', 'AR', 'AR .5',
'AR .75', 'AR (M)', 'AR (L)'
]
info_str = list(zip(stats_names, coco_eval.stats))
return info_str
@@ -0,0 +1,275 @@
# Copyright (c) OpenMMLab. All rights reserved.
import json
import os.path as osp
import warnings
from collections import OrderedDict
import numpy as np
from mmcv import Config, deprecated_api_warning
from scipy.io import loadmat, savemat
from ...builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class TopDownMpiiDataset(Kpt2dSviewRgbImgTopDownDataset):
"""MPII Dataset for top-down pose estimation.
"2D Human Pose Estimation: New Benchmark and State of the Art Analysis"
,CVPR'2014. More details can be found in the `paper
<http://human-pose.mpi-inf.mpg.de/contents/andriluka14cvpr.pdf>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
MPII keypoint indexes::
0: 'right_ankle'
1: 'right_knee',
2: 'right_hip',
3: 'left_hip',
4: 'left_knee',
5: 'left_ankle',
6: 'pelvis',
7: 'thorax',
8: 'upper_neck',
9: 'head_top',
10: 'right_wrist',
11: 'right_elbow',
12: 'right_shoulder',
13: 'left_shoulder',
14: 'left_elbow',
15: 'left_wrist'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/mpii.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
coco_style=False,
test_mode=test_mode)
self.db = self._get_db()
self.image_set = set(x['image_file'] for x in self.db)
self.num_images = len(self.image_set)
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
# create train/val split
with open(self.ann_file) as anno_file:
anno = json.load(anno_file)
gt_db = []
bbox_id = 0
for a in anno:
image_name = a['image']
center = np.array(a['center'], dtype=np.float32)
scale = np.array([a['scale'], a['scale']], dtype=np.float32)
# Adjust center/scale slightly to avoid cropping limbs
if center[0] != -1:
center[1] = center[1] + 15 * scale[1]
# padding to include proper amount of context
scale = scale * 1.25
# MPII uses matlab format, index is 1-based,
# we should first convert to 0-based index
center = center - 1
joints_3d = np.zeros((self.ann_info['num_joints'], 3),
dtype=np.float32)
joints_3d_visible = np.zeros((self.ann_info['num_joints'], 3),
dtype=np.float32)
if not self.test_mode:
joints = np.array(a['joints'])
joints_vis = np.array(a['joints_vis'])
assert len(joints) == self.ann_info['num_joints'], \
f'joint num diff: {len(joints)}' + \
f' vs {self.ann_info["num_joints"]}'
joints_3d[:, 0:2] = joints[:, 0:2] - 1
joints_3d_visible[:, :2] = joints_vis[:, None]
image_file = osp.join(self.img_prefix, image_name)
gt_db.append({
'image_file': image_file,
'bbox_id': bbox_id,
'center': center,
'scale': scale,
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox_score': 1
})
bbox_id = bbox_id + 1
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCKh', **kwargs):
"""Evaluate PCKh for MPII dataset. Adapted from
https://github.com/leoxiaobin/deep-high-resolution-net.pytorch
Copyright (c) Microsoft, under the MIT License.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['/val2017/000000\
397133.jpg']
- heatmap (np.ndarray[N, K, H, W]): model output heatmap.
res_folder (str, optional): The folder to save the testing
results. Default: None.
metric (str | list[str]): Metrics to be performed.
Defaults: 'PCKh'.
Returns:
dict: PCKh for each joint
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCKh']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
kpts = []
for result in results:
preds = result['preds']
bbox_ids = result['bbox_ids']
batch_size = len(bbox_ids)
for i in range(batch_size):
kpts.append({'keypoints': preds[i], 'bbox_id': bbox_ids[i]})
kpts = self._sort_and_unique_bboxes(kpts)
preds = np.stack([kpt['keypoints'] for kpt in kpts])
# convert 0-based index to 1-based index,
# and get the first two dimensions.
preds = preds[..., :2] + 1.0
if res_folder:
pred_file = osp.join(res_folder, 'pred.mat')
savemat(pred_file, mdict={'preds': preds})
SC_BIAS = 0.6
threshold = 0.5
gt_file = osp.join(osp.dirname(self.ann_file), 'mpii_gt_val.mat')
gt_dict = loadmat(gt_file)
dataset_joints = gt_dict['dataset_joints']
jnt_missing = gt_dict['jnt_missing']
pos_gt_src = gt_dict['pos_gt_src']
headboxes_src = gt_dict['headboxes_src']
pos_pred_src = np.transpose(preds, [1, 2, 0])
head = np.where(dataset_joints == 'head')[1][0]
lsho = np.where(dataset_joints == 'lsho')[1][0]
lelb = np.where(dataset_joints == 'lelb')[1][0]
lwri = np.where(dataset_joints == 'lwri')[1][0]
lhip = np.where(dataset_joints == 'lhip')[1][0]
lkne = np.where(dataset_joints == 'lkne')[1][0]
lank = np.where(dataset_joints == 'lank')[1][0]
rsho = np.where(dataset_joints == 'rsho')[1][0]
relb = np.where(dataset_joints == 'relb')[1][0]
rwri = np.where(dataset_joints == 'rwri')[1][0]
rkne = np.where(dataset_joints == 'rkne')[1][0]
rank = np.where(dataset_joints == 'rank')[1][0]
rhip = np.where(dataset_joints == 'rhip')[1][0]
jnt_visible = 1 - jnt_missing
uv_error = pos_pred_src - pos_gt_src
uv_err = np.linalg.norm(uv_error, axis=1)
headsizes = headboxes_src[1, :, :] - headboxes_src[0, :, :]
headsizes = np.linalg.norm(headsizes, axis=0)
headsizes *= SC_BIAS
scale = headsizes * np.ones((len(uv_err), 1), dtype=np.float32)
scaled_uv_err = uv_err / scale
scaled_uv_err = scaled_uv_err * jnt_visible
jnt_count = np.sum(jnt_visible, axis=1)
less_than_threshold = (scaled_uv_err <= threshold) * jnt_visible
PCKh = 100. * np.sum(less_than_threshold, axis=1) / jnt_count
# save
rng = np.arange(0, 0.5 + 0.01, 0.01)
pckAll = np.zeros((len(rng), 16), dtype=np.float32)
for r, threshold in enumerate(rng):
less_than_threshold = (scaled_uv_err <= threshold) * jnt_visible
pckAll[r, :] = 100. * np.sum(
less_than_threshold, axis=1) / jnt_count
PCKh = np.ma.array(PCKh, mask=False)
PCKh.mask[6:8] = True
jnt_count = np.ma.array(jnt_count, mask=False)
jnt_count.mask[6:8] = True
jnt_ratio = jnt_count / np.sum(jnt_count).astype(np.float64)
name_value = [('Head', PCKh[head]),
('Shoulder', 0.5 * (PCKh[lsho] + PCKh[rsho])),
('Elbow', 0.5 * (PCKh[lelb] + PCKh[relb])),
('Wrist', 0.5 * (PCKh[lwri] + PCKh[rwri])),
('Hip', 0.5 * (PCKh[lhip] + PCKh[rhip])),
('Knee', 0.5 * (PCKh[lkne] + PCKh[rkne])),
('Ankle', 0.5 * (PCKh[lank] + PCKh[rank])),
('PCKh', np.sum(PCKh * jnt_ratio)),
('PCKh@0.1', np.sum(pckAll[10, :] * jnt_ratio))]
name_value = OrderedDict(name_value)
return name_value
def _sort_and_unique_bboxes(self, kpts, key='bbox_id'):
"""sort kpts and remove the repeated ones."""
kpts = sorted(kpts, key=lambda x: x[key])
num = len(kpts)
for i in range(num - 1, 0, -1):
if kpts[i][key] == kpts[i - 1][key]:
del kpts[i]
return kpts
@@ -0,0 +1,310 @@
# Copyright (c) OpenMMLab. All rights reserved.
import copy as cp
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict
import json_tricks as json
import numpy as np
from mmcv import Config, deprecated_api_warning
from mmpose.datasets.builder import DATASETS
from ..base import Kpt2dSviewRgbImgTopDownDataset
@DATASETS.register_module()
class TopDownMpiiTrbDataset(Kpt2dSviewRgbImgTopDownDataset):
"""MPII-TRB Dataset dataset for top-down pose estimation.
"TRB: A Novel Triplet Representation for Understanding 2D Human Body",
ICCV'2019. More details can be found in the `paper
<https://arxiv.org/abs/1910.11535>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
MPII-TRB keypoint indexes::
0: 'left_shoulder'
1: 'right_shoulder'
2: 'left_elbow'
3: 'right_elbow'
4: 'left_wrist'
5: 'right_wrist'
6: 'left_hip'
7: 'right_hip'
8: 'left_knee'
9: 'right_knee'
10: 'left_ankle'
11: 'right_ankle'
12: 'head'
13: 'neck'
14: 'right_neck'
15: 'left_neck'
16: 'medial_right_shoulder'
17: 'lateral_right_shoulder'
18: 'medial_right_bow'
19: 'lateral_right_bow'
20: 'medial_right_wrist'
21: 'lateral_right_wrist'
22: 'medial_left_shoulder'
23: 'lateral_left_shoulder'
24: 'medial_left_bow'
25: 'lateral_left_bow'
26: 'medial_left_wrist'
27: 'lateral_left_wrist'
28: 'medial_right_hip'
29: 'lateral_right_hip'
30: 'medial_right_knee'
31: 'lateral_right_knee'
32: 'medial_right_ankle'
33: 'lateral_right_ankle'
34: 'medial_left_hip'
35: 'lateral_left_hip'
36: 'medial_left_knee'
37: 'lateral_left_knee'
38: 'medial_left_ankle'
39: 'lateral_left_ankle'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/mpii_trb.py')
dataset_info = cfg._cfg_dict['dataset_info']
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.db = self._get_db(ann_file)
self.image_set = set(x['image_file'] for x in self.db)
self.num_images = len(self.image_set)
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self, ann_file):
"""Load dataset."""
with open(ann_file, 'r') as f:
data = json.load(f)
tmpl = dict(
image_file=None,
bbox_id=None,
center=None,
scale=None,
rotation=0,
joints_3d=None,
joints_3d_visible=None,
dataset=self.dataset_name)
imid2info = {
int(osp.splitext(x['file_name'])[0]): x
for x in data['images']
}
num_joints = self.ann_info['num_joints']
gt_db = []
for anno in data['annotations']:
newitem = cp.deepcopy(tmpl)
image_id = anno['image_id']
newitem['bbox_id'] = anno['id']
newitem['image_file'] = osp.join(self.img_prefix,
imid2info[image_id]['file_name'])
if max(anno['keypoints']) == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
for ipt in range(num_joints):
joints_3d[ipt, 0] = anno['keypoints'][ipt * 3 + 0]
joints_3d[ipt, 1] = anno['keypoints'][ipt * 3 + 1]
joints_3d[ipt, 2] = 0
t_vis = min(anno['keypoints'][ipt * 3 + 2], 1)
joints_3d_visible[ipt, :] = (t_vis, t_vis, 0)
center = np.array(anno['center'], dtype=np.float32)
scale = self.ann_info['image_size'] / anno['scale'] / 200.0
newitem['center'] = center
newitem['scale'] = scale
newitem['joints_3d'] = joints_3d
newitem['joints_3d_visible'] = joints_3d_visible
if 'headbox' in anno:
newitem['headbox'] = anno['headbox']
gt_db.append(newitem)
gt_db = sorted(gt_db, key=lambda x: x['bbox_id'])
return gt_db
def _evaluate_kernel(self, pred, joints_3d, joints_3d_visible, headbox):
"""Evaluate one example."""
num_joints = self.ann_info['num_joints']
headbox = np.array(headbox)
threshold = np.linalg.norm(headbox[:2] - headbox[2:]) * 0.3
hit = np.zeros(num_joints, dtype=np.float32)
exist = np.zeros(num_joints, dtype=np.float32)
for i in range(num_joints):
pred_pt = pred[i]
gt_pt = joints_3d[i]
vis = joints_3d_visible[i][0]
if vis:
exist[i] = 1
else:
continue
distance = np.linalg.norm(pred_pt[:2] - gt_pt[:2])
if distance < threshold:
hit[i] = 1
return hit, exist
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='PCKh', **kwargs):
"""Evaluate PCKh for MPII-TRB dataset.
Note:
- batch_size: N
- num_keypoints: K
- heatmap height: H
- heatmap width: W
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['/val2017/\
000000397133.jpg']
- heatmap (np.ndarray[N, K, H, W]): model output heatmap.
- bbox_ids (list[str]): For example, ['27407'].
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metrics to be performed.
Defaults: 'PCKh'.
Returns:
dict: PCKh for each joint
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['PCKh']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
res_file = osp.join(res_folder, 'result_keypoints.json')
else:
tmp_folder = tempfile.TemporaryDirectory()
res_file = osp.join(tmp_folder.name, 'result_keypoints.json')
kpts = []
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
str_image_path = image_paths[i]
image_id = int(osp.basename(osp.splitext(str_image_path)[0]))
kpts.append({
'keypoints': preds[i].tolist(),
'center': boxes[i][0:2].tolist(),
'scale': boxes[i][2:4].tolist(),
'area': float(boxes[i][4]),
'score': float(boxes[i][5]),
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
self._write_keypoint_results(kpts, res_file)
info_str = self._report_metric(res_file)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@staticmethod
def _write_keypoint_results(keypoints, res_file):
"""Write results into a json file."""
with open(res_file, 'w') as f:
json.dump(keypoints, f, sort_keys=True, indent=4)
def _report_metric(self, res_file):
"""Keypoint evaluation.
Report Mean Acc of skeleton, contour and all joints.
"""
num_joints = self.ann_info['num_joints']
hit = np.zeros(num_joints, dtype=np.float32)
exist = np.zeros(num_joints, dtype=np.float32)
with open(res_file, 'r') as fin:
preds = json.load(fin)
assert len(preds) == len(
self.db), f'len(preds)={len(preds)}, len(self.db)={len(self.db)}'
for pred, item in zip(preds, self.db):
h, e = self._evaluate_kernel(pred['keypoints'], item['joints_3d'],
item['joints_3d_visible'],
item['headbox'])
hit += h
exist += e
skeleton = np.sum(hit[:14]) / np.sum(exist[:14])
contour = np.sum(hit[14:]) / np.sum(exist[14:])
mean = np.sum(hit) / np.sum(exist)
info_str = []
info_str.append(('Skeleton_acc', skeleton.item()))
info_str.append(('Contour_acc', contour.item()))
info_str.append(('PCKh', mean.item()))
return info_str
def _sort_and_unique_bboxes(self, kpts, key='bbox_id'):
"""sort kpts and remove the repeated ones."""
kpts = sorted(kpts, key=lambda x: x[key])
num = len(kpts)
for i in range(num - 1, 0, -1):
if kpts[i][key] == kpts[i - 1][key]:
del kpts[i]
return kpts
@@ -0,0 +1,97 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
from mmcv import Config
from ...builder import DATASETS
from .topdown_coco_dataset import TopDownCocoDataset
@DATASETS.register_module()
class TopDownOCHumanDataset(TopDownCocoDataset):
"""OChuman dataset for top-down pose estimation.
"Pose2Seg: Detection Free Human Instance Segmentation", CVPR'2019.
More details can be found in the `paper
<https://arxiv.org/abs/1803.10683>`__ .
"Occluded Human (OCHuman)" dataset contains 8110 heavily occluded
human instances within 4731 images. OCHuman dataset is designed for
validation and testing. To evaluate on OCHuman, the model should be
trained on COCO training set, and then test the robustness of the
model to occlusion using OCHuman.
OCHuman keypoint indexes (same as COCO)::
0: 'nose',
1: 'left_eye',
2: 'right_eye',
3: 'left_ear',
4: 'right_ear',
5: 'left_shoulder',
6: 'right_shoulder',
7: 'left_elbow',
8: 'right_elbow',
9: 'left_wrist',
10: 'right_wrist',
11: 'left_hip',
12: 'right_hip',
13: 'left_knee',
14: 'right_knee',
15: 'left_ankle',
16: 'right_ankle'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/ochuman.py')
dataset_info = cfg._cfg_dict['dataset_info']
super(TopDownCocoDataset, self).__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
self.use_nms = data_cfg.get('use_nms', True)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
assert self.use_gt_bbox
gt_db = self._load_coco_keypoint_annotations()
return gt_db
@@ -0,0 +1,312 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict, defaultdict
import json_tricks as json
import numpy as np
from mmcv import Config, deprecated_api_warning
from ....core.post_processing import oks_nms, soft_oks_nms
from ...builder import DATASETS
from .topdown_coco_dataset import TopDownCocoDataset
try:
from poseval import eval_helpers
from poseval.evaluateAP import evaluateAP
has_poseval = True
except (ImportError, ModuleNotFoundError):
has_poseval = False
@DATASETS.register_module()
class TopDownPoseTrack18Dataset(TopDownCocoDataset):
"""PoseTrack18 dataset for top-down pose estimation.
"Posetrack: A benchmark for human pose estimation and tracking", CVPR'2018.
More details can be found in the `paper
<https://arxiv.org/abs/1710.10000>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
PoseTrack2018 keypoint indexes::
0: 'nose',
1: 'head_bottom',
2: 'head_top',
3: 'left_ear',
4: 'right_ear',
5: 'left_shoulder',
6: 'right_shoulder',
7: 'left_elbow',
8: 'right_elbow',
9: 'left_wrist',
10: 'right_wrist',
11: 'left_hip',
12: 'right_hip',
13: 'left_knee',
14: 'right_knee',
15: 'left_ankle',
16: 'right_ankle'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False):
if dataset_info is None:
warnings.warn(
'dataset_info is missing. '
'Check https://github.com/open-mmlab/mmpose/pull/663 '
'for details.', DeprecationWarning)
cfg = Config.fromfile('configs/_base_/datasets/posetrack18.py')
dataset_info = cfg._cfg_dict['dataset_info']
super(TopDownCocoDataset, self).__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
self.use_nms = data_cfg.get('use_nms', True)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='mAP', **kwargs):
"""Evaluate posetrack keypoint results. The pose prediction results
will be saved in ``${res_folder}/result_keypoints.json``.
Note:
- num_keypoints: K
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['val/010016_mpii_test\
/000024.jpg']
- heatmap (np.ndarray[N, K, H, W]): model output heatmap.
- bbox_id (list(int))
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed. Defaults: 'mAP'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['mAP']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
else:
tmp_folder = tempfile.TemporaryDirectory()
res_folder = tmp_folder.name
gt_folder = osp.join(
osp.dirname(self.ann_file),
osp.splitext(self.ann_file.split('_')[-1])[0])
kpts = defaultdict(list)
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
image_id = self.name2id[image_paths[i][len(self.img_prefix):]]
kpts[image_id].append({
'keypoints': preds[i],
'center': boxes[i][0:2],
'scale': boxes[i][2:4],
'area': boxes[i][4],
'score': boxes[i][5],
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
# rescoring and oks nms
num_joints = self.ann_info['num_joints']
vis_thr = self.vis_thr
oks_thr = self.oks_thr
valid_kpts = defaultdict(list)
for image_id in kpts.keys():
img_kpts = kpts[image_id]
for n_p in img_kpts:
box_score = n_p['score']
kpt_score = 0
valid_num = 0
for n_jt in range(0, num_joints):
t_s = n_p['keypoints'][n_jt][2]
if t_s > vis_thr:
kpt_score = kpt_score + t_s
valid_num = valid_num + 1
if valid_num != 0:
kpt_score = kpt_score / valid_num
# rescoring
n_p['score'] = kpt_score * box_score
if self.use_nms:
nms = soft_oks_nms if self.soft_nms else oks_nms
keep = nms(img_kpts, oks_thr, sigmas=self.sigmas)
valid_kpts[image_id].append(
[img_kpts[_keep] for _keep in keep])
else:
valid_kpts[image_id].append(img_kpts)
self._write_posetrack18_keypoint_results(valid_kpts, gt_folder,
res_folder)
info_str = self._do_python_keypoint_eval(gt_folder, res_folder)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@staticmethod
def _write_posetrack18_keypoint_results(keypoint_results, gt_folder,
pred_folder):
"""Write results into a json file.
Args:
keypoint_results (dict): keypoint results organized by image_id.
gt_folder (str): Path of directory for official gt files.
pred_folder (str): Path of directory to save the results.
"""
categories = []
cat = {}
cat['supercategory'] = 'person'
cat['id'] = 1
cat['name'] = 'person'
cat['keypoints'] = [
'nose', 'head_bottom', 'head_top', 'left_ear', 'right_ear',
'left_shoulder', 'right_shoulder', 'left_elbow', 'right_elbow',
'left_wrist', 'right_wrist', 'left_hip', 'right_hip', 'left_knee',
'right_knee', 'left_ankle', 'right_ankle'
]
cat['skeleton'] = [[16, 14], [14, 12], [17, 15], [15, 13], [12, 13],
[6, 12], [7, 13], [6, 7], [6, 8], [7, 9], [8, 10],
[9, 11], [2, 3], [1, 2], [1, 3], [2, 4], [3, 5],
[4, 6], [5, 7]]
categories.append(cat)
json_files = [
pos for pos in os.listdir(gt_folder) if pos.endswith('.json')
]
for json_file in json_files:
with open(osp.join(gt_folder, json_file), 'r') as f:
gt = json.load(f)
annotations = []
images = []
for image in gt['images']:
im = {}
im['id'] = image['id']
im['file_name'] = image['file_name']
images.append(im)
img_kpts = keypoint_results[im['id']]
if len(img_kpts) == 0:
continue
for track_id, img_kpt in enumerate(img_kpts[0]):
ann = {}
ann['image_id'] = img_kpt['image_id']
ann['keypoints'] = np.array(
img_kpt['keypoints']).reshape(-1).tolist()
ann['scores'] = np.array(ann['keypoints']).reshape(
[-1, 3])[:, 2].tolist()
ann['score'] = float(img_kpt['score'])
ann['track_id'] = track_id
annotations.append(ann)
info = {}
info['images'] = images
info['categories'] = categories
info['annotations'] = annotations
with open(osp.join(pred_folder, json_file), 'w') as f:
json.dump(info, f, sort_keys=True, indent=4)
def _do_python_keypoint_eval(self, gt_folder, pred_folder):
"""Keypoint evaluation using poseval."""
if not has_poseval:
raise ImportError('Please install poseval package for evaluation'
'on PoseTrack dataset '
'(see requirements/optional.txt)')
argv = ['', gt_folder + '/', pred_folder + '/']
print('Loading data')
gtFramesAll, prFramesAll = eval_helpers.load_data_dir(argv)
print('# gt frames :', len(gtFramesAll))
print('# pred frames:', len(prFramesAll))
# evaluate per-frame multi-person pose estimation (AP)
# compute AP
print('Evaluation of per-frame multi-person pose estimation')
apAll, _, _ = evaluateAP(gtFramesAll, prFramesAll, None, False, False)
# print AP
print('Average Precision (AP) metric:')
eval_helpers.printTable(apAll)
stats = eval_helpers.getCum(apAll)
stats_names = [
'Head AP', 'Shou AP', 'Elb AP', 'Wri AP', 'Hip AP', 'Knee AP',
'Ankl AP', 'Total AP'
]
info_str = list(zip(stats_names, stats))
return info_str
@@ -0,0 +1,549 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os
import os.path as osp
import tempfile
import warnings
from collections import OrderedDict, defaultdict
import json_tricks as json
import numpy as np
from mmcv import deprecated_api_warning
from ....core.post_processing import oks_nms, soft_oks_nms
from ...builder import DATASETS
from ..base import Kpt2dSviewRgbVidTopDownDataset
try:
from poseval import eval_helpers
from poseval.evaluateAP import evaluateAP
has_poseval = True
except (ImportError, ModuleNotFoundError):
has_poseval = False
@DATASETS.register_module()
class TopDownPoseTrack18VideoDataset(Kpt2dSviewRgbVidTopDownDataset):
"""PoseTrack18 dataset for top-down pose estimation.
"Posetrack: A benchmark for human pose estimation and tracking", CVPR'2018.
More details can be found in the `paper
<https://arxiv.org/abs/1710.10000>`__ .
The dataset loads raw features and apply specified transforms
to return a dict containing the image tensors and other information.
PoseTrack2018 keypoint indexes::
0: 'nose',
1: 'head_bottom',
2: 'head_top',
3: 'left_ear',
4: 'right_ear',
5: 'left_shoulder',
6: 'right_shoulder',
7: 'left_elbow',
8: 'right_elbow',
9: 'left_wrist',
10: 'right_wrist',
11: 'left_hip',
12: 'right_hip',
13: 'left_knee',
14: 'right_knee',
15: 'left_ankle',
16: 'right_ankle'
Args:
ann_file (str): Path to the annotation file.
img_prefix (str): Path to a directory where videos/images are held.
Default: None.
data_cfg (dict): config
pipeline (list[dict | callable]): A sequence of data transforms.
dataset_info (DatasetInfo): A class containing all dataset info.
test_mode (bool): Store True when building test or
validation dataset. Default: False.
ph_fill_len (int): The length of the placeholder to fill in the
image filenames, default: 6 in PoseTrack18.
"""
def __init__(self,
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=None,
test_mode=False,
ph_fill_len=6):
super().__init__(
ann_file,
img_prefix,
data_cfg,
pipeline,
dataset_info=dataset_info,
test_mode=test_mode)
self.use_gt_bbox = data_cfg['use_gt_bbox']
self.bbox_file = data_cfg['bbox_file']
self.det_bbox_thr = data_cfg.get('det_bbox_thr', 0.0)
self.use_nms = data_cfg.get('use_nms', True)
self.soft_nms = data_cfg['soft_nms']
self.nms_thr = data_cfg['nms_thr']
self.oks_thr = data_cfg['oks_thr']
self.vis_thr = data_cfg['vis_thr']
self.frame_weight_train = data_cfg['frame_weight_train']
self.frame_weight_test = data_cfg['frame_weight_test']
self.frame_weight = self.frame_weight_test \
if self.test_mode else self.frame_weight_train
self.ph_fill_len = ph_fill_len
# select the frame indices
self.frame_index_rand = data_cfg.get('frame_index_rand', True)
self.frame_index_range = data_cfg.get('frame_index_range', [-2, 2])
self.num_adj_frames = data_cfg.get('num_adj_frames', 1)
self.frame_indices_train = data_cfg.get('frame_indices_train', None)
self.frame_indices_test = data_cfg.get('frame_indices_test',
[-2, -1, 0, 1, 2])
if self.frame_indices_train is not None:
self.frame_indices_train.sort()
self.frame_indices_test.sort()
self.db = self._get_db()
print(f'=> num_images: {self.num_images}')
print(f'=> load {len(self.db)} samples')
def _get_db(self):
"""Load dataset."""
if (not self.test_mode) or self.use_gt_bbox:
# use ground truth bbox
gt_db = self._load_coco_keypoint_annotations()
else:
# use bbox from detection
gt_db = self._load_posetrack_person_detection_results()
return gt_db
def _load_coco_keypoint_annotations(self):
"""Ground truth bbox and keypoints."""
gt_db = []
for img_id in self.img_ids:
gt_db.extend(self._load_coco_keypoint_annotation_kernel(img_id))
return gt_db
def _load_coco_keypoint_annotation_kernel(self, img_id):
"""load annotation from COCOAPI.
Note:
bbox:[x1, y1, w, h]
Args:
img_id: coco image id
Returns:
dict: db entry
"""
img_ann = self.coco.loadImgs(img_id)[0]
width = img_ann['width']
height = img_ann['height']
num_joints = self.ann_info['num_joints']
file_name = img_ann['file_name']
nframes = int(img_ann['nframes'])
frame_id = int(img_ann['frame_id'])
ann_ids = self.coco.getAnnIds(imgIds=img_id, iscrowd=False)
objs = self.coco.loadAnns(ann_ids)
# sanitize bboxes
valid_objs = []
for obj in objs:
if 'bbox' not in obj:
continue
x, y, w, h = obj['bbox']
x1 = max(0, x)
y1 = max(0, y)
x2 = min(width - 1, x1 + max(0, w - 1))
y2 = min(height - 1, y1 + max(0, h - 1))
if ('area' not in obj or obj['area'] > 0) and x2 > x1 and y2 > y1:
obj['clean_bbox'] = [x1, y1, x2 - x1, y2 - y1]
valid_objs.append(obj)
objs = valid_objs
bbox_id = 0
rec = []
for obj in objs:
if 'keypoints' not in obj:
continue
if max(obj['keypoints']) == 0:
continue
if 'num_keypoints' in obj and obj['num_keypoints'] == 0:
continue
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.zeros((num_joints, 3), dtype=np.float32)
keypoints = np.array(obj['keypoints']).reshape(-1, 3)
joints_3d[:, :2] = keypoints[:, :2]
joints_3d_visible[:, :2] = np.minimum(1, keypoints[:, 2:3])
center, scale = self._xywh2cs(*obj['clean_bbox'][:4])
image_files = []
cur_image_file = osp.join(self.img_prefix, self.id2name[img_id])
image_files.append(cur_image_file)
# "images/val/012834_mpii_test/000000.jpg" -->> "000000.jpg"
cur_image_name = file_name.split('/')[-1]
ref_idx = int(cur_image_name.replace('.jpg', ''))
# select the frame indices
if not self.test_mode and self.frame_indices_train is not None:
indices = self.frame_indices_train
elif not self.test_mode and self.frame_index_rand:
low, high = self.frame_index_range
indices = np.random.randint(low, high + 1, self.num_adj_frames)
else:
indices = self.frame_indices_test
for index in indices:
if self.test_mode and index == 0:
continue
# the supporting frame index
support_idx = ref_idx + index
support_idx = np.clip(support_idx, 0, nframes - 1)
sup_image_file = cur_image_file.replace(
cur_image_name,
str(support_idx).zfill(self.ph_fill_len) + '.jpg')
if osp.exists(sup_image_file):
image_files.append(sup_image_file)
else:
warnings.warn(
f'{sup_image_file} does not exist, '
f'use {cur_image_file} instead.', UserWarning)
image_files.append(cur_image_file)
rec.append({
'image_file': image_files,
'center': center,
'scale': scale,
'bbox': obj['clean_bbox'][:4],
'rotation': 0,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'dataset': self.dataset_name,
'bbox_score': 1,
'bbox_id': bbox_id,
'nframes': nframes,
'frame_id': frame_id,
'frame_weight': self.frame_weight
})
bbox_id = bbox_id + 1
return rec
def _load_posetrack_person_detection_results(self):
"""Load Posetrack person detection results.
Only in test mode.
"""
num_joints = self.ann_info['num_joints']
all_boxes = None
with open(self.bbox_file, 'r') as f:
all_boxes = json.load(f)
if not all_boxes:
raise ValueError('=> Load %s fail!' % self.bbox_file)
print(f'=> Total boxes: {len(all_boxes)}')
kpt_db = []
bbox_id = 0
for det_res in all_boxes:
if det_res['category_id'] != 1:
continue
score = det_res['score']
if score < self.det_bbox_thr:
continue
box = det_res['bbox']
# deal with different bbox file formats
if 'nframes' in det_res and 'frame_id' in det_res:
nframes = int(det_res['nframes'])
frame_id = int(det_res['frame_id'])
elif 'image_name' in det_res:
img_id = self.name2id[det_res['image_name']]
img_ann = self.coco.loadImgs(img_id)[0]
nframes = int(img_ann['nframes'])
frame_id = int(img_ann['frame_id'])
else:
img_id = det_res['image_id']
img_ann = self.coco.loadImgs(img_id)[0]
nframes = int(img_ann['nframes'])
frame_id = int(img_ann['frame_id'])
image_files = []
if 'image_name' in det_res:
file_name = det_res['image_name']
else:
file_name = self.id2name[det_res['image_id']]
cur_image_file = osp.join(self.img_prefix, file_name)
image_files.append(cur_image_file)
# "images/val/012834_mpii_test/000000.jpg" -->> "000000.jpg"
cur_image_name = file_name.split('/')[-1]
ref_idx = int(cur_image_name.replace('.jpg', ''))
indices = self.frame_indices_test
for index in indices:
if self.test_mode and index == 0:
continue
# the supporting frame index
support_idx = ref_idx + index
support_idx = np.clip(support_idx, 0, nframes - 1)
sup_image_file = cur_image_file.replace(
cur_image_name,
str(support_idx).zfill(self.ph_fill_len) + '.jpg')
if osp.exists(sup_image_file):
image_files.append(sup_image_file)
else:
warnings.warn(f'{sup_image_file} does not exist, '
f'use {cur_image_file} instead.')
image_files.append(cur_image_file)
center, scale = self._xywh2cs(*box[:4])
joints_3d = np.zeros((num_joints, 3), dtype=np.float32)
joints_3d_visible = np.ones((num_joints, 3), dtype=np.float32)
kpt_db.append({
'image_file': image_files,
'center': center,
'scale': scale,
'rotation': 0,
'bbox': box[:4],
'bbox_score': score,
'dataset': self.dataset_name,
'joints_3d': joints_3d,
'joints_3d_visible': joints_3d_visible,
'bbox_id': bbox_id,
'nframes': nframes,
'frame_id': frame_id,
'frame_weight': self.frame_weight
})
bbox_id = bbox_id + 1
print(f'=> Total boxes after filter '
f'low score@{self.det_bbox_thr}: {bbox_id}')
return kpt_db
@deprecated_api_warning(name_dict=dict(outputs='results'))
def evaluate(self, results, res_folder=None, metric='mAP', **kwargs):
"""Evaluate posetrack keypoint results. The pose prediction results
will be saved in ``${res_folder}/result_keypoints.json``.
Note:
- num_keypoints: K
Args:
results (list[dict]): Testing results containing the following
items:
- preds (np.ndarray[N,K,3]): The first two dimensions are \
coordinates, score is the third dimension of the array.
- boxes (np.ndarray[N,6]): [center[0], center[1], scale[0], \
scale[1],area, score]
- image_paths (list[str]): For example, ['val/010016_mpii_test\
/000024.jpg']
- heatmap (np.ndarray[N, K, H, W]): model output heatmap.
- bbox_id (list(int))
res_folder (str, optional): The folder to save the testing
results. If not specified, a temp folder will be created.
Default: None.
metric (str | list[str]): Metric to be performed. Defaults: 'mAP'.
Returns:
dict: Evaluation results for evaluation metric.
"""
metrics = metric if isinstance(metric, list) else [metric]
allowed_metrics = ['mAP']
for metric in metrics:
if metric not in allowed_metrics:
raise KeyError(f'metric {metric} is not supported')
if res_folder is not None:
tmp_folder = None
else:
tmp_folder = tempfile.TemporaryDirectory()
res_folder = tmp_folder.name
gt_folder = osp.join(
osp.dirname(self.ann_file),
osp.splitext(self.ann_file.split('_')[-1])[0])
kpts = defaultdict(list)
for result in results:
preds = result['preds']
boxes = result['boxes']
image_paths = result['image_paths']
bbox_ids = result['bbox_ids']
batch_size = len(image_paths)
for i in range(batch_size):
if not isinstance(image_paths[i], list):
image_id = self.name2id[image_paths[i]
[len(self.img_prefix):]]
else:
image_id = self.name2id[image_paths[i][0]
[len(self.img_prefix):]]
kpts[image_id].append({
'keypoints': preds[i],
'center': boxes[i][0:2],
'scale': boxes[i][2:4],
'area': boxes[i][4],
'score': boxes[i][5],
'image_id': image_id,
'bbox_id': bbox_ids[i]
})
kpts = self._sort_and_unique_bboxes(kpts)
# rescoring and oks nms
num_joints = self.ann_info['num_joints']
vis_thr = self.vis_thr
oks_thr = self.oks_thr
valid_kpts = defaultdict(list)
for image_id in kpts.keys():
img_kpts = kpts[image_id]
for n_p in img_kpts:
box_score = n_p['score']
kpt_score = 0
valid_num = 0
for n_jt in range(0, num_joints):
t_s = n_p['keypoints'][n_jt][2]
if t_s > vis_thr:
kpt_score = kpt_score + t_s
valid_num = valid_num + 1
if valid_num != 0:
kpt_score = kpt_score / valid_num
# rescoring
n_p['score'] = kpt_score * box_score
if self.use_nms:
nms = soft_oks_nms if self.soft_nms else oks_nms
keep = nms(img_kpts, oks_thr, sigmas=self.sigmas)
valid_kpts[image_id].append(
[img_kpts[_keep] for _keep in keep])
else:
valid_kpts[image_id].append(img_kpts)
self._write_keypoint_results(valid_kpts, gt_folder, res_folder)
info_str = self._do_keypoint_eval(gt_folder, res_folder)
name_value = OrderedDict(info_str)
if tmp_folder is not None:
tmp_folder.cleanup()
return name_value
@staticmethod
def _write_keypoint_results(keypoint_results, gt_folder, pred_folder):
"""Write results into a json file.
Args:
keypoint_results (dict): keypoint results organized by image_id.
gt_folder (str): Path of directory for official gt files.
pred_folder (str): Path of directory to save the results.
"""
categories = []
cat = {}
cat['supercategory'] = 'person'
cat['id'] = 1
cat['name'] = 'person'
cat['keypoints'] = [
'nose', 'head_bottom', 'head_top', 'left_ear', 'right_ear',
'left_shoulder', 'right_shoulder', 'left_elbow', 'right_elbow',
'left_wrist', 'right_wrist', 'left_hip', 'right_hip', 'left_knee',
'right_knee', 'left_ankle', 'right_ankle'
]
cat['skeleton'] = [[16, 14], [14, 12], [17, 15], [15, 13], [12, 13],
[6, 12], [7, 13], [6, 7], [6, 8], [7, 9], [8, 10],
[9, 11], [2, 3], [1, 2], [1, 3], [2, 4], [3, 5],
[4, 6], [5, 7]]
categories.append(cat)
json_files = [
pos for pos in os.listdir(gt_folder) if pos.endswith('.json')
]
for json_file in json_files:
with open(osp.join(gt_folder, json_file), 'r') as f:
gt = json.load(f)
annotations = []
images = []
for image in gt['images']:
im = {}
im['id'] = image['id']
im['file_name'] = image['file_name']
images.append(im)
img_kpts = keypoint_results[im['id']]
if len(img_kpts) == 0:
continue
for track_id, img_kpt in enumerate(img_kpts[0]):
ann = {}
ann['image_id'] = img_kpt['image_id']
ann['keypoints'] = np.array(
img_kpt['keypoints']).reshape(-1).tolist()
ann['scores'] = np.array(ann['keypoints']).reshape(
[-1, 3])[:, 2].tolist()
ann['score'] = float(img_kpt['score'])
ann['track_id'] = track_id
annotations.append(ann)
info = {}
info['images'] = images
info['categories'] = categories
info['annotations'] = annotations
with open(osp.join(pred_folder, json_file), 'w') as f:
json.dump(info, f, sort_keys=True, indent=4)
def _do_keypoint_eval(self, gt_folder, pred_folder):
"""Keypoint evaluation using poseval."""
if not has_poseval:
raise ImportError('Please install poseval package for evaluation'
'on PoseTrack dataset '
'(see requirements/optional.txt)')
argv = ['', gt_folder + '/', pred_folder + '/']
print('Loading data')
gtFramesAll, prFramesAll = eval_helpers.load_data_dir(argv)
print('# gt frames :', len(gtFramesAll))
print('# pred frames:', len(prFramesAll))
# evaluate per-frame multi-person pose estimation (AP)
# compute AP
print('Evaluation of per-frame multi-person pose estimation')
apAll, _, _ = evaluateAP(gtFramesAll, prFramesAll, None, False, False)
# print AP
print('Average Precision (AP) metric:')
eval_helpers.printTable(apAll)
stats = eval_helpers.getCum(apAll)
stats_names = [
'Head AP', 'Shou AP', 'Elb AP', 'Wri AP', 'Hip AP', 'Knee AP',
'Ankl AP', 'Total AP'
]
info_str = list(zip(stats_names, stats))
return info_str
@@ -0,0 +1,8 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .bottom_up_transform import * # noqa
from .hand_transform import * # noqa
from .loading import LoadImageFromFile # noqa
from .mesh_transform import * # noqa
from .pose3d_transform import * # noqa
from .shared_transform import * # noqa
from .top_down_transform import * # noqa
@@ -0,0 +1,816 @@
# Copyright (c) OpenMMLab. All rights reserved.
import cv2
import numpy as np
from mmpose.core.post_processing import (get_affine_transform, get_warp_matrix,
warp_affine_joints)
from mmpose.datasets.builder import PIPELINES
from .shared_transform import Compose
def _ceil_to_multiples_of(x, base=64):
"""Transform x to the integral multiple of the base."""
return int(np.ceil(x / base)) * base
def _get_multi_scale_size(image,
input_size,
current_scale,
min_scale,
use_udp=False):
"""Get the size for multi-scale training.
Args:
image: Input image.
input_size (np.ndarray[2]): Size (w, h) of the image input.
current_scale (float): Scale factor.
min_scale (float): Minimal scale.
use_udp (bool): To use unbiased data processing.
Paper ref: Huang et al. The Devil is in the Details: Delving into
Unbiased Data Processing for Human Pose Estimation (CVPR 2020).
Returns:
tuple: A tuple containing multi-scale sizes.
- (w_resized, h_resized) (tuple(int)): resized width/height
- center (np.ndarray)image center
- scale (np.ndarray): scales wrt width/height
"""
assert len(input_size) == 2
h, w, _ = image.shape
# calculate the size for min_scale
min_input_w = _ceil_to_multiples_of(min_scale * input_size[0], 64)
min_input_h = _ceil_to_multiples_of(min_scale * input_size[1], 64)
if w < h:
w_resized = int(min_input_w * current_scale / min_scale)
h_resized = int(
_ceil_to_multiples_of(min_input_w / w * h, 64) * current_scale /
min_scale)
if use_udp:
scale_w = w - 1.0
scale_h = (h_resized - 1.0) / (w_resized - 1.0) * (w - 1.0)
else:
scale_w = w / 200.0
scale_h = h_resized / w_resized * w / 200.0
else:
h_resized = int(min_input_h * current_scale / min_scale)
w_resized = int(
_ceil_to_multiples_of(min_input_h / h * w, 64) * current_scale /
min_scale)
if use_udp:
scale_h = h - 1.0
scale_w = (w_resized - 1.0) / (h_resized - 1.0) * (h - 1.0)
else:
scale_h = h / 200.0
scale_w = w_resized / h_resized * h / 200.0
if use_udp:
center = (scale_w / 2.0, scale_h / 2.0)
else:
center = np.array([round(w / 2.0), round(h / 2.0)])
return (w_resized, h_resized), center, np.array([scale_w, scale_h])
def _resize_align_multi_scale(image, input_size, current_scale, min_scale):
"""Resize the images for multi-scale training.
Args:
image: Input image
input_size (np.ndarray[2]): Size (w, h) of the image input
current_scale (float): Current scale
min_scale (float): Minimal scale
Returns:
tuple: A tuple containing image info.
- image_resized (np.ndarray): resized image
- center (np.ndarray): center of image
- scale (np.ndarray): scale
"""
assert len(input_size) == 2
size_resized, center, scale = _get_multi_scale_size(
image, input_size, current_scale, min_scale)
trans = get_affine_transform(center, scale, 0, size_resized)
image_resized = cv2.warpAffine(image, trans, size_resized)
return image_resized, center, scale
def _resize_align_multi_scale_udp(image, input_size, current_scale, min_scale):
"""Resize the images for multi-scale training.
Args:
image: Input image
input_size (np.ndarray[2]): Size (w, h) of the image input
current_scale (float): Current scale
min_scale (float): Minimal scale
Returns:
tuple: A tuple containing image info.
- image_resized (np.ndarray): resized image
- center (np.ndarray): center of image
- scale (np.ndarray): scale
"""
assert len(input_size) == 2
size_resized, _, _ = _get_multi_scale_size(image, input_size,
current_scale, min_scale, True)
_, center, scale = _get_multi_scale_size(image, input_size, min_scale,
min_scale, True)
trans = get_warp_matrix(
theta=0,
size_input=np.array(scale, dtype=np.float32),
size_dst=np.array(size_resized, dtype=np.float32) - 1.0,
size_target=np.array(scale, dtype=np.float32))
image_resized = cv2.warpAffine(
image.copy(), trans, size_resized, flags=cv2.INTER_LINEAR)
return image_resized, center, scale
class HeatmapGenerator:
"""Generate heatmaps for bottom-up models.
Args:
num_joints (int): Number of keypoints
output_size (np.ndarray): Size (w, h) of feature map
sigma (int): Sigma of the heatmaps.
use_udp (bool): To use unbiased data processing.
Paper ref: Huang et al. The Devil is in the Details: Delving into
Unbiased Data Processing for Human Pose Estimation (CVPR 2020).
"""
def __init__(self, output_size, num_joints, sigma=-1, use_udp=False):
if not isinstance(output_size, np.ndarray):
output_size = np.array(output_size)
if output_size.size > 1:
assert len(output_size) == 2
self.output_size = output_size
else:
self.output_size = np.array([output_size, output_size],
dtype=np.int)
self.num_joints = num_joints
if sigma < 0:
sigma = self.output_size.prod()**0.5 / 64
self.sigma = sigma
size = 6 * sigma + 3
self.use_udp = use_udp
if use_udp:
self.x = np.arange(0, size, 1, np.float32)
self.y = self.x[:, None]
else:
x = np.arange(0, size, 1, np.float32)
y = x[:, None]
x0, y0 = 3 * sigma + 1, 3 * sigma + 1
self.g = np.exp(-((x - x0)**2 + (y - y0)**2) / (2 * sigma**2))
def __call__(self, joints):
"""Generate heatmaps."""
hms = np.zeros(
(self.num_joints, self.output_size[1], self.output_size[0]),
dtype=np.float32)
sigma = self.sigma
for p in joints:
for idx, pt in enumerate(p):
if pt[2] > 0:
x, y = int(pt[0]), int(pt[1])
if x < 0 or y < 0 or \
x >= self.output_size[0] or y >= self.output_size[1]:
continue
if self.use_udp:
x0 = 3 * sigma + 1 + pt[0] - x
y0 = 3 * sigma + 1 + pt[1] - y
g = np.exp(-((self.x - x0)**2 + (self.y - y0)**2) /
(2 * sigma**2))
else:
g = self.g
ul = int(np.round(x - 3 * sigma -
1)), int(np.round(y - 3 * sigma - 1))
br = int(np.round(x + 3 * sigma +
2)), int(np.round(y + 3 * sigma + 2))
c, d = max(0,
-ul[0]), min(br[0], self.output_size[0]) - ul[0]
a, b = max(0,
-ul[1]), min(br[1], self.output_size[1]) - ul[1]
cc, dd = max(0, ul[0]), min(br[0], self.output_size[0])
aa, bb = max(0, ul[1]), min(br[1], self.output_size[1])
hms[idx, aa:bb,
cc:dd] = np.maximum(hms[idx, aa:bb, cc:dd], g[a:b,
c:d])
return hms
class JointsEncoder:
"""Encodes the visible joints into (coordinates, score); The coordinate of
one joint and its score are of `int` type.
(idx * output_size**2 + y * output_size + x, 1) or (0, 0).
Args:
max_num_people(int): Max number of people in an image
num_joints(int): Number of keypoints
output_size(np.ndarray): Size (w, h) of feature map
tag_per_joint(bool): Option to use one tag map per joint.
"""
def __init__(self, max_num_people, num_joints, output_size, tag_per_joint):
self.max_num_people = max_num_people
self.num_joints = num_joints
if not isinstance(output_size, np.ndarray):
output_size = np.array(output_size)
if output_size.size > 1:
assert len(output_size) == 2
self.output_size = output_size
else:
self.output_size = np.array([output_size, output_size],
dtype=np.int)
self.tag_per_joint = tag_per_joint
def __call__(self, joints):
"""
Note:
- number of people in image: N
- number of keypoints: K
- max number of people in an image: M
Args:
joints (np.ndarray[N,K,3])
Returns:
visible_kpts (np.ndarray[M,K,2]).
"""
visible_kpts = np.zeros((self.max_num_people, self.num_joints, 2),
dtype=np.float32)
for i in range(len(joints)):
tot = 0
for idx, pt in enumerate(joints[i]):
x, y = int(pt[0]), int(pt[1])
if (pt[2] > 0 and 0 <= y < self.output_size[1]
and 0 <= x < self.output_size[0]):
if self.tag_per_joint:
visible_kpts[i][tot] = \
(idx * self.output_size.prod()
+ y * self.output_size[0] + x, 1)
else:
visible_kpts[i][tot] = (y * self.output_size[0] + x, 1)
tot += 1
return visible_kpts
class PAFGenerator:
"""Generate part affinity fields.
Args:
output_size (np.ndarray): Size (w, h) of feature map.
limb_width (int): Limb width of part affinity fields.
skeleton (list[list]): connections of joints.
"""
def __init__(self, output_size, limb_width, skeleton):
if not isinstance(output_size, np.ndarray):
output_size = np.array(output_size)
if output_size.size > 1:
assert len(output_size) == 2
self.output_size = output_size
else:
self.output_size = np.array([output_size, output_size],
dtype=np.int)
self.limb_width = limb_width
self.skeleton = skeleton
def _accumulate_paf_map_(self, pafs, src, dst, count):
"""Accumulate part affinity fields between two given joints.
Args:
pafs (np.ndarray[2,H,W]): paf maps (2 dimensions:x axis and
y axis) for a certain limb connection. This argument will
be modified inplace.
src (np.ndarray[2,]): coordinates of the source joint.
dst (np.ndarray[2,]): coordinates of the destination joint.
count (np.ndarray[H,W]): count map that preserves the number
of non-zero vectors at each point. This argument will be
modified inplace.
"""
limb_vec = dst - src
norm = np.linalg.norm(limb_vec)
if norm == 0:
unit_limb_vec = np.zeros(2)
else:
unit_limb_vec = limb_vec / norm
min_x = max(np.floor(min(src[0], dst[0]) - self.limb_width), 0)
max_x = min(
np.ceil(max(src[0], dst[0]) + self.limb_width),
self.output_size[0] - 1)
min_y = max(np.floor(min(src[1], dst[1]) - self.limb_width), 0)
max_y = min(
np.ceil(max(src[1], dst[1]) + self.limb_width),
self.output_size[1] - 1)
range_x = list(range(int(min_x), int(max_x + 1), 1))
range_y = list(range(int(min_y), int(max_y + 1), 1))
mask = np.zeros_like(count, dtype=bool)
if len(range_x) > 0 and len(range_y) > 0:
xx, yy = np.meshgrid(range_x, range_y)
delta_x = xx - src[0]
delta_y = yy - src[1]
dist = np.abs(delta_x * unit_limb_vec[1] -
delta_y * unit_limb_vec[0])
mask_local = (dist < self.limb_width)
mask[yy, xx] = mask_local
pafs[0, mask] += unit_limb_vec[0]
pafs[1, mask] += unit_limb_vec[1]
count += mask
return pafs, count
def __call__(self, joints):
"""Generate the target part affinity fields."""
pafs = np.zeros(
(len(self.skeleton) * 2, self.output_size[1], self.output_size[0]),
dtype=np.float32)
for idx, sk in enumerate(self.skeleton):
count = np.zeros((self.output_size[1], self.output_size[0]),
dtype=np.float32)
for p in joints:
src = p[sk[0]]
dst = p[sk[1]]
if src[2] > 0 and dst[2] > 0:
self._accumulate_paf_map_(pafs[2 * idx:2 * idx + 2],
src[:2], dst[:2], count)
pafs[2 * idx:2 * idx + 2] /= np.maximum(count, 1)
return pafs
@PIPELINES.register_module()
class BottomUpRandomFlip:
"""Data augmentation with random image flip for bottom-up.
Args:
flip_prob (float): Probability of flip.
"""
def __init__(self, flip_prob=0.5):
self.flip_prob = flip_prob
def __call__(self, results):
"""Perform data augmentation with random image flip."""
image, mask, joints = results['img'], results['mask'], results[
'joints']
self.flip_index = results['ann_info']['flip_index']
self.output_size = results['ann_info']['heatmap_size']
assert isinstance(mask, list)
assert isinstance(joints, list)
assert len(mask) == len(joints)
assert len(mask) == len(self.output_size)
if np.random.random() < self.flip_prob:
image = image[:, ::-1].copy() - np.zeros_like(image)
for i, _output_size in enumerate(self.output_size):
if not isinstance(_output_size, np.ndarray):
_output_size = np.array(_output_size)
if _output_size.size > 1:
assert len(_output_size) == 2
else:
_output_size = np.array([_output_size, _output_size],
dtype=np.int)
mask[i] = mask[i][:, ::-1].copy()
joints[i] = joints[i][:, self.flip_index]
joints[i][:, :, 0] = _output_size[0] - joints[i][:, :, 0] - 1
results['img'], results['mask'], results[
'joints'] = image, mask, joints
return results
@PIPELINES.register_module()
class BottomUpRandomAffine:
"""Data augmentation with random scaling & rotating.
Args:
rot_factor (int): Rotating to [-rotation_factor, rotation_factor]
scale_factor (float): Scaling to [1-scale_factor, 1+scale_factor]
scale_type: wrt ``long`` or ``short`` length of the image.
trans_factor: Translation factor.
use_udp (bool): To use unbiased data processing.
Paper ref: Huang et al. The Devil is in the Details: Delving into
Unbiased Data Processing for Human Pose Estimation (CVPR 2020).
"""
def __init__(self,
rot_factor,
scale_factor,
scale_type,
trans_factor,
use_udp=False):
self.max_rotation = rot_factor
self.min_scale = scale_factor[0]
self.max_scale = scale_factor[1]
self.scale_type = scale_type
self.trans_factor = trans_factor
self.use_udp = use_udp
def _get_scale(self, image_size, resized_size):
w, h = image_size
w_resized, h_resized = resized_size
if w / w_resized < h / h_resized:
if self.scale_type == 'long':
w_pad = h / h_resized * w_resized
h_pad = h
elif self.scale_type == 'short':
w_pad = w
h_pad = w / w_resized * h_resized
else:
raise ValueError(f'Unknown scale type: {self.scale_type}')
else:
if self.scale_type == 'long':
w_pad = w
h_pad = w / w_resized * h_resized
elif self.scale_type == 'short':
w_pad = h / h_resized * w_resized
h_pad = h
else:
raise ValueError(f'Unknown scale type: {self.scale_type}')
scale = np.array([w_pad, h_pad], dtype=np.float32)
return scale
def __call__(self, results):
"""Perform data augmentation with random scaling & rotating."""
image, mask, joints = results['img'], results['mask'], results[
'joints']
self.input_size = results['ann_info']['image_size']
if not isinstance(self.input_size, np.ndarray):
self.input_size = np.array(self.input_size)
if self.input_size.size > 1:
assert len(self.input_size) == 2
else:
self.input_size = [self.input_size, self.input_size]
self.output_size = results['ann_info']['heatmap_size']
assert isinstance(mask, list)
assert isinstance(joints, list)
assert len(mask) == len(joints)
assert len(mask) == len(self.output_size), (len(mask),
len(self.output_size),
self.output_size)
height, width = image.shape[:2]
if self.use_udp:
center = np.array(((width - 1.0) / 2, (height - 1.0) / 2))
else:
center = np.array((width / 2, height / 2))
img_scale = np.array([width, height], dtype=np.float32)
aug_scale = np.random.random() * (self.max_scale - self.min_scale) \
+ self.min_scale
img_scale *= aug_scale
aug_rot = (np.random.random() * 2 - 1) * self.max_rotation
if self.trans_factor > 0:
dx = np.random.randint(-self.trans_factor * img_scale[0] / 200.0,
self.trans_factor * img_scale[0] / 200.0)
dy = np.random.randint(-self.trans_factor * img_scale[1] / 200.0,
self.trans_factor * img_scale[1] / 200.0)
center[0] += dx
center[1] += dy
if self.use_udp:
for i, _output_size in enumerate(self.output_size):
if not isinstance(_output_size, np.ndarray):
_output_size = np.array(_output_size)
if _output_size.size > 1:
assert len(_output_size) == 2
else:
_output_size = [_output_size, _output_size]
scale = self._get_scale(img_scale, _output_size)
trans = get_warp_matrix(
theta=aug_rot,
size_input=center * 2.0,
size_dst=np.array(
(_output_size[0], _output_size[1]), dtype=np.float32) -
1.0,
size_target=scale)
mask[i] = cv2.warpAffine(
(mask[i] * 255).astype(np.uint8),
trans, (int(_output_size[0]), int(_output_size[1])),
flags=cv2.INTER_LINEAR) / 255
mask[i] = (mask[i] > 0.5).astype(np.float32)
joints[i][:, :, 0:2] = \
warp_affine_joints(joints[i][:, :, 0:2].copy(), trans)
if results['ann_info']['scale_aware_sigma']:
joints[i][:, :, 3] = joints[i][:, :, 3] / aug_scale
scale = self._get_scale(img_scale, self.input_size)
mat_input = get_warp_matrix(
theta=aug_rot,
size_input=center * 2.0,
size_dst=np.array((self.input_size[0], self.input_size[1]),
dtype=np.float32) - 1.0,
size_target=scale)
image = cv2.warpAffine(
image,
mat_input, (int(self.input_size[0]), int(self.input_size[1])),
flags=cv2.INTER_LINEAR)
else:
for i, _output_size in enumerate(self.output_size):
if not isinstance(_output_size, np.ndarray):
_output_size = np.array(_output_size)
if _output_size.size > 1:
assert len(_output_size) == 2
else:
_output_size = [_output_size, _output_size]
scale = self._get_scale(img_scale, _output_size)
mat_output = get_affine_transform(
center=center,
scale=scale / 200.0,
rot=aug_rot,
output_size=_output_size)
mask[i] = cv2.warpAffine(
(mask[i] * 255).astype(np.uint8), mat_output,
(int(_output_size[0]), int(_output_size[1]))) / 255
mask[i] = (mask[i] > 0.5).astype(np.float32)
joints[i][:, :, 0:2] = \
warp_affine_joints(joints[i][:, :, 0:2], mat_output)
if results['ann_info']['scale_aware_sigma']:
joints[i][:, :, 3] = joints[i][:, :, 3] / aug_scale
scale = self._get_scale(img_scale, self.input_size)
mat_input = get_affine_transform(
center=center,
scale=scale / 200.0,
rot=aug_rot,
output_size=self.input_size)
image = cv2.warpAffine(image, mat_input, (int(
self.input_size[0]), int(self.input_size[1])))
results['img'], results['mask'], results[
'joints'] = image, mask, joints
return results
@PIPELINES.register_module()
class BottomUpGenerateHeatmapTarget:
"""Generate multi-scale heatmap target for bottom-up.
Args:
sigma (int): Sigma of heatmap Gaussian
max_num_people (int): Maximum number of people in an image
use_udp (bool): To use unbiased data processing.
Paper ref: Huang et al. The Devil is in the Details: Delving into
Unbiased Data Processing for Human Pose Estimation (CVPR 2020).
"""
def __init__(self, sigma, use_udp=False):
self.sigma = sigma
self.use_udp = use_udp
def _generate(self, num_joints, heatmap_size):
"""Get heatmap generator."""
heatmap_generator = [
HeatmapGenerator(output_size, num_joints, self.sigma, self.use_udp)
for output_size in heatmap_size
]
return heatmap_generator
def __call__(self, results):
"""Generate multi-scale heatmap target for bottom-up."""
heatmap_generator = \
self._generate(results['ann_info']['num_joints'],
results['ann_info']['heatmap_size'])
target_list = list()
joints_list = results['joints']
for scale_id in range(results['ann_info']['num_scales']):
heatmaps = heatmap_generator[scale_id](joints_list[scale_id])
target_list.append(heatmaps.astype(np.float32))
results['target'] = target_list
return results
@PIPELINES.register_module()
class BottomUpGenerateTarget:
"""Generate multi-scale heatmap target for associate embedding.
Args:
sigma (int): Sigma of heatmap Gaussian
max_num_people (int): Maximum number of people in an image
use_udp (bool): To use unbiased data processing.
Paper ref: Huang et al. The Devil is in the Details: Delving into
Unbiased Data Processing for Human Pose Estimation (CVPR 2020).
"""
def __init__(self, sigma, max_num_people, use_udp=False):
self.sigma = sigma
self.max_num_people = max_num_people
self.use_udp = use_udp
def _generate(self, num_joints, heatmap_size):
"""Get heatmap generator and joint encoder."""
heatmap_generator = [
HeatmapGenerator(output_size, num_joints, self.sigma, self.use_udp)
for output_size in heatmap_size
]
joints_encoder = [
JointsEncoder(self.max_num_people, num_joints, output_size, True)
for output_size in heatmap_size
]
return heatmap_generator, joints_encoder
def __call__(self, results):
"""Generate multi-scale heatmap target for bottom-up."""
heatmap_generator, joints_encoder = \
self._generate(results['ann_info']['num_joints'],
results['ann_info']['heatmap_size'])
target_list = list()
mask_list, joints_list = results['mask'], results['joints']
for scale_id in range(results['ann_info']['num_scales']):
target_t = heatmap_generator[scale_id](joints_list[scale_id])
joints_t = joints_encoder[scale_id](joints_list[scale_id])
target_list.append(target_t.astype(np.float32))
mask_list[scale_id] = mask_list[scale_id].astype(np.float32)
joints_list[scale_id] = joints_t.astype(np.int32)
results['masks'], results['joints'] = mask_list, joints_list
results['targets'] = target_list
return results
@PIPELINES.register_module()
class BottomUpGeneratePAFTarget:
"""Generate multi-scale heatmaps and part affinity fields (PAF) target for
bottom-up. Paper ref: Cao et al. Realtime Multi-Person 2D Human Pose
Estimation using Part Affinity Fields (CVPR 2017).
Args:
limb_width (int): Limb width of part affinity fields
"""
def __init__(self, limb_width, skeleton=None):
self.limb_width = limb_width
self.skeleton = skeleton
def _generate(self, heatmap_size, skeleton):
"""Get PAF generator."""
paf_generator = [
PAFGenerator(output_size, self.limb_width, skeleton)
for output_size in heatmap_size
]
return paf_generator
def __call__(self, results):
"""Generate multi-scale part affinity fields for bottom-up."""
if self.skeleton is None:
assert results['ann_info']['skeleton'] is not None
self.skeleton = results['ann_info']['skeleton']
paf_generator = \
self._generate(results['ann_info']['heatmap_size'],
self.skeleton)
target_list = list()
joints_list = results['joints']
for scale_id in range(results['ann_info']['num_scales']):
pafs = paf_generator[scale_id](joints_list[scale_id])
target_list.append(pafs.astype(np.float32))
results['target'] = target_list
return results
@PIPELINES.register_module()
class BottomUpGetImgSize:
"""Get multi-scale image sizes for bottom-up, including base_size and
test_scale_factor. Keep the ratio and the image is resized to
`results['ann_info']['image_size']×current_scale`.
Args:
test_scale_factor (List[float]): Multi scale
current_scale (int): default 1
use_udp (bool): To use unbiased data processing.
Paper ref: Huang et al. The Devil is in the Details: Delving into
Unbiased Data Processing for Human Pose Estimation (CVPR 2020).
"""
def __init__(self, test_scale_factor, current_scale=1, use_udp=False):
self.test_scale_factor = test_scale_factor
self.min_scale = min(test_scale_factor)
self.current_scale = current_scale
self.use_udp = use_udp
def __call__(self, results):
"""Get multi-scale image sizes for bottom-up."""
input_size = results['ann_info']['image_size']
if not isinstance(input_size, np.ndarray):
input_size = np.array(input_size)
if input_size.size > 1:
assert len(input_size) == 2
else:
input_size = np.array([input_size, input_size], dtype=np.int)
img = results['img']
h, w, _ = img.shape
# calculate the size for min_scale
min_input_w = _ceil_to_multiples_of(self.min_scale * input_size[0], 64)
min_input_h = _ceil_to_multiples_of(self.min_scale * input_size[1], 64)
if w < h:
w_resized = int(min_input_w * self.current_scale / self.min_scale)
h_resized = int(
_ceil_to_multiples_of(min_input_w / w * h, 64) *
self.current_scale / self.min_scale)
if self.use_udp:
scale_w = w - 1.0
scale_h = (h_resized - 1.0) / (w_resized - 1.0) * (w - 1.0)
else:
scale_w = w / 200.0
scale_h = h_resized / w_resized * w / 200.0
else:
h_resized = int(min_input_h * self.current_scale / self.min_scale)
w_resized = int(
_ceil_to_multiples_of(min_input_h / h * w, 64) *
self.current_scale / self.min_scale)
if self.use_udp:
scale_h = h - 1.0
scale_w = (w_resized - 1.0) / (h_resized - 1.0) * (h - 1.0)
else:
scale_h = h / 200.0
scale_w = w_resized / h_resized * h / 200.0
if self.use_udp:
center = (scale_w / 2.0, scale_h / 2.0)
else:
center = np.array([round(w / 2.0), round(h / 2.0)])
results['ann_info']['test_scale_factor'] = self.test_scale_factor
results['ann_info']['base_size'] = (w_resized, h_resized)
results['ann_info']['center'] = center
results['ann_info']['scale'] = np.array([scale_w, scale_h])
return results
@PIPELINES.register_module()
class BottomUpResizeAlign:
"""Resize multi-scale size and align transform for bottom-up.
Args:
transforms (List): ToTensor & Normalize
use_udp (bool): To use unbiased data processing.
Paper ref: Huang et al. The Devil is in the Details: Delving into
Unbiased Data Processing for Human Pose Estimation (CVPR 2020).
"""
def __init__(self, transforms, use_udp=False):
self.transforms = Compose(transforms)
if use_udp:
self._resize_align_multi_scale = _resize_align_multi_scale_udp
else:
self._resize_align_multi_scale = _resize_align_multi_scale
def __call__(self, results):
"""Resize multi-scale size and align transform for bottom-up."""
input_size = results['ann_info']['image_size']
if not isinstance(input_size, np.ndarray):
input_size = np.array(input_size)
if input_size.size > 1:
assert len(input_size) == 2
else:
input_size = np.array([input_size, input_size], dtype=np.int)
test_scale_factor = results['ann_info']['test_scale_factor']
aug_data = []
for _, s in enumerate(sorted(test_scale_factor, reverse=True)):
_results = results.copy()
image_resized, _, _ = self._resize_align_multi_scale(
_results['img'], input_size, s, min(test_scale_factor))
_results['img'] = image_resized
_results = self.transforms(_results)
transformed_img = _results['img'].unsqueeze(0)
aug_data.append(transformed_img)
results['ann_info']['aug_data'] = aug_data
return results
@@ -0,0 +1,63 @@
# Copyright (c) OpenMMLab. All rights reserved.
import numpy as np
from mmpose.datasets.builder import PIPELINES
from .top_down_transform import TopDownRandomFlip
@PIPELINES.register_module()
class HandRandomFlip(TopDownRandomFlip):
"""Data augmentation with random image flip. A child class of
TopDownRandomFlip.
Required keys: 'img', 'joints_3d', 'joints_3d_visible', 'center',
'hand_type', 'rel_root_depth' and 'ann_info'.
Modifies key: 'img', 'joints_3d', 'joints_3d_visible', 'center',
'hand_type', 'rel_root_depth'.
Args:
flip_prob (float): Probability of flip.
"""
def __call__(self, results):
"""Perform data augmentation with random image flip."""
# base flip augmentation
super().__call__(results)
# flip hand type and root depth
hand_type = results['hand_type']
rel_root_depth = results['rel_root_depth']
flipped = results['flipped']
if flipped:
hand_type[0], hand_type[1] = hand_type[1], hand_type[0]
rel_root_depth = -rel_root_depth
results['hand_type'] = hand_type
results['rel_root_depth'] = rel_root_depth
return results
@PIPELINES.register_module()
class HandGenerateRelDepthTarget:
"""Generate the target relative root depth.
Required keys: 'rel_root_depth', 'rel_root_valid', 'ann_info'.
Modified keys: 'target', 'target_weight'.
"""
def __init__(self):
pass
def __call__(self, results):
"""Generate the target heatmap."""
rel_root_depth = results['rel_root_depth']
rel_root_valid = results['rel_root_valid']
cfg = results['ann_info']
D = cfg['heatmap_size_root']
root_depth_bound = cfg['root_depth_bound']
target = (rel_root_depth / root_depth_bound + 0.5) * D
target_weight = rel_root_valid * (target >= 0) * (target <= D)
results['target'] = target * np.ones(1, dtype=np.float32)
results['target_weight'] = target_weight * np.ones(1, dtype=np.float32)
return results
@@ -0,0 +1,91 @@
# Copyright (c) OpenMMLab. All rights reserved.
import mmcv
import numpy as np
from ..builder import PIPELINES
@PIPELINES.register_module()
class LoadImageFromFile:
"""Loading image(s) from file.
Required key: "image_file".
Added key: "img".
Args:
to_float32 (bool): Whether to convert the loaded image to a float32
numpy array. If set to False, the loaded image is an uint8 array.
Defaults to False.
color_type (str): Flags specifying the color type of a loaded image,
candidates are 'color', 'grayscale' and 'unchanged'.
channel_order (str): Order of channel, candidates are 'bgr' and 'rgb'.
file_client_args (dict): Arguments to instantiate a FileClient.
See :class:`mmcv.fileio.FileClient` for details.
Defaults to ``dict(backend='disk')``.
"""
def __init__(self,
to_float32=False,
color_type='color',
channel_order='rgb',
file_client_args=dict(backend='disk')):
self.to_float32 = to_float32
self.color_type = color_type
self.channel_order = channel_order
self.file_client_args = file_client_args.copy()
self.file_client = None
def _read_image(self, path):
img_bytes = self.file_client.get(path)
img = mmcv.imfrombytes(
img_bytes, flag=self.color_type, channel_order=self.channel_order)
if img is None:
raise ValueError(f'Fail to read {path}')
if self.to_float32:
img = img.astype(np.float32)
return img
def __call__(self, results):
"""Loading image(s) from file."""
if self.file_client is None:
self.file_client = mmcv.FileClient(**self.file_client_args)
image_file = results.get('image_file', None)
if isinstance(image_file, (list, tuple)):
# Load images from a list of paths
results['img'] = [self._read_image(path) for path in image_file]
elif image_file is not None:
# Load single image from path
results['img'] = self._read_image(image_file)
else:
if 'img' not in results:
# If `image_file`` is not in results, check the `img` exists
# and format the image. This for compatibility when the image
# is manually set outside the pipeline.
raise KeyError('Either `image_file` or `img` should exist in '
'results.')
assert isinstance(results['img'], np.ndarray)
if self.color_type == 'color' and self.channel_order == 'rgb':
# The original results['img'] is assumed to be image(s) in BGR
# order, so we convert the color according to the arguments.
if results['img'].ndim == 3:
results['img'] = mmcv.bgr2rgb(results['img'])
elif results['img'].ndim == 4:
results['img'] = np.concatenate(
[mmcv.bgr2rgb(img) for img in results['img']], axis=0)
else:
raise ValueError('results["img"] has invalid shape '
f'{results["img"].shape}')
results['image_file'] = None
return results
def __repr__(self):
repr_str = (f'{self.__class__.__name__}('
f'to_float32={self.to_float32}, '
f"color_type='{self.color_type}', "
f'file_client_args={self.file_client_args})')
return repr_str
@@ -0,0 +1,399 @@
# Copyright (c) OpenMMLab. All rights reserved.
import cv2
import mmcv
import numpy as np
import torch
from mmpose.core.post_processing import (affine_transform, fliplr_joints,
get_affine_transform)
from mmpose.datasets.builder import PIPELINES
def _flip_smpl_pose(pose):
"""Flip SMPL pose parameters horizontally.
Args:
pose (np.ndarray([72])): SMPL pose parameters
Returns:
pose_flipped
"""
flippedParts = [
0, 1, 2, 6, 7, 8, 3, 4, 5, 9, 10, 11, 15, 16, 17, 12, 13, 14, 18, 19,
20, 24, 25, 26, 21, 22, 23, 27, 28, 29, 33, 34, 35, 30, 31, 32, 36, 37,
38, 42, 43, 44, 39, 40, 41, 45, 46, 47, 51, 52, 53, 48, 49, 50, 57, 58,
59, 54, 55, 56, 63, 64, 65, 60, 61, 62, 69, 70, 71, 66, 67, 68
]
pose_flipped = pose[flippedParts]
# Negate the second and the third dimension of the axis-angle
pose_flipped[1::3] = -pose_flipped[1::3]
pose_flipped[2::3] = -pose_flipped[2::3]
return pose_flipped
def _flip_iuv(iuv, uv_type='BF'):
"""Flip IUV image horizontally.
Note:
IUV image height: H
IUV image width: W
Args:
iuv np.ndarray([H, W, 3]): IUV image
uv_type (str): The type of the UV map.
Candidate values:
'DP': The UV map used in DensePose project.
'SMPL': The default UV map of SMPL model.
'BF': The UV map used in DecoMR project.
Default: 'BF'
Returns:
iuv_flipped np.ndarray([H, W, 3]): Flipped IUV image
"""
assert uv_type in ['DP', 'SMPL', 'BF']
if uv_type == 'BF':
iuv_flipped = iuv[:, ::-1, :]
iuv_flipped[:, :, 1] = 255 - iuv_flipped[:, :, 1]
else:
# The flip of other UV map is complex, not finished yet.
raise NotImplementedError(
f'The flip of {uv_type} UV map is not implemented yet.')
return iuv_flipped
def _construct_rotation_matrix(rot, size=3):
"""Construct the in-plane rotation matrix.
Args:
rot (float): Rotation angle (degree).
size (int): The size of the rotation matrix.
Candidate Values: 2, 3. Defaults to 3.
Returns:
rot_mat (np.ndarray([size, size]): Rotation matrix.
"""
rot_mat = np.eye(size, dtype=np.float32)
if rot != 0:
rot_rad = np.deg2rad(rot)
sn, cs = np.sin(rot_rad), np.cos(rot_rad)
rot_mat[0, :2] = [cs, -sn]
rot_mat[1, :2] = [sn, cs]
return rot_mat
def _rotate_joints_3d(joints_3d, rot):
"""Rotate the 3D joints in the local coordinates.
Note:
Joints number: K
Args:
joints_3d (np.ndarray([K, 3])): Coordinates of keypoints.
rot (float): Rotation angle (degree).
Returns:
joints_3d_rotated
"""
# in-plane rotation
# 3D joints are rotated counterclockwise,
# so the rot angle is inversed.
rot_mat = _construct_rotation_matrix(-rot, 3)
joints_3d_rotated = np.einsum('ij,kj->ki', rot_mat, joints_3d)
joints_3d_rotated = joints_3d_rotated.astype('float32')
return joints_3d_rotated
def _rotate_smpl_pose(pose, rot):
"""Rotate SMPL pose parameters. SMPL (https://smpl.is.tue.mpg.de/) is a 3D
human model.
Args:
pose (np.ndarray([72])): SMPL pose parameters
rot (float): Rotation angle (degree).
Returns:
pose_rotated
"""
pose_rotated = pose.copy()
if rot != 0:
rot_mat = _construct_rotation_matrix(-rot)
orient = pose[:3]
# find the rotation of the body in camera frame
per_rdg, _ = cv2.Rodrigues(orient)
# apply the global rotation to the global orientation
res_rot, _ = cv2.Rodrigues(np.dot(rot_mat, per_rdg))
pose_rotated[:3] = (res_rot.T)[0]
return pose_rotated
def _flip_joints_3d(joints_3d, joints_3d_visible, flip_pairs):
"""Flip human joints in 3D space horizontally.
Note:
num_keypoints: K
Args:
joints_3d (np.ndarray([K, 3])): Coordinates of keypoints.
joints_3d_visible (np.ndarray([K, 1])): Visibility of keypoints.
flip_pairs (list[tuple()]): Pairs of keypoints which are mirrored
(for example, left ear -- right ear).
Returns:
joints_3d_flipped, joints_3d_visible_flipped
"""
assert len(joints_3d) == len(joints_3d_visible)
joints_3d_flipped = joints_3d.copy()
joints_3d_visible_flipped = joints_3d_visible.copy()
# Swap left-right parts
for left, right in flip_pairs:
joints_3d_flipped[left, :] = joints_3d[right, :]
joints_3d_flipped[right, :] = joints_3d[left, :]
joints_3d_visible_flipped[left, :] = joints_3d_visible[right, :]
joints_3d_visible_flipped[right, :] = joints_3d_visible[left, :]
# Flip horizontally
joints_3d_flipped[:, 0] = -joints_3d_flipped[:, 0]
joints_3d_flipped = joints_3d_flipped * joints_3d_visible_flipped
return joints_3d_flipped, joints_3d_visible_flipped
@PIPELINES.register_module()
class LoadIUVFromFile:
"""Loading IUV image from file."""
def __init__(self, to_float32=False):
self.to_float32 = to_float32
self.color_type = 'color'
# channel relations: iuv->bgr
self.channel_order = 'bgr'
def __call__(self, results):
"""Loading image from file."""
has_iuv = results['has_iuv']
use_iuv = results['ann_info']['use_IUV']
if has_iuv and use_iuv:
iuv_file = results['iuv_file']
iuv = mmcv.imread(iuv_file, self.color_type, self.channel_order)
if iuv is None:
raise ValueError(f'Fail to read {iuv_file}')
else:
has_iuv = 0
iuv = None
results['has_iuv'] = has_iuv
results['iuv'] = iuv
return results
@PIPELINES.register_module()
class IUVToTensor:
"""Transform IUV image to part index mask and uv coordinates image. The 3
channels of IUV image means: part index, u coordinates, v coordinates.
Required key: 'iuv', 'ann_info'.
Modifies key: 'part_index', 'uv_coordinates'.
Args:
results (dict): contain all information about training.
"""
def __call__(self, results):
iuv = results['iuv']
if iuv is None:
H, W = results['ann_info']['iuv_size']
part_index = torch.zeros([1, H, W], dtype=torch.long)
uv_coordinates = torch.zeros([2, H, W], dtype=torch.float32)
else:
part_index = torch.LongTensor(iuv[:, :, 0])[None, :, :]
uv_coordinates = torch.FloatTensor(iuv[:, :, 1:]) / 255
uv_coordinates = uv_coordinates.permute(2, 0, 1)
results['part_index'] = part_index
results['uv_coordinates'] = uv_coordinates
return results
@PIPELINES.register_module()
class MeshRandomChannelNoise:
"""Data augmentation with random channel noise.
Required keys: 'img'
Modifies key: 'img'
Args:
noise_factor (float): Multiply each channel with
a factor between``[1-scale_factor, 1+scale_factor]``
"""
def __init__(self, noise_factor=0.4):
self.noise_factor = noise_factor
def __call__(self, results):
"""Perform data augmentation with random channel noise."""
img = results['img']
# Each channel is multiplied with a number
# in the area [1-self.noise_factor, 1+self.noise_factor]
pn = np.random.uniform(1 - self.noise_factor, 1 + self.noise_factor,
(1, 3))
img = cv2.multiply(img, pn)
results['img'] = img
return results
@PIPELINES.register_module()
class MeshRandomFlip:
"""Data augmentation with random image flip.
Required keys: 'img', 'joints_2d','joints_2d_visible', 'joints_3d',
'joints_3d_visible', 'center', 'pose', 'iuv' and 'ann_info'.
Modifies key: 'img', 'joints_2d','joints_2d_visible', 'joints_3d',
'joints_3d_visible', 'center', 'pose', 'iuv'.
Args:
flip_prob (float): Probability of flip.
"""
def __init__(self, flip_prob=0.5):
self.flip_prob = flip_prob
def __call__(self, results):
"""Perform data augmentation with random image flip."""
if np.random.rand() > self.flip_prob:
return results
img = results['img']
joints_2d = results['joints_2d']
joints_2d_visible = results['joints_2d_visible']
joints_3d = results['joints_3d']
joints_3d_visible = results['joints_3d_visible']
pose = results['pose']
center = results['center']
img = img[:, ::-1, :]
pose = _flip_smpl_pose(pose)
joints_2d, joints_2d_visible = fliplr_joints(
joints_2d, joints_2d_visible, img.shape[1],
results['ann_info']['flip_pairs'])
joints_3d, joints_3d_visible = _flip_joints_3d(
joints_3d, joints_3d_visible, results['ann_info']['flip_pairs'])
center[0] = img.shape[1] - center[0] - 1
if 'iuv' in results.keys():
iuv = results['iuv']
if iuv is not None:
iuv = _flip_iuv(iuv, results['ann_info']['uv_type'])
results['iuv'] = iuv
results['img'] = img
results['joints_2d'] = joints_2d
results['joints_2d_visible'] = joints_2d_visible
results['joints_3d'] = joints_3d
results['joints_3d_visible'] = joints_3d_visible
results['pose'] = pose
results['center'] = center
return results
@PIPELINES.register_module()
class MeshGetRandomScaleRotation:
"""Data augmentation with random scaling & rotating.
Required key: 'scale'. Modifies key: 'scale' and 'rotation'.
Args:
rot_factor (int): Rotating to ``[-2*rot_factor, 2*rot_factor]``.
scale_factor (float): Scaling to ``[1-scale_factor, 1+scale_factor]``.
rot_prob (float): Probability of random rotation.
"""
def __init__(self, rot_factor=30, scale_factor=0.25, rot_prob=0.6):
self.rot_factor = rot_factor
self.scale_factor = scale_factor
self.rot_prob = rot_prob
def __call__(self, results):
"""Perform data augmentation with random scaling & rotating."""
s = results['scale']
sf = self.scale_factor
rf = self.rot_factor
s_factor = np.clip(np.random.randn() * sf + 1, 1 - sf, 1 + sf)
s = s * s_factor
r_factor = np.clip(np.random.randn() * rf, -rf * 2, rf * 2)
r = r_factor if np.random.rand() <= self.rot_prob else 0
results['scale'] = s
results['rotation'] = r
return results
@PIPELINES.register_module()
class MeshAffine:
"""Affine transform the image to get input image. Affine transform the 2D
keypoints, 3D kepoints and IUV image too.
Required keys: 'img', 'joints_2d','joints_2d_visible', 'joints_3d',
'joints_3d_visible', 'pose', 'iuv', 'ann_info','scale', 'rotation' and
'center'. Modifies key: 'img', 'joints_2d','joints_2d_visible',
'joints_3d', 'pose', 'iuv'.
"""
def __call__(self, results):
image_size = results['ann_info']['image_size']
img = results['img']
joints_2d = results['joints_2d']
joints_2d_visible = results['joints_2d_visible']
joints_3d = results['joints_3d']
pose = results['pose']
c = results['center']
s = results['scale']
r = results['rotation']
trans = get_affine_transform(c, s, r, image_size)
img = cv2.warpAffine(
img,
trans, (int(image_size[0]), int(image_size[1])),
flags=cv2.INTER_LINEAR)
for i in range(results['ann_info']['num_joints']):
if joints_2d_visible[i, 0] > 0.0:
joints_2d[i] = affine_transform(joints_2d[i], trans)
joints_3d = _rotate_joints_3d(joints_3d, r)
pose = _rotate_smpl_pose(pose, r)
results['img'] = img
results['joints_2d'] = joints_2d
results['joints_2d_visible'] = joints_2d_visible
results['joints_3d'] = joints_3d
results['pose'] = pose
if 'iuv' in results.keys():
iuv = results['iuv']
if iuv is not None:
iuv_size = results['ann_info']['iuv_size']
iuv = cv2.warpAffine(
iuv,
trans, (int(iuv_size[0]), int(iuv_size[1])),
flags=cv2.INTER_NEAREST)
results['iuv'] = iuv
return results
@@ -0,0 +1,643 @@
# Copyright (c) OpenMMLab. All rights reserved.
import copy
import mmcv
import numpy as np
import torch
from mmcv.utils import build_from_cfg
from mmpose.core.camera import CAMERAS
from mmpose.core.post_processing import fliplr_regression
from mmpose.datasets.builder import PIPELINES
@PIPELINES.register_module()
class GetRootCenteredPose:
"""Zero-center the pose around a given root joint. Optionally, the root
joint can be removed from the original pose and stored as a separate item.
Note that the root-centered joints may no longer align with some annotation
information (e.g. flip_pairs, num_joints, inference_channel, etc.) due to
the removal of the root joint.
Args:
item (str): The name of the pose to apply root-centering.
root_index (int): Root joint index in the pose.
visible_item (str): The name of the visibility item.
remove_root (bool): If true, remove the root joint from the pose
root_name (str): Optional. If not none, it will be used as the key to
store the root position separated from the original pose.
Required keys:
item
Modified keys:
item, visible_item, root_name
"""
def __init__(self,
item,
root_index,
visible_item=None,
remove_root=False,
root_name=None):
self.item = item
self.root_index = root_index
self.remove_root = remove_root
self.root_name = root_name
self.visible_item = visible_item
def __call__(self, results):
assert self.item in results
joints = results[self.item]
root_idx = self.root_index
assert joints.ndim >= 2 and joints.shape[-2] > root_idx,\
f'Got invalid joint shape {joints.shape}'
root = joints[..., root_idx:root_idx + 1, :]
joints = joints - root
results[self.item] = joints
if self.root_name is not None:
results[self.root_name] = root
if self.remove_root:
results[self.item] = np.delete(
results[self.item], root_idx, axis=-2)
if self.visible_item is not None:
assert self.visible_item in results
results[self.visible_item] = np.delete(
results[self.visible_item], root_idx, axis=-2)
# Add a flag to avoid latter transforms that rely on the root
# joint or the original joint index
results[f'{self.item}_root_removed'] = True
# Save the root index which is necessary to restore the global pose
if self.root_name is not None:
results[f'{self.root_name}_index'] = self.root_index
return results
@PIPELINES.register_module()
class NormalizeJointCoordinate:
"""Normalize the joint coordinate with given mean and std.
Args:
item (str): The name of the pose to normalize.
mean (array): Mean values of joint coordinates in shape [K, C].
std (array): Std values of joint coordinates in shape [K, C].
norm_param_file (str): Optionally load a dict containing `mean` and
`std` from a file using `mmcv.load`.
Required keys:
item
Modified keys:
item
"""
def __init__(self, item, mean=None, std=None, norm_param_file=None):
self.item = item
self.norm_param_file = norm_param_file
if norm_param_file is not None:
norm_param = mmcv.load(norm_param_file)
assert 'mean' in norm_param and 'std' in norm_param
mean = norm_param['mean']
std = norm_param['std']
else:
assert mean is not None
assert std is not None
self.mean = np.array(mean, dtype=np.float32)
self.std = np.array(std, dtype=np.float32)
def __call__(self, results):
assert self.item in results
results[self.item] = (results[self.item] - self.mean) / self.std
results[f'{self.item}_mean'] = self.mean.copy()
results[f'{self.item}_std'] = self.std.copy()
return results
@PIPELINES.register_module()
class ImageCoordinateNormalization:
"""Normalize the 2D joint coordinate with image width and height. Range [0,
w] is mapped to [-1, 1], while preserving the aspect ratio.
Args:
item (str|list[str]): The name of the pose to normalize.
norm_camera (bool): Whether to normalize camera intrinsics.
Default: False.
camera_param (dict|None): The camera parameter dict. See the camera
class definition for more details. If None is given, the camera
parameter will be obtained during processing of each data sample
with the key "camera_param".
Required keys:
item
Modified keys:
item (, camera_param)
"""
def __init__(self, item, norm_camera=False, camera_param=None):
self.item = item
if isinstance(self.item, str):
self.item = [self.item]
self.norm_camera = norm_camera
if camera_param is None:
self.static_camera = False
else:
self.static_camera = True
self.camera_param = camera_param
def __call__(self, results):
center = np.array(
[0.5 * results['image_width'], 0.5 * results['image_height']],
dtype=np.float32)
scale = np.array(0.5 * results['image_width'], dtype=np.float32)
for item in self.item:
results[item] = (results[item] - center) / scale
if self.norm_camera:
if self.static_camera:
camera_param = copy.deepcopy(self.camera_param)
else:
assert 'camera_param' in results, \
'Camera parameters are missing.'
camera_param = results['camera_param']
assert 'f' in camera_param and 'c' in camera_param
camera_param['f'] = camera_param['f'] / scale
camera_param['c'] = (camera_param['c'] - center[:, None]) / scale
if 'camera_param' not in results:
results['camera_param'] = dict()
results['camera_param'].update(camera_param)
return results
@PIPELINES.register_module()
class CollectCameraIntrinsics:
"""Store camera intrinsics in a 1-dim array, including f, c, k, p.
Args:
camera_param (dict|None): The camera parameter dict. See the camera
class definition for more details. If None is given, the camera
parameter will be obtained during processing of each data sample
with the key "camera_param".
need_distortion (bool): Whether need distortion parameters k and p.
Default: True.
Required keys:
camera_param (if camera parameters are not given in initialization)
Modified keys:
intrinsics
"""
def __init__(self, camera_param=None, need_distortion=True):
if camera_param is None:
self.static_camera = False
else:
self.static_camera = True
self.camera_param = camera_param
self.need_distortion = need_distortion
def __call__(self, results):
if self.static_camera:
camera_param = copy.deepcopy(self.camera_param)
else:
assert 'camera_param' in results, 'Camera parameters are missing.'
camera_param = results['camera_param']
assert 'f' in camera_param and 'c' in camera_param
intrinsics = np.concatenate(
[camera_param['f'].reshape(2), camera_param['c'].reshape(2)])
if self.need_distortion:
assert 'k' in camera_param and 'p' in camera_param
intrinsics = np.concatenate([
intrinsics, camera_param['k'].reshape(3),
camera_param['p'].reshape(2)
])
results['intrinsics'] = intrinsics
return results
@PIPELINES.register_module()
class CameraProjection:
"""Apply camera projection to joint coordinates.
Args:
item (str): The name of the pose to apply camera projection.
mode (str): The type of camera projection, supported options are
- world_to_camera
- world_to_pixel
- camera_to_world
- camera_to_pixel
output_name (str|None): The name of the projected pose. If None
(default) is given, the projected pose will be stored in place.
camera_type (str): The camera class name (should be registered in
CAMERA).
camera_param (dict|None): The camera parameter dict. See the camera
class definition for more details. If None is given, the camera
parameter will be obtained during processing of each data sample
with the key "camera_param".
Required keys:
- item
- camera_param (if camera parameters are not given in initialization)
Modified keys:
output_name
"""
def __init__(self,
item,
mode,
output_name=None,
camera_type='SimpleCamera',
camera_param=None):
self.item = item
self.mode = mode
self.output_name = output_name
self.camera_type = camera_type
allowed_mode = {
'world_to_camera',
'world_to_pixel',
'camera_to_world',
'camera_to_pixel',
}
if mode not in allowed_mode:
raise ValueError(
f'Got invalid mode: {mode}, allowed modes are {allowed_mode}')
if camera_param is None:
self.static_camera = False
else:
self.static_camera = True
self.camera = self._build_camera(camera_param)
def _build_camera(self, param):
cfgs = dict(type=self.camera_type, param=param)
return build_from_cfg(cfgs, CAMERAS)
def __call__(self, results):
assert self.item in results
joints = results[self.item]
if self.static_camera:
camera = self.camera
else:
assert 'camera_param' in results, 'Camera parameters are missing.'
camera = self._build_camera(results['camera_param'])
if self.mode == 'world_to_camera':
output = camera.world_to_camera(joints)
elif self.mode == 'world_to_pixel':
output = camera.world_to_pixel(joints)
elif self.mode == 'camera_to_world':
output = camera.camera_to_world(joints)
elif self.mode == 'camera_to_pixel':
output = camera.camera_to_pixel(joints)
else:
raise NotImplementedError
output_name = self.output_name
if output_name is None:
output_name = self.item
results[output_name] = output
return results
@PIPELINES.register_module()
class RelativeJointRandomFlip:
"""Data augmentation with random horizontal joint flip around a root joint.
Args:
item (str|list[str]): The name of the pose to flip.
flip_cfg (dict|list[dict]): Configurations of the fliplr_regression
function. It should contain the following arguments:
- ``center_mode``: The mode to set the center location on the \
x-axis to flip around.
- ``center_x`` or ``center_index``: Set the x-axis location or \
the root joint's index to define the flip center.
Please refer to the docstring of the fliplr_regression function for
more details.
visible_item (str|list[str]): The name of the visibility item which
will be flipped accordingly along with the pose.
flip_prob (float): Probability of flip.
flip_camera (bool): Whether to flip horizontal distortion coefficients.
camera_param (dict|None): The camera parameter dict. See the camera
class definition for more details. If None is given, the camera
parameter will be obtained during processing of each data sample
with the key "camera_param".
Required keys:
item
Modified keys:
item (, camera_param)
"""
def __init__(self,
item,
flip_cfg,
visible_item=None,
flip_prob=0.5,
flip_camera=False,
camera_param=None):
self.item = item
self.flip_cfg = flip_cfg
self.vis_item = visible_item
self.flip_prob = flip_prob
self.flip_camera = flip_camera
if camera_param is None:
self.static_camera = False
else:
self.static_camera = True
self.camera_param = camera_param
if isinstance(self.item, str):
self.item = [self.item]
if isinstance(self.flip_cfg, dict):
self.flip_cfg = [self.flip_cfg] * len(self.item)
assert len(self.item) == len(self.flip_cfg)
if isinstance(self.vis_item, str):
self.vis_item = [self.vis_item]
def __call__(self, results):
if results.get(f'{self.item}_root_removed', False):
raise RuntimeError('The transform RelativeJointRandomFlip should '
f'not be applied to {self.item} whose root '
'joint has been removed and joint indices have '
'been changed')
if np.random.rand() <= self.flip_prob:
flip_pairs = results['ann_info']['flip_pairs']
# flip joint coordinates
for i, item in enumerate(self.item):
assert item in results
joints = results[item]
joints_flipped = fliplr_regression(joints, flip_pairs,
**self.flip_cfg[i])
results[item] = joints_flipped
# flip joint visibility
for vis_item in self.vis_item:
assert vis_item in results
visible = results[vis_item]
visible_flipped = visible.copy()
for left, right in flip_pairs:
visible_flipped[..., left, :] = visible[..., right, :]
visible_flipped[..., right, :] = visible[..., left, :]
results[vis_item] = visible_flipped
# flip horizontal distortion coefficients
if self.flip_camera:
if self.static_camera:
camera_param = copy.deepcopy(self.camera_param)
else:
assert 'camera_param' in results, \
'Camera parameters are missing.'
camera_param = results['camera_param']
assert 'c' in camera_param
camera_param['c'][0] *= -1
if 'p' in camera_param:
camera_param['p'][0] *= -1
if 'camera_param' not in results:
results['camera_param'] = dict()
results['camera_param'].update(camera_param)
return results
@PIPELINES.register_module()
class PoseSequenceToTensor:
"""Convert pose sequence from numpy array to Tensor.
The original pose sequence should have a shape of [T,K,C] or [K,C], where
T is the sequence length, K and C are keypoint number and dimension. The
converted pose sequence will have a shape of [KxC, T].
Args:
item (str): The name of the pose sequence
Required keys:
item
Modified keys:
item
"""
def __init__(self, item):
self.item = item
def __call__(self, results):
assert self.item in results
seq = results[self.item]
assert isinstance(seq, np.ndarray)
assert seq.ndim in {2, 3}
if seq.ndim == 2:
seq = seq[None, ...]
T = seq.shape[0]
seq = seq.transpose(1, 2, 0).reshape(-1, T)
results[self.item] = torch.from_numpy(seq)
return results
@PIPELINES.register_module()
class Generate3DHeatmapTarget:
"""Generate the target 3d heatmap.
Required keys: 'joints_3d', 'joints_3d_visible', 'ann_info'.
Modified keys: 'target', and 'target_weight'.
Args:
sigma: Sigma of heatmap gaussian.
joint_indices (list): Indices of joints used for heatmap generation.
If None (default) is given, all joints will be used.
max_bound (float): The maximal value of heatmap.
"""
def __init__(self, sigma=2, joint_indices=None, max_bound=1.0):
self.sigma = sigma
self.joint_indices = joint_indices
self.max_bound = max_bound
def __call__(self, results):
"""Generate the target heatmap."""
joints_3d = results['joints_3d']
joints_3d_visible = results['joints_3d_visible']
cfg = results['ann_info']
image_size = cfg['image_size']
W, H, D = cfg['heatmap_size']
heatmap3d_depth_bound = cfg['heatmap3d_depth_bound']
joint_weights = cfg['joint_weights']
use_different_joint_weights = cfg['use_different_joint_weights']
# select the joints used for target generation
if self.joint_indices is not None:
joints_3d = joints_3d[self.joint_indices, ...]
joints_3d_visible = joints_3d_visible[self.joint_indices, ...]
joint_weights = joint_weights[self.joint_indices, ...]
num_joints = joints_3d.shape[0]
# get the joint location in heatmap coordinates
mu_x = joints_3d[:, 0] * W / image_size[0]
mu_y = joints_3d[:, 1] * H / image_size[1]
mu_z = (joints_3d[:, 2] / heatmap3d_depth_bound + 0.5) * D
target = np.zeros([num_joints, D, H, W], dtype=np.float32)
target_weight = joints_3d_visible[:, 0].astype(np.float32)
target_weight = target_weight * (mu_z >= 0) * (mu_z < D)
if use_different_joint_weights:
target_weight = target_weight * joint_weights
target_weight = target_weight[:, None]
# only compute the voxel value near the joints location
tmp_size = 3 * self.sigma
# get neighboring voxels coordinates
x = y = z = np.arange(2 * tmp_size + 1, dtype=np.float32) - tmp_size
zz, yy, xx = np.meshgrid(z, y, x)
xx = xx[None, ...].astype(np.float32)
yy = yy[None, ...].astype(np.float32)
zz = zz[None, ...].astype(np.float32)
mu_x = mu_x[..., None, None, None]
mu_y = mu_y[..., None, None, None]
mu_z = mu_z[..., None, None, None]
xx, yy, zz = xx + mu_x, yy + mu_y, zz + mu_z
# round the coordinates
xx = xx.round().clip(0, W - 1)
yy = yy.round().clip(0, H - 1)
zz = zz.round().clip(0, D - 1)
# compute the target value near joints
local_target = \
np.exp(-((xx - mu_x)**2 + (yy - mu_y)**2 + (zz - mu_z)**2) /
(2 * self.sigma**2))
# put the local target value to the full target heatmap
local_size = xx.shape[1]
idx_joints = np.tile(
np.arange(num_joints)[:, None, None, None],
[1, local_size, local_size, local_size])
idx = np.stack([idx_joints, zz, yy, xx],
axis=-1).astype(int).reshape(-1, 4)
target[idx[:, 0], idx[:, 1], idx[:, 2],
idx[:, 3]] = local_target.reshape(-1)
target = target * self.max_bound
results['target'] = target
results['target_weight'] = target_weight
return results
@PIPELINES.register_module()
class GenerateVoxel3DHeatmapTarget:
"""Generate the target 3d heatmap.
Required keys: 'joints_3d', 'joints_3d_visible', 'ann_info_3d'.
Modified keys: 'target', and 'target_weight'.
Args:
sigma: Sigma of heatmap gaussian (mm).
joint_indices (list): Indices of joints used for heatmap generation.
If None (default) is given, all joints will be used.
"""
def __init__(self, sigma=200.0, joint_indices=None):
self.sigma = sigma # mm
self.joint_indices = joint_indices
def __call__(self, results):
"""Generate the target heatmap."""
joints_3d = results['joints_3d']
joints_3d_visible = results['joints_3d_visible']
cfg = results['ann_info']
num_people = len(joints_3d)
num_joints = joints_3d[0].shape[0]
if self.joint_indices is not None:
num_joints = len(self.joint_indices)
joint_indices = self.joint_indices
else:
joint_indices = list(range(num_joints))
space_size = cfg['space_size']
space_center = cfg['space_center']
cube_size = cfg['cube_size']
grids_x = np.linspace(-space_size[0] / 2, space_size[0] / 2,
cube_size[0]) + space_center[0]
grids_y = np.linspace(-space_size[1] / 2, space_size[1] / 2,
cube_size[1]) + space_center[1]
grids_z = np.linspace(-space_size[2] / 2, space_size[2] / 2,
cube_size[2]) + space_center[2]
target = np.zeros(
(num_joints, cube_size[0], cube_size[1], cube_size[2]),
dtype=np.float32)
for n in range(num_people):
for idx, joint_id in enumerate(joint_indices):
mu_x = joints_3d[n][joint_id][0]
mu_y = joints_3d[n][joint_id][1]
mu_z = joints_3d[n][joint_id][2]
vis = joints_3d_visible[n][joint_id][0]
if vis < 1:
continue
i_x = [
np.searchsorted(grids_x, mu_x - 3 * self.sigma),
np.searchsorted(grids_x, mu_x + 3 * self.sigma, 'right')
]
i_y = [
np.searchsorted(grids_y, mu_y - 3 * self.sigma),
np.searchsorted(grids_y, mu_y + 3 * self.sigma, 'right')
]
i_z = [
np.searchsorted(grids_z, mu_z - 3 * self.sigma),
np.searchsorted(grids_z, mu_z + 3 * self.sigma, 'right')
]
if i_x[0] >= i_x[1] or i_y[0] >= i_y[1] or i_z[0] >= i_z[1]:
continue
kernel_xs, kernel_ys, kernel_zs = np.meshgrid(
grids_x[i_x[0]:i_x[1]],
grids_y[i_y[0]:i_y[1]],
grids_z[i_z[0]:i_z[1]],
indexing='ij')
g = np.exp(-((kernel_xs - mu_x)**2 + (kernel_ys - mu_y)**2 +
(kernel_zs - mu_z)**2) / (2 * self.sigma**2))
target[idx, i_x[0]:i_x[1], i_y[0]:i_y[1], i_z[0]:i_z[1]] \
= np.maximum(target[idx, i_x[0]:i_x[1],
i_y[0]:i_y[1], i_z[0]:i_z[1]], g)
target = np.clip(target, 0, 1)
if target.shape[0] == 1:
target = target[0]
results['targets_3d'] = target
return results
@@ -0,0 +1,527 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
from collections.abc import Sequence
import mmcv
import numpy as np
from mmcv.parallel import DataContainer as DC
from mmcv.utils import build_from_cfg
from numpy import random
from torchvision.transforms import functional as F
from ..builder import PIPELINES
try:
import albumentations
except ImportError:
albumentations = None
@PIPELINES.register_module()
class ToTensor:
"""Transform image to Tensor.
Required key: 'img'. Modifies key: 'img'.
Args:
results (dict): contain all information about training.
"""
def __call__(self, results):
if isinstance(results['img'], (list, tuple)):
results['img'] = [F.to_tensor(img) for img in results['img']]
else:
results['img'] = F.to_tensor(results['img'])
return results
@PIPELINES.register_module()
class NormalizeTensor:
"""Normalize the Tensor image (CxHxW), with mean and std.
Required key: 'img'. Modifies key: 'img'.
Args:
mean (list[float]): Mean values of 3 channels.
std (list[float]): Std values of 3 channels.
"""
def __init__(self, mean, std):
self.mean = mean
self.std = std
def __call__(self, results):
if isinstance(results['img'], (list, tuple)):
results['img'] = [
F.normalize(img, mean=self.mean, std=self.std)
for img in results['img']
]
else:
results['img'] = F.normalize(
results['img'], mean=self.mean, std=self.std)
return results
@PIPELINES.register_module()
class Compose:
"""Compose a data pipeline with a sequence of transforms.
Args:
transforms (list[dict | callable]): Either config
dicts of transforms or transform objects.
"""
def __init__(self, transforms):
assert isinstance(transforms, Sequence)
self.transforms = []
for transform in transforms:
if isinstance(transform, dict):
transform = build_from_cfg(transform, PIPELINES)
self.transforms.append(transform)
elif callable(transform):
self.transforms.append(transform)
else:
raise TypeError('transform must be callable or a dict, but got'
f' {type(transform)}')
def __call__(self, data):
"""Call function to apply transforms sequentially.
Args:
data (dict): A result dict contains the data to transform.
Returns:
dict: Transformed data.
"""
for t in self.transforms:
data = t(data)
if data is None:
return None
return data
def __repr__(self):
"""Compute the string representation."""
format_string = self.__class__.__name__ + '('
for t in self.transforms:
format_string += f'\n {t}'
format_string += '\n)'
return format_string
@PIPELINES.register_module()
class Collect:
"""Collect data from the loader relevant to the specific task.
This keeps the items in `keys` as it is, and collect items in `meta_keys`
into a meta item called `meta_name`.This is usually the last stage of the
data loader pipeline.
For example, when keys='imgs', meta_keys=('filename', 'label',
'original_shape'), meta_name='img_metas', the results will be a dict with
keys 'imgs' and 'img_metas', where 'img_metas' is a DataContainer of
another dict with keys 'filename', 'label', 'original_shape'.
Args:
keys (Sequence[str|tuple]): Required keys to be collected. If a tuple
(key, key_new) is given as an element, the item retrieved by key will
be renamed as key_new in collected data.
meta_name (str): The name of the key that contains meta information.
This key is always populated. Default: "img_metas".
meta_keys (Sequence[str|tuple]): Keys that are collected under
meta_name. The contents of the `meta_name` dictionary depends
on `meta_keys`.
"""
def __init__(self, keys, meta_keys, meta_name='img_metas'):
self.keys = keys
self.meta_keys = meta_keys
self.meta_name = meta_name
def __call__(self, results):
"""Performs the Collect formatting.
Args:
results (dict): The resulting dict to be modified and passed
to the next transform in pipeline.
"""
if 'ann_info' in results:
results.update(results['ann_info'])
data = {}
for key in self.keys:
if isinstance(key, tuple):
assert len(key) == 2
key_src, key_tgt = key[:2]
else:
key_src = key_tgt = key
data[key_tgt] = results[key_src]
meta = {}
if len(self.meta_keys) != 0:
for key in self.meta_keys:
if isinstance(key, tuple):
assert len(key) == 2
key_src, key_tgt = key[:2]
else:
key_src = key_tgt = key
meta[key_tgt] = results[key_src]
if 'bbox_id' in results:
meta['bbox_id'] = results['bbox_id']
data[self.meta_name] = DC(meta, cpu_only=True)
return data
def __repr__(self):
"""Compute the string representation."""
return (f'{self.__class__.__name__}('
f'keys={self.keys}, meta_keys={self.meta_keys})')
@PIPELINES.register_module()
class Albumentation:
"""Albumentation augmentation (pixel-level transforms only). Adds custom
pixel-level transformations from Albumentations library. Please visit
`https://albumentations.readthedocs.io` to get more information.
Note: we only support pixel-level transforms.
Please visit `https://github.com/albumentations-team/`
`albumentations#pixel-level-transforms`
to get more information about pixel-level transforms.
An example of ``transforms`` is as followed:
.. code-block:: python
[
dict(
type='RandomBrightnessContrast',
brightness_limit=[0.1, 0.3],
contrast_limit=[0.1, 0.3],
p=0.2),
dict(type='ChannelShuffle', p=0.1),
dict(
type='OneOf',
transforms=[
dict(type='Blur', blur_limit=3, p=1.0),
dict(type='MedianBlur', blur_limit=3, p=1.0)
],
p=0.1),
]
Args:
transforms (list[dict]): A list of Albumentation transformations
keymap (dict): Contains {'input key':'albumentation-style key'},
e.g., {'img': 'image'}.
"""
def __init__(self, transforms, keymap=None):
if albumentations is None:
raise RuntimeError('albumentations is not installed')
self.transforms = transforms
self.filter_lost_elements = False
self.aug = albumentations.Compose(
[self.albu_builder(t) for t in self.transforms])
if not keymap:
self.keymap_to_albu = {
'img': 'image',
}
else:
self.keymap_to_albu = keymap
self.keymap_back = {v: k for k, v in self.keymap_to_albu.items()}
def albu_builder(self, cfg):
"""Import a module from albumentations.
It resembles some of :func:`build_from_cfg` logic.
Args:
cfg (dict): Config dict. It should at least contain the key "type".
Returns:
obj: The constructed object.
"""
assert isinstance(cfg, dict) and 'type' in cfg
args = cfg.copy()
obj_type = args.pop('type')
if mmcv.is_str(obj_type):
if albumentations is None:
raise RuntimeError('albumentations is not installed')
if not hasattr(albumentations.augmentations.transforms, obj_type):
warnings.warn('{obj_type} is not pixel-level transformations. '
'Please use with caution.')
obj_cls = getattr(albumentations, obj_type)
else:
raise TypeError(f'type must be a str, but got {type(obj_type)}')
if 'transforms' in args:
args['transforms'] = [
self.albu_builder(transform)
for transform in args['transforms']
]
return obj_cls(**args)
@staticmethod
def mapper(d, keymap):
"""Dictionary mapper.
Renames keys according to keymap provided.
Args:
d (dict): old dict
keymap (dict): {'old_key':'new_key'}
Returns:
dict: new dict.
"""
updated_dict = {keymap.get(k, k): v for k, v in d.items()}
return updated_dict
def __call__(self, results):
# dict to albumentations format
results = self.mapper(results, self.keymap_to_albu)
results = self.aug(**results)
# back to the original format
results = self.mapper(results, self.keymap_back)
return results
def __repr__(self):
repr_str = self.__class__.__name__ + f'(transforms={self.transforms})'
return repr_str
@PIPELINES.register_module()
class PhotometricDistortion:
"""Apply photometric distortion to image sequentially, every transformation
is applied with a probability of 0.5. The position of random contrast is in
second or second to last.
1. random brightness
2. random contrast (mode 0)
3. convert color from BGR to HSV
4. random saturation
5. random hue
6. convert color from HSV to BGR
7. random contrast (mode 1)
8. randomly swap channels
Args:
brightness_delta (int): delta of brightness.
contrast_range (tuple): range of contrast.
saturation_range (tuple): range of saturation.
hue_delta (int): delta of hue.
"""
def __init__(self,
brightness_delta=32,
contrast_range=(0.5, 1.5),
saturation_range=(0.5, 1.5),
hue_delta=18):
self.brightness_delta = brightness_delta
self.contrast_lower, self.contrast_upper = contrast_range
self.saturation_lower, self.saturation_upper = saturation_range
self.hue_delta = hue_delta
def convert(self, img, alpha=1, beta=0):
"""Multiple with alpha and add beta with clip."""
img = img.astype(np.float32) * alpha + beta
img = np.clip(img, 0, 255)
return img.astype(np.uint8)
def brightness(self, img):
"""Brightness distortion."""
if random.randint(2):
return self.convert(
img,
beta=random.uniform(-self.brightness_delta,
self.brightness_delta))
return img
def contrast(self, img):
"""Contrast distortion."""
if random.randint(2):
return self.convert(
img,
alpha=random.uniform(self.contrast_lower, self.contrast_upper))
return img
def saturation(self, img):
# Apply saturation distortion to hsv-formatted img
img[:, :, 1] = self.convert(
img[:, :, 1],
alpha=random.uniform(self.saturation_lower, self.saturation_upper))
return img
def hue(self, img):
# Apply hue distortion to hsv-formatted img
img[:, :, 0] = (img[:, :, 0].astype(int) +
random.randint(-self.hue_delta, self.hue_delta)) % 180
return img
def swap_channels(self, img):
# Apply channel swap
if random.randint(2):
img = img[..., random.permutation(3)]
return img
def __call__(self, results):
"""Call function to perform photometric distortion on images.
Args:
results (dict): Result dict from loading pipeline.
Returns:
dict: Result dict with images distorted.
"""
img = results['img']
# random brightness
img = self.brightness(img)
# mode == 0 --> do random contrast first
# mode == 1 --> do random contrast last
mode = random.randint(2)
if mode == 1:
img = self.contrast(img)
hsv_mode = random.randint(4)
if hsv_mode:
# random saturation/hue distortion
img = mmcv.bgr2hsv(img)
if hsv_mode == 1 or hsv_mode == 3:
img = self.saturation(img)
if hsv_mode == 2 or hsv_mode == 3:
img = self.hue(img)
img = mmcv.hsv2bgr(img)
# random contrast
if mode == 0:
img = self.contrast(img)
# randomly swap channels
self.swap_channels(img)
results['img'] = img
return results
def __repr__(self):
repr_str = self.__class__.__name__
repr_str += (f'(brightness_delta={self.brightness_delta}, '
f'contrast_range=({self.contrast_lower}, '
f'{self.contrast_upper}), '
f'saturation_range=({self.saturation_lower}, '
f'{self.saturation_upper}), '
f'hue_delta={self.hue_delta})')
return repr_str
@PIPELINES.register_module()
class MultiItemProcess:
"""Process each item and merge multi-item results to lists.
Args:
pipeline (dict): Dictionary to construct pipeline for a single item.
"""
def __init__(self, pipeline):
self.pipeline = Compose(pipeline)
def __call__(self, results):
results_ = {}
for idx, result in results.items():
single_result = self.pipeline(result)
for k, v in single_result.items():
if k in results_:
results_[k].append(v)
else:
results_[k] = [v]
return results_
@PIPELINES.register_module()
class DiscardDuplicatedItems:
def __init__(self, keys_list):
"""Discard duplicated single-item results.
Args:
keys_list (list): List of keys that need to be deduplicate.
"""
self.keys_list = keys_list
def __call__(self, results):
for k, v in results.items():
if k in self.keys_list:
assert isinstance(v, Sequence)
results[k] = v[0]
return results
@PIPELINES.register_module()
class MultitaskGatherTarget:
"""Gather the targets for multitask heads.
Args:
pipeline_list (list[list]): List of pipelines for all heads.
pipeline_indices (list[int]): Pipeline index of each head.
"""
def __init__(self,
pipeline_list,
pipeline_indices=None,
keys=('target', 'target_weight')):
self.keys = keys
self.pipelines = []
for pipeline in pipeline_list:
self.pipelines.append(Compose(pipeline))
if pipeline_indices is None:
self.pipeline_indices = list(range(len(pipeline_list)))
else:
self.pipeline_indices = pipeline_indices
def __call__(self, results):
# generate target and target weights using all pipelines
pipeline_outputs = []
for pipeline in self.pipelines:
pipeline_output = pipeline(results)
pipeline_outputs.append(pipeline_output.copy())
for key in self.keys:
result_key = []
for ind in self.pipeline_indices:
result_key.append(pipeline_outputs[ind].get(key, None))
results[key] = result_key
return results
@PIPELINES.register_module()
class RenameKeys:
"""Rename the keys.
Args:
key_pairs (Sequence[tuple]): Required keys to be renamed.
If a tuple (key_src, key_tgt) is given as an element,
the item retrieved by key_src will be renamed as key_tgt.
"""
def __init__(self, key_pairs):
self.key_pairs = key_pairs
def __call__(self, results):
"""Rename keys."""
for key_pair in self.key_pairs:
assert len(key_pair) == 2
key_src, key_tgt = key_pair
results[key_tgt] = results.pop(key_src)
return results
@@ -0,0 +1,736 @@
# Copyright (c) OpenMMLab. All rights reserved.
import cv2
import numpy as np
from mmpose.core.post_processing import (affine_transform, fliplr_joints,
get_affine_transform, get_warp_matrix,
warp_affine_joints)
from mmpose.datasets.builder import PIPELINES
@PIPELINES.register_module()
class TopDownRandomFlip:
"""Data augmentation with random image flip.
Required keys: 'img', 'joints_3d', 'joints_3d_visible', 'center' and
'ann_info'.
Modifies key: 'img', 'joints_3d', 'joints_3d_visible', 'center' and
'flipped'.
Args:
flip (bool): Option to perform random flip.
flip_prob (float): Probability of flip.
"""
def __init__(self, flip_prob=0.5):
self.flip_prob = flip_prob
def __call__(self, results):
"""Perform data augmentation with random image flip."""
img = results['img']
joints_3d = results['joints_3d']
joints_3d_visible = results['joints_3d_visible']
center = results['center']
# A flag indicating whether the image is flipped,
# which can be used by child class.
flipped = False
if np.random.rand() <= self.flip_prob:
flipped = True
if not isinstance(img, list):
img = img[:, ::-1, :]
else:
img = [i[:, ::-1, :] for i in img]
if not isinstance(img, list):
joints_3d, joints_3d_visible = fliplr_joints(
joints_3d, joints_3d_visible, img.shape[1],
results['ann_info']['flip_pairs'])
center[0] = img.shape[1] - center[0] - 1
else:
joints_3d, joints_3d_visible = fliplr_joints(
joints_3d, joints_3d_visible, img[0].shape[1],
results['ann_info']['flip_pairs'])
center[0] = img[0].shape[1] - center[0] - 1
results['img'] = img
results['joints_3d'] = joints_3d
results['joints_3d_visible'] = joints_3d_visible
results['center'] = center
results['flipped'] = flipped
return results
@PIPELINES.register_module()
class TopDownHalfBodyTransform:
"""Data augmentation with half-body transform. Keep only the upper body or
the lower body at random.
Required keys: 'joints_3d', 'joints_3d_visible', and 'ann_info'.
Modifies key: 'scale' and 'center'.
Args:
num_joints_half_body (int): Threshold of performing
half-body transform. If the body has fewer number
of joints (< num_joints_half_body), ignore this step.
prob_half_body (float): Probability of half-body transform.
"""
def __init__(self, num_joints_half_body=8, prob_half_body=0.3):
self.num_joints_half_body = num_joints_half_body
self.prob_half_body = prob_half_body
@staticmethod
def half_body_transform(cfg, joints_3d, joints_3d_visible):
"""Get center&scale for half-body transform."""
upper_joints = []
lower_joints = []
for joint_id in range(cfg['num_joints']):
if joints_3d_visible[joint_id][0] > 0:
if joint_id in cfg['upper_body_ids']:
upper_joints.append(joints_3d[joint_id])
else:
lower_joints.append(joints_3d[joint_id])
if np.random.randn() < 0.5 and len(upper_joints) > 2:
selected_joints = upper_joints
elif len(lower_joints) > 2:
selected_joints = lower_joints
else:
selected_joints = upper_joints
if len(selected_joints) < 2:
return None, None
selected_joints = np.array(selected_joints, dtype=np.float32)
center = selected_joints.mean(axis=0)[:2]
left_top = np.amin(selected_joints, axis=0)
right_bottom = np.amax(selected_joints, axis=0)
w = right_bottom[0] - left_top[0]
h = right_bottom[1] - left_top[1]
aspect_ratio = cfg['image_size'][0] / cfg['image_size'][1]
if w > aspect_ratio * h:
h = w * 1.0 / aspect_ratio
elif w < aspect_ratio * h:
w = h * aspect_ratio
scale = np.array([w / 200.0, h / 200.0], dtype=np.float32)
scale = scale * 1.5
return center, scale
def __call__(self, results):
"""Perform data augmentation with half-body transform."""
joints_3d = results['joints_3d']
joints_3d_visible = results['joints_3d_visible']
if (np.sum(joints_3d_visible[:, 0]) > self.num_joints_half_body
and np.random.rand() < self.prob_half_body):
c_half_body, s_half_body = self.half_body_transform(
results['ann_info'], joints_3d, joints_3d_visible)
if c_half_body is not None and s_half_body is not None:
results['center'] = c_half_body
results['scale'] = s_half_body
return results
@PIPELINES.register_module()
class TopDownGetRandomScaleRotation:
"""Data augmentation with random scaling & rotating.
Required key: 'scale'.
Modifies key: 'scale' and 'rotation'.
Args:
rot_factor (int): Rotating to ``[-2*rot_factor, 2*rot_factor]``.
scale_factor (float): Scaling to ``[1-scale_factor, 1+scale_factor]``.
rot_prob (float): Probability of random rotation.
"""
def __init__(self, rot_factor=40, scale_factor=0.5, rot_prob=0.6):
self.rot_factor = rot_factor
self.scale_factor = scale_factor
self.rot_prob = rot_prob
def __call__(self, results):
"""Perform data augmentation with random scaling & rotating."""
s = results['scale']
sf = self.scale_factor
rf = self.rot_factor
s_factor = np.clip(np.random.randn() * sf + 1, 1 - sf, 1 + sf)
s = s * s_factor
r_factor = np.clip(np.random.randn() * rf, -rf * 2, rf * 2)
r = r_factor if np.random.rand() <= self.rot_prob else 0
results['scale'] = s
results['rotation'] = r
return results
@PIPELINES.register_module()
class TopDownAffine:
"""Affine transform the image to make input.
Required keys:'img', 'joints_3d', 'joints_3d_visible', 'ann_info','scale',
'rotation' and 'center'.
Modified keys:'img', 'joints_3d', and 'joints_3d_visible'.
Args:
use_udp (bool): To use unbiased data processing.
Paper ref: Huang et al. The Devil is in the Details: Delving into
Unbiased Data Processing for Human Pose Estimation (CVPR 2020).
"""
def __init__(self, use_udp=False):
self.use_udp = use_udp
def __call__(self, results):
image_size = results['ann_info']['image_size']
img = results['img']
joints_3d = results['joints_3d']
joints_3d_visible = results['joints_3d_visible']
c = results['center']
s = results['scale']
r = results['rotation']
if self.use_udp:
trans = get_warp_matrix(r, c * 2.0, image_size - 1.0, s * 200.0)
if not isinstance(img, list):
img = cv2.warpAffine(
img,
trans, (int(image_size[0]), int(image_size[1])),
flags=cv2.INTER_LINEAR)
else:
img = [
cv2.warpAffine(
i,
trans, (int(image_size[0]), int(image_size[1])),
flags=cv2.INTER_LINEAR) for i in img
]
joints_3d[:, 0:2] = \
warp_affine_joints(joints_3d[:, 0:2].copy(), trans)
else:
trans = get_affine_transform(c, s, r, image_size)
if not isinstance(img, list):
img = cv2.warpAffine(
img,
trans, (int(image_size[0]), int(image_size[1])),
flags=cv2.INTER_LINEAR)
else:
img = [
cv2.warpAffine(
i,
trans, (int(image_size[0]), int(image_size[1])),
flags=cv2.INTER_LINEAR) for i in img
]
for i in range(results['ann_info']['num_joints']):
if joints_3d_visible[i, 0] > 0.0:
joints_3d[i,
0:2] = affine_transform(joints_3d[i, 0:2], trans)
results['img'] = img
results['joints_3d'] = joints_3d
results['joints_3d_visible'] = joints_3d_visible
return results
@PIPELINES.register_module()
class TopDownGenerateTarget:
"""Generate the target heatmap.
Required keys: 'joints_3d', 'joints_3d_visible', 'ann_info'.
Modified keys: 'target', and 'target_weight'.
Args:
sigma: Sigma of heatmap gaussian for 'MSRA' approach.
kernel: Kernel of heatmap gaussian for 'Megvii' approach.
encoding (str): Approach to generate target heatmaps.
Currently supported approaches: 'MSRA', 'Megvii', 'UDP'.
Default:'MSRA'
unbiased_encoding (bool): Option to use unbiased
encoding methods.
Paper ref: Zhang et al. Distribution-Aware Coordinate
Representation for Human Pose Estimation (CVPR 2020).
keypoint_pose_distance: Keypoint pose distance for UDP.
Paper ref: Huang et al. The Devil is in the Details: Delving into
Unbiased Data Processing for Human Pose Estimation (CVPR 2020).
target_type (str): supported targets: 'GaussianHeatmap',
'CombinedTarget'. Default:'GaussianHeatmap'
CombinedTarget: The combination of classification target
(response map) and regression target (offset map).
Paper ref: Huang et al. The Devil is in the Details: Delving into
Unbiased Data Processing for Human Pose Estimation (CVPR 2020).
"""
def __init__(self,
sigma=2,
kernel=(11, 11),
valid_radius_factor=0.0546875,
target_type='GaussianHeatmap',
encoding='MSRA',
unbiased_encoding=False):
self.sigma = sigma
self.unbiased_encoding = unbiased_encoding
self.kernel = kernel
self.valid_radius_factor = valid_radius_factor
self.target_type = target_type
self.encoding = encoding
def _msra_generate_target(self, cfg, joints_3d, joints_3d_visible, sigma):
"""Generate the target heatmap via "MSRA" approach.
Args:
cfg (dict): data config
joints_3d: np.ndarray ([num_joints, 3])
joints_3d_visible: np.ndarray ([num_joints, 3])
sigma: Sigma of heatmap gaussian
Returns:
tuple: A tuple containing targets.
- target: Target heatmaps.
- target_weight: (1: visible, 0: invisible)
"""
num_joints = cfg['num_joints']
image_size = cfg['image_size']
W, H = cfg['heatmap_size']
joint_weights = cfg['joint_weights']
use_different_joint_weights = cfg['use_different_joint_weights']
target_weight = np.zeros((num_joints, 1), dtype=np.float32)
target = np.zeros((num_joints, H, W), dtype=np.float32)
# 3-sigma rule
tmp_size = sigma * 3
if self.unbiased_encoding:
for joint_id in range(num_joints):
target_weight[joint_id] = joints_3d_visible[joint_id, 0]
feat_stride = image_size / [W, H]
mu_x = joints_3d[joint_id][0] / feat_stride[0]
mu_y = joints_3d[joint_id][1] / feat_stride[1]
# Check that any part of the gaussian is in-bounds
ul = [mu_x - tmp_size, mu_y - tmp_size]
br = [mu_x + tmp_size + 1, mu_y + tmp_size + 1]
if ul[0] >= W or ul[1] >= H or br[0] < 0 or br[1] < 0:
target_weight[joint_id] = 0
if target_weight[joint_id] == 0:
continue
x = np.arange(0, W, 1, np.float32)
y = np.arange(0, H, 1, np.float32)
y = y[:, None]
if target_weight[joint_id] > 0.5:
target[joint_id] = np.exp(-((x - mu_x)**2 +
(y - mu_y)**2) /
(2 * sigma**2))
else:
for joint_id in range(num_joints):
target_weight[joint_id] = joints_3d_visible[joint_id, 0]
feat_stride = image_size / [W, H]
mu_x = int(joints_3d[joint_id][0] / feat_stride[0] + 0.5)
mu_y = int(joints_3d[joint_id][1] / feat_stride[1] + 0.5)
# Check that any part of the gaussian is in-bounds
ul = [int(mu_x - tmp_size), int(mu_y - tmp_size)]
br = [int(mu_x + tmp_size + 1), int(mu_y + tmp_size + 1)]
if ul[0] >= W or ul[1] >= H or br[0] < 0 or br[1] < 0:
target_weight[joint_id] = 0
if target_weight[joint_id] > 0.5:
size = 2 * tmp_size + 1
x = np.arange(0, size, 1, np.float32)
y = x[:, None]
x0 = y0 = size // 2
# The gaussian is not normalized,
# we want the center value to equal 1
g = np.exp(-((x - x0)**2 + (y - y0)**2) / (2 * sigma**2))
# Usable gaussian range
g_x = max(0, -ul[0]), min(br[0], W) - ul[0]
g_y = max(0, -ul[1]), min(br[1], H) - ul[1]
# Image range
img_x = max(0, ul[0]), min(br[0], W)
img_y = max(0, ul[1]), min(br[1], H)
target[joint_id][img_y[0]:img_y[1], img_x[0]:img_x[1]] = \
g[g_y[0]:g_y[1], g_x[0]:g_x[1]]
if use_different_joint_weights:
target_weight = np.multiply(target_weight, joint_weights)
return target, target_weight
def _megvii_generate_target(self, cfg, joints_3d, joints_3d_visible,
kernel):
"""Generate the target heatmap via "Megvii" approach.
Args:
cfg (dict): data config
joints_3d: np.ndarray ([num_joints, 3])
joints_3d_visible: np.ndarray ([num_joints, 3])
kernel: Kernel of heatmap gaussian
Returns:
tuple: A tuple containing targets.
- target: Target heatmaps.
- target_weight: (1: visible, 0: invisible)
"""
num_joints = cfg['num_joints']
image_size = cfg['image_size']
W, H = cfg['heatmap_size']
heatmaps = np.zeros((num_joints, H, W), dtype='float32')
target_weight = np.zeros((num_joints, 1), dtype=np.float32)
for i in range(num_joints):
target_weight[i] = joints_3d_visible[i, 0]
if target_weight[i] < 1:
continue
target_y = int(joints_3d[i, 1] * H / image_size[1])
target_x = int(joints_3d[i, 0] * W / image_size[0])
if (target_x >= W or target_x < 0) \
or (target_y >= H or target_y < 0):
target_weight[i] = 0
continue
heatmaps[i, target_y, target_x] = 1
heatmaps[i] = cv2.GaussianBlur(heatmaps[i], kernel, 0)
maxi = heatmaps[i, target_y, target_x]
heatmaps[i] /= maxi / 255
return heatmaps, target_weight
def _udp_generate_target(self, cfg, joints_3d, joints_3d_visible, factor,
target_type):
"""Generate the target heatmap via 'UDP' approach. Paper ref: Huang et
al. The Devil is in the Details: Delving into Unbiased Data Processing
for Human Pose Estimation (CVPR 2020).
Note:
- num keypoints: K
- heatmap height: H
- heatmap width: W
- num target channels: C
- C = K if target_type=='GaussianHeatmap'
- C = 3*K if target_type=='CombinedTarget'
Args:
cfg (dict): data config
joints_3d (np.ndarray[K, 3]): Annotated keypoints.
joints_3d_visible (np.ndarray[K, 3]): Visibility of keypoints.
factor (float): kernel factor for GaussianHeatmap target or
valid radius factor for CombinedTarget.
target_type (str): 'GaussianHeatmap' or 'CombinedTarget'.
GaussianHeatmap: Heatmap target with gaussian distribution.
CombinedTarget: The combination of classification target
(response map) and regression target (offset map).
Returns:
tuple: A tuple containing targets.
- target (np.ndarray[C, H, W]): Target heatmaps.
- target_weight (np.ndarray[K, 1]): (1: visible, 0: invisible)
"""
num_joints = cfg['num_joints']
image_size = cfg['image_size']
heatmap_size = cfg['heatmap_size']
joint_weights = cfg['joint_weights']
use_different_joint_weights = cfg['use_different_joint_weights']
target_weight = np.ones((num_joints, 1), dtype=np.float32)
target_weight[:, 0] = joints_3d_visible[:, 0]
if target_type.lower() == 'GaussianHeatmap'.lower():
target = np.zeros((num_joints, heatmap_size[1], heatmap_size[0]),
dtype=np.float32)
tmp_size = factor * 3
# prepare for gaussian
size = 2 * tmp_size + 1
x = np.arange(0, size, 1, np.float32)
y = x[:, None]
for joint_id in range(num_joints):
feat_stride = (image_size - 1.0) / (heatmap_size - 1.0)
mu_x = int(joints_3d[joint_id][0] / feat_stride[0] + 0.5)
mu_y = int(joints_3d[joint_id][1] / feat_stride[1] + 0.5)
# Check that any part of the gaussian is in-bounds
ul = [int(mu_x - tmp_size), int(mu_y - tmp_size)]
br = [int(mu_x + tmp_size + 1), int(mu_y + tmp_size + 1)]
if ul[0] >= heatmap_size[0] or ul[1] >= heatmap_size[1] \
or br[0] < 0 or br[1] < 0:
# If not, just return the image as is
target_weight[joint_id] = 0
continue
# # Generate gaussian
mu_x_ac = joints_3d[joint_id][0] / feat_stride[0]
mu_y_ac = joints_3d[joint_id][1] / feat_stride[1]
x0 = y0 = size // 2
x0 += mu_x_ac - mu_x
y0 += mu_y_ac - mu_y
g = np.exp(-((x - x0)**2 + (y - y0)**2) / (2 * factor**2))
# Usable gaussian range
g_x = max(0, -ul[0]), min(br[0], heatmap_size[0]) - ul[0]
g_y = max(0, -ul[1]), min(br[1], heatmap_size[1]) - ul[1]
# Image range
img_x = max(0, ul[0]), min(br[0], heatmap_size[0])
img_y = max(0, ul[1]), min(br[1], heatmap_size[1])
v = target_weight[joint_id]
if v > 0.5:
target[joint_id][img_y[0]:img_y[1], img_x[0]:img_x[1]] = \
g[g_y[0]:g_y[1], g_x[0]:g_x[1]]
elif target_type.lower() == 'CombinedTarget'.lower():
target = np.zeros(
(num_joints, 3, heatmap_size[1] * heatmap_size[0]),
dtype=np.float32)
feat_width = heatmap_size[0]
feat_height = heatmap_size[1]
feat_x_int = np.arange(0, feat_width)
feat_y_int = np.arange(0, feat_height)
feat_x_int, feat_y_int = np.meshgrid(feat_x_int, feat_y_int)
feat_x_int = feat_x_int.flatten()
feat_y_int = feat_y_int.flatten()
# Calculate the radius of the positive area in classification
# heatmap.
valid_radius = factor * heatmap_size[1]
feat_stride = (image_size - 1.0) / (heatmap_size - 1.0)
for joint_id in range(num_joints):
mu_x = joints_3d[joint_id][0] / feat_stride[0]
mu_y = joints_3d[joint_id][1] / feat_stride[1]
x_offset = (mu_x - feat_x_int) / valid_radius
y_offset = (mu_y - feat_y_int) / valid_radius
dis = x_offset**2 + y_offset**2
keep_pos = np.where(dis <= 1)[0]
v = target_weight[joint_id]
if v > 0.5:
target[joint_id, 0, keep_pos] = 1
target[joint_id, 1, keep_pos] = x_offset[keep_pos]
target[joint_id, 2, keep_pos] = y_offset[keep_pos]
target = target.reshape(num_joints * 3, heatmap_size[1],
heatmap_size[0])
else:
raise ValueError('target_type should be either '
"'GaussianHeatmap' or 'CombinedTarget'")
if use_different_joint_weights:
target_weight = np.multiply(target_weight, joint_weights)
return target, target_weight
def __call__(self, results):
"""Generate the target heatmap."""
joints_3d = results['joints_3d']
joints_3d_visible = results['joints_3d_visible']
assert self.encoding in ['MSRA', 'Megvii', 'UDP']
if self.encoding == 'MSRA':
if isinstance(self.sigma, list):
num_sigmas = len(self.sigma)
cfg = results['ann_info']
num_joints = cfg['num_joints']
heatmap_size = cfg['heatmap_size']
target = np.empty(
(0, num_joints, heatmap_size[1], heatmap_size[0]),
dtype=np.float32)
target_weight = np.empty((0, num_joints, 1), dtype=np.float32)
for i in range(num_sigmas):
target_i, target_weight_i = self._msra_generate_target(
cfg, joints_3d, joints_3d_visible, self.sigma[i])
target = np.concatenate([target, target_i[None]], axis=0)
target_weight = np.concatenate(
[target_weight, target_weight_i[None]], axis=0)
else:
target, target_weight = self._msra_generate_target(
results['ann_info'], joints_3d, joints_3d_visible,
self.sigma)
elif self.encoding == 'Megvii':
if isinstance(self.kernel, list):
num_kernels = len(self.kernel)
cfg = results['ann_info']
num_joints = cfg['num_joints']
W, H = cfg['heatmap_size']
target = np.empty((0, num_joints, H, W), dtype=np.float32)
target_weight = np.empty((0, num_joints, 1), dtype=np.float32)
for i in range(num_kernels):
target_i, target_weight_i = self._megvii_generate_target(
cfg, joints_3d, joints_3d_visible, self.kernel[i])
target = np.concatenate([target, target_i[None]], axis=0)
target_weight = np.concatenate(
[target_weight, target_weight_i[None]], axis=0)
else:
target, target_weight = self._megvii_generate_target(
results['ann_info'], joints_3d, joints_3d_visible,
self.kernel)
elif self.encoding == 'UDP':
if self.target_type.lower() == 'CombinedTarget'.lower():
factors = self.valid_radius_factor
channel_factor = 3
elif self.target_type.lower() == 'GaussianHeatmap'.lower():
factors = self.sigma
channel_factor = 1
else:
raise ValueError('target_type should be either '
"'GaussianHeatmap' or 'CombinedTarget'")
if isinstance(factors, list):
num_factors = len(factors)
cfg = results['ann_info']
num_joints = cfg['num_joints']
W, H = cfg['heatmap_size']
target = np.empty((0, channel_factor * num_joints, H, W),
dtype=np.float32)
target_weight = np.empty((0, num_joints, 1), dtype=np.float32)
for i in range(num_factors):
target_i, target_weight_i = self._udp_generate_target(
cfg, joints_3d, joints_3d_visible, factors[i],
self.target_type)
target = np.concatenate([target, target_i[None]], axis=0)
target_weight = np.concatenate(
[target_weight, target_weight_i[None]], axis=0)
else:
target, target_weight = self._udp_generate_target(
results['ann_info'], joints_3d, joints_3d_visible, factors,
self.target_type)
else:
raise ValueError(
f'Encoding approach {self.encoding} is not supported!')
if results['ann_info'].get('max_num_joints', None) is not None:
W, H = results['ann_info']['heatmap_size']
padded_length = int(results['ann_info'].get('max_num_joints') - results['ann_info'].get('num_joints'))
target_weight = np.concatenate([target_weight, np.zeros((padded_length, 1), dtype=np.float32)], 0)
target = np.concatenate([target, np.zeros((padded_length, H, W), dtype=np.float32)], 0)
results['target'] = target
results['target_weight'] = target_weight
results['dataset_idx'] = results['ann_info'].get('dataset_idx', 0)
return results
@PIPELINES.register_module()
class TopDownGenerateTargetRegression:
"""Generate the target regression vector (coordinates).
Required keys: 'joints_3d', 'joints_3d_visible', 'ann_info'. Modified keys:
'target', and 'target_weight'.
"""
def __init__(self):
pass
def _generate_target(self, cfg, joints_3d, joints_3d_visible):
"""Generate the target regression vector.
Args:
cfg (dict): data config
joints_3d: np.ndarray([num_joints, 3])
joints_3d_visible: np.ndarray([num_joints, 3])
Returns:
target, target_weight(1: visible, 0: invisible)
"""
image_size = cfg['image_size']
joint_weights = cfg['joint_weights']
use_different_joint_weights = cfg['use_different_joint_weights']
mask = (joints_3d[:, 0] >= 0) * (
joints_3d[:, 0] <= image_size[0] - 1) * (joints_3d[:, 1] >= 0) * (
joints_3d[:, 1] <= image_size[1] - 1)
target = joints_3d[:, :2] / image_size
target = target.astype(np.float32)
target_weight = joints_3d_visible[:, :2] * mask[:, None]
if use_different_joint_weights:
target_weight = np.multiply(target_weight, joint_weights)
return target, target_weight
def __call__(self, results):
"""Generate the target heatmap."""
joints_3d = results['joints_3d']
joints_3d_visible = results['joints_3d_visible']
target, target_weight = self._generate_target(results['ann_info'],
joints_3d,
joints_3d_visible)
results['target'] = target
results['target_weight'] = target_weight
return results
@PIPELINES.register_module()
class TopDownRandomTranslation:
"""Data augmentation with random translation.
Required key: 'scale' and 'center'.
Modifies key: 'center'.
Note:
- bbox height: H
- bbox width: W
Args:
trans_factor (float): Translating center to
``[-trans_factor, trans_factor] * [W, H] + center``.
trans_prob (float): Probability of random translation.
"""
def __init__(self, trans_factor=0.15, trans_prob=1.0):
self.trans_factor = trans_factor
self.trans_prob = trans_prob
def __call__(self, results):
"""Perform data augmentation with random translation."""
center = results['center']
scale = results['scale']
if np.random.rand() <= self.trans_prob:
# reference bbox size is [200, 200] pixels
center += self.trans_factor * np.random.uniform(
-1, 1, size=2) * scale * 200
results['center'] = center
return results
@@ -0,0 +1,13 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
from .builder import DATASETS, PIPELINES
__all__ = ['DATASETS', 'PIPELINES']
warnings.simplefilter('once', DeprecationWarning)
warnings.warn(
'Registries (DATASETS, PIPELINES) have been moved to '
'mmpose.datasets.builder. Importing from '
'mmpose.models.registry will be deprecated in the future.',
DeprecationWarning)
@@ -0,0 +1,4 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .distributed_sampler import DistributedSampler
__all__ = ['DistributedSampler']
@@ -0,0 +1,41 @@
# Copyright (c) OpenMMLab. All rights reserved.
import torch
from torch.utils.data import DistributedSampler as _DistributedSampler
class DistributedSampler(_DistributedSampler):
"""DistributedSampler inheriting from
`torch.utils.data.DistributedSampler`.
In pytorch of lower versions, there is no `shuffle` argument. This child
class will port one to DistributedSampler.
"""
def __init__(self,
dataset,
num_replicas=None,
rank=None,
shuffle=True,
seed=0):
super().__init__(
dataset, num_replicas=num_replicas, rank=rank, shuffle=shuffle)
# for the compatibility from PyTorch 1.3+
self.seed = seed if seed is not None else 0
def __iter__(self):
"""Deterministically shuffle based on epoch."""
if self.shuffle:
g = torch.Generator()
g.manual_seed(self.epoch + self.seed)
indices = torch.randperm(len(self.dataset), generator=g).tolist()
else:
indices = torch.arange(len(self.dataset)).tolist()
# add extra samples to make it evenly divisible
indices += indices[:(self.total_size - len(indices))]
assert len(indices) == self.total_size
# subsample
indices = indices[self.rank:self.total_size:self.num_replicas]
assert len(indices) == self.num_samples
return iter(indices)