Skip to content

Commit

Permalink
Merge close vertices (#1189)
Browse files Browse the repository at this point in the history
* merge close vertices

* handling triangle_normals_ and added unit test
  • Loading branch information
griegler authored and yxlao committed Sep 19, 2019
1 parent 6590e86 commit aae0733
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 73 deletions.
142 changes: 69 additions & 73 deletions examples/Python/Basic/meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,14 @@ def apply_noise(mesh, noise):
def triangle():
mesh = o3d.geometry.TriangleMesh()
mesh.vertices = o3d.utility.Vector3dVector(
np.array([(np.sqrt(8 / 9), 0, -1 / 3),
(-np.sqrt(2 / 9), np.sqrt(2 / 3), -1 / 3),
(-np.sqrt(2 / 9), -np.sqrt(2 / 3), -1 / 3)],
dtype=np.float32))
np.array(
[
(np.sqrt(8 / 9), 0, -1 / 3),
(-np.sqrt(2 / 9), np.sqrt(2 / 3), -1 / 3),
(-np.sqrt(2 / 9), -np.sqrt(2 / 3), -1 / 3),
],
dtype=np.float32,
))
mesh.triangles = o3d.utility.Vector3iVector(np.array([[0, 1, 2]]))
mesh.compute_vertex_normals()
return mesh
Expand Down Expand Up @@ -66,11 +70,28 @@ def non_manifold_edge():


def non_manifold_vertex():
verts = np.array([[-1, 0, -1], [1, 0, -1], [0, 1, -1], [0, 0, 0],
[-1, 0, 1], [1, 0, 1], [0, 1, 1]],
dtype=np.float64)
triangles = np.array([[0, 1, 2], [0, 1, 3], [1, 2, 3], [2, 0, 3], [4, 5, 6],
[4, 5, 3], [5, 6, 3], [4, 6, 3]])
verts = np.array(
[
[-1, 0, -1],
[1, 0, -1],
[0, 1, -1],
[0, 0, 0],
[-1, 0, 1],
[1, 0, 1],
[0, 1, 1],
],
dtype=np.float64,
)
triangles = np.array([
[0, 1, 2],
[0, 1, 3],
[1, 2, 3],
[2, 0, 3],
[4, 5, 6],
[4, 5, 3],
[5, 6, 3],
[4, 6, 3],
])
mesh = o3d.geometry.TriangleMesh()
mesh.vertices = o3d.utility.Vector3dVector(verts)
mesh.triangles = o3d.utility.Vector3iVector(triangles)
Expand Down Expand Up @@ -103,48 +124,50 @@ def _relative_path(path):


def knot():
mesh = o3d.io.read_triangle_mesh(_relative_path('../../TestData/knot.ply'))
mesh = o3d.io.read_triangle_mesh(_relative_path("../../TestData/knot.ply"))
mesh.compute_vertex_normals()
return mesh


def bathtub():
mesh = o3d.io.read_triangle_mesh(
_relative_path('../../TestData/bathtub_0154.ply'))
_relative_path("../../TestData/bathtub_0154.ply"))
mesh.compute_vertex_normals()
return mesh


def armadillo():
armadillo_path = _relative_path('../../TestData/Armadillo.ply')
armadillo_path = _relative_path("../../TestData/Armadillo.ply")
if not os.path.exists(armadillo_path):
print('downloading armadillo mesh')
url = 'http://graphics.stanford.edu/pub/3Dscanrep/armadillo/Armadillo.ply.gz'
urllib.request.urlretrieve(url, armadillo_path + '.gz')
print('extract armadillo mesh')
with gzip.open(armadillo_path + '.gz', 'rb') as fin:
with open(armadillo_path, 'wb') as fout:
print("downloading armadillo mesh")
url = "http://graphics.stanford.edu/pub/3Dscanrep/armadillo/Armadillo.ply.gz"
urllib.request.urlretrieve(url, armadillo_path + ".gz")
print("extract armadillo mesh")
with gzip.open(armadillo_path + ".gz", "rb") as fin:
with open(armadillo_path, "wb") as fout:
shutil.copyfileobj(fin, fout)
os.remove(armadillo_path + '.gz')
os.remove(armadillo_path + ".gz")
mesh = o3d.io.read_triangle_mesh(armadillo_path)
mesh.compute_vertex_normals()
return mesh


