Barycentric Coordinate for Surface Sampling

To convert a mesh into a point cloud, one has to sample points that can uniformly cover the surface. To do so, one must choose the number of samples proportional to the area of a face (polygon).

First, we iterate through all polygons, splitting them into triangles since measuring the area of a triangle is much easier than computing the area of a polygon. Then, for each triangle, we compute its area and store it in an array. Next, we select a triangle with probability proportional to its area.

Sampling uniformly on a triangle requires a trick: Barycentric coordinate system.

Barycentric Coordinate System

The Barycentric coordinate is a way to define a point on a $n$ dimensional simplex using $n$ coordinates. You define $n$ weights $[a_1 ,…, a_n]$ to denote a point $p= a_1 \mathbf{x}_1 + \cdots + a_n \mathbf{x}_n$. It is just a convex hull of the simplex.

Then, for each selected triangle (let’s denote the vertices of the triangle as $A, B, C$ ), we sample a point on its surface by generating two random numbers, $r_1$ and $r_2$ between 0 and 1, and compute the coordinate using the equation on the next section.

Computing the Area of Faces

First, you have to convert all polygons into triangles. You can easily do this in many commercial mesh converters.

Let the vertices of a triangle $A, B, C$ in 3D coorindate as $\mathbf{a}, \mathbf{b}$ and $\mathbf{c}$. Then, the area of the triangle formed by the three vectors is

\[\frac12 \| (\mathbf{a} - \mathbf{c}) \times (\mathbf{b} - \mathbf{c}) \|_2\]

Setting the number of samples for each face

Just sum up all the triangle areas and convert them into a probability distribution. If you multiply the number of points you want by the sample to the distribution, you will get the number of samples per face.

Sampling points using the Barycentric coordinate

For two random variables $r_1, r_2$ uniformly distributed from 0 to 1, we sample a new point $d$ as follows.

\[\mathbf{d} = (1 - \sqrt{r_1})\mathbf{a} + \sqrt{r_1} (1 - r_2) \mathbf{b} + \sqrt{r_1} r_2 \mathbf{c}\]

Intuitively, $r_1$ sets the percentage from vertex $a$ to the opposing edge. Taking the square-root of $r_1$ gives a uniform random point with respect to surface area.

def sample_faces(vertices, faces, n_samples=10**4):
  """
  Samples point cloud on the surface of the model defined as vectices and
  faces. This function uses vectorized operations so fast at the cost of some
  memory.

  Parameters:
    vertices  - n x 3 matrix
    faces     - n x 3 matrix
    n_samples - positive integer

  Return:
    vertices - point cloud

  Reference :
    [1] Barycentric coordinate system

    \begin{align}
      P = (1 - \sqrt{r_1})A + \sqrt{r_1} (1 - r_2) B + \sqrt{r_1} r_2 C
    \end{align}
  """
  vec_cross = np.cross(vertices[faces[:, 0], :] - vertices[faces[:, 2], :],
                       vertices[faces[:, 1], :] - vertices[faces[:, 2], :])
  face_areas = np.sqrt(np.sum(vec_cross ** 2, 1))
  face_areas = face_areas / np.sum(face_areas)

  # Sample exactly n_samples. First, oversample points and remove redundant
  # Error fix by Yangyan (yangyan.lee@gmail.com) 2017-Aug-7
  n_samples_per_face = np.ceil(n_samples * face_areas).astype(int)
  floor_num = np.sum(n_samples_per_face) - n_samples
  if floor_num > 0:
    indices = np.where(n_samples_per_face > 0)[0]
    floor_indices = np.random.choice(indices, floor_num, replace=True)
    n_samples_per_face[floor_indices] -= 1

  n_samples = np.sum(n_samples_per_face)

  # Create a vector that contains the face indices
  sample_face_idx = np.zeros((n_samples, ), dtype=int)
  acc = 0
  for face_idx, _n_sample in enumerate(n_samples_per_face):
    sample_face_idx[acc: acc + _n_sample] = face_idx
    acc += _n_sample

  r = np.random.rand(n_samples, 2);
  A = vertices[faces[sample_face_idx, 0], :]
  B = vertices[faces[sample_face_idx, 1], :]
  C = vertices[faces[sample_face_idx, 2], :]
  P = (1 - np.sqrt(r[:,0:1])) * A + np.sqrt(r[:,0:1]) * (1 - r[:,1:]) * B + \
      np.sqrt(r[:,0:1]) * r[:,1:] * C
  return P

Leave a Comment