Skip to content

SamplingMesh

Functions to sample point cloud from the mesh

farthest_point_sample

Select points using Farthest Point Sampling (FPS).

This function selects npoint indices from an input point cloud such that each newly selected point is as far as possible (in Euclidean distance) from the already selected set. Only the first three dimensions of point are used as XYZ coordinates.

Parameters:

Name Type Description Default
point ndarray

np.ndarray Point cloud array of shape (N, D). Only the first three columns are interpreted as XYZ coordinates, so D must be at least 3.

required
npoint int

np.ndarray Number of points to sample. Must satisfy 1 <= npoint <= N.

required

Returns:

Type Description
ndarray

np.ndarray: Array of shape (npoint,) containing the indices of the sampled points (dtype int32).

Raises:

Type Description
AssertionError

If npoint is greater than the number of input points N.

Source code in src\utils.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def farthest_point_sample(point: np.ndarray, npoint: int) -> np.ndarray:
    """Select points using Farthest Point Sampling (FPS).

    This function selects `npoint` indices from an input point cloud such that
    each newly selected point is as far as possible (in Euclidean distance)
    from the already selected set. Only the first three dimensions of
    `point` are used as XYZ coordinates.

    Args:
        point: np.ndarray
            Point cloud array of shape (N, D). Only the first three columns
            are interpreted as XYZ coordinates, so D must be at least 3.
        npoint: np.ndarray
            Number of points to sample. Must satisfy 1 <= npoint <= N.

    Returns:
        np.ndarray:
            Array of shape (npoint,) containing the indices of the
            sampled points (dtype int32).

    Raises:
        AssertionError: If `npoint` is greater than the number of input points N.
    """
    N, D = point.shape
    assert N >= npoint
    xyz = point[:, :3]
    centroids = np.zeros((npoint,))
    distance = np.ones((N,)) * 1e10
    farthest = np.random.randint(0, N)

    for i in range(npoint):
        centroids[i] = farthest
        centroid = xyz[farthest, :]
        dist = np.sum((xyz - centroid) ** 2, -1)
        mask = dist < distance
        distance[mask] = dist[mask]
        farthest = np.argmax(distance, -1)

    return centroids.astype(np.int32)

View source on GitHub

area_and_normal

Compute per-face normals and areas for a triangular mesh.

Given mesh vertices and triangular faces, this function computes the outward face normals (unit vectors) and the corresponding face areas. Faces with zero area (degenerate triangles) receive a zero normal.

Parameters:

Name Type Description Default
vertices ndarray

np.ndarray Array of shape (N, 3) containing vertex coordinates (XYZ).

required
faces ndarray

np.ndarray Array of shape (M, 3) containing vertex indices for each triangular face. Each row is a triplet of integer indices into vertices.

required

Returns:

Type Description
tuple[ndarray, ndarray]

tuple[np.ndarray, np.ndarray]: - face_normals: Array of shape (M, 3) with unit normal vectors for each face. Degenerate faces have a zero vector. - face_areas: Array of shape (M,) with the area of each face.

