Spaces:
Running on Zero
Running on Zero
SpaceFlow demo
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- LICENSE +201 -0
- README.md +21 -7
- THIRD_PARTY_NOTICES.md +27 -0
- app.py +457 -0
- config/default.yaml +41 -0
- config/trellis_pipeline/pipeline.json +67 -0
- examples/chair.npz +3 -0
- examples/hot_air_balloon.npz +3 -0
- examples/sailboat.npz +3 -0
- examples/toy_elephant.npz +3 -0
- examples/toy_rocket.npz +3 -0
- examples/windmill.npz +3 -0
- lib/opt/self_similarity.py +1072 -0
- lib/util/checkpoint_compat.py +18 -0
- lib/util/common.py +101 -0
- lib/util/generation.py +80 -0
- lib/util/model_revisions.py +16 -0
- lib/util/partfield.py +108 -0
- lib/util/pipeline_compat.py +10 -0
- lib/util/pointcloud.py +24 -0
- lib/util/render.py +133 -0
- lib/util/visualization.py +93 -0
- packages.txt +4 -0
- requirements.txt +38 -0
- requirements/model-revisions.json +23 -0
- run_local_tau.py +773 -0
- third_party/PartField/LICENSE +36 -0
- third_party/PartField/config.yaml +88 -0
- third_party/PartField/partfield/config/__init__.py +26 -0
- third_party/PartField/partfield/config/defaults.py +92 -0
- third_party/PartField/partfield/dataloader.py +78 -0
- third_party/PartField/partfield/model/PVCNN/conv_pointnet.py +251 -0
- third_party/PartField/partfield/model/PVCNN/dnnlib_util.py +1074 -0
- third_party/PartField/partfield/model/PVCNN/encoder_pc.py +243 -0
- third_party/PartField/partfield/model/PVCNN/pc_encoder.py +90 -0
- third_party/PartField/partfield/model/PVCNN/pv_module/__init__.py +2 -0
- third_party/PartField/partfield/model/PVCNN/pv_module/ball_query.py +34 -0
- third_party/PartField/partfield/model/PVCNN/pv_module/frustum.py +141 -0
- third_party/PartField/partfield/model/PVCNN/pv_module/functional/__init__.py +1 -0
- third_party/PartField/partfield/model/PVCNN/pv_module/functional/devoxelization.py +12 -0
- third_party/PartField/partfield/model/PVCNN/pv_module/loss.py +10 -0
- third_party/PartField/partfield/model/PVCNN/pv_module/pointnet.py +113 -0
- third_party/PartField/partfield/model/PVCNN/pv_module/pvconv.py +38 -0
- third_party/PartField/partfield/model/PVCNN/pv_module/shared_mlp.py +35 -0
- third_party/PartField/partfield/model/PVCNN/pv_module/voxelization.py +50 -0
- third_party/PartField/partfield/model/PVCNN/unet_3daware.py +427 -0
- third_party/PartField/partfield/model/UNet/buildingblocks.py +546 -0
- third_party/PartField/partfield/model/UNet/model.py +170 -0
- third_party/PartField/partfield/model/model_utils.py +54 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ 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 |
+
wheels/diff_gaussian_rasterization-0.0.0-cp310-cp310-linux_x86_64.whl filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
README.md
CHANGED
|
@@ -1,13 +1,27 @@
|
|
| 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: SpaceFlow
|
| 3 |
+
emoji: 🧊
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.30.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: Locally controllable 3D generation from primitives
|
| 10 |
+
python_version: "3.10"
|
| 11 |
+
startup_duration_timeout: 1h
|
| 12 |
+
license: apache-2.0
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# SpaceFlow: Locally Controllable 3D Generation
|
| 16 |
+
|
| 17 |
+
Gradio demo of [SpaceFlow](https://github.com/SpaceFlow3D/spaceflow) ([project page](https://spaceflow3d.github.io/), [paper](https://arxiv.org/abs/2610.12399)).
|
| 18 |
+
It runs the authors' `run_local_tau.py` text pipeline: SpaceControl sparse-structure generation with
|
| 19 |
+
local (high/low) geometry control from superquadric primitives, PartField part features, and
|
| 20 |
+
self-similarity guided TRELLIS appearance flow with per-primitive local texture prompts.
|
| 21 |
+
|
| 22 |
+
Models: `microsoft/TRELLIS-text-xlarge`, `microsoft/TRELLIS-image-large` (VAE / decoders),
|
| 23 |
+
`openai/clip-vit-large-patch14`, `mikaelaangel/partfield-ckpt`.
|
| 24 |
+
|
| 25 |
+
Example primitive bundles come from the SpaceFlow repository (Apache-2.0).
|
| 26 |
+
SpaceFlow is Apache-2.0; **PartField is restricted to non-commercial research and educational use**
|
| 27 |
+
(see `THIRD_PARTY_NOTICES.md`).
|
THIRD_PARTY_NOTICES.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Attribution and third-party notices
|
| 2 |
+
|
| 3 |
+
The root `LICENSE` contains SpaceFlow's Apache-2.0 license. It does not replace
|
| 4 |
+
licenses or notices attached to upstream source, checkpoints, or dependencies.
|
| 5 |
+
|
| 6 |
+
| Component | Source and notice |
|
| 7 |
+
| --- | --- |
|
| 8 |
+
| GuideFlow3D | [Upstream project](https://github.com/GradientSpaces/GuideFlow3D); SpaceFlow builds on its generation and similarity-guidance pipeline. |
|
| 9 |
+
| TRELLIS | [Upstream](https://github.com/microsoft/TRELLIS); retained [MIT license](third_party/TRELLIS/LICENSE). |
|
| 10 |
+
| PartField | [Upstream](https://github.com/nv-tlabs/PartField); retained [NVIDIA license](third_party/PartField/LICENSE). |
|
| 11 |
+
| DINOv2 | [Upstream](https://github.com/facebookresearch/dinov2); [Apache-2.0 license](https://github.com/facebookresearch/dinov2/blob/7764ea0f912e53c92e82eb78a2a1631e92725fc8/LICENSE). Image staging records its source commit and checkpoint hash. |
|
| 12 |
+
| rembg / U2Net | [rembg upstream](https://github.com/danielgatis/rembg); background-removal dependency and downloaded U2Net model. Their own upstream notices apply. |
|
| 13 |
+
| Native extensions | Sources/revisions are recorded in `requirements/native-revisions.json`; installed dependencies retain their own notices. |
|
| 14 |
+
| React, Three.js, and editor dependencies | Versions and package integrity records are in `sq_ui/app/package-lock.json`; dependencies retain their own licenses. |
|
| 15 |
+
|
| 16 |
+
## PartField use limitation
|
| 17 |
+
|
| 18 |
+
PartField's license limits use to **non-commercial research and educational
|
| 19 |
+
purposes** (with the exception specified for NVIDIA and its affiliates).
|
| 20 |
+
Keep its license and notices with any redistribution of that component, and
|
| 21 |
+
review the [original terms](third_party/PartField/LICENSE) for the complete rules.
|
| 22 |
+
The same license constraints apply when using the PartField checkpoint.
|
| 23 |
+
|
| 24 |
+
Model revision records and checkpoint hashes identify the downloaded artifacts;
|
| 25 |
+
they do not grant additional rights. Download models from their recorded upstream
|
| 26 |
+
repositories and retain the associated notices. Generated research media and
|
| 27 |
+
experiment outputs are separate artifacts from the thin source release.
|
app.py
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import glob
|
| 4 |
+
import importlib.util
|
| 5 |
+
import subprocess
|
| 6 |
+
|
| 7 |
+
APP_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 8 |
+
|
| 9 |
+
# TRELLIS runtime configuration (matches the reference setup: spconv + flash-attn).
|
| 10 |
+
os.environ.setdefault("SPCONV_ALGO", "native")
|
| 11 |
+
os.environ.setdefault("SPARSE_BACKEND", "spconv")
|
| 12 |
+
os.environ.setdefault("ATTN_BACKEND", "flash_attn")
|
| 13 |
+
os.environ.setdefault("SPARSE_ATTN_BACKEND", "flash_attn")
|
| 14 |
+
os.environ["SPACEFLOW_KEEP_ON_GPU"] = "1" # all weights stay resident on the ZeroGPU device
|
| 15 |
+
|
| 16 |
+
# TRELLIS needs the mip-splatting fork of diff-gaussian-rasterization (kernel_size / subpixel_offset API).
|
| 17 |
+
# A prebuilt sm_120 wheel ships with the Space; install it before torch is imported.
|
| 18 |
+
if importlib.util.find_spec("diff_gaussian_rasterization") is None:
|
| 19 |
+
wheel = glob.glob(os.path.join(APP_DIR, "wheels", "diff_gaussian_rasterization-*.whl"))[0]
|
| 20 |
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "--no-deps", wheel])
|
| 21 |
+
|
| 22 |
+
import spaces # noqa: E402 (must come before torch)
|
| 23 |
+
|
| 24 |
+
import copy # noqa: E402
|
| 25 |
+
import json # noqa: E402
|
| 26 |
+
import random # noqa: E402
|
| 27 |
+
import shutil # noqa: E402
|
| 28 |
+
import tempfile # noqa: E402
|
| 29 |
+
import time # noqa: E402
|
| 30 |
+
|
| 31 |
+
import gradio as gr # noqa: E402
|
| 32 |
+
import numpy as np # noqa: E402
|
| 33 |
+
import torch # noqa: E402
|
| 34 |
+
import trimesh # noqa: E402
|
| 35 |
+
from huggingface_hub import hf_hub_download # noqa: E402
|
| 36 |
+
from omegaconf import OmegaConf # noqa: E402
|
| 37 |
+
|
| 38 |
+
sys.path.insert(0, APP_DIR)
|
| 39 |
+
os.chdir(APP_DIR)
|
| 40 |
+
os.makedirs("debug", exist_ok=True)
|
| 41 |
+
|
| 42 |
+
import run_local_tau as rlt # noqa: E402
|
| 43 |
+
from third_party.TRELLIS.trellis.pipelines import TrellisTextTo3DPipeline # noqa: E402
|
| 44 |
+
from third_party.PartField.partfield.model_trainer_pvcnn_only_demo import Model as PartFieldModel # noqa: E402
|
| 45 |
+
from third_party.PartField.partfield.dataloader import Demo_Dataset # noqa: E402
|
| 46 |
+
from lib.opt import self_similarity # noqa: E402
|
| 47 |
+
from lib.util import pointcloud # noqa: E402
|
| 48 |
+
from lib.util.checkpoint_compat import partfield_checkpoint_scope # noqa: E402
|
| 49 |
+
|
| 50 |
+
CFG = OmegaConf.load("config/default.yaml")
|
| 51 |
+
LOW_CONTROL_BBOX_MARGIN = 0.07 # sq_ui default (DEFAULT_LOW_CONTROL_BBOX_MARGIN)
|
| 52 |
+
|
| 53 |
+
# ---------------------------------------------------------------------------
|
| 54 |
+
# Models (loaded once at module scope, placed on the GPU)
|
| 55 |
+
# ---------------------------------------------------------------------------
|
| 56 |
+
# TRELLIS' text conditioner encodes the empty prompt at init; defer that forward
|
| 57 |
+
# pass to the GPU worker instead of running it at import time.
|
| 58 |
+
_orig_encode_text = TrellisTextTo3DPipeline.encode_text
|
| 59 |
+
TrellisTextTo3DPipeline.encode_text = lambda self, text: None
|
| 60 |
+
PIPE = TrellisTextTo3DPipeline.from_pretrained("config/trellis_pipeline")
|
| 61 |
+
TrellisTextTo3DPipeline.encode_text = _orig_encode_text
|
| 62 |
+
PIPE.cuda()
|
| 63 |
+
PIPE.text_cond_model["model"].to("cuda")
|
| 64 |
+
|
| 65 |
+
PF_CFG = OmegaConf.load("third_party/PartField/config.yaml")
|
| 66 |
+
PARTFIELD = PartFieldModel(PF_CFG, None)
|
| 67 |
+
_pf_ckpt = hf_hub_download(
|
| 68 |
+
"mikaelaangel/partfield-ckpt", "model_objaverse.ckpt",
|
| 69 |
+
revision="90b9b1e08b6a12fdcb6ee26b4854a26235e1765f",
|
| 70 |
+
)
|
| 71 |
+
with partfield_checkpoint_scope():
|
| 72 |
+
_pf_state = torch.load(_pf_ckpt, map_location="cpu", weights_only=True)["state_dict"]
|
| 73 |
+
_missing, _unexpected = PARTFIELD.load_state_dict(_pf_state, strict=False)
|
| 74 |
+
print(f"[PartField] missing keys: {len(_missing)}, unexpected keys: {len(_unexpected)}")
|
| 75 |
+
del _pf_state
|
| 76 |
+
PARTFIELD.eval().to("cuda")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# ---------------------------------------------------------------------------
|
| 80 |
+
# Helpers
|
| 81 |
+
# ---------------------------------------------------------------------------
|
| 82 |
+
def _seed_all(seed: int):
|
| 83 |
+
random.seed(seed)
|
| 84 |
+
np.random.seed(seed)
|
| 85 |
+
torch.manual_seed(seed)
|
| 86 |
+
if torch.cuda.is_available():
|
| 87 |
+
torch.cuda.manual_seed_all(seed)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _ensure_null_cond():
|
| 91 |
+
if PIPE.text_cond_model.get("null_cond") is None:
|
| 92 |
+
PIPE.text_cond_model["null_cond"] = PIPE.encode_text([""])
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _read_metadata(npz_path):
|
| 96 |
+
with np.load(npz_path, allow_pickle=False) as d:
|
| 97 |
+
if "spaceflow_metadata.json" in d.files:
|
| 98 |
+
try:
|
| 99 |
+
return json.loads(bytes(d["spaceflow_metadata.json"]).decode("utf-8"))
|
| 100 |
+
except Exception:
|
| 101 |
+
return {}
|
| 102 |
+
return {}
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _load_arrays(npz_path):
|
| 106 |
+
with np.load(npz_path, allow_pickle=False) as d:
|
| 107 |
+
arrays = {k: np.asarray(d[k]) for k in d.files if k != "spaceflow_metadata.json"}
|
| 108 |
+
n = arrays["scales"].shape[0]
|
| 109 |
+
arrays.setdefault("control_levels", np.ones((n,)))
|
| 110 |
+
return arrays
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _subset(arrays, idx):
|
| 114 |
+
out = {}
|
| 115 |
+
for k in ["scales", "shapes", "translations", "rotations", "control_levels", "tapering", "bending"]:
|
| 116 |
+
if k in arrays:
|
| 117 |
+
out[k] = arrays[k][idx]
|
| 118 |
+
return out
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _low_control_bbox(arrays, low_idx, margin=LOW_CONTROL_BBOX_MARGIN):
|
| 122 |
+
"""Port of sq_ui buildLowControlBoundingBoxPrimitive (padded AABB of the low-control primitives)."""
|
| 123 |
+
verts = []
|
| 124 |
+
for i in low_idx:
|
| 125 |
+
tap = arrays["tapering"][i] if "tapering" in arrays else None
|
| 126 |
+
bend = arrays["bending"][i] if "bending" in arrays else None
|
| 127 |
+
v, _ = rlt.add_superquadric_compact_rot_mat(
|
| 128 |
+
arrays["scales"][i], arrays["shapes"][i], arrays["translations"][i], arrays["rotations"][i],
|
| 129 |
+
tap, bend, resolution=32)
|
| 130 |
+
verts.append(v)
|
| 131 |
+
v = np.concatenate(verts, 0)
|
| 132 |
+
mn, mx = v.min(0), v.max(0)
|
| 133 |
+
pad = np.linalg.norm(mx - mn) * margin
|
| 134 |
+
mn, mx = mn - pad, mx + pad
|
| 135 |
+
center = (mn + mx) / 2
|
| 136 |
+
half = np.maximum((mx - mn) / 2, 1e-4)
|
| 137 |
+
rot = np.array([[1.0, 0.0, 0.0], [0.0, 6.123234e-17, -1.0], [0.0, 1.0, 6.123234e-17]])
|
| 138 |
+
return {
|
| 139 |
+
"scales": np.array([[half[0], half[2], half[1]]]),
|
| 140 |
+
"shapes": np.array([[0.05, 0.05]]),
|
| 141 |
+
"translations": center[None],
|
| 142 |
+
"rotations": rot[None],
|
| 143 |
+
"control_levels": np.array([0.0]),
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _parse_local_prompts(text, n_sq):
|
| 148 |
+
"""'6: pink painted wood' lines (1-based primitive index) -> per-primitive list."""
|
| 149 |
+
prompts = [""] * n_sq
|
| 150 |
+
for line in (text or "").replace(";", "\n").splitlines():
|
| 151 |
+
line = line.strip()
|
| 152 |
+
if not line or ":" not in line:
|
| 153 |
+
continue
|
| 154 |
+
idx, prompt = line.split(":", 1)
|
| 155 |
+
idx = idx.strip().lower().replace("sq", "").strip()
|
| 156 |
+
if not idx.isdigit():
|
| 157 |
+
continue
|
| 158 |
+
k = int(idx) - 1
|
| 159 |
+
if 0 <= k < n_sq:
|
| 160 |
+
prompts[k] = prompt.strip()
|
| 161 |
+
return prompts
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _blender_normalized_ply(glb_path, ply_path):
|
| 165 |
+
"""Python port of TRELLIS' Blender `--save_mesh` export used by SpaceFlow.
|
| 166 |
+
|
| 167 |
+
Blender imports glTF (Y-up) into its Z-up frame, scales the scene so the
|
| 168 |
+
largest bbox side is 1, re-centres the bbox at the origin and writes a PLY.
|
| 169 |
+
"""
|
| 170 |
+
mesh = trimesh.load(glb_path, force="mesh", process=False)
|
| 171 |
+
v = np.asarray(mesh.vertices, dtype=np.float64)
|
| 172 |
+
v = np.stack([v[:, 0], -v[:, 2], v[:, 1]], axis=1)
|
| 173 |
+
mn, mx = v.min(0), v.max(0)
|
| 174 |
+
v = v * (1.0 / (mx - mn).max())
|
| 175 |
+
mn, mx = v.min(0), v.max(0)
|
| 176 |
+
v = v - (mn + mx) / 2
|
| 177 |
+
trimesh.Trimesh(vertices=v, faces=np.asarray(mesh.faces), process=False).export(ply_path)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _partfield_planes(mesh_path, out_dir):
|
| 181 |
+
"""Equivalent of run_local_tau.predict_part without the Lightning Trainer."""
|
| 182 |
+
_seed_all(int(PF_CFG.seed))
|
| 183 |
+
item = Demo_Dataset(mesh_path)[0]
|
| 184 |
+
batch = {"pc": item["pc"][None].to("cuda"), "uid": [item["uid"]]}
|
| 185 |
+
with torch.autocast("cuda", dtype=torch.float16):
|
| 186 |
+
part_planes, uid = PARTFIELD.predict_step(batch, 0)
|
| 187 |
+
np.save(os.path.join(out_dir, f"part_feat_{uid}_batch_part_plane.npy"), part_planes)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def preview_primitives(primitives_file):
|
| 191 |
+
"""Render the uploaded primitives (orange = high control, white = low control) and read saved prompts."""
|
| 192 |
+
if primitives_file is None:
|
| 193 |
+
return None, gr.update(), gr.update(), gr.update(), ""
|
| 194 |
+
path = primitives_file if isinstance(primitives_file, str) else primitives_file.name
|
| 195 |
+
out = tempfile.mkdtemp()
|
| 196 |
+
glb = os.path.join(out, "input_superquadrics.glb")
|
| 197 |
+
rlt.export_colored_superquadrics_glb(path, glb)
|
| 198 |
+
# Show in a Y-up viewer the same way the result is oriented.
|
| 199 |
+
meta = _read_metadata(path)
|
| 200 |
+
arrays = _load_arrays(path)
|
| 201 |
+
n = arrays["scales"].shape[0]
|
| 202 |
+
names = meta.get("primitiveNames") or [f"SQ {i + 1}" for i in range(n)]
|
| 203 |
+
levels = ["high" if float(c) >= 0.5 else "low" for c in arrays["control_levels"]]
|
| 204 |
+
local = meta.get("localTextureTexts") or [""] * n
|
| 205 |
+
rows = "\n".join(
|
| 206 |
+
f"| {i + 1} | {names[i] if i < len(names) else ''} | {levels[i]} | {local[i] if i < len(local) else ''} |"
|
| 207 |
+
for i in range(n))
|
| 208 |
+
table = "| # | name | control | local texture |\n|---|---|---|---|\n" + rows
|
| 209 |
+
shape = meta.get("textPrompt")
|
| 210 |
+
tex = meta.get("globalTextureText")
|
| 211 |
+
local_txt = "\n".join(f"{i + 1}: {t}" for i, t in enumerate(local) if str(t or "").strip())
|
| 212 |
+
return (
|
| 213 |
+
glb,
|
| 214 |
+
gr.update(value=shape) if shape else gr.update(),
|
| 215 |
+
gr.update(value=tex) if tex else gr.update(),
|
| 216 |
+
gr.update(value=local_txt) if meta else gr.update(),
|
| 217 |
+
table,
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def _duration(primitives_file, shape_prompt, texture_prompt, local_prompts="", low_tau=3.0, high_tau=10.0,
|
| 222 |
+
polyak_tau=0.18, repaint_steps=10, texture_steps=300, seed=1, attn_boost=2.5, *args, **kwargs):
|
| 223 |
+
return int(min(300, 45 + 0.5 * int(texture_steps)))
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
@spaces.GPU(duration=_duration)
|
| 227 |
+
def generate(
|
| 228 |
+
primitives_file,
|
| 229 |
+
shape_prompt: str,
|
| 230 |
+
texture_prompt: str,
|
| 231 |
+
local_prompts: str = "",
|
| 232 |
+
low_tau: float = 3.0,
|
| 233 |
+
high_tau: float = 10.0,
|
| 234 |
+
polyak_tau: float = 0.18,
|
| 235 |
+
repaint_steps: int = 10,
|
| 236 |
+
texture_steps: int = 300,
|
| 237 |
+
seed: int = 1,
|
| 238 |
+
attn_boost: float = 2.5,
|
| 239 |
+
progress=gr.Progress(track_tqdm=True),
|
| 240 |
+
):
|
| 241 |
+
"""Generate a textured 3D asset from superquadric primitives with SpaceFlow.
|
| 242 |
+
|
| 243 |
+
Args:
|
| 244 |
+
primitives_file: SpaceFlow primitives `.npz` (scales, shapes, translations, rotations, control_levels).
|
| 245 |
+
shape_prompt: Text describing the object (e.g. "toy elephant").
|
| 246 |
+
texture_prompt: Global appearance text (e.g. "blue painted wood").
|
| 247 |
+
local_prompts: Optional per-primitive appearance overrides, one per line as "<index>: <text>" (1-based).
|
| 248 |
+
low_tau: Structure-control strength for low-control primitives (lower = more freedom).
|
| 249 |
+
high_tau: Structure-control strength for high-control primitives.
|
| 250 |
+
polyak_tau: Polyak blending factor between the low- and high-control trajectories.
|
| 251 |
+
repaint_steps: Repaint resampling steps used to blend the two control levels.
|
| 252 |
+
texture_steps: Number of self-similarity guided texture flow steps.
|
| 253 |
+
seed: Seed for sparse-structure sampling.
|
| 254 |
+
attn_boost: Region-boosted self-attention factor for local texture routing (1.0 = off).
|
| 255 |
+
|
| 256 |
+
Returns:
|
| 257 |
+
Path to the textured GLB and a short run log.
|
| 258 |
+
"""
|
| 259 |
+
if primitives_file is None:
|
| 260 |
+
raise gr.Error("Upload a primitives .npz file (or pick an example).")
|
| 261 |
+
shape_prompt = (shape_prompt or "").strip()
|
| 262 |
+
if not shape_prompt:
|
| 263 |
+
raise gr.Error("Enter a shape prompt.")
|
| 264 |
+
texture_prompt = (texture_prompt or "").strip() or shape_prompt
|
| 265 |
+
t0 = time.perf_counter()
|
| 266 |
+
path = primitives_file if isinstance(primitives_file, str) else primitives_file.name
|
| 267 |
+
|
| 268 |
+
out_dir = tempfile.mkdtemp(prefix="spaceflow_")
|
| 269 |
+
arrays = _load_arrays(path)
|
| 270 |
+
n_sq = arrays["scales"].shape[0]
|
| 271 |
+
all_path = os.path.join(out_dir, "all.npz")
|
| 272 |
+
np.savez(all_path, **arrays)
|
| 273 |
+
high_idx = [i for i in range(n_sq) if float(arrays["control_levels"][i]) >= 0.5]
|
| 274 |
+
low_idx = [i for i in range(n_sq) if float(arrays["control_levels"][i]) < 0.5]
|
| 275 |
+
local_mode = bool(high_idx) and bool(low_idx)
|
| 276 |
+
if local_mode and float(high_tau) < float(low_tau):
|
| 277 |
+
raise gr.Error("High-control tau must be >= low-control tau.")
|
| 278 |
+
|
| 279 |
+
cfg = copy.deepcopy(CFG)
|
| 280 |
+
cfg.sim_guidance.steps = max(2, int(texture_steps))
|
| 281 |
+
cfg.sim_guidance.mixed_self_attn_boost = max(1.0, float(attn_boost))
|
| 282 |
+
|
| 283 |
+
_seed_all(0)
|
| 284 |
+
_ensure_null_cond()
|
| 285 |
+
log = []
|
| 286 |
+
|
| 287 |
+
# --- spatial-control meshes from the superquadrics -------------------------------------
|
| 288 |
+
sc_mesh = os.path.join(out_dir, "spatial_control_mesh.ply")
|
| 289 |
+
aabb, center, scale = rlt.load_superquadrics(all_path, sc_mesh)
|
| 290 |
+
sampler = {
|
| 291 |
+
"steps": rlt.STEPS_SHAPE_GEN,
|
| 292 |
+
"cfg_strength": rlt.CFG_SHAPE_GEN,
|
| 293 |
+
"t0_idx_value": float(low_tau),
|
| 294 |
+
"spatial_control_mesh_path": sc_mesh,
|
| 295 |
+
"high_control_spatial_control_mesh_path": None,
|
| 296 |
+
"low_control_superquadric_mask_path": None,
|
| 297 |
+
"t0_idx_value_high_control": None,
|
| 298 |
+
"polyak_update_tau": float(polyak_tau) if local_mode else 0.0,
|
| 299 |
+
"local_tau_mode": "guidance",
|
| 300 |
+
"n_repaint_steps": int(repaint_steps),
|
| 301 |
+
}
|
| 302 |
+
if local_mode:
|
| 303 |
+
hi_npz = os.path.join(out_dir, "high_control.npz")
|
| 304 |
+
lo_npz = os.path.join(out_dir, "low_control_bbox.npz")
|
| 305 |
+
np.savez(hi_npz, **_subset(arrays, high_idx))
|
| 306 |
+
np.savez(lo_npz, **_low_control_bbox(arrays, low_idx))
|
| 307 |
+
hi_mesh = os.path.join(out_dir, "high_control_spatial_control_mesh.ply")
|
| 308 |
+
lo_mesh = os.path.join(out_dir, "low_control_superquadric_mask.ply")
|
| 309 |
+
rlt.load_superquadrics(hi_npz, hi_mesh, aabb=aabb, center=center, scale=scale)
|
| 310 |
+
rlt.load_superquadrics(lo_npz, lo_mesh, aabb=aabb, center=center, scale=scale)
|
| 311 |
+
sampler.update({
|
| 312 |
+
"high_control_spatial_control_mesh_path": hi_mesh,
|
| 313 |
+
"low_control_superquadric_mask_path": lo_mesh,
|
| 314 |
+
"t0_idx_value_high_control": float(high_tau),
|
| 315 |
+
"local_tau_mode": "low_control_mask",
|
| 316 |
+
})
|
| 317 |
+
log.append(f"Local control: {len(high_idx)} high / {len(low_idx)} low primitives "
|
| 318 |
+
f"(tau {low_tau} / {high_tau}, polyak {polyak_tau}).")
|
| 319 |
+
else:
|
| 320 |
+
log.append(f"Global control: all {n_sq} primitives at tau {low_tau}.")
|
| 321 |
+
|
| 322 |
+
# --- 1. SpaceControl sparse structure ---------------------------------------------------
|
| 323 |
+
coords = PIPE.gen_structure_v2(shape_prompt, seed=int(seed), vis_output_dir=None,
|
| 324 |
+
sparse_structure_sampler_params=sampler)
|
| 325 |
+
voxels = coords.detach().cpu().numpy()[:, 1:]
|
| 326 |
+
log.append(f"Structure: {voxels.shape[0]} voxels ({time.perf_counter() - t0:.1f}s).")
|
| 327 |
+
rlt.sparse_voxels_to_glb(voxels, grid_size=64, output_filename=os.path.join(out_dir, "sample.glb"))
|
| 328 |
+
struct_mesh = trimesh.load(os.path.join(out_dir, "sample.glb"), force="mesh")
|
| 329 |
+
struct_mesh = pointcloud.convert_mesh_yup_to_zup(struct_mesh) # --convert_yup_to_zup
|
| 330 |
+
struct_zup = os.path.join(out_dir, "struct_mesh_zup.glb")
|
| 331 |
+
struct_mesh.export(struct_zup)
|
| 332 |
+
|
| 333 |
+
render_dir = os.path.join(out_dir, "struct_renders")
|
| 334 |
+
os.makedirs(render_dir, exist_ok=True)
|
| 335 |
+
struct_ply = os.path.join(render_dir, "mesh.ply")
|
| 336 |
+
_blender_normalized_ply(struct_zup, struct_ply)
|
| 337 |
+
voxel_dir = os.path.join(out_dir, "voxels")
|
| 338 |
+
os.makedirs(voxel_dir, exist_ok=True)
|
| 339 |
+
pointcloud.voxelize_mesh(struct_ply, save_path=os.path.join(voxel_dir, "struct_voxels.ply"))
|
| 340 |
+
|
| 341 |
+
# --- 2. PartField features on the generated structure -----------------------------------
|
| 342 |
+
pf_dir = os.path.join(out_dir, "partfield")
|
| 343 |
+
os.makedirs(pf_dir, exist_ok=True)
|
| 344 |
+
_partfield_planes(struct_ply, pf_dir)
|
| 345 |
+
|
| 346 |
+
# --- 3. self-similarity guided appearance flow with local texture routing ---------------
|
| 347 |
+
local_list = _parse_local_prompts(local_prompts, n_sq)
|
| 348 |
+
active = [i + 1 for i, p in enumerate(local_list) if p]
|
| 349 |
+
local_list = local_list if active else None
|
| 350 |
+
individual = rlt.build_individual_sq_meshes_normalized(all_path) if local_list else None
|
| 351 |
+
if active:
|
| 352 |
+
log.append("Local texture overrides on primitives " + ", ".join(map(str, active)) + ".")
|
| 353 |
+
self_similarity.optimize_self_similarity(
|
| 354 |
+
cfg, texture_prompt, "text", out_dir,
|
| 355 |
+
local_prompts=local_list,
|
| 356 |
+
local_prompt_type="text" if local_list else None,
|
| 357 |
+
individual_sq_meshes=individual,
|
| 358 |
+
generation_pipeline=PIPE,
|
| 359 |
+
decode_texture=True,
|
| 360 |
+
)
|
| 361 |
+
out_glb = os.path.join(out_dir, "out_sim.glb")
|
| 362 |
+
for wp in glob.glob(os.path.join(out_dir, "*warning*.json")):
|
| 363 |
+
try:
|
| 364 |
+
w = json.load(open(wp))
|
| 365 |
+
items = w if isinstance(w, list) else w.get("warnings", [w])
|
| 366 |
+
for it in items:
|
| 367 |
+
if isinstance(it, dict) and it.get("message"):
|
| 368 |
+
log.append("Warning: " + it["message"])
|
| 369 |
+
except Exception:
|
| 370 |
+
pass
|
| 371 |
+
log.append(f"Done in {time.perf_counter() - t0:.1f}s.")
|
| 372 |
+
print("[SpaceFlow] " + " ".join(log))
|
| 373 |
+
return out_glb, "\n".join(f"- {line}" for line in log)
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
# ---------------------------------------------------------------------------
|
| 377 |
+
# UI
|
| 378 |
+
# ---------------------------------------------------------------------------
|
| 379 |
+
EXAMPLES = [
|
| 380 |
+
["examples/toy_elephant.npz", "toy elephant", "blue painted wood", "6: pink painted wood\n9: pink painted wood"],
|
| 381 |
+
["examples/sailboat.npz", "Sailboat", "white", None],
|
| 382 |
+
["examples/chair.npz", "A chair", "light wood", None],
|
| 383 |
+
["examples/hot_air_balloon.npz", "a vintage hot air balloon with a wicker basket",
|
| 384 |
+
"warm vintage canvas, leather trim, and soft sunlit shading", None],
|
| 385 |
+
["examples/toy_rocket.npz", "a toy rocket", "white metal", None],
|
| 386 |
+
["examples/windmill.npz", "a rustic windmill with four wooden blades",
|
| 387 |
+
"weathered white plaster walls with dark wooden beams", None],
|
| 388 |
+
]
|
| 389 |
+
# Fill local prompts for the examples from the authors' saved metadata.
|
| 390 |
+
for _ex in EXAMPLES:
|
| 391 |
+
if _ex[3] is None:
|
| 392 |
+
_m = _read_metadata(_ex[0])
|
| 393 |
+
_ex[3] = "\n".join(f"{i + 1}: {t}" for i, t in enumerate(_m.get("localTextureTexts", [])) if str(t).strip())
|
| 394 |
+
|
| 395 |
+
CSS = """
|
| 396 |
+
#col-container { max-width: 1200px; margin: 0 auto; }
|
| 397 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 398 |
+
"""
|
| 399 |
+
|
| 400 |
+
with gr.Blocks(title="SpaceFlow") as demo:
|
| 401 |
+
with gr.Column(elem_id="col-container"):
|
| 402 |
+
gr.Markdown(
|
| 403 |
+
"# SpaceFlow: Locally Controllable 3D Generation\n"
|
| 404 |
+
"Training-free 3D generation from **superquadric primitives**: each primitive is marked as "
|
| 405 |
+
"*high* (stick to it) or *low* (only a loose hint) geometry control, and can carry its own "
|
| 406 |
+
"**local texture prompt**. Built on TRELLIS-text-xlarge + PartField. \n"
|
| 407 |
+
"[Project page](https://spaceflow3d.github.io/) · [Paper](https://arxiv.org/abs/2610.12399) · "
|
| 408 |
+
"[Code](https://github.com/SpaceFlow3D/spaceflow) · Author primitives in the "
|
| 409 |
+
"[SpaceFlow editor](https://github.com/SpaceFlow3D/spaceflow#interactive-editor) and upload its `all.npz` here."
|
| 410 |
+
)
|
| 411 |
+
with gr.Row():
|
| 412 |
+
with gr.Column(scale=1):
|
| 413 |
+
primitives = gr.File(label="Primitives (.npz from the SpaceFlow editor)", file_types=[".npz"],
|
| 414 |
+
type="filepath")
|
| 415 |
+
input_view = gr.Model3D(label="Input primitives (orange = high control, white = low control)",
|
| 416 |
+
height=300, interactive=False)
|
| 417 |
+
shape_prompt = gr.Textbox(label="Shape prompt", placeholder="toy elephant")
|
| 418 |
+
texture_prompt = gr.Textbox(label="Global texture prompt", placeholder="blue painted wood")
|
| 419 |
+
local_prompts = gr.Textbox(
|
| 420 |
+
label="Local texture prompts (optional)", lines=3,
|
| 421 |
+
placeholder="One per line, '<primitive #>: <texture>', e.g.\n6: pink painted wood")
|
| 422 |
+
run_btn = gr.Button("Generate", variant="primary")
|
| 423 |
+
with gr.Accordion("Primitive list", open=False):
|
| 424 |
+
prim_table = gr.Markdown()
|
| 425 |
+
with gr.Accordion("Advanced settings", open=False):
|
| 426 |
+
with gr.Row():
|
| 427 |
+
low_tau = gr.Slider(0, 12, value=3.0, step=0.5, label="Low-control tau")
|
| 428 |
+
high_tau = gr.Slider(0, 12, value=10.0, step=0.5, label="High-control tau")
|
| 429 |
+
with gr.Row():
|
| 430 |
+
polyak_tau = gr.Slider(0, 1, value=0.18, step=0.01, label="Polyak blend")
|
| 431 |
+
repaint_steps = gr.Slider(0, 20, value=10, step=1, label="Repaint steps")
|
| 432 |
+
with gr.Row():
|
| 433 |
+
texture_steps = gr.Slider(50, 300, value=300, step=10, label="Texture flow steps")
|
| 434 |
+
attn_boost = gr.Slider(1.0, 4.0, value=2.5, step=0.1, label="Local attention boost")
|
| 435 |
+
seed = gr.Number(value=1, precision=0, label="Structure seed")
|
| 436 |
+
with gr.Column(scale=1):
|
| 437 |
+
output = gr.Model3D(label="Generated textured mesh (GLB)", height=520)
|
| 438 |
+
info = gr.Markdown()
|
| 439 |
+
|
| 440 |
+
inputs = [primitives, shape_prompt, texture_prompt, local_prompts, low_tau, high_tau, polyak_tau,
|
| 441 |
+
repaint_steps, texture_steps, seed, attn_boost]
|
| 442 |
+
primitives.change(preview_primitives, inputs=[primitives],
|
| 443 |
+
outputs=[input_view, shape_prompt, texture_prompt, local_prompts, prim_table],
|
| 444 |
+
api_visibility="private")
|
| 445 |
+
run_btn.click(generate, inputs=inputs, outputs=[output, info], api_name="generate")
|
| 446 |
+
|
| 447 |
+
gr.Examples(
|
| 448 |
+
examples=EXAMPLES,
|
| 449 |
+
inputs=[primitives, shape_prompt, texture_prompt, local_prompts],
|
| 450 |
+
outputs=[output, info],
|
| 451 |
+
fn=generate,
|
| 452 |
+
cache_examples=True,
|
| 453 |
+
cache_mode="lazy",
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
if __name__ == "__main__":
|
| 457 |
+
demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)
|
config/default.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# render
|
| 2 |
+
num_views: 0
|
| 3 |
+
|
| 4 |
+
# trellis
|
| 5 |
+
trellis_img_model_name: "JeffreyXiang/TRELLIS-image-large"
|
| 6 |
+
# Local pipeline config that mixes TRELLIS-image-large (encoder/decoder/SLAT
|
| 7 |
+
# decoders) with TRELLIS-text-xlarge (sparse-structure & SLAT flow models).
|
| 8 |
+
# Necessary for spatial-control: microsoft/TRELLIS-text-large does not ship a
|
| 9 |
+
# sparse_structure_encoder, but encode_spatial_control() requires one.
|
| 10 |
+
trellis_text_model_name: "config/trellis_pipeline"
|
| 11 |
+
flow_model_in_channels: 8
|
| 12 |
+
|
| 13 |
+
img_model:
|
| 14 |
+
rescale_t: 3.0
|
| 15 |
+
cfg_strength: 5.0
|
| 16 |
+
cfg_interval: [0.5, 1.0]
|
| 17 |
+
|
| 18 |
+
text_model:
|
| 19 |
+
rescale_t: 3.0
|
| 20 |
+
cfg_strength: 7.5
|
| 21 |
+
cfg_interval: [0.5, 9.5]
|
| 22 |
+
|
| 23 |
+
# self-similarity guidance
|
| 24 |
+
sim_guidance:
|
| 25 |
+
num_part_clusters: 30
|
| 26 |
+
steps: 300
|
| 27 |
+
learning_rate: 5e-4
|
| 28 |
+
loss_weight: 1.0
|
| 29 |
+
local_condition_dilation: 0
|
| 30 |
+
local_condition_min_cells: 192
|
| 31 |
+
local_condition_max_dilation: 1
|
| 32 |
+
# Multiply self-attention weights by this factor before the softmax when both
|
| 33 |
+
# voxels share a routed condition. 1.0 disables and keeps stock self-attention.
|
| 34 |
+
mixed_self_attn_boost: 2.5
|
| 35 |
+
# "legacy" preserves the original PartField/geometric router. "active_margin"
|
| 36 |
+
# lets active local SQs claim nearby occupied cells when the generated structure
|
| 37 |
+
# is slightly shifted relative to the input superquadric.
|
| 38 |
+
local_routing_mode: legacy
|
| 39 |
+
local_routing_margin: 2.0
|
| 40 |
+
|
| 41 |
+
log_every: 100
|
config/trellis_pipeline/pipeline.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "TrellisTextTo3DPipeline",
|
| 3 |
+
"args": {
|
| 4 |
+
"models": {
|
| 5 |
+
"sparse_structure_decoder": "microsoft/TRELLIS-image-large/ckpts/ss_dec_conv3d_16l8_fp16",
|
| 6 |
+
"sparse_structure_encoder": "microsoft/TRELLIS-image-large/ckpts/ss_enc_conv3d_16l8_fp16",
|
| 7 |
+
"sparse_structure_flow_model": "microsoft/TRELLIS-text-xlarge/ckpts/ss_flow_txt_dit_XL_16l8_fp16",
|
| 8 |
+
"slat_decoder_gs": "microsoft/TRELLIS-image-large/ckpts/slat_dec_gs_swin8_B_64l8gs32_fp16",
|
| 9 |
+
"slat_decoder_mesh": "microsoft/TRELLIS-image-large/ckpts/slat_dec_mesh_swin8_B_64l8m256c_fp16",
|
| 10 |
+
"slat_flow_model_text": "microsoft/TRELLIS-text-xlarge/ckpts/slat_flow_txt_dit_XL_64l8p2_fp16"
|
| 11 |
+
},
|
| 12 |
+
"sparse_structure_sampler": {
|
| 13 |
+
"name": "FlowEulerGuidanceIntervalSampler",
|
| 14 |
+
"args": {
|
| 15 |
+
"sigma_min": 1e-05
|
| 16 |
+
},
|
| 17 |
+
"params": {
|
| 18 |
+
"steps": 25,
|
| 19 |
+
"cfg_strength": 7.5,
|
| 20 |
+
"cfg_interval": [
|
| 21 |
+
0.5,
|
| 22 |
+
0.95
|
| 23 |
+
],
|
| 24 |
+
"rescale_t": 3.0
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
"slat_sampler": {
|
| 28 |
+
"name": "FlowEulerGuidanceIntervalSampler",
|
| 29 |
+
"args": {
|
| 30 |
+
"sigma_min": 1e-05
|
| 31 |
+
},
|
| 32 |
+
"params": {
|
| 33 |
+
"steps": 25,
|
| 34 |
+
"cfg_strength": 7.5,
|
| 35 |
+
"cfg_interval": [
|
| 36 |
+
0.5,
|
| 37 |
+
0.95
|
| 38 |
+
],
|
| 39 |
+
"rescale_t": 3.0
|
| 40 |
+
}
|
| 41 |
+
},
|
| 42 |
+
"slat_normalization": {
|
| 43 |
+
"mean": [
|
| 44 |
+
-2.1687545776367188,
|
| 45 |
+
-0.004347046371549368,
|
| 46 |
+
-0.13352349400520325,
|
| 47 |
+
-0.08418072760105133,
|
| 48 |
+
-0.5271206498146057,
|
| 49 |
+
0.7238689064979553,
|
| 50 |
+
-1.1414450407028198,
|
| 51 |
+
1.2039363384246826
|
| 52 |
+
],
|
| 53 |
+
"std": [
|
| 54 |
+
2.377650737762451,
|
| 55 |
+
2.386378288269043,
|
| 56 |
+
2.124418020248413,
|
| 57 |
+
2.1748552322387695,
|
| 58 |
+
2.663944721221924,
|
| 59 |
+
2.371192216873169,
|
| 60 |
+
2.6217446327209473,
|
| 61 |
+
2.684523105621338
|
| 62 |
+
]
|
| 63 |
+
},
|
| 64 |
+
"text_cond_model": "openai/clip-vit-large-patch14",
|
| 65 |
+
"image_cond_model": "dinov2_vitl14_reg"
|
| 66 |
+
}
|
| 67 |
+
}
|
examples/chair.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dd111d52e0194ae0628dbda65f49cb938c71332709269b592df405f735a3a7d8
|
| 3 |
+
size 4148
|
examples/hot_air_balloon.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1015eb538c4e613c84ac62bb7b8f3ec202d5852ab36f30d4b32ef2989cd06a88
|
| 3 |
+
size 3910
|
examples/sailboat.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9710205f9549302bfd70de5b6f26ec2c461f2197edc89bc341f0c67f9943119b
|
| 3 |
+
size 3503
|
examples/toy_elephant.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b9c5b641efdfc1e6b16ce947b4775cb86606429dde70f1ee19a746beb57942a0
|
| 3 |
+
size 2930
|
examples/toy_rocket.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0523710cdcc30a0bef3491d6239bea355a93e11a13ac752d729477bae309f6aa
|
| 3 |
+
size 2326
|
examples/windmill.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:427e77c4072cbd0703f6f64fd8151bf6cba8a587a0d684f05cf580c19bcee7b9
|
| 3 |
+
size 4558
|
lib/opt/self_similarity.py
ADDED
|
@@ -0,0 +1,1072 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import json
|
| 3 |
+
import os.path as osp
|
| 4 |
+
from PIL import Image
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
import utils3d
|
| 9 |
+
import logging
|
| 10 |
+
import open3d_pycg as o3d
|
| 11 |
+
import time
|
| 12 |
+
import os
|
| 13 |
+
_KEEP_ON_GPU = os.environ.get('SPACEFLOW_KEEP_ON_GPU', '') == '1'
|
| 14 |
+
|
| 15 |
+
import third_party.TRELLIS.trellis.modules.sparse as sp
|
| 16 |
+
from third_party.TRELLIS.trellis.pipelines import TrellisImageTo3DPipeline, TrellisTextTo3DPipeline
|
| 17 |
+
from lib.util import generation, partfield
|
| 18 |
+
from lib.util.pipeline_compat import can_reuse_appearance_pipeline
|
| 19 |
+
|
| 20 |
+
# Global logger
|
| 21 |
+
log = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _select_slat_flow_model(generation_pipeline, app_type):
|
| 25 |
+
"""Return the SLAT flow model key/model for old and split text/image pipeline configs."""
|
| 26 |
+
if app_type == 'image':
|
| 27 |
+
candidates = ['slat_flow_model_image', 'slat_flow_model']
|
| 28 |
+
else:
|
| 29 |
+
candidates = ['slat_flow_model_text', 'slat_flow_model']
|
| 30 |
+
|
| 31 |
+
for key in candidates:
|
| 32 |
+
if key in generation_pipeline.models:
|
| 33 |
+
log.info(f"Using SLAT flow model: {key}")
|
| 34 |
+
return key, generation_pipeline.models[key]
|
| 35 |
+
|
| 36 |
+
available = ', '.join(sorted(generation_pipeline.models.keys()))
|
| 37 |
+
raise KeyError(
|
| 38 |
+
f"No compatible SLAT flow model found for {app_type} conditioning. "
|
| 39 |
+
f"Tried {candidates}; available models: {available}"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _text_conditioner_to(generation_pipeline, device):
|
| 44 |
+
text_cond_model = getattr(generation_pipeline, 'text_cond_model', None)
|
| 45 |
+
if not text_cond_model:
|
| 46 |
+
return
|
| 47 |
+
if 'model' in text_cond_model:
|
| 48 |
+
text_cond_model['model'].to(device)
|
| 49 |
+
if 'null_cond' in text_cond_model:
|
| 50 |
+
text_cond_model['null_cond'] = text_cond_model['null_cond'].to(device)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _preprocess_condition_image(generation_pipeline, path):
|
| 54 |
+
image = Image.open(path).convert('RGB')
|
| 55 |
+
if hasattr(generation_pipeline, 'preprocess_image'):
|
| 56 |
+
return generation_pipeline.preprocess_image(image)
|
| 57 |
+
return image
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _voxelize_mesh(mesh):
|
| 61 |
+
"""Voxelize an Open3D mesh into a (1,1,64,64,64) binary float32 tensor on CPU."""
|
| 62 |
+
mesh = copy.deepcopy(mesh)
|
| 63 |
+
vertices = np.clip(np.asarray(mesh.vertices), -0.5 + 1e-6, 0.5 - 1e-6)
|
| 64 |
+
mesh.vertices = o3d.utility.Vector3dVector(vertices)
|
| 65 |
+
voxel_grid = o3d.geometry.VoxelGrid.create_from_triangle_mesh_within_bounds(
|
| 66 |
+
mesh, voxel_size=1/64, min_bound=(-0.5, -0.5, -0.5), max_bound=(0.5, 0.5, 0.5))
|
| 67 |
+
voxel_indices = np.array([v.grid_index for v in voxel_grid.get_voxels()])
|
| 68 |
+
coords_dense = torch.zeros(1, 1, 64, 64, 64, dtype=torch.float32)
|
| 69 |
+
if len(voxel_indices) > 0:
|
| 70 |
+
coords_dense[0, 0, voxel_indices[:, 0], voxel_indices[:, 1], voxel_indices[:, 2]] = 1.0
|
| 71 |
+
return coords_dense
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _dense_cell_groups(struct_coords, device='cuda'):
|
| 75 |
+
"""Return occupied 32^3 cells and the 64^3 source-voxel group for each point."""
|
| 76 |
+
dense_coords = (struct_coords[:, 1:].to(device).long() // 2).clamp(0, 31)
|
| 77 |
+
linear = (
|
| 78 |
+
dense_coords[:, 0] * 32 * 32
|
| 79 |
+
+ dense_coords[:, 1] * 32
|
| 80 |
+
+ dense_coords[:, 2]
|
| 81 |
+
)
|
| 82 |
+
unique_linear, inverse = torch.unique(linear, sorted=True, return_inverse=True)
|
| 83 |
+
unique_coords = torch.stack(
|
| 84 |
+
[
|
| 85 |
+
unique_linear // (32 * 32),
|
| 86 |
+
(unique_linear // 32) % 32,
|
| 87 |
+
unique_linear % 32,
|
| 88 |
+
],
|
| 89 |
+
dim=1,
|
| 90 |
+
)
|
| 91 |
+
return unique_coords, inverse
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _surface_distance_matrix(generated_coords, sq_coords_list):
|
| 95 |
+
"""Distance from each generated 64^3 voxel to each SQ surface."""
|
| 96 |
+
n_sq = len(sq_coords_list)
|
| 97 |
+
distances = torch.full(
|
| 98 |
+
(n_sq, generated_coords.shape[0]),
|
| 99 |
+
float('inf'),
|
| 100 |
+
dtype=generated_coords.dtype,
|
| 101 |
+
device=generated_coords.device,
|
| 102 |
+
)
|
| 103 |
+
for idx, sq_coords in enumerate(sq_coords_list):
|
| 104 |
+
if sq_coords.shape[0] > 0:
|
| 105 |
+
distances[idx] = torch.cdist(sq_coords, generated_coords).min(0).values
|
| 106 |
+
return distances
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _aggregate_dense_min_distances(point_distances, inverse, n_cells):
|
| 110 |
+
"""Pool point-to-SQ distances into occupied 32^3 cells without last-write bias."""
|
| 111 |
+
n_sq = point_distances.shape[0]
|
| 112 |
+
cell_distances = torch.full(
|
| 113 |
+
(n_sq, n_cells),
|
| 114 |
+
float('inf'),
|
| 115 |
+
dtype=point_distances.dtype,
|
| 116 |
+
device=point_distances.device,
|
| 117 |
+
)
|
| 118 |
+
for sq_idx in range(n_sq):
|
| 119 |
+
cell_distances[sq_idx].scatter_reduce_(
|
| 120 |
+
0,
|
| 121 |
+
inverse,
|
| 122 |
+
point_distances[sq_idx],
|
| 123 |
+
reduce='amin',
|
| 124 |
+
include_self=True,
|
| 125 |
+
)
|
| 126 |
+
return cell_distances
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _aggregate_dense_votes(point_labels, inverse, n_cells, n_labels):
|
| 130 |
+
"""Pool per-point labels into 32^3 cells by majority vote."""
|
| 131 |
+
votes = torch.zeros(
|
| 132 |
+
(n_cells, n_labels),
|
| 133 |
+
dtype=torch.int32,
|
| 134 |
+
device=point_labels.device,
|
| 135 |
+
)
|
| 136 |
+
votes.index_put_(
|
| 137 |
+
(inverse, point_labels.long()),
|
| 138 |
+
torch.ones_like(point_labels, dtype=torch.int32),
|
| 139 |
+
accumulate=True,
|
| 140 |
+
)
|
| 141 |
+
return votes.argmax(dim=1)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def compute_active_margin_indices(
|
| 145 |
+
struct_coords,
|
| 146 |
+
individual_sq_meshes,
|
| 147 |
+
active_indices,
|
| 148 |
+
device='cuda',
|
| 149 |
+
margin=2.0,
|
| 150 |
+
):
|
| 151 |
+
"""Route active local conditions to nearby occupied cells with a distance margin.
|
| 152 |
+
|
| 153 |
+
The legacy router assigns a cell only to the single nearest SQ. This is brittle
|
| 154 |
+
when the generated structure shifts a thin part by a few 64^3 voxels. Here an
|
| 155 |
+
active SQ may claim a cell when it is within ``margin`` voxels of the best SQ;
|
| 156 |
+
competing active SQs are resolved by their distance to the cell. The routing is
|
| 157 |
+
still restricted to occupied generated cells, so the margin cannot create texture
|
| 158 |
+
in empty space.
|
| 159 |
+
"""
|
| 160 |
+
coords_dense_indices = torch.zeros(
|
| 161 |
+
1, 1, 32, 32, 32, dtype=torch.int32, device=device
|
| 162 |
+
)
|
| 163 |
+
n_sq = len(individual_sq_meshes)
|
| 164 |
+
active_indices = [idx for idx in active_indices if 0 <= idx < n_sq]
|
| 165 |
+
if n_sq == 0 or not active_indices or struct_coords.shape[0] == 0:
|
| 166 |
+
return coords_dense_indices
|
| 167 |
+
|
| 168 |
+
generated_coords = struct_coords[:, 1:].float().to(device)
|
| 169 |
+
sq_coords_list = []
|
| 170 |
+
for mesh in individual_sq_meshes:
|
| 171 |
+
sq_vox = _voxelize_mesh(mesh)
|
| 172 |
+
sq_coords = torch.argwhere(sq_vox)[:, 2:].float().to(device)
|
| 173 |
+
sq_coords_list.append(sq_coords)
|
| 174 |
+
|
| 175 |
+
dense_coords, inverse = _dense_cell_groups(struct_coords, device=device)
|
| 176 |
+
point_distances = _surface_distance_matrix(generated_coords, sq_coords_list)
|
| 177 |
+
cell_distances = _aggregate_dense_min_distances(
|
| 178 |
+
point_distances,
|
| 179 |
+
inverse,
|
| 180 |
+
dense_coords.shape[0],
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
active = torch.as_tensor(active_indices, dtype=torch.long, device=device)
|
| 184 |
+
active_distances = cell_distances.index_select(0, active)
|
| 185 |
+
best_active_distances, best_active_pos = active_distances.min(dim=0)
|
| 186 |
+
best_all_distances = cell_distances.min(dim=0).values
|
| 187 |
+
selected = best_active_distances <= (best_all_distances + float(margin))
|
| 188 |
+
if selected.any():
|
| 189 |
+
selected_coords = dense_coords[selected]
|
| 190 |
+
selected_sq = active[best_active_pos[selected]] + 1
|
| 191 |
+
coords_dense_indices[
|
| 192 |
+
0,
|
| 193 |
+
0,
|
| 194 |
+
selected_coords[:, 0],
|
| 195 |
+
selected_coords[:, 1],
|
| 196 |
+
selected_coords[:, 2],
|
| 197 |
+
] = selected_sq.to(torch.int32)
|
| 198 |
+
return coords_dense_indices
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def compute_surface_coverage_indices(
|
| 202 |
+
struct_coords,
|
| 203 |
+
individual_sq_meshes,
|
| 204 |
+
active_indices,
|
| 205 |
+
device='cuda',
|
| 206 |
+
min_cells=64,
|
| 207 |
+
max_cells=512,
|
| 208 |
+
candidate_margin=3.0,
|
| 209 |
+
):
|
| 210 |
+
"""Route local conditions from exact SQ surface coverage.
|
| 211 |
+
|
| 212 |
+
PartField and nearest-SQ routing can lose thin or small parts after the
|
| 213 |
+
structure is pooled into the 32^3 SLAT grid. This router first reserves
|
| 214 |
+
cells touched by the exact 64^3 SQ surface voxelization, then fills only a
|
| 215 |
+
compact nearest-cell budget for each active SQ. Small details therefore
|
| 216 |
+
receive a guaranteed local condition without a broad, unconstrained
|
| 217 |
+
dilation into neighboring parts.
|
| 218 |
+
|
| 219 |
+
Values are SQ indices (1-indexed; zero means global/unassigned), matching
|
| 220 |
+
``compute_coords_dense_indices`` and the later condition remapping.
|
| 221 |
+
"""
|
| 222 |
+
coords_dense_indices = torch.zeros(
|
| 223 |
+
1, 1, 32, 32, 32, dtype=torch.int32, device=device
|
| 224 |
+
)
|
| 225 |
+
n_sq = len(individual_sq_meshes)
|
| 226 |
+
active_indices = [idx for idx in active_indices if 0 <= idx < n_sq]
|
| 227 |
+
min_cells = max(1, int(min_cells))
|
| 228 |
+
max_cells = max(min_cells, int(max_cells))
|
| 229 |
+
if n_sq == 0 or not active_indices or struct_coords.shape[0] == 0:
|
| 230 |
+
return coords_dense_indices, {
|
| 231 |
+
'routing_source': 'surface_coverage',
|
| 232 |
+
'exact_surface_cells': {},
|
| 233 |
+
'reserved_cells': {},
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
generated_coords = struct_coords[:, 1:].float().to(device)
|
| 237 |
+
dense_coords, inverse = _dense_cell_groups(struct_coords, device=device)
|
| 238 |
+
n_cells = int(dense_coords.shape[0])
|
| 239 |
+
if n_cells == 0:
|
| 240 |
+
return coords_dense_indices, {
|
| 241 |
+
'routing_source': 'surface_coverage',
|
| 242 |
+
'exact_surface_cells': {},
|
| 243 |
+
'reserved_cells': {},
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
sq_coords_list = []
|
| 247 |
+
for mesh in individual_sq_meshes:
|
| 248 |
+
sq_vox = _voxelize_mesh(mesh)
|
| 249 |
+
sq_coords_list.append(torch.argwhere(sq_vox)[:, 2:].float().to(device))
|
| 250 |
+
point_distances = _surface_distance_matrix(generated_coords, sq_coords_list)
|
| 251 |
+
cell_distances = _aggregate_dense_min_distances(
|
| 252 |
+
point_distances,
|
| 253 |
+
inverse,
|
| 254 |
+
n_cells,
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
dense_linear = (
|
| 258 |
+
dense_coords[:, 0] * 32 * 32
|
| 259 |
+
+ dense_coords[:, 1] * 32
|
| 260 |
+
+ dense_coords[:, 2]
|
| 261 |
+
).long()
|
| 262 |
+
assigned = torch.zeros(n_cells, dtype=torch.int32, device=device)
|
| 263 |
+
exact_counts = {}
|
| 264 |
+
targets = {}
|
| 265 |
+
candidate_orders = {}
|
| 266 |
+
|
| 267 |
+
# Build a compact candidate list for each active SQ. Exact surface cells
|
| 268 |
+
# are always preferred; the nearest cells fill the minimum budget only
|
| 269 |
+
# when the detail is too small or shifted relative to the structure grid.
|
| 270 |
+
for sq_idx in active_indices:
|
| 271 |
+
surface = sq_coords_list[sq_idx]
|
| 272 |
+
if surface.shape[0] > 0:
|
| 273 |
+
surface_dense = (surface.long() // 2).clamp(0, 31)
|
| 274 |
+
surface_linear = (
|
| 275 |
+
surface_dense[:, 0] * 32 * 32
|
| 276 |
+
+ surface_dense[:, 1] * 32
|
| 277 |
+
+ surface_dense[:, 2]
|
| 278 |
+
).long()
|
| 279 |
+
positions = torch.searchsorted(dense_linear, surface_linear)
|
| 280 |
+
valid = positions < n_cells
|
| 281 |
+
safe_positions = positions.clamp(max=n_cells - 1)
|
| 282 |
+
valid = valid & (dense_linear[safe_positions] == surface_linear)
|
| 283 |
+
exact = torch.unique(safe_positions[valid])
|
| 284 |
+
else:
|
| 285 |
+
exact = torch.empty(0, dtype=torch.long, device=device)
|
| 286 |
+
|
| 287 |
+
exact_counts[sq_idx] = int(exact.numel())
|
| 288 |
+
target = min(max_cells, max(min_cells, int(exact.numel())))
|
| 289 |
+
targets[sq_idx] = target
|
| 290 |
+
order = torch.argsort(cell_distances[sq_idx], stable=True)
|
| 291 |
+
if candidate_margin > 0 and order.numel() > 0:
|
| 292 |
+
best = cell_distances[sq_idx, order[0]]
|
| 293 |
+
near = order[cell_distances[sq_idx, order] <= best + float(candidate_margin)]
|
| 294 |
+
if near.numel() >= min_cells:
|
| 295 |
+
order = near
|
| 296 |
+
if exact.numel() > 0:
|
| 297 |
+
order = torch.cat([exact, order])
|
| 298 |
+
order = torch.unique(order, sorted=False)
|
| 299 |
+
candidate_orders[sq_idx] = order[: min(max_cells, int(order.numel()))]
|
| 300 |
+
|
| 301 |
+
# Reserve compact regions for the smallest details first. This prevents a
|
| 302 |
+
# large neighboring part from consuming all cells needed by a tiny part.
|
| 303 |
+
priority = sorted(active_indices, key=lambda idx: (exact_counts[idx], idx))
|
| 304 |
+
for sq_idx in priority:
|
| 305 |
+
order = candidate_orders[sq_idx]
|
| 306 |
+
available = order[assigned[order] == 0]
|
| 307 |
+
selected = available[: targets[sq_idx]]
|
| 308 |
+
if selected.numel() < targets[sq_idx]:
|
| 309 |
+
# Overlapping SQs may have no fully disjoint candidate budget. A
|
| 310 |
+
# detail still gets its nearest cells, with later assignments
|
| 311 |
+
# intentionally unable to overwrite this reservation.
|
| 312 |
+
selected = order[: targets[sq_idx]]
|
| 313 |
+
assigned[selected] = int(sq_idx + 1)
|
| 314 |
+
|
| 315 |
+
coords_dense_indices[
|
| 316 |
+
0,
|
| 317 |
+
0,
|
| 318 |
+
dense_coords[:, 0],
|
| 319 |
+
dense_coords[:, 1],
|
| 320 |
+
dense_coords[:, 2],
|
| 321 |
+
] = assigned
|
| 322 |
+
reserved_counts = {
|
| 323 |
+
sq_idx: int((assigned == sq_idx + 1).sum().item())
|
| 324 |
+
for sq_idx in active_indices
|
| 325 |
+
}
|
| 326 |
+
return coords_dense_indices, {
|
| 327 |
+
'routing_source': 'surface_coverage',
|
| 328 |
+
'exact_surface_cells': exact_counts,
|
| 329 |
+
'reserved_cells': reserved_counts,
|
| 330 |
+
'target_cells': targets,
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def compute_coords_dense_indices(struct_coords, individual_sq_meshes, device='cuda', vox_cluster_labels=None):
|
| 335 |
+
"""Map each 64^3 voxel to its nearest superquadric (1-indexed; 0=unassigned).
|
| 336 |
+
|
| 337 |
+
When vox_cluster_labels is provided (PartField cluster label per voxel), assigns
|
| 338 |
+
voxels via cluster centroids: each cluster centroid is matched to the nearest SQ,
|
| 339 |
+
and all voxels in that cluster inherit the SQ assignment. This is more accurate
|
| 340 |
+
than per-voxel geometric nearest neighbor because PartField clusters capture
|
| 341 |
+
semantic part structure.
|
| 342 |
+
|
| 343 |
+
struct_coords: (M, 4) int tensor [batch, x, y, z] in 64x64x64 space.
|
| 344 |
+
vox_cluster_labels: (M,) int tensor of PartField cluster indices per voxel.
|
| 345 |
+
Returns a (1, 1, 32, 32, 32) int32 tensor.
|
| 346 |
+
"""
|
| 347 |
+
coords_dense_indices = torch.zeros(1, 1, 32, 32, 32, dtype=torch.int32, device=device)
|
| 348 |
+
n_sq = len(individual_sq_meshes)
|
| 349 |
+
if n_sq == 0:
|
| 350 |
+
return coords_dense_indices
|
| 351 |
+
|
| 352 |
+
generated_coords = struct_coords[:, 1:].float().to(device) # (M, 3)
|
| 353 |
+
|
| 354 |
+
# Precompute SQ surface voxel coords once
|
| 355 |
+
sq_coords_list = []
|
| 356 |
+
for mesh in individual_sq_meshes:
|
| 357 |
+
sq_vox = _voxelize_mesh(mesh) # (1,1,64,64,64)
|
| 358 |
+
sq_c = torch.argwhere(sq_vox)[:, 2:].float().to(device) # (K,3)
|
| 359 |
+
sq_coords_list.append(sq_c)
|
| 360 |
+
|
| 361 |
+
if vox_cluster_labels is not None:
|
| 362 |
+
# PartField-based assignment: cluster centroid → nearest SQ → voxels
|
| 363 |
+
labels = vox_cluster_labels.to(device)
|
| 364 |
+
num_clusters = int(labels.max().item()) + 1
|
| 365 |
+
cluster_to_sq = torch.zeros(num_clusters, dtype=torch.long, device=device)
|
| 366 |
+
for c in range(num_clusters):
|
| 367 |
+
mask = labels == c
|
| 368 |
+
if not mask.any():
|
| 369 |
+
continue
|
| 370 |
+
cluster_voxels = generated_coords[mask]
|
| 371 |
+
mean_pos = cluster_voxels.mean(0, keepdim=True)
|
| 372 |
+
centroid = cluster_voxels[torch.cdist(cluster_voxels, mean_pos).argmin()].unsqueeze(0) # (1, 3) actual voxel
|
| 373 |
+
min_dists = [
|
| 374 |
+
torch.cdist(sq_c, centroid).min().item() if sq_c.shape[0] > 0 else float('inf')
|
| 375 |
+
for sq_c in sq_coords_list
|
| 376 |
+
]
|
| 377 |
+
cluster_to_sq[c] = int(np.argmin(min_dists))
|
| 378 |
+
point_labels = cluster_to_sq[labels]
|
| 379 |
+
dense_coords, inverse = _dense_cell_groups(struct_coords, device=device)
|
| 380 |
+
dense_labels = _aggregate_dense_votes(
|
| 381 |
+
point_labels,
|
| 382 |
+
inverse,
|
| 383 |
+
dense_coords.shape[0],
|
| 384 |
+
n_sq,
|
| 385 |
+
)
|
| 386 |
+
else:
|
| 387 |
+
# Fallback: per-voxel geometric nearest neighbor
|
| 388 |
+
min_distances = _surface_distance_matrix(generated_coords, sq_coords_list)
|
| 389 |
+
dense_coords, inverse = _dense_cell_groups(struct_coords, device=device)
|
| 390 |
+
cell_distances = _aggregate_dense_min_distances(
|
| 391 |
+
min_distances,
|
| 392 |
+
inverse,
|
| 393 |
+
dense_coords.shape[0],
|
| 394 |
+
)
|
| 395 |
+
dense_labels = cell_distances.argmin(dim=0)
|
| 396 |
+
|
| 397 |
+
coords_dense_indices[
|
| 398 |
+
0,
|
| 399 |
+
0,
|
| 400 |
+
dense_coords[:, 0],
|
| 401 |
+
dense_coords[:, 1],
|
| 402 |
+
dense_coords[:, 2],
|
| 403 |
+
] = (dense_labels + 1).to(torch.int32)
|
| 404 |
+
|
| 405 |
+
return coords_dense_indices
|
| 406 |
+
|
| 407 |
+
def _dense_sq_counts(coords_dense_indices, n_sq):
|
| 408 |
+
flat = coords_dense_indices.reshape(-1)
|
| 409 |
+
return {
|
| 410 |
+
sq_idx: int((flat == sq_idx + 1).sum().item())
|
| 411 |
+
for sq_idx in range(n_sq)
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
def _dense_condition_counts(coords_dense_indices, n_conditions):
|
| 415 |
+
flat = coords_dense_indices.reshape(-1)
|
| 416 |
+
return {
|
| 417 |
+
cond_idx: int((flat == cond_idx).sum().item())
|
| 418 |
+
for cond_idx in range(1, n_conditions)
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
def _active_route_coverage(sq_counts, active_indices):
|
| 422 |
+
counts = [sq_counts.get(idx, 0) for idx in active_indices]
|
| 423 |
+
return sum(count > 0 for count in counts), sum(counts)
|
| 424 |
+
|
| 425 |
+
def _format_nonzero_counts(counts):
|
| 426 |
+
nonzero = {idx: count for idx, count in counts.items() if count > 0}
|
| 427 |
+
return nonzero if nonzero else {}
|
| 428 |
+
|
| 429 |
+
def _write_routing_warning(output_dir, warning):
|
| 430 |
+
warning_path = osp.join(output_dir, "routing_warnings.json")
|
| 431 |
+
try:
|
| 432 |
+
if osp.isfile(warning_path):
|
| 433 |
+
with open(warning_path, "r", encoding="utf-8") as fh:
|
| 434 |
+
payload = json.load(fh)
|
| 435 |
+
else:
|
| 436 |
+
payload = {"warnings": []}
|
| 437 |
+
warnings = payload.get("warnings")
|
| 438 |
+
if not isinstance(warnings, list):
|
| 439 |
+
warnings = []
|
| 440 |
+
payload["warnings"] = warnings
|
| 441 |
+
warnings.append(warning)
|
| 442 |
+
with open(warning_path, "w", encoding="utf-8") as fh:
|
| 443 |
+
json.dump(payload, fh, indent=2)
|
| 444 |
+
except Exception as exc: # noqa: BLE001
|
| 445 |
+
log.warning("Could not write routing warning metadata: %s", exc)
|
| 446 |
+
|
| 447 |
+
def _choose_local_routing(
|
| 448 |
+
struct_coords,
|
| 449 |
+
individual_sq_meshes,
|
| 450 |
+
struct_labels,
|
| 451 |
+
active_indices,
|
| 452 |
+
device='cuda',
|
| 453 |
+
routing_mode='legacy',
|
| 454 |
+
routing_margin=2.0,
|
| 455 |
+
surface_min_cells=64,
|
| 456 |
+
surface_max_cells=512,
|
| 457 |
+
surface_candidate_margin=3.0,
|
| 458 |
+
):
|
| 459 |
+
if routing_mode in {'surface_coverage', 'surface_topk'} and active_indices:
|
| 460 |
+
surface_routed, surface_stats = compute_surface_coverage_indices(
|
| 461 |
+
struct_coords,
|
| 462 |
+
individual_sq_meshes,
|
| 463 |
+
active_indices,
|
| 464 |
+
device=device,
|
| 465 |
+
min_cells=surface_min_cells,
|
| 466 |
+
max_cells=surface_max_cells,
|
| 467 |
+
candidate_margin=surface_candidate_margin,
|
| 468 |
+
)
|
| 469 |
+
surface_counts = _dense_sq_counts(surface_routed, len(individual_sq_meshes))
|
| 470 |
+
log.info(
|
| 471 |
+
"Surface-coverage local routing (min=%d, max=%d, margin=%.2f) SQ counts: %s; exact cells: %s",
|
| 472 |
+
int(surface_min_cells),
|
| 473 |
+
int(surface_max_cells),
|
| 474 |
+
float(surface_candidate_margin),
|
| 475 |
+
_format_nonzero_counts(surface_counts),
|
| 476 |
+
surface_stats.get('exact_surface_cells', {}),
|
| 477 |
+
)
|
| 478 |
+
return surface_routed, 'surface_coverage', surface_counts
|
| 479 |
+
|
| 480 |
+
if routing_mode == 'active_margin' and active_indices:
|
| 481 |
+
margin_routed = compute_active_margin_indices(
|
| 482 |
+
struct_coords,
|
| 483 |
+
individual_sq_meshes,
|
| 484 |
+
active_indices,
|
| 485 |
+
device=device,
|
| 486 |
+
margin=routing_margin,
|
| 487 |
+
)
|
| 488 |
+
margin_counts = _dense_sq_counts(margin_routed, len(individual_sq_meshes))
|
| 489 |
+
log.info(
|
| 490 |
+
"Active-margin local routing (margin=%.2f) SQ counts: %s",
|
| 491 |
+
routing_margin,
|
| 492 |
+
_format_nonzero_counts(margin_counts),
|
| 493 |
+
)
|
| 494 |
+
return margin_routed, 'active_margin', margin_counts
|
| 495 |
+
|
| 496 |
+
clustered = compute_coords_dense_indices(
|
| 497 |
+
struct_coords, individual_sq_meshes, device, vox_cluster_labels=struct_labels)
|
| 498 |
+
n_sq = len(individual_sq_meshes)
|
| 499 |
+
clustered_counts = _dense_sq_counts(clustered, n_sq)
|
| 500 |
+
log.info(f"PartField local routing SQ counts: {_format_nonzero_counts(clustered_counts)}")
|
| 501 |
+
|
| 502 |
+
if not active_indices:
|
| 503 |
+
return clustered, "partfield", clustered_counts
|
| 504 |
+
|
| 505 |
+
clustered_coverage = _active_route_coverage(clustered_counts, active_indices)
|
| 506 |
+
if clustered_coverage[0] == len(active_indices):
|
| 507 |
+
return clustered, "partfield", clustered_counts
|
| 508 |
+
|
| 509 |
+
geometric = compute_coords_dense_indices(
|
| 510 |
+
struct_coords, individual_sq_meshes, device, vox_cluster_labels=None)
|
| 511 |
+
geometric_counts = _dense_sq_counts(geometric, n_sq)
|
| 512 |
+
geometric_coverage = _active_route_coverage(geometric_counts, active_indices)
|
| 513 |
+
log.info(f"Geometric local routing SQ counts: {_format_nonzero_counts(geometric_counts)}")
|
| 514 |
+
|
| 515 |
+
if geometric_coverage > clustered_coverage:
|
| 516 |
+
missing = [idx for idx in active_indices if clustered_counts.get(idx, 0) == 0]
|
| 517 |
+
log.warning(
|
| 518 |
+
"PartField local routing missed active SQ(s) %s; using geometric routing "
|
| 519 |
+
"for local texture conditions instead.",
|
| 520 |
+
missing,
|
| 521 |
+
)
|
| 522 |
+
return geometric, "geometric", geometric_counts
|
| 523 |
+
|
| 524 |
+
missing = [idx for idx in active_indices if clustered_counts.get(idx, 0) == 0]
|
| 525 |
+
if missing:
|
| 526 |
+
log.warning(
|
| 527 |
+
"PartField local routing missed active SQ(s) %s, and geometric routing "
|
| 528 |
+
"did not improve coverage; keeping PartField routing.",
|
| 529 |
+
missing,
|
| 530 |
+
)
|
| 531 |
+
return clustered, "partfield", clustered_counts
|
| 532 |
+
|
| 533 |
+
def _dilate_condition_indices(coords_dense_indices, n_conditions, radius=1):
|
| 534 |
+
if radius <= 0 or n_conditions <= 1:
|
| 535 |
+
return coords_dense_indices
|
| 536 |
+
|
| 537 |
+
dilated_indices = coords_dense_indices.clone()
|
| 538 |
+
kernel_size = 2 * radius + 1
|
| 539 |
+
for cond_idx in range(1, n_conditions):
|
| 540 |
+
mask = (coords_dense_indices == cond_idx).float()
|
| 541 |
+
dilated = F.max_pool3d(mask, kernel_size=kernel_size, stride=1, padding=radius) > 0
|
| 542 |
+
dilated_indices[(dilated_indices == 0) & dilated] = cond_idx
|
| 543 |
+
return dilated_indices
|
| 544 |
+
|
| 545 |
+
def _nonnegative_int(value, default=0):
|
| 546 |
+
if value is None:
|
| 547 |
+
return default
|
| 548 |
+
try:
|
| 549 |
+
return max(0, int(value))
|
| 550 |
+
except (TypeError, ValueError):
|
| 551 |
+
return default
|
| 552 |
+
|
| 553 |
+
def _select_compact_candidate_cells(mask, candidates, needed):
|
| 554 |
+
if needed <= 0:
|
| 555 |
+
return candidates[:0]
|
| 556 |
+
candidate_coords = candidates.nonzero(as_tuple=False)
|
| 557 |
+
if candidate_coords.shape[0] <= needed:
|
| 558 |
+
return candidate_coords
|
| 559 |
+
|
| 560 |
+
kernel = torch.ones((1, 1, 3, 3, 3), dtype=mask.dtype, device=mask.device)
|
| 561 |
+
support = F.conv3d(mask, kernel, stride=1, padding=1)[candidates]
|
| 562 |
+
source_coords = mask.nonzero(as_tuple=False)[:, 2:].float()
|
| 563 |
+
centroid = source_coords.mean(dim=0, keepdim=True)
|
| 564 |
+
spatial = candidate_coords[:, 2:].float()
|
| 565 |
+
dist2 = ((spatial - centroid) ** 2).sum(dim=1)
|
| 566 |
+
linear = (
|
| 567 |
+
candidate_coords[:, 2] * mask.shape[-2] * mask.shape[-1]
|
| 568 |
+
+ candidate_coords[:, 3] * mask.shape[-1]
|
| 569 |
+
+ candidate_coords[:, 4]
|
| 570 |
+
).float()
|
| 571 |
+
priority = support.float() * 1_000_000.0 - dist2 - linear / 1_000_000.0
|
| 572 |
+
selected = torch.topk(priority, k=needed, largest=True, sorted=False).indices
|
| 573 |
+
return candidate_coords[selected]
|
| 574 |
+
|
| 575 |
+
def _expand_small_condition_indices(coords_dense_indices, n_conditions, min_cells=0, max_dilation=0):
|
| 576 |
+
"""Grow under-covered local conditions into global cells.
|
| 577 |
+
|
| 578 |
+
The local routing grid is 32^3, so small SQs such as wheels can collapse to
|
| 579 |
+
very few routed cells. This keeps local prompts active by expanding only
|
| 580 |
+
conditions that are below min_cells, never overwriting another local prompt.
|
| 581 |
+
"""
|
| 582 |
+
min_cells = _nonnegative_int(min_cells, 0)
|
| 583 |
+
max_dilation = _nonnegative_int(max_dilation, 0)
|
| 584 |
+
if min_cells <= 0 or max_dilation <= 0 or n_conditions <= 1:
|
| 585 |
+
return coords_dense_indices, {}
|
| 586 |
+
|
| 587 |
+
expanded_indices = coords_dense_indices.clone()
|
| 588 |
+
stats = {}
|
| 589 |
+
for cond_idx in range(1, n_conditions):
|
| 590 |
+
initial_count = int((expanded_indices == cond_idx).sum().item())
|
| 591 |
+
stats[cond_idx] = {
|
| 592 |
+
"initial": initial_count,
|
| 593 |
+
"final": initial_count,
|
| 594 |
+
"dilation": 0,
|
| 595 |
+
"added": 0,
|
| 596 |
+
}
|
| 597 |
+
if initial_count <= 0 or initial_count >= min_cells:
|
| 598 |
+
continue
|
| 599 |
+
|
| 600 |
+
count = initial_count
|
| 601 |
+
for radius in range(1, max_dilation + 1):
|
| 602 |
+
mask = (expanded_indices == cond_idx).float()
|
| 603 |
+
grown = F.max_pool3d(mask, kernel_size=3, stride=1, padding=1) > 0
|
| 604 |
+
candidates = grown & (expanded_indices == 0)
|
| 605 |
+
if not candidates.any():
|
| 606 |
+
break
|
| 607 |
+
needed = min_cells - count
|
| 608 |
+
selected = _select_compact_candidate_cells(mask, candidates, needed)
|
| 609 |
+
expanded_indices[
|
| 610 |
+
selected[:, 0],
|
| 611 |
+
selected[:, 1],
|
| 612 |
+
selected[:, 2],
|
| 613 |
+
selected[:, 3],
|
| 614 |
+
selected[:, 4],
|
| 615 |
+
] = cond_idx
|
| 616 |
+
count = int((expanded_indices == cond_idx).sum().item())
|
| 617 |
+
stats[cond_idx]["final"] = count
|
| 618 |
+
stats[cond_idx]["dilation"] = radius
|
| 619 |
+
stats[cond_idx]["added"] = count - initial_count
|
| 620 |
+
if count >= min_cells:
|
| 621 |
+
break
|
| 622 |
+
|
| 623 |
+
return expanded_indices, stats
|
| 624 |
+
|
| 625 |
+
def attn_cosine_sim(x, eps=1e-08):
|
| 626 |
+
x = x[0] # TEMP: getting rid of redundant dimension, TBF
|
| 627 |
+
norm1 = x.norm(dim=2, keepdim=True)
|
| 628 |
+
factor = torch.clamp(norm1 @ norm1.permute(0, 2, 1), min=eps)
|
| 629 |
+
sim_matrix = (x @ x.permute(0, 2, 1)) / factor
|
| 630 |
+
return sim_matrix
|
| 631 |
+
|
| 632 |
+
def _auto_contrastive_chunk_size(num_voxels):
|
| 633 |
+
if num_voxels <= 16_000:
|
| 634 |
+
return 1024
|
| 635 |
+
if num_voxels <= 28_000:
|
| 636 |
+
return 512
|
| 637 |
+
return 256
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
def chunked_contrastive_loss(feats, labels, eps=1e-08, chunk_size=None):
|
| 641 |
+
"""Supervised contrastive loss without materializing the full N×N similarity matrix.
|
| 642 |
+
Peak memory is O(chunk_size × N) instead of O(N²).
|
| 643 |
+
"""
|
| 644 |
+
# feats: (1, 1, N, C) -> (N, C)
|
| 645 |
+
x = feats[0, 0]
|
| 646 |
+
x_norm = x / x.norm(dim=1, keepdim=True).clamp(min=eps) # (N, C), L2-normalized
|
| 647 |
+
labels = labels.view(-1) # (N,)
|
| 648 |
+
N = x_norm.shape[0]
|
| 649 |
+
chunk_size = chunk_size or _auto_contrastive_chunk_size(N)
|
| 650 |
+
|
| 651 |
+
numerator = torch.zeros(N, device=x.device)
|
| 652 |
+
denominator = torch.zeros(N, device=x.device)
|
| 653 |
+
valid = torch.zeros(N, dtype=torch.bool, device=x.device)
|
| 654 |
+
|
| 655 |
+
for i in range(0, N, chunk_size):
|
| 656 |
+
i_end = min(i + chunk_size, N)
|
| 657 |
+
sim_row = x_norm[i:i_end] @ x_norm.T # (cs, N)
|
| 658 |
+
exp_sim = torch.exp(sim_row)
|
| 659 |
+
|
| 660 |
+
labels_i = labels[i:i_end].unsqueeze(1) # (cs, 1)
|
| 661 |
+
same_label = labels_i == labels.unsqueeze(0) # (cs, N)
|
| 662 |
+
|
| 663 |
+
# Exclude self-similarity on diagonal
|
| 664 |
+
diag_idx = torch.arange(i_end - i, device=x.device)
|
| 665 |
+
self_mask = torch.zeros_like(same_label)
|
| 666 |
+
self_mask[diag_idx, i + diag_idx] = True
|
| 667 |
+
positive_mask = same_label & ~self_mask
|
| 668 |
+
|
| 669 |
+
denominator[i:i_end] = exp_sim.masked_fill(self_mask, 0.0).sum(dim=1)
|
| 670 |
+
valid[i:i_end] = positive_mask.any(dim=1)
|
| 671 |
+
numerator[i:i_end] = exp_sim.masked_fill(~positive_mask, 0.0).sum(dim=1)
|
| 672 |
+
|
| 673 |
+
loss = -torch.log(numerator / (denominator + 1e-8))
|
| 674 |
+
return loss[valid].mean()
|
| 675 |
+
|
| 676 |
+
def optimize_self_similarity(cfg, app, app_type, output_dir,
|
| 677 |
+
local_prompts=None, local_prompt_type=None,
|
| 678 |
+
individual_sq_meshes=None,
|
| 679 |
+
generation_pipeline=None,
|
| 680 |
+
decode_texture=True):
|
| 681 |
+
overall_start = time.perf_counter()
|
| 682 |
+
log.info("Starting self-similarity optimization...")
|
| 683 |
+
|
| 684 |
+
pipeline_start = time.perf_counter()
|
| 685 |
+
if not can_reuse_appearance_pipeline(generation_pipeline, app_type):
|
| 686 |
+
if generation_pipeline is not None:
|
| 687 |
+
log.info("Loading the image appearance pipeline; the structure/text pipeline has no image preprocessing interface.")
|
| 688 |
+
if app_type == 'image':
|
| 689 |
+
generation_pipeline = TrellisImageTo3DPipeline.from_pretrained(cfg.trellis_img_model_name)
|
| 690 |
+
else:
|
| 691 |
+
generation_pipeline = TrellisTextTo3DPipeline.from_pretrained(cfg.trellis_text_model_name)
|
| 692 |
+
else:
|
| 693 |
+
log.info("Reusing preloaded TRELLIS pipeline for self-similarity guidance.")
|
| 694 |
+
generation_pipeline.cuda()
|
| 695 |
+
_text_conditioner_to(generation_pipeline, 'cuda')
|
| 696 |
+
log.info("Prepared TRELLIS pipeline for self-similarity in %.2fs", time.perf_counter() - pipeline_start)
|
| 697 |
+
|
| 698 |
+
if app_type == 'image':
|
| 699 |
+
app = _preprocess_condition_image(generation_pipeline, osp.join(output_dir, 'app_image.png'))
|
| 700 |
+
|
| 701 |
+
# Load Structure Data
|
| 702 |
+
structure_start = time.perf_counter()
|
| 703 |
+
struct_coords = utils3d.io.read_ply(osp.join(output_dir, 'voxels', 'struct_voxels.ply'))[0]
|
| 704 |
+
struct_coords = torch.from_numpy(struct_coords).float().cuda()
|
| 705 |
+
struct_coords = ((struct_coords + 0.5) * 64).long()
|
| 706 |
+
|
| 707 |
+
zeros = torch.zeros((struct_coords.size(0), 1), dtype=struct_coords.dtype, device=struct_coords.device)
|
| 708 |
+
struct_coords = torch.cat([zeros, struct_coords], dim=1)
|
| 709 |
+
|
| 710 |
+
# Load partfield planes (extracted from Blender's struct_renders/mesh.ply — same mesh as voxels)
|
| 711 |
+
path = osp.join(output_dir, "partfield", "part_feat_mesh_batch_part_plane.npy")
|
| 712 |
+
struct_part_planes = torch.from_numpy(np.load(path, allow_pickle=True)).cuda()
|
| 713 |
+
|
| 714 |
+
struct_labels = partfield.cluster_geoms(struct_coords, struct_part_planes, num_clusters=cfg.sim_guidance.num_part_clusters)
|
| 715 |
+
|
| 716 |
+
# Optimization Starts...
|
| 717 |
+
struct_labels = torch.from_numpy(struct_labels.flatten()).cuda()
|
| 718 |
+
struct_feats_params = torch.nn.Parameter(torch.randn((struct_coords.shape[0], cfg.flow_model_in_channels)), requires_grad=True)
|
| 719 |
+
log.info(
|
| 720 |
+
"Loaded structure voxels, PartField planes, and labels in %.2fs (%d voxels)",
|
| 721 |
+
time.perf_counter() - structure_start,
|
| 722 |
+
struct_coords.shape[0],
|
| 723 |
+
)
|
| 724 |
+
|
| 725 |
+
param_list = [struct_feats_params]
|
| 726 |
+
optimizer = torch.optim.AdamW(param_list, lr=cfg.sim_guidance.learning_rate)
|
| 727 |
+
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda x: 1)
|
| 728 |
+
|
| 729 |
+
best_loss = float('inf')
|
| 730 |
+
feats = None
|
| 731 |
+
|
| 732 |
+
cond_start = time.perf_counter()
|
| 733 |
+
if app_type == "image":
|
| 734 |
+
cond = generation_pipeline.get_cond([app]) if hasattr(generation_pipeline, 'get_cond') else generation_pipeline.get_cond_image([app])
|
| 735 |
+
else:
|
| 736 |
+
cond = generation_pipeline.get_cond_text([app])
|
| 737 |
+
log.info("Encoded global %s guidance condition in %.2fs", app_type, time.perf_counter() - cond_start)
|
| 738 |
+
|
| 739 |
+
# Build per-SQ local conditioning if local prompts were provided.
|
| 740 |
+
cond_list = None
|
| 741 |
+
coords_dense_indices = None
|
| 742 |
+
if local_prompts and individual_sq_meshes:
|
| 743 |
+
local_start = time.perf_counter()
|
| 744 |
+
global_emb = cond['cond'] # (1, seq_len, dim)
|
| 745 |
+
n_sq = len(local_prompts)
|
| 746 |
+
active_indices = [
|
| 747 |
+
idx
|
| 748 |
+
for idx, prompt in enumerate(local_prompts)
|
| 749 |
+
if str(prompt or "").strip()
|
| 750 |
+
]
|
| 751 |
+
log.info(
|
| 752 |
+
f"Building local {local_prompt_type} conditioning for "
|
| 753 |
+
f"{len(active_indices)} / {n_sq} superquadrics; active SQ indices: {active_indices}"
|
| 754 |
+
)
|
| 755 |
+
if local_prompt_type == 'text':
|
| 756 |
+
local_embs = []
|
| 757 |
+
for sq_idx, prompt in enumerate(local_prompts):
|
| 758 |
+
prompt = str(prompt or "").strip()
|
| 759 |
+
if prompt:
|
| 760 |
+
log.info(
|
| 761 |
+
"Local text condition for SQ %d: %r",
|
| 762 |
+
sq_idx,
|
| 763 |
+
prompt,
|
| 764 |
+
)
|
| 765 |
+
local_embs.append(
|
| 766 |
+
generation_pipeline.encode_text([prompt])
|
| 767 |
+
if prompt
|
| 768 |
+
else global_emb
|
| 769 |
+
)
|
| 770 |
+
else: # 'image'
|
| 771 |
+
local_embs = []
|
| 772 |
+
for sq_idx, path in enumerate(local_prompts):
|
| 773 |
+
path = str(path or '').strip()
|
| 774 |
+
if path:
|
| 775 |
+
log.info(
|
| 776 |
+
"Local image condition for SQ %d: %s",
|
| 777 |
+
sq_idx,
|
| 778 |
+
path,
|
| 779 |
+
)
|
| 780 |
+
local_image = _preprocess_condition_image(generation_pipeline, path)
|
| 781 |
+
local_embs.append(generation_pipeline.encode_image([local_image]))
|
| 782 |
+
else:
|
| 783 |
+
local_embs.append(global_emb)
|
| 784 |
+
# Only include SQs that have a real (non-global-fallback) local condition.
|
| 785 |
+
conditioned_mask = [emb is not global_emb for emb in local_embs]
|
| 786 |
+
conditioned_embs = [emb for emb, is_cond in zip(local_embs, conditioned_mask) if is_cond]
|
| 787 |
+
cond_list = [global_emb] + conditioned_embs
|
| 788 |
+
|
| 789 |
+
# Build remap: SQ 1-index → new condition index (0=global fallback, 1..n_conditioned).
|
| 790 |
+
remap = torch.zeros(n_sq + 1, dtype=torch.long, device='cuda')
|
| 791 |
+
new_idx = 1
|
| 792 |
+
condition_to_sq = {}
|
| 793 |
+
for sq_i, is_cond in enumerate(conditioned_mask):
|
| 794 |
+
remap[sq_i + 1] = new_idx if is_cond else 0
|
| 795 |
+
if is_cond:
|
| 796 |
+
condition_to_sq[new_idx] = sq_i
|
| 797 |
+
new_idx += 1
|
| 798 |
+
sq_cond_map = remap[1:].tolist() # condition index per SQ (0=global)
|
| 799 |
+
log.info(f"SQ condition mapping (0=global): {sq_cond_map}")
|
| 800 |
+
log.info(f"Built cond_list with {len(cond_list)} entries "
|
| 801 |
+
f"(1 global + {len(conditioned_embs)} real local out of {n_sq} SQs)")
|
| 802 |
+
|
| 803 |
+
local_routing_mode = str(
|
| 804 |
+
getattr(cfg.sim_guidance, 'local_routing_mode', 'legacy')
|
| 805 |
+
).strip().lower()
|
| 806 |
+
local_routing_margin = float(
|
| 807 |
+
getattr(cfg.sim_guidance, 'local_routing_margin', 2.0)
|
| 808 |
+
)
|
| 809 |
+
surface_min_cells = _nonnegative_int(
|
| 810 |
+
getattr(cfg.sim_guidance, 'surface_min_cells', 64), 64
|
| 811 |
+
)
|
| 812 |
+
surface_max_cells = _nonnegative_int(
|
| 813 |
+
getattr(cfg.sim_guidance, 'surface_max_cells', 512), 512
|
| 814 |
+
)
|
| 815 |
+
surface_candidate_margin = float(
|
| 816 |
+
getattr(cfg.sim_guidance, 'surface_candidate_margin', 3.0)
|
| 817 |
+
)
|
| 818 |
+
coords_dense_indices, routing_source, sq_route_counts = _choose_local_routing(
|
| 819 |
+
struct_coords,
|
| 820 |
+
individual_sq_meshes,
|
| 821 |
+
struct_labels,
|
| 822 |
+
active_indices,
|
| 823 |
+
device='cuda',
|
| 824 |
+
routing_mode=local_routing_mode,
|
| 825 |
+
routing_margin=local_routing_margin,
|
| 826 |
+
surface_min_cells=surface_min_cells,
|
| 827 |
+
surface_max_cells=surface_max_cells,
|
| 828 |
+
surface_candidate_margin=surface_candidate_margin,
|
| 829 |
+
)
|
| 830 |
+
coords_dense_indices = remap[coords_dense_indices.long()]
|
| 831 |
+
condition_counts_before_dilation = _dense_condition_counts(coords_dense_indices, len(cond_list))
|
| 832 |
+
local_condition_dilation = _nonnegative_int(
|
| 833 |
+
getattr(cfg.sim_guidance, 'local_condition_dilation', 0), 0)
|
| 834 |
+
coords_dense_indices = _dilate_condition_indices(
|
| 835 |
+
coords_dense_indices, len(cond_list), radius=local_condition_dilation)
|
| 836 |
+
condition_counts_after_dilation = _dense_condition_counts(coords_dense_indices, len(cond_list))
|
| 837 |
+
local_condition_min_cells = _nonnegative_int(
|
| 838 |
+
getattr(cfg.sim_guidance, 'local_condition_min_cells', 0), 0)
|
| 839 |
+
local_condition_max_dilation = _nonnegative_int(
|
| 840 |
+
getattr(cfg.sim_guidance, 'local_condition_max_dilation', 0), 0)
|
| 841 |
+
coords_dense_indices, adaptive_expansion_stats = _expand_small_condition_indices(
|
| 842 |
+
coords_dense_indices,
|
| 843 |
+
len(cond_list),
|
| 844 |
+
min_cells=local_condition_min_cells,
|
| 845 |
+
max_dilation=local_condition_max_dilation,
|
| 846 |
+
)
|
| 847 |
+
condition_counts = _dense_condition_counts(coords_dense_indices, len(cond_list))
|
| 848 |
+
missing_condition_sqs = [
|
| 849 |
+
condition_to_sq.get(cond_idx, 'unknown')
|
| 850 |
+
for cond_idx in range(1, len(cond_list))
|
| 851 |
+
if condition_counts.get(cond_idx, 0) == 0
|
| 852 |
+
]
|
| 853 |
+
if missing_condition_sqs:
|
| 854 |
+
missing_sq_numbers = [
|
| 855 |
+
sq_idx + 1 if isinstance(sq_idx, int) else sq_idx
|
| 856 |
+
for sq_idx in missing_condition_sqs
|
| 857 |
+
]
|
| 858 |
+
sq_label = ", ".join(str(sq_num) for sq_num in missing_sq_numbers)
|
| 859 |
+
warning_message = (
|
| 860 |
+
f"Local {local_prompt_type} prompt(s) for SQs {sq_label} "
|
| 861 |
+
"received zero routed cells; those local overrides had no effect."
|
| 862 |
+
)
|
| 863 |
+
log.warning(
|
| 864 |
+
"Local %s condition(s) for SQ(s) %s have zero routed cells; "
|
| 865 |
+
"those local overrides will have no effect. Check condition_routing.mp4 "
|
| 866 |
+
"or enlarge/adjust the selected superquadric.",
|
| 867 |
+
local_prompt_type,
|
| 868 |
+
missing_condition_sqs,
|
| 869 |
+
)
|
| 870 |
+
_write_routing_warning(
|
| 871 |
+
output_dir,
|
| 872 |
+
{
|
| 873 |
+
"kind": "local_routing_zero_cells",
|
| 874 |
+
"severity": "warning",
|
| 875 |
+
"message": warning_message,
|
| 876 |
+
"local_prompt_type": local_prompt_type,
|
| 877 |
+
"sq_indices": [
|
| 878 |
+
sq_idx for sq_idx in missing_condition_sqs
|
| 879 |
+
if isinstance(sq_idx, int)
|
| 880 |
+
],
|
| 881 |
+
"sq_numbers": missing_sq_numbers,
|
| 882 |
+
"routing_source": routing_source,
|
| 883 |
+
"active_sq_counts": {
|
| 884 |
+
str(idx + 1): int(sq_route_counts.get(idx, 0))
|
| 885 |
+
for idx in active_indices
|
| 886 |
+
},
|
| 887 |
+
"condition_counts": {
|
| 888 |
+
str(cond_idx): int(count)
|
| 889 |
+
for cond_idx, count in condition_counts.items()
|
| 890 |
+
},
|
| 891 |
+
},
|
| 892 |
+
)
|
| 893 |
+
active_sq_counts = {idx: sq_route_counts.get(idx, 0) for idx in active_indices}
|
| 894 |
+
log.info(
|
| 895 |
+
f"Local routing source: {routing_source}; active SQ counts: {active_sq_counts}"
|
| 896 |
+
)
|
| 897 |
+
if adaptive_expansion_stats:
|
| 898 |
+
active_expansions = {
|
| 899 |
+
f"cond_{cond_idx}_sq_{condition_to_sq.get(cond_idx, 'unknown')}": values
|
| 900 |
+
for cond_idx, values in adaptive_expansion_stats.items()
|
| 901 |
+
if values["dilation"] > 0 or values["initial"] < local_condition_min_cells
|
| 902 |
+
}
|
| 903 |
+
if active_expansions:
|
| 904 |
+
log.info(
|
| 905 |
+
"Adaptive local condition expansion: min_cells=%d, max_dilation=%d, stats=%s",
|
| 906 |
+
local_condition_min_cells,
|
| 907 |
+
local_condition_max_dilation,
|
| 908 |
+
active_expansions,
|
| 909 |
+
)
|
| 910 |
+
log.info(
|
| 911 |
+
f"coords_dense_indices: shape={coords_dense_indices.shape}, "
|
| 912 |
+
f"condition_counts_before_dilation={condition_counts_before_dilation}, "
|
| 913 |
+
f"condition_counts_after_dilation={condition_counts_after_dilation}, "
|
| 914 |
+
f"dilation_radius={local_condition_dilation}, "
|
| 915 |
+
f"adaptive_min_cells={local_condition_min_cells}, "
|
| 916 |
+
f"adaptive_max_dilation={local_condition_max_dilation}, "
|
| 917 |
+
f"condition_counts={condition_counts}, non-zero={int((coords_dense_indices > 0).sum())}"
|
| 918 |
+
)
|
| 919 |
+
|
| 920 |
+
log.info("Skipping condition routing visualization; routing indices are still used for local conditioning.")
|
| 921 |
+
torch.cuda.empty_cache()
|
| 922 |
+
log.info("Prepared local condition routing in %.2fs", time.perf_counter() - local_start)
|
| 923 |
+
|
| 924 |
+
# condition routing visualization for routing
|
| 925 |
+
log.info("Visualizing condition routing on structure mesh...")
|
| 926 |
+
try:
|
| 927 |
+
if _KEEP_ON_GPU:
|
| 928 |
+
raise RuntimeError("routing visualization disabled in the Space")
|
| 929 |
+
import trimesh
|
| 930 |
+
from lib.util.visualization import visualize_and_save, map_voxel_labels_to_vertices
|
| 931 |
+
_mesh_path = osp.join(output_dir, 'struct_renders', 'mesh.ply')
|
| 932 |
+
if not osp.isfile(_mesh_path):
|
| 933 |
+
_mesh_path = osp.join(output_dir, 'spatial_control_mesh.ply')
|
| 934 |
+
if osp.isfile(_mesh_path):
|
| 935 |
+
_sv_norm = ((struct_coords[:, 1:].float() + 0.5) / 64 - 0.5).cpu().numpy()
|
| 936 |
+
_sq_labels = coords_dense_indices[0, 0,
|
| 937 |
+
struct_coords[:, 1] // 2,
|
| 938 |
+
struct_coords[:, 2] // 2,
|
| 939 |
+
struct_coords[:, 3] // 2].cpu().numpy()
|
| 940 |
+
_mesh_vis = trimesh.load(_mesh_path, force='mesh')
|
| 941 |
+
_vtx_labels = map_voxel_labels_to_vertices(_mesh_vis.vertices, _sv_norm, _sq_labels)
|
| 942 |
+
visualize_and_save(_mesh_vis, _vtx_labels, output_dir, output_name='condition_routing.mp4')
|
| 943 |
+
del _sv_norm, _sq_labels, _mesh_vis, _vtx_labels
|
| 944 |
+
torch.cuda.empty_cache()
|
| 945 |
+
else:
|
| 946 |
+
log.info("No structure mesh found for routing visualization; continuing.")
|
| 947 |
+
except Exception as _vis_err:
|
| 948 |
+
log.warning("Could not render condition routing video: %s; continuing texture optimization.", _vis_err)
|
| 949 |
+
|
| 950 |
+
else:
|
| 951 |
+
log.info("No local SQ texture overrides provided; all voxels use the global condition.")
|
| 952 |
+
|
| 953 |
+
if not _KEEP_ON_GPU:
|
| 954 |
+
_text_conditioner_to(generation_pipeline, 'cpu')
|
| 955 |
+
|
| 956 |
+
flow_model_key, flow_model = _select_slat_flow_model(generation_pipeline, app_type)
|
| 957 |
+
|
| 958 |
+
# get_cond() is done — only the selected SLAT flow model is needed for the loop. Offload everything else.
|
| 959 |
+
for k, m in generation_pipeline.models.items():
|
| 960 |
+
if k != flow_model_key and not _KEEP_ON_GPU:
|
| 961 |
+
m.cpu()
|
| 962 |
+
torch.cuda.empty_cache()
|
| 963 |
+
|
| 964 |
+
if app_type == 'image':
|
| 965 |
+
sampler_params = {
|
| 966 |
+
"cfg_strength": cfg.img_model.cfg_strength,
|
| 967 |
+
"cfg_interval": cfg.img_model.cfg_interval,
|
| 968 |
+
}
|
| 969 |
+
rescale_t = cfg.img_model.rescale_t
|
| 970 |
+
|
| 971 |
+
else:
|
| 972 |
+
sampler_params = {
|
| 973 |
+
"cfg_strength": cfg.text_model.cfg_strength,
|
| 974 |
+
"cfg_interval": cfg.text_model.cfg_interval,
|
| 975 |
+
}
|
| 976 |
+
rescale_t = cfg.text_model.rescale_t
|
| 977 |
+
|
| 978 |
+
t_seq = np.linspace(1, 0, cfg.sim_guidance.steps + 1)
|
| 979 |
+
t_seq = rescale_t * t_seq / (1 + (rescale_t - 1) * t_seq)
|
| 980 |
+
t_pairs = list((t_seq[i], t_seq[i + 1]) for i in range(cfg.sim_guidance.steps))
|
| 981 |
+
|
| 982 |
+
std = torch.tensor(generation_pipeline.slat_normalization['std'])[None].cuda()
|
| 983 |
+
mean = torch.tensor(generation_pipeline.slat_normalization['mean'])[None].cuda()
|
| 984 |
+
|
| 985 |
+
loop_start = time.perf_counter()
|
| 986 |
+
log.info(f"Beginning self-similarity guidance + flow sampling loop for {len(t_pairs)} steps...")
|
| 987 |
+
|
| 988 |
+
self_attn_region = None
|
| 989 |
+
mixed_self_attn_boost = 1.0
|
| 990 |
+
if coords_dense_indices is not None:
|
| 991 |
+
mixed_self_attn_boost = float(
|
| 992 |
+
getattr(cfg.sim_guidance, 'mixed_self_attn_boost', 1.0) or 1.0)
|
| 993 |
+
if mixed_self_attn_boost > 1.0:
|
| 994 |
+
self_attn_region = coords_dense_indices
|
| 995 |
+
log.info(
|
| 996 |
+
"Region-boosted self-attention ON: in-region weights x%.2f before softmax "
|
| 997 |
+
"over %d condition regions, all %d steps.",
|
| 998 |
+
mixed_self_attn_boost, int(coords_dense_indices.max().item()) + 1,
|
| 999 |
+
len(t_pairs),
|
| 1000 |
+
)
|
| 1001 |
+
else:
|
| 1002 |
+
log.info("Region-boosted self-attention off (mixed_self_attn_boost=%.2f); "
|
| 1003 |
+
"self-attention is stock.", mixed_self_attn_boost)
|
| 1004 |
+
|
| 1005 |
+
for iteration, (t, t_prev) in enumerate(t_pairs):
|
| 1006 |
+
optimizer.zero_grad()
|
| 1007 |
+
|
| 1008 |
+
# Diffusion
|
| 1009 |
+
struct_feats_params_clone = struct_feats_params.clone().cuda()
|
| 1010 |
+
noise = sp.SparseTensor(
|
| 1011 |
+
feats = struct_feats_params_clone,
|
| 1012 |
+
coords = struct_coords.int(),
|
| 1013 |
+
).cuda()
|
| 1014 |
+
|
| 1015 |
+
local_kwargs = {}
|
| 1016 |
+
if cond_list is not None:
|
| 1017 |
+
local_kwargs['cond_list'] = cond_list
|
| 1018 |
+
local_kwargs['coords_dense_indices'] = coords_dense_indices
|
| 1019 |
+
if self_attn_region is not None:
|
| 1020 |
+
local_kwargs['self_attn_region'] = self_attn_region
|
| 1021 |
+
local_kwargs['self_attn_region_boost'] = mixed_self_attn_boost
|
| 1022 |
+
|
| 1023 |
+
with torch.no_grad():
|
| 1024 |
+
out = generation_pipeline.slat_sampler.sample_once(flow_model, noise, t, t_prev, **cond, **sampler_params, **local_kwargs)
|
| 1025 |
+
|
| 1026 |
+
sample = out.pred_x_prev
|
| 1027 |
+
struct_feats_params.data = sample.feats
|
| 1028 |
+
|
| 1029 |
+
# guideflow loss put to 0
|
| 1030 |
+
if cfg.sim_guidance.loss_weight == 0:
|
| 1031 |
+
feats = struct_feats_params.detach() * std + mean
|
| 1032 |
+
|
| 1033 |
+
# Optimization - Structure Loss
|
| 1034 |
+
if cfg.sim_guidance.loss_weight > 0 and iteration < len(t_pairs) - 1:
|
| 1035 |
+
struct_loss = chunked_contrastive_loss(struct_feats_params[None, None, ...], struct_labels)
|
| 1036 |
+
|
| 1037 |
+
total_loss = cfg.sim_guidance.loss_weight * struct_loss
|
| 1038 |
+
total_loss.backward()
|
| 1039 |
+
optimizer.step()
|
| 1040 |
+
scheduler.step()
|
| 1041 |
+
|
| 1042 |
+
if (iteration == 0) or (iteration + 1) % cfg.log_every == 0:
|
| 1043 |
+
message = f"Step: {iteration}, Structure Loss: {struct_loss.item():.4f}, Total Loss: {total_loss.item():.4f}"
|
| 1044 |
+
log.info(message)
|
| 1045 |
+
|
| 1046 |
+
if total_loss < best_loss:
|
| 1047 |
+
best_loss = total_loss.item()
|
| 1048 |
+
feats = struct_feats_params.detach() * std + mean
|
| 1049 |
+
log.info(
|
| 1050 |
+
"Completed self-similarity guidance + flow sampling loop in %.2fs",
|
| 1051 |
+
time.perf_counter() - loop_start,
|
| 1052 |
+
)
|
| 1053 |
+
|
| 1054 |
+
# Move SLAT decoders back to GPU for decoding.
|
| 1055 |
+
decoder_load_start = time.perf_counter()
|
| 1056 |
+
decoder_keys = ['slat_decoder_mesh']
|
| 1057 |
+
if decode_texture:
|
| 1058 |
+
decoder_keys.append('slat_decoder_gs')
|
| 1059 |
+
for k in decoder_keys:
|
| 1060 |
+
if k in generation_pipeline.models:
|
| 1061 |
+
generation_pipeline.models[k].cuda()
|
| 1062 |
+
elif decode_texture:
|
| 1063 |
+
raise KeyError(f"Texture decode requested but required decoder is missing: {k}")
|
| 1064 |
+
log.info("Moved SLAT decoder(s) to CUDA in %.2fs: %s", time.perf_counter() - decoder_load_start, decoder_keys)
|
| 1065 |
+
|
| 1066 |
+
# Decode SLAT
|
| 1067 |
+
log.info("Decoding output SLAT...")
|
| 1068 |
+
decode_start = time.perf_counter()
|
| 1069 |
+
out_meshpath = osp.join(output_dir, 'out_sim.glb')
|
| 1070 |
+
generation.decode_slat(generation_pipeline, feats, struct_coords, out_meshpath, None, texture=decode_texture)
|
| 1071 |
+
log.info("Decoded output SLAT in %.2fs: %s", time.perf_counter() - decode_start, out_meshpath)
|
| 1072 |
+
log.info("Completed self-similarity optimization in %.2fs", time.perf_counter() - overall_start)
|
lib/util/checkpoint_compat.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Scoped compatibility for the recorded upstream PartField checkpoint."""
|
| 2 |
+
|
| 3 |
+
from contextlib import contextmanager
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@contextmanager
|
| 7 |
+
def partfield_checkpoint_scope():
|
| 8 |
+
"""Allow the checkpoint's YACS configuration with PyTorch weights-only loading.
|
| 9 |
+
|
| 10 |
+
PartField stores a CfgNode alongside its tensors. Lightning 2.2 delegates
|
| 11 |
+
loading to torch.load, whose default changed in PyTorch 2.6. Keep that
|
| 12 |
+
default and allow only this configuration class while PartField loads.
|
| 13 |
+
"""
|
| 14 |
+
import torch
|
| 15 |
+
from yacs.config import CfgNode
|
| 16 |
+
|
| 17 |
+
with torch.serialization.safe_globals([CfgNode]):
|
| 18 |
+
yield
|
lib/util/common.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import os.path as osp
|
| 4 |
+
import pickle
|
| 5 |
+
import re
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
import torch
|
| 9 |
+
import random
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
def make_dir(dir_path: str) -> None:
|
| 13 |
+
"""Creates a directory if it does not exist."""
|
| 14 |
+
if not Path(dir_path).exists():
|
| 15 |
+
Path(dir_path).mkdir(parents=True, exist_ok=True)
|
| 16 |
+
|
| 17 |
+
def ensure_dir(path: str) -> None:
|
| 18 |
+
"""
|
| 19 |
+
Ensures that a directory exists; creates it if it does not.
|
| 20 |
+
"""
|
| 21 |
+
if not osp.exists(path):
|
| 22 |
+
os.makedirs(path)
|
| 23 |
+
|
| 24 |
+
def assert_dir(path: str) -> None:
|
| 25 |
+
"""Asserts that a directory exists."""
|
| 26 |
+
assert osp.exists(path)
|
| 27 |
+
|
| 28 |
+
def load_pkl_data(filename: str) -> Any:
|
| 29 |
+
"""Loads data from a pickle file."""
|
| 30 |
+
with open(filename, 'rb') as handle:
|
| 31 |
+
data_dict = pickle.load(handle)
|
| 32 |
+
return data_dict
|
| 33 |
+
|
| 34 |
+
def write_pkl_data(data_dict: Any, filename: str) -> None:
|
| 35 |
+
"""Writes data to a pickle file."""
|
| 36 |
+
with open(filename, 'wb') as handle:
|
| 37 |
+
pickle.dump(data_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
| 38 |
+
|
| 39 |
+
def load_json(filename: str) -> Any:
|
| 40 |
+
"""Loads data from a JSON file."""
|
| 41 |
+
file = open(filename)
|
| 42 |
+
data = json.load(file)
|
| 43 |
+
file.close()
|
| 44 |
+
return data
|
| 45 |
+
|
| 46 |
+
def write_json(data_dict: Any, filename: str) -> None:
|
| 47 |
+
"""Writes data to a JSON file with indentation."""
|
| 48 |
+
json_obj = json.dumps(data_dict, indent=4)
|
| 49 |
+
|
| 50 |
+
with open(filename, "w") as outfile:
|
| 51 |
+
outfile.write(json_obj)
|
| 52 |
+
|
| 53 |
+
def get_print_format(value: Any) -> str:
|
| 54 |
+
"""Determines the appropriate format string for a given value."""
|
| 55 |
+
if isinstance(value, int):
|
| 56 |
+
return 'd'
|
| 57 |
+
if isinstance(value, str):
|
| 58 |
+
return 's'
|
| 59 |
+
if value == 0:
|
| 60 |
+
return '.3f'
|
| 61 |
+
if value < 1e-6:
|
| 62 |
+
return '.3e'
|
| 63 |
+
if value < 1e-3:
|
| 64 |
+
return '.6f'
|
| 65 |
+
return '.6f'
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def get_format_strings(kv_pairs: list) -> list:
|
| 69 |
+
"""Generates format strings for a list of key-value pairs."""
|
| 70 |
+
log_strings = []
|
| 71 |
+
for key, value in kv_pairs:
|
| 72 |
+
fmt = get_print_format(value)
|
| 73 |
+
format_string = '{}: {:' + fmt + '}'
|
| 74 |
+
log_strings.append(format_string.format(key, value))
|
| 75 |
+
return log_strings
|
| 76 |
+
|
| 77 |
+
def get_first_index_batch(x: Any) -> Any:
|
| 78 |
+
"""Retrieves the first index from a batch, handling different data types."""
|
| 79 |
+
if isinstance(x, list):
|
| 80 |
+
x = x[0]
|
| 81 |
+
elif isinstance(x, torch.Tensor):
|
| 82 |
+
x = x.squeeze(0)
|
| 83 |
+
elif isinstance(x, dict):
|
| 84 |
+
x = {key: get_first_index_batch(value) for key, value in x.items()}
|
| 85 |
+
return x
|
| 86 |
+
|
| 87 |
+
def split_sentence(sentence: str) -> list:
|
| 88 |
+
"""Splits a sentence into individual sentences based on periods."""
|
| 89 |
+
sentence = re.split(r'[.]', sentence)
|
| 90 |
+
sentence = [s.strip() for s in sentence]
|
| 91 |
+
sentence = [s for s in sentence if len(s) > 0]
|
| 92 |
+
return sentence
|
| 93 |
+
|
| 94 |
+
def set_random_seed(seed: int) -> None:
|
| 95 |
+
"""Sets the random seed for reproducibility."""
|
| 96 |
+
random.seed(seed)
|
| 97 |
+
np.random.seed(seed)
|
| 98 |
+
torch.manual_seed(seed)
|
| 99 |
+
torch.cuda.manual_seed_all(seed)
|
| 100 |
+
torch.backends.cudnn.deterministic = True
|
| 101 |
+
torch.backends.cudnn.benchmark = False
|
lib/util/generation.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import numpy as np
|
| 4 |
+
import imageio
|
| 5 |
+
import trimesh
|
| 6 |
+
|
| 7 |
+
import third_party.TRELLIS.trellis.modules.sparse as sp
|
| 8 |
+
from third_party.TRELLIS.trellis.utils import render_utils, postprocessing_utils
|
| 9 |
+
|
| 10 |
+
def _white_geometry_mesh(vertices, faces):
|
| 11 |
+
mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
|
| 12 |
+
mesh.visual = trimesh.visual.TextureVisuals(
|
| 13 |
+
material=trimesh.visual.material.PBRMaterial(
|
| 14 |
+
name='geometry_white',
|
| 15 |
+
baseColorFactor=[1.0, 1.0, 1.0, 1.0],
|
| 16 |
+
metallicFactor=0.0,
|
| 17 |
+
roughnessFactor=0.85,
|
| 18 |
+
)
|
| 19 |
+
)
|
| 20 |
+
mesh.vertex_normals
|
| 21 |
+
mesh.face_normals
|
| 22 |
+
return mesh
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _postprocess_mesh_geometry(mesh, simplify=0.95):
|
| 26 |
+
vertices = mesh.vertices.cpu().numpy()
|
| 27 |
+
faces = mesh.faces.cpu().numpy()
|
| 28 |
+
vertices, faces = postprocessing_utils.postprocess_mesh(
|
| 29 |
+
vertices,
|
| 30 |
+
faces,
|
| 31 |
+
simplify=simplify > 0,
|
| 32 |
+
simplify_ratio=simplify,
|
| 33 |
+
fill_holes=True,
|
| 34 |
+
fill_holes_max_hole_size=0.04,
|
| 35 |
+
fill_holes_max_hole_nbe=int(250 * np.sqrt(1 - simplify)),
|
| 36 |
+
fill_holes_resolution=1024,
|
| 37 |
+
fill_holes_num_views=1000,
|
| 38 |
+
debug=False,
|
| 39 |
+
verbose=False,
|
| 40 |
+
)
|
| 41 |
+
return vertices, faces
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def decode_slat(generation_pipeline, feats, coords, out_meshpath, out_gspath, texture=True):
|
| 45 |
+
# Decode Output SLAT
|
| 46 |
+
slat = sp.SparseTensor(
|
| 47 |
+
feats = feats.float(),
|
| 48 |
+
coords = coords.int(),
|
| 49 |
+
).cuda()
|
| 50 |
+
formats = ['mesh', 'gaussian'] if texture else ['mesh']
|
| 51 |
+
with torch.no_grad():
|
| 52 |
+
outputs = generation_pipeline.decode_slat(slat, formats)
|
| 53 |
+
|
| 54 |
+
out_geometry_path = f"{os.path.splitext(out_meshpath)[0]}_geometry.glb"
|
| 55 |
+
|
| 56 |
+
if texture:
|
| 57 |
+
mesh_textured = postprocessing_utils.to_glb(
|
| 58 |
+
outputs['gaussian'][0],
|
| 59 |
+
outputs['mesh'][0],
|
| 60 |
+
# Optional parameters
|
| 61 |
+
simplify=0.95, # Ratio of triangles to remove in the simplification process
|
| 62 |
+
texture_size=1024, # Size of the texture used for the GLB
|
| 63 |
+
verbose=False, # Print logs
|
| 64 |
+
)
|
| 65 |
+
mesh_textured.export(out_meshpath)
|
| 66 |
+
mesh_geometry = _white_geometry_mesh(
|
| 67 |
+
mesh_textured.vertices.copy(),
|
| 68 |
+
mesh_textured.faces.copy(),
|
| 69 |
+
)
|
| 70 |
+
mesh_geometry.export(out_geometry_path)
|
| 71 |
+
|
| 72 |
+
if out_gspath:
|
| 73 |
+
video = render_utils.render_video(outputs['gaussian'][0])['color']
|
| 74 |
+
imageio.mimsave(out_gspath, video, fps=30)
|
| 75 |
+
return
|
| 76 |
+
|
| 77 |
+
vertices, faces = _postprocess_mesh_geometry(outputs['mesh'][0], simplify=0.95)
|
| 78 |
+
mesh_geometry = _white_geometry_mesh(vertices, faces)
|
| 79 |
+
mesh_geometry.export(out_meshpath)
|
| 80 |
+
mesh_geometry.export(out_geometry_path)
|
lib/util/model_revisions.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Recorded source revision for TRELLIS's DINOv2 image conditioner."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import re
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
MODEL_REVISIONS = Path(__file__).resolve().parents[2] / "requirements/model-revisions.json"
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def dinov2_hub_repository(pins_path: Path = MODEL_REVISIONS) -> str:
|
| 12 |
+
"""Use an immutable upstream commit instead of PyTorch Hub's moving main."""
|
| 13 |
+
record = json.loads(pins_path.read_text())["dinov2"]
|
| 14 |
+
if record["repository"] != "facebookresearch/dinov2" or not re.fullmatch(r"[0-9a-f]{40}", record["revision"]):
|
| 15 |
+
raise ValueError("DINOv2 requires the recorded upstream repository and a full commit SHA.")
|
| 16 |
+
return f"{record['repository']}:{record['revision']}"
|
lib/util/partfield.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from sklearn.cluster import KMeans
|
| 3 |
+
from sklearn.neighbors import KDTree
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
|
| 8 |
+
def get_voxel_partfeats(voxel_coords, part_planes):
|
| 9 |
+
voxel_coords = ((voxel_coords[:, 1:] + 0.5) / 64 - 0.5).cpu().numpy()
|
| 10 |
+
bbmin = voxel_coords.min(0)
|
| 11 |
+
bbmax = voxel_coords.max(0)
|
| 12 |
+
center = (bbmin + bbmax) * 0.5
|
| 13 |
+
scale = 2.0 * 0.9 / (bbmax - bbmin).max()
|
| 14 |
+
voxel_coords = (voxel_coords - center) * scale
|
| 15 |
+
|
| 16 |
+
tensor_vertices = torch.from_numpy(voxel_coords).unsqueeze(0).reshape(1, -1, 3).cuda().to(torch.float16)
|
| 17 |
+
part_feats = sample_triplane_feat(part_planes, tensor_vertices) # N, M, C
|
| 18 |
+
part_feats = part_feats.cpu().numpy().reshape(-1, 448)
|
| 19 |
+
|
| 20 |
+
return part_feats
|
| 21 |
+
|
| 22 |
+
def sample_triplane_feat(feature_triplane, normalized_pos):
|
| 23 |
+
'''
|
| 24 |
+
normalized_pos [-1, 1]
|
| 25 |
+
'''
|
| 26 |
+
tri_plane = torch.unbind(feature_triplane, dim=1)
|
| 27 |
+
|
| 28 |
+
x_feat = F.grid_sample(
|
| 29 |
+
tri_plane[0],
|
| 30 |
+
torch.cat(
|
| 31 |
+
[normalized_pos[:, :, 0:1], normalized_pos[:, :, 1:2]],
|
| 32 |
+
dim=-1).unsqueeze(dim=1), padding_mode='border',
|
| 33 |
+
align_corners=True)
|
| 34 |
+
y_feat = F.grid_sample(
|
| 35 |
+
tri_plane[1],
|
| 36 |
+
torch.cat(
|
| 37 |
+
[normalized_pos[:, :, 1:2], normalized_pos[:, :, 2:3]],
|
| 38 |
+
dim=-1).unsqueeze(dim=1), padding_mode='border',
|
| 39 |
+
align_corners=True)
|
| 40 |
+
|
| 41 |
+
z_feat = F.grid_sample(
|
| 42 |
+
tri_plane[2],
|
| 43 |
+
torch.cat(
|
| 44 |
+
[normalized_pos[:, :, 0:1], normalized_pos[:, :, 2:3]],
|
| 45 |
+
dim=-1).unsqueeze(dim=1), padding_mode='border',
|
| 46 |
+
align_corners=True)
|
| 47 |
+
final_feat = (x_feat + y_feat + z_feat)
|
| 48 |
+
final_feat = final_feat.squeeze(dim=2).permute(0, 2, 1) # 32dimension
|
| 49 |
+
return final_feat
|
| 50 |
+
|
| 51 |
+
def cosegment_part(app_coords, app_part_planes, struct_coords, struct_part_planes, num_clusters=30):
|
| 52 |
+
struct_partfield_feats = get_voxel_partfeats(struct_coords, struct_part_planes)
|
| 53 |
+
app_partfield_feats = get_voxel_partfeats(app_coords, app_part_planes)
|
| 54 |
+
|
| 55 |
+
point_feat1 = app_partfield_feats
|
| 56 |
+
point_feat2 = struct_partfield_feats
|
| 57 |
+
|
| 58 |
+
point_feat1 = point_feat1 / np.linalg.norm(point_feat1, axis=-1, keepdims=True)
|
| 59 |
+
point_feat2 = point_feat2 / np.linalg.norm(point_feat2, axis=-1, keepdims=True)
|
| 60 |
+
|
| 61 |
+
clustering1 = KMeans(n_clusters=num_clusters, random_state=0, n_init="auto").fit(point_feat1)
|
| 62 |
+
# Get feature means per cluster
|
| 63 |
+
feature_means1 = []
|
| 64 |
+
for j in range(num_clusters):
|
| 65 |
+
all_cluster_feat = point_feat1[clustering1.labels_==j]
|
| 66 |
+
mean_feat = np.mean(all_cluster_feat, axis=0)
|
| 67 |
+
feature_means1.append(mean_feat)
|
| 68 |
+
|
| 69 |
+
labels1 = clustering1.labels_
|
| 70 |
+
|
| 71 |
+
feature_means1 = np.array(feature_means1)
|
| 72 |
+
tree = KDTree(feature_means1)
|
| 73 |
+
|
| 74 |
+
init_mode = np.array(feature_means1)
|
| 75 |
+
|
| 76 |
+
point_feat2 = point_feat2 / np.linalg.norm(point_feat2, axis=-1, keepdims=True)
|
| 77 |
+
clustering2 = KMeans(n_clusters=num_clusters, random_state=0, init=init_mode).fit(point_feat2)
|
| 78 |
+
|
| 79 |
+
### Get feature means per cluster
|
| 80 |
+
feature_means2 = []
|
| 81 |
+
for j in range(num_clusters):
|
| 82 |
+
all_cluster_feat = point_feat2[clustering2.labels_==j]
|
| 83 |
+
mean_feat = np.mean(all_cluster_feat, axis=0)
|
| 84 |
+
feature_means2.append(mean_feat)
|
| 85 |
+
|
| 86 |
+
feature_means2 = np.array(feature_means2)
|
| 87 |
+
_, nn_idx = tree.query(feature_means2, k=1)
|
| 88 |
+
relabelled_2 = nn_idx[clustering2.labels_]
|
| 89 |
+
|
| 90 |
+
return labels1, relabelled_2, point_feat1, point_feat2
|
| 91 |
+
|
| 92 |
+
def cluster_geoms(struct_coords, struct_part_planes, num_clusters=10):
|
| 93 |
+
struct_partfield_feats = get_voxel_partfeats(struct_coords, struct_part_planes)
|
| 94 |
+
|
| 95 |
+
point_feat = struct_partfield_feats
|
| 96 |
+
point_feat = point_feat / np.linalg.norm(point_feat, axis=-1, keepdims=True)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
clustering = KMeans(n_clusters=num_clusters, random_state=0, n_init="auto").fit(point_feat)
|
| 100 |
+
# Get feature means per cluster
|
| 101 |
+
feature_means = []
|
| 102 |
+
for j in range(num_clusters):
|
| 103 |
+
all_cluster_feat = point_feat[clustering.labels_==j]
|
| 104 |
+
mean_feat = np.mean(all_cluster_feat, axis=0)
|
| 105 |
+
feature_means.append(mean_feat)
|
| 106 |
+
|
| 107 |
+
labels = clustering.labels_
|
| 108 |
+
return labels
|
lib/util/pipeline_compat.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Conditioning compatibility checks without importing GPU dependencies."""
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def can_reuse_appearance_pipeline(pipeline, appearance_type):
|
| 5 |
+
"""Image appearance needs the image pipeline's preprocessing interface."""
|
| 6 |
+
if pipeline is None:
|
| 7 |
+
return False
|
| 8 |
+
if appearance_type == "image":
|
| 9 |
+
return callable(getattr(pipeline, "preprocess_image", None))
|
| 10 |
+
return True
|
lib/util/pointcloud.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import trimesh
|
| 3 |
+
import utils3d
|
| 4 |
+
import open3d_pycg as o3d
|
| 5 |
+
|
| 6 |
+
def convert_mesh_yup_to_zup(mesh):
|
| 7 |
+
# Row vectors v' = v @ R. Use the inverse of the old rotation so +Y maps to +Z vs -Z the other way.
|
| 8 |
+
mesh.vertices = mesh.vertices @ np.array([[1, 0, 0], [0, 0, -1], [0, 1, 0]])
|
| 9 |
+
return mesh
|
| 10 |
+
|
| 11 |
+
def voxelize_mesh(mesh_file, save_path):
|
| 12 |
+
assert mesh_file.endswith('.ply') and save_path.endswith('.ply'), 'Voxelization only supports .ply files'
|
| 13 |
+
|
| 14 |
+
mesh = o3d.io.read_triangle_mesh(mesh_file)
|
| 15 |
+
|
| 16 |
+
# clamp vertices to the range [-0.5, 0.5]
|
| 17 |
+
vertices = np.clip(np.asarray(mesh.vertices), -0.5 + 1e-6, 0.5 - 1e-6)
|
| 18 |
+
mesh.vertices = o3d.utility.Vector3dVector(vertices)
|
| 19 |
+
voxel_grid = o3d.geometry.VoxelGrid.create_from_triangle_mesh_within_bounds(mesh, voxel_size=1/64, min_bound=(-0.5, -0.5, -0.5), max_bound=(0.5, 0.5, 0.5))
|
| 20 |
+
vertices = np.array([voxel.grid_index for voxel in voxel_grid.get_voxels()])
|
| 21 |
+
assert np.all(vertices >= 0) and np.all(vertices < 64), "Some vertices are out of bounds"
|
| 22 |
+
vertices = (vertices + 0.5) / 64 - 0.5
|
| 23 |
+
|
| 24 |
+
utils3d.io.write_ply(save_path, vertices)
|
lib/util/render.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import shutil
|
| 5 |
+
import subprocess
|
| 6 |
+
from urllib.request import urlretrieve
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 10 |
+
BLENDER_LINK = 'https://download.blender.org/release/Blender3.0/blender-3.0.1-linux-x64.tar.xz'
|
| 11 |
+
BLENDER_INSTALLATION_PATH = os.environ.get('SPACEFLOW_BLENDER_ROOT', str(REPO_ROOT))
|
| 12 |
+
BLENDER_PATH = os.environ.get(
|
| 13 |
+
'SPACEFLOW_BLENDER_PATH',
|
| 14 |
+
f'{BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64/blender',
|
| 15 |
+
)
|
| 16 |
+
BLENDER_RENDER_SCRIPT = os.environ.get(
|
| 17 |
+
'SPACEFLOW_BLENDER_RENDER_SCRIPT',
|
| 18 |
+
str(REPO_ROOT / 'third_party' / 'TRELLIS' / 'dataset_toolkits' / 'blender_script' / 'render.py'),
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
def _install_blender():
|
| 22 |
+
if shutil.which(BLENDER_PATH):
|
| 23 |
+
return
|
| 24 |
+
if os.environ.get('SPACEFLOW_BLENDER_PATH'):
|
| 25 |
+
raise FileNotFoundError(
|
| 26 |
+
f'Blender is not executable at SPACEFLOW_BLENDER_PATH={BLENDER_PATH}. '
|
| 27 |
+
'Set it to an installed Blender 3.x executable.'
|
| 28 |
+
)
|
| 29 |
+
installation = Path(BLENDER_INSTALLATION_PATH)
|
| 30 |
+
installation.mkdir(parents=True, exist_ok=True)
|
| 31 |
+
archive = installation / 'blender-3.0.1-linux-x64.tar.xz'
|
| 32 |
+
try:
|
| 33 |
+
urlretrieve(BLENDER_LINK, archive)
|
| 34 |
+
subprocess.run(['tar', '-xf', str(archive), '-C', str(installation)], check=True)
|
| 35 |
+
except (OSError, subprocess.CalledProcessError) as exc:
|
| 36 |
+
raise RuntimeError(
|
| 37 |
+
'Blender installation failed. Install Blender 3.x separately and set '
|
| 38 |
+
'SPACEFLOW_BLENDER_PATH to its executable.'
|
| 39 |
+
) from exc
|
| 40 |
+
if not shutil.which(BLENDER_PATH):
|
| 41 |
+
raise FileNotFoundError(f'Blender installation did not produce an executable: {BLENDER_PATH}')
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _run_blender(args):
|
| 45 |
+
result = subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
|
| 46 |
+
if result.returncode:
|
| 47 |
+
raise RuntimeError(
|
| 48 |
+
f'Blender failed with exit code {result.returncode}. '
|
| 49 |
+
f'Executable: {BLENDER_PATH}. Error output:\n{result.stderr[-3000:]}'
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
def render_all_views(file_path, output_folder, num_views=150):
|
| 53 |
+
_install_blender()
|
| 54 |
+
# Build camera {yaw, pitch, radius, fov}
|
| 55 |
+
yaws = []
|
| 56 |
+
pitchs = []
|
| 57 |
+
offset = (np.random.rand(), np.random.rand())
|
| 58 |
+
for i in range(num_views):
|
| 59 |
+
y, p = sphere_hammersley_sequence(i, num_views, offset)
|
| 60 |
+
yaws.append(y)
|
| 61 |
+
pitchs.append(p)
|
| 62 |
+
radius = [2] * num_views
|
| 63 |
+
fov = [40 / 180 * np.pi] * num_views
|
| 64 |
+
views = [{'yaw': y, 'pitch': p, 'radius': r, 'fov': f} for y, p, r, f in zip(yaws, pitchs, radius, fov)]
|
| 65 |
+
|
| 66 |
+
args = [
|
| 67 |
+
BLENDER_PATH, '-b', '-P', BLENDER_RENDER_SCRIPT,
|
| 68 |
+
'--',
|
| 69 |
+
'--views', json.dumps(views),
|
| 70 |
+
'--object', os.path.expanduser(file_path),
|
| 71 |
+
'--resolution', '512',
|
| 72 |
+
'--output_folder', output_folder,
|
| 73 |
+
'--engine', 'CYCLES',
|
| 74 |
+
'--save_mesh',
|
| 75 |
+
]
|
| 76 |
+
if file_path.endswith('.blend'):
|
| 77 |
+
args.insert(1, file_path)
|
| 78 |
+
|
| 79 |
+
_run_blender(args)
|
| 80 |
+
|
| 81 |
+
if os.path.exists(os.path.join(output_folder, 'transforms.json')):
|
| 82 |
+
return True
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def export_normalized_mesh(file_path, output_folder):
|
| 86 |
+
_install_blender()
|
| 87 |
+
args = [
|
| 88 |
+
BLENDER_PATH, '-b', '-P', BLENDER_RENDER_SCRIPT,
|
| 89 |
+
'--',
|
| 90 |
+
'--views', '[]',
|
| 91 |
+
'--object', os.path.expanduser(file_path),
|
| 92 |
+
'--resolution', '512',
|
| 93 |
+
'--output_folder', output_folder,
|
| 94 |
+
'--engine', 'CYCLES',
|
| 95 |
+
'--save_mesh',
|
| 96 |
+
]
|
| 97 |
+
if file_path.endswith('.blend'):
|
| 98 |
+
args.insert(1, file_path)
|
| 99 |
+
|
| 100 |
+
_run_blender(args)
|
| 101 |
+
|
| 102 |
+
if os.path.exists(os.path.join(output_folder, 'mesh.ply')):
|
| 103 |
+
return True
|
| 104 |
+
|
| 105 |
+
# ===============LOW DISCREPANCY SEQUENCES================
|
| 106 |
+
|
| 107 |
+
PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53]
|
| 108 |
+
|
| 109 |
+
def radical_inverse(base, n):
|
| 110 |
+
val = 0
|
| 111 |
+
inv_base = 1.0 / base
|
| 112 |
+
inv_base_n = inv_base
|
| 113 |
+
while n > 0:
|
| 114 |
+
digit = n % base
|
| 115 |
+
val += digit * inv_base_n
|
| 116 |
+
n //= base
|
| 117 |
+
inv_base_n *= inv_base
|
| 118 |
+
return val
|
| 119 |
+
|
| 120 |
+
def halton_sequence(dim, n):
|
| 121 |
+
return [radical_inverse(PRIMES[dim], n) for dim in range(dim)]
|
| 122 |
+
|
| 123 |
+
def hammersley_sequence(dim, n, num_samples):
|
| 124 |
+
return [n / num_samples] + halton_sequence(dim - 1, n)
|
| 125 |
+
|
| 126 |
+
def sphere_hammersley_sequence(n, num_samples, offset=(0, 0)):
|
| 127 |
+
u, v = hammersley_sequence(2, n, num_samples)
|
| 128 |
+
u += offset[0] / num_samples
|
| 129 |
+
v += offset[1]
|
| 130 |
+
u = 2 * u if u < 0.25 else 2 / 3 * u + 1 / 3
|
| 131 |
+
theta = np.arccos(1 - 2 * u) - np.pi / 2
|
| 132 |
+
phi = v * 2 * np.pi
|
| 133 |
+
return [phi, theta]
|
lib/util/visualization.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging as log
|
| 2 |
+
import os.path as osp
|
| 3 |
+
|
| 4 |
+
import imageio
|
| 5 |
+
import matplotlib
|
| 6 |
+
matplotlib.use('Agg')
|
| 7 |
+
import matplotlib.colors as mcolors
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
from sklearn.neighbors import KDTree
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
|
| 13 |
+
from third_party.TRELLIS.trellis.representations.mesh.cube2mesh import MeshExtractResult
|
| 14 |
+
from third_party.TRELLIS.trellis.renderers.mesh_renderer import MeshRenderer
|
| 15 |
+
from third_party.TRELLIS.trellis.utils.render_utils import yaw_pitch_r_fov_to_extrinsics_intrinsics
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def map_voxel_labels_to_vertices(mesh_vertices, voxel_coords, voxel_labels):
|
| 19 |
+
"""Assign each mesh vertex the label of its nearest voxel via KD-tree lookup.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
mesh_vertices: np.ndarray (V, 3) — mesh vertex positions
|
| 23 |
+
voxel_coords: np.ndarray (M, 3) — voxel center positions (same coordinate space)
|
| 24 |
+
voxel_labels: np.ndarray (M,) — integer label per voxel
|
| 25 |
+
|
| 26 |
+
Returns:
|
| 27 |
+
np.ndarray (V,) — integer label per vertex
|
| 28 |
+
"""
|
| 29 |
+
tree = KDTree(voxel_coords)
|
| 30 |
+
_, idx = tree.query(mesh_vertices, k=1)
|
| 31 |
+
return voxel_labels[idx.flatten()]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def visualize_and_save(structure, partfield_clusters, output_folder,
|
| 35 |
+
output_name='cluster_visualization.mp4',
|
| 36 |
+
num_frames=300, resolution=512):
|
| 37 |
+
"""Render a smooth rotating orbit video of the structure colored by cluster labels.
|
| 38 |
+
|
| 39 |
+
Uses the TRELLIS MeshRenderer (nvdiffrast, ssaa=4) and the same yaw/pitch
|
| 40 |
+
camera trajectory as the pipeline's Gaussian scene videos so all sides are visible.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
structure: trimesh.Trimesh — normalized structure mesh
|
| 44 |
+
(vertices in [-0.5, 0.5])
|
| 45 |
+
partfield_clusters: np.ndarray (V,) — per-vertex integer cluster labels,
|
| 46 |
+
where V matches structure.vertices.shape[0]
|
| 47 |
+
output_folder: str — directory in which to save the video
|
| 48 |
+
output_name: str — filename for the saved video
|
| 49 |
+
(default 'cluster_visualization.mp4')
|
| 50 |
+
num_frames: int — number of video frames (default 300)
|
| 51 |
+
resolution: int — output resolution in pixels (default 512)
|
| 52 |
+
"""
|
| 53 |
+
partfield_clusters = np.asarray(partfield_clusters, dtype=int)
|
| 54 |
+
num_clusters = int(partfield_clusters.max()) + 1
|
| 55 |
+
|
| 56 |
+
# Evenly-spaced HSV hues → maximally distinct colors
|
| 57 |
+
hues = np.linspace(0, 1, num_clusters, endpoint=False)
|
| 58 |
+
cluster_colors = np.array([mcolors.hsv_to_rgb([h, 0.90, 0.95]) for h in hues],
|
| 59 |
+
dtype=np.float32) # (num_clusters, 3)
|
| 60 |
+
vertex_colors = cluster_colors[partfield_clusters] # (V, 3)
|
| 61 |
+
|
| 62 |
+
# Build MeshExtractResult for the TRELLIS MeshRenderer
|
| 63 |
+
verts_t = torch.tensor(structure.vertices, dtype=torch.float32, device='cuda')
|
| 64 |
+
faces_t = torch.tensor(structure.faces, dtype=torch.long, device='cuda')
|
| 65 |
+
colors_t = torch.tensor(vertex_colors, dtype=torch.float32, device='cuda')
|
| 66 |
+
mesh_result = MeshExtractResult(vertices=verts_t, faces=faces_t, vertex_attrs=colors_t)
|
| 67 |
+
|
| 68 |
+
# Camera trajectory: identical to render_utils.render_video
|
| 69 |
+
yaws = torch.linspace(0, 2 * torch.pi, num_frames).tolist()
|
| 70 |
+
pitch = (0.25 + 0.5 * torch.sin(torch.linspace(0, 2 * torch.pi, num_frames))).tolist()
|
| 71 |
+
extrinsics, intrinsics = yaw_pitch_r_fov_to_extrinsics_intrinsics(yaws, pitch, 2, 40)
|
| 72 |
+
|
| 73 |
+
renderer = MeshRenderer(rendering_options={
|
| 74 |
+
"resolution": resolution,
|
| 75 |
+
"near": 1,
|
| 76 |
+
"far": 100,
|
| 77 |
+
"ssaa": 4,
|
| 78 |
+
})
|
| 79 |
+
|
| 80 |
+
frames = []
|
| 81 |
+
log.info(f"Rendering {num_frames} frames → {output_name} ...")
|
| 82 |
+
with torch.no_grad():
|
| 83 |
+
for extr, intr in tqdm(zip(extrinsics, intrinsics), total=num_frames,
|
| 84 |
+
desc=f'Rendering {output_name}'):
|
| 85 |
+
res = renderer.render(mesh_result, extr, intr,
|
| 86 |
+
return_types=["color", "mask"])
|
| 87 |
+
# composite cluster colors over black background
|
| 88 |
+
frame = (res['color'] * res['mask']).permute(1, 2, 0).clamp(0.0, 1.0)
|
| 89 |
+
frames.append((frame.cpu().numpy() * 255).astype(np.uint8))
|
| 90 |
+
|
| 91 |
+
video_path = osp.join(output_folder, output_name)
|
| 92 |
+
imageio.mimsave(video_path, frames, fps=30)
|
| 93 |
+
log.info(f"Saved {video_path}")
|
packages.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
libgl1
|
| 2 |
+
libglib2.0-0
|
| 3 |
+
libgomp1
|
| 4 |
+
libxrender1
|
requirements.txt
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
setuptools==80.9.0
|
| 2 |
+
torch==2.8.0
|
| 3 |
+
torchvision==0.23.0
|
| 4 |
+
numpy==1.26.4
|
| 5 |
+
scipy==1.15.3
|
| 6 |
+
pillow==12.0.0
|
| 7 |
+
imageio==2.37.3
|
| 8 |
+
imageio-ffmpeg==0.6.0
|
| 9 |
+
tqdm
|
| 10 |
+
easydict==1.13
|
| 11 |
+
opencv-python-headless==4.11.0.86
|
| 12 |
+
rembg==2.0.69
|
| 13 |
+
onnxruntime==1.23.2
|
| 14 |
+
trimesh==4.11.4
|
| 15 |
+
open3d_pycg-cpu==0.19.0
|
| 16 |
+
xatlas==0.0.11
|
| 17 |
+
pyvista==0.47.1
|
| 18 |
+
pymeshfix==0.18.0
|
| 19 |
+
igraph==1.0.0
|
| 20 |
+
transformers==5.3.0
|
| 21 |
+
safetensors
|
| 22 |
+
omegaconf==2.3.0
|
| 23 |
+
lightning==2.2.0
|
| 24 |
+
yacs==0.1.8
|
| 25 |
+
scikit-image==0.25.2
|
| 26 |
+
scikit-learn==1.7.2
|
| 27 |
+
loguru==0.7.3
|
| 28 |
+
boto3
|
| 29 |
+
psutil
|
| 30 |
+
pytz
|
| 31 |
+
einops==0.8.2
|
| 32 |
+
matplotlib==3.10.8
|
| 33 |
+
plyfile==1.1.3
|
| 34 |
+
spconv-cu120==2.3.6
|
| 35 |
+
git+https://github.com/EasternJournalist/utils3d.git@9a4eb15e4021b67b12c460c7057d642626897ec8
|
| 36 |
+
https://data.pyg.org/whl/torch-2.8.0%2Bcu128/torch_scatter-2.1.2%2Bpt28cu128-cp310-cp310-linux_x86_64.whl
|
| 37 |
+
https://huggingface.co/datasets/multimodalart/zerogpu-blackwell-wheels/resolve/main/wheels/pt28-cu128-cp310/flash_attn-2.8.3-cp310-cp310-linux_x86_64.whl
|
| 38 |
+
https://huggingface.co/datasets/multimodalart/zerogpu-blackwell-wheels/resolve/main/wheels/pt28-cu128-cp310/nvdiffrast-0.4.0-cp310-cp310-linux_x86_64.whl
|
requirements/model-revisions.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"huggingface": {
|
| 3 |
+
"openai/clip-vit-large-patch14": "32bd64288804d66eefd0ccbe215aa642df71cc41",
|
| 4 |
+
"microsoft/TRELLIS-text-xlarge": "e0b00432b8e3a8ecee0df806ab1df9f7281f2be4",
|
| 5 |
+
"microsoft/TRELLIS-image-large": "25e0d31ffbebe4b5a97464dd851910efc3002d96",
|
| 6 |
+
"mikaelaangel/partfield-ckpt": "90b9b1e08b6a12fdcb6ee26b4854a26235e1765f"
|
| 7 |
+
},
|
| 8 |
+
"partfield_sha256": "463efc8a3afd3913142aa025e0125c00f16ef452b8de6a132ebe32bbe7877ee4",
|
| 9 |
+
"dinov2": {
|
| 10 |
+
"repository": "facebookresearch/dinov2",
|
| 11 |
+
"revision": "7764ea0f912e53c92e82eb78a2a1631e92725fc8",
|
| 12 |
+
"model": "dinov2_vitl14_reg",
|
| 13 |
+
"checkpoint": "dinov2_vitl14_reg4_pretrain.pth",
|
| 14 |
+
"sha256": "36e4deffbaef061a2576705b0c36f93621e2ae20bf6274694821b0b492551b51",
|
| 15 |
+
"source": "Upstream source pinned for fresh release verification; official Meta checkpoint downloaded and hashed on pf-pc69."
|
| 16 |
+
},
|
| 17 |
+
"u2net": {
|
| 18 |
+
"url": "https://github.com/danielgatis/rembg/releases/download/v0.0.0/u2net.onnx",
|
| 19 |
+
"md5": "60024c5c889badc19c04ad937298a77b",
|
| 20 |
+
"source": "Official download URL and checksum in the pinned rembg 2.0.69 U2netSession."
|
| 21 |
+
},
|
| 22 |
+
"source": "TRELLIS/CLIP revisions recovered from original cache refs; PartField pinned from its official linked model repository."
|
| 23 |
+
}
|
run_local_tau.py
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import os.path as osp
|
| 5 |
+
import gc
|
| 6 |
+
import shutil
|
| 7 |
+
import trimesh
|
| 8 |
+
from PIL import Image
|
| 9 |
+
import logging as log
|
| 10 |
+
from omegaconf import OmegaConf
|
| 11 |
+
import argparse
|
| 12 |
+
import random
|
| 13 |
+
import time
|
| 14 |
+
import numpy as np
|
| 15 |
+
from skimage import measure
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
from lightning.pytorch import seed_everything, Trainer
|
| 19 |
+
import open3d_pycg as o3d
|
| 20 |
+
|
| 21 |
+
import utils3d
|
| 22 |
+
|
| 23 |
+
import sys
|
| 24 |
+
sys.path.append('.')
|
| 25 |
+
|
| 26 |
+
from third_party.PartField.partfield.model_trainer_pvcnn_only_demo import Model
|
| 27 |
+
from lib.opt import self_similarity
|
| 28 |
+
from lib.util import common, render, pointcloud
|
| 29 |
+
from lib.util.checkpoint_compat import partfield_checkpoint_scope
|
| 30 |
+
from third_party.TRELLIS.trellis.pipelines import TrellisTextTo3DPipeline
|
| 31 |
+
from utils import merge_meshes
|
| 32 |
+
|
| 33 |
+
log.getLogger().setLevel(log.INFO)
|
| 34 |
+
log.basicConfig(level=log.INFO,
|
| 35 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
| 36 |
+
datefmt='%Y-%m-%d %H:%M:%S')
|
| 37 |
+
|
| 38 |
+
STEPS_SHAPE_GEN = 12
|
| 39 |
+
CFG_SHAPE_GEN = 7.5
|
| 40 |
+
INPUT_SQ_COLORED_GLB = "input_superquadrics_colored.glb"
|
| 41 |
+
INPUT_SQ_HIGH_COLOR = np.array([0xf5, 0x9e, 0x0b, 0xff], dtype=np.uint8)
|
| 42 |
+
INPUT_SQ_LOW_COLOR = np.array([0xf8, 0xfa, 0xfc, 0xff], dtype=np.uint8)
|
| 43 |
+
|
| 44 |
+
def reset_run_state(seed=0):
|
| 45 |
+
random.seed(seed)
|
| 46 |
+
np.random.seed(seed)
|
| 47 |
+
torch.manual_seed(seed)
|
| 48 |
+
if torch.cuda.is_available():
|
| 49 |
+
torch.cuda.manual_seed_all(seed)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def trellis_pipeline_path_from_args(args, cfg):
|
| 53 |
+
return (
|
| 54 |
+
args.trellis_pipeline_path
|
| 55 |
+
or os.environ.get("SPACEFLOW_TRELLIS_PIPELINE_PATH")
|
| 56 |
+
or cfg.trellis_text_model_name
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def load_trellis_pipeline(args, cfg):
|
| 61 |
+
trellis_pipeline_path = trellis_pipeline_path_from_args(args, cfg)
|
| 62 |
+
log.info(f"Loading TRELLIS pipeline from: {trellis_pipeline_path}")
|
| 63 |
+
start = time.perf_counter()
|
| 64 |
+
pipeline = TrellisTextTo3DPipeline.from_pretrained(trellis_pipeline_path)
|
| 65 |
+
log.info(f"Loaded TRELLIS pipeline weights and CLIP in {time.perf_counter() - start:.2f}s")
|
| 66 |
+
return pipeline
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def move_trellis_text_conditioner(pipeline, device):
|
| 70 |
+
text_cond_model = getattr(pipeline, 'text_cond_model', None)
|
| 71 |
+
if not text_cond_model:
|
| 72 |
+
return
|
| 73 |
+
if 'model' in text_cond_model:
|
| 74 |
+
text_cond_model['model'].to(device)
|
| 75 |
+
if 'null_cond' in text_cond_model:
|
| 76 |
+
text_cond_model['null_cond'] = text_cond_model['null_cond'].to(device)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def offload_trellis_pipeline(pipeline):
|
| 80 |
+
if pipeline is None:
|
| 81 |
+
return
|
| 82 |
+
pipeline.cpu()
|
| 83 |
+
move_trellis_text_conditioner(pipeline, 'cpu')
|
| 84 |
+
torch.cuda.empty_cache()
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def init_args(argv=None):
|
| 88 |
+
parser = argparse.ArgumentParser(description='SpaceFlow - controllable 3D generation')
|
| 89 |
+
|
| 90 |
+
# Guidance mode selection
|
| 91 |
+
parser.add_argument('--guidance_mode', type=str, required=True, choices=['similarity'],
|
| 92 |
+
help='Guidance mode. Only "similarity" is supported.')
|
| 93 |
+
parser.add_argument('--output_dir', type=str, required=True,
|
| 94 |
+
help='Output directory for results')
|
| 95 |
+
parser.add_argument('--convert_yup_to_zup', action='store_true',
|
| 96 |
+
help='Convert Y-up coordinate system to Z-up')
|
| 97 |
+
|
| 98 |
+
parser.add_argument('--appearance_image', type=str,
|
| 99 |
+
help='Path to global image prompt for similarity guidance')
|
| 100 |
+
parser.add_argument('--appearance_text', type=str, default='',
|
| 101 |
+
help='Global text prompt for similarity guidance')
|
| 102 |
+
|
| 103 |
+
# Spatial control parameters
|
| 104 |
+
parser.add_argument('--shape_superquadric_path', type=str, required=True,
|
| 105 |
+
help='Path to shape superquadrics file')
|
| 106 |
+
parser.add_argument('--shape_superquadric_high_control_path', type=str, default=None,
|
| 107 |
+
help='Path to high control shape superquadrics file (used local_tau_mode is guidance or masking)')
|
| 108 |
+
parser.add_argument('--low_control_superquadric_mask_path', type=str, default=None,
|
| 109 |
+
help='Path to low control superquadric mask (Only used when local_tau_mode is low_control_mask)')
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
parser.add_argument('--spatial_control_mesh_path', type=str, default=None,
|
| 113 |
+
help='Path to save the spatial control mesh (defaults to <output_dir>/spatial_control_mesh.ply)')
|
| 114 |
+
|
| 115 |
+
parser.add_argument('--shape_tau', type=float, default=6.0, required=True,
|
| 116 |
+
help='Value of tau for superquadric control')
|
| 117 |
+
parser.add_argument('--shape_tau_high_control', type=float, default=None, required=False,
|
| 118 |
+
help='Value of tau for superquadric control')
|
| 119 |
+
parser.add_argument('--polyak_update_tau', type=float, default=0.18, required=False,
|
| 120 |
+
help='Tau value for Polyak averaging of the high-control model (if using high control). If set to 0 it just perfoms spacecontrol with the lowest provided tau value.')
|
| 121 |
+
parser.add_argument('--local_tau_mode', type=str, choices=['guidance', 'masking', 'low_control_mask'], default='guidance',
|
| 122 |
+
help='Whether to use local tau guidance, masking or low control mask mode. ')
|
| 123 |
+
parser.add_argument('--full_pipeline', action='store_true',
|
| 124 |
+
help='Continue past structure generation into PartField and similarity optimization. Default keeps the legacy structure-only behavior.')
|
| 125 |
+
parser.add_argument('--n_repaint_steps', type=int, default=10,
|
| 126 |
+
help='Number of repaint resampling steps to perform during structure generation to improve blending (default: 10). Set to 0 to disable.')
|
| 127 |
+
parser.add_argument('--structure_seed', type=int, default=1,
|
| 128 |
+
help='Random seed for TRELLIS sparse-structure generation (default: 1).')
|
| 129 |
+
parser.add_argument('--texture_optim_steps', type=int, default=None,
|
| 130 |
+
help='Override config/default.yaml sim_guidance.steps for texture similarity optimization. Minimum: 2.')
|
| 131 |
+
parser.add_argument('--mixed_self_attn_boost', type=float, default=None,
|
| 132 |
+
help='Override config/default.yaml sim_guidance.mixed_self_attn_boost. '
|
| 133 |
+
'Multiply self-attention weights by this factor before the softmax '
|
| 134 |
+
'between voxels sharing a routed condition. 1.0 = stock attention.')
|
| 135 |
+
parser.add_argument('--trellis_pipeline_path', type=str, default=None,
|
| 136 |
+
help='TRELLIS pipeline config/model path. Defaults to SPACEFLOW_TRELLIS_PIPELINE_PATH or config/default.yaml trellis_text_model_name.')
|
| 137 |
+
parser.add_argument('--geometry_only_decode', action='store_true',
|
| 138 |
+
help='Decode out_sim.glb as white geometry only. Use with text-only lightweight pipeline configs that do not include a Gaussian decoder.')
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
parser.add_argument('--text_prompt', type=str, required=True,
|
| 142 |
+
help='Text prompt for 3D shape generation')
|
| 143 |
+
parser.add_argument('--local_text_prompts', type=str, default=None,
|
| 144 |
+
help='JSON-encoded list of per-SQ local text prompts (text-similarity mode)')
|
| 145 |
+
parser.add_argument('--local_image_paths', type=str, default=None,
|
| 146 |
+
help='JSON-encoded list of per-SQ local image file paths (image-similarity mode)')
|
| 147 |
+
|
| 148 |
+
args = parser.parse_args(argv)
|
| 149 |
+
|
| 150 |
+
if args.appearance_text and args.appearance_image:
|
| 151 |
+
parser.error("Provide either --appearance_image or --appearance_text for similarity guidance, not both.")
|
| 152 |
+
|
| 153 |
+
if not args.appearance_text and not args.appearance_image:
|
| 154 |
+
parser.error("Provide either --appearance_image or --appearance_text for similarity guidance.")
|
| 155 |
+
|
| 156 |
+
return args
|
| 157 |
+
|
| 158 |
+
def add_superquadric_compact_rot_mat(
|
| 159 |
+
scalings: np.array=np.array([1.0, 1.0, 1.0]),
|
| 160 |
+
exponents: np.array=np.array([2.0, 2.0, 2.0]),
|
| 161 |
+
translation: np.array=np.array([0.0, 0.0, 0.0]),
|
| 162 |
+
rotation: np.array=np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0],[0.0, 0.0,1.0]]),
|
| 163 |
+
tapering=None,
|
| 164 |
+
bending=None,
|
| 165 |
+
resolution: int=10,
|
| 166 |
+
visible: bool=True):
|
| 167 |
+
"""Build a superquadric mesh in scene coordinates."""
|
| 168 |
+
|
| 169 |
+
def apply_taper(x, y, z, c, kx, ky):
|
| 170 |
+
c = float(c) if abs(float(c)) > 1e-8 else 1e-8
|
| 171 |
+
z_norm = z / c
|
| 172 |
+
x *= float(kx) * z_norm + 1.0
|
| 173 |
+
y *= float(ky) * z_norm + 1.0
|
| 174 |
+
|
| 175 |
+
def apply_bending_axis(x, y, z, kb, alpha, axis):
|
| 176 |
+
kb = float(kb)
|
| 177 |
+
if abs(kb) < 1e-3:
|
| 178 |
+
return
|
| 179 |
+
alpha = float(alpha)
|
| 180 |
+
if axis == "z":
|
| 181 |
+
u, v, w = x.copy(), y.copy(), z.copy()
|
| 182 |
+
elif axis == "x":
|
| 183 |
+
u, v, w = y.copy(), z.copy(), x.copy()
|
| 184 |
+
elif axis == "y":
|
| 185 |
+
u, v, w = z.copy(), x.copy(), y.copy()
|
| 186 |
+
else:
|
| 187 |
+
raise ValueError(axis)
|
| 188 |
+
|
| 189 |
+
sin_alpha = np.sin(alpha)
|
| 190 |
+
cos_alpha = np.cos(alpha)
|
| 191 |
+
beta = np.arctan2(v, u)
|
| 192 |
+
r = np.sqrt(u * u + v * v) * np.cos(alpha - beta)
|
| 193 |
+
inv_kb = 1.0 / kb
|
| 194 |
+
gamma = w * kb
|
| 195 |
+
rho = inv_kb - r
|
| 196 |
+
rb = inv_kb - rho * np.cos(gamma)
|
| 197 |
+
expr = rb - r
|
| 198 |
+
u = u + expr * cos_alpha
|
| 199 |
+
v = v + expr * sin_alpha
|
| 200 |
+
w = rho * np.sin(gamma)
|
| 201 |
+
|
| 202 |
+
if axis == "z":
|
| 203 |
+
x[:], y[:], z[:] = u, v, w
|
| 204 |
+
elif axis == "x":
|
| 205 |
+
x[:], y[:], z[:] = w, u, v
|
| 206 |
+
else:
|
| 207 |
+
x[:], y[:], z[:] = v, w, u
|
| 208 |
+
|
| 209 |
+
def create_superquadric_mesh(A, B, C, e1, e2, N):
|
| 210 |
+
def f(o, m):
|
| 211 |
+
return np.sign(np.sin(o)) * np.abs(np.sin(o))**m
|
| 212 |
+
def g(o, m):
|
| 213 |
+
return np.sign(np.cos(o)) * np.abs(np.cos(o))**m
|
| 214 |
+
u = np.linspace(-np.pi, np.pi, N, endpoint=True)
|
| 215 |
+
v = np.linspace(-np.pi/2.0, np.pi/2.0, N, endpoint=True)
|
| 216 |
+
u = np.tile(u, N)
|
| 217 |
+
v = (np.repeat(v, N))
|
| 218 |
+
if np.linalg.det(rotation) < 0:
|
| 219 |
+
u = u[::-1]
|
| 220 |
+
triangles = []
|
| 221 |
+
|
| 222 |
+
x = A * g(v, e1) * g(u, e2)
|
| 223 |
+
y = B * g(v, e1) * f(u, e2)
|
| 224 |
+
z = C * f(v, e1)
|
| 225 |
+
# Set poles to zero to account for numerical instabilities in f and g due to ** operator
|
| 226 |
+
x[:N] = 0.0
|
| 227 |
+
x[-N:] = 0.0
|
| 228 |
+
if tapering is not None:
|
| 229 |
+
apply_taper(x, y, z, C, tapering[0], tapering[1])
|
| 230 |
+
if bending is not None:
|
| 231 |
+
# Packed as [k_z, alpha_z, k_x, alpha_x, k_y, alpha_y].
|
| 232 |
+
apply_bending_axis(x, y, z, bending[4], bending[5], "y")
|
| 233 |
+
apply_bending_axis(x, y, z, bending[2], bending[3], "x")
|
| 234 |
+
apply_bending_axis(x, y, z, bending[0], bending[1], "z")
|
| 235 |
+
vertices = np.concatenate([np.expand_dims(x, 1),
|
| 236 |
+
np.expand_dims(y, 1),
|
| 237 |
+
np.expand_dims(z, 1)], axis=1)
|
| 238 |
+
vertices = (rotation @ vertices.T).T + translation
|
| 239 |
+
|
| 240 |
+
triangles = []
|
| 241 |
+
for i in range(N-1):
|
| 242 |
+
for j in range(N-1):
|
| 243 |
+
triangles.append([i*N+j, i*N+j+1, (i+1)*N+j])
|
| 244 |
+
triangles.append([(i+1)*N+j, i*N+j+1, (i+1)*N+(j+1)])
|
| 245 |
+
# Connect first and last vertex in each row
|
| 246 |
+
for i in range(N - 1):
|
| 247 |
+
triangles.append([i * N + (N - 1), i * N, (i + 1) * N + (N - 1)])
|
| 248 |
+
triangles.append([(i + 1) * N + (N - 1), i * N, (i + 1) * N])
|
| 249 |
+
|
| 250 |
+
triangles.append([(N-1)*N+(N-1), (N-1)*N, (N-1)])
|
| 251 |
+
triangles.append([(N-1), (N-1)*N, 0])
|
| 252 |
+
|
| 253 |
+
return vertices, triangles
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
vertices, triangles = create_superquadric_mesh(scalings[0], scalings[1], scalings[2],
|
| 257 |
+
exponents[0], exponents[1],
|
| 258 |
+
resolution)
|
| 259 |
+
return vertices, triangles
|
| 260 |
+
|
| 261 |
+
def load_superquadric_from_file(file_path: str) -> list:
|
| 262 |
+
par_dict = np.load(file_path)
|
| 263 |
+
print(par_dict)
|
| 264 |
+
scale = par_dict['scales'] # 3 (3x1 vector)
|
| 265 |
+
rotate = par_dict['rotations'] # 3 (3x3 rotation matrix)
|
| 266 |
+
shapes = par_dict['shapes'] # 2 (2x1 vector)
|
| 267 |
+
trans = par_dict['translations'] # 3 (3x1 vector)
|
| 268 |
+
num_el = scale.shape[0] # number of superquadrics
|
| 269 |
+
tapering = par_dict['tapering'] if 'tapering' in par_dict else np.zeros((num_el, 2))
|
| 270 |
+
bending = par_dict['bending'] if 'bending' in par_dict else np.zeros((num_el, 6))
|
| 271 |
+
control_levels = par_dict['control_levels'] if 'control_levels' in par_dict else np.ones((num_el,))
|
| 272 |
+
|
| 273 |
+
superquadrics = {}
|
| 274 |
+
for k in range(num_el):
|
| 275 |
+
superquadric_dict = {}
|
| 276 |
+
superquadric_dict['scale'] = scale[k, :]
|
| 277 |
+
superquadric_dict['shape'] = shapes[k]
|
| 278 |
+
superquadric_dict['rotation'] = rotate[k, :]
|
| 279 |
+
superquadric_dict['translation'] = trans[k, :]
|
| 280 |
+
superquadric_dict['tapering'] = tapering[k, :]
|
| 281 |
+
superquadric_dict['bending'] = bending[k, :]
|
| 282 |
+
superquadric_dict['control_level'] = 'low' if float(control_levels[k]) < 0.5 else 'high'
|
| 283 |
+
superquadric_dict['color'] = [90, 200, 255]
|
| 284 |
+
superquadrics[k] = superquadric_dict
|
| 285 |
+
return superquadrics
|
| 286 |
+
|
| 287 |
+
def export_colored_superquadrics_glb(npz_path, output_path, aabb=None, center=None, scale=None):
|
| 288 |
+
start = time.perf_counter()
|
| 289 |
+
superquadrics = load_superquadric_from_file(npz_path)
|
| 290 |
+
meshes = []
|
| 291 |
+
raw_meshes = []
|
| 292 |
+
for sq_id in superquadrics:
|
| 293 |
+
sq = superquadrics[sq_id]
|
| 294 |
+
vertices, triangles = add_superquadric_compact_rot_mat(
|
| 295 |
+
sq['scale'],
|
| 296 |
+
sq['shape'],
|
| 297 |
+
sq['translation'],
|
| 298 |
+
sq['rotation'],
|
| 299 |
+
sq['tapering'],
|
| 300 |
+
sq['bending'],
|
| 301 |
+
resolution=100,
|
| 302 |
+
)
|
| 303 |
+
raw_mesh = trimesh.Trimesh(vertices=vertices, faces=np.asarray(triangles), process=False)
|
| 304 |
+
raw_meshes.append(raw_mesh)
|
| 305 |
+
|
| 306 |
+
if not raw_meshes:
|
| 307 |
+
raise ValueError(f"No superquadrics found in {npz_path}")
|
| 308 |
+
|
| 309 |
+
if aabb is None or center is None or scale is None:
|
| 310 |
+
all_vertices = np.concatenate([mesh.vertices for mesh in raw_meshes], axis=0)
|
| 311 |
+
aabb = np.stack([all_vertices.min(0), all_vertices.max(0)])
|
| 312 |
+
center = (aabb[0] + aabb[1]) / 2
|
| 313 |
+
scale = 1.0 / ((aabb[1] - aabb[0]).max())
|
| 314 |
+
|
| 315 |
+
for raw_mesh, sq_id in zip(raw_meshes, superquadrics):
|
| 316 |
+
sq = superquadrics[sq_id]
|
| 317 |
+
mesh = raw_mesh.copy()
|
| 318 |
+
mesh.vertices = (mesh.vertices - center) * scale
|
| 319 |
+
color = INPUT_SQ_LOW_COLOR if sq.get('control_level') == 'low' else INPUT_SQ_HIGH_COLOR
|
| 320 |
+
mesh.visual = trimesh.visual.ColorVisuals(
|
| 321 |
+
mesh,
|
| 322 |
+
vertex_colors=np.tile(color, (mesh.vertices.shape[0], 1)),
|
| 323 |
+
)
|
| 324 |
+
meshes.append(mesh)
|
| 325 |
+
|
| 326 |
+
scene = trimesh.Scene()
|
| 327 |
+
for sq_id, mesh in zip(superquadrics, meshes):
|
| 328 |
+
scene.add_geometry(mesh, node_name=f"superquadric_{sq_id}", geom_name=f"superquadric_{sq_id}")
|
| 329 |
+
scene.export(output_path)
|
| 330 |
+
log.info(
|
| 331 |
+
"Colored input superquadrics GLB exported in %.2fs: %s (%d SQs)",
|
| 332 |
+
time.perf_counter() - start,
|
| 333 |
+
output_path,
|
| 334 |
+
len(meshes),
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
def load_superquadrics(path, spatial_control_mesh_path, aabb=None, center=None, scale=None):
|
| 338 |
+
start = time.perf_counter()
|
| 339 |
+
# Generate spatial control mesh from superquadric primitives and write to spatial_control_mesh_path
|
| 340 |
+
superquadrics = load_superquadric_from_file(path)
|
| 341 |
+
|
| 342 |
+
meshes = []
|
| 343 |
+
for superquadric_id in superquadrics.keys():
|
| 344 |
+
vertices, triangles = add_superquadric_compact_rot_mat(
|
| 345 |
+
superquadrics[superquadric_id]['scale'],
|
| 346 |
+
superquadrics[superquadric_id]['shape'],
|
| 347 |
+
superquadrics[superquadric_id]['translation'],
|
| 348 |
+
superquadrics[superquadric_id]['rotation'],
|
| 349 |
+
superquadrics[superquadric_id]['tapering'],
|
| 350 |
+
superquadrics[superquadric_id]['bending'],
|
| 351 |
+
resolution=100)
|
| 352 |
+
mesh = o3d.geometry.TriangleMesh()
|
| 353 |
+
mesh.vertices = o3d.utility.Vector3dVector(vertices)
|
| 354 |
+
mesh.triangles = o3d.utility.Vector3iVector(triangles)
|
| 355 |
+
meshes.append(mesh)
|
| 356 |
+
merged_mesh = merge_meshes(meshes)
|
| 357 |
+
aabb = np.stack([np.asarray(merged_mesh.vertices).min(0), np.asarray(merged_mesh.vertices).max(0)]) if aabb is None else aabb
|
| 358 |
+
center = (aabb[0] + aabb[1]) / 2 if center is None else center
|
| 359 |
+
scale = 1/((aabb[1] - aabb[0]).max()) if scale is None else scale
|
| 360 |
+
|
| 361 |
+
merged_mesh.translate(-center)
|
| 362 |
+
merged_mesh.scale(scale, (0,0,0))
|
| 363 |
+
o3d.io.write_triangle_mesh(spatial_control_mesh_path, merged_mesh)
|
| 364 |
+
log.info(
|
| 365 |
+
"Spatial control mesh generated from superquadrics in %.2fs: %s (%d SQs)",
|
| 366 |
+
time.perf_counter() - start,
|
| 367 |
+
spatial_control_mesh_path,
|
| 368 |
+
len(superquadrics),
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
if aabb is not None and center is not None and scale is not None:
|
| 372 |
+
return aabb, center, scale
|
| 373 |
+
|
| 374 |
+
def build_individual_sq_meshes_normalized(npz_path):
|
| 375 |
+
"""Build individual superquadric meshes in the same normalised space as the merged mesh.
|
| 376 |
+
|
| 377 |
+
The merged mesh is centred and scaled to unit size (same as load_superquadrics).
|
| 378 |
+
Returns a list of open3d TriangleMesh objects in the [-0.5, 0.5] coordinate space.
|
| 379 |
+
"""
|
| 380 |
+
superquadrics = load_superquadric_from_file(npz_path)
|
| 381 |
+
meshes = []
|
| 382 |
+
for sq_id in superquadrics:
|
| 383 |
+
vertices, triangles = add_superquadric_compact_rot_mat(
|
| 384 |
+
superquadrics[sq_id]['scale'],
|
| 385 |
+
superquadrics[sq_id]['shape'],
|
| 386 |
+
superquadrics[sq_id]['translation'],
|
| 387 |
+
superquadrics[sq_id]['rotation'],
|
| 388 |
+
superquadrics[sq_id]['tapering'],
|
| 389 |
+
superquadrics[sq_id]['bending'],
|
| 390 |
+
resolution=100)
|
| 391 |
+
mesh = o3d.geometry.TriangleMesh()
|
| 392 |
+
mesh.vertices = o3d.utility.Vector3dVector(vertices)
|
| 393 |
+
mesh.triangles = o3d.utility.Vector3iVector(triangles)
|
| 394 |
+
meshes.append(mesh)
|
| 395 |
+
|
| 396 |
+
# Compute normalization from merged mesh (mirrors load_superquadrics)
|
| 397 |
+
merged = merge_meshes(meshes)
|
| 398 |
+
all_verts = np.asarray(merged.vertices)
|
| 399 |
+
aabb = np.stack([all_verts.min(0), all_verts.max(0)])
|
| 400 |
+
center = (aabb[0] + aabb[1]) / 2
|
| 401 |
+
scale = 1.0 / ((aabb[1] - aabb[0]).max())
|
| 402 |
+
|
| 403 |
+
normalized = []
|
| 404 |
+
for mesh in meshes:
|
| 405 |
+
m = copy.deepcopy(mesh)
|
| 406 |
+
m.translate(-center)
|
| 407 |
+
m.scale(scale, (0, 0, 0))
|
| 408 |
+
normalized.append(m)
|
| 409 |
+
return normalized
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def count_superquadrics(npz_path):
|
| 413 |
+
with np.load(npz_path) as par_dict:
|
| 414 |
+
return int(par_dict['scales'].shape[0])
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def parse_local_condition_list(raw_json, num_superquadrics, label, require_existing_files=False):
|
| 418 |
+
if not raw_json:
|
| 419 |
+
return None
|
| 420 |
+
values = json.loads(raw_json)
|
| 421 |
+
if not isinstance(values, list):
|
| 422 |
+
raise ValueError(f"{label} must be a JSON list")
|
| 423 |
+
if len(values) > num_superquadrics:
|
| 424 |
+
raise ValueError(f"{label} has {len(values)} entries but only {num_superquadrics} superquadrics")
|
| 425 |
+
normalized = [str(value or "").strip() for value in values]
|
| 426 |
+
normalized.extend([""] * (num_superquadrics - len(normalized)))
|
| 427 |
+
if require_existing_files:
|
| 428 |
+
missing = [path for path in normalized if path and not osp.exists(path)]
|
| 429 |
+
if missing:
|
| 430 |
+
raise FileNotFoundError(f"Local image path(s) not found: {missing}")
|
| 431 |
+
return normalized if any(value for value in normalized) else None
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
def sparse_voxels_to_glb(sparse_points, grid_size=64, output_filename="output.glb"):
|
| 435 |
+
"""
|
| 436 |
+
Converts sparse voxel coordinates to a GLB mesh.
|
| 437 |
+
|
| 438 |
+
:param sparse_points: List or array of (x, y, z) coordinates (integers).
|
| 439 |
+
:param grid_size: The size of the voxel bounding box (e.g., 64).
|
| 440 |
+
:param output_filename: The name of the output GLB file.
|
| 441 |
+
"""
|
| 442 |
+
|
| 443 |
+
start = time.perf_counter()
|
| 444 |
+
log.info("Creating %dx%dx%d sparse voxel grid...", grid_size, grid_size, grid_size)
|
| 445 |
+
# Init grid
|
| 446 |
+
voxel_grid = np.zeros((grid_size, grid_size, grid_size), dtype=bool)
|
| 447 |
+
|
| 448 |
+
# Populate grid with sparse points
|
| 449 |
+
sparse_points = np.round(sparse_points).astype(int)
|
| 450 |
+
for x, y, z in sparse_points:
|
| 451 |
+
# Check bounds just in case
|
| 452 |
+
if 0 <= x < grid_size and 0 <= y < grid_size and 0 <= z < grid_size:
|
| 453 |
+
voxel_grid[x, y, z] = True
|
| 454 |
+
|
| 455 |
+
# Padding the grid (Needed for marching cubes)
|
| 456 |
+
padded_grid = np.pad(voxel_grid, pad_width=1, mode='constant', constant_values=False)
|
| 457 |
+
|
| 458 |
+
log.info("Running Marching Cubes algorithm...")
|
| 459 |
+
# Marching Cubes (Level set at 0.5 to extract the surface between occupied and empty voxels)
|
| 460 |
+
verts, faces, normals, values = measure.marching_cubes(padded_grid, level=0.5)
|
| 461 |
+
|
| 462 |
+
# Shift vertices back by 1 to account for the padding we added
|
| 463 |
+
verts = verts - 1.0
|
| 464 |
+
|
| 465 |
+
log.info("Generating mesh and exporting to GLB...")
|
| 466 |
+
mesh = trimesh.Trimesh(vertices=verts, faces=faces, vertex_normals=normals)
|
| 467 |
+
|
| 468 |
+
# # Smoothing of the mesh
|
| 469 |
+
# trimesh.smoothing.filter_taubin(mesh, iterations=50)
|
| 470 |
+
|
| 471 |
+
# Export to GLB format
|
| 472 |
+
mesh.export(output_filename)
|
| 473 |
+
log.info(
|
| 474 |
+
"Successfully exported sparse voxel mesh in %.2fs: %s",
|
| 475 |
+
time.perf_counter() - start,
|
| 476 |
+
output_filename,
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
def predict_part(obj_path, output_dir):
|
| 480 |
+
start = time.perf_counter()
|
| 481 |
+
log.info("Extracting PartField feature planes...")
|
| 482 |
+
partfield_config = 'third_party/PartField/config.yaml'
|
| 483 |
+
partfield_cfg = OmegaConf.load(partfield_config)
|
| 484 |
+
partfield_cfg.dataset.val_num_workers = 0
|
| 485 |
+
partfield_ckpt = partfield_cfg.continue_ckpt
|
| 486 |
+
if partfield_ckpt and not osp.isabs(partfield_ckpt):
|
| 487 |
+
partfield_ckpt = osp.join(osp.dirname(osp.abspath(partfield_config)), partfield_ckpt)
|
| 488 |
+
|
| 489 |
+
seed_everything(partfield_cfg.seed)
|
| 490 |
+
|
| 491 |
+
torch.manual_seed(0)
|
| 492 |
+
random.seed(0)
|
| 493 |
+
np.random.seed(0)
|
| 494 |
+
|
| 495 |
+
# Lightning defaults to ./lightning_logs under cwd (repo root); team members often
|
| 496 |
+
# cannot mkdir there. Keep all PL artifacts under this run's output_dir.
|
| 497 |
+
pl_root = osp.join(output_dir, 'pl_partfield')
|
| 498 |
+
common.ensure_dir(pl_root)
|
| 499 |
+
|
| 500 |
+
trainer = Trainer(devices=1,
|
| 501 |
+
accelerator="gpu",
|
| 502 |
+
precision="16-mixed",
|
| 503 |
+
max_epochs=partfield_cfg.training_epochs,
|
| 504 |
+
log_every_n_steps=1,
|
| 505 |
+
limit_train_batches=3500,
|
| 506 |
+
limit_val_batches=None,
|
| 507 |
+
default_root_dir=pl_root,
|
| 508 |
+
logger=False,
|
| 509 |
+
enable_checkpointing=False,
|
| 510 |
+
enable_progress_bar=False,
|
| 511 |
+
enable_model_summary=False,
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
partfield_model = Model(partfield_cfg, obj_path)
|
| 515 |
+
with partfield_checkpoint_scope():
|
| 516 |
+
output = trainer.predict(partfield_model, ckpt_path=partfield_ckpt)
|
| 517 |
+
part_planes, uid = output[0]
|
| 518 |
+
np.save(f'{output_dir}/part_feat_{uid}_batch_part_plane.npy', part_planes)
|
| 519 |
+
|
| 520 |
+
del partfield_model
|
| 521 |
+
gc.collect() # Free up memory
|
| 522 |
+
log.info("Extracted PartField feature planes in %.2fs", time.perf_counter() - start)
|
| 523 |
+
|
| 524 |
+
def run(args, cfg=None, generation_pipeline=None):
|
| 525 |
+
run_start = time.perf_counter()
|
| 526 |
+
reset_run_state()
|
| 527 |
+
cfg = cfg or OmegaConf.load('config/default.yaml')
|
| 528 |
+
if args.texture_optim_steps is not None:
|
| 529 |
+
if args.texture_optim_steps < 2:
|
| 530 |
+
raise ValueError("--texture_optim_steps must be an integer at least 2")
|
| 531 |
+
cfg = copy.deepcopy(cfg)
|
| 532 |
+
cfg.sim_guidance.steps = int(args.texture_optim_steps)
|
| 533 |
+
log.info("Overriding texture optimization steps: %d", int(cfg.sim_guidance.steps))
|
| 534 |
+
if args.mixed_self_attn_boost is not None:
|
| 535 |
+
if args.mixed_self_attn_boost < 1.0:
|
| 536 |
+
raise ValueError("--mixed_self_attn_boost must be at least 1.0 (1.0 = stock attention)")
|
| 537 |
+
cfg = copy.deepcopy(cfg)
|
| 538 |
+
cfg.sim_guidance.mixed_self_attn_boost = float(args.mixed_self_attn_boost)
|
| 539 |
+
log.info("Overriding mixed self-attention boost: %.2f",
|
| 540 |
+
float(cfg.sim_guidance.mixed_self_attn_boost))
|
| 541 |
+
|
| 542 |
+
common.ensure_dir(args.output_dir)
|
| 543 |
+
|
| 544 |
+
def copy_input_npz(src_path, output_name):
|
| 545 |
+
if not src_path:
|
| 546 |
+
return
|
| 547 |
+
dst_path = osp.join(args.output_dir, output_name)
|
| 548 |
+
shutil.copy2(src_path, dst_path)
|
| 549 |
+
log.info(f"Copied input superquadrics: {dst_path}")
|
| 550 |
+
|
| 551 |
+
copy_input_npz(args.shape_superquadric_path, 'input_superquadrics_all.npz')
|
| 552 |
+
copy_input_npz(args.shape_superquadric_high_control_path, 'input_superquadrics_high_control.npz')
|
| 553 |
+
copy_input_npz(args.low_control_superquadric_mask_path, 'input_superquadrics_low_control_bbox.npz')
|
| 554 |
+
|
| 555 |
+
# Generate spatial control mesh from superquadrics
|
| 556 |
+
spatial_control_mesh_path = osp.join(args.output_dir, 'spatial_control_mesh.ply')
|
| 557 |
+
control_mesh_start = time.perf_counter()
|
| 558 |
+
aabb, center, scale = load_superquadrics(args.shape_superquadric_path, spatial_control_mesh_path)
|
| 559 |
+
export_colored_superquadrics_glb(
|
| 560 |
+
args.shape_superquadric_path,
|
| 561 |
+
osp.join(args.output_dir, INPUT_SQ_COLORED_GLB),
|
| 562 |
+
aabb=aabb,
|
| 563 |
+
center=center,
|
| 564 |
+
scale=scale,
|
| 565 |
+
)
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
low_control_superquadric_mask_path = None
|
| 569 |
+
if args.shape_tau_high_control is not None:
|
| 570 |
+
assert args.shape_tau_high_control >= args.shape_tau, "shape_tau_high_control must be greater than or equal to shape_tau"
|
| 571 |
+
|
| 572 |
+
print(f"Using high control tau: {args.shape_tau_high_control} and low control tau: {args.shape_tau}, with local tau mode: {args.local_tau_mode}")
|
| 573 |
+
high_control_spatial_control_mesh_path = osp.join(args.output_dir, 'high_control_spatial_control_mesh.ply')
|
| 574 |
+
load_superquadrics(args.shape_superquadric_high_control_path, high_control_spatial_control_mesh_path, aabb=aabb, center=center, scale=scale)
|
| 575 |
+
|
| 576 |
+
if args.local_tau_mode == 'low_control_mask':
|
| 577 |
+
low_control_superquadric_mask_path = osp.join(args.output_dir, 'low_control_superquadric_mask.ply')
|
| 578 |
+
load_superquadrics(args.low_control_superquadric_mask_path, low_control_superquadric_mask_path, aabb=aabb, center=center, scale=scale)
|
| 579 |
+
log.info("Prepared all superquadric control meshes in %.2fs", time.perf_counter() - control_mesh_start)
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
# Load structure mesh
|
| 583 |
+
log.info("Creating structure mesh with SpaceControl code...")
|
| 584 |
+
|
| 585 |
+
owns_pipeline = generation_pipeline is None
|
| 586 |
+
if owns_pipeline:
|
| 587 |
+
pipeline = load_trellis_pipeline(args, cfg)
|
| 588 |
+
else:
|
| 589 |
+
pipeline = generation_pipeline
|
| 590 |
+
log.info(f"Reusing preloaded TRELLIS pipeline from: {trellis_pipeline_path_from_args(args, cfg)}")
|
| 591 |
+
cuda_start = time.perf_counter()
|
| 592 |
+
pipeline.cuda()
|
| 593 |
+
move_trellis_text_conditioner(pipeline, 'cuda')
|
| 594 |
+
log.info(f"Moved TRELLIS pipeline to CUDA in {time.perf_counter() - cuda_start:.2f}s")
|
| 595 |
+
|
| 596 |
+
text_prompt = args.text_prompt
|
| 597 |
+
|
| 598 |
+
# Sparse voxels
|
| 599 |
+
structure_start = time.perf_counter()
|
| 600 |
+
log.info("Generating TRELLIS sparse structure with seed %d", int(args.structure_seed))
|
| 601 |
+
coords = pipeline.gen_structure_v2(text_prompt, seed=int(args.structure_seed), vis_output_dir=None, sparse_structure_sampler_params={
|
| 602 |
+
"steps": STEPS_SHAPE_GEN,
|
| 603 |
+
"cfg_strength": CFG_SHAPE_GEN,
|
| 604 |
+
"t0_idx_value": args.shape_tau,
|
| 605 |
+
"spatial_control_mesh_path": spatial_control_mesh_path,
|
| 606 |
+
"high_control_spatial_control_mesh_path": high_control_spatial_control_mesh_path if args.shape_tau_high_control is not None else None,
|
| 607 |
+
"low_control_superquadric_mask_path": low_control_superquadric_mask_path if low_control_superquadric_mask_path is not None else None,
|
| 608 |
+
"t0_idx_value_high_control": args.shape_tau_high_control if args.shape_tau_high_control is not None else None,
|
| 609 |
+
"polyak_update_tau": args.polyak_update_tau,
|
| 610 |
+
"local_tau_mode": args.local_tau_mode,
|
| 611 |
+
"n_repaint_steps": args.n_repaint_steps,
|
| 612 |
+
})
|
| 613 |
+
log.info(
|
| 614 |
+
"Generated TRELLIS sparse structure in %.2fs (%d voxels)",
|
| 615 |
+
time.perf_counter() - structure_start,
|
| 616 |
+
coords.shape[0],
|
| 617 |
+
)
|
| 618 |
+
|
| 619 |
+
# Convert sparse voxels to mesh
|
| 620 |
+
log.info("Converting sparse voxels to mesh...")
|
| 621 |
+
|
| 622 |
+
coords_np = coords.detach().cpu().numpy()
|
| 623 |
+
|
| 624 |
+
filtered_coords = coords_np[:, 1:]
|
| 625 |
+
print(f"Sparse voxel tensor shape: {coords_np.shape}")
|
| 626 |
+
print(f"Number of valid voxels: {filtered_coords.shape[0]}")
|
| 627 |
+
|
| 628 |
+
sparse_voxels_to_glb(filtered_coords, grid_size=64, output_filename=osp.join(args.output_dir, "sample.glb"))
|
| 629 |
+
|
| 630 |
+
# glb.export("sample.glb")
|
| 631 |
+
|
| 632 |
+
# log.info("Loading generated mesh...")
|
| 633 |
+
|
| 634 |
+
mesh_export_start = time.perf_counter()
|
| 635 |
+
struct_mesh = trimesh.load(osp.join(args.output_dir, "sample.glb"), force='mesh')
|
| 636 |
+
# Generator / marching-cubes output in Y-up; keep a copy for debugging.
|
| 637 |
+
struct_mesh.export(osp.join(args.output_dir, 'struct_mesh.glb'))
|
| 638 |
+
log.info("Loaded and exported raw structure mesh in %.2fs", time.perf_counter() - mesh_export_start)
|
| 639 |
+
|
| 640 |
+
if args.full_pipeline and args.guidance_mode == 'similarity':
|
| 641 |
+
log.info("Keeping TRELLIS pipeline loaded for similarity guidance; offloading it until refinement.")
|
| 642 |
+
offload_trellis_pipeline(pipeline)
|
| 643 |
+
else:
|
| 644 |
+
if owns_pipeline:
|
| 645 |
+
del pipeline
|
| 646 |
+
gc.collect() # Free up memory
|
| 647 |
+
else:
|
| 648 |
+
offload_trellis_pipeline(pipeline)
|
| 649 |
+
|
| 650 |
+
# Canonical mesh for renders, voxels, and PartField must share one frame (Z-up if converting).
|
| 651 |
+
normalize_start = time.perf_counter()
|
| 652 |
+
if args.convert_yup_to_zup:
|
| 653 |
+
struct_mesh = pointcloud.convert_mesh_yup_to_zup(struct_mesh)
|
| 654 |
+
struct_mesh.export(osp.join(args.output_dir, 'struct_mesh_zup.glb'))
|
| 655 |
+
log.info("Prepared canonical structure mesh in %.2fs", time.perf_counter() - normalize_start)
|
| 656 |
+
struct_mesh_for_pipeline = osp.join(args.output_dir, 'struct_mesh_zup.glb')
|
| 657 |
+
|
| 658 |
+
struct_render_dir = osp.join(args.output_dir, 'struct_renders')
|
| 659 |
+
common.ensure_dir(struct_render_dir)
|
| 660 |
+
if args.full_pipeline and args.guidance_mode == 'similarity':
|
| 661 |
+
render_start = time.perf_counter()
|
| 662 |
+
log.info("Exporting Blender-normalized structure mesh without PNG renders...")
|
| 663 |
+
out_renderviews = render.export_normalized_mesh(struct_mesh_for_pipeline, struct_render_dir)
|
| 664 |
+
log.info("Exported Blender-normalized structure mesh in %.2fs", time.perf_counter() - render_start)
|
| 665 |
+
else:
|
| 666 |
+
render_start = time.perf_counter()
|
| 667 |
+
log.info(f"Rendering structure mesh for {cfg.num_views // 10} views...")
|
| 668 |
+
out_renderviews = render.render_all_views(struct_mesh_for_pipeline, struct_render_dir, num_views=cfg.num_views // 10)
|
| 669 |
+
log.info("Rendered structure mesh views in %.2fs", time.perf_counter() - render_start)
|
| 670 |
+
|
| 671 |
+
# struct_renders/mesh.ply is the Blender-normalized mesh; use it as the single source of truth
|
| 672 |
+
# for both voxelization and PartField feature extraction so that both operate in the same
|
| 673 |
+
# coordinate system (Blender's normalize_scene + GLTF→Blender axis convention).
|
| 674 |
+
struct_blender_ply = osp.join(struct_render_dir, 'mesh.ply')
|
| 675 |
+
|
| 676 |
+
voxel_dir = osp.join(args.output_dir, 'voxels')
|
| 677 |
+
common.ensure_dir(voxel_dir)
|
| 678 |
+
voxel_start = time.perf_counter()
|
| 679 |
+
log.info("Voxelizing structure mesh...")
|
| 680 |
+
pointcloud.voxelize_mesh(struct_blender_ply, save_path=osp.join(voxel_dir, 'struct_voxels.ply'))
|
| 681 |
+
log.info("Voxelized structure mesh in %.2fs", time.perf_counter() - voxel_start)
|
| 682 |
+
|
| 683 |
+
if not args.full_pipeline:
|
| 684 |
+
log.info("Structure-only mode complete. Pass --full_pipeline to continue into PartField and refinement.")
|
| 685 |
+
return
|
| 686 |
+
|
| 687 |
+
log.info("Extracting Structure Mesh PartField feature planes...")
|
| 688 |
+
partfield_dir = osp.join(args.output_dir, 'partfield')
|
| 689 |
+
common.ensure_dir(partfield_dir)
|
| 690 |
+
# Use the same Blender-normalized PLY so the PartField triplane canonical space
|
| 691 |
+
# matches the coordinate system of struct_voxels.ply.
|
| 692 |
+
partfield_start = time.perf_counter()
|
| 693 |
+
predict_part(struct_blender_ply, partfield_dir)
|
| 694 |
+
log.info("Completed Structure Mesh PartField extraction in %.2fs", time.perf_counter() - partfield_start)
|
| 695 |
+
|
| 696 |
+
log.info("Visualizing PartField clusters on structure mesh...")
|
| 697 |
+
from lib.util.visualization import visualize_and_save, map_voxel_labels_to_vertices
|
| 698 |
+
from lib.util.partfield import cluster_geoms
|
| 699 |
+
_sv = utils3d.io.read_ply(osp.join(voxel_dir, 'struct_voxels.ply'))[0]
|
| 700 |
+
_sc = torch.from_numpy(_sv).float().cuda()
|
| 701 |
+
_sc4d = torch.cat([torch.zeros(_sc.shape[0], 1, dtype=torch.long, device='cuda'),
|
| 702 |
+
((_sc + 0.5) * 64).long()], dim=1)
|
| 703 |
+
_planes = torch.from_numpy(np.load(
|
| 704 |
+
osp.join(partfield_dir, 'part_feat_mesh_batch_part_plane.npy'),
|
| 705 |
+
allow_pickle=True)).cuda()
|
| 706 |
+
_vlabels = cluster_geoms(_sc4d, _planes, num_clusters=cfg.sim_guidance.num_part_clusters)
|
| 707 |
+
_mesh_vis = trimesh.load(struct_blender_ply, force='mesh')
|
| 708 |
+
_vtx_labels = map_voxel_labels_to_vertices(_mesh_vis.vertices, _sv, _vlabels)
|
| 709 |
+
visualize_and_save(_mesh_vis, _vtx_labels, args.output_dir, output_name='partfield_clusters.mp4')
|
| 710 |
+
del _sv, _sc, _sc4d, _planes, _vlabels, _mesh_vis, _vtx_labels
|
| 711 |
+
gc.collect()
|
| 712 |
+
|
| 713 |
+
if not out_renderviews:
|
| 714 |
+
log.info("Structure rendering failed!")
|
| 715 |
+
|
| 716 |
+
log.info("Running similarity-guided optimization...")
|
| 717 |
+
|
| 718 |
+
if args.appearance_image:
|
| 719 |
+
app_type = 'image'
|
| 720 |
+
app = args.appearance_image
|
| 721 |
+
|
| 722 |
+
app_image = Image.open(args.appearance_image).convert('RGB')
|
| 723 |
+
app_image.save(osp.join(args.output_dir, 'app_image.png'))
|
| 724 |
+
|
| 725 |
+
else:
|
| 726 |
+
app_type = 'text'
|
| 727 |
+
app = args.appearance_text
|
| 728 |
+
|
| 729 |
+
log.info(f"Using {app_type} for self-similarity guidance...")
|
| 730 |
+
|
| 731 |
+
# Parse per-SQ local conditioning args
|
| 732 |
+
num_superquadrics = count_superquadrics(args.shape_superquadric_path)
|
| 733 |
+
local_text_prompts = (
|
| 734 |
+
parse_local_condition_list(args.local_text_prompts, num_superquadrics, "local_text_prompts")
|
| 735 |
+
if app_type == 'text'
|
| 736 |
+
else None
|
| 737 |
+
)
|
| 738 |
+
local_image_paths = (
|
| 739 |
+
parse_local_condition_list(
|
| 740 |
+
args.local_image_paths,
|
| 741 |
+
num_superquadrics,
|
| 742 |
+
"local_image_paths",
|
| 743 |
+
require_existing_files=True,
|
| 744 |
+
)
|
| 745 |
+
if app_type == 'image'
|
| 746 |
+
else None
|
| 747 |
+
)
|
| 748 |
+
|
| 749 |
+
local_prompts = local_text_prompts if app_type == 'text' else local_image_paths
|
| 750 |
+
local_prompt_type = app_type if local_prompts else None
|
| 751 |
+
individual_sq_meshes = build_individual_sq_meshes_normalized(args.shape_superquadric_path) if local_prompts else None
|
| 752 |
+
|
| 753 |
+
# Self-Similarity Optimization
|
| 754 |
+
sim_start = time.perf_counter()
|
| 755 |
+
self_similarity.optimize_self_similarity(
|
| 756 |
+
cfg, app, app_type, args.output_dir,
|
| 757 |
+
local_prompts=local_prompts,
|
| 758 |
+
local_prompt_type=local_prompt_type,
|
| 759 |
+
individual_sq_meshes=individual_sq_meshes,
|
| 760 |
+
generation_pipeline=pipeline,
|
| 761 |
+
decode_texture=not args.geometry_only_decode,
|
| 762 |
+
)
|
| 763 |
+
log.info("Completed SpaceFlow run in %.2fs (similarity optimization %.2fs)",
|
| 764 |
+
time.perf_counter() - run_start,
|
| 765 |
+
time.perf_counter() - sim_start)
|
| 766 |
+
|
| 767 |
+
|
| 768 |
+
def main(argv=None):
|
| 769 |
+
args = init_args(argv)
|
| 770 |
+
run(args)
|
| 771 |
+
|
| 772 |
+
if __name__ == "__main__":
|
| 773 |
+
main()
|
third_party/PartField/LICENSE
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
NVIDIA License
|
| 2 |
+
|
| 3 |
+
1. Definitions
|
| 4 |
+
|
| 5 |
+
“Licensor” means any person or entity that distributes its Work.
|
| 6 |
+
“Work” means (a) the original work of authorship made available under this license, which may include software, documentation, or other files, and (b) any additions to or derivative works thereof that are made available under this license.
|
| 7 |
+
The terms “reproduce,” “reproduction,” “derivative works,” and “distribution” have the meaning as provided under U.S. copyright law; provided, however, that for the purposes of this license, derivative works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work.
|
| 8 |
+
Works are “made available” under this license by including in or with the Work either (a) a copyright notice referencing the applicability of this license to the Work, or (b) a copy of this license.
|
| 9 |
+
|
| 10 |
+
2. License Grant
|
| 11 |
+
|
| 12 |
+
2.1 Copyright Grant. Subject to the terms and conditions of this license, each Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free, copyright license to use, reproduce, prepare derivative works of, publicly display, publicly perform, sublicense and distribute its Work and any resulting derivative works in any form.
|
| 13 |
+
|
| 14 |
+
3. Limitations
|
| 15 |
+
|
| 16 |
+
3.1 Redistribution. You may reproduce or distribute the Work only if (a) you do so under this license, (b) you include a complete copy of this license with your distribution, and (c) you retain without modification any copyright, patent, trademark, or attribution notices that are present in the Work.
|
| 17 |
+
|
| 18 |
+
3.2 Derivative Works. You may specify that additional or different terms apply to the use, reproduction, and distribution of your derivative works of the Work (“Your Terms”) only if (a) Your Terms provide that the use limitation in Section 3.3 applies to your derivative works, and (b) you identify the specific derivative works that are subject to Your Terms. Notwithstanding Your Terms, this license (including the redistribution requirements in Section 3.1) will continue to apply to the Work itself.
|
| 19 |
+
|
| 20 |
+
3.3 Use Limitation. The Work and any derivative works thereof only may be used or intended for use non-commercially. Notwithstanding the foregoing, NVIDIA Corporation and its affiliates may use the Work and any derivative works commercially. As used herein, “non-commercially” means for non-commercial research and educational purposes only.
|
| 21 |
+
|
| 22 |
+
3.4 Patent Claims. If you bring or threaten to bring a patent claim against any Licensor (including any claim, cross-claim or counterclaim in a lawsuit) to enforce any patents that you allege are infringed by any Work, then your rights under this license from such Licensor (including the grant in Section 2.1) will terminate immediately.
|
| 23 |
+
|
| 24 |
+
3.5 Trademarks. This license does not grant any rights to use any Licensor’s or its affiliates’ names, logos, or trademarks, except as necessary to reproduce the notices described in this license.
|
| 25 |
+
|
| 26 |
+
3.6 Termination. If you violate any term of this license, then your rights under this license (including the grant in Section 2.1) will terminate immediately.
|
| 27 |
+
|
| 28 |
+
4. Disclaimer of Warranty.
|
| 29 |
+
|
| 30 |
+
THE WORK IS PROVIDED “AS IS” WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OR CONDITIONS OF
|
| 31 |
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT. YOU BEAR THE RISK OF UNDERTAKING ANY ACTIVITIES UNDER THIS LICENSE.
|
| 32 |
+
|
| 33 |
+
5. Limitation of Liability.
|
| 34 |
+
|
| 35 |
+
EXCEPT AS PROHIBITED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE SHALL ANY LICENSOR BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATED TO THIS LICENSE, THE USE OR INABILITY TO USE THE WORK (INCLUDING BUT NOT LIMITED TO LOSS OF GOODWILL, BUSINESS INTERRUPTION, LOST PROFITS OR DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY OTHER DAMAGES OR LOSSES), EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
| 36 |
+
|
third_party/PartField/config.yaml
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 0
|
| 2 |
+
output_dir: "results"
|
| 3 |
+
result_name: "demo"
|
| 4 |
+
|
| 5 |
+
triplet_sampling: "random"
|
| 6 |
+
load_original_mesh: false
|
| 7 |
+
|
| 8 |
+
num_pos: 64
|
| 9 |
+
num_neg_random: 256
|
| 10 |
+
num_neg_hard_pc: 128
|
| 11 |
+
num_neg_hard_emb: 128
|
| 12 |
+
|
| 13 |
+
vertex_feature: false # if true, sample feature on vertices; if false, sample feature on faces
|
| 14 |
+
n_point_per_face: 1000
|
| 15 |
+
n_sample_each: 10000
|
| 16 |
+
preprocess_mesh: false
|
| 17 |
+
|
| 18 |
+
regress_2d_feat: false
|
| 19 |
+
|
| 20 |
+
is_pc: false
|
| 21 |
+
|
| 22 |
+
cut_manifold: false
|
| 23 |
+
remesh_demo: false
|
| 24 |
+
correspondence_demo: false
|
| 25 |
+
|
| 26 |
+
save_every_epoch: 10
|
| 27 |
+
training_epochs: 30
|
| 28 |
+
continue_training: false
|
| 29 |
+
|
| 30 |
+
continue_ckpt: models/model_objaverse.ckpt
|
| 31 |
+
epoch_selected: "epoch=50.ckpt"
|
| 32 |
+
|
| 33 |
+
triplane_resolution: 128
|
| 34 |
+
triplane_channels_low: 128
|
| 35 |
+
triplane_channels_high: 512
|
| 36 |
+
lr: 1.0e-3
|
| 37 |
+
train: true
|
| 38 |
+
test: false
|
| 39 |
+
|
| 40 |
+
inference_save_pred_sdf_to_mesh: true
|
| 41 |
+
inference_save_feat_pca: true
|
| 42 |
+
name: "test"
|
| 43 |
+
test_subset: false
|
| 44 |
+
test_corres: false
|
| 45 |
+
test_partobjaversetiny: false
|
| 46 |
+
|
| 47 |
+
dataset:
|
| 48 |
+
type: "Mix"
|
| 49 |
+
data_path: "objaverse_data"
|
| 50 |
+
train_batch_size: 1
|
| 51 |
+
val_batch_size: 1
|
| 52 |
+
train_num_workers: 8
|
| 53 |
+
val_num_workers: 32
|
| 54 |
+
all_files: [] # only used for correspondence demo
|
| 55 |
+
|
| 56 |
+
voxel2triplane:
|
| 57 |
+
transformer_dim: 1024
|
| 58 |
+
transformer_layers: 6
|
| 59 |
+
transformer_heads: 8
|
| 60 |
+
triplane_low_res: 32
|
| 61 |
+
triplane_high_res: 256
|
| 62 |
+
triplane_dim: 64
|
| 63 |
+
normalize_vox_feat: false
|
| 64 |
+
|
| 65 |
+
loss:
|
| 66 |
+
triplet: 1.0
|
| 67 |
+
sdf: 1.0
|
| 68 |
+
feat: 10.0
|
| 69 |
+
l1: 0.0
|
| 70 |
+
|
| 71 |
+
use_pvcnn: false
|
| 72 |
+
use_pvcnnonly: true
|
| 73 |
+
|
| 74 |
+
pvcnn:
|
| 75 |
+
point_encoder_type: 'pvcnn'
|
| 76 |
+
use_point_scatter: true
|
| 77 |
+
z_triplane_channels: 256
|
| 78 |
+
z_triplane_resolution: 128
|
| 79 |
+
unet_cfg:
|
| 80 |
+
depth: 3
|
| 81 |
+
enabled: true
|
| 82 |
+
rolled: true
|
| 83 |
+
use_3d_aware: true
|
| 84 |
+
start_hidden_channels: 32
|
| 85 |
+
use_initial_conv: false
|
| 86 |
+
|
| 87 |
+
use_2d_feat: false
|
| 88 |
+
inference_metrics_only: false
|
third_party/PartField/partfield/config/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os.path as osp
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import pytz
|
| 5 |
+
|
| 6 |
+
def default_argument_parser(add_help=True, default_config_file=""):
|
| 7 |
+
parser = argparse.ArgumentParser(add_help=add_help)
|
| 8 |
+
parser.add_argument("--config-file", '-c', default=default_config_file, metavar="FILE", help="path to config file")
|
| 9 |
+
parser.add_argument(
|
| 10 |
+
"--opts",
|
| 11 |
+
help="Modify config options using the command-line",
|
| 12 |
+
default=None,
|
| 13 |
+
nargs=argparse.REMAINDER,
|
| 14 |
+
)
|
| 15 |
+
return parser
|
| 16 |
+
|
| 17 |
+
def setup(args, freeze=True):
|
| 18 |
+
from .defaults import _C as cfg
|
| 19 |
+
cfg = cfg.clone()
|
| 20 |
+
cfg.merge_from_file(args.config_file)
|
| 21 |
+
cfg.merge_from_list(args.opts)
|
| 22 |
+
dt = datetime.now(pytz.timezone('America/Los_Angeles')).strftime('%y%m%d-%H%M%S')
|
| 23 |
+
cfg.output_dir = osp.join(cfg.output_dir, cfg.name, dt)
|
| 24 |
+
if freeze:
|
| 25 |
+
cfg.freeze()
|
| 26 |
+
return cfg
|
third_party/PartField/partfield/config/defaults.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from yacs.config import CfgNode as CN
|
| 2 |
+
|
| 3 |
+
_C = CN()
|
| 4 |
+
_C.seed = 0
|
| 5 |
+
_C.output_dir = "results"
|
| 6 |
+
_C.result_name = "test_all"
|
| 7 |
+
|
| 8 |
+
_C.triplet_sampling = "random"
|
| 9 |
+
_C.load_original_mesh = False
|
| 10 |
+
|
| 11 |
+
_C.num_pos = 64
|
| 12 |
+
_C.num_neg_random = 256
|
| 13 |
+
_C.num_neg_hard_pc = 128
|
| 14 |
+
_C.num_neg_hard_emb = 128
|
| 15 |
+
|
| 16 |
+
_C.vertex_feature = False # if true, sample feature on vertices; if false, sample feature on faces
|
| 17 |
+
_C.n_point_per_face = 2000
|
| 18 |
+
_C.n_sample_each = 10000
|
| 19 |
+
_C.preprocess_mesh = False
|
| 20 |
+
|
| 21 |
+
_C.regress_2d_feat = False
|
| 22 |
+
|
| 23 |
+
_C.is_pc = False
|
| 24 |
+
|
| 25 |
+
_C.cut_manifold = False
|
| 26 |
+
_C.remesh_demo = False
|
| 27 |
+
_C.correspondence_demo = False
|
| 28 |
+
|
| 29 |
+
_C.save_every_epoch = 10
|
| 30 |
+
_C.training_epochs = 30
|
| 31 |
+
_C.continue_training = False
|
| 32 |
+
|
| 33 |
+
_C.continue_ckpt = None
|
| 34 |
+
_C.epoch_selected = "epoch=50.ckpt"
|
| 35 |
+
|
| 36 |
+
_C.triplane_resolution = 128
|
| 37 |
+
_C.triplane_channels_low = 128
|
| 38 |
+
_C.triplane_channels_high = 512
|
| 39 |
+
_C.lr = 1e-3
|
| 40 |
+
_C.train = True
|
| 41 |
+
_C.test = False
|
| 42 |
+
|
| 43 |
+
_C.inference_save_pred_sdf_to_mesh=True
|
| 44 |
+
_C.inference_save_feat_pca=True
|
| 45 |
+
_C.name = "test"
|
| 46 |
+
_C.test_subset = False
|
| 47 |
+
_C.test_corres = False
|
| 48 |
+
_C.test_partobjaversetiny = False
|
| 49 |
+
|
| 50 |
+
_C.dataset = CN()
|
| 51 |
+
_C.dataset.type = "Demo_Dataset"
|
| 52 |
+
_C.dataset.data_path = "objaverse_data/"
|
| 53 |
+
_C.dataset.train_num_workers = 64
|
| 54 |
+
_C.dataset.val_num_workers = 32
|
| 55 |
+
_C.dataset.train_batch_size = 2
|
| 56 |
+
_C.dataset.val_batch_size = 2
|
| 57 |
+
_C.dataset.all_files = [] # only used for correspondence demo
|
| 58 |
+
|
| 59 |
+
_C.voxel2triplane = CN()
|
| 60 |
+
_C.voxel2triplane.transformer_dim = 1024
|
| 61 |
+
_C.voxel2triplane.transformer_layers = 6
|
| 62 |
+
_C.voxel2triplane.transformer_heads = 8
|
| 63 |
+
_C.voxel2triplane.triplane_low_res = 32
|
| 64 |
+
_C.voxel2triplane.triplane_high_res = 256
|
| 65 |
+
_C.voxel2triplane.triplane_dim = 64
|
| 66 |
+
_C.voxel2triplane.normalize_vox_feat = False
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
_C.loss = CN()
|
| 70 |
+
_C.loss.triplet = 0.0
|
| 71 |
+
_C.loss.sdf = 1.0
|
| 72 |
+
_C.loss.feat = 10.0
|
| 73 |
+
_C.loss.l1 = 0.0
|
| 74 |
+
|
| 75 |
+
_C.use_pvcnn = False
|
| 76 |
+
_C.use_pvcnnonly = True
|
| 77 |
+
|
| 78 |
+
_C.pvcnn = CN()
|
| 79 |
+
_C.pvcnn.point_encoder_type = 'pvcnn'
|
| 80 |
+
_C.pvcnn.use_point_scatter = True
|
| 81 |
+
_C.pvcnn.z_triplane_channels = 64
|
| 82 |
+
_C.pvcnn.z_triplane_resolution = 256
|
| 83 |
+
_C.pvcnn.unet_cfg = CN()
|
| 84 |
+
_C.pvcnn.unet_cfg.depth = 3
|
| 85 |
+
_C.pvcnn.unet_cfg.enabled = True
|
| 86 |
+
_C.pvcnn.unet_cfg.rolled = True
|
| 87 |
+
_C.pvcnn.unet_cfg.use_3d_aware = True
|
| 88 |
+
_C.pvcnn.unet_cfg.start_hidden_channels = 32
|
| 89 |
+
_C.pvcnn.unet_cfg.use_initial_conv = False
|
| 90 |
+
|
| 91 |
+
_C.use_2d_feat = False
|
| 92 |
+
_C.inference_metrics_only = False
|
third_party/PartField/partfield/dataloader.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import trimesh
|
| 4 |
+
import numpy as np
|
| 5 |
+
import gc
|
| 6 |
+
|
| 7 |
+
def quad_to_triangle_mesh(F):
|
| 8 |
+
"""
|
| 9 |
+
Converts a quad-dominant mesh into a pure triangle mesh by splitting quads into two triangles.
|
| 10 |
+
|
| 11 |
+
Parameters:
|
| 12 |
+
quad_mesh (trimesh.Trimesh): Input mesh with quad faces.
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
trimesh.Trimesh: A new mesh with only triangle faces.
|
| 16 |
+
"""
|
| 17 |
+
faces = F
|
| 18 |
+
|
| 19 |
+
### If already a triangle mesh -- skip
|
| 20 |
+
if len(faces[0]) == 3:
|
| 21 |
+
return F
|
| 22 |
+
|
| 23 |
+
new_faces = []
|
| 24 |
+
|
| 25 |
+
for face in faces:
|
| 26 |
+
if len(face) == 4: # Quad face
|
| 27 |
+
# Split into two triangles
|
| 28 |
+
new_faces.append([face[0], face[1], face[2]]) # Triangle 1
|
| 29 |
+
new_faces.append([face[0], face[2], face[3]]) # Triangle 2
|
| 30 |
+
else:
|
| 31 |
+
print(f"Warning: Skipping non-triangle/non-quad face {face}")
|
| 32 |
+
|
| 33 |
+
new_faces = np.array(new_faces)
|
| 34 |
+
|
| 35 |
+
return new_faces
|
| 36 |
+
|
| 37 |
+
class Demo_Dataset(torch.utils.data.Dataset):
|
| 38 |
+
def __init__(self, obj_path):
|
| 39 |
+
super().__init__()
|
| 40 |
+
|
| 41 |
+
self.obj_path = obj_path
|
| 42 |
+
self.pc_num_pts = 100000
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def __len__(self):
|
| 46 |
+
return 1
|
| 47 |
+
|
| 48 |
+
def get_model(self):
|
| 49 |
+
uid = os.path.basename(self.obj_path).split(".")[-2]
|
| 50 |
+
mesh = trimesh.load(self.obj_path, force='mesh', process=False)
|
| 51 |
+
vertices = mesh.vertices
|
| 52 |
+
faces = mesh.faces
|
| 53 |
+
|
| 54 |
+
bbmin = vertices.min(0)
|
| 55 |
+
bbmax = vertices.max(0)
|
| 56 |
+
center = (bbmin + bbmax) * 0.5
|
| 57 |
+
scale = 2.0 * 0.9 / (bbmax - bbmin).max()
|
| 58 |
+
vertices = (vertices - center) * scale
|
| 59 |
+
mesh.vertices = vertices
|
| 60 |
+
|
| 61 |
+
### Make sure it is a triangle mesh -- just convert the quad
|
| 62 |
+
mesh.faces = quad_to_triangle_mesh(faces)
|
| 63 |
+
pc, _ = trimesh.sample.sample_surface(mesh, self.pc_num_pts)
|
| 64 |
+
|
| 65 |
+
result = {
|
| 66 |
+
'uid': uid
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
result['pc'] = torch.tensor(pc, dtype=torch.float32)
|
| 70 |
+
result['vertices'] = mesh.vertices
|
| 71 |
+
result['faces'] = mesh.faces
|
| 72 |
+
|
| 73 |
+
return result
|
| 74 |
+
|
| 75 |
+
def __getitem__(self, index):
|
| 76 |
+
gc.collect()
|
| 77 |
+
|
| 78 |
+
return self.get_model()
|
third_party/PartField/partfield/model/PVCNN/conv_pointnet.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Taken from gensdf
|
| 3 |
+
https://github.com/princeton-computational-imaging/gensdf
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
# from dnnlib.util import printarr
|
| 10 |
+
try:
|
| 11 |
+
from torch_scatter import scatter_mean, scatter_max
|
| 12 |
+
except:
|
| 13 |
+
pass
|
| 14 |
+
# from .unet import UNet
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn as nn
|
| 17 |
+
import torch.nn.functional as F
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# Resnet Blocks
|
| 21 |
+
class ResnetBlockFC(nn.Module):
|
| 22 |
+
''' Fully connected ResNet Block class.
|
| 23 |
+
Args:
|
| 24 |
+
size_in (int): input dimension
|
| 25 |
+
size_out (int): output dimension
|
| 26 |
+
size_h (int): hidden dimension
|
| 27 |
+
'''
|
| 28 |
+
|
| 29 |
+
def __init__(self, size_in, size_out=None, size_h=None):
|
| 30 |
+
super().__init__()
|
| 31 |
+
# Attributes
|
| 32 |
+
if size_out is None:
|
| 33 |
+
size_out = size_in
|
| 34 |
+
|
| 35 |
+
if size_h is None:
|
| 36 |
+
size_h = min(size_in, size_out)
|
| 37 |
+
|
| 38 |
+
self.size_in = size_in
|
| 39 |
+
self.size_h = size_h
|
| 40 |
+
self.size_out = size_out
|
| 41 |
+
# Submodules
|
| 42 |
+
self.fc_0 = nn.Linear(size_in, size_h)
|
| 43 |
+
self.fc_1 = nn.Linear(size_h, size_out)
|
| 44 |
+
self.actvn = nn.ReLU()
|
| 45 |
+
|
| 46 |
+
if size_in == size_out:
|
| 47 |
+
self.shortcut = None
|
| 48 |
+
else:
|
| 49 |
+
self.shortcut = nn.Linear(size_in, size_out, bias=False)
|
| 50 |
+
# Initialization
|
| 51 |
+
nn.init.zeros_(self.fc_1.weight)
|
| 52 |
+
|
| 53 |
+
def forward(self, x):
|
| 54 |
+
net = self.fc_0(self.actvn(x))
|
| 55 |
+
dx = self.fc_1(self.actvn(net))
|
| 56 |
+
|
| 57 |
+
if self.shortcut is not None:
|
| 58 |
+
x_s = self.shortcut(x)
|
| 59 |
+
else:
|
| 60 |
+
x_s = x
|
| 61 |
+
|
| 62 |
+
return x_s + dx
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class ConvPointnet(nn.Module):
|
| 66 |
+
''' PointNet-based encoder network with ResNet blocks for each point.
|
| 67 |
+
Number of input points are fixed.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
c_dim (int): dimension of latent code c
|
| 71 |
+
dim (int): input points dimension
|
| 72 |
+
hidden_dim (int): hidden dimension of the network
|
| 73 |
+
scatter_type (str): feature aggregation when doing local pooling
|
| 74 |
+
unet (bool): weather to use U-Net
|
| 75 |
+
unet_kwargs (str): U-Net parameters
|
| 76 |
+
plane_resolution (int): defined resolution for plane feature
|
| 77 |
+
plane_type (str): feature type, 'xz' - 1-plane, ['xz', 'xy', 'yz'] - 3-plane, ['grid'] - 3D grid volume
|
| 78 |
+
padding (float): conventional padding paramter of ONet for unit cube, so [-0.5, 0.5] -> [-0.55, 0.55]
|
| 79 |
+
n_blocks (int): number of blocks ResNetBlockFC layers
|
| 80 |
+
'''
|
| 81 |
+
|
| 82 |
+
def __init__(self, c_dim=128, dim=3, hidden_dim=128, scatter_type='max',
|
| 83 |
+
# unet=False, unet_kwargs=None,
|
| 84 |
+
plane_resolution=None, plane_type=['xz', 'xy', 'yz'], padding=0.1, n_blocks=5):
|
| 85 |
+
super().__init__()
|
| 86 |
+
self.c_dim = c_dim
|
| 87 |
+
|
| 88 |
+
self.fc_pos = nn.Linear(dim, 2*hidden_dim)
|
| 89 |
+
self.blocks = nn.ModuleList([
|
| 90 |
+
ResnetBlockFC(2*hidden_dim, hidden_dim) for i in range(n_blocks)
|
| 91 |
+
])
|
| 92 |
+
self.fc_c = nn.Linear(hidden_dim, c_dim)
|
| 93 |
+
|
| 94 |
+
self.actvn = nn.ReLU()
|
| 95 |
+
self.hidden_dim = hidden_dim
|
| 96 |
+
|
| 97 |
+
# if unet:
|
| 98 |
+
# self.unet = UNet(c_dim, in_channels=c_dim, **unet_kwargs)
|
| 99 |
+
# else:
|
| 100 |
+
# self.unet = None
|
| 101 |
+
|
| 102 |
+
self.reso_plane = plane_resolution
|
| 103 |
+
self.plane_type = plane_type
|
| 104 |
+
self.padding = padding
|
| 105 |
+
|
| 106 |
+
if scatter_type == 'max':
|
| 107 |
+
self.scatter = scatter_max
|
| 108 |
+
elif scatter_type == 'mean':
|
| 109 |
+
self.scatter = scatter_mean
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# takes in "p": point cloud and "query": sdf_xyz
|
| 113 |
+
# sample plane features for unlabeled_query as well
|
| 114 |
+
def forward(self, p):#, query2):
|
| 115 |
+
batch_size, T, D = p.size()
|
| 116 |
+
|
| 117 |
+
# acquire the index for each point
|
| 118 |
+
coord = {}
|
| 119 |
+
index = {}
|
| 120 |
+
if 'xz' in self.plane_type:
|
| 121 |
+
coord['xz'] = self.normalize_coordinate(p.clone(), plane='xz', padding=self.padding)
|
| 122 |
+
index['xz'] = self.coordinate2index(coord['xz'], self.reso_plane)
|
| 123 |
+
if 'xy' in self.plane_type:
|
| 124 |
+
coord['xy'] = self.normalize_coordinate(p.clone(), plane='xy', padding=self.padding)
|
| 125 |
+
index['xy'] = self.coordinate2index(coord['xy'], self.reso_plane)
|
| 126 |
+
if 'yz' in self.plane_type:
|
| 127 |
+
coord['yz'] = self.normalize_coordinate(p.clone(), plane='yz', padding=self.padding)
|
| 128 |
+
index['yz'] = self.coordinate2index(coord['yz'], self.reso_plane)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
net = self.fc_pos(p)
|
| 132 |
+
|
| 133 |
+
net = self.blocks[0](net)
|
| 134 |
+
for block in self.blocks[1:]:
|
| 135 |
+
pooled = self.pool_local(coord, index, net)
|
| 136 |
+
net = torch.cat([net, pooled], dim=2)
|
| 137 |
+
net = block(net)
|
| 138 |
+
|
| 139 |
+
c = self.fc_c(net)
|
| 140 |
+
|
| 141 |
+
fea = {}
|
| 142 |
+
plane_feat_sum = 0
|
| 143 |
+
#second_sum = 0
|
| 144 |
+
if 'xz' in self.plane_type:
|
| 145 |
+
fea['xz'] = self.generate_plane_features(p, c, plane='xz') # shape: batch, latent size, resolution, resolution (e.g. 16, 256, 64, 64)
|
| 146 |
+
# plane_feat_sum += self.sample_plane_feature(query, fea['xz'], 'xz')
|
| 147 |
+
#second_sum += self.sample_plane_feature(query2, fea['xz'], 'xz')
|
| 148 |
+
if 'xy' in self.plane_type:
|
| 149 |
+
fea['xy'] = self.generate_plane_features(p, c, plane='xy')
|
| 150 |
+
# plane_feat_sum += self.sample_plane_feature(query, fea['xy'], 'xy')
|
| 151 |
+
#second_sum += self.sample_plane_feature(query2, fea['xy'], 'xy')
|
| 152 |
+
if 'yz' in self.plane_type:
|
| 153 |
+
fea['yz'] = self.generate_plane_features(p, c, plane='yz')
|
| 154 |
+
# plane_feat_sum += self.sample_plane_feature(query, fea['yz'], 'yz')
|
| 155 |
+
#second_sum += self.sample_plane_feature(query2, fea['yz'], 'yz')
|
| 156 |
+
return fea
|
| 157 |
+
|
| 158 |
+
# return plane_feat_sum.transpose(2,1)#, second_sum.transpose(2,1)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def normalize_coordinate(self, p, padding=0.1, plane='xz'):
|
| 162 |
+
''' Normalize coordinate to [0, 1] for unit cube experiments
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
p (tensor): point
|
| 166 |
+
padding (float): conventional padding paramter of ONet for unit cube, so [-0.5, 0.5] -> [-0.55, 0.55]
|
| 167 |
+
plane (str): plane feature type, ['xz', 'xy', 'yz']
|
| 168 |
+
'''
|
| 169 |
+
if plane == 'xz':
|
| 170 |
+
xy = p[:, :, [0, 2]]
|
| 171 |
+
elif plane =='xy':
|
| 172 |
+
xy = p[:, :, [0, 1]]
|
| 173 |
+
else:
|
| 174 |
+
xy = p[:, :, [1, 2]]
|
| 175 |
+
|
| 176 |
+
xy_new = xy / (1 + padding + 10e-6) # (-0.5, 0.5)
|
| 177 |
+
xy_new = xy_new + 0.5 # range (0, 1)
|
| 178 |
+
|
| 179 |
+
# f there are outliers out of the range
|
| 180 |
+
if xy_new.max() >= 1:
|
| 181 |
+
xy_new[xy_new >= 1] = 1 - 10e-6
|
| 182 |
+
if xy_new.min() < 0:
|
| 183 |
+
xy_new[xy_new < 0] = 0.0
|
| 184 |
+
return xy_new
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def coordinate2index(self, x, reso):
|
| 188 |
+
''' Normalize coordinate to [0, 1] for unit cube experiments.
|
| 189 |
+
Corresponds to our 3D model
|
| 190 |
+
|
| 191 |
+
Args:
|
| 192 |
+
x (tensor): coordinate
|
| 193 |
+
reso (int): defined resolution
|
| 194 |
+
coord_type (str): coordinate type
|
| 195 |
+
'''
|
| 196 |
+
x = (x * reso).long()
|
| 197 |
+
index = x[:, :, 0] + reso * x[:, :, 1]
|
| 198 |
+
index = index[:, None, :]
|
| 199 |
+
return index
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# xy is the normalized coordinates of the point cloud of each plane
|
| 203 |
+
# I'm pretty sure the keys of xy are the same as those of index, so xy isn't needed here as input
|
| 204 |
+
def pool_local(self, xy, index, c):
|
| 205 |
+
bs, fea_dim = c.size(0), c.size(2)
|
| 206 |
+
keys = xy.keys()
|
| 207 |
+
|
| 208 |
+
c_out = 0
|
| 209 |
+
for key in keys:
|
| 210 |
+
# scatter plane features from points
|
| 211 |
+
fea = self.scatter(c.permute(0, 2, 1), index[key], dim_size=self.reso_plane**2)
|
| 212 |
+
if self.scatter == scatter_max:
|
| 213 |
+
fea = fea[0]
|
| 214 |
+
# gather feature back to points
|
| 215 |
+
fea = fea.gather(dim=2, index=index[key].expand(-1, fea_dim, -1))
|
| 216 |
+
c_out += fea
|
| 217 |
+
return c_out.permute(0, 2, 1)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def generate_plane_features(self, p, c, plane='xz'):
|
| 221 |
+
# acquire indices of features in plane
|
| 222 |
+
xy = self.normalize_coordinate(p.clone(), plane=plane, padding=self.padding) # normalize to the range of (0, 1)
|
| 223 |
+
index = self.coordinate2index(xy, self.reso_plane)
|
| 224 |
+
|
| 225 |
+
# scatter plane features from points
|
| 226 |
+
fea_plane = c.new_zeros(p.size(0), self.c_dim, self.reso_plane**2)
|
| 227 |
+
c = c.permute(0, 2, 1) # B x 512 x T
|
| 228 |
+
fea_plane = scatter_mean(c, index, out=fea_plane) # B x 512 x reso^2
|
| 229 |
+
fea_plane = fea_plane.reshape(p.size(0), self.c_dim, self.reso_plane, self.reso_plane) # sparce matrix (B x 512 x reso x reso)
|
| 230 |
+
|
| 231 |
+
# printarr(fea_plane, c, p, xy, index)
|
| 232 |
+
# import pdb; pdb.set_trace()
|
| 233 |
+
|
| 234 |
+
# process the plane features with UNet
|
| 235 |
+
# if self.unet is not None:
|
| 236 |
+
# fea_plane = self.unet(fea_plane)
|
| 237 |
+
|
| 238 |
+
return fea_plane
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
# sample_plane_feature function copied from /src/conv_onet/models/decoder.py
|
| 242 |
+
# uses values from plane_feature and pixel locations from vgrid to interpolate feature
|
| 243 |
+
def sample_plane_feature(self, query, plane_feature, plane):
|
| 244 |
+
xy = self.normalize_coordinate(query.clone(), plane=plane, padding=self.padding)
|
| 245 |
+
xy = xy[:, :, None].float()
|
| 246 |
+
vgrid = 2.0 * xy - 1.0 # normalize to (-1, 1)
|
| 247 |
+
sampled_feat = F.grid_sample(plane_feature, vgrid, padding_mode='border', align_corners=True, mode='bilinear').squeeze(-1)
|
| 248 |
+
return sampled_feat
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
|
third_party/PartField/partfield/model/PVCNN/dnnlib_util.py
ADDED
|
@@ -0,0 +1,1074 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# NVIDIA CORPORATION & AFFILIATES and its licensors retain all intellectual property
|
| 4 |
+
# and proprietary rights in and to this software, related documentation
|
| 5 |
+
# and any modifications thereto. Any use, reproduction, disclosure or
|
| 6 |
+
# distribution of this software and related documentation without an express
|
| 7 |
+
# license agreement from NVIDIA CORPORATION & AFFILIATES is strictly prohibited.
|
| 8 |
+
|
| 9 |
+
"""Miscellaneous utility classes and functions."""
|
| 10 |
+
from collections import namedtuple
|
| 11 |
+
import time
|
| 12 |
+
import ctypes
|
| 13 |
+
import fnmatch
|
| 14 |
+
import importlib
|
| 15 |
+
import inspect
|
| 16 |
+
import numpy as np
|
| 17 |
+
import json
|
| 18 |
+
import os
|
| 19 |
+
import shutil
|
| 20 |
+
import sys
|
| 21 |
+
import types
|
| 22 |
+
import io
|
| 23 |
+
import pickle
|
| 24 |
+
import re
|
| 25 |
+
# import requests
|
| 26 |
+
import html
|
| 27 |
+
import hashlib
|
| 28 |
+
import glob
|
| 29 |
+
import tempfile
|
| 30 |
+
import urllib
|
| 31 |
+
import urllib.request
|
| 32 |
+
import uuid
|
| 33 |
+
import boto3
|
| 34 |
+
import threading
|
| 35 |
+
from contextlib import ContextDecorator
|
| 36 |
+
from contextlib import contextmanager, nullcontext
|
| 37 |
+
|
| 38 |
+
from distutils.util import strtobool
|
| 39 |
+
from typing import Any, List, Tuple, Union
|
| 40 |
+
import importlib
|
| 41 |
+
from loguru import logger
|
| 42 |
+
# import wandb
|
| 43 |
+
import torch
|
| 44 |
+
import psutil
|
| 45 |
+
import subprocess
|
| 46 |
+
|
| 47 |
+
import random
|
| 48 |
+
import string
|
| 49 |
+
import pdb
|
| 50 |
+
|
| 51 |
+
# Util classes
|
| 52 |
+
# ------------------------------------------------------------------------------------------
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class EasyDict(dict):
|
| 56 |
+
"""Convenience class that behaves like a dict but allows access with the attribute syntax."""
|
| 57 |
+
|
| 58 |
+
def __getattr__(self, name: str) -> Any:
|
| 59 |
+
try:
|
| 60 |
+
return self[name]
|
| 61 |
+
except KeyError:
|
| 62 |
+
raise AttributeError(name)
|
| 63 |
+
|
| 64 |
+
def __setattr__(self, name: str, value: Any) -> None:
|
| 65 |
+
self[name] = value
|
| 66 |
+
|
| 67 |
+
def __delattr__(self, name: str) -> None:
|
| 68 |
+
del self[name]
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class Logger(object):
|
| 72 |
+
"""Redirect stderr to stdout, optionally print stdout to a file, and optionally force flushing on both stdout and the file."""
|
| 73 |
+
|
| 74 |
+
def __init__(self, file_name: str = None, file_mode: str = "w", should_flush: bool = True):
|
| 75 |
+
self.file = None
|
| 76 |
+
|
| 77 |
+
if file_name is not None:
|
| 78 |
+
self.file = open(file_name, file_mode)
|
| 79 |
+
|
| 80 |
+
self.should_flush = should_flush
|
| 81 |
+
self.stdout = sys.stdout
|
| 82 |
+
self.stderr = sys.stderr
|
| 83 |
+
|
| 84 |
+
sys.stdout = self
|
| 85 |
+
sys.stderr = self
|
| 86 |
+
|
| 87 |
+
def __enter__(self) -> "Logger":
|
| 88 |
+
return self
|
| 89 |
+
|
| 90 |
+
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
| 91 |
+
self.close()
|
| 92 |
+
|
| 93 |
+
def write(self, text: Union[str, bytes]) -> None:
|
| 94 |
+
"""Write text to stdout (and a file) and optionally flush."""
|
| 95 |
+
if isinstance(text, bytes):
|
| 96 |
+
text = text.decode()
|
| 97 |
+
if len(text) == 0: # workaround for a bug in VSCode debugger: sys.stdout.write(''); sys.stdout.flush() => crash
|
| 98 |
+
return
|
| 99 |
+
|
| 100 |
+
if self.file is not None:
|
| 101 |
+
self.file.write(text)
|
| 102 |
+
|
| 103 |
+
self.stdout.write(text)
|
| 104 |
+
|
| 105 |
+
if self.should_flush:
|
| 106 |
+
self.flush()
|
| 107 |
+
|
| 108 |
+
def flush(self) -> None:
|
| 109 |
+
"""Flush written text to both stdout and a file, if open."""
|
| 110 |
+
if self.file is not None:
|
| 111 |
+
self.file.flush()
|
| 112 |
+
|
| 113 |
+
self.stdout.flush()
|
| 114 |
+
|
| 115 |
+
def close(self) -> None:
|
| 116 |
+
"""Flush, close possible files, and remove stdout/stderr mirroring."""
|
| 117 |
+
self.flush()
|
| 118 |
+
|
| 119 |
+
# if using multiple loggers, prevent closing in wrong order
|
| 120 |
+
if sys.stdout is self:
|
| 121 |
+
sys.stdout = self.stdout
|
| 122 |
+
if sys.stderr is self:
|
| 123 |
+
sys.stderr = self.stderr
|
| 124 |
+
|
| 125 |
+
if self.file is not None:
|
| 126 |
+
self.file.close()
|
| 127 |
+
self.file = None
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
# Cache directories
|
| 131 |
+
# ------------------------------------------------------------------------------------------
|
| 132 |
+
|
| 133 |
+
_dnnlib_cache_dir = None
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def set_cache_dir(path: str) -> None:
|
| 137 |
+
global _dnnlib_cache_dir
|
| 138 |
+
_dnnlib_cache_dir = path
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def make_cache_dir_path(*paths: str) -> str:
|
| 142 |
+
if _dnnlib_cache_dir is not None:
|
| 143 |
+
return os.path.join(_dnnlib_cache_dir, *paths)
|
| 144 |
+
if 'DNNLIB_CACHE_DIR' in os.environ:
|
| 145 |
+
return os.path.join(os.environ['DNNLIB_CACHE_DIR'], *paths)
|
| 146 |
+
if 'HOME' in os.environ:
|
| 147 |
+
return os.path.join(os.environ['HOME'], '.cache', 'dnnlib', *paths)
|
| 148 |
+
if 'USERPROFILE' in os.environ:
|
| 149 |
+
return os.path.join(os.environ['USERPROFILE'], '.cache', 'dnnlib', *paths)
|
| 150 |
+
return os.path.join(tempfile.gettempdir(), '.cache', 'dnnlib', *paths)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
# Small util functions
|
| 154 |
+
# ------------------------------------------------------------------------------------------
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def format_time(seconds: Union[int, float]) -> str:
|
| 158 |
+
"""Convert the seconds to human readable string with days, hours, minutes and seconds."""
|
| 159 |
+
s = int(np.rint(seconds))
|
| 160 |
+
|
| 161 |
+
if s < 60:
|
| 162 |
+
return "{0}s".format(s)
|
| 163 |
+
elif s < 60 * 60:
|
| 164 |
+
return "{0}m {1:02}s".format(s // 60, s % 60)
|
| 165 |
+
elif s < 24 * 60 * 60:
|
| 166 |
+
return "{0}h {1:02}m {2:02}s".format(s // (60 * 60), (s // 60) % 60, s % 60)
|
| 167 |
+
else:
|
| 168 |
+
return "{0}d {1:02}h {2:02}m".format(s // (24 * 60 * 60), (s // (60 * 60)) % 24, (s // 60) % 60)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def format_time_brief(seconds: Union[int, float]) -> str:
|
| 172 |
+
"""Convert the seconds to human readable string with days, hours, minutes and seconds."""
|
| 173 |
+
s = int(np.rint(seconds))
|
| 174 |
+
|
| 175 |
+
if s < 60:
|
| 176 |
+
return "{0}s".format(s)
|
| 177 |
+
elif s < 60 * 60:
|
| 178 |
+
return "{0}m {1:02}s".format(s // 60, s % 60)
|
| 179 |
+
elif s < 24 * 60 * 60:
|
| 180 |
+
return "{0}h {1:02}m".format(s // (60 * 60), (s // 60) % 60)
|
| 181 |
+
else:
|
| 182 |
+
return "{0}d {1:02}h".format(s // (24 * 60 * 60), (s // (60 * 60)) % 24)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def ask_yes_no(question: str) -> bool:
|
| 186 |
+
"""Ask the user the question until the user inputs a valid answer."""
|
| 187 |
+
while True:
|
| 188 |
+
try:
|
| 189 |
+
print("{0} [y/n]".format(question))
|
| 190 |
+
return strtobool(input().lower())
|
| 191 |
+
except ValueError:
|
| 192 |
+
pass
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def tuple_product(t: Tuple) -> Any:
|
| 196 |
+
"""Calculate the product of the tuple elements."""
|
| 197 |
+
result = 1
|
| 198 |
+
|
| 199 |
+
for v in t:
|
| 200 |
+
result *= v
|
| 201 |
+
|
| 202 |
+
return result
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
_str_to_ctype = {
|
| 206 |
+
"uint8": ctypes.c_ubyte,
|
| 207 |
+
"uint16": ctypes.c_uint16,
|
| 208 |
+
"uint32": ctypes.c_uint32,
|
| 209 |
+
"uint64": ctypes.c_uint64,
|
| 210 |
+
"int8": ctypes.c_byte,
|
| 211 |
+
"int16": ctypes.c_int16,
|
| 212 |
+
"int32": ctypes.c_int32,
|
| 213 |
+
"int64": ctypes.c_int64,
|
| 214 |
+
"float32": ctypes.c_float,
|
| 215 |
+
"float64": ctypes.c_double
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def get_dtype_and_ctype(type_obj: Any) -> Tuple[np.dtype, Any]:
|
| 220 |
+
"""Given a type name string (or an object having a __name__ attribute), return matching Numpy and ctypes types that have the same size in bytes."""
|
| 221 |
+
type_str = None
|
| 222 |
+
|
| 223 |
+
if isinstance(type_obj, str):
|
| 224 |
+
type_str = type_obj
|
| 225 |
+
elif hasattr(type_obj, "__name__"):
|
| 226 |
+
type_str = type_obj.__name__
|
| 227 |
+
elif hasattr(type_obj, "name"):
|
| 228 |
+
type_str = type_obj.name
|
| 229 |
+
else:
|
| 230 |
+
raise RuntimeError("Cannot infer type name from input")
|
| 231 |
+
|
| 232 |
+
assert type_str in _str_to_ctype.keys()
|
| 233 |
+
|
| 234 |
+
my_dtype = np.dtype(type_str)
|
| 235 |
+
my_ctype = _str_to_ctype[type_str]
|
| 236 |
+
|
| 237 |
+
assert my_dtype.itemsize == ctypes.sizeof(my_ctype)
|
| 238 |
+
|
| 239 |
+
return my_dtype, my_ctype
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def is_pickleable(obj: Any) -> bool:
|
| 243 |
+
try:
|
| 244 |
+
with io.BytesIO() as stream:
|
| 245 |
+
pickle.dump(obj, stream)
|
| 246 |
+
return True
|
| 247 |
+
except:
|
| 248 |
+
return False
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
# Functionality to import modules/objects by name, and call functions by name
|
| 252 |
+
# ------------------------------------------------------------------------------------------
|
| 253 |
+
|
| 254 |
+
def get_module_from_obj_name(obj_name: str) -> Tuple[types.ModuleType, str]:
|
| 255 |
+
"""Searches for the underlying module behind the name to some python object.
|
| 256 |
+
Returns the module and the object name (original name with module part removed)."""
|
| 257 |
+
|
| 258 |
+
# allow convenience shorthands, substitute them by full names
|
| 259 |
+
obj_name = re.sub("^np.", "numpy.", obj_name)
|
| 260 |
+
obj_name = re.sub("^tf.", "tensorflow.", obj_name)
|
| 261 |
+
|
| 262 |
+
# list alternatives for (module_name, local_obj_name)
|
| 263 |
+
parts = obj_name.split(".")
|
| 264 |
+
name_pairs = [(".".join(parts[:i]), ".".join(parts[i:])) for i in range(len(parts), 0, -1)]
|
| 265 |
+
|
| 266 |
+
# try each alternative in turn
|
| 267 |
+
for module_name, local_obj_name in name_pairs:
|
| 268 |
+
try:
|
| 269 |
+
module = importlib.import_module(module_name) # may raise ImportError
|
| 270 |
+
get_obj_from_module(module, local_obj_name) # may raise AttributeError
|
| 271 |
+
return module, local_obj_name
|
| 272 |
+
except:
|
| 273 |
+
pass
|
| 274 |
+
|
| 275 |
+
# maybe some of the modules themselves contain errors?
|
| 276 |
+
for module_name, _local_obj_name in name_pairs:
|
| 277 |
+
try:
|
| 278 |
+
importlib.import_module(module_name) # may raise ImportError
|
| 279 |
+
except ImportError:
|
| 280 |
+
if not str(sys.exc_info()[1]).startswith("No module named '" + module_name + "'"):
|
| 281 |
+
raise
|
| 282 |
+
|
| 283 |
+
# maybe the requested attribute is missing?
|
| 284 |
+
for module_name, local_obj_name in name_pairs:
|
| 285 |
+
try:
|
| 286 |
+
module = importlib.import_module(module_name) # may raise ImportError
|
| 287 |
+
get_obj_from_module(module, local_obj_name) # may raise AttributeError
|
| 288 |
+
except ImportError:
|
| 289 |
+
pass
|
| 290 |
+
|
| 291 |
+
# we are out of luck, but we have no idea why
|
| 292 |
+
raise ImportError(obj_name)
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def get_obj_from_module(module: types.ModuleType, obj_name: str) -> Any:
|
| 296 |
+
"""Traverses the object name and returns the last (rightmost) python object."""
|
| 297 |
+
if obj_name == '':
|
| 298 |
+
return module
|
| 299 |
+
obj = module
|
| 300 |
+
for part in obj_name.split("."):
|
| 301 |
+
obj = getattr(obj, part)
|
| 302 |
+
return obj
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def get_obj_by_name(name: str) -> Any:
|
| 306 |
+
"""Finds the python object with the given name."""
|
| 307 |
+
module, obj_name = get_module_from_obj_name(name)
|
| 308 |
+
return get_obj_from_module(module, obj_name)
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def call_func_by_name(*args, func_name: str = None, **kwargs) -> Any:
|
| 312 |
+
"""Finds the python object with the given name and calls it as a function."""
|
| 313 |
+
assert func_name is not None
|
| 314 |
+
func_obj = get_obj_by_name(func_name)
|
| 315 |
+
assert callable(func_obj)
|
| 316 |
+
return func_obj(*args, **kwargs)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def construct_class_by_name(*args, class_name: str = None, **kwargs) -> Any:
|
| 320 |
+
"""Finds the python class with the given name and constructs it with the given arguments."""
|
| 321 |
+
return call_func_by_name(*args, func_name=class_name, **kwargs)
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def get_module_dir_by_obj_name(obj_name: str) -> str:
|
| 325 |
+
"""Get the directory path of the module containing the given object name."""
|
| 326 |
+
module, _ = get_module_from_obj_name(obj_name)
|
| 327 |
+
return os.path.dirname(inspect.getfile(module))
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def is_top_level_function(obj: Any) -> bool:
|
| 331 |
+
"""Determine whether the given object is a top-level function, i.e., defined at module scope using 'def'."""
|
| 332 |
+
return callable(obj) and obj.__name__ in sys.modules[obj.__module__].__dict__
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def get_top_level_function_name(obj: Any) -> str:
|
| 336 |
+
"""Return the fully-qualified name of a top-level function."""
|
| 337 |
+
assert is_top_level_function(obj)
|
| 338 |
+
module = obj.__module__
|
| 339 |
+
if module == '__main__':
|
| 340 |
+
module = os.path.splitext(os.path.basename(sys.modules[module].__file__))[0]
|
| 341 |
+
return module + "." + obj.__name__
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
# File system helpers
|
| 345 |
+
# ------------------------------------------------------------------------------------------
|
| 346 |
+
|
| 347 |
+
def list_dir_recursively_with_ignore(dir_path: str, ignores: List[str] = None, add_base_to_relative: bool = False) -> List[Tuple[str, str]]:
|
| 348 |
+
"""List all files recursively in a given directory while ignoring given file and directory names.
|
| 349 |
+
Returns list of tuples containing both absolute and relative paths."""
|
| 350 |
+
assert os.path.isdir(dir_path)
|
| 351 |
+
base_name = os.path.basename(os.path.normpath(dir_path))
|
| 352 |
+
|
| 353 |
+
if ignores is None:
|
| 354 |
+
ignores = []
|
| 355 |
+
|
| 356 |
+
result = []
|
| 357 |
+
|
| 358 |
+
for root, dirs, files in os.walk(dir_path, topdown=True):
|
| 359 |
+
for ignore_ in ignores:
|
| 360 |
+
dirs_to_remove = [d for d in dirs if fnmatch.fnmatch(d, ignore_)]
|
| 361 |
+
|
| 362 |
+
# dirs need to be edited in-place
|
| 363 |
+
for d in dirs_to_remove:
|
| 364 |
+
dirs.remove(d)
|
| 365 |
+
|
| 366 |
+
files = [f for f in files if not fnmatch.fnmatch(f, ignore_)]
|
| 367 |
+
|
| 368 |
+
absolute_paths = [os.path.join(root, f) for f in files]
|
| 369 |
+
relative_paths = [os.path.relpath(p, dir_path) for p in absolute_paths]
|
| 370 |
+
|
| 371 |
+
if add_base_to_relative:
|
| 372 |
+
relative_paths = [os.path.join(base_name, p) for p in relative_paths]
|
| 373 |
+
|
| 374 |
+
assert len(absolute_paths) == len(relative_paths)
|
| 375 |
+
result += zip(absolute_paths, relative_paths)
|
| 376 |
+
|
| 377 |
+
return result
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def copy_files_and_create_dirs(files: List[Tuple[str, str]]) -> None:
|
| 381 |
+
"""Takes in a list of tuples of (src, dst) paths and copies files.
|
| 382 |
+
Will create all necessary directories."""
|
| 383 |
+
for file in files:
|
| 384 |
+
target_dir_name = os.path.dirname(file[1])
|
| 385 |
+
|
| 386 |
+
# will create all intermediate-level directories
|
| 387 |
+
if not os.path.exists(target_dir_name):
|
| 388 |
+
os.makedirs(target_dir_name)
|
| 389 |
+
|
| 390 |
+
shutil.copyfile(file[0], file[1])
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
# URL helpers
|
| 394 |
+
# ------------------------------------------------------------------------------------------
|
| 395 |
+
|
| 396 |
+
def is_url(obj: Any, allow_file_urls: bool = False) -> bool:
|
| 397 |
+
"""Determine whether the given object is a valid URL string."""
|
| 398 |
+
if not isinstance(obj, str) or not "://" in obj:
|
| 399 |
+
return False
|
| 400 |
+
if allow_file_urls and obj.startswith('file://'):
|
| 401 |
+
return True
|
| 402 |
+
try:
|
| 403 |
+
res = requests.compat.urlparse(obj)
|
| 404 |
+
if not res.scheme or not res.netloc or not "." in res.netloc:
|
| 405 |
+
return False
|
| 406 |
+
res = requests.compat.urlparse(requests.compat.urljoin(obj, "/"))
|
| 407 |
+
if not res.scheme or not res.netloc or not "." in res.netloc:
|
| 408 |
+
return False
|
| 409 |
+
except:
|
| 410 |
+
return False
|
| 411 |
+
return True
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
def open_url(url: str, cache_dir: str = None, num_attempts: int = 10, verbose: bool = True, return_filename: bool = False, cache: bool = True) -> Any:
|
| 415 |
+
"""Download the given URL and return a binary-mode file object to access the data."""
|
| 416 |
+
assert num_attempts >= 1
|
| 417 |
+
assert not (return_filename and (not cache))
|
| 418 |
+
|
| 419 |
+
# Doesn't look like an URL scheme so interpret it as a local filename.
|
| 420 |
+
if not re.match('^[a-z]+://', url):
|
| 421 |
+
return url if return_filename else open(url, "rb")
|
| 422 |
+
|
| 423 |
+
# Handle file URLs. This code handles unusual file:// patterns that
|
| 424 |
+
# arise on Windows:
|
| 425 |
+
#
|
| 426 |
+
# file:///c:/foo.txt
|
| 427 |
+
#
|
| 428 |
+
# which would translate to a local '/c:/foo.txt' filename that's
|
| 429 |
+
# invalid. Drop the forward slash for such pathnames.
|
| 430 |
+
#
|
| 431 |
+
# If you touch this code path, you should test it on both Linux and
|
| 432 |
+
# Windows.
|
| 433 |
+
#
|
| 434 |
+
# Some internet resources suggest using urllib.request.url2pathname() but
|
| 435 |
+
# but that converts forward slashes to backslashes and this causes
|
| 436 |
+
# its own set of problems.
|
| 437 |
+
if url.startswith('file://'):
|
| 438 |
+
filename = urllib.parse.urlparse(url).path
|
| 439 |
+
if re.match(r'^/[a-zA-Z]:', filename):
|
| 440 |
+
filename = filename[1:]
|
| 441 |
+
return filename if return_filename else open(filename, "rb")
|
| 442 |
+
|
| 443 |
+
assert is_url(url)
|
| 444 |
+
|
| 445 |
+
# Lookup from cache.
|
| 446 |
+
if cache_dir is None:
|
| 447 |
+
cache_dir = make_cache_dir_path('downloads')
|
| 448 |
+
|
| 449 |
+
url_md5 = hashlib.md5(url.encode("utf-8")).hexdigest()
|
| 450 |
+
if cache:
|
| 451 |
+
cache_files = glob.glob(os.path.join(cache_dir, url_md5 + "_*"))
|
| 452 |
+
if len(cache_files) == 1:
|
| 453 |
+
filename = cache_files[0]
|
| 454 |
+
return filename if return_filename else open(filename, "rb")
|
| 455 |
+
|
| 456 |
+
# Download.
|
| 457 |
+
url_name = None
|
| 458 |
+
url_data = None
|
| 459 |
+
with requests.Session() as session:
|
| 460 |
+
if verbose:
|
| 461 |
+
print("Downloading %s ..." % url, end="", flush=True)
|
| 462 |
+
for attempts_left in reversed(range(num_attempts)):
|
| 463 |
+
try:
|
| 464 |
+
with session.get(url) as res:
|
| 465 |
+
res.raise_for_status()
|
| 466 |
+
if len(res.content) == 0:
|
| 467 |
+
raise IOError("No data received")
|
| 468 |
+
|
| 469 |
+
if len(res.content) < 8192:
|
| 470 |
+
content_str = res.content.decode("utf-8")
|
| 471 |
+
if "download_warning" in res.headers.get("Set-Cookie", ""):
|
| 472 |
+
links = [html.unescape(link) for link in content_str.split('"') if "export=download" in link]
|
| 473 |
+
if len(links) == 1:
|
| 474 |
+
url = requests.compat.urljoin(url, links[0])
|
| 475 |
+
raise IOError("Google Drive virus checker nag")
|
| 476 |
+
if "Google Drive - Quota exceeded" in content_str:
|
| 477 |
+
raise IOError("Google Drive download quota exceeded -- please try again later")
|
| 478 |
+
|
| 479 |
+
match = re.search(r'filename="([^"]*)"', res.headers.get("Content-Disposition", ""))
|
| 480 |
+
url_name = match[1] if match else url
|
| 481 |
+
url_data = res.content
|
| 482 |
+
if verbose:
|
| 483 |
+
print(" done")
|
| 484 |
+
break
|
| 485 |
+
except KeyboardInterrupt:
|
| 486 |
+
raise
|
| 487 |
+
except:
|
| 488 |
+
if not attempts_left:
|
| 489 |
+
if verbose:
|
| 490 |
+
print(" failed")
|
| 491 |
+
raise
|
| 492 |
+
if verbose:
|
| 493 |
+
print(".", end="", flush=True)
|
| 494 |
+
|
| 495 |
+
# Save to cache.
|
| 496 |
+
if cache:
|
| 497 |
+
safe_name = re.sub(r"[^0-9a-zA-Z-._]", "_", url_name)
|
| 498 |
+
cache_file = os.path.join(cache_dir, url_md5 + "_" + safe_name)
|
| 499 |
+
temp_file = os.path.join(cache_dir, "tmp_" + uuid.uuid4().hex + "_" + url_md5 + "_" + safe_name)
|
| 500 |
+
os.makedirs(cache_dir, exist_ok=True)
|
| 501 |
+
with open(temp_file, "wb") as f:
|
| 502 |
+
f.write(url_data)
|
| 503 |
+
os.replace(temp_file, cache_file) # atomic
|
| 504 |
+
if return_filename:
|
| 505 |
+
return cache_file
|
| 506 |
+
|
| 507 |
+
# Return data as file object.
|
| 508 |
+
assert not return_filename
|
| 509 |
+
return io.BytesIO(url_data)
|
| 510 |
+
|
| 511 |
+
# ------------------------------------------------------------------------------------------
|
| 512 |
+
# util function modified from https://github.com/nv-tlabs/LION/blob/0467d2199076e95a7e88bafd99dcd7d48a04b4a7/utils/model_helper.py
|
| 513 |
+
def import_class(model_str):
|
| 514 |
+
from torch_utils.dist_utils import is_rank0
|
| 515 |
+
if is_rank0():
|
| 516 |
+
logger.info('import: {}', model_str)
|
| 517 |
+
p, m = model_str.rsplit('.', 1)
|
| 518 |
+
mod = importlib.import_module(p)
|
| 519 |
+
Model = getattr(mod, m)
|
| 520 |
+
return Model
|
| 521 |
+
|
| 522 |
+
class ScopedTorchProfiler(ContextDecorator):
|
| 523 |
+
"""
|
| 524 |
+
Marks ranges for both nvtx profiling (with nsys) and torch autograd profiler
|
| 525 |
+
"""
|
| 526 |
+
__global_counts = {}
|
| 527 |
+
enabled=False
|
| 528 |
+
|
| 529 |
+
def __init__(self, unique_name: str):
|
| 530 |
+
"""
|
| 531 |
+
Names must be unique!
|
| 532 |
+
"""
|
| 533 |
+
ScopedTorchProfiler.__global_counts[unique_name] = 0
|
| 534 |
+
self._name = unique_name
|
| 535 |
+
self._autograd_scope = torch.profiler.record_function(unique_name)
|
| 536 |
+
|
| 537 |
+
def __enter__(self):
|
| 538 |
+
if ScopedTorchProfiler.enabled:
|
| 539 |
+
torch.cuda.nvtx.range_push(self._name)
|
| 540 |
+
self._autograd_scope.__enter__()
|
| 541 |
+
|
| 542 |
+
def __exit__(self, exc_type, exc_value, traceback):
|
| 543 |
+
self._autograd_scope.__exit__(exc_type, exc_value, traceback)
|
| 544 |
+
if ScopedTorchProfiler.enabled:
|
| 545 |
+
torch.cuda.nvtx.range_pop()
|
| 546 |
+
|
| 547 |
+
class TimingsMonitor():
|
| 548 |
+
CUDATimer = namedtuple('CUDATimer', ['start', 'end'])
|
| 549 |
+
def __init__(self, device, enabled=True, timing_names:List[str]=[], cuda_timing_names:List[str]=[]):
|
| 550 |
+
"""
|
| 551 |
+
Usage:
|
| 552 |
+
tmonitor = TimingsMonitor(device)
|
| 553 |
+
for i in range(n_iter):
|
| 554 |
+
# Record arbitrary scopes
|
| 555 |
+
with tmonitor.timing_scope('regular_scope_name'):
|
| 556 |
+
...
|
| 557 |
+
with tmonitor.cuda_timing_scope('nested_scope_name'):
|
| 558 |
+
...
|
| 559 |
+
with tmonitor.cuda_timing_scope('cuda_scope_name'):
|
| 560 |
+
...
|
| 561 |
+
tmonitor.record_timing('duration_name', end_time - start_time)
|
| 562 |
+
|
| 563 |
+
# Gather timings
|
| 564 |
+
tmonitor.record_all_cuda_timings()
|
| 565 |
+
tmonitor.update_all_averages()
|
| 566 |
+
averages = tmonitor.get_average_timings()
|
| 567 |
+
all_timings = tmonitor.get_timings()
|
| 568 |
+
|
| 569 |
+
Two types of timers, standard report timing and cuda timings.
|
| 570 |
+
Cuda timing supports scoped context manager cuda_event_scope.
|
| 571 |
+
Args:
|
| 572 |
+
device: device to time on (needed for cuda timers)
|
| 573 |
+
# enabled: HACK to only report timings from rank 0, set enabled=(global_rank==0)
|
| 574 |
+
timing_names: timings to report optional (will auto add new names)
|
| 575 |
+
cuda_timing_names: cuda periods to time optional (will auto add new names)
|
| 576 |
+
"""
|
| 577 |
+
self.enabled=enabled
|
| 578 |
+
self.device = device
|
| 579 |
+
|
| 580 |
+
# Normal timing
|
| 581 |
+
# self.all_timings_dict = {k:None for k in timing_names + cuda_timing_names}
|
| 582 |
+
self.all_timings_dict = {}
|
| 583 |
+
self.avg_meter_dict = {}
|
| 584 |
+
|
| 585 |
+
# Cuda event timers to measure time spent on pushing data to gpu and on training step
|
| 586 |
+
self.cuda_event_timers = {}
|
| 587 |
+
|
| 588 |
+
for k in timing_names:
|
| 589 |
+
self.add_new_timing(k)
|
| 590 |
+
|
| 591 |
+
for k in cuda_timing_names:
|
| 592 |
+
self.add_new_cuda_timing(k)
|
| 593 |
+
|
| 594 |
+
# Running averages
|
| 595 |
+
# self.avg_meter_dict = {k:AverageMeter() for k in self.all_timings_dict}
|
| 596 |
+
|
| 597 |
+
def add_new_timing(self, name):
|
| 598 |
+
self.avg_meter_dict[name] = AverageMeter()
|
| 599 |
+
self.all_timings_dict[name] = None
|
| 600 |
+
|
| 601 |
+
def add_new_cuda_timing(self, name):
|
| 602 |
+
start_event = torch.cuda.Event(enable_timing=True)
|
| 603 |
+
end_event = torch.cuda.Event(enable_timing=True)
|
| 604 |
+
self.cuda_event_timers[name] = self.CUDATimer(start=start_event, end=end_event)
|
| 605 |
+
self.add_new_timing(name)
|
| 606 |
+
|
| 607 |
+
def clear_timings(self):
|
| 608 |
+
self.all_timings_dict = {k:None for k in self.all_timings_dict}
|
| 609 |
+
|
| 610 |
+
def get_timings(self):
|
| 611 |
+
return self.all_timings_dict
|
| 612 |
+
|
| 613 |
+
def get_average_timings(self):
|
| 614 |
+
return {k:v.avg for k,v in self.avg_meter_dict.items()}
|
| 615 |
+
|
| 616 |
+
def update_all_averages(self):
|
| 617 |
+
"""
|
| 618 |
+
Once per iter, when timings have been finished recording, one should
|
| 619 |
+
call update_average_iter to keep running average of timings.
|
| 620 |
+
"""
|
| 621 |
+
for k,v in self.all_timings_dict.items():
|
| 622 |
+
if v is None:
|
| 623 |
+
print("none_timing", k)
|
| 624 |
+
continue
|
| 625 |
+
self.avg_meter_dict[k].update(v)
|
| 626 |
+
|
| 627 |
+
def record_timing(self, name, value):
|
| 628 |
+
if name not in self.all_timings_dict: self.add_new_timing(name)
|
| 629 |
+
# assert name in self.all_timings_dict
|
| 630 |
+
self.all_timings_dict[name] = value
|
| 631 |
+
|
| 632 |
+
def _record_cuda_event_start(self, name):
|
| 633 |
+
if name in self.cuda_event_timers:
|
| 634 |
+
self.cuda_event_timers[name].start.record(
|
| 635 |
+
torch.cuda.current_stream(self.device))
|
| 636 |
+
|
| 637 |
+
def _record_cuda_event_end(self, name):
|
| 638 |
+
if name in self.cuda_event_timers:
|
| 639 |
+
self.cuda_event_timers[name].end.record(
|
| 640 |
+
torch.cuda.current_stream(self.device))
|
| 641 |
+
|
| 642 |
+
@contextmanager
|
| 643 |
+
def cuda_timing_scope(self, name, profile=True):
|
| 644 |
+
if name not in self.all_timings_dict: self.add_new_cuda_timing(name)
|
| 645 |
+
with ScopedTorchProfiler(name) if profile else nullcontext():
|
| 646 |
+
self._record_cuda_event_start(name)
|
| 647 |
+
try:
|
| 648 |
+
yield
|
| 649 |
+
finally:
|
| 650 |
+
self._record_cuda_event_end(name)
|
| 651 |
+
|
| 652 |
+
@contextmanager
|
| 653 |
+
def timing_scope(self, name, profile=True):
|
| 654 |
+
if name not in self.all_timings_dict: self.add_new_timing(name)
|
| 655 |
+
with ScopedTorchProfiler(name) if profile else nullcontext():
|
| 656 |
+
start_time = time.time()
|
| 657 |
+
try:
|
| 658 |
+
yield
|
| 659 |
+
finally:
|
| 660 |
+
self.record_timing(name, time.time()-start_time)
|
| 661 |
+
|
| 662 |
+
def record_all_cuda_timings(self):
|
| 663 |
+
""" After all the cuda events call this to synchronize and record down the cuda timings. """
|
| 664 |
+
for k, events in self.cuda_event_timers.items():
|
| 665 |
+
with torch.no_grad():
|
| 666 |
+
events.end.synchronize()
|
| 667 |
+
# Convert to seconds
|
| 668 |
+
time_elapsed = events.start.elapsed_time(events.end)/1000.
|
| 669 |
+
self.all_timings_dict[k] = time_elapsed
|
| 670 |
+
|
| 671 |
+
def init_s3(config_file):
|
| 672 |
+
config = json.load(open(config_file, 'r'))
|
| 673 |
+
s3_client = boto3.client("s3", **config)
|
| 674 |
+
return s3_client
|
| 675 |
+
|
| 676 |
+
def download_from_s3(file_path, target_path, cfg):
|
| 677 |
+
tic = time.time()
|
| 678 |
+
s3_client = init_s3(cfg.checkpoint.write_s3_config) # use to test the s3_client can be init
|
| 679 |
+
bucket_name = file_path.split('/')[2]
|
| 680 |
+
file_key = file_path.split(bucket_name+'/')[-1]
|
| 681 |
+
print(bucket_name, file_key)
|
| 682 |
+
s3_client.download_file(bucket_name, file_key, target_path)
|
| 683 |
+
logger.info(f'finish download from ! s3://{bucket_name}/{file_key} to {target_path} %.1f sec'%(
|
| 684 |
+
time.time() - tic))
|
| 685 |
+
|
| 686 |
+
def upload_to_s3(buffer, bucket_name, key, config_dict):
|
| 687 |
+
logger.info(f'start upload_to_s3! bucket_name={bucket_name}, key={key}')
|
| 688 |
+
tic = time.time()
|
| 689 |
+
s3 = boto3.client('s3', **config_dict)
|
| 690 |
+
s3.put_object(Bucket=bucket_name, Key=key, Body=buffer.getvalue())
|
| 691 |
+
logger.info(f'finish upload_to_s3! s3://{bucket_name}/{key} %.1f sec'%(time.time() - tic))
|
| 692 |
+
|
| 693 |
+
def write_ckpt_to_s3(cfg, all_model_dict, ckpt_name):
|
| 694 |
+
buffer = io.BytesIO()
|
| 695 |
+
tic = time.time()
|
| 696 |
+
torch.save(all_model_dict, buffer) # take ~0.25 sec
|
| 697 |
+
# logger.info('write ckpt to buffer: %.2f sec'%(time.time() - tic))
|
| 698 |
+
group, name = cfg.outdir.rstrip("/").split("/")[-2:]
|
| 699 |
+
key = f"checkpoints/{group}/{name}/ckpt/{ckpt_name}"
|
| 700 |
+
bucket_name = cfg.checkpoint.write_s3_bucket
|
| 701 |
+
|
| 702 |
+
s3_client = init_s3(cfg.checkpoint.write_s3_config) # use to test the s3_client can be init
|
| 703 |
+
|
| 704 |
+
config_dict = json.load(open(cfg.checkpoint.write_s3_config, 'r'))
|
| 705 |
+
upload_thread = threading.Thread(target=upload_to_s3, args=(buffer, bucket_name, key, config_dict))
|
| 706 |
+
upload_thread.start()
|
| 707 |
+
path = f"s3://{bucket_name}/{key}"
|
| 708 |
+
return path
|
| 709 |
+
|
| 710 |
+
def upload_file_to_s3(cfg, file_path, key_name=None):
|
| 711 |
+
# file_path is the local file path, can be a yaml file
|
| 712 |
+
# this function is used to upload the ckecpoint only
|
| 713 |
+
tic = time.time()
|
| 714 |
+
group, name = cfg.outdir.rstrip("/").split("/")[-2:]
|
| 715 |
+
if key_name is None:
|
| 716 |
+
key = os.path.basename(file_path)
|
| 717 |
+
key = f"checkpoints/{group}/{name}/{key}"
|
| 718 |
+
bucket_name = cfg.checkpoint.write_s3_bucket
|
| 719 |
+
s3_client = init_s3(cfg.checkpoint.write_s3_config)
|
| 720 |
+
# Upload the file
|
| 721 |
+
with open(file_path, 'rb') as f:
|
| 722 |
+
s3_client.upload_fileobj(f, bucket_name, key)
|
| 723 |
+
full_s3_path = f"s3://{bucket_name}/{key}"
|
| 724 |
+
logger.info(f'upload_to_s3: {file_path} {full_s3_path} | use time: {time.time()-tic}')
|
| 725 |
+
|
| 726 |
+
return full_s3_path
|
| 727 |
+
|
| 728 |
+
|
| 729 |
+
def load_from_s3(file_path, cfg, load_fn):
|
| 730 |
+
"""
|
| 731 |
+
ckpt_path example:
|
| 732 |
+
s3://xzeng/checkpoints/2023_0413/vae_kl_5e-1/ckpt/snapshot_epo000163_iter164000.pt
|
| 733 |
+
"""
|
| 734 |
+
s3_client = init_s3(cfg.checkpoint.write_s3_config) # use to test the s3_client can be init
|
| 735 |
+
bucket_name = file_path.split("s3://")[-1].split('/')[0]
|
| 736 |
+
key = file_path.split(f'{bucket_name}/')[-1]
|
| 737 |
+
# logger.info(f"-> try to load s3://{bucket_name}/{key} ")
|
| 738 |
+
tic = time.time()
|
| 739 |
+
for attemp in range(10):
|
| 740 |
+
try:
|
| 741 |
+
# Download the state dict from S3 into memory (as a binary stream)
|
| 742 |
+
with io.BytesIO() as buffer:
|
| 743 |
+
s3_client.download_fileobj(bucket_name, key, buffer)
|
| 744 |
+
buffer.seek(0)
|
| 745 |
+
|
| 746 |
+
# Load the state dict into a PyTorch model
|
| 747 |
+
# out = torch.load(buffer, map_location=torch.device("cpu"))
|
| 748 |
+
out = load_fn(buffer)
|
| 749 |
+
break
|
| 750 |
+
except:
|
| 751 |
+
logger.info(f"fail to load s3://{bucket_name}/{key} attemp: {attemp}")
|
| 752 |
+
from torch_utils.dist_utils import is_rank0
|
| 753 |
+
if is_rank0():
|
| 754 |
+
logger.info(f'loaded {file_path} | use time: {time.time()-tic:.1f} sec')
|
| 755 |
+
return out
|
| 756 |
+
|
| 757 |
+
def load_torch_dict_from_s3(ckpt_path, cfg):
|
| 758 |
+
"""
|
| 759 |
+
ckpt_path example:
|
| 760 |
+
s3://xzeng/checkpoints/2023_0413/vae_kl_5e-1/ckpt/snapshot_epo000163_iter164000.pt
|
| 761 |
+
"""
|
| 762 |
+
s3_client = init_s3(cfg.checkpoint.write_s3_config) # use to test the s3_client can be init
|
| 763 |
+
bucket_name = ckpt_path.split("s3://")[-1].split('/')[0]
|
| 764 |
+
key = ckpt_path.split(f'{bucket_name}/')[-1]
|
| 765 |
+
for attemp in range(10):
|
| 766 |
+
try:
|
| 767 |
+
# Download the state dict from S3 into memory (as a binary stream)
|
| 768 |
+
with io.BytesIO() as buffer:
|
| 769 |
+
s3_client.download_fileobj(bucket_name, key, buffer)
|
| 770 |
+
buffer.seek(0)
|
| 771 |
+
|
| 772 |
+
# Load the state dict into a PyTorch model
|
| 773 |
+
out = torch.load(buffer, map_location=torch.device("cpu"))
|
| 774 |
+
break
|
| 775 |
+
except:
|
| 776 |
+
logger.info(f"fail to load s3://{bucket_name}/{key} attemp: {attemp}")
|
| 777 |
+
return out
|
| 778 |
+
|
| 779 |
+
def count_parameters_in_M(model):
|
| 780 |
+
return np.sum(np.prod(v.size()) for name, v in model.named_parameters() if "auxiliary" not in name) / 1e6
|
| 781 |
+
|
| 782 |
+
def printarr(*arrs, float_width=6, **kwargs):
|
| 783 |
+
"""
|
| 784 |
+
Print a pretty table giving name, shape, dtype, type, and content information for input tensors or scalars.
|
| 785 |
+
|
| 786 |
+
Call like: printarr(my_arr, some_other_arr, maybe_a_scalar). Accepts a variable number of arguments.
|
| 787 |
+
|
| 788 |
+
Inputs can be:
|
| 789 |
+
- Numpy tensor arrays
|
| 790 |
+
- Pytorch tensor arrays
|
| 791 |
+
- Jax tensor arrays
|
| 792 |
+
- Python ints / floats
|
| 793 |
+
- None
|
| 794 |
+
|
| 795 |
+
It may also work with other array-like types, but they have not been tested.
|
| 796 |
+
|
| 797 |
+
Use the `float_width` option specify the precision to which floating point types are printed.
|
| 798 |
+
|
| 799 |
+
Author: Nicholas Sharp (nmwsharp.com)
|
| 800 |
+
Canonical source: https://gist.github.com/nmwsharp/54d04af87872a4988809f128e1a1d233
|
| 801 |
+
License: This snippet may be used under an MIT license, and it is also released into the public domain.
|
| 802 |
+
Please retain this docstring as a reference.
|
| 803 |
+
"""
|
| 804 |
+
|
| 805 |
+
frame = inspect.currentframe().f_back
|
| 806 |
+
default_name = "[temporary]"
|
| 807 |
+
|
| 808 |
+
## helpers to gather data about each array
|
| 809 |
+
def name_from_outer_scope(a):
|
| 810 |
+
if a is None:
|
| 811 |
+
return '[None]'
|
| 812 |
+
name = default_name
|
| 813 |
+
for k, v in frame.f_locals.items():
|
| 814 |
+
if v is a:
|
| 815 |
+
name = k
|
| 816 |
+
break
|
| 817 |
+
return name
|
| 818 |
+
|
| 819 |
+
def type_strip(type_str):
|
| 820 |
+
return type_str.lstrip('<class ').rstrip('>').replace('torch.', '').strip("'")
|
| 821 |
+
|
| 822 |
+
def dtype_str(a):
|
| 823 |
+
if a is None:
|
| 824 |
+
return 'None'
|
| 825 |
+
if isinstance(a, int):
|
| 826 |
+
return 'int'
|
| 827 |
+
if isinstance(a, float):
|
| 828 |
+
return 'float'
|
| 829 |
+
if isinstance(a, list) and len(a)>0:
|
| 830 |
+
return type_strip(str(type(a[0])))
|
| 831 |
+
if hasattr(a, 'dtype'):
|
| 832 |
+
return type_strip(str(a.dtype))
|
| 833 |
+
else:
|
| 834 |
+
return ''
|
| 835 |
+
def shape_str(a):
|
| 836 |
+
if a is None:
|
| 837 |
+
return 'N/A'
|
| 838 |
+
if isinstance(a, int):
|
| 839 |
+
return 'scalar'
|
| 840 |
+
if isinstance(a, float):
|
| 841 |
+
return 'scalar'
|
| 842 |
+
if isinstance(a, list):
|
| 843 |
+
return f"[{shape_str(a[0]) if len(a)>0 else '?'}]*{len(a)}"
|
| 844 |
+
if hasattr(a, 'shape'):
|
| 845 |
+
return str(tuple(a.shape))
|
| 846 |
+
else:
|
| 847 |
+
return ''
|
| 848 |
+
def type_str(a):
|
| 849 |
+
return type_strip(str(type(a))) # TODO this is is weird... what's the better way?
|
| 850 |
+
def device_str(a):
|
| 851 |
+
if hasattr(a, 'device'):
|
| 852 |
+
device_str = str(a.device)
|
| 853 |
+
if len(device_str) < 10:
|
| 854 |
+
# heuristic: jax returns some goofy long string we don't want, ignore it
|
| 855 |
+
return device_str
|
| 856 |
+
return ""
|
| 857 |
+
def format_float(x):
|
| 858 |
+
return f"{x:{float_width}g}"
|
| 859 |
+
def minmaxmean_str(a):
|
| 860 |
+
if a is None:
|
| 861 |
+
return ('N/A', 'N/A', 'N/A', 'N/A')
|
| 862 |
+
if isinstance(a, int) or isinstance(a, float):
|
| 863 |
+
return (format_float(a),)*4
|
| 864 |
+
|
| 865 |
+
# compute min/max/mean. if anything goes wrong, just print 'N/A'
|
| 866 |
+
min_str = "N/A"
|
| 867 |
+
try: min_str = format_float(a.min())
|
| 868 |
+
except: pass
|
| 869 |
+
max_str = "N/A"
|
| 870 |
+
try: max_str = format_float(a.max())
|
| 871 |
+
except: pass
|
| 872 |
+
mean_str = "N/A"
|
| 873 |
+
try: mean_str = format_float(a.mean())
|
| 874 |
+
except: pass
|
| 875 |
+
try: median_str = format_float(a.median())
|
| 876 |
+
except:
|
| 877 |
+
try: median_str = format_float(np.median(np.array(a)))
|
| 878 |
+
except: median_str = 'N/A'
|
| 879 |
+
return (min_str, max_str, mean_str, median_str)
|
| 880 |
+
|
| 881 |
+
def get_prop_dict(a,k=None):
|
| 882 |
+
minmaxmean = minmaxmean_str(a)
|
| 883 |
+
props = {
|
| 884 |
+
'name' : name_from_outer_scope(a) if k is None else k,
|
| 885 |
+
# 'type' : str(type(a)).replace('torch.',''),
|
| 886 |
+
'dtype' : dtype_str(a),
|
| 887 |
+
'shape' : shape_str(a),
|
| 888 |
+
'type' : type_str(a),
|
| 889 |
+
'device' : device_str(a),
|
| 890 |
+
'min' : minmaxmean[0],
|
| 891 |
+
'max' : minmaxmean[1],
|
| 892 |
+
'mean' : minmaxmean[2],
|
| 893 |
+
'median': minmaxmean[3]
|
| 894 |
+
}
|
| 895 |
+
return props
|
| 896 |
+
|
| 897 |
+
try:
|
| 898 |
+
|
| 899 |
+
props = ['name', 'type', 'dtype', 'shape', 'device', 'min', 'max', 'mean', 'median']
|
| 900 |
+
|
| 901 |
+
# precompute all of the properties for each input
|
| 902 |
+
str_props = []
|
| 903 |
+
for a in arrs:
|
| 904 |
+
str_props.append(get_prop_dict(a))
|
| 905 |
+
for k,a in kwargs.items():
|
| 906 |
+
str_props.append(get_prop_dict(a, k=k))
|
| 907 |
+
|
| 908 |
+
# for each property, compute its length
|
| 909 |
+
maxlen = {}
|
| 910 |
+
for p in props: maxlen[p] = 0
|
| 911 |
+
for sp in str_props:
|
| 912 |
+
for p in props:
|
| 913 |
+
maxlen[p] = max(maxlen[p], len(sp[p]))
|
| 914 |
+
|
| 915 |
+
# if any property got all empty strings, don't bother printing it, remove if from the list
|
| 916 |
+
props = [p for p in props if maxlen[p] > 0]
|
| 917 |
+
|
| 918 |
+
# print a header
|
| 919 |
+
header_str = ""
|
| 920 |
+
for p in props:
|
| 921 |
+
prefix = "" if p == 'name' else " | "
|
| 922 |
+
fmt_key = ">" if p == 'name' else "<"
|
| 923 |
+
header_str += f"{prefix}{p:{fmt_key}{maxlen[p]}}"
|
| 924 |
+
print(header_str)
|
| 925 |
+
print("-"*len(header_str))
|
| 926 |
+
|
| 927 |
+
# now print the acual arrays
|
| 928 |
+
for strp in str_props:
|
| 929 |
+
for p in props:
|
| 930 |
+
prefix = "" if p == 'name' else " | "
|
| 931 |
+
fmt_key = ">" if p == 'name' else "<"
|
| 932 |
+
print(f"{prefix}{strp[p]:{fmt_key}{maxlen[p]}}", end='')
|
| 933 |
+
print("")
|
| 934 |
+
|
| 935 |
+
finally:
|
| 936 |
+
del frame
|
| 937 |
+
|
| 938 |
+
def debug_print_all_tensor_sizes(min_tot_size = 0):
|
| 939 |
+
import gc
|
| 940 |
+
print("---------------------------------------"*3)
|
| 941 |
+
for obj in gc.get_objects():
|
| 942 |
+
try:
|
| 943 |
+
if torch.is_tensor(obj) or (hasattr(obj, 'data') and torch.is_tensor(obj.data)):
|
| 944 |
+
if np.prod(obj.size())>=min_tot_size:
|
| 945 |
+
print(type(obj), obj.size())
|
| 946 |
+
except:
|
| 947 |
+
pass
|
| 948 |
+
def print_cpu_usage():
|
| 949 |
+
|
| 950 |
+
# Get current CPU usage as a percentage
|
| 951 |
+
cpu_usage = psutil.cpu_percent()
|
| 952 |
+
|
| 953 |
+
# Get current memory usage
|
| 954 |
+
memory_usage = psutil.virtual_memory().used
|
| 955 |
+
|
| 956 |
+
# Convert memory usage to a human-readable format
|
| 957 |
+
memory_usage_str = psutil._common.bytes2human(memory_usage)
|
| 958 |
+
|
| 959 |
+
# Print CPU and memory usage
|
| 960 |
+
msg = f"Current CPU usage: {cpu_usage}% | "
|
| 961 |
+
msg += f"Current memory usage: {memory_usage_str}"
|
| 962 |
+
return msg
|
| 963 |
+
|
| 964 |
+
def calmsize(num_bytes):
|
| 965 |
+
if math.isnan(num_bytes):
|
| 966 |
+
return ''
|
| 967 |
+
for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']:
|
| 968 |
+
if abs(num_bytes) < 1024.0:
|
| 969 |
+
return "{:.1f}{}B".format(num_bytes, unit)
|
| 970 |
+
num_bytes /= 1024.0
|
| 971 |
+
return "{:.1f}{}B".format(num_bytes, 'Y')
|
| 972 |
+
|
| 973 |
+
def readable_size(num_bytes: int) -> str:
|
| 974 |
+
return calmsize(num_bytes) ## '' if math.isnan(num_bytes) else '{:.1f}'.format(calmsize(num_bytes))
|
| 975 |
+
|
| 976 |
+
def get_gpu_memory():
|
| 977 |
+
"""
|
| 978 |
+
Get the current GPU memory usage for each device as a dictionary
|
| 979 |
+
"""
|
| 980 |
+
output = subprocess.check_output(["nvidia-smi", "--query-gpu=memory.used", "--format=csv"])
|
| 981 |
+
output = output.decode("utf-8")
|
| 982 |
+
gpu_memory_values = output.split("\n")[1:-1]
|
| 983 |
+
gpu_memory_values = [int(x.strip().split()[0]) for x in gpu_memory_values]
|
| 984 |
+
gpu_memory = dict(zip(range(len(gpu_memory_values)), gpu_memory_values))
|
| 985 |
+
return gpu_memory
|
| 986 |
+
|
| 987 |
+
def get_gpu_util():
|
| 988 |
+
"""
|
| 989 |
+
Get the current GPU memory usage for each device as a dictionary
|
| 990 |
+
"""
|
| 991 |
+
output = subprocess.check_output(["nvidia-smi", "--query-gpu=utilization.gpu", "--format=csv"])
|
| 992 |
+
output = output.decode("utf-8")
|
| 993 |
+
gpu_memory_values = output.split("\n")[1:-1]
|
| 994 |
+
gpu_memory_values = [int(x.strip().split()[0]) for x in gpu_memory_values]
|
| 995 |
+
gpu_util = dict(zip(range(len(gpu_memory_values)), gpu_memory_values))
|
| 996 |
+
return gpu_util
|
| 997 |
+
|
| 998 |
+
|
| 999 |
+
def print_gpu_usage():
|
| 1000 |
+
useage = get_gpu_memory()
|
| 1001 |
+
msg = f" | GPU usage: "
|
| 1002 |
+
for k, v in useage.items():
|
| 1003 |
+
msg += f"{k}: {v} MB "
|
| 1004 |
+
# utilization = get_gpu_util()
|
| 1005 |
+
# msg + ' | util '
|
| 1006 |
+
# for k, v in utilization.items():
|
| 1007 |
+
# msg += f"{k}: {v} % "
|
| 1008 |
+
return msg
|
| 1009 |
+
|
| 1010 |
+
class AverageMeter(object):
|
| 1011 |
+
|
| 1012 |
+
def __init__(self):
|
| 1013 |
+
self.reset()
|
| 1014 |
+
|
| 1015 |
+
def reset(self):
|
| 1016 |
+
self.avg = 0
|
| 1017 |
+
self.sum = 0
|
| 1018 |
+
self.cnt = 0
|
| 1019 |
+
|
| 1020 |
+
def update(self, val, n=1):
|
| 1021 |
+
self.sum += val * n
|
| 1022 |
+
self.cnt += n
|
| 1023 |
+
self.avg = self.sum / self.cnt
|
| 1024 |
+
|
| 1025 |
+
|
| 1026 |
+
def generate_random_string(length):
|
| 1027 |
+
# This script will generate a string of 10 random ASCII letters (both lowercase and uppercase).
|
| 1028 |
+
# You can adjust the length parameter to fit your needs.
|
| 1029 |
+
letters = string.ascii_letters
|
| 1030 |
+
return ''.join(random.choice(letters) for _ in range(length))
|
| 1031 |
+
|
| 1032 |
+
|
| 1033 |
+
class ForkedPdb(pdb.Pdb):
|
| 1034 |
+
"""
|
| 1035 |
+
PDB Subclass for debugging multi-processed code
|
| 1036 |
+
Suggested in: https://stackoverflow.com/questions/4716533/how-to-attach-debugger-to-a-python-subproccess
|
| 1037 |
+
"""
|
| 1038 |
+
def interaction(self, *args, **kwargs):
|
| 1039 |
+
_stdin = sys.stdin
|
| 1040 |
+
try:
|
| 1041 |
+
sys.stdin = open('/dev/stdin')
|
| 1042 |
+
pdb.Pdb.interaction(self, *args, **kwargs)
|
| 1043 |
+
finally:
|
| 1044 |
+
sys.stdin = _stdin
|
| 1045 |
+
|
| 1046 |
+
def check_exist_in_s3(file_path, s3_config):
|
| 1047 |
+
s3 = init_s3(s3_config)
|
| 1048 |
+
bucket_name, object_name = s3path_to_bucket_key(file_path)
|
| 1049 |
+
|
| 1050 |
+
try:
|
| 1051 |
+
s3.head_object(Bucket=bucket_name, Key=object_name)
|
| 1052 |
+
return 1
|
| 1053 |
+
except:
|
| 1054 |
+
logger.info(f'file not found: s3://{bucket_name}/{object_name}')
|
| 1055 |
+
return 0
|
| 1056 |
+
|
| 1057 |
+
def s3path_to_bucket_key(file_path):
|
| 1058 |
+
bucket_name = file_path.split('/')[2]
|
| 1059 |
+
object_name = file_path.split(bucket_name + '/')[-1]
|
| 1060 |
+
return bucket_name, object_name
|
| 1061 |
+
|
| 1062 |
+
def copy_file_to_s3(cfg, file_path_local, file_path_s3):
|
| 1063 |
+
# work similar as upload_file_to_s3, but not trying to parse the file path
|
| 1064 |
+
# file_path_s3: s3://{bucket}/{key}
|
| 1065 |
+
bucket_name, key = s3path_to_bucket_key(file_path_s3)
|
| 1066 |
+
tic = time.time()
|
| 1067 |
+
s3_client = init_s3(cfg.checkpoint.write_s3_config)
|
| 1068 |
+
|
| 1069 |
+
# Upload the file
|
| 1070 |
+
with open(file_path_local, 'rb') as f:
|
| 1071 |
+
s3_client.upload_fileobj(f, bucket_name, key)
|
| 1072 |
+
full_s3_path = f"s3://{bucket_name}/{key}"
|
| 1073 |
+
logger.info(f'copy file: {file_path_local} {full_s3_path} | use time: {time.time()-tic}')
|
| 1074 |
+
return full_s3_path
|
third_party/PartField/partfield/model/PVCNN/encoder_pc.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# NVIDIA CORPORATION & AFFILIATES and its licensors retain all intellectual property
|
| 4 |
+
# and proprietary rights in and to this software, related documentation
|
| 5 |
+
# and any modifications thereto. Any use, reproduction, disclosure or
|
| 6 |
+
# distribution of this software and related documentation without an express
|
| 7 |
+
# license agreement from NVIDIA CORPORATION & AFFILIATES is strictly prohibited.
|
| 8 |
+
|
| 9 |
+
from ast import Dict
|
| 10 |
+
import math
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import torch
|
| 14 |
+
from torch import nn
|
| 15 |
+
import torch.nn.functional as F
|
| 16 |
+
from torch_scatter import scatter_mean #, scatter_max
|
| 17 |
+
|
| 18 |
+
from .unet_3daware import setup_unet #UNetTriplane3dAware
|
| 19 |
+
from .conv_pointnet import ConvPointnet
|
| 20 |
+
|
| 21 |
+
from .pc_encoder import PVCNNEncoder #PointNet
|
| 22 |
+
|
| 23 |
+
import einops
|
| 24 |
+
|
| 25 |
+
from .dnnlib_util import ScopedTorchProfiler, printarr
|
| 26 |
+
|
| 27 |
+
def generate_plane_features(p, c, resolution, plane='xz'):
|
| 28 |
+
"""
|
| 29 |
+
Args:
|
| 30 |
+
p: (B,3,n_p)
|
| 31 |
+
c: (B,C,n_p)
|
| 32 |
+
"""
|
| 33 |
+
padding = 0.
|
| 34 |
+
c_dim = c.size(1)
|
| 35 |
+
# acquire indices of features in plane
|
| 36 |
+
xy = normalize_coordinate(p.clone(), plane=plane, padding=padding) # normalize to the range of (0, 1)
|
| 37 |
+
index = coordinate2index(xy, resolution)
|
| 38 |
+
|
| 39 |
+
# scatter plane features from points
|
| 40 |
+
fea_plane = c.new_zeros(p.size(0), c_dim, resolution**2)
|
| 41 |
+
fea_plane = scatter_mean(c, index, out=fea_plane) # B x 512 x reso^2
|
| 42 |
+
fea_plane = fea_plane.reshape(p.size(0), c_dim, resolution, resolution) # sparce matrix (B x 512 x reso x reso)
|
| 43 |
+
return fea_plane
|
| 44 |
+
|
| 45 |
+
def normalize_coordinate(p, padding=0.1, plane='xz'):
|
| 46 |
+
''' Normalize coordinate to [0, 1] for unit cube experiments
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
p (tensor): point
|
| 50 |
+
padding (float): conventional padding paramter of ONet for unit cube, so [-0.5, 0.5] -> [-0.55, 0.55]
|
| 51 |
+
plane (str): plane feature type, ['xz', 'xy', 'yz']
|
| 52 |
+
'''
|
| 53 |
+
if plane == 'xz':
|
| 54 |
+
xy = p[:, :, [0, 2]]
|
| 55 |
+
elif plane =='xy':
|
| 56 |
+
xy = p[:, :, [0, 1]]
|
| 57 |
+
else:
|
| 58 |
+
xy = p[:, :, [1, 2]]
|
| 59 |
+
|
| 60 |
+
xy_new = xy / (1 + padding + 10e-6) # (-0.5, 0.5)
|
| 61 |
+
xy_new = xy_new + 0.5 # range (0, 1)
|
| 62 |
+
|
| 63 |
+
# if there are outliers out of the range
|
| 64 |
+
if xy_new.max() >= 1:
|
| 65 |
+
xy_new[xy_new >= 1] = 1 - 10e-6
|
| 66 |
+
if xy_new.min() < 0:
|
| 67 |
+
xy_new[xy_new < 0] = 0.0
|
| 68 |
+
return xy_new
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def coordinate2index(x, resolution):
|
| 72 |
+
''' Normalize coordinate to [0, 1] for unit cube experiments.
|
| 73 |
+
Corresponds to our 3D model
|
| 74 |
+
|
| 75 |
+
Args:
|
| 76 |
+
x (tensor): coordinate
|
| 77 |
+
reso (int): defined resolution
|
| 78 |
+
coord_type (str): coordinate type
|
| 79 |
+
'''
|
| 80 |
+
x = (x * resolution).long()
|
| 81 |
+
index = x[:, :, 0] + resolution * x[:, :, 1]
|
| 82 |
+
index = index[:, None, :]
|
| 83 |
+
return index
|
| 84 |
+
|
| 85 |
+
def softclip(x, min, max, hardness=5):
|
| 86 |
+
# Soft clipping for the logsigma
|
| 87 |
+
x = min + F.softplus(hardness*(x - min))/hardness
|
| 88 |
+
x = max - F.softplus(-hardness*(x - max))/hardness
|
| 89 |
+
return x
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def sample_triplane_feat(feature_triplane, normalized_pos):
|
| 93 |
+
'''
|
| 94 |
+
normalized_pos [-1, 1]
|
| 95 |
+
'''
|
| 96 |
+
tri_plane = torch.unbind(feature_triplane, dim=1)
|
| 97 |
+
|
| 98 |
+
x_feat = F.grid_sample(
|
| 99 |
+
tri_plane[0],
|
| 100 |
+
torch.cat(
|
| 101 |
+
[normalized_pos[:, :, 0:1], normalized_pos[:, :, 1:2]],
|
| 102 |
+
dim=-1).unsqueeze(dim=1), padding_mode='border',
|
| 103 |
+
align_corners=True)
|
| 104 |
+
y_feat = F.grid_sample(
|
| 105 |
+
tri_plane[1],
|
| 106 |
+
torch.cat(
|
| 107 |
+
[normalized_pos[:, :, 1:2], normalized_pos[:, :, 2:3]],
|
| 108 |
+
dim=-1).unsqueeze(dim=1), padding_mode='border',
|
| 109 |
+
align_corners=True)
|
| 110 |
+
|
| 111 |
+
z_feat = F.grid_sample(
|
| 112 |
+
tri_plane[2],
|
| 113 |
+
torch.cat(
|
| 114 |
+
[normalized_pos[:, :, 0:1], normalized_pos[:, :, 2:3]],
|
| 115 |
+
dim=-1).unsqueeze(dim=1), padding_mode='border',
|
| 116 |
+
align_corners=True)
|
| 117 |
+
final_feat = (x_feat + y_feat + z_feat)
|
| 118 |
+
final_feat = final_feat.squeeze(dim=2).permute(0, 2, 1) # 32dimension
|
| 119 |
+
return final_feat
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# @persistence.persistent_class
|
| 123 |
+
class TriPlanePC2Encoder(torch.nn.Module):
|
| 124 |
+
# Encoder that encode point cloud to triplane feature vector similar to ConvOccNet
|
| 125 |
+
def __init__(
|
| 126 |
+
self,
|
| 127 |
+
cfg,
|
| 128 |
+
device='cuda',
|
| 129 |
+
shape_min=-1.0,
|
| 130 |
+
shape_length=2.0,
|
| 131 |
+
use_2d_feat=False,
|
| 132 |
+
# point_encoder='pvcnn',
|
| 133 |
+
# use_point_scatter=False
|
| 134 |
+
):
|
| 135 |
+
"""
|
| 136 |
+
Outputs latent triplane from PC input
|
| 137 |
+
Configs:
|
| 138 |
+
max_logsigma: (float) Soft clip upper range for logsigm
|
| 139 |
+
min_logsigma: (float)
|
| 140 |
+
point_encoder_type: (str) one of ['pvcnn', 'pointnet']
|
| 141 |
+
pvcnn_flatten_voxels: (bool) for pvcnn whether to reduce voxel
|
| 142 |
+
features (instead of scattering point features)
|
| 143 |
+
unet_cfg: (dict)
|
| 144 |
+
z_triplane_channels: (int) output latent triplane
|
| 145 |
+
z_triplane_resolution: (int)
|
| 146 |
+
Args:
|
| 147 |
+
|
| 148 |
+
"""
|
| 149 |
+
# assert img_resolution >= 4 and img_resolution & (img_resolution - 1) == 0
|
| 150 |
+
super().__init__()
|
| 151 |
+
self.device = device
|
| 152 |
+
|
| 153 |
+
self.cfg = cfg
|
| 154 |
+
|
| 155 |
+
self.shape_min = shape_min
|
| 156 |
+
self.shape_length = shape_length
|
| 157 |
+
|
| 158 |
+
self.z_triplane_resolution = cfg.z_triplane_resolution
|
| 159 |
+
z_triplane_channels = cfg.z_triplane_channels
|
| 160 |
+
|
| 161 |
+
point_encoder_out_dim = z_triplane_channels #* 2
|
| 162 |
+
|
| 163 |
+
in_channels = 6
|
| 164 |
+
# self.resample_filter=[1, 3, 3, 1]
|
| 165 |
+
if cfg.point_encoder_type == 'pvcnn':
|
| 166 |
+
self.pc_encoder = PVCNNEncoder(point_encoder_out_dim,
|
| 167 |
+
device=self.device, in_channels=in_channels, use_2d_feat=use_2d_feat) # Encode it to a volume vector.
|
| 168 |
+
elif cfg.point_encoder_type == 'pointnet':
|
| 169 |
+
# TODO the pointnet was buggy, investigate
|
| 170 |
+
self.pc_encoder = ConvPointnet(c_dim=point_encoder_out_dim,
|
| 171 |
+
dim=in_channels, hidden_dim=32,
|
| 172 |
+
plane_resolution=self.z_triplane_resolution,
|
| 173 |
+
padding=0)
|
| 174 |
+
else:
|
| 175 |
+
raise NotImplementedError(f"Point encoder {cfg.point_encoder_type} not implemented")
|
| 176 |
+
|
| 177 |
+
if cfg.unet_cfg.enabled:
|
| 178 |
+
self.unet_encoder = setup_unet(
|
| 179 |
+
output_channels=point_encoder_out_dim,
|
| 180 |
+
input_channels=point_encoder_out_dim,
|
| 181 |
+
unet_cfg=cfg.unet_cfg)
|
| 182 |
+
else:
|
| 183 |
+
self.unet_encoder = None
|
| 184 |
+
|
| 185 |
+
# @ScopedTorchProfiler('encode')
|
| 186 |
+
def encode(self, point_cloud_xyz, point_cloud_feature, mv_feat=None, pc2pc_idx=None) -> Dict:
|
| 187 |
+
# output = AttrDict()
|
| 188 |
+
point_cloud_xyz = (point_cloud_xyz - self.shape_min) / self.shape_length # [0, 1]
|
| 189 |
+
point_cloud_xyz = point_cloud_xyz - 0.5 # [-0.5, 0.5]
|
| 190 |
+
point_cloud = torch.cat([point_cloud_xyz, point_cloud_feature], dim=-1)
|
| 191 |
+
|
| 192 |
+
if self.cfg.point_encoder_type == 'pvcnn':
|
| 193 |
+
if mv_feat is not None:
|
| 194 |
+
pc_feat, points_feat = self.pc_encoder(point_cloud, mv_feat, pc2pc_idx)
|
| 195 |
+
else:
|
| 196 |
+
pc_feat, points_feat = self.pc_encoder(point_cloud) # 3D feature volume: BxDx32x32x32
|
| 197 |
+
if self.cfg.use_point_scatter:
|
| 198 |
+
# Scattering from PVCNN point features
|
| 199 |
+
points_feat_ = points_feat[0]
|
| 200 |
+
# shape: batch, latent size, resolution, resolution (e.g. 16, 256, 64, 64)
|
| 201 |
+
pc_feat_1 = generate_plane_features(point_cloud_xyz, points_feat_,
|
| 202 |
+
resolution=self.z_triplane_resolution, plane='xy')
|
| 203 |
+
pc_feat_2 = generate_plane_features(point_cloud_xyz, points_feat_,
|
| 204 |
+
resolution=self.z_triplane_resolution, plane='yz')
|
| 205 |
+
pc_feat_3 = generate_plane_features(point_cloud_xyz, points_feat_,
|
| 206 |
+
resolution=self.z_triplane_resolution, plane='xz')
|
| 207 |
+
pc_feat = pc_feat[0]
|
| 208 |
+
|
| 209 |
+
else:
|
| 210 |
+
pc_feat = pc_feat[0]
|
| 211 |
+
sf = self.z_triplane_resolution//32 # 32 is PVCNN's voxel dim
|
| 212 |
+
|
| 213 |
+
pc_feat_1 = torch.mean(pc_feat, dim=-1) #xy_plane, normalize in z plane
|
| 214 |
+
pc_feat_2 = torch.mean(pc_feat, dim=-3) #yz_plane, normalize in x plane
|
| 215 |
+
pc_feat_3 = torch.mean(pc_feat, dim=-2) #xz_plane, normalize in y plane
|
| 216 |
+
|
| 217 |
+
# nearest upsample
|
| 218 |
+
pc_feat_1 = einops.repeat(pc_feat_1, 'b c h w -> b c (h hm ) (w wm)', hm = sf, wm = sf)
|
| 219 |
+
pc_feat_2 = einops.repeat(pc_feat_2, 'b c h w -> b c (h hm) (w wm)', hm = sf, wm = sf)
|
| 220 |
+
pc_feat_3 = einops.repeat(pc_feat_3, 'b c h w -> b c (h hm) (w wm)', hm = sf, wm = sf)
|
| 221 |
+
elif self.cfg.point_encoder_type == 'pointnet':
|
| 222 |
+
assert self.cfg.use_point_scatter
|
| 223 |
+
# Run ConvPointnet
|
| 224 |
+
pc_feat = self.pc_encoder(point_cloud)
|
| 225 |
+
pc_feat_1 = pc_feat['xy'] #
|
| 226 |
+
pc_feat_2 = pc_feat['yz']
|
| 227 |
+
pc_feat_3 = pc_feat['xz']
|
| 228 |
+
else:
|
| 229 |
+
raise NotImplementedError()
|
| 230 |
+
|
| 231 |
+
if self.unet_encoder is not None:
|
| 232 |
+
# TODO eval adding a skip connection
|
| 233 |
+
# Unet expects B, 3, C, H, W
|
| 234 |
+
pc_feat_tri_plane_stack_pre = torch.stack([pc_feat_1, pc_feat_2, pc_feat_3], dim=1)
|
| 235 |
+
# dpc_feat_tri_plane_stack = self.unet_encoder(pc_feat_tri_plane_stack_pre)
|
| 236 |
+
# pc_feat_tri_plane_stack = pc_feat_tri_plane_stack_pre + dpc_feat_tri_plane_stack
|
| 237 |
+
pc_feat_tri_plane_stack = self.unet_encoder(pc_feat_tri_plane_stack_pre)
|
| 238 |
+
pc_feat_1, pc_feat_2, pc_feat_3 = torch.unbind(pc_feat_tri_plane_stack, dim=1)
|
| 239 |
+
|
| 240 |
+
return torch.stack([pc_feat_1, pc_feat_2, pc_feat_3], dim=1)
|
| 241 |
+
|
| 242 |
+
def forward(self, point_cloud_xyz, point_cloud_feature=None, mv_feat=None, pc2pc_idx=None):
|
| 243 |
+
return self.encode(point_cloud_xyz, point_cloud_feature=point_cloud_feature, mv_feat=mv_feat, pc2pc_idx=pc2pc_idx)
|
third_party/PartField/partfield/model/PVCNN/pc_encoder.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
import functools
|
| 5 |
+
|
| 6 |
+
from .pv_module import SharedMLP, PVConv
|
| 7 |
+
|
| 8 |
+
def create_pointnet_components(
|
| 9 |
+
blocks, in_channels, with_se=False, normalize=True, eps=0,
|
| 10 |
+
width_multiplier=1, voxel_resolution_multiplier=1, scale_pvcnn=False, device='cuda'):
|
| 11 |
+
r, vr = width_multiplier, voxel_resolution_multiplier
|
| 12 |
+
layers, concat_channels = [], 0
|
| 13 |
+
for out_channels, num_blocks, voxel_resolution in blocks:
|
| 14 |
+
out_channels = int(r * out_channels)
|
| 15 |
+
if voxel_resolution is None:
|
| 16 |
+
block = functools.partial(SharedMLP, device=device)
|
| 17 |
+
else:
|
| 18 |
+
block = functools.partial(
|
| 19 |
+
PVConv, kernel_size=3, resolution=int(vr * voxel_resolution),
|
| 20 |
+
with_se=with_se, normalize=normalize, eps=eps, scale_pvcnn=scale_pvcnn, device=device)
|
| 21 |
+
for _ in range(num_blocks):
|
| 22 |
+
layers.append(block(in_channels, out_channels))
|
| 23 |
+
in_channels = out_channels
|
| 24 |
+
concat_channels += out_channels
|
| 25 |
+
return layers, in_channels, concat_channels
|
| 26 |
+
|
| 27 |
+
class PCMerger(nn.Module):
|
| 28 |
+
# merge surface sampled PC and rendering backprojected PC (w/ 2D features):
|
| 29 |
+
def __init__(self, in_channels=204, device="cuda"):
|
| 30 |
+
super(PCMerger, self).__init__()
|
| 31 |
+
self.mlp_normal = SharedMLP(3, [128, 128], device=device)
|
| 32 |
+
self.mlp_rgb = SharedMLP(3, [128, 128], device=device)
|
| 33 |
+
self.mlp_sam = SharedMLP(204 - 6, [128, 128], device=device)
|
| 34 |
+
|
| 35 |
+
def forward(self, feat, mv_feat, pc2pc_idx):
|
| 36 |
+
mv_feat_normal = self.mlp_normal(mv_feat[:, :3, :])
|
| 37 |
+
mv_feat_rgb = self.mlp_rgb(mv_feat[:, 3:6, :])
|
| 38 |
+
mv_feat_sam = self.mlp_sam(mv_feat[:, 6:, :])
|
| 39 |
+
|
| 40 |
+
mv_feat_normal = mv_feat_normal.permute(0, 2, 1)
|
| 41 |
+
mv_feat_rgb = mv_feat_rgb.permute(0, 2, 1)
|
| 42 |
+
mv_feat_sam = mv_feat_sam.permute(0, 2, 1)
|
| 43 |
+
feat = feat.permute(0, 2, 1)
|
| 44 |
+
|
| 45 |
+
for i in range(mv_feat.shape[0]):
|
| 46 |
+
mask = (pc2pc_idx[i] != -1).reshape(-1)
|
| 47 |
+
idx = pc2pc_idx[i][mask].reshape(-1)
|
| 48 |
+
feat[i][mask] += mv_feat_normal[i][idx] + mv_feat_rgb[i][idx] + mv_feat_sam[i][idx]
|
| 49 |
+
|
| 50 |
+
return feat.permute(0, 2, 1)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class PVCNNEncoder(nn.Module):
|
| 54 |
+
def __init__(self, pvcnn_feat_dim, device='cuda', in_channels=3, use_2d_feat=False):
|
| 55 |
+
super(PVCNNEncoder, self).__init__()
|
| 56 |
+
self.device = device
|
| 57 |
+
self.blocks = ((pvcnn_feat_dim, 1, 32), (128, 2, 16), (256, 1, 8))
|
| 58 |
+
self.use_2d_feat=use_2d_feat
|
| 59 |
+
if in_channels == 6:
|
| 60 |
+
self.append_channel = 2
|
| 61 |
+
elif in_channels == 3:
|
| 62 |
+
self.append_channel = 1
|
| 63 |
+
else:
|
| 64 |
+
raise NotImplementedError
|
| 65 |
+
layers, channels_point, concat_channels_point = create_pointnet_components(
|
| 66 |
+
blocks=self.blocks, in_channels=in_channels + self.append_channel, with_se=False, normalize=False,
|
| 67 |
+
width_multiplier=1, voxel_resolution_multiplier=1, scale_pvcnn=True,
|
| 68 |
+
device=device
|
| 69 |
+
)
|
| 70 |
+
self.encoder = nn.ModuleList(layers)#.to(self.device)
|
| 71 |
+
if self.use_2d_feat:
|
| 72 |
+
self.merger = PCMerger()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def forward(self, input_pc, mv_feat=None, pc2pc_idx=None):
|
| 77 |
+
features = input_pc.permute(0, 2, 1) * 2 # make point cloud [-1, 1]
|
| 78 |
+
coords = features[:, :3, :]
|
| 79 |
+
out_features_list = []
|
| 80 |
+
voxel_feature_list = []
|
| 81 |
+
zero_padding = torch.zeros(features.shape[0], self.append_channel, features.shape[-1], device=features.device, dtype=torch.float)
|
| 82 |
+
features = torch.cat([features, zero_padding], dim=1)##################
|
| 83 |
+
|
| 84 |
+
for i in range(len(self.encoder)):
|
| 85 |
+
features, _, voxel_feature = self.encoder[i]((features, coords))
|
| 86 |
+
if i == 0 and mv_feat is not None:
|
| 87 |
+
features = self.merger(features, mv_feat.permute(0, 2, 1), pc2pc_idx)
|
| 88 |
+
out_features_list.append(features)
|
| 89 |
+
voxel_feature_list.append(voxel_feature)
|
| 90 |
+
return voxel_feature_list, out_features_list
|
third_party/PartField/partfield/model/PVCNN/pv_module/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .pvconv import PVConv
|
| 2 |
+
from .shared_mlp import SharedMLP
|
third_party/PartField/partfield/model/PVCNN/pv_module/ball_query.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
|
| 4 |
+
from . import functional as F
|
| 5 |
+
|
| 6 |
+
__all__ = ['BallQuery']
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class BallQuery(nn.Module):
|
| 10 |
+
def __init__(self, radius, num_neighbors, include_coordinates=True):
|
| 11 |
+
super().__init__()
|
| 12 |
+
self.radius = radius
|
| 13 |
+
self.num_neighbors = num_neighbors
|
| 14 |
+
self.include_coordinates = include_coordinates
|
| 15 |
+
|
| 16 |
+
def forward(self, points_coords, centers_coords, points_features=None):
|
| 17 |
+
points_coords = points_coords.contiguous()
|
| 18 |
+
centers_coords = centers_coords.contiguous()
|
| 19 |
+
neighbor_indices = F.ball_query(centers_coords, points_coords, self.radius, self.num_neighbors)
|
| 20 |
+
neighbor_coordinates = F.grouping(points_coords, neighbor_indices)
|
| 21 |
+
neighbor_coordinates = neighbor_coordinates - centers_coords.unsqueeze(-1)
|
| 22 |
+
|
| 23 |
+
if points_features is None:
|
| 24 |
+
assert self.include_coordinates, 'No Features For Grouping'
|
| 25 |
+
neighbor_features = neighbor_coordinates
|
| 26 |
+
else:
|
| 27 |
+
neighbor_features = F.grouping(points_features, neighbor_indices)
|
| 28 |
+
if self.include_coordinates:
|
| 29 |
+
neighbor_features = torch.cat([neighbor_coordinates, neighbor_features], dim=1)
|
| 30 |
+
return neighbor_features
|
| 31 |
+
|
| 32 |
+
def extra_repr(self):
|
| 33 |
+
return 'radius={}, num_neighbors={}{}'.format(
|
| 34 |
+
self.radius, self.num_neighbors, ', include coordinates' if self.include_coordinates else '')
|
third_party/PartField/partfield/model/PVCNN/pv_module/frustum.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
|
| 6 |
+
from . import functional as PF
|
| 7 |
+
|
| 8 |
+
__all__ = ['FrustumPointNetLoss', 'get_box_corners_3d']
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class FrustumPointNetLoss(nn.Module):
|
| 12 |
+
def __init__(
|
| 13 |
+
self, num_heading_angle_bins, num_size_templates, size_templates, box_loss_weight=1.0,
|
| 14 |
+
corners_loss_weight=10.0, heading_residual_loss_weight=20.0, size_residual_loss_weight=20.0):
|
| 15 |
+
super().__init__()
|
| 16 |
+
self.box_loss_weight = box_loss_weight
|
| 17 |
+
self.corners_loss_weight = corners_loss_weight
|
| 18 |
+
self.heading_residual_loss_weight = heading_residual_loss_weight
|
| 19 |
+
self.size_residual_loss_weight = size_residual_loss_weight
|
| 20 |
+
|
| 21 |
+
self.num_heading_angle_bins = num_heading_angle_bins
|
| 22 |
+
self.num_size_templates = num_size_templates
|
| 23 |
+
self.register_buffer('size_templates', size_templates.view(self.num_size_templates, 3))
|
| 24 |
+
self.register_buffer(
|
| 25 |
+
'heading_angle_bin_centers', torch.arange(0, 2 * np.pi, 2 * np.pi / self.num_heading_angle_bins)
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
def forward(self, inputs, targets):
|
| 29 |
+
mask_logits = inputs['mask_logits'] # (B, 2, N)
|
| 30 |
+
center_reg = inputs['center_reg'] # (B, 3)
|
| 31 |
+
center = inputs['center'] # (B, 3)
|
| 32 |
+
heading_scores = inputs['heading_scores'] # (B, NH)
|
| 33 |
+
heading_residuals_normalized = inputs['heading_residuals_normalized'] # (B, NH)
|
| 34 |
+
heading_residuals = inputs['heading_residuals'] # (B, NH)
|
| 35 |
+
size_scores = inputs['size_scores'] # (B, NS)
|
| 36 |
+
size_residuals_normalized = inputs['size_residuals_normalized'] # (B, NS, 3)
|
| 37 |
+
size_residuals = inputs['size_residuals'] # (B, NS, 3)
|
| 38 |
+
|
| 39 |
+
mask_logits_target = targets['mask_logits'] # (B, N)
|
| 40 |
+
center_target = targets['center'] # (B, 3)
|
| 41 |
+
heading_bin_id_target = targets['heading_bin_id'] # (B, )
|
| 42 |
+
heading_residual_target = targets['heading_residual'] # (B, )
|
| 43 |
+
size_template_id_target = targets['size_template_id'] # (B, )
|
| 44 |
+
size_residual_target = targets['size_residual'] # (B, 3)
|
| 45 |
+
|
| 46 |
+
batch_size = center.size(0)
|
| 47 |
+
batch_id = torch.arange(batch_size, device=center.device)
|
| 48 |
+
|
| 49 |
+
# Basic Classification and Regression losses
|
| 50 |
+
mask_loss = F.cross_entropy(mask_logits, mask_logits_target)
|
| 51 |
+
heading_loss = F.cross_entropy(heading_scores, heading_bin_id_target)
|
| 52 |
+
size_loss = F.cross_entropy(size_scores, size_template_id_target)
|
| 53 |
+
center_loss = PF.huber_loss(torch.norm(center_target - center, dim=-1), delta=2.0)
|
| 54 |
+
center_reg_loss = PF.huber_loss(torch.norm(center_target - center_reg, dim=-1), delta=1.0)
|
| 55 |
+
|
| 56 |
+
# Refinement losses for size/heading
|
| 57 |
+
heading_residuals_normalized = heading_residuals_normalized[batch_id, heading_bin_id_target] # (B, )
|
| 58 |
+
heading_residual_normalized_target = heading_residual_target / (np.pi / self.num_heading_angle_bins)
|
| 59 |
+
heading_residual_normalized_loss = PF.huber_loss(
|
| 60 |
+
heading_residuals_normalized - heading_residual_normalized_target, delta=1.0
|
| 61 |
+
)
|
| 62 |
+
size_residuals_normalized = size_residuals_normalized[batch_id, size_template_id_target] # (B, 3)
|
| 63 |
+
size_residual_normalized_target = size_residual_target / self.size_templates[size_template_id_target]
|
| 64 |
+
size_residual_normalized_loss = PF.huber_loss(
|
| 65 |
+
torch.norm(size_residual_normalized_target - size_residuals_normalized, dim=-1), delta=1.0
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
# Bounding box losses
|
| 69 |
+
heading = (heading_residuals[batch_id, heading_bin_id_target]
|
| 70 |
+
+ self.heading_angle_bin_centers[heading_bin_id_target]) # (B, )
|
| 71 |
+
# Warning: in origin code, size_residuals are added twice (issue #43 and #49 in charlesq34/frustum-pointnets)
|
| 72 |
+
size = (size_residuals[batch_id, size_template_id_target]
|
| 73 |
+
+ self.size_templates[size_template_id_target]) # (B, 3)
|
| 74 |
+
corners = get_box_corners_3d(centers=center, headings=heading, sizes=size, with_flip=False) # (B, 3, 8)
|
| 75 |
+
heading_target = self.heading_angle_bin_centers[heading_bin_id_target] + heading_residual_target # (B, )
|
| 76 |
+
size_target = self.size_templates[size_template_id_target] + size_residual_target # (B, 3)
|
| 77 |
+
corners_target, corners_target_flip = get_box_corners_3d(
|
| 78 |
+
centers=center_target, headings=heading_target,
|
| 79 |
+
sizes=size_target, with_flip=True) # (B, 3, 8)
|
| 80 |
+
corners_loss = PF.huber_loss(
|
| 81 |
+
torch.min(
|
| 82 |
+
torch.norm(corners - corners_target, dim=1), torch.norm(corners - corners_target_flip, dim=1)
|
| 83 |
+
), delta=1.0)
|
| 84 |
+
# Summing up
|
| 85 |
+
loss = mask_loss + self.box_loss_weight * (
|
| 86 |
+
center_loss + center_reg_loss + heading_loss + size_loss
|
| 87 |
+
+ self.heading_residual_loss_weight * heading_residual_normalized_loss
|
| 88 |
+
+ self.size_residual_loss_weight * size_residual_normalized_loss
|
| 89 |
+
+ self.corners_loss_weight * corners_loss
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
return loss
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def get_box_corners_3d(centers, headings, sizes, with_flip=False):
|
| 96 |
+
"""
|
| 97 |
+
:param centers: coords of box centers, FloatTensor[N, 3]
|
| 98 |
+
:param headings: heading angles, FloatTensor[N, ]
|
| 99 |
+
:param sizes: box sizes, FloatTensor[N, 3]
|
| 100 |
+
:param with_flip: bool, whether to return flipped box (headings + np.pi)
|
| 101 |
+
:return:
|
| 102 |
+
coords of box corners, FloatTensor[N, 3, 8]
|
| 103 |
+
NOTE: corner points are in counter clockwise order, e.g.,
|
| 104 |
+
2--1
|
| 105 |
+
3--0 5
|
| 106 |
+
7--4
|
| 107 |
+
"""
|
| 108 |
+
l = sizes[:, 0] # (N,)
|
| 109 |
+
w = sizes[:, 1] # (N,)
|
| 110 |
+
h = sizes[:, 2] # (N,)
|
| 111 |
+
x_corners = torch.stack([l / 2, l / 2, -l / 2, -l / 2, l / 2, l / 2, -l / 2, -l / 2], dim=1) # (N, 8)
|
| 112 |
+
y_corners = torch.stack([h / 2, h / 2, h / 2, h / 2, -h / 2, -h / 2, -h / 2, -h / 2], dim=1) # (N, 8)
|
| 113 |
+
z_corners = torch.stack([w / 2, -w / 2, -w / 2, w / 2, w / 2, -w / 2, -w / 2, w / 2], dim=1) # (N, 8)
|
| 114 |
+
|
| 115 |
+
c = torch.cos(headings) # (N,)
|
| 116 |
+
s = torch.sin(headings) # (N,)
|
| 117 |
+
o = torch.ones_like(headings) # (N,)
|
| 118 |
+
z = torch.zeros_like(headings) # (N,)
|
| 119 |
+
|
| 120 |
+
centers = centers.unsqueeze(-1) # (B, 3, 1)
|
| 121 |
+
corners = torch.stack([x_corners, y_corners, z_corners], dim=1) # (N, 3, 8)
|
| 122 |
+
R = torch.stack([c, z, s, z, o, z, -s, z, c], dim=1).view(-1, 3, 3) # roty matrix: (N, 3, 3)
|
| 123 |
+
if with_flip:
|
| 124 |
+
R_flip = torch.stack([-c, z, -s, z, o, z, s, z, -c], dim=1).view(-1, 3, 3)
|
| 125 |
+
return torch.matmul(R, corners) + centers, torch.matmul(R_flip, corners) + centers
|
| 126 |
+
else:
|
| 127 |
+
return torch.matmul(R, corners) + centers
|
| 128 |
+
|
| 129 |
+
# centers = centers.unsqueeze(1) # (B, 1, 3)
|
| 130 |
+
# corners = torch.stack([x_corners, y_corners, z_corners], dim=-1) # (N, 8, 3)
|
| 131 |
+
# RT = torch.stack([c, z, -s, z, o, z, s, z, c], dim=1).view(-1, 3, 3) # (N, 3, 3)
|
| 132 |
+
# if with_flip:
|
| 133 |
+
# RT_flip = torch.stack([-c, z, s, z, o, z, -s, z, -c], dim=1).view(-1, 3, 3) # (N, 3, 3)
|
| 134 |
+
# return torch.matmul(corners, RT) + centers, torch.matmul(corners, RT_flip) + centers # (N, 8, 3)
|
| 135 |
+
# else:
|
| 136 |
+
# return torch.matmul(corners, RT) + centers # (N, 8, 3)
|
| 137 |
+
|
| 138 |
+
# corners = torch.stack([x_corners, y_corners, z_corners], dim=1) # (N, 3, 8)
|
| 139 |
+
# R = torch.stack([c, z, s, z, o, z, -s, z, c], dim=1).view(-1, 3, 3) # (N, 3, 3)
|
| 140 |
+
# corners = torch.matmul(R, corners) + centers.unsqueeze(2) # (N, 3, 8)
|
| 141 |
+
# corners = corners.transpose(1, 2) # (N, 8, 3)
|
third_party/PartField/partfield/model/PVCNN/pv_module/functional/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .devoxelization import trilinear_devoxelize
|
third_party/PartField/partfield/model/PVCNN/pv_module/functional/devoxelization.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from torch.autograd import Function
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
|
| 5 |
+
__all__ = ['trilinear_devoxelize']
|
| 6 |
+
|
| 7 |
+
def trilinear_devoxelize(c, coords, r, training=None):
|
| 8 |
+
coords = (coords * 2 + 1.0) / r - 1.0
|
| 9 |
+
coords = coords.permute(0, 2, 1).reshape(c.shape[0], 1, 1, -1, 3)
|
| 10 |
+
f = F.grid_sample(input=c, grid=coords, padding_mode='border', align_corners=False)
|
| 11 |
+
f = f.squeeze(dim=2).squeeze(dim=2)
|
| 12 |
+
return f
|
third_party/PartField/partfield/model/PVCNN/pv_module/loss.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn as nn
|
| 2 |
+
|
| 3 |
+
from . import functional as F
|
| 4 |
+
|
| 5 |
+
__all__ = ['KLLoss']
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class KLLoss(nn.Module):
|
| 9 |
+
def forward(self, x, y):
|
| 10 |
+
return F.kl_loss(x, y)
|
third_party/PartField/partfield/model/PVCNN/pv_module/pointnet.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
|
| 4 |
+
from . import functional as F
|
| 5 |
+
from .ball_query import BallQuery
|
| 6 |
+
from .shared_mlp import SharedMLP
|
| 7 |
+
|
| 8 |
+
__all__ = ['PointNetAModule', 'PointNetSAModule', 'PointNetFPModule']
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class PointNetAModule(nn.Module):
|
| 12 |
+
def __init__(self, in_channels, out_channels, include_coordinates=True):
|
| 13 |
+
super().__init__()
|
| 14 |
+
if not isinstance(out_channels, (list, tuple)):
|
| 15 |
+
out_channels = [[out_channels]]
|
| 16 |
+
elif not isinstance(out_channels[0], (list, tuple)):
|
| 17 |
+
out_channels = [out_channels]
|
| 18 |
+
|
| 19 |
+
mlps = []
|
| 20 |
+
total_out_channels = 0
|
| 21 |
+
for _out_channels in out_channels:
|
| 22 |
+
mlps.append(
|
| 23 |
+
SharedMLP(
|
| 24 |
+
in_channels=in_channels + (3 if include_coordinates else 0),
|
| 25 |
+
out_channels=_out_channels, dim=1)
|
| 26 |
+
)
|
| 27 |
+
total_out_channels += _out_channels[-1]
|
| 28 |
+
|
| 29 |
+
self.include_coordinates = include_coordinates
|
| 30 |
+
self.out_channels = total_out_channels
|
| 31 |
+
self.mlps = nn.ModuleList(mlps)
|
| 32 |
+
|
| 33 |
+
def forward(self, inputs):
|
| 34 |
+
features, coords = inputs
|
| 35 |
+
if self.include_coordinates:
|
| 36 |
+
features = torch.cat([features, coords], dim=1)
|
| 37 |
+
coords = torch.zeros((coords.size(0), 3, 1), device=coords.device)
|
| 38 |
+
if len(self.mlps) > 1:
|
| 39 |
+
features_list = []
|
| 40 |
+
for mlp in self.mlps:
|
| 41 |
+
features_list.append(mlp(features).max(dim=-1, keepdim=True).values)
|
| 42 |
+
return torch.cat(features_list, dim=1), coords
|
| 43 |
+
else:
|
| 44 |
+
return self.mlps[0](features).max(dim=-1, keepdim=True).values, coords
|
| 45 |
+
|
| 46 |
+
def extra_repr(self):
|
| 47 |
+
return f'out_channels={self.out_channels}, include_coordinates={self.include_coordinates}'
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class PointNetSAModule(nn.Module):
|
| 51 |
+
def __init__(self, num_centers, radius, num_neighbors, in_channels, out_channels, include_coordinates=True):
|
| 52 |
+
super().__init__()
|
| 53 |
+
if not isinstance(radius, (list, tuple)):
|
| 54 |
+
radius = [radius]
|
| 55 |
+
if not isinstance(num_neighbors, (list, tuple)):
|
| 56 |
+
num_neighbors = [num_neighbors] * len(radius)
|
| 57 |
+
assert len(radius) == len(num_neighbors)
|
| 58 |
+
if not isinstance(out_channels, (list, tuple)):
|
| 59 |
+
out_channels = [[out_channels]] * len(radius)
|
| 60 |
+
elif not isinstance(out_channels[0], (list, tuple)):
|
| 61 |
+
out_channels = [out_channels] * len(radius)
|
| 62 |
+
assert len(radius) == len(out_channels)
|
| 63 |
+
|
| 64 |
+
groupers, mlps = [], []
|
| 65 |
+
total_out_channels = 0
|
| 66 |
+
for _radius, _out_channels, _num_neighbors in zip(radius, out_channels, num_neighbors):
|
| 67 |
+
groupers.append(
|
| 68 |
+
BallQuery(radius=_radius, num_neighbors=_num_neighbors, include_coordinates=include_coordinates)
|
| 69 |
+
)
|
| 70 |
+
mlps.append(
|
| 71 |
+
SharedMLP(
|
| 72 |
+
in_channels=in_channels + (3 if include_coordinates else 0),
|
| 73 |
+
out_channels=_out_channels, dim=2)
|
| 74 |
+
)
|
| 75 |
+
total_out_channels += _out_channels[-1]
|
| 76 |
+
|
| 77 |
+
self.num_centers = num_centers
|
| 78 |
+
self.out_channels = total_out_channels
|
| 79 |
+
self.groupers = nn.ModuleList(groupers)
|
| 80 |
+
self.mlps = nn.ModuleList(mlps)
|
| 81 |
+
|
| 82 |
+
def forward(self, inputs):
|
| 83 |
+
features, coords = inputs
|
| 84 |
+
centers_coords = F.furthest_point_sample(coords, self.num_centers)
|
| 85 |
+
features_list = []
|
| 86 |
+
for grouper, mlp in zip(self.groupers, self.mlps):
|
| 87 |
+
features_list.append(mlp(grouper(coords, centers_coords, features)).max(dim=-1).values)
|
| 88 |
+
if len(features_list) > 1:
|
| 89 |
+
return torch.cat(features_list, dim=1), centers_coords
|
| 90 |
+
else:
|
| 91 |
+
return features_list[0], centers_coords
|
| 92 |
+
|
| 93 |
+
def extra_repr(self):
|
| 94 |
+
return f'num_centers={self.num_centers}, out_channels={self.out_channels}'
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class PointNetFPModule(nn.Module):
|
| 98 |
+
def __init__(self, in_channels, out_channels):
|
| 99 |
+
super().__init__()
|
| 100 |
+
self.mlp = SharedMLP(in_channels=in_channels, out_channels=out_channels, dim=1)
|
| 101 |
+
|
| 102 |
+
def forward(self, inputs):
|
| 103 |
+
if len(inputs) == 3:
|
| 104 |
+
points_coords, centers_coords, centers_features = inputs
|
| 105 |
+
points_features = None
|
| 106 |
+
else:
|
| 107 |
+
points_coords, centers_coords, centers_features, points_features = inputs
|
| 108 |
+
interpolated_features = F.nearest_neighbor_interpolate(points_coords, centers_coords, centers_features)
|
| 109 |
+
if points_features is not None:
|
| 110 |
+
interpolated_features = torch.cat(
|
| 111 |
+
[interpolated_features, points_features], dim=1
|
| 112 |
+
)
|
| 113 |
+
return self.mlp(interpolated_features), points_coords
|
third_party/PartField/partfield/model/PVCNN/pv_module/pvconv.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn as nn
|
| 2 |
+
|
| 3 |
+
from . import functional as F
|
| 4 |
+
from .voxelization import Voxelization
|
| 5 |
+
from .shared_mlp import SharedMLP
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
__all__ = ['PVConv']
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class PVConv(nn.Module):
|
| 12 |
+
def __init__(
|
| 13 |
+
self, in_channels, out_channels, kernel_size, resolution, with_se=False, normalize=True, eps=0, scale_pvcnn=False,
|
| 14 |
+
device='cuda'):
|
| 15 |
+
super().__init__()
|
| 16 |
+
self.in_channels = in_channels
|
| 17 |
+
self.out_channels = out_channels
|
| 18 |
+
self.kernel_size = kernel_size
|
| 19 |
+
self.resolution = resolution
|
| 20 |
+
self.voxelization = Voxelization(resolution, normalize=normalize, eps=eps, scale_pvcnn=scale_pvcnn)
|
| 21 |
+
voxel_layers = [
|
| 22 |
+
nn.Conv3d(in_channels, out_channels, kernel_size, stride=1, padding=kernel_size // 2, device=device),
|
| 23 |
+
nn.InstanceNorm3d(out_channels, eps=1e-4, device=device),
|
| 24 |
+
nn.LeakyReLU(0.1, True),
|
| 25 |
+
nn.Conv3d(out_channels, out_channels, kernel_size, stride=1, padding=kernel_size // 2, device=device),
|
| 26 |
+
nn.InstanceNorm3d(out_channels, eps=1e-4, device=device),
|
| 27 |
+
nn.LeakyReLU(0.1, True),
|
| 28 |
+
]
|
| 29 |
+
self.voxel_layers = nn.Sequential(*voxel_layers)
|
| 30 |
+
self.point_features = SharedMLP(in_channels, out_channels, device=device)
|
| 31 |
+
|
| 32 |
+
def forward(self, inputs):
|
| 33 |
+
features, coords = inputs
|
| 34 |
+
voxel_features, voxel_coords = self.voxelization(features, coords)
|
| 35 |
+
voxel_features = self.voxel_layers(voxel_features)
|
| 36 |
+
devoxel_features = F.trilinear_devoxelize(voxel_features, voxel_coords, self.resolution, self.training)
|
| 37 |
+
fused_features = devoxel_features + self.point_features(features)
|
| 38 |
+
return fused_features, coords, voxel_features
|
third_party/PartField/partfield/model/PVCNN/pv_module/shared_mlp.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn as nn
|
| 2 |
+
|
| 3 |
+
__all__ = ['SharedMLP']
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class SharedMLP(nn.Module):
|
| 7 |
+
def __init__(self, in_channels, out_channels, dim=1, device='cuda'):
|
| 8 |
+
super().__init__()
|
| 9 |
+
# print('==> SharedMLP device: ', device)
|
| 10 |
+
if dim == 1:
|
| 11 |
+
conv = nn.Conv1d
|
| 12 |
+
bn = nn.InstanceNorm1d
|
| 13 |
+
elif dim == 2:
|
| 14 |
+
conv = nn.Conv2d
|
| 15 |
+
bn = nn.InstanceNorm1d
|
| 16 |
+
else:
|
| 17 |
+
raise ValueError
|
| 18 |
+
if not isinstance(out_channels, (list, tuple)):
|
| 19 |
+
out_channels = [out_channels]
|
| 20 |
+
layers = []
|
| 21 |
+
for oc in out_channels:
|
| 22 |
+
layers.extend(
|
| 23 |
+
[
|
| 24 |
+
conv(in_channels, oc, 1, device=device),
|
| 25 |
+
bn(oc, device=device),
|
| 26 |
+
nn.ReLU(True),
|
| 27 |
+
])
|
| 28 |
+
in_channels = oc
|
| 29 |
+
self.layers = nn.Sequential(*layers)
|
| 30 |
+
|
| 31 |
+
def forward(self, inputs):
|
| 32 |
+
if isinstance(inputs, (list, tuple)):
|
| 33 |
+
return (self.layers(inputs[0]), *inputs[1:])
|
| 34 |
+
else:
|
| 35 |
+
return self.layers(inputs)
|
third_party/PartField/partfield/model/PVCNN/pv_module/voxelization.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
|
| 4 |
+
from . import functional as F
|
| 5 |
+
|
| 6 |
+
__all__ = ['Voxelization']
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def my_voxelization(features, coords, resolution):
|
| 10 |
+
b, c, _ = features.shape
|
| 11 |
+
result = torch.zeros(b, c + 1, resolution * resolution * resolution, device=features.device, dtype=torch.float)
|
| 12 |
+
r = resolution
|
| 13 |
+
r2 = resolution * resolution
|
| 14 |
+
indices = coords[:, 0] * r2 + coords[:, 1] * r + coords[:, 2]
|
| 15 |
+
indices = indices.unsqueeze(dim=1).expand(-1, result.shape[1], -1)
|
| 16 |
+
features = torch.cat([features, torch.ones(features.shape[0], 1, features.shape[2], device=features.device, dtype=features.dtype)], dim=1)
|
| 17 |
+
out_feature = result.scatter_(index=indices.long(), src=features, dim=2, reduce='add')
|
| 18 |
+
cnt = out_feature[:, -1:, :]
|
| 19 |
+
zero_mask = (cnt == 0).float()
|
| 20 |
+
cnt = cnt * (1 - zero_mask) + zero_mask * 1e-5
|
| 21 |
+
vox_feature = out_feature[:, :-1, :] / cnt
|
| 22 |
+
return vox_feature.view(b, c, resolution, resolution, resolution)
|
| 23 |
+
|
| 24 |
+
class Voxelization(nn.Module):
|
| 25 |
+
def __init__(self, resolution, normalize=True, eps=0, scale_pvcnn=False):
|
| 26 |
+
super().__init__()
|
| 27 |
+
self.r = int(resolution)
|
| 28 |
+
self.normalize = normalize
|
| 29 |
+
self.eps = eps
|
| 30 |
+
self.scale_pvcnn = scale_pvcnn
|
| 31 |
+
assert not normalize
|
| 32 |
+
|
| 33 |
+
def forward(self, features, coords):
|
| 34 |
+
with torch.no_grad():
|
| 35 |
+
coords = coords.detach()
|
| 36 |
+
|
| 37 |
+
if self.normalize:
|
| 38 |
+
norm_coords = norm_coords / (norm_coords.norm(dim=1, keepdim=True).max(dim=2, keepdim=True).values * 2.0 + self.eps) + 0.5
|
| 39 |
+
else:
|
| 40 |
+
if self.scale_pvcnn:
|
| 41 |
+
norm_coords = (coords + 1) / 2.0 # [0, 1]
|
| 42 |
+
else:
|
| 43 |
+
norm_coords = (norm_coords + 1) / 2.0
|
| 44 |
+
norm_coords = torch.clamp(norm_coords * self.r, 0, self.r - 1)
|
| 45 |
+
vox_coords = torch.round(norm_coords)
|
| 46 |
+
new_vox_feat = my_voxelization(features, vox_coords, self.r)
|
| 47 |
+
return new_vox_feat, norm_coords
|
| 48 |
+
|
| 49 |
+
def extra_repr(self):
|
| 50 |
+
return 'resolution={}{}'.format(self.r, ', normalized eps = {}'.format(self.eps) if self.normalize else '')
|
third_party/PartField/partfield/model/PVCNN/unet_3daware.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
from torch.nn import init
|
| 7 |
+
|
| 8 |
+
import einops
|
| 9 |
+
|
| 10 |
+
def conv3x3(in_channels, out_channels, stride=1,
|
| 11 |
+
padding=1, bias=True, groups=1):
|
| 12 |
+
return nn.Conv2d(
|
| 13 |
+
in_channels,
|
| 14 |
+
out_channels,
|
| 15 |
+
kernel_size=3,
|
| 16 |
+
stride=stride,
|
| 17 |
+
padding=padding,
|
| 18 |
+
bias=bias,
|
| 19 |
+
groups=groups)
|
| 20 |
+
|
| 21 |
+
def upconv2x2(in_channels, out_channels, mode='transpose'):
|
| 22 |
+
if mode == 'transpose':
|
| 23 |
+
return nn.ConvTranspose2d(
|
| 24 |
+
in_channels,
|
| 25 |
+
out_channels,
|
| 26 |
+
kernel_size=2,
|
| 27 |
+
stride=2)
|
| 28 |
+
else:
|
| 29 |
+
# out_channels is always going to be the same
|
| 30 |
+
# as in_channels
|
| 31 |
+
return nn.Sequential(
|
| 32 |
+
nn.Upsample(mode='bilinear', scale_factor=2),
|
| 33 |
+
conv1x1(in_channels, out_channels))
|
| 34 |
+
|
| 35 |
+
def conv1x1(in_channels, out_channels, groups=1):
|
| 36 |
+
return nn.Conv2d(
|
| 37 |
+
in_channels,
|
| 38 |
+
out_channels,
|
| 39 |
+
kernel_size=1,
|
| 40 |
+
groups=groups,
|
| 41 |
+
stride=1)
|
| 42 |
+
|
| 43 |
+
class ConvTriplane3dAware(nn.Module):
|
| 44 |
+
""" 3D aware triplane conv (as described in RODIN) """
|
| 45 |
+
def __init__(self, internal_conv_f, in_channels, out_channels, order='xz'):
|
| 46 |
+
"""
|
| 47 |
+
Args:
|
| 48 |
+
internal_conv_f: function that should return a 2D convolution Module
|
| 49 |
+
given in and out channels
|
| 50 |
+
order: if triplane input is in 'xz' order
|
| 51 |
+
"""
|
| 52 |
+
super(ConvTriplane3dAware, self).__init__()
|
| 53 |
+
# Need 3 seperate convolutions
|
| 54 |
+
self.in_channels = in_channels
|
| 55 |
+
self.out_channels = out_channels
|
| 56 |
+
assert order in ['xz', 'zx']
|
| 57 |
+
self.order = order
|
| 58 |
+
# Going to stack from other planes
|
| 59 |
+
self.plane_convs = nn.ModuleList([
|
| 60 |
+
internal_conv_f(3*self.in_channels, self.out_channels) for _ in range(3)])
|
| 61 |
+
|
| 62 |
+
def forward(self, triplanes_list):
|
| 63 |
+
"""
|
| 64 |
+
Args:
|
| 65 |
+
triplanes_list: [(B,Ci,H,W)]*3 in xy,yz,(zx or xz) depending on order
|
| 66 |
+
Returns:
|
| 67 |
+
out_triplanes_list: [(B,Co,H,W)]*3 in xy,yz,(zx or xz) depending on order
|
| 68 |
+
"""
|
| 69 |
+
inps = list(triplanes_list)
|
| 70 |
+
xp = 1 #(yz)
|
| 71 |
+
yp = 2 #(zx)
|
| 72 |
+
zp = 0 #(xy)
|
| 73 |
+
|
| 74 |
+
if self.order == 'xz':
|
| 75 |
+
# get into zx order
|
| 76 |
+
inps[yp] = einops.rearrange(inps[yp], 'b c x z -> b c z x')
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
oplanes = [None]*3
|
| 80 |
+
# order shouldn't matter
|
| 81 |
+
for iplane in [zp, xp, yp]:
|
| 82 |
+
# i_plane -> (j,k)
|
| 83 |
+
|
| 84 |
+
# need to average out i and convert to (j,k)
|
| 85 |
+
# j_plane -> (k,i)
|
| 86 |
+
# k_plane -> (i,j)
|
| 87 |
+
jplane = (iplane+1)%3
|
| 88 |
+
kplane = (iplane+2)%3
|
| 89 |
+
|
| 90 |
+
ifeat = inps[iplane]
|
| 91 |
+
# need to average out nonshared dim
|
| 92 |
+
# Average pool across
|
| 93 |
+
|
| 94 |
+
# j_plane -> (k,i) -> (k,1) -> (1,k) -> (j,k)
|
| 95 |
+
# b c k i -> b c k 1
|
| 96 |
+
jpool = torch.mean(inps[jplane], dim=3 ,keepdim=True)
|
| 97 |
+
jpool = einops.rearrange(jpool, 'b c k 1 -> b c 1 k')
|
| 98 |
+
jpool = einops.repeat(jpool, 'b c 1 k -> b c j k', j=ifeat.size(2))
|
| 99 |
+
|
| 100 |
+
# k_plane -> (i,j) -> (1,j) -> (j,1) -> (j,k)
|
| 101 |
+
# b c i j -> b c 1 j
|
| 102 |
+
kpool = torch.mean(inps[kplane], dim=2 ,keepdim=True)
|
| 103 |
+
kpool = einops.rearrange(kpool, 'b c 1 j -> b c j 1')
|
| 104 |
+
kpool = einops.repeat(kpool, 'b c j 1 -> b c j k', k=ifeat.size(3))
|
| 105 |
+
|
| 106 |
+
# b c h w
|
| 107 |
+
# jpool = jpool.expand_as(ifeat)
|
| 108 |
+
# kpool = kpool.expand_as(ifeat)
|
| 109 |
+
|
| 110 |
+
# concat and conv on feature dim
|
| 111 |
+
catfeat = torch.cat([ifeat, jpool, kpool], dim=1)
|
| 112 |
+
oplane = self.plane_convs[iplane](catfeat)
|
| 113 |
+
oplanes[iplane] = oplane
|
| 114 |
+
|
| 115 |
+
if self.order == 'xz':
|
| 116 |
+
# get back into xz order
|
| 117 |
+
oplanes[yp] = einops.rearrange(oplanes[yp], 'b c z x -> b c x z')
|
| 118 |
+
|
| 119 |
+
return oplanes
|
| 120 |
+
|
| 121 |
+
def roll_triplanes(triplanes_list):
|
| 122 |
+
# B, C, tri, h, w
|
| 123 |
+
tristack = torch.stack((triplanes_list),dim=2)
|
| 124 |
+
return einops.rearrange(tristack, 'b c tri h w -> b c (tri h) w', tri=3)
|
| 125 |
+
|
| 126 |
+
def unroll_triplanes(rolled_triplane):
|
| 127 |
+
# B, C, tri*h, w
|
| 128 |
+
tristack = einops.rearrange(rolled_triplane, 'b c (tri h) w -> b c tri h w', tri=3)
|
| 129 |
+
return torch.unbind(tristack, dim=2)
|
| 130 |
+
|
| 131 |
+
def conv1x1triplane3daware(in_channels, out_channels, order='xz', **kwargs):
|
| 132 |
+
return ConvTriplane3dAware(lambda inp, out: conv1x1(inp,out,**kwargs),
|
| 133 |
+
in_channels, out_channels,order=order)
|
| 134 |
+
|
| 135 |
+
def Normalize(in_channels, num_groups=32):
|
| 136 |
+
num_groups = min(in_channels, num_groups) # avoid error if in_channels < 32
|
| 137 |
+
return torch.nn.GroupNorm(num_groups=num_groups, num_channels=in_channels, eps=1e-6, affine=True)
|
| 138 |
+
|
| 139 |
+
def nonlinearity(x):
|
| 140 |
+
# return F.relu(x)
|
| 141 |
+
# Swish
|
| 142 |
+
return x*torch.sigmoid(x)
|
| 143 |
+
|
| 144 |
+
class Upsample(nn.Module):
|
| 145 |
+
def __init__(self, in_channels, with_conv):
|
| 146 |
+
super().__init__()
|
| 147 |
+
self.with_conv = with_conv
|
| 148 |
+
if self.with_conv:
|
| 149 |
+
self.conv = torch.nn.Conv2d(in_channels,
|
| 150 |
+
in_channels,
|
| 151 |
+
kernel_size=3,
|
| 152 |
+
stride=1,
|
| 153 |
+
padding=1)
|
| 154 |
+
|
| 155 |
+
def forward(self, x):
|
| 156 |
+
x = torch.nn.functional.interpolate(x, scale_factor=2.0, mode="nearest")
|
| 157 |
+
if self.with_conv:
|
| 158 |
+
x = self.conv(x)
|
| 159 |
+
return x
|
| 160 |
+
|
| 161 |
+
class Downsample(nn.Module):
|
| 162 |
+
def __init__(self, in_channels, with_conv):
|
| 163 |
+
super().__init__()
|
| 164 |
+
self.with_conv = with_conv
|
| 165 |
+
if self.with_conv:
|
| 166 |
+
# no asymmetric padding in torch conv, must do it ourselves
|
| 167 |
+
self.conv = torch.nn.Conv2d(in_channels,
|
| 168 |
+
in_channels,
|
| 169 |
+
kernel_size=3,
|
| 170 |
+
stride=2,
|
| 171 |
+
padding=0)
|
| 172 |
+
|
| 173 |
+
def forward(self, x):
|
| 174 |
+
if self.with_conv:
|
| 175 |
+
pad = (0,1,0,1)
|
| 176 |
+
x = torch.nn.functional.pad(x, pad, mode="constant", value=0)
|
| 177 |
+
x = self.conv(x)
|
| 178 |
+
else:
|
| 179 |
+
x = torch.nn.functional.avg_pool2d(x, kernel_size=2, stride=2)
|
| 180 |
+
return x
|
| 181 |
+
|
| 182 |
+
class ResnetBlock3dAware(nn.Module):
|
| 183 |
+
def __init__(self, in_channels, out_channels=None):
|
| 184 |
+
#, conv_shortcut=False):
|
| 185 |
+
super().__init__()
|
| 186 |
+
self.in_channels = in_channels
|
| 187 |
+
out_channels = in_channels if out_channels is None else out_channels
|
| 188 |
+
self.out_channels = out_channels
|
| 189 |
+
# self.use_conv_shortcut = conv_shortcut
|
| 190 |
+
|
| 191 |
+
self.norm1 = Normalize(in_channels)
|
| 192 |
+
self.conv1 = conv3x3(self.in_channels, self.out_channels)
|
| 193 |
+
|
| 194 |
+
self.norm_mid = Normalize(out_channels)
|
| 195 |
+
self.conv_3daware = conv1x1triplane3daware(self.out_channels, self.out_channels)
|
| 196 |
+
|
| 197 |
+
self.norm2 = Normalize(out_channels)
|
| 198 |
+
self.conv2 = conv3x3(self.out_channels, self.out_channels)
|
| 199 |
+
|
| 200 |
+
if self.in_channels != self.out_channels:
|
| 201 |
+
self.nin_shortcut = torch.nn.Conv2d(in_channels,
|
| 202 |
+
out_channels,
|
| 203 |
+
kernel_size=1,
|
| 204 |
+
stride=1,
|
| 205 |
+
padding=0)
|
| 206 |
+
|
| 207 |
+
def forward(self, x):
|
| 208 |
+
# 3x3 plane comm
|
| 209 |
+
h = x
|
| 210 |
+
h = self.norm1(h)
|
| 211 |
+
h = nonlinearity(h)
|
| 212 |
+
h = self.conv1(h)
|
| 213 |
+
|
| 214 |
+
# 1x1 3d aware, crossplane comm
|
| 215 |
+
h = self.norm_mid(h)
|
| 216 |
+
h = nonlinearity(h)
|
| 217 |
+
h = unroll_triplanes(h)
|
| 218 |
+
h = self.conv_3daware(h)
|
| 219 |
+
h = roll_triplanes(h)
|
| 220 |
+
|
| 221 |
+
# 3x3 plane comm
|
| 222 |
+
h = self.norm2(h)
|
| 223 |
+
h = nonlinearity(h)
|
| 224 |
+
h = self.conv2(h)
|
| 225 |
+
|
| 226 |
+
if self.in_channels != self.out_channels:
|
| 227 |
+
x = self.nin_shortcut(x)
|
| 228 |
+
|
| 229 |
+
return x+h
|
| 230 |
+
|
| 231 |
+
class DownConv3dAware(nn.Module):
|
| 232 |
+
"""
|
| 233 |
+
A helper Module that performs 2 convolutions and 1 MaxPool.
|
| 234 |
+
A ReLU activation follows each convolution.
|
| 235 |
+
"""
|
| 236 |
+
def __init__(self, in_channels, out_channels, downsample=True, with_conv=False):
|
| 237 |
+
super(DownConv3dAware, self).__init__()
|
| 238 |
+
|
| 239 |
+
self.in_channels = in_channels
|
| 240 |
+
self.out_channels = out_channels
|
| 241 |
+
|
| 242 |
+
self.block = ResnetBlock3dAware(in_channels=in_channels,
|
| 243 |
+
out_channels=out_channels)
|
| 244 |
+
|
| 245 |
+
self.do_downsample = downsample
|
| 246 |
+
self.downsample = Downsample(out_channels, with_conv=with_conv)
|
| 247 |
+
|
| 248 |
+
def forward(self, x):
|
| 249 |
+
"""
|
| 250 |
+
rolled input, rolled output
|
| 251 |
+
Args:
|
| 252 |
+
x: rolled (b c (tri*h) w)
|
| 253 |
+
"""
|
| 254 |
+
x = self.block(x)
|
| 255 |
+
before_pool = x
|
| 256 |
+
# if self.pooling:
|
| 257 |
+
# x = self.pool(x)
|
| 258 |
+
if self.do_downsample:
|
| 259 |
+
# unroll and cat channel-wise (to prevent pooling across triplane boundaries)
|
| 260 |
+
x = einops.rearrange(x, 'b c (tri h) w -> b (c tri) h w', tri=3)
|
| 261 |
+
x = self.downsample(x)
|
| 262 |
+
# undo
|
| 263 |
+
x = einops.rearrange(x, 'b (c tri) h w -> b c (tri h) w', tri=3)
|
| 264 |
+
return x, before_pool
|
| 265 |
+
|
| 266 |
+
class UpConv3dAware(nn.Module):
|
| 267 |
+
"""
|
| 268 |
+
A helper Module that performs 2 convolutions and 1 UpConvolution.
|
| 269 |
+
A ReLU activation follows each convolution.
|
| 270 |
+
"""
|
| 271 |
+
def __init__(self, in_channels, out_channels,
|
| 272 |
+
merge_mode='concat', with_conv=False): #up_mode='transpose', ):
|
| 273 |
+
super(UpConv3dAware, self).__init__()
|
| 274 |
+
|
| 275 |
+
self.in_channels = in_channels
|
| 276 |
+
self.out_channels = out_channels
|
| 277 |
+
self.merge_mode = merge_mode
|
| 278 |
+
|
| 279 |
+
self.upsample = Upsample(in_channels, with_conv)
|
| 280 |
+
|
| 281 |
+
if self.merge_mode == 'concat':
|
| 282 |
+
self.norm1 = Normalize(in_channels+out_channels)
|
| 283 |
+
self.block = ResnetBlock3dAware(in_channels=in_channels+out_channels,
|
| 284 |
+
out_channels=out_channels)
|
| 285 |
+
else:
|
| 286 |
+
self.norm1 = Normalize(in_channels)
|
| 287 |
+
self.block = ResnetBlock3dAware(in_channels=in_channels,
|
| 288 |
+
out_channels=out_channels)
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
def forward(self, from_down, from_up):
|
| 292 |
+
""" Forward pass
|
| 293 |
+
rolled inputs, rolled output
|
| 294 |
+
rolled (b c (tri*h) w)
|
| 295 |
+
Arguments:
|
| 296 |
+
from_down: tensor from the encoder pathway
|
| 297 |
+
from_up: upconv'd tensor from the decoder pathway
|
| 298 |
+
"""
|
| 299 |
+
# from_up = self.upconv(from_up)
|
| 300 |
+
from_up = self.upsample(from_up)
|
| 301 |
+
if self.merge_mode == 'concat':
|
| 302 |
+
x = torch.cat((from_up, from_down), 1)
|
| 303 |
+
else:
|
| 304 |
+
x = from_up + from_down
|
| 305 |
+
|
| 306 |
+
x = self.norm1(x)
|
| 307 |
+
x = self.block(x)
|
| 308 |
+
return x
|
| 309 |
+
|
| 310 |
+
class UNetTriplane3dAware(nn.Module):
|
| 311 |
+
def __init__(self, out_channels, in_channels=3, depth=5,
|
| 312 |
+
start_filts=64,# up_mode='transpose',
|
| 313 |
+
use_initial_conv=False,
|
| 314 |
+
merge_mode='concat', **kwargs):
|
| 315 |
+
"""
|
| 316 |
+
Arguments:
|
| 317 |
+
in_channels: int, number of channels in the input tensor.
|
| 318 |
+
Default is 3 for RGB images.
|
| 319 |
+
depth: int, number of MaxPools in the U-Net.
|
| 320 |
+
start_filts: int, number of convolutional filters for the
|
| 321 |
+
first conv.
|
| 322 |
+
"""
|
| 323 |
+
super(UNetTriplane3dAware, self).__init__()
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
self.out_channels = out_channels
|
| 327 |
+
self.in_channels = in_channels
|
| 328 |
+
self.start_filts = start_filts
|
| 329 |
+
self.depth = depth
|
| 330 |
+
|
| 331 |
+
self.use_initial_conv = use_initial_conv
|
| 332 |
+
if use_initial_conv:
|
| 333 |
+
self.conv_initial = conv1x1(self.in_channels, self.start_filts)
|
| 334 |
+
|
| 335 |
+
self.down_convs = []
|
| 336 |
+
self.up_convs = []
|
| 337 |
+
|
| 338 |
+
# create the encoder pathway and add to a list
|
| 339 |
+
for i in range(depth):
|
| 340 |
+
if i == 0:
|
| 341 |
+
ins = self.start_filts if use_initial_conv else self.in_channels
|
| 342 |
+
else:
|
| 343 |
+
ins = outs
|
| 344 |
+
outs = self.start_filts*(2**i)
|
| 345 |
+
downsamp_it = True if i < depth-1 else False
|
| 346 |
+
|
| 347 |
+
down_conv = DownConv3dAware(ins, outs, downsample = downsamp_it)
|
| 348 |
+
self.down_convs.append(down_conv)
|
| 349 |
+
|
| 350 |
+
for i in range(depth-1):
|
| 351 |
+
ins = outs
|
| 352 |
+
outs = ins // 2
|
| 353 |
+
up_conv = UpConv3dAware(ins, outs,
|
| 354 |
+
merge_mode=merge_mode)
|
| 355 |
+
self.up_convs.append(up_conv)
|
| 356 |
+
|
| 357 |
+
# add the list of modules to current module
|
| 358 |
+
self.down_convs = nn.ModuleList(self.down_convs)
|
| 359 |
+
self.up_convs = nn.ModuleList(self.up_convs)
|
| 360 |
+
|
| 361 |
+
self.norm_out = Normalize(outs)
|
| 362 |
+
self.conv_final = conv1x1(outs, self.out_channels)
|
| 363 |
+
|
| 364 |
+
self.reset_params()
|
| 365 |
+
|
| 366 |
+
@staticmethod
|
| 367 |
+
def weight_init(m):
|
| 368 |
+
if isinstance(m, nn.Conv2d):
|
| 369 |
+
# init.xavier_normal_(m.weight, gain=0.1)
|
| 370 |
+
init.xavier_normal_(m.weight)
|
| 371 |
+
init.constant_(m.bias, 0)
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
def reset_params(self):
|
| 375 |
+
for i, m in enumerate(self.modules()):
|
| 376 |
+
self.weight_init(m)
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
def forward(self, x):
|
| 380 |
+
"""
|
| 381 |
+
Args:
|
| 382 |
+
x: Stacked triplane expected to be in (B,3,C,H,W)
|
| 383 |
+
"""
|
| 384 |
+
# Roll
|
| 385 |
+
x = einops.rearrange(x, 'b tri c h w -> b c (tri h) w', tri=3)
|
| 386 |
+
|
| 387 |
+
if self.use_initial_conv:
|
| 388 |
+
x = self.conv_initial(x)
|
| 389 |
+
|
| 390 |
+
encoder_outs = []
|
| 391 |
+
# encoder pathway, save outputs for merging
|
| 392 |
+
for i, module in enumerate(self.down_convs):
|
| 393 |
+
x, before_pool = module(x)
|
| 394 |
+
encoder_outs.append(before_pool)
|
| 395 |
+
|
| 396 |
+
# Spend a block in the middle
|
| 397 |
+
# x = self.block_mid(x)
|
| 398 |
+
|
| 399 |
+
for i, module in enumerate(self.up_convs):
|
| 400 |
+
before_pool = encoder_outs[-(i+2)]
|
| 401 |
+
x = module(before_pool, x)
|
| 402 |
+
|
| 403 |
+
x = self.norm_out(x)
|
| 404 |
+
|
| 405 |
+
# No softmax is used. This means you need to use
|
| 406 |
+
# nn.CrossEntropyLoss is your training script,
|
| 407 |
+
# as this module includes a softmax already.
|
| 408 |
+
x = self.conv_final(nonlinearity(x))
|
| 409 |
+
|
| 410 |
+
# Unroll
|
| 411 |
+
x = einops.rearrange(x, 'b c (tri h) w -> b tri c h w', tri=3)
|
| 412 |
+
return x
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
def setup_unet(output_channels, input_channels, unet_cfg):
|
| 416 |
+
if unet_cfg['use_3d_aware']:
|
| 417 |
+
assert(unet_cfg['rolled'])
|
| 418 |
+
unet = UNetTriplane3dAware(
|
| 419 |
+
out_channels=output_channels,
|
| 420 |
+
in_channels=input_channels,
|
| 421 |
+
depth=unet_cfg['depth'],
|
| 422 |
+
use_initial_conv=unet_cfg['use_initial_conv'],
|
| 423 |
+
start_filts=unet_cfg['start_hidden_channels'],)
|
| 424 |
+
else:
|
| 425 |
+
raise NotImplementedError
|
| 426 |
+
return unet
|
| 427 |
+
|
third_party/PartField/partfield/model/UNet/buildingblocks.py
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#https://github.com/wolny/pytorch-3dunet/blob/master/pytorch3dunet/unet3d/buildingblocks.py
|
| 2 |
+
# MIT License
|
| 3 |
+
|
| 4 |
+
# Copyright (c) 2018 Adrian Wolny
|
| 5 |
+
|
| 6 |
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 7 |
+
# of this software and associated documentation files (the "Software"), to deal
|
| 8 |
+
# in the Software without restriction, including without limitation the rights
|
| 9 |
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 10 |
+
# copies of the Software, and to permit persons to whom the Software is
|
| 11 |
+
# furnished to do so, subject to the following conditions:
|
| 12 |
+
|
| 13 |
+
# The above copyright notice and this permission notice shall be included in all
|
| 14 |
+
# copies or substantial portions of the Software.
|
| 15 |
+
|
| 16 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 17 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 18 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 19 |
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 20 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 21 |
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 22 |
+
# SOFTWARE.
|
| 23 |
+
|
| 24 |
+
from functools import partial
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
from torch import nn as nn
|
| 28 |
+
from torch.nn import functional as F
|
| 29 |
+
|
| 30 |
+
# from pytorch3dunet.unet3d.se import ChannelSELayer3D, ChannelSpatialSELayer3D, SpatialSELayer3D
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def create_conv(in_channels, out_channels, kernel_size, order, num_groups, padding,
|
| 34 |
+
dropout_prob, is3d):
|
| 35 |
+
"""
|
| 36 |
+
Create a list of modules with together constitute a single conv layer with non-linearity
|
| 37 |
+
and optional batchnorm/groupnorm.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
in_channels (int): number of input channels
|
| 41 |
+
out_channels (int): number of output channels
|
| 42 |
+
kernel_size(int or tuple): size of the convolving kernel
|
| 43 |
+
order (string): order of things, e.g.
|
| 44 |
+
'cr' -> conv + ReLU
|
| 45 |
+
'gcr' -> groupnorm + conv + ReLU
|
| 46 |
+
'cl' -> conv + LeakyReLU
|
| 47 |
+
'ce' -> conv + ELU
|
| 48 |
+
'bcr' -> batchnorm + conv + ReLU
|
| 49 |
+
'cbrd' -> conv + batchnorm + ReLU + dropout
|
| 50 |
+
'cbrD' -> conv + batchnorm + ReLU + dropout2d
|
| 51 |
+
num_groups (int): number of groups for the GroupNorm
|
| 52 |
+
padding (int or tuple): add zero-padding added to all three sides of the input
|
| 53 |
+
dropout_prob (float): dropout probability
|
| 54 |
+
is3d (bool): is3d (bool): if True use Conv3d, otherwise use Conv2d
|
| 55 |
+
Return:
|
| 56 |
+
list of tuple (name, module)
|
| 57 |
+
"""
|
| 58 |
+
assert 'c' in order, "Conv layer MUST be present"
|
| 59 |
+
assert order[0] not in 'rle', 'Non-linearity cannot be the first operation in the layer'
|
| 60 |
+
|
| 61 |
+
modules = []
|
| 62 |
+
for i, char in enumerate(order):
|
| 63 |
+
if char == 'r':
|
| 64 |
+
modules.append(('ReLU', nn.ReLU(inplace=True)))
|
| 65 |
+
elif char == 'l':
|
| 66 |
+
modules.append(('LeakyReLU', nn.LeakyReLU(inplace=True)))
|
| 67 |
+
elif char == 'e':
|
| 68 |
+
modules.append(('ELU', nn.ELU(inplace=True)))
|
| 69 |
+
elif char == 'c':
|
| 70 |
+
# add learnable bias only in the absence of batchnorm/groupnorm
|
| 71 |
+
bias = not ('g' in order or 'b' in order)
|
| 72 |
+
if is3d:
|
| 73 |
+
conv = nn.Conv3d(in_channels, out_channels, kernel_size, padding=padding, bias=bias)
|
| 74 |
+
else:
|
| 75 |
+
conv = nn.Conv2d(in_channels, out_channels, kernel_size, padding=padding, bias=bias)
|
| 76 |
+
|
| 77 |
+
modules.append(('conv', conv))
|
| 78 |
+
elif char == 'g':
|
| 79 |
+
is_before_conv = i < order.index('c')
|
| 80 |
+
if is_before_conv:
|
| 81 |
+
num_channels = in_channels
|
| 82 |
+
else:
|
| 83 |
+
num_channels = out_channels
|
| 84 |
+
|
| 85 |
+
# use only one group if the given number of groups is greater than the number of channels
|
| 86 |
+
if num_channels < num_groups:
|
| 87 |
+
num_groups = 1
|
| 88 |
+
|
| 89 |
+
assert num_channels % num_groups == 0, f'Expected number of channels in input to be divisible by num_groups. num_channels={num_channels}, num_groups={num_groups}'
|
| 90 |
+
modules.append(('groupnorm', nn.GroupNorm(num_groups=num_groups, num_channels=num_channels)))
|
| 91 |
+
elif char == 'b':
|
| 92 |
+
is_before_conv = i < order.index('c')
|
| 93 |
+
if is3d:
|
| 94 |
+
bn = nn.BatchNorm3d
|
| 95 |
+
else:
|
| 96 |
+
bn = nn.BatchNorm2d
|
| 97 |
+
|
| 98 |
+
if is_before_conv:
|
| 99 |
+
modules.append(('batchnorm', bn(in_channels)))
|
| 100 |
+
else:
|
| 101 |
+
modules.append(('batchnorm', bn(out_channels)))
|
| 102 |
+
elif char == 'd':
|
| 103 |
+
modules.append(('dropout', nn.Dropout(p=dropout_prob)))
|
| 104 |
+
elif char == 'D':
|
| 105 |
+
modules.append(('dropout2d', nn.Dropout2d(p=dropout_prob)))
|
| 106 |
+
else:
|
| 107 |
+
raise ValueError(f"Unsupported layer type '{char}'. MUST be one of ['b', 'g', 'r', 'l', 'e', 'c', 'd', 'D']")
|
| 108 |
+
|
| 109 |
+
return modules
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class SingleConv(nn.Sequential):
|
| 113 |
+
"""
|
| 114 |
+
Basic convolutional module consisting of a Conv3d, non-linearity and optional batchnorm/groupnorm. The order
|
| 115 |
+
of operations can be specified via the `order` parameter
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
in_channels (int): number of input channels
|
| 119 |
+
out_channels (int): number of output channels
|
| 120 |
+
kernel_size (int or tuple): size of the convolving kernel
|
| 121 |
+
order (string): determines the order of layers, e.g.
|
| 122 |
+
'cr' -> conv + ReLU
|
| 123 |
+
'crg' -> conv + ReLU + groupnorm
|
| 124 |
+
'cl' -> conv + LeakyReLU
|
| 125 |
+
'ce' -> conv + ELU
|
| 126 |
+
num_groups (int): number of groups for the GroupNorm
|
| 127 |
+
padding (int or tuple): add zero-padding
|
| 128 |
+
dropout_prob (float): dropout probability, default 0.1
|
| 129 |
+
is3d (bool): if True use Conv3d, otherwise use Conv2d
|
| 130 |
+
"""
|
| 131 |
+
|
| 132 |
+
def __init__(self, in_channels, out_channels, kernel_size=3, order='gcr', num_groups=8,
|
| 133 |
+
padding=1, dropout_prob=0.1, is3d=True):
|
| 134 |
+
super(SingleConv, self).__init__()
|
| 135 |
+
|
| 136 |
+
for name, module in create_conv(in_channels, out_channels, kernel_size, order,
|
| 137 |
+
num_groups, padding, dropout_prob, is3d):
|
| 138 |
+
self.add_module(name, module)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class DoubleConv(nn.Sequential):
|
| 142 |
+
"""
|
| 143 |
+
A module consisting of two consecutive convolution layers (e.g. BatchNorm3d+ReLU+Conv3d).
|
| 144 |
+
We use (Conv3d+ReLU+GroupNorm3d) by default.
|
| 145 |
+
This can be changed however by providing the 'order' argument, e.g. in order
|
| 146 |
+
to change to Conv3d+BatchNorm3d+ELU use order='cbe'.
|
| 147 |
+
Use padded convolutions to make sure that the output (H_out, W_out) is the same
|
| 148 |
+
as (H_in, W_in), so that you don't have to crop in the decoder path.
|
| 149 |
+
|
| 150 |
+
Args:
|
| 151 |
+
in_channels (int): number of input channels
|
| 152 |
+
out_channels (int): number of output channels
|
| 153 |
+
encoder (bool): if True we're in the encoder path, otherwise we're in the decoder
|
| 154 |
+
kernel_size (int or tuple): size of the convolving kernel
|
| 155 |
+
order (string): determines the order of layers, e.g.
|
| 156 |
+
'cr' -> conv + ReLU
|
| 157 |
+
'crg' -> conv + ReLU + groupnorm
|
| 158 |
+
'cl' -> conv + LeakyReLU
|
| 159 |
+
'ce' -> conv + ELU
|
| 160 |
+
num_groups (int): number of groups for the GroupNorm
|
| 161 |
+
padding (int or tuple): add zero-padding added to all three sides of the input
|
| 162 |
+
upscale (int): number of the convolution to upscale in encoder if DoubleConv, default: 2
|
| 163 |
+
dropout_prob (float or tuple): dropout probability for each convolution, default 0.1
|
| 164 |
+
is3d (bool): if True use Conv3d instead of Conv2d layers
|
| 165 |
+
"""
|
| 166 |
+
|
| 167 |
+
def __init__(self, in_channels, out_channels, encoder, kernel_size=3, order='gcr',
|
| 168 |
+
num_groups=8, padding=1, upscale=2, dropout_prob=0.1, is3d=True):
|
| 169 |
+
super(DoubleConv, self).__init__()
|
| 170 |
+
if encoder:
|
| 171 |
+
# we're in the encoder path
|
| 172 |
+
conv1_in_channels = in_channels
|
| 173 |
+
if upscale == 1:
|
| 174 |
+
conv1_out_channels = out_channels
|
| 175 |
+
else:
|
| 176 |
+
conv1_out_channels = out_channels // 2
|
| 177 |
+
if conv1_out_channels < in_channels:
|
| 178 |
+
conv1_out_channels = in_channels
|
| 179 |
+
conv2_in_channels, conv2_out_channels = conv1_out_channels, out_channels
|
| 180 |
+
else:
|
| 181 |
+
# we're in the decoder path, decrease the number of channels in the 1st convolution
|
| 182 |
+
conv1_in_channels, conv1_out_channels = in_channels, out_channels
|
| 183 |
+
conv2_in_channels, conv2_out_channels = out_channels, out_channels
|
| 184 |
+
|
| 185 |
+
# check if dropout_prob is a tuple and if so
|
| 186 |
+
# split it for different dropout probabilities for each convolution.
|
| 187 |
+
if isinstance(dropout_prob, list) or isinstance(dropout_prob, tuple):
|
| 188 |
+
dropout_prob1 = dropout_prob[0]
|
| 189 |
+
dropout_prob2 = dropout_prob[1]
|
| 190 |
+
else:
|
| 191 |
+
dropout_prob1 = dropout_prob2 = dropout_prob
|
| 192 |
+
|
| 193 |
+
# conv1
|
| 194 |
+
self.add_module('SingleConv1',
|
| 195 |
+
SingleConv(conv1_in_channels, conv1_out_channels, kernel_size, order, num_groups,
|
| 196 |
+
padding=padding, dropout_prob=dropout_prob1, is3d=is3d))
|
| 197 |
+
# conv2
|
| 198 |
+
self.add_module('SingleConv2',
|
| 199 |
+
SingleConv(conv2_in_channels, conv2_out_channels, kernel_size, order, num_groups,
|
| 200 |
+
padding=padding, dropout_prob=dropout_prob2, is3d=is3d))
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
class ResNetBlock(nn.Module):
|
| 204 |
+
"""
|
| 205 |
+
Residual block that can be used instead of standard DoubleConv in the Encoder module.
|
| 206 |
+
Motivated by: https://arxiv.org/pdf/1706.00120.pdf
|
| 207 |
+
|
| 208 |
+
Notice we use ELU instead of ReLU (order='cge') and put non-linearity after the groupnorm.
|
| 209 |
+
"""
|
| 210 |
+
|
| 211 |
+
def __init__(self, in_channels, out_channels, kernel_size=3, order='cge', num_groups=8, is3d=True, **kwargs):
|
| 212 |
+
super(ResNetBlock, self).__init__()
|
| 213 |
+
|
| 214 |
+
if in_channels != out_channels:
|
| 215 |
+
# conv1x1 for increasing the number of channels
|
| 216 |
+
if is3d:
|
| 217 |
+
self.conv1 = nn.Conv3d(in_channels, out_channels, 1)
|
| 218 |
+
else:
|
| 219 |
+
self.conv1 = nn.Conv2d(in_channels, out_channels, 1)
|
| 220 |
+
else:
|
| 221 |
+
self.conv1 = nn.Identity()
|
| 222 |
+
|
| 223 |
+
self.conv2 = SingleConv(in_channels, out_channels, kernel_size=kernel_size, order=order, num_groups=num_groups,
|
| 224 |
+
is3d=is3d)
|
| 225 |
+
# remove non-linearity from the 3rd convolution since it's going to be applied after adding the residual
|
| 226 |
+
n_order = order
|
| 227 |
+
for c in 'rel':
|
| 228 |
+
n_order = n_order.replace(c, '')
|
| 229 |
+
self.conv3 = SingleConv(out_channels, out_channels, kernel_size=kernel_size, order=n_order,
|
| 230 |
+
num_groups=num_groups, is3d=is3d)
|
| 231 |
+
|
| 232 |
+
# create non-linearity separately
|
| 233 |
+
if 'l' in order:
|
| 234 |
+
self.non_linearity = nn.LeakyReLU(negative_slope=0.1, inplace=True)
|
| 235 |
+
elif 'e' in order:
|
| 236 |
+
self.non_linearity = nn.ELU(inplace=True)
|
| 237 |
+
else:
|
| 238 |
+
self.non_linearity = nn.ReLU(inplace=True)
|
| 239 |
+
|
| 240 |
+
def forward(self, x):
|
| 241 |
+
# apply first convolution to bring the number of channels to out_channels
|
| 242 |
+
residual = self.conv1(x)
|
| 243 |
+
|
| 244 |
+
out = self.conv2(x)
|
| 245 |
+
out = self.conv3(out)
|
| 246 |
+
|
| 247 |
+
out += residual
|
| 248 |
+
out = self.non_linearity(out)
|
| 249 |
+
|
| 250 |
+
return out
|
| 251 |
+
|
| 252 |
+
class Encoder(nn.Module):
|
| 253 |
+
"""
|
| 254 |
+
A single module from the encoder path consisting of the optional max
|
| 255 |
+
pooling layer (one may specify the MaxPool kernel_size to be different
|
| 256 |
+
from the standard (2,2,2), e.g. if the volumetric data is anisotropic
|
| 257 |
+
(make sure to use complementary scale_factor in the decoder path) followed by
|
| 258 |
+
a basic module (DoubleConv or ResNetBlock).
|
| 259 |
+
|
| 260 |
+
Args:
|
| 261 |
+
in_channels (int): number of input channels
|
| 262 |
+
out_channels (int): number of output channels
|
| 263 |
+
conv_kernel_size (int or tuple): size of the convolving kernel
|
| 264 |
+
apply_pooling (bool): if True use MaxPool3d before DoubleConv
|
| 265 |
+
pool_kernel_size (int or tuple): the size of the window
|
| 266 |
+
pool_type (str): pooling layer: 'max' or 'avg'
|
| 267 |
+
basic_module(nn.Module): either ResNetBlock or DoubleConv
|
| 268 |
+
conv_layer_order (string): determines the order of layers
|
| 269 |
+
in `DoubleConv` module. See `DoubleConv` for more info.
|
| 270 |
+
num_groups (int): number of groups for the GroupNorm
|
| 271 |
+
padding (int or tuple): add zero-padding added to all three sides of the input
|
| 272 |
+
upscale (int): number of the convolution to upscale in encoder if DoubleConv, default: 2
|
| 273 |
+
dropout_prob (float or tuple): dropout probability, default 0.1
|
| 274 |
+
is3d (bool): use 3d or 2d convolutions/pooling operation
|
| 275 |
+
"""
|
| 276 |
+
|
| 277 |
+
def __init__(self, in_channels, out_channels, conv_kernel_size=3, apply_pooling=True,
|
| 278 |
+
pool_kernel_size=2, pool_type='max', basic_module=DoubleConv, conv_layer_order='gcr',
|
| 279 |
+
num_groups=8, padding=1, upscale=2, dropout_prob=0.1, is3d=True):
|
| 280 |
+
super(Encoder, self).__init__()
|
| 281 |
+
assert pool_type in ['max', 'avg']
|
| 282 |
+
if apply_pooling:
|
| 283 |
+
if pool_type == 'max':
|
| 284 |
+
if is3d:
|
| 285 |
+
self.pooling = nn.MaxPool3d(kernel_size=pool_kernel_size)
|
| 286 |
+
else:
|
| 287 |
+
self.pooling = nn.MaxPool2d(kernel_size=pool_kernel_size)
|
| 288 |
+
else:
|
| 289 |
+
if is3d:
|
| 290 |
+
self.pooling = nn.AvgPool3d(kernel_size=pool_kernel_size)
|
| 291 |
+
else:
|
| 292 |
+
self.pooling = nn.AvgPool2d(kernel_size=pool_kernel_size)
|
| 293 |
+
else:
|
| 294 |
+
self.pooling = None
|
| 295 |
+
|
| 296 |
+
self.basic_module = basic_module(in_channels, out_channels,
|
| 297 |
+
encoder=True,
|
| 298 |
+
kernel_size=conv_kernel_size,
|
| 299 |
+
order=conv_layer_order,
|
| 300 |
+
num_groups=num_groups,
|
| 301 |
+
padding=padding,
|
| 302 |
+
upscale=upscale,
|
| 303 |
+
dropout_prob=dropout_prob,
|
| 304 |
+
is3d=is3d)
|
| 305 |
+
|
| 306 |
+
def forward(self, x):
|
| 307 |
+
if self.pooling is not None:
|
| 308 |
+
x = self.pooling(x)
|
| 309 |
+
x = self.basic_module(x)
|
| 310 |
+
return x
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
class Decoder(nn.Module):
|
| 314 |
+
"""
|
| 315 |
+
A single module for decoder path consisting of the upsampling layer
|
| 316 |
+
(either learned ConvTranspose3d or nearest neighbor interpolation)
|
| 317 |
+
followed by a basic module (DoubleConv or ResNetBlock).
|
| 318 |
+
|
| 319 |
+
Args:
|
| 320 |
+
in_channels (int): number of input channels
|
| 321 |
+
out_channels (int): number of output channels
|
| 322 |
+
conv_kernel_size (int or tuple): size of the convolving kernel
|
| 323 |
+
scale_factor (int or tuple): used as the multiplier for the image H/W/D in
|
| 324 |
+
case of nn.Upsample or as stride in case of ConvTranspose3d, must reverse the MaxPool3d operation
|
| 325 |
+
from the corresponding encoder
|
| 326 |
+
basic_module(nn.Module): either ResNetBlock or DoubleConv
|
| 327 |
+
conv_layer_order (string): determines the order of layers
|
| 328 |
+
in `DoubleConv` module. See `DoubleConv` for more info.
|
| 329 |
+
num_groups (int): number of groups for the GroupNorm
|
| 330 |
+
padding (int or tuple): add zero-padding added to all three sides of the input
|
| 331 |
+
upsample (str): algorithm used for upsampling:
|
| 332 |
+
InterpolateUpsampling: 'nearest' | 'linear' | 'bilinear' | 'trilinear' | 'area'
|
| 333 |
+
TransposeConvUpsampling: 'deconv'
|
| 334 |
+
No upsampling: None
|
| 335 |
+
Default: 'default' (chooses automatically)
|
| 336 |
+
dropout_prob (float or tuple): dropout probability, default 0.1
|
| 337 |
+
"""
|
| 338 |
+
|
| 339 |
+
def __init__(self, in_channels, out_channels, conv_kernel_size=3, scale_factor=2, basic_module=DoubleConv,
|
| 340 |
+
conv_layer_order='gcr', num_groups=8, padding=1, upsample='default',
|
| 341 |
+
dropout_prob=0.1, is3d=True):
|
| 342 |
+
super(Decoder, self).__init__()
|
| 343 |
+
|
| 344 |
+
# perform concat joining per default
|
| 345 |
+
concat = True
|
| 346 |
+
|
| 347 |
+
# don't adapt channels after join operation
|
| 348 |
+
adapt_channels = False
|
| 349 |
+
|
| 350 |
+
if upsample is not None and upsample != 'none':
|
| 351 |
+
if upsample == 'default':
|
| 352 |
+
if basic_module == DoubleConv:
|
| 353 |
+
upsample = 'nearest' # use nearest neighbor interpolation for upsampling
|
| 354 |
+
concat = True # use concat joining
|
| 355 |
+
adapt_channels = False # don't adapt channels
|
| 356 |
+
elif basic_module == ResNetBlock: #or basic_module == ResNetBlockSE:
|
| 357 |
+
upsample = 'deconv' # use deconvolution upsampling
|
| 358 |
+
concat = False # use summation joining
|
| 359 |
+
adapt_channels = True # adapt channels after joining
|
| 360 |
+
|
| 361 |
+
# perform deconvolution upsampling if mode is deconv
|
| 362 |
+
if upsample == 'deconv':
|
| 363 |
+
self.upsampling = TransposeConvUpsampling(in_channels=in_channels, out_channels=out_channels,
|
| 364 |
+
kernel_size=conv_kernel_size, scale_factor=scale_factor,
|
| 365 |
+
is3d=is3d)
|
| 366 |
+
else:
|
| 367 |
+
self.upsampling = InterpolateUpsampling(mode=upsample)
|
| 368 |
+
else:
|
| 369 |
+
# no upsampling
|
| 370 |
+
self.upsampling = NoUpsampling()
|
| 371 |
+
# concat joining
|
| 372 |
+
self.joining = partial(self._joining, concat=True)
|
| 373 |
+
|
| 374 |
+
# perform joining operation
|
| 375 |
+
self.joining = partial(self._joining, concat=concat)
|
| 376 |
+
|
| 377 |
+
# adapt the number of in_channels for the ResNetBlock
|
| 378 |
+
if adapt_channels is True:
|
| 379 |
+
in_channels = out_channels
|
| 380 |
+
|
| 381 |
+
self.basic_module = basic_module(in_channels, out_channels,
|
| 382 |
+
encoder=False,
|
| 383 |
+
kernel_size=conv_kernel_size,
|
| 384 |
+
order=conv_layer_order,
|
| 385 |
+
num_groups=num_groups,
|
| 386 |
+
padding=padding,
|
| 387 |
+
dropout_prob=dropout_prob,
|
| 388 |
+
is3d=is3d)
|
| 389 |
+
|
| 390 |
+
def forward(self, encoder_features, x):
|
| 391 |
+
x = self.upsampling(encoder_features=encoder_features, x=x)
|
| 392 |
+
x = self.joining(encoder_features, x)
|
| 393 |
+
x = self.basic_module(x)
|
| 394 |
+
return x
|
| 395 |
+
|
| 396 |
+
@staticmethod
|
| 397 |
+
def _joining(encoder_features, x, concat):
|
| 398 |
+
if concat:
|
| 399 |
+
return torch.cat((encoder_features, x), dim=1)
|
| 400 |
+
else:
|
| 401 |
+
return encoder_features + x
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def create_encoders(in_channels, f_maps, basic_module, conv_kernel_size, conv_padding,
|
| 405 |
+
conv_upscale, dropout_prob,
|
| 406 |
+
layer_order, num_groups, pool_kernel_size, is3d):
|
| 407 |
+
# create encoder path consisting of Encoder modules. Depth of the encoder is equal to `len(f_maps)`
|
| 408 |
+
encoders = []
|
| 409 |
+
for i, out_feature_num in enumerate(f_maps):
|
| 410 |
+
if i == 0:
|
| 411 |
+
# apply conv_coord only in the first encoder if any
|
| 412 |
+
encoder = Encoder(in_channels, out_feature_num,
|
| 413 |
+
apply_pooling=False, # skip pooling in the firs encoder
|
| 414 |
+
basic_module=basic_module,
|
| 415 |
+
conv_layer_order=layer_order,
|
| 416 |
+
conv_kernel_size=conv_kernel_size,
|
| 417 |
+
num_groups=num_groups,
|
| 418 |
+
padding=conv_padding,
|
| 419 |
+
upscale=conv_upscale,
|
| 420 |
+
dropout_prob=dropout_prob,
|
| 421 |
+
is3d=is3d)
|
| 422 |
+
else:
|
| 423 |
+
encoder = Encoder(f_maps[i - 1], out_feature_num,
|
| 424 |
+
basic_module=basic_module,
|
| 425 |
+
conv_layer_order=layer_order,
|
| 426 |
+
conv_kernel_size=conv_kernel_size,
|
| 427 |
+
num_groups=num_groups,
|
| 428 |
+
pool_kernel_size=pool_kernel_size,
|
| 429 |
+
padding=conv_padding,
|
| 430 |
+
upscale=conv_upscale,
|
| 431 |
+
dropout_prob=dropout_prob,
|
| 432 |
+
is3d=is3d)
|
| 433 |
+
|
| 434 |
+
encoders.append(encoder)
|
| 435 |
+
|
| 436 |
+
return nn.ModuleList(encoders)
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
def create_decoders(f_maps, basic_module, conv_kernel_size, conv_padding, layer_order,
|
| 440 |
+
num_groups, upsample, dropout_prob, is3d):
|
| 441 |
+
# create decoder path consisting of the Decoder modules. The length of the decoder list is equal to `len(f_maps) - 1`
|
| 442 |
+
decoders = []
|
| 443 |
+
reversed_f_maps = list(reversed(f_maps[1:]))
|
| 444 |
+
for i in range(len(reversed_f_maps) - 1):
|
| 445 |
+
if basic_module == DoubleConv and upsample != 'deconv':
|
| 446 |
+
in_feature_num = reversed_f_maps[i] + reversed_f_maps[i + 1]
|
| 447 |
+
else:
|
| 448 |
+
in_feature_num = reversed_f_maps[i]
|
| 449 |
+
|
| 450 |
+
out_feature_num = reversed_f_maps[i + 1]
|
| 451 |
+
|
| 452 |
+
decoder = Decoder(in_feature_num, out_feature_num,
|
| 453 |
+
basic_module=basic_module,
|
| 454 |
+
conv_layer_order=layer_order,
|
| 455 |
+
conv_kernel_size=conv_kernel_size,
|
| 456 |
+
num_groups=num_groups,
|
| 457 |
+
padding=conv_padding,
|
| 458 |
+
upsample=upsample,
|
| 459 |
+
dropout_prob=dropout_prob,
|
| 460 |
+
is3d=is3d)
|
| 461 |
+
decoders.append(decoder)
|
| 462 |
+
return nn.ModuleList(decoders)
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
class AbstractUpsampling(nn.Module):
|
| 466 |
+
"""
|
| 467 |
+
Abstract class for upsampling. A given implementation should upsample a given 5D input tensor using either
|
| 468 |
+
interpolation or learned transposed convolution.
|
| 469 |
+
"""
|
| 470 |
+
|
| 471 |
+
def __init__(self, upsample):
|
| 472 |
+
super(AbstractUpsampling, self).__init__()
|
| 473 |
+
self.upsample = upsample
|
| 474 |
+
|
| 475 |
+
def forward(self, encoder_features, x):
|
| 476 |
+
# get the spatial dimensions of the output given the encoder_features
|
| 477 |
+
output_size = encoder_features.size()[2:]
|
| 478 |
+
# upsample the input and return
|
| 479 |
+
return self.upsample(x, output_size)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
class InterpolateUpsampling(AbstractUpsampling):
|
| 483 |
+
"""
|
| 484 |
+
Args:
|
| 485 |
+
mode (str): algorithm used for upsampling:
|
| 486 |
+
'nearest' | 'linear' | 'bilinear' | 'trilinear' | 'area'. Default: 'nearest'
|
| 487 |
+
used only if transposed_conv is False
|
| 488 |
+
"""
|
| 489 |
+
|
| 490 |
+
def __init__(self, mode='nearest'):
|
| 491 |
+
upsample = partial(self._interpolate, mode=mode)
|
| 492 |
+
super().__init__(upsample)
|
| 493 |
+
|
| 494 |
+
@staticmethod
|
| 495 |
+
def _interpolate(x, size, mode):
|
| 496 |
+
return F.interpolate(x, size=size, mode=mode)
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
class TransposeConvUpsampling(AbstractUpsampling):
|
| 500 |
+
"""
|
| 501 |
+
Args:
|
| 502 |
+
in_channels (int): number of input channels for transposed conv
|
| 503 |
+
used only if transposed_conv is True
|
| 504 |
+
out_channels (int): number of output channels for transpose conv
|
| 505 |
+
used only if transposed_conv is True
|
| 506 |
+
kernel_size (int or tuple): size of the convolving kernel
|
| 507 |
+
used only if transposed_conv is True
|
| 508 |
+
scale_factor (int or tuple): stride of the convolution
|
| 509 |
+
used only if transposed_conv is True
|
| 510 |
+
is3d (bool): if True use ConvTranspose3d, otherwise use ConvTranspose2d
|
| 511 |
+
"""
|
| 512 |
+
|
| 513 |
+
class Upsample(nn.Module):
|
| 514 |
+
"""
|
| 515 |
+
Workaround the 'ValueError: requested an output size...' in the `_output_padding` method in
|
| 516 |
+
transposed convolution. It performs transposed conv followed by the interpolation to the correct size if necessary.
|
| 517 |
+
"""
|
| 518 |
+
|
| 519 |
+
def __init__(self, conv_transposed, is3d):
|
| 520 |
+
super().__init__()
|
| 521 |
+
self.conv_transposed = conv_transposed
|
| 522 |
+
self.is3d = is3d
|
| 523 |
+
|
| 524 |
+
def forward(self, x, size):
|
| 525 |
+
x = self.conv_transposed(x)
|
| 526 |
+
return F.interpolate(x, size=size)
|
| 527 |
+
|
| 528 |
+
def __init__(self, in_channels, out_channels, kernel_size=3, scale_factor=2, is3d=True):
|
| 529 |
+
# make sure that the output size reverses the MaxPool3d from the corresponding encoder
|
| 530 |
+
if is3d is True:
|
| 531 |
+
conv_transposed = nn.ConvTranspose3d(in_channels, out_channels, kernel_size=kernel_size,
|
| 532 |
+
stride=scale_factor, padding=1, bias=False)
|
| 533 |
+
else:
|
| 534 |
+
conv_transposed = nn.ConvTranspose2d(in_channels, out_channels, kernel_size=kernel_size,
|
| 535 |
+
stride=scale_factor, padding=1, bias=False)
|
| 536 |
+
upsample = self.Upsample(conv_transposed, is3d)
|
| 537 |
+
super().__init__(upsample)
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
class NoUpsampling(AbstractUpsampling):
|
| 541 |
+
def __init__(self):
|
| 542 |
+
super().__init__(self._no_upsampling)
|
| 543 |
+
|
| 544 |
+
@staticmethod
|
| 545 |
+
def _no_upsampling(x, size):
|
| 546 |
+
return x
|
third_party/PartField/partfield/model/UNet/model.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# https://github.com/wolny/pytorch-3dunet/blob/master/pytorch3dunet/unet3d/buildingblocks.py
|
| 2 |
+
# MIT License
|
| 3 |
+
|
| 4 |
+
# Copyright (c) 2018 Adrian Wolny
|
| 5 |
+
|
| 6 |
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 7 |
+
# of this software and associated documentation files (the "Software"), to deal
|
| 8 |
+
# in the Software without restriction, including without limitation the rights
|
| 9 |
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 10 |
+
# copies of the Software, and to permit persons to whom the Software is
|
| 11 |
+
# furnished to do so, subject to the following conditions:
|
| 12 |
+
|
| 13 |
+
# The above copyright notice and this permission notice shall be included in all
|
| 14 |
+
# copies or substantial portions of the Software.
|
| 15 |
+
|
| 16 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 17 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 18 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 19 |
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 20 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 21 |
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 22 |
+
# SOFTWARE.
|
| 23 |
+
|
| 24 |
+
import torch.nn as nn
|
| 25 |
+
|
| 26 |
+
from partfield.model.UNet.buildingblocks import DoubleConv, ResNetBlock, \
|
| 27 |
+
create_decoders, create_encoders
|
| 28 |
+
|
| 29 |
+
def number_of_features_per_level(init_channel_number, num_levels):
|
| 30 |
+
return [init_channel_number * 2 ** k for k in range(num_levels)]
|
| 31 |
+
|
| 32 |
+
class AbstractUNet(nn.Module):
|
| 33 |
+
"""
|
| 34 |
+
Base class for standard and residual UNet.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
in_channels (int): number of input channels
|
| 38 |
+
out_channels (int): number of output segmentation masks;
|
| 39 |
+
Note that the of out_channels might correspond to either
|
| 40 |
+
different semantic classes or to different binary segmentation mask.
|
| 41 |
+
It's up to the user of the class to interpret the out_channels and
|
| 42 |
+
use the proper loss criterion during training (i.e. CrossEntropyLoss (multi-class)
|
| 43 |
+
or BCEWithLogitsLoss (two-class) respectively)
|
| 44 |
+
f_maps (int, tuple): number of feature maps at each level of the encoder; if it's an integer the number
|
| 45 |
+
of feature maps is given by the geometric progression: f_maps ^ k, k=1,2,3,4
|
| 46 |
+
final_sigmoid (bool): if True apply element-wise nn.Sigmoid after the final 1x1 convolution,
|
| 47 |
+
otherwise apply nn.Softmax. In effect only if `self.training == False`, i.e. during validation/testing
|
| 48 |
+
basic_module: basic model for the encoder/decoder (DoubleConv, ResNetBlock, ....)
|
| 49 |
+
layer_order (string): determines the order of layers in `SingleConv` module.
|
| 50 |
+
E.g. 'crg' stands for GroupNorm3d+Conv3d+ReLU. See `SingleConv` for more info
|
| 51 |
+
num_groups (int): number of groups for the GroupNorm
|
| 52 |
+
num_levels (int): number of levels in the encoder/decoder path (applied only if f_maps is an int)
|
| 53 |
+
default: 4
|
| 54 |
+
is_segmentation (bool): if True and the model is in eval mode, Sigmoid/Softmax normalization is applied
|
| 55 |
+
after the final convolution; if False (regression problem) the normalization layer is skipped
|
| 56 |
+
conv_kernel_size (int or tuple): size of the convolving kernel in the basic_module
|
| 57 |
+
pool_kernel_size (int or tuple): the size of the window
|
| 58 |
+
conv_padding (int or tuple): add zero-padding added to all three sides of the input
|
| 59 |
+
conv_upscale (int): number of the convolution to upscale in encoder if DoubleConv, default: 2
|
| 60 |
+
upsample (str): algorithm used for decoder upsampling:
|
| 61 |
+
InterpolateUpsampling: 'nearest' | 'linear' | 'bilinear' | 'trilinear' | 'area'
|
| 62 |
+
TransposeConvUpsampling: 'deconv'
|
| 63 |
+
No upsampling: None
|
| 64 |
+
Default: 'default' (chooses automatically)
|
| 65 |
+
dropout_prob (float or tuple): dropout probability, default: 0.1
|
| 66 |
+
is3d (bool): if True the model is 3D, otherwise 2D, default: True
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
def __init__(self, in_channels, out_channels, final_sigmoid, basic_module, f_maps=64, layer_order='gcr',
|
| 70 |
+
num_groups=8, num_levels=4, is_segmentation=False, conv_kernel_size=3, pool_kernel_size=2,
|
| 71 |
+
conv_padding=1, conv_upscale=2, upsample='default', dropout_prob=0.1, is3d=True, encoder_only=False):
|
| 72 |
+
super(AbstractUNet, self).__init__()
|
| 73 |
+
|
| 74 |
+
if isinstance(f_maps, int):
|
| 75 |
+
f_maps = number_of_features_per_level(f_maps, num_levels=num_levels)
|
| 76 |
+
|
| 77 |
+
assert isinstance(f_maps, list) or isinstance(f_maps, tuple)
|
| 78 |
+
assert len(f_maps) > 1, "Required at least 2 levels in the U-Net"
|
| 79 |
+
if 'g' in layer_order:
|
| 80 |
+
assert num_groups is not None, "num_groups must be specified if GroupNorm is used"
|
| 81 |
+
|
| 82 |
+
# create encoder path
|
| 83 |
+
self.encoders = create_encoders(in_channels, f_maps, basic_module, conv_kernel_size,
|
| 84 |
+
conv_padding, conv_upscale, dropout_prob,
|
| 85 |
+
layer_order, num_groups, pool_kernel_size, is3d)
|
| 86 |
+
|
| 87 |
+
self.encoder_only = encoder_only
|
| 88 |
+
|
| 89 |
+
if encoder_only == False:
|
| 90 |
+
# create decoder path
|
| 91 |
+
self.decoders = create_decoders(f_maps, basic_module, conv_kernel_size, conv_padding,
|
| 92 |
+
layer_order, num_groups, upsample, dropout_prob,
|
| 93 |
+
is3d)
|
| 94 |
+
|
| 95 |
+
# in the last layer a 1×1 convolution reduces the number of output channels to the number of labels
|
| 96 |
+
if is3d:
|
| 97 |
+
self.final_conv = nn.Conv3d(f_maps[1], out_channels, 1)
|
| 98 |
+
else:
|
| 99 |
+
self.final_conv = nn.Conv2d(f_maps[1], out_channels, 1)
|
| 100 |
+
|
| 101 |
+
if is_segmentation:
|
| 102 |
+
# semantic segmentation problem
|
| 103 |
+
if final_sigmoid:
|
| 104 |
+
self.final_activation = nn.Sigmoid()
|
| 105 |
+
else:
|
| 106 |
+
self.final_activation = nn.Softmax(dim=1)
|
| 107 |
+
else:
|
| 108 |
+
# regression problem
|
| 109 |
+
self.final_activation = None
|
| 110 |
+
|
| 111 |
+
def forward(self, x, return_bottleneck_feat=False):
|
| 112 |
+
# encoder part
|
| 113 |
+
encoders_features = []
|
| 114 |
+
for encoder in self.encoders:
|
| 115 |
+
x = encoder(x)
|
| 116 |
+
# reverse the encoder outputs to be aligned with the decoder
|
| 117 |
+
encoders_features.insert(0, x)
|
| 118 |
+
|
| 119 |
+
# remove the last encoder's output from the list
|
| 120 |
+
# !!remember: it's the 1st in the list
|
| 121 |
+
bottleneck_feat = encoders_features[0]
|
| 122 |
+
if self.encoder_only:
|
| 123 |
+
return bottleneck_feat
|
| 124 |
+
else:
|
| 125 |
+
encoders_features = encoders_features[1:]
|
| 126 |
+
|
| 127 |
+
# decoder part
|
| 128 |
+
for decoder, encoder_features in zip(self.decoders, encoders_features):
|
| 129 |
+
# pass the output from the corresponding encoder and the output
|
| 130 |
+
# of the previous decoder
|
| 131 |
+
x = decoder(encoder_features, x)
|
| 132 |
+
|
| 133 |
+
x = self.final_conv(x)
|
| 134 |
+
# During training the network outputs logits
|
| 135 |
+
if self.final_activation is not None:
|
| 136 |
+
x = self.final_activation(x)
|
| 137 |
+
|
| 138 |
+
if return_bottleneck_feat:
|
| 139 |
+
return x, bottleneck_feat
|
| 140 |
+
else:
|
| 141 |
+
return x
|
| 142 |
+
|
| 143 |
+
class ResidualUNet3D(AbstractUNet):
|
| 144 |
+
"""
|
| 145 |
+
Residual 3DUnet model implementation based on https://arxiv.org/pdf/1706.00120.pdf.
|
| 146 |
+
Uses ResNetBlock as a basic building block, summation joining instead
|
| 147 |
+
of concatenation joining and transposed convolutions for upsampling (watch out for block artifacts).
|
| 148 |
+
Since the model effectively becomes a residual net, in theory it allows for deeper UNet.
|
| 149 |
+
"""
|
| 150 |
+
|
| 151 |
+
def __init__(self, in_channels, out_channels, final_sigmoid=True, f_maps=(8, 16, 64, 256, 1024), layer_order='gcr',
|
| 152 |
+
num_groups=8, num_levels=5, is_segmentation=True, conv_padding=1,
|
| 153 |
+
conv_upscale=2, upsample='default', dropout_prob=0.1, encoder_only=False, **kwargs):
|
| 154 |
+
super(ResidualUNet3D, self).__init__(in_channels=in_channels,
|
| 155 |
+
out_channels=out_channels,
|
| 156 |
+
final_sigmoid=final_sigmoid,
|
| 157 |
+
basic_module=ResNetBlock,
|
| 158 |
+
f_maps=f_maps,
|
| 159 |
+
layer_order=layer_order,
|
| 160 |
+
num_groups=num_groups,
|
| 161 |
+
num_levels=num_levels,
|
| 162 |
+
is_segmentation=is_segmentation,
|
| 163 |
+
conv_padding=conv_padding,
|
| 164 |
+
conv_upscale=conv_upscale,
|
| 165 |
+
upsample=upsample,
|
| 166 |
+
dropout_prob=dropout_prob,
|
| 167 |
+
encoder_only=encoder_only,
|
| 168 |
+
is3d=True)
|
| 169 |
+
|
| 170 |
+
|
third_party/PartField/partfield/model/model_utils.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
|
| 4 |
+
class VanillaMLP(nn.Module):
|
| 5 |
+
def __init__(self, input_dim, output_dim, out_activation, n_hidden_layers=4, n_neurons=64, activation="ReLU"):
|
| 6 |
+
super().__init__()
|
| 7 |
+
self.n_neurons = n_neurons
|
| 8 |
+
self.n_hidden_layers = n_hidden_layers
|
| 9 |
+
self.activation = activation
|
| 10 |
+
self.out_activation = out_activation
|
| 11 |
+
layers = [
|
| 12 |
+
self.make_linear(input_dim, self.n_neurons, is_first=True, is_last=False),
|
| 13 |
+
self.make_activation(),
|
| 14 |
+
]
|
| 15 |
+
for i in range(self.n_hidden_layers - 1):
|
| 16 |
+
layers += [
|
| 17 |
+
self.make_linear(
|
| 18 |
+
self.n_neurons, self.n_neurons, is_first=False, is_last=False
|
| 19 |
+
),
|
| 20 |
+
self.make_activation(),
|
| 21 |
+
]
|
| 22 |
+
layers += [
|
| 23 |
+
self.make_linear(self.n_neurons, output_dim, is_first=False, is_last=True)
|
| 24 |
+
]
|
| 25 |
+
if self.out_activation == "sigmoid":
|
| 26 |
+
layers += [nn.Sigmoid()]
|
| 27 |
+
elif self.out_activation == "tanh":
|
| 28 |
+
layers += [nn.Tanh()]
|
| 29 |
+
elif self.out_activation == "hardtanh":
|
| 30 |
+
layers += [nn.Hardtanh()]
|
| 31 |
+
elif self.out_activation == "GELU":
|
| 32 |
+
layers += [nn.GELU()]
|
| 33 |
+
elif self.out_activation == "RELU":
|
| 34 |
+
layers += [nn.ReLU()]
|
| 35 |
+
else:
|
| 36 |
+
raise NotImplementedError
|
| 37 |
+
self.layers = nn.Sequential(*layers)
|
| 38 |
+
|
| 39 |
+
def forward(self, x, split_size=100000):
|
| 40 |
+
with torch.cuda.amp.autocast(enabled=False):
|
| 41 |
+
out = self.layers(x)
|
| 42 |
+
return out
|
| 43 |
+
|
| 44 |
+
def make_linear(self, dim_in, dim_out, is_first, is_last):
|
| 45 |
+
layer = nn.Linear(dim_in, dim_out, bias=False)
|
| 46 |
+
return layer
|
| 47 |
+
|
| 48 |
+
def make_activation(self):
|
| 49 |
+
if self.activation == "ReLU":
|
| 50 |
+
return nn.ReLU(inplace=True)
|
| 51 |
+
elif self.activation == "GELU":
|
| 52 |
+
return nn.GELU()
|
| 53 |
+
else:
|
| 54 |
+
raise NotImplementedError
|