autoforge 1.4.2__tar.gz → 1.5.0__tar.gz

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.
Files changed (35) hide show
  1. {autoforge-1.4.2 → autoforge-1.5.0}/PKG-INFO +1 -1
  2. {autoforge-1.4.2 → autoforge-1.5.0}/pyproject.toml +1 -1
  3. {autoforge-1.4.2 → autoforge-1.5.0}/src/AutoForge.egg-info/PKG-INFO +1 -1
  4. {autoforge-1.4.2 → autoforge-1.5.0}/src/AutoForge.egg-info/SOURCES.txt +1 -0
  5. autoforge-1.5.0/src/autoforge/Helper/CAdamW.py +113 -0
  6. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Helper/OptimizerHelper.py +149 -36
  7. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Helper/OutputHelper.py +19 -11
  8. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Helper/PruningHelper.py +5 -9
  9. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Loss/LossFunctions.py +2 -3
  10. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Modules/Optimizer.py +33 -22
  11. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/auto_forge.py +5 -3
  12. {autoforge-1.4.2 → autoforge-1.5.0}/README.md +0 -0
  13. {autoforge-1.4.2 → autoforge-1.5.0}/setup.cfg +0 -0
  14. {autoforge-1.4.2 → autoforge-1.5.0}/src/AutoForge.egg-info/dependency_links.txt +0 -0
  15. {autoforge-1.4.2 → autoforge-1.5.0}/src/AutoForge.egg-info/entry_points.txt +0 -0
  16. {autoforge-1.4.2 → autoforge-1.5.0}/src/AutoForge.egg-info/requires.txt +0 -0
  17. {autoforge-1.4.2 → autoforge-1.5.0}/src/AutoForge.egg-info/top_level.txt +0 -0
  18. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Gui/Views/__init__.py +0 -0
  19. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Gui/Views/backend_database.py +0 -0
  20. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Gui/Views/filament_editor.py +0 -0
  21. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Gui/__init__.py +0 -0
  22. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Gui/app.py +0 -0
  23. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Gui/main.py +0 -0
  24. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Helper/FilamentHelper.py +0 -0
  25. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Helper/ImageHelper.py +0 -0
  26. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Helper/__init__.py +0 -0
  27. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Helper/filamentcolors_library.py +0 -0
  28. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Loss/PerceptionLoss.py +0 -0
  29. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Loss/__init__.py +0 -0
  30. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Misc/__init__.py +0 -0
  31. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Misc/color_fit_all.py +0 -0
  32. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/Modules/__init__.py +0 -0
  33. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/__init__.py +0 -0
  34. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/__main__.py +0 -0
  35. {autoforge-1.4.2 → autoforge-1.5.0}/src/autoforge/old_jax_files.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: AutoForge
3
- Version: 1.4.2
3
+ Version: 1.5.0
4
4
  Summary: AutoForge is a Python tool for generating 3D printed layered models from an input image.
5
5
  Author-email: Hendric Voss <hvoss@techfak.uni-bielefeld.de>
6
6
  License: CC BY-NC-SA 4.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "AutoForge"
7
- version="1.4.2"
7
+ version="1.5.0"
8
8
  description = "AutoForge is a Python tool for generating 3D printed layered models from an input image."
9
9
  authors = [{ name = "Hendric Voss", email = "hvoss@techfak.uni-bielefeld.de" }]
10
10
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: AutoForge
3
- Version: 1.4.2
3
+ Version: 1.5.0
4
4
  Summary: AutoForge is a Python tool for generating 3D printed layered models from an input image.
5
5
  Author-email: Hendric Voss <hvoss@techfak.uni-bielefeld.de>
6
6
  License: CC BY-NC-SA 4.0
@@ -16,6 +16,7 @@ src/autoforge/Gui/main.py
16
16
  src/autoforge/Gui/Views/__init__.py
17
17
  src/autoforge/Gui/Views/backend_database.py
18
18
  src/autoforge/Gui/Views/filament_editor.py
19
+ src/autoforge/Helper/CAdamW.py
19
20
  src/autoforge/Helper/FilamentHelper.py
20
21
  src/autoforge/Helper/ImageHelper.py
21
22
  src/autoforge/Helper/OptimizerHelper.py
