From 98fe0622ffdfc92fa329bb9fcf271a0f79303a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20Vall=C3=A9e?= Date: Thu, 5 Oct 2023 11:48:20 +0200 Subject: [PATCH 1/5] Add RemoveDuplicatedTriangles feature using tensor API --- cpp/open3d/t/geometry/TriangleMesh.cpp | 92 +++++++++++++++++++++ cpp/open3d/t/geometry/TriangleMesh.h | 2 + cpp/pybind/t/geometry/trianglemesh.cpp | 3 + cpp/tests/t/geometry/TriangleMesh.cpp | 50 +++++++++++ python/test/t/geometry/test_trianglemesh.py | 26 ++++++ 5 files changed, 173 insertions(+) diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index 4134ee6f306..96cadc82bd5 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -174,6 +174,98 @@ TriangleMesh &TriangleMesh::Rotate(const core::Tensor &R, return *this; } +TriangleMesh &TriangleMesh::RemoveDuplicatedTriangles() { + core::Tensor triangles = GetTriangleIndices(); + int64_t num_triangles = triangles.GetLength(); + if (num_triangles < 1) { + utility::LogError("Mesh must have at least two triangles."); + } + + // unordered_map to keep track of existing triangles + // hash function with code::Tensor not implemented so we use std::tuple + typedef std::tuple Index3; + std::unordered_map> + triangle_to_old_index; + + // Check is mesh has normals triangles + bool has_tri_normal = HasTriangleNormals(); + // index of triangle to keep (unique triangle) + std::vector index_to_keep; + int64_t new_size = 0; + for (int64_t i = 0; i < num_triangles; i++) { + Index3 triangle_in_tuple; + // Extract vertices of the triangle + core::Tensor triangle_to_check = + triangles.GetItem({core::TensorKey::Index(i)}); + int64_t vertice_0 = + triangle_to_check.GetItem({core::TensorKey::Index(0)}) + .Item(); + int64_t vertice_1 = + triangle_to_check.GetItem({core::TensorKey::Index(1)}) + .Item(); + int64_t vertice_2 = + triangle_to_check.GetItem({core::TensorKey::Index(2)}) + .Item(); + // We first need to find the minimum index. Because triangle (0-1-2) + // and triangle (2-0-1) are the same. + if (vertice_0 <= vertice_1) { + if (vertice_1 <= vertice_2) + triangle_in_tuple = + std::make_tuple(vertice_0, vertice_1, vertice_2); + else + triangle_in_tuple = + std::make_tuple(vertice_0, vertice_2, vertice_1); + } else { + if (vertice_1 <= vertice_2) + triangle_in_tuple = + std::make_tuple(vertice_1, vertice_2, vertice_0); + else + triangle_in_tuple = + std::make_tuple(vertice_1, vertice_0, vertice_2); + } + // check is triangle is already present in the mesh + if (triangle_to_old_index.find(triangle_in_tuple) == + triangle_to_old_index.end()) { + triangle_to_old_index[triangle_in_tuple] = i; + index_to_keep.push_back(i); + new_size++; + } + } + + // do this only if there are some triangle to remove + if (new_size != num_triangles) { + core::Tensor triangles_without_dup = core::Tensor::Empty( + {new_size, 3}, triangles.GetDtype(), GetDevice()); + + core::Tensor triangles_normals, triangles_normals_without_dup; + + // fill only is there are normals triangle to avoid errors + if (has_tri_normal) { + triangles_normals = GetTriangleNormals(); + triangles_normals_without_dup = core::Tensor::Empty( + {new_size, 3}, triangles.GetDtype(), GetDevice()); + } + + for (int64_t i = 0; i < new_size; i++) { + core::Tensor triangle_to_keep = triangles.GetItem( + {core::TensorKey::Index(index_to_keep[i])}); + triangles_without_dup.SetItem({core::TensorKey::Index(i)}, + triangle_to_keep); + if (has_tri_normal) { + core::Tensor triangle_normal_to_keep = + triangles_normals.GetItem( + {core::TensorKey::Index(index_to_keep[i])}); + triangles_normals_without_dup.SetItem( + {core::TensorKey::Index(i)}, triangle_normal_to_keep); + } + } + + SetTriangleIndices(triangles_without_dup); + if (has_tri_normal) SetTriangleNormals(triangles_normals_without_dup); + } + return *this; +} + TriangleMesh &TriangleMesh::NormalizeNormals() { if (HasVertexNormals()) { SetVertexNormals(GetVertexNormals().Contiguous()); diff --git a/cpp/open3d/t/geometry/TriangleMesh.h b/cpp/open3d/t/geometry/TriangleMesh.h index 7828ac16b02..9aa1d815743 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.h +++ b/cpp/open3d/t/geometry/TriangleMesh.h @@ -654,6 +654,8 @@ class TriangleMesh : public Geometry, public DrawableGeometry { /// \return Rotated TriangleMesh TriangleMesh &Rotate(const core::Tensor &R, const core::Tensor ¢er); + TriangleMesh &RemoveDuplicatedTriangles(); + /// Normalize both triangle normals and vertex normals to length 1. TriangleMesh &NormalizeNormals(); diff --git a/cpp/pybind/t/geometry/trianglemesh.cpp b/cpp/pybind/t/geometry/trianglemesh.cpp index 5979238e1b2..b0438bf82d3 100644 --- a/cpp/pybind/t/geometry/trianglemesh.cpp +++ b/cpp/pybind/t/geometry/trianglemesh.cpp @@ -176,6 +176,9 @@ The attributes of the triangle mesh have different levels:: "Scale points."); triangle_mesh.def("rotate", &TriangleMesh::Rotate, "R"_a, "center"_a, "Rotate points and normals (if exist)."); + triangle_mesh.def("remove_duplicated_triangles", + &TriangleMesh::RemoveDuplicatedTriangles, + "Remove duplicate triangles from a triangle"); triangle_mesh.def( "normalize_normals", &TriangleMesh::NormalizeNormals, diff --git a/cpp/tests/t/geometry/TriangleMesh.cpp b/cpp/tests/t/geometry/TriangleMesh.cpp index 4e51b34e31d..73fdb8e5bd1 100644 --- a/cpp/tests/t/geometry/TriangleMesh.cpp +++ b/cpp/tests/t/geometry/TriangleMesh.cpp @@ -327,6 +327,56 @@ TEST_P(TriangleMeshPermuteDevices, Rotate) { core::Tensor::Init({{2, 2, 1}, {2, 2, 1}}, device))); } +TEST_P(TriangleMeshPermuteDevices, RemoveDuplicatedTriangles) { + core::Device device = GetParam(); + + t::geometry::TriangleMesh mesh(device); + + mesh.SetVertexPositions(core::Tensor::Init({{0, 0, 0}, + {1, 0, 0}, + {0, 0, 1}, + {1, 0, 1}, + {0, 1, 0}, + {1, 1, 0}, + {0, 1, 1}, + {1, 1, 1}}, + device)); + + mesh.SetTriangleIndices(core::Tensor::Init({{4, 7, 5}, + {4, 6, 7}, + {0, 2, 4}, + {2, 6, 4}, + {0, 1, 2}, + {1, 3, 2}, + {1, 5, 7}, + {1, 7, 3}, + {2, 3, 7}, + {2, 7, 6}, + {4, 6, 7}, + {0, 4, 1}, + {1, 4, 5}}, + device)); + + mesh.RemoveDuplicatedTriangles(); + + EXPECT_EQ(mesh.GetTriangleIndices().GetLength(), 12); + + EXPECT_TRUE(mesh.GetTriangleIndices().AllClose( + core::Tensor::Init({{4, 7, 5}, + {4, 6, 7}, + {0, 2, 4}, + {2, 6, 4}, + {0, 1, 2}, + {1, 3, 2}, + {1, 5, 7}, + {1, 7, 3}, + {2, 3, 7}, + {2, 7, 6}, + {0, 4, 1}, + {1, 4, 5}}, + device))); +} + TEST_P(TriangleMeshPermuteDevices, NormalizeNormals) { core::Device device = GetParam(); diff --git a/python/test/t/geometry/test_trianglemesh.py b/python/test/t/geometry/test_trianglemesh.py index 843184dd3e6..2d1694aca1c 100644 --- a/python/test/t/geometry/test_trianglemesh.py +++ b/python/test/t/geometry/test_trianglemesh.py @@ -417,3 +417,29 @@ def test_pickle(device): mesh.vertex.positions.cpu().numpy()) np.testing.assert_equal(mesh_load.triangle.indices.cpu().numpy(), mesh.triangle.indices.cpu().numpy()) + + +@pytest.mark.parametrize("device", list_devices()) +def test_remove_duplicated_triangles(device): + + mesh = o3d.t.geometry.TriangleMesh() + vertex_positions = o3c.Tensor( + [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 1.0], + [0.0, 1.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 1.0, 1.0]], + o3c.float32, device) + triangle_indices = o3c.Tensor( + [[4, 7, 5], [4, 6, 7], [0, 2, 4], [2, 6, 4], [0, 1, 2], [1, 3, 2], + [1, 5, 7], [1, 7, 3], [2, 3, 7], [2, 7, 6], [4, 6, 7], [0, 4, 1], + [1, 4, 5]], o3c.int64, device) + mesh.vertex.positions = vertex_positions + mesh.triangle.indices = triangle_indices + + mesh.remove_duplicated_triangles() + + assert mesh.triangle.indices.shape == (12, 3) + np.testing.assert_allclose( + mesh.triangle.indices.cpu().numpy(), + o3c.Tensor( + [[4, 7, 5], [4, 6, 7], [0, 2, 4], [2, 6, 4], [0, 1, 2], [1, 3, 2], + [1, 5, 7], [1, 7, 3], [2, 3, 7], [2, 7, 6], [0, 4, 1], [1, 4, 5]], + o3c.int64, device).cpu().numpy()) From 9c53c9d7e3c1336f6ac92b1d35a27880a63e8603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20Vall=C3=A9e?= Date: Thu, 5 Oct 2023 12:16:40 +0200 Subject: [PATCH 2/5] Add C++ and python documentation --- cpp/open3d/t/geometry/TriangleMesh.h | 2 ++ cpp/pybind/t/geometry/trianglemesh.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cpp/open3d/t/geometry/TriangleMesh.h b/cpp/open3d/t/geometry/TriangleMesh.h index 9aa1d815743..4b5bbde8f74 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.h +++ b/cpp/open3d/t/geometry/TriangleMesh.h @@ -654,6 +654,8 @@ class TriangleMesh : public Geometry, public DrawableGeometry { /// \return Rotated TriangleMesh TriangleMesh &Rotate(const core::Tensor &R, const core::Tensor ¢er); + /// \brief Remove duplicated triangles from the TriangleMesh + /// \return TriangleMesh without duplicated triangles TriangleMesh &RemoveDuplicatedTriangles(); /// Normalize both triangle normals and vertex normals to length 1. diff --git a/cpp/pybind/t/geometry/trianglemesh.cpp b/cpp/pybind/t/geometry/trianglemesh.cpp index b0438bf82d3..0f90fbca370 100644 --- a/cpp/pybind/t/geometry/trianglemesh.cpp +++ b/cpp/pybind/t/geometry/trianglemesh.cpp @@ -178,7 +178,7 @@ The attributes of the triangle mesh have different levels:: "Rotate points and normals (if exist)."); triangle_mesh.def("remove_duplicated_triangles", &TriangleMesh::RemoveDuplicatedTriangles, - "Remove duplicate triangles from a triangle"); + "Remove duplicated triangles from a triangle"); triangle_mesh.def( "normalize_normals", &TriangleMesh::NormalizeNormals, From a6ed51c8eafca3da5eeeca6552051505f2eb14a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20Vall=C3=A9e?= Date: Thu, 12 Oct 2023 08:12:40 +0200 Subject: [PATCH 3/5] Change check of numbers of triangles to 2 and return if the number of triangle is less than 2 --- cpp/open3d/t/geometry/TriangleMesh.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index 96cadc82bd5..f81870c74d5 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -177,8 +177,8 @@ TriangleMesh &TriangleMesh::Rotate(const core::Tensor &R, TriangleMesh &TriangleMesh::RemoveDuplicatedTriangles() { core::Tensor triangles = GetTriangleIndices(); int64_t num_triangles = triangles.GetLength(); - if (num_triangles < 1) { - utility::LogError("Mesh must have at least two triangles."); + if (num_triangles < 2) { + return *this; } // unordered_map to keep track of existing triangles From 3aa93dd50c4b97af754e69ce4ff64df9e1a556ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20Vall=C3=A9e?= Date: Thu, 12 Oct 2023 18:32:58 +0200 Subject: [PATCH 4/5] Use the SelectFacesByMask() function to remove duplicates triangles with a mask. Manage also the removing of duplicate normals triangles and colors. --- cpp/open3d/t/geometry/TriangleMesh.cpp | 59 +++++++++++--------------- cpp/tests/t/geometry/TriangleMesh.cpp | 34 +++++++++++++++ 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index f81870c74d5..ff1fc5f3929 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -187,11 +187,15 @@ TriangleMesh &TriangleMesh::RemoveDuplicatedTriangles() { std::unordered_map> triangle_to_old_index; - // Check is mesh has normals triangles + // Check if mesh has normals triangles and colors bool has_tri_normal = HasTriangleNormals(); - // index of triangle to keep (unique triangle) - std::vector index_to_keep; - int64_t new_size = 0; + bool has_tri_colors = HasTriangleColors(); + bool find_dup = false; + + // mask of triangles to keep + core::Tensor mask_of_triangles = + core::Tensor::Empty({num_triangles}, core::Bool, GetDevice()); + for (int64_t i = 0; i < num_triangles; i++) { Index3 triangle_in_tuple; // Extract vertices of the triangle @@ -223,46 +227,33 @@ TriangleMesh &TriangleMesh::RemoveDuplicatedTriangles() { triangle_in_tuple = std::make_tuple(vertice_1, vertice_0, vertice_2); } - // check is triangle is already present in the mesh + // check if triangle is already present in the mesh if (triangle_to_old_index.find(triangle_in_tuple) == triangle_to_old_index.end()) { + mask_of_triangles[i] = true; triangle_to_old_index[triangle_in_tuple] = i; - index_to_keep.push_back(i); - new_size++; + } else { + find_dup = true; + mask_of_triangles[i] = false; } } - // do this only if there are some triangle to remove - if (new_size != num_triangles) { - core::Tensor triangles_without_dup = core::Tensor::Empty( - {new_size, 3}, triangles.GetDtype(), GetDevice()); - - core::Tensor triangles_normals, triangles_normals_without_dup; - - // fill only is there are normals triangle to avoid errors + // do this only if there are some triangles to remove + if (find_dup) { + TriangleMesh mesh_without_dup = SelectFacesByMask(mask_of_triangles); + SetTriangleIndices(mesh_without_dup.GetTriangleIndices()); if (has_tri_normal) { - triangles_normals = GetTriangleNormals(); - triangles_normals_without_dup = core::Tensor::Empty( - {new_size, 3}, triangles.GetDtype(), GetDevice()); + core::Tensor tris = + GetTriangleNormals().IndexGet({mask_of_triangles}); + SetTriangleNormals(tris); } - - for (int64_t i = 0; i < new_size; i++) { - core::Tensor triangle_to_keep = triangles.GetItem( - {core::TensorKey::Index(index_to_keep[i])}); - triangles_without_dup.SetItem({core::TensorKey::Index(i)}, - triangle_to_keep); - if (has_tri_normal) { - core::Tensor triangle_normal_to_keep = - triangles_normals.GetItem( - {core::TensorKey::Index(index_to_keep[i])}); - triangles_normals_without_dup.SetItem( - {core::TensorKey::Index(i)}, triangle_normal_to_keep); - } + if (has_tri_colors) { + core::Tensor colors = + GetTriangleColors().IndexGet({mask_of_triangles}); + SetTriangleColors(colors); } - - SetTriangleIndices(triangles_without_dup); - if (has_tri_normal) SetTriangleNormals(triangles_normals_without_dup); } + return *this; } diff --git a/cpp/tests/t/geometry/TriangleMesh.cpp b/cpp/tests/t/geometry/TriangleMesh.cpp index 73fdb8e5bd1..3502c80f44e 100644 --- a/cpp/tests/t/geometry/TriangleMesh.cpp +++ b/cpp/tests/t/geometry/TriangleMesh.cpp @@ -357,9 +357,28 @@ TEST_P(TriangleMeshPermuteDevices, RemoveDuplicatedTriangles) { {1, 4, 5}}, device)); + mesh.SetTriangleNormals(core::Tensor::Init({{4, 7, 5}, + {4, 6, 7}, + {0, 2, 4}, + {2, 6, 4}, + {0, 1, 2}, + {1, 3, 2}, + {1, 5, 7}, + {1, 7, 3}, + {2, 3, 7}, + {2, 7, 6}, + {4, 6, 7}, + {0, 4, 1}, + {1, 4, 5}}, + device)); + + mesh.SetTriangleColors(core::Tensor::Ones({13, 3}, core::Float32, device)); + mesh.RemoveDuplicatedTriangles(); EXPECT_EQ(mesh.GetTriangleIndices().GetLength(), 12); + EXPECT_EQ(mesh.GetTriangleNormals().GetLength(), 12); + EXPECT_EQ(mesh.GetTriangleColors().GetLength(), 12); EXPECT_TRUE(mesh.GetTriangleIndices().AllClose( core::Tensor::Init({{4, 7, 5}, @@ -375,6 +394,21 @@ TEST_P(TriangleMeshPermuteDevices, RemoveDuplicatedTriangles) { {0, 4, 1}, {1, 4, 5}}, device))); + + EXPECT_TRUE(mesh.GetTriangleNormals().AllClose( + core::Tensor::Init({{4, 7, 5}, + {4, 6, 7}, + {0, 2, 4}, + {2, 6, 4}, + {0, 1, 2}, + {1, 3, 2}, + {1, 5, 7}, + {1, 7, 3}, + {2, 3, 7}, + {2, 7, 6}, + {0, 4, 1}, + {1, 4, 5}}, + device))); } TEST_P(TriangleMeshPermuteDevices, NormalizeNormals) { From c890dc81096ad395074fad5d02cc813c7cd789bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20Vall=C3=A9e?= Date: Wed, 25 Oct 2023 17:24:03 +0200 Subject: [PATCH 5/5] use the operator[] and sort instead of multiples if --- cpp/open3d/t/geometry/TriangleMesh.cpp | 50 +++++++++++--------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index ff1fc5f3929..9b61f238ad6 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -180,12 +180,12 @@ TriangleMesh &TriangleMesh::RemoveDuplicatedTriangles() { if (num_triangles < 2) { return *this; } - // unordered_map to keep track of existing triangles // hash function with code::Tensor not implemented so we use std::tuple - typedef std::tuple Index3; + typedef std::tuple Index3; std::unordered_map> triangle_to_old_index; + std::vector triangle_vec; // Check if mesh has normals triangles and colors bool has_tri_normal = HasTriangleNormals(); @@ -199,35 +199,27 @@ TriangleMesh &TriangleMesh::RemoveDuplicatedTriangles() { for (int64_t i = 0; i < num_triangles; i++) { Index3 triangle_in_tuple; // Extract vertices of the triangle - core::Tensor triangle_to_check = - triangles.GetItem({core::TensorKey::Index(i)}); - int64_t vertice_0 = - triangle_to_check.GetItem({core::TensorKey::Index(0)}) - .Item(); - int64_t vertice_1 = - triangle_to_check.GetItem({core::TensorKey::Index(1)}) - .Item(); - int64_t vertice_2 = - triangle_to_check.GetItem({core::TensorKey::Index(2)}) - .Item(); + core::Tensor triangle_to_check = triangles[i]; + + if (triangles.GetDtype() == core::Int32) { + auto vertice_0 = triangle_to_check[0].Item(); + auto vertice_1 = triangle_to_check[1].Item(); + auto vertice_2 = triangle_to_check[2].Item(); + triangle_vec = {vertice_0, vertice_1, vertice_2}; + } else if (triangles.GetDtype() == core::Int64) { + auto vertice_0 = triangle_to_check[0].Item(); + auto vertice_1 = triangle_to_check[1].Item(); + auto vertice_2 = triangle_to_check[2].Item(); + triangle_vec = {vertice_0, vertice_1, vertice_2}; + } + // We first need to find the minimum index. Because triangle (0-1-2) // and triangle (2-0-1) are the same. - if (vertice_0 <= vertice_1) { - if (vertice_1 <= vertice_2) - triangle_in_tuple = - std::make_tuple(vertice_0, vertice_1, vertice_2); - else - triangle_in_tuple = - std::make_tuple(vertice_0, vertice_2, vertice_1); - } else { - if (vertice_1 <= vertice_2) - triangle_in_tuple = - std::make_tuple(vertice_1, vertice_2, vertice_0); - else - triangle_in_tuple = - std::make_tuple(vertice_1, vertice_0, vertice_2); - } - // check if triangle is already present in the mesh + std::sort(triangle_vec.begin(), triangle_vec.end()); + triangle_in_tuple = std::make_tuple(triangle_vec[0], triangle_vec[1], + triangle_vec[2]); + + // check is triangle is already present in the mesh if (triangle_to_old_index.find(triangle_in_tuple) == triangle_to_old_index.end()) { mask_of_triangles[i] = true;