autoforge 1.2.1__tar.gz → 1.3.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 (28) hide show
  1. {autoforge-1.2.1 → autoforge-1.3.0}/PKG-INFO +29 -10
  2. {autoforge-1.2.1 → autoforge-1.3.0}/README.md +27 -9
  3. {autoforge-1.2.1 → autoforge-1.3.0}/pyproject.toml +2 -1
  4. {autoforge-1.2.1 → autoforge-1.3.0}/src/AutoForge.egg-info/PKG-INFO +29 -10
  5. {autoforge-1.2.1 → autoforge-1.3.0}/src/AutoForge.egg-info/requires.txt +1 -0
  6. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Helper/OptimizerHelper.py +264 -0
  7. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Helper/PruningHelper.py +192 -13
  8. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Modules/Optimizer.py +29 -3
  9. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/auto_forge.py +80 -12
  10. {autoforge-1.2.1 → autoforge-1.3.0}/setup.cfg +0 -0
  11. {autoforge-1.2.1 → autoforge-1.3.0}/src/AutoForge.egg-info/SOURCES.txt +0 -0
  12. {autoforge-1.2.1 → autoforge-1.3.0}/src/AutoForge.egg-info/dependency_links.txt +0 -0
  13. {autoforge-1.2.1 → autoforge-1.3.0}/src/AutoForge.egg-info/entry_points.txt +0 -0
  14. {autoforge-1.2.1 → autoforge-1.3.0}/src/AutoForge.egg-info/top_level.txt +0 -0
  15. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Helper/FilamentHelper.py +0 -0
  16. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Helper/ImageHelper.py +0 -0
  17. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Helper/OutputHelper.py +0 -0
  18. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Helper/__init__.py +0 -0
  19. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Loss/LossFunctions.py +0 -0
  20. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Loss/PerceptionLoss.py +0 -0
  21. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Loss/__init__.py +0 -0
  22. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Modules/__init__.py +0 -0
  23. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/__init__.py +0 -0
  24. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/__main__.py +0 -0
  25. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/auto_forge_gradio.py +0 -0
  26. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/color_fit_bayesian.py +0 -0
  27. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/filamentcolors_library.py +0 -0
  28. {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/old_jax_files.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: AutoForge
3
- Version: 1.2.1
3
+ Version: 1.3.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
@@ -9,6 +9,7 @@ Description-Content-Type: text/markdown
9
9
  Requires-Dist: numpy>=1.19.0
10
10
  Requires-Dist: torch>=2.6.0
11
11
  Requires-Dist: torchvision>=0.21.0
12
+ Requires-Dist: transformers>=4.48.1
12
13
  Requires-Dist: opencv-python>=4.2.0.32
13
14
  Requires-Dist: matplotlib>=3.3.0
14
15
  Requires-Dist: configargparse>=1.2.3
@@ -92,28 +93,46 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
92
93
  - `--config`: *(Optional)* Path to a configuration file with the settings.
93
94
  - `--input_image`: **(Required)** Path to the input image.
94
95
  - `--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.
95
- - `--output_folder`: **(Required)** Folder where output files will be saved.
96
+ - `--output_folder`: Folder where output files will be saved. (default: `./outputs`).
96
97
  - `--iterations`: Number of optimization iterations (default: 5000).
97
- - `--best_loss_iterations`: Percentage of optimization iterations after which we start to record the best loss (default: 4000).
98
+ - `--best_loss_iterations`: Percentage of optimization iterations after which we start to record the best loss (default: 4000). \
98
99
  **Note:** We currently do this as we increase a penalty loss for color and color swaps as the time goes on. Without this the penalties would have no impact.
99
100
  - `--learning_rate`: Learning rate for the optimizer (default: 1e-2).
100
101
  - `--layer_height`: Layer thickness in millimeters (default: 0.04).
101
- - `--max_layers`: Maximum number of layers (default: 75).
102
+ - `--max_layers`: Maximum number of layers (default: 75). \
102
103
  **Note:** This is about 3mm + the background height
103
- - `--background_height`: Height of the background in millimeters (default: 0.4).
104
+ - `--background_height`: Height of the background in millimeters (default: 0.4). \
104
105
  **Note:** The background height must be divisible by the layer height.
105
- - `--background_color`: Background color in hexadecimal format (default: `#000000` aka Black).
106
+ - `--background_color`: Background color in hexadecimal format (default: `#000000` aka Black). \
106
107
  **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)
107
108
  - `--output_size`: Maximum dimension for target image (default: 1024).
108
- - `--solver_size`: Maximum dimension for solver (fast) image (default: 128).
109
+ - `--solver_size`: Maximum dimension for solver (fast) image (default: 128). \
109
110
  **Note:** We solve on a smaller size as this is many times faster, but also a bit less accurate. Increase if you need more accuracy.
110
111
  - `--decay`: Final tau value for the Gumbel-Softmax formulation (default: 0.01).
111
112
  - `--visualize`: Flag to enable live visualization of the composite image during optimization.
112
- - `--perform_pruning`: Perform pruning after optimization (default: True).
113
- - `--pruning_max_colors`: Max number of colors allowed after pruning (default: 10).
114
- - `--pruning_max_swaps`: Max number of swaps allowed after pruning (default: 20).
113
+ - `--perform_pruning`: Perform pruning after optimization (default: True). \
114
+ **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.
115
+ - `--pruning_max_colors`: Max number of colors allowed after pruning (default: 100).
116
+ - `--pruning_max_swaps`: Max number of swaps allowed after pruning (default: 100).
115
117
  - `--random_seed`: Random seed for reproducibility (default: 0 (disabled) ).
116
118
 
119
+ ### Experimental Deph Anything V2 parameters
120
+ I got a request to add a initializing function that takes the original deph of the image into account when initializing. \
121
+ In theory this should give you a nice background/foreground separation, but in practice it can degrade the quality of the output. \
122
+ In addition you need the VRAM to run the depth model. \
123
+ I currently don't recommend it as the output is not as good as without it, but I will leave it in for now. \
124
+
125
+ - `--use_depth_anything`: Use a depth anything v2 model to initialize the height map (default: False).
126
+ **Note:** This will give you a nice background/foreground separation, but will could degrade quality in some cases.
127
+ **Note:** In addition you need the VRAM to run the depth model.
128
+
129
+ - `--depth_strength`: Weight for blending even spacing with the cluster’s average depth when using depth initialization. (default: 0.25)
130
+ - `--depth_threshold`: Threshold for splitting two distinct colors based on depth. (default: 0.05)
131
+ - `--min_cluster_value`: Minimum normalized value for the lowest cluster (to avoid pure black). (default: 0.1)
132
+ - `--w_depth`: Weight for depth difference in ordering. (default: 0.5)
133
+ - `--w_lum`: Weight for luminance difference in ordering. (default: 1.0)
134
+ - `--order_blend`: Blending factor between original luminance ordering (0) and depth-informed ordering (1). (default: 0.1)
135
+
117
136
 
