Coverage for trimesh/sample.py: 100%
66 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-31 23:55 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-31 23:55 +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, Seed, 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: 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 Seed for deterministic results, otherwise OS entropy.
44 Returns
45 ---------
46 samples : (count, 3) float
47 Points in space on the surface of mesh
48 face_index : (count,) int
49 Indices of faces for each sampled point
50 colors : (count, 4) float
51 Colors of each sampled point
52 Returns only when the sample_color is True
53 """
55 if face_weight is None:
56 # len(mesh.faces) float, array of the areas
57 # of each face of the mesh
58 face_weight = mesh.area_faces
60 # cumulative sum of weights (len(mesh.faces))
61 weight_cum = np.cumsum(face_weight)
63 random = util.random_generator(seed).random
65 # last value of cumulative sum is total summed weight/area
66 face_pick = random(count) * weight_cum[-1]
67 # get the index of the selected faces
68 face_index = np.searchsorted(weight_cum, face_pick)
70 # pull triangles into the form of an origin + 2 vectors
71 tri_origins = mesh.vertices[mesh.faces[:, 0]]
72 tri_vectors = mesh.vertices[mesh.faces[:, 1:]].copy()
73 tri_vectors -= np.tile(tri_origins, (1, 2)).reshape((-1, 2, 3))
75 # pull the vectors for the faces we are going to sample from
76 tri_origins = tri_origins[face_index]
77 tri_vectors = tri_vectors[face_index]
79 if sample_color and hasattr(mesh.visual, "uv"):
80 uv_origins = mesh.visual.uv[mesh.faces[:, 0]]
81 uv_vectors = mesh.visual.uv[mesh.faces[:, 1:]].copy()
82 uv_origins_tile = np.tile(uv_origins, (1, 2)).reshape((-1, 2, 2))
83 uv_vectors -= uv_origins_tile
84 uv_origins = uv_origins[face_index]
85 uv_vectors = uv_vectors[face_index]
87 # randomly generate two 0-1 scalar components to multiply edge vectors b
88 random_lengths = random((len(tri_vectors), 2, 1))
90 # points will be distributed on a quadrilateral if we use 2 0-1 samples
91 # if the two scalar components sum less than 1.0 the point will be
92 # inside the triangle, so we find vectors longer than 1.0 and
93 # transform them to be inside the triangle
94 random_test = random_lengths.sum(axis=1).reshape(-1) > 1.0
95 random_lengths[random_test] -= 1.0
96 random_lengths = np.abs(random_lengths)
98 # multiply triangle edge vectors by the random lengths and sum
99 sample_vector = (tri_vectors * random_lengths).sum(axis=1)
101 # finally, offset by the origin to generate
102 # (n,3) points in space on the triangle
103 samples = sample_vector + tri_origins
105 if sample_color:
106 if hasattr(mesh.visual, "uv"):
107 sample_uv_vector = (uv_vectors * random_lengths).sum(axis=1)
108 uv_samples = sample_uv_vector + uv_origins
109 texture = mesh.visual.material.image
110 colors = uv_to_interpolated_color(uv_samples, texture)
111 else:
112 colors = mesh.visual.face_colors[face_index]
114 return samples, face_index, colors
116 return samples, face_index
119def volume_mesh(mesh, count: Integer, seed: Seed = None) -> NDArray[float64]:
120 """
121 Use rejection sampling to produce points randomly
122 distributed in the volume of a mesh.
125 Parameters
126 -----------
127 mesh : trimesh.Trimesh
128 Geometry to sample
129 count : int
130 Number of points to return
131 seed : None or int
132 Seed for deterministic results, otherwise OS entropy.
134 Returns
135 ---------
136 samples : (n, 3) float
137 Points in the volume of the mesh where n <= count
138 """
139 random = util.random_generator(seed).random
140 points = (random((count, 3)) * mesh.extents) + mesh.bounds[0]
141 contained = mesh.contains(points)
142 samples = points[contained][:count]
143 return samples
146def volume_rectangular(
147 extents,
148 count: Integer,
149 transform: ArrayLike | None = None,
150 seed: Seed = None,
151) -> NDArray[float64]:
152 """
153 Return random samples inside a rectangular volume,
154 useful for sampling inside oriented bounding boxes.
156 Parameters
157 -----------
158 extents : (3,) float
159 Side lengths of rectangular solid
160 count : int
161 Number of points to return
162 transform : (4, 4) float
163 Homogeneous transformation matrix
164 seed : None or int
165 Seed for deterministic results, otherwise OS entropy.
167 Returns
168 ---------
169 samples : (count, 3) float
170 Points in requested volume
171 """
172 samples = util.random_generator(seed).random((count, 3)) - 0.5
173 samples *= extents
174 if transform is not None:
175 samples = transformations.transform_points(samples, transform)
176 return samples
179def sample_surface_even(
180 mesh, count: Integer, radius: Number | None = None, seed: Seed = None
181):
182 """
183 Sample the surface of a mesh, returning samples which are
184 VERY approximately evenly spaced. This is accomplished by
185 sampling and then rejecting pairs that are too close together.
187 Note that since it is using rejection sampling it may return
188 fewer points than requested (i.e. n < count). If this is the
189 case a log.warning will be emitted.
191 Parameters
192 -----------
193 mesh : trimesh.Trimesh
194 Geometry to sample the surface of
195 count : int
196 Number of points to return
197 radius : None or float
198 Removes samples below this radius
199 seed : None or int
200 Provides deterministic values
202 Returns
203 ---------
204 samples : (n, 3) float
205 Points in space on the surface of mesh
206 face_index : (n,) int
207 Indices of faces for each sampled point
208 """
209 from .points import remove_close
211 # guess radius from area
212 if radius is None:
213 radius = np.sqrt(mesh.area / (3 * count))
215 # get points on the surface
216 points, index = sample_surface(mesh, count * 3, seed=seed)
218 # remove the points closer than radius
219 points, mask = remove_close(points, radius)
221 # we got all the samples we expect
222 if len(points) >= count:
223 return points[:count], index[mask][:count]
225 # warn if we didn't get all the samples we expect
226 util.log.warning(f"only got {len(points)}/{count} samples!")
228 return points, index[mask]
231def sample_surface_sphere(count: int, seed: Seed = None) -> NDArray[float64]:
232 """
233 Correctly pick random points on the surface of a unit sphere
235 Uses this method:
236 http://mathworld.wolfram.com/SpherePointPicking.html
238 Parameters
239 -----------
240 count : int
241 Number of points to return
242 seed : None or int
243 Seed for deterministic results, otherwise OS entropy.
245 Returns
246 ----------
247 points : (count, 3) float
248 Random points on the surface of a unit sphere
249 """
250 # get random values 0.0-1.0
251 u, v = util.random_generator(seed).random((2, count))
252 # convert to two angles
253 theta = np.pi * 2 * u
254 phi = np.arccos((2 * v) - 1)
255 # convert spherical coordinates to cartesian
256 points = util.spherical_to_vector(np.column_stack((theta, phi)))
257 return points