Skip to content

Commit 29d3da1

Browse files
Merge pull request #1008 from lardemua/single_tf_noise
Single tf noise
2 parents df787b4 + c571011 commit 29d3da1

File tree

9 files changed

+160
-56
lines changed

9 files changed

+160
-56
lines changed

atom_calibration/scripts/calibrate

+17-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ from atom_calibration.calibration.getters_and_setters import (
2626
from atom_calibration.calibration.objective_function import errorReport, objectiveFunction, replaceTransformsFromJoints
2727
from atom_calibration.calibration.visualization import setupVisualization, visualizationFunction
2828
from atom_core.dataset_io import (addNoiseToInitialGuess,
29-
addNoiseToJointParameters,
29+
addBiasToJointParameters,
30+
addNoiseToTF,
31+
addNoiseFromNoisyTFLinks,
3032
checkIfAtLeastOneLabeledCollectionPerSensor,
3133
filterCollectionsFromDataset,
3234
filterSensorsFromDataset,
@@ -44,6 +46,7 @@ from atom_core.utilities import (atomError, createLambdaExpressionsForArgs,
4446
printComparisonToGroundTruth, saveCommandLineArgsYml)
4547
from atom_core.xacro_io import saveResultsXacro
4648
from atom_core.results_yml_io import saveResultsYml
49+
from atom_core.config_io import parse_list_of_transformations, mutually_inclusive_conditions
4750

4851

4952
# -------------------------------------------------------------------------------
@@ -126,6 +129,13 @@ def main():
126129
type=str, required=False)
127130
ap.add_argument("-jbv", "--joint_bias_values", nargs='+',
128131
help='Operates in tandem with "joint_bias_names"', type=float, required=False)
132+
133+
ap.add_argument("-ntfl", "--noisy_tf_links", type=parse_list_of_transformations, help='''Pairs of links defining the transformations to which noise will be added, in the format linkParent1:linkChild2,linkParentA:linkChildB (links defining a transformation separated by
134+
: and different pairs separeted by ,)
135+
Note : This flag overrides the -nig flag''')
136+
ap.add_argument("-ntfv", "--noisy_tf_values", nargs=2, type=float, metavar=("translation", "rotation"),
137+
help="Translation(m) and Rotation(rad)(respectively) noise values to add to the transformations specified by-ntfl")
138+
129139
ap.add_argument(
130140
"-ssf", "--sensor_selection_function", default=None, type=str,
131141
help="A string to be evaluated into a lambda function that receives a sensor name as input and "
@@ -205,6 +215,9 @@ def main():
205215
args['dataset_folder'] = os.path.dirname(args['json_file'])
206216
output_folder = os.path.dirname(args['json_file'])
207217

218+
# -sce wasn't working because args previously had this key
219+
args['output_folder'] = output_folder
220+
208221
# ---------------------------------------
209222
# --- Read data from file
210223
# ---------------------------------------
@@ -261,8 +274,10 @@ def main():
261274

262275
addNoiseToInitialGuess(dataset, args, selected_collection_key)
263276

277+
addNoiseFromNoisyTFLinks(dataset,args,selected_collection_key)
278+
264279
# Must add noise, transfer new joints to transforms, and only after remove unselected joints
265-
addNoiseToJointParameters(dataset, args)
280+
addBiasToJointParameters(dataset, args)
266281
replaceTransformsFromJoints(dataset)
267282
dataset = filterJointsFromDataset(dataset, args)
268283
dataset = filterJointParametersFromDataset(dataset, args)

atom_core/src/atom_core/config_io.py

+19
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,25 @@
1616
from atom_core.utilities import atomError
1717
from atom_core.system import resolvePath, expandToLaunchEnv
1818

19+
def mutually_inclusive_conditions(A,B):
20+
21+
if A is not None and B is not None:
22+
return True
23+
elif A is None and B is None:
24+
return False
25+
else:
26+
atomError(f'{Fore.RED}-ntfl{Style.RESET_ALL} and {Fore.RED}-ntfv{Style.RESET_ALL} flags are mutually inclusive')
27+
exit()
28+
29+
def parse_list_of_transformations(s):
30+
list_of_transformations = []
31+
for pair in s.split(','):
32+
elements = pair.split(':')
33+
if len(elements) != 2:
34+
raise atomError("Each pair of transformations parsed must contain exactly 2 elements separated by ':'")
35+
list_of_transformations.append(elements)
36+
return list_of_transformations
37+
1938

2039
def dictionaries_have_same_keys(d1, d2):
2140

atom_core/src/atom_core/dataset_io.py

+90-35
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
from colorama import Fore, Style
2525
from rospy_message_converter import message_converter
2626
from std_msgs.msg import Header
27-
from atom_core.config_io import uriReader
28-
from atom_core.naming import generateName, generateKey
27+
from atom_core.config_io import uriReader, mutually_inclusive_conditions
28+
from atom_core.naming import generateName, generateKey, generateCollectionKey
2929
from atom_calibration.collect.label_messages import (
3030
convertDepthImage32FC1to16UC1, convertDepthImage16UC1to32FC1, numpyFromPointCloudMsg)
3131

@@ -787,7 +787,7 @@ def filterAdditionalTfsFromDataset(dataset, args):
787787
return dataset
788788

789789

790-
def addNoiseToJointParameters(dataset, args):
790+
def addBiasToJointParameters(dataset, args):
791791
"""
792792
Adds noise
793793
:param dataset:
@@ -828,6 +828,25 @@ def addNoiseToJointParameters(dataset, args):
828828
collection['joints'][joint_name][joint_param] = collection['joints'][joint_name][
829829
joint_param] + joint_bias
830830

831+
def addNoiseFromNoisyTFLinks(dataset,args,selected_collection_key):
832+
833+
# Verify both arguments were provided
834+
# Unfortunately, mutually inclusive arguments are not built into argparse
835+
# https://github.com/python/cpython/issues/55797
836+
837+
if not mutually_inclusive_conditions(args['noisy_tf_links'], args['noisy_tf_values']):
838+
return
839+
840+
841+
# Iterate through pairs of tf's and apply noise
842+
translation_tf_noise = args['noisy_tf_values'][0]
843+
rotation_tf_noise = args['noisy_tf_values'][1]
844+
845+
for tf_pair in args['noisy_tf_links']:
846+
847+
calibration_parent,calibration_child = tf_pair[0],tf_pair[1]
848+
addNoiseToTF(dataset,selected_collection_key,calibration_parent,calibration_child,translation_tf_noise,rotation_tf_noise)
849+
831850

832851
def addNoiseToInitialGuess(dataset, args, selected_collection_key):
833852
"""
@@ -836,19 +855,38 @@ def addNoiseToInitialGuess(dataset, args, selected_collection_key):
836855
:param args: Makes use of nig, i.e., the amount of noise to add to the initial guess atomic transformations to be
837856
calibrated
838857
"""
858+
# TODO create a issue to discuss if its ok to skip this function call when the noise is 0
859+
# if args['noisy_initial_guess'] == [0,0]:
860+
# print("No noise added to transform's initial guess")
861+
# return
862+
839863
if args['sample_seed'] is not None:
840864
np.random.seed(args['sample_seed'])
841865

842866
nig_trans = args['noisy_initial_guess'][0]
843867
nig_rot = args['noisy_initial_guess'][1]
844868

869+
870+
# Checking if tf to add noise is also defined in -ntfl, in which case the noise shouldn't be added here
871+
# Determining membership in sets is much faster than lists
872+
ntfl_tfs = set()
873+
if args['noisy_tf_links']:
874+
for tf_pair in args['noisy_tf_links']:
875+
ntfl_tfs.add(generateKey(tf_pair[0],tf_pair[1]))
876+
845877
# add noise to additional tfs for simulation
846878
if dataset['calibration_config']['additional_tfs'] is not None:
847879
for _, additional_tf in dataset['calibration_config']['additional_tfs'].items():
880+
848881
calibration_child = additional_tf['child_link']
849882
calibration_parent = additional_tf['parent_link']
850-
addNoiseToTF(dataset, selected_collection_key, calibration_parent,
851-
calibration_child, nig_trans, nig_rot)
883+
884+
tf_to_add_noise = generateKey(calibration_parent,calibration_child)
885+
if tf_to_add_noise in ntfl_tfs:
886+
atomWarn(f'Not adding initial guess noise to {tf_to_add_noise} because its defined in -ntfl')
887+
continue
888+
889+
addNoiseToTF(dataset, selected_collection_key, calibration_parent, calibration_child, nig_trans, nig_rot)
852890

853891
# add noise to sensors tfs for simulation
854892
for sensor_key, sensor in dataset['sensors'].items():
@@ -858,38 +896,63 @@ def addNoiseToInitialGuess(dataset, args, selected_collection_key):
858896
if sensor_key != dataset['calibration_config']['anchored_sensor']:
859897
calibration_child = sensor['calibration_child']
860898
calibration_parent = sensor['calibration_parent']
861-
addNoiseToTF(dataset, selected_collection_key, calibration_parent,
862-
calibration_child, nig_trans, nig_rot)
863899

900+
tf_to_add_noise = generateKey(calibration_parent,calibration_child)
901+
if tf_to_add_noise in ntfl_tfs:
902+
atomWarn(f'Not adding initial guess noise to {tf_to_add_noise} because its defined in -ntfl')
903+
continue
904+
905+
addNoiseToTF(dataset, selected_collection_key, calibration_parent, calibration_child, nig_trans, nig_rot)
864906

865-
def addNoiseToTF(dataset, selected_collection_key, calibration_parent, calibration_child, nig_trans, nig_rot):
907+
# TODO make a basic function called by fixed and multiple
866908

909+
def computeNoise(initial_translation,initial_euler_angles,translation_noise_magnitude,rotation_noise_magnitude):
910+
'''
911+
Computes both the translation and rotation noise given a certain magnitude and returns the new noisy translation/rotation vectors
912+
'''
913+
# Translation
914+
915+
v = np.random.uniform(-1.0, 1.0, 3)
916+
v = v / np.linalg.norm(v)
917+
translation_delta = v * translation_noise_magnitude
918+
919+
new_translation = initial_translation + translation_delta
920+
# Rotation
921+
922+
# Its necessary to redefine 'v' to ensure that the translation and rotation noise aren't always the same given the same magnitude
923+
v = np.random.uniform(-1.0, 1.0, 3)
924+
v = v / np.linalg.norm(v)
925+
rotation_delta = v * rotation_noise_magnitude
926+
927+
new_euler_angles = initial_euler_angles + rotation_delta
928+
929+
return new_translation,new_euler_angles
930+
931+
932+
def addNoiseToTF(dataset, selected_collection_key, calibration_parent, calibration_child, noise_trans, noise_rot):
933+
934+
print(Fore.RED + 'Transformation parent ' + calibration_parent + ' child ' + calibration_child + Style.RESET_ALL)
867935
transform_key = generateKey(calibration_parent, calibration_child, suffix='')
868936

937+
atomWarn(f'Adding noise bigger than 1.0m/rad right now is bugged and might yield unexpected results. Check issue #929 for more information')
938+
869939
# because of #900, and for retrocompatibility with old datasets, we will assume that if the transforms field does
870940
# not exist in the dataset, then the transformation is fixed
871941
if 'transforms' not in dataset or dataset['transforms'][transform_key]['type'] == 'fixed':
872942

873943
# Get original transformation
874944
quat = dataset['collections'][selected_collection_key]['transforms'][transform_key]['quat']
875-
translation = dataset['collections'][selected_collection_key]['transforms'][
876-
transform_key]['trans']
877-
878-
# Add noise to the 6 pose parameters
879-
v = np.random.uniform(-1.0, 1.0, 3)
880-
v = v / np.linalg.norm(v)
881-
new_translation = translation + v * nig_trans
945+
translation = dataset['collections'][selected_collection_key]['transforms'][transform_key]['trans']
882946

883-
v = np.random.choice([-1.0, 1.0], 3) * nig_rot
884947
euler_angles = tf.transformations.euler_from_quaternion(quat)
885-
new_angles = euler_angles + v
948+
949+
950+
new_translation,new_euler_angles = computeNoise(translation,euler_angles,noise_trans,noise_rot)
886951

887952
# Replace the original atomic transformations by the new noisy ones
888-
new_quat = tf.transformations.quaternion_from_euler(
889-
new_angles[0], new_angles[1], new_angles[2])
953+
new_quat = tf.transformations.quaternion_from_euler(new_euler_angles[0], new_euler_angles[1], new_euler_angles[2])
890954
dataset['collections'][selected_collection_key]['transforms'][transform_key]['quat'] = new_quat
891-
dataset['collections'][selected_collection_key]['transforms'][transform_key]['trans'] = list(
892-
new_translation)
955+
dataset['collections'][selected_collection_key]['transforms'][transform_key]['trans'] = list(new_translation)
893956

894957
# Copy randomized transform to all collections
895958
for collection_key, collection in dataset['collections'].items():
@@ -902,26 +965,18 @@ def addNoiseToTF(dataset, selected_collection_key, calibration_parent, calibrati
902965

903966
for collection_key, collection in dataset["collections"].items():
904967

905-
# Get original transformation
906968
quat = dataset['collections'][collection_key]['transforms'][transform_key]['quat']
907-
translation = dataset['collections'][collection_key]['transforms'][transform_key][
908-
'trans']
969+
translation = dataset['collections'][collection_key]['transforms'][transform_key]['trans']
909970

910-
# Add noise to the 6 pose parameters
911-
v = np.random.uniform(-1.0, 1.0, 3)
912-
v = v / np.linalg.norm(v)
913-
new_translation = translation + v * nig_trans
914-
915-
v = np.random.choice([-1.0, 1.0], 3) * nig_rot
916971
euler_angles = tf.transformations.euler_from_quaternion(quat)
917-
new_angles = euler_angles + v
972+
973+
new_translation,new_euler_angles = computeNoise(translation,euler_angles,noise_trans,noise_rot)
974+
975+
new_quat = tf.transformations.quaternion_from_euler(new_euler_angles[0], new_euler_angles[1], new_euler_angles[2])
918976

919977
# Replace the original atomic transformations by the new noisy ones
920-
new_quat = tf.transformations.quaternion_from_euler(
921-
new_angles[0], new_angles[1], new_angles[2])
922978
dataset['collections'][collection_key]['transforms'][transform_key]['quat'] = new_quat
923-
dataset['collections'][collection_key]['transforms'][transform_key]['trans'] = list(
924-
new_translation)
979+
dataset['collections'][collection_key]['transforms'][transform_key]['trans'] = list(new_translation)
925980

926981

927982
def copyTFToDataset(calibration_parent, calibration_child, source_dataset, target_dataset):

atom_core/src/atom_core/naming.py

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ def generateName(name, prefix='', suffix='', separator='_'):
1212
def generateKey(parent, child, suffix=''):
1313
return parent + '-' + child + suffix
1414

15+
def generateCollectionKey(collection_number):
16+
return f"{collection_number:03d}"
1517

1618
def generateLabeledTopic(topic, collection_key=None, type='2d', suffix=''):
1719
"""Returns a standarized string for labeled sensor msg topics.

atom_core/src/atom_core/utilities.py

+21-8
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from pynput import keyboard
2626
import yaml
2727

28-
from tf.transformations import quaternion_matrix, euler_from_matrix
28+
from tf.transformations import quaternion_matrix, euler_from_matrix, euler_from_quaternion
2929

3030
from atom_core.naming import generateKey
3131
from atom_core.system import execute, removeColorsFromText
@@ -53,6 +53,7 @@ def compareAtomTransforms(transform_1, transform_2):
5353
transform_2: transformation 2
5454
"""
5555

56+
5657
# Create a 4x4 transformation for transform_1
5758
t1 = quaternion_matrix(transform_1['quat'])
5859
t1[0:3, 3] = transform_1['trans']
@@ -61,21 +62,33 @@ def compareAtomTransforms(transform_1, transform_2):
6162
t2 = quaternion_matrix(transform_2['quat'])
6263
t2[0:3, 3] = transform_2['trans']
6364

64-
# Method: We will use the following method. If T1 and T2 are the same, then multiplying one by the inverse of the other will produce and identity matrix, with zero translation and rotation. So we will do the multiplication and then evaluation the amount of rotation and translation in the resulting matrix.
65+
v = t2[0:3, 3] - t1[0:3, 3]
66+
67+
# Method: We will use the following method. If T1 and T2 are the same, then multiplying one by the inverse of the other will produce and identity matrix, with zero translation and rotation. So we will do the multiplication and then evaluation of the amount of rotation and translation in the resulting matrix.
6568
# print('Comparing \nt1= ' + str(t1) + ' \n\nt2=' + str(t2))
6669

6770
t_delta = np.dot(np.linalg.inv(t1), t2)
6871
# print('t_delta = ' + str(t_delta))
6972

7073
rotation_delta = t_delta[0:3, 0:3]
71-
roll, pitch, yaw = euler_from_matrix(rotation_delta)
72-
74+
# roll, pitch, yaw = euler_from_matrix(rotation_delta)
7375
translation_delta = t_delta[0:3, 3]
74-
# print('translation_delta = ' + str(translation_delta))
76+
77+
euler_angles_init = euler_from_quaternion(transform_1['quat'])
78+
euler_angles_final = euler_from_quaternion(transform_2['quat'])
79+
80+
81+
deuler = np.subtract(euler_angles_final,euler_angles_init)
82+
rotation_error = np.linalg.norm(deuler)
7583

7684
# global metrics
77-
translation_error = float(abs(np.average(translation_delta)))
78-
rotation_error = float(np.average([abs(roll), abs(pitch), abs(yaw)]))
85+
# translation_error = float(abs(np.average(translation_delta)))
86+
translation_error = np.linalg.norm(translation_delta)
87+
# rotation_error = np.linalg.norm([roll, pitch, yaw])
88+
89+
90+
# print('translation error = ' + str(translation_error))
91+
# print('rotation error = ' + str(rotation_error))
7992

8093
return translation_error, rotation_error
8194

@@ -187,7 +200,7 @@ def printComparisonToGroundTruth(
187200
if dataset['calibration_config']['additional_tfs'] is not None:
188201
for additional_tf_key, additional_tf in dataset['calibration_config']['additional_tfs'].items():
189202

190-
transform_key = generateKey(additional_tf["parent_link"], sensor["child_link"])
203+
transform_key = generateKey(additional_tf["parent_link"], additional_tf["child_link"])
191204
row = [transform_key, Fore.LIGHTCYAN_EX + additional_tf_key + Style.RESET_ALL]
192205

193206

atom_evaluation/scripts/other_calibrations/cv_eye_to_hand.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
from atom_calibration.calibration.visualization import getCvImageFromCollectionSensor
2828
from atom_core.atom import getChain, getTransform
29-
from atom_core.dataset_io import addNoiseToInitialGuess, filterCollectionsFromDataset, loadResultsJSON, saveAtomDataset, addNoiseToJointParameters
29+
from atom_core.dataset_io import addNoiseToInitialGuess, filterCollectionsFromDataset, loadResultsJSON, saveAtomDataset, addBiasToJointParameters
3030
from atom_core.geometry import matrixToTranslationRotation, translationRotationToTransform, traslationRodriguesToTransform, translationQuaternionToTransform
3131
from atom_core.naming import generateKey
3232
from atom_core.transformations import compareTransforms
@@ -136,7 +136,7 @@ def main():
136136
# --- Add noise to the joint parameters to be calibrated.
137137
# ---------------------------------------
138138
addNoiseToInitialGuess(dataset, args, selected_collection_key)
139-
addNoiseToJointParameters(dataset, args)
139+
addBiasToJointParameters(dataset, args)
140140

141141
# Apply new joint values to the tfs
142142
if args['joint_bias_names'] is not None:

atom_evaluation/scripts/other_calibrations/cv_eye_to_hand_robot_world.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
from atom_calibration.calibration.visualization import getCvImageFromCollectionSensor
2828
from atom_core.atom import getChain, getTransform
29-
from atom_core.dataset_io import addNoiseToInitialGuess, filterCollectionsFromDataset, loadResultsJSON, saveAtomDataset, addNoiseToJointParameters
29+
from atom_core.dataset_io import addNoiseToInitialGuess, filterCollectionsFromDataset, loadResultsJSON, saveAtomDataset, addBiasToJointParameters
3030
from atom_core.geometry import matrixToTranslationRotation, translationRotationToTransform, traslationRodriguesToTransform, translationQuaternionToTransform
3131
from atom_core.naming import generateKey
3232
from atom_core.transformations import compareTransforms
@@ -136,7 +136,7 @@ def main():
136136
# --- Add noise to the joint parameters to be calibrated.
137137
# ---------------------------------------
138138
addNoiseToInitialGuess(dataset, args, selected_collection_key)
139-
addNoiseToJointParameters(dataset, args)
139+
addBiasToJointParameters(dataset, args)
140140

141141
# Apply new joint values to the tfs
142142
if args['joint_bias_names'] is not None:

0 commit comments

Comments
 (0)