Skip to content

Commit 678c9f7

Browse files
authored
Pose filtering (#655)
* basic package setup * removed 'lib' from gitignore * generic pose sub setup * removed unused member in favor of variant * config setup * track manager impl * ValidPoseMsg concept * Variant alias * refactor eluer to quat * added confirm_tracks function * landmark support * added debug pose publisher * renamed pkg: ipda_pose -> pose * tests and doxygen comments * changed alpha calculation. Added README * pdaf orientation error-state * added landmark array publisher * added landmarkarray pub * added enu-ned rotation * pr fixes * removed unused stuff. Praise clangd * landmark filtering refactor * update for new landmark_msg type def * fixed tests, update default noise levels * sleep for stable op mode in sim tests * fix bug in get_tracks_by_type
1 parent eebcf9b commit 678c9f7

17 files changed

Lines changed: 1680 additions & 2 deletions

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ install/
99
log/
1010
build/
1111
bin/
12-
lib/
1312
msg_gen/
1413
srv_gen/
1514
msg/*Action.msg

filtering/pose_action_server/src/pose_action_server_ros.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Eigen::Quaterniond PoseActionServerNode::average_quaternions(
133133
const std::vector<Eigen::Quaterniond>& quaternions) {
134134
Eigen::Matrix4d M = Eigen::Matrix4d::Zero();
135135
std::ranges::for_each(quaternions, [&](const auto& q) {
136-
M += q.coeffs() * q.coeffs().transpose();
136+
M += q.normalized().coeffs() * q.normalized().coeffs().transpose();
137137
});
138138

139139
Eigen::SelfAdjointEigenSolver<Eigen::Matrix4d> eigensolver(M);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
cmake_minimum_required(VERSION 3.8)
2+
project(pose_filtering)
3+
4+
if(NOT CMAKE_CXX_STANDARD)
5+
set(CMAKE_CXX_STANDARD 20)
6+
endif()
7+
8+
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
9+
add_compile_options(-Wall -Wextra -Wpedantic)
10+
endif()
11+
12+
find_package(ament_cmake REQUIRED)
13+
find_package(rclcpp REQUIRED)
14+
find_package(rclcpp_components REQUIRED)
15+
find_package(geometry_msgs REQUIRED)
16+
find_package(tf2 REQUIRED)
17+
find_package(tf2_ros REQUIRED)
18+
find_package(tf2_geometry_msgs REQUIRED)
19+
find_package(message_filters REQUIRED)
20+
find_package(vortex_utils REQUIRED)
21+
find_package(vortex_utils_ros REQUIRED)
22+
find_package(vortex_utils_ros_tf REQUIRED)
23+
find_package(vortex_filtering REQUIRED)
24+
find_package(vortex_msgs REQUIRED)
25+
26+
include_directories(include)
27+
28+
set(CORE_LIB_NAME "${PROJECT_NAME}")
29+
30+
add_library(${CORE_LIB_NAME} SHARED
31+
src/lib/pose_track_manager.cpp
32+
)
33+
34+
target_include_directories(${CORE_LIB_NAME}
35+
PUBLIC
36+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
37+
$<INSTALL_INTERFACE:include>
38+
)
39+
40+
ament_target_dependencies(${CORE_LIB_NAME}
41+
vortex_filtering
42+
vortex_utils
43+
vortex_utils_ros
44+
vortex_utils_ros_tf
45+
vortex_msgs
46+
)
47+
48+
set(COMPONENT_LIB_NAME "${PROJECT_NAME}_component")
49+
50+
add_library(${COMPONENT_LIB_NAME} SHARED
51+
src/ros/${PROJECT_NAME}_ros.cpp
52+
src/ros/${PROJECT_NAME}_ros_debug.cpp
53+
src/ros/${PROJECT_NAME}_ros_conversions.cpp
54+
)
55+
56+
ament_target_dependencies(${COMPONENT_LIB_NAME}
57+
rclcpp
58+
rclcpp_components
59+
geometry_msgs
60+
tf2
61+
tf2_ros
62+
tf2_geometry_msgs
63+
message_filters
64+
vortex_filtering
65+
vortex_utils
66+
vortex_utils_ros
67+
vortex_utils_ros_tf
68+
)
69+
70+
target_link_libraries(${COMPONENT_LIB_NAME} ${PROJECT_NAME})
71+
72+
rclcpp_components_register_node(
73+
${COMPONENT_LIB_NAME}
74+
PLUGIN "PoseFilteringNode"
75+
EXECUTABLE ${PROJECT_NAME}_node
76+
)
77+
78+
install(
79+
TARGETS
80+
${CORE_LIB_NAME}
81+
${COMPONENT_LIB_NAME}
82+
EXPORT export_${PROJECT_NAME}
83+
ARCHIVE DESTINATION lib
84+
LIBRARY DESTINATION lib
85+
RUNTIME DESTINATION bin
86+
)
87+
88+
install(
89+
DIRECTORY include/
90+
DESTINATION include
91+
)
92+
93+
install(DIRECTORY
94+
launch
95+
config
96+
DESTINATION share/${PROJECT_NAME}/
97+
)
98+
99+
ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET)
100+
ament_export_dependencies(
101+
vortex_filtering
102+
vortex_utils
103+
)
104+
ament_export_include_directories(include)
105+
ament_export_libraries(${CORE_LIB_NAME})
106+
107+
if(BUILD_TESTING)
108+
add_subdirectory(test)
109+
endif()
110+
111+
ament_package()

filtering/pose_filtering/README.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# pose_filtering
2+
3+
ROS 2 package providing pose-based multi-target tracking using an IPDA
4+
filter for position together with independent orientation logic.
5+
6+
This package transforms incoming pose-like messages into a common
7+
target frame, associates measurements to tracks using spatial and angular
8+
gating, maintains an IPDA filter per track for position estimation and
9+
existence management, and performs a separate orientation update by
10+
averaging measurement quaternions and applying spherical linear
11+
interpolation (slerp) to smoothly update the track orientation.
12+
13+
## Features
14+
15+
- Multi-target tracking using an IPDA (Integrated Probabilistic Data
16+
Association) filter for 3D position state estimation.
17+
- Separate orientation handling: angular gating, quaternion averaging and
18+
slerp-based smoothing so orientation updates remain numerically stable
19+
and physically meaningful.
20+
- TF2-aware subscriptions (message_filters + tf2 message filter) so
21+
incoming messages are automatically dropped until transform to the
22+
configured target frame is available.
23+
- Supports several ROS message inputs (see below) and publishes a
24+
`geometry_msgs/PoseArray` containing the current tracked poses.
25+
26+
## Supported input messages
27+
28+
The node accepts the following message types as input measurements:
29+
30+
- `geometry_msgs/msg/PoseStamped`
31+
- `geometry_msgs/msg/PoseArray`
32+
- `geometry_msgs/msg/PoseWithCovarianceStamped`
33+
- `vortex_msgs/msg/LandmarkArray`
34+
35+
The code uses a templated message type (PoseMsgT) and checks at
36+
compile-time which types are supported. Incoming measurements are
37+
transformed to the configured `target_frame` before being processed.
38+
39+
## Outputs
40+
41+
- `geometry_msgs/msg/PoseArray` — the current set of tracked poses in the
42+
configured target frame. Each Pose contains the track position and a
43+
quaternion orientation representing the track's current orientation.
44+
- (DEBUG) `vortex_msgs/msg/PoseEulerStamped` topics with
45+
measurement-level and track-state-level pose messages when debug mode
46+
is enabled. Only publishes the first element of incoming measurements and stored tracks.
47+
48+
49+
## How the filtering works
50+
51+
Position (IPDA):
52+
53+
- Each track runs a 3D IPDA filter instance (a Gaussian state: mean +
54+
covariance) where the motion model is a small constant-dynamics model
55+
and the sensor model maps state to direct position measurements.
56+
- IPDA takes care of measurement association probabilities, gating and
57+
existence management (confirmation / deletion thresholds). This
58+
provides robust data association for cluttered measurements and
59+
handles tracks appearing/disappearing over time.
60+
61+
Orientation (SO(3) PDAF):
62+
63+
Orientation is handled by a dedicated PDAF-style filter operating on
64+
the SO(3) manifold. Each track
65+
maintains:
66+
67+
- a quaternion mean q (the track orientation), and
68+
- a small Gaussian error state in the 3D tangent space (so(3)) around
69+
that mean.
70+
71+
72+
The log and exp maps are mathematically defined for all unit quaternions
73+
but the Kalman/PDAF linearization on the tangent space assumes small
74+
errors. The implementation gates measurements and uses configurable
75+
covariances to keep tangent residuals small; if large-angle residuals
76+
occur they will either be gated out or yield small β (limited effect).
77+
78+
## Library export and usage
79+
80+
The core tracking logic (`PoseTrackManager`) is built and exported as a
81+
shared library by this package so other packages can reuse the track
82+
management functionality without pulling in the ROS node.
83+
84+
CMake / ament usage
85+
86+
In a consuming CMake-based package (ament_cmake) you can link against
87+
the library like this:
88+
89+
```cmake
90+
find_package(pose_filtering REQUIRED)
91+
92+
add_executable(my_node src/my_node.cpp)
93+
94+
# Link to the exported library target (the package exports the target
95+
# with the same name as the package: `pose_filtering`)
96+
target_link_libraries(my_node PRIVATE pose_filtering)
97+
98+
```
99+
100+
The package installs its public headers under `include/` so simply
101+
including the header below in your code will work after linking:
102+
103+
```cpp
104+
#include <pose_filtering/lib/pose_track_manager.hpp>
105+
106+
int main() {
107+
vortex::filtering::TrackManagerConfig cfg;
108+
// configure cfg.ipda, cfg.dyn_mod, cfg.sensor_mod, cfg.existence, etc.
109+
110+
vortex::filtering::PoseTrackManager manager(cfg);
111+
112+
std::vector<vortex::utils::types::Pose> measurements;
113+
double dt = 0.1; // seconds
114+
manager.step(measurements, dt);
115+
116+
const auto& tracks = manager.get_tracks();
117+
// iterate tracks for id, pose, existence probability, etc.
118+
}
119+
```
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**:
2+
ros__parameters:
3+
pose_sub_topic: "/aruco_detector/board"
4+
pose_array_pub_topic: "/filtered_pose_array"
5+
landmark_pub_topic: "/filtered_landmarks"
6+
target_frame: "odom"
7+
timer_rate_ms: 200
8+
enu_ned_rotation: true
9+
10+
config:
11+
existence:
12+
confirmation_threshold: 0.6
13+
deletion_threshold: 0.2
14+
initial_existence_probability: 0.4
15+
16+
gate:
17+
min_pos_error: 0.0 # in meters
18+
max_pos_error: 0.5 # in meters
19+
min_ori_error: 0.0 # in radians
20+
max_ori_error: 0.5 # in radians
21+
22+
dyn_mod_std_dev: 0.2
23+
sens_mod_std_dev: 0.2
24+
25+
init_pos_std_dev: 0.1
26+
init_ori_std_dev: 0.05
27+
28+
mahalanobis_gate_threshold: 2.5 # in std deviations
29+
prob_of_detection: 0.5
30+
clutter_intensity: 0.01
31+
prob_of_survival: 0.95
32+
estimate_clutter: false
33+
34+
debug:
35+
enable: true
36+
topic_name_meas: "/pose_debug_meas"
37+
topic_name_state: "/pose_debug_state"

0 commit comments

Comments
 (0)