Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module(

bazel_dep(name = "bazel_skylib", version = "1.8.1")
bazel_dep(name = "eigen", version = "3.4.0.bcr.3")
bazel_dep(name = "nanoflann", version = "1.5.5")
bazel_dep(name = "onetbb", version = "2022.1.0")
bazel_dep(name = "package_metadata", version = "0.0.5")
bazel_dep(name = "platforms", version = "1.0.0")
Expand Down
1 change: 1 addition & 0 deletions beluga/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ cc_library(
visibility = ["//visibility:public"],
deps = [
"@eigen",
"@nanoflann",
"@onetbb//:tbb",
"@range-v3",
"@sophus",
Expand Down
2 changes: 2 additions & 0 deletions beluga/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ endif()

find_package(Eigen3 REQUIRED NO_MODULE)
find_package(HDF5 COMPONENTS CXX)
find_package(nanoflann REQUIRED)
find_package(range-v3 REQUIRED)
find_package(Sophus REQUIRED)
find_package(TBB REQUIRED)

set(_deps
Eigen3::Eigen
nanoflann::nanoflann
range-v3::range-v3
Sophus::Sophus
TBB::tbb)
Expand Down
1 change: 1 addition & 0 deletions beluga/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Auto-generated Doxygen documentation can be found in https://ekumen-os.github.io
Beluga is built on top of the following open source libraries:

- [Eigen](https://gitlab.com/libeigen/eigen): A well-known C++ template library for linear algebra: matrices, vectors, numerical solvers, and related algorithms.
- [Nanoflann](https://github.com/jlblancoc/nanoflann): A C++11 header-only library for building KD-Trees.
- [Sophus](https://github.com/strasdat/Sophus): A C++ implementation of Lie groups using Eigen.
- [Range](https://github.com/ericniebler/range-v3): The basis library for C++20's `std::ranges`.

Expand Down
1 change: 1 addition & 0 deletions beluga/cmake/Config.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

include(CMakeFindDependencyMacro)
find_dependency(Eigen3 REQUIRED NO_MODULE)
find_dependency(nanoflann REQUIRED)
find_dependency(range-v3 REQUIRED)
find_dependency(HDF5 COMPONENTS CXX REQUIRED)
find_dependency(Sophus REQUIRED)
Expand Down
1 change: 1 addition & 0 deletions beluga/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ The current set of features includes:
Beluga is built on top of the following open source libraries:

- [Eigen](https://gitlab.com/libeigen/eigen): A well-known C++ template library for linear algebra: matrices, vectors, numerical solvers, and related algorithms.
- [nanoflann](https://github.com/jlblancoc/nanoflann): A C++11 header-only library for building KD-Trees.
- [Sophus](https://github.com/strasdat/Sophus): A C++ implementation of Lie groups using Eigen.
- [Range](https://github.com/ericniebler/range-v3): The basis library for C++20's `std::ranges`.

Expand Down
128 changes: 103 additions & 25 deletions beluga/include/beluga/sensor/data/landmark_map.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@
#define BELUGA_SENSOR_DATA_LANDMARK_MAP_HPP

// external
// Bazel exposes nanoflann with a prefix, CMake does not
#if __has_include(<nanoflann/nanoflann.hpp>)
#include <nanoflann/nanoflann.hpp>
#else
#include <nanoflann.hpp>
#endif
#include <range/v3/view/filter.hpp>
#include <range/v3/view/tail.hpp>
#include <sophus/se3.hpp>

// standard library
#include <algorithm>
#include <cstdint>
#include <memory>
#include <unordered_map>
#include <utility>
#include <vector>

Expand All @@ -48,7 +56,9 @@ class LandmarkMap {
/// @param boundaries Limits of the map.
/// @param landmarks List of landmarks that can be expected to be detected.
explicit LandmarkMap(const LandmarkMapBoundaries& boundaries, landmarks_set_position_data landmarks)
: landmarks_(std::move(landmarks)), map_boundaries_(std::move(boundaries)) {}
: landmarks_(std::move(landmarks)), map_boundaries_(std::move(boundaries)) {
build_category_indices();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels wasteful to store the landmarks along with the nanoflann index, since both contain the same information in different forms, and we only use the index later. We only use the landmarks struct to recreate the index in case of copy/move operations.

We can probably remove landmarks_ if we refactor the category_indices_ storage. Maybe we would have to split build_category_indices in two, one step to fill the pts vectors, and another separate one to create the indices.

The first one is only done in the constructor that takes in the landmarks data. The map of categories and the points for each become trivially copyable/movable in all constructor/assignment operators, and we need only build the nanoflann index from that data.

Does it make sense?

}

/// @brief Constructor with implicit map boundaries (computed from landmarks).
/// @details Note that computing map boundaries from landmarks will effectively
Expand All @@ -67,9 +77,46 @@ class LandmarkMap {
map_boundaries_.min() = map_boundaries_.min().cwiseMin(position);
map_boundaries_.max() = map_boundaries_.max().cwiseMax(position);
}
build_category_indices();
}
}

/// @brief Copy constructor.
/// @details Copying requires expensive reconstruction of the cached kd-tree indices.
/// @deprecated Use move semantics (std::move) instead to avoid performance overhead.
/// @param other Landmark map to copy from.
[[deprecated("LandmarkMap copying is expensive. Use std::move() instead.")]] LandmarkMap(const LandmarkMap& other)
: landmarks_(other.landmarks_), map_boundaries_(other.map_boundaries_) {
build_category_indices();
}

/// @brief Copy assignment operator.
/// @details Copying requires expensive reconstruction of the cached kd-tree indices.
/// @deprecated Use move semantics (std::move) instead to avoid performance overhead.
/// @param other Landmark map to copy from.
/// @return Reference to this landmark map.
[[deprecated("LandmarkMap copying is expensive. Use std::move() instead.")]] LandmarkMap& operator=(
const LandmarkMap& other) {
if (this != &other) {
landmarks_ = other.landmarks_;
map_boundaries_ = other.map_boundaries_;
category_indices_.clear();
build_category_indices();
}
return *this;
}

/// @brief Move constructor.
/// @details Explicitly defaulted so landmark data and cached kd-tree indices
/// can be transferred efficiently while preserving unique ownership semantics.
LandmarkMap(LandmarkMap&&) = default;

/// @brief Move assignment operator.
/// @details Explicitly defaulted so ownership of the cached kd-tree indices can
/// be transferred efficiently without rebuilding them.
/// @return Reference to this landmark map.
LandmarkMap& operator=(LandmarkMap&&) = default;

/// @brief Returns the map boundaries.
/// @return The map boundaries.
[[nodiscard]] LandmarkMapBoundaries map_limits() const { return map_boundaries_; }
Expand All @@ -81,32 +128,19 @@ class LandmarkMap {
[[nodiscard]] std::optional<LandmarkPosition3> find_nearest_landmark(
const LandmarkPosition3& detection_position_in_world,
const LandmarkCategory& detection_category) const {
// only consider those that have the same id
auto same_category_landmarks_view =
landmarks_ | ranges::views::filter([detection_category = detection_category](const auto& l) {
return detection_category == l.category;
});

// find the landmark that minimizes the distance to the detection position
// This is O(n). A spatial data structure should be used instead.
auto min = std::min_element(
same_category_landmarks_view.begin(), same_category_landmarks_view.end(),
[&detection_position_in_world](const auto& a, const auto& b) {
const auto& landmark_a_position_in_world = a.detection_position_in_robot;
const auto& landmark_b_position_in_world = b.detection_position_in_robot;

const auto landmark_b_squared_in_world_squared =
(landmark_a_position_in_world - detection_position_in_world).squaredNorm();
const auto landmark_b_distance_in_world_squared =
(landmark_b_position_in_world - detection_position_in_world).squaredNorm();
return landmark_b_squared_in_world_squared < landmark_b_distance_in_world_squared;
});

if (min == same_category_landmarks_view.end()) {
const auto it = category_indices_.find(detection_category);
if (it == category_indices_.end()) {
return std::nullopt;
}

return min->detection_position_in_robot;
const auto& index = *it->second;
const std::array<double, 3> query = {
detection_position_in_world.x(), detection_position_in_world.y(), detection_position_in_world.z()};
CategoryIndexType result_idx;
double result_dist_sq;
if (index.tree->knnSearch(query.data(), 1, &result_idx, &result_dist_sq) == 0) {
return std::nullopt;
}
return index.cloud.pts[result_idx];
}

/// @brief Finds the landmark that minimizes the bearing error to a given detection and returns its data.
Expand Down Expand Up @@ -161,8 +195,52 @@ class LandmarkMap {
}

private:
/// Point cloud adapter used by the category kd-trees.
struct PositionCloud {
std::vector<LandmarkPosition3> pts;
[[nodiscard]] std::size_t kdtree_get_point_count() const { return pts.size(); }
[[nodiscard]] double kdtree_get_pt(std::size_t i, std::size_t dim) const { return pts[i](static_cast<int>(dim)); }
template <class BBox>
bool kdtree_get_bbox(BBox&) const {
return false;
}
};

/// Index type used by the category kd-trees.
using CategoryIndexType = std::uint32_t;

/// kd-tree type used to query landmarks within each category.
using CategoryKDTree = nanoflann::KDTreeSingleIndexAdaptor<
nanoflann::L2_Simple_Adaptor<double, PositionCloud>,
PositionCloud,
3,
CategoryIndexType>;

/// Cached landmark positions and search tree for a single category.
struct CategoryIndex {
PositionCloud cloud;
std::unique_ptr<CategoryKDTree> tree;
};

landmarks_set_position_data landmarks_;
LandmarkMapBoundaries map_boundaries_;
std::unordered_map<LandmarkCategory, std::unique_ptr<CategoryIndex>> category_indices_;

/// @brief Builds per-category kd-tree indices for nearest-neighbor search.
void build_category_indices() {
for (const auto& l : landmarks_) {
auto& entry = category_indices_[l.category];
if (!entry) {
entry = std::make_unique<CategoryIndex>();
}
entry->cloud.pts.push_back(l.detection_position_in_robot);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be worth it to do a first pass counting how many landmarks for each category are there, to reserve the size in the pts vector and that way avoid reallocations as it grows.

}
for (auto& [_, entry] : category_indices_) {
entry->tree = std::make_unique<CategoryKDTree>(
3, entry->cloud, nanoflann::KDTreeSingleIndexAdaptorParams(/*leaf_max_size=*/10));
entry->tree->buildIndex();
}
}
};

} // namespace beluga
Expand Down
1 change: 1 addition & 0 deletions beluga/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

<depend>eigen</depend>
<depend>libhdf5-dev</depend>
<depend>libnanoflann-dev</depend>
<depend>range-v3</depend>
<depend>sophus</depend>
<depend>tbb</depend>
Expand Down
Loading