118
137
  ## Outputs
119
138
 
@@ -73,28 +73,46 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
73
73
  - `--config`: *(Optional)* Path to a configuration file with the settings.
74
74
  - `--input_image`: **(Required)** Path to the input image.
75
75
  - `--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.
76
- - `--output_folder`: **(Required)** Folder where output files will be saved.
76
+ - `--output_folder`: Folder where output files will be saved. (default: `./outputs`).
77
77
  - `--iterations`: Number of optimization iterations (default: 5000).
78
- - `--best_loss_iterations`: Percentage of optimization iterations after which we start to record the best loss (default: 4000).
78
+ - `--best_loss_iterations`: Percentage of optimization iterations after which we start to record the best loss (default: 4000). \
79
79
  **Note:** We currently do this as we increase a penalty loss for color and color swaps as the time goes on. Without this the penalties would have no impact.
80
80
  - `--learning_rate`: Learning rate for the optimizer (default: 1e-2).
81
81
  - `--layer_height`: Layer thickness in millimeters (default: 0.04).
82
- - `--max_layers`: Maximum number of layers (default: 75).
82
+ - `--max_layers`: Maximum number of layers (default: 75). \
83
83
  **Note:** This is about 3mm + the background height
84
- - `--background_height`: Height of the background in millimeters (default: 0.4).
84
+ - `--background_height`: Height of the background in millimeters (default: 0.4). \
85
85
  **Note:** The background height must be divisible by the layer height.
86
- - `--background_color`: Background color in hexadecimal format (default: `#000000` aka Black).
86
+ - `--background_color`: Background color in hexadecimal format (default: `#000000` aka Black). \
87
87
  **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)
88
88
  - `--output_size`: Maximum dimension for target image (default: 1024).
89
- - `--solver_size`: Maximum dimension for solver (fast) image (default: 128).
89
+ - `--solver_size`: Maximum dimension for solver (fast) image (default: 128). \
90
90
  **Note:** We solve on a smaller size as this is many times faster, but also a bit less accurate. Increase if you need more accuracy.
91
91
  - `--decay`: Final tau value for the Gumbel-Softmax formulation (default: 0.01).
92
92
  - `--visualize`: Flag to enable live visualization of the composite image during optimization.
93
- - `--perform_pruning`: Perform pruning after optimization (default: True).
94
- - `--pruning_max_colors`: Max number of colors allowed after pruning (default: 10).
95
- - `--pruning_max_swaps`: Max number of swaps allowed after pruning (default: 20).
93
+ - `--perform_pruning`: Perform pruning after optimization (default: True). \
94
+ **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.
95
+ - `--pruning_max_colors`: Max number of colors allowed after pruning (default: 100).
96
+ - `--pruning_max_swaps`: Max number of swaps allowed after pruning (default: 100).
96
97
  - `--random_seed`: Random seed for reproducibility (default: 0 (disabled) ).
97
98
 
99
+ ### Experimental Deph Anything V2 parameters
100
+ I got a request to add a initializing function that takes the original deph of the image into account when initializing. \
101
+ In theory this should give you a nice background/foreground separation, but in practice it can degrade the quality of the output. \
102
+ In addition you need the VRAM to run the depth model. \
103
+ I currently don't recommend it as the output is not as good as without it, but I will leave it in for now. \
104
+
105
+ - `--use_depth_anything`: Use a depth anything v2 model to initialize the height map (default: False).
106
+ **Note:** This will give you a nice background/foreground separation, but will could degrade quality in some cases.
107
+ **Note:** In addition you need the VRAM to run the depth model.
108
+
109
+ - `--depth_strength`: Weight for blending even spacing with the cluster’s average depth when using depth initialization. (default: 0.25)
110
+ - `--depth_threshold`: Threshold for splitting two distinct colors based on depth. (default: 0.05)
111
+ - `--min_cluster_value`: Minimum normalized value for the lowest cluster (to avoid pure black). (default: 0.1)
112
+ - `--w_depth`: Weight for depth difference in ordering. (default: 0.5)
113
+ - `--w_lum`: Weight for luminance difference in ordering. (default: 1.0)
114
+ - `--order_blend`: Blending factor between original luminance ordering (0) and depth-informed ordering (1). (default: 0.1)
115
+
98
116
 
99
117
  ## Outputs
100
118
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "AutoForge"
7
- version="1.2.1"
7
+ version="1.3.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"
@@ -14,6 +14,7 @@ dependencies = [
14
14
  "numpy>=1.19.0",
15
15
  "torch>=2.6.0",
16
16
  "torchvision>=0.21.0",
17
+ "transformers>=4.48.1",
17
18
  "opencv-python>=4.2.0.32",
18
19
  "matplotlib>=3.3.0",
19
20
  "configargparse>=1.2.3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: AutoForge
3
- Version: 1.2.1
3
+ Version: 1.3.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
@@ -9,6 +9,7 @@ Description-Content-Type: text/markdown
9
9
  Requires-Dist: numpy>=1.19.0
10
10
  Requires-Dist: torch>=2.6.0
11
11
  Requires-Dist: torchvision>=0.21.0
12
+ Requires-Dist: transformers>=4.48.1
12
13
  Requires-Dist: opencv-python>=4.2.0.32
13
14
  Requires-Dist: matplotlib>=3.3.0
14
15
  Requires-Dist: configargparse>=1.2.3
@@ -92,28 +93,46 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
92
93
  - `--config`: *(Optional)* Path to a configuration file with the settings.
93
94
  - `--input_image`: **(Required)** Path to the input image.
94
95
  - `--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.
95
- - `--output_folder`: **(Required)** Folder where output files will be saved.
96
+ - `--output_folder`: Folder where output files will be saved. (default: `./outputs`).
96
97
  - `--iterations`: Number of optimization iterations (default: 5000).
97
- - `--best_loss_iterations`: Percentage of optimization iterations after which we start to record the best loss (default: 4000).
98
+ - `--best_loss_iterations`: Percentage of optimization iterations after which we start to record the best loss (default: 4000). \
98
99
  **Note:** We currently do this as we increase a penalty loss for color and color swaps as the time goes on. Without this the penalties would have no impact.
99
100
  - `--learning_rate`: Learning rate for the optimizer (default: 1e-2).
100
101
  - `--layer_height`: Layer thickness in millimeters (default: 0.04).
101
- - `--max_layers`: Maximum number of layers (default: 75).
102
+ - `--max_layers`: Maximum number of layers (default: 75). \
102
103
  **Note:** This is about 3mm + the background height
103
- - `--background_height`: Height of the background in millimeters (default: 0.4).
104
+ - `--background_height`: Height of the background in millimeters (default: 0.4). \
104
105
  **Note:** The background height must be divisible by the layer height.
105
- - `--background_color`: Background color in hexadecimal format (default: `#000000` aka Black).
106
+ - `--background_color`: Background color in hexadecimal format (default: `#000000` aka Black). \
106
107
  **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)
