autoforge 1.4.1__tar.gz → 1.4.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 (34) hide show
  1. {autoforge-1.4.1 → autoforge-1.4.2}/PKG-INFO +1 -1
  2. {autoforge-1.4.1 → autoforge-1.4.2}/pyproject.toml +1 -1
  3. {autoforge-1.4.1 → autoforge-1.4.2}/src/AutoForge.egg-info/PKG-INFO +1 -1
  4. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Helper/ImageHelper.py +8 -0
  5. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Helper/OutputHelper.py +93 -65
  6. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/auto_forge.py +48 -18
  7. {autoforge-1.4.1 → autoforge-1.4.2}/README.md +0 -0
  8. {autoforge-1.4.1 → autoforge-1.4.2}/setup.cfg +0 -0
  9. {autoforge-1.4.1 → autoforge-1.4.2}/src/AutoForge.egg-info/SOURCES.txt +0 -0
  10. {autoforge-1.4.1 → autoforge-1.4.2}/src/AutoForge.egg-info/dependency_links.txt +0 -0
  11. {autoforge-1.4.1 → autoforge-1.4.2}/src/AutoForge.egg-info/entry_points.txt +0 -0
  12. {autoforge-1.4.1 → autoforge-1.4.2}/src/AutoForge.egg-info/requires.txt +0 -0
  13. {autoforge-1.4.1 → autoforge-1.4.2}/src/AutoForge.egg-info/top_level.txt +0 -0
  14. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Gui/Views/__init__.py +0 -0
  15. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Gui/Views/backend_database.py +0 -0
  16. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Gui/Views/filament_editor.py +0 -0
  17. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Gui/__init__.py +0 -0
  18. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Gui/app.py +0 -0
  19. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Gui/main.py +0 -0
  20. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Helper/FilamentHelper.py +0 -0
  21. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Helper/OptimizerHelper.py +0 -0
  22. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Helper/PruningHelper.py +0 -0
  23. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Helper/__init__.py +0 -0
  24. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Helper/filamentcolors_library.py +0 -0
  25. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Loss/LossFunctions.py +0 -0
  26. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Loss/PerceptionLoss.py +0 -0
  27. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Loss/__init__.py +0 -0
  28. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Misc/__init__.py +0 -0
  29. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Misc/color_fit_all.py +0 -0
  30. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Modules/Optimizer.py +0 -0
  31. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/Modules/__init__.py +0 -0
  32. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/__init__.py +0 -0
  33. {autoforge-1.4.1 → autoforge-1.4.2}/src/autoforge/__main__.py +0 -0
  34. {autoforge-1.4.1 → autoforge-1.4.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.4.1
3
+ Version: 1.4.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "AutoForge"
7
- version="1.4.1"
7
+ version="1.4.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.4.1
3
+ Version: 1.4.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
@@ -23,6 +23,14 @@ def resize_image(img, max_size):
23
23
  return img_out
24
24
 
25
25
 
26
+ def resize_image_exact(img, new_w, new_h):
27
+ img_out = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_CUBIC)
28
+ # INTER_CUBIC is actually not a good choice for shrinking images. Except in our case.
29
+ # As we shrink the image, we lose information and the image becomes blurry with INTER_AREA. This is detrimental for the solver. If you want to try it out be my guest.
30
+ # INTER_AREA actually destroys a lot of the colors of our solver.
31
+ return img_out
32
+
33
+
26
34
  def srgb_to_lab(srgb, eps=1e-6):
27
35
  """
28
36
  Converts an sRGB image (values in [0, 255]) to the CIELAB color space.
@@ -186,37 +186,50 @@ def generate_project_file(
186
186
  json.dump(project_data, f, indent=4)
187
187
 
188
188
 
189
- def generate_stl(height_map, filename, background_height, maximum_x_y_size):
189
+ import numpy as np
190
+ import struct
191
+
192
+
193
+ def generate_stl(height_map, filename, background_height, maximum_x_y_size, alpha_mask=None):
190
194
  """
191
- Generate a binary STL file from a height map with shared boundary vertices
192
- to fix non-manifold edge warnings, scaling the x and y dimensions to a maximum
193
- size specified in millimeters.
195
+ Generate a binary STL file from a height map with an optional alpha mask.
196
+ If alpha_mask is provided, vertices where alpha < 128 are omitted.
197
+ This function builds a manifold mesh consisting of:
198
+ - a top surface (only quads whose four vertices are valid),
199
+ - side walls along the boundary edges of the top surface, and
200
+ - a bottom face covering the valid region.
194
201
 
