Open gemlongman opened 3 years ago
Hello @gemlongman,
I'm not familiar with BVH motion capture files: do you know how exactly they encode their data? (And would you have an example?)
Here is a BVH file in the zip which could be found more in https://sites.google.com/a/cgspeed.com/cgspeed/motion-capture/daz-friendly-release, they can be imported by Motion Builder to see motion. Meanwhile, there is some BVH python parser such as https://pypi.org/project/bvh/, but it will take lots of work to adapt. Running Jump To Standing Idle.zip
After some investigations I'm joining you in the fact that's a super good idea!
The BVH file's HIERARCHY section is very close to the definition of a URDF, so this shouldn't be so difficult to rewrite a parser, even from scratch. I've found a good spec here: https://research.cs.wisc.edu/graphics/Courses/cs-838-1999/Jeff/BVH.html
Regarding the features, am I right to suppose we will need actually two features?
yeah~you are right Besides, for a BVH animation, the movement smooth is same important as some joint being fix at the target position. Maybe it is another big problem...
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
I'm thinking about picking this up, mostly to work with BVH in Mujoco - but would gladly provide code here too. Was any more progress made outside of this issue?
import xml.dom.minidom
import numpy as np
class BVHtoURDF:
def __init__(self, bvh_file):
self.bvh_file = bvh_file
self.hierarchy = {}
self.urdf_tree = None
self.joint_counter = 0
self.lowest_point = np.array([0, 0, 0])
self.root_offset = np.array([0, 0, 0])
def parse_bvh(self):
with open(self.bvh_file, 'r') as f:
content = f.read()
hierarchy_section = content.split('MOTION:')[0]
self.parse_hierarchy(hierarchy_section)
def parse_hierarchy(self, hierarchy_section):
lines = hierarchy_section.strip().split('\n')
stack = []
current_joint = None
for line in lines:
parts = line.strip().split()
if not parts:
continue
if parts[0] == 'ROOT' or parts[0] == 'JOINT':
joint_name = parts[1]
self.joint_counter += 1
current_joint = {
'name': joint_name,
'id': self.joint_counter,
'offset': None,
'channels': [],
'children': []
}
if stack:
stack[-1]['children'].append(current_joint)
stack.append(current_joint)
elif parts[0] == 'OFFSET':
current_joint['offset'] = [float(x) for x in parts[1:4]]
elif parts[0] == 'CHANNELS':
current_joint['channels'] = parts[2:]
elif parts[0] == 'End':
self.joint_counter += 1
end_site = {
'name': f"{current_joint['name']}_end",
'id': self.joint_counter,
'offset': None,
'channels': [],
'children': []
}
current_joint['children'].append(end_site)
stack.append(end_site)
elif parts[0] == '}':
if stack:
completed_joint = stack.pop()
if not stack:
self.hierarchy = completed_joint
self.calculate_lowest_point(self.hierarchy, np.array([0, 0, 0]))
self.root_offset = np.array([0, -self.lowest_point[1], 0])
def calculate_lowest_point(self, joint, current_position):
if joint['offset']:
current_position = current_position + np.array(joint['offset'])
if current_position[1] < self.lowest_point[1]:
self.lowest_point = current_position
for child in joint['children']:
self.calculate_lowest_point(child, current_position)
def create_urdf(self, rotation_angle=0):
self.urdf_tree = ET.Element('robot', name='bvh_robot')
self.create_link('base_link', mass=1.0, length=0.1, radius=0.025)
root_joint = ET.SubElement(self.urdf_tree, 'joint', name='root_joint', type='floating')
ET.SubElement(root_joint, 'parent', link='base_link')
ET.SubElement(root_joint, 'child', link=f"link_{self.hierarchy['id']}")
origin = ET.SubElement(root_joint, 'origin')
rotated_offset = self.rotate_vector(self.root_offset, rotation_angle)
origin.set('xyz', ' '.join(map(str, rotated_offset)))
origin.set('rpy', f"0 0 {rotation_angle}")
self.create_links_and_joints(self.hierarchy, 'base_link', rotation_angle)
def create_links_and_joints(self, joint, parent, rotation_angle):
link_name = f"link_{joint['id']}"
if 'hip' in joint['name'].lower() or 'pelvis' in joint['name'].lower():
mass = 10.0
elif 'thigh' in joint['name'].lower() or 'upper' in joint['name'].lower():
mass = 5.0
elif 'hand' in joint['name'].lower() or 'foot' in joint['name'].lower():
mass = 1.0
else:
mass = 2.0
self.create_link(link_name, mass=mass)
if parent != 'base_link':
joint_name = f"joint_{joint['id']}"
self.create_joint(joint_name, parent, link_name, joint, rotation_angle)
for child in joint['children']:
self.create_links_and_joints(child, link_name, rotation_angle)
def create_link(self, name, mass=1.0, length=0.1, radius=0.025):
link = ET.SubElement(self.urdf_tree, 'link', name=name)
inertial = ET.SubElement(link, 'inertial')
ET.SubElement(inertial, 'mass', value=str(mass))
inertia = self.calculate_inertia(mass, radius, length)
ET.SubElement(inertial, 'inertia', ixx=str(inertia[0]), iyy=str(inertia[1]), izz=str(inertia[2]),
ixy="0", ixz="0", iyz="0")
visual = ET.SubElement(link, 'visual')
geometry = ET.SubElement(visual, 'geometry')
ET.SubElement(geometry, 'cylinder', radius=str(radius), length=str(length))
material = ET.SubElement(visual, 'material', name=f"{name}_material")
color = ET.SubElement(material, 'color', rgba="0.8 0.8 0.8 1")
collision = ET.SubElement(link, 'collision')
geometry = ET.SubElement(collision, 'geometry')
ET.SubElement(geometry, 'cylinder', radius=str(radius), length=str(length))
def create_joint(self, name, parent, child, joint_data, rotation_angle):
joint = ET.SubElement(self.urdf_tree, 'joint', name=name, type='revolute')
ET.SubElement(joint, 'parent', link=parent)
ET.SubElement(joint, 'child', link=child)
if joint_data['offset']:
origin = ET.SubElement(joint, 'origin')
rotated_offset = self.rotate_vector(joint_data['offset'], rotation_angle)
origin.set('xyz', ' '.join(map(str, rotated_offset)))
axis = np.array([0, 0, 0])
if 'Xrotation' in joint_data['channels']:
axis = np.array([1, 0, 0])
elif 'Yrotation' in joint_data['channels']:
axis = np.array([0, 1, 0])
elif 'Zrotation' in joint_data['channels']:
axis = np.array([0, 0, 1])
rotated_axis = self.rotate_vector(axis, rotation_angle)
if np.linalg.norm(rotated_axis) < 1e-5:
rotated_axis = np.array([0, 0, 1])
ET.SubElement(joint, 'axis', xyz=' '.join(map(str, rotated_axis)))
ET.SubElement(joint, 'limit', lower='-3.14159', upper='3.14159', effort='100', velocity='0.5')
def calculate_inertia(self, mass, radius, length):
ixx = iyy = (1/12) * mass * (3 * radius**2 + length**2)
izz = (1/2) * mass * radius**2
return ixx, iyy, izz
@staticmethod
def rotate_vector(vector, angle):
rotation_matrix = np.array([
[1, 0, 0],
[0, np.cos(angle), -np.sin(angle)],
[0, np.sin(angle), np.cos(angle)]
])
return np.dot(rotation_matrix, vector)
def save_urdf(self, output_file):
xml_str = ET.tostring(self.urdf_tree, encoding='utf-8')
dom = xml.dom.minidom.parseString(xml_str)
pretty_xml_str = dom.toprettyxml(indent=" ")
with open(output_file, 'w') as f:
f.write(pretty_xml_str)
# Usage
parser = BVHtoURDF('input.bvh')
parser.parse_bvh()
parser.create_urdf(rotation_angle=np.pi/2) # Rotate 90 degrees around X-axis
parser.save_urdf('output.urdf')```
Is there a simple way to create a chain with BVH motion files?In fact it will be perfect to add a BVH parser.