From 2d087402207cf94081ce3996b4c08f3d014d2a0e Mon Sep 17 00:00:00 2001
From: Nicola Loi <nicolaloi@outlook.com>
Date: Sat, 2 Nov 2024 12:05:53 +0100
Subject: [PATCH 1/2] Add select_by_index method to Feature class

---
 CHANGELOG.md                                  |  1 +
 cpp/open3d/pipelines/registration/Feature.cpp | 25 +++++++++++++++
 cpp/open3d/pipelines/registration/Feature.h   |  8 +++++
 cpp/pybind/pipelines/registration/feature.cpp | 10 ++++++
 .../t/pipelines/registration/Feature.cpp      | 32 +++++++++++++++++++
 5 files changed, 76 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7fb1cf1e1aa..efc11e57ef5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -51,6 +51,7 @@
 -   Fix tensor EstimatePointWiseNormalsWithFastEigen3x3 (PR #6980)
 -   Fix alpha shape reconstruction if alpha too small for point scale (PR #6998)
 -   Fix render to depth image on Apple Retina displays (PR #7001)
+-   Add select_by_index method to Feature class (PR #7039)
 
 ## 0.13
 
diff --git a/cpp/open3d/pipelines/registration/Feature.cpp b/cpp/open3d/pipelines/registration/Feature.cpp
index 41b036ebbab..e45f5672d9d 100644
--- a/cpp/open3d/pipelines/registration/Feature.cpp
+++ b/cpp/open3d/pipelines/registration/Feature.cpp
@@ -18,6 +18,31 @@ namespace open3d {
 namespace pipelines {
 namespace registration {
 
+std::shared_ptr<Feature> Feature::SelectByIndex(
+        const std::vector<size_t> &indices, bool invert) const {
+    auto output = std::make_shared<Feature>();
+    output->Resize(data_.rows(), indices.size());
+
+    std::vector<bool> mask = std::vector<bool>(data_.cols(), invert);
+    for (size_t i : indices) {
+        mask[i] = !invert;
+    }
+
+    size_t current_col_feature = 0;
+    for (size_t i = 0; i < static_cast<size_t>(data_.cols()); i++) {
+        if (mask[i]) {
+            output->data_.col(current_col_feature) = data_.col(i);
+            current_col_feature++;
+        }
+    }
+
+    utility::LogDebug(
+            "Feature group down sampled from {:d} features to {:d} features.",
+            (int)data_.cols(), (int)output->data_.cols());
+
+    return output;
+}
+
 static Eigen::Vector4d ComputePairFeatures(const Eigen::Vector3d &p1,
                                            const Eigen::Vector3d &n1,
                                            const Eigen::Vector3d &p2,
diff --git a/cpp/open3d/pipelines/registration/Feature.h b/cpp/open3d/pipelines/registration/Feature.h
index 41bb4776a35..f4d9630688d 100644
--- a/cpp/open3d/pipelines/registration/Feature.h
+++ b/cpp/open3d/pipelines/registration/Feature.h
@@ -42,6 +42,14 @@ class Feature {
     /// Returns number of points.
     size_t Num() const { return data_.cols(); }
 
+    /// \brief Selects features from \p input Feature group, with indices in \p
+    /// indices, and returns a new Feature group with selected features.
+    ///
+    /// \param indices Indices of features to be selected.
+    /// \param invert Set to `True` to invert the selection of indices.
+    std::shared_ptr<Feature> SelectByIndex(const std::vector<size_t> &indices,
+                                           bool invert = false) const;
+
 public:
     /// Data buffer storing features.
     Eigen::MatrixXd data_;
diff --git a/cpp/pybind/pipelines/registration/feature.cpp b/cpp/pybind/pipelines/registration/feature.cpp
index ce298dceec3..fd40b5b5f94 100644
--- a/cpp/pybind/pipelines/registration/feature.cpp
+++ b/cpp/pybind/pipelines/registration/feature.cpp
@@ -31,6 +31,11 @@ void pybind_feature_definitions(py::module &m_registration) {
             .def("dimension", &Feature::Dimension,
                  "Returns feature dimensions per point.")
             .def("num", &Feature::Num, "Returns number of points.")
+            .def("select_by_index", &Feature::SelectByIndex,
+                 "Function to select features from input Feature group into "
+                 "output "
+                 "Feature group.",
+                 "indices"_a, "invert"_a = false)
             .def_readwrite("data", &Feature::data_,
                            "``dim x n`` float64 numpy array: Data buffer "
                            "storing features.")
@@ -48,6 +53,11 @@ void pybind_feature_definitions(py::module &m_registration) {
     docstring::ClassMethodDocInject(m_registration, "Feature", "resize",
                                     {{"dim", "Feature dimension per point."},
                                      {"n", "Number of points."}});
+    docstring::ClassMethodDocInject(
+            m_registration, "Feature", "select_by_index",
+            {{"indices", "Indices of features to be selected."},
+             {"invert",
+              "Set to ``True`` to invert the selection of indices."}});
     m_registration.def("compute_fpfh_feature", &ComputeFPFHFeature,
                        "Function to compute FPFH feature for a point cloud",
                        "input"_a, "search_param"_a);
diff --git a/cpp/tests/t/pipelines/registration/Feature.cpp b/cpp/tests/t/pipelines/registration/Feature.cpp
index b636b8cd516..d4adc0958b3 100644
--- a/cpp/tests/t/pipelines/registration/Feature.cpp
+++ b/cpp/tests/t/pipelines/registration/Feature.cpp
@@ -25,6 +25,38 @@ INSTANTIATE_TEST_SUITE_P(Feature,
                          FeaturePermuteDevices,
                          testing::ValuesIn(PermuteDevices::TestCases()));
 
+TEST_P(FeaturePermuteDevices, SelectByIndex) {
+    core::Device device = GetParam();
+
+    open3d::geometry::PointCloud pcd_legacy;
+    data::BunnyMesh bunny;
+    open3d::io::ReadPointCloud(bunny.GetPath(), pcd_legacy);
+
+    pcd_legacy.EstimateNormals();
+    // Convert to float64 to avoid precision loss.
+    const auto pcd = t::geometry::PointCloud::FromLegacy(pcd_legacy,
+                                                         core::Float64, device);
+
+    const auto fpfh = pipelines::registration::ComputeFPFHFeature(
+            pcd_legacy, geometry::KDTreeSearchParamHybrid(0.01, 100));
+    const auto fpfh_t =
+            t::pipelines::registration::ComputeFPFHFeature(pcd, 100, 0.01);
+
+    const auto selected_fpfh =
+            fpfh->SelectByIndex({53, 194, 839, 2543, 6391, 29483}, false);
+    const auto selected_indeces_t =
+            core::TensorKey::IndexTensor(core::Tensor::Init<int64_t>(
+                    {53, 194, 839, 2543, 6391, 29483}, device));
+    const auto selected_fpfh_t = fpfh_t.GetItem(selected_indeces_t);
+
+    EXPECT_TRUE(selected_fpfh_t.AllClose(
+            core::eigen_converter::EigenMatrixToTensor(selected_fpfh->data_)
+                    .T()
+                    .To(selected_fpfh_t.GetDevice(),
+                        selected_fpfh_t.GetDtype()),
+            1e-4, 1e-4));
+}
+
 TEST_P(FeaturePermuteDevices, ComputeFPFHFeature) {
     core::Device device = GetParam();
 

From 2870c69dc6544c7e970dc05b8081fab08317fb23 Mon Sep 17 00:00:00 2001
From: Nicola Loi <nicolaloi@outlook.com>
Date: Sun, 3 Nov 2024 11:30:09 +0100
Subject: [PATCH 2/2] better comments

---
 cpp/open3d/pipelines/registration/Feature.cpp | 2 +-
 cpp/pybind/pipelines/registration/feature.cpp | 3 +--
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/cpp/open3d/pipelines/registration/Feature.cpp b/cpp/open3d/pipelines/registration/Feature.cpp
index e45f5672d9d..c43adbd585f 100644
--- a/cpp/open3d/pipelines/registration/Feature.cpp
+++ b/cpp/open3d/pipelines/registration/Feature.cpp
@@ -19,7 +19,7 @@ namespace pipelines {
 namespace registration {
 
 std::shared_ptr<Feature> Feature::SelectByIndex(
-        const std::vector<size_t> &indices, bool invert) const {
+        const std::vector<size_t> &indices, bool invert /* = false */) const {
     auto output = std::make_shared<Feature>();
     output->Resize(data_.rows(), indices.size());
 
diff --git a/cpp/pybind/pipelines/registration/feature.cpp b/cpp/pybind/pipelines/registration/feature.cpp
index fd40b5b5f94..4baef5b560c 100644
--- a/cpp/pybind/pipelines/registration/feature.cpp
+++ b/cpp/pybind/pipelines/registration/feature.cpp
@@ -33,8 +33,7 @@ void pybind_feature_definitions(py::module &m_registration) {
             .def("num", &Feature::Num, "Returns number of points.")
             .def("select_by_index", &Feature::SelectByIndex,
                  "Function to select features from input Feature group into "
-                 "output "
-                 "Feature group.",
+                 "output Feature group.",
                  "indices"_a, "invert"_a = false)
             .def_readwrite("data", &Feature::data_,
                            "``dim x n`` float64 numpy array: Data buffer "