Program Listing for File particle_cloud.hpp

Return to documentation for file (include/beluga_ros/particle_cloud.hpp)

// Copyright 2024 Ekumen, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef BELUGA_ROS_PARTICLE_CLOUD_HPP
#define BELUGA_ROS_PARTICLE_CLOUD_HPP

#include <cmath>
#include <map>
#include <memory>
#include <type_traits>

#include <range/v3/range/primitives.hpp>
#include <range/v3/view/take_exactly.hpp>

#include <sophus/se2.hpp>
#include <sophus/se3.hpp>
#include <sophus/types.hpp>

#include <beluga/algorithm/spatial_hash.hpp>
#include <beluga/primitives.hpp>
#include <beluga/views/sample.hpp>
#include <beluga/views/zip.hpp>

#include <beluga_ros/messages.hpp>
#include <beluga_ros/tf2_eigen.hpp>
#include <beluga_ros/tf2_sophus.hpp>

namespace beluga_ros {

namespace detail {


inline beluga_ros::msg::ColorRGBA alphaHueToRGBA(float hue, float alpha) {
  beluga_ros::msg::ColorRGBA message;
  // https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative
  // specialized for V = S = 1 and using single precision floats because
  // that is what the color message expects.
  const auto kr = std::fmod(5.0F + hue / 60.0F, 6.0F);
  const auto kg = std::fmod(3.0F + hue / 60.0F, 6.0F);
  const auto kb = std::fmod(1.0F + hue / 60.0F, 6.0F);
  message.r = 1.0F - 1.0F * std::max(0.0F, std::min({kr, 4.0F - kr, 1.0F}));
  message.g = 1.0F - 1.0F * std::max(0.0F, std::min({kg, 4.0F - kg, 1.0F}));
  message.b = 1.0F - 1.0F * std::max(0.0F, std::min({kb, 4.0F - kb, 1.0F}));
  message.a = alpha;
  return message;
}

template <class T>
struct almost_equal_to;

template <typename Scalar>
struct almost_equal_to<Sophus::SE2<Scalar>> {

  explicit almost_equal_to(Scalar _linear_resolution, Scalar _angular_resolution)
      : linear_resolution(_linear_resolution), angular_resolution(_angular_resolution) {}

  bool operator()(const Sophus::SE2<Scalar>& a, const Sophus::SE2<Scalar>& b) const {
    using std::abs;
    const Sophus::SE2<Scalar> diff = a * b.inverse();
    return (abs(diff.translation().x()) < linear_resolution) && (abs(diff.translation().y()) < linear_resolution) &&
           (abs(diff.so2().log()) < angular_resolution);
  }

  const Scalar linear_resolution;
  const Scalar angular_resolution;
};

template <typename Scalar>
struct almost_equal_to<Sophus::SE3<Scalar>> {

  explicit almost_equal_to(Scalar _linear_resolution, Scalar _angular_resolution)
      : linear_resolution(_linear_resolution), angular_resolution(_angular_resolution) {}

  bool operator()(const Sophus::SE3<Scalar>& a, const Sophus::SE3<Scalar>& b) const {
    using std::abs;
    const Sophus::SE3<Scalar> diff = a * b.inverse();
    return (abs(diff.translation().x()) < linear_resolution) && (abs(diff.translation().y()) < linear_resolution) &&
           (abs(diff.translation().z()) < linear_resolution) && (abs(diff.so3().angleX()) < angular_resolution) &&
           (abs(diff.so3().angleY()) < angular_resolution) && (abs(diff.so3().angleZ()) < angular_resolution);
  }

