Source code for rofunc.simulator.utils.ycb2urdf

# Copyright 2023, Junjia LIU, jjliu@mae.cuhk.edu.hk
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import os
import subprocess

import lxml.etree as et
import numpy as np
import trimesh
from tqdm import tqdm
from trimesh.decomposition import convex_decomposition
from trimesh.exchange.export import export_mesh
from trimesh.version import __version__ as trimesh_version

YCB_MASS = {
    "001_chips_can": 0.205,
    "002_master_chef_can": 0.414,
    "003_cracker_box": 0.411,
    "004_sugar_box": 0.514,
    "005_tomato_soup_can": 0.349,
    "006_mustard_bottle": 0.404,
    "007_tuna_fish_can": 0.171,
    "008_pudding_box": 0.187,
    "009_gelatin_box": 0.097,
    "010_potted_meat_can": 0.370,
    "011_banana": 0.066,
    "012_strawberry": 0.018,
    "013_apple": 0.068,
    "014_lemon": 0.029,
    "015_peach": 0.033,
    "016_pear": 0.049,
    "017_orange": 0.047,
    "018_plum": 0.025,
    "019_pitcher_base": 0.178,
    "021_bleach_cleanser": 0.479,  # check
    "022_windex_bottle": 1.022,
    "024_bowl": 0.147,
    "025_mug": 0.118,
    "026_sponge": 0.0062,
    "027_skillet": 0.950,
    "028_skillet_lid": 0.652,
    "029_plate": 0.279,
    "030_fork": 0.034,
    "031_spoon": 0.018,
    "032_knife": 0.031,
    "033_spatula": 0.0515,
    "035_power_drill": 0.895,
    "036_wood_block": 0.729,
    "037_scissors": 0.082,
    "038_padlock": 0.208,
    "040_large_marker": 0.0158,
    "041_small_marker": 0.0082,
    "042_adjustable_wrench": 0.252,
    "043_phillips_screwdriver": 0.097,
    "044_flat_screwdriver": 0.0984,
    "048_hammer": 0.665,
    "050_medium_clamp": 0.059,
    "051_large_clamp": 0.125,
    "052_extra_large_clamp": 0.202,
    "053_mini_soccer_ball": 0.123,
    "054_softball": 0.191,
    "055_baseball": 0.138,
    "056_tennis_ball": 0.057,
    "057_racquetball": 0.041,
    "058_golf_ball": 0.046,
    "059_chain": 0.1,
    "061_foam_brick": 0.028,
    "062_dice": 0.0052,
    "063-a_marbles": 0.020,
    "063-b_marbles": 0.020,
    "063-c_marbles": 0.020,
    "063-d_marbles": 0.020,
    "063-e_marbles": 0.020,
    "063-f_marbles": 0.020,
    "065-a_cups": 0.020,
    "065-b_cups": 0.020,
    "065-c_cups": 0.020,
    "065-d_cups": 0.020,
    "065-e_cups": 0.020,
    "065-f_cups": 0.020,
    "065-g_cups": 0.020,
    "065-h_cups": 0.020,
    "065-i_cups": 0.020,
    "065-j_cups": 0.020,
    "070-a_colored_wood_blocks": 0.018,
    "070-b_colored_wood_blocks": 0.018,
    "071_nine_hole_peg_test": 1.435,
    "072-a_toy_airplane": 0.020,
    "072-b_toy_airplane": 0.020,
    "072-c_toy_airplane": 0.020,
    "072-d_toy_airplane": 0.020,
    "072-e_toy_airplane": 0.020,
    "072-f_toy_airplane": 0.020,
    "072-g_toy_airplane": 0.020,
    "072-h_toy_airplane": 0.020,
    "072-i_toy_airplane": 0.020,
    "072-j_toy_airplane": 0.020,
    "072-k_toy_airplane": 0.020,
    "073-a_lego_duplo": 0.020,
    "073-b_lego_duplo": 0.020,
    "073-c_lego_duplo": 0.020,
    "073-d_lego_duplo": 0.020,
    "073-e_lego_duplo": 0.020,
    "073-f_lego_duplo": 0.020,
    "073-g_lego_duplo": 0.020,
    "073-h_lego_duplo": 0.020,
    "073-i_lego_duplo": 0.020,
    "073-j_lego_duplo": 0.020,
    "073-k_lego_duplo": 0.020,
    "073-l_lego_duplo": 0.020,
    "073-m_lego_duplo": 0.020,
    "076_timer": 0.101,
    "077_rubiks_cube": 0.094,
}


