/home/runner/work/beluga/beluga/beluga_ros/include/beluga_ros/particle_cloud.hpp Source File

/home/runner/work/beluga/beluga/beluga_ros/include/beluga_ros/particle_cloud.hpp Source File#

Beluga ROS: /home/runner/work/beluga/beluga/beluga_ros/include/beluga_ros/particle_cloud.hpp Source File
Beluga ROS
particle_cloud.hpp
Go to the documentation of this file.
1 // Copyright 2024 Ekumen, Inc.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #ifndef BELUGA_ROS_PARTICLE_CLOUD_HPP
16 #define BELUGA_ROS_PARTICLE_CLOUD_HPP
17 
18 #include <cmath>
19 #include <map>
20 #include <memory>
21 #include <type_traits>
22 
23 #include <range/v3/range/primitives.hpp>
24 #include <range/v3/view/take_exactly.hpp>
25 
26 #include <sophus/se2.hpp>
27 #include <sophus/se3.hpp>
28 #include <sophus/types.hpp>
29 
31 #include <beluga/primitives.hpp>
32 #include <beluga/views/sample.hpp>
33 #include <beluga/views/zip.hpp>
34 
35 #include <beluga_ros/tf2_eigen.hpp>
37 
38 #include <geometry_msgs/msg/pose_array.hpp>
39 #include <std_msgs/msg/color_rgba.hpp>
40 #include <visualization_msgs/msg/marker.hpp>
41 #include <visualization_msgs/msg/marker_array.hpp>
42 
48 namespace beluga_ros {
49 
50 namespace detail {
51 
53 
56 inline std_msgs::msg::ColorRGBA alphaHueToRGBA(float hue, float alpha) {
57  std_msgs::msg::ColorRGBA message;
58  // https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative
59  // specialized for V = S = 1 and using single precision floats because
60  // that is what the color message expects.
61  const auto kr = std::fmod(5.0F + hue / 60.0F, 6.0F);
62  const auto kg = std::fmod(3.0F + hue / 60.0F, 6.0F);
63  const auto kb = std::fmod(1.0F + hue / 60.0F, 6.0F);
64  message.r = 1.0F - 1.0F * std::max(0.0F, std::min({kr, 4.0F - kr, 1.0F}));
65  message.g = 1.0F - 1.0F * std::max(0.0F, std::min({kg, 4.0F - kg, 1.0F}));
66  message.b = 1.0F - 1.0F * std::max(0.0F, std::min({kb, 4.0F - kb, 1.0F}));
67  message.a = alpha;
68  return message;
69 }
70 
72 template <class T>
74 
76 template <typename Scalar>
77 struct almost_equal_to<Sophus::SE2<Scalar>> {
79 
83  explicit almost_equal_to(Scalar _linear_resolution, Scalar _angular_resolution)
84  : linear_resolution(_linear_resolution), angular_resolution(_angular_resolution) {}
85 
87  bool operator()(const Sophus::SE2<Scalar>& a, const Sophus::SE2<Scalar>& b) const {
88  using std::abs;
89  const Sophus::SE2<Scalar> diff = a * b.inverse();
90  return (abs(diff.translation().x()) < linear_resolution) && (abs(diff.translation().y()) < linear_resolution) &&
91  (abs(diff.so2().log()) < angular_resolution);
92  }
93 
94  const Scalar linear_resolution;
95  const Scalar angular_resolution;
96 };
97 
99 template <typename Scalar>
100 struct almost_equal_to<Sophus::SE3<Scalar>> {
102 
106  explicit almost_equal_to(Scalar _linear_resolution, Scalar _angular_resolution)
107  : linear_resolution(_linear_resolution), angular_resolution(_angular_resolution) {}
108 
110  bool operator()(const Sophus::SE3<Scalar>& a, const Sophus::SE3<Scalar>& b) const {
111  using std::abs;
112  const Sophus::SE3<Scalar> diff = a * b.inverse();
113  return (abs(diff.translation().x()) < linear_resolution) && (abs(diff.translation().y()) < linear_resolution) &&
114  (abs(diff.translation().z()) < linear_resolution) && (abs(diff.so3().angleX()) < angular_resolution) &&
115  (abs(diff.so3().angleY()) < angular_resolution) && (abs(diff.so3().angleZ()) < angular_resolution);
116  }
117 
118  const Scalar linear_resolution;
119  const Scalar angular_resolution;
120 };
121 
122 } // namespace detail
123 
125 
131 template <
132  class Particles,
133  class Particle = ranges::range_value_t<Particles>,
134  class State = typename beluga::state_t<Particle>,
135  class Scalar = typename State::Scalar,
136  typename = std::enable_if_t<
137  std::is_same_v<State, typename Sophus::SE2<Scalar>> || std::is_same_v<State, typename Sophus::SE3<Scalar>>>>
138 geometry_msgs::msg::PoseArray&
139 assign_particle_cloud(Particles&& particles, std::size_t size, geometry_msgs::msg::PoseArray& message) {
140  static_assert(ranges::sized_range<decltype(particles)>);
141  message.poses.clear();
142  if (ranges::size(particles) > 0) {
143  message.poses.reserve(size);
144  for (const auto& particle : particles | beluga::views::sample | ranges::views::take_exactly(size)) {
145  auto& pose = message.poses.emplace_back();
146  tf2::toMsg(beluga::state(particle), pose);
147  }
148  }
149  return message;
150 }
151 
153 
160 template <class Particles, class Message>
161 Message& assign_particle_cloud(Particles&& particles, Message& message) {
162  static_assert(ranges::sized_range<decltype(particles)>);
163  return assign_particle_cloud(std::forward<Particles>(particles), ranges::size(particles), message);
164 }
165 
167 
174 template <
175  class Particles,
176  class Particle = ranges::range_value_t<Particles>,
177  class State = typename beluga::state_t<Particle>,
178  class Weight = typename beluga::weight_t<Particle>,
179  class Scalar = typename State::Scalar,
180  typename = std::enable_if_t<
181  std::is_same_v<State, typename Sophus::SE2<Scalar>> || std::is_same_v<State, typename Sophus::SE3<Scalar>>>>
182 visualization_msgs::msg::MarkerArray& assign_particle_cloud(
183  Particles&& particles,
184  Scalar linear_resolution,
185  Scalar angular_resolution,
186  visualization_msgs::msg::MarkerArray& message) {
187  // Particle weights from the filter may or may not be representative of the
188  // true distribution. If we resampled, they are not, and there will be multiple copies
189  // of the most likely candidates, all with unit weight. In this case the number of copies
190  // is a proxy for the prob density at each candidate. If we did not resample before updating
191  // the estimation and publishing this message (which can happen if the resample interval
192  // is set to something other than 1), then all particles are expected to be different
193  // and their weights are proportional to the prob density at each candidate.
194  //
195  // Only the combination of both the state distribution and the candidate weights together
196  // provide information about the probability density at each candidate.
197  auto max_bin_weight = Weight{1e-3};
198  auto states = beluga::views::states(particles);
199  auto weights = beluga::views::weights(particles);
200  using StateHistogram = std::unordered_map<State, Weight, beluga::spatial_hash<State>, detail::almost_equal_to<State>>;
201  auto histogram = StateHistogram{
202  10U, beluga::spatial_hash<State>{linear_resolution, angular_resolution},
203  detail::almost_equal_to<State>{linear_resolution, angular_resolution}};
204  for (const auto& [state, weight] : beluga::views::zip(states, weights)) {
205  auto& bin_weight = histogram[state];
206  bin_weight += weight;
207  if (bin_weight > max_bin_weight) {
208  max_bin_weight = bin_weight;
209  }
210  }
211 
212  message.markers.clear();
213 
214  if (histogram.empty()) {
215  return message;
216  }
217 
218  // There are no arrow list markers yet (https://github.com/ros2/rviz/pull/972)
219  // so we replicate them using a line list for arrow bodies and a triangle list
220  // for arrow heads.
221  constexpr auto kArrowBodyLength = Scalar{0.5};
222  constexpr auto kArrowHeadLength = Scalar{0.1};
223  constexpr auto kArrowHeadWidth = kArrowHeadLength / Scalar{5.0};
224  constexpr auto kArrowLength = kArrowBodyLength + kArrowHeadLength;
225 
226  using Translation = typename State::TranslationType;
227  const auto arrow_body_base = Translation::Zero();
228  const auto arrow_head_tip = kArrowLength * Translation::UnitX();
229  const auto arrow_head_base = kArrowBodyLength * Translation::UnitX();
230  const auto arrow_head_right_corner =
231  kArrowBodyLength * Translation::UnitX() - (kArrowHeadWidth / Scalar{2.0}) * Translation::UnitY();
232  const auto arrow_head_left_corner =
233  kArrowBodyLength * Translation::UnitX() + (kArrowHeadWidth / Scalar{2.0}) * Translation::UnitY();
234 
235  message.markers.reserve(2);
236  auto& arrow_bodies = message.markers.emplace_back();
237  arrow_bodies.id = 0;
238  arrow_bodies.ns = "bodies";
239  arrow_bodies.color.a = 1.0;
240  arrow_bodies.pose.orientation.w = 1.0;
241  arrow_bodies.lifetime.sec = 1;
242  arrow_bodies.type = visualization_msgs::msg::Marker::LINE_LIST;
243  arrow_bodies.action = visualization_msgs::msg::Marker::ADD;
244  arrow_bodies.points.reserve(histogram.size() * 2); // 2 vertices per arrow body
245  arrow_bodies.colors.reserve(histogram.size() * 2);
246 
247  auto& arrow_heads = message.markers.emplace_back();
248  arrow_heads.id = 1;
249  arrow_heads.ns = "heads";
250  arrow_heads.scale.x = 1.0;
251  arrow_heads.scale.y = 1.0;
252  arrow_heads.scale.z = 1.0;
253  arrow_heads.color.a = 1.0;
254  arrow_heads.pose.orientation.w = 1.0;
255  arrow_heads.lifetime.sec = 1;
256  arrow_heads.type = visualization_msgs::msg::Marker::TRIANGLE_LIST;
257  arrow_heads.action = visualization_msgs::msg::Marker::ADD;
258  arrow_heads.points.reserve(histogram.size() * 3); // 3 vertices per arrow head
259  arrow_heads.colors.reserve(histogram.size() * 3);
260 
261  auto min_scale_factor = Weight{1.0};
262  for (const auto& [state, weight] : histogram) {
263  // fix markers scale ratio to ensure they can still be seen
264  using std::max;
265  const auto scale_factor = max(weight / max_bin_weight, 1e-1); // or 1:10
266  if (scale_factor < min_scale_factor) {
267  min_scale_factor = scale_factor;
268  }
269 
270  // use an inverted rainbow scale of decreasing opacity: bright red
271  // for the most likely state and dim purple for the least likely one
272  const auto vertex_color = detail::alphaHueToRGBA(
273  static_cast<float>((1.0 - scale_factor) * 270.0), static_cast<float>(0.25 + 0.75 * scale_factor));
274 
275  // linearly scale arrow sizes using particle weights too
276  arrow_bodies.points.push_back(tf2::toMsg(state * (scale_factor * arrow_body_base)));
277  arrow_bodies.points.push_back(tf2::toMsg(state * (scale_factor * arrow_head_base)));
278  arrow_bodies.colors.insert(arrow_bodies.colors.end(), 2, vertex_color);
279 
280  arrow_heads.points.push_back(tf2::toMsg(state * (scale_factor * arrow_head_left_corner)));
281  arrow_heads.points.push_back(tf2::toMsg(state * (scale_factor * arrow_head_right_corner)));
282  arrow_heads.points.push_back(tf2::toMsg(state * (scale_factor * arrow_head_tip)));
283 
284  arrow_heads.colors.insert(arrow_heads.colors.end(), 3, vertex_color);
285  }
286 
287  arrow_bodies.scale.x = static_cast<double>(min_scale_factor * kArrowHeadWidth) * 0.8;
288 
289  return message;
290 }
291 
293 
298 template <
299  class Particles,
300  class Particle = ranges::range_value_t<Particles>,
301  class State = typename beluga::state_t<Particle>,
302  class Scalar = typename State::Scalar>
303 visualization_msgs::msg::MarkerArray& assign_particle_cloud(
304  Particles&& particles,
305  visualization_msgs::msg::MarkerArray& message) {
306  constexpr auto kDefaultLinearResolution = Scalar{1e-3}; // ie. 1 mm
307  constexpr auto kDefaultAngularResolution = Scalar{1e-3}; // ie. 0.05 degrees
308  return assign_particle_cloud(
309  std::forward<Particles>(particles), kDefaultLinearResolution, kDefaultAngularResolution, message);
310 }
311 
312 } // namespace beluga_ros
313 
314 #endif // BELUGA_ROS_PARTICLE_CLOUD_HPP
Sophus namespace extension for message conversion function overload resolution (ADL enabled).
Definition: tf2_sophus.hpp:332
The main Beluga ROS namespace.
Definition: amcl.hpp:47
geometry_msgs::msg::PoseArray & assign_particle_cloud(Particles &&particles, std::size_t size, geometry_msgs::msg::PoseArray &message)
Assign a pose distribution to a particle cloud message.
Definition: particle_cloud.hpp:139
typename particle_traits< T >::weight_type weight_t
constexpr state_detail::state_fn state
Numeric< double, struct WeightTag > Weight
constexpr weight_detail::weight_fn weight
typename particle_traits< T >::state_type state_t
geometry_msgs::msg::Point toMsg(const Eigen::Matrix< Scalar, 2, 1 > &in)
Converts an Eigen Vector2 type to a Point message.
Definition: tf2_eigen.hpp:39
std_msgs::msg::ColorRGBA alphaHueToRGBA(float hue, float alpha)
Make an RGBA color based on hue alone.
Definition: particle_cloud.hpp:56
constexpr auto states
constexpr auto weights
bool operator()(const Sophus::SE2< Scalar > &a, const Sophus::SE2< Scalar > &b) const
Compares a and b for near equality.
Definition: particle_cloud.hpp:87
const Scalar angular_resolution
Resolution for rotational coordinates, in radians.
Definition: particle_cloud.hpp:95
almost_equal_to(Scalar _linear_resolution, Scalar _angular_resolution)
Constructs near equality functor.
Definition: particle_cloud.hpp:83
const Scalar linear_resolution
Resolution for translational coordinates, in meters.
Definition: particle_cloud.hpp:94
const Scalar angular_resolution
Resolution for rotational coordinates, in radians.
Definition: particle_cloud.hpp:119
almost_equal_to(Scalar _linear_resolution, Scalar _angular_resolution)
Constructs near equality functor.
Definition: particle_cloud.hpp:106
bool operator()(const Sophus::SE3< Scalar > &a, const Sophus::SE3< Scalar > &b) const
Compares a and b for near equality.
Definition: particle_cloud.hpp:110
const Scalar linear_resolution
Resolution for translational coordinates, in meters.
Definition: particle_cloud.hpp:118
std::equal_to equivalent for near equality operations.
Definition: particle_cloud.hpp:73
Message conversion API overloads for Eigen types.
Message conversion API overloads for Sophus types.