autoforge 2.0.0__tar.gz → 2.0.1__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-2.0.0 → autoforge-2.0.1}/PKG-INFO +6 -1
- {autoforge-2.0.0 → autoforge-2.0.1}/README.md +5 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/pyproject.toml +4 -1
- {autoforge-2.0.0 → autoforge-2.0.1}/src/AutoForge.egg-info/PKG-INFO +6 -1
- {autoforge-2.0.0 → autoforge-2.0.1}/src/AutoForge.egg-info/SOURCES.txt +4 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/ImageHelper.py +29 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/OtherHelper.py +4 -2
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/OutputHelper.py +128 -145
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/PruningHelper.py +45 -28
- autoforge-2.0.1/src/autoforge/Helper/filamentcolors_library.py +345 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/auto_forge.py +38 -16
- autoforge-2.0.1/src/autoforge/data/filamentcolors_catalog.json +10216 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/filaments.py +49 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/init.py +13 -3
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/optimization.py +8 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/preview.py +8 -2
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/pruning.py +60 -4
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/config.py +6 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/pipeline_runner.py +8 -34
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/slider_render.py +10 -1
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/server.py +7 -0
- autoforge-2.0.1/src/autoforge/webui/services/catalog_service.py +150 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/services/filament_service.py +60 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/services/optimization_service.py +15 -0
- autoforge-2.0.1/tests/test_bugfixes_2026_09_26.py +413 -0
- autoforge-2.0.1/tests/test_filamentcolors_library.py +345 -0
- autoforge-2.0.0/src/autoforge/Helper/filamentcolors_library.py +0 -164
- {autoforge-2.0.0 → autoforge-2.0.1}/setup.cfg +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/AutoForge.egg-info/dependency_links.txt +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/AutoForge.egg-info/entry_points.txt +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/AutoForge.egg-info/requires.txt +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/AutoForge.egg-info/top_level.txt +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/AmpUtils.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/CAdamW.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/DeviceUtils.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/FilamentHelper.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/Heightmaps/DepthEstimateHeightMap.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/Heightmaps/FastTSPHeightMap.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/Heightmaps/__init__.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/OptimizerHelper.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/__init__.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Init_comparer/__init__.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Init_comparer/auto_forge_init_check.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Init_comparer/plot_results.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Loss/LossFunctions.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Loss/PerceptionLoss.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Loss/__init__.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Misc/__init__.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Misc/color_fit_all.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Modules/Optimizer.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Modules/__init__.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/__init__.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/__main__.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/priority_mask_editor.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/__init__.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/__init__.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/images.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/outputs.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/project.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/settings.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/sliders.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/state.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/system.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/ws.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/__init__.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/colored_mesh.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/gpu_memory.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/mesh_persist.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/sliders.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/telemetry.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/models.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/services/__init__.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/services/image_service.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/services/project_service.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_amp_utils.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_auto_forge_cli.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_bug_regressions.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_cadamw.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_christofides_heightmap.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_composite_image.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_depth_adjusted_height_map.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_depth_estimate_height_map.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_device_utils.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_end_to_end_optimizer.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_fasttsp_heightmap.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_filament_helper.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_flatforge.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_image_helper.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_loss_functions.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_optimizer_module.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_optimizer_training.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_other_helper.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_output_helper.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_perception_loss.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_pipeline_smoke.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_property_based.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_prune_repeatability.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_pruning_helper.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_remove_height_spikes.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_webui_backend.py +0 -0
- {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_webui_regressions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AutoForge
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: AutoForge - 3D color images made easy
|
|
5
5
|
Author-email: Hendric Voss <hvoss@techfak.uni-bielefeld.de>
|
|
6
6
|
License: CC BY-NC-SA 4.0
|
|
@@ -98,6 +98,8 @@ The easiest way to use AutoForge is the web UI, a local app with drag-and-drop i
|
|
|
98
98
|
|
|
99
99
|
If HueForge is installed on the same computer, the web UI finds your HueForge personal filament library and offers to import it once, on its first start.
|
|
100
100
|
|
|
101
|
+
To find a filament you own without measuring it yourself, click **Catalog** in the filament library. It searches every filament on [filamentcolors.xyz](https://filamentcolors.xyz) that has a measured TD (search by brand, color name, type or hex code; filter by type, brand or color family; or pick a color to see the closest matches first), and **Add** puts it straight into your library.
|
|
102
|
+
|
|
101
103
|
The web UI sends anonymous usage telemetry to the project via [PostHog](https://posthog.com/) by default, to notify me of problems and any bugs. This includes crash reports (unhandled errors from both the browser frontend and the backend server, with the error type, message, and stack trace) so bugs can get fixed faster. No image data, filament data, or personal information is sent. To disable it, pass `--no-telemetry` (e.g. `./run_webui.sh --no-telemetry` / `run_webui.bat --no-telemetry`), or set `AUTOFORGE_WEBUI_TELEMETRY_ENABLED=false` permanently in your environment.
|
|
102
104
|
|
|
103
105
|
4. **Update to the latest release** whenever you want, from the project folder:
|
|
@@ -248,6 +250,8 @@ conda activate forge
|
|
|
248
250
|
pip install -e .
|
|
249
251
|
```
|
|
250
252
|
|
|
253
|
+
To refresh the bundled filamentcolors.xyz catalog (`src/autoforge/data/filamentcolors_catalog.json`) before a release, run `./generate_filamentcolors_library.sh`. It downloads the whole catalog once, pausing 2 seconds between pages (`--delay` sets the pause; requests are never less than 1 second apart), and keeps only the filaments that have a TD.
|
|
254
|
+
|
|
251
255
|
If the installed pytorch version has no cuda support execute the following:
|
|
252
256
|
|
|
253
257
|
```bash
|
|
@@ -276,6 +280,7 @@ I would love to see what you have done with the software, so it would be great i
|
|
|
276
280
|
First and foremost:
|
|
277
281
|
- [Hueforge](https://shop.thehueforge.com/) for providing the inspiration for this project.
|
|
278
282
|
Without it, this project would not have been possible.
|
|
283
|
+
- [filamentcolors.xyz](https://filamentcolors.xyz) and its community for the measured filament swatches and TD values in the web UI's filament catalog.
|
|
279
284
|
|
|
280
285
|
AutoForge makes use of several open source libraries:
|
|
281
286
|
|
|
@@ -61,6 +61,8 @@ The easiest way to use AutoForge is the web UI, a local app with drag-and-drop i
|
|
|
61
61
|
|
|
62
62
|
If HueForge is installed on the same computer, the web UI finds your HueForge personal filament library and offers to import it once, on its first start.
|
|
63
63
|
|
|
64
|
+
To find a filament you own without measuring it yourself, click **Catalog** in the filament library. It searches every filament on [filamentcolors.xyz](https://filamentcolors.xyz) that has a measured TD (search by brand, color name, type or hex code; filter by type, brand or color family; or pick a color to see the closest matches first), and **Add** puts it straight into your library.
|
|
65
|
+
|
|
64
66
|
The web UI sends anonymous usage telemetry to the project via [PostHog](https://posthog.com/) by default, to notify me of problems and any bugs. This includes crash reports (unhandled errors from both the browser frontend and the backend server, with the error type, message, and stack trace) so bugs can get fixed faster. No image data, filament data, or personal information is sent. To disable it, pass `--no-telemetry` (e.g. `./run_webui.sh --no-telemetry` / `run_webui.bat --no-telemetry`), or set `AUTOFORGE_WEBUI_TELEMETRY_ENABLED=false` permanently in your environment.
|
|
65
67
|
|
|
66
68
|
4. **Update to the latest release** whenever you want, from the project folder:
|
|
@@ -211,6 +213,8 @@ conda activate forge
|
|
|
211
213
|
pip install -e .
|
|
212
214
|
```
|
|
213
215
|
|
|
216
|
+
To refresh the bundled filamentcolors.xyz catalog (`src/autoforge/data/filamentcolors_catalog.json`) before a release, run `./generate_filamentcolors_library.sh`. It downloads the whole catalog once, pausing 2 seconds between pages (`--delay` sets the pause; requests are never less than 1 second apart), and keeps only the filaments that have a TD.
|
|
217
|
+
|
|
214
218
|
If the installed pytorch version has no cuda support execute the following:
|
|
215
219
|
|
|
216
220
|
```bash
|
|
@@ -239,6 +243,7 @@ I would love to see what you have done with the software, so it would be great i
|
|
|
239
243
|
First and foremost:
|
|
240
244
|
- [Hueforge](https://shop.thehueforge.com/) for providing the inspiration for this project.
|
|
241
245
|
Without it, this project would not have been possible.
|
|
246
|
+
- [filamentcolors.xyz](https://filamentcolors.xyz) and its community for the measured filament swatches and TD values in the web UI's filament catalog.
|
|
242
247
|
|
|
243
248
|
AutoForge makes use of several open source libraries:
|
|
244
249
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "AutoForge"
|
|
7
|
-
version="2.0.
|
|
7
|
+
version="2.0.1"
|
|
8
8
|
description = "AutoForge - 3D color images made easy"
|
|
9
9
|
authors = [{ name = "Hendric Voss", email = "hvoss@techfak.uni-bielefeld.de" }]
|
|
10
10
|
readme = "README.md"
|
|
@@ -50,6 +50,9 @@ test = [
|
|
|
50
50
|
[tool.setuptools]
|
|
51
51
|
package-dir = {"" = "src"}
|
|
52
52
|
|
|
53
|
+
[tool.setuptools.package-data]
|
|
54
|
+
autoforge = ["data/*.json"]
|
|
55
|
+
|
|
53
56
|
[project.scripts]
|
|
54
57
|
autoforge = "autoforge.auto_forge:main"
|
|
55
58
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: AutoForge
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: AutoForge - 3D color images made easy
|
|
5
5
|
Author-email: Hendric Voss <hvoss@techfak.uni-bielefeld.de>
|
|
6
6
|
License: CC BY-NC-SA 4.0
|
|
@@ -98,6 +98,8 @@ The easiest way to use AutoForge is the web UI, a local app with drag-and-drop i
|
|
|
98
98
|
|
|
99
99
|
If HueForge is installed on the same computer, the web UI finds your HueForge personal filament library and offers to import it once, on its first start.
|
|
100
100
|
|
|
101
|
+
To find a filament you own without measuring it yourself, click **Catalog** in the filament library. It searches every filament on [filamentcolors.xyz](https://filamentcolors.xyz) that has a measured TD (search by brand, color name, type or hex code; filter by type, brand or color family; or pick a color to see the closest matches first), and **Add** puts it straight into your library.
|
|
102
|
+
|
|
101
103
|
The web UI sends anonymous usage telemetry to the project via [PostHog](https://posthog.com/) by default, to notify me of problems and any bugs. This includes crash reports (unhandled errors from both the browser frontend and the backend server, with the error type, message, and stack trace) so bugs can get fixed faster. No image data, filament data, or personal information is sent. To disable it, pass `--no-telemetry` (e.g. `./run_webui.sh --no-telemetry` / `run_webui.bat --no-telemetry`), or set `AUTOFORGE_WEBUI_TELEMETRY_ENABLED=false` permanently in your environment.
|
|
102
104
|
|
|
103
105
|
4. **Update to the latest release** whenever you want, from the project folder:
|
|
@@ -248,6 +250,8 @@ conda activate forge
|
|
|
248
250
|
pip install -e .
|
|
249
251
|
```
|
|
250
252
|
|
|
253
|
+
To refresh the bundled filamentcolors.xyz catalog (`src/autoforge/data/filamentcolors_catalog.json`) before a release, run `./generate_filamentcolors_library.sh`. It downloads the whole catalog once, pausing 2 seconds between pages (`--delay` sets the pause; requests are never less than 1 second apart), and keeps only the filaments that have a TD.
|
|
254
|
+
|
|
251
255
|
If the installed pytorch version has no cuda support execute the following:
|
|
252
256
|
|
|
253
257
|
```bash
|
|
@@ -276,6 +280,7 @@ I would love to see what you have done with the software, so it would be great i
|
|
|
276
280
|
First and foremost:
|
|
277
281
|
- [Hueforge](https://shop.thehueforge.com/) for providing the inspiration for this project.
|
|
278
282
|
Without it, this project would not have been possible.
|
|
283
|
+
- [filamentcolors.xyz](https://filamentcolors.xyz) and its community for the measured filament swatches and TD values in the web UI's filament catalog.
|
|
279
284
|
|
|
280
285
|
AutoForge makes use of several open source libraries:
|
|
281
286
|
|
|
@@ -35,6 +35,7 @@ src/autoforge/Misc/__init__.py
|
|
|
35
35
|
src/autoforge/Misc/color_fit_all.py
|
|
36
36
|
src/autoforge/Modules/Optimizer.py
|
|
37
37
|
src/autoforge/Modules/__init__.py
|
|
38
|
+
src/autoforge/data/filamentcolors_catalog.json
|
|
38
39
|
src/autoforge/webui/__init__.py
|
|
39
40
|
src/autoforge/webui/config.py
|
|
40
41
|
src/autoforge/webui/models.py
|
|
@@ -62,6 +63,7 @@ src/autoforge/webui/helpers/slider_render.py
|
|
|
62
63
|
src/autoforge/webui/helpers/sliders.py
|
|
63
64
|
src/autoforge/webui/helpers/telemetry.py
|
|
64
65
|
src/autoforge/webui/services/__init__.py
|
|
66
|
+
src/autoforge/webui/services/catalog_service.py
|
|
65
67
|
src/autoforge/webui/services/filament_service.py
|
|
66
68
|
src/autoforge/webui/services/image_service.py
|
|
67
69
|
src/autoforge/webui/services/optimization_service.py
|
|
@@ -69,6 +71,7 @@ src/autoforge/webui/services/project_service.py
|
|
|
69
71
|
tests/test_amp_utils.py
|
|
70
72
|
tests/test_auto_forge_cli.py
|
|
71
73
|
tests/test_bug_regressions.py
|
|
74
|
+
tests/test_bugfixes_2026_09_26.py
|
|
72
75
|
tests/test_cadamw.py
|
|
73
76
|
tests/test_christofides_heightmap.py
|
|
74
77
|
tests/test_composite_image.py
|
|
@@ -78,6 +81,7 @@ tests/test_device_utils.py
|
|
|
78
81
|
tests/test_end_to_end_optimizer.py
|
|
79
82
|
tests/test_fasttsp_heightmap.py
|
|
80
83
|
tests/test_filament_helper.py
|
|
84
|
+
tests/test_filamentcolors_library.py
|
|
81
85
|
tests/test_flatforge.py
|
|
82
86
|
tests/test_image_helper.py
|
|
83
87
|
tests/test_loss_functions.py
|
|
@@ -18,6 +18,35 @@ def imwrite(filename: str, img: MatLike, params: Sequence[int] = ()) -> None:
|
|
|
18
18
|
encoded_img.tofile(filename)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def to_bgr_or_bgra_uint8(img) -> np.ndarray:
|
|
22
|
+
"""Bring any image OpenCV decodes into the 8-bit BGR/BGRA layout the
|
|
23
|
+
pipeline is written for (used by both the CLI and the webui).
|
|
24
|
+
|
|
25
|
+
``cv2.imread(..., IMREAD_UNCHANGED)`` returns whatever the file holds, but
|
|
26
|
+
the pipeline indexed ``img.shape[2]`` and treated values as 0-255: a
|
|
27
|
+
grayscale PNG/JPEG (2-D array) failed with "tuple index out of range",
|
|
28
|
+
grayscale+alpha had two channels and failed in ``cvtColor``, and a
|
|
29
|
+
16-bit PNG ran on values up to 65535 — a nonsense target the optimizer
|
|
30
|
+
could never match."""
|
|
31
|
+
if img is None:
|
|
32
|
+
raise ValueError("The input image could not be read.")
|
|
33
|
+
if img.dtype == np.uint16:
|
|
34
|
+
img = (img.astype(np.float32) / 257.0).round().astype(np.uint8)
|
|
35
|
+
elif img.dtype in (np.float32, np.float64):
|
|
36
|
+
img = (np.clip(img, 0.0, 1.0) * 255.0).round().astype(np.uint8)
|
|
37
|
+
elif img.dtype != np.uint8:
|
|
38
|
+
img = np.clip(img, 0, 255).astype(np.uint8)
|
|
39
|
+
if img.ndim == 2:
|
|
40
|
+
return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
|
|
41
|
+
channels = img.shape[2]
|
|
42
|
+
if channels == 1:
|
|
43
|
+
return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
|
|
44
|
+
if channels == 2:
|
|
45
|
+
gray, alpha = img[:, :, 0], img[:, :, 1:2]
|
|
46
|
+
return np.concatenate([cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR), alpha], axis=2)
|
|
47
|
+
return img
|
|
48
|
+
|
|
49
|
+
|
|
21
50
|
def resize_image(img, max_size):
|
|
22
51
|
h_img, w_img, _ = img.shape
|
|
23
52
|
|
|
@@ -23,8 +23,10 @@ def set_seed(args) -> Any:
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def perform_basic_check(args):
|
|
26
|
-
# Basic checks
|
|
27
|
-
|
|
26
|
+
# Basic checks. With a tolerance: in floating point 0.28 / 0.04 is
|
|
27
|
+
# 7.000000000000001, so an exact is_integer() rejected valid settings.
|
|
28
|
+
ratio = args.background_height / args.layer_height
|
|
29
|
+
if abs(ratio - round(ratio)) > 1e-6:
|
|
28
30
|
print(
|
|
29
31
|
"Error: Background height must be a multiple of layer height.",
|
|
30
32
|
file=sys.stderr,
|
|
@@ -48,6 +48,56 @@ def extract_filament_swaps(disc_global, disc_height_image, background_layers):
|
|
|
48
48
|
return filament_indices, slider_values
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def _is_blank(value) -> bool:
|
|
52
|
+
"""Missing, None, or the NaN pandas reads an empty CSV cell as."""
|
|
53
|
+
if value is None:
|
|
54
|
+
return True
|
|
55
|
+
if isinstance(value, float) and np.isnan(value):
|
|
56
|
+
return True
|
|
57
|
+
return isinstance(value, str) and value.strip() == ""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _hfp_filament(mat, brand="", color="#000000", name=""):
|
|
61
|
+
"""One ``filament_set`` entry of a .hfp file from a material record.
|
|
62
|
+
|
|
63
|
+
The records come from pandas, which reads an empty CSV cell as NaN -
|
|
64
|
+
and ``json.dump`` writes that as a bare ``NaN``, which is not JSON, so
|
|
65
|
+
HueForge (or any strict parser) could not open the project. The webui
|
|
66
|
+
hits this for every filament without a brand. Blank fields get the
|
|
67
|
+
given defaults (a fresh uuid for ``uuid``) instead."""
|
|
68
|
+
|
|
69
|
+
def text(key, default):
|
|
70
|
+
value = mat.get(key)
|
|
71
|
+
return default if _is_blank(value) else str(value)
|
|
72
|
+
|
|
73
|
+
td_raw = mat.get("Transmissivity")
|
|
74
|
+
try:
|
|
75
|
+
td = float(td_raw)
|
|
76
|
+
except (TypeError, ValueError):
|
|
77
|
+
td = float("nan")
|
|
78
|
+
if not np.isfinite(td):
|
|
79
|
+
td = 0.1
|
|
80
|
+
return {
|
|
81
|
+
"Brand": text("Brand", brand),
|
|
82
|
+
"Color": text("Color", color),
|
|
83
|
+
"Name": text("Name", name),
|
|
84
|
+
"Owned": str(mat.get("Owned", False)).strip().lower() == "true",
|
|
85
|
+
"Transmissivity": int(td) if td.is_integer() else td,
|
|
86
|
+
"Type": text("Type", "PLA"),
|
|
87
|
+
"uuid": text("Uuid", str(uuid.uuid4())),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def model_size_mm(width_px, height_px, maximum_x_y_size):
|
|
92
|
+
"""Physical (width, height) in mm of a model built by ``generate_stl``
|
|
93
|
+
from a ``width_px`` x ``height_px`` height map: its longer side spans
|
|
94
|
+
``maximum_x_y_size`` mm across the pixel centres (``max(W, H) - 1``
|
|
95
|
+
steps)."""
|
|
96
|
+
steps = max(int(width_px) - 1, int(height_px) - 1, 1)
|
|
97
|
+
scale = float(maximum_x_y_size) / steps
|
|
98
|
+
return (max(int(width_px) - 1, 0) * scale, max(int(height_px) - 1, 0) * scale)
|
|
99
|
+
|
|
100
|
+
|
|
51
101
|
def generate_project_file(
|
|
52
102
|
project_filename,
|
|
53
103
|
args,
|
|
@@ -78,7 +128,7 @@ def generate_project_file(
|
|
|
78
128
|
csv_filename (str): Path to the CSV file containing material data.
|
|
79
129
|
"""
|
|
80
130
|
# Compute the number of background layers (as in your main())
|
|
81
|
-
background_layers = int(args.background_height / args.layer_height)
|
|
131
|
+
background_layers = int(round(args.background_height / args.layer_height))
|
|
82
132
|
|
|
83
133
|
# Load full material data from CSV
|
|
84
134
|
material_data = load_materials_data(args)
|
|
@@ -95,21 +145,13 @@ def generate_project_file(
|
|
|
95
145
|
# Use actual background filament data if auto selected and index stored
|
|
96
146
|
bg_idx = getattr(args, "background_material_index", None)
|
|
97
147
|
if bg_idx is not None and 0 <= bg_idx < len(material_data):
|
|
98
|
-
bg_mat = material_data[bg_idx]
|
|
99
148
|
filament_set.append(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
"
|
|
103
|
-
|
|
104
|
-
"
|
|
105
|
-
|
|
106
|
-
int(bg_mat["Transmissivity"])
|
|
107
|
-
if float(bg_mat["Transmissivity"]).is_integer()
|
|
108
|
-
else float(bg_mat["Transmissivity"])
|
|
109
|
-
),
|
|
110
|
-
"Type": bg_mat.get("Type", "PLA"),
|
|
111
|
-
"uuid": bg_mat.get("Uuid", str(uuid.uuid4())),
|
|
112
|
-
}
|
|
149
|
+
_hfp_filament(
|
|
150
|
+
material_data[bg_idx],
|
|
151
|
+
brand="Autoforge",
|
|
152
|
+
color=args.background_color,
|
|
153
|
+
name="Background",
|
|
154
|
+
)
|
|
113
155
|
)
|
|
114
156
|
else:
|
|
115
157
|
filament_set.append(
|
|
@@ -125,22 +167,7 @@ def generate_project_file(
|
|
|
125
167
|
)
|
|
126
168
|
|
|
127
169
|
for idx in filament_indices:
|
|
128
|
-
|
|
129
|
-
filament_set.append(
|
|
130
|
-
{
|
|
131
|
-
"Brand": mat["Brand"],
|
|
132
|
-
"Color": mat["Color"],
|
|
133
|
-
"Name": mat["Name"],
|
|
134
|
-
"Owned": str(mat.get("Owned", False)).strip().lower() == "true",
|
|
135
|
-
"Transmissivity": (
|
|
136
|
-
int(mat["Transmissivity"])
|
|
137
|
-
if float(mat["Transmissivity"]).is_integer()
|
|
138
|
-
else float(mat["Transmissivity"])
|
|
139
|
-
),
|
|
140
|
-
"Type": mat.get("Type", "PLA"),
|
|
141
|
-
"uuid": mat.get("Uuid", str(uuid.uuid4())),
|
|
142
|
-
}
|
|
143
|
-
)
|
|
170
|
+
filament_set.append(_hfp_filament(material_data[idx]))
|
|
144
171
|
|
|
145
172
|
filament_set = filament_set[::-1]
|
|
146
173
|
|
|
@@ -204,7 +231,9 @@ def generate_project_file(
|
|
|
204
231
|
|
|
205
232
|
# Write out the project file as JSON
|
|
206
233
|
with open(project_filename, "w") as f:
|
|
207
|
-
|
|
234
|
+
# allow_nan=False: a NaN that slipped through must fail here, loudly,
|
|
235
|
+
# rather than produce a file HueForge cannot parse.
|
|
236
|
+
json.dump(project_data, f, indent=4, allow_nan=False)
|
|
208
237
|
|
|
209
238
|
|
|
210
239
|
def generate_stl(
|
|
@@ -526,93 +555,69 @@ def generate_flatforge_stls(
|
|
|
526
555
|
)
|
|
527
556
|
print(f"Selected clear material: {material_names[most_transparent_idx]} (TD: {material_TDs_np[most_transparent_idx]:.2f})")
|
|
528
557
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
# This layer is a gap in disc_global, extend the color from below
|
|
553
|
-
# This prevents clear from being placed between two colored layers
|
|
554
|
-
layer_materials[layer, i, j] = last_color
|
|
555
|
-
# else: both material and last_color are -1, leave as -1
|
|
556
|
-
|
|
558
|
+
heights = np.asarray(disc_height_image).astype(np.int64)
|
|
559
|
+
|
|
560
|
+
# The material printed at each layer. A gap in disc_global (-1) takes the
|
|
561
|
+
# color from below, so there is never clear between two colored layers.
|
|
562
|
+
layer_color = np.full(max_layer, -1, dtype=np.int64)
|
|
563
|
+
last_color = -1
|
|
564
|
+
for layer in range(max_layer):
|
|
565
|
+
material = int(disc_global[layer])
|
|
566
|
+
if material >= 0:
|
|
567
|
+
last_color = material
|
|
568
|
+
layer_color[layer] = last_color
|
|
569
|
+
|
|
570
|
+
# Contiguous runs of one material: (material, first_layer, end_layer).
|
|
571
|
+
bands = []
|
|
572
|
+
for layer in range(max_layer):
|
|
573
|
+
material = int(layer_color[layer])
|
|
574
|
+
if material < 0:
|
|
575
|
+
continue
|
|
576
|
+
if bands and bands[-1][0] == material and bands[-1][2] == layer:
|
|
577
|
+
bands[-1] = (material, bands[-1][1], layer + 1)
|
|
578
|
+
else:
|
|
579
|
+
bands.append((material, layer, layer + 1))
|
|
580
|
+
|
|
557
581
|
# Get unique materials used (excluding background)
|
|
558
|
-
unique_materials =
|
|
559
|
-
|
|
560
|
-
|
|
582
|
+
unique_materials = sorted({material for material, _, _ in bands})
|
|
583
|
+
|
|
561
584
|
# Calculate the total height of the print (including cap layers)
|
|
562
585
|
total_height = background_height + (max_layer + cap_layers) * layer_height
|
|
563
|
-
|
|
586
|
+
|
|
564
587
|
stl_files = []
|
|
565
|
-
|
|
566
|
-
# Helper function to create a flat box STL for a given material at specific layers
|
|
588
|
+
|
|
567
589
|
def create_color_stl(material_idx, material_name, color_hex):
|
|
568
|
-
"""Create
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
590
|
+
"""Create the STL for one material: one solid per band it prints in.
|
|
591
|
+
|
|
592
|
+
A material can recur in several separate bands (e.g. white low down
|
|
593
|
+
and again as a highlight). A single solid spanning the lowest to the
|
|
594
|
+
highest of its layers used to swallow every other material printed in
|
|
595
|
+
between, so the per-color STLs overlapped. At a pixel of height z, the
|
|
596
|
+
band [a, b) is filled from layer a up to min(b, z)."""
|
|
597
|
+
pieces = []
|
|
598
|
+
for material, first, end in bands:
|
|
599
|
+
if material != material_idx:
|
|
600
|
+
continue
|
|
601
|
+
band_mask = heights > first
|
|
602
|
+
if not np.any(band_mask & valid_mask):
|
|
603
|
+
continue
|
|
604
|
+
top_mm = np.minimum(heights, end).astype(float) * layer_height
|
|
605
|
+
bottom_mm = np.full((H, W), first * layer_height, dtype=float)
|
|
606
|
+
mesh_data = _create_flatforge_box_mesh(
|
|
607
|
+
top_mm, bottom_mm, background_height,
|
|
608
|
+
maximum_x_y_size, valid_mask, band_mask
|
|
609
|
+
)
|
|
610
|
+
if mesh_data is not None:
|
|
611
|
+
pieces.append(mesh_data)
|
|
612
|
+
|
|
613
|
+
if not pieces:
|
|
591
614
|
return None
|
|
592
|
-
|
|
593
|
-
# Convert to mm (layers to mm)
|
|
594
|
-
height_map_mm = height_map * layer_height
|
|
595
|
-
min_height_map_mm = min_height_map * layer_height
|
|
596
|
-
|
|
597
|
-
# Create a 2D mask indicating which pixels have this material (at any layer)
|
|
598
|
-
material_mask_2d = np.any(material_mask_3d, axis=0)
|
|
599
|
-
|
|
600
|
-
# Create vertices for the box
|
|
601
|
-
# We need to create a rectangular box where z goes from min to max for each pixel
|
|
615
|
+
|
|
602
616
|
filename = os.path.join(output_folder, f"{material_name}_{color_hex.lstrip('#')}.stl")
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
maximum_x_y_size, valid_mask, material_mask_2d
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
if mesh_data is not None:
|
|
611
|
-
_save_stl_with_manifold_fix(mesh_data, filename)
|
|
612
|
-
stl_files.append(filename)
|
|
613
|
-
return filename
|
|
614
|
-
return None
|
|
615
|
-
|
|
617
|
+
_save_stl_with_manifold_fix(np.concatenate(pieces, axis=0), filename)
|
|
618
|
+
stl_files.append(filename)
|
|
619
|
+
return filename
|
|
620
|
+
|
|
616
621
|
# Generate STL for each unique material
|
|
617
622
|
for mat_idx in unique_materials:
|
|
618
623
|
material_name = material_names[mat_idx].replace(" ", "_").replace("/", "-")
|
|
@@ -621,42 +626,20 @@ def generate_flatforge_stls(
|
|
|
621
626
|
color_hex = "#{:02x}{:02x}{:02x}".format(
|
|
622
627
|
int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255)
|
|
623
628
|
)
|
|
624
|
-
|
|
629
|
+
|
|
625
630
|
print(f"Generating FlatForge STL for {material_name}...")
|
|
626
631
|
create_color_stl(mat_idx, material_name, color_hex)
|
|
627
|
-
|
|
632
|
+
|
|
628
633
|
# Generate STL for clear/transparent areas
|
|
629
|
-
# Clear
|
|
630
|
-
#
|
|
631
|
-
# we only need to check if the pixel height is less than max_layer (unfilled top layers)
|
|
634
|
+
# Clear is only placed above the topmost colored layer of a pixel, never
|
|
635
|
+
# between colors (gaps take the color from below, see layer_color).
|
|
632
636
|
print("Generating FlatForge STL for clear areas...")
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
for i in range(H):
|
|
640
|
-
for j in range(W):
|
|
641
|
-
if not valid_mask[i, j]:
|
|
642
|
-
continue
|
|
643
|
-
|
|
644
|
-
pixel_height = int(disc_height_image[i, j])
|
|
645
|
-
|
|
646
|
-
# If pixel height is less than max_layer, fill the top with clear
|
|
647
|
-
if pixel_height < max_layer:
|
|
648
|
-
has_clear = True
|
|
649
|
-
clear_min_height_map[i, j] = pixel_height
|
|
650
|
-
clear_height_map[i, j] = max_layer
|
|
651
|
-
# Note: We don't check layer_materials because gaps are already filled with colors
|
|
652
|
-
|
|
653
|
-
if has_clear:
|
|
654
|
-
clear_height_map_mm = clear_height_map * layer_height
|
|
655
|
-
clear_min_height_map_mm = clear_min_height_map * layer_height
|
|
656
|
-
|
|
657
|
-
# Create mask for clear areas
|
|
658
|
-
clear_mask = (clear_height_map > 0) & valid_mask
|
|
659
|
-
|
|
637
|
+
|
|
638
|
+
clear_mask = valid_mask & (heights < max_layer)
|
|
639
|
+
if np.any(clear_mask):
|
|
640
|
+
clear_height_map_mm = np.full((H, W), max_layer * layer_height, dtype=float)
|
|
641
|
+
clear_min_height_map_mm = heights.astype(float) * layer_height
|
|
642
|
+
|
|
660
643
|
filename = os.path.join(output_folder, f"Clear_{clear_material_name}_{clear_color_hex}.stl")
|
|
661
644
|
mesh_data = _create_flatforge_box_mesh(
|
|
662
645
|
clear_height_map_mm, clear_min_height_map_mm, background_height,
|
|
@@ -667,7 +650,7 @@ def generate_flatforge_stls(
|
|
|
667
650
|
stl_files.append(filename)
|
|
668
651
|
else:
|
|
669
652
|
print("No clear areas needed - all layers filled with colored materials.")
|
|
670
|
-
|
|
653
|
+
|
|
671
654
|
# Generate cap layer if requested
|
|
672
655
|
if cap_layers > 0:
|
|
673
656
|
print(f"Generating FlatForge STL for cap layer ({cap_layers} layers)...")
|