Skip to content

Commit

Permalink
Adding sample_mesh.py function (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
odedstein authored Sep 16, 2022
1 parent 97fd0f6 commit 2e2ceef
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 22 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,4 @@ I = in_element_aabb(queries,V,F) # This is a C++ binding
- Triangle-triangle distance and Hausdorff distance (with AABB)
- Package for conda distribution
- Add notes on every docstring mentioning libigl implementations
- `regular_square_mesh` should support different resolutions in `x` and `y` direction (sensible default when n_y is None, to n_y=n_x)
1 change: 1 addition & 0 deletions src/gpytoolbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,5 @@
from .ray_box_intersect import ray_box_intersect
from .ray_triangle_intersect import ray_triangle_intersect
from .catmull_rom_spline import catmull_rom_spline
from .random_points_on_mesh import random_points_on_mesh
from .compactly_supported_normal import compactly_supported_normal
108 changes: 108 additions & 0 deletions src/gpytoolbox/random_points_on_mesh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import numpy as np
from .doublearea import doublearea

def random_points_on_mesh(V,F,
n,
rng=np.random.default_rng(),
distribution='uniform',
return_indices=False):
"""Samples a mesh V,F according to a given distribution.
Valid meshes are polylines or triangle meshes.
Parameters
----------
V : (n_v,d) numpy array
vertex list of vertex positions
F : (m,k) numpy int array
if k==2, interpret input as ordered polyline;
if k==3 numpy int array, interpred as face index list of a triangle
mesh
rng : numpy rng, optional (default: new `np.random.default_rng()`)
which numpy random number generator to use
n : int
how many points to sample
method : string, optional (default: 'uniform')
According to which distribution to sample.
Currently, only uniform is supported.
return_indices : bool, optional (default: False)
Whether to return the indices for each element along with the
barycentric coordinates of the sampled points within each element
Returns
-------
x : (n,d) numpy array
the n sampled points
I : (n,) numpy int array
element indices where sampled points lie (if requested)
u : (n,k) numpy array
barycentric coordinates for sampled points in I
Examples
--------
TODO
"""

assert n>=0

if n==0:
if return_indices:
return np.zeros((0,), dtype=V.dtype), \
np.zeros((0,), dtype=F.dtype), \
np.zeros((0,), dtype=V.dtype)
else:
return np.zeros((0,), dtype=V.dtype)

k = F.shape[1]
if k==2:
if distribution=='uniform':
I,u = _uniform_sample_polyline(V,F,n,rng,distribution)
else:
assert False, "distribution not supported"
x = u[:,0][:,None]*V[F[I,0],:] + \
u[:,1][:,None]*V[F[I,1],:]
elif k==3:
if distribution=='uniform':
I,u = _uniform_sample_triangle_mesh(V,F,n,rng,distribution)
else:
assert False, "distribution not supported"
x = u[:,0][:,None]*V[F[I,0],:] + \
u[:,1][:,None]*V[F[I,1],:] + \
u[:,2][:,None]*V[F[I,2],:]
else:
assert False, "Only polylines and triangles supported"

if return_indices:
return x, I, u
else:
return x


def _uniform_sample_polyline(V,E,n,rng,distribution):
l = np.linalg.norm(V[E[:,1],:] - V[E[:,0],:], axis=1)
w = l / np.sum(l)

I = rng.choice(w.shape[0], size=(n,), p=w)

r = rng.uniform(size=(n,))
u = np.stack([r, 1.-r], axis=-1)

return I, u


def _uniform_sample_triangle_mesh(V,F,n,rng,distribution):
# Adapted partially from code by Justin Solomon, and math from
# https://math.stackexchange.com/questions/18686/uniform-random-point-in-triangle-in-3d

A = doublearea(V,F)
w = A / np.sum(A)

I = rng.choice(w.shape[0], size=(n,), p=w)

r = rng.uniform(size=(n,2))
r2 = r[:,0]
sqrtr = np.sqrt(r[:,1])
u = np.stack([1.-sqrtr, sqrtr * (1.-r2), r2*sqrtr], axis=-1)

return I, u
39 changes: 17 additions & 22 deletions src/gpytoolbox/random_points_on_polyline.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import numpy as np
import warnings
from .edge_indices import edge_indices
from .random_points_on_mesh import random_points_on_mesh

def random_points_on_polyline(V, n, EC=None):
"""Generate points on the edges of a polyline.
Expand All @@ -13,7 +15,7 @@ def random_points_on_polyline(V, n, EC=None):
n : int
Number of desired points
EC : numpy int array, optional (default None)
Matrix of polyline indeces into V. If None, the polyline is assumed to be open and ordered.
Matrix of polyline indices into V. If None, the polyline is assumed to be open and ordered.
Returns
-------
Expand All @@ -24,33 +26,26 @@ def random_points_on_polyline(V, n, EC=None):
See Also
--------
edge_indices.
sample_mesh
edge_indices
Examples
--------
TODO
"""

warnings.warn("random_points_on_polyline will be deprecated in gpytoolbox-0.0.3 in favour of the more general random_points_on_mesh",DeprecationWarning)

if (EC is None):
EC = edge_indices(V.shape[0],closed=False)

x,I,_ = random_points_on_mesh(V, EC, n, return_indices=True)

vecs = V[EC[:,1],:] - V[EC[:,0],:]
vecs /= np.linalg.norm(vecs, axis=1)[:,None]
J = np.array([[0., -1.], [1., 0.]])
N = vecs @ J.T

sampled_N = N[I,:]

edge_lengths = np.linalg.norm(V[EC[:,1],:] - V[EC[:,0],:],axis=1)
normalized_edge_lengths = np.cumsum(edge_lengths)/np.sum(edge_lengths)

# These random numbers will choose the segment
random_numbers = np.random.rand(n)
# These random numbers will choose where in the chosen segment
random_numbers_in_edge = np.random.rand(n)

P = np.zeros((n,2))
N = np.zeros((n,2))
for i in range(n):
# Pick the edge
edge_index = np.argmax((random_numbers[i]<=normalized_edge_lengths))
# Pick the point in the edge
P[i,:] = random_numbers_in_edge[i]*V[EC[edge_index,0],:] + (1-random_numbers_in_edge[i])*V[EC[edge_index,1],:]
#Compute normal
n = np.array([-(V[EC[edge_index,1],1] - V[EC[edge_index,0],1]),V[EC[edge_index,1],0] - V[EC[edge_index,0],0]])
N[i,:] = n/np.linalg.norm(n)

return P, N
return x, sampled_N
70 changes: 70 additions & 0 deletions test/test_random_points_on_mesh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from .context import gpytoolbox as gpy
from .context import unittest
from .context import numpy as np


class TestRandomPointsOnMesh(unittest.TestCase):
def test_is_uniform(self):

# Do not error on zero input
x = gpy.random_points_on_mesh(np.array([]), np.array([], dtype=int), n=0)
self.assertTrue(len(x)==0)

# Test 1: Straight line
V = np.array([[0.0,0.0],[1.0,1.0],[2.0,2.0]])
E = gpy.edge_indices(V.shape[0],closed=False)
n = 100000
rng = np.random.default_rng(5)
x = gpy.random_points_on_mesh(V, E, n, rng=rng)
rng = np.random.default_rng(5)
y,I,u = gpy.random_points_on_mesh(V, E, n, rng=rng, return_indices=True)

self.check_consistency(V, E, [x,y], I, u)
for d in range(2):
hist, bin_edges = np.histogram(x[:,d],bins=20, density=True)
self.assertTrue(np.std(hist)<0.01)
self.assertTrue(np.isclose(np.mean(hist),0.5,atol=1e-3))
self.assertTrue(np.isclose(np.mean(x[:,d]),1.,atol=1e-2))

# Sample grid.
V,F = gpy.regular_square_mesh(30)
n = 100000
rng = np.random.default_rng(526)
x = gpy.random_points_on_mesh(V, F, n, rng=rng)
rng = np.random.default_rng(526)
y,I,u = gpy.random_points_on_mesh(V, F, n, rng=rng, return_indices=True)

self.check_consistency(V, F, [x,y], I, u)
for d in range(2):
hist, bin_edges = np.histogram(x[:,d],bins=20, density=True)
self.assertTrue(np.std(hist)<0.01)
self.assertTrue(np.isclose(np.mean(hist),0.5,atol=1e-3))
self.assertTrue(np.isclose(np.mean(x[:,d]),0.,atol=1e-2))

# Sample cube. This time, no histogram test, just mean of output
V,F = gpy.read_mesh("test/unit_tests_data/cube.obj")
n = 100000
rng = np.random.default_rng(80)
x = gpy.random_points_on_mesh(V, F, n, rng=rng)
rng = np.random.default_rng(80)
y,I,u = gpy.random_points_on_mesh(V, F, n, rng=rng, return_indices=True)

self.check_consistency(V, F, [x,y], I, u)
for d in range(3):
self.assertTrue(np.isclose(np.mean(x[:,d]),0.,atol=1e-2))


def check_consistency(self, V, F, xs, I=None, u=None):
if len(xs)<1:
return None
for x in xs:
self.assertTrue(np.isclose(x,xs[0]).all())
if I is not None and u is not None:
y = 0.
for d in range(F.shape[1]):
y = y + u[:,d][:,None]*V[F[I,d],:]
self.assertTrue(np.isclose(y,xs[0]).all())


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

0 comments on commit 2e2ceef

Please sign in to comment.