def bunny():
bunny_path = _relative_path('../../TestData/Bunny.ply')
bunny_path = _relative_path("../../TestData/Bunny.ply")
if not os.path.exists(bunny_path):
print('downloading bunny mesh')
url = 'http://graphics.stanford.edu/pub/3Dscanrep/bunny.tar.gz'
urllib.request.urlretrieve(url, bunny_path + '.tar.gz')
print('extract bunny mesh')
with tarfile.open(bunny_path + '.tar.gz') as tar:
print("downloading bunny mesh")
url = "http://graphics.stanford.edu/pub/3Dscanrep/bunny.tar.gz"
urllib.request.urlretrieve(url, bunny_path + ".tar.gz")
print("extract bunny mesh")
with tarfile.open(bunny_path + ".tar.gz") as tar:
tar.extractall(path=os.path.dirname(bunny_path))
shutil.move(
os.path.join(os.path.dirname(bunny_path), 'bunny', 'reconstruction',
'bun_zipper.ply'), bunny_path)
os.remove(bunny_path + '.tar.gz')
shutil.rmtree(os.path.join(os.path.dirname(bunny_path), 'bunny'))
os.path.join(os.path.dirname(bunny_path), "bunny", "reconstruction",
"bun_zipper.ply"),
bunny_path,
)
os.remove(bunny_path + ".tar.gz")
shutil.rmtree(os.path.join(os.path.dirname(bunny_path), "bunny"))
mesh = o3d.io.read_triangle_mesh(bunny_path)
mesh.compute_vertex_normals()
return mesh
Expand All @@ -158,47 +181,20 @@ def center_and_scale(mesh):
return mesh


if __name__ == '__main__':

def process(mesh):
mesh.compute_vertex_normals()
mesh = center_and_scale(mesh)
return mesh

print('visualize')
print(' tetrahedron, octahedron, icosahedron')
print(' torus, moebius strip one twist, moebius strip two twists')
d = 1.5
geoms = [
process(o3d.geometry.TriangleMesh.create_tetrahedron()).translate(
(-d, 0, 0)),
process(o3d.geometry.TriangleMesh.create_octahedron()).translate(
(0, 0, 0)),
process(o3d.geometry.TriangleMesh.create_icosahedron()).translate(
(d, 0, 0)),
process(o3d.geometry.TriangleMesh.create_torus()).translate(
(-d, -d, 0)),
process(o3d.geometry.TriangleMesh.create_moebius(twists=1)).translate(
(0, -d, 0)),
process(o3d.geometry.TriangleMesh.create_moebius(twists=2)).translate(
(d, -d, 0)),
]

vis = o3d.visualization.Visualizer()
vis.create_window()
vis.get_render_option().mesh_show_back_face = True
for geom in geoms:
vis.add_geometry(geom)

scales = [0.995 for _ in range(100)] + [1 / 0.995 for _ in range(100)]
axisangles = [(0.2 / np.sqrt(2), 0.2 / np.sqrt(2), 0) for _ in range(200)]

for scale, aa in zip(scales, axisangles):
for geom in geoms:
geom.scale(scale).rotate(aa,
center=True,
type=o3d.geometry.RotationType.AxisAngle)
vis.update_geometry()
vis.poll_events()
vis.update_renderer()
time.sleep(0.05)
def print_mesh_for_cpp(mesh, prefix=""):

def _print(prefix, values, fmt):
if values.shape[0] > 0:
print(f"{prefix} = {{")
print(",\n".join([
f" {{{v[0]:{fmt}}, {v[1]:{fmt}}, {v[2]:{fmt}}}}"
for v in values
]))
print(f"}};")

_print(f"{prefix}vertices_", np.asarray(mesh.vertices), ".6f")
_print(f"{prefix}vertex_normals_", np.asarray(mesh.vertex_normals), ".6f")
_print(f"{prefix}vertex_colors_", np.asarray(mesh.vertex_colors), ".6f")
_print(f"{prefix}triangles_", np.asarray(mesh.triangles), "d")
_print(f"{prefix}triangle_normals_", np.asarray(mesh.triangle_normals),
".6f")
80 changes: 80 additions & 0 deletions src/Open3D/Geometry/TriangleMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,86 @@ TriangleMesh &TriangleMesh::RemoveNonManifoldEdges() {
return *this;
}

