Source code for tests.pose_test

import random
import string
from typing import Optional, Tuple
from unittest import TestCase

import numpy as np
import numpy.ma as ma
import tensorflow as tf

from pose_format.numpy.pose_body import NumPyPoseBody
from pose_format.pose import Pose
from pose_format.pose_header import (PoseHeader, PoseHeaderComponent,
                                     PoseHeaderDimensions)
from pose_format.tensorflow.masked.tensor import MaskedTensor
from pose_format.tensorflow.pose_body import TensorflowPoseBody


def _create_pose_header_component(name: str, num_keypoints: int) -> PoseHeaderComponent:
    """
    Create a PoseHeaderComponent with randomized limbs and colors.

    Parameters
    ----------
    name : str
        Name of the component.
    num_keypoints : int
        Number of keypoints for the component.

    Returns
    -------
    PoseHeaderComponent
        Component with specified name, randomized limbs and colors.
    """

    points = string.ascii_letters[:num_keypoints]
    points = [name + "_" + point for point in points]

    limbs = []
    colors = []

    for _ in points:
        limb = (random.randint(0, num_keypoints), random.randint(0, num_keypoints))
        limbs.append(limb)

        color = (random.randint(0, 256), random.randint(0, 256), random.randint(0, 256))

        colors.append(color)

    component = PoseHeaderComponent(name=name, points=points, limbs=limbs, colors=colors, point_format="XYC")

    return component


def _create_pose_header(width: int, height: int, depth: int, num_components: int, num_keypoints: int) -> PoseHeader:
    """
    Create a PoseHeader with given dimensions and components.

    Parameters
    ----------
    width : int
        Width dimension.
    height : int
        Height dimension.
    depth : int
        Depth dimension.
    num_components : int
        Number of components.
    num_keypoints : int
        Number of keypoints for each component.

    Returns
    -------
    PoseHeader
        Header with specified dimensions and components.
    """
    dimensions = PoseHeaderDimensions(width=width, height=height, depth=depth)

    components = [
        _create_pose_header_component(name=str(index), num_keypoints=num_keypoints) for index in range(num_components)
    ]

    header = PoseHeader(version=1.0, dimensions=dimensions, components=components)

    return header


def _create_random_tensorflow_data(frames_min: Optional[int] = None,
                                   frames_max: Optional[int] = None,
                                   num_frames: Optional[int] = None,
                                   num_keypoints: int = 137,
                                   num_dimensions: int = 2) -> Tuple[tf.Tensor, tf.Tensor, tf.Tensor]:
    """
    Creates random TensorFlow data for testing.

    Parameters
    ----------
    frames_min : Optional[int], default=None
        Minimum number of frames for random generation if `num_frames` is not specified.
    frames_max : Optional[int], default=None
        Maximum number of frames for random generation if `num_frames` is not specified.
    num_frames : Optional[int], default=None
        Specific number of frames.
    num_keypoints : int, default=137
        Number of keypoints in the pose data.
    num_dimensions : int, default=2
        Number of dimensions in the pose data.

    Returns
    -------
    Tuple[tf.Tensor, tf.Tensor, tf.Tensor]
        Random tensor data, mask, and confidence values.
    """
    if num_frames is None:
        assert None not in [frames_min, frames_max]
        num_frames = np.random.randint(frames_min, frames_max + 1)
    else:
        assert frames_min is None and frames_max is None

    # avoid a mean of zero to test certain pose methods
    # (Frames, People, Points, Dims) - eg (93, 1, 137, 2)
    tensor = tf.random.normal(shape=(num_frames, 1, num_keypoints, num_dimensions), mean=1.0)

    # (Frames, People, Points) - eg (93, 1, 137)
    confidence = tf.random.uniform((num_frames, 1, num_keypoints), minval=0.0, maxval=1.0, dtype=tf.dtypes.float32)

    # (Frames, People, Points, Dims) - eg (93, 1, 137, 2)
    mask = tf.random.uniform((num_frames, 1, num_keypoints, num_dimensions), minval=0, maxval=2, dtype=tf.int32)
    mask = tf.cast(mask, tf.bool)

    return tensor, mask, confidence


