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

ComputeAdjacencyList Assignment Open3D #7094

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions cpp/open3d/t/geometry/TriangleMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <Eigen/Core>
#include <string>
#include <unordered_map>
#include <set>

#include "open3d/core/CUDAUtils.h"
#include "open3d/core/Device.h"
Expand Down Expand Up @@ -65,6 +66,54 @@ TriangleMesh::TriangleMesh(const core::Tensor &vertex_positions,
SetVertexPositions(vertex_positions);
SetTriangleIndices(triangle_indices);
}

std::pair<core::Tensor, core::Tensor> TriangleMesh::ComputeAdjacencyList() {

if (IsEmpty()) {
utility::LogWarning("TriangleMesh is empty. No attributes computed!");
return std::make_pair(core::Tensor(), core::Tensor());
}

if(!HasTriangleIndices()) {
utility::LogWarning("TriangleMesh has no Indices. No attributes computed!");
return std::make_pair(core::Tensor(), core::Tensor());
}

core::Tensor tris_cpu =
GetTriangleIndices().To(core::Device()).Contiguous();
Copy link
Member

Choose a reason for hiding this comment

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

TODO: Also enable on cuda and sycl.


std::unordered_map< size_t, std::set<size_t> > adjacencyList;
auto insertEdge = [&adjacencyList](size_t s, size_t t){
adjacencyList[s].insert(t);
};
Copy link
Member

Choose a reason for hiding this comment

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

Edges are undirected. For a triangle mesh, 2 neighboring faces will share an edge. If they are oriented in-consistently, the directions will be opposite, i.e. if one face is [0, 1, 2], another may be [2, 1, 3]. In this case we will get duplicate entries in the adjacency list. Essentially, we just need the lower triangle of the adjacency matrix - the upper triangle is redundant.

Copy link
Member

Choose a reason for hiding this comment

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

Also, some tri meshes are not manifold (degenerate) - this can lead to situations where an edge is shared by 3 faces. Using a set as the value type does address this.

PS: Why do we need an unordered_map<size_t, set> instead of a simpler vector<set>?

Choose a reason for hiding this comment

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

yeah, vector would work. I will update


for(int idx = 0; idx < tris_cpu.GetLength(); idx++){
auto triangle_tensor = tris_cpu[idx];
auto *triangle = triangle_tensor.GetDataPtr<int>();
insertEdge(triangle[0], triangle[1]);
insertEdge(triangle[1], triangle[2]);
insertEdge(triangle[2], triangle[0]);
}

int num_vertices = GetVertexPositions().GetLength();
core::Tensor adjst = core::Tensor::Zeros({num_vertices+1}, core::Dtype::Int64);

int num_edges = tris_cpu.GetLength() * 3;
core::Tensor adjv = core::Tensor::Zeros({num_edges}, core::Dtype::Int64);

long prev_nnz = 0;
for(int idx = 1; idx <= num_vertices; idx++){
adjst[idx] = adjst[idx-1] + static_cast<long>(adjacencyList[idx-1].size());

int i = 0;
for(auto x : adjacencyList[idx-1]){
adjv[ prev_nnz + i] = x;
i++;
}
prev_nnz += static_cast<long>(adjacencyList[idx-1].size());
}
return std::make_pair(adjv, adjst);
}

