Coverage for trimesh/voxel/creation.py: 71%

89 statements  

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

1import numpy as np 

2 

3from .. import grouping, remesh, util 

4from .. import transformations as tr 

5from ..constants import log_time 

6from ..typed import ArrayLike, Integer, Number, VoxelizationMethodsType 

7from . import base 

8from . import encoding as enc 

9 

10 

11@log_time 

12def voxelize_subdivide( 

13 mesh, pitch: Number, max_iter: Integer | None = 10, edge_factor: Number = 2.0 

14) -> base.VoxelGrid: 

15 """ 

16 Voxelize a surface by subdividing a mesh until every edge is 

17 shorter than: (pitch / edge_factor) 

18 

19 Parameters 

20 ----------- 

21 mesh : trimesh.Trimesh 

22 Source mesh 

23 pitch 

24 Side length of a single voxel cube 

25 max_iter 

26 Cap maximum subdivisions or None for no limit. 

27 edge_factor 

28 Proportion of pitch maximum edge length. 

29 

30 Returns 

31 ----------- 

32 VoxelGrid instance representing the voxelized mesh. 

33 """ 

34 max_edge = pitch / edge_factor 

35 

36 if max_iter is None: 

37 longest_edge = np.linalg.norm( 

38 mesh.vertices[mesh.edges[:, 0]] - mesh.vertices[mesh.edges[:, 1]], axis=1 

39 ).max() 

40 max_iter = max(int(np.ceil(np.log2(longest_edge / max_edge))), 0) 

41 

42 # get the same mesh sudivided so every edge is shorter 

43 # than a factor of our pitch 

44 v, _f, _idx = remesh.subdivide_to_size( 

45 mesh.vertices, mesh.faces, max_edge=max_edge, max_iter=max_iter, return_index=True 

46 ) 

47 

48 # convert the vertices to their voxel grid position 

49 # Provided edge_factor > 1 and max_iter is large enough, this is 

50 # sufficient to preserve 6-connectivity at the level of voxels. 

51 hit = np.round(v / pitch).astype(int) 

52 

53 # remove duplicates 

54 unique, _inverse = grouping.unique_rows(hit) 

55 

56 # get the voxel centers in model space 

57 occupied_index = hit[unique] 

58 

59 origin_index = occupied_index.min(axis=0) 

60 origin_position = origin_index * pitch 

61 

62 return base.VoxelGrid( 

63 enc.SparseBinaryEncoding(occupied_index - origin_index), 

64 transform=tr.scale_and_translate(scale=pitch, translate=origin_position), 

65 ) 

66 

67 

68def local_voxelize( 

69 mesh, 

70 point: ArrayLike, 

71 pitch: Number, 

72 radius: Number, 

73 fill: bool = True, 

74 **kwargs, 

75) -> base.VoxelGrid | None: 

76 """ 

77 Voxelize a mesh in the region of a cube around a point. When fill=True, 

78 uses proximity.contains to fill the resulting voxels so may be meaningless 

79 for non-watertight meshes. Useful to reduce memory cost for small values of 

80 pitch as opposed to global voxelization. 

81 

82 Parameters 

83 ----------- 

84 mesh : trimesh.Trimesh 

85 Source geometry 

86 point : (3, ) float 

87 Point in space to voxelize around 

88 pitch 

89 Side length of a single voxel cube 

90 radius 

91 Number of voxel cubes to return in each direction. 

92 kwargs 

93 Parameters to pass to voxelize_subdivide 

94 

95 Returns 

96 ----------- 

97 voxels : VoxelGrid instance with resolution (m, m, m) where m=2*radius+1 

98 or None if the volume is empty 

99 """ 

100 from scipy import ndimage 

101 

102 # make sure point is correct type/shape 

103 point = np.asanyarray(point, dtype=np.float64).reshape(3) 

104 # this is a gotcha- radius sounds a lot like it should be in 

105 # float model space, not int voxel space so check 

106 if not isinstance(radius, int): 

