hub/venv/lib/python3.7/site-packages/trimesh/exchange/binvox.py

577 lines
17 KiB
Python
Raw Normal View History

"""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}