|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
""" |
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
values, counts = raw_data[::2], raw_data[1::2] |
|
data = np.repeat(values, counts).astype(np.bool) |
|
data = data.reshape(dims) |
|
if fix_coords: |
|
|
|
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) |
|
|
|
|
|
|
|
|
|
x = nz_voxels / (dims[0] * dims[1]) |
|
zwpy = nz_voxels % (dims[0] * dims[1]) |
|
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(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 |
|
|
|
xyz = voxel_data.astype(np.int) |
|
|
|
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 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: |
|
|
|
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() |
|
|
|
|
|
state = voxels_flat[0] |
|
ctr = 0 |
|
for c in voxels_flat: |
|
if c == state: |
|
ctr += 1 |
|
|
|
if ctr == 255: |
|
fp.write(chr(state)) |
|
fp.write(chr(ctr)) |
|
ctr = 0 |
|
else: |
|
|
|
fp.write(chr(state)) |
|
fp.write(chr(ctr)) |
|
state = c |
|
ctr = 1 |
|
|
|
if ctr > 0: |
|
fp.write(chr(state)) |
|
fp.write(chr(ctr)) |
|
|
|
|
|
if __name__ == '__main__': |
|
import doctest |
|
doctest.testmod() |
|
|