Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BVH parser #91

Open
gemlongman opened this issue Jul 13, 2020 · 7 comments
Open

BVH parser #91

gemlongman opened this issue Jul 13, 2020 · 7 comments

Comments

@gemlongman
Copy link

Is there a simple way to create a chain with BVH motion files?In fact it will be perfect to add a BVH parser.

@Phylliade
Copy link
Owner

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?)

@gemlongman
Copy link
Author

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

@Phylliade
Copy link
Owner

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?

  • Creating a chain from a BVH HIERARCHY section
  • Importing a forward kinematics trajectory from a BVH MOTION section
@gemlongman
Copy link
Author

gemlongman commented Aug 4, 2020

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...

@stale
Copy link

stale bot commented Oct 3, 2020

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.

@stale stale bot added the wontfix label Oct 3, 2020
@stale stale bot closed this as completed Oct 10, 2020
@Phylliade Phylliade reopened this Nov 8, 2020
@andrewnc
Copy link

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?

@andrewnc
Copy link

andrewnc commented Jun 24, 2024

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')```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
3 participants