Source code for rofunc.simulator.utils.ycb2urdf_legacy

"""
This code is from https://github.com/harvard-microrobotics/object2urdf
"""

from scipy.spatial.transform import Rotation
import numpy as np
import os
import copy
import trimesh
import xml.etree.ElementTree as ET
from tqdm import tqdm
import rofunc as rf


[docs]class ObjectUrdfBuilder: def __init__(self, object_folder="", log_file="vhacd_log.txt", urdf_prototype='_prototype.urdf'): self.object_folder = os.path.abspath(object_folder) self.log_file = os.path.abspath(log_file) self.suffix = "vhacd" self.urdf_base = self._read_xml(os.path.join(object_folder, urdf_prototype)) # Recursively get all files with a specific extension, excluding a certain suffix def _get_files_recursively(self, start_directory, filter_extension=None, exclude_suffix=None): for root, dirs, files in os.walk(start_directory): for file in files: if filter_extension is None or file.lower().endswith(filter_extension): if isinstance(exclude_suffix, str): if not file.lower().endswith(exclude_suffix + filter_extension): yield (root, file, os.path.abspath(os.path.join(root, file))) # Read and parse a URDF from a file def _read_xml(self, filename): root = ET.parse(filename).getroot() return root # Convert a list to a space-separated string def _list2str(self, in_list): out = "" for el in in_list: out += str(el) + " " return out[:-1] # Convert a space-separated string to a list def _str2list(self, in_str): out = in_str.split(' ') out = [float(el) for el in out] return out # Find the center of mass of the object
[docs] def get_center_of_mass(self, filename): mesh = trimesh.load(filename) # print(mesh) if isinstance(mesh, trimesh.Scene): # print("Imported combined mesh: using centroid rather than center of mass") return mesh.centroid else: return mesh.center_mass
# Find the geometric center of the object
[docs] def get_geometric_center(self, filename): mesh = trimesh.load(filename) return copy.deepcopy(mesh.centroid)
# Get the middle of a face of the bounding box
[docs] def get_face(self, filename, edge): mesh = trimesh.load(filename) bounds = mesh.bounds face = copy.deepcopy(mesh.centroid) if edge in ['top', 'xy_pos']: face[2] = bounds[1][2] elif edge in ['bottom', 'xy_neg']: face[2] = bounds[0][2] elif edge in ['xz_pos']: face[1] = bounds[1][1] elif edge in ['xz_neg']: face[1] = bounds[0][1] elif edge in ['yz_pos']: face[0] = bounds[1][0] elif edge in ['yz_neg']: face[0] = bounds[0][0] return face
# Do a convex decomposition
[docs] def do_vhacd(self, filename, outfile, debug=False, **kwargs): try: mesh = trimesh.load(filename) # convex_list = trimesh.decomposition.convex_decomposition(mesh) meshes = mesh.convex_decomposition(**kwargs) convex = trimesh.util.concatenate(meshes) convex.export(outfile) except ValueError: print("No direct VHACD backend available, trying pybullet") pass try: import pybullet as p p.vhacd(filename, outfile, self.log_file, **kwargs) except ModuleNotFoundError: print( '\n' + "ERROR - pybullet module not found: If you want to do convex decomposisiton, make sure you install pybullet (https://pypi.org/project/pybullet) or install VHACD directly (https://github.com/mikedh/trimesh/issues/404)" + '\n') raise
# Find the center of mass of the object
[docs] def save_to_obj(self, filename): name, ext = os.path.splitext(filename) obj_filename = name + '.obj' mesh = trimesh.load(filename) mesh.export(obj_filename) return obj_filename
# Replace an attribute in a feild of a URDF
[docs] def replace_urdf_attribute(self, urdf, feild, attribute, value): urdf = self.replace_urdf_attributes(urdf, feild, {attribute: value}) return urdf
# Replace several attributes in a feild of a URDF
[docs] def replace_urdf_attributes(self, urdf, feild, attribute_dict, sub_feild=None): if sub_feild is None: sub_feild = [] field_obj = urdf.find(feild) if field_obj is not None: if len(sub_feild) > 0: for child in reversed(sub_feild): field_obj = ET.SubElement(field_obj, child) field_obj.attrib.update(attribute_dict) # field_obj.attrib = attribute_dict else: feilds = feild.split("/") new_feild = "/".join(feilds[0:-1]) sub_feild.append(feilds[-1]) self.replace_urdf_attributes(urdf, new_feild, attribute_dict, sub_feild)
# Make an updated copy of the URDF for the current object
[docs] def update_urdf(self, object_file, object_name, collision_file=None, override=None, mass_center=None): # If no separate collision geometry is provided, use the object file if collision_file is None: collision_file = object_file # Update the filenames and object name new_urdf = copy.deepcopy(self.urdf_base) self.replace_urdf_attribute(new_urdf, './/visual/geometry/mesh', 'filename', object_file) self.replace_urdf_attribute(new_urdf, './/collision/geometry/mesh', 'filename', collision_file) new_urdf.attrib['name'] = object_name # Update the overrides if override is not None: for orverride_el in override: # Update attributes out_el_all = new_urdf.findall('.//' + orverride_el.tag) for out_el in out_el_all: for key in orverride_el.attrib: out_el.set(key, orverride_el.attrib[key]) # Remove fields that will be overwritten for child in orverride_el: el = out_el.find(child.tag) if el is not None: out_el.remove(el) # Add updated feilds out_el.extend(orverride_el) # Output the center of mass if provided if mass_center is not None: # Check if there's a geometry offset offset_ob = new_urdf.find('.//collision/origin') if offset_ob is not None: offset_str = offset_ob.attrib.get('xyz', '0 0 0') rot_str = offset_ob.attrib.get('rpy', '0 0 0') offset = self._str2list(offset_str) rpy = self._str2list(rot_str) else: offset = [0, 0, 0] rpy = [0, 0, 0] # Check if there's a scale factor and apply it scale_ob = new_urdf.find('.//collision/geometry/mesh') if scale_ob is not None: scale_str = scale_ob.attrib.get('scale', '1 1 1') scale = self._str2list(scale_str) else: scale = [1, 1, 1] for idx, axis in enumerate(mass_center): mass_center[idx] = -mass_center[idx] * scale[idx] + offset[idx] rot = Rotation.from_euler('xyz', rpy) rot_matrix = rot.as_matrix() mass_center = np.matmul(rot_matrix, np.vstack(np.asarray(mass_center))).squeeze() self.replace_urdf_attributes(new_urdf, './/visual/origin', {'xyz': self._list2str(mass_center), 'rpy': self._list2str(rpy)}) self.replace_urdf_attributes(new_urdf, './/collision/origin', {'xyz': self._list2str(mass_center), 'rpy': self._list2str(rpy)}) return new_urdf
# Save a URDF to a file
[docs] def save_urdf(self, new_urdf, filename, overwrite=False): out_file = os.path.join(self.object_folder, filename) # Do not overwrite the file unless the option is True if os.path.exists(out_file) and not overwrite: return # Save the file mydata = ET.tostring(new_urdf) with open(out_file, "wb") as f: f.write(mydata)
# Build a URDF from an object file
[docs] def build_urdf(self, filename, output_folder=None, force_overwrite=False, decompose_concave=False, force_decompose=False, center='mass', **kwargs): # If no output folder is specified, use the base object folder if output_folder is None: output_folder = self.object_folder # Generate a relative path from the output folder to the geometry files filename = os.path.abspath(filename) common = os.path.commonprefix([output_folder, filename]) rel = os.path.join(filename.replace(common, '')) if rel[0] == os.path.sep: rel = rel[1:] name = rel.split(os.path.sep)[0] rel = rel.replace(os.path.sep, '/') file_name_raw, file_extension = os.path.splitext(filename) # If an override file exists, include its data in the URDF override_file = filename.replace(file_extension, '.ovr') if os.path.exists(override_file): overrides = self._read_xml(override_file) else: overrides = None # Calculate the center of mass if center == 'mass': mass_center = self.get_center_of_mass(filename) elif center == 'geometric': mass_center = self.get_geometric_center(filename) elif center in ['top', 'bottom', 'xy_pos', 'xy_neg', 'xz_pos', 'xz_neg', 'yz_pos', 'yz_neg']: mass_center = self.get_face(filename, center) else: mass_center = None # If the user wants to run convex decomposition on concave objects, do it. if decompose_concave: if file_extension == '.stl': obj_filename = self.save_to_obj(filename) visual_file = rel.replace(file_extension, '.obj') elif file_extension == '.obj': obj_filename = filename visual_file = rel else: raise ValueError("Your filetype needs to be an STL or OBJ to perform concave decomposition") outfile = obj_filename.replace('.obj', '_' + self.suffix + '.obj') collision_file = visual_file.replace('.obj', '_' + self.suffix + '.obj') # Only run a decomposition if one does not exist, or if the user forces an overwrite if not os.path.exists(outfile) or force_decompose: self.do_vhacd(obj_filename, outfile, **kwargs) urdf_out = self.update_urdf(visual_file, name, collision_file=collision_file, override=overrides, mass_center=mass_center) else: urdf_out = self.update_urdf(rel, name, override=overrides, mass_center=mass_center) self.save_urdf(urdf_out, name + '.urdf', force_overwrite)
# Build the URDFs for all objects in your library.
[docs] def build_library(self, **kwargs): rf.logger.beauty_print("\nFOLDER: %s" % self.object_folder) # Get all OBJ files obj_files = self._get_files_recursively(self.object_folder, filter_extension='.obj', exclude_suffix=self.suffix) stl_files = self._get_files_recursively(self.object_folder, filter_extension='.stl', exclude_suffix=self.suffix) # Check if the urdf is already built existing_urdfs = rf.oslab.list_absl_path(self.object_folder, suffix='.urdf') obj_folders = [] for root, _, full_file in tqdm(obj_files, total=101): object_name = root.split("/")[-2] assert object_name[0] == '0' if object_name + '.urdf' in existing_urdfs: continue obj_folders.append(root) self.build_urdf(full_file, **kwargs) # common = os.path.commonprefix([self.object_folder, full_file]) # rel = os.path.join(full_file.replace(common, '')) # print('\tBuilding: %s' % (rel)) for root, _, full_file in tqdm(stl_files): object_name = root.split("/")[-2] assert object_name[0] == '0' if object_name + '.urdf' in existing_urdfs: continue if root not in obj_folders: self.build_urdf(full_file, **kwargs)
# common = os.path.commonprefix([self.object_folder, full_file]) # rel = os.path.join(full_file.replace(common, '')) # print('Building: %s' % (rel))
[docs]def ycb2urdf(force_overwrite=True, decompose_concave=True, force_decompose=False, center='top'): # Build entire libraries of URDFs rofunc_path = rf.oslab.get_rofunc_path() object_folder = os.path.join(rofunc_path, "simulator/assets/urdf/ycb") builder = ObjectUrdfBuilder(object_folder) builder.build_library(force_overwrite=force_overwrite, decompose_concave=decompose_concave, force_decompose=force_decompose, center=center)
if __name__ == '__main__': ycb2urdf()