Module voxelfuse.voxel_model
VoxelModel Class
Copyright 2021 - Cole Brauer, Dan Aukes
Expand source code
"""
VoxelModel Class
----
Copyright 2021 - Cole Brauer, Dan Aukes
"""
import os
import subprocess
import numpy as np
import meshio
import k3d
import zlib
import base64
from enum import Enum
from typing import Union as TypeUnion, Tuple, TextIO
from tqdm import tqdm
from scipy import ndimage
from numba import njit, prange, cuda
from pyvox.parser import VoxParser
from voxelfuse.materials import *
# Floating point error threshold for rounding to zero
FLOATING_ERROR = 0.0000000001
class Axes(Enum):
"""
Options for axes and planes.
"""
X = (1,0,0)
Y = (0,1,0)
Z = (0,0,1)
XY = (1,1,0)
XZ = (1,0,1)
YZ = (0,1,1)
XYZ = (1,1,1)
class Dir(Enum):
"""
Options for projection directions.
"""
UP = 1
DOWN = 2
BOTH = 3
class Process(Enum):
"""
Options for manufacturing process types.
"""
LASER = 1
MILL = 2
PRINT = 3
CAST = 4
INSERT = 5
class Struct(Enum):
"""
Options for structuring element shapes.
"""
STANDARD = 1
SPHERE = 2
class VoxelModel:
"""
VoxelModel object that stores geometry, position, and material information.
"""
def __init__(self, voxels: np.ndarray, materials: TypeUnion[int, np.ndarray] = None, coords: Tuple[int, int, int] = (0, 0, 0), resolution: float = 1):
"""
Initialize a VoxelModel object.
Args:
voxels: Array storing the index of the material present at each voxel
materials: Material index (int), or array of all material mixtures present in model with material format: (a, m0, m1, ... mn)
coords: Model origin coordinates
resolution: Number of voxels per mm (higher number = finer resolution)
"""
self.voxels = np.copy(voxels) # Use np.copy to break references
# Determine how materials were specified and create the materials array accordingly
if materials is None:
self.materials = generateMaterials(1)
elif isinstance(materials, int):
self.materials = generateMaterials(materials)
else:
self.materials = np.copy(materials)
self.coords = coords
self.resolution = resolution
self.components = np.zeros_like(voxels)
self.numComponents = 0
@classmethod
def fromVoxFile(cls, filename: str, coords: Tuple[int, int, int] = (0, 0, 0), resolution: float = 1):
"""
Create a VoxelModel from an imported .vox file.
----
Example:
``model1 = vf.VoxelModel.fromVoxFile('cylinder-red.vox', (0, 5, 0), 1)``
----
Args:
filename: File name with extension
coords: Model origin coordinates
resolution: Number of voxels per mm
Returns:
VoxelModel
"""
# Import data and align axes
v1 = VoxParser(filename).parse()
v2 = np.array(v1.to_dense(), dtype=np.uint16)
v2 = np.flip(v2, 1)
v2 = np.rot90(v2, 1, (2, 0))
v2 = np.rot90(v2, 1, (1, 2))
# Generate materials table assuming indices match materials in material_properties
i = 0
materials = np.zeros((1, len(material_properties) + 1), dtype=np.float32)
for m in np.unique(v2):
if m != 0:
i = i+1
material_vector = np.zeros(len(material_properties) + 1, dtype=np.float32)
material_vector[0] = 1
material_vector[m+1] = 1
materials = np.vstack((materials, material_vector))
v2[v2 == m] = i
return cls(v2, materials, coords=coords, resolution=resolution)
@classmethod
def fromMeshFile(cls, filename: str, coords: Tuple[int, int, int] = (0, 0, 0), material: int = 1, resolution: float = 1, gmsh_on_path: bool = False):
"""
Create a VoxelModel from an imported mesh file.
----
Example:
``model1 = vf.VoxelModel.fromMeshFile('center.stl', (67, 3, 0), 2, 1)``
____
Args:
filename: File name with extension
coords: Model origin coordinates
material: Material id corresponding to materials.py
resolution: Number of voxels per mm
gmsh_on_path: Enable/disable using system gmsh rather than bundled gmsh
Returns:
VoxelModel
"""
data = makeMesh(filename, True, gmsh_on_path)
points = data.points
# Get lists of indices of point
# ii_tri = data.cells_dict['triangle']
ii_tet = data.cells_dict['tetra']
# Convert lists of indices to lists of points
# tris = points[ii_tri]
tets = points[ii_tet]
# Create barycentric coordinate system
T = np.concatenate((tets, tets[:, :, 0:1] * 0 + 1), 2)
T_inv = np.zeros(T.shape)
for ii, t in enumerate(T):
T_inv[ii] = np.linalg.inv(t).T
# Find bounding box
base = 1 / resolution
points_min = points.min(0)
points_max = points.max(0)
points_min_r = base * np.round(points_min/base)
points_max_r = base * np.round(points_max/base)
# Create 3D grid
xx = np.r_[points_min_r[0]:points_max_r[0]+1:base]
yy = np.r_[points_min_r[1]:points_max_r[1]+1:base]
zz = np.r_[points_min_r[2]:points_max_r[2]+1:base]
# Find center of every grid point
xx_mid = (xx[1:] + xx[:-1]) / 2
yy_mid = (yy[1:] + yy[:-1]) / 2
zz_mid = (zz[1:] + zz[:-1]) / 2
# Create grid of voxel centers
xyz_mid = np.array(np.meshgrid(xx_mid, yy_mid, zz_mid, indexing='ij'))
xyz_mid = xyz_mid.transpose(1, 2, 3, 0)
# Convert to list of points
xyz_mid = xyz_mid.reshape(-1, 3)
# Add 1 to allow conversion to barycentric coordinates
xyz_mid = np.concatenate((xyz_mid, xyz_mid[:, 0:1] * 0 + 1), 1)
# Create list of indices of each voxel
ijk_mid = np.array(np.meshgrid(np.r_[:len(xx_mid)], np.r_[:len(yy_mid)], np.r_[:len(zz_mid)], indexing='ij'))
ijk_mid = ijk_mid.transpose(1, 2, 3, 0)
ijk_mid2 = ijk_mid.reshape(-1, 3)
f3 = findFilledVoxels(np.asarray(T_inv, order='c'), np.asarray(xyz_mid, order='c'))
ii, jj = f3.nonzero()
lmn = ijk_mid2[np.unique(jj)]
voxels = np.zeros(ijk_mid.shape[:3], dtype=np.bool_)
voxels[lmn[:, 0], lmn[:, 1], lmn[:, 2]] = True
new_model = cls(voxels, generateMaterials(material), coords=coords, resolution=resolution).fitWorkspace()
return new_model
@classmethod
def empty(cls, size: Tuple[int, int, int], coords: Tuple[int, int, int] = (0, 0, 0), resolution: float = 1, num_materials: int = len(material_properties)):
"""
Initialize an empty VoxelModel.
Args:
size: Size of the empty model in voxels
coords: Model origin coordinates
resolution: Number of voxels per mm
num_materials: Number of material types in materials vector
Returns:
VoxelModel
"""
model_data = np.zeros(size, dtype=np.uint16)
materials = np.zeros((1, num_materials + 1), dtype=np.float32)
new_model = cls(model_data, materials, coords=coords, resolution=resolution)
return new_model
@classmethod
def emptyLike(cls, voxel_model):
"""
Initialize an empty VoxelModel with the same size, materials, coords, and resolution as another model.
Args:
voxel_model: Reference VoxelModel object
Returns:
VoxelModel
"""
new_model = cls(np.zeros_like(voxel_model.voxels, dtype=np.uint16), voxel_model.materials, coords=voxel_model.coords, resolution=voxel_model.resolution)
return new_model
@classmethod
def copy(cls, voxel_model):
"""
Initialize an VoxelModel that is a copy of another model.
Args:
voxel_model: Reference VoxelModel object
Returns:
VoxelModel
"""
new_model = cls(voxel_model.voxels, voxel_model.materials, coords=voxel_model.coords, resolution=voxel_model.resolution)
new_model.numComponents = voxel_model.numComponents
new_model.components = voxel_model.components
return new_model
# Property update operations ##############################
def setResolution(self, res: float):
"""
Change the resolution of a model.
Args:
res: aaa
Returns:
None
"""
new_model = VoxelModel.copy(self)
new_model.resolution = res
return new_model
def fitWorkspace(self):
"""
Remove excess empty space from a model.
Resize the workspace around a model to remove excess empty space.
Model coordinates are updated to reflect the change.
Returns:
VoxelModel
"""
x_len = self.voxels.shape[0]
y_len = self.voxels.shape[1]
z_len = self.voxels.shape[2]
x_min = -1
x_max = -1
y_min = -1
y_max = -1
z_min = -1
z_max = -1
for x in range(x_len):
if np.sum(self.voxels[x, :, :]) > 0:
x_min = x
break
for x in range(x_len-1,-1,-1):
if np.sum(self.voxels[x, :, :]) > 0:
x_max = x+1
break
for y in range(y_len):
if np.sum(self.voxels[:, y, :]) > 0:
y_min = y
break
for y in range(y_len-1,-1,-1):
if np.sum(self.voxels[:, y, :]) > 0:
y_max = y+1
break
for z in range(z_len):
if np.sum(self.voxels[:, :, z]) > 0:
z_min = z
break
for z in range(z_len-1,-1,-1):
if np.sum(self.voxels[:, :, z]) > 0:
z_max = z+1
break
x_min = 0 if x_min == -1 else x_min
y_min = 0 if y_min == -1 else y_min
z_min = 0 if z_min == -1 else z_min
x_max = x_len if x_max == -1 else x_max
y_max = y_len if y_max == -1 else y_max
z_max = z_len if z_max == -1 else z_max
new_voxels = np.copy(self.voxels[x_min:x_max, y_min:y_max, z_min:z_max])
new_components = np.copy(self.components[x_min:x_max, y_min:y_max, z_min:z_max])
new_coords = (self.coords[0] + x_min, self.coords[1] + y_min, self.coords[2] + z_min)
new_model = VoxelModel(new_voxels, self.materials, coords=new_coords, resolution=self.resolution)
new_model.numComponents = self.numComponents
new_model.components = new_components
return new_model
def removeDuplicateMaterials(self):
"""
Remove duplicate rows from a model's material array.
This function can be greatly accelerated using CUDA. For more information on how to enable CUDA in VoxelFuse,
see the GpuSettings class.
Returns:
VoxelModel
"""
new_voxels = np.copy(self.voxels)
new_materials = np.unique(self.materials, axis=0)
# Get CUDA settings
try:
CUDA_enable = bool(os.environ.get('VF_CUDA_ENABLE'))
CUDA_device = int(os.environ.get('VF_CUDA_DEVICE'))
except TypeError:
print('CUDA environment variables not found, defaulting to CUDA disabled')
CUDA_enable = False
CUDA_device = 0
if CUDA_enable:
# Select GPU
cuda.select_device(CUDA_device)
# CUDA blocks
blockdim = (8, 8, 8) # 512 threads (1024 threads max)
griddim = (new_voxels.shape[0] // blockdim[0] + 1, new_voxels.shape[1] // blockdim[1] + 1, new_voxels.shape[2] // blockdim[2] + 1)
# Update material indices
updateMatIndices[griddim, blockdim](new_voxels, self.materials, new_materials)
else:
x_len, y_len, z_len = self.voxels.shape
for x in tqdm(range(x_len), desc='Removing duplicate materials'):
for y in range(y_len):
for z in range(z_len):
i = self.voxels[x, y, z]
m = self.materials[i]
ni = np.where(np.equal(new_materials, m).all(1))[0][0]
new_voxels[x, y, z] = ni
return VoxelModel(new_voxels, new_materials, self.coords, self.resolution)
def getComponents(self, connectivity: int = 1):
"""
Update component labels for a model.
This function uses a disconnected components algorithm and assumes that adjacent
voxels with different materials are connected. Connectivity can be set to 1-3
and defines the shape of the structuring element.
Args:
connectivity: Connectivity of structuring element (1-3)
Returns:
VoxelModel
"""
mask = np.array(self.voxels[:, :, :] > 0, dtype=np.bool_)
struct = ndimage.generate_binary_structure(3, connectivity)
new_model = VoxelModel.copy(self)
new_model.components, new_model.numComponents = ndimage.label(mask, structure=struct)
new_model.components = np.uint8(new_model.components)
return new_model
# Selection operations ##############################
# TODO: Should this reference the material properties table?
# TODO: isolateMaterialVector
def isolateMaterial(self, material: int):
"""
Get all voxels with a specified material.
----
Example:
``model2 = model1.isolateMaterial(4)``
----
Args:
material: Material index corresponding to the materials array for the model
Returns:
VoxelModel
"""
mask = np.array(self.voxels == material, dtype=np.bool_)
materials = np.zeros((2, self.materials.shape[1]), dtype=np.float32)
materials[1] = self.materials[material]
return VoxelModel(mask.astype(int), materials, self.coords, self.resolution)
def isolateLayer(self, layer: int):
"""
Get all voxels in a specified layer.
----
Example:
``model2 = model1.isolateLayer(8)``
----
Args:
layer: Voxel layer to isolate
Returns:
VoxelModel
"""
new_voxels = np.zeros_like(self.voxels, dtype=np.uint16)
new_voxels[:, :, layer - self.coords[2]] = self.voxels[:, :, layer - self.coords[2]]
return VoxelModel(new_voxels, self.materials, self.coords, self.resolution)
def isolateComponent(self, component: int):
"""
Isolate a component by its component label.
Component labels must first be updated with getComponents.
Unrecognized component labels will return an empty object.
Args:
component: Component label to isolate
Returns:
VoxelModel
"""
mask = np.array(self.components == component, dtype=np.bool_)
new_voxels = np.multiply(self.voxels, mask)
return VoxelModel(new_voxels, self.materials, self.coords, self.resolution)
# Mask operations ##############################
# Material defaults to the first material in the input model
def getUnoccupied(self):
"""
Get all voxels not occupied by the input model.
This operation can also be applied using the invert operator (~).
----
Examples:
``model2 = model1.getUnoccupied()``
``model2 = ~model1``
----
Returns:
VoxelModel
"""
mask = np.array(self.voxels == 0, dtype=np.bool_)
return VoxelModel(mask, self.materials[0:2, :], self.coords, self.resolution)
def __invert__(self):
"""
Get all voxels not occupied by the input model.
Overload invert operator (~) for VoxelModel objects with getUnoccupied().
Returns:
VoxelModel
"""
return self.getUnoccupied()
def getOccupied(self):
"""
Get all voxels occupied by the input model.
Returns:
VoxelModel
"""
mask = np.array(self.voxels != 0, dtype=np.bool_)
return VoxelModel(mask, self.materials[0:2, :], self.coords, self.resolution)
def getBoundingBox(self):
"""
Get all voxels contained in the bounding box of the input model.
Returns:
VoxelModel
"""
new_model = VoxelModel.copy(self)
new_model = new_model.fitWorkspace()
new_model.voxels.fill(1)
new_model = new_model.getOccupied()
new_model.materials = self.materials[0:2, :]
return new_model
def setMaterial(self, material: int):
"""
Set the material of all voxels in a model.
----
Example:
``model2 = model1.getBoundingBox()``
``model3 = model2.setMaterial(2)``
----
Args:
material: Material id corresponding to materials.py
Returns:
VoxelModel
"""
new_voxels = self.getOccupied().voxels # Converts input model to a mask, no effect if input is already a mask
material_vector = np.zeros(self.materials.shape[1], dtype=np.float32)
material_vector[0] = 1
material_vector[material+1] = 1
a = np.zeros(self.materials.shape[1], dtype=np.float32)
b = material_vector
m = np.vstack((a, b))
return VoxelModel(new_voxels, m, self.coords, self.resolution)
def setMaterialVector(self, material_vector): # material input is the desired material vector
"""
Set the material of all voxels in a model.
----
Example:
``material_vector = np.zeros(len(materials) + 1)``
``material_vector[0] = 1 # Set a to 1``
``material_vector[3] = 0.3 # Set material 3 to 30%``
``material_vector[4] = 0.7 # Set material 4 to 70%``
``model2 = model1.setMaterialVector(material_vector)``
----
Args:
material_vector: Material mixture vector, format: (a, m0, m1, ... mn)
Returns:
VoxelModel
"""
new_voxels = self.getOccupied().voxels # Converts input model to a mask, no effect if input is already a mask
a = np.zeros(len(material_vector), dtype=np.float32)
b = material_vector
materials = np.vstack((a, b))
return VoxelModel(new_voxels, materials, self.coords, self.resolution)
# Boolean operations ##############################
# Material from base model takes priority
def union(self, model_to_add):
"""
Find the geometric union of two models.
The materials from self will take priority in overlapping areas
of the resulting model. This operation can also be applied using
the OR operator (|)
----
Examples:
``model3 = model1.union(model2)``
``model3 = model1 | model2``
----
Args:
model_to_add: VoxelModel to union with self
Returns:
VoxelModel
"""
checkResolution(self, model_to_add)
materials = np.vstack((self.materials, model_to_add.materials[1:]))
a, b, new_coords = alignDims(self, model_to_add)
i_offset = len(self.materials) - 1
b = b + i_offset
b[b == i_offset] = 0
# Paper uses a symmetric difference operation combined with the left/right intersection
# A condensed version of this operation is used here for code simplicity
mask = np.array(a == 0, dtype=np.bool_)
new_voxels = np.multiply(b, mask)
new_voxels = new_voxels + a # material from left model takes priority
return VoxelModel(new_voxels, materials, new_coords, self.resolution)
def __or__(self, other):
"""
Find the geometric union of two models.
Overload OR operator (|) for VoxelModel objects with union().
Args:
other: VoxelModel to union with self
Returns:
VoxelModel
"""
return self.union(other)
def difference(self, model_to_sub):
"""
Find the geometric difference of two models.
----
Example:
``model3 = model1.difference(model2)``
----
Args:
model_to_sub: VoxelModel to subtract from self
Returns:
VoxelModel
"""
checkResolution(self, model_to_sub)
a, b, new_coords = alignDims(self, model_to_sub)
mask = np.array(b == 0, dtype=np.bool_)
new_voxels = np.multiply(a, mask)
return VoxelModel(new_voxels, self.materials, new_coords, self.resolution)
def intersection(self, model_2):
"""
Find the geometric intersection of two models.
The materials from self will be used in the resulting model.
This operation can also be applied using the AND operator (&)
----
Examples:
``model3 = model1.intersection(model2)``
``model3 = model1 & model2``
----
Args:
model_2: VoxelModel to intersect with self
Returns:
VoxelModel
"""
checkResolution(self, model_2)
a, b, new_coords = alignDims(self, model_2)
mask = np.logical_and(np.array(a != 0, dtype=np.bool_), np.array(b != 0, dtype=np.bool_))
# Paper provides for left/right intersection
# For code simplicity, only a left intersection is provided here
new_voxels = np.multiply(a, mask) # material from left model takes priority
materials = self.materials
return VoxelModel(new_voxels, materials, new_coords, self.resolution)
def __and__(self, other):
"""
Find the geometric intersection of two models.
Overload AND operator (&) for VoxelModel objects with intersection().
Args:
other: VoxelModel to intersect with self
Returns:
VoxelModel
"""
return self.intersection(other)
def xor(self, model_2):
"""
Find the geometric exclusive or of two models.
This operation can also be applied using the XOR operator (^)
----
Examples:
``model3 = model1.xor(model2)``
``model3 = model1 ^ model2``
----
Args:
model_2: VoxelModel to xor with self
Returns:
VoxelModel
"""
checkResolution(self, model_2)
materials = np.vstack((self.materials, model_2.materials[1:]))
a, b, new_coords = alignDims(self, model_2)
i_offset = len(self.materials) - 1
b = b + i_offset
b[b == i_offset] = 0
mask1 = np.array(b == 0, dtype=np.bool_)
mask2 = np.array(a == 0, dtype=np.bool_)
new_voxels = np.multiply(a, mask1) + np.multiply(b, mask2)
return VoxelModel(new_voxels, materials, new_coords, self.resolution)
def __xor__(self, other):
"""
Find the geometric exclusive or of two models.
Overload XOR operator (^) for VoxelModel objects with xor().
Args:
other: VoxelModel to xor with self
Returns:
VoxelModel
"""
return self.xor(other)
# Material is computed
def add(self, model_to_add):
"""
Find the material-wise addition of two models.
The materials of the result are calculated by adding the material vectors for each voxel together.
Example -- Adding a voxel containing material 1 and a voxel containing material 3:
>> Voxel A = [1, 0, 1, 0, 0]\n
>> Voxel B = [1, 0, 0, 0, 1]\n
>> A + B = [1, 0, 1, 0, 1]\n
>> Scale Result (see Cleanup Operations) → [1, 0, 0.5, 0, 0.5]\n
This operation can also be applied using the addition operator (+).
----
Examples:
``model3 = model1.add(model2)``
``model3 = model1 + model2``
----
Args:
model_to_add: VoxelModel to add to self
Returns:
VoxelModel
"""
checkResolution(self, model_to_add)
a, b, new_coords = alignDims(self, model_to_add)
x_len = a.shape[0]
y_len = a.shape[1]
z_len = a.shape[2]
new_voxels = np.zeros_like(a, dtype=np.uint16)
new_materials = np.zeros((1, len(self.materials[0])), dtype=np.float32)
for x in range(x_len):
for y in range(y_len):
for z in range(z_len):
i1 = int(a[x, y, z])
i2 = int(b[x, y, z])
m1 = self.materials[i1]
m2 = model_to_add.materials[i2]
m = m1 + m2
m[0] = np.logical_or(m1[0], m2[0])
i = np.where(np.equal(new_materials, m).all(1))[0]
if len(i) > 0:
new_voxels[x, y, z] = i[0]
else:
new_materials = np.vstack((new_materials, m))
new_voxels[x, y, z] = len(new_materials) - 1
return VoxelModel(new_voxels, new_materials, new_coords, self.resolution)
def __add__(self, other):
"""
Find the material-wise addition of two models.
Overload addition operator (+) for VoxelModel objects with add().
Args:
other: VoxelModel to add to self
Returns:
VoxelModel
"""
return self.add(other)
# Material is computed
def subtract(self, model_to_sub):
"""
Find the material-wise difference of two models.
The materials of the result are calculated for each voxel by subtracting the
second material vector from the first.
Example -- Subtracting a voxel containing material 3 from the result of the
addition example:
>> Voxel A = [1, 0, 0.5, 0, 0.5]\n
>> Voxel B = [1, 0, 0, 0, 1]\n
>> A - B = [1, 0, 0.5, 0, -0.5]\n
>> Remove negatives (see Cleanup Operations) → [1, 0, 0.5, 0, 0]\n
This operation can also be applied using the subtraction operator (-).
----
Examples:
``model3 = model1.subtract(model2)``
``model3 = model1 - model2``
----
Args:
model_to_sub: VoxelModel to subtract from self
Returns:
VoxelModel
"""
checkResolution(self, model_to_sub)
a, b, new_coords = alignDims(self, model_to_sub)
x_len = a.shape[0]
y_len = a.shape[1]
z_len = a.shape[2]
new_voxels = np.zeros_like(a, dtype=np.uint16)
new_materials = np.zeros((1, len(self.materials[0])), dtype=np.float32)
for x in range(x_len):
for y in range(y_len):
for z in range(z_len):
i1 = int(a[x, y, z])
i2 = int(b[x, y, z])
m1 = self.materials[i1]
m2 = model_to_sub.materials[i2]
m = m1 - m2
m[0] = np.logical_or(m1[0], m2[0])
i = np.where(np.equal(new_materials, m).all(1))[0]
if len(i) > 0:
new_voxels[x, y, z] = i[0]
else:
new_materials = np.vstack((new_materials, m))
new_voxels[x, y, z] = len(new_materials) - 1
return VoxelModel(new_voxels, new_materials, new_coords, self.resolution)
def __sub__(self, other):
"""
Find the material-wise difference of two models.
Overload subtraction operator (-) for VoxelModel objects with subtract().
Args:
other: VoxelModel to subtract from self
Returns:
VoxelModel
"""
return self.subtract(other)
def multiply(self, other):
"""
Find the material-wise multiplication of two models.
The materials of the result are calculated by multiplying the material vectors
for each voxel. This function also supports multiplication by a scalar.
This operation can also be applied using the multiplication operator (*).
----
Examples:
``model3 = model1.multiply(model2)``
``model3 = model1 * model2``
``model5 = model4 * 3``
----
Args:
other: VoxelModel to multiply with self
Returns:
VoxelModel
"""
if type(other) is VoxelModel:
checkResolution(self, other)
a, b, new_coords = alignDims(self, other)
x_len = a.shape[0]
y_len = a.shape[1]
z_len = a.shape[2]
new_voxels = np.zeros_like(a, dtype=np.uint16)
new_materials = np.zeros((1, len(self.materials[0])), dtype=np.float32)
for x in range(x_len):
for y in range(y_len):
for z in range(z_len):
i1 = int(a[x, y, z])
i2 = int(b[x, y, z])
m1 = self.materials[i1]
m2 = other.materials[i2]
m = np.multiply(m1, m2)
m[0] = np.logical_and(m1[0], m2[0])
i = np.where(np.equal(new_materials, m).all(1))[0]
if len(i) > 0:
new_voxels[x, y, z] = i[0]
else:
new_materials = np.vstack((new_materials, m))
new_voxels[x, y, z] = len(new_materials) - 1
return VoxelModel(new_voxels, new_materials, new_coords, self.resolution)
else:
new_model = VoxelModel.copy(self)
new_model.materials[1:, 1:] = np.multiply(new_model.materials[1:, 1:], other)
return new_model
def __mul__(self, other):
"""
Find the material-wise multiplication of two models.
Overload multiplication operator (*) for VoxelModel objects with multiply().
Args:
other: VoxelModel to multiply with self
Returns:
VoxelModel
"""
return self.multiply(other)
def divide(self, other):
"""
Find the material-wise division of two models.
The materials of the result are calculated for each voxel by dividing
the first material vector by the second. This function also supports
division by a scalar.
This operation can also be applied using the division operator (/).
----
Examples:
``model3 = model1.divide(model2)``
``model3 = model1 / model2``
``model5 = model4 / 3``
----
Args:
other: VoxelModel to divide self by
Returns:
VoxelModel
"""
if type(other) is VoxelModel:
checkResolution(self, other)
a, b, new_coords = alignDims(self, other)
x_len = a.shape[0]
y_len = a.shape[1]
z_len = a.shape[2]
new_voxels = np.zeros_like(a, dtype=np.uint16)
new_materials = np.zeros((1, len(self.materials[0])), dtype=np.float32)
for x in range(x_len):
for y in range(y_len):
for z in range(z_len):
i1 = int(a[x, y, z])
i2 = int(b[x, y, z])
m1 = self.materials[i1]
m2 = other.materials[i2]
m2[m2 == 0] = 1
m = np.divide(m1, m2)
m[0] = m1[0]
i = np.where(np.equal(new_materials, m).all(1))[0]
if len(i) > 0:
new_voxels[x, y, z] = i[0]
else:
new_materials = np.vstack((new_materials, m))
new_voxels[x, y, z] = len(new_materials) - 1
return VoxelModel(new_voxels, new_materials, new_coords, self.resolution)
else:
if other == 0:
return self
new_model = VoxelModel.copy(self)
new_model.materials[1:, 1:] = np.divide(new_model.materials[1:, 1:], other)
return new_model
def __truediv__(self, other):
"""
Find the material-wise division of two models.
Overload division operator (/) for VoxelModel objects with divide().
Args:
other: VoxelModel to divide self by
Returns:
VoxelModel
"""
return self.divide(other)
# Morphology Operations ##############################
def dilate(self, radius: int = 1, plane: Axes = Axes.XYZ, struct_type: Struct = Struct.STANDARD, connectivity: int = 3): # TODO: Preserve overlapping materials?
"""
Dilate a model along the specified axes.
----
Examples:
``model2 = model1.dilate(3)``
``model4 = model3.dilate(1, Axes.XY, Struct.SPHERE, 2)``
----
Args:
radius: Dilation radius in voxels
plane: Dilation directions, set using Axes class
struct_type: Shape of structuring element, set using Struct class
connectivity: Connectivity of structuring element (1-3)
Returns:
VoxelModel
"""
if radius == 0:
return VoxelModel.copy(self)
x_len = self.voxels.shape[0] + (radius * 2)
y_len = self.voxels.shape[1] + (radius * 2)
z_len = self.voxels.shape[2] + (radius * 2)
new_voxels = np.zeros((x_len, y_len, z_len), dtype=np.uint16)
new_voxels[radius:-radius, radius:-radius, radius:-radius] = self.voxels
if struct_type == Struct.SPHERE:
struct = structSphere(radius, plane)
new_voxels = ndimage.grey_dilation(new_voxels, footprint=struct)
else: # Struct.STANDARD
struct = structStandard(connectivity, plane)
for i in range(radius):
new_voxels = ndimage.grey_dilation(new_voxels, footprint=struct)
return VoxelModel(new_voxels, self.materials, (self.coords[0] - radius, self.coords[1] - radius, self.coords[2] - radius), self.resolution)
def __lshift__(self, radius):
"""
Dilate a model in all three axes.
Overload left shift operator (<<) for VoxelModel objects with dilate().
Uses:
- plane = Axes.XYZ
- struct_type = Struct.STANDARD
- connectivity = 3
Args:
radius: Dilation radius in voxels
Returns:
VoxelModel
"""
return self.dilate(radius)
def dilateBounded(self, radius: int = 1, plane: Axes = Axes.XYZ, structType: Struct = Struct.STANDARD, connectivity: int = 3):
"""
Dilate a model along the specified axes without increasing the size of its bounding box.
Args:
radius: Dilation radius in voxels
plane: Dilation directions, set using Axes class
structType: Shape of structuring element, set using Struct class
connectivity: Connectivity of structuring element (1-3)
Returns:
VoxelModel
"""
if radius == 0:
return VoxelModel.copy(self)
new_voxels = np.copy(self.fitWorkspace().voxels)
if structType == Struct.SPHERE:
struct = structSphere(radius, plane)
new_voxels = ndimage.grey_dilation(new_voxels, footprint=struct)
else: # Struct.STANDARD
struct = structStandard(connectivity, plane)
for i in range(radius):
new_voxels = ndimage.grey_dilation(new_voxels, footprint=struct)
return VoxelModel(new_voxels, self.materials, self.coords, self.resolution)
def erode(self, radius: int = 1, plane: Axes = Axes.XYZ, struct_type: Struct = Struct.STANDARD, connectivity: int = 3):
"""
Erode a model along the specified axes.
----
Examples:
``model2 = model1.erode(5, connectivity=2)``
``model4 = model3.erode(2, Axes.X, Struct.SPHERE, 1)``
----
Args:
radius: Erosion radius in voxels
plane: Erosion directions, set using Axes class
struct_type: Shape of structuring element, set using Struct class
connectivity: Connectivity of structuring element (1-3)
Returns:
VoxelModel
"""
if radius == 0:
return VoxelModel.copy(self)
new_voxels = np.copy(self.voxels)
mask = np.array(new_voxels != 0, dtype=np.bool_)
if struct_type == Struct.SPHERE:
struct = structSphere(radius, plane)
mask = ndimage.binary_erosion(mask, structure=struct)
else: # Struct.STANDARD
struct = structStandard(connectivity, plane)
mask = ndimage.binary_erosion(mask, structure=struct, iterations=radius)
new_voxels = np.multiply(new_voxels, mask)
return VoxelModel(new_voxels, self.materials, self.coords, self.resolution)
def __rshift__(self, radius):
"""
Erode a model in all three axes.
Overload right shift operator (>>) for VoxelModel objects with erode().
Uses:
- plane = Axes.XYZ
- struct_type = Struct.STANDARD
- connectivity = 3
Args:
radius: Dilation radius in voxels
Returns:
VoxelModel
"""
return self.erode(radius)
def closing(self, radius: int = 1, plane: Axes = Axes.XYZ, structType: Struct = Struct.STANDARD, connectivity: int = 3):
"""
Apply a closing algorithm along the specified axes.
This algorithm consists of dilation followed by erosion and will remove small holes.
Depending on the structuring element used, this will apply a chamfer or fillet effect
to inside corners.
Args:
radius: Radius for dilation/erosion in voxels
plane: Dilation/erosion directions, set using Axes class
structType: Shape of structuring element, set using Struct class
connectivity: connectivity of structuring element (1-3)
Returns:
VoxelModel
"""
if radius == 0:
return VoxelModel.copy(self)
else:
return self.dilate(radius, plane, structType, connectivity).erode(radius, plane, structType, connectivity).fitWorkspace()
def opening(self, radius: int = 1, plane: Axes = Axes.XYZ, structType: Struct = Struct.STANDARD, connectivity: int = 3):
"""
Apply an opening algorithm along the specified axes.
This algorithm consists of erosion followed by dilation and will remove small features.
Depending on the structuring element used, this will apply a chamfer or fillet effect
to outside corners.
Args:
radius: Radius for dilation/erosion in voxels
plane: Dilation/erosion directions, set using Axes class
structType: Shape of structuring element, set using Struct class
connectivity: Connectivity of structuring element (1-3)
Returns:
VoxelModel
"""
if radius == 0:
return VoxelModel.copy(self)
new_voxels = np.copy(self.voxels)
mask = np.array(new_voxels != 0, dtype=np.bool_)
if structType == Struct.SPHERE:
struct = structSphere(radius, plane)
mask = ndimage.binary_opening(mask, structure=struct)
else: # Struct.STANDARD
struct = structStandard(connectivity, plane)
mask = ndimage.binary_opening(mask, structure=struct, iterations=radius)
new_voxels = np.multiply(new_voxels, mask)
return VoxelModel(new_voxels, self.materials, self.coords, self.resolution)
# Material Interface Modification ##############################
def blur(self, radius: float = 1):
"""
Apply a Gaussian blur with the defined radius to the entire model.
The blur radius corresponds to two times the standard deviation
(2 * sigma) of the Gaussian distribution. The blurred effect is limited
to voxels that were occupied by material in the input model.
----
Example:
``model2 = model1.blur(2)``
___
Args:
radius: Blur radius in voxels
Returns:
VoxelModel
"""
if radius == 0:
return VoxelModel.copy(self)
full_model = toFullMaterials(self.voxels, self.materials, len(self.materials[0]))
for m in tqdm(range(len(self.materials[0])-1), desc='Blur - applying gaussian filter'):
full_model[:, :, :, m+1] = ndimage.gaussian_filter(full_model[:, :, :, m+1], sigma=radius/2)
mask = full_model[:, :, :, 0]
mask = np.repeat(mask[..., None], len(self.materials[0]), axis=3)
full_model = np.multiply(full_model, mask)
return toIndexedMaterials(full_model, self, self.resolution)
def blurRegion(self, radius: float, region):
"""
Apply a Gaussian blur with the defined radius to voxels that intersect with the region model.
The blur radius corresponds to two times the standard deviation
(2 * sigma) of the Gaussian distribution. The blurred effect is limited
to voxels that were occupied by material in the intersection result
and the material of the region model is ignored.
----
Example:
``model2 = model1.blurRegion(3, regionModel)``
___
Args:
radius: Blur radius in voxels
region: VoxelModel defining the target blur region
Returns:
VoxelModel
"""
new_model = self.blur(radius)
new_model = new_model.intersection(region)
new_model = new_model.union(self)
return new_model
def dither(self, use_full=True, x_error=0.0, y_error=0.0, z_error=0.0, error_spread_threshold=0.8, blur=False, radius=1):
"""
Apply material-wise dithering to a model.
Applying dithering will modify the model so that each voxel contains material in only a single material channel.
Regions of the model which contained mixtures of materials will be converted to distributions of adjacent single
material voxels.
Args:
use_full: Enabling will use a Stucki error diffusion filter. Disabling will use the provided values for x, y, and z error
x_error: Percentage of error to spread in X
y_error: Percentage of error to spread in Y
z_error: Percentage of error to spread in Z
error_spread_threshold: If a voxel contains a material channel that accounts for more than this percentage of the voxel, no additional error will be spread to it
blur: Enable/disable applying a blur operation before the dither operation
radius: Radius value for the optional blur operation
Returns:
VoxelModel
"""
if blur and (radius > 0):
new_model = self.blur(radius)
new_model = new_model.scaleValues()
else:
new_model = self.scaleValues()
new_model.voxels = new_model.voxels.astype(dtype=np.uint16)
full_model = toFullMaterials(new_model.voxels, new_model.materials, len(material_properties) + 1)
full_model = ditherOptimized(full_model, use_full, x_error, y_error, z_error, error_spread_threshold)
return toIndexedMaterials(full_model, self, self.resolution)
# Cleanup ##############################
def removeNegatives(self):
"""
Remove negative material values from a model (these have no physical meaning).
----
Example:
``model2 = model1.removeNegatives()``
___
Returns:
VoxelModel
"""
new_model = VoxelModel.copy(self)
new_model.materials[new_model.materials < 1e-10] = 0
material_sums = np.sum(new_model.materials[:,1:], 1) # This and following update the a values
material_sums[material_sums > 0] = 1
new_model.materials[:, 0] = material_sums
return new_model
def scaleValues(self):
"""
Scale nonzero material values to make all voxels contain 100% material while
maintaining the ratio between materials.
----
Example:
``model2 = model1.scaleValues()``
___
Returns:
VoxelModel
"""
new_model = self.removeNegatives()
material_sums = np.sum(new_model.materials[:, 1:], 1)
material_sums[material_sums == 0] = 1
material_sums = np.repeat(material_sums[..., None], len(self.materials[0])-1, axis=1)
new_model.materials[:,1:] = np.divide(new_model.materials[:,1:], material_sums)
return new_model
def scaleNull(self):
"""
Scale null material values to make all voxels contain 100% material.
Voxels that contained less than 100% material will contain the same material percentages as
before, but will have varying density. Voxels that contained greater than 100% material
will be scaled using scaleValues().
----
Example:
``model2 = model1.scaleNull()``
___
Returns:
VoxelModel
"""
new_model = self.removeNegatives()
material_sums = np.sum(new_model.materials[:, 1:], 1)
material_sums = np.ones(np.shape(material_sums)) - material_sums
material_sums[material_sums < 0] = 0
new_model.materials[:,1] = np.multiply(material_sums, new_model.materials[:,0])
new_model = new_model.scaleValues()
return new_model
def round(self, toNearest: float = 0.1):
"""
Round material percentages to nearest multiple of an input value.
Args:
toNearest: Value to round to
Returns:
VoxelModel
"""
new_materials = np.copy(self.materials)
new_model = VoxelModel.copy(self)
mult = new_materials / toNearest
floorDiff = np.round(abs(mult - np.floor(mult)), 10)
ceilDiff = np.round(abs(mult - np.ceil(mult)), 10)
new_materials[floorDiff < ceilDiff] = toNearest * np.floor(mult[floorDiff < ceilDiff])
new_materials[floorDiff >= ceilDiff] = toNearest * np.ceil(mult[floorDiff >= ceilDiff])
new_model.materials = new_materials
return new_model
def clearNull(self):
"""
Set all null material percentages to zero.
Returns:
VoxelModel
"""
new_model = VoxelModel.copy(self)
new_model.materials[1:, 1] = np.zeros(np.shape(new_model.materials[1:,1]))
return new_model
def setDensity(self, density: float = 1.0):
"""
Set the density of all voxels.
Args:
density: Target density value
Returns:
VoxelModel
"""
new_model = self.clearNull()
new_model = new_model.scaleValues()
null_material_values = np.multiply(np.ones(np.shape(new_model.materials[1:,1])), 1-density)
new_model.materials[1:, 1] = null_material_values
new_model.materials[1:, 2:] = np.multiply(new_model.materials[1:, 2:], density)
return new_model
# Transformations ##############################
def translate(self, vector: Tuple[int, int, int]):
"""
Translate a model by the specified vector.
Args:
vector: Translation vector in voxels
Returns:
VoxelModel
"""
new_model = VoxelModel.copy(self)
new_model.coords = (self.coords[0]+vector[0], self.coords[1]+vector[1], self.coords[2]+vector[2])
return new_model
def translateMM(self, vector: Tuple[float, float, float]):
"""
Translate a model by the specified vector.
Args:
vector: Translation vector in mm
Returns:
VoxelModel
"""
xV = int(round(vector[0] * self.resolution))
yV = int(round(vector[1] * self.resolution))
zV = int(round(vector[2] * self.resolution))
new_model = self.translate((xV, yV, zV))
return new_model
def rotate(self, angle: float, axis: Axes = Axes.Z):
"""
Rotate a model about its center.
Floating point errors may slightly affect the angle of the resulting model.
To rotate a model in precise 90 degree increments, use rotate90().
Args:
angle: Angle of rotation in degrees
axis: Axis of rotation, set using Axes class
Returns:
VoxelModel
"""
if axis == Axes.X:
plane = (1, 2)
sign = 1
elif axis == Axes.Y:
plane = (2, 0)
sign = -1 # For some reason, Y rotates the opposite direction than expected
else: # axis == Axes.Z
plane = (0, 1)
sign = 1
centerCoords = self.getCenter()
new_voxels = ndimage.rotate(self.voxels, sign*angle, plane, order=0)
new_model = VoxelModel(new_voxels, self.materials, self.coords, self.resolution)
new_model = new_model.setCenter(centerCoords)
return new_model
def rotate90(self, times: int = 1, axis: Axes = Axes.Z):
"""
Rotate a model about its center in increments of 90 degrees.
Args:
times: Number of 90 degree increments to rotate model
axis: Axis of rotation, set using Axes class
Returns:
VoxelModel
"""
if axis == Axes.X or axis == 0:
plane = (1, 2)
elif axis == Axes.Y or axis == 1:
plane = (0, 2)
else: # axis == Axes.Z or axis = 2
plane = (0, 1)
centerCoords = self.getCenter()
new_voxels = np.rot90(self.voxels, times, axes=plane)
new_model = VoxelModel(new_voxels, self.materials, self.coords, self.resolution)
new_model = new_model.setCenter(centerCoords)
return new_model
def mirror(self, axes: Axes = Axes.X):
"""
Mirror a model along the given axes.
This operation will mirror ALONG the given axes. For example:
- Axes.X performs a mirror about the YZ plane
- Axes.XY performs a mirror about the YZ plane and the XZ plane
- Axes.XYZ performs a mirror along all three axes
Args:
axes: Axes for mirror operation, set using Axes class
Returns:
VoxelModel
"""
flip_axis = []
for i in range(len(axes.value)):
if axes.value[i]:
flip_axis.append(i)
flip_axis = tuple(flip_axis)
centerCoords = self.getCenter()
new_voxels = np.flip(self.voxels, flip_axis)
new_model = VoxelModel(new_voxels, self.materials, self.coords, self.resolution)
new_model = new_model.setCenter(centerCoords)
return new_model
def scale(self, factor: float, adjustResolution: bool = True):
"""
Scale a model.
If adjustResolution is enabled, the resolution attribute of the model will
also be multiplied by the scaling factor.
Enable adjustResolution if using this operation to change the resolution of a model.
Disable adjustResolution if using this operation to change the size of a model.
Args:
factor: Scale factor
adjustResolution: Enable/disable automatic resolution adjustment
Returns:
VoxelModel
"""
model = self.fitWorkspace()
x_len = int(model.voxels.shape[0] * factor)
y_len = int(model.voxels.shape[1] * factor)
z_len = int(model.voxels.shape[2] * factor)
new_voxels = np.zeros((x_len, y_len, z_len))
for x in tqdm(range(x_len), desc='Scaling'):
for y in range(y_len):
for z in range(z_len):
x_source = int(((x+1) / x_len) * (model.voxels.shape[0]-1))
y_source = int(((y+1) / y_len) * (model.voxels.shape[1]-1))
z_source = int(((z+1) / z_len) * (model.voxels.shape[2]-1))
new_voxels[x,y,z] = model.voxels[x_source, y_source, z_source]
model.voxels = new_voxels.astype(dtype=np.uint16)
model = model.setCoords(model.coords)
if adjustResolution:
model.resolution = model.resolution * factor
return model
def scaleToSize(self, size: Tuple[int, int, int]):
"""
Scale a model to fit the given dimensions.
Args:
size: Target dimensions in voxels
Returns:
VoxelModel
"""
model = self.fitWorkspace()
new_voxels = np.zeros(size)
for x in tqdm(range(size[0]), desc='Scaling'):
for y in range(size[1]):
for z in range(size[2]):
x_source = int(((x+1) / size[0]) * (model.voxels.shape[0]-1))
y_source = int(((y+1) / size[1]) * (model.voxels.shape[1]-1))
z_source = int(((z+1) / size[2]) * (model.voxels.shape[2]-1))
new_voxels[x,y,z] = model.voxels[x_source, y_source, z_source]
model.voxels = new_voxels.astype(dtype=np.uint16)
new_model = model.setCoords(model.coords)
return new_model
def getCenter(self):
"""
Find the coordinates of the center of a model.
Returns:
Center coordinates in voxels
"""
model = self.fitWorkspace()
x_center = (model.voxels.shape[0] / 2) + model.coords[0]
y_center = (model.voxels.shape[1] / 2) + model.coords[1]
z_center = (model.voxels.shape[2] / 2) + model.coords[2]
centerCoords = (x_center, y_center, z_center)
return centerCoords
def setCenter(self, coords: Tuple[float, float, float]):
"""
Set the center of a model to the specified coordinates.
Args:
coords: Target coordinates in voxels
Returns:
VoxelModel
"""
new_model = self.fitWorkspace()
x_new = int(round(coords[0] - (new_model.voxels.shape[0] / 2)))
y_new = int(round(coords[1] - (new_model.voxels.shape[1] / 2)))
z_new = int(round(coords[2] - (new_model.voxels.shape[2] / 2)))
new_model.coords = (x_new, y_new, z_new)
return new_model
def getCoords(self):
"""
Get the origin coordinates of a model.
Returns:
Origin coordinates in voxels
"""
model = self.fitWorkspace()
return model.coords
def setCoords(self, coords: Tuple[int, int, int]):
"""
Set the origin of a model to the specified coordinates.
Args:
coords: Target coordinates in voxels
Returns:
VoxelModel
"""
new_model = self.fitWorkspace()
new_model.coords = coords
return new_model
def getMaxCoords(self):
"""
Get the maximum coordinate location in a model.
This point is equal to origin coordinates + model dimensions.
Returns:
Maximum coordinates in voxels
"""
model = self.fitWorkspace()
x = model.coords[0] + model.voxels.shape[0]
y = model.coords[1] + model.voxels.shape[1]
z = model.coords[2] + model.voxels.shape[2]
return x, y, z
def getDim(self):
"""
Get the dimensions of model.
Returns:
Model dimensions in voxels
"""
model = self.fitWorkspace()
x = model.voxels.shape[0]
y = model.voxels.shape[1]
z = model.voxels.shape[2]
return x, y, z
def isOccupied(self, coords: Tuple[int, int, int]):
"""
Determine if a specific voxel is occupied.
Returns:
True/False
"""
x = coords[0] - self.coords[0]
y = coords[1] - self.coords[1]
z = coords[2] - self.coords[2]
if x < 0 or x >= self.voxels.shape[0]:
return False
if y < 0 or y >= self.voxels.shape[1]:
return False
if z < 0 or z >= self.voxels.shape[2]:
return False
v = self.voxels[x, y, z]
if v == 0:
return False
else:
return True
# Model Info ##############################
def getVoxelDim(self):
"""
Get the side dimension of a voxel in mm.
Returns:
Float
"""
res = self.resolution
return (1.0/res) * 0.001
def getVolume(self, component: int = 0, material: int = 0):
"""
Get the volume of a model or model component.
Args:
component: Component label to measure, set to 0 for all components
material: Material index to measure, set to 0 for all materials
Returns:
Volume in voxels, volume in mm^3
"""
new_model = VoxelModel.copy(self)
if component > 0:
new_model = new_model.isolateComponent(component)
if material > 0:
new_model = new_model.isolateMaterial(material)
volumeVoxels = np.count_nonzero(new_model.voxels)
volumeMM3 = volumeVoxels * ((1/self.resolution)**3)
return volumeVoxels, volumeMM3
def getMaterialProperties(self, material):
"""
Get the average material properties of a row in a model's material array.
Args:
material: Material index
Returns:
Dictionary of material properties
"""
avg_properties = {}
for key in material_properties[0]:
if key == 'name' or key == 'process':
string = ''
for i in range(len(self.materials[0])-1):
if self.materials[material][i + 1] > 0:
current_material_data = getMaterialData(i)
string = string + current_material_data[key] + ' '
avg_properties.update({key: string})
elif key == 'MM' or key == 'MMD' or key == 'FM' or key == 'HG' or key == 'HGM':
material_id = self.materials[material][1:].argmax()
current_material_data = getMaterialData(material_id)
var = current_material_data[key]
avg_properties.update({key: var})
else:
var = 0
for i in range(len(self.materials[0])-1):
current_material_data = getMaterialData(i)
var = var + self.materials[material][i + 1] * current_material_data[key]
avg_properties.update({key: var})
return avg_properties
def getSSData(self, material):
"""
Get the stress-strain data for a row in a model's material array.
This is currently returned based on the material present in the highest percentage.
TODO: Make this average multiple stress-strain curves
Args:
material: Material index
Returns:
Dictionary of stress-strain data
"""
material_id = self.materials[material][1:].argmax()
current_material_data = getMaterialData(material_id)
try:
ss_data_index = current_material_data['MMD']
current_ss_data = next((item for item in ss_data if item['id'] == ss_data_index), None)
if current_ss_data is None:
raise KeyError
except KeyError:
print('Stress-strain data not available for ' + current_material_data['name'])
current_ss_data = None
return current_ss_data
def getHGModel(self, material):
"""
Get the hydrogel model parameters for a row in a model's material array.
This is currently returned based on the material present in the highest percentage.
TODO: Make this average multiple model parameters
Args:
material: Material index
Returns:
Dictionary of hydrogel model parameters
"""
material_id = self.materials[material][1:].argmax()
current_material_data = getMaterialData(material_id)
try:
hg_model_index = current_material_data['HGM']
current_hg_model = next((item for item in hg_models if item['id'] == hg_model_index), None)
if current_hg_model is None:
raise KeyError
except KeyError:
print('Hydrogel model data not available for ' + current_material_data['name'])
current_hg_model = None
return current_hg_model
def getVoxelProperties(self, coords: Tuple[int, int, int]):
"""
Get the average material properties of a specific voxel.
Args:
coords: Voxel coordinates
Returns:
Dictionary of material properties
"""
return self.getMaterialProperties(self.voxels[coords[0], coords[1], coords[2]])
# Manufacturing Features ##############################
def projection(self, direction: Dir, material: int = 1):
"""
Generate a model representing all voxels within the workspace that contain
material or that lie in the specified direction with respect to a voxel
that contains material.
---
Example:
``modelResult = model1.projection(Dir.DOWN)``
---
Args:
direction: Projection direction, set using Dir class
material: Material index corresponding to materials.py
Returns:
VoxelModel
"""
new_voxels = np.zeros_like(self.voxels)
x_len = self.voxels.shape[0]
y_len = self.voxels.shape[1]
z_len = self.voxels.shape[2]
if direction == Dir.BOTH:
# Loop through model data
for x in range(x_len):
for y in range(y_len):
if np.sum(self.voxels[x, y, :]) > 0:
new_voxels[x, y, :].fill(1)
elif direction == Dir.DOWN:
# Loop through model data
for x in range(x_len):
for y in range(y_len):
for z in range(z_len):
if np.sum(self.voxels[x, y, z:]) > 0:
new_voxels[x, y, z] = 1
elif np.sum(self.voxels[x, y, z:]) == 0:
break
elif direction == Dir.UP:
# Loop through model data
for x in range(x_len):
for y in range(y_len):
for z in range(z_len):
if np.sum(self.voxels[x, y, :z]) > 0:
new_voxels[x, y, z] = 1
return VoxelModel(new_voxels, generateMaterials(material), self.coords, self.resolution)
def keepout(self, method: Process, material: int = 1):
"""
Generate a model representing the keep-out region of a model.
The keep-out region for a given process and part represents material which
the process may not modify while creating the part. This feature primarily
applies to subtractive processes. It includes material that will be present
in the final part and regions of the workspace that cannot be accessed
without affecting this material. In general, additive processes will have
no keep-out region because they deposit material from the bottom up.
----
Example:
``modelResult = model1.keepout(Process.MILL)``
----
Args:
method: Target manufacturing method, set using Process class
material: Material index corresponding to materials.py
Returns:
VoxelModel
"""
if method == Process.LASER:
new_model = self.projection(Dir.BOTH, material)
elif method == Process.MILL:
new_model = self.projection(Dir.DOWN, material)
elif method == Process.INSERT:
new_model = self.projection(Dir.UP, material)
else:
new_model = self
return new_model
def clearance(self, method: Process, material: int = 1):
"""
Generate a model representing the clearance region of a model.
The clearance region for a given process and part represents regions that
will be affected by the process acting on the part. Clearance can be
used to identify regions of a model that conflict with the manufacturing
of another model.
----
Example:
``modelResult = model1.clearance(Process.PRINT)``
----
Args:
method: Target manufacturing method, set using Process class
material: Material index corresponding to materials.py
Returns:
VoxelModel
"""
if method == Process.LASER:
new_model = self.projection(Dir.BOTH, material).difference(self)
elif method == Process.MILL:
new_model = self.projection(Dir.BOTH, material).difference(self.projection(Dir.DOWN, material))
elif (method == Process.INSERT) or (method == Process.PRINT):
new_model = self.projection(Dir.UP, material)
else:
new_model = self
return new_model
def support(self, method: Process, r1: int = 1, r2: int = 1, plane: Axes = Axes.XY, material: int = 1):
"""
Generate a model representing where support material may be added to an
object as characterized by the process that is used to remove the supports.
----
Example:
``modelResult = model1.support(Process.LASER)``
----
Args:
method: Target support removal method, set using Process class
r1: Parameter used to determine areas where support is ineffective based on proximity to empty regions that are inaccessible to the removal process
r2: Desired thickness of the support material
plane: Directions in which to add support material, set using Axes class
material: Material index corresponding to materials.py
Returns:
VoxelModel
"""
model_A = self.keepout(method, material)
model_A = model_A.dilate(r2, plane).difference(model_A)
model_A = model_A.difference(self.keepout(method, material).difference(self).dilate(r1, plane)) # Valid support regions
return model_A
def userSupport(self, support_model, method: Process, r1: int = 1, r2: int = 1, plane: Axes = Axes.XY, material: int = -1):
"""
Generate a model representing the intersection of the supportable region and a user support model.
----
Example:
``modelResult = model1.userSupport(model2, Process.LASER)``
----
Args:
support_model: User provided support model
method: Target support removal method, set using Process class
r1: Parameter used to determine areas where support is ineffective based on proximity to empty regions that are inaccessible to the removal process
r2: Desired thickness of the support material
plane: Directions in which to add support material, set using Axes class
material: Material index corresponding to materials.py, set to -1 to use support model material
Returns:
VoxelModel
"""
if material > -1:
model_A = self.support(method, r1, r2, plane)
model_A = support_model.intersection(model_A)
else:
model_A = self.support(method, r1, r2, plane, material)
model_A = model_A.intersection(support_model)
return model_A
def web(self, method: Process, r1: int = 1, r2: int = 1, layer: int = -1, material = 1):
"""
Generate a model representing the scrap material surrounding a model.
Web can be used in the creation of supports or layer alignment fixtures.
----
Example:
``modelResult = model1.web(Process.LASER, 1, 5)``
----
Args:
method: Target web removal method, set using Process class
r1: Distance from surface of part to inside of web in
r2: Width of web in voxels
layer: Voxel layer at which to generate web, set to -1 to generate for all layers
material: Material index corresponding to materials.py
Returns:
VoxelModel
"""
model_A = self.keepout(method, material)
if layer != -1:
model_A = model_A.isolateLayer(layer)
model_A = model_A.dilate(r1, Axes.XY)
model_A = model_A.dilate(r2, Axes.XY).getBoundingBox().difference(model_A)
return model_A
# File IO ##############################
# Add model to a K3D plot in Jupyter Notebook
def plot(self, plot=None, name: str = 'model', wireframe: bool = False, **kwargs):
"""
Add model to a K3D plot.
Additional display options:
- opacity: `float`. Opacity of voxels.
- outlines: `bool`. Whether mesh should display with outlines.
- outlines_color: `int`. Packed RGB color of the resulting outlines (0xff0000 is red, 0xff is blue)
- kwargs: `dict`. Dictionary arguments to configure transform and model_matrix.
More information available at: https://github.com/K3D-tools/K3D-jupyter
Args:
plot: Plot object to add model to
name: Model name
wireframe: Enable displaying model as a wireframe
kwargs: Additional display options (see above)
Returns:
K3D plot object
"""
model = self.fitWorkspace() | VoxelModel.empty((1, 1, 1), resolution=self.resolution)
model = model.removeDuplicateMaterials()
# Get colors
colors = []
for m in model.materials:
r = 0
g = 0
b = 0
for i in range(1, len(m)):
r = r + m[i] * material_properties[i - 1]['r']
g = g + m[i] * material_properties[i - 1]['g']
b = b + m[i] * material_properties[i - 1]['b']
r = 1 if r > 1 else r
g = 1 if g > 1 else g
b = 1 if b > 1 else b
colors.append(rgb_to_hex(r, g, b))
colors = np.array(colors, dtype=np.uint32)[1:]
# Plot
if plot is None:
plot = k3d.plot()
plot += k3d.voxels(np.swapaxes(model.voxels, 0, 2).astype(np.uint8), color_map=colors, name=name, wireframe=wireframe, **kwargs)
return plot
def saveVF(self, filename: str):
"""
Save model data to a .vf file
The native VoxelFuse file format stores the same information as the attributes of
a VoxelModel object. This includes geometry and material mixture data. Material
attributes remain stored in the materials.py file, so the same version of
this file must be used when saving and opening models. The .vf file type can be reopened
by a VoxelFuse script.
----
Example:
``modelResult.saveVF("test-file")``
----
Args:
filename: File name
Returns:
None
"""
f = open(filename+'.vf', 'w+')
print('Saving file: ' + f.name)
x_coord = self.coords[0]
y_coord = self.coords[1]
z_coord = self.coords[2]
writeOpen(f, 'coords')
f.write(str(x_coord) + ',' + str(y_coord) + ',' + str(z_coord) + ',\n')
writeClos(f, 'coords')
writeOpen(f, 'resolution')
f.write(str(self.resolution) + '\n')
writeClos(f, 'resolution')
writeOpen(f, 'materials')
for r in range(len(self.materials[:,0])): # tqdm(range(len(self.materials[:,0])), desc='Writing materials'):
for c in range(len(self.materials[0,:])):
f.write(str(self.materials[r,c]) + ',')
f.write('\n')
writeClos(f, 'materials')
x_len = self.voxels.shape[0]
y_len = self.voxels.shape[1]
z_len = self.voxels.shape[2]
writeOpen(f, 'size')
f.write(str(x_len) + ',' + str(y_len) + ',' + str(z_len) + ',\n')
writeClos(f, 'size')
writeOpen(f, 'voxels')
for x in range(x_len): # tqdm(range(x_len), desc='Writing voxels'):
for z in range(z_len):
for y in range(y_len):
f.write(str(int(self.voxels[x,y,z])) + ',')
f.write(';')
f.write('\n')
writeClos(f, 'voxels')
writeOpen(f, 'components')
f.write(str(self.numComponents) + '\n')
writeClos(f, 'components')
if self.numComponents > 0:
writeOpen(f, 'labels')
for x in range(x_len): # tqdm(range(x_len), desc='Writing components'):
for z in range(z_len):
for y in range(y_len):
f.write(str(int(self.components[x,y,z])) + ',')
f.write(';')
f.write('\n')
writeClos(f, 'labels')
f.close()
@classmethod
def openVF(cls, filename: str):
"""
Load model data from a .vf file
This method will create a new VoxelModel object using the data from the .vf file.
Material attributes are stored in the materials.py file, so the same version of
this file must be used when saving and opening models.
----
Example:
``model1 = vf.VoxelModel.openVF("test-file")``
----
Args:
filename: File name
Returns:
VoxelModel
"""
if filename[-3:] == '.vf':
f = open(filename, 'r')
else:
f = open(filename + '.vf', 'r')
print('Opening file: ' + f.name)
data = f.readlines()
loc = np.ones((7,2), dtype=np.uint16)
loc = np.multiply(loc, -1)
for i in range(len(data)): # tqdm(range(len(data)), desc='Finding tags'):
if data[i][:-1] == '<coords>':
loc[0,0] = i+1
if data[i][:-1] == '</coords>':
loc[0,1] = i
if data[i][:-1] == '<materials>':
loc[1,0] = i+1
if data[i][:-1] == '</materials>':
loc[1,1] = i
if data[i][:-1] == '<size>':
loc[2,0] = i+1
if data[i][:-1] == '</size>':
loc[2,1] = i
if data[i][:-1] == '<voxels>':
loc[3,0] = i+1
if data[i][:-1] == '</voxels>':
loc[3,1] = i
if data[i][:-1] == '<components>':
loc[4,0] = i+1
if data[i][:-1] == '</components>':
loc[4,1] = i
if data[i][:-1] == '<labels>':
loc[5,0] = i+1
if data[i][:-1] == '</labels>':
loc[5,1] = i
if data[i][:-1] == '<resolution>':
loc[6,0] = i+1
if data[i][:-1] == '</resolution>':
loc[6,1] = i
coords = np.array(data[loc[0,0]][:-2].split(","), dtype=np.int16)
if loc[6,0] > -1:
resolution = float(data[loc[6,0]][:-1])
else:
resolution = 1
materials = np.array(data[loc[1,0]][:-2].split(","), dtype=np.float32)
for i in range(loc[1,0]+1, loc[1,1]): # tqdm(range(loc[1,0]+1, loc[1,1]), desc='Reading materials'):
materials = np.vstack((materials, np.array(data[i][:-2].split(","), dtype=np.float32)))
size = tuple(np.array(data[loc[2,0]][:-2].split(","), dtype=np.uint16))
voxels = np.zeros(size, dtype=np.uint16)
for i in range(loc[3,0], loc[3,1]): # tqdm(range(loc[3,0], loc[3,1]), desc='Reading voxels'):
x = i - loc[3,0]
yz = data[i][:-2].split(";")
for z in range(len(yz)):
y = np.array(yz[z][:-1].split(","), dtype=np.uint16)
voxels[x, :, z] = y
numComponents = int(data[loc[4,0]][:-1])
components = np.zeros(size, dtype=np.uint8)
if numComponents > 0:
for i in range(loc[5,0], loc[5,1]): # tqdm(range(loc[5,0], loc[5,1]), desc='Reading components'):
x = i - loc[5, 0]
yz = data[i][:-2].split(";")
for z in range(len(yz)):
y = np.array(yz[z][:-1].split(","), dtype=np.uint8)
components[x, :, z] = y
new_model = cls(voxels, materials, coords=tuple(coords), resolution=resolution)
new_model.numComponents = numComponents
new_model.components = components
f.close()
return new_model
def saveVXC(self, filename: str, compression: bool = False):
"""
Save model data to a .vxc file
The VoxCad file format stores geometry and full material palette data. The material
palette includes the properties for each material and material mixtures are
converted into distinct palette entries.
This format supports compression for the voxel data. Enabling compression allows
for larger models, but it may introduce geometry errors that particularly affect
small models.
The .vxc file type can be opened using VoxCad simulation software. However, it
cannot currently be reopened by a VoxelFuse script.
----
Example:
``modelResult.saveVXC("test-file", compression=False)``
----
Args:
filename: File name
compression: Enable/disable voxel data compression
Returns:
None
"""
f = open(filename + '.vxc', 'w+')
print('Saving file: ' + f.name)
empty_model = VoxelModel.empty((1,1,1), resolution=self.resolution)
export_model = (VoxelModel.copy(self).fitWorkspace()) | empty_model # Fit workspace and union with an empty object at the origin to clear offsets if object is raised
export_model.coords = (0, 0, 0) # Set coords to zero to move object to origin if it is at negative coordinates
writeHeader(f, '1.0', 'ISO-8859-1')
export_model.writeVXCData(f, compression)
f.close()
def writeVXCData(self, f: TextIO, compression: bool = False):
"""
Write geometry and material data to a text file using the .vxc format.
The VXC/VXA format stores geometry as a 3D grid of single-digit decimal numbers. As such, it is limited to 9
distinct materials.
Args:
f: File to write to
compression: Enable/disable voxel data compression
Returns:
None
"""
if len(self.materials[:, 0]) > 10:
f.close()
os.remove(f.name)
raise ValueError('The VXC/VXA file format supports a maximum of 9 distinct materials')
writeOpen(f, 'VXC Version="' + str(0.94) + '"', 0)
# Lattice settings
writeOpen(f, 'Lattice', 1)
writeData(f, 'Lattice_Dim', (1 / self.resolution) * 0.001, 2)
writeData(f, 'X_Dim_Adj', 1, 2)
writeData(f, 'Y_Dim_Adj', 1, 2)
writeData(f, 'Z_Dim_Adj', 1, 2)
writeData(f, 'X_Line_Offset', 0, 2)
writeData(f, 'Y_Line_Offset', 0, 2)
writeData(f, 'X_Layer_Offset', 0, 2)
writeData(f, 'Y_Layer_Offset', 0, 2)
writeClos(f, 'Lattice', 1)
# Voxel settings
writeOpen(f, 'Voxel', 1)
writeData(f, 'Vox_Name', 'BOX', 2)
writeData(f, 'X_Squeeze', 1, 2)
writeData(f, 'Y_Squeeze', 1, 2)
writeData(f, 'Z_Squeeze', 1, 2)
writeClos(f, 'Voxel', 1)
# Materials
writeOpen(f, 'Palette', 1)
for row in range(1, len(self.materials[:, 0])): # tqdm(range(1, len(self.materials[:, 0])), desc='Writing materials'):
avgProps = self.getMaterialProperties(row)
writeOpen(f, 'Material ID="' + str(row) + '"', 2)
writeData(f, 'MatType', 0, 3)
writeData(f, 'Name', avgProps['name'][0:-1], 3)
writeOpen(f, 'Display', 3)
writeData(f, 'Red', avgProps['r'], 4)
writeData(f, 'Green', avgProps['g'], 4)
writeData(f, 'Blue', avgProps['b'], 4)
writeData(f, 'Alpha', 1, 4)
writeClos(f, 'Display', 3)
writeOpen(f, 'Mechanical', 3)
if int(avgProps['MM']) == 3:
current_ss_data = self.getSSData(row)
if current_ss_data is not None:
writeData(f, 'MatModel', 3, 4)
writeOpen(f, 'SSData', 4)
writeData(f, 'NumDataPts', len(current_ss_data['strain']), 5)
writeOpen(f, 'StrainData', 5)
for point in current_ss_data['strain']:
writeData(f, 'Strain', point, 6)
writeClos(f, 'StrainData', 5)
writeOpen(f, 'StressData', 5)
for point in current_ss_data['stress']:
writeData(f, 'Stress', point, 6)
writeClos(f, 'StressData', 5)
writeClos(f, 'SSData', 4)
else:
writeData(f, 'MatModel', 0, 4)
else:
writeData(f, 'MatModel', avgProps['MM'], 4)
writeData(f, 'Elastic_Mod', avgProps['E'], 4)
writeData(f, 'Plastic_Mod', avgProps['Z'], 4)
writeData(f, 'Yield_Stress', avgProps['eY'], 4)
writeData(f, 'FailModel', int(avgProps['FM']), 4)
writeData(f, 'Fail_Stress', avgProps['eF'], 4)
writeData(f, 'Fail_Strain', avgProps['SF'], 4)
writeData(f, 'Density', avgProps['p'] * 1e3, 4)
writeData(f, 'Poissons_Ratio', avgProps['v'], 4)
writeData(f, 'CTE', avgProps['CTE'], 4)
writeData(f, 'MaterialTempPhase', avgProps['TP'], 4)
writeData(f, 'uStatic', avgProps['uS'], 4)
writeData(f, 'uDynamic', avgProps['uD'], 4)
if int(avgProps['HG']) == 1:
current_hg_model = self.getHGModel(row)
if current_hg_model is not None:
writeData(f, 'HydrogelModel', 1, 4)
writeClos(f, 'Mechanical', 3)
writeOpen(f, 'Hydrogel', 3)
writeData(f, 'Name', current_hg_model['name'], 4)
writeData(f, 'VoxelDim', current_hg_model['test_voxel_dim'], 4)
writeData(f, 'IdealDisplacement', current_hg_model['ideal_displacement'], 4)
writeData(f, 'TestDisplacement', current_hg_model['test_displacement'], 4)
writeData(f, 'TimeStepCorrection', current_hg_model['test_time_step'], 4)
writeData(f, 'KpRising', current_hg_model['kp_rising'], 4)
writeData(f, 'KpFalling', current_hg_model['kp_falling'], 4)
writeData(f, 'MaxTemp', current_hg_model['ideal_max_temp'], 4)
writeData(f, 'MinTemp', current_hg_model['ideal_min_temp'], 4)
writeData(f, 'TestMax', current_hg_model['test_max_temp'], 4)
writeData(f, 'TestMin', current_hg_model['test_min_temp'], 4)
writeData(f, 'C0', current_hg_model['c0'], 4)
writeData(f, 'C1', current_hg_model['c1'], 4)
writeData(f, 'C2', current_hg_model['c2'], 4)
writeData(f, 'C3', current_hg_model['c3'], 4)
writeData(f, 'C4', current_hg_model['c4'], 4)
writeData(f, 'C5', current_hg_model['c5'], 4)
writeClos(f, 'Hydrogel', 3)
else:
writeData(f, 'HydrogelModel', 0, 4)
writeClos(f, 'Mechanical', 3)
else:
writeData(f, 'HydrogelModel', 0, 4)
writeClos(f, 'Mechanical', 3)
writeClos(f, 'Material', 2)
writeClos(f, 'Palette', 1)
# Structure
if compression:
writeOpen(f, 'Structure Compression="ZLIB"', 1)
else:
writeOpen(f, 'Structure Compression="ASCII_READABLE"', 1)
x_len = self.voxels.shape[0]
y_len = self.voxels.shape[1]
z_len = self.voxels.shape[2]
writeData(f, 'X_Voxels', x_len, 2)
writeData(f, 'Y_Voxels', y_len, 2)
writeData(f, 'Z_Voxels', z_len, 2)
writeOpen(f, 'Data', 2)
for z in range(z_len): # tqdm(range(z_len), desc='Writing voxels'):
layer = np.copy(self.voxels[:, :, z])
layer = layer.transpose()
layerData = layer.flatten()
layerData = layerData.astype('uint8')
if compression:
layerData = zlib.compress(layerData.tobytes())
layerData = base64.encodebytes(layerData)
layerDataStr = str(layerData)[2:-3]
else:
layerDataStr = ''
for vox in layerData:
layerDataStr = layerDataStr + str(vox)
writeData(f, 'Layer', '<![CDATA[' + layerDataStr + ']]>', 3)
writeClos(f, 'Data', 2)
writeClos(f, 'Structure', 1)
writeClos(f, 'VXC', 0)
class GpuSettings:
"""
Object to store GPU settings.
After initializing and configuring the GPU settings, use applySettings() to
apply them. Changes will only persist for the current Python session.
For persistent GPU settings, configure these environment variables:
``VF_CUDA_ENABLE = 1``
``VF_CUDA_DEVICE = <desired GPU ID>``
----
Example:
``gpu = GpuSettings()``
``print('Default CUDA settings:' + str(gpu.CUDA_enable) + ', ' + str(gpu.CUDA_device))``
``gpu.setCUDA(True, 1)``
``gpu.applySettings()``
----
"""
def __init__(self):
"""
Initialize a GpuSettings object.
"""
# Get CUDA settings from environment variables
try:
CUDA_enable = bool(os.environ.get('VF_CUDA_ENABLE'))
CUDA_device = int(os.environ.get('VF_CUDA_DEVICE'))
except TypeError:
print('CUDA environment variables not found')
CUDA_enable = False
CUDA_device = 0
self.CUDA_enable = CUDA_enable
self.CUDA_device = CUDA_device
def setCUDA(self, CUDA_enable: bool = True, CUDA_device: int = 0):
"""
Set overrides for CUDA settings.
Args:
CUDA_enable: Enable/disable CUDA acceleration
CUDA_device: Select CUDA device
Returns:
None
"""
self.CUDA_enable = CUDA_enable
self.CUDA_device = CUDA_device
def applySettings(self):
"""
Apply GPU settings as overrides.
These changes will only persist for the current python session.
Returns:
None
"""
os.environ['VF_CUDA_ENABLE'] = str(int(self.CUDA_enable))
os.environ['VF_CUDA_DEVICE'] = str(self.CUDA_device)
# Helper functions ##############################################################
def rgb_to_hex(r: float, g: float, b: float):
"""
Convert RGB values to a single hexadecimal value.
Args:
r: Red percentage (0-1)
g: Green percentage (0-1)
b: Blue percentage (0-1)
Returns:
Hexadecimal color as an integer
"""
r = round(r * 255)
g = round(g * 255)
b = round(b * 255)
hex_str = '0x{:02x}{:02x}{:02x}'.format(r, g, b)
return int(hex_str, base=16)
def getMaterialData(material_id):
"""
Get the material data for a specific material id.
Args:
material_id: Material id corresponding to materials.py
Returns:
Dictionary of material properties
"""
current_material_data = next((item for item in material_properties if item['id'] == material_id))
if current_material_data is None:
print('Material data not available for id ' + str(material_id) + ' -- using first nonzero material')
current_material_data = next((item for item in material_properties if item['id'] != 0))
return current_material_data
def makeMesh(filename: str, delete_files: bool = True, gmsh_on_path: bool = False):
"""
Import mesh data from file
Args:
filename: File name with extension
delete_files: Enable/disable deleting temporary files when finished
gmsh_on_path: Enable/disable using system gmsh rather than bundled gmsh
Returns:
Mesh data (points, tris, and tets)
"""
template = '''
Merge "{0}";
Surface Loop(1) = {{1}};
//+
Volume(1) = {{1}};
'''
geo_string = template.format(filename)
with open('output.geo', 'w') as f:
f.writelines(geo_string)
if gmsh_on_path:
command_string = 'gmsh '
else:
# Check OS type
if os.name.startswith('nt'):
# Windows
command_string = f'"{os.path.dirname(os.path.realpath(__file__))}\\utils\\gmsh.exe"'
else:
# Linux
command_string = f'"{os.path.dirname(os.path.realpath(__file__))}/utils/gmsh"'
command_string = command_string + ' output.geo -3 -format msh'
print('Launching gmsh using: ' + command_string)
p = subprocess.Popen(command_string, shell=True)
p.wait()
mesh_file = 'output.msh'
data = meshio.read(mesh_file)
if delete_files:
os.remove('output.msh')
os.remove('output.geo')
return data
def alignDims(modelA, modelB):
"""
Make object dimensions compatible for solid body operations.
This function accounts for location coordinates.
Args:
modelA: Input model A
modelB: Input model B
Returns:
Resized model A, resized model B, New model coordinates
"""
ax = modelA.coords[0]
ay = modelA.coords[1]
az = modelA.coords[2]
bx = modelB.coords[0]
by = modelB.coords[1]
bz = modelB.coords[2]
xMaxA = ax + modelA.voxels.shape[0]
yMaxA = ay + modelA.voxels.shape[1]
zMaxA = az + modelA.voxels.shape[2]
xMaxB = bx + modelB.voxels.shape[0]
yMaxB = by + modelB.voxels.shape[1]
zMaxB = bz + modelB.voxels.shape[2]
xNew = min(ax, bx)
yNew = min(ay, by)
zNew = min(az, bz)
xMaxNew = max(xMaxA, xMaxB)
yMaxNew = max(yMaxA, yMaxB)
zMaxNew = max(zMaxA, zMaxB)
voxelsANew = np.zeros((xMaxNew - xNew, yMaxNew - yNew, zMaxNew - zNew), dtype=np.uint16)
voxelsBNew = np.zeros((xMaxNew - xNew, yMaxNew - yNew, zMaxNew - zNew), dtype=np.uint16)
voxelsANew[(ax - xNew):(xMaxA - xNew), (ay - yNew):(yMaxA - yNew), (az - zNew):(zMaxA - zNew)] = modelA.voxels
voxelsBNew[(bx - xNew):(xMaxB - xNew), (by - yNew):(yMaxB - yNew), (bz - zNew):(zMaxB - zNew)] = modelB.voxels
return voxelsANew, voxelsBNew, (xNew, yNew, zNew)
def checkResolution(modelA, modelB):
"""
Check if model resolutions are compatible for solid geometry operations.
Incompatible resolutions will print an error. This will not prevent the
operation from running, but it may indicate that the models are at
different scales or that a resolution value has been incorrectly set.
Args:
modelA: Input model A
modelB: Input model B
Returns:
Check successful T/F
"""
a = modelA.resolution
b = modelB.resolution
if a != b:
print('WARNING: inconsistent resolutions: ' + str(a) + ', ' + str(b))
return False
else:
return True
"""
Functions to generate structuring elements
"""
def structSphere(radius: int, plane: Axes):
"""
Generate a spherical structuring element.
Args:
radius: Radius of structuring element in voxels
plane: Structuring element directions, set using Axes class
Returns:
Structuring element array
"""
diameter = (radius * 2) + 1
struct = np.zeros((diameter, diameter, diameter), dtype=np.bool_)
for x in range(diameter):
for y in range(diameter):
for z in range(diameter):
xd = (x - radius)
yd = (y - radius)
zd = (z - radius)
r = np.sqrt(xd ** 2 + yd ** 2 + zd ** 2)
if r < (radius + .5):
struct[x, y, z] = 1
if plane.value[0] != 1:
struct[:radius, :, :].fill(0)
struct[-radius:, :, :].fill(0)
if plane.value[1] != 1:
struct[:, :radius, :].fill(0)
struct[:, -radius:, :].fill(0)
if plane.value[2] != 1:
struct[:, :, :radius].fill(0)
struct[:, :, -radius:].fill(0)
return struct
def structStandard(connectivity: int, plane: Axes):
"""
Generate a 3x3x3 structuring element with the specified connectivity.
Outer face of structuring element illustrated for connectivity values 1-3:
0,0,0 | 0,1,0 | 1,1,1\n
0,1,0 | 1,1,1 | 1,1,1\n
0,0,0 | 0,1,0 | 1,1,1
Args:
connectivity: Connectivity of structuring element (1-3)
plane: Structuring element directions, set using Axes class
Returns:
Structuring element array
"""
struct = ndimage.generate_binary_structure(3, connectivity)
if plane.value[0] != 1:
struct[0, :, :].fill(0)
struct[2, :, :].fill(0)
if plane.value[1] != 1:
struct[:, 0, :].fill(0)
struct[:, 2, :].fill(0)
if plane.value[2] != 1:
struct[:, :, 0].fill(0)
struct[:, :, 2].fill(0)
return struct
def generateMaterials(m):
"""
Generate the materials table for a single-material VoxelModel.
Args:
m: Material id corresponding to materials.py
Returns:
Array containing the specified material and the empty material
"""
materials = np.zeros(len(material_properties) + 1, dtype=np.float32)
material_vector = np.zeros(len(material_properties) + 1, dtype=np.float32)
material_vector[0] = 1
material_vector[m+1] = 1
materials = np.vstack((materials, material_vector))
return materials
@njit(parallel=True)
def findFilledVoxels(a, b):
x_len = len(a[:, 0, 0])
y_len = len(a[0, :, 0])
z_len = len(b[:, 0])
f3 = np.zeros((x_len, z_len), dtype=np.float32)
for x in prange(x_len):
temp = np.zeros((y_len, z_len), dtype=np.float32)
for y in range(y_len):
for z in range(z_len):
temp[y, z] = a[x, y, :].dot(b[z, :])
f1 = ((temp[:, :] >= (0 - FLOATING_ERROR)).sum(0) == 4)
f2 = ((temp[:, :] <= (1 + FLOATING_ERROR)).sum(0) == 4)
f3[x] = f1 & f2
return f3
@njit()
def toFullMaterials(voxels, materials, n_materials):
"""
Convert from index-based material mixture storage to storing
full material mixtures at every voxel.
This representation requires much more memory, but is
needed for some operations. Also see toIndexedMaterials().
Args:
voxels: VoxelModel.voxels
materials: VoxelModel.materials
n_materials: Number of materials in the material properties table
Returns:
Model data array
"""
x_len = voxels.shape[0]
y_len = voxels.shape[1]
z_len = voxels.shape[2]
full_model = np.zeros((x_len, y_len, z_len, n_materials), dtype=np.float32)
for x in range(x_len):
for y in range(y_len):
for z in range(z_len):
i = voxels[x,y,z]
full_model[x,y,z,:] = materials[i]
return full_model
def toIndexedMaterials(voxels, model, resolution):
"""
Convert from storing full material mixtures at every voxel
to index-based material mixture storage.
Also see toFullMaterials().
Args:
voxels: Model data array
model: Reference VoxelModel for size and coords
resolution: Model resolution
Returns:
VoxelModel
"""
x_len = model.voxels.shape[0]
y_len = model.voxels.shape[1]
z_len = model.voxels.shape[2]
new_voxels = np.zeros((x_len, y_len, z_len), dtype=np.int32)
new_materials = np.zeros((1, len(model.materials[0])), dtype=np.float32)
for x in range(x_len): # tqdm(range(x_len), desc='Converting to indexed materials'):
for y in range(y_len):
for z in range(z_len):
m = voxels[x, y, z, :]
i = np.where(np.equal(new_materials, m).all(1))[0]
if len(i) > 0:
new_voxels[x, y, z] = i[0]
else:
new_materials = np.vstack((new_materials, m))
new_voxels[x, y, z] = len(new_materials) - 1
return VoxelModel(new_voxels, new_materials, coords=model.coords, resolution=resolution)
@njit()
def addError(model, error, constant, i, x, y, z, x_len, y_len, z_len, error_spread_threshold):
if y < y_len and x < x_len and z < z_len:
high = np.where(model[x, y, z, 1:] > error_spread_threshold)[0]
if len(high) == 0:
model[x, y, z, i] += error * constant * model[x, y, z, 0]
@njit()
def ditherOptimized(full_model, use_full, x_error, y_error, z_error, error_spread_threshold):
x_len = full_model.shape[0]
y_len = full_model.shape[1]
z_len = full_model.shape[2]
for z in range(z_len):
for y in range(y_len):
for x in range(x_len):
voxel = full_model[x, y, z]
if voxel[0] == 1.0:
max_i = voxel[1:].argmax()+1
for i in range(1, len(voxel)):
if full_model[x, y, z, i] != 0:
old = full_model[x, y, z, i]
if i == max_i:
full_model[x, y, z, i] = 1
else:
full_model[x, y, z, i] = 0
error = old - full_model[x, y, z, i]
if use_full:
# Based on Fundamentals of 3D Halftoning by Lou and Stucki
addError(full_model, error, 4/21, i, x+1, y, z, x_len, y_len, z_len, error_spread_threshold)
addError(full_model, error, 1/21, i, x+2, y, z, x_len, y_len, z_len, error_spread_threshold)
addError(full_model, error, 4/21, i, x, y+1, z, x_len, y_len, z_len, error_spread_threshold)
addError(full_model, error, 1/21, i, x, y+2, z, x_len, y_len, z_len, error_spread_threshold)
addError(full_model, error, 1/21, i, x+1, y+1, z, x_len, y_len, z_len, error_spread_threshold)
addError(full_model, error, 1/21, i, x-1, y+1, z, x_len, y_len, z_len, error_spread_threshold)
addError(full_model, error, 1/21, i, x, y-1, z+1, x_len, y_len, z_len, error_spread_threshold)
addError(full_model, error, 1/21, i, x-1, y, z+1, x_len, y_len, z_len, error_spread_threshold)
addError(full_model, error, 1/21, i, x, y+1, z+1, x_len, y_len, z_len, error_spread_threshold)
addError(full_model, error, 1/21, i, x+1, y, z+1, x_len, y_len, z_len, error_spread_threshold)
addError(full_model, error, 4/21, i, x, y, z+1, x_len, y_len, z_len, error_spread_threshold)
addError(full_model, error, 1/21, i, x, y, z+2, x_len, y_len, z_len, error_spread_threshold)
else:
addError(full_model, error, x_error, i, x+1, y, z, x_len, y_len, z_len, error_spread_threshold)
addError(full_model, error, y_error, i, x, y+1, z, x_len, y_len, z_len, error_spread_threshold)
addError(full_model, error, z_error, i, x, y, z+1, x_len, y_len, z_len, error_spread_threshold)
return full_model
@cuda.jit
def updateMatIndices(voxels, old_materials, new_materials):
# Get current voxel coordinates
x, y, z = cuda.grid(3)
x_max, y_max, z_max = voxels.shape
# Ignore coordinates outside of model
if (x >= x_max) or (y >= y_max) or (z >= z_max):
return
# Get target material
target_mat_index = voxels[x, y, z]
target_mat = old_materials[target_mat_index, :]
# Search for material
for m in range(new_materials.shape[0]):
# If all material channels match...
match = True
for c in range(new_materials.shape[1]):
error = abs(new_materials[m, c] - target_mat[c])
if error > FLOATING_ERROR:
match = False
break
# ...save the current index and end search
if match:
voxels[x, y, z] = m
break
else:
voxels[x, y, z] = -1
'''
Functions for writing to xml files
'''
def writeHeader(f: TextIO, version: str, encoding: str):
"""
Write XML file header
Args:
f: File object
version: XML version number
encoding: Encoding type
Returns:
None
"""
f.write('<?xml version="' + version + '" encoding="' + encoding + '"?>\n')
def writeData(f: TextIO, name: str, value, tab_level: int = 0):
"""
Write a data element and the surrounding tags.
Args:
f: File object
name: Tag name
value: Data value
tab_level: Number of tabs (2 spaces) before start of line
Returns:
None
"""
for i in range(tab_level):
f.write(' ')
f.write('<' + name + '>')
f.write(str(value))
f.write('</' + name + '>\n')
def writeOpen(f: TextIO, name: str, tab_level: int = 0):
"""
Write an opening tag.
Args:
f: File object
name: Tag name
tab_level: Number of tabs (2 spaces) before start of line
Returns:
None
"""
for i in range(tab_level):
f.write(' ')
f.write('<' + name + '>\n')
def writeClos(f: TextIO, name: str, tab_level: int = 0):
"""
Write a closing tag.
Args:
f: File object
name: Tag name
tab_level: Number of tabs (2 spaces) before start of line
Returns:
None
"""
for i in range(tab_level):
f.write(' ')
f.write('</' + name + '>\n')
Functions
def addError(model, error, constant, i, x, y, z, x_len, y_len, z_len, error_spread_threshold)
-
Expand source code
@njit() def addError(model, error, constant, i, x, y, z, x_len, y_len, z_len, error_spread_threshold): if y < y_len and x < x_len and z < z_len: high = np.where(model[x, y, z, 1:] > error_spread_threshold)[0] if len(high) == 0: model[x, y, z, i] += error * constant * model[x, y, z, 0]
def alignDims(modelA, modelB)
-
Make object dimensions compatible for solid body operations.
This function accounts for location coordinates.
Args
modelA
- Input model A
modelB
- Input model B
Returns
Resized model A, resized model B, New model coordinates
Expand source code
def alignDims(modelA, modelB): """ Make object dimensions compatible for solid body operations. This function accounts for location coordinates. Args: modelA: Input model A modelB: Input model B Returns: Resized model A, resized model B, New model coordinates """ ax = modelA.coords[0] ay = modelA.coords[1] az = modelA.coords[2] bx = modelB.coords[0] by = modelB.coords[1] bz = modelB.coords[2] xMaxA = ax + modelA.voxels.shape[0] yMaxA = ay + modelA.voxels.shape[1] zMaxA = az + modelA.voxels.shape[2] xMaxB = bx + modelB.voxels.shape[0] yMaxB = by + modelB.voxels.shape[1] zMaxB = bz + modelB.voxels.shape[2] xNew = min(ax, bx) yNew = min(ay, by) zNew = min(az, bz) xMaxNew = max(xMaxA, xMaxB) yMaxNew = max(yMaxA, yMaxB) zMaxNew = max(zMaxA, zMaxB) voxelsANew = np.zeros((xMaxNew - xNew, yMaxNew - yNew, zMaxNew - zNew), dtype=np.uint16) voxelsBNew = np.zeros((xMaxNew - xNew, yMaxNew - yNew, zMaxNew - zNew), dtype=np.uint16) voxelsANew[(ax - xNew):(xMaxA - xNew), (ay - yNew):(yMaxA - yNew), (az - zNew):(zMaxA - zNew)] = modelA.voxels voxelsBNew[(bx - xNew):(xMaxB - xNew), (by - yNew):(yMaxB - yNew), (bz - zNew):(zMaxB - zNew)] = modelB.voxels return voxelsANew, voxelsBNew, (xNew, yNew, zNew)
def checkResolution(modelA, modelB)
-
Check if model resolutions are compatible for solid geometry operations.
Incompatible resolutions will print an error. This will not prevent the operation from running, but it may indicate that the models are at different scales or that a resolution value has been incorrectly set.
Args
modelA
- Input model A
modelB
- Input model B
Returns
Check successful T/F
Expand source code
def checkResolution(modelA, modelB): """ Check if model resolutions are compatible for solid geometry operations. Incompatible resolutions will print an error. This will not prevent the operation from running, but it may indicate that the models are at different scales or that a resolution value has been incorrectly set. Args: modelA: Input model A modelB: Input model B Returns: Check successful T/F """ a = modelA.resolution b = modelB.resolution if a != b: print('WARNING: inconsistent resolutions: ' + str(a) + ', ' + str(b)) return False else: return True
def ditherOptimized(full_model, use_full, x_error, y_error, z_error, error_spread_threshold)
-
Expand source code
@njit() def ditherOptimized(full_model, use_full, x_error, y_error, z_error, error_spread_threshold): x_len = full_model.shape[0] y_len = full_model.shape[1] z_len = full_model.shape[2] for z in range(z_len): for y in range(y_len): for x in range(x_len): voxel = full_model[x, y, z] if voxel[0] == 1.0: max_i = voxel[1:].argmax()+1 for i in range(1, len(voxel)): if full_model[x, y, z, i] != 0: old = full_model[x, y, z, i] if i == max_i: full_model[x, y, z, i] = 1 else: full_model[x, y, z, i] = 0 error = old - full_model[x, y, z, i] if use_full: # Based on Fundamentals of 3D Halftoning by Lou and Stucki addError(full_model, error, 4/21, i, x+1, y, z, x_len, y_len, z_len, error_spread_threshold) addError(full_model, error, 1/21, i, x+2, y, z, x_len, y_len, z_len, error_spread_threshold) addError(full_model, error, 4/21, i, x, y+1, z, x_len, y_len, z_len, error_spread_threshold) addError(full_model, error, 1/21, i, x, y+2, z, x_len, y_len, z_len, error_spread_threshold) addError(full_model, error, 1/21, i, x+1, y+1, z, x_len, y_len, z_len, error_spread_threshold) addError(full_model, error, 1/21, i, x-1, y+1, z, x_len, y_len, z_len, error_spread_threshold) addError(full_model, error, 1/21, i, x, y-1, z+1, x_len, y_len, z_len, error_spread_threshold) addError(full_model, error, 1/21, i, x-1, y, z+1, x_len, y_len, z_len, error_spread_threshold) addError(full_model, error, 1/21, i, x, y+1, z+1, x_len, y_len, z_len, error_spread_threshold) addError(full_model, error, 1/21, i, x+1, y, z+1, x_len, y_len, z_len, error_spread_threshold) addError(full_model, error, 4/21, i, x, y, z+1, x_len, y_len, z_len, error_spread_threshold) addError(full_model, error, 1/21, i, x, y, z+2, x_len, y_len, z_len, error_spread_threshold) else: addError(full_model, error, x_error, i, x+1, y, z, x_len, y_len, z_len, error_spread_threshold) addError(full_model, error, y_error, i, x, y+1, z, x_len, y_len, z_len, error_spread_threshold) addError(full_model, error, z_error, i, x, y, z+1, x_len, y_len, z_len, error_spread_threshold) return full_model
def findFilledVoxels(a, b)
-
Expand source code
@njit(parallel=True) def findFilledVoxels(a, b): x_len = len(a[:, 0, 0]) y_len = len(a[0, :, 0]) z_len = len(b[:, 0]) f3 = np.zeros((x_len, z_len), dtype=np.float32) for x in prange(x_len): temp = np.zeros((y_len, z_len), dtype=np.float32) for y in range(y_len): for z in range(z_len): temp[y, z] = a[x, y, :].dot(b[z, :]) f1 = ((temp[:, :] >= (0 - FLOATING_ERROR)).sum(0) == 4) f2 = ((temp[:, :] <= (1 + FLOATING_ERROR)).sum(0) == 4) f3[x] = f1 & f2 return f3
def generateMaterials(m)
-
Generate the materials table for a single-material VoxelModel.
Args
m
- Material id corresponding to materials.py
Returns
Array containing the specified material and the empty material
Expand source code
def generateMaterials(m): """ Generate the materials table for a single-material VoxelModel. Args: m: Material id corresponding to materials.py Returns: Array containing the specified material and the empty material """ materials = np.zeros(len(material_properties) + 1, dtype=np.float32) material_vector = np.zeros(len(material_properties) + 1, dtype=np.float32) material_vector[0] = 1 material_vector[m+1] = 1 materials = np.vstack((materials, material_vector)) return materials
def getMaterialData(material_id)
-
Get the material data for a specific material id.
Args
material_id
- Material id corresponding to materials.py
Returns
Dictionary of material properties
Expand source code
def getMaterialData(material_id): """ Get the material data for a specific material id. Args: material_id: Material id corresponding to materials.py Returns: Dictionary of material properties """ current_material_data = next((item for item in material_properties if item['id'] == material_id)) if current_material_data is None: print('Material data not available for id ' + str(material_id) + ' -- using first nonzero material') current_material_data = next((item for item in material_properties if item['id'] != 0)) return current_material_data
def makeMesh(filename: str, delete_files: bool = True, gmsh_on_path: bool = False)
-
Import mesh data from file
Args
filename
- File name with extension
delete_files
- Enable/disable deleting temporary files when finished
gmsh_on_path
- Enable/disable using system gmsh rather than bundled gmsh
Returns
Mesh data (points, tris, and tets)
Expand source code
def makeMesh(filename: str, delete_files: bool = True, gmsh_on_path: bool = False): """ Import mesh data from file Args: filename: File name with extension delete_files: Enable/disable deleting temporary files when finished gmsh_on_path: Enable/disable using system gmsh rather than bundled gmsh Returns: Mesh data (points, tris, and tets) """ template = ''' Merge "{0}"; Surface Loop(1) = {{1}}; //+ Volume(1) = {{1}}; ''' geo_string = template.format(filename) with open('output.geo', 'w') as f: f.writelines(geo_string) if gmsh_on_path: command_string = 'gmsh ' else: # Check OS type if os.name.startswith('nt'): # Windows command_string = f'"{os.path.dirname(os.path.realpath(__file__))}\\utils\\gmsh.exe"' else: # Linux command_string = f'"{os.path.dirname(os.path.realpath(__file__))}/utils/gmsh"' command_string = command_string + ' output.geo -3 -format msh' print('Launching gmsh using: ' + command_string) p = subprocess.Popen(command_string, shell=True) p.wait() mesh_file = 'output.msh' data = meshio.read(mesh_file) if delete_files: os.remove('output.msh') os.remove('output.geo') return data
def rgb_to_hex(r: float, g: float, b: float)
-
Convert RGB values to a single hexadecimal value.
Args
r
- Red percentage (0-1)
g
- Green percentage (0-1)
b
- Blue percentage (0-1)
Returns
Hexadecimal color as an integer
Expand source code
def rgb_to_hex(r: float, g: float, b: float): """ Convert RGB values to a single hexadecimal value. Args: r: Red percentage (0-1) g: Green percentage (0-1) b: Blue percentage (0-1) Returns: Hexadecimal color as an integer """ r = round(r * 255) g = round(g * 255) b = round(b * 255) hex_str = '0x{:02x}{:02x}{:02x}'.format(r, g, b) return int(hex_str, base=16)
def structSphere(radius: int, plane: Axes)
-
Generate a spherical structuring element.
Args
radius
- Radius of structuring element in voxels
plane
- Structuring element directions, set using Axes class
Returns
Structuring element array
Expand source code
def structSphere(radius: int, plane: Axes): """ Generate a spherical structuring element. Args: radius: Radius of structuring element in voxels plane: Structuring element directions, set using Axes class Returns: Structuring element array """ diameter = (radius * 2) + 1 struct = np.zeros((diameter, diameter, diameter), dtype=np.bool_) for x in range(diameter): for y in range(diameter): for z in range(diameter): xd = (x - radius) yd = (y - radius) zd = (z - radius) r = np.sqrt(xd ** 2 + yd ** 2 + zd ** 2) if r < (radius + .5): struct[x, y, z] = 1 if plane.value[0] != 1: struct[:radius, :, :].fill(0) struct[-radius:, :, :].fill(0) if plane.value[1] != 1: struct[:, :radius, :].fill(0) struct[:, -radius:, :].fill(0) if plane.value[2] != 1: struct[:, :, :radius].fill(0) struct[:, :, -radius:].fill(0) return struct
def structStandard(connectivity: int, plane: Axes)
-
Generate a 3x3x3 structuring element with the specified connectivity.
Outer face of structuring element illustrated for connectivity values 1-3:
0,0,0 | 0,1,0 | 1,1,1
0,1,0 | 1,1,1 | 1,1,1
0,0,0 | 0,1,0 | 1,1,1
Args
connectivity
- Connectivity of structuring element (1-3)
plane
- Structuring element directions, set using Axes class
Returns
Structuring element array
Expand source code
def structStandard(connectivity: int, plane: Axes): """ Generate a 3x3x3 structuring element with the specified connectivity. Outer face of structuring element illustrated for connectivity values 1-3: 0,0,0 | 0,1,0 | 1,1,1\n 0,1,0 | 1,1,1 | 1,1,1\n 0,0,0 | 0,1,0 | 1,1,1 Args: connectivity: Connectivity of structuring element (1-3) plane: Structuring element directions, set using Axes class Returns: Structuring element array """ struct = ndimage.generate_binary_structure(3, connectivity) if plane.value[0] != 1: struct[0, :, :].fill(0) struct[2, :, :].fill(0) if plane.value[1] != 1: struct[:, 0, :].fill(0) struct[:, 2, :].fill(0) if plane.value[2] != 1: struct[:, :, 0].fill(0) struct[:, :, 2].fill(0) return struct
def toFullMaterials(voxels, materials, n_materials)
-
Convert from index-based material mixture storage to storing full material mixtures at every voxel.
This representation requires much more memory, but is needed for some operations. Also see toIndexedMaterials().
Args
voxels
- VoxelModel.voxels
materials
- VoxelModel.materials
n_materials
- Number of materials in the material properties table
Returns
Model data array
Expand source code
@njit() def toFullMaterials(voxels, materials, n_materials): """ Convert from index-based material mixture storage to storing full material mixtures at every voxel. This representation requires much more memory, but is needed for some operations. Also see toIndexedMaterials(). Args: voxels: VoxelModel.voxels materials: VoxelModel.materials n_materials: Number of materials in the material properties table Returns: Model data array """ x_len = voxels.shape[0] y_len = voxels.shape[1] z_len = voxels.shape[2] full_model = np.zeros((x_len, y_len, z_len, n_materials), dtype=np.float32) for x in range(x_len): for y in range(y_len): for z in range(z_len): i = voxels[x,y,z] full_model[x,y,z,:] = materials[i] return full_model
def toIndexedMaterials(voxels, model, resolution)
-
Convert from storing full material mixtures at every voxel to index-based material mixture storage.
Also see toFullMaterials().
Args
voxels
- Model data array
model
- Reference VoxelModel for size and coords
resolution
- Model resolution
Returns
VoxelModel
Expand source code
def toIndexedMaterials(voxels, model, resolution): """ Convert from storing full material mixtures at every voxel to index-based material mixture storage. Also see toFullMaterials(). Args: voxels: Model data array model: Reference VoxelModel for size and coords resolution: Model resolution Returns: VoxelModel """ x_len = model.voxels.shape[0] y_len = model.voxels.shape[1] z_len = model.voxels.shape[2] new_voxels = np.zeros((x_len, y_len, z_len), dtype=np.int32) new_materials = np.zeros((1, len(model.materials[0])), dtype=np.float32) for x in range(x_len): # tqdm(range(x_len), desc='Converting to indexed materials'): for y in range(y_len): for z in range(z_len): m = voxels[x, y, z, :] i = np.where(np.equal(new_materials, m).all(1))[0] if len(i) > 0: new_voxels[x, y, z] = i[0] else: new_materials = np.vstack((new_materials, m)) new_voxels[x, y, z] = len(new_materials) - 1 return VoxelModel(new_voxels, new_materials, coords=model.coords, resolution=resolution)
def writeClos(f:
, name: str, tab_level: int = 0) -
Write a closing tag.
Args
f
- File object
name
- Tag name
tab_level
- Number of tabs (2 spaces) before start of line
Returns
None
Expand source code
def writeClos(f: TextIO, name: str, tab_level: int = 0): """ Write a closing tag. Args: f: File object name: Tag name tab_level: Number of tabs (2 spaces) before start of line Returns: None """ for i in range(tab_level): f.write(' ') f.write('</' + name + '>\n')
def writeData(f:
, name: str, value, tab_level: int = 0) -
Write a data element and the surrounding tags.
Args
f
- File object
name
- Tag name
value
- Data value
tab_level
- Number of tabs (2 spaces) before start of line
Returns
None
Expand source code
def writeData(f: TextIO, name: str, value, tab_level: int = 0): """ Write a data element and the surrounding tags. Args: f: File object name: Tag name value: Data value tab_level: Number of tabs (2 spaces) before start of line Returns: None """ for i in range(tab_level): f.write(' ') f.write('<' + name + '>') f.write(str(value)) f.write('</' + name + '>\n')
def writeHeader(f:
, version: str, encoding: str) -
Write XML file header
Args
f
- File object
version
- XML version number
encoding
- Encoding type
Returns
None
Expand source code
def writeHeader(f: TextIO, version: str, encoding: str): """ Write XML file header Args: f: File object version: XML version number encoding: Encoding type Returns: None """ f.write('<?xml version="' + version + '" encoding="' + encoding + '"?>\n')
def writeOpen(f:
, name: str, tab_level: int = 0) -
Write an opening tag.
Args
f
- File object
name
- Tag name
tab_level
- Number of tabs (2 spaces) before start of line
Returns
None
Expand source code
def writeOpen(f: TextIO, name: str, tab_level: int = 0): """ Write an opening tag. Args: f: File object name: Tag name tab_level: Number of tabs (2 spaces) before start of line Returns: None """ for i in range(tab_level): f.write(' ') f.write('<' + name + '>\n')
Classes
class Axes (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
Options for axes and planes.
Expand source code
class Axes(Enum): """ Options for axes and planes. """ X = (1,0,0) Y = (0,1,0) Z = (0,0,1) XY = (1,1,0) XZ = (1,0,1) YZ = (0,1,1) XYZ = (1,1,1)
Ancestors
- enum.Enum
Class variables
var X
var XY
var XYZ
var XZ
var Y
var YZ
var Z
class Dir (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
Options for projection directions.
Expand source code
class Dir(Enum): """ Options for projection directions. """ UP = 1 DOWN = 2 BOTH = 3
Ancestors
- enum.Enum
Class variables
var BOTH
var DOWN
var UP
class GpuSettings
-
Object to store GPU settings.
After initializing and configuring the GPU settings, use applySettings() to apply them. Changes will only persist for the current Python session.
For persistent GPU settings, configure these environment variables:
VF_CUDA_ENABLE = 1
VF_CUDA_DEVICE = <desired GPU ID>
Example:
gpu = GpuSettings()
print('Default CUDA settings:' + str(gpu.CUDA_enable) + ', ' + str(gpu.CUDA_device))
gpu.setCUDA(True, 1)
gpu.applySettings()
Initialize a GpuSettings object.
Expand source code
class GpuSettings: """ Object to store GPU settings. After initializing and configuring the GPU settings, use applySettings() to apply them. Changes will only persist for the current Python session. For persistent GPU settings, configure these environment variables: ``VF_CUDA_ENABLE = 1`` ``VF_CUDA_DEVICE = <desired GPU ID>`` ---- Example: ``gpu = GpuSettings()`` ``print('Default CUDA settings:' + str(gpu.CUDA_enable) + ', ' + str(gpu.CUDA_device))`` ``gpu.setCUDA(True, 1)`` ``gpu.applySettings()`` ---- """ def __init__(self): """ Initialize a GpuSettings object. """ # Get CUDA settings from environment variables try: CUDA_enable = bool(os.environ.get('VF_CUDA_ENABLE')) CUDA_device = int(os.environ.get('VF_CUDA_DEVICE')) except TypeError: print('CUDA environment variables not found') CUDA_enable = False CUDA_device = 0 self.CUDA_enable = CUDA_enable self.CUDA_device = CUDA_device def setCUDA(self, CUDA_enable: bool = True, CUDA_device: int = 0): """ Set overrides for CUDA settings. Args: CUDA_enable: Enable/disable CUDA acceleration CUDA_device: Select CUDA device Returns: None """ self.CUDA_enable = CUDA_enable self.CUDA_device = CUDA_device def applySettings(self): """ Apply GPU settings as overrides. These changes will only persist for the current python session. Returns: None """ os.environ['VF_CUDA_ENABLE'] = str(int(self.CUDA_enable)) os.environ['VF_CUDA_DEVICE'] = str(self.CUDA_device)
Methods
def applySettings(self)
-
Apply GPU settings as overrides.
These changes will only persist for the current python session.
Returns
None
Expand source code
def applySettings(self): """ Apply GPU settings as overrides. These changes will only persist for the current python session. Returns: None """ os.environ['VF_CUDA_ENABLE'] = str(int(self.CUDA_enable)) os.environ['VF_CUDA_DEVICE'] = str(self.CUDA_device)
def setCUDA(self, CUDA_enable: bool = True, CUDA_device: int = 0)
-
Set overrides for CUDA settings.
Args
CUDA_enable
- Enable/disable CUDA acceleration
CUDA_device
- Select CUDA device
Returns
None
Expand source code
def setCUDA(self, CUDA_enable: bool = True, CUDA_device: int = 0): """ Set overrides for CUDA settings. Args: CUDA_enable: Enable/disable CUDA acceleration CUDA_device: Select CUDA device Returns: None """ self.CUDA_enable = CUDA_enable self.CUDA_device = CUDA_device
class Process (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
Options for manufacturing process types.
Expand source code
class Process(Enum): """ Options for manufacturing process types. """ LASER = 1 MILL = 2 PRINT = 3 CAST = 4 INSERT = 5
Ancestors
- enum.Enum
Class variables
var CAST
var INSERT
var LASER
var MILL
var PRINT
class Struct (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
Options for structuring element shapes.
Expand source code
class Struct(Enum): """ Options for structuring element shapes. """ STANDARD = 1 SPHERE = 2
Ancestors
- enum.Enum
Class variables
var SPHERE
var STANDARD
class VoxelModel (voxels: numpy.ndarray, materials: Union[int, numpy.ndarray] = None, coords: Tuple[int, int, int] = (0, 0, 0), resolution: float = 1)
-
VoxelModel object that stores geometry, position, and material information.
Initialize a VoxelModel object.
Args
voxels
- Array storing the index of the material present at each voxel
materials
- Material index (int), or array of all material mixtures present in model with material format: (a, m0, m1, … mn)
coords
- Model origin coordinates
resolution
- Number of voxels per mm (higher number = finer resolution)
Expand source code
class VoxelModel: """ VoxelModel object that stores geometry, position, and material information. """ def __init__(self, voxels: np.ndarray, materials: TypeUnion[int, np.ndarray] = None, coords: Tuple[int, int, int] = (0, 0, 0), resolution: float = 1): """ Initialize a VoxelModel object. Args: voxels: Array storing the index of the material present at each voxel materials: Material index (int), or array of all material mixtures present in model with material format: (a, m0, m1, ... mn) coords: Model origin coordinates resolution: Number of voxels per mm (higher number = finer resolution) """ self.voxels = np.copy(voxels) # Use np.copy to break references # Determine how materials were specified and create the materials array accordingly if materials is None: self.materials = generateMaterials(1) elif isinstance(materials, int): self.materials = generateMaterials(materials) else: self.materials = np.copy(materials) self.coords = coords self.resolution = resolution self.components = np.zeros_like(voxels) self.numComponents = 0 @classmethod def fromVoxFile(cls, filename: str, coords: Tuple[int, int, int] = (0, 0, 0), resolution: float = 1): """ Create a VoxelModel from an imported .vox file. ---- Example: ``model1 = vf.VoxelModel.fromVoxFile('cylinder-red.vox', (0, 5, 0), 1)`` ---- Args: filename: File name with extension coords: Model origin coordinates resolution: Number of voxels per mm Returns: VoxelModel """ # Import data and align axes v1 = VoxParser(filename).parse() v2 = np.array(v1.to_dense(), dtype=np.uint16) v2 = np.flip(v2, 1) v2 = np.rot90(v2, 1, (2, 0)) v2 = np.rot90(v2, 1, (1, 2)) # Generate materials table assuming indices match materials in material_properties i = 0 materials = np.zeros((1, len(material_properties) + 1), dtype=np.float32) for m in np.unique(v2): if m != 0: i = i+1 material_vector = np.zeros(len(material_properties) + 1, dtype=np.float32) material_vector[0] = 1 material_vector[m+1] = 1 materials = np.vstack((materials, material_vector)) v2[v2 == m] = i return cls(v2, materials, coords=coords, resolution=resolution) @classmethod def fromMeshFile(cls, filename: str, coords: Tuple[int, int, int] = (0, 0, 0), material: int = 1, resolution: float = 1, gmsh_on_path: bool = False): """ Create a VoxelModel from an imported mesh file. ---- Example: ``model1 = vf.VoxelModel.fromMeshFile('center.stl', (67, 3, 0), 2, 1)`` ____ Args: filename: File name with extension coords: Model origin coordinates material: Material id corresponding to materials.py resolution: Number of voxels per mm gmsh_on_path: Enable/disable using system gmsh rather than bundled gmsh Returns: VoxelModel """ data = makeMesh(filename, True, gmsh_on_path) points = data.points # Get lists of indices of point # ii_tri = data.cells_dict['triangle'] ii_tet = data.cells_dict['tetra'] # Convert lists of indices to lists of points # tris = points[ii_tri] tets = points[ii_tet] # Create barycentric coordinate system T = np.concatenate((tets, tets[:, :, 0:1] * 0 + 1), 2) T_inv = np.zeros(T.shape) for ii, t in enumerate(T): T_inv[ii] = np.linalg.inv(t).T # Find bounding box base = 1 / resolution points_min = points.min(0) points_max = points.max(0) points_min_r = base * np.round(points_min/base) points_max_r = base * np.round(points_max/base) # Create 3D grid xx = np.r_[points_min_r[0]:points_max_r[0]+1:base] yy = np.r_[points_min_r[1]:points_max_r[1]+1:base] zz = np.r_[points_min_r[2]:points_max_r[2]+1:base] # Find center of every grid point xx_mid = (xx[1:] + xx[:-1]) / 2 yy_mid = (yy[1:] + yy[:-1]) / 2 zz_mid = (zz[1:] + zz[:-1]) / 2 # Create grid of voxel centers xyz_mid = np.array(np.meshgrid(xx_mid, yy_mid, zz_mid, indexing='ij')) xyz_mid = xyz_mid.transpose(1, 2, 3, 0) # Convert to list of points xyz_mid = xyz_mid.reshape(-1, 3) # Add 1 to allow conversion to barycentric coordinates xyz_mid = np.concatenate((xyz_mid, xyz_mid[:, 0:1] * 0 + 1), 1) # Create list of indices of each voxel ijk_mid = np.array(np.meshgrid(np.r_[:len(xx_mid)], np.r_[:len(yy_mid)], np.r_[:len(zz_mid)], indexing='ij')) ijk_mid = ijk_mid.transpose(1, 2, 3, 0) ijk_mid2 = ijk_mid.reshape(-1, 3) f3 = findFilledVoxels(np.asarray(T_inv, order='c'), np.asarray(xyz_mid, order='c')) ii, jj = f3.nonzero() lmn = ijk_mid2[np.unique(jj)] voxels = np.zeros(ijk_mid.shape[:3], dtype=np.bool_) voxels[lmn[:, 0], lmn[:, 1], lmn[:, 2]] = True new_model = cls(voxels, generateMaterials(material), coords=coords, resolution=resolution).fitWorkspace() return new_model @classmethod def empty(cls, size: Tuple[int, int, int], coords: Tuple[int, int, int] = (0, 0, 0), resolution: float = 1, num_materials: int = len(material_properties)): """ Initialize an empty VoxelModel. Args: size: Size of the empty model in voxels coords: Model origin coordinates resolution: Number of voxels per mm num_materials: Number of material types in materials vector Returns: VoxelModel """ model_data = np.zeros(size, dtype=np.uint16) materials = np.zeros((1, num_materials + 1), dtype=np.float32) new_model = cls(model_data, materials, coords=coords, resolution=resolution) return new_model @classmethod def emptyLike(cls, voxel_model): """ Initialize an empty VoxelModel with the same size, materials, coords, and resolution as another model. Args: voxel_model: Reference VoxelModel object Returns: VoxelModel """ new_model = cls(np.zeros_like(voxel_model.voxels, dtype=np.uint16), voxel_model.materials, coords=voxel_model.coords, resolution=voxel_model.resolution) return new_model @classmethod def copy(cls, voxel_model): """ Initialize an VoxelModel that is a copy of another model. Args: voxel_model: Reference VoxelModel object Returns: VoxelModel """ new_model = cls(voxel_model.voxels, voxel_model.materials, coords=voxel_model.coords, resolution=voxel_model.resolution) new_model.numComponents = voxel_model.numComponents new_model.components = voxel_model.components return new_model # Property update operations ############################## def setResolution(self, res: float): """ Change the resolution of a model. Args: res: aaa Returns: None """ new_model = VoxelModel.copy(self) new_model.resolution = res return new_model def fitWorkspace(self): """ Remove excess empty space from a model. Resize the workspace around a model to remove excess empty space. Model coordinates are updated to reflect the change. Returns: VoxelModel """ x_len = self.voxels.shape[0] y_len = self.voxels.shape[1] z_len = self.voxels.shape[2] x_min = -1 x_max = -1 y_min = -1 y_max = -1 z_min = -1 z_max = -1 for x in range(x_len): if np.sum(self.voxels[x, :, :]) > 0: x_min = x break for x in range(x_len-1,-1,-1): if np.sum(self.voxels[x, :, :]) > 0: x_max = x+1 break for y in range(y_len): if np.sum(self.voxels[:, y, :]) > 0: y_min = y break for y in range(y_len-1,-1,-1): if np.sum(self.voxels[:, y, :]) > 0: y_max = y+1 break for z in range(z_len): if np.sum(self.voxels[:, :, z]) > 0: z_min = z break for z in range(z_len-1,-1,-1): if np.sum(self.voxels[:, :, z]) > 0: z_max = z+1 break x_min = 0 if x_min == -1 else x_min y_min = 0 if y_min == -1 else y_min z_min = 0 if z_min == -1 else z_min x_max = x_len if x_max == -1 else x_max y_max = y_len if y_max == -1 else y_max z_max = z_len if z_max == -1 else z_max new_voxels = np.copy(self.voxels[x_min:x_max, y_min:y_max, z_min:z_max]) new_components = np.copy(self.components[x_min:x_max, y_min:y_max, z_min:z_max]) new_coords = (self.coords[0] + x_min, self.coords[1] + y_min, self.coords[2] + z_min) new_model = VoxelModel(new_voxels, self.materials, coords=new_coords, resolution=self.resolution) new_model.numComponents = self.numComponents new_model.components = new_components return new_model def removeDuplicateMaterials(self): """ Remove duplicate rows from a model's material array. This function can be greatly accelerated using CUDA. For more information on how to enable CUDA in VoxelFuse, see the GpuSettings class. Returns: VoxelModel """ new_voxels = np.copy(self.voxels) new_materials = np.unique(self.materials, axis=0) # Get CUDA settings try: CUDA_enable = bool(os.environ.get('VF_CUDA_ENABLE')) CUDA_device = int(os.environ.get('VF_CUDA_DEVICE')) except TypeError: print('CUDA environment variables not found, defaulting to CUDA disabled') CUDA_enable = False CUDA_device = 0 if CUDA_enable: # Select GPU cuda.select_device(CUDA_device) # CUDA blocks blockdim = (8, 8, 8) # 512 threads (1024 threads max) griddim = (new_voxels.shape[0] // blockdim[0] + 1, new_voxels.shape[1] // blockdim[1] + 1, new_voxels.shape[2] // blockdim[2] + 1) # Update material indices updateMatIndices[griddim, blockdim](new_voxels, self.materials, new_materials) else: x_len, y_len, z_len = self.voxels.shape for x in tqdm(range(x_len), desc='Removing duplicate materials'): for y in range(y_len): for z in range(z_len): i = self.voxels[x, y, z] m = self.materials[i] ni = np.where(np.equal(new_materials, m).all(1))[0][0] new_voxels[x, y, z] = ni return VoxelModel(new_voxels, new_materials, self.coords, self.resolution) def getComponents(self, connectivity: int = 1): """ Update component labels for a model. This function uses a disconnected components algorithm and assumes that adjacent voxels with different materials are connected. Connectivity can be set to 1-3 and defines the shape of the structuring element. Args: connectivity: Connectivity of structuring element (1-3) Returns: VoxelModel """ mask = np.array(self.voxels[:, :, :] > 0, dtype=np.bool_) struct = ndimage.generate_binary_structure(3, connectivity) new_model = VoxelModel.copy(self) new_model.components, new_model.numComponents = ndimage.label(mask, structure=struct) new_model.components = np.uint8(new_model.components) return new_model # Selection operations ############################## # TODO: Should this reference the material properties table? # TODO: isolateMaterialVector def isolateMaterial(self, material: int): """ Get all voxels with a specified material. ---- Example: ``model2 = model1.isolateMaterial(4)`` ---- Args: material: Material index corresponding to the materials array for the model Returns: VoxelModel """ mask = np.array(self.voxels == material, dtype=np.bool_) materials = np.zeros((2, self.materials.shape[1]), dtype=np.float32) materials[1] = self.materials[material] return VoxelModel(mask.astype(int), materials, self.coords, self.resolution) def isolateLayer(self, layer: int): """ Get all voxels in a specified layer. ---- Example: ``model2 = model1.isolateLayer(8)`` ---- Args: layer: Voxel layer to isolate Returns: VoxelModel """ new_voxels = np.zeros_like(self.voxels, dtype=np.uint16) new_voxels[:, :, layer - self.coords[2]] = self.voxels[:, :, layer - self.coords[2]] return VoxelModel(new_voxels, self.materials, self.coords, self.resolution) def isolateComponent(self, component: int): """ Isolate a component by its component label. Component labels must first be updated with getComponents. Unrecognized component labels will return an empty object. Args: component: Component label to isolate Returns: VoxelModel """ mask = np.array(self.components == component, dtype=np.bool_) new_voxels = np.multiply(self.voxels, mask) return VoxelModel(new_voxels, self.materials, self.coords, self.resolution) # Mask operations ############################## # Material defaults to the first material in the input model def getUnoccupied(self): """ Get all voxels not occupied by the input model. This operation can also be applied using the invert operator (~). ---- Examples: ``model2 = model1.getUnoccupied()`` ``model2 = ~model1`` ---- Returns: VoxelModel """ mask = np.array(self.voxels == 0, dtype=np.bool_) return VoxelModel(mask, self.materials[0:2, :], self.coords, self.resolution) def __invert__(self): """ Get all voxels not occupied by the input model. Overload invert operator (~) for VoxelModel objects with getUnoccupied(). Returns: VoxelModel """ return self.getUnoccupied() def getOccupied(self): """ Get all voxels occupied by the input model. Returns: VoxelModel """ mask = np.array(self.voxels != 0, dtype=np.bool_) return VoxelModel(mask, self.materials[0:2, :], self.coords, self.resolution) def getBoundingBox(self): """ Get all voxels contained in the bounding box of the input model. Returns: VoxelModel """ new_model = VoxelModel.copy(self) new_model = new_model.fitWorkspace() new_model.voxels.fill(1) new_model = new_model.getOccupied() new_model.materials = self.materials[0:2, :] return new_model def setMaterial(self, material: int): """ Set the material of all voxels in a model. ---- Example: ``model2 = model1.getBoundingBox()`` ``model3 = model2.setMaterial(2)`` ---- Args: material: Material id corresponding to materials.py Returns: VoxelModel """ new_voxels = self.getOccupied().voxels # Converts input model to a mask, no effect if input is already a mask material_vector = np.zeros(self.materials.shape[1], dtype=np.float32) material_vector[0] = 1 material_vector[material+1] = 1 a = np.zeros(self.materials.shape[1], dtype=np.float32) b = material_vector m = np.vstack((a, b)) return VoxelModel(new_voxels, m, self.coords, self.resolution) def setMaterialVector(self, material_vector): # material input is the desired material vector """ Set the material of all voxels in a model. ---- Example: ``material_vector = np.zeros(len(materials) + 1)`` ``material_vector[0] = 1 # Set a to 1`` ``material_vector[3] = 0.3 # Set material 3 to 30%`` ``material_vector[4] = 0.7 # Set material 4 to 70%`` ``model2 = model1.setMaterialVector(material_vector)`` ---- Args: material_vector: Material mixture vector, format: (a, m0, m1, ... mn) Returns: VoxelModel """ new_voxels = self.getOccupied().voxels # Converts input model to a mask, no effect if input is already a mask a = np.zeros(len(material_vector), dtype=np.float32) b = material_vector materials = np.vstack((a, b)) return VoxelModel(new_voxels, materials, self.coords, self.resolution) # Boolean operations ############################## # Material from base model takes priority def union(self, model_to_add): """ Find the geometric union of two models. The materials from self will take priority in overlapping areas of the resulting model. This operation can also be applied using the OR operator (|) ---- Examples: ``model3 = model1.union(model2)`` ``model3 = model1 | model2`` ---- Args: model_to_add: VoxelModel to union with self Returns: VoxelModel """ checkResolution(self, model_to_add) materials = np.vstack((self.materials, model_to_add.materials[1:])) a, b, new_coords = alignDims(self, model_to_add) i_offset = len(self.materials) - 1 b = b + i_offset b[b == i_offset] = 0 # Paper uses a symmetric difference operation combined with the left/right intersection # A condensed version of this operation is used here for code simplicity mask = np.array(a == 0, dtype=np.bool_) new_voxels = np.multiply(b, mask) new_voxels = new_voxels + a # material from left model takes priority return VoxelModel(new_voxels, materials, new_coords, self.resolution) def __or__(self, other): """ Find the geometric union of two models. Overload OR operator (|) for VoxelModel objects with union(). Args: other: VoxelModel to union with self Returns: VoxelModel """ return self.union(other) def difference(self, model_to_sub): """ Find the geometric difference of two models. ---- Example: ``model3 = model1.difference(model2)`` ---- Args: model_to_sub: VoxelModel to subtract from self Returns: VoxelModel """ checkResolution(self, model_to_sub) a, b, new_coords = alignDims(self, model_to_sub) mask = np.array(b == 0, dtype=np.bool_) new_voxels = np.multiply(a, mask) return VoxelModel(new_voxels, self.materials, new_coords, self.resolution) def intersection(self, model_2): """ Find the geometric intersection of two models. The materials from self will be used in the resulting model. This operation can also be applied using the AND operator (&) ---- Examples: ``model3 = model1.intersection(model2)`` ``model3 = model1 & model2`` ---- Args: model_2: VoxelModel to intersect with self Returns: VoxelModel """ checkResolution(self, model_2) a, b, new_coords = alignDims(self, model_2) mask = np.logical_and(np.array(a != 0, dtype=np.bool_), np.array(b != 0, dtype=np.bool_)) # Paper provides for left/right intersection # For code simplicity, only a left intersection is provided here new_voxels = np.multiply(a, mask) # material from left model takes priority materials = self.materials return VoxelModel(new_voxels, materials, new_coords, self.resolution) def __and__(self, other): """ Find the geometric intersection of two models. Overload AND operator (&) for VoxelModel objects with intersection(). Args: other: VoxelModel to intersect with self Returns: VoxelModel """ return self.intersection(other) def xor(self, model_2): """ Find the geometric exclusive or of two models. This operation can also be applied using the XOR operator (^) ---- Examples: ``model3 = model1.xor(model2)`` ``model3 = model1 ^ model2`` ---- Args: model_2: VoxelModel to xor with self Returns: VoxelModel """ checkResolution(self, model_2) materials = np.vstack((self.materials, model_2.materials[1:])) a, b, new_coords = alignDims(self, model_2) i_offset = len(self.materials) - 1 b = b + i_offset b[b == i_offset] = 0 mask1 = np.array(b == 0, dtype=np.bool_) mask2 = np.array(a == 0, dtype=np.bool_) new_voxels = np.multiply(a, mask1) + np.multiply(b, mask2) return VoxelModel(new_voxels, materials, new_coords, self.resolution) def __xor__(self, other): """ Find the geometric exclusive or of two models. Overload XOR operator (^) for VoxelModel objects with xor(). Args: other: VoxelModel to xor with self Returns: VoxelModel """ return self.xor(other) # Material is computed def add(self, model_to_add): """ Find the material-wise addition of two models. The materials of the result are calculated by adding the material vectors for each voxel together. Example -- Adding a voxel containing material 1 and a voxel containing material 3: >> Voxel A = [1, 0, 1, 0, 0]\n >> Voxel B = [1, 0, 0, 0, 1]\n >> A + B = [1, 0, 1, 0, 1]\n >> Scale Result (see Cleanup Operations) → [1, 0, 0.5, 0, 0.5]\n This operation can also be applied using the addition operator (+). ---- Examples: ``model3 = model1.add(model2)`` ``model3 = model1 + model2`` ---- Args: model_to_add: VoxelModel to add to self Returns: VoxelModel """ checkResolution(self, model_to_add) a, b, new_coords = alignDims(self, model_to_add) x_len = a.shape[0] y_len = a.shape[1] z_len = a.shape[2] new_voxels = np.zeros_like(a, dtype=np.uint16) new_materials = np.zeros((1, len(self.materials[0])), dtype=np.float32) for x in range(x_len): for y in range(y_len): for z in range(z_len): i1 = int(a[x, y, z]) i2 = int(b[x, y, z]) m1 = self.materials[i1] m2 = model_to_add.materials[i2] m = m1 + m2 m[0] = np.logical_or(m1[0], m2[0]) i = np.where(np.equal(new_materials, m).all(1))[0] if len(i) > 0: new_voxels[x, y, z] = i[0] else: new_materials = np.vstack((new_materials, m)) new_voxels[x, y, z] = len(new_materials) - 1 return VoxelModel(new_voxels, new_materials, new_coords, self.resolution) def __add__(self, other): """ Find the material-wise addition of two models. Overload addition operator (+) for VoxelModel objects with add(). Args: other: VoxelModel to add to self Returns: VoxelModel """ return self.add(other) # Material is computed def subtract(self, model_to_sub): """ Find the material-wise difference of two models. The materials of the result are calculated for each voxel by subtracting the second material vector from the first. Example -- Subtracting a voxel containing material 3 from the result of the addition example: >> Voxel A = [1, 0, 0.5, 0, 0.5]\n >> Voxel B = [1, 0, 0, 0, 1]\n >> A - B = [1, 0, 0.5, 0, -0.5]\n >> Remove negatives (see Cleanup Operations) → [1, 0, 0.5, 0, 0]\n This operation can also be applied using the subtraction operator (-). ---- Examples: ``model3 = model1.subtract(model2)`` ``model3 = model1 - model2`` ---- Args: model_to_sub: VoxelModel to subtract from self Returns: VoxelModel """ checkResolution(self, model_to_sub) a, b, new_coords = alignDims(self, model_to_sub) x_len = a.shape[0] y_len = a.shape[1] z_len = a.shape[2] new_voxels = np.zeros_like(a, dtype=np.uint16) new_materials = np.zeros((1, len(self.materials[0])), dtype=np.float32) for x in range(x_len): for y in range(y_len): for z in range(z_len): i1 = int(a[x, y, z]) i2 = int(b[x, y, z]) m1 = self.materials[i1] m2 = model_to_sub.materials[i2] m = m1 - m2 m[0] = np.logical_or(m1[0], m2[0]) i = np.where(np.equal(new_materials, m).all(1))[0] if len(i) > 0: new_voxels[x, y, z] = i[0] else: new_materials = np.vstack((new_materials, m)) new_voxels[x, y, z] = len(new_materials) - 1 return VoxelModel(new_voxels, new_materials, new_coords, self.resolution) def __sub__(self, other): """ Find the material-wise difference of two models. Overload subtraction operator (-) for VoxelModel objects with subtract(). Args: other: VoxelModel to subtract from self Returns: VoxelModel """ return self.subtract(other) def multiply(self, other): """ Find the material-wise multiplication of two models. The materials of the result are calculated by multiplying the material vectors for each voxel. This function also supports multiplication by a scalar. This operation can also be applied using the multiplication operator (*). ---- Examples: ``model3 = model1.multiply(model2)`` ``model3 = model1 * model2`` ``model5 = model4 * 3`` ---- Args: other: VoxelModel to multiply with self Returns: VoxelModel """ if type(other) is VoxelModel: checkResolution(self, other) a, b, new_coords = alignDims(self, other) x_len = a.shape[0] y_len = a.shape[1] z_len = a.shape[2] new_voxels = np.zeros_like(a, dtype=np.uint16) new_materials = np.zeros((1, len(self.materials[0])), dtype=np.float32) for x in range(x_len): for y in range(y_len): for z in range(z_len): i1 = int(a[x, y, z]) i2 = int(b[x, y, z]) m1 = self.materials[i1] m2 = other.materials[i2] m = np.multiply(m1, m2) m[0] = np.logical_and(m1[0], m2[0]) i = np.where(np.equal(new_materials, m).all(1))[0] if len(i) > 0: new_voxels[x, y, z] = i[0] else: new_materials = np.vstack((new_materials, m)) new_voxels[x, y, z] = len(new_materials) - 1 return VoxelModel(new_voxels, new_materials, new_coords, self.resolution) else: new_model = VoxelModel.copy(self) new_model.materials[1:, 1:] = np.multiply(new_model.materials[1:, 1:], other) return new_model def __mul__(self, other): """ Find the material-wise multiplication of two models. Overload multiplication operator (*) for VoxelModel objects with multiply(). Args: other: VoxelModel to multiply with self Returns: VoxelModel """ return self.multiply(other) def divide(self, other): """ Find the material-wise division of two models. The materials of the result are calculated for each voxel by dividing the first material vector by the second. This function also supports division by a scalar. This operation can also be applied using the division operator (/). ---- Examples: ``model3 = model1.divide(model2)`` ``model3 = model1 / model2`` ``model5 = model4 / 3`` ---- Args: other: VoxelModel to divide self by Returns: VoxelModel """ if type(other) is VoxelModel: checkResolution(self, other) a, b, new_coords = alignDims(self, other) x_len = a.shape[0] y_len = a.shape[1] z_len = a.shape[2] new_voxels = np.zeros_like(a, dtype=np.uint16) new_materials = np.zeros((1, len(self.materials[0])), dtype=np.float32) for x in range(x_len): for y in range(y_len): for z in range(z_len): i1 = int(a[x, y, z]) i2 = int(b[x, y, z]) m1 = self.materials[i1] m2 = other.materials[i2] m2[m2 == 0] = 1 m = np.divide(m1, m2) m[0] = m1[0] i = np.where(np.equal(new_materials, m).all(1))[0] if len(i) > 0: new_voxels[x, y, z] = i[0] else: new_materials = np.vstack((new_materials, m)) new_voxels[x, y, z] = len(new_materials) - 1 return VoxelModel(new_voxels, new_materials, new_coords, self.resolution) else: if other == 0: return self new_model = VoxelModel.copy(self) new_model.materials[1:, 1:] = np.divide(new_model.materials[1:, 1:], other) return new_model def __truediv__(self, other): """ Find the material-wise division of two models. Overload division operator (/) for VoxelModel objects with divide(). Args: other: VoxelModel to divide self by Returns: VoxelModel """ return self.divide(other) # Morphology Operations ############################## def dilate(self, radius: int = 1, plane: Axes = Axes.XYZ, struct_type: Struct = Struct.STANDARD, connectivity: int = 3): # TODO: Preserve overlapping materials? """ Dilate a model along the specified axes. ---- Examples: ``model2 = model1.dilate(3)`` ``model4 = model3.dilate(1, Axes.XY, Struct.SPHERE, 2)`` ---- Args: radius: Dilation radius in voxels plane: Dilation directions, set using Axes class struct_type: Shape of structuring element, set using Struct class connectivity: Connectivity of structuring element (1-3) Returns: VoxelModel """ if radius == 0: return VoxelModel.copy(self) x_len = self.voxels.shape[0] + (radius * 2) y_len = self.voxels.shape[1] + (radius * 2) z_len = self.voxels.shape[2] + (radius * 2) new_voxels = np.zeros((x_len, y_len, z_len), dtype=np.uint16) new_voxels[radius:-radius, radius:-radius, radius:-radius] = self.voxels if struct_type == Struct.SPHERE: struct = structSphere(radius, plane) new_voxels = ndimage.grey_dilation(new_voxels, footprint=struct) else: # Struct.STANDARD struct = structStandard(connectivity, plane) for i in range(radius): new_voxels = ndimage.grey_dilation(new_voxels, footprint=struct) return VoxelModel(new_voxels, self.materials, (self.coords[0] - radius, self.coords[1] - radius, self.coords[2] - radius), self.resolution) def __lshift__(self, radius): """ Dilate a model in all three axes. Overload left shift operator (<<) for VoxelModel objects with dilate(). Uses: - plane = Axes.XYZ - struct_type = Struct.STANDARD - connectivity = 3 Args: radius: Dilation radius in voxels Returns: VoxelModel """ return self.dilate(radius) def dilateBounded(self, radius: int = 1, plane: Axes = Axes.XYZ, structType: Struct = Struct.STANDARD, connectivity: int = 3): """ Dilate a model along the specified axes without increasing the size of its bounding box. Args: radius: Dilation radius in voxels plane: Dilation directions, set using Axes class structType: Shape of structuring element, set using Struct class connectivity: Connectivity of structuring element (1-3) Returns: VoxelModel """ if radius == 0: return VoxelModel.copy(self) new_voxels = np.copy(self.fitWorkspace().voxels) if structType == Struct.SPHERE: struct = structSphere(radius, plane) new_voxels = ndimage.grey_dilation(new_voxels, footprint=struct) else: # Struct.STANDARD struct = structStandard(connectivity, plane) for i in range(radius): new_voxels = ndimage.grey_dilation(new_voxels, footprint=struct) return VoxelModel(new_voxels, self.materials, self.coords, self.resolution) def erode(self, radius: int = 1, plane: Axes = Axes.XYZ, struct_type: Struct = Struct.STANDARD, connectivity: int = 3): """ Erode a model along the specified axes. ---- Examples: ``model2 = model1.erode(5, connectivity=2)`` ``model4 = model3.erode(2, Axes.X, Struct.SPHERE, 1)`` ---- Args: radius: Erosion radius in voxels plane: Erosion directions, set using Axes class struct_type: Shape of structuring element, set using Struct class connectivity: Connectivity of structuring element (1-3) Returns: VoxelModel """ if radius == 0: return VoxelModel.copy(self) new_voxels = np.copy(self.voxels) mask = np.array(new_voxels != 0, dtype=np.bool_) if struct_type == Struct.SPHERE: struct = structSphere(radius, plane) mask = ndimage.binary_erosion(mask, structure=struct) else: # Struct.STANDARD struct = structStandard(connectivity, plane) mask = ndimage.binary_erosion(mask, structure=struct, iterations=radius) new_voxels = np.multiply(new_voxels, mask) return VoxelModel(new_voxels, self.materials, self.coords, self.resolution) def __rshift__(self, radius): """ Erode a model in all three axes. Overload right shift operator (>>) for VoxelModel objects with erode(). Uses: - plane = Axes.XYZ - struct_type = Struct.STANDARD - connectivity = 3 Args: radius: Dilation radius in voxels Returns: VoxelModel """ return self.erode(radius) def closing(self, radius: int = 1, plane: Axes = Axes.XYZ, structType: Struct = Struct.STANDARD, connectivity: int = 3): """ Apply a closing algorithm along the specified axes. This algorithm consists of dilation followed by erosion and will remove small holes. Depending on the structuring element used, this will apply a chamfer or fillet effect to inside corners. Args: radius: Radius for dilation/erosion in voxels plane: Dilation/erosion directions, set using Axes class structType: Shape of structuring element, set using Struct class connectivity: connectivity of structuring element (1-3) Returns: VoxelModel """ if radius == 0: return VoxelModel.copy(self) else: return self.dilate(radius, plane, structType, connectivity).erode(radius, plane, structType, connectivity).fitWorkspace() def opening(self, radius: int = 1, plane: Axes = Axes.XYZ, structType: Struct = Struct.STANDARD, connectivity: int = 3): """ Apply an opening algorithm along the specified axes. This algorithm consists of erosion followed by dilation and will remove small features. Depending on the structuring element used, this will apply a chamfer or fillet effect to outside corners. Args: radius: Radius for dilation/erosion in voxels plane: Dilation/erosion directions, set using Axes class structType: Shape of structuring element, set using Struct class connectivity: Connectivity of structuring element (1-3) Returns: VoxelModel """ if radius == 0: return VoxelModel.copy(self) new_voxels = np.copy(self.voxels) mask = np.array(new_voxels != 0, dtype=np.bool_) if structType == Struct.SPHERE: struct = structSphere(radius, plane) mask = ndimage.binary_opening(mask, structure=struct) else: # Struct.STANDARD struct = structStandard(connectivity, plane) mask = ndimage.binary_opening(mask, structure=struct, iterations=radius) new_voxels = np.multiply(new_voxels, mask) return VoxelModel(new_voxels, self.materials, self.coords, self.resolution) # Material Interface Modification ############################## def blur(self, radius: float = 1): """ Apply a Gaussian blur with the defined radius to the entire model. The blur radius corresponds to two times the standard deviation (2 * sigma) of the Gaussian distribution. The blurred effect is limited to voxels that were occupied by material in the input model. ---- Example: ``model2 = model1.blur(2)`` ___ Args: radius: Blur radius in voxels Returns: VoxelModel """ if radius == 0: return VoxelModel.copy(self) full_model = toFullMaterials(self.voxels, self.materials, len(self.materials[0])) for m in tqdm(range(len(self.materials[0])-1), desc='Blur - applying gaussian filter'): full_model[:, :, :, m+1] = ndimage.gaussian_filter(full_model[:, :, :, m+1], sigma=radius/2) mask = full_model[:, :, :, 0] mask = np.repeat(mask[..., None], len(self.materials[0]), axis=3) full_model = np.multiply(full_model, mask) return toIndexedMaterials(full_model, self, self.resolution) def blurRegion(self, radius: float, region): """ Apply a Gaussian blur with the defined radius to voxels that intersect with the region model. The blur radius corresponds to two times the standard deviation (2 * sigma) of the Gaussian distribution. The blurred effect is limited to voxels that were occupied by material in the intersection result and the material of the region model is ignored. ---- Example: ``model2 = model1.blurRegion(3, regionModel)`` ___ Args: radius: Blur radius in voxels region: VoxelModel defining the target blur region Returns: VoxelModel """ new_model = self.blur(radius) new_model = new_model.intersection(region) new_model = new_model.union(self) return new_model def dither(self, use_full=True, x_error=0.0, y_error=0.0, z_error=0.0, error_spread_threshold=0.8, blur=False, radius=1): """ Apply material-wise dithering to a model. Applying dithering will modify the model so that each voxel contains material in only a single material channel. Regions of the model which contained mixtures of materials will be converted to distributions of adjacent single material voxels. Args: use_full: Enabling will use a Stucki error diffusion filter. Disabling will use the provided values for x, y, and z error x_error: Percentage of error to spread in X y_error: Percentage of error to spread in Y z_error: Percentage of error to spread in Z error_spread_threshold: If a voxel contains a material channel that accounts for more than this percentage of the voxel, no additional error will be spread to it blur: Enable/disable applying a blur operation before the dither operation radius: Radius value for the optional blur operation Returns: VoxelModel """ if blur and (radius > 0): new_model = self.blur(radius) new_model = new_model.scaleValues() else: new_model = self.scaleValues() new_model.voxels = new_model.voxels.astype(dtype=np.uint16) full_model = toFullMaterials(new_model.voxels, new_model.materials, len(material_properties) + 1) full_model = ditherOptimized(full_model, use_full, x_error, y_error, z_error, error_spread_threshold) return toIndexedMaterials(full_model, self, self.resolution) # Cleanup ############################## def removeNegatives(self): """ Remove negative material values from a model (these have no physical meaning). ---- Example: ``model2 = model1.removeNegatives()`` ___ Returns: VoxelModel """ new_model = VoxelModel.copy(self) new_model.materials[new_model.materials < 1e-10] = 0 material_sums = np.sum(new_model.materials[:,1:], 1) # This and following update the a values material_sums[material_sums > 0] = 1 new_model.materials[:, 0] = material_sums return new_model def scaleValues(self): """ Scale nonzero material values to make all voxels contain 100% material while maintaining the ratio between materials. ---- Example: ``model2 = model1.scaleValues()`` ___ Returns: VoxelModel """ new_model = self.removeNegatives() material_sums = np.sum(new_model.materials[:, 1:], 1) material_sums[material_sums == 0] = 1 material_sums = np.repeat(material_sums[..., None], len(self.materials[0])-1, axis=1) new_model.materials[:,1:] = np.divide(new_model.materials[:,1:], material_sums) return new_model def scaleNull(self): """ Scale null material values to make all voxels contain 100% material. Voxels that contained less than 100% material will contain the same material percentages as before, but will have varying density. Voxels that contained greater than 100% material will be scaled using scaleValues(). ---- Example: ``model2 = model1.scaleNull()`` ___ Returns: VoxelModel """ new_model = self.removeNegatives() material_sums = np.sum(new_model.materials[:, 1:], 1) material_sums = np.ones(np.shape(material_sums)) - material_sums material_sums[material_sums < 0] = 0 new_model.materials[:,1] = np.multiply(material_sums, new_model.materials[:,0]) new_model = new_model.scaleValues() return new_model def round(self, toNearest: float = 0.1): """ Round material percentages to nearest multiple of an input value. Args: toNearest: Value to round to Returns: VoxelModel """ new_materials = np.copy(self.materials) new_model = VoxelModel.copy(self) mult = new_materials / toNearest floorDiff = np.round(abs(mult - np.floor(mult)), 10) ceilDiff = np.round(abs(mult - np.ceil(mult)), 10) new_materials[floorDiff < ceilDiff] = toNearest * np.floor(mult[floorDiff < ceilDiff]) new_materials[floorDiff >= ceilDiff] = toNearest * np.ceil(mult[floorDiff >= ceilDiff]) new_model.materials = new_materials return new_model def clearNull(self): """ Set all null material percentages to zero. Returns: VoxelModel """ new_model = VoxelModel.copy(self) new_model.materials[1:, 1] = np.zeros(np.shape(new_model.materials[1:,1])) return new_model def setDensity(self, density: float = 1.0): """ Set the density of all voxels. Args: density: Target density value Returns: VoxelModel """ new_model = self.clearNull() new_model = new_model.scaleValues() null_material_values = np.multiply(np.ones(np.shape(new_model.materials[1:,1])), 1-density) new_model.materials[1:, 1] = null_material_values new_model.materials[1:, 2:] = np.multiply(new_model.materials[1:, 2:], density) return new_model # Transformations ############################## def translate(self, vector: Tuple[int, int, int]): """ Translate a model by the specified vector. Args: vector: Translation vector in voxels Returns: VoxelModel """ new_model = VoxelModel.copy(self) new_model.coords = (self.coords[0]+vector[0], self.coords[1]+vector[1], self.coords[2]+vector[2]) return new_model def translateMM(self, vector: Tuple[float, float, float]): """ Translate a model by the specified vector. Args: vector: Translation vector in mm Returns: VoxelModel """ xV = int(round(vector[0] * self.resolution)) yV = int(round(vector[1] * self.resolution)) zV = int(round(vector[2] * self.resolution)) new_model = self.translate((xV, yV, zV)) return new_model def rotate(self, angle: float, axis: Axes = Axes.Z): """ Rotate a model about its center. Floating point errors may slightly affect the angle of the resulting model. To rotate a model in precise 90 degree increments, use rotate90(). Args: angle: Angle of rotation in degrees axis: Axis of rotation, set using Axes class Returns: VoxelModel """ if axis == Axes.X: plane = (1, 2) sign = 1 elif axis == Axes.Y: plane = (2, 0) sign = -1 # For some reason, Y rotates the opposite direction than expected else: # axis == Axes.Z plane = (0, 1) sign = 1 centerCoords = self.getCenter() new_voxels = ndimage.rotate(self.voxels, sign*angle, plane, order=0) new_model = VoxelModel(new_voxels, self.materials, self.coords, self.resolution) new_model = new_model.setCenter(centerCoords) return new_model def rotate90(self, times: int = 1, axis: Axes = Axes.Z): """ Rotate a model about its center in increments of 90 degrees. Args: times: Number of 90 degree increments to rotate model axis: Axis of rotation, set using Axes class Returns: VoxelModel """ if axis == Axes.X or axis == 0: plane = (1, 2) elif axis == Axes.Y or axis == 1: plane = (0, 2) else: # axis == Axes.Z or axis = 2 plane = (0, 1) centerCoords = self.getCenter() new_voxels = np.rot90(self.voxels, times, axes=plane) new_model = VoxelModel(new_voxels, self.materials, self.coords, self.resolution) new_model = new_model.setCenter(centerCoords) return new_model def mirror(self, axes: Axes = Axes.X): """ Mirror a model along the given axes. This operation will mirror ALONG the given axes. For example: - Axes.X performs a mirror about the YZ plane - Axes.XY performs a mirror about the YZ plane and the XZ plane - Axes.XYZ performs a mirror along all three axes Args: axes: Axes for mirror operation, set using Axes class Returns: VoxelModel """ flip_axis = [] for i in range(len(axes.value)): if axes.value[i]: flip_axis.append(i) flip_axis = tuple(flip_axis) centerCoords = self.getCenter() new_voxels = np.flip(self.voxels, flip_axis) new_model = VoxelModel(new_voxels, self.materials, self.coords, self.resolution) new_model = new_model.setCenter(centerCoords) return new_model def scale(self, factor: float, adjustResolution: bool = True): """ Scale a model. If adjustResolution is enabled, the resolution attribute of the model will also be multiplied by the scaling factor. Enable adjustResolution if using this operation to change the resolution of a model. Disable adjustResolution if using this operation to change the size of a model. Args: factor: Scale factor adjustResolution: Enable/disable automatic resolution adjustment Returns: VoxelModel """ model = self.fitWorkspace() x_len = int(model.voxels.shape[0] * factor) y_len = int(model.voxels.shape[1] * factor) z_len = int(model.voxels.shape[2] * factor) new_voxels = np.zeros((x_len, y_len, z_len)) for x in tqdm(range(x_len), desc='Scaling'): for y in range(y_len): for z in range(z_len): x_source = int(((x+1) / x_len) * (model.voxels.shape[0]-1)) y_source = int(((y+1) / y_len) * (model.voxels.shape[1]-1)) z_source = int(((z+1) / z_len) * (model.voxels.shape[2]-1)) new_voxels[x,y,z] = model.voxels[x_source, y_source, z_source] model.voxels = new_voxels.astype(dtype=np.uint16) model = model.setCoords(model.coords) if adjustResolution: model.resolution = model.resolution * factor return model def scaleToSize(self, size: Tuple[int, int, int]): """ Scale a model to fit the given dimensions. Args: size: Target dimensions in voxels Returns: VoxelModel """ model = self.fitWorkspace() new_voxels = np.zeros(size) for x in tqdm(range(size[0]), desc='Scaling'): for y in range(size[1]): for z in range(size[2]): x_source = int(((x+1) / size[0]) * (model.voxels.shape[0]-1)) y_source = int(((y+1) / size[1]) * (model.voxels.shape[1]-1)) z_source = int(((z+1) / size[2]) * (model.voxels.shape[2]-1)) new_voxels[x,y,z] = model.voxels[x_source, y_source, z_source] model.voxels = new_voxels.astype(dtype=np.uint16) new_model = model.setCoords(model.coords) return new_model def getCenter(self): """ Find the coordinates of the center of a model. Returns: Center coordinates in voxels """ model = self.fitWorkspace() x_center = (model.voxels.shape[0] / 2) + model.coords[0] y_center = (model.voxels.shape[1] / 2) + model.coords[1] z_center = (model.voxels.shape[2] / 2) + model.coords[2] centerCoords = (x_center, y_center, z_center) return centerCoords def setCenter(self, coords: Tuple[float, float, float]): """ Set the center of a model to the specified coordinates. Args: coords: Target coordinates in voxels Returns: VoxelModel """ new_model = self.fitWorkspace() x_new = int(round(coords[0] - (new_model.voxels.shape[0] / 2))) y_new = int(round(coords[1] - (new_model.voxels.shape[1] / 2))) z_new = int(round(coords[2] - (new_model.voxels.shape[2] / 2))) new_model.coords = (x_new, y_new, z_new) return new_model def getCoords(self): """ Get the origin coordinates of a model. Returns: Origin coordinates in voxels """ model = self.fitWorkspace() return model.coords def setCoords(self, coords: Tuple[int, int, int]): """ Set the origin of a model to the specified coordinates. Args: coords: Target coordinates in voxels Returns: VoxelModel """ new_model = self.fitWorkspace() new_model.coords = coords return new_model def getMaxCoords(self): """ Get the maximum coordinate location in a model. This point is equal to origin coordinates + model dimensions. Returns: Maximum coordinates in voxels """ model = self.fitWorkspace() x = model.coords[0] + model.voxels.shape[0] y = model.coords[1] + model.voxels.shape[1] z = model.coords[2] + model.voxels.shape[2] return x, y, z def getDim(self): """ Get the dimensions of model. Returns: Model dimensions in voxels """ model = self.fitWorkspace() x = model.voxels.shape[0] y = model.voxels.shape[1] z = model.voxels.shape[2] return x, y, z def isOccupied(self, coords: Tuple[int, int, int]): """ Determine if a specific voxel is occupied. Returns: True/False """ x = coords[0] - self.coords[0] y = coords[1] - self.coords[1] z = coords[2] - self.coords[2] if x < 0 or x >= self.voxels.shape[0]: return False if y < 0 or y >= self.voxels.shape[1]: return False if z < 0 or z >= self.voxels.shape[2]: return False v = self.voxels[x, y, z] if v == 0: return False else: return True # Model Info ############################## def getVoxelDim(self): """ Get the side dimension of a voxel in mm. Returns: Float """ res = self.resolution return (1.0/res) * 0.001 def getVolume(self, component: int = 0, material: int = 0): """ Get the volume of a model or model component. Args: component: Component label to measure, set to 0 for all components material: Material index to measure, set to 0 for all materials Returns: Volume in voxels, volume in mm^3 """ new_model = VoxelModel.copy(self) if component > 0: new_model = new_model.isolateComponent(component) if material > 0: new_model = new_model.isolateMaterial(material) volumeVoxels = np.count_nonzero(new_model.voxels) volumeMM3 = volumeVoxels * ((1/self.resolution)**3) return volumeVoxels, volumeMM3 def getMaterialProperties(self, material): """ Get the average material properties of a row in a model's material array. Args: material: Material index Returns: Dictionary of material properties """ avg_properties = {} for key in material_properties[0]: if key == 'name' or key == 'process': string = '' for i in range(len(self.materials[0])-1): if self.materials[material][i + 1] > 0: current_material_data = getMaterialData(i) string = string + current_material_data[key] + ' ' avg_properties.update({key: string}) elif key == 'MM' or key == 'MMD' or key == 'FM' or key == 'HG' or key == 'HGM': material_id = self.materials[material][1:].argmax() current_material_data = getMaterialData(material_id) var = current_material_data[key] avg_properties.update({key: var}) else: var = 0 for i in range(len(self.materials[0])-1): current_material_data = getMaterialData(i) var = var + self.materials[material][i + 1] * current_material_data[key] avg_properties.update({key: var}) return avg_properties def getSSData(self, material): """ Get the stress-strain data for a row in a model's material array. This is currently returned based on the material present in the highest percentage. TODO: Make this average multiple stress-strain curves Args: material: Material index Returns: Dictionary of stress-strain data """ material_id = self.materials[material][1:].argmax() current_material_data = getMaterialData(material_id) try: ss_data_index = current_material_data['MMD'] current_ss_data = next((item for item in ss_data if item['id'] == ss_data_index), None) if current_ss_data is None: raise KeyError except KeyError: print('Stress-strain data not available for ' + current_material_data['name']) current_ss_data = None return current_ss_data def getHGModel(self, material): """ Get the hydrogel model parameters for a row in a model's material array. This is currently returned based on the material present in the highest percentage. TODO: Make this average multiple model parameters Args: material: Material index Returns: Dictionary of hydrogel model parameters """ material_id = self.materials[material][1:].argmax() current_material_data = getMaterialData(material_id) try: hg_model_index = current_material_data['HGM'] current_hg_model = next((item for item in hg_models if item['id'] == hg_model_index), None) if current_hg_model is None: raise KeyError except KeyError: print('Hydrogel model data not available for ' + current_material_data['name']) current_hg_model = None return current_hg_model def getVoxelProperties(self, coords: Tuple[int, int, int]): """ Get the average material properties of a specific voxel. Args: coords: Voxel coordinates Returns: Dictionary of material properties """ return self.getMaterialProperties(self.voxels[coords[0], coords[1], coords[2]]) # Manufacturing Features ############################## def projection(self, direction: Dir, material: int = 1): """ Generate a model representing all voxels within the workspace that contain material or that lie in the specified direction with respect to a voxel that contains material. --- Example: ``modelResult = model1.projection(Dir.DOWN)`` --- Args: direction: Projection direction, set using Dir class material: Material index corresponding to materials.py Returns: VoxelModel """ new_voxels = np.zeros_like(self.voxels) x_len = self.voxels.shape[0] y_len = self.voxels.shape[1] z_len = self.voxels.shape[2] if direction == Dir.BOTH: # Loop through model data for x in range(x_len): for y in range(y_len): if np.sum(self.voxels[x, y, :]) > 0: new_voxels[x, y, :].fill(1) elif direction == Dir.DOWN: # Loop through model data for x in range(x_len): for y in range(y_len): for z in range(z_len): if np.sum(self.voxels[x, y, z:]) > 0: new_voxels[x, y, z] = 1 elif np.sum(self.voxels[x, y, z:]) == 0: break elif direction == Dir.UP: # Loop through model data for x in range(x_len): for y in range(y_len): for z in range(z_len): if np.sum(self.voxels[x, y, :z]) > 0: new_voxels[x, y, z] = 1 return VoxelModel(new_voxels, generateMaterials(material), self.coords, self.resolution) def keepout(self, method: Process, material: int = 1): """ Generate a model representing the keep-out region of a model. The keep-out region for a given process and part represents material which the process may not modify while creating the part. This feature primarily applies to subtractive processes. It includes material that will be present in the final part and regions of the workspace that cannot be accessed without affecting this material. In general, additive processes will have no keep-out region because they deposit material from the bottom up. ---- Example: ``modelResult = model1.keepout(Process.MILL)`` ---- Args: method: Target manufacturing method, set using Process class material: Material index corresponding to materials.py Returns: VoxelModel """ if method == Process.LASER: new_model = self.projection(Dir.BOTH, material) elif method == Process.MILL: new_model = self.projection(Dir.DOWN, material) elif method == Process.INSERT: new_model = self.projection(Dir.UP, material) else: new_model = self return new_model def clearance(self, method: Process, material: int = 1): """ Generate a model representing the clearance region of a model. The clearance region for a given process and part represents regions that will be affected by the process acting on the part. Clearance can be used to identify regions of a model that conflict with the manufacturing of another model. ---- Example: ``modelResult = model1.clearance(Process.PRINT)`` ---- Args: method: Target manufacturing method, set using Process class material: Material index corresponding to materials.py Returns: VoxelModel """ if method == Process.LASER: new_model = self.projection(Dir.BOTH, material).difference(self) elif method == Process.MILL: new_model = self.projection(Dir.BOTH, material).difference(self.projection(Dir.DOWN, material)) elif (method == Process.INSERT) or (method == Process.PRINT): new_model = self.projection(Dir.UP, material) else: new_model = self return new_model def support(self, method: Process, r1: int = 1, r2: int = 1, plane: Axes = Axes.XY, material: int = 1): """ Generate a model representing where support material may be added to an object as characterized by the process that is used to remove the supports. ---- Example: ``modelResult = model1.support(Process.LASER)`` ---- Args: method: Target support removal method, set using Process class r1: Parameter used to determine areas where support is ineffective based on proximity to empty regions that are inaccessible to the removal process r2: Desired thickness of the support material plane: Directions in which to add support material, set using Axes class material: Material index corresponding to materials.py Returns: VoxelModel """ model_A = self.keepout(method, material) model_A = model_A.dilate(r2, plane).difference(model_A) model_A = model_A.difference(self.keepout(method, material).difference(self).dilate(r1, plane)) # Valid support regions return model_A def userSupport(self, support_model, method: Process, r1: int = 1, r2: int = 1, plane: Axes = Axes.XY, material: int = -1): """ Generate a model representing the intersection of the supportable region and a user support model. ---- Example: ``modelResult = model1.userSupport(model2, Process.LASER)`` ---- Args: support_model: User provided support model method: Target support removal method, set using Process class r1: Parameter used to determine areas where support is ineffective based on proximity to empty regions that are inaccessible to the removal process r2: Desired thickness of the support material plane: Directions in which to add support material, set using Axes class material: Material index corresponding to materials.py, set to -1 to use support model material Returns: VoxelModel """ if material > -1: model_A = self.support(method, r1, r2, plane) model_A = support_model.intersection(model_A) else: model_A = self.support(method, r1, r2, plane, material) model_A = model_A.intersection(support_model) return model_A def web(self, method: Process, r1: int = 1, r2: int = 1, layer: int = -1, material = 1): """ Generate a model representing the scrap material surrounding a model. Web can be used in the creation of supports or layer alignment fixtures. ---- Example: ``modelResult = model1.web(Process.LASER, 1, 5)`` ---- Args: method: Target web removal method, set using Process class r1: Distance from surface of part to inside of web in r2: Width of web in voxels layer: Voxel layer at which to generate web, set to -1 to generate for all layers material: Material index corresponding to materials.py Returns: VoxelModel """ model_A = self.keepout(method, material) if layer != -1: model_A = model_A.isolateLayer(layer) model_A = model_A.dilate(r1, Axes.XY) model_A = model_A.dilate(r2, Axes.XY).getBoundingBox().difference(model_A) return model_A # File IO ############################## # Add model to a K3D plot in Jupyter Notebook def plot(self, plot=None, name: str = 'model', wireframe: bool = False, **kwargs): """ Add model to a K3D plot. Additional display options: - opacity: `float`. Opacity of voxels. - outlines: `bool`. Whether mesh should display with outlines. - outlines_color: `int`. Packed RGB color of the resulting outlines (0xff0000 is red, 0xff is blue) - kwargs: `dict`. Dictionary arguments to configure transform and model_matrix. More information available at: https://github.com/K3D-tools/K3D-jupyter Args: plot: Plot object to add model to name: Model name wireframe: Enable displaying model as a wireframe kwargs: Additional display options (see above) Returns: K3D plot object """ model = self.fitWorkspace() | VoxelModel.empty((1, 1, 1), resolution=self.resolution) model = model.removeDuplicateMaterials() # Get colors colors = [] for m in model.materials: r = 0 g = 0 b = 0 for i in range(1, len(m)): r = r + m[i] * material_properties[i - 1]['r'] g = g + m[i] * material_properties[i - 1]['g'] b = b + m[i] * material_properties[i - 1]['b'] r = 1 if r > 1 else r g = 1 if g > 1 else g b = 1 if b > 1 else b colors.append(rgb_to_hex(r, g, b)) colors = np.array(colors, dtype=np.uint32)[1:] # Plot if plot is None: plot = k3d.plot() plot += k3d.voxels(np.swapaxes(model.voxels, 0, 2).astype(np.uint8), color_map=colors, name=name, wireframe=wireframe, **kwargs) return plot def saveVF(self, filename: str): """ Save model data to a .vf file The native VoxelFuse file format stores the same information as the attributes of a VoxelModel object. This includes geometry and material mixture data. Material attributes remain stored in the materials.py file, so the same version of this file must be used when saving and opening models. The .vf file type can be reopened by a VoxelFuse script. ---- Example: ``modelResult.saveVF("test-file")`` ---- Args: filename: File name Returns: None """ f = open(filename+'.vf', 'w+') print('Saving file: ' + f.name) x_coord = self.coords[0] y_coord = self.coords[1] z_coord = self.coords[2] writeOpen(f, 'coords') f.write(str(x_coord) + ',' + str(y_coord) + ',' + str(z_coord) + ',\n') writeClos(f, 'coords') writeOpen(f, 'resolution') f.write(str(self.resolution) + '\n') writeClos(f, 'resolution') writeOpen(f, 'materials') for r in range(len(self.materials[:,0])): # tqdm(range(len(self.materials[:,0])), desc='Writing materials'): for c in range(len(self.materials[0,:])): f.write(str(self.materials[r,c]) + ',') f.write('\n') writeClos(f, 'materials') x_len = self.voxels.shape[0] y_len = self.voxels.shape[1] z_len = self.voxels.shape[2] writeOpen(f, 'size') f.write(str(x_len) + ',' + str(y_len) + ',' + str(z_len) + ',\n') writeClos(f, 'size') writeOpen(f, 'voxels') for x in range(x_len): # tqdm(range(x_len), desc='Writing voxels'): for z in range(z_len): for y in range(y_len): f.write(str(int(self.voxels[x,y,z])) + ',') f.write(';') f.write('\n') writeClos(f, 'voxels') writeOpen(f, 'components') f.write(str(self.numComponents) + '\n') writeClos(f, 'components') if self.numComponents > 0: writeOpen(f, 'labels') for x in range(x_len): # tqdm(range(x_len), desc='Writing components'): for z in range(z_len): for y in range(y_len): f.write(str(int(self.components[x,y,z])) + ',') f.write(';') f.write('\n') writeClos(f, 'labels') f.close() @classmethod def openVF(cls, filename: str): """ Load model data from a .vf file This method will create a new VoxelModel object using the data from the .vf file. Material attributes are stored in the materials.py file, so the same version of this file must be used when saving and opening models. ---- Example: ``model1 = vf.VoxelModel.openVF("test-file")`` ---- Args: filename: File name Returns: VoxelModel """ if filename[-3:] == '.vf': f = open(filename, 'r') else: f = open(filename + '.vf', 'r') print('Opening file: ' + f.name) data = f.readlines() loc = np.ones((7,2), dtype=np.uint16) loc = np.multiply(loc, -1) for i in range(len(data)): # tqdm(range(len(data)), desc='Finding tags'): if data[i][:-1] == '<coords>': loc[0,0] = i+1 if data[i][:-1] == '</coords>': loc[0,1] = i if data[i][:-1] == '<materials>': loc[1,0] = i+1 if data[i][:-1] == '</materials>': loc[1,1] = i if data[i][:-1] == '<size>': loc[2,0] = i+1 if data[i][:-1] == '</size>': loc[2,1] = i if data[i][:-1] == '<voxels>': loc[3,0] = i+1 if data[i][:-1] == '</voxels>': loc[3,1] = i if data[i][:-1] == '<components>': loc[4,0] = i+1 if data[i][:-1] == '</components>': loc[4,1] = i if data[i][:-1] == '<labels>': loc[5,0] = i+1 if data[i][:-1] == '</labels>': loc[5,1] = i if data[i][:-1] == '<resolution>': loc[6,0] = i+1 if data[i][:-1] == '</resolution>': loc[6,1] = i coords = np.array(data[loc[0,0]][:-2].split(","), dtype=np.int16) if loc[6,0] > -1: resolution = float(data[loc[6,0]][:-1]) else: resolution = 1 materials = np.array(data[loc[1,0]][:-2].split(","), dtype=np.float32) for i in range(loc[1,0]+1, loc[1,1]): # tqdm(range(loc[1,0]+1, loc[1,1]), desc='Reading materials'): materials = np.vstack((materials, np.array(data[i][:-2].split(","), dtype=np.float32))) size = tuple(np.array(data[loc[2,0]][:-2].split(","), dtype=np.uint16)) voxels = np.zeros(size, dtype=np.uint16) for i in range(loc[3,0], loc[3,1]): # tqdm(range(loc[3,0], loc[3,1]), desc='Reading voxels'): x = i - loc[3,0] yz = data[i][:-2].split(";") for z in range(len(yz)): y = np.array(yz[z][:-1].split(","), dtype=np.uint16) voxels[x, :, z] = y numComponents = int(data[loc[4,0]][:-1]) components = np.zeros(size, dtype=np.uint8) if numComponents > 0: for i in range(loc[5,0], loc[5,1]): # tqdm(range(loc[5,0], loc[5,1]), desc='Reading components'): x = i - loc[5, 0] yz = data[i][:-2].split(";") for z in range(len(yz)): y = np.array(yz[z][:-1].split(","), dtype=np.uint8) components[x, :, z] = y new_model = cls(voxels, materials, coords=tuple(coords), resolution=resolution) new_model.numComponents = numComponents new_model.components = components f.close() return new_model def saveVXC(self, filename: str, compression: bool = False): """ Save model data to a .vxc file The VoxCad file format stores geometry and full material palette data. The material palette includes the properties for each material and material mixtures are converted into distinct palette entries. This format supports compression for the voxel data. Enabling compression allows for larger models, but it may introduce geometry errors that particularly affect small models. The .vxc file type can be opened using VoxCad simulation software. However, it cannot currently be reopened by a VoxelFuse script. ---- Example: ``modelResult.saveVXC("test-file", compression=False)`` ---- Args: filename: File name compression: Enable/disable voxel data compression Returns: None """ f = open(filename + '.vxc', 'w+') print('Saving file: ' + f.name) empty_model = VoxelModel.empty((1,1,1), resolution=self.resolution) export_model = (VoxelModel.copy(self).fitWorkspace()) | empty_model # Fit workspace and union with an empty object at the origin to clear offsets if object is raised export_model.coords = (0, 0, 0) # Set coords to zero to move object to origin if it is at negative coordinates writeHeader(f, '1.0', 'ISO-8859-1') export_model.writeVXCData(f, compression) f.close() def writeVXCData(self, f: TextIO, compression: bool = False): """ Write geometry and material data to a text file using the .vxc format. The VXC/VXA format stores geometry as a 3D grid of single-digit decimal numbers. As such, it is limited to 9 distinct materials. Args: f: File to write to compression: Enable/disable voxel data compression Returns: None """ if len(self.materials[:, 0]) > 10: f.close() os.remove(f.name) raise ValueError('The VXC/VXA file format supports a maximum of 9 distinct materials') writeOpen(f, 'VXC Version="' + str(0.94) + '"', 0) # Lattice settings writeOpen(f, 'Lattice', 1) writeData(f, 'Lattice_Dim', (1 / self.resolution) * 0.001, 2) writeData(f, 'X_Dim_Adj', 1, 2) writeData(f, 'Y_Dim_Adj', 1, 2) writeData(f, 'Z_Dim_Adj', 1, 2) writeData(f, 'X_Line_Offset', 0, 2) writeData(f, 'Y_Line_Offset', 0, 2) writeData(f, 'X_Layer_Offset', 0, 2) writeData(f, 'Y_Layer_Offset', 0, 2) writeClos(f, 'Lattice', 1) # Voxel settings writeOpen(f, 'Voxel', 1) writeData(f, 'Vox_Name', 'BOX', 2) writeData(f, 'X_Squeeze', 1, 2) writeData(f, 'Y_Squeeze', 1, 2) writeData(f, 'Z_Squeeze', 1, 2) writeClos(f, 'Voxel', 1) # Materials writeOpen(f, 'Palette', 1) for row in range(1, len(self.materials[:, 0])): # tqdm(range(1, len(self.materials[:, 0])), desc='Writing materials'): avgProps = self.getMaterialProperties(row) writeOpen(f, 'Material ID="' + str(row) + '"', 2) writeData(f, 'MatType', 0, 3) writeData(f, 'Name', avgProps['name'][0:-1], 3) writeOpen(f, 'Display', 3) writeData(f, 'Red', avgProps['r'], 4) writeData(f, 'Green', avgProps['g'], 4) writeData(f, 'Blue', avgProps['b'], 4) writeData(f, 'Alpha', 1, 4) writeClos(f, 'Display', 3) writeOpen(f, 'Mechanical', 3) if int(avgProps['MM']) == 3: current_ss_data = self.getSSData(row) if current_ss_data is not None: writeData(f, 'MatModel', 3, 4) writeOpen(f, 'SSData', 4) writeData(f, 'NumDataPts', len(current_ss_data['strain']), 5) writeOpen(f, 'StrainData', 5) for point in current_ss_data['strain']: writeData(f, 'Strain', point, 6) writeClos(f, 'StrainData', 5) writeOpen(f, 'StressData', 5) for point in current_ss_data['stress']: writeData(f, 'Stress', point, 6) writeClos(f, 'StressData', 5) writeClos(f, 'SSData', 4) else: writeData(f, 'MatModel', 0, 4) else: writeData(f, 'MatModel', avgProps['MM'], 4) writeData(f, 'Elastic_Mod', avgProps['E'], 4) writeData(f, 'Plastic_Mod', avgProps['Z'], 4) writeData(f, 'Yield_Stress', avgProps['eY'], 4) writeData(f, 'FailModel', int(avgProps['FM']), 4) writeData(f, 'Fail_Stress', avgProps['eF'], 4) writeData(f, 'Fail_Strain', avgProps['SF'], 4) writeData(f, 'Density', avgProps['p'] * 1e3, 4) writeData(f, 'Poissons_Ratio', avgProps['v'], 4) writeData(f, 'CTE', avgProps['CTE'], 4) writeData(f, 'MaterialTempPhase', avgProps['TP'], 4) writeData(f, 'uStatic', avgProps['uS'], 4) writeData(f, 'uDynamic', avgProps['uD'], 4) if int(avgProps['HG']) == 1: current_hg_model = self.getHGModel(row) if current_hg_model is not None: writeData(f, 'HydrogelModel', 1, 4) writeClos(f, 'Mechanical', 3) writeOpen(f, 'Hydrogel', 3) writeData(f, 'Name', current_hg_model['name'], 4) writeData(f, 'VoxelDim', current_hg_model['test_voxel_dim'], 4) writeData(f, 'IdealDisplacement', current_hg_model['ideal_displacement'], 4) writeData(f, 'TestDisplacement', current_hg_model['test_displacement'], 4) writeData(f, 'TimeStepCorrection', current_hg_model['test_time_step'], 4) writeData(f, 'KpRising', current_hg_model['kp_rising'], 4) writeData(f, 'KpFalling', current_hg_model['kp_falling'], 4) writeData(f, 'MaxTemp', current_hg_model['ideal_max_temp'], 4) writeData(f, 'MinTemp', current_hg_model['ideal_min_temp'], 4) writeData(f, 'TestMax', current_hg_model['test_max_temp'], 4) writeData(f, 'TestMin', current_hg_model['test_min_temp'], 4) writeData(f, 'C0', current_hg_model['c0'], 4) writeData(f, 'C1', current_hg_model['c1'], 4) writeData(f, 'C2', current_hg_model['c2'], 4) writeData(f, 'C3', current_hg_model['c3'], 4) writeData(f, 'C4', current_hg_model['c4'], 4) writeData(f, 'C5', current_hg_model['c5'], 4) writeClos(f, 'Hydrogel', 3) else: writeData(f, 'HydrogelModel', 0, 4) writeClos(f, 'Mechanical', 3) else: writeData(f, 'HydrogelModel', 0, 4) writeClos(f, 'Mechanical', 3) writeClos(f, 'Material', 2) writeClos(f, 'Palette', 1) # Structure if compression: writeOpen(f, 'Structure Compression="ZLIB"', 1) else: writeOpen(f, 'Structure Compression="ASCII_READABLE"', 1) x_len = self.voxels.shape[0] y_len = self.voxels.shape[1] z_len = self.voxels.shape[2] writeData(f, 'X_Voxels', x_len, 2) writeData(f, 'Y_Voxels', y_len, 2) writeData(f, 'Z_Voxels', z_len, 2) writeOpen(f, 'Data', 2) for z in range(z_len): # tqdm(range(z_len), desc='Writing voxels'): layer = np.copy(self.voxels[:, :, z]) layer = layer.transpose() layerData = layer.flatten() layerData = layerData.astype('uint8') if compression: layerData = zlib.compress(layerData.tobytes()) layerData = base64.encodebytes(layerData) layerDataStr = str(layerData)[2:-3] else: layerDataStr = '' for vox in layerData: layerDataStr = layerDataStr + str(vox) writeData(f, 'Layer', '<![CDATA[' + layerDataStr + ']]>', 3) writeClos(f, 'Data', 2) writeClos(f, 'Structure', 1) writeClos(f, 'VXC', 0)
Static methods
def copy(voxel_model)
-
Initialize an VoxelModel that is a copy of another model.
Args
voxel_model
- Reference VoxelModel object
Returns
VoxelModel
Expand source code
@classmethod def copy(cls, voxel_model): """ Initialize an VoxelModel that is a copy of another model. Args: voxel_model: Reference VoxelModel object Returns: VoxelModel """ new_model = cls(voxel_model.voxels, voxel_model.materials, coords=voxel_model.coords, resolution=voxel_model.resolution) new_model.numComponents = voxel_model.numComponents new_model.components = voxel_model.components return new_model
def empty(size: Tuple[int, int, int], coords: Tuple[int, int, int] = (0, 0, 0), resolution: float = 1, num_materials: int = 15)
-
Initialize an empty VoxelModel.
Args
size
- Size of the empty model in voxels
coords
- Model origin coordinates
resolution
- Number of voxels per mm
num_materials
- Number of material types in materials vector
Returns
VoxelModel
Expand source code
@classmethod def empty(cls, size: Tuple[int, int, int], coords: Tuple[int, int, int] = (0, 0, 0), resolution: float = 1, num_materials: int = len(material_properties)): """ Initialize an empty VoxelModel. Args: size: Size of the empty model in voxels coords: Model origin coordinates resolution: Number of voxels per mm num_materials: Number of material types in materials vector Returns: VoxelModel """ model_data = np.zeros(size, dtype=np.uint16) materials = np.zeros((1, num_materials + 1), dtype=np.float32) new_model = cls(model_data, materials, coords=coords, resolution=resolution) return new_model
def emptyLike(voxel_model)
-
Initialize an empty VoxelModel with the same size, materials, coords, and resolution as another model.
Args
voxel_model
- Reference VoxelModel object
Returns
VoxelModel
Expand source code
@classmethod def emptyLike(cls, voxel_model): """ Initialize an empty VoxelModel with the same size, materials, coords, and resolution as another model. Args: voxel_model: Reference VoxelModel object Returns: VoxelModel """ new_model = cls(np.zeros_like(voxel_model.voxels, dtype=np.uint16), voxel_model.materials, coords=voxel_model.coords, resolution=voxel_model.resolution) return new_model
def fromMeshFile(filename: str, coords: Tuple[int, int, int] = (0, 0, 0), material: int = 1, resolution: float = 1, gmsh_on_path: bool = False)
-
Create a VoxelModel from an imported mesh file.
Example:
model1 = vf.VoxelModel.fromMeshFile('center.stl', (67, 3, 0), 2, 1)
Args
filename
- File name with extension
coords
- Model origin coordinates
material
- Material id corresponding to materials.py
resolution
- Number of voxels per mm
gmsh_on_path
- Enable/disable using system gmsh rather than bundled gmsh
Returns
VoxelModel
Expand source code
@classmethod def fromMeshFile(cls, filename: str, coords: Tuple[int, int, int] = (0, 0, 0), material: int = 1, resolution: float = 1, gmsh_on_path: bool = False): """ Create a VoxelModel from an imported mesh file. ---- Example: ``model1 = vf.VoxelModel.fromMeshFile('center.stl', (67, 3, 0), 2, 1)`` ____ Args: filename: File name with extension coords: Model origin coordinates material: Material id corresponding to materials.py resolution: Number of voxels per mm gmsh_on_path: Enable/disable using system gmsh rather than bundled gmsh Returns: VoxelModel """ data = makeMesh(filename, True, gmsh_on_path) points = data.points # Get lists of indices of point # ii_tri = data.cells_dict['triangle'] ii_tet = data.cells_dict['tetra'] # Convert lists of indices to lists of points # tris = points[ii_tri] tets = points[ii_tet] # Create barycentric coordinate system T = np.concatenate((tets, tets[:, :, 0:1] * 0 + 1), 2) T_inv = np.zeros(T.shape) for ii, t in enumerate(T): T_inv[ii] = np.linalg.inv(t).T # Find bounding box base = 1 / resolution points_min = points.min(0) points_max = points.max(0) points_min_r = base * np.round(points_min/base) points_max_r = base * np.round(points_max/base) # Create 3D grid xx = np.r_[points_min_r[0]:points_max_r[0]+1:base] yy = np.r_[points_min_r[1]:points_max_r[1]+1:base] zz = np.r_[points_min_r[2]:points_max_r[2]+1:base] # Find center of every grid point xx_mid = (xx[1:] + xx[:-1]) / 2 yy_mid = (yy[1:] + yy[:-1]) / 2 zz_mid = (zz[1:] + zz[:-1]) / 2 # Create grid of voxel centers xyz_mid = np.array(np.meshgrid(xx_mid, yy_mid, zz_mid, indexing='ij')) xyz_mid = xyz_mid.transpose(1, 2, 3, 0) # Convert to list of points xyz_mid = xyz_mid.reshape(-1, 3) # Add 1 to allow conversion to barycentric coordinates xyz_mid = np.concatenate((xyz_mid, xyz_mid[:, 0:1] * 0 + 1), 1) # Create list of indices of each voxel ijk_mid = np.array(np.meshgrid(np.r_[:len(xx_mid)], np.r_[:len(yy_mid)], np.r_[:len(zz_mid)], indexing='ij')) ijk_mid = ijk_mid.transpose(1, 2, 3, 0) ijk_mid2 = ijk_mid.reshape(-1, 3) f3 = findFilledVoxels(np.asarray(T_inv, order='c'), np.asarray(xyz_mid, order='c')) ii, jj = f3.nonzero() lmn = ijk_mid2[np.unique(jj)] voxels = np.zeros(ijk_mid.shape[:3], dtype=np.bool_) voxels[lmn[:, 0], lmn[:, 1], lmn[:, 2]] = True new_model = cls(voxels, generateMaterials(material), coords=coords, resolution=resolution).fitWorkspace() return new_model
def fromVoxFile(filename: str, coords: Tuple[int, int, int] = (0, 0, 0), resolution: float = 1)
-
Create a VoxelModel from an imported .vox file.
Example:
model1 = vf.VoxelModel.fromVoxFile('cylinder-red.vox', (0, 5, 0), 1)
Args
filename
- File name with extension
coords
- Model origin coordinates
resolution
- Number of voxels per mm
Returns
VoxelModel
Expand source code
@classmethod def fromVoxFile(cls, filename: str, coords: Tuple[int, int, int] = (0, 0, 0), resolution: float = 1): """ Create a VoxelModel from an imported .vox file. ---- Example: ``model1 = vf.VoxelModel.fromVoxFile('cylinder-red.vox', (0, 5, 0), 1)`` ---- Args: filename: File name with extension coords: Model origin coordinates resolution: Number of voxels per mm Returns: VoxelModel """ # Import data and align axes v1 = VoxParser(filename).parse() v2 = np.array(v1.to_dense(), dtype=np.uint16) v2 = np.flip(v2, 1) v2 = np.rot90(v2, 1, (2, 0)) v2 = np.rot90(v2, 1, (1, 2)) # Generate materials table assuming indices match materials in material_properties i = 0 materials = np.zeros((1, len(material_properties) + 1), dtype=np.float32) for m in np.unique(v2): if m != 0: i = i+1 material_vector = np.zeros(len(material_properties) + 1, dtype=np.float32) material_vector[0] = 1 material_vector[m+1] = 1 materials = np.vstack((materials, material_vector)) v2[v2 == m] = i return cls(v2, materials, coords=coords, resolution=resolution)
def openVF(filename: str)
-
Load model data from a .vf file
This method will create a new VoxelModel object using the data from the .vf file. Material attributes are stored in the materials.py file, so the same version of this file must be used when saving and opening models.
Example:
model1 = vf.VoxelModel.openVF("test-file")
Args
filename
- File name
Returns
VoxelModel
Expand source code
@classmethod def openVF(cls, filename: str): """ Load model data from a .vf file This method will create a new VoxelModel object using the data from the .vf file. Material attributes are stored in the materials.py file, so the same version of this file must be used when saving and opening models. ---- Example: ``model1 = vf.VoxelModel.openVF("test-file")`` ---- Args: filename: File name Returns: VoxelModel """ if filename[-3:] == '.vf': f = open(filename, 'r') else: f = open(filename + '.vf', 'r') print('Opening file: ' + f.name) data = f.readlines() loc = np.ones((7,2), dtype=np.uint16) loc = np.multiply(loc, -1) for i in range(len(data)): # tqdm(range(len(data)), desc='Finding tags'): if data[i][:-1] == '<coords>': loc[0,0] = i+1 if data[i][:-1] == '</coords>': loc[0,1] = i if data[i][:-1] == '<materials>': loc[1,0] = i+1 if data[i][:-1] == '</materials>': loc[1,1] = i if data[i][:-1] == '<size>': loc[2,0] = i+1 if data[i][:-1] == '</size>': loc[2,1] = i if data[i][:-1] == '<voxels>': loc[3,0] = i+1 if data[i][:-1] == '</voxels>': loc[3,1] = i if data[i][:-1] == '<components>': loc[4,0] = i+1 if data[i][:-1] == '</components>': loc[4,1] = i if data[i][:-1] == '<labels>': loc[5,0] = i+1 if data[i][:-1] == '</labels>': loc[5,1] = i if data[i][:-1] == '<resolution>': loc[6,0] = i+1 if data[i][:-1] == '</resolution>': loc[6,1] = i coords = np.array(data[loc[0,0]][:-2].split(","), dtype=np.int16) if loc[6,0] > -1: resolution = float(data[loc[6,0]][:-1]) else: resolution = 1 materials = np.array(data[loc[1,0]][:-2].split(","), dtype=np.float32) for i in range(loc[1,0]+1, loc[1,1]): # tqdm(range(loc[1,0]+1, loc[1,1]), desc='Reading materials'): materials = np.vstack((materials, np.array(data[i][:-2].split(","), dtype=np.float32))) size = tuple(np.array(data[loc[2,0]][:-2].split(","), dtype=np.uint16)) voxels = np.zeros(size, dtype=np.uint16) for i in range(loc[3,0], loc[3,1]): # tqdm(range(loc[3,0], loc[3,1]), desc='Reading voxels'): x = i - loc[3,0] yz = data[i][:-2].split(";") for z in range(len(yz)): y = np.array(yz[z][:-1].split(","), dtype=np.uint16) voxels[x, :, z] = y numComponents = int(data[loc[4,0]][:-1]) components = np.zeros(size, dtype=np.uint8) if numComponents > 0: for i in range(loc[5,0], loc[5,1]): # tqdm(range(loc[5,0], loc[5,1]), desc='Reading components'): x = i - loc[5, 0] yz = data[i][:-2].split(";") for z in range(len(yz)): y = np.array(yz[z][:-1].split(","), dtype=np.uint8) components[x, :, z] = y new_model = cls(voxels, materials, coords=tuple(coords), resolution=resolution) new_model.numComponents = numComponents new_model.components = components f.close() return new_model
Methods
def add(self, model_to_add)
-
Find the material-wise addition of two models.
The materials of the result are calculated by adding the material vectors for each voxel together.
Example – Adding a voxel containing material 1 and a voxel containing material 3:
Voxel A = [1, 0, 1, 0, 0]
Voxel B = [1, 0, 0, 0, 1]
A + B = [1, 0, 1, 0, 1]
Scale Result (see Cleanup Operations) → [1, 0, 0.5, 0, 0.5]
This operation can also be applied using the addition operator (+).
Examples:
model3 = model1.add(model2)
model3 = model1 + model2
Args
model_to_add
- VoxelModel to add to self
Returns
VoxelModel
Expand source code
def add(self, model_to_add): """ Find the material-wise addition of two models. The materials of the result are calculated by adding the material vectors for each voxel together. Example -- Adding a voxel containing material 1 and a voxel containing material 3: >> Voxel A = [1, 0, 1, 0, 0]\n >> Voxel B = [1, 0, 0, 0, 1]\n >> A + B = [1, 0, 1, 0, 1]\n >> Scale Result (see Cleanup Operations) → [1, 0, 0.5, 0, 0.5]\n This operation can also be applied using the addition operator (+). ---- Examples: ``model3 = model1.add(model2)`` ``model3 = model1 + model2`` ---- Args: model_to_add: VoxelModel to add to self Returns: VoxelModel """ checkResolution(self, model_to_add) a, b, new_coords = alignDims(self, model_to_add) x_len = a.shape[0] y_len = a.shape[1] z_len = a.shape[2] new_voxels = np.zeros_like(a, dtype=np.uint16) new_materials = np.zeros((1, len(self.materials[0])), dtype=np.float32) for x in range(x_len): for y in range(y_len): for z in range(z_len): i1 = int(a[x, y, z]) i2 = int(b[x, y, z]) m1 = self.materials[i1] m2 = model_to_add.materials[i2] m = m1 + m2 m[0] = np.logical_or(m1[0], m2[0]) i = np.where(np.equal(new_materials, m).all(1))[0] if len(i) > 0: new_voxels[x, y, z] = i[0] else: new_materials = np.vstack((new_materials, m)) new_voxels[x, y, z] = len(new_materials) - 1 return VoxelModel(new_voxels, new_materials, new_coords, self.resolution)
def blur(self, radius: float = 1)
-
Apply a Gaussian blur with the defined radius to the entire model.
The blur radius corresponds to two times the standard deviation (2 * sigma) of the Gaussian distribution. The blurred effect is limited to voxels that were occupied by material in the input model.
Example:
model2 = model1.blur(2)
Args
radius
- Blur radius in voxels
Returns
VoxelModel
Expand source code
def blur(self, radius: float = 1): """ Apply a Gaussian blur with the defined radius to the entire model. The blur radius corresponds to two times the standard deviation (2 * sigma) of the Gaussian distribution. The blurred effect is limited to voxels that were occupied by material in the input model. ---- Example: ``model2 = model1.blur(2)`` ___ Args: radius: Blur radius in voxels Returns: VoxelModel """ if radius == 0: return VoxelModel.copy(self) full_model = toFullMaterials(self.voxels, self.materials, len(self.materials[0])) for m in tqdm(range(len(self.materials[0])-1), desc='Blur - applying gaussian filter'): full_model[:, :, :, m+1] = ndimage.gaussian_filter(full_model[:, :, :, m+1], sigma=radius/2) mask = full_model[:, :, :, 0] mask = np.repeat(mask[..., None], len(self.materials[0]), axis=3) full_model = np.multiply(full_model, mask) return toIndexedMaterials(full_model, self, self.resolution)
def blurRegion(self, radius: float, region)
-
Apply a Gaussian blur with the defined radius to voxels that intersect with the region model.
The blur radius corresponds to two times the standard deviation (2 * sigma) of the Gaussian distribution. The blurred effect is limited to voxels that were occupied by material in the intersection result and the material of the region model is ignored.
Example:
model2 = model1.blurRegion(3, regionModel)
Args
radius
- Blur radius in voxels
region
- VoxelModel defining the target blur region
Returns
VoxelModel
Expand source code
def blurRegion(self, radius: float, region): """ Apply a Gaussian blur with the defined radius to voxels that intersect with the region model. The blur radius corresponds to two times the standard deviation (2 * sigma) of the Gaussian distribution. The blurred effect is limited to voxels that were occupied by material in the intersection result and the material of the region model is ignored. ---- Example: ``model2 = model1.blurRegion(3, regionModel)`` ___ Args: radius: Blur radius in voxels region: VoxelModel defining the target blur region Returns: VoxelModel """ new_model = self.blur(radius) new_model = new_model.intersection(region) new_model = new_model.union(self) return new_model
def clearNull(self)
-
Set all null material percentages to zero.
Returns
VoxelModel
Expand source code
def clearNull(self): """ Set all null material percentages to zero. Returns: VoxelModel """ new_model = VoxelModel.copy(self) new_model.materials[1:, 1] = np.zeros(np.shape(new_model.materials[1:,1])) return new_model
def clearance(self, method: Process, material: int = 1)
-
Generate a model representing the clearance region of a model.
The clearance region for a given process and part represents regions that will be affected by the process acting on the part. Clearance can be used to identify regions of a model that conflict with the manufacturing of another model.
Example:
modelResult = model1.clearance(Process.PRINT)
Args
method
- Target manufacturing method, set using Process class
material
- Material index corresponding to materials.py
Returns
VoxelModel
Expand source code
def clearance(self, method: Process, material: int = 1): """ Generate a model representing the clearance region of a model. The clearance region for a given process and part represents regions that will be affected by the process acting on the part. Clearance can be used to identify regions of a model that conflict with the manufacturing of another model. ---- Example: ``modelResult = model1.clearance(Process.PRINT)`` ---- Args: method: Target manufacturing method, set using Process class material: Material index corresponding to materials.py Returns: VoxelModel """ if method == Process.LASER: new_model = self.projection(Dir.BOTH, material).difference(self) elif method == Process.MILL: new_model = self.projection(Dir.BOTH, material).difference(self.projection(Dir.DOWN, material)) elif (method == Process.INSERT) or (method == Process.PRINT): new_model = self.projection(Dir.UP, material) else: new_model = self return new_model
def closing(self, radius: int = 1, plane: Axes = Axes.XYZ, structType: Struct = Struct.STANDARD, connectivity: int = 3)
-
Apply a closing algorithm along the specified axes.
This algorithm consists of dilation followed by erosion and will remove small holes. Depending on the structuring element used, this will apply a chamfer or fillet effect to inside corners.
Args
radius
- Radius for dilation/erosion in voxels
plane
- Dilation/erosion directions, set using Axes class
structType
- Shape of structuring element, set using Struct class
connectivity
- connectivity of structuring element (1-3)
Returns
VoxelModel
Expand source code
def closing(self, radius: int = 1, plane: Axes = Axes.XYZ, structType: Struct = Struct.STANDARD, connectivity: int = 3): """ Apply a closing algorithm along the specified axes. This algorithm consists of dilation followed by erosion and will remove small holes. Depending on the structuring element used, this will apply a chamfer or fillet effect to inside corners. Args: radius: Radius for dilation/erosion in voxels plane: Dilation/erosion directions, set using Axes class structType: Shape of structuring element, set using Struct class connectivity: connectivity of structuring element (1-3) Returns: VoxelModel """ if radius == 0: return VoxelModel.copy(self) else: return self.dilate(radius, plane, structType, connectivity).erode(radius, plane, structType, connectivity).fitWorkspace()
def difference(self, model_to_sub)
-
Find the geometric difference of two models.
Example:
model3 = model1.difference(model2)
Args
model_to_sub
- VoxelModel to subtract from self
Returns
VoxelModel
Expand source code
def difference(self, model_to_sub): """ Find the geometric difference of two models. ---- Example: ``model3 = model1.difference(model2)`` ---- Args: model_to_sub: VoxelModel to subtract from self Returns: VoxelModel """ checkResolution(self, model_to_sub) a, b, new_coords = alignDims(self, model_to_sub) mask = np.array(b == 0, dtype=np.bool_) new_voxels = np.multiply(a, mask) return VoxelModel(new_voxels, self.materials, new_coords, self.resolution)
def dilate(self, radius: int = 1, plane: Axes = Axes.XYZ, struct_type: Struct = Struct.STANDARD, connectivity: int = 3)
-
Dilate a model along the specified axes.
Examples:
model2 = model1.dilate(3)
model4 = model3.dilate(1, Axes.XY, Struct.SPHERE, 2)
Args
radius
- Dilation radius in voxels
plane
- Dilation directions, set using Axes class
struct_type
- Shape of structuring element, set using Struct class
connectivity
- Connectivity of structuring element (1-3)
Returns
VoxelModel
Expand source code
def dilate(self, radius: int = 1, plane: Axes = Axes.XYZ, struct_type: Struct = Struct.STANDARD, connectivity: int = 3): # TODO: Preserve overlapping materials? """ Dilate a model along the specified axes. ---- Examples: ``model2 = model1.dilate(3)`` ``model4 = model3.dilate(1, Axes.XY, Struct.SPHERE, 2)`` ---- Args: radius: Dilation radius in voxels plane: Dilation directions, set using Axes class struct_type: Shape of structuring element, set using Struct class connectivity: Connectivity of structuring element (1-3) Returns: VoxelModel """ if radius == 0: return VoxelModel.copy(self) x_len = self.voxels.shape[0] + (radius * 2) y_len = self.voxels.shape[1] + (radius * 2) z_len = self.voxels.shape[2] + (radius * 2) new_voxels = np.zeros((x_len, y_len, z_len), dtype=np.uint16) new_voxels[radius:-radius, radius:-radius, radius:-radius] = self.voxels if struct_type == Struct.SPHERE: struct = structSphere(radius, plane) new_voxels = ndimage.grey_dilation(new_voxels, footprint=struct) else: # Struct.STANDARD struct = structStandard(connectivity, plane) for i in range(radius): new_voxels = ndimage.grey_dilation(new_voxels, footprint=struct) return VoxelModel(new_voxels, self.materials, (self.coords[0] - radius, self.coords[1] - radius, self.coords[2] - radius), self.resolution)
def dilateBounded(self, radius: int = 1, plane: Axes = Axes.XYZ, structType: Struct = Struct.STANDARD, connectivity: int = 3)
-
Dilate a model along the specified axes without increasing the size of its bounding box.
Args
radius
- Dilation radius in voxels
plane
- Dilation directions, set using Axes class
structType
- Shape of structuring element, set using Struct class
connectivity
- Connectivity of structuring element (1-3)
Returns
VoxelModel
Expand source code
def dilateBounded(self, radius: int = 1, plane: Axes = Axes.XYZ, structType: Struct = Struct.STANDARD, connectivity: int = 3): """ Dilate a model along the specified axes without increasing the size of its bounding box. Args: radius: Dilation radius in voxels plane: Dilation directions, set using Axes class structType: Shape of structuring element, set using Struct class connectivity: Connectivity of structuring element (1-3) Returns: VoxelModel """ if radius == 0: return VoxelModel.copy(self) new_voxels = np.copy(self.fitWorkspace().voxels) if structType == Struct.SPHERE: struct = structSphere(radius, plane) new_voxels = ndimage.grey_dilation(new_voxels, footprint=struct) else: # Struct.STANDARD struct = structStandard(connectivity, plane) for i in range(radius): new_voxels = ndimage.grey_dilation(new_voxels, footprint=struct) return VoxelModel(new_voxels, self.materials, self.coords, self.resolution)
def dither(self, use_full=True, x_error=0.0, y_error=0.0, z_error=0.0, error_spread_threshold=0.8, blur=False, radius=1)
-
Apply material-wise dithering to a model.
Applying dithering will modify the model so that each voxel contains material in only a single material channel. Regions of the model which contained mixtures of materials will be converted to distributions of adjacent single material voxels.
Args
use_full
- Enabling will use a Stucki error diffusion filter. Disabling will use the provided values for x, y, and z error
x_error
- Percentage of error to spread in X
y_error
- Percentage of error to spread in Y
z_error
- Percentage of error to spread in Z
error_spread_threshold
- If a voxel contains a material channel that accounts for more than this percentage of the voxel, no additional error will be spread to it
blur
- Enable/disable applying a blur operation before the dither operation
radius
- Radius value for the optional blur operation
Returns
VoxelModel
Expand source code
def dither(self, use_full=True, x_error=0.0, y_error=0.0, z_error=0.0, error_spread_threshold=0.8, blur=False, radius=1): """ Apply material-wise dithering to a model. Applying dithering will modify the model so that each voxel contains material in only a single material channel. Regions of the model which contained mixtures of materials will be converted to distributions of adjacent single material voxels. Args: use_full: Enabling will use a Stucki error diffusion filter. Disabling will use the provided values for x, y, and z error x_error: Percentage of error to spread in X y_error: Percentage of error to spread in Y z_error: Percentage of error to spread in Z error_spread_threshold: If a voxel contains a material channel that accounts for more than this percentage of the voxel, no additional error will be spread to it blur: Enable/disable applying a blur operation before the dither operation radius: Radius value for the optional blur operation Returns: VoxelModel """ if blur and (radius > 0): new_model = self.blur(radius) new_model = new_model.scaleValues() else: new_model = self.scaleValues() new_model.voxels = new_model.voxels.astype(dtype=np.uint16) full_model = toFullMaterials(new_model.voxels, new_model.materials, len(material_properties) + 1) full_model = ditherOptimized(full_model, use_full, x_error, y_error, z_error, error_spread_threshold) return toIndexedMaterials(full_model, self, self.resolution)
def divide(self, other)
-
Find the material-wise division of two models.
The materials of the result are calculated for each voxel by dividing the first material vector by the second. This function also supports division by a scalar.
This operation can also be applied using the division operator (/).
Examples:
model3 = model1.divide(model2)
model3 = model1 / model2
model5 = model4 / 3
Args
other
- VoxelModel to divide self by
Returns
VoxelModel
Expand source code
def divide(self, other): """ Find the material-wise division of two models. The materials of the result are calculated for each voxel by dividing the first material vector by the second. This function also supports division by a scalar. This operation can also be applied using the division operator (/). ---- Examples: ``model3 = model1.divide(model2)`` ``model3 = model1 / model2`` ``model5 = model4 / 3`` ---- Args: other: VoxelModel to divide self by Returns: VoxelModel """ if type(other) is VoxelModel: checkResolution(self, other) a, b, new_coords = alignDims(self, other) x_len = a.shape[0] y_len = a.shape[1] z_len = a.shape[2] new_voxels = np.zeros_like(a, dtype=np.uint16) new_materials = np.zeros((1, len(self.materials[0])), dtype=np.float32) for x in range(x_len): for y in range(y_len): for z in range(z_len): i1 = int(a[x, y, z]) i2 = int(b[x, y, z]) m1 = self.materials[i1] m2 = other.materials[i2] m2[m2 == 0] = 1 m = np.divide(m1, m2) m[0] = m1[0] i = np.where(np.equal(new_materials, m).all(1))[0] if len(i) > 0: new_voxels[x, y, z] = i[0] else: new_materials = np.vstack((new_materials, m)) new_voxels[x, y, z] = len(new_materials) - 1 return VoxelModel(new_voxels, new_materials, new_coords, self.resolution) else: if other == 0: return self new_model = VoxelModel.copy(self) new_model.materials[1:, 1:] = np.divide(new_model.materials[1:, 1:], other) return new_model
def erode(self, radius: int = 1, plane: Axes = Axes.XYZ, struct_type: Struct = Struct.STANDARD, connectivity: int = 3)
-
Erode a model along the specified axes.
Examples:
model2 = model1.erode(5, connectivity=2)
model4 = model3.erode(2, Axes.X, Struct.SPHERE, 1)
Args
radius
- Erosion radius in voxels
plane
- Erosion directions, set using Axes class
struct_type
- Shape of structuring element, set using Struct class
connectivity
- Connectivity of structuring element (1-3)
Returns
VoxelModel
Expand source code
def erode(self, radius: int = 1, plane: Axes = Axes.XYZ, struct_type: Struct = Struct.STANDARD, connectivity: int = 3): """ Erode a model along the specified axes. ---- Examples: ``model2 = model1.erode(5, connectivity=2)`` ``model4 = model3.erode(2, Axes.X, Struct.SPHERE, 1)`` ---- Args: radius: Erosion radius in voxels plane: Erosion directions, set using Axes class struct_type: Shape of structuring element, set using Struct class connectivity: Connectivity of structuring element (1-3) Returns: VoxelModel """ if radius == 0: return VoxelModel.copy(self) new_voxels = np.copy(self.voxels) mask = np.array(new_voxels != 0, dtype=np.bool_) if struct_type == Struct.SPHERE: struct = structSphere(radius, plane) mask = ndimage.binary_erosion(mask, structure=struct) else: # Struct.STANDARD struct = structStandard(connectivity, plane) mask = ndimage.binary_erosion(mask, structure=struct, iterations=radius) new_voxels = np.multiply(new_voxels, mask) return VoxelModel(new_voxels, self.materials, self.coords, self.resolution)
def fitWorkspace(self)
-
Remove excess empty space from a model.
Resize the workspace around a model to remove excess empty space. Model coordinates are updated to reflect the change.
Returns
VoxelModel
Expand source code
def fitWorkspace(self): """ Remove excess empty space from a model. Resize the workspace around a model to remove excess empty space. Model coordinates are updated to reflect the change. Returns: VoxelModel """ x_len = self.voxels.shape[0] y_len = self.voxels.shape[1] z_len = self.voxels.shape[2] x_min = -1 x_max = -1 y_min = -1 y_max = -1 z_min = -1 z_max = -1 for x in range(x_len): if np.sum(self.voxels[x, :, :]) > 0: x_min = x break for x in range(x_len-1,-1,-1): if np.sum(self.voxels[x, :, :]) > 0: x_max = x+1 break for y in range(y_len): if np.sum(self.voxels[:, y, :]) > 0: y_min = y break for y in range(y_len-1,-1,-1): if np.sum(self.voxels[:, y, :]) > 0: y_max = y+1 break for z in range(z_len): if np.sum(self.voxels[:, :, z]) > 0: z_min = z break for z in range(z_len-1,-1,-1): if np.sum(self.voxels[:, :, z]) > 0: z_max = z+1 break x_min = 0 if x_min == -1 else x_min y_min = 0 if y_min == -1 else y_min z_min = 0 if z_min == -1 else z_min x_max = x_len if x_max == -1 else x_max y_max = y_len if y_max == -1 else y_max z_max = z_len if z_max == -1 else z_max new_voxels = np.copy(self.voxels[x_min:x_max, y_min:y_max, z_min:z_max]) new_components = np.copy(self.components[x_min:x_max, y_min:y_max, z_min:z_max]) new_coords = (self.coords[0] + x_min, self.coords[1] + y_min, self.coords[2] + z_min) new_model = VoxelModel(new_voxels, self.materials, coords=new_coords, resolution=self.resolution) new_model.numComponents = self.numComponents new_model.components = new_components return new_model
def getBoundingBox(self)
-
Get all voxels contained in the bounding box of the input model.
Returns
VoxelModel
Expand source code
def getBoundingBox(self): """ Get all voxels contained in the bounding box of the input model. Returns: VoxelModel """ new_model = VoxelModel.copy(self) new_model = new_model.fitWorkspace() new_model.voxels.fill(1) new_model = new_model.getOccupied() new_model.materials = self.materials[0:2, :] return new_model
def getCenter(self)
-
Find the coordinates of the center of a model.
Returns
Center coordinates in voxels
Expand source code
def getCenter(self): """ Find the coordinates of the center of a model. Returns: Center coordinates in voxels """ model = self.fitWorkspace() x_center = (model.voxels.shape[0] / 2) + model.coords[0] y_center = (model.voxels.shape[1] / 2) + model.coords[1] z_center = (model.voxels.shape[2] / 2) + model.coords[2] centerCoords = (x_center, y_center, z_center) return centerCoords
def getComponents(self, connectivity: int = 1)
-
Update component labels for a model.
This function uses a disconnected components algorithm and assumes that adjacent voxels with different materials are connected. Connectivity can be set to 1-3 and defines the shape of the structuring element.
Args
connectivity
- Connectivity of structuring element (1-3)
Returns
VoxelModel
Expand source code
def getComponents(self, connectivity: int = 1): """ Update component labels for a model. This function uses a disconnected components algorithm and assumes that adjacent voxels with different materials are connected. Connectivity can be set to 1-3 and defines the shape of the structuring element. Args: connectivity: Connectivity of structuring element (1-3) Returns: VoxelModel """ mask = np.array(self.voxels[:, :, :] > 0, dtype=np.bool_) struct = ndimage.generate_binary_structure(3, connectivity) new_model = VoxelModel.copy(self) new_model.components, new_model.numComponents = ndimage.label(mask, structure=struct) new_model.components = np.uint8(new_model.components) return new_model
def getCoords(self)
-
Get the origin coordinates of a model.
Returns
Origin coordinates in voxels
Expand source code
def getCoords(self): """ Get the origin coordinates of a model. Returns: Origin coordinates in voxels """ model = self.fitWorkspace() return model.coords
def getDim(self)
-
Get the dimensions of model.
Returns
Model dimensions in voxels
Expand source code
def getDim(self): """ Get the dimensions of model. Returns: Model dimensions in voxels """ model = self.fitWorkspace() x = model.voxels.shape[0] y = model.voxels.shape[1] z = model.voxels.shape[2] return x, y, z
def getHGModel(self, material)
-
Get the hydrogel model parameters for a row in a model's material array.
This is currently returned based on the material present in the highest percentage.
TODO: Make this average multiple model parameters
Args
material
- Material index
Returns
Dictionary of hydrogel model parameters
Expand source code
def getHGModel(self, material): """ Get the hydrogel model parameters for a row in a model's material array. This is currently returned based on the material present in the highest percentage. TODO: Make this average multiple model parameters Args: material: Material index Returns: Dictionary of hydrogel model parameters """ material_id = self.materials[material][1:].argmax() current_material_data = getMaterialData(material_id) try: hg_model_index = current_material_data['HGM'] current_hg_model = next((item for item in hg_models if item['id'] == hg_model_index), None) if current_hg_model is None: raise KeyError except KeyError: print('Hydrogel model data not available for ' + current_material_data['name']) current_hg_model = None return current_hg_model
def getMaterialProperties(self, material)
-
Get the average material properties of a row in a model's material array.
Args
material
- Material index
Returns
Dictionary of material properties
Expand source code
def getMaterialProperties(self, material): """ Get the average material properties of a row in a model's material array. Args: material: Material index Returns: Dictionary of material properties """ avg_properties = {} for key in material_properties[0]: if key == 'name' or key == 'process': string = '' for i in range(len(self.materials[0])-1): if self.materials[material][i + 1] > 0: current_material_data = getMaterialData(i) string = string + current_material_data[key] + ' ' avg_properties.update({key: string}) elif key == 'MM' or key == 'MMD' or key == 'FM' or key == 'HG' or key == 'HGM': material_id = self.materials[material][1:].argmax() current_material_data = getMaterialData(material_id) var = current_material_data[key] avg_properties.update({key: var}) else: var = 0 for i in range(len(self.materials[0])-1): current_material_data = getMaterialData(i) var = var + self.materials[material][i + 1] * current_material_data[key] avg_properties.update({key: var}) return avg_properties
def getMaxCoords(self)
-
Get the maximum coordinate location in a model.
This point is equal to origin coordinates + model dimensions.
Returns
Maximum coordinates in voxels
Expand source code
def getMaxCoords(self): """ Get the maximum coordinate location in a model. This point is equal to origin coordinates + model dimensions. Returns: Maximum coordinates in voxels """ model = self.fitWorkspace() x = model.coords[0] + model.voxels.shape[0] y = model.coords[1] + model.voxels.shape[1] z = model.coords[2] + model.voxels.shape[2] return x, y, z
def getOccupied(self)
-
Get all voxels occupied by the input model.
Returns
VoxelModel
Expand source code
def getOccupied(self): """ Get all voxels occupied by the input model. Returns: VoxelModel """ mask = np.array(self.voxels != 0, dtype=np.bool_) return VoxelModel(mask, self.materials[0:2, :], self.coords, self.resolution)
def getSSData(self, material)
-
Get the stress-strain data for a row in a model's material array.
This is currently returned based on the material present in the highest percentage.
TODO: Make this average multiple stress-strain curves
Args
material
- Material index
Returns
Dictionary of stress-strain data
Expand source code
def getSSData(self, material): """ Get the stress-strain data for a row in a model's material array. This is currently returned based on the material present in the highest percentage. TODO: Make this average multiple stress-strain curves Args: material: Material index Returns: Dictionary of stress-strain data """ material_id = self.materials[material][1:].argmax() current_material_data = getMaterialData(material_id) try: ss_data_index = current_material_data['MMD'] current_ss_data = next((item for item in ss_data if item['id'] == ss_data_index), None) if current_ss_data is None: raise KeyError except KeyError: print('Stress-strain data not available for ' + current_material_data['name']) current_ss_data = None return current_ss_data
def getUnoccupied(self)
-
Get all voxels not occupied by the input model.
This operation can also be applied using the invert operator (~).
Examples:
model2 = model1.getUnoccupied()
model2 = ~model1
Returns
VoxelModel
Expand source code
def getUnoccupied(self): """ Get all voxels not occupied by the input model. This operation can also be applied using the invert operator (~). ---- Examples: ``model2 = model1.getUnoccupied()`` ``model2 = ~model1`` ---- Returns: VoxelModel """ mask = np.array(self.voxels == 0, dtype=np.bool_) return VoxelModel(mask, self.materials[0:2, :], self.coords, self.resolution)
def getVolume(self, component: int = 0, material: int = 0)
-
Get the volume of a model or model component.
Args
component
- Component label to measure, set to 0 for all components
material
- Material index to measure, set to 0 for all materials
Returns
Volume in voxels, volume in mm^3
Expand source code
def getVolume(self, component: int = 0, material: int = 0): """ Get the volume of a model or model component. Args: component: Component label to measure, set to 0 for all components material: Material index to measure, set to 0 for all materials Returns: Volume in voxels, volume in mm^3 """ new_model = VoxelModel.copy(self) if component > 0: new_model = new_model.isolateComponent(component) if material > 0: new_model = new_model.isolateMaterial(material) volumeVoxels = np.count_nonzero(new_model.voxels) volumeMM3 = volumeVoxels * ((1/self.resolution)**3) return volumeVoxels, volumeMM3
def getVoxelDim(self)
-
Get the side dimension of a voxel in mm.
Returns
Float
Expand source code
def getVoxelDim(self): """ Get the side dimension of a voxel in mm. Returns: Float """ res = self.resolution return (1.0/res) * 0.001
def getVoxelProperties(self, coords: Tuple[int, int, int])
-
Get the average material properties of a specific voxel.
Args
coords
- Voxel coordinates
Returns
Dictionary of material properties
Expand source code
def getVoxelProperties(self, coords: Tuple[int, int, int]): """ Get the average material properties of a specific voxel. Args: coords: Voxel coordinates Returns: Dictionary of material properties """ return self.getMaterialProperties(self.voxels[coords[0], coords[1], coords[2]])
def intersection(self, model_2)
-
Find the geometric intersection of two models.
The materials from self will be used in the resulting model. This operation can also be applied using the AND operator (&)
Examples:
model3 = model1.intersection(model2)
model3 = model1 & model2
Args
model_2
- VoxelModel to intersect with self
Returns
VoxelModel
Expand source code
def intersection(self, model_2): """ Find the geometric intersection of two models. The materials from self will be used in the resulting model. This operation can also be applied using the AND operator (&) ---- Examples: ``model3 = model1.intersection(model2)`` ``model3 = model1 & model2`` ---- Args: model_2: VoxelModel to intersect with self Returns: VoxelModel """ checkResolution(self, model_2) a, b, new_coords = alignDims(self, model_2) mask = np.logical_and(np.array(a != 0, dtype=np.bool_), np.array(b != 0, dtype=np.bool_)) # Paper provides for left/right intersection # For code simplicity, only a left intersection is provided here new_voxels = np.multiply(a, mask) # material from left model takes priority materials = self.materials return VoxelModel(new_voxels, materials, new_coords, self.resolution)
def isOccupied(self, coords: Tuple[int, int, int])
-
Determine if a specific voxel is occupied.
Returns
True/False
Expand source code
def isOccupied(self, coords: Tuple[int, int, int]): """ Determine if a specific voxel is occupied. Returns: True/False """ x = coords[0] - self.coords[0] y = coords[1] - self.coords[1] z = coords[2] - self.coords[2] if x < 0 or x >= self.voxels.shape[0]: return False if y < 0 or y >= self.voxels.shape[1]: return False if z < 0 or z >= self.voxels.shape[2]: return False v = self.voxels[x, y, z] if v == 0: return False else: return True
def isolateComponent(self, component: int)
-
Isolate a component by its component label.
Component labels must first be updated with getComponents. Unrecognized component labels will return an empty object.
Args
component
- Component label to isolate
Returns
VoxelModel
Expand source code
def isolateComponent(self, component: int): """ Isolate a component by its component label. Component labels must first be updated with getComponents. Unrecognized component labels will return an empty object. Args: component: Component label to isolate Returns: VoxelModel """ mask = np.array(self.components == component, dtype=np.bool_) new_voxels = np.multiply(self.voxels, mask) return VoxelModel(new_voxels, self.materials, self.coords, self.resolution)
def isolateLayer(self, layer: int)
-
Get all voxels in a specified layer.
Example:
model2 = model1.isolateLayer(8)
Args
layer
- Voxel layer to isolate
Returns
VoxelModel
Expand source code
def isolateLayer(self, layer: int): """ Get all voxels in a specified layer. ---- Example: ``model2 = model1.isolateLayer(8)`` ---- Args: layer: Voxel layer to isolate Returns: VoxelModel """ new_voxels = np.zeros_like(self.voxels, dtype=np.uint16) new_voxels[:, :, layer - self.coords[2]] = self.voxels[:, :, layer - self.coords[2]] return VoxelModel(new_voxels, self.materials, self.coords, self.resolution)
def isolateMaterial(self, material: int)
-
Get all voxels with a specified material.
Example:
model2 = model1.isolateMaterial(4)
Args
material
- Material index corresponding to the materials array for the model
Returns
VoxelModel
Expand source code
def isolateMaterial(self, material: int): """ Get all voxels with a specified material. ---- Example: ``model2 = model1.isolateMaterial(4)`` ---- Args: material: Material index corresponding to the materials array for the model Returns: VoxelModel """ mask = np.array(self.voxels == material, dtype=np.bool_) materials = np.zeros((2, self.materials.shape[1]), dtype=np.float32) materials[1] = self.materials[material] return VoxelModel(mask.astype(int), materials, self.coords, self.resolution)
def keepout(self, method: Process, material: int = 1)
-
Generate a model representing the keep-out region of a model.
The keep-out region for a given process and part represents material which the process may not modify while creating the part. This feature primarily applies to subtractive processes. It includes material that will be present in the final part and regions of the workspace that cannot be accessed without affecting this material. In general, additive processes will have no keep-out region because they deposit material from the bottom up.
Example:
modelResult = model1.keepout(Process.MILL)
Args
method
- Target manufacturing method, set using Process class
material
- Material index corresponding to materials.py
Returns
VoxelModel
Expand source code
def keepout(self, method: Process, material: int = 1): """ Generate a model representing the keep-out region of a model. The keep-out region for a given process and part represents material which the process may not modify while creating the part. This feature primarily applies to subtractive processes. It includes material that will be present in the final part and regions of the workspace that cannot be accessed without affecting this material. In general, additive processes will have no keep-out region because they deposit material from the bottom up. ---- Example: ``modelResult = model1.keepout(Process.MILL)`` ---- Args: method: Target manufacturing method, set using Process class material: Material index corresponding to materials.py Returns: VoxelModel """ if method == Process.LASER: new_model = self.projection(Dir.BOTH, material) elif method == Process.MILL: new_model = self.projection(Dir.DOWN, material) elif method == Process.INSERT: new_model = self.projection(Dir.UP, material) else: new_model = self return new_model
def mirror(self, axes: Axes = Axes.X)
-
Mirror a model along the given axes.
This operation will mirror ALONG the given axes. For example:
- Axes.X performs a mirror about the YZ plane
- Axes.XY performs a mirror about the YZ plane and the XZ plane
- Axes.XYZ performs a mirror along all three axes
Args
axes
- Axes for mirror operation, set using Axes class
Returns
VoxelModel
Expand source code
def mirror(self, axes: Axes = Axes.X): """ Mirror a model along the given axes. This operation will mirror ALONG the given axes. For example: - Axes.X performs a mirror about the YZ plane - Axes.XY performs a mirror about the YZ plane and the XZ plane - Axes.XYZ performs a mirror along all three axes Args: axes: Axes for mirror operation, set using Axes class Returns: VoxelModel """ flip_axis = [] for i in range(len(axes.value)): if axes.value[i]: flip_axis.append(i) flip_axis = tuple(flip_axis) centerCoords = self.getCenter() new_voxels = np.flip(self.voxels, flip_axis) new_model = VoxelModel(new_voxels, self.materials, self.coords, self.resolution) new_model = new_model.setCenter(centerCoords) return new_model
def multiply(self, other)
-
Find the material-wise multiplication of two models.
The materials of the result are calculated by multiplying the material vectors for each voxel. This function also supports multiplication by a scalar.
This operation can also be applied using the multiplication operator (*).
Examples:
model3 = model1.multiply(model2)
model3 = model1 * model2
model5 = model4 * 3
Args
other
- VoxelModel to multiply with self
Returns
VoxelModel
Expand source code
def multiply(self, other): """ Find the material-wise multiplication of two models. The materials of the result are calculated by multiplying the material vectors for each voxel. This function also supports multiplication by a scalar. This operation can also be applied using the multiplication operator (*). ---- Examples: ``model3 = model1.multiply(model2)`` ``model3 = model1 * model2`` ``model5 = model4 * 3`` ---- Args: other: VoxelModel to multiply with self Returns: VoxelModel """ if type(other) is VoxelModel: checkResolution(self, other) a, b, new_coords = alignDims(self, other) x_len = a.shape[0] y_len = a.shape[1] z_len = a.shape[2] new_voxels = np.zeros_like(a, dtype=np.uint16) new_materials = np.zeros((1, len(self.materials[0])), dtype=np.float32) for x in range(x_len): for y in range(y_len): for z in range(z_len): i1 = int(a[x, y, z]) i2 = int(b[x, y, z]) m1 = self.materials[i1] m2 = other.materials[i2] m = np.multiply(m1, m2) m[0] = np.logical_and(m1[0], m2[0]) i = np.where(np.equal(new_materials, m).all(1))[0] if len(i) > 0: new_voxels[x, y, z] = i[0] else: new_materials = np.vstack((new_materials, m)) new_voxels[x, y, z] = len(new_materials) - 1 return VoxelModel(new_voxels, new_materials, new_coords, self.resolution) else: new_model = VoxelModel.copy(self) new_model.materials[1:, 1:] = np.multiply(new_model.materials[1:, 1:], other) return new_model
def opening(self, radius: int = 1, plane: Axes = Axes.XYZ, structType: Struct = Struct.STANDARD, connectivity: int = 3)
-
Apply an opening algorithm along the specified axes.
This algorithm consists of erosion followed by dilation and will remove small features. Depending on the structuring element used, this will apply a chamfer or fillet effect to outside corners.
Args
radius
- Radius for dilation/erosion in voxels
plane
- Dilation/erosion directions, set using Axes class
structType
- Shape of structuring element, set using Struct class
connectivity
- Connectivity of structuring element (1-3)
Returns
VoxelModel
Expand source code
def opening(self, radius: int = 1, plane: Axes = Axes.XYZ, structType: Struct = Struct.STANDARD, connectivity: int = 3): """ Apply an opening algorithm along the specified axes. This algorithm consists of erosion followed by dilation and will remove small features. Depending on the structuring element used, this will apply a chamfer or fillet effect to outside corners. Args: radius: Radius for dilation/erosion in voxels plane: Dilation/erosion directions, set using Axes class structType: Shape of structuring element, set using Struct class connectivity: Connectivity of structuring element (1-3) Returns: VoxelModel """ if radius == 0: return VoxelModel.copy(self) new_voxels = np.copy(self.voxels) mask = np.array(new_voxels != 0, dtype=np.bool_) if structType == Struct.SPHERE: struct = structSphere(radius, plane) mask = ndimage.binary_opening(mask, structure=struct) else: # Struct.STANDARD struct = structStandard(connectivity, plane) mask = ndimage.binary_opening(mask, structure=struct, iterations=radius) new_voxels = np.multiply(new_voxels, mask) return VoxelModel(new_voxels, self.materials, self.coords, self.resolution)
def plot(self, plot=None, name: str = 'model', wireframe: bool = False, **kwargs)
-
Add model to a K3D plot.
Additional display options:
- opacity:
float
. Opacity of voxels. - outlines:
bool
. Whether mesh should display with outlines. - outlines_color:
int
. Packed RGB color of the resulting outlines (0xff0000 is red, 0xff is blue) - kwargs:
dict
. Dictionary arguments to configure transform and model_matrix.
More information available at: https://github.com/K3D-tools/K3D-jupyter
Args
plot
- Plot object to add model to
name
- Model name
wireframe
- Enable displaying model as a wireframe
kwargs
- Additional display options (see above)
Returns
K3D plot object
Expand source code
def plot(self, plot=None, name: str = 'model', wireframe: bool = False, **kwargs): """ Add model to a K3D plot. Additional display options: - opacity: `float`. Opacity of voxels. - outlines: `bool`. Whether mesh should display with outlines. - outlines_color: `int`. Packed RGB color of the resulting outlines (0xff0000 is red, 0xff is blue) - kwargs: `dict`. Dictionary arguments to configure transform and model_matrix. More information available at: https://github.com/K3D-tools/K3D-jupyter Args: plot: Plot object to add model to name: Model name wireframe: Enable displaying model as a wireframe kwargs: Additional display options (see above) Returns: K3D plot object """ model = self.fitWorkspace() | VoxelModel.empty((1, 1, 1), resolution=self.resolution) model = model.removeDuplicateMaterials() # Get colors colors = [] for m in model.materials: r = 0 g = 0 b = 0 for i in range(1, len(m)): r = r + m[i] * material_properties[i - 1]['r'] g = g + m[i] * material_properties[i - 1]['g'] b = b + m[i] * material_properties[i - 1]['b'] r = 1 if r > 1 else r g = 1 if g > 1 else g b = 1 if b > 1 else b colors.append(rgb_to_hex(r, g, b)) colors = np.array(colors, dtype=np.uint32)[1:] # Plot if plot is None: plot = k3d.plot() plot += k3d.voxels(np.swapaxes(model.voxels, 0, 2).astype(np.uint8), color_map=colors, name=name, wireframe=wireframe, **kwargs) return plot
- opacity:
def projection(self, direction: Dir, material: int = 1)
-
Generate a model representing all voxels within the workspace that contain material or that lie in the specified direction with respect to a voxel that contains material.
Example:
modelResult = model1.projection(Dir.DOWN)
Args
direction
- Projection direction, set using Dir class
material
- Material index corresponding to materials.py
Returns
VoxelModel
Expand source code
def projection(self, direction: Dir, material: int = 1): """ Generate a model representing all voxels within the workspace that contain material or that lie in the specified direction with respect to a voxel that contains material. --- Example: ``modelResult = model1.projection(Dir.DOWN)`` --- Args: direction: Projection direction, set using Dir class material: Material index corresponding to materials.py Returns: VoxelModel """ new_voxels = np.zeros_like(self.voxels) x_len = self.voxels.shape[0] y_len = self.voxels.shape[1] z_len = self.voxels.shape[2] if direction == Dir.BOTH: # Loop through model data for x in range(x_len): for y in range(y_len): if np.sum(self.voxels[x, y, :]) > 0: new_voxels[x, y, :].fill(1) elif direction == Dir.DOWN: # Loop through model data for x in range(x_len): for y in range(y_len): for z in range(z_len): if np.sum(self.voxels[x, y, z:]) > 0: new_voxels[x, y, z] = 1 elif np.sum(self.voxels[x, y, z:]) == 0: break elif direction == Dir.UP: # Loop through model data for x in range(x_len): for y in range(y_len): for z in range(z_len): if np.sum(self.voxels[x, y, :z]) > 0: new_voxels[x, y, z] = 1 return VoxelModel(new_voxels, generateMaterials(material), self.coords, self.resolution)
def removeDuplicateMaterials(self)
-
Remove duplicate rows from a model's material array.
This function can be greatly accelerated using CUDA. For more information on how to enable CUDA in VoxelFuse, see the GpuSettings class.
Returns
VoxelModel
Expand source code
def removeDuplicateMaterials(self): """ Remove duplicate rows from a model's material array. This function can be greatly accelerated using CUDA. For more information on how to enable CUDA in VoxelFuse, see the GpuSettings class. Returns: VoxelModel """ new_voxels = np.copy(self.voxels) new_materials = np.unique(self.materials, axis=0) # Get CUDA settings try: CUDA_enable = bool(os.environ.get('VF_CUDA_ENABLE')) CUDA_device = int(os.environ.get('VF_CUDA_DEVICE')) except TypeError: print('CUDA environment variables not found, defaulting to CUDA disabled') CUDA_enable = False CUDA_device = 0 if CUDA_enable: # Select GPU cuda.select_device(CUDA_device) # CUDA blocks blockdim = (8, 8, 8) # 512 threads (1024 threads max) griddim = (new_voxels.shape[0] // blockdim[0] + 1, new_voxels.shape[1] // blockdim[1] + 1, new_voxels.shape[2] // blockdim[2] + 1) # Update material indices updateMatIndices[griddim, blockdim](new_voxels, self.materials, new_materials) else: x_len, y_len, z_len = self.voxels.shape for x in tqdm(range(x_len), desc='Removing duplicate materials'): for y in range(y_len): for z in range(z_len): i = self.voxels[x, y, z] m = self.materials[i] ni = np.where(np.equal(new_materials, m).all(1))[0][0] new_voxels[x, y, z] = ni return VoxelModel(new_voxels, new_materials, self.coords, self.resolution)
def removeNegatives(self)
-
Remove negative material values from a model (these have no physical meaning).
Example:
model2 = model1.removeNegatives()
Returns
VoxelModel
Expand source code
def removeNegatives(self): """ Remove negative material values from a model (these have no physical meaning). ---- Example: ``model2 = model1.removeNegatives()`` ___ Returns: VoxelModel """ new_model = VoxelModel.copy(self) new_model.materials[new_model.materials < 1e-10] = 0 material_sums = np.sum(new_model.materials[:,1:], 1) # This and following update the a values material_sums[material_sums > 0] = 1 new_model.materials[:, 0] = material_sums return new_model
def rotate(self, angle: float, axis: Axes = Axes.Z)
-
Rotate a model about its center.
Floating point errors may slightly affect the angle of the resulting model. To rotate a model in precise 90 degree increments, use rotate90().
Args
angle
- Angle of rotation in degrees
axis
- Axis of rotation, set using Axes class
Returns
VoxelModel
Expand source code
def rotate(self, angle: float, axis: Axes = Axes.Z): """ Rotate a model about its center. Floating point errors may slightly affect the angle of the resulting model. To rotate a model in precise 90 degree increments, use rotate90(). Args: angle: Angle of rotation in degrees axis: Axis of rotation, set using Axes class Returns: VoxelModel """ if axis == Axes.X: plane = (1, 2) sign = 1 elif axis == Axes.Y: plane = (2, 0) sign = -1 # For some reason, Y rotates the opposite direction than expected else: # axis == Axes.Z plane = (0, 1) sign = 1 centerCoords = self.getCenter() new_voxels = ndimage.rotate(self.voxels, sign*angle, plane, order=0) new_model = VoxelModel(new_voxels, self.materials, self.coords, self.resolution) new_model = new_model.setCenter(centerCoords) return new_model
def rotate90(self, times: int = 1, axis: Axes = Axes.Z)
-
Rotate a model about its center in increments of 90 degrees.
Args
times
- Number of 90 degree increments to rotate model
axis
- Axis of rotation, set using Axes class
Returns
VoxelModel
Expand source code
def rotate90(self, times: int = 1, axis: Axes = Axes.Z): """ Rotate a model about its center in increments of 90 degrees. Args: times: Number of 90 degree increments to rotate model axis: Axis of rotation, set using Axes class Returns: VoxelModel """ if axis == Axes.X or axis == 0: plane = (1, 2) elif axis == Axes.Y or axis == 1: plane = (0, 2) else: # axis == Axes.Z or axis = 2 plane = (0, 1) centerCoords = self.getCenter() new_voxels = np.rot90(self.voxels, times, axes=plane) new_model = VoxelModel(new_voxels, self.materials, self.coords, self.resolution) new_model = new_model.setCenter(centerCoords) return new_model
def round(self, toNearest: float = 0.1)
-
Round material percentages to nearest multiple of an input value.
Args
toNearest
- Value to round to
Returns
VoxelModel
Expand source code
def round(self, toNearest: float = 0.1): """ Round material percentages to nearest multiple of an input value. Args: toNearest: Value to round to Returns: VoxelModel """ new_materials = np.copy(self.materials) new_model = VoxelModel.copy(self) mult = new_materials / toNearest floorDiff = np.round(abs(mult - np.floor(mult)), 10) ceilDiff = np.round(abs(mult - np.ceil(mult)), 10) new_materials[floorDiff < ceilDiff] = toNearest * np.floor(mult[floorDiff < ceilDiff]) new_materials[floorDiff >= ceilDiff] = toNearest * np.ceil(mult[floorDiff >= ceilDiff]) new_model.materials = new_materials return new_model
def saveVF(self, filename: str)
-
Save model data to a .vf file
The native VoxelFuse file format stores the same information as the attributes of a VoxelModel object. This includes geometry and material mixture data. Material attributes remain stored in the materials.py file, so the same version of this file must be used when saving and opening models. The .vf file type can be reopened by a VoxelFuse script.
Example:
modelResult.saveVF("test-file")
Args
filename
- File name
Returns
None
Expand source code
def saveVF(self, filename: str): """ Save model data to a .vf file The native VoxelFuse file format stores the same information as the attributes of a VoxelModel object. This includes geometry and material mixture data. Material attributes remain stored in the materials.py file, so the same version of this file must be used when saving and opening models. The .vf file type can be reopened by a VoxelFuse script. ---- Example: ``modelResult.saveVF("test-file")`` ---- Args: filename: File name Returns: None """ f = open(filename+'.vf', 'w+') print('Saving file: ' + f.name) x_coord = self.coords[0] y_coord = self.coords[1] z_coord = self.coords[2] writeOpen(f, 'coords') f.write(str(x_coord) + ',' + str(y_coord) + ',' + str(z_coord) + ',\n') writeClos(f, 'coords') writeOpen(f, 'resolution') f.write(str(self.resolution) + '\n') writeClos(f, 'resolution') writeOpen(f, 'materials') for r in range(len(self.materials[:,0])): # tqdm(range(len(self.materials[:,0])), desc='Writing materials'): for c in range(len(self.materials[0,:])): f.write(str(self.materials[r,c]) + ',') f.write('\n') writeClos(f, 'materials') x_len = self.voxels.shape[0] y_len = self.voxels.shape[1] z_len = self.voxels.shape[2] writeOpen(f, 'size') f.write(str(x_len) + ',' + str(y_len) + ',' + str(z_len) + ',\n') writeClos(f, 'size') writeOpen(f, 'voxels') for x in range(x_len): # tqdm(range(x_len), desc='Writing voxels'): for z in range(z_len): for y in range(y_len): f.write(str(int(self.voxels[x,y,z])) + ',') f.write(';') f.write('\n') writeClos(f, 'voxels') writeOpen(f, 'components') f.write(str(self.numComponents) + '\n') writeClos(f, 'components') if self.numComponents > 0: writeOpen(f, 'labels') for x in range(x_len): # tqdm(range(x_len), desc='Writing components'): for z in range(z_len): for y in range(y_len): f.write(str(int(self.components[x,y,z])) + ',') f.write(';') f.write('\n') writeClos(f, 'labels') f.close()
def saveVXC(self, filename: str, compression: bool = False)
-
Save model data to a .vxc file
The VoxCad file format stores geometry and full material palette data. The material palette includes the properties for each material and material mixtures are converted into distinct palette entries.
This format supports compression for the voxel data. Enabling compression allows for larger models, but it may introduce geometry errors that particularly affect small models.
The .vxc file type can be opened using VoxCad simulation software. However, it cannot currently be reopened by a VoxelFuse script.
Example:
modelResult.saveVXC("test-file", compression=False)
Args
filename
- File name
compression
- Enable/disable voxel data compression
Returns
None
Expand source code
def saveVXC(self, filename: str, compression: bool = False): """ Save model data to a .vxc file The VoxCad file format stores geometry and full material palette data. The material palette includes the properties for each material and material mixtures are converted into distinct palette entries. This format supports compression for the voxel data. Enabling compression allows for larger models, but it may introduce geometry errors that particularly affect small models. The .vxc file type can be opened using VoxCad simulation software. However, it cannot currently be reopened by a VoxelFuse script. ---- Example: ``modelResult.saveVXC("test-file", compression=False)`` ---- Args: filename: File name compression: Enable/disable voxel data compression Returns: None """ f = open(filename + '.vxc', 'w+') print('Saving file: ' + f.name) empty_model = VoxelModel.empty((1,1,1), resolution=self.resolution) export_model = (VoxelModel.copy(self).fitWorkspace()) | empty_model # Fit workspace and union with an empty object at the origin to clear offsets if object is raised export_model.coords = (0, 0, 0) # Set coords to zero to move object to origin if it is at negative coordinates writeHeader(f, '1.0', 'ISO-8859-1') export_model.writeVXCData(f, compression) f.close()
def scale(self, factor: float, adjustResolution: bool = True)
-
Scale a model.
If adjustResolution is enabled, the resolution attribute of the model will also be multiplied by the scaling factor. Enable adjustResolution if using this operation to change the resolution of a model. Disable adjustResolution if using this operation to change the size of a model.
Args
factor
- Scale factor
adjustResolution
- Enable/disable automatic resolution adjustment
Returns
VoxelModel
Expand source code
def scale(self, factor: float, adjustResolution: bool = True): """ Scale a model. If adjustResolution is enabled, the resolution attribute of the model will also be multiplied by the scaling factor. Enable adjustResolution if using this operation to change the resolution of a model. Disable adjustResolution if using this operation to change the size of a model. Args: factor: Scale factor adjustResolution: Enable/disable automatic resolution adjustment Returns: VoxelModel """ model = self.fitWorkspace() x_len = int(model.voxels.shape[0] * factor) y_len = int(model.voxels.shape[1] * factor) z_len = int(model.voxels.shape[2] * factor) new_voxels = np.zeros((x_len, y_len, z_len)) for x in tqdm(range(x_len), desc='Scaling'): for y in range(y_len): for z in range(z_len): x_source = int(((x+1) / x_len) * (model.voxels.shape[0]-1)) y_source = int(((y+1) / y_len) * (model.voxels.shape[1]-1)) z_source = int(((z+1) / z_len) * (model.voxels.shape[2]-1)) new_voxels[x,y,z] = model.voxels[x_source, y_source, z_source] model.voxels = new_voxels.astype(dtype=np.uint16) model = model.setCoords(model.coords) if adjustResolution: model.resolution = model.resolution * factor return model
def scaleNull(self)
-
Scale null material values to make all voxels contain 100% material.
Voxels that contained less than 100% material will contain the same material percentages as before, but will have varying density. Voxels that contained greater than 100% material will be scaled using scaleValues().
Example:
model2 = model1.scaleNull()
Returns
VoxelModel
Expand source code
def scaleNull(self): """ Scale null material values to make all voxels contain 100% material. Voxels that contained less than 100% material will contain the same material percentages as before, but will have varying density. Voxels that contained greater than 100% material will be scaled using scaleValues(). ---- Example: ``model2 = model1.scaleNull()`` ___ Returns: VoxelModel """ new_model = self.removeNegatives() material_sums = np.sum(new_model.materials[:, 1:], 1) material_sums = np.ones(np.shape(material_sums)) - material_sums material_sums[material_sums < 0] = 0 new_model.materials[:,1] = np.multiply(material_sums, new_model.materials[:,0]) new_model = new_model.scaleValues() return new_model
def scaleToSize(self, size: Tuple[int, int, int])
-
Scale a model to fit the given dimensions.
Args
size
- Target dimensions in voxels
Returns
VoxelModel
Expand source code
def scaleToSize(self, size: Tuple[int, int, int]): """ Scale a model to fit the given dimensions. Args: size: Target dimensions in voxels Returns: VoxelModel """ model = self.fitWorkspace() new_voxels = np.zeros(size) for x in tqdm(range(size[0]), desc='Scaling'): for y in range(size[1]): for z in range(size[2]): x_source = int(((x+1) / size[0]) * (model.voxels.shape[0]-1)) y_source = int(((y+1) / size[1]) * (model.voxels.shape[1]-1)) z_source = int(((z+1) / size[2]) * (model.voxels.shape[2]-1)) new_voxels[x,y,z] = model.voxels[x_source, y_source, z_source] model.voxels = new_voxels.astype(dtype=np.uint16) new_model = model.setCoords(model.coords) return new_model
def scaleValues(self)
-
Scale nonzero material values to make all voxels contain 100% material while maintaining the ratio between materials.
Example:
model2 = model1.scaleValues()
Returns
VoxelModel
Expand source code
def scaleValues(self): """ Scale nonzero material values to make all voxels contain 100% material while maintaining the ratio between materials. ---- Example: ``model2 = model1.scaleValues()`` ___ Returns: VoxelModel """ new_model = self.removeNegatives() material_sums = np.sum(new_model.materials[:, 1:], 1) material_sums[material_sums == 0] = 1 material_sums = np.repeat(material_sums[..., None], len(self.materials[0])-1, axis=1) new_model.materials[:,1:] = np.divide(new_model.materials[:,1:], material_sums) return new_model
def setCenter(self, coords: Tuple[float, float, float])
-
Set the center of a model to the specified coordinates.
Args
coords
- Target coordinates in voxels
Returns
VoxelModel
Expand source code
def setCenter(self, coords: Tuple[float, float, float]): """ Set the center of a model to the specified coordinates. Args: coords: Target coordinates in voxels Returns: VoxelModel """ new_model = self.fitWorkspace() x_new = int(round(coords[0] - (new_model.voxels.shape[0] / 2))) y_new = int(round(coords[1] - (new_model.voxels.shape[1] / 2))) z_new = int(round(coords[2] - (new_model.voxels.shape[2] / 2))) new_model.coords = (x_new, y_new, z_new) return new_model
def setCoords(self, coords: Tuple[int, int, int])
-
Set the origin of a model to the specified coordinates.
Args
coords
- Target coordinates in voxels
Returns
VoxelModel
Expand source code
def setCoords(self, coords: Tuple[int, int, int]): """ Set the origin of a model to the specified coordinates. Args: coords: Target coordinates in voxels Returns: VoxelModel """ new_model = self.fitWorkspace() new_model.coords = coords return new_model
def setDensity(self, density: float = 1.0)
-
Set the density of all voxels.
Args
density
- Target density value
Returns
VoxelModel
Expand source code
def setDensity(self, density: float = 1.0): """ Set the density of all voxels. Args: density: Target density value Returns: VoxelModel """ new_model = self.clearNull() new_model = new_model.scaleValues() null_material_values = np.multiply(np.ones(np.shape(new_model.materials[1:,1])), 1-density) new_model.materials[1:, 1] = null_material_values new_model.materials[1:, 2:] = np.multiply(new_model.materials[1:, 2:], density) return new_model
def setMaterial(self, material: int)
-
Set the material of all voxels in a model.
Example:
model2 = model1.getBoundingBox()
model3 = model2.setMaterial(2)
Args
material
- Material id corresponding to materials.py
Returns
VoxelModel
Expand source code
def setMaterial(self, material: int): """ Set the material of all voxels in a model. ---- Example: ``model2 = model1.getBoundingBox()`` ``model3 = model2.setMaterial(2)`` ---- Args: material: Material id corresponding to materials.py Returns: VoxelModel """ new_voxels = self.getOccupied().voxels # Converts input model to a mask, no effect if input is already a mask material_vector = np.zeros(self.materials.shape[1], dtype=np.float32) material_vector[0] = 1 material_vector[material+1] = 1 a = np.zeros(self.materials.shape[1], dtype=np.float32) b = material_vector m = np.vstack((a, b)) return VoxelModel(new_voxels, m, self.coords, self.resolution)
def setMaterialVector(self, material_vector)
-
Set the material of all voxels in a model.
Example:
material_vector = np.zeros(len(materials) + 1)
material_vector[0] = 1 # Set a to 1
material_vector[3] = 0.3 # Set material 3 to 30%
material_vector[4] = 0.7 # Set material 4 to 70%
model2 = model1.setMaterialVector(material_vector)
Args
material_vector
- Material mixture vector, format: (a, m0, m1, … mn)
Returns
VoxelModel
Expand source code
def setMaterialVector(self, material_vector): # material input is the desired material vector """ Set the material of all voxels in a model. ---- Example: ``material_vector = np.zeros(len(materials) + 1)`` ``material_vector[0] = 1 # Set a to 1`` ``material_vector[3] = 0.3 # Set material 3 to 30%`` ``material_vector[4] = 0.7 # Set material 4 to 70%`` ``model2 = model1.setMaterialVector(material_vector)`` ---- Args: material_vector: Material mixture vector, format: (a, m0, m1, ... mn) Returns: VoxelModel """ new_voxels = self.getOccupied().voxels # Converts input model to a mask, no effect if input is already a mask a = np.zeros(len(material_vector), dtype=np.float32) b = material_vector materials = np.vstack((a, b)) return VoxelModel(new_voxels, materials, self.coords, self.resolution)
def setResolution(self, res: float)
-
Change the resolution of a model.
Args
res
- aaa
Returns
None
Expand source code
def setResolution(self, res: float): """ Change the resolution of a model. Args: res: aaa Returns: None """ new_model = VoxelModel.copy(self) new_model.resolution = res return new_model
def subtract(self, model_to_sub)
-
Find the material-wise difference of two models.
The materials of the result are calculated for each voxel by subtracting the second material vector from the first.
Example – Subtracting a voxel containing material 3 from the result of the addition example:
Voxel A = [1, 0, 0.5, 0, 0.5]
Voxel B = [1, 0, 0, 0, 1]
A - B = [1, 0, 0.5, 0, -0.5]
Remove negatives (see Cleanup Operations) → [1, 0, 0.5, 0, 0]
This operation can also be applied using the subtraction operator (-).
Examples:
model3 = model1.subtract(model2)
model3 = model1 - model2
Args
model_to_sub
- VoxelModel to subtract from self
Returns
VoxelModel
Expand source code
def subtract(self, model_to_sub): """ Find the material-wise difference of two models. The materials of the result are calculated for each voxel by subtracting the second material vector from the first. Example -- Subtracting a voxel containing material 3 from the result of the addition example: >> Voxel A = [1, 0, 0.5, 0, 0.5]\n >> Voxel B = [1, 0, 0, 0, 1]\n >> A - B = [1, 0, 0.5, 0, -0.5]\n >> Remove negatives (see Cleanup Operations) → [1, 0, 0.5, 0, 0]\n This operation can also be applied using the subtraction operator (-). ---- Examples: ``model3 = model1.subtract(model2)`` ``model3 = model1 - model2`` ---- Args: model_to_sub: VoxelModel to subtract from self Returns: VoxelModel """ checkResolution(self, model_to_sub) a, b, new_coords = alignDims(self, model_to_sub) x_len = a.shape[0] y_len = a.shape[1] z_len = a.shape[2] new_voxels = np.zeros_like(a, dtype=np.uint16) new_materials = np.zeros((1, len(self.materials[0])), dtype=np.float32) for x in range(x_len): for y in range(y_len): for z in range(z_len): i1 = int(a[x, y, z]) i2 = int(b[x, y, z]) m1 = self.materials[i1] m2 = model_to_sub.materials[i2] m = m1 - m2 m[0] = np.logical_or(m1[0], m2[0]) i = np.where(np.equal(new_materials, m).all(1))[0] if len(i) > 0: new_voxels[x, y, z] = i[0] else: new_materials = np.vstack((new_materials, m)) new_voxels[x, y, z] = len(new_materials) - 1 return VoxelModel(new_voxels, new_materials, new_coords, self.resolution)
def support(self, method: Process, r1: int = 1, r2: int = 1, plane: Axes = Axes.XY, material: int = 1)
-
Generate a model representing where support material may be added to an object as characterized by the process that is used to remove the supports.
Example:
modelResult = model1.support(Process.LASER)
Args
method
- Target support removal method, set using Process class
r1
- Parameter used to determine areas where support is ineffective based on proximity to empty regions that are inaccessible to the removal process
r2
- Desired thickness of the support material
plane
- Directions in which to add support material, set using Axes class
material
- Material index corresponding to materials.py
Returns
VoxelModel
Expand source code
def support(self, method: Process, r1: int = 1, r2: int = 1, plane: Axes = Axes.XY, material: int = 1): """ Generate a model representing where support material may be added to an object as characterized by the process that is used to remove the supports. ---- Example: ``modelResult = model1.support(Process.LASER)`` ---- Args: method: Target support removal method, set using Process class r1: Parameter used to determine areas where support is ineffective based on proximity to empty regions that are inaccessible to the removal process r2: Desired thickness of the support material plane: Directions in which to add support material, set using Axes class material: Material index corresponding to materials.py Returns: VoxelModel """ model_A = self.keepout(method, material) model_A = model_A.dilate(r2, plane).difference(model_A) model_A = model_A.difference(self.keepout(method, material).difference(self).dilate(r1, plane)) # Valid support regions return model_A
def translate(self, vector: Tuple[int, int, int])
-
Translate a model by the specified vector.
Args
vector
- Translation vector in voxels
Returns
VoxelModel
Expand source code
def translate(self, vector: Tuple[int, int, int]): """ Translate a model by the specified vector. Args: vector: Translation vector in voxels Returns: VoxelModel """ new_model = VoxelModel.copy(self) new_model.coords = (self.coords[0]+vector[0], self.coords[1]+vector[1], self.coords[2]+vector[2]) return new_model
def translateMM(self, vector: Tuple[float, float, float])
-
Translate a model by the specified vector.
Args
vector
- Translation vector in mm
Returns
VoxelModel
Expand source code
def translateMM(self, vector: Tuple[float, float, float]): """ Translate a model by the specified vector. Args: vector: Translation vector in mm Returns: VoxelModel """ xV = int(round(vector[0] * self.resolution)) yV = int(round(vector[1] * self.resolution)) zV = int(round(vector[2] * self.resolution)) new_model = self.translate((xV, yV, zV)) return new_model
def union(self, model_to_add)
-
Find the geometric union of two models.
The materials from self will take priority in overlapping areas of the resulting model. This operation can also be applied using the OR operator (|)
Examples:
model3 = model1.union(model2)
model3 = model1 | model2
Args
model_to_add
- VoxelModel to union with self
Returns
VoxelModel
Expand source code
def union(self, model_to_add): """ Find the geometric union of two models. The materials from self will take priority in overlapping areas of the resulting model. This operation can also be applied using the OR operator (|) ---- Examples: ``model3 = model1.union(model2)`` ``model3 = model1 | model2`` ---- Args: model_to_add: VoxelModel to union with self Returns: VoxelModel """ checkResolution(self, model_to_add) materials = np.vstack((self.materials, model_to_add.materials[1:])) a, b, new_coords = alignDims(self, model_to_add) i_offset = len(self.materials) - 1 b = b + i_offset b[b == i_offset] = 0 # Paper uses a symmetric difference operation combined with the left/right intersection # A condensed version of this operation is used here for code simplicity mask = np.array(a == 0, dtype=np.bool_) new_voxels = np.multiply(b, mask) new_voxels = new_voxels + a # material from left model takes priority return VoxelModel(new_voxels, materials, new_coords, self.resolution)
def userSupport(self, support_model, method: Process, r1: int = 1, r2: int = 1, plane: Axes = Axes.XY, material: int = -1)
-
Generate a model representing the intersection of the supportable region and a user support model.
Example:
modelResult = model1.userSupport(model2, Process.LASER)
Args
support_model
- User provided support model
method
- Target support removal method, set using Process class
r1
- Parameter used to determine areas where support is ineffective based on proximity to empty regions that are inaccessible to the removal process
r2
- Desired thickness of the support material
plane
- Directions in which to add support material, set using Axes class
material
- Material index corresponding to materials.py, set to -1 to use support model material
Returns
VoxelModel
Expand source code
def userSupport(self, support_model, method: Process, r1: int = 1, r2: int = 1, plane: Axes = Axes.XY, material: int = -1): """ Generate a model representing the intersection of the supportable region and a user support model. ---- Example: ``modelResult = model1.userSupport(model2, Process.LASER)`` ---- Args: support_model: User provided support model method: Target support removal method, set using Process class r1: Parameter used to determine areas where support is ineffective based on proximity to empty regions that are inaccessible to the removal process r2: Desired thickness of the support material plane: Directions in which to add support material, set using Axes class material: Material index corresponding to materials.py, set to -1 to use support model material Returns: VoxelModel """ if material > -1: model_A = self.support(method, r1, r2, plane) model_A = support_model.intersection(model_A) else: model_A = self.support(method, r1, r2, plane, material) model_A = model_A.intersection(support_model) return model_A
def web(self, method: Process, r1: int = 1, r2: int = 1, layer: int = -1, material=1)
-
Generate a model representing the scrap material surrounding a model.
Web can be used in the creation of supports or layer alignment fixtures.
Example:
modelResult = model1.web(Process.LASER, 1, 5)
Args
method
- Target web removal method, set using Process class
r1
- Distance from surface of part to inside of web in
r2
- Width of web in voxels
layer
- Voxel layer at which to generate web, set to -1 to generate for all layers
material
- Material index corresponding to materials.py
Returns
VoxelModel
Expand source code
def web(self, method: Process, r1: int = 1, r2: int = 1, layer: int = -1, material = 1): """ Generate a model representing the scrap material surrounding a model. Web can be used in the creation of supports or layer alignment fixtures. ---- Example: ``modelResult = model1.web(Process.LASER, 1, 5)`` ---- Args: method: Target web removal method, set using Process class r1: Distance from surface of part to inside of web in r2: Width of web in voxels layer: Voxel layer at which to generate web, set to -1 to generate for all layers material: Material index corresponding to materials.py Returns: VoxelModel """ model_A = self.keepout(method, material) if layer != -1: model_A = model_A.isolateLayer(layer) model_A = model_A.dilate(r1, Axes.XY) model_A = model_A.dilate(r2, Axes.XY).getBoundingBox().difference(model_A) return model_A
def writeVXCData(self, f:
, compression: bool = False) -
Write geometry and material data to a text file using the .vxc format.
The VXC/VXA format stores geometry as a 3D grid of single-digit decimal numbers. As such, it is limited to 9 distinct materials.
Args
f
- File to write to
compression
- Enable/disable voxel data compression
Returns
None
Expand source code
def writeVXCData(self, f: TextIO, compression: bool = False): """ Write geometry and material data to a text file using the .vxc format. The VXC/VXA format stores geometry as a 3D grid of single-digit decimal numbers. As such, it is limited to 9 distinct materials. Args: f: File to write to compression: Enable/disable voxel data compression Returns: None """ if len(self.materials[:, 0]) > 10: f.close() os.remove(f.name) raise ValueError('The VXC/VXA file format supports a maximum of 9 distinct materials') writeOpen(f, 'VXC Version="' + str(0.94) + '"', 0) # Lattice settings writeOpen(f, 'Lattice', 1) writeData(f, 'Lattice_Dim', (1 / self.resolution) * 0.001, 2) writeData(f, 'X_Dim_Adj', 1, 2) writeData(f, 'Y_Dim_Adj', 1, 2) writeData(f, 'Z_Dim_Adj', 1, 2) writeData(f, 'X_Line_Offset', 0, 2) writeData(f, 'Y_Line_Offset', 0, 2) writeData(f, 'X_Layer_Offset', 0, 2) writeData(f, 'Y_Layer_Offset', 0, 2) writeClos(f, 'Lattice', 1) # Voxel settings writeOpen(f, 'Voxel', 1) writeData(f, 'Vox_Name', 'BOX', 2) writeData(f, 'X_Squeeze', 1, 2) writeData(f, 'Y_Squeeze', 1, 2) writeData(f, 'Z_Squeeze', 1, 2) writeClos(f, 'Voxel', 1) # Materials writeOpen(f, 'Palette', 1) for row in range(1, len(self.materials[:, 0])): # tqdm(range(1, len(self.materials[:, 0])), desc='Writing materials'): avgProps = self.getMaterialProperties(row) writeOpen(f, 'Material ID="' + str(row) + '"', 2) writeData(f, 'MatType', 0, 3) writeData(f, 'Name', avgProps['name'][0:-1], 3) writeOpen(f, 'Display', 3) writeData(f, 'Red', avgProps['r'], 4) writeData(f, 'Green', avgProps['g'], 4) writeData(f, 'Blue', avgProps['b'], 4) writeData(f, 'Alpha', 1, 4) writeClos(f, 'Display', 3) writeOpen(f, 'Mechanical', 3) if int(avgProps['MM']) == 3: current_ss_data = self.getSSData(row) if current_ss_data is not None: writeData(f, 'MatModel', 3, 4) writeOpen(f, 'SSData', 4) writeData(f, 'NumDataPts', len(current_ss_data['strain']), 5) writeOpen(f, 'StrainData', 5) for point in current_ss_data['strain']: writeData(f, 'Strain', point, 6) writeClos(f, 'StrainData', 5) writeOpen(f, 'StressData', 5) for point in current_ss_data['stress']: writeData(f, 'Stress', point, 6) writeClos(f, 'StressData', 5) writeClos(f, 'SSData', 4) else: writeData(f, 'MatModel', 0, 4) else: writeData(f, 'MatModel', avgProps['MM'], 4) writeData(f, 'Elastic_Mod', avgProps['E'], 4) writeData(f, 'Plastic_Mod', avgProps['Z'], 4) writeData(f, 'Yield_Stress', avgProps['eY'], 4) writeData(f, 'FailModel', int(avgProps['FM']), 4) writeData(f, 'Fail_Stress', avgProps['eF'], 4) writeData(f, 'Fail_Strain', avgProps['SF'], 4) writeData(f, 'Density', avgProps['p'] * 1e3, 4) writeData(f, 'Poissons_Ratio', avgProps['v'], 4) writeData(f, 'CTE', avgProps['CTE'], 4) writeData(f, 'MaterialTempPhase', avgProps['TP'], 4) writeData(f, 'uStatic', avgProps['uS'], 4) writeData(f, 'uDynamic', avgProps['uD'], 4) if int(avgProps['HG']) == 1: current_hg_model = self.getHGModel(row) if current_hg_model is not None: writeData(f, 'HydrogelModel', 1, 4) writeClos(f, 'Mechanical', 3) writeOpen(f, 'Hydrogel', 3) writeData(f, 'Name', current_hg_model['name'], 4) writeData(f, 'VoxelDim', current_hg_model['test_voxel_dim'], 4) writeData(f, 'IdealDisplacement', current_hg_model['ideal_displacement'], 4) writeData(f, 'TestDisplacement', current_hg_model['test_displacement'], 4) writeData(f, 'TimeStepCorrection', current_hg_model['test_time_step'], 4) writeData(f, 'KpRising', current_hg_model['kp_rising'], 4) writeData(f, 'KpFalling', current_hg_model['kp_falling'], 4) writeData(f, 'MaxTemp', current_hg_model['ideal_max_temp'], 4) writeData(f, 'MinTemp', current_hg_model['ideal_min_temp'], 4) writeData(f, 'TestMax', current_hg_model['test_max_temp'], 4) writeData(f, 'TestMin', current_hg_model['test_min_temp'], 4) writeData(f, 'C0', current_hg_model['c0'], 4) writeData(f, 'C1', current_hg_model['c1'], 4) writeData(f, 'C2', current_hg_model['c2'], 4) writeData(f, 'C3', current_hg_model['c3'], 4) writeData(f, 'C4', current_hg_model['c4'], 4) writeData(f, 'C5', current_hg_model['c5'], 4) writeClos(f, 'Hydrogel', 3) else: writeData(f, 'HydrogelModel', 0, 4) writeClos(f, 'Mechanical', 3) else: writeData(f, 'HydrogelModel', 0, 4) writeClos(f, 'Mechanical', 3) writeClos(f, 'Material', 2) writeClos(f, 'Palette', 1) # Structure if compression: writeOpen(f, 'Structure Compression="ZLIB"', 1) else: writeOpen(f, 'Structure Compression="ASCII_READABLE"', 1) x_len = self.voxels.shape[0] y_len = self.voxels.shape[1] z_len = self.voxels.shape[2] writeData(f, 'X_Voxels', x_len, 2) writeData(f, 'Y_Voxels', y_len, 2) writeData(f, 'Z_Voxels', z_len, 2) writeOpen(f, 'Data', 2) for z in range(z_len): # tqdm(range(z_len), desc='Writing voxels'): layer = np.copy(self.voxels[:, :, z]) layer = layer.transpose() layerData = layer.flatten() layerData = layerData.astype('uint8') if compression: layerData = zlib.compress(layerData.tobytes()) layerData = base64.encodebytes(layerData) layerDataStr = str(layerData)[2:-3] else: layerDataStr = '' for vox in layerData: layerDataStr = layerDataStr + str(vox) writeData(f, 'Layer', '<![CDATA[' + layerDataStr + ']]>', 3) writeClos(f, 'Data', 2) writeClos(f, 'Structure', 1) writeClos(f, 'VXC', 0)
def xor(self, model_2)
-
Find the geometric exclusive or of two models.
This operation can also be applied using the XOR operator (^)
Examples:
model3 = model1.xor(model2)
model3 = model1 ^ model2
Args
model_2
- VoxelModel to xor with self
Returns
VoxelModel
Expand source code
def xor(self, model_2): """ Find the geometric exclusive or of two models. This operation can also be applied using the XOR operator (^) ---- Examples: ``model3 = model1.xor(model2)`` ``model3 = model1 ^ model2`` ---- Args: model_2: VoxelModel to xor with self Returns: VoxelModel """ checkResolution(self, model_2) materials = np.vstack((self.materials, model_2.materials[1:])) a, b, new_coords = alignDims(self, model_2) i_offset = len(self.materials) - 1 b = b + i_offset b[b == i_offset] = 0 mask1 = np.array(b == 0, dtype=np.bool_) mask2 = np.array(a == 0, dtype=np.bool_) new_voxels = np.multiply(a, mask1) + np.multiply(b, mask2) return VoxelModel(new_voxels, materials, new_coords, self.resolution)