Skip to content

Instantly share code, notes, and snippets.

@chrischoy
Last active February 26, 2023 16:08
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save chrischoy/c15bb11cf976a9f5504be2f6758b41dd to your computer and use it in GitHub Desktop.
Save chrischoy/c15bb11cf976a9f5504be2f6758b41dd to your computer and use it in GitHub Desktop.
ShapeNet Voxelization
# 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()
"""
Written by Christopher B. Choy <chrischoy@ai.stanford.edu>
Choy et al., 3D-R2N2: A Unified Approach for Single and Multi-view 3D Object Reconstruction, ECCV 2016
"""
import _init_paths
import os
import numpy as np
from lib.utils import stdout_redirected
from tempfile import TemporaryFile
import tools.binvox_rw as binvox_rw
def evaluate_voxel_prediction(preds, gt, thresh):
preds_occupy = preds[:, 1, :, :] >= thresh
diff = np.sum(np.logical_xor(preds_occupy, gt[:, 1, :, :]))
intersection = np.sum(np.logical_and(preds_occupy, gt[:, 1, :, :]))
union = np.sum(np.logical_or(preds_occupy, gt[:, 1, :, :]))
num_fp = np.sum(np.logical_and(preds_occupy, gt[:, 0, :, :])) # false positive
num_fn = np.sum(np.logical_and(np.logical_not(preds_occupy), gt[:, 1, :, :])) # false negative
return np.array([diff, intersection, union, num_fp, num_fn])
def voxelize_model_binvox(obj, n_vox, return_voxel=True, binvox_add_param=''):
cmd = "./tools/binvox -d %d -cb -dc -aw -pb %s -t binvox %s" % (
n_vox, binvox_add_param, obj)
if not os.path.exists(obj):
raise ValueError('No obj found : %s' % obj)
# Stop printing command line output
with TemporaryFile() as f, stdout_redirected(f):
os.system(cmd)
# load voxelized model
if return_voxel:
with open('%s.binvox' % obj[:-4], 'rb') as f:
vox = binvox_rw.read_as_3d_array(f)
return vox.data
"""
Written by Christopher B. Choy <chrischoy@ai.stanford.edu>
Choy et al., 3D-R2N2: A Unified Approach for Single and Multi-view 3D Object Reconstruction, ECCV 2016
"""
import _init_paths
import sys
import os
import json
import traceback
from multiprocessing import Process
from lib.data_io import get_model_file, get_voxel_file
from lib.config import cfg
from lib.voxel import voxelize_model_binvox
USE_XVFP_SERVER = False
USE_SUBPROCESS = True
N_VOX = 64
OVERWRITE = True
def voxelize_model_subprocess(category_id, model_list_path, n_vox=N_VOX):
model_ids = [line.rstrip('\n') for line in
open(os.path.join(model_list_path, 'models.txt'), 'r')]
for i, model_id in enumerate(model_ids):
model_fn = get_model_file(category_id, model_id)
print('voxelizing %d/%d: %s' % (i+1, len(model_ids), model_fn))
if not OVERWRITE and os.path.exists(model_fn):
print('Already voxelized, skipping')
continue
sys.stdout.flush() # To push print while running inside a Process
voxelize_model_binvox(model_fn, n_vox, return_voxel=False)
def main():
cats = json.load(open(cfg.DATASET))
pl = []
# Use binvox server
if USE_XVFP_SERVER:
os.system('Xvfb :99 -screen 0 640x480x24 &')
os.system('export DISPLAY=:99')
# setup
for category_id, cat in cats.items():
model_list_path = cat['dir']
if USE_SUBPROCESS:
p = Process(target=voxelize_model_subprocess,
args=(category_id, model_list_path))
p.start()
pl.append(p)
else:
voxelize_model_subprocess(category_id, model_list_path)
if USE_SUBPROCESS:
for p in pl:
p.join()
if __name__ == '__main__':
main()
@crysoberil
Copy link

crysoberil commented May 5, 2021

Can you tell me how to transform the voxels into a pointcloud aligned with the original mesh for the ShapeNet voxels you released? I tried using the transformation mentioned in https://gist.github.com/chrischoy/c15bb11cf976a9f5504be2f6758b41dd#file-binvox_rw-py-L81-L86, but this does not align the pointcloud with the original mesh. Attached is an example for class=02691156, model=72fcc9dfb927311935fc197bbabcd5bd. The problem seems to be with how exactly to use the translate parameter in the binvox metadata.

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment