Spaces:
Running on Zero
Running on Zero
Initial Hi3D v1.0 demo
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +5 -0
- AUTHORS +26 -0
- LICENSE +223 -0
- NOTICE +49 -0
- README.md +28 -7
- THIRD_PARTY_LICENSES.md +474 -0
- app.py +240 -0
- examples/typical_building_castle.png +3 -0
- examples/typical_creature_robot_dinosour.png +3 -0
- examples/typical_humanoid_mech.png +3 -0
- examples/typical_misc_phonograph.png +3 -0
- examples/typical_vehicle_excavator.png +3 -0
- requirements.txt +24 -0
- sparc3d/__init__.py +31 -0
- sparc3d/models/__init__.py +1 -0
- sparc3d/models/autoencoders/__init__.py +11 -0
- sparc3d/models/autoencoders/blocks.py +575 -0
- sparc3d/models/autoencoders/config.py +151 -0
- sparc3d/models/autoencoders/ptv3.py +118 -0
- sparc3d/models/autoencoders/sparconv_vae.py +63 -0
- sparc3d/models/conditioners/__init__.py +5 -0
- sparc3d/models/conditioners/image.py +169 -0
- sparc3d/models/transformers/__init__.py +6 -0
- sparc3d/models/transformers/config.py +195 -0
- sparc3d/models/transformers/sparc3d_transformer.py +280 -0
- sparc3d/nn/__init__.py +5 -0
- sparc3d/nn/norm.py +17 -0
- sparc3d/nn/rope.py +93 -0
- sparc3d/nn/sparse/__init__.py +14 -0
- sparc3d/nn/sparse/attention/__init__.py +5 -0
- sparc3d/nn/sparse/attention/full_attn.py +77 -0
- sparc3d/nn/sparse/attention/modules.py +123 -0
- sparc3d/nn/sparse/basic.py +185 -0
- sparc3d/nn/sparse/conv.py +88 -0
- sparc3d/nn/sparse/linear.py +18 -0
- sparc3d/nn/sparse/nonlinearity.py +18 -0
- sparc3d/nn/sparse/spatial.py +125 -0
- sparc3d/nn/sparse/transformer/__init__.py +5 -0
- sparc3d/nn/sparse/transformer/blocks.py +34 -0
- sparc3d/nn/sparse/transformer/modulated.py +65 -0
- sparc3d/nn/utils.py +29 -0
- sparc3d/ops/__init__.py +8 -0
- sparc3d/ops/_triton.py +21 -0
- sparc3d/ops/flexicubes/LICENSE.txt +202 -0
- sparc3d/ops/flexicubes/__init__.py +12 -0
- sparc3d/ops/flexicubes/flexicubes.py +349 -0
- sparc3d/ops/flexicubes/tables.py +546 -0
- sparc3d/ops/sparse_backend.py +288 -0
- sparc3d/ops/triton/__init__.py +7 -0
- sparc3d/ops/triton/voxelize.py +302 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
examples/typical_building_castle.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
examples/typical_creature_robot_dinosour.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
examples/typical_humanoid_mech.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
examples/typical_misc_phonograph.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
examples/typical_vehicle_excavator.png filter=lfs diff=lfs merge=lfs -text
|
AUTHORS
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SPARC3D authors and contributors
|
| 2 |
+
================================
|
| 3 |
+
|
| 4 |
+
Sparc3D: Sparse Representation and Construction for High-Resolution 3D Shapes
|
| 5 |
+
Modeling — https://arxiv.org/abs/2505.14521
|
| 6 |
+
|
| 7 |
+
Paper authors
|
| 8 |
+
-------------
|
| 9 |
+
|
| 10 |
+
Zhihao Li
|
| 11 |
+
Yufei Wang
|
| 12 |
+
Heliang Zheng
|
| 13 |
+
Yihao Luo
|
| 14 |
+
Bihan Wen
|
| 15 |
+
|
| 16 |
+
Component credits
|
| 17 |
+
-----------------
|
| 18 |
+
|
| 19 |
+
Named here because the credit belongs with the code, not only with the paper.
|
| 20 |
+
|
| 21 |
+
Zhihao Li GPU surface voxelization in sparc3d/ops, originally written as
|
| 22 |
+
the pytorch-voxelization extension and implemented as Triton
|
| 23 |
+
kernels for this release.
|
| 24 |
+
|
| 25 |
+
Third-party code redistributed with this package is credited separately in
|
| 26 |
+
NOTICE and THIRD_PARTY_LICENSES.md.
|
LICENSE
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hi3D Open License Agreement Version 1.0
|
| 2 |
+
|
| 3 |
+
| Item | Details |
|
| 4 |
+
| --- | --- |
|
| 5 |
+
| Licensor | OCEANO HK HOLDING LIMITED |
|
| 6 |
+
| Licensed Project | Hi3D V1.0 |
|
| 7 |
+
| Version | Version 1.0 |
|
| 8 |
+
| Date | October 22, 2026 |
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
IMPORTANT NOTICE: By downloading, accessing, using, reproducing, modifying, distributing, displaying or otherwise exercising any rights in the Hi3D Materials, you acknowledge that you have read, understood and agreed to this Agreement. If you do not agree to this Agreement, do not use the Hi3D Materials.
|
| 13 |
+
|
| 14 |
+
# 1 Definitions
|
| 15 |
+
|
| 16 |
+
For purposes of this Agreement, the following terms have the meanings set out below.
|
| 17 |
+
|
| 18 |
+
## 1.1 Agreement
|
| 19 |
+
|
| 20 |
+
"Agreement" means this Hi3D Open License Agreement Version 1.0 issued with the applicable version of the Hi3D Materials, including Schedule 1, Acceptable Use Policy, and Schedule 2, Licensed Asset Schedule.
|
| 21 |
+
|
| 22 |
+
## 1.2 Licensor
|
| 23 |
+
|
| 24 |
+
"Licensor," "we," "us" or "our" means OCEANO HK HOLDING LIMITED, solely to the extent that it owns or is authorized to license the relevant rights in the Hi3D Materials.
|
| 25 |
+
|
| 26 |
+
## 1.3 You
|
| 27 |
+
|
| 28 |
+
"You" means the individual or legal entity exercising rights under this Agreement. A "legal entity" includes the entity acting hereunder and any entity that controls, is controlled by, or is under common control with it. "Control" means direct or indirect ownership of more than fifty percent of the voting interests, or the power to direct management.
|
| 29 |
+
|
| 30 |
+
## 1.4 Hi3D Materials
|
| 31 |
+
|
| 32 |
+
"Hi3D Materials" means the source code, model architecture, model weights and parameters, inference code, documentation and other materials that are owned by or licensable by the Licensor and expressly identified in the specific release listed in Schedule 2 as being subject to this Agreement. An asset is not automatically included in this Agreement merely because it has a similar name or purpose or is stored in the same repository, unless it has actually been released and is listed in Schedule 2.
|
| 33 |
+
|
| 34 |
+
## 1.5 Excluded Materials
|
| 35 |
+
|
| 36 |
+
"Excluded Materials" means third-party software, models, weights and their outputs; training, validation and test datasets; sample media, fonts and other third-party assets; first-stage systems or server-side code not listed in Schedule 2; trademarks and brand assets except as expressly permitted under Section 8; and any other items expressly subject to separate terms. Excluded Materials are not licensed under this Agreement. Sample code calling a third-party model does not alter the licensing status of that third-party model or its outputs.
|
| 37 |
+
|
| 38 |
+
## 1.6 Model Derivative
|
| 39 |
+
|
| 40 |
+
"Model Derivative" means another machine-learning model created by modifying, fine-tuning, adapting or quantizing all or a substantial part of the Hi3D model weights or parameters, or otherwise based on or incorporating the foregoing. Standalone Outputs, including generated 3D meshes and model files and finished works created by editing, converting or combining such files, do not become Model Derivatives merely because they were generated or post-processed; however, any portion of a finished work that actually contains Hi3D code, weights or parameters remains subject to this Agreement according to its nature.
|
| 41 |
+
|
| 42 |
+
## 1.7 Output
|
| 43 |
+
|
| 44 |
+
"Output" means content or information generated by running the Hi3D Materials or a Model Derivative, including generated 3D geometry, meshes and exported 3D files. Any third-party asset actually contained in an Output remains subject to the applicable third-party rights or separate license.
|
| 45 |
+
|
| 46 |
+
## 1.8 Commercial Product or Service
|
| 47 |
+
|
| 48 |
+
"Commercial Product or Service" means any product, service or hosted functionality that uses or contains the Hi3D Materials or a Model Derivative and is offered for commercial advantage, monetary consideration, revenue generation or other material commercial benefit.
|
| 49 |
+
|
| 50 |
+
## 1.9 Related Product or Service
|
| 51 |
+
|
| 52 |
+
"Related Product or Service" means a product or service that uses or contains the Hi3D Materials or a Model Derivative. Substantially similar services split among different brands, applications, interfaces, domain names or affiliated entities to evade the Scale Threshold must be aggregated.
|
| 53 |
+
|
| 54 |
+
## 1.10 Monthly Active Users
|
| 55 |
+
|
| 56 |
+
"Monthly Active Users" or "MAU" means the number of unique natural persons who actively use a Related Product or Service during a calendar month.
|
| 57 |
+
|
| 58 |
+
## 1.11 First Stage
|
| 59 |
+
|
| 60 |
+
"First Stage" means coarse-mesh generation that generates the basic three-dimensional shape of an object from a reference image, determines its principal structure and approximate contours, and outputs a coarse mesh for subsequent processing.
|
| 61 |
+
|
| 62 |
+
## 1.12 Second Stage
|
| 63 |
+
|
| 64 |
+
"Second Stage" means geometry refinement that takes a reference image and the coarse mesh produced by the First Stage as inputs, progressively refines the three-dimensional shape, and generates higher-resolution three-dimensional geometry and mesh files.
|
| 65 |
+
|
| 66 |
+
# 2 License Grant
|
| 67 |
+
|
| 68 |
+
Subject to your continuing compliance with this Agreement, and solely with respect to rights in the Hi3D Materials that the Licensor owns or is authorized to license, the Licensor grants you a worldwide, non-exclusive, royalty-free license to use, reproduce, modify, fine-tune and adapt the Hi3D Materials in source-code, object-code, weight or other applicable form; create Model Derivatives and other derivative works; publicly display and publicly perform them; and distribute, sublicense and sell the Hi3D Materials. You may also deploy the Hi3D Materials on servers, cloud platforms or other computing environments that you control or lawfully use, and provide third parties with hosted services based on the Hi3D Materials through websites, applications, application programming interfaces (APIs), software as a service (SaaS) or other remote-access methods. This license may be transferred only through sublicensing or distribution that complies with this Agreement.
|
| 69 |
+
|
| 70 |
+
## 2.1 Commercial Use
|
| 71 |
+
|
| 72 |
+
Commercial use is permitted subject to Sections 5, 6, 7 and 8. No revenue threshold applies separately from Section 5.
|
| 73 |
+
|
| 74 |
+
## 2.2 Training Other AI Models
|
| 75 |
+
|
| 76 |
+
To the extent the Licensor has the right to grant such permission, you may use the Hi3D Materials and their Outputs to train, evaluate, improve or develop other artificial-intelligence or machine-learning models, subject to applicable law, third-party rights and Schedule 1. This permission does not extend to coarse models, outputs or other third-party materials generated through third-party models, and does not replace their separate license terms. Another model is a Model Derivative only if it satisfies the definition in Section 1.6.
|
| 77 |
+
|
| 78 |
+
# 3 Patent License
|
| 79 |
+
|
| 80 |
+
Subject to your compliance with this Agreement, and solely with respect to patent claims that the Licensor owns or is authorized to license and that are necessarily infringed by implementation of the Hi3D Materials, the Licensor grants you a perpetual, worldwide, non-exclusive, royalty-free patent license to make, have made, use, offer for sale, sell, import and otherwise transfer the Hi3D Materials. A patent license from any other contributor arises only if that contributor has the right to grant it and expressly agrees to provide it under this Agreement.
|
| 81 |
+
|
| 82 |
+
## 3.1 Patent Litigation
|
| 83 |
+
|
| 84 |
+
If you institute patent litigation against the Licensor or any contributor alleging that the Hi3D Materials or a contribution directly or contributorily infringes a patent, all patent licenses granted to you under this Agreement for the Hi3D Materials terminate as of the date the claim is filed.
|
| 85 |
+
|
| 86 |
+
# 4 Distribution and Sublicensing Conditions
|
| 87 |
+
|
| 88 |
+
You may distribute the Hi3D Materials and Model Derivatives, including as part of a proprietary or closed-source product, only if you comply with all of the following conditions. Merely reproducing, editing, displaying, selling or distributing Outputs that do not contain Hi3D Materials or Model Derivatives is not distribution under this Section, and does not by itself require those Outputs to be accompanied by this Agreement, a NOTICE file or a modification notice.
|
| 89 |
+
|
| 90 |
+
- Provide each recipient of files containing the Hi3D Materials or a Model Derivative with the full text of this Agreement upon delivery, or a link that provides continuous, free access to the full text of the corresponding version of this Agreement.
|
| 91 |
+
- Retain all applicable copyright, patent and attribution notices contained in the Hi3D Materials.
|
| 92 |
+
- Prominently identify the files, weights or materials that you modified.
|
| 93 |
+
- When distributing files containing the Hi3D Materials or a Model Derivative, include a readable NOTICE file containing the notice specified in Section 8.2 and all applicable original third-party notices and license requirements.
|
| 94 |
+
- Ensure that your terms do not restrict recipients from exercising the rights granted under this Agreement in unmodified portions of the Hi3D Materials, and pass the Acceptable Use Policy downstream as an enforceable condition.
|
| 95 |
+
|
| 96 |
+
## 4.1 Licensing of Model Derivatives
|
| 97 |
+
|
| 98 |
+
You may apply different terms, including proprietary terms, to your own modifications and to a Model Derivative as a whole; however, the portions that actually reproduce or embed Hi3D code, weights or parameters remain subject to this Agreement, and the applicable attribution, branding, scale and use conditions continue to apply. A finished 3D work containing only Outputs and not the foregoing Hi3D Materials is not subject to this subsection's conditions for Model Derivatives merely because it was generated using Hi3D.
|
| 99 |
+
|
| 100 |
+
# 5 Scale Threshold and Separate License
|
| 101 |
+
|
| 102 |
+
The Scale Threshold is triggered if one or more Related Products or Services exceed 200,000 Monthly Active Users in the most recent complete calendar month or generate monthly revenue of at least USD 1,000,000. Except for the anti-circumvention aggregation rule in Section 1.9, only Related Products or Services are counted.
|
| 103 |
+
|
| 104 |
+
## 5.1 Application
|
| 105 |
+
|
| 106 |
+
Within sixty days after first triggering the Scale Threshold, you must notify OCEANO HK HOLDING LIMITED and apply for a separate written license. The application must truthfully disclose the applicant, the affected products and services, the MAU calculation methodology, use cases, deployment regions, security measures and any other information reasonably requested by us.
|
| 107 |
+
|
| 108 |
+
## 5.2 Transition Period
|
| 109 |
+
|
| 110 |
+
If you submit a complete, truthful and timely application and remain compliant, you may continue your existing use during the sixty-day application period and while both parties are actively negotiating in good faith; however, without our written consent, you may not materially expand the affected use beyond its scale and scope when the Scale Threshold was first exceeded.
|
| 111 |
+
|
| 112 |
+
## 5.3 Decision on Separate License
|
| 113 |
+
|
| 114 |
+
A separate license may include fees, branding, reporting, audit, security, service-level or other commercial terms. We may decide, in our sole discretion, whether to grant or deny a separate license. No separate license is effective unless signed by an authorized representative of OCEANO HK HOLDING LIMITED or otherwise expressly confirmed by it in writing.
|
| 115 |
+
|
| 116 |
+
## 5.4 Failure to Obtain a License
|
| 117 |
+
|
| 118 |
+
If you fail to apply within the required period, make a materially false statement in an application, or continue the affected use after written denial or after good-faith negotiations end without agreement, your right to continue above-threshold use terminates. Any continuing above-threshold use is unauthorized and constitutes a material breach.
|
| 119 |
+
|
| 120 |
+
# 6 Acceptable Use Policy
|
| 121 |
+
|
| 122 |
+
Your use of the Hi3D Materials and Model Derivatives, and your conduct in generating Outputs with them, must comply with Schedule 1, applicable law and third-party rights. This Agreement does not otherwise restrict the ordinary reproduction, editing, display, sale or distribution of Outputs that have been generated and do not contain Hi3D Materials or Model Derivatives; you remain responsible for complying with applicable law and any third-party rights contained in the Outputs. You may not knowingly assist another person in circumventing restrictions on the use of the Hi3D Materials or Model Derivatives.
|
| 123 |
+
|
| 124 |
+
## 6.1 Policy Version
|
| 125 |
+
|
| 126 |
+
The version of Schedule 1 accompanying the Hi3D version you receive applies to that version. We may issue a revised policy for later versions. Material revisions do not apply retroactively to prior versions unless you expressly accept them.
|
| 127 |
+
|
| 128 |
+
# 7 Commercial Branding Display
|
| 129 |
+
|
| 130 |
+
Any entity integrating the Hi3D Materials or a Model Derivative into a Commercial Product or Service must display "Powered by Hi3D 1.0" clearly and with reasonable prominence. A product with a user interface must display it in the interface, on an About page or in product acknowledgements. An API, SDK, backend or embedded product without a conventional user interface must display it in primary customer-accessible technical documentation, a service description or a legal notice. Where the display location supports a clickable link, the text must link to the official Hi3D website listed in Schedule 2; if a link cannot be provided, the official website address must be stated in the same location or related documentation. Merely obtaining or using a generated 3D Output file does not, by itself, create the branding-display obligation under this Section.
|
| 131 |
+
|
| 132 |
+
You may use the Licensor's commercial marks only to comply with the branding-display obligation under this Section. This permission may be revoked at any time and does not transfer any rights or constitute the Licensor's endorsement of you or any related product or service. The Licensor may require you in writing to cease using or remove the relevant marks, and you must do so within five (5) business days after receiving the notice. Once the marks have been removed, you will no longer be subject to the corresponding branding-display obligation.
|
| 133 |
+
|
| 134 |
+
## 7.1 No Endorsement
|
| 135 |
+
|
| 136 |
+
Unless we expressly agree in writing, you may not state or imply that your product, service, Model Derivative or Output was created, certified, endorsed or supported by OCEANO HK HOLDING LIMITED, or is an official Hi3D product.
|
| 137 |
+
|
| 138 |
+
# 8 Trademarks and Notices
|
| 139 |
+
|
| 140 |
+
Except for the limited license below, this Agreement grants no right to use the names, logos, trade dress, domain names or trademarks of OCEANO HK HOLDING LIMITED, Hi3D or their affiliates. Reasonable descriptive use to identify the source of the Hi3D Materials is permitted.
|
| 141 |
+
|
| 142 |
+
## 8.1 Limited Mark License
|
| 143 |
+
|
| 144 |
+
While you remain in compliance with this Agreement, we grant you a limited, non-exclusive, non-transferable and non-sublicensable license to use the "Hi3D" word mark only to the extent necessary to comply with Sections 4 and 7. This license terminates when your right to use the relevant Hi3D Materials terminates under Section 14. It does not include use of any logo or any use that implies endorsement, sponsorship or official status. All goodwill belongs to the applicable trademark owner.
|
| 145 |
+
|
| 146 |
+
## 8.2 Required Notice
|
| 147 |
+
|
| 148 |
+
The NOTICE file must state: "Hi3D is licensed under the Hi3D Open License Agreement Version 1.0. Copyright © 2026 OCEANO HK HOLDING LIMITED. All rights not expressly granted are reserved. The Hi3D trademarks are owned by OCEANO HK HOLDING LIMITED or its applicable affiliate(s)."
|
| 149 |
+
|
| 150 |
+
# 9 Outputs
|
| 151 |
+
|
| 152 |
+
As between you and us, we claim no ownership of Outputs generated by you. So long as an Output itself does not contain Hi3D Materials or a Model Derivative, you and downstream users who obtain that Output may reproduce, edit, display, sell and distribute it without being required, solely because of that use, to display "Powered by Hi3D 1.0," include this Agreement or a NOTICE file, or apply for a scale-based license under Section 5. This Section does not transfer rights and does not guarantee that an Output is copyrightable, original or non-infringing. Any applicable terms governing third-party models, inputs, materials or services must still be observed.
|
| 153 |
+
|
| 154 |
+
# 10 Third-Party Materials
|
| 155 |
+
|
| 156 |
+
Third-party software, models, weights and their outputs remain subject to their respective licenses and terms of use, and you are responsible for independently obtaining and complying with the relevant rights. Sample code included with this release may call Hunyuan3D-2mini to generate a first-stage coarse model, but Hi3D does not license that model's software, weights or outputs under this Agreement; the model version, source and license used by the sample must be identified in the release documentation. Other external models loaded for second-stage inference, and their actual versions, must also be listed in the third-party dependency schedule. Unless expressly stated otherwise, this Agreement does not license training data, sample media, fonts, datasets or other Excluded Materials.
|
| 157 |
+
|
| 158 |
+
# 11 Disclaimer
|
| 159 |
+
|
| 160 |
+
TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE HI3D MATERIALS, MODEL DERIVATIVES, OUTPUTS AND ALL RELATED RIGHTS ARE PROVIDED "AS IS" AND "AS AVAILABLE," WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, ACCURACY, SECURITY, RELIABILITY OR UNINTERRUPTED OPERATION. YOU ARE RESPONSIBLE FOR DETERMINING WHETHER YOUR USE IS APPROPRIATE AND LAWFUL.
|
| 161 |
+
|
| 162 |
+
# 12 Limitation of Liability
|
| 163 |
+
|
| 164 |
+
TO THE MAXIMUM EXTENT PERMITTED BY LAW, OCEANO HK HOLDING LIMITED, ITS AFFILIATES AND CONTRIBUTORS WILL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, PUNITIVE OR CONSEQUENTIAL DAMAGES, OR FOR ANY LOSS OF PROFITS, REVENUE, DATA OR GOODWILL, OR BUSINESS INTERRUPTION, ARISING FROM THIS AGREEMENT OR THE HI3D MATERIALS, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH LOSS. LIABILITY THAT CANNOT LAWFULLY BE EXCLUDED IS NOT EXCLUDED BY THIS SECTION.
|
| 165 |
+
|
| 166 |
+
# 13 Indemnification
|
| 167 |
+
|
| 168 |
+
You will, to the applicable extent, defend, indemnify and hold harmless OCEANO HK HOLDING LIMITED, its affiliates, officers, employees and contributors from and against third-party claims, losses, liabilities, damages, judgments, costs and reasonable legal fees arising from your breach of this Agreement; unlawful use; inputs; Outputs; modifications; Model Derivatives; products; services; or distribution. We will promptly notify you of a claim and, at your expense, provide reasonable assistance and permit you to control the defense and settlement; however, without the indemnified party's prior written consent, you may not admit liability on its behalf, impose obligations on it or agree to non-monetary relief against it.
|
| 169 |
+
|
| 170 |
+
# 14 Term and Termination
|
| 171 |
+
|
| 172 |
+
This Agreement takes effect when you first access the Hi3D Materials or exercise the relevant rights, and continues until terminated. Upon a material breach, your rights terminate immediately following our written notice. For any other breach, your rights terminate if you fail to cure within thirty days after our written notice. Patent termination under Section 3.1 and scale-based termination under Section 5.4 operate according to their respective terms.
|
| 173 |
+
|
| 174 |
+
## 14.1 Effect of Termination
|
| 175 |
+
|
| 176 |
+
Upon termination, you must cease any use no longer authorized under this Agreement and delete any Hi3D model weights and other Hi3D Materials in your possession or control that are required to be deleted. Records required by law and routine backups that cannot be immediately purged must not be used and must be deleted in accordance with the applicable retention period. Termination does not automatically terminate licenses lawfully granted before termination where downstream recipients remain compliant, but you may make no new distributions. Sections 1 and 7 through 16, and accrued rights, survive to the extent applicable.
|
| 177 |
+
|
| 178 |
+
# 15 Governing Law and Arbitration
|
| 179 |
+
|
| 180 |
+
This Agreement and any non-contractual obligations arising from it are governed by the laws of the Hong Kong Special Administrative Region, without regard to conflict-of-laws rules. Any dispute arising out of or relating to this Agreement will be finally resolved by arbitration administered by the Hong Kong International Arbitration Centre under the HKIAC Administered Arbitration Rules in force when the Notice of Arbitration is submitted. The seat of arbitration is Hong Kong, the tribunal will consist of one arbitrator, and the language of arbitration is English. Either party may seek urgent interim or injunctive relief from a court of competent jurisdiction.
|
| 181 |
+
|
| 182 |
+
# 16 General
|
| 183 |
+
|
| 184 |
+
This Agreement constitutes the entire agreement concerning this public license. To the extent a separate written commercial license expressly addresses a matter and conflicts with this Agreement, the commercial license controls. If any provision is unenforceable, it will be adjusted only to the minimum extent necessary and the remainder will remain in effect. Failure to enforce a right is not a waiver. Headings and translations do not affect the meaning of the terms. Except for a permitted transfer with a related product or business where the transferee accepts this Agreement, you may not assign this Agreement.
|
| 185 |
+
|
| 186 |
+
## 16.1 Non-Retroactive Updates
|
| 187 |
+
|
| 188 |
+
The license text accompanying the version of the Hi3D Materials you receive continues to apply to that version. We may license later versions under revised terms, but revisions do not retroactively alter rights already granted for earlier versions.
|
| 189 |
+
|
| 190 |
+
## 16.2 Contact
|
| 191 |
+
|
| 192 |
+
Applications for scale-based licenses and legal notices must be submitted using the contact details published on the official Hi3D release page, or sent to another address formally designated by OCEANO HK HOLDING LIMITED.
|
| 193 |
+
|
| 194 |
+
# Schedule 1 Acceptable Use Policy
|
| 195 |
+
|
| 196 |
+
You may not use the Hi3D Materials or Model Derivatives for any of the following activities, or generate or disseminate the prohibited content listed below through such use. This Schedule does not impose model-use obligations on a person merely because that person obtains or ordinarily uses a generated 3D finished work that does not contain Hi3D Materials or a Model Derivative; subsequent use of such a finished work remains subject to applicable law and third-party rights.
|
| 197 |
+
|
| 198 |
+
- Violate applicable law, sanctions, export controls, court orders or binding regulatory requirements;
|
| 199 |
+
- Infringe or misappropriate copyright, patent rights, trademark rights, trade secrets, privacy, data protection, publicity rights, image rights or other rights;
|
| 200 |
+
- Exploit, sexualize, groom or traffic minors, or otherwise harm minors;
|
| 201 |
+
- Create, distribute or facilitate malware, ransomware, unauthorized access, credential theft, destructive code or material network abuse;
|
| 202 |
+
- Deceptively impersonate another person, commit fraud, manipulate identity, or generate deceptive synthetic media without disclosure where disclosure is required by law;
|
| 203 |
+
- Intentionally facilitate violence, terrorism, self-harm, unlawful discrimination, stalking, harassment or targeted abuse;
|
| 204 |
+
- Unlawfully collect, infer, disclose or use personal or sensitive information, or undertake any of those activities for the purpose of harming another person;
|
| 205 |
+
- Circumvent technical safeguards or access controls in the Hi3D Materials, except for authorized good-faith security research conducted with appropriate safeguards;
|
| 206 |
+
- Make fully automated decisions in healthcare, employment, credit, housing, education, insurance, law enforcement or essential services that have legal or similarly significant effects, without legally required human oversight, testing, notice and appeal mechanisms; or
|
| 207 |
+
- Falsely represent a Model Derivative or product as an official Hi3D version or as endorsed by OCEANO HK HOLDING LIMITED.
|
| 208 |
+
|
| 209 |
+
This Policy does not prohibit lawful research, testing, auditing, red-teaming or development conducted with reasonable safeguards and not deployed for a prohibited purpose.
|
| 210 |
+
|
| 211 |
+
# Schedule 2 Licensed Asset Schedule
|
| 212 |
+
|
| 213 |
+
This Schedule identifies the assets in this release that are subject to the Hi3D Open License Agreement Version 1.0. The second-stage inference code, documentation and configuration listed below as "Included," together with the eight second-stage model files identified below for distribution under the name Hi3D v1.0 in the Hugging Face model repository at https://huggingface.co/hi3d-official/Hi3Dv1.0, are included under this Agreement to the extent the Licensor owns or is authorized to license them. Inclusion is not affected by the code and weights being stored separately on GitHub and Hugging Face. No third-party model, external dependency, unreleased first-stage material or file listed as "Excluded" is licensed under this Agreement.
|
| 214 |
+
|
| 215 |
+
| Item | Details |
|
| 216 |
+
| --- | --- |
|
| 217 |
+
| Code Release (Basis of Included Scope) | The Hi3D v1.0 source release at https://github.com/hi3d-official/Hi3Dv1.0 containing this Agreement. The included code, documentation and configuration are identified by the paths below. This Schedule applies to the files accompanying this version of the Agreement, not automatically to later repository versions. |
|
| 218 |
+
| Second-Stage Inference Code and Architecture (Included) | Under sparc3d/: models/, nn/, ops/, pipelines/, schedulers/, sparcubes/, structures/, utils/ and package initialization files; demo/refine_mesh.py, demo/image_to_mesh.py and demo/coarse_stage.py. Coverage is limited to implementations and modifications that the Licensor is authorized to license. coarse_stage.py is sample adapter code that calls an external Hunyuan model and does not include the Hunyuan code or weights. |
|
| 219 |
+
| Second-Stage Model Weights and Configuration (Included) | The following eight Hi3D v1.0 files are included under this Agreement at paths relative to the root of the Hugging Face model repository: model_index.json; scheduler/scheduler_config.json; transformer_512/config.json; transformer_512/model.safetensors (3,675,679,280 bytes); transformer_1024/config.json; transformer_1024/model.safetensors (3,675,679,280 bytes); vae/config.json; and vae/model.safetensors (376,629,712 bytes). transformer_1024/model.safetensors is used for both the 1024³ and 1536³ settings; there is no separate transformer_1536 weight in this release. The designated release location for these files is https://huggingface.co/hi3d-official/Hi3Dv1.0. This item excludes all other files in that repository and also excludes external models and weights such as Hunyuan3D-2mini, DINOv2 and CLIP. |
|
| 220 |
+
| Documentation and Configuration (Included to the Extent Licensable) | README.md, README.zh-CN.md, docs/installation.md, docs/pipeline.md and docs/reproduction.md, together with content in pyproject.toml, MANIFEST.in, docker/Dockerfile and docker/constraints.txt that the Licensor is authorized to license. Original notices in AUTHORS, NOTICE, THIRD_PARTY_LICENSES.md and third-party license files must be preserved unchanged. |
|
| 221 |
+
| Sample Media (Excluded) | assets/example.png, assets/framework.png, assets/global-comparison-1.jpg, assets/global-comparison-2.jpg and assets/local-detail-comparison.jpg are excluded. Their presence in this release does not grant any license to use them under this Agreement. Any use is subject to the applicable rights and separate permissions. |
|
| 222 |
+
| Unreleased and Third-Party Materials (Excluded) | Unreleased proprietary Hi3D first-stage materials, training code and datasets; original third-party rights in TRELLIS, Pointcept, O-CNN and FlexiCubes; Hunyuan3D-2mini code, weights and outputs called externally by the sample; and the facebook/dinov2-large and openai/clip-vit-large-patch14 weights separately obtained at runtime are not sublicensed under this Hi3D Agreement. Original third-party notices and their respective terms continue to apply. |
|
| 223 |
+
| Official Hi3D Websites | The link target for "Powered by Hi3D 1.0" under Section 7 is http://hi3d.ai/. The China website is https://www.hi3d.cn/. |
|
NOTICE
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
SPARC3D
|
| 2 |
+
Copyright 2026 The SPARC3D Authors
|
| 3 |
+
|
| 4 |
+
This product includes software developed by third parties. The following
|
| 5 |
+
components are redistributed under their own licenses, reproduced in full in
|
| 6 |
+
THIRD_PARTY_LICENSES.md:
|
| 7 |
+
|
| 8 |
+
* Microsoft TRELLIS (MIT License)
|
| 9 |
+
Copyright (c) Microsoft Corporation.
|
| 10 |
+
https://github.com/microsoft/TRELLIS
|
| 11 |
+
|
| 12 |
+
* Pointcept / Point Transformer V3 (MIT License)
|
| 13 |
+
Copyright (c) 2023 Pointcept
|
| 14 |
+
https://github.com/Pointcept/Pointcept
|
| 15 |
+
|
| 16 |
+
* O-CNN, ocnn-pytorch (MIT License)
|
| 17 |
+
Copyright (c) 2022 Peng-Shuai Wang <wangps@hotmail.com>
|
| 18 |
+
https://github.com/octree-nn/ocnn-pytorch
|
| 19 |
+
|
| 20 |
+
* NVIDIA FlexiCubes (Apache License 2.0)
|
| 21 |
+
Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES
|
| 22 |
+
https://github.com/nv-tlabs/FlexiCubes
|
| 23 |
+
License also included at sparc3d/ops/flexicubes/LICENSE.txt
|
| 24 |
+
|
| 25 |
+
GPU surface voxelization in sparc3d/ops descends from the PyTorch extension
|
| 26 |
+
written by Zhihao Li; see AUTHORS.
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
Hi3D v1.0
|
| 30 |
+
---------
|
| 31 |
+
Hi3D v1.0 includes the SPARC3D inference implementation. Original copyright,
|
| 32 |
+
attribution, license texts, and per-file modification notices are retained.
|
| 33 |
+
The Python package and API names are sparc3d / Sparc3DPipeline.
|
| 34 |
+
|
| 35 |
+
Hunyuan3D is referenced only by an optional coarse-mesh example adapter. Tencent's
|
| 36 |
+
Hunyuan3D implementation and weights are not bundled with or relicensed by this
|
| 37 |
+
distribution. External image encoders and any separately obtained model weights
|
| 38 |
+
retain their own terms. See THIRD_PARTY_LICENSES.md for the source and model
|
| 39 |
+
license index.
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
Hi3D Open License Agreement Version 1.0 notice
|
| 43 |
+
--------------------------------------------
|
| 44 |
+
The following notice applies to the Hi3D Materials identified in Schedule 2 of
|
| 45 |
+
LICENSE, only to the extent OCEANO HK HOLDING LIMITED owns or is authorized to
|
| 46 |
+
license them. It does not replace the original third-party notices above or
|
| 47 |
+
extend the Agreement to Excluded Materials. See THIRD_PARTY_LICENSES.md.
|
| 48 |
+
|
| 49 |
+
Hi3D is licensed under the Hi3D Open License Agreement Version 1.0. Copyright © 2026 OCEANO HK HOLDING LIMITED. All rights not expressly granted are reserved. The Hi3D trademarks are owned by OCEANO HK HOLDING LIMITED or its applicable affiliate(s).
|
README.md
CHANGED
|
@@ -1,13 +1,34 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.30.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Hi3D v1.0
|
| 3 |
+
emoji: 🗿
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.30.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
python_version: "3.10"
|
| 10 |
+
short_description: Image to high-res 3D mesh with Hi3D v1.0 refinement
|
| 11 |
+
startup_duration_timeout: 1h
|
| 12 |
+
license: other
|
| 13 |
+
license_name: hi3d-open-license-agreement-1.0
|
| 14 |
+
license_link: LICENSE
|
| 15 |
+
models:
|
| 16 |
+
- hi3d-official/Hi3Dv1.0
|
| 17 |
+
- tencent/Hunyuan3D-2mini
|
| 18 |
+
- facebook/dinov2-large
|
| 19 |
+
- openai/clip-vit-large-patch14
|
| 20 |
---
|
| 21 |
|
| 22 |
+
# Hi3D v1.0 demo
|
| 23 |
+
|
| 24 |
+
Image → coarse mesh ([Hunyuan3D-2mini-Turbo](https://huggingface.co/tencent/Hunyuan3D-2mini), or upload your own) →
|
| 25 |
+
high-resolution geometry refinement with [hi3d-official/Hi3Dv1.0](https://huggingface.co/hi3d-official/Hi3Dv1.0).
|
| 26 |
+
|
| 27 |
+
*Powered by Hi3D 1.0* — <http://hi3d.ai/>
|
| 28 |
+
|
| 29 |
+
- The `sparc3d/` package is the official inference code from <https://github.com/hi3d-official/Hi3Dv1.0>
|
| 30 |
+
(Hi3D Open License Agreement 1.0, plus retained Apache-2.0/MIT terms — see `LICENSE`, `NOTICE`,
|
| 31 |
+
`THIRD_PARTY_LICENSES.md`, `AUTHORS`). One patch: sparse convolutions use spconv's Native algorithm
|
| 32 |
+
(`SPCONV_ALGO=native`) so they run on ZeroGPU's Blackwell GPUs.
|
| 33 |
+
- Hunyuan3D-2mini is a third-party model under the Tencent Hunyuan Community License.
|
| 34 |
+
- Example images come from [microsoft/TRELLIS](https://github.com/microsoft/TRELLIS) (MIT License, © Microsoft Corporation).
|
THIRD_PARTY_LICENSES.md
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Third-party licenses
|
| 2 |
+
|
| 3 |
+
This license index adapts the SPARC3D third-party notices for Hi3D v1.0. The
|
| 4 |
+
original license texts and attributions are retained, with additional references
|
| 5 |
+
for separately obtained models and optional examples.
|
| 6 |
+
|
| 7 |
+
The original SPARC3D license is retained in [Original Apache license](#original-apache-license).
|
| 8 |
+
Hi3D-owned or otherwise licensable materials identified in Schedule 2 are governed
|
| 9 |
+
by the Hi3D Agreement in [LICENSE](LICENSE). See [License scope](#license-scope)
|
| 10 |
+
for the component boundaries; the new Agreement does not replace original
|
| 11 |
+
third-party licenses or previously granted rights.
|
| 12 |
+
|
| 13 |
+
Parts of this package are derived from other open-source projects. Each is listed
|
| 14 |
+
below with the files it covers and the full text of its license, as those licenses
|
| 15 |
+
require. Every file named here also carries a provenance header pointing back to
|
| 16 |
+
this document.
|
| 17 |
+
|
| 18 |
+
| Upstream | License | Covers |
|
| 19 |
+
| --- | --- | --- |
|
| 20 |
+
| [Microsoft TRELLIS](https://github.com/microsoft/TRELLIS) | MIT | the sparse transformer and its building blocks |
|
| 21 |
+
| [Pointcept / Point Transformer V3](https://github.com/Pointcept/Pointcept) | MIT | the `Point` structure and the sparse PTv3 decoder |
|
| 22 |
+
| [O-CNN (ocnn-pytorch)](https://github.com/octree-nn/ocnn-pytorch) | MIT | Z-order (Morton) encoding |
|
| 23 |
+
| [NVIDIA FlexiCubes](https://github.com/nv-tlabs/FlexiCubes) | Apache-2.0 | dual-marching-cubes surface extraction |
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## License scope
|
| 28 |
+
|
| 29 |
+
This index explains the component boundaries in Schedule 2 of the
|
| 30 |
+
[Hi3D Open License Agreement Version 1.0](LICENSE). It does not amend the
|
| 31 |
+
Agreement or grant additional rights. The Agreement and each applicable
|
| 32 |
+
third-party license govern their respective materials.
|
| 33 |
+
|
| 34 |
+
| Component | Applicable terms |
|
| 35 |
+
| --- | --- |
|
| 36 |
+
| Second-stage implementation and modifications in `sparc3d/`, including package initialization | The Hi3D Agreement applies only to content OCEANO HK HOLDING LIMITED owns or is authorized to license; original third-party rights in the same files remain under their original terms |
|
| 37 |
+
| `demo/refine_mesh.py`, `demo/image_to_mesh.py`, `demo/coarse_stage.py` | The Hi3D Agreement applies to licensable example code; Hunyuan's separate implementation, weights and outputs are excluded |
|
| 38 |
+
| The README, documentation and configuration paths listed in Schedule 2 | The Hi3D Agreement applies to licensable content; original notices and third-party terms remain in effect |
|
| 39 |
+
| The eight refinement artifacts listed in Schedule 2 | Distributed separately from the source code and Python packages under the Hi3D Agreement; other files or versions are not included merely by sharing a repository |
|
| 40 |
+
| Original SPARC3D, TRELLIS, Pointcept, O-CNN and FlexiCubes rights | Retained Apache-2.0 or MIT terms, as detailed in this file and the original source headers |
|
| 41 |
+
| Hunyuan3D-2mini, DINOv2, CLIP and other external dependencies | Their own upstream licenses; not relicensed by Hi3D |
|
| 42 |
+
| The five files listed as Sample Media in Schedule 2 | Excluded from the Hi3D Agreement; their presence or display does not grant reuse rights |
|
| 43 |
+
| Unreleased first-stage materials, training code, datasets, and brand assets | Excluded, subject to the limited trademark permissions expressly granted in the Agreement |
|
| 44 |
+
|
| 45 |
+
`LicenseRef-Hi3D-1.0` denotes the custom Agreement in [LICENSE](LICENSE), not a
|
| 46 |
+
blanket license for every file. Previously granted rights under applicable
|
| 47 |
+
Apache-2.0 or MIT licenses are not withdrawn or replaced by the Hi3D Agreement.
|
| 48 |
+
|
| 49 |
+
The source archive contains media expressly excluded by Schedule 2. Therefore
|
| 50 |
+
its package metadata does not assert a single SPDX license expression for the
|
| 51 |
+
whole archive. The complete applicable agreements and notices are included with
|
| 52 |
+
the Python distributions.
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
## Microsoft TRELLIS
|
| 57 |
+
|
| 58 |
+
Covers:
|
| 59 |
+
|
| 60 |
+
- `sparc3d/models/transformers/sparc3d_transformer.py`
|
| 61 |
+
- `sparc3d/nn/norm.py`
|
| 62 |
+
- `sparc3d/nn/rope.py`
|
| 63 |
+
- `sparc3d/nn/utils.py`
|
| 64 |
+
- `sparc3d/nn/sparse/basic.py`
|
| 65 |
+
- `sparc3d/nn/sparse/conv.py`
|
| 66 |
+
- `sparc3d/nn/sparse/linear.py`
|
| 67 |
+
- `sparc3d/nn/sparse/nonlinearity.py`
|
| 68 |
+
- `sparc3d/nn/sparse/spatial.py`
|
| 69 |
+
- `sparc3d/nn/sparse/attention/full_attn.py`
|
| 70 |
+
- `sparc3d/nn/sparse/attention/modules.py`
|
| 71 |
+
- `sparc3d/nn/sparse/transformer/blocks.py`
|
| 72 |
+
- `sparc3d/nn/sparse/transformer/modulated.py`
|
| 73 |
+
|
| 74 |
+
Each of these files states the specific SPARC3D modification in its header. In
|
| 75 |
+
general the training paths, the alternative attention backends, and the dense
|
| 76 |
+
variants were removed, leaving the released inference architecture.
|
| 77 |
+
|
| 78 |
+
```
|
| 79 |
+
MIT License
|
| 80 |
+
|
| 81 |
+
Copyright (c) Microsoft Corporation.
|
| 82 |
+
|
| 83 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 84 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 85 |
+
in the Software without restriction, including without limitation the rights
|
| 86 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 87 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 88 |
+
furnished to do so, subject to the following conditions:
|
| 89 |
+
|
| 90 |
+
The above copyright notice and this permission notice shall be included in all
|
| 91 |
+
copies or substantial portions of the Software.
|
| 92 |
+
|
| 93 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 94 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 95 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 96 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 97 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 98 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 99 |
+
SOFTWARE
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
---
|
| 103 |
+
|
| 104 |
+
## Pointcept / Point Transformer V3
|
| 105 |
+
|
| 106 |
+
Point Transformer V3 (Xiaoyang Wu et al.) is released as part of Pointcept.
|
| 107 |
+
|
| 108 |
+
Covers:
|
| 109 |
+
|
| 110 |
+
- `sparc3d/models/autoencoders/blocks.py`
|
| 111 |
+
- `sparc3d/models/autoencoders/ptv3.py`
|
| 112 |
+
- `sparc3d/structures/point.py`
|
| 113 |
+
- `sparc3d/structures/point_modules.py`
|
| 114 |
+
- `sparc3d/utils/misc.py`
|
| 115 |
+
- `sparc3d/utils/precision.py`
|
| 116 |
+
- `sparc3d/utils/serialization/default.py`
|
| 117 |
+
|
| 118 |
+
The decoder half was specialized for the sparse mesh autoencoder; the encoder,
|
| 119 |
+
the training paths, and the Hilbert serialization order were removed.
|
| 120 |
+
|
| 121 |
+
```
|
| 122 |
+
MIT License
|
| 123 |
+
|
| 124 |
+
Copyright (c) 2023 Pointcept
|
| 125 |
+
|
| 126 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 127 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 128 |
+
in the Software without restriction, including without limitation the rights
|
| 129 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 130 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 131 |
+
furnished to do so, subject to the following conditions:
|
| 132 |
+
|
| 133 |
+
The above copyright notice and this permission notice shall be included in all
|
| 134 |
+
copies or substantial portions of the Software.
|
| 135 |
+
|
| 136 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 137 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 138 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 139 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 140 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 141 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 142 |
+
SOFTWARE.
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
---
|
| 146 |
+
|
| 147 |
+
## O-CNN (ocnn-pytorch)
|
| 148 |
+
|
| 149 |
+
Covers:
|
| 150 |
+
|
| 151 |
+
- `sparc3d/utils/serialization/z_order.py`
|
| 152 |
+
|
| 153 |
+
The Morton decode tables and the batch-index field of the key were removed; only
|
| 154 |
+
`xyz2key` encoding is retained.
|
| 155 |
+
|
| 156 |
+
```
|
| 157 |
+
MIT License
|
| 158 |
+
|
| 159 |
+
Copyright (c) 2022 Peng-Shuai Wang <wangps@hotmail.com>
|
| 160 |
+
|
| 161 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 162 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 163 |
+
in the Software without restriction, including without limitation the rights
|
| 164 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 165 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 166 |
+
furnished to do so, subject to the following conditions:
|
| 167 |
+
|
| 168 |
+
The above copyright notice and this permission notice shall be included in
|
| 169 |
+
all copies or substantial portions of the Software.
|
| 170 |
+
|
| 171 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 172 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 173 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 174 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 175 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 176 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
| 177 |
+
THE SOFTWARE.
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
---
|
| 181 |
+
|
| 182 |
+
## NVIDIA FlexiCubes
|
| 183 |
+
|
| 184 |
+
Covers:
|
| 185 |
+
|
| 186 |
+
- `sparc3d/ops/flexicubes/`
|
| 187 |
+
|
| 188 |
+
FlexiCubes retains its Apache-2.0 license. Its copy of the
|
| 189 |
+
license ships in the package at `sparc3d/ops/flexicubes/LICENSE.txt`, satisfying
|
| 190 |
+
Apache-2.0 section 4(a); the NVIDIA copyright notices are retained in the source
|
| 191 |
+
files, satisfying 4(c); and `sparc3d/ops/flexicubes/flexicubes.py` carries a
|
| 192 |
+
header enumerating every SPARC3D modification, satisfying 4(b). Upstream ships no
|
| 193 |
+
`NOTICE` file, so 4(d) does not apply.
|
| 194 |
+
|
| 195 |
+
The vendored copy is inference-only: the tetrahedral output, the learned
|
| 196 |
+
interpolation weights, the developability regularizer, and the gradient hooks
|
| 197 |
+
were removed.
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
---
|
| 201 |
+
|
| 202 |
+
## External models and optional examples
|
| 203 |
+
|
| 204 |
+
This section indexes components obtained separately at runtime or by the user.
|
| 205 |
+
It does not add their code or weights to this distribution, grant a license to
|
| 206 |
+
them, or replace their upstream terms. Neither the Hi3D Agreement nor a retained
|
| 207 |
+
source-code license automatically applies to external model weights or
|
| 208 |
+
third-party outputs.
|
| 209 |
+
|
| 210 |
+
### Required external image encoders
|
| 211 |
+
|
| 212 |
+
The default image conditioner loads the following models separately. Neither is
|
| 213 |
+
included in the Hi3D refinement checkpoint or in this source tree.
|
| 214 |
+
|
| 215 |
+
| Component | Role | Official model source | Upstream license reference |
|
| 216 |
+
| --- | --- | --- | --- |
|
| 217 |
+
| DINOv2-large | Spatial image features | [facebook/dinov2-large](https://huggingface.co/facebook/dinov2-large) | [Apache-2.0, DINOv2 LICENSE](https://github.com/facebookresearch/dinov2/blob/main/LICENSE) |
|
| 218 |
+
| CLIP ViT-L/14 | Global image conditioning | [openai/clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) | [MIT, CLIP LICENSE](https://github.com/openai/CLIP/blob/main/LICENSE) |
|
| 219 |
+
|
| 220 |
+
Consult each model's own model card and the terms supplied with the version you
|
| 221 |
+
obtain. Background-removal models used by the optional `rembg` integration are
|
| 222 |
+
also obtained separately and retain their own terms.
|
| 223 |
+
|
| 224 |
+
### Optional Hunyuan3D coarse-mesh example
|
| 225 |
+
|
| 226 |
+
`demo/coarse_stage.py` is a Hi3D / SPARC3D example adapter. It imports
|
| 227 |
+
`hy3dgen.shapegen.Hunyuan3DDiTFlowMatchingPipeline` only when that example is used.
|
| 228 |
+
Its default model is `tencent/Hunyuan3D-2mini`, subfolder
|
| 229 |
+
`hunyuan3d-dit-v2-mini-turbo`.
|
| 230 |
+
|
| 231 |
+
**Hunyuan3D is only an optional example of coarse-mesh generation.** It is not a
|
| 232 |
+
mandatory dependency of the Hi3D refinement library or a required product
|
| 233 |
+
integration. `demo/refine_mesh.py` and the core `sparc3d` package accept a supplied
|
| 234 |
+
coarse mesh without using Hunyuan3D. Other suitable coarse-mesh generators can be
|
| 235 |
+
used instead. Installing the `[inference]` or `[demo]` extras does not install
|
| 236 |
+
Hunyuan3D.
|
| 237 |
+
|
| 238 |
+
**Tencent's Hunyuan3D implementation and pretrained weights are third-party
|
| 239 |
+
releases, not open-source content released by Hi3D.** They are not bundled with
|
| 240 |
+
this repository and are not relicensed under the Hi3D Agreement or any
|
| 241 |
+
retained source-code license. Users who
|
| 242 |
+
choose this example obtain them separately and comply with their upstream terms.
|
| 243 |
+
|
| 244 |
+
| Component used by the example | Official source | Applicable upstream agreement |
|
| 245 |
+
| --- | --- | --- |
|
| 246 |
+
| Hunyuan3D-2 implementation (`hy3dgen`) | [Tencent-Hunyuan/Hunyuan3D-2](https://github.com/Tencent-Hunyuan/Hunyuan3D-2) | [Tencent Hunyuan 3D 2.0 Community License Agreement — source LICENSE](https://github.com/Tencent-Hunyuan/Hunyuan3D-2/blob/main/LICENSE) |
|
| 247 |
+
| Hunyuan3D-2mini weights, mini-turbo variant | [tencent/Hunyuan3D-2mini](https://huggingface.co/tencent/Hunyuan3D-2mini) | [Tencent Hunyuan 3D 2.0 Community License Agreement — model LICENSE](https://huggingface.co/tencent/Hunyuan3D-2mini/blob/main/LICENSE) |
|
| 248 |
+
|
| 249 |
+
Upstream license terms are version-specific. If you select a different model,
|
| 250 |
+
subfolder, or source version, consult its matching license and model card.
|
| 251 |
+
Example-only use does not waive applicable restrictions, attribution,
|
| 252 |
+
redistribution, or hosted-service obligations in those agreements.
|
| 253 |
+
|
| 254 |
+
### Hi3D refinement weights
|
| 255 |
+
|
| 256 |
+
The eight Hi3D refinement artifacts identified and released under Schedule 2
|
| 257 |
+
are governed by the Hi3D Agreement in [LICENSE](LICENSE). They are distributed
|
| 258 |
+
separately from this source repository; see [License scope](#license-scope).
|
| 259 |
+
Other model files are not covered merely because they share the repository or
|
| 260 |
+
use a similar name.
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
---
|
| 264 |
+
|
| 265 |
+
## Original Apache license
|
| 266 |
+
|
| 267 |
+
The following is the original SPARC3D Apache-2.0 text, retained without changes.
|
| 268 |
+
It continues to govern the materials and rights to which it originally applied;
|
| 269 |
+
the Hi3D Agreement does not revoke those grants.
|
| 270 |
+
|
| 271 |
+
```text
|
| 272 |
+
|
| 273 |
+
Apache License
|
| 274 |
+
Version 2.0, January 2004
|
| 275 |
+
http://www.apache.org/licenses/
|
| 276 |
+
|
| 277 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 278 |
+
|
| 279 |
+
1. Definitions.
|
| 280 |
+
|
| 281 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 282 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 283 |
+
|
| 284 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 285 |
+
the copyright owner that is granting the License.
|
| 286 |
+
|
| 287 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 288 |
+
other entities that control, are controlled by, or are under common
|
| 289 |
+
control with that entity. For the purposes of this definition,
|
| 290 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 291 |
+
direction or management of such entity, whether by contract or
|
| 292 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 293 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 294 |
+
|
| 295 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 296 |
+
exercising permissions granted by this License.
|
| 297 |
+
|
| 298 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 299 |
+
including but not limited to software source code, documentation
|
| 300 |
+
source, and configuration files.
|
| 301 |
+
|
| 302 |
+
"Object" form shall mean any form resulting from mechanical
|
| 303 |
+
transformation or translation of a Source form, including but
|
| 304 |
+
not limited to compiled object code, generated documentation,
|
| 305 |
+
and conversions to other media types.
|
| 306 |
+
|
| 307 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 308 |
+
Object form, made available under the License, as indicated by a
|
| 309 |
+
copyright notice that is included in or attached to the work
|
| 310 |
+
(an example is provided in the Appendix below).
|
| 311 |
+
|
| 312 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 313 |
+
form, that is based on (or derived from) the Work and for which the
|
| 314 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 315 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 316 |
+
of this License, Derivative Works shall not include works that remain
|
| 317 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 318 |
+
the Work and Derivative Works thereof.
|
| 319 |
+
|
| 320 |
+
"Contribution" shall mean any work of authorship, including
|
| 321 |
+
the original version of the Work and any modifications or additions
|
| 322 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 323 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 324 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 325 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 326 |
+
means any form of electronic, verbal, or written communication sent
|
| 327 |
+
to the Licensor or its representatives, including but not limited to
|
| 328 |
+
communication on electronic mailing lists, source code control systems,
|
| 329 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 330 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 331 |
+
excluding communication that is conspicuously marked or otherwise
|
| 332 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 333 |
+
|
| 334 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 335 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 336 |
+
subsequently incorporated within the Work.
|
| 337 |
+
|
| 338 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 339 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 340 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 341 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 342 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 343 |
+
Work and such Derivative Works in Source or Object form.
|
| 344 |
+
|
| 345 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 346 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 347 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 348 |
+
(except as stated in this section) patent license to make, have made,
|
| 349 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 350 |
+
where such license applies only to those patent claims licensable
|
| 351 |
+
by such Contributor that are necessarily infringed by their
|
| 352 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 353 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 354 |
+
institute patent litigation against any entity (including a
|
| 355 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 356 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 357 |
+
or contributory patent infringement, then any patent licenses
|
| 358 |
+
granted to You under this License for that Work shall terminate
|
| 359 |
+
as of the date such litigation is filed.
|
| 360 |
+
|
| 361 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 362 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 363 |
+
modifications, and in Source or Object form, provided that You
|
| 364 |
+
meet the following conditions:
|
| 365 |
+
|
| 366 |
+
(a) You must give any other recipients of the Work or
|
| 367 |
+
Derivative Works a copy of this License; and
|
| 368 |
+
|
| 369 |
+
(b) You must cause any modified files to carry prominent notices
|
| 370 |
+
stating that You changed the files; and
|
| 371 |
+
|
| 372 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 373 |
+
that You distribute, all copyright, patent, trademark, and
|
| 374 |
+
attribution notices from the Source form of the Work,
|
| 375 |
+
excluding those notices that do not pertain to any part of
|
| 376 |
+
the Derivative Works; and
|
| 377 |
+
|
| 378 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 379 |
+
distribution, then any Derivative Works that You distribute must
|
| 380 |
+
include a readable copy of the attribution notices contained
|
| 381 |
+
within such NOTICE file, excluding those notices that do not
|
| 382 |
+
pertain to any part of the Derivative Works, in at least one
|
| 383 |
+
of the following places: within a NOTICE text file distributed
|
| 384 |
+
as part of the Derivative Works; within the Source form or
|
| 385 |
+
documentation, if provided along with the Derivative Works; or,
|
| 386 |
+
within a display generated by the Derivative Works, if and
|
| 387 |
+
wherever such third-party notices normally appear. The contents
|
| 388 |
+
of the NOTICE file are for informational purposes only and
|
| 389 |
+
do not modify the License. You may add Your own attribution
|
| 390 |
+
notices within Derivative Works that You distribute, alongside
|
| 391 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 392 |
+
that such additional attribution notices cannot be construed
|
| 393 |
+
as modifying the License.
|
| 394 |
+
|
| 395 |
+
You may add Your own copyright statement to Your modifications and
|
| 396 |
+
may provide additional or different license terms and conditions
|
| 397 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 398 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 399 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 400 |
+
the conditions stated in this License.
|
| 401 |
+
|
| 402 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 403 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 404 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 405 |
+
this License, without any additional terms or conditions.
|
| 406 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 407 |
+
the terms of any separate license agreement you may have executed
|
| 408 |
+
with Licensor regarding such Contributions.
|
| 409 |
+
|
| 410 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 411 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 412 |
+
except as required for reasonable and customary use in describing the
|
| 413 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 414 |
+
|
| 415 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 416 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 417 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 418 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 419 |
+
implied, including, without limitation, any warranties or conditions
|
| 420 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 421 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 422 |
+
appropriateness of using or redistributing the Work and assume any
|
| 423 |
+
risks associated with Your exercise of permissions under this License.
|
| 424 |
+
|
| 425 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 426 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 427 |
+
unless required by applicable law (such as deliberate and grossly
|
| 428 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 429 |
+
liable to You for damages, including any direct, indirect, special,
|
| 430 |
+
incidental, or consequential damages of any character arising as a
|
| 431 |
+
result of this License or out of the use or inability to use the
|
| 432 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 433 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 434 |
+
other commercial damages or losses), even if such Contributor
|
| 435 |
+
has been advised of the possibility of such damages.
|
| 436 |
+
|
| 437 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 438 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 439 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 440 |
+
or other liability obligations and/or rights consistent with this
|
| 441 |
+
License. However, in accepting such obligations, You may act only
|
| 442 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 443 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 444 |
+
defend, and hold each Contributor harmless for any liability
|
| 445 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 446 |
+
of your accepting any such warranty or additional liability.
|
| 447 |
+
|
| 448 |
+
END OF TERMS AND CONDITIONS
|
| 449 |
+
|
| 450 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 451 |
+
|
| 452 |
+
To apply the Apache License to your work, attach the following
|
| 453 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 454 |
+
replaced with your own identifying information. (Don't include
|
| 455 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 456 |
+
comment syntax for the file format. We also recommend that a
|
| 457 |
+
file or class name and description of purpose be included on the
|
| 458 |
+
same "printed page" as the copyright notice for easier
|
| 459 |
+
identification within third-party archives.
|
| 460 |
+
|
| 461 |
+
Copyright 2026 The SPARC3D Authors
|
| 462 |
+
|
| 463 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 464 |
+
you may not use this file except in compliance with the License.
|
| 465 |
+
You may obtain a copy of the License at
|
| 466 |
+
|
| 467 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 468 |
+
|
| 469 |
+
Unless required by applicable law or agreed to in writing, software
|
| 470 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 471 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 472 |
+
See the License for the specific language governing permissions and
|
| 473 |
+
limitations under the License.
|
| 474 |
+
```
|
app.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
os.environ.setdefault("SPCONV_ALGO", "native")
|
| 4 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 5 |
+
|
| 6 |
+
import spaces # noqa: E402 (must precede torch)
|
| 7 |
+
|
| 8 |
+
import random # noqa: E402
|
| 9 |
+
import tempfile # noqa: E402
|
| 10 |
+
import time # noqa: E402
|
| 11 |
+
|
| 12 |
+
import gradio as gr # noqa: E402
|
| 13 |
+
import numpy as np # noqa: E402
|
| 14 |
+
import torch # noqa: E402
|
| 15 |
+
import trimesh # noqa: E402
|
| 16 |
+
from PIL import Image # noqa: E402
|
| 17 |
+
|
| 18 |
+
from sparc3d import Sparc3DPipeline # noqa: E402
|
| 19 |
+
|
| 20 |
+
MODEL_ID = "hi3d-official/Hi3Dv1.0"
|
| 21 |
+
COARSE_REPO = "tencent/Hunyuan3D-2mini"
|
| 22 |
+
COARSE_SUBFOLDER = "hunyuan3d-dit-v2-mini-turbo"
|
| 23 |
+
COARSE_STEPS = 30 # matches demo/coarse_stage.py
|
| 24 |
+
COARSE_OCTREE_RESOLUTION = 256 # matches demo/coarse_stage.py
|
| 25 |
+
MAX_SEED = 2**31 - 1
|
| 26 |
+
|
| 27 |
+
# ---------------------------------------------------------------- models
|
| 28 |
+
print("Loading Hi3D v1.0 refinement pipeline...")
|
| 29 |
+
pipe = Sparc3DPipeline.from_pretrained(MODEL_ID)
|
| 30 |
+
_ = pipe.transformer_1024 # materialize the deferred high-res transformer now
|
| 31 |
+
pipe.to("cuda")
|
| 32 |
+
|
| 33 |
+
print("Loading Hunyuan3D-2mini (turbo) coarse generator...")
|
| 34 |
+
from hy3dgen.shapegen import Hunyuan3DDiTFlowMatchingPipeline # noqa: E402
|
| 35 |
+
|
| 36 |
+
coarse_pipe = Hunyuan3DDiTFlowMatchingPipeline.from_pretrained(
|
| 37 |
+
COARSE_REPO,
|
| 38 |
+
subfolder=COARSE_SUBFOLDER,
|
| 39 |
+
use_safetensors=True,
|
| 40 |
+
device="cuda",
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
print("Loading background remover...")
|
| 44 |
+
from rembg import new_session, remove # noqa: E402
|
| 45 |
+
|
| 46 |
+
rembg_session = new_session()
|
| 47 |
+
print("All models loaded.")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# ---------------------------------------------------------------- helpers
|
| 51 |
+
def _has_transparency(image: Image.Image) -> bool:
|
| 52 |
+
if image.mode not in ("RGBA", "LA") and "transparency" not in image.info:
|
| 53 |
+
return False
|
| 54 |
+
alpha = np.asarray(image.convert("RGBA").getchannel("A"))
|
| 55 |
+
return bool((alpha < 250).any())
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _prepare_input(image: Image.Image, remove_background: bool) -> Image.Image:
|
| 59 |
+
if remove_background and not _has_transparency(image):
|
| 60 |
+
image = remove(image.convert("RGB"), session=rembg_session, bgcolor=[255, 255, 255, 0])
|
| 61 |
+
return image.convert("RGBA")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _export(mesh: trimesh.Trimesh, suffix: str = ".glb") -> str:
|
| 65 |
+
path = tempfile.NamedTemporaryFile(suffix=suffix, delete=False).name
|
| 66 |
+
mesh.export(path)
|
| 67 |
+
return path
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _duration(image, coarse_mesh=None, resolution=1024, num_inference_steps=25, *args, **kwargs):
|
| 71 |
+
passes = {512: 1, 1024: 2, 1536: 3}.get(int(resolution), 2)
|
| 72 |
+
return int(40 + passes * (30 + 2.4 * int(num_inference_steps)))
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# ---------------------------------------------------------------- inference
|
| 76 |
+
@spaces.GPU(duration=_duration)
|
| 77 |
+
def refine(
|
| 78 |
+
image: Image.Image,
|
| 79 |
+
coarse_mesh: str | None = None,
|
| 80 |
+
resolution: int = 1024,
|
| 81 |
+
num_inference_steps: int = 25,
|
| 82 |
+
seed: int = 0,
|
| 83 |
+
randomize_seed: bool = False,
|
| 84 |
+
remove_background: bool = True,
|
| 85 |
+
progress=gr.Progress(),
|
| 86 |
+
):
|
| 87 |
+
"""Generate a high-resolution 3D mesh from a single image with Hi3D v1.0.
|
| 88 |
+
|
| 89 |
+
A coarse mesh is generated with Hunyuan3D-2mini-Turbo (or taken from the
|
| 90 |
+
optional upload), then refined by the Hi3D v1.0 sparse flow model
|
| 91 |
+
conditioned on the reference image.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
image: Reference image of a single object (a cut-out with alpha works best).
|
| 95 |
+
coarse_mesh: Optional path to your own coarse mesh (.glb/.obj/.ply). When
|
| 96 |
+
omitted, Hunyuan3D-2mini-Turbo produces it from the image.
|
| 97 |
+
resolution: Target sparse extraction grid: 512 (1 pass), 1024 (2 passes)
|
| 98 |
+
or 1536 (3 passes).
|
| 99 |
+
num_inference_steps: Scheduler updates per refinement pass.
|
| 100 |
+
seed: Random seed.
|
| 101 |
+
randomize_seed: Pick a random seed instead of `seed`.
|
| 102 |
+
remove_background: Remove the background with rembg when the image has
|
| 103 |
+
no transparency.
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
Paths to the coarse mesh GLB, the refined mesh GLB, the refined GLB
|
| 107 |
+
as a downloadable file, the seed used and a status string.
|
| 108 |
+
"""
|
| 109 |
+
if image is None:
|
| 110 |
+
raise gr.Error("Please upload an image.")
|
| 111 |
+
if randomize_seed:
|
| 112 |
+
seed = random.randint(0, MAX_SEED)
|
| 113 |
+
seed = int(seed)
|
| 114 |
+
resolution = int(resolution)
|
| 115 |
+
num_inference_steps = int(num_inference_steps)
|
| 116 |
+
t0 = time.perf_counter()
|
| 117 |
+
|
| 118 |
+
progress(0.0, desc="Preparing image")
|
| 119 |
+
image = _prepare_input(image, remove_background)
|
| 120 |
+
|
| 121 |
+
if coarse_mesh:
|
| 122 |
+
progress(0.02, desc="Loading coarse mesh")
|
| 123 |
+
coarse = trimesh.load(coarse_mesh, force="mesh", process=False)
|
| 124 |
+
else:
|
| 125 |
+
progress(0.02, desc="Generating coarse mesh (Hunyuan3D-2mini-Turbo)")
|
| 126 |
+
coarse = coarse_pipe(
|
| 127 |
+
image=image,
|
| 128 |
+
num_inference_steps=COARSE_STEPS,
|
| 129 |
+
octree_resolution=COARSE_OCTREE_RESOLUTION,
|
| 130 |
+
generator=torch.manual_seed(seed),
|
| 131 |
+
output_type="trimesh",
|
| 132 |
+
)[0]
|
| 133 |
+
t_coarse = time.perf_counter() - t0
|
| 134 |
+
coarse_path = _export(coarse)
|
| 135 |
+
|
| 136 |
+
def _report(event):
|
| 137 |
+
frac = (event.pass_index + event.step / event.num_inference_steps) / event.num_passes
|
| 138 |
+
progress(
|
| 139 |
+
0.1 + 0.8 * frac,
|
| 140 |
+
desc=f"Hi3D pass {event.pass_index + 1}/{event.num_passes} "
|
| 141 |
+
f"(grid {event.structure_resolution}³) step {event.step}/{event.num_inference_steps}",
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
pipe.progress_callback = _report
|
| 145 |
+
try:
|
| 146 |
+
result = pipe(
|
| 147 |
+
image,
|
| 148 |
+
coarse,
|
| 149 |
+
resolution=resolution,
|
| 150 |
+
num_inference_steps=num_inference_steps,
|
| 151 |
+
seed=seed,
|
| 152 |
+
)
|
| 153 |
+
finally:
|
| 154 |
+
pipe.progress_callback = None
|
| 155 |
+
progress(0.95, desc="Exporting mesh")
|
| 156 |
+
refined_path = _export(result.mesh)
|
| 157 |
+
total = time.perf_counter() - t0
|
| 158 |
+
status = (
|
| 159 |
+
f"Done in {total:.1f}s (coarse {t_coarse:.1f}s) — resolution {result.resolution}, "
|
| 160 |
+
f"{len(result.mesh.faces):,} faces, seed {seed}"
|
| 161 |
+
)
|
| 162 |
+
print(status)
|
| 163 |
+
return coarse_path, refined_path, refined_path, seed, status
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
# ---------------------------------------------------------------- UI
|
| 167 |
+
CSS = """
|
| 168 |
+
#col-container { max-width: 1200px; margin: 0 auto; }
|
| 169 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "examples")
|
| 173 |
+
EXAMPLES = [
|
| 174 |
+
[os.path.join(EXAMPLES_DIR, name)]
|
| 175 |
+
for name in (
|
| 176 |
+
"typical_creature_robot_dinosour.png",
|
| 177 |
+
"typical_humanoid_mech.png",
|
| 178 |
+
"typical_building_castle.png",
|
| 179 |
+
"typical_misc_phonograph.png",
|
| 180 |
+
"typical_vehicle_excavator.png",
|
| 181 |
+
)
|
| 182 |
+
]
|
| 183 |
+
|
| 184 |
+
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="Hi3D v1.0") as demo:
|
| 185 |
+
with gr.Column(elem_id="col-container"):
|
| 186 |
+
gr.Markdown(
|
| 187 |
+
"""
|
| 188 |
+
# Hi3D v1.0 — high-resolution 3D geometry refinement
|
| 189 |
+
Upload an image of an object. A coarse mesh is generated with
|
| 190 |
+
[Hunyuan3D-2mini-Turbo](https://huggingface.co/tencent/Hunyuan3D-2mini) (or upload your own),
|
| 191 |
+
then [**Hi3D v1.0**](https://huggingface.co/hi3d-official/Hi3Dv1.0) — built on Sparc3D — refines
|
| 192 |
+
it into high-resolution geometry conditioned on the image. Geometry only (no textures).
|
| 193 |
+
[Model](https://huggingface.co/hi3d-official/Hi3Dv1.0) · [Code](https://github.com/hi3d-official/Hi3Dv1.0) · [Website](http://hi3d.ai/) — *Powered by Hi3D 1.0*
|
| 194 |
+
"""
|
| 195 |
+
)
|
| 196 |
+
with gr.Row():
|
| 197 |
+
with gr.Column(scale=1):
|
| 198 |
+
image_in = gr.Image(label="Reference image", type="pil", image_mode="RGBA", height=360)
|
| 199 |
+
coarse_in = gr.File(
|
| 200 |
+
label="Optional: your own coarse mesh (.glb / .obj / .ply) — leave empty to auto-generate",
|
| 201 |
+
file_types=[".glb", ".gltf", ".obj", ".ply", ".stl", ".off"],
|
| 202 |
+
type="filepath",
|
| 203 |
+
)
|
| 204 |
+
resolution = gr.Radio(
|
| 205 |
+
[512, 1024, 1536],
|
| 206 |
+
value=1024,
|
| 207 |
+
label="Resolution (refinement passes: 1 / 2 / 3)",
|
| 208 |
+
)
|
| 209 |
+
run_btn = gr.Button("Generate", variant="primary")
|
| 210 |
+
with gr.Accordion("Advanced settings", open=False):
|
| 211 |
+
steps = gr.Slider(5, 50, value=25, step=1, label="Inference steps per pass")
|
| 212 |
+
seed = gr.Slider(0, MAX_SEED, value=0, step=1, label="Seed")
|
| 213 |
+
randomize_seed = gr.Checkbox(value=False, label="Randomize seed")
|
| 214 |
+
remove_bg = gr.Checkbox(
|
| 215 |
+
value=True,
|
| 216 |
+
label="Remove background (only applied if the image has no transparency)",
|
| 217 |
+
)
|
| 218 |
+
with gr.Column(scale=2):
|
| 219 |
+
with gr.Row():
|
| 220 |
+
coarse_out = gr.Model3D(label="Coarse mesh", height=420, clear_color=[1, 1, 1, 1])
|
| 221 |
+
refined_out = gr.Model3D(label="Hi3D refined mesh", height=420, clear_color=[1, 1, 1, 1])
|
| 222 |
+
refined_file = gr.File(label="Download refined GLB")
|
| 223 |
+
status = gr.Markdown()
|
| 224 |
+
|
| 225 |
+
inputs = [image_in, coarse_in, resolution, steps, seed, randomize_seed, remove_bg]
|
| 226 |
+
outputs = [coarse_out, refined_out, refined_file, seed, status]
|
| 227 |
+
run_btn.click(refine, inputs=inputs, outputs=outputs, api_name="refine")
|
| 228 |
+
|
| 229 |
+
gr.Examples(
|
| 230 |
+
examples=EXAMPLES,
|
| 231 |
+
inputs=[image_in],
|
| 232 |
+
outputs=outputs,
|
| 233 |
+
fn=refine,
|
| 234 |
+
cache_examples=True,
|
| 235 |
+
cache_mode="lazy",
|
| 236 |
+
label="Examples (images from the MIT-licensed TRELLIS repo)",
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
if __name__ == "__main__":
|
| 240 |
+
demo.launch(mcp_server=True)
|
examples/typical_building_castle.png
ADDED
|
Git LFS Details
|
examples/typical_creature_robot_dinosour.png
ADDED
|
Git LFS Details
|
examples/typical_humanoid_mech.png
ADDED
|
Git LFS Details
|
examples/typical_misc_phonograph.png
ADDED
|
Git LFS Details
|
examples/typical_vehicle_excavator.png
ADDED
|
Git LFS Details
|
requirements.txt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch==2.8.0
|
| 2 |
+
torchvision==0.23.0
|
| 3 |
+
numpy<2
|
| 4 |
+
transformers>=5
|
| 5 |
+
diffusers
|
| 6 |
+
accelerate
|
| 7 |
+
safetensors
|
| 8 |
+
einops
|
| 9 |
+
omegaconf
|
| 10 |
+
pyyaml
|
| 11 |
+
tqdm
|
| 12 |
+
trimesh>=4.4.0,<5
|
| 13 |
+
pyfqmr
|
| 14 |
+
scikit-image
|
| 15 |
+
pymeshlab
|
| 16 |
+
pygltflib
|
| 17 |
+
xatlas
|
| 18 |
+
opencv-python==4.11.0.86
|
| 19 |
+
opencv-python-headless==4.11.0.86
|
| 20 |
+
rembg
|
| 21 |
+
onnxruntime
|
| 22 |
+
spconv-cu120==2.3.6
|
| 23 |
+
https://huggingface.co/datasets/multimodalart/zerogpu-blackwell-wheels/resolve/main/wheels/pt28-cu128-cp310/flash_attn-2.7.4.post1-cp310-cp310-linux_x86_64.whl
|
| 24 |
+
hy3dgen @ git+https://github.com/Tencent-Hunyuan/Hunyuan3D-2@f8db63096c8282cb27354314d896feba5ba6ff8a
|
sparc3d/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lazy public package for the SPARC3D pipeline, output, and progress types."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
__version__ = "0.1.0"
|
| 8 |
+
|
| 9 |
+
__all__ = ["Sparc3DPipeline", "Sparc3DPipelineOutput", "Sparc3DProgressEvent"]
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def __getattr__(name: str) -> Any:
|
| 13 |
+
"""Load one of the public symbols on first access."""
|
| 14 |
+
if name == "Sparc3DPipeline":
|
| 15 |
+
from sparc3d.pipelines.sparc3d_pipeline import Sparc3DPipeline
|
| 16 |
+
|
| 17 |
+
return Sparc3DPipeline
|
| 18 |
+
if name == "Sparc3DPipelineOutput":
|
| 19 |
+
from sparc3d.pipelines.output import Sparc3DPipelineOutput
|
| 20 |
+
|
| 21 |
+
return Sparc3DPipelineOutput
|
| 22 |
+
if name == "Sparc3DProgressEvent":
|
| 23 |
+
from sparc3d.pipelines.progress import Sparc3DProgressEvent
|
| 24 |
+
|
| 25 |
+
return Sparc3DProgressEvent
|
| 26 |
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def __dir__() -> list[str]:
|
| 30 |
+
"""Return module names including the lazy public symbols."""
|
| 31 |
+
return sorted({*globals(), *__all__})
|
sparc3d/models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Neural network models (sparse mesh autoencoder)."""
|
sparc3d/models/autoencoders/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sparse mesh autoencoder: the PTv3 decoder and its JSON loader."""
|
| 2 |
+
|
| 3 |
+
from sparc3d.models.autoencoders.config import load_sparconv_vae
|
| 4 |
+
from sparc3d.models.autoencoders.ptv3 import PTv3MeshDecoder
|
| 5 |
+
from sparc3d.models.autoencoders.sparconv_vae import SparconvVAE
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"SparconvVAE",
|
| 9 |
+
"PTv3MeshDecoder",
|
| 10 |
+
"load_sparconv_vae",
|
| 11 |
+
]
|
sparc3d/models/autoencoders/blocks.py
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sparse PTv3 building blocks: norms, residual blocks, serialized attention, resampling.
|
| 2 |
+
|
| 3 |
+
Module and parameter names are load-bearing: they define the checkpoint key layout.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
# Adapted from Pointcept / Point Transformer V3 (Xiaoyang Wu et al.) under the
|
| 7 |
+
# MIT License. Modified for SPARC3D: specialized for the sparse mesh
|
| 8 |
+
# autoencoder, with the training paths and drop-path removed.
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import warnings
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
from torch import nn
|
| 16 |
+
|
| 17 |
+
from sparc3d.ops import sparse_backend
|
| 18 |
+
from sparc3d.structures.point import Point
|
| 19 |
+
from sparc3d.structures.point_modules import PointModule, PointSequential
|
| 20 |
+
from sparc3d.utils.misc import batch2offset, offset2bincount
|
| 21 |
+
from sparc3d.utils.precision import force_fp32
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _flash_attn():
|
| 25 |
+
"""Import flash-attn when serialized attention is first constructed."""
|
| 26 |
+
try:
|
| 27 |
+
import flash_attn
|
| 28 |
+
except ImportError as exc:
|
| 29 |
+
raise ImportError(
|
| 30 |
+
"SparconvVAE serialized attention requires flash-attn; "
|
| 31 |
+
"install sparc3d[inference]"
|
| 32 |
+
) from exc
|
| 33 |
+
return flash_attn
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
_RESNET_SKIP_POINTS = 12_000_000
|
| 37 |
+
"""Default ``SparseResnetBlock.skip_points``: above this token count the residual
|
| 38 |
+
blocks pass the point through untouched and warn. The 1024 path can exceed what a
|
| 39 |
+
full residual pass fits in memory, and the released weights were measured with this
|
| 40 |
+
guard active, so it is reproduced behavior rather than a tuning knob.
|
| 41 |
+
|
| 42 |
+
Deliberately the same number as ``pipelines.sparc3d_pipeline.DECODE_FEATURE_CEILING``,
|
| 43 |
+
but a different policy: this one degrades a single block, while the pipeline raises
|
| 44 |
+
when a *completed* decode is too large to feed another pass."""
|
| 45 |
+
|
| 46 |
+
_GROUP_NORM_SPLIT = 8
|
| 47 |
+
"""Groups normalized per iteration. Group statistics are independent, so this only
|
| 48 |
+
bounds the temporaries, not the result."""
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class Swish(nn.Module):
|
| 52 |
+
"""``x * sigmoid(x)``."""
|
| 53 |
+
|
| 54 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 55 |
+
"""Apply the Swish activation elementwise."""
|
| 56 |
+
return x * torch.sigmoid(x)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class PointGroupNorm(PointModule):
|
| 60 |
+
"""Group normalization over the points of each batch element of a sparse point cloud."""
|
| 61 |
+
|
| 62 |
+
def __init__(
|
| 63 |
+
self,
|
| 64 |
+
channels: int,
|
| 65 |
+
group: int = 32,
|
| 66 |
+
min_group_channels: int = 4,
|
| 67 |
+
eps: float = 1e-6,
|
| 68 |
+
):
|
| 69 |
+
"""
|
| 70 |
+
Args:
|
| 71 |
+
channels: Feature channels.
|
| 72 |
+
group: Requested number of groups; reduced when ``channels`` is too small
|
| 73 |
+
to give every group ``min_group_channels`` channels.
|
| 74 |
+
min_group_channels: Minimum channels per group.
|
| 75 |
+
eps: Variance floor.
|
| 76 |
+
|
| 77 |
+
Raises:
|
| 78 |
+
ValueError: ``channels`` is not divisible by the resolved group count.
|
| 79 |
+
"""
|
| 80 |
+
super().__init__()
|
| 81 |
+
self.eps = eps
|
| 82 |
+
self.group = group
|
| 83 |
+
self.channels = channels
|
| 84 |
+
self.min_group_channels = min_group_channels
|
| 85 |
+
|
| 86 |
+
if self.min_group_channels * self.group > channels:
|
| 87 |
+
self.group = channels // self.min_group_channels
|
| 88 |
+
if self.group < 1 or channels % self.group != 0:
|
| 89 |
+
raise ValueError(
|
| 90 |
+
f"channels {channels} must be divisible by group {self.group}"
|
| 91 |
+
)
|
| 92 |
+
self.channels_per_group = channels // self.group
|
| 93 |
+
|
| 94 |
+
self.weights = nn.Parameter(torch.ones(1, channels))
|
| 95 |
+
self.bias = nn.Parameter(torch.zeros(1, channels))
|
| 96 |
+
|
| 97 |
+
@force_fp32(mode="convert_back")
|
| 98 |
+
def forward(self, point: Point) -> Point:
|
| 99 |
+
"""Normalize ``point.feat`` per batch element and group, in float32."""
|
| 100 |
+
feat = point.feat
|
| 101 |
+
batch = point.batch.detach().clone()
|
| 102 |
+
N, C = feat.shape
|
| 103 |
+
B = int(batch[-1] + 1)
|
| 104 |
+
|
| 105 |
+
G = self.group
|
| 106 |
+
CP = self.channels_per_group
|
| 107 |
+
feat_group = feat.view(N, G, CP)
|
| 108 |
+
feat_norm_all = feat.new_zeros((N, G, CP))
|
| 109 |
+
|
| 110 |
+
for start in range(0, G, _GROUP_NORM_SPLIT):
|
| 111 |
+
end = min(start + _GROUP_NORM_SPLIT, G)
|
| 112 |
+
|
| 113 |
+
feat_slice = feat_group[:, start:end, :]
|
| 114 |
+
sum_feat = feat_slice.sum(dim=2)
|
| 115 |
+
sum_feat_sq = feat_slice.square().sum(dim=2)
|
| 116 |
+
|
| 117 |
+
# Per-(batch, group) moments, accumulated over the points of each element.
|
| 118 |
+
batch_idx = batch.view(-1, 1).expand(-1, end - start).long()
|
| 119 |
+
sums = feat.new_zeros((B, end - start)).scatter_add(0, batch_idx, sum_feat)
|
| 120 |
+
sums_sq = feat.new_zeros((B, end - start)).scatter_add(
|
| 121 |
+
0, batch_idx, sum_feat_sq
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
counts = torch.bincount(batch, minlength=B).float().view(B, 1) * CP
|
| 125 |
+
mean = sums.div_(counts + self.eps)
|
| 126 |
+
var = sums_sq.div_(counts + self.eps).sub_(mean.square())
|
| 127 |
+
inv_std = torch.rsqrt(var + self.eps)
|
| 128 |
+
|
| 129 |
+
mean_exp = mean.index_select(0, batch).unsqueeze(2)
|
| 130 |
+
inv_std_exp = inv_std.index_select(0, batch).unsqueeze(2)
|
| 131 |
+
feat_norm_all[:, start:end, :] = feat_slice.sub(mean_exp).mul_(inv_std_exp)
|
| 132 |
+
|
| 133 |
+
del feat_slice, sum_feat, sum_feat_sq, sums, sums_sq
|
| 134 |
+
del mean, var, inv_std, mean_exp, inv_std_exp
|
| 135 |
+
|
| 136 |
+
feat_norm_all = feat_norm_all.view(N, C)
|
| 137 |
+
feat_norm_all.mul_(self.weights).add_(self.bias)
|
| 138 |
+
|
| 139 |
+
point.update_feat(feat_norm_all)
|
| 140 |
+
return point
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class LayerNormFP32(nn.LayerNorm):
|
| 144 |
+
"""Layer norm that always computes in float32, then restores the input dtype."""
|
| 145 |
+
|
| 146 |
+
@force_fp32(mode="convert_back")
|
| 147 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 148 |
+
"""Normalize the last dimension of ``x`` in float32."""
|
| 149 |
+
return super().forward(x)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class MLP(nn.Module):
|
| 153 |
+
"""Two-layer feed-forward block."""
|
| 154 |
+
|
| 155 |
+
def __init__(
|
| 156 |
+
self,
|
| 157 |
+
in_channels: int,
|
| 158 |
+
hidden_channels: int | None = None,
|
| 159 |
+
out_channels: int | None = None,
|
| 160 |
+
act_layer: type[nn.Module] = nn.GELU,
|
| 161 |
+
):
|
| 162 |
+
"""
|
| 163 |
+
Args:
|
| 164 |
+
in_channels: Input channels.
|
| 165 |
+
hidden_channels: Width of the hidden layer; defaults to ``in_channels``.
|
| 166 |
+
out_channels: Output channels; defaults to ``in_channels``.
|
| 167 |
+
act_layer: Activation constructed between the two linear layers.
|
| 168 |
+
"""
|
| 169 |
+
super().__init__()
|
| 170 |
+
out_channels = out_channels or in_channels
|
| 171 |
+
hidden_channels = hidden_channels or in_channels
|
| 172 |
+
self.fc1 = nn.Linear(in_channels, hidden_channels)
|
| 173 |
+
self.act = act_layer()
|
| 174 |
+
self.fc2 = nn.Linear(hidden_channels, out_channels)
|
| 175 |
+
|
| 176 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 177 |
+
"""Project ``x`` through the hidden layer and back."""
|
| 178 |
+
return self.fc2(self.act(self.fc1(x)))
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
class SparseClsHead(PointModule):
|
| 182 |
+
"""Per-voxel classification head, used to predict octree occupancy on upsampling."""
|
| 183 |
+
|
| 184 |
+
def __init__(self, in_channels: int, out_channels: int):
|
| 185 |
+
"""
|
| 186 |
+
Args:
|
| 187 |
+
in_channels: Input feature channels.
|
| 188 |
+
out_channels: Logits emitted per voxel; eight for octree occupancy.
|
| 189 |
+
"""
|
| 190 |
+
super().__init__()
|
| 191 |
+
self.fc = PointSequential(
|
| 192 |
+
PointGroupNorm(in_channels),
|
| 193 |
+
Swish(),
|
| 194 |
+
sparse_backend.Conv3d(
|
| 195 |
+
in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=True
|
| 196 |
+
),
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
@force_fp32(mode="convert")
|
| 200 |
+
def forward(self, point: Point) -> Point:
|
| 201 |
+
"""Return a point whose ``feat`` holds the per-voxel occupancy logits."""
|
| 202 |
+
# Wrap only the tensors the head reads; cloning the whole point here would
|
| 203 |
+
# keep every intermediate of the calling stage alive.
|
| 204 |
+
head_point = Point(
|
| 205 |
+
dict(
|
| 206 |
+
feat=point.feat,
|
| 207 |
+
sparse_conv_feat=point.sparse_conv_feat,
|
| 208 |
+
batch=point.batch,
|
| 209 |
+
)
|
| 210 |
+
)
|
| 211 |
+
head_point.is_sparsified = True
|
| 212 |
+
return self.fc(head_point)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
class ConvOut(PointModule):
|
| 216 |
+
"""Output projection that splits off the eight per-corner SDF sign logits."""
|
| 217 |
+
|
| 218 |
+
def __init__(self, in_channels: int, out_channels: int):
|
| 219 |
+
"""
|
| 220 |
+
Args:
|
| 221 |
+
in_channels: Input feature channels.
|
| 222 |
+
out_channels: Output channels, the first eight being the SDF sign logits.
|
| 223 |
+
"""
|
| 224 |
+
super().__init__()
|
| 225 |
+
self.conv = PointSequential(
|
| 226 |
+
PointGroupNorm(in_channels),
|
| 227 |
+
Swish(),
|
| 228 |
+
sparse_backend.Conv3d(
|
| 229 |
+
in_channels,
|
| 230 |
+
out_channels,
|
| 231 |
+
kernel_size=3,
|
| 232 |
+
stride=1,
|
| 233 |
+
padding=1,
|
| 234 |
+
bias=True,
|
| 235 |
+
),
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
@force_fp32(mode="convert")
|
| 239 |
+
def forward(self, point: Point) -> Point:
|
| 240 |
+
"""Split the projection into ``point["sdf_sign"]`` and the remaining features."""
|
| 241 |
+
point = self.conv(point)
|
| 242 |
+
point["sdf_sign"] = point.feat[:, :8]
|
| 243 |
+
point.update_feat(point.feat[:, 8:])
|
| 244 |
+
return point
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
class SparseZToF(PointModule):
|
| 248 |
+
"""Project latent channels back to decoder features."""
|
| 249 |
+
|
| 250 |
+
def __init__(self, in_channels: int, out_channels: int):
|
| 251 |
+
"""
|
| 252 |
+
Args:
|
| 253 |
+
in_channels: Latent channels arriving from the transformer.
|
| 254 |
+
out_channels: Decoder feature channels at the coarsest stage.
|
| 255 |
+
"""
|
| 256 |
+
super().__init__()
|
| 257 |
+
self.conv = PointSequential(
|
| 258 |
+
sparse_backend.Conv3d(
|
| 259 |
+
in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=True
|
| 260 |
+
)
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
@force_fp32(mode="enforce")
|
| 264 |
+
def forward(self, point: Point) -> Point:
|
| 265 |
+
"""Project latent channels up to the decoder's feature width."""
|
| 266 |
+
return self.conv(point)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
class SparseResnetBlock(PointModule):
|
| 270 |
+
"""Pre-norm residual block of two sparse 3x3x3 convolutions."""
|
| 271 |
+
|
| 272 |
+
def __init__(
|
| 273 |
+
self,
|
| 274 |
+
in_channels: int,
|
| 275 |
+
out_channels: int,
|
| 276 |
+
skip_points: int = _RESNET_SKIP_POINTS,
|
| 277 |
+
):
|
| 278 |
+
"""
|
| 279 |
+
Args:
|
| 280 |
+
in_channels: Input channels.
|
| 281 |
+
out_channels: Output channels.
|
| 282 |
+
skip_points: Token count above which the block passes its input through
|
| 283 |
+
unchanged. Defaults to the product's own guard.
|
| 284 |
+
"""
|
| 285 |
+
super().__init__()
|
| 286 |
+
self.skip_points = skip_points
|
| 287 |
+
self.block = PointSequential(
|
| 288 |
+
PointGroupNorm(in_channels),
|
| 289 |
+
Swish(),
|
| 290 |
+
sparse_backend.Conv3d(
|
| 291 |
+
in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=True
|
| 292 |
+
),
|
| 293 |
+
PointGroupNorm(out_channels),
|
| 294 |
+
Swish(),
|
| 295 |
+
sparse_backend.Conv3d(
|
| 296 |
+
out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=True
|
| 297 |
+
),
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
# A PointSequential shortcut would alias and then overwrite point.feat.
|
| 301 |
+
if in_channels != out_channels:
|
| 302 |
+
self.res_conv = nn.Linear(in_channels, out_channels)
|
| 303 |
+
else:
|
| 304 |
+
self.res_conv = None
|
| 305 |
+
|
| 306 |
+
def forward(self, point: Point) -> Point:
|
| 307 |
+
"""Add the residual branch to ``point``, or pass it through when oversized."""
|
| 308 |
+
if point.feat.shape[0] > self.skip_points:
|
| 309 |
+
warnings.warn(
|
| 310 |
+
f"SparseResnetBlock skipped: {point.feat.shape[0]} points exceeds the "
|
| 311 |
+
f"skip_points budget of {self.skip_points}. The block is a no-op, so "
|
| 312 |
+
"the output is the input; raise skip_points to run it.",
|
| 313 |
+
stacklevel=2,
|
| 314 |
+
)
|
| 315 |
+
return point
|
| 316 |
+
if self.res_conv is not None:
|
| 317 |
+
shortcut = self.res_conv(point.feat)
|
| 318 |
+
else:
|
| 319 |
+
shortcut = point.feat
|
| 320 |
+
point.update_feat(shortcut + self.block(point).feat)
|
| 321 |
+
return point
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
class SerializedAttention(PointModule):
|
| 325 |
+
"""Windowed self-attention over patches of a space-filling-curve ordering."""
|
| 326 |
+
|
| 327 |
+
def __init__(
|
| 328 |
+
self,
|
| 329 |
+
channels: int,
|
| 330 |
+
num_heads: int,
|
| 331 |
+
patch_size: int,
|
| 332 |
+
order_index: int = 0,
|
| 333 |
+
):
|
| 334 |
+
"""
|
| 335 |
+
Args:
|
| 336 |
+
channels: Feature channels; must be divisible by ``num_heads``.
|
| 337 |
+
num_heads: Attention heads.
|
| 338 |
+
patch_size: Points per attention window.
|
| 339 |
+
order_index: Which of the point's serialized orders to attend along.
|
| 340 |
+
|
| 341 |
+
Raises:
|
| 342 |
+
ValueError: ``channels`` is not divisible by ``num_heads``.
|
| 343 |
+
ImportError: flash-attn is not installed.
|
| 344 |
+
"""
|
| 345 |
+
super().__init__()
|
| 346 |
+
if channels % num_heads != 0:
|
| 347 |
+
raise ValueError(
|
| 348 |
+
f"channels {channels} must be divisible by num_heads {num_heads}"
|
| 349 |
+
)
|
| 350 |
+
self._flash_attn = _flash_attn()
|
| 351 |
+
self.channels = channels
|
| 352 |
+
self.num_heads = num_heads
|
| 353 |
+
self.scale = (channels // num_heads)**-0.5
|
| 354 |
+
self.order_index = order_index
|
| 355 |
+
self.patch_size = patch_size
|
| 356 |
+
|
| 357 |
+
self.qkv = torch.nn.Linear(channels, channels * 3, bias=True)
|
| 358 |
+
self.proj = torch.nn.Linear(channels, channels)
|
| 359 |
+
|
| 360 |
+
@torch.no_grad()
|
| 361 |
+
def get_padding_and_inverse(self, point: Point):
|
| 362 |
+
"""Return, and cache on ``point``, the patch padding maps and sequence bounds.
|
| 363 |
+
|
| 364 |
+
Each batch element is padded up to a whole number of patches by repeating its
|
| 365 |
+
last full patch, so attention never needs a mask.
|
| 366 |
+
|
| 367 |
+
Returns:
|
| 368 |
+
``(pad, unpad, cu_seqlens)``: the padded gather index, its inverse, and
|
| 369 |
+
the cumulative patch boundaries flash attention expects.
|
| 370 |
+
"""
|
| 371 |
+
pad_key = "pad"
|
| 372 |
+
unpad_key = "unpad"
|
| 373 |
+
cu_seqlens_key = "cu_seqlens_key"
|
| 374 |
+
if (
|
| 375 |
+
pad_key not in point.keys()
|
| 376 |
+
or unpad_key not in point.keys()
|
| 377 |
+
or cu_seqlens_key not in point.keys()
|
| 378 |
+
):
|
| 379 |
+
offset = point.offset
|
| 380 |
+
bincount = offset2bincount(offset)
|
| 381 |
+
bincount_pad = (
|
| 382 |
+
torch.div(
|
| 383 |
+
bincount + self.patch_size - 1,
|
| 384 |
+
self.patch_size,
|
| 385 |
+
rounding_mode="trunc",
|
| 386 |
+
)
|
| 387 |
+
* self.patch_size
|
| 388 |
+
)
|
| 389 |
+
# Only pad an element when it has more points than one patch.
|
| 390 |
+
mask_pad = bincount > self.patch_size
|
| 391 |
+
bincount_pad = ~mask_pad * bincount + mask_pad * bincount_pad
|
| 392 |
+
_offset = nn.functional.pad(offset, (1, 0))
|
| 393 |
+
_offset_pad = nn.functional.pad(torch.cumsum(bincount_pad, dim=0), (1, 0))
|
| 394 |
+
pad = torch.arange(_offset_pad[-1], device=offset.device)
|
| 395 |
+
unpad = torch.arange(_offset[-1], device=offset.device)
|
| 396 |
+
cu_seqlens = []
|
| 397 |
+
for i in range(len(offset)):
|
| 398 |
+
# yapf: disable
|
| 399 |
+
unpad[_offset[i] : _offset[i + 1]] += _offset_pad[i] - _offset[i]
|
| 400 |
+
if bincount[i] != bincount_pad[i]:
|
| 401 |
+
pad[_offset_pad[i + 1] - self.patch_size + (bincount[i] % self.patch_size) : _offset_pad[i + 1]] = \
|
| 402 |
+
pad[_offset_pad[i + 1] - 2 * self.patch_size + (bincount[i] % self.patch_size) : _offset_pad[i + 1] - self.patch_size]
|
| 403 |
+
pad[_offset_pad[i] : _offset_pad[i + 1]] -= _offset_pad[i] - _offset[i]
|
| 404 |
+
# yapf: enable
|
| 405 |
+
cu_seqlens.append(
|
| 406 |
+
torch.arange(
|
| 407 |
+
_offset_pad[i],
|
| 408 |
+
_offset_pad[i + 1],
|
| 409 |
+
step=self.patch_size,
|
| 410 |
+
dtype=torch.int32,
|
| 411 |
+
device=offset.device,
|
| 412 |
+
)
|
| 413 |
+
)
|
| 414 |
+
point[pad_key] = pad
|
| 415 |
+
point[unpad_key] = unpad
|
| 416 |
+
point[cu_seqlens_key] = nn.functional.pad(
|
| 417 |
+
torch.concat(cu_seqlens), (0, 1), value=_offset_pad[-1]
|
| 418 |
+
)
|
| 419 |
+
return point[pad_key], point[unpad_key], point[cu_seqlens_key]
|
| 420 |
+
|
| 421 |
+
def forward(self, point: Point) -> Point:
|
| 422 |
+
"""Attend within each patch of the serialized ordering and project the result."""
|
| 423 |
+
H = self.num_heads
|
| 424 |
+
C = self.channels
|
| 425 |
+
|
| 426 |
+
pad, unpad, cu_seqlens = self.get_padding_and_inverse(point)
|
| 427 |
+
|
| 428 |
+
order = point.serialized_order[self.order_index][pad]
|
| 429 |
+
inverse = unpad[point.serialized_inverse[self.order_index]]
|
| 430 |
+
|
| 431 |
+
qkv = self.qkv(point.feat)[order]
|
| 432 |
+
|
| 433 |
+
feat = self._flash_attn.flash_attn_varlen_qkvpacked_func(
|
| 434 |
+
qkv.half().reshape(-1, 3, H, C // H),
|
| 435 |
+
cu_seqlens,
|
| 436 |
+
max_seqlen=self.patch_size,
|
| 437 |
+
dropout_p=0,
|
| 438 |
+
softmax_scale=self.scale,
|
| 439 |
+
).reshape(-1, C)
|
| 440 |
+
feat = feat.to(qkv.dtype)[inverse]
|
| 441 |
+
|
| 442 |
+
point.feat = self.proj(feat)
|
| 443 |
+
return point
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
class SparseAttentionBlock(PointModule):
|
| 447 |
+
"""Serialized attention block with a sparse conditional position encoding and MLP."""
|
| 448 |
+
|
| 449 |
+
def __init__(
|
| 450 |
+
self,
|
| 451 |
+
channels: int,
|
| 452 |
+
num_heads: int,
|
| 453 |
+
patch_size: int = 48,
|
| 454 |
+
mlp_ratio: float = 4.0,
|
| 455 |
+
order_index: int = 0,
|
| 456 |
+
):
|
| 457 |
+
"""
|
| 458 |
+
Args:
|
| 459 |
+
channels: Feature channels, preserved across the block.
|
| 460 |
+
num_heads: Attention heads.
|
| 461 |
+
patch_size: Points per attention window.
|
| 462 |
+
mlp_ratio: Hidden width of the MLP as a multiple of ``channels``.
|
| 463 |
+
order_index: Which of the point's serialized orders to attend along.
|
| 464 |
+
"""
|
| 465 |
+
super().__init__()
|
| 466 |
+
self.channels = channels
|
| 467 |
+
|
| 468 |
+
norm_layer = LayerNormFP32
|
| 469 |
+
act_layer = nn.GELU
|
| 470 |
+
|
| 471 |
+
self.cpe = PointSequential(
|
| 472 |
+
sparse_backend.Conv3d(channels, channels, kernel_size=3, bias=True),
|
| 473 |
+
nn.Linear(channels, channels),
|
| 474 |
+
norm_layer(channels),
|
| 475 |
+
)
|
| 476 |
+
|
| 477 |
+
self.norm1 = PointSequential(norm_layer(channels))
|
| 478 |
+
self.attn = SerializedAttention(
|
| 479 |
+
channels=channels,
|
| 480 |
+
patch_size=patch_size,
|
| 481 |
+
num_heads=num_heads,
|
| 482 |
+
order_index=order_index,
|
| 483 |
+
)
|
| 484 |
+
self.norm2 = PointSequential(norm_layer(channels))
|
| 485 |
+
self.mlp = PointSequential(
|
| 486 |
+
MLP(
|
| 487 |
+
in_channels=channels,
|
| 488 |
+
hidden_channels=int(channels * mlp_ratio),
|
| 489 |
+
out_channels=channels,
|
| 490 |
+
act_layer=act_layer,
|
| 491 |
+
)
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
def forward(self, point: Point) -> Point:
|
| 495 |
+
"""Apply the position encoding, attention and MLP residual branches in turn."""
|
| 496 |
+
shortcut = point.feat
|
| 497 |
+
point = self.cpe(point)
|
| 498 |
+
point.feat = shortcut + point.feat
|
| 499 |
+
shortcut = point.feat
|
| 500 |
+
point = self.norm1(point)
|
| 501 |
+
point = self.attn(point)
|
| 502 |
+
point.feat = shortcut + point.feat
|
| 503 |
+
|
| 504 |
+
shortcut = point.feat
|
| 505 |
+
point = self.norm2(point)
|
| 506 |
+
point = self.mlp(point)
|
| 507 |
+
point.feat = shortcut + point.feat
|
| 508 |
+
# Everything above touched only feat; resync the sparse view once.
|
| 509 |
+
point.sparse_conv_feat = sparse_backend.replace_feats(
|
| 510 |
+
point.sparse_conv_feat, point.feat
|
| 511 |
+
)
|
| 512 |
+
return point
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
class SerializedUnpooling(PointModule):
|
| 516 |
+
"""Double the grid resolution, keeping the children the occupancy head predicts."""
|
| 517 |
+
|
| 518 |
+
def __init__(self, channels: int):
|
| 519 |
+
"""
|
| 520 |
+
Args:
|
| 521 |
+
channels: Feature channels, preserved across the resolution doubling.
|
| 522 |
+
"""
|
| 523 |
+
super().__init__()
|
| 524 |
+
self.cls_head = SparseClsHead(channels, 8)
|
| 525 |
+
self.proj = nn.Linear(channels, channels * 8, bias=False)
|
| 526 |
+
self.inv_grid_coord = nn.Parameter(
|
| 527 |
+
torch.tensor(
|
| 528 |
+
[
|
| 529 |
+
[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1],
|
| 530 |
+
[1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1],
|
| 531 |
+
]
|
| 532 |
+
),
|
| 533 |
+
requires_grad=False,
|
| 534 |
+
)
|
| 535 |
+
|
| 536 |
+
def forward(self, point: Point) -> Point:
|
| 537 |
+
"""Split each voxel into eight children and keep the ones predicted occupied."""
|
| 538 |
+
feat = self.proj(point.feat)
|
| 539 |
+
pred_octree = self.cls_head(point).feat
|
| 540 |
+
|
| 541 |
+
m, c = feat.shape[0], feat.shape[1] // 8
|
| 542 |
+
mask = pred_octree.flatten() > 0
|
| 543 |
+
|
| 544 |
+
feat = feat.view(m * 8, c)[mask]
|
| 545 |
+
grid_coord = point.grid_coord.unsqueeze(1) * 2 + self.inv_grid_coord.unsqueeze(0)
|
| 546 |
+
grid_coord = grid_coord.view(-1, 3)[mask]
|
| 547 |
+
batch = point.batch.unsqueeze(1).expand(-1, 8).flatten()[mask]
|
| 548 |
+
|
| 549 |
+
point = point.clone(
|
| 550 |
+
feat=feat,
|
| 551 |
+
grid_coord=grid_coord,
|
| 552 |
+
resolution=point.resolution * 2,
|
| 553 |
+
batch=batch,
|
| 554 |
+
offset=batch2offset(batch),
|
| 555 |
+
)
|
| 556 |
+
point.sparsify()
|
| 557 |
+
return point
|
| 558 |
+
|
| 559 |
+
|
| 560 |
+
class Upsample(PointModule):
|
| 561 |
+
"""Stage-boundary upsampling by serialized unpooling."""
|
| 562 |
+
|
| 563 |
+
def __init__(self, channels: int):
|
| 564 |
+
"""
|
| 565 |
+
Args:
|
| 566 |
+
channels: Feature channels, preserved across the upsampling.
|
| 567 |
+
"""
|
| 568 |
+
super().__init__()
|
| 569 |
+
self.upsample = SerializedUnpooling(channels)
|
| 570 |
+
|
| 571 |
+
def forward(self, point: Point) -> Point:
|
| 572 |
+
"""Double the grid resolution of ``point``."""
|
| 573 |
+
# The point count grows up to eightfold here; release the freed stage first.
|
| 574 |
+
torch.cuda.empty_cache()
|
| 575 |
+
return self.upsample(point)
|
sparc3d/models/autoencoders/config.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicit JSON-to-module construction for the sparse mesh autoencoder."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import inspect
|
| 6 |
+
import json
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Mapping
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
from sparc3d.models.autoencoders.sparconv_vae import SparconvVAE
|
| 13 |
+
from sparc3d.models.autoencoders.ptv3 import PTv3MeshDecoder
|
| 14 |
+
from sparc3d.structures.point import Point
|
| 15 |
+
|
| 16 |
+
_SUPPORTED_TYPES: dict[str, type] = {
|
| 17 |
+
"SparconvVAE": SparconvVAE,
|
| 18 |
+
"PTv3MeshDecoder": PTv3MeshDecoder,
|
| 19 |
+
}
|
| 20 |
+
"""The only ``type`` discriminators a runtime config may name."""
|
| 21 |
+
|
| 22 |
+
_SUB_CONFIG_TYPES: dict[str, str] = {"decoder_cfg": "PTv3MeshDecoder"}
|
| 23 |
+
"""The ``type`` each ``SparconvVAE`` sub-config must name."""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def load_config(config: str | Path | Mapping[str, Any]) -> dict[str, Any]:
|
| 27 |
+
"""Read a runtime config and return its model block.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
config: Path to a JSON file, or an already-parsed mapping. Either the whole
|
| 31 |
+
document (with a ``model`` key) or the model block itself is accepted.
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
The model block as a plain dict.
|
| 35 |
+
|
| 36 |
+
Raises:
|
| 37 |
+
ValueError: The document has no model block carrying a ``type``.
|
| 38 |
+
"""
|
| 39 |
+
if isinstance(config, (str, Path)):
|
| 40 |
+
with Path(config).open("r", encoding="utf-8") as f:
|
| 41 |
+
document = json.load(f)
|
| 42 |
+
else:
|
| 43 |
+
document = dict(config)
|
| 44 |
+
|
| 45 |
+
model = document.get("model", document)
|
| 46 |
+
if not isinstance(model, Mapping) or "type" not in model:
|
| 47 |
+
raise ValueError("config has no 'model' block with a 'type' discriminator")
|
| 48 |
+
return dict(model)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def build_sparconv_vae(config: str | Path | Mapping[str, Any]) -> SparconvVAE:
|
| 52 |
+
"""Instantiate a :class:`SparconvVAE` from a runtime config, without loading weights.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
config: Runtime config, as accepted by :func:`load_config`.
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
Model on the CPU in eval mode.
|
| 59 |
+
|
| 60 |
+
Raises:
|
| 61 |
+
ValueError: An unsupported ``type``, a missing sub-config, or an unknown
|
| 62 |
+
constructor key.
|
| 63 |
+
"""
|
| 64 |
+
model_cfg = load_config(config)
|
| 65 |
+
_resolve_type(model_cfg, "model", "SparconvVAE")
|
| 66 |
+
# Training sections (losses, optimizer, scheduler) are rejected rather than
|
| 67 |
+
# ignored, so a training config can never be mistaken for a runtime artifact.
|
| 68 |
+
extra = sorted(set(model_cfg) - set(_SUB_CONFIG_TYPES))
|
| 69 |
+
if extra:
|
| 70 |
+
raise ValueError(f"model config has non-architecture keys: {extra}")
|
| 71 |
+
|
| 72 |
+
expected_attributes = sum(
|
| 73 |
+
Point.ALL_LAYOUTS[key]["size"] for key in ("sdf", "deform")
|
| 74 |
+
)
|
| 75 |
+
if "decoder_cfg" not in model_cfg:
|
| 76 |
+
raise ValueError("model config is missing 'decoder_cfg'")
|
| 77 |
+
sub_cfg = dict(model_cfg["decoder_cfg"])
|
| 78 |
+
cls = _resolve_type(sub_cfg, "decoder_cfg", "PTv3MeshDecoder")
|
| 79 |
+
kwargs = _validated_kwargs(cls, sub_cfg, "decoder_cfg")
|
| 80 |
+
output_channels = kwargs.get(
|
| 81 |
+
"out_channels", inspect.signature(cls).parameters["out_channels"].default
|
| 82 |
+
)
|
| 83 |
+
if output_channels != expected_attributes:
|
| 84 |
+
raise ValueError(
|
| 85 |
+
f"decoder out_channels must be {expected_attributes}, "
|
| 86 |
+
f"got {output_channels}"
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
model = SparconvVAE(decoder=cls(**kwargs))
|
| 90 |
+
model.eval()
|
| 91 |
+
return model
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def load_sparconv_vae(
|
| 95 |
+
weights: str | Path,
|
| 96 |
+
config: str | Path | Mapping[str, Any] | None = None,
|
| 97 |
+
*,
|
| 98 |
+
device: str | torch.device = "cuda",
|
| 99 |
+
) -> SparconvVAE:
|
| 100 |
+
"""Build a :class:`SparconvVAE` and load its weights, requiring an exact key match.
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
weights: Path to a ``.safetensors`` file, or to a directory holding
|
| 104 |
+
``model.safetensors`` and ``config.json``.
|
| 105 |
+
config: Runtime config; defaults to ``config.json`` beside the weights.
|
| 106 |
+
device: Device to move the model to.
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
Model in eval mode on ``device``.
|
| 110 |
+
|
| 111 |
+
Raises:
|
| 112 |
+
FileNotFoundError: The weights, or the implied ``config.json``, are missing.
|
| 113 |
+
RuntimeError: The checkpoint keys do not match the model's parameters exactly.
|
| 114 |
+
"""
|
| 115 |
+
from safetensors.torch import load_file
|
| 116 |
+
|
| 117 |
+
weights_path = Path(weights)
|
| 118 |
+
if weights_path.is_dir():
|
| 119 |
+
weights_path = weights_path / "model.safetensors"
|
| 120 |
+
if not weights_path.is_file():
|
| 121 |
+
raise FileNotFoundError(f"no weights at {weights_path}")
|
| 122 |
+
|
| 123 |
+
if config is None:
|
| 124 |
+
config = weights_path.parent / "config.json"
|
| 125 |
+
if not Path(config).is_file():
|
| 126 |
+
raise FileNotFoundError(
|
| 127 |
+
f"no config given and no config.json beside {weights_path}"
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
model = build_sparconv_vae(config)
|
| 131 |
+
model.load_state_dict(load_file(str(weights_path)), strict=True)
|
| 132 |
+
return model.to(device).eval()
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def _resolve_type(cfg: dict[str, Any], where: str, expected: str) -> type:
|
| 136 |
+
"""Pop the ``type`` discriminator and resolve it to its class."""
|
| 137 |
+
type_name = cfg.pop("type", None)
|
| 138 |
+
if type_name != expected:
|
| 139 |
+
raise ValueError(
|
| 140 |
+
f"{where}: unsupported type {type_name!r}; expected {expected!r}"
|
| 141 |
+
)
|
| 142 |
+
return _SUPPORTED_TYPES[type_name]
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _validated_kwargs(cls: type, cfg: dict[str, Any], where: str) -> dict[str, Any]:
|
| 146 |
+
"""Reject config keys that are not constructor parameters of ``cls``."""
|
| 147 |
+
accepted = set(inspect.signature(cls).parameters)
|
| 148 |
+
unknown = sorted(set(cfg) - accepted)
|
| 149 |
+
if unknown:
|
| 150 |
+
raise ValueError(f"{where}: unknown keys for {cls.__name__}: {unknown}")
|
| 151 |
+
return cfg
|
sparc3d/models/autoencoders/ptv3.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sparse PTv3 mesh decoder."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Pointcept / Point Transformer V3 under the MIT License.
|
| 4 |
+
# Modified for SPARC3D: the decoder half only, reading a latent grid instead of
|
| 5 |
+
# an encoded point cloud.
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from typing import Sequence
|
| 10 |
+
|
| 11 |
+
from sparc3d.models.autoencoders.blocks import (
|
| 12 |
+
ConvOut,
|
| 13 |
+
SparseAttentionBlock,
|
| 14 |
+
SparseResnetBlock,
|
| 15 |
+
SparseZToF,
|
| 16 |
+
Upsample,
|
| 17 |
+
)
|
| 18 |
+
from sparc3d.structures.point import Point
|
| 19 |
+
from sparc3d.structures.point_modules import PointModule, PointSequential
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class PTv3MeshDecoder(PointModule):
|
| 23 |
+
"""Decode a coarse latent point cloud back into per-corner SDF and deformation.
|
| 24 |
+
|
| 25 |
+
A serialized attention stack mixes globally at the coarsest level, then residual
|
| 26 |
+
stages double the resolution and prune children by predicted occupancy.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(
|
| 30 |
+
self,
|
| 31 |
+
out_channels: int = 32,
|
| 32 |
+
order: str | Sequence[str] = ("z", "z-trans"),
|
| 33 |
+
depths: Sequence[int] = (2, 2, 2, 2),
|
| 34 |
+
channels: Sequence[int] = (128, 256, 512, 512),
|
| 35 |
+
mid_depth: int = 2,
|
| 36 |
+
mid_num_head: int = 32,
|
| 37 |
+
z_channels: int = 32,
|
| 38 |
+
patch_size: int = 48,
|
| 39 |
+
mlp_ratio: float = 4,
|
| 40 |
+
):
|
| 41 |
+
"""
|
| 42 |
+
Args:
|
| 43 |
+
out_channels: Attribute channels emitted, on top of the eight sign logits.
|
| 44 |
+
order: Space-filling curve orders; attention blocks cycle through them.
|
| 45 |
+
depths: Residual blocks per stage.
|
| 46 |
+
channels: Feature width per stage.
|
| 47 |
+
mid_depth: Attention blocks at the coarsest level.
|
| 48 |
+
mid_num_head: Attention heads at the coarsest level.
|
| 49 |
+
z_channels: Latent channels consumed.
|
| 50 |
+
patch_size: Points per attention window.
|
| 51 |
+
mlp_ratio: Attention-block MLP expansion ratio.
|
| 52 |
+
|
| 53 |
+
Raises:
|
| 54 |
+
ValueError: ``depths`` and ``channels`` describe different stage counts.
|
| 55 |
+
"""
|
| 56 |
+
super().__init__()
|
| 57 |
+
|
| 58 |
+
self.num_stages = len(depths)
|
| 59 |
+
self.order = [order] if isinstance(order, str) else list(order)
|
| 60 |
+
|
| 61 |
+
if len(channels) != self.num_stages:
|
| 62 |
+
raise ValueError(
|
| 63 |
+
f"channels has {len(channels)} entries but depths describes "
|
| 64 |
+
f"{self.num_stages} stages"
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
mid_ch = channels[-1]
|
| 68 |
+
self.z_to_f = SparseZToF(z_channels, mid_ch)
|
| 69 |
+
|
| 70 |
+
self.mid = PointSequential()
|
| 71 |
+
self.mid.add(SparseResnetBlock(mid_ch, mid_ch), name="block_in")
|
| 72 |
+
for i in range(mid_depth):
|
| 73 |
+
self.mid.add(
|
| 74 |
+
SparseAttentionBlock(
|
| 75 |
+
channels=mid_ch,
|
| 76 |
+
num_heads=mid_num_head,
|
| 77 |
+
patch_size=patch_size,
|
| 78 |
+
mlp_ratio=mlp_ratio,
|
| 79 |
+
order_index=i % len(self.order),
|
| 80 |
+
),
|
| 81 |
+
name=f"block{i}",
|
| 82 |
+
)
|
| 83 |
+
self.mid.add(SparseResnetBlock(mid_ch, mid_ch), name="block_out")
|
| 84 |
+
|
| 85 |
+
self.dec = PointSequential()
|
| 86 |
+
for s in reversed(range(self.num_stages)):
|
| 87 |
+
dec = PointSequential()
|
| 88 |
+
block_in = channels[s]
|
| 89 |
+
block_out = channels[s - 1] if s > 0 else channels[0]
|
| 90 |
+
for i in range(depths[s]):
|
| 91 |
+
dec.add(SparseResnetBlock(block_in, block_out), name=f"block{i}")
|
| 92 |
+
block_in = block_out
|
| 93 |
+
if s != 0:
|
| 94 |
+
dec.add(Upsample(block_out), name="up")
|
| 95 |
+
self.dec.add(module=dec, name=f"dec{s}")
|
| 96 |
+
|
| 97 |
+
# Eight extra output channels carry the per-corner SDF sign logits.
|
| 98 |
+
self.conv_out = ConvOut(block_out, 8 + out_channels)
|
| 99 |
+
|
| 100 |
+
def forward(self, point: Point) -> Point:
|
| 101 |
+
"""Decode latents into per-corner SDF signs, distances and deformations.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
point: Sparse point carrying latent ``feat`` on its grid. Sparsified and
|
| 105 |
+
serialized in place if the caller has not done so already.
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
The same point, with ``sdf_sign`` set and ``feat`` holding the remaining
|
| 109 |
+
per-corner attributes at the final resolution.
|
| 110 |
+
"""
|
| 111 |
+
if not point.is_sparsified:
|
| 112 |
+
point.sparsify()
|
| 113 |
+
point = self.z_to_f(point)
|
| 114 |
+
if not point.is_serialized:
|
| 115 |
+
point.serialization(order=self.order)
|
| 116 |
+
point = self.mid(point)
|
| 117 |
+
point = self.dec(point)
|
| 118 |
+
return self.conv_out(point)
|
sparc3d/models/autoencoders/sparconv_vae.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sparse decoder over voxel-corner mesh attributes (``SparconvVAE``)."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Mapping
|
| 6 |
+
|
| 7 |
+
from torch import nn
|
| 8 |
+
|
| 9 |
+
from sparc3d.models.autoencoders.ptv3 import PTv3MeshDecoder
|
| 10 |
+
from sparc3d.structures.point import Point
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class SparconvVAE(nn.Module):
|
| 14 |
+
"""Sparse latent decoder over per-corner mesh attributes.
|
| 15 |
+
|
| 16 |
+
Named for the autoencoder the weights were trained as, but this is the decoder
|
| 17 |
+
alone: latents arrive from the diffusion transformer, so no encoder or Gaussian
|
| 18 |
+
bottleneck is reachable at inference and neither is included here.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(self, decoder: PTv3MeshDecoder):
|
| 22 |
+
"""
|
| 23 |
+
Args:
|
| 24 |
+
decoder: Latent-to-attribute decoder.
|
| 25 |
+
"""
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.decoder = decoder
|
| 28 |
+
|
| 29 |
+
def decode(self, z_q: Point | Mapping) -> Point:
|
| 30 |
+
"""Decode a latent point cloud into activated per-corner attributes.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
z_q: Latent point cloud, or a diffusion output mapping with ``grid_coord``, ``feat``, ``resolution``, and a
|
| 34 |
+
batch descriptor.
|
| 35 |
+
|
| 36 |
+
Returns:
|
| 37 |
+
Decoded point cloud whose ``feat`` holds activated attributes and whose
|
| 38 |
+
``sdf_sign`` holds the per-corner sign logits.
|
| 39 |
+
"""
|
| 40 |
+
if not isinstance(z_q, Point):
|
| 41 |
+
data = dict(z_q)
|
| 42 |
+
data.setdefault("sdf", None)
|
| 43 |
+
data.setdefault("deform", None)
|
| 44 |
+
z_q = Point(data)
|
| 45 |
+
z_q.initialization()
|
| 46 |
+
elif "layouts" not in z_q or "matrix" not in z_q:
|
| 47 |
+
z_q.initialization()
|
| 48 |
+
|
| 49 |
+
if not {"sdf", "deform"}.issubset(z_q.layouts):
|
| 50 |
+
start = 0
|
| 51 |
+
layouts = {}
|
| 52 |
+
for key in ("sdf", "deform"):
|
| 53 |
+
entry = Point.ALL_LAYOUTS[key]
|
| 54 |
+
layouts[key] = {
|
| 55 |
+
**entry,
|
| 56 |
+
"range": (start, start + entry["size"]),
|
| 57 |
+
}
|
| 58 |
+
start += entry["size"]
|
| 59 |
+
z_q["layouts"] = layouts
|
| 60 |
+
|
| 61 |
+
x_hat = self.decoder(z_q)
|
| 62 |
+
x_hat.activate()
|
| 63 |
+
return x_hat
|
sparc3d/models/conditioners/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Image conditioning models."""
|
| 2 |
+
|
| 3 |
+
from sparc3d.models.conditioners.image import ImageConditioner
|
| 4 |
+
|
| 5 |
+
__all__ = ["ImageConditioner"]
|
sparc3d/models/conditioners/image.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared DINOv2 and CLIP image conditioner."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
from PIL import Image
|
| 12 |
+
from torch import Tensor, nn
|
| 13 |
+
from transformers import AutoModel, CLIPVisionModel
|
| 14 |
+
|
| 15 |
+
DINO_MODEL_ID = "facebook/dinov2-large"
|
| 16 |
+
"""Moving Hugging Face repository used for DINOv2 conditioning."""
|
| 17 |
+
|
| 18 |
+
CLIP_MODEL_ID = "openai/clip-vit-large-patch14"
|
| 19 |
+
"""Moving Hugging Face repository used for CLIP conditioning."""
|
| 20 |
+
|
| 21 |
+
_DINO_SIZE = 756
|
| 22 |
+
_CLIP_SIZE = 224
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class ImageConditioner(nn.Module):
|
| 26 |
+
"""Frozen fp32 image conditioner combining DINOv2 tokens and CLIP context."""
|
| 27 |
+
|
| 28 |
+
def __init__(
|
| 29 |
+
self,
|
| 30 |
+
*,
|
| 31 |
+
cache_dir: str | Path | None = None,
|
| 32 |
+
local_files_only: bool = False,
|
| 33 |
+
dino_model_id: str | Path = DINO_MODEL_ID,
|
| 34 |
+
clip_model_id: str | Path = CLIP_MODEL_ID,
|
| 35 |
+
) -> None:
|
| 36 |
+
"""Load and freeze the shared Hugging Face vision encoders.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
cache_dir: Optional Hugging Face cache directory. This is the directory
|
| 40 |
+
that holds the ``models--*`` trees, i.e. ``$HF_HOME/hub`` rather
|
| 41 |
+
than ``$HF_HOME``.
|
| 42 |
+
local_files_only: Whether encoder loading must use cached files only.
|
| 43 |
+
dino_model_id: DINOv2 repository ID, or a local directory holding the
|
| 44 |
+
encoder. A directory needs no Hub cache layout, which is the
|
| 45 |
+
simplest way to run without network access.
|
| 46 |
+
clip_model_id: CLIP repository ID, or a local directory.
|
| 47 |
+
"""
|
| 48 |
+
super().__init__()
|
| 49 |
+
load_kwargs: dict[str, Any] = {}
|
| 50 |
+
if cache_dir is not None:
|
| 51 |
+
load_kwargs["cache_dir"] = str(cache_dir)
|
| 52 |
+
if local_files_only:
|
| 53 |
+
load_kwargs["local_files_only"] = True
|
| 54 |
+
self.dino = AutoModel.from_pretrained(str(dino_model_id), **load_kwargs).float()
|
| 55 |
+
self.clip = CLIPVisionModel.from_pretrained(
|
| 56 |
+
str(clip_model_id), **load_kwargs
|
| 57 |
+
).float()
|
| 58 |
+
|
| 59 |
+
for parameter in self.parameters():
|
| 60 |
+
parameter.requires_grad_(False)
|
| 61 |
+
|
| 62 |
+
self.register_buffer(
|
| 63 |
+
"_dino_mean",
|
| 64 |
+
torch.tensor([0.485, 0.456, 0.406], dtype=torch.float32).view(1, 3, 1, 1),
|
| 65 |
+
persistent=False,
|
| 66 |
+
)
|
| 67 |
+
self.register_buffer(
|
| 68 |
+
"_dino_std",
|
| 69 |
+
torch.tensor([0.229, 0.224, 0.225], dtype=torch.float32).view(1, 3, 1, 1),
|
| 70 |
+
persistent=False,
|
| 71 |
+
)
|
| 72 |
+
self.register_buffer(
|
| 73 |
+
"_clip_mean",
|
| 74 |
+
torch.tensor(
|
| 75 |
+
[0.48145466, 0.4578275, 0.40821073], dtype=torch.float32
|
| 76 |
+
).view(1, 3, 1, 1),
|
| 77 |
+
persistent=False,
|
| 78 |
+
)
|
| 79 |
+
self.register_buffer(
|
| 80 |
+
"_clip_std",
|
| 81 |
+
torch.tensor(
|
| 82 |
+
[0.26862954, 0.26130258, 0.27577711], dtype=torch.float32
|
| 83 |
+
).view(1, 3, 1, 1),
|
| 84 |
+
persistent=False,
|
| 85 |
+
)
|
| 86 |
+
self.eval()
|
| 87 |
+
|
| 88 |
+
def train(self, mode: bool = True) -> "ImageConditioner":
|
| 89 |
+
"""Keep the conditioner and both frozen encoders in evaluation mode."""
|
| 90 |
+
del mode
|
| 91 |
+
return super().train(False)
|
| 92 |
+
|
| 93 |
+
@torch.no_grad()
|
| 94 |
+
def forward(self, image: Image.Image) -> Tensor:
|
| 95 |
+
"""Encode one prepared PIL image into batch-1 conditioning tokens.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
image: Prepared RGB PIL image. Only a single image is accepted.
|
| 99 |
+
|
| 100 |
+
Returns:
|
| 101 |
+
L2-normalized DINOv2 tokens concatenated with a broadcast CLIP
|
| 102 |
+
embedding, shaped ``[1, 2917, 2048]``.
|
| 103 |
+
|
| 104 |
+
Raises:
|
| 105 |
+
TypeError: ``image`` is not a PIL image.
|
| 106 |
+
ValueError: The encoders return an unexpected token or channel shape.
|
| 107 |
+
"""
|
| 108 |
+
if not isinstance(image, Image.Image):
|
| 109 |
+
raise TypeError(
|
| 110 |
+
f"image must be one prepared PIL image, got {type(image).__name__}"
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
device = self._dino_mean.device
|
| 114 |
+
base_input = self._pil_tensor(image, device)
|
| 115 |
+
dino_input = F.interpolate(
|
| 116 |
+
base_input,
|
| 117 |
+
size=(_DINO_SIZE, _DINO_SIZE),
|
| 118 |
+
mode="bilinear",
|
| 119 |
+
align_corners=False,
|
| 120 |
+
)
|
| 121 |
+
clip_input = self._image_tensor(
|
| 122 |
+
image, _CLIP_SIZE, Image.Resampling.LANCZOS, device
|
| 123 |
+
)
|
| 124 |
+
dino_input = (dino_input - self._dino_mean) / self._dino_std
|
| 125 |
+
clip_input = (clip_input - self._clip_mean) / self._clip_std
|
| 126 |
+
|
| 127 |
+
# Disable an enclosing mixed-precision context because conditioner numerics
|
| 128 |
+
# are fixed to fp32 independently of transformer inference precision.
|
| 129 |
+
with torch.autocast(device_type=device.type, enabled=False):
|
| 130 |
+
dino_output: Any = self.dino(pixel_values=dino_input)
|
| 131 |
+
clip_output: Any = self.clip(pixel_values=clip_input)
|
| 132 |
+
|
| 133 |
+
dino_tokens = F.normalize(
|
| 134 |
+
dino_output.last_hidden_state.float(), p=2, dim=-1
|
| 135 |
+
)
|
| 136 |
+
clip_embedding = F.normalize(clip_output.pooler_output.float(), p=2, dim=-1)
|
| 137 |
+
|
| 138 |
+
if dino_tokens.shape != (1, 2917, 1024):
|
| 139 |
+
raise ValueError(
|
| 140 |
+
"DINOv2 output must have shape [1, 2917, 1024], "
|
| 141 |
+
f"got {list(dino_tokens.shape)}"
|
| 142 |
+
)
|
| 143 |
+
if clip_embedding.shape != (1, 1024):
|
| 144 |
+
raise ValueError(
|
| 145 |
+
"CLIP output must have shape [1, 1024], "
|
| 146 |
+
f"got {list(clip_embedding.shape)}"
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
clip_tokens = clip_embedding[:, None, :].expand(
|
| 150 |
+
-1, dino_tokens.shape[1], -1
|
| 151 |
+
)
|
| 152 |
+
return torch.cat((dino_tokens, clip_tokens), dim=-1)
|
| 153 |
+
|
| 154 |
+
@staticmethod
|
| 155 |
+
def _image_tensor(
|
| 156 |
+
image: Image.Image,
|
| 157 |
+
size: int,
|
| 158 |
+
resample: Image.Resampling,
|
| 159 |
+
device: torch.device,
|
| 160 |
+
) -> Tensor:
|
| 161 |
+
rgb = image.convert("RGB").resize((size, size), resample=resample)
|
| 162 |
+
return ImageConditioner._pil_tensor(rgb, device)
|
| 163 |
+
|
| 164 |
+
@staticmethod
|
| 165 |
+
def _pil_tensor(image: Image.Image, device: torch.device) -> Tensor:
|
| 166 |
+
rgb = image.convert("RGB")
|
| 167 |
+
array = np.asarray(rgb, dtype=np.float32)
|
| 168 |
+
tensor = torch.from_numpy(array.copy()).permute(2, 0, 1).unsqueeze(0)
|
| 169 |
+
return tensor.to(device=device, dtype=torch.float32).div_(255.0)
|
sparc3d/models/transformers/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sparse latent transformer and its validated runtime configuration."""
|
| 2 |
+
|
| 3 |
+
from .config import Sparc3DTransformerConfig
|
| 4 |
+
from .sparc3d_transformer import Sparc3DTransformer
|
| 5 |
+
|
| 6 |
+
__all__ = ["Sparc3DTransformer", "Sparc3DTransformerConfig"]
|
sparc3d/models/transformers/config.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Validated configuration for the released sparse latent transformer."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import asdict, dataclass, fields
|
| 6 |
+
from typing import Any, Mapping
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
_LATENT_MEAN = (
|
| 10 |
+
-0.00155994,
|
| 11 |
+
0.03314649,
|
| 12 |
+
-0.00307796,
|
| 13 |
+
0.00630751,
|
| 14 |
+
-0.00835457,
|
| 15 |
+
-0.03860179,
|
| 16 |
+
0.02528263,
|
| 17 |
+
0.03740620,
|
| 18 |
+
-0.06209833,
|
| 19 |
+
-0.00539356,
|
| 20 |
+
-0.02110271,
|
| 21 |
+
0.05126001,
|
| 22 |
+
0.00753371,
|
| 23 |
+
0.02866821,
|
| 24 |
+
0.02772475,
|
| 25 |
+
-0.02131735,
|
| 26 |
+
-0.02799912,
|
| 27 |
+
0.01084617,
|
| 28 |
+
0.03433723,
|
| 29 |
+
-0.00903100,
|
| 30 |
+
-0.00121899,
|
| 31 |
+
-0.01944244,
|
| 32 |
+
0.01641873,
|
| 33 |
+
-0.01063657,
|
| 34 |
+
-0.01990618,
|
| 35 |
+
0.04856062,
|
| 36 |
+
0.01747232,
|
| 37 |
+
-0.02387775,
|
| 38 |
+
0.03216169,
|
| 39 |
+
-0.03732105,
|
| 40 |
+
-0.03730354,
|
| 41 |
+
-0.01948937,
|
| 42 |
+
)
|
| 43 |
+
"""Per-channel mean of the released structured latent."""
|
| 44 |
+
|
| 45 |
+
_LATENT_STD = (
|
| 46 |
+
0.61686146,
|
| 47 |
+
0.80115320,
|
| 48 |
+
0.85489200,
|
| 49 |
+
0.96384716,
|
| 50 |
+
0.77005910,
|
| 51 |
+
0.62859850,
|
| 52 |
+
0.61541015,
|
| 53 |
+
0.74368310,
|
| 54 |
+
0.62762610,
|
| 55 |
+
1.01842280,
|
| 56 |
+
0.77113867,
|
| 57 |
+
0.62627990,
|
| 58 |
+
0.68749195,
|
| 59 |
+
0.62698966,
|
| 60 |
+
0.75491240,
|
| 61 |
+
0.61577400,
|
| 62 |
+
0.63780660,
|
| 63 |
+
0.80406000,
|
| 64 |
+
0.83846325,
|
| 65 |
+
0.69120390,
|
| 66 |
+
0.69227580,
|
| 67 |
+
0.79321617,
|
| 68 |
+
0.63475930,
|
| 69 |
+
0.63848300,
|
| 70 |
+
0.67194235,
|
| 71 |
+
0.73519380,
|
| 72 |
+
0.84781533,
|
| 73 |
+
0.93012910,
|
| 74 |
+
0.62691440,
|
| 75 |
+
0.67906080,
|
| 76 |
+
0.65198770,
|
| 77 |
+
0.77692040,
|
| 78 |
+
)
|
| 79 |
+
"""Per-channel standard deviation of the released structured latent."""
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
@dataclass(frozen=True)
|
| 83 |
+
class Sparc3DTransformerConfig:
|
| 84 |
+
"""Narrow architecture configuration for :class:`Sparc3DTransformer`.
|
| 85 |
+
|
| 86 |
+
Only the dimensions that actually vary between the two released checkpoints are
|
| 87 |
+
configurable. The architecture choices the weights were trained under -- rotary
|
| 88 |
+
position encoding, full flash attention, self-attention QK RMS normalization,
|
| 89 |
+
shared timestep modulation, and I/O skip connections -- are fixed in the model.
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
resolution: int = 64
|
| 93 |
+
"""Structure-grid resolution represented by input coordinates."""
|
| 94 |
+
|
| 95 |
+
in_channels: int = 32
|
| 96 |
+
"""Input latent feature channels."""
|
| 97 |
+
|
| 98 |
+
out_channels: int = 32
|
| 99 |
+
"""Predicted latent feature channels."""
|
| 100 |
+
|
| 101 |
+
model_channels: int = 2112
|
| 102 |
+
"""Transformer hidden width."""
|
| 103 |
+
|
| 104 |
+
cond_channels: int = 2048
|
| 105 |
+
"""Image-conditioning width."""
|
| 106 |
+
|
| 107 |
+
num_blocks: int = 24
|
| 108 |
+
"""Number of shared-modulation transformer blocks."""
|
| 109 |
+
|
| 110 |
+
num_heads: int = 22
|
| 111 |
+
"""Number of attention heads."""
|
| 112 |
+
|
| 113 |
+
mlp_ratio: float = 4.0
|
| 114 |
+
"""Expansion ratio of each feed-forward layer."""
|
| 115 |
+
|
| 116 |
+
patch_size: int = 2
|
| 117 |
+
"""Sparse I/O downsampling factor."""
|
| 118 |
+
|
| 119 |
+
num_io_res_blocks: int = 2
|
| 120 |
+
"""Residual blocks in each I/O stage."""
|
| 121 |
+
|
| 122 |
+
io_block_channels: tuple[int, ...] = (256,)
|
| 123 |
+
"""Channels in each sparse I/O stage."""
|
| 124 |
+
|
| 125 |
+
use_fp16: bool = True
|
| 126 |
+
"""Whether the transformer torso uses float16 parameters."""
|
| 127 |
+
|
| 128 |
+
latent_mean: tuple[float, ...] = _LATENT_MEAN
|
| 129 |
+
"""Per-channel latent mean used by pipeline normalization."""
|
| 130 |
+
|
| 131 |
+
latent_std: tuple[float, ...] = _LATENT_STD
|
| 132 |
+
"""Per-channel latent standard deviation used by pipeline normalization."""
|
| 133 |
+
|
| 134 |
+
def __post_init__(self) -> None:
|
| 135 |
+
positive = {
|
| 136 |
+
"resolution": self.resolution,
|
| 137 |
+
"in_channels": self.in_channels,
|
| 138 |
+
"out_channels": self.out_channels,
|
| 139 |
+
"model_channels": self.model_channels,
|
| 140 |
+
"cond_channels": self.cond_channels,
|
| 141 |
+
"num_blocks": self.num_blocks,
|
| 142 |
+
"num_heads": self.num_heads,
|
| 143 |
+
"patch_size": self.patch_size,
|
| 144 |
+
"num_io_res_blocks": self.num_io_res_blocks,
|
| 145 |
+
}
|
| 146 |
+
invalid = [name for name, value in positive.items() if value <= 0]
|
| 147 |
+
if invalid:
|
| 148 |
+
raise ValueError(f"positive dimensions required for {invalid}")
|
| 149 |
+
if self.model_channels % self.num_heads:
|
| 150 |
+
raise ValueError("model_channels must be divisible by num_heads")
|
| 151 |
+
head_dim = self.model_channels // self.num_heads
|
| 152 |
+
if head_dim % 2 or head_dim < 6:
|
| 153 |
+
raise ValueError("attention head dimension must be even and at least 6")
|
| 154 |
+
if self.patch_size & (self.patch_size - 1):
|
| 155 |
+
raise ValueError("patch_size must be a power of two")
|
| 156 |
+
if self.patch_size != 2 ** len(self.io_block_channels):
|
| 157 |
+
raise ValueError("I/O stage count must equal log2(patch_size)")
|
| 158 |
+
if any(channels <= 1 for channels in self.io_block_channels):
|
| 159 |
+
raise ValueError("I/O stage channels must be greater than one")
|
| 160 |
+
if len(self.latent_mean) != self.in_channels:
|
| 161 |
+
raise ValueError("latent_mean length must match in_channels")
|
| 162 |
+
if len(self.latent_std) != self.in_channels:
|
| 163 |
+
raise ValueError("latent_std length must match in_channels")
|
| 164 |
+
if any(value <= 0 for value in self.latent_std):
|
| 165 |
+
raise ValueError("latent_std values must be positive")
|
| 166 |
+
|
| 167 |
+
@classmethod
|
| 168 |
+
def from_dict(cls, values: Mapping[str, Any]) -> "Sparc3DTransformerConfig":
|
| 169 |
+
"""Build a config from an allowlisted mapping.
|
| 170 |
+
|
| 171 |
+
Args:
|
| 172 |
+
values: Parsed transformer configuration.
|
| 173 |
+
|
| 174 |
+
Returns:
|
| 175 |
+
Validated config.
|
| 176 |
+
|
| 177 |
+
Raises:
|
| 178 |
+
ValueError: Unknown keys are present.
|
| 179 |
+
"""
|
| 180 |
+
values = dict(values)
|
| 181 |
+
allowed = {item.name for item in fields(cls)}
|
| 182 |
+
unknown = sorted(set(values) - allowed)
|
| 183 |
+
if unknown:
|
| 184 |
+
raise ValueError(f"unknown transformer config keys: {unknown}")
|
| 185 |
+
for key in ("io_block_channels", "latent_mean", "latent_std"):
|
| 186 |
+
if key in values:
|
| 187 |
+
values[key] = tuple(values[key])
|
| 188 |
+
return cls(**values)
|
| 189 |
+
|
| 190 |
+
def to_dict(self) -> dict[str, Any]:
|
| 191 |
+
"""Return a JSON-compatible configuration mapping."""
|
| 192 |
+
values = asdict(self)
|
| 193 |
+
for key in ("io_block_channels", "latent_mean", "latent_std"):
|
| 194 |
+
values[key] = list(values[key])
|
| 195 |
+
return values
|
sparc3d/models/transformers/sparc3d_transformer.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sparse latent flow transformer for image-conditioned mesh refinement."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Microsoft TRELLIS under the MIT License.
|
| 4 |
+
# Modified for SPARC3D: the released sparse inference architecture.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import math
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
import torch.nn.functional as F
|
| 13 |
+
from torch import nn
|
| 14 |
+
|
| 15 |
+
from sparc3d.nn import LayerNorm32
|
| 16 |
+
from sparc3d.nn.sparse import (
|
| 17 |
+
SparseConv3d,
|
| 18 |
+
SparseDownsample,
|
| 19 |
+
SparseLinear,
|
| 20 |
+
SparseTensor,
|
| 21 |
+
SparseUpsample,
|
| 22 |
+
)
|
| 23 |
+
from sparc3d.nn.sparse.transformer import ModulatedSparseTransformerCrossBlock
|
| 24 |
+
from sparc3d.nn.utils import convert_module_to_f16, zero_module
|
| 25 |
+
|
| 26 |
+
from .config import Sparc3DTransformerConfig
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class TimestepEmbedder(nn.Module):
|
| 30 |
+
"""Sinusoidal scalar timestep embedding followed by a two-layer MLP."""
|
| 31 |
+
|
| 32 |
+
def __init__(self, hidden_size: int, frequency_embedding_size: int = 256):
|
| 33 |
+
"""
|
| 34 |
+
Args:
|
| 35 |
+
hidden_size: Width of the embedding the MLP produces.
|
| 36 |
+
frequency_embedding_size: Sinusoidal features formed before the MLP.
|
| 37 |
+
"""
|
| 38 |
+
super().__init__()
|
| 39 |
+
self.mlp = nn.Sequential(
|
| 40 |
+
nn.Linear(frequency_embedding_size, hidden_size, bias=True),
|
| 41 |
+
nn.SiLU(),
|
| 42 |
+
nn.Linear(hidden_size, hidden_size, bias=True),
|
| 43 |
+
)
|
| 44 |
+
self.frequency_embedding_size = frequency_embedding_size
|
| 45 |
+
|
| 46 |
+
@staticmethod
|
| 47 |
+
def timestep_embedding(
|
| 48 |
+
timestep: torch.Tensor, dim: int, max_period: int = 10000
|
| 49 |
+
) -> torch.Tensor:
|
| 50 |
+
"""Create sinusoidal embeddings for scalar timesteps.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
timestep: Fractional or integral timesteps ``[B]``.
|
| 54 |
+
dim: Embedding width.
|
| 55 |
+
max_period: Minimum-frequency denominator.
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
Embeddings ``[B, dim]``.
|
| 59 |
+
"""
|
| 60 |
+
half = dim // 2
|
| 61 |
+
frequencies = torch.exp(
|
| 62 |
+
-math.log(max_period) * torch.arange(half, dtype=torch.float32) / half
|
| 63 |
+
).to(timestep.device)
|
| 64 |
+
arguments = timestep[:, None].float() * frequencies[None]
|
| 65 |
+
embedding = torch.cat((torch.cos(arguments), torch.sin(arguments)), dim=-1)
|
| 66 |
+
if dim % 2:
|
| 67 |
+
embedding = torch.cat(
|
| 68 |
+
(embedding, torch.zeros_like(embedding[:, :1])), dim=-1
|
| 69 |
+
)
|
| 70 |
+
return embedding
|
| 71 |
+
|
| 72 |
+
def forward(self, timestep: torch.Tensor) -> torch.Tensor:
|
| 73 |
+
"""Embed ``timestep`` into the transformer hidden width."""
|
| 74 |
+
frequencies = self.timestep_embedding(timestep, self.frequency_embedding_size)
|
| 75 |
+
return self.mlp(frequencies)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class SparseResBlock3d(nn.Module):
|
| 79 |
+
"""Timestep-modulated sparse residual block with optional paired resampling."""
|
| 80 |
+
|
| 81 |
+
def __init__(
|
| 82 |
+
self,
|
| 83 |
+
channels: int,
|
| 84 |
+
emb_channels: int,
|
| 85 |
+
out_channels: int | None = None,
|
| 86 |
+
*,
|
| 87 |
+
downsample: bool = False,
|
| 88 |
+
upsample: bool = False,
|
| 89 |
+
):
|
| 90 |
+
"""
|
| 91 |
+
Args:
|
| 92 |
+
channels: Input feature channels.
|
| 93 |
+
emb_channels: Timestep embedding channels consumed by the modulation.
|
| 94 |
+
out_channels: Output channels; defaults to ``channels``.
|
| 95 |
+
downsample: Halve the spatial resolution across the block.
|
| 96 |
+
upsample: Double the spatial resolution across the block.
|
| 97 |
+
|
| 98 |
+
Raises:
|
| 99 |
+
ValueError: Both ``downsample`` and ``upsample`` are set.
|
| 100 |
+
"""
|
| 101 |
+
super().__init__()
|
| 102 |
+
if downsample and upsample:
|
| 103 |
+
raise ValueError("cannot downsample and upsample in one block")
|
| 104 |
+
self.channels = channels
|
| 105 |
+
self.emb_channels = emb_channels
|
| 106 |
+
self.out_channels = out_channels or channels
|
| 107 |
+
self.hidden_channels = self.out_channels // 2
|
| 108 |
+
self.downsample = downsample
|
| 109 |
+
self.upsample = upsample
|
| 110 |
+
self.norm1 = LayerNorm32(channels, elementwise_affine=True, eps=1e-6)
|
| 111 |
+
self.norm2 = LayerNorm32(
|
| 112 |
+
self.hidden_channels, elementwise_affine=False, eps=1e-6
|
| 113 |
+
)
|
| 114 |
+
self.conv1 = SparseConv3d(channels, self.hidden_channels, 3)
|
| 115 |
+
self.conv2 = SparseConv3d(self.hidden_channels, self.hidden_channels, 3)
|
| 116 |
+
self.linear = zero_module(SparseLinear(self.hidden_channels, self.out_channels))
|
| 117 |
+
self.emb_layers = nn.Sequential(
|
| 118 |
+
nn.SiLU(),
|
| 119 |
+
nn.Linear(emb_channels, 2 * self.hidden_channels, bias=True),
|
| 120 |
+
)
|
| 121 |
+
self.skip_connection = (
|
| 122 |
+
SparseLinear(channels, self.out_channels)
|
| 123 |
+
if channels != self.out_channels
|
| 124 |
+
else nn.Identity()
|
| 125 |
+
)
|
| 126 |
+
self.updown: nn.Module | None = None
|
| 127 |
+
if downsample:
|
| 128 |
+
self.updown = SparseDownsample(2)
|
| 129 |
+
elif upsample:
|
| 130 |
+
self.updown = SparseUpsample(2)
|
| 131 |
+
|
| 132 |
+
def forward(self, x: SparseTensor, embedding: torch.Tensor) -> SparseTensor:
|
| 133 |
+
"""Apply residual convolution and optional paired resampling."""
|
| 134 |
+
scale, shift = self.emb_layers(embedding).to(x.dtype).chunk(2, dim=1)
|
| 135 |
+
if self.updown is not None:
|
| 136 |
+
x = self.updown(x)
|
| 137 |
+
h = x.replace(self.norm1(x.feats))
|
| 138 |
+
h = h.replace(F.silu(h.feats))
|
| 139 |
+
h = self.conv1(h)
|
| 140 |
+
h = h.replace(self.norm2(h.feats)) * (1 + scale) + shift
|
| 141 |
+
h = h.replace(F.silu(h.feats))
|
| 142 |
+
h = self.conv2(h)
|
| 143 |
+
h = h.replace(F.silu(h.feats))
|
| 144 |
+
return self.linear(h) + self.skip_connection(x)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
class Sparc3DTransformer(nn.Module):
|
| 148 |
+
"""Released sparse latent flow transformer with a validated narrow config."""
|
| 149 |
+
|
| 150 |
+
def __init__(
|
| 151 |
+
self,
|
| 152 |
+
config: Sparc3DTransformerConfig | None = None,
|
| 153 |
+
**config_values: Any,
|
| 154 |
+
):
|
| 155 |
+
"""
|
| 156 |
+
Args:
|
| 157 |
+
config: Validated configuration. Mutually exclusive with ``config_values``.
|
| 158 |
+
**config_values: Configuration fields to validate into a config instead.
|
| 159 |
+
|
| 160 |
+
Raises:
|
| 161 |
+
ValueError: Both a config and configuration values were given, or a value
|
| 162 |
+
fails validation.
|
| 163 |
+
"""
|
| 164 |
+
super().__init__()
|
| 165 |
+
if config is not None and config_values:
|
| 166 |
+
raise ValueError("pass either config or configuration keyword values")
|
| 167 |
+
self.config = config or Sparc3DTransformerConfig.from_dict(config_values)
|
| 168 |
+
config = self.config
|
| 169 |
+
self.dtype = torch.float16 if config.use_fp16 else torch.float32
|
| 170 |
+
|
| 171 |
+
channels = list(config.io_block_channels)
|
| 172 |
+
self.t_embedder = TimestepEmbedder(config.model_channels)
|
| 173 |
+
self.adaLN_modulation = nn.Sequential(
|
| 174 |
+
nn.SiLU(),
|
| 175 |
+
nn.Linear(config.model_channels, 6 * config.model_channels, bias=True),
|
| 176 |
+
)
|
| 177 |
+
self.input_layer = SparseLinear(config.in_channels, channels[0])
|
| 178 |
+
self.input_blocks = nn.ModuleList()
|
| 179 |
+
for current, following in zip(channels, channels[1:] + [config.model_channels]):
|
| 180 |
+
self.input_blocks.extend(
|
| 181 |
+
SparseResBlock3d(current, config.model_channels)
|
| 182 |
+
for _ in range(config.num_io_res_blocks - 1)
|
| 183 |
+
)
|
| 184 |
+
self.input_blocks.append(
|
| 185 |
+
SparseResBlock3d(
|
| 186 |
+
current,
|
| 187 |
+
config.model_channels,
|
| 188 |
+
out_channels=following,
|
| 189 |
+
downsample=True,
|
| 190 |
+
)
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
self.blocks = nn.ModuleList(
|
| 194 |
+
ModulatedSparseTransformerCrossBlock(
|
| 195 |
+
config.model_channels,
|
| 196 |
+
config.cond_channels,
|
| 197 |
+
config.num_heads,
|
| 198 |
+
mlp_ratio=config.mlp_ratio,
|
| 199 |
+
)
|
| 200 |
+
for _ in range(config.num_blocks)
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
self.out_blocks = nn.ModuleList()
|
| 204 |
+
previous_channels = [config.model_channels] + list(reversed(channels[1:]))
|
| 205 |
+
for current, previous in zip(reversed(channels), previous_channels):
|
| 206 |
+
self.out_blocks.append(
|
| 207 |
+
SparseResBlock3d(
|
| 208 |
+
previous * 2,
|
| 209 |
+
config.model_channels,
|
| 210 |
+
out_channels=current,
|
| 211 |
+
upsample=True,
|
| 212 |
+
)
|
| 213 |
+
)
|
| 214 |
+
self.out_blocks.extend(
|
| 215 |
+
SparseResBlock3d(
|
| 216 |
+
current * 2,
|
| 217 |
+
config.model_channels,
|
| 218 |
+
out_channels=current,
|
| 219 |
+
)
|
| 220 |
+
for _ in range(config.num_io_res_blocks - 1)
|
| 221 |
+
)
|
| 222 |
+
self.out_layer = SparseLinear(channels[0], config.out_channels)
|
| 223 |
+
|
| 224 |
+
self.initialize_weights()
|
| 225 |
+
if config.use_fp16:
|
| 226 |
+
self.convert_to_fp16()
|
| 227 |
+
|
| 228 |
+
def initialize_weights(self) -> None:
|
| 229 |
+
"""Initialize linear, timestep, modulation, and output projections."""
|
| 230 |
+
|
| 231 |
+
def initialize_linear(module: nn.Module) -> None:
|
| 232 |
+
if isinstance(module, nn.Linear):
|
| 233 |
+
nn.init.xavier_uniform_(module.weight)
|
| 234 |
+
if module.bias is not None:
|
| 235 |
+
nn.init.constant_(module.bias, 0)
|
| 236 |
+
|
| 237 |
+
self.apply(initialize_linear)
|
| 238 |
+
nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
|
| 239 |
+
nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
|
| 240 |
+
nn.init.constant_(self.adaLN_modulation[-1].weight, 0)
|
| 241 |
+
nn.init.constant_(self.adaLN_modulation[-1].bias, 0)
|
| 242 |
+
nn.init.constant_(self.out_layer.weight, 0)
|
| 243 |
+
nn.init.constant_(self.out_layer.bias, 0)
|
| 244 |
+
|
| 245 |
+
def convert_to_fp16(self) -> None:
|
| 246 |
+
"""Convert the I/O and transformer torso to float16."""
|
| 247 |
+
self.input_blocks.apply(convert_module_to_f16)
|
| 248 |
+
self.blocks.apply(convert_module_to_f16)
|
| 249 |
+
self.out_blocks.apply(convert_module_to_f16)
|
| 250 |
+
self.dtype = torch.float16
|
| 251 |
+
|
| 252 |
+
def forward(
|
| 253 |
+
self, x: SparseTensor, timestep: torch.Tensor, cond: torch.Tensor
|
| 254 |
+
) -> SparseTensor:
|
| 255 |
+
"""Predict latent flow for sparse ``x`` under image ``cond``."""
|
| 256 |
+
if x.shape[0] != timestep.shape[0] or x.shape[0] != cond.shape[0]:
|
| 257 |
+
raise ValueError("sparse input, timestep, and condition batches must match")
|
| 258 |
+
if x.feats.shape[1] != self.config.in_channels:
|
| 259 |
+
raise ValueError(
|
| 260 |
+
f"expected {self.config.in_channels} input channels, "
|
| 261 |
+
f"got {x.feats.shape[1]}"
|
| 262 |
+
)
|
| 263 |
+
h = self.input_layer(x).type(self.dtype)
|
| 264 |
+
timestep_embedding = self.t_embedder(timestep)
|
| 265 |
+
convolution_embedding = timestep_embedding.to(self.dtype)
|
| 266 |
+
skips = []
|
| 267 |
+
for block in self.input_blocks:
|
| 268 |
+
h = block(h, convolution_embedding)
|
| 269 |
+
skips.append(h.feats)
|
| 270 |
+
|
| 271 |
+
modulation = self.adaLN_modulation(timestep_embedding).to(self.dtype)
|
| 272 |
+
context = cond.to(self.dtype)
|
| 273 |
+
for block in self.blocks:
|
| 274 |
+
h = block(h, modulation, context)
|
| 275 |
+
|
| 276 |
+
for block, skip in zip(self.out_blocks, reversed(skips)):
|
| 277 |
+
h = h.replace(torch.cat((h.feats, skip), dim=1))
|
| 278 |
+
h = block(h, convolution_embedding)
|
| 279 |
+
h = h.replace(F.layer_norm(h.feats, h.feats.shape[-1:]))
|
| 280 |
+
return self.out_layer(h.type(x.dtype))
|
sparc3d/nn/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Private neural network primitives for the sparse latent transformer."""
|
| 2 |
+
|
| 3 |
+
from .norm import LayerNorm32
|
| 4 |
+
|
| 5 |
+
__all__ = ["LayerNorm32"]
|
sparc3d/nn/norm.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Numerically stable normalization layers for the sparse transformer."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Microsoft TRELLIS under the MIT License.
|
| 4 |
+
# Modified for SPARC3D to retain float32 layer normalization only.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from torch import nn
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class LayerNorm32(nn.LayerNorm):
|
| 13 |
+
"""Layer normalization with float32 statistics and input-dtype output."""
|
| 14 |
+
|
| 15 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 16 |
+
"""Normalize ``x`` in float32 and restore its dtype."""
|
| 17 |
+
return super().forward(x.float()).to(x.dtype)
|
sparc3d/nn/rope.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Spatial rotary position embedding shared by the sparse attention blocks."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Microsoft TRELLIS under the MIT License.
|
| 4 |
+
# Modified for SPARC3D to retain long-grid frequency scaling only.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import math
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from torch import nn
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class RotaryPositionEmbedder(nn.Module):
|
| 15 |
+
"""Spatial rotary embedding with frequency scaling for long coordinate grids."""
|
| 16 |
+
|
| 17 |
+
def __init__(self, hidden_size: int, in_channels: int = 3):
|
| 18 |
+
"""
|
| 19 |
+
Args:
|
| 20 |
+
hidden_size: Head dimension the rotation is applied over; must be even.
|
| 21 |
+
in_channels: Spatial coordinate axes sharing that dimension.
|
| 22 |
+
|
| 23 |
+
Raises:
|
| 24 |
+
ValueError: ``hidden_size`` is odd or too small to split across axes.
|
| 25 |
+
"""
|
| 26 |
+
super().__init__()
|
| 27 |
+
if hidden_size % 2:
|
| 28 |
+
raise ValueError("hidden_size must be divisible by 2")
|
| 29 |
+
if hidden_size // in_channels // 2 == 0:
|
| 30 |
+
raise ValueError("hidden_size is too small for spatial RoPE")
|
| 31 |
+
self.hidden_size = hidden_size
|
| 32 |
+
self.in_channels = in_channels
|
| 33 |
+
self.freq_dim = hidden_size // in_channels // 2
|
| 34 |
+
freqs = torch.arange(self.freq_dim, dtype=torch.float32) / self.freq_dim
|
| 35 |
+
self.register_buffer("freqs", 1.0 / (10000**freqs), persistent=False)
|
| 36 |
+
|
| 37 |
+
@staticmethod
|
| 38 |
+
def _update_freqs(
|
| 39 |
+
freqs: torch.Tensor, train_length: int, infer_length: int
|
| 40 |
+
) -> torch.Tensor:
|
| 41 |
+
wavelengths = freqs * train_length / (2 * math.pi)
|
| 42 |
+
threshold = 24
|
| 43 |
+
ratio = (wavelengths - 1) / (threshold - 1)
|
| 44 |
+
ratio = ratio + (1 - ratio) * train_length / infer_length
|
| 45 |
+
ratio = torch.where(wavelengths > threshold, 1.0, ratio)
|
| 46 |
+
ratio = torch.where(wavelengths < 1, train_length / infer_length, ratio)
|
| 47 |
+
return freqs * ratio
|
| 48 |
+
|
| 49 |
+
def _get_phases(self, indices: torch.Tensor) -> torch.Tensor:
|
| 50 |
+
max_length = int(indices.max().item())
|
| 51 |
+
if max_length < 64:
|
| 52 |
+
freqs = self.freqs
|
| 53 |
+
else:
|
| 54 |
+
freqs = self._update_freqs(
|
| 55 |
+
self.freqs, train_length=64, infer_length=(64 + max_length) // 2
|
| 56 |
+
)
|
| 57 |
+
phases = torch.outer(indices, freqs.to(indices.device))
|
| 58 |
+
return torch.polar(torch.ones_like(phases), phases)
|
| 59 |
+
|
| 60 |
+
@staticmethod
|
| 61 |
+
def _rotary_embedding(x: torch.Tensor, phases: torch.Tensor) -> torch.Tensor:
|
| 62 |
+
x_complex = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
|
| 63 |
+
rotated = x_complex * phases.unsqueeze(1)
|
| 64 |
+
return torch.view_as_real(rotated).reshape(*rotated.shape[:-1], -1).to(x.dtype)
|
| 65 |
+
|
| 66 |
+
def forward(
|
| 67 |
+
self,
|
| 68 |
+
q: torch.Tensor,
|
| 69 |
+
k: torch.Tensor,
|
| 70 |
+
indices: torch.Tensor,
|
| 71 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 72 |
+
"""Rotate queries and keys at ``indices``.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
q: Queries ending in ``[H, D]``.
|
| 76 |
+
k: Keys with the same shape as ``q``.
|
| 77 |
+
indices: Spatial positions ending in three coordinates.
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
Rotated queries and keys.
|
| 81 |
+
"""
|
| 82 |
+
phases = self._get_phases(indices.reshape(-1)).reshape(*indices.shape[:-1], -1)
|
| 83 |
+
missing = self.hidden_size // 2 - phases.shape[-1]
|
| 84 |
+
if missing > 0:
|
| 85 |
+
padding = torch.polar(
|
| 86 |
+
torch.ones(*phases.shape[:-1], missing, device=phases.device),
|
| 87 |
+
torch.zeros(*phases.shape[:-1], missing, device=phases.device),
|
| 88 |
+
)
|
| 89 |
+
phases = torch.cat((phases, padding), dim=-1)
|
| 90 |
+
return (
|
| 91 |
+
self._rotary_embedding(q, phases),
|
| 92 |
+
self._rotary_embedding(k, phases),
|
| 93 |
+
)
|
sparc3d/nn/sparse/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Private sparse tensor and neural network operations."""
|
| 2 |
+
|
| 3 |
+
from .basic import SparseTensor
|
| 4 |
+
from .conv import SparseConv3d
|
| 5 |
+
from .linear import SparseLinear
|
| 6 |
+
from .spatial import SparseDownsample, SparseUpsample
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
"SparseConv3d",
|
| 10 |
+
"SparseDownsample",
|
| 11 |
+
"SparseLinear",
|
| 12 |
+
"SparseTensor",
|
| 13 |
+
"SparseUpsample",
|
| 14 |
+
]
|
sparc3d/nn/sparse/attention/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Full flash attention over sparse variable-length sequences."""
|
| 2 |
+
|
| 3 |
+
from .modules import SparseMultiHeadAttention
|
| 4 |
+
|
| 5 |
+
__all__ = ["SparseMultiHeadAttention"]
|
sparc3d/nn/sparse/attention/full_attn.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Variable-length full flash attention for sparse feature sequences."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Microsoft TRELLIS under the MIT License.
|
| 4 |
+
# Modified for SPARC3D: variable-length flash attention only.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from ..basic import SparseTensor
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _flash_attn():
|
| 14 |
+
"""Import flash-attn when sparse attention is first executed."""
|
| 15 |
+
try:
|
| 16 |
+
import flash_attn
|
| 17 |
+
except ImportError as exc:
|
| 18 |
+
raise ImportError(
|
| 19 |
+
"Sparc3DTransformer attention requires flash-attn; "
|
| 20 |
+
"install sparc3d[inference]"
|
| 21 |
+
) from exc
|
| 22 |
+
return flash_attn
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _lengths(tensor: SparseTensor) -> list[int]:
|
| 26 |
+
return [span.stop - span.start for span in tensor.layout]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _cumulative(lengths: list[int], device: torch.device) -> torch.Tensor:
|
| 30 |
+
return torch.tensor(
|
| 31 |
+
[0, *lengths], device=device, dtype=torch.int32
|
| 32 |
+
).cumsum(0, dtype=torch.int32)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def sparse_scaled_dot_product_attention(
|
| 36 |
+
query: SparseTensor, kv: torch.Tensor | None = None
|
| 37 |
+
) -> SparseTensor:
|
| 38 |
+
"""Apply variable-length full flash attention over sparse queries.
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
query: Packed sparse ``qkv`` of shape ``[N, 3, H, D]`` for self-attention,
|
| 42 |
+
or sparse queries ``[N, H, D]`` when ``kv`` is given.
|
| 43 |
+
kv: Dense packed keys and values ``[B, L, 2, H, D]`` for cross-attention;
|
| 44 |
+
``None`` selects self-attention over the packed ``query``.
|
| 45 |
+
|
| 46 |
+
Returns:
|
| 47 |
+
Sparse output over the query's coordinates.
|
| 48 |
+
|
| 49 |
+
Raises:
|
| 50 |
+
TypeError: ``query`` is not sparse.
|
| 51 |
+
ValueError: ``kv`` does not share the query's batch size.
|
| 52 |
+
"""
|
| 53 |
+
flash_attn = _flash_attn()
|
| 54 |
+
if not isinstance(query, SparseTensor):
|
| 55 |
+
raise TypeError("Sparc3D sparse attention requires sparse queries")
|
| 56 |
+
query_lengths = _lengths(query)
|
| 57 |
+
cumulative_query = _cumulative(query_lengths, query.device)
|
| 58 |
+
|
| 59 |
+
if kv is None:
|
| 60 |
+
output = flash_attn.flash_attn_varlen_qkvpacked_func(
|
| 61 |
+
query.feats, cumulative_query, max(query_lengths)
|
| 62 |
+
)
|
| 63 |
+
return query.replace(output)
|
| 64 |
+
|
| 65 |
+
batch, length = kv.shape[:2]
|
| 66 |
+
if batch != query.shape[0]:
|
| 67 |
+
raise ValueError("query and context batch sizes must match")
|
| 68 |
+
key_lengths = [length] * batch
|
| 69 |
+
output = flash_attn.flash_attn_varlen_kvpacked_func(
|
| 70 |
+
query.feats,
|
| 71 |
+
kv.reshape(batch * length, *kv.shape[2:]),
|
| 72 |
+
cumulative_query,
|
| 73 |
+
_cumulative(key_lengths, query.device),
|
| 74 |
+
max(query_lengths),
|
| 75 |
+
max(key_lengths),
|
| 76 |
+
)
|
| 77 |
+
return query.replace(output)
|
sparc3d/nn/sparse/attention/modules.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Full multi-head attention over sparse variable-length batches."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Microsoft TRELLIS under the MIT License.
|
| 4 |
+
# Modified for SPARC3D: full flash attention with spatial RoPE.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn.functional as F
|
| 10 |
+
from torch import nn
|
| 11 |
+
|
| 12 |
+
from ...rope import RotaryPositionEmbedder
|
| 13 |
+
from ..basic import SparseTensor
|
| 14 |
+
from .full_attn import sparse_scaled_dot_product_attention
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class SparseMultiHeadRMSNorm(nn.Module):
|
| 18 |
+
"""Per-head RMS normalization of a ``[N, H, D]`` query or key projection."""
|
| 19 |
+
|
| 20 |
+
def __init__(self, dim: int, heads: int):
|
| 21 |
+
"""
|
| 22 |
+
Args:
|
| 23 |
+
dim: Channels per head.
|
| 24 |
+
heads: Attention heads, each with its own gain.
|
| 25 |
+
"""
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.scale = dim**0.5
|
| 28 |
+
self.gamma = nn.Parameter(torch.ones(heads, dim))
|
| 29 |
+
|
| 30 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 31 |
+
"""Normalize the head dimension in float32 and restore ``x``'s dtype."""
|
| 32 |
+
return (F.normalize(x.float(), dim=-1) * self.gamma * self.scale).to(x.dtype)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class SparseMultiHeadAttention(nn.Module):
|
| 36 |
+
"""Full flash self- or cross-attention over sparse queries."""
|
| 37 |
+
|
| 38 |
+
def __init__(
|
| 39 |
+
self,
|
| 40 |
+
channels: int,
|
| 41 |
+
num_heads: int,
|
| 42 |
+
ctx_channels: int | None = None,
|
| 43 |
+
*,
|
| 44 |
+
attention_type: str = "self",
|
| 45 |
+
):
|
| 46 |
+
"""
|
| 47 |
+
Args:
|
| 48 |
+
channels: Query feature channels; must be divisible by ``num_heads``.
|
| 49 |
+
num_heads: Attention heads.
|
| 50 |
+
ctx_channels: Context channels for cross-attention; defaults to
|
| 51 |
+
``channels``. Ignored for self-attention.
|
| 52 |
+
attention_type: ``"self"`` or ``"cross"``. Self-attention alone carries
|
| 53 |
+
rotary embeddings and query/key RMS normalization, matching the
|
| 54 |
+
parameters the released weights provide.
|
| 55 |
+
|
| 56 |
+
Raises:
|
| 57 |
+
ValueError: ``channels`` is not divisible by ``num_heads``, or
|
| 58 |
+
``attention_type`` is not one of the two supported kinds.
|
| 59 |
+
"""
|
| 60 |
+
super().__init__()
|
| 61 |
+
if channels % num_heads:
|
| 62 |
+
raise ValueError("channels must be divisible by num_heads")
|
| 63 |
+
if attention_type not in {"self", "cross"}:
|
| 64 |
+
raise ValueError("attention_type must be 'self' or 'cross'")
|
| 65 |
+
self.channels = channels
|
| 66 |
+
self.ctx_channels = ctx_channels or channels
|
| 67 |
+
self.num_heads = num_heads
|
| 68 |
+
self.head_dim = channels // num_heads
|
| 69 |
+
self._type = attention_type
|
| 70 |
+
if attention_type == "self":
|
| 71 |
+
self.to_qkv = nn.Linear(channels, channels * 3, bias=True)
|
| 72 |
+
# Self-attention alone is positional and query/key normalized; the
|
| 73 |
+
# released weights carry no RoPE or RMS parameters on the cross path.
|
| 74 |
+
self.q_rms_norm = SparseMultiHeadRMSNorm(self.head_dim, num_heads)
|
| 75 |
+
self.k_rms_norm = SparseMultiHeadRMSNorm(self.head_dim, num_heads)
|
| 76 |
+
self.rope = RotaryPositionEmbedder(self.head_dim)
|
| 77 |
+
else:
|
| 78 |
+
self.to_q = nn.Linear(channels, channels, bias=True)
|
| 79 |
+
self.to_kv = nn.Linear(self.ctx_channels, channels * 2, bias=True)
|
| 80 |
+
self.to_out = nn.Linear(channels, channels)
|
| 81 |
+
|
| 82 |
+
@staticmethod
|
| 83 |
+
def _linear(
|
| 84 |
+
module: nn.Linear, x: SparseTensor | torch.Tensor
|
| 85 |
+
) -> SparseTensor | torch.Tensor:
|
| 86 |
+
if isinstance(x, SparseTensor):
|
| 87 |
+
return x.replace(module(x.feats))
|
| 88 |
+
return module(x)
|
| 89 |
+
|
| 90 |
+
def _fused_pre(
|
| 91 |
+
self, x: SparseTensor | torch.Tensor, count: int
|
| 92 |
+
) -> SparseTensor | torch.Tensor:
|
| 93 |
+
if isinstance(x, SparseTensor):
|
| 94 |
+
return x.reshape(count, self.num_heads, self.head_dim)
|
| 95 |
+
return x.reshape(*x.shape[:2], count, self.num_heads, self.head_dim)
|
| 96 |
+
|
| 97 |
+
def forward(
|
| 98 |
+
self,
|
| 99 |
+
x: SparseTensor,
|
| 100 |
+
context: SparseTensor | torch.Tensor | None = None,
|
| 101 |
+
) -> SparseTensor:
|
| 102 |
+
"""Attend from sparse ``x`` to itself or dense ``context``."""
|
| 103 |
+
if self._type == "self":
|
| 104 |
+
qkv = self._fused_pre(self._linear(self.to_qkv, x), 3)
|
| 105 |
+
assert isinstance(qkv, SparseTensor)
|
| 106 |
+
q, k, v = qkv.feats.unbind(dim=1)
|
| 107 |
+
q, k = self.rope(q, k, qkv.coords[:, 1:])
|
| 108 |
+
q = self.q_rms_norm(q)
|
| 109 |
+
k = self.k_rms_norm(k)
|
| 110 |
+
qkv = qkv.replace(torch.stack((q, k, v), dim=1))
|
| 111 |
+
output = sparse_scaled_dot_product_attention(qkv)
|
| 112 |
+
else:
|
| 113 |
+
if context is None:
|
| 114 |
+
raise ValueError("cross-attention requires context")
|
| 115 |
+
query = self._linear(self.to_q, x)
|
| 116 |
+
assert isinstance(query, SparseTensor)
|
| 117 |
+
query = query.reshape(self.num_heads, self.head_dim)
|
| 118 |
+
kv = self._fused_pre(self._linear(self.to_kv, context), 2)
|
| 119 |
+
output = sparse_scaled_dot_product_attention(query, kv)
|
| 120 |
+
output = output.reshape(-1)
|
| 121 |
+
projected = self._linear(self.to_out, output)
|
| 122 |
+
assert isinstance(projected, SparseTensor)
|
| 123 |
+
return projected
|
sparc3d/nn/sparse/basic.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Private sparse tensor wrapper with contiguous batches and spatial caches."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Microsoft TRELLIS under the MIT License.
|
| 4 |
+
# Modified for SPARC3D to use the local sparse backend and paired resampling cache.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from collections.abc import Callable
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
from sparc3d.ops import sparse_backend
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class SparseTensor:
|
| 17 |
+
"""Sparse features with batch-first coordinates and shared spatial caches."""
|
| 18 |
+
|
| 19 |
+
def __init__(
|
| 20 |
+
self,
|
| 21 |
+
feats: torch.Tensor | None = None,
|
| 22 |
+
coords: torch.Tensor | None = None,
|
| 23 |
+
shape: torch.Size | None = None,
|
| 24 |
+
layout: list[slice] | None = None,
|
| 25 |
+
*,
|
| 26 |
+
data: Any | None = None,
|
| 27 |
+
scale: tuple[int, ...] = (1, 1, 1),
|
| 28 |
+
spatial_cache: dict[str, dict[str, Any]] | None = None,
|
| 29 |
+
):
|
| 30 |
+
"""
|
| 31 |
+
Args:
|
| 32 |
+
feats: Features ``[N, C]``, one row per occupied coordinate.
|
| 33 |
+
coords: Coordinates ``[N, 4]`` as ``(batch, x, y, z)``, sorted by batch.
|
| 34 |
+
shape: Logical shape; derived from ``coords`` and ``feats`` when omitted.
|
| 35 |
+
layout: Per-batch row slices; recomputed from ``coords`` when omitted.
|
| 36 |
+
data: Backend sparse tensor to adopt instead of ``feats`` and ``coords``.
|
| 37 |
+
scale: Cumulative downsampling factor relative to the original grid, so
|
| 38 |
+
``(1, 1, 1)`` is the original resolution and each downsample
|
| 39 |
+
multiplies it. It also keys :attr:`_spatial_cache`, so it has to
|
| 40 |
+
differ between nesting levels.
|
| 41 |
+
spatial_cache: Cache shared with tensors over the same geometry, which is
|
| 42 |
+
how a downsample hands its inverse mapping to the paired upsample.
|
| 43 |
+
|
| 44 |
+
Raises:
|
| 45 |
+
ValueError: Both ``data`` and explicit features or coordinates were given,
|
| 46 |
+
either is missing, the shapes disagree, or the tensor would be empty.
|
| 47 |
+
"""
|
| 48 |
+
if data is not None:
|
| 49 |
+
if feats is not None or coords is not None:
|
| 50 |
+
raise ValueError("provide either data or feats and coords")
|
| 51 |
+
feats = sparse_backend.get_feats(data)
|
| 52 |
+
coords = data.indices
|
| 53 |
+
if feats is None or coords is None:
|
| 54 |
+
raise ValueError("feats and coords are required")
|
| 55 |
+
if coords.ndim != 2 or coords.shape[1] != 4:
|
| 56 |
+
raise ValueError(f"expected coords [N, 4], got {tuple(coords.shape)}")
|
| 57 |
+
if feats.shape[0] != coords.shape[0]:
|
| 58 |
+
raise ValueError("features and coordinates must have the same row count")
|
| 59 |
+
if coords.shape[0] == 0:
|
| 60 |
+
raise ValueError("a SparseTensor cannot be empty")
|
| 61 |
+
self._feats = feats
|
| 62 |
+
self._coords = coords.int().contiguous()
|
| 63 |
+
self.data = data
|
| 64 |
+
batch_size = int(self._coords[:, 0].max().item()) + 1
|
| 65 |
+
self._shape = shape or torch.Size((batch_size, *feats.shape[1:]))
|
| 66 |
+
self._layout = layout or self._calculate_layout(self._coords, batch_size)
|
| 67 |
+
self._scale = tuple(scale)
|
| 68 |
+
self._spatial_cache = spatial_cache if spatial_cache is not None else {}
|
| 69 |
+
self._validate_layout()
|
| 70 |
+
|
| 71 |
+
@staticmethod
|
| 72 |
+
def _calculate_layout(coords: torch.Tensor, batch_size: int) -> list[slice]:
|
| 73 |
+
counts = torch.bincount(coords[:, 0].long(), minlength=batch_size)
|
| 74 |
+
offsets = torch.cumsum(counts, dim=0)
|
| 75 |
+
return [
|
| 76 |
+
slice(int(offsets[i] - counts[i]), int(offsets[i]))
|
| 77 |
+
for i in range(batch_size)
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
def _validate_layout(self) -> None:
|
| 81 |
+
for batch, span in enumerate(self._layout):
|
| 82 |
+
if not torch.all(self._coords[span, 0] == batch):
|
| 83 |
+
raise ValueError("coordinates for each batch must be contiguous")
|
| 84 |
+
|
| 85 |
+
def backend_data(self) -> Any:
|
| 86 |
+
"""Return the sparse-backend tensor, creating it lazily."""
|
| 87 |
+
if self.data is None:
|
| 88 |
+
self.data = sparse_backend.sparse_tensor(self._feats, self._coords)
|
| 89 |
+
return self.data
|
| 90 |
+
|
| 91 |
+
@property
|
| 92 |
+
def shape(self) -> torch.Size:
|
| 93 |
+
"""Logical shape ``[B, *feature_shape]``."""
|
| 94 |
+
return self._shape
|
| 95 |
+
|
| 96 |
+
@property
|
| 97 |
+
def layout(self) -> list[slice]:
|
| 98 |
+
"""Contiguous feature slice for each batch element."""
|
| 99 |
+
return self._layout
|
| 100 |
+
|
| 101 |
+
@property
|
| 102 |
+
def feats(self) -> torch.Tensor:
|
| 103 |
+
"""Feature values in coordinate-row order."""
|
| 104 |
+
return self._feats
|
| 105 |
+
|
| 106 |
+
@property
|
| 107 |
+
def coords(self) -> torch.Tensor:
|
| 108 |
+
"""Batch-first integer coordinates in ``[batch, x, y, z]`` order."""
|
| 109 |
+
return self._coords
|
| 110 |
+
|
| 111 |
+
@property
|
| 112 |
+
def dtype(self) -> torch.dtype:
|
| 113 |
+
"""Feature dtype."""
|
| 114 |
+
return self.feats.dtype
|
| 115 |
+
|
| 116 |
+
@property
|
| 117 |
+
def device(self) -> torch.device:
|
| 118 |
+
"""Feature device."""
|
| 119 |
+
return self.feats.device
|
| 120 |
+
|
| 121 |
+
@property
|
| 122 |
+
def scale(self) -> tuple[int, ...]:
|
| 123 |
+
"""Current spatial scale tracked by paired resampling."""
|
| 124 |
+
return self._scale
|
| 125 |
+
|
| 126 |
+
def replace(self, feats: torch.Tensor) -> "SparseTensor":
|
| 127 |
+
"""Return a tensor sharing coordinates and caches with new features."""
|
| 128 |
+
if feats.shape[0] != self.feats.shape[0]:
|
| 129 |
+
raise ValueError("replacement features must preserve the sparse row count")
|
| 130 |
+
data = None
|
| 131 |
+
if self.data is not None and feats.ndim == 2:
|
| 132 |
+
data = sparse_backend.replace_feats(self.data, feats)
|
| 133 |
+
return SparseTensor(
|
| 134 |
+
feats=feats if data is None else None,
|
| 135 |
+
coords=self.coords if data is None else None,
|
| 136 |
+
data=data,
|
| 137 |
+
shape=torch.Size((self.shape[0], *feats.shape[1:])),
|
| 138 |
+
layout=self.layout,
|
| 139 |
+
scale=self._scale,
|
| 140 |
+
spatial_cache=self._spatial_cache,
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
def reshape(self, *shape: int) -> "SparseTensor":
|
| 144 |
+
"""Reshape feature dimensions while retaining the sparse coordinates."""
|
| 145 |
+
return self.replace(self.feats.reshape(self.feats.shape[0], *shape))
|
| 146 |
+
|
| 147 |
+
def type(self, dtype: torch.dtype) -> "SparseTensor":
|
| 148 |
+
"""Cast features to ``dtype``."""
|
| 149 |
+
return self.replace(self.feats.to(dtype=dtype))
|
| 150 |
+
|
| 151 |
+
def register_spatial_cache(self, key: str, value: Any) -> None:
|
| 152 |
+
"""Store ``value`` under ``key`` for the current scale."""
|
| 153 |
+
self._spatial_cache.setdefault(str(self._scale), {})[key] = value
|
| 154 |
+
|
| 155 |
+
def get_spatial_cache(self, key: str) -> Any | None:
|
| 156 |
+
"""Return a cached value for the current scale."""
|
| 157 |
+
return self._spatial_cache.get(str(self._scale), {}).get(key)
|
| 158 |
+
|
| 159 |
+
def _elementwise(
|
| 160 |
+
self,
|
| 161 |
+
other: torch.Tensor | "SparseTensor" | float,
|
| 162 |
+
operation: Callable[[torch.Tensor, Any], torch.Tensor],
|
| 163 |
+
) -> "SparseTensor":
|
| 164 |
+
if isinstance(other, SparseTensor):
|
| 165 |
+
if not torch.equal(self.coords, other.coords):
|
| 166 |
+
raise ValueError("sparse operands must have identical coordinates")
|
| 167 |
+
other = other.feats
|
| 168 |
+
elif isinstance(other, torch.Tensor) and other.ndim >= 1:
|
| 169 |
+
if other.shape[0] == self.shape[0] and other.shape != self.feats.shape:
|
| 170 |
+
other = sparse_batch_broadcast(self, other)
|
| 171 |
+
return self.replace(operation(self.feats, other))
|
| 172 |
+
|
| 173 |
+
def __add__(self, other):
|
| 174 |
+
return self._elementwise(other, torch.add)
|
| 175 |
+
|
| 176 |
+
def __mul__(self, other):
|
| 177 |
+
return self._elementwise(other, torch.mul)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def sparse_batch_broadcast(input: SparseTensor, other: torch.Tensor) -> torch.Tensor:
|
| 181 |
+
"""Broadcast a batch-first tensor over sparse feature rows."""
|
| 182 |
+
output = torch.empty_like(input.feats)
|
| 183 |
+
for batch, span in enumerate(input.layout):
|
| 184 |
+
output[span] = other[batch]
|
| 185 |
+
return output
|
sparc3d/nn/sparse/conv.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sparse submanifold convolution over spconv."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Microsoft TRELLIS under the MIT License.
|
| 4 |
+
# Modified for SPARC3D to route convolution through ``ops.sparse_backend``.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from torch import nn
|
| 10 |
+
|
| 11 |
+
from sparc3d.ops import sparse_backend
|
| 12 |
+
|
| 13 |
+
from .basic import SparseTensor
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class _DeferredConv3d(nn.Module):
|
| 17 |
+
"""State-compatible placeholder used when spconv is not installed."""
|
| 18 |
+
|
| 19 |
+
def __init__(
|
| 20 |
+
self,
|
| 21 |
+
in_channels: int,
|
| 22 |
+
out_channels: int,
|
| 23 |
+
kernel_size: int,
|
| 24 |
+
bias: bool,
|
| 25 |
+
):
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.weight = nn.Parameter(
|
| 28 |
+
torch.empty(
|
| 29 |
+
out_channels,
|
| 30 |
+
kernel_size,
|
| 31 |
+
kernel_size,
|
| 32 |
+
kernel_size,
|
| 33 |
+
in_channels,
|
| 34 |
+
)
|
| 35 |
+
)
|
| 36 |
+
self.bias = nn.Parameter(torch.empty(out_channels)) if bias else None
|
| 37 |
+
nn.init.kaiming_uniform_(self.weight, a=5**0.5)
|
| 38 |
+
if self.bias is not None:
|
| 39 |
+
nn.init.zeros_(self.bias)
|
| 40 |
+
|
| 41 |
+
def forward(self, _input):
|
| 42 |
+
"""Raise the deferred sparse-backend import error."""
|
| 43 |
+
raise ImportError("sparse convolution requires spconv")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class SparseConv3d(nn.Module):
|
| 47 |
+
"""Coordinate-preserving three-dimensional sparse convolution."""
|
| 48 |
+
|
| 49 |
+
_sparc3d_convertible = True
|
| 50 |
+
|
| 51 |
+
def __init__(
|
| 52 |
+
self,
|
| 53 |
+
in_channels: int,
|
| 54 |
+
out_channels: int,
|
| 55 |
+
kernel_size: int,
|
| 56 |
+
bias: bool = True,
|
| 57 |
+
):
|
| 58 |
+
"""
|
| 59 |
+
Args:
|
| 60 |
+
in_channels: Input feature channels.
|
| 61 |
+
out_channels: Output feature channels.
|
| 62 |
+
kernel_size: Cubic kernel extent.
|
| 63 |
+
bias: Add a bias term.
|
| 64 |
+
"""
|
| 65 |
+
super().__init__()
|
| 66 |
+
try:
|
| 67 |
+
self.conv = sparse_backend.Conv3d(
|
| 68 |
+
in_channels,
|
| 69 |
+
out_channels,
|
| 70 |
+
kernel_size=kernel_size,
|
| 71 |
+
bias=bias,
|
| 72 |
+
)
|
| 73 |
+
except ImportError:
|
| 74 |
+
# Construction and state-dict validation must work without spconv.
|
| 75 |
+
# Forward still raises; this is not an inference fallback.
|
| 76 |
+
self.conv = _DeferredConv3d(in_channels, out_channels, kernel_size, bias)
|
| 77 |
+
self.out_channels = out_channels
|
| 78 |
+
|
| 79 |
+
def forward(self, x: SparseTensor) -> SparseTensor:
|
| 80 |
+
"""Convolve ``x`` while preserving its coordinate lineage."""
|
| 81 |
+
output = self.conv(x.backend_data())
|
| 82 |
+
return SparseTensor(
|
| 83 |
+
data=output,
|
| 84 |
+
shape=x.shape[:1] + (self.out_channels,),
|
| 85 |
+
layout=x.layout,
|
| 86 |
+
scale=x.scale,
|
| 87 |
+
spatial_cache=x._spatial_cache,
|
| 88 |
+
)
|
sparc3d/nn/sparse/linear.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Linear projection over sparse feature rows."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Microsoft TRELLIS under the MIT License.
|
| 4 |
+
# Modified for SPARC3D's private sparse tensor.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from torch import nn
|
| 9 |
+
|
| 10 |
+
from .basic import SparseTensor
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class SparseLinear(nn.Linear):
|
| 14 |
+
"""Linear layer preserving sparse coordinates and caches."""
|
| 15 |
+
|
| 16 |
+
def forward(self, input: SparseTensor) -> SparseTensor:
|
| 17 |
+
"""Project ``input`` features."""
|
| 18 |
+
return input.replace(super().forward(input.feats))
|
sparc3d/nn/sparse/nonlinearity.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Nonlinear activations over sparse feature rows."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Microsoft TRELLIS under the MIT License.
|
| 4 |
+
# Modified for SPARC3D to retain GELU only.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from torch import nn
|
| 9 |
+
|
| 10 |
+
from .basic import SparseTensor
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class SparseGELU(nn.GELU):
|
| 14 |
+
"""GELU activation preserving sparse coordinates and caches."""
|
| 15 |
+
|
| 16 |
+
def forward(self, input: SparseTensor) -> SparseTensor:
|
| 17 |
+
"""Activate ``input`` features."""
|
| 18 |
+
return input.replace(super().forward(input.feats))
|
sparc3d/nn/sparse/spatial.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Paired average downsampling and cached nearest-neighbor upsampling."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Microsoft TRELLIS under the MIT License.
|
| 4 |
+
# Modified for SPARC3D to retain only paired downsample and upsample operations.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from collections.abc import Sequence
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from torch import nn
|
| 12 |
+
|
| 13 |
+
from .basic import SparseTensor
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _factor_tuple(factor: int | Sequence[int], dimensions: int) -> tuple[int, ...]:
|
| 17 |
+
values = tuple(factor) if isinstance(factor, Sequence) else (factor,) * dimensions
|
| 18 |
+
if len(values) != dimensions or any(value <= 0 for value in values):
|
| 19 |
+
raise ValueError("factor must provide one positive value per spatial dimension")
|
| 20 |
+
return values
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class SparseDownsample(nn.Module):
|
| 24 |
+
"""Average features that collapse onto the same downsampled coordinate."""
|
| 25 |
+
|
| 26 |
+
def __init__(self, factor: int | Sequence[int]):
|
| 27 |
+
"""
|
| 28 |
+
Args:
|
| 29 |
+
factor: Per-axis downsampling factor, or one factor for every axis.
|
| 30 |
+
"""
|
| 31 |
+
super().__init__()
|
| 32 |
+
self.factor = tuple(factor) if isinstance(factor, Sequence) else factor
|
| 33 |
+
|
| 34 |
+
def forward(self, input: SparseTensor) -> SparseTensor:
|
| 35 |
+
"""Downsample ``input`` and register the inverse mapping."""
|
| 36 |
+
dimensions = input.coords.shape[-1] - 1
|
| 37 |
+
factor = _factor_tuple(self.factor, dimensions)
|
| 38 |
+
coordinate_columns = list(input.coords.unbind(dim=-1))
|
| 39 |
+
for axis, value in enumerate(factor):
|
| 40 |
+
coordinate_columns[axis + 1] = coordinate_columns[axis + 1] // value
|
| 41 |
+
|
| 42 |
+
extents = [
|
| 43 |
+
int(coordinate_columns[axis + 1].max().item()) + 1
|
| 44 |
+
for axis in range(dimensions)
|
| 45 |
+
]
|
| 46 |
+
offsets = torch.cumprod(torch.tensor(extents[::-1]), 0).tolist()[::-1] + [1]
|
| 47 |
+
codes = sum(
|
| 48 |
+
coordinate * offset
|
| 49 |
+
for coordinate, offset in zip(coordinate_columns, offsets)
|
| 50 |
+
)
|
| 51 |
+
unique_codes, inverse = codes.unique(return_inverse=True)
|
| 52 |
+
new_feats = torch.scatter_reduce(
|
| 53 |
+
torch.zeros(
|
| 54 |
+
unique_codes.shape[0],
|
| 55 |
+
input.feats.shape[1],
|
| 56 |
+
device=input.device,
|
| 57 |
+
dtype=input.dtype,
|
| 58 |
+
),
|
| 59 |
+
dim=0,
|
| 60 |
+
index=inverse[:, None].expand(-1, input.feats.shape[1]),
|
| 61 |
+
src=input.feats,
|
| 62 |
+
reduce="mean",
|
| 63 |
+
)
|
| 64 |
+
new_coords = torch.stack(
|
| 65 |
+
[unique_codes // offsets[0]]
|
| 66 |
+
+ [
|
| 67 |
+
(unique_codes // offsets[axis + 1]) % extents[axis]
|
| 68 |
+
for axis in range(dimensions)
|
| 69 |
+
],
|
| 70 |
+
dim=-1,
|
| 71 |
+
)
|
| 72 |
+
output = SparseTensor(
|
| 73 |
+
new_feats,
|
| 74 |
+
new_coords,
|
| 75 |
+
shape=input.shape,
|
| 76 |
+
scale=tuple(scale * value for scale, value in zip(input.scale, factor)),
|
| 77 |
+
spatial_cache=input._spatial_cache,
|
| 78 |
+
)
|
| 79 |
+
output.register_spatial_cache(f"upsample_{factor}_coords", input.coords)
|
| 80 |
+
output.register_spatial_cache(f"upsample_{factor}_layout", input.layout)
|
| 81 |
+
output.register_spatial_cache(f"upsample_{factor}_idx", inverse)
|
| 82 |
+
return output
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class SparseUpsample(nn.Module):
|
| 86 |
+
"""Restore coordinates from a paired downsample cache."""
|
| 87 |
+
|
| 88 |
+
def __init__(self, factor: int | Sequence[int]):
|
| 89 |
+
"""
|
| 90 |
+
Args:
|
| 91 |
+
factor: Per-axis factor, matching the paired :class:`SparseDownsample`.
|
| 92 |
+
"""
|
| 93 |
+
super().__init__()
|
| 94 |
+
self.factor = tuple(factor) if isinstance(factor, Sequence) else factor
|
| 95 |
+
|
| 96 |
+
def forward(self, input: SparseTensor) -> SparseTensor:
|
| 97 |
+
"""Upsample ``input`` through the recorded inverse mapping.
|
| 98 |
+
|
| 99 |
+
Raises:
|
| 100 |
+
ValueError: ``input`` is not at a scale this factor could have produced,
|
| 101 |
+
or no paired :class:`SparseDownsample` registered a cache for it.
|
| 102 |
+
"""
|
| 103 |
+
dimensions = input.coords.shape[-1] - 1
|
| 104 |
+
factor = _factor_tuple(self.factor, dimensions)
|
| 105 |
+
if any(scale % value for scale, value in zip(input.scale, factor)):
|
| 106 |
+
raise ValueError(
|
| 107 |
+
f"cannot upsample by {factor} from scale {input.scale}; "
|
| 108 |
+
"SparseUpsample must mirror its paired SparseDownsample"
|
| 109 |
+
)
|
| 110 |
+
coords = input.get_spatial_cache(f"upsample_{factor}_coords")
|
| 111 |
+
layout = input.get_spatial_cache(f"upsample_{factor}_layout")
|
| 112 |
+
inverse = input.get_spatial_cache(f"upsample_{factor}_idx")
|
| 113 |
+
if coords is None or layout is None or inverse is None:
|
| 114 |
+
raise ValueError(
|
| 115 |
+
"upsample cache not found; SparseUpsample must follow its "
|
| 116 |
+
"paired SparseDownsample"
|
| 117 |
+
)
|
| 118 |
+
return SparseTensor(
|
| 119 |
+
input.feats[inverse],
|
| 120 |
+
coords,
|
| 121 |
+
shape=input.shape,
|
| 122 |
+
layout=layout,
|
| 123 |
+
scale=tuple(scale // value for scale, value in zip(input.scale, factor)),
|
| 124 |
+
spatial_cache=input._spatial_cache,
|
| 125 |
+
)
|
sparc3d/nn/sparse/transformer/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Transformer blocks over private sparse tensors."""
|
| 2 |
+
|
| 3 |
+
from .modulated import ModulatedSparseTransformerCrossBlock
|
| 4 |
+
|
| 5 |
+
__all__ = ["ModulatedSparseTransformerCrossBlock"]
|
sparc3d/nn/sparse/transformer/blocks.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Feed-forward building block for the sparse transformer."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Microsoft TRELLIS under the MIT License.
|
| 4 |
+
# Modified for SPARC3D to retain the sparse feed-forward path only.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from torch import nn
|
| 9 |
+
|
| 10 |
+
from ..basic import SparseTensor
|
| 11 |
+
from ..linear import SparseLinear
|
| 12 |
+
from ..nonlinearity import SparseGELU
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class SparseFeedForwardNet(nn.Module):
|
| 16 |
+
"""Two-layer sparse MLP with tanh-approximated GELU."""
|
| 17 |
+
|
| 18 |
+
def __init__(self, channels: int, mlp_ratio: float = 4.0):
|
| 19 |
+
"""
|
| 20 |
+
Args:
|
| 21 |
+
channels: Input and output channels.
|
| 22 |
+
mlp_ratio: Hidden width as a multiple of ``channels``.
|
| 23 |
+
"""
|
| 24 |
+
super().__init__()
|
| 25 |
+
hidden_channels = int(channels * mlp_ratio)
|
| 26 |
+
self.mlp = nn.Sequential(
|
| 27 |
+
SparseLinear(channels, hidden_channels),
|
| 28 |
+
SparseGELU(approximate="tanh"),
|
| 29 |
+
SparseLinear(hidden_channels, channels),
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
def forward(self, x: SparseTensor) -> SparseTensor:
|
| 33 |
+
"""Transform sparse features with the MLP."""
|
| 34 |
+
return self.mlp(x)
|
sparc3d/nn/sparse/transformer/modulated.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared-modulation sparse transformer cross-attention block."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Microsoft TRELLIS under the MIT License.
|
| 4 |
+
# Modified for SPARC3D: shared timestep modulation with full attention.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from torch import nn
|
| 10 |
+
|
| 11 |
+
from ...norm import LayerNorm32
|
| 12 |
+
from ..attention import SparseMultiHeadAttention
|
| 13 |
+
from ..basic import SparseTensor
|
| 14 |
+
from .blocks import SparseFeedForwardNet
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ModulatedSparseTransformerCrossBlock(nn.Module):
|
| 18 |
+
"""Sparse self/cross-attention block with shared adaptive normalization."""
|
| 19 |
+
|
| 20 |
+
def __init__(
|
| 21 |
+
self,
|
| 22 |
+
channels: int,
|
| 23 |
+
ctx_channels: int,
|
| 24 |
+
num_heads: int,
|
| 25 |
+
mlp_ratio: float = 4.0,
|
| 26 |
+
):
|
| 27 |
+
super().__init__()
|
| 28 |
+
self.norm1 = LayerNorm32(channels, elementwise_affine=False, eps=1e-6)
|
| 29 |
+
self.norm2 = LayerNorm32(channels, elementwise_affine=True, eps=1e-6)
|
| 30 |
+
self.norm3 = LayerNorm32(channels, elementwise_affine=False, eps=1e-6)
|
| 31 |
+
self.self_attn = SparseMultiHeadAttention(
|
| 32 |
+
channels,
|
| 33 |
+
num_heads,
|
| 34 |
+
attention_type="self",
|
| 35 |
+
)
|
| 36 |
+
self.cross_attn = SparseMultiHeadAttention(
|
| 37 |
+
channels,
|
| 38 |
+
num_heads,
|
| 39 |
+
ctx_channels=ctx_channels,
|
| 40 |
+
attention_type="cross",
|
| 41 |
+
)
|
| 42 |
+
self.mlp = SparseFeedForwardNet(channels, mlp_ratio)
|
| 43 |
+
self.modulation = nn.Parameter(torch.randn(1, 6 * channels) / channels**0.5)
|
| 44 |
+
|
| 45 |
+
def forward(
|
| 46 |
+
self, x: SparseTensor, mod: torch.Tensor, context: torch.Tensor
|
| 47 |
+
) -> SparseTensor:
|
| 48 |
+
"""Transform ``x`` with timestep modulation and image context."""
|
| 49 |
+
(
|
| 50 |
+
shift_msa,
|
| 51 |
+
scale_msa,
|
| 52 |
+
gate_msa,
|
| 53 |
+
shift_mlp,
|
| 54 |
+
scale_mlp,
|
| 55 |
+
gate_mlp,
|
| 56 |
+
) = (self.modulation + mod).chunk(6, dim=1)
|
| 57 |
+
h = x.replace(self.norm1(x.feats))
|
| 58 |
+
h = h * (1 + scale_msa) + shift_msa
|
| 59 |
+
h = self.self_attn(h) * gate_msa
|
| 60 |
+
x = x + h
|
| 61 |
+
h = x.replace(self.norm2(x.feats))
|
| 62 |
+
x = x + self.cross_attn(h, context)
|
| 63 |
+
h = x.replace(self.norm3(x.feats))
|
| 64 |
+
h = h * (1 + scale_mlp) + shift_mlp
|
| 65 |
+
return x + self.mlp(h) * gate_mlp
|
sparc3d/nn/utils.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Parameter precision and initialization helpers for neural network modules."""
|
| 2 |
+
|
| 3 |
+
# Adapted from Microsoft TRELLIS under the MIT License.
|
| 4 |
+
# Modified for SPARC3D to retain the sparse-transformer precision closure.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from torch import nn
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _is_convertible(module: nn.Module) -> bool:
|
| 12 |
+
return isinstance(module, (nn.Conv1d, nn.Conv2d, nn.Conv3d, nn.Linear)) or bool(
|
| 13 |
+
getattr(module, "_sparc3d_convertible", False)
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def convert_module_to_f16(module: nn.Module) -> None:
|
| 18 |
+
"""Convert primitive module parameters to float16."""
|
| 19 |
+
if _is_convertible(module):
|
| 20 |
+
recurse = bool(getattr(module, "_sparc3d_convertible", False))
|
| 21 |
+
for parameter in module.parameters(recurse=recurse):
|
| 22 |
+
parameter.data = parameter.data.half()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def zero_module(module: nn.Module) -> nn.Module:
|
| 26 |
+
"""Zero ``module`` parameters and return the module."""
|
| 27 |
+
for parameter in module.parameters():
|
| 28 |
+
parameter.detach().zero_()
|
| 29 |
+
return module
|
sparc3d/ops/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public GPU operations backed by Triton kernels."""
|
| 2 |
+
|
| 3 |
+
from sparc3d.ops.voxelize import MeshVoxelizer, adaptive_vox
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
"MeshVoxelizer",
|
| 7 |
+
"adaptive_vox",
|
| 8 |
+
]
|
sparc3d/ops/_triton.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lazy loader for the Triton operation backend."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from types import ModuleType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def load_triton_ops() -> ModuleType:
|
| 9 |
+
"""Import and return ``sparc3d.ops.triton``, or raise a clear ``ImportError``.
|
| 10 |
+
|
| 11 |
+
Raises:
|
| 12 |
+
ImportError: Triton is unavailable in the running environment.
|
| 13 |
+
"""
|
| 14 |
+
try:
|
| 15 |
+
from sparc3d.ops import triton as triton_ops
|
| 16 |
+
except ImportError as exc:
|
| 17 |
+
raise ImportError(
|
| 18 |
+
"The SPARC3D operations require Triton. Install the verified Linux "
|
| 19 |
+
"CUDA PyTorch stack, which supplies it."
|
| 20 |
+
) from exc
|
| 21 |
+
return triton_ops
|
sparc3d/ops/flexicubes/LICENSE.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.
|
sparc3d/ops/flexicubes/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Vendored FlexiCubes isosurface extractor.
|
| 3 |
+
|
| 4 |
+
Vendored from `nv-tlabs/FlexiCubes <https://github.com/nv-tlabs/FlexiCubes>`_ and
|
| 5 |
+
distributed under the Apache License 2.0 in ``LICENSE.txt``. The modifications
|
| 6 |
+
this package makes are listed in the header of ``flexicubes.py`` and marked at
|
| 7 |
+
each site.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from sparc3d.ops.flexicubes.flexicubes import FlexiCubes
|
| 11 |
+
|
| 12 |
+
__all__ = ["FlexiCubes"]
|
sparc3d/ops/flexicubes/flexicubes.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
#
|
| 16 |
+
# This file has been modified from nv-tlabs/FlexiCubes. Every change is marked
|
| 17 |
+
# below with a "SPARC3D modification" comment. In summary:
|
| 18 |
+
#
|
| 19 |
+
# * sparse cube indexing -- ``_get_case_id_sparse`` resolves the ambiguous DMC
|
| 20 |
+
# configurations through an explicit ``cube_index_map`` instead of scanning a
|
| 21 |
+
# dense ``res x res x res`` array, so the extractor never materializes the
|
| 22 |
+
# full grid;
|
| 23 |
+
# * lookup tables and index tensors are int32 rather than int64;
|
| 24 |
+
# * ``__call__`` takes a ``dtype`` so the extractor can run in half precision;
|
| 25 |
+
# * the quadrilateral winding fix is out-of-place (``torch.cat``) instead of
|
| 26 |
+
# masked in-place assignment;
|
| 27 |
+
# * removed upstream code that this inference-only package never reaches: the
|
| 28 |
+
# ``construct_voxel_grid`` dense grid builder, ``_tetrahedralize`` and the
|
| 29 |
+
# ``output_tetmesh`` output mode, the ``grad_func`` gradient path, the dense
|
| 30 |
+
# ``_get_case_id`` superseded by ``_get_case_id_sparse``, the unused
|
| 31 |
+
# ``_solve_vd_QEF`` dual-vertex solver, the ``L_dev`` developability
|
| 32 |
+
# regularizer, and the differentiable ``training`` quad-splitting branch;
|
| 33 |
+
# * removed the learned per-cube weights ``alpha``/``beta``/``gamma_f`` and the
|
| 34 |
+
# ``weight_scale`` that normalizes them. They are trainable features of the
|
| 35 |
+
# upstream extractor and no released checkpoint carries them, so the
|
| 36 |
+
# inference path always passed ``None`` and ``_normalize_weights`` always
|
| 37 |
+
# returned tensors of ones. Folding those ones through leaves plain linear
|
| 38 |
+
# interpolation, an unweighted mean over each dual vertex, and the fixed
|
| 39 |
+
# ``quad_split_2`` triangulation of every quadrilateral. The float32
|
| 40 |
+
# accumulation the all-ones weights used to force is kept explicitly, so
|
| 41 |
+
# half-precision inputs still reduce in float32;
|
| 42 |
+
# * ``cube_index_map`` is required rather than defaulting to ``None``, and the
|
| 43 |
+
# unread ``cube_corners`` table is dropped.
|
| 44 |
+
"""FlexiCubes dual marching cubes extractor with sparse cube indexing."""
|
| 45 |
+
|
| 46 |
+
import torch
|
| 47 |
+
|
| 48 |
+
# SPARC3D modification: relative import, since the tables live in this package.
|
| 49 |
+
from .tables import *
|
| 50 |
+
|
| 51 |
+
__all__ = [
|
| 52 |
+
'FlexiCubes'
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class FlexiCubes:
|
| 57 |
+
"""Dual marching cubes extractor over a sparse set of occupied cubes."""
|
| 58 |
+
|
| 59 |
+
# SPARC3D modification: every table below is int32 rather than upstream's int64.
|
| 60 |
+
def __init__(self, device="cuda"):
|
| 61 |
+
"""
|
| 62 |
+
Args:
|
| 63 |
+
device: Device the lookup tables are allocated on. Inputs to
|
| 64 |
+
:meth:`__call__` must live on the same device.
|
| 65 |
+
"""
|
| 66 |
+
self.device = device
|
| 67 |
+
self.dmc_table = torch.tensor(dmc_table, dtype=torch.int32, device=device, requires_grad=False)
|
| 68 |
+
self.num_vd_table = torch.tensor(num_vd_table,
|
| 69 |
+
dtype=torch.int32, device=device, requires_grad=False)
|
| 70 |
+
self.check_table = torch.tensor(
|
| 71 |
+
check_table,
|
| 72 |
+
dtype=torch.int32, device=device, requires_grad=False)
|
| 73 |
+
|
| 74 |
+
self.quad_split_2 = torch.tensor([0, 1, 3, 3, 1, 2], dtype=torch.int32, device=device, requires_grad=False)
|
| 75 |
+
|
| 76 |
+
self.cube_corners_idx = torch.pow(2, torch.arange(8, dtype=torch.int32, requires_grad=False))
|
| 77 |
+
self.cube_edges = torch.tensor([0, 1, 1, 5, 4, 5, 0, 4, 2, 3, 3, 7, 6, 7, 2, 6,
|
| 78 |
+
2, 0, 3, 1, 7, 5, 6, 4], dtype=torch.int32, device=device, requires_grad=False)
|
| 79 |
+
|
| 80 |
+
# SPARC3D modification: renamed arguments, added dtype and cube_index_map, and
|
| 81 |
+
# dropped upstream's output_tetmesh and grad_func. Extracts a triangle mesh from the
|
| 82 |
+
# sparse scalar field; returns (vertices, faces).
|
| 83 |
+
def __call__(self, voxelgrid_vertices, scalar_field, cube_idx, resolution,
|
| 84 |
+
cube_index_map, dtype=None): # dtype so that it can work with half precision.
|
| 85 |
+
"""Extract a triangle mesh from a sparse signed scalar field.
|
| 86 |
+
|
| 87 |
+
Args:
|
| 88 |
+
voxelgrid_vertices: Cube-corner positions ``[V, 3]``.
|
| 89 |
+
scalar_field: Signed value at each corner ``[V]``; the surface is its
|
| 90 |
+
zero level set.
|
| 91 |
+
cube_idx: Corner indices of each occupied cube ``[C, 8]``.
|
| 92 |
+
resolution: Grid resolution the cube indices are expressed in.
|
| 93 |
+
cube_index_map: Integer grid coordinate of each cube ``[C, 3]``, used to
|
| 94 |
+
resolve ambiguous configurations against neighbours. Required: the
|
| 95 |
+
sparse case-id path indexes it directly, so omitting it fails only
|
| 96 |
+
once a mesh happens to contain an ambiguous cube.
|
| 97 |
+
dtype: Float dtype to unify inputs to, e.g. ``torch.float16``. Inferred
|
| 98 |
+
from ``voxelgrid_vertices`` when ``None``, defaulting to float32.
|
| 99 |
+
|
| 100 |
+
Returns:
|
| 101 |
+
``(vertices, faces)``, both empty when no cube straddles the surface.
|
| 102 |
+
"""
|
| 103 |
+
# unify floating tensors to a chosen dtype, so it can work with half, etc:
|
| 104 |
+
if dtype is None:
|
| 105 |
+
if voxelgrid_vertices.dtype in (torch.float16, torch.bfloat16, torch.float32, torch.float64):
|
| 106 |
+
dtype = voxelgrid_vertices.dtype
|
| 107 |
+
else:
|
| 108 |
+
dtype = torch.float32
|
| 109 |
+
|
| 110 |
+
# cast all to dtype, so that it works with half-precision:
|
| 111 |
+
voxelgrid_vertices = voxelgrid_vertices.to(dtype) # Cast the geometry to the chosen dtype
|
| 112 |
+
scalar_field = scalar_field.to(dtype)
|
| 113 |
+
|
| 114 |
+
surf_cubes, occ_fx8 = self._identify_surf_cubes(scalar_field, cube_idx)
|
| 115 |
+
if surf_cubes.sum() == 0:
|
| 116 |
+
return (
|
| 117 |
+
torch.zeros((0, 3), device=self.device, dtype=dtype),
|
| 118 |
+
torch.zeros((0, 3), dtype=torch.int32, device=self.device),
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
case_ids = self._get_case_id_sparse(occ_fx8, surf_cubes, resolution, cube_index_map)
|
| 122 |
+
|
| 123 |
+
surf_edges, idx_map, edge_counts, surf_edges_mask = self._identify_surf_edges(
|
| 124 |
+
scalar_field, cube_idx, surf_cubes
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
vd, vd_idx_map = self._compute_vd(
|
| 128 |
+
voxelgrid_vertices, surf_edges, scalar_field, case_ids, idx_map)
|
| 129 |
+
|
| 130 |
+
return self._triangulate(
|
| 131 |
+
scalar_field, surf_edges, vd, edge_counts, idx_map,
|
| 132 |
+
vd_idx_map, surf_edges_mask)
|
| 133 |
+
|
| 134 |
+
# SPARC3D modification: sparse replacement for _get_case_id. Upstream builds a
|
| 135 |
+
# dense res^3 x 5 array to look up the configuration of adjacent cubes; this
|
| 136 |
+
# version sorts the occupied cube keys and binary-searches them instead, so
|
| 137 |
+
# cost scales with the number of surface cubes rather than with res^3.
|
| 138 |
+
@torch.no_grad()
|
| 139 |
+
def _get_case_id_sparse(self, occ_fx8, surf_cubes, res, cube_index_map):
|
| 140 |
+
"""
|
| 141 |
+
Obtains the ID of topology cases based on cell corner occupancy. This function resolves the
|
| 142 |
+
ambiguity in the Dual Marching Cubes (DMC) configurations as described in Section 1.3 of the
|
| 143 |
+
supplementary material.
|
| 144 |
+
"""
|
| 145 |
+
occ_int = occ_fx8[surf_cubes].to(torch.int32) # Convert boolean to int32
|
| 146 |
+
corners_idx = self.cube_corners_idx.to(self.device).unsqueeze(0)
|
| 147 |
+
case_ids = (occ_int * corners_idx).sum(-1, dtype=torch.int32) # int32 else sum defaults to int64 by default
|
| 148 |
+
problem_config = self.check_table.to(self.device)[case_ids]
|
| 149 |
+
to_check = problem_config[..., 0] == 1
|
| 150 |
+
|
| 151 |
+
problem_config = problem_config[to_check]
|
| 152 |
+
problem_config_index = cube_index_map[surf_cubes][to_check]
|
| 153 |
+
|
| 154 |
+
# SPARC3D modification: an ambiguous cube is resolved against its neighbour, which
|
| 155 |
+
# upstream looks up in a dense res^3 array of per-cube configurations. Here the
|
| 156 |
+
# neighbour is found by binary-searching the sorted keys of the occupied cubes
|
| 157 |
+
# below, so no dense volume is ever allocated.
|
| 158 |
+
vol_idx = cube_index_map # N, 3
|
| 159 |
+
vol_idx_problem = vol_idx[surf_cubes][to_check]
|
| 160 |
+
|
| 161 |
+
vol_idx_problem_adj = vol_idx_problem + problem_config[..., 1:4]
|
| 162 |
+
|
| 163 |
+
within_range = (
|
| 164 |
+
vol_idx_problem_adj[..., 0] >= 0) & (
|
| 165 |
+
vol_idx_problem_adj[..., 0] < res) & (
|
| 166 |
+
vol_idx_problem_adj[..., 1] >= 0) & (
|
| 167 |
+
vol_idx_problem_adj[..., 1] < res) & (
|
| 168 |
+
vol_idx_problem_adj[..., 2] >= 0) & (
|
| 169 |
+
vol_idx_problem_adj[..., 2] < res)
|
| 170 |
+
|
| 171 |
+
vol_idx_problem = vol_idx_problem[within_range]
|
| 172 |
+
vol_idx_problem_adj = vol_idx_problem_adj[within_range]
|
| 173 |
+
problem_config = problem_config[within_range]
|
| 174 |
+
problem_config_index = problem_config_index[within_range]
|
| 175 |
+
|
| 176 |
+
if problem_config.shape[0] == 0:
|
| 177 |
+
return case_ids
|
| 178 |
+
|
| 179 |
+
inverse_keys = (
|
| 180 |
+
(vol_idx_problem_adj[..., 0].long() * res
|
| 181 |
+
+ vol_idx_problem_adj[..., 1].long()) * res
|
| 182 |
+
+ vol_idx_problem_adj[..., 2].long()
|
| 183 |
+
)
|
| 184 |
+
problem_keys = (
|
| 185 |
+
(problem_config_index[..., 0].long() * res
|
| 186 |
+
+ problem_config_index[..., 1].long()) * res
|
| 187 |
+
+ problem_config_index[..., 2].long()
|
| 188 |
+
)
|
| 189 |
+
sorted_keys, sorted_order = torch.sort(problem_keys)
|
| 190 |
+
positions = torch.searchsorted(sorted_keys, inverse_keys)
|
| 191 |
+
bounded_positions = positions.clamp(max=sorted_keys.shape[0] - 1)
|
| 192 |
+
found = (positions < sorted_keys.shape[0]) & (
|
| 193 |
+
sorted_keys[bounded_positions] == inverse_keys
|
| 194 |
+
)
|
| 195 |
+
inverse_indices = torch.full_like(positions, -1, dtype=torch.long)
|
| 196 |
+
inverse_indices[found] = sorted_order[bounded_positions[found]]
|
| 197 |
+
problem_config = torch.cat(
|
| 198 |
+
(
|
| 199 |
+
problem_config,
|
| 200 |
+
torch.zeros(
|
| 201 |
+
(1, 5),
|
| 202 |
+
dtype=problem_config.dtype,
|
| 203 |
+
device=problem_config.device,
|
| 204 |
+
),
|
| 205 |
+
)
|
| 206 |
+
)
|
| 207 |
+
problem_config_adj = problem_config[inverse_indices]
|
| 208 |
+
|
| 209 |
+
# If two cubes with cases C16 and C19 share an ambiguous face, both cases are inverted.
|
| 210 |
+
to_invert = (problem_config_adj[..., 0] == 1)
|
| 211 |
+
idx = torch.arange(case_ids.shape[0], dtype=torch.int32, device=self.device)[to_check][within_range][to_invert]
|
| 212 |
+
case_ids.index_put_((idx,), problem_config[:-1][to_invert][..., -1])
|
| 213 |
+
|
| 214 |
+
return case_ids
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
@torch.no_grad()
|
| 218 |
+
def _identify_surf_edges(self, scalar_field, cube_idx, surf_cubes):
|
| 219 |
+
"""
|
| 220 |
+
Identifies grid edges that intersect with the underlying surface by checking for opposite signs. As each edge
|
| 221 |
+
can be shared by multiple cubes, this function also assigns a unique index to each surface-intersecting edge
|
| 222 |
+
and marks the cube edges with this index.
|
| 223 |
+
"""
|
| 224 |
+
occ_n = scalar_field < 0
|
| 225 |
+
all_edges = cube_idx[surf_cubes][:, self.cube_edges].reshape(-1, 2)
|
| 226 |
+
unique_edges, _idx_map, counts = torch.unique(all_edges, dim=0, return_inverse=True, return_counts=True)
|
| 227 |
+
|
| 228 |
+
unique_edges = unique_edges.int()
|
| 229 |
+
mask_edges = occ_n[unique_edges.reshape(-1)].reshape(-1, 2).sum(-1) == 1
|
| 230 |
+
|
| 231 |
+
surf_edges_mask = mask_edges[_idx_map]
|
| 232 |
+
counts = counts[_idx_map]
|
| 233 |
+
|
| 234 |
+
mapping = torch.ones((unique_edges.shape[0]), dtype=torch.int32, device=cube_idx.device) * -1
|
| 235 |
+
mapping[mask_edges] = torch.arange(mask_edges.sum(), dtype=torch.int32, device=cube_idx.device) # int32 else arrange defaults to int64
|
| 236 |
+
# Shaped as [number of cubes x 12 edges per cube]. This is later used to map a cube edge to the unique index
|
| 237 |
+
# for a surface-intersecting edge. Non-surface-intersecting edges are marked with -1.
|
| 238 |
+
idx_map = mapping[_idx_map]
|
| 239 |
+
surf_edges = unique_edges[mask_edges]
|
| 240 |
+
return surf_edges, idx_map, counts, surf_edges_mask
|
| 241 |
+
|
| 242 |
+
@torch.no_grad()
|
| 243 |
+
def _identify_surf_cubes(self, scalar_field, cube_idx):
|
| 244 |
+
"""
|
| 245 |
+
Identifies grid cubes that intersect with the underlying surface by checking if the signs at
|
| 246 |
+
all corners are not identical.
|
| 247 |
+
"""
|
| 248 |
+
occ_n = scalar_field < 0
|
| 249 |
+
occ_fx8 = occ_n[cube_idx.reshape(-1)].reshape(-1, 8)
|
| 250 |
+
_occ_sum = torch.sum(occ_fx8, -1)
|
| 251 |
+
surf_cubes = (_occ_sum > 0) & (_occ_sum < 8)
|
| 252 |
+
return surf_cubes, occ_fx8
|
| 253 |
+
|
| 254 |
+
def _linear_interp(self, edges_weight, edges_x):
|
| 255 |
+
"""
|
| 256 |
+
Computes the location of zero-crossings on 'edges_x' using linear interpolation with 'edges_weight'.
|
| 257 |
+
"""
|
| 258 |
+
edge_dim = edges_weight.dim() - 2
|
| 259 |
+
assert edges_weight.shape[edge_dim] == 2
|
| 260 |
+
edges_weight = torch.cat([torch.index_select(input=edges_weight, index=torch.tensor(1, device=self.device), dim=edge_dim), -
|
| 261 |
+
torch.index_select(input=edges_weight, index=torch.tensor(0, device=self.device), dim=edge_dim)]
|
| 262 |
+
, edge_dim)
|
| 263 |
+
denominator = edges_weight.sum(edge_dim)
|
| 264 |
+
ue = (edges_x * edges_weight).sum(edge_dim) / denominator
|
| 265 |
+
return ue
|
| 266 |
+
|
| 267 |
+
def _compute_vd(self, voxelgrid_vertices, surf_edges, scalar_field,
|
| 268 |
+
case_ids, idx_map):
|
| 269 |
+
"""
|
| 270 |
+
Computes the location of dual vertices as described in Section 4.2
|
| 271 |
+
"""
|
| 272 |
+
surf_edges_x = torch.index_select(input=voxelgrid_vertices, index=surf_edges.reshape(-1), dim=0).reshape(-1, 2, 3)
|
| 273 |
+
surf_edges_s = torch.index_select(input=scalar_field, index=surf_edges.reshape(-1), dim=0).reshape(-1, 2, 1)
|
| 274 |
+
|
| 275 |
+
idx_map = idx_map.reshape(-1, 12)
|
| 276 |
+
num_vd = torch.index_select(input=self.num_vd_table, index=case_ids, dim=0)
|
| 277 |
+
edge_group, edge_group_to_vd, edge_group_to_cube = [], [], []
|
| 278 |
+
|
| 279 |
+
total_num_vd = 0
|
| 280 |
+
vd_idx_map = torch.zeros((case_ids.shape[0], 12), dtype=torch.int64, device=self.device, requires_grad=False)
|
| 281 |
+
|
| 282 |
+
for num in torch.unique(num_vd):
|
| 283 |
+
cur_cubes = (num_vd == num) # consider cubes with the same numbers of vd emitted (for batching)
|
| 284 |
+
curr_num_vd = cur_cubes.sum() * num
|
| 285 |
+
curr_edge_group = self.dmc_table[case_ids[cur_cubes], :num].reshape(-1, num * 7)
|
| 286 |
+
curr_edge_group_to_vd = torch.arange(
|
| 287 |
+
curr_num_vd, device=self.device).unsqueeze(-1).repeat(1, 7) + total_num_vd
|
| 288 |
+
total_num_vd += curr_num_vd
|
| 289 |
+
curr_edge_group_to_cube = torch.arange(idx_map.shape[0], device=self.device)[
|
| 290 |
+
cur_cubes].unsqueeze(-1).repeat(1, num * 7).reshape_as(curr_edge_group)
|
| 291 |
+
|
| 292 |
+
curr_mask = (curr_edge_group != -1)
|
| 293 |
+
edge_group.append(torch.masked_select(curr_edge_group, curr_mask))
|
| 294 |
+
edge_group_to_vd.append(torch.masked_select(curr_edge_group_to_vd.reshape_as(curr_edge_group), curr_mask))
|
| 295 |
+
edge_group_to_cube.append(torch.masked_select(curr_edge_group_to_cube, curr_mask))
|
| 296 |
+
|
| 297 |
+
edge_group = torch.cat(edge_group)
|
| 298 |
+
edge_group_to_vd = torch.cat(edge_group_to_vd)
|
| 299 |
+
edge_group_to_cube = torch.cat(edge_group_to_cube)
|
| 300 |
+
|
| 301 |
+
# SPARC3D modification: dual vertices accumulate in float32 even when the inputs
|
| 302 |
+
# are half. Averaging up to twelve edge crossings per vertex in half precision
|
| 303 |
+
# loses enough mantissa to move the surface visibly.
|
| 304 |
+
vd = torch.zeros((total_num_vd, 3), device=self.device, dtype=torch.float)
|
| 305 |
+
edges_per_vd = torch.zeros((total_num_vd, 1), device=self.device, dtype=torch.float)
|
| 306 |
+
|
| 307 |
+
idx_group = torch.gather(input=idx_map.reshape(-1), dim=0, index=edge_group_to_cube * 12 + edge_group)
|
| 308 |
+
|
| 309 |
+
x_group = torch.index_select(input=surf_edges_x, index=idx_group.reshape(-1), dim=0).reshape(-1, 2, 3)
|
| 310 |
+
s_group = torch.index_select(input=surf_edges_s, index=idx_group.reshape(-1), dim=0).reshape(-1, 2, 1)
|
| 311 |
+
|
| 312 |
+
ue_group = self._linear_interp(s_group.float(), x_group)
|
| 313 |
+
|
| 314 |
+
edges_per_vd = edges_per_vd.index_add_(
|
| 315 |
+
0, index=edge_group_to_vd, source=torch.ones_like(ue_group[:, :1]))
|
| 316 |
+
vd = vd.index_add_(0, index=edge_group_to_vd, source=ue_group) / edges_per_vd
|
| 317 |
+
|
| 318 |
+
v_idx = torch.arange(vd.shape[0], dtype=torch.int64, device=self.device) # + total_num_vd
|
| 319 |
+
index = edge_group_to_cube * 12 + edge_group
|
| 320 |
+
vd_idx_map = (vd_idx_map.reshape(-1)).scatter(dim=0, index=index, src=v_idx[edge_group_to_vd])
|
| 321 |
+
|
| 322 |
+
return vd, vd_idx_map
|
| 323 |
+
|
| 324 |
+
# SPARC3D modification: every quadrilateral splits along the same fixed diagonal.
|
| 325 |
+
# Choosing per-quad between the two diagonals needs the learned gamma weights, which
|
| 326 |
+
# the released inference weights do not carry.
|
| 327 |
+
def _triangulate(self, scalar_field, surf_edges, vd, edge_counts, idx_map, vd_idx_map, surf_edges_mask):
|
| 328 |
+
"""
|
| 329 |
+
Connects four neighboring dual vertices to form a quadrilateral. Each quadrilateral is then split
|
| 330 |
+
into two triangles along a fixed diagonal, as described in Section 4.3.
|
| 331 |
+
"""
|
| 332 |
+
with torch.no_grad():
|
| 333 |
+
group_mask = (edge_counts == 4) & surf_edges_mask # surface edges shared by 4 cubes.
|
| 334 |
+
group = idx_map.reshape(-1)[group_mask]
|
| 335 |
+
vd_idx = vd_idx_map[group_mask]
|
| 336 |
+
edge_indices, indices = torch.sort(group, stable=True)
|
| 337 |
+
indices = indices.to(torch.int32)
|
| 338 |
+
quad_vd_idx = vd_idx[indices].reshape(-1, 4).to(torch.int32)
|
| 339 |
+
|
| 340 |
+
# Ensure all face directions point towards the positive SDF to maintain consistent winding.
|
| 341 |
+
# SPARC3D modification: rebuilt out-of-place rather than by masked
|
| 342 |
+
# in-place assignment, which reorders the quadrilaterals.
|
| 343 |
+
s_edges = scalar_field[surf_edges[edge_indices.reshape(-1, 4)[:, 0]].reshape(-1)].reshape(-1, 2)
|
| 344 |
+
flip_mask = s_edges[:, 0] > 0
|
| 345 |
+
quad_vd_idx = torch.cat((quad_vd_idx[flip_mask][:, [0, 1, 3, 2]],
|
| 346 |
+
quad_vd_idx[~flip_mask][:, [2, 3, 1, 0]]))
|
| 347 |
+
|
| 348 |
+
faces = quad_vd_idx[:, self.quad_split_2].reshape(-1, 3)
|
| 349 |
+
return vd, faces
|
sparc3d/ops/flexicubes/tables.py
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
#
|
| 16 |
+
# Vendored from nv-tlabs/FlexiCubes. The surviving table data is unmodified.
|
| 17 |
+
# SPARC3D modification: upstream's ``tet_table`` is dropped along with the
|
| 18 |
+
# tetrahedral output mode that was its only consumer.
|
| 19 |
+
"""Dual marching cubes lookup tables used by :class:`FlexiCubes`."""
|
| 20 |
+
|
| 21 |
+
dmc_table = [
|
| 22 |
+
[[-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 23 |
+
[[0, 3, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 24 |
+
[[0, 1, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 25 |
+
[[1, 3, 8, 9, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 26 |
+
[[4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 27 |
+
[[0, 3, 4, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 28 |
+
[[0, 1, 9, -1, -1, -1, -1], [4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 29 |
+
[[1, 3, 4, 7, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 30 |
+
[[4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 31 |
+
[[0, 3, 8, -1, -1, -1, -1], [4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 32 |
+
[[0, 1, 4, 5, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 33 |
+
[[1, 3, 4, 5, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 34 |
+
[[5, 7, 8, 9, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 35 |
+
[[0, 3, 5, 7, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 36 |
+
[[0, 1, 5, 7, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 37 |
+
[[1, 3, 5, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 38 |
+
[[2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 39 |
+
[[0, 2, 8, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 40 |
+
[[0, 1, 9, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 41 |
+
[[1, 2, 8, 9, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 42 |
+
[[4, 7, 8, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 43 |
+
[[0, 2, 4, 7, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 44 |
+
[[0, 1, 9, -1, -1, -1, -1], [4, 7, 8, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 45 |
+
[[1, 2, 4, 7, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 46 |
+
[[4, 5, 9, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 47 |
+
[[0, 2, 8, 11, -1, -1, -1], [4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 48 |
+
[[0, 1, 4, 5, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 49 |
+
[[1, 2, 4, 5, 8, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 50 |
+
[[5, 7, 8, 9, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 51 |
+
[[0, 2, 5, 7, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 52 |
+
[[0, 1, 5, 7, 8, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 53 |
+
[[1, 2, 5, 7, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 54 |
+
[[1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 55 |
+
[[0, 3, 8, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 56 |
+
[[0, 2, 9, 10, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 57 |
+
[[2, 3, 8, 9, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 58 |
+
[[4, 7, 8, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 59 |
+
[[0, 3, 4, 7, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 60 |
+
[[0, 2, 9, 10, -1, -1, -1], [4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 61 |
+
[[2, 3, 4, 7, 9, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 62 |
+
[[4, 5, 9, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 63 |
+
[[0, 3, 8, -1, -1, -1, -1], [4, 5, 9, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 64 |
+
[[0, 2, 4, 5, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 65 |
+
[[2, 3, 4, 5, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 66 |
+
[[5, 7, 8, 9, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 67 |
+
[[0, 3, 5, 7, 9, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 68 |
+
[[0, 2, 5, 7, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 69 |
+
[[2, 3, 5, 7, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 70 |
+
[[1, 3, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 71 |
+
[[0, 1, 8, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 72 |
+
[[0, 3, 9, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 73 |
+
[[8, 9, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 74 |
+
[[4, 7, 8, -1, -1, -1, -1], [1, 3, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 75 |
+
[[0, 1, 4, 7, 10, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 76 |
+
[[0, 3, 9, 10, 11, -1, -1], [4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 77 |
+
[[4, 7, 9, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 78 |
+
[[4, 5, 9, -1, -1, -1, -1], [1, 3, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 79 |
+
[[0, 1, 8, 10, 11, -1, -1], [4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 80 |
+
[[0, 3, 4, 5, 10, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 81 |
+
[[4, 5, 8, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 82 |
+
[[5, 7, 8, 9, -1, -1, -1], [1, 3, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 83 |
+
[[0, 1, 5, 7, 9, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 84 |
+
[[0, 3, 5, 7, 8, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 85 |
+
[[5, 7, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 86 |
+
[[6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 87 |
+
[[0, 3, 8, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 88 |
+
[[0, 1, 9, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 89 |
+
[[1, 3, 8, 9, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 90 |
+
[[4, 6, 8, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 91 |
+
[[0, 3, 4, 6, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 92 |
+
[[0, 1, 9, -1, -1, -1, -1], [4, 6, 8, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 93 |
+
[[1, 3, 4, 6, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 94 |
+
[[4, 5, 9, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 95 |
+
[[0, 3, 8, -1, -1, -1, -1], [4, 5, 9, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 96 |
+
[[0, 1, 4, 5, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 97 |
+
[[1, 3, 4, 5, 8, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 98 |
+
[[5, 6, 8, 9, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 99 |
+
[[0, 3, 5, 6, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 100 |
+
[[0, 1, 5, 6, 8, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 101 |
+
[[1, 3, 5, 6, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 102 |
+
[[2, 3, 6, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 103 |
+
[[0, 2, 6, 7, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 104 |
+
[[0, 1, 9, -1, -1, -1, -1], [2, 3, 6, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 105 |
+
[[1, 2, 6, 7, 8, 9, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 106 |
+
[[2, 3, 4, 6, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 107 |
+
[[0, 2, 4, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 108 |
+
[[0, 1, 9, -1, -1, -1, -1], [2, 3, 4, 6, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 109 |
+
[[1, 2, 4, 6, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 110 |
+
[[4, 5, 9, -1, -1, -1, -1], [2, 3, 6, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 111 |
+
[[0, 2, 6, 7, 8, -1, -1], [4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 112 |
+
[[0, 1, 4, 5, -1, -1, -1], [2, 3, 6, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 113 |
+
[[1, 2, 4, 5, 6, 7, 8], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 114 |
+
[[2, 3, 5, 6, 8, 9, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 115 |
+
[[0, 2, 5, 6, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 116 |
+
[[0, 1, 2, 3, 5, 6, 8], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 117 |
+
[[1, 2, 5, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 118 |
+
[[1, 2, 10, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 119 |
+
[[0, 3, 8, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 120 |
+
[[0, 2, 9, 10, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 121 |
+
[[2, 3, 8, 9, 10, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 122 |
+
[[4, 6, 8, 11, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 123 |
+
[[0, 3, 4, 6, 11, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 124 |
+
[[0, 2, 9, 10, -1, -1, -1], [4, 6, 8, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 125 |
+
[[2, 3, 4, 6, 9, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 126 |
+
[[4, 5, 9, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 127 |
+
[[0, 3, 8, -1, -1, -1, -1], [4, 5, 9, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1]],
|
| 128 |
+
[[0, 2, 4, 5, 10, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 129 |
+
[[2, 3, 4, 5, 8, 10, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 130 |
+
[[5, 6, 8, 9, 11, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 131 |
+
[[0, 3, 5, 6, 9, 11, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 132 |
+
[[0, 2, 5, 6, 8, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 133 |
+
[[2, 3, 5, 6, 10, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 134 |
+
[[1, 3, 6, 7, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 135 |
+
[[0, 1, 6, 7, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 136 |
+
[[0, 3, 6, 7, 9, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 137 |
+
[[6, 7, 8, 9, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 138 |
+
[[1, 3, 4, 6, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 139 |
+
[[0, 1, 4, 6, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 140 |
+
[[0, 3, 4, 6, 8, 9, 10], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 141 |
+
[[4, 6, 9, 10, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 142 |
+
[[4, 5, 9, -1, -1, -1, -1], [1, 3, 6, 7, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 143 |
+
[[0, 1, 6, 7, 8, 10, -1], [4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 144 |
+
[[0, 3, 4, 5, 6, 7, 10], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 145 |
+
[[4, 5, 6, 7, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 146 |
+
[[1, 3, 5, 6, 8, 9, 10], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 147 |
+
[[0, 1, 5, 6, 9, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 148 |
+
[[0, 3, 8, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 149 |
+
[[5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 150 |
+
[[5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 151 |
+
[[0, 3, 8, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 152 |
+
[[0, 1, 9, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 153 |
+
[[1, 3, 8, 9, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 154 |
+
[[4, 7, 8, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 155 |
+
[[0, 3, 4, 7, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 156 |
+
[[0, 1, 9, -1, -1, -1, -1], [4, 7, 8, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 157 |
+
[[1, 3, 4, 7, 9, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 158 |
+
[[4, 6, 9, 10, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 159 |
+
[[0, 3, 8, -1, -1, -1, -1], [4, 6, 9, 10, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 160 |
+
[[0, 1, 4, 6, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 161 |
+
[[1, 3, 4, 6, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 162 |
+
[[6, 7, 8, 9, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 163 |
+
[[0, 3, 6, 7, 9, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 164 |
+
[[0, 1, 6, 7, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 165 |
+
[[1, 3, 6, 7, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 166 |
+
[[2, 3, 11, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 167 |
+
[[0, 2, 8, 11, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 168 |
+
[[0, 1, 9, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 169 |
+
[[1, 2, 8, 9, 11, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 170 |
+
[[4, 7, 8, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 171 |
+
[[0, 2, 4, 7, 11, -1, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 172 |
+
[[0, 1, 9, -1, -1, -1, -1], [4, 7, 8, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [5, 6, 10, -1, -1, -1, -1]],
|
| 173 |
+
[[1, 2, 4, 7, 9, 11, -1], [5, 6, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 174 |
+
[[4, 6, 9, 10, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 175 |
+
[[0, 2, 8, 11, -1, -1, -1], [4, 6, 9, 10, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 176 |
+
[[0, 1, 4, 6, 10, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 177 |
+
[[1, 2, 4, 6, 8, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 178 |
+
[[6, 7, 8, 9, 10, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 179 |
+
[[0, 2, 6, 7, 9, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 180 |
+
[[0, 1, 6, 7, 8, 10, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 181 |
+
[[1, 2, 6, 7, 10, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 182 |
+
[[1, 2, 5, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 183 |
+
[[0, 3, 8, -1, -1, -1, -1], [1, 2, 5, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 184 |
+
[[0, 2, 5, 6, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 185 |
+
[[2, 3, 5, 6, 8, 9, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 186 |
+
[[4, 7, 8, -1, -1, -1, -1], [1, 2, 5, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 187 |
+
[[0, 3, 4, 7, -1, -1, -1], [1, 2, 5, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 188 |
+
[[0, 2, 5, 6, 9, -1, -1], [4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 189 |
+
[[2, 3, 4, 5, 6, 7, 9], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 190 |
+
[[1, 2, 4, 6, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 191 |
+
[[0, 3, 8, -1, -1, -1, -1], [1, 2, 4, 6, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 192 |
+
[[0, 2, 4, 6, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 193 |
+
[[2, 3, 4, 6, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 194 |
+
[[1, 2, 6, 7, 8, 9, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 195 |
+
[[0, 1, 2, 3, 6, 7, 9], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 196 |
+
[[0, 2, 6, 7, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 197 |
+
[[2, 3, 6, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 198 |
+
[[1, 3, 5, 6, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 199 |
+
[[0, 1, 5, 6, 8, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 200 |
+
[[0, 3, 5, 6, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 201 |
+
[[5, 6, 8, 9, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 202 |
+
[[4, 7, 8, -1, -1, -1, -1], [1, 3, 5, 6, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 203 |
+
[[0, 1, 4, 5, 6, 7, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 204 |
+
[[0, 3, 5, 6, 9, 11, -1], [4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 205 |
+
[[4, 5, 6, 7, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 206 |
+
[[1, 3, 4, 6, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 207 |
+
[[0, 1, 4, 6, 8, 9, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 208 |
+
[[0, 3, 4, 6, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 209 |
+
[[4, 6, 8, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 210 |
+
[[1, 3, 6, 7, 8, 9, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 211 |
+
[[0, 1, 9, -1, -1, -1, -1], [6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 212 |
+
[[0, 3, 6, 7, 8, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 213 |
+
[[6, 7, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 214 |
+
[[5, 7, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 215 |
+
[[0, 3, 8, -1, -1, -1, -1], [5, 7, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 216 |
+
[[0, 1, 9, -1, -1, -1, -1], [5, 7, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 217 |
+
[[1, 3, 8, 9, -1, -1, -1], [5, 7, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 218 |
+
[[4, 5, 8, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 219 |
+
[[0, 3, 4, 5, 10, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 220 |
+
[[0, 1, 9, -1, -1, -1, -1], [4, 5, 8, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 221 |
+
[[1, 3, 4, 5, 9, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 222 |
+
[[4, 7, 9, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 223 |
+
[[0, 3, 8, -1, -1, -1, -1], [4, 7, 9, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 224 |
+
[[0, 1, 4, 7, 10, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 225 |
+
[[1, 3, 4, 7, 8, 10, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 226 |
+
[[8, 9, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 227 |
+
[[0, 3, 9, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 228 |
+
[[0, 1, 8, 10, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 229 |
+
[[1, 3, 10, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 230 |
+
[[2, 3, 5, 7, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 231 |
+
[[0, 2, 5, 7, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 232 |
+
[[0, 1, 9, -1, -1, -1, -1], [2, 3, 5, 7, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 233 |
+
[[1, 2, 5, 7, 8, 9, 10], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 234 |
+
[[2, 3, 4, 5, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 235 |
+
[[0, 2, 4, 5, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 236 |
+
[[0, 1, 9, -1, -1, -1, -1], [2, 3, 4, 5, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 237 |
+
[[1, 2, 4, 5, 9, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 238 |
+
[[2, 3, 4, 7, 9, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 239 |
+
[[0, 2, 4, 7, 8, 9, 10], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 240 |
+
[[0, 1, 2, 3, 4, 7, 10], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 241 |
+
[[4, 7, 8, -1, -1, -1, -1], [1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 242 |
+
[[2, 3, 8, 9, 10, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 243 |
+
[[0, 2, 9, 10, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 244 |
+
[[0, 1, 2, 3, 8, 10, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 245 |
+
[[1, 2, 10, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 246 |
+
[[1, 2, 5, 7, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 247 |
+
[[0, 3, 8, -1, -1, -1, -1], [1, 2, 5, 7, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 248 |
+
[[0, 2, 5, 7, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 249 |
+
[[2, 3, 5, 7, 8, 9, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 250 |
+
[[1, 2, 4, 5, 8, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 251 |
+
[[0, 1, 2, 3, 4, 5, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 252 |
+
[[0, 2, 4, 5, 8, 9, 11], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 253 |
+
[[4, 5, 9, -1, -1, -1, -1], [2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 254 |
+
[[1, 2, 4, 7, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 255 |
+
[[0, 3, 8, -1, -1, -1, -1], [1, 2, 4, 7, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 256 |
+
[[0, 2, 4, 7, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 257 |
+
[[2, 3, 4, 7, 8, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 258 |
+
[[1, 2, 8, 9, 11, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 259 |
+
[[0, 1, 2, 3, 9, 11, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 260 |
+
[[0, 2, 8, 11, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 261 |
+
[[2, 3, 11, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 262 |
+
[[1, 3, 5, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 263 |
+
[[0, 1, 5, 7, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 264 |
+
[[0, 3, 5, 7, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 265 |
+
[[5, 7, 8, 9, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 266 |
+
[[1, 3, 4, 5, 8, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 267 |
+
[[0, 1, 4, 5, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 268 |
+
[[0, 3, 4, 5, 8, 9, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 269 |
+
[[4, 5, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 270 |
+
[[1, 3, 4, 7, 9, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 271 |
+
[[0, 1, 4, 7, 8, 9, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 272 |
+
[[0, 3, 4, 7, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 273 |
+
[[4, 7, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 274 |
+
[[1, 3, 8, 9, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 275 |
+
[[0, 1, 9, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 276 |
+
[[0, 3, 8, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]],
|
| 277 |
+
[[-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1, -1]]
|
| 278 |
+
]
|
| 279 |
+
num_vd_table = [0, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 2, 2,
|
| 280 |
+
2, 1, 2, 1, 2, 1, 1, 2, 1, 1, 2, 2, 2, 1, 2, 3, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 2,
|
| 281 |
+
1, 2, 1, 2, 2, 1, 1, 2, 1, 1, 1, 1, 2, 2, 2, 1, 1, 2, 1, 2, 3, 2, 2, 1, 1, 1, 1,
|
| 282 |
+
1, 1, 2, 1, 1, 1, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1, 2, 3, 2, 2, 2, 2, 2, 1, 3, 4, 2,
|
| 283 |
+
2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 2, 1, 1, 2, 2, 2, 2, 2,
|
| 284 |
+
3, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 2, 3, 2, 3, 2, 4, 2, 2, 2, 2, 1, 2, 1, 2, 1, 1,
|
| 285 |
+
2, 1, 1, 2, 2, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1,
|
| 286 |
+
1, 2, 1, 1, 1, 2, 2, 2, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2,
|
| 287 |
+
1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1,
|
| 288 |
+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0]
|
| 289 |
+
check_table = [
|
| 290 |
+
[0, 0, 0, 0, 0],
|
| 291 |
+
[0, 0, 0, 0, 0],
|
| 292 |
+
[0, 0, 0, 0, 0],
|
| 293 |
+
[0, 0, 0, 0, 0],
|
| 294 |
+
[0, 0, 0, 0, 0],
|
| 295 |
+
[0, 0, 0, 0, 0],
|
| 296 |
+
[0, 0, 0, 0, 0],
|
| 297 |
+
[0, 0, 0, 0, 0],
|
| 298 |
+
[0, 0, 0, 0, 0],
|
| 299 |
+
[0, 0, 0, 0, 0],
|
| 300 |
+
[0, 0, 0, 0, 0],
|
| 301 |
+
[0, 0, 0, 0, 0],
|
| 302 |
+
[0, 0, 0, 0, 0],
|
| 303 |
+
[0, 0, 0, 0, 0],
|
| 304 |
+
[0, 0, 0, 0, 0],
|
| 305 |
+
[0, 0, 0, 0, 0],
|
| 306 |
+
[0, 0, 0, 0, 0],
|
| 307 |
+
[0, 0, 0, 0, 0],
|
| 308 |
+
[0, 0, 0, 0, 0],
|
| 309 |
+
[0, 0, 0, 0, 0],
|
| 310 |
+
[0, 0, 0, 0, 0],
|
| 311 |
+
[0, 0, 0, 0, 0],
|
| 312 |
+
[0, 0, 0, 0, 0],
|
| 313 |
+
[0, 0, 0, 0, 0],
|
| 314 |
+
[0, 0, 0, 0, 0],
|
| 315 |
+
[0, 0, 0, 0, 0],
|
| 316 |
+
[0, 0, 0, 0, 0],
|
| 317 |
+
[0, 0, 0, 0, 0],
|
| 318 |
+
[0, 0, 0, 0, 0],
|
| 319 |
+
[0, 0, 0, 0, 0],
|
| 320 |
+
[0, 0, 0, 0, 0],
|
| 321 |
+
[0, 0, 0, 0, 0],
|
| 322 |
+
[0, 0, 0, 0, 0],
|
| 323 |
+
[0, 0, 0, 0, 0],
|
| 324 |
+
[0, 0, 0, 0, 0],
|
| 325 |
+
[0, 0, 0, 0, 0],
|
| 326 |
+
[0, 0, 0, 0, 0],
|
| 327 |
+
[0, 0, 0, 0, 0],
|
| 328 |
+
[0, 0, 0, 0, 0],
|
| 329 |
+
[0, 0, 0, 0, 0],
|
| 330 |
+
[0, 0, 0, 0, 0],
|
| 331 |
+
[0, 0, 0, 0, 0],
|
| 332 |
+
[0, 0, 0, 0, 0],
|
| 333 |
+
[0, 0, 0, 0, 0],
|
| 334 |
+
[0, 0, 0, 0, 0],
|
| 335 |
+
[0, 0, 0, 0, 0],
|
| 336 |
+
[0, 0, 0, 0, 0],
|
| 337 |
+
[0, 0, 0, 0, 0],
|
| 338 |
+
[0, 0, 0, 0, 0],
|
| 339 |
+
[0, 0, 0, 0, 0],
|
| 340 |
+
[0, 0, 0, 0, 0],
|
| 341 |
+
[0, 0, 0, 0, 0],
|
| 342 |
+
[0, 0, 0, 0, 0],
|
| 343 |
+
[0, 0, 0, 0, 0],
|
| 344 |
+
[0, 0, 0, 0, 0],
|
| 345 |
+
[0, 0, 0, 0, 0],
|
| 346 |
+
[0, 0, 0, 0, 0],
|
| 347 |
+
[0, 0, 0, 0, 0],
|
| 348 |
+
[0, 0, 0, 0, 0],
|
| 349 |
+
[0, 0, 0, 0, 0],
|
| 350 |
+
[0, 0, 0, 0, 0],
|
| 351 |
+
[1, 1, 0, 0, 194],
|
| 352 |
+
[1, -1, 0, 0, 193],
|
| 353 |
+
[0, 0, 0, 0, 0],
|
| 354 |
+
[0, 0, 0, 0, 0],
|
| 355 |
+
[0, 0, 0, 0, 0],
|
| 356 |
+
[0, 0, 0, 0, 0],
|
| 357 |
+
[0, 0, 0, 0, 0],
|
| 358 |
+
[0, 0, 0, 0, 0],
|
| 359 |
+
[0, 0, 0, 0, 0],
|
| 360 |
+
[0, 0, 0, 0, 0],
|
| 361 |
+
[0, 0, 0, 0, 0],
|
| 362 |
+
[0, 0, 0, 0, 0],
|
| 363 |
+
[0, 0, 0, 0, 0],
|
| 364 |
+
[0, 0, 0, 0, 0],
|
| 365 |
+
[0, 0, 0, 0, 0],
|
| 366 |
+
[0, 0, 0, 0, 0],
|
| 367 |
+
[0, 0, 0, 0, 0],
|
| 368 |
+
[0, 0, 0, 0, 0],
|
| 369 |
+
[0, 0, 0, 0, 0],
|
| 370 |
+
[0, 0, 0, 0, 0],
|
| 371 |
+
[0, 0, 0, 0, 0],
|
| 372 |
+
[0, 0, 0, 0, 0],
|
| 373 |
+
[0, 0, 0, 0, 0],
|
| 374 |
+
[0, 0, 0, 0, 0],
|
| 375 |
+
[0, 0, 0, 0, 0],
|
| 376 |
+
[0, 0, 0, 0, 0],
|
| 377 |
+
[0, 0, 0, 0, 0],
|
| 378 |
+
[0, 0, 0, 0, 0],
|
| 379 |
+
[0, 0, 0, 0, 0],
|
| 380 |
+
[0, 0, 0, 0, 0],
|
| 381 |
+
[1, 0, 1, 0, 164],
|
| 382 |
+
[0, 0, 0, 0, 0],
|
| 383 |
+
[0, 0, 0, 0, 0],
|
| 384 |
+
[1, 0, -1, 0, 161],
|
| 385 |
+
[0, 0, 0, 0, 0],
|
| 386 |
+
[0, 0, 0, 0, 0],
|
| 387 |
+
[0, 0, 0, 0, 0],
|
| 388 |
+
[0, 0, 0, 0, 0],
|
| 389 |
+
[0, 0, 0, 0, 0],
|
| 390 |
+
[0, 0, 0, 0, 0],
|
| 391 |
+
[0, 0, 0, 0, 0],
|
| 392 |
+
[0, 0, 0, 0, 0],
|
| 393 |
+
[1, 0, 0, 1, 152],
|
| 394 |
+
[0, 0, 0, 0, 0],
|
| 395 |
+
[0, 0, 0, 0, 0],
|
| 396 |
+
[0, 0, 0, 0, 0],
|
| 397 |
+
[0, 0, 0, 0, 0],
|
| 398 |
+
[0, 0, 0, 0, 0],
|
| 399 |
+
[0, 0, 0, 0, 0],
|
| 400 |
+
[1, 0, 0, 1, 145],
|
| 401 |
+
[1, 0, 0, 1, 144],
|
| 402 |
+
[0, 0, 0, 0, 0],
|
| 403 |
+
[0, 0, 0, 0, 0],
|
| 404 |
+
[0, 0, 0, 0, 0],
|
| 405 |
+
[0, 0, 0, 0, 0],
|
| 406 |
+
[0, 0, 0, 0, 0],
|
| 407 |
+
[0, 0, 0, 0, 0],
|
| 408 |
+
[1, 0, 0, -1, 137],
|
| 409 |
+
[0, 0, 0, 0, 0],
|
| 410 |
+
[0, 0, 0, 0, 0],
|
| 411 |
+
[0, 0, 0, 0, 0],
|
| 412 |
+
[1, 0, 1, 0, 133],
|
| 413 |
+
[1, 0, 1, 0, 132],
|
| 414 |
+
[1, 1, 0, 0, 131],
|
| 415 |
+
[1, 1, 0, 0, 130],
|
| 416 |
+
[0, 0, 0, 0, 0],
|
| 417 |
+
[0, 0, 0, 0, 0],
|
| 418 |
+
[0, 0, 0, 0, 0],
|
| 419 |
+
[0, 0, 0, 0, 0],
|
| 420 |
+
[0, 0, 0, 0, 0],
|
| 421 |
+
[0, 0, 0, 0, 0],
|
| 422 |
+
[0, 0, 0, 0, 0],
|
| 423 |
+
[0, 0, 0, 0, 0],
|
| 424 |
+
[0, 0, 0, 0, 0],
|
| 425 |
+
[0, 0, 0, 0, 0],
|
| 426 |
+
[0, 0, 0, 0, 0],
|
| 427 |
+
[0, 0, 0, 0, 0],
|
| 428 |
+
[0, 0, 0, 0, 0],
|
| 429 |
+
[0, 0, 0, 0, 0],
|
| 430 |
+
[0, 0, 0, 0, 0],
|
| 431 |
+
[0, 0, 0, 0, 0],
|
| 432 |
+
[0, 0, 0, 0, 0],
|
| 433 |
+
[0, 0, 0, 0, 0],
|
| 434 |
+
[0, 0, 0, 0, 0],
|
| 435 |
+
[0, 0, 0, 0, 0],
|
| 436 |
+
[0, 0, 0, 0, 0],
|
| 437 |
+
[0, 0, 0, 0, 0],
|
| 438 |
+
[0, 0, 0, 0, 0],
|
| 439 |
+
[0, 0, 0, 0, 0],
|
| 440 |
+
[0, 0, 0, 0, 0],
|
| 441 |
+
[0, 0, 0, 0, 0],
|
| 442 |
+
[0, 0, 0, 0, 0],
|
| 443 |
+
[0, 0, 0, 0, 0],
|
| 444 |
+
[0, 0, 0, 0, 0],
|
| 445 |
+
[1, 0, 0, 1, 100],
|
| 446 |
+
[0, 0, 0, 0, 0],
|
| 447 |
+
[1, 0, 0, 1, 98],
|
| 448 |
+
[0, 0, 0, 0, 0],
|
| 449 |
+
[1, 0, 0, 1, 96],
|
| 450 |
+
[0, 0, 0, 0, 0],
|
| 451 |
+
[0, 0, 0, 0, 0],
|
| 452 |
+
[0, 0, 0, 0, 0],
|
| 453 |
+
[0, 0, 0, 0, 0],
|
| 454 |
+
[0, 0, 0, 0, 0],
|
| 455 |
+
[0, 0, 0, 0, 0],
|
| 456 |
+
[0, 0, 0, 0, 0],
|
| 457 |
+
[1, 0, 1, 0, 88],
|
| 458 |
+
[0, 0, 0, 0, 0],
|
| 459 |
+
[0, 0, 0, 0, 0],
|
| 460 |
+
[0, 0, 0, 0, 0],
|
| 461 |
+
[0, 0, 0, 0, 0],
|
| 462 |
+
[0, 0, 0, 0, 0],
|
| 463 |
+
[1, 0, -1, 0, 82],
|
| 464 |
+
[0, 0, 0, 0, 0],
|
| 465 |
+
[0, 0, 0, 0, 0],
|
| 466 |
+
[0, 0, 0, 0, 0],
|
| 467 |
+
[0, 0, 0, 0, 0],
|
| 468 |
+
[0, 0, 0, 0, 0],
|
| 469 |
+
[0, 0, 0, 0, 0],
|
| 470 |
+
[0, 0, 0, 0, 0],
|
| 471 |
+
[1, 0, 1, 0, 74],
|
| 472 |
+
[0, 0, 0, 0, 0],
|
| 473 |
+
[1, 0, 1, 0, 72],
|
| 474 |
+
[0, 0, 0, 0, 0],
|
| 475 |
+
[1, 0, 0, -1, 70],
|
| 476 |
+
[0, 0, 0, 0, 0],
|
| 477 |
+
[0, 0, 0, 0, 0],
|
| 478 |
+
[1, -1, 0, 0, 67],
|
| 479 |
+
[0, 0, 0, 0, 0],
|
| 480 |
+
[1, -1, 0, 0, 65],
|
| 481 |
+
[0, 0, 0, 0, 0],
|
| 482 |
+
[0, 0, 0, 0, 0],
|
| 483 |
+
[0, 0, 0, 0, 0],
|
| 484 |
+
[0, 0, 0, 0, 0],
|
| 485 |
+
[0, 0, 0, 0, 0],
|
| 486 |
+
[0, 0, 0, 0, 0],
|
| 487 |
+
[0, 0, 0, 0, 0],
|
| 488 |
+
[0, 0, 0, 0, 0],
|
| 489 |
+
[1, 1, 0, 0, 56],
|
| 490 |
+
[0, 0, 0, 0, 0],
|
| 491 |
+
[0, 0, 0, 0, 0],
|
| 492 |
+
[0, 0, 0, 0, 0],
|
| 493 |
+
[1, -1, 0, 0, 52],
|
| 494 |
+
[0, 0, 0, 0, 0],
|
| 495 |
+
[0, 0, 0, 0, 0],
|
| 496 |
+
[0, 0, 0, 0, 0],
|
| 497 |
+
[0, 0, 0, 0, 0],
|
| 498 |
+
[0, 0, 0, 0, 0],
|
| 499 |
+
[0, 0, 0, 0, 0],
|
| 500 |
+
[0, 0, 0, 0, 0],
|
| 501 |
+
[1, 1, 0, 0, 44],
|
| 502 |
+
[0, 0, 0, 0, 0],
|
| 503 |
+
[0, 0, 0, 0, 0],
|
| 504 |
+
[0, 0, 0, 0, 0],
|
| 505 |
+
[1, 1, 0, 0, 40],
|
| 506 |
+
[0, 0, 0, 0, 0],
|
| 507 |
+
[1, 0, 0, -1, 38],
|
| 508 |
+
[1, 0, -1, 0, 37],
|
| 509 |
+
[0, 0, 0, 0, 0],
|
| 510 |
+
[0, 0, 0, 0, 0],
|
| 511 |
+
[0, 0, 0, 0, 0],
|
| 512 |
+
[1, 0, -1, 0, 33],
|
| 513 |
+
[0, 0, 0, 0, 0],
|
| 514 |
+
[0, 0, 0, 0, 0],
|
| 515 |
+
[0, 0, 0, 0, 0],
|
| 516 |
+
[0, 0, 0, 0, 0],
|
| 517 |
+
[1, -1, 0, 0, 28],
|
| 518 |
+
[0, 0, 0, 0, 0],
|
| 519 |
+
[1, 0, -1, 0, 26],
|
| 520 |
+
[1, 0, 0, -1, 25],
|
| 521 |
+
[0, 0, 0, 0, 0],
|
| 522 |
+
[0, 0, 0, 0, 0],
|
| 523 |
+
[0, 0, 0, 0, 0],
|
| 524 |
+
[0, 0, 0, 0, 0],
|
| 525 |
+
[1, -1, 0, 0, 20],
|
| 526 |
+
[0, 0, 0, 0, 0],
|
| 527 |
+
[1, 0, -1, 0, 18],
|
| 528 |
+
[0, 0, 0, 0, 0],
|
| 529 |
+
[0, 0, 0, 0, 0],
|
| 530 |
+
[0, 0, 0, 0, 0],
|
| 531 |
+
[0, 0, 0, 0, 0],
|
| 532 |
+
[0, 0, 0, 0, 0],
|
| 533 |
+
[0, 0, 0, 0, 0],
|
| 534 |
+
[0, 0, 0, 0, 0],
|
| 535 |
+
[0, 0, 0, 0, 0],
|
| 536 |
+
[1, 0, 0, -1, 9],
|
| 537 |
+
[0, 0, 0, 0, 0],
|
| 538 |
+
[0, 0, 0, 0, 0],
|
| 539 |
+
[1, 0, 0, -1, 6],
|
| 540 |
+
[0, 0, 0, 0, 0],
|
| 541 |
+
[0, 0, 0, 0, 0],
|
| 542 |
+
[0, 0, 0, 0, 0],
|
| 543 |
+
[0, 0, 0, 0, 0],
|
| 544 |
+
[0, 0, 0, 0, 0],
|
| 545 |
+
[0, 0, 0, 0, 0]
|
| 546 |
+
]
|
sparc3d/ops/sparse_backend.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sparse-convolution primitives built on spconv."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from math import prod
|
| 6 |
+
from typing import Any, Optional, Tuple
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from torch import nn
|
| 10 |
+
|
| 11 |
+
_SPARSE_CONV_FLAG = "_sparc3d_sparse_conv"
|
| 12 |
+
"""Instance attribute marking a module that consumes and returns a sparse tensor."""
|
| 13 |
+
|
| 14 |
+
_SPCONV_INT32_BYTE_LIMIT = 2**31 - 1
|
| 15 |
+
"""Largest input matrix spconv's implicit-GEMM indexing can address."""
|
| 16 |
+
|
| 17 |
+
_spconv_conv_class: Optional[type] = None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _spconv():
|
| 21 |
+
"""Import spconv's PyTorch namespace, with an actionable error if it is absent."""
|
| 22 |
+
try:
|
| 23 |
+
import spconv.pytorch as spconv
|
| 24 |
+
except ImportError as error:
|
| 25 |
+
raise ImportError(
|
| 26 |
+
"the sparse autoencoder needs spconv; install sparc3d[inference]"
|
| 27 |
+
) from error
|
| 28 |
+
return spconv
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _submanifold_conv_class() -> type:
|
| 32 |
+
"""Build (once) a submanifold convolution that ignores autocast and preserves dtype.
|
| 33 |
+
|
| 34 |
+
The released weights require a convolution that returns whatever precision its input
|
| 35 |
+
features carry and casts its kernel to match, regardless of any enclosing autocast
|
| 36 |
+
context. Stock spconv does the opposite: it downcasts under autocast, and outside one
|
| 37 |
+
it has no kernel for half features at all. The subclass restores the required
|
| 38 |
+
behaviour, at the cost of running the GEMM in the weight's precision.
|
| 39 |
+
"""
|
| 40 |
+
global _spconv_conv_class
|
| 41 |
+
if _spconv_conv_class is not None:
|
| 42 |
+
return _spconv_conv_class
|
| 43 |
+
|
| 44 |
+
spconv = _spconv()
|
| 45 |
+
|
| 46 |
+
class SubMConv3d(spconv.SubMConv3d):
|
| 47 |
+
def _large_forward(self, input: Any) -> Any:
|
| 48 |
+
"""Evaluate a submanifold convolution without cumm's int32 GEMM limit."""
|
| 49 |
+
from spconv.pytorch import ops
|
| 50 |
+
from spconv.pytorch.core import IndiceData
|
| 51 |
+
|
| 52 |
+
indice_key = (
|
| 53 |
+
f"{self.indice_key}_large" if self.indice_key is not None else None
|
| 54 |
+
)
|
| 55 |
+
data = input.find_indice_pair(indice_key)
|
| 56 |
+
if data is None:
|
| 57 |
+
out_indices, pairs, pair_counts = ops.get_indice_pairs(
|
| 58 |
+
input.indices,
|
| 59 |
+
input.batch_size,
|
| 60 |
+
input.spatial_shape,
|
| 61 |
+
spconv.ConvAlgo.Native,
|
| 62 |
+
self.kernel_size,
|
| 63 |
+
self.stride,
|
| 64 |
+
self.padding,
|
| 65 |
+
self.dilation,
|
| 66 |
+
self.output_padding,
|
| 67 |
+
True,
|
| 68 |
+
False,
|
| 69 |
+
)
|
| 70 |
+
data = IndiceData(
|
| 71 |
+
out_indices,
|
| 72 |
+
input.indices,
|
| 73 |
+
pairs,
|
| 74 |
+
pair_counts,
|
| 75 |
+
input.spatial_shape,
|
| 76 |
+
input.spatial_shape,
|
| 77 |
+
is_subm=True,
|
| 78 |
+
algo=spconv.ConvAlgo.Native,
|
| 79 |
+
ksize=self.kernel_size,
|
| 80 |
+
stride=self.stride,
|
| 81 |
+
padding=self.padding,
|
| 82 |
+
dilation=self.dilation,
|
| 83 |
+
)
|
| 84 |
+
else:
|
| 85 |
+
out_indices = data.out_indices
|
| 86 |
+
pairs = data.indice_pairs
|
| 87 |
+
pair_counts = data.indice_pair_num
|
| 88 |
+
|
| 89 |
+
features = input.features
|
| 90 |
+
kernel_volume = prod(self.kernel_size)
|
| 91 |
+
weights = self.weight.reshape(
|
| 92 |
+
self.out_channels, kernel_volume, self.in_channels
|
| 93 |
+
)
|
| 94 |
+
center = kernel_volume // 2
|
| 95 |
+
output = torch.empty(
|
| 96 |
+
(features.shape[0], self.out_channels),
|
| 97 |
+
dtype=features.dtype,
|
| 98 |
+
device=features.device,
|
| 99 |
+
)
|
| 100 |
+
chunk_rows = 250_000
|
| 101 |
+
for start in range(0, len(features), chunk_rows):
|
| 102 |
+
stop = min(start + chunk_rows, len(features))
|
| 103 |
+
output[start:stop] = features[start:stop] @ weights[:, center, :].T
|
| 104 |
+
|
| 105 |
+
def accumulate(
|
| 106 |
+
source: torch.Tensor,
|
| 107 |
+
target: torch.Tensor,
|
| 108 |
+
weight: torch.Tensor,
|
| 109 |
+
) -> None:
|
| 110 |
+
for start in range(0, len(source), chunk_rows):
|
| 111 |
+
stop = min(start + chunk_rows, len(source))
|
| 112 |
+
contribution = features[source[start:stop]] @ weight.T
|
| 113 |
+
output.index_add_(0, target[start:stop], contribution)
|
| 114 |
+
|
| 115 |
+
for offset in range(center):
|
| 116 |
+
count = int(pair_counts[offset])
|
| 117 |
+
source = pairs[0, offset, :count].long()
|
| 118 |
+
target = pairs[1, offset, :count].long()
|
| 119 |
+
accumulate(source, target, weights[:, offset, :])
|
| 120 |
+
accumulate(target, source, weights[:, kernel_volume - 1 - offset, :])
|
| 121 |
+
if self.bias is not None:
|
| 122 |
+
output.add_(self.bias)
|
| 123 |
+
|
| 124 |
+
result = input.replace_feature(output)
|
| 125 |
+
if indice_key is not None:
|
| 126 |
+
result.indice_dict[indice_key] = data
|
| 127 |
+
result.indices = out_indices
|
| 128 |
+
return result
|
| 129 |
+
|
| 130 |
+
def forward(self, input: Any) -> Any:
|
| 131 |
+
feats_dtype = input.features.dtype
|
| 132 |
+
matrix_element_size = max(
|
| 133 |
+
input.features.element_size(), self.weight.element_size()
|
| 134 |
+
)
|
| 135 |
+
input_bytes = input.features.numel() * matrix_element_size
|
| 136 |
+
with torch.autocast("cuda", enabled=False):
|
| 137 |
+
if feats_dtype != self.weight.dtype:
|
| 138 |
+
input = input.replace_feature(input.features.to(self.weight.dtype))
|
| 139 |
+
if input_bytes >= _SPCONV_INT32_BYTE_LIMIT:
|
| 140 |
+
# Both cumm convolution algorithms flatten the feature matrix
|
| 141 |
+
# with int32 indices. Chunked PyTorch GEMMs retain the exact
|
| 142 |
+
# native pair traversal for final 1536-scale decoding.
|
| 143 |
+
out = self._large_forward(input)
|
| 144 |
+
else:
|
| 145 |
+
out = super().forward(input)
|
| 146 |
+
if out.features.dtype != feats_dtype:
|
| 147 |
+
out = out.replace_feature(out.features.to(feats_dtype))
|
| 148 |
+
return out
|
| 149 |
+
|
| 150 |
+
_spconv_conv_class = SubMConv3d
|
| 151 |
+
return _spconv_conv_class
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def Conv3d(
|
| 155 |
+
in_channels: int,
|
| 156 |
+
out_channels: int,
|
| 157 |
+
kernel_size: int = 3,
|
| 158 |
+
stride: int = 1,
|
| 159 |
+
padding: Optional[int] = None,
|
| 160 |
+
bias: bool = True,
|
| 161 |
+
) -> nn.Module:
|
| 162 |
+
"""Build a submanifold sparse convolution.
|
| 163 |
+
|
| 164 |
+
A submanifold convolution keeps the input's occupied coordinates and their row
|
| 165 |
+
order, which is what lets the surrounding code index features positionally.
|
| 166 |
+
|
| 167 |
+
Args:
|
| 168 |
+
in_channels: Input channels.
|
| 169 |
+
out_channels: Output channels.
|
| 170 |
+
kernel_size: Cubic kernel extent; must be odd.
|
| 171 |
+
stride: Only ``1`` is supported; a strided convolution would change the
|
| 172 |
+
occupied set and is not part of this architecture.
|
| 173 |
+
padding: Only the submanifold value ``kernel_size // 2`` (or ``None``, which
|
| 174 |
+
means the same) is supported.
|
| 175 |
+
bias: Whether to add a per-output-channel bias.
|
| 176 |
+
|
| 177 |
+
Returns:
|
| 178 |
+
A module taking and returning an spconv sparse tensor.
|
| 179 |
+
|
| 180 |
+
Raises:
|
| 181 |
+
ValueError: ``kernel_size`` is even, or ``stride``/``padding`` are not the
|
| 182 |
+
submanifold configuration.
|
| 183 |
+
"""
|
| 184 |
+
if kernel_size % 2 == 0:
|
| 185 |
+
raise ValueError(f"kernel_size must be odd, got {kernel_size}")
|
| 186 |
+
if stride != 1:
|
| 187 |
+
raise ValueError(f"only stride 1 is supported, got {stride}")
|
| 188 |
+
if padding is not None and padding != kernel_size // 2:
|
| 189 |
+
raise ValueError(
|
| 190 |
+
f"only submanifold padding {kernel_size // 2} is supported, got {padding}"
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
import os
|
| 194 |
+
|
| 195 |
+
extra = {}
|
| 196 |
+
if os.environ.get("SPCONV_ALGO", "").lower() == "native":
|
| 197 |
+
# HF Space patch: the implicit-GEMM kernels shipped by spconv-cu120 are not
|
| 198 |
+
# built for Blackwell (sm_120); the native gather-GEMM-scatter path is.
|
| 199 |
+
extra["algo"] = _spconv().ConvAlgo.Native
|
| 200 |
+
conv = _submanifold_conv_class()(
|
| 201 |
+
in_channels,
|
| 202 |
+
out_channels,
|
| 203 |
+
kernel_size=kernel_size,
|
| 204 |
+
bias=bias,
|
| 205 |
+
**extra,
|
| 206 |
+
# Submanifold output coordinates always equal the input's, so every
|
| 207 |
+
# convolution over one tensor lineage can share the cached index pairs.
|
| 208 |
+
indice_key=f"subm{kernel_size}",
|
| 209 |
+
)
|
| 210 |
+
setattr(conv, _SPARSE_CONV_FLAG, True)
|
| 211 |
+
return conv
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def is_sparse_conv(module: nn.Module) -> bool:
|
| 215 |
+
"""Return whether ``module`` was built by :func:`Conv3d`."""
|
| 216 |
+
return getattr(module, _SPARSE_CONV_FLAG, False)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def sparse_tensor(
|
| 220 |
+
feats: torch.Tensor,
|
| 221 |
+
coords: torch.Tensor,
|
| 222 |
+
spatial_shape: Optional[Tuple[int, int, int]] = None,
|
| 223 |
+
) -> Any:
|
| 224 |
+
"""Wrap features and coordinates in an spconv sparse tensor.
|
| 225 |
+
|
| 226 |
+
Args:
|
| 227 |
+
feats: Features ``[N, C]``.
|
| 228 |
+
coords: Integer coordinates ``[N, 4]`` laid out as ``[batch, x, y, z]``.
|
| 229 |
+
spatial_shape: Grid extent per axis; derived from ``coords`` when ``None``.
|
| 230 |
+
|
| 231 |
+
Returns:
|
| 232 |
+
The spconv sparse tensor.
|
| 233 |
+
|
| 234 |
+
Raises:
|
| 235 |
+
ValueError: ``coords`` is not ``[N, 4]``, holds negative entries, or exceeds
|
| 236 |
+
``spatial_shape``.
|
| 237 |
+
"""
|
| 238 |
+
if coords.ndim != 2 or coords.shape[1] != 4:
|
| 239 |
+
raise ValueError(f"expected coords [N, 4], got {tuple(coords.shape)}")
|
| 240 |
+
coords = coords.int().contiguous()
|
| 241 |
+
|
| 242 |
+
spconv = _spconv()
|
| 243 |
+
grid = coords[:, 1:]
|
| 244 |
+
if int(grid.min()) < 0:
|
| 245 |
+
raise ValueError("grid coordinates must be non-negative")
|
| 246 |
+
extent = (grid.max(dim=0).values + 1).tolist()
|
| 247 |
+
if spatial_shape is None:
|
| 248 |
+
spatial_shape = tuple(extent)
|
| 249 |
+
elif any(bound < size for bound, size in zip(spatial_shape, extent)):
|
| 250 |
+
raise ValueError(
|
| 251 |
+
f"grid coordinates reach {extent}, outside spatial_shape {spatial_shape}"
|
| 252 |
+
)
|
| 253 |
+
return spconv.SparseConvTensor(
|
| 254 |
+
features=feats,
|
| 255 |
+
indices=coords,
|
| 256 |
+
spatial_shape=list(spatial_shape),
|
| 257 |
+
batch_size=int(coords[:, 0].max()) + 1,
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def get_feats(tensor: Any) -> torch.Tensor:
|
| 262 |
+
"""Return the feature matrix of a sparse tensor."""
|
| 263 |
+
return tensor.features
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def replace_feats(tensor: Any, feats: torch.Tensor) -> Any:
|
| 267 |
+
"""Return a sparse tensor with the same coordinates and new features.
|
| 268 |
+
|
| 269 |
+
spconv rejects assignment to ``features``, so the result may be a different
|
| 270 |
+
object than ``tensor``; callers must use the returned value.
|
| 271 |
+
|
| 272 |
+
Args:
|
| 273 |
+
tensor: Sparse tensor.
|
| 274 |
+
feats: Replacement features ``[N, C]``, one row per coordinate.
|
| 275 |
+
|
| 276 |
+
Returns:
|
| 277 |
+
The updated sparse tensor.
|
| 278 |
+
|
| 279 |
+
Raises:
|
| 280 |
+
ValueError: ``feats`` has a different number of rows than the tensor holds.
|
| 281 |
+
"""
|
| 282 |
+
current = get_feats(tensor)
|
| 283 |
+
if feats.shape[0] != current.shape[0]:
|
| 284 |
+
raise ValueError(
|
| 285 |
+
f"expected {current.shape[0]} rows to match the coordinates, "
|
| 286 |
+
f"got {feats.shape[0]}"
|
| 287 |
+
)
|
| 288 |
+
return tensor.replace_feature(feats)
|
sparc3d/ops/triton/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Triton implementations of SPARC3D surface voxelization."""
|
| 2 |
+
|
| 3 |
+
from sparc3d.ops.triton.voxelize import coarse_voxelize
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
"coarse_voxelize",
|
| 7 |
+
]
|
sparc3d/ops/triton/voxelize.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Triton surface-voxelization kernels and CSR construction."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Tuple
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import triton
|
| 9 |
+
import triton.language as tl
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@triton.jit
|
| 13 |
+
def _axis_overlaps(
|
| 14 |
+
v0x,
|
| 15 |
+
v0y,
|
| 16 |
+
v0z,
|
| 17 |
+
v1x,
|
| 18 |
+
v1y,
|
| 19 |
+
v1z,
|
| 20 |
+
v2x,
|
| 21 |
+
v2y,
|
| 22 |
+
v2z,
|
| 23 |
+
ax,
|
| 24 |
+
ay,
|
| 25 |
+
az,
|
| 26 |
+
):
|
| 27 |
+
p0 = v0x * ax + v0y * ay + v0z * az
|
| 28 |
+
p1 = v1x * ax + v1y * ay + v1z * az
|
| 29 |
+
p2 = v2x * ax + v2y * ay + v2z * az
|
| 30 |
+
lower = tl.minimum(tl.minimum(p0, p1), p2)
|
| 31 |
+
upper = tl.maximum(tl.maximum(p0, p1), p2)
|
| 32 |
+
radius = 0.5 * (tl.abs(ax) + tl.abs(ay) + tl.abs(az))
|
| 33 |
+
return (lower <= radius) & (upper >= -radius)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@triton.jit
|
| 37 |
+
def _triangle_overlaps_voxel(
|
| 38 |
+
a0,
|
| 39 |
+
a1,
|
| 40 |
+
a2,
|
| 41 |
+
b0,
|
| 42 |
+
b1,
|
| 43 |
+
b2,
|
| 44 |
+
c0,
|
| 45 |
+
c1,
|
| 46 |
+
c2,
|
| 47 |
+
x,
|
| 48 |
+
y,
|
| 49 |
+
z,
|
| 50 |
+
):
|
| 51 |
+
center_x = x.to(tl.float32) + 0.5
|
| 52 |
+
center_y = y.to(tl.float32) + 0.5
|
| 53 |
+
center_z = z.to(tl.float32) + 0.5
|
| 54 |
+
v0x, v0y, v0z = a0 - center_x, a1 - center_y, a2 - center_z
|
| 55 |
+
v1x, v1y, v1z = b0 - center_x, b1 - center_y, b2 - center_z
|
| 56 |
+
v2x, v2y, v2z = c0 - center_x, c1 - center_y, c2 - center_z
|
| 57 |
+
|
| 58 |
+
overlaps = (
|
| 59 |
+
(tl.minimum(tl.minimum(v0x, v1x), v2x) <= 0.5)
|
| 60 |
+
& (tl.maximum(tl.maximum(v0x, v1x), v2x) >= -0.5)
|
| 61 |
+
& (tl.minimum(tl.minimum(v0y, v1y), v2y) <= 0.5)
|
| 62 |
+
& (tl.maximum(tl.maximum(v0y, v1y), v2y) >= -0.5)
|
| 63 |
+
& (tl.minimum(tl.minimum(v0z, v1z), v2z) <= 0.5)
|
| 64 |
+
& (tl.maximum(tl.maximum(v0z, v1z), v2z) >= -0.5)
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
e0x, e0y, e0z = v1x - v0x, v1y - v0y, v1z - v0z
|
| 68 |
+
e1x, e1y, e1z = v2x - v1x, v2y - v1y, v2z - v1z
|
| 69 |
+
e2x, e2y, e2z = v0x - v2x, v0y - v2y, v0z - v2z
|
| 70 |
+
|
| 71 |
+
overlaps &= _axis_overlaps(
|
| 72 |
+
v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, 0.0, e0z, -e0y
|
| 73 |
+
)
|
| 74 |
+
overlaps &= _axis_overlaps(
|
| 75 |
+
v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, -e0z, 0.0, e0x
|
| 76 |
+
)
|
| 77 |
+
overlaps &= _axis_overlaps(
|
| 78 |
+
v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, e0y, -e0x, 0.0
|
| 79 |
+
)
|
| 80 |
+
overlaps &= _axis_overlaps(
|
| 81 |
+
v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, 0.0, e1z, -e1y
|
| 82 |
+
)
|
| 83 |
+
overlaps &= _axis_overlaps(
|
| 84 |
+
v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, -e1z, 0.0, e1x
|
| 85 |
+
)
|
| 86 |
+
overlaps &= _axis_overlaps(
|
| 87 |
+
v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, e1y, -e1x, 0.0
|
| 88 |
+
)
|
| 89 |
+
overlaps &= _axis_overlaps(
|
| 90 |
+
v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, 0.0, e2z, -e2y
|
| 91 |
+
)
|
| 92 |
+
overlaps &= _axis_overlaps(
|
| 93 |
+
v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, -e2z, 0.0, e2x
|
| 94 |
+
)
|
| 95 |
+
overlaps &= _axis_overlaps(
|
| 96 |
+
v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z, e2y, -e2x, 0.0
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
nx = e0y * e1z - e0z * e1y
|
| 100 |
+
ny = e0z * e1x - e0x * e1z
|
| 101 |
+
nz = e0x * e1y - e0y * e1x
|
| 102 |
+
plane_offset = -(nx * v0x + ny * v0y + nz * v0z)
|
| 103 |
+
plane_radius = 0.5 * (tl.abs(nx) + tl.abs(ny) + tl.abs(nz))
|
| 104 |
+
return overlaps & (tl.abs(plane_offset) <= plane_radius)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
@triton.jit
|
| 108 |
+
def _load_triangle(vertices_ptr, faces_ptr, face_id):
|
| 109 |
+
index0 = tl.load(faces_ptr + face_id * 3)
|
| 110 |
+
index1 = tl.load(faces_ptr + face_id * 3 + 1)
|
| 111 |
+
index2 = tl.load(faces_ptr + face_id * 3 + 2)
|
| 112 |
+
a0 = tl.load(vertices_ptr + index0 * 3)
|
| 113 |
+
a1 = tl.load(vertices_ptr + index0 * 3 + 1)
|
| 114 |
+
a2 = tl.load(vertices_ptr + index0 * 3 + 2)
|
| 115 |
+
b0 = tl.load(vertices_ptr + index1 * 3)
|
| 116 |
+
b1 = tl.load(vertices_ptr + index1 * 3 + 1)
|
| 117 |
+
b2 = tl.load(vertices_ptr + index1 * 3 + 2)
|
| 118 |
+
c0 = tl.load(vertices_ptr + index2 * 3)
|
| 119 |
+
c1 = tl.load(vertices_ptr + index2 * 3 + 1)
|
| 120 |
+
c2 = tl.load(vertices_ptr + index2 * 3 + 2)
|
| 121 |
+
return a0, a1, a2, b0, b1, b2, c0, c1, c2
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@triton.jit
|
| 125 |
+
def _count_intersections_kernel(
|
| 126 |
+
vertices_ptr,
|
| 127 |
+
faces_ptr,
|
| 128 |
+
counts_ptr,
|
| 129 |
+
resolution: tl.constexpr,
|
| 130 |
+
):
|
| 131 |
+
face_id = tl.program_id(0)
|
| 132 |
+
a0, a1, a2, b0, b1, b2, c0, c1, c2 = _load_triangle(
|
| 133 |
+
vertices_ptr, faces_ptr, face_id
|
| 134 |
+
)
|
| 135 |
+
start_x = tl.maximum(
|
| 136 |
+
tl.floor(tl.minimum(tl.minimum(a0, b0), c0)).to(tl.int32), 0
|
| 137 |
+
)
|
| 138 |
+
start_y = tl.maximum(
|
| 139 |
+
tl.floor(tl.minimum(tl.minimum(a1, b1), c1)).to(tl.int32), 0
|
| 140 |
+
)
|
| 141 |
+
start_z = tl.maximum(
|
| 142 |
+
tl.floor(tl.minimum(tl.minimum(a2, b2), c2)).to(tl.int32), 0
|
| 143 |
+
)
|
| 144 |
+
end_x = tl.minimum(
|
| 145 |
+
tl.floor(tl.maximum(tl.maximum(a0, b0), c0)).to(tl.int32),
|
| 146 |
+
resolution - 1,
|
| 147 |
+
)
|
| 148 |
+
end_y = tl.minimum(
|
| 149 |
+
tl.floor(tl.maximum(tl.maximum(a1, b1), c1)).to(tl.int32),
|
| 150 |
+
resolution - 1,
|
| 151 |
+
)
|
| 152 |
+
end_z = tl.minimum(
|
| 153 |
+
tl.floor(tl.maximum(tl.maximum(a2, b2), c2)).to(tl.int32),
|
| 154 |
+
resolution - 1,
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
count = 0
|
| 158 |
+
for z in tl.range(start_z, end_z + 1):
|
| 159 |
+
for y in tl.range(start_y, end_y + 1):
|
| 160 |
+
for x in tl.range(start_x, end_x + 1):
|
| 161 |
+
count += _triangle_overlaps_voxel(
|
| 162 |
+
a0, a1, a2, b0, b1, b2, c0, c1, c2, x, y, z
|
| 163 |
+
).to(tl.int32)
|
| 164 |
+
tl.store(counts_ptr + face_id, count)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
@triton.jit
|
| 168 |
+
def _emit_intersections_kernel(
|
| 169 |
+
vertices_ptr,
|
| 170 |
+
faces_ptr,
|
| 171 |
+
offsets_ptr,
|
| 172 |
+
keys_ptr,
|
| 173 |
+
face_ids_ptr,
|
| 174 |
+
resolution: tl.constexpr,
|
| 175 |
+
):
|
| 176 |
+
face_id = tl.program_id(0)
|
| 177 |
+
a0, a1, a2, b0, b1, b2, c0, c1, c2 = _load_triangle(
|
| 178 |
+
vertices_ptr, faces_ptr, face_id
|
| 179 |
+
)
|
| 180 |
+
start_x = tl.maximum(
|
| 181 |
+
tl.floor(tl.minimum(tl.minimum(a0, b0), c0)).to(tl.int32), 0
|
| 182 |
+
)
|
| 183 |
+
start_y = tl.maximum(
|
| 184 |
+
tl.floor(tl.minimum(tl.minimum(a1, b1), c1)).to(tl.int32), 0
|
| 185 |
+
)
|
| 186 |
+
start_z = tl.maximum(
|
| 187 |
+
tl.floor(tl.minimum(tl.minimum(a2, b2), c2)).to(tl.int32), 0
|
| 188 |
+
)
|
| 189 |
+
end_x = tl.minimum(
|
| 190 |
+
tl.floor(tl.maximum(tl.maximum(a0, b0), c0)).to(tl.int32),
|
| 191 |
+
resolution - 1,
|
| 192 |
+
)
|
| 193 |
+
end_y = tl.minimum(
|
| 194 |
+
tl.floor(tl.maximum(tl.maximum(a1, b1), c1)).to(tl.int32),
|
| 195 |
+
resolution - 1,
|
| 196 |
+
)
|
| 197 |
+
end_z = tl.minimum(
|
| 198 |
+
tl.floor(tl.maximum(tl.maximum(a2, b2), c2)).to(tl.int32),
|
| 199 |
+
resolution - 1,
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
output = tl.load(offsets_ptr + face_id)
|
| 203 |
+
for z in tl.range(start_z, end_z + 1):
|
| 204 |
+
for y in tl.range(start_y, end_y + 1):
|
| 205 |
+
for x in tl.range(start_x, end_x + 1):
|
| 206 |
+
hit = _triangle_overlaps_voxel(
|
| 207 |
+
a0, a1, a2, b0, b1, b2, c0, c1, c2, x, y, z
|
| 208 |
+
)
|
| 209 |
+
key = (
|
| 210 |
+
(z.to(tl.int64) << 22)
|
| 211 |
+
| (y.to(tl.int64) << 11)
|
| 212 |
+
| x.to(tl.int64)
|
| 213 |
+
)
|
| 214 |
+
tl.store(keys_ptr + output, key, mask=hit)
|
| 215 |
+
tl.store(face_ids_ptr + output, face_id, mask=hit)
|
| 216 |
+
output += hit.to(tl.int32)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def coarse_voxelize(
|
| 220 |
+
vertices: torch.Tensor,
|
| 221 |
+
faces: torch.Tensor,
|
| 222 |
+
resolution: int,
|
| 223 |
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 224 |
+
"""Voxelize triangles and return voxel-to-face CSR tensors.
|
| 225 |
+
|
| 226 |
+
Args:
|
| 227 |
+
vertices: CUDA float coordinates with shape ``[V, 3]`` in voxel space.
|
| 228 |
+
faces: CUDA triangle indices with shape ``[F, 3]``.
|
| 229 |
+
resolution: Side length of the voxel grid.
|
| 230 |
+
|
| 231 |
+
Returns:
|
| 232 |
+
Encoded coordinates, grouped face IDs, CSR starts, CSR counts, and
|
| 233 |
+
per-face voxel counts.
|
| 234 |
+
|
| 235 |
+
Raises:
|
| 236 |
+
ValueError: Inputs do not satisfy the low-level voxelizer contract.
|
| 237 |
+
"""
|
| 238 |
+
if not vertices.is_cuda or not faces.is_cuda:
|
| 239 |
+
raise ValueError("vertices and faces must be CUDA tensors")
|
| 240 |
+
if vertices.device != faces.device:
|
| 241 |
+
raise ValueError("vertices and faces must be on the same device")
|
| 242 |
+
if vertices.ndim != 2 or vertices.shape[1] != 3:
|
| 243 |
+
raise ValueError("vertices must have shape [num_vertices, 3]")
|
| 244 |
+
if faces.ndim != 2 or faces.shape[1] != 3:
|
| 245 |
+
raise ValueError("faces must have shape [num_faces, 3]")
|
| 246 |
+
if resolution < 1 or resolution > 2048:
|
| 247 |
+
raise ValueError(f"resolution must be in [1, 2048], got {resolution}")
|
| 248 |
+
|
| 249 |
+
vertices = vertices.to(torch.float32).contiguous()
|
| 250 |
+
faces = faces.to(torch.int32).contiguous()
|
| 251 |
+
num_faces = faces.shape[0]
|
| 252 |
+
counts = torch.zeros(num_faces, dtype=torch.int32, device=faces.device)
|
| 253 |
+
if num_faces == 0:
|
| 254 |
+
empty_u64 = torch.empty(0, dtype=torch.uint64, device=faces.device)
|
| 255 |
+
empty_u32 = torch.empty(0, dtype=torch.uint32, device=faces.device)
|
| 256 |
+
return empty_u64, empty_u32, counts, counts, counts
|
| 257 |
+
|
| 258 |
+
_count_intersections_kernel[(num_faces,)](
|
| 259 |
+
vertices, faces, counts, resolution, num_warps=1
|
| 260 |
+
)
|
| 261 |
+
inclusive = torch.cumsum(counts, dim=0, dtype=torch.int32)
|
| 262 |
+
offsets = torch.cat(
|
| 263 |
+
[torch.zeros(1, dtype=torch.int32, device=faces.device), inclusive[:-1]]
|
| 264 |
+
)
|
| 265 |
+
total = int(inclusive[-1].item())
|
| 266 |
+
if total == 0:
|
| 267 |
+
empty_u64 = torch.empty(0, dtype=torch.uint64, device=faces.device)
|
| 268 |
+
empty_u32 = torch.empty(0, dtype=torch.uint32, device=faces.device)
|
| 269 |
+
empty_i32 = torch.empty(0, dtype=torch.int32, device=faces.device)
|
| 270 |
+
return empty_u64, empty_u32, empty_i32, empty_i32, counts
|
| 271 |
+
|
| 272 |
+
keys = torch.empty(total, dtype=torch.int64, device=faces.device)
|
| 273 |
+
face_ids = torch.empty(total, dtype=torch.int32, device=faces.device)
|
| 274 |
+
_emit_intersections_kernel[(num_faces,)](
|
| 275 |
+
vertices,
|
| 276 |
+
faces,
|
| 277 |
+
offsets,
|
| 278 |
+
keys,
|
| 279 |
+
face_ids,
|
| 280 |
+
resolution,
|
| 281 |
+
num_warps=1,
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
sorted_keys, order = torch.sort(keys, stable=True)
|
| 285 |
+
grouped_faces = face_ids[order]
|
| 286 |
+
unique_keys, unique_counts = torch.unique_consecutive(
|
| 287 |
+
sorted_keys, return_counts=True
|
| 288 |
+
)
|
| 289 |
+
unique_counts = unique_counts.to(torch.int32)
|
| 290 |
+
unique_starts = torch.cat(
|
| 291 |
+
[
|
| 292 |
+
torch.zeros(1, dtype=torch.int32, device=faces.device),
|
| 293 |
+
torch.cumsum(unique_counts, dim=0, dtype=torch.int32)[:-1],
|
| 294 |
+
]
|
| 295 |
+
)
|
| 296 |
+
return (
|
| 297 |
+
unique_keys.to(torch.uint64),
|
| 298 |
+
grouped_faces.to(torch.uint32),
|
| 299 |
+
unique_starts,
|
| 300 |
+
unique_counts,
|
| 301 |
+
counts,
|
| 302 |
+
)
|