Source code in src\utils.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def area_and_normal(vertices: np.ndarray, faces: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
    """Compute per-face normals and areas for a triangular mesh.

    Given mesh vertices and triangular faces, this function computes the
    outward face normals (unit vectors) and the corresponding face areas.
    Faces with zero area (degenerate triangles) receive a zero normal.

    Args:
        vertices: np.ndarray
            Array of shape (N, 3) containing vertex coordinates (XYZ).
        faces: np.ndarray
            Array of shape (M, 3) containing vertex indices for each
            triangular face. Each row is a triplet of integer indices into ``vertices``.

    Returns:
        tuple[np.ndarray, np.ndarray]:
            - face_normals: Array of shape (M, 3) with unit normal vectors
              for each face. Degenerate faces have a zero vector.
            - face_areas: Array of shape (M,) with the area of each face.
    """
    cross_product = np.cross(vertices[faces[:, 1]] - vertices[faces[:, 0]],
                             vertices[faces[:, 2]] - vertices[faces[:, 1]])   # [M, 3]
    cross_product_normal = np.sqrt(np.sum(cross_product ** 2, axis=1))        # [M, ]
    cross_product_normal_broadcast = cross_product_normal[:, np.newaxis]      # [M, 1]
    # if cross product normal is 0, the result is zero
    face_normals = np.divide(cross_product, cross_product_normal_broadcast,
                             out=np.zeros_like(cross_product), where=cross_product_normal_broadcast != 0)   # [M ,3]
    face_areas = cross_product_normal * 0.5   # [M, ]
    return face_normals, face_areas

sample_points_with_normal_features

Sample points on a mesh surface with associated face normals.

Points are sampled on the triangular mesh defined by vertices and faces. Triangles are chosen with probability proportional to their surface area, and points are sampled uniformly within each selected triangle using random barycentric coordinates. The function returns both the sampled 3D points and the corresponding per-point normals, taken from the face normals.

Parameters:

Name Type Description Default
vertices ndarray

Array of shape (V, 3) containing the mesh vertex coordinates (XYZ).

required
faces ndarray

Array of shape (F, 3) containing vertex indices for each triangular face. Each row is a triplet of integer indices into vertices.

required
face_normals ndarray

Array of shape (F, 3) with the normal vector for each face, typically unit-length.

required
face_areas ndarray

Array of shape (F,) with the area of each face.

required
n_points int

Number of points to sample on the mesh surface.

required

Returns:

Type Description
tuple[ndarray, ndarray]

tuple[np.ndarray, np.ndarray]: - sampled_points: Array of shape (n_points, 3) with the sampled XYZ coordinates on the mesh surface. - sampled_normals: Array of shape (n_points, 3) with the corresponding normal vectors (one per sampled point), copied from the selected face normals.

Source code in src\utils.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def sample_points_with_normal_features(vertices: np.ndarray, faces: np.ndarray, face_normals: np.ndarray,
                                       face_areas: np.ndarray, n_points: int) -> tuple[np.ndarray, np.ndarray]:
    """Sample points on a mesh surface with associated face normals.

        Points are sampled on the triangular mesh defined by ``vertices`` and
        ``faces``. Triangles are chosen with probability proportional to their
        surface area, and points are sampled uniformly within each selected
        triangle using random barycentric coordinates. The function returns
        both the sampled 3D points and the corresponding per-point normals,
        taken from the face normals.

        Args:
            vertices: Array of shape (V, 3) containing the mesh vertex
                coordinates (XYZ).
            faces: Array of shape (F, 3) containing vertex indices for each
                triangular face. Each row is a triplet of integer indices into
                ``vertices``.
            face_normals: Array of shape (F, 3) with the normal vector for
                each face, typically unit-length.
            face_areas: Array of shape (F,) with the area of each face.
            n_points: Number of points to sample on the mesh surface.

        Returns:
            tuple[np.ndarray, np.ndarray]:
                - sampled_points: Array of shape (n_points, 3) with the sampled
                  XYZ coordinates on the mesh surface.
                - sampled_normals: Array of shape (n_points, 3) with the
                  corresponding normal vectors (one per sampled point), copied
                  from the selected face normals.
        """
    prob = face_areas / np.sum(face_areas)
    index = np.random.choice(faces.shape[0], size=n_points, replace=True, p=prob)
    sampled_faces = faces[index]
    sampled_face_normals = face_normals[index]  # [n_points, 3]

    sampled_points = []
    for sampled_face in sampled_faces:
        v1_idx, v2_idx, v3_idx = sampled_face
        v1, v2, v3 = vertices[v1_idx], vertices[v2_idx], vertices[v3_idx]
        s, t = sorted([random.random(), random.random()])
        f_v = lambda i: s * v1[i] + (t - s) * v2[i] + (1 - t) * v3[i]

        sampled_points.append([f_v(0), f_v(1), f_v(2)])
    sampled_points = np.array(sampled_points)
    return sampled_points, sampled_face_normals

process_file

Source code in src\create_pc.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def process_file(file_path, saved_folder, degrees=None, n_pts=10000, scale=10.0):
    file_id = "tmp"
    saved_path = os.path.join(str(saved_folder), f"{file_id}.pth")
    ms = simple_clean(file_path)
    ms.apply_filter("compute_matrix_from_scaling_or_normalization",
                    uniformflag=True, axisx=1. / scale, axisy=1. / scale, axisz=1. / scale, scalecenter="origin")
    ms.apply_filter("apply_matrix_freeze")

    data = meshlab_to_open3d(ms)
    data.orient_triangles()
    data.remove_unreferenced_vertices()
    # center and rotate
    data = move_to_center(data, middle=True, in_place=True)
    if degrees:
        for d in degrees:
            data = rotate_data_3d(data, degrees=d, in_place=True)
    data = move_to_center(data, middle=True, in_place=True)

    vertices = np.array(data.vertices)
    faces = np.array(data.triangles)
    data.compute_vertex_normals()
    normals = np.array(data.vertex_normals)
    data.compute_triangle_normals()
    face_normals = np.array(data.triangle_normals)
    surface_area = data.get_surface_area()
    _, face_areas = area_and_normal(vertices, faces)


    N = vertices.shape[0]
    original_index = np.arange(N)
    if N < n_pts:
        sampled_points, sampled_face_normals = sample_points_with_normal_features(vertices, faces, face_normals, face_areas, n_points=n_pts - N)
        coord = np.concatenate((vertices, sampled_points), axis=0)
        norm = np.concatenate((normals, sampled_face_normals), axis=0)

    else:
        coord = vertices
        norm = normals
    # fps
    fps_index = farthest_point_sample(coord, n_pts)
    norm = preprocessing.normalize(norm, norm='l2')
    res = dict(coord=coord, norm=norm, fps_index=fps_index, original_index=original_index, area=surface_area)
    torch.save(res, saved_path)

Extract PC from Mesh

Before After