multimodalart HF Staff commited on
Commit
43ca9e0
·
verified ·
1 Parent(s): 506bef0

SpaceFlow demo

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. LICENSE +201 -0
  3. README.md +21 -7
  4. THIRD_PARTY_NOTICES.md +27 -0
  5. app.py +457 -0
  6. config/default.yaml +41 -0
  7. config/trellis_pipeline/pipeline.json +67 -0
  8. examples/chair.npz +3 -0
  9. examples/hot_air_balloon.npz +3 -0
  10. examples/sailboat.npz +3 -0
  11. examples/toy_elephant.npz +3 -0
  12. examples/toy_rocket.npz +3 -0
  13. examples/windmill.npz +3 -0
  14. lib/opt/self_similarity.py +1072 -0
  15. lib/util/checkpoint_compat.py +18 -0
  16. lib/util/common.py +101 -0
  17. lib/util/generation.py +80 -0
  18. lib/util/model_revisions.py +16 -0
  19. lib/util/partfield.py +108 -0
  20. lib/util/pipeline_compat.py +10 -0
  21. lib/util/pointcloud.py +24 -0
  22. lib/util/render.py +133 -0
  23. lib/util/visualization.py +93 -0
  24. packages.txt +4 -0
  25. requirements.txt +38 -0
  26. requirements/model-revisions.json +23 -0
  27. run_local_tau.py +773 -0
  28. third_party/PartField/LICENSE +36 -0
  29. third_party/PartField/config.yaml +88 -0
  30. third_party/PartField/partfield/config/__init__.py +26 -0
  31. third_party/PartField/partfield/config/defaults.py +92 -0
  32. third_party/PartField/partfield/dataloader.py +78 -0
  33. third_party/PartField/partfield/model/PVCNN/conv_pointnet.py +251 -0
  34. third_party/PartField/partfield/model/PVCNN/dnnlib_util.py +1074 -0
  35. third_party/PartField/partfield/model/PVCNN/encoder_pc.py +243 -0
  36. third_party/PartField/partfield/model/PVCNN/pc_encoder.py +90 -0
  37. third_party/PartField/partfield/model/PVCNN/pv_module/__init__.py +2 -0
  38. third_party/PartField/partfield/model/PVCNN/pv_module/ball_query.py +34 -0
  39. third_party/PartField/partfield/model/PVCNN/pv_module/frustum.py +141 -0
  40. third_party/PartField/partfield/model/PVCNN/pv_module/functional/__init__.py +1 -0
  41. third_party/PartField/partfield/model/PVCNN/pv_module/functional/devoxelization.py +12 -0
  42. third_party/PartField/partfield/model/PVCNN/pv_module/loss.py +10 -0
  43. third_party/PartField/partfield/model/PVCNN/pv_module/pointnet.py +113 -0
  44. third_party/PartField/partfield/model/PVCNN/pv_module/pvconv.py +38 -0
  45. third_party/PartField/partfield/model/PVCNN/pv_module/shared_mlp.py +35 -0
  46. third_party/PartField/partfield/model/PVCNN/pv_module/voxelization.py +50 -0
  47. third_party/PartField/partfield/model/PVCNN/unet_3daware.py +427 -0
  48. third_party/PartField/partfield/model/UNet/buildingblocks.py +546 -0
  49. third_party/PartField/partfield/model/UNet/model.py +170 -0
  50. 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: Spaceflow
3
- emoji: 🏢
4
- colorFrom: blue
5
- colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.30.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
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