Coverage for trimesh/sample.py: 100%
67 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1"""
2sample.py
3------------
5Randomly sample surface and volume of meshes.
6"""
8import numpy as np
10from . import transformations, util
11from .typed import ArrayLike, Integer, NDArray, Number, float64
12from .visual import uv_to_interpolated_color
15def sample_surface(
16 mesh,
17 count: Integer,
18 face_weight: ArrayLike | None = None,
19 sample_color=False,
20 seed=None,
21):
22 """
23 Sample the surface of a mesh, returning the specified
24 number of points
26 For individual triangle sampling uses this method:
27 http://mathworld.wolfram.com/TrianglePointPicking.html
29 Parameters
30 -----------
31 mesh : trimesh.Trimesh
32 Geometry to sample the surface of
33 count : int
34 Number of points to return
35 face_weight : None or len(mesh.faces) float
36 Weight faces by a factor other than face area.
37 If None will be the same as face_weight=mesh.area
38 sample_color : bool
39 Option to calculate the color of the sampled points.
40 Default is False.
41 seed : None or int
42 If passed as an integer will provide deterministic results
43 otherwise pulls the seed from operating system entropy.
45 Returns
46 ---------
47 samples : (count, 3) float
48 Points in space on the surface of mesh
49 face_index : (count,) int
50 Indices of faces for each sampled point
51 colors : (count, 4) float
52 Colors of each sampled point
53 Returns only when the sample_color is True
54 """
56 if face_weight is None:
57 # len(mesh.faces) float, array of the areas
58 # of each face of the mesh
59 face_weight = mesh.area_faces
61 # cumulative sum of weights (len(mesh.faces))
62 weight_cum = np.cumsum(face_weight)
64 # seed the random number generator as requested
65 if seed is None:
66 random = np.random.random
67 else:
68 random = np.random.default_rng(seed).random
70 # last value of cumulative sum is total summed weight/area
71 face_pick = random(count) * weight_cum[-1]
72 # get the index of the selected faces
73 face_index = np.searchsorted(weight_cum, face_pick)
75 # pull triangles into the form of an origin + 2 vectors
76 tri_origins = mesh.vertices[mesh.faces[:, 0]]
77 tri_vectors = mesh.vertices[mesh.faces[:, 1:]].copy()
78 tri_vectors -= np.tile(tri_origins, (1, 2)).reshape((-1, 2, 3))
80 # pull the vectors for the faces we are going to sample from
81 tri_origins = tri_origins[face_index]
82 tri_vectors = tri_vectors[face_index]
84 if sample_color and hasattr(mesh.visual, "uv"):
85 uv_origins = mesh.visual.uv[mesh.faces[:, 0]]
86 uv_vectors = mesh.visual.uv[mesh.faces[:, 1:]].copy()
87 uv_origins_tile = np.tile(uv_origins, (1, 2)).reshape((-1, 2, 2))
88 uv_vectors -= uv_origins_tile
89 uv_origins = uv_origins[face_index]
90 uv_vectors = uv_vectors[face_index]
92 # randomly generate two 0-1 scalar components to multiply edge vectors b
93 random_lengths = random((len(tri_vectors), 2, 1))
95 # points will be distributed on a quadrilateral if we use 2 0-1 samples
96 # if the two scalar components sum less than 1.0 the point will be
97 # inside the triangle, so we find vectors longer than 1.0 and
98 # transform them to be inside the triangle
99 random_test = random_lengths.sum(axis=1).reshape(-1) > 1.0
100 random_lengths[random_test] -= 1.0
101 random_lengths = np.abs(random_lengths)
103 # multiply triangle edge vectors by the random lengths and sum
104 sample_vector = (tri_vectors * random_lengths).sum(axis=1)
106 # finally, offset by the origin to generate
107 # (n,3) points in space on the triangle
108 samples = sample_vector + tri_origins
110 if sample_color:
111 if hasattr(mesh.visual, "uv"):
112 sample_uv_vector = (uv_vectors * random_lengths).sum(axis=1)
113 uv_samples = sample_uv_vector + uv_origins
114 texture = mesh.visual.material.image
115 colors = uv_to_interpolated_color(uv_samples, texture)
116 else:
117 colors = mesh.visual.face_colors[face_index]
119 return samples, face_index, colors
121 return samples, face_index
124def volume_mesh(mesh, count: Integer) -> NDArray[float64]:
125 """
126 Use rejection sampling to produce points randomly
127 distributed in the volume of a mesh.
130 Parameters
131 -----------
132 mesh : trimesh.Trimesh
133 Geometry to sample
134 count : int
135 Number of points to return
137 Returns
138 ---------
139 samples : (n, 3) float
140 Points in the volume of the mesh where n <= count
141 """
142 points = (np.random.random((count, 3)) * mesh.extents) + mesh.bounds[0]
143 contained = mesh.contains(points)
144 samples = points[contained][:count]
145 return samples
148def volume_rectangular(
149 extents, count: Integer, transform: ArrayLike | None = None
150) -> NDArray[float64]:
151 """
152 Return random samples inside a rectangular volume,
153 useful for sampling inside oriented bounding boxes.
155 Parameters
156 -----------
157 extents : (3,) float
158 Side lengths of rectangular solid
159 count : int
160 Number of points to return
161 transform : (4, 4) float
162 Homogeneous transformation matrix
164 Returns
165 ---------
166 samples : (count, 3) float
167 Points in requested volume
168 """
169 samples = np.random.random((count, 3)) - 0.5
170 samples *= extents
171 if transform is not None:
172 samples = transformations.transform_points(samples, transform)
173 return samples
176def sample_surface_even(mesh, count: Integer, radius: Number | None = None, seed=None):
177 """
178 Sample the surface of a mesh, returning samples which are
179 VERY approximately evenly spaced. This is accomplished by
180 sampling and then rejecting pairs that are too close together.
182 Note that since it is using rejection sampling it may return
183 fewer points than requested (i.e. n < count). If this is the
184 case a log.warning will be emitted.
186 Parameters
187 -----------
188 mesh : trimesh.Trimesh
189 Geometry to sample the surface of
190 count : int
191 Number of points to return
192 radius : None or float
193 Removes samples below this radius
194 seed : None or int
195 Provides deterministic values
197 Returns
198 ---------
199 samples : (n, 3) float
200 Points in space on the surface of mesh
201 face_index : (n,) int
202 Indices of faces for each sampled point
203 """
204 from .points import remove_close
206 # guess radius from area
207 if radius is None:
208 radius = np.sqrt(mesh.area / (3 * count))
210 # get points on the surface
211 points, index = sample_surface(mesh, count * 3, seed=seed)
213 # remove the points closer than radius
214 points, mask = remove_close(points, radius)
216 # we got all the samples we expect
217 if len(points) >= count:
218 return points[:count], index[mask][:count]
220 # warn if we didn't get all the samples we expect
221 util.log.warning(f"only got {len(points)}/{count} samples!")
223 return points, index[mask]
226def sample_surface_sphere(count: int) -> NDArray[float64]:
227 """
228 Correctly pick random points on the surface of a unit sphere
230 Uses this method:
231 http://mathworld.wolfram.com/SpherePointPicking.html
233 Parameters
234 -----------
235 count : int
236 Number of points to return
238 Returns
239 ----------
240 points : (count, 3) float
241 Random points on the surface of a unit sphere
242 """
243 # get random values 0.0-1.0
244 u, v = np.random.random((2, count))
245 # convert to two angles
246 theta = np.pi * 2 * u
247 phi = np.arccos((2 * v) - 1)
248 # convert spherical coordinates to cartesian
249 points = util.spherical_to_vector(np.column_stack((theta, phi)))
250 return points