std::string TriangleMesh::ToString() const {
size_t num_vertices = 0;
Expand Down
39 changes: 38 additions & 1 deletion cpp/open3d/t/geometry/TriangleMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@

#pragma once

#include <list>
#include <map>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

#include "open3d/core/Tensor.h"
#include "open3d/core/TensorCheck.h"
Expand Down Expand Up @@ -243,6 +248,33 @@ class TriangleMesh : public Geometry, public DrawableGeometry {
return GetTriangleAttr("colors");
}

/// Getter for mesh_attr_ TensorMap.
std::map<std::string, core::Tensor> &GetMeshAttr() { return mesh_attr_; }

/// Get mesh attributes in mesh_attr_. Throws exception if the
/// attribute does not exist.
///
/// \param key Attribute name.
core::Tensor &GetMeshAttr(const std::string &key) {
return mesh_attr_.at(key);
}

/// Removes mesh attribute by key value. Throws warning if attribute key
/// does not exists.
///
/// \param key Attribute name.
void RemoveMeshAttr(const std::string &key) { mesh_attr_.erase(key); }

/// Set mesh attributes. If the attribute key already exists, its value
/// will be overwritten, otherwise, the new key will be created.
///
/// \param key Attribute name.
/// \param value A tensor.
void SetMeshAttr(const std::string &key, const core::Tensor &value) {
core::AssertTensorDevice(value, device_);
mesh_attr_[key] = value;
}

/// Set vertex attributes. If the attribute key already exists, its value
/// will be overwritten, otherwise, the new key will be created.
///
Expand Down Expand Up @@ -828,6 +860,8 @@ class TriangleMesh : public Geometry, public DrawableGeometry {
TriangleMesh BooleanDifference(const TriangleMesh &mesh,
double tolerance = 1e-6) const;

std::pair<core::Tensor, core::Tensor> ComputeAdjacencyList();

/// Create an axis-aligned bounding box from vertex attribute "positions".
AxisAlignedBoundingBox GetAxisAlignedBoundingBox() const;

Expand Down Expand Up @@ -1065,6 +1099,9 @@ class TriangleMesh : public Geometry, public DrawableGeometry {
core::Device device_ = core::Device("CPU:0");
TensorMap vertex_attr_;
TensorMap triangle_attr_;
/// Extra mesh attributes. There are no constraints (size synchronization or
/// primary key), so TensorMap is not appropriate.
std::map<std::string, core::Tensor> mesh_attr_;
};

} // namespace geometry
Expand Down
5 changes: 4 additions & 1 deletion cpp/pybind/t/geometry/trianglemesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -734,11 +734,14 @@ This function always uses the CPU device.
o3d.visualization.draw([{'name': 'difference', 'geometry': ans}])
)");

triangle_mesh.def("compute_adjacency_list", &TriangleMesh::ComputeAdjacencyList,
"Return Mesh Adjacency Matrix in CSR format using Triangle indices attribute.");

Copy link
Member

Choose a reason for hiding this comment

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

Describe returned pair explicitly for users that don't know CSR format.

Choose a reason for hiding this comment

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

I am guessing that adjst() would be added to vertex attribute and adjv() to triangle attributes ?

However the check HasVertexAttribute will fail because it checks the length of attribute is equal to the number of vertices, however as per CSR format the length of adjv is "num_vertices + 1"

Copy link
Member

Choose a reason for hiding this comment

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

Good point. We need a different container for storing attributes for the TriangleMesh as a whole. I've added a new container to TriangleMesh for this purpose. Can you use that? I'll update the python bindings / tests for the new container later.

triangle_mesh.def("get_axis_aligned_bounding_box",
&TriangleMesh::GetAxisAlignedBoundingBox,
"Create an axis-aligned bounding box from vertex "
"attribute 'positions'.");

