Skip to content

Commit e57e93f

Browse files
committed
use the ground truth for making the increments instead of the commanded vel, update the models with the new params
1 parent 71074b4 commit e57e93f

2 files changed

Lines changed: 155 additions & 33 deletions

File tree

Industrial/gz_noisy_odometry/include/gz_noisy_odometry/NoisyOdometryPlugin.hpp

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
namespace custom_plugins
1919
{
20-
class NoisyOdometryPlugin :
20+
class NoisyOdometryPlugin :
2121
public gz::sim::System,
2222
public gz::sim::ISystemConfigure,
2323
public gz::sim::ISystemPostUpdate
@@ -37,9 +37,12 @@ namespace custom_plugins
3737
private:
3838
void OnCmdVel(const gz::msgs::Twist &_msg);
3939

40+
// Keeps angles bounded in [-pi, pi] so yaw differences remain well behaved.
41+
double NormalizeAngle(double _angle) const;
42+
4043
gz::sim::Model model_;
4144
gz::transport::Node gz_node_;
42-
45+
4346
std::shared_ptr<rclcpp::Node> ros_node_;
4447
rclcpp::Publisher<nav_msgs::msg::Odometry>::SharedPtr ros_pub_;
4548

@@ -49,15 +52,38 @@ namespace custom_plugins
4952
std::string child_frame_id_;
5053

5154
bool initialized_{false};
55+
56+
// Internal odometric estimate that is propagated only with noisy increments.
57+
// This is the state published as odom_noisy and it is allowed to drift.
5258
gz::math::Pose3d noisy_pose_internal_;
59+
60+
// True simulator pose used only to extract the real motion increment
61+
// between consecutive simulation steps.
62+
gz::math::Pose3d last_true_pose_;
63+
5364
std::chrono::steady_clock::duration last_update_time_{0};
5465

66+
// Latest commanded velocities are still stored because they are useful
67+
// as auxiliary information and to keep compatibility with the original plugin.
5568
double current_v_{0.0};
5669
double current_w_{0.0};
5770

71+
// Last noisy body-frame velocities published in the odometry message.
72+
double last_noisy_linear_velocity_{0.0};
73+
double last_noisy_angular_velocity_{0.0};
74+
5875
std::mutex mutex_;
76+
77+
// Legacy generic noise parameter kept for compatibility with the original SDF.
5978
double gaussian_noise_coeff_{0.0};
79+
80+
// Standard odometry motion-model coefficients.
81+
// They control how rotation and translation uncertainties affect each other.
82+
double alpha1_{0.0};
83+
double alpha2_{0.0};
84+
double alpha3_{0.0};
85+
double alpha4_{0.0};
6086
};
61-
}
87+
}
6288

6389
#endif

Industrial/gz_noisy_odometry/src/gz_noisy_odometry/NoisyOdometryPlugin.cpp

Lines changed: 126 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#include <gz/plugin/Register.hh>
44
#include <tf2/LinearMath/Quaternion.h>
55

