Phylliade / ikpy

An Inverse Kinematics library aiming performance and modularity
http://phylliade.github.io/ikpy
Apache License 2.0
694 stars 151 forks source link

BVH parser #91

Open gemlongman opened 3 years ago

gemlongman commented 3 years ago

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

gemlongman commented 3 years ago

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 commented 3 years ago

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?

gemlongman commented 3 years ago

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[bot] commented 3 years ago

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.

andrewnc commented 1 week ago

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 commented 6 days ago

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')```