-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpreprocessing.py
More file actions
131 lines (105 loc) · 4.22 KB
/
preprocessing.py
File metadata and controls
131 lines (105 loc) · 4.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import numpy as np
import pandas as pd
def fill_missing(x, kind="nearest", warn_all_nan=False, **kwargs):
"""Fill missing values in a timeseries.
Args:
x: Timeseries of shape (time, _) or (_, time, _).
kind: Type of interpolation to use. Defaults to "nearest".
warn_all_nan: If True, emit a warning when a column remains entirely
NaN after interpolation (i.e. it had no valid values to begin with).
Defaults to False to preserve existing behaviour in batch pipelines.
Returns:
Timeseries of the same shape as the input with NaNs filled in.
"""
if x.ndim == 3:
return np.stack(
[fill_missing(xi, kind=kind, warn_all_nan=warn_all_nan, **kwargs) for xi in x],
axis=0,
)
result = (
pd.DataFrame(x)
.interpolate(kind=kind, axis=0, limit_direction="both", **kwargs)
.to_numpy()
)
if warn_all_nan and np.isnan(result).all(axis=0).any():
import warnings
all_nan_cols = np.where(np.isnan(result).all(axis=0))[0]
warnings.warn(
f"fill_missing: {len(all_nan_cols)} column(s) are entirely NaN "
f"after interpolation (column indices: {all_nan_cols.tolist()}). "
f"These columns had no valid values to interpolate from.",
stacklevel=2,
)
return result
def normalize_to_egocentric(x, rel_to=None, scale_factor=1, ctr_ind=1, fwd_ind=0, fill=True, return_angles=False):
"""Normalize pose estimates to egocentric coordinates.
Args:
x: Pose of shape (joints, 2) or (time, joints, 2)
rel_to: Pose to align x with of shape (joints, 2) or (time, joints, 2). Defaults
to x if not specified.
scale_factor: Spatial scaling to apply to coordinates after centering.
ctr_ind: Index of centroid joint. Defaults to 1.
fwd_ind: Index of "forward" joint (e.g., head). Defaults to 0.
fill: If True, interpolate missing ctr and fwd coordinates. If False, timesteps
with missing coordinates will be all NaN. Defaults to True.
return_angles: If True, return angles with the aligned coordinates.
Returns:
Egocentrically aligned poses of the same shape as the input.
If return_angles is True, also returns a vector of angles.
"""
if rel_to is None:
rel_to = x
is_singleton = (x.ndim == 2) and (rel_to.ndim == 2)
if x.ndim == 2:
x = np.expand_dims(x, axis=0)
if rel_to.ndim == 2:
rel_to = np.expand_dims(rel_to, axis=0)
# Find egocentric forward coordinates.
ctr = rel_to[..., ctr_ind, :] # (t, 2)
fwd = rel_to[..., fwd_ind, :] # (t, 2)
if fill:
ctr = fill_missing(ctr, kind="nearest")
fwd = fill_missing(fwd, kind="nearest")
ego_fwd = fwd - ctr
# Compute angle.
ang = np.arctan2(ego_fwd[..., 1], ego_fwd[..., 0]) # arctan2(y, x) -> radians in [-pi, pi]
ca = np.cos(ang) # (t,)
sa = np.sin(ang) # (t,)
# Build rotation matrix.
rot = np.zeros([len(ca), 3, 3], dtype=ca.dtype)
rot[..., 0, 0] = ca
rot[..., 0, 1] = -sa
rot[..., 1, 0] = sa
rot[..., 1, 1] = ca
rot[..., 2, 2] = 1
# Center and scale.
x = x - np.expand_dims(ctr, axis=1)
x /= scale_factor
# Pad, rotate and crop.
x = np.pad(x, ((0, 0), (0, 0), (0, 1)), "constant", constant_values=1) @ rot
x = x[..., :2]
if is_singleton:
x = x[0]
if return_angles:
return x, ang
else:
return x
def signed_angle(a, b):
"""Finds the signed angle between two 2D vectors a and b.
Args:
a: Array of shape (n, 2).
b: Array of shape (n, 2).
Returns:
The signed angles in degrees in vector of shape (n, 2).
This angle is positive if a is rotated clockwise to align to b and negative if
this rotation is counter-clockwise.
"""
a = a / np.linalg.norm(a, axis=1, keepdims=True)
b = b / np.linalg.norm(b, axis=1, keepdims=True)
theta = np.arccos(np.around(np.sum(a * b, axis=1), decimals=4))
# cross = np.cross(a, b, axis=1)
cross = a[:, 0] * b[:, 1] - a[:, 1] * b[:, 0]
sign = np.zeros(cross.shape)
sign[cross >= 0] = -1
sign[cross < 0] = 1
return np.rad2deg(theta) * sign