Skip to content

Commit be740b5

Browse files
Outlier filter and changes from hardware testing (#28)
* fix publisher when two frames are the same * remove unnecessary frame check * outlier filtering
1 parent f3693bf commit be740b5

File tree

8 files changed

+170
-68
lines changed

8 files changed

+170
-68
lines changed

fuse_models/doc/sensor_models/transform_sensor.md

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ This allows the transform sensor to use any number of cameras to estimate the po
1717

1818
A slightly confusing aspect of the sensor is the need for multiple definitions of the target frame. Every frame in `transforms` needs a corresponding `target_frame` to be published for it to be used. This is simply because `tf` uses a tree data structure, and the target frames are leaves. These should all end up being in the same global location (discounting measurement noise) but will have different names.
1919

20+
Finally, an optional feature of the transform sensor is outlier filtering. This can be enabled by setting `filter_outliers` to `true`. This filters outliers based on a simple distance threshold. This distance value and the associated timeout before any distance is allowed (to avoid the situation in which measurements are lost for a period and return but are ignored) can be configured with `outlier_distance` and `outlier_time_threshold`, respectively. Mahalanobis distance thresholding was explored as well, but performs poorly in most cases with external forcing (because this is generally not accounted for in the model or measured).
21+
2022
## Example
2123

2224
For example, say we have `estimation_frame` `camera`, `transforms` {`apriltag_1`, `apriltag_2`}, and `target_frame` `robot_center_of_mass`.
@@ -34,3 +36,5 @@ Thus, our measurement of the actual position of `target_frame` that will be used
3436
It is not (-1.25, -0.5, 0) because the translation of `T_a_to_b` is the negation of the pose of `b` in frame `a`.
3537

3638
Finally, this transaction is sent to the optimizer, as normal.
39+
40+
To see this sensor running in an example, run `ros2 launch fuse_tutorials fuse_apriltag_tutorial.launch.py`.

fuse_models/include/fuse_models/parameters/odometry_3d_publisher_params.hpp

-18
Original file line numberDiff line numberDiff line change
@@ -111,24 +111,6 @@ struct Odometry3DPublisherParams : public ParameterBase
111111
world_frame_id =
112112
fuse_core::getParam(interfaces, fuse_core::joinParameterName(ns, "world_frame_id"), world_frame_id);
113113

114-
bool const frames_valid = map_frame_id != odom_frame_id && map_frame_id != base_link_frame_id &&
115-
map_frame_id != base_link_output_frame_id && odom_frame_id != base_link_frame_id &&
116-
odom_frame_id != base_link_output_frame_id &&
117-
(world_frame_id == map_frame_id || world_frame_id == odom_frame_id);
118-
119-
if (!frames_valid)
120-
{
121-
RCLCPP_FATAL_STREAM(interfaces.get_node_logging_interface()->get_logger(),
122-
"Invalid frame configuration! Please note:\n"
123-
<< " - The values for map_frame_id, odom_frame_id, and base_link_frame_id must be "
124-
<< "unique\n"
125-
<< " - The values for map_frame_id, odom_frame_id, and base_link_output_frame_id must be "
126-
<< "unique\n"
127-
<< " - The world_frame_id must be the same as the map_frame_id or odom_frame_id\n");
128-
129-
assert(frames_valid);
130-
}
131-
132114
topic = fuse_core::getParam(interfaces, fuse_core::joinParameterName(ns, "topic"), topic);
133115
acceleration_topic =
134116
fuse_core::getParam(interfaces, fuse_core::joinParameterName(ns, "acceleration_topic"), acceleration_topic);

fuse_models/include/fuse_models/parameters/transform_sensor_params.hpp

+14-1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ struct TransformSensorParams : public ParameterBase
8888
estimation_frames =
8989
fuse_core::getParam(interfaces, fuse_core::joinParameterName(ns, "estimation_frames"), estimation_frames);
9090

91+
filter_outliers =
92+
fuse_core::getParam(interfaces, fuse_core::joinParameterName(ns, "filter_outliers"), filter_outliers);
93+
94+
outlier_distance =
95+
fuse_core::getParam(interfaces, fuse_core::joinParameterName(ns, "outlier_distance"), outlier_distance);
96+
97+
outlier_time_threshold = fuse_core::getParam(interfaces, fuse_core::joinParameterName(ns, "outlier_time_threshold"),
98+
outlier_time_threshold);
99+
91100
pose_loss = fuse_core::loadLossConfig(interfaces, fuse_core::joinParameterName(ns, "pose_loss"));
92101
pose_covariance = fuse_core::getParam(interfaces, fuse_core::joinParameterName(ns, "pose_covariance"),
93102
std::vector<double>{ 1, 1, 1, 1, 1, 1 });
@@ -96,7 +105,11 @@ struct TransformSensorParams : public ParameterBase
96105
bool disable_checks{ false };
97106
bool independent{ true };
98107
fuse_core::Matrix6d minimum_pose_relative_covariance; //!< Minimum pose relative covariance matrix
99-
rclcpp::Duration tf_timeout{ 0, 0 }; //!< The maximum time to wait for a transform to become available
108+
rclcpp::Duration tf_timeout{ 0, 0 }; //!< The maximum time to wait for a transform to become available
109+
110+
bool filter_outliers{ false };
111+
double outlier_distance{ 0.2 };
112+
double outlier_time_threshold{ 0.2 };
100113
rclcpp::Duration throttle_period{ 0, 0 }; //!< The throttle period duration in seconds
101114
bool throttle_use_wall_time{ false }; //!< Whether to throttle using ros::WallTime or not
102115
std::vector<double> pose_covariance; //!< The diagonal elements of the tag pose covariance

fuse_models/include/fuse_models/transform_sensor.hpp

+8
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
#include <tf2_ros/transform_listener.h>
3939

4040
#include <memory>
41+
#include <optional>
4142
#include <string>
4243

4344
#include <fuse_models/parameters/transform_sensor_params.hpp>
@@ -119,6 +120,9 @@ class TransformSensor : public fuse_core::AsyncSensorModel
119120
*/
120121
void onInit() override;
121122

123+
// only used to reject outlier measurements
124+
void onGraphUpdate(fuse_core::Graph::ConstSharedPtr graph) override;
125+
122126
/**
123127
* @brief Subscribe to the input topic to start sending transactions to the optimizer
124128
*/
@@ -140,6 +144,10 @@ class TransformSensor : public fuse_core::AsyncSensorModel
140144

141145
ParameterType params_;
142146

147+
std::optional<rclcpp::Time> last_stamp_;
148+
std::optional<fuse_core::UUID> last_uuid_;
149+
std::optional<geometry_msgs::msg::Point> last_position_;
150+
143151
std::unique_ptr<tf2_ros::Buffer> tf_buffer_;
144152
std::unique_ptr<tf2_ros::TransformListener> tf_listener_;
145153
std::set<std::string> fiducial_frames_;

fuse_models/src/odometry_3d_publisher.cpp

+18-14
Original file line numberDiff line numberDiff line change
@@ -551,23 +551,27 @@ void Odometry3DPublisher::publishTF(nav_msgs::msg::Odometry const& odom_output,
551551
trans.transform = tf2::toMsg(pose);
552552
if (!params_.invert_tf && params_.world_frame_id == params_.map_frame_id)
553553
{
554-
try
554+
if (params_.base_link_frame_id != params_.odom_frame_id)
555555
{
556-
auto base_to_odom = tf_buffer_->lookupTransform(params_.base_link_frame_id, params_.odom_frame_id,
557-
trans.header.stamp, params_.tf_timeout);
556+
try
557+
{
558+
auto base_to_odom = tf_buffer_->lookupTransform(params_.base_link_frame_id, params_.odom_frame_id,
559+
trans.header.stamp, params_.tf_timeout);
558560

559-
geometry_msgs::msg::TransformStamped map_to_odom;
560-
tf2::doTransform(base_to_odom, map_to_odom, trans);
561-
map_to_odom.child_frame_id = params_.odom_frame_id;
562-
trans = map_to_odom;
563-
}
564-
catch (std::exception const& e)
565-
{
566-
RCLCPP_WARN_STREAM_THROTTLE(logger_, *clock_, 5.0 * 1000,
567-
"Could not lookup the " << params_.base_link_frame_id << "->" << params_.odom_frame_id
568-
<< " transform. Error: " << e.what());
561+
geometry_msgs::msg::TransformStamped map_to_odom;
562+
tf2::doTransform(base_to_odom, map_to_odom, trans);
563+
map_to_odom.child_frame_id = params_.odom_frame_id;
564+
trans = map_to_odom;
565+
}
566+
catch (std::exception const& e)
567+
{
568+
RCLCPP_WARN_STREAM_THROTTLE(logger_, *clock_, 5.0 * 1000,
569+
"Could not lookup the " << params_.base_link_frame_id << "->"
570+
<< params_.odom_frame_id
571+
<< " transform. Error: " << e.what());
569572

570-
return;
573+
return;
574+
}
571575
}
572576
}
573577

fuse_models/src/transform_sensor.cpp

+43
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
#include <tf2/impl/utils.h>
3737
#include <tf2_ros/buffer.h>
3838
#include <tf2_ros/message_filter.h>
39+
#include <fuse_core/graph.hpp>
40+
#include <fuse_variables/position_3d_stamped.hpp>
41+
#include <geometry_msgs/msg/detail/point__struct.hpp>
3942
#include <memory>
4043

4144
#include <fuse_core/transaction.hpp>
@@ -46,6 +49,7 @@
4649
#include <geometry_msgs/msg/twist_with_covariance_stamped.hpp>
4750
#include <nav_msgs/msg/odometry.hpp>
4851
#include <pluginlib/class_list_macros.hpp>
52+
#include <rclcpp/logging.hpp>
4953
#include <rclcpp/rclcpp.hpp>
5054
#include <stdexcept>
5155
#include <string>
@@ -73,6 +77,21 @@ void TransformSensor::initialize(fuse_core::node_interfaces::NodeInterfaces<ALL_
7377
fuse_core::AsyncSensorModel::initialize(interfaces, name, transaction_callback);
7478
}
7579

80+
void TransformSensor::onGraphUpdate(fuse_core::Graph::ConstSharedPtr graph)
81+
{
82+
if (last_uuid_.has_value())
83+
{
84+
if (graph->variableExists(last_uuid_.value()))
85+
{
86+
auto const& last_position = graph->getVariable(last_uuid_.value());
87+
last_position_ = geometry_msgs::msg::Point();
88+
last_position_->x = last_position.data()[fuse_variables::Position3DStamped::X];
89+
last_position_->y = last_position.data()[fuse_variables::Position3DStamped::Y];
90+
last_position_->z = last_position.data()[fuse_variables::Position3DStamped::Z];
91+
}
92+
}
93+
}
94+
7695
void TransformSensor::onInit()
7796
{
7897
logger_ = interfaces_.get_node_logging_interface()->get_logger();
@@ -254,6 +273,30 @@ void TransformSensor::process(MessageType const& msg)
254273
pose.pose.covariance[i * 7] = pose_covariances_[estimation_index][i];
255274
}
256275

276+
// outlier filtering
277+
if (params_.filter_outliers && last_position_.has_value() && last_stamp_.has_value())
278+
{
279+
Eigen::Vector3d position_difference = Eigen::Vector3d::Zero();
280+
position_difference.x() = last_position_->x - pose.pose.pose.position.x;
281+
position_difference.y() = last_position_->y - pose.pose.pose.position.y;
282+
position_difference.z() = last_position_->z - pose.pose.pose.position.z;
283+
auto const distance = position_difference.norm();
284+
auto const time_difference = (rclcpp::Time(transform.header.stamp) - last_stamp_.value()).seconds();
285+
286+
if (distance >= params_.outlier_distance && time_difference <= params_.outlier_time_threshold)
287+
{
288+
// this is an outlier
289+
RCLCPP_WARN(logger_,
290+
"Filtered outlier with distance %.3f from (%.3f, %.3f, %.3f) %.3f seconds after most recent update",
291+
distance, last_position_->x, last_position_->y, last_position_->z, time_difference);
292+
return;
293+
}
294+
}
295+
296+
// update outlier finding variables (must occur after outlier filtering)
297+
last_stamp_ = transform.header.stamp;
298+
last_uuid_ = fuse_variables::Position3DStamped(transform.header.stamp, device_id_).uuid();
299+
257300
bool const validate = !params_.disable_checks;
258301
common::processAbsolutePose3DWithCovariance(name(), device_id_, pose, params_.pose_loss, "",
259302
params_.position_indices, params_.orientation_indices, *tf_buffer_,

fuse_tutorials/config/fuse_apriltag_tutorial.yaml

+42-5
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,24 @@ state_estimator:
1414
3d_motion_model:
1515
# x, y, z, roll, pitch, yaw, x_vel, y_vel, z_vel, roll_vel, pitch_vel, yaw_vel, x_acc, y_acc, z_acc
1616
# use high values for the acceleration process noise because we aren't measuring the applied forces
17-
process_noise_diagonal: [0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 10., 10., 10.]
17+
process_noise_diagonal:
18+
[
19+
0.01,
20+
0.01,
21+
0.01,
22+
0.01,
23+
0.01,
24+
0.01,
25+
0.01,
26+
0.01,
27+
0.01,
28+
0.01,
29+
0.01,
30+
0.01,
31+
10.,
32+
10.,
33+
10.,
34+
]
1835

1936
sensor_models:
2037
initial_localization_sensor:
@@ -34,11 +51,13 @@ state_estimator:
3451
transform_sensor:
3552
transforms: ['april_1', 'april_2', 'april_3', 'april_4', 'april_5', 'april_6', 'april_7', 'april_8']
3653
target_frame: 'base_link'
37-
estimation_frame: 'odom'
54+
estimation_frames: ['odom']
3855
position_dimensions: ['x', 'y', 'z']
3956
orientation_dimensions: ['roll', 'pitch', 'yaw']
40-
# these are the true covariance values used by the simulator; what happens if we change these?
41-
pose_covariance: [0.1, 0.1, 0.1, 0.25, 0.25, 0.25]
57+
# these values are the real values but the orientation variance is set to nonzero to avoid numerical issues
58+
pose_covariance: [0.01, 0.01, 0.01, 0.001, 0.001, 0.001]
59+
filter_outliers: true
60+
outlier_distance: 0.5
4261

4362
# this publishes our estimated odometry
4463
publishers:
@@ -54,4 +73,22 @@ state_estimator:
5473
world_frame_id: 'odom'
5574
publish_tf: true
5675
publish_frequency: 100.0
57-
predict_to_future: true
76+
predict_to_current_time: true
77+
process_noise_diagonal:
78+
[
79+
0.01,
80+
0.01,
81+
0.01,
82+
0.01,
83+
0.01,
84+
0.01,
85+
0.01,
86+
0.01,
87+
0.01,
88+
0.01,
89+
0.01,
90+
0.01,
91+
10.,
92+
10.,
93+
10.,
94+
]

0 commit comments

Comments
 (0)