diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index 431efd9032b..34b960b7390 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -1347,6 +1347,163 @@ TriangleMesh TriangleMesh::RemoveUnreferencedVertices() { return *this; } +template ::value && + !std::is_same::value, + T>::type * = nullptr> +using Edge = std::tuple; + +/// brief Helper function to get an edge with ordered vertex indices. +template +static inline Edge GetOrderedEdge(T vidx0, T vidx1) { + return (vidx0 < vidx1) ? Edge{vidx0, vidx1} : Edge{vidx1, vidx0}; +} + +/// brief Helper +/// +template +static std::unordered_map, + std::vector, + utility::hash_tuple>> +GetEdgeToTrianglesMap(const core::Tensor &tris_cpu) { + std::unordered_map, std::vector, + utility::hash_tuple>> + tris_per_edge; + auto AddEdge = [&](T vidx0, T vidx1, int64_t tidx) { + tris_per_edge[GetOrderedEdge(vidx0, vidx1)].push_back(tidx); + }; + const T *tris_ptr = tris_cpu.GetDataPtr(); + for (int64_t tidx = 0; tidx < tris_cpu.GetLength(); ++tidx) { + const T *triangle = &tris_ptr[3 * tidx]; + AddEdge(triangle[0], triangle[1], tidx); + AddEdge(triangle[1], triangle[2], tidx); + AddEdge(triangle[2], triangle[0], tidx); + } + return tris_per_edge; +} + +TriangleMesh TriangleMesh::RemoveNonManifoldEdges() { + if (!HasVertexPositions() || GetVertexPositions().GetLength() == 0) { + utility::LogWarning( + "[RemoveNonManifildEdges] TriangleMesh has no vertices."); + return *this; + } + + if (!HasTriangleIndices() || GetTriangleIndices().GetLength() == 0) { + utility::LogWarning( + "[RemoveNonManifoldEdges] TriangleMesh has no triangles."); + return *this; + } + + GetVertexAttr().AssertSizeSynchronized(); + GetTriangleAttr().AssertSizeSynchronized(); + + core::Tensor tris_cpu = + GetTriangleIndices().To(core::Device()).Contiguous(); + + ComputeTriangleAreas(); + core::Tensor tri_areas_cpu = + GetTriangleAttr("areas").To(core::Device()).Contiguous(); + + DISPATCH_FLOAT_INT_DTYPE_TO_TEMPLATE( + GetVertexPositions().GetDtype(), tris_cpu.GetDtype(), [&]() { + scalar_t *tri_areas_ptr = tri_areas_cpu.GetDataPtr(); + auto edges_to_tris = GetEdgeToTrianglesMap(tris_cpu); + + // lambda to compare triangles areas by index + auto area_greater_compare = [&tri_areas_ptr](size_t lhs, + size_t rhs) { + return tri_areas_ptr[lhs] > tri_areas_ptr[rhs]; + }; + + // go through all edges and for those that have more than 2 + // triangles attached, remove the triangles with the minimal + // area + for (auto &kv : edges_to_tris) { + // remove all triangles which are already marked for removal + // (area < 0) note, the erasing of triangles happens + // afterwards + auto tris_end = std::remove_if( + kv.second.begin(), kv.second.end(), + [=](size_t t) { return tri_areas_ptr[t] < 0; }); + // count non-removed triangles (with area > 0). + int n_tris = std::distance(kv.second.begin(), tris_end); + + if (n_tris <= 2) { + // nothing to do here as either: + // - all triangles of the edge are already marked for + // deletion + // - the edge is manifold: it has 1 or 2 triangles with + // a non-negative area + continue; + } + + // now erase all triangle indices already marked for removal + kv.second.erase(tris_end, kv.second.end()); + + // find first to triangles with the maximal area + std::nth_element(kv.second.begin(), kv.second.begin() + 1, + kv.second.end(), area_greater_compare); + + // mark others for deletion + for (auto it = kv.second.begin() + 2; it < kv.second.end(); + ++it) { + tri_areas_ptr[*it] = -1; + } + } + }); + + // mask for triangles with positive area + core::Tensor tri_mask = tri_areas_cpu.Gt(0.0).To(GetDevice()); + + // pick up positive-area triangles (and their attributes) + for (auto item : GetTriangleAttr()) { + SetTriangleAttr(item.first, item.second.IndexGet({tri_mask})); + } + + return *this; +} + +core::Tensor TriangleMesh::GetNonManifoldEdges( + bool allow_boundary_edges /* = true */) const { + if (!HasVertexPositions()) { + utility::LogWarning( + "[GetNonManifoldEdges] TriangleMesh has no vertices."); + return {}; + } + + if (!HasTriangleIndices()) { + utility::LogWarning( + "[GetNonManifoldEdges] TriangleMesh has no triangles."); + return {}; + } + + core::Tensor result; + core::Tensor tris_cpu = + GetTriangleIndices().To(core::Device()).Contiguous(); + core::Dtype tri_dtype = tris_cpu.GetDtype(); + + DISPATCH_INT_DTYPE_PREFIX_TO_TEMPLATE(tri_dtype, tris, [&]() { + auto edges = GetEdgeToTrianglesMap(tris_cpu); + std::vector non_manifold_edges; + + for (auto &kv : edges) { + if ((allow_boundary_edges && + (kv.second.size() < 1 || kv.second.size() > 2)) || + (!allow_boundary_edges && kv.second.size() != 2)) { + non_manifold_edges.push_back(std::get<0>(kv.first)); + non_manifold_edges.push_back(std::get<1>(kv.first)); + } + } + + result = core::Tensor(non_manifold_edges, + {(long int)non_manifold_edges.size() / 2, 2}, + tri_dtype, GetTriangleIndices().GetDevice()); + }); + + return result; +} + } // namespace geometry } // namespace t } // namespace open3d diff --git a/cpp/open3d/t/geometry/TriangleMesh.h b/cpp/open3d/t/geometry/TriangleMesh.h index 285c353fb78..c2916c987ca 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.h +++ b/cpp/open3d/t/geometry/TriangleMesh.h @@ -979,6 +979,20 @@ class TriangleMesh : public Geometry, public DrawableGeometry { /// \return The reference to itself. TriangleMesh RemoveUnreferencedVertices(); + /// Removes all non-manifold edges, by successively deleting triangles + /// with the smallest surface area adjacent to the + /// non-manifold edge until the number of adjacent triangles to the edge is + /// `<= 2`. If mesh is empty or has no triangles, prints a warning and + /// returns immediately. \return The reference to itself. + TriangleMesh RemoveNonManifoldEdges(); + + /// Returns the non-manifold edges of the triangle mesh. + /// If \param allow_boundary_edges is set to false, then also boundary + /// edges are returned. + /// \return 2d integer tensor with shape {n,2} encoding ordered edges. + /// If mesh is empty or has no triangles, returns an empty tensor. + core::Tensor GetNonManifoldEdges(bool allow_boundary_edges = true) const; + 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 bb6cb6b5448..c34021b3bab 100644 --- a/cpp/pybind/t/geometry/trianglemesh.cpp +++ b/cpp/pybind/t/geometry/trianglemesh.cpp @@ -1006,6 +1006,22 @@ or has a negative value, it is ignored. surface_area = box.compute_triangle_areas().triangle.areas.sum() )"); + triangle_mesh.def("remove_non_manifold_edges", + &TriangleMesh::RemoveNonManifoldEdges, + R"(Function that removes all non-manifold edges, by +successively deleting triangles with the smallest surface +area adjacent to the non-manifold edge until the number of +adjacent triangles to the edge is `<= 2`. + +Returns: + The mesh. +)"); + + triangle_mesh.def("get_non_manifold_edges", + &TriangleMesh::GetNonManifoldEdges, + "allow_boundary_edges"_a = true, + R"(Returns the list consisting of non-manifold edges.)"); + } } // namespace geometry diff --git a/cpp/tests/t/geometry/TriangleMesh.cpp b/cpp/tests/t/geometry/TriangleMesh.cpp index e5657256a20..089f85a852d 100644 --- a/cpp/tests/t/geometry/TriangleMesh.cpp +++ b/cpp/tests/t/geometry/TriangleMesh.cpp @@ -1358,5 +1358,69 @@ TEST_P(TriangleMeshPermuteDevices, ComputeTriangleAreas) { EXPECT_TRUE(t_mesh.GetTriangleAttr("areas").AllClose( core::Tensor(areas, {(int)areas.size()}, core::Float64))); } + +TEST_P(TriangleMeshPermuteDevices, RemoveNonManifoldEdges) { + core::Device device = GetParam(); + t::geometry::TriangleMesh mesh_empty; + EXPECT_TRUE(mesh_empty.RemoveNonManifoldEdges().IsEmpty()); + + core::Tensor verts = 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}, + {0.0, -0.2, 0.0}, + }, + device); + + mesh_empty.SetVertexPositions(verts); + EXPECT_TRUE(mesh_empty.GetVertexPositions().AllClose(verts)); + + core::Tensor tris = core::Tensor::Init( + {{4, 7, 5}, {8, 0, 1}, {8, 0, 1}, {8, 0, 1}, {4, 6, 7}, {0, 2, 4}, + {2, 6, 4}, {0, 1, 2}, {1, 3, 2}, {1, 5, 7}, {8, 0, 2}, {8, 0, 2}, + {8, 0, 1}, {1, 7, 3}, {2, 3, 7}, {2, 7, 6}, {8, 0, 2}, {6, 6, 7}, + {0, 4, 1}, {8, 0, 4}, {1, 4, 5}}, + device); + + core::Tensor tri_labels = tris * 10; + + t::geometry::TriangleMesh mesh(verts, tris); + mesh.SetTriangleAttr("labels", tri_labels); + + geometry::TriangleMesh legacy_mesh = mesh.ToLegacy(); + core::Tensor expected_edges = + core::eigen_converter::EigenVector2iVectorToTensor( + legacy_mesh.GetNonManifoldEdges(), core::Int64, device); + EXPECT_TRUE(mesh.GetNonManifoldEdges().AllClose(expected_edges)); + + expected_edges = core::eigen_converter::EigenVector2iVectorToTensor( + legacy_mesh.GetNonManifoldEdges(true), core::Int64, device); + EXPECT_TRUE(mesh.GetNonManifoldEdges(true).AllClose(expected_edges)); + expected_edges = core::eigen_converter::EigenVector2iVectorToTensor( + legacy_mesh.GetNonManifoldEdges(false), core::Int64, device); + EXPECT_TRUE(mesh.GetNonManifoldEdges(false).AllClose(expected_edges)); + + mesh.RemoveNonManifoldEdges(); + + EXPECT_TRUE(mesh.GetNonManifoldEdges(true).AllClose( + core::Tensor({0, 2}, core::Int64))); + + EXPECT_TRUE(mesh.GetNonManifoldEdges(false).AllClose( + core::Tensor({0, 2}, core::Int64))); + + t::geometry::TriangleMesh box = t::geometry::TriangleMesh::CreateBox(); + EXPECT_TRUE(mesh.GetVertexPositions().AllClose(verts)); + EXPECT_TRUE(mesh.GetTriangleIndices().AllClose(box.GetTriangleIndices())); + core::Tensor expected_labels = tri_labels.IndexGet( + {core::Tensor::Init({1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, + 0, 0, 1, 1, 1, 0, 0, 1, 0, 1})}); + EXPECT_TRUE(mesh.GetTriangleAttr("labels").AllClose(expected_labels)); +} } // namespace tests } // namespace open3d diff --git a/python/test/t/geometry/test_trianglemesh.py b/python/test/t/geometry/test_trianglemesh.py index 5f116732533..e79c188d31b 100644 --- a/python/test/t/geometry/test_trianglemesh.py +++ b/python/test/t/geometry/test_trianglemesh.py @@ -734,3 +734,38 @@ def test_compute_triangle_areas(device, int_t, float_t): ], float_t, device) assert torus.compute_triangle_areas().triangle.areas.allclose( expected_areas) + + +@pytest.mark.parametrize("device", list_devices()) +@pytest.mark.parametrize("int_t", (o3c.int32, o3c.int64)) +@pytest.mark.parametrize("float_t", (o3c.float32, o3c.float64)) +def test_remove_non_manifold_edges(device, int_t, float_t): + verts = 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], [0.0, -0.2, 0.0]], + float_t, device) + + tris = o3c.Tensor( + [[4, 7, 5], [8, 0, 1], [8, 0, 1], [8, 0, 1], [4, 6, 7], [0, 2, 4], + [2, 6, 4], [0, 1, 2], [1, 3, 2], [1, 5, 7], [8, 0, 2], [8, 0, 2], + [8, 0, 1], [1, 7, 3], [2, 3, 7], [2, 7, 6], [8, 0, 2], [6, 6, 7], + [0, 4, 1], [8, 0, 4], [1, 4, 5]], int_t, device) + + test_box = o3d.t.geometry.TriangleMesh(verts, tris) + test_box_legacy = test_box.to_legacy() + + # allow boundary edges + edges = test_box_legacy.get_non_manifold_edges() + np.testing.assert_allclose(test_box.get_non_manifold_edges().numpy(), + np.asarray(edges)) + # disallow boundary edges + edges = test_box_legacy.get_non_manifold_edges(False) + np.testing.assert_allclose( + test_box.get_non_manifold_edges(False).numpy(), np.asarray(edges)) + + test_box.remove_non_manifold_edges() + + box = o3d.t.geometry.TriangleMesh.create_box(float_dtype=float_t, + int_dtype=int_t) + assert test_box.vertex.positions.allclose(verts) + assert test_box.triangle.indices.allclose(box.triangle.indices)