diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index 4134ee6f306..9b61f238ad6 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -174,6 +174,81 @@ 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 < 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; + std::unordered_map> + triangle_to_old_index; + std::vector triangle_vec; + + // Check if mesh has normals triangles and colors + bool has_tri_normal = HasTriangleNormals(); + 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 + 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. + 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; + triangle_to_old_index[triangle_in_tuple] = i; + } else { + find_dup = true; + mask_of_triangles[i] = false; + } + } + + // 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) { + core::Tensor tris = + GetTriangleNormals().IndexGet({mask_of_triangles}); + SetTriangleNormals(tris); + } + if (has_tri_colors) { + core::Tensor colors = + GetTriangleColors().IndexGet({mask_of_triangles}); + SetTriangleColors(colors); + } + } + + 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..4b5bbde8f74 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.h +++ b/cpp/open3d/t/geometry/TriangleMesh.h @@ -654,6 +654,10 @@ 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. TriangleMesh &NormalizeNormals(); diff --git a/cpp/pybind/t/geometry/trianglemesh.cpp b/cpp/pybind/t/geometry/trianglemesh.cpp index 5979238e1b2..0f90fbca370 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 duplicated 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..3502c80f44e 100644 --- a/cpp/tests/t/geometry/TriangleMesh.cpp +++ b/cpp/tests/t/geometry/TriangleMesh.cpp @@ -327,6 +327,90 @@ 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.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}, + {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))); + + 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) { 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())