├── .gitignore ├── LICENSE ├── README.md ├── app.py ├── docs ├── 692db.png ├── dinosaur.png ├── elu.png ├── lego.png ├── scene0707.png ├── sph.png ├── teaser.png └── xyz.png ├── inputs ├── colmap │ └── scene0707 │ │ ├── images.txt │ │ └── images │ │ ├── 0.jpg │ │ ├── 192.jpg │ │ └── 64.jpg ├── nerf │ └── lego │ │ ├── images │ │ ├── r_0.png │ │ ├── r_19.png │ │ ├── r_20.png │ │ ├── r_21.png │ │ └── r_23.png │ │ └── transforms.json └── quick │ ├── cam_c2w │ ├── images │ │ ├── 001.png │ │ ├── 011.png │ │ ├── 065.png │ │ └── 077.png │ ├── poses.json │ └── poses │ │ ├── 001.npy │ │ ├── 011.npy │ │ ├── 065.npy │ │ └── 077.npy │ ├── cam_elu │ ├── poses.json │ └── poses │ │ ├── 0.txt │ │ ├── 1.txt │ │ ├── 2.txt │ │ ├── 3.txt │ │ ├── 4.txt │ │ └── 5.txt │ ├── cam_sph │ ├── poses.json │ └── poses │ │ ├── 0.txt │ │ ├── 1.txt │ │ ├── 2.txt │ │ └── 3.txt │ ├── cam_w2c │ ├── images │ │ ├── 001.png │ │ ├── 004.png │ │ ├── 008.png │ │ └── 010.png │ ├── poses.json │ └── poses │ │ ├── 001.npy │ │ ├── 004.npy │ │ ├── 008.npy │ │ └── 010.npy │ └── cam_xyz │ ├── poses.json │ └── poses │ ├── 0.txt │ ├── 1.txt │ ├── 2.txt │ └── 3.txt ├── requirements.txt └── src ├── loader.py ├── utils.py └── visualizer.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CameraViewer: A lightweight tool for camera pose visualization 2 | Visualize camera poses in seconds! 3 | A python tool that helps plot cameras for 3D computer vision research. 4 | 5 | 6 | 7 | ## Installation 8 | Create an environment with Python >= 3.9 (Recommend to use [Anaconda](https://www.anaconda.com/download/) or [Miniconda](https://docs.conda.io/en/latest/miniconda.html)) 9 | ``` 10 | conda create -n viewer python=3.9 11 | conda activate viewer 12 | pip install -r requirements.txt 13 | ``` 14 | 15 | ## Run Demo 16 | 17 | #### 1. Camera poses by positions in [spherical coordinates](https://en.wikipedia.org/wiki/Spherical_coordinate_system) (polar, azimuth, radius). 18 | Cameras will look at the origin point, and their up directions will be +Z. 19 | 20 | To read poses from a single JSON file ([details](#prepare_json)), run the command: 21 | ``` 22 | python app.py --root inputs/quick/cam_sph/ 23 | ``` 24 | 25 | To read poses from a folder of pose files ([details](#prepare_folder)), run the command with ```--type sph``` to parse the files: 26 | ``` 27 | python app.py --root inputs/quick/cam_sph/ --type sph 28 | ``` 29 | 30 | The plotted figure will be opened in your browser: 31 | 32 | 33 | 34 | 35 | 36 | 37 | #### 2. Camera poses by positions in [cartesian coordinates](https://en.wikipedia.org/wiki/Cartesian_coordinate_system) (x, y, z). 38 | Cameras will look at the origin point, and their up directions will be +Z. 39 | 40 | To read poses from a single JSON file ([details](#prepare_json)), run the command: 41 | ``` 42 | python app.py --root inputs/quick/cam_xyz/ 43 | ``` 44 | 45 | To read poses from a folder of pose files ([details](#prepare_folder)), run the command with ```--type xyz``` to parse the files: 46 | ``` 47 | python app.py --root inputs/quick/cam_xyz/ --type xyz 48 | ``` 49 | 50 | 51 | 52 | #### 3. Camera poses by eye (camera) positions, look-at positions, and up vectors. 53 | 54 | To read poses from a single JSON file ([details](#prepare_json)), run the command: 55 | ``` 56 | python app.py --root inputs/quick/cam_elu/ 57 | ``` 58 | 59 | To read poses from a folder of pose files ([details](#prepare_folder)), run the command with ```--type elu``` to parse the files: 60 | ``` 61 | python app.py --root inputs/quick/cam_elu/ --type elu 62 | ``` 63 | 64 | 65 | 66 | #### 4. Camera poses by camera-to-world matrix. 67 | 68 | To read poses from a single JSON file ([details](#prepare_json)), run the command: 69 | ``` 70 | python app.py --root inputs/quick/cam_c2w/ 71 | ``` 72 | 73 | To read poses from a folder of pose files ([details](#prepare_folder)), run the command with ```--type c2w``` to parse the files: 74 | ``` 75 | python app.py --root inputs/quick/cam_c2w/ --type c2w 76 | ``` 77 | 78 | 79 | 80 | #### 5. Camera poses by world-to-camera matrix. 81 | 82 | To read poses from a single JSON file ([details](#prepare_json)), run the command: 83 | ``` 84 | python app.py --root inputs/quick/cam_w2c/ --image_size 128 85 | ``` 86 | 87 | To read poses from a folder of pose files ([details](#prepare_folder)), run the command with ```--type w2c``` to parse the files: 88 | ``` 89 | python app.py --root inputs/quick/cam_w2c/ --type w2c --image_size 128 90 | ``` 91 | 92 | 93 | #### 6. Camera poses in NeRF format. 94 | Poses are read from ```transforms.json``` under ```--root``` folder. 95 | ``` 96 | python app.py --root inputs/nerf/lego/ --format nerf --scene_size 6 97 | ``` 98 | 99 | 100 | #### 7. Camera poses in COLMAP format. 101 | Poses are read from ```images.txt``` under ```--root``` folder. 102 | ``` 103 | python app.py --root inputs/colmap/scene0707/ --format colmap --scene_size 7 104 | ``` 105 | 106 | 107 | ## Prepare Cameras in Quick Format 108 | 109 | There are two options to prepare camera poses: JSON or folder 110 | 111 | 112 | ### 1. Prepare JSON 113 | 114 | Step 1: Create a folder containing ```poses.json``` and ```images/``` (optional). For example: 115 | ``` 116 | mkdir -p inputs/obj/ 117 | touch inputs/obj/poses.json 118 | mkdir -p inputs/obj/images/ #optional 119 | ``` 120 | 121 | Step 2: Prepare the ```poses.json``` file with the following structure: 122 | ``` 123 | { 124 | "type": "sph", 125 | "frames": [ 126 | { 127 | "image_name": "0.png", 128 | "pose": [75, 0, 4] 129 | }, 130 | { 131 | "image_name": "1.png", 132 | "pose": [75, 90, 4] 133 | }, 134 | { 135 | "image_name": "2.png", 136 | "pose": [75, 180, 4] 137 | } 138 | ] 139 | } 140 | ``` 141 | The value of ```"type"``` specifies the type of camera poses, which can be: 142 | 1. ```sph```: A row vector of spherical coordinates: polar (degree), azimuth (degree), radius. [Demo sph](#demo_sph). 143 | 2. ```xyz```: A row vector of cartesian coordinates: x, y, z. [Demo xyz](#demo_xyz). 144 | 3. ```elu```: A matrix including eye position (1st row), look-at position (2nd row), and up vector (3rd row). [Demo elu](#demo_elu). 145 | 4. ```c2w```: A camera-to-world matrix. [Demo cam2world](#demo_c2w). 146 | 5. ```w2c```: A world-to-camera matrix. [Demo world2cam](#demo_w2c). 147 | 148 | Step 3 (Optional): Put the corresponding images under ```images/```. For example: 149 | ``` 150 | inputs 151 | ├── obj 152 | ├── images 153 | ├── 0.png 154 | ├── 1.png 155 | ├── 2.png 156 | ``` 157 | 158 | The image files can be in PNG (.png) or JPEG (.jpg / .jpeg) format. 159 | 160 | 161 | ### 2. Prepare folder 162 | 163 | Step 1: Create a folder containing ```poses/``` and ```images/``` (optional). For example: 164 | ``` 165 | mkdir -p inputs/obj/ 166 | mkdir -p inputs/obj/poses/ 167 | mkdir -p inputs/obj/images/ #optional 168 | ``` 169 | 170 | Step 2: Put camera pose files under ```poses/```. For example: 171 | ``` 172 | inputs 173 | ├── obj 174 | ├── poses 175 | ├── 0.txt 176 | ├── 1.txt 177 | ├── 2.txt 178 | ``` 179 | The camera poses can be in the mentioned 5 types: sph, xyz, elu, c2w, w2c. The pose files can be in plain text (.txt) or Numpy (.npy). 180 | 181 | 182 | Step 3 (Optional): Put the corresponding images under ```images/``` with the same names to the pose files. For example: 183 | ``` 184 | inputs 185 | ├── obj 186 | ├── images 187 | ├── 0.png 188 | ├── 1.png 189 | ├── 2.png 190 | ``` 191 | 192 | The image files can be in PNG (.png) or JPEG (.jpg / .jpeg) format. 193 | 194 | ## Known Issues 195 | - Running the commands in PyCharm might not open the figures in your browser. Please use terminal if this issue occurred. 196 | ## Acknowledgement 197 | 198 | Part of the code is modified from: 199 | - [Zero-1-to-3](https://github.com/cvlab-columbia/zero123) 200 | - [Instant-NGP](https://github.com/NVlabs/instant-ngp) 201 | 202 | Thanks to the maintainers of these projects! -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import argparse 3 | import numpy as np 4 | 5 | from src.visualizer import CameraVisualizer 6 | from src.loader import load_quick, load_nerf, load_colmap 7 | from src.utils import load_image, rescale_cameras, recenter_cameras 8 | 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument('--root', type=str) 11 | parser.add_argument('--format', default='quick', choices=['quick', 'nerf', 'colmap']) 12 | parser.add_argument('--type', default=None, choices=[None, 'sph', 'xyz', 'elu', 'c2w', 'w2c']) 13 | parser.add_argument('--no_images', action='store_true') 14 | parser.add_argument('--mesh_path', type=str, default=None) 15 | parser.add_argument('--image_size', type=int, default=256) 16 | parser.add_argument('--scene_size', type=int, default=5) 17 | parser.add_argument('--y_up', action='store_true') 18 | parser.add_argument('--recenter', action='store_true') 19 | parser.add_argument('--rescale', type=float, default=None) 20 | 21 | args = parser.parse_args() 22 | 23 | root_path = args.root 24 | 25 | poses = [] 26 | legends = [] 27 | colors = [] 28 | images = None 29 | 30 | if args.format == 'quick': 31 | poses, legends, colors, image_paths = load_quick(root_path, args.type) 32 | 33 | elif args.format == 'nerf': 34 | poses, legends, colors, image_paths = load_nerf(root_path) 35 | 36 | elif args.format == 'colmap': 37 | poses, legends, colors, image_paths = load_colmap(root_path) 38 | 39 | if args.recenter: 40 | poses = recenter_cameras(poses) 41 | 42 | if args.rescale is not None: 43 | poses = rescale_cameras(poses, args.rescale) 44 | 45 | if args.y_up: 46 | for i in range(0, len(poses)): 47 | poses[i] = poses[i][[0, 2, 1, 3]] 48 | poses[i][1, :] *= -1 49 | 50 | if not args.no_images: 51 | images = [] 52 | for fpath in image_paths: 53 | if fpath is None: 54 | images.append(None) 55 | continue 56 | 57 | if not os.path.exists(fpath): 58 | images.append(None) 59 | print(f'Image not found at {fpath}') 60 | continue 61 | 62 | images.append(load_image(fpath, sz=args.image_size)) 63 | 64 | viz = CameraVisualizer(poses, legends, colors, images=images) 65 | fig = viz.update_figure(args.scene_size, base_radius=1, zoom_scale=1, show_grid=True, show_ticklabels=True, show_background=True, y_up=args.y_up) 66 | 67 | fig.show() 68 | -------------------------------------------------------------------------------- /docs/692db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/docs/692db.png -------------------------------------------------------------------------------- /docs/dinosaur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/docs/dinosaur.png -------------------------------------------------------------------------------- /docs/elu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/docs/elu.png -------------------------------------------------------------------------------- /docs/lego.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/docs/lego.png -------------------------------------------------------------------------------- /docs/scene0707.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/docs/scene0707.png -------------------------------------------------------------------------------- /docs/sph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/docs/sph.png -------------------------------------------------------------------------------- /docs/teaser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/docs/teaser.png -------------------------------------------------------------------------------- /docs/xyz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/docs/xyz.png -------------------------------------------------------------------------------- /inputs/colmap/scene0707/images.txt: -------------------------------------------------------------------------------- 1 | # Image list with two lines of data per image: 2 | # IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME 3 | # POINTS2D[] as (X, Y, POINT3D_ID) 4 | 0 0.96189108396292755 0.039409358542869311 0.24606776867738878 0.11253043263933116 -2.1236667882274385 -2.4074735104736198 5.9633090289628292 0 0.jpg 5 | placeholder 6 | 64 0.9974813822968478 0.0066881297756471014 0.057842570805899662 0.040501825808703103 -1.1100342290448786 -0.74444845526585324 2.2274061359123856 0 64.jpg 7 | placeholder 8 | 192 0.83593989892900133 -0.019022056137035705 -0.53056310342994784 -0.13908788602057728 3.0060690020464436 -0.16022586999382854 0.65428841970310569 0 192.jpg 9 | placeholder -------------------------------------------------------------------------------- /inputs/colmap/scene0707/images/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/colmap/scene0707/images/0.jpg -------------------------------------------------------------------------------- /inputs/colmap/scene0707/images/192.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/colmap/scene0707/images/192.jpg -------------------------------------------------------------------------------- /inputs/colmap/scene0707/images/64.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/colmap/scene0707/images/64.jpg -------------------------------------------------------------------------------- /inputs/nerf/lego/images/r_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/nerf/lego/images/r_0.png -------------------------------------------------------------------------------- /inputs/nerf/lego/images/r_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/nerf/lego/images/r_19.png -------------------------------------------------------------------------------- /inputs/nerf/lego/images/r_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/nerf/lego/images/r_20.png -------------------------------------------------------------------------------- /inputs/nerf/lego/images/r_21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/nerf/lego/images/r_21.png -------------------------------------------------------------------------------- /inputs/nerf/lego/images/r_23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/nerf/lego/images/r_23.png -------------------------------------------------------------------------------- /inputs/nerf/lego/transforms.json: -------------------------------------------------------------------------------- 1 | { 2 | "camera_angle_x": 0.6911112070083618, 3 | "frames": [ 4 | { 5 | "file_path": "./images/r_0.png", 6 | "rotation": 0.012566370614359171, 7 | "transform_matrix": [ 8 | [ 9 | -0.963964581489563, 10 | -0.2611401677131653, 11 | 0.0507759265601635, 12 | 0.2046843022108078 13 | ], 14 | [ 15 | 0.26603081822395325, 16 | -0.9462433457374573, 17 | 0.18398693203926086, 18 | 0.7416750192642212 19 | ], 20 | [ 21 | 7.450580596923828e-09, 22 | 0.1908649355173111, 23 | 0.9816163182258606, 24 | 3.957021951675415 25 | ], 26 | [ 27 | 0.0, 28 | 0.0, 29 | 0.0, 30 | 1.0 31 | ] 32 | ] 33 | }, 34 | { 35 | "file_path": "./images/r_19.png", 36 | "rotation": 0.012566370614359171, 37 | "transform_matrix": [ 38 | [ 39 | 0.13194742798805237, 40 | 0.33291926980018616, 41 | -0.9336779117584229, 42 | -3.7637763023376465 43 | ], 44 | [ 45 | -0.9912567138671875, 46 | 0.04431530460715294, 47 | -0.12428304553031921, 48 | -0.5010010004043579 49 | ], 50 | [ 51 | 0.0, 52 | 0.9419134259223938, 53 | 0.33585572242736816, 54 | 1.3538779020309448 55 | ], 56 | [ 57 | 0.0, 58 | 0.0, 59 | 0.0, 60 | 1.0 61 | ] 62 | ] 63 | }, 64 | { 65 | "file_path": "./images/r_20.png", 66 | "rotation": 0.012566370614359171, 67 | "transform_matrix": [ 68 | [ 69 | -0.915581226348877, 70 | 0.09515299648046494, 71 | -0.390713095664978, 72 | -1.5750149488449097 73 | ], 74 | [ 75 | -0.4021328091621399, 76 | -0.21664556860923767, 77 | 0.8895806670188904, 78 | 3.58601450920105 79 | ], 80 | [ 81 | 0.0, 82 | 0.9716021418571472, 83 | 0.23662075400352478, 84 | 0.9538488388061523 85 | ], 86 | [ 87 | 0.0, 88 | 0.0, 89 | 0.0, 90 | 1.0 91 | ] 92 | ] 93 | }, 94 | { 95 | "file_path": "./images/r_21.png", 96 | "rotation": 0.012566370614359171, 97 | "transform_matrix": [ 98 | [ 99 | 0.995094895362854, 100 | 0.040533654391765594, 101 | -0.09023939073085785, 102 | -0.3637666404247284 103 | ], 104 | [ 105 | -0.09892485290765762, 106 | 0.4077320694923401, 107 | -0.9077270030975342, 108 | -3.6591649055480957 109 | ], 110 | [ 111 | 0.0, 112 | 0.912201464176178, 113 | 0.4097418785095215, 114 | 1.6517224311828613 115 | ], 116 | [ 117 | 0.0, 118 | 0.0, 119 | 0.0, 120 | 1.0 121 | ] 122 | ] 123 | }, 124 | { 125 | "file_path": "./images/r_23.png", 126 | "rotation": 0.012566370614359171, 127 | "transform_matrix": [ 128 | [ 129 | -0.24700850248336792, 130 | -0.2722141444683075, 131 | 0.9299926161766052, 132 | 3.74891996383667 133 | ], 134 | [ 135 | 0.9690133333206177, 136 | -0.06938935071229935, 137 | 0.23706182837486267, 138 | 0.9556267261505127 139 | ], 140 | [ 141 | 0.0, 142 | 0.9597314596176147, 143 | 0.2809188961982727, 144 | 1.132420301437378 145 | ], 146 | [ 147 | 0.0, 148 | 0.0, 149 | 0.0, 150 | 1.0 151 | ] 152 | ] 153 | } 154 | ] 155 | } 156 | -------------------------------------------------------------------------------- /inputs/quick/cam_c2w/images/001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_c2w/images/001.png -------------------------------------------------------------------------------- /inputs/quick/cam_c2w/images/011.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_c2w/images/011.png -------------------------------------------------------------------------------- /inputs/quick/cam_c2w/images/065.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_c2w/images/065.png -------------------------------------------------------------------------------- /inputs/quick/cam_c2w/images/077.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_c2w/images/077.png -------------------------------------------------------------------------------- /inputs/quick/cam_c2w/poses.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "c2w", 3 | "frames": [ 4 | { 5 | "image_name": "001.png", 6 | "pose": [ 7 | [ 8 | 0.9105374217033386, 9 | -0.09619740396738052, 10 | 0.4020790457725525, 11 | 1.6208324432373047 12 | ], 13 | [ 14 | 0.41342654824256897, 15 | 0.21186675131320953, 16 | -0.885545551776886, 17 | -3.5697484016418457 18 | ], 19 | [ 20 | 0.0, 21 | 0.972552478313446, 22 | 0.2326831966638565, 23 | 0.9379759430885315 24 | ], 25 | [ 26 | 0.0, 27 | 0.0, 28 | 0.0, 29 | 1.0 30 | ] 31 | ] 32 | }, 33 | { 34 | "image_name": "011.png", 35 | "pose": [ 36 | [ 37 | 0.36621522903442383, 38 | 0.21183644235134125, 39 | -0.9060969352722168, 40 | -3.6525936126708984 41 | ], 42 | [ 43 | -0.9305301904678345, 44 | 0.08336939662694931, 45 | -0.3565994203090668, 46 | -1.4374982118606567 47 | ], 48 | [ 49 | 0.0, 50 | 0.9737426042556763, 51 | 0.22765137255191803, 52 | 0.9176920652389526 53 | ], 54 | [ 55 | 0.0, 56 | 0.0, 57 | 0.0, 58 | 1.0 59 | ] 60 | ] 61 | }, 62 | { 63 | "image_name": "077.png", 64 | "pose": [ 65 | [ 66 | -0.9482272863388062, 67 | 0.09684965759515762, 68 | -0.3024653494358063, 69 | -1.219276785850525 70 | ], 71 | [ 72 | -0.3175927400588989, 73 | -0.28916114568710327, 74 | 0.9030618071556091, 75 | 3.6403586864471436 76 | ], 77 | [ 78 | 0.0, 79 | 0.9523684978485107, 80 | 0.30494922399520874, 81 | 1.2292896509170532 82 | ], 83 | [ 84 | 0.0, 85 | 0.0, 86 | 0.0, 87 | 1.0 88 | ] 89 | ] 90 | }, 91 | { 92 | "image_name": "065.png", 93 | "pose": [ 94 | [ 95 | -0.35092002153396606, 96 | -0.2937421202659607, 97 | 0.8891403675079346, 98 | 3.5842397212982178 99 | ], 100 | [ 101 | 0.9364054203033447, 102 | -0.11008051037788391, 103 | 0.33320730924606323, 104 | 1.343201756477356 105 | ], 106 | [ 107 | 0.0, 108 | 0.9495250582695007, 109 | 0.3136911392211914, 110 | 1.2645295858383179 111 | ], 112 | [ 113 | 0.0, 114 | 0.0, 115 | 0.0, 116 | 1.0 117 | ] 118 | ] 119 | } 120 | ] 121 | } -------------------------------------------------------------------------------- /inputs/quick/cam_c2w/poses/001.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_c2w/poses/001.npy -------------------------------------------------------------------------------- /inputs/quick/cam_c2w/poses/011.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_c2w/poses/011.npy -------------------------------------------------------------------------------- /inputs/quick/cam_c2w/poses/065.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_c2w/poses/065.npy -------------------------------------------------------------------------------- /inputs/quick/cam_c2w/poses/077.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_c2w/poses/077.npy -------------------------------------------------------------------------------- /inputs/quick/cam_elu/poses.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "elu", 3 | "frames": [ 4 | { 5 | "image_name": null, 6 | "pose": [[3.5, 0, 1], [0, 0, 0], [0, 0, 1]] 7 | }, 8 | { 9 | "pose": [[0, 3.5, 1], [0, 0, 0], [0, 0, 1]] 10 | }, 11 | { 12 | "pose": [[-3.5, 0, 1], [0, 0, 0], [0, 0, 1]] 13 | }, 14 | { 15 | "pose": [[0, -3.5, 1], [0, 0, 0], [0, 0, 1]] 16 | }, 17 | { 18 | "pose": [[0, 0, 3.64], [0, 0, 0], [0, 0, 1]] 19 | }, 20 | { 21 | "pose": [[0, 0, -3.64], [0, 0, 1], [0, 0, -1]] 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /inputs/quick/cam_elu/poses/0.txt: -------------------------------------------------------------------------------- 1 | 3.5 0 1 2 | 0 0 0 3 | 0 0 1 -------------------------------------------------------------------------------- /inputs/quick/cam_elu/poses/1.txt: -------------------------------------------------------------------------------- 1 | 0 3.5 1 2 | 0 0 0 3 | 0 0 1 -------------------------------------------------------------------------------- /inputs/quick/cam_elu/poses/2.txt: -------------------------------------------------------------------------------- 1 | -3.5 0 1 2 | 0 0 0 3 | 0 0 1 -------------------------------------------------------------------------------- /inputs/quick/cam_elu/poses/3.txt: -------------------------------------------------------------------------------- 1 | 0 -3.5 1 2 | 0 0 0 3 | 0 0 1 -------------------------------------------------------------------------------- /inputs/quick/cam_elu/poses/4.txt: -------------------------------------------------------------------------------- 1 | 0 0 3.64 2 | 0 0 0 3 | 0 0 1 -------------------------------------------------------------------------------- /inputs/quick/cam_elu/poses/5.txt: -------------------------------------------------------------------------------- 1 | 0 0 -3.64 2 | 0 0 1 3 | 0 0 -1 -------------------------------------------------------------------------------- /inputs/quick/cam_sph/poses.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "sph", 3 | "frames": [ 4 | { 5 | "image_name": null, 6 | "pose": [75, 0, 4] 7 | }, 8 | { 9 | "pose": [75, 90, 4] 10 | }, 11 | { 12 | "pose": [75, 180, 4] 13 | }, 14 | { 15 | "pose": [75, 270, 4] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /inputs/quick/cam_sph/poses/0.txt: -------------------------------------------------------------------------------- 1 | 75 0 4 -------------------------------------------------------------------------------- /inputs/quick/cam_sph/poses/1.txt: -------------------------------------------------------------------------------- 1 | 75 90 4 -------------------------------------------------------------------------------- /inputs/quick/cam_sph/poses/2.txt: -------------------------------------------------------------------------------- 1 | 75 180 4 -------------------------------------------------------------------------------- /inputs/quick/cam_sph/poses/3.txt: -------------------------------------------------------------------------------- 1 | 75 270 4 -------------------------------------------------------------------------------- /inputs/quick/cam_w2c/images/001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_w2c/images/001.png -------------------------------------------------------------------------------- /inputs/quick/cam_w2c/images/004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_w2c/images/004.png -------------------------------------------------------------------------------- /inputs/quick/cam_w2c/images/008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_w2c/images/008.png -------------------------------------------------------------------------------- /inputs/quick/cam_w2c/images/010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_w2c/images/010.png -------------------------------------------------------------------------------- /inputs/quick/cam_w2c/poses.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "w2c", 3 | "frames": [ 4 | { 5 | "image_name": "008.png", 6 | "pose": [ 7 | [ 8 | 0.732423722743988, 9 | 0.6808491349220276, 10 | 2.1576926911848204e-08, 11 | 2.5004760928482028e-08 12 | ], 13 | [ 14 | 0.44146132469177246, 15 | -0.47490230202674866, 16 | 0.7613013982772827, 17 | -5.960464477539063e-08 18 | ], 19 | [ 20 | 0.5183313488960266, 21 | -0.5575951933860779, 22 | -0.6483982801437378, 23 | -1.7872748374938965 24 | ] 25 | ] 26 | }, 27 | { 28 | "image_name": "001.png", 29 | "pose": [ 30 | [ 31 | 0.2826493978500366, 32 | 0.9592232704162598, 33 | 1.1397052190886825e-08, 34 | -4.187575797232057e-08 35 | ], 36 | [ 37 | -0.6753110289573669, 38 | 0.19899044930934906, 39 | 0.710181474685669, 40 | -1.341104507446289e-07 41 | ], 42 | [ 43 | 0.6812226176261902, 44 | -0.20073238015174866, 45 | 0.7040186524391174, 46 | -1.504714846611023 47 | ] 48 | ] 49 | }, 50 | { 51 | "image_name": "010.png", 52 | "pose": [ 53 | [ 54 | -0.8365102410316467, 55 | -0.5479512214660645, 56 | 2.562700984753974e-08, 57 | 1.353646723600832e-07 58 | ], 59 | [ 60 | -0.22922249138355255, 61 | 0.349934458732605, 62 | 0.9082967042922974, 63 | -1.043081283569336e-07 64 | ], 65 | [ 66 | -0.49770230054855347, 67 | 0.7597995400428772, 68 | -0.4183264672756195, 69 | -1.506967306137085 70 | ] 71 | ] 72 | }, 73 | { 74 | "image_name": "004.png", 75 | "pose": [ 76 | [ 77 | -0.899004340171814, 78 | 0.4379400610923767, 79 | -3.6114071733095443e-09, 80 | 1.2242657021488412e-07 81 | ], 82 | [ 83 | -0.19711413979530334, 84 | -0.4046363830566406, 85 | 0.8929812908172607, 86 | 2.2351741790771484e-07 87 | ], 88 | [ 89 | 0.39107227325439453, 90 | 0.8027939200401306, 91 | 0.4500938355922699, 92 | -1.9792877435684204 93 | ] 94 | ] 95 | } 96 | ] 97 | } -------------------------------------------------------------------------------- /inputs/quick/cam_w2c/poses/001.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_w2c/poses/001.npy -------------------------------------------------------------------------------- /inputs/quick/cam_w2c/poses/004.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_w2c/poses/004.npy -------------------------------------------------------------------------------- /inputs/quick/cam_w2c/poses/008.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_w2c/poses/008.npy -------------------------------------------------------------------------------- /inputs/quick/cam_w2c/poses/010.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xt4d/CameraViewer/d951c403e0a156f77c96e334f824d07f5f10633c/inputs/quick/cam_w2c/poses/010.npy -------------------------------------------------------------------------------- /inputs/quick/cam_xyz/poses.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "xyz", 3 | "frames": [ 4 | { 5 | "image_name": null, 6 | "pose": [4, 0, 1] 7 | }, 8 | { 9 | "pose": [0, 4, 1] 10 | }, 11 | { 12 | "pose": [-4, 0, 1] 13 | }, 14 | { 15 | "pose": [0, -4, 1] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /inputs/quick/cam_xyz/poses/0.txt: -------------------------------------------------------------------------------- 1 | 4 0 1 -------------------------------------------------------------------------------- /inputs/quick/cam_xyz/poses/1.txt: -------------------------------------------------------------------------------- 1 | 0 4 1 -------------------------------------------------------------------------------- /inputs/quick/cam_xyz/poses/2.txt: -------------------------------------------------------------------------------- 1 | -4 0 1 -------------------------------------------------------------------------------- /inputs/quick/cam_xyz/poses/3.txt: -------------------------------------------------------------------------------- 1 | 0 -4 1 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.24.3 2 | Pillow==9.5.0 3 | plotly==5.14.1 -------------------------------------------------------------------------------- /src/loader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import json 4 | 5 | from .utils import elu_to_c2w, spherical_to_cartesian, load_image, qvec_to_rotmat, rotmat 6 | 7 | 8 | def load_quick(root_path, type): 9 | 10 | poses = [] 11 | legends = [] 12 | colors = [] 13 | image_paths = [] 14 | 15 | if type is None: 16 | pose_path = os.path.join(root_path, 'poses.json') 17 | print(f'Load poses from {pose_path}') 18 | with open(pose_path, 'r') as fin: 19 | jdata = json.load(fin) 20 | type = jdata['type'] 21 | frame_list = jdata['frames'] 22 | else: 23 | pose_root = os.path.join(root_path, 'poses') 24 | print(f'Load poses from {pose_root}') 25 | frame_list = os.listdir(pose_root) 26 | 27 | image_root = os.path.join(root_path, 'images') 28 | print(f'Load images from {image_root}') 29 | 30 | for idx, frame in enumerate(frame_list): 31 | 32 | fid = idx 33 | 34 | if isinstance(frame, str): 35 | 36 | fname = frame 37 | vals = fname.split('.') 38 | fid, ext = vals[0], vals[-1] 39 | 40 | fpath = os.path.join(pose_root, fname) 41 | 42 | if ext == 'npy': 43 | mat = np.load(fpath) 44 | elif ext == 'txt': 45 | mat = np.loadtxt(fpath) 46 | 47 | img_paths = [ os.path.join(image_root, f'{fid}.{ext}') for ext in ['png', 'jpg', 'jpeg']] 48 | img_paths = [ fpath for fpath in img_paths if os.path.exists(fpath) ] 49 | 50 | img_path = img_paths[0] if len(img_paths) > 0 else None 51 | 52 | elif isinstance(frame, dict): 53 | 54 | if 'image_name' in frame and frame['image_name']: 55 | fname = frame['image_name'] 56 | img_path = os.path.join(image_root, fname) 57 | else: 58 | img_path = None 59 | 60 | mat = np.array(frame['pose']) 61 | 62 | if type == 'c2w': 63 | c2w = mat 64 | if c2w.shape[0] == 3: 65 | c2w = np.concatenate([c2w, np.zeros((1, 4))], axis=0) 66 | c2w[-1, -1] = 1 67 | 68 | if type == 'w2c': 69 | w2c = mat 70 | if w2c.shape[0] == 3: 71 | w2c = np.concatenate([w2c, np.zeros((1, 4))], axis=0) 72 | w2c[-1, -1] = 1 73 | c2w = np.linalg.inv(w2c) 74 | 75 | elif type == 'elu': 76 | eye = mat[0, :] 77 | lookat = mat[1, :] 78 | up = mat[2, :] 79 | c2w = elu_to_c2w(eye, lookat, up) 80 | 81 | elif type == 'sph' or type == 'xyz': 82 | 83 | assert (mat.size == 3) 84 | 85 | if type == 'sph': 86 | eye = spherical_to_cartesian((np.deg2rad(mat[0]), np.deg2rad(mat[1]), mat[2])) 87 | else: 88 | eye = mat 89 | 90 | lookat = np.zeros(3) 91 | up = np.array([0, 0, 1]) 92 | c2w = elu_to_c2w(eye, lookat, up) 93 | 94 | poses.append(c2w) 95 | legends.append( os.path.basename(img_path) if img_path else str(fid) ) 96 | colors.append('blue') 97 | image_paths.append(img_path) 98 | 99 | return poses, legends, colors, image_paths 100 | 101 | 102 | def load_nerf(root_path): 103 | 104 | poses = [] 105 | legends = [] 106 | colors = [] 107 | image_paths = [] 108 | 109 | pose_path = os.path.join(root_path, 'transforms.json') 110 | print(f'Load poses from {pose_path}') 111 | 112 | with open(pose_path, 'r') as fin: 113 | jdata = json.load(fin) 114 | 115 | for fi, frm in enumerate(jdata['frames']): 116 | 117 | c2w = np.array(frm['transform_matrix']) 118 | poses.append(c2w) 119 | colors.append('blue') 120 | 121 | if 'file_path' in frm: 122 | fpath = frm['file_path'] 123 | fname = os.path.basename(fpath) 124 | 125 | legends.append(fname) 126 | image_paths.append(os.path.join(root_path, fpath)) 127 | else: 128 | legends.append(str(fi)) 129 | image_paths.append(None) 130 | 131 | return poses, legends, colors, image_paths 132 | 133 | 134 | def load_colmap(root_path): 135 | 136 | poses = [] 137 | legends = [] 138 | colors = [] 139 | image_paths = [] 140 | 141 | pose_path = os.path.join(root_path, 'images.txt') 142 | print(f'Load poses from {pose_path}') 143 | 144 | fin = open(pose_path, 'r') 145 | 146 | up = np.zeros(3) 147 | 148 | i = 0 149 | for line in fin: 150 | line = line.strip() 151 | if line[0] == "#": 152 | continue 153 | i = i + 1 154 | if i % 2 == 0: 155 | continue 156 | elems = line.split(' ') 157 | 158 | fname = '_'.join(elems[9:]) 159 | legends.append(fname) 160 | 161 | fpath = os.path.join(root_path, 'images', fname) 162 | image_paths.append(fpath) 163 | 164 | qvec = np.array(tuple(map(float, elems[1:5]))) 165 | tvec = np.array(tuple(map(float, elems[5:8]))) 166 | rot = qvec_to_rotmat(-qvec) 167 | tvec = tvec.reshape(3) 168 | 169 | w2c = np.eye(4) 170 | w2c[:3, :3] = rot 171 | w2c[:3, -1] = tvec 172 | c2w = np.linalg.inv(w2c) 173 | 174 | c2w[0:3,2] *= -1 # flip the y and z axis 175 | c2w[0:3,1] *= -1 176 | c2w = c2w[[1,0,2,3],:] 177 | c2w[2,:] *= -1 # flip whole world upside down 178 | 179 | up += c2w[0:3,1] 180 | 181 | poses.append(c2w) 182 | colors.append('blue') 183 | 184 | fin.close() 185 | 186 | up = up / np.linalg.norm(up) 187 | up_rot = rotmat(up,[0,0,1]) # rotate up vector to [0,0,1] 188 | up_rot = np.pad(up_rot,[0,1]) 189 | up_rot[-1, -1] = 1 190 | 191 | for i in range(0, len(poses)): 192 | poses[i] = np.matmul(up_rot, poses[i]) 193 | 194 | return poses, legends, colors, image_paths 195 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | from PIL import Image 4 | 5 | 6 | def load_image(fpath, sz=256): 7 | img = Image.open(fpath) 8 | img = img.resize((sz, sz)) 9 | return np.asarray(img)[:, :, :3] 10 | 11 | 12 | def spherical_to_cartesian(sph): 13 | 14 | theta, azimuth, radius = sph 15 | 16 | return np.array([ 17 | radius * np.sin(theta) * np.cos(azimuth), 18 | radius * np.sin(theta) * np.sin(azimuth), 19 | radius * np.cos(theta), 20 | ]) 21 | 22 | 23 | def cartesian_to_spherical(xyz): 24 | 25 | xy = xyz[0]**2 + xyz[1]**2 26 | radius = np.sqrt(xy + xyz[2]**2) 27 | theta = np.arctan2(np.sqrt(xy), xyz[2]) 28 | azimuth = np.arctan2(xyz[1], xyz[0]) 29 | 30 | return np.array([theta, azimuth, radius]) 31 | 32 | 33 | def elu_to_c2w(eye, lookat, up): 34 | 35 | if isinstance(eye, list): 36 | eye = np.array(eye) 37 | if isinstance(lookat, list): 38 | lookat = np.array(lookat) 39 | if isinstance(up, list): 40 | up = np.array(up) 41 | 42 | l = eye - lookat 43 | if np.linalg.norm(l) < 1e-8: 44 | l[-1] = 1 45 | l = l / np.linalg.norm(l) 46 | 47 | s = np.cross(l, up) 48 | if np.linalg.norm(s) < 1e-8: 49 | s[0] = 1 50 | s = s / np.linalg.norm(s) 51 | uu = np.cross(s, l) 52 | 53 | rot = np.eye(3) 54 | rot[0, :] = -s 55 | rot[1, :] = uu 56 | rot[2, :] = l 57 | 58 | c2w = np.eye(4) 59 | c2w[:3, :3] = rot.T 60 | c2w[:3, 3] = eye 61 | 62 | return c2w 63 | 64 | 65 | def c2w_to_elu(c2w): 66 | 67 | w2c = np.linalg.inv(c2w) 68 | eye = c2w[:3, 3] 69 | lookat_dir = -w2c[2, :3] 70 | lookat = eye + lookat_dir 71 | up = w2c[1, :3] 72 | 73 | return eye, lookat, up 74 | 75 | 76 | def qvec_to_rotmat(qvec): 77 | return np.array([ 78 | [ 79 | 1 - 2 * qvec[2]**2 - 2 * qvec[3]**2, 80 | 2 * qvec[1] * qvec[2] - 2 * qvec[0] * qvec[3], 81 | 2 * qvec[3] * qvec[1] + 2 * qvec[0] * qvec[2] 82 | ], [ 83 | 2 * qvec[1] * qvec[2] + 2 * qvec[0] * qvec[3], 84 | 1 - 2 * qvec[1]**2 - 2 * qvec[3]**2, 85 | 2 * qvec[2] * qvec[3] - 2 * qvec[0] * qvec[1] 86 | ], [ 87 | 2 * qvec[3] * qvec[1] - 2 * qvec[0] * qvec[2], 88 | 2 * qvec[2] * qvec[3] + 2 * qvec[0] * qvec[1], 89 | 1 - 2 * qvec[1]**2 - 2 * qvec[2]**2 90 | ] 91 | ]) 92 | 93 | 94 | def rotmat(a, b): 95 | a, b = a / np.linalg.norm(a), b / np.linalg.norm(b) 96 | v = np.cross(a, b) 97 | c = np.dot(a, b) 98 | # handle exception for the opposite direction input 99 | if c < -1 + 1e-10: 100 | return rotmat(a + np.random.uniform(-1e-2, 1e-2, 3), b) 101 | s = np.linalg.norm(v) 102 | kmat = np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]]) 103 | return np.eye(3) + kmat + kmat.dot(kmat) * ((1 - c) / (s ** 2 + 1e-10)) 104 | 105 | 106 | def recenter_cameras(c2ws): 107 | 108 | is_list = False 109 | if isinstance(c2ws, list): 110 | is_list = True 111 | c2ws = np.stack(c2ws) 112 | 113 | center = c2ws[..., :3, -1].mean(axis=0) 114 | c2ws[..., :3, -1] = c2ws[..., :3, -1] - center 115 | 116 | if is_list: 117 | c2ws = [ c2w for c2w in c2ws ] 118 | 119 | return c2ws 120 | 121 | 122 | def rescale_cameras(c2ws, scale): 123 | 124 | is_list = False 125 | if isinstance(c2ws, list): 126 | is_list = True 127 | c2ws = np.stack(c2ws) 128 | 129 | c2ws[..., :3, -1] *= scale 130 | 131 | if is_list: 132 | c2ws = [ c2w for c2w in c2ws ] 133 | 134 | return c2ws 135 | -------------------------------------------------------------------------------- /src/visualizer.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from PIL import Image 4 | import plotly.graph_objects as go 5 | import numpy as np 6 | 7 | 8 | def calc_cam_cone_pts_3d(c2w, fov_deg, zoom = 1.0): 9 | 10 | fov_rad = np.deg2rad(fov_deg) 11 | 12 | cam_x = c2w[0, -1] 13 | cam_y = c2w[1, -1] 14 | cam_z = c2w[2, -1] 15 | 16 | corn1 = [np.tan(fov_rad / 2.0), np.tan(fov_rad / 2.0), -1.0] 17 | corn2 = [-np.tan(fov_rad / 2.0), np.tan(fov_rad / 2.0), -1.0] 18 | corn3 = [-np.tan(fov_rad / 2.0), -np.tan(fov_rad / 2.0), -1.0] 19 | corn4 = [np.tan(fov_rad / 2.0), -np.tan(fov_rad / 2.0), -1.0] 20 | corn5 = [0, np.tan(fov_rad / 2.0), -1.0] 21 | 22 | corn1 = np.dot(c2w[:3, :3], corn1) 23 | corn2 = np.dot(c2w[:3, :3], corn2) 24 | corn3 = np.dot(c2w[:3, :3], corn3) 25 | corn4 = np.dot(c2w[:3, :3], corn4) 26 | corn5 = np.dot(c2w[:3, :3], corn5) 27 | 28 | # Now attach as offset to actual 3D camera position: 29 | corn1 = np.array(corn1) / np.linalg.norm(corn1, ord=2) * zoom 30 | corn_x1 = cam_x + corn1[0] 31 | corn_y1 = cam_y + corn1[1] 32 | corn_z1 = cam_z + corn1[2] 33 | corn2 = np.array(corn2) / np.linalg.norm(corn2, ord=2) * zoom 34 | corn_x2 = cam_x + corn2[0] 35 | corn_y2 = cam_y + corn2[1] 36 | corn_z2 = cam_z + corn2[2] 37 | corn3 = np.array(corn3) / np.linalg.norm(corn3, ord=2) * zoom 38 | corn_x3 = cam_x + corn3[0] 39 | corn_y3 = cam_y + corn3[1] 40 | corn_z3 = cam_z + corn3[2] 41 | corn4 = np.array(corn4) / np.linalg.norm(corn4, ord=2) * zoom 42 | corn_x4 = cam_x + corn4[0] 43 | corn_y4 = cam_y + corn4[1] 44 | corn_z4 = cam_z + corn4[2] 45 | corn5 = np.array(corn5) / np.linalg.norm(corn5, ord=2) * zoom 46 | corn_x5 = cam_x + corn5[0] 47 | corn_y5 = cam_y + corn5[1] 48 | corn_z5 = cam_z + corn5[2] 49 | 50 | xs = [cam_x, corn_x1, corn_x2, corn_x3, corn_x4, corn_x5] 51 | ys = [cam_y, corn_y1, corn_y2, corn_y3, corn_y4, corn_y5] 52 | zs = [cam_z, corn_z1, corn_z2, corn_z3, corn_z4, corn_z5] 53 | 54 | return np.array([xs, ys, zs]).T 55 | 56 | 57 | class CameraVisualizer: 58 | 59 | def __init__(self, poses, legends, colors, images=None, mesh_path=None, camera_x=1.0): 60 | self._fig = None 61 | 62 | self._camera_x = camera_x 63 | 64 | self._poses = poses 65 | self._legends = legends 66 | self._colors = colors 67 | 68 | self._raw_images = None 69 | self._bit_images = None 70 | self._image_colorscale = None 71 | 72 | if images is not None: 73 | self._raw_images = images 74 | self._bit_images = [] 75 | self._image_colorscale = [] 76 | for img in images: 77 | if img is None: 78 | self._bit_images.append(None) 79 | self._image_colorscale.append(None) 80 | continue 81 | 82 | bit_img, colorscale = self.encode_image(img) 83 | self._bit_images.append(bit_img) 84 | self._image_colorscale.append(colorscale) 85 | 86 | self._mesh = None 87 | if mesh_path is not None and os.path.exists(mesh_path): 88 | import trimesh 89 | self._mesh = trimesh.load(mesh_path, force='mesh') 90 | 91 | 92 | def encode_image(self, raw_image): 93 | ''' 94 | :param raw_image (H, W, 3) array of uint8 in [0, 255]. 95 | ''' 96 | # https://stackoverflow.com/questions/60685749/python-plotly-how-to-add-an-image-to-a-3d-scatter-plot 97 | 98 | dum_img = Image.fromarray(np.ones((3, 3, 3), dtype='uint8')).convert('P', palette='WEB') 99 | idx_to_color = np.array(dum_img.getpalette()).reshape((-1, 3)) 100 | 101 | bit_image = Image.fromarray(raw_image).convert('P', palette='WEB', dither=None) 102 | # bit_image = Image.fromarray(raw_image.clip(0, 254)).convert( 103 | # 'P', palette='WEB', dither=None) 104 | colorscale = [ 105 | [i / 255.0, 'rgb({}, {}, {})'.format(*rgb)] for i, rgb in enumerate(idx_to_color)] 106 | 107 | return bit_image, colorscale 108 | 109 | 110 | def update_figure( 111 | self, scene_bounds, 112 | base_radius=0.0, zoom_scale=1.0, fov_deg=50., 113 | mesh_z_shift=0.0, mesh_scale=1.0, 114 | show_background=False, show_grid=False, show_ticklabels=False, y_up=False 115 | ): 116 | 117 | fig = go.Figure() 118 | 119 | if self._mesh is not None: 120 | fig.add_trace( 121 | go.Mesh3d( 122 | x=self._mesh.vertices[:, 0] * mesh_scale, 123 | y=self._mesh.vertices[:, 2] * -mesh_scale, 124 | z=(self._mesh.vertices[:, 1] + mesh_z_shift) * mesh_scale, 125 | i=self._mesh.faces[:, 0], 126 | j=self._mesh.faces[:, 1], 127 | k=self._mesh.faces[:, 2], 128 | color=None, 129 | facecolor=None, 130 | opacity=0.8, 131 | lighting={'ambient': 1}, 132 | ) 133 | ) 134 | 135 | for i in range(len(self._poses)): 136 | 137 | pose = self._poses[i] 138 | clr = self._colors[i] 139 | legend = self._legends[i] 140 | 141 | edges = [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (2, 3), (3, 4), (4, 1), (0, 5)] 142 | 143 | cone = calc_cam_cone_pts_3d(pose, fov_deg) 144 | radius = np.linalg.norm(pose[:3, -1]) 145 | 146 | if self._bit_images and self._bit_images[i]: 147 | 148 | raw_image = self._raw_images[i] 149 | bit_image = self._bit_images[i] 150 | colorscale = self._image_colorscale[i] 151 | 152 | (H, W, C) = raw_image.shape 153 | 154 | z = np.zeros((H, W)) + base_radius 155 | (x, y) = np.meshgrid(np.linspace(-1.0 * self._camera_x, 1.0 * self._camera_x, W), np.linspace(1.0, -1.0, H) * H / W) 156 | 157 | xyz = np.concatenate([x[..., None], y[..., None], z[..., None]], axis=-1) 158 | 159 | rot_xyz = np.matmul(xyz, pose[:3, :3].T) + pose[:3, -1] 160 | 161 | x, y, z = rot_xyz[:, :, 0], rot_xyz[:, :, 1], rot_xyz[:, :, 2] 162 | 163 | fig.add_trace(go.Surface( 164 | x=x, y=y, z=z, 165 | surfacecolor=bit_image, 166 | cmin=0, 167 | cmax=255, 168 | colorscale=colorscale, 169 | showscale=False, 170 | lighting_diffuse=1.0, 171 | lighting_ambient=1.0, 172 | lighting_fresnel=1.0, 173 | lighting_roughness=1.0, 174 | lighting_specular=0.3)) 175 | 176 | for (i, edge) in enumerate(edges): 177 | (x1, x2) = (cone[edge[0], 0], cone[edge[1], 0]) 178 | (y1, y2) = (cone[edge[0], 1], cone[edge[1], 1]) 179 | (z1, z2) = (cone[edge[0], 2], cone[edge[1], 2]) 180 | fig.add_trace(go.Scatter3d( 181 | x=[x1, x2], y=[y1, y2], z=[z1, z2], mode='lines', 182 | line=dict(color=clr, width=3), 183 | name=legend, showlegend=(i == 0))) 184 | 185 | # Add label. 186 | if cone[0, 2] < 0: 187 | fig.add_trace(go.Scatter3d( 188 | x=[cone[0, 0]], y=[cone[0, 1]], z=[cone[0, 2] - 0.05], showlegend=False, 189 | mode='text', text=legend, textposition='bottom center')) 190 | else: 191 | fig.add_trace(go.Scatter3d( 192 | x=[cone[0, 0]], y=[cone[0, 1]], z=[cone[0, 2] + 0.05], showlegend=False, 193 | mode='text', text=legend, textposition='top center')) 194 | 195 | # look at the center of scene 196 | fig.update_layout( 197 | height=720, 198 | autosize=True, 199 | hovermode=False, 200 | margin=go.layout.Margin(l=0, r=0, b=0, t=0), 201 | showlegend=True, 202 | legend=dict( 203 | yanchor='bottom', 204 | y=0.01, 205 | xanchor='right', 206 | x=0.99, 207 | ), 208 | scene=dict( 209 | aspectmode='manual', 210 | aspectratio=dict(x=1, y=1, z=1), 211 | camera=dict( 212 | eye=dict(x=1.5, y=1.5, z=1.0), 213 | center=dict(x=0.0, y=0.0, z=0.0), 214 | up=dict(x=0.0, y=0.0, z=1.0)), 215 | xaxis_title='X', 216 | yaxis_title='Z' if y_up else 'Y', 217 | zaxis_title='Y' if y_up else 'Z', 218 | xaxis=dict( 219 | range=[-scene_bounds, scene_bounds], 220 | showticklabels=show_ticklabels, 221 | showgrid=show_grid, 222 | zeroline=False, 223 | showbackground=show_background, 224 | showspikes=False, 225 | showline=False, 226 | ticks=''), 227 | yaxis=dict( 228 | range=[-scene_bounds, scene_bounds], 229 | showticklabels=show_ticklabels, 230 | showgrid=show_grid, 231 | zeroline=False, 232 | showbackground=show_background, 233 | showspikes=False, 234 | showline=False, 235 | ticks=''), 236 | zaxis=dict( 237 | range=[-scene_bounds, scene_bounds], 238 | showticklabels=show_ticklabels, 239 | showgrid=show_grid, 240 | zeroline=False, 241 | showbackground=show_background, 242 | showspikes=False, 243 | showline=False, 244 | ticks='') 245 | ) 246 | ) 247 | 248 | self._fig = fig 249 | return fig 250 | --------------------------------------------------------------------------------