-
Notifications
You must be signed in to change notification settings - Fork 0
/
png2stl.py
263 lines (199 loc) · 7.58 KB
/
png2stl.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
from pathlib import PurePath
from skimage import measure
from stl import mesh
import numpy as np
import argparse
import cv2
import os
def get_local_folder():
"""
Returns the PurePath of the project folder
"""
try:
return PurePath(os.path.dirname(os.path.realpath(__file__))) # py
except NameError:
pass
return os.path.abspath("") # ipynb
def _unpad(x, pad_width):
slices = []
for c in pad_width:
e = None if c[1] == 0 else -c[1]
slices.append(slice(c[0], e))
return x[tuple(slices)]
def preprocess (image, extrude_black=True, hmirror=False, vmirror=True, threshold=255//2):
"""
Prepares the image for the conversion
Attributes
----------
image: np.array
Numpy array containing an rgb image
extrude_black: bool
Input images usually are black (traces) on white (non-conductive surface);
the marching cube algorithm considers False as no data and True as a region
to extrude; by default, if a pixel is above the threshold it means it is
white so we set it to True (extruded) and the black area is set to False;
if extrude_black is True we need to invert the image in order to extrude
blacks instead
hmirror: bool
True if we have to horizontally mirror the image
vmirror: bool
True if we have to vertically mirror the image; this needs to be done
in order to successfully print a circuit with a SLA printer
Returns
-------
The preprocessed image ready to be converted to a 3d object
Throws
------
ValueError
if the input is not a suitable numpy array
"""
if len(image.shape) < 2 or len(image.shape) > 3:
raise ValueError(f'Invalid shape [{image.shape}] for image array')
# grayscale the image
if len(image.shape) == 3:
image = np.mean(image, axis=2, dtype=int)
# threshold the image so that we end up with only two colors, black and white
image = image > threshold
# import matplotlib.pyplot as plt
# plt.imshow(image)
# pdfs generated by cad softwares often come with a border;
# suppose the border is defined by the first value encountered;
# as soon as another value is found, the image starts
top_padding = 0
while len(np.unique(image[top_padding])) == 1:
top_padding += 1
bottom_padding = 0
while len(np.unique(image[-bottom_padding])) == 1:
bottom_padding += 1
left_padding = 0
while len(np.unique(image[:, left_padding])) == 1:
left_padding += 1
right_padding = 0
while len(np.unique(image[:, -right_padding])) == 1:
right_padding += 1
"""
# find top padding
top_padding = -1
previous_row_content = np.unique(image[0])
for i in range(1, len(image)):
current_row_content = np.unique(image[i])
if not np.array_equal(current_row_content, previous_row_content):
break
previous_row_content = current_row_content
top_padding += 1
# find bottom padding
bottom_padding = -1
previous_row_content = np.unique(image[-1])
for i in range(len(image) - 1, 0, -1):
current_row_content = np.unique(image[i])
if not np.array_equal(current_row_content, previous_row_content):
break
previous_row_content = current_row_content
bottom_padding += 1
# find left padding
left_padding = -1
previous_col_content = np.unique(image[:, 0])
for i in range(1, len(image[0])):
current_col_content = np.unique(image[:, i])
if not np.array_equal(current_col_content, previous_col_content):
break
previous_col_content = current_col_content
left_padding += 1
# find right padding
right_padding = -1
previous_col_content = np.unique(image[:, -1])
for i in range(len(image[0]) - 1, 1, -1):
current_col_content = np.unique(image[:, i])
if not np.array_equal(current_col_content, previous_col_content):
break
previous_col_content = current_col_content
right_padding += 1
"""
pad = ((top_padding, bottom_padding),(left_padding, right_padding))
# unpad
image = _unpad(image, pad)
# the marching cube algorithm considers False as no data and True as a region
# to extrude; by default, if a pixel is above the threshold it means it is
# white so we set it to True and the black area is set to False;
# if extrude_black is True we need to invert the image.
# This makes sense to me, but for some reason it is the opposite, so there you
# have a not in front of extrude_black, hope you like it
if not extrude_black:
image = ~image
"""
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
fig, ax = plt.subplots()
# plot the boolean mask with different colors for True and False
true_patch = mpatches.Patch(color='black', label='True')
false_patch = mpatches.Patch(color='white', label='False')
legend_handles = [true_patch, false_patch]
# plot the mask alongside a legend
ax.imshow(image, cmap='binary') # in the binary color map, True is black and False is white
ax.legend(handles=legend_handles, loc='upper right')
plt.show()
"""
if hmirror:
image = np.flip(image, 0)
if vmirror:
image = np.flip(image, 1)
return image
def numpy_to_stl(mask, output_shape):
"""
Converts the 3d mask into a 3d object using the marching cube
algorithm. The 3d object will have the dimensions specified
by the output_shape parameter (mm).
Attributes
----------
mask: np.array
2d nunpy array to erode into a 3d stl object
output_shape: int tuple
size in mm of the output 3d object
Returns
-------
The 3d stl object
"""
width, depth, height = output_shape
mask = np.repeat(mask[:, :, np.newaxis], 3, axis=2)
# contour of the object
mask = np.pad(mask, ((1, 1), (1, 1), (1, 1)), constant_values=True)
#import matplotlib.pyplot as plt
#plt.imshow(mask[:, :, 1])
# create the 3d meshgrid
shp = mask.shape
x = np.linspace(0, width, shp[1])
y = np.linspace(0, depth, shp[0])
z = np.linspace(0, height, shp[2])
# get vertices and faces
verts, faces, normals, values = measure.marching_cubes(mask)
# scaling
dx = np.diff(x)[0]
dy = np.diff(y)[0]
dz = np.diff(z)[0]
dr = np.array([dx, dy, dz])
verts = verts * dr
# build the 3d object
obj_3d = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype))
for i, f in enumerate(faces):
obj_3d.vectors[i] = verts[f]
return obj_3d
if __name__ == '__main__':
# ----------------------------- argument handling ---------------------------- #
parser = argparse.ArgumentParser()
# sample command: python png2stl.py path/to/png.png width(mm) height(mm) depth(mm)
parser.add_argument('filename') # e.g. ./samples/load_sharing_panelized.png
parser.add_argument('size', nargs='+', type=int) # e.g. 18 25 1
args = parser.parse_args()
size = tuple(args.size)
input_path = PurePath(args.filename)
# ----------------------------------- main ----------------------------------- #
stem = input_path.stem
folder = input_path.parent
img = cv2.imread(str(input_path))
# we have the image as a numpy array, remove external padding
# and apply other preprocessing steps
img = preprocess(img)
# convert the image to
obj_3d = numpy_to_stl(img, size) # (width, depth, height)
output_file = PurePath(folder, stem + '.stl')
obj_3d.save(str(output_file))