autoforge 1.5.0__tar.gz → 1.5.2__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 (35) hide show
  1. {autoforge-1.5.0 → autoforge-1.5.2}/PKG-INFO +8 -15
  2. {autoforge-1.5.0 → autoforge-1.5.2}/README.md +7 -14
  3. {autoforge-1.5.0 → autoforge-1.5.2}/pyproject.toml +1 -1
  4. {autoforge-1.5.0 → autoforge-1.5.2}/src/AutoForge.egg-info/PKG-INFO +8 -15
  5. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Helper/OptimizerHelper.py +319 -148
  6. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Modules/Optimizer.py +0 -1
  7. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/auto_forge.py +3 -3
  8. {autoforge-1.5.0 → autoforge-1.5.2}/setup.cfg +0 -0
  9. {autoforge-1.5.0 → autoforge-1.5.2}/src/AutoForge.egg-info/SOURCES.txt +0 -0
  10. {autoforge-1.5.0 → autoforge-1.5.2}/src/AutoForge.egg-info/dependency_links.txt +0 -0
  11. {autoforge-1.5.0 → autoforge-1.5.2}/src/AutoForge.egg-info/entry_points.txt +0 -0
  12. {autoforge-1.5.0 → autoforge-1.5.2}/src/AutoForge.egg-info/requires.txt +0 -0
  13. {autoforge-1.5.0 → autoforge-1.5.2}/src/AutoForge.egg-info/top_level.txt +0 -0
  14. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Gui/Views/__init__.py +0 -0
  15. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Gui/Views/backend_database.py +0 -0
  16. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Gui/Views/filament_editor.py +0 -0
  17. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Gui/__init__.py +0 -0
  18. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Gui/app.py +0 -0
  19. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Gui/main.py +0 -0
  20. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Helper/CAdamW.py +0 -0
  21. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Helper/FilamentHelper.py +0 -0
  22. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Helper/ImageHelper.py +0 -0
  23. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Helper/OutputHelper.py +0 -0
  24. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Helper/PruningHelper.py +0 -0
  25. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Helper/__init__.py +0 -0
  26. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Helper/filamentcolors_library.py +0 -0
  27. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Loss/LossFunctions.py +0 -0
  28. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Loss/PerceptionLoss.py +0 -0
  29. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Loss/__init__.py +0 -0
  30. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Misc/__init__.py +0 -0
  31. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Misc/color_fit_all.py +0 -0
  32. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/Modules/__init__.py +0 -0
  33. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/__init__.py +0 -0
  34. {autoforge-1.5.0 → autoforge-1.5.2}/src/autoforge/__main__.py +0 -0
  35. {autoforge-1.5.0 → autoforge-1.5.2}/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.5.0
3
+ Version: 1.5.2
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
@@ -25,16 +25,6 @@ AutoForge is a Python tool for generating 3D printed layered models from an inpu
25
25
 