107
108
  - `--output_size`: Maximum dimension for target image (default: 1024).
108
- - `--solver_size`: Maximum dimension for solver (fast) image (default: 128).
109
+ - `--solver_size`: Maximum dimension for solver (fast) image (default: 128). \
109
110
  **Note:** We solve on a smaller size as this is many times faster, but also a bit less accurate. Increase if you need more accuracy.
110
111
  - `--decay`: Final tau value for the Gumbel-Softmax formulation (default: 0.01).
111
112
  - `--visualize`: Flag to enable live visualization of the composite image during optimization.
112
- - `--perform_pruning`: Perform pruning after optimization (default: True).
113
- - `--pruning_max_colors`: Max number of colors allowed after pruning (default: 10).
114
- - `--pruning_max_swaps`: Max number of swaps allowed after pruning (default: 20).
113
+ - `--perform_pruning`: Perform pruning after optimization (default: True). \
114
+ **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.
115
+ - `--pruning_max_colors`: Max number of colors allowed after pruning (default: 100).
116
+ - `--pruning_max_swaps`: Max number of swaps allowed after pruning (default: 100).
115
117
  - `--random_seed`: Random seed for reproducibility (default: 0 (disabled) ).
116
118
 
119
+ ### Experimental Deph Anything V2 parameters
120
+ I got a request to add a initializing function that takes the original deph of the image into account when initializing. \
121
+ In theory this should give you a nice background/foreground separation, but in practice it can degrade the quality of the output. \
122
+ In addition you need the VRAM to run the depth model. \
123
+ I currently don't recommend it as the output is not as good as without it, but I will leave it in for now. \
124
+
125
+ - `--use_depth_anything`: Use a depth anything v2 model to initialize the height map (default: False).
126
+ **Note:** This will give you a nice background/foreground separation, but will could degrade quality in some cases.
127
+ **Note:** In addition you need the VRAM to run the depth model.
128
+
129
+ - `--depth_strength`: Weight for blending even spacing with the cluster’s average depth when using depth initialization. (default: 0.25)
130
+ - `--depth_threshold`: Threshold for splitting two distinct colors based on depth. (default: 0.05)
131
+ - `--min_cluster_value`: Minimum normalized value for the lowest cluster (to avoid pure black). (default: 0.1)
132
+ - `--w_depth`: Weight for depth difference in ordering. (default: 0.5)
133
+ - `--w_lum`: Weight for luminance difference in ordering. (default: 1.0)
134
+ - `--order_blend`: Blending factor between original luminance ordering (0) and depth-informed ordering (1). (default: 0.1)
135
+
117
136
 
118
137
  ## Outputs
119
138
 
@@ -1,6 +1,7 @@
1
1
  numpy>=1.19.0
2
2
  torch>=2.6.0
3
3
  torchvision>=0.21.0
4
+ transformers>=4.48.1
4
5
  opencv-python>=4.2.0.32
5
6
  matplotlib>=3.3.0
6
7
  configargparse>=1.2.3
@@ -5,7 +5,12 @@ import torch
5
5
  import torch.nn.functional as F
6
6
  from skimage.color import rgb2lab
7
7
  from sklearn.cluster import KMeans
8
+ from sklearn.exceptions import ConvergenceWarning
9
+ from sklearn.utils._testing import ignore_warnings
8
10
  from tqdm import tqdm
11
+ from transformers import pipeline
12
+ from PIL import Image
13
+ from sklearn.metrics import silhouette_score
9
14
 
10
15
 
11
16
  @torch.jit.script
@@ -223,6 +228,265 @@ def initialize_pixel_height_logits(target):
223
228
  return pixel_height_logits
224
229
 
225
230
 
