mujoco_worldgen/objs/obj_from_xml.py (134 lines of code) (raw):
""" Creates an object based on MujocoXML. XML has to have annotation such as
- annotation:outer_bound : defines box that spans the entire object.
Moreover, its left lower corner should be located at (0, 0, 0)
"""
import glob
import os
from collections import OrderedDict
import numpy as np
from mujoco_worldgen.util.types import store_args
from mujoco_worldgen.util.path import worldgen_path
from mujoco_worldgen.parser import parse_file
from mujoco_worldgen.util.obj_util import get_name_index, recursive_rename
from mujoco_worldgen.objs.obj import Obj
class ObjFromXML(Obj):
"""
Creates an object based on MujocoXML. XML has to have annotation such as
- annotation:outer_bound : defines box that spans the entire object.
Moreover, its left lower corner should be located at (0, 0, 0)
"""
@store_args
def __init__(self, model_path, name=None, default_qpos=None):
super(ObjFromXML, self).__init__()
def generate(self, random_state, world_params, placement_size):
# Only do this once, because it sometimes picks object at random
self.xml_path = self._generate_xml_path(random_state)
self.xml = parse_file(self.xml_path)
self.placements = OrderedDict()
bodies = []
for body in self.xml["worldbody"]["body"]:
name = body.get('@name', '')
if name.startswith('annotation:'):
# Placement annotation, for example the insides of shelves,
# or outer_bound, which determines size and "top" placement.
assert '@pos' in body, "Annotation %s must have pos" % name
assert 'geom' in body, "Annotation %s must have geom" % name
assert len(body['geom']) == 1, "%s must have 1 geom" % name
geom = body['geom'][0]
assert geom.get('@type') == 'box', "%s must have box" % name
assert '@size' in geom, "%s geom must have size" % name
if '@pos' in geom:
# Worldgen places objects by moving qpos (slide joints)
# to put them in position, and their final position is:
# qpos + pos + parent_pos + ...
# In order for objects to end up where worldgen wants them,
# all of the pos + parent_pos + ... have to equal zero.
# Otherwise the offsets get messed up.
assert np.array_equal(geom['@pos'], np.zeros(3)), \
"%s: Set pos on body instead of geom" % name
size = geom['@size'] * 2 # given as halfsize
origin = body['@pos'] - (size / 2) # given as center coord
placement_name = name[len('annotation:'):]
if placement_name == 'outer_bound':
# Note: "top" placement is not automatically created
# Must be explicitly added in XML
# bin/annotate.py --suggestions will show possible ones
self.size = size
if world_params.show_outer_bounds:
bodies.append(body)
continue
placement = OrderedDict(size=size, origin=origin)
self.placements[placement_name] = placement
for body in self.xml["worldbody"]["body"]:
name = body.get('@name', '')
if not name.startswith("annotation:"): # Not an annotation, must be a main body
if self.name is not None:
body_name = self.name
if name:
body_name += ":" + name
body['@name'] = body_name
body["@pos"] = body["@pos"]
bodies.append(body)
self.xml['worldbody']['body'] = bodies
def add_joints(self, body):
joint_names = []
if 'joint' not in body:
body['joint'] = []
if isinstance(body['joint'], OrderedDict):
body['joint'] = [body['joint']]
for i, slide_axis in enumerate(np.eye(3)):
found = False
for joint in body['joint']:
if not isinstance(joint, OrderedDict):
continue
if joint.get('@type') != 'slide':
continue
if '@axis' not in joint:
continue
axis = joint['@axis']
if np.linalg.norm(slide_axis - axis) < 1e-6:
joint_names.append(joint['@name'])
found = True
break # Found axis
if not found: # add this joint
slide = OrderedDict()
joint_name = self.name + ':slide%d' % i
slide['@name'] = joint_name
slide['@type'] = 'slide'
slide['@axis'] = slide_axis
slide['@damping'] = '0.01'
slide['@pos'] = np.zeros(3)
body['joint'].append(slide)
joint_names.append(joint_name)
return joint_names
def generate_name(self, name_indexes):
if self.name is None:
if self.model_path.split("/")[0] == "robot":
assert self.name is None or self.name == "robot", \
"Detected robot XML. " \
"Robot should be named \"robot\". Abording."
name = "robot"
else:
name = self.model_path.replace('/', '_')
self.name = get_name_index(name_indexes, name)
def generate_xml_dict(self):
'''
Generate XML DOM nodes needed for MuJoCo model.
doc - XML Document, used to create elements/nodes
name_indexes - dictionary to keep track of names,
see get_name_index() for internals
Returns a dictionary with keys as names of top-level nodes:
e.g. 'worldbody', 'materials', 'assets'
And the values are lists of XML DOM nodes
'''
# Iterate over all names inside and prepend self.name
recursive_rename(self.xml, self.name)
main_body = None
worldbody = self.xml["worldbody"]
bodies = worldbody["body"]
for body in bodies:
name = body.get('@name', '') # Might not be present in main body
if "annotation" not in name and not body.get('@mocap'):
assert main_body is None, "We support only a single main body."
main_body = body
for rot in ('@euler', '@quat'):
assert rot not in main_body, 'We dont support rotations in the main body.'\
'Please move it inward.'
self.add_joints(main_body)
return self.xml
def _get_xml_dir_path(self, *args):
'''
If you want to use custom XMLs, subclass this class and overwrite this
method to return the path to your 'xmls' folder
'''
return worldgen_path('assets/xmls', *args)
def _generate_xml_path(self, random_state=None):
'''Separated because some subclasses need to override just this'''
if random_state is None:
random_state = np.random.RandomState(0)
xml_path = self._get_xml_dir_path(self.model_path)
if not xml_path.endswith(".xml"):
# Didn't find it, go to a subdirectory
if not os.path.isfile(os.path.join(xml_path, "main.xml")):
dirs = glob.glob(os.path.join(xml_path, '*'))
assert dirs, "Failed to find dirs matching {}".format(xml_path)
xml_path = dirs[random_state.randint(0, len(dirs))]
xml_path = os.path.join(xml_path, "main.xml")
return xml_path