@@ -0,0 +1,113 @@
1
+ # Taken from https://github.com/kyleliang919/C-Optim/blob/main/c_adamw.py
2
+ # copy dependencies from transformers/optimization.py
3
+ import math
4
+ import warnings
5
+ from typing import Callable, Iterable, Tuple
6
+
7
+ import torch
8
+ from torch import nn
9
+ from torch.optim import Optimizer
10
+
11
+
12
+ class CAdamW(Optimizer):
13
+ """
14
+ Implements Adam algorithm with weight decay fix as introduced in [Decoupled Weight Decay
15
+ Regularization](https://arxiv.org/abs/1711.05101).
16
+
17
+ Parameters:
18
+ params (`Iterable[nn.parameter.Parameter]`):
19
+ Iterable of parameters to optimize or dictionaries defining parameter groups.
20
+ lr (`float`, *optional*, defaults to 0.001):
21
+ The learning rate to use.
22
+ betas (`Tuple[float,float]`, *optional*, defaults to `(0.9, 0.999)`):
23
+ Adam's betas parameters (b1, b2).
24
+ eps (`float`, *optional*, defaults to 1e-06):
25
+ Adam's epsilon for numerical stability.
26
+ weight_decay (`float`, *optional*, defaults to 0.0):
27
+ Decoupled weight decay to apply.
28
+ correct_bias (`bool`, *optional*, defaults to `True`):
29
+ Whether or not to correct bias in Adam (for instance, in Bert TF repository they use `False`).
30
+ no_deprecation_warning (`bool`, *optional*, defaults to `False`):
31
+ A flag used to disable the deprecation warning (set to `True` to disable the warning).
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ params: Iterable[nn.parameter.Parameter],
37
+ lr: float = 1e-3,
38
+ betas: Tuple[float, float] = (0.9, 0.999),
39
+ eps: float = 1e-6,
40
+ weight_decay: float = 0.0,
41
+ correct_bias: bool = True,
42
+ ):
43
+
44
+ if lr < 0.0:
45
+ raise ValueError(f"Invalid learning rate: {lr} - should be >= 0.0")
46
+ if not 0.0 <= betas[0] < 1.0:
47
+ raise ValueError(f"Invalid beta parameter: {betas[0]} - should be in [0.0, 1.0)")
48
+ if not 0.0 <= betas[1] < 1.0:
49
+ raise ValueError(f"Invalid beta parameter: {betas[1]} - should be in [0.0, 1.0)")
50
+ if not 0.0 <= eps:
51
+ raise ValueError(f"Invalid epsilon value: {eps} - should be >= 0.0")
52
+ defaults = {"lr": lr, "betas": betas, "eps": eps, "weight_decay": weight_decay, "correct_bias": correct_bias}
53
+ super().__init__(params, defaults)
54
+ self.init_lr = lr
55
+
56
+ @torch.no_grad()
57
+ def step(self, closure: Callable = None):
58
+ """
59
+ Performs a single optimization step.
60
+
61
+ Arguments:
62
+ closure (`Callable`, *optional*): A closure that reevaluates the model and returns the loss.
63
+ """
64
+ loss = None
65
+ if closure is not None:
66
+ loss = closure()
67
+
68
+ for group in self.param_groups:
69
+ for i, p in enumerate(group["params"]):
70
+ if p.grad is None:
71
+ continue
72
+
73
+ grad = p.grad
74
+ state = self.state[p]
75
+
76
+ if "step" not in state:
77
+ state["step"] = 0
78
+
79
+ # State initialization
80
+ if "exp_avg" not in state:
81
+ # Exponential moving average of gradient values
82
+ state["exp_avg"] = torch.zeros_like(grad)
83
+ # Exponential moving average of squared gradient values
84
+ state["exp_avg_sq"] = torch.zeros_like(grad)
85
+
86
+ exp_avg, exp_avg_sq = state["exp_avg"], state["exp_avg_sq"]
87
+ beta1, beta2 = group["betas"]
88
+
89
+ state["step"] += 1
90
+
91
+ # apply weight decay
92
+ if group["weight_decay"] > 0.0:
93
+ p.add_(p, alpha=(-group["lr"] * group["weight_decay"]))
94
+
95
+ # Decay the first and second moment running average coefficient
96
+ # In-place operations to update the averages at the same time
97
+ exp_avg.mul_(beta1).add_(grad, alpha=(1.0 - beta1))
98
+ exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1.0 - beta2)
99
+ denom = exp_avg_sq.sqrt().add_(group["eps"])
100
+
101
+ step_size = group["lr"]
102
+ if group["correct_bias"]: # No bias correction for Bert
103
+ bias_correction1 = 1.0 - beta1 ** state["step"]
104
+ bias_correction2 = 1.0 - beta2 ** state["step"]
105
+ step_size = step_size * math.sqrt(bias_correction2) / bias_correction1
106
+
107
+ # compute norm gradient
108
+ mask = (exp_avg * grad > 0).to(grad.dtype)
109
+ # mask = mask * (mask.numel() / (mask.sum() + 1)) ## original implementation, leaving it here for record
110
+ mask.div_(mask.mean().clamp_(min=1e-3)) # https://huggingface.co/rwightman/timm-optim-caution found this implementation is more favoarable in many cases
111
+ norm_grad = (exp_avg * mask) / denom
112
+ p.add_(norm_grad, alpha=-step_size)
113
+ return loss
@@ -76,7 +76,7 @@ def deterministic_gumbel_softmax(
76
76
 
77
77
 
78
78
  @torch.jit.script
79
- def composite_image(
79
+ def composite_image_cont(
80
80
  pixel_height_logits: torch.Tensor,
81
81
  global_logits: torch.Tensor,
82
82
  tau_height: float,
@@ -86,32 +86,27 @@ def composite_image(
86
86
  material_colors: torch.Tensor,
87
87
  material_TDs: torch.Tensor,
88
88
  background: torch.Tensor,
89
- mode: str = "continuous",
90
- rng_seed: int = -1, # <--- new optional argument
91
89
  ) -> torch.Tensor:
92
90
  """