231
+ @ignore_warnings(category=ConvergenceWarning)
232
+ def init_height_map_depth_color_adjusted(
233
+ target,
234
+ max_layers,
235
+ eps=1e-6,
236
+ random_seed=None,
237
+ depth_strength=0.25,
238
+ depth_threshold=0.2,
239
+ min_cluster_value=0.1,
240
+ w_depth=0.5,
241
+ w_lum=0.5,
242
+ order_blend=0.1,
243
+ ):
244
+ """
245
+ Initialize pixel height logits by combining depth and color information while allowing a blend
246
+ between the original luminance-based ordering and a depth-informed ordering.
247
+
248
+ Steps:
249
+ 1. Obtain a normalized depth map using Depth Anything v2.
250
+ 2. Determine the optimal number of color clusters (between 2 and max_layers) via silhouette score.
251
+ 3. Cluster the image colors and (if needed) split clusters with large depth spreads.
252
+ 4. For each final cluster, compute its average depth and average luminance.
253
+ 5. Compute two orderings:
254
+ - ordering_orig: Sorted purely by average luminance (approximating the original code).
255
+ - ordering_depth: A TSP-inspired ordering using a weighted distance based on depth and luminance.
256
+ 6. For each cluster, blend its rank (normalized position) between the two orderings using order_blend.
257
+ 7. Based on the blended ordering, assign an even spacing value from min_cluster_value to 1.
258
+ 8. Finally, blend the even spacing with the cluster's average depth using depth_strength and
259
+ convert the result to logits via an inverse sigmoid transform.
260
+
261
+ Args:
262
+ target (np.ndarray): Input image of shape (H, W, 3) in [0, 255].
263
+ max_layers (int): Maximum number of clusters to consider.
264
+ eps (float): Small constant to avoid division by zero.
265
+ random_seed (int): Random seed for reproducibility.
266
+ depth_strength (float): Weight (0 to 1) for blending even spacing with the cluster's average depth.
267
+ depth_threshold (float): If a cluster’s depth spread exceeds this value, it is split.
268
+ min_cluster_value (float): Minimum normalized value for the lowest cluster.
269
+ w_depth (float): Weight for depth difference in ordering_depth.
270
+ w_lum (float): Weight for luminance difference in ordering_depth.
271
+ order_blend (float): Slider (0 to 1) blending original luminance ordering (0) and depth-informed ordering (1).
272
+
273
+ Returns:
274
+ np.ndarray: Pixel height logits (H, W).
275
+ """
276
+
277
+ # ---------------------------
278
+ # Step 1: Obtain normalized depth map using Depth Anything v2
279
+ # ---------------------------
280
+ target_uint8 = target.astype(np.uint8)
281
+ image_pil = Image.fromarray(target_uint8)
282
+ pipe = pipeline(
283
+ task="depth-estimation", model="depth-anything/Depth-Anything-V2-Small-hf"
284
+ )
285
+ depth_result = pipe(image_pil)
286
+ depth_map = depth_result["depth"]
287
+ if hasattr(depth_map, "convert"):
288
+ depth_map = np.array(depth_map)
289
+ depth_map = depth_map.astype(np.float32)
290
+ depth_min = depth_map.min()
291
+ depth_max = depth_map.max()
292
+ depth_norm = (depth_map - depth_min) / (depth_max - depth_min + eps)
293
+
294
+ # ---------------------------
295
+ # Step 2: Find optimal number of clusters (n in [2, max_layers]) for color clustering
296
+ # ---------------------------
297
+ H, W, _ = target.shape
298
+ pixels = target.reshape(-1, 3).astype(np.float32)
299
+
300
+ def find_best_n_clusters(pixels, max_layers, random_seed):
301
+ sample_size = 1000
302
+ if pixels.shape[0] > sample_size:
303
+ indices = np.random.choice(pixels.shape[0], sample_size, replace=False)
304
+ sample_pixels = pixels[indices]
305
+ else:
306
+ sample_pixels = pixels
307
+ best_n = None
308
+ best_score = -np.inf
309
+ for n in range(2, max_layers + 1):
310
+ kmeans_temp = KMeans(n_clusters=n, random_state=random_seed)
311
+ labels_temp = kmeans_temp.fit_predict(sample_pixels)
312
+ score = silhouette_score(sample_pixels, labels_temp)
313
+ if score > best_score:
314
+ best_score = score
315
+ best_n = n
316
+ return best_n
317
+
318
+ optimal_n = find_best_n_clusters(pixels, max_layers, random_seed)
319
+ print(f"Optimal number of clusters: {optimal_n}")
320
+ # ---------------------------
321
+ # Step 3: Perform color clustering on the full image
322
+ # ---------------------------
323
+ kmeans = KMeans(n_clusters=optimal_n, random_state=random_seed).fit(pixels)
324
+ labels = kmeans.labels_.reshape(H, W)
325
+
326
+ # ---------------------------
327
+ # Step 4: Adjust clusters based on depth (split clusters with high depth spread)
328
+ # ---------------------------
329
+ final_labels = np.copy(labels)
330
+ new_cluster_id = 0
331
+ cluster_info = {} # Mapping: final_cluster_id -> avg_depth
332
+ unique_labels = np.unique(labels)
333
+ for orig_label in unique_labels:
334
+ mask = labels == orig_label
335
+ cluster_depths = depth_norm[mask]
336
+ avg_depth = np.mean(cluster_depths)
337
+ depth_range = cluster_depths.max() - cluster_depths.min()
338
+ if depth_range > depth_threshold:
339
+ # Split this cluster into 2 subclusters based on depth values.
340
+ depth_values = cluster_depths.reshape(-1, 1)
341
+ k_split = 2
342
+ kmeans_split = KMeans(n_clusters=k_split, random_state=random_seed)
343
+ split_labels = kmeans_split.fit_predict(depth_values)
344
+ indices = np.argwhere(mask)
345
+ for split in range(k_split):
346
+ sub_mask = split_labels == split
347
+ inds = indices[sub_mask]
348
+ if inds.size == 0:
349
+ continue
350
+ for i, j in inds:
351
+ final_labels[i, j] = new_cluster_id
352
+ sub_avg_depth = np.mean(depth_norm[mask][split_labels == split])
353
+ cluster_info[new_cluster_id] = sub_avg_depth
354
+ new_cluster_id += 1
355
+ else:
356
+ indices = np.argwhere(mask)
357
+ for i, j in indices:
358
+ final_labels[i, j] = new_cluster_id
359
+ cluster_info[new_cluster_id] = avg_depth
360
+ new_cluster_id += 1
361
+
362
+ num_final_clusters = new_cluster_id
363
+
364
+ # ---------------------------
365
+ # Step 5: Compute average luminance for each final cluster (using standard weights)
366
+ # ---------------------------
367
+ cluster_colors = {}
368
+ for cid in range(num_final_clusters):
369
+ mask = final_labels == cid
370
+ if np.sum(mask) == 0:
371
+ continue
372
+ avg_color = np.mean(
373
+ target.reshape(-1, 3)[final_labels.reshape(-1) == cid], axis=0
374
+ )
375
+ lum = (
376
+ 0.299 * avg_color[0] + 0.587 * avg_color[1] + 0.114 * avg_color[2]
377
+ ) / 255.0
378
+ cluster_colors[cid] = lum
379
+
380
+ # ---------------------------
381
+ # Step 6: Build cluster feature list: (cid, avg_depth, avg_luminance)
382
+ # ---------------------------
383
+ cluster_features = []
384
+ for cid in range(num_final_clusters):
385
+ avg_depth = cluster_info[cid]
386
+ avg_lum = cluster_colors.get(cid, 0.5)
387
+ cluster_features.append((cid, avg_depth, avg_lum))
388
+
389
+ # ---------------------------
390
+ # Step 7: Compute depth-informed ordering (TSP-inspired, using w_depth and w_lum)
391
+ # ---------------------------
392
+ def distance(feat1, feat2):
393
+ return w_depth * abs(feat1[1] - feat2[1]) + w_lum * abs(feat1[2] - feat2[2])
394
+
395
+ # Greedy nearest-neighbor ordering starting from cluster with lowest avg_depth
396
+ start_idx = np.argmin([feat[1] for feat in cluster_features])
397
+ unvisited = cluster_features.copy()
398
+ ordering_depth = []
399
+ current = unvisited.pop(start_idx)
400
+ ordering_depth.append(current)
401
+ while unvisited:
402
+ next_idx = np.argmin([distance(current, candidate) for candidate in unvisited])
403
+ current = unvisited.pop(next_idx)
404
+ ordering_depth.append(current)
405
+
406
+ # 2-opt refinement for ordering_depth
407
+ def total_distance(ordering):
408
+ return sum(
409
+ distance(ordering[i], ordering[i + 1]) for i in range(len(ordering) - 1)
410
+ )
411
+
412
+ improved = True
413
+ best_order_depth = ordering_depth
414
+ best_dist = total_distance(ordering_depth)
415
+ while improved:
416
+ improved = False
417
+ for i in range(1, len(best_order_depth) - 1):
418
+ for j in range(i + 1, len(best_order_depth)):
419
+ new_order = (
420
+ best_order_depth[:i]
421
+ + best_order_depth[i : j + 1][::-1]
422
+ + best_order_depth[j + 1 :]
423
+ )
424
+ new_dist = total_distance(new_order)
425
+ if new_dist < best_dist:
426
+ best_order_depth = new_order
427
+ best_dist = new_dist
428
+ improved = True
429
+ ordering_depth = best_order_depth
430
+
431
+ # ---------------------------
432
+ # Step 8: Compute original (luminance-based) ordering: simply sort by avg_lum (darkest first)
433
+ # ---------------------------
434
+ ordering_orig = sorted(cluster_features, key=lambda x: x[2])
435
+
436
+ # ---------------------------
437
+ # Step 9: Blend the two orderings via their rank positions using order_blend
438
+ # ---------------------------
439
+ # Map each cluster id to its rank in each ordering.
440
+ rank_orig = {feat[0]: idx for idx, feat in enumerate(ordering_orig)}
441
+ rank_depth = {feat[0]: idx for idx, feat in enumerate(ordering_depth)}
442
+ # Normalize ranks to [0, 1]
443
+ norm_rank_orig = {
444
+ cid: rank_orig[cid] / (len(ordering_orig) - 1) if len(ordering_orig) > 1 else 0
445
+ for cid in rank_orig
446
+ }
447
+ norm_rank_depth = {
448
+ cid: rank_depth[cid] / (len(ordering_depth) - 1)
449
+ if len(ordering_depth) > 1
450
+ else 0
451
+ for cid in rank_depth
452
+ }
453
+
454
+ # Compute blended rank for each cluster
455
+ blended_ranks = {}
456
+ for cid in norm_rank_orig:
457
+ blended_ranks[cid] = (1 - order_blend) * norm_rank_orig[
458
+ cid
459
+ ] + order_blend * norm_rank_depth[cid]
460
+
461
+ # Final ordering: sort clusters by blended rank (ascending)
462
+ final_order = sorted(cluster_features, key=lambda x: blended_ranks[x[0]])
463
+
464
+ # ---------------------------
465
+ # Step 10: Assign new normalized values to clusters
466
+ # Even spacing from min_cluster_value to 1 based on final ordering
467
+ even_spacing = np.linspace(min_cluster_value, 1, num_final_clusters)
468
+ final_mapping = {}
469
+ for rank, (cid, avg_depth, avg_lum) in enumerate(final_order):
470
+ # Blend even spacing with the cluster's average depth using depth_strength.
471
+ # (When depth_strength=0, purely even spacing; when 1, purely avg_depth.)
472
+ blended_value = (1 - depth_strength) * even_spacing[
473
+ rank
474
+ ] + depth_strength * avg_depth
475
+ blended_value = np.clip(blended_value, min_cluster_value, 1)
476
+ final_mapping[cid] = blended_value
477
+
478
+ # ---------------------------
479
+ # Step 11: Create new normalized label image and convert to logits.
480
+ # ---------------------------
481
+ new_labels = np.vectorize(lambda x: final_mapping[x])(final_labels).astype(
482
+ np.float32
483
+ )
484
+ if new_labels.max() > 0:
485
+ new_labels = new_labels / new_labels.max()
486
+ pixel_height_logits = np.log((new_labels + eps) / (1 - new_labels + eps))
487
+ return pixel_height_logits
488
+
489
+
226
490
  def init_height_map(target, max_layers, h, eps=1e-6, random_seed=None):