107 raise ValueError("radius needs to be an integer number of cubes!") 

108 

109 # Bounds of region 

110 bounds = np.concatenate( 

111 (point - (radius + 0.5) * pitch, point + (radius + 0.5) * pitch) 

112 ) 

113 

114 # faces that intersect axis aligned bounding box 

115 faces = list(mesh.triangles_tree.intersection(bounds)) 

116 

117 # didn't hit anything so exit 

118 if len(faces) == 0: 

119 return None 

120 

121 local = mesh.submesh([[f] for f in faces], append=True) 

122 

123 # Translate mesh so point is at 0,0,0 

124 local.apply_translation(-point) 

125 

126 # sparse, origin = voxelize_subdivide(local, pitch, **kwargs) 

127 vox = voxelize_subdivide(local, pitch, **kwargs) 

128 origin = vox.transform[:3, 3] 

129 matrix = vox.encoding.dense 

130 

131 # Find voxel index for point 

132 center = np.round(-origin / pitch).astype(np.int64) 

133 

134 # pad matrix if necessary 

135 prepad = np.maximum(radius - center, 0) 

136 postpad = np.maximum(center + radius + 1 - matrix.shape, 0) 

137 

138 matrix = np.pad(matrix, np.stack((prepad, postpad), axis=-1), mode="constant") 

139 center += prepad 

140 

141 # Extract voxels within the bounding box 

142 voxels = matrix[ 

143 center[0] - radius : center[0] + radius + 1, 

144 center[1] - radius : center[1] + radius + 1, 

145 center[2] - radius : center[2] + radius + 1, 

146 ] 

147 local_origin = point - radius * pitch # origin of local voxels 

148 

149 # Fill internal regions 

150 if fill: 

151 regions, n = ndimage.label(~voxels) 

152 distance = ndimage.distance_transform_cdt(~voxels) 

153 representatives = [ 

154 np.unravel_index((distance * (regions == i)).argmax(), distance.shape) 

155 for i in range(1, n + 1) 

156 ] 

157 contains = mesh.contains(np.asarray(representatives) * pitch + local_origin) 

158 where = np.where(contains)[0] + 1 

159 internal = np.isin(regions.flatten(), where).reshape(regions.shape) 

160 voxels = np.logical_or(voxels, internal) 

161 

162 return base.VoxelGrid(voxels, tr.translation_matrix(local_origin)) 

163 

164 

165@log_time 

166def voxelize_ray( 

167 mesh, pitch: Number, per_cell: ArrayLike | None = None 

168) -> base.VoxelGrid: 

169 """ 

170 Voxelize a mesh using ray queries. 

171 

172 Parameters 

173 ------------- 

174 mesh 

175 Mesh to be voxelized 

176 pitch 

177 Length of voxel cube 

178 per_cell : (2,) int 

179 How many ray queries to make per cell 

180 

181 Returns 

182 ------------- 

183 grid 

184 VoxelGrid instance representing the voxelized mesh. 

185 """ 

186 if per_cell is None: 

187 # how many rays per cell 

188 per_cell = np.array([2, 2], dtype=np.int64) 

189 else: 

190 per_cell = np.array(per_cell, dtype=np.int64).reshape(2) 

191 

192 # edge length of cube voxels 

193 pitch = float(pitch) 

194 

195 # create the ray origins in a grid 

196 bounds = mesh.bounds[:, :2].copy() 

197 # offset start so we get the requested number per cell 

198 bounds[0] += pitch / (1.0 + per_cell) 

199 # offset end so arange doesn't short us 

200 bounds[1] += pitch 

201 # on X we are doing multiple rays per voxel step 

202 step = pitch / per_cell 

203 # 2D grid 

204 ray_ori = util.grid_arange(bounds, step=step) 

205 # a Z position below the mesh 

206 z = np.ones(len(ray_ori)) * (mesh.bounds[0][2] - pitch) 

207 ray_ori = np.column_stack((ray_ori, z)) 

208 # all rays are along positive Z 

