T2M-GPT 코드 분석 (3) HumanML3D preprocessing 정복하기
HumanML3D preprocessing
T2M-GPT를 보면 HumanML3D Dataset을 사용하는 것을 알 수가 있는데 여기에서 Extract and Process Data를 motion_representation.ipynb를 확인하고자 한다.
Motion Representation preprocessing
어제 진행한 코드
def inverse_kinematics_np(self, joints, face_joint_idx, smooth_forward=False):
assert len(face_joint_idx) == 4
'''Get Forward Direction'''
l_hip, r_hip, sdr_r, sdr_l = face_joint_idx
across1 = joints[:, r_hip] - joints[:, l_hip]
across2 = joints[:, sdr_r] - joints[:, sdr_l]
across = across1 + across2
across = across / np.sqrt((across**2).sum(axis=-1))[:, np.newaxis]
# print(across1.shape, across2.shape)
# forward (batch_size, 3)
forward = np.cross(np.array([[0, 1, 0]]), across, axis=-1)
if smooth_forward:
forward = filters.gaussian_filter1d(forward, 20, axis=0, mode='nearest')
# forward (batch_size, 3)
forward = forward / np.sqrt((forward**2).sum(axis=-1))[..., np.newaxis]
'''Get Root Rotation'''
target = np.array([[0,0,1]]).repeat(len(forward), axis=0)
root_quat = qbetween_np(forward, target)
'''Inverse Kinematics'''
# quat_params (batch_size, joints_num, 4)
# print(joints.shape[:-1])
quat_params = np.zeros(joints.shape[:-1] + (4,))
# print(quat_params.shape)
root_quat[0] = np.array([[1.0, 0.0, 0.0, 0.0]])
quat_params[:, 0] = root_quat
# quat_params[0, 0] = np.array([[1.0, 0.0, 0.0, 0.0]])
for chain in self._kinematic_tree:
R = root_quat
for j in range(len(chain) - 1):
# (batch, 3)
u = self._raw_offset_np[chain[j+1]][np.newaxis,...].repeat(len(joints), axis=0)
# print(u.shape)
# (batch, 3)
v = joints[:, chain[j+1]] - joints[:, chain[j]]
v = v / np.sqrt((v**2).sum(axis=-1))[:, np.newaxis]
# print(u.shape, v.shape)
rot_u_v = qbetween_np(u, v)
R_loc = qmul_np(qinv_np(R), rot_u_v)
quat_params[:,chain[j + 1], :] = R_loc
R = qmul_np(R, R_loc)
return quat_params
Get Root Rotation
은 motion의 길이만큼 qbetween_np를 통해 root_quat를 만들고 가장 첫번째 root_quat[0]에서 x만 1로 둔다.
kinematic_tree와 raw_offsets은 다음과 같다.
t2m_raw_offsets = np.array([[0,0,0],
[1,0,0],
[-1,0,0],
[0,1,0],
[0,-1,0],
[0,-1,0],
[0,1,0],
[0,-1,0],
[0,-1,0],
[0,1,0],
[0,0,1],
[0,0,1],
[0,1,0],
[1,0,0],
[-1,0,0],
[0,0,1],
[0,-1,0],
[0,-1,0],
[0,-1,0],
[0,-1,0],
[0,-1,0],
[0,-1,0]])
t2m_kinematic_chain = [[0, 2, 5, 8, 11], [0, 1, 4, 7, 10], [0, 3, 6, 9, 12, 15], [9, 14, 17, 19, 21], [9, 13, 16, 18, 20]]
R은 root_quat이며 u는 chain에서 다음 joint number를 의미하고 v는 chain에서 앞 뒤에 대한 vector 차이 값을 의미한다.
u와 v에 대한 외적 및 스칼라 값을 구한 rot_u_v를 구한 후 R_loc을 만들어주는데 qinv_np
는
def qinv(q):
assert q.shape[-1] == 4, 'q must be a tensor of shape (*, 4)'
mask = torch.ones_like(q)
mask[..., 1:] = -mask[..., 1:]
return q * mask
def qinv_np(q):
assert q.shape[-1] == 4, 'q must be a tensor of shape (*, 4)'
return qinv(torch.from_numpy(q).float()).numpy()
의 코드로 생성되며 qmul_np
는 다음과 같다.
def qmul_np(q, r):
q = torch.from_numpy(q).contiguous().float()
r = torch.from_numpy(r).contiguous().float()
return qmul(q, r).numpy()
def qmul(q, r):
"""
Multiply quaternion(s) q with quaternion(s) r.
Expects two equally-sized tensors of shape (*, 4), where * denotes any number of dimensions.
Returns q*r as a tensor of shape (*, 4).
"""
assert q.shape[-1] == 4
assert r.shape[-1] == 4
original_shape = q.shape
# Compute outer product
terms = torch.bmm(r.view(-1, 4, 1), q.view(-1, 1, 4))
w = terms[:, 0, 0] - terms[:, 1, 1] - terms[:, 2, 2] - terms[:, 3, 3]
x = terms[:, 0, 1] + terms[:, 1, 0] - terms[:, 2, 3] + terms[:, 3, 2]
y = terms[:, 0, 2] + terms[:, 1, 3] + terms[:, 2, 0] - terms[:, 3, 1]
z = terms[:, 0, 3] - terms[:, 1, 2] + terms[:, 2, 1] + terms[:, 3, 0]
return torch.stack((w, x, y, z), dim=1).view(original_shape)
quaternion q,r에 대한 곱셈을 보여준다 생각하면 될 것 같다. 이렇게 quat_params
를 만들어줘 return을 한다고 생각하면 될 것 같다.
이렇게 quat_params를 만들어주면 Quaternion to continuous 6D
를 거친다.
cont_6d_params = quaternion_to_cont6d_np(quat_params)
# (seq_len, 4)
r_rot = quat_params[:, 0].copy()
quaternion_to_cont6d_np
의 경우 다음과 같은 과정을 통해 나오게 된다.
def quaternion_to_matrix_np(quaternions):
q = torch.from_numpy(quaternions).contiguous().float()
return quaternion_to_matrix(q).numpy()
def quaternion_to_cont6d_np(quaternions):
rotation_mat = quaternion_to_matrix_np(quaternions)
cont_6d = np.concatenate([rotation_mat[..., 0], rotation_mat[..., 1]], axis=-1)
return cont_6d
def quaternion_to_matrix(quaternions):
"""
Convert rotations given as quaternions to rotation matrices.
Args:
quaternions: quaternions with real part first,
as tensor of shape (..., 4).
Returns:
Rotation matrices as tensor of shape (..., 3, 3).
"""
r, i, j, k = torch.unbind(quaternions, -1)
two_s = 2.0 / (quaternions * quaternions).sum(-1)
o = torch.stack(
(
1 - two_s * (j * j + k * k),
two_s * (i * j - k * r),
two_s * (i * k + j * r),
two_s * (i * j + k * r),
1 - two_s * (i * i + k * k),
two_s * (j * k - i * r),
two_s * (i * k - j * r),
two_s * (j * k + i * r),
1 - two_s * (i * i + j * j),
),
-1,
)
return o.reshape(quaternions.shape[:-1] + (3, 3))
이렇게 6D parameter가 나오게 되면 이제부터 velocity를 구하게 된다.
Root Linear velocity
와 Root Angular Velocity
Code
'''Root Linear Velocity'''
# (seq_len - 1, 3)
velocity = (positions[1:, 0] - positions[:-1, 0]).copy()
# print(r_rot.shape, velocity.shape)
velocity = qrot_np(r_rot[1:], velocity)
'''Root Angular Velocity'''
# (seq_len - 1, 4)
r_velocity = qmul_np(r_rot[1:], qinv_np(r_rot[:-1]))
# (seq_len, joints_num, 4)
positions[1:, 0] - positions[:-1, 0]
의 의미는 1 이후 프레임들 - 0부터 프레임들의 차라고 생각하면 될 것 같다.
이렇게 나온 velocity를 r_rot와 qrot를 통해 quaternions velocity를 구해주고 마지막으로 r_velocity = qmul_np(r_rot[1:], qinv_np(r_rot[:-1]))
를 해주는데 정확한 의미는 모르겠다…
get_rifke
를 진행하는데
def get_rifke(positions):
'''Local pose'''
positions[..., 0] -= positions[:, 0:1, 0]
positions[..., 2] -= positions[:, 0:1, 2]
'''All pose face Z+'''
positions = qrot_np(np.repeat(r_rot[:, None], positions.shape[1], axis=1), positions)
return positions
position들의 x,z를 첫번째 motion에 대해 다시 옮기고 모든 pose가 바라보는 방향을 Z+로 만든다.
그 다음 Root height를 구하는데 모든 frame에서 pelvis에 대한 y 값만 구한다.
'''Root height'''
root_y = positions[:, 0, 1:2]
Root rotation and linear velocity
를 구하는데
'''Root rotation and linear velocity'''
# (seq_len-1, 1) rotation velocity along y-axis
# (seq_len-1, 2) linear velovity on xz plane
r_velocity = np.arcsin(r_velocity[:, 2:3])
l_velocity = velocity[:, [0, 2]]
# print(r_velocity.shape, l_velocity.shape, root_y.shape)
root_data = np.concatenate([r_velocity, l_velocity, root_y[:-1]], axis=-1)
의미를 파악해보면 y에 대한 rotation을 구한 후 xz에 대한 linear velocity를 구하는 것 같다.
그 다음Get Joint Rotation Representation, Get Joint Rotation Invariant Position Represention, Get Joint Velocity Representation
를 진행해 data를 만든 후에 concat을 시켜 data를 만든다.
'''Get Joint Rotation Representation'''
# (seq_len, (joints_num-1) *6) quaternion for skeleton joints
rot_data = cont_6d_params[:, 1:].reshape(len(cont_6d_params), -1)
'''Get Joint Rotation Invariant Position Represention'''
# (seq_len, (joints_num-1)*3) local joint position
ric_data = positions[:, 1:].reshape(len(positions), -1)
'''Get Joint Velocity Representation'''
# (seq_len-1, joints_num*3)
local_vel = qrot_np(np.repeat(r_rot[:-1, None], global_positions.shape[1], axis=1),
global_positions[1:] - global_positions[:-1])
local_vel = local_vel.reshape(len(local_vel), -1)
이 data로 훈련을 시키게 되는데 정확한 의미를 파악해보자면
data에 들어간 정보들 => Root height + linear, angular velocity + positions에 대한 정보 + inverse kinematic에 대한 6D 정보 + Joint Velocity Representation + 발에대한 threshold 정보이다.
후기
너무 어렵다 :)
Leave a comment