Coverage for trimesh/sample.py: 99%
73 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-08-31 18:21 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-08-31 18:21 +0000
1"""
2sample.py
3------------
5Randomly sample surface and volume of meshes.
6"""
8from logging import getLogger
10import numpy as np
12from . import transformations
13from .typed import ArrayLike, Integer, NDArray, Number, Seed, float64
14from .util import random_generator, spherical_to_vector
15from .visual import uv_to_interpolated_color
17log = getLogger(__name__)
20def sample_surface(
21 mesh,
22 count: Integer,
23 face_weight: ArrayLike | None = None,
24 sample_color=False,
25 return_barycentric: bool = False,
26 seed: Seed = None,
27):
28 """
29 Sample the surface of a mesh, returning the specified
30 number of points
32 For individual triangle sampling uses this method:
33 http://mathworld.wolfram.com/TrianglePointPicking.html
35 Parameters
36 -----------
37 mesh : trimesh.Trimesh
38 Geometry to sample the surface of
39 count : int
40 Number of points to return
41 face_weight : None or len(mesh.faces) float
42 Weight faces by a factor other than face area.
43 If None will be the same as face_weight=mesh.area
44 sample_color : bool
45 Option to calculate the color of the sampled points.
46 Default is False.
47 return_barycentric : bool
48 If True will also return the barycentric coordinates
49 of each sampled point.
50 seed : None or int
51 Seed for deterministic results, otherwise OS entropy.
53 Returns
54 ---------
55 samples : (count, 3) float
56 Points in space on the surface of mesh
57 face_index : (count,) int
58 Indices of faces for each sampled point
59 colors : (count, 4) float
60 Colors of each sampled point
61 Returns only when the sample_color is True
62 barycentric : (count, 3) float
63 Coordinates on `mesh.faces[face_index]`
64 Returned only when return_barycentric is True
65 """
67 if face_weight is None:
68 # len(mesh.faces) float, array of the areas
69 # of each face of the mesh
70 face_weight = mesh.area_faces
72 # cumulative sum of weights (len(mesh.faces))
73 weight_cum = np.cumsum(face_weight)
75 random = random_generator(seed).random
77 # last value of cumulative sum is total summed weight/area
78 face_pick = random(count) * weight_cum[-1]
79 # get the index of the selected faces
80 face_index = np.searchsorted(weight_cum, face_pick)
82 # pull triangles into the form of an origin + 2 vectors
83 tri_origins = mesh.vertices[mesh.faces[:, 0]]
84 tri_vectors = mesh.vertices[mesh.faces[:, 1:]].copy()
85 tri_vectors -= np.tile(tri_origins, (1, 2)).reshape((-1, 2, 3))
87 # pull the vectors for the faces we are going to sample from
88 tri_origins = tri_origins[face_index]
89 tri_vectors = tri_vectors[face_index]
91 if sample_color and hasattr(mesh.visual, "uv"):
92 uv_origins = mesh.visual.uv[mesh.faces[:, 0]]
93 uv_vectors = mesh.visual.uv[mesh.faces[:, 1:]].copy()
94 uv_origins_tile = np.tile(uv_origins, (1, 2)).reshape((-1, 2, 2))
95 uv_vectors -= uv_origins_tile
96 uv_origins = uv_origins[face_index]
97 uv_vectors = uv_vectors[face_index]
99 # randomly generate two 0-1 scalar components to multiply edge vectors b
100 random_lengths = random((len(tri_vectors), 2, 1))
102 # points will be distributed on a quadrilateral if we use 2 0-1 samples
103 # if the two scalar components sum less than 1.0 the point will be
104 # inside the triangle, so we find vectors longer than 1.0 and
105 # transform them to be inside the triangle
106 random_test = random_lengths.sum(axis=1).reshape(-1) > 1.0
107 random_lengths[random_test] -= 1.0
108 random_lengths = np.abs(random_lengths)
110 if return_barycentric:
111 # the two random lengths are the barycentric coordinates of the
112 # second and third vertex - the first vertex is what remains
113 barycentric = np.hstack((1 - random_lengths.sum(axis=1), random_lengths[:, :, 0]))
115 # multiply triangle edge vectors by the random lengths and sum
116 sample_vector = (tri_vectors * random_lengths).sum(axis=1)
118 # finally, offset by the origin to generate
119 # (n,3) points in space on the triangle
120 samples = sample_vector + tri_origins
122 if sample_color:
123 if hasattr(mesh.visual, "uv"):
124 sample_uv_vector = (uv_vectors * random_lengths).sum(axis=1)
125 uv_samples = sample_uv_vector + uv_origins
126 texture = mesh.visual.material.image
127 colors = uv_to_interpolated_color(uv_samples, texture)
128 else:
129 colors = mesh.visual.face_colors[face_index]
131 if return_barycentric:
132 return samples, face_index, colors, barycentric
134 return samples, face_index, colors
136 if return_barycentric:
137 return samples, face_index, barycentric
139 return samples, face_index
142def volume_mesh(mesh, count: Integer, seed: Seed = None) -> NDArray[float64]:
143 """
144 Use rejection sampling to produce points randomly
145 distributed in the volume of a mesh.
148 Parameters
149 -----------
150 mesh : trimesh.Trimesh
151 Geometry to sample
152 count : int
153 Number of points to return
154 seed : None or int
155 Seed for deterministic results, otherwise OS entropy.
157 Returns
158 ---------
159 samples : (n, 3) float
160 Points in the volume of the mesh where n <= count
161 """
162 random = random_generator(seed).random
163 points = (random((count, 3)) * mesh.extents) + mesh.bounds[0]
164 contained = mesh.contains(points)
165 samples = points[contained][:count]
166 return samples
169def volume_rectangular(
170 extents,
171 count: Integer,
172 transform: ArrayLike | None = None,
173 seed: Seed = None,
174) -> NDArray[float64]:
175 """
176 Return random samples inside a rectangular volume,
177 useful for sampling inside oriented bounding boxes.
179 Parameters
180 -----------
181 extents : (3,) float
182 Side lengths of rectangular solid
183 count : int
184 Number of points to return
185 transform : (4, 4) float
186 Homogeneous transformation matrix
187 seed : None or int
188 Seed for deterministic results, otherwise OS entropy.
190 Returns
191 ---------
192 samples : (count, 3) float
193 Points in requested volume
194 """
195 samples = (random_generator(seed).random((count, 3)) - 0.5) * extents
196 if transform is not None:
197 samples = transformations.transform_points(samples, transform)
198 return samples
201def sample_surface_even(
202 mesh, count: Integer, radius: Number | None = None, seed: Seed = None
203):
204 """
205 Sample the surface of a mesh, returning samples which are
206 VERY approximately evenly spaced. This is accomplished by
207 sampling and then rejecting pairs that are too close together.
209 Note that since it is using rejection sampling it may return
210 fewer points than requested (i.e. n < count). If this is the
211 case a log.warning will be emitted.
213 Parameters
214 -----------
215 mesh : trimesh.Trimesh
216 Geometry to sample the surface of
217 count : int
218 Number of points to return
219 radius : None or float
220 Removes samples below this radius
221 seed : None or int
222 Provides deterministic values
224 Returns
225 ---------
226 samples : (n, 3) float
227 Points in space on the surface of mesh
228 face_index : (n,) int
229 Indices of faces for each sampled point
230 """
231 from .points import remove_close
233 # guess radius from area
234 if radius is None:
235 radius = np.sqrt(mesh.area / (3 * count))
237 # get points on the surface
238 points, index = sample_surface(mesh, count * 3, seed=seed)
240 # remove the points closer than radius
241 points, mask = remove_close(points, radius)
243 # we got all the samples we expect
244 if len(points) >= count:
245 return points[:count], index[mask][:count]
247 # warn if we didn't get all the samples we expect
248 log.warning(f"only got {len(points)}/{count} samples!")
250 return points, index[mask]
253def sample_surface_sphere(count: int, seed: Seed = None) -> NDArray[float64]:
254 """
255 Correctly pick random points on the surface of a unit sphere
257 Uses this method:
258 http://mathworld.wolfram.com/SpherePointPicking.html
260 Parameters
261 -----------
262 count : int
263 Number of points to return
264 seed : None or int
265 Seed for deterministic results, otherwise OS entropy.
267 Returns
268 ----------
269 points : (count, 3) float
270 Random points on the surface of a unit sphere
271 """
272 # get random values 0.0-1.0
273 u, v = random_generator(seed).random((2, count))
274 # convert to two angles
275 theta = np.pi * 2 * u
276 phi = np.arccos((2 * v) - 1)
277 # convert spherical coordinates to cartesian
278 return spherical_to_vector(np.column_stack((theta, phi)))