autoforge 1.6.3__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 (43) hide show
  1. {autoforge-1.6.3 → autoforge-1.7.0}/PKG-INFO +7 -3
  2. {autoforge-1.6.3 → autoforge-1.7.0}/README.md +4 -2
  3. {autoforge-1.6.3 → autoforge-1.7.0}/pyproject.toml +4 -2
  4. {autoforge-1.6.3 → autoforge-1.7.0}/src/AutoForge.egg-info/PKG-INFO +7 -3
  5. {autoforge-1.6.3 → autoforge-1.7.0}/src/AutoForge.egg-info/requires.txt +2 -0
  6. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py +91 -38
  7. autoforge-1.7.0/src/autoforge/Helper/OptimizerHelper.py +302 -0
  8. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/PruningHelper.py +303 -201
  9. autoforge-1.7.0/src/autoforge/Loss/LossFunctions.py +143 -0
  10. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Modules/Optimizer.py +124 -73
  11. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/auto_forge.py +77 -27
  12. autoforge-1.6.3/src/autoforge/Helper/OptimizerHelper.py +0 -466
  13. autoforge-1.6.3/src/autoforge/Loss/LossFunctions.py +0 -91
  14. {autoforge-1.6.3 → autoforge-1.7.0}/setup.cfg +0 -0
  15. {autoforge-1.6.3 → autoforge-1.7.0}/src/AutoForge.egg-info/SOURCES.txt +0 -0
  16. {autoforge-1.6.3 → autoforge-1.7.0}/src/AutoForge.egg-info/dependency_links.txt +0 -0
  17. {autoforge-1.6.3 → autoforge-1.7.0}/src/AutoForge.egg-info/entry_points.txt +0 -0
  18. {autoforge-1.6.3 → autoforge-1.7.0}/src/AutoForge.egg-info/top_level.txt +0 -0
  19. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Gui/__init__.py +0 -0
  20. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/Views/__init__.py +0 -0
  21. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/Views/backend_database.py +0 -0
  22. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/Views/filament_editor.py +0 -0
  23. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/app.py +0 -0
  24. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/main.py +0 -0
  25. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/CAdamW.py +0 -0
  26. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/FilamentHelper.py +0 -0
  27. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/DepthEstimateHeightMap.py +0 -0
  28. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/OldHeightmap.py +0 -0
  29. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/__init__.py +0 -0
  30. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/ImageHelper.py +0 -0
  31. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/OutputHelper.py +0 -0
  32. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/__init__.py +0 -0
  33. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/filamentcolors_library.py +0 -0
  34. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Init_comparer/__init__.py +0 -0
  35. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Init_comparer/auto_forge_init_check.py +0 -0
  36. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Init_comparer/plot_results.py +0 -0
  37. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Loss/PerceptionLoss.py +0 -0
  38. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Loss/__init__.py +0 -0
  39. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Misc/__init__.py +0 -0
  40. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Misc/color_fit_all.py +0 -0
  41. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Modules/__init__.py +0 -0
  42. {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/__init__.py +0 -0
  43. {autoforge-1.6.3 → 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.3
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
@@ -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
 
@@ -114,7 +116,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
114
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.
115
117
 
116
118
  - `--output_folder` Folder where output files will be saved (default: `output`).
117
- - `--iterations` Number of optimization iterations (default: 5000).
119
+ - `--iterations` Number of optimization iterations (default: 2000).
118
120
  - `--learning_rate` Learning rate for optimization (default: 0.015).
119
121
  - `--layer_height` Layer thickness in millimeters (default: 0.04).
120
122
  - `--max_layers` Maximum number of layers (default: 75).
@@ -125,7 +127,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
125
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).
126
128
  - `--init_tau` Initial tau value for Gumbel-Softmax (default: 1.0).
127
129
  - `--final_tau` Final tau value for the Gumbel-Softmax formulation (default: 0.01).
128
- - `--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).
129
131
  - `--stl_output_size` Size of the longest dimension of the output STL file in millimeters (default: 150).
130
132
  - `--nozzle_diameter` Diameter of the printer nozzle in millimeters (default: 0.4).
131
133
  **Note:** Details smaller than half this value will be ignored.
@@ -134,6 +136,8 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
134
136
 
135
137
  - `--perform_pruning` Perform pruning after optimization (default: True).
136
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%).
137
141
  - `--pruning_max_colors` Max number of colors allowed after pruning (default: 100).
138
142
  - `--pruning_max_swaps` Max number of swaps allowed after pruning (default: 100).
139
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.3"
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"
@@ -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.3
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
@@ -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
 
@@ -114,7 +116,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
114
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.
115
117
 
116
118
  - `--output_folder` Folder where output files will be saved (default: `output`).
117
- - `--iterations` Number of optimization iterations (default: 5000).
119
+ - `--iterations` Number of optimization iterations (default: 2000).
118
120
  - `--learning_rate` Learning rate for optimization (default: 0.015).
119
121
  - `--layer_height` Layer thickness in millimeters (default: 0.04).
120
122
  - `--max_layers` Maximum number of layers (default: 75).
@@ -125,7 +127,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
125
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).
126
128
  - `--init_tau` Initial tau value for Gumbel-Softmax (default: 1.0).
127
129
  - `--final_tau` Final tau value for the Gumbel-Softmax formulation (default: 0.01).
128
- - `--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).
129
131
  - `--stl_output_size` Size of the longest dimension of the output STL file in millimeters (default: 150).
