Skip to content

Commit c1e6ff2

Browse files
Merge pull request #568 from lardemua:JorgeFernandes-Git/issue543
orgeFernandes-Git/issue543
2 parents c7cb751 + d8b1c24 commit c1e6ff2

39 files changed

+2837
-48
lines changed

atom_calibration/scripts/calibrate

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ from atom_core.dataset_io import (addNoiseToInitialGuess, checkIfAtLeastOneLabel
2626
filterCollectionsFromDataset, filterSensorsFromDataset, loadResultsJSON,
2727
saveResultsJSON)
2828
from atom_core.naming import generateName, generateKey
29-
from atom_core.utilities import waitForKeyPress2
29+
from atom_core.utilities import waitForKeyPress2, checkAdditionalTfs
3030
from atom_core.xacro_io import saveResultsXacro
3131

3232

@@ -91,6 +91,9 @@ def main():
9191
help="Draw a ghost mesh with the systems initial pose. Good for debugging.")
9292
ap.add_argument("-oj", "--output_json", help="Full path to output json file.",
9393
type=str, required=False, default=None)
94+
95+
# save results of calibration in a csv file
96+
ap.add_argument("-sfr", "--save_file_results", help="Output folder to where the results will be stored.", type=str, required=False)
9497

9598
# Roslaunch adds two arguments (__name and __log) that break our parser. Lets remove those.
9699
arglist = [x for x in sys.argv[1:] if not x.startswith("__")]
@@ -199,6 +202,11 @@ def main():
199202
transform_key = generateKey(sensor["calibration_parent"], sensor["calibration_child"])
200203
transforms_set.add(transform_key)
201204

205+
if checkAdditionalTfs(dataset):
206+
for _, additional_tf in dataset['calibration_config']['additional_tfs'].items():
207+
transform_key = generateKey(additional_tf['parent_link'], additional_tf['child_link'])
208+
transforms_set.add(transform_key)
209+
202210
for transform_key in transforms_set: # push six parameters for each transform to be optimized.
203211
initial_transform = getterTransform(dataset, transform_key=transform_key,
204212
collection_name=selected_collection_key)
@@ -321,9 +329,17 @@ def main():
321329
continue
322330

323331
# Sensor related parameters
324-
sensors_transform_key = generateKey(
325-
sensor["calibration_parent"], sensor["calibration_child"])
326-
params = opt.getParamsContainingPattern(sensors_transform_key)
332+
# sensors_transform_key = generateKey(
333+
# sensor["calibration_parent"], sensor["calibration_child"])
334+
# params = opt.getParamsContainingPattern(sensors_transform_key)
335+
336+
# Issue #543: Create the list of transformations that influence this residual by analyzing the transformation chain.
337+
params = []
338+
transforms_list = list(transforms_set) # because not sure if we can use a set for some of the next steps
339+
for transform_in_chain in sensor['chain']:
340+
transform_key = generateKey(transform_in_chain["parent"], transform_in_chain["child"])
341+
if transform_key in transforms_list:
342+
params.extend(opt.getParamsContainingPattern(transform_key))
327343

328344
# Intrinsics parameters
329345
if sensor["modality"] == "rgb" and args["optimize_intrinsics"]:
@@ -434,14 +450,14 @@ def main():
434450
options = {"ftol": 1e-4, "xtol": 1e-4, "gtol": 1e-4, "diff_step": None, "x_scale": "jac", }
435451
# options = {'ftol': 1e-5, 'xtol': 1e-5, 'gtol': 1e-5, 'diff_step': None, 'x_scale': 'jac'}
436452
# options = {'ftol': 1e-6, 'xtol': 1e-6, 'gtol': 1e-6, 'diff_step': None, 'x_scale': 'jac'}
437-
# options = {'ftol': 1e-8, 'xtol': 1e-8, 'gtol': 1e-8, 'diff_step': None, 'x_scale': 'jac'o
453+
# options = {'ftol': 1e-8, 'xtol': 1e-8, 'gtol': 1e-8, 'diff_step': None, 'x_scale': 'jac'}
438454
opt.startOptimization(optimization_options=options)
439455