195
202
  Args:
196
203
  height_map (np.ndarray): 2D array representing the height map.
197
204
  filename (str): The name of the output STL file.
198
205
  background_height (float): The height of the background in the STL model.
199
206
  maximum_x_y_size (float): Maximum size (in millimeters) for the x and y dimensions.
207
+ alpha_mask (np.ndarray): Optional alpha mask (same shape as height_map).
208
+ A pixel is “valid” only if its alpha is ≥ 128.
200
209
  """
201
210
  H, W = height_map.shape
202
211
 
203
- # Create the top vertices grid.
204
- # Note: y is computed as H-1-i.
212
+ # Compute valid mask: True if no alpha mask or alpha >= 128.
213
+ if alpha_mask is None:
214
+ valid_mask = np.ones((H, W), dtype=bool)
215
+ else:
216
+ valid_mask = (alpha_mask >= 128)
217
+
218
+ # --- Create vertices for the top surface ---
219
+ # We compute a grid of vertices (even for masked-out regions) but later only use those that are valid.
205
220
  top_vertices = np.zeros((H, W, 3), dtype=np.float32)
206
221
  for i in range(H):
207
222
  for j in range(W):
208
223
  top_vertices[i, j, 0] = j
209
- top_vertices[i, j, 1] = H - 1 - i # y coordinate: top row has highest value
224
+ # Note: y coordinate is set so that row 0 is at the top.
225
+ top_vertices[i, j, 1] = H - 1 - i
210
226
  top_vertices[i, j, 2] = height_map[i, j] + background_height
211
227
 
212
- # Create the bottom vertices for the boundary by copying x and y coordinates,
213
- # but setting z to 0.
228
+ # Create corresponding bottom vertices (same x,y but z = 0)
214
229
  bottom_vertices = top_vertices.copy()
215
230
  bottom_vertices[:, :, 2] = 0
216
231
 
217
232
  # --- Scale vertices so that the maximum x or y dimension equals maximum_x_y_size ---
218
- # The current extents in x and y are 0 to (W - 1) and 0 to (H - 1), respectively.
219
- # We use the maximum of these as our original size.
220
233
  original_max = max(W - 1, H - 1)
221
234
  scale = maximum_x_y_size / original_max
222
235
  top_vertices[:, :, 0] *= scale
@@ -224,81 +237,96 @@ def generate_stl(height_map, filename, background_height, maximum_x_y_size):
224
237
  bottom_vertices[:, :, 0] *= scale
225
238
  bottom_vertices[:, :, 1] *= scale
226
239
 
227
- triangles = []
240
+ triangles = [] # List to collect all triangles (each as a tuple of three vertices)
228
241
 
229
242
  def add_triangle(v1, v2, v3):
230
- """Add a triangle defined by vertices v1, v2, and v3."""
243
+ """Append a triangle (defined by 3 vertices) to the triangle list."""
231
244
  triangles.append((v1, v2, v3))
232
245
 
233
246
  # --- Top Surface ---
247
+ # For the top surface, we iterate over each cell (quad) in the grid.
248
+ # Only if all 4 corners of the quad are valid do we add two triangles.
249
+ # We also store the grid indices (i,j) of the vertices used so we can later extract the boundary edges.
250
+ top_triangles_indices = [] # Each element is a tuple of 3 (i,j) indices.
234
251
  for i in range(H - 1):
235
252
  for j in range(W - 1):
236
- v0 = top_vertices[i, j]
237
- v1 = top_vertices[i, j + 1]
238
- v2 = top_vertices[i + 1, j + 1]
239
- v3 = top_vertices[i + 1, j]
240
- # Use reversed order so that normals face upward.
241
- add_triangle(v2, v1, v0)
242
- add_triangle(v3, v2, v0)
253
+ if valid_mask[i, j] and valid_mask[i, j + 1] and valid_mask[i + 1, j + 1] and valid_mask[i + 1, j]:
254
+ # Define the four corners of the quad.
255
+ v0 = top_vertices[i, j]
256
+ v1 = top_vertices[i, j + 1]
257
+ v2 = top_vertices[i + 1, j + 1]
258
+ v3 = top_vertices[i + 1, j]
259
+ # For the top surface, use a reversed order so that the computed normal (via cross product) faces upward.
260
+ add_triangle(v2, v1, v0)
261
+ add_triangle(v3, v2, v0)
262
+ # Save the corresponding grid indices (for later boundary detection)
263
+ top_triangles_indices.append(((i, j), (i, j + 1), (i + 1, j + 1)))
264
+ top_triangles_indices.append(((i + 1, j), (i + 1, j + 1), (i, j)))
243
265
 
244
266
  # --- Side Walls ---
245
- # Top edge (i = 0)
246
- for j in range(W - 1):
247
- vt0 = top_vertices[0, j]
248
- vt1 = top_vertices[0, j + 1]
249
- vb0 = bottom_vertices[0, j]
250
- vb1 = bottom_vertices[0, j + 1]
251
- add_triangle(vt0, vt1, vb1)
252
- add_triangle(vt0, vb1, vb0)
253
-
254
- # Bottom edge (i = H - 1)
255
- for j in range(W - 1):
256
- vt0 = top_vertices[H - 1, j]
257
- vt1 = top_vertices[H - 1, j + 1]
258
- vb0 = bottom_vertices[H - 1, j]
259
- vb1 = bottom_vertices[H - 1, j + 1]
260
- add_triangle(vt1, vt0, vb0)
261
- add_triangle(vt1, vb0, vb1)
262
-
263
- # Left edge (j = 0)
264
- for i in range(H - 1):
265
- vt0 = top_vertices[i, 0]
266
- vt1 = top_vertices[i + 1, 0]
267
- vb0 = bottom_vertices[i, 0]
268
- vb1 = bottom_vertices[i + 1, 0]
269
- add_triangle(vt1, vt0, vb0)
270
- add_triangle(vt1, vb0, vb1)
271
-
272
- # Right edge (j = W - 1)
273
- for i in range(H - 1):
274
- vt0 = top_vertices[i, W - 1]
275
- vt1 = top_vertices[i + 1, W - 1]
276
- vb0 = bottom_vertices[i, W - 1]
277
- vb1 = bottom_vertices[i + 1, W - 1]
278
- add_triangle(vt0, vt1, vb1)
279
- add_triangle(vt0, vb1, vb0)
267
+ # Instead of simply using the rectangular border, we compute the boundary of the top surface.
268
+ # We count each undirected edge from the top surface triangles and select those that appear only once.
269
+ edge_count = {}
270
+
271
+ def add_edge(idx1, idx2):
272
+ # Store an undirected edge as a sorted tuple of the grid indices.
273
+ key = tuple(sorted((idx1, idx2)))
274
+ edge_count[key] = edge_count.get(key, 0) + 1
275
+
276
+ for tri in top_triangles_indices:
277
+ add_edge(tri[0], tri[1])
278
+ add_edge(tri[1], tri[2])
279
+ add_edge(tri[2], tri[0])
280
+
281
+ # A boundary edge appears only once.
282
+ boundary_edges = [edge for edge, count in edge_count.items() if count == 1]
283
+
284
+ # Helper functions to get the coordinate for a given grid index.
285
+ def get_top_vertex(idx):
286
+ i, j = idx
287
+ return top_vertices[i, j]
288
+
289
+ def get_bottom_vertex(idx):
290
+ i, j = idx
291
+ return bottom_vertices[i, j]
292
+
293
+ # For each boundary edge, create a vertical wall.
294
+ for edge in boundary_edges:
295
+ idx1, idx2 = edge
296
+ v_top1 = get_top_vertex(idx1)
297
+ v_top2 = get_top_vertex(idx2)
298
+ v_bottom1 = get_bottom_vertex(idx1)
299
+ v_bottom2 = get_bottom_vertex(idx2)
300
+ # Create a quad (two triangles) connecting the top edge to the bottom.
301
+ add_triangle(v_top1, v_top2, v_bottom2)
302
+ add_triangle(v_top1, v_bottom2, v_bottom1)
280
303
 
281
304
  # --- Bottom Face ---
282
- # Use the four corner vertices from the bottom_vertices grid.
283
- b_top_left = bottom_vertices[0, 0]
284
- b_top_right = bottom_vertices[0, W - 1]
285
- b_bottom_right = bottom_vertices[H - 1, W - 1]
286
- b_bottom_left = bottom_vertices[H - 1, 0]
287
- # Triangulate the bottom face (ensure the normal faces downward).
288
- add_triangle(b_bottom_right, b_top_right, b_top_left)
289
- add_triangle(b_bottom_left, b_bottom_right, b_top_left)
305
+ # For the bottom face, we mimic the top-surface logic but using the bottom vertices.
306
+ # The ordering is reversed (relative to the top face) so that the normals face downward.
307
+ for i in range(H - 1):
308
+ for j in range(W - 1):
309
+ if valid_mask[i, j] and valid_mask[i, j + 1] and valid_mask[i + 1, j + 1] and valid_mask[i + 1, j]:
310
+ v0 = bottom_vertices[i, j]
311
+ v1 = bottom_vertices[i, j + 1]
312
+ v2 = bottom_vertices[i + 1, j + 1]
313
+ v3 = bottom_vertices[i + 1, j]
314
+ # Order so that the normal will point downward.
315
+ add_triangle(v0, v1, v2)
316
+ add_triangle(v0, v2, v3)
290
317
 
291
318
  num_triangles = len(triangles)
292
319
 
293
- # --- Write Binary STL ---
320
+ # --- Write Binary STL File ---
294
321
  with open(filename, "wb") as f:
295
- header_str = "Binary STL generated from heightmap"
322
+ header_str = "Binary STL generated from heightmap with alpha mask"
296
323
  header = header_str.encode("utf-8")
297
324
  header = header.ljust(80, b" ")
298
325
  f.write(header)
299
326
  f.write(struct.pack("<I", num_triangles))
300
327
  for tri in triangles:
301
328
  v1, v2, v3 = tri
329
+ # Compute the normal as the normalized cross product of (v2-v1) and (v3-v1).
302
330
  normal = np.cross(v2 - v1, v3 - v1)
303
331
  norm = np.linalg.norm(normal)
304
332
  if norm == 0:
@@ -7,7 +7,7 @@ import numpy as np
7
7
  from tqdm import tqdm
8
8
 
9
9
  from autoforge.Helper.FilamentHelper import hex_to_rgb, load_materials
10
- from autoforge.Helper.ImageHelper import resize_image
10
+ from autoforge.Helper.ImageHelper import resize_image, resize_image_exact
11
11
  from autoforge.Helper.OptimizerHelper import (
12
12
  init_height_map,
13
13
  composite_image,
@@ -86,10 +86,16 @@ def main():
86
86
  help="Maximum dimension for solver (fast) image",
87
87
  )
88
88
  parser.add_argument(
89
- "--init_tau", type=float, default=1.0, help="Initial tau value for Gumbel-Softmax"
89
+ "--init_tau",
90
+ type=float,
91
+ default=1.0,
92
+ help="Initial tau value for Gumbel-Softmax",
90
93
  )
91
94
  parser.add_argument(
92
- "--final_tau", type=float, default=0.01, help="Final tau value for Gumbel-Softmax"
95
+ "--final_tau",
96
+ type=float,
97
+ default=0.01,
98
+ help="Final tau value for Gumbel-Softmax",
93
99
  )
94
100
 
95
101
  parser.add_argument(
@@ -178,14 +184,10 @@ def main():
178
184
  help="Use the Metal Performance Shaders (MPS) backend, if available.",
179
185
  )
180
186
  parser.add_argument(
181
- "--run-name",
182
- type=str,
183
- help="Name of the run used for TensorBoard logging"
187
+ "--run-name", type=str, help="Name of the run used for TensorBoard logging"
184
188
  )
185
189
  parser.add_argument(
186
- "--tensorboard",
187
- action="store_true",
188
- help="Enable TensorBoard logging"
190
+ "--tensorboard", action="store_true", help="Enable TensorBoard logging"
189
191
  )
190
192
 
191
193
  args = parser.parse_args()
@@ -225,7 +227,18 @@ def main():
225
227
  material_TDs = torch.tensor(material_TDs_np, dtype=torch.float32, device=device)
226
228
 
227
229
  # Read input image
228
- img = cv2.imread(args.input_image)
230
+ img = cv2.imread(args.input_image, cv2.IMREAD_UNCHANGED)
231
+
232
+ alpha = None
233
+ # check for alpha mask
234
+ if img.shape[2] == 4:
235
+ # Extract the alpha channel
236
+ alpha = img[:, :, 3]
237
+ alpha = alpha[..., None]
238
+ alpha = resize_image(alpha, args.output_size)
239
+ # Convert the image from BGRA to BGR
240
+ img = img[:, :, :3]
241
+
229
242
  img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
230
243
 
231
244
  # This smaller 'target' is the solver resolution
@@ -254,11 +267,15 @@ def main():
254
267
  output_img_np, args.max_layers, args.layer_height, random_seed=random_seed
255
268
  )
256
269
 
270
+ # if we have an alpha mask we set the height for thos pixels to -13.815512 (the lowest init sigmoid value)
271
+ if alpha is not None:
272
+ pixel_height_logits_init[alpha < 128] = -13.815512
273
+
257
274
  # But we will solve at the smaller resolution => resize that init to solver size
258
275
  # Add a dummy channel so that cv2.resize can handle it
259
276
  tmp_3d = pixel_height_logits_init[..., None] # shape (H, W, 1)
260
- phl_solver_np = resize_image(
261
- tmp_3d, args.solver_size
277
+ phl_solver_np = resize_image_exact(
278
+ tmp_3d, target_solver.shape[1], target_solver.shape[0]
262
279
  ) # shape (H', W') cause of weird opencv quirk
263
280
 
264
281
  # VGG Perceptual Loss
@@ -305,7 +322,9 @@ def main():
305
322
  optimizer.best_discrete_loss = new_loss
306
323
  print(f"New best seed found: {new_rng_seed} with loss: {new_loss}")
307
324
 
308
- optimizer.log_to_tensorboard(interval=1, namespace="post_opt", step=(post_opt_step := post_opt_step + 1))
325
+ optimizer.log_to_tensorboard(
326
+ interval=1, namespace="post_opt", step=(post_opt_step := post_opt_step + 1)
327
+ )
309
328
 
310
329
  # Optionally prune
311
330
  if args.perform_pruning:
@@ -317,14 +336,18 @@ def main():
317
336
  tau_g=optimizer.best_tau,
318
337
  )
319
338
 
320
- optimizer.log_to_tensorboard(interval=1, namespace="post_opt", step=(post_opt_step := post_opt_step + 1))
339
+ optimizer.log_to_tensorboard(
340
+ interval=1, namespace="post_opt", step=(post_opt_step := post_opt_step + 1)
341
+ )
321
342
 
322
343
  # Do a quick search for a better rng seed
323
344
  new_rng_seed, new_loss = optimizer.rng_seed_search(
324
345
  optimizer.best_discrete_loss, 500
325
346
  )
326
347
 
327
- optimizer.log_to_tensorboard(interval=1, namespace="post_opt", step=(post_opt_step := post_opt_step + 1))
348
+ optimizer.log_to_tensorboard(
349
+ interval=1, namespace="post_opt", step=(post_opt_step := post_opt_step + 1)
350
+ )
328
351
 
329
352
  if new_loss < optimizer.best_discrete_loss:
330
353
  optimizer.best_seed = new_rng_seed
@@ -378,8 +401,9 @@ def main():
378
401
  rng_seed=optimizer.best_seed,
379
402
  )
380
403
 
381
- optimizer.log_to_tensorboard(interval=1, namespace="post_opt", step=(post_opt_step := post_opt_step + 1))
382
-
404
+ optimizer.log_to_tensorboard(
405
+ interval=1, namespace="post_opt", step=(post_opt_step := post_opt_step + 1)
406
+ )
383
407
 
384
408
  comp_disc_np = comp_disc.cpu().numpy().astype(np.uint8)
385
409
  comp_disc_np = cv2.cvtColor(comp_disc_np, cv2.COLOR_RGB2BGR)
@@ -389,7 +413,13 @@ def main():
389
413
  height_map_mm = (
390
414
  disc_height_image.cpu().numpy().astype(np.float32)
391
415
  ) * args.layer_height
392
- generate_stl(height_map_mm, stl_filename, args.background_height, maximum_x_y_size=args.stl_output_size)
416
+ generate_stl(
417
+ height_map_mm,
418
+ stl_filename,
419
+ args.background_height,
420
+ maximum_x_y_size=args.stl_output_size,
421
+ alpha_mask=alpha,
422
+ )
393
423
 
394
424
  # Swap instructions
395
425
  background_layers = int(args.background_height // args.layer_height)
File without changes
File without changes