3

Given a set of points,

points = np.random.randn(...) # n 3d points

I would like to uniformly fill the volume defined by the convex hull in which they lie by a list (np.array of shape nx3) of 3d points.

I can get the convex hull by

hull = scipy.spatial.ConvexHull(points)

What would be the fastest way to get a list of points that uniformly fills this hull's volume?

japreiss
  • 11,111
  • 2
  • 40
  • 77
Gulzar
  • 23,452
  • 27
  • 113
  • 201

3 Answers3

5
  1. Find delaunay simplices of the hull

  2. randomly sample the simplices based on their area

  3. for each simplex, find uniform distribution of sampled points using dirichelet distribution

  4. multiply the distributions with the simplices to find final points.

    import numpy as np
    from numpy.linalg import det
    from scipy.stats import dirichlet
    
    
    def dist_in_hull(points, n):
        dims = points.shape[-1]
        hull = points[ConvexHull(points).vertices]
        deln = hull[Delaunay(hull).simplices]
    
        vols = np.abs(det(deln[:, :dims, :] - deln[:, dims:, :])) / np.math.factorial(dims)    
        sample = np.random.choice(len(vols), size = n, p = vols / vols.sum())
    
        return np.einsum('ijk, ij -> ik', deln[sample], dirichlet.rvs([1]*(dims + 1), size = n))
    
    

EDIT: functionalized and extended to higher dimensions (Warning: ConvexHull only works up to 9D)

hvater
  • 3
  • 2
Daniel F
  • 13,620
  • 2
  • 29
  • 55
  • I tried your answer but it doesn't work for me (see [image](https://i.stack.imgur.com/szJaZ.png)). – Puco4 Apr 20 '21 at 15:44
  • This does not seem to be a uniform distribution, see [here](https://i.imgur.com/ZRLTU3T.png). For reference this is a slice of the first and second column of 7D points – jstm May 16 '23 at 02:30
  • 1
    Edit: Never mind, I don't believe the image i shared disproves that the sampling is uniform. I supposed such an image is to be expected for high dimensional data. For example if you uniformly sample from a sphere, then project the samples to the xy plane, there will be a higher concentration of points in the center of the circle – jstm May 16 '23 at 03:03
4

In case it helps someone, here is the code to implement Yves's answer:

  1. Draw points uniformly in the bounding box of the convex hull.
  2. Reject the points falling outside the hull.
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial import ConvexHull
from matplotlib.patches import Rectangle
from matplotlib.path import Path

#Initial positions
pos = np.random.uniform(-30, 10, (20, 2))

#Convex hull of initial positions
hull = ConvexHull( pos )

#Bounding box
bbox = [hull.min_bound, hull.max_bound]

#Hull path
hull_path = Path( hull.points[hull.vertices] )

#Draw n random points inside the convex hull
n = 10**3
rand_points = np.empty((n, 2))
for i in range(n):
    #Draw a random point in the bounding box of the convex hull
    rand_points[i] = np.array([np.random.uniform(bbox[0][0], bbox[1][0]), np.random.uniform(bbox[0][1], bbox[1][1])])

    #We check if the random point is inside the convex hull, otherwise we draw it again            
    while hull_path.contains_point(rand_points[i]) == False:
        rand_points[i] = np.array([np.random.uniform(bbox[0][0], bbox[1][0]), np.random.uniform(bbox[0][1], bbox[1][1])])

#Plot
plt.scatter(pos[:, 0],pos[:, 1], marker='o',  c='blue', alpha = 1, label ='Initial points')
for simplex in hull.simplices:
        plt.plot(hull.points[simplex, 0], hull.points[simplex, 1], '-k')
plt.gca().add_patch(Rectangle((bbox[0][0], bbox[0][1]), bbox[1][0] - bbox[0][0], bbox[1][1] - bbox[0][1],facecolor = 'None', edgecolor = 'cyan'))
plt.scatter(rand_points[:, 0],rand_points[:, 1], marker='o',  c='red', alpha = 0.31, label ='Random points inside hull')
plt.legend()
plt.savefig("fig.png", dpi = 300)

enter image description here

Puco4
  • 491
  • 5
  • 16
  • This is unvectorized and will be very slow. Can you make a vectorized version? As-is, this is not usable. – Gulzar Apr 20 '21 at 11:43
  • What do you mean with vectorize @Gulzar? I am not a programmer and I do not know python in depth, but I needed to do this algorithm and I managed to make it to work so I decided to share it. If you can point to me how can I make it better, I will change it. – Puco4 Apr 20 '21 at 11:50
  • I can't really teach everything related, but if it interests you, start here https://realpython.com/numpy-array-programming/ – Gulzar Apr 20 '21 at 12:18
  • Point being Python loops are (very) slow. Numpy vector operations are implemented in c, and have underlying optimizations of many kinds. Using them is mandatory for production grade code. – Gulzar Apr 20 '21 at 12:19
  • 2
    @Gulzar I looked a bit if I could manage to vectorize the for loop with a list comprehension, but I haven't managed because of the while loop. If someone can think of a better way, I will be happy to improve the answer. Anyway, this algorithm worked in my particular problem and I believe it may still help other people. – Puco4 Apr 20 '21 at 15:12
2

Draw points uniformly in the bounding box and reject those that aren't inside the hull. (As the hull is convex, this can be done in linear time O(F) without preprocessing, and logarithmic time O(log F) after preprocessing, projecting the faces to a plane and considering a planar subdivision).

  • Sirry, i don't understand what to do. What is F? What preprocessing? Which plane should i project which points to, and why is that enough – Gulzar Nov 29 '19 at 16:41
  • @Gulzar: F is the number of faces. You can project on any plane. Have a look at https://en.wikipedia.org/wiki/Point_location –  Nov 29 '19 at 16:46
  • Oh. Yes i have that implemented already. I thought this could be done more quickly, because actually not all points have to be classified as in/out. How can you acheive log(f)? – Gulzar Nov 29 '19 at 17:18
  • 1
    This is a bit dangerous because you may need to generate a very large amount of points. Unless you rotate your axes to match the principal axes of the hull, your hull can be arbitrarily small compared to its bounding box (imagine a hull represented by a skinny ellipse rotated around the line x=y=z) and you can end up rejecting most of your generated points. Granted, with axis transformation and 3d, your worst case rejection is pi/6 = 52%, but as your dimensionality goes higher, your worst case rejection will get progressively worse. 4d = pi^2/32 = 30%, 5d: 16%, 6d: 8% – Daniel F Dec 02 '19 at 08:02
  • @DanielF: as far as I know, 3 is not a growing number. You can of course find pathological cases such that even in 2D the efficiency is 0%. My answer is for practical purposes, though the efficient solution isn't that easy to implement and would only make sense if the number of faces warrants it. –  Dec 02 '19 at 09:03
  • 1
    @DanielF: time stability ? Please elaborate. –  Dec 02 '19 at 09:12