triangle_mesh.def("get_oriented_bounding_box",
&TriangleMesh::GetOrientedBoundingBox,
"Create an oriented bounding box from vertex attribute "
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 @@ -25,6 +25,70 @@ INSTANTIATE_TEST_SUITE_P(TriangleMesh,
TriangleMeshPermuteDevices,
testing::ValuesIn(PermuteDevices::TestCases()));

TEST_P(TriangleMeshPermuteDevices, ComputeAdjacencyList_emptyMesh) {
//Test the interface and the case when mesh is empty

t::geometry::TriangleMesh empty_mesh;

auto listCSR = empty_mesh.ComputeAdjacencyList();

core::Tensor adjacent_vertex = listCSR.first;
core::Tensor adjacent_index_start = listCSR.second;
EXPECT_TRUE(adjacent_vertex.GetLength() == 0);
EXPECT_TRUE(adjacent_index_start.GetLength() == 0);
}

TEST_P(TriangleMeshPermuteDevices, ComputeAdjacencyList_matchValues) {
//Test the actual values computed in the function

core::Device device = GetParam();
core::Dtype float_dtype_custom = core::Float64;
core::Dtype int_dtype_custom = core::Int32;

t::geometry::TriangleMesh mesh =
t::geometry::TriangleMesh::CreateTetrahedron(
2, float_dtype_custom, int_dtype_custom, device);

auto listCSR = mesh.ComputeAdjacencyList();
core::Tensor adjv = listCSR.first;
core::Tensor adjst = listCSR.second;

EXPECT_TRUE( adjv.GetLength() > 0);
EXPECT_TRUE( adjst.GetLength() > 0);

core::Tensor csr_col = core::Tensor::Init<int64_t>(
{1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2}, device);

core::Tensor csr_row_idx = core::Tensor::Init<int64_t>(
{0, 3, 6, 9, 12}, device);

EXPECT_EQ(adjv.GetLength(), csr_col.GetLength());
EXPECT_EQ(adjst.GetLength(), csr_row_idx.GetLength());
EXPECT_TRUE(adjv.AllEqual(csr_col));
EXPECT_TRUE(adjst.AllEqual(csr_row_idx));
}

TEST_P(TriangleMeshPermuteDevices, ComputeAdjacencyList_expectedBehaviour) {
//On a larger mesh, test the interface and few expected properties

core::Device device = GetParam();
core::Dtype float_dtype_custom = core::Float64;
core::Dtype int_dtype_custom = core::Int32;

t::geometry::TriangleMesh mesh =
t::geometry::TriangleMesh::CreateIcosahedron(
2, float_dtype_custom, int_dtype_custom, device);


auto listCSR = mesh.ComputeAdjacencyList();
core::Tensor adjv = listCSR.first;
core::Tensor adjst = listCSR.second;

EXPECT_TRUE( adjv.GetLength() > 0);
EXPECT_TRUE( adjst.GetLength() > 0);
EXPECT_EQ(adjst.ToFlatVector<int64_t>()[adjst.GetLength()-1], adjv.GetLength());
}
Copy link
Member

Choose a reason for hiding this comment

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

You can also add a test for Euler characteristic (n_faces + n_vertices - 2 = n_edges).
face = triangle, n_edges = length of csr_col.


TEST_P(TriangleMeshPermuteDevices, DefaultConstructor) {
t::geometry::TriangleMesh mesh;

Expand Down
60 changes: 60 additions & 0 deletions python/test/t/geometry/test_trianglemesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,66 @@ def test_create_octahedron(device):
assert octahedron_custom.vertex.positions.allclose(vertex_positions_custom)
assert octahedron_custom.triangle.indices.allclose(triangle_indices_custom)

@pytest.mark.parametrize("device", list_devices())
def test_compute_adjacency_list(device):
# Test with custom parameters.
mesh = o3d.t.geometry.TriangleMesh.create_icosahedron(
2, o3c.float64, o3c.int32, device)
adjv, adjst = mesh.compute_adjacency_list()

# Check the number of edges at the end of row_index as per CRS format
assert adjst[-1] == len(adjv)

num_vertices = len(adjst)-1
# Create a adjacency matrix
adjacencyMatrix = np.zeros((num_vertices, num_vertices))

for s in range(num_vertices):
start = adjst[s].item()
end = adjst[s+1].item()
for v in range(start, end):
t = adjv[v].item()
adjacencyMatrix[s,t] = 1

# Number of edges
assert len(adjv) == adjacencyMatrix.sum()

# Adjacency Matrix should be symmetric
assert np.array_equal(adjacencyMatrix, adjacencyMatrix.T)

#Triangle faces from computed adjacency matrix should match
actual_indices = [
[0, 1, 4],
[0, 1, 5],
[0, 4, 8],
[0, 5, 11],
[0, 8, 11],
[1, 4, 9],
[1, 5, 10],
[1, 9, 10],
[2, 3, 6],
[2, 3, 7],
[2, 6, 10],
[2, 7, 9],
[2, 9, 10],
[3, 6, 11],
[3, 7, 8],
[3, 8, 11],
[4, 7, 8],
[4, 7, 9],
[5, 6, 10],
[5, 6, 11]
]
Copy link
Member

Choose a reason for hiding this comment

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

You can get this from mesh.triangle.indices.numpy() May need to be lexicographically sorted. (np.lexsort)


computed_triangles = []
for i in range(num_vertices):
for j in range(i+1, num_vertices):
for k in range(j+1, num_vertices):
if (adjacencyMatrix[i,j] + adjacencyMatrix[j,k] + adjacencyMatrix[k,i] == 3):
computed_triangles.append([i,j,k])

assert len(computed_triangles) == len(actual_indices)
assert np.array_equal(np.array(actual_indices,int), np.array(computed_triangles,int))

@pytest.mark.parametrize("device", list_devices())
def test_create_icosahedron(device):
Expand Down
Loading