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,17 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .associative_embedding import AssociativeEmbedding
from .interhand_3d import Interhand3D
from .mesh import ParametricMesh
from .multi_task import MultiTask
from .multiview_pose import (DetectAndRegress, VoxelCenterDetector,
VoxelSinglePose)
from .pose_lifter import PoseLifter
from .posewarper import PoseWarper
from .top_down import TopDown
from .top_down_moe import TopDownMoE
__all__ = [
'TopDown', 'AssociativeEmbedding', 'ParametricMesh', 'MultiTask',
'PoseLifter', 'Interhand3D', 'PoseWarper', 'DetectAndRegress',
'VoxelCenterDetector', 'VoxelSinglePose', 'TopDownMoE'
]
@@ -0,0 +1,420 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
import mmcv
import torch
from mmcv.image import imwrite
from mmcv.utils.misc import deprecated_api_warning
from mmcv.visualization.image import imshow
from mmpose.core.evaluation import (aggregate_scale, aggregate_stage_flip,
flip_feature_maps, get_group_preds,
split_ae_outputs)
from mmpose.core.post_processing.group import HeatmapParser
from mmpose.core.visualization import imshow_keypoints
from .. import builder
from ..builder import POSENETS
from .base import BasePose
try:
from mmcv.runner import auto_fp16
except ImportError:
warnings.warn('auto_fp16 from mmpose will be deprecated from v0.15.0'
'Please install mmcv>=1.1.4')
from mmpose.core import auto_fp16
@POSENETS.register_module()
class AssociativeEmbedding(BasePose):
"""Associative embedding pose detectors.
Args:
backbone (dict): Backbone modules to extract feature.
keypoint_head (dict): Keypoint head to process feature.
train_cfg (dict): Config for training. Default: None.
test_cfg (dict): Config for testing. Default: None.
pretrained (str): Path to the pretrained models.
loss_pose (None): Deprecated arguments. Please use
``loss_keypoint`` for heads instead.
"""
def __init__(self,
backbone,
keypoint_head=None,
train_cfg=None,
test_cfg=None,
pretrained=None,
loss_pose=None):
super().__init__()
self.fp16_enabled = False
self.backbone = builder.build_backbone(backbone)
if keypoint_head is not None:
if 'loss_keypoint' not in keypoint_head and loss_pose is not None:
warnings.warn(
'`loss_pose` for BottomUp is deprecated, '
'use `loss_keypoint` for heads instead. See '
'https://github.com/open-mmlab/mmpose/pull/382'
' for more information.', DeprecationWarning)
keypoint_head['loss_keypoint'] = loss_pose
self.keypoint_head = builder.build_head(keypoint_head)
self.train_cfg = train_cfg
self.test_cfg = test_cfg
self.use_udp = test_cfg.get('use_udp', False)
self.parser = HeatmapParser(self.test_cfg)
self.init_weights(pretrained=pretrained)
@property
def with_keypoint(self):
"""Check if has keypoint_head."""
return hasattr(self, 'keypoint_head')
def init_weights(self, pretrained=None):
"""Weight initialization for model."""
self.backbone.init_weights(pretrained)
if self.with_keypoint:
self.keypoint_head.init_weights()
@auto_fp16(apply_to=('img', ))
def forward(self,
img=None,
targets=None,
masks=None,
joints=None,
img_metas=None,
return_loss=True,
return_heatmap=False,
**kwargs):
"""Calls either forward_train or forward_test depending on whether
return_loss is True.
Note:
- batch_size: N
- num_keypoints: K
- num_img_channel: C
- img_width: imgW
- img_height: imgH
- heatmaps weight: W
- heatmaps height: H
- max_num_people: M
Args:
img (torch.Tensor[N,C,imgH,imgW]): Input image.
targets (list(torch.Tensor[N,K,H,W])): Multi-scale target heatmaps.
masks (list(torch.Tensor[N,H,W])): Masks of multi-scale target
heatmaps
joints (list(torch.Tensor[N,M,K,2])): Joints of multi-scale target
heatmaps for ae loss
img_metas (dict): Information about val & test.
By default it includes:
- "image_file": image path
- "aug_data": input
- "test_scale_factor": test scale factor
- "base_size": base size of input
- "center": center of image
- "scale": scale of image
- "flip_index": flip index of keypoints
return loss (bool): ``return_loss=True`` for training,
``return_loss=False`` for validation & test.
return_heatmap (bool) : Option to return heatmap.
Returns:
dict|tuple: if 'return_loss' is true, then return losses. \
Otherwise, return predicted poses, scores, image \
paths and heatmaps.
"""
if return_loss:
return self.forward_train(img, targets, masks, joints, img_metas,
**kwargs)
return self.forward_test(
img, img_metas, return_heatmap=return_heatmap, **kwargs)
def forward_train(self, img, targets, masks, joints, img_metas, **kwargs):
"""Forward the bottom-up model and calculate the loss.
Note:
batch_size: N
num_keypoints: K
num_img_channel: C
img_width: imgW
img_height: imgH
heatmaps weight: W
heatmaps height: H
max_num_people: M
Args:
img (torch.Tensor[N,C,imgH,imgW]): Input image.
targets (List(torch.Tensor[N,K,H,W])): Multi-scale target heatmaps.
masks (List(torch.Tensor[N,H,W])): Masks of multi-scale target
heatmaps
joints (List(torch.Tensor[N,M,K,2])): Joints of multi-scale target
heatmaps for ae loss
img_metas (dict):Information about val&test
By default this includes:
- "image_file": image path
- "aug_data": input
- "test_scale_factor": test scale factor
- "base_size": base size of input
- "center": center of image
- "scale": scale of image
- "flip_index": flip index of keypoints
Returns:
dict: The total loss for bottom-up
"""
output = self.backbone(img)
if self.with_keypoint:
output = self.keypoint_head(output)
# if return loss
losses = dict()
if self.with_keypoint:
keypoint_losses = self.keypoint_head.get_loss(
output, targets, masks, joints)
losses.update(keypoint_losses)
return losses
def forward_dummy(self, img):
"""Used for computing network FLOPs.
See ``tools/get_flops.py``.
Args:
img (torch.Tensor): Input image.
Returns:
Tensor: Outputs.
"""
output = self.backbone(img)
if self.with_keypoint:
output = self.keypoint_head(output)
return output
def forward_test(self, img, img_metas, return_heatmap=False, **kwargs):
"""Inference the bottom-up model.
Note:
- Batchsize: N (currently support batchsize = 1)
- num_img_channel: C
- img_width: imgW
- img_height: imgH
Args:
flip_index (List(int)):
aug_data (List(Tensor[NxCximgHximgW])): Multi-scale image
test_scale_factor (List(float)): Multi-scale factor
base_size (Tuple(int)): Base size of image when scale is 1
center (np.ndarray): center of image
scale (np.ndarray): the scale of image
"""
assert img.size(0) == 1
assert len(img_metas) == 1
img_metas = img_metas[0]
aug_data = img_metas['aug_data']
test_scale_factor = img_metas['test_scale_factor']
base_size = img_metas['base_size']
center = img_metas['center']
scale = img_metas['scale']
result = {}
scale_heatmaps_list = []
scale_tags_list = []
for idx, s in enumerate(sorted(test_scale_factor, reverse=True)):
image_resized = aug_data[idx].to(img.device)
features = self.backbone(image_resized)
if self.with_keypoint:
outputs = self.keypoint_head(features)
heatmaps, tags = split_ae_outputs(
outputs, self.test_cfg['num_joints'],
self.test_cfg['with_heatmaps'], self.test_cfg['with_ae'],
self.test_cfg.get('select_output_index', range(len(outputs))))
if self.test_cfg.get('flip_test', True):
# use flip test
features_flipped = self.backbone(
torch.flip(image_resized, [3]))
if self.with_keypoint:
outputs_flipped = self.keypoint_head(features_flipped)
heatmaps_flipped, tags_flipped = split_ae_outputs(
outputs_flipped, self.test_cfg['num_joints'],
self.test_cfg['with_heatmaps'], self.test_cfg['with_ae'],
self.test_cfg.get('select_output_index',
range(len(outputs))))
heatmaps_flipped = flip_feature_maps(
heatmaps_flipped, flip_index=img_metas['flip_index'])
if self.test_cfg['tag_per_joint']:
tags_flipped = flip_feature_maps(
tags_flipped, flip_index=img_metas['flip_index'])
else:
tags_flipped = flip_feature_maps(
tags_flipped, flip_index=None, flip_output=True)
else:
heatmaps_flipped = None
tags_flipped = None
aggregated_heatmaps = aggregate_stage_flip(
heatmaps,
heatmaps_flipped,
index=-1,
project2image=self.test_cfg['project2image'],
size_projected=base_size,
align_corners=self.test_cfg.get('align_corners', True),
aggregate_stage='average',
aggregate_flip='average')
aggregated_tags = aggregate_stage_flip(
tags,
tags_flipped,
index=-1,
project2image=self.test_cfg['project2image'],
size_projected=base_size,
align_corners=self.test_cfg.get('align_corners', True),
aggregate_stage='concat',
aggregate_flip='concat')
if s == 1 or len(test_scale_factor) == 1:
if isinstance(aggregated_tags, list):
scale_tags_list.extend(aggregated_tags)
else:
scale_tags_list.append(aggregated_tags)
if isinstance(aggregated_heatmaps, list):
scale_heatmaps_list.extend(aggregated_heatmaps)
else:
scale_heatmaps_list.append(aggregated_heatmaps)
aggregated_heatmaps = aggregate_scale(
scale_heatmaps_list,
align_corners=self.test_cfg.get('align_corners', True),
aggregate_scale='average')
aggregated_tags = aggregate_scale(
scale_tags_list,
align_corners=self.test_cfg.get('align_corners', True),
aggregate_scale='unsqueeze_concat')
heatmap_size = aggregated_heatmaps.shape[2:4]
tag_size = aggregated_tags.shape[2:4]
if heatmap_size != tag_size:
tmp = []
for idx in range(aggregated_tags.shape[-1]):
tmp.append(
torch.nn.functional.interpolate(
aggregated_tags[..., idx],
size=heatmap_size,
mode='bilinear',
align_corners=self.test_cfg.get('align_corners',
True)).unsqueeze(-1))
aggregated_tags = torch.cat(tmp, dim=-1)
# perform grouping
grouped, scores = self.parser.parse(aggregated_heatmaps,
aggregated_tags,
self.test_cfg['adjust'],
self.test_cfg['refine'])
preds = get_group_preds(
grouped,
center,
scale, [aggregated_heatmaps.size(3),
aggregated_heatmaps.size(2)],
use_udp=self.use_udp)
image_paths = []
image_paths.append(img_metas['image_file'])
if return_heatmap:
output_heatmap = aggregated_heatmaps.detach().cpu().numpy()
else:
output_heatmap = None
result['preds'] = preds
result['scores'] = scores
result['image_paths'] = image_paths
result['output_heatmap'] = output_heatmap
return result
@deprecated_api_warning({'pose_limb_color': 'pose_link_color'},
cls_name='AssociativeEmbedding')
def show_result(self,
img,
result,
skeleton=None,
kpt_score_thr=0.3,
bbox_color=None,
pose_kpt_color=None,
pose_link_color=None,
radius=4,
thickness=1,
font_scale=0.5,
win_name='',
show=False,
show_keypoint_weight=False,
wait_time=0,
out_file=None):
"""Draw `result` over `img`.
Args:
img (str or Tensor): The image to be displayed.
result (list[dict]): The results to draw over `img`
(bbox_result, pose_result).
skeleton (list[list]): The connection of keypoints.
skeleton is 0-based indexing.
kpt_score_thr (float, optional): Minimum score of keypoints
to be shown. Default: 0.3.
pose_kpt_color (np.array[Nx3]`): Color of N keypoints.
If None, do not draw keypoints.
pose_link_color (np.array[Mx3]): Color of M links.
If None, do not draw links.
radius (int): Radius of circles.
thickness (int): Thickness of lines.
font_scale (float): Font scales of texts.
win_name (str): The window name.
show (bool): Whether to show the image. Default: False.
show_keypoint_weight (bool): Whether to change the transparency
using the predicted confidence scores of keypoints.
wait_time (int): Value of waitKey param.
Default: 0.
out_file (str or None): The filename to write the image.
Default: None.
Returns:
Tensor: Visualized image only if not `show` or `out_file`
"""
img = mmcv.imread(img)
img = img.copy()
img_h, img_w, _ = img.shape
pose_result = []
for res in result:
pose_result.append(res['keypoints'])
imshow_keypoints(img, pose_result, skeleton, kpt_score_thr,
pose_kpt_color, pose_link_color, radius, thickness)
if show:
imshow(img, win_name, wait_time)
if out_file is not None:
imwrite(img, out_file)
return img
@@ -0,0 +1,131 @@
# Copyright (c) OpenMMLab. All rights reserved.
from abc import ABCMeta, abstractmethod
from collections import OrderedDict
import torch
import torch.distributed as dist
import torch.nn as nn
class BasePose(nn.Module, metaclass=ABCMeta):
"""Base class for pose detectors.
All recognizers should subclass it.
All subclass should overwrite:
Methods:`forward_train`, supporting to forward when training.
Methods:`forward_test`, supporting to forward when testing.
Args:
backbone (dict): Backbone modules to extract feature.
head (dict): Head modules to give output.
train_cfg (dict): Config for training. Default: None.
test_cfg (dict): Config for testing. Default: None.
"""
@abstractmethod
def forward_train(self, img, img_metas, **kwargs):
"""Defines the computation performed at training."""
@abstractmethod
def forward_test(self, img, img_metas, **kwargs):
"""Defines the computation performed at testing."""
@abstractmethod
def forward(self, img, img_metas, return_loss=True, **kwargs):
"""Forward function."""
@staticmethod
def _parse_losses(losses):
"""Parse the raw outputs (losses) of the network.
Args:
losses (dict): Raw output of the network, which usually contain
losses and other necessary information.
Returns:
tuple[Tensor, dict]: (loss, log_vars), loss is the loss tensor \
which may be a weighted sum of all losses, log_vars \
contains all the variables to be sent to the logger.
"""
log_vars = OrderedDict()
for loss_name, loss_value in losses.items():
if isinstance(loss_value, torch.Tensor):
log_vars[loss_name] = loss_value.mean()
elif isinstance(loss_value, float):
log_vars[loss_name] = loss_value
elif isinstance(loss_value, list):
log_vars[loss_name] = sum(_loss.mean() for _loss in loss_value)
else:
raise TypeError(
f'{loss_name} is not a tensor or list of tensors or float')
loss = sum(_value for _key, _value in log_vars.items()
if 'loss' in _key)
log_vars['loss'] = loss
for loss_name, loss_value in log_vars.items():
# reduce loss when distributed training
if not isinstance(loss_value, float):
if dist.is_available() and dist.is_initialized():
loss_value = loss_value.data.clone()
dist.all_reduce(loss_value.div_(dist.get_world_size()))
log_vars[loss_name] = loss_value.item()
else:
log_vars[loss_name] = loss_value
return loss, log_vars
def train_step(self, data_batch, optimizer, **kwargs):
"""The iteration step during training.
This method defines an iteration step during training, except for the
back propagation and optimizer updating, which are done in an optimizer
hook. Note that in some complicated cases or models, the whole process
including back propagation and optimizer updating is also defined in
this method, such as GAN.
Args:
data_batch (dict): The output of dataloader.
optimizer (:obj:`torch.optim.Optimizer` | dict): The optimizer of
runner is passed to ``train_step()``. This argument is unused
and reserved.
Returns:
dict: It should contain at least 3 keys: ``loss``, ``log_vars``,
``num_samples``.
``loss`` is a tensor for back propagation, which can be a
weighted sum of multiple losses.
``log_vars`` contains all the variables to be sent to the
logger.
``num_samples`` indicates the batch size (when the model is
DDP, it means the batch size on each GPU), which is used for
averaging the logs.
"""
losses = self.forward(**data_batch)
loss, log_vars = self._parse_losses(losses)
outputs = dict(
loss=loss,
log_vars=log_vars,
num_samples=len(next(iter(data_batch.values()))))
return outputs
def val_step(self, data_batch, optimizer, **kwargs):
"""The iteration step during validation.
This method shares the same signature as :func:`train_step`, but used
during val epochs. Note that the evaluation after training epochs is
not implemented with this method, but an evaluation hook.
"""
results = self.forward(return_loss=False, **data_batch)
outputs = dict(results=results)
return outputs
@abstractmethod
def show_result(self, **kwargs):
"""Visualize the results."""
raise NotImplementedError
@@ -0,0 +1,227 @@
# Copyright (c) OpenMMLab. All rights reserved.
import mmcv
import numpy as np
from mmcv.utils.misc import deprecated_api_warning
from mmpose.core import imshow_keypoints, imshow_keypoints_3d
from ..builder import POSENETS
from .top_down import TopDown
@POSENETS.register_module()
class Interhand3D(TopDown):
"""Top-down interhand 3D pose detector of paper ref: Gyeongsik Moon.
"InterHand2.6M: A Dataset and Baseline for 3D Interacting Hand Pose
Estimation from a Single RGB Image". A child class of TopDown detector.
"""
def forward(self,
img,
target=None,
target_weight=None,
img_metas=None,
return_loss=True,
**kwargs):
"""Calls either forward_train or forward_test depending on whether
return_loss=True. Note this setting will change the expected inputs.
When `return_loss=True`, img and img_meta are single-nested (i.e.
Tensor and List[dict]), and when `resturn_loss=False`, img and img_meta
should be double nested (i.e. list[Tensor], list[list[dict]]), with
the outer list indicating test time augmentations.
Note:
- batch_size: N
- num_keypoints: K
- num_img_channel: C (Default: 3)
- img height: imgH
- img width: imgW
- heatmaps height: H
- heatmaps weight: W
Args:
img (torch.Tensor[NxCximgHximgW]): Input images.
target (list[torch.Tensor]): Target heatmaps, relative hand
root depth and hand type.
target_weight (list[torch.Tensor]): Weights for target
heatmaps, relative hand root depth and hand type.
img_metas (list(dict)): Information about data augmentation
By default this includes:
- "image_file: path to the image file
- "center": center of the bbox
- "scale": scale of the bbox
- "rotation": rotation of the bbox
- "bbox_score": score of bbox
- "heatmap3d_depth_bound": depth bound of hand keypoint 3D
heatmap
- "root_depth_bound": depth bound of relative root depth 1D
heatmap
return_loss (bool): Option to `return loss`. `return loss=True`
for training, `return loss=False` for validation & test.
Returns:
dict|tuple: if `return loss` is true, then return losses. \
Otherwise, return predicted poses, boxes, image paths, \
heatmaps, relative hand root depth and hand type.
"""
if return_loss:
return self.forward_train(img, target, target_weight, img_metas,
**kwargs)
return self.forward_test(img, img_metas, **kwargs)
def forward_test(self, img, img_metas, **kwargs):
"""Defines the computation performed at every call when testing."""
assert img.size(0) == len(img_metas)
batch_size, _, img_height, img_width = img.shape
if batch_size > 1:
assert 'bbox_id' in img_metas[0]
features = self.backbone(img)
if self.with_neck:
features = self.neck(features)
if self.with_keypoint:
output = self.keypoint_head.inference_model(
features, flip_pairs=None)
if self.test_cfg.get('flip_test', True):
img_flipped = img.flip(3)
features_flipped = self.backbone(img_flipped)
if self.with_neck:
features_flipped = self.neck(features_flipped)
if self.with_keypoint:
output_flipped = self.keypoint_head.inference_model(
features_flipped, img_metas[0]['flip_pairs'])
output = [(out + out_flipped) * 0.5
for out, out_flipped in zip(output, output_flipped)]
if self.with_keypoint:
result = self.keypoint_head.decode(
img_metas, output, img_size=[img_width, img_height])
else:
result = {}
return result
@deprecated_api_warning({'pose_limb_color': 'pose_link_color'},
cls_name='Interhand3D')
def show_result(self,
result,
img=None,
skeleton=None,
kpt_score_thr=0.3,
radius=8,
bbox_color='green',
thickness=2,
pose_kpt_color=None,
pose_link_color=None,
vis_height=400,
num_instances=-1,
win_name='',
show=False,
wait_time=0,
out_file=None):
"""Visualize 3D pose estimation results.
Args:
result (list[dict]): The pose estimation results containing:
- "keypoints_3d" ([K,4]): 3D keypoints
- "keypoints" ([K,3] or [T,K,3]): Optional for visualizing
2D inputs. If a sequence is given, only the last frame
will be used for visualization
- "bbox" ([4,] or [T,4]): Optional for visualizing 2D inputs
- "title" (str): title for the subplot
img (str or Tensor): Optional. The image to visualize 2D inputs on.
skeleton (list of [idx_i,idx_j]): Skeleton described by a list of
links, each is a pair of joint indices.
kpt_score_thr (float, optional): Minimum score of keypoints
to be shown. Default: 0.3.
radius (int): Radius of circles.
bbox_color (str or tuple or :obj:`Color`): Color of bbox lines.
thickness (int): Thickness of lines.
pose_kpt_color (np.array[Nx3]`): Color of N keypoints.
If None, do not draw keypoints.
pose_link_color (np.array[Mx3]): Color of M limbs.
If None, do not draw limbs.
vis_height (int): The image height of the visualization. The width
will be N*vis_height depending on the number of visualized
items.
num_instances (int): Number of instances to be shown in 3D. If
smaller than 0, all the instances in the pose_result will be
shown. Otherwise, pad or truncate the pose_result to a length
of num_instances.
win_name (str): The window name.
show (bool): Whether to show the image. Default: False.
wait_time (int): Value of waitKey param.
Default: 0.
out_file (str or None): The filename to write the image.
Default: None.
Returns:
Tensor: Visualized img, only if not `show` or `out_file`.
"""
if num_instances < 0:
assert len(result) > 0
result = sorted(result, key=lambda x: x.get('track_id', 0))
# draw image and 2d poses
if img is not None:
img = mmcv.imread(img)
bbox_result = []
pose_2d = []
for res in result:
if 'bbox' in res:
bbox = np.array(res['bbox'])
if bbox.ndim != 1:
assert bbox.ndim == 2
bbox = bbox[-1] # Get bbox from the last frame
bbox_result.append(bbox)
if 'keypoints' in res:
kpts = np.array(res['keypoints'])
if kpts.ndim != 2:
assert kpts.ndim == 3
kpts = kpts[-1] # Get 2D keypoints from the last frame
pose_2d.append(kpts)
if len(bbox_result) > 0:
bboxes = np.vstack(bbox_result)
mmcv.imshow_bboxes(
img,
bboxes,
colors=bbox_color,
top_k=-1,
thickness=2,
show=False)
if len(pose_2d) > 0:
imshow_keypoints(
img,
pose_2d,
skeleton,
kpt_score_thr=kpt_score_thr,
pose_kpt_color=pose_kpt_color,
pose_link_color=pose_link_color,
radius=radius,
thickness=thickness)
img = mmcv.imrescale(img, scale=vis_height / img.shape[0])
img_vis = imshow_keypoints_3d(
result,
img,
skeleton,
pose_kpt_color,
pose_link_color,
vis_height,
axis_limit=300,
axis_azimuth=-115,
axis_elev=15,
kpt_score_thr=kpt_score_thr,
num_instances=num_instances)
if show:
mmcv.visualization.imshow(img_vis, win_name, wait_time)
if out_file is not None:
mmcv.imwrite(img_vis, out_file)
return img_vis
@@ -0,0 +1,438 @@
# Copyright (c) OpenMMLab. All rights reserved.
import cv2
import mmcv
import numpy as np
import torch
from mmpose.core.visualization.image import imshow_mesh_3d
from mmpose.models.misc.discriminator import SMPLDiscriminator
from .. import builder
from ..builder import POSENETS
from .base import BasePose
def set_requires_grad(nets, requires_grad=False):
"""Set requies_grad for all the networks.
Args:
nets (nn.Module | list[nn.Module]): A list of networks or a single
network.
requires_grad (bool): Whether the networks require gradients or not
"""
if not isinstance(nets, list):
nets = [nets]
for net in nets:
if net is not None:
for param in net.parameters():
param.requires_grad = requires_grad
@POSENETS.register_module()
class ParametricMesh(BasePose):
"""Model-based 3D human mesh detector. Take a single color image as input
and output 3D joints, SMPL parameters and camera parameters.
Args:
backbone (dict): Backbone modules to extract feature.
mesh_head (dict): Mesh head to process feature.
smpl (dict): Config for SMPL model.
disc (dict): Discriminator for SMPL parameters. Default: None.
loss_gan (dict): Config for adversarial loss. Default: None.
loss_mesh (dict): Config for mesh loss. Default: None.
train_cfg (dict): Config for training. Default: None.
test_cfg (dict): Config for testing. Default: None.
pretrained (str): Path to the pretrained models.
"""
def __init__(self,
backbone,
mesh_head,
smpl,
disc=None,
loss_gan=None,
loss_mesh=None,
train_cfg=None,
test_cfg=None,
pretrained=None):
super().__init__()
self.backbone = builder.build_backbone(backbone)
self.mesh_head = builder.build_head(mesh_head)
self.generator = torch.nn.Sequential(self.backbone, self.mesh_head)
self.smpl = builder.build_mesh_model(smpl)
self.with_gan = disc is not None and loss_gan is not None
if self.with_gan:
self.discriminator = SMPLDiscriminator(**disc)
self.loss_gan = builder.build_loss(loss_gan)
self.disc_step_count = 0
self.train_cfg = train_cfg
self.test_cfg = test_cfg
self.loss_mesh = builder.build_loss(loss_mesh)
self.init_weights(pretrained=pretrained)
def init_weights(self, pretrained=None):
"""Weight initialization for model."""
self.backbone.init_weights(pretrained)
self.mesh_head.init_weights()
if self.with_gan:
self.discriminator.init_weights()
def train_step(self, data_batch, optimizer, **kwargs):
"""Train step function.
In this function, the detector will finish the train step following
the pipeline:
1. get fake and real SMPL parameters
2. optimize discriminator (if have)
3. optimize generator
If `self.train_cfg.disc_step > 1`, the train step will contain multiple
iterations for optimizing discriminator with different input data and
only one iteration for optimizing generator after `disc_step`
iterations for discriminator.
Args:
data_batch (torch.Tensor): Batch of data as input.
optimizer (dict[torch.optim.Optimizer]): Dict with optimizers for
generator and discriminator (if have).
Returns:
outputs (dict): Dict with loss, information for logger,
the number of samples.
"""
img = data_batch['img']
pred_smpl = self.generator(img)
pred_pose, pred_beta, pred_camera = pred_smpl
# optimize discriminator (if have)
if self.train_cfg['disc_step'] > 0 and self.with_gan:
set_requires_grad(self.discriminator, True)
fake_data = (pred_camera.detach(), pred_pose.detach(),
pred_beta.detach())
mosh_theta = data_batch['mosh_theta']
real_data = (mosh_theta[:, :3], mosh_theta[:,
3:75], mosh_theta[:,
75:])
fake_score = self.discriminator(fake_data)
real_score = self.discriminator(real_data)
disc_losses = {}
disc_losses['real_loss'] = self.loss_gan(
real_score, target_is_real=True, is_disc=True)
disc_losses['fake_loss'] = self.loss_gan(
fake_score, target_is_real=False, is_disc=True)
loss_disc, log_vars_d = self._parse_losses(disc_losses)
optimizer['discriminator'].zero_grad()
loss_disc.backward()
optimizer['discriminator'].step()
self.disc_step_count = \
(self.disc_step_count + 1) % self.train_cfg['disc_step']
if self.disc_step_count != 0:
outputs = dict(
loss=loss_disc,
log_vars=log_vars_d,
num_samples=len(next(iter(data_batch.values()))))
return outputs
# optimize generator
pred_out = self.smpl(
betas=pred_beta,
body_pose=pred_pose[:, 1:],
global_orient=pred_pose[:, :1])
pred_vertices, pred_joints_3d = pred_out['vertices'], pred_out[
'joints']
gt_beta = data_batch['beta']
gt_pose = data_batch['pose']
gt_vertices = self.smpl(
betas=gt_beta,
body_pose=gt_pose[:, 3:],
global_orient=gt_pose[:, :3])['vertices']
pred = dict(
pose=pred_pose,
beta=pred_beta,
camera=pred_camera,
vertices=pred_vertices,
joints_3d=pred_joints_3d)
target = {
key: data_batch[key]
for key in [
'pose', 'beta', 'has_smpl', 'joints_3d', 'joints_2d',
'joints_3d_visible', 'joints_2d_visible'
]
}
target['vertices'] = gt_vertices
losses = self.loss_mesh(pred, target)
if self.with_gan:
set_requires_grad(self.discriminator, False)
pred_theta = (pred_camera, pred_pose, pred_beta)
pred_score = self.discriminator(pred_theta)
loss_adv = self.loss_gan(
pred_score, target_is_real=True, is_disc=False)
losses['adv_loss'] = loss_adv
loss, log_vars = self._parse_losses(losses)
optimizer['generator'].zero_grad()
loss.backward()
optimizer['generator'].step()
outputs = dict(
loss=loss,
log_vars=log_vars,
num_samples=len(next(iter(data_batch.values()))))
return outputs
def forward_train(self, *args, **kwargs):
"""Forward function for training.
For ParametricMesh, we do not use this interface.
"""
raise NotImplementedError('This interface should not be used in '
'current training schedule. Please use '
'`train_step` for training.')
def val_step(self, data_batch, **kwargs):
"""Forward function for evaluation.
Args:
data_batch (dict): Contain data for forward.
Returns:
dict: Contain the results from model.
"""
output = self.forward_test(**data_batch, **kwargs)
return output
def forward_dummy(self, img):
"""Used for computing network FLOPs.
See ``tools/get_flops.py``.
Args:
img (torch.Tensor): Input image.
Returns:
Tensor: Outputs.
"""
output = self.generator(img)
return output
def forward_test(self,
img,
img_metas,
return_vertices=False,
return_faces=False,
**kwargs):
"""Defines the computation performed at every call when testing."""
pred_smpl = self.generator(img)
pred_pose, pred_beta, pred_camera = pred_smpl
pred_out = self.smpl(
betas=pred_beta,
body_pose=pred_pose[:, 1:],
global_orient=pred_pose[:, :1])
pred_vertices, pred_joints_3d = pred_out['vertices'], pred_out[
'joints']
all_preds = {}
all_preds['keypoints_3d'] = pred_joints_3d.detach().cpu().numpy()
all_preds['smpl_pose'] = pred_pose.detach().cpu().numpy()
all_preds['smpl_beta'] = pred_beta.detach().cpu().numpy()
all_preds['camera'] = pred_camera.detach().cpu().numpy()
if return_vertices:
all_preds['vertices'] = pred_vertices.detach().cpu().numpy()
if return_faces:
all_preds['faces'] = self.smpl.get_faces()
all_boxes = []
image_path = []
for img_meta in img_metas:
box = np.zeros(6, dtype=np.float32)
c = img_meta['center']
s = img_meta['scale']
if 'bbox_score' in img_metas:
score = np.array(img_metas['bbox_score']).reshape(-1)
else:
score = 1.0
box[0:2] = c
box[2:4] = s
box[4] = np.prod(s * 200.0, axis=0)
box[5] = score
all_boxes.append(box)
image_path.append(img_meta['image_file'])
all_preds['bboxes'] = np.stack(all_boxes, axis=0)
all_preds['image_path'] = image_path
return all_preds
def get_3d_joints_from_mesh(self, vertices):
"""Get 3D joints from 3D mesh using predefined joints regressor."""
return torch.matmul(
self.joints_regressor.to(vertices.device), vertices)
def forward(self, img, img_metas=None, return_loss=False, **kwargs):
"""Forward function.
Calls either forward_train or forward_test depending on whether
return_loss=True.
Note:
- batch_size: N
- num_img_channel: C (Default: 3)
- img height: imgH
- img width: imgW
Args:
img (torch.Tensor[N x C x imgH x imgW]): Input images.
img_metas (list(dict)): Information about data augmentation
By default this includes:
- "image_file: path to the image file
- "center": center of the bbox
- "scale": scale of the bbox
- "rotation": rotation of the bbox
- "bbox_score": score of bbox
return_loss (bool): Option to `return loss`. `return loss=True`
for training, `return loss=False` for validation & test.
Returns:
Return predicted 3D joints, SMPL parameters, boxes and image paths.
"""
if return_loss:
return self.forward_train(img, img_metas, **kwargs)
return self.forward_test(img, img_metas, **kwargs)
def show_result(self,
result,
img,
show=False,
out_file=None,
win_name='',
wait_time=0,
bbox_color='green',
mesh_color=(76, 76, 204),
**kwargs):
"""Visualize 3D mesh estimation results.
Args:
result (list[dict]): The mesh estimation results containing:
- "bbox" (ndarray[4]): instance bounding bbox
- "center" (ndarray[2]): bbox center
- "scale" (ndarray[2]): bbox scale
- "keypoints_3d" (ndarray[K,3]): predicted 3D keypoints
- "camera" (ndarray[3]): camera parameters
- "vertices" (ndarray[V, 3]): predicted 3D vertices
- "faces" (ndarray[F, 3]): mesh faces
img (str or Tensor): Optional. The image to visualize 2D inputs on.
win_name (str): The window name.
show (bool): Whether to show the image. Default: False.
wait_time (int): Value of waitKey param. Default: 0.
out_file (str or None): The filename to write the image.
Default: None.
bbox_color (str or tuple or :obj:`Color`): Color of bbox lines.
mesh_color (str or tuple or :obj:`Color`): Color of mesh surface.
Returns:
ndarray: Visualized img, only if not `show` or `out_file`.
"""
if img is not None:
img = mmcv.imread(img)
focal_length = self.loss_mesh.focal_length
H, W, C = img.shape
img_center = np.array([[0.5 * W], [0.5 * H]])
# show bounding boxes
bboxes = [res['bbox'] for res in result]
bboxes = np.vstack(bboxes)
mmcv.imshow_bboxes(
img, bboxes, colors=bbox_color, top_k=-1, thickness=2, show=False)
vertex_list = []
face_list = []
for res in result:
vertices = res['vertices']
faces = res['faces']
camera = res['camera']
camera_center = res['center']
scale = res['scale']
# predicted vertices are in root-relative space,
# we need to translate them to camera space.
translation = np.array([
camera[1], camera[2],
2 * focal_length / (scale[0] * 200.0 * camera[0] + 1e-9)
])
mean_depth = vertices[:, -1].mean() + translation[-1]
translation[:2] += (camera_center -
img_center[:, 0]) / focal_length * mean_depth
vertices += translation[None, :]
vertex_list.append(vertices)
face_list.append(faces)
# render from front view
img_vis = imshow_mesh_3d(
img,
vertex_list,
face_list,
img_center, [focal_length, focal_length],
colors=mesh_color)
# render from side view
# rotate mesh vertices
R = cv2.Rodrigues(np.array([0, np.radians(90.), 0]))[0]
rot_vertex_list = [np.dot(vert, R) for vert in vertex_list]
# get the 3D bbox containing all meshes
rot_vertices = np.concatenate(rot_vertex_list, axis=0)
min_corner = rot_vertices.min(0)
max_corner = rot_vertices.max(0)
center_3d = 0.5 * (min_corner + max_corner)
ratio = 0.8
bbox3d_size = max_corner - min_corner
# set appropriate translation to make all meshes appear in the image
z_x = bbox3d_size[0] * focal_length / (ratio * W) - min_corner[2]
z_y = bbox3d_size[1] * focal_length / (ratio * H) - min_corner[2]
z = max(z_x, z_y)
translation = -center_3d
translation[2] = z
translation = translation[None, :]
rot_vertex_list = [
rot_vert + translation for rot_vert in rot_vertex_list
]
# render from side view
img_side = imshow_mesh_3d(
np.ones_like(img) * 255, rot_vertex_list, face_list, img_center,
[focal_length, focal_length])
# merger images from front view and side view
img_vis = np.concatenate([img_vis, img_side], axis=1)
if show:
mmcv.visualization.imshow(img_vis, win_name, wait_time)
if out_file is not None:
mmcv.imwrite(img_vis, out_file)
return img_vis
@@ -0,0 +1,187 @@
# Copyright (c) OpenMMLab. All rights reserved.
import torch.nn as nn
from .. import builder
from ..builder import POSENETS
@POSENETS.register_module()
class MultiTask(nn.Module):
"""Multi-task detectors.
Args:
backbone (dict): Backbone modules to extract feature.
heads (list[dict]): heads to output predictions.
necks (list[dict] | None): necks to process feature.
head2neck (dict{int:int}): head index to neck index.
pretrained (str): Path to the pretrained models.
"""
def __init__(self,
backbone,
heads,
necks=None,
head2neck=None,
pretrained=None):
super().__init__()
self.backbone = builder.build_backbone(backbone)
if head2neck is None:
assert necks is None
head2neck = {}
self.head2neck = {}
for i in range(len(heads)):
self.head2neck[i] = head2neck[i] if i in head2neck else -1
self.necks = nn.ModuleList([])
if necks is not None:
for neck in necks:
self.necks.append(builder.build_neck(neck))
self.necks.append(nn.Identity())
self.heads = nn.ModuleList([])
assert heads is not None
for head in heads:
assert head is not None
self.heads.append(builder.build_head(head))
self.init_weights(pretrained=pretrained)
@property
def with_necks(self):
"""Check if has keypoint_head."""
return hasattr(self, 'necks')
def init_weights(self, pretrained=None):
"""Weight initialization for model."""
self.backbone.init_weights(pretrained)
if self.with_necks:
for neck in self.necks:
if hasattr(neck, 'init_weights'):
neck.init_weights()
for head in self.heads:
if hasattr(head, 'init_weights'):
head.init_weights()
def forward(self,
img,
target=None,
target_weight=None,
img_metas=None,
return_loss=True,
**kwargs):
"""Calls either forward_train or forward_test depending on whether
return_loss=True. Note this setting will change the expected inputs.
When `return_loss=True`, img and img_meta are single-nested (i.e.
Tensor and List[dict]), and when `resturn_loss=False`, img and img_meta
should be double nested (i.e. List[Tensor], List[List[dict]]), with
the outer list indicating test time augmentations.
Note:
- batch_size: N
- num_keypoints: K
- num_img_channel: C (Default: 3)
- img height: imgH
- img weight: imgW
- heatmaps height: H
- heatmaps weight: W
Args:
img (torch.Tensor[N,C,imgH,imgW]): Input images.
target (list[torch.Tensor]): Targets.
target_weight (List[torch.Tensor]): Weights.
img_metas (list(dict)): Information about data augmentation
By default this includes:
- "image_file: path to the image file
- "center": center of the bbox
- "scale": scale of the bbox
- "rotation": rotation of the bbox
- "bbox_score": score of bbox
return_loss (bool): Option to `return loss`. `return loss=True`
for training, `return loss=False` for validation & test.
Returns:
dict|tuple: if `return loss` is true, then return losses. \
Otherwise, return predicted poses, boxes, image paths \
and heatmaps.
"""
if return_loss:
return self.forward_train(img, target, target_weight, img_metas,
**kwargs)
return self.forward_test(img, img_metas, **kwargs)
def forward_train(self, img, target, target_weight, img_metas, **kwargs):
"""Defines the computation performed at every call when training."""
features = self.backbone(img)
outputs = []
for head_id, head in enumerate(self.heads):
neck_id = self.head2neck[head_id]
outputs.append(head(self.necks[neck_id](features)))
# if return loss
losses = dict()
for head, output, gt, gt_weight in zip(self.heads, outputs, target,
target_weight):
loss = head.get_loss(output, gt, gt_weight)
assert len(set(losses.keys()).intersection(set(loss.keys()))) == 0
losses.update(loss)
if hasattr(head, 'get_accuracy'):
acc = head.get_accuracy(output, gt, gt_weight)
assert len(set(losses.keys()).intersection(set(
acc.keys()))) == 0
losses.update(acc)
return losses
def forward_test(self, img, img_metas, **kwargs):
"""Defines the computation performed at every call when testing."""
assert img.size(0) == len(img_metas)
batch_size, _, img_height, img_width = img.shape
if batch_size > 1:
assert 'bbox_id' in img_metas[0]
results = {}
features = self.backbone(img)
outputs = []
for head_id, head in enumerate(self.heads):
neck_id = self.head2neck[head_id]
if hasattr(head, 'inference_model'):
head_output = head.inference_model(
self.necks[neck_id](features), flip_pairs=None)
else:
head_output = head(
self.necks[neck_id](features)).detach().cpu().numpy()
outputs.append(head_output)
for head, output in zip(self.heads, outputs):
result = head.decode(
img_metas, output, img_size=[img_width, img_height])
results.update(result)
return results
def forward_dummy(self, img):
"""Used for computing network FLOPs.
See ``tools/get_flops.py``.
Args:
img (torch.Tensor): Input image.
Returns:
list[Tensor]: Outputs.
"""
features = self.backbone(img)
outputs = []
for head_id, head in enumerate(self.heads):
neck_id = self.head2neck[head_id]
outputs.append(head(self.necks[neck_id](features)))
return outputs
@@ -0,0 +1,889 @@
# Copyright (c) OpenMMLab. All rights reserved.
import torch
import torch.nn as nn
import torch.nn.functional as F
from mmcv.runner import load_checkpoint
from mmpose.core.camera import SimpleCameraTorch
from mmpose.core.post_processing.post_transforms import (
affine_transform_torch, get_affine_transform)
from .. import builder
from ..builder import POSENETS
from .base import BasePose
class ProjectLayer(nn.Module):
def __init__(self, image_size, heatmap_size):
"""Project layer to get voxel feature. Adapted from
https://github.com/microsoft/voxelpose-
pytorch/blob/main/lib/models/project_layer.py.
Args:
image_size (int or list): input size of the 2D model
heatmap_size (int or list): output size of the 2D model
"""
super(ProjectLayer, self).__init__()
self.image_size = image_size
self.heatmap_size = heatmap_size
if isinstance(self.image_size, int):
self.image_size = [self.image_size, self.image_size]
if isinstance(self.heatmap_size, int):
self.heatmap_size = [self.heatmap_size, self.heatmap_size]
def compute_grid(self, box_size, box_center, num_bins, device=None):
if isinstance(box_size, int) or isinstance(box_size, float):
box_size = [box_size, box_size, box_size]
if isinstance(num_bins, int):
num_bins = [num_bins, num_bins, num_bins]
grid_1D_x = torch.linspace(
-box_size[0] / 2, box_size[0] / 2, num_bins[0], device=device)
grid_1D_y = torch.linspace(
-box_size[1] / 2, box_size[1] / 2, num_bins[1], device=device)
grid_1D_z = torch.linspace(
-box_size[2] / 2, box_size[2] / 2, num_bins[2], device=device)
grid_x, grid_y, grid_z = torch.meshgrid(
grid_1D_x + box_center[0],
grid_1D_y + box_center[1],
grid_1D_z + box_center[2],
)
grid_x = grid_x.contiguous().view(-1, 1)
grid_y = grid_y.contiguous().view(-1, 1)
grid_z = grid_z.contiguous().view(-1, 1)
grid = torch.cat([grid_x, grid_y, grid_z], dim=1)
return grid
def get_voxel(self, feature_maps, meta, grid_size, grid_center, cube_size):
device = feature_maps[0].device
batch_size = feature_maps[0].shape[0]
num_channels = feature_maps[0].shape[1]
num_bins = cube_size[0] * cube_size[1] * cube_size[2]
n = len(feature_maps)
cubes = torch.zeros(
batch_size, num_channels, 1, num_bins, n, device=device)
w, h = self.heatmap_size
grids = torch.zeros(batch_size, num_bins, 3, device=device)
bounding = torch.zeros(batch_size, 1, 1, num_bins, n, device=device)
for i in range(batch_size):
if len(grid_center[0]) == 3 or grid_center[i][3] >= 0:
if len(grid_center) == 1:
grid = self.compute_grid(
grid_size, grid_center[0], cube_size, device=device)
else:
grid = self.compute_grid(
grid_size, grid_center[i], cube_size, device=device)
grids[i:i + 1] = grid
for c in range(n):
center = meta[i]['center'][c]
scale = meta[i]['scale'][c]
width, height = center * 2
trans = torch.as_tensor(
get_affine_transform(center, scale / 200.0, 0,
self.image_size),
dtype=torch.float,
device=device)
cam_param = meta[i]['camera'][c].copy()
single_view_camera = SimpleCameraTorch(
param=cam_param, device=device)
xy = single_view_camera.world_to_pixel(grid)
bounding[i, 0, 0, :, c] = (xy[:, 0] >= 0) & (
xy[:, 1] >= 0) & (xy[:, 0] < width) & (
xy[:, 1] < height)
xy = torch.clamp(xy, -1.0, max(width, height))
xy = affine_transform_torch(xy, trans)
xy = xy * torch.tensor(
[w, h], dtype=torch.float,
device=device) / torch.tensor(
self.image_size, dtype=torch.float, device=device)
sample_grid = xy / torch.tensor([w - 1, h - 1],
dtype=torch.float,
device=device) * 2.0 - 1.0
sample_grid = torch.clamp(
sample_grid.view(1, 1, num_bins, 2), -1.1, 1.1)
cubes[i:i + 1, :, :, :, c] += F.grid_sample(
feature_maps[c][i:i + 1, :, :, :],
sample_grid,
align_corners=True)
cubes = torch.sum(
torch.mul(cubes, bounding), dim=-1) / (
torch.sum(bounding, dim=-1) + 1e-6)
cubes[cubes != cubes] = 0.0
cubes = cubes.clamp(0.0, 1.0)
cubes = cubes.view(batch_size, num_channels, cube_size[0],
cube_size[1], cube_size[2])
return cubes, grids
def forward(self, feature_maps, meta, grid_size, grid_center, cube_size):
cubes, grids = self.get_voxel(feature_maps, meta, grid_size,
grid_center, cube_size)
return cubes, grids
@POSENETS.register_module()
class DetectAndRegress(BasePose):
"""DetectAndRegress approach for multiview human pose detection.
Args:
backbone (ConfigDict): Dictionary to construct the 2D pose detector
human_detector (ConfigDict): dictionary to construct human detector
pose_regressor (ConfigDict): dictionary to construct pose regressor
train_cfg (ConfigDict): Config for training. Default: None.
test_cfg (ConfigDict): Config for testing. Default: None.
pretrained (str): Path to the pretrained 2D model. Default: None.
freeze_2d (bool): Whether to freeze the 2D model in training.
Default: True.
"""
def __init__(self,
backbone,
human_detector,
pose_regressor,
train_cfg=None,
test_cfg=None,
pretrained=None,
freeze_2d=True):
super(DetectAndRegress, self).__init__()
if backbone is not None:
self.backbone = builder.build_posenet(backbone)
if self.training and pretrained is not None:
load_checkpoint(self.backbone, pretrained)
else:
self.backbone = None
self.freeze_2d = freeze_2d
self.human_detector = builder.MODELS.build(human_detector)
self.pose_regressor = builder.MODELS.build(pose_regressor)
self.train_cfg = train_cfg
self.test_cfg = test_cfg
@staticmethod
def _freeze(model):
"""Freeze parameters."""
model.eval()
for param in model.parameters():
param.requires_grad = False
def train(self, mode=True):
"""Sets the module in training mode.
Args:
mode (bool): whether to set training mode (``True``)
or evaluation mode (``False``). Default: ``True``.
Returns:
Module: self
"""
super().train(mode)
if mode and self.freeze_2d and self.backbone is not None:
self._freeze(self.backbone)
return self
def forward(self,
img=None,
img_metas=None,
return_loss=True,
targets=None,
masks=None,
targets_3d=None,
input_heatmaps=None,
**kwargs):
"""
Note:
batch_size: N
num_keypoints: K
num_img_channel: C
img_width: imgW
img_height: imgH
feature_maps width: W
feature_maps height: H
volume_length: cubeL
volume_width: cubeW
volume_height: cubeH
Args:
img (list(torch.Tensor[NxCximgHximgW])):
Multi-camera input images to the 2D model.
img_metas (list(dict)):
Information about image, 3D groundtruth and camera parameters.
return_loss: Option to `return loss`. `return loss=True`
for training, `return loss=False` for validation & test.
targets (list(torch.Tensor[NxKxHxW])):
Multi-camera target feature_maps of the 2D model.
masks (list(torch.Tensor[NxHxW])):
Multi-camera masks of the input to the 2D model.
targets_3d (torch.Tensor[NxcubeLxcubeWxcubeH]):
Ground-truth 3D heatmap of human centers.
input_heatmaps (list(torch.Tensor[NxKxHxW])):
Multi-camera feature_maps when the 2D model is not available.
Default: None.
**kwargs:
Returns:
dict: if 'return_loss' is true, then return losses.
Otherwise, return predicted poses, human centers and sample_id
"""
if return_loss:
return self.forward_train(img, img_metas, targets, masks,
targets_3d, input_heatmaps)
else:
return self.forward_test(img, img_metas, input_heatmaps)
def train_step(self, data_batch, optimizer, **kwargs):
"""The iteration step during training.
This method defines an iteration step during training, except for the
back propagation and optimizer updating, which are done in an optimizer
hook. Note that in some complicated cases or models, the whole process
including back propagation and optimizer updating is also defined in
this method, such as GAN.
Args:
data_batch (dict): The output of dataloader.
optimizer (:obj:`torch.optim.Optimizer` | dict): The optimizer of
runner is passed to ``train_step()``. This argument is unused
and reserved.
Returns:
dict: It should contain at least 3 keys: ``loss``, ``log_vars``,
``num_samples``.
``loss`` is a tensor for back propagation, which can be a
weighted sum of multiple losses.
``log_vars`` contains all the variables to be sent to the
logger.
``num_samples`` indicates the batch size (when the model is
DDP, it means the batch size on each GPU), which is used for
averaging the logs.
"""
losses = self.forward(**data_batch)
loss, log_vars = self._parse_losses(losses)
if 'img' in data_batch:
batch_size = data_batch['img'][0].shape[0]
else:
assert 'input_heatmaps' in data_batch
batch_size = data_batch['input_heatmaps'][0][0].shape[0]
outputs = dict(loss=loss, log_vars=log_vars, num_samples=batch_size)
return outputs
def forward_train(self,
img,
img_metas,
targets=None,
masks=None,
targets_3d=None,
input_heatmaps=None):
"""
Note:
batch_size: N
num_keypoints: K
num_img_channel: C
img_width: imgW
img_height: imgH
feature_maps width: W
feature_maps height: H
volume_length: cubeL
volume_width: cubeW
volume_height: cubeH
Args:
img (list(torch.Tensor[NxCximgHximgW])):
Multi-camera input images to the 2D model.
img_metas (list(dict)):
Information about image, 3D groundtruth and camera parameters.
targets (list(torch.Tensor[NxKxHxW])):
Multi-camera target feature_maps of the 2D model.
masks (list(torch.Tensor[NxHxW])):
Multi-camera masks of the input to the 2D model.
targets_3d (torch.Tensor[NxcubeLxcubeWxcubeH]):
Ground-truth 3D heatmap of human centers.
input_heatmaps (list(torch.Tensor[NxKxHxW])):
Multi-camera feature_maps when the 2D model is not available.
Default: None.
Returns:
dict: losses.
"""
if self.backbone is None:
assert input_heatmaps is not None
feature_maps = []
for input_heatmap in input_heatmaps:
feature_maps.append(input_heatmap[0])
else:
feature_maps = []
assert isinstance(img, list)
for img_ in img:
feature_maps.append(self.backbone.forward_dummy(img_)[0])
losses = dict()
human_candidates, human_loss = self.human_detector.forward_train(
None, img_metas, feature_maps, targets_3d, return_preds=True)
losses.update(human_loss)
pose_loss = self.pose_regressor(
None,
img_metas,
return_loss=True,
feature_maps=feature_maps,
human_candidates=human_candidates)
losses.update(pose_loss)
if not self.freeze_2d:
losses_2d = {}
heatmaps_tensor = torch.cat(feature_maps, dim=0)
targets_tensor = torch.cat(targets, dim=0)
masks_tensor = torch.cat(masks, dim=0)
losses_2d_ = self.backbone.get_loss(heatmaps_tensor,
targets_tensor, masks_tensor)
for k, v in losses_2d_.items():
losses_2d[k + '_2d'] = v
losses.update(losses_2d)
return losses
def forward_test(
self,
img,
img_metas,
input_heatmaps=None,
):
"""
Note:
batch_size: N
num_keypoints: K
num_img_channel: C
img_width: imgW
img_height: imgH
feature_maps width: W
feature_maps height: H
volume_length: cubeL
volume_width: cubeW
volume_height: cubeH
Args:
img (list(torch.Tensor[NxCximgHximgW])):
Multi-camera input images to the 2D model.
img_metas (list(dict)):
Information about image, 3D groundtruth and camera parameters.
input_heatmaps (list(torch.Tensor[NxKxHxW])):
Multi-camera feature_maps when the 2D model is not available.
Default: None.
Returns:
dict: predicted poses, human centers and sample_id
"""
if self.backbone is None:
assert input_heatmaps is not None
feature_maps = []
for input_heatmap in input_heatmaps:
feature_maps.append(input_heatmap[0])
else:
feature_maps = []
assert isinstance(img, list)
for img_ in img:
feature_maps.append(self.backbone.forward_dummy(img_)[0])
human_candidates = self.human_detector.forward_test(
None, img_metas, feature_maps)
human_poses = self.pose_regressor(
None,
img_metas,
return_loss=False,
feature_maps=feature_maps,
human_candidates=human_candidates)
result = {}
result['pose_3d'] = human_poses.cpu().numpy()
result['human_detection_3d'] = human_candidates.cpu().numpy()
result['sample_id'] = [img_meta['sample_id'] for img_meta in img_metas]
return result
def show_result(self, **kwargs):
"""Visualize the results."""
raise NotImplementedError
def forward_dummy(self, img, input_heatmaps=None, num_candidates=5):
"""Used for computing network FLOPs."""
if self.backbone is None:
assert input_heatmaps is not None
feature_maps = []
for input_heatmap in input_heatmaps:
feature_maps.append(input_heatmap[0])
else:
feature_maps = []
assert isinstance(img, list)
for img_ in img:
feature_maps.append(self.backbone.forward_dummy(img_)[0])
_ = self.human_detector.forward_dummy(feature_maps)
_ = self.pose_regressor.forward_dummy(feature_maps, num_candidates)
@POSENETS.register_module()
class VoxelSinglePose(BasePose):
"""VoxelPose Please refer to the `paper <https://arxiv.org/abs/2004.06239>`
for details.
Args:
image_size (list): input size of the 2D model.
heatmap_size (list): output size of the 2D model.
sub_space_size (list): Size of the cuboid human proposal.
sub_cube_size (list): Size of the input volume to the pose net.
pose_net (ConfigDict): Dictionary to construct the pose net.
pose_head (ConfigDict): Dictionary to construct the pose head.
train_cfg (ConfigDict): Config for training. Default: None.
test_cfg (ConfigDict): Config for testing. Default: None.
"""
def __init__(
self,
image_size,
heatmap_size,
sub_space_size,
sub_cube_size,
num_joints,
pose_net,
pose_head,
train_cfg=None,
test_cfg=None,
):
super(VoxelSinglePose, self).__init__()
self.project_layer = ProjectLayer(image_size, heatmap_size)
self.pose_net = builder.build_backbone(pose_net)
self.pose_head = builder.build_head(pose_head)
self.sub_space_size = sub_space_size
self.sub_cube_size = sub_cube_size
self.num_joints = num_joints
self.train_cfg = train_cfg
self.test_cfg = test_cfg
def forward(self,
img,
img_metas,
return_loss=True,
feature_maps=None,
human_candidates=None,
**kwargs):
"""
Note:
batch_size: N
num_keypoints: K
num_img_channel: C
img_width: imgW
img_height: imgH
feature_maps width: W
feature_maps height: H
volume_length: cubeL
volume_width: cubeW
volume_height: cubeH
Args:
img (list(torch.Tensor[NxCximgHximgW])):
Multi-camera input images to the 2D model.
feature_maps (list(torch.Tensor[NxCxHxW])):
Multi-camera input feature_maps.
img_metas (list(dict)):
Information about image, 3D groundtruth and camera parameters.
human_candidates (torch.Tensor[NxPx5]):
Human candidates.
return_loss: Option to `return loss`. `return loss=True`
for training, `return loss=False` for validation & test.
"""
if return_loss:
return self.forward_train(img, img_metas, feature_maps,
human_candidates)
else:
return self.forward_test(img, img_metas, feature_maps,
human_candidates)
def forward_train(self,
img,
img_metas,
feature_maps=None,
human_candidates=None,
return_preds=False,
**kwargs):
"""Defines the computation performed at training.
Note:
batch_size: N
num_keypoints: K
num_img_channel: C
img_width: imgW
img_height: imgH
feature_maps width: W
feature_maps height: H
volume_length: cubeL
volume_width: cubeW
volume_height: cubeH
Args:
img (list(torch.Tensor[NxCximgHximgW])):
Multi-camera input images to the 2D model.
feature_maps (list(torch.Tensor[NxCxHxW])):
Multi-camera input feature_maps.
img_metas (list(dict)):
Information about image, 3D groundtruth and camera parameters.
human_candidates (torch.Tensor[NxPx5]):
Human candidates.
return_preds (bool): Whether to return prediction results
Returns:
dict: losses.
"""
batch_size, num_candidates, _ = human_candidates.shape
pred = human_candidates.new_zeros(batch_size, num_candidates,
self.num_joints, 5)
pred[:, :, :, 3:] = human_candidates[:, :, None, 3:]
device = feature_maps[0].device
gt_3d = torch.stack([
torch.tensor(img_meta['joints_3d'], device=device)
for img_meta in img_metas
])
gt_3d_vis = torch.stack([
torch.tensor(img_meta['joints_3d_visible'], device=device)
for img_meta in img_metas
])
valid_preds = []
valid_targets = []
valid_weights = []
for n in range(num_candidates):
index = pred[:, n, 0, 3] >= 0
num_valid = index.sum()
if num_valid > 0:
pose_input_cube, coordinates \
= self.project_layer(feature_maps,
img_metas,
self.sub_space_size,
human_candidates[:, n, :3],
self.sub_cube_size)
pose_heatmaps_3d = self.pose_net(pose_input_cube)
pose_3d = self.pose_head(pose_heatmaps_3d[index],
coordinates[index])
pred[index, n, :, 0:3] = pose_3d.detach()
valid_targets.append(gt_3d[index, pred[index, n, 0, 3].long()])
valid_weights.append(gt_3d_vis[index, pred[index, n, 0,
3].long(), :,
0:1].float())
valid_preds.append(pose_3d)
losses = dict()
if len(valid_preds) > 0:
valid_targets = torch.cat(valid_targets, dim=0)
valid_weights = torch.cat(valid_weights, dim=0)
valid_preds = torch.cat(valid_preds, dim=0)
losses.update(
self.pose_head.get_loss(valid_preds, valid_targets,
valid_weights))
else:
pose_input_cube = feature_maps[0].new_zeros(
batch_size, self.num_joints, *self.sub_cube_size)
coordinates = feature_maps[0].new_zeros(batch_size,
*self.sub_cube_size,
3).view(batch_size, -1, 3)
pseudo_targets = feature_maps[0].new_zeros(batch_size,
self.num_joints, 3)
pseudo_weights = feature_maps[0].new_zeros(batch_size,
self.num_joints, 1)
pose_heatmaps_3d = self.pose_net(pose_input_cube)
pose_3d = self.pose_head(pose_heatmaps_3d, coordinates)
losses.update(
self.pose_head.get_loss(pose_3d, pseudo_targets,
pseudo_weights))
if return_preds:
return pred, losses
else:
return losses
def forward_test(self,
img,
img_metas,
feature_maps=None,
human_candidates=None,
**kwargs):
"""Defines the computation performed at training.
Note:
batch_size: N
num_keypoints: K
num_img_channel: C
img_width: imgW
img_height: imgH
feature_maps width: W
feature_maps height: H
volume_length: cubeL
volume_width: cubeW
volume_height: cubeH
Args:
img (list(torch.Tensor[NxCximgHximgW])):
Multi-camera input images to the 2D model.
feature_maps (list(torch.Tensor[NxCxHxW])):
Multi-camera input feature_maps.
img_metas (list(dict)):
Information about image, 3D groundtruth and camera parameters.
human_candidates (torch.Tensor[NxPx5]):
Human candidates.
Returns:
dict: predicted poses, human centers and sample_id
"""
batch_size, num_candidates, _ = human_candidates.shape
pred = human_candidates.new_zeros(batch_size, num_candidates,
self.num_joints, 5)
pred[:, :, :, 3:] = human_candidates[:, :, None, 3:]
for n in range(num_candidates):
index = pred[:, n, 0, 3] >= 0
num_valid = index.sum()
if num_valid > 0:
pose_input_cube, coordinates \
= self.project_layer(feature_maps,
img_metas,
self.sub_space_size,
human_candidates[:, n, :3],
self.sub_cube_size)
pose_heatmaps_3d = self.pose_net(pose_input_cube)
pose_3d = self.pose_head(pose_heatmaps_3d[index],
coordinates[index])
pred[index, n, :, 0:3] = pose_3d.detach()
return pred
def show_result(self, **kwargs):
"""Visualize the results."""
raise NotImplementedError
def forward_dummy(self, feature_maps, num_candidates=5):
"""Used for computing network FLOPs."""
batch_size, num_channels = feature_maps[0].shape
pose_input_cube = feature_maps[0].new_zeros(batch_size, num_channels,
*self.sub_cube_size)
for n in range(num_candidates):
_ = self.pose_net(pose_input_cube)
@POSENETS.register_module()
class VoxelCenterDetector(BasePose):
"""Detect human center by 3D CNN on voxels.
Please refer to the
`paper <https://arxiv.org/abs/2004.06239>` for details.
Args:
image_size (list): input size of the 2D model.
heatmap_size (list): output size of the 2D model.
space_size (list): Size of the 3D space.
cube_size (list): Size of the input volume to the 3D CNN.
space_center (list): Coordinate of the center of the 3D space.
center_net (ConfigDict): Dictionary to construct the center net.
center_head (ConfigDict): Dictionary to construct the center head.
train_cfg (ConfigDict): Config for training. Default: None.
test_cfg (ConfigDict): Config for testing. Default: None.
"""
def __init__(
self,
image_size,
heatmap_size,
space_size,
cube_size,
space_center,
center_net,
center_head,
train_cfg=None,
test_cfg=None,
):
super(VoxelCenterDetector, self).__init__()
self.project_layer = ProjectLayer(image_size, heatmap_size)
self.center_net = builder.build_backbone(center_net)
self.center_head = builder.build_head(center_head)
self.space_size = space_size
self.cube_size = cube_size
self.space_center = space_center
self.train_cfg = train_cfg
self.test_cfg = test_cfg
def assign2gt(self, center_candidates, gt_centers, gt_num_persons):
""""Assign gt id to each valid human center candidate."""
det_centers = center_candidates[..., :3]
batch_size = center_candidates.shape[0]
cand_num = center_candidates.shape[1]
cand2gt = torch.zeros(batch_size, cand_num)
for i in range(batch_size):
cand = det_centers[i].view(cand_num, 1, -1)
gt = gt_centers[None, i, :gt_num_persons[i]]
dist = torch.sqrt(torch.sum((cand - gt)**2, dim=-1))
min_dist, min_gt = torch.min(dist, dim=-1)
cand2gt[i] = min_gt
cand2gt[i][min_dist > self.train_cfg['dist_threshold']] = -1.0
center_candidates[:, :, 3] = cand2gt
return center_candidates
def forward(self,
img,
img_metas,
return_loss=True,
feature_maps=None,
targets_3d=None):
"""
Note:
batch_size: N
num_keypoints: K
num_img_channel: C
img_width: imgW
img_height: imgH
heatmaps width: W
heatmaps height: H
Args:
img (list(torch.Tensor[NxCximgHximgW])):
Multi-camera input images to the 2D model.
img_metas (list(dict)):
Information about image, 3D groundtruth and camera parameters.
return_loss: Option to `return loss`. `return loss=True`
for training, `return loss=False` for validation & test.
targets_3d (torch.Tensor[NxcubeLxcubeWxcubeH]):
Ground-truth 3D heatmap of human centers.
feature_maps (list(torch.Tensor[NxKxHxW])):
Multi-camera feature_maps.
Returns:
dict: if 'return_loss' is true, then return losses.
Otherwise, return predicted poses
"""
if return_loss:
return self.forward_train(img, img_metas, feature_maps, targets_3d)
else:
return self.forward_test(img, img_metas, feature_maps)
def forward_train(self,
img,
img_metas,
feature_maps=None,
targets_3d=None,
return_preds=False):
"""
Note:
batch_size: N
num_keypoints: K
num_img_channel: C
img_width: imgW
img_height: imgH
heatmaps width: W
heatmaps height: H
Args:
img (list(torch.Tensor[NxCximgHximgW])):
Multi-camera input images to the 2D model.
img_metas (list(dict)):
Information about image, 3D groundtruth and camera parameters.
targets_3d (torch.Tensor[NxcubeLxcubeWxcubeH]):
Ground-truth 3D heatmap of human centers.
feature_maps (list(torch.Tensor[NxKxHxW])):
Multi-camera feature_maps.
return_preds (bool): Whether to return prediction results
Returns:
dict: if 'return_pred' is true, then return losses
and human centers. Otherwise, return losses only
"""
initial_cubes, _ = self.project_layer(feature_maps, img_metas,
self.space_size,
[self.space_center],
self.cube_size)
center_heatmaps_3d = self.center_net(initial_cubes)
center_heatmaps_3d = center_heatmaps_3d.squeeze(1)
center_candidates = self.center_head(center_heatmaps_3d)
device = center_candidates.device
gt_centers = torch.stack([
torch.tensor(img_meta['roots_3d'], device=device)
for img_meta in img_metas
])
gt_num_persons = torch.stack([
torch.tensor(img_meta['num_persons'], device=device)
for img_meta in img_metas
])
center_candidates = self.assign2gt(center_candidates, gt_centers,
gt_num_persons)
losses = dict()
losses.update(
self.center_head.get_loss(center_heatmaps_3d, targets_3d))
if return_preds:
return center_candidates, losses
else:
return losses
def forward_test(self, img, img_metas, feature_maps=None):
"""
Note:
batch_size: N
num_keypoints: K
num_img_channel: C
img_width: imgW
img_height: imgH
heatmaps width: W
heatmaps height: H
Args:
img (list(torch.Tensor[NxCximgHximgW])):
Multi-camera input images to the 2D model.
img_metas (list(dict)):
Information about image, 3D groundtruth and camera parameters.
feature_maps (list(torch.Tensor[NxKxHxW])):
Multi-camera feature_maps.
Returns:
human centers
"""
initial_cubes, _ = self.project_layer(feature_maps, img_metas,
self.space_size,
[self.space_center],
self.cube_size)
center_heatmaps_3d = self.center_net(initial_cubes)
center_heatmaps_3d = center_heatmaps_3d.squeeze(1)
center_candidates = self.center_head(center_heatmaps_3d)
center_candidates[..., 3] = \
(center_candidates[..., 4] >
self.test_cfg['center_threshold']).float() - 1.0
return center_candidates
def show_result(self, **kwargs):
"""Visualize the results."""
raise NotImplementedError
def forward_dummy(self, feature_maps):
"""Used for computing network FLOPs."""
batch_size, num_channels, _, _ = feature_maps[0].shape
initial_cubes = feature_maps[0].new_zeros(batch_size, num_channels,
*self.cube_size)
_ = self.center_net(initial_cubes)
@@ -0,0 +1,392 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
import mmcv
import numpy as np
from mmcv.utils.misc import deprecated_api_warning
from mmpose.core import imshow_bboxes, imshow_keypoints, imshow_keypoints_3d
from .. import builder
from ..builder import POSENETS
from .base import BasePose
try:
from mmcv.runner import auto_fp16
except ImportError:
warnings.warn('auto_fp16 from mmpose will be deprecated from v0.15.0'
'Please install mmcv>=1.1.4')
from mmpose.core import auto_fp16
@POSENETS.register_module()
class PoseLifter(BasePose):
"""Pose lifter that lifts 2D pose to 3D pose.
The basic model is a pose model that predicts root-relative pose. If
traj_head is not None, a trajectory model that predicts absolute root joint
position is also built.
Args:
backbone (dict): Config for the backbone of pose model.
neck (dict|None): Config for the neck of pose model.
keypoint_head (dict|None): Config for the head of pose model.
traj_backbone (dict|None): Config for the backbone of trajectory model.
If traj_backbone is None and traj_head is not None, trajectory
model will share backbone with pose model.
traj_neck (dict|None): Config for the neck of trajectory model.
traj_head (dict|None): Config for the head of trajectory model.
loss_semi (dict|None): Config for semi-supervision loss.
train_cfg (dict|None): Config for keypoint head during training.
test_cfg (dict|None): Config for keypoint head during testing.
pretrained (str|None): Path to pretrained weights.
"""
def __init__(self,
backbone,
neck=None,
keypoint_head=None,
traj_backbone=None,
traj_neck=None,
traj_head=None,
loss_semi=None,
train_cfg=None,
test_cfg=None,
pretrained=None):
super().__init__()
self.fp16_enabled = False
self.train_cfg = train_cfg
self.test_cfg = test_cfg
# pose model
self.backbone = builder.build_backbone(backbone)
if neck is not None:
self.neck = builder.build_neck(neck)
if keypoint_head is not None:
keypoint_head['train_cfg'] = train_cfg
keypoint_head['test_cfg'] = test_cfg
self.keypoint_head = builder.build_head(keypoint_head)
# trajectory model
if traj_head is not None:
self.traj_head = builder.build_head(traj_head)
if traj_backbone is not None:
self.traj_backbone = builder.build_backbone(traj_backbone)
else:
self.traj_backbone = self.backbone
if traj_neck is not None:
self.traj_neck = builder.build_neck(traj_neck)
# semi-supervised learning
self.semi = loss_semi is not None
if self.semi:
assert keypoint_head is not None and traj_head is not None
self.loss_semi = builder.build_loss(loss_semi)
self.init_weights(pretrained=pretrained)
@property
def with_neck(self):
"""Check if has keypoint_neck."""
return hasattr(self, 'neck')
@property
def with_keypoint(self):
"""Check if has keypoint_head."""
return hasattr(self, 'keypoint_head')
@property
def with_traj_backbone(self):
"""Check if has trajectory_backbone."""
return hasattr(self, 'traj_backbone')
@property
def with_traj_neck(self):
"""Check if has trajectory_neck."""
return hasattr(self, 'traj_neck')
@property
def with_traj(self):
"""Check if has trajectory_head."""
return hasattr(self, 'traj_head')
@property
def causal(self):
if hasattr(self.backbone, 'causal'):
return self.backbone.causal
else:
raise AttributeError('A PoseLifter\'s backbone should have '
'the bool attribute "causal" to indicate if'
'it performs causal inference.')
def init_weights(self, pretrained=None):
"""Weight initialization for model."""
self.backbone.init_weights(pretrained)
if self.with_neck:
self.neck.init_weights()
if self.with_keypoint:
self.keypoint_head.init_weights()
if self.with_traj_backbone:
self.traj_backbone.init_weights(pretrained)
if self.with_traj_neck:
self.traj_neck.init_weights()
if self.with_traj:
self.traj_head.init_weights()
@auto_fp16(apply_to=('input', ))
def forward(self,
input,
target=None,
target_weight=None,
metas=None,
return_loss=True,
**kwargs):
"""Calls either forward_train or forward_test depending on whether
return_loss=True.
Note:
- batch_size: N
- num_input_keypoints: Ki
- input_keypoint_dim: Ci
- input_sequence_len: Ti
- num_output_keypoints: Ko
- output_keypoint_dim: Co
- input_sequence_len: To
Args:
input (torch.Tensor[NxKixCixTi]): Input keypoint coordinates.
target (torch.Tensor[NxKoxCoxTo]): Output keypoint coordinates.
Defaults to None.
target_weight (torch.Tensor[NxKox1]): Weights across different
joint types. Defaults to None.
metas (list(dict)): Information about data augmentation
return_loss (bool): Option to `return loss`. `return loss=True`
for training, `return loss=False` for validation & test.
Returns:
dict|Tensor: If `reutrn_loss` is true, return losses. \
Otherwise return predicted poses.
"""
if return_loss:
return self.forward_train(input, target, target_weight, metas,
**kwargs)
else:
return self.forward_test(input, metas, **kwargs)
def forward_train(self, input, target, target_weight, metas, **kwargs):
"""Defines the computation performed at every call when training."""
assert input.size(0) == len(metas)
# supervised learning
# pose model
features = self.backbone(input)
if self.with_neck:
features = self.neck(features)
if self.with_keypoint:
output = self.keypoint_head(features)
losses = dict()
if self.with_keypoint:
keypoint_losses = self.keypoint_head.get_loss(
output, target, target_weight)
keypoint_accuracy = self.keypoint_head.get_accuracy(
output, target, target_weight, metas)
losses.update(keypoint_losses)
losses.update(keypoint_accuracy)
# trajectory model
if self.with_traj:
traj_features = self.traj_backbone(input)
if self.with_traj_neck:
traj_features = self.traj_neck(traj_features)
traj_output = self.traj_head(traj_features)
traj_losses = self.traj_head.get_loss(traj_output,
kwargs['traj_target'], None)
losses.update(traj_losses)
# semi-supervised learning
if self.semi:
ul_input = kwargs['unlabeled_input']
ul_features = self.backbone(ul_input)
if self.with_neck:
ul_features = self.neck(ul_features)
ul_output = self.keypoint_head(ul_features)
ul_traj_features = self.traj_backbone(ul_input)
if self.with_traj_neck:
ul_traj_features = self.traj_neck(ul_traj_features)
ul_traj_output = self.traj_head(ul_traj_features)
output_semi = dict(
labeled_pose=output,
unlabeled_pose=ul_output,
unlabeled_traj=ul_traj_output)
target_semi = dict(
unlabeled_target_2d=kwargs['unlabeled_target_2d'],
intrinsics=kwargs['intrinsics'])
semi_losses = self.loss_semi(output_semi, target_semi)
losses.update(semi_losses)
return losses
def forward_test(self, input, metas, **kwargs):
"""Defines the computation performed at every call when training."""
assert input.size(0) == len(metas)
results = {}
features = self.backbone(input)
if self.with_neck:
features = self.neck(features)
if self.with_keypoint:
output = self.keypoint_head.inference_model(features)
keypoint_result = self.keypoint_head.decode(metas, output)
results.update(keypoint_result)
if self.with_traj:
traj_features = self.traj_backbone(input)
if self.with_traj_neck:
traj_features = self.traj_neck(traj_features)
traj_output = self.traj_head.inference_model(traj_features)
results['traj_preds'] = traj_output
return results
def forward_dummy(self, input):
"""Used for computing network FLOPs. See ``tools/get_flops.py``.
Args:
input (torch.Tensor): Input pose
Returns:
Tensor: Model output
"""
output = self.backbone(input)
if self.with_neck:
output = self.neck(output)
if self.with_keypoint:
output = self.keypoint_head(output)
if self.with_traj:
traj_features = self.traj_backbone(input)
if self.with_neck:
traj_features = self.traj_neck(traj_features)
traj_output = self.traj_head(traj_features)
output = output + traj_output
return output
@deprecated_api_warning({'pose_limb_color': 'pose_link_color'},
cls_name='PoseLifter')
def show_result(self,
result,
img=None,
skeleton=None,
pose_kpt_color=None,
pose_link_color=None,
radius=8,
thickness=2,
vis_height=400,
num_instances=-1,
win_name='',
show=False,
wait_time=0,
out_file=None):
"""Visualize 3D pose estimation results.
Args:
result (list[dict]): The pose estimation results containing:
- "keypoints_3d" ([K,4]): 3D keypoints
- "keypoints" ([K,3] or [T,K,3]): Optional for visualizing
2D inputs. If a sequence is given, only the last frame
will be used for visualization
- "bbox" ([4,] or [T,4]): Optional for visualizing 2D inputs
- "title" (str): title for the subplot
img (str or Tensor): Optional. The image to visualize 2D inputs on.
skeleton (list of [idx_i,idx_j]): Skeleton described by a list of
links, each is a pair of joint indices.
pose_kpt_color (np.array[Nx3]`): Color of N keypoints.
If None, do not draw keypoints.
pose_link_color (np.array[Mx3]): Color of M links.
If None, do not draw links.
radius (int): Radius of circles.
thickness (int): Thickness of lines.
vis_height (int): The image height of the visualization. The width
will be N*vis_height depending on the number of visualized
items.
win_name (str): The window name.
wait_time (int): Value of waitKey param.
Default: 0.
out_file (str or None): The filename to write the image.
Default: None.
Returns:
Tensor: Visualized img, only if not `show` or `out_file`.
"""
if num_instances < 0:
assert len(result) > 0
result = sorted(result, key=lambda x: x.get('track_id', 1e4))
# draw image and input 2d poses
if img is not None:
img = mmcv.imread(img)
bbox_result = []
pose_input_2d = []
for res in result:
if 'bbox' in res:
bbox = np.array(res['bbox'])
if bbox.ndim != 1:
assert bbox.ndim == 2
bbox = bbox[-1] # Get bbox from the last frame
bbox_result.append(bbox)
if 'keypoints' in res:
kpts = np.array(res['keypoints'])
if kpts.ndim != 2:
assert kpts.ndim == 3
kpts = kpts[-1] # Get 2D keypoints from the last frame
pose_input_2d.append(kpts)
if len(bbox_result) > 0:
bboxes = np.vstack(bbox_result)
imshow_bboxes(
img,
bboxes,
colors='green',
thickness=thickness,
show=False)
if len(pose_input_2d) > 0:
imshow_keypoints(
img,
pose_input_2d,
skeleton,
kpt_score_thr=0.3,
pose_kpt_color=pose_kpt_color,
pose_link_color=pose_link_color,
radius=radius,
thickness=thickness)
img = mmcv.imrescale(img, scale=vis_height / img.shape[0])
img_vis = imshow_keypoints_3d(
result,
img,
skeleton,
pose_kpt_color,
pose_link_color,
vis_height,
num_instances=num_instances)
if show:
mmcv.visualization.imshow(img_vis, win_name, wait_time)
if out_file is not None:
mmcv.imwrite(img_vis, out_file)
return img_vis
@@ -0,0 +1,244 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
import numpy as np
import torch
from ..builder import POSENETS
from .top_down import TopDown
try:
from mmcv.runner import auto_fp16
except ImportError:
warnings.warn('auto_fp16 from mmpose will be deprecated from v0.15.0'
'Please install mmcv>=1.1.4')
from mmpose.core import auto_fp16
@POSENETS.register_module()
class PoseWarper(TopDown):
"""Top-down pose detectors for multi-frame settings for video inputs.
`"Learning temporal pose estimation from sparsely-labeled videos"
<https://arxiv.org/abs/1906.04016>`_.
A child class of TopDown detector. The main difference between PoseWarper
and TopDown lies in that the former takes a list of tensors as input image
while the latter takes a single tensor as input image in forward method.
Args:
backbone (dict): Backbone modules to extract features.
neck (dict): intermediate modules to transform features.
keypoint_head (dict): Keypoint head to process feature.
train_cfg (dict): Config for training. Default: None.
test_cfg (dict): Config for testing. Default: None.
pretrained (str): Path to the pretrained models.
loss_pose (None): Deprecated arguments. Please use
`loss_keypoint` for heads instead.
concat_tensors (bool): Whether to concat the tensors on the batch dim,
which can speed up, Default: True
"""
def __init__(self,
backbone,
neck=None,
keypoint_head=None,
train_cfg=None,
test_cfg=None,
pretrained=None,
loss_pose=None,
concat_tensors=True):
super().__init__(
backbone=backbone,
neck=neck,
keypoint_head=keypoint_head,
train_cfg=train_cfg,
test_cfg=test_cfg,
pretrained=pretrained,
loss_pose=loss_pose)
self.concat_tensors = concat_tensors
@auto_fp16(apply_to=('img', ))
def forward(self,
img,
target=None,
target_weight=None,
img_metas=None,
return_loss=True,
return_heatmap=False,
**kwargs):
"""Calls either forward_train or forward_test depending on whether
return_loss=True. Note this setting will change the expected inputs.
When `return_loss=True`, img and img_meta are single-nested (i.e.
Tensor and List[dict]), and when `resturn_loss=False`, img and img_meta
should be double nested (i.e. List[Tensor], List[List[dict]]), with
the outer list indicating test time augmentations.
Note:
- number of frames: F
- batch_size: N
- num_keypoints: K
- num_img_channel: C (Default: 3)
- img height: imgH
- img width: imgW
- heatmaps height: H
- heatmaps weight: W
Args:
imgs (list[F,torch.Tensor[N,C,imgH,imgW]]): multiple input frames
target (torch.Tensor[N,K,H,W]): Target heatmaps for one frame.
target_weight (torch.Tensor[N,K,1]): Weights across
different joint types.
img_metas (list(dict)): Information about data augmentation
By default this includes:
- "image_file: paths to multiple video frames
- "center": center of the bbox
- "scale": scale of the bbox
- "rotation": rotation of the bbox
- "bbox_score": score of bbox
return_loss (bool): Option to `return loss`. `return loss=True`
for training, `return loss=False` for validation & test.
return_heatmap (bool) : Option to return heatmap.
Returns:
dict|tuple: if `return loss` is true, then return losses. \
Otherwise, return predicted poses, boxes, image paths \
and heatmaps.
"""
if return_loss:
return self.forward_train(img, target, target_weight, img_metas,
**kwargs)
return self.forward_test(
img, img_metas, return_heatmap=return_heatmap, **kwargs)
def forward_train(self, imgs, target, target_weight, img_metas, **kwargs):
"""Defines the computation performed at every call when training."""
# imgs (list[Fxtorch.Tensor[NxCximgHximgW]]): multiple input frames
assert imgs[0].size(0) == len(img_metas)
num_frames = len(imgs)
frame_weight = img_metas[0]['frame_weight']
assert num_frames == len(frame_weight), f'The number of frames ' \
f'({num_frames}) and the length of weights for each frame ' \
f'({len(frame_weight)}) must match'
if self.concat_tensors:
features = [self.backbone(torch.cat(imgs, 0))]
else:
features = [self.backbone(img) for img in imgs]
if self.with_neck:
features = self.neck(features, frame_weight=frame_weight)
if self.with_keypoint:
output = self.keypoint_head(features)
# if return loss
losses = dict()
if self.with_keypoint:
keypoint_losses = self.keypoint_head.get_loss(
output, target, target_weight)
losses.update(keypoint_losses)
keypoint_accuracy = self.keypoint_head.get_accuracy(
output, target, target_weight)
losses.update(keypoint_accuracy)
return losses
def forward_test(self, imgs, img_metas, return_heatmap=False, **kwargs):
"""Defines the computation performed at every call when testing."""
# imgs (list[Fxtorch.Tensor[NxCximgHximgW]]): multiple input frames
assert imgs[0].size(0) == len(img_metas)
num_frames = len(imgs)
frame_weight = img_metas[0]['frame_weight']
assert num_frames == len(frame_weight), f'The number of frames ' \
f'({num_frames}) and the length of weights for each frame ' \
f'({len(frame_weight)}) must match'
batch_size, _, img_height, img_width = imgs[0].shape
if batch_size > 1:
assert 'bbox_id' in img_metas[0]
result = {}
if self.concat_tensors:
features = [self.backbone(torch.cat(imgs, 0))]
else:
features = [self.backbone(img) for img in imgs]
if self.with_neck:
features = self.neck(features, frame_weight=frame_weight)
if self.with_keypoint:
output_heatmap = self.keypoint_head.inference_model(
features, flip_pairs=None)
if self.test_cfg.get('flip_test', True):
imgs_flipped = [img.flip(3) for img in imgs]
if self.concat_tensors:
features_flipped = [self.backbone(torch.cat(imgs_flipped, 0))]
else:
features_flipped = [
self.backbone(img_flipped) for img_flipped in imgs_flipped
]
if self.with_neck:
features_flipped = self.neck(
features_flipped, frame_weight=frame_weight)
if self.with_keypoint:
output_flipped_heatmap = self.keypoint_head.inference_model(
features_flipped, img_metas[0]['flip_pairs'])
output_heatmap = (output_heatmap +
output_flipped_heatmap) * 0.5
if self.with_keypoint:
keypoint_result = self.keypoint_head.decode(
img_metas, output_heatmap, img_size=[img_width, img_height])
result.update(keypoint_result)
if not return_heatmap:
output_heatmap = None
result['output_heatmap'] = output_heatmap
return result
def forward_dummy(self, img):
"""Used for computing network FLOPs.
See ``tools/get_flops.py``.
Args:
img (torch.Tensor[N,C,imgH,imgW], or list|tuple of tensors):
multiple input frames, N >= 2.
Returns:
Tensor: Output heatmaps.
"""
# concat tensors if they are in a list
if isinstance(img, (list, tuple)):
img = torch.cat(img, 0)
batch_size = img.size(0)
assert batch_size > 1, 'Input batch size to PoseWarper ' \
'should be larger than 1.'
if batch_size == 2:
warnings.warn('Current batch size: 2, for pytorch2onnx and '
'getting flops both.')
else:
warnings.warn(
f'Current batch size: {batch_size}, for getting flops only.')
frame_weight = np.random.uniform(0, 1, batch_size)
output = [self.backbone(img)]
if self.with_neck:
output = self.neck(output, frame_weight=frame_weight)
if self.with_keypoint:
output = self.keypoint_head(output)
return output
@@ -0,0 +1,307 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
import mmcv
import numpy as np
from mmcv.image import imwrite
from mmcv.utils.misc import deprecated_api_warning
from mmcv.visualization.image import imshow
from mmpose.core import imshow_bboxes, imshow_keypoints
from .. import builder
from ..builder import POSENETS
from .base import BasePose
try:
from mmcv.runner import auto_fp16
except ImportError:
warnings.warn('auto_fp16 from mmpose will be deprecated from v0.15.0'
'Please install mmcv>=1.1.4')
from mmpose.core import auto_fp16
@POSENETS.register_module()
class TopDown(BasePose):
"""Top-down pose detectors.
Args:
backbone (dict): Backbone modules to extract feature.
keypoint_head (dict): Keypoint head to process feature.
train_cfg (dict): Config for training. Default: None.
test_cfg (dict): Config for testing. Default: None.
pretrained (str): Path to the pretrained models.
loss_pose (None): Deprecated arguments. Please use
`loss_keypoint` for heads instead.
"""
def __init__(self,
backbone,
neck=None,
keypoint_head=None,
train_cfg=None,
test_cfg=None,
pretrained=None,
loss_pose=None):
super().__init__()
self.fp16_enabled = False
self.backbone = builder.build_backbone(backbone)
self.train_cfg = train_cfg
self.test_cfg = test_cfg
if neck is not None:
self.neck = builder.build_neck(neck)
if keypoint_head is not None:
keypoint_head['train_cfg'] = train_cfg
keypoint_head['test_cfg'] = test_cfg
if 'loss_keypoint' not in keypoint_head and loss_pose is not None:
warnings.warn(
'`loss_pose` for TopDown is deprecated, '
'use `loss_keypoint` for heads instead. See '
'https://github.com/open-mmlab/mmpose/pull/382'
' for more information.', DeprecationWarning)
keypoint_head['loss_keypoint'] = loss_pose
self.keypoint_head = builder.build_head(keypoint_head)
self.init_weights(pretrained=pretrained)
@property
def with_neck(self):
"""Check if has neck."""
return hasattr(self, 'neck')
@property
def with_keypoint(self):
"""Check if has keypoint_head."""
return hasattr(self, 'keypoint_head')
def init_weights(self, pretrained=None):
"""Weight initialization for model."""
self.backbone.init_weights(pretrained)
if self.with_neck:
self.neck.init_weights()
if self.with_keypoint:
self.keypoint_head.init_weights()
@auto_fp16(apply_to=('img', ))
def forward(self,
img,
target=None,
target_weight=None,
img_metas=None,
return_loss=True,
return_heatmap=False,
**kwargs):
"""Calls either forward_train or forward_test depending on whether
return_loss=True. Note this setting will change the expected inputs.
When `return_loss=True`, img and img_meta are single-nested (i.e.
Tensor and List[dict]), and when `resturn_loss=False`, img and img_meta
should be double nested (i.e. List[Tensor], List[List[dict]]), with
the outer list indicating test time augmentations.
Note:
- batch_size: N
- num_keypoints: K
- num_img_channel: C (Default: 3)
- img height: imgH
- img width: imgW
- heatmaps height: H
- heatmaps weight: W
Args:
img (torch.Tensor[NxCximgHximgW]): Input images.
target (torch.Tensor[NxKxHxW]): Target heatmaps.
target_weight (torch.Tensor[NxKx1]): Weights across
different joint types.
img_metas (list(dict)): Information about data augmentation
By default this includes:
- "image_file: path to the image file
- "center": center of the bbox
- "scale": scale of the bbox
- "rotation": rotation of the bbox
- "bbox_score": score of bbox
return_loss (bool): Option to `return loss`. `return loss=True`
for training, `return loss=False` for validation & test.
return_heatmap (bool) : Option to return heatmap.
Returns:
dict|tuple: if `return loss` is true, then return losses. \
Otherwise, return predicted poses, boxes, image paths \
and heatmaps.
"""
if return_loss:
return self.forward_train(img, target, target_weight, img_metas,
**kwargs)
return self.forward_test(
img, img_metas, return_heatmap=return_heatmap, **kwargs)
def forward_train(self, img, target, target_weight, img_metas, **kwargs):
"""Defines the computation performed at every call when training."""
output = self.backbone(img)
if self.with_neck:
output = self.neck(output)
if self.with_keypoint:
output = self.keypoint_head(output)
# if return loss
losses = dict()
if self.with_keypoint:
keypoint_losses = self.keypoint_head.get_loss(
output, target, target_weight)
losses.update(keypoint_losses)
keypoint_accuracy = self.keypoint_head.get_accuracy(
output, target, target_weight)
losses.update(keypoint_accuracy)
return losses
def forward_test(self, img, img_metas, return_heatmap=False, **kwargs):
"""Defines the computation performed at every call when testing."""
assert img.size(0) == len(img_metas)
batch_size, _, img_height, img_width = img.shape
if batch_size > 1:
assert 'bbox_id' in img_metas[0]
result = {}
features = self.backbone(img)
if self.with_neck:
features = self.neck(features)
if self.with_keypoint:
output_heatmap = self.keypoint_head.inference_model(
features, flip_pairs=None)
if self.test_cfg.get('flip_test', True):
img_flipped = img.flip(3)
features_flipped = self.backbone(img_flipped)
if self.with_neck:
features_flipped = self.neck(features_flipped)
if self.with_keypoint:
output_flipped_heatmap = self.keypoint_head.inference_model(
features_flipped, img_metas[0]['flip_pairs'])
output_heatmap = (output_heatmap +
output_flipped_heatmap) * 0.5
if self.with_keypoint:
keypoint_result = self.keypoint_head.decode(
img_metas, output_heatmap, img_size=[img_width, img_height])
result.update(keypoint_result)
if not return_heatmap:
output_heatmap = None
result['output_heatmap'] = output_heatmap
return result
def forward_dummy(self, img):
"""Used for computing network FLOPs.
See ``tools/get_flops.py``.
Args:
img (torch.Tensor): Input image.
Returns:
Tensor: Output heatmaps.
"""
output = self.backbone(img)
if self.with_neck:
output = self.neck(output)
if self.with_keypoint:
output = self.keypoint_head(output)
return output
@deprecated_api_warning({'pose_limb_color': 'pose_link_color'},
cls_name='TopDown')
def show_result(self,
img,
result,
skeleton=None,
kpt_score_thr=0.3,
bbox_color='green',
pose_kpt_color=None,
pose_link_color=None,
text_color='white',
radius=4,
thickness=1,
font_scale=0.5,
bbox_thickness=1,
win_name='',
show=False,
show_keypoint_weight=False,
wait_time=0,
out_file=None):
"""Draw `result` over `img`.
Args:
img (str or Tensor): The image to be displayed.
result (list[dict]): The results to draw over `img`
(bbox_result, pose_result).
skeleton (list[list]): The connection of keypoints.
skeleton is 0-based indexing.
kpt_score_thr (float, optional): Minimum score of keypoints
to be shown. Default: 0.3.
bbox_color (str or tuple or :obj:`Color`): Color of bbox lines.
pose_kpt_color (np.array[Nx3]`): Color of N keypoints.
If None, do not draw keypoints.
pose_link_color (np.array[Mx3]): Color of M links.
If None, do not draw links.
text_color (str or tuple or :obj:`Color`): Color of texts.
radius (int): Radius of circles.
thickness (int): Thickness of lines.
font_scale (float): Font scales of texts.
win_name (str): The window name.
show (bool): Whether to show the image. Default: False.
show_keypoint_weight (bool): Whether to change the transparency
using the predicted confidence scores of keypoints.
wait_time (int): Value of waitKey param.
Default: 0.
out_file (str or None): The filename to write the image.
Default: None.
Returns:
Tensor: Visualized img, only if not `show` or `out_file`.
"""
img = mmcv.imread(img)
img = img.copy()
bbox_result = []
bbox_labels = []
pose_result = []
for res in result:
if 'bbox' in res:
bbox_result.append(res['bbox'])
bbox_labels.append(res.get('label', None))
pose_result.append(res['keypoints'])
if bbox_result:
bboxes = np.vstack(bbox_result)
# draw bounding boxes
imshow_bboxes(
img,
bboxes,
labels=bbox_labels,
colors=bbox_color,
text_color=text_color,
thickness=bbox_thickness,
font_scale=font_scale,
show=False)
if pose_result:
imshow_keypoints(img, pose_result, skeleton, kpt_score_thr,
pose_kpt_color, pose_link_color, radius,
thickness)
if show:
imshow(img, win_name, wait_time)
if out_file is not None:
imwrite(img, out_file)
return img
@@ -0,0 +1,351 @@
# Copyright (c) OpenMMLab. All rights reserved.
import warnings
import torch
import torch.nn as nn
import mmcv
import numpy as np
from mmcv.image import imwrite
from mmcv.utils.misc import deprecated_api_warning
from mmcv.visualization.image import imshow
from mmpose.core import imshow_bboxes, imshow_keypoints
from .. import builder
from ..builder import POSENETS
from .base import BasePose
try:
from mmcv.runner import auto_fp16
except ImportError:
warnings.warn('auto_fp16 from mmpose will be deprecated from v0.15.0'
'Please install mmcv>=1.1.4')
from mmpose.core import auto_fp16
@POSENETS.register_module()
class TopDownMoE(BasePose):
"""Top-down pose detectors.
Args:
backbone (dict): Backbone modules to extract feature.
keypoint_head (dict): Keypoint head to process feature.
train_cfg (dict): Config for training. Default: None.
test_cfg (dict): Config for testing. Default: None.
pretrained (str): Path to the pretrained models.
loss_pose (None): Deprecated arguments. Please use
`loss_keypoint` for heads instead.
"""
def __init__(self,
backbone,
neck=None,
keypoint_head=None,
associate_keypoint_head=None,
train_cfg=None,
test_cfg=None,
pretrained=None,
loss_pose=None):
super().__init__()
self.fp16_enabled = False
self.backbone = builder.build_backbone(backbone)
self.train_cfg = train_cfg
self.test_cfg = test_cfg
if neck is not None:
self.neck = builder.build_neck(neck)
if keypoint_head is not None:
keypoint_head['train_cfg'] = train_cfg
keypoint_head['test_cfg'] = test_cfg
if 'loss_keypoint' not in keypoint_head and loss_pose is not None:
warnings.warn(
'`loss_pose` for TopDown is deprecated, '
'use `loss_keypoint` for heads instead. See '
'https://github.com/open-mmlab/mmpose/pull/382'
' for more information.', DeprecationWarning)
keypoint_head['loss_keypoint'] = loss_pose
self.keypoint_head = builder.build_head(keypoint_head)
associate_keypoint_heads = []
keypoint_heads_cnt = 1
if associate_keypoint_head is not None:
if not isinstance(associate_keypoint_head, list):
associate_keypoint_head = [associate_keypoint_head]
for single_keypoint_head in associate_keypoint_head:
single_keypoint_head['train_cfg'] = train_cfg
single_keypoint_head['test_cfg'] = test_cfg
associate_keypoint_heads.append(builder.build_head(single_keypoint_head))
keypoint_heads_cnt += 1
self.associate_keypoint_heads = nn.ModuleList(associate_keypoint_heads)
self.keypoint_heads_cnt = keypoint_heads_cnt
self.init_weights(pretrained=pretrained)
@property
def with_neck(self):
"""Check if has neck."""
return hasattr(self, 'neck')
@property
def with_keypoint(self):
"""Check if has keypoint_head."""
return hasattr(self, 'keypoint_head')
def init_weights(self, pretrained=None):
"""Weight initialization for model."""
self.backbone.init_weights(pretrained)
if self.with_neck:
self.neck.init_weights()
if self.with_keypoint:
self.keypoint_head.init_weights()
for item in self.associate_keypoint_heads:
item.init_weights()
@auto_fp16(apply_to=('img', ))
def forward(self,
img,
target=None,
target_weight=None,
img_metas=None,
return_loss=True,
return_heatmap=False,
**kwargs):
"""Calls either forward_train or forward_test depending on whether
return_loss=True. Note this setting will change the expected inputs.
When `return_loss=True`, img and img_meta are single-nested (i.e.
Tensor and List[dict]), and when `resturn_loss=False`, img and img_meta
should be double nested (i.e. List[Tensor], List[List[dict]]), with
the outer list indicating test time augmentations.
Note:
- batch_size: N
- num_keypoints: K
- num_img_channel: C (Default: 3)
- img height: imgH
- img width: imgW
- heatmaps height: H
- heatmaps weight: W
Args:
img (torch.Tensor[NxCximgHximgW]): Input images.
target (torch.Tensor[NxKxHxW]): Target heatmaps.
target_weight (torch.Tensor[NxKx1]): Weights across
different joint types.
img_metas (list(dict)): Information about data augmentation
By default this includes:
- "image_file: path to the image file
- "center": center of the bbox
- "scale": scale of the bbox
- "rotation": rotation of the bbox
- "bbox_score": score of bbox
return_loss (bool): Option to `return loss`. `return loss=True`
for training, `return loss=False` for validation & test.
return_heatmap (bool) : Option to return heatmap.
Returns:
dict|tuple: if `return loss` is true, then return losses. \
Otherwise, return predicted poses, boxes, image paths \
and heatmaps.
"""
if return_loss:
return self.forward_train(img, target, target_weight, img_metas,
**kwargs)
return self.forward_test(
img, img_metas, return_heatmap=return_heatmap, **kwargs)
def forward_train(self, img, target, target_weight, img_metas, **kwargs):
"""Defines the computation performed at every call when training."""
img_sources = torch.from_numpy(np.array([ele['dataset_idx'] for ele in img_metas])).to(img.device)
output = self.backbone(img, img_sources)
if self.with_neck:
output = self.neck(output)
# if return loss
losses = dict()
main_stream_select = (img_sources == 0)
# if torch.sum(main_stream_select) > 0:
output_select = self.keypoint_head(output)
target_select = target * main_stream_select.view(-1, 1, 1, 1)
target_weight_select = target_weight * main_stream_select.view(-1, 1, 1)
keypoint_losses = self.keypoint_head.get_loss(
output_select, target_select, target_weight_select)
losses['main_stream_loss'] = keypoint_losses['heatmap_loss']
keypoint_accuracy = self.keypoint_head.get_accuracy(
output_select, target_select, target_weight_select)
losses['main_stream_acc'] = keypoint_accuracy['acc_pose']
for idx in range(1, self.keypoint_heads_cnt):
idx_select = (img_sources == idx)
target_select = target * idx_select.view(-1, 1, 1, 1)
target_weight_select = target_weight * idx_select.view(-1, 1, 1)
output_select = self.associate_keypoint_heads[idx - 1](output)
keypoint_losses = self.associate_keypoint_heads[idx - 1].get_loss(
output_select, target_select, target_weight_select)
losses[f'{idx}_loss'] = keypoint_losses['heatmap_loss']
keypoint_accuracy = self.associate_keypoint_heads[idx - 1].get_accuracy(
output_select, target_select, target_weight_select)
losses[f'{idx}_acc'] = keypoint_accuracy['acc_pose']
return losses
def forward_test(self, img, img_metas, return_heatmap=False, **kwargs):
"""Defines the computation performed at every call when testing."""
assert img.size(0) == len(img_metas)
batch_size, _, img_height, img_width = img.shape
if batch_size > 1:
assert 'bbox_id' in img_metas[0]
result = {}
img_sources = torch.from_numpy(np.array([ele['dataset_idx'] for ele in img_metas])).to(img.device)
features = self.backbone(img, img_sources)
if self.with_neck:
features = self.neck(features)
if self.with_keypoint:
output_heatmap = self.keypoint_head.inference_model(
features, flip_pairs=None)
if self.test_cfg.get('flip_test', True):
img_flipped = img.flip(3)
features_flipped = self.backbone(img_flipped, img_sources)
if self.with_neck:
features_flipped = self.neck(features_flipped)
if self.with_keypoint:
output_flipped_heatmap = self.keypoint_head.inference_model(
features_flipped, img_metas[0]['flip_pairs'])
output_heatmap = (output_heatmap +
output_flipped_heatmap) * 0.5
if self.with_keypoint:
keypoint_result = self.keypoint_head.decode(
img_metas, output_heatmap, img_size=[img_width, img_height])
result.update(keypoint_result)
if not return_heatmap:
output_heatmap = None
result['output_heatmap'] = output_heatmap
return result
def forward_dummy(self, img):
"""Used for computing network FLOPs.
See ``tools/get_flops.py``.
Args:
img (torch.Tensor): Input image.
Returns:
Tensor: Output heatmaps.
"""
output = self.backbone(img)
if self.with_neck:
output = self.neck(output)
if self.with_keypoint:
output = self.keypoint_head(output)
return output
@deprecated_api_warning({'pose_limb_color': 'pose_link_color'},
cls_name='TopDown')
def show_result(self,
img,
result,
skeleton=None,
kpt_score_thr=0.3,
bbox_color='green',
pose_kpt_color=None,
pose_link_color=None,
text_color='white',
radius=4,
thickness=1,
font_scale=0.5,
bbox_thickness=1,
win_name='',
show=False,
show_keypoint_weight=False,
wait_time=0,
out_file=None):
"""Draw `result` over `img`.
Args:
img (str or Tensor): The image to be displayed.
result (list[dict]): The results to draw over `img`
(bbox_result, pose_result).
skeleton (list[list]): The connection of keypoints.
skeleton is 0-based indexing.
kpt_score_thr (float, optional): Minimum score of keypoints
to be shown. Default: 0.3.
bbox_color (str or tuple or :obj:`Color`): Color of bbox lines.
pose_kpt_color (np.array[Nx3]`): Color of N keypoints.
If None, do not draw keypoints.
pose_link_color (np.array[Mx3]): Color of M links.
If None, do not draw links.
text_color (str or tuple or :obj:`Color`): Color of texts.
radius (int): Radius of circles.
thickness (int): Thickness of lines.
font_scale (float): Font scales of texts.
win_name (str): The window name.
show (bool): Whether to show the image. Default: False.
show_keypoint_weight (bool): Whether to change the transparency
using the predicted confidence scores of keypoints.
wait_time (int): Value of waitKey param.
Default: 0.
out_file (str or None): The filename to write the image.
Default: None.
Returns:
Tensor: Visualized img, only if not `show` or `out_file`.
"""
img = mmcv.imread(img)
img = img.copy()
bbox_result = []
bbox_labels = []
pose_result = []
for res in result:
if 'bbox' in res:
bbox_result.append(res['bbox'])
bbox_labels.append(res.get('label', None))
pose_result.append(res['keypoints'])
if bbox_result:
bboxes = np.vstack(bbox_result)
# draw bounding boxes
imshow_bboxes(
img,
bboxes,
labels=bbox_labels,
colors=bbox_color,
text_color=text_color,
thickness=bbox_thickness,
font_scale=font_scale,
show=False)
if pose_result:
imshow_keypoints(img, pose_result, skeleton, kpt_score_thr,
pose_kpt_color, pose_link_color, radius,
thickness)
if show:
imshow(img, win_name, wait_time)
if out_file is not None:
imwrite(img, out_file)
return img