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.
- {autoforge-1.6.3 → autoforge-1.7.0}/PKG-INFO +7 -3
- {autoforge-1.6.3 → autoforge-1.7.0}/README.md +4 -2
- {autoforge-1.6.3 → autoforge-1.7.0}/pyproject.toml +4 -2
- {autoforge-1.6.3 → autoforge-1.7.0}/src/AutoForge.egg-info/PKG-INFO +7 -3
- {autoforge-1.6.3 → autoforge-1.7.0}/src/AutoForge.egg-info/requires.txt +2 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py +91 -38
- autoforge-1.7.0/src/autoforge/Helper/OptimizerHelper.py +302 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/PruningHelper.py +303 -201
- autoforge-1.7.0/src/autoforge/Loss/LossFunctions.py +143 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Modules/Optimizer.py +124 -73
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/auto_forge.py +77 -27
- autoforge-1.6.3/src/autoforge/Helper/OptimizerHelper.py +0 -466
- autoforge-1.6.3/src/autoforge/Loss/LossFunctions.py +0 -91
- {autoforge-1.6.3 → autoforge-1.7.0}/setup.cfg +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/AutoForge.egg-info/SOURCES.txt +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/AutoForge.egg-info/dependency_links.txt +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/AutoForge.egg-info/entry_points.txt +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/AutoForge.egg-info/top_level.txt +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Gui/__init__.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/Views/__init__.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/Views/backend_database.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/Views/filament_editor.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/app.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Gui/browser_gui/main.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/CAdamW.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/FilamentHelper.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/DepthEstimateHeightMap.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/OldHeightmap.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/__init__.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/ImageHelper.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/OutputHelper.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/__init__.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/filamentcolors_library.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Init_comparer/__init__.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Init_comparer/auto_forge_init_check.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Init_comparer/plot_results.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Loss/PerceptionLoss.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Loss/__init__.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Misc/__init__.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Misc/color_fit_all.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Modules/__init__.py +0 -0
- {autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/__init__.py +0 -0
- {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.
|
|
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:
|
|
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`
|
|
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:
|
|
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"
|
|
@@ -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.
|
|
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:
|
|
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`
|
|
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).
|
{autoforge-1.6.3 → autoforge-1.7.0}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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="
|
|
531
|
+
init_method="kmeans",
|
|
480
532
|
cluster_layers=None,
|
|
481
|
-
|
|
533
|
+
material_colors=None,
|
|
482
534
|
):
|
|
483
535
|
if random_seed is None:
|
|
484
536
|
random_seed = np.random.randint(1e6)
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|