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