TriangleMesh &TriangleMesh::MergeCloseVertices(double eps) {
KDTreeFlann kdtree(*this);
// precompute all neighbours
utility::LogDebug("Precompute Neighbours\n");
std::vector<std::vector<int>> nbs(vertices_.size());
#ifdef _OPENMP
#pragma omp parallel for schedule(static)
#endif
for (int idx = 0; idx < int(vertices_.size()); ++idx) {
std::vector<double> dists2;
kdtree.SearchRadius(vertices_[idx], eps, nbs[idx], dists2);
}
utility::LogDebug("Done Precompute Neighbours\n");

bool has_vertex_normals = HasVertexNormals();
bool has_vertex_colors = HasVertexColors();
std::vector<Eigen::Vector3d> new_vertices;
std::vector<Eigen::Vector3d> new_vertex_normals;
std::vector<Eigen::Vector3d> new_vertex_colors;
std::unordered_map<int, int> new_vert_mapping;
for (int vidx = 0; vidx < int(vertices_.size()); ++vidx) {
if (new_vert_mapping.count(vidx) > 0) {
continue;
}

int new_vidx = int(new_vertices.size());
new_vert_mapping[vidx] = new_vidx;

Eigen::Vector3d vertex = vertices_[vidx];
Eigen::Vector3d normal;
if (has_vertex_normals) {
normal = vertex_normals_[vidx];
}
Eigen::Vector3d color;
if (has_vertex_colors) {
color = vertex_colors_[vidx];
}
int n = 1;
for (int nb : nbs[vidx]) {
if (vidx == nb || new_vert_mapping.count(nb) > 0) {
continue;
}
vertex += vertices_[nb];
if (has_vertex_normals) {
normal += vertex_normals_[nb];
}
if (has_vertex_colors) {
color += vertex_colors_[nb];
}
new_vert_mapping[nb] = new_vidx;
n += 1;
}
new_vertices.push_back(vertex / n);
if (has_vertex_normals) {
new_vertex_normals.push_back(normal / n);
}
if (has_vertex_colors) {
new_vertex_colors.push_back(color / n);
}
}
utility::LogDebug("Merged {} vertices\n",
vertices_.size() - new_vertices.size());

std::swap(vertices_, new_vertices);
std::swap(vertex_normals_, new_vertex_normals);
std::swap(vertex_colors_, new_vertex_colors);

for (auto &triangle : triangles_) {
triangle(0) = new_vert_mapping[triangle(0)];
triangle(1) = new_vert_mapping[triangle(1)];
triangle(2) = new_vert_mapping[triangle(2)];
}

if (HasTriangleNormals()) {
ComputeTriangleNormals();
}

return *this;
}

