├── .gitignore
├── .gitmodules
├── LICENSE.md
├── LICENSE_GS.md
├── README.md
├── arguments
└── __init__.py
├── assets
└── teaser.png
├── bash_scripts
├── m360_indoor
│ ├── eval_bonsai.sh
│ ├── eval_counter.sh
│ ├── eval_kitchen.sh
│ └── eval_room.sh
├── m360_outdoor
│ ├── eval_bicycle.sh
│ ├── eval_flowers.sh
│ ├── eval_garden.sh
│ ├── eval_stump.sh
│ └── eval_treehill.sh
├── run_all.sh
├── tandt
│ ├── eval_train.sh
│ └── eval_truck.sh
└── video_all.sh
├── compile.bat
├── compile.sh
├── convert.py
├── create_off.py
├── create_video.py
├── full_eval.py
├── lpipsPyTorch
├── __init__.py
└── modules
│ ├── lpips.py
│ ├── networks.py
│ └── utils.py
├── mesh.py
├── metrics.py
├── render.py
├── requirements.yaml
├── scene
├── __init__.py
├── cameras.py
├── colmap_loader.py
├── dataset_readers.py
└── triangle_model.py
├── scripts
├── dtu_eval.py
├── eval_dtu
│ ├── eval.py
│ ├── evaluate_single_scene.py
│ └── render_utils.py
├── eval_tnt
│ ├── README.md
│ ├── compute_bbox_for_mesh.py
│ ├── config.py
│ ├── cull_mesh.py
│ ├── evaluate_single_scene.py
│ ├── evaluation.py
│ ├── help_func.py
│ ├── plot.py
│ ├── registration.py
│ ├── requirements.txt
│ ├── run.py
│ ├── trajectory_io.py
│ └── util.py
└── tnt_eval.py
├── train.py
├── train_game_engine.py
├── triangle_renderer
└── __init__.py
└── utils
├── camera_utils.py
├── general_utils.py
├── graphics_utils.py
├── image_utils.py
├── loss_utils.py
├── mesh_utils.py
├── point_utils.py
├── render_utils.py
├── sh_utils.py
└── system_utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | */__pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
110 | .pdm.toml
111 | .pdm-python
112 | .pdm-build/
113 |
114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115 | __pypackages__/
116 |
117 | # Celery stuff
118 | celerybeat-schedule
119 | celerybeat.pid
120 |
121 | # SageMath parsed files
122 | *.sage.py
123 |
124 | # Environments
125 | .env
126 | .venv
127 | env/
128 | venv/
129 | ENV/
130 | env.bak/
131 | venv.bak/
132 |
133 | # Spyder project settings
134 | .spyderproject
135 | .spyproject
136 |
137 | # Rope project settings
138 | .ropeproject
139 |
140 | # mkdocs documentation
141 | /site
142 |
143 | # mypy
144 | .mypy_cache/
145 | .dmypy.json
146 | dmypy.json
147 |
148 | # Pyre type checker
149 | .pyre/
150 |
151 | # pytype static type analyzer
152 | .pytype/
153 |
154 | # Cython debug symbols
155 | cython_debug/
156 |
157 | # PyCharm
158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160 | # and can be added to the global gitignore or merged into this file. For a more nuclear
161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162 | #.idea/
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "submodules/simple-knn"]
2 | path = submodules/simple-knn
3 | url = https://gitlab.inria.fr/bkerbl/simple-knn.git
4 | [submodule "submodules/diff-triangle-rasterization"]
5 | path = submodules/diff-triangle-rasterization
6 | url = https://github.com/trianglesplatting/diff-triangle-rasterization.git
7 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
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 | Copyright (C) 2024, University of Liege, KAUST and University of Oxford
179 |
180 | Licensed under the Apache License, Version 2.0 (the "License");
181 | you may not use this file except in compliance with the License.
182 | You may obtain a copy of the License at
183 |
184 | http://www.apache.org/licenses/LICENSE-2.0
185 |
186 | Unless required by applicable law or agreed to in writing, software
187 | distributed under the License is distributed on an "AS IS" BASIS,
188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189 | See the License for the specific language governing permissions and
190 | limitations under the License.
191 |
--------------------------------------------------------------------------------
/LICENSE_GS.md:
--------------------------------------------------------------------------------
1 | Gaussian-Splatting License
2 | ===========================
3 |
4 | **Inria** and **the Max Planck Institut for Informatik (MPII)** hold all the ownership rights on the *Software* named **gaussian-splatting**.
5 | The *Software* is in the process of being registered with the Agence pour la Protection des
6 | Programmes (APP).
7 |
8 | The *Software* is still being developed by the *Licensor*.
9 |
10 | *Licensor*'s goal is to allow the research community to use, test and evaluate
11 | the *Software*.
12 |
13 | ## 1. Definitions
14 |
15 | *Licensee* means any person or entity that uses the *Software* and distributes
16 | its *Work*.
17 |
18 | *Licensor* means the owners of the *Software*, i.e Inria and MPII
19 |
20 | *Software* means the original work of authorship made available under this
21 | License ie gaussian-splatting.
22 |
23 | *Work* means the *Software* and any additions to or derivative works of the
24 | *Software* that are made available under this License.
25 |
26 |
27 | ## 2. Purpose
28 | This license is intended to define the rights granted to the *Licensee* by
29 | Licensors under the *Software*.
30 |
31 | ## 3. Rights granted
32 |
33 | For the above reasons Licensors have decided to distribute the *Software*.
34 | Licensors grant non-exclusive rights to use the *Software* for research purposes
35 | to research users (both academic and industrial), free of charge, without right
36 | to sublicense.. The *Software* may be used "non-commercially", i.e., for research
37 | and/or evaluation purposes only.
38 |
39 | Subject to the terms and conditions of this License, you are granted a
40 | non-exclusive, royalty-free, license to reproduce, prepare derivative works of,
41 | publicly display, publicly perform and distribute its *Work* and any resulting
42 | derivative works in any form.
43 |
44 | ## 4. Limitations
45 |
46 | **4.1 Redistribution.** You may reproduce or distribute the *Work* only if (a) you do
47 | so under this License, (b) you include a complete copy of this License with
48 | your distribution, and (c) you retain without modification any copyright,
49 | patent, trademark, or attribution notices that are present in the *Work*.
50 |
51 | **4.2 Derivative Works.** You may specify that additional or different terms apply
52 | to the use, reproduction, and distribution of your derivative works of the *Work*
53 | ("Your Terms") only if (a) Your Terms provide that the use limitation in
54 | Section 2 applies to your derivative works, and (b) you identify the specific
55 | derivative works that are subject to Your Terms. Notwithstanding Your Terms,
56 | this License (including the redistribution requirements in Section 3.1) will
57 | continue to apply to the *Work* itself.
58 |
59 | **4.3** Any other use without of prior consent of Licensors is prohibited. Research
60 | users explicitly acknowledge having received from Licensors all information
61 | allowing to appreciate the adequacy between of the *Software* and their needs and
62 | to undertake all necessary precautions for its execution and use.
63 |
64 | **4.4** The *Software* is provided both as a compiled library file and as source
65 | code. In case of using the *Software* for a publication or other results obtained
66 | through the use of the *Software*, users are strongly encouraged to cite the
67 | corresponding publications as explained in the documentation of the *Software*.
68 |
69 | ## 5. Disclaimer
70 |
71 | THE USER CANNOT USE, EXPLOIT OR DISTRIBUTE THE *SOFTWARE* FOR COMMERCIAL PURPOSES
72 | WITHOUT PRIOR AND EXPLICIT CONSENT OF LICENSORS. YOU MUST CONTACT INRIA FOR ANY
73 | UNAUTHORIZED USE: stip-sophia.transfert@inria.fr . ANY SUCH ACTION WILL
74 | CONSTITUTE A FORGERY. THIS *SOFTWARE* IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES
75 | OF ANY NATURE AND ANY EXPRESS OR IMPLIED WARRANTIES, WITH REGARDS TO COMMERCIAL
76 | USE, PROFESSIONNAL USE, LEGAL OR NOT, OR OTHER, OR COMMERCIALISATION OR
77 | ADAPTATION. UNLESS EXPLICITLY PROVIDED BY LAW, IN NO EVENT, SHALL INRIA OR THE
78 | AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
79 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
80 | GOODS OR SERVICES, LOSS OF USE, DATA, OR PROFITS OR BUSINESS INTERRUPTION)
81 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
82 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING FROM, OUT OF OR
83 | IN CONNECTION WITH THE *SOFTWARE* OR THE USE OR OTHER DEALINGS IN THE *SOFTWARE*.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Triangle Splatting for Real-Time Radiance Field Rendering
2 |
3 |
7 |
8 |
9 |
10 | Jan Held*, Renaud Vandeghen*, Adrien Deliege, Abdullah Hamdi, Silvio Giancola, Anthony Cioppa, Andrea Vedaldi, Bernard Ghanem, Andrea Tagliasacchi, Marc Van Droogenbroeck
11 |
12 |
13 |
14 |
15 |
16 |