6+
#include <cmath>
7+
68
GZ_ADD_PLUGIN(
79
custom_plugins::NoisyOdometryPlugin,
810
gz::sim::System,
@@ -15,6 +17,13 @@ namespace custom_plugins
1517
{
1618
NoisyOdometryPlugin::NoisyOdometryPlugin() {}
1719

20+
double NoisyOdometryPlugin::NormalizeAngle(double _angle) const
21+
{
22+
while (_angle > M_PI) _angle -= 2.0 * M_PI;
23+
while (_angle < -M_PI) _angle += 2.0 * M_PI;
24+
return _angle;
25+
}
26+
1827
void NoisyOdometryPlugin::Configure(const gz::sim::Entity &_entity,
1928
const std::shared_ptr<const sdf::Element> &_sdf,
2029
gz::sim::EntityComponentManager &_ecm,
@@ -29,11 +38,18 @@ namespace custom_plugins
2938
frame_id_ = _sdf->Get<std::string>("frame_id", "odom").first;
3039
child_frame_id_ = _sdf->Get<std::string>("child_frame_id", "base_footprint").first;
3140

41+
// If the SDF does not provide the odometry motion-model coefficients
42+
// explicitly, derive reasonable defaults from the legacy noise parameter.
43+
alpha1_ = _sdf->Get<double>("alpha1", gaussian_noise_coeff_).first;
44+
alpha2_ = _sdf->Get<double>("alpha2", gaussian_noise_coeff_).first;
45+
alpha3_ = _sdf->Get<double>("alpha3", gaussian_noise_coeff_).first;
46+
alpha4_ = _sdf->Get<double>("alpha4", gaussian_noise_coeff_).first;
47+
3248
gz_node_.Subscribe(gz_cmd_vel_topic_, &NoisyOdometryPlugin::OnCmdVel, this);
3349

3450
if (!rclcpp::ok()) rclcpp::init(0, nullptr);
3551
ros_node_ = rclcpp::Node::make_shared("noisy_odom_node_" + model_.Name(_ecm));
36-
52+
3753
rclcpp::QoS qos(10);
3854
qos.transient_local();
3955
ros_pub_ = ros_node_->create_publisher<nav_msgs::msg::Odometry>(ros_topic_, qos);
@@ -51,43 +67,91 @@ namespace custom_plugins
5167
{
5268
if (_info.paused) return;
5369

70+
auto poseComp = _ecm.Component<gz::sim::components::Pose>(model_.Entity());
71+
if (!poseComp) return;
72+
73+
const gz::math::Pose3d current_true_pose = poseComp->Data();
74+
5475
if (!initialized_)
5576
{
56-
auto poseComp = _ecm.Component<gz::sim::components::Pose>(model_.Entity());
57-
if (poseComp) {
58-
noisy_pose_internal_ = poseComp->Data();
59-
last_update_time_ = _info.simTime;
60-
initialized_ = true;
61-
}
77+
// The noisy odometry starts aligned with the true pose only once.
78+
// After that, it evolves independently by integrating noisy motion
79+
// increments, so drift accumulates naturally over time.
80+
noisy_pose_internal_ = current_true_pose;
81+
last_true_pose_ = current_true_pose;
82+
last_update_time_ = _info.simTime;
83+
initialized_ = true;
6284
return;
6385
}
6486

65-
double dt = std::chrono::duration<double>(_info.simTime - last_update_time_).count();
87+
const double dt =
88+
std::chrono::duration<double>(_info.simTime - last_update_time_).count();
89+
6690
if (dt <= 0.0) return;
6791
last_update_time_ = _info.simTime;
6892

69-
double v_cmd, w_cmd;
70-
{
71-
std::lock_guard<std::mutex> lock(mutex_);
72-
v_cmd = current_v_;
73-
w_cmd = current_w_;
74-
}
75-
76-
double linear_noise = gaussian_noise_coeff_ * gz::math::Rand::DblNormal(0, 1) * std::abs(v_cmd);
77-
double angular_noise = gaussian_noise_coeff_ * gz::math::Rand::DblNormal(0, 1) * std::abs(w_cmd);
78-
79-
double v_noisy = v_cmd + linear_noise;
80-
double w_noisy = w_cmd + angular_noise;
81-
82-
double angle = w_noisy * dt;
83-
gz::math::Vector3d axis(0, 0, 1);
84-
gz::math::Quaterniond deltaOrientation(std::cos(angle / 2.0), 0, 0, std::sin(angle / 2.0));
85-
86-
noisy_pose_internal_.Rot() = noisy_pose_internal_.Rot() * deltaOrientation;
93+
// Compute the true relative motion between the last and current simulator pose.
94+
// This gives a physically meaningful motion increment without publishing the
95+
// simulator pose directly.
96+
const gz::math::Vector3d delta_world =
97+
current_true_pose.Pos() - last_true_pose_.Pos();
98+
99+
const double previous_true_yaw = last_true_pose_.Rot().Yaw();
100+
const double current_true_yaw = current_true_pose.Rot().Yaw();
101+
102+
// Express the translation increment in the previous robot frame so the
103+
// odometry model operates in the body frame, as usual in planar robotics.
104+
const gz::math::Vector3d delta_body =
105+
last_true_pose_.Rot().RotateVectorReverse(delta_world);
106+
107+
const double trans = std::sqrt(
108+
delta_body.X() * delta_body.X() + delta_body.Y() * delta_body.Y());
109+
110+
const double rot1 =
111+
(trans > 1e-9) ? NormalizeAngle(std::atan2(delta_body.Y(), delta_body.X())) : 0.0;
112+
113+
const double delta_yaw = NormalizeAngle(current_true_yaw - previous_true_yaw);
114+
const double rot2 = NormalizeAngle(delta_yaw - rot1);
115+
116+
// Odometry motion model:
117+
// - rotation uncertainty grows with both rotation and translation
118+
// - translation uncertainty grows with both translation and turning
119+
//
120+
// This is more robust than injecting noise directly on cmd_vel, and still
121+
// preserves the expected long-term drift because only noisy increments are
122+
// integrated into the internal odometric state.
123+
const double sigma_rot1 =
124+
alpha1_ * std::abs(rot1) + alpha2_ * std::abs(trans);
125+
const double sigma_trans =
126+
alpha3_ * std::abs(trans) + alpha4_ * (std::abs(rot1) + std::abs(rot2));
127+
const double sigma_rot2 =
128+
alpha1_ * std::abs(rot2) + alpha2_ * std::abs(trans);
129+
130+
const double rot1_noisy = rot1 + gz::math::Rand::DblNormal(0.0, sigma_rot1);
131+
const double trans_noisy = trans + gz::math::Rand::DblNormal(0.0, sigma_trans);
132+
const double rot2_noisy = rot2 + gz::math::Rand::DblNormal(0.0, sigma_rot2);
133+
134+
// Integrate the noisy relative motion over the internal odometry state.
135+
// This is the key point that makes the estimate drift over time instead of
136+
// snapping back to the true simulator pose.
137+
const double current_noisy_yaw = noisy_pose_internal_.Rot().Yaw();
138+
const double heading_after_rot1 = current_noisy_yaw + rot1_noisy;
139+
const double new_noisy_yaw =
140+
NormalizeAngle(current_noisy_yaw + rot1_noisy + rot2_noisy);
141+
142+
noisy_pose_internal_.Pos().X(
143+
noisy_pose_internal_.Pos().X() + trans_noisy * std::cos(heading_after_rot1));
144+
noisy_pose_internal_.Pos().Y(
145+
noisy_pose_internal_.Pos().Y() + trans_noisy * std::sin(heading_after_rot1));
146+
noisy_pose_internal_.Pos().Z(current_true_pose.Pos().Z());
147+
148+
noisy_pose_internal_.Rot() = gz::math::Quaterniond(0.0, 0.0, new_noisy_yaw);
87149
noisy_pose_internal_.Rot().Normalize();
88150

89-
gz::math::Vector3d deltaPos(v_noisy * dt, 0, 0);
90-
noisy_pose_internal_.Pos() += noisy_pose_internal_.Rot().RotateVector(deltaPos);
151+
// The published twist is consistent with the noisy increment that has just
152+
// been integrated. This keeps the message self-consistent.
153+
last_noisy_linear_velocity_ = trans_noisy / dt;
154+
last_noisy_angular_velocity_ = NormalizeAngle(rot1_noisy + rot2_noisy) / dt;
91155

92156
nav_msgs::msg::Odometry odom_msg;
93157
odom_msg.header.stamp = ros_node_->now();
@@ -103,9 +167,41 @@ namespace custom_plugins
103167
odom_msg.pose.pose.orientation.z = noisy_pose_internal_.Rot().Z();
104168
odom_msg.pose.pose.orientation.w = noisy_pose_internal_.Rot().W();
105169

106-
odom_msg.twist.twist.linear.x = v_noisy;
107-
odom_msg.twist.twist.angular.z = w_noisy;
170+
odom_msg.twist.twist.linear.x = last_noisy_linear_velocity_;
171+
odom_msg.twist.twist.linear.y = 0.0;
172+
odom_msg.twist.twist.linear.z = 0.0;
173+
174+
odom_msg.twist.twist.angular.x = 0.0;
175+
odom_msg.twist.twist.angular.y = 0.0;
176+
odom_msg.twist.twist.angular.z = last_noisy_angular_velocity_;
177+
178+
// Fill the most relevant covariance entries with values consistent with the
179+
// motion model. The message remains simple, but no longer pretends to have
180+
// perfect certainty.
181+
const double pose_var_xy = sigma_trans * sigma_trans;
182+
const double pose_var_yaw =
183+
(sigma_rot1 * sigma_rot1) + (sigma_rot2 * sigma_rot2);
184+
185+
const double twist_var_v = pose_var_xy / (dt * dt);
186+
const double twist_var_w = pose_var_yaw / (dt * dt);
187+
188+
odom_msg.pose.covariance[0] = pose_var_xy;
189+
odom_msg.pose.covariance[7] = pose_var_xy;
190+
odom_msg.pose.covariance[14] = 1e-9;
191+
odom_msg.pose.covariance[21] = 1e-9;
192+
odom_msg.pose.covariance[28] = 1e-9;
193+
odom_msg.pose.covariance[35] = pose_var_yaw;
194+
195+
odom_msg.twist.covariance[0] = twist_var_v;
196+
odom_msg.twist.covariance[7] = 1e-9;
197+
odom_msg.twist.covariance[14] = 1e-9;
198+
odom_msg.twist.covariance[21] = 1e-9;
199+
odom_msg.twist.covariance[28] = 1e-9;
200+
odom_msg.twist.covariance[35] = twist_var_w;
108201

109202
ros_pub_->publish(odom_msg);
203+
204+
// Store the true pose only to extract the next real motion increment.
205+
last_true_pose_ = current_true_pose;
110206
}
111207
}

0 commit comments

Comments
 (0)