欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

解析 BVH 文件与执行 FK 过程

最编程 2024-08-14 12:07:35
...

本文将对 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 部分有以下信息:

  1. Frames 表示接下来动画中帧的数量;

  2. Frame Time 表示帧率,即每帧持续时间;

  3. 接下来每一行代表一帧中的运动数据。这些数据, 是按照前面 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.pyGroundScene.eggcharacter_model.pywalk60.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
						

上一篇: games101 ray-tracing加速结构改进 BVH+SAH

下一篇: BVH格式的人体动作捕捉及其与三维坐标的转换

推荐阅读