227
491
  """
228
492
  Initialize pixel height logits based on the luminance of the target image.
@@ -1,7 +1,7 @@
1
1
  import torch
2
2
  from tqdm import tqdm
3
3
 
4
- from autoforge.Helper.OptimizerHelper import composite_image
4
+ from autoforge.Helper.OptimizerHelper import composite_image, discretize_solution
5
5
  from autoforge.Loss.LossFunctions import compute_loss
6
6
 
7
7
 
@@ -66,7 +66,7 @@ def prune_num_colors(
66
66
  material_TDs,
67
67
  background,
68
68
  mode="discrete",
69
- rng_seed = rng_seed
69
+ rng_seed=rng_seed,
70
70
  )
71
71
 
72
72
  # For the loss, we pass the actual dg_test so that compute_loss() knows it’s a discrete assignment
@@ -86,16 +86,16 @@ def prune_num_colors(
86
86
  distinct_mats = torch.unique(best_dg)
87
87
  tbar = tqdm(
88
88
  range(100),
89
- desc=f"Pruning colors down to {max_colors_allowed}. Current colors: {len(distinct_mats)}, Loss: {best_loss:.4f}",
89
+ desc=f"Pruning colors down to {max_colors_allowed} or until loss increases. Current colors: {len(distinct_mats)}, Loss: {best_loss:.4f}",
90
90
  )
91
- while len(distinct_mats) > max_colors_allowed:
91
+ while True:
92
92
  found_merge = False
93
93
  best_merge_loss = None
94
94
  best_merge_dg_candidate = None
95
95
 
96
96
  distinct_mats = torch.unique(best_dg)
97
97
  tbar.set_description(
98
- f"Pruning colors down to {max_colors_allowed}. Current colors: {len(distinct_mats)}, Loss: {best_loss:.4f}",
98
+ f"Pruning colors down to {max_colors_allowed} or until loss increases. Current colors: {len(distinct_mats)}, Loss: {best_loss:.4f}",
99
99
  )
100
100
  tbar.update(1)
101
101
 
@@ -111,7 +111,7 @@ def prune_num_colors(
111
111
  best_merge_dg_candidate = dg_test
112
112
 
113
113
  if best_merge_dg_candidate is not None:
114
- if best_merge_loss < best_loss or True:
114
+ if best_merge_loss < best_loss or len(distinct_mats) > max_colors_allowed:
115
115
  best_dg = best_merge_dg_candidate
116
116
  best_loss = best_merge_loss
117
117
  found_merge = True
@@ -159,7 +159,7 @@ def prune_num_swaps(
159
159
  material_TDs,
160
160
  background,
161
161
  mode="discrete",
162
- rng_seed=rng_seed
162
+ rng_seed=rng_seed,
163
163
  )
164
164
  return compute_loss(
165
165
  material_assignment=dg_test,
@@ -175,17 +175,15 @@ def prune_num_swaps(
175
175
  best_loss = get_image_loss(best_dg)
176
176
  tbar = tqdm(
177
177
  range(100),
178
- desc=f"Pruning swaps down to {max_swaps_allowed}. Current swaps: {len(find_color_bands(best_dg)) - 1}, Loss = {best_loss:.4f}",
178
+ desc=f"Pruning swaps down to {max_swaps_allowed} or until loss increases. Current swaps: {len(find_color_bands(best_dg)) - 1}, Loss = {best_loss:.4f}",
179
179
  )
180
180
  while True:
181
181
  bands = find_color_bands(best_dg)
182
182
  num_swaps = len(bands) - 1
183
183
  tbar.set_description(
184
- f"Pruning swaps down to {max_swaps_allowed}. Current swaps: {num_swaps}, Loss = {best_loss:.4f}",
184
+ f"Pruning swaps down to {max_swaps_allowed} or until loss increases. Current swaps: {num_swaps}, Loss = {best_loss:.4f}",
185
185
  )
186
186
  tbar.update(1)
187
- if num_swaps <= max_swaps_allowed:
188
- break
189
187
 
190
188
  best_merge_loss = None
191
189
  best_merge_dg_candidate = None
@@ -211,7 +209,9 @@ def prune_num_swaps(
211
209
  best_merge_loss = candidate_loss
212
210
  best_merge_dg_candidate = candidate_dg
213
211
 
214
- if best_merge_dg_candidate is not None:
212
+ if best_merge_dg_candidate is not None and (
213
+ best_merge_loss < best_loss or num_swaps > max_swaps_allowed
214
+ ):
215
215
  best_dg = best_merge_dg_candidate
216
216
  best_loss = best_merge_loss
217
217
  else:
@@ -234,7 +234,6 @@ def prune_colors_and_swaps(
234
234
  tau_for_comp: float,
235
235
  rng_seed: int,
236
236
  perception_loss_module: torch.nn.Module,
237
-
238
237
  ) -> torch.Tensor:
239
238
  """