26
26
  **TLDR:** It uses a picture to generate a 3D layer image that you can print with a 3d printer. Similar to [Hueforge](https://shop.thehueforge.com/), but without the manual work (and without the artistic control).
27
27
 
28
-
29
- ## Important Information
30
-
31
- We recently switched from JAX to PyTorch, to allow for a more streamlined development process. \
32
- This can have some unforeseen consequences, so please report any bugs you find.
33
-
34
- This release should give better gpu support for more users, especially on windows. \
35
- If you have problems running the code on your gpu, please refer to the [Pytorch Homepage](https://pytorch.org/) for help. \
36
- Both CUDA and ROCm are supported, but you need to install the correct version of pytorch for your system.
37
-
38
28
  ## Example
39
29
  All examples use only the 13 BambuLab Basic filaments, currently available in Hueforge.
40
30
  <div style="display: flex; justify-content: center; gap: 20px;">
@@ -46,9 +36,9 @@ All examples use only the 13 BambuLab Basic filaments, currently available in Hu
46
36
  </div>
47
37
  <div style="text-align: center;">
48
38
  <h3>Autoforge Output (75 color layers (3.0mm) + 10 gray background layers (0.4mm)) </h3>
49
- <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/lofi_discretized.jpg" width="200" />
39
+ <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/lofi_discretized.png" width="200" />
50
40
  <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/nature_discretized.jpg" width="200" />
51
- <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/cat_discretized.jpg" width="200" />
41
+ <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/cat_discretized.png" width="200" />
52
42
  </div>
53
43
  <div style="text-align: center;">
54
44
  <h3>Imported Hueforge View</h3>
@@ -80,6 +70,9 @@ To install AutoForge, simply install the current version from PyPI:
80
70
  pip install autoforge
81
71
  ```
82
72
 
73
+ If you have problems running the code on your gpu, please refer to the [Pytorch Homepage](https://pytorch.org/) for help. \
74
+ Both CUDA and ROCm are supported, but you need to install the correct version of pytorch for your system.
75
+
83
76
  ## Usage
84
77
 
85
78
  The script is run from the command line and accepts several arguments. Below is an example command:
@@ -125,7 +118,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
125
118
  - `--random_seed`: Random seed for reproducibility (default: 0 (disabled) ).
126
119
 
127
120
  ### Experimental Deph Anything V2 parameters
128
- I got a request to add a initializing function that takes the original deph of the image into account when initializing. \
121
+ I got a request to add an initializing function that takes the original depth of the image into account when initializing. \
129
122
  In theory this should give you a nice background/foreground separation, but in practice it can degrade the quality of the output. \
130
123
  In addition you need the VRAM to run the depth model. \
131
124
  I currently don't recommend it as the output is not as good as without it, but I will leave it in for now. \
@@ -172,7 +165,7 @@ The software is provided as-is and comes with no warranty or guarantee of suppor
172
165
  ## Acknowledgements
173
166
 
174
167
  First and foremost:
175
- - [Hueforge](https://shop.thehueforge.com/) for providing the filament data and inspiration for this project.
168
+ - [Hueforge](https://shop.thehueforge.com/) for providing the inspiration for this project.
176
169
  Without it, this project would not have been possible.
177
170
 
178
171
  AutoForge makes use of several open source libraries:
@@ -4,16 +4,6 @@ AutoForge is a Python tool for generating 3D printed layered models from an inpu
4
4
 
5
5
  **TLDR:** It uses a picture to generate a 3D layer image that you can print with a 3d printer. Similar to [Hueforge](https://shop.thehueforge.com/), but without the manual work (and without the artistic control).
6
6
 
7
-
8
- ## Important Information
9
-
10
- We recently switched from JAX to PyTorch, to allow for a more streamlined development process. \
11
- This can have some unforeseen consequences, so please report any bugs you find.
12
-
13
- This release should give better gpu support for more users, especially on windows. \
14
- If you have problems running the code on your gpu, please refer to the [Pytorch Homepage](https://pytorch.org/) for help. \
15
- Both CUDA and ROCm are supported, but you need to install the correct version of pytorch for your system.
16
-
17
7
  ## Example
18
8
  All examples use only the 13 BambuLab Basic filaments, currently available in Hueforge.
19
9
  <div style="display: flex; justify-content: center; gap: 20px;">
@@ -25,9 +15,9 @@ All examples use only the 13 BambuLab Basic filaments, currently available in Hu
25
15
  </div>
26
16
  <div style="text-align: center;">
27
17
  <h3>Autoforge Output (75 color layers (3.0mm) + 10 gray background layers (0.4mm)) </h3>
28
- <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/lofi_discretized.jpg" width="200" />
18
+ <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/lofi_discretized.png" width="200" />
29
19
  <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/nature_discretized.jpg" width="200" />
30
- <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/cat_discretized.jpg" width="200" />
20
+ <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/cat_discretized.png" width="200" />
31
21
  </div>
32
22
  <div style="text-align: center;">
33
23
  <h3>Imported Hueforge View</h3>
@@ -59,6 +49,9 @@ To install AutoForge, simply install the current version from PyPI:
59
49
  pip install autoforge
60
50
  ```
61
51
 
52
+ If you have problems running the code on your gpu, please refer to the [Pytorch Homepage](https://pytorch.org/) for help. \
53
+ Both CUDA and ROCm are supported, but you need to install the correct version of pytorch for your system.
54
+
62
55
  ## Usage
63
56
 
64
57
  The script is run from the command line and accepts several arguments. Below is an example command:
@@ -104,7 +97,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
104
97
  - `--random_seed`: Random seed for reproducibility (default: 0 (disabled) ).
105
98
 
106
99
  ### Experimental Deph Anything V2 parameters
107
- I got a request to add a initializing function that takes the original deph of the image into account when initializing. \
100
+ I got a request to add an initializing function that takes the original depth of the image into account when initializing. \
108
101
  In theory this should give you a nice background/foreground separation, but in practice it can degrade the quality of the output. \
109
102
  In addition you need the VRAM to run the depth model. \
110
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. \
@@ -151,7 +144,7 @@ The software is provided as-is and comes with no warranty or guarantee of suppor
151
144
  ## Acknowledgements
152
145
 
153
146
  First and foremost:
154
- - [Hueforge](https://shop.thehueforge.com/) for providing the filament data and inspiration for this project.
147
+ - [Hueforge](https://shop.thehueforge.com/) for providing the inspiration for this project.
155
148
  Without it, this project would not have been possible.
156
149
 
157
150
  AutoForge makes use of several open source libraries:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "AutoForge"
7
- version="1.5.0"
7
+ version="1.5.2"
8
8
  description = "AutoForge is a Python tool for generating 3D printed layered models from an input image."
9
9
  authors = [{ name = "Hendric Voss", email = "hvoss@techfak.uni-bielefeld.de" }]
10
10
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: AutoForge
3
- Version: 1.5.0
3
+ Version: 1.5.2
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
@@ -25,16 +25,6 @@ AutoForge is a Python tool for generating 3D printed layered models from an inpu
25
25
 
26
26
  **TLDR:** It uses a picture to generate a 3D layer image that you can print with a 3d printer. Similar to [Hueforge](https://shop.thehueforge.com/), but without the manual work (and without the artistic control).
27
27
 
28
-
29
- ## Important Information
30
-
31
- We recently switched from JAX to PyTorch, to allow for a more streamlined development process. \
32
- This can have some unforeseen consequences, so please report any bugs you find.
33
-
34
- This release should give better gpu support for more users, especially on windows. \
35
- If you have problems running the code on your gpu, please refer to the [Pytorch Homepage](https://pytorch.org/) for help. \
36
- Both CUDA and ROCm are supported, but you need to install the correct version of pytorch for your system.
37
-
38
28
  ## Example
39
29
  All examples use only the 13 BambuLab Basic filaments, currently available in Hueforge.
40
30
  <div style="display: flex; justify-content: center; gap: 20px;">
@@ -46,9 +36,9 @@ All examples use only the 13 BambuLab Basic filaments, currently available in Hu
46
36
  </div>
47
37
  <div style="text-align: center;">
48
38
  <h3>Autoforge Output (75 color layers (3.0mm) + 10 gray background layers (0.4mm)) </h3>
49
- <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/lofi_discretized.jpg" width="200" />
39
+ <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/lofi_discretized.png" width="200" />
50
40
  <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/nature_discretized.jpg" width="200" />
51
- <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/cat_discretized.jpg" width="200" />
41
+ <img src="https://github.com/hvoss-techfak/AutoForge/blob/main/images/cat_discretized.png" width="200" />
52
42
  </div>
53
43
  <div style="text-align: center;">
54
44
  <h3>Imported Hueforge View</h3>
@@ -80,6 +70,9 @@ To install AutoForge, simply install the current version from PyPI:
80
70
  pip install autoforge
81
71
  ```
82
72
 
73
+ If you have problems running the code on your gpu, please refer to the [Pytorch Homepage](https://pytorch.org/) for help. \
74
+ Both CUDA and ROCm are supported, but you need to install the correct version of pytorch for your system.
75
+
83
76
  ## Usage
84
77
 
85
78
  The script is run from the command line and accepts several arguments. Below is an example command:
@@ -125,7 +118,7 @@ autoforge --input_image path/to/input_image.jpg --csv_file path/to/materials.csv
125
118
  - `--random_seed`: Random seed for reproducibility (default: 0 (disabled) ).
126
119
 
127
120
  ### Experimental Deph Anything V2 parameters
128
- I got a request to add a initializing function that takes the original deph of the image into account when initializing. \
121
+ I got a request to add an initializing function that takes the original depth of the image into account when initializing. \
129
122
  In theory this should give you a nice background/foreground separation, but in practice it can degrade the quality of the output. \
130
123
  In addition you need the VRAM to run the depth model. \
131
124
  I currently don't recommend it as the output is not as good as without it, but I will leave it in for now. \
@@ -172,7 +165,7 @@ The software is provided as-is and comes with no warranty or guarantee of suppor
172
165
  ## Acknowledgements
173
166
 
174
167
  First and foremost:
175
- - [Hueforge](https://shop.thehueforge.com/) for providing the filament data and inspiration for this project.
168
+ - [Hueforge](https://shop.thehueforge.com/) for providing the inspiration for this project.
176
169
  Without it, this project would not have been possible.
177
170
 
178
171
  AutoForge makes use of several open source libraries:
@@ -20,6 +20,16 @@ def adaptive_round(
20
20
  ) -> torch.Tensor:
21
21
  """
22
22
  Smooth rounding based on temperature 'tau'.
23
+
24
+ Args:
25
+ x (torch.Tensor): The input tensor to be rounded.
26
+ tau (float): The current temperature parameter.
27
+ high_tau (float): The high threshold for the temperature.
28
+ low_tau (float): The low threshold for the temperature.
29
+ temp (float): The temperature parameter for the sigmoid function.
30
+
31
+ Returns:
32
+ torch.Tensor: The rounded tensor.
23
33
  """
24
34
  if tau <= low_tau:
25
35
  return torch.round(x)
@@ -40,6 +50,16 @@ def adaptive_round(
40
50
  # A deterministic random generator that mimics torch.rand_like.
41
51
  @torch.jit.script
42
52
  def deterministic_rand_like(tensor: torch.Tensor, seed: int) -> torch.Tensor:
53
+ """
54
+ Generate a deterministic random tensor that mimics torch.rand_like.
55
+
56
+ Args:
57
+ tensor (torch.Tensor): The input tensor whose shape and device will be used.
58
+ seed (int): The seed for the deterministic random generator.
59
+
60
+ Returns:
61
+ torch.Tensor: A tensor with the same shape as the input tensor, filled with deterministic random values.
62
+ """
43
63
  # Compute the total number of elements.
44
64
  n: int = 1
45
65
  for d in tensor.shape:
@@ -59,6 +79,18 @@ def deterministic_rand_like(tensor: torch.Tensor, seed: int) -> torch.Tensor:
59
79
  def deterministic_gumbel_softmax(
60
80
  logits: torch.Tensor, tau: float, hard: bool, rng_seed: int
61
81
  ) -> torch.Tensor:
82
+ """
83
+ Apply the Gumbel-Softmax trick in a deterministic manner using a fixed random seed.
84
+
85
+ Args:
86
+ logits (torch.Tensor): The input logits tensor.
87
+ tau (float): The temperature parameter for the Gumbel-Softmax.
88
+ hard (bool): If True, the output will be one-hot encoded.
89
+ rng_seed (int): The seed for the deterministic random generator.
90
+
91
+ Returns:
92
+ torch.Tensor: The resulting tensor after applying the Gumbel-Softmax trick.
93
+ """
62
94
  eps: float = 1e-20
63
95
  # Instead of torch.rand_like(..., generator=...), use our deterministic_rand_like.
64
96
  U = deterministic_rand_like(logits, rng_seed)
@@ -83,218 +115,341 @@ def composite_image_cont(
83
115
  tau_global: float,
84
116
  h: float,
85
117
  max_layers: int,
86
- material_colors: torch.Tensor,
87
- material_TDs: torch.Tensor,
88
- background: torch.Tensor,
118
+ material_colors: torch.Tensor, # [n_materials, 3]
119
+ material_TDs: torch.Tensor, # [n_materials]
120
+ background: torch.Tensor, # [3]
89
121
  ) -> torch.Tensor:
90
122
  """
91
123
  Continuous compositing over all pixels.
92
124
  Uses Gumbel softmax with either hard or soft sampling depending on tau_global.
125
+
126
+ Args:
127
+ pixel_height_logits (torch.Tensor): Logits for pixel heights, shape [H, W].
128
+ global_logits (torch.Tensor): Logits for global material assignment, shape [max_layers, n_materials].
129
+ tau_height (float): Temperature parameter for height rounding.
130
+ tau_global (float): Temperature parameter for global material assignment.
131
+ h (float): Height of each layer.
132
+ max_layers (int): Maximum number of layers.
133
+ material_colors (torch.Tensor): Tensor of material colors, shape [n_materials, 3].
134
+ material_TDs (torch.Tensor): Tensor of material transmission/opacity parameters, shape [n_materials].
135
+ background (torch.Tensor): Background color tensor, shape [3].
136
+
137
+ Returns:
138
+ torch.Tensor: Composite image tensor, shape [H, W, 3].
93
139
  """
140
+ # Compute continuous and discrete layers for each pixel
94
141
  pixel_height = (max_layers * h) * torch.sigmoid(pixel_height_logits)
95
142
  continuous_layers = pixel_height / h
96
143
 
144
+ # adaptive rounding
97
145
  adaptive_layers = adaptive_round(
98
146
  continuous_layers, tau_height, high_tau=0.1, low_tau=0.01, temp=0.1
99
147
  )
148
+
149
+ # "Straight-through" trick to keep gradient from adaptive rounding
100
150
  discrete_layers_temp = torch.round(continuous_layers)
101
151
  discrete_layers = (
102
152
  discrete_layers_temp + (adaptive_layers - discrete_layers_temp).detach()
103
- ).to(torch.int32)
104
-
153
+ ).to(torch.int32) # shape [H, W]
154
+
155
+ # Precompute global material assignments for each layer
156
+ # Use a single Gumbel Softmax call for all layers.
157
+ hard_flag = tau_global < 1e-3
158
+ # global_logits: [max_layers, n_materials]
159
+ p = F.gumbel_softmax(
160
+ global_logits, tau_global, hard=hard_flag, dim=1
161
+ ) # [L, n_materials]
162
+
163
+ # Compute color_i and TD_i for each layer in one go.
164
+ # color_i -> [max_layers, 3], TD_i -> [max_layers]
165
+ layer_colors = p @ material_colors # [L, 3]
166
+ layer_TDs = p @ material_TDs # [L]
167
+ layer_TDs.clamp_(1e-8, 1e8)
168
+
169
+ # Prepare output and compositing variables
105
170
  H, W = pixel_height.shape
106
171
  comp = torch.zeros(H, W, 3, dtype=torch.float32, device=pixel_height.device)
107
172
  remaining = torch.ones(H, W, dtype=torch.float32, device=pixel_height.device)
108
173
 
109
- # opacity function parameters
174
+ # Opacity function parameters
110
175
  o = -1.2416557e-02
111
176
  A = 9.6407950e-01
112
177
  k = 3.4103447e01
113
178
  b = -4.1554203e00
114
179
 
180
+ # Composite from top layer (max_layers-1) down to 0
115
181
  for i in range(max_layers):
116
182
  layer_idx = max_layers - 1 - i
117
- p_print = (discrete_layers > layer_idx).float() # [H,W]
118
- eff_thick = p_print * h # effective thickness per pixel
119
-
120
- # continuous mode: use hard gumbel if tau_global is very low
121
- if tau_global < 1e-3:
122
- p_i = F.gumbel_softmax(global_logits[layer_idx], tau_global, hard=True)
123
- else:
124
- p_i = F.gumbel_softmax(global_logits[layer_idx], tau_global, hard=False)
183
+ # Mask of which pixels actually print this layer:
184
+ p_print = (discrete_layers > layer_idx).to(dtype=comp.dtype)
125
185
 
126
- color_i = torch.matmul(p_i, material_colors)
127
- TD_i = torch.matmul(p_i, material_TDs)
128
- TD_i = torch.clamp(TD_i, 1e-8, 1e8)
186
+ # Effective thickness for those pixels
187
+ eff_thick = p_print * h
129
188
 
189
+ # Opacity based on thickness and material TD
190
+ TD_i = layer_TDs[layer_idx]
130
191
  opac = o + (A * torch.log1p(k * (eff_thick / TD_i)) + b * (eff_thick / TD_i))
131
192
  opac = torch.clamp(opac, 0.0, 1.0)
132
193
 
133
- comp = comp + ((remaining * opac).unsqueeze(-1) * color_i)
134
- remaining = remaining * (1 - opac)
194
+ # Add contribution to comp
195
+ color_i = layer_colors[layer_idx] # shape [3]
196
+ # We need to broadcast opac: [H, W] -> [H, W, 1]
197
+ comp = comp + (remaining * opac).unsqueeze(-1) * color_i
198
+
199
+ # Update the remaining factor
200
+ remaining = remaining * (1.0 - opac)
135
201
 
202
+ # Add background contribution
136
203
  comp = comp + remaining.unsqueeze(-1) * background
204
+
205
+ # 6. Scale to [0, 255] range
137
206
  return comp * 255.0
138
207
 
139
208
 
140
209
  @torch.jit.script
141
210
  def composite_image_disc(
142
- pixel_height_logits: torch.Tensor,
143
- global_logits: torch.Tensor,
211
+ pixel_height_logits: torch.Tensor, # [H,W]
212
+ global_logits: torch.Tensor, # [max_layers, n_materials]
144
213
  tau_height: float,
145
214
  tau_global: float,
146
215
  h: float,
147
216
  max_layers: int,
148
- material_colors: torch.Tensor,
149
- material_TDs: torch.Tensor,
150
- background: torch.Tensor,
217
+ material_colors: torch.Tensor, # [n_materials, 3]
218
+ material_TDs: torch.Tensor, # [n_materials]
219
+ background: torch.Tensor, # [3]
151
220
  rng_seed: int = -1,
152
221
  ) -> torch.Tensor:
153
222
  """
154
- Discrete compositing over all pixels.
155
- For each layer (traversed from top to bottom), we compute a one-hot material selection.
156
- For pixels where the layer is printed (as determined by discrete_layers),
157
- contiguous layers with the same material are merged (i.e. their effective thickness is accumulated)
158
- and then composited using the opacity function. The accumulated segment is applied when
159
- the material changes or if the layer is not printed.
223
+ Perform discrete compositing over all pixels.
224
+
225
+ Args:
226
+ pixel_height_logits (torch.Tensor): Logits for pixel heights, shape [H, W].
227
+ global_logits (torch.Tensor): Logits for global material assignment, shape [max_layers, n_materials].
228
+ tau_height (float): Temperature parameter for height rounding.
229
+ tau_global (float): Temperature parameter for global material assignment.
230
+ h (float): Height of each layer.
231
+ max_layers (int): Maximum number of layers.
232
+ material_colors (torch.Tensor): Tensor of material colors, shape [n_materials, 3].
233
+ material_TDs (torch.Tensor): Tensor of material transmission/opacity parameters, shape [n_materials].
234
+ background (torch.Tensor): Background color tensor, shape [3].
235
+ rng_seed (int, optional): Random seed for deterministic sampling. Defaults to -1.
236
+
237
+ Returns:
238
+ torch.Tensor: Composite image tensor, shape [H, W, 3].
160
239
  """
161
- # Compute pixel height and determine discrete layers.
240
+
241
+ # -------------------------------------------------------------------------
242
+ # 1) Compute discrete per-pixel layer counts (discrete_layers).
243
+ # -------------------------------------------------------------------------
244
+ # pixel_height ~ [0, max_layers*h]
162
245
  pixel_height = (max_layers * h) * torch.sigmoid(pixel_height_logits)
246
+
247
+ # continuous_layers ~ [0, max_layers]
163
248
  continuous_layers = pixel_height / h
164
249
 
250
+ # Use your "adaptive rounding" trick if desired:
165
251
  adaptive_layers = adaptive_round(
166
252
  continuous_layers, tau_height, high_tau=0.1, low_tau=0.01, temp=0.1
167
253
  )
168
254
  discrete_layers_temp = torch.round(continuous_layers)
169
255
  discrete_layers = (
170
256
  discrete_layers_temp + (adaptive_layers - discrete_layers_temp).detach()
171
- ).to(torch.int32)
172
-
173
- H, W = pixel_height.shape
174
- comp = torch.zeros(H, W, 3, dtype=torch.float32, device=pixel_height.device)
175
- remaining = torch.ones(H, W, dtype=torch.float32, device=pixel_height.device)
176
-
177
- # Current material and accumulated thickness per pixel.
178
- # cur_mat is initialized to -1 meaning "no material".
179
- cur_mat = -torch.ones((H, W), dtype=torch.int32, device=pixel_height.device)
180
- acc_thick = torch.zeros((H, W), dtype=torch.float32, device=pixel_height.device)
181
-
182
- # Opacity function parameters.
183
- o = -1.2416557e-02
184
- A = 9.6407950e-01
185
- k = 3.4103447e01
186
- b = -4.1554203e00
187
-
188
- for i in range(max_layers):
189
- layer_idx = max_layers - 1 - i
190
- p_print = discrete_layers > layer_idx # Boolean mask [H,W]
191
- eff_thick = (
192
- p_print.to(torch.float32) * h
193
- ) # effective thickness for printed pixels
194
-
195
- # Determine material for this layer using discrete (one-hot) gumbel softmax.
196
- if rng_seed >= 0:
257
+ ).to(torch.int32) # [H,W]
258
+
259
+ # -------------------------------------------------------------------------
260
+ # 2) Pick a single global material per layer, either deterministically
261
+ # or via gumbel softmax, as in original code.
262
+ # -------------------------------------------------------------------------
263
+ if rng_seed >= 0:
264
+ # Deterministic sampling for each layer
265
+ new_mats_list = []
266
+ for layer_idx in range(max_layers):
197
267
  p_i = deterministic_gumbel_softmax(
198
- global_logits[layer_idx], tau_global, True, rng_seed + layer_idx
199
- )
200
- else:
201
- p_i = F.gumbel_softmax(global_logits[layer_idx], tau_global, hard=True)
202
- # Cast new_mat to int32 so it matches cur_mat.
203
- new_mat = torch.argmax(p_i, 0).to(torch.int32)
204
-
205
- # 1. For pixels where no current material is set, start a new segment.
206
- mask_new = p_print & (cur_mat == -1)
207
- if mask_new.any():
208
- cur_mat[mask_new] = new_mat
209
- acc_thick[mask_new] = eff_thick[mask_new]
210
-
211
- # 2. For pixels where the current segment has the same material, accumulate thickness.
212
- mask_same = p_print & (cur_mat == new_mat)
213
- if mask_same.any():
214
- acc_thick[mask_same] = acc_thick[mask_same] + eff_thick[mask_same]
215
-
216
- # 3. For pixels where the current segment has a different material,
217
- # composite the accumulated segment and start a new one.
218
- mask_diff = p_print & (cur_mat != -1) & (cur_mat != new_mat)
219
- if mask_diff.any():
220
- # Convert indices to int64 for index_select.
221
- indices_diff = cur_mat[mask_diff].to(torch.int64)
222
- td_vals = material_TDs.index_select(0, indices_diff)
223
- col_vals = material_colors.index_select(0, indices_diff)
224
- opac_vals = o + (
225
- A * torch.log1p(k * (acc_thick[mask_diff] / td_vals))
226
- + b * (acc_thick[mask_diff] / td_vals)
227
- )
228
- opac_vals = torch.clamp(opac_vals, 0.0, 1.0)
229
- comp[mask_diff] = comp[mask_diff] + (
230
- (remaining[mask_diff] * opac_vals).unsqueeze(-1) * col_vals
231
- )
232
- remaining[mask_diff] = remaining[mask_diff] * (1 - opac_vals)
233
- cur_mat[mask_diff] = new_mat
234
- acc_thick[mask_diff] = eff_thick[mask_diff]
235
-
236
- # 4. For pixels where this layer is not printed but we have a pending segment,
237
- # composite that segment and reset.
238
- mask_not_printed = (~p_print) & (cur_mat != -1)
239
- if mask_not_printed.any():
240
- indices_np = cur_mat[mask_not_printed].to(torch.int64)
241
- td_vals = material_TDs.index_select(0, indices_np)
242
- col_vals = material_colors.index_select(0, indices_np)
243
- opac_vals = o + (
244
- A * torch.log1p(k * (acc_thick[mask_not_printed] / td_vals))
245
- + b * (acc_thick[mask_not_printed] / td_vals)
268
+ global_logits[layer_idx],
269
+ tau_global,
270
+ hard=True,
271
+ rng_seed=(rng_seed + layer_idx),
246
272
  )
247
- opac_vals = torch.clamp(opac_vals, 0.0, 1.0)
248
- comp[mask_not_printed] = comp[mask_not_printed] + (
249
- (remaining[mask_not_printed] * opac_vals).unsqueeze(-1) * col_vals
250
- )
251
- remaining[mask_not_printed] = remaining[mask_not_printed] * (1 - opac_vals)
252
- cur_mat[mask_not_printed] = -1
253
- acc_thick[mask_not_printed] = 0.0
254
-
255
- # After the loop, composite any remaining accumulated segment.
256
- mask_remain = cur_mat != -1
257
- if mask_remain.any():
258
- indices_rem = cur_mat[mask_remain].to(torch.int64)
259
- td_vals = material_TDs.index_select(0, indices_rem)
260
- col_vals = material_colors.index_select(0, indices_rem)
261
- opac_vals = o + (
262
- A * torch.log1p(k * (acc_thick[mask_remain] / td_vals))
263
- + b * (acc_thick[mask_remain] / td_vals)
264
- )
265
- opac_vals = torch.clamp(opac_vals, 0.0, 1.0)
266
- comp[mask_remain] = comp[mask_remain] + (
267
- (remaining[mask_remain] * opac_vals).unsqueeze(-1) * col_vals
268
- )
269
- remaining[mask_remain] = remaining[mask_remain] * (1 - opac_vals)
270
- cur_mat[mask_remain] = -1
271
- acc_thick[mask_remain] = 0.0
273
+ mat_i = torch.argmax(p_i, dim=0).to(torch.int32)
274
+ new_mats_list.append(mat_i)
275
+ new_mats = torch.stack(new_mats_list, dim=0) # [max_layers]
276
+ else:
277
+ # Standard (random) Gumbel softmax
278
+ p_all = F.gumbel_softmax(global_logits, tau_global, hard=True, dim=1) # [L, M]
279
+ new_mats = torch.argmax(p_all, dim=1).to(torch.int32) # [max_layers]
272
280
 
281
+ H, W = pixel_height.shape
282
+ device = pixel_height.device
283
+
284
+ comp = torch.zeros(H, W, 3, dtype=torch.float32, device=device)
285
+ remaining = torch.ones(H, W, dtype=torch.float32, device=device)
286
+
287
+ # Current material index for each pixel, or -1 for none
288
+ cur_mat = -torch.ones((H, W), dtype=torch.int32, device=device)
289
+
290
+ # Accumulated thickness for the current segment
291
+ acc_thick = torch.zeros((H, W), dtype=torch.float32, device=device)
292
+
293
+ # Opacity function parameters
294
+ o = 0.10868816
295
+ A = 0.3077416
296
+ k = 76.928215
297
+ b = 2.2291653
298
+
299
+ # Main compositing loop: top to bottom
300
+ for layer_idx in range(max_layers - 1, -1, -1):
301
+ # layer_mat is the global material chosen for this layer (int32 scalar).
302
+ layer_mat = new_mats[layer_idx] # shape []
303
+
304
+ # Which pixels actually print on this layer?
305
+ # p_print = (discrete_layers > layer_idx)
306
+ p_print = discrete_layers.gt(layer_idx) # bool
307
+
308
+ # ---------------------------------------------------------------------
309
+ # (A) "Finish" any existing segments that are now 'done'.
310
+ #
311
+ # A segment is done if:
312
+ # 1) cur_mat != -1, i.e. the pixel had an ongoing segment
313
+ # 2) EITHER
314
+ # - the pixel does not print now (~p_print),
315
+ # - OR the new layer material differs (cur_mat != layer_mat).
316
+ # ---------------------------------------------------------------------
317
+ mask_done = (cur_mat.ne(-1)) & ((~p_print) | (cur_mat.ne(layer_mat)))
318
+
319
+ # Convert to float for multiplications
320
+ mask_done_f = mask_done.to(torch.float32)
321
+
322
+ # Gather thickness densities & colors for the old segment
323
+ # We'll clamp cur_mat so -1 becomes 0 (doesn't matter since we multiply by 0).
324
+ old_inds_clamped = torch.clamp(cur_mat, min=0)
325
+ td_vals = material_TDs[old_inds_clamped] # [H, W]
326
+ col_vals = material_colors[old_inds_clamped] # [H, W, 3]
327
+
328
+ # Compute alpha from accumulated thickness
329
+ thick_ratio = acc_thick / td_vals
330
+ opac_vals = o + (A * torch.log1p(k * thick_ratio) + b * thick_ratio)
331
+ opac_vals = torch.clamp(opac_vals, 0.0, 1.0) # [H, W]
332
+
333
+ # Compositing the old segment:
334
+ # comp += mask_done_f * remaining * opac_vals * col_vals
335
+ # remaining *= (1 - mask_done_f * opac_vals) ...
336
+ # but we have to broadcast for color:
337
+ comp_add = (mask_done_f * remaining * opac_vals).unsqueeze(-1) * col_vals
338
+ comp = comp + comp_add
339
+ remaining = remaining - (mask_done_f * remaining * opac_vals)
340
+
341
+ # Reset old segment where mask_done is True
342
+ # cur_mat = -1, acc_thick = 0
343
+ # We'll do it by `torch.where(mask, val_if_true, val_if_false)`
344
+ cur_mat = torch.where(mask_done, torch.full_like(cur_mat, -1), cur_mat)
345
+ acc_thick = torch.where(mask_done, torch.zeros_like(acc_thick), acc_thick)
346
+
347
+ # ---------------------------------------------------------------------
348
+ # (B) For pixels that print this layer:
349
+ # - Start a new segment if cur_mat == -1
350
+ # - Accumulate thickness if cur_mat == layer_mat
351
+ # ---------------------------------------------------------------------
352
+ eff_thick = p_print.to(torch.float32) * h
353
+
354
+ # (B1) Start new segment where cur_mat == -1
355
+ mask_new = p_print & (cur_mat.eq(-1))
356
+ mask_new_f = mask_new.to(torch.float32)
357
+
358
+ # Set cur_mat to layer_mat if mask_new is True
359
+ # (layer_mat is shape [], so it will broadcast)
360
+ cur_mat = torch.where(mask_new, layer_mat, cur_mat)
361
+
362
+ # We add thickness:
363
+ acc_thick = acc_thick + mask_new_f * eff_thick
364
+
365
+ # (B2) Accumulate thickness where cur_mat == layer_mat
366
+ # We do this in a second mask to avoid confusion, but you can combine.
367
+ mask_same = p_print & (cur_mat.eq(layer_mat))
368
+ acc_thick = acc_thick + (mask_same.to(torch.float32) * eff_thick)
369
+
370
+ # -------------------------------------------------------------------------
371
+ # 5) After the loop, composite any remaining segments (cur_mat != -1).
372
+ # -------------------------------------------------------------------------
373
+ mask_remain = cur_mat.ne(-1)
374
+ mask_remain_f = mask_remain.to(torch.float32)
375
+
376
+ old_inds_clamped = torch.clamp(cur_mat, min=0)
377
+ td_vals = material_TDs[old_inds_clamped]
378
+ col_vals = material_colors[old_inds_clamped]
379
+
380
+ thick_ratio = acc_thick / td_vals
381
+ opac_vals = o + (A * torch.log1p(k * thick_ratio) + b * thick_ratio)
382
+ opac_vals = torch.clamp(opac_vals, 0.0, 1.0)
383
+
384
+ comp_add = (mask_remain_f * remaining * opac_vals).unsqueeze(-1) * col_vals
385
+ comp = comp + comp_add
386
+ remaining = remaining - (mask_remain_f * remaining * opac_vals)
387
+
388
+ # -------------------------------------------------------------------------
389
+ # 6) Composite background
390
+ # -------------------------------------------------------------------------
273
391
  comp = comp + remaining.unsqueeze(-1) * background
274
392
  return comp * 255.0
275
393
 
276
394
 
277
395
  @torch.jit.script
278
- def adaptive_round(
279
- x: torch.Tensor, tau: float, high_tau: float, low_tau: float, temp: float
396
+ def composite_image_combined(
397
+ pixel_height_logits: torch.Tensor, # [H,W]
398
+ global_logits: torch.Tensor, # [max_layers, n_materials]
399
+ tau_height: float,
400
+ tau_global: float,
401
+ h: float,
402
+ max_layers: int,
403
+ material_colors: torch.Tensor, # [n_materials, 3]
404
+ material_TDs: torch.Tensor, # [n_materials]
405
+ background: torch.Tensor, # [3]
406
+ rng_seed: int = -1,
280
407
  ) -> torch.Tensor:
281
408
  """
282
- Smooth rounding based on temperature 'tau'.
409
+ Combine continuous and discrete compositing over all pixels.
410
+
411
+ Args:
412
+ pixel_height_logits (torch.Tensor): Logits for pixel heights, shape [H, W].
413
+ global_logits (torch.Tensor): Logits for global material assignment, shape [max_layers, n_materials].
414
+ tau_height (float): Temperature parameter for height rounding.
415
+ tau_global (float): Temperature parameter for global material assignment.
416
+ h (float): Height of each layer.
417
+ max_layers (int): Maximum number of layers.
418
+ material_colors (torch.Tensor): Tensor of material colors, shape [n_materials, 3].
419
+ material_TDs (torch.Tensor): Tensor of material transmission/opacity parameters, shape [n_materials].
420
+ background (torch.Tensor): Background color tensor, shape [3].
421
+ rng_seed (int, optional): Random seed for deterministic sampling. Defaults to -1.
422
+
423
+ Returns:
424
+ torch.Tensor: Composite image tensor, shape [H, W, 3].
283
425
  """
284
- if tau <= low_tau:
285
- return torch.round(x)
286
- elif tau >= high_tau:
287
- floor_val = torch.floor(x)
288
- diff = x - floor_val
289
- soft_round = floor_val + torch.sigmoid((diff - 0.5) / temp)
290
- return soft_round
426
+ cont = composite_image_cont(
427
+ pixel_height_logits,
428
+ global_logits,
429
+ tau_height,
430
+ tau_global,
431
+ h,
432
+ max_layers,
433
+ material_colors,
434
+ material_TDs,
435
+ background,
436
+ )
437
+ if tau_global < 1.0:
438
+ disc = composite_image_disc(
439
+ pixel_height_logits,
440
+ global_logits,
441
+ tau_height,
442
+ tau_global,
443
+ h,
444
+ max_layers,
445
+ material_colors,
446
+ material_TDs,
447
+ background,
448
+ rng_seed,
449
+ )
450
+ return cont * tau_global + disc * (1 - tau_global)
291
451
  else:
292
- ratio = (tau - low_tau) / (high_tau - low_tau)
293
- hard_round = torch.round(x)
294
- floor_val = torch.floor(x)
295
- diff = x - floor_val
296
- soft_round = floor_val + torch.sigmoid((diff - 0.5) / temp)
297
- return ratio * soft_round + (1 - ratio) * hard_round
452
+ return cont
298
453
 
299
454
 
300
455
  def discretize_solution(
@@ -302,6 +457,18 @@ def discretize_solution(
302
457
  ):
303
458
  """
304
459
  Convert continuous logs to discrete layer counts and discrete color IDs.
460
+
461
+ Args:
462
+ params (dict): Dictionary containing the parameters 'pixel_height_logits' and 'global_logits'.
463
+ tau_global (float): Temperature parameter for global material assignment.
464
+ h (float): Height of each layer.
465
+ max_layers (int): Maximum number of layers.
466
+ rng_seed (int, optional): Random seed for deterministic sampling. Defaults to -1.
467
+
468
+ Returns:
469
+ tuple: A tuple containing:
470
+ - torch.Tensor: Discrete global material assignments, shape [max_layers].
471
+ - torch.Tensor: Discrete height image, shape [H, W].
305
472
  """
306
473
  pixel_height_logits = params["pixel_height_logits"]
307
474
  global_logits = params["global_logits"]
@@ -328,10 +495,10 @@ def initialize_pixel_height_logits(target):
328
495
  Uses the formula: L = 0.299*R + 0.587*G + 0.114*B.
329
496
 
330
497
  Args:
331
- target (jnp.ndarray): The target image array with shape (H, W, 3).
498
+ target (np.ndarray): The target image array with shape (H, W, 3).
332
499
 
333
500
  Returns:
334
- jnp.ndarray: The initialized pixel height logits.
501
+ np.ndarray: The initialized pixel height logits.
335
502
  """
336
503
  # Compute normalized luminance in [0,1]
337
504
  normalized_lum = (
@@ -711,6 +878,10 @@ def init_height_map(target, max_layers, h, eps=1e-6, random_seed=None):
711
878
 
712
879
  Args:
713
880
  target (jnp.ndarray): The target image array with shape (H, W, 3).
881
+ max_layers (int): Maximum number of layers.
882
+ h (float): Height of each layer.
883
+ eps (float): Small constant to avoid division by zero.
884
+ random_seed (int): Random seed for reproducibility.
714
885
 
715
886
  Returns:
716
887
  jnp.ndarray: The initialized pixel height logits.
@@ -106,7 +106,6 @@ class FilamentOptimizer:
106
106
  self.optimizer = CAdamW(
107
107
  [self.params["pixel_height_logits"], self.params["global_logits"]],
108
108
  lr=self.learning_rate,
109
- weight_decay=1e-2,
110
109
  )
111
110
 
112
111
  # Setup best discrete solution tracking
@@ -20,7 +20,6 @@ from autoforge.Helper.OutputHelper import (
20
20
  generate_project_file,
21
21
  )
22
22
  from autoforge.Helper.PruningHelper import prune_redundant_layers
23
- from autoforge.Loss.PerceptionLoss import MultiLayerVGGPerceptualLoss
24
23
  from autoforge.Modules.Optimizer import FilamentOptimizer
25
24
 
26
25
 
@@ -282,7 +281,8 @@ def main():
282
281
  ) # shape (H', W') cause of weird opencv quirk
283
282
 
284
283
  # VGG Perceptual Loss
285
- perception_loss_module = MultiLayerVGGPerceptualLoss().to(device).eval()
284
+ # We currently disable this as it is not used in the optimization.
285
+ perception_loss_module = None # MultiLayerVGGPerceptualLoss().to(device).eval()
286
286
 
287
287
  # Create an optimizer instance
288
288
  optimizer = FilamentOptimizer(
@@ -300,7 +300,7 @@ def main():
300
300
  print("Starting optimization...")
301
301
  tbar = tqdm(range(args.iterations))
302
302
  for i in tbar:
303
- loss_val = optimizer.step(record_best=i % 3 == 0)
303
+ loss_val = optimizer.step(record_best=i % 10 == 0)
304
304
 
305
305
  optimizer.visualize(interval=25)
306
306
  optimizer.log_to_tensorboard(interval=100)
File without changes