autoforge 1.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autoforge/__init__.py +0 -0
- autoforge/__main__.py +8 -0
- autoforge/auto_forge.py +713 -0
- autoforge/filamentcolors_library.py +123 -0
- autoforge/helper_functions.py +655 -0
- autoforge-1.1.2.dist-info/METADATA +178 -0
- autoforge-1.1.2.dist-info/RECORD +10 -0
- autoforge-1.1.2.dist-info/WHEEL +5 -0
- autoforge-1.1.2.dist-info/entry_points.txt +2 -0
- autoforge-1.1.2.dist-info/top_level.txt +1 -0
autoforge/auto_forge.py
ADDED
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Script for generating 3D printed layered models from an input image.
|
|
4
|
+
|
|
5
|
+
This script uses a learned optimization with a Gumbel softmax formulation
|
|
6
|
+
to assign materials per layer and produce both a discretized composite that
|
|
7
|
+
is exported as an STL file along with swap instructions.
|
|
8
|
+
"""
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
import configargparse
|
|
12
|
+
import cv2
|
|
13
|
+
import jax
|
|
14
|
+
|
|
15
|
+
from autoforge.helper_functions import adaptive_round, gumbel_softmax, hex_to_rgb, load_materials, \
|
|
16
|
+
generate_stl, generate_swap_instructions, generate_project_file, rgb_to_lab, init_height_map, resize_image
|
|
17
|
+
|
|
18
|
+
jax.config.update("jax_enable_x64", True)
|
|
19
|
+
import jax.numpy as jnp
|
|
20
|
+
import jax.random as random
|
|
21
|
+
import optax
|
|
22
|
+
import matplotlib.pyplot as plt
|
|
23
|
+
import math
|
|
24
|
+
from tqdm import tqdm
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import numpy as np
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def composite_pixel_combined(pixel_height_logit, global_logits, tau_height, tau_global,
|
|
32
|
+
h, max_layers, material_colors, material_TDs,
|
|
33
|
+
background, gumbel_keys, mode="continuous"):
|
|
34
|
+
"""
|
|
35
|
+
Composite one pixel using either a continuous or discrete method,
|
|
36
|
+
depending on the `mode` parameter.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
pixel_height_logit: Raw logit for pixel height.
|
|
40
|
+
global_logits: Global logits per layer for material selection.
|
|
41
|
+
tau_height: Temperature parameter for height (soft printing).
|
|
42
|
+
tau_global: Temperature parameter for material selection.
|
|
43
|
+
h: Layer thickness.
|
|
44
|
+
max_layers: Maximum number of layers.
|
|
45
|
+
material_colors: Array of material colors.
|
|
46
|
+
material_TDs: Array of material transmission/opacity parameters.
|
|
47
|
+
background: Background color.
|
|
48
|
+
gumbel_keys: Random keys for sampling in each layer.
|
|
49
|
+
mode: "continuous" for soft compositing, "discrete" for hard discretization, "pruning" for pruning discretization.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Composite color for the pixel (scaled to [0,255]).
|
|
53
|
+
"""
|
|
54
|
+
# Compute continuous pixel height (in physical units)
|
|
55
|
+
pixel_height = (max_layers * h) * jax.nn.sigmoid(pixel_height_logit)
|
|
56
|
+
# Continuous number of layers
|
|
57
|
+
continuous_layers = pixel_height / h
|
|
58
|
+
# Adaptive rounding: when tau_height is high, we get a soft round; when tau_height is low (<=0.01), we get hard rounding.
|
|
59
|
+
adaptive_layers = adaptive_round(continuous_layers, tau_height, high_tau=0.1, low_tau=0.01, temp=0.1)
|
|
60
|
+
# For the forward pass we want a crisp decision; however, we want to use gradients from the adaptive value.
|
|
61
|
+
discrete_layers = jnp.round(continuous_layers) + jax.lax.stop_gradient(
|
|
62
|
+
adaptive_layers - jnp.round(continuous_layers))
|
|
63
|
+
discrete_layers = discrete_layers.astype(jnp.int32)
|
|
64
|
+
|
|
65
|
+
# Parameters for opacity calculation.
|
|
66
|
+
|
|
67
|
+
if mode == "pruning":
|
|
68
|
+
global_logits = jax.nn.one_hot(global_logits, len(material_colors))
|
|
69
|
+
|
|
70
|
+
A = 0.178763
|
|
71
|
+
k = 39.302848
|
|
72
|
+
b = 0.351177
|
|
73
|
+
|
|
74
|
+
def step_fn(carry, i):
|
|
75
|
+
comp, remaining = carry
|
|
76
|
+
# Process layers from top (last layer) to bottom (first layer)
|
|
77
|
+
j = max_layers - 1 - i
|
|
78
|
+
|
|
79
|
+
# Use a crisp (binary) decision:
|
|
80
|
+
p_print = jnp.where(j < discrete_layers, 1.0, 0.0)
|
|
81
|
+
eff_thick = p_print * h
|
|
82
|
+
|
|
83
|
+
# For material selection, force a one-hot (hard) result when tau_global is very small.
|
|
84
|
+
if mode == "discrete":
|
|
85
|
+
p_i = gumbel_softmax(global_logits[j], tau_global, gumbel_keys[j], hard=True)
|
|
86
|
+
elif mode == "continuous":
|
|
87
|
+
p_i = jax.lax.cond(
|
|
88
|
+
tau_global < 1e-3,
|
|
89
|
+
lambda _: gumbel_softmax(global_logits[j], tau_global, gumbel_keys[j], hard=True),
|
|
90
|
+
lambda _: gumbel_softmax(global_logits[j], tau_global, gumbel_keys[j], hard=False),
|
|
91
|
+
operand=None
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
p_i = global_logits[j]
|
|
95
|
+
#jax.debug.print("foo {bar}", bar=p_i)
|
|
96
|
+
color_i = jnp.dot(p_i, material_colors)
|
|
97
|
+
TD_i = jnp.dot(p_i, material_TDs) * 0.1
|
|
98
|
+
# Compute opacity
|
|
99
|
+
opac = A * jnp.log(1 + k * (eff_thick / TD_i)) + b * (eff_thick / TD_i)
|
|
100
|
+
opac = jnp.clip(opac, 0.0, 1.0)
|
|
101
|
+
new_comp = comp + remaining * opac * color_i
|
|
102
|
+
new_remaining = remaining * (1 - opac)
|
|
103
|
+
return (new_comp, new_remaining), None
|
|
104
|
+
|
|
105
|
+
init_state = (jnp.zeros(3), 1.0)
|
|
106
|
+
(comp, remaining), _ = jax.lax.scan(step_fn, init_state, jnp.arange(max_layers))
|
|
107
|
+
result = comp + remaining * background
|
|
108
|
+
return result * 255.0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def composite_image_combined(pixel_height_logits, global_logits, tau_height, tau_global, gumbel_keys,
|
|
112
|
+
h, max_layers, material_colors, material_TDs, background, mode="continuous"):
|
|
113
|
+
"""
|
|
114
|
+
Apply composite_pixel_combined over the entire image.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
pixel_height_logits: 2D array of pixel height logits.
|
|
118
|
+
global_logits: Global logits for each layer.
|
|
119
|
+
tau_height: Temperature for height compositing.
|
|
120
|
+
tau_global: Temperature for material selection.
|
|
121
|
+
gumbel_keys: Random keys per layer.
|
|
122
|
+
h: Layer thickness.
|
|
123
|
+
max_layers: Maximum number of layers.
|
|
124
|
+
material_colors: Array of material colors.
|
|
125
|
+
material_TDs: Array of material transmission/opacity parameters.
|
|
126
|
+
background: Background color.
|
|
127
|
+
mode: "continuous" or "discrete".
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The composite image (with values scaled to [0,255]).
|
|
131
|
+
"""
|
|
132
|
+
return jax.vmap(jax.vmap(
|
|
133
|
+
lambda ph_logit: composite_pixel_combined(
|
|
134
|
+
ph_logit, global_logits, tau_height, tau_global, h, max_layers,
|
|
135
|
+
material_colors, material_TDs, background, gumbel_keys, mode
|
|
136
|
+
)
|
|
137
|
+
))(pixel_height_logits)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
composite_image_combined_jit = jax.jit(composite_image_combined, static_argnums=(5,6,10))
|
|
141
|
+
|
|
142
|
+
def huber_loss(pred, target, delta=0.1):
|
|
143
|
+
"""
|
|
144
|
+
Compute the Huber loss between predictions and targets.
|
|
145
|
+
|
|
146
|
+
Parameters:
|
|
147
|
+
pred (jnp.array): Predicted values.
|
|
148
|
+
target (jnp.array): Ground-truth values.
|
|
149
|
+
delta (float): Threshold at which to change between quadratic and linear loss.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
jnp.array: The Huber loss.
|
|
153
|
+
"""
|
|
154
|
+
error = pred - target
|
|
155
|
+
abs_error = jnp.abs(error)
|
|
156
|
+
quadratic = jnp.minimum(abs_error, delta)
|
|
157
|
+
linear = abs_error - quadratic
|
|
158
|
+
return jnp.mean(0.5 * quadratic**2 + delta * linear)
|
|
159
|
+
|
|
160
|
+
def loss_fn_perceptual(params, target, tau_height, tau_global, gumbel_keys, h, max_layers, material_colors, material_TDs, background):
|
|
161
|
+
"""
|
|
162
|
+
Compute a perceptual loss between the composite and target images.
|
|
163
|
+
|
|
164
|
+
Both images are normalized to [0,1], converted to CIELAB, and then the MSE is computed.
|
|
165
|
+
"""
|
|
166
|
+
comp = composite_image_combined_jit(params['pixel_height_logits'], params['global_logits'],
|
|
167
|
+
tau_height, tau_global, gumbel_keys,
|
|
168
|
+
h, max_layers, material_colors, material_TDs, background, mode="continuous")
|
|
169
|
+
comp_norm = comp# / 255.0
|
|
170
|
+
target_norm = target# / 255.0
|
|
171
|
+
comp_lab = rgb_to_lab(comp_norm)
|
|
172
|
+
target_lab = rgb_to_lab(target_norm)
|
|
173
|
+
loss_lab = jnp.mean((comp_lab - target_lab) ** 2)
|
|
174
|
+
#jax.debug.print("hello {bar}", bar=loss_lab)
|
|
175
|
+
return huber_loss(comp, target)
|
|
176
|
+
|
|
177
|
+
def loss_fn(params, target, tau_height, tau_global, gumbel_keys, h, max_layers, material_colors, material_TDs, background):
|
|
178
|
+
"""
|
|
179
|
+
Compute the mean squared error loss between the composite and target images.
|
|
180
|
+
By default, we use continuous (soft) compositing.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
params (dict): Dictionary containing the parameters 'pixel_height_logits' and 'global_logits'.
|
|
184
|
+
target (jnp.ndarray): The target image array.
|
|
185
|
+
tau_height (float): Temperature parameter for height compositing.
|
|
186
|
+
tau_global (float): Temperature parameter for material selection.
|
|
187
|
+
gumbel_keys (jnp.ndarray): Random keys for sampling in each layer.
|
|
188
|
+
h (float): Layer thickness.
|
|
189
|
+
max_layers (int): Maximum number of layers.
|
|
190
|
+
material_colors (jnp.ndarray): Array of material colors.
|
|
191
|
+
material_TDs (jnp.ndarray): Array of material transmission/opacity parameters.
|
|
192
|
+
background (jnp.ndarray): Background color.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
jnp.ndarray: The mean squared error loss.
|
|
196
|
+
"""
|
|
197
|
+
comp = composite_image_combined_jit(params['pixel_height_logits'], params['global_logits'],
|
|
198
|
+
tau_height, tau_global, gumbel_keys,
|
|
199
|
+
h, max_layers, material_colors, material_TDs, background, mode="continuous")
|
|
200
|
+
return jnp.mean((comp - target) ** 2)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def create_update_step(optimizer, loss_function, h, max_layers, material_colors, material_TDs, background):
|
|
204
|
+
"""
|
|
205
|
+
Create a JIT-compiled update step function using the specified loss function.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
optimizer (optax.GradientTransformation): The optimizer to use for updating parameters.
|
|
209
|
+
loss_function (callable): The loss function to compute gradients.
|
|
210
|
+
h (float): Layer thickness.
|
|
211
|
+
max_layers (int): Maximum number of layers.
|
|
212
|
+
material_colors (jnp.ndarray): Array of material colors.
|
|
213
|
+
material_TDs (jnp.ndarray): Array of material transmission/opacity parameters.
|
|
214
|
+
background (jnp.ndarray): Background color.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
callable: A JIT-compiled function that performs a single update step.
|
|
218
|
+
"""
|
|
219
|
+
@jax.jit
|
|
220
|
+
def update_step(params, target, tau_height, tau_global, gumbel_keys, opt_state):
|
|
221
|
+
"""
|
|
222
|
+
Perform a single update step.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
params (dict): Dictionary containing the model parameters.
|
|
226
|
+
target (jnp.ndarray): The target image array.
|
|
227
|
+
tau_height (float): Temperature parameter for height compositing.
|
|
228
|
+
tau_global (float): Temperature parameter for material selection.
|
|
229
|
+
gumbel_keys (jnp.ndarray): Random keys for sampling in each layer.
|
|
230
|
+
opt_state (optax.OptState): The current state of the optimizer.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
tuple: A tuple containing the updated parameters, new optimizer state, and the loss value.
|
|
234
|
+
"""
|
|
235
|
+
loss_val, grads = jax.value_and_grad(loss_function)(
|
|
236
|
+
params, target, tau_height, tau_global, gumbel_keys,
|
|
237
|
+
h, max_layers, material_colors, material_TDs, background)
|
|
238
|
+
|
|
239
|
+
updates, new_opt_state = optimizer.update(grads, opt_state)
|
|
240
|
+
new_params = optax.apply_updates(params, updates)
|
|
241
|
+
|
|
242
|
+
return new_params, new_opt_state, loss_val
|
|
243
|
+
|
|
244
|
+
return update_step
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def discretize_solution_jax(params, tau_global, gumbel_keys, h, max_layers):
|
|
248
|
+
"""
|
|
249
|
+
Discretize the continuous pixel height logits into integer layer counts,
|
|
250
|
+
and force hard material selections.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
params (dict): Dictionary containing the parameters 'pixel_height_logits' and 'global_logits'.
|
|
254
|
+
tau_global (float): Temperature parameter for material selection.
|
|
255
|
+
gumbel_keys (jnp.ndarray): Random keys for sampling in each layer.
|
|
256
|
+
h (float): Layer thickness.
|
|
257
|
+
max_layers (int): Maximum number of layers.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
tuple: A tuple containing the discrete global material assignments and the discrete height image.
|
|
261
|
+
"""
|
|
262
|
+
pixel_height_logits = params['pixel_height_logits']
|
|
263
|
+
global_logits = params['global_logits']
|
|
264
|
+
pixel_heights = (max_layers * h) * jax.nn.sigmoid(pixel_height_logits)
|
|
265
|
+
discrete_height_image = jnp.round(pixel_heights / h).astype(jnp.int32)
|
|
266
|
+
discrete_height_image = jnp.clip(discrete_height_image, 0, max_layers)
|
|
267
|
+
|
|
268
|
+
def discretize_layer(logits, key):
|
|
269
|
+
p = gumbel_softmax(logits, tau_global, key, hard=True)
|
|
270
|
+
return jnp.argmax(p)
|
|
271
|
+
discrete_global = jax.vmap(discretize_layer)(global_logits, gumbel_keys)
|
|
272
|
+
return discrete_global, discrete_height_image
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def run_optimizer(rng_key, target, H, W, max_layers, h, material_colors, material_TDs, background,
|
|
278
|
+
num_iters, learning_rate, decay_v, loss_function, visualize=False,
|
|
279
|
+
output_folder=None, save_interval_pct=None,
|
|
280
|
+
img_width=None, img_height=None, background_height=None,
|
|
281
|
+
material_names=None, csv_file=None,args=None):
|
|
282
|
+
"""
|
|
283
|
+
Run the optimization loop to learn per-pixel heights and per-layer material assignments.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
rng_key (jax.random.PRNGKey): The random key for JAX operations.
|
|
287
|
+
target (jnp.ndarray): The target image array.
|
|
288
|
+
H (int): Height of the target image.
|
|
289
|
+
W (int): Width of the target image.
|
|
290
|
+
max_layers (int): Maximum number of layers.
|
|
291
|
+
h (float): Layer thickness.
|
|
292
|
+
material_colors (jnp.ndarray): Array of material colors.
|
|
293
|
+
material_TDs (jnp.ndarray): Array of material transmission/opacity parameters.
|
|
294
|
+
background (jnp.ndarray): Background color.
|
|
295
|
+
num_iters (int): Number of optimization iterations.
|
|
296
|
+
learning_rate (float): Learning rate for optimization.
|
|
297
|
+
decay_v (float): Final tau value for Gumbel-Softmax.
|
|
298
|
+
loss_function (callable): The loss function to compute gradients.
|
|
299
|
+
visualize (bool, optional): Enable visualization during optimization. Defaults to False.
|
|
300
|
+
save_max_tau (float, optional): Tau threshold to save best result. Defaults to 0.001.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
tuple: A tuple containing the best parameters and the best composite image.
|
|
304
|
+
"""
|
|
305
|
+
num_materials = material_colors.shape[0]
|
|
306
|
+
rng_key, subkey = random.split(rng_key)
|
|
307
|
+
# Initialize global_logits with a base bias and distinct per-layer bias.
|
|
308
|
+
global_logits = jnp.ones((max_layers, num_materials)) * -1.0
|
|
309
|
+
for i in range(max_layers):
|
|
310
|
+
global_logits = global_logits.at[i, i % num_materials].set(1.0)
|
|
311
|
+
global_logits += random.uniform(subkey, global_logits.shape, minval=-0.1, maxval=0.1)
|
|
312
|
+
|
|
313
|
+
rng_key, subkey = random.split(rng_key)
|
|
314
|
+
|
|
315
|
+
#pixel_height_logits = initialize_pixel_height_logits(target)
|
|
316
|
+
pixel_height_logits = init_height_map(target,max_layers,h)
|
|
317
|
+
#pixel_height_logits = random.uniform(subkey, (H, W), minval=-2.0, maxval=2.0)
|
|
318
|
+
|
|
319
|
+
params = {'global_logits': global_logits, 'pixel_height_logits': pixel_height_logits}
|
|
320
|
+
|
|
321
|
+
optimizer = optax.adam(learning_rate)
|
|
322
|
+
opt_state = optimizer.init(params)
|
|
323
|
+
update_step = create_update_step(optimizer, loss_function, h, max_layers, material_colors, material_TDs, background)
|
|
324
|
+
|
|
325
|
+
warmup_steps = num_iters // 4
|
|
326
|
+
decay_rate = -math.log(decay_v) / (num_iters - warmup_steps)
|
|
327
|
+
|
|
328
|
+
def get_tau(i, tau_init=1.0, tau_final=decay_v, decay_rate=decay_rate):
|
|
329
|
+
"""
|
|
330
|
+
Compute the tau value for the current iteration.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
i (int): Current iteration.
|
|
334
|
+
tau_init (float): Initial tau value.
|
|
335
|
+
tau_final (float): Final tau value.
|
|
336
|
+
decay_rate (float): Decay rate for tau.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
float: The computed tau value.
|
|
340
|
+
"""
|
|
341
|
+
if i < warmup_steps:
|
|
342
|
+
return tau_init
|
|
343
|
+
else:
|
|
344
|
+
return max(tau_final, tau_init * math.exp(-decay_rate * (i - warmup_steps)))
|
|
345
|
+
|
|
346
|
+
best_params = None
|
|
347
|
+
best_loss = float('inf')
|
|
348
|
+
best_params_since_last_save = None
|
|
349
|
+
best_loss_since_last_save = float('inf')
|
|
350
|
+
# Determine the checkpoint interval (in iterations) based on percentage progress.
|
|
351
|
+
checkpoint_interval = int(num_iters * save_interval_pct / 100) if save_interval_pct is not None else None
|
|
352
|
+
|
|
353
|
+
if visualize:
|
|
354
|
+
plt.ion()
|
|
355
|
+
fig, ax = plt.subplots(1, 5, figsize=(17, 6))
|
|
356
|
+
target_im = ax[0].imshow(np.array(target, dtype=np.uint8))
|
|
357
|
+
ax[0].set_title("Target Image")
|
|
358
|
+
comp_im = ax[1].imshow(np.zeros((H, W, 3), dtype=np.uint8))
|
|
359
|
+
ax[1].set_title("Current Composite (Continuous)")
|
|
360
|
+
best_comp_im = ax[2].imshow(np.zeros((H, W, 3), dtype=np.uint8))
|
|
361
|
+
ax[2].set_title("Best Composite (Continuous)")
|
|
362
|
+
height_map_im = ax[3].imshow(np.zeros((H, W)), cmap='viridis')
|
|
363
|
+
height_map_im.set_clim(0, max_layers * h)
|
|
364
|
+
ax[3].set_title("Height Map")
|
|
365
|
+
disc_comp_im = ax[4].imshow(np.zeros((H, W, 3), dtype=np.uint8))
|
|
366
|
+
ax[4].set_title("Composite (Discrete)")
|
|
367
|
+
plt.pause(0.1)
|
|
368
|
+
|
|
369
|
+
val_gumbel_keys_list = []
|
|
370
|
+
val_gumbel_keys_images = []
|
|
371
|
+
for i in range(3):
|
|
372
|
+
rng_key, subkey = random.split(rng_key)
|
|
373
|
+
val_gumbel_keys_list.append(random.split(subkey, max_layers))
|
|
374
|
+
|
|
375
|
+
tbar = tqdm(range(num_iters))
|
|
376
|
+
for i in tbar:
|
|
377
|
+
tau_height = get_tau(i, tau_init=1.0, tau_final=decay_v, decay_rate=decay_rate)
|
|
378
|
+
tau_global = get_tau(i, tau_init=1.0, tau_final=decay_v, decay_rate=decay_rate)
|
|
379
|
+
rng_key, subkey = random.split(rng_key)
|
|
380
|
+
gumbel_keys = random.split(subkey, max_layers)
|
|
381
|
+
params, opt_state, loss = update_step(params, target, tau_height, tau_global, gumbel_keys, opt_state)
|
|
382
|
+
|
|
383
|
+
val_loss_list = []
|
|
384
|
+
for j in range(3):
|
|
385
|
+
disc_comp = composite_image_combined_jit(params['pixel_height_logits'], params['global_logits'],
|
|
386
|
+
decay_v, decay_v, val_gumbel_keys_list[j],
|
|
387
|
+
h, max_layers, material_colors, material_TDs, background,
|
|
388
|
+
mode="discrete")
|
|
389
|
+
|
|
390
|
+
loss_val = jnp.mean((disc_comp - target) ** 2)
|
|
391
|
+
val_loss_list.append(loss_val)
|
|
392
|
+
val_gumbel_keys_images.append(disc_comp)
|
|
393
|
+
|
|
394
|
+
#get index of the best validation loss
|
|
395
|
+
best_val_loss_idx = jnp.argmin(jnp.array(val_loss_list))
|
|
396
|
+
loss_val = val_loss_list[best_val_loss_idx]
|
|
397
|
+
val_gumbel_keys = val_gumbel_keys_list[best_val_loss_idx]
|
|
398
|
+
val_gumbel_keys_list = [val_gumbel_keys]
|
|
399
|
+
disc_comp = val_gumbel_keys_images[best_val_loss_idx]
|
|
400
|
+
val_gumbel_keys_images = []
|
|
401
|
+
|
|
402
|
+
for _ in range(9):
|
|
403
|
+
rng_key, subkey = random.split(rng_key)
|
|
404
|
+
val_gumbel_keys_list.append(random.split(subkey, max_layers))
|
|
405
|
+
|
|
406
|
+
if loss_val < best_loss_since_last_save:
|
|
407
|
+
best_loss_since_last_save = loss_val
|
|
408
|
+
best_params_since_last_save = {k: jnp.array(np.asarray(v)).copy() for k, v in params.items()}
|
|
409
|
+
|
|
410
|
+
if loss_val < best_loss or best_params is None:
|
|
411
|
+
best_loss = loss_val
|
|
412
|
+
best_params = {k: jnp.array(np.asarray(v)).copy() for k, v in params.items()}
|
|
413
|
+
if visualize:
|
|
414
|
+
comp = composite_image_combined_jit(best_params['pixel_height_logits'], best_params['global_logits'],
|
|
415
|
+
tau_height, tau_global, gumbel_keys,
|
|
416
|
+
h, max_layers, material_colors, material_TDs, background, mode="continuous")
|
|
417
|
+
comp_np = np.clip(np.array(comp), 0, 255).astype(np.uint8)
|
|
418
|
+
best_comp_im.set_data(comp_np)
|
|
419
|
+
# For discrete composite visualization, force discrete mode.
|
|
420
|
+
|
|
421
|
+
disc_comp_np = np.clip(np.array(disc_comp), 0, 255).astype(np.uint8)
|
|
422
|
+
disc_comp_im.set_data(disc_comp_np)
|
|
423
|
+
|
|
424
|
+
if visualize and (i % 50 == 0):
|
|
425
|
+
comp = composite_image_combined_jit(params['pixel_height_logits'], params['global_logits'],
|
|
426
|
+
tau_height, tau_global, gumbel_keys,
|
|
427
|
+
h, max_layers, material_colors, material_TDs, background, mode="continuous")
|
|
428
|
+
comp_np = np.clip(np.array(comp), 0, 255).astype(np.uint8)
|
|
429
|
+
comp_im.set_data(comp_np)
|
|
430
|
+
# Update height map.
|
|
431
|
+
height_map = (max_layers * h) * jax.nn.sigmoid(best_params['pixel_height_logits'])
|
|
432
|
+
height_map_np = np.array(height_map)
|
|
433
|
+
height_map_im.set_data(height_map_np)
|
|
434
|
+
highest_layer = np.max(height_map_np)
|
|
435
|
+
fig.suptitle(f"Iteration {i}, Loss: {loss:.4f}, Best Validation Loss: {best_loss:.4f}, Tau: {tau_height:.3f}, Highest Layer: {highest_layer:.2f}mm")
|
|
436
|
+
plt.pause(0.01)
|
|
437
|
+
if checkpoint_interval is not None and (i+1) % checkpoint_interval == 0 and i > 10:
|
|
438
|
+
|
|
439
|
+
print("Saving intermediate outputs. This can take some time. You can turn off this feature by setting save_interval_pct to 0.")
|
|
440
|
+
save_intermediate_outputs(i, best_params_since_last_save, tau_global, gumbel_keys, h, max_layers,
|
|
441
|
+
material_colors, material_TDs, background,
|
|
442
|
+
output_folder, W, H, background_height, material_names, csv_file,args=args)
|
|
443
|
+
best_params_since_last_save = None
|
|
444
|
+
best_loss_since_last_save = float('inf')
|
|
445
|
+
tbar.set_description(f"loss = {loss_val:.4f}, Best Loss = {best_loss:.4f}")
|
|
446
|
+
|
|
447
|
+
if visualize:
|
|
448
|
+
plt.ioff()
|
|
449
|
+
plt.close()
|
|
450
|
+
best_comp = composite_image_combined_jit(best_params['pixel_height_logits'], best_params['global_logits'],
|
|
451
|
+
tau_height, tau_global, gumbel_keys,
|
|
452
|
+
h, max_layers, material_colors, material_TDs, background, mode="continuous")
|
|
453
|
+
return best_params, best_comp,val_gumbel_keys
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def save_intermediate_outputs(iteration, params, tau_global, gumbel_keys, h, max_layers,
|
|
457
|
+
material_colors, material_TDs, background,
|
|
458
|
+
output_folder, img_width, img_height, background_height,
|
|
459
|
+
material_names, csv_file, args):
|
|
460
|
+
import os
|
|
461
|
+
import cv2
|
|
462
|
+
import numpy as np
|
|
463
|
+
|
|
464
|
+
# Compute the discrete composite image.
|
|
465
|
+
disc_comp = composite_image_combined_jit(
|
|
466
|
+
params['pixel_height_logits'], params['global_logits'],
|
|
467
|
+
tau_global, tau_global, gumbel_keys,
|
|
468
|
+
h, max_layers, material_colors, material_TDs, background, mode="discrete")
|
|
469
|
+
discrete_comp_np = np.clip(np.array(disc_comp), 0, 255).astype(np.uint8)
|
|
470
|
+
image_filename = os.path.join(output_folder, f"intermediate_iter_{iteration}_comp.jpg")
|
|
471
|
+
cv2.imwrite(image_filename, cv2.cvtColor(discrete_comp_np, cv2.COLOR_RGB2BGR))
|
|
472
|
+
|
|
473
|
+
# Discretize the solution to obtain layer counts and material assignments.
|
|
474
|
+
disc_global, disc_height_image = discretize_solution_jax(params, tau_global, gumbel_keys, h, max_layers)
|
|
475
|
+
|
|
476
|
+
# Generate the STL file.
|
|
477
|
+
height_map_mm = (np.array(disc_height_image, dtype=np.float32)) * h
|
|
478
|
+
stl_filename = os.path.join(output_folder, f"intermediate_iter_{iteration}_model.stl")
|
|
479
|
+
generate_stl(height_map_mm, stl_filename, background_height, scale=1.0)
|
|
480
|
+
|
|
481
|
+
# Generate swap instructions.
|
|
482
|
+
background_layers = int(background_height // h)
|
|
483
|
+
swap_instructions = generate_swap_instructions(np.array(disc_global), np.array(disc_height_image),
|
|
484
|
+
h, background_layers, background_height, material_names)
|
|
485
|
+
instructions_filename = os.path.join(output_folder, f"intermediate_iter_{iteration}_swap_instructions.txt")
|
|
486
|
+
with open(instructions_filename, "w") as f:
|
|
487
|
+
for line in swap_instructions:
|
|
488
|
+
f.write(line + "\n")
|
|
489
|
+
|
|
490
|
+
# Generate the project file.
|
|
491
|
+
project_filename = os.path.join(output_folder, f"intermediate_iter_{iteration}_project.hfp")
|
|
492
|
+
generate_project_file(project_filename, args,
|
|
493
|
+
np.array(disc_global),
|
|
494
|
+
np.array(disc_height_image),
|
|
495
|
+
img_width, img_height, stl_filename, csv_file)
|
|
496
|
+
|
|
497
|
+
def pruning(target,best_params,disc_global,tau_global_disc,gumbel_keys_disc,h,max_layers,material_colors,material_TDs,background,max_loss_increase=0.05):
|
|
498
|
+
initial_image = composite_image_combined_jit(best_params['pixel_height_logits'], disc_global,
|
|
499
|
+
tau_global_disc, tau_global_disc, gumbel_keys_disc,
|
|
500
|
+
h, max_layers, material_colors, material_TDs, background, mode="pruning")
|
|
501
|
+
initial_loss = jnp.mean((initial_image - target) ** 2)
|
|
502
|
+
print(f"Initial Pruning loss: {np.asarray(initial_loss)}")
|
|
503
|
+
max_loss = initial_loss + initial_loss*max_loss_increase
|
|
504
|
+
|
|
505
|
+
def get_color_bands(disc_global):
|
|
506
|
+
color_bands = []
|
|
507
|
+
prev_color = -1
|
|
508
|
+
start_i = -1
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
for i in range(0, max_layers):
|
|
513
|
+
if disc_global[i] != prev_color:
|
|
514
|
+
if start_i != -1:
|
|
515
|
+
color_bands.append((start_i, i - 1, prev_color))
|
|
516
|
+
start_i = i
|
|
517
|
+
prev_color = disc_global[i]
|
|
518
|
+
if start_i != -1:
|
|
519
|
+
color_bands.append((start_i, max_layers - 1, prev_color))
|
|
520
|
+
return color_bands
|
|
521
|
+
|
|
522
|
+
color_bands = get_color_bands(disc_global)
|
|
523
|
+
|
|
524
|
+
# Pruning test 1 - Try removing entire color bands
|
|
525
|
+
current_bands = len(color_bands)
|
|
526
|
+
|
|
527
|
+
for _ in tqdm(range(100),desc="Pruning - Removing entire color bands"):
|
|
528
|
+
for i in range(1,len(color_bands)):
|
|
529
|
+
band = color_bands[i]
|
|
530
|
+
disc_global_copy = np.asarray(disc_global).copy()
|
|
531
|
+
disc_global_copy[band[0]:band[1]+1] = color_bands[i-1][2]
|
|
532
|
+
disc_global_copy = jnp.array(disc_global_copy)
|
|
533
|
+
new_image = composite_image_combined_jit(best_params['pixel_height_logits'], disc_global_copy,
|
|
534
|
+
tau_global_disc, tau_global_disc, gumbel_keys_disc,
|
|
535
|
+
h, max_layers, material_colors, material_TDs, background, mode="pruning")
|
|
536
|
+
new_loss = jnp.mean((new_image - target) ** 2)
|
|
537
|
+
if new_loss < max_loss:
|
|
538
|
+
disc_global = disc_global_copy
|
|
539
|
+
max_loss = new_loss
|
|
540
|
+
color_bands[i] = color_bands[i-1]
|
|
541
|
+
|
|
542
|
+
# Same as above but in reverse
|
|
543
|
+
for i in reversed(range(0,len(color_bands)-1)):
|
|
544
|
+
band = color_bands[i]
|
|
545
|
+
disc_global_copy = np.asarray(disc_global).copy()
|
|
546
|
+
disc_global_copy[band[0]:band[1]+1] = color_bands[i+1][2]
|
|
547
|
+
disc_global_copy = jnp.array(disc_global_copy)
|
|
548
|
+
new_image = composite_image_combined_jit(best_params['pixel_height_logits'], disc_global_copy,
|
|
549
|
+
tau_global_disc, tau_global_disc, gumbel_keys_disc,
|
|
550
|
+
h, max_layers, material_colors, material_TDs, background, mode="pruning")
|
|
551
|
+
new_loss = jnp.mean((new_image - target) ** 2)
|
|
552
|
+
if new_loss < max_loss:
|
|
553
|
+
disc_global = disc_global_copy
|
|
554
|
+
max_loss = new_loss
|
|
555
|
+
color_bands[i] = color_bands[i+1]
|
|
556
|
+
if current_bands == get_color_bands(disc_global):
|
|
557
|
+
print("No changes in color bands. Exiting pruning.")
|
|
558
|
+
break
|
|
559
|
+
else:
|
|
560
|
+
current_bands = get_color_bands(disc_global)
|
|
561
|
+
#calculate color band difference
|
|
562
|
+
new_color_bands = get_color_bands(disc_global)
|
|
563
|
+
print(f"Color bands before pruning: {len(color_bands)}")
|
|
564
|
+
print(f"Color bands after pruning: {len(new_color_bands)}")
|
|
565
|
+
return disc_global
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def gumbal_bruteforce(background, best_params, decay_v_value, h_value, material_TDs, material_colors, max_layers_value,
|
|
569
|
+
rng_key, target, val_gumbel_keys, iterations=10000, desc="Searching Gumbal Keys"):
|
|
570
|
+
disc_comp = composite_image_combined_jit(best_params['pixel_height_logits'], best_params['global_logits'],
|
|
571
|
+
decay_v_value, decay_v_value, val_gumbel_keys,
|
|
572
|
+
h_value, max_layers_value, material_colors, material_TDs, background,
|
|
573
|
+
mode="discrete")
|
|
574
|
+
opt_loss = jnp.mean((disc_comp - target) ** 2)
|
|
575
|
+
print(f"Initial gumbal search loss: {opt_loss}")
|
|
576
|
+
tbar = tqdm(range(iterations), desc=f"{desc} with lowest loss: {opt_loss}")
|
|
577
|
+
for _ in tbar:
|
|
578
|
+
rng_key, subkey = random.split(rng_key)
|
|
579
|
+
gumbel_keys_disc = random.split(subkey, max_layers_value)
|
|
580
|
+
disc_comp = composite_image_combined_jit(best_params['pixel_height_logits'], best_params['global_logits'],
|
|
581
|
+
decay_v_value, decay_v_value, gumbel_keys_disc,
|
|
582
|
+
h_value, max_layers_value, material_colors, material_TDs, background,
|
|
583
|
+
mode="discrete")
|
|
584
|
+
new_loss = jnp.mean((disc_comp - target) ** 2)
|
|
585
|
+
if new_loss < opt_loss:
|
|
586
|
+
opt_loss = new_loss
|
|
587
|
+
val_gumbel_keys = gumbel_keys_disc
|
|
588
|
+
tbar.set_description(f"{desc} with lowest loss: {opt_loss}")
|
|
589
|
+
return val_gumbel_keys
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def main():
|
|
593
|
+
parser = configargparse.ArgParser()
|
|
594
|
+
parser.add_argument("--config", is_config_file=True, help="Path to config file")
|
|
595
|
+
parser.add_argument("--input_image", type=str, required=True, help="Path to input image")
|
|
596
|
+
parser.add_argument("--csv_file", type=str, required=True, help="Path to CSV file with material data")
|
|
597
|
+
parser.add_argument("--output_folder", type=str, default="output", help="Folder to write outputs")
|
|
598
|
+
parser.add_argument("--iterations", type=int, default=5000, help="Number of optimization iterations")
|
|
599
|
+
parser.add_argument("--learning_rate", type=float, default=1e-2, help="Learning rate for optimization")
|
|
600
|
+
parser.add_argument("--layer_height", type=float, default=0.04, help="Layer thickness in mm")
|
|
601
|
+
parser.add_argument("--max_layers", type=int, default=75, help="Maximum number of layers")
|
|
602
|
+
parser.add_argument("--background_height", type=float, default=0.4, help="Height of the background in mm")
|
|
603
|
+
parser.add_argument("--background_color", type=str, default="#000000", help="Background color")
|
|
604
|
+
parser.add_argument("--output_size", type=int, default=1024, help="Maximum dimension for target image")
|
|
605
|
+
parser.add_argument("--solver_size", type=int, default=128, help="Maximum dimension for target image")
|
|
606
|
+
parser.add_argument("--decay", type=float, default=0.01, help="Final tau value for Gumbel-Softmax")
|
|
607
|
+
parser.add_argument("--visualize", action="store_true", help="Enable visualization during optimization")
|
|
608
|
+
parser.add_argument("--perform_gumbal_search", type=bool, default=True, help="Perform gumbal search after optimization")
|
|
609
|
+
parser.add_argument("--perform_pruning", type=bool, default=True, help="Perform pruning after optimization")
|
|
610
|
+
parser.add_argument("--save_interval_pct", type=float, default=20,help="Percentage interval to save intermediate results")
|
|
611
|
+
|
|
612
|
+
args = parser.parse_args()
|
|
613
|
+
|
|
614
|
+
os.makedirs(args.output_folder, exist_ok=True)
|
|
615
|
+
print("Output folder:", args.output_folder)
|
|
616
|
+
assert (args.background_height / args.layer_height).is_integer(), "Background height must be divisible by layer height."
|
|
617
|
+
assert args.max_layers > 1, "max_layers must be positive."
|
|
618
|
+
assert args.output_size > 0, "output_size must be positive."
|
|
619
|
+
assert args.solver_size > 0, "solver_size must be positive."
|
|
620
|
+
assert args.iterations > 0, "iterations must be positive."
|
|
621
|
+
assert args.learning_rate > 0, "learning_rate must be positive."
|
|
622
|
+
assert args.layer_height > 0, "layer_height must be positive."
|
|
623
|
+
|
|
624
|
+
h_value = args.layer_height
|
|
625
|
+
max_layers_value = args.max_layers
|
|
626
|
+
background_height_value = args.background_height
|
|
627
|
+
background_layers_value = background_height_value // h_value
|
|
628
|
+
decay_v_value = args.decay
|
|
629
|
+
|
|
630
|
+
background = jnp.array(hex_to_rgb(args.background_color), dtype=jnp.float64)
|
|
631
|
+
material_colors, material_TDs, material_names, material_hex = load_materials(args.csv_file)
|
|
632
|
+
|
|
633
|
+
img = cv2.imread(args.input_image)
|
|
634
|
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
|
635
|
+
h_img, w_img, _ = img.shape
|
|
636
|
+
|
|
637
|
+
target = resize_image(img,args.solver_size)
|
|
638
|
+
new_h, new_w, _ = target.shape
|
|
639
|
+
|
|
640
|
+
output_target = resize_image(img,args.output_size)
|
|
641
|
+
|
|
642
|
+
target = jnp.array(target, dtype=jnp.float64)
|
|
643
|
+
|
|
644
|
+
rng_key = random.PRNGKey(int(time.time()))
|
|
645
|
+
best_params, _, val_gumbel_keys = run_optimizer(
|
|
646
|
+
rng_key, target, new_h, new_w, max_layers_value, h_value,
|
|
647
|
+
material_colors, material_TDs, background,
|
|
648
|
+
args.iterations, args.learning_rate, decay_v_value,
|
|
649
|
+
loss_function=loss_fn,
|
|
650
|
+
visualize=args.visualize,
|
|
651
|
+
output_folder=args.output_folder,
|
|
652
|
+
save_interval_pct=args.save_interval_pct if args.save_interval_pct > 0 else None,
|
|
653
|
+
img_width=new_w, img_height=new_h,
|
|
654
|
+
background_height=background_height_value,
|
|
655
|
+
material_names=material_names,
|
|
656
|
+
csv_file=args.csv_file,
|
|
657
|
+
args=args
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
tau_global_disc = decay_v_value
|
|
661
|
+
|
|
662
|
+
if args.perform_gumbal_search:
|
|
663
|
+
val_gumbel_keys = gumbal_bruteforce(background, best_params, decay_v_value, h_value, material_TDs, material_colors,
|
|
664
|
+
max_layers_value, rng_key, target, val_gumbel_keys,iterations=1000,desc="Searching small size image for best gumbal keys")
|
|
665
|
+
|
|
666
|
+
if args.solver_size != args.output_size and args.perform_gumbal_search:
|
|
667
|
+
best_params["pixel_height_logits"] = init_height_map(output_target, args.max_layers, h_value)
|
|
668
|
+
|
|
669
|
+
val_gumbel_keys = gumbal_bruteforce(background, best_params, decay_v_value, h_value, material_TDs, material_colors,
|
|
670
|
+
max_layers_value, rng_key, output_target, val_gumbel_keys, iterations=1000,
|
|
671
|
+
desc="Searching large size image for best gumbal keys")
|
|
672
|
+
|
|
673
|
+
disc_global, disc_height_image = discretize_solution_jax(best_params, tau_global_disc, val_gumbel_keys, h_value, max_layers_value)
|
|
674
|
+
|
|
675
|
+
if args.perform_pruning:
|
|
676
|
+
disc_global = pruning(output_target,best_params,disc_global,tau_global_disc,val_gumbel_keys,h_value,args.max_layers,material_colors,material_TDs,background)
|
|
677
|
+
|
|
678
|
+
disc_comp = composite_image_combined_jit(best_params['pixel_height_logits'], disc_global,
|
|
679
|
+
tau_global_disc, tau_global_disc, val_gumbel_keys,
|
|
680
|
+
h_value, max_layers_value, material_colors, material_TDs, background, mode="pruning")
|
|
681
|
+
discrete_comp_np = np.clip(np.array(disc_comp), 0, 255).astype(np.uint8)
|
|
682
|
+
cv2.imwrite(os.path.join(args.output_folder, "discrete_comp.jpg"),
|
|
683
|
+
cv2.cvtColor(discrete_comp_np, cv2.COLOR_RGB2BGR))
|
|
684
|
+
|
|
685
|
+
height_map_mm = (np.array(disc_height_image, dtype=np.float32)) * h_value
|
|
686
|
+
stl_filename = os.path.join(args.output_folder, "final_model.stl")
|
|
687
|
+
generate_stl(height_map_mm, stl_filename, background_height_value, scale=1.0)
|
|
688
|
+
|
|
689
|
+
swap_instructions = generate_swap_instructions(np.array(disc_global), np.array(disc_height_image),
|
|
690
|
+
h_value, background_layers_value, background_height_value, material_names)
|
|
691
|
+
instructions_filename = os.path.join(args.output_folder, "swap_instructions.txt")
|
|
692
|
+
with open(instructions_filename, "w") as f:
|
|
693
|
+
for line in swap_instructions:
|
|
694
|
+
f.write(line + "\n")
|
|
695
|
+
|
|
696
|
+
width_mm = new_w
|
|
697
|
+
height_mm = new_h
|
|
698
|
+
|
|
699
|
+
project_filename = os.path.join(args.output_folder, "project_file.hfp")
|
|
700
|
+
generate_project_file(project_filename, args,
|
|
701
|
+
np.array(disc_global),
|
|
702
|
+
np.array(disc_height_image),
|
|
703
|
+
width_mm, height_mm, stl_filename, args.csv_file)
|
|
704
|
+
print("Project file saved to", project_filename)
|
|
705
|
+
print("All outputs saved to", args.output_folder)
|
|
706
|
+
print("Happy printing!")
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
if __name__ == '__main__':
|
|
713
|
+
main()
|