240
239
  First limit # colors, then limit # swaps.
@@ -318,3 +317,183 @@ def merge_bands(
318
317
  else:
319
318
  dg_new[band_a[0] : band_a[1] + 1] = c_b
320
319
  return dg_new
320
+
321
+
322
+ def remove_layer_from_solution(
323
+ params, layer_to_remove, final_tau, h, current_max_layers, rng_seed
324
+ ):
325
+ """
326
+ Remove one layer from the solution.
327
+
328
+ Args:
329
+ params (dict): Current parameters with keys "global_logits" and "pixel_height_logits".
330
+ layer_to_remove (int): Candidate layer index to remove.
331
+ final_tau (float): Final tau value used in discretization/compositing.
332
+ h (float): Layer height.
333
+ current_max_layers (int): Current total number of layers.
334
+ rng_seed (int): Seed used in discretization/compositing.
335
+
336
+ Returns:
337
+ new_params (dict): New parameters with the candidate layer removed.
338
+ new_max_layers (int): Updated number of layers.
339
+ """
340
+ # Get the current discrete height image (used to decide which pixels need adjusting)
341
+ _, disc_height = discretize_solution(
342
+ params, final_tau, h, current_max_layers, rng_seed
343
+ )
344
+
345
+ # Remove the candidate layer from the global (color) assignment.
346
+ new_global_logits = torch.cat(
347
+ [
348
+ params["global_logits"][:layer_to_remove],
349
+ params["global_logits"][layer_to_remove + 1 :],
350
+ ],
351
+ dim=0,
352
+ )
353
+ new_max_layers = current_max_layers - 1
354
+
355
+ # Compute current effective height: height = (current_max_layers * h) * sigmoid(pixel_height_logits)
356
+ current_height = (
357
+ current_max_layers * h * torch.sigmoid(params["pixel_height_logits"])
358
+ )
359
+
360
+ # For pixels where the discrete height is at or above the removed layer, subtract h.
361
+ new_height = current_height.clone()
362
+ mask = disc_height >= layer_to_remove # <-- Changed from > to >= here
363
+ new_height[mask] = new_height[mask] - h
364
+
365
+ # Invert the sigmoid mapping:
366
+ # We need new_pixel_height_logits such that:
367
+ # sigmoid(new_pixel_height_logits) = new_height / (new_max_layers * h)
368
+ eps = 1e-6
369
+ new_ratio = torch.clamp(new_height / (new_max_layers * h), eps, 1 - eps)
370
+ new_pixel_height_logits = torch.log(new_ratio) - torch.log(1 - new_ratio)
371
+
372
+ new_params = {
373
+ "global_logits": new_global_logits,
374
+ "pixel_height_logits": new_pixel_height_logits,
375
+ }
376
+ return new_params, new_max_layers
377
+
378
+
379
+ def prune_redundant_layers(
380
+ params,
381
+ final_tau,
382
+ h,
383
+ target,
384
+ material_colors,
385
+ material_TDs,
386
+ background,
387
+ rng_seed,
388
+ perception_loss_module,
389
+ tolerance=1e-3,
390
+ ):
391
+ """
392
+ Iteratively check each layer and remove it if the removal does not worsen the loss by more than `tolerance`.
393
+
394
+ Args:
395
+ params (dict): Dictionary with keys "global_logits" and "pixel_height_logits".
396
+ final_tau (float): Final tau (decay) value for discretization.
397
+ h (float): Layer height.
398
+ target (torch.Tensor): Target image tensor.
399
+ material_colors (torch.Tensor): Tensor of material colors.
400
+ material_TDs (torch.Tensor): Tensor of material transmission parameters.
401
+ background (torch.Tensor): Background color tensor.
402
+ rng_seed (int): Random seed for discretization.
403
+ perception_loss_module (torch.nn.Module): Perceptual loss module.
404
+ tolerance (float): Acceptable increase in loss.
405
+
406
+ Returns:
407
+ current_params (dict): Updated parameters with redundant layers removed.
408
+ best_loss (float): Loss corresponding to the pruned solution.
409
+ current_max_layers (int): New number of layers after pruning.
410
+ """
411
+ current_params = params
412
+ current_max_layers = current_params["global_logits"].shape[0]
413
+
414
+ # Compute baseline composite and loss.
415
+ comp = composite_image(
416
+ current_params["pixel_height_logits"],
417
+ current_params["global_logits"],
418
+ final_tau,
419
+ final_tau,
420
+ h,
421
+ current_max_layers,
422
+ material_colors,
423
+ material_TDs,
424
+ background,
425
+ mode="discrete",
426
+ rng_seed=rng_seed,
427
+ )
428
+ disc_global, _ = discretize_solution(
429
+ current_params, final_tau, h, current_max_layers, rng_seed
430
+ )
431
+ best_loss = compute_loss(
432
+ disc_global,
433
+ comp,
434
+ target,
435
+ perception_loss_module,
436
+ final_tau,
437
+ material_colors.shape[0],
438
+ add_penalty_loss=False,
439
+ ).item()
440
+
441
+ improvement = True
442
+ # Optional: use a progress bar to indicate the number of layers removed.
443
+ from tqdm import tqdm
444
+
445
+ tbar = tqdm(
446
+ desc=f"Pruning redundant layers: Loss {best_loss:.4f}",
447
+ total=current_max_layers - 1,
448
+ )
449
+ removed_layers = 0
450
+
451
+ # Continue until no layer can be removed.
452
+ while improvement and current_max_layers > 1:
453
+ tbar.update(1)
454
+ improvement = False
455
+ # Try each layer as a candidate for removal.
456
+ for layer in range(current_max_layers):
457
+ candidate_params, candidate_max_layers = remove_layer_from_solution(
458
+ current_params, layer, final_tau, h, current_max_layers, rng_seed
459
+ )
460
+ candidate_comp = composite_image(
461
+ candidate_params["pixel_height_logits"],
462
+ candidate_params["global_logits"],
463
+ final_tau,
464
+ final_tau,
465
+ h,
466
+ candidate_max_layers,
467
+ material_colors,
468
+ material_TDs,
469
+ background,
470
+ mode="discrete",
471
+ rng_seed=rng_seed,
472
+ )
473
+ candidate_disc_global, _ = discretize_solution(
474
+ candidate_params, final_tau, h, candidate_max_layers, rng_seed
475
+ )
476
+ candidate_loss = compute_loss(
477
+ candidate_disc_global,
478
+ candidate_comp,
479
+ target,
480
+ perception_loss_module,
481
+ final_tau,
482
+ material_colors.shape[0],
483
+ add_penalty_loss=False,
484
+ ).item()
485
+
486
+ # Accept the removal if loss increase is within tolerance.
487
+ if candidate_loss <= best_loss + tolerance:
488
+ removed_layers += 1
489
+ tbar.set_description(
490
+ f"Pruning redundant layers: Loss {candidate_loss:.4f}, Removed {removed_layers}, new max layers {candidate_max_layers}"
491
+ )
492
+ current_params = candidate_params
493
+ current_max_layers = candidate_max_layers
494
+ best_loss = candidate_loss
495
+ improvement = True
496
+ break # Restart search after a successful removal.
497
+ # End for each candidate.
498
+ tbar.close()
499
+ return current_params, best_loss, current_max_layers
@@ -108,7 +108,7 @@ class FilamentOptimizer:
108
108
  "We only record the best discrete function at the end to allow for penalty loss to function correctly."
109
109
  )
