BVH 文件解析和 FK 过程 您所在的位置:网站首页 动作捕捉文件格式 BVH 文件解析和 FK 过程

BVH 文件解析和 FK 过程

2024-07-17 20:28| 来源: 网络整理| 查看: 265

本文将对 BVH 文件进行讲解,用 Python 代码用递归下降的方式解析。解析完成后,通过前向运动学(Forward Kinematics)方法进行计算,并使用 panda3d 库进行播放。

BVH 文件介绍

BVH 是一种通用的人体特征动画文件格式,基于人体关节(Joint)的树状结构进行存储。

BVH 文件分为 Hierarchy 和 Motion 两部分, Hierarchy部分是描述虚拟角色的树形结构,Motion 部分是记录每一帧虚拟角色运动的姿态。下面是一个标准的 BVH 文件。

HIERARCHY ROOT RootJoint { OFFSET 0.000000 0.000000 0.000000 CHANNELS 6 Xposition Yposition Zposition Xrotation Yrotation Zrotation JOINT lHip { OFFSET 0.100000 -0.051395 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT lKnee { OFFSET 0.000000 -0.410000 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT lAnkle { OFFSET 0.000000 -0.390000 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT lToeJoint { OFFSET 0.000000 -0.050000 0.130000 CHANNELS 3 Xrotation Yrotation Zrotation End Site { OFFSET 0.010000 0.002000 0.060000 } } } } } JOINT pelvis_lowerback { OFFSET 0.000000 0.093605 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT lowerback_torso { OFFSET 0.000000 0.100000 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT lTorso_Clavicle { OFFSET 0.001000 0.157500 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT lShoulder { OFFSET 0.117647 0.000000 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT lElbow { OFFSET 0.245000 0.000000 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT lWrist { OFFSET 0.240000 0.000000 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation End Site { OFFSET 0.116353 -0.002500 0.000000 } } } } } JOINT rTorso_Clavicle { OFFSET -0.001000 0.157500 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT rShoulder { OFFSET -0.117647 0.000000 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT rElbow { OFFSET -0.245000 0.000000 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT rWrist { OFFSET -0.240000 0.000000 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation End Site { OFFSET -0.116353 -0.002500 0.000000 } } } } } JOINT torso_head { OFFSET 0.000000 0.282350 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation End Site { OFFSET 0.000000 0.192650 0.000000 } } } } JOINT rHip { OFFSET -0.100000 -0.051395 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT rKnee { OFFSET 0.000000 -0.410000 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT rAnkle { OFFSET 0.000000 -0.390000 0.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT rToeJoint { OFFSET 0.000000 -0.050000 0.130000 CHANNELS 3 Xrotation Yrotation Zrotation End 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.000000 CHANNELS 3 Xrotation Yrotation Zrotation JOINT rAnkle { ... } } OFFSET - 当前结点相对于父结点的相对位置;CHANNELS - 表示欧拉角的旋转顺序;JOINT - 表示子节点,可能有多个。

而 RootJoint 是一个根节点(Root):

ROOT RootJoint { OFFSET 0.000000 0.000000 0.000000 CHANNELS 6 Xposition Yposition Zposition Xrotation Yrotation Zrotation JOINT 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 = parent self.name = name self.offset = offset self.channel = channel # 6 self.children = [] class joint(object): def __init__(self, parent, name, offset, channel): self.parent = parent self.name = name self.offset = offset self.channel = channel # 3 self.children = [] class end(object): def __init__(self, parent, name, offset, channel): self.parent = parent self.name = name self.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 = 0 self.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'): break else: 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 += 2 if 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 += 1 if 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 += 1 while self.lines[self.line_number].startswith('JOINT'): self.parse_joint(0) self.line_number += 1 def 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 += 2 if 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 += 1 if 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 += 1 while 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 += 1 def parse_end(self, parent): self.joint_parents.append(parent) self.joint_names.append(self.joint_names[parent] + '_end') self.line_number += 2 if 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 += 1 if 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 R def 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 = 1 for i in range(m): if '_end' not in joint_name[i]: for j in range(3): rotations[i][j] = channels[cnt * 3 + j] cnt += 1 for 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 R class HierarchyParser(object): def __init__(self, bvh_file_path): self.lines = self.get_hierarchy_lines(bvh_file_path) self.line_number = 0 self.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'): break else: hierarchy_lines.append(line) return hierarchy_lines 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 += 2 if 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 += 1 if 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 += 1 while self.lines[self.line_number].startswith('JOINT'): self.parse_joint(0) self.line_number += 1 def 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 += 2 if 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 += 1 if 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 += 1 while 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 += 1 def parse_end(self, parent): self.joint_parents.append(parent) self.joint_names.append(self.joint_names[parent] + '_end') self.line_number += 2 if 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 def analyze(self): if not self.lines[self.line_number].startswith('HIERARCHY'): print('cannot find hierarchy') self.line_number += 1 if self.lines[self.line_number].startswith('ROOT'): self.parse_root() return self.joint_names, self.joint_parents, self.joint_offsets def 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 = 1 for i in range(m): if '_end' not in joint_name[i]: for j in range(3): rotations[i][j] = channels[cnt * 3 + j] cnt += 1 for 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 def 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'): 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 def animation(viewer, joint_names, joint_parents, joint_offsets, motion_data): frame_num = motion_data.shape[0] class UpdateHandle: def __init__(self): self.current_frame = 0 def 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_num handle = UpdateHandle() viewer.update_func = handle.update_func viewer.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()


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

      专题文章
        CopyRight 2018-2019 实验室设备网 版权所有