17 |
18 |
19 |
20 | This repo contains the official implementation for the paper "Triangle Splatting for Real-Time Radiance Field Rendering".
21 |
22 | Our work represents a significant advancement in radiance field rendering by introducing 3D triangles as rendering primitive. By leveraging the same primitive used in classical mesh representations, our method bridges the gap between neural rendering and traditional graphics pipelines. Triangle Splatting offers a compelling alternative to volumetric and implicit methods, achieving high visual fidelity with faster rendering performance. These results establish Triangle Splatting as a promising step toward mesh-aware neural rendering, unifying decades of GPU-accelerated graphics with modern differentiable frameworks.
23 |
24 | ## Cloning the Repository + Installation
25 |
26 | The code has been used and tested with Python 3.11 and CUDA 12.6.
27 |
28 | You should clone the repository with the different submodules by running the following command:
29 |
30 | ```bash
31 | git clone https://github.com/trianglesplatting/triangle-splatting --recursive
32 | cd triangle-splatting
33 | ```
34 |
35 | Then, we suggest to use a virtual environment to install the dependencies.
36 |
37 | ```bash
38 | micromamba create -f requirements.yaml
39 | ```
40 |
41 | Finally, you can compile the custom CUDA kernels by running the following command:
42 |
43 | ```bash
44 | bash compile.sh
45 | cd submodules/simple-knn
46 | pip install .
47 | ```
48 |
49 | ## Training
50 | To train our model, you can use the following command:
51 | ```bash
52 | python train.py -s -m --eval
53 | ```
54 |
55 | If you want to train the model on outdoor scenes, you should add the following command:
56 | ```bash
57 | python train.py -s -m --outdoor --eval
58 | ```
59 |
60 | ## Rendering
61 | To render a scene, you can use the following command:
62 | ```bash
63 | python render.py -m
64 | ```
65 |
66 | ## Evaluation
67 | To evaluate the model, you can use the following command:
68 | ```bash
69 | python metrics.py -m
70 | ```
71 |
72 | ## Video
73 | To render a video, you can use the following command:
74 | ```bash
75 | python create_video.py -m
76 | ```
77 |
78 | ## Replication of the results
79 | To replicate the results of our paper, you can use the following command:
80 | ```bash
81 | python full_eval.py --output_path -m360 -tat
82 | ```
83 |
84 | ## Game engine
85 | To create your own .off file:
86 |
87 | 1. Train your scene using ```train_game_engine.py```. This version includes some modifications, such as pruning low-opacity triangles and applying an additional loss in the final training iterations to encourage higher opacity. This makes the result more compatible with how game engines render geometry. These modifications are experimental, so feel free to adjust them or try your own variants. (For example, increasing the normal loss often improves quality by making triangles better aligned and reducing black holes.)
88 |
89 | 2. Run ```create_off.py``` to convert the optimized triangles into a .off file that can be imported into a game engine. You only need to provide the path to the trained model (e.g., point_cloud_state_dict.pt) and specify the desired output file name (e.g., mesh_colored.off).
90 |
91 | Note: The script generates fully opaque triangles. If you want to include per-triangle opacity, you can extract and activate the raw opacity values using:
92 | ```
93 | opacity_raw = sd["opacity"]
94 | opacity = torch.sigmoid(opacity_raw.view(-1))
95 | opacity_uint8 = (opacity * 255).to(torch.uint8)
96 | ```
97 | Each triangle has a single opacity value, so if needed, assign the same value to all three of its vertices when exporting with:
98 | ```
99 | for i, face in enumerate(faces):
100 | r, g, b = colors[i].tolist()
101 | a = opacity_uint8[i].item()
102 | f.write(f"3 {face[0].item()} {face[1].item()} {face[2].item()} {r} {g} {b} {a}\n")
103 | ```
104 |
105 | If you want to run some pretrained scene on a game engine for yourself, you can download the *Garden* and *Room* scenes from the [following link](https://drive.google.com/drive/folders/1_TMXEFTdEACpHHvsmc5UeZMM-cMgJ3xW?usp=sharing).
106 |
107 |
108 | ## Acknowledgements
109 | This project is built upon 3D Convex Splatting and 3D Gaussian Splatting. We want to thank the authors for their contributions.
110 |
111 | J. Held and A. Cioppa are funded by the F.R.S.-FNRS. The research reported in this publication was supported by funding from KAUST Center of Excellence on GenAI, under award number 5940. This work was also supported by KAUST Ibn Rushd Postdoc Fellowship program. The present research benefited from computational resources made available on Lucia, the Tier-1 supercomputer of the Walloon Region, infrastructure funded by the Walloon Region under the grant agreement n°1910247.
112 |
113 |
114 | ## BibTeX
115 | ```bibtex
116 | @article{Held2025Triangle,
117 | title = {Triangle Splatting for Real-Time Radiance Field Rendering},
118 | author = {Held, Jan and Vandeghen, Renaud and Deliege, Adrien and Hamdi, Abdullah and Cioppa, Anthony and Giancola, Silvio and Vedaldi, Andrea and Ghanem, Bernard and Tagliasacchi, Andrea and Van Droogenbroeck, Marc},
119 | journal = {arXiv},
120 | year = {2025},
121 | }
122 | ```
123 |
--------------------------------------------------------------------------------
/arguments/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2023, Inria
4 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
5 | # All rights reserved.
6 | #
7 | # This software is free for non-commercial, research and evaluation use
8 | # under the terms of the LICENSE_GS.md file.
9 | #
10 | # For inquiries contact george.drettakis@inria.fr
11 | #
12 | # The modifications of the code are under the following copyright:
13 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
14 | # TELIM research group, http://www.telecom.ulg.ac.be/
15 | # IVUL research group, https://ivul.kaust.edu.sa/
16 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
17 | # All rights reserved.
18 | # The modifications are under the LICENSE.md file.
19 | #
20 | # For inquiries contact jan.held@uliege.be
21 | #
22 |
23 | from argparse import ArgumentParser, Namespace
24 | import sys
25 | import os
26 |
27 | class GroupParams:
28 | pass
29 |
30 | class ParamGroup:
31 | def __init__(self, parser: ArgumentParser, name : str, fill_none = False):
32 | group = parser.add_argument_group(name)
33 | for key, value in vars(self).items():
34 | shorthand = False
35 | if key.startswith("_"):
36 | shorthand = True
37 | key = key[1:]
38 | t = type(value)
39 | value = value if not fill_none else None
40 | if shorthand:
41 | if t == bool:
42 | group.add_argument("--" + key, ("-" + key[0:1]), default=value, action="store_true")
43 | else:
44 | group.add_argument("--" + key, ("-" + key[0:1]), default=value, type=t)
45 | else:
46 | if t == bool:
47 | group.add_argument("--" + key, default=value, action="store_true")
48 | else:
49 | group.add_argument("--" + key, default=value, type=t)
50 |
51 | def extract(self, args):
52 | group = GroupParams()
53 | for arg in vars(args).items():
54 | if arg[0] in vars(self) or ("_" + arg[0]) in vars(self):
55 | setattr(group, arg[0], arg[1])
56 | return group
57 |
58 | class ModelParams(ParamGroup):
59 | def __init__(self, parser, sentinel=False):
60 | self.sh_degree = 3
61 | self._source_path = ""
62 | self._model_path = ""
63 | self._images = "images"
64 | self._resolution = -1
65 | self._white_background = False
66 | self.data_device = "cuda"
67 | self.eval = False
68 | super().__init__(parser, "Loading Parameters", sentinel)
69 |
70 | def extract(self, args):
71 | g = super().extract(args)
72 | g.source_path = os.path.abspath(g.source_path)
73 | return g
74 |
75 | class PipelineParams(ParamGroup):
76 | def __init__(self, parser):
77 | self.convert_SHs_python = False
78 | self.compute_cov3D_python = False
79 | self.depth_ratio = 1.0
80 | self.debug = False
81 | super().__init__(parser, "Pipeline Parameters")
82 |
83 | class OptimizationParams(ParamGroup):
84 | def __init__(self, parser):
85 | self.iterations = 30_000
86 | self.position_lr_delay_mult = 0.01
87 | self.position_lr_max_steps = 30_000
88 | self.feature_lr = 0.0025
89 | self.opacity_lr = 0.014
90 | self.lambda_dssim = 0.2
91 |
92 | self.densification_interval = 500
93 |
94 | self.densify_from_iter = 500
95 | self.densify_until_iter = 25000
96 |
97 | self.random_background = False
98 | self.mask_threshold = 0.01
99 | self.lr_mask = 0.01
100 |
101 | self.nb_points = 3
102 | self.triangle_size = 2.23
103 | self.set_opacity = 0.28
104 | self.set_sigma = 1.16
105 |
106 | self.noise_lr = 5e5
107 | self.mask_dead = 0.08
108 | self.lambda_normals = 0.0001
109 | self.lambda_dist = 0.0
110 | self.lambda_opacity = 0.0055
111 | self.lambda_size = 0.00000001
112 | self.opacity_dead = 0.014
113 | self.importance_threshold = 0.022
114 | self.iteration_mesh = 5000
115 |
116 | self.cloning_sigma = 1.0
117 | self.cloning_opacity = 1.0
118 | self.lr_sigma = 0.0008
119 | self.lr_triangles_points_init = 0.0018
120 |
121 | self.proba_distr = 2 # 0 is based on opacity, 1 is based on sigma and 2 is alternating
122 | self.split_size = 24.0
123 | self.start_lr_sigma = 0
124 | self.max_noise_factor = 1.5
125 |
126 | self.max_shapes = 4000000
127 |
128 | self.add_shape = 1.3
129 |
130 | self.p = 1.6
131 |
132 | super().__init__(parser, "Optimization Parameters")
133 |
134 | def get_combined_args(parser : ArgumentParser):
135 | cmdlne_string = sys.argv[1:]
136 | cfgfile_string = "Namespace()"
137 | args_cmdline = parser.parse_args(cmdlne_string)
138 |
139 | try:
140 | cfgfilepath = os.path.join(args_cmdline.model_path, "cfg_args")
141 | print("Looking for config file in", cfgfilepath)
142 | with open(cfgfilepath) as cfg_file:
143 | print("Config file found: {}".format(cfgfilepath))
144 | cfgfile_string = cfg_file.read()
145 | except TypeError:
146 | print("Config file not found at")
147 | pass
148 | args_cfgfile = eval(cfgfile_string)
149 |
150 | merged_dict = vars(args_cfgfile).copy()
151 | for k,v in vars(args_cmdline).items():
152 | if v != None:
153 | merged_dict[k] = v
154 | return Namespace(**merged_dict)
--------------------------------------------------------------------------------
/assets/teaser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trianglesplatting/triangle-splatting/bcde2448c36e672f1a69548faa5651c68626df93/assets/teaser.png
--------------------------------------------------------------------------------
/bash_scripts/m360_indoor/eval_bonsai.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #SBATCH --output=logs/eval_bonsai.log
3 | #SBATCH --partition=gpu
4 | #SBATCH --nodes=1
5 | #SBATCH --ntasks-per-node=1
6 | #SBATCH --cpus-per-task=8
7 | #SBATCH --gpus=1
8 | #SBATCH --mem-per-gpu=30G # Memory to allocate in MB per allocated CPU core
9 | #SBATCH --time="0-00:50:00" # Max execution time
10 |
11 |
12 | micromamba activate triangle_splatting
13 |
14 | python train.py \
15 | -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/bonsai \
16 | -i images_2 \
17 | -m models/$1/bonsai \
18 | --quiet \
19 | --eval \
20 | --max_shapes 3000000 \
21 | --importance_threshold 0.025 \
22 | --lr_sigma 0.0008 \
23 | --opacity_lr 0.014 \
24 | --lambda_normals 0.00004 \
25 | --lambda_dist 1 \
26 | --iteration_mesh 5000 \
27 | --lambda_opacity 0.0055 \
28 | --lambda_dssim 0.4 \
29 | --lr_triangles_points_init 0.0015 \
30 | --lambda_size 5e-8
31 |
32 |
33 | python render.py --iteration 30000 -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/bonsai -m models/$1/bonsai --eval --skip_train --quiet
34 |
35 | python metrics.py -m models/$1/bonsai
--------------------------------------------------------------------------------
/bash_scripts/m360_indoor/eval_counter.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #SBATCH --output=logs/eval_counter.log
3 | #SBATCH --partition=gpu
4 | #SBATCH --nodes=1
5 | #SBATCH --ntasks-per-node=1
6 | #SBATCH --cpus-per-task=8
7 | #SBATCH --gpus=1
8 | #SBATCH --mem-per-gpu=30G # Memory to allocate in MB per allocated CPU core
9 | #SBATCH --time="0-00:50:00" # Max execution time
10 |
11 |
12 | micromamba activate triangle_splatting
13 |
14 | python train.py \
15 | -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/counter \
16 | -i images_2 \
17 | -m models/$1/counter \
18 | --quiet \
19 | --eval \
20 | --max_shapes 2500000 \
21 | --importance_threshold 0.025 \
22 | --lr_sigma 0.0008 \
23 | --opacity_lr 0.014 \
24 | --lambda_normals 0.00004 \
25 | --lambda_dist 1 \
26 | --iteration_mesh 5000 \
27 | --lambda_opacity 0.0055 \
28 | --lambda_dssim 0.4 \
29 | --lr_triangles_points_init 0.0015 \
30 | --lambda_size 5e-8
31 |
32 |
33 | python render.py --iteration 30000 -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/counter -m models/$1/counter --eval --skip_train --quiet
34 |
35 | python metrics.py -m models/$1/counter
--------------------------------------------------------------------------------
/bash_scripts/m360_indoor/eval_kitchen.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #SBATCH --output=logs/eval_kitchen.log
3 | #SBATCH --partition=gpu
4 | #SBATCH --nodes=1
5 | #SBATCH --ntasks-per-node=1
6 | #SBATCH --cpus-per-task=8
7 | #SBATCH --gpus=1
8 | #SBATCH --mem-per-gpu=30G # Memory to allocate in MB per allocated CPU core
9 | #SBATCH --time="0-00:50:00" # Max execution time
10 |
11 |
12 | micromamba activate triangle_splatting
13 |
14 | python train.py \
15 | -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/kitchen \
16 | -i images_2 \
17 | -m models/$1/kitchen \
18 | --quiet \
19 | --eval \
20 | --max_shapes 2400000 \
21 | --importance_threshold 0.025 \
22 | --lr_sigma 0.0008 \
23 | --opacity_lr 0.014 \
24 | --lambda_normals 0.00004 \
25 | --lambda_dist 1 \
26 | --iteration_mesh 5000 \
27 | --lambda_opacity 0.0055 \
28 | --lambda_dssim 0.4 \
29 | --lr_triangles_points_init 0.0015 \
30 | --lambda_size 5e-8
31 |
32 |
33 | python render.py --iteration 30000 -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/kitchen -m models/$1/kitchen --eval --skip_train --quiet
34 |
35 | python metrics.py -m models/$1/kitchen
--------------------------------------------------------------------------------
/bash_scripts/m360_indoor/eval_room.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #SBATCH --output=logs/eval_room.log
3 | #SBATCH --partition=gpu
4 | #SBATCH --nodes=1
5 | #SBATCH --ntasks-per-node=1
6 | #SBATCH --cpus-per-task=8
7 | #SBATCH --gpus=1
8 | #SBATCH --mem-per-gpu=30G # Memory to allocate in MB per allocated CPU core
9 | #SBATCH --time="0-00:50:00" # Max execution time
10 |
11 |
12 | micromamba activate triangle_splatting
13 |
14 | python train.py \
15 | -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/room \
16 | -i images_2 \
17 | -m models/$1/room \
18 | --quiet \
19 | --eval \
20 | --max_shapes 2100000 \
21 | --importance_threshold 0.025 \
22 | --lr_sigma 0.0008 \
23 | --opacity_lr 0.014 \
24 | --lambda_normals 0.00004 \
25 | --lambda_dist 1 \
26 | --iteration_mesh 5000 \
27 | --lambda_opacity 0.0055 \
28 | --lambda_dssim 0.4 \
29 | --lr_triangles_points_init 0.0015 \
30 | --lambda_size 5e-8
31 |
32 |
33 | python render.py --iteration 30000 -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/room -m models/$1/room --eval --skip_train --quiet
34 |
35 | python metrics.py -m models/$1/room
--------------------------------------------------------------------------------
/bash_scripts/m360_outdoor/eval_bicycle.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #SBATCH --output=logs/eval_bicycle.log
3 | #SBATCH --partition=gpu
4 | #SBATCH --nodes=1
5 | #SBATCH --ntasks-per-node=1
6 | #SBATCH --cpus-per-task=8
7 | #SBATCH --gpus=1
8 | #SBATCH --mem-per-gpu=30G # Memory to allocate in MB per allocated CPU core
9 | #SBATCH --time="0-00:55:00" # Max execution time
10 |
11 |
12 | micromamba activate triangle_splatting
13 |
14 | python train.py \
15 | -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/bicycle \
16 | -i images_4 \
17 | -m models/$1/bicycle \
18 | --quiet \
19 | --eval \
20 | --max_shapes 6400000 \
21 | --outdoor \
22 |
23 |
24 | python render.py --iteration 30000 -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/bicycle -m models/$1/bicycle --eval --skip_train --quiet
25 |
26 | python metrics.py -m models/$1/bicycle
--------------------------------------------------------------------------------
/bash_scripts/m360_outdoor/eval_flowers.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #SBATCH --output=logs/eval_flowers.log
3 | #SBATCH --partition=gpu
4 | #SBATCH --nodes=1
5 | #SBATCH --ntasks-per-node=1
6 | #SBATCH --cpus-per-task=8
7 | #SBATCH --gpus=1
8 | #SBATCH --mem-per-gpu=30G # Memory to allocate in MB per allocated CPU core
9 | #SBATCH --time="0-00:50:00" # Max execution time
10 |
11 |
12 | micromamba activate triangle_splatting
13 |
14 | python train.py \
15 | -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/flowers \
16 | -i images_4 \
17 | -m models/$1/flowers \
18 | --quiet \
19 | --eval \
20 | --max_shapes 5500000 \
21 | --outdoor \
22 |
23 |
24 | python render.py --iteration 30000 -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/flowers -m models/$1/flowers --eval --skip_train --quiet
25 |
26 | python metrics.py -m models/$1/flowers
--------------------------------------------------------------------------------
/bash_scripts/m360_outdoor/eval_garden.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #SBATCH --output=logs/eval_garden.log
3 | #SBATCH --partition=gpu
4 | #SBATCH --nodes=1
5 | #SBATCH --ntasks-per-node=1
6 | #SBATCH --cpus-per-task=8
7 | #SBATCH --gpus=1
8 | #SBATCH --mem-per-gpu=30G # Memory to allocate in MB per allocated CPU core
9 | #SBATCH --time="0-00:55:00" # Max execution time
10 |
11 |
12 | micromamba activate triangle_splatting
13 |
14 | python train.py \
15 | -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/garden \
16 | -i images_4 \
17 | -m models/$1/garden \
18 | --quiet \
19 | --eval \
20 | --max_shapes 5200000 \
21 | --outdoor \
22 |
23 |
24 | python render.py --iteration 30000 -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/garden -m models/$1/garden --eval --skip_train --quiet
25 |
26 | python metrics.py -m models/$1/garden
--------------------------------------------------------------------------------
/bash_scripts/m360_outdoor/eval_stump.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #SBATCH --output=logs/eval_stump.log
3 | #SBATCH --partition=gpu
4 | #SBATCH --nodes=1
5 | #SBATCH --ntasks-per-node=1
6 | #SBATCH --cpus-per-task=8
7 | #SBATCH --gpus=1
8 | #SBATCH --mem-per-gpu=30G # Memory to allocate in MB per allocated CPU core
9 | #SBATCH --time="0-00:50:00" # Max execution time
10 |
11 |
12 | micromamba activate triangle_splatting
13 |
14 | python train.py \
15 | -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/stump \
16 | -i images_4 \
17 | -m models/$1/stump \
18 | --quiet \
19 | --eval \
20 | --max_shapes 4750000 \
21 | --outdoor \
22 |
23 |
24 | python render.py --iteration 30000 -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/stump -m models/$1/stump --eval --skip_train --quiet
25 |
26 | python metrics.py -m models/$1/stump
--------------------------------------------------------------------------------
/bash_scripts/m360_outdoor/eval_treehill.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #SBATCH --output=logs/eval_treehill.log
3 | #SBATCH --partition=gpu
4 | #SBATCH --nodes=1
5 | #SBATCH --ntasks-per-node=1
6 | #SBATCH --cpus-per-task=8
7 | #SBATCH --gpus=1
8 | #SBATCH --mem-per-gpu=30G # Memory to allocate in MB per allocated CPU core
9 | #SBATCH --time="0-00:50:00" # Max execution time
10 |
11 |
12 | micromamba activate triangle_splatting
13 |
14 | python train.py \
15 | -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/treehill \
16 | -i images_4 \
17 | -m models/$1/treehill \
18 | --quiet \
19 | --eval \
20 | --max_shapes 5000000 \
21 | --outdoor \
22 |
23 |
24 | python render.py --iteration 30000 -s /gpfs/scratch/acad/telim/datasets/MipNeRF360/treehill -m models/$1/treehill --eval --skip_train --quiet
25 |
26 | python metrics.py -m models/$1/treehill
--------------------------------------------------------------------------------
/bash_scripts/run_all.sh:
--------------------------------------------------------------------------------
1 | # tandt
2 | sbatch bash_scripts/tandt/eval_truck.sh $1
3 | sbatch bash_scripts/tandt/eval_train.sh $1
4 |
5 | # m360 indoor
6 | sbatch bash_scripts/m360_indoor/eval_room.sh $1
7 | sbatch bash_scripts/m360_indoor/eval_counter.sh $1
8 | sbatch bash_scripts/m360_indoor/eval_kitchen.sh $1
9 | sbatch bash_scripts/m360_indoor/eval_bonsai.sh $1
10 |
11 | # m360 outdoor
12 | sbatch bash_scripts/m360_outdoor/eval_bicycle.sh $1
13 | sbatch bash_scripts/m360_outdoor/eval_flowers.sh $1
14 | sbatch bash_scripts/m360_outdoor/eval_garden.sh $1
15 | sbatch bash_scripts/m360_outdoor/eval_stump.sh $1
16 | sbatch bash_scripts/m360_outdoor/eval_treehill.sh $1
--------------------------------------------------------------------------------
/bash_scripts/tandt/eval_train.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #SBATCH --output=logs/eval_train.log
3 | #SBATCH --partition=gpu
4 | #SBATCH --nodes=1
5 | #SBATCH --ntasks-per-node=1
6 | #SBATCH --cpus-per-task=8
7 | #SBATCH --gpus=1
8 | #SBATCH --mem-per-gpu=30G # Memory to allocate in MB per allocated CPU core
9 | #SBATCH --time="0-00:40:00" # Max execution time
10 |
11 |
12 | micromamba activate triangle_splatting
13 |
14 | python train.py \
15 | -s /gpfs/scratch/acad/telim/datasets/tandt/train/ \
16 | -m models/$1/train \
17 | --quiet \
18 | --eval \
19 | --max_shapes 2500000 \
20 | --outdoor \
21 |
22 |
23 | python render.py --iteration 30000 -s /gpfs/scratch/acad/telim/datasets/tandt/train/ -m models/$1/train --eval --skip_train --quiet
24 |
25 | python metrics.py -m models/$1/train
--------------------------------------------------------------------------------
/bash_scripts/tandt/eval_truck.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #SBATCH --output=logs/eval_truck.log
3 | #SBATCH --partition=gpu
4 | #SBATCH --nodes=1
5 | #SBATCH --ntasks-per-node=1
6 | #SBATCH --cpus-per-task=8
7 | #SBATCH --gpus=1
8 | #SBATCH --mem-per-gpu=30G # Memory to allocate in MB per allocated CPU core
9 | #SBATCH --time="0-00:40:00" # Max execution time
10 |
11 |
12 | micromamba activate triangle_splatting
13 |
14 | python train.py \
15 | -s /gpfs/scratch/acad/telim/datasets/tandt/truck/ \
16 | -m models/$1/truck \
17 | --quiet \
18 | --eval \
19 | --max_shapes 2000000 \
20 | --outdoor \
21 |
22 |
23 | python render.py --iteration 30000 -s /gpfs/scratch/acad/telim/datasets/tandt/truck/ -m models/$1/truck --eval --skip_train --quiet
24 |
25 | python metrics.py -m models/$1/truck
--------------------------------------------------------------------------------
/bash_scripts/video_all.sh:
--------------------------------------------------------------------------------
1 | # tandt
2 | python create_video.py -m models/$1/truck
3 | python create_video.py -m models/$1/train
4 |
5 | # db
6 | python create_video.py -m models/$1/drjohnson
7 | python create_video.py -m models/$1/playroom
8 |
9 | # m360 indoor
10 | python create_video.py -m models/$1/room
11 | python create_video.py -m models/$1/counter
12 | python create_video.py -m models/$1/kitchen
13 | python create_video.py -m models/$1/bonsai
14 |
15 | # m360 outdoor
16 | python create_video.py -m models/$1/bicycle
17 | python create_video.py -m models/$1/flowers
18 | python create_video.py -m models/$1/garden
19 | python create_video.py -m models/$1/stump
20 | python create_video.py -m models/$1/treehill
--------------------------------------------------------------------------------
/compile.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | cd submodules\diff-convex-rasterization
4 |
5 | :: Delete build and egg-info folders if they exist
6 | if exist build rmdir /s /q build
7 | if exist diff_triangle_rasterization.egg-info rmdir /s /q diff_triangle_rasterization.egg-info
8 |
9 | pip install .
10 |
11 | cd ..\..
12 |
--------------------------------------------------------------------------------
/compile.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cd submodules/diff-triangle-rasterization/
4 |
5 | # # Delete the build, diff_triangle_rasterization.egg-info, and dist folders if they exist
6 | rm -rf build
7 | rm -rf diff_triangle_rasterization.egg-info
8 |
9 | pip install .
10 |
11 | cd ..
12 | cd ..
--------------------------------------------------------------------------------
/convert.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2023, Inria
3 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
4 | # All rights reserved.
5 | #
6 | # This software is free for non-commercial, research and evaluation use
7 | # under the terms of the LICENSE.md file.
8 | #
9 | # For inquiries contact george.drettakis@inria.fr
10 | #
11 |
12 | import os
13 | import logging
14 | from argparse import ArgumentParser
15 | import shutil
16 |
17 | # This Python script is based on the shell converter script provided in the MipNerF 360 repository.
18 | parser = ArgumentParser("Colmap converter")
19 | parser.add_argument("--no_gpu", action='store_true')
20 | parser.add_argument("--skip_matching", action='store_true')
21 | parser.add_argument("--source_path", "-s", required=True, type=str)
22 | parser.add_argument("--camera", default="OPENCV", type=str)
23 | parser.add_argument("--colmap_executable", default="", type=str)
24 | parser.add_argument("--resize", action="store_true")
25 | parser.add_argument("--magick_executable", default="", type=str)
26 | args = parser.parse_args()
27 | colmap_command = '"{}"'.format(args.colmap_executable) if len(args.colmap_executable) > 0 else "colmap"
28 | magick_command = '"{}"'.format(args.magick_executable) if len(args.magick_executable) > 0 else "magick"
29 | use_gpu = 1 if not args.no_gpu else 0
30 |
31 | if not args.skip_matching:
32 | os.makedirs(args.source_path + "/distorted/sparse", exist_ok=True)
33 |
34 | ## Feature extraction
35 | feat_extracton_cmd = colmap_command + " feature_extractor "\
36 | "--database_path " + args.source_path + "/distorted/database.db \
37 | --image_path " + args.source_path + "/input \
38 | --ImageReader.single_camera 1 \
39 | --ImageReader.camera_model " + args.camera + " \
40 | --SiftExtraction.use_gpu " + str(use_gpu)
41 | exit_code = os.system(feat_extracton_cmd)
42 | if exit_code != 0:
43 | logging.error(f"Feature extraction failed with code {exit_code}. Exiting.")
44 | exit(exit_code)
45 |
46 | ## Feature matching
47 | feat_matching_cmd = colmap_command + " exhaustive_matcher \
48 | --database_path " + args.source_path + "/distorted/database.db \
49 | --SiftMatching.use_gpu " + str(use_gpu)
50 | exit_code = os.system(feat_matching_cmd)
51 | if exit_code != 0:
52 | logging.error(f"Feature matching failed with code {exit_code}. Exiting.")
53 | exit(exit_code)
54 |
55 | ### Bundle adjustment
56 | # The default Mapper tolerance is unnecessarily large,
57 | # decreasing it speeds up bundle adjustment steps.
58 | mapper_cmd = (colmap_command + " mapper \
59 | --database_path " + args.source_path + "/distorted/database.db \
60 | --image_path " + args.source_path + "/input \
61 | --output_path " + args.source_path + "/distorted/sparse \
62 | --Mapper.ba_global_function_tolerance=0.000001")
63 | exit_code = os.system(mapper_cmd)
64 | if exit_code != 0:
65 | logging.error(f"Mapper failed with code {exit_code}. Exiting.")
66 | exit(exit_code)
67 |
68 | ### Image undistortion
69 | ## We need to undistort our images into ideal pinhole intrinsics.
70 | img_undist_cmd = (colmap_command + " image_undistorter \
71 | --image_path " + args.source_path + "/input \
72 | --input_path " + args.source_path + "/distorted/sparse/0 \
73 | --output_path " + args.source_path + "\
74 | --output_type COLMAP")
75 | exit_code = os.system(img_undist_cmd)
76 | if exit_code != 0:
77 | logging.error(f"Mapper failed with code {exit_code}. Exiting.")
78 | exit(exit_code)
79 |
80 | files = os.listdir(args.source_path + "/sparse")
81 | os.makedirs(args.source_path + "/sparse/0", exist_ok=True)
82 | # Copy each file from the source directory to the destination directory
83 | for file in files:
84 | if file == '0':
85 | continue
86 | source_file = os.path.join(args.source_path, "sparse", file)
87 | destination_file = os.path.join(args.source_path, "sparse", "0", file)
88 | shutil.move(source_file, destination_file)
89 |
90 | if(args.resize):
91 | print("Copying and resizing...")
92 |
93 | # Resize images.
94 | os.makedirs(args.source_path + "/images_2", exist_ok=True)
95 | os.makedirs(args.source_path + "/images_4", exist_ok=True)
96 | os.makedirs(args.source_path + "/images_8", exist_ok=True)
97 | # Get the list of files in the source directory
98 | files = os.listdir(args.source_path + "/images")
99 | # Copy each file from the source directory to the destination directory
100 | for file in files:
101 | source_file = os.path.join(args.source_path, "images", file)
102 |
103 | destination_file = os.path.join(args.source_path, "images_2", file)
104 | shutil.copy2(source_file, destination_file)
105 | exit_code = os.system(magick_command + " mogrify -resize 50% " + destination_file)
106 | if exit_code != 0:
107 | logging.error(f"50% resize failed with code {exit_code}. Exiting.")
108 | exit(exit_code)
109 |
110 | destination_file = os.path.join(args.source_path, "images_4", file)
111 | shutil.copy2(source_file, destination_file)
112 | exit_code = os.system(magick_command + " mogrify -resize 25% " + destination_file)
113 | if exit_code != 0:
114 | logging.error(f"25% resize failed with code {exit_code}. Exiting.")
115 | exit(exit_code)
116 |
117 | destination_file = os.path.join(args.source_path, "images_8", file)
118 | shutil.copy2(source_file, destination_file)
119 | exit_code = os.system(magick_command + " mogrify -resize 12.5% " + destination_file)
120 | if exit_code != 0:
121 | logging.error(f"12.5% resize failed with code {exit_code}. Exiting.")
122 | exit(exit_code)
123 |
124 | print("Done.")
--------------------------------------------------------------------------------
/create_off.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import numpy as np
3 | from utils.sh_utils import eval_sh
4 | import argparse
5 |
6 | def main():
7 | parser = argparse.ArgumentParser(description="Convert a checkpoint to a colored OFF file.")
8 | parser.add_argument("--checkpoint_path", type=str, help="Path to the input checkpoint file (e.g., point_cloud_state_dict.pt)")
9 | parser.add_argument("--output_name", type=str, help="Name of the output OFF file (e.g., mesh_colored.off)")
10 | args = parser.parse_args()
11 |
12 | sd = torch.load(args.checkpoint_path, map_location="cpu", weights_only=False)
13 |
14 | points = sd["triangles_points"]
15 | features_dc = sd["features_dc"]
16 | features_rest = sd["features_rest"]
17 |
18 | features = torch.cat((features_dc, features_rest), dim=1)
19 |
20 | num_coeffs = features.shape[1]
21 | max_sh_degree = int(np.sqrt(num_coeffs) - 1)
22 |
23 | shs = features.permute(0, 2, 1).contiguous()
24 |
25 | centroids = points.mean(dim=1)
26 | camera_center = torch.zeros(3)
27 | dirs = centroids - camera_center
28 | dirs_norm = dirs / dirs.norm(dim=1, keepdim=True)
29 |
30 | rgb = eval_sh(max_sh_degree, shs, dirs_norm)
31 |
32 | colors_f = torch.clamp(rgb + 0.5, 0.0, 1.0)
33 | colors = (colors_f * 255).to(torch.uint8)
34 |
35 | all_verts = points.reshape(-1, 3)
36 | unique_verts, inv_idx = torch.unique(all_verts, dim=0, return_inverse=True)
37 | faces = inv_idx.reshape(-1, 3)
38 |
39 | with open(args.output_name, "w") as f:
40 | f.write("COFF\n")
41 | f.write(f"{len(unique_verts)} {len(faces)} 0\n")
42 | for v in unique_verts:
43 | f.write(f"{v[0].item()} {v[1].item()} {v[2].item()}\n")
44 | for i, face in enumerate(faces):
45 | r, g, b = colors[i].tolist()
46 | f.write(f"3 {face[0].item()} {face[1].item()} {face[2].item()} {r} {g} {b} 255\n")
47 |
48 | print(f"saved {args.output_name}")
49 |
50 | if __name__ == "__main__":
51 | main()
--------------------------------------------------------------------------------
/create_video.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2024, Inria, University of Liege, KAUST and University of Oxford
3 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
4 | # TELIM research group, http://www.telecom.ulg.ac.be/
5 | # IVUL research group, https://ivul.kaust.edu.sa/
6 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
7 | # All rights reserved.
8 | #
9 | # This software is free for non-commercial, research and evaluation use
10 | # under the terms of the LICENSE.md file.
11 | #
12 | # For inquiries contact jan.held@uliege.be
13 | #
14 |
15 |
16 | import torch
17 | from scene import Scene
18 | import os
19 | from tqdm import tqdm
20 | from os import makedirs
21 | from triangle_renderer import render
22 | import torchvision
23 | from argparse import ArgumentParser
24 | from arguments import ModelParams, PipelineParams, get_combined_args
25 | from triangle_renderer import TriangleModel
26 | import numpy as np
27 | from utils.render_utils import generate_path, create_videos
28 | import cv2
29 | from PIL import Image
30 |
31 | # --- Helper for progressive zoom trajectory ---
32 | def generate_zoom_trajectory(viewpoint_cameras, n_frames=480, zoom_start=0, zoom_duration=120, zoom_intensity=2.0):
33 | """
34 | Generate a camera trajectory with a progressive zoom in and out.
35 | zoom_start: frame index to start zoom in
36 | zoom_duration: number of frames for zoom in (zoom out will be symmetric)
37 | zoom_intensity: factor to multiply the focal length at max zoom
38 | """
39 | import copy
40 | traj = generate_path(viewpoint_cameras, n_frames=n_frames)
41 | # Get original focal length from the first camera
42 | cam0 = viewpoint_cameras[0]
43 | orig_fovx = cam0.FoVx
44 | orig_fovy = cam0.FoVy
45 | orig_focalx = cam0.image_width / (2 * np.tan(orig_fovx / 2))
46 | orig_focaly = cam0.image_height / (2 * np.tan(orig_fovy / 2))
47 | # Compute new focal for each frame
48 | for i, cam in enumerate(traj):
49 | cam = copy.deepcopy(cam)
50 | # Zoom in
51 | if zoom_start <= i < zoom_start + zoom_duration:
52 | t = (i - zoom_start) / max(zoom_duration - 1, 1)
53 | zoom_factor = 1 + t * (zoom_intensity - 1)
54 | # Zoom out
55 | elif zoom_start + zoom_duration <= i < zoom_start + 2 * zoom_duration:
56 | t = (i - (zoom_start + zoom_duration)) / max(zoom_duration - 1, 1)
57 | zoom_factor = zoom_intensity - t * (zoom_intensity - 1)
58 | else:
59 | zoom_factor = 1.0
60 | # Update focal length and FoV
61 | new_focalx = orig_focalx * zoom_factor
62 | new_focaly = orig_focaly * zoom_factor
63 | new_fovx = 2 * np.arctan(cam.image_width / (2 * new_focalx))
64 | new_fovy = 2 * np.arctan(cam.image_height / (2 * new_focaly))
65 | cam.FoVx = new_fovx
66 | cam.FoVy = new_fovy
67 | # Update projection matrix
68 | from utils.graphics_utils import getProjectionMatrix
69 | cam.projection_matrix = getProjectionMatrix(znear=cam.znear, zfar=cam.zfar, fovX=new_fovx, fovY=new_fovy).transpose(0,1).cuda()
70 | cam.full_proj_transform = (cam.world_view_transform.unsqueeze(0).bmm(cam.projection_matrix.unsqueeze(0))).squeeze(0)
71 | traj[i] = cam
72 | return traj
73 |
74 | if __name__ == "__main__":
75 | # Set up command line argument parser
76 | parser = ArgumentParser(description="Testing script parameters")
77 | model = ModelParams(parser, sentinel=True)
78 | pipeline = PipelineParams(parser)
79 | parser.add_argument("--iteration", default=-1, type=int)
80 | parser.add_argument("--save_as", default="output_video", type=str)
81 | args = get_combined_args(parser)
82 | print("Creating video for " + args.model_path)
83 |
84 | dataset, pipe = model.extract(args), pipeline.extract(args)
85 |
86 | triangles = TriangleModel(dataset.sh_degree)
87 | scene = Scene(args=dataset,
88 | triangles=triangles,
89 | init_opacity=None,
90 | init_size=None,
91 | nb_points=None,
92 | set_sigma=None,
93 | load_iteration=args.iteration,
94 | shuffle=False)
95 |
96 | bg_color = [1,1,1] if dataset.white_background else [0, 0, 0]
97 | background = torch.tensor(bg_color, dtype=torch.float32, device="cuda")
98 |
99 | traj_dir = os.path.join(args.model_path, 'traj')
100 | os.makedirs(traj_dir, exist_ok=True)
101 |
102 | render_path = os.path.join(traj_dir, "renders")
103 | os.makedirs(render_path, exist_ok=True)
104 |
105 | n_frames = 240*5
106 | cam_traj = generate_path(scene.getTrainCameras(), n_frames=n_frames)
107 |
108 | with torch.no_grad():
109 | for idx, view in enumerate(tqdm(cam_traj, desc="Rendering progress")):
110 | rendering = render(view, triangles, pipe, background)["render"]
111 | gt = view.original_image[0:3, :, :]
112 | torchvision.utils.save_image(rendering, os.path.join(traj_dir, "renders", '{0:05d}'.format(idx) + ".png"))
113 |
114 | create_videos(base_dir=traj_dir,
115 | input_dir=traj_dir,
116 | out_name='render_traj',
117 | num_frames=n_frames)
118 |
--------------------------------------------------------------------------------
/full_eval.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2023, Inria
4 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
5 | # All rights reserved.
6 | #
7 | # This software is free for non-commercial, research and evaluation use
8 | # under the terms of the LICENSE_GS.md file.
9 | #
10 | # For inquiries contact george.drettakis@inria.fr
11 | #
12 | # The modifications of the code are under the following copyright:
13 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
14 | # TELIM research group, http://www.telecom.ulg.ac.be/
15 | # IVUL research group, https://ivul.kaust.edu.sa/
16 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
17 | # All rights reserved.
18 | # The modifications are under the LICENSE.md file.
19 | #
20 | # For inquiries contact jan.held@uliege.be
21 | #
22 |
23 | import os
24 | from argparse import ArgumentParser
25 |
26 | mipnerf360_outdoor_scenes = ["bicycle", "flowers", "garden", "stump", "treehill"]
27 | mipnerf360_indoor_scenes = ["room", "counter", "kitchen", "bonsai"]
28 | tanks_and_temples_scenes = ["truck", "train"]
29 |
30 |
31 | cap_max = {
32 | 'bicycle': 6400000,
33 | 'flowers': 5500000,
34 | 'garden': 5200000,
35 | 'stump': 4750000,
36 | 'treehill': 5000000,
37 | 'room': 2100000,
38 | 'counter': 2500000,
39 | 'kitchen': 2400000,
40 | 'bonsai': 3000000,
41 | 'truck': 2000000,
42 | 'train': 2500000,
43 | }
44 |
45 | parser = ArgumentParser(description="Full evaluation script parameters")
46 | parser.add_argument("--skip_training", action="store_true")
47 | parser.add_argument("--skip_rendering", action="store_true")
48 | parser.add_argument("--skip_metrics", action="store_true")
49 | parser.add_argument("--output_path", default="./eval")
50 | args, _ = parser.parse_known_args()
51 |
52 | all_scenes = []
53 | all_scenes.extend(mipnerf360_outdoor_scenes)
54 | all_scenes.extend(mipnerf360_indoor_scenes)
55 | all_scenes.extend(tanks_and_temples_scenes)
56 |
57 | if not args.skip_training or not args.skip_rendering:
58 | parser.add_argument('--mipnerf360', "-m360", required=True, type=str)
59 | parser.add_argument("--tanksandtemples", "-tat", required=True, type=str)
60 | args = parser.parse_args()
61 |
62 | if not args.skip_training:
63 | common_args = " --quiet --eval --test_iterations -1 "
64 | for scene in mipnerf360_outdoor_scenes:
65 | source = args.mipnerf360 + "/" + scene + " --max_shapes " + str(cap_max[scene])
66 | common_args += " --outdoor "
67 | print("python train.py -s " + source + " -i images_4 -m " + args.output_path + "/" + scene + common_args)
68 | os.system("python train.py -s " + source + " -i images_4 -m " + args.output_path + "/" + scene + common_args)
69 | for scene in mipnerf360_indoor_scenes:
70 | source = args.mipnerf360 + "/" + scene + " --max_shapes " + str(cap_max[scene])
71 | common_args += ""
72 | os.system("python train.py -s " + source + " -i images_2 -m " + args.output_path + "/" + scene + common_args)
73 | for scene in tanks_and_temples_scenes:
74 | source = args.tanksandtemples + "/" + scene + " --max_shapes " + str(cap_max[scene])
75 | common_args += " --outdoor "
76 | os.system("python train.py -s " + source + " -m " + args.output_path + "/" + scene + common_args)
77 |
78 | if not args.skip_rendering:
79 | all_sources = []
80 | for scene in mipnerf360_outdoor_scenes:
81 | all_sources.append(args.mipnerf360 + "/" + scene)
82 | for scene in mipnerf360_indoor_scenes:
83 | all_sources.append(args.mipnerf360 + "/" + scene)
84 | for scene in tanks_and_temples_scenes:
85 | all_sources.append(args.tanksandtemples + "/" + scene)
86 |
87 | common_args = " --quiet --eval --skip_train"
88 | for scene, source in zip(all_scenes, all_sources):
89 | # os.system("python render.py --iteration 7000 -s " + source + " -m " + args.output_path + "/" + scene + common_args)
90 | os.system("python render.py --iteration 30000 -s " + source + " -m " + args.output_path + "/" + scene + common_args)
91 |
92 | if not args.skip_metrics:
93 | scenes_string = ""
94 | for scene in all_scenes:
95 | scenes_string += "\"" + args.output_path + "/" + scene + "\" "
96 |
97 | os.system("python metrics.py -m " + scenes_string)
--------------------------------------------------------------------------------
/lpipsPyTorch/__init__.py:
--------------------------------------------------------------------------------
1 | import torch
2 |
3 | from .modules.lpips import LPIPS
4 |
5 |
6 | def lpips(x: torch.Tensor,
7 | y: torch.Tensor,
8 | net_type: str = 'alex',
9 | version: str = '0.1'):
10 | r"""Function that measures
11 | Learned Perceptual Image Patch Similarity (LPIPS).
12 |
13 | Arguments:
14 | x, y (torch.Tensor): the input tensors to compare.
15 | net_type (str): the network type to compare the features:
16 | 'alex' | 'squeeze' | 'vgg'. Default: 'alex'.
17 | version (str): the version of LPIPS. Default: 0.1.
18 | """
19 | device = x.device
20 | criterion = LPIPS(net_type, version).to(device)
21 | return criterion(x, y)
22 |
--------------------------------------------------------------------------------
/lpipsPyTorch/modules/lpips.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import torch.nn as nn
3 |
4 | from .networks import get_network, LinLayers
5 | from .utils import get_state_dict
6 |
7 |
8 | class LPIPS(nn.Module):
9 | r"""Creates a criterion that measures
10 | Learned Perceptual Image Patch Similarity (LPIPS).
11 |
12 | Arguments:
13 | net_type (str): the network type to compare the features:
14 | 'alex' | 'squeeze' | 'vgg'. Default: 'alex'.
15 | version (str): the version of LPIPS. Default: 0.1.
16 | """
17 | def __init__(self, net_type: str = 'alex', version: str = '0.1'):
18 |
19 | assert version in ['0.1'], 'v0.1 is only supported now'
20 |
21 | super(LPIPS, self).__init__()
22 |
23 | # pretrained network
24 | self.net = get_network(net_type)
25 |
26 | # linear layers
27 | self.lin = LinLayers(self.net.n_channels_list)
28 | self.lin.load_state_dict(get_state_dict(net_type, version))
29 |
30 | def forward(self, x: torch.Tensor, y: torch.Tensor):
31 | feat_x, feat_y = self.net(x), self.net(y)
32 |
33 | diff = [(fx - fy) ** 2 for fx, fy in zip(feat_x, feat_y)]
34 | res = [l(d).mean((2, 3), True) for d, l in zip(diff, self.lin)]
35 |
36 | return torch.sum(torch.cat(res, 0), 0, True)
37 |
--------------------------------------------------------------------------------
/lpipsPyTorch/modules/networks.py:
--------------------------------------------------------------------------------
1 | from typing import Sequence
2 |
3 | from itertools import chain
4 |
5 | import torch
6 | import torch.nn as nn
7 | from torchvision import models
8 |
9 | from .utils import normalize_activation
10 |
11 |
12 | def get_network(net_type: str):
13 | if net_type == 'alex':
14 | return AlexNet()
15 | elif net_type == 'squeeze':
16 | return SqueezeNet()
17 | elif net_type == 'vgg':
18 | return VGG16()
19 | else:
20 | raise NotImplementedError('choose net_type from [alex, squeeze, vgg].')
21 |
22 |
23 | class LinLayers(nn.ModuleList):
24 | def __init__(self, n_channels_list: Sequence[int]):
25 | super(LinLayers, self).__init__([
26 | nn.Sequential(
27 | nn.Identity(),
28 | nn.Conv2d(nc, 1, 1, 1, 0, bias=False)
29 | ) for nc in n_channels_list
30 | ])
31 |
32 | for param in self.parameters():
33 | param.requires_grad = False
34 |
35 |
36 | class BaseNet(nn.Module):
37 | def __init__(self):
38 | super(BaseNet, self).__init__()
39 |
40 | # register buffer
41 | self.register_buffer(
42 | 'mean', torch.Tensor([-.030, -.088, -.188])[None, :, None, None])
43 | self.register_buffer(
44 | 'std', torch.Tensor([.458, .448, .450])[None, :, None, None])
45 |
46 | def set_requires_grad(self, state: bool):
47 | for param in chain(self.parameters(), self.buffers()):
48 | param.requires_grad = state
49 |
50 | def z_score(self, x: torch.Tensor):
51 | return (x - self.mean) / self.std
52 |
53 | def forward(self, x: torch.Tensor):
54 | x = self.z_score(x)
55 |
56 | output = []
57 | for i, (_, layer) in enumerate(self.layers._modules.items(), 1):
58 | x = layer(x)
59 | if i in self.target_layers:
60 | output.append(normalize_activation(x))
61 | if len(output) == len(self.target_layers):
62 | break
63 | return output
64 |
65 |
66 | class SqueezeNet(BaseNet):
67 | def __init__(self):
68 | super(SqueezeNet, self).__init__()
69 |
70 | self.layers = models.squeezenet1_1(True).features
71 | self.target_layers = [2, 5, 8, 10, 11, 12, 13]
72 | self.n_channels_list = [64, 128, 256, 384, 384, 512, 512]
73 |
74 | self.set_requires_grad(False)
75 |
76 |
77 | class AlexNet(BaseNet):
78 | def __init__(self):
79 | super(AlexNet, self).__init__()
80 |
81 | self.layers = models.alexnet(True).features
82 | self.target_layers = [2, 5, 8, 10, 12]
83 | self.n_channels_list = [64, 192, 384, 256, 256]
84 |
85 | self.set_requires_grad(False)
86 |
87 |
88 | class VGG16(BaseNet):
89 | def __init__(self):
90 | super(VGG16, self).__init__()
91 |
92 | self.layers = models.vgg16(weights=models.VGG16_Weights.IMAGENET1K_V1).features
93 | self.target_layers = [4, 9, 16, 23, 30]
94 | self.n_channels_list = [64, 128, 256, 512, 512]
95 |
96 | self.set_requires_grad(False)
97 |
--------------------------------------------------------------------------------
/lpipsPyTorch/modules/utils.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 |
3 | import torch
4 |
5 |
6 | def normalize_activation(x, eps=1e-10):
7 | norm_factor = torch.sqrt(torch.sum(x ** 2, dim=1, keepdim=True))
8 | return x / (norm_factor + eps)
9 |
10 |
11 | def get_state_dict(net_type: str = 'alex', version: str = '0.1'):
12 | # build url
13 | url = 'https://raw.githubusercontent.com/richzhang/PerceptualSimilarity/' \
14 | + f'master/lpips/weights/v{version}/{net_type}.pth'
15 |
16 | # download
17 | old_state_dict = torch.hub.load_state_dict_from_url(
18 | url, progress=True,
19 | map_location=None if torch.cuda.is_available() else torch.device('cpu')
20 | )
21 |
22 | # rename keys
23 | new_state_dict = OrderedDict()
24 | for key, val in old_state_dict.items():
25 | new_key = key
26 | new_key = new_key.replace('lin', '')
27 | new_key = new_key.replace('model.', '')
28 | new_state_dict[new_key] = val
29 |
30 | return new_state_dict
31 |
--------------------------------------------------------------------------------
/mesh.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2023, Inria
3 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
4 | # All rights reserved.
5 | #
6 | # This software is free for non-commercial, research and evaluation use
7 | # under the terms of the LICENSE.md file.
8 | #
9 | # For inquiries contact george.drettakis@inria.fr
10 | #
11 |
12 | import os
13 | import torch
14 | from triangle_renderer import render
15 | import sys
16 | from scene import Scene
17 | from triangle_renderer import TriangleModel
18 | from argparse import ArgumentParser, Namespace
19 | from arguments import ModelParams, PipelineParams, OptimizationParams, get_combined_args
20 | import numpy as np
21 | import matplotlib.pyplot as plt
22 | from PIL import Image
23 | import torchvision.transforms.functional as F
24 | from utils.mesh_utils import GaussianExtractor, post_process_mesh
25 |
26 | import open3d as o3d
27 |
28 | if __name__ == "__main__":
29 | # Set up command line argument parser
30 | parser = ArgumentParser(description="Testing script parameters")
31 | model = ModelParams(parser, sentinel=False)
32 | pipeline = PipelineParams(parser)
33 | parser.add_argument("--iteration", default=-1, type=int)
34 | parser.add_argument("--skip_train", action="store_true")
35 | parser.add_argument("--skip_test", action="store_true")
36 | parser.add_argument("--skip_mesh", action="store_true")
37 | parser.add_argument("--quiet", action="store_true")
38 | parser.add_argument("--render_path", action="store_true")
39 | parser.add_argument("--voxel_size", default=-1.0, type=float, help='Mesh: voxel size for TSDF')
40 | parser.add_argument("--depth_trunc", default=-1.0, type=float, help='Mesh: Max depth range for TSDF')
41 | parser.add_argument("--sdf_trunc", default=-1.0, type=float, help='Mesh: truncation value for TSDF')
42 | parser.add_argument("--num_cluster", default=50, type=int, help='Mesh: number of connected clusters to export')
43 | parser.add_argument("--unbounded", action="store_true", help='Mesh: using unbounded mode for meshing')
44 | parser.add_argument("--mesh_res", default=1024, type=int, help='Mesh: resolution for unbounded mesh extraction')
45 | # args = parser.parse_args(sys.argv[1:])
46 | args = get_combined_args(parser)
47 | print("Rendering " + args.model_path)
48 |
49 |
50 | dataset, iteration, pipe = model.extract(args), args.iteration, pipeline.extract(args)
51 | triangles = TriangleModel(dataset.sh_degree)
52 |
53 | scene = Scene(args=dataset,
54 | triangles=triangles,
55 | init_opacity=None,
56 | init_size=None,
57 | nb_points=None,
58 | set_sigma=None,
59 | no_dome=False,
60 | load_iteration=args.iteration,
61 | shuffle=False)
62 |
63 | bg_color = [1,1,1] if dataset.white_background else [0, 0, 0]
64 | background = torch.tensor(bg_color, dtype=torch.float32, device="cuda")
65 |
66 | train_dir = os.path.join(args.model_path, 'train', "ours_{}".format(scene.loaded_iter))
67 | test_dir = os.path.join(args.model_path, 'test', "ours_{}".format(scene.loaded_iter))
68 | gaussExtractor = GaussianExtractor(scene.triangles, render, pipe, bg_color=bg_color)
69 |
70 | if not args.skip_train:
71 | print("export training images ...")
72 | os.makedirs(train_dir, exist_ok=True)
73 | gaussExtractor.reconstruction(scene.getTrainCameras())
74 | gaussExtractor.export_image(train_dir)
75 |
76 |
77 | if (not args.skip_test) and (len(scene.getTestCameras()) > 0):
78 | print("export rendered testing images ...")
79 | os.makedirs(test_dir, exist_ok=True)
80 | gaussExtractor.reconstruction(scene.getTestCameras())
81 | gaussExtractor.export_image(test_dir)
82 |
83 | if not args.skip_mesh:
84 | print("export mesh ...")
85 | os.makedirs(train_dir, exist_ok=True)
86 | # set the active_sh to 0 to export only diffuse texture
87 | gaussExtractor.gaussians.active_sh_degree = 0
88 | gaussExtractor.reconstruction(scene.getTrainCameras())
89 | # extract the mesh and save
90 | if args.unbounded:
91 | name = 'fuse_unbounded.ply'
92 | mesh = gaussExtractor.extract_mesh_unbounded(resolution=args.mesh_res)
93 | else:
94 | name = 'fuse.ply'
95 | depth_trunc = (gaussExtractor.radius * 2.0) if args.depth_trunc < 0 else args.depth_trunc
96 | voxel_size = (depth_trunc / args.mesh_res) if args.voxel_size < 0 else args.voxel_size
97 | sdf_trunc = 5.0 * voxel_size if args.sdf_trunc < 0 else args.sdf_trunc
98 | mesh = gaussExtractor.extract_mesh_bounded(voxel_size=voxel_size, sdf_trunc=sdf_trunc, depth_trunc=depth_trunc)
99 |
100 | o3d.io.write_triangle_mesh(os.path.join(train_dir, name), mesh)
101 | print("mesh saved at {}".format(os.path.join(train_dir, name)))
102 | # post-process the mesh and save, saving the largest N clusters
103 | mesh_post = post_process_mesh(mesh, cluster_to_keep=args.num_cluster)
104 | o3d.io.write_triangle_mesh(os.path.join(train_dir, name.replace('.ply', '_post.ply')), mesh_post)
105 | print("mesh post processed saved at {}".format(os.path.join(train_dir, name.replace('.ply', '_post.ply'))))
--------------------------------------------------------------------------------
/metrics.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2023, Inria
4 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
5 | # All rights reserved.
6 | #
7 | # This software is free for non-commercial, research and evaluation use
8 | # under the terms of the LICENSE_GS.md file.
9 | #
10 | # For inquiries contact george.drettakis@inria.fr
11 | #
12 | # The modifications of the code are under the following copyright:
13 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
14 | # TELIM research group, http://www.telecom.ulg.ac.be/
15 | # IVUL research group, https://ivul.kaust.edu.sa/
16 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
17 | # All rights reserved.
18 | # The modifications are under the LICENSE.md file.
19 | #
20 | # For inquiries contact jan.held@uliege.be
21 | #
22 |
23 | from pathlib import Path
24 | import os
25 | from PIL import Image
26 | import torch
27 | import torchvision.transforms.functional as tf
28 | from utils.loss_utils import ssim
29 | from lpipsPyTorch import lpips
30 | import json
31 | from tqdm import tqdm
32 | from utils.image_utils import psnr
33 | from argparse import ArgumentParser
34 |
35 | def readImages(renders_dir, gt_dir):
36 | renders = []
37 | gts = []
38 | image_names = []
39 | for fname in os.listdir(renders_dir):
40 | render = Image.open(renders_dir / fname)
41 | gt = Image.open(gt_dir / fname)
42 | renders.append(tf.to_tensor(render).unsqueeze(0)[:, :3, :, :].cuda())
43 | gts.append(tf.to_tensor(gt).unsqueeze(0)[:, :3, :, :].cuda())
44 | image_names.append(fname)
45 | return renders, gts, image_names
46 |
47 | def evaluate(model_paths):
48 |
49 | full_dict = {}
50 | per_view_dict = {}
51 | full_dict_polytopeonly = {}
52 | per_view_dict_polytopeonly = {}
53 | print("")
54 |
55 | for scene_dir in model_paths:
56 | try:
57 | print("Scene:", scene_dir)
58 | full_dict[scene_dir] = {}
59 | per_view_dict[scene_dir] = {}
60 | full_dict_polytopeonly[scene_dir] = {}
61 | per_view_dict_polytopeonly[scene_dir] = {}
62 |
63 | test_dir = Path(scene_dir) / "test"
64 |
65 | for method in os.listdir(test_dir):
66 | print("Method:", method)
67 |
68 | full_dict[scene_dir][method] = {}
69 | per_view_dict[scene_dir][method] = {}
70 | full_dict_polytopeonly[scene_dir][method] = {}
71 | per_view_dict_polytopeonly[scene_dir][method] = {}
72 |
73 | method_dir = test_dir / method
74 | gt_dir = method_dir/ "gt"
75 | renders_dir = method_dir / "renders"
76 | renders, gts, image_names = readImages(renders_dir, gt_dir)
77 |
78 | ssims = []
79 | psnrs = []
80 | lpipss = []
81 |
82 | for idx in tqdm(range(len(renders)), desc="Metric evaluation progress"):
83 | ssims.append(ssim(renders[idx], gts[idx]))
84 | psnrs.append(psnr(renders[idx], gts[idx]))
85 | lpipss.append(lpips(renders[idx], gts[idx], net_type='vgg'))
86 |
87 | print(" SSIM : {:>12.7f}".format(torch.tensor(ssims).mean(), ".5"))
88 | print(" PSNR : {:>12.7f}".format(torch.tensor(psnrs).mean(), ".5"))
89 | print(" LPIPS: {:>12.7f}".format(torch.tensor(lpipss).mean(), ".5"))
90 | print("")
91 |
92 | full_dict[scene_dir][method].update({"SSIM": torch.tensor(ssims).mean().item(),
93 | "PSNR": torch.tensor(psnrs).mean().item(),
94 | "LPIPS": torch.tensor(lpipss).mean().item()})
95 | per_view_dict[scene_dir][method].update({"SSIM": {name: ssim for ssim, name in zip(torch.tensor(ssims).tolist(), image_names)},
96 | "PSNR": {name: psnr for psnr, name in zip(torch.tensor(psnrs).tolist(), image_names)},
97 | "LPIPS": {name: lp for lp, name in zip(torch.tensor(lpipss).tolist(), image_names)}})
98 |
99 | with open(scene_dir + "/results.json", 'w') as fp:
100 | json.dump(full_dict[scene_dir], fp, indent=True)
101 | with open(scene_dir + "/per_view.json", 'w') as fp:
102 | json.dump(per_view_dict[scene_dir], fp, indent=True)
103 | except:
104 | print("Unable to compute metrics for model", scene_dir)
105 |
106 | if __name__ == "__main__":
107 | device = torch.device("cuda:0")
108 | torch.cuda.set_device(device)
109 |
110 | # Set up command line argument parser
111 | parser = ArgumentParser(description="Training script parameters")
112 | parser.add_argument('--model_paths', '-m', required=True, nargs="+", type=str, default=[])
113 | args = parser.parse_args()
114 | evaluate(args.model_paths)
--------------------------------------------------------------------------------
/render.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2023, Inria
4 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
5 | # All rights reserved.
6 | #
7 | # This software is free for non-commercial, research and evaluation use
8 | # under the terms of the LICENSE_GS.md file.
9 | #
10 | # For inquiries contact george.drettakis@inria.fr
11 | #
12 | # The modifications of the code are under the following copyright:
13 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
14 | # TELIM research group, http://www.telecom.ulg.ac.be/
15 | # IVUL research group, https://ivul.kaust.edu.sa/
16 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
17 | # All rights reserved.
18 | # The modifications are under the LICENSE.md file.
19 | #
20 | # For inquiries contact jan.held@uliege.be
21 | #
22 |
23 | import torch
24 | from scene import Scene
25 | import os
26 | from tqdm import tqdm
27 | from os import makedirs
28 | from triangle_renderer import render
29 | import torchvision
30 | from utils.general_utils import safe_state
31 | from argparse import ArgumentParser
32 | from arguments import ModelParams, PipelineParams, get_combined_args
33 | from triangle_renderer import TriangleModel
34 |
35 | def render_set(model_path, name, iteration, views, triangles, pipeline, background):
36 | render_path = os.path.join(model_path, name, "ours_{}".format(iteration), "renders")
37 | gts_path = os.path.join(model_path, name, "ours_{}".format(iteration), "gt")
38 |
39 | makedirs(render_path, exist_ok=True)
40 | makedirs(gts_path, exist_ok=True)
41 |
42 | for idx, view in enumerate(tqdm(views, desc="Rendering progress")):
43 | rendering = render(view, triangles, pipeline, background)["render"]
44 | gt = view.original_image[0:3, :, :]
45 | torchvision.utils.save_image(rendering, os.path.join(render_path, '{0:05d}'.format(idx) + ".png"))
46 | torchvision.utils.save_image(gt, os.path.join(gts_path, '{0:05d}'.format(idx) + ".png"))
47 |
48 | def render_sets(dataset : ModelParams, iteration : int, pipeline : PipelineParams, skip_train : bool, skip_test : bool):
49 | with torch.no_grad():
50 | triangles = TriangleModel(dataset.sh_degree)
51 | scene = Scene(args=dataset,
52 | triangles=triangles,
53 | init_opacity=None,
54 | init_size=None,
55 | nb_points=None,
56 | set_sigma=None,
57 | no_dome=False,
58 | load_iteration=args.iteration,
59 | shuffle=False)
60 |
61 | bg_color = [1,1,1] if dataset.white_background else [0, 0, 0]
62 | background = torch.tensor(bg_color, dtype=torch.float32, device="cuda")
63 |
64 | if not skip_train:
65 | render_set(dataset.model_path, "train", scene.loaded_iter, scene.getTrainCameras(), triangles, pipeline, background)
66 |
67 | if not skip_test:
68 | render_set(dataset.model_path, "test", scene.loaded_iter, scene.getTestCameras(), triangles, pipeline, background)
69 |
70 | if __name__ == "__main__":
71 | # Set up command line argument parser
72 | parser = ArgumentParser(description="Testing script parameters")
73 | model = ModelParams(parser, sentinel=True)
74 | pipeline = PipelineParams(parser)
75 | parser.add_argument("--iteration", default=-1, type=int)
76 | parser.add_argument("--skip_train", action="store_true")
77 | parser.add_argument("--skip_test", action="store_true")
78 | parser.add_argument("--quiet", action="store_true")
79 | args = get_combined_args(parser)
80 | print("Rendering " + args.model_path)
81 |
82 | # Initialize system state (RNG)
83 | safe_state(args.quiet)
84 |
85 | render_sets(model.extract(args), args.iteration, pipeline.extract(args), args.skip_train, args.skip_test)
--------------------------------------------------------------------------------
/requirements.yaml:
--------------------------------------------------------------------------------
1 | name: triangle_splatting
2 | channels:
3 | - nvidia
4 | - conda-forge
5 | - defaults
6 | dependencies:
7 | - cuda=12.6
8 | - python=3.11
9 | - pip:
10 | - torch==2.4.0
11 | - torchvision==0.19.0
12 | - tqdm
13 | - plyfile
14 | - open3d
15 | - lpips
16 | - mediapy
17 | - opencv-python
18 |
19 |
--------------------------------------------------------------------------------
/scene/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2023, Inria
4 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
5 | # All rights reserved.
6 | #
7 | # This software is free for non-commercial, research and evaluation use
8 | # under the terms of the LICENSE_GS.md file.
9 | #
10 | # For inquiries contact george.drettakis@inria.fr
11 | #
12 | # The modifications of the code are under the following copyright:
13 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
14 | # TELIM research group, http://www.telecom.ulg.ac.be/
15 | # IVUL research group, https://ivul.kaust.edu.sa/
16 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
17 | # All rights reserved.
18 | # The modifications are under the LICENSE.md file.
19 | #
20 | # For inquiries contact jan.held@uliege.be
21 | #
22 |
23 | import os
24 | import random
25 | import json
26 | from utils.system_utils import searchForMaxIteration
27 | from scene.dataset_readers import sceneLoadTypeCallbacks
28 | from scene.triangle_model import TriangleModel
29 | from arguments import ModelParams
30 | from utils.camera_utils import cameraList_from_camInfos, camera_to_JSON
31 |
32 | class Scene:
33 |
34 | triangles : TriangleModel
35 |
36 | def __init__(self, args : ModelParams, triangles : TriangleModel, init_opacity, init_size, nb_points, set_sigma, no_dome=False, load_iteration=None, shuffle=True, resolution_scales=[1.0]):
37 | """b
38 | :param path: Path to colmap scene main folder.
39 | """
40 | self.model_path = args.model_path
41 | self.loaded_iter = None
42 | self.triangles = triangles
43 |
44 | if load_iteration:
45 | if load_iteration == -1:
46 | self.loaded_iter = searchForMaxIteration(os.path.join(self.model_path, "point_cloud"))
47 | else:
48 | self.loaded_iter = load_iteration
49 | print("Loading trained model at iteration {}".format(self.loaded_iter))
50 |
51 | self.train_cameras = {}
52 | self.test_cameras = {}
53 |
54 | if os.path.exists(os.path.join(args.source_path, "sparse")):
55 | scene_info = sceneLoadTypeCallbacks["Colmap"](args.source_path, args.images, args.eval)
56 | elif os.path.exists(os.path.join(args.source_path, "transforms_train.json")):
57 | print("Found transforms_train.json file, assuming Blender data set!")
58 | scene_info = sceneLoadTypeCallbacks["Blender"](args.source_path, args.white_background, args.eval)
59 | else:
60 | assert False, "Could not recognize scene type!"
61 |
62 | if not self.loaded_iter:
63 | with open(scene_info.ply_path, 'rb') as src_file, open(os.path.join(self.model_path, "input.ply") , 'wb') as dest_file:
64 | dest_file.write(src_file.read())
65 | json_cams = []
66 | camlist = []
67 | if scene_info.test_cameras:
68 | camlist.extend(scene_info.test_cameras)
69 | if scene_info.train_cameras:
70 | camlist.extend(scene_info.train_cameras)
71 | for id, cam in enumerate(camlist):
72 | json_cams.append(camera_to_JSON(id, cam))
73 | with open(os.path.join(self.model_path, "cameras.json"), 'w') as file:
74 | json.dump(json_cams, file)
75 |
76 | if shuffle:
77 | random.shuffle(scene_info.train_cameras) # Multi-res consistent random shuffling
78 | random.shuffle(scene_info.test_cameras) # Multi-res consistent random shuffling
79 |
80 | self.cameras_extent = scene_info.nerf_normalization["radius"]
81 |
82 | for resolution_scale in resolution_scales:
83 | print("Loading Training Cameras")
84 | self.train_cameras[resolution_scale] = cameraList_from_camInfos(scene_info.train_cameras, resolution_scale, args)
85 | print("Loading Test Cameras")
86 | self.test_cameras[resolution_scale] = cameraList_from_camInfos(scene_info.test_cameras, resolution_scale, args)
87 |
88 | if self.loaded_iter:
89 | self.triangles.load(os.path.join(self.model_path,
90 | "point_cloud",
91 | "iteration_" + str(self.loaded_iter)
92 | )
93 | )
94 | else:
95 | self.triangles.create_from_pcd(scene_info.point_cloud, self.cameras_extent, init_opacity, init_size, nb_points, set_sigma, no_dome)
96 |
97 | def save(self, iteration):
98 | point_cloud_path = os.path.join(self.model_path, "point_cloud/iteration_{}".format(iteration))
99 | self.triangles.save(point_cloud_path)
100 |
101 | def getTrainCameras(self, scale=1.0):
102 | return self.train_cameras[scale]
103 |
104 | def getTestCameras(self, scale=1.0):
105 | return self.test_cameras[scale]
--------------------------------------------------------------------------------
/scene/cameras.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2023, Inria
4 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
5 | # All rights reserved.
6 | #
7 | # This software is free for non-commercial, research and evaluation use
8 | # under the terms of the LICENSE_GS.md file.
9 | #
10 | # For inquiries contact george.drettakis@inria.fr
11 | #
12 | # The modifications of the code are under the following copyright:
13 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
14 | # TELIM research group, http://www.telecom.ulg.ac.be/
15 | # IVUL research group, https://ivul.kaust.edu.sa/
16 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
17 | # All rights reserved.
18 | # The modifications are under the LICENSE.md file.
19 | #
20 | # For inquiries contact jan.held@uliege.be
21 | #
22 |
23 | import torch
24 | from torch import nn
25 | import numpy as np
26 | from utils.graphics_utils import getWorld2View2, getProjectionMatrix
27 |
28 | class Camera(nn.Module):
29 | def __init__(self, colmap_id, R, T, FoVx, FoVy, image, gt_alpha_mask,
30 | image_name, uid,
31 | trans=np.array([0.0, 0.0, 0.0]), scale=1.0, data_device = "cuda"
32 | ):
33 | super(Camera, self).__init__()
34 |
35 | self.uid = uid
36 | self.colmap_id = colmap_id
37 | self.R = R
38 | self.T = T
39 | self.FoVx = FoVx
40 | self.FoVy = FoVy
41 | self.image_name = image_name
42 |
43 | try:
44 | self.data_device = torch.device(data_device)
45 | except Exception as e:
46 | print(e)
47 | print(f"[Warning] Custom device {data_device} failed, fallback to default cuda device" )
48 | self.data_device = torch.device("cuda")
49 |
50 | self.original_image = image.clamp(0.0, 1.0).to(self.data_device)
51 | self.image_width = self.original_image.shape[2]
52 | self.image_height = self.original_image.shape[1]
53 |
54 | if gt_alpha_mask is not None:
55 | # self.original_image *= gt_alpha_mask.to(self.data_device)
56 | self.gt_alpha_mask = gt_alpha_mask.to(self.data_device)
57 | else:
58 | self.original_image *= torch.ones((1, self.image_height, self.image_width), device=self.data_device)
59 | self.gt_alpha_mask = None
60 |
61 | self.zfar = 100.0
62 | self.znear = 0.01
63 |
64 | self.trans = trans
65 | self.scale = scale
66 |
67 | self.world_view_transform = torch.tensor(getWorld2View2(R, T, trans, scale)).transpose(0, 1).cuda()
68 | self.projection_matrix = getProjectionMatrix(znear=self.znear, zfar=self.zfar, fovX=self.FoVx, fovY=self.FoVy).transpose(0,1).cuda()
69 | self.full_proj_transform = (self.world_view_transform.unsqueeze(0).bmm(self.projection_matrix.unsqueeze(0))).squeeze(0)
70 | self.camera_center = self.world_view_transform.inverse()[3, :3]
71 |
72 | class MiniCam:
73 | def __init__(self, width, height, fovy, fovx, znear, zfar, world_view_transform, full_proj_transform):
74 | self.image_width = width
75 | self.image_height = height
76 | self.FoVy = fovy
77 | self.FoVx = fovx
78 | self.znear = znear
79 | self.zfar = zfar
80 | self.world_view_transform = world_view_transform
81 | self.full_proj_transform = full_proj_transform
82 | view_inv = torch.inverse(self.world_view_transform)
83 | self.camera_center = view_inv[3][:3]
84 |
--------------------------------------------------------------------------------
/scene/dataset_readers.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2023, Inria
4 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
5 | # All rights reserved.
6 | #
7 | # This software is free for non-commercial, research and evaluation use
8 | # under the terms of the LICENSE_GS.md file.
9 | #
10 | # For inquiries contact george.drettakis@inria.fr
11 | #
12 | # The modifications of the code are under the following copyright:
13 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
14 | # TELIM research group, http://www.telecom.ulg.ac.be/
15 | # IVUL research group, https://ivul.kaust.edu.sa/
16 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
17 | # All rights reserved.
18 | # The modifications are under the LICENSE.md file.
19 | #
20 | # For inquiries contact jan.held@uliege.be
21 | #
22 |
23 | import os
24 | import sys
25 | from PIL import Image
26 | from typing import NamedTuple
27 | from scene.colmap_loader import read_extrinsics_text, read_intrinsics_text, qvec2rotmat, \
28 | read_extrinsics_binary, read_intrinsics_binary, read_points3D_binary, read_points3D_text
29 | from utils.graphics_utils import getWorld2View2, focal2fov, fov2focal
30 | import numpy as np
31 | import json
32 | from pathlib import Path
33 | from plyfile import PlyData, PlyElement
34 | from utils.sh_utils import SH2RGB
35 | from scene.triangle_model import BasicPointCloud
36 |
37 | class CameraInfo(NamedTuple):
38 | uid: int
39 | R: np.array
40 | T: np.array
41 | FovY: np.array
42 | FovX: np.array
43 | image: np.array
44 | image_path: str
45 | image_name: str
46 | width: int
47 | height: int
48 |
49 | class SceneInfo(NamedTuple):
50 | point_cloud: BasicPointCloud
51 | train_cameras: list
52 | test_cameras: list
53 | nerf_normalization: dict
54 | ply_path: str
55 |
56 | def getNerfppNorm(cam_info):
57 | def get_center_and_diag(cam_centers):
58 | cam_centers = np.hstack(cam_centers)
59 | avg_cam_center = np.mean(cam_centers, axis=1, keepdims=True)
60 | center = avg_cam_center
61 | dist = np.linalg.norm(cam_centers - center, axis=0, keepdims=True)
62 | diagonal = np.max(dist)
63 | return center.flatten(), diagonal
64 |
65 | cam_centers = []
66 |
67 | for cam in cam_info:
68 | W2C = getWorld2View2(cam.R, cam.T)
69 | C2W = np.linalg.inv(W2C)
70 | cam_centers.append(C2W[:3, 3:4])
71 |
72 | center, diagonal = get_center_and_diag(cam_centers)
73 | radius = diagonal * 1.1
74 |
75 | translate = -center
76 |
77 | return {"translate": translate, "radius": radius}
78 |
79 | def readColmapCameras(cam_extrinsics, cam_intrinsics, images_folder):
80 | cam_infos = []
81 | for idx, key in enumerate(cam_extrinsics):
82 | sys.stdout.write('\r')
83 | # the exact output you're looking for:
84 | sys.stdout.write("Reading camera {}/{}".format(idx+1, len(cam_extrinsics)))
85 | sys.stdout.flush()
86 |
87 | extr = cam_extrinsics[key]
88 | intr = cam_intrinsics[extr.camera_id]
89 | height = intr.height
90 | width = intr.width
91 |
92 | uid = intr.id
93 | R = np.transpose(qvec2rotmat(extr.qvec))
94 | T = np.array(extr.tvec)
95 |
96 | if intr.model=="SIMPLE_PINHOLE":
97 | focal_length_x = intr.params[0]
98 | FovY = focal2fov(focal_length_x, height)
99 | FovX = focal2fov(focal_length_x, width)
100 | elif intr.model=="PINHOLE":
101 | focal_length_x = intr.params[0]
102 | focal_length_y = intr.params[1]
103 | FovY = focal2fov(focal_length_y, height)
104 | FovX = focal2fov(focal_length_x, width)
105 | else:
106 | assert False, "Colmap camera model not handled: only undistorted datasets (PINHOLE or SIMPLE_PINHOLE cameras) supported!"
107 |
108 | image_path = os.path.join(images_folder, os.path.basename(extr.name))
109 | image_name = os.path.basename(image_path).split(".")[0]
110 | image = Image.open(image_path)
111 |
112 | cam_info = CameraInfo(uid=uid, R=R, T=T, FovY=FovY, FovX=FovX, image=image,
113 | image_path=image_path, image_name=image_name, width=width, height=height)
114 | cam_infos.append(cam_info)
115 | sys.stdout.write('\n')
116 | return cam_infos
117 |
118 | def fetchPly(path):
119 | plydata = PlyData.read(path)
120 | vertices = plydata['vertex']
121 | positions = np.vstack([vertices['x'], vertices['y'], vertices['z']]).T
122 | colors = np.vstack([vertices['red'], vertices['green'], vertices['blue']]).T / 255.0
123 | normals = np.vstack([vertices['nx'], vertices['ny'], vertices['nz']]).T
124 | return BasicPointCloud(points=positions, colors=colors, normals=normals)
125 |
126 | def storePly(path, xyz, rgb):
127 | # Define the dtype for the structured array
128 | dtype = [('x', 'f4'), ('y', 'f4'), ('z', 'f4'),
129 | ('nx', 'f4'), ('ny', 'f4'), ('nz', 'f4'),
130 | ('red', 'u1'), ('green', 'u1'), ('blue', 'u1')]
131 |
132 | normals = np.zeros_like(xyz)
133 |
134 | elements = np.empty(xyz.shape[0], dtype=dtype)
135 | attributes = np.concatenate((xyz, normals, rgb), axis=1)
136 | elements[:] = list(map(tuple, attributes))
137 |
138 | # Create the PlyData object and write to file
139 | vertex_element = PlyElement.describe(elements, 'vertex')
140 | ply_data = PlyData([vertex_element])
141 | ply_data.write(path)
142 |
143 | def readColmapSceneInfo(path, images, eval, llffhold=8):
144 | try:
145 | cameras_extrinsic_file = os.path.join(path, "sparse/0", "images.bin")
146 | cameras_intrinsic_file = os.path.join(path, "sparse/0", "cameras.bin")
147 | cam_extrinsics = read_extrinsics_binary(cameras_extrinsic_file)
148 | cam_intrinsics = read_intrinsics_binary(cameras_intrinsic_file)
149 | except:
150 | cameras_extrinsic_file = os.path.join(path, "sparse/0", "images.txt")
151 | cameras_intrinsic_file = os.path.join(path, "sparse/0", "cameras.txt")
152 | cam_extrinsics = read_extrinsics_text(cameras_extrinsic_file)
153 | cam_intrinsics = read_intrinsics_text(cameras_intrinsic_file)
154 |
155 | reading_dir = "images" if images == None else images
156 | cam_infos_unsorted = readColmapCameras(cam_extrinsics=cam_extrinsics, cam_intrinsics=cam_intrinsics, images_folder=os.path.join(path, reading_dir))
157 | cam_infos = sorted(cam_infos_unsorted.copy(), key = lambda x : x.image_name)
158 |
159 | if eval:
160 | train_cam_infos = [c for idx, c in enumerate(cam_infos) if idx % llffhold != 0]
161 | test_cam_infos = [c for idx, c in enumerate(cam_infos) if idx % llffhold == 0]
162 | else:
163 | train_cam_infos = cam_infos
164 | test_cam_infos = []
165 |
166 | nerf_normalization = getNerfppNorm(train_cam_infos)
167 |
168 | ply_path = os.path.join(path, "sparse/0/points3D.ply")
169 | bin_path = os.path.join(path, "sparse/0/points3D.bin")
170 | txt_path = os.path.join(path, "sparse/0/points3D.txt")
171 | if not os.path.exists(ply_path):
172 | print("Converting point3d.bin to .ply, will happen only the first time you open the scene.")
173 | try:
174 | xyz, rgb, _ = read_points3D_binary(bin_path)
175 | except:
176 | xyz, rgb, _ = read_points3D_text(txt_path)
177 | storePly(ply_path, xyz, rgb)
178 | try:
179 | pcd = fetchPly(ply_path)
180 | except:
181 | pcd = None
182 |
183 | scene_info = SceneInfo(point_cloud=pcd,
184 | train_cameras=train_cam_infos,
185 | test_cameras=test_cam_infos,
186 | nerf_normalization=nerf_normalization,
187 | ply_path=ply_path)
188 | return scene_info
189 |
190 | def readCamerasFromTransforms(path, transformsfile, white_background, extension=".png"):
191 | cam_infos = []
192 |
193 | with open(os.path.join(path, transformsfile)) as json_file:
194 | contents = json.load(json_file)
195 | fovx = contents["camera_angle_x"]
196 |
197 | frames = contents["frames"]
198 | for idx, frame in enumerate(frames):
199 | cam_name = os.path.join(path, frame["file_path"] + extension)
200 |
201 | # NeRF 'transform_matrix' is a camera-to-world transform
202 | c2w = np.array(frame["transform_matrix"])
203 | # change from OpenGL/Blender camera axes (Y up, Z back) to COLMAP (Y down, Z forward)
204 | c2w[:3, 1:3] *= -1
205 |
206 | # get the world-to-camera transform and set R, T
207 | w2c = np.linalg.inv(c2w)
208 | R = np.transpose(w2c[:3,:3]) # R is stored transposed due to 'glm' in CUDA code
209 | T = w2c[:3, 3]
210 |
211 | image_path = os.path.join(path, cam_name)
212 | image_name = Path(cam_name).stem
213 | image = Image.open(image_path)
214 |
215 | im_data = np.array(image.convert("RGBA"))
216 |
217 | bg = np.array([1,1,1]) if white_background else np.array([0, 0, 0])
218 |
219 | norm_data = im_data / 255.0
220 | arr = norm_data[:,:,:3] * norm_data[:, :, 3:4] + bg * (1 - norm_data[:, :, 3:4])
221 | image = Image.fromarray(np.array(arr*255.0, dtype=np.byte), "RGB")
222 |
223 | fovy = focal2fov(fov2focal(fovx, image.size[0]), image.size[1])
224 | FovY = fovy
225 | FovX = fovx
226 |
227 | cam_infos.append(CameraInfo(uid=idx, R=R, T=T, FovY=FovY, FovX=FovX, image=image,
228 | image_path=image_path, image_name=image_name, width=image.size[0], height=image.size[1]))
229 |
230 | return cam_infos
231 |
232 | def readNerfSyntheticInfo(path, white_background, eval, extension=".png"):
233 | print("Reading Training Transforms")
234 | train_cam_infos = readCamerasFromTransforms(path, "transforms_train.json", white_background, extension)
235 | print("Reading Test Transforms")
236 | test_cam_infos = readCamerasFromTransforms(path, "transforms_test.json", white_background, extension)
237 |
238 | if not eval:
239 | train_cam_infos.extend(test_cam_infos)
240 | test_cam_infos = []
241 |
242 | nerf_normalization = getNerfppNorm(train_cam_infos)
243 |
244 | ply_path = os.path.join(path, "points3d.ply")
245 | if not os.path.exists(ply_path):
246 | # Since this data set has no colmap data, we start with random points
247 | num_pts = 100_000
248 | print(f"Generating random point cloud ({num_pts})...")
249 |
250 | # We create random points inside the bounds of the synthetic Blender scenes
251 | xyz = np.random.random((num_pts, 3)) * 2.6 - 1.3
252 | shs = np.random.random((num_pts, 3)) / 255.0
253 | pcd = BasicPointCloud(points=xyz, colors=SH2RGB(shs), normals=np.zeros((num_pts, 3)))
254 |
255 | storePly(ply_path, xyz, SH2RGB(shs) * 255)
256 | try:
257 | pcd = fetchPly(ply_path)
258 | except:
259 | pcd = None
260 |
261 | scene_info = SceneInfo(point_cloud=pcd,
262 | train_cameras=train_cam_infos,
263 | test_cameras=test_cam_infos,
264 | nerf_normalization=nerf_normalization,
265 | ply_path=ply_path)
266 | return scene_info
267 |
268 | sceneLoadTypeCallbacks = {
269 | "Colmap": readColmapSceneInfo,
270 | "Blender" : readNerfSyntheticInfo
271 | }
--------------------------------------------------------------------------------
/scripts/dtu_eval.py:
--------------------------------------------------------------------------------
1 | import os
2 | from argparse import ArgumentParser
3 |
4 | dtu_scenes = ['scan24', 'scan37', 'scan40', 'scan55', 'scan63', 'scan65', 'scan69', 'scan83', 'scan97', 'scan105', 'scan106', 'scan110', 'scan114', 'scan118', 'scan122']
5 |
6 |
7 | parser = ArgumentParser(description="Full evaluation script parameters")
8 | parser.add_argument("--skip_training", action="store_true")
9 | parser.add_argument("--skip_rendering", action="store_true")
10 | parser.add_argument("--skip_metrics", action="store_true")
11 | parser.add_argument("--output_path", default="./eval/dtu")
12 | parser.add_argument('--dtu', "-dtu", required=True, type=str)
13 |
14 | parser.add_argument('--max_shapes', default=500000, type=int)
15 | parser.add_argument('--lambda_normals', default=0.0028, type=float)
16 | parser.add_argument('--lambda_dist', default=0.014, type=float)
17 | parser.add_argument('--iteration_mesh', default=25000, type=int)
18 | parser.add_argument('--densify_until_iter', default=25000, type=int)
19 | parser.add_argument('--lambda_opacity', default=0.0044, type=float)
20 | parser.add_argument('--importance_threshold', default=0.027, type=float)
21 | parser.add_argument('--lr_triangles_points_init', default=0.0015, type=float)
22 |
23 |
24 | args, _ = parser.parse_known_args()
25 |
26 | all_scenes = []
27 | all_scenes.extend(dtu_scenes)
28 |
29 | if not args.skip_metrics:
30 | parser.add_argument('--DTU_Official', "-DTU", required=True, type=str)
31 | args = parser.parse_args()
32 |
33 | if not args.skip_training:
34 | common_args = (
35 | f" --test_iterations -1 --depth_ratio 1.0 -r 2 --eval --max_shapes {args.max_shapes}"
36 | f" --lambda_normals {args.lambda_normals}"
37 | f" --lambda_dist {args.lambda_dist}"
38 | f" --iteration_mesh {args.iteration_mesh}"
39 | f" --densify_until_iter {args.densify_until_iter}"
40 | f" --lambda_opacity {args.lambda_opacity}"
41 | f" --importance_threshold {args.importance_threshold}"
42 | f" --lr_triangles_points_init {args.lr_triangles_points_init}"
43 | f" --lambda_size {0.0}"
44 | f" --no_dome"
45 | )
46 |
47 | for scene in dtu_scenes:
48 | source = args.dtu + "/" + scene
49 | print("python train.py -s " + source + " -m " + args.output_path + "/" + scene + common_args)
50 | os.system("python train.py -s " + source + " -m " + args.output_path + "/" + scene + common_args)
51 |
52 |
53 | if not args.skip_rendering:
54 | all_sources = []
55 | common_args = " --quiet --skip_train --depth_ratio 1.0 --num_cluster 1 --voxel_size 0.004 --sdf_trunc 0.016 --depth_trunc 3.0"
56 | for scene in dtu_scenes:
57 | source = args.dtu + "/" + scene
58 | print("python mesh.py --iteration 30000 -s " + source + " -m" + args.output_path + "/" + scene + common_args)
59 | os.system("python mesh.py --iteration 30000 -s " + source + " -m" + args.output_path + "/" + scene + common_args)
60 |
61 |
62 | if not args.skip_metrics:
63 | script_dir = os.path.dirname(os.path.abspath(__file__))
64 | for scene in dtu_scenes:
65 | scan_id = scene[4:]
66 | ply_file = f"{args.output_path}/{scene}/train/ours_30000/"
67 | iteration = 30000
68 | output_dir = f"{args.output_path}/{scene}/"
69 | string = f"python {script_dir}/eval_dtu/evaluate_single_scene.py " + \
70 | f"--input_mesh {args.output_path}/{scene}/train/ours_30000/fuse_post.ply " + \
71 | f"--scan_id {scan_id} --output_dir {output_dir}/ " + \
72 | f"--mask_dir {args.dtu} " + \
73 | f"--DTU {args.DTU_Official}"
74 | print(string)
75 | os.system(string)
76 |
77 |
78 | import json
79 |
80 | average = 0
81 | for scene in dtu_scenes:
82 | output_dir = f"{args.output_path}/{scene}/"
83 | with open(output_dir + '/results.json', 'r') as f:
84 | results = json.load(f)
85 | print("Results: ", results)
86 | average += results['overall']
87 | average /= len(dtu_scenes)
88 |
--------------------------------------------------------------------------------
/scripts/eval_dtu/eval.py:
--------------------------------------------------------------------------------
1 | # adapted from https://github.com/jzhangbs/DTUeval-python
2 | import numpy as np
3 | import open3d as o3d
4 | import sklearn.neighbors as skln
5 | from tqdm import tqdm
6 | from scipy.io import loadmat
7 | import multiprocessing as mp
8 | import argparse
9 |
10 | def sample_single_tri(input_):
11 | n1, n2, v1, v2, tri_vert = input_
12 | c = np.mgrid[:n1+1, :n2+1]
13 | c += 0.5
14 | c[0] /= max(n1, 1e-7)
15 | c[1] /= max(n2, 1e-7)
16 | c = np.transpose(c, (1,2,0))
17 | k = c[c.sum(axis=-1) < 1] # m2
18 | q = v1 * k[:,:1] + v2 * k[:,1:] + tri_vert
19 | return q
20 |
21 | def write_vis_pcd(file, points, colors):
22 | pcd = o3d.geometry.PointCloud()
23 | pcd.points = o3d.utility.Vector3dVector(points)
24 | pcd.colors = o3d.utility.Vector3dVector(colors)
25 | o3d.io.write_point_cloud(file, pcd)
26 |
27 | if __name__ == '__main__':
28 | mp.freeze_support()
29 |
30 | parser = argparse.ArgumentParser()
31 | parser.add_argument('--data', type=str, default='data_in.ply')
32 | parser.add_argument('--scan', type=int, default=1)
33 | parser.add_argument('--mode', type=str, default='mesh', choices=['mesh', 'pcd'])
34 | parser.add_argument('--dataset_dir', type=str, default='.')
35 | parser.add_argument('--vis_out_dir', type=str, default='.')
36 | parser.add_argument('--downsample_density', type=float, default=0.2)
37 | parser.add_argument('--patch_size', type=float, default=60)
38 | parser.add_argument('--max_dist', type=float, default=20)
39 | parser.add_argument('--visualize_threshold', type=float, default=10)
40 | args = parser.parse_args()
41 |
42 | thresh = args.downsample_density
43 | if args.mode == 'mesh':
44 | pbar = tqdm(total=9)
45 | pbar.set_description('read data mesh')
46 | data_mesh = o3d.io.read_triangle_mesh(args.data)
47 |
48 | vertices = np.asarray(data_mesh.vertices)
49 | triangles = np.asarray(data_mesh.triangles)
50 | tri_vert = vertices[triangles]
51 |
52 | pbar.update(1)
53 | pbar.set_description('sample pcd from mesh')
54 | v1 = tri_vert[:,1] - tri_vert[:,0]
55 | v2 = tri_vert[:,2] - tri_vert[:,0]
56 | l1 = np.linalg.norm(v1, axis=-1, keepdims=True)
57 | l2 = np.linalg.norm(v2, axis=-1, keepdims=True)
58 | area2 = np.linalg.norm(np.cross(v1, v2), axis=-1, keepdims=True)
59 | non_zero_area = (area2 > 0)[:,0]
60 | l1, l2, area2, v1, v2, tri_vert = [
61 | arr[non_zero_area] for arr in [l1, l2, area2, v1, v2, tri_vert]
62 | ]
63 | thr = thresh * np.sqrt(l1 * l2 / area2)
64 | n1 = np.floor(l1 / thr)
65 | n2 = np.floor(l2 / thr)
66 |
67 | with mp.Pool() as mp_pool:
68 | new_pts = mp_pool.map(sample_single_tri, ((n1[i,0], n2[i,0], v1[i:i+1], v2[i:i+1], tri_vert[i:i+1,0]) for i in range(len(n1))), chunksize=1024)
69 |
70 | new_pts = np.concatenate(new_pts, axis=0)
71 | data_pcd = np.concatenate([vertices, new_pts], axis=0)
72 |
73 | elif args.mode == 'pcd':
74 | pbar = tqdm(total=8)
75 | pbar.set_description('read data pcd')
76 | data_pcd_o3d = o3d.io.read_point_cloud(args.data)
77 | data_pcd = np.asarray(data_pcd_o3d.points)
78 |
79 | pbar.update(1)
80 | pbar.set_description('random shuffle pcd index')
81 | shuffle_rng = np.random.default_rng()
82 | shuffle_rng.shuffle(data_pcd, axis=0)
83 |
84 | pbar.update(1)
85 | pbar.set_description('downsample pcd')
86 | nn_engine = skln.NearestNeighbors(n_neighbors=1, radius=thresh, algorithm='kd_tree', n_jobs=-1)
87 | nn_engine.fit(data_pcd)
88 | rnn_idxs = nn_engine.radius_neighbors(data_pcd, radius=thresh, return_distance=False)
89 | mask = np.ones(data_pcd.shape[0], dtype=np.bool_)
90 | for curr, idxs in enumerate(rnn_idxs):
91 | if mask[curr]:
92 | mask[idxs] = 0
93 | mask[curr] = 1
94 | data_down = data_pcd[mask]
95 |
96 | pbar.update(1)
97 | pbar.set_description('masking data pcd')
98 | obs_mask_file = loadmat(f'{args.dataset_dir}/ObsMask/ObsMask{args.scan}_10.mat')
99 | ObsMask, BB, Res = [obs_mask_file[attr] for attr in ['ObsMask', 'BB', 'Res']]
100 | BB = BB.astype(np.float32)
101 |
102 | patch = args.patch_size
103 | inbound = ((data_down >= BB[:1]-patch) & (data_down < BB[1:]+patch*2)).sum(axis=-1) ==3
104 | data_in = data_down[inbound]
105 |
106 | data_grid = np.around((data_in - BB[:1]) / Res).astype(np.int32)
107 | grid_inbound = ((data_grid >= 0) & (data_grid < np.expand_dims(ObsMask.shape, 0))).sum(axis=-1) ==3
108 | data_grid_in = data_grid[grid_inbound]
109 | in_obs = ObsMask[data_grid_in[:,0], data_grid_in[:,1], data_grid_in[:,2]].astype(np.bool_)
110 | data_in_obs = data_in[grid_inbound][in_obs]
111 |
112 | pbar.update(1)
113 | pbar.set_description('read STL pcd')
114 | stl_pcd = o3d.io.read_point_cloud(f'{args.dataset_dir}/Points/stl/stl{args.scan:03}_total.ply')
115 | stl = np.asarray(stl_pcd.points)
116 |
117 | pbar.update(1)
118 | pbar.set_description('compute data2stl')
119 | nn_engine.fit(stl)
120 | dist_d2s, idx_d2s = nn_engine.kneighbors(data_in_obs, n_neighbors=1, return_distance=True)
121 | max_dist = args.max_dist
122 | mean_d2s = dist_d2s[dist_d2s < max_dist].mean()
123 |
124 | pbar.update(1)
125 | pbar.set_description('compute stl2data')
126 | ground_plane = loadmat(f'{args.dataset_dir}/ObsMask/Plane{args.scan}.mat')['P']
127 |
128 | stl_hom = np.concatenate([stl, np.ones_like(stl[:,:1])], -1)
129 | above = (ground_plane.reshape((1,4)) * stl_hom).sum(-1) > 0
130 | stl_above = stl[above]
131 |
132 | nn_engine.fit(data_in)
133 | dist_s2d, idx_s2d = nn_engine.kneighbors(stl_above, n_neighbors=1, return_distance=True)
134 | mean_s2d = dist_s2d[dist_s2d < max_dist].mean()
135 |
136 | pbar.update(1)
137 | pbar.set_description('visualize error')
138 | vis_dist = args.visualize_threshold
139 | R = np.array([[1,0,0]], dtype=np.float64)
140 | G = np.array([[0,1,0]], dtype=np.float64)
141 | B = np.array([[0,0,1]], dtype=np.float64)
142 | W = np.array([[1,1,1]], dtype=np.float64)
143 | data_color = np.tile(B, (data_down.shape[0], 1))
144 | data_alpha = dist_d2s.clip(max=vis_dist) / vis_dist
145 | data_color[ np.where(inbound)[0][grid_inbound][in_obs] ] = R * data_alpha + W * (1-data_alpha)
146 | data_color[ np.where(inbound)[0][grid_inbound][in_obs][dist_d2s[:,0] >= max_dist] ] = G
147 | write_vis_pcd(f'{args.vis_out_dir}/vis_{args.scan:03}_d2s.ply', data_down, data_color)
148 | stl_color = np.tile(B, (stl.shape[0], 1))
149 | stl_alpha = dist_s2d.clip(max=vis_dist) / vis_dist
150 | stl_color[ np.where(above)[0] ] = R * stl_alpha + W * (1-stl_alpha)
151 | stl_color[ np.where(above)[0][dist_s2d[:,0] >= max_dist] ] = G
152 | write_vis_pcd(f'{args.vis_out_dir}/vis_{args.scan:03}_s2d.ply', stl, stl_color)
153 |
154 | pbar.update(1)
155 | pbar.set_description('done')
156 | pbar.close()
157 | over_all = (mean_d2s + mean_s2d) / 2
158 | print(mean_d2s, mean_s2d, over_all)
159 |
160 | import json
161 | print('Saving results to JSON to path:', f'{args.vis_out_dir}/results.json')
162 | with open(f'{args.vis_out_dir}/results.json', 'w') as fp:
163 | json.dump({
164 | 'mean_d2s': mean_d2s,
165 | 'mean_s2d': mean_s2d,
166 | 'overall': over_all,
167 | }, fp, indent=True)
--------------------------------------------------------------------------------
/scripts/eval_dtu/evaluate_single_scene.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import torch.nn as nn
3 | import torch.nn.functional as F
4 | import cv2
5 | import numpy as np
6 | import os
7 | import glob
8 | from skimage.morphology import binary_dilation, disk
9 | import argparse
10 |
11 | import trimesh
12 | from pathlib import Path
13 | import subprocess
14 |
15 | import sys
16 | import render_utils as rend_util
17 | from tqdm import tqdm
18 |
19 | def cull_scan(scan, mesh_path, result_mesh_file, instance_dir):
20 |
21 | print("Culling scan %s" % scan)
22 | print("Loading mesh %s" % mesh_path)
23 | print("Loading instance dir %s" % instance_dir)
24 |
25 | # load poses
26 | image_dir = '{0}/images'.format(instance_dir)
27 | image_paths = sorted(glob.glob(os.path.join(image_dir, "*.png")))
28 | n_images = len(image_paths)
29 | cam_file = '{0}/cameras.npz'.format(instance_dir)
30 | camera_dict = np.load(cam_file)
31 | scale_mats = [camera_dict['scale_mat_%d' % idx].astype(np.float32) for idx in range(n_images)]
32 | world_mats = [camera_dict['world_mat_%d' % idx].astype(np.float32) for idx in range(n_images)]
33 |
34 | intrinsics_all = []
35 | pose_all = []
36 | for scale_mat, world_mat in zip(scale_mats, world_mats):
37 | P = world_mat @ scale_mat
38 | P = P[:3, :4]
39 | intrinsics, pose = rend_util.load_K_Rt_from_P(None, P)
40 | intrinsics_all.append(torch.from_numpy(intrinsics).float())
41 | pose_all.append(torch.from_numpy(pose).float())
42 |
43 | # load mask
44 | mask_dir = '{0}/mask'.format(instance_dir)
45 | print("Loading mask dir %s" % mask_dir)
46 | mask_paths = sorted(glob.glob(os.path.join(mask_dir, "*.png")))
47 | masks = []
48 | for p in mask_paths:
49 | mask = cv2.imread(p)
50 | masks.append(mask)
51 |
52 | # hard-coded image shape
53 | W, H = 1600, 1200
54 |
55 | # load mesh
56 | mesh = trimesh.load(mesh_path)
57 |
58 | # load transformation matrix
59 |
60 | vertices = mesh.vertices
61 |
62 | # project and filter
63 | vertices = torch.from_numpy(vertices).cuda()
64 | vertices = torch.cat((vertices, torch.ones_like(vertices[:, :1])), dim=-1)
65 | vertices = vertices.permute(1, 0)
66 | vertices = vertices.float()
67 |
68 | sampled_masks = []
69 | for i in tqdm(range(n_images), desc="Culling mesh given masks"):
70 | pose = pose_all[i]
71 | w2c = torch.inverse(pose).cuda()
72 | intrinsic = intrinsics_all[i].cuda()
73 |
74 | with torch.no_grad():
75 | # transform and project
76 | cam_points = intrinsic @ w2c @ vertices
77 | pix_coords = cam_points[:2, :] / (cam_points[2, :].unsqueeze(0) + 1e-6)
78 | pix_coords = pix_coords.permute(1, 0)
79 | pix_coords[..., 0] /= W - 1
80 | pix_coords[..., 1] /= H - 1
81 | pix_coords = (pix_coords - 0.5) * 2
82 | valid = ((pix_coords > -1. ) & (pix_coords < 1.)).all(dim=-1).float()
83 |
84 | # dialate mask similar to unisurf
85 | maski = masks[i][:, :, 0].astype(np.float32) / 256.
86 | maski = torch.from_numpy(binary_dilation(maski, disk(24))).float()[None, None].cuda()
87 |
88 | sampled_mask = F.grid_sample(maski, pix_coords[None, None], mode='nearest', padding_mode='zeros', align_corners=True)[0, -1, 0]
89 |
90 | sampled_mask = sampled_mask + (1. - valid)
91 | sampled_masks.append(sampled_mask)
92 |
93 | sampled_masks = torch.stack(sampled_masks, -1)
94 | # filter
95 |
96 | mask = (sampled_masks > 0.).all(dim=-1).cpu().numpy()
97 | face_mask = mask[mesh.faces].all(axis=1)
98 |
99 | mesh.update_vertices(mask)
100 | mesh.update_faces(face_mask)
101 |
102 | # transform vertices to world
103 | scale_mat = scale_mats[0]
104 | mesh.vertices = mesh.vertices * scale_mat[0, 0] + scale_mat[:3, 3][None]
105 | mesh.export(result_mesh_file)
106 | del mesh
107 |
108 |
109 | if __name__ == "__main__":
110 |
111 | parser = argparse.ArgumentParser(
112 | description='Arguments to evaluate the mesh.'
113 | )
114 |
115 | parser.add_argument('--input_mesh', type=str, help='path to the mesh to be evaluated')
116 | parser.add_argument('--scan_id', type=str, help='scan id of the input mesh')
117 | parser.add_argument('--output_dir', type=str, default='evaluation_results_single', help='path to the output folder')
118 | parser.add_argument('--mask_dir', type=str, default='mask', help='path to uncropped mask')
119 | parser.add_argument('--DTU', type=str, default='Offical_DTU_Dataset', help='path to the GT DTU point clouds')
120 | args = parser.parse_args()
121 |
122 | Offical_DTU_Dataset = args.DTU
123 | out_dir = args.output_dir
124 | Path(out_dir).mkdir(parents=True, exist_ok=True)
125 |
126 | scan = args.scan_id
127 | ply_file = args.input_mesh
128 | print("cull mesh ....")
129 | result_mesh_file = os.path.join(out_dir, "culled_mesh.ply")
130 | cull_scan(scan, ply_file, result_mesh_file, instance_dir=os.path.join(args.mask_dir, f'scan{args.scan_id}'))
131 |
132 | script_dir = os.path.dirname(os.path.abspath(__file__))
133 | cmd = f"python {script_dir}/eval.py --data {result_mesh_file} --scan {scan} --mode mesh --dataset_dir {Offical_DTU_Dataset} --vis_out_dir {out_dir}"
134 | os.system(cmd)
--------------------------------------------------------------------------------
/scripts/eval_dtu/render_utils.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import imageio
3 | import skimage
4 | import cv2
5 | import torch
6 | from torch.nn import functional as F
7 |
8 |
9 | def get_psnr(img1, img2, normalize_rgb=False):
10 | if normalize_rgb: # [-1,1] --> [0,1]
11 | img1 = (img1 + 1.) / 2.
12 | img2 = (img2 + 1. ) / 2.
13 |
14 | mse = torch.mean((img1 - img2) ** 2)
15 | psnr = -10. * torch.log(mse) / torch.log(torch.Tensor([10.]).cuda())
16 |
17 | return psnr
18 |
19 |
20 | def load_rgb(path, normalize_rgb = False):
21 | img = imageio.imread(path)
22 | img = skimage.img_as_float32(img)
23 |
24 | if normalize_rgb: # [-1,1] --> [0,1]
25 | img -= 0.5
26 | img *= 2.
27 | img = img.transpose(2, 0, 1)
28 | return img
29 |
30 |
31 | def load_K_Rt_from_P(filename, P=None):
32 | if P is None:
33 | lines = open(filename).read().splitlines()
34 | if len(lines) == 4:
35 | lines = lines[1:]
36 | lines = [[x[0], x[1], x[2], x[3]] for x in (x.split(" ") for x in lines)]
37 | P = np.asarray(lines).astype(np.float32).squeeze()
38 |
39 | out = cv2.decomposeProjectionMatrix(P)
40 | K = out[0]
41 | R = out[1]
42 | t = out[2]
43 |
44 | K = K/K[2,2]
45 | intrinsics = np.eye(4)
46 | intrinsics[:3, :3] = K
47 |
48 | pose = np.eye(4, dtype=np.float32)
49 | pose[:3, :3] = R.transpose()
50 | pose[:3,3] = (t[:3] / t[3])[:,0]
51 |
52 | return intrinsics, pose
53 |
54 |
55 | def get_camera_params(uv, pose, intrinsics):
56 | if pose.shape[1] == 7: #In case of quaternion vector representation
57 | cam_loc = pose[:, 4:]
58 | R = quat_to_rot(pose[:,:4])
59 | p = torch.eye(4).repeat(pose.shape[0],1,1).cuda().float()
60 | p[:, :3, :3] = R
61 | p[:, :3, 3] = cam_loc
62 | else: # In case of pose matrix representation
63 | cam_loc = pose[:, :3, 3]
64 | p = pose
65 |
66 | batch_size, num_samples, _ = uv.shape
67 |
68 | depth = torch.ones((batch_size, num_samples)).cuda()
69 | x_cam = uv[:, :, 0].view(batch_size, -1)
70 | y_cam = uv[:, :, 1].view(batch_size, -1)
71 | z_cam = depth.view(batch_size, -1)
72 |
73 | pixel_points_cam = lift(x_cam, y_cam, z_cam, intrinsics=intrinsics)
74 |
75 | # permute for batch matrix product
76 | pixel_points_cam = pixel_points_cam.permute(0, 2, 1)
77 |
78 | world_coords = torch.bmm(p, pixel_points_cam).permute(0, 2, 1)[:, :, :3]
79 | ray_dirs = world_coords - cam_loc[:, None, :]
80 | ray_dirs = F.normalize(ray_dirs, dim=2)
81 |
82 | return ray_dirs, cam_loc
83 |
84 |
85 | def get_camera_for_plot(pose):
86 | if pose.shape[1] == 7: #In case of quaternion vector representation
87 | cam_loc = pose[:, 4:].detach()
88 | R = quat_to_rot(pose[:,:4].detach())
89 | else: # In case of pose matrix representation
90 | cam_loc = pose[:, :3, 3]
91 | R = pose[:, :3, :3]
92 | cam_dir = R[:, :3, 2]
93 | return cam_loc, cam_dir
94 |
95 |
96 | def lift(x, y, z, intrinsics):
97 | # parse intrinsics
98 | intrinsics = intrinsics.cuda()
99 | fx = intrinsics[:, 0, 0]
100 | fy = intrinsics[:, 1, 1]
101 | cx = intrinsics[:, 0, 2]
102 | cy = intrinsics[:, 1, 2]
103 | sk = intrinsics[:, 0, 1]
104 |
105 | x_lift = (x - cx.unsqueeze(-1) + cy.unsqueeze(-1)*sk.unsqueeze(-1)/fy.unsqueeze(-1) - sk.unsqueeze(-1)*y/fy.unsqueeze(-1)) / fx.unsqueeze(-1) * z
106 | y_lift = (y - cy.unsqueeze(-1)) / fy.unsqueeze(-1) * z
107 |
108 | # homogeneous
109 | return torch.stack((x_lift, y_lift, z, torch.ones_like(z).cuda()), dim=-1)
110 |
111 |
112 | def quat_to_rot(q):
113 | batch_size, _ = q.shape
114 | q = F.normalize(q, dim=1)
115 | R = torch.ones((batch_size, 3,3)).cuda()
116 | qr=q[:,0]
117 | qi = q[:, 1]
118 | qj = q[:, 2]
119 | qk = q[:, 3]
120 | R[:, 0, 0]=1-2 * (qj**2 + qk**2)
121 | R[:, 0, 1] = 2 * (qj *qi -qk*qr)
122 | R[:, 0, 2] = 2 * (qi * qk + qr * qj)
123 | R[:, 1, 0] = 2 * (qj * qi + qk * qr)
124 | R[:, 1, 1] = 1-2 * (qi**2 + qk**2)
125 | R[:, 1, 2] = 2*(qj*qk - qi*qr)
126 | R[:, 2, 0] = 2 * (qk * qi-qj * qr)
127 | R[:, 2, 1] = 2 * (qj*qk + qi*qr)
128 | R[:, 2, 2] = 1-2 * (qi**2 + qj**2)
129 | return R
130 |
131 |
132 | def rot_to_quat(R):
133 | batch_size, _,_ = R.shape
134 | q = torch.ones((batch_size, 4)).cuda()
135 |
136 | R00 = R[:, 0,0]
137 | R01 = R[:, 0, 1]
138 | R02 = R[:, 0, 2]
139 | R10 = R[:, 1, 0]
140 | R11 = R[:, 1, 1]
141 | R12 = R[:, 1, 2]
142 | R20 = R[:, 2, 0]
143 | R21 = R[:, 2, 1]
144 | R22 = R[:, 2, 2]
145 |
146 | q[:,0]=torch.sqrt(1.0+R00+R11+R22)/2
147 | q[:, 1]=(R21-R12)/(4*q[:,0])
148 | q[:, 2] = (R02 - R20) / (4 * q[:, 0])
149 | q[:, 3] = (R10 - R01) / (4 * q[:, 0])
150 | return q
151 |
152 |
153 | def get_sphere_intersections(cam_loc, ray_directions, r = 1.0):
154 | # Input: n_rays x 3 ; n_rays x 3
155 | # Output: n_rays x 1, n_rays x 1 (close and far)
156 |
157 | ray_cam_dot = torch.bmm(ray_directions.view(-1, 1, 3),
158 | cam_loc.view(-1, 3, 1)).squeeze(-1)
159 | under_sqrt = ray_cam_dot ** 2 - (cam_loc.norm(2, 1, keepdim=True) ** 2 - r ** 2)
160 |
161 | # sanity check
162 | if (under_sqrt <= 0).sum() > 0:
163 | print('BOUNDING SPHERE PROBLEM!')
164 | exit()
165 |
166 | sphere_intersections = torch.sqrt(under_sqrt) * torch.Tensor([-1, 1]).cuda().float() - ray_cam_dot
167 | sphere_intersections = sphere_intersections.clamp_min(0.0)
168 |
169 | return sphere_intersections
--------------------------------------------------------------------------------
/scripts/eval_tnt/README.md:
--------------------------------------------------------------------------------
1 | # Python Toolbox for Evaluation
2 |
3 | This Python script evaluates **training** dataset of TanksAndTemples benchmark.
4 | The script requires ``Open3D`` and a few Python packages such as ``matplotlib``, ``json``, and ``numpy``.
5 |
6 | ## How to use:
7 | **Step 0**. Reconstruct 3D models and recover camera poses from the training dataset.
8 | The raw videos of the training dataset can be found from:
9 | https://tanksandtemples.org/download/
10 |
11 | **Step 1**. Download evaluation data (ground truth geometry + reference reconstruction) using
12 | [this link](https://drive.google.com/open?id=1UoKPiUUsKa0AVHFOrnMRhc5hFngjkE-t). In this example, we regard ``TanksAndTemples/evaluation/data/`` as a dataset folder.
13 |
14 | **Step 2**. Install Open3D. Follow instructions in http://open3d.org/docs/getting_started.html
15 |
16 | **Step 3**. Run the evaluation script and grab some coffee.
17 | ```
18 | # firstly, run cull_mesh.py to cull mesh and then
19 | ./run.sh Barn
20 | ```
21 | Output (evaluation of Ignatius):
22 | ```
23 | ===========================
24 | Evaluating Ignatius
25 | ===========================
26 | path/to/TanksAndTemples/evaluation/data/Ignatius/Ignatius_COLMAP.ply
27 | Reading PLY: [========================================] 100%
28 | Read PointCloud: 6929586 vertices.
29 | path/to/TanksAndTemples/evaluation/data/Ignatius/Ignatius.ply
30 | Reading PLY: [========================================] 100%
31 | :
32 | ICP Iteration #0: Fitness 0.9980, RMSE 0.0044
33 | ICP Iteration #1: Fitness 0.9980, RMSE 0.0043
34 | ICP Iteration #2: Fitness 0.9980, RMSE 0.0043
35 | ICP Iteration #3: Fitness 0.9980, RMSE 0.0043
36 | ICP Iteration #4: Fitness 0.9980, RMSE 0.0042
37 | ICP Iteration #5: Fitness 0.9980, RMSE 0.0042
38 | ICP Iteration #6: Fitness 0.9979, RMSE 0.0042
39 | ICP Iteration #7: Fitness 0.9979, RMSE 0.0042
40 | ICP Iteration #8: Fitness 0.9979, RMSE 0.0042
41 | ICP Iteration #9: Fitness 0.9979, RMSE 0.0042
42 | ICP Iteration #10: Fitness 0.9979, RMSE 0.0042
43 | [EvaluateHisto]
44 | Cropping geometry: [========================================] 100%
45 | Pointcloud down sampled from 6929586 points to 1449840 points.
46 | Pointcloud down sampled from 1449840 points to 1365628 points.
47 | path/to/TanksAndTemples/evaluation/data/Ignatius/evaluation//Ignatius.precision.ply
48 | Cropping geometry: [========================================] 100%
49 | Pointcloud down sampled from 5016769 points to 4957123 points.
50 | Pointcloud down sampled from 4957123 points to 4181506 points.
51 | [compute_point_cloud_to_point_cloud_distance]
52 | [compute_point_cloud_to_point_cloud_distance]
53 | :
54 | [ViewDistances] Add color coding to visualize error
55 | [ViewDistances] Add color coding to visualize error
56 | :
57 | [get_f1_score_histo2]
58 | ==============================
59 | evaluation result : Ignatius
60 | ==============================
61 | distance tau : 0.003
62 | precision : 0.7679
63 | recall : 0.7937
64 | f-score : 0.7806
65 | ==============================
66 | ```
67 |
68 | **Step 5**. Go to the evaluation folder. ``TanksAndTemples/evaluation/data/Ignatius/evaluation/`` will have the following outputs.
69 |
70 |
71 |
72 | ``PR_Ignatius_@d_th_0_0030.pdf`` (Precision and recall curves with a F-score)
73 |
74 | |
|
|
75 | |--|--|
76 | | ``Ignatius.precision.ply`` | ``Ignatius.recall.ply`` |
77 |
78 | (3D visualization of precision and recall. Each mesh is color coded using hot colormap)
79 |
80 | # Requirements
81 |
82 | - Python 3
83 | - open3d v0.9.0
84 | - matplotlib
85 |
--------------------------------------------------------------------------------
/scripts/eval_tnt/compute_bbox_for_mesh.py:
--------------------------------------------------------------------------------
1 | # ----------------------------------------------------------------------------
2 | # - TanksAndTemples Website Toolbox -
3 | # - http://www.tanksandtemples.org -
4 | # ----------------------------------------------------------------------------
5 | # The MIT License (MIT)
6 | #
7 | # Copyright (c) 2017
8 | # Arno Knapitsch
9 | # Jaesik Park
10 | # Qian-Yi Zhou
11 | # Vladlen Koltun
12 | #
13 | # Permission is hereby granted, free of charge, to any person obtaining a copy
14 | # of this software and associated documentation files (the "Software"), to deal
15 | # in the Software without restriction, including without limitation the rights
16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 | # copies of the Software, and to permit persons to whom the Software is
18 | # furnished to do so, subject to the following conditions:
19 | #
20 | # The above copyright notice and this permission notice shall be included in
21 | # all copies or substantial portions of the Software.
22 | #
23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29 | # THE SOFTWARE.
30 | # ----------------------------------------------------------------------------
31 | #
32 | # This python script is for downloading dataset from www.tanksandtemples.org
33 | # The dataset has a different license, please refer to
34 | # https://tanksandtemples.org/license/
35 |
36 | # this script requires Open3D python binding
37 | # please follow the intructions in setup.py before running this script.
38 | import numpy as np
39 | import open3d as o3d
40 | import os
41 | import argparse
42 | import torch
43 |
44 | from config import scenes_tau_dict
45 | from registration import (
46 | trajectory_alignment,
47 | registration_vol_ds,
48 | registration_unif,
49 | read_trajectory,
50 | )
51 | from help_func import auto_orient_and_center_poses
52 | from trajectory_io import CameraPose
53 | from evaluation import EvaluateHisto
54 | from util import make_dir
55 | from plot import plot_graph
56 |
57 |
58 | def run_evaluation(dataset_dir, traj_path, ply_path, out_dir, view_crop):
59 | scene = os.path.basename(os.path.normpath(dataset_dir))
60 |
61 | if scene not in scenes_tau_dict:
62 | print(dataset_dir, scene)
63 | raise Exception("invalid dataset-dir, not in scenes_tau_dict")
64 |
65 | print("")
66 | print("===========================")
67 | print("Evaluating %s" % scene)
68 | print("===========================")
69 |
70 | dTau = scenes_tau_dict[scene]
71 | # put the crop-file, the GT file, the COLMAP SfM log file and
72 | # the alignment of the according scene in a folder of
73 | # the same scene name in the dataset_dir
74 | colmap_ref_logfile = os.path.join(dataset_dir, scene + "_COLMAP_SfM.log")
75 |
76 | # this is for groundtruth pointcloud, we can use it
77 | alignment = os.path.join(dataset_dir, scene + "_trans.txt")
78 | gt_filen = os.path.join(dataset_dir, scene + ".ply")
79 | # this crop file is also w.r.t the groundtruth pointcloud, we can use it.
80 | # Otherwise we have to crop the estimated pointcloud by ourself
81 | cropfile = os.path.join(dataset_dir, scene + ".json")
82 | # this is not so necessary
83 | map_file = os.path.join(dataset_dir, scene + "_mapping_reference.txt")
84 | if not os.path.isfile(map_file):
85 | map_file = None
86 | map_file = None
87 |
88 | make_dir(out_dir)
89 |
90 | # Load reconstruction and according GT
91 | print(ply_path)
92 | pcd = o3d.io.read_point_cloud(ply_path)
93 | print(gt_filen)
94 | gt_pcd = o3d.io.read_point_cloud(gt_filen)
95 |
96 | gt_trans = np.loadtxt(alignment)
97 | print(traj_path)
98 | traj_to_register = []
99 | if traj_path.endswith('.npy'):
100 | ld = np.load(traj_path)
101 | for i in range(len(ld)):
102 | traj_to_register.append(CameraPose(meta=None, mat=ld[i]))
103 | elif traj_path.endswith('.json'): # instant-npg or sdfstudio format
104 | import json
105 | with open(traj_path, encoding='UTF-8') as f:
106 | meta = json.load(f)
107 | poses_dict = {}
108 | for i, frame in enumerate(meta['frames']):
109 | filepath = frame['file_path']
110 | new_i = int(filepath[13:18]) - 1
111 | poses_dict[new_i] = np.array(frame['transform_matrix'])
112 | poses = []
113 | for i in range(len(poses_dict)):
114 | poses.append(poses_dict[i])
115 | poses = torch.from_numpy(np.array(poses).astype(np.float32))
116 | poses, _ = auto_orient_and_center_poses(poses, method='up', center_poses=True)
117 | scale_factor = 1.0 / float(torch.max(torch.abs(poses[:, :3, 3])))
118 | poses[:, :3, 3] *= scale_factor
119 | poses = poses.numpy()
120 | for i in range(len(poses)):
121 | traj_to_register.append(CameraPose(meta=None, mat=poses[i]))
122 |
123 | else:
124 | traj_to_register = read_trajectory(traj_path)
125 | print(colmap_ref_logfile)
126 | gt_traj_col = read_trajectory(colmap_ref_logfile)
127 |
128 | trajectory_transform = trajectory_alignment(map_file, traj_to_register,
129 | gt_traj_col, gt_trans, scene)
130 | inv_transform = np.linalg.inv(trajectory_transform)
131 | points = np.asarray(gt_pcd.points)
132 | points = points @ inv_transform[:3, :3].T + inv_transform[:3, 3:].T
133 | print(points.min(axis=0), points.max(axis=0))
134 | print(np.concatenate([points.min(axis=0), points.max(axis=0)]).reshape(-1).tolist())
135 |
136 |
137 | if __name__ == "__main__":
138 | parser = argparse.ArgumentParser()
139 | parser.add_argument(
140 | "--dataset-dir",
141 | type=str,
142 | required=True,
143 | help="path to a dataset/scene directory containing X.json, X.ply, ...",
144 | )
145 | parser.add_argument(
146 | "--traj-path",
147 | type=str,
148 | required=True,
149 | help=
150 | "path to trajectory file. See `convert_to_logfile.py` to create this file.",
151 | )
152 | parser.add_argument(
153 | "--ply-path",
154 | type=str,
155 | required=True,
156 | help="path to reconstruction ply file",
157 | )
158 | parser.add_argument(
159 | "--out-dir",
160 | type=str,
161 | default="",
162 | help=
163 | "output directory, default: an evaluation directory is created in the directory of the ply file",
164 | )
165 | parser.add_argument(
166 | "--view-crop",
167 | type=int,
168 | default=0,
169 | help="whether view the crop pointcloud after aligned",
170 | )
171 | args = parser.parse_args()
172 |
173 | args.view_crop = False # (args.view_crop > 0)
174 | if args.out_dir.strip() == "":
175 | args.out_dir = os.path.join(os.path.dirname(args.ply_path),
176 | "evaluation")
177 |
178 | run_evaluation(
179 | dataset_dir=args.dataset_dir,
180 | traj_path=args.traj_path,
181 | ply_path=args.ply_path,
182 | out_dir=args.out_dir,
183 | view_crop=args.view_crop
184 | )
185 |
--------------------------------------------------------------------------------
/scripts/eval_tnt/config.py:
--------------------------------------------------------------------------------
1 | # ----------------------------------------------------------------------------
2 | # - TanksAndTemples Website Toolbox -
3 | # - http://www.tanksandtemples.org -
4 | # ----------------------------------------------------------------------------
5 | # The MIT License (MIT)
6 | #
7 | # Copyright (c) 2017
8 | # Arno Knapitsch
9 | # Jaesik Park
10 | # Qian-Yi Zhou
11 | # Vladlen Koltun
12 | #
13 | # Permission is hereby granted, free of charge, to any person obtaining a copy
14 | # of this software and associated documentation files (the "Software"), to deal
15 | # in the Software without restriction, including without limitation the rights
16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 | # copies of the Software, and to permit persons to whom the Software is
18 | # furnished to do so, subject to the following conditions:
19 | #
20 | # The above copyright notice and this permission notice shall be included in
21 | # all copies or substantial portions of the Software.
22 | #
23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29 | # THE SOFTWARE.
30 | # ----------------------------------------------------------------------------
31 |
32 | # some global parameters - do not modify
33 | scenes_tau_dict = {
34 | "Barn": 0.01,
35 | "Caterpillar": 0.005,
36 | "Church": 0.025,
37 | "Courthouse": 0.025,
38 | "Ignatius": 0.003,
39 | "Meetingroom": 0.01,
40 | "Truck": 0.005,
41 | }
42 |
--------------------------------------------------------------------------------
/scripts/eval_tnt/evaluate_single_scene.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import torch.nn as nn
3 | import torch.nn.functional as F
4 | import cv2
5 | import numpy as np
6 | import os
7 | import glob
8 | from skimage.morphology import binary_dilation, disk
9 | import argparse
10 |
11 | import trimesh
12 | from pathlib import Path
13 | import subprocess
14 | import sys
15 | import json
16 |
17 |
18 | if __name__ == "__main__":
19 |
20 | parser = argparse.ArgumentParser(
21 | description='Arguments to evaluate the mesh.'
22 | )
23 |
24 | parser.add_argument('--input_mesh', type=str, help='path to the mesh to be evaluated')
25 | parser.add_argument('--scene', type=str, help='scan id of the input mesh')
26 | parser.add_argument('--output_dir', type=str, default='evaluation_results_single', help='path to the output folder')
27 | parser.add_argument('--TNT', type=str, default='Offical_DTU_Dataset', help='path to the GT DTU point clouds')
28 | args = parser.parse_args()
29 |
30 |
31 | TNT_Dataset = args.TNT
32 | out_dir = args.output_dir
33 | Path(out_dir).mkdir(parents=True, exist_ok=True)
34 | scene = args.scene
35 | ply_file = args.input_mesh
36 | result_mesh_file = os.path.join(out_dir, "culled_mesh.ply")
37 | # read scene.json
38 | f"python run.py --dataset-dir {ply_file} --traj-path {TNT_Dataset}/{scene}/{scene}_COLMAP_SfM.log --ply-path {TNT_Dataset}/{scene}/{scene}_COLMAP.ply"
--------------------------------------------------------------------------------
/scripts/eval_tnt/evaluation.py:
--------------------------------------------------------------------------------
1 | # ----------------------------------------------------------------------------
2 | # - TanksAndTemples Website Toolbox -
3 | # - http://www.tanksandtemples.org -
4 | # ----------------------------------------------------------------------------
5 | # The MIT License (MIT)
6 | #
7 | # Copyright (c) 2017
8 | # Arno Knapitsch
9 | # Jaesik Park
10 | # Qian-Yi Zhou
11 | # Vladlen Koltun
12 | #
13 | # Permission is hereby granted, free of charge, to any person obtaining a copy
14 | # of this software and associated documentation files (the "Software"), to deal
15 | # in the Software without restriction, including without limitation the rights
16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 | # copies of the Software, and to permit persons to whom the Software is
18 | # furnished to do so, subject to the following conditions:
19 | #
20 | # The above copyright notice and this permission notice shall be included in
21 | # all copies or substantial portions of the Software.
22 | #
23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29 | # THE SOFTWARE.
30 | # ----------------------------------------------------------------------------
31 | #
32 | # This python script is for downloading dataset from www.tanksandtemples.org
33 | # The dataset has a different license, please refer to
34 | # https://tanksandtemples.org/license/
35 |
36 | import json
37 | import copy
38 | import os
39 | import numpy as np
40 | import open3d as o3d
41 | import matplotlib.pyplot as plt
42 |
43 |
44 | def read_alignment_transformation(filename):
45 | with open(filename) as data_file:
46 | data = json.load(data_file)
47 | return np.asarray(data["transformation"]).reshape((4, 4)).transpose()
48 |
49 |
50 | def write_color_distances(path, pcd, distances, max_distance):
51 | o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Debug)
52 | # cmap = plt.get_cmap("afmhot")
53 | cmap = plt.get_cmap("hot_r")
54 | distances = np.array(distances)
55 | colors = cmap(np.minimum(distances, max_distance) / max_distance)[:, :3]
56 | pcd.colors = o3d.utility.Vector3dVector(colors)
57 | o3d.io.write_point_cloud(path, pcd)
58 |
59 |
60 | def EvaluateHisto(
61 | source,
62 | target,
63 | trans,
64 | crop_volume,
65 | voxel_size,
66 | threshold,
67 | filename_mvs,
68 | plot_stretch,
69 | scene_name,
70 | view_crop,
71 | verbose=True,
72 | ):
73 | print("[EvaluateHisto]")
74 | o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Debug)
75 | s = copy.deepcopy(source)
76 | s.transform(trans)
77 | if crop_volume is not None:
78 | s = crop_volume.crop_point_cloud(s)
79 | if view_crop:
80 | o3d.visualization.draw_geometries([s, ])
81 | else:
82 | print("No bounding box provided to crop estimated point cloud, leaving it as the loaded version!!")
83 | s = s.voxel_down_sample(voxel_size)
84 | s.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamKNN(knn=20))
85 | print(filename_mvs + "/" + scene_name + ".precision.ply")
86 |
87 | t = copy.deepcopy(target)
88 | if crop_volume is not None:
89 | t = crop_volume.crop_point_cloud(t)
90 | else:
91 | print("No bounding box provided to crop groundtruth point cloud, leaving it as the loaded version!!")
92 |
93 | t = t.voxel_down_sample(voxel_size)
94 | t.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamKNN(knn=20))
95 | print("[compute_point_cloud_to_point_cloud_distance]")
96 | distance1 = s.compute_point_cloud_distance(t)
97 | print("[compute_point_cloud_to_point_cloud_distance]")
98 | distance2 = t.compute_point_cloud_distance(s)
99 |
100 | # write the distances to bin files
101 | # np.array(distance1).astype("float64").tofile(
102 | # filename_mvs + "/" + scene_name + ".precision.bin"
103 | # )
104 | # np.array(distance2).astype("float64").tofile(
105 | # filename_mvs + "/" + scene_name + ".recall.bin"
106 | # )
107 |
108 | # Colorize the poincloud files prith the precision and recall values
109 | # o3d.io.write_point_cloud(
110 | # filename_mvs + "/" + scene_name + ".precision.ply", s
111 | # )
112 | # o3d.io.write_point_cloud(
113 | # filename_mvs + "/" + scene_name + ".precision.ncb.ply", s
114 | # )
115 | # o3d.io.write_point_cloud(filename_mvs + "/" + scene_name + ".recall.ply", t)
116 |
117 | source_n_fn = filename_mvs + "/" + scene_name + ".precision.ply"
118 | target_n_fn = filename_mvs + "/" + scene_name + ".recall.ply"
119 |
120 | print("[ViewDistances] Add color coding to visualize error")
121 | # eval_str_viewDT = (
122 | # OPEN3D_EXPERIMENTAL_BIN_PATH
123 | # + "ViewDistances "
124 | # + source_n_fn
125 | # + " --max_distance "
126 | # + str(threshold * 3)
127 | # + " --write_color_back --without_gui"
128 | # )
129 | # os.system(eval_str_viewDT)
130 | write_color_distances(source_n_fn, s, distance1, 3 * threshold)
131 |
132 | print("[ViewDistances] Add color coding to visualize error")
133 | # eval_str_viewDT = (
134 | # OPEN3D_EXPERIMENTAL_BIN_PATH
135 | # + "ViewDistances "
136 | # + target_n_fn
137 | # + " --max_distance "
138 | # + str(threshold * 3)
139 | # + " --write_color_back --without_gui"
140 | # )
141 | # os.system(eval_str_viewDT)
142 | write_color_distances(target_n_fn, t, distance2, 3 * threshold)
143 |
144 | # get histogram and f-score
145 | [
146 | precision,
147 | recall,
148 | fscore,
149 | edges_source,
150 | cum_source,
151 | edges_target,
152 | cum_target,
153 | ] = get_f1_score_histo2(threshold, filename_mvs, plot_stretch, distance1,
154 | distance2)
155 | np.savetxt(filename_mvs + "/" + scene_name + ".recall.txt", cum_target)
156 | np.savetxt(filename_mvs + "/" + scene_name + ".precision.txt", cum_source)
157 | np.savetxt(
158 | filename_mvs + "/" + scene_name + ".prf_tau_plotstr.txt",
159 | np.array([precision, recall, fscore, threshold, plot_stretch]),
160 | )
161 |
162 | return [
163 | precision,
164 | recall,
165 | fscore,
166 | edges_source,
167 | cum_source,
168 | edges_target,
169 | cum_target,
170 | ]
171 |
172 |
173 | def get_f1_score_histo2(threshold,
174 | filename_mvs,
175 | plot_stretch,
176 | distance1,
177 | distance2,
178 | verbose=True):
179 | print("[get_f1_score_histo2]")
180 | dist_threshold = threshold
181 | if len(distance1) and len(distance2):
182 |
183 | recall = float(sum(d < threshold for d in distance2)) / float(
184 | len(distance2))
185 | precision = float(sum(d < threshold for d in distance1)) / float(
186 | len(distance1))
187 | fscore = 2 * recall * precision / (recall + precision)
188 | num = len(distance1)
189 | bins = np.arange(0, dist_threshold * plot_stretch, dist_threshold / 100)
190 | hist, edges_source = np.histogram(distance1, bins)
191 | cum_source = np.cumsum(hist).astype(float) / num
192 |
193 | num = len(distance2)
194 | bins = np.arange(0, dist_threshold * plot_stretch, dist_threshold / 100)
195 | hist, edges_target = np.histogram(distance2, bins)
196 | cum_target = np.cumsum(hist).astype(float) / num
197 |
198 | else:
199 | precision = 0
200 | recall = 0
201 | fscore = 0
202 | edges_source = np.array([0])
203 | cum_source = np.array([0])
204 | edges_target = np.array([0])
205 | cum_target = np.array([0])
206 |
207 | return [
208 | precision,
209 | recall,
210 | fscore,
211 | edges_source,
212 | cum_source,
213 | edges_target,
214 | cum_target,
215 | ]
216 |
--------------------------------------------------------------------------------
/scripts/eval_tnt/help_func.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding=utf-8
3 | import torch
4 |
5 | def rotation_matrix(a, b):
6 | """Compute the rotation matrix that rotates vector a to vector b.
7 |
8 | Args:
9 | a: The vector to rotate.
10 | b: The vector to rotate to.
11 | Returns:
12 | The rotation matrix.
13 | """
14 | a = a / torch.linalg.norm(a)
15 | b = b / torch.linalg.norm(b)
16 | v = torch.cross(a, b)
17 | c = torch.dot(a, b)
18 | # If vectors are exactly opposite, we add a little noise to one of them
19 | if c < -1 + 1e-8:
20 | eps = (torch.rand(3) - 0.5) * 0.01
21 | return rotation_matrix(a + eps, b)
22 | s = torch.linalg.norm(v)
23 | skew_sym_mat = torch.Tensor(
24 | [
25 | [0, -v[2], v[1]],
26 | [v[2], 0, -v[0]],
27 | [-v[1], v[0], 0],
28 | ]
29 | )
30 | return torch.eye(3) + skew_sym_mat + skew_sym_mat @ skew_sym_mat * ((1 - c) / (s**2 + 1e-8))
31 |
32 |
33 | def auto_orient_and_center_poses(
34 | poses, method="up", center_poses=True
35 | ):
36 | """Orients and centers the poses. We provide two methods for orientation: pca and up.
37 |
38 | pca: Orient the poses so that the principal component of the points is aligned with the axes.
39 | This method works well when all of the cameras are in the same plane.
40 | up: Orient the poses so that the average up vector is aligned with the z axis.
41 | This method works well when images are not at arbitrary angles.
42 |
43 |
44 | Args:
45 | poses: The poses to orient.
46 | method: The method to use for orientation.
47 | center_poses: If True, the poses are centered around the origin.
48 |
49 | Returns:
50 | The oriented poses.
51 | """
52 |
53 | translation = poses[..., :3, 3]
54 |
55 | mean_translation = torch.mean(translation, dim=0)
56 | translation_diff = translation - mean_translation
57 |
58 | if center_poses:
59 | translation = mean_translation
60 | else:
61 | translation = torch.zeros_like(mean_translation)
62 |
63 | if method == "pca":
64 | _, eigvec = torch.linalg.eigh(translation_diff.T @ translation_diff)
65 | eigvec = torch.flip(eigvec, dims=(-1,))
66 |
67 | if torch.linalg.det(eigvec) < 0:
68 | eigvec[:, 2] = -eigvec[:, 2]
69 |
70 | transform = torch.cat([eigvec, eigvec @ -translation[..., None]], dim=-1)
71 | oriented_poses = transform @ poses
72 |
73 | if oriented_poses.mean(axis=0)[2, 1] < 0:
74 | oriented_poses[:, 1:3] = -1 * oriented_poses[:, 1:3]
75 | elif method == "up":
76 | up = torch.mean(poses[:, :3, 1], dim=0)
77 | up = up / torch.linalg.norm(up)
78 |
79 | rotation = rotation_matrix(up, torch.Tensor([0, 0, 1]))
80 | transform = torch.cat([rotation, rotation @ -translation[..., None]], dim=-1)
81 | oriented_poses = transform @ poses
82 | elif method == "none":
83 | transform = torch.eye(4)
84 | transform[:3, 3] = -translation
85 | transform = transform[:3, :]
86 | oriented_poses = transform @ poses
87 |
88 | return oriented_poses, transform
89 |
90 |
91 |
--------------------------------------------------------------------------------
/scripts/eval_tnt/plot.py:
--------------------------------------------------------------------------------
1 | # ----------------------------------------------------------------------------
2 | # - TanksAndTemples Website Toolbox -
3 | # - http://www.tanksandtemples.org -
4 | # ----------------------------------------------------------------------------
5 | # The MIT License (MIT)
6 | #
7 | # Copyright (c) 2017
8 | # Arno Knapitsch
9 | # Jaesik Park
10 | # Qian-Yi Zhou
11 | # Vladlen Koltun
12 | #
13 | # Permission is hereby granted, free of charge, to any person obtaining a copy
14 | # of this software and associated documentation files (the "Software"), to deal
15 | # in the Software without restriction, including without limitation the rights
16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 | # copies of the Software, and to permit persons to whom the Software is
18 | # furnished to do so, subject to the following conditions:
19 | #
20 | # The above copyright notice and this permission notice shall be included in
21 | # all copies or substantial portions of the Software.
22 | #
23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29 | # THE SOFTWARE.
30 | # ----------------------------------------------------------------------------
31 | #
32 | # This python script is for downloading dataset from www.tanksandtemples.org
33 | # The dataset has a different license, please refer to
34 | # https://tanksandtemples.org/license/
35 |
36 | import matplotlib.pyplot as plt
37 | from cycler import cycler
38 |
39 |
40 | def plot_graph(
41 | scene,
42 | fscore,
43 | dist_threshold,
44 | edges_source,
45 | cum_source,
46 | edges_target,
47 | cum_target,
48 | plot_stretch,
49 | mvs_outpath,
50 | show_figure=False,
51 | ):
52 | f = plt.figure()
53 | plt_size = [14, 7]
54 | pfontsize = "medium"
55 |
56 | ax = plt.subplot(111)
57 | label_str = "precision"
58 | ax.plot(
59 | edges_source[1::],
60 | cum_source * 100,
61 | c="red",
62 | label=label_str,
63 | linewidth=2.0,
64 | )
65 |
66 | label_str = "recall"
67 | ax.plot(
68 | edges_target[1::],
69 | cum_target * 100,
70 | c="blue",
71 | label=label_str,
72 | linewidth=2.0,
73 | )
74 |
75 | ax.grid(True)
76 | plt.rcParams["figure.figsize"] = plt_size
77 | plt.rc("axes", prop_cycle=cycler("color", ["r", "g", "b", "y"]))
78 | plt.title("Precision and Recall: " + scene + ", " + "%02.2f f-score" %
79 | (fscore * 100))
80 | plt.axvline(x=dist_threshold, c="black", ls="dashed", linewidth=2.0)
81 |
82 | plt.ylabel("# of points (%)", fontsize=15)
83 | plt.xlabel("Meters", fontsize=15)
84 | plt.axis([0, dist_threshold * plot_stretch, 0, 100])
85 | ax.legend(shadow=True, fancybox=True, fontsize=pfontsize)
86 | # plt.axis([0, dist_threshold*plot_stretch, 0, 100])
87 |
88 | plt.setp(ax.get_legend().get_texts(), fontsize=pfontsize)
89 |
90 | plt.legend(loc=2, borderaxespad=0.0, fontsize=pfontsize)
91 | plt.legend(loc=4)
92 | leg = plt.legend(loc="lower right")
93 |
94 | box = ax.get_position()
95 | ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
96 |
97 | # Put a legend to the right of the current axis
98 | ax.legend(loc="center left", bbox_to_anchor=(1, 0.5))
99 | plt.setp(ax.get_legend().get_texts(), fontsize=pfontsize)
100 | png_name = mvs_outpath + "/PR_{0}_@d_th_0_{1}.png".format(
101 | scene, "%04d" % (dist_threshold * 10000))
102 | pdf_name = mvs_outpath + "/PR_{0}_@d_th_0_{1}.pdf".format(
103 | scene, "%04d" % (dist_threshold * 10000))
104 |
105 | # save figure and display
106 | f.savefig(png_name, format="png", bbox_inches="tight")
107 | f.savefig(pdf_name, format="pdf", bbox_inches="tight")
108 | if show_figure:
109 | plt.show()
110 |
--------------------------------------------------------------------------------
/scripts/eval_tnt/registration.py:
--------------------------------------------------------------------------------
1 | # ----------------------------------------------------------------------------
2 | # - TanksAndTemples Website Toolbox -
3 | # - http://www.tanksandtemples.org -
4 | # ----------------------------------------------------------------------------
5 | # The MIT License (MIT)
6 | #
7 | # Copyright (c) 2017
8 | # Arno Knapitsch
9 | # Jaesik Park
10 | # Qian-Yi Zhou
11 | # Vladlen Koltun
12 | #
13 | # Permission is hereby granted, free of charge, to any person obtaining a copy
14 | # of this software and associated documentation files (the "Software"), to deal
15 | # in the Software without restriction, including without limitation the rights
16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 | # copies of the Software, and to permit persons to whom the Software is
18 | # furnished to do so, subject to the following conditions:
19 | #
20 | # The above copyright notice and this permission notice shall be included in
21 | # all copies or substantial portions of the Software.
22 | #
23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29 | # THE SOFTWARE.
30 | # ----------------------------------------------------------------------------
31 | #
32 | # This python script is for downloading dataset from www.tanksandtemples.org
33 | # The dataset has a different license, please refer to
34 | # https://tanksandtemples.org/license/
35 |
36 | from trajectory_io import read_trajectory, convert_trajectory_to_pointcloud
37 | import copy
38 | import numpy as np
39 | import open3d as o3d
40 |
41 | MAX_POINT_NUMBER = 4e6
42 |
43 |
44 | def read_mapping(filename):
45 | mapping = []
46 | with open(filename, "r") as f:
47 | n_sampled_frames = int(f.readline())
48 | n_total_frames = int(f.readline())
49 | mapping = np.zeros(shape=(n_sampled_frames, 2))
50 | metastr = f.readline()
51 | for iter in range(n_sampled_frames):
52 | metadata = list(map(int, metastr.split()))
53 | mapping[iter, :] = metadata
54 | metastr = f.readline()
55 | return [n_sampled_frames, n_total_frames, mapping]
56 |
57 |
58 | def gen_sparse_trajectory(mapping, f_trajectory):
59 | sparse_traj = []
60 | for m in mapping:
61 | sparse_traj.append(f_trajectory[int(m[1] - 1)])
62 | return sparse_traj
63 |
64 |
65 | def trajectory_alignment(map_file, traj_to_register, gt_traj_col, gt_trans,
66 | scene):
67 | traj_pcd_col = convert_trajectory_to_pointcloud(gt_traj_col)
68 | if gt_trans is not None:
69 | traj_pcd_col.transform(gt_trans)
70 | corres = o3d.utility.Vector2iVector(
71 | np.asarray(list(map(lambda x: [x, x], range(len(gt_traj_col))))))
72 | rr = o3d.registration.RANSACConvergenceCriteria()
73 | rr.max_iteration = 100000
74 | rr.max_validation = 100000
75 |
76 | # in this case a log file was used which contains
77 | # every movie frame (see tutorial for details)
78 | if len(traj_to_register) > 1600 and map_file is not None:
79 | n_sampled_frames, n_total_frames, mapping = read_mapping(map_file)
80 | traj_col2 = gen_sparse_trajectory(mapping, traj_to_register)
81 | traj_to_register_pcd = convert_trajectory_to_pointcloud(traj_col2)
82 | else:
83 | print("Estimated trajectory will leave as it is, no sparsity op is performed!")
84 | traj_to_register_pcd = convert_trajectory_to_pointcloud(
85 | traj_to_register)
86 | randomvar = 0.0
87 | if randomvar < 1e-5:
88 | traj_to_register_pcd_rand = traj_to_register_pcd
89 | else:
90 | nr_of_cam_pos = len(traj_to_register_pcd.points)
91 | rand_number_added = np.asanyarray(traj_to_register_pcd.points) * (
92 | np.random.rand(nr_of_cam_pos, 3) * randomvar - randomvar / 2.0 + 1)
93 | list_rand = list(rand_number_added)
94 | traj_to_register_pcd_rand = o3d.geometry.PointCloud()
95 | for elem in list_rand:
96 | traj_to_register_pcd_rand.points.append(elem)
97 |
98 | # Rough registration based on aligned colmap SfM data
99 | reg = o3d.registration.registration_ransac_based_on_correspondence(
100 | traj_to_register_pcd_rand,
101 | traj_pcd_col,
102 | corres,
103 | 0.2,
104 | o3d.registration.TransformationEstimationPointToPoint(True),
105 | 6,
106 | rr,
107 | )
108 | return reg.transformation
109 |
110 |
111 | def crop_and_downsample(
112 | pcd,
113 | crop_volume,
114 | down_sample_method="voxel",
115 | voxel_size=0.01,
116 | trans=np.identity(4),
117 | ):
118 | pcd_copy = copy.deepcopy(pcd)
119 | pcd_copy.transform(trans)
120 | pcd_crop = crop_volume.crop_point_cloud(pcd_copy)
121 | if down_sample_method == "voxel":
122 | # return voxel_down_sample(pcd_crop, voxel_size)
123 | return pcd_crop.voxel_down_sample(voxel_size)
124 | elif down_sample_method == "uniform":
125 | n_points = len(pcd_crop.points)
126 | if n_points > MAX_POINT_NUMBER:
127 | ds_rate = int(round(n_points / float(MAX_POINT_NUMBER)))
128 | return pcd_crop.uniform_down_sample(ds_rate)
129 | return pcd_crop
130 |
131 |
132 | def registration_unif(
133 | source,
134 | gt_target,
135 | init_trans,
136 | crop_volume,
137 | threshold,
138 | max_itr,
139 | max_size=4 * MAX_POINT_NUMBER,
140 | verbose=True,
141 | ):
142 | if verbose:
143 | print("[Registration] threshold: %f" % threshold)
144 | o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Debug)
145 | s = crop_and_downsample(source,
146 | crop_volume,
147 | down_sample_method="uniform",
148 | trans=init_trans)
149 | t = crop_and_downsample(gt_target,
150 | crop_volume,
151 | down_sample_method="uniform")
152 | reg = o3d.registration.registration_icp(
153 | s,
154 | t,
155 | threshold,
156 | np.identity(4),
157 | o3d.registration.TransformationEstimationPointToPoint(True),
158 | o3d.registration.ICPConvergenceCriteria(1e-6, max_itr),
159 | )
160 | reg.transformation = np.matmul(reg.transformation, init_trans)
161 | return reg
162 |
163 |
164 | def registration_vol_ds(
165 | source,
166 | gt_target,
167 | init_trans,
168 | crop_volume,
169 | voxel_size,
170 | threshold,
171 | max_itr,
172 | verbose=True,
173 | ):
174 | if verbose:
175 | print("[Registration] voxel_size: %f, threshold: %f" %
176 | (voxel_size, threshold))
177 | o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Debug)
178 | s = crop_and_downsample(
179 | source,
180 | crop_volume,
181 | down_sample_method="voxel",
182 | voxel_size=voxel_size,
183 | trans=init_trans,
184 | )
185 | t = crop_and_downsample(
186 | gt_target,
187 | crop_volume,
188 | down_sample_method="voxel",
189 | voxel_size=voxel_size,
190 | )
191 | reg = o3d.registration.registration_icp(
192 | s,
193 | t,
194 | threshold,
195 | np.identity(4),
196 | o3d.registration.TransformationEstimationPointToPoint(True),
197 | o3d.registration.ICPConvergenceCriteria(1e-6, max_itr),
198 | )
199 | reg.transformation = np.matmul(reg.transformation, init_trans)
200 | return reg
201 |
--------------------------------------------------------------------------------
/scripts/eval_tnt/requirements.txt:
--------------------------------------------------------------------------------
1 | matplotlib>=1.3
2 | open3d==0.10
3 |
--------------------------------------------------------------------------------
/scripts/eval_tnt/run.py:
--------------------------------------------------------------------------------
1 | # ----------------------------------------------------------------------------
2 | # - TanksAndTemples Website Toolbox -
3 | # - http://www.tanksandtemples.org -
4 | # ----------------------------------------------------------------------------
5 | # The MIT License (MIT)
6 | #
7 | # Copyright (c) 2017
8 | # Arno Knapitsch
9 | # Jaesik Park
10 | # Qian-Yi Zhou
11 | # Vladlen Koltun
12 | #
13 | # Permission is hereby granted, free of charge, to any person obtaining a copy
14 | # of this software and associated documentation files (the "Software"), to deal
15 | # in the Software without restriction, including without limitation the rights
16 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 | # copies of the Software, and to permit persons to whom the Software is
18 | # furnished to do so, subject to the following conditions:
19 | #
20 | # The above copyright notice and this permission notice shall be included in
21 | # all copies or substantial portions of the Software.
22 | #
23 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29 | # THE SOFTWARE.
30 | # ----------------------------------------------------------------------------
31 | #
32 | # This python script is for downloading dataset from www.tanksandtemples.org
33 | # The dataset has a different license, please refer to
34 | # https://tanksandtemples.org/license/
35 |
36 | # this script requires Open3D python binding
37 | # please follow the intructions in setup.py before running this script.
38 | import numpy as np
39 | import open3d as o3d
40 | import os
41 | import argparse
42 | # import torch
43 |
44 | from config import scenes_tau_dict
45 | from registration import (
46 | trajectory_alignment,
47 | registration_vol_ds,
48 | registration_unif,
49 | read_trajectory,
50 | )
51 | # from help_func import auto_orient_and_center_poses
52 | from trajectory_io import CameraPose
53 | from evaluation import EvaluateHisto
54 | from util import make_dir
55 | from plot import plot_graph
56 |
57 |
58 | def run_evaluation(dataset_dir, traj_path, ply_path, out_dir, view_crop):
59 | scene = os.path.basename(os.path.normpath(dataset_dir))
60 |
61 | if scene not in scenes_tau_dict:
62 | print(dataset_dir, scene)
63 | raise Exception("invalid dataset-dir, not in scenes_tau_dict")
64 |
65 | print("")
66 | print("===========================")
67 | print("Evaluating %s" % scene)
68 | print("===========================")
69 |
70 | dTau = scenes_tau_dict[scene]
71 | # put the crop-file, the GT file, the COLMAP SfM log file and
72 | # the alignment of the according scene in a folder of
73 | # the same scene name in the dataset_dir
74 | colmap_ref_logfile = os.path.join(dataset_dir, scene + "_COLMAP_SfM.log")
75 |
76 | # this is for groundtruth pointcloud, we can use it
77 | alignment = os.path.join(dataset_dir, scene + "_trans.txt")
78 | gt_filen = os.path.join(dataset_dir, scene + ".ply")
79 | # this crop file is also w.r.t the groundtruth pointcloud, we can use it.
80 | # Otherwise we have to crop the estimated pointcloud by ourself
81 | cropfile = os.path.join(dataset_dir, scene + ".json")
82 | # this is not so necessary
83 | map_file = os.path.join(dataset_dir, scene + "_mapping_reference.txt")
84 | if not os.path.isfile(map_file):
85 | map_file = None
86 | map_file = None
87 |
88 | make_dir(out_dir)
89 |
90 | # Load reconstruction and according GT
91 | print(ply_path)
92 | pcd = o3d.io.read_point_cloud(ply_path)
93 | # add center points
94 | import trimesh
95 | mesh = trimesh.load_mesh(ply_path)
96 | # add center points
97 | sampled_vertices = mesh.vertices[mesh.faces].mean(axis=1)
98 | # add 4 points based on the face vertices
99 | # face_vertices = mesh.vertices[mesh.faces]# .mean(axis=1)
100 | # weights = np.array([[3, 3, 3],
101 | # [4, 4, 1],
102 | # [4, 1, 4],
103 | # [1, 4, 4]],dtype=np.float32) / 9.0
104 | # sampled_vertices = np.sum(face_vertices.reshape(-1, 1, 3, 3) * weights.reshape(1, 4, 3, 1), axis=2).reshape(-1, 3)
105 |
106 | vertices = np.concatenate([mesh.vertices, sampled_vertices], axis=0)
107 | pcd = o3d.geometry.PointCloud()
108 | pcd.points = o3d.utility.Vector3dVector(vertices)
109 | ### end add center points
110 |
111 | print(gt_filen)
112 | gt_pcd = o3d.io.read_point_cloud(gt_filen)
113 |
114 | gt_trans = np.loadtxt(alignment)
115 | print(traj_path)
116 | traj_to_register = []
117 | if traj_path.endswith('.npy'):
118 | ld = np.load(traj_path)
119 | for i in range(len(ld)):
120 | traj_to_register.append(CameraPose(meta=None, mat=ld[i]))
121 | elif traj_path.endswith('.json'): # instant-npg or sdfstudio format
122 | import json
123 | with open(traj_path, encoding='UTF-8') as f:
124 | meta = json.load(f)
125 | poses_dict = {}
126 | for i, frame in enumerate(meta['frames']):
127 | filepath = frame['file_path']
128 | new_i = int(filepath[13:18]) - 1
129 | poses_dict[new_i] = np.array(frame['transform_matrix'])
130 | poses = []
131 | for i in range(len(poses_dict)):
132 | poses.append(poses_dict[i])
133 | poses = torch.from_numpy(np.array(poses).astype(np.float32))
134 | poses, _ = auto_orient_and_center_poses(poses, method='up', center_poses=True)
135 | scale_factor = 1.0 / float(torch.max(torch.abs(poses[:, :3, 3])))
136 | poses[:, :3, 3] *= scale_factor
137 | poses = poses.numpy()
138 | for i in range(len(poses)):
139 | traj_to_register.append(CameraPose(meta=None, mat=poses[i]))
140 |
141 | else:
142 | traj_to_register = read_trajectory(traj_path)
143 | print(colmap_ref_logfile)
144 | gt_traj_col = read_trajectory(colmap_ref_logfile)
145 |
146 | trajectory_transform = trajectory_alignment(map_file, traj_to_register,
147 | gt_traj_col, gt_trans, scene)
148 |
149 |
150 | # big pointclouds will be downlsampled to this number to speed up alignment
151 | dist_threshold = dTau
152 | # Refine alignment by using the actual GT and MVS pointclouds
153 | vol = o3d.visualization.read_selection_polygon_volume(cropfile)
154 |
155 | # Registration refinment in 3 iterations
156 | r2 = registration_vol_ds(pcd, gt_pcd, trajectory_transform, vol, dTau,
157 | dTau * 80, 20)
158 | r3 = registration_vol_ds(pcd, gt_pcd, r2.transformation, vol, dTau / 2.0,
159 | dTau * 20, 20)
160 | r = registration_unif(pcd, gt_pcd, r3.transformation, vol, 2 * dTau, 20)
161 | trajectory_transform = r.transformation
162 |
163 | # Histogramms and P/R/F1
164 | plot_stretch = 5
165 | [
166 | precision,
167 | recall,
168 | fscore,
169 | edges_source,
170 | cum_source,
171 | edges_target,
172 | cum_target,
173 | ] = EvaluateHisto(
174 | pcd,
175 | gt_pcd,
176 | trajectory_transform, # r.transformation,
177 | vol,
178 | dTau / 2.0,
179 | dTau,
180 | out_dir,
181 | plot_stretch,
182 | scene,
183 | view_crop
184 | )
185 | eva = [precision, recall, fscore]
186 | print("==============================")
187 | print("evaluation result : %s" % scene)
188 | print("==============================")
189 | print("distance tau : %.3f" % dTau)
190 | print("precision : %.4f" % eva[0])
191 | print("recall : %.4f" % eva[1])
192 | print("f-score : %.4f" % eva[2])
193 | print("==============================")
194 |
195 | # Plotting
196 | plot_graph(
197 | scene,
198 | fscore,
199 | dist_threshold,
200 | edges_source,
201 | cum_source,
202 | edges_target,
203 | cum_target,
204 | plot_stretch,
205 | out_dir,
206 | )
207 |
208 |
209 | if __name__ == "__main__":
210 | parser = argparse.ArgumentParser()
211 | parser.add_argument(
212 | "--dataset-dir",
213 | type=str,
214 | required=True,
215 | help="path to a dataset/scene directory containing X.json, X.ply, ...",
216 | )
217 | parser.add_argument(
218 | "--traj-path",
219 | type=str,
220 | required=True,
221 | help=
222 | "path to trajectory file. See `convert_to_logfile.py` to create this file.",
223 | )
224 | parser.add_argument(
225 | "--ply-path",
226 | type=str,
227 | required=True,
228 | help="path to reconstruction ply file",
229 | )
230 | parser.add_argument(
231 | "--out-dir",
232 | type=str,
233 | default="",
234 | help=
235 | "output directory, default: an evaluation directory is created in the directory of the ply file",
236 | )
237 | parser.add_argument(
238 | "--view-crop",
239 | type=int,
240 | default=0,
241 | help="whether view the crop pointcloud after aligned",
242 | )
243 | args = parser.parse_args()
244 |
245 | args.view_crop = False # (args.view_crop > 0)
246 | if args.out_dir.strip() == "":
247 | args.out_dir = os.path.join(os.path.dirname(args.ply_path),
248 | "evaluation")
249 |
250 | run_evaluation(
251 | dataset_dir=args.dataset_dir,
252 | traj_path=args.traj_path,
253 | ply_path=args.ply_path,
254 | out_dir=args.out_dir,
255 | view_crop=args.view_crop
256 | )
257 |
--------------------------------------------------------------------------------
/scripts/eval_tnt/trajectory_io.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import open3d as o3d
3 |
4 |
5 | class CameraPose:
6 |
7 | def __init__(self, meta, mat):
8 | self.metadata = meta
9 | self.pose = mat
10 |
11 | def __str__(self):
12 | return ("Metadata : " + " ".join(map(str, self.metadata)) + "\n" +
13 | "Pose : " + "\n" + np.array_str(self.pose))
14 |
15 |
16 | def convert_trajectory_to_pointcloud(traj):
17 | pcd = o3d.geometry.PointCloud()
18 | for t in traj:
19 | pcd.points.append(t.pose[:3, 3])
20 | return pcd
21 |
22 |
23 | def read_trajectory(filename):
24 | traj = []
25 | with open(filename, "r") as f:
26 | metastr = f.readline()
27 | while metastr:
28 | metadata = map(int, metastr.split())
29 | mat = np.zeros(shape=(4, 4))
30 | for i in range(4):
31 | matstr = f.readline()
32 | mat[i, :] = np.fromstring(matstr, dtype=float, sep=" \t")
33 | traj.append(CameraPose(metadata, mat))
34 | metastr = f.readline()
35 | return traj
36 |
37 |
38 | def write_trajectory(traj, filename):
39 | with open(filename, "w") as f:
40 | for x in traj:
41 | p = x.pose.tolist()
42 | f.write(" ".join(map(str, x.metadata)) + "\n")
43 | f.write("\n".join(
44 | " ".join(map("{0:.12f}".format, p[i])) for i in range(4)))
45 | f.write("\n")
46 |
--------------------------------------------------------------------------------
/scripts/eval_tnt/util.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | def make_dir(path):
5 | if not os.path.exists(path):
6 | os.makedirs(path)
7 |
--------------------------------------------------------------------------------
/scripts/tnt_eval.py:
--------------------------------------------------------------------------------
1 | import os
2 | from argparse import ArgumentParser
3 |
4 | tnt_360_scenes = ['Truck']
5 | tnt_large_scenes = []
6 |
7 | parser = ArgumentParser(description="Full evaluation script parameters")
8 | parser.add_argument("--skip_training", action="store_true")
9 | parser.add_argument("--skip_rendering", action="store_true")
10 | parser.add_argument("--skip_metrics", action="store_true")
11 | parser.add_argument("--output_path", default="./eval/tnt")
12 | parser.add_argument('--TNT_data', "-TNT_data", required=True, type=str)
13 | args, _ = parser.parse_known_args()
14 |
15 | if not args.skip_metrics:
16 | parser.add_argument('--TNT_GT', required=True, type=str)
17 | args = parser.parse_args()
18 |
19 |
20 | if not args.skip_training:
21 | common_args = " --quiet --test_iterations -1 --depth_ratio 1.0 -r 2 --eval --max_shapes 2600000 --lambda_normals 0.005" # --lambda_dist 1000"
22 |
23 | for scene in tnt_360_scenes:
24 | source = args.TNT_data + "/" + scene
25 | print("python train_db_mesh.py -s " + source + " -m " + args.output_path + "/" + scene + common_args + ' --lambda_dist 100')
26 | os.system("python train_db_mesh.py -s " + source + " -m " + args.output_path + "/" + scene + common_args)
27 |
28 | for scene in tnt_large_scenes:
29 | source = args.TNT_data + "/" + scene
30 | print("python train_db_mesh.py -s " + source + " -m " + args.output_path + "/" + scene + common_args+ ' --lambda_dist 10')
31 | os.system("python train_db_mesh.py -s " + source + " -m " + args.output_path + "/" + scene + common_args)
32 |
33 |
34 | if not args.skip_rendering:
35 | all_sources = []
36 | common_args = " --quiet --depth_ratio 1.0 "
37 |
38 | for scene in tnt_360_scenes:
39 | source = args.TNT_data + "/" + scene
40 | print("python render.py --iteration 30000 -s " + source + " -m " + args.output_path + "/" + scene + common_args + ' --num_cluster 1 --voxel_size 0.004 --sdf_trunc 0.016 --depth_trunc 3.0')
41 | os.system("python render.py --iteration 30000 -s " + source + " -m " + args.output_path + "/" + scene + common_args + ' --num_cluster 1 --voxel_size 0.004 --sdf_trunc 0.016 --depth_trunc 3.0')
42 |
43 | for scene in tnt_large_scenes:
44 | source = args.TNT_data + "/" + scene
45 | print("python render.py --iteration 30000 -s " + source + " -m " + args.output_path + "/" + scene + common_args + ' --num_cluster 1 --voxel_size 0.006 --sdf_trunc 0.024 --depth_trunc 4.5')
46 | os.system("python render.py --iteration 30000 -s " + source + " -m " + args.output_path + "/" + scene + common_args + ' --num_cluster 1 --voxel_size 0.006 --sdf_trunc 0.024 --depth_trunc 4.5')
47 |
48 | if not args.skip_metrics:
49 | script_dir = os.path.dirname(os.path.abspath(__file__))
50 | all_scenes = tnt_360_scenes + tnt_large_scenes
51 |
52 | for scene in all_scenes:
53 | ply_file = f"{args.output_path}/{scene}/train/ours_{30000}/fuse_post.ply"
54 | string = f"OMP_NUM_THREADS=4 python {script_dir}/eval_tnt/run.py " + \
55 | f"--dataset-dir {args.TNT_GT}/{scene} " + \
56 | f"--traj-path {args.TNT_data}/{scene}/{scene}_COLMAP_SfM.log " + \
57 | f"--ply-path {ply_file}"
58 | print(string)
59 | os.system(string)
60 |
61 |
62 | # python scripts/tnt_eval.py --TNT_data /gpfs/scratch/acad/telim/datasets/TNT_GOF/TrainingSet --TNT_GT /gpfs/scratch/acad/telim/datasets/tandt_gt
--------------------------------------------------------------------------------
/triangle_renderer/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2023, Inria
4 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
5 | # All rights reserved.
6 | #
7 | # This software is free for non-commercial, research and evaluation use
8 | # under the terms of the LICENSE_GS.md file.
9 | #
10 | # For inquiries contact george.drettakis@inria.fr
11 | #
12 | # The modifications of the code are under the following copyright:
13 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
14 | # TELIM research group, http://www.telecom.ulg.ac.be/
15 | # IVUL research group, https://ivul.kaust.edu.sa/
16 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
17 | # All rights reserved.
18 | # The modifications are under the LICENSE.md file.
19 | #
20 | # For inquiries contact jan.held@uliege.be
21 | #
22 |
23 | import torch
24 | import math
25 | from diff_triangle_rasterization import TriangleRasterizationSettings, TriangleRasterizer
26 | from scene.triangle_model import TriangleModel
27 | from utils.sh_utils import eval_sh
28 | from utils.point_utils import depth_to_normal
29 |
30 |
31 | def render(viewpoint_camera, pc : TriangleModel, pipe, bg_color : torch.Tensor, scaling_modifier = 1.0, override_color = None):
32 | """
33 | Render the scene.
34 |
35 | Background tensor (bg_color) must be on GPU!
36 | """
37 |
38 | # Create zero tensor. We will use it to make pytorch return gradients of the 2D (screen-space) means
39 | screenspace_points = torch.zeros_like(pc.get_triangles_points[:,0,:].squeeze(), dtype=pc.get_triangles_points.dtype, requires_grad=True, device="cuda") + 0
40 | scaling = torch.zeros_like(pc.get_triangles_points[:,0,0].squeeze(), dtype=pc.get_triangles_points.dtype, requires_grad=True, device="cuda").detach()
41 | density_factor = torch.zeros_like(pc.get_triangles_points[:,0,0].squeeze(), dtype=pc.get_triangles_points.dtype, requires_grad=True, device="cuda").detach()
42 |
43 | try:
44 | screenspace_points.retain_grad()
45 | except:
46 | pass
47 |
48 | # Set up rasterization configuration
49 | tanfovx = math.tan(viewpoint_camera.FoVx * 0.5)
50 | tanfovy = math.tan(viewpoint_camera.FoVy * 0.5)
51 |
52 | raster_settings = TriangleRasterizationSettings(
53 | image_height=int(viewpoint_camera.image_height),
54 | image_width=int(viewpoint_camera.image_width),
55 | tanfovx=tanfovx,
56 | tanfovy=tanfovy,
57 | bg=bg_color,
58 | scale_modifier=scaling_modifier,
59 | viewmatrix=viewpoint_camera.world_view_transform,
60 | projmatrix=viewpoint_camera.full_proj_transform,
61 | sh_degree=pc.active_sh_degree,
62 | campos=viewpoint_camera.camera_center,
63 | prefiltered=False,
64 | debug=pipe.debug
65 | )
66 |
67 | rasterizer = TriangleRasterizer(raster_settings=raster_settings)
68 |
69 | opacity = pc.get_opacity
70 | triangles_points = pc.get_triangles_points_flatten
71 | sigma = pc.get_sigma
72 | num_points_per_triangle = pc.get_num_points_per_triangle
73 | cumsum_of_points_per_triangle = pc.get_cumsum_of_points_per_triangle
74 | number_of_points = pc.get_number_of_points
75 | means2D = screenspace_points
76 |
77 | # If precomputed colors are provided, use them. Otherwise, if it is desired to precompute colors
78 | # from SHs in Python, do it. If not, then SH -> RGB conversion will be done by rasterizer.
79 | shs = None
80 | colors_precomp = None
81 | if override_color is None:
82 | if pipe.convert_SHs_python:
83 | shs_view = pc.get_features.transpose(1, 2).view(-1, 3, (pc.max_sh_degree+1)**2)
84 | dir_pp = (pc.get_xyz - viewpoint_camera.camera_center.repeat(pc.get_features.shape[0], 1))
85 | dir_pp_normalized = dir_pp/dir_pp.norm(dim=1, keepdim=True)
86 | sh2rgb = eval_sh(pc.active_sh_degree, shs_view, dir_pp_normalized)
87 | colors_precomp = torch.clamp_min(sh2rgb + 0.5, 0.0)
88 | else:
89 | shs = pc.get_features
90 |
91 | else:
92 | colors_precomp = override_color
93 |
94 |
95 |
96 | mask = ((torch.sigmoid(pc._mask) > 0.01).float()- torch.sigmoid(pc._mask)).detach() + torch.sigmoid(pc._mask)
97 | opacity = opacity * mask
98 |
99 | # Rasterize visible triangles to image, obtain their radii (on screen).
100 | rendered_image, radii, scaling, density_factor, allmap, max_blending = rasterizer(
101 | triangles_points=triangles_points,
102 | sigma=sigma,
103 | num_points_per_triangle = num_points_per_triangle,
104 | cumsum_of_points_per_triangle = cumsum_of_points_per_triangle,
105 | number_of_points = number_of_points,
106 | shs = shs,
107 | colors_precomp = colors_precomp,
108 | opacities = opacity,
109 | means2D = means2D,
110 | scaling = scaling,
111 | density_factor = density_factor
112 | )
113 |
114 | rets = {"render": rendered_image,
115 | "viewspace_points": screenspace_points,
116 | "visibility_filter" : radii > 0,
117 | "radii": radii,
118 | "scaling": scaling,
119 | "density_factor": density_factor,
120 | "max_blending": max_blending
121 | }
122 |
123 | # additional regularizations
124 | render_alpha = allmap[1:2]
125 |
126 | # get normal map
127 | # transform normal from view space to world space
128 | render_normal = allmap[2:5]
129 | render_normal = (render_normal.permute(1,2,0) @ (viewpoint_camera.world_view_transform[:3,:3].T)).permute(2,0,1)
130 |
131 | # get median depth map
132 | render_depth_median = allmap[5:6]
133 | render_depth_median = torch.nan_to_num(render_depth_median, 0, 0)
134 |
135 | # get expected depth map
136 | render_depth_expected = allmap[0:1]
137 | render_depth_expected = (render_depth_expected / render_alpha)
138 | render_depth_expected = torch.nan_to_num(render_depth_expected, 0, 0)
139 |
140 | # get depth distortion map
141 | render_dist = allmap[6:7]
142 |
143 | # psedo surface attributes
144 | # surf depth is either median or expected by setting depth_ratio to 1 or 0
145 | # for bounded scene, use median depth, i.e., depth_ratio = 1;
146 | # for unbounded scene, use expected depth, i.e., depth_ration = 0, to reduce disk anliasing.
147 | #depth_ratio=0
148 | surf_depth = render_depth_expected * (1-pipe.depth_ratio) + (pipe.depth_ratio) * render_depth_median
149 |
150 | # assume the depth points form the 'surface' and generate psudo surface normal for regularizations.
151 | surf_normal = depth_to_normal(viewpoint_camera, surf_depth)
152 | surf_normal = surf_normal.permute(2,0,1)
153 | # remember to multiply with accum_alpha since render_normal is unnormalized.
154 | surf_normal = surf_normal * (render_alpha).detach()
155 |
156 | rets.update({
157 | 'rend_alpha': render_alpha,
158 | 'rend_normal': render_normal,
159 | 'rend_dist': render_dist,
160 | 'surf_depth': surf_depth,
161 | 'surf_normal': surf_normal,
162 | })
163 |
164 | return rets
165 |
166 |
167 |
168 |
169 |
170 |
--------------------------------------------------------------------------------
/utils/camera_utils.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2023, Inria
3 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
4 | # All rights reserved.
5 | #
6 | # This software is free for non-commercial, research and evaluation use
7 | # under the terms of the LICENSE.md file.
8 | #
9 | # For inquiries contact george.drettakis@inria.fr
10 | #
11 |
12 | from scene.cameras import Camera
13 | import numpy as np
14 | from utils.general_utils import PILtoTorch
15 | from utils.graphics_utils import fov2focal
16 |
17 | WARNED = False
18 |
19 | def loadCam(args, id, cam_info, resolution_scale):
20 | orig_w, orig_h = cam_info.image.size
21 |
22 | if args.resolution in [1, 2, 4, 8]:
23 | resolution = round(orig_w/(resolution_scale * args.resolution)), round(orig_h/(resolution_scale * args.resolution))
24 | else: # should be a type that converts to float
25 | if args.resolution == -1:
26 | if orig_w > 1600:
27 | global WARNED
28 | if not WARNED:
29 | print("[ INFO ] Encountered quite large input images (>1.6K pixels width), rescaling to 1.6K.\n "
30 | "If this is not desired, please explicitly specify '--resolution/-r' as 1")
31 | WARNED = True
32 | global_down = orig_w / 1600
33 | else:
34 | global_down = 1
35 | else:
36 | global_down = orig_w / args.resolution
37 |
38 | scale = float(global_down) * float(resolution_scale)
39 | resolution = (int(orig_w / scale), int(orig_h / scale))
40 |
41 | if len(cam_info.image.split()) > 3:
42 | import torch
43 | resized_image_rgb = torch.cat([PILtoTorch(im, resolution) for im in cam_info.image.split()[:3]], dim=0)
44 | loaded_mask = PILtoTorch(cam_info.image.split()[3], resolution)
45 | gt_image = resized_image_rgb
46 | else:
47 | resized_image_rgb = PILtoTorch(cam_info.image, resolution)
48 | loaded_mask = None
49 | gt_image = resized_image_rgb
50 |
51 | return Camera(colmap_id=cam_info.uid, R=cam_info.R, T=cam_info.T,
52 | FoVx=cam_info.FovX, FoVy=cam_info.FovY,
53 | image=gt_image, gt_alpha_mask=loaded_mask,
54 | image_name=cam_info.image_name, uid=id, data_device=args.data_device)
55 |
56 | def cameraList_from_camInfos(cam_infos, resolution_scale, args):
57 | camera_list = []
58 |
59 | for id, c in enumerate(cam_infos):
60 | camera_list.append(loadCam(args, id, c, resolution_scale))
61 |
62 | return camera_list
63 |
64 | def camera_to_JSON(id, camera : Camera):
65 | Rt = np.zeros((4, 4))
66 | Rt[:3, :3] = camera.R.transpose()
67 | Rt[:3, 3] = camera.T
68 | Rt[3, 3] = 1.0
69 |
70 | W2C = np.linalg.inv(Rt)
71 | pos = W2C[:3, 3]
72 | rot = W2C[:3, :3]
73 | serializable_array_2d = [x.tolist() for x in rot]
74 | camera_entry = {
75 | 'id' : id,
76 | 'img_name' : camera.image_name,
77 | 'width' : camera.width,
78 | 'height' : camera.height,
79 | 'position': pos.tolist(),
80 | 'rotation': serializable_array_2d,
81 | 'fy' : fov2focal(camera.FovY, camera.height),
82 | 'fx' : fov2focal(camera.FovX, camera.width)
83 | }
84 | return camera_entry
--------------------------------------------------------------------------------
/utils/general_utils.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2023, Inria
4 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
5 | # All rights reserved.
6 | #
7 | # This software is free for non-commercial, research and evaluation use
8 | # under the terms of the LICENSE_GS.md file.
9 | #
10 | # For inquiries contact george.drettakis@inria.fr
11 | #
12 | # The modifications of the code are under the following copyright:
13 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
14 | # TELIM research group, http://www.telecom.ulg.ac.be/
15 | # IVUL research group, https://ivul.kaust.edu.sa/
16 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
17 | # All rights reserved.
18 | # The modifications are under the LICENSE.md file.
19 | #
20 | # For inquiries contact jan.held@uliege.be
21 | #
22 |
23 | import torch
24 | import sys
25 | from datetime import datetime
26 | import numpy as np
27 | import random
28 |
29 |
30 | def scaled_sigmoid(x):
31 | return 5 * torch.sigmoid(x)
32 |
33 | def inverse_sigmoid(x):
34 | return torch.log(x/(1-x))
35 |
36 | def inverse_sigmoid_10(x):
37 | return -torch.log((10 / x) - 1)
38 |
39 | def PILtoTorch(pil_image, resolution):
40 | resized_image_PIL = pil_image.resize(resolution)
41 | resized_image = torch.from_numpy(np.array(resized_image_PIL)) / 255.0
42 | if len(resized_image.shape) == 3:
43 | return resized_image.permute(2, 0, 1)
44 | else:
45 | return resized_image.unsqueeze(dim=-1).permute(2, 0, 1)
46 |
47 | def get_expon_lr_func(
48 | lr_init, lr_final, lr_delay_steps=0, lr_delay_mult=1.0, max_steps=1000000
49 | ):
50 | """
51 | Copied from Plenoxels
52 |
53 | Continuous learning rate decay function. Adapted from JaxNeRF
54 | The returned rate is lr_init when step=0 and lr_final when step=max_steps, and
55 | is log-linearly interpolated elsewhere (equivalent to exponential decay).
56 | If lr_delay_steps>0 then the learning rate will be scaled by some smooth
57 | function of lr_delay_mult, such that the initial learning rate is
58 | lr_init*lr_delay_mult at the beginning of optimization but will be eased back
59 | to the normal learning rate when steps>lr_delay_steps.
60 | :param conf: config subtree 'lr' or similar
61 | :param max_steps: int, the number of steps during optimization.
62 | :return HoF which takes step as input
63 | """
64 |
65 | def helper(step):
66 | if step < 0 or (lr_init == 0.0 and lr_final == 0.0):
67 | # Disable this parameter
68 | return 0.0
69 | if lr_delay_steps > 0:
70 | # A kind of reverse cosine decay.
71 | delay_rate = lr_delay_mult + (1 - lr_delay_mult) * np.sin(
72 | 0.5 * np.pi * np.clip(step / lr_delay_steps, 0, 1)
73 | )
74 | else:
75 | delay_rate = 1.0
76 | t = np.clip(step / max_steps, 0, 1)
77 | log_lerp = np.exp(np.log(lr_init) * (1 - t) + np.log(lr_final) * t)
78 | return delay_rate * log_lerp
79 |
80 | return helper
81 |
82 | def strip_lowerdiag(L):
83 | uncertainty = torch.zeros((L.shape[0], 6), dtype=torch.float, device="cuda")
84 |
85 | uncertainty[:, 0] = L[:, 0, 0]
86 | uncertainty[:, 1] = L[:, 0, 1]
87 | uncertainty[:, 2] = L[:, 0, 2]
88 | uncertainty[:, 3] = L[:, 1, 1]
89 | uncertainty[:, 4] = L[:, 1, 2]
90 | uncertainty[:, 5] = L[:, 2, 2]
91 | return uncertainty
92 |
93 | def strip_symmetric(sym):
94 | return strip_lowerdiag(sym)
95 |
96 | def build_rotation(r):
97 | norm = torch.sqrt(r[:,0]*r[:,0] + r[:,1]*r[:,1] + r[:,2]*r[:,2] + r[:,3]*r[:,3])
98 |
99 | q = r / norm[:, None]
100 |
101 | R = torch.zeros((q.size(0), 3, 3), device='cuda')
102 |
103 | r = q[:, 0]
104 | x = q[:, 1]
105 | y = q[:, 2]
106 | z = q[:, 3]
107 |
108 | R[:, 0, 0] = 1 - 2 * (y*y + z*z)
109 | R[:, 0, 1] = 2 * (x*y - r*z)
110 | R[:, 0, 2] = 2 * (x*z + r*y)
111 | R[:, 1, 0] = 2 * (x*y + r*z)
112 | R[:, 1, 1] = 1 - 2 * (x*x + z*z)
113 | R[:, 1, 2] = 2 * (y*z - r*x)
114 | R[:, 2, 0] = 2 * (x*z - r*y)
115 | R[:, 2, 1] = 2 * (y*z + r*x)
116 | R[:, 2, 2] = 1 - 2 * (x*x + y*y)
117 | return R
118 |
119 | def build_scaling_rotation(s, r):
120 | L = torch.zeros((s.shape[0], 3, 3), dtype=torch.float, device="cuda")
121 | R = build_rotation(r)
122 |
123 | L[:,0,0] = s[:,0]
124 | L[:,1,1] = s[:,1]
125 | L[:,2,2] = s[:,2]
126 |
127 | L = R @ L
128 | return L
129 |
130 | def safe_state(silent):
131 | old_f = sys.stdout
132 | class F:
133 | def __init__(self, silent):
134 | self.silent = silent
135 |
136 | def write(self, x):
137 | if not self.silent:
138 | if x.endswith("\n"):
139 | old_f.write(x.replace("\n", " [{}]\n".format(str(datetime.now().strftime("%d/%m %H:%M:%S")))))
140 | else:
141 | old_f.write(x)
142 |
143 | def flush(self):
144 | old_f.flush()
145 |
146 | sys.stdout = F(silent)
147 |
148 | random.seed(0)
149 | np.random.seed(0)
150 | torch.manual_seed(0)
151 | torch.cuda.set_device(torch.device("cuda:0"))
152 |
--------------------------------------------------------------------------------
/utils/graphics_utils.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2023, Inria
4 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
5 | # All rights reserved.
6 | #
7 | # This software is free for non-commercial, research and evaluation use
8 | # under the terms of the LICENSE_GS.md file.
9 | #
10 | # For inquiries contact george.drettakis@inria.fr
11 | #
12 | # The modifications of the code are under the following copyright:
13 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
14 | # TELIM research group, http://www.telecom.ulg.ac.be/
15 | # IVUL research group, https://ivul.kaust.edu.sa/
16 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
17 | # All rights reserved.
18 | # The modifications are under the LICENSE.md file.
19 | #
20 | # For inquiries contact jan.held@uliege.be
21 | #
22 |
23 | import torch
24 | import math
25 | import numpy as np
26 | from typing import NamedTuple
27 |
28 | class BasicPointCloud(NamedTuple):
29 | points : np.array
30 | colors : np.array
31 | normals : np.array
32 |
33 | def geom_transform_points(points, transf_matrix):
34 | P, _ = points.shape
35 | ones = torch.ones(P, 1, dtype=points.dtype, device=points.device)
36 | points_hom = torch.cat([points, ones], dim=1)
37 | points_out = torch.matmul(points_hom, transf_matrix.unsqueeze(0))
38 |
39 | denom = points_out[..., 3:] + 0.0000001
40 | return (points_out[..., :3] / denom).squeeze(dim=0)
41 |
42 | def getWorld2View(R, t):
43 | Rt = np.zeros((4, 4))
44 | Rt[:3, :3] = R.transpose()
45 | Rt[:3, 3] = t
46 | Rt[3, 3] = 1.0
47 | return np.float32(Rt)
48 |
49 | def getWorld2View2(R, t, translate=np.array([.0, .0, .0]), scale=1.0):
50 | Rt = np.zeros((4, 4))
51 | Rt[:3, :3] = R.transpose()
52 | Rt[:3, 3] = t
53 | Rt[3, 3] = 1.0
54 |
55 | C2W = np.linalg.inv(Rt)
56 | cam_center = C2W[:3, 3]
57 | cam_center = (cam_center + translate) * scale
58 | C2W[:3, 3] = cam_center
59 | Rt = np.linalg.inv(C2W)
60 | return np.float32(Rt)
61 |
62 | def getProjectionMatrix(znear, zfar, fovX, fovY):
63 | tanHalfFovY = math.tan((fovY / 2))
64 | tanHalfFovX = math.tan((fovX / 2))
65 |
66 | top = tanHalfFovY * znear
67 | bottom = -top
68 | right = tanHalfFovX * znear
69 | left = -right
70 |
71 | P = torch.zeros(4, 4)
72 |
73 | z_sign = 1.0
74 |
75 | P[0, 0] = 2.0 * znear / (right - left)
76 | P[1, 1] = 2.0 * znear / (top - bottom)
77 | P[0, 2] = (right + left) / (right - left)
78 | P[1, 2] = (top + bottom) / (top - bottom)
79 | P[3, 2] = z_sign
80 | P[2, 2] = z_sign * zfar / (zfar - znear)
81 | P[2, 3] = -(zfar * znear) / (zfar - znear)
82 | return P
83 |
84 | def fov2focal(fov, pixels):
85 | return pixels / (2 * math.tan(fov / 2))
86 |
87 | def focal2fov(focal, pixels):
88 | return 2*math.atan(pixels/(2*focal))
--------------------------------------------------------------------------------
/utils/image_utils.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2023, Inria
4 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
5 | # All rights reserved.
6 | #
7 | # This software is free for non-commercial, research and evaluation use
8 | # under the terms of the LICENSE_GS.md file.
9 | #
10 | # For inquiries contact george.drettakis@inria.fr
11 | #
12 | # The modifications of the code are under the following copyright:
13 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
14 | # TELIM research group, http://www.telecom.ulg.ac.be/
15 | # IVUL research group, https://ivul.kaust.edu.sa/
16 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
17 | # All rights reserved.
18 | # The modifications are under the LICENSE.md file.
19 | #
20 | # For inquiries contact jan.held@uliege.be
21 | #
22 |
23 | import torch
24 | import torch.nn as nn
25 | import torch.nn.functional as F
26 |
27 | def mse(img1, img2):
28 | return (((img1 - img2)) ** 2).view(img1.shape[0], -1).mean(1, keepdim=True)
29 |
30 | def psnr(img1, img2):
31 | mse = (((img1 - img2)) ** 2).view(img1.shape[0], -1).mean(1, keepdim=True)
32 | return 20 * torch.log10(1.0 / torch.sqrt(mse))
33 |
34 | class DoGFilter(nn.Module):
35 | def __init__(self, channels, sigma1):
36 | super(DoGFilter, self).__init__()
37 | self.channels = channels
38 | self.sigma1 = sigma1
39 | self.sigma2 = 2 * sigma1 # Ensure the 1:2 ratio
40 | self.kernel_size1 = int(2 * round(3 * self.sigma1) + 1)
41 | self.kernel_size2 = int(2 * round(3 * self.sigma2) + 1)
42 | self.padding1 = (self.kernel_size1 - 1) // 2
43 | self.padding2 = (self.kernel_size2 - 1) // 2
44 | self.weight1 = self.get_gaussian_kernel(self.kernel_size1, self.sigma1)
45 | self.weight2 = self.get_gaussian_kernel(self.kernel_size2, self.sigma2)
46 |
47 |
48 | def get_gaussian_kernel(self, kernel_size, sigma):
49 | x_cord = torch.arange(kernel_size)
50 | x_grid = x_cord.repeat(kernel_size).view(kernel_size, kernel_size)
51 | y_grid = x_grid.t()
52 | xy_grid = torch.stack([x_grid, y_grid], dim=-1).float()
53 |
54 | mean = (kernel_size - 1) / 2.
55 | variance = sigma**2.
56 |
57 | kernel = torch.exp(-(xy_grid - mean).pow(2).sum(dim=-1) / (2 * variance))
58 | kernel = kernel / kernel.sum() # Normalize the kernel
59 | kernel = kernel.repeat(self.channels, 1, 1, 1)
60 |
61 | return kernel
62 |
63 | @torch.no_grad()
64 | def forward(self, x):
65 | gaussian1 = F.conv2d(x, self.weight1.to(x.device), bias=None, stride=1, padding=self.padding1, groups=self.channels)
66 | gaussian2 = F.conv2d(x, self.weight2.to(x.device), bias=None, stride=1, padding=self.padding2, groups=self.channels)
67 | return gaussian1 - gaussian2
68 |
69 | def apply_dog_filter(batch, freq=50, scale_factor=0.5):
70 | """
71 | Apply a Difference of Gaussian filter to a batch of images.
72 |
73 | Args:
74 | batch: torch.Tensor, shape (B, C, H, W)
75 | freq: Control variable ranging from 0 to 100.
76 | - 0 means original image
77 | - 1.0 means smoother difference
78 | - 100 means sharpest difference
79 | scale_factor: Factor by which the image is downscaled before applying DoG.
80 |
81 | Returns:
82 | torch.Tensor: Processed image using DoG.
83 | """
84 | # Convert to grayscale if it's a color image
85 | if batch.size(1) == 3:
86 | batch = torch.mean(batch, dim=1, keepdim=True)
87 |
88 | # Downscale the image
89 | downscaled = F.interpolate(batch, scale_factor=scale_factor, mode='bilinear', align_corners=False)
90 |
91 | channels = downscaled.size(1)
92 |
93 | # Set sigma1 value based on freq parameter. sigma2 will be 2*sigma1.
94 | sigma1 = 0.1 + (100 - freq) * 0.1 if freq >=50 else 0.1 + freq * 0.1
95 |
96 | dog_filter = DoGFilter(channels, sigma1)
97 | mask = dog_filter(downscaled)
98 |
99 | # Upscale the mask back to original size
100 | upscaled_mask = F.interpolate(mask, size=batch.shape[-2:], mode='bilinear', align_corners=False)
101 |
102 | upscaled_mask = upscaled_mask - upscaled_mask.min()
103 | upscaled_mask = upscaled_mask / upscaled_mask.max() if freq >=50 else 1.0 - upscaled_mask / upscaled_mask.max()
104 |
105 | upscaled_mask = (upscaled_mask >=0.5).to(torch.float)
106 | return upscaled_mask[:,0,...]
--------------------------------------------------------------------------------
/utils/loss_utils.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2023, Inria
4 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
5 | # All rights reserved.
6 | #
7 | # This software is free for non-commercial, research and evaluation use
8 | # under the terms of the LICENSE_GS.md file.
9 | #
10 | # For inquiries contact george.drettakis@inria.fr
11 | #
12 | # The modifications of the code are under the following copyright:
13 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
14 | # TELIM research group, http://www.telecom.ulg.ac.be/
15 | # IVUL research group, https://ivul.kaust.edu.sa/
16 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
17 | # All rights reserved.
18 | # The modifications are under the LICENSE.md file.
19 | #
20 | # For inquiries contact jan.held@uliege.be
21 | #
22 |
23 | import torch
24 | import torch.nn.functional as F
25 | from torch.autograd import Variable
26 | from math import exp
27 |
28 |
29 |
30 | def equilateral_regularizer(triangles):
31 |
32 | nan_mask = torch.isnan(triangles).any(dim=(1, 2))
33 | if nan_mask.any():
34 | print("NaN detected in triangle(s):")
35 |
36 | v0 = triangles[:, 1, :] - triangles[:, 0, :]
37 | v1 = triangles[:, 2, :] - triangles[:, 0, :]
38 | cross = torch.cross(v0, v1, dim=1)
39 | area = 0.5 * torch.norm(cross, dim=1)
40 |
41 | return area
42 |
43 |
44 | def l1_loss(network_output, gt):
45 | return torch.abs((network_output - gt)).mean()
46 |
47 | def l2_loss(network_output, gt):
48 | return ((network_output - gt) ** 2).mean()
49 |
50 | def lp_loss(pred, target, p=0.7, eps=1e-6):
51 | """
52 | Computes Lp loss with 0 < p < 1.
53 | Args:
54 | pred: (N, C, H, W) predicted image
55 | target: (N, C, H, W) groundtruth image
56 | p: norm degree < 1
57 | eps: small constant for numerical stability
58 | """
59 | diff = torch.abs(pred - target) + eps
60 | loss = torch.pow(diff, p).mean()
61 | return loss
62 |
63 | def gaussian(window_size, sigma):
64 | gauss = torch.Tensor([exp(-(x - window_size // 2) ** 2 / float(2 * sigma ** 2)) for x in range(window_size)])
65 | return gauss / gauss.sum()
66 |
67 | def create_window(window_size, channel):
68 | _1D_window = gaussian(window_size, 1.5).unsqueeze(1)
69 | _2D_window = _1D_window.mm(_1D_window.t()).float().unsqueeze(0).unsqueeze(0)
70 | window = Variable(_2D_window.expand(channel, 1, window_size, window_size).contiguous())
71 | return window
72 |
73 | def ssim(img1, img2, window_size=11, size_average=True):
74 | channel = img1.size(-3)
75 | window = create_window(window_size, channel)
76 | if img1.is_cuda:
77 | window = window.cuda(img1.get_device())
78 | window = window.type_as(img1)
79 |
80 | return _ssim(img1, img2, window, window_size, channel, size_average)
81 |
82 | def _ssim(img1, img2, window, window_size, channel, size_average=True):
83 | mu1 = F.conv2d(img1, window, padding=window_size // 2, groups=channel)
84 | mu2 = F.conv2d(img2, window, padding=window_size // 2, groups=channel)
85 |
86 | mu1_sq = mu1.pow(2)
87 | mu2_sq = mu2.pow(2)
88 | mu1_mu2 = mu1 * mu2
89 |
90 | sigma1_sq = F.conv2d(img1 * img1, window, padding=window_size // 2, groups=channel) - mu1_sq
91 | sigma2_sq = F.conv2d(img2 * img2, window, padding=window_size // 2, groups=channel) - mu2_sq
92 | sigma12 = F.conv2d(img1 * img2, window, padding=window_size // 2, groups=channel) - mu1_mu2
93 |
94 | C1 = 0.01 ** 2
95 | C2 = 0.03 ** 2
96 |
97 | ssim_map = ((2 * mu1_mu2 + C1) * (2 * sigma12 + C2)) / ((mu1_sq + mu2_sq + C1) * (sigma1_sq + sigma2_sq + C2))
98 |
99 | if size_average:
100 | return ssim_map.mean()
101 | else:
102 | return ssim_map.mean(1).mean(1).mean(1)
103 |
104 |
--------------------------------------------------------------------------------
/utils/point_utils.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import torch.nn as nn
3 | import torch.nn.functional as F
4 | import numpy as np
5 | import os, cv2
6 | import matplotlib.pyplot as plt
7 | import math
8 |
9 | def depths_to_points(view, depthmap):
10 | c2w = (view.world_view_transform.T).inverse()
11 | W, H = view.image_width, view.image_height
12 | ndc2pix = torch.tensor([
13 | [W / 2, 0, 0, (W) / 2],
14 | [0, H / 2, 0, (H) / 2],
15 | [0, 0, 0, 1]]).float().cuda().T
16 | projection_matrix = c2w.T @ view.full_proj_transform
17 | intrins = (projection_matrix @ ndc2pix)[:3,:3].T
18 |
19 | grid_x, grid_y = torch.meshgrid(torch.arange(W, device='cuda').float(), torch.arange(H, device='cuda').float(), indexing='xy')
20 | points = torch.stack([grid_x, grid_y, torch.ones_like(grid_x)], dim=-1).reshape(-1, 3)
21 | rays_d = points @ intrins.inverse().T @ c2w[:3,:3].T
22 | rays_o = c2w[:3,3]
23 | points = depthmap.reshape(-1, 1) * rays_d + rays_o
24 | return points
25 |
26 | def depth_to_normal(view, depth):
27 | """
28 | view: view camera
29 | depth: depthmap
30 | """
31 | points = depths_to_points(view, depth).reshape(*depth.shape[1:], 3)
32 | output = torch.zeros_like(points)
33 | dx = torch.cat([points[2:, 1:-1] - points[:-2, 1:-1]], dim=0)
34 | dy = torch.cat([points[1:-1, 2:] - points[1:-1, :-2]], dim=1)
35 | normal_map = torch.nn.functional.normalize(torch.cross(dx, dy, dim=-1), dim=-1)
36 | output[1:-1, 1:-1, :] = normal_map
37 | return output
--------------------------------------------------------------------------------
/utils/render_utils.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2022, Google LLC
4 | # Licensed under the Apache License, Version 2.0
5 | #
6 | # The modifications of the code are under the following copyright:
7 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
8 | # TELIM research group, http://www.telecom.ulg.ac.be/
9 | # IVUL research group, https://ivul.kaust.edu.sa/
10 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
11 | # All rights reserved.
12 | # The modifications are under the LICENSE.md file.
13 | #
14 | # For inquiries contact jan.held@uliege.be
15 | #
16 |
17 | import numpy as np
18 | import os
19 | from typing import List, Mapping, Optional, Text, Tuple, Union
20 | import copy
21 | from PIL import Image
22 | import mediapy as media
23 | from matplotlib import cm
24 | from tqdm import tqdm
25 | from utils.graphics_utils import getProjectionMatrix
26 |
27 | import torch
28 |
29 | def normalize(x: np.ndarray) -> np.ndarray:
30 | """Normalization helper function."""
31 | return x / np.linalg.norm(x)
32 |
33 | def pad_poses(p: np.ndarray) -> np.ndarray:
34 | """Pad [..., 3, 4] pose matrices with a homogeneous bottom row [0,0,0,1]."""
35 | bottom = np.broadcast_to([0, 0, 0, 1.], p[..., :1, :4].shape)
36 | return np.concatenate([p[..., :3, :4], bottom], axis=-2)
37 |
38 |
39 | def unpad_poses(p: np.ndarray) -> np.ndarray:
40 | """Remove the homogeneous bottom row from [..., 4, 4] pose matrices."""
41 | return p[..., :3, :4]
42 |
43 |
44 | def recenter_poses(poses: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
45 | """Recenter poses around the origin."""
46 | cam2world = average_pose(poses)
47 | transform = np.linalg.inv(pad_poses(cam2world))
48 | poses = transform @ pad_poses(poses)
49 | return unpad_poses(poses), transform
50 |
51 |
52 | def average_pose(poses: np.ndarray) -> np.ndarray:
53 | """New pose using average position, z-axis, and up vector of input poses."""
54 | position = poses[:, :3, 3].mean(0)
55 | z_axis = poses[:, :3, 2].mean(0)
56 | up = poses[:, :3, 1].mean(0)
57 | cam2world = viewmatrix(z_axis, up, position)
58 | return cam2world
59 |
60 | def viewmatrix(lookdir: np.ndarray, up: np.ndarray,
61 | position: np.ndarray) -> np.ndarray:
62 | """Construct lookat view matrix."""
63 | vec2 = normalize(lookdir)
64 | vec0 = normalize(np.cross(up, vec2))
65 | vec1 = normalize(np.cross(vec2, vec0))
66 | m = np.stack([vec0, vec1, vec2, position], axis=1)
67 | return m
68 |
69 | def focus_point_fn(poses: np.ndarray) -> np.ndarray:
70 | """Calculate nearest point to all focal axes in poses."""
71 | directions, origins = poses[:, :3, 2:3], poses[:, :3, 3:4]
72 | m = np.eye(3) - directions * np.transpose(directions, [0, 2, 1])
73 | mt_m = np.transpose(m, [0, 2, 1]) @ m
74 | focus_pt = np.linalg.inv(mt_m.mean(0)) @ (mt_m @ origins).mean(0)[:, 0]
75 | return focus_pt
76 |
77 | def transform_poses_pca(poses: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
78 | """Transforms poses so principal components lie on XYZ axes.
79 |
80 | Args:
81 | poses: a (N, 3, 4) array containing the cameras' camera to world transforms.
82 |
83 | Returns:
84 | A tuple (poses, transform), with the transformed poses and the applied
85 | camera_to_world transforms.
86 | """
87 | t = poses[:, :3, 3]
88 | t_mean = t.mean(axis=0)
89 | t = t - t_mean
90 |
91 | eigval, eigvec = np.linalg.eig(t.T @ t)
92 | # Sort eigenvectors in order of largest to smallest eigenvalue.
93 | inds = np.argsort(eigval)[::-1]
94 | eigvec = eigvec[:, inds]
95 | rot = eigvec.T
96 | if np.linalg.det(rot) < 0:
97 | rot = np.diag(np.array([1, 1, -1])) @ rot
98 |
99 | transform = np.concatenate([rot, rot @ -t_mean[:, None]], -1)
100 | poses_recentered = unpad_poses(transform @ pad_poses(poses))
101 | transform = np.concatenate([transform, np.eye(4)[3:]], axis=0)
102 |
103 | # Flip coordinate system if z component of y-axis is negative
104 | if poses_recentered.mean(axis=0)[2, 1] < 0:
105 | poses_recentered = np.diag(np.array([1, -1, -1])) @ poses_recentered
106 | transform = np.diag(np.array([1, -1, -1, 1])) @ transform
107 |
108 | return poses_recentered, transform
109 | # points = np.random.rand(3,100)
110 | # points_h = np.concatenate((points,np.ones_like(points[:1])), axis=0)
111 | # (poses_recentered @ points_h)[0]
112 | # (transform @ pad_poses(poses) @ points_h)[0,:3]
113 | # import pdb; pdb.set_trace()
114 |
115 | # # Just make sure it's it in the [-1, 1]^3 cube
116 | # scale_factor = 1. / np.max(np.abs(poses_recentered[:, :3, 3]))
117 | # poses_recentered[:, :3, 3] *= scale_factor
118 | # transform = np.diag(np.array([scale_factor] * 3 + [1])) @ transform
119 |
120 | # return poses_recentered, transform
121 |
122 | def generate_ellipse_path(poses: np.ndarray,
123 | n_frames: int = 120,
124 | const_speed: bool = True,
125 | z_variation: float = 0.,
126 | z_phase: float = 0.) -> np.ndarray:
127 | """Generate an elliptical render path based on the given poses."""
128 | # Calculate the focal point for the path (cameras point toward this).
129 | center = focus_point_fn(poses)
130 | # Path height sits at z=0 (in middle of zero-mean capture pattern).
131 | offset = np.array([center[0], center[1], 0])
132 |
133 | # Calculate scaling for ellipse axes based on input camera positions.
134 | sc = np.percentile(np.abs(poses[:, :3, 3] - offset), 90, axis=0)
135 | # Use ellipse that is symmetric about the focal point in xy.
136 | low = -sc + offset
137 | high = sc + offset
138 | # Optional height variation need not be symmetric
139 | z_low = np.percentile((poses[:, :3, 3]), 10, axis=0)
140 | z_high = np.percentile((poses[:, :3, 3]), 90, axis=0)
141 |
142 | def get_positions(theta):
143 | # Interpolate between bounds with trig functions to get ellipse in x-y.
144 | # Optionally also interpolate in z to change camera height along path.
145 | return np.stack([
146 | low[0] + (high - low)[0] * (np.cos(theta) * .5 + .5),
147 | low[1] + (high - low)[1] * (np.sin(theta) * .5 + .5),
148 | z_variation * (z_low[2] + (z_high - z_low)[2] *
149 | (np.cos(theta + 2 * np.pi * z_phase) * .5 + .5)),
150 | ], -1)
151 |
152 | theta = np.linspace(0, 2. * np.pi, n_frames + 1, endpoint=True)
153 | positions = get_positions(theta)
154 |
155 | #if const_speed:
156 |
157 | # # Resample theta angles so that the velocity is closer to constant.
158 | # lengths = np.linalg.norm(positions[1:] - positions[:-1], axis=-1)
159 | # theta = stepfun.sample(None, theta, np.log(lengths), n_frames + 1)
160 | # positions = get_positions(theta)
161 |
162 | # Throw away duplicated last position.
163 | positions = positions[:-1]
164 |
165 | # Set path's up vector to axis closest to average of input pose up vectors.
166 | avg_up = poses[:, :3, 1].mean(0)
167 | avg_up = avg_up / np.linalg.norm(avg_up)
168 | ind_up = np.argmax(np.abs(avg_up))
169 | up = np.eye(3)[ind_up] * np.sign(avg_up[ind_up])
170 |
171 | return np.stack([viewmatrix(p - center, up, p) for p in positions])
172 |
173 |
174 | def generate_path(viewpoint_cameras, n_frames=480):
175 | c2ws = np.array([np.linalg.inv(np.asarray((cam.world_view_transform.T).cpu().numpy())) for cam in viewpoint_cameras])
176 | pose = c2ws[:,:3,:] @ np.diag([1, -1, -1, 1])
177 | pose_recenter, colmap_to_world_transform = transform_poses_pca(pose)
178 |
179 | # generate new poses
180 | new_poses = generate_ellipse_path(poses=pose_recenter, n_frames=n_frames)
181 | # warp back to orignal scale
182 | new_poses = np.linalg.inv(colmap_to_world_transform) @ pad_poses(new_poses)
183 |
184 | traj = []
185 | for c2w in new_poses:
186 | c2w = c2w @ np.diag([1, -1, -1, 1])
187 | cam = copy.deepcopy(viewpoint_cameras[0])
188 | cam.image_height = int(cam.image_height / 2) * 2
189 | cam.image_width = int(cam.image_width / 2) * 2
190 | cam.world_view_transform = torch.from_numpy(np.linalg.inv(c2w).T).float().cuda()
191 | cam.full_proj_transform = (cam.world_view_transform.unsqueeze(0).bmm(cam.projection_matrix.unsqueeze(0))).squeeze(0)
192 | cam.camera_center = cam.world_view_transform.inverse()[3, :3]
193 | traj.append(cam)
194 |
195 | return traj
196 |
197 | def generate_zoom_trajectory(viewpoint_cameras, n_frames=480, zoom_start=0, zoom_duration=120, zoom_intensity=2.0):
198 | traj = generate_path(viewpoint_cameras, n_frames=n_frames)
199 |
200 | cam0 = viewpoint_cameras[0]
201 | orig_fovx = cam0.FoVx
202 | orig_fovy = cam0.FoVy
203 | orig_focalx = cam0.image_width / (2 * np.tan(orig_fovx / 2))
204 | orig_focaly = cam0.image_height / (2 * np.tan(orig_fovy / 2))
205 |
206 | for i, cam in enumerate(traj):
207 | cam = copy.deepcopy(cam)
208 |
209 | if zoom_start <= i < zoom_start + zoom_duration:
210 | t = (i - zoom_start) / max(zoom_duration - 1, 1)
211 | zoom_factor = 1 + t * (zoom_intensity - 1)
212 |
213 | elif zoom_start + zoom_duration <= i < zoom_start + 2 * zoom_duration:
214 | t = (i - (zoom_start + zoom_duration)) / max(zoom_duration - 1, 1)
215 | zoom_factor = zoom_intensity - t * (zoom_intensity - 1)
216 | else:
217 | zoom_factor = 1.0
218 |
219 | new_focalx = orig_focalx * zoom_factor
220 | new_focaly = orig_focaly * zoom_factor
221 | new_fovx = 2 * np.arctan(cam.image_width / (2 * new_focalx))
222 | new_fovy = 2 * np.arctan(cam.image_height / (2 * new_focaly))
223 | cam.FoVx = new_fovx
224 | cam.FoVy = new_fovy
225 |
226 | cam.projection_matrix = getProjectionMatrix(znear=cam.znear, zfar=cam.zfar, fovX=new_fovx, fovY=new_fovy).transpose(0,1).cuda()
227 | cam.full_proj_transform = (cam.world_view_transform.unsqueeze(0).bmm(cam.projection_matrix.unsqueeze(0))).squeeze(0)
228 | traj[i] = cam
229 | return traj
230 |
231 | def load_img(pth: str) -> np.ndarray:
232 | """Load an image and cast to float32."""
233 | with open(pth, 'rb') as f:
234 | image = np.array(Image.open(f), dtype=np.float32)
235 | return image
236 |
237 |
238 | def create_videos(base_dir, input_dir, out_name, num_frames=480):
239 | """Creates videos out of the images saved to disk."""
240 | # Last two parts of checkpoint path are experiment name and scene name.
241 | video_prefix = f'{out_name}'
242 |
243 | zpad = max(5, len(str(num_frames - 1)))
244 | idx_to_str = lambda idx: str(idx).zfill(zpad)
245 |
246 | os.makedirs(base_dir, exist_ok=True)
247 |
248 | img_file = os.path.join(input_dir, 'renders', f'{idx_to_str(0)}.png')
249 | img = load_img(img_file)
250 | shape = img.shape
251 |
252 | print(f'Video shape is {shape[:2]}')
253 |
254 | video_kwargs = {
255 | 'shape': shape[:2],
256 | 'codec': 'h264',
257 | 'fps': 120,
258 | 'crf': 18,
259 | }
260 |
261 | video_file = os.path.join(base_dir, f'{video_prefix}_color.mp4')
262 | input_format = 'rgb'
263 |
264 | file_ext = 'png'
265 | idx = 0
266 |
267 | file0 = os.path.join(input_dir, 'renders', f'{idx_to_str(0)}.{file_ext}')
268 |
269 | if not os.path.exists(file0):
270 | return
271 | print(f'Making video {video_file}...')
272 | with media.VideoWriter(video_file, **video_kwargs, input_format=input_format) as writer:
273 | for idx in tqdm(range(num_frames)):
274 |
275 | img_file = os.path.join(input_dir, 'renders', f'{idx_to_str(idx)}.{file_ext}')
276 |
277 | if not os.path.exists(img_file):
278 | ValueError(f'Image file {img_file} does not exist.')
279 | img = load_img(img_file)
280 | img = img / 255.
281 |
282 | frame = (np.clip(np.nan_to_num(img), 0., 1.) * 255.).astype(np.uint8)
283 | writer.add_image(frame)
284 | idx += 1
285 |
286 | def save_img_u8(img, pth):
287 | """Save an image (probably RGB) in [0, 1] to disk as a uint8 PNG."""
288 | with open(pth, 'wb') as f:
289 | Image.fromarray(
290 | (np.clip(np.nan_to_num(img), 0., 1.) * 255.).astype(np.uint8)).save(
291 | f, 'PNG')
292 |
293 | def save_img_f32(depthmap, pth):
294 | """Save an image (probably a depthmap) to disk as a float32 TIFF."""
295 | with open(pth, 'wb') as f:
296 | Image.fromarray(np.nan_to_num(depthmap).astype(np.float32)).save(f, 'TIFF')
297 |
--------------------------------------------------------------------------------
/utils/sh_utils.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 The PlenOctree Authors.
2 | # Redistribution and use in source and binary forms, with or without
3 | # modification, are permitted provided that the following conditions are met:
4 | #
5 | # 1. Redistributions of source code must retain the above copyright notice,
6 | # this list of conditions and the following disclaimer.
7 | #
8 | # 2. Redistributions in binary form must reproduce the above copyright notice,
9 | # this list of conditions and the following disclaimer in the documentation
10 | # and/or other materials provided with the distribution.
11 | #
12 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
13 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
14 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
15 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
16 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
17 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
18 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
19 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
20 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
21 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
22 | # POSSIBILITY OF SUCH DAMAGE.
23 |
24 | import torch
25 |
26 | C0 = 0.28209479177387814
27 | C1 = 0.4886025119029199
28 | C2 = [
29 | 1.0925484305920792,
30 | -1.0925484305920792,
31 | 0.31539156525252005,
32 | -1.0925484305920792,
33 | 0.5462742152960396
34 | ]
35 | C3 = [
36 | -0.5900435899266435,
37 | 2.890611442640554,
38 | -0.4570457994644658,
39 | 0.3731763325901154,
40 | -0.4570457994644658,
41 | 1.445305721320277,
42 | -0.5900435899266435
43 | ]
44 | C4 = [
45 | 2.5033429417967046,
46 | -1.7701307697799304,
47 | 0.9461746957575601,
48 | -0.6690465435572892,
49 | 0.10578554691520431,
50 | -0.6690465435572892,
51 | 0.47308734787878004,
52 | -1.7701307697799304,
53 | 0.6258357354491761,
54 | ]
55 |
56 |
57 | def eval_sh(deg, sh, dirs):
58 | """
59 | Evaluate spherical harmonics at unit directions
60 | using hardcoded SH polynomials.
61 | Works with torch/np/jnp.
62 | ... Can be 0 or more batch dimensions.
63 | Args:
64 | deg: int SH deg. Currently, 0-3 supported
65 | sh: jnp.ndarray SH coeffs [..., C, (deg + 1) ** 2]
66 | dirs: jnp.ndarray unit directions [..., 3]
67 | Returns:
68 | [..., C]
69 | """
70 | assert deg <= 4 and deg >= 0
71 | coeff = (deg + 1) ** 2
72 | assert sh.shape[-1] >= coeff
73 |
74 | result = C0 * sh[..., 0]
75 | if deg > 0:
76 | x, y, z = dirs[..., 0:1], dirs[..., 1:2], dirs[..., 2:3]
77 | result = (result -
78 | C1 * y * sh[..., 1] +
79 | C1 * z * sh[..., 2] -
80 | C1 * x * sh[..., 3])
81 |
82 | if deg > 1:
83 | xx, yy, zz = x * x, y * y, z * z
84 | xy, yz, xz = x * y, y * z, x * z
85 | result = (result +
86 | C2[0] * xy * sh[..., 4] +
87 | C2[1] * yz * sh[..., 5] +
88 | C2[2] * (2.0 * zz - xx - yy) * sh[..., 6] +
89 | C2[3] * xz * sh[..., 7] +
90 | C2[4] * (xx - yy) * sh[..., 8])
91 |
92 | if deg > 2:
93 | result = (result +
94 | C3[0] * y * (3 * xx - yy) * sh[..., 9] +
95 | C3[1] * xy * z * sh[..., 10] +
96 | C3[2] * y * (4 * zz - xx - yy)* sh[..., 11] +
97 | C3[3] * z * (2 * zz - 3 * xx - 3 * yy) * sh[..., 12] +
98 | C3[4] * x * (4 * zz - xx - yy) * sh[..., 13] +
99 | C3[5] * z * (xx - yy) * sh[..., 14] +
100 | C3[6] * x * (xx - 3 * yy) * sh[..., 15])
101 |
102 | if deg > 3:
103 | result = (result + C4[0] * xy * (xx - yy) * sh[..., 16] +
104 | C4[1] * yz * (3 * xx - yy) * sh[..., 17] +
105 | C4[2] * xy * (7 * zz - 1) * sh[..., 18] +
106 | C4[3] * yz * (7 * zz - 3) * sh[..., 19] +
107 | C4[4] * (zz * (35 * zz - 30) + 3) * sh[..., 20] +
108 | C4[5] * xz * (7 * zz - 3) * sh[..., 21] +
109 | C4[6] * (xx - yy) * (7 * zz - 1) * sh[..., 22] +
110 | C4[7] * xz * (xx - 3 * yy) * sh[..., 23] +
111 | C4[8] * (xx * (xx - 3 * yy) - yy * (3 * xx - yy)) * sh[..., 24])
112 | return result
113 |
114 | def RGB2SH(rgb):
115 | return (rgb - 0.5) / C0
116 |
117 | def SH2RGB(sh):
118 | return sh * C0 + 0.5
--------------------------------------------------------------------------------
/utils/system_utils.py:
--------------------------------------------------------------------------------
1 | #
2 | # The original code is under the following copyright:
3 | # Copyright (C) 2023, Inria
4 | # GRAPHDECO research group, https://team.inria.fr/graphdeco
5 | # All rights reserved.
6 | #
7 | # This software is free for non-commercial, research and evaluation use
8 | # under the terms of the LICENSE_GS.md file.
9 | #
10 | # For inquiries contact george.drettakis@inria.fr
11 | #
12 | # The modifications of the code are under the following copyright:
13 | # Copyright (C) 2024, University of Liege, KAUST and University of Oxford
14 | # TELIM research group, http://www.telecom.ulg.ac.be/
15 | # IVUL research group, https://ivul.kaust.edu.sa/
16 | # VGG research group, https://www.robots.ox.ac.uk/~vgg/
17 | # All rights reserved.
18 | # The modifications are under the LICENSE.md file.
19 | #
20 | # For inquiries contact jan.held@uliege.be
21 | #
22 |
23 | from errno import EEXIST
24 | from os import makedirs, path
25 | import os
26 |
27 | def mkdir_p(folder_path):
28 | # Creates a directory. equivalent to using mkdir -p on the command line
29 | try:
30 | makedirs(folder_path)
31 | except OSError as exc: # Python >2.5
32 | if exc.errno == EEXIST and path.isdir(folder_path):
33 | pass
34 | else:
35 | raise
36 |
37 | def searchForMaxIteration(folder):
38 | saved_iters = [int(fname.split("_")[-1]) for fname in os.listdir(folder)]
39 | return max(saved_iters)
40 |
--------------------------------------------------------------------------------