-
-
Notifications
You must be signed in to change notification settings - Fork 45
Automatic conversion tool + urdf library w/ meshes #74
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
Open
Simon-Steinmann
wants to merge
34
commits into
master
Choose a base branch
from
Simon-Steinmann-batch-conversion
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
08ef58b
add optparser
Simon-Steinmann 1f77776
Test world generation
Simon-Steinmann ed2c886
add LineSet Collada fix, so there are no errors
Simon-Steinmann 6c09cfc
Add motomann
Simon-Steinmann cad05ee
pep8 compliancy
Simon-Steinmann a8cdae1
fix for first time running
Simon-Steinmann da6fc0e
delete urdf data
Simon-Steinmann d471af3
parse input, output direcotries
Simon-Steinmann 949aa57
include latest collada fix
Simon-Steinmann 88a3167
gitignore cleaned
Simon-Steinmann f452237
pep8 cleanup
Simon-Steinmann 972a499
revert to master
Simon-Steinmann 33c354a
Merge branch 'master' into Simon-Steinmann-batch-conversion
Simon-Steinmann 280a6e4
add batch_conversion changes back in
Simon-Steinmann 3ab9905
change expected proto files
Simon-Steinmann 4dfaf4f
Update batch_conversion.py
Simon-Steinmann c89b9e1
Update batch_conversion.py
Simon-Steinmann 71646b3
Update batch_conversion.py
Simon-Steinmann 1787e7c
Update batch_conversion.py
Simon-Steinmann 9536839
Update batch_conversion.py
Simon-Steinmann 0040788
Update batch_conversion.py
Simon-Steinmann b33b2d4
Update batch_conversion.py
Simon-Steinmann 55e21ec
Update batch_conversion.py
Simon-Steinmann b3e4405
Update batch_conversion.py
Simon-Steinmann 28490a8
Update batch_conversion.py
DavidMansolino e20c429
Update batch_conversion.py
Simon-Steinmann 1778318
Update batch_conversion.py
Simon-Steinmann 0c9e3e0
Update test.
DavidMansolino 640afae
Merge branch 'Simon-Steinmann-batch-conversion' of https://github.com…
DavidMansolino bbdd0c2
Add execution flag.
DavidMansolino e347776
Update batch_conversion.py
Simon-Steinmann 5a6d27a
Update batch_conversion.py
Simon-Steinmann 34f1576
Update batch_conversion.py
Simon-Steinmann d2a7bfc
Detailed description
Simon-Steinmann File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,287 @@ | ||
#!/usr/bin/env python3 | ||
|
||
"""URDF files to Webots PROTO batch-converter. | ||
------------------------------------------------------------------------ | ||
This script searches for all urdf files in the <input> directory, converts | ||
them to proto files and saves them in the output directory. It retains | ||
the folder structure of the input directory while doing so. | ||
|
||
The first time a urdf file gets converted, this script creates a config | ||
.json file next to it. Here you can specify any arguments you want | ||
to parse to that specific file conversion. The definiton of the default | ||
config is in the code below and needs to be updated when features get added. | ||
|
||
For each batch-conversion, a new (unique) folder is generated inside the | ||
output folder, where the conversion is being placed in. This way, previous | ||
conversions are not overridden. In addition, the converted files override | ||
those in '<output>/ExtraProjectTest' and the TestAllRobots.wbt world is | ||
created and placed there, containing all converted models, placed in a grid. | ||
|
||
To test the conversions, open webots and change your | ||
Tools>Prefernces>Extra_projects_path to '<output>/ExtraProjectTest' and | ||
open the generated TestAllRobots.wbt. From now on, after another conversion, | ||
you simply have to reload the world. | ||
|
||
Example: | ||
<input>/robots/kuka/protos/kr5.urdf | ||
gets exportet to | ||
'<output>/<unique_folder>/robots/kuka/protos/kr5.proto' | ||
and | ||
'<output>/ExtraProjectTest/robots/kuka/protos/kr5.proto' | ||
|
||
Note: Webots will only automatically find your proto file, if it is inside a | ||
'protos' folder. | ||
""" | ||
|
||
import optparse | ||
import datetime | ||
import os | ||
import shutil | ||
import json | ||
import math | ||
# from urdf2webots.importer import convert2urdf | ||
import urdf2webots.importer as importer | ||
from copy import deepcopy | ||
|
||
# Set Override = True if you want to override ALL config settings with the | ||
# value in the OverrideCfg. This can be usefull for testing functionality | ||
# quickly, and then doing a slow optimized conversion | ||
Override = False | ||
OverrideCfg = { | ||
'disableMeshOptimization': False | ||
} | ||
|
||
# This defines the default config. If a urdf model doesnt have a config, one | ||
# will created after this. If the importer.py is updated, so should this to | ||
# add the additional functionality- | ||
default_config = { | ||
# outFile will get constructed with protosPath + robotName + '.proto' | ||
'robotName': None, | ||
'normal': False, | ||
'boxCollision': True, | ||
'disableMeshOptimization': True, | ||
'enableMultiFile': True, | ||
'staticBase': True, | ||
'toolSlot': None, | ||
'initRotation': "1 0 0 -1.5708" | ||
} | ||
|
||
|
||
class BatchConversion(): | ||
def __init__(self, sourcePath, outPath): | ||
self.urdf2webots_path = os.path.dirname(os.path.abspath(__file__)) | ||
# create a unique path for a new batch-conversion | ||
today = datetime.date.today() | ||
todaystr = today.isoformat() | ||
self.protoTargetDir = outPath + '/protos_' + todaystr | ||
n = 2 | ||
while os.path.exists(self.protoTargetDir): | ||
while os.path.exists(self.protoTargetDir + '_Nr-' + str(n)): | ||
n += 1 | ||
self.protoTargetDir = self.protoTargetDir + '_Nr-' + str(n) | ||
|
||
# Find all the urdf files, and create the corresponding proto filePaths | ||
urdf_root_dir = sourcePath | ||
os.chdir(urdf_root_dir) | ||
# Walk the tree. | ||
self.urdf_files = [] # List of the full filepaths. | ||
self.protosPaths = [] | ||
for root, directories, files in os.walk('./'): | ||
for filename in files: | ||
# Join the two strings in order to form the full filepath. | ||
if filename.endswith(".urdf"): | ||
filepath = os.path.join(root, filename) | ||
filepath = filepath[1:] | ||
self.urdf_files.append(urdf_root_dir + filepath) | ||
self.protosPaths.append(self.protoTargetDir + os.path.dirname(filepath) + '/') | ||
os.chdir(self.urdf2webots_path) | ||
|
||
# Report dict we can print at the end | ||
self.EndReportMessage = { | ||
'updateConfigs': [], | ||
'newConfigs': [], | ||
'converted': [], | ||
'failed': [] | ||
} | ||
|
||
def update_and_convert(self): | ||
"""Make sure all configs exist and are up to date, then convert all URDF files.""" | ||
self.check_all_configs() | ||
self.convert_all_urdfs() | ||
self.print_end_report() | ||
|
||
def create_proto_dir(self): | ||
"""Create a new unique directory for our conversions.""" | ||
print(self.protoTargetDir) | ||
os.makedirs(self.protoTargetDir) | ||
|
||
def replace_ExtraProjectPath(self): | ||
# this copies the converted files and puts them in the "<protoTargetDir>/ExtraProjectTest" directory | ||
# This path can be added to webots, so we can test each new conversion, without having to adjust the Path | ||
# every time | ||
destination = os.path.dirname(self.protoTargetDir) + '/ExtraProjectTest' | ||
if os.path.isfile(destination) or os.path.islink(destination): | ||
os.remove(destination) # remove the file | ||
elif os.path.isdir(destination): | ||
shutil.rmtree(destination) # remove dir and all contains | ||
shutil.copytree(self.protoTargetDir, destination) | ||
|
||
def update_config(self, configFile, config=None): | ||
"""Makes sure the existing configs are in the same format as the default, existing options are conserved.""" | ||
new_config = deepcopy(default_config) | ||
for key in new_config.keys(): | ||
try: | ||
new_config[key] = config[key] | ||
except: | ||
pass | ||
with open(configFile, 'w') as outfile: | ||
json.dump(new_config, outfile, indent=4, sort_keys=True) | ||
|
||
def check_all_configs(self): | ||
"""Makes sure ever URDF file has a config and it is up to date.""" | ||
print('Checking config files...') | ||
print('---------------------------------------') | ||
for i in range(len(self.urdf_files)): | ||
configFile = os.path.splitext(self.urdf_files[i])[0] + '.json' | ||
try: | ||
with open(configFile) as json_file: | ||
config = json.load(json_file) | ||
if config.keys() != default_config.keys(): | ||
print('Updating config (old settings will be carried over) - ', configFile) | ||
self.EndReportMessage['updateConfigs'].append(configFile) | ||
self.update_config(configFile, config) | ||
else: | ||
print('Config up to date - ', configFile) | ||
except: | ||
print('Generating new config for - ' + os.path.splitext(self.urdf_files[i])[0]) | ||
self.EndReportMessage['newConfigs'].append(configFile) | ||
self.update_config(configFile) | ||
|
||
def convert_all_urdfs(self): | ||
"""Converts all URDF files according to their '.json' config files.""" | ||
self.create_proto_dir() | ||
print('---------------------------------------') | ||
print('Converting URDF to PROTO...') | ||
print('---------------------------------------') | ||
print('---------------------------------------') | ||
for i in range(len(self.urdf_files)): | ||
# clears any previous Geometry and Material references, so there is no conflict | ||
importer.urdf2webots.parserURDF.Geometry.reference = {} | ||
importer.urdf2webots.parserURDF.Material.namedMaterial = {} | ||
configFile = os.path.splitext(self.urdf_files[i])[0] + '.json' | ||
print('---------------------------------------') | ||
print('Converting: ', self.urdf_files[i]) | ||
print('---------------------------------------') | ||
try: | ||
with open(configFile) as json_file: | ||
config = json.load(json_file) | ||
importer.convert2urdf( | ||
inFile=self.urdf_files[i], | ||
outFile=self.protosPaths[i] if not config['robotName'] else self.protosPaths[i] + config['robotName'] + '.proto', | ||
normal=config['normal'], | ||
boxCollision=config['boxCollision'], | ||
disableMeshOptimization=OverrideCfg['disableMeshOptimization'] if Override else config['disableMeshOptimization'], | ||
enableMultiFile=config['enableMultiFile'], | ||
staticBase=config['staticBase'], | ||
toolSlot=config['toolSlot'], | ||
initRotation=config['initRotation']) | ||
self.EndReportMessage['converted'].append(self.urdf_files[i]) | ||
except Exception as e: | ||
print(e) | ||
self.EndReportMessage['failed'].append(self.urdf_files[i]) | ||
print('---------------------------------------') | ||
|
||
def print_end_report(self): | ||
"""Print the report.""" | ||
print('---------------------------------------') | ||
print('------------End Report-----------------') | ||
print('---------------------------------------') | ||
print('Configs updated:') | ||
print('---------------------------------------') | ||
for config in self.EndReportMessage['updateConfigs']: | ||
print(config) | ||
print('---------------------------------------') | ||
print('Configs created:') | ||
print('---------------------------------------') | ||
for config in self.EndReportMessage['newConfigs']: | ||
print(config) | ||
print('---------------------------------------') | ||
print('Successfully converted URDF files:') | ||
print('---------------------------------------') | ||
for urdf in self.EndReportMessage['converted']: | ||
print(urdf) | ||
if len(self.EndReportMessage['failed']) > 0: | ||
print('---------------------------------------') | ||
print('Failedd URDF conversions:') | ||
print('---------------------------------------') | ||
for urdf in self.EndReportMessage['failed']: | ||
print(urdf) | ||
|
||
def create_test_world(self, spacing=3): | ||
'''Create a TestWorld (<output>/ExtraProjectTest/TestAllRobots.wbt') | ||
where every converted robot or model is placed in a grid, with the | ||
spacing representing the grid size''' | ||
n_models = len(self.urdf_files) | ||
n_row = math.ceil(n_models ** 0.5) # round up the sqrt of the number of models | ||
grid_size = spacing * (n_row + 1) | ||
projectDir = os.path.dirname(self.protoTargetDir) + '/ExtraProjectTest/TestAllRobots.wbt' | ||
worldFile = open(projectDir, 'w') | ||
worldFile.write('#VRML_SIM R2020b utf8\n') | ||
worldFile.write('\n') | ||
worldFile.write('WorldInfo {\n') | ||
worldFile.write(' basicTimeStep 16\n') | ||
worldFile.write(' coordinateSystem "NUE"\n') | ||
worldFile.write('}\n') | ||
worldFile.write('Viewpoint {\n') | ||
worldFile.write(' orientation 0.7180951961816571 -0.6372947429425837 -0.27963315225232777 5.235063704863283\n') | ||
worldFile.write(' position 1.9410928989638234 2.447392518518642 1.7311802992777219\n') | ||
worldFile.write('}\n') | ||
worldFile.write('TexturedBackground {\n') | ||
worldFile.write('}\n') | ||
worldFile.write('TexturedBackgroundLight {\n') | ||
worldFile.write('}\n') | ||
worldFile.write('RectangleArena {\n') | ||
worldFile.write(' floorSize ' + str(grid_size) + ' ' + str(grid_size) + '\n') | ||
worldFile.write(' floorTileSize 0.25 0.25\n') | ||
worldFile.write(' wallHeight 0.05\n') | ||
worldFile.write('}\n') | ||
row = 0 | ||
column = 1 | ||
for root, directories, files in os.walk(os.path.dirname(projectDir)): | ||
for filename in files: | ||
# Join the two strings in order to form the full filepath. | ||
if filename.endswith(".proto"): | ||
filepath = os.path.join(root, filename) | ||
if os.path.dirname(filepath).split('_')[-1] != 'meshes': | ||
name = os.path.splitext(os.path.basename(filename))[0] | ||
if row == n_row: | ||
column += 1 | ||
row = 0 | ||
row += 1 | ||
# add the model to the world file with translation to be spaced in a grid | ||
worldFile.write(name + ' {\n') | ||
worldFile.write(' translation ' + str(column * spacing - grid_size / 2) + ' 0 ' + str(row * spacing - grid_size / 2) + '\n') | ||
worldFile.write('}\n') | ||
worldFile.close() | ||
os.chdir(self.urdf2webots_path) | ||
|
||
|
||
if __name__ == '__main__': | ||
optParser = optparse.OptionParser(usage='usage: %prog [options]') | ||
optParser.add_option('--input', dest='sourcePath', default='', help='Specifies the urdf file to convert.') | ||
optParser.add_option('--output', dest='outPath', default='', help='Specifies the path and, if ending in ".proto", name of the resulting PROTO file.' | ||
' The filename minus the .proto extension will be the robot name.') | ||
optParser.add_option('--force-mesh-optimization', dest='forceMesh', action='store_true', default=False, help='Set if mesh-optimization should be turned on for all conversions. This will take much longer!') | ||
optParser.add_option('--no-project-override', dest='extraProj', action='store_true', default=False, help='Set if new conversions should NOT replace existing ones in "<input>/ExtraProjectTest".') | ||
optParser.add_option('--update-cfg', dest='cfgUpdate', action='store_true', default=False, help='No conversion. Only updates or creates .json config for every URDF file in "automatic_conversion/urdf".') | ||
|
||
options, args = optParser.parse_args() | ||
bc = BatchConversion(options.sourcePath, options.outPath) | ||
Override = options.forceMesh | ||
if options.cfgUpdate: | ||
bc.check_all_configs() | ||
else: | ||
bc.update_and_convert() | ||
if not options.extraProj: | ||
bc.replace_ExtraProjectPath() | ||
bc.create_test_world() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.