Skip to content

Commit

Permalink
Implement t::geometry::TriangleMesh::RemoveNonManifoldEdges...
Browse files Browse the repository at this point in the history
... and t::geometry::TriangleMesh::GetNonManifoldEdges methods.
The methods are defined in geometry::TriangleMesh API.

t::geometry::TriangleMesh::GetNonManifoldEdges mimics the logic of the
legacy method.

t::geometry::TriangleMesh::RemoveNonManifoldEdges follows the logic of
the legacy method but there are a few differences:
* the main difference is that the outer while-loop is removed. I don't
  see how after the first iteration any edge can have more than 2
  adjacent triangles, which makes the further iterations unnecessary.
* I count triangles with non-negative areas immediately and do not rely
  on the total number of adjacent triangles (which would also include
  triangles marked for removal).
* To choose a triangle with the minimal area out of the existing
  adjacent triangles I use a heap structure.
  • Loading branch information
nsaiapova committed Apr 16, 2024
1 parent a363a0b commit 20782b4
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 1 deletion.
157 changes: 157 additions & 0 deletions cpp/open3d/t/geometry/TriangleMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,163 @@ TriangleMesh TriangleMesh::RemoveUnreferencedVertices() {
return *this;
}

template <typename T,
typename std::enable_if<std::is_integral<T>::value &&
!std::is_same<T, bool>::value,
T>::type * = nullptr>
using Edge = std::tuple<T, T>;

/// brief Helper function to get an edge with ordered vertex indices.
template <typename T>
static inline Edge<T> GetOrderedEdge(T vidx0, T vidx1) {
return (vidx0 < vidx1) ? Edge<T>{vidx0, vidx1} : Edge<T>{vidx1, vidx0};
}

/// brief Helper
///
template <typename T>
static std::unordered_map<Edge<T>,
std::vector<size_t>,
utility::hash_tuple<Edge<T>>>
GetEdgeToTrianglesMap(const core::Tensor &tris_cpu) {
std::unordered_map<Edge<T>, std::vector<size_t>,
utility::hash_tuple<Edge<T>>>
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<T>();
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<scalar_t>();
auto edges_to_tris = GetEdgeToTrianglesMap<int_t>(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<scalar_tris_t>(tris_cpu);
std::vector<scalar_tris_t> 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
14 changes: 14 additions & 0 deletions cpp/open3d/t/geometry/TriangleMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,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_;
Expand Down
8 changes: 8 additions & 0 deletions cpp/pybind/t/geometry/trianglemesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,14 @@ or has a negative value, it is ignored.
box = o3d.t.geometry.TriangleMesh.create_box()
top_face = box.select_by_index([2, 3, 6, 7])
)");
triangle_mesh.def("remove_non_manifold_edges",
&TriangleMesh::RemoveNonManifoldEdges,
"Remove non-manifold edges from the mesh.");

triangle_mesh.def("get_non_manifold_edges",
&TriangleMesh::GetNonManifoldEdges,
"allow_boundary_edges"_a = true,
"Return the list consisting of non-manifold edges.");

triangle_mesh.def("remove_unreferenced_vertices",
&TriangleMesh::RemoveUnreferencedVertices,
Expand Down
64 changes: 64 additions & 0 deletions cpp/tests/t/geometry/TriangleMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1357,5 +1357,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<float>(
{
{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<int64_t>(
{{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<bool>({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
36 changes: 35 additions & 1 deletion python/test/t/geometry/test_trianglemesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,6 @@ def test_select_by_index_64(device):
untouched_sphere.triangle.indices)



def check_no_unreferenced_vertices(device, int_t, float_t):
sphere = o3d.t.geometry.TriangleMesh.create_sphere(1, 3, float_t, int_t,
device)
Expand Down Expand Up @@ -735,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)

0 comments on commit 20782b4

Please sign in to comment.