Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RemoveDuplicateVertices Implementation for TriangleMesh (continued) #6636

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- Fix geometry picker Error when LineSet objects are presented (PR #6499)
- Fix mis-configured application .desktop link for the Open3D viewer when installing to a custom path (PR #6599)
- Fix regression in printing cuda tensor from PR #6444 🐛
- Implemented functionality for removing duplicate vertices from TriangleMesh (PR #6414).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please align this line with the rest of the list (e.g. add two more spaces after -)?


## 0.13

Expand Down
137 changes: 137 additions & 0 deletions cpp/open3d/t/geometry/TriangleMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,143 @@ TriangleMesh TriangleMesh::SelectByIndex(const core::Tensor &indices) const {
return result;
}

namespace {

/// 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
Comment on lines +1262 to +1268
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// (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
/// * point_to_old_index maps vertex coordinates to their indices.
// This is implemented using unordered_map.
/// * index_old_to_new maps vertex indices before duplicate removal to indices after
/// the removal. This is implemented using std::vector.
/// The first map allows the function to detect duplicates and the second allows it to 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 <typename Coord_t, typename Tindex_t>
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<Coord_t, Coord_t, Coord_t> Point3d;
std::unordered_map<Point3d, size_t, utility::hash_tuple<Point3d>>
point_to_old_index;

auto vertices = mesh.GetVertexPositions().To(mesh.GetDevice()).Contiguous();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, the vertex tensor is already on the device of the mesh, but we want to move the tensor to CPU, so I think we need to use core::Device() here.

auto orig_num_vertices = vertices.GetLength();
const auto vmask_type = core::Bool;
using vmask_itype = bool;
Comment on lines +1295 to +1296
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a personal taste, but I find the original types, bool and core::Bool, more descriptive in this case.

core::Tensor vertex_mask =
core::Tensor::Zeros({orig_num_vertices}, vmask_type);
using Length_t = decltype(orig_num_vertices);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we just use int64_t here? It looks like the GetLength type is always int64_t.

std::vector<Length_t> 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<Coord_t>();
vmask_itype *vertex_mask_ptr = vertex_mask.GetDataPtr<vmask_itype>();
Length_t k = 0; // new 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;
++k;
vertex_mask_ptr[i] = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 1 -> true, since it is a bool value here. (it is 1 in other functions above as we use int there for the prefix_sum approach).

} 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:
// 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 required.
if (k < orig_num_vertices && mesh.HasTriangleIndices()) {
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 = tris_cpu.GetDataPtr<Tindex_t>();
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)(orig_num_vertices - k));

return mesh;
}

} // Anonymous namespace

///
/// 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 triangle_dtype = core::Int32;
if (HasTriangleIndices()) {
triangle_dtype = GetTriangleIndices().GetDtype();
}
if (core::Int32 != triangle_dtype && core::Int64 != triangle_dtype) {
utility::LogWarning(
"TriangleMesh::RemoveDuplicateVertices: Only Int32 or Int64 "
"are supported for triangle indices");
return *this;
}
if (core::Float32 != vertices.GetDtype() &&
core::Float64 != vertices.GetDtype()) {
utility::LogWarning(
"TriangleMesh::RemoveDuplicateVertices: Only Float32 or "
"Float64 is supported for vertex coordinates");
return *this;
}

DISPATCH_FLOAT_INT_DTYPE_TO_TEMPLATE(
vertices.GetDtype(), triangle_dtype,
[&]() { RemoveDuplicateVerticesWorker<scalar_t, int_t>(*this); });
return *this;
}

} // namespace geometry
} // namespace t
} // namespace open3d
5 changes: 5 additions & 0 deletions cpp/open3d/t/geometry/TriangleMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,11 @@ class TriangleMesh : public Geometry, public DrawableGeometry {
/// empty, return an empty mesh.
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();

/// Returns a new mesh with the vertices selected by a vector of indices.
/// If an item from the indices list exceeds the max vertex number of
/// the mesh or has a negative value, it is ignored.
Expand Down
5 changes: 5 additions & 0 deletions cpp/pybind/t/geometry/trianglemesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,11 @@ 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,
Expand Down
51 changes: 51 additions & 0 deletions cpp/tests/t/geometry/TriangleMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,57 @@ TEST_P(TriangleMeshPermuteDevices, Transform) {
core::Tensor::Init<float>({{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<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, 0.0, 0.0},
{1.0, 1.0, 1.0}},
device));
mesh.SetTriangleIndices(core::Tensor::Init<int>({{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<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}})));
EXPECT_TRUE(mesh.GetTriangleIndices().AllClose(
core::Tensor::Init<int>({{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();

Expand Down
Loading
Loading