-
Notifications
You must be signed in to change notification settings - Fork 34
Added: beam-skipping to likelihood-field-prob #577
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,21 +18,40 @@ | |
| #include <algorithm> | ||
| #include <beluga/sensor/likelihood_field_model_base.hpp> | ||
| #include <cmath> | ||
| #include <cstddef> | ||
| #include <cstdint> | ||
| #include <random> | ||
| #include <vector> | ||
|
|
||
| #include <sophus/se2.hpp> | ||
|
|
||
| /** | ||
| * \file | ||
| * \brief Implementation of a likelihood field prob sensor model for range finders. | ||
| */ | ||
|
|
||
| namespace beluga { | ||
|
|
||
| /// Parameters used to construct a LikelihoodFieldProbModelParam instance. | ||
| /// Parameters used to construct a LikelihoodFieldProbModel instance. | ||
| /** | ||
| * See Probabilistic Robotics \cite thrun2005probabilistic Chapter 6.4, particularly Table 6.3. | ||
| * | ||
| * Inherits the likelihood field parameters from beluga::LikelihoodFieldModelBaseParam and | ||
| * adds the fields configuring the optional beam skipping heuristic, which detects beams that | ||
| * disagree with the map across a large fraction of the particle set (e.g. caused by | ||
| * unmapped/dynamic obstacles) and excludes them from the weight computation. Beam skipping is | ||
| * disabled by default; see https://github.com/Ekumen-OS/beluga/issues/187 for context. | ||
| */ | ||
| using LikelihoodFieldProbModelParam = LikelihoodFieldModelBaseParam; | ||
| struct LikelihoodFieldProbModelParam : public LikelihoodFieldModelBaseParam { | ||
| /// Whether to enable the beam skipping heuristic. | ||
| bool do_beamskip = false; | ||
| /// Distance to the nearest obstacle below which a beam is considered to agree with the map. | ||
| double beam_skip_distance = 0.5; | ||
| /// Fraction of particles that must agree on a beam for it to be kept. | ||
| double beam_skip_threshold = 0.3; | ||
| /// If the fraction of skipped beams exceeds this value, skipping is disabled for the update. | ||
| double beam_skip_error_threshold = 0.9; | ||
| }; | ||
|
|
||
| /// Likelihood field prob sensor model for range finders. | ||
| /** | ||
|
|
@@ -54,10 +73,104 @@ class LikelihoodFieldProbModel : public LikelihoodFieldModelBase<OccupancyGrid> | |
|
|
||
| /// Constructs a LikelihoodFieldProbModel instance. | ||
| /** | ||
| * @copydoc LikelihoodFieldModelBase::LikelihoodFieldModelBase | ||
| * \param params Parameters to configure this instance. | ||
| * See beluga::LikelihoodFieldProbModelParam for details. | ||
| * \param grid Occupancy grid representing the static map that the sensor model | ||
| * uses to compute a likelihood field for lidar hits and compute importance weights | ||
| * for particle states. | ||
| */ | ||
| explicit LikelihoodFieldProbModel(const param_type& params, const map_type& grid) | ||
| : LikelihoodFieldModelBase<OccupancyGrid>(params, grid) {} | ||
| : LikelihoodFieldModelBase<OccupancyGrid>(params, grid), | ||
| do_beamskip_{params.do_beamskip}, | ||
| beam_skip_threshold_{params.beam_skip_threshold}, | ||
| beam_skip_error_threshold_{params.beam_skip_error_threshold}, | ||
| likelihood_threshold_{compute_likelihood_threshold(params)} {} | ||
|
|
||
| /// Precomputes the beam skipping mask from the current particle states. | ||
| /** | ||
| * Runs the first pass of the beam skipping heuristic: for every beam, it counts the | ||
| * fraction of particle states for which the beam endpoint lands close enough to a mapped | ||
| * obstacle (within `beam_skip_distance`, expressed here as a likelihood threshold). Beams | ||
| * for which that fraction falls below `beam_skip_threshold` are masked out and ignored by | ||
| * the subsequent weight computation in `operator()`. If skipping would discard more than | ||
| * `beam_skip_error_threshold` of the beams, the mask is reset so that all beams are used, | ||
| * preventing filter divergence. This is a no-op when beam skipping is disabled. | ||
| * | ||
| * Must be called once per update, after motion propagation and before reweighting. | ||
| * | ||
| * \tparam StateRange A range of particle states (Sophus::SE2d). | ||
| * \param points 2D lidar hit points in the reference frame of particle states. | ||
| * \param states Range with the (propagated) particle states for the current update. | ||
| */ | ||
| template <class StateRange> | ||
| void prepare(const measurement_type& points, StateRange&& states) { | ||
| if (!do_beamskip_) { | ||
| return; | ||
| } | ||
|
|
||
| const std::size_t num_beams = points.size(); | ||
| beam_mask_.assign(num_beams, std::uint8_t{1}); | ||
| if (num_beams == 0) { | ||
| return; | ||
| } | ||
|
|
||
| const auto unknown_space_occupancy_prob = static_cast<float>(1. / this->params_.max_laser_distance); | ||
| std::vector<std::size_t> obs_count(num_beams, 0); | ||
| std::size_t num_states = 0; | ||
|
|
||
| for (const auto& state : states) { | ||
| ++num_states; | ||
| const auto transform = this->world_to_likelihood_field_transform_ * state; | ||
| const auto x_offset = transform.translation().x(); | ||
| const auto y_offset = transform.translation().y(); | ||
| const auto cos_theta = transform.so2().unit_complex().x(); | ||
| const auto sin_theta = transform.so2().unit_complex().y(); | ||
| for (std::size_t i = 0; i < num_beams; ++i) { | ||
| const auto& point = points[i]; | ||
| // Transform the end point of the laser to the grid local coordinate system. | ||
| // Not using Eigen/Sophus because they make the routine x10 slower. | ||
| // See `benchmark_likelihood_field_model.cpp` for reference. | ||
| const auto x = point.first * cos_theta - point.second * sin_theta + x_offset; | ||
| const auto y = point.first * sin_theta + point.second * cos_theta + y_offset; | ||
|
ralcoberro marked this conversation as resolved.
|
||
| const auto pz = this->likelihood_field_.data_near(x, y).value_or(unknown_space_occupancy_prob); | ||
| // The likelihood field is monotonically decreasing in the distance to the nearest | ||
| // obstacle, so "distance < beam_skip_distance" is equivalent to "pz > threshold". | ||
| if (pz > likelihood_threshold_) { | ||
| ++obs_count[i]; | ||
| } | ||
|
Comment on lines
+135
to
+140
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a bit unfortunate that we need to use the probability as a proxy for distance, because it will not be correct. That is mainly because there exists a That means if the if the beam_skip_distance > laser_likelihood_max_dist , we will count all rays that hit beyond laser_likelihood_max_dist as being "in". We don't have a distance map, which would solve this, but I think that's good because having one would bring trouble:
Both issues probably have other solutions that the ones currently in use, but that would mean a deviation from the current behavior of nav2. We can discuss it, though.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you Gera, let's talk about this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let me know when you want to talk about this.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tagging @LaBruma for awareness. |
||
| } | ||
| } | ||
|
|
||
| if (num_states == 0) { | ||
| return; | ||
| } | ||
|
|
||
| std::size_t skipped = 0; | ||
| for (std::size_t i = 0; i < num_beams; ++i) { | ||
| const double ratio = static_cast<double>(obs_count[i]) / static_cast<double>(num_states); | ||
| beam_mask_[i] = static_cast<std::uint8_t>(ratio > beam_skip_threshold_); | ||
| if (!beam_mask_[i]) { | ||
| ++skipped; | ||
| } | ||
| } | ||
|
|
||
| // Safety fallback: if too many beams would be skipped, integrate all of them instead. | ||
| const double skipped_ratio = static_cast<double>(skipped) / static_cast<double>(num_beams); | ||
| if (skipped_ratio > beam_skip_error_threshold_) { | ||
| std::fill(beam_mask_.begin(), beam_mask_.end(), std::uint8_t{1}); | ||
| } | ||
| } | ||
|
|
||
| /// Returns the current beam skipping mask (one flag per beam, non-zero means the beam is used). | ||
| /** | ||
| * The mask is populated by `prepare()`. It is empty until the first call, which is | ||
| * equivalent to using every beam. Mainly useful for introspection and testing. | ||
| * | ||
| * A `std::vector<std::uint8_t>` is used instead of `std::vector<bool>` to avoid the bit-packed | ||
| * specialization: the mask is read once per beam per particle in the reweight hot path, where a | ||
| * plain byte load is faster and more cache friendly than the bit-masking `std::vector<bool>` does. | ||
| */ | ||
| [[nodiscard]] const std::vector<std::uint8_t>& beam_mask() const { return beam_mask_; } | ||
|
|
||
| /// Returns a state weighting function conditioned on 2D lidar hits. | ||
| /** | ||
|
|
@@ -74,20 +187,46 @@ class LikelihoodFieldProbModel : public LikelihoodFieldModelBase<OccupancyGrid> | |
| const auto sin_theta = transform.so2().unit_complex().y(); | ||
| const auto unknown_space_occupancy_prob = static_cast<float>(1. / this->params_.max_laser_distance); | ||
|
|
||
| return std::exp(std::transform_reduce( | ||
| points.cbegin(), points.cend(), 0.0, std::plus{}, | ||
| [this, x_offset, y_offset, cos_theta, sin_theta, unknown_space_occupancy_prob](const auto& point) { | ||
| // Transform the end point of the laser to the grid local coordinate system. | ||
| // Not using Eigen/Sophus because they make the routine x10 slower. | ||
| // See `benchmark_likelihood_field_model.cpp` for reference. | ||
| const auto x = point.first * cos_theta - point.second * sin_theta + x_offset; | ||
| const auto y = point.first * sin_theta + point.second * cos_theta + y_offset; | ||
| const auto pz = | ||
| static_cast<double>(this->likelihood_field_.data_near(x, y).value_or(unknown_space_occupancy_prob)); | ||
| return std::log(pz); | ||
| })); | ||
| double log_weight = 0.0; | ||
| for (std::size_t i = 0; i < points.size(); ++i) { | ||
| // Skip beams that were masked out by prepare(). When beam skipping is disabled (or | ||
| // prepare() was never called) the mask is not consulted and every beam contributes, | ||
| // reproducing the plain likelihood field prob behavior. | ||
| if (do_beamskip_ && i < beam_mask_.size() && !beam_mask_[i]) { | ||
| continue; | ||
| } | ||
| // Transform the end point of the laser to the grid local coordinate system. | ||
| // Not using Eigen/Sophus because they make the routine x10 slower. | ||
| // See `benchmark_likelihood_field_model.cpp` for reference. | ||
| const auto& point = points[i]; | ||
| const auto x = point.first * cos_theta - point.second * sin_theta + x_offset; | ||
| const auto y = point.first * sin_theta + point.second * cos_theta + y_offset; | ||
| const auto pz = | ||
| static_cast<double>(this->likelihood_field_.data_near(x, y).value_or(unknown_space_occupancy_prob)); | ||
| log_weight += std::log(pz); | ||
| } | ||
| return std::exp(log_weight); | ||
|
Comment on lines
+190
to
+208
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can do this without resorting back to for loops and at the same time reduce the amount of changes by zipping together the beam_mask and the points vectors, and returning 0.0 from the lambda if the beam is masked. You don't need to check do_beamskip_ because the mask will be true for all beams in that case anyways.,
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| }; | ||
| } | ||
|
|
||
| private: | ||
| bool do_beamskip_; ///< Whether the beam skipping heuristic is enabled. | ||
| double beam_skip_threshold_; ///< Fraction of particles that must agree for a beam to be kept. | ||
| double beam_skip_error_threshold_; ///< Skipped-beam fraction above which skipping is disabled. | ||
| float likelihood_threshold_; ///< Likelihood equivalent of `beam_skip_distance`. | ||
| std::vector<std::uint8_t> beam_mask_; ///< Per-beam mask computed by `prepare()` (non-zero means used). | ||
|
|
||
| /// Converts `beam_skip_distance` into the equivalent likelihood field value. | ||
| /** | ||
| * Reuses the base class likelihood profile (the same gaussian used to build the likelihood | ||
| * field), so that the "distance to obstacle < beam_skip_distance" agreement test can be | ||
| * evaluated directly on the precomputed likelihood field without keeping the distance map around. | ||
| */ | ||
| static float compute_likelihood_threshold(const param_type& params) { | ||
| const auto profile = LikelihoodFieldModelBase<OccupancyGrid>::make_likelihood_profile(params); | ||
| const double squared_distance = params.beam_skip_distance * params.beam_skip_distance; | ||
| return static_cast<float>(profile(squared_distance)); | ||
| } | ||
|
ralcoberro marked this conversation as resolved.
|
||
| }; | ||
|
|
||
| } // namespace beluga | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.