Spaces:
Sleeping
Sleeping
marta-marta
commited on
Commit
·
4661dbe
1
Parent(s):
6f15aad
Added to incorporate a function to export the STL files directly from the user generated inputs
Browse files- voxel_to_SDF_to_STL.py +220 -0
voxel_to_SDF_to_STL.py
ADDED
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import trimesh
|
3 |
+
from scipy.ndimage import morphology
|
4 |
+
from skimage import measure
|
5 |
+
from stl import mesh
|
6 |
+
import pyvista as pv
|
7 |
+
import streamlit as st
|
8 |
+
from stpyvista import stpyvista
|
9 |
+
import tempfile
|
10 |
+
import os
|
11 |
+
|
12 |
+
|
13 |
+
########################################################################################################################
|
14 |
+
def voxel_to_sdf(voxel, voxel_threshold, pad_size=5):
|
15 |
+
"""
|
16 |
+
:param voxel: - a continuous or binary 3D array of values that range [0,1]
|
17 |
+
:param voxel_threshold: - the value that is used to threshold the array and convert it into a binary array,
|
18 |
+
a larger value will restrict the array to more concrete values but will produce a rougher shape.
|
19 |
+
:param pad_size: - the size of padding to encompass the array to ensure the body is internal to the SDF
|
20 |
+
:return: sdf - a signed distance array, where -'s are the internal components and +'s and the external components
|
21 |
+
"""
|
22 |
+
# Convert a 3D voxel array to a 3D SDF
|
23 |
+
# Where voxel is a continuous array [0,1]
|
24 |
+
# Pad the voxel array with extra border elements
|
25 |
+
padded_voxel = np.pad(voxel, pad_size, mode='constant', constant_values=0)
|
26 |
+
|
27 |
+
# The values are switched, since SDF considers +'s to be outside the body and -'s internal to the body
|
28 |
+
binary_voxel_data = np.where(np.array(padded_voxel) > voxel_threshold, 0, 1)
|
29 |
+
|
30 |
+
# Calculate the Signed Distance Field (SDF)
|
31 |
+
sdf = morphology.distance_transform_edt(binary_voxel_data) - 0.5
|
32 |
+
|
33 |
+
# Plot Binary Data to see how it looks
|
34 |
+
# voxel_plot = pv.wrap(np.array(voxel))
|
35 |
+
# voxel_plot.plot(volume=True)
|
36 |
+
|
37 |
+
# Plot the SDF to see how it looks before proceeding
|
38 |
+
|
39 |
+
# HOW TO PLOT THE SDF IN STREAMLIT
|
40 |
+
# # Create a PyVista grid from the SDF
|
41 |
+
# grid = pv.wrap(sdf)
|
42 |
+
# # # Plot the grid as a volume rendering
|
43 |
+
# # grid.plot(volume=True)
|
44 |
+
#
|
45 |
+
# # Set up plotter
|
46 |
+
# plotter = pv.Plotter(window_size=[600,600])
|
47 |
+
# plotter.add_volume(grid, opacity='linear')
|
48 |
+
# plotter.view_zy()
|
49 |
+
#
|
50 |
+
# # Pass the plotter (not the mesh) to stpyvista
|
51 |
+
# stpyvista(plotter)
|
52 |
+
|
53 |
+
return sdf
|
54 |
+
|
55 |
+
|
56 |
+
########################################################################################################################
|
57 |
+
def single_body_check(sdf, threshold_divisor):
|
58 |
+
"""
|
59 |
+
:param sdf: - a signed distance array, where -'s are the internal components and +'s and the external components
|
60 |
+
:return: boolean - indicates whether the SDF is a single body, which can be converted into an STL file
|
61 |
+
"""
|
62 |
+
# This function is intended to be used to determine whether the sdf contains more than one body prior to converting to 3MF
|
63 |
+
|
64 |
+
# Determine the SDF range (minimum and maximum values)
|
65 |
+
sdf_min = np.min(sdf)
|
66 |
+
sdf_max = np.max(sdf)
|
67 |
+
|
68 |
+
# Set the surface level within the SDF range (or close to zero for the surface)
|
69 |
+
surface_level = (sdf_min + sdf_max) / threshold_divisor # For example, set it to the middle of the range
|
70 |
+
|
71 |
+
# Threshold the SDF to identify regions close to the surface (threshold_value may vary based on your data)
|
72 |
+
threshold_value = 0.05
|
73 |
+
surface_regions = (np.abs(sdf) <= surface_level)
|
74 |
+
|
75 |
+
# Perform connected component analysis on the thresholded SDF to label individual bodies
|
76 |
+
connected_components = measure.label(surface_regions)
|
77 |
+
|
78 |
+
# Count the number of connected components to determine the number of bodies
|
79 |
+
num_labels = np.max(connected_components)
|
80 |
+
|
81 |
+
# Count the number of connected components to determine the number of bodies
|
82 |
+
if num_labels == 1:
|
83 |
+
print("SDF contains single body, processing into STL")
|
84 |
+
return True
|
85 |
+
else:
|
86 |
+
st.warning(f"ERROR: SDF was not a single body, contained {num_labels} bodies, therefore model requires further "
|
87 |
+
f"modification before proceeding. Recommended to start by changing threshold value for SDF (3), "
|
88 |
+
f"or restart with modifying the image threshold (2).")
|
89 |
+
return False
|
90 |
+
|
91 |
+
|
92 |
+
########################################################################################################################
|
93 |
+
def sdf_to_stl(sdf, threshold_divsor):
|
94 |
+
"""
|
95 |
+
:param sdf: - a signed distance array, where -'s are the internal components and +'s and the external components
|
96 |
+
:param filename_stl: - the file name desired for the STL
|
97 |
+
:param threshold_divsor: - this divisor determines the threshold for the SDF to be converted into the STL.
|
98 |
+
A lower threshold will result in a lower resolution model with more spherical shapes,
|
99 |
+
and a higher threshold will produce rugged shapes that better match the original voxel format
|
100 |
+
:return: stl file - an STL file is saved based on the SDF provided, it is also displayed in the IDE
|
101 |
+
"""
|
102 |
+
# Determine the SDF range (minimum and maximum values)
|
103 |
+
sdf_min = np.min(sdf)
|
104 |
+
sdf_max = np.max(sdf)
|
105 |
+
|
106 |
+
# Set the surface level within the SDF range (or close to zero for the surface)
|
107 |
+
surface_level = (sdf_min + abs(sdf_max)) / threshold_divsor # For example, set it to the middle of the range
|
108 |
+
|
109 |
+
# Generate a 3D surface mesh using marching cubes from the SDF
|
110 |
+
vertices, triangles, _, _ = measure.marching_cubes(sdf, level=surface_level)
|
111 |
+
|
112 |
+
'''
|
113 |
+
# Display resulting triangular mesh using Matplotlib. This can also be done
|
114 |
+
# with mayavi (see skimage.measure.marching_cubes docstring).
|
115 |
+
fig = plt.figure(figsize=(10, 10))
|
116 |
+
ax = fig.add_subplot(111, projection='3d')
|
117 |
+
|
118 |
+
# Fancy indexing: `verts[faces]` to generate a collection of triangles
|
119 |
+
mesh = Poly3DCollection(vertices[triangles])
|
120 |
+
mesh.set_edgecolor('k')
|
121 |
+
ax.add_collection3d(mesh)
|
122 |
+
|
123 |
+
ax.set_xlim(0, 50) # a = 6 (times two for 2nd ellipsoid)
|
124 |
+
ax.set_ylim(0, 50) # b = 10
|
125 |
+
ax.set_zlim(0, 50) # c = 16
|
126 |
+
|
127 |
+
plt.tight_layout()
|
128 |
+
plt.show()
|
129 |
+
'''
|
130 |
+
|
131 |
+
# Create an STL mesh object
|
132 |
+
stl_mesh = mesh.Mesh(np.zeros(triangles.shape[0], dtype=mesh.Mesh.dtype))
|
133 |
+
for i, triangle in enumerate(triangles):
|
134 |
+
for j in range(3):
|
135 |
+
stl_mesh.vectors[i][j] = vertices[triangle[j], :]
|
136 |
+
|
137 |
+
|
138 |
+
# # Save the STL mesh to a file
|
139 |
+
# stl_mesh.save(filename_stl)
|
140 |
+
#
|
141 |
+
# # Load the STL file using trimesh
|
142 |
+
# stl = trimesh.load(filename_stl)
|
143 |
+
#
|
144 |
+
# # Show the mesh using trimesh's built-in viewer
|
145 |
+
# stl.show()
|
146 |
+
|
147 |
+
|
148 |
+
# # Set up plotter
|
149 |
+
# plotter = pv.Plotter(window_size=[600, 600])
|
150 |
+
# plotter.add_mesh(stl_mesh, color='teal')
|
151 |
+
|
152 |
+
# Create a PyVista mesh from the stl_mesh
|
153 |
+
pyvista_mesh = pv.PolyData(vertices)
|
154 |
+
# pyvista_mesh = pv.PolyData(stl_mesh.vectors.copy().reshape(-1, 3))
|
155 |
+
faces = np.hstack([np.full((len(triangles), 1), 3), triangles])
|
156 |
+
pyvista_mesh.faces = faces
|
157 |
+
|
158 |
+
# # Optionally, you can assign a color to the mesh
|
159 |
+
# pyvista_mesh['color'] = [0.5, 0.5, 1.0] # RGB color, e.g., teal
|
160 |
+
|
161 |
+
# Create a plotter and add the PyVista mesh to it
|
162 |
+
plotter = pv.Plotter(window_size=[600, 600])
|
163 |
+
plotter.add_mesh(pyvista_mesh, style='wireframe', show_edges=True, lighting=True, edge_color='black')
|
164 |
+
plotter.background_color = 'white'
|
165 |
+
plotter.view_zy() # if mesh_2D is on the xy plane.
|
166 |
+
|
167 |
+
# # Show the plot
|
168 |
+
# plotter.show()
|
169 |
+
|
170 |
+
# Pass the plotter (not the mesh) to stpyvista
|
171 |
+
stpyvista(plotter)
|
172 |
+
|
173 |
+
# Create a temporary directory to store the STL file
|
174 |
+
temp_dir = tempfile.mkdtemp()
|
175 |
+
|
176 |
+
# Define the temporary STL file path
|
177 |
+
temp_stl_file = os.path.join(temp_dir, 'interpolation.stl')
|
178 |
+
|
179 |
+
# Save the STL data to the temporary file
|
180 |
+
stl_mesh.save(temp_stl_file)
|
181 |
+
|
182 |
+
return temp_stl_file
|
183 |
+
|
184 |
+
|
185 |
+
########################################################################################################################
|
186 |
+
# Creating a unit cell using two interpolation points
|
187 |
+
#
|
188 |
+
# def linear_interp_unit_cell(image_1, image_2, test_data, geometry_train, encoder, decoder, number_of_inputs, filename_stl):
|
189 |
+
# image_size = np.shape(test_data[image_1])[0]
|
190 |
+
# interpolation_length = int(image_size/2)
|
191 |
+
#
|
192 |
+
# # 1. Select Latent Endpoints to Interpolate Between
|
193 |
+
# sample_indices, [latent_point_1, latent_point_2] = encode_random_latent_endpoints(test_data, encoder,
|
194 |
+
# number_latent_points=2,
|
195 |
+
# sample_indices=[image_1, image_2],
|
196 |
+
# random_seed=0,
|
197 |
+
# num_inputs=number_of_inputs)
|
198 |
+
#
|
199 |
+
# # 2. Create a list of all latent points in a linear interpolation between two endpoints
|
200 |
+
# latent_interpolation_points = linear_interpolation_matrix(interpolation_length, latent_point_1, latent_point_2)
|
201 |
+
#
|
202 |
+
# # 3. Plot the results of the linear interpolation
|
203 |
+
# linear_predicted_geometry = linear_interpolation_reconstruction_plot(latent_interpolation_points,
|
204 |
+
# geometry_train, decoder,
|
205 |
+
# sample_indices[0], sample_indices[1])
|
206 |
+
#
|
207 |
+
#
|
208 |
+
# # Mirror the array along the x-axis
|
209 |
+
# mirrored_array = np.flip(linear_predicted_geometry, axis=0)
|
210 |
+
#
|
211 |
+
# # Create the combined array with shape (50, 50, 50) by concatenating the mirrored array with the original array
|
212 |
+
# single_axis_cell = np.concatenate((linear_predicted_geometry, mirrored_array), axis=0)
|
213 |
+
#
|
214 |
+
# sdf = voxel_to_sdf(single_axis_cell, 0.1)
|
215 |
+
#
|
216 |
+
# sdf_to_stl(sdf, filename_stl, 21.0)
|
217 |
+
|
218 |
+
|
219 |
+
|
220 |
+
|