ECON / lib /common /voxelize.py
Yuliang's picture
Support TEXTure
487ee6d
raw
history blame
10.3 kB
import os
import traceback
import numpy as np
import torch
import trimesh
from scipy import ndimage
from skimage.measure import block_reduce
from lib.common.libmesh.inside_mesh import check_mesh_contains
from lib.common.libvoxelize.voxelize import voxelize_mesh_
# From Occupancy Networks, Mescheder et. al. CVPR'19
def make_3d_grid(bb_min, bb_max, shape):
''' Makes a 3D grid.
Args:
bb_min (tuple): bounding box minimum
bb_max (tuple): bounding box maximum
shape (tuple): output shape
'''
size = shape[0] * shape[1] * shape[2]
pxs = torch.linspace(bb_min[0], bb_max[0], shape[0])
pys = torch.linspace(bb_min[1], bb_max[1], shape[1])
pzs = torch.linspace(bb_min[2], bb_max[2], shape[2])
pxs = pxs.view(-1, 1, 1).expand(*shape).contiguous().view(size)
pys = pys.view(1, -1, 1).expand(*shape).contiguous().view(size)
pzs = pzs.view(1, 1, -1).expand(*shape).contiguous().view(size)
p = torch.stack([pxs, pys, pzs], dim=1)
return p
class VoxelGrid:
def __init__(self, data, loc=(0., 0., 0.), scale=1):
assert (data.shape[0] == data.shape[1] == data.shape[2])
data = np.asarray(data, dtype=np.bool)
loc = np.asarray(loc)
self.data = data
self.loc = loc
self.scale = scale
@classmethod
def from_mesh(cls, mesh, resolution, loc=None, scale=None, method='ray'):
bounds = mesh.bounds
# Default location is center
if loc is None:
loc = (bounds[0] + bounds[1]) / 2
# Default scale, scales the mesh to [-0.45, 0.45]^3
if scale is None:
scale = (bounds[1] - bounds[0]).max() / 0.9
loc = np.asarray(loc)
scale = float(scale)
# Transform mesh
mesh = mesh.copy()
mesh.apply_translation(-loc)
mesh.apply_scale(1 / scale)
# Apply method
if method == 'ray':
voxel_data = voxelize_ray(mesh, resolution)
elif method == 'fill':
voxel_data = voxelize_fill(mesh, resolution)
voxels = cls(voxel_data, loc, scale)
return voxels
def down_sample(self, factor=2):
if not (self.resolution % factor) == 0:
raise ValueError('Resolution must be divisible by factor.')
new_data = block_reduce(self.data, (factor, ) * 3, np.max)
return VoxelGrid(new_data, self.loc, self.scale)
def to_mesh(self):
# Shorthand
occ = self.data
# Shape of voxel grid
nx, ny, nz = occ.shape
# Shape of corresponding occupancy grid
grid_shape = (nx + 1, ny + 1, nz + 1)
# Convert values to occupancies
occ = np.pad(occ, 1, 'constant')
# Determine if face present
f1_r = (occ[:-1, 1:-1, 1:-1] & ~occ[1:, 1:-1, 1:-1])
f2_r = (occ[1:-1, :-1, 1:-1] & ~occ[1:-1, 1:, 1:-1])
f3_r = (occ[1:-1, 1:-1, :-1] & ~occ[1:-1, 1:-1, 1:])
f1_l = (~occ[:-1, 1:-1, 1:-1] & occ[1:, 1:-1, 1:-1])
f2_l = (~occ[1:-1, :-1, 1:-1] & occ[1:-1, 1:, 1:-1])
f3_l = (~occ[1:-1, 1:-1, :-1] & occ[1:-1, 1:-1, 1:])
f1 = f1_r | f1_l
f2 = f2_r | f2_l
f3 = f3_r | f3_l
assert (f1.shape == (nx + 1, ny, nz))
assert (f2.shape == (nx, ny + 1, nz))
assert (f3.shape == (nx, ny, nz + 1))
# Determine if vertex present
v = np.full(grid_shape, False)
v[:, :-1, :-1] |= f1
v[:, :-1, 1:] |= f1
v[:, 1:, :-1] |= f1
v[:, 1:, 1:] |= f1
v[:-1, :, :-1] |= f2
v[:-1, :, 1:] |= f2
v[1:, :, :-1] |= f2
v[1:, :, 1:] |= f2
v[:-1, :-1, :] |= f3
v[:-1, 1:, :] |= f3
v[1:, :-1, :] |= f3
v[1:, 1:, :] |= f3
# Calculate indices for vertices
n_vertices = v.sum()
v_idx = np.full(grid_shape, -1)
v_idx[v] = np.arange(n_vertices)
# Vertices
v_x, v_y, v_z = np.where(v)
v_x = v_x / nx - 0.5
v_y = v_y / ny - 0.5
v_z = v_z / nz - 0.5
vertices = np.stack([v_x, v_y, v_z], axis=1)
# Face indices
f1_l_x, f1_l_y, f1_l_z = np.where(f1_l)
f2_l_x, f2_l_y, f2_l_z = np.where(f2_l)
f3_l_x, f3_l_y, f3_l_z = np.where(f3_l)
f1_r_x, f1_r_y, f1_r_z = np.where(f1_r)
f2_r_x, f2_r_y, f2_r_z = np.where(f2_r)
f3_r_x, f3_r_y, f3_r_z = np.where(f3_r)
faces_1_l = np.stack([
v_idx[f1_l_x, f1_l_y, f1_l_z],
v_idx[f1_l_x, f1_l_y, f1_l_z + 1],
v_idx[f1_l_x, f1_l_y + 1, f1_l_z + 1],
v_idx[f1_l_x, f1_l_y + 1, f1_l_z],
],
axis=1)
faces_1_r = np.stack([
v_idx[f1_r_x, f1_r_y, f1_r_z],
v_idx[f1_r_x, f1_r_y + 1, f1_r_z],
v_idx[f1_r_x, f1_r_y + 1, f1_r_z + 1],
v_idx[f1_r_x, f1_r_y, f1_r_z + 1],
],
axis=1)
faces_2_l = np.stack([
v_idx[f2_l_x, f2_l_y, f2_l_z],
v_idx[f2_l_x + 1, f2_l_y, f2_l_z],
v_idx[f2_l_x + 1, f2_l_y, f2_l_z + 1],
v_idx[f2_l_x, f2_l_y, f2_l_z + 1],
],
axis=1)
faces_2_r = np.stack([
v_idx[f2_r_x, f2_r_y, f2_r_z],
v_idx[f2_r_x, f2_r_y, f2_r_z + 1],
v_idx[f2_r_x + 1, f2_r_y, f2_r_z + 1],
v_idx[f2_r_x + 1, f2_r_y, f2_r_z],
],
axis=1)
faces_3_l = np.stack([
v_idx[f3_l_x, f3_l_y, f3_l_z],
v_idx[f3_l_x, f3_l_y + 1, f3_l_z],
v_idx[f3_l_x + 1, f3_l_y + 1, f3_l_z],
v_idx[f3_l_x + 1, f3_l_y, f3_l_z],
],
axis=1)
faces_3_r = np.stack([
v_idx[f3_r_x, f3_r_y, f3_r_z],
v_idx[f3_r_x + 1, f3_r_y, f3_r_z],
v_idx[f3_r_x + 1, f3_r_y + 1, f3_r_z],
v_idx[f3_r_x, f3_r_y + 1, f3_r_z],
],
axis=1)
faces = np.concatenate([
faces_1_l,
faces_1_r,
faces_2_l,
faces_2_r,
faces_3_l,
faces_3_r,
],
axis=0)
vertices = self.loc + self.scale * vertices
mesh = trimesh.Trimesh(vertices, faces, process=False)
return mesh
@property
def resolution(self):
assert (self.data.shape[0] == self.data.shape[1] == self.data.shape[2])
return self.data.shape[0]
def contains(self, points):
nx = self.resolution
# Rescale bounding box to [-0.5, 0.5]^3
points = (points - self.loc) / self.scale
# Discretize points to [0, nx-1]^3
points_i = ((points + 0.5) * nx).astype(np.int32)
# i1, i2, i3 have sizes (batch_size, T)
i1, i2, i3 = points_i[..., 0], points_i[..., 1], points_i[..., 2]
# Only use indices inside bounding box
mask = ((i1 >= 0) & (i2 >= 0) & (i3 >= 0) & (nx > i1) & (nx > i2) & (nx > i3))
# Prevent out of bounds error
i1 = i1[mask]
i2 = i2[mask]
i3 = i3[mask]
# Compute values, default value outside box is 0
occ = np.zeros(points.shape[:-1], dtype=np.bool)
occ[mask] = self.data[i1, i2, i3]
return occ
def voxelize_ray(mesh, resolution):
occ_surface = voxelize_surface(mesh, resolution)
# TODO: use surface voxels here?
occ_interior = voxelize_interior(mesh, resolution)
occ = (occ_interior | occ_surface)
return occ
def voxelize_fill(mesh, resolution):
bounds = mesh.bounds
if (np.abs(bounds) >= 0.5).any():
raise ValueError('voxelize fill is only supported if mesh is inside [-0.5, 0.5]^3/')
occ = voxelize_surface(mesh, resolution)
occ = ndimage.morphology.binary_fill_holes(occ)
return occ
def voxelize_surface(mesh, resolution):
vertices = mesh.vertices
faces = mesh.faces
vertices = (vertices + 0.5) * resolution
face_loc = vertices[faces]
occ = np.full((resolution, ) * 3, 0, dtype=np.int32)
face_loc = face_loc.astype(np.float32)
voxelize_mesh_(occ, face_loc)
occ = (occ != 0)
return occ
def voxelize_interior(mesh, resolution):
shape = (resolution, ) * 3
bb_min = (0.5, ) * 3
bb_max = (resolution - 0.5, ) * 3
# Create points. Add noise to break symmetry
points = make_3d_grid(bb_min, bb_max, shape=shape).numpy()
points = points + 0.1 * (np.random.rand(*points.shape) - 0.5)
points = (points / resolution - 0.5)
occ = check_mesh_contains(mesh, points)[0]
occ = occ.reshape(shape)
return occ
def check_voxel_occupied(occupancy_grid):
occ = occupancy_grid
occupied = (
occ[..., :-1, :-1, :-1] & occ[..., :-1, :-1, 1:] & occ[..., :-1, 1:, :-1] &
occ[..., :-1, 1:, 1:] & occ[..., 1:, :-1, :-1] & occ[..., 1:, :-1, 1:] &
occ[..., 1:, 1:, :-1] & occ[..., 1:, 1:, 1:]
)
return occupied
def check_voxel_unoccupied(occupancy_grid):
occ = occupancy_grid
unoccupied = ~(
occ[..., :-1, :-1, :-1] | occ[..., :-1, :-1, 1:] | occ[..., :-1, 1:, :-1] |
occ[..., :-1, 1:, 1:] | occ[..., 1:, :-1, :-1] | occ[..., 1:, :-1, 1:] |
occ[..., 1:, 1:, :-1] | occ[..., 1:, 1:, 1:]
)
return unoccupied
def check_voxel_boundary(occupancy_grid):
occupied = check_voxel_occupied(occupancy_grid)
unoccupied = check_voxel_unoccupied(occupancy_grid)
return ~occupied & ~unoccupied
def voxelize(in_path, res):
try:
filename = os.path.join(in_path, 'voxelization_{}.npy'.format(res))
if os.path.exists(filename):
return
mesh = trimesh.load(in_path + '/isosurf_scaled.off', process=False)
occupancies = VoxelGrid.from_mesh(mesh, res, loc=[0, 0, 0], scale=1).data
occupancies = np.reshape(occupancies, -1)
if not occupancies.any():
raise ValueError('No empty voxel grids allowed.')
occupancies = np.packbits(occupancies)
np.save(filename, occupancies)
except Exception as err:
path = os.path.normpath(in_path)
print('Error with {}: {}'.format(path, traceback.format_exc()))
print('finished {}'.format(in_path))