def _create_random_numpy_data(frames_min: Optional[int] = None,
                              frames_max: Optional[int] = None,
                              num_frames: Optional[int] = None,
                              num_keypoints: int = 137,
                              num_dimensions: int = 2,
                              probability_for_masked: float = 0.2) -> Tuple[np.array, np.array, np.array]:
    """
    Creates random Numpy data for testing.

    Parameters
    ----------
    frames_min : Optional[int], default=None
        Minimum number of frames for random generation if `num_frames` is not specified.
    frames_max : Optional[int], default=None
        Maximum number of frames for random generation if `num_frames` is not specified.
    num_frames : Optional[int], default=None
        Specific number of frames.
    num_keypoints : int, default=137
        Number of keypoints in the pose data.
    num_dimensions : int, default=2
        Number of dimensions in the pose data.
    probability_for_masked : float, default=0.2
        Probability for generating masked values.

    Returns
    -------
    Tuple[np.array, np.array, np.array]
        Random numpy array data, mask, and confidence values.
    """
    if num_frames is None:
        assert None not in [frames_min, frames_max]
        num_frames = np.random.randint(frames_min, frames_max + 1)
    else:
        assert frames_min is None and frames_max is None

    # avoid a mean of zero to test certain pose methods
    # (Frames, People, Points, Dims) - eg (93, 1, 137, 2)
    tensor = np.random.normal(loc=1.0, size=(num_frames, 1, num_keypoints, num_dimensions))

    # (Frames, People, Points) - eg (93, 1, 137)
    confidence = np.random.uniform(size=(num_frames, 1, num_keypoints), low=0.0, high=1.0)

    # (Frames, People, Points, Dims) - eg (93, 1, 137, 2)
    mask = np.random.choice(a=[False, True],
                            size=(num_frames, 1, num_keypoints, num_dimensions),
                            p=[probability_for_masked, 1 - probability_for_masked])

    return tensor, mask, confidence


def _get_random_pose_object_with_tf_posebody(num_keypoints: int, frames_min: int = 1, frames_max: int = 10) -> Pose:
    """
    Generates a random Pose object with TensorFlow pose body for testing.

    Parameters
    ----------
    num_keypoints : int
        Number of keypoints in the pose data.
    frames_min : int, default=1
        Minimum number of frames for random generation.
    frames_max : int, default=10
        Maximum number of frames for random generation.

    Returns
    -------
    Pose
        Randomly generated Pose object.
    """

    tensor, mask, confidence = _create_random_tensorflow_data(frames_min=frames_min,
                                                              frames_max=frames_max,
                                                              num_keypoints=num_keypoints)

    masked_tensor = MaskedTensor(tensor=tensor, mask=mask)
    body = TensorflowPoseBody(fps=10, data=masked_tensor, confidence=confidence)

    header = _create_pose_header(width=10, height=7, depth=0, num_components=3, num_keypoints=num_keypoints)

    return Pose(header=header, body=body)


def _get_random_pose_object_with_numpy_posebody(num_keypoints: int, frames_min: int = 1, frames_max: int = 10) -> Pose:
    """
    Creates a random Pose object with Numpy pose body for testing.

    Parameters
    ----------
    num_keypoints : int
        Number of keypoints in the pose data.
    frames_min : int, default=1
        Minimum number of frames for random generation.
    frames_max : int, default=10
        Maximum number of frames for random generation.

    Returns
    -------
    Pose
        Randomly generated Pose object.
    """

    tensor, mask, confidence = _create_random_numpy_data(frames_min=frames_min,
                                                         frames_max=frames_max,
                                                         num_keypoints=num_keypoints)

    masked_array = ma.array(tensor, mask=np.logical_not(mask))

    body = NumPyPoseBody(fps=10, data=masked_array, confidence=confidence)

    header = _create_pose_header(width=10, height=7, depth=0, num_components=3, num_keypoints=num_keypoints)

    return Pose(header=header, body=body)