130
132
  - `--nozzle_diameter` Diameter of the printer nozzle in millimeters (default: 0.4).
131
133
  **Note:** Details smaller than half this value will be ignored.
@@ -134,6 +136,8 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
134
136
 
135
137
  - `--perform_pruning` Perform pruning after optimization (default: True).
136
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%).
137
141
  - `--pruning_max_colors` Max number of colors allowed after pruning (default: 100).
138
142
  - `--pruning_max_swaps` Max number of swaps allowed after pruning (default: 100).
139
143
  - `--pruning_max_layer` Max number of layers allowed after pruning (default: 75).
@@ -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):
@@ -323,6 +321,23 @@ def compute_ordering_metric(ordering, labs):
323
321
 
324
322
 
325
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
326
341
 
327
342
 
328
343
  def init_height_map(
@@ -336,6 +351,7 @@ def init_height_map(
336
351
  init_method="quantize_maxcoverage",
337
352
  cluster_layers=None,
338
353
  lab_space=True,
354
+ material_colors=None,
339
355
  ):
340
356
  """
341
357
  init_method should be one of quantize_median,quantize_maxcoverage,quantize_fastoctree,kmeans
@@ -455,60 +471,97 @@ def init_height_map(
455
471
  # Get the ordering via TSP ordering function.
456
472
  final_ordering = tsp_order_christofides_path(nodes, labs, bg_cluster, fg_cluster)
457
473
 
458
- # Optionally prune out outliers.
459
- final_ordering = prune_ordering(
460
- final_ordering, labs, bg_cluster, fg_cluster, min_length=3, improvement_factor=3
461
- )
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
+ # )
462
478
 
463
479
  # Create a mapping that covers all clusters.
464
480
  new_values = create_mapping(final_ordering, labs, unique_clusters)
465
481
  new_labels = np.vectorize(lambda x: new_values[x])(labels).astype(np.float32)
482
+
466
483
  pixel_height_logits = np.log((new_labels + eps) / (1 - new_labels + eps))
467
484
  ordering_metric = compute_ordering_metric(final_ordering, labs)
468
- 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
469
521
 
470
522
 
471
523
  def run_init_threads(
472
524
  target,
473
525
  max_layers,
474
- h, # unused here but preserved for API compatibility
526
+ h, # unused but preserved for API compatibility
475
527
  background_tuple,
476
528
  eps=1e-6,
477
529
  random_seed=None,
478
530
  num_threads=64,
479
- init_method="quantize_maxcoverage",
531
+ init_method="kmeans",
480
532
  cluster_layers=None,
481
- lab_space=True,
533
+ material_colors=None,
482
534
  ):
483
535
  if random_seed is None:
484
536
  random_seed = np.random.randint(1e6)
485
- exec = ThreadPoolExecutor(max_workers=os.cpu_count())
486
- futures = []
487
- for i in range(num_threads):
488
- futures.append(
489
- exec.submit(
490
- init_height_map,
491
- target,
492
- max_layers,
493
- h,
494
- background_tuple,
495
- eps,
496
- random_seed + i,
497
- init_method=init_method,
498
- cluster_layers=cluster_layers,
499
- lab_space=lab_space,
500
- )
501
- )
502
- results = [
503
- f.result()
504
- for f in tqdm(
505
- concurrent.futures.as_completed(futures),
506
- desc="Running Height Map Initialization Threads",
507
- total=num_threads,
537
+ lab_space = False
538
+ tasks = [
539
+ delayed(init_height_map)(
540
+ target,
541
+ max_layers,
542
+ h,
543
+ background_tuple,
544
+ eps,
545
+ random_seed + i,
546
+ init_method=init_method,
547
+ cluster_layers=cluster_layers,
548
+ lab_space=lab_space,
549
+ material_colors=material_colors,
508
550
  )
551
+ for i in range(num_threads)
509
552
  ]
510
- # print(f"metric values: {[r[1] for r in results]}")
511
- # get lowest ordering metric
512
- best_result = min(results, key=lambda x: x[1])
513
553
 
514
- return best_result[0]
554
+ # Execute tasks in parallel; adjust n_jobs to match your available cores
555
+ results = Parallel(n_jobs=os.cpu_count(), verbose=10)(tasks)
556
+
557
+ metrics = [r[2] for r in results]
558
+ mean_metric = np.mean(metrics)
559
+ std_metric = np.std(metrics)
560
+ min_metric = np.min(metrics)
561
+ max_metric = np.max(metrics)
562
+ print(
563
+ f"mean: {mean_metric}, std: {std_metric}, min: {min_metric}, max: {max_metric}"
564
+ )
565
+ print(f"Choosing best ordering with metric: {min_metric}")
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