93
- Vectorized compositing over all pixels (H x W).
94
- ...
95
- If rng_seed >= 0 and mode=="discrete", we will fix the random seed
96
- for each layer so that we get the same random Gumbel noise for reproducibility.
91
+ Continuous compositing over all pixels.
92
+ Uses Gumbel softmax with either hard or soft sampling depending on tau_global.
97
93
  """
98
94
  pixel_height = (max_layers * h) * torch.sigmoid(pixel_height_logits)
99
95
  continuous_layers = pixel_height / h
100
96
 
101
- # same as before
102
97
  adaptive_layers = adaptive_round(
103
98
  continuous_layers, tau_height, high_tau=0.1, low_tau=0.01, temp=0.1
104
99
  )
105
100
  discrete_layers_temp = torch.round(continuous_layers)
106
101
  discrete_layers = (
107
102
  discrete_layers_temp + (adaptive_layers - discrete_layers_temp).detach()
108
- )
109
- discrete_layers = discrete_layers.to(torch.int32)
103
+ ).to(torch.int32)
110
104
 
111
105
  H, W = pixel_height.shape
112
106
  comp = torch.zeros(H, W, 3, dtype=torch.float32, device=pixel_height.device)
113
107
  remaining = torch.ones(H, W, dtype=torch.float32, device=pixel_height.device)
114
108
 
109
+ # opacity function parameters
115
110
  o = -1.2416557e-02
116
111
  A = 9.6407950e-01
117
112
  k = 3.4103447e01
@@ -119,39 +114,20 @@ def composite_image(
119
114
 
120
115
  for i in range(max_layers):
121
116
  layer_idx = max_layers - 1 - i
122
- p_print = (discrete_layers > layer_idx).float()
123
- eff_thick = p_print * h
124
-
125
- # This is the key difference for "discrete":
126
- # we now optionally fix the random seed if rng_seed >= 0
127
- if mode == "discrete":
128
- if rng_seed >= 0:
129
- # Use a context manager so we don’t pollute the global seed
130
- p_i = deterministic_gumbel_softmax(
131
- global_logits[layer_idx],
132
- tau_global,
133
- hard=True,
134
- rng_seed=rng_seed + layer_idx,
135
- )
136
- else:
137
- # same as before, but will be random every call
138
- p_i = F.gumbel_softmax(global_logits[layer_idx], tau_global, hard=True)
139
-
140
- elif mode == "continuous":
141
- if tau_global < 1e-3:
142
- p_i = F.gumbel_softmax(global_logits[layer_idx], tau_global, hard=True)
143
- else:
144
- p_i = F.gumbel_softmax(global_logits[layer_idx], tau_global, hard=False)
145
- else:
146
- # e.g. "pruning" or any other fallback
117
+ p_print = (discrete_layers > layer_idx).float() # [H,W]
118
+ eff_thick = p_print * h # effective thickness per pixel
119
+
120
+ # continuous mode: use hard gumbel if tau_global is very low
121
+ if tau_global < 1e-3:
147
122
  p_i = F.gumbel_softmax(global_logits[layer_idx], tau_global, hard=True)
123
+ else:
124
+ p_i = F.gumbel_softmax(global_logits[layer_idx], tau_global, hard=False)
148
125
 
149
126
  color_i = torch.matmul(p_i, material_colors)
150
127
  TD_i = torch.matmul(p_i, material_TDs)
151
128
  TD_i = torch.clamp(TD_i, 1e-8, 1e8)
152
129
 
153
130
  opac = o + (A * torch.log1p(k * (eff_thick / TD_i)) + b * (eff_thick / TD_i))
154
- # opac = torch.where(TD_i / 10 <= h, torch.tensor(1.0), opac)
155
131
  opac = torch.clamp(opac, 0.0, 1.0)
156
132
 
157
133
  comp = comp + ((remaining * opac).unsqueeze(-1) * color_i)
@@ -161,6 +137,143 @@ def composite_image(
161
137
  return comp * 255.0
162
138
 
163
139
 
140
+ @torch.jit.script
141
+ def composite_image_disc(
142
+ pixel_height_logits: torch.Tensor,
143
+ global_logits: torch.Tensor,
144
+ tau_height: float,
145
+ tau_global: float,
146
+ h: float,
147
+ max_layers: int,
148
+ material_colors: torch.Tensor,
149
+ material_TDs: torch.Tensor,
150
+ background: torch.Tensor,
151
+ rng_seed: int = -1,
152
+ ) -> torch.Tensor:
153
+ """
154
+ Discrete compositing over all pixels.
155
+ For each layer (traversed from top to bottom), we compute a one-hot material selection.
156
+ For pixels where the layer is printed (as determined by discrete_layers),
157
+ contiguous layers with the same material are merged (i.e. their effective thickness is accumulated)
158
+ and then composited using the opacity function. The accumulated segment is applied when
159
+ the material changes or if the layer is not printed.
160
+ """
161
+ # Compute pixel height and determine discrete layers.
162
+ pixel_height = (max_layers * h) * torch.sigmoid(pixel_height_logits)
163
+ continuous_layers = pixel_height / h
164
+
165
+ adaptive_layers = adaptive_round(
166
+ continuous_layers, tau_height, high_tau=0.1, low_tau=0.01, temp=0.1
167
+ )
168
+ discrete_layers_temp = torch.round(continuous_layers)
169
+ discrete_layers = (
170
+ discrete_layers_temp + (adaptive_layers - discrete_layers_temp).detach()
171
+ ).to(torch.int32)
172
+
173
+ H, W = pixel_height.shape
174
+ comp = torch.zeros(H, W, 3, dtype=torch.float32, device=pixel_height.device)
175
+ remaining = torch.ones(H, W, dtype=torch.float32, device=pixel_height.device)
176
+
177
+ # Current material and accumulated thickness per pixel.
178
+ # cur_mat is initialized to -1 meaning "no material".
179
+ cur_mat = -torch.ones((H, W), dtype=torch.int32, device=pixel_height.device)
180
+ acc_thick = torch.zeros((H, W), dtype=torch.float32, device=pixel_height.device)
181
+
182
+ # Opacity function parameters.
183
+ o = -1.2416557e-02
184
+ A = 9.6407950e-01
185
+ k = 3.4103447e01
186
+ b = -4.1554203e00
187
+
188
+ for i in range(max_layers):
189
+ layer_idx = max_layers - 1 - i
190
+ p_print = discrete_layers > layer_idx # Boolean mask [H,W]
191
+ eff_thick = (
192
+ p_print.to(torch.float32) * h
193
+ ) # effective thickness for printed pixels
194
+
195
+ # Determine material for this layer using discrete (one-hot) gumbel softmax.
196
+ if rng_seed >= 0:
197
+ p_i = deterministic_gumbel_softmax(
198
+ global_logits[layer_idx], tau_global, True, rng_seed + layer_idx
199
+ )
200
+ else:
201
+ p_i = F.gumbel_softmax(global_logits[layer_idx], tau_global, hard=True)
202
+ # Cast new_mat to int32 so it matches cur_mat.
203
+ new_mat = torch.argmax(p_i, 0).to(torch.int32)
204
+
205
+ # 1. For pixels where no current material is set, start a new segment.
206
+ mask_new = p_print & (cur_mat == -1)
207
+ if mask_new.any():
208
+ cur_mat[mask_new] = new_mat
209
+ acc_thick[mask_new] = eff_thick[mask_new]
210
+
211
+ # 2. For pixels where the current segment has the same material, accumulate thickness.
212
+ mask_same = p_print & (cur_mat == new_mat)
213
+ if mask_same.any():
214
+ acc_thick[mask_same] = acc_thick[mask_same] + eff_thick[mask_same]
215
+
216
+ # 3. For pixels where the current segment has a different material,
217
+ # composite the accumulated segment and start a new one.
218
+ mask_diff = p_print & (cur_mat != -1) & (cur_mat != new_mat)
219
+ if mask_diff.any():
220
+ # Convert indices to int64 for index_select.
221
+ indices_diff = cur_mat[mask_diff].to(torch.int64)
222
+ td_vals = material_TDs.index_select(0, indices_diff)
223
+ col_vals = material_colors.index_select(0, indices_diff)
224
+ opac_vals = o + (
225
+ A * torch.log1p(k * (acc_thick[mask_diff] / td_vals))
226
+ + b * (acc_thick[mask_diff] / td_vals)
227
+ )
228
+ opac_vals = torch.clamp(opac_vals, 0.0, 1.0)
229
+ comp[mask_diff] = comp[mask_diff] + (
230
+ (remaining[mask_diff] * opac_vals).unsqueeze(-1) * col_vals
231
+ )
232
+ remaining[mask_diff] = remaining[mask_diff] * (1 - opac_vals)
233
+ cur_mat[mask_diff] = new_mat
234
+ acc_thick[mask_diff] = eff_thick[mask_diff]
235
+
236
+ # 4. For pixels where this layer is not printed but we have a pending segment,
237
+ # composite that segment and reset.
238
+ mask_not_printed = (~p_print) & (cur_mat != -1)
239
+ if mask_not_printed.any():
240
+ indices_np = cur_mat[mask_not_printed].to(torch.int64)
241
+ td_vals = material_TDs.index_select(0, indices_np)
242
+ col_vals = material_colors.index_select(0, indices_np)
243
+ opac_vals = o + (
244
+ A * torch.log1p(k * (acc_thick[mask_not_printed] / td_vals))
245
+ + b * (acc_thick[mask_not_printed] / td_vals)
246
+ )
247
+ opac_vals = torch.clamp(opac_vals, 0.0, 1.0)
248
+ comp[mask_not_printed] = comp[mask_not_printed] + (
249
+ (remaining[mask_not_printed] * opac_vals).unsqueeze(-1) * col_vals
250
+ )
251
+ remaining[mask_not_printed] = remaining[mask_not_printed] * (1 - opac_vals)
252
+ cur_mat[mask_not_printed] = -1
253
+ acc_thick[mask_not_printed] = 0.0
254
+
255
+ # After the loop, composite any remaining accumulated segment.
256
+ mask_remain = cur_mat != -1
257
+ if mask_remain.any():
258
+ indices_rem = cur_mat[mask_remain].to(torch.int64)
259
+ td_vals = material_TDs.index_select(0, indices_rem)
260
+ col_vals = material_colors.index_select(0, indices_rem)
261
+ opac_vals = o + (
262
+ A * torch.log1p(k * (acc_thick[mask_remain] / td_vals))
263
+ + b * (acc_thick[mask_remain] / td_vals)
264
+ )
265
+ opac_vals = torch.clamp(opac_vals, 0.0, 1.0)
266
+ comp[mask_remain] = comp[mask_remain] + (
267
+ (remaining[mask_remain] * opac_vals).unsqueeze(-1) * col_vals
268
+ )
269
+ remaining[mask_remain] = remaining[mask_remain] * (1 - opac_vals)
270
+ cur_mat[mask_remain] = -1
271
+ acc_thick[mask_remain] = 0.0
272
+
273
+ comp = comp + remaining.unsqueeze(-1) * background
274
+ return comp * 255.0
275
+
276
+
164
277
  @torch.jit.script
165
278
  def adaptive_round(
166
279
  x: torch.Tensor, tau: float, high_tau: float, low_tau: float, temp: float
@@ -33,7 +33,7 @@ def extract_filament_swaps(disc_global, disc_height_image, background_layers):
33
33
  current = int(disc_global[i])
34
34
  # If this is the first layer or the material changes from the previous layer…
35
35
  if current != prev:
36
- slider = (i + background_layers) - 1
36
+ slider = i + background_layers
37
37
  slider_values.append(slider)
38
38
  filament_indices.append(prev)
39
39
  prev = current
@@ -118,7 +118,7 @@ def generate_project_file(
118
118
  },
119
119
  )
120
120
  # add black to slider value
121
- slider_values.insert(0, (args.background_height // args.layer_height) - 1)
121
+ slider_values.insert(0, (args.background_height // args.layer_height))
122
122
 
123
123
  # reverse order of filament set
124
124
  filament_set = filament_set[::-1]
@@ -186,11 +186,9 @@ def generate_project_file(
186
186
  json.dump(project_data, f, indent=4)
187
187
 
188
188
 
189
- import numpy as np
190
- import struct
191
-
192
-
193
- def generate_stl(height_map, filename, background_height, maximum_x_y_size, alpha_mask=None):
189
+ def generate_stl(
190
+ height_map, filename, background_height, maximum_x_y_size, alpha_mask=None
191
+ ):
194
192
  """