110
110
  plt.ion()
111
- self.fig, self.ax = plt.subplots(1, 3, figsize=(14, 6))
111
+ self.fig, self.ax = plt.subplots(1, 4, figsize=(14, 6))
112
112
  self.target_im_ax = self.ax[0].imshow(
113
113
  np.array(self.target.cpu(), dtype=np.uint8)
114
114
  )
@@ -117,10 +117,16 @@ class FilamentOptimizer:
117
117
  np.zeros((self.H, self.W, 3), dtype=np.uint8)
118
118
  )
119
119
  self.ax[1].set_title("Current Composite")
120
- self.best_comp_ax = self.ax[2].imshow(
120
+
121
+ self.depth_map_ax = self.ax[2].imshow(
122
+ np.zeros((self.H, self.W), dtype=np.uint8), cmap="viridis"
123
+ )
124
+ self.ax[2].set_title("Current Depth Map")
125
+
126
+ self.best_comp_ax = self.ax[3].imshow(
121
127
  np.zeros((self.H, self.W, 3), dtype=np.uint8)
122
128
  )
123
- self.ax[2].set_title("Best Discrete Composite")
129
+ self.ax[3].set_title("Best Discrete Composite")
124
130
  plt.pause(0.1)
125
131
 
126
132
  def _get_tau(self):
@@ -232,6 +238,26 @@ class FilamentOptimizer:
232
238
  )
233
239
  self.best_comp_ax.set_data(best_comp_np)
234
240
 
