-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbvh_loader.py
258 lines (219 loc) · 10.6 KB
/
bvh_loader.py
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
import numpy as np
import copy
import smooth_utils as smooth
from scipy.spatial.transform import Rotation as R
from scipy.spatial.transform import Slerp
def load_meta_data(bvh_path):
with open(bvh_path, 'r') as f:
channels = []
joints = []
joint_parents = []
joint_offsets = []
end_sites = []
parent_stack = [None]
for line in f:
if 'ROOT' in line or 'JOINT' in line:
joints.append(line.split()[-1])
joint_parents.append(parent_stack[-1])
channels.append('')
joint_offsets.append([0, 0, 0])
elif 'End Site' in line:
end_sites.append(len(joints))
joints.append(parent_stack[-1] + '_end')
joint_parents.append(parent_stack[-1])
channels.append('')
joint_offsets.append([0, 0, 0])
elif '{' in line:
parent_stack.append(joints[-1])
elif '}' in line:
parent_stack.pop()
elif 'OFFSET' in line:
joint_offsets[-1] = np.array([float(x) for x in line.split()[-3:]]).reshape(1,3)
elif 'CHANNELS' in line:
trans_order = []
rot_order = []
for token in line.split():
if 'position' in token:
trans_order.append(token[0])
if 'rotation' in token:
rot_order.append(token[0])
channels[-1] = ''.join(trans_order)+ ''.join(rot_order)
elif 'Frame Time:' in line:
break
joint_parents = [-1]+ [joints.index(i) for i in joint_parents[1:]]
channels = [len(i) for i in channels]
return joints, joint_parents, channels, joint_offsets
def load_motion_data(bvh_path):
with open(bvh_path, 'r') as f:
lines = f.readlines()
for i in range(len(lines)):
if lines[i].startswith('Frame Time'):
break
motion_data = []
for line in lines[i+1:]:
data = [float(x) for x in line.split()]
if len(data) == 0:
break
motion_data.append(np.array(data).reshape(1,-1))
motion_data = np.concatenate(motion_data, axis=0)
return motion_data
# ------------- 实现一个简易的BVH对象,进行数据处理 -------------#
'''
注释里统一N表示帧数,M表示关节数
position, rotation表示局部平移和旋转
translation, orientation表示全局平移和旋转
'''
class BVHMotion():
def __init__(self, bvh_file_name = None) -> None:
# 一些 meta data
self.joint_name = []
self.joint_channel = []
self.joint_parent = []
self.num_frames = 0
# 一些local数据, 对应bvh里的channel, XYZposition和 XYZrotation
#! 这里我们把没有XYZ position的joint的position设置为offset, 从而进行统一
self.joint_position = None # (N,M,3) 的ndarray, 局部平移
self.joint_rotation = None # (N,M,4)的ndarray, 用四元数表示的局部旋转
self.joint_vel= None # (N,M,3) 的ndarray, 局部平移速度
self.joint_avel = None # (N,M,3) 的ndarray, 局部旋转速度
if bvh_file_name is not None:
self.load_motion(bvh_file_name)
pass
#------------------- 一些辅助函数 ------------------- #
def load_motion(self, bvh_file_path):
'''
读取bvh文件,初始化元数据和局部数据
'''
self.joint_name, self.joint_parent, self.joint_channel, joint_offset = \
load_meta_data(bvh_file_path)
motion_data = load_motion_data(bvh_file_path)
# 把motion_data里的数据分配到joint_position和joint_rotation里
self.joint_position = np.zeros((motion_data.shape[0], len(self.joint_name), 3))
self.joint_rotation = np.zeros((motion_data.shape[0], len(self.joint_name), 4))
self.joint_rotation[:,:,3] = 1.0 # 四元数的w分量默认为1
cur_channel = 0
for i in range(len(self.joint_name)):
if self.joint_channel[i] == 0:
self.joint_position[:,i,:] = joint_offset[i].reshape(1,3)
continue
elif self.joint_channel[i] == 3:
self.joint_position[:,i,:] = joint_offset[i].reshape(1,3)
rotation = motion_data[:, cur_channel:cur_channel+3]
elif self.joint_channel[i] == 6:
self.joint_position[:, i, :] = motion_data[:, cur_channel:cur_channel+3]
#print(self.joint_position[:, i, :])
rotation = motion_data[:, cur_channel+3:cur_channel+6]
self.joint_rotation[:, i, :] = R.from_euler('XYZ', rotation,degrees=True).as_quat()
cur_channel += self.joint_channel[i]
self.num_frames = self.joint_position.shape[0]
self.joint_vel = (self.joint_position[1:,:,:] - self.joint_position[:-1,:,:])/(1.0/60.0)
self.joint_avel = smooth.quat_to_avel(self.joint_rotation, 1.0/60.0)
return
def batch_forward_kinematics(self, joint_position = None, joint_rotation = None, frame_id_list = None, root_pos = None, root_quat = None):
'''
利用自身的metadata进行批量前向运动学
joint_position: (N,M,3)的ndarray, 局部平移
joint_rotation: (N,M,4)的ndarray, 用四元数表示的局部旋转
frame_id_list: 一个list, 表示需要计算的帧的id,如果是None则计算所有帧
root_pos: (3,)的ndarray, 手动指定root joint的位置
root_quat: (4,)的ndarray, 手动指定root joint的四元数表示的旋转
'''
if joint_position is None:
joint_position = self.joint_position
if joint_rotation is None:
joint_rotation = self.joint_rotation
if frame_id_list is not None:
joint_position = joint_position[frame_id_list].copy()
joint_rotation = joint_rotation[frame_id_list].copy()
if root_pos is not None:
root_pos = root_pos.reshape(1,3).repeat(joint_position.shape[0], axis=0)
joint_position[:,0,:] = root_pos
# 这种写法的问题是,joint_position是一个引用,所以每次root的信息输入,会直接改写保存的值;下同
if root_quat is not None:
root_quat = root_quat.reshape(1,4).repeat(joint_position.shape[0], axis=0)
joint_rotation[:,0,:] = root_quat
joint_translation = np.zeros_like(joint_position)
joint_orientation = np.zeros_like(joint_rotation)
joint_orientation[:,:,3] = 1.0 # 四元数的w分量默认为1
# 一个小hack是root joint的parent是-1, 对应最后一个关节
# 计算根节点时最后一个关节还未被计算,刚好是0偏移和单位朝向
for i in range(len(self.joint_name)):
pi = self.joint_parent[i]
parent_orientation = R.from_quat(joint_orientation[:,pi,:])
joint_translation[:, i, :] = joint_translation[:, pi, :] + \
parent_orientation.apply(joint_position[:, i, :])
joint_orientation[:, i, :] = (parent_orientation * R.from_quat(joint_rotation[:, i, :])).as_quat()
return joint_translation, joint_orientation
def adjust_joint_name(self, target_joint_name):
'''
调整关节顺序为target_joint_name
'''
idx = [self.joint_name.index(joint_name) for joint_name in target_joint_name]
idx_inv = [target_joint_name.index(joint_name) for joint_name in self.joint_name]
self.joint_name = [self.joint_name[i] for i in idx]
self.joint_parent = [idx_inv[self.joint_parent[i]] for i in idx]
self.joint_parent[0] = -1
self.joint_channel = [self.joint_channel[i] for i in idx]
self.joint_position = self.joint_position[:,idx,:]
self.joint_rotation = self.joint_rotation[:,idx,:]
pass
def raw_copy(self):
'''
返回一个拷贝
'''
return copy.deepcopy(self)
@property
def motion_length(self):
return self.joint_position.shape[0]
def sub_sequence(self, start, end):
'''
返回一个子序列
start: 开始帧
end: 结束帧
'''
res = self.raw_copy()
res.joint_position = res.joint_position[start:end,:,:]
res.joint_rotation = res.joint_rotation[start:end,:,:]
return res
def append(self, other):
'''
在末尾添加另一个动作
'''
other = other.raw_copy()
other.adjust_joint_name(self.joint_name)
self.joint_position = np.concatenate((self.joint_position, other.joint_position), axis=0)
self.joint_rotation = np.concatenate((self.joint_rotation, other.joint_rotation), axis=0)
pass
def translation_and_rotation(self, frame_num, target_translation_xz, target_facing_direction_xz):
'''
计算出新的joint_position和joint_rotation
使第frame_num帧的根节点平移为target_translation_xz, 水平面朝向为target_facing_direction_xz
frame_num: int
target_translation_xz: (2,)的ndarray
target_faceing_direction_xz: (2,)的ndarray,表示水平朝向。你可以理解为原本的z轴被旋转到这个方向。
'''
res = self.raw_copy() # 拷贝一份,不要修改原始数据
offset = target_translation_xz - res.joint_position[frame_num, 0, [0,2]]
res.joint_position[:, 0, [0,2]] += offset
Ry, Rxz = self.decompose_rotation_with_yaxis(res.joint_rotation[frame_num, 0, :])
axis = np.array([0,1,0]).reshape(3,)
angle = np.arctan2(target_facing_direction_xz[0], target_facing_direction_xz[1])
desired_rot = R.from_rotvec(axis * angle)
rot = R.from_quat(res.joint_rotation[:, 0, :])
rot_diff = desired_rot*(R.from_quat(Ry).inv())
res.joint_rotation[:, 0, :] = (rot_diff*rot).as_quat()
pos_offset = res.joint_position[:, 0, :] - res.joint_position[frame_num, 0, :]
rotated_pos_offset = rot_diff.apply(pos_offset)
res.joint_position[:,0,:] = rotated_pos_offset + res.joint_position[frame_num, 0]
return res
def build_loop_motion(bvh_motion):
'''
将bvh动作变为循环动作
由于比较复杂,作为福利,不用自己实现
(当然你也可以自己实现试一下)
推荐阅读 https://theorangeduck.com/
Creating Looping Animations from Motion Capture
'''
res = bvh_motion.raw_copy()
from smooth_utils import build_loop_motion
return build_loop_motion(res)