195
193
  Generate a binary STL file from a height map with an optional alpha mask.
196
194
  If alpha_mask is provided, vertices where alpha < 128 are omitted.
@@ -213,7 +211,7 @@ def generate_stl(height_map, filename, background_height, maximum_x_y_size, alph
213
211
  if alpha_mask is None:
214
212
  valid_mask = np.ones((H, W), dtype=bool)
215
213
  else:
216
- valid_mask = (alpha_mask >= 128)
214
+ valid_mask = alpha_mask >= 128
217
215
 
218
216
  # --- Create vertices for the top surface ---
219
217
  # We compute a grid of vertices (even for masked-out regions) but later only use those that are valid.
@@ -250,7 +248,12 @@ def generate_stl(height_map, filename, background_height, maximum_x_y_size, alph
250
248
  top_triangles_indices = [] # Each element is a tuple of 3 (i,j) indices.
251
249
  for i in range(H - 1):
252
250
  for j in range(W - 1):
253
- if valid_mask[i, j] and valid_mask[i, j + 1] and valid_mask[i + 1, j + 1] and valid_mask[i + 1, j]:
251
+ if (
252
+ valid_mask[i, j]
253
+ and valid_mask[i, j + 1]
254
+ and valid_mask[i + 1, j + 1]
255
+ and valid_mask[i + 1, j]
256
+ ):
254
257
  # Define the four corners of the quad.
255
258
  v0 = top_vertices[i, j]
256
259
  v1 = top_vertices[i, j + 1]
@@ -306,7 +309,12 @@ def generate_stl(height_map, filename, background_height, maximum_x_y_size, alph
306
309
  # The ordering is reversed (relative to the top face) so that the normals face downward.
307
310
  for i in range(H - 1):
308
311
  for j in range(W - 1):
309
- if valid_mask[i, j] and valid_mask[i, j + 1] and valid_mask[i + 1, j + 1] and valid_mask[i + 1, j]:
312
+ if (
313
+ valid_mask[i, j]
314
+ and valid_mask[i, j + 1]
315
+ and valid_mask[i + 1, j + 1]
316
+ and valid_mask[i + 1, j]
317
+ ):
310
318
  v0 = bottom_vertices[i, j]
311
319
  v1 = bottom_vertices[i, j + 1]
312
320
  v2 = bottom_vertices[i + 1, j + 1]
@@ -383,7 +391,7 @@ def generate_swap_instructions(
383
391
  instructions.append("Start with your background color")
384
392
  for i in range(0, L):
385
393
  if i == 0 or int(discrete_global[i]) != int(discrete_global[i - 1]):
386
- ie = i
394
+ ie = i + 1
387
395
  instructions.append(
388
396
  f"At layer #{ie + background_layers} ({(ie * h) + background_height:.2f}mm) swap to {material_names[int(discrete_global[i])]}"
389
397
  )
@@ -1,7 +1,7 @@
1
1
  import torch
2
2
  from tqdm import tqdm
3
3
 
4
- from autoforge.Helper.OptimizerHelper import composite_image, discretize_solution
4
+ from autoforge.Helper.OptimizerHelper import discretize_solution, composite_image_disc
5
5
  from autoforge.Loss.LossFunctions import compute_loss
6
6
 
7
7
 
@@ -55,7 +55,7 @@ def prune_num_colors(
55
55
  )
56
56
 
57
57
  with torch.no_grad():
58
- out_im = composite_image(
58
+ out_im = composite_image_disc(
59
59
  pixel_height_logits,
60
60
  logits_for_disc,
61
61
  tau_for_comp,
@@ -65,7 +65,6 @@ def prune_num_colors(
65
65
  material_colors,
66
66
  material_TDs,
67
67
  background,
68
- mode="discrete",
69
68
  rng_seed=rng_seed,
70
69
  )
71
70
 
@@ -148,7 +147,7 @@ def prune_num_swaps(
148
147
  )
149
148
 
150
149
  with torch.no_grad():
151
- out_im = composite_image(
150
+ out_im = composite_image_disc(
152
151
  pixel_height_logits,
153
152
  logits_for_disc,
154
153
  tau_for_comp,
@@ -158,7 +157,6 @@ def prune_num_swaps(
158
157
  material_colors,
159
158
  material_TDs,
160
159
  background,
161
- mode="discrete",
162
160
  rng_seed=rng_seed,
163
161
  )
164
162
  return compute_loss(
@@ -413,7 +411,7 @@ def prune_redundant_layers(
413
411
  current_max_layers = current_params["global_logits"].shape[0]
414
412
 
415
413
  # Compute baseline composite and loss.
416
- comp = composite_image(
414
+ comp = composite_image_disc(
417
415
  current_params["pixel_height_logits"],
418
416
  current_params["global_logits"],
419
417
  final_tau,
@@ -423,7 +421,6 @@ def prune_redundant_layers(
423
421
  material_colors,
424
422
  material_TDs,
425
423
  background,
426
- mode="discrete",
427
424
  rng_seed=rng_seed,
428
425
  )
429
426
  disc_global, _ = discretize_solution(
@@ -458,7 +455,7 @@ def prune_redundant_layers(
458
455
  candidate_params, candidate_max_layers = remove_layer_from_solution(
459
456
  current_params, layer, final_tau, h, current_max_layers, rng_seed
460
457
  )
461
- candidate_comp = composite_image(
458
+ candidate_comp = composite_image_disc(
462
459
  candidate_params["pixel_height_logits"],
463
460
  candidate_params["global_logits"],
464
461
  final_tau,
@@ -468,7 +465,6 @@ def prune_redundant_layers(
468
465
  material_colors,
469
466
  material_TDs,
470
467
  background,
471
- mode="discrete",
472
468
  rng_seed=rng_seed,
473
469
  )
474
470
  candidate_disc_global, _ = discretize_solution(
@@ -2,7 +2,7 @@ import torch
2
2
  import torch.nn.functional as F
3
3
 
4
4
  from autoforge.Helper.ImageHelper import srgb_to_lab, increase_saturation
5
- from autoforge.Helper.OptimizerHelper import composite_image
5
+ from autoforge.Helper.OptimizerHelper import composite_image_cont
6
6
 
7
7
 
8
8
  def loss_fn(
@@ -22,7 +22,7 @@ def loss_fn(
22
22
  Full forward pass for continuous assignment:
23
23
  composite, then compute unified loss on (global_logits).
24
24
  """
25
- comp = composite_image(
25
+ comp = composite_image_cont(
26
26
  params["pixel_height_logits"],
27
27
  params["global_logits"],
28
28
  tau_height,
@@ -32,7 +32,6 @@ def loss_fn(
32
32
  material_colors,
33
33
  material_TDs,
34
34
  background,
35
- mode="continuous",
36
35
  )
37
36
  return compute_loss(
38
37
  material_assignment=params["global_logits"],
@@ -5,11 +5,15 @@ import random
5
5
  import matplotlib.pyplot as plt
6
6
  import numpy as np
7
7
  import torch
8
- import torch.optim as optim
9
8
  from tqdm import tqdm
10
9
  from torch.utils.tensorboard import SummaryWriter
11
10
 
12
- from autoforge.Helper.OptimizerHelper import composite_image, discretize_solution
11
+ from autoforge.Helper.CAdamW import CAdamW
12
+ from autoforge.Helper.OptimizerHelper import (
13
+ discretize_solution,
14
+ composite_image_cont,
15
+ composite_image_disc,
16
+ )
13
17
  from autoforge.Helper.PruningHelper import prune_colors_and_swaps, find_color_bands
14
18
  from autoforge.Loss.LossFunctions import loss_fn, compute_loss
15
19
 
@@ -64,7 +68,7 @@ class FilamentOptimizer:
64
68
  # Initialize TensorBoard writer
65
69
  if args.tensorboard:
66
70
  if args.run_name:
67
- self.writer = SummaryWriter(log_dir=f'runs/{args.run_name}')
71
+ self.writer = SummaryWriter(log_dir=f"runs/{args.run_name}")
68
72
  else:
69
73
  self.writer = SummaryWriter()
70
74
  else:
@@ -99,9 +103,10 @@ class FilamentOptimizer:
99
103
  )
100
104
 
101
105
  # Initialize optimizer
102
- self.optimizer = optim.Adam(
106
+ self.optimizer = CAdamW(
103
107
  [self.params["pixel_height_logits"], self.params["global_logits"]],
104
108
  lr=self.learning_rate,
109
+ weight_decay=1e-2,
105
110
  )
106
111
 
107
112
  # Setup best discrete solution tracking
@@ -193,7 +198,9 @@ class FilamentOptimizer:
193
198
 
194
199
  return loss
195
200
 
196
- def log_to_tensorboard(self, interval: int = 100, namespace: str = "", step: int = None):
201
+ def log_to_tensorboard(
202
+ self, interval: int = 100, namespace: str = "", step: int = None
203
+ ):
197
204
  """
198
205
  Log metrics and images to TensorBoard.
199
206
 
@@ -211,22 +218,26 @@ class FilamentOptimizer:
211
218
  steps = step if step is not None else self.num_steps_done
212
219
 
213
220
  # Log metrics
214
- self.writer.add_scalar(f'Loss/{prefix}best_discrete', self.best_discrete_loss, steps)
215
- self.writer.add_scalar(f'Loss/{prefix}best_swaps', self.best_swaps, steps)
221
+ self.writer.add_scalar(
222
+ f"Loss/{prefix}best_discrete", self.best_discrete_loss, steps
223
+ )
224
+ self.writer.add_scalar(f"Loss/{prefix}best_swaps", self.best_swaps, steps)
216
225
 
217
226
  tau_height, tau_global = self._get_tau()
218
227
 
219
228
  # Metrics that are only relevant for the main optimization loop
220
229
  if not prefix:
221
- self.writer.add_scalar('Params/tau_height', tau_height, steps)
222
- self.writer.add_scalar('Params/tau_global', tau_global, steps)
223
- self.writer.add_scalar('Params/lr', self.optimizer.param_groups[0]['lr'], steps)
224
- self.writer.add_scalar('Loss/train', self.loss, steps)
230
+ self.writer.add_scalar("Params/tau_height", tau_height, steps)
231
+ self.writer.add_scalar("Params/tau_global", tau_global, steps)
232
+ self.writer.add_scalar(
233
+ "Params/lr", self.optimizer.param_groups[0]["lr"], steps
234
+ )
235
+ self.writer.add_scalar("Loss/train", self.loss, steps)
225
236
 
226
237
  # Log images periodically
227
238
  if (steps + 1) % interval == 0:
228
239
  with torch.no_grad():
229
- comp_img = composite_image(
240
+ comp_img = composite_image_cont(
230
241
  self.params["pixel_height_logits"],
231
242
  self.params["global_logits"],
232
243
  tau_height,
@@ -235,9 +246,13 @@ class FilamentOptimizer:
235
246
  self.max_layers,
236
247
  self.material_colors,
237
248
  self.material_TDs,
238
- self.background
249
+ self.background,
250
+ )
251
+ self.writer.add_images(
252
+ f"Current Output/{prefix}composite",
253
+ comp_img.permute(2, 0, 1).unsqueeze(0) / 255.0,
254
+ steps,
239
255
  )
240
- self.writer.add_images(f'Current Output/{prefix}composite', comp_img.permute(2, 0, 1).unsqueeze(0) / 255.0, steps)
241
256
 
242
257
  def visualize(self, interval: int = 25):
243
258
  """
@@ -255,7 +270,7 @@ class FilamentOptimizer:
255
270
 
256
271
  with torch.no_grad():
257
272
  tau_h, tau_g = self._get_tau()
258
- comp = composite_image(
273
+ comp = composite_image_cont(
259
274
  self.params["pixel_height_logits"],
260
275
  self.params["global_logits"],
261
276
  tau_h,
@@ -265,14 +280,13 @@ class FilamentOptimizer:
265
280
  self.material_colors,
266
281
  self.material_TDs,
267
282
  self.background,
268
- mode="continuous",
269
283
  )
270
284
  comp_np = np.clip(comp.cpu().detach().numpy(), 0, 255).astype(np.uint8)
271
285
  self.current_comp_ax.set_data(comp_np)
272
286
 
273
287
  if self.best_params is not None:
274
288
  with torch.no_grad():
275
- best_comp = composite_image(
289
+ best_comp = composite_image_disc(
276
290
  self.best_params["pixel_height_logits"],
277
291
  self.best_params["global_logits"],
278
292
  self.final_tau,
@@ -282,7 +296,6 @@ class FilamentOptimizer:
282
296
  self.material_colors,
283
297
  self.material_TDs,
284
298
  self.background,
285
- mode="discrete",
286
299
  rng_seed=self.best_seed,
287
300
  )
288
301
  best_comp_np = np.clip(best_comp.cpu().detach().numpy(), 0, 255).astype(
@@ -434,7 +447,7 @@ class FilamentOptimizer:
434
447
 
435
448
  # 2) Compute discrete-mode composite
436
449
  with torch.no_grad():
437
- comp_disc = composite_image(
450
+ comp_disc = composite_image_disc(
438
451
  self.params["pixel_height_logits"],
439
452
  self.params["global_logits"],
440
453
  self.final_tau,
@@ -444,7 +457,6 @@ class FilamentOptimizer:
444
457
  self.material_colors,
445
458
  self.material_TDs,
446
459
  self.background,
447
- mode="discrete", # or "discrete" if you prefer fully discrete
448
460
  rng_seed=seed,
449
461
  )
450
462
 
@@ -487,7 +499,7 @@ class FilamentOptimizer:
487
499
  disc_global, disc_height_image = discretize_solution(
488
500
  self.best_params, self.final_tau, self.h, self.max_layers, rng_seed=seed
489
501
  )
490
- comp_disc = composite_image(
502
+ comp_disc = composite_image_disc(
491
503
  self.best_params["pixel_height_logits"],
492
504
  self.best_params["global_logits"],
493
505
  self.final_tau,
@@ -497,7 +509,6 @@ class FilamentOptimizer:
497
509
  self.material_colors,
498
510
  self.material_TDs,
499
511
  self.background,
500
- mode="discrete",
501
512
  rng_seed=seed,
502
513
  )
503
514
  current_disc_loss = compute_loss(
@@ -10,9 +10,9 @@ from autoforge.Helper.FilamentHelper import hex_to_rgb, load_materials
10
10
  from autoforge.Helper.ImageHelper import resize_image, resize_image_exact
11
11
  from autoforge.Helper.OptimizerHelper import (
12
12
  init_height_map,
13
- composite_image,
14
13
  discretize_solution,
15
14
  init_height_map_depth_color_adjusted,
15
+ composite_image_disc,
16
16
  )
17
17
  from autoforge.Helper.OutputHelper import (
18
18
  generate_stl,
@@ -207,6 +207,9 @@ def main():
207
207
  "Background must be multiple of layer_height"
208
208
  )
209
209
 
210
+ assert os.path.exists(args.input_image), "Input image not found"
211
+ assert os.path.exists(args.csv_file), "CSV file not found"
212
+
210
213
  random_seed = args.random_seed
211
214
  if random_seed == 0:
212
215
  random_seed = int(time.time())
@@ -387,7 +390,7 @@ def main():
387
390
 
388
391
  print("Done. Saving outputs...")
389
392
  # Save Image
390
- comp_disc = composite_image(
393
+ comp_disc = composite_image_disc(
391
394
  params["pixel_height_logits"],
392
395
  params["global_logits"],
393
396
  args.final_tau,
@@ -397,7 +400,6 @@ def main():
397
400
  material_colors,
398
401
  material_TDs,
399
402
  background,
400
- mode="discrete", # or "discrete" if you prefer fully discrete
401
403
  rng_seed=optimizer.best_seed,
402
404
  )
403
405
 
File without changes
File without changes