diff --git a/cpp/open3d/t/geometry/PointCloud.cpp b/cpp/open3d/t/geometry/PointCloud.cpp index 30b5dd95929..032a13d3437 100644 --- a/cpp/open3d/t/geometry/PointCloud.cpp +++ b/cpp/open3d/t/geometry/PointCloud.cpp @@ -1335,6 +1335,9 @@ int PointCloud::PCAPartition(int max_points) { core::Tensor PointCloud::ComputeMetrics(const PointCloud &pcd2, std::vector metrics, MetricParameters params) const { + if (IsEmpty() || pcd2.IsEmpty()) { + utility::LogError("One or both input point clouds are empty!"); + } if (!IsCPU() || !pcd2.IsCPU()) { utility::LogWarning( "ComputeDistance is implemented only on CPU. Computing on " @@ -1342,7 +1345,8 @@ core::Tensor PointCloud::ComputeMetrics(const PointCloud &pcd2, } core::Tensor points1 = GetPointPositions().To(core::Device("CPU:0")), points2 = pcd2.GetPointPositions().To(core::Device("CPU:0")); - core::Tensor indices12, distance12, indices21, distance21; + [[maybe_unused]] core::Tensor indices12, indices21; + core::Tensor sqr_distance12, sqr_distance21; core::nns::NearestNeighborSearch tree1(points1); core::nns::NearestNeighborSearch tree2(points2); @@ -1354,10 +1358,11 @@ core::Tensor PointCloud::ComputeMetrics(const PointCloud &pcd2, utility::LogError("[ComputeDistance] Building KNN-Index failed!"); } - std::tie(indices12, distance12) = tree2.KnnSearch(points1, 1); - std::tie(indices21, distance21) = tree2.KnnSearch(points2, 1); + std::tie(indices12, sqr_distance12) = tree2.KnnSearch(points1, 1); + std::tie(indices21, sqr_distance21) = tree1.KnnSearch(points2, 1); - return ComputeMetricsCommon(distance12, distance21, metrics, params); + return ComputeMetricsCommon(sqr_distance12.Sqrt_(), sqr_distance21.Sqrt_(), + metrics, params); } } // namespace geometry diff --git a/cpp/open3d/t/geometry/PointCloud.h b/cpp/open3d/t/geometry/PointCloud.h index eb1f6942c91..4cc0ba87ff7 100644 --- a/cpp/open3d/t/geometry/PointCloud.h +++ b/cpp/open3d/t/geometry/PointCloud.h @@ -712,17 +712,17 @@ class PointCloud : public Geometry, public DrawableGeometry { /// \f{eqnarray*}{ /// \text{Chamfer Distance: } d_{CD}(X,Y) &=& \frac{1}{|X|}\sum_{i \in X} - /// || x_i - n(x_i, Y) || + - /// \frac{1}{|Y|}\sum_{i \in Y} || y_i - n(y_i, X) || \\ + /// || x_i - n(x_i, Y) || + \frac{1}{|Y|}\sum_{i \in Y} || y_i - n(y_i, X) + /// || \\{} /// \text{Hausdorff distance: } d_H(X,Y) &=& \max \left\{ \max_{i \in X} /// || x_i - n(x_i, Y) ||, \max_{i \in Y} || y_i - n(y_i, X) || \right\} - /// \\ + /// \\{} /// \text{Precision: } P(X,Y|d) &=& \frac{100}{|X|} \sum_{i \in X} || x_i - /// - n(x_i, Y) || < d \\ + /// - n(x_i, Y) || < d \\{} /// \text{Recall: } R(X,Y|d) &=& \frac{100}{|Y|} \sum_{i \in Y} || y_i - - /// n(y_i, X) || < d \\ + /// n(y_i, X) || < d \\{} /// \text{F-Score: } F(X,Y|d) &=& \frac{2 P(X,Y|d) R(X,Y|d)}{P(X,Y|d) + - /// R(X,Y|d)} \\ + /// R(X,Y|d)} /// \f} /// \param pcd2 Other point cloud to compare with. diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index d02483f27d1..05e8cc7b66f 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -1594,6 +1594,9 @@ PointCloud TriangleMesh::SamplePointsUniformly( core::Tensor TriangleMesh::ComputeMetrics(const TriangleMesh &mesh2, std::vector metrics, MetricParameters params) const { + if (IsEmpty() || mesh2.IsEmpty()) { + utility::LogError("One or both input triangle meshes are empty!"); + } if (!IsCPU() || !mesh2.IsCPU()) { utility::LogWarning( "ComputeDistance is implemented only on CPU. Computing on " diff --git a/cpp/open3d/t/geometry/TriangleMesh.h b/cpp/open3d/t/geometry/TriangleMesh.h index f6681f222db..6dd228aab3e 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.h +++ b/cpp/open3d/t/geometry/TriangleMesh.h @@ -1036,17 +1036,17 @@ class TriangleMesh : public Geometry, public DrawableGeometry { /// \f{eqnarray*}{ /// \text{Chamfer Distance: } d_{CD}(X,Y) &=& \frac{1}{|X|}\sum_{i \in X} - /// || x_i - n(x_i, Y) || + - /// \frac{1}{|Y|}\sum_{i \in Y} || y_i - n(y_i, X) || \\ + /// || x_i - n(x_i, Y) || + \frac{1}{|Y|}\sum_{i \in Y} || y_i - n(y_i, X) + /// || \\{} /// \text{Hausdorff distance: } d_H(X,Y) &=& \max \left\{ \max_{i \in X} /// || x_i - n(x_i, Y) ||, \max_{i \in Y} || y_i - n(y_i, X) || \right\} - /// \\ + /// \\{} /// \text{Precision: } P(X,Y|d) &=& \frac{100}{|X|} \sum_{i \in X} || x_i - /// - n(x_i, Y) || < d \\ + /// - n(x_i, Y) || < d \\{} /// \text{Recall: } R(X,Y|d) &=& \frac{100}{|Y|} \sum_{i \in Y} || y_i - - /// n(y_i, X) || < d \\ + /// n(y_i, X) || < d \\{} /// \text{F-Score: } F(X,Y|d) &=& \frac{2 P(X,Y|d) R(X,Y|d)}{P(X,Y|d) + - /// R(X,Y|d)} \\ + /// R(X,Y|d)} /// \f} /// As a side effect, the triangle areas are saved in the "areas" attribute. diff --git a/cpp/open3d/t/geometry/kernel/Metrics.cpp b/cpp/open3d/t/geometry/kernel/Metrics.cpp index adf06e3b100..7926d71738c 100644 --- a/cpp/open3d/t/geometry/kernel/Metrics.cpp +++ b/cpp/open3d/t/geometry/kernel/Metrics.cpp @@ -30,14 +30,14 @@ core::Tensor ComputeMetricsCommon(core::Tensor distance12, for (Metric metric : metrics) { switch (metric) { case Metric::ChamferDistance: - metric_val = (distance21.Reshape({-1}).Mean({0}).Item() + - distance12.Reshape({-1}).Mean({0}).Item()); + metric_val = distance21.Reshape({-1}).Mean({-1}).Item() + + distance12.Reshape({-1}).Mean({-1}).Item(); metric_values[idx++] = metric_val; break; case Metric::HausdorffDistance: metric_val = std::max( - distance12.Reshape({-1}).Max({0}).Item(), - distance21.Reshape({-1}).Max({0}).Item()); + distance12.Reshape({-1}).Max({-1}).Item(), + distance21.Reshape({-1}).Max({-1}).Item()); metric_values[idx++] = metric_val; break; case Metric::FScore: diff --git a/cpp/open3d/t/geometry/kernel/Metrics.h b/cpp/open3d/t/geometry/kernel/Metrics.h index 23c461807a4..e64a7883ff5 100644 --- a/cpp/open3d/t/geometry/kernel/Metrics.h +++ b/cpp/open3d/t/geometry/kernel/Metrics.h @@ -13,10 +13,13 @@ namespace open3d { namespace t { namespace geometry { +/// Common code for computing geometry metrics from pairwise point distances. +/// This function expects Euclidean distances as input and returns the requested +/// metrics between point clouds / meshes. core::Tensor ComputeMetricsCommon(core::Tensor distance12, core::Tensor distance21, std::vector metrics, MetricParameters params); -} +} // namespace geometry } // namespace t } // namespace open3d diff --git a/cpp/pybind/t/geometry/pointcloud.cpp b/cpp/pybind/t/geometry/pointcloud.cpp index c36f2063666..10a5735389c 100644 --- a/cpp/pybind/t/geometry/pointcloud.cpp +++ b/cpp/pybind/t/geometry/pointcloud.cpp @@ -787,7 +787,30 @@ the partition id for each point. params (t.geometry.MetricParameters): This holds parameters required by different metrics. Returns: - Tensor containing the requested metrics.)"); + Tensor containing the requested metrics. + +Example:: + + from open3d.t.geometry import TriangleMesh, PointCloud, Metric, MetricParameters + # box is a cube with one vertex at the origin and a side length 1 + pos = TriangleMesh.create_box().vertex.positions + pcd1 = PointCloud(pos.clone()) + pcd2 = PointCloud(pos * 1.1) + + # (1, 3, 3, 1) vertices are shifted by (0, 0.1, 0.1*sqrt(2), 0.1*sqrt(3)) + # respectively + metric_params = MetricParameters( + fscore_radius=o3d.utility.FloatVector((0.01, 0.11, 0.15, 0.18))) + metrics = pcd1.compute_metrics( + pcd2, (Metric.ChamferDistance, Metric.HausdorffDistance, Metric.FScore), + metric_params) + + print(metrics) + np.testing.assert_allclose( + metrics.cpu().numpy(), + (0.22436734, np.sqrt(3) / 10, 100. / 8, 400. / 8, 700. / 8, 100.), + rtol=1e-6) + )"); } } // namespace geometry diff --git a/cpp/pybind/t/geometry/trianglemesh.cpp b/cpp/pybind/t/geometry/trianglemesh.cpp index d0c6d395486..e0c108f04a0 100644 --- a/cpp/pybind/t/geometry/trianglemesh.cpp +++ b/cpp/pybind/t/geometry/trianglemesh.cpp @@ -1113,7 +1113,27 @@ As a side effect, the triangle areas are saved in the "areas" attribute. params (t.geometry.MetricParameters): This holds parameters required by different metrics. Returns: - Tensor containing the requested metrics.)"); + Tensor containing the requested metrics. + +Example:: + + from open3d.t.geometry import TriangleMesh, Metric, MetricParameters + # box is a cube with one vertex at the origin and a side length 1 + box1 = TriangleMesh.create_box() + box2 = TriangleMesh.create_box() + box2.vertex.positions *= 1.1 + + # 3 faces of the cube are the same, and 3 are shifted up by 0.1 + metric_params = MetricParameters(fscore_radius=o3d.utility.FloatVector( + (0.05, 0.15)), n_sampled_points=100000) + metrics = box1.compute_metrics( + box2, (Metric.ChamferDistance, Metric.HausdorffDistance, Metric.FScore), + metric_params) + + print(metrics) + np.testing.assert_allclose(metrics.cpu().numpy(), (0.1, 0.17, 50, 100), + rtol=0.05) + )"); } } // namespace geometry diff --git a/python/test/t/geometry/test_pointcloud.py b/python/test/t/geometry/test_pointcloud.py index 08eb1e550e0..ac02aa860da 100644 --- a/python/test/t/geometry/test_pointcloud.py +++ b/python/test/t/geometry/test_pointcloud.py @@ -197,18 +197,22 @@ def test_pickle(device): def test_metrics(): - from open3d.t.geometry import Metric, MetricParameters - pos = o3d.t.geometry.TriangleMesh.create_box().vertex.positions - pcd1 = o3d.t.geometry.PointCloud(pos.clone()) - pcd2 = o3d.t.geometry.PointCloud(pos * 1.2) - + from open3d.t.geometry import TriangleMesh, PointCloud, Metric, MetricParameters + # box is a cube with one vertex at the origin and a side length 1 + pos = TriangleMesh.create_box().vertex.positions + pcd1 = PointCloud(pos.clone()) + pcd2 = PointCloud(pos * 1.1) + + # (1, 3, 3, 1) vertices are shifted by (0, 0.1, 0.1*sqrt(2), 0.1*sqrt(3)) + # respectively metric_params = MetricParameters( - fscore_radius=o3d.utility.FloatVector((0.05, 0.15))) + fscore_radius=o3d.utility.FloatVector((0.01, 0.11, 0.15, 0.18))) metrics = pcd1.compute_metrics( pcd2, (Metric.ChamferDistance, Metric.HausdorffDistance, Metric.FScore), metric_params) print(metrics) - np.testing.assert_allclose(metrics.cpu().numpy(), - (0.06, 0.12, 200. / 3, 100), - rtol=1e-6) + np.testing.assert_allclose( + metrics.cpu().numpy(), + (0.22436734, np.sqrt(3) / 10, 100. / 8, 400. / 8, 700. / 8, 100.), + rtol=1e-6) diff --git a/python/test/t/geometry/test_trianglemesh.py b/python/test/t/geometry/test_trianglemesh.py index 31b193542fe..91b130dfeae 100644 --- a/python/test/t/geometry/test_trianglemesh.py +++ b/python/test/t/geometry/test_trianglemesh.py @@ -1396,18 +1396,22 @@ def test_remove_non_manifold_edges(device, int_t, float_t): def test_metrics(): - from open3d.t.geometry import Metric, MetricParameters - box1 = o3d.t.geometry.TriangleMesh.create_box() - box2 = o3d.t.geometry.TriangleMesh.create_box() - box2.vertex.positions *= 1.2 - + from open3d.t.geometry import TriangleMesh, Metric, MetricParameters + # box is a cube with one vertex at the origin and a side length 1 + box1 = TriangleMesh.create_box() + box2 = TriangleMesh.create_box() + box2.vertex.positions *= 1.1 + + # 3 faces of the cube are the same, and 3 are shifted up by 0.1 - raycast + # distances should follow this. metric_params = MetricParameters(fscore_radius=o3d.utility.FloatVector( (0.05, 0.15)), n_sampled_points=100000) + # n_sampled_points=100000) metrics = box1.compute_metrics( box2, (Metric.ChamferDistance, Metric.HausdorffDistance, Metric.FScore), metric_params) print(metrics) - np.testing.assert_allclose(metrics.cpu().numpy(), (0.2, 0.34, 45, 53.2), + np.testing.assert_allclose(metrics.cpu().numpy(), (0.1, 0.17, 50, 100), rtol=0.05)