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