209 ray_dir = np.ones_like(ray_ori) * [0, 0, 1] 

210 

211 # if you have pyembree this should be decently fast 

212 hits = mesh.ray.intersects_location(ray_ori, ray_dir)[0] 

213 

214 # just convert hit locations to integer positions 

215 voxels = np.round(hits / pitch).astype(np.int64) 

216 

217 # offset voxels by min, so matrix isn't huge 

218 origin_index = voxels.min(axis=0) 

219 voxels -= origin_index 

220 encoding = enc.SparseBinaryEncoding(voxels) 

221 origin_position = origin_index * pitch 

222 return base.VoxelGrid( 

223 encoding, tr.scale_and_translate(scale=pitch, translate=origin_position) 

224 ) 

225 

226 

227@log_time 

228def voxelize_binvox( 

229 mesh, 

230 pitch: Number | None = None, 

231 dimension: Integer | None = None, 

232 bounds: ArrayLike | None = None, 

233 **binvoxer_kwargs, 

234) -> base.VoxelGrid: 

235 """ 

236 Voxelize via binvox tool. 

237 

238 Parameters 

239 -------------- 

240 mesh : trimesh.Trimesh 

241 Mesh to voxelize 

242 pitch : float 

243 Side length of each voxel. Ignored if dimension is provided 

244 dimension: int 

245 Number of voxels along each dimension. If not provided, this is 

246 calculated based on pitch and bounds/mesh extents 

247 bounds: (2, 3) float 

248 min/max values of the returned `VoxelGrid` in each instance. Uses 

249 `mesh.bounds` if not provided. 

250 **binvoxer_kwargs: 

251 Passed to `trimesh.exchange.binvox.Binvoxer`. 

252 Should not contain `bounding_box` if bounds is not None. 

253 

254 Returns 

255 -------------- 

256 grid 

257 `VoxelGrid` instance 

258 

259 Raises 

260 -------------- 

261 `ValueError` if `bounds is not None and 'bounding_box' in binvoxer_kwargs`. 

262 """ 

263 from trimesh.exchange import binvox 

264 

265 if dimension is None: 

266 # pitch must be provided 

267 if bounds is None: 

268 extents = mesh.extents 

269 else: 

270 mins, maxs = bounds 

271 extents = maxs - mins 

272 dimension = int(np.ceil(np.max(extents) / pitch)) 

273 if bounds is not None: 

274 if "bounding_box" in binvoxer_kwargs: 

275 raise ValueError("Cannot provide both bounds and bounding_box") 

276 binvoxer_kwargs["bounding_box"] = np.asanyarray(bounds).flatten() 

277 

278 binvoxer = binvox.Binvoxer(dimension=dimension, **binvoxer_kwargs) 

279 return binvox.voxelize_mesh(mesh, binvoxer) 

280 

281 

282voxelizers = util.FunctionRegistry( 

283 ray=voxelize_ray, subdivide=voxelize_subdivide, binvox=voxelize_binvox 

284) 

285 

286 

287def voxelize( 

288 mesh, 

289 pitch: Number | None, 

290 method: VoxelizationMethodsType = "subdivide", 

291 **kwargs, 

292) -> base.VoxelGrid | None: 

293 """ 

294 Voxelize the given mesh using the specified implementation. 

295 

296 See `voxelizers` for available implementations or to add your own, e.g. via 

297 `voxelizers['custom_key'] = custom_fn`. 

298 

299 `custom_fn` should have signature `(mesh, pitch, **kwargs) -> VoxelGrid` 

300 and should not modify encoding. 

301 

302 Parameters 

303 -------------- 

304 mesh 

305 Geometry to voxelize 

306 pitch 

307 Side length of each voxel. 

308 method 

309 Which voxelization method to use. 

310 kwargs 

311 Passed through to the specified implementation. 

312 

313 Returns 

314 -------------- 

315 grid 

316 A VoxelGrid instance. 

317 """ 

318 return voxelizers(method, mesh=mesh, pitch=pitch, **kwargs)