Coverage for trimesh/exchange/binvox.py: 75%
176 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1"""
2Parsing functions for Binvox files.
4https://www.patrickmin.com/binvox/binvox.html
6Exporting meshes as binvox files requires the
7`binvox` executable to be in your path.
8"""
10import collections
11import os
12import subprocess
13from tempfile import TemporaryDirectory
15import numpy as np
17from .. import util
18from ..base import Trimesh
19from ..util import comment_strip, decode_text
21# find the executable for binvox in PATH
22binvox_encoder = util.which("binvox")
23Binvox = collections.namedtuple("Binvox", ["rle_data", "shape", "translate", "scale"])
26def _parse_binvox_header(file_obj):
27 """
28 Read the header from a binvox file.
29 Spec available:
30 https://www.patrickmin.com/binvox/binvox.html
32 Parameters
33 ------------
34 fp: file-object
35 File like object with binvox file
37 Returns
38 ----------
39 shape : tuple
40 Shape of binvox according to binvox spec
41 translate : tuple
42 Translation
43 scale : float
44 Scale of voxels
46 Raises
47 ------------
48 IOError
49 If invalid binvox file.
50 """
52 # check for the magic string in the first line
53 first = decode_text(file_obj.readline()).strip()
54 if "binvox" not in first.lower():
55 raise ValueError("File is not in the binvox format!")
57 header = {}
58 reached_data = False
59 # do a capped iteration
60 for _ in range(100):
61 # get the line as a lower-case, comment-stripped split list
62 line = (
63 comment_strip(decode_text(file_obj.readline()).lower(), "#").strip().split()
64 )
65 # if the line was a comment or whitespace don't include it
66 if len(line) == 0:
67 continue
69 elif line[0] == "data":
70 # we need to read up until we see "data" so the
71 # read-the-rest-of-the-payload operation is correct
72 reached_data = True
73 break
75 # save the keyed header data
76 header[line[0]] = line[1:]
78 if not reached_data:
79 raise ValueError("Didn't reach header termination magic word `data`")
81 if "dim" not in header.keys():
82 raise ValueError(
83 f"Malformed binvox header: `dim` is required, only received `{header.keys()}`"
84 )
86 # dimension of voxel array is required
87 shape = np.array(header["dim"], dtype=np.int64)
89 # provide default values for translation and scale
90 translate = np.array(header.get("translate", [0, 0, 0]), np.float64)
91 scale = np.array(header.get("scale", [1]), dtype=np.float64)
93 return shape, translate, scale[0]
96def parse_binvox(fp, writeable=False):
97 """
98 Read a binvox file, spec at
99 https://www.patrickmin.com/binvox/binvox.html
101 Parameters
102 ------------
103 fp: file-object
104 File like object with binvox file
106 Returns
107 ----------
108 binvox : namedtuple
109 Containing data
110 rle : numpy array
111 Run length encoded data
113 Raises
114 ------------
115 IOError
116 If invalid binvox file
117 """
118 # get the header info
119 shape, translate, scale = _parse_binvox_header(fp)
120 # get the rest of the file
121 data = fp.read()
122 # convert to numpy array
123 rle_data = np.frombuffer(data, dtype=np.uint8)
125 if writeable:
126 rle_data = rle_data.copy()
127 return Binvox(rle_data, shape, translate, scale)
130def binvox_header(shape, translate, scale):
131 """
132 Get a binvox header string.
134 Parameters
135 --------
136 shape: length 3 iterable of ints denoting shape of voxel grid.
137 translate: length 3 iterable of floats denoting translation.
138 scale: num length of entire voxel grid.
140 Returns
141 --------
142 string including "data\n" line.
143 """
144 sx, sy, sz = (int(s) for s in shape)
145 tx, ty, tz = translate
147 return f"""#binvox 1
148# generated in `trimesh`
149dim {sx} {sy} {sz}
150translate {tx} {ty} {tz}
151scale {scale}
152data
153"""
156def binvox_bytes(rle_data, shape, translate=(0, 0, 0), scale=1):
157 """Get a binary representation of binvox data.
159 Parameters
160 --------
161 rle_data : numpy array
162 Run-length encoded numpy array.
163 shape : (3,) int
164 Shape of voxel grid.
165 translate : (3,) float
166 Translation of voxels
167 scale : float
168 Length of entire voxel grid.
170 Returns
171 --------
172 data : bytes
173 Suitable for writing to binary file
174 """
175 if rle_data.dtype != np.uint8:
176 raise ValueError(f"rle_data.dtype must be np.uint8, got {rle_data.dtype}")
178 header = binvox_header(shape, translate, scale).encode()
179 return header + rle_data.tobytes()
182def voxel_from_binvox(rle_data, shape, translate=None, scale=1.0, axis_order="xzy"):
183 """
184 Factory for building from data associated with binvox files.
186 Parameters
187 ---------
188 rle_data : numpy
189 Run-length-encoded of flat voxel
190 values, or a `trimesh.rle.RunLengthEncoding` object.
191 See `trimesh.rle` documentation for description of encoding
192 shape : (3,) int
193 Shape of voxel grid.
194 translate : (3,) float
195 Translation of voxels
196 scale : float
197 Length of entire voxel grid.
198 encoded_axes : iterable
199 With values in ('x', 'y', 'z', 0, 1, 2),
200 where x => 0, y => 1, z => 2
201 denoting the order of axes in the encoded data. binvox by
202 default saves in xzy order, but using `xyz` (or (0, 1, 2)) will
203 be faster in some circumstances.
205 Returns
206 ---------
207 result : VoxelGrid
208 Loaded voxels
209 """
210 # shape must be uniform else scale is ambiguous
211 from .. import transformations
212 from ..voxel import encoding as enc
213 from ..voxel.base import VoxelGrid
215 if isinstance(rle_data, enc.RunLengthEncoding):
216 encoding = rle_data
217 else:
218 encoding = enc.RunLengthEncoding(rle_data, dtype=bool)
220 # translate = np.asanyarray(translate) * scale)
221 # translate = [0, 0, 0]
222 transform = transformations.scale_and_translate(
223 scale=scale / (np.array(shape) - 1), translate=translate
224 )
226 if axis_order == "xzy":
227 perm = (0, 2, 1)
228 shape = tuple(shape[p] for p in perm)
229 encoding = encoding.reshape(shape).transpose(perm)
230 elif axis_order is None or axis_order == "xyz":
231 encoding = encoding.reshape(shape)
232 else:
233 raise ValueError(
234 "Invalid axis_order '%s': must be None, 'xyz' or 'xzy'", axis_order
235 )
237 assert encoding.shape == shape
239 return VoxelGrid(encoding, transform)
242def load_binvox(file_obj, resolver=None, axis_order="xzy", file_type=None):
243 """
244 Load trimesh `VoxelGrid` instance from file.
246 Parameters
247 -----------
248 file_obj : file-like object
249 Contains binvox data
250 resolver : unused
251 axis_order : str
252 Order of axes in encoded data.
253 Binvox default is 'xzy', but 'xyz' may be faster
254 where this is not relevant.
256 Returns
257 ---------
258 result : trimesh.voxel.VoxelGrid
259 Loaded voxel data
260 """
261 if file_type is not None and file_type != "binvox":
262 raise ValueError(f"file_type must be None or binvox, got {file_type}")
263 data = parse_binvox(file_obj, writeable=True)
264 return voxel_from_binvox(
265 rle_data=data.rle_data,
266 shape=data.shape,
267 translate=data.translate,
268 scale=data.scale,
269 axis_order=axis_order,
270 )
273def export_binvox(voxel, axis_order="xzy"):
274 """
275 Export `trimesh.voxel.VoxelGrid` instance to bytes
277 Parameters
278 ------------
279 voxel : `trimesh.voxel.VoxelGrid`
280 Assumes axis ordering of `xyz` and encodes
281 in binvox default `xzy` ordering.
282 axis_order : str
283 Eements in ('x', 'y', 'z', 0, 1, 2), the order
284 of axes to encode data (standard is 'xzy' for binvox). `voxel`
285 data is assumed to be in order 'xyz'.
287 Returns
288 -----------
289 result : bytes
290 Representation according to binvox spec
291 """
292 translate = voxel.translation
293 scale = voxel.scale * (np.array(voxel.shape) - 1)
294 (neg_scale,) = np.where(scale < 0)
295 encoding = voxel.encoding.flip(neg_scale)
296 scale = np.abs(scale)
297 if not util.allclose(scale[0], scale[1:], 1e-6 * scale[0] + 1e-8):
298 raise ValueError("Can only export binvox with uniform scale")
299 scale = scale[0]
300 if axis_order == "xzy":
301 encoding = encoding.transpose((0, 2, 1))
302 elif axis_order != "xyz":
303 raise ValueError('Invalid axis_order: must be one of ("xyz", "xzy")')
304 rle_data = encoding.flat.run_length_data(dtype=np.uint8)
305 return binvox_bytes(rle_data, shape=voxel.shape, translate=translate, scale=scale)
308class Binvoxer:
309 """
310 Interface for binvox CL tool.
312 This class is responsible purely for making calls to the CL tool. It
313 makes no attempt to integrate with the rest of trimesh at all.
315 Constructor args configure command line options.
317 `Binvoxer.__call__` operates on the path to a mode file.
319 If using this interface in published works, please cite the references
320 below.
322 See CL tool website for further details.
324 https://www.patrickmin.com/binvox/
326 @article{nooruddin03,
327 author = {Fakir S. Nooruddin and Greg Turk},
328 title = {Simplification and Repair of Polygonal Models Using Volumetric
329 Techniques},
330 journal = {IEEE Transactions on Visualization and Computer Graphics},
331 volume = {9},
332 number = {2},
333 pages = {191--205},
334 year = {2003}
335 }
337 @Misc{binvox,
338 author = {Patrick Min},
339 title = {binvox},
340 howpublished = {{\tt http://www.patrickmin.com/binvox} or
341 {\tt https://www.google.com/search?q=binvox}},
342 year = {2004 - 2019},
343 note = {Accessed: yyyy-mm-dd}
344 }
345 """
347 SUPPORTED_INPUT_TYPES = (
348 "ug",
349 "obj",
350 "off",
351 "dfx",
352 "xgl",
353 "pov",
354 "brep",
355 "ply",
356 "jot",
357 )
359 SUPPORTED_OUTPUT_TYPES = (
360 "binvox",
361 "hips",
362 "mira",
363 "vtk",
364 "raw",
365 "schematic",
366 "msh",
367 )
369 def __init__(
370 self,
371 dimension=32,
372 file_type="binvox",
373 z_buffer_carving=True,
374 z_buffer_voting=True,
375 dilated_carving=False,
376 exact=True,
377 bounding_box=None,
378 remove_internal=False,
379 center=False,
380 rotate_x=0,
381 rotate_z=0,
382 wireframe=False,
383 fit=False,
384 block_id=None,
385 use_material_block_id=False,
386 use_offscreen_pbuffer=False,
387 downsample_factor=None,
388 downsample_threshold=None,
389 verbose=False,
390 binvox_path=None,
391 ):
392 """
393 Configure the voxelizer.
395 Parameters
396 ------------
397 dimension: voxel grid size (max 1024 when not using exact)
398 file_type: str
399 Output file type, supported types are:
400 'binvox'
401 'hips'
402 'mira'
403 'vtk'
404 'raw'
405 'schematic'
406 'msh'
407 z_buffer_carving : use z buffer based carving. At least one of
408 `z_buffer_carving` and `z_buffer_voting` must be True.
409 z_buffer_voting: use z-buffer based parity voting method.
410 dilated_carving: stop carving 1 voxel before intersection.
411 exact: any voxel with part of a triangle gets set. Does not use
412 graphics card.
413 bounding_box: 6-element float list/tuple of min, max values,
414 (minx, miny, minz, maxx, maxy, maxz)
415 remove_internal: remove internal voxels if True. Note there is some odd
416 behaviour if boundary voxels are occupied.
417 center: center model inside unit cube.
418 rotate_x: number of 90 degree ccw rotations around x-axis before
419 voxelizing.
420 rotate_z: number of 90 degree cw rotations around z-axis before
421 voxelizing.
422 wireframe: also render the model in wireframe (helps with thin parts).
423 fit: only write voxels in the voxel bounding box.
424 block_id: when converting to schematic, use this as the block ID.
425 use_matrial_block_id: when converting from obj to schematic, parse
426 block ID from material spec "usemtl blockid_<id>" (ids 1-255 only).
427 use_offscreen_pbuffer: use offscreen pbuffer instead of onscreen
428 window.
429 downsample_factor: downsample voxels by this factor in each dimension.
430 Must be a power of 2 or None. If not None/1 and `core dumped`
431 errors occur, try slightly adjusting dimensions.
432 downsample_threshold: when downsampling, destination voxel is on if
433 more than this number of voxels are on.
434 verbose : bool
435 If False, silences stdout/stderr from subprocess call.
436 binvox_path : str
437 Path to binvox executable. The default looks for an
438 executable called `binvox` on your `PATH`.
439 """
440 if binvox_path is None:
441 encoder = binvox_encoder
442 else:
443 encoder = binvox_path
445 if encoder is None:
446 raise OSError(
447 " ".join(
448 [
449 "No `binvox_path` provided and no binvox executable found",
450 "on PATH, please go to https://www.patrickmin.com/binvox/ and",
451 "download the appropriate version.",
452 ]
453 )
454 )
456 if dimension > 1024 and not exact:
457 raise ValueError("Maximum dimension using exact is 1024, got %d", dimension)
458 if file_type not in Binvoxer.SUPPORTED_OUTPUT_TYPES:
459 raise ValueError(
460 f"file_type {file_type} not in set of supported output types {Binvoxer.SUPPORTED_OUTPUT_TYPES!s}"
461 )
462 args = [encoder, "-d", str(dimension), "-t", file_type]
463 if exact:
464 args.append("-e")
465 if z_buffer_carving:
466 if z_buffer_voting:
467 pass
468 else:
469 args.append("-c")
470 elif z_buffer_voting:
471 args.append("-v")
472 else:
473 raise ValueError(
474 "One of `z_buffer_carving` or `z_buffer_voting` must be True"
475 )
476 if dilated_carving:
477 args.append("-dc")
479 # Additional parameters
480 if bounding_box is not None:
481 if len(bounding_box) != 6:
482 raise ValueError("bounding_box must have 6 elements")
483 args.append("-bb")
484 args.extend(str(b) for b in bounding_box)
485 if remove_internal:
486 args.append("-ri")
487 if center:
488 args.append("-cb")
489 args.extend(("-rotx",) * rotate_x)
490 args.extend(("-rotz",) * rotate_z)
491 if wireframe:
492 args.append("-aw")
493 if fit:
494 args.append("-fit")
495 if block_id is not None:
496 args.extend(("-bi", block_id))
497 if use_material_block_id:
498 args.append("-mb")
499 if use_offscreen_pbuffer:
500 args.append("-pb")
501 if downsample_factor is not None:
502 times = np.log2(downsample_factor)
503 if int(times) != times:
504 raise ValueError(
505 "downsample_factor must be a power of 2, got %d", downsample_factor
506 )
507 args.extend(("-down",) * int(times))
508 if downsample_threshold is not None:
509 args.extend(("-dmin", str(downsample_threshold)))
510 args.append("PATH")
511 self._args = args
512 self._file_type = file_type
514 self.verbose = verbose
516 @property
517 def file_type(self):
518 return self._file_type
520 def __call__(self, path, overwrite=False):
521 """
522 Create an voxel file in the same directory as model at `path`.
524 Parameters
525 ------------
526 path: string path to model file. Supported types:
527 'ug'
528 'obj'
529 'off'
530 'dfx'
531 'xgl'
532 'pov'
533 'brep'
534 'ply'
535 'jot' (polygongs only)
536 overwrite: if False, checks the output path (head.file_type) is empty
537 before running. If True and a file exists, raises an IOError.
539 Returns
540 ------------
541 string path to voxel file. File type give by file_type in constructor.
542 """
543 head, ext = os.path.splitext(path)
544 ext = ext[1:].lower()
545 if ext not in Binvoxer.SUPPORTED_INPUT_TYPES:
546 raise ValueError(
547 f"file_type {ext} not in set of supported input types {Binvoxer.SUPPORTED_INPUT_TYPES!s}"
548 )
549 out_path = f"{head}.{self._file_type}"
550 if os.path.isfile(out_path) and not overwrite:
551 raise OSError("Attempted to voxelize object at existing path")
552 self._args[-1] = path
554 # generalizes to python2 and python3
555 # will capture terminal output into variable rather than printing
556 verbosity = subprocess.check_output(self._args, stderr=subprocess.STDOUT)
558 # if requested print ourselves
559 if self.verbose:
560 util.log.debug(verbosity)
562 return out_path
565def voxelize_mesh(mesh, binvoxer=None, export_type="off", **binvoxer_kwargs):
566 """
567 Interface for voxelizing Trimesh object via the binvox tool.
569 Implementation simply saved the mesh in the specified export_type then
570 runs the `Binvoxer.__call__` (using either the supplied `binvoxer` or
571 creating one via `binvoxer_kwargs`)
573 Parameters
574 ------------
575 mesh: Trimesh object to voxelize.
576 binvoxer: optional Binvoxer instance.
577 export_type: file type to export mesh as temporarily for Binvoxer to
578 operate on.
579 **binvoxer_kwargs: kwargs for creating a new Binvoxer instance. If binvoxer
580 if provided, this must be empty.
582 Returns
583 ------------
584 `VoxelGrid` object resulting.
585 """
586 if not isinstance(mesh, Trimesh):
587 raise ValueError(f"mesh must be Trimesh instance, got {mesh!s}")
588 if binvoxer is None:
589 binvoxer = Binvoxer(**binvoxer_kwargs)
590 elif len(binvoxer_kwargs) > 0:
591 raise ValueError("Cannot provide binvoxer and binvoxer_kwargs")
592 if binvoxer.file_type != "binvox":
593 raise ValueError('Only "binvox" binvoxer `file_type` currently supported')
594 with TemporaryDirectory() as folder:
595 model_path = os.path.join(folder, f"model.{export_type}")
596 with open(model_path, "wb") as fp:
597 mesh.export(fp, file_type=export_type)
598 out_path = binvoxer(model_path)
599 with open(out_path, "rb") as fp:
600 out_model = load_binvox(fp)
602 return out_model
605_binvox_loaders = {"binvox": load_binvox}