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. #6414

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
182 changes: 182 additions & 0 deletions cpp/open3d/t/geometry/TriangleMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
intelshashi marked this conversation as resolved.
Show resolved Hide resolved
{
auto old_tensor = mesh.GetVertexAttr(attrib);
std::vector<decltype(new_size)> old_shape_vec(old_tensor.GetShape());
assert (new_size <= old_shape_vec[0]);
intelshashi marked this conversation as resolved.
Show resolved Hide resolved
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<typename T, typename Length_t>
void RemapTriangleIndices(T *indices_ptr, std::vector<Length_t> &index_old_to_new, TriangleMesh &mesh)
intelshashi marked this conversation as resolved.
Show resolved Hide resolved
{
auto triangles = mesh.GetTriangleIndices();
auto nIndices = triangles.GetLength();
std::vector<T> flat_indices(nIndices * 3);
for(Length_t tI = 0; tI < nIndices; ++tI) {
auto curI = static_cast<Length_t>(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<typename Coord_t, typename Tindex_t>
TriangleMesh& RemoveDuplicateVerticesWorker(TriangleMesh &mesh)
intelshashi marked this conversation as resolved.
Show resolved Hide resolved
{
//Algorithm based on Eigen implementation
if(!mesh.HasVertexPositions()) {
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 run python util/check_style.py --apply on your changes? There are a few accidental space misses.

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();
auto triangles = mesh.GetTriangleIndices();

//Create a vector of pointers to attribute tensors.
std::vector<core::Tensor *> vertexAttrs;
std::vector<std::string> vertexAttrNames;
std::vector<bool> 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<Length_t> 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<Coord_t>();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to move vertices tensor to CPU device, too.

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<Tindex_t>();
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();
Copy link
Contributor

Choose a reason for hiding this comment

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

There could be meshes w/o triangles tensor. In this case, the call to GetTrianglesIndices throws.
Could you please add safety checks for this case?

if (core::Int32 != triangles.GetDtype() && core::Int64 != triangles.GetDtype()) {
utility::LogError("Only Int32 or Int64 are supported for triangle indices");
return *this;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we don't need to return after utility::LogError. (here and below)

}
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<float, int>(*this);
} else {
return RemoveDuplicateVerticesWorker<float, int64_t>(*this);
}
} else {
if (core::Int32 == triangles.GetDtype()) {
return RemoveDuplicateVerticesWorker<double, int>(*this);
} else {
return RemoveDuplicateVerticesWorker<double, int64_t>(*this);
}
}
Comment on lines +1335 to +1347
Copy link
Contributor

Choose a reason for hiding this comment

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

While working on my SelectByIndex method, I discovered a set of macros to do this in more compact way:
https://github.com/isl-org/Open3D/blob/main/cpp/open3d/core/Dispatch.h#L90
I think we could rewrite it as smt like:

Suggested change
if(core::Float32 == vertices.GetDtype()) {
if (core::Int32 == triangles.GetDtype()) {
return RemoveDuplicateVerticesWorker<float, int>(*this);
} else {
return RemoveDuplicateVerticesWorker<float, int64_t>(*this);
}
} else {
if (core::Int32 == triangles.GetDtype()) {
return RemoveDuplicateVerticesWorker<double, int>(*this);
} else {
return RemoveDuplicateVerticesWorker<double, int64_t>(*this);
}
}
DISPATCH_FLOAT_INT_DTYPE_TO_TEMPLATE(vertices.GetDtype(), triangles.GetDtype()), [&]() {
RemoveDuplicateVerticesWorker<scalar_t, int_t>(*this);
});

}


} // namespace geometry
} // namespace t
} // namespace open3d
6 changes: 6 additions & 0 deletions cpp/open3d/t/geometry/TriangleMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
Expand Down
4 changes: 4 additions & 0 deletions cpp/pybind/t/geometry/trianglemesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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