33import numpy as np
44import rclpy
55import tf2_ros
6- from geometry_msgs .msg import TransformStamped
6+ from geometry_msgs .msg import Pose , Transform , TransformStamped
77from nav_msgs .msg import Odometry
8+ from pose_graph_tools_msgs .msg import PoseGraph , PoseGraphEdge
89from rclpy .node import Node
9- from scipy .spatial .transform import Rotation as R
10+ from rclpy .time import Time
11+ from scipy .spatial .transform import Rotation
1012
1113
1214def _invert_pose (p ):
1315 p_inv = np .zeros ((4 , 4 ))
1416 p_inv [:3 , :3 ] = p [:3 , :3 ].T
1517 p_inv [:3 , 3 ] = - p [:3 , :3 ].T @ p [:3 , 3 ]
16- p_inv [3 , 3 ] = 1
18+ p_inv [3 , 3 ] = 1.0
1719 return p_inv
1820
1921
20- def _mat_from_pose (pose ):
21- pose_out = np .zeros ((4 , 4 ))
22- q = [pose .orientation .x , pose .orientation .y , pose .orientation .z , pose .orientation .w ]
23- t = [pose .position .x , pose .position .y , pose .position .z ]
24- Rmat = R .from_quat (q ).as_matrix ()
25- pose_out [:3 , :3 ] = Rmat
26- pose_out [:3 , 3 ] = t
27- pose_out [3 , 3 ] = 1
28- return pose_out
29-
30-
31- def Exp (rho , theta ):
32- cos = np .cos
33- sin = np .sin
34- norm = np .linalg .norm
22+ def _exp (rho , theta ):
23+ theta_norm = np .linalg .norm (theta )
3524 xi_hat = np .zeros ((4 , 4 ))
3625 theta_hat = np .array (
3726 [[0 , - theta [2 ], theta [1 ]], [theta [2 ], 0 , - theta [0 ]], [- theta [1 ], theta [0 ], 0 ]]
@@ -41,20 +30,54 @@ def Exp(rho, theta):
4130 return (
4231 np .eye (4 )
4332 + xi_hat
44- + (1 - cos (norm (theta ))) / (norm (theta ) ** 2 ) * xi_hat @ xi_hat
45- + (norm (theta ) - sin (norm (theta )))
46- / (norm (theta ) ** 3 )
47- * xi_hat
48- @ xi_hat
49- @ xi_hat
33+ + (1 - np .cos (theta_norm )) / (theta_norm ** 2 ) * xi_hat @ xi_hat
34+ + (theta_norm - np .sin (theta_norm )) / (theta_norm ** 3 ) * xi_hat @ xi_hat @ xi_hat
5035 )
5136
5237
53- def _calc_odom_delta (odom_old , odom_new ):
54- w_T_old = _mat_from_pose (odom_old .pose .pose )
55- w_T_new = _mat_from_pose (odom_new .pose .pose )
56- delta = _invert_pose (w_T_old ) @ w_T_new
57- return delta
38+ def _translation_distance (pose1 , pose2 ):
39+ return np .linalg .norm (pose1 [:3 , 3 ] - pose2 [:3 , 3 ])
40+
41+
42+ def _rot_from_pose (pose ):
43+ q = [pose .orientation .x , pose .orientation .y , pose .orientation .z , pose .orientation .w ]
44+ return Rotation .from_quat (q ).as_matrix ()
45+
46+
47+ def _mat_from_pose (pose ):
48+ pose_out = np .zeros ((4 , 4 ))
49+ pose_out [:3 , :3 ] = _rot_from_pose (pose )
50+ pose_out [:3 , 3 ] = [pose .position .x , pose .position .y , pose .position .z ]
51+ pose_out [3 , 3 ] = 1.0
52+ return pose_out
53+
54+
55+ def _pose_from_mat (p ):
56+ q = Rotation .from_matrix (p [:3 , :3 ]).as_quat ()
57+
58+ msg = Pose ()
59+ msg .position .x = p [0 , 3 ]
60+ msg .position .y = p [1 , 3 ]
61+ msg .position .z = p [2 , 3 ]
62+ msg .orientation .x = q [0 ]
63+ msg .orientation .y = q [1 ]
64+ msg .orientation .z = q [2 ]
65+ msg .orientation .w = q [3 ]
66+ return msg
67+
68+
69+ def _transform_from_mat (p ):
70+ q = Rotation .from_matrix (p [:3 , :3 ]).as_quat ()
71+
72+ msg = Transform ()
73+ msg .translation .x = p [0 , 3 ]
74+ msg .translation .y = p [1 , 3 ]
75+ msg .translation .z = p [2 , 3 ]
76+ msg .rotation .x = q [0 ]
77+ msg .rotation .y = q [1 ]
78+ msg .rotation .z = q [2 ]
79+ msg .rotation .w = q [3 ]
80+ return msg
5881
5982
6083class NoisyTransformPublisher (Node ):
@@ -65,106 +88,140 @@ class NoisyTransformPublisher(Node):
6588
6689 def __init__ (self ):
6790 super ().__init__ ("noisy_tf_publisher_node" )
68- self ._yaw_std_dev = self ._get_param ("yaw_std_dev" , 0.01 ).double_value
69- self ._xyz_std_dev = self ._get_param ("xyz_std_dev" , 0.01 ).double_value
70- self ._wrong_velocity_frame = self ._get_param (
71- "wrong_velocity_frame" , False
72- ).bool_value
73- self ._start_from_origin = self ._get_param ("start_from_origin" , True ).bool_value
74- self ._add_noise = self ._get_param ("add_noise" , True ).bool_value
91+ self ._last_noisy_pose = None
92+ self ._last_loop_closure_stamp = None
93+ self ._poses = []
94+ self ._stamps = []
95+
7596 self ._parent_frame = self ._get_param ("parent_frame" , "odom" ).string_value
7697 self ._child_frame = self ._get_param ("child_frame" , "base_link" ).string_value
77- self ._br = tf2_ros .TransformBroadcaster (self )
78- self ._sub = self .create_subscription (Odometry , "~/odom" , self ._odom_cb , 10 )
79- self ._pub = self .create_publisher (Odometry , "noisy_odom_out" , 10 )
80- self ._last_gt_odom = None
81- self ._last_noisy_odom = None
82-
83- def _odom_cb (self , odom_msg ):
84- if self ._last_gt_odom is None :
85- self ._last_gt_odom = odom_msg
86- self ._last_noisy_odom = Odometry () if self ._start_from_origin else odom_msg
87- return
88-
89- pose_delta = _calc_odom_delta (self ._last_gt_odom , odom_msg )
98+ self ._yaw_std_dev = self ._get_param ("yaw_std_dev" , 0.001 ).double_value
99+ self ._xy_std_dev = self ._get_param ("xy_std_dev" , 0.001 ).double_value
100+ self ._z_std_dev = self ._get_param ("z_std_dev" , 0.001 ).double_value
90101
91- self ._last_noisy_odom = self ._add_odom_noise (
92- self ._last_noisy_odom , odom_msg , pose_delta
93- )
94-
95- self ._publish_noisy_transform (
96- self ._parent_frame ,
97- self ._child_frame ,
98- odom_msg .header .stamp ,
99- self ._last_noisy_odom .pose .pose ,
100- )
101-
102- self ._pub .publish (self ._last_noisy_odom )
103- self ._last_gt_odom = odom_msg
104-
105- def _publish_noisy_transform (self , fixed_frame , child_frame , time , noisy_pose ):
106- t = TransformStamped ()
107- t .header .stamp = time
108- t .header .frame_id = fixed_frame
109- t .child_frame_id = child_frame
102+ self ._start_from_origin = self ._get_param ("start_from_origin" , True ).bool_value
103+ self ._add_noise = self ._get_param ("add_noise" , True ).bool_value
104+ self ._wrong_velocity_frame = self ._get_param (
105+ "wrong_velocity_frame" , False
106+ ).bool_value
110107
111- # Set position from noisy pose
112- t .transform .translation .x = noisy_pose .position .x
113- t .transform .translation .y = noisy_pose .position .y
114- t .transform .translation .z = noisy_pose .position .z
108+ self ._enable_pseudo_loop_closures = self ._get_param (
109+ "enable_pseudo_loop_closures" , True
110+ ).bool_value
111+ self ._robot_id = self ._get_param ("robot_id" , 0 ).integer_value
112+ self ._loop_closure_threshold_m = self ._get_param (
113+ "loop_closure_threshold_m" , 0.5
114+ ).double_value
115+ min_lc_sep_s = self ._get_param (
116+ "minimum_loop_closure_separation_s" , 1.0
117+ ).double_value
118+ self ._min_loop_closure_separation_ns = int (1.0e9 * min_lc_sep_s )
119+ lc_window_s = self ._get_param ("loop_closure_window_ns" , 30.0 ).double_value
120+ self ._loop_closure_window_ns = int (1.0e9 * lc_window_s )
121+
122+ seed = self ._get_param ("random_seed" , 0 ).integer_value
123+ self ._rng = np .random .default_rng (seed = seed if seed > 0 else None )
115124
116- # Set orientation from noisy pose
117- t .transform .rotation = noisy_pose .orientation
125+ self ._br = tf2_ros .TransformBroadcaster (self )
126+ self ._sub = self .create_subscription (Odometry , "odom" , self ._odom_cb , 10 )
127+ self ._pub = self .create_publisher (Odometry , "noisy_odom" , 10 )
128+ if self ._enable_pseudo_loop_closures :
129+ self ._lc_pub = self .create_publisher (PoseGraph , "loop_closures" , 10 )
130+
131+ def _odom_cb (self , msg ):
132+ map_T_new = _mat_from_pose (msg .pose .pose )
133+ self ._poses .append (map_T_new )
134+ self ._stamps .append (Time .from_msg (msg .header .stamp ).nanoseconds )
135+ if len (self ._poses ) == 1 :
136+ self ._last_noisy_pose = np .eye (4 ) if self ._start_from_origin else map_T_new
137+ return
118138
119- # Broadcast the noisy transform
120- self ._br .sendTransform (t )
139+ map_T_old = self ._poses [- 2 ]
140+ old_T_new = _invert_pose (map_T_old ) @ map_T_new
141+ if self ._add_noise :
142+ old_T_new @= _exp (* self ._sample_noise ())
121143
122- def _add_odom_noise (self , prev_noisy_odom , cur_gt_odom , pose_delta ):
123- updated_odom = Odometry ()
124- updated_odom .header = cur_gt_odom .header
144+ self ._last_noisy_pose = self ._last_noisy_pose @ old_T_new
145+ self ._publish_noisy_transform (msg .header .stamp , self ._last_noisy_pose )
146+ self ._publish_noisy_odom (self ._last_noisy_pose , msg )
147+ if not self ._enable_pseudo_loop_closures :
148+ return
125149
126- prev_pose = _mat_from_pose (prev_noisy_odom .pose .pose )
127- updated_pose = prev_pose @ pose_delta
128- if self ._add_noise :
129- noisy_trans = np .random .normal (scale = self ._xyz_std_dev )
130- noisy_rot = [0 , 0 , np .random .normal (scale = self ._yaw_std_dev )]
131- updated_pose @= Exp (noisy_trans , noisy_rot )
132-
133- updated_odom .pose .pose .position .x = updated_pose [0 , 3 ]
134- updated_odom .pose .pose .position .y = updated_pose [1 , 3 ]
135- updated_odom .pose .pose .position .z = updated_pose [2 , 3 ]
136-
137- q = R .from_matrix (updated_pose [:3 , :3 ]).as_quat ()
138- updated_odom .pose .pose .orientation .x = q [0 ]
139- updated_odom .pose .pose .orientation .y = q [1 ]
140- updated_odom .pose .pose .orientation .z = q [2 ]
141- updated_odom .pose .pose .orientation .w = q [3 ]
142-
143- q_gt = [
144- cur_gt_odom .pose .pose .orientation .x ,
145- cur_gt_odom .pose .pose .orientation .y ,
146- cur_gt_odom .pose .pose .orientation .z ,
147- cur_gt_odom .pose .pose .orientation .w ,
148- ]
149-
150- R_gt = R .from_quat (q_gt ).as_matrix ()
151- R_noisy = updated_pose [:3 , :3 ]
152-
153- vel = np .array (
154- [
155- cur_gt_odom .twist .twist .linear .x ,
156- cur_gt_odom .twist .twist .linear .y ,
157- cur_gt_odom .twist .twist .linear .z ,
158- ]
159- )
150+ candidate = self ._find_pseudo_loop_closure ()
151+ if candidate is None :
152+ return
160153
154+ self ._last_loop_closure_stamp = self ._stamps [- 1 ]
155+ self ._publish_loop_closure (candidate )
156+
157+ def _find_pseudo_loop_closure (self ):
158+ curr_stamp = self ._stamps [- 1 ]
159+ if self ._last_loop_closure_stamp is not None :
160+ last_diff_ns = curr_stamp - self ._last_loop_closure_stamp
161+ if last_diff_ns < self ._min_loop_closure_separation_ns :
162+ return None
163+
164+ curr_pose = self ._poses [- 1 ]
165+ for idx , prev_pose in enumerate (self ._poses [:- 1 ]):
166+ prev_stamp = self ._stamps [idx ]
167+ time_diff_ns = curr_stamp - prev_stamp
168+ if time_diff_ns < self ._loop_closure_window_ns :
169+ return None
170+
171+ diff_m = _translation_distance (curr_pose , prev_pose )
172+ if diff_m < self ._loop_closure_threshold_m :
173+ return idx
174+
175+ def _publish_loop_closure (self , candidate ):
176+ map_T_ref = self ._poses [candidate ]
177+ map_T_query = self ._poses [- 1 ]
178+ ref_T_query = _invert_pose (map_T_ref ) @ map_T_query
179+
180+ edge = PoseGraphEdge ()
181+ edge .header .stamp = Time (nanoseconds = self ._stamps [- 1 ]).to_msg ()
182+ edge .key_from = self ._stamps [candidate ]
183+ edge .key_to = self ._stamps [- 1 ]
184+ edge .robot_from = self ._robot_id
185+ edge .robot_to = self ._robot_id
186+ edge .type = PoseGraphEdge .LOOPCLOSE
187+ edge .pose = _pose_from_mat (ref_T_query )
188+
189+ msg = PoseGraph ()
190+ msg .edges .append (edge )
191+ self ._lc_pub .publish (msg )
192+
193+ def _sample_noise (self ):
194+ t_noise = np .zeros (3 )
195+ t_noise [:2 ] = self ._rng .normal (scale = self ._xy_std_dev )
196+ if self ._z_std_dev > 0.0 :
197+ t_noise [2 ] = self ._rng .normal (scale = self ._z_std_dev )
198+
199+ R_noise = [0 , 0 , self ._rng .normal (scale = self ._yaw_std_dev )]
200+ return t_noise , R_noise
201+
202+ def _publish_noisy_transform (self , time , noisy_pose ):
203+ msg = TransformStamped ()
204+ msg .header .stamp = time
205+ msg .header .frame_id = self ._parent_frame
206+ msg .child_frame_id = self ._child_frame
207+ msg .transform = _transform_from_mat (noisy_pose )
208+ self ._br .sendTransform (msg )
209+
210+ def _publish_noisy_odom (self , noisy_pose , cur_gt_odom ):
211+ curr_twist = cur_gt_odom .twist .twist
212+ vel = np .array ([curr_twist .linear .x , curr_twist .linear .y , curr_twist .linear .z ])
161213 if self ._wrong_velocity_frame :
162- vel = (R_gt .T @ R_noisy ).T @ vel
163-
164- updated_odom .twist .twist .linear .x = vel [0 ]
165- updated_odom .twist .twist .linear .y = vel [1 ]
166- updated_odom .twist .twist .linear .z = vel [2 ]
167- return updated_odom
214+ R_gt = _rot_from_pose (cur_gt_odom .pose .pose )
215+ R_noisy = noisy_pose [:3 , :3 ]
216+ vel = (R_noisy .T @ R_gt ) @ vel
217+
218+ msg = Odometry ()
219+ msg .header = cur_gt_odom .header
220+ msg .pose .pose = _pose_from_mat (noisy_pose )
221+ msg .twist .twist .linear .x = vel [0 ]
222+ msg .twist .twist .linear .y = vel [1 ]
223+ msg .twist .twist .linear .z = vel [2 ]
224+ self ._pub .publish (msg )
168225
169226
170227def main (args = None ):
0 commit comments