[docs]class TestPose(TestCase): """ A suite of tests for the Pose object. """
[docs] def test_pose_object_should_be_callable(self): """ Tests if the Pose object is callable. """ assert callable(Pose)
[docs]class TestPoseTensorflowPoseBody(TestCase): """ Tests for Pose objects containing TensorFlow PoseBody data. """
[docs] def test_pose_tf_posebody_normalize_eager_mode_preserves_shape(self): """ Tests if the normalization of Pose object with TensorFlow PoseBody in eager mode preserves tensor shape. """ pose = _get_random_pose_object_with_tf_posebody(num_keypoints=5) shape_before = pose.body.data.tensor.shape # in the mock data header components are named 0, 1, and so on # individual points are named 0_a, 0_b, and so on pose.normalize(pose.header.normalization_info(p1=("0", "0_a"), p2=("0", "0_b"))) shape_after = pose.body.data.tensor.shape self.assertEqual(shape_before, shape_after, "Normalize did not preserve tensor shape.")
[docs] def test_pose_tf_posebody_normalize_distribution_eager_mode_correct_result(self): """ Tests if the normalization of distribution for Pose object with TensorFlow PoseBody returns expected result. """ pose = _get_random_pose_object_with_tf_posebody(num_keypoints=5, frames_min=2) tensor_as_numpy = pose.body.data.tensor.numpy() mask_as_numpy = pose.body.data.mask.numpy() masked_numpy_array = ma.array(tensor_as_numpy, mask=np.logical_not(mask_as_numpy)) axis = (0, 1) numpy_mean = ma.mean(masked_numpy_array, axis=axis) numpy_std = ma.std(masked_numpy_array, axis=axis) expected_tensor = (masked_numpy_array - numpy_mean) / numpy_std expected_tensor = expected_tensor.filled(0) pose.normalize_distribution(axis=axis) actual_tensor_nan_fixed = pose.body.data.fix_nan() actual_tensor_zero_filled = actual_tensor_nan_fixed.zero_filled() actual_tensor_as_numpy = actual_tensor_zero_filled.numpy() self.assertTrue(np.allclose(actual_tensor_as_numpy, expected_tensor), "Normalize distribution did not return the expected result.")
[docs] def test_pose_tf_posebody_frame_dropout_normal_eager_mode_num_frames_not_zero(self): """ Tests if frame dropout using normal distribution in Pose object with TensorFlow PoseBody preserves frame count. """ pose = _get_random_pose_object_with_tf_posebody(num_keypoints=5, frames_min=3) pose_after_dropout, _ = pose.frame_dropout_normal() num_frames = pose_after_dropout.body.data.tensor.shape[0] self.assertNotEqual(num_frames, 0, "Number of frames after dropout can never be 0.")
[docs] def test_pose_tf_posebody_frame_dropout_uniform_eager_mode_num_frames_not_zero(self): """ Checks if frame dropout using uniform distribution in Pose object with TensorFlow PoseBody preserves frame count. """ pose = _get_random_pose_object_with_tf_posebody(num_keypoints=5, frames_min=3) pose_after_dropout, _ = pose.frame_dropout_uniform() num_frames = pose_after_dropout.body.data.tensor.shape[0] self.assertNotEqual(num_frames, 0, "Number of frames after dropout can never be 0.")
# test if pose object methods can be used as tf.data.Dataset operations
[docs] def test_pose_normalize_can_be_used_in_tf_dataset_map(self): """ Tests if the normalize method of Pose object can be used within a tf.data.Dataset map operation. """ num_examples = 10 dataset = tf.data.Dataset.range(num_examples) def create_and_normalize_pose(example: tf.Tensor) -> tf.Tensor: pose = _get_random_pose_object_with_tf_posebody(num_keypoints=5) _ = pose.normalize(pose.header.normalization_info(p1=("0", "0_a"), p2=("0", "0_b"))) return example dataset.map(create_and_normalize_pose)
[docs] def test_pose_normalize_distribution_can_be_used_in_tf_dataset_map(self): """ Tests if the normalize_distribution method of Pose object can be used within a tf.data.Dataset map operation. """ num_examples = 10 dataset = tf.data.Dataset.range(num_examples) def create_pose_and_normalize_distribution(example: tf.Tensor) -> tf.Tensor: pose = _get_random_pose_object_with_tf_posebody(num_keypoints=5) _ = pose.normalize_distribution(axis=(0, 1)) return example dataset.map(create_pose_and_normalize_distribution)
[docs] def test_pose_frame_dropout_normal_can_be_used_in_tf_dataset_map(self): """ Tests if the frame_dropout_normal method of Pose object can be used within a tf.data.Dataset map operation. """ num_examples = 10 dataset = tf.data.Dataset.range(num_examples) def create_pose_and_frame_dropout_normal(example: tf.Tensor) -> tf.Tensor: pose = _get_random_pose_object_with_tf_posebody(num_keypoints=5) _, _ = pose.frame_dropout_normal() return example dataset.map(create_pose_and_frame_dropout_normal)
[docs] def test_pose_frame_dropout_uniform_can_be_used_in_tf_dataset_map(self): """ Tests if the frame_dropout_uniform method of Pose object can be used within a tf.data.Dataset map operation. """ num_examples = 10 dataset = tf.data.Dataset.range(num_examples) def create_pose_and_frame_dropout_uniform(example: tf.Tensor) -> tf.Tensor: pose = _get_random_pose_object_with_tf_posebody(num_keypoints=5) _, _ = pose.frame_dropout_uniform() return example dataset.map(create_pose_and_frame_dropout_uniform)
[docs]class TestPoseNumpyPoseBody(TestCase): """ Testcases for Pose objects containing NumPy PoseBody data. """
[docs] def test_pose_numpy_posebody_normalize_preserves_shape(self): """ Tests if the normalization of Pose object with NumPy PoseBody preserves array shape. """ pose = _get_random_pose_object_with_numpy_posebody(num_keypoints=5, frames_min=3) shape_before = pose.body.data.shape # in the mock data header components are named 0, 1, and so on # individual points are named 0_a, 0_b, and so on pose.normalize(pose.header.normalization_info(p1=("0", "0_a"), p2=("0", "0_b"))) shape_after = pose.body.data.shape self.assertEqual(shape_before, shape_after, "Normalize did not preserve tensor shape.")
[docs] def test_pose_numpy_posebody_frame_dropout_normal_eager_mode_num_frames_not_zero(self): """ Tests if frame dropout using normal distribution in Pose object with NumPy PoseBody preserves frame count. """ pose = _get_random_pose_object_with_numpy_posebody(num_keypoints=5, frames_min=3) pose_after_dropout, _ = pose.frame_dropout_normal() num_frames = pose_after_dropout.body.data.shape[0] self.assertNotEqual(num_frames, 0, "Number of frames after dropout can never be 0.")
[docs] def test_pose_numpy_posebody_frame_dropout_uniform_eager_mode_num_frames_not_zero(self): """ Tests if frame dropout using uniform distribution in Pose object with NumPy PoseBody preserves frame count. """ pose = _get_random_pose_object_with_numpy_posebody(num_keypoints=5, frames_min=3) pose_after_dropout, _ = pose.frame_dropout_uniform() num_frames = pose_after_dropout.body.data.shape[0] self.assertNotEqual(num_frames, 0, "Number of frames after dropout can never be 0.")