Coverage for trimesh/sample.py: 100%

66 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-07-31 23:55 +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, Seed, 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: 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 Seed for deterministic results, otherwise OS entropy. 

43 

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

54 

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 

59 

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

61 weight_cum = np.cumsum(face_weight) 

62 

63 random = util.random_generator(seed).random 

64 

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) 

69 

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

74 

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] 

78 

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] 

86 

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

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

89 

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) 

97 

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

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

100 

101 # finally, offset by the origin to generate 

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

103 samples = sample_vector + tri_origins 

104 

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] 

113 

114 return samples, face_index, colors 

115 

116 return samples, face_index 

117 

118 

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. 

123 

124 

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. 

133 

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 

144 

145 

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. 

155 

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. 

166 

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 

177 

178 

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. 

186 

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. 

190 

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 

201 

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 

210 

211 # guess radius from area 

212 if radius is None: 

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

214 

215 # get points on the surface 

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

217 

218 # remove the points closer than radius 

219 points, mask = remove_close(points, radius) 

220 

221 # we got all the samples we expect 

222 if len(points) >= count: 

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

224 

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

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

227 

228 return points, index[mask] 

229 

230 

231def sample_surface_sphere(count: int, seed: Seed = None) -> NDArray[float64]: 

232 """ 

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

234 

235 Uses this method: 

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

237 

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. 

244 

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