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.
@@ -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()