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.
- {autoforge-1.2.1 → autoforge-1.3.0}/PKG-INFO +29 -10
- {autoforge-1.2.1 → autoforge-1.3.0}/README.md +27 -9
- {autoforge-1.2.1 → autoforge-1.3.0}/pyproject.toml +2 -1
- {autoforge-1.2.1 → autoforge-1.3.0}/src/AutoForge.egg-info/PKG-INFO +29 -10
- {autoforge-1.2.1 → autoforge-1.3.0}/src/AutoForge.egg-info/requires.txt +1 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Helper/OptimizerHelper.py +264 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Helper/PruningHelper.py +192 -13
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Modules/Optimizer.py +29 -3
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/auto_forge.py +80 -12
- {autoforge-1.2.1 → autoforge-1.3.0}/setup.cfg +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/AutoForge.egg-info/SOURCES.txt +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/AutoForge.egg-info/dependency_links.txt +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/AutoForge.egg-info/entry_points.txt +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/AutoForge.egg-info/top_level.txt +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Helper/FilamentHelper.py +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Helper/ImageHelper.py +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Helper/OutputHelper.py +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Helper/__init__.py +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Loss/LossFunctions.py +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Loss/PerceptionLoss.py +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Loss/__init__.py +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/Modules/__init__.py +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/__init__.py +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/__main__.py +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/auto_forge_gradio.py +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/color_fit_bayesian.py +0 -0
- {autoforge-1.2.1 → autoforge-1.3.0}/src/autoforge/filamentcolors_library.py +0 -0
- {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.
|
|
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`:
|
|
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
|
-
|
|
114
|
-
- `--
|
|
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`:
|
|
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
|
-
|
|
95
|
-
- `--
|
|
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.
|
|
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.
|
|
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`:
|
|
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
|
-
|
|
114
|
-
- `--
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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[
|
|
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=
|
|
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=
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|