Coverage for trimesh/sample.py: 100%

67 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +0000

1""" 

2sample.py 

3------------ 

4 

5Randomly sample surface and volume of meshes. 

6""" 

7 

8import numpy as np 

9 

10from . import transformations, util 

11from .typed import ArrayLike, Integer, NDArray, Number, float64 

12from .visual import uv_to_interpolated_color 

13 

14 

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 

25 

26 For individual triangle sampling uses this method: 

27 http://mathworld.wolfram.com/TrianglePointPicking.html 

28 

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. 

44 

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 """ 

55 

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 

60 

61 # cumulative sum of weights (len(mesh.faces)) 

62 weight_cum = np.cumsum(face_weight) 

63 

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 

69 

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) 

74 

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)) 

79 

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] 

83 

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] 

91 

92 # randomly generate two 0-1 scalar components to multiply edge vectors b 

93 random_lengths = random((len(tri_vectors), 2, 1)) 

94 

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) 

102 

103 # multiply triangle edge vectors by the random lengths and sum 

104 sample_vector = (tri_vectors * random_lengths).sum(axis=1) 

105 

106 # finally, offset by the origin to generate 

107 # (n,3) points in space on the triangle 

108 samples = sample_vector + tri_origins 

109 

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] 

118 

119 return samples, face_index, colors 

120 

121 return samples, face_index 

122 

123 

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. 

128 

129 

130 Parameters 

131 ----------- 

132 mesh : trimesh.Trimesh 

133 Geometry to sample 

134 count : int 

135 Number of points to return 

136 

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 

146 

147 

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. 

154 

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 

163 

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 

174 

175 

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. 

181 

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. 

185 

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 

196 

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 

205 

206 # guess radius from area 

207 if radius is None: 

208 radius = np.sqrt(mesh.area / (3 * count)) 

209 

210 # get points on the surface 

211 points, index = sample_surface(mesh, count * 3, seed=seed) 

212 

213 # remove the points closer than radius 

214 points, mask = remove_close(points, radius) 

215 

216 # we got all the samples we expect 

217 if len(points) >= count: 

218 return points[:count], index[mask][:count] 

219 

220 # warn if we didn't get all the samples we expect 

221 util.log.warning(f"only got {len(points)}/{count} samples!") 

222 

223 return points, index[mask] 

224 

225 

226def sample_surface_sphere(count: int) -> NDArray[float64]: 

227 """ 

228 Correctly pick random points on the surface of a unit sphere 

229 

230 Uses this method: 

231 http://mathworld.wolfram.com/SpherePointPicking.html 

232 

233 Parameters 

234 ----------- 

235 count : int 

236 Number of points to return 

237 

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