Skip to content

Commit

Permalink
Clean up normal estimation docs. Add mesh face orientation
Browse files Browse the repository at this point in the history
  • Loading branch information
fwilliams committed Aug 19, 2022
1 parent e7de1e1 commit 2e2b86e
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 10 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ npe_add_module(_pcu_internal
${CMAKE_CURRENT_SOURCE_DIR}/src/remove_unreferenced_mesh_vertices.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/face_areas.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/fast_winding_numbers.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/orient_mesh_faces.cpp
EXTRA_MODULE_FUNCTIONS
hack_extra_bindings
hack_extra_ray_mesh_bindings
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ The following dependencies are required to install with `pip`:
- [Estimating normals from a point cloud](#estimating-normals-from-a-point-cloud)
- [Computing mesh normals per vertex](#computing-mesh-normals-per-vertex)
- [Computing mesh normals per face](#computing-mesh-normals-per-face)
- [Consistently orienting faces of a mesh](#consistently-orienting-faces-of-a-mesh)
- [Approximate Wasserstein (Sinkhorn) distance between two point clouds](#approximate-wasserstein-sinkhorn-distance-between-two-point-clouds)
- [Chamfer distance between two point clouds](#chamfer-distance-between-two-point-clouds)
- [Hausdorff distance between two point clouds](#hausdorff-distance-between-two-point-clouds)
Expand Down Expand Up @@ -465,6 +466,22 @@ n = pcu.estimate_mesh_face_normals(v, f)
```


### Consistently orienting faces of a mesh
```python
import point_cloud_utils as pcu

# v is a nv by 3 NumPy array of vertices
# f is an nf by 3 NumPy array of face indexes into v
v, f = pcu.load_mesh_vf("my_model.ply")

# Re-orient faces in a mesh so they are consistent within each connected component
# f_orient is a (nf, 3)-shaped array of re-oriented faces indexes into v
# f_comp_ids is a (nf,)-shaped array of component ids for each face
# i.e. f_comp_ids[i] is the connected component id of face f[i] (and f_orient[i])
f_oriented, f_comp_ids = pcu.orient_mesh_faces(f)
```


### Approximate Wasserstein (Sinkhorn) distance between two point clouds
```python
import point_cloud_utils as pcu
Expand Down
2 changes: 1 addition & 1 deletion point_cloud_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from ._pcu_internal import sample_mesh_poisson_disk, sample_mesh_random, \
downsample_point_cloud_poisson_disk, estimate_mesh_vertex_normals, \
estimate_mesh_face_normals, \
estimate_mesh_face_normals, orient_mesh_faces, \
k_nearest_neighbors, one_sided_hausdorff_distance, \
morton_encode, morton_decode, morton_knn, \
lloyd_2d, lloyd_3d, voronoi_centroids_unit_cube, sample_mesh_lloyd, \
Expand Down
19 changes: 19 additions & 0 deletions src/common/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,25 @@ void validate_point_cloud_normals(const TV& v, const TN& n, bool allow_0=true) {
}
}

template <typename TF>
//void validate_mesh(const Eigen::MatrixBase<TV>& v, const Eigen::MatrixBase<TF>& f) {
void validate_mesh_faces(const TF& f) {
if (f.rows() == 0) {
std::stringstream ss;
ss << "Invalid input faces with zero elements: f must have shape (n, 3) where n > 0. Got f.shape =("
<< f.rows() << ", " << f.cols() << ").";
throw pybind11::value_error(ss.str());
}

if (f.cols() != 3) {
std::stringstream ss;
ss << "Only triangle inputs are supported: f must have shape (n, 3) where n > 0. Got f.shape =("
<< f.rows() << ", " << f.cols() << ").";
throw pybind11::value_error(ss.str());
}
}



template <typename TV, typename TF>
//void validate_mesh(const Eigen::MatrixBase<TV>& v, const Eigen::MatrixBase<TF>& f) {
Expand Down
18 changes: 9 additions & 9 deletions src/mesh_normals.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#include <npe.h>
#include <vcg/complex/complex.h>
#include <vcg/complex/algorithms/pointcloud_normal.h>

#include <igl/per_vertex_normals.h>
#include <igl/per_face_normals.h>

#include <string>

#include "common/common.h"
Expand All @@ -14,13 +14,13 @@ Compute vertex normals of a mesh from its vertices and faces using face area wei
Parameters
----------
v : #v by 3 Matrix of mesh vertex 3D positions
f : #f by 3 Matrix of face (triangle) indices
v : (#v, 3)-shaped NumPy array of mesh vertex 3D positions
f : (#f, 3)-shaped NumPy array of face (triangle) indices
weighting_type : Weighting type must be one of 'uniform', 'angle', or 'area' (default is 'uniform')
Returns
-------
n : list of vertex normals of shape #v by 3
n : (#v, 3)-shaped NumPy array of vertex normals (i.e. n[i] is the normal at vertex v[i])
See also
--------
Expand Down Expand Up @@ -62,12 +62,13 @@ Compute vertex normals of a mesh from its vertices and faces using face area wei

Parameters
----------
v : #v by 3 Matrix of mesh vertex 3D positions
f : #f by 3 Matrix of face (triangle) indices
v : (#v, 3)-shaped NumPy array of mesh vertex 3D positions
f : (#f, 3)-shaped NumPy array of face (triangle) indices

Returns
-------
n : list of face normals of shape #f by 3. Note that any degenerate faces will have a zero normal.
n : (#f, 3)-shaped NumPy array of face normals (i.e. n[i] is the normal at face f[i]).
Note that any degenerate faces will have a zero normal.

See also
--------
Expand All @@ -78,7 +79,6 @@ npe_function(estimate_mesh_face_normals)
npe_doc(estimate_mesh_face_normals_doc)
npe_arg(v, dense_float, dense_double)
npe_arg(f, dense_int, dense_longlong, dense_uint, dense_ulonglong)
npe_default_arg(weighting_type, std::string, std::string("uniform"))
npe_begin_code()
{
validate_mesh(v, f);
Expand Down
41 changes: 41 additions & 0 deletions src/orient_mesh_faces.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#include <npe.h>

#include <igl/bfs_orient.h>

#include <tuple>

#include "common/common.h"


const char* orient_mesh_faces_doc = R"igl_Qu8mg5v7(
Consistently orient faces of a mesh within each connected component
Parameters
----------
f : (#f, 3)-shaped NumPy array of face (triangle) indices
Returns
-------
oriented_faces : (#f, 3)-shaped NumPy array of faces which are consistently oriented
face_components : (#f,)-shaped NumPy array of connected component ids
(i.e. face_components[i] is the component id of facef[i])
See also
--------
)igl_Qu8mg5v7";
npe_function(orient_mesh_faces)
npe_doc(orient_mesh_faces_doc)
npe_arg(f, dense_int, dense_longlong, dense_uint, dense_ulonglong)
npe_default_arg(weighting_type, std::string, std::string("uniform"))
npe_begin_code()
{
validate_mesh_faces(f);

npe_Matrix_f oriented_faces;
npe_Matrix_f face_components;
igl::bfs_orient(f, oriented_faces, face_components);

return std::make_tuple(npe::move(oriented_faces), npe::move(face_components));
}
npe_end_code()
8 changes: 8 additions & 0 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,15 @@ def test_per_face_normals(self):
nf = pcu.estimate_mesh_face_normals(v, f)
self.assertEqual(nf.shape, f.shape)

def test_orient_mesh_faces(self):
import point_cloud_utils as pcu
import numpy as np

v, f = pcu.load_mesh_vf(os.path.join(self.test_path, "bunny.ply"))
f_oriented, f_comp_ids = pcu.orient_mesh_faces(f)
self.assertEqual(f_oriented.shape, f.shape)
self.assertTrue(np.all(f_oriented == f))
self.assertTrue(np.all(f_comp_ids == 0))

if __name__ == '__main__':
unittest.main()

0 comments on commit 2e2b86e

Please sign in to comment.