BVH 文件解析和 FK 过程
本文将对 BVH 文件进行讲解,用 Python 代码用递归下降的方式解析。解析完成后,通过前向运动学(Forward Kinematics)方法进行计算,并使用 panda3d 库进行播放。
BVH 文件介绍
BVH 是一种通用的人体特征动画文件格式,基于人体关节(Joint)的树状结构进行存储。
BVH 文件分为 Hierarchy 和 Motion 两部分, Hierarchy部分是描述虚拟角色的树形结构,Motion 部分是记录每一帧虚拟角色运动的姿态。下面是一个标准的 BVH 文件。
HIERARCHY
ROOT RootJoint
{OFFSET 0.000000 0.000000 0.000000CHANNELS 6 Xposition Yposition Zposition Xrotation Yrotation ZrotationJOINT lHip{OFFSET 0.100000 -0.051395 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT lKnee{OFFSET 0.000000 -0.410000 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT lAnkle{OFFSET 0.000000 -0.390000 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT lToeJoint{OFFSET 0.000000 -0.050000 0.130000CHANNELS 3 Xrotation Yrotation ZrotationEnd Site{OFFSET 0.010000 0.002000 0.060000}}}}}JOINT pelvis_lowerback{OFFSET 0.000000 0.093605 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT lowerback_torso{OFFSET 0.000000 0.100000 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT lTorso_Clavicle{OFFSET 0.001000 0.157500 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT lShoulder{OFFSET 0.117647 0.000000 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT lElbow{OFFSET 0.245000 0.000000 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT lWrist{OFFSET 0.240000 0.000000 0.000000CHANNELS 3 Xrotation Yrotation ZrotationEnd Site{OFFSET 0.116353 -0.002500 0.000000}}}}}JOINT rTorso_Clavicle{OFFSET -0.001000 0.157500 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT rShoulder{OFFSET -0.117647 0.000000 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT rElbow{OFFSET -0.245000 0.000000 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT rWrist{OFFSET -0.240000 0.000000 0.000000CHANNELS 3 Xrotation Yrotation ZrotationEnd Site{OFFSET -0.116353 -0.002500 0.000000}}}}}JOINT torso_head{OFFSET 0.000000 0.282350 0.000000CHANNELS 3 Xrotation Yrotation ZrotationEnd Site{OFFSET 0.000000 0.192650 0.000000}}}}JOINT rHip{OFFSET -0.100000 -0.051395 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT rKnee{OFFSET 0.000000 -0.410000 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT rAnkle{OFFSET 0.000000 -0.390000 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT rToeJoint{OFFSET 0.000000 -0.050000 0.130000CHANNELS 3 Xrotation Yrotation ZrotationEnd Site{OFFSET -0.010000 0.002000 0.060000}}}}}
}
MOTION
Frames: 2
Frame Time: 0.016667
-0.001735 0.855388 0.315499 2.008551 7.606260 -0.798294 11.216058 -3.286777 -1.592436 13.521250 -1.153514 -4.213484 -17.754157 -3.216621 9.232892 -7.948705 0.211932 -1.528529 2.220789 -0.981058 -1.133630 2.071938 -6.311876 2.083844 2.020309 -0.533885 -19.342332 -5.129554 -37.575293 -50.190804 0.198025 -24.741038 4.442069 0.442380 2.547494 4.858004 1.951773 -5.809334 21.100535 23.710456 30.003467 53.240376 0.414981 10.414544 1.952633 3.576914 -9.482057 6.918939 1.457480 -0.035296 0.111891 -27.722826 -1.655032 2.430426 -2.964232 -5.507982 1.444119 2.239212 -3.180259 -0.892285 -0.008100 -0.007000 0.024400
-0.003810 0.853981 0.337002 2.017405 7.825929 -1.809751 11.713970 -2.355625 0.062023 15.198954 -1.861308 -4.389417 -17.189762 -3.614663 9.244711 -9.397213 0.262158 -1.565413 2.647300 -1.021514 0.131973 1.458470 -6.632789 1.868957 1.928817 -0.148344 -19.543616 -3.937845 -37.139413 -49.957499 0.204371 -24.672734 4.317351 0.916151 2.440320 4.849158 2.068848 -5.518149 21.184327 23.795785 30.805519 52.865110 0.417817 10.118764 1.952408 3.104611 -9.774695 7.021717 1.448893 -0.004226 0.069402 -27.594885 -2.728812 3.499348 -1.670271 -5.527619 1.835016 6.676684 -3.330738 -4.015991 -0.008600 -0.007000 0.026400
Hierarchy 部分
Hierarchy 描述了骨骼的树形结构,比如 rKnee 是一个关节(Joint):
JOINT rKnee
{OFFSET 0.000000 -0.410000 0.000000CHANNELS 3 Xrotation Yrotation ZrotationJOINT rAnkle{...}
}
- OFFSET - 当前结点相对于父结点的相对位置;
- CHANNELS - 表示欧拉角的旋转顺序;
- JOINT - 表示子节点,可能有多个。
而 RootJoint 是一个根节点(Root):
ROOT RootJoint
{OFFSET 0.000000 0.000000 0.000000CHANNELS 6 Xposition Yposition Zposition Xrotation Yrotation ZrotationJOINT lHip{...}
}
ROOT 与 JOINT 的不同之处在于 CHANNELS 属性有6个维度,前三维是该骨骼对应的 X, Y, Z 三个轴的顺序。一般来说,根节点的 OFFSET 为 (0, 0, 0) 。
END Site 是骨骼的末端,即骨骼树的叶子结点。
End Site
{OFFSET -0.010000 0.002000 0.060000
}
显然,只需要 OFFSET 即可表示。
Motion 部分
Motion 部分有以下信息:
-
Frames 表示接下来动画中帧的数量;
-
Frame Time 表示帧率,即每帧持续时间;
-
接下来每一行代表一帧中的运动数据。这些数据, 是按照前面 CHANNEL 定义顺序出现的. 按照上面 BVH 结构的定义, 首先是根关节的平移量:Xposition, Yposition, Zposition, 接下来是根关节的旋转量:Xrotation, Yrotation, Zrotation ,然后是各个关节的旋转量。
Frames: 2
Frame Time: 0.016667
-0.001735 0.855388 0.315499 2.008551 7.606260 -0.798294 11.216058 -3.286777 -1.592436 13.521250 -1.153514 -4.213484 -17.754157 -3.216621 9.232892 -7.948705 0.211932 -1.528529 2.220789 -0.981058 -1.133630 2.071938 -6.311876 2.083844 2.020309 -0.533885 -19.342332 -5.129554 -37.575293 -50.190804 0.198025 -24.741038 4.442069 0.442380 2.547494 4.858004 1.951773 -5.809334 21.100535 23.710456 30.003467 53.240376 0.414981 10.414544 1.952633 3.576914 -9.482057 6.918939 1.457480 -0.035296 0.111891 -27.722826 -1.655032 2.430426 -2.964232 -5.507982 1.444119 2.239212 -3.180259 -0.892285 -0.008100 -0.007000 0.024400
...
总而言之,每个 CHANNEL 按照顺序对应 Motion 中的每个数据。
BVH 文件解析
根据上述说明,对于 Hierarchy 部分,我们建立的骨骼树应该包括以下三种结点:
class root(object):def __init__(self, parent, name, offset, channel):self.parent = parentself.name = nameself.offset = offsetself.channel = channel # 6self.children = []class joint(object):def __init__(self, parent, name, offset, channel):self.parent = parentself.name = nameself.offset = offsetself.channel = channel # 3self.children = []class end(object):def __init__(self, parent, name, offset, channel):self.parent = parentself.name = nameself.offset = offset
但是实际上用类的方式来储存和访问各个结点情况过于冗余了:因为骨骼树结构很简单,而且自上而下可以给每个关节都赋予一个编号,用数组记录每个关节对应的 name, parent, offset 以及 channel 的情况即可表达所有的骨骼树信息。
为了解析 Hierarchy 部分,我们先定义一个 hierarchy_parser 类并预处理得到 HIERARCHY 部分:
class hierarchy_parser(object):def __init__(self, bvh_file_path):self.lines = get_hierarchy_lines(bvh_file_path)self.line_number = 0self.root_position_channel = []self.joint_rotation_channels = []self.joint_names = []self.joint_parents = []self.joint_offsets = []def get_hierarchy_lines(bvh_file_path):hierarchy_lines = []for line in open(bvh_file_path, 'r'):line = line.strip()if line.startwith('MOTION'):breakelse:hierarchy_lines.append(line)return hierarchy_lines
然后用递归下降的思想,分别编写三种类的解析函数:
class hierarchy_parser(object): # ...def parse_offset(self, line):return [float(x) for x in line.split()[1:]]def parse_channels(self, line):return [x for x in line.split()[2:]]def parse_root(self, parent=-1):self.joint_parents.append(parent)self.joint_names.append(self.lines[self.line_number].split()[1])self.line_number += 2if self.lines[self.line_number].startswith('OFFSET'):self.joint_offsets.append(self.parse_offset(self.lines[self.line_number]))else:print('cannot find root offset')self.line_number += 1if self.lines[self.line_number].startswith('CHANNELS'):channels = self.parse_channels(self.lines[self.line_number])if self.lines[self.line_number].split()[1] == '3':self.joint_rotation_channels.append((channels[0], channels[1], channels[2]))elif self.lines[self.line_number].split()[1] == '6':self.root_position_channels.append((channels[0], channels[1], channels[2]))self.joint_rotation_channels.append((channels[3], channels[4], channels[5]))else:print('cannot find root channels')self.line_number += 1while self.lines[self.line_number].startswith('JOINT'):self.parse_joint(0)self.line_number += 1def parse_joint(self, parent):self.joint_parents.append(parent)index = len(self.joint_names)self.joint_names.append(self.lines[self.line_number].split()[1])self.line_number += 2if self.lines[self.line_number].startswith('OFFSET'):self.joint_offsets.append(self.parse_offset(self.lines[self.line_number]))else:print('cannot find joint offset')self.line_number += 1if self.lines[self.line_number].startswith('CHANNELS'):channels = self.parse_channels(self.lines[self.line_number])if self.lines[self.line_number].split()[1] == '3':self.joint_rotation_channels.append((channels[0], channels[1], channels[2]))else:print('cannot find joint channels')self.line_number += 1while self.lines[self.line_number].startswith('JOINT') or \self.lines[self.line_number].startswith('End'):if self.lines[self.line_number].startswith('JOINT'):self.parse_joint(index)elif self.lines[self.line_number].startswith('End'):self.parse_end(index)self.line_number += 1def parse_end(self, parent):self.joint_parents.append(parent)self.joint_names.append(self.joint_names[parent] + '_end')self.line_number += 2if self.lines[self.line_number].startswith('OFFSET'):self.joint_offsets.append(self.parse_offset(self.lines[self.line_number]))else:print('cannot find joint offset')self.line_number += 2
最后提供一个解析的入口:
class hierarchy_parser(object): # ...def analyze(self):if not self.lines[self.line_number].startswith('HIERARCHY'):print('cannot find hierarchy')self.line_number += 1if self.lines[self.line_number].startswith('ROOT'):self.parse_root()return self.joint_names, self.joint_parents, self.joint_offsets
前向运动学(Forward Kinematics)
对于骨骼树,要想确定每个关节在每一帧的位置,应该从 Root 结点开始,向下遍历计算每个关节的旋转,从而得到每个关节的位置。
因此,我们可以通过对树进行解析的方式得到每一帧下每个关节的全局旋转和全局坐标。由于我的 BVH 文件所有 CHANNEL 的顺序都是 (X, Y, Z) ,因此并未处理其它情况,如果有特殊情况需要注意。
import numpy as np
from scipy.spatial.transform import Rotation as Rdef forward_kinematics(joint_name, joint_parent, joint_offset, motion_data, frame_id):m = len(joint_name)joint_positions = np.zeros((m, 3), dtype=np.float64)joint_orientations = np.zeros((m, 4), dtype=np.float64)channels = motion_data[frame_id]rotations = np.zeros((m, 3), dtype=np.float64)cnt = 1for i in range(m):if '_end' not in joint_name[i]:for j in range(3):rotations[i][j] = channels[cnt * 3 + j]cnt += 1for i in range(m):parent = joint_parent[i]if parent == -1:for j in range(3):joint_positions[0][j] = channels[j]joint_orientations[0] = R.from_euler('XYZ', [rotations[0][0], \rotations[0][1], rotations[0][2]], degrees=True).as_quat()else:if '_end' in joint_name[i]:joint_orientations[i] = np.array([0, 0, 0, 1])joint_positions[i] = joint_positions[parent] + \R.from_quat(joint_orientations[parent]).as_matrix() @ joint_offset[i]else:rotation = R.from_euler('XYZ', [rotations[i][0], \rotations[i][1], rotations[i][2]], degrees=True)joint_orientations[i] = (R.from_quat(joint_orientations[parent]) * rotation).as_quat()joint_positions[i] = joint_positions[parent] + \R.from_quat(joint_orientations[parent]).as_matrix() @ joint_offset[i]return joint_positions, joint_orientations
计算中涉及一些四元数(quaternion)的相关知识,可以通过 四元数和旋转 稍作了解。
动画播放
这部分内容参考了 GAMES105 课程。首先安装依赖库,pip install panda3d 。
然后直接将仓库中的 viewer.py 、GroundScene.egg 、character_model.py 和 walk60.bvh 和文件放在同一文件夹下,调用运行即可。
Hibiki33/BVHPlayer
完整的代码如下:
from viewer import SimpleViewer
import numpy as np
from scipy.spatial.transform import Rotation as Rclass HierarchyParser(object):def __init__(self, bvh_file_path):self.lines = self.get_hierarchy_lines(bvh_file_path)self.line_number = 0self.root_position_channels = []self.joint_rotation_channels = []self.joint_names = []self.joint_parents = []self.joint_offsets = []def get_hierarchy_lines(self, bvh_file_path):hierarchy_lines = []for line in open(bvh_file_path, 'r'):line = line.strip()if line.startswith('MOTION'):breakelse:hierarchy_lines.append(line)return hierarchy_linesdef parse_offset(self, line):return [float(x) for x in line.split()[1:]]def parse_channels(self, line):return [x for x in line.split()[2:]]def parse_root(self, parent=-1):self.joint_parents.append(parent)self.joint_names.append(self.lines[self.line_number].split()[1])self.line_number += 2if self.lines[self.line_number].startswith('OFFSET'):self.joint_offsets.append(self.parse_offset(self.lines[self.line_number]))else:print('cannot find root offset')self.line_number += 1if self.lines[self.line_number].startswith('CHANNELS'):channels = self.parse_channels(self.lines[self.line_number])if self.lines[self.line_number].split()[1] == '3':self.joint_rotation_channels.append((channels[0], channels[1], channels[2]))elif self.lines[self.line_number].split()[1] == '6':self.root_position_channels.append((channels[0], channels[1], channels[2]))self.joint_rotation_channels.append((channels[3], channels[4], channels[5]))else:print('cannot find root channels')self.line_number += 1while self.lines[self.line_number].startswith('JOINT'):self.parse_joint(0)self.line_number += 1def parse_joint(self, parent):self.joint_parents.append(parent)index = len(self.joint_names)self.joint_names.append(self.lines[self.line_number].split()[1])self.line_number += 2if self.lines[self.line_number].startswith('OFFSET'):self.joint_offsets.append(self.parse_offset(self.lines[self.line_number]))else:print('cannot find joint offset')self.line_number += 1if self.lines[self.line_number].startswith('CHANNELS'):channels = self.parse_channels(self.lines[self.line_number])if self.lines[self.line_number].split()[1] == '3':self.joint_rotation_channels.append((channels[0], channels[1], channels[2]))else:print('cannot find joint channels')self.line_number += 1while self.lines[self.line_number].startswith('JOINT') or \self.lines[self.line_number].startswith('End'):if self.lines[self.line_number].startswith('JOINT'):self.parse_joint(index)elif self.lines[self.line_number].startswith('End'):self.parse_end(index)self.line_number += 1def parse_end(self, parent):self.joint_parents.append(parent)self.joint_names.append(self.joint_names[parent] + '_end')self.line_number += 2if self.lines[self.line_number].startswith('OFFSET'):self.joint_offsets.append(self.parse_offset(self.lines[self.line_number]))else:print('cannot find joint offset')self.line_number += 2def analyze(self):if not self.lines[self.line_number].startswith('HIERARCHY'):print('cannot find hierarchy')self.line_number += 1if self.lines[self.line_number].startswith('ROOT'):self.parse_root()return self.joint_names, self.joint_parents, self.joint_offsetsdef forward_kinematics(joint_name, joint_parent, joint_offset, motion_data, frame_id):m = len(joint_name)joint_positions = np.zeros((m, 3), dtype=np.float64)joint_orientations = np.zeros((m, 4), dtype=np.float64)channels = motion_data[frame_id]rotations = np.zeros((m, 3), dtype=np.float64)cnt = 1for i in range(m):if '_end' not in joint_name[i]:for j in range(3):rotations[i][j] = channels[cnt * 3 + j]cnt += 1for i in range(m):parent = joint_parent[i]if parent == -1:for j in range(3):joint_positions[0][j] = channels[j]joint_orientations[0] = R.from_euler('XYZ', [rotations[0][0], rotations[0][1], rotations[0][2]], degrees=True).as_quat()else:if '_end' in joint_name[i]:joint_orientations[i] = np.array([0, 0, 0, 1])joint_positions[i] = joint_positions[parent] + R.from_quat(joint_orientations[parent]).as_matrix() @ joint_offset[i]else:rotation = R.from_euler('XYZ', [rotations[i][0], rotations[i][1], rotations[i][2]], degrees=True)joint_orientations[i] = (R.from_quat(joint_orientations[parent]) * rotation).as_quat()joint_positions[i] = joint_positions[parent] + R.from_quat(joint_orientations[parent]).as_matrix() @ joint_offset[i]return joint_positions, joint_orientationsdef load_motion_data(bvh_file_path):with open(bvh_file_path, 'r') as f:lines = f.readlines()for i in range(len(lines)):if lines[i].startswith('Frame Time'):breakmotion_data = []for line in lines[i+1:]:data = [float(x) for x in line.split()]if len(data) == 0:breakmotion_data.append(np.array(data).reshape(1,-1))motion_data = np.concatenate(motion_data, axis=0)return motion_datadef animation(viewer, joint_names, joint_parents, joint_offsets, motion_data):frame_num = motion_data.shape[0]class UpdateHandle:def __init__(self):self.current_frame = 0def update_func(self, viewer_):joint_positions, joint_orientations = forward_kinematics(joint_names, \joint_parents, joint_offsets, motion_data, self.current_frame)viewer.show_pose(joint_names, joint_positions, joint_orientations)self.current_frame = (self.current_frame + 1) % frame_numhandle = UpdateHandle()viewer.update_func = handle.update_funcviewer.run()def main():bvh_file_path = 'walk60.bvh'viewer = SimpleViewer()parser = HierarchyParser(bvh_file_path)joint_names, joint_parents, joint_offsets = parser.analyze()motion_data = load_motion_data(bvh_file_path)animation(viewer, joint_names, joint_parents, joint_offsets, motion_data)if __name__ == "__main__":main()
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
