EasyMocap/scripts/postprocess/convert2bvh.py

261 lines
9.7 KiB
Python

'''
@ Date: 2020-07-27 16:51:24
@ Author: Qing Shuai
@ LastEditors: Qing Shuai
@ LastEditTime: 2021-03-13 21:54:03
@ FilePath: /EasyMocapRelease/scripts/postprocess/convert2bvh.py
'''
import sys
import bpy
from os.path import join
import math
import numpy as np
from mathutils import Matrix, Vector, Quaternion, Euler
def deg2rad(angle):
return -np.pi * (angle + 90) / 180.
part_match = {'root': 'root', 'bone_00': 'Pelvis', 'bone_01': 'L_Hip', 'bone_02': 'R_Hip',
'bone_03': 'Spine1', 'bone_04': 'L_Knee', 'bone_05': 'R_Knee', 'bone_06': 'Spine2',
'bone_07': 'L_Ankle', 'bone_08': 'R_Ankle', 'bone_09': 'Spine3', 'bone_10': 'L_Foot',
'bone_11': 'R_Foot', 'bone_12': 'Neck', 'bone_13': 'L_Collar', 'bone_14': 'R_Collar',
'bone_15': 'Head', 'bone_16': 'L_Shoulder', 'bone_17': 'R_Shoulder', 'bone_18': 'L_Elbow',
'bone_19': 'R_Elbow', 'bone_20': 'L_Wrist', 'bone_21': 'R_Wrist', 'bone_22': 'L_Hand', 'bone_23': 'R_Hand'}
def init_location(cam, theta, r):
# Originally, theta is negtivate
# the center of circle coord is (-1, 0), r is np.random.normal(8, 1)
x, z = (math.cos(theta) * r, math.sin(theta) * r)
cam.location = Vector((x, -2, z))
cam.rotation_euler = Euler((-np.pi / 20, -np.pi / 2 - theta, 0))
cam.scale = Vector((-1, -1, -1))
return cam
def init_scene(scene, params, gender='male', angle=0):
# load fbx model
bpy.ops.import_scene.fbx(filepath=join(params['smpl_data_folder'], 'basicModel_%s_lbs_10_207_0_v1.0.2.fbx' % gender[0]), axis_forward='-Y', axis_up='-Z', global_scale=100)
print('success load')
obname = '%s_avg' % gender[0]
ob = bpy.data.objects[obname]
ob.data.use_auto_smooth = False # autosmooth creates artifacts
# assign the existing spherical harmonics material
ob.active_material = bpy.data.materials['Material']
# delete the default cube (which held the material)
bpy.ops.object.select_all(action='DESELECT')
bpy.data.objects['Cube'].select_set(True)
bpy.ops.object.delete(use_global=False)
# set camera properties and initial position
bpy.ops.object.select_all(action='DESELECT')
cam_ob = bpy.data.objects['Camera']
scn = bpy.context.scene
bpy.context.view_layer.objects.active = cam_ob
th = deg2rad(angle)
# cam_ob = init_location(cam_ob, th, params['camera_distance'])
'''
cam_ob.matrix_world = Matrix(((0., 0., 1, params['camera_distance']+dis),
(0., -1, 0., -1.0),
(-1., 0., 0., 0.),
(0.0, 0.0, 0.0, 1.0)))
'''
cam_ob.data.angle = math.radians(60)
cam_ob.data.lens = 60
cam_ob.data.clip_start = 0.1
cam_ob.data.sensor_width = 32
# setup an empty object in the center which will be the parent of the Camera
# this allows to easily rotate an object around the origin
scn.cycles.film_transparent = True
bpy.context.view_layer.use_pass_vector = True
bpy.context.view_layer.use_pass_normal = True
bpy.context.view_layer.use_pass_emit = True
bpy.context.view_layer.use_pass_material_index = True
# set render size
# scn.render.resolution_x = params['resy']
# scn.render.resolution_y = params['resx']
scn.render.resolution_percentage = 100
scn.render.image_settings.file_format = 'PNG'
# clear existing animation data
ob.data.shape_keys.animation_data_clear()
arm_ob = bpy.data.objects['Armature']
arm_ob.animation_data_clear()
return(ob, obname, arm_ob, cam_ob)
def setState0():
for ob in bpy.data.objects.values():
ob.select_set(False)
bpy.context.view_layer.objects.active = None
def Rodrigues(rotvec):
theta = np.linalg.norm(rotvec)
r = (rotvec/theta).reshape(3, 1) if theta > 0. else rotvec
cost = np.cos(theta)
mat = np.asarray([[0, -r[2], r[1]],
[r[2], 0, -r[0]],
[-r[1], r[0], 0]])
return(cost*np.eye(3) + (1-cost)*r.dot(r.T) + np.sin(theta)*mat)
def rodrigues2bshapes(pose):
rod_rots = np.asarray(pose).reshape(24, 3)
mat_rots = [Rodrigues(rod_rot) for rod_rot in rod_rots]
bshapes = np.concatenate([(mat_rot - np.eye(3)).ravel()
for mat_rot in mat_rots[1:]])
return(mat_rots, bshapes)
# apply trans pose and shape to character
def apply_trans_pose_shape(trans, pose, shape, ob, arm_ob, obname, scene, cam_ob, frame=None):
# transform pose into rotation matrices (for pose) and pose blendshapes
mrots, bsh = rodrigues2bshapes(pose)
# set the location of the first bone to the translation parameter
arm_ob.pose.bones[obname+'_Pelvis'].location = trans
arm_ob.pose.bones[obname+'_root'].location = trans
arm_ob.pose.bones[obname +'_root'].keyframe_insert('location', frame=frame)
# set the pose of each bone to the quaternion specified by pose
for ibone, mrot in enumerate(mrots):
bone = arm_ob.pose.bones[obname+'_'+part_match['bone_%02d' % ibone]]
bone.rotation_quaternion = Matrix(mrot).to_quaternion()
if frame is not None:
bone.keyframe_insert('rotation_quaternion', frame=frame)
bone.keyframe_insert('location', frame=frame)
# apply pose blendshapes
for ibshape, bshape in enumerate(bsh):
ob.data.shape_keys.key_blocks['Pose%03d' % ibshape].value = bshape
if frame is not None:
ob.data.shape_keys.key_blocks['Pose%03d' % ibshape].keyframe_insert(
'value', index=-1, frame=frame)
# apply shape blendshapes
for ibshape, shape_elem in enumerate(shape):
ob.data.shape_keys.key_blocks['Shape%03d' % ibshape].value = shape_elem
if frame is not None:
ob.data.shape_keys.key_blocks['Shape%03d' % ibshape].keyframe_insert(
'value', index=-1, frame=frame)
import os
import json
def read_json(path):
with open(path) as f:
data = json.load(f)
return data
def read_smpl(outname):
assert os.path.exists(outname), outname
datas = read_json(outname)
outputs = []
if isinstance(datas, dict):
datas = datas['annots']
for data in datas:
for key in ['Rh', 'Th', 'poses', 'shapes']:
data[key] = np.array(data[key])
outputs.append(data)
return outputs
def merge_params(param_list, share_shape=True):
output = {}
for key in ['poses', 'shapes', 'Rh', 'Th', 'expression']:
if key in param_list[0].keys():
output[key] = np.vstack([v[key] for v in param_list])
if share_shape:
output['shapes'] = output['shapes'].mean(axis=0, keepdims=True)
return output
def load_motions(path):
from glob import glob
filenames = sorted(glob(join(path, '*.json')))
print(filenames)
motions = {}
# for filename in filenames[300:900]:
for filename in filenames:
infos = read_smpl(filename)
for data in infos:
pid = data['id']
if pid not in motions.keys():
motions[pid] = []
motions[pid].append(data)
keys = list(motions.keys())
# BUG: not strictly equal: (Rh, Th, poses) != (Th, (Rh, poses))
for pid in motions.keys():
motions[pid] = merge_params(motions[pid])
motions[pid]['poses'][:, :3] = motions[pid]['Rh']
return motions
def load_smpl_params(datapath):
motions = load_motions(datapath)
return motions
def main(params):
scene = bpy.data.scenes['Scene']
ob, obname, arm_ob, cam_ob = init_scene(scene, params, params['gender'])
setState0()
ob.select_set(True)
bpy.context.view_layer.objects.active = ob
# unblocking both the pose and the blendshape limits
for k in ob.data.shape_keys.key_blocks.keys():
bpy.data.shape_keys["Key"].key_blocks[k].slider_min = -10
bpy.data.shape_keys["Key"].key_blocks[k].slider_max = 10
bpy.context.view_layer.objects.active = arm_ob
motions = load_smpl_params(params['path'])
for pid, data in motions.items():
# animation
arm_ob.animation_data_clear()
cam_ob.animation_data_clear()
# load smpl params:
nFrames = data['poses'].shape[0]
for frame in range(nFrames):
print(frame)
scene.frame_set(frame)
# apply
trans = data['Th'][frame]
shape = data['shapes'][0]
pose = data['poses'][frame]
apply_trans_pose_shape(Vector(trans), pose, shape, ob,
arm_ob, obname, scene, cam_ob, frame)
bpy.context.view_layer.update()
bpy.ops.export_anim.bvh(filepath=join(params['out'], '{}.bvh'.format(pid)), frame_start=0, frame_end=nFrames-1)
return 0
if __name__ == '__main__':
try:
import argparse
if bpy.app.background:
parser = argparse.ArgumentParser(
description='Create keyframed animated skinned SMPL mesh from VIBE output')
parser.add_argument('path', type=str,
help='Input file or directory')
parser.add_argument('--out', dest='out', type=str, required=True,
help='Output file or directory')
parser.add_argument('--smpl_data_folder', type=str,
default='./data/smplx/SMPL_maya',
help='Output file or directory')
parser.add_argument('--gender', type=str,
default='male')
args = parser.parse_args(sys.argv[sys.argv.index('--') + 1:])
print(vars(args))
main(vars(args))
except SystemExit as ex:
if ex.code is None:
exit_status = 0
else:
exit_status = ex.code
print('Exiting. Exit status: ' + str(exit_status))
# Only exit to OS when we are not running in Blender GUI
if bpy.app.background:
sys.exit(exit_status)