241
+ # Update the depth map correctly.
242
+ with torch.no_grad():
243
+ height_map = (self.max_layers * self.h) * torch.sigmoid(
244
+ self.params["pixel_height_logits"]
245
+ )
246
+ height_map = height_map.cpu().detach().numpy()
247
+
248
+ # Normalize safely, checking for a constant image.
249
+ if np.allclose(height_map.max(), height_map.min()):
250
+ height_map_norm = np.zeros_like(height_map)
251
+ else:
252
+ height_map_norm = (height_map - height_map.min()) / (
253
+ height_map.max() - height_map.min()
254
+ )
255
+
256
+ height_map_uint8 = (height_map_norm * 255).astype(np.uint8)
257
+ self.depth_map_ax.set_data(height_map_uint8)
258
+ # Update the color limits so that the colormap correctly maps the range 0–255.
259
+ self.depth_map_ax.set_clim(0, 255)
260
+
235
261
  self.fig.suptitle(
236
262
  f"Step {self.num_steps_done}, Tau: {tau_g:.4f}, Loss: {self.loss:.4f}, Best Discrete Loss: {self.best_discrete_loss:.4f}"
237
263
  )
@@ -12,12 +12,14 @@ from autoforge.Helper.OptimizerHelper import (
12
12
  init_height_map,
13
13
  composite_image,
14
14
  discretize_solution,
15
+ init_height_map_depth_color_adjusted,
15
16
  )
16
17
  from autoforge.Helper.OutputHelper import (
17
18
  generate_stl,
18
19
  generate_swap_instructions,
19
20
  generate_project_file,
20
21
  )
22
+ from autoforge.Helper.PruningHelper import prune_redundant_layers
21
23
  from autoforge.Loss.PerceptionLoss import MultiLayerVGGPerceptualLoss
22
24
  from autoforge.Modules.Optimizer import FilamentOptimizer
23
25
 
@@ -102,13 +104,13 @@ def main():
102
104
  parser.add_argument(
103
105
  "--pruning_max_colors",
104
106
  type=int,
105
- default=10,
107
+ default=100,
106
108
  help="Max number of colors allowed after pruning",
107
109
  )
108
110
  parser.add_argument(
109
111
  "--pruning_max_swaps",
110
112
  type=int,
111
- default=20,
113
+ default=100,
112
114
  help="Max number of swaps allowed after pruning",
113
115
  )
114
116
 
@@ -119,6 +121,48 @@ def main():
119
121
  help="Specify the random seed, or use 0 for automatic generation",
120
122
  )
121
123
 
124
+ parser.add_argument(
125
+ "--use_depth_anything_height_initialization",
126
+ action="store_true",
127
+ help="Use Depth Anything v2 for height image initialization",
128
+ )
129
+ parser.add_argument(
130
+ "--depth_strength",
131
+ type=float,
132
+ default=0.25,
133
+ help="Weight (0 to 1) for blending even spacing with the cluster's average depth",
134
+ )
135
+ parser.add_argument(
136
+ "--depth_threshold",
137
+ type=float,
138
+ default=0.05,
139
+ help="Threshold for splitting two distinct colors based on depth",
140
+ )
141
+ parser.add_argument(
142
+ "--min_cluster_value",
143
+ type=float,
144
+ default=0.1,
145
+ help="Minimum normalized value for the lowest cluster (to avoid pure black)",
146
+ )
147
+ parser.add_argument(
148
+ "--w_depth",
149
+ type=float,
150
+ default=0.5,
151
+ help="Weight for depth difference in ordering_depth",
152
+ )
153
+ parser.add_argument(
154
+ "--w_lum",
155
+ type=float,
156
+ default=1.0,
157
+ help="Weight for luminance difference in ordering_depth",
158
+ )
159
+ parser.add_argument(
160
+ "--order_blend",
161
+ type=float,
162
+ default=0.1,
163
+ help="Slider (0 to 1) blending original luminance ordering (0) and depth-informed ordering (1).",
164
+ )
165
+
122
166
  args = parser.parse_args()
123
167
 
124
168
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
@@ -163,9 +207,22 @@ def main():
163
207
  output_target = torch.tensor(output_img_np, dtype=torch.float32, device=device)
164
208
 
165
209
  # Initialize pixel_height_logits from the large (final) image
166
- pixel_height_logits_init = init_height_map(
167
- output_img_np, args.max_layers, args.layer_height, random_seed=random_seed
168
- )
210
+ if args.use_depth_anything_height_initialization:
211
+ pixel_height_logits_init = init_height_map_depth_color_adjusted(
212
+ output_img_np,
213
+ args.max_layers,
214
+ depth_strength=args.depth_strength,
215
+ depth_threshold=args.depth_threshold,
216
+ min_cluster_value=args.min_cluster_value,
217
+ w_depth=args.w_depth,
218
+ w_lum=args.w_lum,
219
+ order_blend=args.order_blend,
220
+ )
221
+ else:
222
+ # Default initialization using your existing method
223
+ pixel_height_logits_init = init_height_map(
224
+ output_img_np, args.max_layers, args.layer_height, random_seed=random_seed
225
+ )
169
226
 
170
227
  # But we will solve at the smaller resolution => resize that init to solver size
171
228
  # Add a dummy channel so that cv2.resize can handle it
@@ -231,16 +288,27 @@ def main():
231
288
  "global_logits": disc_global,
232
289
  "pixel_height_logits": torch.from_numpy(pixel_height_logits_init).to(device),
233
290
  }
291
+
292
+ if args.perform_pruning:
293
+ print("Removing redundant layers from the discrete solution...")
294
+ params, pruned_loss, max_layers = prune_redundant_layers(
295
+ params,
296
+ final_tau=args.decay,
297
+ h=args.layer_height,
298
+ target=output_target, # or whichever target image you use for final compositing
299
+ material_colors=material_colors,
300
+ material_TDs=material_TDs,
301
+ background=background,
302
+ rng_seed=optimizer.best_seed, # using the best seed from your optimizer
303
+ perception_loss_module=perception_loss_module,
304
+ tolerance=1e-3, # adjust as needed
305
+ )
306
+ args.max_layers = max_layers
307
+
234
308
  disc_global, disc_height_image = discretize_solution(
235
309
  params, args.decay, args.layer_height, args.max_layers
236
310
  )
237
- # Overwrite the big resolution height logits into optimizer params
238
- # If you want to do a final compositing or more sophisticated approach, do it here:
239
- # Example: (but keep in mind the shapes might differ)
240
- # _, disc_height_image = optimizer.get_discretized_solution(
241
- # best=True,
242
- # custom_height_logits=torch.from_numpy(pixel_height_logits_init).to(device),
243
- # )
311
+
244
312
  print("Done. Saving outputs...")
245
313
  # Save Image
246
314
  comp_disc = composite_image(
File without changes