Lucasgitguy-co commited on
Commit
3469f52
·
verified ·
1 Parent(s): 6b9bf31

Publish evaluated Forma3D Tiny 3 ABO continuation

Browse files
ATTRIBUTIONS.json ADDED
The diff for this file is too large to render. See raw diff
 
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
NOTICE ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Forma3D-Tiny v0.2.0, derived from Image3D Student v0.1.0
2
+ Copyright 2026 Lucasgitguy-co.
3
+
4
+ Original project source, documentation and procedural demonstration artifacts are released under Apache License 2.0.
5
+
6
+ The optional Idol integration and Gaussian attribute conventions were developed with reference to
7
+ speridlabs/idol-sdk, revision 4702a7f00850e34b6e83a2170cc123c3e9570744 (Apache-2.0).
8
+ Its architecture is imported as an optional dependency; pretrained teacher weights are not redistributed.
9
+ Adaptations in this project: staged loading, query-chunk attention, explicit real-valued RoPE selection,
10
+ independent student training, coordinate-preserving Gaussian IO, and diagnostic rendering.
11
+ Upstream attribution is retained in licenses/Idol-APACHE-2.0.txt and THIRD_PARTY.md.
12
+
13
+ ABO continuation data: Amazon.com, Amazon Berkeley Objects contributors; converted to GLB by CodePawl. CC BY 4.0. Training preprocessing decompresses geometry, normalizes objects, samples surface colors and renders Gaussian proxies. See ATTRIBUTIONS.json and licenses/LICENSE-ABO-CC-BY-4.0.txt. No endorsement implied.
README.md ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ pipeline_tag: image-to-3d
4
+ base_model: Lucasgitguy-co/forma3d-tiny-2
5
+ base_model_relation: finetune
6
+ model_name: Forma3D Tiny 3
7
+ datasets:
8
+ - codepawl/abo-glb
9
+ tags:
10
+ - pytorch
11
+ - gaussian-splatting
12
+ - mesh-proxy-supervision
13
+ - research-prototype
14
+ ---
15
+ # Forma3D Tiny 3
16
+
17
+ Continued from [Lucasgitguy-co/forma3d-tiny-2](https://huggingface.co/Lucasgitguy-co/forma3d-tiny-2) at `1648582b07a0f7f9d482a8c0f4cf89e6ecd86972`.
18
+ Same 306,321-parameter architecture, 16 cubed grid, width 64, maximum 4,096 Gaussian slots.
19
+ Fresh AdamW optimizer at the start of continuation. Completed 6000 steps;
20
+ selected step 6000 by validation loss only.
21
+
22
+ ## Data and scope
23
+
24
+ 512 ABO objects, 2048 orthographic Gaussian-proxy images on black.
25
+ Data revision: `c7c8293fed3b7ab122ef5d7896521a187e0962d7`. Targets are isotropic mesh surface samples, compressed to
26
+ one Gaussian per occupied grid cell. Texture surface colors are sampled; full PBR rendering
27
+ and differentiable rendering are not used. No natural-photo training is claimed.
28
+ MeshAnythingV2, Idol Diffusers, SHARP, Pixal3D, Idol Turbo and Qwen3.5-4B-3D-Coder
29
+ were **not run, merged, or distilled** in this Mac-only recipe.
30
+
31
+ Objects, shared Amazon item IDs and exact mesh/image/target hashes are grouped before splitting.
32
+ Split groups: {"train": 348, "validation": 87, "test": 77}. Test data is excluded from training and checkpoint selection.
33
+ Near duplicates can remain; overlap with ancestor training datasets is not fully known.
34
+
35
+ ## Evaluation
36
+
37
+ Both checkpoints were evaluated on the identical held-out ABO proxy views.
38
+
39
+ | Metric | Starting v2 | Selected v3 |
40
+ |---|---:|---:|
41
+ | Mean test loss | 0.690551 | 0.544487 |
42
+ | Mean view occupancy IoU, threshold 0.5 | 0.286866 | 0.339860 |
43
+
44
+ These measurements do not establish natural-photo reconstruction, photorealism, mesh quality,
45
+ novel-view rendering quality or improvement on v2's original domains. No confidence intervals
46
+ or broad generalization claim. See evaluation.json, preparation.json and split manifests.
47
+
48
+ ## Use
49
+
50
+ With Python 3.11, install this repository using `pip install .`, then:
51
+
52
+ ```bash
53
+ image3d generate --checkpoint model.pt --image centered-object-on-black.png --out object.ply --device mps
54
+ image3d preview --ply object.ply --out preview.png
55
+ ```
56
+
57
+ Output is Gaussian PLY, not a textured GLB mesh. Inputs should match centered objects on black.
58
+ The model has no text prompt conditioning. Reproduction launcher and locked dependencies are in recipe/.
59
+
60
+ ## Attribution
61
+
62
+ Publisher source and weights: Apache 2.0. Retain inherited NOTICE, THIRD_PARTY.md and ATTRIBUTIONS.json.
63
+ ABO models, images and listing text: Amazon.com and the Amazon Berkeley Objects contributors,
64
+ CC BY 4.0; converted to GLB by CodePawl. The recipe further decompresses geometry,
65
+ normalizes coordinates, samples colored surfaces and renders proxy images.
66
+ See [ABO dataset](https://huggingface.co/datasets/codepawl/abo-glb),
67
+ ATTRIBUTIONS.json and licenses/LICENSE-ABO-CC-BY-4.0.txt. Raw assets are not bundled.
SHA256SUMS ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 952b78774f6d41182b685271d6ed64abbca9b0c5ff4081e8e55ab92ce256e737 ATTRIBUTIONS.json
2
+ cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 LICENSE
3
+ d84c0de5fa230ed94d4a2c84c791ffe4a13a2f73ac2657b19bcf0b55f11726a7 NOTICE
4
+ 0ae2c6c30c34fd70281e576023e040052c7546adad85c530c91e179299c2dc16 README.md
5
+ ef7c689f184a7921b02887eeb8d859e8cca77da6d2082999bb19bf29beda61f1 THIRD_PARTY.md
6
+ 54439a905e24b39dfc4d2c28a4b45b97e584a3cf40c612700e71aba4b51d9409 completed.json
7
+ 25d53c6500d9e45aca8a2ce1fe54030e05a0822c554ab24cfcbfeaa631ee9c91 config.json
8
+ e6aca692ff4fe1a6066c67152253435bebb86bcd33983efa73d9b45b0709da23 environment.txt
9
+ 5bab5e4f1938a873a5f2eb8405fe4d94e0b532bc566250189c5b1e764ed3eebf evaluation.json
10
+ cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 licenses/Idol-APACHE-2.0.txt
11
+ 419896aea50c15d6e40c5b4baf4bd346f78223b9a354c7357ad00262afdc08ec licenses/LICENSE-ABO-CC-BY-4.0.txt
12
+ c9175419fd345f1b2831201495cf27d9929b92dee52288c798ece243b13f6db1 metrics.jsonl
13
+ ec7c37e8ff293877149822150a5e08e8dde3e37fe0e6cfc1dc71e05adecd7790 model.pt
14
+ 14d89f03bdae1c5cb416b121248d024a49c30d5545bd560b6b390c52afa049c2 preparation.json
15
+ e8f66f2a90fbe6c42cc098357cc862d8ea00eaf3a4f38fa22eb59b168eab2ee9 pyproject.toml
16
+ da5bb78d178599428987111fc83e27678fd3589e59c4756653f612114e139a29 recipe/decode.mjs
17
+ 72730d8990cffc36b3f77ced370306978d01ddeb9b417c891613c0e95cb475ac recipe/package-lock.json
18
+ 4d87fdff2312cb3afc1ecd98c39e152c6b44e792b4ea717c8f94204ab8d857a7 recipe/package.json
19
+ 33ef603b3550a78eb982138540bcdbbca596d3b3f697368593a6d5ca3ff4b436 recipe/pipeline.py
20
+ 890fa9075ec44ab44d42db01a893df1fd1ab8b6c989324041f4ee3537a2d5720 recipe/requirements.txt
21
+ b9e8acb85f25b4e35ab555e81d09a3679e5daa4edafdc5f28a80c4442da83db3 recipe/run.command
22
+ 2d0c28622dfb8d3a51f1cc631bbb62ed48c135c83aee2b9edf72aed161b9a3e3 settings.json
23
+ c8f927c42ba2ccecd960536de8c7c7431c8fe674401e1fbe7d7613d18cab860d splits/test.jsonl
24
+ 65af1c16cefde7e9cd411af055ccdee36c183382db18a4fd9a9d4ea82fb248ee splits/train.jsonl
25
+ 65ca208e5b7d59d398056c997751b12a18ce05c9361e37f4160f3b1420cbe52a splits/validation.jsonl
26
+ c09ce8cb78cd68ae32b618ba49f97508b6ce14bc89e88c5b80cc3e2487bd919c src/image3d_student/__init__.py
27
+ b4be53a864458a84009e91277ff64767dffc6139ca3afcc50526747743722c02 src/image3d_student/cli.py
28
+ d34f9fa50d574764e5e2c1c31c1ca477f2bc4a1887e103c5c47b754f1f1d8c72 src/image3d_student/data.py
29
+ e01d1b42929c993d92cad1bde8944b14c6a8e325fc279d8fd3e9c7287523ad69 src/image3d_student/demo.py
30
+ 7335dcc99fcd9cea5c217841376f0ca8c0ca179809b4df34e344bf929d11da51 src/image3d_student/engine.py
31
+ a18005f75a66846446b2574b4f1b085ebde5321e835c4e5b4507e15e812ee5ef src/image3d_student/model.py
32
+ 41961fae62d2819c1e3f455e74539a66b8c4f57bedd9fb9379497b3a5764c692 src/image3d_student/splats.py
33
+ 2e2439bc0a48c52e32a8e273f879ed7328fdb82632a996ed3e239e0391ca3a59 src/image3d_student/teacher_idol.py
34
+ ebe640c6078f02ef84fb9992bd3f7585a326b523e9f480526c2057d5a2892074 training.json
THIRD_PARTY.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Third-party scope
2
+
3
+ This training kit extends Lucasgitguy-co/image3d-student, Apache-2.0. The original
4
+ checkpoint is downloaded separately. No new trained weights are bundled in the kit.
5
+
6
+ Optional Idol SDK integration is inherited from the original project, pinned to
7
+ `4702a7f00850e34b6e83a2170cc123c3e9570744`; its Apache notice is retained in
8
+ `licenses/Idol-APACHE-2.0.txt`. The Mac recipe does not install or run Idol, Hi3D,
9
+ InstantMesh, Hunyuan, or gated encoders. Their weights are not redistributed.
10
+
11
+ Dependencies retain their respective licenses. Third-party dataset terms are
12
+ documented in DATASETS.md; per-asset attribution is generated during preparation
13
+ and copied into any published release. Code licensing does not relicense datasets.
completed.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "completed_steps": 6000,
3
+ "selected_step": 6000,
4
+ "best_validation_loss": 0.5845230878598389
5
+ }
config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "grid": 16,
3
+ "width": 64
4
+ }
environment.txt ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Jinja2==3.1.6
2
+ MarkupSafe==3.0.4
3
+ PyYAML==6.0.3
4
+ anyio==4.16.0
5
+ click==8.5.1
6
+ filelock==4.2.9
7
+ fsspec==2026.9.0
8
+ h11==0.16.0
9
+ hf-xet==1.8.0
10
+ httpcore2==2.16.0
11
+ httpx2==2.16.0
12
+ huggingface_hub==2.4.0
13
+ idna==3.20
14
+ mpmath==1.3.0
15
+ networkx==3.6.1
16
+ numpy==2.4.6
17
+ packaging==26.3
18
+ pillow==13.0.0
19
+ plyfile==1.1.5
20
+ pyarrow==26.0.0
21
+ scipy==1.17.1
22
+ setuptools==84.2.1
23
+ sympy==1.14.0
24
+ torch==2.15.0
25
+ tqdm==4.70.1
26
+ trimesh==5.2.0
27
+ truststore==0.10.5
28
+ typing_extensions==4.16.0
evaluation.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "base_v2": {
3
+ "sha256": "c051b3f93d874f7c638588c9d0381754d3f954b1cd271121a033b2478d36bb1a",
4
+ "metrics": {
5
+ "occupancy": 0.5241638378953779,
6
+ "position": 0.0944967952302911,
7
+ "colour": 0.03564881652105273,
8
+ "scale": 0.0022415842836557938,
9
+ "rotation": 1.5440619935813406e-05,
10
+ "opacity": 0.00036697591761591643,
11
+ "geometry": 0.0,
12
+ "loss": 0.6905509463765405,
13
+ "occupancy_iou": 0.2868661397414935
14
+ }
15
+ },
16
+ "candidate_v3": {
17
+ "sha256": "ec7c37e8ff293877149822150a5e08e8dde3e37fe0e6cfc1dc71e05adecd7790",
18
+ "metrics": {
19
+ "occupancy": 0.38844528866859224,
20
+ "position": 0.09291809090933242,
21
+ "colour": 0.03145392079041612,
22
+ "scale": 0.0012586073008405238,
23
+ "rotation": 2.456774355854217e-06,
24
+ "opacity": 8.929977638831883e-05,
25
+ "geometry": 0.0,
26
+ "loss": 0.5444866280954379,
27
+ "occupancy_iou": 0.33986033104940666
28
+ }
29
+ },
30
+ "gates": {
31
+ "training_complete": true,
32
+ "validation_loss_improved": true,
33
+ "test_loss_improved_at_least_half_percent": true,
34
+ "test_occupancy_iou_not_worse": true,
35
+ "weights_changed": true
36
+ },
37
+ "passed": true,
38
+ "test_manifest_sha256": "c8f927c42ba2ccecd960536de8c7c7431c8fe674401e1fbe7d7613d18cab860d",
39
+ "measurement": "mean per-view proxy loss and occupancy IoU, threshold 0.5, batch size 1",
40
+ "limitation": "Not evidence of natural-photo, mesh, texture or photorealistic rendering quality."
41
+ }
licenses/Idol-APACHE-2.0.txt ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
licenses/LICENSE-ABO-CC-BY-4.0.txt ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Creative Commons Attribution 4.0 International Public License
2
+
3
+ By exercising the Licensed Rights (defined below), You accept and agree
4
+ to be bound by the terms and conditions of this Creative Commons
5
+ Attribution 4.0 International Public License ("Public License"). To the
6
+ extent this Public License may be interpreted as a contract, You are
7
+ granted the Licensed Rights in consideration of Your acceptance of
8
+ these terms and conditions, and the Licensor grants You such rights in
9
+ consideration of benefits the Licensor receives from making the
10
+ Licensed Material available under these terms and conditions.
11
+
12
+
13
+ Section 1 -- Definitions.
14
+
15
+ a. Adapted Material means material subject to Copyright and Similar
16
+ Rights that is derived from or based upon the Licensed Material
17
+ and in which the Licensed Material is translated, altered,
18
+ arranged, transformed, or otherwise modified in a manner requiring
19
+ permission under the Copyright and Similar Rights held by the
20
+ Licensor. For purposes of this Public License, where the Licensed
21
+ Material is a musical work, performance, or sound recording,
22
+ Adapted Material is always produced where the Licensed Material is
23
+ synched in timed relation with a moving image.
24
+
25
+ b. Adapter's License means the license You apply to Your Copyright
26
+ and Similar Rights in Your contributions to Adapted Material in
27
+ accordance with the terms and conditions of this Public License.
28
+
29
+ c. Copyright and Similar Rights means copyright and/or similar rights
30
+ closely related to copyright including, without limitation,
31
+ performance, broadcast, sound recording, and Sui Generis Database
32
+ Rights, without regard to how the rights are labeled or
33
+ categorized. For purposes of this Public License, the rights
34
+ specified in Section 2(b)(1)-(2) are not Copyright and Similar
35
+ Rights.
36
+
37
+ d. Effective Technological Measures means those measures that, in the
38
+ absence of proper authority, may not be circumvented under laws
39
+ fulfilling obligations under Article 11 of the WIPO Copyright
40
+ Treaty adopted on December 20, 1996, and/or similar international
41
+ agreements.
42
+
43
+ e. Exceptions and Limitations means fair use, fair dealing, and/or
44
+ any other exception or limitation to Copyright and Similar Rights
45
+ that applies to Your use of the Licensed Material.
46
+
47
+ f. Licensed Material means the artistic or literary work, database,
48
+ or other material to which the Licensor applied this Public
49
+ License.
50
+
51
+ g. Licensed Rights means the rights granted to You subject to the
52
+ terms and conditions of this Public License, which are limited to
53
+ all Copyright and Similar Rights that apply to Your use of the
54
+ Licensed Material and that the Licensor has authority to license.
55
+
56
+ h. Licensor means the individual(s) or entity(ies) granting rights
57
+ under this Public License.
58
+
59
+ i. Share means to provide material to the public by any means or
60
+ process that requires permission under the Licensed Rights, such
61
+ as reproduction, public display, public performance, distribution,
62
+ dissemination, communication, or importation, and to make material
63
+ available to the public including in ways that members of the
64
+ public may access the material from a place and at a time
65
+ individually chosen by them.
66
+
67
+ j. Sui Generis Database Rights means rights other than copyright
68
+ resulting from Directive 96/9/EC of the European Parliament and of
69
+ the Council of 11 March 1996 on the legal protection of databases,
70
+ as amended and/or succeeded, as well as other essentially
71
+ equivalent rights anywhere in the world.
72
+
73
+ k. You means the individual or entity exercising the Licensed Rights
74
+ under this Public License. Your has a corresponding meaning.
75
+
76
+
77
+ Section 2 -- Scope.
78
+
79
+ a. License grant.
80
+
81
+ 1. Subject to the terms and conditions of this Public License,
82
+ the Licensor hereby grants You a worldwide, royalty-free,
83
+ non-sublicensable, non-exclusive, irrevocable license to
84
+ exercise the Licensed Rights in the Licensed Material to:
85
+
86
+ a. reproduce and Share the Licensed Material, in whole or
87
+ in part; and
88
+
89
+ b. produce, reproduce, and Share Adapted Material.
90
+
91
+ 2. Exceptions and Limitations. For the avoidance of doubt, where
92
+ Exceptions and Limitations apply to Your use, this Public
93
+ License does not apply, and You do not need to comply with
94
+ its terms and conditions.
95
+
96
+ 3. Term. The term of this Public License is specified in Section
97
+ 6(a).
98
+
99
+ 4. Media and formats; technical modifications allowed. The
100
+ Licensor authorizes You to exercise the Licensed Rights in
101
+ all media and formats whether now known or hereafter created,
102
+ and to make technical modifications necessary to do so. The
103
+ Licensor waives and/or agrees not to assert any right or
104
+ authority to forbid You from making technical modifications
105
+ necessary to exercise the Licensed Rights, including
106
+ technical modifications necessary to circumvent Effective
107
+ Technological Measures. For purposes of this Public License,
108
+ simply making modifications authorized by this Section 2(a)
109
+ (4) never produces Adapted Material.
110
+
111
+ 5. Downstream recipients.
112
+
113
+ a. Offer from the Licensor -- Licensed Material. Every
114
+ recipient of the Licensed Material automatically
115
+ receives an offer from the Licensor to exercise the
116
+ Licensed Rights under the terms and conditions of this
117
+ Public License.
118
+
119
+ b. No downstream restrictions. You may not offer or impose
120
+ any additional or different terms or conditions on, or
121
+ apply any Effective Technological Measures to, the
122
+ Licensed Material if doing so restricts exercise of the
123
+ Licensed Rights by any recipient of the Licensed
124
+ Material.
125
+
126
+ 6. No endorsement. Nothing in this Public License constitutes or
127
+ may be construed as permission to assert or imply that You
128
+ are, or that Your use of the Licensed Material is, connected
129
+ with, or sponsored, endorsed, or granted official status by,
130
+ the Licensor or others designated to receive attribution as
131
+ provided in Section 3(a)(1)(A)(i).
132
+
133
+ b. Other rights.
134
+
135
+ 1. Moral rights, such as the right of integrity, are not
136
+ licensed under this Public License, nor are publicity,
137
+ privacy, and/or other similar personality rights; however, to
138
+ the extent possible, the Licensor waives and/or agrees not to
139
+ assert any such rights held by the Licensor to the limited
140
+ extent necessary to allow You to exercise the Licensed
141
+ Rights, but not otherwise.
142
+
143
+ 2. Patent and trademark rights are not licensed under this
144
+ Public License.
145
+
146
+ 3. To the extent possible, the Licensor waives any right to
147
+ collect royalties from You for the exercise of the Licensed
148
+ Rights, whether directly or through a collecting society
149
+ under any voluntary or waivable statutory or compulsory
150
+ licensing scheme. In all other cases the Licensor expressly
151
+ reserves any right to collect such royalties.
152
+
153
+
154
+ Section 3 -- License Conditions.
155
+
156
+ Your exercise of the Licensed Rights is expressly made subject to the
157
+ following conditions.
158
+
159
+ a. Attribution.
160
+
161
+ 1. If You Share the Licensed Material (including in modified
162
+ form), You must:
163
+
164
+ a. retain the following if it is supplied by the Licensor
165
+ with the Licensed Material:
166
+
167
+ i. identification of the creator(s) of the Licensed
168
+ Material and any others designated to receive
169
+ attribution, in any reasonable manner requested by
170
+ the Licensor (including by pseudonym if
171
+ designated);
172
+
173
+ ii. a copyright notice;
174
+
175
+ iii. a notice that refers to this Public License;
176
+
177
+ iv. a notice that refers to the disclaimer of
178
+ warranties;
179
+
180
+ v. a URI or hyperlink to the Licensed Material to the
181
+ extent reasonably practicable;
182
+
183
+ b. indicate if You modified the Licensed Material and
184
+ retain an indication of any previous modifications; and
185
+
186
+ c. indicate the Licensed Material is licensed under this
187
+ Public License, and include the text of, or the URI or
188
+ hyperlink to, this Public License.
189
+
190
+ 2. You may satisfy the conditions in Section 3(a)(1) in any
191
+ reasonable manner based on the medium, means, and context in
192
+ which You Share the Licensed Material. For example, it may be
193
+ reasonable to satisfy the conditions by providing a URI or
194
+ hyperlink to a resource that includes the required
195
+ information.
196
+
197
+ 3. If requested by the Licensor, You must remove any of the
198
+ information required by Section 3(a)(1)(A) to the extent
199
+ reasonably practicable.
200
+
201
+ 4. If You Share Adapted Material You produce, the Adapter's
202
+ License You apply must not prevent recipients of the Adapted
203
+ Material from complying with this Public License.
204
+
205
+
206
+ Section 4 -- Sui Generis Database Rights.
207
+
208
+ Where the Licensed Rights include Sui Generis Database Rights that
209
+ apply to Your use of the Licensed Material:
210
+
211
+ a. for the avoidance of doubt, Section 2(a)(1) grants You the right
212
+ to extract, reuse, reproduce, and Share all or a substantial
213
+ portion of the contents of the database;
214
+
215
+ b. if You include all or a substantial portion of the database
216
+ contents in a database in which You have Sui Generis Database
217
+ Rights, then the database in which You have Sui Generis Database
218
+ Rights (but not its individual contents) is Adapted Material; and
219
+
220
+ c. You must comply with the conditions in Section 3(a) if You Share
221
+ all or a substantial portion of the contents of the database.
222
+
223
+ For the avoidance of doubt, this Section 4 supplements and does not
224
+ replace Your obligations under this Public License where the Licensed
225
+ Rights include other Copyright and Similar Rights.
226
+
227
+
228
+ Section 5 -- Disclaimer of Warranties and Limitation of Liability.
229
+
230
+ a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
231
+ EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
232
+ AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
233
+ ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
234
+ IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
235
+ WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
236
+ PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
237
+ ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
238
+ KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
239
+ ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
240
+
241
+ b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
242
+ TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
243
+ NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
244
+ INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
245
+ COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
246
+ USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
247
+ ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
248
+ DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
249
+ IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
250
+
251
+ c. The disclaimer of warranties and limitation of liability provided
252
+ above shall be interpreted in a manner that, to the extent
253
+ possible, most closely approximates an absolute disclaimer and
254
+ waiver of all liability.
255
+
256
+
257
+ Section 6 -- Term and Termination.
258
+
259
+ a. This Public License applies for the term of the Copyright and
260
+ Similar Rights licensed here. However, if You fail to comply with
261
+ this Public License, then Your rights under this Public License
262
+ terminate automatically.
263
+
264
+ b. Where Your right to use the Licensed Material has terminated under
265
+ Section 6(a), it reinstates:
266
+
267
+ 1. automatically as of the date the violation is cured, provided
268
+ it is cured within 30 days of Your discovery of the
269
+ violation; or
270
+
271
+ 2. upon express reinstatement by the Licensor.
272
+
273
+ For the avoidance of doubt, this Section 6(b) does not affect any
274
+ right the Licensor may have to seek remedies for Your violations
275
+ of this Public License.
276
+
277
+ c. For the avoidance of doubt, the Licensor may also offer the
278
+ Licensed Material under separate terms or conditions or stop
279
+ distributing the Licensed Material at any time; however, doing so
280
+ will not terminate this Public License.
281
+
282
+ d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
283
+ License.
284
+
285
+
286
+ Section 7 -- Other Terms and Conditions.
287
+
288
+ a. The Licensor shall not be bound by any additional or different
289
+ terms or conditions communicated by You unless expressly agreed.
290
+
291
+ b. Any arrangements, understandings, or agreements regarding the
292
+ Licensed Material not stated herein are separate from and
293
+ independent of the terms and conditions of this Public License.
294
+
295
+
296
+ Section 8 -- Interpretation.
297
+
298
+ a. For the avoidance of doubt, this Public License does not, and
299
+ shall not be interpreted to, reduce, limit, restrict, or impose
300
+ conditions on any use of the Licensed Material that could lawfully
301
+ be made without permission under this Public License.
302
+
303
+ b. To the extent possible, if any provision of this Public License is
304
+ deemed unenforceable, it shall be automatically reformed to the
305
+ minimum extent necessary to make it enforceable. If the provision
306
+ cannot be reformed, it shall be severed from this Public License
307
+ without affecting the enforceability of the remaining terms and
308
+ conditions.
309
+
310
+ c. No term or condition of this Public License will be waived and no
311
+ failure to comply consented to unless expressly agreed to by the
312
+ Licensor.
313
+
314
+ d. Nothing in this Public License constitutes or may be interpreted
315
+ as a limitation upon, or waiver of, any privileges and immunities
316
+ that apply to the Licensor or You, including from the legal
317
+ processes of any jurisdiction or authority.
metrics.jsonl ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"step": 250, "validation": {"occupancy": 0.46106726838939494, "position": 0.09389111219808974, "colour": 0.04310893663230296, "scale": 0.0017047523277184133, "rotation": 7.976808186366066e-06, "opacity": 0.0001671786365754631, "geometry": 0.0, "loss": 0.6415147080846216, "occupancy_iou": 0.28041237205181313}, "best_step": 250}
2
+ {"step": 500, "validation": {"occupancy": 0.45162729049722355, "position": 0.09384130607305587, "colour": 0.04260444884213897, "scale": 0.001661866389471909, "rotation": 7.302595148517532e-06, "opacity": 0.00015287357286561227, "geometry": 0.0, "loss": 0.6309972834484331, "occupancy_iou": 0.2809994066583699}, "best_step": 500}
3
+ {"step": 750, "validation": {"occupancy": 0.4463731253455425, "position": 0.09366091319370544, "colour": 0.04174524028390785, "scale": 0.0015576746118731327, "rotation": 6.378893373750188e-06, "opacity": 0.00014788560389499106, "geometry": 0.0, "loss": 0.623828811121398, "occupancy_iou": 0.28580000422809315}, "best_step": 750}
4
+ {"step": 1000, "validation": {"occupancy": 0.44312713128225556, "position": 0.09379177611192753, "colour": 0.04165307760182447, "scale": 0.0015555841554874212, "rotation": 5.9876457319153386e-06, "opacity": 0.00015077445404988366, "geometry": 0.0, "loss": 0.6205319956630125, "occupancy_iou": 0.2945208903830284}, "best_step": 1000}
5
+ {"step": 1250, "validation": {"occupancy": 0.44061438426717947, "position": 0.09367082068889306, "colour": 0.04216459069863342, "scale": 0.0014779921650355306, "rotation": 5.084008926746981e-06, "opacity": 0.00011434342663647087, "geometry": 0.0, "loss": 0.6188770349340877, "occupancy_iou": 0.2978490527569391}, "best_step": 1250}
6
+ {"step": 1500, "validation": {"occupancy": 0.4407315632735176, "position": 0.09352761307923958, "colour": 0.04121334745345273, "scale": 0.001461793350182265, "rotation": 5.134973895207005e-06, "opacity": 0.00013860988812520263, "geometry": 0.0, "loss": 0.6169711736769512, "occupancy_iou": 0.30566408061261835}, "best_step": 1500}
7
+ {"step": 1750, "validation": {"occupancy": 0.4404393983469612, "position": 0.09349199953949315, "colour": 0.04051890540187282, "scale": 0.0014701240166457071, "rotation": 4.675330279103517e-06, "opacity": 0.0001237910014685764, "geometry": 0.0, "loss": 0.6152404779675363, "occupancy_iou": 0.2812621619595193}, "best_step": 1750}
8
+ {"step": 2000, "validation": {"occupancy": 0.4346818130506181, "position": 0.09341091703323798, "colour": 0.040201177800073554, "scale": 0.001307281967079877, "rotation": 4.513101891955652e-06, "opacity": 0.00011089177358361217, "geometry": 0.0, "loss": 0.6087371564802082, "occupancy_iou": 0.3118145927444272}, "best_step": 2000}
9
+ {"step": 2250, "validation": {"occupancy": 0.43869901795325605, "position": 0.09344819003994438, "colour": 0.03963602398473357, "scale": 0.0013264096483364754, "rotation": 4.0235361918947135e-06, "opacity": 0.00012048149227112667, "geometry": 0.0, "loss": 0.6116727790955839, "occupancy_iou": 0.327563153215866}, "best_step": 2000}
10
+ {"step": 2500, "validation": {"occupancy": 0.43227739353803385, "position": 0.0933333754154115, "colour": 0.04032809674817113, "scale": 0.0014659382510196347, "rotation": 3.973887600551834e-06, "opacity": 0.00011141035008285113, "geometry": 0.0, "loss": 0.6065253679608477, "occupancy_iou": 0.317919851869516}, "best_step": 2500}
11
+ {"step": 2750, "validation": {"occupancy": 0.4353166666181608, "position": 0.09328038397448501, "colour": 0.040686976895854945, "scale": 0.0012758097041166794, "rotation": 3.63702629057905e-06, "opacity": 0.0001189368781472266, "geometry": 0.0, "loss": 0.6102178843542078, "occupancy_iou": 0.3161811849487753}, "best_step": 2500}
12
+ {"step": 3000, "validation": {"occupancy": 0.4327074812147124, "position": 0.09331944621243012, "colour": 0.039487305421605653, "scale": 0.0012567756840880242, "rotation": 3.5919594875489063e-06, "opacity": 0.00010452814740869792, "geometry": 0.0, "loss": 0.6052321044878027, "occupancy_iou": 0.32888385336632014}, "best_step": 3000}
13
+ {"step": 3250, "validation": {"occupancy": 0.4364492620031039, "position": 0.09324339584543787, "colour": 0.04059594625760346, "scale": 0.0014138426100627798, "rotation": 3.2943687288110042e-06, "opacity": 9.927087626499412e-05, "geometry": 0.0, "loss": 0.6111255342076565, "occupancy_iou": 0.3483805300389556}, "best_step": 3000}
14
+ {"step": 3500, "validation": {"occupancy": 0.42581871918391906, "position": 0.09325098914319072, "colour": 0.03928116287306171, "scale": 0.0011773849444413002, "rotation": 3.056185458513812e-06, "opacity": 9.935026393535757e-05, "geometry": 0.0, "loss": 0.5978494251425239, "occupancy_iou": 0.30775405541490547}, "best_step": 3500}
15
+ {"step": 3750, "validation": {"occupancy": 0.432027333469569, "position": 0.09319557866145825, "colour": 0.040138308227131804, "scale": 0.0011525598650526565, "rotation": 3.166901425911733e-06, "opacity": 9.929725881935239e-05, "geometry": 0.0, "loss": 0.6057144006093343, "occupancy_iou": 0.3355185413711715}, "best_step": 3500}
16
+ {"step": 4000, "validation": {"occupancy": 0.42550981757712775, "position": 0.09316891669456301, "colour": 0.04052523164856033, "scale": 0.0012177044145887364, "rotation": 2.9242365325929356e-06, "opacity": 0.00010036813353445754, "geometry": 0.0, "loss": 0.5999516276107437, "occupancy_iou": 0.3211282266667862}, "best_step": 3500}
17
+ {"step": 4250, "validation": {"occupancy": 0.43654163040477656, "position": 0.09321942790571985, "colour": 0.03907850670414822, "scale": 0.0011486642537023015, "rotation": 2.998764346520848e-06, "opacity": 0.00011165465859537745, "geometry": 0.0, "loss": 0.6081448930433426, "occupancy_iou": 0.35518712877970315}, "best_step": 3500}
18
+ {"step": 4500, "validation": {"occupancy": 0.42716445825223265, "position": 0.09306304662049501, "colour": 0.03876508917791994, "scale": 0.001119821725045893, "rotation": 2.8751217178591753e-06, "opacity": 9.490183666541229e-05, "geometry": 0.0, "loss": 0.5979648541616297, "occupancy_iou": 0.33583940336502144}, "best_step": 3500}
19
+ {"step": 4750, "validation": {"occupancy": 0.4252900903637724, "position": 0.09302231579505164, "colour": 0.03992765121651685, "scale": 0.001186143789385531, "rotation": 2.6761208888841394e-06, "opacity": 9.641269463733399e-05, "geometry": 0.0, "loss": 0.5983830038244011, "occupancy_iou": 0.32429440710265406}, "best_step": 3500}
20
+ {"step": 5000, "validation": {"occupancy": 0.4202662355469904, "position": 0.09291419757251766, "colour": 0.03968908278396669, "scale": 0.0011747911177380224, "rotation": 2.4885126973019615e-06, "opacity": 9.251441614933669e-05, "geometry": 0.0, "loss": 0.5927688404061329, "occupancy_iou": 0.32473888893708075}, "best_step": 5000}
21
+ {"step": 5250, "validation": {"occupancy": 0.41929296279946965, "position": 0.09285500444386197, "colour": 0.03871898723538061, "scale": 0.001147763923529118, "rotation": 2.570338795158545e-06, "opacity": 8.911200696316862e-05, "geometry": 0.0, "loss": 0.5897900897195969, "occupancy_iou": 0.3356060852328765}, "best_step": 5250}
22
+ {"step": 5500, "validation": {"occupancy": 0.42326834313999645, "position": 0.09288628300887415, "colour": 0.039693311325156654, "scale": 0.0011034196943690967, "rotation": 2.521009106911167e-06, "opacity": 9.737238293126566e-05, "geometry": 0.0, "loss": 0.5957492148910446, "occupancy_iou": 0.34857936678775425}, "best_step": 5250}
23
+ {"step": 5750, "validation": {"occupancy": 0.4186605631491576, "position": 0.09286394899432686, "colour": 0.039635561564341774, "scale": 0.001126612422368275, "rotation": 2.4452578455453876e-06, "opacity": 9.208203308760899e-05, "geometry": 0.0, "loss": 0.591000622288249, "occupancy_iou": 0.3344719207941972}, "best_step": 5250}
24
+ {"step": 6000, "validation": {"occupancy": 0.4138930352597401, "position": 0.09282098901083415, "colour": 0.038808255775693815, "scale": 0.0010773984927934296, "rotation": 2.286030634838985e-06, "opacity": 8.458352363703299e-05, "geometry": 0.0, "loss": 0.5845230878598389, "occupancy_iou": 0.3310552115669881}, "best_step": 6000}
model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ec7c37e8ff293877149822150a5e08e8dde3e37fe0e6cfc1dc71e05adecd7790
3
+ size 1725673
preparation.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dataset": "codepawl/abo-glb",
3
+ "revision": "c7c8293fed3b7ab122ef5d7896521a187e0962d7",
4
+ "objects": 512,
5
+ "views": 2048,
6
+ "groups": {
7
+ "train": 348,
8
+ "validation": 87,
9
+ "test": 77
10
+ },
11
+ "split_views": {
12
+ "train": 1392,
13
+ "validation": 348,
14
+ "test": 308
15
+ },
16
+ "manifest_sha256": {
17
+ "train": "65af1c16cefde7e9cd411af055ccdee36c183382db18a4fd9a9d4ea82fb248ee",
18
+ "validation": "65ca208e5b7d59d398056c997751b12a18ce05c9361e37f4160f3b1420cbe52a",
19
+ "test": "c8f927c42ba2ccecd960536de8c7c7431c8fe674401e1fbe7d7613d18cab860d"
20
+ },
21
+ "grouping": "object IDs, shared Amazon item IDs, exact mesh/image/target hashes",
22
+ "limitations": "Near duplicates can remain. Cross-dataset overlap with ancestor training is not fully known; only recorded exact source hashes were excluded."
23
+ }
pyproject.toml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "forma3d-tiny-3"
7
+ version = "0.4.0"
8
+ license = "Apache-2.0"
9
+ license-files = ["LICENSE", "NOTICE", "THIRD_PARTY.md", "licenses/*.txt"]
10
+ readme = "README.md"
11
+ authors = [{name = "Lucasgitguy-co"}]
12
+ description = "Experimental local image-to-Gaussian student with cached teacher supervision"
13
+ requires-python = ">=3.11,<3.14"
14
+ dependencies = ["torch>=2.5", "numpy>=1.26", "pillow>=10", "plyfile>=1", "trimesh>=4", "scipy>=1.11"]
15
+
16
+ [project.optional-dependencies]
17
+ test = ["pytest>=8"]
18
+ data = ["huggingface_hub>=0.34", "pyarrow>=17"]
19
+ teacher = ["idol @ git+https://github.com/speridlabs/idol-sdk.git@4702a7f00850e34b6e83a2170cc123c3e9570744"]
20
+
21
+ [project.scripts]
22
+ image3d = "image3d_student.cli:main"
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
26
+
27
+ [tool.pytest.ini_options]
28
+ testpaths = ["tests"]
recipe/decode.mjs ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NodeIO } from '@gltf-transform/core';
2
+ import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
3
+ import { dequantize } from '@gltf-transform/functions';
4
+ import { MeshoptDecoder } from 'meshoptimizer';
5
+ await MeshoptDecoder.ready;
6
+ const io = new NodeIO().registerExtensions(ALL_EXTENSIONS)
7
+ .registerDependencies({ 'meshopt.decoder': MeshoptDecoder });
8
+ const doc = await io.read(process.argv[2]);
9
+ await doc.transform(dequantize());
10
+ for (const ext of doc.getRoot().listExtensionsUsed()) {
11
+ if (ext.extensionName === 'EXT_meshopt_compression') ext.dispose();
12
+ }
13
+ await io.write(process.argv[3], doc);
recipe/package-lock.json ADDED
@@ -0,0 +1,784 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "forma3d-tiny-3-mac",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "dependencies": {
8
+ "@gltf-transform/core": "4.5.1",
9
+ "@gltf-transform/extensions": "4.5.1",
10
+ "@gltf-transform/functions": "4.5.1",
11
+ "meshoptimizer": "1.3.0"
12
+ }
13
+ },
14
+ "node_modules/@emnapi/runtime": {
15
+ "version": "1.11.3",
16
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
17
+ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
18
+ "license": "MIT",
19
+ "optional": true,
20
+ "dependencies": {
21
+ "tslib": "^2.4.0"
22
+ }
23
+ },
24
+ "node_modules/@gltf-transform/core": {
25
+ "version": "4.5.1",
26
+ "resolved": "https://registry.npmjs.org/@gltf-transform/core/-/core-4.5.1.tgz",
27
+ "integrity": "sha512-OWgBYssKHKoXMIrMb2N20jXqWRnMP9HdtcTFQBEAJtz4P+OOxx5fozl5eAeK7nktIs5uDE/lu4NbkE5VCZaR9g==",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "property-graph": "^4.1.0"
31
+ },
32
+ "funding": {
33
+ "url": "https://github.com/sponsors/donmccurdy"
34
+ }
35
+ },
36
+ "node_modules/@gltf-transform/extensions": {
37
+ "version": "4.5.1",
38
+ "resolved": "https://registry.npmjs.org/@gltf-transform/extensions/-/extensions-4.5.1.tgz",
39
+ "integrity": "sha512-3jTe+GWRy4fj1TU790LPsO+rpO/4byUWPTpNkjy6M6h8RvrUivRf12Mptc0770K4ZczibpJDNHXZFkFsFMRDLA==",
40
+ "license": "MIT",
41
+ "dependencies": {
42
+ "@gltf-transform/core": "^4.5.1",
43
+ "ktx-parse": "^1.1.0"
44
+ },
45
+ "funding": {
46
+ "url": "https://github.com/sponsors/donmccurdy"
47
+ }
48
+ },
49
+ "node_modules/@gltf-transform/functions": {
50
+ "version": "4.5.1",
51
+ "resolved": "https://registry.npmjs.org/@gltf-transform/functions/-/functions-4.5.1.tgz",
52
+ "integrity": "sha512-rfm4nQY2DJ+ip1e15xp7YfQwhRno3kPLcGw4EZiRk1dVwMV3cdFMcF08glYcO/+oLyYkUEs3xXMJYXa0cPgcTg==",
53
+ "license": "MIT",
54
+ "dependencies": {
55
+ "@gltf-transform/core": "^4.5.1",
56
+ "@gltf-transform/extensions": "^4.5.1",
57
+ "ktx-parse": "^1.1.0",
58
+ "ndarray": "^1.0.19",
59
+ "ndarray-lanczos": "^0.3.0",
60
+ "ndarray-pixels": "^5.2.0"
61
+ },
62
+ "funding": {
63
+ "url": "https://github.com/sponsors/donmccurdy"
64
+ }
65
+ },
66
+ "node_modules/@img/colour": {
67
+ "version": "1.1.0",
68
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
69
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
70
+ "license": "MIT",
71
+ "engines": {
72
+ "node": ">=18"
73
+ }
74
+ },
75
+ "node_modules/@img/sharp-darwin-arm64": {
76
+ "version": "0.35.5",
77
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz",
78
+ "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==",
79
+ "cpu": [
80
+ "arm64"
81
+ ],
82
+ "license": "Apache-2.0",
83
+ "optional": true,
84
+ "os": [
85
+ "darwin"
86
+ ],
87
+ "engines": {
88
+ "node": ">=20.9.0"
89
+ },
90
+ "funding": {
91
+ "url": "https://opencollective.com/libvips"
92
+ },
93
+ "optionalDependencies": {
94
+ "@img/sharp-libvips-darwin-arm64": "1.3.4"
95
+ }
96
+ },
97
+ "node_modules/@img/sharp-darwin-x64": {
98
+ "version": "0.35.5",
99
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz",
100
+ "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==",
101
+ "cpu": [
102
+ "x64"
103
+ ],
104
+ "license": "Apache-2.0",
105
+ "optional": true,
106
+ "os": [
107
+ "darwin"
108
+ ],
109
+ "engines": {
110
+ "node": ">=20.9.0"
111
+ },
112
+ "funding": {
113
+ "url": "https://opencollective.com/libvips"
114
+ },
115
+ "optionalDependencies": {
116
+ "@img/sharp-libvips-darwin-x64": "1.3.4"
117
+ }
118
+ },
119
+ "node_modules/@img/sharp-freebsd-wasm32": {
120
+ "version": "0.35.5",
121
+ "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz",
122
+ "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==",
123
+ "license": "Apache-2.0",
124
+ "optional": true,
125
+ "os": [
126
+ "freebsd"
127
+ ],
128
+ "dependencies": {
129
+ "@img/sharp-wasm32": "0.35.5"
130
+ },
131
+ "engines": {
132
+ "node": ">=20.9.0"
133
+ },
134
+ "funding": {
135
+ "url": "https://opencollective.com/libvips"
136
+ }
137
+ },
138
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
139
+ "version": "1.3.4",
140
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz",
141
+ "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==",
142
+ "cpu": [
143
+ "arm64"
144
+ ],
145
+ "license": "LGPL-3.0-or-later",
146
+ "optional": true,
147
+ "os": [
148
+ "darwin"
149
+ ],
150
+ "funding": {
151
+ "url": "https://opencollective.com/libvips"
152
+ }
153
+ },
154
+ "node_modules/@img/sharp-libvips-darwin-x64": {
155
+ "version": "1.3.4",
156
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz",
157
+ "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==",
158
+ "cpu": [
159
+ "x64"
160
+ ],
161
+ "license": "LGPL-3.0-or-later",
162
+ "optional": true,
163
+ "os": [
164
+ "darwin"
165
+ ],
166
+ "funding": {
167
+ "url": "https://opencollective.com/libvips"
168
+ }
169
+ },
170
+ "node_modules/@img/sharp-libvips-linux-arm": {
171
+ "version": "1.3.4",
172
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz",
173
+ "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==",
174
+ "cpu": [
175
+ "arm"
176
+ ],
177
+ "libc": [
178
+ "glibc"
179
+ ],
180
+ "license": "LGPL-3.0-or-later",
181
+ "optional": true,
182
+ "os": [
183
+ "linux"
184
+ ],
185
+ "funding": {
186
+ "url": "https://opencollective.com/libvips"
187
+ }
188
+ },
189
+ "node_modules/@img/sharp-libvips-linux-arm64": {
190
+ "version": "1.3.4",
191
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz",
192
+ "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==",
193
+ "cpu": [
194
+ "arm64"
195
+ ],
196
+ "libc": [
197
+ "glibc"
198
+ ],
199
+ "license": "LGPL-3.0-or-later",
200
+ "optional": true,
201
+ "os": [
202
+ "linux"
203
+ ],
204
+ "funding": {
205
+ "url": "https://opencollective.com/libvips"
206
+ }
207
+ },
208
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
209
+ "version": "1.3.4",
210
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz",
211
+ "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==",
212
+ "cpu": [
213
+ "ppc64"
214
+ ],
215
+ "libc": [
216
+ "glibc"
217
+ ],
218
+ "license": "LGPL-3.0-or-later",
219
+ "optional": true,
220
+ "os": [
221
+ "linux"
222
+ ],
223
+ "funding": {
224
+ "url": "https://opencollective.com/libvips"
225
+ }
226
+ },
227
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
228
+ "version": "1.3.4",
229
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz",
230
+ "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==",
231
+ "cpu": [
232
+ "riscv64"
233
+ ],
234
+ "libc": [
235
+ "glibc"
236
+ ],
237
+ "license": "LGPL-3.0-or-later",
238
+ "optional": true,
239
+ "os": [
240
+ "linux"
241
+ ],
242
+ "funding": {
243
+ "url": "https://opencollective.com/libvips"
244
+ }
245
+ },
246
+ "node_modules/@img/sharp-libvips-linux-s390x": {
247
+ "version": "1.3.4",
248
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz",
249
+ "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==",
250
+ "cpu": [
251
+ "s390x"
252
+ ],
253
+ "libc": [
254
+ "glibc"
255
+ ],
256
+ "license": "LGPL-3.0-or-later",
257
+ "optional": true,
258
+ "os": [
259
+ "linux"
260
+ ],
261
+ "funding": {
262
+ "url": "https://opencollective.com/libvips"
263
+ }
264
+ },
265
+ "node_modules/@img/sharp-libvips-linux-x64": {
266
+ "version": "1.3.4",
267
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz",
268
+ "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==",
269
+ "cpu": [
270
+ "x64"
271
+ ],
272
+ "libc": [
273
+ "glibc"
274
+ ],
275
+ "license": "LGPL-3.0-or-later",
276
+ "optional": true,
277
+ "os": [
278
+ "linux"
279
+ ],
280
+ "funding": {
281
+ "url": "https://opencollective.com/libvips"
282
+ }
283
+ },
284
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
285
+ "version": "1.3.4",
286
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz",
287
+ "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==",
288
+ "cpu": [
289
+ "arm64"
290
+ ],
291
+ "libc": [
292
+ "musl"
293
+ ],
294
+ "license": "LGPL-3.0-or-later",
295
+ "optional": true,
296
+ "os": [
297
+ "linux"
298
+ ],
299
+ "funding": {
300
+ "url": "https://opencollective.com/libvips"
301
+ }
302
+ },
303
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
304
+ "version": "1.3.4",
305
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz",
306
+ "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==",
307
+ "cpu": [
308
+ "x64"
309
+ ],
310
+ "libc": [
311
+ "musl"
312
+ ],
313
+ "license": "LGPL-3.0-or-later",
314
+ "optional": true,
315
+ "os": [
316
+ "linux"
317
+ ],
318
+ "funding": {
319
+ "url": "https://opencollective.com/libvips"
320
+ }
321
+ },
322
+ "node_modules/@img/sharp-linux-arm": {
323
+ "version": "0.35.5",
324
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz",
325
+ "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==",
326
+ "cpu": [
327
+ "arm"
328
+ ],
329
+ "libc": [
330
+ "glibc"
331
+ ],
332
+ "license": "Apache-2.0",
333
+ "optional": true,
334
+ "os": [
335
+ "linux"
336
+ ],
337
+ "engines": {
338
+ "node": ">=20.9.0"
339
+ },
340
+ "funding": {
341
+ "url": "https://opencollective.com/libvips"
342
+ },
343
+ "optionalDependencies": {
344
+ "@img/sharp-libvips-linux-arm": "1.3.4"
345
+ }
346
+ },
347
+ "node_modules/@img/sharp-linux-arm64": {
348
+ "version": "0.35.5",
349
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz",
350
+ "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==",
351
+ "cpu": [
352
+ "arm64"
353
+ ],
354
+ "libc": [
355
+ "glibc"
356
+ ],
357
+ "license": "Apache-2.0",
358
+ "optional": true,
359
+ "os": [
360
+ "linux"
361
+ ],
362
+ "engines": {
363
+ "node": ">=20.9.0"
364
+ },
365
+ "funding": {
366
+ "url": "https://opencollective.com/libvips"
367
+ },
368
+ "optionalDependencies": {
369
+ "@img/sharp-libvips-linux-arm64": "1.3.4"
370
+ }
371
+ },
372
+ "node_modules/@img/sharp-linux-ppc64": {
373
+ "version": "0.35.5",
374
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz",
375
+ "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==",
376
+ "cpu": [
377
+ "ppc64"
378
+ ],
379
+ "libc": [
380
+ "glibc"
381
+ ],
382
+ "license": "Apache-2.0",
383
+ "optional": true,
384
+ "os": [
385
+ "linux"
386
+ ],
387
+ "engines": {
388
+ "node": ">=20.9.0"
389
+ },
390
+ "funding": {
391
+ "url": "https://opencollective.com/libvips"
392
+ },
393
+ "optionalDependencies": {
394
+ "@img/sharp-libvips-linux-ppc64": "1.3.4"
395
+ }
396
+ },
397
+ "node_modules/@img/sharp-linux-riscv64": {
398
+ "version": "0.35.5",
399
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz",
400
+ "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==",
401
+ "cpu": [
402
+ "riscv64"
403
+ ],
404
+ "libc": [
405
+ "glibc"
406
+ ],
407
+ "license": "Apache-2.0",
408
+ "optional": true,
409
+ "os": [
410
+ "linux"
411
+ ],
412
+ "engines": {
413
+ "node": ">=20.9.0"
414
+ },
415
+ "funding": {
416
+ "url": "https://opencollective.com/libvips"
417
+ },
418
+ "optionalDependencies": {
419
+ "@img/sharp-libvips-linux-riscv64": "1.3.4"
420
+ }
421
+ },
422
+ "node_modules/@img/sharp-linux-s390x": {
423
+ "version": "0.35.5",
424
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz",
425
+ "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==",
426
+ "cpu": [
427
+ "s390x"
428
+ ],
429
+ "libc": [
430
+ "glibc"
431
+ ],
432
+ "license": "Apache-2.0",
433
+ "optional": true,
434
+ "os": [
435
+ "linux"
436
+ ],
437
+ "engines": {
438
+ "node": ">=20.9.0"
439
+ },
440
+ "funding": {
441
+ "url": "https://opencollective.com/libvips"
442
+ },
443
+ "optionalDependencies": {
444
+ "@img/sharp-libvips-linux-s390x": "1.3.4"
445
+ }
446
+ },
447
+ "node_modules/@img/sharp-linux-x64": {
448
+ "version": "0.35.5",
449
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz",
450
+ "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==",
451
+ "cpu": [
452
+ "x64"
453
+ ],
454
+ "libc": [
455
+ "glibc"
456
+ ],
457
+ "license": "Apache-2.0",
458
+ "optional": true,
459
+ "os": [
460
+ "linux"
461
+ ],
462
+ "engines": {
463
+ "node": ">=20.9.0"
464
+ },
465
+ "funding": {
466
+ "url": "https://opencollective.com/libvips"
467
+ },
468
+ "optionalDependencies": {
469
+ "@img/sharp-libvips-linux-x64": "1.3.4"
470
+ }
471
+ },
472
+ "node_modules/@img/sharp-linuxmusl-arm64": {
473
+ "version": "0.35.5",
474
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz",
475
+ "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==",
476
+ "cpu": [
477
+ "arm64"
478
+ ],
479
+ "libc": [
480
+ "musl"
481
+ ],
482
+ "license": "Apache-2.0",
483
+ "optional": true,
484
+ "os": [
485
+ "linux"
486
+ ],
487
+ "engines": {
488
+ "node": ">=20.9.0"
489
+ },
490
+ "funding": {
491
+ "url": "https://opencollective.com/libvips"
492
+ },
493
+ "optionalDependencies": {
494
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.4"
495
+ }
496
+ },
497
+ "node_modules/@img/sharp-linuxmusl-x64": {
498
+ "version": "0.35.5",
499
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz",
500
+ "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==",
501
+ "cpu": [
502
+ "x64"
503
+ ],
504
+ "libc": [
505
+ "musl"
506
+ ],
507
+ "license": "Apache-2.0",
508
+ "optional": true,
509
+ "os": [
510
+ "linux"
511
+ ],
512
+ "engines": {
513
+ "node": ">=20.9.0"
514
+ },
515
+ "funding": {
516
+ "url": "https://opencollective.com/libvips"
517
+ },
518
+ "optionalDependencies": {
519
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.4"
520
+ }
521
+ },
522
+ "node_modules/@img/sharp-wasm32": {
523
+ "version": "0.35.5",
524
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz",
525
+ "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==",
526
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
527
+ "optional": true,
528
+ "dependencies": {
529
+ "@emnapi/runtime": "^1.11.3"
530
+ },
531
+ "engines": {
532
+ "node": ">=20.9.0"
533
+ },
534
+ "funding": {
535
+ "url": "https://opencollective.com/libvips"
536
+ }
537
+ },
538
+ "node_modules/@img/sharp-webcontainers-wasm32": {
539
+ "version": "0.35.5",
540
+ "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz",
541
+ "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==",
542
+ "cpu": [
543
+ "wasm32"
544
+ ],
545
+ "license": "Apache-2.0",
546
+ "optional": true,
547
+ "dependencies": {
548
+ "@img/sharp-wasm32": "0.35.5"
549
+ },
550
+ "engines": {
551
+ "node": ">=20.9.0"
552
+ },
553
+ "funding": {
554
+ "url": "https://opencollective.com/libvips"
555
+ }
556
+ },
557
+ "node_modules/@img/sharp-win32-arm64": {
558
+ "version": "0.35.5",
559
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz",
560
+ "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==",
561
+ "cpu": [
562
+ "arm64"
563
+ ],
564
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
565
+ "optional": true,
566
+ "os": [
567
+ "win32"
568
+ ],
569
+ "engines": {
570
+ "node": ">=20.9.0"
571
+ },
572
+ "funding": {
573
+ "url": "https://opencollective.com/libvips"
574
+ }
575
+ },
576
+ "node_modules/@img/sharp-win32-ia32": {
577
+ "version": "0.35.5",
578
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz",
579
+ "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==",
580
+ "cpu": [
581
+ "ia32"
582
+ ],
583
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
584
+ "optional": true,
585
+ "os": [
586
+ "win32"
587
+ ],
588
+ "engines": {
589
+ "node": "^20.9.0"
590
+ },
591
+ "funding": {
592
+ "url": "https://opencollective.com/libvips"
593
+ }
594
+ },
595
+ "node_modules/@img/sharp-win32-x64": {
596
+ "version": "0.35.5",
597
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz",
598
+ "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==",
599
+ "cpu": [
600
+ "x64"
601
+ ],
602
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
603
+ "optional": true,
604
+ "os": [
605
+ "win32"
606
+ ],
607
+ "engines": {
608
+ "node": ">=20.9.0"
609
+ },
610
+ "funding": {
611
+ "url": "https://opencollective.com/libvips"
612
+ }
613
+ },
614
+ "node_modules/@types/ndarray": {
615
+ "version": "1.1.0",
616
+ "resolved": "https://registry.npmjs.org/@types/ndarray/-/ndarray-1.1.0.tgz",
617
+ "integrity": "sha512-go+C6vHBUuO9jH6Tj6qEZ63JEGoMSxFdmd+6QAtthGD6xumBfIz3RbNDDs2aoRVLf9naH8lk5jnWy2UHHKgYQQ==",
618
+ "license": "MIT"
619
+ },
620
+ "node_modules/cwise-compiler": {
621
+ "version": "1.1.3",
622
+ "resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz",
623
+ "integrity": "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==",
624
+ "license": "MIT",
625
+ "dependencies": {
626
+ "uniq": "^1.0.0"
627
+ }
628
+ },
629
+ "node_modules/detect-libc": {
630
+ "version": "2.1.2",
631
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
632
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
633
+ "license": "Apache-2.0",
634
+ "engines": {
635
+ "node": ">=8"
636
+ }
637
+ },
638
+ "node_modules/iota-array": {
639
+ "version": "1.0.0",
640
+ "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz",
641
+ "integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==",
642
+ "license": "MIT"
643
+ },
644
+ "node_modules/is-buffer": {
645
+ "version": "1.1.6",
646
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
647
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
648
+ "license": "MIT"
649
+ },
650
+ "node_modules/ktx-parse": {
651
+ "version": "1.1.0",
652
+ "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-1.1.0.tgz",
653
+ "integrity": "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ==",
654
+ "license": "MIT"
655
+ },
656
+ "node_modules/meshoptimizer": {
657
+ "version": "1.3.0",
658
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.3.0.tgz",
659
+ "integrity": "sha512-2oZqMAcEEiCtXWUjIGtZ71oPx1B0ZxYGTqIKsXB1wzWFhleNsCFxQuUJzgCV706bRcwPWtMGf1odmQvAcqKkIw==",
660
+ "license": "MIT"
661
+ },
662
+ "node_modules/ndarray": {
663
+ "version": "1.1.1",
664
+ "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.1.1.tgz",
665
+ "integrity": "sha512-AsE8w/M2MS+0pD3nP5qbRRtJFeJJJaJ+qXZaZr0XjVGXhy6LIj2TERTlkpdK2AI3WufoOP+pwPk3q2ilAJcZ9g==",
666
+ "license": "MIT",
667
+ "dependencies": {
668
+ "iota-array": "^1.0.0",
669
+ "is-buffer": "^1.0.2"
670
+ }
671
+ },
672
+ "node_modules/ndarray-lanczos": {
673
+ "version": "0.3.0",
674
+ "resolved": "https://registry.npmjs.org/ndarray-lanczos/-/ndarray-lanczos-0.3.0.tgz",
675
+ "integrity": "sha512-5kBmmG3Zvyj77qxIAC4QFLKuYdDIBJwCG+DukT6jQHNa1Ft74/hPH1z5mbQXeHBt8yvGPBGVrr3wEOdJPYYZYg==",
676
+ "license": "MIT",
677
+ "dependencies": {
678
+ "@types/ndarray": "^1.0.11",
679
+ "ndarray": "^1.0.19"
680
+ }
681
+ },
682
+ "node_modules/ndarray-ops": {
683
+ "version": "1.2.2",
684
+ "resolved": "https://registry.npmjs.org/ndarray-ops/-/ndarray-ops-1.2.2.tgz",
685
+ "integrity": "sha512-BppWAFRjMYF7N/r6Ie51q6D4fs0iiGmeXIACKY66fLpnwIui3Wc3CXiD/30mgLbDjPpSLrsqcp3Z62+IcHZsDw==",
686
+ "license": "MIT",
687
+ "dependencies": {
688
+ "cwise-compiler": "^1.0.0"
689
+ }
690
+ },
691
+ "node_modules/ndarray-pixels": {
692
+ "version": "5.2.0",
693
+ "resolved": "https://registry.npmjs.org/ndarray-pixels/-/ndarray-pixels-5.2.0.tgz",
694
+ "integrity": "sha512-lTh4tFKziAatVTa9crIsidUyn+lqujVOQpzfdBWvdFu2wo9Uo6z261lVX7SgMyP89xGmj3TMTPbbxl9YDnV4SA==",
695
+ "license": "MIT",
696
+ "dependencies": {
697
+ "@types/ndarray": "^1.0.14",
698
+ "ndarray": "^1.0.19",
699
+ "ndarray-ops": "^1.2.2",
700
+ "sharp": "^0.35.0"
701
+ }
702
+ },
703
+ "node_modules/property-graph": {
704
+ "version": "4.1.0",
705
+ "resolved": "https://registry.npmjs.org/property-graph/-/property-graph-4.1.0.tgz",
706
+ "integrity": "sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ==",
707
+ "license": "MIT"
708
+ },
709
+ "node_modules/semver": {
710
+ "version": "7.8.6",
711
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz",
712
+ "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==",
713
+ "license": "ISC",
714
+ "bin": {
715
+ "semver": "bin/semver.js"
716
+ },
717
+ "engines": {
718
+ "node": ">=10"
719
+ }
720
+ },
721
+ "node_modules/sharp": {
722
+ "version": "0.35.5",
723
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz",
724
+ "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==",
725
+ "license": "Apache-2.0",
726
+ "dependencies": {
727
+ "@img/colour": "^1.1.0",
728
+ "detect-libc": "^2.1.2",
729
+ "semver": "^7.8.5"
730
+ },
731
+ "engines": {
732
+ "node": ">=20.9.0"
733
+ },
734
+ "funding": {
735
+ "url": "https://opencollective.com/libvips"
736
+ },
737
+ "optionalDependencies": {
738
+ "@img/sharp-darwin-arm64": "0.35.5",
739
+ "@img/sharp-darwin-x64": "0.35.5",
740
+ "@img/sharp-freebsd-wasm32": "0.35.5",
741
+ "@img/sharp-libvips-darwin-arm64": "1.3.4",
742
+ "@img/sharp-libvips-darwin-x64": "1.3.4",
743
+ "@img/sharp-libvips-linux-arm": "1.3.4",
744
+ "@img/sharp-libvips-linux-arm64": "1.3.4",
745
+ "@img/sharp-libvips-linux-ppc64": "1.3.4",
746
+ "@img/sharp-libvips-linux-riscv64": "1.3.4",
747
+ "@img/sharp-libvips-linux-s390x": "1.3.4",
748
+ "@img/sharp-libvips-linux-x64": "1.3.4",
749
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.4",
750
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.4",
751
+ "@img/sharp-linux-arm": "0.35.5",
752
+ "@img/sharp-linux-arm64": "0.35.5",
753
+ "@img/sharp-linux-ppc64": "0.35.5",
754
+ "@img/sharp-linux-riscv64": "0.35.5",
755
+ "@img/sharp-linux-s390x": "0.35.5",
756
+ "@img/sharp-linux-x64": "0.35.5",
757
+ "@img/sharp-linuxmusl-arm64": "0.35.5",
758
+ "@img/sharp-linuxmusl-x64": "0.35.5",
759
+ "@img/sharp-webcontainers-wasm32": "0.35.5",
760
+ "@img/sharp-win32-arm64": "0.35.5",
761
+ "@img/sharp-win32-ia32": "0.35.5",
762
+ "@img/sharp-win32-x64": "0.35.5"
763
+ },
764
+ "peerDependenciesMeta": {
765
+ "@types/node": {
766
+ "optional": true
767
+ }
768
+ }
769
+ },
770
+ "node_modules/tslib": {
771
+ "version": "2.9.1",
772
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz",
773
+ "integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==",
774
+ "license": "0BSD",
775
+ "optional": true
776
+ },
777
+ "node_modules/uniq": {
778
+ "version": "1.0.1",
779
+ "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
780
+ "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==",
781
+ "license": "MIT"
782
+ }
783
+ }
784
+ }
recipe/package.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"private":true,"type":"module","dependencies":{"@gltf-transform/core":"4.5.1","@gltf-transform/extensions":"4.5.1","@gltf-transform/functions":"4.5.1","meshoptimizer":"1.3.0"}}
recipe/pipeline.py ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ABO mesh-proxy continuation of Forma3D Tiny 2. No large teacher models run."""
2
+ import argparse
3
+ import fcntl
4
+ import getpass
5
+ import hashlib
6
+ import importlib.util
7
+ import importlib.metadata
8
+ import json
9
+ import math
10
+ from pathlib import Path
11
+ import random
12
+ import re
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+ import time
17
+
18
+ BASE = 'Lucasgitguy-co/forma3d-tiny-2'
19
+ BASE_REV = '1648582b07a0f7f9d482a8c0f4cf89e6ecd86972'
20
+ DATA = 'codepawl/abo-glb'
21
+ DATA_REV = 'c7c8293fed3b7ab122ef5d7896521a187e0962d7'
22
+ DEST = 'Lucasgitguy-co/forma3d-tiny-3'
23
+ HERE = Path(__file__).resolve().parent
24
+
25
+
26
+ def dump(path, value):
27
+ path = Path(path)
28
+ tmp = path.with_suffix(path.suffix + '.tmp')
29
+ tmp.write_text(json.dumps(value, indent=2, allow_nan=False) + '\n')
30
+ tmp.replace(path)
31
+
32
+
33
+ def sha(path):
34
+ h = hashlib.sha256()
35
+ with Path(path).open('rb') as f:
36
+ for chunk in iter(lambda: f.read(1024 * 1024), b''):
37
+ h.update(chunk)
38
+ return h.hexdigest()
39
+
40
+
41
+ def read(path):
42
+ return json.loads(Path(path).read_text())
43
+
44
+
45
+ def log(**kw):
46
+ print(json.dumps(kw, allow_nan=False), flush=True)
47
+
48
+
49
+ def authenticate():
50
+ from huggingface_hub import HfApi, get_token, login
51
+ token = get_token()
52
+ if not token:
53
+ print('Enter a Hugging Face WRITE token for Lucasgitguy-co. Input is hidden.\n'
54
+ 'Create one at https://huggingface.co/settings/tokens', flush=True)
55
+ token = getpass.getpass('Hugging Face token: ')
56
+ login(token=token, add_to_git_credential=False)
57
+ api = HfApi(token=token)
58
+ identity = api.whoami()
59
+ owner = DEST.split('/')[0]
60
+ valid_owner = identity['name'].lower() == owner.lower()
61
+ valid_org = any(o['name'].lower() == owner.lower() and o.get('roleInOrg') in ('admin', 'write')
62
+ for o in identity.get('orgs', []))
63
+ if not (valid_owner or valid_org):
64
+ raise RuntimeError(f'This login cannot publish under {owner}. Use a token for that account.')
65
+ if identity.get('auth', {}).get('accessToken', {}).get('role') == 'read':
66
+ raise RuntimeError('This is a read-only token. Use a write token.')
67
+ print('Hugging Face login verified. No repository is created until evaluation passes.', flush=True)
68
+ return api
69
+
70
+
71
+ def upstream(root):
72
+ from huggingface_hub import snapshot_download
73
+ source = root / 'upstream'
74
+ snapshot_download(BASE, revision=BASE_REV, local_dir=source,
75
+ allow_patterns=['src/*', 'scripts/prepare_hf.py', 'model.pt', 'config.json',
76
+ 'pyproject.toml', 'LICENSE', 'NOTICE', 'THIRD_PARTY.md',
77
+ 'licenses/*', 'ATTRIBUTIONS.json', 'README.md'], max_workers=2)
78
+ sys.path.insert(0, str(source / 'src'))
79
+ spec = importlib.util.spec_from_file_location('base_prepare', source / 'scripts/prepare_hf.py')
80
+ mod = importlib.util.module_from_spec(spec)
81
+ spec.loader.exec_module(mod)
82
+ return source, mod
83
+
84
+
85
+ def prepare(root, source, helper, args):
86
+ import numpy as np
87
+ from huggingface_hub import hf_hub_download
88
+ from image3d_student.data import cache_sample, CachedDataset
89
+ from image3d_student.splats import Splats, preview
90
+ from image3d_student.engine import split_rows
91
+ folder = root / 'data'
92
+ folder.mkdir(exist_ok=True)
93
+ ready = folder / 'preparation.json'
94
+ if ready.exists():
95
+ for split in ('train', 'validation', 'test'):
96
+ CachedDataset(folder / f'{split}.jsonl')
97
+ print('Reusing verified prepared data.', flush=True)
98
+ return folder
99
+ metadata = hf_hub_download(DATA, 'metadata.jsonl', repo_type='dataset', revision=DATA_REV,
100
+ local_dir=root / 'metadata')
101
+ license_path = hf_hub_download(DATA, 'LICENSE-CC-BY-4.0.txt', repo_type='dataset',
102
+ revision=DATA_REV, local_dir=root / 'metadata')
103
+ shutil.copy2(license_path, folder / 'LICENSE-ABO-CC-BY-4.0.txt')
104
+ records = [json.loads(line) for line in Path(metadata).read_text().splitlines() if line.strip()]
105
+ candidates = [r for r in records if 0 < r.get('triangles', 0) <= 200_000
106
+ and 0 < r.get('pack_bytes', 0) <= 8 * 1024**2
107
+ and re.fullmatch(r'[A-Za-z0-9_-]+', r['id'])]
108
+ candidates.sort(key=lambda r: r['id'])
109
+ random.Random(args.seed).shuffle(candidates)
110
+ progress = folder / 'progress.json'
111
+ state = read(progress) if progress.exists() else {'rows': [], 'sources': [], 'skipped': []}
112
+ rows, sources = state['rows'], state['sources']
113
+ completed = {s['asset_id'] for s in sources}
114
+ prior = read(source / 'ATTRIBUTIONS.json')
115
+ blocked_hashes = set()
116
+ def visit(x):
117
+ if isinstance(x, dict):
118
+ for k, v in x.items():
119
+ if k == 'sha256' and isinstance(v, str):
120
+ blocked_hashes.add(v)
121
+ visit(v)
122
+ elif isinstance(x, list):
123
+ for v in x:
124
+ visit(v)
125
+ visit(prior)
126
+ seen_hashes = {s['sha256'] for s in sources} | blocked_hashes
127
+ fetch = helper.Downloader(root / 'downloads', 8, 3072, 4)
128
+ for record in candidates:
129
+ if len(sources) >= args.objects:
130
+ break
131
+ uid = record['id']
132
+ if uid in completed:
133
+ continue
134
+ stage = folder / '_staging'
135
+ if stage.exists():
136
+ shutil.rmtree(stage)
137
+ stage.mkdir()
138
+ try:
139
+ filename = f'objects/{uid}/{uid}.glb'
140
+ raw = fetch.get(DATA, DATA_REV, filename)
141
+ digest = sha(raw)
142
+ if digest in seen_hashes:
143
+ continue
144
+ decoded = stage / 'decoded.glb'
145
+ subprocess.run(['node', str(HERE / 'decode.mjs'), str(raw), str(decoded)],
146
+ check=True, capture_output=True, text=True, timeout=120)
147
+ sample_seed = int(digest[:8], 16) ^ args.seed
148
+ splats, normalization = helper.mesh_splats(decoded, 4096, 16, sample_seed)
149
+ info = {'repo': DATA, 'revision': DATA_REV, 'path': filename, 'sha256': digest,
150
+ 'decoded_sha256': sha(decoded), 'asset_id': uid, 'object_id': 'abo-' + uid,
151
+ 'amazon_item_ids': record.get('amazon_item_ids', []),
152
+ 'title': record.get('name', uid), 'product_type': record.get('product_type'),
153
+ 'author': 'Amazon.com; Amazon Berkeley Objects contributors',
154
+ 'conversion': 'CodePawl GLB conversion; decompressed, normalized and sampled for training',
155
+ 'license': 'cc-by-4.0', 'license_url': 'https://creativecommons.org/licenses/by/4.0/',
156
+ 'source_url': f'https://huggingface.co/datasets/{DATA}/blob/{DATA_REV}/{filename}'}
157
+ new_rows = []
158
+ for view in range(4):
159
+ yaw = 2 * np.pi * view / 4
160
+ c, s = np.cos(yaw), np.sin(yaw)
161
+ rotation = np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]], np.float32)
162
+ rotated = Splats(splats.xyz @ rotation.T, splats.rgb, splats.scale,
163
+ splats.quat, splats.opacity)
164
+ picture = preview(rotated, size=128)
165
+ if picture.getbbox() is None:
166
+ raise ValueError('Invisible proxy render')
167
+ picture.save(stage / 'input.png')
168
+ new_rows.append(cache_sample(stage, f'abo-{uid}-v{view}', stage / 'input.png',
169
+ rotated, 16, source='mesh-proxy:' + DATA,
170
+ extra={**info, 'normalization_matrix': normalization, 'yaw_radians': float(yaw),
171
+ 'render': 'orthographic Gaussian proxy on black; camera +z',
172
+ 'target': 'mesh-derived isotropic surface samples; no pretrained teacher'}))
173
+ for name in ('images', 'targets'):
174
+ (folder / name).mkdir(exist_ok=True)
175
+ for p in (stage / name).iterdir():
176
+ p.replace(folder / name / p.name)
177
+ rows.extend(new_rows)
178
+ sources.append(info)
179
+ seen_hashes.add(digest)
180
+ completed.add(uid)
181
+ dump(progress, state)
182
+ log(prepared_objects=len(sources), requested=args.objects, asset=uid)
183
+ except RuntimeError:
184
+ raise
185
+ except Exception as exc:
186
+ error = str(exc)[:300]
187
+ if isinstance(exc, subprocess.CalledProcessError):
188
+ error = exc.stderr[-500:]
189
+ state['skipped'].append({'id': uid, 'error': error})
190
+ dump(progress, state)
191
+ log(skipped=uid, error=error)
192
+ if len(state['skipped']) >= 20 and not sources:
193
+ raise RuntimeError('Twenty objects failed. Inspect data/progress.json before continuing.') from exc
194
+ finally:
195
+ shutil.rmtree(stage, ignore_errors=True)
196
+ if len(sources) < args.objects:
197
+ raise RuntimeError(f'Only {len(sources)} usable objects; need {args.objects}. Progress is saved.')
198
+ # Connect duplicate objects, shared Amazon item IDs and exact rendered data before splitting.
199
+ parent = {s['object_id']: s['object_id'] for s in sources}
200
+ def find(x):
201
+ while parent[x] != x:
202
+ parent[x] = parent[parent[x]]
203
+ x = parent[x]
204
+ return x
205
+ def union(a, b):
206
+ a, b = find(a), find(b)
207
+ parent[max(a, b)] = min(a, b)
208
+ identities = {}
209
+ for info in sources:
210
+ for key in [('mesh', info['sha256'])] + [('amazon', a) for a in info['amazon_item_ids']]:
211
+ if key in identities:
212
+ union(info['object_id'], identities[key])
213
+ identities[key] = info['object_id']
214
+ for row in rows:
215
+ for field in ('image_sha256', 'target_sha256'):
216
+ key = (field, row[field])
217
+ if key in identities:
218
+ union(row['metadata']['object_id'], identities[key])
219
+ identities[key] = row['metadata']['object_id']
220
+ for row in rows:
221
+ row['metadata']['object_id'] = find(row['metadata']['object_id'])
222
+ for info in sources:
223
+ info['object_id'] = find(info['object_id'])
224
+ development, test = split_rows(rows, .15, args.seed + 1)
225
+ training, validation = split_rows(development, .20, args.seed + 2)
226
+ splits = {'train': training, 'validation': validation, 'test': test}
227
+ for name, items in splits.items():
228
+ (folder / f'{name}.jsonl').write_text(''.join(json.dumps(r) + '\n' for r in items))
229
+ CachedDataset(folder / f'{name}.jsonl')
230
+ dump(folder / 'sources.json', sources)
231
+ dump(ready, {'dataset': DATA, 'revision': DATA_REV, 'objects': len(sources), 'views': len(rows),
232
+ 'groups': {k: len({r['metadata']['object_id'] for r in v}) for k, v in splits.items()},
233
+ 'split_views': {k: len(v) for k, v in splits.items()},
234
+ 'manifest_sha256': {k: sha(folder / f'{k}.jsonl') for k in splits},
235
+ 'grouping': 'object IDs, shared Amazon item IDs, exact mesh/image/target hashes',
236
+ 'limitations': 'Near duplicates can remain. Cross-dataset overlap with ancestor training '
237
+ 'is not fully known; only recorded exact source hashes were excluded.'})
238
+ return folder
239
+
240
+
241
+ def cpu_tree(value):
242
+ import torch
243
+ if isinstance(value, torch.Tensor):
244
+ return value.detach().cpu()
245
+ if isinstance(value, dict):
246
+ return {k: cpu_tree(v) for k, v in value.items()}
247
+ if isinstance(value, list):
248
+ return [cpu_tree(v) for v in value]
249
+ if isinstance(value, tuple):
250
+ return tuple(cpu_tree(v) for v in value)
251
+ return value
252
+
253
+
254
+ def train(root, source, folder, args):
255
+ import torch
256
+ from torch.utils.data import DataLoader, default_collate
257
+ from image3d_student.data import CachedDataset
258
+ from image3d_student.model import Student, distillation_loss
259
+ from image3d_student.engine import device_for, evaluate, on_device
260
+ device = device_for(args.device)
261
+ torch.manual_seed(args.seed)
262
+ run = root / 'training'
263
+ run.mkdir(exist_ok=True)
264
+ train_data = CachedDataset(folder / 'train.jsonl')
265
+ val_data = CachedDataset(folder / 'validation.jsonl')
266
+ val_loader = DataLoader(val_data, batch_size=1, num_workers=0)
267
+ base = torch.load(source / 'model.pt', map_location='cpu', weights_only=True)
268
+ if base['config'] != {'grid': 16, 'width': 64} or base['format'] != 1:
269
+ raise RuntimeError('Unexpected base model architecture.')
270
+ model = Student(**base['config']).to(device)
271
+ model.load_state_dict(base['model'], strict=True)
272
+ optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=1e-4)
273
+ rng = random.Random(args.seed)
274
+ meta = {'base_model': BASE, 'base_revision': BASE_REV, 'base_sha256': sha(source / 'model.pt'),
275
+ 'parameters': sum(p.numel() for p in model.parameters()), 'device': str(device),
276
+ 'torch': str(torch.__version__), 'dataset': DATA, 'dataset_revision': DATA_REV,
277
+ 'steps_requested': args.steps, 'batch_size': args.batch_size, 'learning_rate': args.lr,
278
+ 'seed': args.seed, 'teacher_models_run': [], 'contains_synthetic': False,
279
+ 'rendered_mesh_proxies': True, 'optimizer': 'fresh AdamW, resumed on interruption',
280
+ 'data': read(folder / 'preparation.json')}
281
+ step, best_step = 0, 0
282
+ if (run / 'last.pt').exists():
283
+ last = torch.load(run / 'last.pt', map_location='cpu', weights_only=True)
284
+ model.load_state_dict(last['model'])
285
+ optimizer.load_state_dict(last['optimizer'])
286
+ rng.setstate(last['python_rng'])
287
+ step, best_step, best_loss = last['step'], last['best_step'], last['best_loss']
288
+ meta = last['metadata']
289
+ log(resuming_from_step=step)
290
+ else:
291
+ meta['baseline_validation'] = evaluate(model, val_loader, device)
292
+ best_loss = meta['baseline_validation']['loss']
293
+ if not math.isfinite(best_loss):
294
+ raise RuntimeError('Nonfinite baseline evaluation.')
295
+ dump(run / 'training.json', meta)
296
+ started = time.monotonic()
297
+ starting_step = step
298
+ def save(path, include_optimizer):
299
+ payload = {'format': 1, 'config': model.config, 'model': cpu_tree(model.state_dict()),
300
+ 'step': step, 'metadata': meta}
301
+ if include_optimizer:
302
+ payload.update(optimizer=cpu_tree(optimizer.state_dict()), python_rng=rng.getstate(),
303
+ best_step=best_step, best_loss=best_loss)
304
+ tmp = path.with_suffix('.tmp')
305
+ torch.save(payload, tmp)
306
+ tmp.replace(path)
307
+ try:
308
+ while step < args.steps:
309
+ images, target = default_collate([train_data[rng.randrange(len(train_data))]
310
+ for _ in range(args.batch_size)])
311
+ model.train()
312
+ optimizer.zero_grad(set_to_none=True)
313
+ prediction = model(images.to(device))
314
+ loss, _ = distillation_loss(prediction, on_device(target, device), model.grid)
315
+ if not torch.isfinite(loss):
316
+ raise RuntimeError('Nonfinite training loss; publication disabled.')
317
+ loss.backward()
318
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1., error_if_nonfinite=True)
319
+ optimizer.step()
320
+ step += 1
321
+ if step == 1 or step % 50 == 0:
322
+ log(step=step, total=args.steps, loss=float(loss.detach()),
323
+ elapsed_seconds=round(time.monotonic() - started),
324
+ steps_per_second=round((step - starting_step) / max(time.monotonic()-started, .001), 3))
325
+ if step % args.eval_every == 0 or step == args.steps:
326
+ metrics = evaluate(model, val_loader, device)
327
+ if not all(math.isfinite(v) for v in metrics.values()):
328
+ raise RuntimeError('Nonfinite validation metrics.')
329
+ if metrics['loss'] < best_loss:
330
+ best_loss, best_step = metrics['loss'], step
331
+ save(run / 'best.pt', False)
332
+ record = {'step': step, 'validation': metrics, 'best_step': best_step}
333
+ with (run / 'metrics.jsonl').open('a') as f:
334
+ f.write(json.dumps(record) + '\n')
335
+ log(**record)
336
+ save(run / 'last.pt', True)
337
+ finally:
338
+ if step:
339
+ save(run / 'last.pt', True)
340
+ if step != args.steps:
341
+ raise RuntimeError('Training incomplete; no publication.')
342
+ if not best_step:
343
+ raise RuntimeError('Validation did not improve over v2. Checkpoints are local; no publication.')
344
+ dump(run / 'completed.json', {'completed_steps': step, 'selected_step': best_step,
345
+ 'best_validation_loss': best_loss})
346
+ return run
347
+
348
+
349
+ def assess(root, source, folder, run, args):
350
+ import torch
351
+ from torch.utils.data import DataLoader
352
+ from image3d_student.data import CachedDataset
353
+ from image3d_student.model import Student
354
+ from image3d_student.engine import device_for, evaluate
355
+ device = device_for(args.device)
356
+ loader = DataLoader(CachedDataset(folder / 'test.jsonl'), batch_size=1, num_workers=0)
357
+ result = {}
358
+ for name, checkpoint in [('base_v2', source / 'model.pt'), ('candidate_v3', run / 'best.pt')]:
359
+ state = torch.load(checkpoint, map_location='cpu', weights_only=True)
360
+ model = Student(**state['config']).to(device)
361
+ model.load_state_dict(state['model'], strict=True)
362
+ metrics = evaluate(model, loader, device)
363
+ if not all(math.isfinite(v) for v in metrics.values()):
364
+ raise RuntimeError('Nonfinite test metrics.')
365
+ result[name] = {'sha256': sha(checkpoint), 'metrics': metrics}
366
+ del model
367
+ before, after = result['base_v2']['metrics'], result['candidate_v3']['metrics']
368
+ meta = read(run / 'training.json')
369
+ completed = read(run / 'completed.json')
370
+ gates = {'training_complete': completed['completed_steps'] == args.steps,
371
+ 'validation_loss_improved': completed['best_validation_loss'] < meta['baseline_validation']['loss'],
372
+ 'test_loss_improved_at_least_half_percent': after['loss'] <= before['loss'] * .995,
373
+ 'test_occupancy_iou_not_worse': after['occupancy_iou'] >= before['occupancy_iou'],
374
+ 'weights_changed': result['base_v2']['sha256'] != result['candidate_v3']['sha256']}
375
+ report = {**result, 'gates': gates, 'passed': all(gates.values()),
376
+ 'test_manifest_sha256': sha(folder / 'test.jsonl'),
377
+ 'measurement': 'mean per-view proxy loss and occupancy IoU, threshold 0.5, batch size 1',
378
+ 'limitation': 'Not evidence of natural-photo, mesh, texture or photorealistic rendering quality.'}
379
+ dump(root / 'evaluation.json', report)
380
+ log(evaluation=report)
381
+ return report
382
+
383
+
384
+ def package(root, source, folder, run, report):
385
+ release = root / 'release'
386
+ release.mkdir(exist_ok=True)
387
+ shutil.copy2(run / 'best.pt', release / 'model.pt')
388
+ for name in ('LICENSE', 'NOTICE', 'THIRD_PARTY.md', 'config.json'):
389
+ shutil.copy2(source / name, release / name)
390
+ for name in ('src', 'licenses'):
391
+ shutil.copytree(source / name, release / name, dirs_exist_ok=True,
392
+ ignore=shutil.ignore_patterns('__pycache__', '*.pyc'))
393
+ project = (source / 'pyproject.toml').read_text().replace('forma3d-tiny-2', 'forma3d-tiny-3')
394
+ project = project.replace('version = "0.3.0"', 'version = "0.4.0"')
395
+ (release / 'pyproject.toml').write_text(project)
396
+ shutil.copy2(folder / 'LICENSE-ABO-CC-BY-4.0.txt', release / 'licenses/LICENSE-ABO-CC-BY-4.0.txt')
397
+ for path in (run / 'training.json', run / 'completed.json', run / 'metrics.jsonl',
398
+ folder / 'preparation.json', root / 'evaluation.json', root / 'settings.json'):
399
+ shutil.copy2(path, release / path.name)
400
+ dump(release / 'ATTRIBUTIONS.json', {'inherited': read(source / 'ATTRIBUTIONS.json'),
401
+ 'abo_continuation': read(folder / 'sources.json')})
402
+ (release / 'splits').mkdir(exist_ok=True)
403
+ for name in ('train', 'validation', 'test'):
404
+ shutil.copy2(folder / f'{name}.jsonl', release / 'splits' / f'{name}.jsonl')
405
+ recipe = release / 'recipe'
406
+ recipe.mkdir(exist_ok=True)
407
+ for name in ('pipeline.py', 'decode.mjs', 'requirements.txt', 'package.json', 'package-lock.json', 'run.command'):
408
+ shutil.copy2(HERE / name, recipe / name)
409
+ inherited_notice = (release / 'NOTICE').read_text()
410
+ (release / 'NOTICE').write_text(inherited_notice + '\nABO continuation data: Amazon.com, Amazon Berkeley '
411
+ 'Objects contributors; converted to GLB by CodePawl. CC BY 4.0. Training preprocessing '
412
+ 'decompresses geometry, normalizes objects, samples surface colors and renders Gaussian proxies. '
413
+ 'See ATTRIBUTIONS.json and licenses/LICENSE-ABO-CC-BY-4.0.txt. No endorsement implied.\n')
414
+ base = report['base_v2']['metrics']
415
+ candidate = report['candidate_v3']['metrics']
416
+ prep = read(folder / 'preparation.json')
417
+ completed = read(run / 'completed.json')
418
+ (release / 'README.md').write_text(f'''---
419
+ license: apache-2.0
420
+ pipeline_tag: image-to-3d
421
+ base_model: {BASE}
422
+ base_model_relation: finetune
423
+ model_name: Forma3D Tiny 3
424
+ datasets:
425
+ - codepawl/abo-glb
426
+ tags:
427
+ - pytorch
428
+ - gaussian-splatting
429
+ - mesh-proxy-supervision
430
+ - research-prototype
431
+ ---
432
+ # Forma3D Tiny 3
433
+
434
+ Continued from [{BASE}](https://huggingface.co/{BASE}) at `{BASE_REV}`.
435
+ Same 306,321-parameter architecture, 16 cubed grid, width 64, maximum 4,096 Gaussian slots.
436
+ Fresh AdamW optimizer at the start of continuation. Completed {completed['completed_steps']} steps;
437
+ selected step {completed['selected_step']} by validation loss only.
438
+
439
+ ## Data and scope
440
+
441
+ {prep['objects']} ABO objects, {prep['views']} orthographic Gaussian-proxy images on black.
442
+ Data revision: `{DATA_REV}`. Targets are isotropic mesh surface samples, compressed to
443
+ one Gaussian per occupied grid cell. Texture surface colors are sampled; full PBR rendering
444
+ and differentiable rendering are not used. No natural-photo training is claimed.
445
+ MeshAnythingV2, Idol Diffusers, SHARP, Pixal3D, Idol Turbo and Qwen3.5-4B-3D-Coder
446
+ were **not run, merged, or distilled** in this Mac-only recipe.
447
+
448
+ Objects, shared Amazon item IDs and exact mesh/image/target hashes are grouped before splitting.
449
+ Split groups: {json.dumps(prep['groups'])}. Test data is excluded from training and checkpoint selection.
450
+ Near duplicates can remain; overlap with ancestor training datasets is not fully known.
451
+
452
+ ## Evaluation
453
+
454
+ Both checkpoints were evaluated on the identical held-out ABO proxy views.
455
+
456
+ | Metric | Starting v2 | Selected v3 |
457
+ |---|---:|---:|
458
+ | Mean test loss | {base['loss']:.6f} | {candidate['loss']:.6f} |
459
+ | Mean view occupancy IoU, threshold 0.5 | {base['occupancy_iou']:.6f} | {candidate['occupancy_iou']:.6f} |
460
+
461
+ These measurements do not establish natural-photo reconstruction, photorealism, mesh quality,
462
+ novel-view rendering quality or improvement on v2's original domains. No confidence intervals
463
+ or broad generalization claim. See evaluation.json, preparation.json and split manifests.
464
+
465
+ ## Use
466
+
467
+ With Python 3.11, install this repository using `pip install .`, then:
468
+
469
+ ```bash
470
+ image3d generate --checkpoint model.pt --image centered-object-on-black.png --out object.ply --device mps
471
+ image3d preview --ply object.ply --out preview.png
472
+ ```
473
+
474
+ Output is Gaussian PLY, not a textured GLB mesh. Inputs should match centered objects on black.
475
+ The model has no text prompt conditioning. Reproduction launcher and locked dependencies are in recipe/.
476
+
477
+ ## Attribution
478
+
479
+ Publisher source and weights: Apache 2.0. Retain inherited NOTICE, THIRD_PARTY.md and ATTRIBUTIONS.json.
480
+ ABO models, images and listing text: Amazon.com and the Amazon Berkeley Objects contributors,
481
+ CC BY 4.0; converted to GLB by CodePawl. The recipe further decompresses geometry,
482
+ normalizes coordinates, samples colored surfaces and renders proxy images.
483
+ See [ABO dataset](https://huggingface.co/datasets/codepawl/abo-glb),
484
+ ATTRIBUTIONS.json and licenses/LICENSE-ABO-CC-BY-4.0.txt. Raw assets are not bundled.
485
+ ''')
486
+ freeze = ''.join(sorted(f"{d.metadata['Name']}=={d.version}\n"
487
+ for d in importlib.metadata.distributions()))
488
+ (release / 'environment.txt').write_text(freeze)
489
+ sums = ''.join(f'{sha(p)} {p.relative_to(release)}\n' for p in sorted(release.rglob('*'))
490
+ if p.is_file() and p.name != 'SHA256SUMS')
491
+ (release / 'SHA256SUMS').write_text(sums)
492
+ return release
493
+
494
+
495
+ def publish(api, root, release):
496
+ from huggingface_hub import hf_hub_download
497
+ from huggingface_hub.errors import RepositoryNotFoundError, EntryNotFoundError
498
+ marker = root / 'publication.json'
499
+ expected = sha(release / 'model.pt')
500
+ try:
501
+ info = api.model_info(DEST)
502
+ except RepositoryNotFoundError:
503
+ info = None
504
+ if info is not None:
505
+ try:
506
+ remote = hf_hub_download(DEST, 'model.pt', revision=info.sha, token=api.token)
507
+ except EntryNotFoundError:
508
+ if not marker.exists():
509
+ raise RuntimeError(f'{DEST} already exists. Refusing to overwrite an unrelated repository.')
510
+ else:
511
+ if sha(remote) == expected:
512
+ print(f'Already published and verified: https://huggingface.co/{DEST}', flush=True)
513
+ return
514
+ raise RuntimeError(f'{DEST} already has different weights; refusing to overwrite them.')
515
+ else:
516
+ api.create_repo(DEST, repo_type='model', private=False, exist_ok=False)
517
+ dump(marker, {'repo_id': DEST, 'expected_model_sha256': expected})
518
+ commit = api.upload_folder(repo_id=DEST, repo_type='model', folder_path=release,
519
+ commit_message='Publish evaluated Forma3D Tiny 3 ABO continuation')
520
+ remote = hf_hub_download(DEST, 'model.pt', revision=commit.oid, token=api.token)
521
+ if sha(remote) != expected:
522
+ raise RuntimeError('Uploaded checkpoint verification failed.')
523
+ dump(marker, {'repo_id': DEST, 'model_sha256': expected, 'commit': commit.oid, 'verified': True})
524
+ print(f'Published and verified: https://huggingface.co/{DEST}', flush=True)
525
+
526
+
527
+ def main():
528
+ p = argparse.ArgumentParser(description=__doc__)
529
+ p.add_argument('--root', type=Path, default=HERE / 'run')
530
+ p.add_argument('--objects', type=int, default=512)
531
+ p.add_argument('--steps', type=int, default=6000)
532
+ p.add_argument('--batch-size', type=int, default=4)
533
+ p.add_argument('--lr', type=float, default=5e-5)
534
+ p.add_argument('--eval-every', type=int, default=250)
535
+ p.add_argument('--seed', type=int, default=44)
536
+ p.add_argument('--device', choices=['mps', 'cpu'], default='mps')
537
+ p.add_argument('--publish', action='store_true')
538
+ p.add_argument('--prepare-only', action='store_true')
539
+ args = p.parse_args()
540
+ if args.objects < 12 or min(args.steps, args.batch_size, args.eval_every) < 1 or args.lr <= 0:
541
+ p.error('Need at least 12 objects and positive training settings.')
542
+ if args.publish and (args.objects < 128 or args.steps < 1000 or args.device != 'mps'):
543
+ p.error('Publication requires >=128 objects, >=1000 steps and MPS; small runs are tests only.')
544
+ root = args.root.resolve()
545
+ root.mkdir(parents=True, exist_ok=True)
546
+ with (root / '.lock').open('w') as lock:
547
+ try:
548
+ fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
549
+ except BlockingIOError:
550
+ raise RuntimeError('Another run is already using this directory.')
551
+ settings = {k: v for k, v in vars(args).items() if k not in ('root', 'publish', 'prepare_only')}
552
+ settings.update(base_revision=BASE_REV, data_revision=DATA_REV, recipe_sha256=sha(__file__))
553
+ settings_path = root / 'settings.json'
554
+ if settings_path.exists() and read(settings_path) != settings:
555
+ raise RuntimeError('Run settings/code changed. Use --root with a new directory.')
556
+ dump(settings_path, settings)
557
+ if shutil.disk_usage(root).free < 8 * 1024**3:
558
+ raise RuntimeError('Need at least 8 GiB free disk space before starting.')
559
+ api = authenticate() if args.publish and not args.prepare_only else None
560
+ source, helper = upstream(root)
561
+ folder = prepare(root, source, helper, args)
562
+ if args.prepare_only:
563
+ return
564
+ run = train(root, source, folder, args)
565
+ report = assess(root, source, folder, run, args)
566
+ if not report['passed']:
567
+ print('Release checks did not pass. Results and checkpoints are saved locally; nothing published.')
568
+ sys.exit(2)
569
+ release = package(root, source, folder, run, report)
570
+ if args.publish:
571
+ publish(api, root, release)
572
+ else:
573
+ print(f'Local test only; no publication. Release package: {release}')
574
+
575
+
576
+ if __name__ == '__main__':
577
+ try:
578
+ main()
579
+ except KeyboardInterrupt:
580
+ print('\nInterrupted. Rerun the same command to continue from saved progress.', file=sys.stderr)
581
+ sys.exit(130)
recipe/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ torch==2.15.0
2
+ numpy==2.4.6
3
+ pillow==13.0.0
4
+ plyfile==1.1.5
5
+ trimesh==5.2.0
6
+ scipy==1.17.1
7
+ huggingface-hub==2.4.0
8
+ pyarrow==26.0.0
recipe/run.command ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ cd -- "$(dirname -- "$0")"
4
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
5
+ if [[ "$(uname -s)" != Darwin || "$(uname -m)" != arm64 ]]; then
6
+ echo 'Run this launcher in a native Apple Silicon macOS Terminal.'; exit 1
7
+ fi
8
+ if ! command -v uv >/dev/null || ! command -v node >/dev/null || ! command -v npm >/dev/null; then
9
+ echo 'Missing prerequisites. Install uv and Node.js, then rerun. With Homebrew: brew install uv node'
10
+ exit 1
11
+ fi
12
+ node -e 'if(Number(process.versions.node.split(".")[0])<20)process.exit(1)' || {
13
+ echo 'Node.js 20 or later is required.'; exit 1;
14
+ }
15
+ export UV_CACHE_DIR="$PWD/.cache/uv"
16
+ export HF_HOME="$PWD/.cache/huggingface"
17
+ export PYTHONUNBUFFERED=1
18
+ export OMP_NUM_THREADS=4
19
+ export PYTORCH_ENABLE_MPS_FALLBACK=1
20
+ uv venv --python 3.11 --allow-existing .venv
21
+ uv pip install --python .venv/bin/python -r requirements.txt
22
+ if [[ ! -d node_modules ]]; then
23
+ npm ci --ignore-scripts --no-audit --no-fund
24
+ fi
25
+ echo 'Preparing 512 ABO objects, training 6,000 steps, then evaluating and publishing if the release checks pass.'
26
+ echo 'Keep your Mac connected to power and its lid open. Rerun this same command after an interruption.'
27
+ exec caffeinate -i .venv/bin/python pipeline.py --publish "$@"
settings.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "objects": 512,
3
+ "steps": 6000,
4
+ "batch_size": 4,
5
+ "lr": 5e-05,
6
+ "eval_every": 250,
7
+ "seed": 44,
8
+ "device": "mps",
9
+ "base_revision": "1648582b07a0f7f9d482a8c0f4cf89e6ecd86972",
10
+ "data_revision": "c7c8293fed3b7ab122ef5d7896521a187e0962d7",
11
+ "recipe_sha256": "33ef603b3550a78eb982138540bcdbbca596d3b3f697368593a6d5ca3ff4b436"
12
+ }
splits/test.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
splits/train.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
splits/validation.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
src/image3d_student/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Lucasgitguy-co
3
+ """Research prototype; no pretrained general-purpose student weights are bundled."""
4
+
5
+ __version__ = "0.2.0"
src/image3d_student/cli.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Lucasgitguy-co
3
+ import argparse
4
+ import importlib.util
5
+ import json
6
+ import platform
7
+ import shutil
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ import torch
12
+
13
+
14
+ def parser():
15
+ p = argparse.ArgumentParser(description="Local image-to-Gaussian student research prototype")
16
+ sub = p.add_subparsers(dest="command", required=True)
17
+ sub.add_parser("doctor", help="Check dependencies, Metal availability and disk space; no downloads")
18
+ demo = sub.add_parser("demo-data", help="Create synthetic code-test fixtures, NOT teacher data")
19
+ demo.add_argument("--out", required=True)
20
+ demo.add_argument("--count", type=int, default=12)
21
+ demo.add_argument("--grid", type=int, choices=[8,16,32], default=8)
22
+ cache = sub.add_parser("cache", help="Cache Idol PLY with optional aligned geometry teacher meshes")
23
+ cache.add_argument("--image", required=True, help="Exact prepared/framed input used by teacher")
24
+ cache.add_argument("--idol-ply", required=True)
25
+ cache.add_argument("--out", required=True)
26
+ cache.add_argument("--id", required=True)
27
+ cache.add_argument("--object-id", help="Shared ID for all images of the same object, to prevent validation leakage")
28
+ cache.add_argument("--grid", type=int, choices=[8,16,32], default=16)
29
+ cache.add_argument("--coarse-mesh", help="InstantMesh output; this command imports, does not run InstantMesh")
30
+ cache.add_argument("--coarse-transform", help="JSON 4x4 mesh-to-Idol-PLY matrix")
31
+ cache.add_argument("--fine-mesh", help="Hi3D output; this command imports, does not run Hi3D")
32
+ cache.add_argument("--fine-transform", help="JSON 4x4 mesh-to-Idol-PLY matrix")
33
+ train = sub.add_parser("train", help="Train the student on cached targets")
34
+ train.add_argument("--manifest", required=True)
35
+ train.add_argument("--out", required=True)
36
+ train.add_argument("--device", choices=["auto","cuda","mps","cpu"], default="auto")
37
+ train.add_argument("--init-from", help="Post-train checkpoint weights; optimizer starts fresh")
38
+ train.add_argument("--steps", type=int, default=1000)
39
+ train.add_argument("--batch-size", type=int, default=1)
40
+ train.add_argument("--width", type=int, default=128)
41
+ train.add_argument("--lr", type=float, default=3e-4)
42
+ train.add_argument("--seed", type=int, default=42)
43
+ train.add_argument("--eval-every", type=int, default=50)
44
+ train.add_argument("--validation-fraction", type=float, default=.2)
45
+ train.add_argument("--allow-synthetic", action="store_true")
46
+ generate = sub.add_parser("generate", help="Generate standard 3DGS PLY with a trained student")
47
+ generate.add_argument("--checkpoint", required=True)
48
+ generate.add_argument("--image", required=True, help="Centered cutout or teacher-prepared image, black background")
49
+ generate.add_argument("--out", required=True)
50
+ generate.add_argument("--device", choices=["auto","cuda","mps","cpu"], default="auto")
51
+ generate.add_argument("--threshold", type=float, default=.5)
52
+ generate.add_argument("--max-splats", type=int, default=32768)
53
+ render = sub.add_parser("preview", help="CPU diagnostic turntable contact sheet")
54
+ render.add_argument("--ply", required=True)
55
+ render.add_argument("--out", required=True)
56
+ teacher = sub.add_parser("teacher-idol", help="EXPERIMENTAL staged Mac runner; weights not bundled")
57
+ teacher.add_argument("--image", required=True)
58
+ teacher.add_argument("--out", required=True)
59
+ teacher.add_argument("--cache", default=".cache/huggingface")
60
+ teacher.add_argument("--weights", default="speridlabs/idol")
61
+ teacher.add_argument("--device", choices=["mps","cpu"], default="mps")
62
+ teacher.add_argument("--mode", choices=["real","synthetic"], default="synthetic")
63
+ teacher.add_argument("--steps", type=int, default=20)
64
+ teacher.add_argument("--gaussians", type=int, default=32768)
65
+ teacher.add_argument("--query-chunk", type=int, default=128)
66
+ teacher.add_argument("--seed", type=int, default=42)
67
+ teacher.add_argument("--allow-download", action="store_true", help="Explicitly allow downloading model weights and gated encoders")
68
+ return p
69
+
70
+
71
+ def main():
72
+ p = parser()
73
+ args = p.parse_args()
74
+ try:
75
+ if args.command == "doctor":
76
+ report = {"python": platform.python_version(), "platform": platform.platform(), "torch": str(torch.__version__),
77
+ "mps_available": torch.backends.mps.is_available(), "mps_built": torch.backends.mps.is_built(),
78
+ "free_disk_gib": round(shutil.disk_usage(Path.cwd()).free / 1024**3, 2),
79
+ "idol_installed": importlib.util.find_spec("idol") is not None,
80
+ "teacher_status": {"Idol": "experimental adapter, real weights unverified",
81
+ "InstantMesh": "cached mesh import only; no Mac execution adapter",
82
+ "Hi3D": "cached mesh import only; no Mac execution adapter"},
83
+ "weights_bundled": False, "quantization": "FP32 student; no binary kernels or 1-bit claims"}
84
+ print(json.dumps(report, indent=2))
85
+ elif args.command == "demo-data":
86
+ from .demo import create_demo
87
+ print(create_demo(args.out, args.count, args.grid))
88
+ elif args.command == "cache":
89
+ from .data import cache_sample
90
+ from .splats import read_ply
91
+ print(json.dumps(cache_sample(args.out, args.id, args.image, read_ply(args.idol_ply), args.grid,
92
+ coarse=args.coarse_mesh, coarse_transform=args.coarse_transform,
93
+ fine=args.fine_mesh, fine_transform=args.fine_transform,
94
+ extra={"object_id": args.object_id or args.id, "idol_ply": str(Path(args.idol_ply).resolve())}), indent=2))
95
+ elif args.command == "train":
96
+ from .engine import train
97
+ train(args)
98
+ elif args.command == "generate":
99
+ from .engine import generate
100
+ print(json.dumps(generate(args.checkpoint, args.image, args.out, args.device, args.threshold, args.max_splats), indent=2))
101
+ elif args.command == "preview":
102
+ from PIL import Image, ImageDraw
103
+ import numpy as np
104
+ from .splats import preview, read_ply
105
+ splats = read_ply(args.ply)
106
+ sheet = Image.new("RGB", (1024, 280), "#111111")
107
+ draw = ImageDraw.Draw(sheet)
108
+ for i in range(4):
109
+ sheet.paste(preview(splats, yaw=i*np.pi/2), (i*256, 24))
110
+ draw.text((i*256+8, 6), f"{i*90} degrees - diagnostic view", fill="white")
111
+ out = Path(args.out)
112
+ if out.exists():
113
+ raise ValueError("Preview already exists")
114
+ out.parent.mkdir(parents=True, exist_ok=True)
115
+ sheet.save(out)
116
+ elif args.command == "teacher-idol":
117
+ from .teacher_idol import run_idol
118
+ run_idol(args)
119
+ except (ValueError, RuntimeError, FileNotFoundError, ImportError) as exc:
120
+ print(f"Error: {exc}", file=sys.stderr)
121
+ return 2
122
+ return 0
123
+
124
+
125
+ if __name__ == "__main__":
126
+ raise SystemExit(main())
src/image3d_student/data.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Lucasgitguy-co
3
+ import hashlib
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ import torch
9
+ from PIL import Image, ImageOps
10
+ from torch.utils.data import Dataset
11
+
12
+ from .splats import Splats, read_ply
13
+
14
+
15
+ def grid_centers(grid):
16
+ axis = (np.arange(grid, dtype=np.float32) + .5) / grid - .5
17
+ return np.stack(np.meshgrid(axis, axis, axis, indexing="ij"), axis=-1).reshape(-1, 3)
18
+
19
+
20
+ def cell_indices(xyz, grid):
21
+ if not np.isfinite(xyz).all() or (np.abs(xyz) > .50001).any():
22
+ raise ValueError("Targets must be aligned inside [-0.5,0.5]^3. No silent normalization is performed.")
23
+ ijk = np.floor((xyz + .5) * grid).astype(np.int64).clip(0, grid-1)
24
+ return (ijk[:, 0] * grid + ijk[:, 1]) * grid + ijk[:, 2]
25
+
26
+
27
+ def splat_targets(splats, grid):
28
+ """One highest-opacity teacher Gaussian per occupied cell; explicitly lossy."""
29
+ cells = cell_indices(splats.xyz, grid)
30
+ order = np.argsort(-splats.opacity[:, 0], kind="stable")
31
+ _, first = np.unique(cells[order], return_index=True)
32
+ keep = order[first]
33
+ slots = cells[keep]
34
+ result = {"occ": np.zeros((grid**3, 1), np.float32), "xyz": grid_centers(grid),
35
+ "rgb": np.zeros((grid**3, 3), np.float32),
36
+ "scale": np.full((grid**3, 3), 1 / grid / 3, np.float32),
37
+ "quat": np.tile(np.array([1, 0, 0, 0], np.float32), (grid**3, 1)),
38
+ "opacity": np.zeros((grid**3, 1), np.float32),
39
+ "geometry": np.zeros((grid**3, 2), np.float32),
40
+ "geometry_valid": np.zeros(2, np.float32), "grid": np.array(grid, np.int32)}
41
+ result["occ"][slots] = 1
42
+ for key in ("xyz", "rgb", "scale", "quat", "opacity"):
43
+ result[key][slots] = getattr(splats, key)[keep]
44
+ return result
45
+
46
+
47
+ def mesh_occupancy(path, transform_path, grid):
48
+ import trimesh
49
+ if not transform_path:
50
+ raise ValueError("Every geometry teacher needs an explicit --*-transform JSON matrix, even for identity.")
51
+ transform = np.asarray(json.loads(Path(transform_path).read_text()), dtype=float)
52
+ if transform.shape != (4, 4) or not np.isfinite(transform).all() or not np.allclose(transform[3], [0,0,0,1]):
53
+ raise ValueError("Transform must be a finite affine 4x4 matrix")
54
+ mesh = trimesh.load(str(path), force="mesh", process=False)
55
+ if not isinstance(mesh, trimesh.Trimesh) or len(mesh.faces) == 0:
56
+ raise ValueError("Geometry teacher must contain a triangle mesh")
57
+ mesh.apply_transform(transform)
58
+ cell_indices(np.asarray(mesh.vertices), grid) # validates the whole object
59
+ points, _ = trimesh.sample.sample_surface(mesh, min(200_000, grid**3 * 16), seed=42)
60
+ mask = np.zeros(grid**3, np.float32)
61
+ mask[np.unique(cell_indices(points, grid))] = 1
62
+ return mask
63
+
64
+
65
+ def cache_sample(root, sample_id, image_path, splats, grid=16, source="idol",
66
+ coarse=None, coarse_transform=None, fine=None, fine_transform=None, extra=None):
67
+ root = Path(root)
68
+ if not sample_id or any(c not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" for c in sample_id):
69
+ raise ValueError("Sample ID may contain only letters, digits, - and _")
70
+ if grid not in (8, 16, 32):
71
+ raise ValueError("Grid must be 8, 16 or 32")
72
+ target = splat_targets(splats, grid)
73
+ for channel, mesh, transform in [(0, coarse, coarse_transform), (1, fine, fine_transform)]:
74
+ if mesh:
75
+ target["geometry"][:, channel] = mesh_occupancy(mesh, transform, grid)
76
+ target["geometry_valid"][channel] = 1
77
+ row = {"id": sample_id, "image": f"images/{sample_id}.png", "target": f"targets/{sample_id}.npz",
78
+ "source": source, "grid": grid, "teachers": {"appearance": source,
79
+ "coarse": "InstantMesh" if coarse else None, "fine": "Hi3D" if fine else None},
80
+ "teacher_gaussians": len(splats.xyz), "retained_gaussians": int(target["occ"].sum()),
81
+ "geometry_transforms": {"coarse": json.loads(Path(coarse_transform).read_text()) if coarse else None,
82
+ "fine": json.loads(Path(fine_transform).read_text()) if fine else None},
83
+ "metadata": extra or {}}
84
+ manifest = root / "manifest.jsonl"
85
+ rows = load_manifest(manifest) if manifest.exists() else []
86
+ if any(r["id"] == sample_id for r in rows):
87
+ raise ValueError(f"Sample {sample_id} already exists; choose a new ID")
88
+ if rows and any(r["grid"] != grid for r in rows):
89
+ raise ValueError("Do not mix grid resolutions in one dataset")
90
+ image = Image.open(image_path)
91
+ image.load()
92
+ if root.joinpath(row["image"]).exists() or root.joinpath(row["target"]).exists():
93
+ raise ValueError("Sample files already exist; refusing to overwrite")
94
+ root.joinpath("images").mkdir(parents=True, exist_ok=True)
95
+ root.joinpath("targets").mkdir(parents=True, exist_ok=True)
96
+ image.save(root / row["image"])
97
+ row["image_sha256"] = hashlib.sha256((root / row["image"]).read_bytes()).hexdigest()
98
+ np.savez_compressed(root / row["target"], **target)
99
+ row["target_sha256"] = hashlib.sha256((root / row["target"]).read_bytes()).hexdigest()
100
+ # Single-writer dataset. Atomic manifest replacement avoids partial JSON lines.
101
+ tmp = manifest.with_suffix(".tmp")
102
+ tmp.write_text("".join(json.dumps(r) + "\n" for r in [*rows, row]))
103
+ tmp.replace(manifest)
104
+ return row
105
+
106
+
107
+ def load_manifest(path):
108
+ rows = [json.loads(line) for line in Path(path).read_text().splitlines() if line.strip()]
109
+ if not rows or len({r["id"] for r in rows}) != len(rows):
110
+ raise ValueError("Manifest must contain nonempty, unique sample IDs")
111
+ return rows
112
+
113
+
114
+ def image_tensor(path, size=128):
115
+ """Already-framed RGB or RGBA cutout; black background, square letterbox."""
116
+ with Image.open(path) as original:
117
+ rgba = ImageOps.exif_transpose(original).convert("RGBA")
118
+ bg = Image.new("RGBA", rgba.size, (0, 0, 0, 255))
119
+ bg.alpha_composite(rgba)
120
+ image = ImageOps.pad(bg.convert("RGB"), (size, size), method=Image.Resampling.LANCZOS, color="black")
121
+ return torch.from_numpy(np.array(image, dtype=np.float32).transpose(2, 0, 1) / 255)
122
+
123
+
124
+ class CachedDataset(Dataset):
125
+ def __init__(self, manifest, rows=None):
126
+ self.root = Path(manifest).resolve().parent
127
+ self.rows = load_manifest(manifest) if rows is None else rows
128
+ if not self.rows:
129
+ raise ValueError("Empty dataset")
130
+ self.grid = self.rows[0]["grid"]
131
+ for row in self.rows:
132
+ if row["grid"] != self.grid:
133
+ raise ValueError("Mixed grids")
134
+ for key in ("image", "target"):
135
+ p = (self.root / row[key]).resolve()
136
+ if not p.is_relative_to(self.root) or not p.is_file():
137
+ raise ValueError(f"Invalid dataset {key} path")
138
+ digest = hashlib.sha256(p.read_bytes()).hexdigest()
139
+ if digest != row[f"{key}_sha256"]:
140
+ raise ValueError(f"Dataset {key} hash mismatch for {row['id']}")
141
+
142
+ def __len__(self):
143
+ return len(self.rows)
144
+
145
+ def __getitem__(self, i):
146
+ row = self.rows[i]
147
+ with np.load(self.root / row["target"], allow_pickle=False) as cached:
148
+ target = {k: torch.from_numpy(cached[k].copy()).float() for k in cached.files if k != "grid"}
149
+ return image_tensor(self.root / row["image"]), target
src/image3d_student/demo.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Lucasgitguy-co
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+
7
+ from .data import cache_sample
8
+ from .splats import Splats, preview
9
+
10
+
11
+ def create_demo(root, count=12, grid=8):
12
+ """Procedural ellipsoids only. Never presented as outputs of any real teacher."""
13
+ if count < 4:
14
+ raise ValueError("Use at least four fixtures")
15
+ root = Path(root)
16
+ root.mkdir(parents=True, exist_ok=True)
17
+ rng = np.random.default_rng(17)
18
+ phi, theta = np.meshgrid(np.linspace(.06, np.pi-.06, 14), np.linspace(0, 2*np.pi, 28, endpoint=False))
19
+ unit = np.stack([np.sin(phi)*np.cos(theta), np.cos(phi), np.sin(phi)*np.sin(theta)], axis=-1).reshape(-1, 3)
20
+ for i in range(count):
21
+ radii = rng.uniform(.17, .36, 3)
22
+ xyz = (unit * radii).astype(np.float32)
23
+ colour = rng.uniform(.2, .95, 3)
24
+ rgb = np.clip(colour[None] * (.8 + .2 * unit[:, 1:2]), 0, 1)
25
+ splats = Splats(xyz, rgb, np.full_like(xyz, .035), np.tile([1,0,0,0], (len(xyz),1)), np.full((len(xyz),1), .9))
26
+ image_path = root / f"fixture-{i:03}.png"
27
+ preview(splats, size=128).save(image_path)
28
+ cache_sample(root, f"fixture-{i:03}", image_path, splats, grid, source="synthetic",
29
+ extra={"purpose": "code verification only", "object_id": f"fixture-{i:03}"})
30
+ image_path.unlink()
31
+ return root / "manifest.jsonl"
src/image3d_student/engine.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Lucasgitguy-co
3
+ import json
4
+ import hashlib
5
+ import random
6
+ import time
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import torch
11
+ from torch.utils.data import DataLoader
12
+
13
+ from .data import CachedDataset, image_tensor, load_manifest
14
+ from .model import Student, distillation_loss
15
+ from .splats import Splats, write_ply
16
+
17
+
18
+ def device_for(name="auto"):
19
+ if name == "auto":
20
+ name = "cuda" if torch.cuda.is_available() else ("mps" if torch.backends.mps.is_available() else "cpu")
21
+ if name == "mps" and not torch.backends.mps.is_available():
22
+ raise ValueError("Metal GPU unavailable in this process. Run in a normal macOS Terminal, or use --device cpu.")
23
+ if name == "cuda" and not torch.cuda.is_available():
24
+ raise ValueError("CUDA requested but unavailable")
25
+ if name not in ("mps", "cpu", "cuda"):
26
+ raise ValueError("Supported devices: cpu, mps, cuda")
27
+ torch.set_num_threads(min(4, torch.get_num_threads()))
28
+ return torch.device(name)
29
+
30
+
31
+ def split_rows(rows, fraction, seed):
32
+ if not 0 <= fraction < 1:
33
+ raise ValueError("Validation fraction must lie in [0,1)")
34
+ # Keep repeated views/crops of the same object together. Supply object_id in cache metadata.
35
+ groups = {}
36
+ for row in rows:
37
+ group = row.get("metadata", {}).get("object_id", row["id"])
38
+ groups.setdefault(group, []).append(row)
39
+ keys = sorted(groups)
40
+ random.Random(seed).shuffle(keys)
41
+ n = max(1, round(len(keys) * fraction)) if fraction else 0
42
+ if n >= len(keys):
43
+ raise ValueError("Need at least two different objects for validation, or explicitly use --validation-fraction 0 for an overfit test")
44
+ val_keys = set(keys[:n])
45
+ train = [r for k in keys if k not in val_keys for r in groups[k]]
46
+ val = [r for k in keys if k in val_keys for r in groups[k]]
47
+ if {r["image_sha256"] for r in train} & {r["image_sha256"] for r in val}:
48
+ raise ValueError("Identical images occur across training/validation. Assign the same object-id to related samples.")
49
+ return train, val
50
+
51
+
52
+ def on_device(target, device):
53
+ return {k: v.to(device) for k, v in target.items()}
54
+
55
+
56
+ @torch.no_grad()
57
+ def evaluate(model, loader, device):
58
+ model.eval()
59
+ sums = {}
60
+ for image, target in loader:
61
+ target = on_device(target, device)
62
+ pred = model(image.to(device))
63
+ loss, terms = distillation_loss(pred, target, model.grid)
64
+ occupied = pred["occ"].sigmoid() > .5
65
+ truth = target["occ"] > .5
66
+ terms = {**terms, "loss": loss, "occupancy_iou": (occupied & truth).sum() / (occupied | truth).sum().clamp_min(1)}
67
+ for key, value in terms.items():
68
+ sums[key] = sums.get(key, 0.) + float(value)
69
+ return {k: v / len(loader) for k, v in sums.items()}
70
+
71
+
72
+ def save_checkpoint(path, model, optimizer, step, metadata):
73
+ payload = {"format": 1, "config": model.config, "model": {k: v.detach().cpu() for k, v in model.state_dict().items()},
74
+ "optimizer": optimizer.state_dict(), "step": step, "metadata": metadata}
75
+ tmp = Path(path).with_suffix(".tmp")
76
+ torch.save(payload, tmp)
77
+ tmp.replace(path)
78
+
79
+
80
+ def train(args):
81
+ if args.steps < 1 or args.batch_size < 1 or args.lr <= 0 or args.eval_every < 1:
82
+ raise ValueError("Steps, batch size, learning rate, and evaluation interval must be positive")
83
+ device = device_for(args.device)
84
+ random.seed(args.seed)
85
+ np.random.seed(args.seed)
86
+ torch.manual_seed(args.seed)
87
+ rows = load_manifest(args.manifest)
88
+ synthetic = any(r["source"] == "synthetic" for r in rows)
89
+ if synthetic and not args.allow_synthetic:
90
+ raise ValueError("Synthetic fixtures are code tests, not teacher data. Add --allow-synthetic for an explicit smoke test.")
91
+ train_rows, val_rows = split_rows(rows, args.validation_fraction, args.seed)
92
+ train_data = CachedDataset(args.manifest, train_rows)
93
+ generator = torch.Generator().manual_seed(args.seed)
94
+ loader = DataLoader(train_data, batch_size=args.batch_size, shuffle=True, num_workers=0, generator=generator)
95
+ val_loader = DataLoader(CachedDataset(args.manifest, val_rows), batch_size=1) if val_rows else None
96
+ model = Student(train_data.grid, args.width).to(device)
97
+ initialization = None
98
+ if getattr(args, "init_from", None):
99
+ initial_path = Path(args.init_from)
100
+ state = torch.load(initial_path, map_location="cpu", weights_only=True)
101
+ if state.get("format") != 1 or state["config"]["width"] != args.width:
102
+ raise ValueError("Initialization requires format=1 and matching --width")
103
+ # Coordinates are deterministic buffers, not learned weights. Rebuild them
104
+ # for the target grid while preserving every learned encoder/decoder weight.
105
+ learned = {k: v for k, v in state["model"].items() if k not in ("centers", "coords")}
106
+ result = model.load_state_dict(learned, strict=False)
107
+ if set(result.missing_keys) != {"centers", "coords"} or result.unexpected_keys:
108
+ raise ValueError("Checkpoint architecture mismatch")
109
+ initialization = {"sha256": hashlib.sha256(initial_path.read_bytes()).hexdigest(),
110
+ "config": state["config"], "step": state["step"],
111
+ "optimizer": "fresh AdamW; weights-only post-training"}
112
+ optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=1e-4)
113
+ run = Path(args.out)
114
+ if run.exists() and any(run.iterdir()):
115
+ raise ValueError("Run directory must be empty; choose a new --out directory")
116
+ run.mkdir(parents=True, exist_ok=True)
117
+ meta = {"synthetic_only": all(r["source"] == "synthetic" for r in rows),
118
+ "contains_synthetic": synthetic, "device": str(device), "torch": str(torch.__version__),
119
+ "seed": args.seed, "train_ids": [r["id"] for r in train_rows], "val_ids": [r["id"] for r in val_rows],
120
+ "parameters": sum(p.numel() for p in model.parameters()),
121
+ "teachers": sorted({v for r in rows for v in r["teachers"].values() if v}),
122
+ "validation": "held-out objects" if val_rows else "none; training overfit test only",
123
+ "dataset": str(Path(args.manifest).resolve()),
124
+ "manifest_sha256": hashlib.sha256(Path(args.manifest).read_bytes()).hexdigest(),
125
+ "initialization": initialization, "steps_requested": args.steps,
126
+ "learning_rate": args.lr, "batch_size": args.batch_size,
127
+ "validation_fraction": args.validation_fraction}
128
+ if val_loader:
129
+ meta["baseline_validation"] = evaluate(model, val_loader, device)
130
+ (run / "run.json").write_text(json.dumps(meta, indent=2))
131
+ print(json.dumps(meta), flush=True)
132
+ step, best, started = 0, float("inf"), time.monotonic()
133
+ iterator = iter(loader)
134
+ try:
135
+ while step < args.steps:
136
+ try:
137
+ images, target = next(iterator)
138
+ except StopIteration:
139
+ iterator = iter(loader)
140
+ images, target = next(iterator)
141
+ model.train()
142
+ optimizer.zero_grad(set_to_none=True)
143
+ pred = model(images.to(device))
144
+ loss, terms = distillation_loss(pred, on_device(target, device), model.grid)
145
+ if not torch.isfinite(loss):
146
+ raise RuntimeError("Nonfinite training loss; stopping")
147
+ loss.backward()
148
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.)
149
+ optimizer.step()
150
+ step += 1
151
+ if step == 1 or step % args.eval_every == 0 or step == args.steps:
152
+ record = {"step": step, "seconds": round(time.monotonic()-started, 3),
153
+ "train_loss": float(loss.detach()), **{k: float(v.detach()) for k, v in terms.items()}}
154
+ if val_loader:
155
+ record["validation"] = evaluate(model, val_loader, device)
156
+ score = record["validation"]["loss"]
157
+ if score < best:
158
+ best = score
159
+ save_checkpoint(run / "best.pt", model, optimizer, step, meta)
160
+ with (run / "metrics.jsonl").open("a") as handle:
161
+ handle.write(json.dumps(record) + "\n")
162
+ print(json.dumps(record), flush=True)
163
+ save_checkpoint(run / "last.pt", model, optimizer, step, meta)
164
+ except KeyboardInterrupt:
165
+ print("Interrupted; saving completed optimizer steps.", flush=True)
166
+ finally:
167
+ if step:
168
+ save_checkpoint(run / "last.pt", model, optimizer, step, meta)
169
+ return meta
170
+
171
+
172
+ @torch.no_grad()
173
+ def generate(checkpoint, image, out, device="auto", threshold=.5, max_splats=32768):
174
+ if not 0 < threshold < 1 or max_splats < 1:
175
+ raise ValueError("Threshold must be in (0,1) and max-splats positive")
176
+ device = device_for(device)
177
+ state = torch.load(checkpoint, map_location="cpu", weights_only=True)
178
+ if state.get("format") != 1 or state.get("step", 0) < 1:
179
+ raise ValueError("Expected a trained image3d-student checkpoint")
180
+ model = Student(**state["config"])
181
+ model.load_state_dict(state["model"])
182
+ model.to(device).eval()
183
+ if device.type == "mps":
184
+ torch.mps.synchronize()
185
+ started = time.monotonic()
186
+ pred = model(image_tensor(image)[None].to(device))
187
+ score = pred["occ"][0, :, 0].sigmoid()
188
+ valid = torch.nonzero(score >= threshold).flatten()
189
+ if len(valid) == 0:
190
+ raise ValueError("No occupied cells above threshold. Train longer or inspect validation; no fabricated fallback splats were exported.")
191
+ valid = valid[torch.argsort(score[valid], descending=True)[:max_splats]]
192
+ values = {k: pred[k][0, valid].cpu().numpy() for k in ("xyz", "rgb", "scale", "quat", "opacity")}
193
+ elapsed = time.monotonic() - started
194
+ splats = Splats(**values)
195
+ out = Path(out)
196
+ if out.exists() or out.with_suffix(".json").exists():
197
+ raise ValueError("Output already exists; choose a new filename")
198
+ write_ply(out, splats)
199
+ report = {"gaussians": len(valid), "seconds_preprocess_and_forward": elapsed,
200
+ "device": str(device), "checkpoint_step": state["step"], "training": state["metadata"],
201
+ "note": ("Synthetic-trained output is a pipeline demonstration, not a pretrained image-to-3D result."
202
+ if state["metadata"].get("contains_synthetic") else "Experimental cached-target student; inspect unseen-object quality.")}
203
+ out.with_suffix(".json").write_text(json.dumps(report, indent=2))
204
+ return report
src/image3d_student/model.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Lucasgitguy-co
3
+ import math
4
+
5
+ import torch
6
+ from torch import nn
7
+ from torch.nn import functional as F
8
+
9
+ from .data import grid_centers
10
+
11
+
12
+ class Student(nn.Module):
13
+ """Small image-conditioned coordinate MLP; fixed cells avoid unordered point regression.
14
+
15
+ 15 Gaussian outputs + 2 auxiliary geometry logits. This is a coarse baseline,
16
+ not an architecture expected to reproduce million-splat teacher detail.
17
+ """
18
+ def __init__(self, grid=16, width=128):
19
+ super().__init__()
20
+ if grid not in (8, 16, 32) or width < 16:
21
+ raise ValueError("Unsupported grid or width")
22
+ self.config = {"grid": grid, "width": width}
23
+ self.grid = grid
24
+ blocks, channels = [], 3
25
+ for out in (24, 48, 96, 128):
26
+ blocks.extend([nn.Conv2d(channels, out, 3, stride=2, padding=1), nn.GroupNorm(8, out), nn.SiLU()])
27
+ channels = out
28
+ self.encoder = nn.Sequential(*blocks, nn.AdaptiveAvgPool2d(4), nn.Flatten(),
29
+ nn.Linear(128*4*4, width), nn.SiLU())
30
+ centers = torch.from_numpy(grid_centers(grid))
31
+ freq = 2**torch.arange(4, dtype=torch.float32) * math.pi * 2
32
+ angles = centers[..., None] * freq
33
+ self.register_buffer("centers", centers)
34
+ self.register_buffer("coords", torch.cat([centers, angles.sin().flatten(1), angles.cos().flatten(1)], dim=1))
35
+ self.query = nn.Sequential(nn.Linear(27, width), nn.SiLU())
36
+ self.decoder = nn.Sequential(nn.Linear(width, width), nn.SiLU(), nn.Linear(width, width), nn.SiLU(), nn.Linear(width, 17))
37
+ with torch.no_grad():
38
+ self.decoder[-1].bias.zero_()
39
+ self.decoder[-1].bias[0] = -2
40
+
41
+ def forward(self, images):
42
+ features = self.encoder(images)
43
+ raw = self.decoder(features[:, None, :] + self.query(self.coords)[None])
44
+ quat = raw[..., 10:14] + raw.new_tensor([1, 0, 0, 0])
45
+ return {"occ": raw[..., :1], "xyz": self.centers[None] + raw[..., 1:4].tanh() / (2*self.grid),
46
+ "rgb": raw[..., 4:7].sigmoid(),
47
+ "scale": (raw[..., 7:10].clamp(-8, 5) - math.log(self.grid*3)).exp(),
48
+ "quat": F.normalize(quat, dim=-1, eps=1e-8), "opacity": raw[..., 14:15].sigmoid(),
49
+ "geometry": raw[..., 15:17]}
50
+
51
+
52
+ def balanced_bce(logits, target):
53
+ error = F.binary_cross_entropy_with_logits(logits, target, reduction="none")
54
+ positive = (error * target).sum() / target.sum().clamp_min(1)
55
+ negative = (error * (1-target)).sum() / (1-target).sum().clamp_min(1)
56
+ return (positive + negative) / 2
57
+
58
+
59
+ def distillation_loss(pred, target, grid):
60
+ mask = target["occ"]
61
+ def masked(value):
62
+ return (value * mask).sum() / (mask.sum().clamp_min(1) * value.shape[-1])
63
+ terms = {"occupancy": balanced_bce(pred["occ"], mask),
64
+ "position": masked(((pred["xyz"] - target["xyz"]) * grid).square()),
65
+ "colour": masked((pred["rgb"] - target["rgb"].clamp(0, 1)).square()),
66
+ "scale": masked((pred["scale"].log() - target["scale"].clamp_min(1e-8).log()).square()),
67
+ "rotation": masked(1 - (pred["quat"] * target["quat"]).sum(-1, keepdim=True).square().clamp(0, 1)),
68
+ "opacity": masked((pred["opacity"] - target["opacity"]).square())}
69
+ geometry = pred["geometry"].sum() * 0
70
+ for i in range(2):
71
+ available = target["geometry_valid"][:, i] > 0
72
+ if available.any():
73
+ geometry = geometry + balanced_bce(pred["geometry"][available, :, i:i+1], target["geometry"][available, :, i:i+1])
74
+ terms["geometry"] = geometry
75
+ total = terms["occupancy"] + terms["position"] + 2*terms["colour"] + .1*terms["scale"] + .1*terms["rotation"] + terms["opacity"] + .25*geometry
76
+ return total, terms
src/image3d_student/splats.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Lucasgitguy-co
3
+ """Activated Gaussian attributes and standard degree-zero 3DGS PLY IO.
4
+
5
+ Coordinates are preserved as supplied. Idol's *exported PLY* uses its y-up frame;
6
+ we do not apply Idol's internal z-up conversion a second time.
7
+ """
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+ import numpy as np
12
+ from plyfile import PlyData, PlyElement
13
+
14
+ C0 = 0.28209479177387814
15
+
16
+
17
+ @dataclass
18
+ class Splats:
19
+ xyz: np.ndarray
20
+ rgb: np.ndarray
21
+ scale: np.ndarray
22
+ quat: np.ndarray # w,x,y,z
23
+ opacity: np.ndarray
24
+
25
+ def __post_init__(self):
26
+ n = len(self.xyz)
27
+ if n == 0:
28
+ raise ValueError("No Gaussians: refusing to write or train on an empty object.")
29
+ for name, width in [("xyz", 3), ("rgb", 3), ("scale", 3), ("quat", 4), ("opacity", 1)]:
30
+ a = np.asarray(getattr(self, name), dtype=np.float32).reshape(-1, width)
31
+ if a.shape != (n, width) or not np.isfinite(a).all():
32
+ raise ValueError(f"Invalid {name} shape or nonfinite values")
33
+ setattr(self, name, a)
34
+ if (self.scale <= 0).any():
35
+ raise ValueError("Gaussian scales must be positive")
36
+ if (self.opacity < 0).any() or (self.opacity > 1).any():
37
+ raise ValueError("Opacity must lie in [0,1]")
38
+ norm = np.linalg.norm(self.quat, axis=1, keepdims=True)
39
+ if (norm < 1e-8).any():
40
+ raise ValueError("Zero quaternion")
41
+ self.quat = self.quat / norm
42
+
43
+ def take(self, indices):
44
+ return Splats(**{k: getattr(self, k)[indices] for k in self.__dataclass_fields__})
45
+
46
+
47
+ def read_ply(path):
48
+ v = PlyData.read(str(path))["vertex"].data
49
+ required = ["x", "y", "z", "f_dc_0", "f_dc_1", "f_dc_2", "opacity",
50
+ "scale_0", "scale_1", "scale_2", "rot_0", "rot_1", "rot_2", "rot_3"]
51
+ if not set(required).issubset(v.dtype.names):
52
+ raise ValueError("Expected a Gaussian-splat PLY, not a mesh or ordinary point-cloud PLY")
53
+ col = lambda names: np.column_stack([v[k] for k in names]).astype(np.float32)
54
+ return Splats(col(["x", "y", "z"]), 0.5 + C0 * col([f"f_dc_{i}" for i in range(3)]),
55
+ np.exp(np.clip(col([f"scale_{i}" for i in range(3)]), -20, 5)),
56
+ col([f"rot_{i}" for i in range(4)]),
57
+ 1 / (1 + np.exp(-np.clip(col(["opacity"]), -30, 30))))
58
+
59
+
60
+ def write_ply(path, splats):
61
+ path = Path(path)
62
+ path.parent.mkdir(parents=True, exist_ok=True)
63
+ names = ["x", "y", "z", "nx", "ny", "nz", "f_dc_0", "f_dc_1", "f_dc_2", "opacity",
64
+ "scale_0", "scale_1", "scale_2", "rot_0", "rot_1", "rot_2", "rot_3"]
65
+ opacity = np.clip(splats.opacity, 1e-6, 1 - 1e-6)
66
+ values = np.concatenate([splats.xyz, np.zeros_like(splats.xyz), (splats.rgb - 0.5) / C0,
67
+ np.log(opacity / (1 - opacity)), np.log(splats.scale), splats.quat], axis=1)
68
+ table = np.empty(len(values), dtype=[(k, "f4") for k in names])
69
+ for i, name in enumerate(names):
70
+ table[name] = values[:, i]
71
+ PlyData([PlyElement.describe(table, "vertex")], text=False).write(str(path))
72
+
73
+
74
+ def rotation_matrices(q):
75
+ w, x, y, z = q.T
76
+ return np.stack([1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+y*w),
77
+ 2*(x*y+z*w), 1-2*(x*x+z*z), 2*(y*z-x*w),
78
+ 2*(x*z-y*w), 2*(y*z+x*w), 1-2*(x*x+y*y)], axis=-1).reshape(-1, 3, 3)
79
+
80
+
81
+ def preview(splats, size=256, yaw=0.0):
82
+ """CPU orthographic Gaussian preview; debugging only, not an evaluation renderer."""
83
+ from PIL import Image
84
+ c, s = np.cos(yaw), np.sin(yaw)
85
+ view = np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]], dtype=np.float32)
86
+ xyz = splats.xyz @ view.T
87
+ rot = rotation_matrices(splats.quat)
88
+ cov = (rot * (splats.scale ** 2)[:, None, :]) @ rot.transpose(0, 2, 1)
89
+ cov = view[None] @ cov @ view.T[None]
90
+ pixels = size / 1.25
91
+ canvas = np.zeros((size, size, 3), dtype=np.float32)
92
+ # Back to front, camera on +z. Image y points down.
93
+ for i in np.argsort(xyz[:, 2]):
94
+ center = np.array([xyz[i, 0], -xyz[i, 1]]) * pixels + size / 2
95
+ flip = np.diag([1., -1.])
96
+ covariance = flip @ cov[i, :2, :2] @ flip * pixels ** 2 + np.eye(2) * 0.3
97
+ radius = min(size, int(np.ceil(3 * np.sqrt(np.linalg.eigvalsh(covariance).max()))))
98
+ lo = np.maximum(0, np.floor(center - radius).astype(int))
99
+ hi = np.minimum(size, np.ceil(center + radius + 1).astype(int))
100
+ if np.any(hi <= lo):
101
+ continue
102
+ yy, xx = np.mgrid[lo[1]:hi[1], lo[0]:hi[0]]
103
+ delta = np.stack([xx - center[0], yy - center[1]], axis=-1)
104
+ exponent = np.einsum("...i,ij,...j->...", delta, np.linalg.inv(covariance), delta)
105
+ alpha = (splats.opacity[i, 0] * np.exp(-0.5 * exponent)).clip(0, .99)[..., None]
106
+ patch = canvas[lo[1]:hi[1], lo[0]:hi[0]]
107
+ patch[:] = patch * (1-alpha) + np.clip(splats.rgb[i], 0, 1) * alpha
108
+ return Image.fromarray((canvas.clip(0, 1) * 255).astype(np.uint8))
src/image3d_student/teacher_idol.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Lucasgitguy-co
3
+ """Experimental, staged Idol inference for Apple MPS.
4
+
5
+ Upstream revision: 4702a7f00850e34b6e83a2170cc123c3e9570744.
6
+ No weights are bundled. Full-weight inference has NOT been verified by this project.
7
+ """
8
+ import gc
9
+ import json
10
+ import os
11
+ import shutil
12
+ from pathlib import Path
13
+
14
+ import numpy as np
15
+ import torch
16
+ from PIL import Image
17
+
18
+ from .engine import device_for
19
+
20
+
21
+ def patch_attention(layers, query_chunk=128):
22
+ """Bound attention working memory by splitting only the query dimension.
23
+
24
+ Each query still sees ALL keys/values. Does not split or change softmax's key axis.
25
+ Disable CUDA-specific optimized sampling and avoid complex RoPE tensors on MPS.
26
+ """
27
+ if query_chunk < 1:
28
+ raise ValueError("query_chunk must be positive")
29
+ from torch.nn import functional as F
30
+
31
+ def attention(*args):
32
+ q, k, v = args[0].unbind(dim=2) if len(args) == 1 else args
33
+ q, k, v = (t.permute(0, 2, 1, 3) for t in (q, k, v))
34
+ chunks = [F.scaled_dot_product_attention(q[:, :, start:start+query_chunk], k, v)
35
+ for start in range(0, q.shape[2], query_chunk)]
36
+ return torch.cat(chunks, dim=2).permute(0, 2, 1, 3)
37
+
38
+ layers.scaled_dot_product_attention = attention
39
+ layers.RePo3DRotaryEmbedding.real_valued = True
40
+ return attention
41
+
42
+
43
+ def release(device):
44
+ gc.collect()
45
+ if device.type == "mps":
46
+ torch.mps.synchronize()
47
+ torch.mps.empty_cache()
48
+
49
+
50
+ @torch.no_grad()
51
+ def run_idol(args):
52
+ if args.steps < 1 or args.gaussians < 32768 or args.gaussians > 1048576 or args.gaussians % 32:
53
+ raise ValueError("Steps must be positive; Idol requires 32768..1048576 Gaussians, divisible by 32")
54
+ device = device_for(args.device)
55
+ cache = Path(args.cache).resolve()
56
+ cache.mkdir(parents=True, exist_ok=True)
57
+ os.environ["HF_HOME"] = str(cache)
58
+ os.environ["HF_HUB_OFFLINE"] = "0" if args.allow_download else "1"
59
+ os.environ["ATTN_BACKEND"] = "sdpa"
60
+ if args.allow_download and shutil.disk_usage(cache).free < 16 * 1024**3:
61
+ raise ValueError("Less than 16 GiB disk space available. This conservative download guard leaves room for teacher weights, encoders and temporary files. Free space, or use an existing complete cache without --allow-download.")
62
+ try:
63
+ from huggingface_hub import hf_hub_download, snapshot_download
64
+ from safetensors.torch import load_file
65
+ from tensordict import TensorDict
66
+ from idol.conditioning import ImageEncoder, require_encoder_access
67
+ from idol.models import BiRefNet, GaussianFixedlenDecoder, LatentSeqMMFlowModel, OctreeProbabilityFixedlenDecoder, layers
68
+ from idol.preprocess import CROP_MARGIN, preprocess_image, has_real_alpha
69
+ from idol.sampler import sample_flow
70
+ from idol.camera import decode_camera
71
+ except ImportError as exc:
72
+ raise RuntimeError("Install the optional teacher dependencies: python -m pip install '.[teacher]'") from exc
73
+ out = Path(args.out)
74
+ if out.exists():
75
+ raise ValueError("Teacher output directory already exists; choose a new --out")
76
+ # Local-only by default. Hugging Face authentication is never written to project files.
77
+ config_path = hf_hub_download(args.weights, "config.json", local_files_only=not args.allow_download)
78
+ config = json.loads(Path(config_path).read_text())
79
+ encoders = config["image_encoders"]
80
+ require_encoder_access([encoders["dinov3"], encoders["flux2_vae"]])
81
+ weights = Path(snapshot_download(args.weights, allow_patterns=["config.json", "*.safetensors"],
82
+ local_files_only=not args.allow_download))
83
+ patch_attention(layers, args.query_chunk)
84
+ torch.manual_seed(args.seed)
85
+
86
+ def load(model, name):
87
+ state = load_file(str(weights / f"{name}.safetensors"), device="cpu")
88
+ model.load_state_dict(state, strict=True)
89
+ del state
90
+ return model.eval().requires_grad_(False).to(device)
91
+
92
+ print("Experimental Idol MPS adapter: full-weight compatibility and quality remain unverified.", flush=True)
93
+ with Image.open(args.image) as opened:
94
+ image = opened.copy()
95
+ background = None
96
+ if not has_real_alpha(image):
97
+ print("Stage 1: background removal", flush=True)
98
+ background = load(BiRefNet(), "birefnet")
99
+ prepared = preprocess_image(image, background, config["image_size"], CROP_MARGIN[args.mode])
100
+ del background
101
+ release(device)
102
+ pixels = torch.from_numpy(np.array(prepared)).permute(2, 0, 1).float()[None].div(255).to(device)
103
+ print("Stage 2: image conditioning", flush=True)
104
+ encoder = ImageEncoder(encoders["dinov3"], encoders["flux2_vae"], device="cpu")
105
+ # Both encoders initially reside on CPU; only the active encoder is moved to MPS.
106
+ encoder.dinov3.to(device)
107
+ feature1 = encoder._dinov3_tokens(pixels)
108
+ del encoder.dinov3
109
+ release(device)
110
+ encoder.flux2_vae.to(device)
111
+ feature2 = encoder._flux2_tokens(pixels)
112
+ del encoder, pixels
113
+ release(device)
114
+ cond = TensorDict({"feature1": feature1, "feature2": feature2}, batch_size=1, device=device)
115
+ print("Stage 3: latent sampling", flush=True)
116
+ model = load(LatentSeqMMFlowModel(**config["denoiser"]["args"]), "denoiser")
117
+ noise = TensorDict({"latent": torch.randn(1, model.q_token_length, model.in_channels, device=device),
118
+ "camera": torch.randn(1, 1, model.cam_channels, device=device)}, batch_size=1, device=device)
119
+ result = sample_flow(model, noise, cond=cond, neg_cond=cond.apply(torch.zeros_like), steps=args.steps,
120
+ rescale_t=3., cfg_strength=3., verbose=True, batch_cfg=False)
121
+ latent, camera = result["latent"], decode_camera(result["camera"][0, 0])
122
+ del model, noise, cond, feature1, feature2, result
123
+ release(device)
124
+ print("Stage 4: octree decoding", flush=True)
125
+ model = load(OctreeProbabilityFixedlenDecoder(**config["decoder"]["args"]), "decoder")
126
+ per_anchor = config["decoder_gs"]["args"]["representation_config"]["num_gaussians"]
127
+ anchors = OctreeProbabilityFixedlenDecoder.sample(model, latent, num_points=args.gaussians // per_anchor,
128
+ level=config["max_voxel_level"])
129
+ del model
130
+ release(device)
131
+ print("Stage 5: Gaussian decoding", flush=True)
132
+ model = load(GaussianFixedlenDecoder(**config["decoder_gs"]["args"]), "decoder_gs")
133
+ gaussian = model.to_representation(anchors, model(anchors, latent))[0]
134
+ out.mkdir(parents=True)
135
+ gaussian.save_ply(str(out / "teacher.ply"))
136
+ prepared.save(out / "input.png")
137
+ (out / "metadata.json").write_text(json.dumps({"teacher": args.weights, "steps": args.steps,
138
+ "gaussians": args.gaussians, "seed": args.seed, "device": str(device), "camera": camera,
139
+ "sdk_revision": "4702a7f00850e34b6e83a2170cc123c3e9570744", "weights_snapshot": weights.name,
140
+ "coordinate_frame": "Idol exported PLY (y-up)", "adapter": "experimental staged MPS"}, indent=2))
141
+ print(f"Teacher output saved to {out}", flush=True)
training.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "base_model": "Lucasgitguy-co/forma3d-tiny-2",
3
+ "base_revision": "1648582b07a0f7f9d482a8c0f4cf89e6ecd86972",
4
+ "base_sha256": "c051b3f93d874f7c638588c9d0381754d3f954b1cd271121a033b2478d36bb1a",
5
+ "parameters": 306321,
6
+ "device": "mps",
7
+ "torch": "2.15.0",
8
+ "dataset": "codepawl/abo-glb",
9
+ "dataset_revision": "c7c8293fed3b7ab122ef5d7896521a187e0962d7",
10
+ "steps_requested": 6000,
11
+ "batch_size": 4,
12
+ "learning_rate": 5e-05,
13
+ "seed": 44,
14
+ "teacher_models_run": [],
15
+ "contains_synthetic": false,
16
+ "rendered_mesh_proxies": true,
17
+ "optimizer": "fresh AdamW, resumed on interruption",
18
+ "data": {
19
+ "dataset": "codepawl/abo-glb",
20
+ "revision": "c7c8293fed3b7ab122ef5d7896521a187e0962d7",
21
+ "objects": 512,
22
+ "views": 2048,
23
+ "groups": {
24
+ "train": 348,
25
+ "validation": 87,
26
+ "test": 77
27
+ },
28
+ "split_views": {
29
+ "train": 1392,
30
+ "validation": 348,
31
+ "test": 308
32
+ },
33
+ "manifest_sha256": {
34
+ "train": "65af1c16cefde7e9cd411af055ccdee36c183382db18a4fd9a9d4ea82fb248ee",
35
+ "validation": "65ca208e5b7d59d398056c997751b12a18ce05c9361e37f4160f3b1420cbe52a",
36
+ "test": "c8f927c42ba2ccecd960536de8c7c7431c8fe674401e1fbe7d7613d18cab860d"
37
+ },
38
+ "grouping": "object IDs, shared Amazon item IDs, exact mesh/image/target hashes",
39
+ "limitations": "Near duplicates can remain. Cross-dataset overlap with ancestor training is not fully known; only recorded exact source hashes were excluded."
40
+ },
41
+ "baseline_validation": {
42
+ "occupancy": 0.5408538651877436,
43
+ "position": 0.0941663452948647,
44
+ "colour": 0.046021819156333646,
45
+ "scale": 0.002074153210627231,
46
+ "rotation": 1.2924113261747727e-05,
47
+ "opacity": 0.0002730773832950192,
48
+ "geometry": 0.0,
49
+ "loss": 0.7275456369950853,
50
+ "occupancy_iou": 0.2717048296217817
51
+ }
52
+ }