template <typename F>
bool OrientTriangleHelper(const std::vector<Eigen::Vector3i> &triangles,
F &swap) {
Expand Down
6 changes: 6 additions & 0 deletions src/Open3D/Geometry/TriangleMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ class TriangleMesh : public MeshBase {
/// edge until the number of adjacent triangles to the edge is `<= 2`.
TriangleMesh &RemoveNonManifoldEdges();

/// Function that will merge close by vertices to a single one. The vertex
/// position, normal and color will be the average of the vertices. The
/// parameter \param eps defines the maximum distance of close by vertices.
/// This function might help to close triangle soups.
TriangleMesh &MergeCloseVertices(double eps);

/// Function to sharpen triangle mesh. The output value ($v_o$) is the
/// input value ($v_i$) plus \param strength times the input value minus
/// the sum of he adjacent values.
Expand Down
14 changes: 14 additions & 0 deletions src/Python/geometry/trianglemesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ void pybind_trianglemesh(py::module &m) {
"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`.")
.def("merge_close_vertices",
&geometry::TriangleMesh::MergeCloseVertices,
"Function that will merge close by vertices to a single one. "
"The vertex position, "
"normal and color will be the average of the vertices. The "
"parameter eps "
"defines the maximum distance of close by vertices. This "
"function might help to "
"close triangle soups.",
"eps"_a)
.def("filter_sharpen", &geometry::TriangleMesh::FilterSharpen,
"Function to sharpen triangle mesh. The output value "
"(:math:`v_o`) is the input value (:math:`v_i`) plus strength "
Expand Down Expand Up @@ -417,6 +427,10 @@ void pybind_trianglemesh(py::module &m) {
"remove_degenerate_triangles");
docstring::ClassMethodDocInject(m, "TriangleMesh",
"remove_non_manifold_edges");
docstring::ClassMethodDocInject(
m, "TriangleMesh", "merge_close_vertices",
{{"eps",
"Parameter that defines the distance between close vertices."}});
docstring::ClassMethodDocInject(
m, "TriangleMesh", "filter_sharpen",
{{"number_of_iterations",
Expand Down
65 changes: 65 additions & 0 deletions src/UnitTest/Geometry/TriangleMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ using namespace open3d;
using namespace std;
using namespace unit_test;

void ExpectEQ(const open3d::geometry::TriangleMesh& mesh0,
const open3d::geometry::TriangleMesh& mesh1) {
ExpectEQ(mesh0.vertices_, mesh1.vertices_);
ExpectEQ(mesh0.vertex_normals_, mesh1.vertex_normals_);
ExpectEQ(mesh0.vertex_colors_, mesh1.vertex_colors_);
ExpectEQ(mesh0.triangles_, mesh1.triangles_);
ExpectEQ(mesh0.triangle_normals_, mesh1.triangle_normals_);
}

// ----------------------------------------------------------------------------
//
// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -668,6 +677,62 @@ TEST(TriangleMesh, Purge) {
ExpectEQ(ref_triangle_normals, tm.triangle_normals_);
}

// ----------------------------------------------------------------------------
//
// ----------------------------------------------------------------------------
TEST(TriangleMesh, MergeCloseVertices) {
geometry::TriangleMesh mesh;
mesh.vertices_ = {{0.000000, 0.000000, 0.000000},
{0.000000, 0.200000, 0.000000},
{1.000000, 0.200000, 0.000000},
{1.000000, 0.000000, 0.000000}};
mesh.vertex_normals_ = {{0.000000, 0.000000, 1.000000},
{0.000000, 0.000000, 1.000000},
{0.000000, 0.000000, 1.000000},
{0.000000, 0.000000, 1.000000}};
mesh.triangles_ = {{0, 2, 1}, {2, 0, 3}};
mesh.triangle_normals_ = {{0.000000, 0.000000, 1.000000},
{0.000000, 0.000000, 1.000000}};

geometry::TriangleMesh ref;
ref.vertices_ = {{0.000000, 0.100000, 0.000000},
{1.000000, 0.100000, 0.000000}};
ref.vertex_normals_ = {{0.000000, 0.000000, 1.000000},
{0.000000, 0.000000, 1.000000}};
ref.triangles_ = {{0, 1, 0}, {1, 0, 1}};
ref.triangle_normals_ = {{0.000000, 0.000000, 0.000000},
{0.000000, 0.000000, -0.000000}};

mesh.MergeCloseVertices(1);
ExpectEQ(mesh, ref);

mesh.vertices_ = {{0.000000, 0.000000, 0.000000},
{0.000000, 0.200000, 0.000000},
{1.000000, 0.200000, 0.000000},
{1.000000, 0.000000, 0.000000}};
mesh.vertex_normals_ = {{0.000000, 0.000000, 1.000000},
{0.000000, 0.000000, 1.000000},
{0.000000, 0.000000, 1.000000},
{0.000000, 0.000000, 1.000000}};
mesh.triangles_ = {{0, 2, 1}, {2, 0, 3}};
mesh.triangle_normals_ = {{0.000000, 0.000000, 1.000000},
{0.000000, 0.000000, 1.000000}};
ref.vertices_ = {{0.000000, 0.000000, 0.000000},
{0.000000, 0.200000, 0.000000},
{1.000000, 0.200000, 0.000000},
{1.000000, 0.000000, 0.000000}};
ref.vertex_normals_ = {{0.000000, 0.000000, 1.000000},
{0.000000, 0.000000, 1.000000},
{0.000000, 0.000000, 1.000000},
{0.000000, 0.000000, 1.000000}};
ref.triangles_ = {{0, 2, 1}, {2, 0, 3}};
ref.triangle_normals_ = {{0.000000, 0.000000, 1.000000},
{0.000000, 0.000000, 1.000000}};

mesh.MergeCloseVertices(0.1);
ExpectEQ(mesh, ref);
}

// ----------------------------------------------------------------------------
//
// ----------------------------------------------------------------------------
Expand Down

0 comments on commit aae0733

Please sign in to comment.