import re import numpy as np channelmap = { 'Xrotation': 'x', 'Yrotation': 'y', 'Zrotation': 'z' } channelmap_inv = { 'x': 'Xrotation', 'y': 'Yrotation', 'z': 'Zrotation', } ordermap = { 'x': 0, 'y': 1, 'z': 2, } def load(filename:str, order:str=None) -> dict: """Loads a BVH file. Args: filename (str): Path to the BVH file. order (str): The order of the rotation channels. (i.e."xyz") Returns: dict: A dictionary containing the following keys: * names (list)(jnum): The names of the joints. * parents (list)(jnum): The parent indices. * offsets (np.ndarray)(jnum, 3): The offsets of the joints. * rotations (np.ndarray)(fnum, jnum, 3) : The local coordinates of rotations of the joints. * positions (np.ndarray)(fnum, jnum, 3) : The positions of the joints. * order (str): The order of the channels. * frametime (float): The time between two frames. """ f = open(filename, "r") i = 0 active = -1 end_site = False # Create empty lists for saving parameters names = [] offsets = np.array([]).reshape((0, 3)) parents = np.array([], dtype=int) # Parse the file, line by line for line in f: if "HIERARCHY" in line: continue if "MOTION" in line: continue rmatch = re.match(r"ROOT (\w+)", line) if rmatch: names.append(rmatch.group(1)) offsets = np.append(offsets, np.array([[0, 0, 0]]), axis=0) parents = np.append(parents, active) active = (len(parents) - 1) continue if "{" in line: continue if "}" in line: if end_site: end_site = False else: active = parents[active] continue offmatch = re.match(r"\s*OFFSET\s+([\-\d\.e]+)\s+([\-\d\.e]+)\s+([\-\d\.e]+)", line) if offmatch: if not end_site: offsets[active] = np.array([list(map(float, offmatch.groups()))]) continue chanmatch = re.match(r"\s*CHANNELS\s+(\d+)", line) if chanmatch: channels = int(chanmatch.group(1)) if order is None: channelis = 0 if channels == 3 else 3 channelie = 3 if channels == 3 else 6 parts = line.split()[2 + channelis:2 + channelie] if any([p not in channelmap for p in parts]): continue order = "".join([channelmap[p] for p in parts]) continue jmatch = re.match("\s*JOINT\s+(\w+)", line) if jmatch: names.append(jmatch.group(1)) offsets = np.append(offsets, np.array([[0, 0, 0]]), axis=0) parents = np.append(parents, active) active = (len(parents) - 1) continue if "End Site" in line: end_site = True continue fmatch = re.match("\s*Frames:\s+(\d+)", line) if fmatch: fnum = int(fmatch.group(1)) positions = offsets[None].repeat(fnum, axis=0) rotations = np.zeros((fnum, len(offsets), 3)) continue fmatch = re.match("\s*Frame Time:\s+([\d\.]+)", line) if fmatch: frametime = float(fmatch.group(1)) continue dmatch = line.strip().split(' ') if dmatch: data_block = np.array(list(map(float, dmatch))) N = len(parents) fi = i if channels == 3: positions[fi, 0:1] = data_block[0:3] rotations[fi, :] = data_block[3:].reshape(N, 3) elif channels == 6: data_block = data_block.reshape(N, 6) positions[fi, :] = data_block[:, 0:3] rotations[fi, :] = data_block[:, 3:6] elif channels == 9: positions[fi, 0] = data_block[0:3] data_block = data_block[3:].reshape(N - 1, 9) rotations[fi, 1:] = data_block[:, 3:6] positions[fi, 1:] += data_block[:, 0:3] * data_block[:, 6:9] else: raise Exception("Too many channels! %i" % channels) i += 1 f.close() return { 'rotations': rotations, 'positions': positions, 'offsets': offsets, 'parents': parents, 'names': names, 'order': order, 'frametime': frametime } def save_joint(f, data, t, i, save_order, order='zyx', save_positions=False): save_order.append(i) f.write("%sJOINT %s\n" % (t, data['names'][i])) f.write("%s{\n" % t) t += '\t' f.write("%sOFFSET %f %f %f\n" % (t, data['offsets'][i,0], data['offsets'][i,1], data['offsets'][i,2])) if save_positions: f.write("%sCHANNELS 6 Xposition Yposition Zposition %s %s %s \n" % (t, channelmap_inv[order[0]], channelmap_inv[order[1]], channelmap_inv[order[2]])) else: f.write("%sCHANNELS 3 %s %s %s\n" % (t, channelmap_inv[order[0]], channelmap_inv[order[1]], channelmap_inv[order[2]])) end_site = True for j in range(len(data['parents'])): if data['parents'][j] == i: t = save_joint(f, data, t, j, save_order, order=order, save_positions=save_positions) end_site = False if end_site: f.write("%sEnd Site\n" % t) f.write("%s{\n" % t) t += '\t' f.write("%sOFFSET %f %f %f\n" % (t, 0.0, 0.0, 0.0)) t = t[:-1] f.write("%s}\n" % t) t = t[:-1] f.write("%s}\n" % t) return t def save(filename, data, save_positions=False): """ Save a joint hierarchy to a file. Args: filename (str): The output will save on the bvh file. data (dict): The data to save.(rotations, positions, offsets, parents, names, order, frametime) save_positions (bool): Whether to save all of joint positions on MOTION. (False is recommended.) """ order = data['order'] frametime = data['frametime'] with open(filename, 'w') as f: t = "" f.write("%sHIERARCHY\n" % t) f.write("%sROOT %s\n" % (t, data['names'][0])) f.write("%s{\n" % t) t += '\t' f.write("%sOFFSET %f %f %f\n" % (t, data['offsets'][0,0], data['offsets'][0,1], data['offsets'][0,2]) ) f.write("%sCHANNELS 6 Xposition Yposition Zposition %s %s %s \n" % (t, channelmap_inv[order[0]], channelmap_inv[order[1]], channelmap_inv[order[2]])) save_order = [0] for i in range(len(data['parents'])): if data['parents'][i] == 0: t = save_joint(f, data, t, i, save_order, order=order, save_positions=save_positions) t = t[:-1] f.write("%s}\n" % t) rots, poss = data['rotations'], data['positions'] f.write("MOTION\n") f.write("Frames: %i\n" % len(rots)); f.write("Frame Time: %f\n" % frametime); for i in range(rots.shape[0]): for j in save_order: if save_positions or j == 0: f.write("%f %f %f %f %f %f " % ( poss[i,j,0], poss[i,j,1], poss[i,j,2], rots[i,j,0], rots[i,j,1], rots[i,j,2])) else: f.write("%f %f %f " % ( rots[i,j,0], rots[i,j,1], rots[i,j,2])) f.write("\n")