Spaces:
Running
on
L40S
Running
on
L40S
# Copyright (C) 2012 Daniel Maturana | |
# This file is part of binvox-rw-py. | |
# | |
# binvox-rw-py is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# binvox-rw-py is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with binvox-rw-py. If not, see <http://www.gnu.org/licenses/>. | |
# | |
# Modified by Christopher B. Choy <chrischoy at ai dot stanford dot edu> | |
# for python 3 support | |
""" | |
Binvox to Numpy and back. | |
import numpy as np | |
import binvox_rw | |
with open('chair.binvox', 'rb') as f: | |
m1 = binvox_rw.read_as_3d_array(f) | |
... | |
m1.dims | |
[32, 32, 32] | |
m1.scale | |
41.133000000000003 | |
m1.translate | |
[0.0, 0.0, 0.0] | |
with open('chair_out.binvox', 'wb') as f: | |
m1.write(f) | |
... | |
with open('chair_out.binvox', 'rb') as f: | |
m2 = binvox_rw.read_as_3d_array(f) | |
... | |
m1.dims==m2.dims | |
True | |
m1.scale==m2.scale | |
True | |
m1.translate==m2.translate | |
True | |
np.all(m1.data==m2.data) | |
True | |
with open('chair.binvox', 'rb') as f: | |
md = binvox_rw.read_as_3d_array(f) | |
... | |
with open('chair.binvox', 'rb') as f: | |
ms = binvox_rw.read_as_coord_array(f) | |
... | |
data_ds = binvox_rw.dense_to_sparse(md.data) | |
data_sd = binvox_rw.sparse_to_dense(ms.data, 32) | |
np.all(data_sd==md.data) | |
True | |
# the ordering of elements returned by numpy.nonzero changes with axis | |
# ordering, so to compare for equality we first lexically sort the voxels. | |
np.all(ms.data[:, np.lexsort(ms.data)] == data_ds[:, np.lexsort(data_ds)]) | |
True | |
""" | |
import numpy as np | |
class Voxels(object): | |
""" Holds a binvox model. | |
data is either a three-dimensional numpy boolean array (dense representation) | |
or a two-dimensional numpy float array (coordinate representation). | |
dims, translate and scale are the model metadata. | |
dims are the voxel dimensions, e.g. [32, 32, 32] for a 32x32x32 model. | |
scale and translate relate the voxels to the original model coordinates. | |
To translate voxel coordinates i, j, k to original coordinates x, y, z: | |
x_n = (i+.5)/dims[0] | |
y_n = (j+.5)/dims[1] | |
z_n = (k+.5)/dims[2] | |
x = scale*x_n + translate[0] | |
y = scale*y_n + translate[1] | |
z = scale*z_n + translate[2] | |
""" | |
def __init__(self, data, dims, translate, scale, axis_order): | |
self.data = data | |
self.dims = dims | |
self.translate = translate | |
self.scale = scale | |
assert (axis_order in ('xzy', 'xyz')) | |
self.axis_order = axis_order | |
def clone(self): | |
data = self.data.copy() | |
dims = self.dims[:] | |
translate = self.translate[:] | |
return Voxels(data, dims, translate, self.scale, self.axis_order) | |
def write(self, fp): | |
write(self, fp) | |
def read_header(fp): | |
""" Read binvox header. Mostly meant for internal use. | |
""" | |
line = fp.readline().strip() | |
if not line.startswith(b'#binvox'): | |
raise IOError('Not a binvox file') | |
dims = [int(i) for i in fp.readline().strip().split(b' ')[1:]] | |
translate = [float(i) for i in fp.readline().strip().split(b' ')[1:]] | |
scale = [float(i) for i in fp.readline().strip().split(b' ')[1:]][0] | |
line = fp.readline() | |
return dims, translate, scale | |
def read_as_3d_array(fp, fix_coords=True): | |
""" Read binary binvox format as array. | |
Returns the model with accompanying metadata. | |
Voxels are stored in a three-dimensional numpy array, which is simple and | |
direct, but may use a lot of memory for large models. (Storage requirements | |
are 8*(d^3) bytes, where d is the dimensions of the binvox model. Numpy | |
boolean arrays use a byte per element). | |
Doesn't do any checks on input except for the '#binvox' line. | |
""" | |
dims, translate, scale = read_header(fp) | |
raw_data = np.frombuffer(fp.read(), dtype=np.uint8) | |
# if just using reshape() on the raw data: | |
# indexing the array as array[i,j,k], the indices map into the | |
# coords as: | |
# i -> x | |
# j -> z | |
# k -> y | |
# if fix_coords is true, then data is rearranged so that | |
# mapping is | |
# i -> x | |
# j -> y | |
# k -> z | |
values, counts = raw_data[::2], raw_data[1::2] | |
data = np.repeat(values, counts).astype(np.bool) | |
data = data.reshape(dims) | |
if fix_coords: | |
# xzy to xyz TODO the right thing | |
data = np.transpose(data, (0, 2, 1)) | |
axis_order = 'xyz' | |
else: | |
axis_order = 'xzy' | |
return Voxels(data, dims, translate, scale, axis_order) | |
def read_as_coord_array(fp, fix_coords=True): | |
""" Read binary binvox format as coordinates. | |
Returns binvox model with voxels in a "coordinate" representation, i.e. an | |
3 x N array where N is the number of nonzero voxels. Each column | |
corresponds to a nonzero voxel and the 3 rows are the (x, z, y) coordinates | |
of the voxel. (The odd ordering is due to the way binvox format lays out | |
data). Note that coordinates refer to the binvox voxels, without any | |
scaling or translation. | |
Use this to save memory if your model is very sparse (mostly empty). | |
Doesn't do any checks on input except for the '#binvox' line. | |
""" | |
dims, translate, scale = read_header(fp) | |
raw_data = np.frombuffer(fp.read(), dtype=np.uint8) | |
values, counts = raw_data[::2], raw_data[1::2] | |
sz = np.prod(dims) | |
index, end_index = 0, 0 | |
end_indices = np.cumsum(counts) | |
indices = np.concatenate(([0], end_indices[:-1])).astype(end_indices.dtype) | |
values = values.astype(np.bool) | |
indices = indices[values] | |
end_indices = end_indices[values] | |
nz_voxels = [] | |
for index, end_index in zip(indices, end_indices): | |
nz_voxels.extend(range(index, end_index)) | |
nz_voxels = np.array(nz_voxels) | |
# TODO are these dims correct? | |
# according to docs, | |
# index = x * wxh + z * width + y; // wxh = width * height = d * d | |
x = nz_voxels / (dims[0] * dims[1]) | |
zwpy = nz_voxels % (dims[0] * dims[1]) # z*w + y | |
z = zwpy / dims[0] | |
y = zwpy % dims[0] | |
if fix_coords: | |
data = np.vstack((x, y, z)) | |
axis_order = 'xyz' | |
else: | |
data = np.vstack((x, z, y)) | |
axis_order = 'xzy' | |
#return Voxels(data, dims, translate, scale, axis_order) | |
return Voxels(np.ascontiguousarray(data), dims, translate, scale, axis_order) | |
def dense_to_sparse(voxel_data, dtype=np.int): | |
""" From dense representation to sparse (coordinate) representation. | |
No coordinate reordering. | |
""" | |
if voxel_data.ndim != 3: | |
raise ValueError('voxel_data is wrong shape; should be 3D array.') | |
return np.asarray(np.nonzero(voxel_data), dtype) | |
def sparse_to_dense(voxel_data, dims, dtype=np.bool): | |
if voxel_data.ndim != 2 or voxel_data.shape[0] != 3: | |
raise ValueError('voxel_data is wrong shape; should be 3xN array.') | |
if np.isscalar(dims): | |
dims = [dims] * 3 | |
dims = np.atleast_2d(dims).T | |
# truncate to integers | |
xyz = voxel_data.astype(np.int) | |
# discard voxels that fall outside dims | |
valid_ix = ~np.any((xyz < 0) | (xyz >= dims), 0) | |
xyz = xyz[:, valid_ix] | |
out = np.zeros(dims.flatten(), dtype=dtype) | |
out[tuple(xyz)] = True | |
return out | |
#def get_linear_index(x, y, z, dims): | |
#""" Assuming xzy order. (y increasing fastest. | |
#TODO ensure this is right when dims are not all same | |
#""" | |
#return x*(dims[1]*dims[2]) + z*dims[1] + y | |
def write(voxel_model, fp): | |
""" Write binary binvox format. | |
Note that when saving a model in sparse (coordinate) format, it is first | |
converted to dense format. | |
Doesn't check if the model is 'sane'. | |
""" | |
if voxel_model.data.ndim == 2: | |
# TODO avoid conversion to dense | |
dense_voxel_data = sparse_to_dense(voxel_model.data, voxel_model.dims) | |
else: | |
dense_voxel_data = voxel_model.data | |
fp.write('#binvox 1\n') | |
fp.write('dim ' + ' '.join(map(str, voxel_model.dims)) + '\n') | |
fp.write('translate ' + ' '.join(map(str, voxel_model.translate)) + '\n') | |
fp.write('scale ' + str(voxel_model.scale) + '\n') | |
fp.write('data\n') | |
if not voxel_model.axis_order in ('xzy', 'xyz'): | |
raise ValueError('Unsupported voxel model axis order') | |
if voxel_model.axis_order == 'xzy': | |
voxels_flat = dense_voxel_data.flatten() | |
elif voxel_model.axis_order == 'xyz': | |
voxels_flat = np.transpose(dense_voxel_data, (0, 2, 1)).flatten() | |
# keep a sort of state machine for writing run length encoding | |
state = voxels_flat[0] | |
ctr = 0 | |
for c in voxels_flat: | |
if c == state: | |
ctr += 1 | |
# if ctr hits max, dump | |
if ctr == 255: | |
fp.write(chr(state)) | |
fp.write(chr(ctr)) | |
ctr = 0 | |
else: | |
# if switch state, dump | |
fp.write(chr(state)) | |
fp.write(chr(ctr)) | |
state = c | |
ctr = 1 | |
# flush out remainders | |
if ctr > 0: | |
fp.write(chr(state)) | |
fp.write(chr(ctr)) | |
if __name__ == '__main__': | |
import doctest | |
doctest.testmod() | |