Repository navigation
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’ll 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 |
|---|---|---|
|
|
@@ -17,22 +17,51 @@ | |
|
|
||
| #include <algorithm> | ||
| #include <beluga/sensor/likelihood_field_model_base.hpp> | ||
| #include <cassert> | ||
| #include <cmath> | ||
| #include <cstddef> | ||
| #include <cstdint> | ||
| #include <functional> | ||
| #include <random> | ||
| #include <range/v3/numeric/accumulate.hpp> | ||
| #include <range/v3/view/zip.hpp> | ||
| #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. | ||
| /** | ||
| * Must not be greater than `max_obstacle_distance` when `do_beamskip` is enabled. The likelihood | ||
| * field saturates at `max_obstacle_distance`, so beyond that distance it is flat and the agreement | ||
| * test (evaluated on the likelihood field, see beluga::LikelihoodFieldProbModel) stops | ||
| * discriminating: every beam landing further away would be counted as agreeing 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 +83,107 @@ 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. | ||
| * | ||
| * When beam skipping is enabled, `params.beam_skip_distance` must not be greater than | ||
| * `params.max_obstacle_distance`. See beluga::LikelihoodFieldProbModelParam::beam_skip_distance. | ||
| */ | ||
| 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. When beam skipping is disabled this only sizes the mask to the | ||
| * measurement, leaving every beam enabled. | ||
| * | ||
| * 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) { | ||
| // The mask is always sized to match the measurement, so that `operator()` can consume it | ||
| // unconditionally: when beam skipping is disabled every beam stays enabled. | ||
| const std::size_t num_beams = points.size(); | ||
| beam_mask_.assign(num_beams, std::uint8_t{1}); | ||
|
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. Use a static cast instead.
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. Clear the vector and reserve() instead of using asssing. assign will spend time O(N) setting the whole vector to 1s, only for these to be overwritten in line 164. Better leave it empty here, reseve the storage to avoid reallocations, and push_back() the values in line 164. In line 127, do std::fill to ensure these are all 1s before returning if we are not doing beam_skipping. |
||
|
|
||
| if (!do_beamskip_ || 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. | ||
|
Comment on lines
+143
to
+145
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. As a general statement this is dubious to me, since for SE2 Sophus literally store represents the orientation as the sin and cosin values, so the product itself cannot really be calculated that much different from this. If it takes 10x its because we are loading the sophus data and doing an arctan/log at some point along the chain. Still, not for this PR to fix, but I would remove the comment not to repeat an very broad assertion without contextual verifcation. |
||
| 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]; | ||
| } | ||
|
glpuga marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
|
||
| 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}); | ||
|
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. use static cast. |
||
| } | ||
| } | ||
|
|
||
| /// 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 `operator()` | ||
| * treats as 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. | ||
| /** | ||
|
|
@@ -66,28 +192,66 @@ class LikelihoodFieldProbModel : public LikelihoodFieldModelBase<OccupancyGrid> | |
| * and borrowing a reference to this sensor model (and thus their lifetime are bound). | ||
| */ | ||
| [[nodiscard]] auto operator()(measurement_type&& points) const { | ||
| return [this, points = std::move(points)](const state_type& state) -> weight_type { | ||
| // Beams masked out by prepare() contribute nothing to the weight. The mask is resolved once | ||
| // here, so that the per-particle loop below can zip it against the measurement without any | ||
| // further bookkeeping. When prepare() was not called for this measurement (e.g. when the model | ||
| // is used standalone, outside the two pass update) every beam is integrated, reproducing the | ||
| // plain likelihood field prob behavior. | ||
| auto mask = beam_mask_.size() == points.size() // | ||
|
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. doesn't prepare ensure that these two are equal length? |
||
| ? beam_mask_ | ||
| : std::vector<std::uint8_t>(points.size(), std::uint8_t{1}); | ||
|
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. This will materialize an std::vector, allocating memory on each call, substantially slowing down the function. Use a ranges generator that returns the same number on all invocations. |
||
|
|
||
| return [this, points = std::move(points), mask = std::move(mask)](const state_type& state) -> weight_type { | ||
| 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(); | ||
| 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); | ||
| })); | ||
| const auto beam_log_likelihood = [this, x_offset, y_offset, cos_theta, sin_theta, | ||
| unknown_space_occupancy_prob](const auto& tuple) { | ||
| const auto& [point, is_beam_used] = tuple; | ||
| if (!is_beam_used) { | ||
| return 0.0; // Neutral element of the log-likelihood sum below. | ||
| } | ||
| // 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); | ||
| }; | ||
|
|
||
| return std::exp(ranges::accumulate(ranges::views::zip(points, mask), 0.0, std::plus{}, beam_log_likelihood)); | ||
| }; | ||
| } | ||
|
|
||
| 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). | ||
|
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. A vector of booleans would work here. 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. I would consider keeping the
|
||
|
|
||
| /// 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. | ||
| * | ||
| * That equivalence only holds within the range the likelihood field actually resolves distances | ||
| * in, hence `beam_skip_distance <= max_obstacle_distance` is a requirement of the configuration. | ||
| */ | ||
| static float compute_likelihood_threshold(const param_type& params) { | ||
| assert(!params.do_beamskip || params.beam_skip_distance <= params.max_obstacle_distance); | ||
|
|
||
| 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.