Skip to content

Commit fec7231

Browse files
authored
Merge pull request #15 from cleong110/pose_utils
Pose utils and some tests.
2 parents 1d9ba7a + 8a75753 commit fec7231

File tree

9 files changed

+1029
-2
lines changed

9 files changed

+1029
-2
lines changed

.gitignore

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
.idea/
22
build/
33
pose_evaluation.egg-info/
4-
**/__pycache__/
4+
**/__pycache__/
5+
.coverage
6+
.vscode/
7+
coverage.lcov
8+
**/test_data/
9+
*.npz
10+
*.code-workspace

pose_evaluation/metrics/.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
temp/
1+
tests

pose_evaluation/utils/conftest.py

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import json
2+
import copy
3+
from pathlib import Path
4+
from typing import List, Dict
5+
6+
import pytest
7+
from pose_format import Pose
8+
from pose_format.utils.generic import fake_pose
9+
from pose_format.utils.openpose_135 import (
10+
OpenPose_Components as openpose_135_components,
11+
)
12+
13+
from pose_evaluation.utils.pose_utils import load_pose_file
14+
15+
16+
17+
utils_test_data_dir = Path(__file__).parent / "test" / "test_data"
18+
19+
20+
@pytest.fixture(scope="function")
21+
def mediapipe_poses_test_data_paths() -> List[Path]:
22+
pose_file_paths = list(utils_test_data_dir.glob("*.pose"))
23+
return pose_file_paths
24+
25+
26+
@pytest.fixture(scope="function")
27+
def mediapipe_poses_test_data(mediapipe_poses_test_data_paths) -> List[Pose]:
28+
original_poses = [
29+
load_pose_file(pose_path) for pose_path in mediapipe_poses_test_data_paths
30+
]
31+
# I ran into issues where if one test would modify a Pose, it would affect other tests.
32+
# specifically, pose.header.components[0].name = unsupported_component_name in test_detect_format
33+
# this ensures we get a fresh object each time.
34+
return copy.deepcopy(original_poses)
35+
36+
37+
@pytest.fixture
38+
def standard_mediapipe_components_dict() -> Dict[str, List[str]]:
39+
format_json = utils_test_data_dir / "mediapipe_components_and_points.json"
40+
with open(format_json, "r", encoding="utf-8") as f:
41+
return json.load(f)
42+
43+
44+
@pytest.fixture
45+
def fake_openpose_poses(count: int = 3) -> List[Pose]:
46+
return [fake_pose(30) for _ in range(count)]
47+
48+
49+
@pytest.fixture
50+
def fake_openpose_135_poses(count: int = 3) -> List[Pose]:
51+
return [fake_pose(30, components=openpose_135_components) for _ in range(count)]

pose_evaluation/utils/pose_utils.py

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from pathlib import Path
2+
from typing import List, Tuple, Dict, Iterable
3+
from collections import defaultdict
4+
import numpy as np
5+
from numpy import ma
6+
from pose_format import Pose
7+
8+
9+
def pose_remove_world_landmarks(pose: Pose) -> Pose:
10+
return pose.remove_components(["POSE_WORLD_LANDMARKS"])
11+
12+
13+
def get_component_names_and_points_dict(
14+
pose: Pose,
15+
) -> Tuple[List[str], Dict[str, List[str]]]:
16+
component_names = []
17+
points_dict = defaultdict(list)
18+
for component in pose.header.components:
19+
component_names.append(component.name)
20+
21+
for point in component.points:
22+
points_dict[component.name].append(point)
23+
24+
return component_names, points_dict
25+
26+
27+
def get_face_and_hands_from_pose(pose: Pose) -> Pose:
28+
# based on MediaPipe Holistic format.
29+
components_to_keep = [
30+
"FACE_LANDMARKS",
31+
"LEFT_HAND_LANDMARKS",
32+
"RIGHT_HAND_LANDMARKS",
33+
]
34+
return pose.get_components(components_to_keep)
35+
36+
37+
def load_pose_file(pose_path: Path) -> Pose:
38+
pose_path = Path(pose_path).resolve()
39+
with pose_path.open("rb") as f:
40+
pose = Pose.read(f.read())
41+
return pose
42+
43+
44+
def reduce_poses_to_intersection(
45+
poses: Iterable[Pose],
46+
) -> List[Pose]:
47+
poses = list(poses) # get a list, no need to copy
48+
49+
# look at the first pose
50+
component_names = {c.name for c in poses[0].header.components}
51+
points = {c.name: set(c.points) for c in poses[0].header.components}
52+
53+
# remove anything that other poses don't have
54+
for pose in poses[1:]:
55+
component_names.intersection_update({c.name for c in pose.header.components})
56+
for component in pose.header.components:
57+
points[component.name].intersection_update(set(component.points))
58+
59+
# change datatypes to match get_components, then update the poses
60+
points_dict = {}
61+
for c_name in points.keys():
62+
points_dict[c_name] = list(points[c_name])
63+
poses = [pose.get_components(list(component_names), points_dict) for pose in poses]
64+
return poses
65+
66+
67+
def zero_pad_shorter_poses(poses: Iterable[Pose]) -> List[Pose]:
68+
poses = [pose.copy() for pose in poses]
69+
# arrays = [pose.body.data for pose in poses]
70+
71+
# first dimension is frames. Then People, joint-points, XYZ or XY
72+
max_frame_count = max(len(pose.body.data) for pose in poses)
73+
# Pad the shorter array with zeros
74+
for pose in poses:
75+
if len(pose.body.data) < max_frame_count:
76+
desired_shape = list(pose.body.data.shape)
77+
desired_shape[0] = max_frame_count - len(pose.body.data)
78+
padding_tensor = ma.zeros(desired_shape)
79+
padding_tensor_conf = ma.ones(desired_shape[:-1])
80+
pose.body.data = ma.concatenate([pose.body.data, padding_tensor], axis=0)
81+
pose.body.confidence = ma.concatenate(
82+
[pose.body.confidence, padding_tensor_conf]
83+
)
84+
return poses
85+
86+
87+
def pose_hide_low_conf(pose: Pose, confidence_threshold: float = 0.2) -> None:
88+
mask = pose.body.confidence <= confidence_threshold
89+
pose.body.confidence[mask] = 0
90+
stacked_confidence = np.stack([mask, mask, mask], axis=3)
91+
masked_data = ma.masked_array(pose.body.data, mask=stacked_confidence)
92+
pose.body.data = masked_data
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)