├── Fig └── Example.png ├── README.md ├── LICENSE ├── parser.py └── view_bvh.py /Fig/Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TemugeB/Python_BVH_viewer/HEAD/Fig/Example.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python BVH viewer 2 | Simple python only BVH file viewer. This viewer is based on the BVH file parser written by 20tab: [https://github.com/20tab/bvh-python](https://github.com/20tab/bvh-python). 3 | 4 | ![Example_figure](Fig/Example.png?raw=true "Example") 5 | 6 | 7 | **Features**: 8 | Local space and world space coordinates are calculated frame by frame and plotted in 3D using matplotlib. A dictionary containing local and world space coordinates are calculated in the Draw_bvh function. If you need these coordinates only, simply save the contents of these variables to file. 9 | 10 | **Requirements**: 11 | Python 3.7 12 | Matplotlib 13 | 14 | **Usage**: 15 | Simply call as 16 | ``` 17 | python view_bvh.py /path/to/file.bvh 18 | ``` 19 | 20 | **Note**: 21 | By default, the figure only shows the bvh file in local space. If you want to see the motion in world space, uncomment the world space plotting code in Draw_bvh functions. Additionally, you may need to change the limits of the viewing window. This can be done by setting figure_limit to some large value in Draw_bvh function. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Temuge Batpurev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class BvhNode: 5 | 6 | def __init__(self, value=[], parent=None): 7 | self.value = value 8 | self.children = [] 9 | self.parent = parent 10 | if self.parent: 11 | self.parent.add_child(self) 12 | 13 | def add_child(self, item): 14 | item.parent = self 15 | self.children.append(item) 16 | 17 | def filter(self, key): 18 | for child in self.children: 19 | if child.value[0] == key: 20 | yield child 21 | 22 | def __iter__(self): 23 | for child in self.children: 24 | yield child 25 | 26 | def __getitem__(self, key): 27 | for child in self.children: 28 | for index, item in enumerate(child.value): 29 | if item == key: 30 | if index + 1 >= len(child.value): 31 | return None 32 | else: 33 | return child.value[index + 1:] 34 | raise IndexError('key {} not found'.format(key)) 35 | 36 | def __repr__(self): 37 | return str(' '.join(self.value)) 38 | 39 | @property 40 | def name(self): 41 | return self.value[1] 42 | 43 | 44 | class Bvh: 45 | 46 | def __init__(self, data): 47 | self.data = data 48 | self.root = BvhNode() 49 | self.frames = [] 50 | self.tokenize() 51 | 52 | def tokenize(self): 53 | first_round = [] 54 | accumulator = '' 55 | for char in self.data: 56 | if char not in ('\n', '\r'): 57 | accumulator += char 58 | elif accumulator: 59 | first_round.append(re.split('\\s+', accumulator.strip())) 60 | accumulator = '' 61 | node_stack = [self.root] 62 | frame_time_found = False 63 | node = None 64 | for item in first_round: 65 | if frame_time_found: 66 | self.frames.append(item) 67 | continue 68 | key = item[0] 69 | if key == '{': 70 | node_stack.append(node) 71 | elif key == '}': 72 | node_stack.pop() 73 | else: 74 | node = BvhNode(item) 75 | node_stack[-1].add_child(node) 76 | if item[0] == 'Frame' and item[1] == 'Time:': 77 | frame_time_found = True 78 | 79 | def search(self, *items): 80 | found_nodes = [] 81 | 82 | def check_children(node): 83 | if len(node.value) >= len(items): 84 | failed = False 85 | for index, item in enumerate(items): 86 | if node.value[index] != item: 87 | failed = True 88 | break 89 | if not failed: 90 | found_nodes.append(node) 91 | for child in node: 92 | check_children(child) 93 | check_children(self.root) 94 | return found_nodes 95 | 96 | def get_joints(self): 97 | joints = [] 98 | 99 | def iterate_joints(joint): 100 | joints.append(joint) 101 | for child in joint.filter('JOINT'): 102 | iterate_joints(child) 103 | iterate_joints(next(self.root.filter('ROOT'))) 104 | return joints 105 | 106 | def get_joints_names(self): 107 | joints = [] 108 | 109 | def iterate_joints(joint): 110 | joints.append(joint.value[1]) 111 | for child in joint.filter('JOINT'): 112 | iterate_joints(child) 113 | iterate_joints(next(self.root.filter('ROOT'))) 114 | return joints 115 | 116 | def joint_direct_children(self, name): 117 | joint = self.get_joint(name) 118 | return [child for child in joint.filter('JOINT')] 119 | 120 | def get_joint_index(self, name): 121 | return self.get_joints().index(self.get_joint(name)) 122 | 123 | def get_joint(self, name): 124 | found = self.search('ROOT', name) 125 | if not found: 126 | found = self.search('JOINT', name) 127 | if found: 128 | return found[0] 129 | raise LookupError('joint not found') 130 | 131 | def joint_offset(self, name): 132 | joint = self.get_joint(name) 133 | offset = joint['OFFSET'] 134 | return (float(offset[0]), float(offset[1]), float(offset[2])) 135 | 136 | def joint_channels(self, name): 137 | joint = self.get_joint(name) 138 | return joint['CHANNELS'][1:] 139 | 140 | def get_joint_channels_index(self, joint_name): 141 | index = 0 142 | for joint in self.get_joints(): 143 | if joint.value[1] == joint_name: 144 | return index 145 | index += int(joint['CHANNELS'][0]) 146 | raise LookupError('joint not found') 147 | 148 | def get_joint_channel_index(self, joint, channel): 149 | channels = self.joint_channels(joint) 150 | if channel in channels: 151 | channel_index = channels.index(channel) 152 | else: 153 | channel_index = -1 154 | return channel_index 155 | 156 | def frame_joint_channel(self, frame_index, joint, channel, value=None): 157 | joint_index = self.get_joint_channels_index(joint) 158 | channel_index = self.get_joint_channel_index(joint, channel) 159 | if channel_index == -1 and value is not None: 160 | return value 161 | return float(self.frames[frame_index][joint_index + channel_index]) 162 | 163 | def frame_joint_channels(self, frame_index, joint, channels, value=None): 164 | values = [] 165 | joint_index = self.get_joint_channels_index(joint) 166 | for channel in channels: 167 | channel_index = self.get_joint_channel_index(joint, channel) 168 | if channel_index == -1 and value is not None: 169 | values.append(value) 170 | else: 171 | values.append( 172 | float( 173 | self.frames[frame_index][joint_index + channel_index] 174 | ) 175 | ) 176 | return values 177 | 178 | def frames_joint_channels(self, joint, channels, value=None): 179 | all_frames = [] 180 | joint_index = self.get_joint_channels_index(joint) 181 | for frame in self.frames: 182 | values = [] 183 | for channel in channels: 184 | channel_index = self.get_joint_channel_index(joint, channel) 185 | if channel_index == -1 and value is not None: 186 | values.append(value) 187 | else: 188 | values.append( 189 | float(frame[joint_index + channel_index])) 190 | all_frames.append(values) 191 | return all_frames 192 | 193 | def joint_parent(self, name): 194 | joint = self.get_joint(name) 195 | if joint.parent == self.root: 196 | return None 197 | return joint.parent 198 | 199 | def joint_parent_index(self, name): 200 | joint = self.get_joint(name) 201 | if joint.parent == self.root: 202 | return -1 203 | return self.get_joints().index(joint.parent) 204 | 205 | @property 206 | def nframes(self): 207 | try: 208 | return int(next(self.root.filter('Frames:')).value[1]) 209 | except StopIteration: 210 | raise LookupError('number of frames not found') 211 | 212 | @property 213 | def frame_time(self): 214 | try: 215 | return float(next(self.root.filter('Frame')).value[2]) 216 | except StopIteration: 217 | raise LookupError('frame time not found') 218 | -------------------------------------------------------------------------------- /view_bvh.py: -------------------------------------------------------------------------------- 1 | from parser import Bvh 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import sys 5 | from mpl_toolkits.mplot3d import Axes3D 6 | 7 | def _separate_angles(frames, joints, joints_saved_channels): 8 | 9 | frame_i = 0 10 | joints_saved_angles = {} 11 | get_channels = [] 12 | for joint in joints: 13 | _saved_channels = joints_saved_channels[joint] 14 | 15 | saved_rotations = [] 16 | for chan in _saved_channels: 17 | if chan.lower().find('rotation') != -1: 18 | saved_rotations.append(chan) 19 | get_channels.append(frame_i) 20 | 21 | frame_i += 1 22 | joints_saved_angles[joint] = saved_rotations 23 | 24 | joints_rotations = frames[:,get_channels] 25 | 26 | return joints_rotations, joints_saved_angles 27 | 28 | def _separate_positions(frames, joints, joints_saved_channels): 29 | 30 | frame_i = 0 31 | joints_saved_positions = {} 32 | get_channels = [] 33 | for joint in joints: 34 | _saved_channels = joints_saved_channels[joint] 35 | 36 | saved_positions = [] 37 | for chan in _saved_channels: 38 | if chan.lower().find('position') != -1: 39 | saved_positions.append(chan) 40 | get_channels.append(frame_i) 41 | 42 | frame_i += 1 43 | joints_saved_positions[joint] = saved_positions 44 | 45 | 46 | if len(get_channels) == 3*len(joints): 47 | #print('all joints have saved positions') 48 | return frames[:,get_channels], joints_saved_positions 49 | 50 | #no positions saved for the joints or only some are saved. 51 | else: 52 | return np.array([]), joints_saved_positions 53 | 54 | pass 55 | 56 | def ProcessBVH(filename): 57 | 58 | with open(filename) as f: 59 | mocap = Bvh(f.read()) 60 | 61 | #get the names of the joints 62 | joints = mocap.get_joints_names() 63 | 64 | #this contains all of the frames data. 65 | frames = np.array(mocap.frames).astype('float32') 66 | 67 | #determine the structure of the skeleton and how the data was saved 68 | joints_offsets = {} 69 | joints_hierarchy = {} 70 | joints_saved_channels = {} 71 | for joint in joints: 72 | #get offsets. This is the length of skeleton body parts 73 | joints_offsets[joint] = np.array(mocap.joint_offset(joint)) 74 | 75 | #Some bvh files save only rotation channels while others also save positions. 76 | #the order of rotation is important 77 | joints_saved_channels[joint] = mocap.joint_channels(joint) 78 | 79 | #determine the hierarcy of each joint. 80 | joint_hierarchy = [] 81 | parent_joint = joint 82 | while True: 83 | parent_name = mocap.joint_parent(parent_joint) 84 | if parent_name == None:break 85 | 86 | joint_hierarchy.append(parent_name.name) 87 | parent_joint = parent_name.name 88 | 89 | joints_hierarchy[joint] = joint_hierarchy 90 | 91 | #seprate the rotation angles and the positions of joints 92 | joints_rotations, joints_saved_angles = _separate_angles(frames, joints, joints_saved_channels) 93 | joints_positions, joints_saved_positions = _separate_positions(frames, joints, joints_saved_channels) 94 | 95 | #root positions are always saved 96 | root_positions = frames[:, 0:3] 97 | 98 | return [joints, joints_offsets, joints_hierarchy, root_positions, joints_rotations, joints_saved_angles, joints_positions, joints_saved_positions] 99 | 100 | #rotation matrices 101 | def Rx(ang, in_radians = False): 102 | if in_radians == False: 103 | ang = np.radians(ang) 104 | 105 | Rot_Mat = np.array([ 106 | [1, 0, 0], 107 | [0, np.cos(ang), -1*np.sin(ang)], 108 | [0, np.sin(ang), np.cos(ang)] 109 | ]) 110 | return Rot_Mat 111 | 112 | def Ry(ang, in_radians = False): 113 | if in_radians == False: 114 | ang = np.radians(ang) 115 | 116 | Rot_Mat = np.array([ 117 | [np.cos(ang), 0, np.sin(ang)], 118 | [0, 1, 0], 119 | [-1*np.sin(ang), 0, np.cos(ang)] 120 | ]) 121 | return Rot_Mat 122 | 123 | def Rz(ang, in_radians = False): 124 | if in_radians == False: 125 | ang = np.radians(ang) 126 | 127 | Rot_Mat = np.array([ 128 | [np.cos(ang), -1*np.sin(ang), 0], 129 | [np.sin(ang), np.cos(ang), 0], 130 | [0, 0, 1] 131 | ]) 132 | return Rot_Mat 133 | 134 | #the rotation matrices need to be chained according to the order in the file 135 | def _get_rotation_chain(joint_channels, joint_rotations): 136 | 137 | #the rotation matrices are constructed in the order given in the file 138 | Rot_Mat = np.array([[1,0,0],[0,1,0],[0,0,1]])#identity matrix 3x3 139 | order = '' 140 | index = 0 141 | for chan in joint_channels: #if file saves xyz ordered rotations, then rotation matrix must be chained as R_x @ R_y @ R_z 142 | if chan[0].lower() == 'x': 143 | Rot_Mat = Rot_Mat @ Rx(joint_rotations[index]) 144 | order += 'x' 145 | 146 | elif chan[0].lower() == 'y': 147 | Rot_Mat = Rot_Mat @ Ry(joint_rotations[index]) 148 | order += 'y' 149 | 150 | elif chan[0].lower() == 'z': 151 | Rot_Mat = Rot_Mat @ Rz(joint_rotations[index]) 152 | order += 'z' 153 | index += 1 154 | #print(order) 155 | return Rot_Mat 156 | 157 | #Here root position is used as local coordinate origin. 158 | def _calculate_frame_joint_positions_in_local_space(joints, joints_offsets, frame_joints_rotations, joints_saved_angles, joints_hierarchy): 159 | 160 | local_positions = {} 161 | 162 | for joint in joints: 163 | 164 | #ignore root joint and set local coordinate to (0,0,0) 165 | if joint == joints[0]: 166 | local_positions[joint] = [0,0,0] 167 | continue 168 | 169 | connected_joints = joints_hierarchy[joint] 170 | connected_joints = connected_joints[::-1] 171 | connected_joints.append(joint) #this contains the chain of joints that finally end with the current joint that we want the coordinate of. 172 | Rot = np.eye(3) 173 | pos = [0,0,0] 174 | for i, con_joint in enumerate(connected_joints): 175 | if i == 0: 176 | pass 177 | else: 178 | parent_joint = connected_joints[i - 1] 179 | Rot = Rot @ _get_rotation_chain(joints_saved_angles[parent_joint], frame_joints_rotations[parent_joint]) 180 | joint_pos = joints_offsets[con_joint] 181 | joint_pos = Rot @ joint_pos 182 | pos = pos + joint_pos 183 | 184 | local_positions[joint] = pos 185 | 186 | return local_positions 187 | 188 | def _calculate_frame_joint_positions_in_world_space(local_positions, root_position, root_rotation, saved_angles): 189 | 190 | world_pos = {} 191 | for joint in local_positions: 192 | pos = local_positions[joint] 193 | 194 | Rot = _get_rotation_chain(saved_angles, root_rotation) 195 | pos = Rot @ pos 196 | 197 | pos = np.array(root_position) + pos 198 | world_pos[joint] = pos 199 | 200 | return world_pos 201 | 202 | 203 | def Draw_bvh(joints, joints_offsets, joints_hierarchy, root_positions, joints_rotations, joints_saved_angles): 204 | 205 | fig = plt.figure() 206 | ax = fig.add_subplot(111, projection='3d') 207 | 208 | frame_joints_rotations = {en:[] for en in joints} 209 | 210 | """ 211 | Number of frames skipped is controlled with this variable below. If you want all frames, set to 1. 212 | """ 213 | frame_skips = 5 214 | 215 | figure_limit = None #used to set figure axis limits 216 | 217 | for i in range(0,len(joints_rotations), frame_skips): 218 | 219 | frame_data = joints_rotations[i] 220 | 221 | #fill in the rotations dict 222 | joint_index = 0 223 | for joint in joints: 224 | frame_joints_rotations[joint] = frame_data[joint_index:joint_index+3] 225 | joint_index += 3 226 | 227 | #this returns a dictionary of joint positions in local space. This can be saved to file to get the joint positions. 228 | local_pos = _calculate_frame_joint_positions_in_local_space(joints, joints_offsets, frame_joints_rotations, joints_saved_angles, joints_hierarchy) 229 | 230 | #calculate world positions 231 | world_pos = _calculate_frame_joint_positions_in_world_space(local_pos, root_positions[i], frame_joints_rotations[joints[0]], joints_saved_angles[joints[0]]) 232 | 233 | #calculate the limits of the figure. Usually the last joint in the dictionary is one of the feet. 234 | if figure_limit == None: 235 | lim_min = np.abs(np.min(local_pos[list(local_pos)[-1]])) 236 | lim_max = np.abs(np.max(local_pos[list(local_pos)[-1]])) 237 | lim = lim_min if lim_min > lim_max else lim_max 238 | figure_limit = lim 239 | 240 | for joint in joints: 241 | if joint == joints[0]: continue #skip root joint 242 | parent_joint = joints_hierarchy[joint][0] 243 | plt.plot(xs = [local_pos[parent_joint][0], local_pos[joint][0]], 244 | zs = [local_pos[parent_joint][1], local_pos[joint][1]], 245 | ys = [local_pos[parent_joint][2], local_pos[joint][2]], c = 'blue', lw = 2.5) 246 | 247 | #uncomment here if you want to see the world coords. If nothing appears on screen, change the axis limits below! 248 | # plt.plot(xs = [world_pos[parent_joint][0], world_pos[joint][0]], 249 | # zs = [world_pos[parent_joint][1], world_pos[joint][1]], 250 | # ys = [world_pos[parent_joint][2], world_pos[joint][2]], c = 'red', lw = 2.5) 251 | 252 | #Depending on the file, the axis limits might be too small or too big. Change accordingly. 253 | ax.set_axis_off() 254 | ax.set_xlim(-0.6*figure_limit, 0.6*figure_limit) 255 | ax.set_ylim(-0.6*figure_limit, 0.6*figure_limit) 256 | ax.set_zlim(-0.2*figure_limit, 1.*figure_limit) 257 | plt.title('frame: ' + str(i)) 258 | plt.pause(0.001) 259 | ax.cla() 260 | 261 | pass 262 | 263 | 264 | if __name__ == "__main__": 265 | 266 | if len(sys.argv) != 2: 267 | print('Call the function with the BVH file') 268 | quit() 269 | 270 | filename = sys.argv[1] 271 | skeleton_data = ProcessBVH(filename) 272 | 273 | joints = skeleton_data[0] 274 | joints_offsets = skeleton_data[1] 275 | joints_hierarchy = skeleton_data[2] 276 | root_positions = skeleton_data[3] 277 | joints_rotations = skeleton_data[4] #this contains the angles in degrees 278 | joints_saved_angles = skeleton_data[5] #this contains channel information. E.g ['Xrotation', 'Yrotation', 'Zrotation'] 279 | joints_positions = skeleton_data[6] 280 | joints_saved_positions = skeleton_data[7] 281 | 282 | Draw_bvh(joints, joints_offsets, joints_hierarchy, root_positions, joints_rotations, joints_saved_angles) 283 | --------------------------------------------------------------------------------