From 4535bb2481125f4840bd10f7495e86afdd1cabab Mon Sep 17 00:00:00 2001 From: "Ganjugunte, Shashidhara Krishnamurthy" Date: Thu, 5 Oct 2023 22:57:56 -0700 Subject: [PATCH 1/4] RemoveDuplicateVertices Implementation for TriangleMesh. Functionality to remove duplicate vertices and update other vertex attributes and triangle indices is implemented. The C++ and python tests are also written to test the code. On branch sganjugu/remdup2 Changes to be committed: modified: cpp/open3d/t/geometry/TriangleMesh.cpp modified: cpp/open3d/t/geometry/TriangleMesh.h modified: cpp/pybind/t/geometry/trianglemesh.cpp modified: cpp/tests/t/geometry/TriangleMesh.cpp modified: python/test/t/geometry/test_trianglemesh.py --- cpp/open3d/t/geometry/TriangleMesh.cpp | 182 +++++++++ cpp/open3d/t/geometry/TriangleMesh.h | 6 + cpp/pybind/t/geometry/trianglemesh.cpp | 4 + cpp/tests/t/geometry/TriangleMesh.cpp | 51 +++ python/test/t/geometry/test_trianglemesh.py | 420 ++++++++++++++++++++ 5 files changed, 663 insertions(+) diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index 4134ee6f306..3de1bc90783 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -1061,6 +1061,188 @@ TriangleMesh TriangleMesh::SelectFacesByMask(const core::Tensor &mask) const { return result; } +/// +/// This function shrinks a vertex attribute tensor to have length new_size. +/// This function is used in RemoveDuplicateVerticesWorker. +/// Assumptions: +/// 1. The new_size is the number of unique vertices in the mesh. +/// 2. The attribute tensor has been updated during duplicate removal in such a way that +/// attributes corresponding to unique vertices are moved to the beginning and the rest +/// can be discarded. +/// \param attrib The attribute whose tensor has to be shrunk. +/// \param mesh The mesh from which duplicates have to be removed. +/// \param new_size The size to shrink the attribute tensor to. +void ShrinkVertexAttributeTensor(const std::string &attrib, TriangleMesh &mesh, int64_t new_size) +{ + auto old_tensor = mesh.GetVertexAttr(attrib); + std::vector old_shape_vec(old_tensor.GetShape()); + assert (new_size <= old_shape_vec[0]); + old_shape_vec[0] = new_size; + + auto new_shape = core::SizeVector(old_shape_vec); + core::Tensor new_tensor = core::Tensor(new_shape, old_tensor.GetDtype(), old_tensor.GetDevice()); + for(decltype(new_size) i = 0; i < new_size; ++i) { + new_tensor[i] = old_tensor[i]; + } + mesh.SetVertexAttr(attrib, new_tensor); +} + + +/// This function is used in RemoveDuplicateVerticesWorker to update +/// the triangle indices once a mapping from old indices to newer ones is computed. +/// \param indices_ptr The triangle indices pointer computed for appropriate datatype from mesh.triangle.indices. +/// \param index_old_to_new Map from old indices to new indices. +/// \param mesh The mesh on which the new index mapping for triangles is to be computed +template +void RemapTriangleIndices(T *indices_ptr, std::vector &index_old_to_new, TriangleMesh &mesh) +{ + auto triangles = mesh.GetTriangleIndices(); + auto nIndices = triangles.GetLength(); + std::vector flat_indices(nIndices * 3); + for(Length_t tI = 0; tI < nIndices; ++tI) { + auto curI = static_cast(tI * 3); + flat_indices[curI] = index_old_to_new[indices_ptr[curI]]; + curI++; + flat_indices[curI] = index_old_to_new[indices_ptr[curI]]; + curI++; + flat_indices[curI] = index_old_to_new[indices_ptr[curI]]; + } + core::Tensor new_indices = core::Tensor(flat_indices, triangles.GetShape(), + triangles.GetDtype(), triangles.GetDevice()); + mesh.SetTriangleIndices(new_indices); +} + + +/// This is a templatized local version of RemoveDuplicates. +/// Use of templates allows us to use common code for different +/// datatype possibilities for vertex.positions, namely, float32, float64, +/// and triangle.indices, namely, int32, int64. +/// This function first computes two maps +/// (MapA.) From vertex coordinates to their indices. +// This is implemented using unordered_map. +/// (MapB.) From the vertex indices before duplicate removal to indices after removal. +/// This is implemented using std::vector. +/// +/// MapA allows the function to detect duplicates and MapB allows it update triangle indices. +/// During the computation of the above maps unique vertex attributes including positions +/// are moved to beginning of the corresponding attribute tensor. +/// +/// Caveats: +/// The unordered_map approach is fast in general but if the inputs are close, the +/// hashing could cause problems and it might be good to consider a sorting based algorithms, +/// which can do a better job of comparing the double values. +/// \param mesh The mesh from which duplicate vertices are to be removed. +template +TriangleMesh& RemoveDuplicateVerticesWorker(TriangleMesh &mesh) +{ + //Algorithm based on Eigen implementation + if(!mesh.HasVertexPositions()) { + utility::LogWarning("TriangeMesh::RemoveDuplicateVertices: No vertices present, ignoring."); + return mesh; + } + + typedef std::tuple Point3d; + std::unordered_map> + point_to_old_index; + + auto vertices = mesh.GetVertexPositions(); + auto triangles = mesh.GetTriangleIndices(); + + //Create a vector of pointers to attribute tensors. + std::vector vertexAttrs; + std::vector vertexAttrNames; + std::vector hasVertexAttrs; + for(auto item: mesh.GetVertexAttr()) { + vertexAttrNames.push_back(item.first); + vertexAttrs.push_back(&mesh.GetVertexAttr(item.first)); + hasVertexAttrs.push_back(mesh.HasVertexAttr(item.first)); + } + + auto old_vertex_num = vertices.GetLength(); + using Length_t = decltype(old_vertex_num); + std::vector index_old_to_new(old_vertex_num); + + //REM_DUP_VERT_STEP 1: + // Compute map from points to old indices, and use a counter + // to compute a map from old indices to new unique indices. + const Coord_t *vertices_ptr = vertices.GetDataPtr(); + Length_t k = 0; // new index + for (Length_t i = 0; i < old_vertex_num; i++) { // old index + Point3d coord = std::make_tuple(vertices_ptr[i * 3], vertices_ptr[i * 3 + 1], + vertices_ptr[i * 3 + 2]); + if (point_to_old_index.find(coord) == point_to_old_index.end()) { + point_to_old_index[coord] = i; + index_old_to_new[i] = k; + //Update attributes, including positions + for(size_t j = 0; j < vertexAttrs.size(); ++j) { + if (!hasVertexAttrs[j]) continue; + auto &vattr = *vertexAttrs[j]; + vattr[k] = vattr[i]; + } + k++; + } else { + index_old_to_new[i] = index_old_to_new[point_to_old_index[coord]]; + } + } + //REM_DUP_VERT_STEP 2: + // Shrink all the vertex attribute tensors to size equal to number of unique vertices. + for(size_t j = 0; j < vertexAttrNames.size(); ++j) { + ShrinkVertexAttributeTensor(vertexAttrNames[j], mesh, k); + } + + //REM_DUP_VERT_STEP 3: + // Remap triangle indices to new unique vertex indices. + if (k < old_vertex_num) { + //Update triangle indices. + Tindex_t *indices_ptr = triangles.GetDataPtr(); + RemapTriangleIndices(indices_ptr, index_old_to_new, mesh); + } + utility::LogDebug( + "[RemoveDuplicatedVertices] {:d} vertices have been removed.", + (int)(old_vertex_num - k)); + + return mesh; +} + + +/// +/// Remove duplicate vertices from a mesh and return the resulting mesh. +/// +TriangleMesh& TriangleMesh::RemoveDuplicateVertices() +{ + /// This function mainly does some checks and then calls the RemoveDuplicateVerticesWorker + /// with appropriate data type. + if(!HasVertexPositions()) { + utility::LogWarning("TriangeMesh::RemoveDuplicateVertices: No vertices present, ignoring."); + return *this; + } + auto vertices = GetVertexPositions(); + auto triangles = GetTriangleIndices(); + if (core::Int32 != triangles.GetDtype() && core::Int64 != triangles.GetDtype()) { + utility::LogError("Only Int32 or Int64 are supported for triangle indices"); + return *this; + } + if (core::Float32 != vertices.GetDtype() && core::Float64 != vertices.GetDtype()) { + utility::LogError("Only Float32 or Float64 is supported for vertex coordinates"); + return *this; + } + + if(core::Float32 == vertices.GetDtype()) { + if (core::Int32 == triangles.GetDtype()) { + return RemoveDuplicateVerticesWorker(*this); + } else { + return RemoveDuplicateVerticesWorker(*this); + } + } else { + if (core::Int32 == triangles.GetDtype()) { + return RemoveDuplicateVerticesWorker(*this); + } else { + return RemoveDuplicateVerticesWorker(*this); + } + } +} + + } // namespace geometry } // namespace t } // namespace open3d diff --git a/cpp/open3d/t/geometry/TriangleMesh.h b/cpp/open3d/t/geometry/TriangleMesh.h index 7828ac16b02..2a92292823b 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.h +++ b/cpp/open3d/t/geometry/TriangleMesh.h @@ -930,6 +930,12 @@ class TriangleMesh : public Geometry, public DrawableGeometry { /// \return A new mesh with the selected faces. TriangleMesh SelectFacesByMask(const core::Tensor &mask) const; + + /// Removes duplicate vertices and their associated attributes. + /// It also updates the triangle indices to refer to the reduced array of vertices. + /// \return Mesh with the duplicate vertices removed. + TriangleMesh& RemoveDuplicateVertices(); + protected: core::Device device_ = core::Device("CPU:0"); TensorMap vertex_attr_; diff --git a/cpp/pybind/t/geometry/trianglemesh.cpp b/cpp/pybind/t/geometry/trianglemesh.cpp index 5979238e1b2..3deb051a096 100644 --- a/cpp/pybind/t/geometry/trianglemesh.cpp +++ b/cpp/pybind/t/geometry/trianglemesh.cpp @@ -891,6 +891,10 @@ the partition id for each face. print(np.unique(mesh.triangle.partition_ids.numpy(), return_counts=True)) )"); + triangle_mesh.def( + "remove_duplicate_vertices", &TriangleMesh::RemoveDuplicateVertices, + "Removes duplicate vertices from vertex attribute" + "'positions' and updates other vertex attributes and triangle indices."); triangle_mesh.def( "select_faces_by_mask", &TriangleMesh::SelectFacesByMask, "mask"_a, diff --git a/cpp/tests/t/geometry/TriangleMesh.cpp b/cpp/tests/t/geometry/TriangleMesh.cpp index 4e51b34e31d..1a4682e1901 100644 --- a/cpp/tests/t/geometry/TriangleMesh.cpp +++ b/cpp/tests/t/geometry/TriangleMesh.cpp @@ -260,6 +260,57 @@ TEST_P(TriangleMeshPermuteDevices, Transform) { core::Tensor::Init({{2, 2, 1}, {2, 2, 1}}, device))); } +TEST_P(TriangleMeshPermuteDevices, RemoveDuplicateVertices) { + core::Device device = GetParam(); + t::geometry::TriangleMesh mesh(device); + mesh.SetVertexPositions( + core::Tensor::Init({{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, 0.0, 0.0}, + {1.0, 1.0, 1.0}}, device)); + mesh.SetTriangleIndices( + core::Tensor::Init({{4, 8, 5}, + {4, 6, 8}, + {0, 2, 4}, + {2, 6, 4}, + {0, 1, 2}, + {1, 3, 2}, + {7, 5, 8}, + {7, 8, 3}, + {2, 3, 8}, + {2, 8, 6}, + {0, 4, 1}, + {1, 4, 5}}, device)); + mesh.RemoveDuplicateVertices(); + EXPECT_TRUE(mesh.GetVertexPositions().AllClose( + core::Tensor::Init({{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}}))); + 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}}))); +} + TEST_P(TriangleMeshPermuteDevices, Translate) { core::Device device = GetParam(); diff --git a/python/test/t/geometry/test_trianglemesh.py b/python/test/t/geometry/test_trianglemesh.py index 843184dd3e6..2cb4cb3af72 100644 --- a/python/test/t/geometry/test_trianglemesh.py +++ b/python/test/t/geometry/test_trianglemesh.py @@ -14,6 +14,8 @@ import sys import os +from copy import deepcopy +from typing import List sys.path.append(os.path.dirname(os.path.realpath(__file__)) + "/../..") from open3d_test import list_devices @@ -403,6 +405,424 @@ def test_extrude_linear(): assert ans.triangle.indices.shape == (8, 3) +@pytest.mark.parametrize("device", list_devices()) +def test_remove_duplicate_vertices1(device): + """Test Remove duplicates works for float32 coords and int32 indices""" + dtype_g = o3d.core.float32 + dtype_i = o3d.core.int32 + mesh = o3d.t.geometry.TriangleMesh(device) + mesh.vertex.positions = o3d.core.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, 0.0, 0.0], + [1.0, 1.0, 1.0]], dtype=dtype_g) + mesh.triangle.indices = o3d.core.Tensor([[4, 8, 5], + [4, 6, 8], + [0, 2, 4], + [2, 6, 4], + [0, 1, 2], + [1, 3, 2], + [7, 5, 8], + [7, 8, 3], + [2, 3, 8], + [2, 8, 6], + [0, 4, 1], + [1, 4, 5]], dtype=dtype_i) + mesh.remove_duplicate_vertices() + expected_pos = o3d.core.Tensor([[0., 0., 0.], + [1., 0., 0.], + [0., 0., 1.], + [1., 0., 1.], + [0., 1., 0.], + [1., 1., 0.], + [0., 1., 1.], + [1., 1., 1.]], dtype=dtype_g) + expected_ind = o3d.core.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]], dtype=dtype_i) + + assert(mesh.vertex.positions.allclose(expected_pos)) + assert(mesh.triangle.indices.allclose(expected_ind)) + + +@pytest.mark.parametrize("device", list_devices()) +def test_remove_duplicate_vertices2(device): + """Test Remove duplicates works for float32 coords and int64 indices""" + dtype_g = o3d.core.float32 + dtype_i = o3d.core.int64 + mesh = o3d.t.geometry.TriangleMesh(device) + mesh.vertex.positions = o3d.core.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, 0.0, 0.0], + [1.0, 1.0, 1.0]], dtype=dtype_g) + mesh.triangle.indices = o3d.core.Tensor([[4, 8, 5], + [4, 6, 8], + [0, 2, 4], + [2, 6, 4], + [0, 1, 2], + [1, 3, 2], + [7, 5, 8], + [7, 8, 3], + [2, 3, 8], + [2, 8, 6], + [0, 4, 1], + [1, 4, 5]], dtype=dtype_i) + mesh.remove_duplicate_vertices() + expected_pos = o3d.core.Tensor([[0., 0., 0.], + [1., 0., 0.], + [0., 0., 1.], + [1., 0., 1.], + [0., 1., 0.], + [1., 1., 0.], + [0., 1., 1.], + [1., 1., 1.]], dtype=dtype_g) + expected_ind = o3d.core.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]], dtype=dtype_i) + + assert(mesh.vertex.positions.allclose(expected_pos)) + assert(mesh.triangle.indices.allclose(expected_ind)) + + +@pytest.mark.parametrize("device", list_devices()) +def test_remove_duplicate_vertices3(device): + """Test Remove duplicates works for float64 coords and int32 indices""" + dtype_g = o3d.core.float64 + dtype_i = o3d.core.int32 + mesh = o3d.t.geometry.TriangleMesh(device) + mesh.vertex.positions = o3d.core.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, 0.0, 0.0], + [1.0, 1.0, 1.0]], dtype=dtype_g) + mesh.triangle.indices = o3d.core.Tensor([[4, 8, 5], + [4, 6, 8], + [0, 2, 4], + [2, 6, 4], + [0, 1, 2], + [1, 3, 2], + [7, 5, 8], + [7, 8, 3], + [2, 3, 8], + [2, 8, 6], + [0, 4, 1], + [1, 4, 5]], dtype=dtype_i) + mesh.remove_duplicate_vertices() + expected_pos = o3d.core.Tensor([[0., 0., 0.], + [1., 0., 0.], + [0., 0., 1.], + [1., 0., 1.], + [0., 1., 0.], + [1., 1., 0.], + [0., 1., 1.], + [1., 1., 1.]], dtype=dtype_g) + expected_ind = o3d.core.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]], dtype=dtype_i) + + print(f"DEBUG V: {mesh.vertex.positions}") + assert(mesh.vertex.positions.allclose(expected_pos)) + print(f"DEBUG T: {mesh.triangle.indices}") + assert(mesh.triangle.indices.allclose(expected_ind)) + + +@pytest.mark.parametrize("device", list_devices()) +def test_remove_duplicate_vertices4(device): + """Test Remove duplicates works for float64 coords and int64 indices""" + dtype_g = o3d.core.float64 + dtype_i = o3d.core.int64 + mesh = o3d.t.geometry.TriangleMesh(device) + mesh.vertex.positions = o3d.core.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, 0.0, 0.0], + [1.0, 1.0, 1.0]], dtype=dtype_g) + mesh.triangle.indices = o3d.core.Tensor([[4, 8, 5], + [4, 6, 8], + [0, 2, 4], + [2, 6, 4], + [0, 1, 2], + [1, 3, 2], + [7, 5, 8], + [7, 8, 3], + [2, 3, 8], + [2, 8, 6], + [0, 4, 1], + [1, 4, 5]], dtype=dtype_i) + mesh.remove_duplicate_vertices() + expected_pos = o3d.core.Tensor([[0., 0., 0.], + [1., 0., 0.], + [0., 0., 1.], + [1., 0., 1.], + [0., 1., 0.], + [1., 1., 0.], + [0., 1., 1.], + [1., 1., 1.]], dtype=dtype_g) + expected_ind = o3d.core.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]], dtype=dtype_i) + + print(f"DEBUG V: {mesh.vertex.positions}") + assert(mesh.vertex.positions.allclose(expected_pos)) + print(f"DEBUG T: {mesh.triangle.indices}") + assert(mesh.triangle.indices.allclose(expected_ind)) + + +@pytest.mark.parametrize("device", list_devices()) +def test_remove_duplicate_vertices5(device): + """ + Test that the vertex attribute colors are updated properly + and triangle color attribute are all preserved correctly + after remove duplicate operation. + """ + dtype_g = o3d.core.float64 + dtype_i = o3d.core.int32 + mesh = o3d.t.geometry.TriangleMesh(device) + mesh.vertex.positions = o3d.core.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, 0.0, 0.0], + [1.0, 1.0, 1.0]], dtype=dtype_g) + mesh.vertex.colors = o3d.core.Tensor([[10, 20, 30], + [20, 30, 40], + [30, 40, 50], + [40, 50, 60], + [50, 60, 70], + [60, 70, 80], + [70, 80, 90], + [20, 30, 40], + [80, 90, 100]], dtype=dtype_g) + expected_vertex_colors = o3d.core.Tensor([[10, 20, 30], + [20, 30, 40], + [30, 40, 50], + [40, 50, 60], + [50, 60, 70], + [60, 70, 80], + [70, 80, 90], + [80, 90, 100]], dtype=dtype_g) + mesh.triangle.indices = o3d.core.Tensor([[4, 8, 5], + [4, 6, 8], + [0, 2, 4], + [2, 6, 4], + [0, 1, 2], + [1, 3, 2], + [7, 5, 8], + [7, 8, 3], + [2, 3, 8], + [2, 8, 6], + [0, 4, 1], + [1, 4, 5]], dtype=dtype_i) + expected_tri_colors = o3d.core.Tensor([[100, 11, 20], + [110, 12, 32], + [120, 13, 42], + [130, 14, 52], + [140, 15, 62], + [150, 16, 72], + [160, 17, 82], + [170, 18, 92], + [180, 19, 102], + [190, 20, 112], + [200, 21, 122], + [210, 22, 132]], dtype=dtype_g) + mesh.triangle.colors = expected_tri_colors + mesh.remove_duplicate_vertices() + expected_pos = o3d.core.Tensor([[0., 0., 0.], + [1., 0., 0.], + [0., 0., 1.], + [1., 0., 1.], + [0., 1., 0.], + [1., 1., 0.], + [0., 1., 1.], + [1., 1., 1.]], dtype=dtype_g) + expected_ind = o3d.core.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]], dtype=dtype_i) + print(f"DEBUG V: {mesh.vertex.positions}") + assert(mesh.vertex.positions.allclose(expected_pos)) + print(f"DEBUG VC: {mesh.vertex.colors}") + assert(mesh.vertex.colors.allclose(expected_vertex_colors)) + print(f"DEBUG T: {mesh.triangle.indices}") + assert(mesh.triangle.indices.allclose(expected_ind)) + print(f"DEBUG TC: {mesh.triangle.colors.numpy()}") + assert(mesh.triangle.colors.allclose(expected_tri_colors)) + + +def verify_rem_dup_success(unique_v: List[List], unique_i: List[List], dup_v: List[List], dup_i: List[List]): + """Verifies that the remove duplicate operation on dup_v, dup_i, + was successful""" + ver2uniqindx = set() + # Make sure no duplicates are present + for i, v in enumerate(unique_v): + vs = str(v) + if vs in ver2uniqindx: + return False + ver2uniqindx.add(vs) + + # Verify that the length of the indices did not change. + if len(dup_i) != len(unique_i): + return False + + # Make sure all dup and uniq indices point to + # same vertex coordinates. + for i in range(len(unique_i)): + for t in range(3): + dI = dup_i[i][t] + uI = unique_i[i][t] + if not np.allclose(unique_v[uI], dup_v[dI]): + return False + return True + + +def gen_box_mesh_dup_vertex(unique_v, unique_i, v_index, insert_pos): + """ + Generates a duplicate vertex list and an updated index list from + the uniques afte inserting a vertex at v_index in unique_v to position + insert_pos. + """ + n_uniq = len(unique_v) + assert (v_index < n_uniq and insert_pos <= n_uniq) + index_old_to_new = [i for i in range(n_uniq)] + dup_v = unique_v[:insert_pos] + dup_v.append(unique_v[v_index]) + dup_v.extend(unique_v[insert_pos:]) + for i in range(insert_pos, n_uniq): + index_old_to_new[i] = i + 1 + dup_i = deepcopy(unique_i) + for triple in dup_i: + for t in range(3): + triple[t] = index_old_to_new[triple[t]] + return dup_v, dup_i + + +def gen_rem_dup_test_cases(): + """ + Utility to generate test cases. + The duplicates are generated at various positions. + """ + unique_v = [[0., 0., 0.], + [1., 0., 0.], + [0., 0., 1.], + [1., 0., 1.], + [0., 1., 0.], + [1., 1., 0.], + [0., 1., 1.], + [1., 1., 1.]] + unique_i = [[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]] + nv = len(unique_v) + # Test cases depending on position of replication. + vertind_repind = [] + # Scenario 1: Replicate vertex at the beginning to end and middle. + vertind_repind.extend([(0, 0), (0, nv - 1), (0, nv), (0, nv//2)]) + # Scenario 2: Replicate vertex at the end to begining and middle. + vertind_repind.extend([(nv - 1, nv - 1), (nv - 1, 0), (nv - 1, 1), (nv - 1, nv//2)]) + # Scenario 3: Replicate vertex at 3 to ends and somewhere in the middle. + vertind_repind.extend([(3, 0), (3, nv - 1), (3, nv//2)]) + for vind, repind in vertind_repind: + dup_v, dup_i = gen_box_mesh_dup_vertex(unique_v, unique_i, vind, repind) + yield dup_v, dup_i + # print(f"Duplicates for vertex at index: {vind}, replicated at: {repind}") + # print(f"Dup V: \n{dup_v}") + # print(f"Dup I: \n{dup_i}") + # Scenario 4: some double replication cases. + if vind < repind: + dup_v2, dup_i2 = gen_box_mesh_dup_vertex(dup_v, dup_i, vind, repind) + yield dup_v2, dup_i2 + # print(f"Duplicates2 for vertex at index: {vind}, replicated at: {repind}") + # print(f"Dup2 V: \n{dup_v2}") + # print(f"Dup2 I: \n{dup_i2}") + + +@pytest.mark.parametrize("device", list_devices()) +def test_remove_duplicate_vertices_gen6(device): + # Runs each generated test case verifies that duplicate removal was successful. + mesh = o3d.t.geometry.TriangleMesh(device) + for verts, indices in gen_rem_dup_test_cases(): + mesh.vertex.positions = o3c.Tensor(verts, dtype=o3c.float64) + mesh.triangle.indices = o3c.Tensor(indices, dtype=o3c.int32) + mesh.remove_duplicate_vertices() + unique_v = mesh.vertex.positions.numpy().tolist() + unique_i = mesh.triangle.indices.numpy().tolist() + # Verify duplicate removal was successful + assert(verify_rem_dup_success(unique_v, unique_i, verts, indices)) + + @pytest.mark.parametrize("device", list_devices()) def test_pickle(device): mesh = o3d.t.geometry.TriangleMesh.create_box().to(device) From b56ca5fc2c4b13b03033cfa59d35276d2f93a30a Mon Sep 17 00:00:00 2001 From: "Ganjugunte, Shashidhara Krishnamurthy" Date: Fri, 6 Oct 2023 10:08:37 -0700 Subject: [PATCH 2/4] Updated CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96bedc8af1f..4a1c06c4131 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * Support msgpack versions without cmake * Support multi-threading in the RayCastingScene function to commit scene (PR #6051). * Fix some bad triangle generation in TriangleMesh::SimplifyQuadricDecimation +* Implemented functionality for removing duplicate vertices from TriangleMesh (PR #6414). ## 0.13 From 158d965ad55aef598f742e76a205b3efe9d3a9d0 Mon Sep 17 00:00:00 2001 From: "Ganjugunte, Shashidhara Krishnamurthy" Date: Tue, 14 Nov 2023 18:11:21 +0530 Subject: [PATCH 3/4] Moving standalone functions to anonymous namespace --- cpp/open3d/t/geometry/TriangleMesh.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index 3de1bc90783..bb962fd5512 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -1061,6 +1061,7 @@ TriangleMesh TriangleMesh::SelectFacesByMask(const core::Tensor &mask) const { return result; } +namespace { /// /// This function shrinks a vertex attribute tensor to have length new_size. /// This function is used in RemoveDuplicateVerticesWorker. @@ -1203,7 +1204,7 @@ TriangleMesh& RemoveDuplicateVerticesWorker(TriangleMesh &mesh) return mesh; } - +} //Anonymous namespace /// /// Remove duplicate vertices from a mesh and return the resulting mesh. From a7664c84c37f26c06e1237e8a433f2853b5f8e9b Mon Sep 17 00:00:00 2001 From: "Ganjugunte, Shashidhara Krishnamurthy" Date: Wed, 15 Nov 2023 18:38:37 +0530 Subject: [PATCH 4/4] Duplicate vertex attributes shrunk using IndexGet Previously, I was doing a manual copy and shrinking the vertices. Instead in this checkin all vertex attributes, including the coordinates are shrunk using IndexGet method using a vertex mask. Further, the triangle index remapping is similar to what was done earlier, with some simplications. One thing to note, is that we cannot use utility::InclusivePrefixSum to remap vertex indices because the duplicates can occur in any position and may be duplicates of any earlier vertex. For e.g., suppose there were 9 vertices to start with, and 8th (index 7, starting from 0), was a duplicate of 2nd (index 1, starting from 0). So, the vertex mask would look like this: Vertex indices: [0, 1, 2, 3, 4, 5, 6, 7, 8] Vertex mask: [1, 1, 1, 1, 1, 1, 1, 0, 1] Prefix sum: [0, 1, 2, 3, 4, 5, 6, 7, 7, 8] This gives an incorrect index map for 8th vertex, which is mapped to index 7 instead of 1. On branch sganjugu/remdup2 Your branch is up to date with 'origin/sganjugu/remdup2'. Changes to be committed: modified: ../cpp/open3d/t/geometry/TriangleMesh.cpp modified: ../python/test/t/geometry/test_trianglemesh.py --- cpp/open3d/t/geometry/TriangleMesh.cpp | 116 ++++++-------------- python/test/t/geometry/test_trianglemesh.py | 2 +- 2 files changed, 34 insertions(+), 84 deletions(-) diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index bb962fd5512..0632e999457 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -1062,57 +1062,6 @@ TriangleMesh TriangleMesh::SelectFacesByMask(const core::Tensor &mask) const { } namespace { -/// -/// This function shrinks a vertex attribute tensor to have length new_size. -/// This function is used in RemoveDuplicateVerticesWorker. -/// Assumptions: -/// 1. The new_size is the number of unique vertices in the mesh. -/// 2. The attribute tensor has been updated during duplicate removal in such a way that -/// attributes corresponding to unique vertices are moved to the beginning and the rest -/// can be discarded. -/// \param attrib The attribute whose tensor has to be shrunk. -/// \param mesh The mesh from which duplicates have to be removed. -/// \param new_size The size to shrink the attribute tensor to. -void ShrinkVertexAttributeTensor(const std::string &attrib, TriangleMesh &mesh, int64_t new_size) -{ - auto old_tensor = mesh.GetVertexAttr(attrib); - std::vector old_shape_vec(old_tensor.GetShape()); - assert (new_size <= old_shape_vec[0]); - old_shape_vec[0] = new_size; - - auto new_shape = core::SizeVector(old_shape_vec); - core::Tensor new_tensor = core::Tensor(new_shape, old_tensor.GetDtype(), old_tensor.GetDevice()); - for(decltype(new_size) i = 0; i < new_size; ++i) { - new_tensor[i] = old_tensor[i]; - } - mesh.SetVertexAttr(attrib, new_tensor); -} - - -/// This function is used in RemoveDuplicateVerticesWorker to update -/// the triangle indices once a mapping from old indices to newer ones is computed. -/// \param indices_ptr The triangle indices pointer computed for appropriate datatype from mesh.triangle.indices. -/// \param index_old_to_new Map from old indices to new indices. -/// \param mesh The mesh on which the new index mapping for triangles is to be computed -template -void RemapTriangleIndices(T *indices_ptr, std::vector &index_old_to_new, TriangleMesh &mesh) -{ - auto triangles = mesh.GetTriangleIndices(); - auto nIndices = triangles.GetLength(); - std::vector flat_indices(nIndices * 3); - for(Length_t tI = 0; tI < nIndices; ++tI) { - auto curI = static_cast(tI * 3); - flat_indices[curI] = index_old_to_new[indices_ptr[curI]]; - curI++; - flat_indices[curI] = index_old_to_new[indices_ptr[curI]]; - curI++; - flat_indices[curI] = index_old_to_new[indices_ptr[curI]]; - } - core::Tensor new_indices = core::Tensor(flat_indices, triangles.GetShape(), - triangles.GetDtype(), triangles.GetDevice()); - mesh.SetTriangleIndices(new_indices); -} - /// This is a templatized local version of RemoveDuplicates. /// Use of templates allows us to use common code for different @@ -1135,7 +1084,7 @@ void RemapTriangleIndices(T *indices_ptr, std::vector &index_old_to_ne /// \param mesh The mesh from which duplicate vertices are to be removed. template TriangleMesh& RemoveDuplicateVerticesWorker(TriangleMesh &mesh) -{ +{ //Algorithm based on Eigen implementation if(!mesh.HasVertexPositions()) { utility::LogWarning("TriangeMesh::RemoveDuplicateVertices: No vertices present, ignoring."); @@ -1147,63 +1096,64 @@ TriangleMesh& RemoveDuplicateVerticesWorker(TriangleMesh &mesh) point_to_old_index; auto vertices = mesh.GetVertexPositions(); - auto triangles = mesh.GetTriangleIndices(); - - //Create a vector of pointers to attribute tensors. - std::vector vertexAttrs; - std::vector vertexAttrNames; - std::vector hasVertexAttrs; - for(auto item: mesh.GetVertexAttr()) { - vertexAttrNames.push_back(item.first); - vertexAttrs.push_back(&mesh.GetVertexAttr(item.first)); - hasVertexAttrs.push_back(mesh.HasVertexAttr(item.first)); - } - - auto old_vertex_num = vertices.GetLength(); - using Length_t = decltype(old_vertex_num); - std::vector index_old_to_new(old_vertex_num); + auto orig_num_vertices = vertices.GetLength(); + const auto vmask_type = core::Int32; + using vmask_itype = int32_t; + core::Tensor vertex_mask = core::Tensor::Zeros({orig_num_vertices}, vmask_type); + using Length_t = decltype(orig_num_vertices); + std::vector index_old_to_new(orig_num_vertices); //REM_DUP_VERT_STEP 1: // Compute map from points to old indices, and use a counter // to compute a map from old indices to new unique indices. const Coord_t *vertices_ptr = vertices.GetDataPtr(); + vmask_itype * vertex_mask_ptr = vertex_mask.GetDataPtr(); Length_t k = 0; // new index - for (Length_t i = 0; i < old_vertex_num; i++) { // old index + for (Length_t i = 0; i < orig_num_vertices; ++i) { // old index Point3d coord = std::make_tuple(vertices_ptr[i * 3], vertices_ptr[i * 3 + 1], vertices_ptr[i * 3 + 2]); if (point_to_old_index.find(coord) == point_to_old_index.end()) { point_to_old_index[coord] = i; index_old_to_new[i] = k; - //Update attributes, including positions - for(size_t j = 0; j < vertexAttrs.size(); ++j) { - if (!hasVertexAttrs[j]) continue; - auto &vattr = *vertexAttrs[j]; - vattr[k] = vattr[i]; - } - k++; + ++k; + vertex_mask_ptr[i] = 1; } else { index_old_to_new[i] = index_old_to_new[point_to_old_index[coord]]; } } + vertex_mask = vertex_mask.To(mesh.GetDevice(), core::Bool); + //REM_DUP_VERT_STEP 2: - // Shrink all the vertex attribute tensors to size equal to number of unique vertices. - for(size_t j = 0; j < vertexAttrNames.size(); ++j) { - ShrinkVertexAttributeTensor(vertexAttrNames[j], mesh, k); + // Update vertex attributes, by shrinking them appropriately. + for (auto item : mesh.GetVertexAttr()) { + mesh.SetVertexAttr(item.first, item.second.IndexGet({vertex_mask})); } //REM_DUP_VERT_STEP 3: - // Remap triangle indices to new unique vertex indices. - if (k < old_vertex_num) { + // Remap triangle indices to new unique vertex indices, if required. + if (k < orig_num_vertices) { + core::Tensor tris = mesh.GetTriangleIndices(); + core::Tensor tris_cpu = tris.To(core::Device()).Contiguous(); + const int64_t num_tris = tris_cpu.GetLength(); //Update triangle indices. - Tindex_t *indices_ptr = triangles.GetDataPtr(); - RemapTriangleIndices(indices_ptr, index_old_to_new, mesh); + Tindex_t *indices_ptr = tris_cpu.GetDataPtr(); + for(int64_t i = 0; i < num_tris * 3; ++i) { + int64_t new_idx = index_old_to_new[indices_ptr[i]]; + indices_ptr[i] = Tindex_t(new_idx); + } + //Update triangle indices, no need to update other attributes. + tris = tris_cpu.To(mesh.GetDevice()); + mesh.SetTriangleIndices(tris); } + utility::LogDebug( "[RemoveDuplicatedVertices] {:d} vertices have been removed.", - (int)(old_vertex_num - k)); + (int)(orig_num_vertices - k)); return mesh; + } + } //Anonymous namespace /// diff --git a/python/test/t/geometry/test_trianglemesh.py b/python/test/t/geometry/test_trianglemesh.py index 2cb4cb3af72..a179bf02a4b 100644 --- a/python/test/t/geometry/test_trianglemesh.py +++ b/python/test/t/geometry/test_trianglemesh.py @@ -730,7 +730,7 @@ def verify_rem_dup_success(unique_v: List[List], unique_i: List[List], dup_v: Li # Make sure all dup and uniq indices point to # same vertex coordinates. - for i in range(len(unique_i)): + for i, _ in enumerate(unique_i): for t in range(3): dI = dup_i[i][t] uI = unique_i[i][t]