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.
- {autoforge-1.6.4 → autoforge-1.7.0}/PKG-INFO +5 -3
- {autoforge-1.6.4 → autoforge-1.7.0}/README.md +4 -2
- {autoforge-1.6.4 → autoforge-1.7.0}/pyproject.toml +1 -1
- {autoforge-1.6.4 → autoforge-1.7.0}/src/AutoForge.egg-info/PKG-INFO +5 -3
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py +66 -11
- autoforge-1.7.0/src/autoforge/Helper/OptimizerHelper.py +302 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/PruningHelper.py +303 -201
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Loss/LossFunctions.py +36 -2
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Modules/Optimizer.py +54 -37
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/auto_forge.py +70 -43
- autoforge-1.6.4/src/autoforge/Helper/OptimizerHelper.py +0 -466
- {autoforge-1.6.4 → autoforge-1.7.0}/setup.cfg +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/AutoForge.egg-info/SOURCES.txt +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/AutoForge.egg-info/dependency_links.txt +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/AutoForge.egg-info/entry_points.txt +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/AutoForge.egg-info/requires.txt +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/AutoForge.egg-info/top_level.txt +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Gui/__init__.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/Views/__init__.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/Views/backend_database.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/Views/filament_editor.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/app.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/main.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/CAdamW.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/FilamentHelper.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/DepthEstimateHeightMap.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/OldHeightmap.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/__init__.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/ImageHelper.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/OutputHelper.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/__init__.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/filamentcolors_library.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Init_comparer/__init__.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Init_comparer/auto_forge_init_check.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Init_comparer/plot_results.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Loss/PerceptionLoss.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Loss/__init__.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Misc/__init__.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Misc/color_fit_all.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Modules/__init__.py +0 -0
- {autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/__init__.py +0 -0
- {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.
|
|
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:
|
|
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`
|
|
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:
|
|
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`
|
|
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.
|
|
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.
|
|
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:
|
|
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`
|
|
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).
|
{autoforge-1.6.4 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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="
|
|
531
|
+
init_method="kmeans",
|
|
478
532
|
cluster_layers=None,
|
|
479
|
-
|
|
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[
|
|
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[
|
|
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
|