autoforge 1.6.2__tar.gz → 1.6.4__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 (42) hide show
  1. {autoforge-1.6.2 → autoforge-1.6.4}/PKG-INFO +4 -2
  2. {autoforge-1.6.2 → autoforge-1.6.4}/README.md +1 -1
  3. {autoforge-1.6.2 → autoforge-1.6.4}/pyproject.toml +4 -2
  4. {autoforge-1.6.2 → autoforge-1.6.4}/src/AutoForge.egg-info/PKG-INFO +4 -2
  5. {autoforge-1.6.2 → autoforge-1.6.4}/src/AutoForge.egg-info/requires.txt +2 -0
  6. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py +50 -55
  7. autoforge-1.6.4/src/autoforge/Loss/LossFunctions.py +109 -0
  8. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Modules/Optimizer.py +77 -43
  9. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/auto_forge.py +34 -11
  10. autoforge-1.6.2/src/autoforge/Loss/LossFunctions.py +0 -91
  11. {autoforge-1.6.2 → autoforge-1.6.4}/setup.cfg +0 -0
  12. {autoforge-1.6.2 → autoforge-1.6.4}/src/AutoForge.egg-info/SOURCES.txt +0 -0
  13. {autoforge-1.6.2 → autoforge-1.6.4}/src/AutoForge.egg-info/dependency_links.txt +0 -0
  14. {autoforge-1.6.2 → autoforge-1.6.4}/src/AutoForge.egg-info/entry_points.txt +0 -0
  15. {autoforge-1.6.2 → autoforge-1.6.4}/src/AutoForge.egg-info/top_level.txt +0 -0
  16. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Gui/__init__.py +0 -0
  17. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Gui/browser_gui/Views/__init__.py +0 -0
  18. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Gui/browser_gui/Views/backend_database.py +0 -0
  19. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Gui/browser_gui/Views/filament_editor.py +0 -0
  20. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Gui/browser_gui/app.py +0 -0
  21. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Gui/browser_gui/main.py +0 -0
  22. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Helper/CAdamW.py +0 -0
  23. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Helper/FilamentHelper.py +0 -0
  24. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Helper/Heightmaps/DepthEstimateHeightMap.py +0 -0
  25. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Helper/Heightmaps/OldHeightmap.py +0 -0
  26. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Helper/Heightmaps/__init__.py +0 -0
  27. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Helper/ImageHelper.py +0 -0
  28. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Helper/OptimizerHelper.py +0 -0
  29. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Helper/OutputHelper.py +0 -0
  30. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Helper/PruningHelper.py +0 -0
  31. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Helper/__init__.py +0 -0
  32. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Helper/filamentcolors_library.py +0 -0
  33. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Init_comparer/__init__.py +0 -0
  34. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Init_comparer/auto_forge_init_check.py +0 -0
  35. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Init_comparer/plot_results.py +0 -0
  36. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Loss/PerceptionLoss.py +0 -0
  37. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Loss/__init__.py +0 -0
  38. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Misc/__init__.py +0 -0
  39. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Misc/color_fit_all.py +0 -0
  40. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/Modules/__init__.py +0 -0
  41. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/__init__.py +0 -0
  42. {autoforge-1.6.2 → autoforge-1.6.4}/src/autoforge/__main__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AutoForge
3
- Version: 1.6.2
3
+ Version: 1.6.4
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
@@ -21,6 +21,8 @@ Requires-Dist: tensorboard
21
21
  Requires-Dist: pillow>=11.1.0
22
22
  Requires-Dist: shapely>=2.0.7
23
23
  Requires-Dist: trimesh>=4.6.4
24
+ Requires-Dist: torchcam>=0.4.0
25
+ Requires-Dist: joblib>=1.4.2
24
26
 
25
27
  # AutoForge
26
28
 
@@ -61,7 +63,7 @@ The pruning is set to a maximum of 8 color and 20 swaps, so each image uses at m
61
63
 
62
64
  ## [Google Colab Notebook](https://colab.research.google.com/drive/13sWCss9-ew2xnoJl4c4SSpComSNi-1ih)
63
65
 
64
- If you're new to Google Colab, follow these simple steps to run the notebook:
66
+ If you're new to Google Colab, follow these simple steps to run autoforge and get your output:
65
67
 
66
68
  1. **Open the Notebook:**
