autoforge 1.6.4__tar.gz → 1.7.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 (42) hide show
  1. {autoforge-1.6.4 → autoforge-1.7.0}/PKG-INFO +5 -3
  2. {autoforge-1.6.4 → autoforge-1.7.0}/README.md +4 -2
  3. {autoforge-1.6.4 → autoforge-1.7.0}/pyproject.toml +1 -1
  4. {autoforge-1.6.4 → autoforge-1.7.0}/src/AutoForge.egg-info/PKG-INFO +5 -3
  5. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py +66 -11
  6. autoforge-1.7.0/src/autoforge/Helper/OptimizerHelper.py +302 -0
  7. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/PruningHelper.py +303 -201
  8. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Loss/LossFunctions.py +36 -2
  9. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Modules/Optimizer.py +54 -37
  10. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/auto_forge.py +70 -43
  11. autoforge-1.6.4/src/autoforge/Helper/OptimizerHelper.py +0 -466
  12. {autoforge-1.6.4 → autoforge-1.7.0}/setup.cfg +0 -0
  13. {autoforge-1.6.4 → autoforge-1.7.0}/src/AutoForge.egg-info/SOURCES.txt +0 -0
  14. {autoforge-1.6.4 → autoforge-1.7.0}/src/AutoForge.egg-info/dependency_links.txt +0 -0
  15. {autoforge-1.6.4 → autoforge-1.7.0}/src/AutoForge.egg-info/entry_points.txt +0 -0
  16. {autoforge-1.6.4 → autoforge-1.7.0}/src/AutoForge.egg-info/requires.txt +0 -0
  17. {autoforge-1.6.4 → autoforge-1.7.0}/src/AutoForge.egg-info/top_level.txt +0 -0
  18. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Gui/__init__.py +0 -0
  19. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/Views/__init__.py +0 -0
  20. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/Views/backend_database.py +0 -0
  21. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/Views/filament_editor.py +0 -0
  22. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/app.py +0 -0
  23. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/main.py +0 -0
  24. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/CAdamW.py +0 -0
  25. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/FilamentHelper.py +0 -0
  26. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/DepthEstimateHeightMap.py +0 -0
  27. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/OldHeightmap.py +0 -0
  28. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/__init__.py +0 -0
  29. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/ImageHelper.py +0 -0
  30. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/OutputHelper.py +0 -0
  31. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/__init__.py +0 -0
  32. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/filamentcolors_library.py +0 -0
  33. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Init_comparer/__init__.py +0 -0
  34. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Init_comparer/auto_forge_init_check.py +0 -0
  35. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Init_comparer/plot_results.py +0 -0
  36. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Loss/PerceptionLoss.py +0 -0
  37. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Loss/__init__.py +0 -0
  38. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Misc/__init__.py +0 -0
  39. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Misc/color_fit_all.py +0 -0
  40. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Modules/__init__.py +0 -0
  41. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/__init__.py +0 -0
  42. {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/__main__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AutoForge
3
- Version: 1.6.4
3
+ Version: 1.7.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
@@ -116,7 +116,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
116
116
  - `--csv_file` **(Required)** Path to the CSV file containing material data. The CSV should include columns for the brand, name, color (hex code), and TD values.
117
117
 
118
118
  - `--output_folder` Folder where output files will be saved (default: `output`).
119
- - `--iterations` Number of optimization iterations (default: 5000).
119
+ - `--iterations` Number of optimization iterations (default: 2000).
120
120
  - `--learning_rate` Learning rate for optimization (default: 0.015).
121
121
  - `--layer_height` Layer thickness in millimeters (default: 0.04).
122
122
  - `--max_layers` Maximum number of layers (default: 75).
@@ -127,7 +127,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
127
127
  **Note:** The solver currently assumes that you have a solid color in the background, which means a color with a TD value of 4 or less (if you have a background height of 0.4).
128
128
  - `--init_tau` Initial tau value for Gumbel-Softmax (default: 1.0).
129
129
  - `--final_tau` Final tau value for the Gumbel-Softmax formulation (default: 0.01).
130
- - `--visualize` Flag to enable live visualization of the composite image during optimization.
130
+ - `--visualize` enable live visualization of the composite image during optimization (default: True).
131
131
  - `--stl_output_size` Size of the longest dimension of the output STL file in millimeters (default: 150).
132
132
  - `--nozzle_diameter` Diameter of the printer nozzle in millimeters (default: 0.4).
133
133
  **Note:** Details smaller than half this value will be ignored.
@@ -136,6 +136,8 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
136
136
 
137
137
  - `--perform_pruning` Perform pruning after optimization (default: True).
138
138
  **Note:** This is highly recommended even if you don't have a color/color swap limit, as it actually increases the quality of the output.
139
+ - `--fast_pruning` Perform pruning in chunks. 10-15x speedup compared to accurate method (default: True).
140
+ - `--fast_pruning_percent` Size of fast pruning chunks in percent (default: 0.05) (5%).
139
141
  - `--pruning_max_colors` Max number of colors allowed after pruning (default: 100).
140
142
  - `--pruning_max_swaps` Max number of swaps allowed after pruning (default: 100).
141
143
  - `--pruning_max_layer` Max number of layers allowed after pruning (default: 75).
@@ -90,7 +90,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
90
90
  - `--csv_file` **(Required)** Path to the CSV file containing material data. The CSV should include columns for the brand, name, color (hex code), and TD values.
91
91
 
92
92
  - `--output_folder` Folder where output files will be saved (default: `output`).
93
- - `--iterations` Number of optimization iterations (default: 5000).
93
+ - `--iterations` Number of optimization iterations (default: 2000).
94
94
  - `--learning_rate` Learning rate for optimization (default: 0.015).
95
95
  - `--layer_height` Layer thickness in millimeters (default: 0.04).
96
96
  - `--max_layers` Maximum number of layers (default: 75).
@@ -101,7 +101,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
101
101
  **Note:** The solver currently assumes that you have a solid color in the background, which means a color with a TD value of 4 or less (if you have a background height of 0.4).
102
102
  - `--init_tau` Initial tau value for Gumbel-Softmax (default: 1.0).
103
103
  - `--final_tau` Final tau value for the Gumbel-Softmax formulation (default: 0.01).
104
- - `--visualize` Flag to enable live visualization of the composite image during optimization.
104
+ - `--visualize` enable live visualization of the composite image during optimization (default: True).
105
105
  - `--stl_output_size` Size of the longest dimension of the output STL file in millimeters (default: 150).
106
106
  - `--nozzle_diameter` Diameter of the printer nozzle in millimeters (default: 0.4).
107
107
  **Note:** Details smaller than half this value will be ignored.
@@ -110,6 +110,8 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
110
110
 
111
111
  - `--perform_pruning` Perform pruning after optimization (default: True).
112
112
  **Note:** This is highly recommended even if you don't have a color/color swap limit, as it actually increases the quality of the output.
113
+ - `--fast_pruning` Perform pruning in chunks. 10-15x speedup compared to accurate method (default: True).
114
+ - `--fast_pruning_percent` Size of fast pruning chunks in percent (default: 0.05) (5%).
113
115
  - `--pruning_max_colors` Max number of colors allowed after pruning (default: 100).
114
116
  - `--pruning_max_swaps` Max number of swaps allowed after pruning (default: 100).
115
117
  - `--pruning_max_layer` Max number of layers allowed after pruning (default: 75).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "AutoForge"
7
- version="1.6.4"
7
+ version="1.7.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.4
2
2
  Name: AutoForge
3
- Version: 1.6.4
3
+ Version: 1.7.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
@@ -116,7 +116,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
116
116
  - `--csv_file` **(Required)** Path to the CSV file containing material data. The CSV should include columns for the brand, name, color (hex code), and TD values.
117
117
 
118
118
  - `--output_folder` Folder where output files will be saved (default: `output`).
119
- - `--iterations` Number of optimization iterations (default: 5000).
119
+ - `--iterations` Number of optimization iterations (default: 2000).
120
120
  - `--learning_rate` Learning rate for optimization (default: 0.015).
121
121
  - `--layer_height` Layer thickness in millimeters (default: 0.04).
122
122
  - `--max_layers` Maximum number of layers (default: 75).
@@ -127,7 +127,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
127
127
  **Note:** The solver currently assumes that you have a solid color in the background, which means a color with a TD value of 4 or less (if you have a background height of 0.4).
128
128
  - `--init_tau` Initial tau value for Gumbel-Softmax (default: 1.0).
129
129
  - `--final_tau` Final tau value for the Gumbel-Softmax formulation (default: 0.01).
130
- - `--visualize` Flag to enable live visualization of the composite image during optimization.
130
+ - `--visualize` enable live visualization of the composite image during optimization (default: True).
131
131
  - `--stl_output_size` Size of the longest dimension of the output STL file in millimeters (default: 150).
132
132
  - `--nozzle_diameter` Diameter of the printer nozzle in millimeters (default: 0.4).
133
133
  **Note:** Details smaller than half this value will be ignored.
@@ -136,6 +136,8 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
136
136
 
137
137
  - `--perform_pruning` Perform pruning after optimization (default: True).
138
138
  **Note:** This is highly recommended even if you don't have a color/color swap limit, as it actually increases the quality of the output.
139
+ - `--fast_pruning` Perform pruning in chunks. 10-15x speedup compared to accurate method (default: True).
140
+ - `--fast_pruning_percent` Size of fast pruning chunks in percent (default: 0.05) (5%).
139
141
  - `--pruning_max_colors` Max number of colors allowed after pruning (default: 100).
140
142
  - `--pruning_max_swaps` Max number of swaps allowed after pruning (default: 100).
141
143
  - `--pruning_max_layer` Max number of layers allowed after pruning (default: 75).
@@ -321,6 +321,23 @@ def compute_ordering_metric(ordering, labs):
321
321
 
322
322
 
323
323
  # --- Revised init_height_map with Optimizations ---
324
+ def interpolate_arrays(value_array_pairs, num_points):
325
+ # Sort pairs by the value (first element in each pair)
326
+ value_array_pairs.sort(key=lambda x: x[0])
327
+
328
+ values = np.array([pair[0] for pair in value_array_pairs])
329
+ arrays = np.array([pair[1] for pair in value_array_pairs])
330
+
331
+ # Generate the new interpolation positions
332
+ new_values = np.linspace(values[0], values[-1], num_points)
333
+
334
+ # Interpolate each element in the arrays
335
+ interpolated_arrays = []
336
+ for i in range(arrays.shape[1]): # Assuming arrays are 2D or more
337
+ interpolated_column = np.interp(new_values, values, arrays[:, i])
338
+ interpolated_arrays.append(interpolated_column)
339
+
340
+ return np.stack(interpolated_arrays, axis=1) # Reconstruct the interpolated array
324
341
 
325
342
 
326
343
  def init_height_map(
@@ -334,6 +351,7 @@ def init_height_map(
334
351
  init_method="quantize_maxcoverage",
335
352
  cluster_layers=None,
336
353
  lab_space=True,
354
+ material_colors=None,
337
355
  ):
338
356
  """
339
357
  init_method should be one of quantize_median,quantize_maxcoverage,quantize_fastoctree,kmeans
@@ -453,17 +471,53 @@ def init_height_map(
453
471
  # Get the ordering via TSP ordering function.
454
472
  final_ordering = tsp_order_christofides_path(nodes, labs, bg_cluster, fg_cluster)
455
473
 
456
- # Optionally prune out outliers.
457
- final_ordering = prune_ordering(
458
- final_ordering, labs, bg_cluster, fg_cluster, min_length=3, improvement_factor=3
459
- )
474
+ # # Optionally prune out outliers.
475
+ # final_ordering = prune_ordering(
476
+ # final_ordering, labs, bg_cluster, fg_cluster, min_length=3, improvement_factor=3
477
+ # )
460
478
 
461
479
  # Create a mapping that covers all clusters.
462
480
  new_values = create_mapping(final_ordering, labs, unique_clusters)
463
481
  new_labels = np.vectorize(lambda x: new_values[x])(labels).astype(np.float32)
482
+
464
483
  pixel_height_logits = np.log((new_labels + eps) / (1 - new_labels + eps))
465
484
  ordering_metric = compute_ordering_metric(final_ordering, labs)
466
- return pixel_height_logits, ordering_metric
485
+
486
+ global_logits_out = None
487
+ if material_colors is not None:
488
+ # Convert material_colors (assumed in [-1, 3]) to normalized [0, 1] for Lab conversion.
489
+ # Adjust as needed depending on your color convention.
490
+ # Reshape to (1, num_materials, 3) so that rgb2lab returns shape (1, num_materials, 3)
491
+ if lab_space:
492
+ material_lab = rgb2lab(material_colors.reshape(1, -1, 3)).reshape(-1, 3)
493
+ # Apply the same lab_weights.
494
+ material_lab[:, 0] *= lab_weights[0]
495
+ material_lab[:, 1] *= lab_weights[1]
496
+ material_lab[:, 2] *= lab_weights[2]
497
+ materials = material_colors
498
+ else:
499
+ materials = material_colors
500
+
501
+ num_materials = materials.shape[0]
502
+
503
+ # Initialize global logits for each cluster in unique_clusters.
504
+ global_logits = []
505
+
506
+ for idx, label in enumerate(unique_clusters):
507
+ # Use the final mapping value for this cluster.
508
+ t = new_values[label]
509
+ # Compute distances from this cluster’s lab value to each material (in Lab space).
510
+ cluster_lab = labs[label]
511
+ dists = np.linalg.norm(materials - cluster_lab, axis=1)
512
+ best_j = np.argmin(dists)
513
+ out_logit = np.ones(num_materials) * -1.0
514
+ out_logit[best_j] = 1.0
515
+ global_logits.append((t, out_logit))
516
+
517
+ global_logits = sorted(global_logits, key=lambda x: x[0])
518
+ global_logits_out = interpolate_arrays(global_logits, max_layers)
519
+
520
+ return pixel_height_logits, global_logits_out, ordering_metric
467
521
 
468
522
 
469
523
  def run_init_threads(
@@ -474,13 +528,13 @@ def run_init_threads(
474
528
  eps=1e-6,
475
529
  random_seed=None,
476
530
  num_threads=64,
477
- init_method="quantize_maxcoverage",
531
+ init_method="kmeans",
478
532
  cluster_layers=None,
479
- lab_space=True,
533
+ material_colors=None,
480
534
  ):
481
535
  if random_seed is None:
482
536
  random_seed = np.random.randint(1e6)
483
-
537
+ lab_space = False
484
538
  tasks = [
485
539
  delayed(init_height_map)(
486
540
  target,
@@ -492,6 +546,7 @@ def run_init_threads(
492
546
  init_method=init_method,
493
547
  cluster_layers=cluster_layers,
494
548
  lab_space=lab_space,
549
+ material_colors=material_colors,
495
550
  )
496
551
  for i in range(num_threads)
497
552
  ]
@@ -499,7 +554,7 @@ def run_init_threads(
499
554
  # Execute tasks in parallel; adjust n_jobs to match your available cores
500
555
  results = Parallel(n_jobs=os.cpu_count(), verbose=10)(tasks)
501
556
 
502
- metrics = [r[1] for r in results]
557
+ metrics = [r[2] for r in results]
503
558
  mean_metric = np.mean(metrics)
504
559
  std_metric = np.std(metrics)
505
560
  min_metric = np.min(metrics)
@@ -508,5 +563,5 @@ def run_init_threads(
508
563
  f"mean: {mean_metric}, std: {std_metric}, min: {min_metric}, max: {max_metric}"
509
564
  )
510
565
  print(f"Choosing best ordering with metric: {min_metric}")
511
- best_result = min(results, key=lambda x: x[1])
512
- return best_result[0]
566
+ best_result = min(results, key=lambda x: x[2])
567
+ return best_result[0], best_result[1]
@@ -0,0 +1,302 @@
1
+ import torch
2
+ import torch.nn.functional as F
3
+
4
+
5
+ @torch.jit.script
6
+ def adaptive_round(
7
+ x: torch.Tensor, tau: float, high_tau: float, low_tau: float, temp: float
8
+ ) -> torch.Tensor:
9
+ """
10
+ Smooth rounding based on temperature 'tau'.
11
+
12
+ Args:
13
+ x (torch.Tensor): The input tensor to be rounded.
14
+ tau (float): The current temperature parameter.
15
+ high_tau (float): The high threshold for the temperature.
16
+ low_tau (float): The low threshold for the temperature.
17
+ temp (float): The temperature parameter for the sigmoid function.
18
+
19
+ Returns:
20
+ torch.Tensor: The rounded tensor.
21
+ """
22
+ if tau <= low_tau:
23
+ return torch.round(x)
24
+ elif tau >= high_tau:
25
+ floor_val = torch.floor(x)
26
+ diff = x - floor_val
27
+ soft_round = floor_val + torch.sigmoid((diff - 0.5) / temp)
28
+ return soft_round
29
+ else:
30
+ ratio = (tau - low_tau) / (high_tau - low_tau)
31
+ hard_round = torch.round(x)
32
+ floor_val = torch.floor(x)
33
+ diff = x - floor_val
34
+ soft_round = floor_val + torch.sigmoid((diff - 0.5) / temp)
35
+ return ratio * soft_round + (1 - ratio) * hard_round
36
+
37
+
38
+ # A deterministic random generator that mimics torch.rand_like.
39
+ @torch.jit.script
40
+ def deterministic_rand_like(tensor: torch.Tensor, seed: int) -> torch.Tensor:
41
+ """
42
+ Generate a deterministic random tensor that mimics torch.rand_like.
43
+
44
+ Args:
45
+ tensor (torch.Tensor): The input tensor whose shape and device will be used.
46
+ seed (int): The seed for the deterministic random generator.
47
+
48
+ Returns:
49
+ torch.Tensor: A tensor with the same shape as the input tensor, filled with deterministic random values.
50
+ """
51
+ # Compute the total number of elements.
52
+ n: int = 1
53
+ for d in tensor.shape:
54
+ n = n * d
55
+ # Create a 1D tensor of indices [0, 1, 2, ..., n-1].
56
+ indices = torch.arange(n, dtype=torch.float32, device=tensor.device)
57
+ # Offset the indices by the seed.
58
+ indices = indices + seed
59
+ # Use a simple hash function: sin(x)*constant, then take the fractional part.
60
+ r = torch.sin(indices) * 43758.5453123
61
+ r = r - torch.floor(r)
62
+ # Reshape to the shape of the original tensor.
63
+ return r.view(tensor.shape)
64
+
65
+
66
+ @torch.jit.script
67
+ def deterministic_gumbel_softmax(
68
+ logits: torch.Tensor, tau: float, hard: bool, rng_seed: int
69
+ ) -> torch.Tensor:
70
+ """
71
+ Apply the Gumbel-Softmax trick in a deterministic manner using a fixed random seed.
72
+
73
+ Args:
74
+ logits (torch.Tensor): The input logits tensor.
75
+ tau (float): The temperature parameter for the Gumbel-Softmax.
76
+ hard (bool): If True, the output will be one-hot encoded.
77
+ rng_seed (int): The seed for the deterministic random generator.
78
+
79
+ Returns:
80
+ torch.Tensor: The resulting tensor after applying the Gumbel-Softmax trick.
81
+ """
82
+ eps: float = 1e-20
83
+ # Instead of torch.rand_like(..., generator=...), use our deterministic_rand_like.
84
+ U = deterministic_rand_like(logits, rng_seed)
85
+ # Compute Gumbel noise.
86
+ gumbel_noise = -torch.log(-torch.log(U + eps) + eps)
87
+ y = (logits + gumbel_noise) / tau
88
+ y_soft = F.softmax(y, dim=-1)
89
+ if hard:
90
+ # Compute one-hot using argmax and scatter.
91
+ index = torch.argmax(y_soft, dim=-1, keepdim=True)
92
+ y_hard = torch.zeros_like(y_soft).scatter_(-1, index, 1.0)
93
+ # Use the straight-through estimator.
94
+ y = (y_hard - y_soft).detach() + y_soft
95
+ return y
96
+
97
+
98
+ @torch.jit.script
99
+ def composite_image_cont(
100
+ pixel_height_logits: torch.Tensor, # [H,W]
101
+ global_logits: torch.Tensor, # [L,M]
102
+ tau_height: float,
103
+ tau_global: float,
104
+ h: float,
105
+ max_layers: int,
106
+ material_colors: torch.Tensor, # [M,3]
107
+ material_TDs: torch.Tensor, # [M]
108
+ background: torch.Tensor, # [3]
109
+ ) -> torch.Tensor:
110
+ # 1. per-pixel continuous layer index
111
+ pixel_height = (max_layers * h) * torch.sigmoid(pixel_height_logits) # [H,W]
112
+ continuous_z = pixel_height / h # [H,W]
113
+
114
+ # 2. global material weights with Gumbel-Softmax
115
+ hard_flag = tau_global < 1e-3
116
+ p_mat = F.gumbel_softmax(global_logits, tau_global, hard=hard_flag, dim=1) # [L,M]
117
+
118
+ layer_colors = p_mat @ material_colors # [L,3]
119
+ layer_TDs = (p_mat @ material_TDs).clamp(1e-8, 1e8) # [L]
120
+
121
+ # 3. soft print mask for all layers (layer 0 = bottom, layer L-1 = top)
122
+ scale = 10.0 * tau_height
123
+ layer_idx = torch.arange(
124
+ max_layers, dtype=torch.float32, device=pixel_height.device
125
+ ).view(-1, 1, 1) # [L,1,1]
126
+ p_print = torch.sigmoid(
127
+ (continuous_z.unsqueeze(0) - (layer_idx + 0.5)) * scale
128
+ ) # [L,H,W]
129
+
130
+ # 4. thickness and opacity
131
+ eff_thick = p_print * h # [L,H,W]
132
+ thick_ratio = eff_thick / layer_TDs.view(-1, 1, 1) # [L,H,W]
133
+
134
+ o, A, k, b = -1.2416557e-02, 9.6407950e-01, 3.4103447e01, -4.1554203e00
135
+ opac = o + (A * torch.log1p(k * thick_ratio) + b * thick_ratio)
136
+ opac = torch.clamp(opac, 0.0, 1.0) # [L,H,W]
137
+
138
+ # 5. flip to top→bottom order before compositing
139
+ opac_fb = torch.flip(opac, dims=[0]) # [L,H,W]
140
+ colors_fb = torch.flip(layer_colors, dims=[0]) # [L,3]
141
+
142
+ trans_fb = 1.0 - opac_fb # [L,H,W]
143
+ trans_shift = torch.cat([torch.ones_like(trans_fb[:1]), trans_fb[:-1]], dim=0)
144
+ remain_fb = torch.cumprod(trans_shift, dim=0) # remaining before each layer [L,H,W]
145
+
146
+ comp_layers = (remain_fb * opac_fb).unsqueeze(-1) * colors_fb.view(
147
+ -1, 1, 1, 3
148
+ ) # [L,H,W,3]
149
+ comp = comp_layers.sum(dim=0) # [H,W,3]
150
+
151
+ # 6. background
152
+ rem_after = remain_fb[-1] * trans_fb[-1] # remaining after bottom layer
153
+ comp = comp + rem_after.unsqueeze(-1) * background # [H,W,3]
154
+ return comp * 255.0
155
+
156
+
157
+ @torch.jit.script
158
+ def _runs_from_materials(mats: torch.Tensor):
159
+ """
160
+ Given a 1D int tensor of per-layer materials (top to bottom),
161
+ return the start indices, end indices (exclusive) and material id
162
+ for each run of equal values.
163
+
164
+ Returns:
165
+ run_starts [R] int64
166
+ run_ends [R] int64
167
+ run_mats [R] same dtype as mats
168
+ """
169
+ L = int(mats.shape[0])
170
+ if L == 0:
171
+ empty_i = torch.empty(0, dtype=torch.int64, device=mats.device)
172
+ return empty_i, empty_i, torch.empty(0, dtype=mats.dtype, device=mats.device)
173
+
174
+ change = torch.ones(L, dtype=torch.bool, device=mats.device)
175
+ change[1:] = mats[1:] != mats[:-1]
176
+
177
+ # TorchScript friendly: no keyword args
178
+ run_starts = torch.nonzero(change).squeeze(1).to(torch.int64) # [R]
179
+ run_ends = torch.cat([run_starts[1:], torch.tensor([L], device=mats.device)])
180
+ run_mats = mats[run_starts] # [R]
181
+
182
+ return run_starts, run_ends, run_mats
183
+
184
+
185
+ @torch.jit.script
186
+ def composite_image_disc(
187
+ pixel_height_logits: torch.Tensor, # [H,W]
188
+ global_logits: torch.Tensor, # [max_layers, n_materials]
189
+ tau_height: float,
190
+ tau_global: float,
191
+ h: float,
192
+ max_layers: int,
193
+ material_colors: torch.Tensor, # [n_materials, 3]
194
+ material_TDs: torch.Tensor, # [n_materials]
195
+ background: torch.Tensor, # [3]
196
+ rng_seed: int = -1,
197
+ ) -> torch.Tensor:
198
+ # 1. discrete layer counts per pixel (with adaptive rounding)
199
+ pixel_height = (max_layers * h) * torch.sigmoid(pixel_height_logits)
200
+ continuous_z = pixel_height / h
201
+ adaptive_layers = adaptive_round(
202
+ continuous_z, tau_height, high_tau=0.1, low_tau=0.01, temp=0.1
203
+ )
204
+ discrete_temp = torch.round(continuous_z)
205
+ discrete_layers = (discrete_temp + (adaptive_layers - discrete_temp).detach()).to(
206
+ torch.int32
207
+ ) # [H,W]
208
+
209
+ # 2. one material per physical layer
210
+ if rng_seed >= 0:
211
+ mats = []
212
+ for i in range(max_layers):
213
+ p_i = deterministic_gumbel_softmax(
214
+ global_logits[i], tau_global, hard=True, rng_seed=rng_seed + i
215
+ )
216
+ mats.append(torch.argmax(p_i).to(torch.int32))
217
+ new_mats = torch.stack(mats, dim=0) # [L]
218
+ else:
219
+ probs = F.gumbel_softmax(global_logits, tau_global, hard=True, dim=1) # [L,M]
220
+ new_mats = torch.argmax(probs, dim=1).to(torch.int32) # [L]
221
+
222
+ # 3. flip to top→bottom order and collect material runs
223
+ new_mats_top = torch.flip(new_mats, dims=[0]) # index 0 = top
224
+ run_starts, run_ends, run_m = _runs_from_materials(new_mats_top) # R runs
225
+
226
+ R = int(run_starts.shape[0])
227
+ H, W = pixel_height.shape
228
+ device = pixel_height.device
229
+ dtype_f = torch.float32
230
+
231
+ # 4. printed layers per run (vectorised) – same logic as the loop
232
+ # top 'D_no_print' layers stay empty
233
+ D_no_print = (max_layers - discrete_layers).to(torch.int32) # [H,W]
234
+ run_s = run_starts.view(R, 1, 1)
235
+ run_e = run_ends.view(R, 1, 1)
236
+ printed_L = torch.clamp(
237
+ run_e - torch.maximum(run_s, D_no_print.unsqueeze(0)), min=0
238
+ ) # [R,H,W]
239
+
240
+ # *** FIX ***
241
+ # add one extra layer thickness where at least one layer is printed
242
+ extra_layer = (printed_L > 0).to(dtype_f) # [R,H,W]
243
+ thickness_r = (printed_L.to(dtype_f) + extra_layer) * h # [R,H,W]
244
+
245
+ # 5. opacity for every run
246
+ TD_r = material_TDs[run_m].view(R, 1, 1) # [R,1,1]
247
+ thick_ratio = thickness_r / TD_r # [R,H,W]
248
+
249
+ o, A, k, b = 0.10868816, 0.3077416, 76.928215, 2.2291653
250
+ opac_r = o + (A * torch.log1p(k * thick_ratio) + b * thick_ratio)
251
+ opac_r = torch.clamp(opac_r, 0.0, 1.0) * (printed_L > 0).to(
252
+ dtype_f
253
+ ) # zero when nothing printed
254
+
255
+ # 6. front-to-back compositing run by run
256
+ trans_r = 1.0 - opac_r
257
+ trans_shift = torch.cat([torch.ones_like(trans_r[:1]), trans_r[:-1]], dim=0)
258
+ remain_r = torch.cumprod(trans_shift, dim=0) # remaining light before run
259
+
260
+ colors_r = material_colors[run_m].view(R, 1, 1, 3) # [R,1,1,3]
261
+ comp_runs = (remain_r * opac_r).unsqueeze(-1) * colors_r # [R,H,W,3]
262
+ comp = comp_runs.sum(dim=0) # [H,W,3]
263
+
264
+ # 7. background
265
+ rem_after = remain_r[-1] * trans_r[-1]
266
+ comp = comp + rem_after.unsqueeze(-1) * background
267
+ return comp * 255.0
268
+
269
+
270
+ def discretize_solution(
271
+ params: dict, tau_global: float, h: float, max_layers: int, rng_seed: int = -1
272
+ ):
273
+ """
274
+ Convert continuous logs to discrete layer counts and discrete color IDs.
275
+
276
+ Args:
277
+ params (dict): Dictionary containing the parameters 'pixel_height_logits' and 'global_logits'.
278
+ tau_global (float): Temperature parameter for global material assignment.
279
+ h (float): Height of each layer.
280
+ max_layers (int): Maximum number of layers.
281
+ rng_seed (int, optional): Random seed for deterministic sampling. Defaults to -1.
282
+
283
+ Returns:
284
+ tuple: A tuple containing:
285
+ - torch.Tensor: Discrete global material assignments, shape [max_layers].
286
+ - torch.Tensor: Discrete height image, shape [H, W].
287
+ """
288
+ pixel_height_logits = params["pixel_height_logits"]
289
+ global_logits = params["global_logits"]
290
+ pixel_heights = (max_layers * h) * torch.sigmoid(pixel_height_logits)
291
+ discrete_height_image = torch.round(pixel_heights / h).to(torch.int32)
292
+ discrete_height_image = torch.clamp(discrete_height_image, 0, max_layers)
293
+
294
+ num_layers = global_logits.shape[0]
295
+ discrete_global_vals = []
296
+ for j in range(num_layers):
297
+ p = deterministic_gumbel_softmax(
298
+ global_logits[j], tau_global, hard=True, rng_seed=rng_seed + j
299
+ )
300
+ discrete_global_vals.append(torch.argmax(p))
301
+ discrete_global = torch.stack(discrete_global_vals, dim=0)
302
+ return discrete_global, discrete_height_image