Skip to content

Commit a708b77

Browse files
Feature/update noisy odom (#68)
* clean up noisy odom node * (wip) start tracking pose history * publish loop closures * update loop closure parameters * fix linting * revert config name
1 parent 29522e7 commit a708b77

4 files changed

Lines changed: 196 additions & 135 deletions

File tree

hydra_ros/app/noisy_tf_publisher

Lines changed: 176 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,24 @@
33
import numpy as np
44
import rclpy
55
import tf2_ros
6-
from geometry_msgs.msg import TransformStamped
6+
from geometry_msgs.msg import Pose, Transform, TransformStamped
77
from nav_msgs.msg import Odometry
8+
from pose_graph_tools_msgs.msg import PoseGraph, PoseGraphEdge
89
from 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

1214
def _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

6083
class 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

170227
def main(args=None):

hydra_ros/launch/datasets/uhumans2.launch.yaml

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ launch:
33
- arg: {name: scene, default: office, description: uhumans2 scene name}
44
- arg: {name: use_gt_semantics, default: 'true'}
55
- arg: {name: use_openset_places, default: 'false'}
6+
- arg: {name: use_gt_poses, default: 'true'}
67
- arg: {name: labelspace, default: $(if $(var use_gt_semantics) uhumans2_$(var scene) ade20k_full)}
78
- arg: {name: log_prefix, default: uhumans2_$(var scene)}
89
- arg: {name: label_remap_path, default: $(find-pkg-share hydra)/config/label_remaps/uhumans2_$(var scene).yaml}
@@ -13,6 +14,11 @@ launch:
1314
- set_remap: {from: hydra/input/left_cam/feature, to: /tesse/left_cam/semantic/feature}
1415
- let: {name: extra_yaml_gt, value: '{semantic_label_remap_filepath: $(var label_remap_path)}'}
1516
- let: {name: extra_yaml_no_gt, value: '{}'}
17+
- node:
18+
pkg: tf2_ros
19+
name: world_to_map
20+
exec: static_transform_publisher
21+
args: --frame-id world --child-frame-id map
1622
- group:
1723
- push_ros_namespace: {namespace: tesse/left_cam}
1824
- set_remap: {from: color/image_raw, to: rgb/image_raw}
@@ -26,20 +32,20 @@ launch:
2632
file: $(find-pkg-share semantic_inference_ros)/launch/image_embedding_node.launch.yaml
2733
arg:
2834
- {name: min_period_s, value: '0.2'}
35+
- group:
36+
- set_remap: {from: odom, to: /tesse/odom}
37+
- set_remap: {from: loop_closures, to: /hydra/external_loop_closures}
38+
- include:
39+
if: $(not $(var use_gt_poses))
40+
file: $(find-pkg-share hydra_ros)/launch/noisy_tf_publisher_node.launch.yaml
2941
- include:
3042
file: $(find-pkg-share hydra_ros)/launch/hydra.launch.yaml
3143
arg:
3244
- {name: dataset, value: uhumans2}
3345
- {name: labelspace, value: $(var labelspace)}
3446
- {name: log_prefix, value: $(var log_prefix)}
35-
- {name: sensor_frame, value: left_cam}
36-
- {name: robot_frame, value: base_link_gt}
37-
- {name: odom_frame, value: world}
47+
- {name: robot_frame, value: $(if $(var use_gt_poses) base_link_gt base_link)}
48+
- {name: odom_frame, value: $(if $(var use_gt_poses) world odom)}
3849
- {name: map_frame, value: map}
3950
- {name: lcd_config_path, value: $(find-pkg-share hydra)/config/lcd/uhumans2.yaml}
4051
- {name: extra_yaml, value: $(if $(var use_gt_semantics) $(var extra_yaml_gt) $(var extra_yaml_no_gt))}
41-
- node:
42-
pkg: tf2_ros
43-
name: world_to_map
44-
exec: static_transform_publisher
45-
args: --frame-id world --child-frame-id map

hydra_ros/launch/hydra.launch.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ launch:
1010
- arg: {name: dataset, description: Dataset hydra is running}
1111
- arg: {name: labelspace, description: Closed-set semantics description}
1212
- arg: {name: robot_id, default: '0', description: Unique robot identifier}
13-
- arg: {name: sensor_frame, default: left_cam, description: Camera frame}
1413
- arg: {name: robot_frame, default: base_link_gt, description: Robot body tf frame}
1514
- arg: {name: odom_frame, default: world, description: Odometry (map) frame}
1615
- arg: {name: map_frame, default: map, description: Backend scene graph frame}

0 commit comments

Comments
 (0)