67
69
  Click [here](https://colab.research.google.com/drive/13sWCss9-ew2xnoJl4c4SSpComSNi-1ih) to open the notebook in Google Colab.
@@ -37,7 +37,7 @@ The pruning is set to a maximum of 8 color and 20 swaps, so each image uses at m
37
37
 
38
38
  ## [Google Colab Notebook](https://colab.research.google.com/drive/13sWCss9-ew2xnoJl4c4SSpComSNi-1ih)
39
39
 
40
- If you're new to Google Colab, follow these simple steps to run the notebook:
40
+ If you're new to Google Colab, follow these simple steps to run autoforge and get your output:
41
41
 
42
42
  1. **Open the Notebook:**
43
43
  Click [here](https://colab.research.google.com/drive/13sWCss9-ew2xnoJl4c4SSpComSNi-1ih) to open the notebook in Google Colab.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "AutoForge"
7
- version="1.6.2"
7
+ version="1.6.4"
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"
@@ -25,7 +25,9 @@ dependencies = [
25
25
  "tensorboard",
26
26
  "pillow>=11.1.0",
27
27
  "shapely>=2.0.7",
28
- "trimesh>=4.6.4"
28
+ "trimesh>=4.6.4",
29
+ "torchcam>=0.4.0",
30
+ "joblib>=1.4.2"
29
31
  ]
30
32
 
31
33
  [tool.setuptools]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AutoForge
3
- Version: 1.6.2
3
+ Version: 1.6.4
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
@@ -21,6 +21,8 @@ Requires-Dist: tensorboard
21
21
  Requires-Dist: pillow>=11.1.0
22
22
  Requires-Dist: shapely>=2.0.7
23
23
  Requires-Dist: trimesh>=4.6.4
24
+ Requires-Dist: torchcam>=0.4.0
25
+ Requires-Dist: joblib>=1.4.2
24
26
 
25
27
  # AutoForge
26
28
 
@@ -61,7 +63,7 @@ The pruning is set to a maximum of 8 color and 20 swaps, so each image uses at m
61
63
 
62
64
  ## [Google Colab Notebook](https://colab.research.google.com/drive/13sWCss9-ew2xnoJl4c4SSpComSNi-1ih)
63
65
 
64
- If you're new to Google Colab, follow these simple steps to run the notebook:
66
+ If you're new to Google Colab, follow these simple steps to run autoforge and get your output:
65
67
 
66
68
  1. **Open the Notebook:**
67
69
  Click [here](https://colab.research.google.com/drive/13sWCss9-ew2xnoJl4c4SSpComSNi-1ih) to open the notebook in Google Colab.
@@ -13,3 +13,5 @@ tensorboard
13
13
  pillow>=11.1.0
14
14
  shapely>=2.0.7
15
15
  trimesh>=4.6.4
16
+ torchcam>=0.4.0
17
+ joblib>=1.4.2
@@ -1,15 +1,13 @@
1
- import concurrent.futures
2
1
  import os
3
2
  import random
4
- from concurrent.futures import ThreadPoolExecutor
5
3
 
6
4
  import numpy as np
7
5
  from PIL import Image
8
6
  from PIL.Image import Quantize
7
+ from joblib import Parallel, delayed
9
8
  from scipy.spatial.distance import cdist
10
9
  from skimage.color import rgb2lab
11
10
  from sklearn.cluster import MiniBatchKMeans
12
- from tqdm import tqdm
13
11
 
14
12
 
15
13
  def build_distance_matrix(labs, nodes):
@@ -240,25 +238,28 @@ def create_mapping(final_ordering, labs, all_labels):
240
238
 
241
239
  def tsp_order_christofides_path(nodes, labs, bg, fg):
242
240
  """
243
- nodes: list of cluster indices (must include bg and fg)
241
+ Ensure that the background and foreground nodes are always in the TSP cycle.
242
+ nodes: list of cluster indices (ideally including bg and fg)
244
243
  labs: Lab-space coordinates (indexed by cluster index)
245
244
  bg, fg: background and foreground cluster indices
246
-
247
245
  Returns an ordering (list of cluster indices) that contains all nodes,
248
246
  starts with bg and ends with fg.
249
247
  """
248
+ # Guarantee that bg and fg are included in nodes.
249
+ nodes = list(set(nodes) | {bg, fg})
250
+
250
251
  artificial = -1
251
- augmented_nodes = nodes + [artificial]
252
252
  LARGE = 1e6
253
+ n = len(nodes)
253
254
 
254
- # Precompute the distance matrix for original nodes.
255
+ # Precompute the distance matrix for the given nodes.
255
256
  D = build_distance_matrix(labs, nodes)
256
- n = len(nodes)
257
257
 
258
- # Build an augmented matrix of size (n+1)x(n+1).
258
+ # Build an augmented (n+1)x(n+1) matrix.
259
259
  aug_mat = np.zeros((n + 1, n + 1))
260
260
  aug_mat[:n, :n] = D
261
- # For the artificial node, set cost = 0 for bg/fg, LARGE otherwise.
261
+
262
+ # For the artificial node, set cost = 0 for bg/fg and LARGE for others.
262
263
  for i, u in enumerate(nodes):
263
264
  if u in {bg, fg}:
264
265
  aug_mat[i, n] = 0.0
@@ -268,32 +269,33 @@ def tsp_order_christofides_path(nodes, labs, bg, fg):
268
269
  aug_mat[n, i] = LARGE
269
270
  aug_mat[n, n] = 0.0
270
271
 
271
- # Convert the augmented matrix into a graph dictionary.
272
+ # Create augmented nodes list.
272
273
  aug_nodes = nodes + [artificial]
273
274
  graph = matrix_to_graph(aug_mat, aug_nodes)
274
275
 
275
- # Run Christofides on the augmented graph.
276
+ # Run Christofides algorithm on the augmented graph.
276
277
  cycle = christofides_tsp(graph)
277
- # Remove the artificial node.
278
+ # Remove the artificial node if present.
278
279
  cycle = [node for node in cycle if node != artificial]
279
280
 
281
+ # Ensure bg and fg are in the cycle.
282
+ if bg not in cycle:
283
+ cycle.insert(0, bg)
284
+ if fg not in cycle:
285
+ cycle.append(fg)
286
+
280
287
  # Rotate the cycle so that bg is first.
281
- if bg in cycle:
288
+ if cycle[0] != bg:
282
289
  idx = cycle.index(bg)
283
290
  cycle = cycle[idx:] + cycle[:idx]
284
- else:
285
- raise ValueError("Background node missing from cycle")
286
291
 
287
292
  # Force fg to be the last element.
288
- if fg not in cycle:
289
- raise ValueError("Foreground node missing from cycle")
290
293
  if cycle[-1] != fg:
291
294
  cycle.remove(fg)
292
295
  cycle.append(fg)
293
296
 
294
- # --- NEW: Check if reversing the internal order yields a lower ordering metric ---
297
+ # Optionally, check if reversing the internal order improves the ordering metric.
295
298
  if len(cycle) > 2:
296
- # Reverse the ordering of the nodes between bg and fg.
297
299
  reversed_cycle = [cycle[0]] + cycle[1:-1][::-1] + [cycle[-1]]
298
300
  if compute_ordering_metric(reversed_cycle, labs) < compute_ordering_metric(
299
301
  cycle, labs
@@ -449,14 +451,7 @@ def init_height_map(
449
451
  nodes = unique_clusters
450
452
 
451
453
  # Get the ordering via TSP ordering function.
452
- for i in range(50):
453
- try:
454
- final_ordering = tsp_order_christofides_path(nodes, labs, bg_cluster, fg_cluster)
455
- break
456
- except Exception:
457
- print(f"Failed to find ordering, retrying {i+1}/10")
458
- if i == 49:
459
- raise ValueError("Failed to find ordering after 50 retries")
454
+ final_ordering = tsp_order_christofides_path(nodes, labs, bg_cluster, fg_cluster)
460
455
 
461
456
  # Optionally prune out outliers.
462
457
  final_ordering = prune_ordering(
@@ -474,7 +469,7 @@ def init_height_map(
474
469
  def run_init_threads(
475
470
  target,
476
471
  max_layers,
477
- h, # unused here but preserved for API compatibility
472
+ h, # unused but preserved for API compatibility
478
473
  background_tuple,
479
474
  eps=1e-6,
480
475
  random_seed=None,
@@ -485,33 +480,33 @@ def run_init_threads(
485
480
  ):
486
481
  if random_seed is None:
487
482
  random_seed = np.random.randint(1e6)
488
- exec = ThreadPoolExecutor(max_workers=os.cpu_count())
489
- futures = []
490
- for i in range(num_threads):
491
- futures.append(
492
- exec.submit(
493
- init_height_map,
494
- target,
495
- max_layers,
496
- h,
497
- background_tuple,
498
- eps,
499
- random_seed + i,
500
- init_method=init_method,
501
- cluster_layers=cluster_layers,
502
- lab_space=lab_space,
503
- )
504
- )
505
- results = [
506
- f.result()
507
- for f in tqdm(
508
- concurrent.futures.as_completed(futures),
509
- desc="Running Height Map Initialization Threads",
510
- total=num_threads,
483
+
484
+ tasks = [
485
+ delayed(init_height_map)(
486
+ target,
487
+ max_layers,
488
+ h,
489
+ background_tuple,
490
+ eps,
491
+ random_seed + i,
492
+ init_method=init_method,
493
+ cluster_layers=cluster_layers,
494
+ lab_space=lab_space,
511
495
  )
496
+ for i in range(num_threads)
512
497
  ]
513
- # print(f"metric values: {[r[1] for r in results]}")
514
- # get lowest ordering metric
515
- best_result = min(results, key=lambda x: x[1])
516
498
 
499
+ # Execute tasks in parallel; adjust n_jobs to match your available cores
500
+ results = Parallel(n_jobs=os.cpu_count(), verbose=10)(tasks)
501
+
502
+ metrics = [r[1] for r in results]
503
+ mean_metric = np.mean(metrics)
504
+ std_metric = np.std(metrics)
505
+ min_metric = np.min(metrics)
506
+ max_metric = np.max(metrics)
507
+ print(
508
+ f"mean: {mean_metric}, std: {std_metric}, min: {min_metric}, max: {max_metric}"
509
+ )
510
+ print(f"Choosing best ordering with metric: {min_metric}")
511
+ best_result = min(results, key=lambda x: x[1])
517
512
  return best_result[0]
@@ -0,0 +1,109 @@
1
+ import torch
2
+ import torch.nn.functional as F
3
+
4
+ from autoforge.Helper.ImageHelper import srgb_to_lab
5
+ from autoforge.Helper.OptimizerHelper import composite_image_cont
6
+
7
+
8
+ def loss_fn(
9
+ params: dict,
10
+ target: torch.Tensor,
11
+ tau_height: float,
12
+ tau_global: float,
13
+ h: float,
14
+ max_layers: int,
15
+ material_colors: torch.Tensor,
16
+ material_TDs: torch.Tensor,
17
+ background: torch.Tensor,
18
+ add_penalty_loss: float = 0.0,
19
+ focus_map: torch.Tensor = None,
20
+ focus_strength: float = 10.0,
21
+ ) -> torch.Tensor:
22
+ """
23
+ Full forward pass for continuous assignment:
24
+ composite, then compute unified loss on (global_logits).
25
+ """
26
+ comp = composite_image_cont(
27
+ params["pixel_height_logits"],
28
+ params["global_logits"],
29
+ tau_height,
30
+ tau_global,
31
+ h,
32
+ max_layers,
33
+ material_colors,
34
+ material_TDs,
35
+ background,
36
+ )
37
+ return compute_loss(
38
+ comp=comp,
39
+ target=target,
40
+ pixel_height_logits=params["pixel_height_logits"],
41
+ tau_height=tau_height,
42
+ add_penalty_loss=add_penalty_loss,
43
+ focus_map=focus_map,
44
+ focus_strength=focus_strength,
45
+ )
46
+
47
+
48
+ def compute_loss(
49
+ comp: torch.Tensor,
50
+ target: torch.Tensor,
51
+ pixel_height_logits: torch.Tensor = None,
52
+ tau_height: float = 1.0,
53
+ add_penalty_loss: float = 0.0,
54
+ focus_map: torch.Tensor = None,
55
+ focus_strength: float = 10.0,
56
+ ) -> torch.Tensor:
57
+ comp_lab = srgb_to_lab(comp)
58
+ target_lab = srgb_to_lab(target)
59
+ loss1 = F.huber_loss(comp_lab, target_lab, delta=0.1)
60
+
61
+ return loss1
62
+
63
+ # if focus_map is not None and focus_strength > 0.0:
64
+ # # Expand focus_map to [H, W, 1] to match the color channels.
65
+ # focus_map_exp = focus_map.unsqueeze(-1)
66
+ # base_loss = F.huber_loss(comp_mse, target_mse, reduction="none")
67
+ # # we need to sum up to a maximum of one, so normalize based on strength
68
+ # min_strength = 1 / focus_strength
69
+ # max_strength = 1 - min_strength
70
+ # # focus map is normalized between 0 and 1
71
+ # normalized_focus = min_strength + focus_map_exp * max_strength
72
+ # weighted_loss = base_loss * normalized_focus
73
+ #
74
+ # mse_loss = weighted_loss.mean()
75
+ # else:
76
+ #
77
+
78
+ # loss_color = global_color_loss(comp_lab, target_lab)
79
+
80
+ # if pixel_height_logits is not None:
81
+ # # Existing neighbor-based smoothness loss:
82
+ # target_gray = target.mean(dim=2) # shape becomes [H, W]
83
+ # weight_x = torch.exp(-torch.abs(target_gray[:, 1:] - target_gray[:, :-1]))
84
+ # weight_y = torch.exp(-torch.abs(target_gray[1:, :] - target_gray[:-1, :]))
85
+ # weight_x = torch.clamp(weight_x, 0.5, 1.0)
86
+ # weight_y = torch.clamp(weight_y, 0.5, 1.0)
87
+ # dx = torch.abs(pixel_height_logits[:, 1:] - pixel_height_logits[:, :-1])
88
+ # dy = torch.abs(pixel_height_logits[1:, :] - pixel_height_logits[:-1, :])
89
+ # loss_dx = torch.mean(F.huber_loss(dx * weight_x, torch.zeros_like(dx)))
90
+ # loss_dy = torch.mean(F.huber_loss(dy * weight_y, torch.zeros_like(dy)))
91
+ # smoothness_loss = (loss_dx + loss_dy) * add_penalty_loss
92
+ #
93
+ # # Additional patch-based smoothness loss (using a 3x3 Laplacian):
94
+ # laplacian_kernel = (
95
+ # torch.tensor(
96
+ # [[0, 1, 0], [1, -4, 1], [0, 1, 0]],
97
+ # dtype=pixel_height_logits.dtype,
98
+ # device=pixel_height_logits.device,
99
+ # )
100
+ # .unsqueeze(0)
101
+ # .unsqueeze(0)
102
+ # )
103
+ # height_map = pixel_height_logits.unsqueeze(0).unsqueeze(0)
104
+ # laplacian_output = F.conv2d(height_map, laplacian_kernel, padding=1)
105
+ # patch_smooth_loss = F.huber_loss(
106
+ # laplacian_output, torch.zeros_like(laplacian_output)
107
+ # )
108
+ # total_loss = mse_loss + smoothness_loss + add_penalty_loss * patch_smooth_loss
109
+ # else:
@@ -5,6 +5,12 @@ import matplotlib.pyplot as plt
5
5
  import numpy as np
6
6
  import torch
7
7
  from torch.utils.tensorboard import SummaryWriter
8
+ from torchcam.methods import SmoothGradCAMpp
9
+ from torchcam.utils import overlay_mask
10
+ from torchvision import models
11
+ from torchvision.models import ResNet18_Weights
12
+ from torchvision.transforms._functional_tensor import resize, normalize
13
+ from torchvision.transforms.functional import to_pil_image
8
14
  from tqdm import tqdm
9
15
 
10
16
  from autoforge.Helper.CAdamW import CAdamW
@@ -95,7 +101,9 @@ class FilamentOptimizer:
95
101
 
96
102
  # Tau schedule
97
103
  self.num_steps_done = 0
98
- self.warmup_steps = min(args.iterations-1,args.warmup_fraction * args.iterations)
104
+ self.warmup_steps = min(
105
+ args.iterations - 1, args.warmup_fraction * args.iterations
106
+ )
99
107
  self.decay_rate = (self.init_tau - self.final_tau) / (
100
108
  args.iterations - self.warmup_steps
101
109
  )
@@ -106,6 +114,49 @@ class FilamentOptimizer:
106
114
  lr=self.learning_rate,
107
115
  )
108
116
 
117
+ # Setup the focus map component
118
+ # Load a state-of-the-art ResNet50 model with pretrained weights.
119
+ cam_model = models.resnet18(weights=ResNet18_Weights.DEFAULT).to(
120
+ self.target.device
121
+ )
122
+ cam_model.eval()
123
+ # Use ScoreCAM from torchcam on the 'layer4' block.
124
+ cam_extractor = SmoothGradCAMpp(cam_model, target_layer="layer4")
125
+ # Preprocess the target image for ResNet50.
126
+ target_preprocessed = normalize(
127
+ resize(self.target.permute(2, 0, 1), (224, 224)) / 255.0,
128
+ [0.485, 0.456, 0.406],
129
+ [0.229, 0.224, 0.225],
130
+ )
131
+ focus_maps = []
132
+ for _ in tqdm(range(1), desc="Creating loss function attention map..."):
133
+ out = cam_model(target_preprocessed.unsqueeze(0))
134
+ # ScoreCAM returns a dict with keys corresponding to layers.
135
+ focus_map = cam_extractor(out.squeeze(0).argmax().item(), out)[0]
136
+ # Normalize the focus map to [0, 1]
137
+ focus_map = (focus_map - focus_map.min()) / (
138
+ focus_map.max() - focus_map.min() + 1e-8
139
+ )
140
+ focus_maps.append(focus_map)
141
+ # get maximum over all maps
142
+ self.focus_map = torch.stack(focus_maps)
143
+ self.focus_map = self.focus_map.max(dim=0)[0]
144
+ # normalize again
145
+ self.focus_map = (self.focus_map - self.focus_map.min()) / (
146
+ self.focus_map.max() - self.focus_map.min() + 1e-8
147
+ )
148
+
149
+ # resize back to target size, current dim is [1,h,w]
150
+ self.focus_map = (
151
+ torch.nn.functional.interpolate(
152
+ self.focus_map.unsqueeze(0),
153
+ (self.H, self.W),
154
+ mode="bilinear",
155
+ )
156
+ .squeeze(0)
157
+ .squeeze(0)
158
+ )
159
+
109
160
  # Setup best discrete solution tracking
110
161
  self.best_discrete_loss = float("inf")
111
162
  self.best_params = None
@@ -137,17 +188,24 @@ class FilamentOptimizer:
137
188
  self.depth_map_ax = self.ax[1, 0].imshow(
138
189
  np.zeros((self.H, self.W), dtype=np.uint8), cmap="viridis"
139
190
  )
140
- self.ax[1, 0].set_title("Current Depth Map")
191
+ self.ax[1, 0].set_title("Current Height Map")
141
192
 
142
193
  self.diff_depth_map_ax = self.ax[1, 1].imshow(
143
194
  np.zeros((self.H, self.W), dtype=np.uint8), cmap="viridis"
144
195
  )
145
- self.ax[1, 1].set_title("Depth Map Difference")
196
+ self.ax[1, 1].set_title("Height Map Changes")
146
197
 
147
- self.disc_depth_map_ax = self.ax[1, 2].imshow(
148
- np.zeros((self.H, self.W), dtype=np.uint8), cmap="viridis"
198
+ target_vis = self.target.cpu().detach().numpy()
199
+ focus_vis = self.focus_map.cpu().detach().numpy()
200
+
201
+ overlay = overlay_mask(
202
+ to_pil_image(target_vis),
203
+ to_pil_image(focus_vis, mode="F"),
204
+ alpha=0.5,
149
205
  )
150
- self.ax[1, 2].set_title("Discrete Depth Map")
206
+
207
+ self.focus_map_ax = self.ax[1, 2].imshow(overlay)
208
+ self.ax[1, 2].set_title("Image Focus Map (currently unused)")
151
209
 
152
210
  # Compute and store the initial height map for later difference computation.
153
211
  with torch.no_grad():
@@ -188,17 +246,8 @@ class FilamentOptimizer:
188
246
 
189
247
  tau_height, tau_global = self._get_tau()
190
248
 
191
- if self.num_steps_done < self.args.iterations // 10:
192
- penalty_loss = 0
193
- else:
194
- penalty_loss = min(
195
- 1.0,
196
- (self.num_steps_done - self.args.iterations // 10)
197
- / (self.args.iterations // 5),
198
- )
199
-
200
- #start_fraction = self.args.height_logits_learning_start_fraction
201
- #full_fraction = self.args.height_logits_learning_full_fraction
249
+ # start_fraction = self.args.height_logits_learning_start_fraction
250
+ # full_fraction = self.args.height_logits_learning_full_fraction
202
251
 
203
252
  # total_iters = self.args.iterations
204
253
  # current_step = self.num_steps_done
@@ -219,18 +268,17 @@ class FilamentOptimizer:
219
268
  material_colors=self.material_colors,
220
269
  material_TDs=self.material_TDs,
221
270
  background=self.background,
222
- perception_loss_module=self.perception_loss_module,
223
- add_penalty_loss=1.0,
271
+ add_penalty_loss=10.0,
272
+ focus_map=self.focus_map,
273
+ focus_strength=0.0,
224
274
  )
225
275
 
226
276
  loss.backward()
227
277
 
228
- # We scale the gradients for the height logits by a factor of 'scaling' to force no updates in the beginning
229
- # and allow stronger and stronger updates as we go along.
278
+ # We scale the gradients for the height logits by a factor to only allow updates for very strong (wrong layer/color) gradients.
230
279
  if self.params["pixel_height_logits"].grad is not None:
231
280
  self.params["pixel_height_logits"].grad.mul_(1e-8)
232
281
 
233
-
234
282
  self.optimizer.step()
235
283
 
236
284
  self.num_steps_done += 1
@@ -349,24 +397,6 @@ class FilamentOptimizer:
349
397
  )
350
398
  self.best_comp_ax.set_data(best_comp_np)
351
399
 
352
- with torch.no_grad():
353
- height_map = (self.max_layers * self.h) * torch.sigmoid(
354
- self.best_params["pixel_height_logits"]
355
- )
356
- height_map = height_map.cpu().detach().numpy()
357
-
358
- # Normalize safely, checking for a constant image.
359
- if np.allclose(height_map.max(), height_map.min()):
360
- height_map_norm = np.zeros_like(height_map)
361
- else:
362
- height_map_norm = (height_map - height_map.min()) / (
363
- height_map.max() - height_map.min()
364
- )
365
-
366
- height_map_uint8 = (height_map_norm * 255).astype(np.uint8)
367
- self.disc_depth_map_ax.set_data(height_map_uint8)
368
- self.disc_depth_map_ax.set_clim(0, 255)
369
-
370
400
  # Update the depth map correctly.
371
401
  with torch.no_grad():
372
402
  height_map = (self.max_layers * self.h) * torch.sigmoid(
@@ -489,6 +519,7 @@ class FilamentOptimizer:
489
519
  max_swaps_allowed: int,
490
520
  min_layers_allowed: int,
491
521
  max_layers_allowed: int,
522
+ search_seed: bool = True,
492
523
  ):
493
524
  # Now run pruning
494
525
  from autoforge.Helper.PruningHelper import (
@@ -509,19 +540,22 @@ class FilamentOptimizer:
509
540
  self.final_tau,
510
541
  None,
511
542
  )
512
- self.rng_seed_search(self.best_discrete_loss, 100, autoset_seed=True)
543
+ if search_seed:
544
+ self.rng_seed_search(self.best_discrete_loss, 100, autoset_seed=True)
513
545
  prune_num_swaps(
514
546
  self,
515
547
  max_swaps_allowed,
516
548
  self.final_tau,
517
549
  None,
518
550
  )
519
- self.rng_seed_search(self.best_discrete_loss, 100, autoset_seed=True)
551
+ if search_seed:
552
+ self.rng_seed_search(self.best_discrete_loss, 100, autoset_seed=True)
520
553
 
521
554
  # prune_fireflies(self)
522
555
 
523
556
  prune_redundant_layers(self, None, min_layers_allowed, max_layers_allowed)
524
- self.rng_seed_search(self.best_discrete_loss, 100, autoset_seed=True)
557
+ if search_seed:
558
+ self.rng_seed_search(self.best_discrete_loss, 100, autoset_seed=True)
525
559
 
526
560
  def _maybe_update_best_discrete(self):
527
561
  """
@@ -133,6 +133,12 @@ def main():
133
133
  default=True,
134
134
  help="Perform pruning after optimization",
135
135
  )
136
+ parser.add_argument(
137
+ "--pruning_rounds",
138
+ type=int,
139
+ default=2,
140
+ help="Number of pruning rounds to perform",
141
+ )
136
142
  parser.add_argument(
137
143
  "--pruning_max_colors",
138
144
  type=int,
@@ -211,6 +217,18 @@ def main():
211
217
  parser.add_argument(
212
218
  "--tensorboard", action="store_true", help="Enable TensorBoard logging"
213
219
  )
220
+ parser.add_argument(
221
+ "--num_init_rounds",
222
+ type=int,
223
+ default=64,
224
+ help="Number of rounds to choose the starting height map from.",
225
+ )
226
+ parser.add_argument(
227
+ "--num_init_cluster_layers",
228
+ type=int,
229
+ default=18,
230
+ help="Number of layers to cluster the image into.",
231
+ )
214
232
 
215
233
  args = parser.parse_args()
216
234
 
@@ -302,9 +320,9 @@ def main():
302
320
  args.layer_height,
303
321
  bgr_tuple,
304
322
  random_seed=random_seed,
305
- num_threads=16,
323
+ num_threads=args.num_init_rounds,
306
324
  init_method="kmeans",
307
- cluster_layers=18,
325
+ cluster_layers=args.num_init_cluster_layers,
308
326
  lab_space=True,
309
327
  )
310
328
 
@@ -360,15 +378,20 @@ def main():
360
378
  )
361
379
 
362
380
  if args.perform_pruning:
363
- optimizer.prune(
364
- max_colors_allowed=args.pruning_max_colors,
365
- max_swaps_allowed=args.pruning_max_swaps,
366
- min_layers_allowed=args.min_layers,
367
- max_layers_allowed=args.pruning_max_layer,
368
- )
369
- optimizer.log_to_tensorboard(
370
- interval=1, namespace="post_opt", step=(post_opt_step := post_opt_step + 1)
371
- )
381
+ for i in range(args.pruning_rounds):
382
+ print(f"Pruning round {i + 1}/{args.pruning_rounds}:")
383
+ optimizer.prune(
384
+ max_colors_allowed=args.pruning_max_colors,
385
+ max_swaps_allowed=args.pruning_max_swaps,
386
+ min_layers_allowed=args.min_layers,
387
+ max_layers_allowed=args.pruning_max_layer,
388
+ search_seed=True if i == 0 else False,
389
+ )
390
+ optimizer.log_to_tensorboard(
391
+ interval=1,
392
+ namespace="post_opt",
393
+ step=(post_opt_step := post_opt_step + 1),
394
+ )
372
395
 
373
396
  disc_global, disc_height_image = optimizer.get_discretized_solution(best=True)
374
397
 
@@ -1,91 +0,0 @@
1
- import torch
2
- import torch.nn.functional as F
3
-
4
- from autoforge.Helper.ImageHelper import srgb_to_lab
5
- from autoforge.Helper.OptimizerHelper import composite_image_cont
6
-
7
-
8
- def loss_fn(
9
- params: dict,
10
- target: torch.Tensor,
11
- tau_height: float,
12
- tau_global: float,
13
- h: float,
14
- max_layers: int,
15
- material_colors: torch.Tensor,
16
- material_TDs: torch.Tensor,
17
- background: torch.Tensor,
18
- perception_loss_module: torch.nn.Module,
19
- add_penalty_loss: float = 0.0,
20
- ) -> torch.Tensor:
21
- """
22
- Full forward pass for continuous assignment:
23
- composite, then compute unified loss on (global_logits).
24
- """
25
- comp = composite_image_cont(
26
- params["pixel_height_logits"],
27
- params["global_logits"],
28
- tau_height,
29
- tau_global,
30
- h,
31
- max_layers,
32
- material_colors,
33
- material_TDs,
34
- background,
35
- )
36
- return compute_loss(
37
- comp=comp,
38
- target=target,
39
- pixel_height_logits=params["pixel_height_logits"],
40
- tau_height=tau_height,
41
- add_penalty_loss=add_penalty_loss,
42
- )
43
-
44
-
45
- def compute_loss(
46
- comp: torch.Tensor,
47
- target: torch.Tensor,
48
- pixel_height_logits: torch.Tensor = None,
49
- tau_height: float = 1.0,
50
- add_penalty_loss: float = 0.0,
51
- ) -> torch.Tensor:
52
- """
53
- Combined MSE + Perceptual + penalty losses with patch-based smoothness.
54
- """
55
- # MSE Loss
56
- comp_mse = srgb_to_lab(comp)
57
- target_mse = srgb_to_lab(target)
58
- mse_loss = F.huber_loss(comp_mse, target_mse)
59
-
60
- if pixel_height_logits is not None:
61
- # Existing neighbor-based smoothness loss:
62
- target_gray = target.mean(dim=2) # shape becomes [H, W]
63
- weight_x = torch.exp(-torch.abs(target_gray[:, 1:] - target_gray[:, :-1]))
64
- weight_y = torch.exp(-torch.abs(target_gray[1:, :] - target_gray[:-1, :]))
65
- weight_x = torch.clamp(weight_x, 0.5, 1.0)
66
- weight_y = torch.clamp(weight_y, 0.5, 1.0)
67
- dx = torch.abs(pixel_height_logits[:, 1:] - pixel_height_logits[:, :-1])
68
- dy = torch.abs(pixel_height_logits[1:, :] - pixel_height_logits[:-1, :])
69
- loss_dx = torch.mean(F.huber_loss(dx * weight_x, torch.zeros_like(dx)))
70
- loss_dy = torch.mean(F.huber_loss(dy * weight_y, torch.zeros_like(dy)))
71
- smoothness_loss = (loss_dx + loss_dy) * (10 * add_penalty_loss)
72
-
73
- # Additional patch-based smoothness loss (using a 3x3 Laplacian):
74
- laplacian_kernel = (
75
- torch.tensor(
76
- [[0, 1, 0], [1, -4, 1], [0, 1, 0]],
77
- dtype=pixel_height_logits.dtype,
78
- device=pixel_height_logits.device,
79
- )
80
- .unsqueeze(0)
81
- .unsqueeze(0)
82
- )
83
- height_map = pixel_height_logits.unsqueeze(0).unsqueeze(0)
84
- laplacian_output = F.conv2d(height_map, laplacian_kernel, padding=1)
85
- patch_smooth_loss = (
86
- F.huber_loss(laplacian_output, torch.zeros_like(laplacian_output)) * 10
87
- )
88
- total_loss = mse_loss + smoothness_loss + add_penalty_loss * patch_smooth_loss
89
- else:
90
- total_loss = mse_loss
91
- return total_loss
File without changes