440456
if args["phased_execution"]:
441457
waitForKeyPress2(opt.callObjectiveFunction, timeout=0.1,
442458
message=Fore.BLUE + "Optimization finished" + Style.RESET_ALL)
443459
# Final error report
444-
errorReport(dataset=dataset, residuals=objectiveFunction(opt.data_models), normalizer=normalizer)
460+
errorReport(dataset=dataset, residuals=objectiveFunction(opt.data_models), normalizer=normalizer, args=args)
445461

446462
# ---------------------------------------
447463
# --- Save updated JSON file
@@ -462,7 +478,8 @@ def main():
462478
# ---------------------------------------
463479
# --- Save updated xacro
464480
# ---------------------------------------
465-
saveResultsXacro(dataset, selected_collection_key)
481+
# saveResultsXacro(dataset, selected_collection_key)
482+
saveResultsXacro(dataset, selected_collection_key, list(transforms_set))
466483

467484

468485
if __name__ == "__main__":

atom_calibration/scripts/collect_data

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ if __name__ == "__main__":
122122
'returns True or False to indicate if the sensor should be labelled.'
123123
' The Syntax is lambda name: f(x), where f(x) is the function in python '
124124
'language. Example: lambda name: name in ["lidar1", "camera2"] , to avoid labeling of sensors'
125-
' lida1 and camera2.')
125+
' lidar1 and camera2.')
126126

127127
args = vars(ap.parse_args(args=atom_core.ros_utils.filterLaunchArguments(sys.argv)))
128128
print("\nArgument list=" + str(args) + '\n')

atom_calibration/scripts/configure_calibration_pkg

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,49 @@ if __name__ == "__main__":
248248
for frame in ['child_link', 'parent_link', 'link']:
249249
frames_to_verify[sensor_key + '.' + frame] = config['sensors'][sensor_key][frame]
250250

251+
# Verify if exist additional tfs to be estimated
252+
if 'additional_tfs' in config:
253+
if config['additional_tfs'] != '':
254+
# Check if link, child_link and parent_link exist in the bagfile.
255+
for additional_tf_key in config['additional_tfs']:
256+
for frame in ['child_link', 'parent_link']:
257+
frames_to_verify[additional_tf_key + '.' +
258+
frame] = config['additional_tfs'][additional_tf_key][frame]
259+
260+
# Verify that the parent and child links for each additional_tfs are a direct transformation
261+
for additional_tf_key, additional_tf in config['additional_tfs'].items():
262+
edge_in_config = (config['additional_tfs'][additional_tf_key]['parent_link'],
263+
config['additional_tfs'][additional_tf_key]['child_link'])
264+
265+
if not edge_in_config in gx.edges():
266+
raise ValueError(
267+
'Error: additional_tfs ' + Fore.BLUE + additional_tf_key + Style.RESET_ALL + ' parent and child links are ' + Fore.YELLOW +
268+
edge_in_config[0] + Fore.BLACK + ' and ' + Fore.YELLOW + edge_in_config[1] + Fore.BLACK +
269+
' but there is no such atomic transformation in the tf tree.' + Style.RESET_ALL)
270+
271+
for additional_tf_key, additional_tf in config['additional_tfs'].items():
272+
# path between root and additional_tfs data frame
273+
paths = nx.shortest_path(gx, config['world_link'], additional_tf['child_link'])
274+
print('Path from ' + Fore.RED + config['world_link'] + Fore.RESET + ' to additional_tfs ' + Fore.LIGHTMAGENTA_EX +
275+
additional_tf_key + Fore.RESET + ':')
276+
at = '['
277+
for path in paths:
278+
if path == config['world_link']:
279+
at += Fore.RED + path + Fore.RESET + ', '
280+
elif path == additional_tf['parent_link'] or path == additional_tf['child_link']:
281+
at += Fore.BLUE + path + Fore.RESET + ', '
282+
else:
283+
for joint in description.joints: # atomic transformations are given by the joints
284+
if path == joint.parent or path == joint.child:
285+
if joint.type == 'fixed':
286+
at += path + ', '
287+
else:
288+
at += Fore.GREEN + path + Fore.RESET + ', '
289+
break
290+
at = at[:-2]
291+
at += ']'
292+
print(at)
293+
251294
print('Checking if all tf frames exist ...')
252295
print(frames_to_verify)
253296
for key, frame in frames_to_verify.items():
@@ -350,6 +393,13 @@ if __name__ == "__main__":
350393
label += 'To be calibrated\n'
351394
rgb = matplotlib.colors.rgb2hex([0, 0, 1])
352395

