├── .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 |
4 | Project page  |  5 | Arxiv  |  6 |
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 | Abstract Image 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 | --------------------------------------------------------------------------------