[docs]def export_urdf(mesh, directory, scale=1.0, color=[0.75, 0.75, 0.75], convex_decompose=False, **kwargs): """ Convert a Trimesh object into a URDF package for physics simulation. This breaks the mesh into convex pieces and writes them to the same directory as the .urdf file. :param mesh: Trimesh object :param directory: str, the directory name for the URDF package :param scale: :param color: :param convex_decompose: bool, whether to decompose the mesh into convex pieces :param kwargs: :return: mesh: The decomposed mesh """ # Extract the save directory and the file name fullpath = os.path.abspath(directory) name = os.path.basename(fullpath) _, ext = os.path.splitext(name) minimum_mass = kwargs.get('minimum_mass', 0.001) if ext != '': raise ValueError('URDF path must be a directory!') # Create directory if needed if not os.path.exists(fullpath): os.mkdir(fullpath) elif not os.path.isdir(fullpath): raise ValueError('URDF path must be a directory!') # Perform a convex decomposition if convex_decompose: try: convex_pieces = convex_decomposition(mesh, **kwargs) if not isinstance(convex_pieces, list): convex_pieces = [convex_pieces] except subprocess.CalledProcessError: convex_pieces = [mesh] else: convex_pieces = [mesh] # Get the effective density of the mesh effective_density = mesh.volume / sum([m.volume for m in convex_pieces]) # open an XML tree root = et.Element('robot', name='root') # Loop through all pieces, adding each as a link prev_link_name = None for i, piece in enumerate(convex_pieces): if convex_decompose: # Save each convex piece out to a file piece_name = '{}_convex_piece_{}'.format(name, i) piece_filename = '{}.obj'.format(piece_name) piece_filepath = os.path.join(fullpath, piece_filename) export_mesh(piece, piece_filepath) geom_name = '{}'.format(piece_filename) visual_name = geom_name else: piece_name = name visual_name = 'google_16k/textured.obj' geom_name = 'google_16k/nontextured.stl' # Set the mass properties of the piece piece.center_mass = mesh.center_mass piece.density = effective_density * mesh.density link_name = 'link_{}'.format(piece_name) I = [['{:.2E}'.format(y) for y in x] for x in piece.moment_inertia] # Write the link out to the XML Tree link = et.SubElement(root, 'link', name=link_name) # contact = et.SubElement(link, 'contact') # lateral_friction = et.SubElement(contact, 'lateral_friction', value="1.0") # rolling_friction = et.SubElement(contact, 'rolling_friction', value="0.0") # contact_cfm = et.SubElement(contact, 'contact_cfm', value="0.0") # contact_erp = et.SubElement(contact, 'rolling_friction', value="1.0") # <contact> # <lateral_friction value="1.0"/> # <rolling_friction value="0.0"/> # <contact_cfm value="0.0"/> # <contact_erp value="1.0"/> # </contact> # Inertial information inertial = et.SubElement(link, 'inertial') et.SubElement(inertial, 'origin', xyz="0 0 0", rpy="0 0 0") if convex_decompose: mass = np.maximum(piece.mass.reshape(-1)[0], minimum_mass) et.SubElement(inertial, 'inertia', ixx=I[0][0], ixy=I[0][1], ixz=I[0][2], iyy=I[1][1], iyz=I[1][2], izz=I[2][2]) else: mass = np.maximum(YCB_MASS[piece_name], minimum_mass) et.SubElement(inertial, 'inertia', ixx='0.0001', ixy='0.0', ixz='0.0', iyy='0.0001', iyz='0.0', izz='0.0001') et.SubElement(inertial, 'mass', value='{}'.format(mass)) # Visual Information visual = et.SubElement(link, 'visual') et.SubElement(visual, 'origin', xyz="0 0 0", rpy="0 0 0") geometry = et.SubElement(visual, 'geometry') et.SubElement(geometry, 'mesh', filename=visual_name, scale="{} {} {}".format(scale, scale, scale)) material = et.SubElement(visual, 'material', name='') et.SubElement(material, 'color', rgba="{:.2E} {:.2E} {:.2E} 1".format( color[0], color[1], color[2])) # Collision Information collision = et.SubElement(link, 'collision') et.SubElement(collision, 'origin', xyz="0 0 0", rpy="0 0 0") geometry = et.SubElement(collision, 'geometry') et.SubElement(geometry, 'mesh', filename=geom_name, scale="{} {} {}".format(scale, scale, scale)) # Create rigid joint to previous link if prev_link_name is not None: joint_name = '{}_joint'.format(link_name) joint = et.SubElement(root, 'joint', name=joint_name, type='fixed') et.SubElement(joint, 'origin', xyz="0 0 0", rpy="0 0 0") et.SubElement(joint, 'parent', link=prev_link_name) et.SubElement(joint, 'child', link=link_name) prev_link_name = link_name # Write URDF file tree = et.ElementTree(root) urdf_filename = '{}.urdf'.format(name) tree.write(os.path.join(fullpath, urdf_filename), pretty_print=True) # Write Gazebo config file root = et.Element('model') model = et.SubElement(root, 'name') model.text = name version = et.SubElement(root, 'version') version.text = '1.0' sdf = et.SubElement(root, 'sdf', version='1.4') sdf.text = '{}.urdf'.format(name) author = et.SubElement(root, 'author') et.SubElement(author, 'name').text = 'Junjia LIU'.format(trimesh_version) et.SubElement(author, 'email').text = 'jjliu@mae.cuhk.edu.hk' description = et.SubElement(root, 'description') description.text = name tree = et.ElementTree(root) tree.write(os.path.join(fullpath, 'model.config')) return np.sum(convex_pieces)
[docs]def create_urdf_file(output_directory, input_mesh): mesh = trimesh.load(input_mesh) folder_name = input_mesh.split('/')[-3].split('.')[0] outpath = "%s/%s" % (output_directory, folder_name) if not os.path.exists(outpath): os.makedirs(outpath) export_urdf(mesh, outpath) return outpath + '/%s%s' % (folder_name, '.urdf')
[docs]def ycb2urdf(): ycb_output_directory = "../assets/urdf/ycb" urdf_output_directory = "../assets/urdf/ycb" objects = os.listdir(ycb_output_directory) with tqdm(total=len(objects)) as t: for obj in objects: if not obj[0] == "0": continue try: urdf_root_path = create_urdf_file(output_directory=urdf_output_directory, input_mesh=ycb_output_directory + ('/%s/%s') % ( obj, 'google_16k/nontextured.stl')) except Exception as e: logging.error(e) continue t.set_postfix_str(f"{urdf_root_path}") t.update(1)
# if test_object: # test_urdf(urdf_root_path=urdf_root_path) if __name__ == '__main__': ycb2urdf()