577 lines
17 KiB
Python
577 lines
17 KiB
Python
|
"""Parsing functions for Binvox files.
|
||
|
|
||
|
https://www.patrickmin.com/binvox/binvox.html
|
||
|
|
||
|
Exporting meshes as binvox files requires binvox CL tool to be on your path.
|
||
|
"""
|
||
|
import os
|
||
|
import subprocess
|
||
|
import numpy as np
|
||
|
import collections
|
||
|
|
||
|
from distutils.spawn import find_executable
|
||
|
|
||
|
from .. import util
|
||
|
from ..base import Trimesh
|
||
|
|
||
|
# find the executable
|
||
|
binvox_encoder = find_executable('binvox')
|
||
|
|
||
|
Binvox = collections.namedtuple(
|
||
|
'Binvox', ['rle_data', 'shape', 'translate', 'scale'])
|
||
|
|
||
|
|
||
|
def parse_binvox_header(fp):
|
||
|
"""
|
||
|
Read the header from a binvox file.
|
||
|
Spec available:
|
||
|
https://www.patrickmin.com/binvox/binvox.html
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
fp: file-object
|
||
|
File like object with binvox file
|
||
|
|
||
|
Returns
|
||
|
----------
|
||
|
shape : tuple
|
||
|
Shape of binvox according to binvox spec
|
||
|
translate : tuple
|
||
|
Translation
|
||
|
scale : float
|
||
|
Scale of voxels
|
||
|
|
||
|
Raises
|
||
|
------------
|
||
|
IOError
|
||
|
If invalid binvox file.
|
||
|
"""
|
||
|
|
||
|
line = fp.readline().strip()
|
||
|
if hasattr(line, 'decode'):
|
||
|
binvox = b'#binvox'
|
||
|
space = b' '
|
||
|
else:
|
||
|
binvox = '#binvox'
|
||
|
space = ' '
|
||
|
if not line.startswith(binvox):
|
||
|
raise IOError('Not a binvox file')
|
||
|
shape = tuple(
|
||
|
int(s) for s in fp.readline().strip().split(space)[1:])
|
||
|
translate = tuple(
|
||
|
float(s) for s in fp.readline().strip().split(space)[1:])
|
||
|
scale = float(fp.readline().strip().split(space)[1])
|
||
|
fp.readline()
|
||
|
return shape, translate, scale
|
||
|
|
||
|
|
||
|
def parse_binvox(fp, writeable=False):
|
||
|
"""
|
||
|
Read a binvox file, spec at
|
||
|
https://www.patrickmin.com/binvox/binvox.html
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
fp: file-object
|
||
|
File like object with binvox file
|
||
|
|
||
|
Returns
|
||
|
----------
|
||
|
binvox : namedtuple
|
||
|
Containing data
|
||
|
rle : numpy array
|
||
|
Run length encoded data
|
||
|
|
||
|
Raises
|
||
|
------------
|
||
|
IOError
|
||
|
If invalid binvox file
|
||
|
"""
|
||
|
# get the header info
|
||
|
shape, translate, scale = parse_binvox_header(fp)
|
||
|
# get the rest of the file
|
||
|
data = fp.read()
|
||
|
# convert to numpy array
|
||
|
rle_data = np.frombuffer(data, dtype=np.uint8)
|
||
|
if writeable:
|
||
|
rle_data = rle_data.copy()
|
||
|
return Binvox(rle_data, shape, translate, scale)
|
||
|
|
||
|
|
||
|
_binvox_header = '''#binvox 1
|
||
|
dim {sx} {sy} {sz}
|
||
|
translate {tx} {ty} {tz}
|
||
|
scale {scale}
|
||
|
data
|
||
|
'''
|
||
|
|
||
|
|
||
|
def binvox_header(shape, translate, scale):
|
||
|
"""
|
||
|
Get a binvox header string.
|
||
|
|
||
|
Parameters
|
||
|
--------
|
||
|
shape: length 3 iterable of ints denoting shape of voxel grid.
|
||
|
translate: length 3 iterable of floats denoting translation.
|
||
|
scale: num length of entire voxel grid.
|
||
|
|
||
|
Returns
|
||
|
--------
|
||
|
string including "data\n" line.
|
||
|
"""
|
||
|
sx, sy, sz = (int(s) for s in shape)
|
||
|
tx, ty, tz = translate
|
||
|
return _binvox_header.format(
|
||
|
sx=sx, sy=sy, sz=sz, tx=tx, ty=ty, tz=tz, scale=scale)
|
||
|
|
||
|
|
||
|
def binvox_bytes(rle_data, shape, translate=(0, 0, 0), scale=1):
|
||
|
"""Get a binary representation of binvox data.
|
||
|
|
||
|
Parameters
|
||
|
--------
|
||
|
rle_data : numpy array
|
||
|
Run-length encoded numpy array.
|
||
|
shape : (3,) int
|
||
|
Shape of voxel grid.
|
||
|
translate : (3,) float
|
||
|
Translation of voxels
|
||
|
scale : float
|
||
|
Length of entire voxel grid.
|
||
|
|
||
|
Returns
|
||
|
--------
|
||
|
data : bytes
|
||
|
Suitable for writing to binary file
|
||
|
"""
|
||
|
if rle_data.dtype != np.uint8:
|
||
|
raise ValueError(
|
||
|
"rle_data.dtype must be np.uint8, got %s" % rle_data.dtype)
|
||
|
|
||
|
header = binvox_header(shape, translate, scale).encode()
|
||
|
return header + rle_data.tostring()
|
||
|
|
||
|
|
||
|
def voxel_from_binvox(
|
||
|
rle_data, shape, translate=None, scale=1.0, axis_order='xzy'):
|
||
|
"""
|
||
|
Factory for building from data associated with binvox files.
|
||
|
|
||
|
Parameters
|
||
|
---------
|
||
|
rle_data : numpy
|
||
|
Run-length-encoded of flat voxel
|
||
|
values, or a `trimesh.rle.RunLengthEncoding` object.
|
||
|
See `trimesh.rle` documentation for description of encoding
|
||
|
shape : (3,) int
|
||
|
Shape of voxel grid.
|
||
|
translate : (3,) float
|
||
|
Translation of voxels
|
||
|
scale : float
|
||
|
Length of entire voxel grid.
|
||
|
encoded_axes : iterable
|
||
|
With values in ('x', 'y', 'z', 0, 1, 2),
|
||
|
where x => 0, y => 1, z => 2
|
||
|
denoting the order of axes in the encoded data. binvox by
|
||
|
default saves in xzy order, but using `xyz` (or (0, 1, 2)) will
|
||
|
be faster in some circumstances.
|
||
|
|
||
|
Returns
|
||
|
---------
|
||
|
result : VoxelGrid
|
||
|
Loaded voxels
|
||
|
"""
|
||
|
# shape must be uniform else scale is ambiguous
|
||
|
from ..voxel import encoding as enc
|
||
|
from ..voxel.base import VoxelGrid
|
||
|
|
||
|
from .. import transformations
|
||
|
|
||
|
if isinstance(rle_data, enc.RunLengthEncoding):
|
||
|
encoding = rle_data
|
||
|
else:
|
||
|
encoding = enc.RunLengthEncoding(rle_data, dtype=bool)
|
||
|
|
||
|
# translate = np.asanyarray(translate) * scale)
|
||
|
# translate = [0, 0, 0]
|
||
|
transform = transformations.scale_and_translate(
|
||
|
scale=scale / (np.array(shape) - 1),
|
||
|
translate=translate)
|
||
|
|
||
|
if axis_order == 'xzy':
|
||
|
perm = (0, 2, 1)
|
||
|
shape = tuple(shape[p] for p in perm)
|
||
|
encoding = encoding.reshape(shape).transpose(perm)
|
||
|
elif axis_order is None or axis_order == 'xyz':
|
||
|
encoding = encoding.reshape(shape)
|
||
|
else:
|
||
|
raise ValueError(
|
||
|
"Invalid axis_order '%s': must be None, 'xyz' or 'xzy'")
|
||
|
|
||
|
assert(encoding.shape == shape)
|
||
|
return VoxelGrid(encoding, transform)
|
||
|
|
||
|
|
||
|
def load_binvox(file_obj,
|
||
|
resolver=None,
|
||
|
axis_order='xzy',
|
||
|
file_type=None):
|
||
|
"""
|
||
|
Load trimesh `VoxelGrid` instance from file.
|
||
|
|
||
|
Parameters
|
||
|
-----------
|
||
|
file_obj : file-like object
|
||
|
Contains binvox data
|
||
|
resolver : unused
|
||
|
axis_order : str
|
||
|
Order of axes in encoded data.
|
||
|
Binvox default is 'xzy', but 'xyz' may be faster
|
||
|
where this is not relevant.
|
||
|
|
||
|
Returns
|
||
|
---------
|
||
|
result : trimesh.voxel.VoxelGrid
|
||
|
Loaded voxel data
|
||
|
"""
|
||
|
if file_type is not None and file_type != 'binvox':
|
||
|
raise ValueError(
|
||
|
'file_type must be None or binvox, got %s' % file_type)
|
||
|
data = parse_binvox(file_obj, writeable=True)
|
||
|
return voxel_from_binvox(
|
||
|
rle_data=data.rle_data,
|
||
|
shape=data.shape,
|
||
|
translate=data.translate,
|
||
|
scale=data.scale,
|
||
|
axis_order=axis_order)
|
||
|
|
||
|
|
||
|
def export_binvox(voxel, axis_order='xzy'):
|
||
|
"""
|
||
|
Export `trimesh.voxel.VoxelGrid` instance to bytes
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
voxel : `trimesh.voxel.VoxelGrid`
|
||
|
Assumes axis ordering of `xyz` and encodes
|
||
|
in binvox default `xzy` ordering.
|
||
|
axis_order : str
|
||
|
Eements in ('x', 'y', 'z', 0, 1, 2), the order
|
||
|
of axes to encode data (standard is 'xzy' for binvox). `voxel`
|
||
|
data is assumed to be in order 'xyz'.
|
||
|
|
||
|
Returns
|
||
|
-----------
|
||
|
result : bytes
|
||
|
Representation according to binvox spec
|
||
|
"""
|
||
|
translate = voxel.translation
|
||
|
scale = voxel.scale * ((np.array(voxel.shape) - 1))
|
||
|
neg_scale, = np.where(scale < 0)
|
||
|
encoding = voxel.encoding.flip(neg_scale)
|
||
|
scale = np.abs(scale)
|
||
|
if not util.allclose(scale[0], scale[1:], 1e-6 * scale[0] + 1e-8):
|
||
|
raise ValueError('Can only export binvox with uniform scale')
|
||
|
scale = scale[0]
|
||
|
if axis_order == 'xzy':
|
||
|
encoding = encoding.transpose((0, 2, 1))
|
||
|
elif axis_order != 'xyz':
|
||
|
raise ValueError('Invalid axis_order: must be one of ("xyz", "xzy")')
|
||
|
rle_data = encoding.flat.run_length_data(dtype=np.uint8)
|
||
|
return binvox_bytes(
|
||
|
rle_data, shape=voxel.shape, translate=translate, scale=scale)
|
||
|
|
||
|
|
||
|
class Binvoxer(object):
|
||
|
"""
|
||
|
Interface for binvox CL tool.
|
||
|
|
||
|
This class is responsible purely for making calls to the CL tool. It
|
||
|
makes no attempt to integrate with the rest of trimesh at all.
|
||
|
|
||
|
Constructor args configure command line options.
|
||
|
|
||
|
`Binvoxer.__call__` operates on the path to a mode file.
|
||
|
|
||
|
If using this interface in published works, please cite the references
|
||
|
below.
|
||
|
|
||
|
See CL tool website for further details.
|
||
|
|
||
|
https://www.patrickmin.com/binvox/
|
||
|
|
||
|
@article{nooruddin03,
|
||
|
author = {Fakir S. Nooruddin and Greg Turk},
|
||
|
title = {Simplification and Repair of Polygonal Models Using Volumetric
|
||
|
Techniques},
|
||
|
journal = {IEEE Transactions on Visualization and Computer Graphics},
|
||
|
volume = {9},
|
||
|
number = {2},
|
||
|
pages = {191--205},
|
||
|
year = {2003}
|
||
|
}
|
||
|
|
||
|
@Misc{binvox,
|
||
|
author = {Patrick Min},
|
||
|
title = {binvox},
|
||
|
howpublished = {{\tt http://www.patrickmin.com/binvox} or
|
||
|
{\tt https://www.google.com/search?q=binvox}},
|
||
|
year = {2004 - 2019},
|
||
|
note = {Accessed: yyyy-mm-dd}
|
||
|
}
|
||
|
"""
|
||
|
|
||
|
SUPPORTED_INPUT_TYPES = (
|
||
|
'ug',
|
||
|
'obj',
|
||
|
'off',
|
||
|
'dfx',
|
||
|
'xgl',
|
||
|
'pov',
|
||
|
'brep',
|
||
|
'ply',
|
||
|
'jot',
|
||
|
)
|
||
|
|
||
|
SUPPORTED_OUTPUT_TYPES = (
|
||
|
'binvox',
|
||
|
'hips',
|
||
|
'mira',
|
||
|
'vtk',
|
||
|
'raw',
|
||
|
'schematic',
|
||
|
'msh',
|
||
|
)
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
dimension=32,
|
||
|
file_type='binvox',
|
||
|
z_buffer_carving=True,
|
||
|
z_buffer_voting=True,
|
||
|
dilated_carving=False,
|
||
|
exact=False,
|
||
|
bounding_box=None,
|
||
|
remove_internal=False,
|
||
|
center=False,
|
||
|
rotate_x=0,
|
||
|
rotate_z=0,
|
||
|
wireframe=False,
|
||
|
fit=False,
|
||
|
block_id=None,
|
||
|
use_material_block_id=False,
|
||
|
use_offscreen_pbuffer=True,
|
||
|
downsample_factor=None,
|
||
|
downsample_threshold=None,
|
||
|
verbose=False,
|
||
|
binvox_path=binvox_encoder,
|
||
|
):
|
||
|
"""
|
||
|
Configure the voxelizer.
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
dimension: voxel grid size (max 1024 when not using exact)
|
||
|
file_type: str
|
||
|
Output file type, supported types are:
|
||
|
'binvox'
|
||
|
'hips'
|
||
|
'mira'
|
||
|
'vtk'
|
||
|
'raw'
|
||
|
'schematic'
|
||
|
'msh'
|
||
|
z_buffer_carving : use z buffer based carving. At least one of
|
||
|
`z_buffer_carving` and `z_buffer_voting` must be True.
|
||
|
z_buffer_voting: use z-buffer based parity voting method.
|
||
|
dilated_carving: stop carving 1 voxel before intersection.
|
||
|
exact: any voxel with part of a triangle gets set. Does not use
|
||
|
graphics card.
|
||
|
bounding_box: 6-element float list/tuple of min, max values,
|
||
|
(minx, miny, minz, maxx, maxy, maxz)
|
||
|
remove_internal: remove internal voxels if True. Note there is some odd
|
||
|
behaviour if boundary voxels are occupied.
|
||
|
center: center model inside unit cube.
|
||
|
rotate_x: number of 90 degree ccw rotations around x-axis before
|
||
|
voxelizing.
|
||
|
rotate_z: number of 90 degree cw rotations around z-axis before
|
||
|
voxelizing.
|
||
|
wireframe: also render the model in wireframe (helps with thin parts).
|
||
|
fit: only write voxels in the voxel bounding box.
|
||
|
block_id: when converting to schematic, use this as the block ID.
|
||
|
use_matrial_block_id: when converting from obj to schematic, parse
|
||
|
block ID from material spec "usemtl blockid_<id>" (ids 1-255 only).
|
||
|
use_offscreen_pbuffer: use offscreen pbuffer instead of onscreen
|
||
|
window.
|
||
|
downsample_factor: downsample voxels by this factor in each dimension.
|
||
|
Must be a power of 2 or None. If not None/1 and `core dumped`
|
||
|
errors occur, try slightly adjusting dimensions.
|
||
|
downsample_threshold: when downsampling, destination voxel is on if
|
||
|
more than this number of voxels are on.
|
||
|
verbose: if False, silences stdout/stderr from subprocess call.
|
||
|
binvox_path: path to binvox executable. The default looks for an
|
||
|
executable called `binvox` on your `PATH`.
|
||
|
"""
|
||
|
if binvox_encoder is None:
|
||
|
raise IOError(
|
||
|
'No `binvox_path` provided, and no binvox executable found '
|
||
|
'on PATH. \nPlease go to https://www.patrickmin.com/binvox/ and '
|
||
|
'download the appropriate version.')
|
||
|
|
||
|
if dimension > 1024 and not exact:
|
||
|
raise ValueError(
|
||
|
'Maximum dimension using exact is 1024, got %d' % dimension)
|
||
|
if file_type not in Binvoxer.SUPPORTED_OUTPUT_TYPES:
|
||
|
raise ValueError(
|
||
|
'file_type %s not in set of supported output types %s' %
|
||
|
(file_type, str(Binvoxer.SUPPORTED_OUTPUT_TYPES)))
|
||
|
args = [binvox_path, '-d', str(dimension), '-t', file_type]
|
||
|
if exact:
|
||
|
args.append('-e')
|
||
|
if z_buffer_carving:
|
||
|
if z_buffer_voting:
|
||
|
pass
|
||
|
else:
|
||
|
args.append('-c')
|
||
|
elif z_buffer_voting:
|
||
|
args.append('-v')
|
||
|
else:
|
||
|
raise ValueError(
|
||
|
'At least one of `z_buffer_carving` or `z_buffer_voting` must '
|
||
|
'be True')
|
||
|
if dilated_carving:
|
||
|
args.append('-dc')
|
||
|
|
||
|
# Additional parameters
|
||
|
if bounding_box is not None:
|
||
|
if len(bounding_box) != 6:
|
||
|
raise ValueError('bounding_box must have 6 elements')
|
||
|
args.append('-bb')
|
||
|
args.extend(str(b) for b in bounding_box)
|
||
|
if remove_internal:
|
||
|
args.append('-ri')
|
||
|
if center:
|
||
|
args.append('-cb')
|
||
|
args.extend(('-rotx',) * rotate_x)
|
||
|
args.extend(('-rotz',) * rotate_z)
|
||
|
if wireframe:
|
||
|
args.append('-aw')
|
||
|
if fit:
|
||
|
args.append('-fit')
|
||
|
if block_id is not None:
|
||
|
args.extend(('-bi', block_id))
|
||
|
if use_material_block_id:
|
||
|
args.append('-mb')
|
||
|
if use_offscreen_pbuffer:
|
||
|
args.append('-pb')
|
||
|
if downsample_factor is not None:
|
||
|
times = np.log2(downsample_factor)
|
||
|
if int(times) != times:
|
||
|
raise ValueError(
|
||
|
'downsample_factor must be a power of 2, got %d'
|
||
|
% downsample_factor)
|
||
|
args.extend(('-down',) * int(times))
|
||
|
if downsample_threshold is not None:
|
||
|
args.extend(('-dmin', str(downsample_threshold)))
|
||
|
args.append('PATH')
|
||
|
self._args = args
|
||
|
self._file_type = file_type
|
||
|
|
||
|
self.verbose = verbose
|
||
|
|
||
|
@property
|
||
|
def file_type(self):
|
||
|
return self._file_type
|
||
|
|
||
|
def __call__(self, path, overwrite=False):
|
||
|
"""
|
||
|
Create an voxel file in the same directory as model at `path`.
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
path: string path to model file. Supported types:
|
||
|
'ug'
|
||
|
'obj'
|
||
|
'off'
|
||
|
'dfx'
|
||
|
'xgl'
|
||
|
'pov'
|
||
|
'brep'
|
||
|
'ply'
|
||
|
'jot' (polygongs only)
|
||
|
overwrite: if False, checks the output path (head.file_type) is empty
|
||
|
before running. If True and a file exists, raises an IOError.
|
||
|
|
||
|
Returns
|
||
|
------------
|
||
|
string path to voxel file. File type give by file_type in constructor.
|
||
|
"""
|
||
|
head, ext = os.path.splitext(path)
|
||
|
ext = ext[1:].lower()
|
||
|
if ext not in Binvoxer.SUPPORTED_INPUT_TYPES:
|
||
|
raise ValueError(
|
||
|
'file_type %s not in set of supported input types %s' %
|
||
|
(ext, str(Binvoxer.SUPPORTED_INPUT_TYPES)))
|
||
|
out_path = '%s.%s' % (head, self._file_type)
|
||
|
if os.path.isfile(out_path) and not overwrite:
|
||
|
raise IOError(
|
||
|
'Attempted to voxelize object a %s, but there is already a '
|
||
|
'file at output path %s' % (path, out_path))
|
||
|
self._args[-1] = path
|
||
|
|
||
|
# generalizes to python2 and python3
|
||
|
# will capture terminal output into variable rather than printing
|
||
|
verbosity = subprocess.check_output(self._args)
|
||
|
# if requested print ourselves
|
||
|
if self.verbose:
|
||
|
print(verbosity)
|
||
|
|
||
|
return out_path
|
||
|
|
||
|
|
||
|
def voxelize_mesh(mesh,
|
||
|
binvoxer=None,
|
||
|
export_type='off',
|
||
|
**binvoxer_kwargs):
|
||
|
"""
|
||
|
Interface for voxelizing Trimesh object via the binvox tool.
|
||
|
|
||
|
Implementation simply saved the mesh in the specified export_type then
|
||
|
runs the `Binvoxer.__call__` (using either the supplied `binvoxer` or
|
||
|
creating one via `binvoxer_kwargs`)
|
||
|
|
||
|
Parameters
|
||
|
------------
|
||
|
mesh: Trimesh object to voxelize.
|
||
|
binvoxer: optional Binvoxer instance.
|
||
|
export_type: file type to export mesh as temporarily for Binvoxer to
|
||
|
operate on.
|
||
|
**binvoxer_kwargs: kwargs for creating a new Binvoxer instance. If binvoxer
|
||
|
if provided, this must be empty.
|
||
|
|
||
|
Returns
|
||
|
------------
|
||
|
`VoxelGrid` object resulting.
|
||
|
"""
|
||
|
if not isinstance(mesh, Trimesh):
|
||
|
raise ValueError('mesh must be Trimesh instance, got %s' % str(mesh))
|
||
|
if binvoxer is None:
|
||
|
binvoxer = Binvoxer(**binvoxer_kwargs)
|
||
|
elif len(binvoxer_kwargs) > 0:
|
||
|
raise ValueError('Cannot provide binvoxer and binvoxer_kwargs')
|
||
|
if binvoxer.file_type != 'binvox':
|
||
|
raise ValueError(
|
||
|
'Only "binvox" binvoxer `file_type` currently supported')
|
||
|
with util.TemporaryDirectory() as folder:
|
||
|
model_path = os.path.join(folder, 'model.%s' % export_type)
|
||
|
with open(model_path, 'wb') as fp:
|
||
|
mesh.export(fp, file_type=export_type)
|
||
|
out_path = binvoxer(model_path)
|
||
|
with open(out_path, 'rb') as fp:
|
||
|
out_model = load_binvox(fp)
|
||
|
return out_model
|
||
|
|
||
|
|
||
|
_binvox_loaders = {'binvox': load_binvox}
|