396+
if 'additional_tfs' in config:
397+
if config['additional_tfs'] != '':
398+
for additional_tfs_key, additional_tfs in config['additional_tfs'].items():
399+
if additional_tfs['parent_link'] == parent and additional_tfs['child_link'] == child:
400+
label += 'To be calibrated\n'
401+
rgb = matplotlib.colors.rgb2hex([0, 0, 1])
402+
353403
label += type
354404

355405
g.edge(parent, child, color=rgb, style='solid', _attributes={'penwidth': '1', 'fontcolor': rgb},

atom_calibration/scripts/set_initial_estimate

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ from urdf_parser_py.urdf import Pose as URDFPose
2222
from atom_core.ros_utils import filterLaunchArguments
2323
from atom_core.config_io import loadConfig
2424
from atom_calibration.initial_estimate.sensor import Sensor
25+
from atom_calibration.initial_estimate.additional_tf import Additional_tf
26+
2527

2628

2729
class InteractiveFirstGuess(object):
@@ -31,6 +33,7 @@ class InteractiveFirstGuess(object):
3133
self.args = args # command line arguments
3234
self.config = None # calibration config
3335
self.sensors = [] # sensors
36+
self.additional_tfs = [] # additional_tfs
3437
self.urdf = None # robot URDF
3538
self.server = None # interactive markers server
3639
self.menu = None
@@ -54,13 +57,30 @@ class InteractiveFirstGuess(object):
5457

5558
print('Number of sensors: ' + str(len(self.config['sensors'])))
5659

60+
if self.checkAdditionalTfs():
61+
print('Number of additional_tfs: ' +
62+
str(len(self.config['additional_tfs'])))
63+
5764
# Init interaction
5865
self.server = InteractiveMarkerServer('set_initial_estimate')
5966
self.menu = MenuHandler()
6067

6168
self.createInteractiveMarker(self.config['world_link'])
62-
self.menu.insert("Save sensors configuration", callback=self.onSaveInitialEstimate)
63-
self.menu.insert("Reset all sensors to initial configuration", callback=self.onResetAll)
69+
70+
if self.checkAdditionalTfs():
71+
# sensors and additional tfs to be calibrated
72+
self.menu.insert("Save sensors and additional transformations configuration",
73+
callback=self.onSaveInitialEstimate)
74+
self.menu.insert(
75+
"Reset all sensors and additional transformations to initial configuration", callback=self.onResetAll)
76+
77+
else:
78+
# only sensors to be calibrated
79+
self.menu.insert("Save sensors configuration",
80+
callback=self.onSaveInitialEstimate)
81+
self.menu.insert(
82+
"Reset all sensors to initial configuration", callback=self.onResetAll)
83+
6484
self.menu.reApply(self.server)
6585

6686
# Waiting for the loading of the bagfile
@@ -69,6 +89,11 @@ class InteractiveFirstGuess(object):
6989
# Create a colormap so that we have one color per sensor
7090
self.cm_sensors = cm.Pastel2(np.linspace(0, 1, len(self.config['sensors'].keys())))
7191

92+
# Create a colormap so that we have one color per additional_tfs
93+
if self.checkAdditionalTfs():
94+
self.cm_additional_tf = cm.Pastel2(np.linspace(
95+
0, 1, len(self.config['additional_tfs'].keys())))
96+
7297
# For each node generate an interactive marker.
7398
sensor_idx = 0
7499
for name, sensor in self.config['sensors'].items():
@@ -87,6 +112,24 @@ class InteractiveFirstGuess(object):
87112
sensor_idx += 1
88113
print('... done')
89114

115+
# For each node generate an interactive marker.
116+
if self.checkAdditionalTfs():
117+
additional_tf_idx = 0
118+
for name, additional_tf in self.config['additional_tfs'].items():
119+
print(Fore.BLUE + '\nAdditional_tfs name is ' +
120+
name + Style.RESET_ALL)
121+
params = {
122+
"frame_world": self.config['world_link'],
123+
"frame_opt_parent": additional_tf['parent_link'],
124+
"frame_opt_child": additional_tf['child_link'],
125+
"frame_additional_tf": additional_tf['child_link'],
126+
"additional_tf_color": tuple(self.cm_additional_tf[additional_tf_idx, :]),
127+
"marker_scale": self.args['marker_scale']}
128+
# Append to the list of additional_tfs
129+
self.additional_tfs.append(Additional_tf(name, self.server, self.menu, **params))
130+
additional_tf_idx += 1
131+
print('... done')
132+
90133
self.server.applyChanges()
91134

92135
def onSaveInitialEstimate(self, feedback):
@@ -115,10 +158,33 @@ class InteractiveFirstGuess(object):
115158
print("Writing fist guess urdf to '{}'".format(outfile))
116159
out.write(self.urdf.to_xml_string())
117160

161+
if self.checkAdditionalTfs():
162+
for additional_tfs in self.additional_tfs:
163+
# find corresponding joint for this additional_tfs
164+
for joint in self.urdf.joints:
165+
if additional_tfs.opt_child_link == joint.child and additional_tfs.opt_parent_link == joint.parent:
166+
trans = additional_tfs.optT.getTranslation()
167+
euler = additional_tfs.optT.getEulerAngles()
168+
joint.origin.xyz = list(trans)
169+
joint.origin.rpy = list(euler)
170+
171+
# Write the urdf file with atom_calibration's
172+
# source path as base directory.
173+
rospack = rospkg.RosPack()
174+
# outfile = rospack.get_path('atom_calibration') + os.path.abspath('/' + self.args['filename'])
175+
outfile = self.args['filename']
176+
with open(outfile, 'w') as out:
177+
print("Writing fist guess urdf to '{}'".format(outfile))
178+
out.write(self.urdf.to_xml_string())
179+
118180
def onResetAll(self, feedback):
119181
for sensor in self.sensors:
120182
sensor.resetToInitalPose()
121183

184+
if self.checkAdditionalTfs():
185+
for additional_tfs in self.additional_tfs:
186+
additional_tfs.resetToInitalPose()
187+
122188
def createInteractiveMarker(self, world_link):
123189
marker = InteractiveMarker()
124190
marker.header.frame_id = world_link
@@ -186,8 +252,19 @@ class InteractiveFirstGuess(object):
186252
marker.controls.append(control)
187253

188254
self.server.insert(marker, self.onSaveInitialEstimate)
255+
256+
if self.checkAdditionalTfs():
257+
self.server.insert(marker, self.onSaveInitialEstimate)
258+
259+
189260
self.menu.apply(self.server, marker.name)
190261

262+
def checkAdditionalTfs(self):
263+
if 'additional_tfs' in self.config:
264+
if self.config['additional_tfs'] != '':
265+
return True
266+
else:
267+
return False
191268

192269
if __name__ == "__main__":
193270
# Parse command line arguments

atom_calibration/src/atom_calibration/calibration/objective_function.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
# -------------------------------------------------------------------------------
3535
# --- FUNCTIONS
3636
# -------------------------------------------------------------------------------
37-
def errorReport(dataset, residuals, normalizer):
37+
def errorReport(dataset, residuals, normalizer, args):
3838

3939
from prettytable import PrettyTable
4040
table_header = ['Collection']
@@ -56,28 +56,35 @@ def errorReport(dataset, residuals, normalizer):
5656
table_header.append(sensor_key + units)
5757

5858
table = PrettyTable(table_header)
59+
table_to_save = PrettyTable(table_header) # table to save. This table was created, because the original has colors and the output csv save them as random characters
5960

6061
# Build each row in the table
6162
keys = sorted(dataset['collections'].keys(), key=lambda x: int(x))
6263
for collection_key in keys:
6364
row = [collection_key]
65+
row_save = [collection_key]
6466
v = []
6567
for sensor_key, sensor in dataset['sensors'].items():
6668
keys = [k for k in residuals.keys() if ('c' + collection_key) == k.split('_')[0] and sensor_key in k]
6769
v = [residuals[k] * normalizer[sensor['modality']] for k in keys if residuals[k]]
6870
if v:
6971
value = '%.4f' % np.mean(v)
7072
row.append(value)
73+
row_save.append(value)
7174
else:
7275
row.append(Fore.LIGHTBLACK_EX + '---' + Style.RESET_ALL)
76+
row_save.append('---')
7377

7478
table.add_row(row)
79+
table_to_save.add_row(row)
7580

7681
# Compute averages and add a bottom row
7782
bottom_row = [] # Compute averages and add bottom row to table
83+
bottom_row_save = []
7884
for col_idx, _ in enumerate(table_header):
7985
if col_idx == 0:
8086
bottom_row.append(Fore.BLUE + Style.BRIGHT + 'Averages' + Fore.BLACK + Style.NORMAL)
87+
bottom_row_save.append('Averages')
8188
continue
8289

8390
total = 0
@@ -97,8 +104,10 @@ def errorReport(dataset, residuals, normalizer):
97104
value = '---'
98105

99106
bottom_row.append(Fore.BLUE + value + Fore.BLACK)
107+
bottom_row_save.append(value)
100108

101109
table.add_row(bottom_row)
110+
table_to_save.add_row(bottom_row_save)
102111

103112
# Put larger errors in red per column (per sensor)
104113
for col_idx, _ in enumerate(table_header):
@@ -121,11 +130,17 @@ def errorReport(dataset, residuals, normalizer):
121130
table.rows[max_row_idx][col_idx] = Fore.RED + table.rows[max_row_idx][col_idx] + Style.RESET_ALL
122131

123132
table.align = 'c'
133+
table_to_save.align = 'c'
124134
print(Style.BRIGHT + 'Errors per collection' + Style.RESET_ALL + ' (' + Fore.YELLOW + 'anchored sensor' +
125135
Fore.BLACK + ', ' + Fore.RED + ' max error per sensor' + Fore.BLACK + ', ' + Fore.LIGHTBLACK_EX +
126136
'not detected as \"---\")' + Style.RESET_ALL)
127137
print(table)
128138

139+
# save results in csv file
140+
if args['save_file_results'] != None:
141+
with open(args['save_file_results'] + 'calibration_results.csv', 'w', newline='') as f_output:
142+
f_output.write(table_to_save.get_csv_string())
143+
129144

130145
@Cache(args_to_ignore=['_dataset'])
131146
def getPointsInPatternAsNPArray(_collection_key, _sensor_key, _dataset):
@@ -694,6 +709,6 @@ def objectiveFunction(data):
694709
raise ValueError("Unknown sensor msg_type or modality")
695710

696711
if args['verbose'] and data['status']['is_iteration']:
697-
errorReport(dataset=dataset, residuals=r, normalizer=normalizer)
712+
errorReport(dataset=dataset, residuals=r, normalizer=normalizer, args=args)
698713

699714
return r # Return the residuals

0 commit comments

Comments
 (0)