init
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
import numpy as np
|
||||
|
||||
from .. import util
|
||||
from ..constants import log
|
||||
from ..typed import ArrayLike, Number, Optional, Union
|
||||
|
||||
|
||||
def fill_orthographic(dense):
|
||||
shape = dense.shape
|
||||
indices = np.stack(
|
||||
np.meshgrid(*(np.arange(s) for s in shape), indexing="ij"), axis=-1
|
||||
)
|
||||
empty = np.logical_not(dense)
|
||||
|
||||
def fill_axis(axis):
|
||||
base_local_indices = indices[..., axis]
|
||||
local_indices = base_local_indices.copy()
|
||||
local_indices[empty] = shape[axis]
|
||||
mins = np.min(local_indices, axis=axis, keepdims=True)
|
||||
local_indices = base_local_indices.copy()
|
||||
local_indices[empty] = -1
|
||||
maxs = np.max(local_indices, axis=axis, keepdims=True)
|
||||
|
||||
return np.logical_and(
|
||||
base_local_indices >= mins,
|
||||
base_local_indices <= maxs,
|
||||
)
|
||||
|
||||
filled = fill_axis(axis=0)
|
||||
for axis in range(1, len(shape)):
|
||||
filled = np.logical_and(filled, fill_axis(axis))
|
||||
return filled
|
||||
|
||||
|
||||
def fill_base(sparse_indices):
|
||||
"""
|
||||
Given a sparse surface voxelization, fill in between columns.
|
||||
|
||||
Parameters
|
||||
--------------
|
||||
sparse_indices: (n, 3) int, location of filled cells
|
||||
|
||||
Returns
|
||||
--------------
|
||||
filled: (m, 3) int, location of filled cells
|
||||
"""
|
||||
# validate inputs
|
||||
sparse_indices = np.asanyarray(sparse_indices, dtype=np.int64)
|
||||
if not util.is_shape(sparse_indices, (-1, 3)):
|
||||
raise ValueError("incorrect shape")
|
||||
|
||||
# create grid and mark inner voxels
|
||||
max_value = sparse_indices.max() + 3
|
||||
|
||||
grid = np.zeros((max_value, max_value, max_value), bool)
|
||||
voxels_sparse = np.add(sparse_indices, 1)
|
||||
|
||||
grid[tuple(voxels_sparse.T)] = 1
|
||||
|
||||
for i in range(max_value):
|
||||
check_dir2 = False
|
||||
for j in range(0, max_value - 1):
|
||||
idx = []
|
||||
# find transitions first
|
||||
# transition positions are from 0 to 1 and from 1 to 0
|
||||
eq = np.equal(grid[i, j, :-1], grid[i, j, 1:])
|
||||
idx = np.where(np.logical_not(eq))[0] + 1
|
||||
c = len(idx)
|
||||
check_dir2 = (c % 4) > 0 and c > 4
|
||||
if c < 4:
|
||||
continue
|
||||
for s in range(0, c - c % 4, 4):
|
||||
grid[i, j, idx[s] : idx[s + 3]] = 1
|
||||
if not check_dir2:
|
||||
continue
|
||||
|
||||
# check another direction for robustness
|
||||
for k in range(0, max_value - 1):
|
||||
idx = []
|
||||
# find transitions first
|
||||
eq = np.equal(grid[i, :-1, k], grid[i, 1:, k])
|
||||
idx = np.where(np.logical_not(eq))[0] + 1
|
||||
c = len(idx)
|
||||
if c < 4:
|
||||
continue
|
||||
for s in range(0, c - c % 4, 4):
|
||||
grid[i, idx[s] : idx[s + 3], k] = 1
|
||||
|
||||
# generate new voxels
|
||||
filled = np.column_stack(np.where(grid))
|
||||
filled -= 1
|
||||
|
||||
return filled
|
||||
|
||||
|
||||
fill_voxelization = fill_base
|
||||
|
||||
|
||||
def matrix_to_marching_cubes(
|
||||
matrix: ArrayLike,
|
||||
pitch: Union[Number, ArrayLike] = 1.0,
|
||||
threshold: Optional[Number] = None,
|
||||
):
|
||||
"""
|
||||
Convert an (n, m, p) matrix into a mesh, using marching_cubes.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
matrix : (n, m, p) bool
|
||||
Occupancy array
|
||||
pitch : float or length-3 tuple of floats, optional
|
||||
Voxel spacing in each dimension
|
||||
threshold : float or None, optional
|
||||
If specified, converts the input into a boolean
|
||||
matrix by considering values above `threshold` as True
|
||||
|
||||
|
||||
Returns
|
||||
----------
|
||||
mesh : trimesh.Trimesh
|
||||
Mesh generated by meshing voxels using
|
||||
the marching cubes algorithm in skimage
|
||||
"""
|
||||
from skimage import measure
|
||||
|
||||
from ..base import Trimesh
|
||||
|
||||
if threshold is not None:
|
||||
matrix = np.asanyarray(matrix) > threshold
|
||||
else:
|
||||
matrix = np.asanyarray(matrix, dtype=bool)
|
||||
|
||||
rev_matrix = np.logical_not(matrix) # Takes set about 0.
|
||||
# Add in padding so marching cubes can function properly with
|
||||
# voxels on edge of AABB
|
||||
pad_width = 1
|
||||
rev_matrix = np.pad(
|
||||
rev_matrix, pad_width=(pad_width), mode="constant", constant_values=(1)
|
||||
)
|
||||
|
||||
# pick between old and new API
|
||||
if hasattr(measure, "marching_cubes_lewiner"):
|
||||
func = measure.marching_cubes_lewiner
|
||||
else:
|
||||
func = measure.marching_cubes
|
||||
|
||||
# Run marching cubes.
|
||||
pitch = np.asanyarray(pitch)
|
||||
if pitch.size == 1:
|
||||
pitch = (pitch,) * 3
|
||||
meshed = func(
|
||||
volume=rev_matrix,
|
||||
level=0.5,
|
||||
spacing=pitch, # it is a boolean voxel grid
|
||||
)
|
||||
|
||||
# allow results from either marching cubes function in skimage
|
||||
# binaries available for python 3.3 and 3.4 appear to use the classic
|
||||
# method
|
||||
if len(meshed) == 2:
|
||||
log.warning("using old marching cubes, may not be watertight!")
|
||||
vertices, faces = meshed
|
||||
normals = None
|
||||
elif len(meshed) == 4:
|
||||
vertices, faces, normals, _vals = meshed
|
||||
|
||||
# Return to the origin, add in the pad_width
|
||||
vertices = np.subtract(vertices, pad_width * pitch)
|
||||
# create the mesh
|
||||
mesh = Trimesh(vertices=vertices, faces=faces, vertex_normals=normals)
|
||||
return mesh
|
||||
|
||||
|
||||
def sparse_to_matrix(sparse):
|
||||
"""
|
||||
Take a sparse (n,3) list of integer indexes of filled cells,
|
||||
turn it into a dense (m,o,p) matrix.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
sparse : (n, 3) int
|
||||
Index of filled cells
|
||||
|
||||
Returns
|
||||
------------
|
||||
dense : (m, o, p) bool
|
||||
Matrix of filled cells
|
||||
"""
|
||||
|
||||
sparse = np.asanyarray(sparse, dtype=np.int64)
|
||||
if not util.is_shape(sparse, (-1, 3)):
|
||||
raise ValueError("sparse must be (n,3)!")
|
||||
|
||||
shape = sparse.max(axis=0) + 1
|
||||
matrix = np.zeros(np.prod(shape), dtype=bool)
|
||||
multiplier = np.array([np.prod(shape[1:]), shape[2], 1])
|
||||
|
||||
index = (sparse * multiplier).sum(axis=1)
|
||||
matrix[index] = True
|
||||
|
||||
dense = matrix.reshape(shape)
|
||||
return dense
|
||||
|
||||
|
||||
def points_to_marching_cubes(points, pitch=1.0):
|
||||
"""
|
||||
Mesh points by assuming they fill a voxel box, and then
|
||||
running marching cubes on them
|
||||
|
||||
Parameters
|
||||
------------
|
||||
points : (n, 3) float
|
||||
Points in 3D space
|
||||
|
||||
Returns
|
||||
-------------
|
||||
mesh : trimesh.Trimesh
|
||||
Points meshed using marching cubes
|
||||
"""
|
||||
# make sure inputs are as expected
|
||||
points = np.asanyarray(points, dtype=np.float64)
|
||||
pitch = np.asanyarray(pitch, dtype=float)
|
||||
|
||||
# find the minimum value of points for origin
|
||||
origin = points.min(axis=0)
|
||||
# convert points to occupied voxel cells
|
||||
index = ((points - origin) / pitch).round().astype(np.int64)
|
||||
|
||||
# convert voxel indices to a matrix
|
||||
matrix = sparse_to_matrix(index)
|
||||
|
||||
# run marching cubes on the matrix to generate a mesh
|
||||
mesh = matrix_to_marching_cubes(matrix, pitch=pitch)
|
||||
mesh.vertices += origin
|
||||
|
||||
return mesh
|
||||
|
||||
|
||||
def multibox(centers, pitch=1.0, colors=None, remove_internal_faces=False):
|
||||
"""
|
||||
Return a Trimesh object with a box at every center.
|
||||
|
||||
Doesn't do anything nice or fancy.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
centers : (n, 3) float
|
||||
Center of boxes that are occupied
|
||||
pitch : float
|
||||
The edge length of a voxel
|
||||
colors : (3,) or (4,) or (n,3) or (n, 4) float
|
||||
Color of boxes
|
||||
remove_internal_faces : bool
|
||||
If True, removes internal faces shared between adjacent boxes
|
||||
|
||||
Returns
|
||||
---------
|
||||
rough : Trimesh
|
||||
Mesh object representing inputs
|
||||
"""
|
||||
from .. import primitives
|
||||
from ..base import Trimesh
|
||||
|
||||
# get centers as numpy array
|
||||
centers = np.asanyarray(centers, dtype=np.float64)
|
||||
|
||||
# get a basic box
|
||||
b = primitives.Box()
|
||||
# apply the pitch
|
||||
b.apply_scale(float(pitch))
|
||||
# tile into one box vertex per center
|
||||
v = np.tile(centers, (1, len(b.vertices))).reshape((-1, 3))
|
||||
# offset to centers
|
||||
v += np.tile(b.vertices, (len(centers), 1))
|
||||
|
||||
f = np.tile(b.faces, (len(centers), 1))
|
||||
f += np.repeat(np.arange(len(centers)) * len(b.vertices), len(b.faces))[:, None]
|
||||
|
||||
if remove_internal_faces:
|
||||
# Get 12 unit normals (1 per triangle face) indicating face direction
|
||||
base_normals = np.round(b.face_normals).astype(int) # (12, 3)
|
||||
# Expand those directions across all voxel boxes so as to check neighbor presence
|
||||
face_normals = np.tile(base_normals, (len(centers), 1)) # (len(centers) * 12, 3)
|
||||
|
||||
# Maps each face to the voxel box it came from
|
||||
face_voxel_idx = np.repeat(
|
||||
np.arange(len(centers)), len(b.faces)
|
||||
) # (len(centers) * 12, )
|
||||
# Converts voxel centers to discrete grid coordinates
|
||||
voxel_coords = np.round(centers / pitch).astype(int) # (len(centers), 3)
|
||||
# Creates a fast lookup structure for checking voxel neighbors
|
||||
voxel_set = set(map(tuple, voxel_coords))
|
||||
|
||||
# Gets the grid coordinate of the voxel that owns each face
|
||||
voxel_face_coords = voxel_coords[face_voxel_idx]
|
||||
# Computes the adjacent voxel coordinate in the face direction
|
||||
neighbor_coords = voxel_face_coords + face_normals
|
||||
# Keeps only faces whose neighboring voxel does not exist
|
||||
keep_mask = np.array([tuple(c) not in voxel_set for c in neighbor_coords])
|
||||
else:
|
||||
keep_mask = np.ones(len(f), dtype=bool)
|
||||
|
||||
face_colors = None
|
||||
if colors is not None:
|
||||
colors = np.asarray(colors)
|
||||
if colors.ndim == 1:
|
||||
colors = colors[None].repeat(len(centers), axis=0)
|
||||
if colors.ndim == 2 and len(colors) == len(centers):
|
||||
face_colors = colors.repeat(12, axis=0)[keep_mask]
|
||||
|
||||
mesh = Trimesh(vertices=v, faces=f[keep_mask], face_colors=face_colors)
|
||||
|
||||
return mesh
|
||||
|
||||
|
||||
def boolean_sparse(a, b, operation=np.logical_and):
|
||||
"""
|
||||
Find common rows between two arrays very quickly
|
||||
using 3D boolean sparse matrices.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
a: (n, d) int, coordinates in space
|
||||
b: (m, d) int, coordinates in space
|
||||
operation: numpy operation function, ie:
|
||||
np.logical_and
|
||||
np.logical_or
|
||||
|
||||
Returns
|
||||
-----------
|
||||
coords: (q, d) int, coordinates in space
|
||||
"""
|
||||
# 3D sparse arrays, using wrapped scipy.sparse
|
||||
# pip install sparse
|
||||
import sparse
|
||||
|
||||
# find the bounding box of both arrays
|
||||
extrema = np.array([a.min(axis=0), a.max(axis=0), b.min(axis=0), b.max(axis=0)])
|
||||
origin = extrema.min(axis=0) - 1
|
||||
size = tuple(np.ptp(extrema, axis=0) + 2)
|
||||
|
||||
# put nearby voxel arrays into same shape sparse array
|
||||
sp_a = sparse.COO((a - origin).T, data=np.ones(len(a), dtype=bool), shape=size)
|
||||
sp_b = sparse.COO((b - origin).T, data=np.ones(len(b), dtype=bool), shape=size)
|
||||
|
||||
# apply the logical operation
|
||||
# get a sparse matrix out
|
||||
applied = operation(sp_a, sp_b)
|
||||
# reconstruct the original coordinates
|
||||
coords = np.column_stack(applied.coords) + origin
|
||||
|
||||
return coords
|
||||
|
||||
|
||||
def strip_array(data):
|
||||
shape = data.shape
|
||||
ndims = len(shape)
|
||||
padding = []
|
||||
slices = []
|
||||
for dim in range(len(shape)):
|
||||
axis = tuple(range(dim)) + tuple(range(dim + 1, ndims))
|
||||
filled = np.any(data, axis=axis)
|
||||
(indices,) = np.nonzero(filled)
|
||||
pad_left = indices[0]
|
||||
pad_right = indices[-1]
|
||||
padding.append([pad_left, pad_right])
|
||||
slices.append(slice(pad_left, pad_right))
|
||||
return data[tuple(slices)], np.array(padding, int)
|
||||
|
||||
|
||||
def indices_to_points(indices, pitch=None, origin=None):
|
||||
"""
|
||||
Convert indices of an (n,m,p) matrix into a set of voxel center points.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
indices: (q, 3) int, index of voxel matrix (n,m,p)
|
||||
pitch: float, what pitch was the voxel matrix computed with
|
||||
origin: (3,) float, what is the origin of the voxel matrix
|
||||
|
||||
Returns
|
||||
----------
|
||||
points: (q, 3) float, list of points
|
||||
"""
|
||||
indices = np.asanyarray(indices)
|
||||
if indices.shape[1:] != (3,):
|
||||
raise ValueError("shape of indices must be (q, 3)")
|
||||
|
||||
points = np.array(indices, dtype=np.float64)
|
||||
if pitch is not None:
|
||||
points *= float(pitch)
|
||||
if origin is not None:
|
||||
origin = np.asanyarray(origin)
|
||||
if origin.shape != (3,):
|
||||
raise ValueError("shape of origin must be (3,)")
|
||||
points += origin
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def matrix_to_points(matrix, pitch=None, origin=None):
|
||||
"""
|
||||
Convert an (n,m,p) matrix into a set of points for each voxel center.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
matrix: (n,m,p) bool, voxel matrix
|
||||
pitch: float, what pitch was the voxel matrix computed with
|
||||
origin: (3,) float, what is the origin of the voxel matrix
|
||||
|
||||
Returns
|
||||
----------
|
||||
points: (q, 3) list of points
|
||||
"""
|
||||
indices = np.column_stack(np.nonzero(matrix))
|
||||
points = indices_to_points(indices=indices, pitch=pitch, origin=origin)
|
||||
return points
|
||||
|
||||
|
||||
def points_to_indices(points, pitch=None, origin=None):
|
||||
"""
|
||||
Convert center points of an (n,m,p) matrix into its indices.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
points : (q, 3) float
|
||||
Center points of voxel matrix (n,m,p)
|
||||
pitch : float
|
||||
What pitch was the voxel matrix computed with
|
||||
origin : (3,) float
|
||||
What is the origin of the voxel matrix
|
||||
|
||||
Returns
|
||||
----------
|
||||
indices : (q, 3) int
|
||||
List of indices
|
||||
"""
|
||||
points = np.array(points, dtype=np.float64)
|
||||
if points.shape != (points.shape[0], 3):
|
||||
raise ValueError("shape of points must be (q, 3)")
|
||||
|
||||
if origin is not None:
|
||||
origin = np.asanyarray(origin)
|
||||
if origin.shape != (3,):
|
||||
raise ValueError("shape of origin must be (3,)")
|
||||
points -= origin
|
||||
if pitch is not None:
|
||||
points /= pitch
|
||||
|
||||
origin = np.asanyarray(origin, dtype=np.float64)
|
||||
pitch = float(pitch)
|
||||
|
||||
indices = np.round(points).astype(int)
|
||||
return indices
|
||||
Reference in New Issue
Block a user