  const Scalar linear_resolution;
  const Scalar angular_resolution;
};

}  // namespace detail


template <
    class Particles,
    class Particle = ranges::range_value_t<Particles>,
    class State = typename beluga::state_t<Particle>,
    class Scalar = typename State::Scalar,
    typename = std::enable_if_t<
        std::is_same_v<State, typename Sophus::SE2<Scalar>> || std::is_same_v<State, typename Sophus::SE3<Scalar>>>>
beluga_ros::msg::PoseArray&
assign_particle_cloud(Particles&& particles, std::size_t size, beluga_ros::msg::PoseArray& message) {
  static_assert(ranges::sized_range<decltype(particles)>);
  message.poses.clear();
  if (ranges::size(particles) > 0) {
    message.poses.reserve(size);
    for (const auto& particle : particles | beluga::views::sample | ranges::views::take_exactly(size)) {
      auto& pose = message.poses.emplace_back();
      tf2::toMsg(beluga::state(particle), pose);
    }
  }
  return message;
}


template <class Particles, class Message>
Message& assign_particle_cloud(Particles&& particles, Message& message) {
  static_assert(ranges::sized_range<decltype(particles)>);
  return assign_particle_cloud(std::forward<Particles>(particles), ranges::size(particles), message);
}


template <
    class Particles,
    class Particle = ranges::range_value_t<Particles>,
    class State = typename beluga::state_t<Particle>,
    class Weight = typename beluga::weight_t<Particle>,
    class Scalar = typename State::Scalar,
    typename = std::enable_if_t<
        std::is_same_v<State, typename Sophus::SE2<Scalar>> || std::is_same_v<State, typename Sophus::SE3<Scalar>>>>
beluga_ros::msg::MarkerArray& assign_particle_cloud(
    Particles&& particles,
    Scalar linear_resolution,
    Scalar angular_resolution,
    beluga_ros::msg::MarkerArray& message) {
  // Particle weights from the filter may or may not be representative of the
  // true distribution. If we resampled, they are not, and there will be multiple copies
  // of the most likely candidates, all with unit weight. In this case the number of copies
  // is a proxy for the prob density at each candidate. If we did not resample before updating
  // the estimation and publishing this message (which can happen if the resample interval
  // is set to something other than 1), then all particles are expected to be different
  // and their weights are proportional to the prob density at each candidate.
  //
  // Only the combination of both the state distribution and the candidate weights together
  // provide information about the probability density at each candidate.
  auto max_bin_weight = Weight{1e-3};
  auto states = beluga::views::states(particles);
  auto weights = beluga::views::weights(particles);
  using StateHistogram = std::unordered_map<State, Weight, beluga::spatial_hash<State>, detail::almost_equal_to<State>>;
  auto histogram = StateHistogram{
      10U, beluga::spatial_hash<State>{linear_resolution, angular_resolution},
      detail::almost_equal_to<State>{linear_resolution, angular_resolution}};
  for (const auto& [state, weight] : beluga::views::zip(states, weights)) {
    auto& bin_weight = histogram[state];
    bin_weight += weight;
    if (bin_weight > max_bin_weight) {
      max_bin_weight = bin_weight;
    }
  }

  message.markers.clear();

  if (histogram.empty()) {
    return message;
  }

  // There are no arrow list markers yet (https://github.com/ros2/rviz/pull/972)
  // so we replicate them using a line list for arrow bodies and a triangle list
  // for arrow heads.
  constexpr auto kArrowBodyLength = Scalar{0.5};
  constexpr auto kArrowHeadLength = Scalar{0.1};
  constexpr auto kArrowHeadWidth = kArrowHeadLength / Scalar{5.0};
  constexpr auto kArrowLength = kArrowBodyLength + kArrowHeadLength;

  using Translation = typename State::TranslationType;
  const auto arrow_body_base = Translation::Zero();
  const auto arrow_head_tip = kArrowLength * Translation::UnitX();
  const auto arrow_head_base = kArrowBodyLength * Translation::UnitX();
  const auto arrow_head_right_corner =
      kArrowBodyLength * Translation::UnitX() - (kArrowHeadWidth / Scalar{2.0}) * Translation::UnitY();
  const auto arrow_head_left_corner =
      kArrowBodyLength * Translation::UnitX() + (kArrowHeadWidth / Scalar{2.0}) * Translation::UnitY();

  message.markers.reserve(2);
  auto& arrow_bodies = message.markers.emplace_back();
  arrow_bodies.id = 0;
  arrow_bodies.ns = "bodies";
  arrow_bodies.color.a = 1.0;
  arrow_bodies.pose.orientation.w = 1.0;
  arrow_bodies.lifetime.sec = 1;
  arrow_bodies.type = beluga_ros::msg::Marker::LINE_LIST;
  arrow_bodies.action = beluga_ros::msg::Marker::ADD;
  arrow_bodies.points.reserve(histogram.size() * 2);  // 2 vertices per arrow body
  arrow_bodies.colors.reserve(histogram.size() * 2);

  auto& arrow_heads = message.markers.emplace_back();
  arrow_heads.id = 1;
  arrow_heads.ns = "heads";
  arrow_heads.scale.x = 1.0;
  arrow_heads.scale.y = 1.0;
  arrow_heads.scale.z = 1.0;
  arrow_heads.color.a = 1.0;
  arrow_heads.pose.orientation.w = 1.0;
  arrow_heads.lifetime.sec = 1;
  arrow_heads.type = beluga_ros::msg::Marker::TRIANGLE_LIST;
  arrow_heads.action = beluga_ros::msg::Marker::ADD;
  arrow_heads.points.reserve(histogram.size() * 3);  // 3 vertices per arrow head
  arrow_heads.colors.reserve(histogram.size() * 3);

  auto min_scale_factor = Weight{1.0};
  for (const auto& [state, weight] : histogram) {
    // fix markers scale ratio to ensure they can still be seen
    using std::max;
    const auto scale_factor = max(weight / max_bin_weight, 1e-1);  // or 1:10
    if (scale_factor < min_scale_factor) {
      min_scale_factor = scale_factor;
    }

    // use an inverted rainbow scale of decreasing opacity: bright red
    // for the most likely state and dim purple for the least likely one
    const auto vertex_color = detail::alphaHueToRGBA(
        static_cast<float>((1.0 - scale_factor) * 270.0), static_cast<float>(0.25 + 0.75 * scale_factor));

    // linearly scale arrow sizes using particle weights too
    arrow_bodies.points.push_back(tf2::toMsg(state * (scale_factor * arrow_body_base)));
    arrow_bodies.points.push_back(tf2::toMsg(state * (scale_factor * arrow_head_base)));
    arrow_bodies.colors.insert(arrow_bodies.colors.end(), 2, vertex_color);

    arrow_heads.points.push_back(tf2::toMsg(state * (scale_factor * arrow_head_left_corner)));
    arrow_heads.points.push_back(tf2::toMsg(state * (scale_factor * arrow_head_right_corner)));
    arrow_heads.points.push_back(tf2::toMsg(state * (scale_factor * arrow_head_tip)));

    arrow_heads.colors.insert(arrow_heads.colors.end(), 3, vertex_color);
  }

  arrow_bodies.scale.x = static_cast<double>(min_scale_factor * kArrowHeadWidth) * 0.8;

  return message;
}


template <
    class Particles,
    class Particle = ranges::range_value_t<Particles>,
    class State = typename beluga::state_t<Particle>,
    class Scalar = typename State::Scalar>
beluga_ros::msg::MarkerArray& assign_particle_cloud(Particles&& particles, beluga_ros::msg::MarkerArray& message) {
  constexpr auto kDefaultLinearResolution = Scalar{1e-3};   // ie. 1 mm
  constexpr auto kDefaultAngularResolution = Scalar{1e-3};  // ie. 0.05 degrees
  return assign_particle_cloud(
      std::forward<Particles>(particles), kDefaultLinearResolution, kDefaultAngularResolution, message);
}

}  // namespace beluga_ros

#endif  // BELUGA_ROS_PARTICLE_CLOUD_HPP