diff --git a/.dockerignore b/.dockerignore index e1590ed27..92619bde0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,7 +12,7 @@ build/ develop-eggs/ dist/ downloads/ -!scripts/downloads/ +!nerfstudio/scripts/downloads/ eggs/ .eggs/ lib/ diff --git a/.github/workflows/core_code_checks.yml b/.github/workflows/core_code_checks.yml index 86ff5ccca..3e30fba7d 100644 --- a/.github/workflows/core_code_checks.yml +++ b/.github/workflows/core_code_checks.yml @@ -15,10 +15,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7.13 + - name: Set up Python 3.8.13 uses: actions/setup-python@v4 with: - python-version: '3.7.13' + python-version: '3.8.13' - uses: actions/cache@v2 with: path: ${{ env.pythonLocation }} @@ -28,17 +28,17 @@ jobs: pip install --upgrade --upgrade-strategy eager -e .[dev] - name: Run license checks run: | - ./scripts/licensing/license_headers.sh --check + ./nerfstudio/scripts/licensing/license_headers.sh --check - name: Check notebook cell metadata run: | - python ./scripts/docs/add_nb_tags.py --check + python ./nerfstudio/scripts/docs/add_nb_tags.py --check - name: Run isort - run: isort docs/ nerfstudio/ scripts/ tests/ --profile black --check + run: isort docs/ nerfstudio/ tests/ --profile black --check - name: Run Black - run: black docs/ nerfstudio/ scripts/ tests/ --check + run: black docs/ nerfstudio/ tests/ --check - name: Python Pylint run: | - pylint nerfstudio tests scripts + pylint nerfstudio tests - name: Test with pytest run: | pytest diff --git a/.gitignore b/.gitignore index ab8a4e108..7f1688d59 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ build/ develop-eggs/ dist/ downloads/ -!scripts/downloads/ +!nerfstudio/scripts/downloads/ eggs/ .eggs/ lib/ @@ -171,6 +171,7 @@ events.out.* # Data data !*/data +!docs/reference/api/data/ # Misc old/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 118282928..8719fd8a1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -30,7 +30,7 @@ "name": "Python: train", "type": "python", "request": "launch", - "program": "scripts/train.py", + "program": "nerfstudio/scripts/train.py", "console": "integratedTerminal", "args": [ "instant_ngp", @@ -42,7 +42,7 @@ "name": "Python: train ngp", "type": "python", "request": "launch", - "program": "scripts/train.py", + "program": "nerfstudio/scripts/train.py", "console": "integratedTerminal", "args": ["instant_ngp"] }, @@ -50,7 +50,7 @@ "name": "Python: train ngp nerfstudio", "type": "python", "request": "launch", - "program": "scripts/train.py", + "program": "nerfstudio/scripts/train.py", "console": "integratedTerminal", "args": [ "instant_ngp", @@ -70,7 +70,7 @@ "name": "Python: train ngp-friends", "type": "python", "request": "launch", - "program": "scripts/train.py", + "program": "nerfstudio/scripts/train.py", "console": "integratedTerminal", "args": [ "instant_ngp", @@ -89,7 +89,7 @@ "name": "Python: train semantic_nerfw", "type": "python", "request": "launch", - "program": "scripts/train.py", + "program": "nerfstudio/scripts/train.py", "console": "integratedTerminal", "args": ["semantic_nerf"] }, @@ -97,10 +97,17 @@ "name": "Python: NeuS on Replica", "type": "python", "request": "launch", +<<<<<<< HEAD "program": "scripts/train.py", "console": "integratedTerminal", "justMyCode": false, "env": {"CUDA_VISIBLE_DEVICES":"0"}, +======= + "program": "nerfstudio/scripts/train.py", + "console": "integratedTerminal", + "justMyCode": false, + "env": { "CUDA_VISIBLE_DEVICES": "0" }, +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb "args": [ "neus", "--pipeline.model.sdf-field.inside-outside=True", @@ -112,5 +119,45 @@ "--include_mono_prior=True" ] }, +<<<<<<< HEAD +======= + { + "name": "Python: NeuS-facto on Replica", + "type": "python", + "request": "launch", + "program": "nerfstudio/scripts/train.py", + "console": "integratedTerminal", + "justMyCode": false, + "env": { "CUDA_VISIBLE_DEVICES": "0" }, + "args": [ + "neus-facto", + "--pipeline.model.sdf-field.inside-outside=True", + "--pipeline.model.mono-depth-loss-mult=0.1", + "--pipeline.model.mono-normal-loss-mult=0.05", + "--vis=wandb", + "sdfstudio-data", + "--data=data/sdfstudio-demo-data/replica-room0", + "--include_mono_prior=True" + ] + }, + { + "name": "Python: NeuS-facto on DTU (no mono-prior)", + "type": "python", + "request": "launch", + "program": "nerfstudio/scripts/train.py", + "console": "integratedTerminal", + "justMyCode": false, + "env": { "CUDA_VISIBLE_DEVICES": "0" }, + "args": [ + "neus-facto", + "--pipeline.model.sdf-field.inside-outside=False", + "--vis=wandb", + "sdfstudio-data", + "--data=data/sdfstudio-demo-data/dtu-scan65", + "--include_mono_prior=False", + "--auto-orient=True" + ] + } +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 5e3f61611..90f5ae01c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -121,5 +121,6 @@ "__node_handle": "cpp" }, "python.analysis.typeCheckingMode": "basic", - "python.analysis.diagnosticMode": "workspace" + "python.analysis.diagnosticMode": "workspace", + "eslint.packageManager": "yarn" } diff --git a/README.md b/README.md index 8730cb20a..2ce5b3b07 100644 --- a/README.md +++ b/README.md @@ -59,12 +59,14 @@ # About +_It’s as simple as plug and play with nerfstudio!_ + Nerfstudio provides a simple API that allows for a simplified end-to-end process of creating, training, and testing NeRFs. The library supports a **more interpretable implementation of NeRFs by modularizing each component.** With more modular NeRFs, we hope to create a more user-friendly experience in exploring the technology. -Nerfstudio is a contributor-friendly repo with the goal of building a community where users can more easily build upon each other's contributions. -It’s as simple as plug and play with nerfstudio! +This is a contributor-friendly repo with the goal of building a community where users can more easily build upon each other's contributions. +Nerfstudio initially launched as an opensource project by Berkeley students in [KAIR lab](https://people.eecs.berkeley.edu/~kanazawa/index.html#kair) at [Berkeley AI Research (BAIR)](https://bair.berkeley.edu/) in October 2022 as a part of a research project ([paper](https://arxiv.org/abs/2302.04264)). It is currently developed by Berkeley students and community contributors. We are committed to providing learning resources to help you understand the basics of (if you're just getting started), and keep up-to-date with (if you're a seasoned veteran) all things NeRF. As researchers, we know just how hard it is to get onboarded with this next-gen technology. So we're here to help with tutorials, documentation, and more! @@ -85,7 +87,7 @@ You must have an NVIDIA video card with CUDA installed on the system. This libra ### Create environment -Nerfstudio requires `python >= 3.7`. We recommend using conda to manage dependencies. Make sure to install [Conda](https://docs.conda.io/en/latest/miniconda.html) before proceeding. +Nerfstudio requires `python >= 3.8`. We recommend using conda to manage dependencies. Make sure to install [Conda](https://docs.conda.io/en/latest/miniconda.html) before proceeding. ```bash conda create --name nerfstudio -y python=3.8 @@ -101,14 +103,14 @@ For CUDA 11.3: ```bash pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 -f https://download.pytorch.org/whl/torch_stable.html -pip install git+https://github.com/NVlabs/tiny-cuda-nn/#subdirectory=bindings/torch +pip install ninja git+https://github.com/NVlabs/tiny-cuda-nn/#subdirectory=bindings/torch ``` For CUDA 11.7: ```bash -pip install torch torchvision functorch --extra-index-url https://download.pytorch.org/whl/cu117 -pip install git+https://github.com/NVlabs/tiny-cuda-nn/#subdirectory=bindings/torch +pip install torch==1.13.1 torchvision functorch --extra-index-url https://download.pytorch.org/whl/cu117 +pip install ninja git+https://github.com/NVlabs/tiny-cuda-nn/#subdirectory=bindings/torch ``` See [Dependencies](https://github.com/nerfstudio-project/nerfstudio/blob/main/docs/quickstart/installation.md#dependencies) @@ -166,7 +168,13 @@ It is possible to load a pretrained model by running ns-train nerfacto --data data/nerfstudio/poster --load-dir {outputs/.../nerfstudio_models} ``` -This will automatically start training. If you do not want it to train, add `--viewer.start-train False` to your training command. +## Visualize existing run + +Given a pretrained model checkpoint, you can start the viewer by running + +```bash +ns-viewer --load-config {outputs/.../config.yml} +``` ## 3. Exporting Results @@ -196,6 +204,7 @@ ns-export pointcloud --help Using an existing dataset is great, but likely you want to use your own data! We support various methods for using your own data. Before it can be used in nerfstudio, the camera location and orientations must be determined and then converted into our format using `ns-process-data`. We rely on external tools for this, instructions and information can be found in the documentation. +<<<<<<< HEAD | Data | Capture Device | Requirements | `ns-process-data` Speed | | ------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------- | ----------------------- | | πŸ“· [Images](https://docs.nerf.studio/en/latest/quickstart/custom_dataset.html#images-and-video) | Any | [COLMAP](https://colmap.github.io/install.html) | 🐒 | @@ -206,6 +215,19 @@ Using an existing dataset is great, but likely you want to use your own data! We | πŸ“± [Record3D](https://docs.nerf.studio/en/latest/quickstart/custom_dataset.html#record3d-capture) | IOS with LiDAR | [Record3D app](https://record3d.app/) | πŸ‡ | | πŸ–₯ [Metashape](https://docs.nerf.studio/en/latest/quickstart/custom_dataset.html#metashape) | Any | [Metashape](https://www.agisoft.com/) | πŸ‡ | | πŸ›  [Custom](https://docs.nerf.studio/en/latest/quickstart/data_conventions.html) | Any | Camera Poses | πŸ‡ | +======= +| Data | Capture Device | Requirements | `ns-process-data` Speed | +| ---------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------- | ----------------------- | +| πŸ“· [Images](https://docs.nerf.studio/en/latest/quickstart/custom_dataset.html#images-and-video) | Any | [COLMAP](https://colmap.github.io/install.html) | 🐒 | +| πŸ“Ή [Video](https://docs.nerf.studio/en/latest/quickstart/custom_dataset.html#images-and-video) | Any | [COLMAP](https://colmap.github.io/install.html) | 🐒 | +| 🌎 [360 Data](https://docs.nerf.studio/en/latest/quickstart/custom_dataset.html#360_data) | Any | [COLMAP](https://colmap.github.io/install.html) | 🐒 | +| πŸ“± [Polycam](https://docs.nerf.studio/en/latest/quickstart/custom_dataset.html#polycam-capture) | IOS with LiDAR | [Polycam App](https://poly.cam/) | πŸ‡ | +| πŸ“± [KIRI Engine](https://docs.nerf.studio/en/latest/quickstart/custom_dataset.html#kiri-capture) | IOS or Android | [KIRI Engine App](https://www.kiriengine.com/) | πŸ‡ | +| πŸ“± [Record3D](https://docs.nerf.studio/en/latest/quickstart/custom_dataset.html#record3d-capture) | IOS with LiDAR | [Record3D app](https://record3d.app/) | πŸ‡ | +| πŸ–₯ [Metashape](https://docs.nerf.studio/en/latest/quickstart/custom_dataset.html#metashape) | Any | [Metashape](https://www.agisoft.com/) | πŸ‡ | +| πŸ–₯ [RealityCapture](https://docs.nerf.studio/en/latest/quickstart/custom_dataset.html#realitycapture) | Any | [RealityCapture](https://www.capturingreality.com/realitycapture) | πŸ‡ | +| πŸ›  [Custom](https://docs.nerf.studio/en/latest/quickstart/data_conventions.html) | Any | Camera Poses | πŸ‡ | +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb ## 5. Advanced Options @@ -227,9 +249,9 @@ Each model contains many parameters that can be changed, too many to list here. ns-train nerfacto --help ``` -### Tensorboard / WandB +### Tensorboard / WandB / Viewer -We support three different methods to track training progress, using the viewer, [tensorboard](https://www.tensorflow.org/tensorboard), and [Weights and Biases](https://wandb.ai/site). You can specify which visualizer to use by appending `--vis {viewer, tensorboard, wandb}` to the training command. Note that only one may be used at a time. Additionally the viewer only works for methods that are fast (ie. nerfacto, instant-ngp), for slower methods like NeRF, use the other loggers. +We support three different methods to track training progress, using the viewer, [tensorboard](https://www.tensorflow.org/tensorboard), and [Weights and Biases](https://wandb.ai/site). You can specify which visualizer to use by appending `--vis {viewer, tensorboard, wandb, viewer+wandb, viewer+tensorboard}` to the training command. Simultaneously utilizing the viewer alongside wandb or tensorboard may cause stuttering issues during evaluation steps. The viewer only works for methods that are fast (ie. nerfacto, instant-ngp), for slower methods like NeRF, use the other loggers. # Learn More diff --git a/colab/demo.ipynb b/colab/demo.ipynb index 4a87f1718..0bce8b064 100644 --- a/colab/demo.ipynb +++ b/colab/demo.ipynb @@ -7,7 +7,11 @@ "colab_type": "text" }, "source": [ +<<<<<<< HEAD "\"Open" +======= + "\"Open" +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb ] }, { @@ -329,7 +333,7 @@ "#@markdown
The rendered video should be at renders/output.mp4!
\n", "\n", "\n", - "base_dir = \"/content/outputs/data-nerfstudio-\" + scene + \"/nerfacto/\"\n", + "base_dir = \"/content/outputs/unnamed/nerfacto/\"\n", "training_run_dir = base_dir + os.listdir(base_dir)[0]\n", "\n", "from IPython.core.display import display, HTML\n", diff --git a/docs/conf.py b/docs/conf.py index 2071057b9..824a61d78 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,6 +42,7 @@ extensions = [ "sphinx_design", "sphinx.ext.mathjax", "sphinxext.opengraph", + "sphinx.ext.viewcode", ] # List of patterns, relative to source directory, that match files and diff --git a/docs/developer_guides/config.md b/docs/developer_guides/config.md index 29d093f9d..bf216278f 100644 --- a/docs/developer_guides/config.md +++ b/docs/developer_guides/config.md @@ -118,33 +118,3 @@ Often times, you just want to play with the parameters of an existing model with # NOTE: the dataparser and associated configurations go at the end of the command ns-train {METHOD_NAME} --vis viewer {DATA_PARSER} --scale-factor 0.5 ``` - -### Extending Nerfstudio with custom methods -In order to extend the Nerfstudio and register your own methods, you can package your code as a python package -and register it with Nerfstudio as a `nerfstudio.method_configs` entrypoint in the `pyproject.toml` file. -The Nerfstudio will automatically look for all registered methods and will register them to be used -by method such as `ns-train`. - -Here is an example: -```python -"""my_package/my_config.py""" - -from nerfstudio.engine.trainer import TrainerConfig -from nerfstudio.plugins.types import MethodSpecification - -MyMethod = MethodSpecification( - config=TrainerConfig( - method_name="my-method", - ... - ), - description="Custom description" -) - -"""pyproject.toml""" -[project] -name = "my_package" -... - -[project.entry-points.'nerfstudio.method_configs'] -my-method = 'my_package.my_config:MyMethod' -``` \ No newline at end of file diff --git a/docs/developer_guides/debugging_tools/benchmarking.md b/docs/developer_guides/debugging_tools/benchmarking.md index 5e213fb1b..b38c09277 100644 --- a/docs/developer_guides/debugging_tools/benchmarking.md +++ b/docs/developer_guides/debugging_tools/benchmarking.md @@ -9,7 +9,7 @@ To launch training jobs automatically on each of these items, you can call: ```bash -./scripts/benchmarking/launch_train_blender.sh -m {METHOD_NAME} [-s] [-v {VIS}] [{GPU_LIST}] +./nerfstudio/scripts/benchmarking/launch_train_blender.sh -m {METHOD_NAME} [-s] [-v {VIS}] [{GPU_LIST}] ``` Simply replace the arguments in brackets with the correct arguments. @@ -18,31 +18,31 @@ Simply replace the arguments in brackets with the correct arguments. - `-s`: Launch a single job per GPU. - `-v {VIS}`: Use another visualization than wandb, which is the default. Only other option is tensorboard. - `{GPU_LIST}`: (optional) Specify the list of gpus you want to use on your machine space separated. for instance, if you want to use GPU's 0-3, you will need to pass in `0 1 2 3`. If left empty, the script will automatically find available GPU's and distribute training jobs on the available GPUs. - + :::{admonition} Tip :class: info -To view all the arguments and annotations, you can run `./scripts/benchmarking/launch_train_blender.sh --help` - ::: - +To view all the arguments and annotations, you can run `./nerfstudio/scripts/benchmarking/launch_train_blender.sh --help` +::: A full example would be: - Specifying gpus - ```bash - ./scripts/benchmarking/launch_train_blender.sh -m nerfacto 0 1 2 3 - ``` + + ```bash + ./nerfstudio/scripts/benchmarking/launch_train_blender.sh -m nerfacto 0 1 2 3 + ``` - Automatically find available gpus - ```bash - ./scripts/benchmarking/launch_train_blender.sh -m nerfacto - ``` + ```bash + ./nerfstudio/scripts/benchmarking/launch_train_blender.sh -m nerfacto + ``` The script will automatically launch training on all of the items and save the checkpoints in an output directory with the experiment name and current timestamp. ## Evaluating trained Blender models -Once you have launched training, and training converges, you can test your method with `scripts/benchmarking/launch_eval_blender.sh`. +Once you have launched training, and training converges, you can test your method with `nerfstudio/scripts/benchmarking/launch_eval_blender.sh`. Say we ran a benchmark on 08-10-2022 for `instant-ngp`. By default, the train script will save the benchmarks in the following format: @@ -65,7 +65,7 @@ If we wanted to run the benchmark on all the blender data for the above example, ```bash -./scripts/benchmarking/launch_eval_blender.sh -m instant-ngp -o outputs/ -t 2022-08-10_172517 [{GPU_LIST}] +./nerfstudio/scripts/benchmarking/launch_eval_blender.sh -m instant-ngp -o outputs/ -t 2022-08-10_172517 [{GPU_LIST}] ``` The flags used in the benchmarking script are defined as follows: @@ -90,4 +90,4 @@ outputs :class: warning Since we are running multiple backgrounded processes concurrently with this script, please note the terminal logs may be messy. - ::: +::: diff --git a/docs/developer_guides/debugging_tools/profiling.md b/docs/developer_guides/debugging_tools/profiling.md index ed8796cec..aabada5f4 100644 --- a/docs/developer_guides/debugging_tools/profiling.md +++ b/docs/developer_guides/debugging_tools/profiling.md @@ -23,7 +23,24 @@ class RayGenerator(nn.Module): ... ``` -At termination of training or end of the training run, the profiler will print out the average execution time for all of the functions that have the profiler tag. +Alternatively, you can also time parts of the code: +```python +... + +def forward(self, ray_indices: TensorType["num_rays", 3]) -> RayBundle: + # implementation here + with profiler.time_function("code1"): + # some code here + ... + + with profiler.time_function("code2"): + # some code here + ... + ... +``` + + +At termination of training or end of the training run, the profiler will print out the average execution time for all of the functions or code blocks that have the profiler tag. :::{admonition} Tip :class: info @@ -32,6 +49,14 @@ Use this profiler if there are *specific/individual functions* you want to measu ::: +#### Profiling with PyTorch profiler + +If you want to profile the training or evaluation code and track the memory and CUDA kernel launches, consider using [PyTorch profiler](https://pytorch.org/tutorials/recipes/recipes/profiler_recipe.html). +It will run the profiler for some selected step numbers, once with `CUDA_LAUNCH_BLOCKING=1`, once with `CUDA_LAUNCH_BLOCKING=0`. +The PyTorch profiler can be enabled with `--logging.profiler=pytorch` flag. +The outputs of the profiler are trace files stored in `{PATH_TO_MODEL_OUTPUT}/profiler_traces`, and can be loaded in Google Chrome by typing `chrome://tracing`. + + #### Profiling with PySpy If you want to profile the entire codebase, consider using [PySpy](https://github.com/benfred/py-spy). diff --git a/docs/developer_guides/new_methods.md b/docs/developer_guides/new_methods.md new file mode 100644 index 000000000..baafad440 --- /dev/null +++ b/docs/developer_guides/new_methods.md @@ -0,0 +1,89 @@ +# Adding a New Method + +Nerfstudio aims to offer researchers a codebase that they can utilize to extend and develop novel methods. Our vision is for users to establish a distinct repository that imports nerfstudio and overrides pipeline components to cater to specific functionality requirements of the new approach. If any of the new features require modifications to the core nerfstudio repository and can be generally useful, we encourage you to submit a PR to enable others to benefit from it. + +Examples are often the best way to learn, take a look at the [LERF](https://github.com/kerrj/lerf) repository for good example of how to use nerfstudio in your projects. + +## File Structure + +We recommend the following file structure: + +``` +β”œβ”€β”€ my_method +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ my_config.py +β”‚ β”œβ”€β”€ custom_pipeline.py [optional] +β”‚ β”œβ”€β”€ custom_model.py [optional] +β”‚ β”œβ”€β”€ custom_field.py [optional] +β”‚ β”œβ”€β”€ custom_datamanger.py [optional] +β”‚ β”œβ”€β”€ ... +β”œβ”€β”€ pyproject.toml +``` + +## Registering custom model with nerfstudio + +In order to extend the Nerfstudio and register your own methods, you can package your code as a python package +and register it with Nerfstudio as a `nerfstudio.method_configs` entrypoint in the `pyproject.toml` file. +Nerfstudio will automatically look for all registered methods and will register them to be used +by methods such as `ns-train`. + +First create a config file: + +```python +"""my_method/my_config.py""" + +from nerfstudio.engine.trainer import TrainerConfig +from nerfstudio.plugins.types import MethodSpecification + +MyMethod = MethodSpecification( + config=TrainerConfig( + method_name="my-method", + pipeline=... + ... + ), + description="Custom description" +) +``` + +Then create a `pyproject.toml` file. This is where the entrypoint to your method is set and also where you can specify additional dependencies required by your codebase. + +```python +"""pyproject.toml""" + +[project] +name = "my_method" + +dependencies = [ + "nerfstudio" # you may want to consider pinning the version, ie "nerfstudio==0.1.19" +] + +[tool.setuptools.packages.find] +include = ["my_method*"] + +[project.entry-points.'nerfstudio.method_configs'] +my-method = 'my_method.my_config:MyMethod' +``` + +finally run the following to register the method, + +``` +pip install -e . +``` + +When developing a new method you don't always want to install your code as a package. +Instead, you may use the `NERFSTUDIO_METHOD_CONFIGS` environment variable to temporarily register your custom method. +``` +export NERFSTUDIO_METHOD_CONFIGS="my-method=my_method.my_config:MyMethod" +``` + +## Running custom method + +After registering your method you should be able to run the method with, + +``` +ns-train my-method --data DATA_DIR +``` + +## Adding to the _nerf.studio_ documentation + +We invite researchers to contribute their own methods to our online documentation. You can find more information on how to do this {ref}`here`. diff --git a/docs/developer_guides/pipelines/dataparsers.md b/docs/developer_guides/pipelines/dataparsers.md index 3a38664f9..19b964e5a 100644 --- a/docs/developer_guides/pipelines/dataparsers.md +++ b/docs/developer_guides/pipelines/dataparsers.md @@ -30,8 +30,16 @@ class DataparserOutputs: """Color of dataset background.""" scene_box: SceneBox = SceneBox() """Scene box of dataset. Used to bound the scene or provide the scene scale depending on model.""" - semantics: Optional[Semantics] = None - """Semantics information.""" + mask_filenames: Optional[List[Path]] = None + """Filenames for any masks that are required""" + metadata: Dict[str, Any] = to_immutable_dict({}) + """Dictionary of any metadata that be required for the given experiment. + Will be processed by the InputDataset to create any additional tensors that may be required. + """ + dataparser_transform: TensorType[3, 4] = torch.eye(4)[:3, :] + """Transform applied by the dataparser.""" + dataparser_scale: float = 1.0 + """Scale applied by the dataparser.""" @dataclass class DataParser: @@ -115,48 +123,12 @@ You can also pull out information from the DataParserOutputs for other DataManga ray_generator = RayGenerator(dataparser_outputs.cameras) ``` -## Our Implementations +## Included DataParsers -Below we enumerate the various dataparsers that we have implemented in our codebase. Feel free to use ours or add your own. Also any contributions are welcome and appreciated! +```{toctree} +--- +maxdepth: 2 +--- -###### Nerfstudio - -This is our custom dataparser. We have a script to convert images or videos with COLMAP to this format. - -```{button-link} https://github.com/nerfstudio-project/nerfstudio/blob/master/nerfstudio/data/dataparsers/nerfstudio_dataparser.py -:color: primary -:outline: -See the code! -``` - -###### Blender - -We support the synthetic Blender dataset from the original NeRF paper. - -```{button-link} https://github.com/nerfstudio-project/nerfstudio/blob/master/nerfstudio/data/dataparsers/blender_dataparser.py -:color: primary -:outline: -See the code! -``` - -###### Instant NGP - -This supports the Instant NGP dataset. - -```{button-link} https://github.com/nerfstudio-project/nerfstudio/blob/master/nerfstudio/data/dataparsers/instant_ngp_dataparser.py -:color: primary -:outline: -See the code! -``` - -###### Record3D - -This dataparser can use recorded data from a >= iPhone 12 Pro using the [Record3D app](https://record3d.app/) . Record a video and export with the `EXR + JPG sequence` format. Unzip export and `rgb` folder before training. - -For more information on capturing with Record3D, see the [Custom Dataset Docs](/quickstart/custom_dataset.md). - -```{button-link} https://github.com/nerfstudio-project/nerfstudio/blob/master/nerfstudio/data/dataparsers/record3d_dataparser.py -:color: primary -:outline: -See the code! +../../reference/api/data/dataparsers ``` diff --git a/docs/developer_guides/viewer/custom_gui.md b/docs/developer_guides/viewer/custom_gui.md new file mode 100644 index 000000000..e11381d02 --- /dev/null +++ b/docs/developer_guides/viewer/custom_gui.md @@ -0,0 +1,105 @@ +# Custom GUI + +We provide support for custom viewer GUI elements that can be defined in any `nn.Module`. Although we don't have any specific use cases in mind, here are some examples of what can be achieved with this feature: + +- Using text input to modify the rendering +- Logging numerical values to the viewer +- Using checkboxes to turn off and on losses +- Using a dropdown to switch between appearances + +## Adding an Element + +To define a custom element, create an instance of one of the provided classes in `nerfstudio.viewer.server.viewer_elements`, and assign it as a class variable in your `nn.Module`. + +```python +from nerfstudio.viewer.server.viewer_elements import ViewerNumber + +class MyClass(nn.Module):#must inherit from nn.Module + def __init__(self): + # Must be a class variable + self.custom_value = ViewerNumber(name="My Value", default_value=1.0) +``` +**Element Hierarchy** +The viewer recursively searches all `nn.Module` children of the base `Pipeline` object, and arranges parameters into folders based on their variable names. +For example, a `ViewerElement` defined in `pipeline.model.field` will be in the "Custom/model/field" folder in the GUI. + +**Reading the value** +To read the value of a custom element, simply access its `value` attribute. In this case it will be `1.0` unless modified by the user in the viewer. + +```python +current_value = self.custom_value.value +``` + +**Callbacks** +You can register a callback that will be called whenever a new value for your GUI element is available. For example, one can use a callback to update config parameters when elements are changed: +```python +def on_change_callback(handle: ViewerCheckbox) -> None: + self.config.example_parameter = handle.value + +self.custom_checkbox = ViewerCheckbox( + name="Checkbox", + default_value=False, + cb_hook=on_change_callback, +) +``` + +**Thread safety** +Note that `ViewerElement` values can change asynchronously to model execution. So, it's best practice to store the value of a viewer element once at the beginning +of a forward pass and refer to the static variable afterwards. +```python +class MyModel(Model): + def __init__(self): + self.slider = ViewerSlider(name="Slider", default_value=0.5, min_value=0.0, max_value=1.0) + + def get_outputs(self,ray) + slider_val = self.slider.value + #self.slider.value could change after this, unsafe to use + +``` + + +**Writing to the element** +You can write to a viewer element in Python, which provides a convenient way to track values in your code without the need for wandb/tensorboard or relying on `print` statements. + +```python +self.custom_value.value = x +``` + +:::{admonition} Warning +:class: warning + +Updating module state while training can have unexpected side effects. It is up to the user to ensure that GUI actions are safe. Conditioning on `self.training` can help determine whether effects are applied during forward passes for training or rendering. +::: + +## Example Elements + +```{image} imgs/custom_controls.png +:align: center +:width: 400 +``` + +This was created with the following elements: + +```python +from nerfstudio.viewer.server.viewer_elements import * + +class MyModel(Model): + def __init__(self): + self.a = ViewerButton(name="My Button", cb_hook=self.handle_btn) + self.b = ViewerNumber(name="Number", default_value=1.0) + self.c = ViewerCheckbox(name="Checkbox", default_value=False) + self.d = ViewerDropdown(name="Dropdown", default_value="A", options=["A", "B"]) + self.e = ViewerSlider(name="Slider", default_value=0.5, min_value=0.0, max_value=1.0) + self.f = ViewerText(name="Text", default_value="Hello World") + self.g = ViewerVec3(name="3D Vector", default_value=(0.1, 0.7, 0.1)) + + self.rgb_renderer = RGBRenderer() +... +class RGBRenderer(nn.Module): + def __init__(self): + #lives in "Custom/model/rgb_renderer" GUI folder + self.a = ViewerRGB(name="F", default_value=(0.1, 0.7, 0.1)) +... +``` + +For more information on the available classes and their arguments, refer to the [API documentation](../../reference/api/viewer.rst) diff --git a/docs/developer_guides/viewer/imgs/custom_controls.png b/docs/developer_guides/viewer/imgs/custom_controls.png new file mode 100644 index 000000000..557f76b5a Binary files /dev/null and b/docs/developer_guides/viewer/imgs/custom_controls.png differ diff --git a/docs/developer_guides/viewer/imgs/viewer_figure.png b/docs/developer_guides/viewer/imgs/viewer_figure.png deleted file mode 100644 index 3d0fd2846..000000000 Binary files a/docs/developer_guides/viewer/imgs/viewer_figure.png and /dev/null differ diff --git a/docs/developer_guides/viewer/index.md b/docs/developer_guides/viewer/index.md new file mode 100644 index 000000000..eebc29d32 --- /dev/null +++ b/docs/developer_guides/viewer/index.md @@ -0,0 +1,22 @@ +# Viewer + +> We have a real-time web viewer that requires no installation. It's available at [https://viewer.nerf.studio/](https://viewer.nerf.studio/), where you can connect to your training job. + +The viewer is built on [Viser](https://github.com/brentyi/viser/tree/main/viser) using [ThreeJS](https://threejs.org/) and packaged into a [ReactJS](https://reactjs.org/) application. This client viewer application will connect via a websocket to a server running on your machine. + +```{toctree} +:titlesonly: + +custom_gui +local_viewer +``` + +## Acknowledgements and references + +We thank the authors and contributors to the following repos, which we've started, used, and modified for our use-cases. + +- [Viser](https://github.com/brentyi/viser/tree/main/viser) - made by [Brent Yi](https://github.com/brentyi) +- [meshcat-python](https://github.com/rdeits/meshcat-python) - made by [Robin Deits](https://github.com/rdeits) +- [meshcat](https://github.com/rdeits/meshcat) - made by [Robin Deits](https://github.com/rdeits) +- [ThreeJS](https://threejs.org/) +- [ReactJS](https://reactjs.org/) diff --git a/docs/developer_guides/viewer/local_viewer.md b/docs/developer_guides/viewer/local_viewer.md new file mode 100644 index 000000000..d5912bd89 --- /dev/null +++ b/docs/developer_guides/viewer/local_viewer.md @@ -0,0 +1,62 @@ +# Local Server + +If you are unable to connect to `https://viewer.nerf.studio`, want to use Safari, or want develop the viewer codebase, you can launch your own local viewer. + +## Installing Dependencies + +```shell +cd nerfstudio/viewer/app +``` + +Install npm (to install yarn) and yarn + +```shell +sudo apt-get install npm +npm install --global yarn +``` + +Install nvm and set the node version +Install nvm with [instructions](https://heynode.com/tutorial/install-nodejs-locally-nvm/). + +```shell +nvm install 17.8.0 +``` + +Now running `node --version` in the shell should print "v17.8.0". +Install package.json dependencies and start the client viewer app: + +```shell +yarn install +``` + +## Launch the web client + +From the `nerfstudio/viewer/app` folder, run: + +```shell +yarn start +``` + +The local webserver runs on port 4000 by default, +so when `ns-train` is running, you can connect to the viewer locally at +[http://localhost:4000/?websocket_url=ws://localhost:7007](http://localhost:4000/?websocket_url=ws://localhost:7007) + +## FAQ + +### Engine node incompatible + +While running `yarn install`, you run into: `The engine "node" is incompatible with this module.` + +**Solution**: + +Install nvm with instructions at [instructions](https://heynode.com/tutorial/install-nodejs-locally-nvm/). + +```shell +nvm install 17.8.0 +``` + +If you cannot install nvm, try ignoring the engines + +```shell +yarn install --ignore-engines +``` diff --git a/docs/extensions/blender_addon.md b/docs/extensions/blender_addon.md index 2f410ff4f..8e2fa4bd2 100644 --- a/docs/extensions/blender_addon.md +++ b/docs/extensions/blender_addon.md @@ -1,58 +1,75 @@ # Blender VFX add-on

- image +

- ## Overview -This Blender add-on allows for compositing with a Nerfstudio render as a background layer by generating a camera path JSON file from the Blender camera path, as well as a way to import Nerfstudio JSON files as a Blender camera baked with the Nerfstudio camera path. This is achieved by importing a mesh or point-cloud representation of the NeRF scene from Nerfstudio into Blender and getting the camera coordinates relative to the transformations of the NeRF representation. Dynamic FOV from the Blender camera is supported and will match the Nerfstudio render. Perspective and equirectangular cameras are also supported. +This Blender add-on allows for compositing with a Nerfstudio render as a background layer by generating a camera path JSON file from the Blender camera path, as well as a way to import Nerfstudio JSON files as a Blender camera baked with the Nerfstudio camera path. This add-on also allows compositing multiple NeRF objects into a NeRF scene. This is achieved by importing a mesh or point-cloud representation of the NeRF scene from Nerfstudio into Blender and getting the camera coordinates relative to the transformations of the NeRF representation. Dynamic FOV from the Blender camera is supported and will match the Nerfstudio render. Perspective and equirectangular cameras are supported. -## Prerequisites - -First install [Blender](https://www.blender.org/). The python file add-on can be downloaded from the main repo in the `scripts/blender` folder. Before starting, export the mesh or point cloud representation of the NeRF from Nerfstudio, which will be used as reference for the actual NeRF in the Blender scene. Mesh export at a good quality is preferred, however, if the export is not clear or the NeRF is very large, a detailed point cloud export will also work. +
+ image +
## Add-on Setup Instructions -1. Open a new or existing scene to add the NeRF background in. +1. The add-on requires Blender 3.0 or newer, install Blender [here](https://www.blender.org/). +2. Download Blender Add-on Script -2. Install and enable the add-on in Blender in Edit β†’ Preferences β†’ Add-Ons. The add-on will be visible in the Render Properties tab on the right panel. -

- image - image -

+3. Install and enable the add-on in Blender in `Edit β†’ Preferences β†’ Add-Ons`. The add-on will be visible in the Render Properties tab on the right panel. +
+ image +
-3. Import the mesh or point cloud representation of the NeRF into the scene. It is preferred that a mesh is used at a good enough fidelity. You may need to crop the mesh further. Since it is used as a reference and won't be visible in the final render, only the parts that the blender animation will interact with may be necessary to import. +4. The add-on should now be installed in the `Render Properties` panel +
+ image +
+ +## Scene Setup + +1. Export the mesh or point cloud representation of the NeRF from Nerfstudio, which will be used as reference for the actual NeRF in the Blender scene. Mesh export at a good quality is preferred, however, if the export is not clear or the NeRF is large, a detailed point cloud export will also work. + +2. Import the mesh or point cloud representation of the NeRF into the scene. You may need to crop the mesh further. Since it is used as a reference and won't be visible in the final render, only the parts that the blender animation will interact with may be necessary to import. + +3. Select the NeRF mesh or point cloud in the add-on. 4. Resize, position, or rotate the NeRF representation to fit your scene. - ## Generate Nerfstudio JSON Camera Path from Blender Camera 1. There are a few ways to hide the reference mesh for the Blender render - - In object properties, select "Shadow Catcher". This makes the representation invisible in the render, but all shadows cast on it will render. You may have to switch to the cycles renderer to see the shadow catcher option. - image - - - Note: This may not give ideal results if the mesh is not very clear or occludes other objects in the scene. If this is the case, you can hide the mesh from the render instead by clicking the camera button in the Outliner next to its name. - image + - In object properties, select "Shadow Catcher". This makes the representation invisible in the render, but all shadows cast on it will render. You may have to switch to the cycles renderer to see the shadow catcher option. +
+ image +
+ + - Note: This may not give ideal results if the mesh is not very clear or occludes other objects in the scene. If this is the case, you can hide the mesh from the render instead by clicking the camera button in the Outliner next to its name. +
+ image +
2. Verify that the animation plays and the NeRF representation does not occlude the camera. -3. Go to the Nerfstudio Add-on in Render Properties and expand the "Nerfstudio Path Generator" tab in the panel. Use the object selector to select the NeRF representation. Then, select the file path for the output JSON camera path file. -4. Click "Generate JSON file". The output JSON file is named `camera_path_blender.json`. +3. Go to the Nerfstudio Add-on in Render Properties and expand the "Nerfstudio Path Generator" tab in the panel. Use the object selector to select the NeRF representation. Then, select the file path for the output JSON camera path file. +4. Click "Generate JSON file". The output JSON file is named `camera_path_blender.json`. +
image - +
5. Render the NeRF with the generated camera path using Nerfstudio in the command prompt or terminal. -6. Before rendering the Blender animation, go to the Render Properties and in the Film settings select "Transparent" so that the render will be rendered with a clear background to allow it to be composited over the NeRF render. +6. Before rendering the Blender animation, go to the Render Properties and in the Film settings select "Transparent" so that the render will be rendered with a clear background to allow it to be composited over the NeRF render. +
image +
- -7. Now the scene can be rendered and composited over the camera aligned Nerfstudio render. +7. Now the scene can be rendered and composited over the camera aligned Nerfstudio render. +
image +
### Examples @@ -63,23 +80,29 @@ First install [Blender](https://www.blender.org/). The python file add-on can be ### Additional details -- You can also apply an environment texture to the Blender scene by using Nerfstudio to render an equirectangular image (360 image) of the NeRF from a place in the scene. -- The settings for the Blender equirectangular camera are: "Panoramic" camera type and panorama type of "Equirectangular". -image +- You can also apply an environment texture to the Blender scene by using Nerfstudio to render an equirectangular image (360 image) of the NeRF from a place in the scene. +- The settings for the Blender equirectangular camera are: "Panoramic" camera type and panorama type of "Equirectangular". +
+ image +
- The generated JSON camera path follows the user specified frame start, end, and step fields in the Output Properties in Blender. The JSON file specifies the user specified x and y render resolutions at the given % in the Output Properties. - The add-on computes the camera coordinates based on the active camera in the scene. - FOV animated changes of the camera will be matched with the NeRF render. - Perspective and equirectangular cameras are supported and can be selected within Blender. - The generated JSON file can be imported into Nerfstudio. Each keyframe of the camera transform in the frame sequence in Blender will be a keyframe in Nerfstudio. The exported JSON camera path is baked, where each frame in the sequence is a keyframe. This is to ensure that frame interpolation across Nerfstudio and Blender do not differ. +
image +
- It is recommended to run the camera path in the Nerfstudio web interface with the NeRF to ensure that the NeRF is visible throughout the camera path. - The fps and keyframe timestamps are based on the "Frame Rate" setting in the Output Properties in Blender. - The NeRF representation can also be transformed (position, rotation, and scale) as well as animated. +- The pivot point of the NeRF representation should not be changed - It is recommended to export a high fidelity mesh as the NeRF representation from Nerfstudio. However if that is not possible or the scene is too large, a point cloud representation also works. - For compositing, it is recommended to convert the video render into image frames to ensure the Blender and NeRF renders are in sync. - Currently, dynamic camera focus is not supported. - Fisheye and orthographic cameras are not supported. +- A walkthrough of this section is included in the tutorial video. ## Create Blender Camera from Nerfstudio JSON Camera Path @@ -87,7 +110,6 @@ First install [Blender](https://www.blender.org/). The python file add-on can be image

- 1. Expand the "Nerfstudio Camera Generator" tab in the panel. After inputting the NeRF representation, select the JSON Nerfstudio file and click "Create Camera from JSON" 2. A new camera named "NerfstudioCamera" should appear in the viewport with the camera path and FOV of the input file. This camera's type will match the Nerfstudio input file, except fisheye cameras. @@ -103,13 +125,97 @@ First install [Blender](https://www.blender.org/). The python file add-on can be - Scale of the camera is not keyframed from the Nerfstudio camera path. - This newly created camera has a sensor fit of "Vertical" +## Compositing NeRF Objects in NeRF Environments + +You can composite NeRF objects into a scene with a NeRF background by rendering the cropped NeRF object along with an accumulation render as an alpha mask and compositing that over the background NeRF render. + +

+ image +

+ +1. Import the background NeRF scene as a point cloud (or mesh, but point cloud is preferred for large scenes). + +2. Export a cropped NeRF mesh of the NeRF object(s) + + - Open the NeRF object from the Nerfstudio viewer and select "Crop Viewport" and accordingly adjust the Scale and Center values of the bounding box to crop the NeRF scene to around the bounds of the object of interest. + +
+ image +
+ + - Copy over the scale and center values into the Export panel and export the NeRF object as a mesh (point cloud will also work but shadows can be rendered if the object is a mesh) + + - Keep note of the scale and center values for the crop. This can be done by creating a new JSON camera path in the editor which will add a crop section towards the end of the file. + +
+ image +
+ +3. Import the NeRF object representation. Rescale and position the scene and NeRF object and background environment. You can also animate either of them. + +4. (Optional) To add shadows of the NeRF object(s) + + - Add a plane representing the ground of the environment. In object properties, select "Shadow Catcher" under Visibility. You may have to switch to the cycles renderer to see the shadow catcher option. + + - In the object properties of the NeRF object, go to the Ray Visibility section and deselect the "Camera" option. This will hide the mesh in the Blender render, but keep its shadow. + +
+ image +
+ + - Render the Blender animation with only the shadow of the object on the shadow catcher visible. Render with a transparent background by selecting "Transparent" under the Film Settings in the Render Properties. + +5. Go to the Nerfstudio Add-on in Render Properties and expand the "Nerfstudio Path Generator" tab in the panel. Use the object selector to select the NeRF environment representation. Then, select the file path for the output JSON camera path file. Click "Generate JSON file". The output JSON file is named `camera_path_blender.json`. It is recommended to rename this JSON file to keep track of the multiple camera paths that will be used to construct the full render. + +6. Generate a Nerfstudio camera path now for the NeRF object. Select the object selector to select the NeRF object. Before generating the new JSON file, you may need to rename the previously generated camera path for the environment or move it to a different directory otherwise the new JSON file will overwrite it. Click "Generate JSON file". The new output JSON file is named `camera_path_blender.json`. You will need to repeat this process for each NeRF object you want to composite in your scene. + +7. Render the NeRF of the background environment using the generated camera path for the environment using the Nerfstudio in the command line or terminal. + +8. Render the NeRF object + + - Open the recently generated blender Nerfstudio JSON camera path and add the crop parameters to the camera path. This section will be placed towards the end of the Blender Nerfstudio camera path after the `is_cycle` field. This can be done by copying over the crop section from the JSON camera path with the scene crop which was exported earlier. Alternatively, you can enter the crop values manually in this format. +
+ image +
+ + - Render the NeRF object using Nerfstudio and the edited camera path in the command line or terminal. This will be the RGB render. + + - Next, render the accumulation render as an alpha mask of the NeRF object by adding the command line argument `--rendered-output-names accumulation` to the render command. + +
+ image +
+ +9. Convert each of the Nerfstudio render videos into an image sequence of frames as PNG or JPG files. This will ensure that the frames will be aligned when compositing. You can convert the video mp4 to an image sequence by creating a Blender Video Editing file and rendering the mp4 as JPGs or PNGs. + +10. Composite the NeRF renders in a video editing software such as Adobe Premiere Pro. + + - Place the render of the Nerfstudio background environment at the lowest layer, then place the shadow render of the NeRF object if created. + + - Place the RGB NeRF render of the NeRF object over the environment (and shadow if present) layers and then place the accumulation NeRF object render over the RGB NeRF object render. + + - Apply a filter to use the accumulation render as an alpha mask. In Premiere Pro, apply the effect "Track Matte Key" to the RGB render and select the "Matte" as the video track of the accumulation render and under "Composite Using" select "Matte Luma". + +### Additional Details + +- The RGB and accumulation renders will need to be rendered for each NeRF cropped object in the scene, but not for the NeRF environment if it is not cropped. +- If you will composite a shadow layer, the quality of the exported mesh of the NeRF object should have enough fidelity to cast a shadow, but the texture doesn't need to be clear. +- If motion tracking or compositing over real camera footage, you can add planes or cubes to represent walls or doorways as shadow catcher or holdout objects. This will composite the shadow layer over the NeRF environment and help create alpha masks. +- The pivot point of the NeRF representations should not be changed. +- A walkthrough of this section is included in the tutorial video. + +### Examples + +

+ image + image +

## Implementation Details -For generating the JSON camera path, we iterate over the scene frame sequence (from the start to the end with step intervals) and get the camera 4x4 world matrix at each frame. The world transformation matrix gives the position, rotation, and scale of the camera. We then obtain the world matrix of the NeRF representation at each frame and transform the camera coordinates with this to get the final camera world matrix. This allows us to re-position, rotate, and scale the NeRF representation in Blender and generate the right camera path to render the NeRF accordingly in Nerfstudio. Additionally, we calculate the FOV of the camera at each frame based on the sensor fit (horizontal or vertical), angle of view, and aspect ratio. +For generating the JSON camera path, we iterate over the scene frame sequence (from the start to the end with step intervals) and get the camera 4x4 world matrix at each frame. The world transformation matrix gives the position, rotation, and scale of the camera. We then obtain the world matrix of the NeRF representation at each frame and transform the camera coordinates with this to get the final camera world matrix. This allows us to re-position, rotate, and scale the NeRF representation in Blender and generate the right camera path to render the NeRF accordingly in Nerfstudio. Additionally, we calculate the FOV of the camera at each frame based on the sensor fit (horizontal or vertical), angle of view, and aspect ratio. Next, we construct the list of keyframes which is very similar to the world matrices of the transformed camera matrix. Camera properties in the JSON file are based on user specified fields such as resolution (user specified in Output Properties in Blender), camera type (Perspective or Equirectangular). In the JSON file, `aspect` is specified as 1.0, `smoothness_value` is set to 0, and `is_cycle` is set to false. The Nerfstudio render is the fps specified in Blender where the duration is the total number of frames divided by the fps. Finally, we construct the full JSON object and write it to the file path specified by the user. -For generating the camera from the JSON file, we create a new Blender camera based on the input file and iterate through the `camera_path` field in the JSON to get the world matrix of the object from the `matrix_to_world` and similarly get the FOV from the `fov` fields. At each iteration, we set the camera to these parameters and insert a keyframe based on the position, rotation, and scale of the camera as well as the focal length of the camera based on the vertical FOV input. - +For generating the camera from the JSON file, we create a new Blender camera based on the input file and iterate through the `camera_path` field in the JSON to get the world matrix of the object from the `matrix_to_world` and similarly get the FOV from the `fov` fields. At each iteration, we set the camera to these parameters and insert a keyframe based on the position, rotation, and scale of the camera as well as the focal length of the camera based on the vertical FOV input. diff --git a/docs/extensions/imgs/sdfstudio_overview.svg b/docs/extensions/imgs/sdfstudio_overview.svg new file mode 100644 index 000000000..9183ef453 --- /dev/null +++ b/docs/extensions/imgs/sdfstudio_overview.svg @@ -0,0 +1,33888 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/extensions/sdfstudio.md b/docs/extensions/sdfstudio.md new file mode 100644 index 000000000..4c8f24874 --- /dev/null +++ b/docs/extensions/sdfstudio.md @@ -0,0 +1,51 @@ +# SDFStudio + +[project website](https://autonomousvision.github.io/sdfstudio/) + +```{image} imgs/sdfstudio_overview.svg +:width: 800 +:align: center +:alt: sdfstudio overview figure +``` + +## Overview + +SDFStudio is built on top of nerfstudio. It implements multiple implicit surface reconstruction methods including: + +- UniSurf +- VolSDF +- NeuS +- MonoSDF +- Mono-UniSurf +- Mono-NeuS +- Geo-NeuS +- Geo-UniSurf +- Geo-VolSDF +- NeuS-acc +- NeuS-facto +- NeuralReconW + +You can learn more about these methods [here](https://github.com/autonomousvision/sdfstudio/blob/master/docs/sdfstudio-methods.md#Methods) + +## Surface models in nerfstudio + +We intend to integrate many of the SDFStudio improvments back into the nerstudio core repository. + +Supported methods: + +- NeuS +- NeuS-facto + +## Citation + +If you use these surface based models in your research, you should consider citing the authors of SDFStudio, + +```none +@misc{Yu2022SDFStudio, + author = {Yu, Zehao and Chen, Anpei and Antic, Bozidar and Peng, Songyou Peng and Bhattacharyya, Apratim + and Niemeyer, Michael and Tang, Siyu and Sattler, Torsten and Geiger, Andreas}, + title = {SDFStudio: A Unified Framework for Surface Reconstruction}, + year = {2022}, + url = {https://github.com/autonomousvision/sdfstudio}, +} +``` diff --git a/docs/index.md b/docs/index.md index b03e8712b..ee611c573 100644 --- a/docs/index.md +++ b/docs/index.md @@ -60,6 +60,7 @@ Contributing :caption: Extensions extensions/blender_addon extensions/unreal_engine +extensions/sdfstudio ``` ```{toctree} @@ -74,8 +75,9 @@ nerfology/model_components/index :hidden: :caption: Developer Guides +developer_guides/new_methods developer_guides/pipelines/index -developer_guides/viewer/viewer_overview +developer_guides/viewer/index developer_guides/config developer_guides/debugging_tools/index ``` diff --git a/docs/nerfology/methods/in2n.md b/docs/nerfology/methods/in2n.md new file mode 100644 index 000000000..bd9ebccb6 --- /dev/null +++ b/docs/nerfology/methods/in2n.md @@ -0,0 +1,96 @@ +# Instruct-NeRF2NeRF + +

Editing 3D Scenes with Instructions

+ +```{button-link} https://instruct-nerf2nerf.github.io/ +:color: primary +:outline: +Paper Website +``` + +```{button-link} https://github.com/ayaanzhaque/instruct-nerf2nerf +:color: primary +:outline: +Code +``` + + + +**Instruct-NeRF2NeRF enables instruction-based editing of NeRFs via a 2D diffusion model** + +## Installation + +First install nerfstudio dependencies. Then run: + +```bash +pip install git+https://github.com/ayaanzhaque/instruct-nerf2nerf +``` + +## Running Instruct-NeRF2NeRF + +Details for running Instruct-NeRF2NeRF (built with Nerfstudio!) can be found [here](https://github.com/ayaanzhaque/instruct-nerf2nerf). Once installed, run: + +```bash +ns-train in2n --help +``` + +Three variants of Instruct-NeRF2NeRF are provided: + +| Method | Description | Memory | Quality | +| ------------ | ---------------------------- | ------ | ------- | +| `in2n` | Full model, used in paper | ~15GB | Best | +| `in2n-small` | Half precision model | ~12GB | Good | +| `in2n-tiny` | Half prevision with no LPIPS | ~10GB | Ok | + +## Method + +### Overview + +Instruct-NeRF2NeRF is a method for editing NeRF scenes with text-instructions. Given a NeRF of a scene and the collection of images used to reconstruct it, the method uses an image-conditioned diffusion model ([InstructPix2Pix](https://www.timothybrooks.com/instruct-pix2pix)) to iteratively edit the input images while optimizing the underlying scene, resulting in an optimized 3D scene that respects the edit instruction. The paper demonstrates that their method is able to edit large-scale, real-world scenes, and is able to accomplish more realistic, targeted edits than prior work. + +## Pipeline + + + +This section will walk through each component of the Instruct-NeRF2NeRF method. + +### How it Works + +Instruct-NeRF2NeRF gradually updates a reconstructed NeRF scene by iteratively updating the dataset images while training the NeRF: + +1. An image is rendered from the scene at a training viewpoint. +2. It is edited by InstructPix2Pix given a global text instruction. +3. The training dataset image is replaced with the edited image. +4. The NeRF continues training as usual. + +### Editing Images with InstructPix2Pix + +InstructPix2Pix is an image-editing diffusion model which can be prompted using text instructions. More details on InstructPix2Pix can be found [here](https://www.timothybrooks.com/instruct-pix2pix). + +Traditionally, at inference time, InstructPix2Pix takes as input random noise and is conditioned on an image (the image to edit) and a text instruction. The strength of the edit can be controlled using the image and text classifier-free guidance scales. + +To update a dataset image a given viewpoint, Instruct-NeRF2NeRF first takes the original, unedited training image as image conditioning and uses the global text instruction as text conditioning. The main input to the diffusion model is a noised version of the current render from the given viewpoint. The noise is sampled from a normal distribution and scaled based on a randomly chosen timestep. Then InstructPix2Pix slowly denoises the rendered image by predicting the noised version of the image at previous timesteps until the image is fully denoised. This will produce an edited version of the input image. + +This process mixes the information of the diffusion model, which attempts to edit the image, the current 3D structure of the NeRF, and view-consistent information from the unedited, ground-truth images. By combining this set of information, the edit is respected while maintaining 3D consistency. + +The code snippet for how an image is edited in the pipeline can be found [here](https://github.com/ayaanzhaque/instruct-nerf2nerf/blob/main/in2n/ip2p.py). + +### Iterative Dataset Update + +When NeRF training starts, the dataset consists of the original, unedited images used to train the original scene. These images are saved separately to use as conditioning for InstructPix2Pix. At each optimization step, some number of NeRF optimization steps are performed, and then some number of images (often just one) are updated. The images are randomly ordered prior to training and then at each step, the images are chosen in order to edit. Once an image has been edited, it is replaced in the dataset. Importantly, at each NeRF step, rays are sampled across the entire dataset, meaning there is a mixed source of supervision between edited images and unedited images. This allows for a gradual optimization that balances maintaining the 3D structure and consistency of the NeRF as well as performing the edit. + +At early iterations of this process, the edited images may be inconistent with one another, as InstructPix2Pix often doesn't perform consistent edits across viewpoints. However, over time, since images are edited using the current render of the NeRF, the edits begin to converge towards a globally consistent depiction of the underlying scene. Here is an example of how the underlying dataset evolves and becomes more consistent. + + + +The traditional method for supervising NeRFs using diffusion models is to use a Score Distillation Sampling (SDS) loss, as proposed in [DreamFusion](https://dreamfusion3d.github.io/). The Iterative Dataset Update method can be viewed as a variant of SDS, as instead of updating a discrete set of images at each step, the loss is a mix of rays from various viewpoints which are edited to varying degrees. The results show that this leads to higher quality performance and more stable optimization. + +## Results + +For results, view the [project page](https://instruct-nerf2nerf.github.io/)! diff --git a/docs/nerfology/methods/index.md b/docs/nerfology/methods/index.md index 9eb1af0a5..441a6d705 100644 --- a/docs/nerfology/methods/index.md +++ b/docs/nerfology/methods/index.md @@ -1,8 +1,8 @@ # Methods -We provide a set of pre implemented nerfstudio methods. +We provide a set of pre implemented nerfstudio methods. -**The goal of nerfstudio is to modularize the various NeRF techniques as much as possible.** +**The goal of nerfstudio is to modularize the various NeRF techniques as much as possible.** As a result, many of the techniques from these pre-implemented methods can be mixed 🎨. @@ -20,9 +20,9 @@ To list the available methods run: ns-train --help ``` -## Guides +## Implemented Methods -In addition to their implementations, we have provided guides that walk through each of these method. +The following methods are supported in nerfstudio: ```{toctree} :maxdepth: 1 @@ -30,5 +30,29 @@ In addition to their implementations, we have provided guides that walk through Mip-NeRF Nerfacto Instant-NGP + Instruct-NeRF2NeRF + LERF Semantic NeRF-W + Tetra-NeRF ``` + +(own_method_docs)= + +## Adding Your Own Method + +If you're a researcher looking to develop new NeRF-related methods, we hope that you find nerfstudio to be a useful tool. We've provided documentation about integrating with the nerfstudio codebase, which you can find [here](../../developer_guides/new_methods.md). + +We also welcome additions to the list of methods above. To do this, simply create a pull request that adds a markdown file describing the model to the docs/nerfology/methods folder, and update the list in this file. For reference on the layout, you can check out the [Instruct-NeRF2NeRF](in2n) page. Please try to include the following information: + +- Installation instructions +- Instructions for running the method +- Requirements (dataset, GPU, ect) +- Method description (the more detailed the better, treat it like a blog post) + +You are welcome to include assets (such as images or video) in the description, but please host them elsewhere. + +:::{admonition} Note +:class: note + +Please ensure that the documentation is clear and easy to understand for other users who may want to try out your method. +::: diff --git a/docs/nerfology/methods/lerf.md b/docs/nerfology/methods/lerf.md new file mode 100644 index 000000000..509c88fac --- /dev/null +++ b/docs/nerfology/methods/lerf.md @@ -0,0 +1,91 @@ +# LERF + +

πŸ“Ž Language Embedded Radiance Fields 🚜

+ +```{button-link} https://www.lerf.io/ +:color: primary +:outline: +Paper Website +``` + +```{button-link} https://github.com/kerrj/lerf +:color: primary +:outline: +Code +``` + + + +**Grounding CLIP vectors volumetrically inside NeRF allows flexible natural language queries in 3D** + +## Installation + +First install nerfstudio dependencies. Then run: + +```bash +pip install git+https://github.com/kerrj/lerf +``` + +## Running LERF + +Details for running LERF (built with Nerfstudio!) can be found [here](https://github.com/kerrj/lerf). Once installed, run: + +```bash +ns-train lerf --help +``` + +Three variants of LERF are provided: + +| Method | Description | Memory | Quality | +| ----------- | ----------------------------------------------- | ------ | ------- | +| `lerf-big` | LERF with OpenCLIP ViT-L/14 | ~22 GB | Best | +| `lerf` | Model with OpenCLIP ViT-B/16, used in paper | ~15 GB | Good | +| `lerf-lite` | LERF with smaller network and less LERF samples | ~8 GB | Ok | + +`lerf-lite` should work on a single NVIDIA 2080. +`lerf-big` is experimental, and needs further tuning. + +## Method + +LERF enables pixel-aligned queries of the distilled 3D CLIP embeddings without relying on region proposals, masks, or fine-tuning, supporting long-tail open-vocabulary queries hierarchically across the volume. + +### Multi-scale supervision + +To supervise language embeddings, we pre-compute an image pyramid of CLIP features for each training view. Then, each sampled ray during optimization is supervised by interpolating the CLIP embedding within this pyramid. + + + +### LERF Optimization + +LERF optimizes a dense, multi-scale language 3D field by volume rendering CLIP embeddings along training rays, supervising these embeddings with multi-scale CLIP features across multi-view training images. + +Inspired by Distilled Feature Fields (DFF), we use DINO features to regularize CLIP features. This leads to qualitative improvements in object boundaries, as CLIP embeddings in 3D can be sensitive to floaters and regions with few views. + +After optimization, LERF can extract 3D relevancy maps for language queries interactively in real-time. + + + +### Visualizing relevancy for text queries + +Set the "Output Render" type to `relevancy_0`, and enter the text query in the "LERF Positives" textbox (see image). The output render will show the 3D relevancy map for the query. View the [project page](https://lerf.io) for more examples and details about the relevancy map normalization. + +
+ +
+ +## Results + +For results, view the [project page](https://lerf.io)! + +Datasets used in the original paper can be found [here](https://drive.google.com/drive/folders/1LUzwEvBCE19PNYcwfmrG-9FLpZLbi4on?usp=sharing). + +```none +@article{lerf2023, + author = {Kerr, Justin and Kim, Chung Min and Goldberg, Ken and Kanazawa, Angjoo and Tancik, Matthew}, + title = {LERF: Language Embedded Radiance Fields}, + journal = {arXiv preprint arXiv:2303.09553}, + year = {2023}, +} +``` diff --git a/docs/nerfology/methods/nerfacto.md b/docs/nerfology/methods/nerfacto.md index a38f91bff..c1137884a 100644 --- a/docs/nerfology/methods/nerfacto.md +++ b/docs/nerfology/methods/nerfacto.md @@ -7,13 +7,21 @@ ```bash ns-train nerfacto --help ``` - + +We provide a few additional variants: + +| Method | Description | Memory | Speed | +| ---------------- | --------------------- | ------ | ------ | +| `nerfacto` | Default Model | ~6GB | Fast | +| `nerfacto-big` | Larger higher quality | ~12GB | Slower | +| `depth-nerfacto` | Supervise on depth | ~6GB | Fast | + ## Method - + ### Overview - + We created the nerfacto model to act as our default for real data captures of static scenes. The model is not existing published work, but rather a combination of many published methods that we have found work well for real data. This guide discusses the details of the model, understanding the [NeRF model](./nerf.md) is a prerequisite. - + ```{admonition} TLDR We combine the following techniques in this model: * Camera pose refinement @@ -22,55 +30,55 @@ We combine the following techniques in this model: * Scene contraction * Hash encoding ``` - + ```{warning} πŸ—οΈ This guide is under construction πŸ—οΈ ``` - + ## Pipeline - + ```{image} imgs/nerfacto/models_nerfacto_pipeline-light.png :align: center :class: only-light ``` - + ```{image} imgs/nerfacto/models_nerfacto_pipeline-dark.png :align: center :class: only-dark ``` - + Here is an overview pipeline for nerfacto, we will walk through each component in this guide. - + ### Pose Refinement - + It is not uncommon to have errors in the predicted camera poses. This is even more of a factor when using poses acquired from devices such as phones (ie. if you use the Record3D IOS app to capture data). Misaligned poses result in both cloudy artifacts in the scene and a reduction of sharpness and details. The NeRF framework allows us to backpropagate loss gradients to the input pose calculations. We can use this information to optimize and refine the poses. - + ### Piecewise Sampler - + We use a Piecewise sampler to produce the initial set of samples of the scene. This sampler allocates half of the samples uniformly up to a distance of 1 from the camera. The remaining samples are distributed such that the step size increases with each sample. The step size is chosen such that the [frustums](../model_components/visualize_samples.ipynb) are scaled versions of themselves. By increasing the step sizes, we are able to sample distant objects will still having a dense set of samples for nearby objects. - + ### Proposal Sampler - + The proposal sampler consolidates the sample locations to the regions of the scene that contribute most to the final render (typically the first surface intersection). This greatly improves reconstruction quality. The proposal network sampler requires a density function for the scene. The density function can be implemented in a variety of ways, we find that using a small fused-mlp with a hash encoding has sufficient accuracy and is fast. The proposal network sampler can be chained together with multiple density functions to further consolidate the sampling. We have found that two density functions are better than one. Larger than 2 leads to diminishing returns. - + #### Density Field - + The density field only needs to represent a coarse density representation of the scene to guide sampling. Combining a hash encoding with a small fused MLP (from [tiny-cuda-nn](https://github.com/NVlabs/tiny-cuda-nn)) provides a fast way to query the scene. We can make it more efficient by decreasing the encoding dictionary size and number of feature levels. These simplifications have little impact on the reconstruction quality because the density function does not need to learn high frequency details during the initial passes. - + ### Nerfacto Field - + ```{image} imgs/nerfacto/models_nerfacto_field-light.png :align: center :class: only-light :width: 600 ``` - + ```{image} imgs/nerfacto/models_nerfacto_field-dark.png :align: center :class: only-dark :width: 600 ``` - + ```{button-link} https://github.com/nerfstudio-project/nerfstudio/blob/main/nerfstudio/models/nerfacto.py :color: primary :outline: diff --git a/docs/nerfology/methods/tetranerf.md b/docs/nerfology/methods/tetranerf.md new file mode 100644 index 000000000..e18163087 --- /dev/null +++ b/docs/nerfology/methods/tetranerf.md @@ -0,0 +1,77 @@ +# Tetra-NeRF + +

Tetra-NeRF: Representing Neural Radiance Fields Using Tetrahedra

+ +```{button-link} https://jkulhanek.com/tetra-nerf +:color: primary +:outline: +Paper Website +``` + +```{button-link} https://github.com/jkulhanek/tetra-nerf +:color: primary +:outline: +Code +``` + + + +**SfM input pointcloud is triangulated and resulting tetrahedra is used as the radiance field representation** + +## Installation + +First, make sure to install the following: +``` +CUDA (>=11.3) +PyTorch (>=1.12.1) +Nerfstudio (>=0.2.0) +OptiX (>=7.2, preferably 7.6) +CGAL +CMake (>=3.22.1) +``` +Follow the [installation section](https://github.com/jkulhanek/tetra-nerf/blob/master/README.md#installation) in the tetra-nerf repository + +Finally, you can install **Tetra-NeRF** by running: +```bash +pip install git+https://github.com/jkulhanek/tetra-nerf +``` + +## Running Tetra-NeRF on custom data +Details for running Tetra-NeRF can be found [here](https://github.com/jkulhanek/tetra-nerf). + +```bash +python -m tetranerf.scripts.process_images --path +python -m tetranerf.scripts.triangulate --pointcloud /sparse.ply --output /sparse.th +ns-train tetra-nerf --pipeline.model.tetrahedra-path /sparse.th minimal-parser --data +``` + +Three following variants of Tetra-NeRF are provided: + +| Method | Description | Memory | Quality | +| --------------------- | -------------------------------------- | ------- | ------- | +| `tetra-nerf-original` | Official implementation from the paper | ~18GB* | Good | +| `tetra-nerf` | Different sampler - faster and better | ~16GB* | Best | + +*Depends on the size of the input pointcloud, estimate is given for a larger scene (1M points) + +## Method +![method overview](https://jkulhanek.com/tetra-nerf/resources/overview-white.svg)
+The input to Tetra-NeRF is a point cloud which is triangulated to get a set of tetrahedra used to represent the radiance field. Rays are sampled, and the field is queried. The barycentric interpolation is used to interpolate tetrahedra vertices, and the resulting features are passed to a shallow MLP to get the density and colours for volumetric rendering.
+ +[![demo blender lego (sparse)](https://jkulhanek.com/tetra-nerf/resources/images/blender-lego-sparse-100k-animated-cover.gif)](https://jkulhanek.com/tetra-nerf/demo.html?scene=blender-lego-sparse) +[![demo mipnerf360 garden (sparse)](https://jkulhanek.com/tetra-nerf/resources/images/360-garden-sparse-100k-animated-cover.gif)](https://jkulhanek.com/tetra-nerf/demo.html?scene=360-garden-sparse) +[![demo mipnerf360 garden (sparse)](https://jkulhanek.com/tetra-nerf/resources/images/360-bonsai-sparse-100k-animated-cover.gif)](https://jkulhanek.com/tetra-nerf/demo.html?scene=360-bonsai-sparse) +[![demo mipnerf360 kitchen (dense)](https://jkulhanek.com/tetra-nerf/resources/images/360-kitchen-dense-300k-animated-cover.gif)](https://jkulhanek.com/tetra-nerf/demo.html?scene=360-kitchen-dense) + + +## Existing checkpoints and predictions +For an easier comparisons with Tetra-NeRF, published checkpoints and predictions can be used: + +| Dataset | Checkpoints | Predictions | Input tetrahedra | +| -------- | ----------- | ----------- | ---------------- | +| Mip-NeRF 360 (public scenes) | [download](https://data.ciirc.cvut.cz/public/projects/2023TetraNeRF/assets/mipnerf360-public-checkpoints.tar.gz) | [download](https://data.ciirc.cvut.cz/public/projects/2023TetraNeRF/assets/mipnerf360-public-predictions.tar.gz) | [download](https://data.ciirc.cvut.cz/public/projects/2023TetraNeRF/assets/mipnerf360-public-tetrahedra.tar.gz) | +| Blender | [download](https://data.ciirc.cvut.cz/public/projects/2023TetraNeRF/assets/blender-checkpoints.tar.gz) | [download](https://data.ciirc.cvut.cz/public/projects/2023TetraNeRF/assets/blender-predictions.tar.gz) | [download](https://data.ciirc.cvut.cz/public/projects/2023TetraNeRF/assets/blender-tetrahedra.tar.gz) | +| Tanks and Temples | [download](https://data.ciirc.cvut.cz/public/projects/2023TetraNeRF/assets/nsvf-tanks-and-temples-checkpoints.tar.gz) | [download](https://data.ciirc.cvut.cz/public/projects/2023TetraNeRF/assets/nsvf-tanks-and-temples-predictions.tar.gz) | [download](https://data.ciirc.cvut.cz/public/projects/2023TetraNeRF/assets/nsvf-tanks-and-temples-tetrahedra.tar.gz) | + diff --git a/docs/nerfology/model_components/visualize_samplers.ipynb b/docs/nerfology/model_components/visualize_samplers.ipynb index a23500d58..05ffbe926 100644 --- a/docs/nerfology/model_components/visualize_samplers.ipynb +++ b/docs/nerfology/model_components/visualize_samplers.ipynb @@ -1,4 +1,5 @@ { +<<<<<<< HEAD "cells": [ { "attachments": {}, @@ -7435,7 +7436,7445 @@ "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1" } } +======= + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Ray Samplers\n", + "\n", + "## Overview\n", + "\n", + "Once we have a set of cameras, we want to cast camera rays associated with each pixel. \n", + "\n", + "Along these ray we will sample the _field_ and aggregate the samples to predict the pixels value (ie. color). The parameterization of the samples are described [here](./visualize_samples.ipynb) however we must decide where to place these samples along a ray. For this task we will use a [`Sampler`](../../reference/api/model_components/ray_sampler.rst).\n", + "\n", + "In the ideal world we would compute many dense samples along a ray. Unfortunately, each additional sample adds a computation cost to the system as it needs to be processed by the _field_ which is often a neural network.\n", + "\n", + "As a result it is common for NeRF methods to use on the order of 100 samples. Therefore, we want to optimize where those samples are placed in the scene.\n", + "\n", + "For example, if the scene can be bounded by a box and the objects are all similar scales, uniform sampling along the ray may be a good option. On the other hand if the scene is unbounded (potential extending as far as the eye can see) uniform sampling does not make sense as the samples would be very sparse for close objects. In this case a different sampling like _Uniform in Disparity_ may perform better.\n", + "\n", + "```{image} imgs/samplers_type-light.png\n", + ":align: center\n", + ":class: only-light\n", + "```\n", + "\n", + "```{image} imgs/samplers_type-dark.png\n", + ":align: center\n", + ":class: only-dark\n", + "```\n", + "\n", + "### Stratified Sampling\n", + "\n", + "Most samplers has the option to _stratify_ the samplers. When stratified, each sample is randomly perturbed. \n", + "\n", + "The magnitude of the pertubation is such that the sample ordering remains consistent and the overall distribution statistics are not changed. Using stratified samples during training generally improves the reconstructions as it help prevent overfitting.\n", + "\n", + "During inference stratified sampling should be disabled (nerfstudio samplers will do this) as it can cause noisy artifacts when the camera moves.\n", + "\n", + "```{image} imgs/samplers_stratified-light.png\n", + ":align: center\n", + ":class: only-light\n", + "```\n", + "\n", + "```{image} imgs/samplers_stratified-dark.png\n", + ":align: center\n", + ":class: only-dark\n", + "```\n", + "\n", + "### Hierarchical Sampling\n", + "\n", + "It is important to sample the scene where it has content otherwise the reconstruction quality will be reduced. \n", + "\n", + "One trick that is often employed in NeRF methods is to do multiple round of sampling. The first round can use a predefined sampler (ie. Uniform) to generate an image. Once the space is sampled, we have an idea which samples contributed to the final color. \n", + "\n", + "We can use this information to sample more around those regions using a `PDFSampler`. The PDF sampler is described more below." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Spaced Samplers\n", + "\n", + "These are the most basic samplers that spaces samples based on a predefined function. These samplers have all have a starting and ending distance (also known as a near/far plane). The plots below are histograms of points sampled from some predefined samplers. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3e6b9d9b42cb4dc89f40a570007d00ec", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb }, - "nbformat": 4, - "nbformat_minor": 2 + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "        \n",
+       "        "
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "application/vnd.plotly.v1+json": {
+       "config": {
+        "plotlyServerURL": "https://plot.ly"
+       },
+       "data": [
+        {
+         "nbinsx": 50,
+         "opacity": 0.7,
+         "type": "histogram",
+         "x": [
+          2,
+          2.003000020980835,
+          2.00600004196167,
+          2.009000062942505,
+          2.01200008392334,
+          2.015000104904175,
+          2.0180001258850098,
+          2.0209999084472656,
+          2.0239999294281006,
+          2.0269999504089355,
+          2.0299999713897705,
+          2.0329999923706055,
+          2.0360000133514404,
+          2.0390000343322754,
+          2.0420000553131104,
+          2.0450000762939453,
+          2.0480000972747803,
+          2.050999879837036,
+          2.053999900817871,
+          2.056999921798706,
+          2.059999942779541,
+          2.062999963760376,
+          2.065999984741211,
+          2.069000005722046,
+          2.072000026702881,
+          2.075000047683716,
+          2.078000068664551,
+          2.0810000896453857,
+          2.0840001106262207,
+          2.0870001316070557,
+          2.0900001525878906,
+          2.0929999351501465,
+          2.0959999561309814,
+          2.0989999771118164,
+          2.1019999980926514,
+          2.1050000190734863,
+          2.1080000400543213,
+          2.1110000610351562,
+          2.114000082015991,
+          2.117000102996826,
+          2.119999885559082,
+          2.122999906539917,
+          2.125999927520752,
+          2.128999948501587,
+          2.131999969482422,
+          2.134999990463257,
+          2.138000011444092,
+          2.1410000324249268,
+          2.1440000534057617,
+          2.1469998359680176,
+          2.1500000953674316,
+          2.1529998779296875,
+          2.1560001373291016,
+          2.1589999198913574,
+          2.1619999408721924,
+          2.1649999618530273,
+          2.1679999828338623,
+          2.1710000038146973,
+          2.1740000247955322,
+          2.177000045776367,
+          2.180000066757202,
+          2.183000087738037,
+          2.186000108718872,
+          2.188999891281128,
+          2.191999912261963,
+          2.194999933242798,
+          2.197999954223633,
+          2.2009999752044678,
+          2.2039999961853027,
+          2.2070000171661377,
+          2.2100000381469727,
+          2.2130000591278076,
+          2.2160000801086426,
+          2.2190001010894775,
+          2.2220001220703125,
+          2.2249999046325684,
+          2.2280001640319824,
+          2.2309999465942383,
+          2.2339999675750732,
+          2.236999988555908,
+          2.240000009536743,
+          2.243000030517578,
+          2.246000051498413,
+          2.249000072479248,
+          2.252000093460083,
+          2.254999876022339,
+          2.257999897003174,
+          2.260999917984009,
+          2.2639999389648438,
+          2.267000198364258,
+          2.2699999809265137,
+          2.2730000019073486,
+          2.2760000228881836,
+          2.2790000438690186,
+          2.2820000648498535,
+          2.2849998474121094,
+          2.2880001068115234,
+          2.2909998893737793,
+          2.2939999103546143,
+          2.296999931335449,
+          2.299999952316284,
+          2.302999973297119,
+          2.305999994277954,
+          2.309000015258789,
+          2.312000036239624,
+          2.315000057220459,
+          2.317999839782715,
+          2.321000099182129,
+          2.324000120162964,
+          2.3269999027252197,
+          2.3299999237060547,
+          2.3329999446868896,
+          2.3359999656677246,
+          2.3389999866485596,
+          2.3420000076293945,
+          2.3450000286102295,
+          2.3480000495910645,
+          2.3510000705718994,
+          2.3540000915527344,
+          2.3569998741149902,
+          2.3600001335144043,
+          2.36299991607666,
+          2.366000175476074,
+          2.36899995803833,
+          2.371999979019165,
+          2.375,
+          2.378000020980835,
+          2.38100004196167,
+          2.384000062942505,
+          2.38700008392334,
+          2.390000104904175,
+          2.3930001258850098,
+          2.3959999084472656,
+          2.3989999294281006,
+          2.4019999504089355,
+          2.4049999713897705,
+          2.4079999923706055,
+          2.4110000133514404,
+          2.4140000343322754,
+          2.4170000553131104,
+          2.4200000762939453,
+          2.422999858856201,
+          2.4260001182556152,
+          2.428999900817871,
+          2.432000160217285,
+          2.434999942779541,
+          2.437999963760376,
+          2.440999984741211,
+          2.444000005722046,
+          2.447000026702881,
+          2.450000047683716,
+          2.453000068664551,
+          2.4560000896453857,
+          2.4590001106262207,
+          2.4619998931884766,
+          2.4650001525878906,
+          2.4679999351501465,
+          2.4709999561309814,
+          2.4739999771118164,
+          2.4769999980926514,
+          2.4800000190734863,
+          2.4830000400543213,
+          2.4860000610351562,
+          2.489000082015991,
+          2.492000102996826,
+          2.494999885559082,
+          2.498000144958496,
+          2.500999927520752,
+          2.504000186920166,
+          2.506999969482422,
+          2.509999990463257,
+          2.513000011444092,
+          2.5160000324249268,
+          2.5190000534057617,
+          2.5220000743865967,
+          2.5250000953674316,
+          2.5280001163482666,
+          2.5310001373291016,
+          2.5339999198913574,
+          2.5370001792907715,
+          2.5399999618530273,
+          2.5429999828338623,
+          2.5460000038146973,
+          2.5490000247955322,
+          2.552000045776367,
+          2.555000066757202,
+          2.558000087738037,
+          2.561000108718872,
+          2.564000129699707,
+          2.566999912261963,
+          2.570000171661377,
+          2.572999954223633,
+          2.5759999752044678,
+          2.5789999961853027,
+          2.5820000171661377,
+          2.5850000381469727,
+          2.5880000591278076,
+          2.5910000801086426,
+          2.5939998626708984,
+          2.5970001220703125,
+          2.5999999046325684,
+          2.6030001640319824,
+          2.6059999465942383,
+          2.6090002059936523,
+          2.611999988555908,
+          2.615000009536743,
+          2.618000030517578,
+          2.620999813079834,
+          2.624000072479248,
+          2.627000093460083,
+          2.630000114440918,
+          2.632999897003174,
+          2.635999917984009,
+          2.6389999389648438,
+          2.642000198364258,
+          2.6449999809265137,
+          2.6480000019073486,
+          2.6510000228881836,
+          2.6540000438690186,
+          2.6570000648498535,
+          2.6599998474121094,
+          2.6630001068115234,
+          2.6659998893737793,
+          2.6690001487731934,
+          2.671999931335449,
+          2.674999952316284,
+          2.677999973297119,
+          2.681000232696533,
+          2.684000015258789,
+          2.687000036239624,
+          2.690000057220459,
+          2.692999839782715,
+          2.696000099182129,
+          2.6989998817443848,
+          2.702000141143799,
+          2.7049999237060547,
+          2.7080001831054688,
+          2.7109999656677246,
+          2.7139999866485596,
+          2.7170000076293945,
+          2.7200000286102295,
+          2.7230000495910645,
+          2.7260000705718994,
+          2.7290000915527344,
+          2.7319998741149902,
+          2.7350001335144043,
+          2.73799991607666,
+          2.741000175476074,
+          2.74399995803833,
+          2.747000217437744,
+          2.75,
+          2.753000020980835,
+          2.75600004196167,
+          2.759000062942505,
+          2.76200008392334,
+          2.765000104904175,
+          2.7680001258850098,
+          2.7709999084472656,
+          2.7740001678466797,
+          2.7769999504089355,
+          2.7800002098083496,
+          2.7829999923706055,
+          2.7860002517700195,
+          2.7890000343322754,
+          2.7919998168945312,
+          2.7950000762939453,
+          2.797999858856201,
+          2.8010001182556152,
+          2.803999900817871,
+          2.807000160217285,
+          2.809999942779541,
+          2.812999963760376,
+          2.815999984741211,
+          2.819000005722046,
+          2.822000026702881,
+          2.825000047683716,
+          2.828000068664551,
+          2.8309998512268066,
+          2.8340001106262207,
+          2.8369998931884766,
+          2.8400001525878906,
+          2.8429999351501465,
+          2.8460001945495605,
+          2.8489999771118164,
+          2.8519999980926514,
+          2.8550000190734863,
+          2.8580000400543213,
+          2.8610000610351562,
+          2.864000082015991,
+          2.867000102996826,
+          2.869999885559082,
+          2.873000144958496,
+          2.875999927520752,
+          2.879000186920166,
+          2.881999969482422,
+          2.885000228881836,
+          2.888000011444092,
+          2.8909997940063477,
+          2.8940000534057617,
+          2.8969998359680176,
+          2.9000000953674316,
+          2.9029998779296875,
+          2.9060001373291016,
+          2.9089999198913574,
+          2.9120001792907715,
+          2.9149999618530273,
+          2.9180002212524414,
+          2.9210000038146973,
+          2.9240002632141113,
+          2.927000045776367,
+          2.930000066757202,
+          2.933000087738037,
+          2.935999870300293,
+          2.939000129699707,
+          2.941999912261963,
+          2.945000171661377,
+          2.947999954223633,
+          2.9509999752044678,
+          2.9539999961853027,
+          2.9570000171661377,
+          2.9600000381469727,
+          2.9630000591278076,
+          2.9660000801086426,
+          2.9689998626708984,
+          2.9720001220703125,
+          2.9749999046325684,
+          2.9780001640319824,
+          2.9809999465942383,
+          2.9840002059936523,
+          2.986999988555908,
+          2.990000009536743,
+          2.993000030517578,
+          2.996000051498413,
+          2.999000072479248,
+          3.002000093460083,
+          3.005000114440918,
+          3.007999897003174,
+          3.011000156402588,
+          3.0139999389648438,
+          3.017000198364258,
+          3.0199999809265137,
+          3.0230002403259277,
+          3.0260000228881836,
+          3.0290000438690186,
+          3.0320000648498535,
+          3.0349998474121094,
+          3.0380001068115234,
+          3.0409998893737793,
+          3.0440001487731934,
+          3.046999931335449,
+          3.0500001907348633,
+          3.052999973297119,
+          3.056000232696533,
+          3.059000015258789,
+          3.062000274658203,
+          3.065000057220459,
+          3.068000078201294,
+          3.071000099182129,
+          3.074000120162964,
+          3.077000141143799,
+          3.0799999237060547,
+          3.0830001831054688,
+          3.0859999656677246,
+          3.0889999866485596,
+          3.0920000076293945,
+          3.0950000286102295,
+          3.0980000495910645,
+          3.1010000705718994,
+          3.1040000915527344,
+          3.1069998741149902,
+          3.1100001335144043,
+          3.11299991607666,
+          3.116000175476074,
+          3.11899995803833,
+          3.122000217437744,
+          3.125,
+          3.128000020980835,
+          3.13100004196167,
+          3.134000062942505,
+          3.13700008392334,
+          3.140000104904175,
+          3.1430001258850098,
+          3.1459999084472656,
+          3.1490001678466797,
+          3.1519999504089355,
+          3.1549999713897705,
+          3.1579999923706055,
+          3.1610000133514404,
+          3.1640000343322754,
+          3.1669998168945312,
+          3.1700000762939453,
+          3.172999858856201,
+          3.1760001182556152,
+          3.178999900817871,
+          3.182000160217285,
+          3.184999942779541,
+          3.188000202178955,
+          3.190999984741211,
+          3.194000244140625,
+          3.197000026702881,
+          3.200000047683716,
+          3.2029998302459717,
+          3.2059998512268066,
+          3.2089998722076416,
+          3.2119998931884766,
+          3.2150001525878906,
+          3.2180001735687256,
+          3.2210001945495605,
+          3.2240002155303955,
+          3.2269999980926514,
+          3.2300000190734863,
+          3.2330000400543213,
+          3.2360000610351562,
+          3.239000082015991,
+          3.242000102996826,
+          3.244999885559082,
+          3.247999906539917,
+          3.250999927520752,
+          3.254000186920166,
+          3.256999969482422,
+          3.260000228881836,
+          3.263000011444092,
+          3.2660000324249268,
+          3.2690000534057617,
+          3.2720000743865967,
+          3.2750000953674316,
+          3.2780001163482666,
+          3.2809998989105225,
+          3.2839999198913574,
+          3.2869999408721924,
+          3.2899999618530273,
+          3.2930002212524414,
+          3.2960000038146973,
+          3.2990000247955322,
+          3.302000045776367,
+          3.304999828338623,
+          3.308000087738037,
+          3.310999870300293,
+          3.314000129699707,
+          3.317000150680542,
+          3.319999933242798,
+          3.323000192642212,
+          3.3259999752044678,
+          3.3289999961853027,
+          3.332000255584717,
+          3.3350000381469727,
+          3.3380002975463867,
+          3.3410000801086426,
+          3.3439998626708984,
+          3.3469998836517334,
+          3.3499999046325684,
+          3.3530001640319824,
+          3.3560001850128174,
+          3.3590002059936523,
+          3.3620002269744873,
+          3.365000009536743,
+          3.368000030517578,
+          3.371000051498413,
+          3.374000072479248,
+          3.377000093460083,
+          3.380000114440918,
+          3.382999897003174,
+          3.386000156402588,
+          3.3889999389648438,
+          3.392000198364258,
+          3.3949999809265137,
+          3.3980002403259277,
+          3.4010000228881836,
+          3.4040000438690186,
+          3.4070000648498535,
+          3.4100000858306885,
+          3.4130001068115234,
+          3.4160001277923584,
+          3.4189999103546143,
+          3.421999931335449,
+          3.424999952316284,
+          3.427999973297119,
+          3.431000232696533,
+          3.434000253677368,
+          3.437000274658203,
+          3.440000057220459,
+          3.442999839782715,
+          3.446000099182129,
+          3.4489998817443848,
+          3.452000141143799,
+          3.455000162124634,
+          3.4579999446868896,
+          3.4610002040863037,
+          3.4639999866485596,
+          3.4670000076293945,
+          3.4700002670288086,
+          3.4730000495910645,
+          3.4760003089904785,
+          3.4790000915527344,
+          3.4820001125335693,
+          3.4850001335144043,
+          3.48799991607666,
+          3.491000175476074,
+          3.494000196456909,
+          3.497000217437744,
+          3.5,
+          3.503000020980835,
+          3.50600004196167,
+          3.509000062942505,
+          3.5119998455047607,
+          3.5149998664855957,
+          3.5179998874664307,
+          3.5209999084472656,
+          3.5239996910095215,
+          3.5269997119903564,
+          3.5299997329711914,
+          3.5329999923706055,
+          3.5360000133514404,
+          3.5390000343322754,
+          3.5419998168945312,
+          3.544999837875366,
+          3.547999858856201,
+          3.550999879837036,
+          3.553999900817871,
+          3.557000160217285,
+          3.559999942779541,
+          3.562999725341797,
+          3.565999746322632,
+          3.569000005722046,
+          3.572000026702881,
+          3.575000047683716,
+          3.578000068664551,
+          3.5809998512268066,
+          3.5839998722076416,
+          3.5869998931884766,
+          3.5899999141693115,
+          3.5930001735687256,
+          3.5960001945495605,
+          3.5989999771118164,
+          3.6019997596740723,
+          3.6050000190734863,
+          3.607999801635742,
+          3.610999822616577,
+          3.613999843597412,
+          3.617000102996826,
+          3.619999885559082,
+          3.622999668121338,
+          3.625999689102173,
+          3.628999948501587,
+          3.631999969482422,
+          3.634999990463257,
+          3.638000011444092,
+          3.6409997940063477,
+          3.6439998149871826,
+          3.6469998359680176,
+          3.6499998569488525,
+          3.6530001163482666,
+          3.6560001373291016,
+          3.6589999198913574,
+          3.6619997024536133,
+          3.6649999618530273,
+          3.6679999828338623,
+          3.6710000038146973,
+          3.6740000247955322,
+          3.677000045776367,
+          3.679999828338623,
+          3.682999849319458,
+          3.685999870300293,
+          3.689000129699707,
+          3.692000150680542,
+          3.695000171661377,
+          3.697999954223633,
+          3.7009999752044678,
+          3.7039997577667236,
+          3.7069997787475586,
+          3.7099997997283936,
+          3.7130000591278076,
+          3.7160000801086426,
+          3.7189998626708984,
+          3.7219996452331543,
+          3.7249999046325684,
+          3.7279999256134033,
+          3.7309999465942383,
+          3.7339999675750732,
+          3.736999988555908,
+          3.739999771118164,
+          3.742999792098999,
+          3.745999813079834,
+          3.749000072479248,
+          3.752000093460083,
+          3.755000114440918,
+          3.757999897003174,
+          3.760999917984009,
+          3.7639999389648438,
+          3.7669999599456787,
+          3.7699999809265137,
+          3.7730002403259277,
+          3.7760000228881836,
+          3.7789998054504395,
+          3.7819998264312744,
+          3.7850000858306885,
+          3.7880001068115234,
+          3.7910001277923584,
+          3.7940001487731934,
+          3.796999931335449,
+          3.799999713897705,
+          3.80299973487854,
+          3.805999755859375,
+          3.809000015258789,
+          3.812000036239624,
+          3.815000057220459,
+          3.817999839782715,
+          3.82099986076355,
+          3.8239998817443848,
+          3.8269999027252197,
+          3.8299999237060547,
+          3.8330001831054688,
+          3.8359999656677246,
+          3.8389997482299805,
+          3.8419997692108154,
+          3.8450000286102295,
+          3.8480000495910645,
+          3.8510000705718994,
+          3.8540000915527344,
+          3.8569998741149902,
+          3.859999895095825,
+          3.86299991607666,
+          3.865999937057495,
+          3.869000196456909,
+          3.871999979019165,
+          3.875,
+          3.877999782562256,
+          3.880999803543091,
+          3.883999824523926,
+          3.8869998455047607,
+          3.8899998664855957,
+          3.8930001258850098,
+          3.8959999084472656,
+          3.8989996910095215,
+          3.9019997119903564,
+          3.9049999713897705,
+          3.9079999923706055,
+          3.9110000133514404,
+          3.9140000343322754,
+          3.9169998168945312,
+          3.919999837875366,
+          3.922999858856201,
+          3.925999879837036,
+          3.92900013923645,
+          3.932000160217285,
+          3.934999942779541,
+          3.937999725341797,
+          3.940999984741211,
+          3.944000005722046,
+          3.947000026702881,
+          3.950000047683716,
+          3.953000068664551,
+          3.9559998512268066,
+          3.9589998722076416,
+          3.9619998931884766,
+          3.9650001525878906,
+          3.9679999351501465,
+          3.9709999561309814,
+          3.9739999771118164,
+          3.9769997596740723,
+          3.9799997806549072,
+          3.982999801635742,
+          3.985999822616577,
+          3.989000082015991,
+          3.992000102996826,
+          3.994999885559082,
+          3.997999668121338,
+          4.000999927520752,
+          4.003999710083008,
+          4.006999969482422,
+          4.010000228881836,
+          4.013000011444092,
+          4.015999794006348,
+          4.019000053405762,
+          4.021999835968018,
+          4.025000095367432,
+          4.0279998779296875,
+          4.031000137329102,
+          4.033999919891357,
+          4.036999702453613,
+          4.039999961853027,
+          4.043000221252441,
+          4.046000003814697,
+          4.049000263214111,
+          4.052000045776367,
+          4.054999828338623,
+          4.057999610900879,
+          4.060999870300293,
+          4.064000129699707,
+          4.066999912261963,
+          4.069999694824219,
+          4.072999954223633,
+          4.075999736785889,
+          4.0789995193481445,
+          4.081999778747559,
+          4.085000038146973,
+          4.088000297546387,
+          4.091000080108643,
+          4.093999862670898,
+          4.0970001220703125,
+          4.099999904632568,
+          4.102999687194824,
+          4.105999946594238,
+          4.109000205993652,
+          4.111999988555908,
+          4.114999771118164,
+          4.118000030517578,
+          4.121000289916992,
+          4.124000072479248,
+          4.126999855041504,
+          4.130000114440918,
+          4.132999897003174,
+          4.13599967956543,
+          4.138999938964844,
+          4.142000198364258,
+          4.145000457763672,
+          4.148000240325928,
+          4.151000022888184,
+          4.1539998054504395,
+          4.1570000648498535,
+          4.159999847412109,
+          4.163000106811523,
+          4.165999889373779,
+          4.169000148773193,
+          4.171999931335449,
+          4.174999713897705,
+          4.177999496459961,
+          4.180999755859375,
+          4.184000015258789,
+          4.187000274658203,
+          4.190000057220459,
+          4.192999839782715,
+          4.196000099182129,
+          4.198999881744385,
+          4.201999664306641,
+          4.204999923706055,
+          4.208000183105469,
+          4.210999965667725,
+          4.2139997482299805,
+          4.2170000076293945,
+          4.220000267028809,
+          4.2230000495910645,
+          4.22599983215332,
+          4.229000091552734,
+          4.23199987411499,
+          4.234999656677246,
+          4.23799991607666,
+          4.241000175476074,
+          4.24399995803833,
+          4.246999740600586,
+          4.25,
+          4.252999782562256,
+          4.255999565124512,
+          4.258999824523926,
+          4.26200008392334,
+          4.265000343322754,
+          4.26800012588501,
+          4.270999908447266,
+          4.2739996910095215,
+          4.2769999504089355,
+          4.279999732971191,
+          4.2829999923706055,
+          4.2860002517700195,
+          4.289000034332275,
+          4.291999816894531,
+          4.295000076293945,
+          4.297999858856201,
+          4.301000118255615,
+          4.303999900817871,
+          4.307000160217285,
+          4.309999942779541,
+          4.312999725341797,
+          4.315999984741211,
+          4.319000244140625,
+          4.322000026702881,
+          4.325000286102295,
+          4.328000068664551,
+          4.330999851226807,
+          4.3339996337890625,
+          4.336999893188477,
+          4.340000152587891,
+          4.3429999351501465,
+          4.345999717712402,
+          4.348999977111816,
+          4.351999759674072,
+          4.354999542236328,
+          4.357999801635742,
+          4.361000061035156,
+          4.36400032043457,
+          4.367000102996826,
+          4.369999885559082,
+          4.373000144958496,
+          4.375999927520752,
+          4.378999710083008,
+          4.381999969482422,
+          4.385000228881836,
+          4.388000011444092,
+          4.390999794006348,
+          4.394000053405762,
+          4.397000312805176,
+          4.400000095367432,
+          4.4029998779296875,
+          4.405999660491943,
+          4.409000396728516,
+          4.411999702453613,
+          4.414999961853027,
+          4.417999744415283,
+          4.4210004806518555,
+          4.423999786376953,
+          4.426999568939209,
+          4.429999828338623,
+          4.432999610900879,
+          4.435999870300293,
+          4.438999652862549,
+          4.441999912261963,
+          4.444999694824219,
+          4.447999954223633,
+          4.451000213623047,
+          4.4539995193481445,
+          4.457000255584717,
+          4.460000038146973,
+          4.463000297546387,
+          4.465999603271484,
+          4.469000339508057,
+          4.4720001220703125,
+          4.474999904632568,
+          4.478000164031982,
+          4.480999946594238,
+          4.483999729156494,
+          4.486999988555908,
+          4.490000247955322,
+          4.493000030517578,
+          4.495999813079834,
+          4.499000072479248,
+          4.501999855041504,
+          4.505000114440918,
+          4.508000373840332,
+          4.51099967956543,
+          4.513999938964844,
+          4.517000198364258,
+          4.519999980926514,
+          4.5229997634887695,
+          4.525999546051025,
+          4.529000282287598,
+          4.531999588012695,
+          4.534999847412109,
+          4.537999629974365,
+          4.5410003662109375,
+          4.543999671936035,
+          4.546999931335449,
+          4.550000190734863,
+          4.552999973297119,
+          4.556000232696533,
+          4.559000015258789,
+          4.562000274658203,
+          4.565000057220459,
+          4.568000316619873,
+          4.571000099182129,
+          4.573999881744385,
+          4.577000141143799,
+          4.579999923706055,
+          4.5829997062683105,
+          4.585999965667725,
+          4.5889997482299805,
+          4.5920000076293945,
+          4.59499979019165,
+          4.5980000495910645,
+          4.60099983215332,
+          4.604000091552734,
+          4.607000350952148,
+          4.609999656677246,
+          4.613000392913818,
+          4.615999698638916,
+          4.61899995803833,
+          4.621999740600586,
+          4.625,
+          4.628000259399414,
+          4.630999565124512,
+          4.633999824523926,
+          4.63700008392334,
+          4.640000343322754,
+          4.642999649047852,
+          4.645999908447266,
+          4.64900016784668,
+          4.6519999504089355,
+          4.65500020980835,
+          4.6579999923706055,
+          4.660999774932861,
+          4.664000034332275,
+          4.6670002937316895,
+          4.670000076293945,
+          4.672999858856201,
+          4.676000118255615,
+          4.678999900817871,
+          4.681999683380127,
+          4.685000419616699,
+          4.688000202178955,
+          4.690999984741211,
+          4.693999767303467,
+          4.697000026702881,
+          4.699999809265137,
+          4.702999591827393,
+          4.705999851226807,
+          4.7089996337890625,
+          4.711999893188477,
+          4.714999675750732,
+          4.7179999351501465,
+          4.720999717712402,
+          4.723999977111816,
+          4.7270002365112305,
+          4.729999542236328,
+          4.7330002784729,
+          4.736000061035156,
+          4.73900032043457,
+          4.741999626159668,
+          4.74500036239624,
+          4.748000144958496,
+          4.750999927520752,
+          4.754000186920166,
+          4.756999969482422,
+          4.759999752044678,
+          4.763000011444092,
+          4.766000270843506,
+          4.769000053405762,
+          4.771999835968018,
+          4.775000095367432,
+          4.7779998779296875,
+          4.781000137329102,
+          4.783999919891357,
+          4.7870001792907715,
+          4.789999961853027,
+          4.792999744415283,
+          4.796000003814697,
+          4.798999786376953,
+          4.801999568939209,
+          4.805000305175781,
+          4.807999610900879,
+          4.810999870300293,
+          4.813999652862549,
+          4.817000389099121,
+          4.819999694824219,
+          4.822999954223633,
+          4.826000213623047,
+          4.828999996185303,
+          4.832000255584717,
+          4.835000038146973,
+          4.838000297546387,
+          4.841000080108643,
+          4.844000339508057,
+          4.8470001220703125,
+          4.849999904632568,
+          4.853000164031982,
+          4.855999946594238,
+          4.858999729156494,
+          4.861999988555908,
+          4.864999771118164,
+          4.868000030517578,
+          4.870999813079834,
+          4.874000072479248,
+          4.876999855041504,
+          4.87999963760376,
+          4.882999897003174,
+          4.886000156402588,
+          4.888999938964844,
+          4.8919997215271,
+          4.894999980926514,
+          4.8979997634887695,
+          4.901000022888184,
+          4.904000282287598,
+          4.906999588012695,
+          4.909999847412109,
+          4.913000106811523,
+          4.9160003662109375,
+          4.918999671936035,
+          4.921999931335449,
+          4.925000190734863,
+          4.927999973297119,
+          4.931000232696533,
+          4.934000015258789,
+          4.936999797821045,
+          4.940000057220459,
+          4.943000316619873,
+          4.946000099182129,
+          4.948999881744385,
+          4.952000141143799,
+          4.954999923706055,
+          4.9579997062683105,
+          4.961000442504883,
+          4.9639997482299805,
+          4.9670000076293945,
+          4.96999979019165,
+          4.973000526428223,
+          4.97599983215332,
+          4.978999614715576,
+          4.982000350952148,
+          4.984999656677246,
+          4.98799991607666,
+          4.990999698638916,
+          4.994000434875488,
+          4.996999740600586
+         ],
+         "xaxis": "x",
+         "yaxis": "y"
+        },
+        {
+         "nbinsx": 50,
+         "opacity": 0.7,
+         "type": "histogram",
+         "x": [
+          2,
+          2.0012006759643555,
+          2.0024027824401855,
+          2.0036065578460693,
+          2.0048115253448486,
+          2.0060181617736816,
+          2.00722599029541,
+          2.0084354877471924,
+          2.009646415710449,
+          2.0108585357666016,
+          2.0120723247528076,
+          2.0132875442504883,
+          2.0145044326782227,
+          2.0157225131988525,
+          2.016942262649536,
+          2.0181634426116943,
+          2.019386053085327,
+          2.0206103324890137,
+          2.0218358039855957,
+          2.0230629444122314,
+          2.024291515350342,
+          2.0255215167999268,
+          2.0267531871795654,
+          2.0279862880706787,
+          2.0292208194732666,
+          2.030456781387329,
+          2.0316944122314453,
+          2.032933473587036,
+          2.0341742038726807,
+          2.0354161262512207,
+          2.0366597175598145,
+          2.037905216217041,
+          2.039151668548584,
+          2.0404000282287598,
+          2.041649580001831,
+          2.042901039123535,
+          2.0441536903381348,
+          2.045408010482788,
+          2.046663761138916,
+          2.0479211807250977,
+          2.049180507659912,
+          2.050440788269043,
+          2.0517029762268066,
+          2.052966594696045,
+          2.054231882095337,
+          2.0554983615875244,
+          2.0567667484283447,
+          2.0580365657806396,
+          2.0593080520629883,
+          2.0605812072753906,
+          2.0618557929992676,
+          2.063131809234619,
+          2.0644094944000244,
+          2.0656888484954834,
+          2.066969871520996,
+          2.0682523250579834,
+          2.0695364475250244,
+          2.07082200050354,
+          2.0721094608306885,
+          2.0733983516693115,
+          2.0746889114379883,
+          2.0759809017181396,
+          2.0772745609283447,
+          2.0785698890686035,
+          2.079866886138916,
+          2.081165313720703,
+          2.082465648651123,
+          2.0837676525115967,
+          2.085071086883545,
+          2.0863759517669678,
+          2.0876827239990234,
+          2.0889909267425537,
+          2.090301036834717,
+          2.0916125774383545,
+          2.092925786972046,
+          2.094240665435791,
+          2.095557451248169,
+          2.0968756675720215,
+          2.0981955528259277,
+          2.0995171070098877,
+          2.1008403301239014,
+          2.1021652221679688,
+          2.10349178314209,
+          2.1048200130462646,
+          2.106149911880493,
+          2.1074817180633545,
+          2.1088149547576904,
+          2.11014986038208,
+          2.1114864349365234,
+          2.1128249168395996,
+          2.1141650676727295,
+          2.115506649017334,
+          2.1168501377105713,
+          2.1181952953338623,
+          2.119542121887207,
+          2.1208908557891846,
+          2.1222410202026367,
+          2.1235930919647217,
+          2.1249468326568604,
+          2.1263022422790527,
+          2.127659559249878,
+          2.129018545150757,
+          2.1303791999816895,
+          2.131741523742676,
+          2.133105754852295,
+          2.1344716548919678,
+          2.1358394622802734,
+          2.1372087001800537,
+          2.138579845428467,
+          2.1399528980255127,
+          2.1413276195526123,
+          2.1427042484283447,
+          2.1440823078155518,
+          2.1454625129699707,
+          2.1468441486358643,
+          2.1482276916503906,
+          2.14961314201355,
+          2.1510000228881836,
+          2.1523890495300293,
+          2.153779983520508,
+          2.15517258644104,
+          2.156566858291626,
+          2.1579627990722656,
+          2.159360885620117,
+          2.1607606410980225,
+          2.1621620655059814,
+          2.1635653972625732,
+          2.164970636367798,
+          2.1663777828216553,
+          2.1677868366241455,
+          2.1691973209381104,
+          2.170609951019287,
+          2.1720242500305176,
+          2.17344069480896,
+          2.174858570098877,
+          2.176278591156006,
+          2.1777002811431885,
+          2.179124116897583,
+          2.1805496215820312,
+          2.181976795196533,
+          2.183406114578247,
+          2.1848373413085938,
+          2.186270236968994,
+          2.1877050399780273,
+          2.1891417503356934,
+          2.190580368041992,
+          2.192021131515503,
+          2.1934635639190674,
+          2.1949079036712646,
+          2.1963541507720947,
+          2.1978020668029785,
+          2.1992523670196533,
+          2.2007040977478027,
+          2.202157974243164,
+          2.2036139965057373,
+          2.2050716876983643,
+          2.206531524658203,
+          2.2079930305480957,
+          2.209456443786621,
+          2.2109220027923584,
+          2.2123894691467285,
+          2.2138588428497314,
+          2.215330123901367,
+          2.2168033123016357,
+          2.218278646469116,
+          2.2197558879852295,
+          2.2212350368499756,
+          2.2227160930633545,
+          2.2241992950439453,
+          2.225684404373169,
+          2.2271714210510254,
+          2.2286605834960938,
+          2.230151653289795,
+          2.231644630432129,
+          2.233139991760254,
+          2.2346370220184326,
+          2.236135959625244,
+          2.2376370429992676,
+          2.239140272140503,
+          2.240645408630371,
+          2.242152452468872,
+          2.243661642074585,
+          2.2451729774475098,
+          2.2466862201690674,
+          2.248201608657837,
+          2.2497189044952393,
+          2.2512381076812744,
+          2.2527596950531006,
+          2.2542831897735596,
+          2.2558085918426514,
+          2.257336139678955,
+          2.25886607170105,
+          2.2603979110717773,
+          2.2619316577911377,
+          2.26346755027771,
+          2.265005588531494,
+          2.2665457725524902,
+          2.2680881023406982,
+          2.269632339477539,
+          2.271178722381592,
+          2.2727272510528564,
+          2.274277925491333,
+          2.2758307456970215,
+          2.2773854732513428,
+          2.278942584991455,
+          2.2805016040802,
+          2.2820627689361572,
+          2.2836263179779053,
+          2.2851920127868652,
+          2.286759614944458,
+          2.288329601287842,
+          2.2899014949798584,
+          2.291475772857666,
+          2.2930519580841064,
+          2.294630527496338,
+          2.2962112426757812,
+          2.2977943420410156,
+          2.299379348754883,
+          2.300966501235962,
+          2.302555799484253,
+          2.304147481918335,
+          2.305741310119629,
+          2.3073372840881348,
+          2.3089354038238525,
+          2.3105359077453613,
+          2.312138795852661,
+          2.313743829727173,
+          2.3153507709503174,
+          2.316960096359253,
+          2.3185715675354004,
+          2.320185661315918,
+          2.3218016624450684,
+          2.3234200477600098,
+          2.325040817260742,
+          2.3266637325286865,
+          2.3282887935638428,
+          2.32991623878479,
+          2.331545829772949,
+          2.3331778049468994,
+          2.3348121643066406,
+          2.3364486694335938,
+          2.338087320327759,
+          2.339728355407715,
+          2.341372013092041,
+          2.343017816543579,
+          2.344666004180908,
+          2.346316337585449,
+          2.3479690551757812,
+          2.3496241569519043,
+          2.3512814044952393,
+          2.3529410362243652,
+          2.3546032905578613,
+          2.3562676906585693,
+          2.3579344749450684,
+          2.3596036434173584,
+          2.3612751960754395,
+          2.3629491329193115,
+          2.3646249771118164,
+          2.3663039207458496,
+          2.3679847717285156,
+          2.3696682453155518,
+          2.371354103088379,
+          2.373042345046997,
+          2.3747329711914062,
+          2.3764257431030273,
+          2.3781211376190186,
+          2.37981915473938,
+          2.381519317626953,
+          2.3832221031188965,
+          2.384927272796631,
+          2.3866348266601562,
+          2.3883450031280518,
+          2.390057325363159,
+          2.3917722702026367,
+          2.3934898376464844,
+          2.395209550857544,
+          2.3969318866729736,
+          2.3986566066741943,
+          2.400383949279785,
+          2.402114152908325,
+          2.403846025466919,
+          2.405580997467041,
+          2.407318353652954,
+          2.409058094024658,
+          2.4108004570007324,
+          2.4125449657440186,
+          2.414292573928833,
+          2.4160425662994385,
+          2.417794942855835,
+          2.4195501804351807,
+          2.4213075637817383,
+          2.423067569732666,
+          2.424830436706543,
+          2.426595449447632,
+          2.42836332321167,
+          2.430133581161499,
+          2.4319067001342773,
+          2.4336822032928467,
+          2.435460329055786,
+          2.4372410774230957,
+          2.4390244483947754,
+          2.440810203552246,
+          2.442599058151245,
+          2.444390058517456,
+          2.446183919906616,
+          2.4479804039001465,
+          2.449779510498047,
+          2.4515814781188965,
+          2.453385829925537,
+          2.455192804336548,
+          2.457002639770508,
+          2.458814859390259,
+          2.460629940032959,
+          2.46244740486145,
+          2.464268207550049,
+          2.4660913944244385,
+          2.467916965484619,
+          2.469745635986328,
+          2.4715769290924072,
+          2.4734108448028564,
+          2.475247621536255,
+          2.4770867824554443,
+          2.478929042816162,
+          2.48077392578125,
+          2.482621431350708,
+          2.4844722747802734,
+          2.486325263977051,
+          2.4881811141967773,
+          2.490039825439453,
+          2.491901159286499,
+          2.4937655925750732,
+          2.4956324100494385,
+          2.497502565383911,
+          2.499375104904175,
+          2.5012505054473877,
+          2.503129005432129,
+          2.5050101280212402,
+          2.5068938732147217,
+          2.5087809562683105,
+          2.5106701850891113,
+          2.5125627517700195,
+          2.514457941055298,
+          2.5163562297821045,
+          2.5182576179504395,
+          2.5201611518859863,
+          2.5220682621002197,
+          2.523977756500244,
+          2.525890350341797,
+          2.527805805206299,
+          2.52972412109375,
+          2.5316457748413086,
+          2.533569812774658,
+          2.535496950149536,
+          2.5374271869659424,
+          2.5393600463867188,
+          2.5412960052490234,
+          2.5432350635528564,
+          2.5451767444610596,
+          2.54712176322937,
+          2.54906964302063,
+          2.551020383834839,
+          2.552974224090576,
+          2.5549309253692627,
+          2.5568909645080566,
+          2.5588536262512207,
+          2.560819387435913,
+          2.562788486480713,
+          2.5647599697113037,
+          2.56673526763916,
+          2.5687131881713867,
+          2.5706942081451416,
+          2.572678327560425,
+          2.5746653079986572,
+          2.576655626296997,
+          2.5786490440368652,
+          2.5806450843811035,
+          2.5826447010040283,
+          2.5846471786499023,
+          2.586652994155884,
+          2.5886616706848145,
+          2.5906734466552734,
+          2.592688798904419,
+          2.5947067737579346,
+          2.5967280864715576,
+          2.598752498626709,
+          2.6007802486419678,
+          2.602811098098755,
+          2.604844808578491,
+          2.606882095336914,
+          2.6089224815368652,
+          2.6109659671783447,
+          2.6130127906799316,
+          2.615062713623047,
+          2.6171159744262695,
+          2.6191723346710205,
+          2.621232032775879,
+          2.6232950687408447,
+          2.6253609657287598,
+          2.6274304389953613,
+          2.629503011703491,
+          2.6315786838531494,
+          2.6336581707000732,
+          2.6357405185699463,
+          2.637826442718506,
+          2.639915704727173,
+          2.642007827758789,
+          2.644103765487671,
+          2.646202564239502,
+          2.6483049392700195,
+          2.6504108905792236,
+          2.652519941329956,
+          2.654632329940796,
+          2.6567482948303223,
+          2.658867359161377,
+          2.660989999771118,
+          2.663115978240967,
+          2.665245294570923,
+          2.6673779487609863,
+          2.6695139408111572,
+          2.6716537475585938,
+          2.6737966537475586,
+          2.675943374633789,
+          2.678093433380127,
+          2.680246591567993,
+          2.682403564453125,
+          2.684563636779785,
+          2.686727523803711,
+          2.6888949871063232,
+          2.691065549850464,
+          2.693240165710449,
+          2.695417881011963,
+          2.697599172592163,
+          2.69978404045105,
+          2.701972246170044,
+          2.704164505004883,
+          2.70635986328125,
+          2.7085587978363037,
+          2.710761785507202,
+          2.712967872619629,
+          2.7151780128479004,
+          2.7173914909362793,
+          2.7196083068847656,
+          2.7218291759490967,
+          2.724053382873535,
+          2.72628116607666,
+          2.72851300239563,
+          2.730748176574707,
+          2.732987403869629,
+          2.735229730606079,
+          2.737476110458374,
+          2.7397260665893555,
+          2.7419795989990234,
+          2.744237184524536,
+          2.7464981079101562,
+          2.748762845993042,
+          2.7510316371917725,
+          2.7533040046691895,
+          2.755580186843872,
+          2.7578601837158203,
+          2.760143518447876,
+          2.7624311447143555,
+          2.7647221088409424,
+          2.767017126083374,
+          2.7693159580230713,
+          2.771618366241455,
+          2.773925304412842,
+          2.776235580444336,
+          2.7785496711730957,
+          2.7808678150177,
+          2.783189296722412,
+          2.785515308380127,
+          2.7878448963165283,
+          2.7901785373687744,
+          2.792515993118286,
+          2.7948575019836426,
+          2.7972028255462646,
+          2.7995524406433105,
+          2.8019051551818848,
+          2.804262638092041,
+          2.806623697280884,
+          2.8089888095855713,
+          2.8113577365875244,
+          2.8137307167053223,
+          2.816108226776123,
+          2.8184893131256104,
+          2.8208744525909424,
+          2.8232638835906982,
+          2.8256568908691406,
+          2.828054428100586,
+          2.830455780029297,
+          2.8328611850738525,
+          2.835270881652832,
+          2.837684392929077,
+          2.840102434158325,
+          2.842524528503418,
+          2.8449501991271973,
+          2.8473806381225586,
+          2.8498146533966064,
+          2.8522531986236572,
+          2.8546957969665527,
+          2.857142925262451,
+          2.8595941066741943,
+          2.8620493412017822,
+          2.864508867263794,
+          2.8669724464416504,
+          2.8694403171539307,
+          2.8719124794006348,
+          2.874389171600342,
+          2.8768699169158936,
+          2.879354953765869,
+          2.8818440437316895,
+          2.884337902069092,
+          2.886835813522339,
+          2.8893380165100098,
+          2.8918449878692627,
+          2.8943560123443604,
+          2.896871328353882,
+          2.899390935897827,
+          2.9019150733947754,
+          2.9044437408447266,
+          2.9069766998291016,
+          2.9095139503479004,
+          2.9120559692382812,
+          2.914602041244507,
+          2.9171526432037354,
+          2.919708013534546,
+          2.9222676753997803,
+          2.9248318672180176,
+          2.9274003505706787,
+          2.929973602294922,
+          2.932551145553589,
+          2.935133457183838,
+          2.93772029876709,
+          2.9403116703033447,
+          2.9429075717926025,
+          2.9455080032348633,
+          2.948112964630127,
+          2.9507226943969727,
+          2.9533369541168213,
+          2.955955982208252,
+          2.9585797786712646,
+          2.9612081050872803,
+          2.963840961456299,
+          2.9664788246154785,
+          2.969120979309082,
+          2.9717681407928467,
+          2.9744198322296143,
+          2.977076530456543,
+          2.9797375202178955,
+          2.982403516769409,
+          2.985074520111084,
+          2.987750291824341,
+          2.9904305934906006,
+          2.9931156635284424,
+          2.9958057403564453,
+          2.9985008239746094,
+          3.0012004375457764,
+          3.0039048194885254,
+          3.0066146850585938,
+          3.009328842163086,
+          3.0120480060577393,
+          3.014772415161133,
+          3.0175013542175293,
+          3.020235776901245,
+          3.022974729537964,
+          3.0257184505462646,
+          3.0284674167633057,
+          3.031221628189087,
+          3.03398060798645,
+          3.0367443561553955,
+          3.039513349533081,
+          3.042287826538086,
+          3.0450668334960938,
+          3.047851085662842,
+          3.050640344619751,
+          3.0534350872039795,
+          3.056234836578369,
+          3.059039354324341,
+          3.0618491172790527,
+          3.064664363861084,
+          3.0674846172332764,
+          3.07030987739563,
+          3.0731406211853027,
+          3.075976610183716,
+          3.07881760597229,
+          3.0816638469696045,
+          3.0845155715942383,
+          3.0873727798461914,
+          3.0902349948883057,
+          3.09310245513916,
+          3.095975160598755,
+          3.098853588104248,
+          3.1017370223999023,
+          3.104625701904297,
+          3.107520341873169,
+          3.110419988632202,
+          3.1133248805999756,
+          3.1162357330322266,
+          3.1191515922546387,
+          3.122073173522949,
+          3.124999761581421,
+          3.12793231010437,
+          3.1308703422546387,
+          3.1338136196136475,
+          3.136762857437134,
+          3.1397173404693604,
+          3.1426773071289062,
+          3.1456432342529297,
+          3.1486144065856934,
+          3.1515913009643555,
+          3.154573917388916,
+          3.157562255859375,
+          3.1605563163757324,
+          3.163555860519409,
+          3.1665608882904053,
+          3.169572114944458,
+          3.17258882522583,
+          3.1756112575531006,
+          3.1786394119262695,
+          3.181673526763916,
+          3.184713363647461,
+          3.187758684158325,
+          3.190810441970825,
+          3.1938679218292236,
+          3.1969306468963623,
+          3.1999998092651367,
+          3.2030749320983887,
+          3.20615553855896,
+          3.209242343902588,
+          3.2123348712921143,
+          3.2154338359832764,
+          3.218538761138916,
+          3.221649408340454,
+          3.2247660160064697,
+          3.227888584136963,
+          3.231017589569092,
+          3.2341527938842773,
+          3.2372937202453613,
+          3.240440607070923,
+          3.243593692779541,
+          3.246752977371216,
+          3.2499186992645264,
+          3.2530901432037354,
+          3.256268262863159,
+          3.2594523429870605,
+          3.2626426219940186,
+          3.265839099884033,
+          3.269042491912842,
+          3.2722513675689697,
+          3.2754666805267334,
+          3.278688430786133,
+          3.281916379928589,
+          3.285151243209839,
+          3.2883918285369873,
+          3.2916393280029297,
+          3.2948930263519287,
+          3.2981529235839844,
+          3.301419496536255,
+          3.304692268371582,
+          3.3079721927642822,
+          3.31125807762146,
+          3.3145508766174316,
+          3.31784987449646,
+          3.321155548095703,
+          3.324467897415161,
+          3.327786684036255,
+          3.3311123847961426,
+          3.334444761276245,
+          3.3377835750579834,
+          3.3411290645599365,
+          3.3444812297821045,
+          3.3478405475616455,
+          3.3512065410614014,
+          3.354578971862793,
+          3.3579583168029785,
+          3.361344337463379,
+          3.3647372722625732,
+          3.3681373596191406,
+          3.371544122695923,
+          3.374957799911499,
+          3.378378391265869,
+          3.381805658340454,
+          3.385240077972412,
+          3.3886821269989014,
+          3.3921303749084473,
+          3.395585775375366,
+          3.399048328399658,
+          3.402517795562744,
+          3.405994176864624,
+          3.409477949142456,
+          3.412968873977661,
+          3.4164674282073975,
+          3.4199726581573486,
+          3.423485040664673,
+          3.42700457572937,
+          3.4305317401885986,
+          3.434065818786621,
+          3.437607526779175,
+          3.4411561489105225,
+          3.4447121620178223,
+          3.448275566101074,
+          3.4518463611602783,
+          3.4554247856140137,
+          3.4590108394622803,
+          3.462603807449341,
+          3.4662044048309326,
+          3.4698123931884766,
+          3.473428249359131,
+          3.4770517349243164,
+          3.480682373046875,
+          3.484320640563965,
+          3.487966299057007,
+          3.491619825363159,
+          3.495281457901001,
+          3.498950242996216,
+          3.50262713432312,
+          3.5063114166259766,
+          3.5100033283233643,
+          3.5137031078338623,
+          3.517411470413208,
+          3.5211267471313477,
+          3.5248498916625977,
+          3.5285816192626953,
+          3.532320737838745,
+          3.5360677242279053,
+          3.539822578430176,
+          3.5435855388641357,
+          3.5473573207855225,
+          3.5511364936828613,
+          3.5549235343933105,
+          3.558718681335449,
+          3.5625221729278564,
+          3.566333770751953,
+          3.5701537132263184,
+          3.573981523513794,
+          3.577817440032959,
+          3.5816617012023926,
+          3.5855140686035156,
+          3.5893752574920654,
+          3.593244791030884,
+          3.5971224308013916,
+          3.601008176803589,
+          3.604902505874634,
+          3.6088054180145264,
+          3.6127161979675293,
+          3.616636276245117,
+          3.6205644607543945,
+          3.6245014667510986,
+          3.6284468173980713,
+          3.6324007511138916,
+          3.6363632678985596,
+          3.6403350830078125,
+          3.644315004348755,
+          3.648303508758545,
+          3.6523008346557617,
+          3.656306743621826,
+          3.6603219509124756,
+          3.6643457412719727,
+          3.6683785915374756,
+          3.672420024871826,
+          3.6764702796936035,
+          3.6805295944213867,
+          3.684597969055176,
+          3.688675880432129,
+          3.6927623748779297,
+          3.6968576908111572,
+          3.7009620666503906,
+          3.705075979232788,
+          3.7091987133026123,
+          3.7133309841156006,
+          3.7174723148345947,
+          3.7216224670410156,
+          3.7257821559906006,
+          3.7299511432647705,
+          3.7341299057006836,
+          3.7383179664611816,
+          3.7425150871276855,
+          3.7467215061187744,
+          3.7509374618530273,
+          3.7551629543304443,
+          3.7593979835510254,
+          3.7636430263519287,
+          3.767897129058838,
+          3.7721614837646484,
+          3.776434898376465,
+          3.7807180881500244,
+          3.785010814666748,
+          3.7893145084381104,
+          3.7936267852783203,
+          3.7979490756988525,
+          3.802281141281128,
+          3.8066232204437256,
+          3.8109755516052246,
+          3.8153374195098877,
+          3.8197097778320312,
+          3.824091672897339,
+          3.828483819961548,
+          3.8328857421875,
+          3.8372981548309326,
+          3.841721296310425,
+          3.846153974533081,
+          3.8505969047546387,
+          3.8550498485565186,
+          3.859513759613037,
+          3.863987684249878,
+          3.8684723377227783,
+          3.872966766357422,
+          3.877471923828125,
+          3.8819873332977295,
+          3.8865134716033936,
+          3.891050100326538,
+          3.8955979347229004,
+          3.900156259536743,
+          3.9047248363494873,
+          3.909303903579712,
+          3.913893938064575,
+          3.918494939804077,
+          3.9231069087982178,
+          3.927729606628418,
+          3.932363271713257,
+          3.9370076656341553,
+          3.9416630268096924,
+          3.9463295936584473,
+          3.951007843017578,
+          3.9556963443756104,
+          3.9603960514068604,
+          3.965106964111328,
+          3.9698290824890137,
+          3.974562883377075,
+          3.9793074131011963,
+          3.9840638637542725,
+          3.9888312816619873,
+          3.993610143661499,
+          3.9984004497528076,
+          4.003202438354492,
+          4.008016109466553,
+          4.01284122467041,
+          4.0176777839660645,
+          4.022526264190674,
+          4.027386665344238,
+          4.032258033752441,
+          4.0371413230896,
+          4.042037010192871,
+          4.046944618225098,
+          4.051863670349121,
+          4.0567946434021,
+          4.06173849105835,
+          4.0666937828063965,
+          4.071661472320557,
+          4.076640605926514,
+          4.081632137298584,
+          4.086636543273926,
+          4.091652870178223,
+          4.096681594848633,
+          4.101722717285156,
+          4.106776237487793,
+          4.111842155456543,
+          4.116920471191406,
+          4.122011184692383,
+          4.127115249633789,
+          4.13223123550415,
+          4.137360095977783,
+          4.1425018310546875,
+          4.147656440734863,
+          4.1528239250183105,
+          4.158004283905029,
+          4.1631975173950195,
+          4.168403625488281,
+          4.173623085021973,
+          4.178854942321777,
+          4.184100151062012,
+          4.189359188079834,
+          4.1946306228637695,
+          4.199915885925293,
+          4.205214023590088,
+          4.210526466369629,
+          4.215851306915283,
+          4.221189975738525,
+          4.2265424728393555,
+          4.231908321380615,
+          4.237287998199463,
+          4.24268102645874,
+          4.248088359832764,
+          4.253509044647217,
+          4.258944034576416,
+          4.264392375946045,
+          4.269854545593262,
+          4.275331497192383,
+          4.280821800231934,
+          4.2863264083862305,
+          4.291845321655273,
+          4.297379016876221,
+          4.302926063537598,
+          4.308487892150879,
+          4.314063549041748,
+          4.319654941558838,
+          4.325259208679199,
+          4.330878734588623,
+          4.336513042449951,
+          4.342162132263184,
+          4.34782600402832,
+          4.353504180908203,
+          4.359197616577148,
+          4.364906311035156,
+          4.37062931060791,
+          4.376367568969727,
+          4.382120609283447,
+          4.387889385223389,
+          4.393672943115234,
+          4.399472236633301,
+          4.4052863121032715,
+          4.411116123199463,
+          4.416961193084717,
+          4.422821521759033,
+          4.42869758605957,
+          4.434589862823486,
+          4.440497398376465,
+          4.446420669555664,
+          4.452359676361084,
+          4.458314895629883,
+          4.464285850524902,
+          4.470272541046143,
+          4.476275444030762,
+          4.482295036315918,
+          4.488330364227295,
+          4.494381904602051,
+          4.500450134277344,
+          4.506534576416016,
+          4.512635231018066,
+          4.518752574920654,
+          4.524886608123779,
+          4.531037330627441,
+          4.537204742431641,
+          4.543388843536377,
+          4.549590110778809,
+          4.5558085441589355,
+          4.5620436668396,
+          4.568295955657959,
+          4.5745649337768555,
+          4.580852031707764,
+          4.587155818939209,
+          4.593477249145508,
+          4.599815845489502,
+          4.606172561645508,
+          4.612545967102051,
+          4.6189374923706055,
+          4.625346660614014,
+          4.631774425506592,
+          4.638218879699707,
+          4.644681453704834,
+          4.651162624359131,
+          4.6576619148254395,
+          4.66417932510376,
+          4.670714378356934,
+          4.6772685050964355,
+          4.683840751647949,
+          4.690431594848633,
+          4.697041034698486,
+          4.703668594360352,
+          4.710315704345703,
+          4.716980934143066,
+          4.723665237426758,
+          4.730368614196777,
+          4.737091541290283,
+          4.743833065032959,
+          4.750593662261963,
+          4.757373809814453,
+          4.7641730308532715,
+          4.770992279052734,
+          4.777830600738525,
+          4.784688949584961,
+          4.791566848754883,
+          4.798464775085449,
+          4.805381774902344,
+          4.812319278717041,
+          4.819277286529541,
+          4.826254844665527,
+          4.833252906799316,
+          4.84027099609375,
+          4.8473100662231445,
+          4.854369163513184,
+          4.861448764801025,
+          4.86854887008667,
+          4.875670909881592,
+          4.8828125,
+          4.889975547790527,
+          4.897159576416016,
+          4.904365062713623,
+          4.911591529846191,
+          4.918838977813721,
+          4.926108360290527,
+          4.933399200439453,
+          4.94071102142334,
+          4.948045253753662,
+          4.955401420593262,
+          4.9627790451049805,
+          4.970178604125977,
+          4.977600574493408,
+          4.985044956207275,
+          4.99251127243042
+         ],
+         "xaxis": "x2",
+         "yaxis": "y2"
+        },
+        {
+         "nbinsx": 50,
+         "opacity": 0.7,
+         "type": "histogram",
+         "x": [
+          1.9999998807907104,
+          2.0023250579833984,
+          2.0046520233154297,
+          2.006979465484619,
+          2.0093090534210205,
+          2.0116395950317383,
+          2.0139718055725098,
+          2.0163047313690186,
+          2.0186398029327393,
+          2.020975351333618,
+          2.023313045501709,
+          2.025651693344116,
+          2.027991771697998,
+          2.0303330421447754,
+          2.0326759815216064,
+          2.035020112991333,
+          2.0373659133911133,
+          2.039712429046631,
+          2.0420608520507812,
+          2.0444107055664062,
+          2.0467615127563477,
+          2.0491137504577637,
+          2.051467180252075,
+          2.0538220405578613,
+          2.056178331375122,
+          2.0585358142852783,
+          2.060894727706909,
+          2.0632550716400146,
+          2.065617084503174,
+          2.0679800510406494,
+          2.0703446865081787,
+          2.0727100372314453,
+          2.0750772953033447,
+          2.0774457454681396,
+          2.0798158645629883,
+          2.082186698913574,
+          2.084559440612793,
+          2.086933135986328,
+          2.089308261871338,
+          2.0916848182678223,
+          2.094062566757202,
+          2.0964417457580566,
+          2.098822832107544,
+          2.1012046337127686,
+          2.1035878658294678,
+          2.1059725284576416,
+          2.108358860015869,
+          2.110746145248413,
+          2.1131350994110107,
+          2.1155247688293457,
+          2.1179163455963135,
+          2.1203091144561768,
+          2.1227030754089355,
+          2.125098466873169,
+          2.127495288848877,
+          2.1298933029174805,
+          2.132293224334717,
+          2.1346940994262695,
+          2.1370961666107178,
+          2.1394996643066406,
+          2.141904830932617,
+          2.1443111896514893,
+          2.146718978881836,
+          2.14912748336792,
+          2.151538133621216,
+          2.1539502143859863,
+          2.156363010406494,
+          2.1587769985198975,
+          2.1611931324005127,
+          2.1636102199554443,
+          2.1660284996032715,
+          2.1684482097625732,
+          2.1708693504333496,
+          2.1732916831970215,
+          2.175715684890747,
+          2.178140878677368,
+          2.180567741394043,
+          2.182995557785034,
+          2.1854248046875,
+          2.1878552436828613,
+          2.1902875900268555,
+          2.192720413208008,
+          2.195155382156372,
+          2.1975908279418945,
+          2.200028419494629,
+          2.2024667263031006,
+          2.204907178878784,
+          2.207348346710205,
+          2.209791421890259,
+          2.212235450744629,
+          2.2146809101104736,
+          2.217127561569214,
+          2.219576120376587,
+          2.2220258712768555,
+          2.2244765758514404,
+          2.226928472518921,
+          2.229382276535034,
+          2.231837034225464,
+          2.234293222427368,
+          2.236750841140747,
+          2.2392096519470215,
+          2.2416698932647705,
+          2.2441320419311523,
+          2.2465949058532715,
+          2.2490594387054443,
+          2.2515249252319336,
+          2.2539920806884766,
+          2.256460428237915,
+          2.2589306831359863,
+          2.2614011764526367,
+          2.263874053955078,
+          2.266347885131836,
+          2.2688229084014893,
+          2.2712996006011963,
+          2.2737772464752197,
+          2.2762563228607178,
+          2.2787370681762695,
+          2.281219005584717,
+          2.2837021350860596,
+          2.286186695098877,
+          2.288673162460327,
+          2.2911605834960938,
+          2.293649196624756,
+          2.2961390018463135,
+          2.298630475997925,
+          2.3011231422424316,
+          2.303617238998413,
+          2.30611252784729,
+          2.3086097240448,
+          2.3111073970794678,
+          2.3136072158813477,
+          2.3161075115203857,
+          2.318610429763794,
+          2.3211135864257812,
+          2.3236186504364014,
+          2.326124906539917,
+          2.328632354736328,
+          2.331141233444214,
+          2.3336517810821533,
+          2.3361637592315674,
+          2.338676691055298,
+          2.3411905765533447,
+          2.3437066078186035,
+          2.3462233543395996,
+          2.3487420082092285,
+          2.351261615753174,
+          2.3537826538085938,
+          2.3563051223754883,
+          2.3588292598724365,
+          2.361354112625122,
+          2.3638811111450195,
+          2.366408348083496,
+          2.3689379692077637,
+          2.3714685440063477,
+          2.374000310897827,
+          2.3765335083007812,
+          2.379068374633789,
+          2.3816041946411133,
+          2.384141445159912,
+          2.3866801261901855,
+          2.3892199993133545,
+          2.3917617797851562,
+          2.3943042755126953,
+          2.396848440170288,
+          2.3993937969207764,
+          2.40194034576416,
+          2.4044888019561768,
+          2.4070382118225098,
+          2.4095892906188965,
+          2.4121413230895996,
+          2.4146947860717773,
+          2.4172496795654297,
+          2.4198057651519775,
+          2.42236328125,
+          2.424922227859497,
+          2.4274823665618896,
+          2.430044412612915,
+          2.4326069355010986,
+          2.435171604156494,
+          2.437737464904785,
+          2.4403045177459717,
+          2.4428727626800537,
+          2.4454424381256104,
+          2.4480133056640625,
+          2.4505860805511475,
+          2.453160285949707,
+          2.455734968185425,
+          2.4583117961883545,
+          2.4608895778656006,
+          2.463468313217163,
+          2.4660491943359375,
+          2.468630790710449,
+          2.4712140560150146,
+          2.4737985134124756,
+          2.4763848781585693,
+          2.4789719581604004,
+          2.4815609455108643,
+          2.4841506481170654,
+          2.4867422580718994,
+          2.48933482170105,
+          2.491929292678833,
+          2.4945242404937744,
+          2.4971210956573486,
+          2.4997189044952393,
+          2.5023181438446045,
+          2.5049192905426025,
+          2.507521867752075,
+          2.510124921798706,
+          2.512730121612549,
+          2.515336036682129,
+          2.5179436206817627,
+          2.520552396774292,
+          2.523162603378296,
+          2.5257744789123535,
+          2.5283877849578857,
+          2.5310018062591553,
+          2.5336177349090576,
+          2.536234140396118,
+          2.5388529300689697,
+          2.5414724349975586,
+          2.544093608856201,
+          2.5467159748077393,
+          2.549339532852173,
+          2.5519649982452393,
+          2.5545918941497803,
+          2.5572192668914795,
+          2.5598485469818115,
+          2.562479019165039,
+          2.565110921859741,
+          2.567744493484497,
+          2.570378541946411,
+          2.573014736175537,
+          2.5756516456604004,
+          2.5782904624938965,
+          2.580930471420288,
+          2.5835721492767334,
+          2.586214780807495,
+          2.5888586044311523,
+          2.591503858566284,
+          2.5941505432128906,
+          2.5967986583709717,
+          2.5994484424591064,
+          2.6020994186401367,
+          2.6047515869140625,
+          2.607404947280884,
+          2.6100597381591797,
+          2.612715721130371,
+          2.615373373031616,
+          2.618032217025757,
+          2.620692253112793,
+          2.6233537197113037,
+          2.626016616821289,
+          2.6286816596984863,
+          2.631347179412842,
+          2.634014129638672,
+          2.6366825103759766,
+          2.6393520832061768,
+          2.6420233249664307,
+          2.64469575881958,
+          2.647369384765625,
+          2.6500449180603027,
+          2.6527209281921387,
+          2.6553988456726074,
+          2.6580777168273926,
+          2.6607584953308105,
+          2.663440227508545,
+          2.666123390197754,
+          2.6688082218170166,
+          2.6714937686920166,
+          2.6741812229156494,
+          2.676870107650757,
+          2.6795597076416016,
+          2.682251214981079,
+          2.684943914413452,
+          2.6876378059387207,
+          2.690333127975464,
+          2.6930301189422607,
+          2.695727825164795,
+          2.698427438735962,
+          2.7011282444000244,
+          2.7038307189941406,
+          2.706533193588257,
+          2.7092387676239014,
+          2.711944818496704,
+          2.7146522998809814,
+          2.7173614501953125,
+          2.720071315765381,
+          2.722783088684082,
+          2.725496530532837,
+          2.728210210800171,
+          2.730926275253296,
+          2.733643054962158,
+          2.736361265182495,
+          2.7390809059143066,
+          2.7418019771575928,
+          2.7445242404937744,
+          2.7472479343414307,
+          2.7499728202819824,
+          2.752700090408325,
+          2.755427360534668,
+          2.7581567764282227,
+          2.7608871459960938,
+          2.7636189460754395,
+          2.7663521766662598,
+          2.7690865993499756,
+          2.771822452545166,
+          2.7745602130889893,
+          2.7772979736328125,
+          2.780038595199585,
+          2.7827799320220947,
+          2.7855224609375,
+          2.78826642036438,
+          2.7910118103027344,
+          2.7937586307525635,
+          2.796506404876709,
+          2.7992560863494873,
+          2.8020071983337402,
+          2.8047587871551514,
+          2.8075125217437744,
+          2.810267210006714,
+          2.813023328781128,
+          2.8157806396484375,
+          2.818539619445801,
+          2.8212995529174805,
+          2.824061870574951,
+          2.8268239498138428,
+          2.8295888900756836,
+          2.8323545455932617,
+          2.8351213932037354,
+          2.8378896713256836,
+          2.8406591415405273,
+          2.8434300422668457,
+          2.8462023735046387,
+          2.8489761352539062,
+          2.8517513275146484,
+          2.854527473449707,
+          2.8573057651519775,
+          2.8600850105285645,
+          2.8628652095794678,
+          2.865647077560425,
+          2.8684301376342773,
+          2.8712146282196045,
+          2.8740007877349854,
+          2.8767879009246826,
+          2.8795769214630127,
+          2.882366418838501,
+          2.885157823562622,
+          2.8879504203796387,
+          2.89074444770813,
+          2.8935396671295166,
+          2.896336317062378,
+          2.8991341590881348,
+          2.9019343852996826,
+          2.9047341346740723,
+          2.9075369834899902,
+          2.9103403091430664,
+          2.913145065307617,
+          2.9159510135650635,
+          2.9187583923339844,
+          2.92156720161438,
+          2.92437744140625,
+          2.927189350128174,
+          2.930001974105835,
+          2.93281626701355,
+          2.93563175201416,
+          2.938448905944824,
+          2.941267251968384,
+          2.944086790084839,
+          2.9469077587127686,
+          2.9497299194335938,
+          2.9525539875030518,
+          2.955378532409668,
+          2.9582056999206543,
+          2.9610331058502197,
+          2.963862419128418,
+          2.9666926860809326,
+          2.969524621963501,
+          2.9723575115203857,
+          2.975192070007324,
+          2.978027820587158,
+          2.980865240097046,
+          2.983703374862671,
+          2.986544132232666,
+          2.9893853664398193,
+          2.992227792739868,
+          2.9950716495513916,
+          2.9979169368743896,
+          3.000763416290283,
+          3.0036113262176514,
+          3.006460666656494,
+          3.009312152862549,
+          3.0121636390686035,
+          3.015017509460449,
+          3.0178720951080322,
+          3.020728349685669,
+          3.023585557937622,
+          3.026444435119629,
+          3.0293045043945312,
+          3.0321667194366455,
+          3.0350286960601807,
+          3.037893772125244,
+          3.0407583713531494,
+          3.043626070022583,
+          3.046494245529175,
+          3.049363851547241,
+          3.052234649658203,
+          3.0551068782806396,
+          3.057981014251709,
+          3.0608558654785156,
+          3.063732147216797,
+          3.0666098594665527,
+          3.0694892406463623,
+          3.072369337081909,
+          3.075251340866089,
+          3.078134298324585,
+          3.0810189247131348,
+          3.083904504776001,
+          3.086791753768921,
+          3.0896809101104736,
+          3.0925700664520264,
+          3.0954620838165283,
+          3.0983545780181885,
+          3.101248264312744,
+          3.1041436195373535,
+          3.1070401668548584,
+          3.109938144683838,
+          3.112837791442871,
+          3.1157379150390625,
+          3.118640661239624,
+          3.121544122695923,
+          3.124448776245117,
+          3.127354860305786,
+          3.1302621364593506,
+          3.1331708431243896,
+          3.1360809803009033,
+          3.1389923095703125,
+          3.1419055461883545,
+          3.1448192596435547,
+          3.147735595703125,
+          3.1506524085998535,
+          3.1535706520080566,
+          3.1564900875091553,
+          3.1594109535217285,
+          3.1623332500457764,
+          3.165257692337036,
+          3.1681816577911377,
+          3.1711089611053467,
+          3.1740360260009766,
+          3.1769649982452393,
+          3.1798958778381348,
+          3.1828272342681885,
+          3.185760498046875,
+          3.1886954307556152,
+          3.1916308403015137,
+          3.194568634033203,
+          3.1975061893463135,
+          3.200446844100952,
+          3.203387975692749,
+          3.2063305377960205,
+          3.2092742919921875,
+          3.212219476699829,
+          3.2151660919189453,
+          3.2181143760681152,
+          3.2210633754730225,
+          3.2240142822265625,
+          3.226966381072998,
+          3.229919672012329,
+          3.232874631881714,
+          3.235830545425415,
+          3.23878812789917,
+          3.241746664047241,
+          3.244706869125366,
+          3.247668981552124,
+          3.2506308555603027,
+          3.2535958290100098,
+          3.256561279296875,
+          3.259528160095215,
+          3.26249623298645,
+          3.26546573638916,
+          3.2684366703033447,
+          3.271409034729004,
+          3.2743828296661377,
+          3.277357339859009,
+          3.280333995819092,
+          3.2833120822906494,
+          3.2862911224365234,
+          3.289271354675293,
+          3.292253017425537,
+          3.295236110687256,
+          3.29822039604187,
+          3.301206588745117,
+          3.3041932582855225,
+          3.3071820735931396,
+          3.3101718425750732,
+          3.3131635189056396,
+          3.3161559104919434,
+          3.3191492557525635,
+          3.3221449851989746,
+          3.325141429901123,
+          3.328139305114746,
+          3.3311386108398438,
+          3.334139108657837,
+          3.337141990661621,
+          3.3401453495025635,
+          3.343149423599243,
+          3.3461554050445557,
+          3.349163293838501,
+          3.3521718978881836,
+          3.355181932449341,
+          3.3581933975219727,
+          3.3612060546875,
+          3.364220142364502,
+          3.3672356605529785,
+          3.370253324508667,
+          3.3732714653015137,
+          3.376291036605835,
+          3.3793118000030518,
+          3.382333993911743,
+          3.385357618331909,
+          3.3883824348449707,
+          3.391409158706665,
+          3.3944365978240967,
+          3.397465705871582,
+          3.400496006011963,
+          3.4035279750823975,
+          3.4065611362457275,
+          3.409595489501953,
+          3.412631034851074,
+          3.415668249130249,
+          3.4187066555023193,
+          3.421746253967285,
+          3.424788236618042,
+          3.427830696105957,
+          3.4308745861053467,
+          3.433919668197632,
+          3.4369661808013916,
+          3.440014123916626,
+          3.443063259124756,
+          3.4461138248443604,
+          3.4491662979125977,
+          3.452219009399414,
+          3.4552736282348633,
+          3.4583306312561035,
+          3.461387872695923,
+          3.464446544647217,
+          3.4675066471099854,
+          3.4705679416656494,
+          3.473630666732788,
+          3.4766948223114014,
+          3.4797606468200684,
+          3.4828274250030518,
+          3.4858953952789307,
+          3.4889655113220215,
+          3.4920363426208496,
+          3.4951083660125732,
+          3.4981822967529297,
+          3.5012567043304443,
+          3.504333019256592,
+          3.5074105262756348,
+          3.5104904174804688,
+          3.513570785522461,
+          3.5166523456573486,
+          3.5197348594665527,
+          3.522819757461548,
+          3.5259053707122803,
+          3.5289924144744873,
+          3.532081127166748,
+          3.5351710319519043,
+          3.538261651992798,
+          3.541353940963745,
+          3.5444488525390625,
+          3.547544002532959,
+          3.550640344619751,
+          3.5537381172180176,
+          3.556837320327759,
+          3.5599379539489746,
+          3.563039779663086,
+          3.56614351272583,
+          3.5692481994628906,
+          3.5723540782928467,
+          3.5754618644714355,
+          3.578570604324341,
+          3.5816805362701416,
+          3.584791898727417,
+          3.587904930114746,
+          3.5910186767578125,
+          3.5941340923309326,
+          3.5972516536712646,
+          3.600369691848755,
+          3.6034891605377197,
+          3.606610059738159,
+          3.609732151031494,
+          3.6128556728363037,
+          3.615980625152588,
+          3.6191067695617676,
+          3.6222352981567383,
+          3.62536358833313,
+          3.6284940242767334,
+          3.6316261291503906,
+          3.634758949279785,
+          3.6378931999206543,
+          3.641028881072998,
+          3.6441659927368164,
+          3.6473042964935303,
+          3.6504440307617188,
+          3.6535861492156982,
+          3.6567280292510986,
+          3.65987229347229,
+          3.6630170345306396,
+          3.666163921356201,
+          3.669311761856079,
+          3.6724607944488525,
+          3.675611734390259,
+          3.6787636280059814,
+          3.6819164752960205,
+          3.6850714683532715,
+          3.688227891921997,
+          3.691385269165039,
+          3.6945438385009766,
+          3.6977038383483887,
+          3.7008650302886963,
+          3.7040278911590576,
+          3.7071919441223145,
+          3.710358142852783,
+          3.713524341583252,
+          3.7166924476623535,
+          3.7198615074157715,
+          3.7230331897735596,
+          3.726205348968506,
+          3.7293789386749268,
+          3.732553720474243,
+          3.735729932785034,
+          3.7389073371887207,
+          3.742086410522461,
+          3.745266914367676,
+          3.7484490871429443,
+          3.75163197517395,
+          3.7548162937164307,
+          3.7580020427703857,
+          3.7611889839172363,
+          3.7643773555755615,
+          3.7675673961639404,
+          3.770758628845215,
+          3.7739508152008057,
+          3.7771451473236084,
+          3.7803401947021484,
+          3.783536672592163,
+          3.7867345809936523,
+          3.789933681488037,
+          3.7931342124938965,
+          3.7963359355926514,
+          3.7995402812957764,
+          3.8027443885803223,
+          3.805950164794922,
+          3.809157371520996,
+          3.812366485595703,
+          3.8155765533447266,
+          3.8187878131866455,
+          3.822000503540039,
+          3.8252148628234863,
+          3.828429937362671,
+          3.831646680831909,
+          3.834865093231201,
+          3.8380849361419678,
+          3.841305732727051,
+          3.84452748298645,
+          3.8477513790130615,
+          3.8509762287139893,
+          3.8542022705078125,
+          3.8574302196502686,
+          3.860659122467041,
+          3.863889455795288,
+          3.8671209812164307,
+          3.870354413986206,
+          3.873588800430298,
+          3.876824378967285,
+          3.8800618648529053,
+          3.8832995891571045,
+          3.8865392208099365,
+          3.8897812366485596,
+          3.893023729324341,
+          3.8962669372558594,
+          3.8995120525360107,
+          3.902758836746216,
+          3.9060068130493164,
+          3.9092557430267334,
+          3.912506341934204,
+          3.9157586097717285,
+          3.9190118312835693,
+          3.9222660064697266,
+          3.9255218505859375,
+          3.9287800788879395,
+          3.9320385456085205,
+          3.935297966003418,
+          3.9385597705841064,
+          3.9418225288391113,
+          3.9450864791870117,
+          3.9483518600463867,
+          3.9516189098358154,
+          3.9548871517181396,
+          3.9581563472747803,
+          3.961427688598633,
+          3.9646997451782227,
+          3.967973232269287,
+          3.9712483882904053,
+          3.974524736404419,
+          3.97780179977417,
+          3.981081008911133,
+          3.9843616485595703,
+          3.987643241882324,
+          3.9909257888793945,
+          3.9942104816436768,
+          3.9974961280822754,
+          4.0007829666137695,
+          4.004071235656738,
+          4.007361888885498,
+          4.010653018951416,
+          4.01394510269165,
+          4.017238140106201,
+          4.020534038543701,
+          4.023830413818359,
+          4.027128219604492,
+          4.0304274559021,
+          4.033727645874023,
+          4.03702974319458,
+          4.040332794189453,
+          4.043638229370117,
+          4.0469441413879395,
+          4.050251483917236,
+          4.053560256958008,
+          4.056869983673096,
+          4.060181140899658,
+          4.063493728637695,
+          4.066807746887207,
+          4.070123195648193,
+          4.073440074920654,
+          4.076758861541748,
+          4.080078125,
+          4.083398818969727,
+          4.086720943450928,
+          4.0900444984436035,
+          4.093369483947754,
+          4.096695423126221,
+          4.1000237464904785,
+          4.1033525466918945,
+          4.106681823730469,
+          4.110013484954834,
+          4.11334753036499,
+          4.1166815757751465,
+          4.120017051696777,
+          4.123353958129883,
+          4.126692295074463,
+          4.130032062530518,
+          4.133372783660889,
+          4.136716365814209,
+          4.140059947967529,
+          4.143404960632324,
+          4.146751403808594,
+          4.150099277496338,
+          4.153448104858398,
+          4.156798839569092,
+          4.160150527954102,
+          4.163503646850586,
+          4.166858196258545,
+          4.1702141761779785,
+          4.173572063446045,
+          4.176930904388428,
+          4.180290699005127,
+          4.183651924133301,
+          4.187014579772949,
+          4.190378665924072,
+          4.193743705749512,
+          4.1971116065979,
+          4.200479507446289,
+          4.203847885131836,
+          4.20721960067749,
+          4.210591793060303,
+          4.213964939117432,
+          4.217339992523193,
+          4.2207159996032715,
+          4.224093437194824,
+          4.227472305297852,
+          4.2308526039123535,
+          4.234234809875488,
+          4.2376179695129395,
+          4.241002082824707,
+          4.244387626647949,
+          4.247774600982666,
+          4.251163005828857,
+          4.254552364349365,
+          4.257943630218506,
+          4.261336803436279,
+          4.2647294998168945,
+          4.268125534057617,
+          4.27152156829834,
+          4.274919509887695,
+          4.278318405151367,
+          4.281719207763672,
+          4.285120964050293,
+          4.288524150848389,
+          4.291929244995117,
+          4.295335292816162,
+          4.298742294311523,
+          4.302151203155518,
+          4.305561065673828,
+          4.308972358703613,
+          4.312384605407715,
+          4.315799713134766,
+          4.3192138671875,
+          4.322630405426025,
+          4.326048374176025,
+          4.329468727111816,
+          4.332889556884766,
+          4.3363118171691895,
+          4.33973503112793,
+          4.3431596755981445,
+          4.346585750579834,
+          4.350013256072998,
+          4.353443145751953,
+          4.356873512268066,
+          4.360304832458496,
+          4.363736629486084,
+          4.367171764373779,
+          4.370607376098633,
+          4.374044418334961,
+          4.3774824142456055,
+          4.380922317504883,
+          4.384363174438477,
+          4.387806415557861,
+          4.391250133514404,
+          4.394695281982422,
+          4.398141384124756,
+          4.401589393615723,
+          4.405038356781006,
+          4.408488750457764,
+          4.4119415283203125,
+          4.4153947830200195,
+          4.418848037719727,
+          4.422303676605225,
+          4.425762176513672,
+          4.429220676422119,
+          4.432680606842041,
+          4.436141490936279,
+          4.43960428237915,
+          4.443068027496338,
+          4.446533203125,
+          4.449999809265137,
+          4.4534687995910645,
+          4.45693826675415,
+          4.460409164428711,
+          4.463881015777588,
+          4.4673542976379395,
+          4.470829010009766,
+          4.474305152893066,
+          4.477782726287842,
+          4.481261253356934,
+          4.484741687774658,
+          4.488224029541016,
+          4.491706848144531,
+          4.4951910972595215,
+          4.498676776885986,
+          4.502163410186768,
+          4.505651950836182,
+          4.509141445159912,
+          4.512633323669434,
+          4.516125679016113,
+          4.519618034362793,
+          4.523114204406738,
+          4.526610374450684,
+          4.530108451843262,
+          4.533607482910156,
+          4.537107467651367,
+          4.540609359741211,
+          4.544112682342529,
+          4.547616958618164,
+          4.55112361907959,
+          4.554630756378174,
+          4.558139324188232,
+          4.561649322509766,
+          4.565160274505615,
+          4.568673133850098,
+          4.5721869468688965,
+          4.57570219039917,
+          4.579218864440918,
+          4.582736968994141,
+          4.586256980895996,
+          4.589777946472168,
+          4.593299865722656,
+          4.596823215484619,
+          4.600347995758057,
+          4.6038737297058105,
+          4.607401371002197,
+          4.610930919647217,
+          4.614461421966553,
+          4.617992877960205,
+          4.621525287628174,
+          4.625059604644775,
+          4.628593921661377,
+          4.632132053375244,
+          4.635670185089111,
+          4.639209747314453,
+          4.642751693725586,
+          4.6462931632995605,
+          4.649836540222168,
+          4.653382778167725,
+          4.656929016113281,
+          4.6604766845703125,
+          4.664025783538818,
+          4.667575836181641,
+          4.671127796173096,
+          4.674680709838867,
+          4.6782355308532715,
+          4.681791305541992,
+          4.685349464416504,
+          4.688906669616699,
+          4.69246768951416,
+          4.696028709411621,
+          4.699591159820557,
+          4.703155040740967,
+          4.706721305847168,
+          4.710287094116211,
+          4.713855743408203,
+          4.717424392700195,
+          4.720995903015137,
+          4.724567890167236,
+          4.7281413078308105,
+          4.731715679168701,
+          4.735292911529541,
+          4.7388691902160645,
+          4.742447853088379,
+          4.746027946472168,
+          4.749609470367432,
+          4.753193378448486,
+          4.756777763366699,
+          4.7603631019592285,
+          4.763949871063232,
+          4.767538070678711,
+          4.7711286544799805,
+          4.774718761444092,
+          4.778310775756836,
+          4.781904697418213,
+          4.785500526428223,
+          4.789095878601074,
+          4.792693614959717,
+          4.796293258666992,
+          4.799893856048584,
+          4.803495407104492,
+          4.80709981918335,
+          4.810702800750732,
+          4.814309597015381,
+          4.817915916442871,
+          4.821525573730469,
+          4.825135231018066,
+          4.828746318817139,
+          4.832359790802002,
+          4.835973739624023,
+          4.839588165283203,
+          4.843204498291016,
+          4.846823692321777,
+          4.850442886352539,
+          4.854063510894775,
+          4.857684135437012,
+          4.861308574676514,
+          4.864933490753174,
+          4.86855936050415,
+          4.872187614440918,
+          4.875815391540527,
+          4.879445552825928,
+          4.883078098297119,
+          4.8867106437683105,
+          4.89034366607666,
+          4.893980503082275,
+          4.897617340087891,
+          4.9012556076049805,
+          4.904894828796387,
+          4.908535957336426,
+          4.912178039550781,
+          4.915822505950928,
+          4.919466495513916,
+          4.923113822937012,
+          4.926761627197266,
+          4.930410385131836,
+          4.934061050415039,
+          4.937713623046875,
+          4.941365718841553,
+          4.945021152496338,
+          4.948677062988281,
+          4.952333927154541,
+          4.955991268157959,
+          4.959651947021484,
+          4.963313102722168,
+          4.966975688934326,
+          4.970639705657959,
+          4.974304676055908,
+          4.977972507476807,
+          4.981639385223389,
+          4.985309600830078,
+          4.988980293273926,
+          4.992652416229248,
+          4.996325492858887
+         ],
+         "xaxis": "x3",
+         "yaxis": "y3"
+        },
+        {
+         "nbinsx": 50,
+         "opacity": 0.7,
+         "type": "histogram",
+         "x": [
+          2,
+          2.001833438873291,
+          2.0036685466766357,
+          2.005505323410034,
+          2.0073437690734863,
+          2.009183883666992,
+          2.011025905609131,
+          2.012869119644165,
+          2.014714479446411,
+          2.016561508178711,
+          2.0184099674224854,
+          2.0202603340148926,
+          2.0221123695373535,
+          2.023966073989868,
+          2.0258214473724365,
+          2.0276784896850586,
+          2.0295372009277344,
+          2.031397819519043,
+          2.033259868621826,
+          2.0351240634918213,
+          2.036989688873291,
+          2.0388569831848145,
+          2.0407259464263916,
+          2.0425965785980225,
+          2.044469118118286,
+          2.0463433265686035,
+          2.0482192039489746,
+          2.0500967502593994,
+          2.051976203918457,
+          2.0538573265075684,
+          2.0557401180267334,
+          2.057624340057373,
+          2.0595107078552246,
+          2.06139874458313,
+          2.063288450241089,
+          2.0651798248291016,
+          2.067073106765747,
+          2.0689680576324463,
+          2.070864677429199,
+          2.072763204574585,
+          2.0746631622314453,
+          2.0765652656555176,
+          2.0784685611724854,
+          2.080374002456665,
+          2.0822811126708984,
+          2.0841898918151855,
+          2.0861005783081055,
+          2.088012933731079,
+          2.0899269580841064,
+          2.0918428897857666,
+          2.0937604904174805,
+          2.095679759979248,
+          2.0976009368896484,
+          2.0995237827301025,
+          2.1014485359191895,
+          2.10337495803833,
+          2.1053032875061035,
+          2.1072332859039307,
+          2.1091647148132324,
+          2.111098289489746,
+          2.1130335330963135,
+          2.1149706840515137,
+          2.1169095039367676,
+          2.118849992752075,
+          2.1207923889160156,
+          2.122736692428589,
+          2.124682664871216,
+          2.1266300678253174,
+          2.128579616546631,
+          2.130530834197998,
+          2.132483959197998,
+          2.134438991546631,
+          2.1363956928253174,
+          2.1383540630340576,
+          2.1403143405914307,
+          2.1422765254974365,
+          2.144240379333496,
+          2.1462059020996094,
+          2.1481733322143555,
+          2.1501426696777344,
+          2.152113676071167,
+          2.1540863513946533,
+          2.1560611724853516,
+          2.1580376625061035,
+          2.1600160598754883,
+          2.1619961261749268,
+          2.163978099822998,
+          2.165961742401123,
+          2.167947292327881,
+          2.1699347496032715,
+          2.171923875808716,
+          2.173914909362793,
+          2.175907850265503,
+          2.1779024600982666,
+          2.179898977279663,
+          2.1818971633911133,
+          2.1838972568511963,
+          2.185899496078491,
+          2.1879031658172607,
+          2.189908981323242,
+          2.1919164657592773,
+          2.1939258575439453,
+          2.195936918258667,
+          2.1979501247406006,
+          2.199965000152588,
+          2.201981544494629,
+          2.204000234603882,
+          2.2060208320617676,
+          2.208043098449707,
+          2.2100670337677,
+          2.2120931148529053,
+          2.214120864868164,
+          2.2161505222320557,
+          2.218182325363159,
+          2.2202155590057373,
+          2.2222509384155273,
+          2.22428822517395,
+          2.2263271808624268,
+          2.228368043899536,
+          2.2304108142852783,
+          2.2324554920196533,
+          2.234502077102661,
+          2.2365503311157227,
+          2.238600492477417,
+          2.2406527996063232,
+          2.242706775665283,
+          2.244762659072876,
+          2.2468204498291016,
+          2.248880386352539,
+          2.250941753387451,
+          2.253005266189575,
+          2.255070686340332,
+          2.2571377754211426,
+          2.259207010269165,
+          2.261277914047241,
+          2.2633509635925293,
+          2.26542592048645,
+          2.267502546310425,
+          2.2695813179016113,
+          2.2716617584228516,
+          2.2737441062927246,
+          2.2758283615112305,
+          2.2779147624969482,
+          2.2800028324127197,
+          2.2820932865142822,
+          2.2841851711273193,
+          2.2862789630889893,
+          2.288374900817871,
+          2.2904727458953857,
+          2.292572259902954,
+          2.2946739196777344,
+          2.2967774868011475,
+          2.2988831996917725,
+          2.300990581512451,
+          2.3030998706817627,
+          2.305211067199707,
+          2.3073244094848633,
+          2.3094394207000732,
+          2.311556577682495,
+          2.3136754035949707,
+          2.3157966136932373,
+          2.3179194927215576,
+          2.32004451751709,
+          2.322171211242676,
+          2.3242998123168945,
+          2.326430559158325,
+          2.3285632133483887,
+          2.330698013305664,
+          2.332834482192993,
+          2.334972858428955,
+          2.337113618850708,
+          2.3392558097839355,
+          2.341400384902954,
+          2.3435468673706055,
+          2.3456952571868896,
+          2.3478455543518066,
+          2.3499977588653564,
+          2.352151870727539,
+          2.3543083667755127,
+          2.356466293334961,
+          2.3586268424987793,
+          2.3607888221740723,
+          2.362952947616577,
+          2.365119218826294,
+          2.3672873973846436,
+          2.369457483291626,
+          2.371629476547241,
+          2.3738036155700684,
+          2.3759796619415283,
+          2.3781578540802,
+          2.380337953567505,
+          2.3825199604034424,
+          2.3847038745880127,
+          2.386890172958374,
+          2.389078140258789,
+          2.391268253326416,
+          2.393460512161255,
+          2.3956544399261475,
+          2.397850513458252,
+          2.4000487327575684,
+          2.4022488594055176,
+          2.4044508934020996,
+          2.4066553115844727,
+          2.4088613986968994,
+          2.411069631576538,
+          2.4132800102233887,
+          2.415492296218872,
+          2.4177064895629883,
+          2.4199228286743164,
+          2.4221413135528564,
+          2.4243617057800293,
+          2.426584005355835,
+          2.4288084506988525,
+          2.431035041809082,
+          2.4332635402679443,
+          2.4354941844940186,
+          2.4377267360687256,
+          2.4399616718292236,
+          2.4421985149383545,
+          2.444437265396118,
+          2.4466779232025146,
+          2.448920726776123,
+          2.4511656761169434,
+          2.4534127712249756,
+          2.4556617736816406,
+          2.4579129219055176,
+          2.4601662158966064,
+          2.462421417236328,
+          2.4646787643432617,
+          2.4669382572174072,
+          2.4691996574401855,
+          2.4714629650115967,
+          2.473728895187378,
+          2.475996494293213,
+          2.4782662391662598,
+          2.4805381298065186,
+          2.48281192779541,
+          2.4850881099700928,
+          2.487366199493408,
+          2.4896464347839355,
+          2.4919285774230957,
+          2.494213104248047,
+          2.496499538421631,
+          2.4987881183624268,
+          2.5010786056518555,
+          2.503371477127075,
+          2.5056662559509277,
+          2.5079634189605713,
+          2.5102622509002686,
+          2.512563705444336,
+          2.514866828918457,
+          2.51717209815979,
+          2.519479751586914,
+          2.52178955078125,
+          2.5241012573242188,
+          2.5264151096343994,
+          2.528730869293213,
+          2.5310492515563965,
+          2.533369302749634,
+          2.535691738128662,
+          2.5380163192749023,
+          2.5403428077697754,
+          2.5426716804504395,
+          2.5450026988983154,
+          2.547335624694824,
+          2.549670696258545,
+          2.5520079135894775,
+          2.554347515106201,
+          2.5566890239715576,
+          2.559032917022705,
+          2.5613789558410645,
+          2.5637266635894775,
+          2.5660769939422607,
+          2.5684292316436768,
+          2.570783853530884,
+          2.5731403827667236,
+          2.5754995346069336,
+          2.5778603553771973,
+          2.580223798751831,
+          2.5825886726379395,
+          2.584956169128418,
+          2.5873258113861084,
+          2.58969783782959,
+          2.592071771621704,
+          2.5944480895996094,
+          2.5968263149261475,
+          2.5992069244384766,
+          2.6015896797180176,
+          2.6039745807647705,
+          2.6063616275787354,
+          2.608750820159912,
+          2.61114239692688,
+          2.6135358810424805,
+          2.615931987762451,
+          2.6183300018310547,
+          2.62073016166687,
+          2.6231327056884766,
+          2.625537157058716,
+          2.627944231033325,
+          2.6303532123565674,
+          2.6327643394470215,
+          2.6351780891418457,
+          2.6375935077667236,
+          2.6400115489959717,
+          2.6424317359924316,
+          2.6448538303375244,
+          2.6472787857055664,
+          2.649705410003662,
+          2.652134656906128,
+          2.6545655727386475,
+          2.656999111175537,
+          2.6594347953796387,
+          2.661872625350952,
+          2.6643130779266357,
+          2.666755437850952,
+          2.6691999435424805,
+          2.6716468334198,
+          2.674095869064331,
+          2.6765475273132324,
+          2.6790008544921875,
+          2.6814568042755127,
+          2.68391489982605,
+          2.686375379562378,
+          2.688838005065918,
+          2.691303014755249,
+          2.693769931793213,
+          2.696239471435547,
+          2.6987109184265137,
+          2.7011852264404297,
+          2.7036612033843994,
+          2.70613956451416,
+          2.708620548248291,
+          2.7111034393310547,
+          2.7135889530181885,
+          2.716076374053955,
+          2.718566417694092,
+          2.7210583686828613,
+          2.723552703857422,
+          2.7260496616363525,
+          2.728548526763916,
+          2.7310500144958496,
+          2.733553409576416,
+          2.7360594272613525,
+          2.7385671138763428,
+          2.7410778999328613,
+          2.7435903549194336,
+          2.746105670928955,
+          2.7486228942871094,
+          2.751142978668213,
+          2.75366473197937,
+          2.7561888694763184,
+          2.7587156295776367,
+          2.761244535446167,
+          2.763775587081909,
+          2.7663094997406006,
+          2.768845558166504,
+          2.771383762359619,
+          2.7739243507385254,
+          2.7764670848846436,
+          2.779012680053711,
+          2.781559944152832,
+          2.7841098308563232,
+          2.7866618633270264,
+          2.7892162799835205,
+          2.7917733192443848,
+          2.794332504272461,
+          2.7968943119049072,
+          2.7994580268859863,
+          2.8020246028900146,
+          2.804593086242676,
+          2.807164192199707,
+          2.80973744392395,
+          2.8123130798339844,
+          2.8148913383483887,
+          2.817471742630005,
+          2.820054769515991,
+          2.8226397037506104,
+          2.8252270221710205,
+          2.82781720161438,
+          2.830409288406372,
+          2.8330042362213135,
+          2.8356010913848877,
+          2.838200569152832,
+          2.8408024311065674,
+          2.8434064388275146,
+          2.846013307571411,
+          2.8486220836639404,
+          2.851233720779419,
+          2.8538472652435303,
+          2.8564634323120117,
+          2.8590822219848633,
+          2.8617029190063477,
+          2.8643264770507812,
+          2.8669521808624268,
+          2.8695805072784424,
+          2.87221097946167,
+          2.8748438358306885,
+          2.877479076385498,
+          2.8801169395446777,
+          2.8827571868896484,
+          2.8854000568389893,
+          2.888044834136963,
+          2.8906922340393066,
+          2.8933424949645996,
+          2.8959946632385254,
+          2.8986496925354004,
+          2.901306629180908,
+          2.9039666652679443,
+          2.9066286087036133,
+          2.9092929363250732,
+          2.9119601249694824,
+          2.9146294593811035,
+          2.9173014163970947,
+          2.919975757598877,
+          2.9226527214050293,
+          2.9253318309783936,
+          2.928013324737549,
+          2.930697202682495,
+          2.933384418487549,
+          2.936073064804077,
+          2.938764810562134,
+          2.9414587020874023,
+          2.944154977798462,
+          2.9468541145324707,
+          2.9495556354522705,
+          2.9522595405578613,
+          2.954965591430664,
+          2.957674741744995,
+          2.960386037826538,
+          2.963099479675293,
+          2.965816020965576,
+          2.9685347080230713,
+          2.9712560176849365,
+          2.973979949951172,
+          2.9767065048217773,
+          2.9794349670410156,
+          2.982166051864624,
+          2.9848997592926025,
+          2.9876363277435303,
+          2.990375280380249,
+          2.9931163787841797,
+          2.9958600997924805,
+          2.9986066818237305,
+          3.0013554096221924,
+          3.0041069984436035,
+          3.0068607330322266,
+          3.0096170902252197,
+          3.012376308441162,
+          3.0151374340057373,
+          3.017901659011841,
+          3.0206682682037354,
+          3.023437023162842,
+          3.0262088775634766,
+          3.0289828777313232,
+          3.031759738922119,
+          3.034538984298706,
+          3.037320613861084,
+          3.040104866027832,
+          3.0428919792175293,
+          3.0456814765930176,
+          3.048473358154297,
+          3.0512678623199463,
+          3.054065227508545,
+          3.0568647384643555,
+          3.0596671104431152,
+          3.062471866607666,
+          3.065279245376587,
+          3.068089246749878,
+          3.070901870727539,
+          3.0737171173095703,
+          3.0765347480773926,
+          3.079354763031006,
+          3.0821778774261475,
+          3.085003137588501,
+          3.087831497192383,
+          3.0906620025634766,
+          3.0934951305389404,
+          3.0963311195373535,
+          3.0991697311401367,
+          3.102010726928711,
+          3.104854106903076,
+          3.1077003479003906,
+          3.1105494499206543,
+          3.11340069770813,
+          3.116255044937134,
+          3.1191115379333496,
+          3.1219706535339355,
+          3.12483286857605,
+          3.127697229385376,
+          3.1305646896362305,
+          3.133434534072876,
+          3.1363067626953125,
+          3.1391820907592773,
+          3.1420600414276123,
+          3.144940137863159,
+          3.1478230953216553,
+          3.1507084369659424,
+          3.153597116470337,
+          3.1564881801605225,
+          3.159381628036499,
+          3.1622776985168457,
+          3.1651768684387207,
+          3.1680781841278076,
+          3.1709823608398438,
+          3.173888921737671,
+          3.1767988204956055,
+          3.179710865020752,
+          3.1826255321502686,
+          3.1855432987213135,
+          3.1884634494781494,
+          3.1913864612579346,
+          3.1943118572235107,
+          3.1972403526306152,
+          3.2001712322235107,
+          3.2031047344207764,
+          3.2060413360595703,
+          3.208980083465576,
+          3.2119216918945312,
+          3.2148663997650146,
+          3.217813491821289,
+          3.2207632064819336,
+          3.2237155437469482,
+          3.226670742034912,
+          3.229628801345825,
+          3.2325897216796875,
+          3.2355527877807617,
+          3.238518714904785,
+          3.241487741470337,
+          3.244459629058838,
+          3.247433662414551,
+          3.250410318374634,
+          3.253390312194824,
+          3.2563726902008057,
+          3.2593576908111572,
+          3.262345790863037,
+          3.265336275100708,
+          3.268329381942749,
+          3.2713253498077393,
+          3.274324417114258,
+          3.2773263454437256,
+          3.2803304195404053,
+          3.283337354660034,
+          3.2863476276397705,
+          3.289360284805298,
+          3.2923755645751953,
+          3.295393705368042,
+          3.298414468765259,
+          3.301438331604004,
+          3.30446457862854,
+          3.3074941635131836,
+          3.310525894165039,
+          3.3135604858398438,
+          3.316598415374756,
+          3.319638729095459,
+          3.3226819038391113,
+          3.325727701187134,
+          3.3287768363952637,
+          3.3318281173706055,
+          3.3348822593688965,
+          3.337939739227295,
+          3.3409993648529053,
+          3.344061851501465,
+          3.347127676010132,
+          3.350196361541748,
+          3.3532674312591553,
+          3.3563411235809326,
+          3.359417676925659,
+          3.362497568130493,
+          3.365579843521118,
+          3.3686654567718506,
+          3.371753215789795,
+          3.374844551086426,
+          3.3779380321502686,
+          3.3810346126556396,
+          3.384134292602539,
+          3.3872363567352295,
+          3.390341281890869,
+          3.393449544906616,
+          3.3965601921081543,
+          3.3996739387512207,
+          3.4027903079986572,
+          3.405909538269043,
+          3.409032106399536,
+          3.4121572971343994,
+          3.415285110473633,
+          3.4184157848358154,
+          3.4215493202209473,
+          3.4246866703033447,
+          3.427825927734375,
+          3.4309680461883545,
+          3.434113025665283,
+          3.4372613430023193,
+          3.4404122829437256,
+          3.443565845489502,
+          3.446722984313965,
+          3.4498822689056396,
+          3.453045129776001,
+          3.4562103748321533,
+          3.459378957748413,
+          3.462550163269043,
+          3.465724229812622,
+          3.4689009189605713,
+          3.472081184387207,
+          3.475264310836792,
+          3.478450059890747,
+          3.4816386699676514,
+          3.484830141067505,
+          3.488024950027466,
+          3.4912221431732178,
+          3.4944229125976562,
+          3.4976260662078857,
+          3.5008327960968018,
+          3.504041910171509,
+          3.507254123687744,
+          3.5104691982269287,
+          3.5136871337890625,
+          3.5169084072113037,
+          3.520132303237915,
+          3.5233592987060547,
+          3.5265891551971436,
+          3.52982234954834,
+          3.533057928085327,
+          3.5362966060638428,
+          3.539538621902466,
+          3.542782783508301,
+          3.5460307598114014,
+          3.549281120300293,
+          3.5525355339050293,
+          3.5557916164398193,
+          3.559051513671875,
+          3.5623137950897217,
+          3.565579414367676,
+          3.568848133087158,
+          3.572119951248169,
+          3.57539439201355,
+          3.578671932220459,
+          3.5819528102874756,
+          3.585236072540283,
+          3.5885231494903564,
+          3.5918126106262207,
+          3.595104932785034,
+          3.598400831222534,
+          3.6016998291015625,
+          3.605001449584961,
+          3.6083059310913086,
+          3.6116139888763428,
+          3.614924192428589,
+          3.618238687515259,
+          3.6215555667877197,
+          3.624875545501709,
+          3.6281979084014893,
+          3.6315243244171143,
+          3.6348536014556885,
+          3.6381850242614746,
+          3.6415205001831055,
+          3.6448590755462646,
+          3.648200035095215,
+          3.6515438556671143,
+          3.6548914909362793,
+          3.6582422256469727,
+          3.6615960597991943,
+          3.664952039718628,
+          3.6683120727539062,
+          3.671674966812134,
+          3.6750407218933105,
+          3.6784095764160156,
+          3.681781768798828,
+          3.685157299041748,
+          3.688534736633301,
+          3.6919164657592773,
+          3.695300579071045,
+          3.698688507080078,
+          3.7020788192749023,
+          3.705472946166992,
+          3.708869457244873,
+          3.7122693061828613,
+          3.715672731399536,
+          3.71907901763916,
+          3.7224881649017334,
+          3.725900411605835,
+          3.729316473007202,
+          3.7327349185943604,
+          3.736156940460205,
+          3.739581823348999,
+          3.7430098056793213,
+          3.746441125869751,
+          3.749875545501709,
+          3.7533133029937744,
+          3.75675368309021,
+          3.760197401046753,
+          3.7636446952819824,
+          3.767094850540161,
+          3.7705483436584473,
+          3.7740046977996826,
+          3.7774641513824463,
+          3.7809271812438965,
+          3.784393072128296,
+          3.787862539291382,
+          3.791334390640259,
+          3.7948100566864014,
+          3.7982890605926514,
+          3.8017709255218506,
+          3.805255889892578,
+          3.808744430541992,
+          3.8122363090515137,
+          3.815730333328247,
+          3.819228410720825,
+          3.8227298259735107,
+          3.8262341022491455,
+          3.8297414779663086,
+          3.833252429962158,
+          3.8367667198181152,
+          3.8402836322784424,
+          3.843803882598877,
+          3.847327470779419,
+          3.8508546352386475,
+          3.854384422302246,
+          3.8579182624816895,
+          3.861454486846924,
+          3.8649942874908447,
+          3.868537187576294,
+          3.8720836639404297,
+          3.875633478164673,
+          3.8791861534118652,
+          3.882742166519165,
+          3.8863017559051514,
+          3.889864206314087,
+          3.893429756164551,
+          3.8969993591308594,
+          3.900571584701538,
+          3.9041473865509033,
+          3.907726287841797,
+          3.911308765411377,
+          3.9148941040039062,
+          3.918482780456543,
+          3.9220752716064453,
+          3.925670862197876,
+          3.929269313812256,
+          3.932871103286743,
+          3.936476707458496,
+          3.9400851726531982,
+          3.943696975708008,
+          3.9473118782043457,
+          3.9509313106536865,
+          3.954552412033081,
+          3.9581780433654785,
+          3.961805820465088,
+          3.9654383659362793,
+          3.9690732955932617,
+          3.9727115631103516,
+          3.976353168487549,
+          3.97999906539917,
+          3.983647584915161,
+          3.9872992038726807,
+          3.9909541606903076,
+          3.9946129322052,
+          3.998274803161621,
+          4.00193977355957,
+          4.005608558654785,
+          4.009281158447266,
+          4.012956142425537,
+          4.016634464263916,
+          4.020317077636719,
+          4.0240020751953125,
+          4.027690887451172,
+          4.031383514404297,
+          4.035079479217529,
+          4.038778305053711,
+          4.04248046875,
+          4.046186447143555,
+          4.049895286560059,
+          4.053608417510986,
+          4.057323932647705,
+          4.061043739318848,
+          4.0647664070129395,
+          4.068492412567139,
+          4.072221755981445,
+          4.075955390930176,
+          4.079691410064697,
+          4.083431243896484,
+          4.087174892425537,
+          4.090921878814697,
+          4.094671249389648,
+          4.098425388336182,
+          4.102182388305664,
+          4.105942726135254,
+          4.109706878662109,
+          4.113474369049072,
+          4.117245197296143,
+          4.12101936340332,
+          4.124797344207764,
+          4.1285786628723145,
+          4.132363319396973,
+          4.136151313781738,
+          4.139942646026611,
+          4.143738746643066,
+          4.147536754608154,
+          4.151339054107666,
+          4.155144214630127,
+          4.158953666687012,
+          4.162765979766846,
+          4.1665825843811035,
+          4.170401573181152,
+          4.174224376678467,
+          4.178051471710205,
+          4.181881427764893,
+          4.185715198516846,
+          4.189551830291748,
+          4.193392276763916,
+          4.19723653793335,
+          4.201084136962891,
+          4.204935550689697,
+          4.208790302276611,
+          4.212648391723633,
+          4.21651029586792,
+          4.2203755378723145,
+          4.224244594573975,
+          4.228116989135742,
+          4.231992721557617,
+          4.235872745513916,
+          4.239755153656006,
+          4.243642330169678,
+          4.247532367706299,
+          4.251425743103027,
+          4.25532341003418,
+          4.259224891662598,
+          4.263128280639648,
+          4.2670369148254395,
+          4.270948886871338,
+          4.2748637199401855,
+          4.278782367706299,
+          4.282705307006836,
+          4.2866315841674805,
+          4.290560245513916,
+          4.294493675231934,
+          4.298430919647217,
+          4.302371025085449,
+          4.306314945220947,
+          4.310262203216553,
+          4.31421422958374,
+          4.318169116973877,
+          4.322126865386963,
+          4.326089382171631,
+          4.330055236816406,
+          4.334023952484131,
+          4.3379974365234375,
+          4.341973781585693,
+          4.345954895019531,
+          4.34993839263916,
+          4.353926181793213,
+          4.357917308807373,
+          4.361912250518799,
+          4.36591100692749,
+          4.369913101196289,
+          4.373919486999512,
+          4.377928733825684,
+          4.381942272186279,
+          4.385959148406982,
+          4.389980316162109,
+          4.3940043449401855,
+          4.3980326652526855,
+          4.402064323425293,
+          4.406099796295166,
+          4.4101386070251465,
+          4.414181232452393,
+          4.4182281494140625,
+          4.422277927398682,
+          4.426332473754883,
+          4.430389881134033,
+          4.434451580047607,
+          4.438516139984131,
+          4.442584991455078,
+          4.446657657623291,
+          4.4507341384887695,
+          4.454813480377197,
+          4.458898067474365,
+          4.462985515594482,
+          4.467076778411865,
+          4.4711713790893555,
+          4.4752702713012695,
+          4.479372978210449,
+          4.483478546142578,
+          4.487588882446289,
+          4.491703033447266,
+          4.495820999145508,
+          4.499941825866699,
+          4.5040669441223145,
+          4.5081963539123535,
+          4.512328147888184,
+          4.516465187072754,
+          4.52060604095459,
+          4.524750232696533,
+          4.528897285461426,
+          4.533049583435059,
+          4.537204742431641,
+          4.541363716125488,
+          4.545526504516602,
+          4.549694061279297,
+          4.553864479064941,
+          4.558039665222168,
+          4.562217712402344,
+          4.566400051116943,
+          4.57058572769165,
+          4.574775695800781,
+          4.578969478607178,
+          4.58316707611084,
+          4.587368965148926,
+          4.591573715209961,
+          4.595783233642578,
+          4.599996089935303,
+          4.604213237762451,
+          4.608433723449707,
+          4.612658500671387,
+          4.616887092590332,
+          4.621119022369385,
+          4.6253557205200195,
+          4.629595756530762,
+          4.633840084075928,
+          4.638086795806885,
+          4.642339706420898,
+          4.646595001220703,
+          4.650854587554932,
+          4.655117511749268,
+          4.659385681152344,
+          4.663657188415527,
+          4.66793155670166,
+          4.672211170196533,
+          4.676494598388672,
+          4.680781364440918,
+          4.68507194519043,
+          4.689366340637207,
+          4.693665504455566,
+          4.69796895980835,
+          4.702274799346924,
+          4.70658540725708,
+          4.71090030670166,
+          4.715219020843506,
+          4.719541072845459,
+          4.723867893218994,
+          4.728198528289795,
+          4.732532024383545,
+          4.736870765686035,
+          4.741212844848633,
+          4.7455596923828125,
+          4.7499098777771,
+          4.7542643547058105,
+          4.758622646331787,
+          4.762984275817871,
+          4.767351150512695,
+          4.771721839904785,
+          4.776095867156982,
+          4.780473709106445,
+          4.78485631942749,
+          4.789242744445801,
+          4.793633460998535,
+          4.798027515411377,
+          4.802426338195801,
+          4.806828498840332,
+          4.811234474182129,
+          4.815645694732666,
+          4.820059776306152,
+          4.8244781494140625,
+          4.828900337219238,
+          4.833327770233154,
+          4.837758541107178,
+          4.842193126678467,
+          4.84663200378418,
+          4.851075172424316,
+          4.855522155761719,
+          4.859972953796387,
+          4.8644280433654785,
+          4.868887901306152,
+          4.873351097106934,
+          4.877818584442139,
+          4.882289886474609,
+          4.886765956878662,
+          4.891245365142822,
+          4.895729064941406,
+          4.900217056274414,
+          4.904709815979004,
+          4.909205436706543,
+          4.913705825805664,
+          4.918210506439209,
+          4.9227190017700195,
+          4.927231311798096,
+          4.931748390197754,
+          4.936269760131836,
+          4.940794944763184,
+          4.945323944091797,
+          4.949857711791992,
+          4.954394817352295,
+          4.958937168121338,
+          4.963481903076172,
+          4.968032360076904,
+          4.9725871086120605,
+          4.977145195007324,
+          4.981707572937012,
+          4.986274719238281,
+          4.990845680236816,
+          4.995420455932617
+         ],
+         "xaxis": "x4",
+         "yaxis": "y4"
+        }
+       ],
+       "layout": {
+        "annotations": [
+         {
+          "font": {
+           "size": 16
+          },
+          "showarrow": false,
+          "text": "Uniform",
+          "x": 0.225,
+          "xanchor": "center",
+          "xref": "paper",
+          "y": 1,
+          "yanchor": "bottom",
+          "yref": "paper"
+         },
+         {
+          "font": {
+           "size": 16
+          },
+          "showarrow": false,
+          "text": "Linear in Disparity",
+          "x": 0.775,
+          "xanchor": "center",
+          "xref": "paper",
+          "y": 1,
+          "yanchor": "bottom",
+          "yref": "paper"
+         },
+         {
+          "font": {
+           "size": 16
+          },
+          "showarrow": false,
+          "text": "Square Root",
+          "x": 0.225,
+          "xanchor": "center",
+          "xref": "paper",
+          "y": 0.45,
+          "yanchor": "bottom",
+          "yref": "paper"
+         },
+         {
+          "font": {
+           "size": 16
+          },
+          "showarrow": false,
+          "text": "Log Sampler",
+          "x": 0.775,
+          "xanchor": "center",
+          "xref": "paper",
+          "y": 0.45,
+          "yanchor": "bottom",
+          "yref": "paper"
+         }
+        ],
+        "height": 700,
+        "hovermode": false,
+        "margin": {
+         "b": 20,
+         "l": 20,
+         "r": 20,
+         "t": 50
+        },
+        "showlegend": false,
+        "template": {
+         "data": {
+          "bar": [
+           {
+            "error_x": {
+             "color": "#2a3f5f"
+            },
+            "error_y": {
+             "color": "#2a3f5f"
+            },
+            "marker": {
+             "line": {
+              "color": "#E5ECF6",
+              "width": 0.5
+             },
+             "pattern": {
+              "fillmode": "overlay",
+              "size": 10,
+              "solidity": 0.2
+             }
+            },
+            "type": "bar"
+           }
+          ],
+          "barpolar": [
+           {
+            "marker": {
+             "line": {
+              "color": "#E5ECF6",
+              "width": 0.5
+             },
+             "pattern": {
+              "fillmode": "overlay",
+              "size": 10,
+              "solidity": 0.2
+             }
+            },
+            "type": "barpolar"
+           }
+          ],
+          "carpet": [
+           {
+            "aaxis": {
+             "endlinecolor": "#2a3f5f",
+             "gridcolor": "white",
+             "linecolor": "white",
+             "minorgridcolor": "white",
+             "startlinecolor": "#2a3f5f"
+            },
+            "baxis": {
+             "endlinecolor": "#2a3f5f",
+             "gridcolor": "white",
+             "linecolor": "white",
+             "minorgridcolor": "white",
+             "startlinecolor": "#2a3f5f"
+            },
+            "type": "carpet"
+           }
+          ],
+          "choropleth": [
+           {
+            "colorbar": {
+             "outlinewidth": 0,
+             "ticks": ""
+            },
+            "type": "choropleth"
+           }
+          ],
+          "contour": [
+           {
+            "colorbar": {
+             "outlinewidth": 0,
+             "ticks": ""
+            },
+            "colorscale": [
+             [
+              0,
+              "#0d0887"
+             ],
+             [
+              0.1111111111111111,
+              "#46039f"
+             ],
+             [
+              0.2222222222222222,
+              "#7201a8"
+             ],
+             [
+              0.3333333333333333,
+              "#9c179e"
+             ],
+             [
+              0.4444444444444444,
+              "#bd3786"
+             ],
+             [
+              0.5555555555555556,
+              "#d8576b"
+             ],
+             [
+              0.6666666666666666,
+              "#ed7953"
+             ],
+             [
+              0.7777777777777778,
+              "#fb9f3a"
+             ],
+             [
+              0.8888888888888888,
+              "#fdca26"
+             ],
+             [
+              1,
+              "#f0f921"
+             ]
+            ],
+            "type": "contour"
+           }
+          ],
+          "contourcarpet": [
+           {
+            "colorbar": {
+             "outlinewidth": 0,
+             "ticks": ""
+            },
+            "type": "contourcarpet"
+           }
+          ],
+          "heatmap": [
+           {
+            "colorbar": {
+             "outlinewidth": 0,
+             "ticks": ""
+            },
+            "colorscale": [
+             [
+              0,
+              "#0d0887"
+             ],
+             [
+              0.1111111111111111,
+              "#46039f"
+             ],
+             [
+              0.2222222222222222,
+              "#7201a8"
+             ],
+             [
+              0.3333333333333333,
+              "#9c179e"
+             ],
+             [
+              0.4444444444444444,
+              "#bd3786"
+             ],
+             [
+              0.5555555555555556,
+              "#d8576b"
+             ],
+             [
+              0.6666666666666666,
+              "#ed7953"
+             ],
+             [
+              0.7777777777777778,
+              "#fb9f3a"
+             ],
+             [
+              0.8888888888888888,
+              "#fdca26"
+             ],
+             [
+              1,
+              "#f0f921"
+             ]
+            ],
+            "type": "heatmap"
+           }
+          ],
+          "heatmapgl": [
+           {
+            "colorbar": {
+             "outlinewidth": 0,
+             "ticks": ""
+            },
+            "colorscale": [
+             [
+              0,
+              "#0d0887"
+             ],
+             [
+              0.1111111111111111,
+              "#46039f"
+             ],
+             [
+              0.2222222222222222,
+              "#7201a8"
+             ],
+             [
+              0.3333333333333333,
+              "#9c179e"
+             ],
+             [
+              0.4444444444444444,
+              "#bd3786"
+             ],
+             [
+              0.5555555555555556,
+              "#d8576b"
+             ],
+             [
+              0.6666666666666666,
+              "#ed7953"
+             ],
+             [
+              0.7777777777777778,
+              "#fb9f3a"
+             ],
+             [
+              0.8888888888888888,
+              "#fdca26"
+             ],
+             [
+              1,
+              "#f0f921"
+             ]
+            ],
+            "type": "heatmapgl"
+           }
+          ],
+          "histogram": [
+           {
+            "marker": {
+             "pattern": {
+              "fillmode": "overlay",
+              "size": 10,
+              "solidity": 0.2
+             }
+            },
+            "type": "histogram"
+           }
+          ],
+          "histogram2d": [
+           {
+            "colorbar": {
+             "outlinewidth": 0,
+             "ticks": ""
+            },
+            "colorscale": [
+             [
+              0,
+              "#0d0887"
+             ],
+             [
+              0.1111111111111111,
+              "#46039f"
+             ],
+             [
+              0.2222222222222222,
+              "#7201a8"
+             ],
+             [
+              0.3333333333333333,
+              "#9c179e"
+             ],
+             [
+              0.4444444444444444,
+              "#bd3786"
+             ],
+             [
+              0.5555555555555556,
+              "#d8576b"
+             ],
+             [
+              0.6666666666666666,
+              "#ed7953"
+             ],
+             [
+              0.7777777777777778,
+              "#fb9f3a"
+             ],
+             [
+              0.8888888888888888,
+              "#fdca26"
+             ],
+             [
+              1,
+              "#f0f921"
+             ]
+            ],
+            "type": "histogram2d"
+           }
+          ],
+          "histogram2dcontour": [
+           {
+            "colorbar": {
+             "outlinewidth": 0,
+             "ticks": ""
+            },
+            "colorscale": [
+             [
+              0,
+              "#0d0887"
+             ],
+             [
+              0.1111111111111111,
+              "#46039f"
+             ],
+             [
+              0.2222222222222222,
+              "#7201a8"
+             ],
+             [
+              0.3333333333333333,
+              "#9c179e"
+             ],
+             [
+              0.4444444444444444,
+              "#bd3786"
+             ],
+             [
+              0.5555555555555556,
+              "#d8576b"
+             ],
+             [
+              0.6666666666666666,
+              "#ed7953"
+             ],
+             [
+              0.7777777777777778,
+              "#fb9f3a"
+             ],
+             [
+              0.8888888888888888,
+              "#fdca26"
+             ],
+             [
+              1,
+              "#f0f921"
+             ]
+            ],
+            "type": "histogram2dcontour"
+           }
+          ],
+          "mesh3d": [
+           {
+            "colorbar": {
+             "outlinewidth": 0,
+             "ticks": ""
+            },
+            "type": "mesh3d"
+           }
+          ],
+          "parcoords": [
+           {
+            "line": {
+             "colorbar": {
+              "outlinewidth": 0,
+              "ticks": ""
+             }
+            },
+            "type": "parcoords"
+           }
+          ],
+          "pie": [
+           {
+            "automargin": true,
+            "type": "pie"
+           }
+          ],
+          "scatter": [
+           {
+            "fillpattern": {
+             "fillmode": "overlay",
+             "size": 10,
+             "solidity": 0.2
+            },
+            "type": "scatter"
+           }
+          ],
+          "scatter3d": [
+           {
+            "line": {
+             "colorbar": {
+              "outlinewidth": 0,
+              "ticks": ""
+             }
+            },
+            "marker": {
+             "colorbar": {
+              "outlinewidth": 0,
+              "ticks": ""
+             }
+            },
+            "type": "scatter3d"
+           }
+          ],
+          "scattercarpet": [
+           {
+            "marker": {
+             "colorbar": {
+              "outlinewidth": 0,
+              "ticks": ""
+             }
+            },
+            "type": "scattercarpet"
+           }
+          ],
+          "scattergeo": [
+           {
+            "marker": {
+             "colorbar": {
+              "outlinewidth": 0,
+              "ticks": ""
+             }
+            },
+            "type": "scattergeo"
+           }
+          ],
+          "scattergl": [
+           {
+            "marker": {
+             "colorbar": {
+              "outlinewidth": 0,
+              "ticks": ""
+             }
+            },
+            "type": "scattergl"
+           }
+          ],
+          "scattermapbox": [
+           {
+            "marker": {
+             "colorbar": {
+              "outlinewidth": 0,
+              "ticks": ""
+             }
+            },
+            "type": "scattermapbox"
+           }
+          ],
+          "scatterpolar": [
+           {
+            "marker": {
+             "colorbar": {
+              "outlinewidth": 0,
+              "ticks": ""
+             }
+            },
+            "type": "scatterpolar"
+           }
+          ],
+          "scatterpolargl": [
+           {
+            "marker": {
+             "colorbar": {
+              "outlinewidth": 0,
+              "ticks": ""
+             }
+            },
+            "type": "scatterpolargl"
+           }
+          ],
+          "scatterternary": [
+           {
+            "marker": {
+             "colorbar": {
+              "outlinewidth": 0,
+              "ticks": ""
+             }
+            },
+            "type": "scatterternary"
+           }
+          ],
+          "surface": [
+           {
+            "colorbar": {
+             "outlinewidth": 0,
+             "ticks": ""
+            },
+            "colorscale": [
+             [
+              0,
+              "#0d0887"
+             ],
+             [
+              0.1111111111111111,
+              "#46039f"
+             ],
+             [
+              0.2222222222222222,
+              "#7201a8"
+             ],
+             [
+              0.3333333333333333,
+              "#9c179e"
+             ],
+             [
+              0.4444444444444444,
+              "#bd3786"
+             ],
+             [
+              0.5555555555555556,
+              "#d8576b"
+             ],
+             [
+              0.6666666666666666,
+              "#ed7953"
+             ],
+             [
+              0.7777777777777778,
+              "#fb9f3a"
+             ],
+             [
+              0.8888888888888888,
+              "#fdca26"
+             ],
+             [
+              1,
+              "#f0f921"
+             ]
+            ],
+            "type": "surface"
+           }
+          ],
+          "table": [
+           {
+            "cells": {
+             "fill": {
+              "color": "#EBF0F8"
+             },
+             "line": {
+              "color": "white"
+             }
+            },
+            "header": {
+             "fill": {
+              "color": "#C8D4E3"
+             },
+             "line": {
+              "color": "white"
+             }
+            },
+            "type": "table"
+           }
+          ]
+         },
+         "layout": {
+          "annotationdefaults": {
+           "arrowcolor": "#2a3f5f",
+           "arrowhead": 0,
+           "arrowwidth": 1
+          },
+          "autotypenumbers": "strict",
+          "coloraxis": {
+           "colorbar": {
+            "outlinewidth": 0,
+            "ticks": ""
+           }
+          },
+          "colorscale": {
+           "diverging": [
+            [
+             0,
+             "#8e0152"
+            ],
+            [
+             0.1,
+             "#c51b7d"
+            ],
+            [
+             0.2,
+             "#de77ae"
+            ],
+            [
+             0.3,
+             "#f1b6da"
+            ],
+            [
+             0.4,
+             "#fde0ef"
+            ],
+            [
+             0.5,
+             "#f7f7f7"
+            ],
+            [
+             0.6,
+             "#e6f5d0"
+            ],
+            [
+             0.7,
+             "#b8e186"
+            ],
+            [
+             0.8,
+             "#7fbc41"
+            ],
+            [
+             0.9,
+             "#4d9221"
+            ],
+            [
+             1,
+             "#276419"
+            ]
+           ],
+           "sequential": [
+            [
+             0,
+             "#0d0887"
+            ],
+            [
+             0.1111111111111111,
+             "#46039f"
+            ],
+            [
+             0.2222222222222222,
+             "#7201a8"
+            ],
+            [
+             0.3333333333333333,
+             "#9c179e"
+            ],
+            [
+             0.4444444444444444,
+             "#bd3786"
+            ],
+            [
+             0.5555555555555556,
+             "#d8576b"
+            ],
+            [
+             0.6666666666666666,
+             "#ed7953"
+            ],
+            [
+             0.7777777777777778,
+             "#fb9f3a"
+            ],
+            [
+             0.8888888888888888,
+             "#fdca26"
+            ],
+            [
+             1,
+             "#f0f921"
+            ]
+           ],
+           "sequentialminus": [
+            [
+             0,
+             "#0d0887"
+            ],
+            [
+             0.1111111111111111,
+             "#46039f"
+            ],
+            [
+             0.2222222222222222,
+             "#7201a8"
+            ],
+            [
+             0.3333333333333333,
+             "#9c179e"
+            ],
+            [
+             0.4444444444444444,
+             "#bd3786"
+            ],
+            [
+             0.5555555555555556,
+             "#d8576b"
+            ],
+            [
+             0.6666666666666666,
+             "#ed7953"
+            ],
+            [
+             0.7777777777777778,
+             "#fb9f3a"
+            ],
+            [
+             0.8888888888888888,
+             "#fdca26"
+            ],
+            [
+             1,
+             "#f0f921"
+            ]
+           ]
+          },
+          "colorway": [
+           "#636efa",
+           "#EF553B",
+           "#00cc96",
+           "#ab63fa",
+           "#FFA15A",
+           "#19d3f3",
+           "#FF6692",
+           "#B6E880",
+           "#FF97FF",
+           "#FECB52"
+          ],
+          "font": {
+           "color": "#2a3f5f"
+          },
+          "geo": {
+           "bgcolor": "white",
+           "lakecolor": "white",
+           "landcolor": "#E5ECF6",
+           "showlakes": true,
+           "showland": true,
+           "subunitcolor": "white"
+          },
+          "hoverlabel": {
+           "align": "left"
+          },
+          "hovermode": "closest",
+          "mapbox": {
+           "style": "light"
+          },
+          "paper_bgcolor": "white",
+          "plot_bgcolor": "#E5ECF6",
+          "polar": {
+           "angularaxis": {
+            "gridcolor": "white",
+            "linecolor": "white",
+            "ticks": ""
+           },
+           "bgcolor": "#E5ECF6",
+           "radialaxis": {
+            "gridcolor": "white",
+            "linecolor": "white",
+            "ticks": ""
+           }
+          },
+          "scene": {
+           "xaxis": {
+            "backgroundcolor": "#E5ECF6",
+            "gridcolor": "white",
+            "gridwidth": 2,
+            "linecolor": "white",
+            "showbackground": true,
+            "ticks": "",
+            "zerolinecolor": "white"
+           },
+           "yaxis": {
+            "backgroundcolor": "#E5ECF6",
+            "gridcolor": "white",
+            "gridwidth": 2,
+            "linecolor": "white",
+            "showbackground": true,
+            "ticks": "",
+            "zerolinecolor": "white"
+           },
+           "zaxis": {
+            "backgroundcolor": "#E5ECF6",
+            "gridcolor": "white",
+            "gridwidth": 2,
+            "linecolor": "white",
+            "showbackground": true,
+            "ticks": "",
+            "zerolinecolor": "white"
+           }
+          },
+          "shapedefaults": {
+           "line": {
+            "color": "#2a3f5f"
+           }
+          },
+          "ternary": {
+           "aaxis": {
+            "gridcolor": "white",
+            "linecolor": "white",
+            "ticks": ""
+           },
+           "baxis": {
+            "gridcolor": "white",
+            "linecolor": "white",
+            "ticks": ""
+           },
+           "bgcolor": "#E5ECF6",
+           "caxis": {
+            "gridcolor": "white",
+            "linecolor": "white",
+            "ticks": ""
+           }
+          },
+          "title": {
+           "x": 0.05
+          },
+          "xaxis": {
+           "automargin": true,
+           "gridcolor": "white",
+           "linecolor": "white",
+           "ticks": "",
+           "title": {
+            "standoff": 15
+           },
+           "zerolinecolor": "white",
+           "zerolinewidth": 2
+          },
+          "yaxis": {
+           "automargin": true,
+           "gridcolor": "white",
+           "linecolor": "white",
+           "ticks": "",
+           "title": {
+            "standoff": 15
+           },
+           "zerolinecolor": "white",
+           "zerolinewidth": 2
+          }
+         }
+        },
+        "xaxis": {
+         "anchor": "y",
+         "domain": [
+          0,
+          0.45
+         ],
+         "matches": "x3",
+         "showticklabels": false
+        },
+        "xaxis2": {
+         "anchor": "y2",
+         "domain": [
+          0.55,
+          1
+         ],
+         "matches": "x4",
+         "showticklabels": false
+        },
+        "xaxis3": {
+         "anchor": "y3",
+         "domain": [
+          0,
+          0.45
+         ],
+         "title": {
+          "text": "Distance"
+         }
+        },
+        "xaxis4": {
+         "anchor": "y4",
+         "domain": [
+          0.55,
+          1
+         ],
+         "title": {
+          "text": "Distance"
+         }
+        },
+        "yaxis": {
+         "anchor": "x",
+         "domain": [
+          0.55,
+          1
+         ],
+         "range": [
+          0,
+          80
+         ],
+         "title": {
+          "text": "# Samples"
+         }
+        },
+        "yaxis2": {
+         "anchor": "x2",
+         "domain": [
+          0.55,
+          1
+         ],
+         "matches": "y",
+         "range": [
+          0,
+          80
+         ],
+         "showticklabels": false
+        },
+        "yaxis3": {
+         "anchor": "x3",
+         "domain": [
+          0,
+          0.45
+         ],
+         "range": [
+          0,
+          80
+         ],
+         "title": {
+          "text": "# Samples"
+         }
+        },
+        "yaxis4": {
+         "anchor": "x4",
+         "domain": [
+          0,
+          0.45
+         ],
+         "matches": "y3",
+         "range": [
+          0,
+          80
+         ],
+         "showticklabels": false
+        }
+       }
+      },
+      "text/html": [
+       "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# COLLAPSED\n", + "import torch\n", + "from nerfstudio.cameras.rays import RayBundle\n", + "from nerfstudio.model_components import ray_sampler\n", + "import plotly.graph_objects as go\n", + "from plotly.subplots import make_subplots\n", + "\n", + "\n", + "num_samples = 1000\n", + "near = 2\n", + "far = 5\n", + "train_stratified = False\n", + "\n", + "samplers = [\n", + " ray_sampler.UniformSampler,\n", + " ray_sampler.LinearDisparitySampler,\n", + " ray_sampler.SqrtSampler,\n", + " ray_sampler.LogSampler,\n", + "]\n", + "\n", + "fig = make_subplots(\n", + " rows=2,\n", + " cols=2,\n", + " subplot_titles=(\"Uniform\", \"Linear in Disparity\", \"Square Root\", \"Log Sampler\"),\n", + " shared_xaxes=True,\n", + " shared_yaxes=True,\n", + " vertical_spacing=0.1,\n", + ")\n", + "\n", + "for i, Sampler in enumerate(samplers):\n", + " sampler = Sampler(num_samples=num_samples, train_stratified=train_stratified)\n", + "\n", + " ray_bundle = RayBundle(\n", + " origins=torch.ones([1, 3]),\n", + " directions=torch.ones([1, 3]),\n", + " pixel_area=torch.ones([1, 1]),\n", + " nears=torch.ones([1, 1]) * near,\n", + " fars=torch.ones([1, 1]) * far,\n", + " )\n", + "\n", + " samples = sampler.generate_ray_samples(ray_bundle)\n", + "\n", + " trace = go.Histogram(x=samples.frustums.starts[0, :, 0], nbinsx=50)\n", + " fig.append_trace(trace, i // 2 + 1, i % 2 + 1)\n", + "\n", + "fig.update_yaxes(title_text=\"# Samples\", row=1, col=1)\n", + "fig.update_yaxes(title_text=\"# Samples\", row=2, col=1)\n", + "fig.update_xaxes(title_text=\"Distance\", row=2, col=1)\n", + "fig.update_xaxes(title_text=\"Distance\", row=2, col=2)\n", + "\n", + "# Overlay both histograms\n", + "fig.update_layout(height=700, hovermode=False, showlegend=False, margin=dict(l=20, r=20, t=50, b=20))\n", + "fig.update_yaxes(range=[0, 80])\n", + "fig.update_traces(opacity=0.7)\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PDF Sampler\n", + "\n", + "The Probability Distribution Function (PDF) Sampler generates samples that match a given distribution. \n", + "\n", + "In the example below we first create a `UniformSampler` to generate a set of initial samples. We then assign weights to each of these samples to define the PDF (here it is an arbitrary function, but usually you would use the predicted weights from the field). The left plot the target PDF, on the right we plot a histogram of samples generated from the `PDFSampler`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "opacity": 0.7, + "type": "scatter", + "x": [ + 2, + 2.1500000953674316, + 2.1500000953674316, + 2.299999952316284, + 2.299999952316284, + 2.450000047683716, + 2.450000047683716, + 2.5999999046325684, + 2.5999999046325684, + 2.75, + 2.75, + 2.9000000953674316, + 2.9000000953674316, + 3.049999952316284, + 3.049999952316284, + 3.200000047683716, + 3.200000047683716, + 3.3499999046325684, + 3.3499999046325684, + 3.5, + 3.5, + 3.6500000953674316, + 3.6500000953674316, + 3.799999952316284, + 3.799999952316284, + 3.950000047683716, + 3.950000047683716, + 4.100000381469727, + 4.100000381469727, + 4.25, + 4.25, + 4.400000095367432, + 4.400000095367432, + 4.550000190734863, + 4.550000190734863, + 4.699999809265137, + 4.699999809265137, + 4.849999904632568, + 4.849999904632568, + 5 + ], + "xaxis": "x", + "y": [ + 0.013052567839622498, + 0.013052567839622498, + 0.03800889104604721, + 0.03800889104604721, + 0.057813677936792374, + 0.057813677936792374, + 0.06855124980211258, + 0.06855124980211258, + 0.06847988814115524, + 0.06847988814115524, + 0.058445800095796585, + 0.058445800095796585, + 0.041673533618450165, + 0.041673533618450165, + 0.022983169183135033, + 0.022983169183135033, + 0.007623155601322651, + 0.007623155601322651, + 0, + 0, + 0.002611340256407857, + 0.002611340256407857, + 0.015439355745911598, + 0.015439355745911598, + 0.03595082834362984, + 0.03595082834362984, + 0.05970405042171478, + 0.05970405042171478, + 0.0814160406589508, + 0.0814160406589508, + 0.09623293578624725, + 0.09623293578624725, + 0.10089751332998276, + 0.10089751332998276, + 0.09453219175338745, + 0.09453219175338745, + 0.07884885370731354, + 0.07884885370731354, + 0.057734981179237366, + 0.057734981179237366 + ], + "yaxis": "y" + }, + { + "nbinsx": 100, + "opacity": 0.7, + "type": "histogram", + "x": [ + 2.0039002895355225, + 2.0117006301879883, + 2.019501209259033, + 2.027301549911499, + 2.035102128982544, + 2.0429024696350098, + 2.0507028102874756, + 2.0585033893585205, + 2.0663037300109863, + 2.074104070663452, + 2.081904649734497, + 2.089704990386963, + 2.097505569458008, + 2.1053059101104736, + 2.1131062507629395, + 2.1209068298339844, + 2.12870717048645, + 2.136507749557495, + 2.144308090209961, + 2.151012420654297, + 2.1547579765319824, + 2.158503532409668, + 2.1622490882873535, + 2.165994644165039, + 2.1697402000427246, + 2.17348575592041, + 2.1772313117980957, + 2.1809771060943604, + 2.184722423553467, + 2.1884679794311523, + 2.192213535308838, + 2.1959590911865234, + 2.199704647064209, + 2.2034502029418945, + 2.207195997238159, + 2.2109413146972656, + 2.214686870574951, + 2.218432664871216, + 2.2221782207489014, + 2.225923538208008, + 2.2296690940856934, + 2.233414888381958, + 2.2371604442596436, + 2.24090576171875, + 2.2446515560150146, + 2.2483971118927, + 2.2521424293518066, + 2.2558882236480713, + 2.259633779525757, + 2.2633793354034424, + 2.267124891281128, + 2.2708704471588135, + 2.274616003036499, + 2.2783615589141846, + 2.28210711479187, + 2.2858526706695557, + 2.289598226547241, + 2.2933437824249268, + 2.2970893383026123, + 2.300590991973877, + 2.3032426834106445, + 2.305894374847412, + 2.3085460662841797, + 2.3111977577209473, + 2.313849449157715, + 2.3165011405944824, + 2.31915283203125, + 2.3218045234680176, + 2.324456214904785, + 2.3271079063415527, + 2.3297595977783203, + 2.332411289215088, + 2.3350629806518555, + 2.337714672088623, + 2.3403663635253906, + 2.343018054962158, + 2.3456695079803467, + 2.3483211994171143, + 2.350973129272461, + 2.3536248207092285, + 2.356276273727417, + 2.3589279651641846, + 2.361579656600952, + 2.3642313480377197, + 2.3668830394744873, + 2.369534730911255, + 2.3721864223480225, + 2.37483811378479, + 2.3774898052215576, + 2.380141496658325, + 2.3827931880950928, + 2.3854446411132812, + 2.388096570968628, + 2.3907480239868164, + 2.393399715423584, + 2.3960514068603516, + 2.398703098297119, + 2.4013547897338867, + 2.4040064811706543, + 2.406658172607422, + 2.4093098640441895, + 2.411961555480957, + 2.4146132469177246, + 2.417264938354492, + 2.4199166297912598, + 2.4225683212280273, + 2.425220012664795, + 2.4278717041015625, + 2.43052339553833, + 2.4331750869750977, + 2.4358267784118652, + 2.438478469848633, + 2.4411301612854004, + 2.443781852722168, + 2.4464335441589355, + 2.449085235595703, + 2.4514994621276855, + 2.4537887573242188, + 2.456077814102173, + 2.458367109298706, + 2.46065616607666, + 2.4629454612731934, + 2.4652347564697266, + 2.4675238132476807, + 2.469813108444214, + 2.472102165222168, + 2.474391460418701, + 2.4766807556152344, + 2.4789698123931885, + 2.4812591075897217, + 2.483548402786255, + 2.485837459564209, + 2.488126754760742, + 2.4904160499572754, + 2.4927051067352295, + 2.4949944019317627, + 2.497283458709717, + 2.49957275390625, + 2.501862049102783, + 2.5041511058807373, + 2.5064404010772705, + 2.5087296962738037, + 2.511018753051758, + 2.513308048248291, + 2.515597343444824, + 2.5178864002227783, + 2.5201756954193115, + 2.5224647521972656, + 2.524754047393799, + 2.527043342590332, + 2.529332399368286, + 2.5316216945648193, + 2.5339109897613525, + 2.5362000465393066, + 2.53848934173584, + 2.540778636932373, + 2.543067693710327, + 2.5453569889068604, + 2.5476460456848145, + 2.5499353408813477, + 2.552224636077881, + 2.554513692855835, + 2.556802988052368, + 2.5590920448303223, + 2.5613813400268555, + 2.5636706352233887, + 2.565959930419922, + 2.568248987197876, + 2.570538282394409, + 2.5728273391723633, + 2.5751166343688965, + 2.5774059295654297, + 2.579694986343384, + 2.581984281539917, + 2.58427357673645, + 2.5865626335144043, + 2.5888519287109375, + 2.5911412239074707, + 2.593430280685425, + 2.595719575881958, + 2.598008632659912, + 2.6002981662750244, + 2.6025896072387695, + 2.6048808097839355, + 2.6071720123291016, + 2.6094634532928467, + 2.611754894256592, + 2.6140458583831787, + 2.616337299346924, + 2.61862850189209, + 2.620919704437256, + 2.623211145401001, + 2.625502586364746, + 2.627793788909912, + 2.630084991455078, + 2.632376194000244, + 2.6346676349639893, + 2.6369590759277344, + 2.6392502784729004, + 2.6415414810180664, + 2.6438326835632324, + 2.6461238861083984, + 2.6484153270721436, + 2.6507067680358887, + 2.6529979705810547, + 2.6552891731262207, + 2.6575803756713867, + 2.659871816635132, + 2.662163257598877, + 2.664454460144043, + 2.666745662689209, + 2.669036865234375, + 2.671328067779541, + 2.673619508743286, + 2.6759109497070312, + 2.6782021522521973, + 2.6804933547973633, + 2.6827845573425293, + 2.6850759983062744, + 2.6873672008514404, + 2.6896584033966064, + 2.6919498443603516, + 2.6942410469055176, + 2.6965322494506836, + 2.6988236904144287, + 2.701115131378174, + 2.70340633392334, + 2.705697536468506, + 2.707988739013672, + 2.710280179977417, + 2.712571620941162, + 2.714862823486328, + 2.717154026031494, + 2.71944522857666, + 2.721736431121826, + 2.724027633666992, + 2.7263193130493164, + 2.7286105155944824, + 2.7309017181396484, + 2.7331929206848145, + 2.7354843616485596, + 2.7377758026123047, + 2.7400670051574707, + 2.7423582077026367, + 2.7446494102478027, + 2.7469406127929688, + 2.749232053756714, + 2.751746654510498, + 2.754373788833618, + 2.7570009231567383, + 2.7596282958984375, + 2.7622551918029785, + 2.7648825645446777, + 2.767509698867798, + 2.770136833190918, + 2.772764205932617, + 2.7753913402557373, + 2.7780184745788574, + 2.7806458473205566, + 2.7832727432250977, + 2.785900115966797, + 2.788527488708496, + 2.791154384613037, + 2.7937817573547363, + 2.7964086532592773, + 2.7990360260009766, + 2.8016631603240967, + 2.804290294647217, + 2.806917667388916, + 2.8095450401306152, + 2.8121719360351562, + 2.8147993087768555, + 2.8174264430999756, + 2.8200535774230957, + 2.822680950164795, + 2.825307846069336, + 2.827935218811035, + 2.8305623531341553, + 2.8331894874572754, + 2.8358168601989746, + 2.8384439945220947, + 2.841071128845215, + 2.843698501586914, + 2.846325397491455, + 2.8489527702331543, + 2.8515799045562744, + 2.8542070388793945, + 2.8568344116210938, + 2.8594613075256348, + 2.862088680267334, + 2.864715814590454, + 2.867342948913574, + 2.8699703216552734, + 2.8725974559783936, + 2.8752245903015137, + 2.877851963043213, + 2.880478858947754, + 2.883106231689453, + 2.8857333660125732, + 2.8883605003356934, + 2.8909878730773926, + 2.8936147689819336, + 2.896242141723633, + 2.898869037628174, + 2.901982307434082, + 2.9054622650146484, + 2.908942222595215, + 2.9124221801757812, + 2.9159021377563477, + 2.919382095336914, + 2.9228615760803223, + 2.9263415336608887, + 2.929821491241455, + 2.9333014488220215, + 2.936781406402588, + 2.9402613639831543, + 2.9437413215637207, + 2.947221279144287, + 2.9507012367248535, + 2.95418119430542, + 2.9576611518859863, + 2.9611411094665527, + 2.964621067047119, + 2.9681007862091064, + 2.971580743789673, + 2.9750607013702393, + 2.9785404205322266, + 2.982020378112793, + 2.9855003356933594, + 2.988980293273926, + 2.992460250854492, + 2.9959402084350586, + 2.999420166015625, + 3.0029001235961914, + 3.006380081176758, + 3.009860038757324, + 3.0133399963378906, + 3.016819953918457, + 3.0202996730804443, + 3.02377986907959, + 3.027259588241577, + 3.0307395458221436, + 3.034219264984131, + 3.0376992225646973, + 3.0411791801452637, + 3.04465913772583, + 3.0481390953063965, + 3.0525364875793457, + 3.057988405227661, + 3.0634403228759766, + 3.068892002105713, + 3.0743441581726074, + 3.0797958374023438, + 3.08524751663208, + 3.0906996726989746, + 3.096151351928711, + 3.1016035079956055, + 3.107055187225342, + 3.1125071048736572, + 3.1179590225219727, + 3.123410940170288, + 3.1288628578186035, + 3.134314775466919, + 3.1397666931152344, + 3.14521861076355, + 3.1506705284118652, + 3.1561222076416016, + 3.161573886871338, + 3.1670260429382324, + 3.1724777221679688, + 3.1779298782348633, + 3.1833815574645996, + 3.188833713531494, + 3.1942853927612305, + 3.199737071990967, + 3.209711790084839, + 3.2199153900146484, + 3.230119228363037, + 3.2403225898742676, + 3.2505264282226562, + 3.260730028152466, + 3.2709336280822754, + 3.281137228012085, + 3.2913408279418945, + 3.3015449047088623, + 3.311748504638672, + 3.3219521045684814, + 3.332155704498291, + 3.3423593044281006, + 3.3545167446136475, + 3.3724982738494873, + 3.3904805183410645, + 3.4084625244140625, + 3.4264445304870605, + 3.4444267749786377, + 3.462409019470215, + 3.480390787124634, + 3.498373031616211, + 3.512968063354492, + 3.5272269248962402, + 3.54148530960083, + 3.555744171142578, + 3.570002555847168, + 3.584261417388916, + 3.598519802093506, + 3.612778663635254, + 3.6270365715026855, + 3.6412954330444336, + 3.6527533531188965, + 3.6598219871520996, + 3.6668906211853027, + 3.6739590167999268, + 3.681027889251709, + 3.688096523284912, + 3.695164680480957, + 3.70223331451416, + 3.7093024253845215, + 3.7163705825805664, + 3.7234394550323486, + 3.7305080890655518, + 3.737576484680176, + 3.744645118713379, + 3.751713514328003, + 3.758782386779785, + 3.76585054397583, + 3.7729194164276123, + 3.7799880504608154, + 3.7870564460754395, + 3.7941250801086426, + 3.8006608486175537, + 3.8045742511749268, + 3.8084874153137207, + 3.8124008178710938, + 3.816314220428467, + 3.8202273845672607, + 3.824141025543213, + 3.828054189682007, + 3.831967353820801, + 3.835880756378174, + 3.8397939205169678, + 3.843707323074341, + 3.8476204872131348, + 3.851533889770508, + 3.855447292327881, + 3.859360456466675, + 3.863274097442627, + 3.867187023162842, + 3.871100425720215, + 3.875014066696167, + 3.878927230834961, + 3.882840633392334, + 3.886753797531128, + 3.890666961669922, + 3.894580602645874, + 3.898493528366089, + 3.902407169342041, + 3.906320333480835, + 3.910233497619629, + 3.914147138595581, + 3.918060302734375, + 3.921973705291748, + 3.925887107849121, + 3.929800033569336, + 3.933713674545288, + 3.937626838684082, + 3.941540241241455, + 3.945453643798828, + 3.949366807937622, + 3.952162265777588, + 3.954742193222046, + 3.9573216438293457, + 3.9599015712738037, + 3.9624814987182617, + 3.9650611877441406, + 3.9676411151885986, + 3.9702205657958984, + 3.9728004932403564, + 3.9753804206848145, + 3.9779598712921143, + 3.9805397987365723, + 3.983119487762451, + 3.985699415206909, + 3.988279342651367, + 3.990858793258667, + 3.993438720703125, + 3.996018409729004, + 3.998598098754883, + 4.001177787780762, + 4.003757476806641, + 4.0063371658325195, + 4.008917331695557, + 4.0114970207214355, + 4.014077186584473, + 4.016656398773193, + 4.0192365646362305, + 4.021816253662109, + 4.024395942687988, + 4.026975631713867, + 4.029555320739746, + 4.032135009765625, + 4.034715175628662, + 4.037294387817383, + 4.039874076843262, + 4.042454242706299, + 4.045034408569336, + 4.047614097595215, + 4.050193786621094, + 4.052773475646973, + 4.055353164672852, + 4.0579328536987305, + 4.060512542724609, + 4.063092231750488, + 4.065672397613525, + 4.068252086639404, + 4.070831775665283, + 4.07341194152832, + 4.075991153717041, + 4.078571319580078, + 4.081151008605957, + 4.083730697631836, + 4.086310386657715, + 4.088890075683594, + 4.091469764709473, + 4.09404993057251, + 4.096629619598389, + 4.099209308624268, + 4.1013641357421875, + 4.103331089019775, + 4.105298042297363, + 4.107264995574951, + 4.109231948852539, + 4.111199378967285, + 4.113166332244873, + 4.115133285522461, + 4.117100715637207, + 4.119067668914795, + 4.121034622192383, + 4.123002052307129, + 4.124969005584717, + 4.126935958862305, + 4.128902912139893, + 4.1308698654174805, + 4.132836818695068, + 4.134803771972656, + 4.136771202087402, + 4.138737678527832, + 4.140705108642578, + 4.142672061920166, + 4.144639015197754, + 4.1466064453125, + 4.148573398590088, + 4.150540351867676, + 4.152507781982422, + 4.15447473526001, + 4.156441688537598, + 4.1584086418151855, + 4.160375595092773, + 4.162342548370361, + 4.164309501647949, + 4.166276931762695, + 4.168243885040283, + 4.170210838317871, + 4.172178268432617, + 4.174145221710205, + 4.176112174987793, + 4.178079605102539, + 4.180046558380127, + 4.182013511657715, + 4.183980464935303, + 4.185947418212891, + 4.1879143714904785, + 4.189881324768066, + 4.191848278045654, + 4.193815231323242, + 4.195782661437988, + 4.197749614715576, + 4.199716567993164, + 4.20168399810791, + 4.203650951385498, + 4.205617904663086, + 4.207585334777832, + 4.209551811218262, + 4.211519241333008, + 4.213486194610596, + 4.215453147888184, + 4.2174201011657715, + 4.219387054443359, + 4.2213544845581055, + 4.223321437835693, + 4.225288391113281, + 4.227255821228027, + 4.229222297668457, + 4.231189727783203, + 4.233156681060791, + 4.235123634338379, + 4.237091064453125, + 4.239058017730713, + 4.241024971008301, + 4.242991924285889, + 4.244958877563477, + 4.2469258308410645, + 4.248892784118652, + 4.250740051269531, + 4.252432823181152, + 4.254125595092773, + 4.2558183670043945, + 4.257511138916016, + 4.259203910827637, + 4.2608962059021, + 4.262588977813721, + 4.2642822265625, + 4.265974521636963, + 4.267666816711426, + 4.269359588623047, + 4.271052837371826, + 4.272745132446289, + 4.27443790435791, + 4.276130676269531, + 4.277823448181152, + 4.279515743255615, + 4.2812089920043945, + 4.282901763916016, + 4.2845940589904785, + 4.286286354064941, + 4.287979602813721, + 4.289672374725342, + 4.291364669799805, + 4.293057441711426, + 4.294750213623047, + 4.296442985534668, + 4.298135757446289, + 4.29982852935791, + 4.301521301269531, + 4.303213596343994, + 4.304906368255615, + 4.306599140167236, + 4.308291912078857, + 4.30998420715332, + 4.3116774559021, + 4.313370227813721, + 4.315062522888184, + 4.316755294799805, + 4.318448066711426, + 4.320140838623047, + 4.321833610534668, + 4.323526382446289, + 4.32521915435791, + 4.326911449432373, + 4.328604221343994, + 4.330296993255615, + 4.331989765167236, + 4.333682060241699, + 4.33537483215332, + 4.337067604064941, + 4.3387603759765625, + 4.340453147888184, + 4.342145919799805, + 4.343838691711426, + 4.345530986785889, + 4.34722375869751, + 4.348916530609131, + 4.350609302520752, + 4.352301597595215, + 4.353994369506836, + 4.355687141418457, + 4.357379913330078, + 4.359072685241699, + 4.36076545715332, + 4.362458229064941, + 4.3641510009765625, + 4.365843772888184, + 4.367536544799805, + 4.369228839874268, + 4.370921611785889, + 4.37261438369751, + 4.374307155609131, + 4.375999450683594, + 4.377692222595215, + 4.379384994506836, + 4.381077766418457, + 4.382770538330078, + 4.384463310241699, + 4.38615608215332, + 4.387848377227783, + 4.389541149139404, + 4.391233921051025, + 4.3929266929626465, + 4.394618988037109, + 4.3963117599487305, + 4.398004531860352, + 4.399697303771973, + 4.401331424713135, + 4.402953147888184, + 4.404574871063232, + 4.406196117401123, + 4.407817363739014, + 4.409439563751221, + 4.411060810089111, + 4.41268253326416, + 4.414303779602051, + 4.4159255027771, + 4.41754674911499, + 4.419168472290039, + 4.42078971862793, + 4.4224114418029785, + 4.424032688140869, + 4.425654411315918, + 4.427275657653809, + 4.428897380828857, + 4.430518627166748, + 4.432140350341797, + 4.4337615966796875, + 4.435382843017578, + 4.437005043029785, + 4.438626289367676, + 4.440247535705566, + 4.441868782043457, + 4.443490982055664, + 4.445112228393555, + 4.446733474731445, + 4.448355197906494, + 4.449976444244385, + 4.451598167419434, + 4.453219413757324, + 4.454841136932373, + 4.456462383270264, + 4.4580841064453125, + 4.459705352783203, + 4.461327075958252, + 4.462948322296143, + 4.464570045471191, + 4.466191291809082, + 4.467813014984131, + 4.4694342613220215, + 4.4710564613342285, + 4.472677707672119, + 4.47429895401001, + 4.475920677185059, + 4.477542400360107, + 4.479163646697998, + 4.480784893035889, + 4.4824066162109375, + 4.484028339385986, + 4.485649585723877, + 4.487271308898926, + 4.488893032073975, + 4.490514278411865, + 4.492136001586914, + 4.493757247924805, + 4.4953789710998535, + 4.497000217437744, + 4.498621940612793, + 4.500243186950684, + 4.501864910125732, + 4.503486156463623, + 4.505107879638672, + 4.5067291259765625, + 4.508350849151611, + 4.50997257232666, + 4.511593818664551, + 4.513215065002441, + 4.514836311340332, + 4.516458511352539, + 4.51807975769043, + 4.51970100402832, + 4.521322250366211, + 4.52294397354126, + 4.524565696716309, + 4.526186943054199, + 4.52780818939209, + 4.529430389404297, + 4.5310516357421875, + 4.532672882080078, + 4.534294605255127, + 4.535915851593018, + 4.537537574768066, + 4.539158821105957, + 4.540780544281006, + 4.5424017906188965, + 4.544023513793945, + 4.545645236968994, + 4.547266483306885, + 4.548888206481934, + 4.550540924072266, + 4.5522613525390625, + 4.553980827331543, + 4.55570125579834, + 4.557421684265137, + 4.559141635894775, + 4.560862064361572, + 4.562582015991211, + 4.564302444458008, + 4.5660223960876465, + 4.567742824554443, + 4.56946325302124, + 4.571183681488037, + 4.572904109954834, + 4.574624061584473, + 4.5763444900512695, + 4.578064441680908, + 4.579784393310547, + 4.581504821777344, + 4.583225250244141, + 4.5849456787109375, + 4.586665630340576, + 4.588385581970215, + 4.590106010437012, + 4.59182596206665, + 4.593546390533447, + 4.595266819000244, + 4.596987247467041, + 4.59870719909668, + 4.600427627563477, + 4.602148056030273, + 4.60386848449707, + 4.605587959289551, + 4.607308387756348, + 4.6090288162231445, + 4.610749244689941, + 4.612468719482422, + 4.614189147949219, + 4.615909576416016, + 4.617629528045654, + 4.619349956512451, + 4.621070384979248, + 4.622790813446045, + 4.624510765075684, + 4.626230716705322, + 4.627951622009277, + 4.629671573638916, + 4.631391525268555, + 4.633111953735352, + 4.634832382202148, + 4.636552810668945, + 4.638272762298584, + 4.639992713928223, + 4.6417131423950195, + 4.643433094024658, + 4.645153522491455, + 4.646873950958252, + 4.648594379425049, + 4.6503143310546875, + 4.652034282684326, + 4.653754711151123, + 4.65547513961792, + 4.657195568084717, + 4.6589155197143555, + 4.660635948181152, + 4.662356376647949, + 4.66407585144043, + 4.665796279907227, + 4.667516708374023, + 4.669236660003662, + 4.670957088470459, + 4.672677516937256, + 4.674397945404053, + 4.676117897033691, + 4.67783784866333, + 4.679558277130127, + 4.681278705596924, + 4.682999134063721, + 4.684719085693359, + 4.686439514160156, + 4.688159465789795, + 4.689879417419434, + 4.6915998458862305, + 4.693320274353027, + 4.695040702819824, + 4.696760654449463, + 4.698480606079102, + 4.7002363204956055, + 4.702260494232178, + 4.704284191131592, + 4.706308364868164, + 4.708332538604736, + 4.710355758666992, + 4.712380409240723, + 4.7144036293029785, + 4.716427803039551, + 4.718451976776123, + 4.720475196838379, + 4.722499370574951, + 4.724523067474365, + 4.7265472412109375, + 4.72857141494751, + 4.730594635009766, + 4.732619285583496, + 4.734642505645752, + 4.736666679382324, + 4.7386908531188965, + 4.740714073181152, + 4.742738246917725, + 4.744761943817139, + 4.746786117553711, + 4.748810291290283, + 4.750833511352539, + 4.7528581619262695, + 4.754881381988525, + 4.756905555725098, + 4.75892972946167, + 4.760953426361084, + 4.762977600097656, + 4.76500129699707, + 4.767024993896484, + 4.769049167633057, + 4.7710723876953125, + 4.773097038269043, + 4.775120258331299, + 4.777144432067871, + 4.779168605804443, + 4.781191825866699, + 4.78321647644043, + 4.7852396965026855, + 4.787263870239258, + 4.78928804397583, + 4.791311264038086, + 4.793335914611816, + 4.795359134674072, + 4.7973833084106445, + 4.799407482147217, + 4.801431179046631, + 4.803455352783203, + 4.805479049682617, + 4.807502746582031, + 4.8095269203186035, + 4.811550617218018, + 4.81357479095459, + 4.815598487854004, + 4.817622184753418, + 4.81964635848999, + 4.821669578552246, + 4.823694229125977, + 4.825717449188232, + 4.827741622924805, + 4.829765796661377, + 4.831789016723633, + 4.833813667297363, + 4.835836887359619, + 4.837861061096191, + 4.839885234832764, + 4.841908931732178, + 4.84393310546875, + 4.845956802368164, + 4.847980499267578, + 4.850006103515625, + 4.852660655975342, + 4.855315208435059, + 4.857970237731934, + 4.86062479019165, + 4.863279819488525, + 4.8659348487854, + 4.868589401245117, + 4.871243953704834, + 4.873898506164551, + 4.876553535461426, + 4.879208087921143, + 4.881862640380859, + 4.884518146514893, + 4.887172698974609, + 4.889827728271484, + 4.892482280731201, + 4.895136833190918, + 4.897791862487793, + 4.90044641494751, + 4.903100967407227, + 4.905755996704102, + 4.908410549163818, + 4.911065101623535, + 4.91372013092041, + 4.916375160217285, + 4.919029712677002, + 4.921684265136719, + 4.924339294433594, + 4.9269938468933105, + 4.929648399353027, + 4.932303428649902, + 4.934958457946777, + 4.937613010406494, + 4.940267562866211, + 4.942922592163086, + 4.945577144622803, + 4.948232173919678, + 4.950887203216553, + 4.9535417556762695, + 4.9561967849731445, + 4.958850860595703, + 4.961505889892578, + 4.964160442352295, + 4.96681547164917, + 4.969470500946045, + 4.972125053405762, + 4.974780082702637, + 4.9774346351623535, + 4.9800896644592285, + 4.982744216918945, + 4.98539924621582, + 4.988053321838379, + 4.990708351135254, + 4.993362903594971, + 4.996017932891846 + ], + "xaxis": "x2", + "yaxis": "y2" + } + ], + "layout": { + "annotations": [ + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "PDF", + "x": 0.225, + "xanchor": "center", + "xref": "paper", + "y": 1, + "yanchor": "bottom", + "yref": "paper" + }, + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "Samples", + "x": 0.775, + "xanchor": "center", + "xref": "paper", + "y": 1, + "yanchor": "bottom", + "yref": "paper" + } + ], + "height": 400, + "hovermode": false, + "margin": { + "b": 20, + "l": 20, + "r": 20, + "t": 50 + }, + "showlegend": false, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 0.45 + ], + "title": { + "text": "Distance" + } + }, + "xaxis2": { + "anchor": "y2", + "domain": [ + 0.55, + 1 + ], + "title": { + "text": "Distance" + } + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0, + 1 + ] + }, + "yaxis2": { + "anchor": "x2", + "domain": [ + 0, + 1 + ], + "title": { + "text": "# Samples" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# COLLAPSED\n", + "import torch\n", + "from nerfstudio.cameras.rays import RayBundle\n", + "from nerfstudio.model_components import ray_sampler\n", + "import plotly.graph_objects as go\n", + "from plotly.subplots import make_subplots\n", + "\n", + "num_coarse_samples = 20\n", + "num_samples = 1000\n", + "near = 2\n", + "far = 5\n", + "train_stratified = False\n", + "\n", + "fig = make_subplots(\n", + " rows=1,\n", + " cols=2,\n", + " subplot_titles=(\"PDF\", \"Samples\"),\n", + ")\n", + "\n", + "uniform_sampler = ray_sampler.UniformSampler(num_samples=num_coarse_samples, train_stratified=train_stratified)\n", + "pdf_sampler = ray_sampler.PDFSampler(num_samples=num_samples, train_stratified=train_stratified, include_original=False)\n", + "\n", + "ray_bundle = RayBundle(\n", + " origins=torch.ones([1, 3]),\n", + " directions=torch.ones([1, 3]),\n", + " pixel_area=torch.ones([1, 1]),\n", + " nears=torch.ones([1, 1]) * near,\n", + " fars=torch.ones([1, 1]) * far,\n", + ")\n", + "\n", + "coarse_ray_samples = uniform_sampler(ray_bundle)\n", + "\n", + "# Generate arbitrary PDF\n", + "weights = torch.ones(num_coarse_samples)\n", + "weights += torch.sin(torch.linspace(0, 3 * torch.pi, num_coarse_samples))\n", + "weights += torch.sin(torch.linspace(0, 0.5 * torch.pi, num_coarse_samples))\n", + "weights -= torch.min(weights)\n", + "weights /= torch.sum(weights)\n", + "\n", + "samples = pdf_sampler.generate_ray_samples(ray_bundle, coarse_ray_samples, weights[None, :, None], num_samples)\n", + "\n", + "# Plotting stuff\n", + "x = torch.ones((num_coarse_samples * 2))\n", + "x[::2] = coarse_ray_samples.frustums.starts[0, :, 0]\n", + "x[1::2] = coarse_ray_samples.frustums.ends[0, :, 0]\n", + "\n", + "y = torch.ones((num_coarse_samples * 2))\n", + "y[::2] = weights\n", + "y[1::2] = weights\n", + "\n", + "pdf_trace = go.Scatter(x=x, y=y)\n", + "fig.append_trace(pdf_trace, 1, 1)\n", + "\n", + "samples_trace = go.Histogram(x=samples.frustums.starts[0, :, 0], nbinsx=100)\n", + "fig.append_trace(samples_trace, 1, 2)\n", + "\n", + "fig.update_yaxes(title_text=\"# Samples\", row=1, col=2)\n", + "fig.update_xaxes(title_text=\"Distance\", row=1, col=1)\n", + "fig.update_xaxes(title_text=\"Distance\", row=1, col=2)\n", + "\n", + "# Overlay both histograms\n", + "fig.update_layout(height=400, hovermode=False, showlegend=False, margin=dict(l=20, r=20, t=50, b=20))\n", + "fig.update_traces(opacity=0.7)\n", + "fig.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.7" + }, + "vscode": { + "interpreter": { + "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/docs/quickstart/custom_dataset.md b/docs/quickstart/custom_dataset.md index 4295bf4bc..5132df1c6 100644 --- a/docs/quickstart/custom_dataset.md +++ b/docs/quickstart/custom_dataset.md @@ -5,7 +5,7 @@ Training model on existing datasets is only so fun. If you would like to train o To process your own data run: ```bash -ns-process-data {video,images,polycam,insta360,record3d} --data {DATA_PATH} --output-dir {PROCESSED_DATA_DIR} +ns-process-data {video,images,polycam,record3d} --data {DATA_PATH} --output-dir {PROCESSED_DATA_DIR} ``` A full set of arguments can be found {doc}`here
`. @@ -20,6 +20,7 @@ We Currently support the following custom data types: | πŸ“± [KIRI Engine](kiri) | IOS or Android | [KIRI Engine App](https://www.kiriengine.com/) | πŸ‡ | | πŸ“± [Record3D](record3d) | IOS with LiDAR | [Record3D app](https://record3d.app/) | πŸ‡ | | πŸ–₯ [Metashape](metashape) | Any | [Metashape](https://www.agisoft.com/) | πŸ‡ | +| πŸ–₯ [RealityCapture](realitycapture) | Any | [RealityCapture](https://www.capturingreality.com/realitycapture) | πŸ‡ | (images_and_video)= @@ -33,12 +34,6 @@ To assist running on custom data we have a script that will process a video or f - COLMAP can be finicky. Try your best to capture overlapping, non-blurry images. ::: -:::{admonition} Note -:class: warning - -- If COLMAP opens in a new window, simply close it to resume processing. - ::: - ### Processing Data ```bash @@ -264,6 +259,8 @@ ns-train nerfacto --data {output directory} ## Metashape +All images must use the same sensor type (but multiple sensors are supported). + 1. Align your images using Metashape. `File -> Workflow -> Align Photos...` ```{image} https://user-images.githubusercontent.com/3310961/203389662-12760210-2b52-49d4-ab21-4f23bfa4a2b3.png @@ -292,6 +289,29 @@ ns-process-data metashape --data {data directory} --xml {xml file} --output-dir ns-train nerfacto --data {output directory} ``` +<<<<<<< HEAD +======= +(realitycapture)= + +## RealityCapture + +1. Align your images using RealityCapture. `ALIGNMENT -> Align Images` + +2. Export the camera alignment as a `csv` file. Choose `Internal/External camera parameters` + +3. Convert the data to the nerfstudio format. + +```bash +ns-process-data realitycapture --data {data directory} --csv {csv file} --output-dir {output directory} +``` + +4. Train with nerfstudio! + +```bash +ns-train nerfacto --data {output directory} +``` + +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb (360_data)= ## 360 Data (Equirectangular) @@ -303,10 +323,17 @@ Equirectangular data is data that has been taken by a 360 camera such as Insta36 For a set of equirectangular images, process the data using the following command: ```bash +<<<<<<< HEAD ns-process-data images --camera-type equirectangular --images-per-equirect {8, or 14} --data {data directory} --output-dir {output directory} ``` The images-per-equirect argument is the number of images that will be sampled from each equirectangular image. We have found that 8 images per equirectangular image is sufficient for most use cases so it defaults to that. However, if you find that there isn't enough detail in the nerf or that colmap is having trouble aligning the images, you can try increasing the number of images per equirectangular image to 14. +======= +ns-process-data images --camera-type equirectangular --images-per-equirect {8, or 14} --crop-factor {top bottom left right} --data {data directory} --output-dir {output directory} +``` + +The images-per-equirect argument is the number of images that will be sampled from each equirectangular image. We have found that 8 images per equirectangular image is sufficient for most use cases so it defaults to that. However, if you find that there isn't enough detail in the nerf or that colmap is having trouble aligning the images, you can try increasing the number of images per equirectangular image to 14. See the video section below for details on cropping. +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb ### Videos @@ -326,4 +353,10 @@ The `crop-factor` argument is optional but often very helpful. This is because e +<<<<<<< HEAD This obscene mesh of human is obviously not helpful in training a nerf so we can remove it by cropping the bottom 20% of the image. This can be done by using the `--crop-factor 0 0.2 0 0` argument. +======= +The pixels representing the distorted hand and head are obviously not useful in training a nerf so we can remove it by cropping the bottom 20% of the image. This can be done by using the `--crop-factor 0 0.2 0 0` argument. + +If cropping only needs to be done from the bottom, you can use the `--crop-bottom [num]` argument which would be the same as doing `--crop-factor 0.0 [num] 0.0 0.0` +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb diff --git a/docs/quickstart/export_geometry.md b/docs/quickstart/export_geometry.md index 28d29fc54..2ffd51895 100644 --- a/docs/quickstart/export_geometry.md +++ b/docs/quickstart/export_geometry.md @@ -19,7 +19,6 @@ Poisson surface reconstruction gives the highest quality meshes. See the steps b > **Note:** > This will only work with a Model that computes or predicts normals, e.g., nerfacto. - 1. Train nerfacto with network settings that predict normals. ```bash @@ -51,7 +50,7 @@ ns-export --help Say you want to simplify and/or smooth a mesh offline, and then you want to texture it with NeRF. You can do that with the following command. It will work for any mesh filetypes that [PyMeshLab](https://pymeshlab.readthedocs.io/en/latest/) can support, for example a `.ply`. ```python -python scripts/texture.py --load-config CONFIG.yml --input-mesh-filename FILENAME --output-dir OUTPUT_DIR +python nerfstudio/scripts/texture.py --load-config CONFIG.yml --input-mesh-filename FILENAME --output-dir OUTPUT_DIR ``` ## Dependencies diff --git a/docs/quickstart/first_nerf.md b/docs/quickstart/first_nerf.md index 73e40a9d8..1d71fac49 100644 --- a/docs/quickstart/first_nerf.md +++ b/docs/quickstart/first_nerf.md @@ -38,8 +38,6 @@ It is possible to load a pretrained model by running ns-train nerfacto --data data/nerfstudio/poster --load-dir {outputs/.../nerfstudio_models} ``` -This will automatically start training. If you do not want it to train, add `--viewer.start-train False` to your training command. - ## Visualize existing run Given a pretrained model checkpoint, you can start the viewer by running @@ -98,9 +96,9 @@ ns-train nerfacto nerfstudio-data --help Each script will have some other minor quirks (like the training script dataparser subcommand needing to come after the model subcommand), read up on them [here](../reference/cli/index.md). -## Tensorboard / WandB +## Tensorboard / WandB / Viewer -We support three different methods to track training progress, using the viewer, [tensorboard](https://www.tensorflow.org/tensorboard), and [Weights and Biases](https://wandb.ai/site). You can specify which visualizer to use by appending `--vis {viewer, tensorboard, wandb}` to the training command. Note that only one may be used at a time. Additionally the viewer only works for methods that are fast (ie. nerfacto, instant-ngp), for slower methods like NeRF, use the other loggers. +We support three different methods to track training progress, using the viewer, [tensorboard](https://www.tensorflow.org/tensorboard), and [Weights and Biases](https://wandb.ai/site). You can specify which visualizer to use by appending `--vis {viewer, tensorboard, wandb, viewer+wandb, viewer+tensorboard}` to the training command. Simultaneously utilizing the viewer alongside wandb or tensorboard may cause stuttering issues during evaluation steps. The viewer only works for methods that are fast (ie. nerfacto, instant-ngp), for slower methods like NeRF, use the other loggers. ## Evaluating Runs diff --git a/docs/quickstart/installation.md b/docs/quickstart/installation.md index 950aefb2a..58f9ffe1d 100644 --- a/docs/quickstart/installation.md +++ b/docs/quickstart/installation.md @@ -33,7 +33,7 @@ Install CUDA 11.7. You can find CUDA download links [here](https://developer.nvi ## Create environment -Nerfstudio requires `python >= 3.7`. We recommend using conda to manage dependencies. Make sure to install [Conda](https://docs.conda.io/en/latest/miniconda.html) before proceeding. +Nerfstudio requires `python >= 3.8`. We recommend using conda to manage dependencies. Make sure to install [Conda](https://docs.conda.io/en/latest/miniconda.html) before proceeding. ```bash conda create --name nerfstudio -y python=3.8 @@ -71,7 +71,7 @@ pip uninstall torch torchvision functorch Install pytorch 1.13.1 with CUDA and [tiny-cuda-nn](https://github.com/NVlabs/tiny-cuda-nn) ```bash -pip install torch torchvision functorch --extra-index-url https://download.pytorch.org/whl/cu117 +pip install torch==1.13.1 torchvision functorch --extra-index-url https://download.pytorch.org/whl/cu117 ``` ::: @@ -79,10 +79,10 @@ pip install torch torchvision functorch --extra-index-url https://download.pytor ### tinycudann -After pytorch, install the torch bindings for tinycudann: +After pytorch and ninja, install the torch bindings for tinycudann: ```bash -pip install git+https://github.com/NVlabs/tiny-cuda-nn/#subdirectory=bindings/torch +pip install ninja git+https://github.com/NVlabs/tiny-cuda-nn/#subdirectory=bindings/torch ``` ## Installing nerfstudio diff --git a/docs/reference/api/data.rst b/docs/reference/api/data.rst deleted file mode 100644 index de7b57abe..000000000 --- a/docs/reference/api/data.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. _dataset: - -Data -============ - -Data Parsers ----------------- - -.. automodule:: nerfstudio.data.dataparsers - :members: - :show-inheritance: - -Datasets ----------------- - -.. automodule:: nerfstudio.data.datasets - :members: - :show-inheritance: - -Dataloaders ----------------- - -.. automodule:: nerfstudio.data.utils.dataloaders - :members: - :show-inheritance: - -Pixel Samplers ----------------- - -.. automodule:: nerfstudio.data.pixel_samplers - :members: - :show-inheritance: - -Scene Box ----------------- - -.. automodule:: nerfstudio.data.scene_box - :members: - :show-inheritance: \ No newline at end of file diff --git a/docs/reference/api/data/datamanagers.rst b/docs/reference/api/data/datamanagers.rst new file mode 100644 index 000000000..43e5d6cce --- /dev/null +++ b/docs/reference/api/data/datamanagers.rst @@ -0,0 +1,10 @@ +.. _datamanagers: + +Datamanagers +============ + +Base +---------------- + +.. automodule:: nerfstudio.data.datamanagers.base_datamanager + :members: \ No newline at end of file diff --git a/docs/reference/api/data/dataparsers.rst b/docs/reference/api/data/dataparsers.rst new file mode 100644 index 000000000..e35f83ea0 --- /dev/null +++ b/docs/reference/api/data/dataparsers.rst @@ -0,0 +1,117 @@ +.. _dataparser: + +Data Parsers +============ + +* `Base Data Parser`_ +* `ARKitScenes`_ +* `Blender`_ +* `D-NeRF`_ +* `dycheck`_ +* `Instant-NGP`_ +* `Minimal`_ +* `NeRF-OSR`_ +* `Nerfstudio`_ +* `nuScenes`_ +* `Phototourism`_ +* `ScanNet`_ +* `SDFStudio`_ +* `sitcoms3D`_ + + +Base Data Parser +---------------- + +.. automodule:: nerfstudio.data.dataparsers.base_dataparser + :members: + +ARKitScenes +---------------- + +.. automodule:: nerfstudio.data.dataparsers.arkitscenes_dataparser + :members: + :show-inheritance: + +Blender +---------------- + +.. automodule:: nerfstudio.data.dataparsers.blender_dataparser + :members: + :show-inheritance: + +D-NeRF +---------------- + +.. automodule:: nerfstudio.data.dataparsers.dnerf_dataparser + :members: + :show-inheritance: + +dycheck +---------------- + +.. automodule:: nerfstudio.data.dataparsers.dycheck_dataparser + :members: + :show-inheritance: + +Instant-NGP +---------------- + +.. automodule:: nerfstudio.data.dataparsers.instant_ngp_dataparser + :members: + :show-inheritance: + +Minimal +---------------- + +.. automodule:: nerfstudio.data.dataparsers.minimal_dataparser + :members: + :show-inheritance: + +NeRF-OSR +---------------- + +.. automodule:: nerfstudio.data.dataparsers.nerfosr_dataparser + :members: + :show-inheritance: + +Nerfstudio +---------------- + +.. automodule:: nerfstudio.data.dataparsers.nerfstudio_dataparser + :members: + :show-inheritance: + +nuScenes +---------------- + +.. automodule:: nerfstudio.data.dataparsers.nuscenes_dataparser + :members: + :show-inheritance: + +Phototourism +---------------- + +.. automodule:: nerfstudio.data.dataparsers.phototourism_dataparser + :members: + :show-inheritance: + +ScanNet +---------------- + +.. automodule:: nerfstudio.data.dataparsers.scannet_dataparser + :members: + :show-inheritance: + +SDFStudio +---------------- + +.. automodule:: nerfstudio.data.dataparsers.sdfstudio_dataparser + :members: + :show-inheritance: + +sitcoms3D +---------------- + +.. automodule:: nerfstudio.data.dataparsers.sitcoms3d_dataparser + :members: + :show-inheritance: diff --git a/docs/reference/api/data/datasets.rst b/docs/reference/api/data/datasets.rst new file mode 100644 index 000000000..ee4fd1ab5 --- /dev/null +++ b/docs/reference/api/data/datasets.rst @@ -0,0 +1,24 @@ +.. _datasets: + +Datasets +============ + +Base +---------------- + +.. automodule:: nerfstudio.data.datasets.base_dataset + :members: + +SDF Dataset +---------------- + +.. automodule:: nerfstudio.data.datasets.sdf_dataset + :members: + :show-inheritance: + +Semantic Dataset +---------------- + +.. automodule:: nerfstudio.data.datasets.semantic_dataset + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/reference/api/data/index.rst b/docs/reference/api/data/index.rst new file mode 100644 index 000000000..6e10f9384 --- /dev/null +++ b/docs/reference/api/data/index.rst @@ -0,0 +1,26 @@ +.. _dataset: + +Data +============ + +.. toctree:: + :titlesonly: + + dataparsers + datamanagers + datasets + utils + +Pixel Samplers +---------------- + +.. automodule:: nerfstudio.data.pixel_samplers + :members: + :show-inheritance: + +Scene Box +---------------- + +.. automodule:: nerfstudio.data.scene_box + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/reference/api/data/utils.rst b/docs/reference/api/data/utils.rst new file mode 100644 index 000000000..5f225e6da --- /dev/null +++ b/docs/reference/api/data/utils.rst @@ -0,0 +1,28 @@ +.. _datautils: + +Utils +============ + +Base +---------------- + +.. automodule:: nerfstudio.data.utils.colmap_parsing_utils + :members: + +Data +---------------- + +.. automodule:: nerfstudio.data.utils.data_utils + :members: + +Dataloader +---------------- + +.. automodule:: nerfstudio.data.utils.dataloaders + :members: + +Nerfstudio Collate +------------------- + +.. automodule:: nerfstudio.data.utils.nerfstudio_collate + :members: \ No newline at end of file diff --git a/docs/reference/api/index.rst b/docs/reference/api/index.rst index c1c9ce0b2..01c7d7fe7 100644 --- a/docs/reference/api/index.rst +++ b/docs/reference/api/index.rst @@ -8,7 +8,7 @@ TODO: Explanation of each component .. toctree:: config - data + data/index fields field_components/index models diff --git a/docs/reference/api/viewer.rst b/docs/reference/api/viewer.rst index 508fddf68..4be105c86 100644 --- a/docs/reference/api/viewer.rst +++ b/docs/reference/api/viewer.rst @@ -3,6 +3,6 @@ Viewer ============ -.. automodule:: nerfstudio.viewer +.. automodule:: nerfstudio.viewer.server.viewer_elements :members: :show-inheritance: diff --git a/docs/reference/cli/index.md b/docs/reference/cli/index.md index 9e1b8fae4..4daee6555 100644 --- a/docs/reference/cli/index.md +++ b/docs/reference/cli/index.md @@ -6,21 +6,22 @@ We provide a command line interface for training your own NeRFs (no coding neces Here are the popular commands that we offer. If you've cloned the repo, you can also look at the [pyproject.toml file](https://github.com/nerfstudio-project/nerfstudio/blob/main/pyproject.toml) at the `[project.scripts]` section for details. -| Command | Description | Filename | -| ---------------- | -------------------------------------- | ---------------------------------- | -| ns-install-cli | Install tab completion for all scripts | scripts/completions/install.py | -| ns-process-data | Generate a dataset from your own data | scripts/process_data.py | -| ns-download-data | Download existing captures | scripts/downloads/download_data.py | -| ns-train | Generate a NeRF | scripts/train.py | -| ns-viewer | View a trained NeRF | scripts/viewer/run_viewer.py | -| ns-eval | Run evaluation metrics for your Model | scripts/eval.py | -| ns-render | Render out a video of your NeRF | scripts/render.py | -| ns-export | Export a NeRF into other formats | scripts/exporter.py | +| Command | Description | Filename | +| ------------------------------------ | -------------------------------------- | --------------------------------------------- | +| [ns-install-cli](ns_install_cli) | Install tab completion for all scripts | nerfstudio/scripts/completions/install.py | +| [ns-process-data](ns_process_data) | Generate a dataset from your own data | nerfstudio/scripts/process_data.py | +| [ns-download-data](ns_download_data) | Download existing captures | nerfstudio/scripts/downloads/download_data.py | +| [ns-train](ns_train) | Generate a NeRF | nerfstudio/scripts/train.py | +| [ns-viewer](ns_viewer) | View a trained NeRF | nerfstudio/scripts/viewer/run_viewer.py | +| [ns-eval](ns_eval) | Run evaluation metrics for your Model | nerfstudio/scripts/eval.py | +| [ns-render](ns_render) | Render out a video of your NeRF | nerfstudio/scripts/render.py | +| [ns-export](ns_export) | Export a NeRF into other formats | nerfstudio/scripts/exporter.py | ```{toctree} :maxdepth: 1 :hidden: +ns_install_cli ns_process_data ns_download_data ns_train diff --git a/docs/reference/cli/ns_download_data.md b/docs/reference/cli/ns_download_data.md index d096b4e07..1105ef12b 100644 --- a/docs/reference/cli/ns_download_data.md +++ b/docs/reference/cli/ns_download_data.md @@ -2,7 +2,7 @@ ```{eval-rst} .. argparse:: - :module: scripts.downloads.download_data + :module: nerfstudio.scripts.downloads.download_data :func: get_parser_fn :prog: ns-download-data :nodefault: diff --git a/docs/reference/cli/ns_eval.md b/docs/reference/cli/ns_eval.md index a794ac9e8..86e4aa0ba 100644 --- a/docs/reference/cli/ns_eval.md +++ b/docs/reference/cli/ns_eval.md @@ -2,7 +2,7 @@ ```{eval-rst} .. argparse:: - :module: scripts.eval + :module: nerfstudio.scripts.eval :func: get_parser_fn :prog: ns-eval :nodefault: diff --git a/docs/reference/cli/ns_export.md b/docs/reference/cli/ns_export.md index cdfc54f2f..839211915 100644 --- a/docs/reference/cli/ns_export.md +++ b/docs/reference/cli/ns_export.md @@ -2,7 +2,7 @@ ```{eval-rst} .. argparse:: - :module: scripts.exporter + :module: nerfstudio.scripts.exporter :func: get_parser_fn :prog: ns-export :nodefault: diff --git a/docs/reference/cli/ns_install_cli.md b/docs/reference/cli/ns_install_cli.md new file mode 100644 index 000000000..30fa9598f --- /dev/null +++ b/docs/reference/cli/ns_install_cli.md @@ -0,0 +1,9 @@ +# ns-install-cli + +```{eval-rst} +.. argparse:: + :module: nerfstudio.scripts.completions.install + :func: get_parser_fn + :prog: ns-install-cli + :nodefault: +``` diff --git a/docs/reference/cli/ns_process_data.md b/docs/reference/cli/ns_process_data.md index bb5bd24ac..9f9540d44 100644 --- a/docs/reference/cli/ns_process_data.md +++ b/docs/reference/cli/ns_process_data.md @@ -8,7 +8,7 @@ You may also want to install [hloc](https://github.com/cvg/Hierarchical-Localiza ```{eval-rst} .. argparse:: - :module: scripts.process_data + :module: nerfstudio.scripts.process_data :func: get_parser_fn :prog: ns-process-data :nodefault: diff --git a/docs/reference/cli/ns_render.md b/docs/reference/cli/ns_render.md index 7a01f9bae..e006e62ee 100644 --- a/docs/reference/cli/ns_render.md +++ b/docs/reference/cli/ns_render.md @@ -7,7 +7,7 @@ Make sure to have [FFmpeg](https://ffmpeg.org/download.html) installed. ```{eval-rst} .. argparse:: - :module: scripts.render + :module: nerfstudio.scripts.render :func: get_parser_fn :prog: ns-render :nodefault: diff --git a/docs/reference/cli/ns_viewer.md b/docs/reference/cli/ns_viewer.md index 841472a5f..bb6333550 100644 --- a/docs/reference/cli/ns_viewer.md +++ b/docs/reference/cli/ns_viewer.md @@ -2,7 +2,7 @@ ```{eval-rst} .. argparse:: - :module: scripts.viewer.run_viewer + :module: nerfstudio.scripts.viewer.run_viewer :func: get_parser_fn :prog: ns-viewer :nodefault: diff --git a/docs/reference/contributing.md b/docs/reference/contributing.md index a9651cbed..101cb873e 100644 --- a/docs/reference/contributing.md +++ b/docs/reference/contributing.md @@ -7,13 +7,14 @@ Below, we document the contribution pipeline and good-to-knows for when you're r The project is set up for development in VSCode, we recommend using it if you plan to contribute. ## Overview + Below are the various tooling features our team uses to maintain this codebase. -| Tooling | Support | -| --------------- | ------------- | -| Linter | [Black](https://black.readthedocs.io/en/stable/) | -| Testing | [PyTest](https://docs.pytest.org/en/7.1.x/) | -| Docs | [Sphinx](https://www.sphinx-doc.org/en/master/) | +| Tooling | Support | +| --------------- | ---------------------------------------------------------- | +| Linter | [Black](https://black.readthedocs.io/en/stable/) | +| Testing | [PyTest](https://docs.pytest.org/en/7.1.x/) | +| Docs | [Sphinx](https://www.sphinx-doc.org/en/master/) | | Docstring style | [Google](https://google.github.io/styleguide/pyguide.html) | ## Requirements @@ -53,8 +54,8 @@ conda install -c conda-forge pandoc :::{admonition} Attention :class: attention - In order to merge changes to the code base, all of these checks must be passing. If you pass these tests locally, you will likely pass on github servers as well (results in a green checkmark next to your commit). - ::: + In order to merge changes to the code base, all of these checks must be passing. If you pass these tests locally, you will likely pass on github servers as well (results in a green checkmark next to your commit). + ::: 3. Open pull request! πŸ’Œ @@ -65,7 +66,7 @@ conda install -c conda-forge pandoc Run the following to build the documentation: ```bash -python scripts/docs/build_docs.py +python nerfstudio/scripts/docs/build_docs.py ``` :::{admonition} Tip @@ -84,7 +85,7 @@ If you want the code to build on save you can use [sphinx autobuild](https://git :class: info If changes to the structure are made, the build files may be incorrect. - ::: +::: ```bash pip install sphinx-autobuild diff --git a/nerfstudio/cameras/camera_optimizers.py b/nerfstudio/cameras/camera_optimizers.py index afd002a71..4b37f2072 100644 --- a/nerfstudio/cameras/camera_optimizers.py +++ b/nerfstudio/cameras/camera_optimizers.py @@ -20,13 +20,13 @@ from __future__ import annotations import functools from dataclasses import dataclass, field -from typing import Type, Union +from typing import Literal, Type, Union import torch import tyro from torch import nn from torchtyping import TensorType -from typing_extensions import Literal, assert_never +from typing_extensions import assert_never from nerfstudio.cameras.lie_groups import exp_map_SE3, exp_map_SO3xR3 from nerfstudio.configs.base_config import InstantiateConfig diff --git a/nerfstudio/cameras/camera_paths.py b/nerfstudio/cameras/camera_paths.py index a313cad4d..9754a2bbf 100644 --- a/nerfstudio/cameras/camera_paths.py +++ b/nerfstudio/cameras/camera_paths.py @@ -125,10 +125,6 @@ def get_path_from_json(camera_path: Dict[str, Any]) -> Cameras: image_height = camera_path["render_height"] image_width = camera_path["render_width"] - if "camera_type" in camera_path: - camera_type = camera_path["camera_type"] - else: - camera_type = "perspective" if "camera_type" not in camera_path: camera_type = CameraType.PERSPECTIVE diff --git a/nerfstudio/cameras/camera_utils.py b/nerfstudio/cameras/camera_utils.py index 79490ae67..7c2364929 100644 --- a/nerfstudio/cameras/camera_utils.py +++ b/nerfstudio/cameras/camera_utils.py @@ -17,13 +17,12 @@ Camera transformation helper code. """ import math -from typing import List, Optional, Tuple +from typing import List, Literal, Optional, Tuple import numpy as np import torch from numpy.typing import ArrayLike from torchtyping import TensorType -from typing_extensions import Literal _EPS = np.finfo(float).eps * 4.0 @@ -164,8 +163,8 @@ def quaternion_matrix(quaternion: ArrayLike) -> np.ndarray: def get_interpolated_poses(pose_a: ArrayLike, pose_b: ArrayLike, steps: int = 10) -> List[float]: """Return interpolation of poses with specified number of steps. Args: - poseA: first pose - poseB: second pose + pose_a: first pose + pose_b: second pose steps: number of steps the interpolated pose path should contain """ @@ -190,8 +189,8 @@ def get_interpolated_k(k_a, k_b, steps: int = 10) -> TensorType[3, 4]: Returns interpolated path between two camera poses with specified number of steps. Args: - KA: camera matrix 1 - KB: camera matrix 2 + k_a: camera matrix 1 + k_b: camera matrix 2 steps: number of steps the interpolated pose path should contain """ Ks = [] diff --git a/nerfstudio/cameras/cameras.py b/nerfstudio/cameras/cameras.py index e0ed08e4f..9d0fb26d8 100644 --- a/nerfstudio/cameras/cameras.py +++ b/nerfstudio/cameras/cameras.py @@ -17,7 +17,6 @@ Camera Models """ import base64 import math -import os from dataclasses import dataclass from enum import Enum, auto from typing import Dict, List, Optional, Tuple, Union @@ -33,7 +32,6 @@ import nerfstudio.utils.poses as pose_utils from nerfstudio.cameras import camera_utils from nerfstudio.cameras.rays import RayBundle from nerfstudio.data.scene_box import SceneBox -from nerfstudio.utils.misc import strtobool from nerfstudio.utils.tensor_dataclass import TensorDataclass TORCH_DEVICE = Union[torch.device, str] # pylint: disable=invalid-name @@ -62,11 +60,7 @@ CAMERA_MODEL_TO_TYPE = { class Cameras(TensorDataclass): """Dataparser outputs for the image dataset and the ray generator. - Note: currently only supports cameras with the same principal points and types. The reason we type - the focal lengths, principal points, and image sizes as tensors is to allow for batched cameras - down the line in cases where your batches of camera data don't come from the same cameras. - - If a single value is provided, it is broadcasted to all cameras. + If a single value is provided, it is broadcasted to all cameras. Args: camera_to_worlds: Camera to world matrices. Tensor of per-image c2w matrices, in [R | t] format @@ -79,6 +73,8 @@ class Cameras(TensorDataclass): distortion_params: OpenCV 6 radial distortion coefficients camera_type: Type of camera model. This will be an int corresponding to the CameraType enum. times: Timestamps for each camera + metadata: Additional metadata or data needed for interpolation, will mimic shape of the cameras + and will be broadcasted to the rays generated from any derivative RaySamples we create with this """ camera_to_worlds: TensorType["num_cameras":..., 3, 4] @@ -91,6 +87,7 @@ class Cameras(TensorDataclass): distortion_params: Optional[TensorType["num_cameras":..., 6]] camera_type: TensorType["num_cameras":..., 1] times: Optional[TensorType["num_cameras", 1]] + metadata: Optional[Dict] def __init__( self, @@ -111,6 +108,7 @@ class Cameras(TensorDataclass): ] ] = CameraType.PERSPECTIVE, times: Optional[TensorType["num_cameras"]] = None, + metadata: Optional[Dict] = None, ) -> None: """Initializes the Cameras object. @@ -146,9 +144,9 @@ class Cameras(TensorDataclass): self.camera_type = self._init_get_camera_type(camera_type) self.times = self._init_get_times(times) - self.__post_init__() # This will do the dataclass post_init and broadcast all the tensors + self.metadata = metadata - self._use_nerfacc = strtobool(os.environ.get("INTERSECT_WITH_NERFACC", "TRUE")) + self.__post_init__() # This will do the dataclass post_init and broadcast all the tensors def _init_get_fc_xy(self, fc_xy: Union[float, torch.Tensor], name: str) -> torch.Tensor: """ @@ -631,7 +629,7 @@ class Cameras(TensorDataclass): if distortion_params is not None: mask = (self.camera_type[true_indices] != CameraType.EQUIRECTANGULAR.value).squeeze(-1) # (num_rays) coord_mask = torch.stack([mask, mask, mask], dim=0) - if mask.any(): + if mask.any() and (distortion_params != 0).any(): coord_stack[coord_mask, :] = camera_utils.radial_and_tangential_undistort( coord_stack[coord_mask, :].reshape(3, -1, 2), distortion_params[mask, :], @@ -716,13 +714,23 @@ class Cameras(TensorDataclass): times = self.times[camera_indices, 0] if self.times is not None else None + metadata = ( + self._apply_fn_to_dict(self.metadata, lambda x: x[true_indices]) if self.metadata is not None else None + ) + if metadata is not None: + metadata["directions_norm"] = directions_norm[0].detach() + else: + metadata = {"directions_norm": directions_norm[0].detach()} + + times = self.times[camera_indices, 0] if self.times is not None else None + return RayBundle( origins=origins, directions=directions, pixel_area=pixel_area, camera_indices=camera_indices, times=times, - metadata={"directions_norm": directions_norm[0].detach()}, + metadata=metadata, ) def to_json( diff --git a/nerfstudio/cameras/rays.py b/nerfstudio/cameras/rays.py index 0184b4c80..0eee58ba5 100644 --- a/nerfstudio/cameras/rays.py +++ b/nerfstudio/cameras/rays.py @@ -57,7 +57,11 @@ class Frustums(TensorDataclass): return pos def get_start_positions(self) -> TensorType[..., 3]: +<<<<<<< HEAD """Calulates "start" position of frustum. +======= + """Calculates "start" position of frustum. +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb Returns: xyz positions. @@ -193,7 +197,7 @@ class RayBundle(TensorDataclass): """Times at which rays are sampled""" def set_camera_indices(self, camera_index: int) -> None: - """Sets all of the the camera indices to a specific camera index. + """Sets all the camera indices to a specific camera index. Args: camera_index: Camera index. diff --git a/nerfstudio/configs/base_config.py b/nerfstudio/configs/base_config.py index 9d95cc269..22c149441 100644 --- a/nerfstudio/configs/base_config.py +++ b/nerfstudio/configs/base_config.py @@ -21,13 +21,18 @@ from __future__ import annotations import warnings from dataclasses import dataclass from pathlib import Path +<<<<<<< HEAD from typing import Any, List, Optional, Tuple, Type +======= +from typing import Any, List, Literal, Optional, Tuple, Type +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb # model instances from nerfstudio.utils import writer warnings.filterwarnings("ignore", module="torchvision") + # Pretty printing class class PrintableConfig: # pylint: disable=too-few-public-methods """Printable Config defining str function""" @@ -116,9 +121,11 @@ class LoggingConfig(PrintableConfig): e.g. if 20, averages will be computed over past 20 occurrences.""" local_writer: LocalWriterConfig = LocalWriterConfig(enable=True) """if provided, will print stats locally. if None, will disable printing""" - enable_profiler: bool = True - """whether to enable profiling code; prints speed of functions at the end of a program. - profiler logs run times of functions and prints at end of training""" + profiler: Literal["none", "basic", "pytorch"] = "basic" + """how to profile the code; + "basic" - prints speed of all decorated functions at the end of a program. + "pytorch" - same as basic, but it also traces few training steps. + """ # Viewer related configs @@ -128,17 +135,12 @@ class ViewerConfig(PrintableConfig): relative_log_filename: str = "viewer_log_filename.txt" """Filename to use for the log file.""" - start_train: bool = True - """whether to immediately start training upon loading viewer - if False, will just visualize dataset but you can toggle training in viewer""" - zmq_port: Optional[int] = None - """The zmq port to connect to for communication. If None, find an available port.""" - launch_bridge_server: bool = True - """whether or not to launch the bridge server""" - websocket_port: Optional[int] = 7007 - """the default websocket port to connect to""" - ip_address: str = "127.0.0.1" - """the ip address where the bridge server is running""" + websocket_port: Optional[int] = None + """The websocket port to connect to. If None, find an available port.""" + websocket_port_default: int = 7007 + """The default websocket port to connect to if websocket_port is not specified""" + websocket_host: str = "0.0.0.0" + """The host address to bind the websocket server to.""" num_rays_per_chunk: int = 32768 """number of rays per chunk to render with viewer""" max_num_display_images: int = 512 @@ -146,3 +148,10 @@ class ViewerConfig(PrintableConfig): actually used in training/evaluation. If -1, display all.""" quit_on_train_completion: bool = False """Whether to kill the training job when it has completed. Note this will stop rendering in the viewer.""" +<<<<<<< HEAD +======= + image_format: Literal["jpeg", "png"] = "jpeg" + """Image format viewer should use; jpeg is lossy compression, while png is lossless.""" + jpeg_quality: int = 90 + """Quality tradeoff to use for jpeg compression.""" +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb diff --git a/nerfstudio/configs/config_utils.py b/nerfstudio/configs/config_utils.py index 59b1b252b..42bf7a082 100644 --- a/nerfstudio/configs/config_utils.py +++ b/nerfstudio/configs/config_utils.py @@ -26,6 +26,7 @@ from rich.console import Console CONSOLE = Console() # pylint: disable=import-outside-toplevel + # cannot use mutable types directly within dataclass; abstracting default factory calls def to_immutable_dict(d: Dict[str, Any]): """Method to convert mutable dict to default factory dict diff --git a/nerfstudio/configs/experiment_config.py b/nerfstudio/configs/experiment_config.py index 765c64ef7..0439f2af6 100644 --- a/nerfstudio/configs/experiment_config.py +++ b/nerfstudio/configs/experiment_config.py @@ -19,11 +19,10 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Dict, Literal, Optional import yaml from rich.console import Console -from typing_extensions import Literal from nerfstudio.configs.base_config import ( InstantiateConfig, @@ -50,6 +49,8 @@ class ExperimentConfig(InstantiateConfig): """Method name. Required to set in python or via cli""" experiment_name: Optional[str] = None """Experiment name. If None, will automatically be set to dataset name""" + project_name: Optional[str] = "nerfstudio-project" + """Project name.""" timestamp: str = "{timestamp}" """Experiment timestamp.""" machine: MachineConfig = MachineConfig() diff --git a/nerfstudio/configs/method_configs.py b/nerfstudio/configs/method_configs.py index 2dcfe452c..b07bedc5e 100644 --- a/nerfstudio/configs/method_configs.py +++ b/nerfstudio/configs/method_configs.py @@ -21,10 +21,10 @@ from __future__ import annotations from typing import Dict import tyro -from nerfacc import ContractionType from nerfstudio.cameras.camera_optimizers import CameraOptimizerConfig from nerfstudio.configs.base_config import ViewerConfig +<<<<<<< HEAD from nerfstudio.data.datamanagers.base_datamanager import VanillaDataManagerConfig from nerfstudio.data.datamanagers.depth_datamanager import DepthDataManagerConfig from nerfstudio.data.datamanagers.sdf_datamanager import SDFDataManagerConfig @@ -34,6 +34,11 @@ from nerfstudio.data.datamanagers.dreamfusion_datamanager import ( from nerfstudio.data.datamanagers.semantic_datamanager import SemanticDataManagerConfig from nerfstudio.data.datamanagers.variable_res_datamanager import ( VariableResDataManagerConfig, +======= +from nerfstudio.data.datamanagers.base_datamanager import ( + VanillaDataManager, + VanillaDataManagerConfig, +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb ) from nerfstudio.data.dataparsers.blender_dataparser import BlenderDataParserConfig from nerfstudio.data.dataparsers.dnerf_dataparser import DNeRFDataParserConfig @@ -47,13 +52,21 @@ from nerfstudio.data.dataparsers.phototourism_dataparser import ( ) from nerfstudio.data.dataparsers.sdfstudio_dataparser import SDFStudioDataParserConfig from nerfstudio.data.dataparsers.sitcoms3d_dataparser import Sitcoms3DDataParserConfig +from nerfstudio.data.datasets.depth_dataset import DepthDataset +from nerfstudio.data.datasets.sdf_dataset import SDFDataset +from nerfstudio.data.datasets.semantic_dataset import SemanticDataset from nerfstudio.engine.optimizers import AdamOptimizerConfig, RAdamOptimizerConfig from nerfstudio.engine.schedulers import ( CosineDecaySchedulerConfig, ExponentialDecaySchedulerConfig, +<<<<<<< HEAD +======= + MultiStepSchedulerConfig, +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb ) from nerfstudio.engine.trainer import TrainerConfig from nerfstudio.field_components.temporal_distortions import TemporalDistortionKind +from nerfstudio.fields.sdf_field import SDFFieldConfig from nerfstudio.models.depth_nerfacto import DepthNerfactoModelConfig from nerfstudio.models.dreamfusion import DreamFusionModelConfig from nerfstudio.models.instant_ngp import InstantNGPModelConfig @@ -62,6 +75,10 @@ from nerfstudio.models.nerfacto import NerfactoModelConfig from nerfstudio.models.nerfplayer_nerfacto import NerfplayerNerfactoModelConfig from nerfstudio.models.nerfplayer_ngp import NerfplayerNGPModelConfig from nerfstudio.models.neus import NeuSModelConfig +<<<<<<< HEAD +======= +from nerfstudio.models.neus_facto import NeuSFactoModelConfig +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb from nerfstudio.models.semantic_nerfw import SemanticNerfWModelConfig from nerfstudio.models.tensorf import TensoRFModelConfig from nerfstudio.models.vanilla_nerf import NeRFModel, VanillaModelConfig @@ -86,6 +103,10 @@ descriptions = { "nerfplayer-nerfacto": "NeRFPlayer with nerfacto backbone.", "nerfplayer-ngp": "NeRFPlayer with InstantNGP backbone.", "neus": "Implementation of NeuS. (slow)", +<<<<<<< HEAD +======= + "neus-facto": "Implementation of NeuS-Facto. (slow)", +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb } method_configs["nerfacto"] = TrainerConfig( @@ -100,7 +121,9 @@ method_configs["nerfacto"] = TrainerConfig( train_num_rays_per_batch=4096, eval_num_rays_per_batch=4096, camera_optimizer=CameraOptimizerConfig( - mode="SO3xR3", optimizer=AdamOptimizerConfig(lr=6e-4, eps=1e-8, weight_decay=1e-2) + mode="SO3xR3", + optimizer=AdamOptimizerConfig(lr=6e-4, eps=1e-8, weight_decay=1e-2), + scheduler=ExponentialDecaySchedulerConfig(lr_final=6e-6, max_steps=200000), ), ), model=NerfactoModelConfig(eval_num_rays_per_chunk=1 << 15), @@ -108,12 +131,52 @@ method_configs["nerfacto"] = TrainerConfig( optimizers={ "proposal_networks": { "optimizer": AdamOptimizerConfig(lr=1e-2, eps=1e-15), - "scheduler": None, + "scheduler": ExponentialDecaySchedulerConfig(lr_final=0.0001, max_steps=200000), }, "fields": { "optimizer": AdamOptimizerConfig(lr=1e-2, eps=1e-15), + "scheduler": ExponentialDecaySchedulerConfig(lr_final=0.0001, max_steps=200000), + }, + }, + viewer=ViewerConfig(num_rays_per_chunk=1 << 15), + vis="viewer", +) +method_configs["nerfacto-big"] = TrainerConfig( + method_name="nerfacto", + steps_per_eval_batch=500, + steps_per_save=2000, + max_num_iterations=100000, + mixed_precision=True, + pipeline=VanillaPipelineConfig( + datamanager=VanillaDataManagerConfig( + dataparser=NerfstudioDataParserConfig(), + train_num_rays_per_batch=4096, + eval_num_rays_per_batch=4096, + camera_optimizer=CameraOptimizerConfig( + mode="SO3xR3", optimizer=RAdamOptimizerConfig(lr=6e-4, eps=1e-8, weight_decay=1e-3) + ), + ), + model=NerfactoModelConfig( + eval_num_rays_per_chunk=1 << 15, + num_nerf_samples_per_ray=128, + num_proposal_samples_per_ray=(512, 256), + hidden_dim=128, + hidden_dim_color=128, + hidden_dim_transient=128, + max_res=3000, + proposal_weights_anneal_max_num_iters=5000, + log2_hashmap_size=21, + ), + ), + optimizers={ + "proposal_networks": { + "optimizer": RAdamOptimizerConfig(lr=1e-2, eps=1e-15), "scheduler": None, }, + "fields": { + "optimizer": RAdamOptimizerConfig(lr=1e-2, eps=1e-15), + "scheduler": ExponentialDecaySchedulerConfig(lr_final=1e-4, max_steps=100000), + }, }, viewer=ViewerConfig(num_rays_per_chunk=1 << 15), vis="viewer", @@ -126,7 +189,8 @@ method_configs["depth-nerfacto"] = TrainerConfig( max_num_iterations=30000, mixed_precision=True, pipeline=VanillaPipelineConfig( - datamanager=DepthDataManagerConfig( + datamanager=VanillaDataManagerConfig( + _target=VanillaDataManager[DepthDataset], dataparser=NerfstudioDataParserConfig(), train_num_rays_per_batch=4096, eval_num_rays_per_batch=4096, @@ -198,16 +262,20 @@ method_configs["instant-ngp"] = TrainerConfig( max_num_iterations=30000, mixed_precision=True, pipeline=DynamicBatchPipelineConfig( - datamanager=VanillaDataManagerConfig(dataparser=NerfstudioDataParserConfig(), train_num_rays_per_batch=8192), + datamanager=VanillaDataManagerConfig( + dataparser=NerfstudioDataParserConfig(), + train_num_rays_per_batch=4096, + eval_num_rays_per_batch=4096, + ), model=InstantNGPModelConfig(eval_num_rays_per_chunk=8192), ), optimizers={ "fields": { "optimizer": AdamOptimizerConfig(lr=1e-2, eps=1e-15), - "scheduler": None, + "scheduler": ExponentialDecaySchedulerConfig(lr_final=0.0001, max_steps=200000), } }, - viewer=ViewerConfig(num_rays_per_chunk=64000), + viewer=ViewerConfig(num_rays_per_chunk=1 << 12), vis="viewer", ) @@ -222,9 +290,10 @@ method_configs["instant-ngp-bounded"] = TrainerConfig( datamanager=VanillaDataManagerConfig(dataparser=InstantNGPDataParserConfig(), train_num_rays_per_batch=8192), model=InstantNGPModelConfig( eval_num_rays_per_chunk=8192, - contraction_type=ContractionType.AABB, - render_step_size=0.001, - max_num_samples_per_ray=48, + grid_levels=1, + alpha_thre=0.0, + cone_angle=0.0, + disable_scene_contraction=True, near_plane=0.01, background_color="black", ), @@ -232,10 +301,10 @@ method_configs["instant-ngp-bounded"] = TrainerConfig( optimizers={ "fields": { "optimizer": AdamOptimizerConfig(lr=1e-2, eps=1e-15), - "scheduler": None, + "scheduler": ExponentialDecaySchedulerConfig(lr_final=0.0001, max_steps=200000), } }, - viewer=ViewerConfig(num_rays_per_chunk=64000), + viewer=ViewerConfig(num_rays_per_chunk=1 << 12), vis="viewer", ) @@ -267,8 +336,11 @@ method_configs["semantic-nerfw"] = TrainerConfig( max_num_iterations=30000, mixed_precision=True, pipeline=VanillaPipelineConfig( - datamanager=SemanticDataManagerConfig( - dataparser=Sitcoms3DDataParserConfig(), train_num_rays_per_batch=4096, eval_num_rays_per_batch=8192 + datamanager=VanillaDataManagerConfig( + _target=VanillaDataManager[SemanticDataset], + dataparser=Sitcoms3DDataParserConfig(), + train_num_rays_per_batch=4096, + eval_num_rays_per_batch=8192, ), model=SemanticNerfWModelConfig(eval_num_rays_per_chunk=1 << 16), ), @@ -315,8 +387,12 @@ method_configs["tensorf"] = TrainerConfig( pipeline=VanillaPipelineConfig( datamanager=VanillaDataManagerConfig( dataparser=BlenderDataParserConfig(), + train_num_rays_per_batch=4096, + eval_num_rays_per_batch=4096, + ), + model=TensoRFModelConfig( + regularization="tv", ), - model=TensoRFModelConfig(), ), optimizers={ "fields": { @@ -361,13 +437,18 @@ method_configs["phototourism"] = TrainerConfig( max_num_iterations=30000, mixed_precision=True, pipeline=VanillaPipelineConfig( - datamanager=VariableResDataManagerConfig( # NOTE: one of the only differences with nerfacto + datamanager=VanillaDataManagerConfig( dataparser=PhototourismDataParserConfig(), # NOTE: one of the only differences with nerfacto train_num_rays_per_batch=4096, eval_num_rays_per_batch=4096, camera_optimizer=CameraOptimizerConfig( mode="SO3xR3", optimizer=AdamOptimizerConfig(lr=6e-4, eps=1e-8, weight_decay=1e-2) ), + # Large dataset, so using prior values from VariableResDataManager. + train_num_images_to_sample_from=40, + train_num_times_to_repeat_images=100, + eval_num_images_to_sample_from=40, + eval_num_times_to_repeat_images=100, ), model=NerfactoModelConfig(eval_num_rays_per_chunk=1 << 15), ), @@ -394,6 +475,7 @@ method_configs["dreamfusion"] = TrainerConfig( max_num_iterations=30000, mixed_precision=True, pipeline=VanillaPipelineConfig( +<<<<<<< HEAD generative=True, datamanager=DreamFusionDataManagerConfig( horizontal_rotation_warmup=4000, @@ -437,6 +519,10 @@ method_configs["nerfplayer-nerfacto"] = TrainerConfig( method_name="nerfplayer-nerfacto", pipeline=VanillaPipelineConfig( datamanager=DepthDataManagerConfig( +======= + datamanager=VanillaDataManagerConfig( + _target=VanillaDataManager[DepthDataset], +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb dataparser=DycheckDataParserConfig(), train_num_rays_per_batch=4096, eval_num_rays_per_batch=4096, @@ -467,12 +553,17 @@ method_configs["nerfplayer-ngp"] = TrainerConfig( max_num_iterations=30000, mixed_precision=True, pipeline=DynamicBatchPipelineConfig( - datamanager=DepthDataManagerConfig(dataparser=DycheckDataParserConfig(), train_num_rays_per_batch=8192), + datamanager=VanillaDataManagerConfig( + _target=VanillaDataManager[DepthDataset], + dataparser=DycheckDataParserConfig(), + train_num_rays_per_batch=8192, + ), model=NerfplayerNGPModelConfig( eval_num_rays_per_chunk=8192, - contraction_type=ContractionType.AABB, + grid_levels=1, + alpha_thre=0.0, render_step_size=0.001, - max_num_samples_per_ray=48, + disable_scene_contraction=True, near_plane=0.01, ), ), @@ -495,7 +586,12 @@ method_configs["neus"] = TrainerConfig( max_num_iterations=100000, mixed_precision=False, pipeline=VanillaPipelineConfig( +<<<<<<< HEAD datamanager=SDFDataManagerConfig( +======= + datamanager=VanillaDataManagerConfig( + _target=VanillaDataManager[SDFDataset], +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb dataparser=SDFStudioDataParserConfig(), train_num_rays_per_batch=1024, eval_num_rays_per_batch=1024, @@ -519,13 +615,67 @@ method_configs["neus"] = TrainerConfig( vis="viewer", ) +<<<<<<< HEAD +======= +method_configs["neus-facto"] = TrainerConfig( + method_name="neus-facto", + steps_per_eval_image=5000, + steps_per_eval_batch=5000, + steps_per_save=2000, + steps_per_eval_all_images=1000000, # set to a very large model so we don't eval with all images + max_num_iterations=20001, + mixed_precision=False, + pipeline=VanillaPipelineConfig( + datamanager=VanillaDataManagerConfig( + _target=VanillaDataManager[SDFDataset], + dataparser=SDFStudioDataParserConfig(), + train_num_rays_per_batch=2048, + eval_num_rays_per_batch=2048, + camera_optimizer=CameraOptimizerConfig( + mode="SO3xR3", optimizer=AdamOptimizerConfig(lr=6e-4, eps=1e-8, weight_decay=1e-2) + ), + ), + model=NeuSFactoModelConfig( + # proposal network allows for signifanctly smaller sdf/color network + sdf_field=SDFFieldConfig( + use_grid_feature=True, + num_layers=2, + num_layers_color=2, + hidden_dim=256, + bias=0.5, + beta_init=0.8, + use_appearance_embedding=False, + ), + background_model="none", + eval_num_rays_per_chunk=2048, + ), + ), + optimizers={ + "proposal_networks": { + "optimizer": AdamOptimizerConfig(lr=1e-2, eps=1e-15), + "scheduler": MultiStepSchedulerConfig(max_steps=20001, milestones=(10000, 1500, 18000)), + }, + "fields": { + "optimizer": AdamOptimizerConfig(lr=5e-4, eps=1e-15), + "scheduler": CosineDecaySchedulerConfig(warm_up_end=500, learning_rate_alpha=0.05, max_steps=20001), + }, + "field_background": { + "optimizer": AdamOptimizerConfig(lr=5e-4, eps=1e-15), + "scheduler": CosineDecaySchedulerConfig(warm_up_end=500, learning_rate_alpha=0.05, max_steps=20001), + }, + }, + viewer=ViewerConfig(num_rays_per_chunk=1 << 15), + vis="viewer", +) + +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb external_methods, external_descriptions = discover_methods() -method_configs.update(external_methods) -descriptions.update(external_descriptions) +all_methods = {**method_configs, **external_methods} +all_descriptions = {**descriptions, **external_descriptions} AnnotatedBaseConfigUnion = tyro.conf.SuppressFixed[ # Don't show unparseable (fixed) arguments in helptext. tyro.conf.FlagConversionOff[ - tyro.extras.subcommand_type_from_defaults(defaults=method_configs, descriptions=descriptions) + tyro.extras.subcommand_type_from_defaults(defaults=all_methods, descriptions=all_descriptions) ] ] """Union[] type over config types, annotated with default instances for use with diff --git a/nerfstudio/data/datamanagers/base_datamanager.py b/nerfstudio/data/datamanagers/base_datamanager.py index 7ff2f9aaf..4d4f88dae 100644 --- a/nerfstudio/data/datamanagers/base_datamanager.py +++ b/nerfstudio/data/datamanagers/base_datamanager.py @@ -18,10 +18,11 @@ Datamanager. from __future__ import annotations +import functools from abc import abstractmethod from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Type, Union +from typing import Any, Dict, Generic, List, Literal, Optional, Tuple, Type, Union import torch import tyro @@ -30,7 +31,7 @@ from torch import nn from torch.nn import Parameter from torch.utils.data import Dataset from torch.utils.data.distributed import DistributedSampler -from typing_extensions import Literal +from typing_extensions import TypeVar from nerfstudio.cameras.camera_optimizers import CameraOptimizerConfig from nerfstudio.cameras.cameras import CameraType @@ -47,6 +48,7 @@ from nerfstudio.data.dataparsers.instant_ngp_dataparser import ( InstantNGPDataParserConfig, ) from nerfstudio.data.dataparsers.minimal_dataparser import MinimalDataParserConfig +from nerfstudio.data.dataparsers.nerfosr_dataparser import NeRFOSRDataParserConfig from nerfstudio.data.dataparsers.nerfstudio_dataparser import NerfstudioDataParserConfig from nerfstudio.data.dataparsers.nuscenes_dataparser import NuScenesDataParserConfig from nerfstudio.data.dataparsers.phototourism_dataparser import ( @@ -73,6 +75,31 @@ from nerfstudio.utils.misc import IterableWrapper CONSOLE = Console(width=120) + +def variable_res_collate(batch: List[Dict]) -> Dict: + """Default collate function for the cached dataloader. + Args: + batch: Batch of samples from the dataset. + Returns: + Collated batch. + """ + images = [] + masks = [] + for data in batch: + image = data.pop("image") + mask = data.pop("mask", None) + images.append(image) + if mask is not None: + masks.append(mask) + + new_batch: dict = nerfstudio_collate(batch) + new_batch["image"] = images + if masks: + new_batch["mask"] = masks + + return new_batch + + AnnotatedDataParserUnion = tyro.conf.OmitSubcommandPrefixes[ # Omit prefixes of flags in subcommands. tyro.extras.subcommand_type_from_defaults( { @@ -87,6 +114,7 @@ AnnotatedDataParserUnion = tyro.conf.OmitSubcommandPrefixes[ # Omit prefixes of "dycheck-data": DycheckDataParserConfig(), "scannet-data": ScanNetDataParserConfig(), "sdfstudio-data": SDFStudioDataParserConfig(), + "nerfosr-data": NeRFOSRDataParserConfig(), "sitcoms3d-data": Sitcoms3DDataParserConfig(), }, prefix_names=False, # Omit prefixes in subcommands themselves. @@ -122,10 +150,11 @@ class DataManager(nn.Module): Usage: To get data, use the next_train and next_eval functions. This data manager's next_train and next_eval methods will return 2 things: - 1. A Raybundle: This will contain the rays we are sampling, with latents and - conditionals attached (everything needed at inference) - 2. A "batch" of auxiliary information: This will contain the mask, the ground truth - pixels, etc needed to actually train, score, etc the model + + 1. A Raybundle: This will contain the rays we are sampling, with latents and + conditionals attached (everything needed at inference) + 2. A "batch" of auxiliary information: This will contain the mask, the ground truth + pixels, etc needed to actually train, score, etc the model Rationale: Because of this abstraction we've added, we can support more NeRF paradigms beyond the @@ -151,6 +180,7 @@ class DataManager(nn.Module): eval_count (int): the step number of our eval iteration, needs to be incremented manually train_dataset (Dataset): the dataset for the train dataset eval_dataset (Dataset): the dataset for the eval dataset + includes_time (bool): whether the dataset includes time information Additional attributes specific to each subclass are defined in the setup_train and setup_eval functions. @@ -161,6 +191,7 @@ class DataManager(nn.Module): eval_dataset: Optional[Dataset] = None train_sampler: Optional[DistributedSampler] = None eval_sampler: Optional[DistributedSampler] = None + includes_time: bool = False def __init__(self): """Constructor for the DataManager class. @@ -337,7 +368,10 @@ class VanillaDataManagerConfig(DataManagerConfig): """Size of patch to sample from. If >1, patch-based sampling will be used.""" -class VanillaDataManager(DataManager): # pylint: disable=abstract-method +TDataset = TypeVar("TDataset", bound=InputDataset, default=InputDataset) + + +class VanillaDataManager(DataManager, Generic[TDataset]): # pylint: disable=abstract-method """Basic stored data manager implementation. This is pretty much a port over from our old dataloading utilities, and is a little jank @@ -351,8 +385,8 @@ class VanillaDataManager(DataManager): # pylint: disable=abstract-method """ config: VanillaDataManagerConfig - train_dataset: InputDataset - eval_dataset: InputDataset + train_dataset: TDataset + eval_dataset: TDataset train_dataparser_outputs: DataparserOutputs train_pixel_sampler: Optional[PixelSampler] = None eval_pixel_sampler: Optional[PixelSampler] = None @@ -366,6 +400,7 @@ class VanillaDataManager(DataManager): # pylint: disable=abstract-method local_rank: int = 0, **kwargs, # pylint: disable=unused-argument ): + self.dataset_type: Type[TDataset] = kwargs.get("_dataset_type", TDataset.__default__) self.config = config self.device = device self.world_size = world_size @@ -379,28 +414,49 @@ class VanillaDataManager(DataManager): # pylint: disable=abstract-method else: self.config.data = self.config.dataparser.data self.dataparser = self.dataparser_config.setup() +<<<<<<< HEAD +======= + self.includes_time = self.dataparser.includes_time +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb self.train_dataparser_outputs = self.dataparser.get_dataparser_outputs(split="train") self.train_dataset = self.create_train_dataset() self.eval_dataset = self.create_eval_dataset() + + if self.train_dataparser_outputs is not None: + cameras = self.train_dataparser_outputs.cameras + if len(cameras) > 1: + for i in range(1, len(cameras)): + if cameras[0].width != cameras[i].width or cameras[0].height != cameras[i].height: + CONSOLE.print("Variable resolution, using variable_res_collate") + self.config.collate_fn = variable_res_collate + break + super().__init__() - def create_train_dataset(self) -> InputDataset: + def __class_getitem__(cls, item): + return type( + cls.__name__, + (cls,), + {"__module__": cls.__module__, "__init__": functools.partialmethod(cls.__init__, _dataset_type=item)}, + ) + + def create_train_dataset(self) -> TDataset: """Sets up the data loaders for training""" - return InputDataset( + return self.dataset_type( dataparser_outputs=self.train_dataparser_outputs, scale_factor=self.config.camera_res_scale_factor, ) - def create_eval_dataset(self) -> InputDataset: + def create_eval_dataset(self) -> TDataset: """Sets up the data loaders for evaluation""" - return InputDataset( + return self.dataset_type( dataparser_outputs=self.dataparser.get_dataparser_outputs(split=self.test_split), scale_factor=self.config.camera_res_scale_factor, ) def _get_pixel_sampler( # pylint: disable=no-self-use - self, dataset: InputDataset, *args: Any, **kwargs: Any + self, dataset: TDataset, *args: Any, **kwargs: Any ) -> PixelSampler: """Infer pixel sampler to use.""" if self.config.patch_size > 1: diff --git a/nerfstudio/data/datamanagers/depth_datamanager.py b/nerfstudio/data/datamanagers/depth_datamanager.py deleted file mode 100644 index 465e79e2d..000000000 --- a/nerfstudio/data/datamanagers/depth_datamanager.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2022 The Nerfstudio Team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Depth datamanager. -""" - -from dataclasses import dataclass, field -from typing import Type - -from nerfstudio.data.datamanagers import base_datamanager -from nerfstudio.data.datasets.depth_dataset import DepthDataset - - -@dataclass -class DepthDataManagerConfig(base_datamanager.VanillaDataManagerConfig): - """A depth datamanager - required to use with .setup()""" - - _target: Type = field(default_factory=lambda: DepthDataManager) - - -class DepthDataManager(base_datamanager.VanillaDataManager): # pylint: disable=abstract-method - """Data manager implementation for data that also requires processing depth data. - Args: - config: the DataManagerConfig used to instantiate class - """ - - def create_train_dataset(self) -> DepthDataset: - self.train_dataparser_outputs = self.dataparser.get_dataparser_outputs(split="train") - return DepthDataset( - dataparser_outputs=self.train_dataparser_outputs, - ) - - def create_eval_dataset(self) -> DepthDataset: - return DepthDataset( - dataparser_outputs=self.dataparser.get_dataparser_outputs(split=self.test_split), - ) diff --git a/nerfstudio/data/datamanagers/semantic_datamanager.py b/nerfstudio/data/datamanagers/semantic_datamanager.py deleted file mode 100644 index 322b1d3bd..000000000 --- a/nerfstudio/data/datamanagers/semantic_datamanager.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2022 The Nerfstudio Team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Semantic datamanager. -""" - -from dataclasses import dataclass, field -from typing import Type - -from nerfstudio.data.datamanagers.base_datamanager import ( - VanillaDataManager, - VanillaDataManagerConfig, -) -from nerfstudio.data.datasets.semantic_dataset import SemanticDataset - - -@dataclass -class SemanticDataManagerConfig(VanillaDataManagerConfig): - """A semantic datamanager - required to use with .setup()""" - - _target: Type = field(default_factory=lambda: SemanticDataManager) - - -class SemanticDataManager(VanillaDataManager): # pylint: disable=abstract-method - """Data manager implementation for data that also requires processing semantic data. - - Args: - config: the DataManagerConfig used to instantiate class - """ - - def create_train_dataset(self) -> SemanticDataset: - self.train_dataparser_outputs = self.dataparser.get_dataparser_outputs(split="train") - return SemanticDataset( - dataparser_outputs=self.train_dataparser_outputs, - scale_factor=self.config.camera_res_scale_factor, - ) - - def create_eval_dataset(self) -> SemanticDataset: - return SemanticDataset( - dataparser_outputs=self.dataparser.get_dataparser_outputs(split=self.test_split), - scale_factor=self.config.camera_res_scale_factor, - ) diff --git a/nerfstudio/data/datamanagers/variable_res_datamanager.py b/nerfstudio/data/datamanagers/variable_res_datamanager.py deleted file mode 100644 index b821bc034..000000000 --- a/nerfstudio/data/datamanagers/variable_res_datamanager.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2022 The Nerfstudio Team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Data loader for variable resolution datasets, where batching raw image tensors isn't possible. -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Dict, List - -from nerfstudio.data.datamanagers.base_datamanager import VanillaDataManagerConfig -from nerfstudio.data.utils.nerfstudio_collate import nerfstudio_collate - - -def variable_res_collate(batch: List[Dict]) -> Dict: - """Default collate function for the cached dataloader. - Args: - batch: Batch of samples from the dataset. - Returns: - Collated batch. - """ - images = [] - masks = [] - for data in batch: - image = data.pop("image") - mask = data.pop("mask", None) - images.append(image) - if mask: - masks.append(mask) - - new_batch: dict = nerfstudio_collate(batch) - new_batch["image"] = images - if masks: - new_batch["mask"] = masks - - return new_batch - - -@dataclass -class VariableResDataManagerConfig(VanillaDataManagerConfig): - """A datamanager for variable resolution datasets, with presets to optimize - for the fact that we are now dealing with lists of images and masks. - """ - - train_num_images_to_sample_from: int = 40 - """Number of images to sample during training iteration.""" - train_num_times_to_repeat_images: int = 100 - """When not training on all images, number of iterations before picking new - images. If -1, never pick new images.""" - eval_num_images_to_sample_from: int = 40 - """Number of images to sample during eval iteration.""" - eval_num_times_to_repeat_images: int = 100 - collate_fn = staticmethod(variable_res_collate) diff --git a/nerfstudio/data/dataparsers/arkitscenes_dataparser.py b/nerfstudio/data/dataparsers/arkitscenes_dataparser.py index a92745bc6..cda832ef8 100644 --- a/nerfstudio/data/dataparsers/arkitscenes_dataparser.py +++ b/nerfstudio/data/dataparsers/arkitscenes_dataparser.py @@ -16,7 +16,7 @@ import math from dataclasses import dataclass, field from pathlib import Path -from typing import Type +from typing import Literal, Type import cv2 import numpy as np diff --git a/nerfstudio/data/dataparsers/base_dataparser.py b/nerfstudio/data/dataparsers/base_dataparser.py index 89c7c4e21..74d934ced 100644 --- a/nerfstudio/data/dataparsers/base_dataparser.py +++ b/nerfstudio/data/dataparsers/base_dataparser.py @@ -20,7 +20,7 @@ import json from abc import abstractmethod from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Dict, List, Optional, Type +from typing import Any, Dict, List, Literal, Optional, Type import torch from torchtyping import TensorType @@ -89,6 +89,26 @@ class DataparserOutputs: with open(path, "w", encoding="UTF-8") as file: json.dump(data, file, indent=4) + def transform_poses_to_original_space( + self, + poses: TensorType["num_poses", 3, 4], + camera_convention: Literal["opengl", "opencv"] = "opencv", + ) -> TensorType["num_poses", 3, 4]: + """ + Transforms the poses in the transformed space back to the original world coordinate system. + Args: + poses: Poses in the transformed space + camera_convention: Camera system convention used for the transformed poses + Returns: + Original poses + """ + return transform_poses_to_original_space( + poses, + self.dataparser_transform, + self.dataparser_scale, + camera_convention=camera_convention, + ) + @dataclass class DataParserConfig(cfg.InstantiateConfig): @@ -106,9 +126,14 @@ class DataParser: Args: config: datasetparser config containing all information needed to instantiate dataset + + Attributes: + config: datasetparser config containing all information needed to instantiate dataset + includes_time: Does the dataset include time information in the camera poses. """ config: DataParserConfig + includes_time: bool = False def __init__(self, config: DataParserConfig): super().__init__() @@ -136,3 +161,46 @@ class DataParser: """ dataparser_outputs = self._generate_dataparser_outputs(split) return dataparser_outputs + + +def transform_poses_to_original_space( + poses: TensorType["num_poses", 3, 4], + applied_transform: TensorType[3, 4], + applied_scale: float, + camera_convention: Literal["opengl", "opencv"] = "opencv", +) -> TensorType["num_poses", 3, 4]: + """ + Transforms the poses in the transformed space back to the original world coordinate system. + Args: + poses: Poses in the transformed space + applied_transform: Transform matrix applied in the data processing step + applied_scale: Scale used in the data processing step + camera_convention: Camera system convention used for the transformed poses + Returns: + Original poses + """ + output_poses = torch.cat( + ( + poses, + torch.tensor([[[0, 0, 0, 1]]], dtype=poses.dtype, device=poses.device).repeat_interleave(len(poses), 0), + ), + 1, + ) + output_poses[..., :3, 3] /= applied_scale + inv_transform = torch.linalg.inv( + torch.cat( + ( + applied_transform, + torch.tensor([[0, 0, 0, 1]], dtype=applied_transform.dtype, device=applied_transform.device), + ), + 0, + ) + ) + output_poses = torch.einsum("ij,bjk->bik", inv_transform, output_poses) + if camera_convention == "opencv": + output_poses[..., 0:3, 1:3] *= -1 + elif camera_convention == "opengl": + pass + else: + raise ValueError(f"Camera convention {camera_convention} is not supported.") + return output_poses[:, :3] diff --git a/nerfstudio/data/dataparsers/blender_dataparser.py b/nerfstudio/data/dataparsers/blender_dataparser.py index f1044e826..b365f95cb 100644 --- a/nerfstudio/data/dataparsers/blender_dataparser.py +++ b/nerfstudio/data/dataparsers/blender_dataparser.py @@ -77,7 +77,7 @@ class Blender(DataParser): poses.append(np.array(frame["transform_matrix"])) poses = np.array(poses).astype(np.float32) - img_0 = imageio.imread(image_filenames[0]) + img_0 = imageio.v2.imread(image_filenames[0]) image_height, image_width = img_0.shape[:2] camera_angle_x = float(meta["camera_angle_x"]) focal_length = 0.5 * image_width / np.tan(0.5 * camera_angle_x) diff --git a/nerfstudio/data/dataparsers/dnerf_dataparser.py b/nerfstudio/data/dataparsers/dnerf_dataparser.py index c26621e92..d7411b4c6 100644 --- a/nerfstudio/data/dataparsers/dnerf_dataparser.py +++ b/nerfstudio/data/dataparsers/dnerf_dataparser.py @@ -53,6 +53,7 @@ class DNeRF(DataParser): """DNeRF Dataset""" config: DNeRFDataParserConfig + includes_time: bool = True def __init__(self, config: DNeRFDataParserConfig): super().__init__(config=config) diff --git a/nerfstudio/data/dataparsers/dycheck_dataparser.py b/nerfstudio/data/dataparsers/dycheck_dataparser.py index 92cca35ff..7ddf051e1 100644 --- a/nerfstudio/data/dataparsers/dycheck_dataparser.py +++ b/nerfstudio/data/dataparsers/dycheck_dataparser.py @@ -202,6 +202,7 @@ class Dycheck(DataParser): """Dycheck (https://arxiv.org/abs/2210.13445) Dataset `iphone` subset""" config: DycheckDataParserConfig + includes_time: bool = True def __init__(self, config: DycheckDataParserConfig): super().__init__(config=config) diff --git a/nerfstudio/data/dataparsers/instant_ngp_dataparser.py b/nerfstudio/data/dataparsers/instant_ngp_dataparser.py index 2fdc2df85..8b4bf724c 100644 --- a/nerfstudio/data/dataparsers/instant_ngp_dataparser.py +++ b/nerfstudio/data/dataparsers/instant_ngp_dataparser.py @@ -65,6 +65,7 @@ class InstantNGP(DataParser): data_dir = self.config.data image_filenames = [] + mask_filenames = [] poses = [] num_skipped_image_filenames = 0 for frame in meta["frames"]: @@ -87,6 +88,9 @@ class InstantNGP(DataParser): meta["h"] = h image_filenames.append(fname) poses.append(np.array(frame["transform_matrix"])) + if "mask_path" in frame: + mask_fname = data_dir / Path(frame["mask_path"]) + mask_filenames.append(mask_fname) if num_skipped_image_filenames >= 0: CONSOLE.print(f"Skipping {num_skipped_image_filenames} files in dataset split {split}.") assert ( @@ -144,6 +148,7 @@ class InstantNGP(DataParser): image_filenames=image_filenames, cameras=cameras, scene_box=scene_box, + mask_filenames=mask_filenames if len(mask_filenames) > 0 else None, dataparser_scale=self.config.scene_scale, ) diff --git a/nerfstudio/data/dataparsers/nerfosr_dataparser.py b/nerfstudio/data/dataparsers/nerfosr_dataparser.py new file mode 100644 index 000000000..97f430e1a --- /dev/null +++ b/nerfstudio/data/dataparsers/nerfosr_dataparser.py @@ -0,0 +1,235 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" Data parser for NeRF-OSR datasets + + Presented in the paper: https://4dqv.mpi-inf.mpg.de/NeRF-OSR/ + +""" + +from __future__ import annotations + +import glob +import os +from dataclasses import dataclass, field +from pathlib import Path +from typing import List, Literal, Tuple, Type + +import numpy as np +import torch +from rich.console import Console + +from nerfstudio.cameras import camera_utils +from nerfstudio.cameras.cameras import Cameras, CameraType +from nerfstudio.data.dataparsers.base_dataparser import ( + DataParser, + DataParserConfig, + DataparserOutputs, +) +from nerfstudio.data.scene_box import SceneBox + +CONSOLE = Console(width=120) + + +def _find_files(directory: str, exts: List[str]): + """Find all files in a directory that have a certain file extension. + + Args: + directory : The directory to search for files. + exts : A list of file extensions to search for. Each file extension should be in the form '*.ext'. + + Returns: + A list of file paths for all the files that were found. The list is sorted alphabetically. + """ + if os.path.isdir(directory): + # types should be ['*.png', '*.jpg', '*.JPG', '*.PNG'] + files_grabbed = [] + for ext in exts: + files_grabbed.extend(glob.glob(os.path.join(directory, ext))) + if len(files_grabbed) > 0: + files_grabbed = sorted(files_grabbed) + return files_grabbed + return [] + + +def _parse_osm_txt(filename: str): + """Parse a text file containing numbers and return a 4x4 numpy array of float32 values. + + Args: + filename : a file containing numbers in a 4x4 matrix. + + Returns: + A numpy array of shape [4, 4] containing the numbers from the file. + """ + assert os.path.isfile(filename) + with open(filename, encoding="UTF-8") as f: + nums = f.read().split() + return np.array([float(x) for x in nums]).reshape([4, 4]).astype(np.float32) + + +def get_camera_params( + scene_dir: str, split: Literal["train", "validation", "test"] +) -> Tuple[torch.Tensor, torch.Tensor, int]: + """Load camera intrinsic and extrinsic parameters for a given scene split. + + Args" + scene_dir : The directory containing the scene data. + split : The split for which to load the camera parameters. + + Returns + A tuple containing the intrinsic parameters (as a torch.Tensor of shape [N, 4, 4]), + the camera-to-world matrices (as a torch.Tensor of shape [N, 4, 4]), and the number of cameras (N). + """ + split_dir = f"{scene_dir}/{split}" + + # camera parameters files + intrinsics_files = _find_files(f"{split_dir}/intrinsics", exts=["*.txt"]) + pose_files = _find_files(f"{split_dir}/pose", exts=["*.txt"]) + + num_cams = len(pose_files) + + intrinsics = [] + camera_to_worlds = [] + for i in range(num_cams): + intrinsics.append(_parse_osm_txt(intrinsics_files[i])) + + pose = _parse_osm_txt(pose_files[i]) + + # convert from COLMAP/OpenCV to nerfstudio camera (OpenGL/Blender) + pose[0:3, 1:3] *= -1 + + camera_to_worlds.append(pose) + + intrinsics = torch.from_numpy(np.stack(intrinsics).astype(np.float32)) # [N, 4, 4] + camera_to_worlds = torch.from_numpy(np.stack(camera_to_worlds).astype(np.float32)) # [N, 4, 4] + + return intrinsics, camera_to_worlds, num_cams + + +@dataclass +class NeRFOSRDataParserConfig(DataParserConfig): + """Nerfstudio dataset config""" + + _target: Type = field(default_factory=lambda: NeRFOSR) + """target class to instantiate""" + data: Path = Path("data/NeRF-OSR/Data/") + """Directory specifying location of data.""" + scene: str = "stjacob" + """Which scene to load""" + scene_scale: float = 1.0 + """How much to scale the region of interest by.""" + scale_factor: float = 1.0 + """How much to scale the camera origins by.""" + use_masks: bool = False + """Whether to use masks.""" + orientation_method: Literal["pca", "up", "vertical", "none"] = "vertical" + """The method to use for orientation.""" + center_method: Literal["poses", "focus", "none"] = "focus" + """The method to use for centering.""" + auto_scale_poses: bool = True + """Whether to automatically scale the poses to fit in +/- 1 bounding box.""" + + +@dataclass +class NeRFOSR(DataParser): + """NeRFOSR Dataparser + Presented in the paper: https://4dqv.mpi-inf.mpg.de/NeRF-OSR/ + + Some of this code comes from https://github.com/r00tman/NeRF-OSR/blob/main/data_loader_split.py + + Source data convention is: + camera coordinate system: x-->right, y-->down, z-->scene (opencv/colmap convention) + poses is camera-to-world + masks are 0 for dynamic content, 255 for static content + """ + + config: NeRFOSRDataParserConfig + + def _generate_dataparser_outputs(self, split="train"): + data = self.config.data + scene = self.config.scene + split = "validation" if split == "val" else split + + if scene == "trevi": + scene_dir = f"{data}/{scene}/final_clean" + split_dir = f"{data}/{scene}/final_clean/{split}" + else: + scene_dir = f"{data}/{scene}/final" + split_dir = f"{data}/{scene}/final/{split}" + + # get all split cam params + intrinsics_train, camera_to_worlds_train, n_train = get_camera_params(scene_dir, "train") + intrinsics_val, camera_to_worlds_val, n_val = get_camera_params(scene_dir, "validation") + intrinsics_test, camera_to_worlds_test, _ = get_camera_params(scene_dir, "test") + + # combine all cam params + intrinsics = torch.cat([intrinsics_train, intrinsics_val, intrinsics_test], dim=0) + camera_to_worlds = torch.cat([camera_to_worlds_train, camera_to_worlds_val, camera_to_worlds_test], dim=0) + + camera_to_worlds, _ = camera_utils.auto_orient_and_center_poses( + camera_to_worlds, + method=self.config.orientation_method, + center_method=self.config.center_method, + ) + + # Scale poses + scale_factor = 1.0 + if self.config.auto_scale_poses: + scale_factor /= torch.max(torch.abs(camera_to_worlds[:, :3, 3])) + + camera_to_worlds[:, :3, 3] *= scale_factor * self.config.scale_factor + + if split == "train": + camera_to_worlds = camera_to_worlds[:n_train] + intrinsics = intrinsics[:n_train] + elif split == "validation": + camera_to_worlds = camera_to_worlds[n_train : n_train + n_val] + intrinsics = intrinsics[n_train : n_train + n_val] + elif split == "test": + camera_to_worlds = camera_to_worlds[n_train + n_val :] + intrinsics = intrinsics[n_train + n_val :] + + cameras = Cameras( + camera_to_worlds=camera_to_worlds[:, :3, :4], + fx=intrinsics[:, 0, 0], + fy=intrinsics[:, 1, 1], + cx=intrinsics[:, 0, 2], + cy=intrinsics[:, 1, 2], + camera_type=CameraType.PERSPECTIVE, + ) + + # in x,y,z order + # assumes that the scene is centered at the origin + aabb_scale = self.config.scene_scale + scene_box = SceneBox( + aabb=torch.tensor( + [[-aabb_scale, -aabb_scale, -aabb_scale], [aabb_scale, aabb_scale, aabb_scale]], dtype=torch.float32 + ) + ) + + # --- images --- + image_filenames = _find_files(f"{split_dir}/rgb", exts=["*.png", "*.jpg", "*.JPG", "*.PNG"]) + + # --- masks --- + mask_filenames = [] + if self.config.use_masks: + mask_filenames = _find_files(f"{split_dir}/mask", exts=["*.png", "*.jpg", "*.JPG", "*.PNG"]) + + dataparser_outputs = DataparserOutputs( + image_filenames=image_filenames, + cameras=cameras, + scene_box=scene_box, + mask_filenames=mask_filenames if len(mask_filenames) > 0 else None, + dataparser_scale=self.config.scale_factor, + ) + return dataparser_outputs diff --git a/nerfstudio/data/dataparsers/nerfstudio_dataparser.py b/nerfstudio/data/dataparsers/nerfstudio_dataparser.py index 4a74cd961..d88bf46e7 100644 --- a/nerfstudio/data/dataparsers/nerfstudio_dataparser.py +++ b/nerfstudio/data/dataparsers/nerfstudio_dataparser.py @@ -18,13 +18,12 @@ from __future__ import annotations import math from dataclasses import dataclass, field from pathlib import Path, PurePath -from typing import Optional, Type +from typing import Literal, Optional, Type import numpy as np import torch from PIL import Image from rich.console import Console -from typing_extensions import Literal from nerfstudio.cameras import camera_utils from nerfstudio.cameras.cameras import CAMERA_MODEL_TO_TYPE, Cameras, CameraType @@ -46,7 +45,11 @@ class NerfstudioDataParserConfig(DataParserConfig): _target: Type = field(default_factory=lambda: Nerfstudio) """target class to instantiate""" +<<<<<<< HEAD data: Path = Path("data/nerfstudio") +======= + data: Path = Path() +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb """Directory or explicit json file path specifying location of data.""" scale_factor: float = 1.0 """How much to scale the camera origins by.""" @@ -76,6 +79,8 @@ class Nerfstudio(DataParser): def _generate_dataparser_outputs(self, split="train"): # pylint: disable=too-many-statements + assert self.config.data.exists(), f"Data directory {self.config.data} does not exist." + if self.config.data.suffix == ".json": meta = load_from_json(self.config.data) data_dir = self.config.data.parent diff --git a/nerfstudio/data/dataparsers/nuscenes_dataparser.py b/nerfstudio/data/dataparsers/nuscenes_dataparser.py index 6b35cf356..99b75a214 100644 --- a/nerfstudio/data/dataparsers/nuscenes_dataparser.py +++ b/nerfstudio/data/dataparsers/nuscenes_dataparser.py @@ -17,13 +17,12 @@ import math import os from dataclasses import dataclass, field from pathlib import Path -from typing import Optional, Tuple, Type +from typing import Literal, Optional, Tuple, Type import numpy as np import pyquaternion import torch from nuscenes.nuscenes import NuScenes as NuScenesDatabase -from typing_extensions import Literal from nerfstudio.cameras.cameras import Cameras, CameraType from nerfstudio.data.dataparsers.base_dataparser import ( @@ -56,7 +55,7 @@ class NuScenesDataParserConfig(DataParserConfig): Each clip was recorded with a suite of sensors including 6 surround cameras. It also includes 3D cuboid annotations around objects. We optionally use these cuboids to mask dynamic objects by specifying the mask_dir flag. - To create these masks use scripts/datasets/process_nuscenes_masks.py. + To create these masks use nerfstudio/scripts/datasets/process_nuscenes_masks.py. """ _target: Type = field(default_factory=lambda: NuScenes) diff --git a/nerfstudio/data/dataparsers/phototourism_dataparser.py b/nerfstudio/data/dataparsers/phototourism_dataparser.py index e5a88e10e..8b6049c64 100644 --- a/nerfstudio/data/dataparsers/phototourism_dataparser.py +++ b/nerfstudio/data/dataparsers/phototourism_dataparser.py @@ -18,12 +18,11 @@ from __future__ import annotations import math from dataclasses import dataclass, field from pathlib import Path -from typing import Type +from typing import Literal, Type import numpy as np import torch from rich.progress import Console -from typing_extensions import Literal from nerfstudio.cameras import camera_utils from nerfstudio.cameras.cameras import Cameras, CameraType @@ -136,8 +135,8 @@ class Phototourism(DataParser): ) # equally spaced training images starting and ending at 0 and num_images-1 i_eval = np.setdiff1d(i_all, i_train) # eval images are the remaining images i_all = torch.tensor(i_all) - i_train = torch.tensor(i_train) - i_eval = torch.tensor(i_eval) + i_train = torch.tensor(i_train, dtype=torch.long) + i_eval = torch.tensor(i_eval, dtype=torch.long) assert len(i_eval) == num_eval_images if split == "train": indices = i_train diff --git a/nerfstudio/data/dataparsers/scannet_dataparser.py b/nerfstudio/data/dataparsers/scannet_dataparser.py index d5a787799..eb0493926 100644 --- a/nerfstudio/data/dataparsers/scannet_dataparser.py +++ b/nerfstudio/data/dataparsers/scannet_dataparser.py @@ -16,7 +16,7 @@ import math from dataclasses import dataclass, field from pathlib import Path -from typing import Type +from typing import Literal, Type import cv2 import numpy as np @@ -39,10 +39,14 @@ class ScanNetDataParserConfig(DataParserConfig): ScanNet dataset (https://www.scan-net.org/) is a large-scale 3D dataset of indoor scenes. This dataparser assumes that the dense stream was extracted from .sens files. Expected structure of scene directory: - - color/ - - depth/ - - intrinsic/ - - pose/ + + .. code-block:: text + + root/ + β”œβ”€β”€ color/ + β”œβ”€β”€ depth/ + β”œβ”€β”€ intrinsic/ + β”œβ”€β”€ pose/ """ _target: Type = field(default_factory=lambda: ScanNet) diff --git a/nerfstudio/data/dataparsers/sdfstudio_dataparser.py b/nerfstudio/data/dataparsers/sdfstudio_dataparser.py index 9b692c65b..97c7a299d 100644 --- a/nerfstudio/data/dataparsers/sdfstudio_dataparser.py +++ b/nerfstudio/data/dataparsers/sdfstudio_dataparser.py @@ -51,11 +51,11 @@ class SDFStudioDataParserConfig(DataParserConfig): scene_scale: float = 2.0 """ Sets the bounding cube to have edge length of this size. - The longest dimension of the Friends axis-aligned bbox will be scaled to this value. + The longest dimension of the axis-aligned bbox will be scaled to this value. """ skip_every_for_val_split: int = 1 """sub sampling validation images""" - auto_orient: bool = False + auto_orient: bool = True @dataclass @@ -141,8 +141,8 @@ class SDFStudio(DataParser): # TODO supports downsample # cameras.rescale_output_resolution(scaling_factor=1.0 / self.config.downscale_factor) - - assert meta["has_mono_prior"] == self.config.include_mono_prior, f"no mono prior in {self.config.data}" + if self.config.include_mono_prior: + assert meta["has_mono_prior"], f"no mono prior in {self.config.data}" dataparser_outputs = DataparserOutputs( image_filenames=image_filenames, diff --git a/nerfstudio/data/dataparsers/sitcoms3d_dataparser.py b/nerfstudio/data/dataparsers/sitcoms3d_dataparser.py index b6906ce22..6dfc8896b 100644 --- a/nerfstudio/data/dataparsers/sitcoms3d_dataparser.py +++ b/nerfstudio/data/dataparsers/sitcoms3d_dataparser.py @@ -65,7 +65,6 @@ class Sitcoms3D(DataParser): config: Sitcoms3DDataParserConfig def _generate_dataparser_outputs(self, split="train"): # pylint: disable=unused-argument,too-many-statements - cameras_json = load_from_json(self.config.data / "cameras.json") frames = cameras_json["frames"] bbox = torch.tensor(cameras_json["bbox"]) diff --git a/nerfstudio/data/pixel_samplers.py b/nerfstudio/data/pixel_samplers.py index 244498df8..74374f381 100644 --- a/nerfstudio/data/pixel_samplers.py +++ b/nerfstudio/data/pixel_samplers.py @@ -96,11 +96,12 @@ class PixelSampler: # pylint: disable=too-few-public-methods indices = self.sample_method(num_rays_per_batch, num_images, image_height, image_width, device=device) c, y, x = (i.flatten() for i in torch.split(indices, 1, dim=-1)) + c, y, x = c.cpu(), y.cpu(), x.cpu() collated_batch = { key: value[c, y, x] for key, value in batch.items() if key != "image_idx" and value is not None } - assert collated_batch["image"].shape == (num_rays_per_batch, 3), collated_batch["image"].shape + assert collated_batch["image"].shape[0] == num_rays_per_batch # Needed to correct the random indices to their actual camera idx locations. indices[:, 0] = batch["image_idx"][c] @@ -136,7 +137,6 @@ class PixelSampler: # pylint: disable=too-few-public-methods if "mask" in batch: num_rays_in_batch = num_rays_per_batch // num_images for i in range(num_images): - image_height, image_width, _ = batch["image"][i].shape if i == num_images - 1: @@ -171,7 +171,7 @@ class PixelSampler: # pylint: disable=too-few-public-methods collated_batch["image"] = torch.cat(all_images, dim=0) - assert collated_batch["image"].shape == (num_rays_per_batch, 3), collated_batch["image"].shape + assert collated_batch["image"].shape[0] == num_rays_per_batch # Needed to correct the random indices to their actual camera idx locations. indices[:, 0] = batch["image_idx"][c] @@ -221,7 +221,6 @@ class EquirectangularPixelSampler(PixelSampler): # pylint: disable=too-few-publ mask: Optional[TensorType] = None, device: Union[torch.device, str] = "cpu", ) -> TensorType["batch_size", 3]: - if isinstance(mask, torch.Tensor): # Note: if there is a mask, sampling reduces back to uniform sampling, which gives more # sampling weight to the poles of the image than the equators. @@ -229,7 +228,6 @@ class EquirectangularPixelSampler(PixelSampler): # pylint: disable=too-few-publ indices = super().sample_method(batch_size, num_images, image_height, image_width, mask=mask, device=device) else: - # We sample theta uniformly in [0, 2*pi] # We sample phi in [0, pi] according to the PDF f(phi) = sin(phi) / 2. # This is done by inverse transform sampling. @@ -279,7 +277,6 @@ class PatchPixelSampler(PixelSampler): # pylint: disable=too-few-public-methods mask: Optional[TensorType] = None, device: Union[torch.device, str] = "cpu", ) -> TensorType["batch_size", 3]: - if mask: # Note: if there is a mask, sampling reduces back to uniform sampling indices = super().sample_method(batch_size, num_images, image_height, image_width, mask=mask, device=device) diff --git a/nerfstudio/data/scene_box.py b/nerfstudio/data/scene_box.py index e28761e29..e513a1428 100644 --- a/nerfstudio/data/scene_box.py +++ b/nerfstudio/data/scene_box.py @@ -17,7 +17,7 @@ Dataset input structures. """ from dataclasses import dataclass -from typing import Dict, Union +from typing import Union import torch from torchtyping import TensorType @@ -64,21 +64,6 @@ class SceneBox: normalized_positions = (positions - aabb[0]) / aabb_lengths return normalized_positions - def to_json(self) -> Dict: - """Returns a json object from the Python object.""" - return {"type": "aabb", "min_point": self.aabb[0].tolist(), "max_point": self.aabb[1].tolist()} - - @staticmethod - def from_json(json_: Dict) -> "SceneBox": - """Returns the an instance of SceneBox from a json dictionary. - - Args: - json_: the json dictionary containing scene box information - """ - assert json_["type"] == "aabb" - aabb = torch.tensor([json_[0], json_[1]]) - return SceneBox(aabb=aabb) - @staticmethod def from_camera_poses(poses: TensorType[..., 3, 4], scale_factor: float) -> "SceneBox": """Returns the instance of SceneBox that fully envelopes a set of poses diff --git a/nerfstudio/data/utils/dataloaders.py b/nerfstudio/data/utils/dataloaders.py index 23d712d7e..aa30407ea 100644 --- a/nerfstudio/data/utils/dataloaders.py +++ b/nerfstudio/data/utils/dataloaders.py @@ -225,30 +225,16 @@ class FixedIndicesEvalDataloader(EvalDataloader): class RandIndicesEvalDataloader(EvalDataloader): """Dataloader that returns random images. - Args: input_dataset: InputDataset to load data from device: Device to load data to """ - def __init__( - self, - input_dataset: InputDataset, - device: Union[torch.device, str] = "cpu", - **kwargs, - ): - super().__init__(input_dataset, device, **kwargs) - self.count = 0 - def __iter__(self): - self.count = 0 return self def __next__(self): - if self.count < 1: - image_indices = range(self.cameras.size) - image_idx = random.choice(image_indices) - ray_bundle, batch = self.get_data_from_image_idx(image_idx) - self.count += 1 - return ray_bundle, batch - raise StopIteration + # choose a random image index + image_idx = random.randint(0, len(self.cameras) - 1) + ray_bundle, batch = self.get_data_from_image_idx(image_idx) + return ray_bundle, batch diff --git a/nerfstudio/data/utils/nerfstudio_collate.py b/nerfstudio/data/utils/nerfstudio_collate.py index ffcd989b4..91774343a 100644 --- a/nerfstudio/data/utils/nerfstudio_collate.py +++ b/nerfstudio/data/utils/nerfstudio_collate.py @@ -23,7 +23,6 @@ from typing import Callable, Dict, Union import torch import torch.utils.data -from torch._six import string_classes from nerfstudio.cameras.cameras import Cameras @@ -119,7 +118,7 @@ def nerfstudio_collate( return torch.tensor(batch, dtype=torch.float64) elif isinstance(elem, int): return torch.tensor(batch) - elif isinstance(elem, string_classes): + elif isinstance(elem, (str, bytes)): return batch elif isinstance(elem, collections.abc.Mapping): try: diff --git a/nerfstudio/engine/schedulers.py b/nerfstudio/engine/schedulers.py index 48a656650..a67e2c697 100644 --- a/nerfstudio/engine/schedulers.py +++ b/nerfstudio/engine/schedulers.py @@ -18,11 +18,10 @@ from abc import abstractmethod from dataclasses import dataclass, field -from typing import Optional, Type +from typing import Literal, Optional, Tuple, Type import numpy as np from torch.optim import Optimizer, lr_scheduler -from typing_extensions import Literal from nerfstudio.configs.base_config import InstantiateConfig @@ -56,6 +55,34 @@ class Scheduler: """ +@dataclass +class MultiStepSchedulerConfig(SchedulerConfig): + """Config for multi step scheduler where lr decays by gamma every milestone""" + + _target: Type = field(default_factory=lambda: MultiStepScheduler) + """target class to instantiate""" + max_steps: int = 1000000 + """The maximum number of steps.""" + gamma: float = 0.33 + """The learning rate decay factor.""" + milestones: Tuple[int, ...] = (500000, 750000, 900000) + """The milestone steps at which to decay the learning rate.""" + + +class MultiStepScheduler(Scheduler): + """Multi step scheduler where lr decays by gamma every milestone""" + + config: MultiStepSchedulerConfig + + def get_scheduler(self, optimizer: Optimizer, lr_init: float) -> lr_scheduler._LRScheduler: + scheduler = lr_scheduler.MultiStepLR( + optimizer=optimizer, + milestones=self.config.milestones, + gamma=self.config.gamma, + ) + return scheduler + + @dataclass class ExponentialDecaySchedulerConfig(SchedulerConfig): """Config for exponential decay scheduler with warmup""" @@ -111,7 +138,11 @@ class ExponentialDecayScheduler(Scheduler): @dataclass class CosineDecaySchedulerConfig(SchedulerConfig): +<<<<<<< HEAD """Basic scheduler config with self-defined exponential decay schedule""" +======= + """Config for cosine decay schedule""" +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb _target: Type = field(default_factory=lambda: CosineDecayScheduler) """target class to instantiate""" @@ -124,7 +155,11 @@ class CosineDecaySchedulerConfig(SchedulerConfig): class CosineDecayScheduler(Scheduler): +<<<<<<< HEAD """Starts with a flat lr schedule until it reaches N epochs then applies a given scheduler""" +======= + """Cosine decay scheduler with linear warmup""" +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb config: CosineDecaySchedulerConfig diff --git a/nerfstudio/engine/trainer.py b/nerfstudio/engine/trainer.py index 97ff9244a..ae36d2326 100644 --- a/nerfstudio/engine/trainer.py +++ b/nerfstudio/engine/trainer.py @@ -23,12 +23,15 @@ import os import time from dataclasses import dataclass, field from pathlib import Path -from typing import Dict, List, Optional, Tuple, Type, Union +from threading import Lock +from typing import Dict, List, Literal, Optional, Tuple, Type, Union import torch +from rich import box, style from rich.console import Console +from rich.panel import Panel +from rich.table import Table from torch.cuda.amp.grad_scaler import GradScaler -from typing_extensions import Literal from nerfstudio.configs.experiment_config import ExperimentConfig from nerfstudio.engine.callbacks import ( @@ -46,7 +49,7 @@ from nerfstudio.utils.decorators import ( ) from nerfstudio.utils.misc import step_check from nerfstudio.utils.writer import EventName, TimeWriter -from nerfstudio.viewer.server import viewer_utils +from nerfstudio.viewer.server.viewer_state import ViewerState CONSOLE = Console(width=120) @@ -74,6 +77,8 @@ class TrainerConfig(ExperimentConfig): """Maximum number of iterations to run.""" mixed_precision: bool = False """Whether or not to use mixed precision for training.""" + use_grad_scaler: bool = False + """Use gradient scaler even if the automatic mixed precision is disabled.""" save_only_latest_checkpoint: bool = True """Whether to only save the latest checkpoint or all checkpoints.""" # optional parameters if we want to resume training @@ -83,6 +88,8 @@ class TrainerConfig(ExperimentConfig): """Optionally specify model step to load from; if none, will find most recent model in load_dir.""" load_config: Optional[Path] = None """Path to config YAML file.""" + load_checkpoint: Optional[Path] = None + """Path to checkpoint file.""" log_gradients: bool = False """Optionally log gradients during training""" @@ -103,6 +110,7 @@ class Trainer: pipeline: The pipeline object. optimizers: The optimizers object. callbacks: The callbacks object. + training_state: Current model training state. """ pipeline: VanillaPipeline @@ -110,17 +118,21 @@ class Trainer: callbacks: List[TrainingCallback] def __init__(self, config: TrainerConfig, local_rank: int = 0, world_size: int = 1) -> None: + self.train_lock = Lock() self.config = config self.local_rank = local_rank self.world_size = world_size self.device: TORCH_DEVICE = "cpu" if world_size == 0 else f"cuda:{local_rank}" self.mixed_precision: bool = self.config.mixed_precision + self.use_grad_scaler: bool = self.mixed_precision or self.config.use_grad_scaler + self.training_state: Literal["training", "paused", "completed"] = "training" + if self.device == "cpu": self.mixed_precision = False CONSOLE.print("Mixed precision is disabled for CPU training.") self._start_step: int = 0 # optimizers - self.grad_scaler = GradScaler(enabled=self.mixed_precision) + self.grad_scaler = GradScaler(enabled=self.use_grad_scaler) self.base_dir: Path = config.get_base_dir() # directory to save checkpoints @@ -147,6 +159,24 @@ class Trainer: ) self.optimizers = self.setup_optimizers() + # set up viewer if enabled + viewer_log_path = self.base_dir / self.config.viewer.relative_log_filename + self.viewer_state, banner_messages = None, None + if self.config.is_viewer_enabled() and self.local_rank == 0: + datapath = self.config.data + if datapath is None: + datapath = self.base_dir + self.viewer_state = ViewerState( + self.config.viewer, + log_filename=viewer_log_path, + datapath=datapath, + pipeline=self.pipeline, + trainer=self, + train_lock=self.train_lock, + ) + banner_messages = [f"Viewer at: {self.viewer_state.viewer_url}"] + self._check_viewer_warnings() + self._load_checkpoint() self.callbacks = self.pipeline.get_training_callbacks( @@ -157,27 +187,20 @@ class Trainer: ) ) - # set up viewer if enabled - viewer_log_path = self.base_dir / self.config.viewer.relative_log_filename - self.viewer_state, banner_messages = None, None - if self.config.is_viewer_enabled() and self.local_rank == 0: - datapath = self.pipeline.datamanager.get_datapath() - if datapath is None: - datapath = self.base_dir - self.viewer_state, banner_messages = viewer_utils.setup_viewer( - self.config.viewer, log_filename=viewer_log_path, datapath=datapath - ) - self._check_viewer_warnings() # set up writers/profilers if enabled writer_log_path = self.base_dir / self.config.logging.relative_log_dir writer.setup_event_writer( - self.config.is_wandb_enabled(), self.config.is_tensorboard_enabled(), log_dir=writer_log_path + self.config.is_wandb_enabled(), + self.config.is_tensorboard_enabled(), + log_dir=writer_log_path, + experiment_name=self.config.experiment_name, + project_name=self.config.project_name, ) writer.setup_local_writer( self.config.logging, max_iter=self.config.max_num_iterations, banner_messages=banner_messages ) writer.put_config(name="config", config_dict=dataclasses.asdict(self.config), step=0) - profiler.setup_profiler(self.config.logging) + profiler.setup_profiler(self.config.logging, writer_log_path) def setup_optimizers(self) -> Optimizers: """Helper to set up the optimizers @@ -210,21 +233,26 @@ class Trainer: num_iterations = self.config.max_num_iterations step = 0 for step in range(self._start_step, self._start_step + num_iterations): - with TimeWriter(writer, EventName.ITER_TRAIN_TIME, step=step) as train_t: - self.pipeline.train() + while self.training_state == "paused": + time.sleep(0.01) + with self.train_lock: + with TimeWriter(writer, EventName.ITER_TRAIN_TIME, step=step) as train_t: + self.pipeline.train() - # training callbacks before the training iteration - for callback in self.callbacks: - callback.run_callback_at_location( - step, location=TrainingCallbackLocation.BEFORE_TRAIN_ITERATION - ) + # training callbacks before the training iteration + for callback in self.callbacks: + callback.run_callback_at_location( + step, location=TrainingCallbackLocation.BEFORE_TRAIN_ITERATION + ) - # time the forward pass - loss, loss_dict, metrics_dict = self.train_iteration(step) + # time the forward pass + loss, loss_dict, metrics_dict = self.train_iteration(step) - # training callbacks after the training iteration - for callback in self.callbacks: - callback.run_callback_at_location(step, location=TrainingCallbackLocation.AFTER_TRAIN_ITERATION) + # training callbacks after the training iteration + for callback in self.callbacks: + callback.run_callback_at_location( + step, location=TrainingCallbackLocation.AFTER_TRAIN_ITERATION + ) # Skip the first two steps to avoid skewed timings that break the viewer rendering speed estimate. if step > 1: @@ -242,6 +270,14 @@ class Trainer: writer.put_scalar(name="Train Loss", scalar=loss, step=step) writer.put_dict(name="Train Loss Dict", scalar_dict=loss_dict, step=step) writer.put_dict(name="Train Metrics Dict", scalar_dict=metrics_dict, step=step) + # The actual memory allocated by Pytorch. This is likely less than the amount + # shown in nvidia-smi since some unused memory can be held by the caching + # allocator and some context needs to be created on GPU. See Memory management + # (https://pytorch.org/docs/stable/notes/cuda.html#cuda-memory-management) + # for more details about GPU memory management. + writer.put_scalar( + name="GPU Memory (MB)", scalar=torch.cuda.max_memory_allocated() / (1024**2), step=step + ) # Do not perform evaluation if there are no validation images if self.pipeline.datamanager.eval_dataset: @@ -258,18 +294,18 @@ class Trainer: # write out any remaining events (e.g., total train time) writer.write_out_storage() - CONSOLE.rule() - CONSOLE.print("[bold green]:tada: :tada: :tada: Training Finished :tada: :tada: :tada:", justify="center") - if not self.config.viewer.quit_on_train_completion: - CONSOLE.print("Use ctrl+c to quit", justify="center") - self._always_render(step) + table = Table( + title=None, + show_header=False, + box=box.MINIMAL, + title_style=style.Style(bold=True), + ) + table.add_row("Config File", str(self.config.get_base_dir() / "config.yml")) + table.add_row("Checkpoint Directory", str(self.checkpoint_dir)) + CONSOLE.print(Panel(table, title="[bold][green]:tada: Training Finished :tada:[/bold]", expand=False)) - @check_main_thread - def _always_render(self, step: int) -> None: - if self.viewer_state is not None: - while True: - self.viewer_state.vis["renderingState/isTraining"].write(False) - self._update_viewer_state(step) + if not self.config.viewer.quit_on_train_completion: + self._train_complete_viewer() @check_main_thread def _check_viewer_warnings(self) -> None: @@ -291,10 +327,8 @@ class Trainer: assert self.viewer_state and self.pipeline.datamanager.train_dataset self.viewer_state.init_scene( dataset=self.pipeline.datamanager.train_dataset, - start_train=self.config.viewer.start_train, + train_state="training", ) - if not self.config.viewer.start_train: - self._always_render(self._start_step) @check_viewer_enabled def _update_viewer_state(self, step: int) -> None: @@ -305,16 +339,26 @@ class Trainer: step: current train step """ assert self.viewer_state is not None - with TimeWriter(writer, EventName.ITER_VIS_TIME, step=step) as _: - num_rays_per_batch: int = self.pipeline.datamanager.get_train_rays_per_batch() - try: - self.viewer_state.update_scene(self, step, self.pipeline.model, num_rays_per_batch) - except RuntimeError: - time.sleep(0.03) # sleep to allow buffer to reset - assert self.viewer_state.vis is not None - self.viewer_state.vis["renderingState/log_errors"].write( - "Error: GPU out of memory. Reduce resolution to prevent viewer from crashing." - ) + num_rays_per_batch: int = self.pipeline.datamanager.get_train_rays_per_batch() + try: + self.viewer_state.update_scene(step, num_rays_per_batch) + except RuntimeError: + time.sleep(0.03) # sleep to allow buffer to reset + CONSOLE.log("Viewer failed. Continuing training.") + + @check_viewer_enabled + def _train_complete_viewer(self) -> None: + """Let the viewer know that the training is complete""" + assert self.viewer_state is not None + self.training_state = "completed" + try: + self.viewer_state.training_complete() + except RuntimeError: + time.sleep(0.03) # sleep to allow buffer to reset + CONSOLE.log("Viewer failed. Continuing training.") + CONSOLE.print("Use ctrl+c to quit", justify="center") + while True: + time.sleep(0.01) @check_viewer_enabled def _update_viewer_rays_per_sec(self, train_t: TimeWriter, vis_t: TimeWriter, step: int) -> None: @@ -336,10 +380,11 @@ class Trainer: def _load_checkpoint(self) -> None: """Helper function to load pipeline and optimizer from prespecified checkpoint""" load_dir: Path = self.config.load_dir + load_checkpoint: Path = self.config.load_checkpoint if load_dir is not None: load_step = self.config.load_step if load_step is None: - print("Loading latest checkpoint from load_dir") + print("Loading latest Nerfstudio checkpoint from load_dir...") # NOTE: this is specific to the checkpoint name format load_step = sorted(int(x[x.find("-") + 1 : x.find(".")]) for x in os.listdir(load_dir))[-1] load_path: Path = load_dir / f"step-{load_step:09d}.ckpt" @@ -350,9 +395,18 @@ class Trainer: self.pipeline.load_pipeline(loaded_state["pipeline"], loaded_state["step"]) self.optimizers.load_optimizers(loaded_state["optimizers"]) self.grad_scaler.load_state_dict(loaded_state["scalers"]) - CONSOLE.print(f"done loading checkpoint from {load_path}") + CONSOLE.print(f"Done loading Nerfstudio checkpoint from {load_path}") + elif load_checkpoint is not None: + assert load_checkpoint.exists(), f"Checkpoint {load_checkpoint} does not exist" + loaded_state = torch.load(load_checkpoint, map_location="cpu") + self._start_step = loaded_state["step"] + 1 + # load the checkpoints for pipeline, optimizers, and gradient scalar + self.pipeline.load_pipeline(loaded_state["pipeline"], loaded_state["step"]) + self.optimizers.load_optimizers(loaded_state["optimizers"]) + self.grad_scaler.load_state_dict(loaded_state["scalers"]) + CONSOLE.print(f"Done loading Nerfstudio checkpoint from {load_checkpoint}") else: - CONSOLE.print("No checkpoints to load, training from scratch") + CONSOLE.print("No Nerfstudio checkpoint to load, so training from scratch.") @check_main_thread def save_checkpoint(self, step: int) -> None: @@ -391,8 +445,10 @@ class Trainer: Args: step: Current training step. """ + self.optimizers.zero_grad_all() cpu_or_cuda_str: str = self.device.split(":")[0] + with torch.autocast(device_type=cpu_or_cuda_str, enabled=self.mixed_precision): _, loss_dict, metrics_dict = self.pipeline.get_train_loss_dict(step=step) loss = functools.reduce(torch.add, loss_dict.values()) @@ -410,8 +466,11 @@ class Trainer: metrics_dict["Gradients/Total"] = total_grad + scale = self.grad_scaler.get_scale() self.grad_scaler.update() - self.optimizers.scheduler_step_all(step) + # If the gradient scaler is decreased, no optimization step is performed so we should not step the scheduler. + if scale <= self.grad_scaler.get_scale(): + self.optimizers.scheduler_step_all(step) # Merging loss and metrics dict into a single output. return loss, loss_dict, metrics_dict diff --git a/nerfstudio/exporter/marching_cubes.py b/nerfstudio/exporter/marching_cubes.py new file mode 100644 index 000000000..a88dc9017 --- /dev/null +++ b/nerfstudio/exporter/marching_cubes.py @@ -0,0 +1,252 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module implements the Marching Cubes algorithm for extracting +isosurfaces +""" + +from typing import Callable, List, Tuple, Union + +import numpy as np +import torch +import trimesh +from skimage import measure +from torchtyping import TensorType + +avg_pool_3d = torch.nn.AvgPool3d(2, stride=2) +upsample = torch.nn.Upsample(scale_factor=2, mode="nearest") + + +def create_point_pyramid( + points: TensorType[3, "height", "width", "depth"] +) -> List[TensorType[3, "height", "width", "depth"]]: + """ + Create a point pyramid for multi-resolution evaluation. + + Args: + points: A torch tensor containing 3D points. + + Returns: + A list of torch tensors representing points at different resolutions. + """ + points_pyramid = [points] + for _ in range(3): + points = avg_pool_3d(points[None])[0] + points_pyramid.append(points) + points_pyramid = points_pyramid[::-1] + return points_pyramid + + +def evaluate_sdf(sdf: Callable, points: TensorType["batch", 3]) -> TensorType["batch"]: + """ + Evaluate a signed distance function (SDF) for a batch of points. + + Args: + sdf: A callable function that takes a tensor of size (N, 3) containing + 3D points and returns a tensor of size (N,) with the SDF values. + points: A torch tensor containing 3D points. + + Returns: + A torch tensor with the SDF values evaluated at the given points. + """ + z = [] + for _, pnts in enumerate(torch.split(points, 100000, dim=0)): + z.append(sdf(pnts)) + z = torch.cat(z, axis=0) + return z + + +def evaluate_multiresolution_sdf( + evaluate: Callable, + points_pyramid: List[TensorType[3, "height", "width", "depth"]], + coarse_mask: Union[TensorType[1, 1, "height", "width", "depth"], None], + x_max: float, + x_min: float, + crop_n: int, +) -> TensorType["batch"]: + """ + Evaluate SDF values using a multi-resolution approach with a given point pyramid. + + Args: + evaluate: A callable function to evaluate SDF values at given points. + points_pyramid: A list of torch tensors representing points at different resolutions. + coarse_mask: A binary mask tensor indicating regions of the bounding box where the SDF + is expected to have a zero-crossing. + x_max: The maximum x-coordinate of the bounding box. + x_min: The minimum x-coordinate of the bounding box. + crop_n: The resolution of the grid used to sample the SDF. + + Returns: + A torch tensor with the SDF values evaluated at the given points. + """ + mask = None + threshold = 2 * (x_max - x_min) / crop_n * 8 + for pid, pts in enumerate(points_pyramid): + coarse_n = pts.shape[-1] + pts = pts.reshape(3, -1).permute(1, 0).contiguous() + + if mask is None: + # Only evaluate SDF + if coarse_mask is not None: + pts_sdf = torch.ones_like(pts[:, 1]) + valid_mask = torch.nn.functional.grid_sample(coarse_mask, pts[None, None, None])[0, 0, 0, 0] > 0 + if valid_mask.any(): + pts_sdf[valid_mask] = evaluate(pts[valid_mask].contiguous()) + else: + pts_sdf = evaluate(pts) + else: + mask = mask.reshape(-1) + pts_to_eval = pts[mask] + + if pts_to_eval.shape[0] > 0: + pts_sdf_eval = evaluate(pts_to_eval.contiguous()) + pts_sdf[mask] = pts_sdf_eval + + if pid < 3: + # Update mask + mask = torch.abs(pts_sdf) < threshold + mask = mask.reshape(coarse_n, coarse_n, coarse_n)[None, None] + mask = upsample(mask.float()).bool() + + pts_sdf = pts_sdf.reshape(coarse_n, coarse_n, coarse_n)[None, None] + pts_sdf = upsample(pts_sdf) + pts_sdf = pts_sdf.reshape(-1) + + threshold /= 2.0 + return pts_sdf + + +@torch.no_grad() +def generate_mesh_with_multires_marching_cubes( + geometry_callable_field: Callable, + resolution: int = 512, + bounding_box_min: Tuple[float, float, float] = (-1.0, -1.0, -1.0), + bounding_box_max: Tuple[float, float, float] = (1.0, 1.0, 1.0), + isosurface_threshold: float = 0.0, + coarse_mask: Union[None, TensorType["height", "width", "depth"]] = None, +) -> trimesh.Trimesh: + """ + Computes the isosurface of a signed distance function (SDF) defined by the + callable `sdf` in a given bounding box with a specified resolution. The SDF + is sampled at a set of points within a regular grid, and the marching cubes + algorithm is used to generate a mesh that approximates the isosurface at a + specified isovalue `level`. + + Args: + sdf: A callable function that takes as input a tensor of size + (N, 3) containing 3D points, and returns a tensor of size (N,) containing + the signed distance function evaluated at those points. + output_path: The output directory where the resulting mesh will be saved. + resolution: The resolution of the grid used to sample the SDF. + bounding_box_min: The minimum coordinates of the bounding box in which the SDF + will be evaluated. + bounding_box_max: The maximum coordinates of the bounding box in which the SDF + will be evaluated. + isosurface_threshold: The isovalue at which to approximate the isosurface. + coarse_mask: A binary mask tensor of size ("height", "width", "depth") that indicates the regions + of the bounding box where the SDF is expected to have a zero-crossing. If + provided, the algorithm first evaluates the SDF at the coarse voxels where + the mask is True, and then refines the evaluation within these voxels using + a multi-scale approach. If None, evaluates the SDF at all points in the + bounding box. + Returns: + A torch tensor with the SDF values evaluated at the given points. + """ + # Check if resolution is divisible by 512 + assert ( + resolution % 512 == 0 + ), f"""resolution must be divisible by 512, got {resolution}. + This is important because the algorithm uses a multi-resolution approach + to evaluate the SDF where the mimimum resolution is 512.""" + # Prepare coarse mask if provided + if coarse_mask is not None: + coarse_mask = coarse_mask.permute(2, 1, 0)[None, None].cuda().float() + + # Initialize variables + crop_n = 512 + N = resolution // crop_n + grid_min = bounding_box_min + grid_max = bounding_box_max + xs = np.linspace(grid_min[0], grid_max[0], N + 1) + ys = np.linspace(grid_min[1], grid_max[1], N + 1) + zs = np.linspace(grid_min[2], grid_max[2], N + 1) + + # Initialize meshes list + meshes = [] + + # Iterate over the grid + for i in range(N): + for j in range(N): + for k in range(N): + # Calculate grid cell boundaries + x_min, x_max = xs[i], xs[i + 1] + y_min, y_max = ys[j], ys[j + 1] + z_min, z_max = zs[k], zs[k + 1] + + # Create point grid + x = np.linspace(x_min, x_max, crop_n) + y = np.linspace(y_min, y_max, crop_n) + z = np.linspace(z_min, z_max, crop_n) + xx, yy, zz = np.meshgrid(x, y, z, indexing="ij") + points = torch.tensor(np.vstack([xx.ravel(), yy.ravel(), zz.ravel()]).T, dtype=torch.float).cuda() + + # Function to evaluate SDF for a batch of points + def evaluate(points: torch.Tensor) -> torch.Tensor: + return evaluate_sdf(geometry_callable_field, points) + + # Construct point pyramids + points = points.reshape(crop_n, crop_n, crop_n, 3).permute(3, 0, 1, 2) + if coarse_mask is not None: + points_tmp = points.permute(1, 2, 3, 0)[None].cuda() + current_mask = torch.nn.functional.grid_sample(coarse_mask, points_tmp) + current_mask = (current_mask > 0.0).cpu().numpy()[0, 0] + else: + current_mask = None + + # Create point pyramid for multi-resolution evaluation + points_pyramid = create_point_pyramid(points) + + # Evaluate SDF using multi-resolution approach and mask + pts_sdf = evaluate_multiresolution_sdf(evaluate, points_pyramid, coarse_mask, x_max, x_min, crop_n) + + z = pts_sdf.detach().cpu().numpy() + + # Skip if no surface found + if current_mask is not None: + valid_z = z.reshape(crop_n, crop_n, crop_n)[current_mask] + if valid_z.shape[0] <= 0 or ( + np.min(valid_z) > isosurface_threshold or np.max(valid_z) < isosurface_threshold + ): + continue + + if not (np.min(z) > isosurface_threshold or np.max(z) < isosurface_threshold): + z = z.astype(np.float32) + verts, faces, normals, _ = measure.marching_cubes( + volume=z.reshape(crop_n, crop_n, crop_n), + level=isosurface_threshold, + spacing=( + (x_max - x_min) / (crop_n - 1), + (y_max - y_min) / (crop_n - 1), + (z_max - z_min) / (crop_n - 1), + ), + mask=current_mask, + ) + verts = verts + np.array([x_min, y_min, z_min]) + + meshcrop = trimesh.Trimesh(verts, faces, normals) + meshes.append(meshcrop) + + combined_mesh: trimesh.Trimesh = trimesh.util.concatenate(meshes) + return combined_mesh diff --git a/nerfstudio/exporter/texture_utils.py b/nerfstudio/exporter/texture_utils.py index 4001d1c60..9c0bb0ee0 100644 --- a/nerfstudio/exporter/texture_utils.py +++ b/nerfstudio/exporter/texture_utils.py @@ -22,7 +22,7 @@ from __future__ import annotations import math from pathlib import Path -from typing import Optional, Tuple, Union +from typing import Literal, Optional, Tuple, Union import mediapy as media import numpy as np @@ -30,7 +30,6 @@ import torch import xatlas from rich.console import Console from torchtyping import TensorType -from typing_extensions import Literal from nerfstudio.cameras.rays import RayBundle from nerfstudio.exporter.exporter_utils import Mesh @@ -401,6 +400,7 @@ def export_textured_mesh( camera_indices = torch.zeros_like(origins[..., 0:1]) nears = torch.zeros_like(origins[..., 0:1]) fars = torch.ones_like(origins[..., 0:1]) * raylen + directions_norm = torch.ones_like(origins[..., 0:1]) # for surface model camera_ray_bundle = RayBundle( origins=origins, directions=directions, @@ -408,6 +408,7 @@ def export_textured_mesh( camera_indices=camera_indices, nears=nears, fars=fars, + metadata={"directions_norm": directions_norm}, ) CONSOLE.print("Creating texture image by rendering with NeRF...") diff --git a/nerfstudio/field_components/encodings.py b/nerfstudio/field_components/encodings.py index f77c17f4d..21dacbd0e 100644 --- a/nerfstudio/field_components/encodings.py +++ b/nerfstudio/field_components/encodings.py @@ -16,15 +16,15 @@ Encoding functions """ +import itertools from abc import abstractmethod -from typing import Optional +from typing import Literal, Optional, Sequence import numpy as np import torch import torch.nn.functional as F from torch import nn from torchtyping import TensorType -from typing_extensions import Literal from nerfstudio.field_components.base_field_component import FieldComponent from nerfstudio.utils.math import components_from_spherical_harmonics, expected_sin @@ -140,9 +140,9 @@ class NeRFEncoding(Encoding): Returns: Output values will be between -1 and 1 """ - in_tensor = 2 * torch.pi * in_tensor # scale to [0, 2pi] + scaled_in_tensor = 2 * torch.pi * in_tensor # scale to [0, 2pi] freqs = 2 ** torch.linspace(self.min_freq, self.max_freq, self.num_frequencies).to(in_tensor.device) - scaled_inputs = in_tensor[..., None] * freqs # [..., "input_dim", "num_scales"] + scaled_inputs = scaled_in_tensor[..., None] * freqs # [..., "input_dim", "num_scales"] scaled_inputs = scaled_inputs.view(*scaled_inputs.shape[:-2], -1) # [..., "input_dim" * "num_scales"] if covs is None: @@ -200,8 +200,8 @@ class RFFEncoding(Encoding): Returns: Output values will be between -1 and 1 """ - in_tensor = 2 * torch.pi * in_tensor # scale to [0, 2pi] - scaled_inputs = in_tensor @ self.b_matrix # [..., "num_frequencies"] + scaled_in_tensor = 2 * torch.pi * in_tensor # scale to [0, 2pi] + scaled_inputs = scaled_in_tensor @ self.b_matrix # [..., "num_frequencies"] if covs is None: encoded_inputs = torch.sin(torch.cat([scaled_inputs, scaled_inputs + torch.pi / 2.0], dim=-1)) @@ -242,7 +242,6 @@ class HashEncoding(Encoding): implementation: Literal["tcnn", "torch"] = "tcnn", interpolation: Optional[Literal["Nearest", "Linear", "Smoothstep"]] = None, ) -> None: - super().__init__(in_dim=3) self.num_levels = num_levels self.features_per_level = features_per_level @@ -557,6 +556,87 @@ class TriplaneEncoding(Encoding): self.resolution = resolution +class KPlanesEncoding(Encoding): + """Learned K-Planes encoding + + A plane encoding supporting both 3D and 4D coordinates. With 3D coordinates this is similar to + :class:`TriplaneEncoding`. With 4D coordinates, the encoding at point ``[i,j,k,q]`` is + a n-dimensional vector computed as the elementwise product of 6 n-dimensional vectors at + ``planes[i,j]``, ``planes[i,k]``, ``planes[i,q]``, ``planes[j,k]``, ``planes[j,q]``, + ``planes[k,q]``. + + Unlike :class:`TriplaneEncoding` this class supports different resolution along each axis. + + This will return a tensor of shape (bs:..., num_components) + + Args: + resolution: Resolution of the grid. Can be a sequence of 3 or 4 integers. + num_components: The number of scalar planes to use (ie: output feature size) + init_a: The lower-bound of the uniform distribution used to initialize the spatial planes + init_b: The upper-bound of the uniform distribution used to initialize the spatial planes + reduce: Whether to use the element-wise product of the planes or the sum + """ + + def __init__( + self, + resolution: Sequence[int] = (128, 128, 128), + num_components: int = 64, + init_a: float = 0.1, + init_b: float = 0.5, + reduce: Literal["sum", "product"] = "product", + ) -> None: + super().__init__(in_dim=len(resolution)) + + self.resolution = resolution + self.num_components = num_components + self.reduce = reduce + if self.in_dim not in {3, 4}: + raise ValueError( + f"The dimension of coordinates must be either 3 (static scenes) " + f"or 4 (dynamic scenes). Found resolution with {self.in_dim} dimensions." + ) + has_time_planes = self.in_dim == 4 + + self.coo_combs = list(itertools.combinations(range(self.in_dim), 2)) + # Unlike the Triplane encoding, we use a parameter list instead of batching all planes + # together to support uneven resolutions (especially useful for time). + # Dynamic models (in_dim == 4) will have 6 planes: + # (y, x), (z, x), (t, x), (z, y), (t, y), (t, z) + # static models (in_dim == 3) will only have the 1st, 2nd and 4th planes. + self.plane_coefs = nn.ParameterList() + for coo_comb in self.coo_combs: + new_plane_coef = nn.Parameter( + torch.empty([self.num_components] + [self.resolution[cc] for cc in coo_comb[::-1]]) + ) + if has_time_planes and 3 in coo_comb: # Time planes initialized to 1 + nn.init.ones_(new_plane_coef) + else: + nn.init.uniform_(new_plane_coef, a=init_a, b=init_b) + self.plane_coefs.append(new_plane_coef) + + def get_out_dim(self) -> int: + return self.num_components + + def forward(self, in_tensor: TensorType["bs":..., "input_dim"]) -> TensorType["bs":..., "output_dim"]: + """Sample features from this encoder. Expects ``in_tensor`` to be in range [-1, 1]""" + original_shape = in_tensor.shape + + output = 1.0 if self.reduce == "product" else 0.0 # identity for corresponding op + for ci, coo_comb in enumerate(self.coo_combs): + grid = self.plane_coefs[ci].unsqueeze(0) # [1, feature_dim, reso1, reso2] + coords = in_tensor[..., coo_comb].view(1, 1, -1, 2) # [1, 1, flattened_bs, 2] + interp = F.grid_sample( + grid, coords, align_corners=True, padding_mode="border" + ) # [1, output_dim, 1, flattened_bs] + interp = interp.view(self.num_components, -1).T # [flattened_bs, output_dim] + if self.reduce == "product": + output = output * interp + else: + output = output + interp + + return output.reshape(*original_shape[:-1], self.num_components) + + class SHEncoding(Encoding): """Spherical harmonic encoding diff --git a/nerfstudio/field_components/field_heads.py b/nerfstudio/field_components/field_heads.py index a5d699a31..e9a893994 100644 --- a/nerfstudio/field_components/field_heads.py +++ b/nerfstudio/field_components/field_heads.py @@ -60,7 +60,6 @@ class FieldHead(FieldComponent): in_dim: Optional[int] = None, activation: Optional[Union[nn.Module, Callable]] = None, ) -> None: - super().__init__() self.out_dim = out_dim self.activation = activation @@ -132,7 +131,6 @@ class SHFieldHead(FieldHead): def __init__( self, in_dim: Optional[int] = None, levels: int = 3, channels: int = 3, activation: Optional[nn.Module] = None ) -> None: - out_dim = channels * levels**2 super().__init__(in_dim=in_dim, out_dim=out_dim, field_head_name=FieldHeadNames.SH, activation=activation) diff --git a/nerfstudio/field_components/mlp.py b/nerfstudio/field_components/mlp.py index 545f32564..8cc5a73b6 100644 --- a/nerfstudio/field_components/mlp.py +++ b/nerfstudio/field_components/mlp.py @@ -46,7 +46,6 @@ class MLP(FieldComponent): activation: Optional[nn.Module] = nn.ReLU(), out_activation: Optional[nn.Module] = None, ) -> None: - super().__init__() self.in_dim = in_dim assert self.in_dim > 0 diff --git a/nerfstudio/field_components/spatial_distortions.py b/nerfstudio/field_components/spatial_distortions.py index 02da548a3..da32b4595 100644 --- a/nerfstudio/field_components/spatial_distortions.py +++ b/nerfstudio/field_components/spatial_distortions.py @@ -51,7 +51,7 @@ class SceneContraction(SpatialDistortion): \\end{cases} If the order is not specified, we use the Frobenius norm, this will contract the space to a sphere of - radius 1. If the order is L_inf (order=float("inf")), we will contract the space to a cube of side length 2. + radius 2. If the order is L_inf (order=float("inf")), we will contract the space to a cube of side length 4. If using voxel based encodings such as the Hash encoder, we recommend using the L_inf norm. Args: diff --git a/nerfstudio/fields/instant_ngp_field.py b/nerfstudio/fields/instant_ngp_field.py index d1b3e2fa7..e8917423d 100644 --- a/nerfstudio/fields/instant_ngp_field.py +++ b/nerfstudio/fields/instant_ngp_field.py @@ -21,7 +21,6 @@ from typing import Dict, Optional, Tuple import numpy as np import torch -from nerfacc import ContractionType, contract from torch.nn.parameter import Parameter from torchtyping import TensorType @@ -30,6 +29,13 @@ from nerfstudio.data.scene_box import SceneBox from nerfstudio.field_components.activations import trunc_exp from nerfstudio.field_components.embedding import Embedding from nerfstudio.field_components.field_heads import FieldHeadNames +<<<<<<< HEAD +======= +from nerfstudio.field_components.spatial_distortions import ( + SceneContraction, + SpatialDistortion, +) +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb from nerfstudio.fields.base_field import Field, shift_directions_for_tcnn try: @@ -69,16 +75,19 @@ class TCNNInstantNGPField(Field): use_appearance_embedding: Optional[bool] = False, num_images: Optional[int] = None, appearance_embedding_dim: int = 32, - contraction_type: ContractionType = ContractionType.UN_BOUNDED_SPHERE, num_levels: int = 16, log2_hashmap_size: int = 19, max_res: int = 2048, +<<<<<<< HEAD +======= + spatial_distortion: Optional[SpatialDistortion] = SceneContraction(), +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb ) -> None: super().__init__() self.aabb = Parameter(aabb, requires_grad=False) self.geo_feat_dim = geo_feat_dim - self.contraction_type = contraction_type + self.spatial_distortion = spatial_distortion self.use_appearance_embedding = use_appearance_embedding if use_appearance_embedding: @@ -88,7 +97,12 @@ class TCNNInstantNGPField(Field): # TODO: set this properly based on the aabb base_res: int = 16 +<<<<<<< HEAD per_level_scale = np.exp((np.log(max_res) - np.log(base_res)) / (num_levels - 1)) +======= + features_per_level: int = 2 + growth_factor = np.exp((np.log(max_res) - np.log(base_res)) / (num_levels - 1)) +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb self.direction_encoding = tcnn.Encoding( n_input_dims=3, @@ -104,10 +118,14 @@ class TCNNInstantNGPField(Field): encoding_config={ "otype": "HashGrid", "n_levels": num_levels, - "n_features_per_level": 2, + "n_features_per_level": features_per_level, "log2_hashmap_size": log2_hashmap_size, "base_resolution": base_res, +<<<<<<< HEAD "per_level_scale": per_level_scale, +======= + "per_level_scale": growth_factor, +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb }, network_config={ "otype": "FullyFusedMLP", @@ -134,9 +152,16 @@ class TCNNInstantNGPField(Field): ) def get_density(self, ray_samples: RaySamples) -> Tuple[TensorType, TensorType]: - positions = ray_samples.frustums.get_positions() + if self.spatial_distortion is not None: + positions = ray_samples.frustums.get_positions() + positions = self.spatial_distortion(positions) + positions = (positions + 2.0) / 4.0 + else: + positions = SceneBox.get_normalized_positions(ray_samples.frustums.get_positions(), self.aabb) + # Make sure the tcnn gets inputs between 0 and 1. + selector = ((positions > 0.0) & (positions < 1.0)).all(dim=-1) + positions = positions * selector[..., None] positions_flat = positions.view(-1, 3) - positions_flat = contract(x=positions_flat, roi=self.aabb, type=self.contraction_type) h = self.mlp_base(positions_flat).view(*ray_samples.frustums.shape, -1) density_before_activation, base_mlp_out = torch.split(h, [1, self.geo_feat_dim], dim=-1) @@ -145,20 +170,21 @@ class TCNNInstantNGPField(Field): # softplus, because it enables high post-activation (float32) density outputs # from smaller internal (float16) parameters. density = trunc_exp(density_before_activation.to(positions)) + density = density * selector[..., None] return density, base_mlp_out def get_outputs( self, ray_samples: RaySamples, density_embedding: Optional[TensorType] = None ) -> Dict[FieldHeadNames, TensorType]: +<<<<<<< HEAD +======= + assert density_embedding is not None +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb directions = shift_directions_for_tcnn(ray_samples.frustums.directions) directions_flat = directions.view(-1, 3) - d = self.direction_encoding(directions_flat) - if density_embedding is None: - positions = SceneBox.get_normalized_positions(ray_samples.frustums.get_positions(), self.aabb) - h = torch.cat([d, positions.view(-1, 3)], dim=-1) - else: - h = torch.cat([d, density_embedding.view(-1, self.geo_feat_dim)], dim=-1) + + h = torch.cat([d, density_embedding.view(-1, self.geo_feat_dim)], dim=-1) if self.use_appearance_embedding: if ray_samples.camera_indices is None: @@ -183,20 +209,5 @@ class TCNNInstantNGPField(Field): step_size: the step size to use for the opacity evaluation. """ density = self.density_fn(positions) - ## TODO: We should scale step size based on the distortion. Currently it uses too much memory. - # aabb_min, aabb_max = self.aabb[0], self.aabb[1] - # if self.contraction_type is not ContractionType.AABB: - # x = (positions - aabb_min) / (aabb_max - aabb_min) - # x = x * 2 - 1 # aabb is at [-1, 1] - # mag = x.norm(dim=-1, keepdim=True) - # mask = mag.squeeze(-1) > 1 - - # dev = (2 * mag - 1) / mag**2 + 2 * x**2 * (1 / mag**3 - (2 * mag - 1) / mag**4) - # dev[~mask] = 1.0 - # dev = torch.clamp(dev, min=1e-6) - # step_size = step_size / dev.norm(dim=-1, keepdim=True) - # else: - # step_size = step_size * (aabb_max - aabb_min) - opacity = density * step_size return opacity diff --git a/nerfstudio/fields/nerfacto_field.py b/nerfstudio/fields/nerfacto_field.py index de0fd7c7d..3f3953558 100644 --- a/nerfstudio/fields/nerfacto_field.py +++ b/nerfstudio/fields/nerfacto_field.py @@ -103,7 +103,7 @@ class TCNNNerfactoField(Field): pass_semantic_gradients: bool = False, use_pred_normals: bool = False, use_average_appearance_embedding: bool = False, - spatial_distortion: SpatialDistortion = None, + spatial_distortion: Optional[SpatialDistortion] = None, ) -> None: super().__init__() @@ -381,8 +381,8 @@ class TorchNerfactoField(Field): positions = self.spatial_distortion(positions) else: positions = ray_samples.frustums.get_positions() - encoded_xyz = self.position_encoding(positions) - base_mlp_out = self.mlp_base(encoded_xyz) + encoded_xyz = self.position_encoding(positions.view(-1, 3)).to(torch.float32) + base_mlp_out = self.mlp_base(encoded_xyz).view(*ray_samples.frustums.shape, -1) density = self.field_output_density(base_mlp_out) return density, base_mlp_out @@ -404,18 +404,20 @@ class TorchNerfactoField(Field): outputs = {} for field_head in self.field_heads: - encoded_dir = self.direction_encoding(ray_samples.frustums.directions) + directions = shift_directions_for_tcnn(ray_samples.frustums.directions) + directions_flat = directions.view(-1, 3) + encoded_dir = self.direction_encoding(directions_flat) mlp_out = self.mlp_head( torch.cat( [ - encoded_dir, - density_embedding, # type:ignore + encoded_dir.view(-1, self.direction_encoding.get_out_dim()), + density_embedding.view(-1, self.mlp_base.get_out_dim()), # type:ignore embedded_appearance.view(-1, self.appearance_embedding_dim), ], dim=-1, # type:ignore ) ) - outputs[field_head.field_head_name] = field_head(mlp_out) + outputs[field_head.field_head_name] = field_head(mlp_out).view(*outputs_shape, -1).to(directions) return outputs diff --git a/nerfstudio/fields/nerfplayer_ngp_field.py b/nerfstudio/fields/nerfplayer_ngp_field.py index 2952a4369..914a59820 100644 --- a/nerfstudio/fields/nerfplayer_ngp_field.py +++ b/nerfstudio/fields/nerfplayer_ngp_field.py @@ -20,7 +20,6 @@ NeRFPlayer (https://arxiv.org/abs/2210.15947) field implementations with Instant from typing import Dict, Optional, Tuple import torch -from nerfacc import ContractionType, contract from torch.nn.parameter import Parameter from torchtyping import TensorType @@ -29,6 +28,10 @@ from nerfstudio.data.scene_box import SceneBox from nerfstudio.field_components.activations import trunc_exp from nerfstudio.field_components.embedding import Embedding from nerfstudio.field_components.field_heads import FieldHeadNames +from nerfstudio.field_components.spatial_distortions import ( + SceneContraction, + SpatialDistortion, +) from nerfstudio.field_components.temporal_grid import TemporalGridEncoder from nerfstudio.fields.base_field import Field, shift_directions_for_tcnn @@ -82,13 +85,13 @@ class NerfplayerNGPField(Field): disable_viewing_dependent: bool = False, num_images: Optional[int] = None, appearance_embedding_dim: int = 32, - contraction_type: ContractionType = ContractionType.UN_BOUNDED_SPHERE, + spatial_distortion: Optional[SpatialDistortion] = SceneContraction(), ) -> None: super().__init__() self.aabb = Parameter(aabb, requires_grad=False) self.geo_feat_dim = geo_feat_dim - self.contraction_type = contraction_type + self.spatial_distortion = spatial_distortion self.use_appearance_embedding = use_appearance_embedding if use_appearance_embedding: @@ -144,9 +147,16 @@ class NerfplayerNGPField(Field): ) def get_density(self, ray_samples: RaySamples) -> Tuple[TensorType, TensorType]: - positions = ray_samples.frustums.get_positions() + if self.spatial_distortion is not None: + positions = ray_samples.frustums.get_positions() + positions = self.spatial_distortion(positions) + positions = (positions + 2.0) / 4.0 + else: + positions = SceneBox.get_normalized_positions(ray_samples.frustums.get_positions(), self.aabb) + # Make sure the tcnn gets inputs between 0 and 1. + selector = ((positions > 0.0) & (positions < 1.0)).all(dim=-1) + positions = positions * selector[..., None] positions_flat = positions.view(-1, 3) - positions_flat = contract(x=positions_flat, roi=self.aabb, type=self.contraction_type) assert ray_samples.times is not None, "Time should be included in the input for NeRFPlayer" times_flat = ray_samples.times.view(-1, 1) @@ -158,28 +168,25 @@ class NerfplayerNGPField(Field): # softplus, because it enables high post-activation (float32) density outputs # from smaller internal (float16) parameters. density = trunc_exp(density_before_activation.to(positions)) + density = density * selector[..., None] return density, base_mlp_out def get_outputs( self, ray_samples: RaySamples, density_embedding: Optional[TensorType] = None ) -> Dict[FieldHeadNames, TensorType]: +<<<<<<< HEAD +======= + assert density_embedding is not None +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb directions = shift_directions_for_tcnn(ray_samples.frustums.directions) directions_flat = directions.view(-1, 3) if self.direction_encoding is not None: d = self.direction_encoding(directions_flat) - if density_embedding is None: - positions = SceneBox.get_normalized_positions(ray_samples.frustums.get_positions(), self.aabb) - h = torch.cat([d, positions.view(-1, 3)], dim=-1) - else: - h = torch.cat([d, density_embedding.view(-1, self.geo_feat_dim)], dim=-1) + h = torch.cat([d, density_embedding.view(-1, self.geo_feat_dim)], dim=-1) else: # viewing direction is disabled - if density_embedding is None: - positions = SceneBox.get_normalized_positions(ray_samples.frustums.get_positions(), self.aabb) - h = positions.view(-1, 3) - else: - h = density_embedding.view(-1, self.geo_feat_dim) + h = density_embedding.view(-1, self.geo_feat_dim) if self.use_appearance_embedding: if ray_samples.camera_indices is None: diff --git a/nerfstudio/fields/sdf_field.py b/nerfstudio/fields/sdf_field.py index e9fd10cb8..2a67ef30e 100644 --- a/nerfstudio/fields/sdf_field.py +++ b/nerfstudio/fields/sdf_field.py @@ -18,7 +18,11 @@ a signed distance function (SDF) for surface representation is used to help with """ from dataclasses import dataclass, field +<<<<<<< HEAD from typing import Dict, Optional, Type +======= +from typing import Dict, Literal, Optional, Type +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb import numpy as np import torch @@ -26,7 +30,10 @@ import torch.nn.functional as F from torch import nn from torch.nn.parameter import Parameter from torchtyping import TensorType +<<<<<<< HEAD from typing_extensions import Literal +======= +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb from nerfstudio.cameras.rays import RaySamples from nerfstudio.field_components.embedding import Embedding @@ -80,6 +87,7 @@ class SDFFieldConfig(FieldConfig): appearance_embedding_dim: int = 32 """Dimension of appearance embedding""" use_appearance_embedding: bool = False +<<<<<<< HEAD """Dimension of appearance embedding""" bias: float = 0.8 """sphere size of geometric initializaion""" @@ -87,6 +95,15 @@ class SDFFieldConfig(FieldConfig): """Whether to use geometric initialization""" inside_outside: bool = True """whether to revert signed distance value, set to True for indoor scene""" +======= + """Whether to use appearance embedding""" + bias: float = 0.8 + """Sphere size of geometric initialization""" + geometric_init: bool = True + """Whether to use geometric initialization""" + inside_outside: bool = True + """Whether to revert signed distance value, set to True for indoor scene""" +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb weight_norm: bool = True """Whether to use weight norm for linear layer""" use_grid_feature: bool = False @@ -257,8 +274,13 @@ class SDFField(Field): """forward the geonetwork""" if self.use_grid_feature: positions = self.spatial_distortion(inputs) +<<<<<<< HEAD positions = (positions + 1.0) / 2.0 +======= + # map range [-2, 2] to [0, 1] + positions = (positions + 2.0) / 4.0 +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb feature = self.encoding(positions) else: feature = torch.zeros_like(inputs[:, :1].repeat(1, self.encoding.n_output_dims)) diff --git a/nerfstudio/model_components/losses.py b/nerfstudio/model_components/losses.py index 174c131cc..43fbbd271 100644 --- a/nerfstudio/model_components/losses.py +++ b/nerfstudio/model_components/losses.py @@ -16,11 +16,11 @@ Collection of Losses. """ from enum import Enum +from typing import Literal import torch from torch import nn from torchtyping import TensorType -from typing_extensions import Literal from nerfstudio.cameras.rays import RaySamples from nerfstudio.utils.math import masked_reduction, normalized_depth_scale_and_shift @@ -338,7 +338,7 @@ class MiDaSMSELoss(nn.Module): def __init__(self, reduction_type: Literal["image", "batch"] = "batch"): super().__init__() - self.reduction_type = reduction_type + self.reduction_type: Literal["image", "batch"] = reduction_type # reduction here is different from the image/batch-based reduction. This is either "mean" or "sum" self.mse_loss = MSELoss(reduction="none") @@ -485,3 +485,20 @@ class ScaleAndShiftInvariantLoss(nn.Module): return self.__prediction_ssi prediction_ssi = property(__get_prediction_ssi) + + +def tv_loss(grids: TensorType["grids", "feature_dim", "row", "column"]) -> TensorType[()]: + """ + https://github.com/apchenstu/TensoRF/blob/4ec894dc1341a2201fe13ae428631b58458f105d/utils.py#L139 + + Args: + grids: stacks of explicit feature grids (stacked at dim 0) + Returns: + average total variation loss for neighbor rows and columns. + """ + number_of_grids = grids.shape[0] + h_tv_count = grids[:, :, 1:, :].shape[1] * grids[:, :, 1:, :].shape[2] * grids[:, :, 1:, :].shape[3] + w_tv_count = grids[:, :, :, 1:].shape[1] * grids[:, :, :, 1:].shape[2] * grids[:, :, :, 1:].shape[3] + h_tv = torch.pow((grids[:, :, 1:, :] - grids[:, :, :-1, :]), 2).sum() + w_tv = torch.pow((grids[:, :, :, 1:] - grids[:, :, :, :-1]), 2).sum() + return 2 * (h_tv / h_tv_count + w_tv / w_tv_count) / number_of_grids diff --git a/nerfstudio/model_components/ray_samplers.py b/nerfstudio/model_components/ray_samplers.py index d6cb080c6..39a5c0047 100644 --- a/nerfstudio/model_components/ray_samplers.py +++ b/nerfstudio/model_components/ray_samplers.py @@ -19,9 +19,8 @@ Collection of sampling strategies from abc import abstractmethod from typing import Callable, List, Optional, Tuple, Union -import nerfacc import torch -from nerfacc import OccupancyGrid +from nerfacc import OccGridEstimator from torch import nn from torchtyping import TensorType @@ -382,24 +381,21 @@ class VolumetricSampler(Sampler): def __init__( self, - occupancy_grid: Optional[OccupancyGrid] = None, + occupancy_grid: OccGridEstimator, density_fn: Optional[Callable[[TensorType[..., 3]], TensorType[..., 1]]] = None, - scene_aabb: Optional[TensorType[2, 3]] = None, ) -> None: - super().__init__() - self.scene_aabb = scene_aabb + assert occupancy_grid is not None self.density_fn = density_fn self.occupancy_grid = occupancy_grid - if self.scene_aabb is not None: - self.scene_aabb = self.scene_aabb.to("cuda").flatten() - def get_sigma_fn(self, origins, directions) -> Optional[Callable]: + def get_sigma_fn(self, origins, directions, times=None) -> Optional[Callable]: """Returns a function that returns the density of a point. Args: origins: Origins of rays directions: Directions of rays + times: Times at which rays are sampled Returns: Function that returns the density of a point or None if a density function is not provided. """ @@ -412,8 +408,10 @@ class VolumetricSampler(Sampler): def sigma_fn(t_starts, t_ends, ray_indices): t_origins = origins[ray_indices] t_dirs = directions[ray_indices] - positions = t_origins + t_dirs * (t_starts + t_ends) / 2.0 - return density_fn(positions) + positions = t_origins + t_dirs * (t_starts + t_ends)[:, None] / 2.0 + if times is None: + return density_fn(positions).squeeze(-1) + return density_fn(positions, times[ray_indices]).squeeze(-1) return sigma_fn @@ -429,6 +427,7 @@ class VolumetricSampler(Sampler): render_step_size: float, near_plane: float = 0.0, far_plane: Optional[float] = None, + alpha_thre: float = 0.01, cone_angle: float = 0.0, ) -> Tuple[RaySamples, TensorType["total_samples",]]: """Generate ray samples in a bounding box. @@ -438,6 +437,7 @@ class VolumetricSampler(Sampler): render_step_size: Minimum step size to use for rendering near_plane: Near plane for raymarching far_plane: Far plane for raymarching + alpha_thre: Opacity threshold skipping samples. cone_angle: Cone angle for raymarching, set to 0 for uniform marching. Returns: @@ -448,6 +448,7 @@ class VolumetricSampler(Sampler): rays_o = ray_bundle.origins.contiguous() rays_d = ray_bundle.directions.contiguous() + times = ray_bundle.times if ray_bundle.nears is not None and ray_bundle.fars is not None: t_min = ray_bundle.nears.contiguous().reshape(-1) @@ -457,34 +458,33 @@ class VolumetricSampler(Sampler): t_min = None t_max = None + if far_plane is None: + far_plane = 1e10 + if ray_bundle.camera_indices is not None: camera_indices = ray_bundle.camera_indices.contiguous() else: camera_indices = None - - ray_indices, starts, ends = nerfacc.ray_marching( + ray_indices, starts, ends = self.occupancy_grid.sampling( rays_o=rays_o, rays_d=rays_d, t_min=t_min, t_max=t_max, - scene_aabb=self.scene_aabb, - grid=self.occupancy_grid, - # this is a workaround - using density causes crash and damage quality. should be fixed - sigma_fn=None, # self.get_sigma_fn(rays_o, rays_d), + sigma_fn=self.get_sigma_fn(rays_o, rays_d, times), render_step_size=render_step_size, near_plane=near_plane, far_plane=far_plane, stratified=self.training, cone_angle=cone_angle, - alpha_thre=1e-2, + alpha_thre=alpha_thre, ) num_samples = starts.shape[0] if num_samples == 0: # create a single fake sample and update packed_info accordingly # this says the last ray in packed_info has 1 sample, which starts and ends at 1 ray_indices = torch.zeros((1,), dtype=torch.long, device=rays_o.device) - starts = torch.ones((1, 1), dtype=starts.dtype, device=rays_o.device) - ends = torch.ones((1, 1), dtype=ends.dtype, device=rays_o.device) + starts = torch.ones((1,), dtype=starts.dtype, device=rays_o.device) + ends = torch.ones((1,), dtype=ends.dtype, device=rays_o.device) origins = rays_o[ray_indices] dirs = rays_d[ray_indices] @@ -496,8 +496,8 @@ class VolumetricSampler(Sampler): frustums=Frustums( origins=origins, directions=dirs, - starts=starts, - ends=ends, + starts=starts[..., None], + ends=ends[..., None], pixel_area=zeros, ), camera_indices=camera_indices, @@ -645,6 +645,10 @@ class NeuSSampler(Sampler): # Start with uniform sampling if ray_samples is None: ray_samples = self.uniform_sampler(ray_bundle, num_samples=self.num_samples) +<<<<<<< HEAD +======= + assert ray_samples is not None +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb total_iters = 0 sorted_index = None @@ -653,7 +657,10 @@ class NeuSSampler(Sampler): base_variance = self.base_variance while total_iters < self.num_upsample_steps: +<<<<<<< HEAD +======= +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb with torch.no_grad(): new_sdf = sdf_fn(new_samples) @@ -701,6 +708,10 @@ class NeuSSampler(Sampler): """ batch_size = ray_samples.shape[0] prev_sdf, next_sdf = sdf[:, :-1], sdf[:, 1:] +<<<<<<< HEAD +======= + assert ray_samples.deltas is not None +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb deltas = ray_samples.deltas[:, :-1, 0] mid_sdf = (prev_sdf + next_sdf) * 0.5 cos_val = (next_sdf - prev_sdf) / (deltas + 1e-5) @@ -742,6 +753,12 @@ class NeuSSampler(Sampler): ray_samples_2 : ray_samples to merge """ +<<<<<<< HEAD +======= + assert ray_samples_1.spacing_starts is not None and ray_samples_2.spacing_starts is not None + assert ray_samples_1.spacing_ends is not None and ray_samples_2.spacing_ends is not None + assert ray_samples_1.spacing_to_euclidean_fn is not None +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb starts_1 = ray_samples_1.spacing_starts[..., 0] starts_2 = ray_samples_2.spacing_starts[..., 0] diff --git a/nerfstudio/model_components/renderers.py b/nerfstudio/model_components/renderers.py index 5e488bc35..5deafad74 100644 --- a/nerfstudio/model_components/renderers.py +++ b/nerfstudio/model_components/renderers.py @@ -28,13 +28,12 @@ Example: """ import contextlib import math -from typing import Generator, Optional, Union +from typing import Generator, Literal, Optional, Union import nerfacc import torch from torch import nn from torchtyping import TensorType -from typing_extensions import Literal from nerfstudio.cameras.rays import RaySamples from nerfstudio.utils import colors @@ -91,8 +90,12 @@ class RGBRenderer(nn.Module): # Necessary for packed samples from volumetric ray sampler if background_color == "last_sample": raise NotImplementedError("Background color 'last_sample' not implemented for packed samples.") - comp_rgb = nerfacc.accumulate_along_rays(weights, ray_indices, rgb, num_rays) - accumulated_weight = nerfacc.accumulate_along_rays(weights, ray_indices, None, num_rays) + comp_rgb = nerfacc.accumulate_along_rays( + weights[..., 0], values=rgb, ray_indices=ray_indices, n_rays=num_rays + ) + accumulated_weight = nerfacc.accumulate_along_rays( + weights[..., 0], values=None, ray_indices=ray_indices, n_rays=num_rays + ) else: comp_rgb = torch.sum(weights * rgb, dim=-2) accumulated_weight = torch.sum(weights, dim=-2) @@ -180,7 +183,7 @@ class SHRenderer(nn.Module): components = components_from_spherical_harmonics(levels=levels, directions=directions) rgb = sh * components[..., None, :] # [..., num_samples, 3, sh_components] - rgb = torch.sum(sh, dim=-1) + 0.5 # [..., num_samples, 3] + rgb = torch.sum(rgb, dim=-1) # [..., num_samples, 3] if self.activation is not None: rgb = self.activation(rgb) @@ -217,7 +220,9 @@ class AccumulationRenderer(nn.Module): if ray_indices is not None and num_rays is not None: # Necessary for packed samples from volumetric ray sampler - accumulation = nerfacc.accumulate_along_rays(weights, ray_indices, None, num_rays) + accumulation = nerfacc.accumulate_along_rays( + weights[..., 0], values=None, ray_indices=ray_indices, n_rays=num_rays + ) else: accumulation = torch.sum(weights, dim=-2) return accumulation @@ -274,8 +279,12 @@ class DepthRenderer(nn.Module): if ray_indices is not None and num_rays is not None: # Necessary for packed samples from volumetric ray sampler - depth = nerfacc.accumulate_along_rays(weights, ray_indices, steps, num_rays) - accumulation = nerfacc.accumulate_along_rays(weights, ray_indices, None, num_rays) + depth = nerfacc.accumulate_along_rays( + weights[..., 0], values=steps, ray_indices=ray_indices, n_rays=num_rays + ) + accumulation = nerfacc.accumulate_along_rays( + weights[..., 0], values=None, ray_indices=ray_indices, n_rays=num_rays + ) depth = depth / (accumulation + eps) else: depth = torch.sum(weights * steps, dim=-2) / (torch.sum(weights, -2) + eps) diff --git a/nerfstudio/models/base_model.py b/nerfstudio/models/base_model.py index 58de64f95..5b0ffa522 100644 --- a/nerfstudio/models/base_model.py +++ b/nerfstudio/models/base_model.py @@ -103,6 +103,7 @@ class Model(nn.Module): # NOTE: call `super().populate_modules()` in subclasses if self.config.enable_collider: + assert self.config.collider_params is not None self.collider = NearFarCollider( near_plane=self.config.collider_params["near_plane"], far_plane=self.config.collider_params["far_plane"] ) @@ -178,12 +179,12 @@ class Model(nn.Module): ray_bundle = camera_ray_bundle.get_row_major_sliced_ray_bundle(start_idx, end_idx) outputs = self.forward(ray_bundle=ray_bundle) for output_name, output in outputs.items(): # type: ignore + if not torch.is_tensor(output): + # TODO: handle lists of tensors as well + continue outputs_lists[output_name].append(output) outputs = {} for output_name, outputs_list in outputs_lists.items(): - if not torch.is_tensor(outputs_list[0]): - # TODO: handle lists of tensors as well - continue outputs[output_name] = torch.cat(outputs_list).view(image_height, image_width, -1) # type: ignore return outputs diff --git a/nerfstudio/models/base_surface_model.py b/nerfstudio/models/base_surface_model.py index c83a07032..a6a1d8384 100644 --- a/nerfstudio/models/base_surface_model.py +++ b/nerfstudio/models/base_surface_model.py @@ -20,7 +20,11 @@ from __future__ import annotations from abc import abstractmethod from dataclasses import dataclass, field +<<<<<<< HEAD from typing import Dict, List, Tuple, Type +======= +from typing import Dict, List, Literal, Tuple, Type +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb import torch import torch.nn.functional as F @@ -28,7 +32,10 @@ from torch.nn import Parameter from torchmetrics import PeakSignalNoiseRatio from torchmetrics.functional import structural_similarity_index_measure from torchmetrics.image.lpip import LearnedPerceptualImagePatchSimilarity +<<<<<<< HEAD from typing_extensions import Literal +======= +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb from nerfstudio.cameras.rays import RayBundle from nerfstudio.field_components.encodings import NeRFEncoding diff --git a/nerfstudio/models/instant_ngp.py b/nerfstudio/models/instant_ngp.py index 2bdf85b6a..079ae10e0 100644 --- a/nerfstudio/models/instant_ngp.py +++ b/nerfstudio/models/instant_ngp.py @@ -19,16 +19,14 @@ Implementation of Instant NGP. from __future__ import annotations from dataclasses import dataclass, field -from typing import Dict, List, Optional, Tuple, Type +from typing import Dict, List, Literal, Optional, Tuple, Type import nerfacc import torch -from nerfacc import ContractionType from torch.nn import Parameter from torchmetrics import PeakSignalNoiseRatio from torchmetrics.functional import structural_similarity_index_measure from torchmetrics.image.lpip import LearnedPerceptualImagePatchSimilarity -from typing_extensions import Literal from nerfstudio.cameras.rays import RayBundle from nerfstudio.engine.callbacks import ( @@ -37,7 +35,8 @@ from nerfstudio.engine.callbacks import ( TrainingCallbackLocation, ) from nerfstudio.field_components.field_heads import FieldHeadNames -from nerfstudio.fields.instant_ngp_field import TCNNInstantNGPField +from nerfstudio.field_components.spatial_distortions import SceneContraction +from nerfstudio.fields.nerfacto_field import TCNNNerfactoField from nerfstudio.model_components.losses import MSELoss from nerfstudio.model_components.ray_samplers import VolumetricSampler from nerfstudio.model_components.renderers import ( @@ -61,19 +60,27 @@ class InstantNGPModelConfig(ModelConfig): """Whether to create a scene collider to filter rays.""" collider_params: Optional[Dict[str, float]] = None """Instant NGP doesn't use a collider.""" - max_num_samples_per_ray: int = 24 - """Number of samples in field evaluation.""" grid_resolution: int = 128 """Resolution of the grid used for the field.""" +<<<<<<< HEAD +======= + grid_levels: int = 4 + """Levels of the grid used for the field.""" +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb max_res: int = 2048 """Maximum resolution of the hashmap for the base mlp.""" log2_hashmap_size: int = 19 """Size of the hashmap for the base mlp""" +<<<<<<< HEAD contraction_type: ContractionType = ContractionType.UN_BOUNDED_SPHERE """Contraction type used for spatial deformation of the field.""" +======= + alpha_thre: float = 0.01 + """Threshold for opacity skipping.""" +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb cone_angle: float = 0.004 """Should be set to 0.0 for blender scenes but 1./256 for real scenes.""" - render_step_size: float = 0.01 + render_step_size: Optional[float] = None """Minimum step size for rendering.""" near_plane: float = 0.05 """How far along ray to start sampling.""" @@ -83,6 +90,8 @@ class InstantNGPModelConfig(ModelConfig): """Whether to use an appearance embedding.""" background_color: Literal["random", "black", "white"] = "random" """The color that is given to untrained areas.""" + disable_scene_contraction: bool = False + """Whether to disable scene contraction or not.""" class NGPModel(Model): @@ -93,7 +102,7 @@ class NGPModel(Model): """ config: InstantNGPModelConfig - field: TCNNInstantNGPField + field: TCNNNerfactoField def __init__(self, config: InstantNGPModelConfig, **kwargs) -> None: super().__init__(config=config, **kwargs) @@ -102,28 +111,36 @@ class NGPModel(Model): """Set the fields and modules.""" super().populate_modules() - self.field = TCNNInstantNGPField( + if self.config.disable_scene_contraction: + scene_contraction = None + else: + scene_contraction = SceneContraction(order=float("inf")) + + self.field = TCNNNerfactoField( aabb=self.scene_box.aabb, - contraction_type=self.config.contraction_type, - use_appearance_embedding=self.config.use_appearance_embedding, num_images=self.num_train_data, log2_hashmap_size=self.config.log2_hashmap_size, max_res=self.config.max_res, +<<<<<<< HEAD +======= + spatial_distortion=scene_contraction, +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb ) self.scene_aabb = Parameter(self.scene_box.aabb.flatten(), requires_grad=False) - # Occupancy Grid - self.occupancy_grid = nerfacc.OccupancyGrid( + if self.config.render_step_size is None: + # auto step size: ~1000 samples in the base level grid + self.config.render_step_size = ((self.scene_aabb[3:] - self.scene_aabb[:3]) ** 2).sum().sqrt().item() / 1000 + # Occupancy Grid. + self.occupancy_grid = nerfacc.OccGridEstimator( roi_aabb=self.scene_aabb, resolution=self.config.grid_resolution, - contraction_type=self.config.contraction_type, + levels=self.config.grid_levels, ) # Sampler - vol_sampler_aabb = self.scene_box.aabb if self.config.contraction_type == ContractionType.AABB else None self.sampler = VolumetricSampler( - scene_aabb=vol_sampler_aabb, occupancy_grid=self.occupancy_grid, density_fn=self.field.density_fn, ) @@ -145,11 +162,9 @@ class NGPModel(Model): self, training_callback_attributes: TrainingCallbackAttributes ) -> List[TrainingCallback]: def update_occupancy_grid(step: int): - # TODO: needs to get access to the sampler, on how the step size is determinated at each x. See - # https://github.com/KAIR-BAIR/nerfacc/blob/127223b11401125a9fce5ce269bb0546ee4de6e8/examples/train_ngp_nerf.py#L190-L213 - self.occupancy_grid.every_n_step( + self.occupancy_grid.update_every_n_steps( step=step, - occ_eval_fn=lambda x: self.field.get_opacity(x, self.config.render_step_size), + occ_eval_fn=lambda x: self.field.density_fn(x) * self.config.render_step_size, ) return [ @@ -177,6 +192,7 @@ class NGPModel(Model): near_plane=self.config.near_plane, far_plane=self.config.far_plane, render_step_size=self.config.render_step_size, + alpha_thre=self.config.alpha_thre, cone_angle=self.config.cone_angle, ) @@ -185,11 +201,12 @@ class NGPModel(Model): # accumulation packed_info = nerfacc.pack_info(ray_indices, num_rays) weights = nerfacc.render_weight_from_density( + t_starts=ray_samples.frustums.starts[..., 0], + t_ends=ray_samples.frustums.ends[..., 0], + sigmas=field_outputs[FieldHeadNames.DENSITY][..., 0], packed_info=packed_info, - sigmas=field_outputs[FieldHeadNames.DENSITY], - t_starts=ray_samples.frustums.starts, - t_ends=ray_samples.frustums.ends, - ) + )[0] + weights = weights[..., None] rgb = self.renderer_rgb( rgb=field_outputs[FieldHeadNames.RGB], @@ -201,13 +218,11 @@ class NGPModel(Model): weights=weights, ray_samples=ray_samples, ray_indices=ray_indices, num_rays=num_rays ) accumulation = self.renderer_accumulation(weights=weights, ray_indices=ray_indices, num_rays=num_rays) - alive_ray_mask = accumulation.squeeze(-1) > 0 outputs = { "rgb": rgb, "accumulation": accumulation, "depth": depth, - "alive_ray_mask": alive_ray_mask, # the rays we kept from sampler "num_samples_per_ray": packed_info[:, 1], } return outputs @@ -221,15 +236,13 @@ class NGPModel(Model): def get_loss_dict(self, outputs, batch, metrics_dict=None): image = batch["image"].to(self.device) - mask = outputs["alive_ray_mask"] - rgb_loss = self.rgb_loss(image[mask], outputs["rgb"][mask]) + rgb_loss = self.rgb_loss(image, outputs["rgb"]) loss_dict = {"rgb_loss": rgb_loss} return loss_dict def get_image_metrics_and_images( self, outputs: Dict[str, torch.Tensor], batch: Dict[str, torch.Tensor] ) -> Tuple[Dict[str, float], Dict[str, torch.Tensor]]: - image = batch["image"].to(self.device) rgb = outputs["rgb"] acc = colormaps.apply_colormap(outputs["accumulation"]) @@ -237,12 +250,10 @@ class NGPModel(Model): outputs["depth"], accumulation=outputs["accumulation"], ) - alive_ray_mask = colormaps.apply_colormap(outputs["alive_ray_mask"]) combined_rgb = torch.cat([image, rgb], dim=1) combined_acc = torch.cat([acc], dim=1) combined_depth = torch.cat([depth], dim=1) - combined_alive_ray_mask = torch.cat([alive_ray_mask], dim=1) # Switch images from [H, W, C] to [1, C, H, W] for metrics computations image = torch.moveaxis(image, -1, 0)[None, ...] @@ -260,7 +271,6 @@ class NGPModel(Model): "img": combined_rgb, "accumulation": combined_acc, "depth": combined_depth, - "alive_ray_mask": combined_alive_ray_mask, } return metrics_dict, images_dict diff --git a/nerfstudio/models/mipnerf.py b/nerfstudio/models/mipnerf.py index 31a337d02..0fae0bdac 100644 --- a/nerfstudio/models/mipnerf.py +++ b/nerfstudio/models/mipnerf.py @@ -96,7 +96,6 @@ class MipNerfModel(Model): return param_groups def get_outputs(self, ray_bundle: RayBundle): - if self.field is None: raise ValueError("populate_fields() must be called before get_outputs") diff --git a/nerfstudio/models/nerfacto.py b/nerfstudio/models/nerfacto.py index 53173640a..0a7583ebe 100644 --- a/nerfstudio/models/nerfacto.py +++ b/nerfstudio/models/nerfacto.py @@ -19,7 +19,7 @@ NeRF implementation that combines many recent advancements. from __future__ import annotations from dataclasses import dataclass, field -from typing import Dict, List, Tuple, Type +from typing import Dict, List, Literal, Tuple, Type import numpy as np import torch @@ -27,7 +27,6 @@ from torch.nn import Parameter from torchmetrics import PeakSignalNoiseRatio from torchmetrics.functional import structural_similarity_index_measure from torchmetrics.image.lpip import LearnedPerceptualImagePatchSimilarity -from typing_extensions import Literal from nerfstudio.cameras.rays import RayBundle from nerfstudio.engine.callbacks import ( diff --git a/nerfstudio/models/nerfplayer_nerfacto.py b/nerfstudio/models/nerfplayer_nerfacto.py index e10b3cad2..245d3bac2 100644 --- a/nerfstudio/models/nerfplayer_nerfacto.py +++ b/nerfstudio/models/nerfplayer_nerfacto.py @@ -20,14 +20,13 @@ from __future__ import annotations import functools from dataclasses import dataclass, field -from typing import Dict, List, Type +from typing import Dict, List, Literal, Type import numpy as np import torch from torchmetrics import PeakSignalNoiseRatio from torchmetrics.functional import structural_similarity_index_measure from torchmetrics.image.lpip import LearnedPerceptualImagePatchSimilarity -from typing_extensions import Literal from nerfstudio.cameras.rays import RayBundle from nerfstudio.field_components.field_heads import FieldHeadNames diff --git a/nerfstudio/models/nerfplayer_ngp.py b/nerfstudio/models/nerfplayer_ngp.py index 09f68d67f..59390d908 100644 --- a/nerfstudio/models/nerfplayer_ngp.py +++ b/nerfstudio/models/nerfplayer_ngp.py @@ -19,19 +19,23 @@ Implementation of NeRFPlayer (https://arxiv.org/abs/2210.15947) with InstantNGP from __future__ import annotations from dataclasses import dataclass, field -from typing import Type +from typing import List, Literal, Type import nerfacc import torch -from nerfacc import ContractionType from torch.nn import Parameter from torchmetrics import PeakSignalNoiseRatio from torchmetrics.functional import structural_similarity_index_measure from torchmetrics.image.lpip import LearnedPerceptualImagePatchSimilarity -from typing_extensions import Literal from nerfstudio.cameras.rays import RayBundle +from nerfstudio.engine.callbacks import ( + TrainingCallback, + TrainingCallbackAttributes, + TrainingCallbackLocation, +) from nerfstudio.field_components.field_heads import FieldHeadNames +from nerfstudio.field_components.spatial_distortions import SceneContraction from nerfstudio.fields.nerfplayer_ngp_field import NerfplayerNGPField from nerfstudio.model_components.losses import MSELoss from nerfstudio.model_components.ray_samplers import VolumetricSampler @@ -80,6 +84,8 @@ class NerfplayerNGPModelConfig(InstantNGPModelConfig): """The training background color that is given to untrained areas.""" disable_viewing_dependent: bool = True """Disable viewing dependent effects.""" + disable_scene_contraction: bool = False + """Whether to disable scene contraction or not.""" class NerfplayerNGPModel(NGPModel): @@ -96,9 +102,13 @@ class NerfplayerNGPModel(NGPModel): """Set the fields and modules.""" Model.populate_modules(self) + if self.config.disable_scene_contraction: + scene_contraction = None + else: + scene_contraction = SceneContraction(order=float("inf")) + self.field = NerfplayerNGPField( aabb=self.scene_box.aabb, - contraction_type=self.config.contraction_type, use_appearance_embedding=self.config.use_appearance_embedding, num_images=self.num_train_data, temporal_dim=self.config.temporal_dim, @@ -107,21 +117,20 @@ class NerfplayerNGPModel(NGPModel): log2_hashmap_size=self.config.log2_hashmap_size, base_resolution=self.config.base_resolution, disable_viewing_dependent=self.config.disable_viewing_dependent, + spatial_distortion=scene_contraction, ) self.scene_aabb = Parameter(self.scene_box.aabb.flatten(), requires_grad=False) # Occupancy Grid - self.occupancy_grid = nerfacc.OccupancyGrid( + self.occupancy_grid = nerfacc.OccGridEstimator( roi_aabb=self.scene_aabb, resolution=self.config.grid_resolution, - contraction_type=self.config.contraction_type, + levels=self.config.grid_levels, ) # Sampler - vol_sampler_aabb = self.scene_box.aabb if self.config.contraction_type == ContractionType.AABB else None self.sampler = VolumetricSampler( - scene_aabb=vol_sampler_aabb, occupancy_grid=self.occupancy_grid, density_fn=self.field.density_fn, ) # need to update the density_fn later during forward (for input time) @@ -140,11 +149,26 @@ class NerfplayerNGPModel(NGPModel): self.lpips = LearnedPerceptualImagePatchSimilarity(normalize=True) self.temporal_distortion = True # for viewer + def get_training_callbacks( + self, training_callback_attributes: TrainingCallbackAttributes + ) -> List[TrainingCallback]: + def update_occupancy_grid(step: int): + self.occupancy_grid.update_every_n_steps( + step=step, + occ_eval_fn=lambda x: self.field.get_opacity(x, self.config.render_step_size), + ) + + return [ + TrainingCallback( + where_to_run=[TrainingCallbackLocation.BEFORE_TRAIN_ITERATION], + update_every_num_iters=1, + func=update_occupancy_grid, + ), + ] + def get_outputs(self, ray_bundle: RayBundle): num_rays = len(ray_bundle) - # update the density_fn of the sampler so that the density is time aware - self.sampler.density_fn = lambda x: self.field.density_fn(x, ray_bundle.times) with torch.no_grad(): ray_samples, ray_indices = self.sampler( ray_bundle=ray_bundle, @@ -152,6 +176,7 @@ class NerfplayerNGPModel(NGPModel): far_plane=self.config.far_plane, render_step_size=self.config.render_step_size, cone_angle=self.config.cone_angle, + alpha_thre=self.config.alpha_thre, ) field_outputs = self.field(ray_samples) @@ -160,10 +185,11 @@ class NerfplayerNGPModel(NGPModel): packed_info = nerfacc.pack_info(ray_indices, num_rays) weights = nerfacc.render_weight_from_density( packed_info=packed_info, - sigmas=field_outputs[FieldHeadNames.DENSITY], - t_starts=ray_samples.frustums.starts, - t_ends=ray_samples.frustums.ends, - ) + sigmas=field_outputs[FieldHeadNames.DENSITY][..., 0], + t_starts=ray_samples.frustums.starts[..., 0], + t_ends=ray_samples.frustums.ends[..., 0], + )[0] + weights = weights[..., None] # update bgcolor in the renderer; usually random color for training and fixed color for inference if self.training: @@ -180,13 +206,11 @@ class NerfplayerNGPModel(NGPModel): weights=weights, ray_samples=ray_samples, ray_indices=ray_indices, num_rays=num_rays ) accumulation = self.renderer_accumulation(weights=weights, ray_indices=ray_indices, num_rays=num_rays) - alive_ray_mask = accumulation.squeeze(-1) > 0 outputs = { "rgb": rgb, "accumulation": accumulation, "depth": depth, - "alive_ray_mask": alive_ray_mask, # the rays we kept from sampler "num_samples_per_ray": packed_info[:, 1], } # adding them to outputs for calculating losses @@ -199,10 +223,9 @@ class NerfplayerNGPModel(NGPModel): def get_loss_dict(self, outputs, batch, metrics_dict=None): image = batch["image"].to(self.device) - mask = outputs["alive_ray_mask"] - rgb_loss = self.rgb_loss(image[mask], outputs["rgb"][mask]) + rgb_loss = self.rgb_loss(image, outputs["rgb"]) loss_dict = {"rgb_loss": rgb_loss} - if "depth_image" in batch.keys() and self.config.depth_weight > 0: + if "depth_image" in batch.keys() and self.config.depth_weight > 0 and self.training: mask = batch["depth_image"] != 0 # First we calculate the depth value, just like most of the papers. loss_dict["depth_loss"] = (outputs["depth"][mask] - batch["depth_image"][mask]).abs().mean() @@ -228,6 +251,6 @@ class NerfplayerNGPModel(NGPModel): density_min_mask = (gt_depth_packed - steps > margin) & (gt_depth_packed != 0) loss_dict["depth_loss"] += (outputs["sigmas"][density_min_mask[..., 0]].pow(2)).mean() * 1e-2 loss_dict["depth_loss"] *= self.config.depth_weight - if self.config.temporal_tv_weight > 0: + if self.config.temporal_tv_weight > 0 and self.training: loss_dict["temporal_tv_loss"] = self.config.temporal_tv_weight * self.field.mlp_base.get_temporal_tv_loss() return loss_dict diff --git a/nerfstudio/models/neus_facto.py b/nerfstudio/models/neus_facto.py new file mode 100644 index 000000000..554f0d90f --- /dev/null +++ b/nerfstudio/models/neus_facto.py @@ -0,0 +1,222 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Implementation of NeuS similar to nerfacto where proposal sampler is used. +Based on SDFStudio https://github.com/autonomousvision/sdfstudio/ +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Tuple, Type + +import numpy as np +import torch +from torch.nn import Parameter + +from nerfstudio.cameras.rays import RayBundle +from nerfstudio.engine.callbacks import ( + TrainingCallback, + TrainingCallbackAttributes, + TrainingCallbackLocation, +) +from nerfstudio.field_components.field_heads import FieldHeadNames +from nerfstudio.fields.density_fields import HashMLPDensityField +from nerfstudio.model_components.losses import interlevel_loss +from nerfstudio.model_components.ray_samplers import ( + ProposalNetworkSampler, + UniformSampler, +) +from nerfstudio.models.neus import NeuSModel, NeuSModelConfig +from nerfstudio.utils import colormaps + + +@dataclass +class NeuSFactoModelConfig(NeuSModelConfig): + """NeusFacto Model Config""" + + _target: Type = field(default_factory=lambda: NeuSFactoModel) + num_proposal_samples_per_ray: Tuple[int, int] = (256, 96) + """Number of samples per ray for the proposal network.""" + num_neus_samples_per_ray: int = 48 + """Number of samples per ray for the nerf network.""" + proposal_update_every: int = 5 + """Sample every n steps after the warmup""" + proposal_warmup: int = 5000 + """Scales n from 1 to proposal_update_every over this many steps""" + num_proposal_iterations: int = 2 + """Number of proposal network iterations.""" + use_same_proposal_network: bool = False + """Use the same proposal network. Otherwise use different ones.""" + proposal_net_args_list: List[Dict] = field( + default_factory=lambda: [ + {"hidden_dim": 16, "log2_hashmap_size": 17, "num_levels": 5, "max_res": 64}, + {"hidden_dim": 16, "log2_hashmap_size": 17, "num_levels": 5, "max_res": 256}, + ] + ) + """Arguments for the proposal density fields.""" + interlevel_loss_mult: float = 1.0 + """Proposal loss multiplier.""" + use_proposal_weight_anneal: bool = True + """Whether to use proposal weight annealing.""" + proposal_weights_anneal_slope: float = 10.0 + """Slope of the annealing function for the proposal weights.""" + proposal_weights_anneal_max_num_iters: int = 1000 + """Max num iterations for the annealing function.""" + use_single_jitter: bool = True + """Whether use single jitter or not for the proposal networks.""" + + +class NeuSFactoModel(NeuSModel): + """NeuSFactoModel extends NeuSModel for a more efficient sampling strategy. + + The model improves the rendering speed and quality by incorporating a learning-based + proposal distribution to guide the sampling process.(similar to mipnerf-360) + + Args: + config: NeuS configuration to instantiate model + """ + + config: NeuSFactoModelConfig + + def populate_modules(self): + """Instantiate modules and fields, including proposal networks.""" + super().populate_modules() + + self.density_fns = [] + num_prop_nets = self.config.num_proposal_iterations + # Build the proposal network(s) + self.proposal_networks = torch.nn.ModuleList() + if self.config.use_same_proposal_network: + assert len(self.config.proposal_net_args_list) == 1, "Only one proposal network is allowed." + prop_net_args = self.config.proposal_net_args_list[0] + network = HashMLPDensityField( + self.scene_box.aabb, spatial_distortion=self.scene_contraction, **prop_net_args + ) + self.proposal_networks.append(network) + self.density_fns.extend([network.density_fn for _ in range(num_prop_nets)]) + else: + for i in range(num_prop_nets): + prop_net_args = self.config.proposal_net_args_list[min(i, len(self.config.proposal_net_args_list) - 1)] + network = HashMLPDensityField( + self.scene_box.aabb, + spatial_distortion=self.scene_contraction, + **prop_net_args, + ) + self.proposal_networks.append(network) + self.density_fns.extend([network.density_fn for network in self.proposal_networks]) + + # update proposal network every iterations + update_schedule = lambda step: -1 + + initial_sampler = UniformSampler(single_jitter=self.config.use_single_jitter) + self.proposal_sampler = ProposalNetworkSampler( + num_nerf_samples_per_ray=self.config.num_neus_samples_per_ray, + num_proposal_samples_per_ray=self.config.num_proposal_samples_per_ray, + num_proposal_network_iterations=self.config.num_proposal_iterations, + single_jitter=self.config.use_single_jitter, + update_sched=update_schedule, + initial_sampler=initial_sampler, + ) + + def get_param_groups(self) -> Dict[str, List[Parameter]]: + """Return a dictionary with the parameters of the proposal networks.""" + param_groups = super().get_param_groups() + param_groups["proposal_networks"] = list(self.proposal_networks.parameters()) + return param_groups + + def get_training_callbacks( + self, training_callback_attributes: TrainingCallbackAttributes + ) -> List[TrainingCallback]: + callbacks = super().get_training_callbacks(training_callback_attributes) + + if self.config.use_proposal_weight_anneal: + # anneal the weights of the proposal network before doing PDF sampling + N = self.config.proposal_weights_anneal_max_num_iters + + def set_anneal(step: int): + # https://arxiv.org/pdf/2111.12077.pdf eq. 18 + train_frac = np.clip(step / N, 0, 1) + bias = lambda x, b: (b * x) / ((b - 1) * x + 1) + anneal = bias(train_frac, self.config.proposal_weights_anneal_slope) + self.proposal_sampler.set_anneal(anneal) + + callbacks.append( + TrainingCallback( + where_to_run=[TrainingCallbackLocation.BEFORE_TRAIN_ITERATION], + update_every_num_iters=1, + func=set_anneal, + ) + ) + callbacks.append( + TrainingCallback( + where_to_run=[TrainingCallbackLocation.AFTER_TRAIN_ITERATION], + update_every_num_iters=1, + func=self.proposal_sampler.step_cb, + ) + ) + + return callbacks + + def sample_and_forward_field(self, ray_bundle: RayBundle) -> Dict[str, Any]: + """Sample rays using proposal networks and compute the corresponding field outputs.""" + ray_samples, weights_list, ray_samples_list = self.proposal_sampler(ray_bundle, density_fns=self.density_fns) + + field_outputs = self.field(ray_samples, return_alphas=True) + weights, transmittance = ray_samples.get_weights_and_transmittance_from_alphas( + field_outputs[FieldHeadNames.ALPHA] + ) + bg_transmittance = transmittance[:, -1, :] + + weights_list.append(weights) + ray_samples_list.append(ray_samples) + + samples_and_field_outputs = { + "ray_samples": ray_samples, + "field_outputs": field_outputs, + "weights": weights, + "bg_transmittance": bg_transmittance, + "weights_list": weights_list, + "ray_samples_list": ray_samples_list, + } + return samples_and_field_outputs + + def get_loss_dict( + self, outputs: Dict[str, Any], batch: Dict[str, Any], metrics_dict: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Compute the loss dictionary, including interlevel loss for proposal networks.""" + loss_dict = super().get_loss_dict(outputs, batch, metrics_dict) + + if self.training: + loss_dict["interlevel_loss"] = self.config.interlevel_loss_mult * interlevel_loss( + outputs["weights_list"], outputs["ray_samples_list"] + ) + + return loss_dict + + def get_image_metrics_and_images( + self, outputs: Dict[str, Any], batch: Dict[str, Any] + ) -> Tuple[Dict[str, float], Dict[str, torch.Tensor]]: + """Compute image metrics and images, including the proposal depth for each iteration.""" + metrics_dict, images_dict = super().get_image_metrics_and_images(outputs, batch) + for i in range(self.config.num_proposal_iterations): + key = f"prop_depth_{i}" + prop_depth_i = colormaps.apply_depth_colormap( + outputs[key], + accumulation=outputs["accumulation"], + ) + images_dict[key] = prop_depth_i + + return metrics_dict, images_dict diff --git a/nerfstudio/models/semantic_nerfw.py b/nerfstudio/models/semantic_nerfw.py index 4324e1eeb..c36b2d21c 100644 --- a/nerfstudio/models/semantic_nerfw.py +++ b/nerfstudio/models/semantic_nerfw.py @@ -258,7 +258,6 @@ class SemanticNerfWModel(Model): def get_image_metrics_and_images( self, outputs: Dict[str, torch.Tensor], batch: Dict[str, torch.Tensor] ) -> Tuple[Dict[str, float], Dict[str, torch.Tensor]]: - image = batch["image"].to(self.device) rgb = outputs["rgb"] rgb = torch.clamp(rgb, min=0, max=1) diff --git a/nerfstudio/models/tensorf.py b/nerfstudio/models/tensorf.py index 3b27354d1..109c117fb 100644 --- a/nerfstudio/models/tensorf.py +++ b/nerfstudio/models/tensorf.py @@ -19,7 +19,7 @@ TensorRF implementation. from __future__ import annotations from dataclasses import dataclass, field -from typing import Dict, List, Tuple, Type +from typing import Dict, List, Literal, Tuple, Type import numpy as np import torch @@ -27,7 +27,6 @@ from torch.nn import Parameter from torchmetrics import PeakSignalNoiseRatio from torchmetrics.functional import structural_similarity_index_measure from torchmetrics.image.lpip import LearnedPerceptualImagePatchSimilarity -from typing_extensions import Literal from nerfstudio.cameras.rays import RayBundle from nerfstudio.configs.config_utils import to_immutable_dict @@ -44,7 +43,7 @@ from nerfstudio.field_components.encodings import ( ) from nerfstudio.field_components.field_heads import FieldHeadNames from nerfstudio.fields.tensorf_field import TensoRFField -from nerfstudio.model_components.losses import MSELoss +from nerfstudio.model_components.losses import MSELoss, tv_loss from nerfstudio.model_components.ray_samplers import PDFSampler, UniformSampler from nerfstudio.model_components.renderers import ( AccumulationRenderer, @@ -68,10 +67,19 @@ class TensoRFModelConfig(ModelConfig): """final render resolution""" upsampling_iters: Tuple[int, ...] = (2000, 3000, 4000, 5500, 7000) """specifies a list of iteration step numbers to perform upsampling""" - loss_coefficients: Dict[str, float] = to_immutable_dict({"rgb_loss": 1.0}) + loss_coefficients: Dict[str, float] = to_immutable_dict( + { + "rgb_loss": 1.0, + "tv_reg_density": 1e-3, + "tv_reg_color": 1e-4, + "l1_reg": 5e-4, + } + ) """Loss specific weights.""" - num_samples: int = 256 + num_samples: int = 50 """Number of samples in field evaluation""" + num_uniform_samples: int = 200 + """Number of samples in density evaluation""" num_den_components: int = 16 """Number of components in density encoding""" num_color_components: int = 48 @@ -79,6 +87,8 @@ class TensoRFModelConfig(ModelConfig): appearance_dim: int = 27 """Number of channels for color encoding""" tensorf_encoding: Literal["triplane", "vm", "cp"] = "vm" + regularization: Literal["none", "l1", "tv"] = "l1" + """Regularization method used in tensorf paper""" class TensoRFModel(Model): @@ -88,6 +98,8 @@ class TensoRFModel(Model): config: TensoRF configuration to instantiate model """ + config: TensoRFModelConfig + def __init__( self, config: TensoRFModelConfig, @@ -116,7 +128,6 @@ class TensoRFModel(Model): def get_training_callbacks( self, training_callback_attributes: TrainingCallbackAttributes ) -> List[TrainingCallback]: - # the callback that we want to run every X iterations after the training iteration def reinitialize_optimizer( self, training_callback_attributes: TrainingCallbackAttributes, step: int # pylint: disable=unused-argument @@ -219,8 +230,8 @@ class TensoRFModel(Model): ) # samplers - self.sampler_uniform = UniformSampler(num_samples=self.config.num_samples, single_jitter=True) - self.sampler_pdf = PDFSampler(num_samples=self.config.num_samples // 2, single_jitter=True) + self.sampler_uniform = UniformSampler(num_samples=self.config.num_uniform_samples, single_jitter=True) + self.sampler_pdf = PDFSampler(num_samples=self.config.num_samples, single_jitter=True, include_original=False) # renderers self.renderer_rgb = RGBRenderer(background_color=colors.WHITE) @@ -239,6 +250,10 @@ class TensoRFModel(Model): if self.config.enable_collider: self.collider = AABBBoxCollider(scene_box=self.scene_box) + # regularizations + if self.config.tensorf_encoding == "cp" and self.config.regularization == "tv": + raise RuntimeError("TV reg not supported for CP decomposition") + def get_param_groups(self) -> Dict[str, List[Parameter]]: param_groups = {} @@ -293,6 +308,20 @@ class TensoRFModel(Model): rgb_loss = self.rgb_loss(image, outputs["rgb"]) loss_dict = {"rgb_loss": rgb_loss} + + if self.config.regularization == "l1": + l1_parameters = [] + for parameter in self.field.density_encoding.parameters(): + l1_parameters.append(parameter.view(-1)) + loss_dict["l1_reg"] = torch.abs(torch.cat(l1_parameters)).mean() + elif self.config.regularization == "tv": + loss_dict["tv_reg_density"] = tv_loss(self.field.density_encoding.plane_coef) + loss_dict["tv_reg_color"] = tv_loss(self.field.color_encoding.plane_coef) + elif self.config.regularization == "none": + pass + else: + raise ValueError(f"Regularization {self.config.regularization} not supported") + loss_dict = misc.scale_dict(loss_dict, self.config.loss_coefficients) return loss_dict @@ -302,6 +331,7 @@ class TensoRFModel(Model): image = batch["image"].to(outputs["rgb"].device) rgb = outputs["rgb"] acc = colormaps.apply_colormap(outputs["accumulation"]) + assert self.config.collider_params is not None depth = colormaps.apply_depth_colormap( outputs["depth"], accumulation=outputs["accumulation"], diff --git a/nerfstudio/models/vanilla_nerf.py b/nerfstudio/models/vanilla_nerf.py index 53e838891..e9c74e93d 100644 --- a/nerfstudio/models/vanilla_nerf.py +++ b/nerfstudio/models/vanilla_nerf.py @@ -135,7 +135,6 @@ class NeRFModel(Model): return param_groups def get_outputs(self, ray_bundle: RayBundle): - if self.field_coarse is None or self.field_fine is None: raise ValueError("populate_fields() must be called before get_outputs") @@ -201,6 +200,7 @@ class NeRFModel(Model): rgb_fine = outputs["rgb_fine"] acc_coarse = colormaps.apply_colormap(outputs["accumulation_coarse"]) acc_fine = colormaps.apply_colormap(outputs["accumulation_fine"]) + assert self.config.collider_params is not None depth_coarse = colormaps.apply_depth_colormap( outputs["depth_coarse"], accumulation=outputs["accumulation_coarse"], diff --git a/nerfstudio/pipelines/base_pipeline.py b/nerfstudio/pipelines/base_pipeline.py index 22405f60c..22a45dfd3 100644 --- a/nerfstudio/pipelines/base_pipeline.py +++ b/nerfstudio/pipelines/base_pipeline.py @@ -21,7 +21,7 @@ import typing from abc import abstractmethod from dataclasses import dataclass, field from time import time -from typing import Any, Dict, List, Mapping, Optional, Type, Union, cast +from typing import Any, Dict, List, Literal, Mapping, Optional, Type, Union, cast import torch import torch.distributed as dist @@ -36,7 +36,6 @@ from torch import nn from torch.cuda.amp.grad_scaler import GradScaler from torch.nn import Parameter from torch.nn.parallel import DistributedDataParallel as DDP -from typing_extensions import Literal from nerfstudio.configs import base_config as cfg from nerfstudio.data.datamanagers.base_datamanager import ( @@ -96,7 +95,11 @@ class Pipeline(nn.Module): datamanager: DataManager _model: Model +<<<<<<< HEAD generative: bool +======= + world_size: int +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb @property def model(self): @@ -109,9 +112,26 @@ class Pipeline(nn.Module): return self.model.device def load_state_dict(self, state_dict: Mapping[str, Any], strict: bool = True): +<<<<<<< HEAD model_state = {key[len("_model.") :]: value for key, value in state_dict.items() if key.startswith("_model.")} +======= + is_ddp_model_state = True + model_state = {} + for key, value in state_dict.items(): + if key.startswith("_model."): + # remove the "_model." prefix from key + model_state[key[len("_model.") :]] = value + # make sure that the "module." prefix comes from DDP, + # rather than an attribute of the model named "module" + if not key.startswith("_model.module."): + is_ddp_model_state = False + # remove "module." prefix added by DDP + if is_ddp_model_state: + model_state = {key[len("module.") :]: value for key, value in model_state.items()} + +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb pipeline_state = {key: value for key, value in state_dict.items() if not key.startswith("_model.")} - self._model.load_state_dict(model_state, strict=strict) + self.model.load_state_dict(model_state, strict=strict) super().load_state_dict(pipeline_state, strict=False) @profiler.time_function @@ -205,6 +225,7 @@ class VanillaPipelineConfig(cfg.InstantiateConfig): class VanillaPipeline(Pipeline): """The pipeline class for the vanilla nerf setup of multiple cameras for one or a few scenes. + Args: config: configuration to instantiate pipeline device: location to place model and data test_mode: @@ -379,7 +400,11 @@ class VanillaPipeline(Pipeline): state = { (key[len("module.") :] if key.startswith("module.") else key): value for key, value in loaded_state.items() } +<<<<<<< HEAD self._model.update_to_step(step) +======= + self.model.update_to_step(step) +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb self.load_state_dict(state, strict=True) def get_training_callbacks( diff --git a/nerfstudio/pipelines/dynamic_batch.py b/nerfstudio/pipelines/dynamic_batch.py index 36536eef2..cddfa7841 100644 --- a/nerfstudio/pipelines/dynamic_batch.py +++ b/nerfstudio/pipelines/dynamic_batch.py @@ -17,11 +17,17 @@ A pipeline that dynamically chooses the number of rays to sample. """ from dataclasses import dataclass, field +<<<<<<< HEAD from typing import Optional, Type import torch from torch.cuda.amp.grad_scaler import GradScaler from typing_extensions import Literal +======= +from typing import Literal, Type + +import torch +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb from nerfstudio.data.datamanagers.base_datamanager import VanillaDataManager from nerfstudio.pipelines.base_pipeline import VanillaPipeline, VanillaPipelineConfig diff --git a/nerfstudio/plugins/registry.py b/nerfstudio/plugins/registry.py index 9e7a61c8d..15eb1779d 100644 --- a/nerfstudio/plugins/registry.py +++ b/nerfstudio/plugins/registry.py @@ -16,6 +16,8 @@ Module that keeps all registered plugins and allows for plugin discovery. """ +import importlib +import os import sys import typing as t @@ -34,6 +36,7 @@ CONSOLE = Console(width=120) def discover_methods() -> t.Tuple[t.Dict[str, TrainerConfig], t.Dict[str, str]]: """ Discovers all methods registered using the `nerfstudio.method_configs` entrypoint. + And also methods in the NERFSTUDIO_METHOD_CONFIGS environment variable. """ methods = {} descriptions = {} @@ -48,4 +51,22 @@ def discover_methods() -> t.Tuple[t.Dict[str, TrainerConfig], t.Dict[str, str]]: specification = t.cast(MethodSpecification, specification) methods[specification.config.method_name] = specification.config descriptions[specification.config.method_name] = specification.description + + if "NERFSTUDIO_METHOD_CONFIGS" in os.environ: + try: + strings = os.environ["NERFSTUDIO_METHOD_CONFIGS"].split(",") + for definition in strings: + if not definition: + continue + name, path = definition.split("=") + CONSOLE.print(f"[bold green]Info: Loading method {name} from environment variable") + module, config_name = path.split(":") + method_config = getattr(importlib.import_module(module), config_name) + assert isinstance(method_config, MethodSpecification) + methods[name] = method_config.config + descriptions[name] = method_config.description + except Exception: # pylint: disable=broad-except + CONSOLE.print_exception() + CONSOLE.print("[bold red]Error: Could not load methods from environment variable NERFSTUDIO_METHOD_CONFIGS") + return methods, descriptions diff --git a/nerfstudio/process_data/base_converter_to_nerfstudio_dataset.py b/nerfstudio/process_data/base_converter_to_nerfstudio_dataset.py new file mode 100644 index 000000000..a1578cc05 --- /dev/null +++ b/nerfstudio/process_data/base_converter_to_nerfstudio_dataset.py @@ -0,0 +1,46 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Base class to process images or video into a nerfstudio dataset +""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class BaseConverterToNerfstudioDataset(ABC): + """Base class to process images or video into a nerfstudio dataset""" + + data: Path + """Path the data, either a video file or a directory of images.""" + output_dir: Path + """Path to the output directory.""" + verbose: bool = False + """If True, print extra logging.""" + + def __post_init__(self) -> None: + self.output_dir.mkdir(parents=True, exist_ok=True) + self.image_dir.mkdir(parents=True, exist_ok=True) + + @property + def image_dir(self) -> Path: # pylint: disable=missing-function-docstring + return self.output_dir / "images" + + @abstractmethod + def main(self) -> None: + """This method implements the conversion logic for each type of data""" + raise NotImplementedError("the main method for conversion needs to be implemented") diff --git a/nerfstudio/process_data/colmap_converter_to_nerfstudio_dataset.py b/nerfstudio/process_data/colmap_converter_to_nerfstudio_dataset.py new file mode 100644 index 000000000..93a123d93 --- /dev/null +++ b/nerfstudio/process_data/colmap_converter_to_nerfstudio_dataset.py @@ -0,0 +1,231 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base class to processes a video or image sequence to a nerfstudio compatible dataset.""" + +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Literal, Optional, Tuple + +from rich.console import Console + +from nerfstudio.process_data import colmap_utils, hloc_utils, process_data_utils +from nerfstudio.process_data.base_converter_to_nerfstudio_dataset import ( + BaseConverterToNerfstudioDataset, +) +from nerfstudio.process_data.process_data_utils import CAMERA_MODELS +from nerfstudio.utils import install_checks + +CONSOLE = Console(width=120) + + +@dataclass +class ColmapConverterToNerfstudioDataset(BaseConverterToNerfstudioDataset): + """Base class to process images or video into a nerfstudio dataset using colmap""" + + camera_type: Literal["perspective", "fisheye", "equirectangular"] = "perspective" + """Camera model to use.""" + matching_method: Literal["exhaustive", "sequential", "vocab_tree"] = "vocab_tree" + """Feature matching method to use. Vocab tree is recommended for a balance of speed + and accuracy. Exhaustive is slower but more accurate. Sequential is faster but + should only be used for videos.""" + sfm_tool: Literal["any", "colmap", "hloc"] = "any" + """Structure from motion tool to use. Colmap will use sift features, hloc can use + many modern methods such as superpoint features and superglue matcher""" + refine_pixsfm: bool = False + """If True, runs refinement using Pixel Perfect SFM. + Only works with hloc sfm_tool""" + feature_type: Literal[ + "any", + "sift", + "superpoint", + "superpoint_aachen", + "superpoint_max", + "superpoint_inloc", + "r2d2", + "d2net-ss", + "sosnet", + "disk", + ] = "any" + """Type of feature to use.""" + matcher_type: Literal[ + "any", + "NN", + "superglue", + "superglue-fast", + "NN-superpoint", + "NN-ratio", + "NN-mutual", + "adalam", + ] = "any" + """Matching algorithm.""" + num_downscales: int = 3 + """Number of times to downscale the images. Downscales by 2 each time. For example a value of 3 will downscale the + images by 2x, 4x, and 8x.""" + skip_colmap: bool = False + """If True, skips COLMAP and generates transforms.json if possible.""" + skip_image_processing: bool = False + """If True, skips copying and downscaling of images and only runs COLMAP if possible and enabled""" + colmap_model_path: Path = Path("colmap/sparse/0") + """Optionally sets the path of the colmap model. Used only when --skip-colmap is set to True. The path is relative + to the output directory. + """ + colmap_cmd: str = "colmap" + """How to call the COLMAP executable.""" + images_per_equirect: Literal[8, 14] = 8 + """Number of samples per image to take from each equirectangular image. + Used only when camera-type is equirectangular. + """ + crop_factor: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0) + """Portion of the image to crop. All values should be in [0,1]. (top, bottom, left, right)""" + crop_bottom: float = 0.0 + """Portion of the image to crop from the bottom. + Can be used instead of `crop-factor 0.0 [num] 0.0 0.0` Should be in [0,1]. + """ + gpu: bool = True + """If True, use GPU.""" + use_sfm_depth: bool = False + """If True, export and use depth maps induced from SfM points.""" + include_depth_debug: bool = False + """If --use-sfm-depth and this flag is True, also export debug images showing Sf overlaid upon input images.""" + + @staticmethod + def default_colmap_path() -> Path: # pylint: disable=missing-function-docstring + return Path("colmap/sparse/0") + + @property + def absolute_colmap_model_path(self) -> Path: # pylint: disable=missing-function-docstring + return self.output_dir / self.colmap_model_path + + @property + def absolute_colmap_path(self) -> Path: # pylint: disable=missing-function-docstring + return self.output_dir / "colmap" + + def _save_transforms( + self, + num_frames: int, + image_id_to_depth_path: Optional[Dict[int, Path]] = None, + camera_mask_path: Optional[Path] = None, + image_rename_map: Optional[Dict[str, str]] = None, + ) -> List[str]: + """Save colmap transforms into the output folder + + Args: + image_id_to_depth_path: When including sfm-based depth, embed these depth file paths in the exported json + image_rename_map: Use these image names instead of the names embedded in the COLMAP db + """ + summary_log = [] + if (self.absolute_colmap_model_path / "cameras.bin").exists(): + with CONSOLE.status("[bold yellow]Saving results to transforms.json", spinner="balloon"): + num_matched_frames = colmap_utils.colmap_to_json( + recon_dir=self.absolute_colmap_model_path, + output_dir=self.output_dir, + image_id_to_depth_path=image_id_to_depth_path, + camera_mask_path=camera_mask_path, + image_rename_map=image_rename_map, + ) + summary_log.append(f"Colmap matched {num_matched_frames} images") + summary_log.append(colmap_utils.get_matching_summary(num_frames, num_matched_frames)) + + else: + CONSOLE.log( + "[bold yellow]Warning: Could not find existing COLMAP results. " "Not generating transforms.json" + ) + return summary_log + + def _export_depth(self) -> Tuple[Optional[Dict[int, Path]], List[str]]: + """If SFM is used for creating depth image, this method will create the depth images from image in + `self.image_dir`. + + Returns: + Depth file paths indexed by COLMAP image id, logs + """ + summary_log = [] + if self.use_sfm_depth: + depth_dir = self.output_dir / "depth" + depth_dir.mkdir(parents=True, exist_ok=True) + image_id_to_depth_path = colmap_utils.create_sfm_depth( + recon_dir=self.output_dir / self.default_colmap_path(), + output_dir=depth_dir, + include_depth_debug=self.include_depth_debug, + input_images_dir=self.image_dir, + verbose=self.verbose, + ) + summary_log.append( + process_data_utils.downscale_images( + depth_dir, + self.num_downscales, + folder_name="depths", + nearest_neighbor=True, + verbose=self.verbose, + ) + ) + return image_id_to_depth_path, summary_log + return None, summary_log + + def _run_colmap(self, mask_path: Optional[Path] = None): + """ + Args: + mask_path: Path to the camera mask. Defaults to None. + """ + self.absolute_colmap_path.mkdir(parents=True, exist_ok=True) + + ( + sfm_tool, + feature_type, + matcher_type, + ) = process_data_utils.find_tool_feature_matcher_combination( + self.sfm_tool, self.feature_type, self.matcher_type + ) + # check that sfm_tool is hloc if using refine_pixsfm + if self.refine_pixsfm: + assert sfm_tool == "hloc", "refine_pixsfm only works with sfm_tool hloc" + + if sfm_tool == "colmap": + colmap_utils.run_colmap( + image_dir=self.image_dir, + colmap_dir=self.absolute_colmap_path, + camera_model=CAMERA_MODELS[self.camera_type], + camera_mask_path=mask_path, + gpu=self.gpu, + verbose=self.verbose, + matching_method=self.matching_method, + colmap_cmd=self.colmap_cmd, + ) + elif sfm_tool == "hloc": + if mask_path is not None: + raise RuntimeError("Cannot use a mask with hloc. Please remove the cropping options " "and try again.") + hloc_utils.run_hloc( + image_dir=self.image_dir, + colmap_dir=self.absolute_colmap_path, + camera_model=CAMERA_MODELS[self.camera_type], + verbose=self.verbose, + matching_method=self.matching_method, + feature_type=feature_type, + matcher_type=matcher_type, + refine_pixsfm=self.refine_pixsfm, + ) + else: + raise RuntimeError("Invalid combination of sfm_tool, feature_type, and matcher_type, " "exiting") + + def __post_init__(self) -> None: + super().__post_init__() + install_checks.check_ffmpeg_installed() + install_checks.check_colmap_installed() + + if self.crop_bottom < 0.0 or self.crop_bottom > 1: + raise RuntimeError("crop_bottom must be set between 0 and 1.") + + if self.crop_bottom > 0.0: + self.crop_factor = (0.0, self.crop_bottom, 0.0, 0.0) diff --git a/nerfstudio/process_data/colmap_utils.py b/nerfstudio/process_data/colmap_utils.py index 859222441..690080771 100644 --- a/nerfstudio/process_data/colmap_utils.py +++ b/nerfstudio/process_data/colmap_utils.py @@ -13,12 +13,12 @@ # limitations under the License. """ -Tools supporting the execution of COLMAP and preparation of COLMAP-based datasets for nerstudio training. +Tools supporting the execution of COLMAP and preparation of COLMAP-based datasets for nerfstudio training. """ import json from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Dict, Literal, Optional import appdirs import cv2 @@ -27,7 +27,6 @@ import requests import torch from rich.console import Console from rich.progress import track -from typing_extensions import Literal # TODO(1480) use pycolmap instead of colmap_parsing_utils # import pycolmap @@ -55,7 +54,7 @@ def get_colmap_version(colmap_cmd: str, default_version=3.8) -> float: Returns: The version of COLMAP. """ - output = run_command(colmap_cmd, verbose=False) + output = run_command(f"{colmap_cmd} -h", verbose=False) assert output is not None for line in output.split("\n"): if line.startswith("COLMAP"): @@ -115,9 +114,7 @@ def run_colmap( colmap_version = get_colmap_version(colmap_cmd) colmap_database_path = colmap_dir / "database.db" - if colmap_database_path.exists(): - # Can't use missing_ok argument because of Python 3.7 compatibility. - colmap_database_path.unlink() + colmap_database_path.unlink(missing_ok=True) # Feature extraction feature_extractor_cmd = [ diff --git a/nerfstudio/process_data/hloc_utils.py b/nerfstudio/process_data/hloc_utils.py index 0aec2376e..90354c3bc 100644 --- a/nerfstudio/process_data/hloc_utils.py +++ b/nerfstudio/process_data/hloc_utils.py @@ -21,9 +21,9 @@ Requires hloc module from : https://github.com/cvg/Hierarchical-Localization import sys from pathlib import Path +from typing import Literal from rich.console import Console -from typing_extensions import Literal from nerfstudio.process_data.process_data_utils import CameraModel @@ -75,6 +75,11 @@ def run_hloc( camera_model: Camera model to use. gpu: If True, use GPU. verbose: If True, logs the output of the command. + matching_method: Method to use for matching images. + feature_type: Type of visual features to use. + matcher_type: Type of feature matcher to use. + num_matched: Number of image pairs for loc. + refine_pixsfm: If True, refine the reconstruction using pixel-perfect-sfm. """ if not _HAS_HLOC: CONSOLE.print( @@ -112,7 +117,17 @@ def run_hloc( camera_model=camera_model.value ) if refine_pixsfm: +<<<<<<< HEAD sfm = PixSfM({"dense_features": {"max_edge": 1024}}) +======= + sfm = PixSfM( + conf={ + "dense_features": {"use_cache": True}, + "KA": {"dense_features": {"use_cache": True}, "max_kps_per_problem": 1000}, + "BA": {"strategy": "costmaps"}, + } + ) +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb refined, _ = sfm.reconstruction( sfm_dir, image_dir, diff --git a/nerfstudio/process_data/images_to_nerfstudio_dataset.py b/nerfstudio/process_data/images_to_nerfstudio_dataset.py new file mode 100644 index 000000000..dc6163816 --- /dev/null +++ b/nerfstudio/process_data/images_to_nerfstudio_dataset.py @@ -0,0 +1,107 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Processes an image sequence to a nerfstudio compatible dataset.""" + +from dataclasses import dataclass +from typing import Optional + +from rich.console import Console + +from nerfstudio.process_data import equirect_utils, process_data_utils +from nerfstudio.process_data.colmap_converter_to_nerfstudio_dataset import ( + ColmapConverterToNerfstudioDataset, +) + +CONSOLE = Console(width=120) + + +@dataclass +class ImagesToNerfstudioDataset(ColmapConverterToNerfstudioDataset): + """Process images into a nerfstudio dataset. + + 1. Scales images to a specified size. + 2. Calculates the camera poses for each image using `COLMAP `_. + """ + + def main(self) -> None: # pylint: disable=R0915 + """Process images into a nerfstudio dataset.""" + + # pylint: disable=too-many-statements + require_cameras_exist = False + if self.colmap_model_path != ColmapConverterToNerfstudioDataset.default_colmap_path(): + if not self.skip_colmap: + raise RuntimeError("The --colmap-model-path can only be used when --skip-colmap is not set.") + if not (self.output_dir / self.colmap_model_path).exists(): + raise RuntimeError(f"The colmap-model-path {self.output_dir / self.colmap_model_path} does not exist.") + require_cameras_exist = True + + image_rename_map: Optional[dict[str, str]] = None + + # Generate planar projections if equirectangular + if self.camera_type == "equirectangular": + pers_size = equirect_utils.compute_resolution_from_equirect(self.data, self.images_per_equirect) + CONSOLE.log(f"Generating {self.images_per_equirect} {pers_size} sized images per equirectangular image") + self.data = equirect_utils.generate_planar_projections_from_equirectangular( + self.data, pers_size, self.images_per_equirect, crop_factor=self.crop_factor + ) + self.camera_type = "perspective" + + summary_log = [] + + # Copy and downscale images + if not self.skip_image_processing: + # Copy images to output directory + image_rename_map_paths = process_data_utils.copy_images( + self.data, image_dir=self.image_dir, crop_factor=self.crop_factor, verbose=self.verbose + ) + image_rename_map = dict((a.name, b.name) for a, b in image_rename_map_paths.items()) + num_frames = len(image_rename_map) + summary_log.append(f"Starting with {num_frames} images") + + # Downscale images + summary_log.append( + process_data_utils.downscale_images(self.image_dir, self.num_downscales, verbose=self.verbose) + ) + else: + num_frames = len(process_data_utils.list_images(self.data)) + if num_frames == 0: + raise RuntimeError("No usable images in the data folder.") + summary_log.append(f"Starting with {num_frames} images") + + # Run COLMAP + if not self.skip_colmap: + require_cameras_exist = True + self._run_colmap() + # Colmap uses renamed images + image_rename_map = None + + # Export depth maps + image_id_to_depth_path, log_tmp = self._export_depth() + summary_log += log_tmp + + if require_cameras_exist and not (self.absolute_colmap_model_path / "cameras.bin").exists(): + raise RuntimeError(f"Could not find existing COLMAP results ({self.colmap_model_path / 'cameras.bin'}).") + + summary_log += self._save_transforms( + num_frames, + image_id_to_depth_path, + None, + image_rename_map, + ) + + CONSOLE.log("[bold green]:tada: :tada: :tada: All DONE :tada: :tada: :tada:") + + for summary in summary_log: + CONSOLE.log(summary) diff --git a/nerfstudio/process_data/insta360_utils.py b/nerfstudio/process_data/insta360_utils.py deleted file mode 100644 index 56e554e01..000000000 --- a/nerfstudio/process_data/insta360_utils.py +++ /dev/null @@ -1,190 +0,0 @@ -# Copyright 2022 The Nerfstudio Team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Helper utils for processing insta360 data.""" - -import sys -from pathlib import Path -from typing import List, Tuple - -from rich.console import Console - -from nerfstudio.process_data.process_data_utils import get_num_frames_in_video -from nerfstudio.utils.rich_utils import status -from nerfstudio.utils.scripts import run_command - -CONSOLE = Console(width=120) - - -def get_insta360_filenames(data: Path) -> Tuple[Path, Path]: - """Returns the filenames of the Insta360 videos from a single video file. - - Example input name: VID_20220212_070353_00_003.insv - - Args: - data: Path to a Insta360 file. - - Returns: - The filenames of the Insta360 videios. - """ - if data.suffix != ".insv": - raise ValueError("The input file must be an .insv file.") - file_parts = data.stem.split("_") - - stem_back = f"VID_{file_parts[1]}_{file_parts[2]}_00_{file_parts[4]}.insv" - stem_front = f"VID_{file_parts[1]}_{file_parts[2]}_10_{file_parts[4]}.insv" - - filename_back = data.parent / stem_back - filename_front = data.parent / stem_front - - return filename_back, filename_front - - -def convert_insta360_to_images( - video_front: Path, - video_back: Path, - image_dir: Path, - num_frames_target: int, - crop_percentage: float = 0.7, - verbose: bool = False, -) -> Tuple[List[str], int]: - """Converts a video into a sequence of images. - - Args: - video_front: Path to the front video. - video_back: Path to the back video. - output_dir: Path to the output directory. - num_frames_target: Number of frames to extract. - crop_percentage: Percentage used to calculate the cropped dimensions of extracted frames. Currently used to crop - out the curved portions of the fish-eye lens. - verbose: If True, logs the output of the command. - Returns: - A tuple containing summary of the conversion and the number of extracted frames. - """ - - with status(msg="Converting video to images...", spinner="bouncingBall", verbose=verbose): - # delete existing images in folder - for img in image_dir.glob("*.png"): - if verbose: - CONSOLE.log(f"Deleting {img}") - img.unlink() - - num_frames_front = get_num_frames_in_video(video_front) - num_frames_back = get_num_frames_in_video(video_back) - if num_frames_front == 0: - CONSOLE.print(f"[bold red]Error: Video has no frames: {video_front}") - sys.exit(1) - if num_frames_back == 0: - CONSOLE.print(f"[bold red]Error: Video has no frames: {video_front}") - sys.exit(1) - - spacing = num_frames_front // (num_frames_target // 2) - vf_cmds = [] - if spacing > 1: - vf_cmds = [f"thumbnail={spacing}", "setpts=N/TB"] - else: - CONSOLE.print("[bold red]Can't satisfy requested number of frames. Extracting all frames.") - - vf_cmds.append(f"crop=iw*{crop_percentage}:ih*{crop_percentage}") - - front_vf_cmds = vf_cmds + ["transpose=2"] - back_vf_cmds = vf_cmds + ["transpose=1"] - - front_ffmpeg_cmd = f"ffmpeg -i {video_front} -vf {','.join(front_vf_cmds)} -r 1 {image_dir / 'frame_%05d.png'}" - back_ffmpeg_cmd = ( - f"ffmpeg -i {video_back} -vf {','.join(back_vf_cmds)} -r 1 {image_dir / 'back_frame_%05d.png'}" - ) - - run_command(front_ffmpeg_cmd, verbose=verbose) - run_command(back_ffmpeg_cmd, verbose=verbose) - - num_extracted_front_frames = len(list(image_dir.glob("frame*.png"))) - for i, img in enumerate(image_dir.glob("back_frame_*.png")): - img.rename(image_dir / f"frame_{i+1+num_extracted_front_frames:05d}.png") - - num_final_frames = len(list(image_dir.glob("*.png"))) - summary_log = [] - summary_log.append(f"Starting with {num_frames_front + num_frames_back} video frames") - summary_log.append(f"We extracted {num_final_frames} images") - CONSOLE.log("[bold green]:tada: Done converting insta360 to images.") - - return summary_log, num_final_frames - - -def convert_insta360_single_file_to_images( - video: Path, - image_dir: Path, - num_frames_target: int, - crop_percentage: float = 0.7, - verbose: bool = False, -) -> Tuple[List[str], int]: - """Converts a video into a sequence of images. - - Args: - video: Path to the video. - output_dir: Path to the output directory. - num_frames_target: Number of frames to extract. - crop_percentage: Percentage used to calculate the cropped dimensions of extracted frames. Currently used to crop - out the curved portions of the fish-eye lens. - verbose: If True, logs the output of the command. - Returns: - A tuple containing summary of the conversion and the number of extracted frames. - """ - - with status(msg="Converting video to images...", spinner="bouncingBall", verbose=verbose): - # delete existing images in folder - for img in image_dir.glob("*.png"): - if verbose: - CONSOLE.log(f"Deleting {img}") - img.unlink() - - num_frames = get_num_frames_in_video(video) - if num_frames == 0: - CONSOLE.print(f"[bold red]Error: Video has no frames: {video}") - sys.exit(1) - - spacing = num_frames // (num_frames_target // 2) - vf_cmds = [] - if spacing > 1: - vf_cmds = [f"thumbnail={spacing}", "setpts=N/TB"] - else: - CONSOLE.print("[bold red]Can't satisfy requested number of frames. Extracting all frames.") - - vf_cmds_back = vf_cmds.copy() - vf_cmds_front = vf_cmds.copy() - - vf_cmds_back.append( - f"crop=ih*{crop_percentage}:ih*{crop_percentage}:ih*{crop_percentage}/4:ih*{crop_percentage}/4" - ) - vf_cmds_front.append( - f"crop=ih*{crop_percentage}:ih*{crop_percentage}:iw/2+ih*{crop_percentage/4}:ih*{crop_percentage/4}" - ) - - front_ffmpeg_cmd = f"ffmpeg -i {video} -vf {','.join(vf_cmds_front)} -r 1 {image_dir / 'frame_%05d.png'}" - back_ffmpeg_cmd = f"ffmpeg -i {video} -vf {','.join(vf_cmds_back)} -r 1 {image_dir / 'back_frame_%05d.png'}" - - run_command(back_ffmpeg_cmd, verbose=verbose) - run_command(front_ffmpeg_cmd, verbose=verbose) - - num_extracted_frames = len(list(image_dir.glob("frame*.png"))) - for i, img in enumerate(image_dir.glob("back_frame_*.png")): - img.rename(image_dir / f"frame_{i+1+num_extracted_frames:05d}.png") - - num_final_frames = len(list(image_dir.glob("*.png"))) - summary_log = [] - summary_log.append(f"Starting with {num_frames} video frames") - summary_log.append(f"We extracted {num_final_frames} images") - CONSOLE.log("[bold green]:tada: Done converting insta360 to images.") - - return summary_log, num_final_frames diff --git a/nerfstudio/process_data/metashape_utils.py b/nerfstudio/process_data/metashape_utils.py index 17d7771e8..4e0b1a680 100644 --- a/nerfstudio/process_data/metashape_utils.py +++ b/nerfstudio/process_data/metashape_utils.py @@ -27,7 +27,7 @@ from nerfstudio.process_data.process_data_utils import CAMERA_MODELS CONSOLE = Console(width=120) -def _find_distortion_param(calib_xml: ET.Element, param_name: str): +def _find_param(calib_xml: ET.Element, param_name: str): param = calib_xml.find(param_name) if param is not None: return float(param.text) # type: ignore @@ -61,35 +61,84 @@ def metashape_to_json( # pylint: disable=too-many-statements if sensors is None: raise ValueError("No sensors found") - calibrated_sensors = [sensor for sensor in sensors if sensor.find("calibration")] - if len(calibrated_sensors) != 1: - raise ValueError("Only one sensor is supported for now") - - sensor = calibrated_sensors[0] - + calibrated_sensors = [ + sensor for sensor in sensors if sensor.get("type") == "spherical" or sensor.find("calibration") + ] + if not calibrated_sensors: + raise ValueError("No calibrated sensor found in Metashape XML") + sensor_type = [s.get("type") for s in calibrated_sensors] + if sensor_type.count(sensor_type[0]) != len(sensor_type): + raise ValueError( + "All Metashape sensors do not have the same sensor type. " + "nerfstudio does not support per-frame camera_model." + ) data = {} + if sensor_type[0] == "frame": + data["camera_model"] = CAMERA_MODELS["perspective"].value + elif sensor_type[0] == "fisheye": + data["camera_model"] = CAMERA_MODELS["fisheye"].value + elif sensor_type[0] == "spherical": + data["camera_model"] = CAMERA_MODELS["equirectangular"].value + else: + # Cylindrical and RPC sensor types are not supported + raise ValueError(f"Unsupported Metashape sensor type '{sensor_type[0]}'") - assert sensor is not None, "Sensor not found in Metashape XML" - resolution = sensor.find("resolution") - assert resolution is not None, "Resolution not found in Metashape xml" - data["w"] = int(resolution.get("width")) # type: ignore - data["h"] = int(resolution.get("height")) # type: ignore + sensor_dict = {} + for sensor in calibrated_sensors: + s = {} + resolution = sensor.find("resolution") + assert resolution is not None, "Resolution not found in Metashape xml" + s["w"] = int(resolution.get("width")) # type: ignore + s["h"] = int(resolution.get("height")) # type: ignore - calib = sensor.find("calibration") - assert calib is not None, "Calibration not found in Metashape xml" - data["fl_x"] = float(calib.find("f").text) # type: ignore - data["fl_y"] = float(calib.find("f").text) # type: ignore - data["cx"] = float(calib.find("cx").text) + data["w"] / 2.0 # type: ignore - data["cy"] = float(calib.find("cy").text) + data["h"] / 2.0 # type: ignore + calib = sensor.find("calibration") + if calib is None: + assert sensor_type[0] == "spherical", "Only spherical sensors should have no intrinsics" + s["fl_x"] = s["w"] / 2.0 + s["fl_y"] = s["h"] + s["cx"] = s["w"] / 2.0 + s["cy"] = s["h"] / 2.0 + else: + f = calib.find("f") + assert f is not None, "Focal length not found in Metashape xml" + s["fl_x"] = s["fl_y"] = float(f.text) # type: ignore + s["cx"] = _find_param(calib, "cx") + s["w"] / 2.0 # type: ignore + s["cy"] = _find_param(calib, "cy") + s["h"] / 2.0 # type: ignore - data["k1"] = _find_distortion_param(calib, "k1") - data["k2"] = _find_distortion_param(calib, "k2") - data["k3"] = _find_distortion_param(calib, "k3") - data["k4"] = _find_distortion_param(calib, "k4") - data["p1"] = _find_distortion_param(calib, "p1") - data["p2"] = _find_distortion_param(calib, "p2") + s["k1"] = _find_param(calib, "k1") + s["k2"] = _find_param(calib, "k2") + s["k3"] = _find_param(calib, "k3") + s["k4"] = _find_param(calib, "k4") + s["p1"] = _find_param(calib, "p1") + s["p2"] = _find_param(calib, "p2") - data["camera_model"] = CAMERA_MODELS["perspective"].value + sensor_dict[sensor.get("id")] = s + + components = chunk.find("components") + component_dict = {} + for component in components: + transform = component.find("transform") + if transform is not None: + rotation = transform.find("rotation") + if rotation is None: + r = np.eye(3) + else: + r = np.array([float(x) for x in rotation.text.split()]).reshape((3, 3)) + translation = transform.find("translation") + if translation is None: + t = np.zeros(3) + else: + t = np.array([float(x) for x in translation.text.split()]) + scale = transform.find("scale") + if scale is None: + s = 1.0 + else: + s = float(scale.text) + + m = np.eye(4) + m[:3, :3] = r + m[:3, 3] = t / s + component_dict[component.get("id")] = m frames = [] cameras = chunk.find("cameras") @@ -97,33 +146,36 @@ def metashape_to_json( # pylint: disable=too-many-statements num_skipped = 0 for camera in cameras: frame = {} - # Labels sometimes have a file extension. We remove it for consistency. - camera_label = camera.get("label").split(".")[0] # type: ignore + camera_label = camera.get("label") if camera_label not in image_filename_map: - continue + # Labels sometimes have a file extension. Try without the extension. + # (maybe it's just a '.' in the image name) + camera_label = camera_label.split(".")[0] # type: ignore + if camera_label not in image_filename_map: + continue frame["file_path"] = image_filename_map[camera_label].as_posix() - if camera.get("sensor_id") != sensor.get("id"): + sensor_id = camera.get("sensor_id") + if sensor_id not in sensor_dict: # this should only happen when we have a sensor that doesn't have calibration if verbose: CONSOLE.print(f"Missing sensor calibration for {camera.get('label')}, Skipping") num_skipped += 1 continue + # Add all sensor parameters to this frame. + frame.update(sensor_dict[sensor_id]) if camera.find("transform") is None: if verbose: CONSOLE.print(f"Missing transforms data for {camera.get('label')}, Skipping") num_skipped += 1 continue - t = [float(x) for x in camera.find("transform").text.split()] # type: ignore - transform = np.array( - [ - [t[8], -t[9], -t[10], t[11]], - [t[0], -t[1], -t[2], t[3]], - [t[4], -t[5], -t[6], t[7]], - [t[12], -t[13], -t[14], t[15]], - ] - ) + transform = np.array([float(x) for x in camera.find("transform").text.split()]).reshape((4, 4)) + component_id = camera.get("component_id") + if component_id in component_dict: + transform = component_dict[component_id] @ transform + transform = transform[[2, 0, 1, 3], :] + transform[:, 1:3] *= -1 frame["transform_matrix"] = transform.tolist() frames.append(frame) diff --git a/nerfstudio/process_data/process_data_utils.py b/nerfstudio/process_data/process_data_utils.py index a73127fcb..b47562ca5 100644 --- a/nerfstudio/process_data/process_data_utils.py +++ b/nerfstudio/process_data/process_data_utils.py @@ -14,17 +14,17 @@ """Helper utils for processing data into the nerfstudio format.""" +import math import os import shutil import sys from enum import Enum from pathlib import Path -from typing import List, Optional, Tuple +from typing import List, Literal, Optional, OrderedDict, Tuple import cv2 import numpy as np from rich.console import Console -from typing_extensions import Literal, OrderedDict from nerfstudio.utils.rich_utils import status from nerfstudio.utils.scripts import run_command @@ -38,12 +38,13 @@ class CameraModel(Enum): OPENCV = "OPENCV" OPENCV_FISHEYE = "OPENCV_FISHEYE" + EQUIRECTANGULAR = "EQUIRECTANGULAR" CAMERA_MODELS = { "perspective": CameraModel.OPENCV, "fisheye": CameraModel.OPENCV_FISHEYE, - "equirectangular": CameraModel.OPENCV, + "equirectangular": CameraModel.EQUIRECTANGULAR, } @@ -141,7 +142,7 @@ def convert_video_to_images( if num_frames == 0: CONSOLE.print(f"[bold red]Error: Video has no frames: {video_path}") sys.exit(1) - print("Number of frames in video:", num_frames) + CONSOLE.print("Number of frames in video:", num_frames) out_filename = image_dir / "frame_%05d.png" ffmpeg_cmd = f'ffmpeg -i "{video_path}"' @@ -157,9 +158,18 @@ def convert_video_to_images( spacing = num_frames // num_frames_target if spacing > 1: ffmpeg_cmd += f" -vf thumbnail={spacing},setpts=N/TB{crop_cmd} -r 1" +<<<<<<< HEAD else: CONSOLE.print("[bold red]Can't satisfy requested number of frames. Extracting all frames.") ffmpeg_cmd += f" -pix_fmt bgr8 -vf {crop_cmd[1:]}" +======= + CONSOLE.print("Number of frames to extract:", math.ceil(num_frames / spacing)) + else: + CONSOLE.print("[bold red]Can't satisfy requested number of frames. Extracting all frames.") + ffmpeg_cmd += " -pix_fmt bgr8" + if crop_cmd != "": + ffmpeg_cmd += f" -vf {crop_cmd[1:]}" +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb ffmpeg_cmd += f" {out_filename}" run_command(ffmpeg_cmd, verbose=verbose) diff --git a/nerfstudio/process_data/realitycapture_utils.py b/nerfstudio/process_data/realitycapture_utils.py index 7bc73e132..918aa84a5 100644 --- a/nerfstudio/process_data/realitycapture_utils.py +++ b/nerfstudio/process_data/realitycapture_utils.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Helper utils for processing polycam data into the nerfstudio format.""" +"""Helper utils for processing reality capture data into the nerfstudio format.""" import csv import json @@ -59,27 +59,21 @@ def realitycapture_to_json( for column, value in row.items(): cameras.setdefault(column, []).append(value) - for name in cameras["#name"]: - camera_label = name.split(".")[0] - if camera_label in image_filename_map: - img = np.array(Image.open(output_dir / image_filename_map[camera_label])) - break - - height, width, _ = img.shape - - data["h"] = int(height) - data["w"] = int(width) - missing_image_data = 0 for i, name in enumerate(cameras["#name"]): - basename = name.split(".")[0] + basename = name.rpartition(".")[0] if basename not in image_filename_map: if verbose: CONSOLE.print(f"Missing image for camera data {basename}, Skipping") missing_image_data += 1 continue + frame = {} + img = np.array(Image.open(output_dir / image_filename_map[basename])) + height, width, _ = img.shape + frame["h"] = int(height) + frame["w"] = int(width) frame["file_path"] = image_filename_map[basename].as_posix() frame["fl_x"] = float(cameras["f"][i]) * max(width, height) / 36 frame["fl_y"] = float(cameras["f"][i]) * max(width, height) / 36 diff --git a/nerfstudio/process_data/video_to_nerfstudio_dataset.py b/nerfstudio/process_data/video_to_nerfstudio_dataset.py new file mode 100644 index 000000000..f8054ff42 --- /dev/null +++ b/nerfstudio/process_data/video_to_nerfstudio_dataset.py @@ -0,0 +1,124 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Processes a video to a nerfstudio compatible dataset.""" + +import shutil +from dataclasses import dataclass + +from rich.console import Console + +from nerfstudio.process_data import equirect_utils, process_data_utils +from nerfstudio.process_data.colmap_converter_to_nerfstudio_dataset import ( + ColmapConverterToNerfstudioDataset, +) + +CONSOLE = Console(width=120) + + +@dataclass +class VideoToNerfstudioDataset(ColmapConverterToNerfstudioDataset): + """Process videos into a nerfstudio dataset. + + This script does the following: + + 1. Converts the video into images. + 2. Scales images to a specified size. + 3. Calculates the camera poses for each image using `COLMAP `_. + """ + + num_frames_target: int = 300 + """Target number of frames to use for the dataset, results may not be exact.""" + percent_radius_crop: float = 1.0 + """Create circle crop mask. The radius is the percent of the image diagonal.""" + + def main(self) -> None: # pylint: disable=R0915 + """Process video into a nerfstudio dataset.""" + + summary_log = [] + # Convert video to images + if self.camera_type == "equirectangular": + # create temp images folder to store the equirect and perspective images + temp_image_dir = self.output_dir / "temp_images" + temp_image_dir.mkdir(parents=True, exist_ok=True) + summary_log, num_extracted_frames = process_data_utils.convert_video_to_images( + self.data, + image_dir=temp_image_dir, + num_frames_target=self.num_frames_target, + crop_factor=(0.0, 0.0, 0.0, 0.0), + verbose=self.verbose, + ) + else: + summary_log, num_extracted_frames = process_data_utils.convert_video_to_images( + self.data, + image_dir=self.image_dir, + num_frames_target=self.num_frames_target, + crop_factor=self.crop_factor, + verbose=self.verbose, + ) + + # Generate planar projections if equirectangular + if self.camera_type == "equirectangular": + perspective_image_size = equirect_utils.compute_resolution_from_equirect( + self.output_dir / "temp_images", self.images_per_equirect + ) + + equirect_utils.generate_planar_projections_from_equirectangular( + self.output_dir / "temp_images", + perspective_image_size, + self.images_per_equirect, + crop_factor=self.crop_factor, + ) + + # copy the perspective images to the image directory + process_data_utils.copy_images( + self.output_dir / "temp_images" / "planar_projections", + image_dir=self.output_dir / "images", + verbose=False, + ) + + # remove the temp_images folder + shutil.rmtree(self.output_dir / "temp_images", ignore_errors=True) + + self.camera_type = "perspective" + + # # Create mask + mask_path = process_data_utils.save_mask( + image_dir=self.image_dir, + num_downscales=self.num_downscales, + crop_factor=(0.0, 0.0, 0.0, 0.0), + percent_radius=self.percent_radius_crop, + ) + if mask_path is not None: + summary_log.append(f"Saved mask to {mask_path}") + + # # Downscale images + summary_log.append( + process_data_utils.downscale_images(self.image_dir, self.num_downscales, verbose=self.verbose) + ) + + # Run Colmap + if not self.skip_colmap: + self._run_colmap(mask_path) + + # Export depth maps + image_id_to_depth_path, log_tmp = self._export_depth() + summary_log += log_tmp + + summary_log += self._save_transforms(num_extracted_frames, image_id_to_depth_path, mask_path) + + CONSOLE.log("[bold green]:tada: :tada: :tada: All DONE :tada: :tada: :tada:") + + for summary in summary_log: + CONSOLE.log(summary) diff --git a/nerfstudio/scripts/__init__.py b/nerfstudio/scripts/__init__.py new file mode 100644 index 000000000..d926cf95c --- /dev/null +++ b/nerfstudio/scripts/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/scripts/benchmarking/launch_eval_blender.sh b/nerfstudio/scripts/benchmarking/launch_eval_blender.sh similarity index 100% rename from scripts/benchmarking/launch_eval_blender.sh rename to nerfstudio/scripts/benchmarking/launch_eval_blender.sh diff --git a/scripts/benchmarking/launch_train_blender.sh b/nerfstudio/scripts/benchmarking/launch_train_blender.sh similarity index 100% rename from scripts/benchmarking/launch_train_blender.sh rename to nerfstudio/scripts/benchmarking/launch_train_blender.sh diff --git a/nerfstudio/scripts/blender/__init__.py b/nerfstudio/scripts/blender/__init__.py new file mode 100644 index 000000000..d926cf95c --- /dev/null +++ b/nerfstudio/scripts/blender/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/scripts/blender/nerfstudio_blender.py b/nerfstudio/scripts/blender/nerfstudio_blender.py similarity index 94% rename from scripts/blender/nerfstudio_blender.py rename to nerfstudio/scripts/blender/nerfstudio_blender.py index dee0b4ab3..6a416e369 100644 --- a/scripts/blender/nerfstudio_blender.py +++ b/nerfstudio/scripts/blender/nerfstudio_blender.py @@ -1,3 +1,17 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ nerfstudio_blender.py """ @@ -141,7 +155,6 @@ class CreateJSONCameraPath(bpy.types.Operator): keyframe_list = [] for i, transformed_camera_path_mat_val in enumerate(self.transformed_camera_path_mat): - curr_properties = ( '[["FOV",' + str(self.fov_list[i]) @@ -361,7 +374,7 @@ class NerfstudioBgPanel(bpy.types.Panel): # pylint: disable=too-few-public-meth self.layout.label(text="Camera path for Nerfstudio") col = self.layout.column() - for (prop_name, _) in INPUT_PROPERTIES: + for prop_name, _ in INPUT_PROPERTIES: row = col.row() row.prop(context.scene, prop_name) @@ -385,7 +398,7 @@ class NerfstudioInputPanel(bpy.types.Panel): # pylint: disable=too-few-public-m self.layout.label(text="Create Blender Camera From Nerfstudio JSON") col = self.layout.column() - for (prop_name, _) in INPUT_PROPERTIES_NS_CAMERA: + for prop_name, _ in INPUT_PROPERTIES_NS_CAMERA: row = col.row() row.prop(context.scene, prop_name) @@ -425,10 +438,10 @@ OBJ_PROPERTIES = ["NeRF", "RenderCamera"] def register(): """Register classes for UI panel.""" - for (prop_name, prop_value) in INPUT_PROPERTIES: + for prop_name, prop_value in INPUT_PROPERTIES: setattr(bpy.types.Scene, prop_name, prop_value) - for (prop_name, prop_value) in INPUT_PROPERTIES_NS_CAMERA: + for prop_name, prop_value in INPUT_PROPERTIES_NS_CAMERA: setattr(bpy.types.Scene, prop_name, prop_value) bpy.types.Scene.NeRF = bpy.props.PointerProperty(type=bpy.types.Object) @@ -440,10 +453,10 @@ def register(): def unregister(): """Unregister classes for UI panel.""" - for (prop_name, _) in INPUT_PROPERTIES: + for prop_name, _ in INPUT_PROPERTIES: delattr(bpy.types.Scene, prop_name) - for (prop_name, _) in INPUT_PROPERTIES_NS_CAMERA: + for prop_name, _ in INPUT_PROPERTIES_NS_CAMERA: delattr(bpy.types.Scene, prop_name) del bpy.types.Scene.NeRF diff --git a/scripts/completions/.gitignore b/nerfstudio/scripts/completions/.gitignore similarity index 100% rename from scripts/completions/.gitignore rename to nerfstudio/scripts/completions/.gitignore diff --git a/nerfstudio/scripts/completions/__init__.py b/nerfstudio/scripts/completions/__init__.py new file mode 100644 index 000000000..d926cf95c --- /dev/null +++ b/nerfstudio/scripts/completions/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/scripts/completions/install.py b/nerfstudio/scripts/completions/install.py similarity index 94% rename from scripts/completions/install.py rename to nerfstudio/scripts/completions/install.py index 98e67fea2..cba1d672b 100644 --- a/scripts/completions/install.py +++ b/nerfstudio/scripts/completions/install.py @@ -1,3 +1,17 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #!/usr/bin/env python """Configuration script for setting up tab completion for nerfstudio in bash and zsh.""" @@ -10,13 +24,13 @@ import shutil import stat import subprocess import sys -from typing import List, Optional, Union +from typing import List, Literal, Optional, Union +from typing import get_args as typing_get_args import tyro from rich.console import Console from rich.prompt import Confirm -from typing_extensions import Literal, assert_never -from typing_extensions import get_args as typing_get_args +from typing_extensions import assert_never if sys.version_info < (3, 10): import importlib_metadata @@ -31,6 +45,7 @@ HEADER_LINE = "# Source nerfstudio autocompletions." def _get_all_entry_points() -> List[str]: + # TODO: we should filter out entrypoints that are not tyro CLIs. entry_points = importlib_metadata.distribution("nerfstudio").entry_points return [x.name for x in entry_points] @@ -399,3 +414,6 @@ def entrypoint(): if __name__ == "__main__": entrypoint() + +# For sphinx docs +get_parser_fn = lambda: tyro.extras.get_parser(main) # noqa diff --git a/scripts/completions/setup.bash b/nerfstudio/scripts/completions/setup.bash similarity index 100% rename from scripts/completions/setup.bash rename to nerfstudio/scripts/completions/setup.bash diff --git a/scripts/completions/setup.zsh b/nerfstudio/scripts/completions/setup.zsh similarity index 100% rename from scripts/completions/setup.zsh rename to nerfstudio/scripts/completions/setup.zsh diff --git a/scripts/datasets/process_nuscenes_masks.py b/nerfstudio/scripts/datasets/process_nuscenes_masks.py similarity index 87% rename from scripts/datasets/process_nuscenes_masks.py rename to nerfstudio/scripts/datasets/process_nuscenes_masks.py index cb9328f95..748a27fd2 100644 --- a/scripts/datasets/process_nuscenes_masks.py +++ b/nerfstudio/scripts/datasets/process_nuscenes_masks.py @@ -1,7 +1,21 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import os from dataclasses import dataclass from pathlib import Path -from typing import Tuple +from typing import Literal, Tuple import cv2 import numpy as np @@ -10,7 +24,6 @@ from nuscenes.nuscenes import NuScenes as NuScenesDatabase from nuscenes.utils.data_classes import Box from nuscenes.utils.geometry_utils import BoxVisibility, view_points from tqdm import tqdm -from typing_extensions import Literal @dataclass @@ -79,8 +92,11 @@ class ProcessNuScenesMasks: # Instead use BoxVisibility.NONE and make sure to rasterize box faces correctly mask = np.ones((900, 1600), dtype=np.uint8) - for box in boxes: + # If is backcam, mask the truck of the ego vehicle + if camera == "CAM_BACK": + mask[-100:] = 0 + for box in boxes: # Dont mask out static objects (static in all frames) instance_token = nusc.get("sample_annotation", box.token)["instance_token"] if not instances_is_dynamic[instance_token]: diff --git a/nerfstudio/scripts/docs/__init__.py b/nerfstudio/scripts/docs/__init__.py new file mode 100644 index 000000000..d926cf95c --- /dev/null +++ b/nerfstudio/scripts/docs/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/scripts/docs/add_nb_tags.py b/nerfstudio/scripts/docs/add_nb_tags.py similarity index 78% rename from scripts/docs/add_nb_tags.py rename to nerfstudio/scripts/docs/add_nb_tags.py index 4a7d97fe0..a110e0d76 100644 --- a/scripts/docs/add_nb_tags.py +++ b/nerfstudio/scripts/docs/add_nb_tags.py @@ -1,3 +1,17 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """Helper that add tags to notebooks based on cell comments.""" import sys @@ -60,7 +74,8 @@ def main(check: bool = False): if incorrect_metadata: if check: CONSOLE.print( - f"[bold yellow]{ipath} has incorrect metadata. Call `python scripts.docs.add_nb_tags.py` to add it." + f"[bold yellow]{ipath} has incorrect metadata. " + "Call `python nerfstudio.scripts.docs.add_nb_tags.py` to add it." ) any_missing = True else: diff --git a/scripts/docs/build_docs.py b/nerfstudio/scripts/docs/build_docs.py similarity index 66% rename from scripts/docs/build_docs.py rename to nerfstudio/scripts/docs/build_docs.py index cb31e54b3..1959220a1 100644 --- a/scripts/docs/build_docs.py +++ b/nerfstudio/scripts/docs/build_docs.py @@ -1,3 +1,17 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #!/usr/bin/env python """Simple yaml debugger""" import subprocess @@ -32,7 +46,7 @@ def main(clean_cache: bool = False): """ CONSOLE.print("[green]Adding notebook documentation metadata") - run_command("python scripts/docs/add_nb_tags.py") + run_command("python nerfstudio/scripts/docs/add_nb_tags.py") # Add checks for building documentation CONSOLE.print("[green]Building Documentation") diff --git a/nerfstudio/scripts/downloads/__init__.py b/nerfstudio/scripts/downloads/__init__.py new file mode 100644 index 000000000..d926cf95c --- /dev/null +++ b/nerfstudio/scripts/downloads/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/scripts/downloads/download_data.py b/nerfstudio/scripts/downloads/download_data.py similarity index 81% rename from scripts/downloads/download_data.py rename to nerfstudio/scripts/downloads/download_data.py index 3339b50bc..d97dd659b 100644 --- a/scripts/downloads/download_data.py +++ b/nerfstudio/scripts/downloads/download_data.py @@ -1,3 +1,17 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """Download datasets and specific captures from the datasets.""" from __future__ import annotations @@ -359,6 +373,65 @@ class SDFstudioDemoDownload(DatasetDownload): os.remove(download_path) +# pylint: disable=line-too-long +nerfosr_downloads = { + "europa": "https://nextcloud.mpi-klsb.mpg.de/index.php/s/mGXYKpD8raQ8nMk/download?path=%2FData&files=europa&downloadStartSecret=0k2r95c1fdej", + "lk2": "https://nextcloud.mpi-klsb.mpg.de/index.php/s/mGXYKpD8raQ8nMk/download?path=%2FData&files=lk2&downloadStartSecret=w8kuvjzmchc", + "lwp": "https://nextcloud.mpi-klsb.mpg.de/index.php/s/mGXYKpD8raQ8nMk/download?path=%2FData&files=lwp&downloadStartSecret=gtnc4vmkcjq", + "rathaus": "https://nextcloud.mpi-klsb.mpg.de/index.php/s/mGXYKpD8raQ8nMk/download?path=%2FData&files=rathaus&downloadStartSecret=7372aewy6rr", + "schloss": "https://nextcloud.mpi-klsb.mpg.de/index.php/s/mGXYKpD8raQ8nMk/download?path=%2FData&files=schloss&downloadStartSecret=y8t00nqx0h", + "st": "https://nextcloud.mpi-klsb.mpg.de/index.php/s/mGXYKpD8raQ8nMk/download?path=%2FData&files=st&downloadStartSecret=kl9ptuxe8v", + "stjacob": "https://nextcloud.mpi-klsb.mpg.de/index.php/s/mGXYKpD8raQ8nMk/download?path=%2FData&files=stjacob&downloadStartSecret=sntsim6ebvm", + "stjohann": "https://nextcloud.mpi-klsb.mpg.de/index.php/s/mGXYKpD8raQ8nMk/download?path=%2FData&files=stjohann&downloadStartSecret=g80ug1fsbmh", + "trevi": "https://nextcloud.mpi-klsb.mpg.de/index.php/s/mGXYKpD8raQ8nMk/download?path=%2FData&files=trevi&downloadStartSecret=ot1483bigjm", + "all": None, +} + + +NeRFOSRCaptureName = tyro.extras.literal_type_from_choices(nerfosr_downloads.keys()) + + +@dataclass +class NeRFOSRDownload(DatasetDownload): + """Download the NeRF-OSR dataset.""" + + capture_name: NeRFOSRCaptureName = "europa" + + def download(self, save_dir: Path): + """Download the NeRF-OSR dataset: https://nextcloud.mpi-klsb.mpg.de/index.php/s/mGXYKpD8raQ8nMk""" + + if self.capture_name == "all": + for capture_name in nerfosr_downloads: + if capture_name != "all": + NeRFOSRDownload(capture_name=capture_name).download(save_dir) + return + + assert ( + self.capture_name in nerfosr_downloads + ), f"Capture name {self.capture_name} not found in {nerfosr_downloads.keys()}" + url = nerfosr_downloads[self.capture_name] + target_path = str(save_dir / f"NeRF-OSR/Data/{self.capture_name}") + os.makedirs(target_path, exist_ok=True) + download_path = Path(f"{target_path}.zip") + tmp_path = str(save_dir / ".temp") + shutil.rmtree(tmp_path, ignore_errors=True) + os.makedirs(tmp_path, exist_ok=True) + + os.system(f"curl -L '{url}' > {download_path}") + + # Extract the zip file + with zipfile.ZipFile(download_path, "r") as zip_ref: + zip_ref.extractall(tmp_path) + + inner_folders = os.listdir(tmp_path) + assert len(inner_folders) == 1, "There is more than one folder inside this zip file." + folder = os.path.join(tmp_path, inner_folders[0]) + shutil.rmtree(target_path) + shutil.move(folder, target_path) + shutil.rmtree(tmp_path) + os.remove(download_path) + + Commands = Union[ Annotated[BlenderDownload, tyro.conf.subcommand(name="blender")], Annotated[Sitcoms3DDownload, tyro.conf.subcommand(name="sitcoms3d")], @@ -367,6 +440,7 @@ Commands = Union[ Annotated[DNerfDownload, tyro.conf.subcommand(name="dnerf")], Annotated[PhototourismDownload, tyro.conf.subcommand(name="phototourism")], Annotated[SDFstudioDemoDownload, tyro.conf.subcommand(name="sdfstudio")], + Annotated[NeRFOSRDownload, tyro.conf.subcommand(name="nerfosr")], ] diff --git a/scripts/eval.py b/nerfstudio/scripts/eval.py similarity index 68% rename from scripts/eval.py rename to nerfstudio/scripts/eval.py index e9ba12f78..517da3179 100644 --- a/scripts/eval.py +++ b/nerfstudio/scripts/eval.py @@ -1,3 +1,17 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #!/usr/bin/env python """ eval.py @@ -27,7 +41,7 @@ class ComputePSNR: def main(self) -> None: """Main function.""" - config, pipeline, checkpoint_path = eval_setup(self.load_config) + config, pipeline, checkpoint_path, _ = eval_setup(self.load_config) assert self.output_path.suffix == ".json" metrics_dict = pipeline.get_average_eval_image_metrics() self.output_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/scripts/exporter.py b/nerfstudio/scripts/exporter.py similarity index 79% rename from scripts/exporter.py rename to nerfstudio/scripts/exporter.py index cd18aa354..dfca20637 100644 --- a/scripts/exporter.py +++ b/nerfstudio/scripts/exporter.py @@ -1,3 +1,17 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ Script for exporting NeRF into other formats. """ @@ -27,6 +41,9 @@ from nerfstudio.exporter.exporter_utils import ( generate_point_cloud, get_mesh_from_filename, ) +from nerfstudio.exporter.marching_cubes import ( + generate_mesh_with_multires_marching_cubes, +) from nerfstudio.pipelines.base_pipeline import Pipeline, VanillaPipeline from nerfstudio.utils.eval_utils import eval_setup @@ -74,7 +91,7 @@ class ExportPointCloud(Exporter): if not self.output_dir.exists(): self.output_dir.mkdir(parents=True) - _, pipeline, _ = eval_setup(self.load_config) + _, pipeline, _, _ = eval_setup(self.load_config) # Increase the batchsize to speed up the evaluation. pipeline.datamanager.train_pixel_sampler.num_rays_per_batch = self.num_rays_per_batch @@ -144,7 +161,7 @@ class ExportTSDFMesh(Exporter): if not self.output_dir.exists(): self.output_dir.mkdir(parents=True) - _, pipeline, _ = eval_setup(self.load_config) + _, pipeline, _, _ = eval_setup(self.load_config) tsdf_utils.export_tsdf_mesh( pipeline, @@ -250,7 +267,7 @@ class ExportPoissonMesh(Exporter): if not self.output_dir.exists(): self.output_dir.mkdir(parents=True) - _, pipeline, _ = eval_setup(self.load_config) + _, pipeline, _, _ = eval_setup(self.load_config) self.validate_pipeline(pipeline) # Increase the batchsize to speed up the evaluation. @@ -313,14 +330,68 @@ class ExportPoissonMesh(Exporter): @dataclass class ExportMarchingCubesMesh(Exporter): - """ - NOT YET IMPLEMENTED - Export a mesh using marching cubes. - """ + """Export a mesh using marching cubes.""" + + isosurface_threshold: float = 0.0 + """The isosurface threshold for extraction. For SDF based methods the surface is the zero level set.""" + resolution: int = 1024 + """Marching cube resolution.""" + simplify_mesh: bool = False + """Whether to simplify the mesh.""" + bounding_box_min: Tuple[float, float, float] = (-1.0, -1.0, -1.0) + """Minimum of the bounding box.""" + bounding_box_max: Tuple[float, float, float] = (1.0, 1.0, 1.0) + """Maximum of the bounding box.""" + px_per_uv_triangle: int = 4 + """Number of pixels per UV triangle.""" + unwrap_method: Literal["xatlas", "custom"] = "xatlas" + """The method to use for unwrapping the mesh.""" + num_pixels_per_side: int = 2048 + """If using xatlas for unwrapping, the pixels per side of the texture image.""" + target_num_faces: Optional[int] = 50000 + """Target number of faces for the mesh to texture.""" def main(self) -> None: - """Export mesh""" - raise NotImplementedError("Marching cubes not implemented yet.") + """Main function.""" + if not self.output_dir.exists(): + self.output_dir.mkdir(parents=True) + + _, pipeline, _, _ = eval_setup(self.load_config) + + # TODO: Make this work with Density Field + assert hasattr(pipeline.model.config, "sdf_field"), "Model must have an SDF field." + + CONSOLE.print("Extracting mesh with marching cubes... which may take a while") + + assert ( + self.resolution % 512 == 0 + ), f"""resolution must be divisible by 512, got {self.resolution}. + This is important because the algorithm uses a multi-resolution approach + to evaluate the SDF where the mimimum resolution is 512.""" + + # Extract mesh using marching cubes for sdf at a multi-scale resolution. + multi_res_mesh = generate_mesh_with_multires_marching_cubes( + geometry_callable_field=lambda x: pipeline.model.field.forward_geonetwork(x)[:, 0].contiguous(), + resolution=self.resolution, + bounding_box_min=self.bounding_box_min, + bounding_box_max=self.bounding_box_max, + isosurface_threshold=self.isosurface_threshold, + coarse_mask=None, + ) + filename = self.output_dir / "sdf_marching_cubes_mesh.ply" + multi_res_mesh.export(filename) + + # load the mesh from the marching cubes export + mesh = get_mesh_from_filename(str(filename), target_num_faces=self.target_num_faces) + CONSOLE.print("Texturing mesh with NeRF...") + texture_utils.export_textured_mesh( + mesh, + pipeline, + self.output_dir, + px_per_uv_triangle=self.px_per_uv_triangle if self.unwrap_method == "custom" else None, + unwrap_method=self.unwrap_method, + num_pixels_per_side=self.num_pixels_per_side, + ) @dataclass @@ -334,7 +405,7 @@ class ExportCameraPoses(Exporter): if not self.output_dir.exists(): self.output_dir.mkdir(parents=True) - _, pipeline, _ = eval_setup(self.load_config) + _, pipeline, _, _ = eval_setup(self.load_config) assert isinstance(pipeline, VanillaPipeline) train_frames, eval_frames = collect_camera_poses(pipeline) diff --git a/nerfstudio/scripts/generative/__init__.py b/nerfstudio/scripts/generative/__init__.py new file mode 100644 index 000000000..d926cf95c --- /dev/null +++ b/nerfstudio/scripts/generative/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/scripts/generative/trace_stable_diffusion.py b/nerfstudio/scripts/generative/trace_stable_diffusion.py similarity index 84% rename from scripts/generative/trace_stable_diffusion.py rename to nerfstudio/scripts/generative/trace_stable_diffusion.py index eee069eb3..3a426cb2c 100644 --- a/scripts/generative/trace_stable_diffusion.py +++ b/nerfstudio/scripts/generative/trace_stable_diffusion.py @@ -1,3 +1,17 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """Trace Stable Diffusion UNet for speed improvement""" import functools diff --git a/nerfstudio/scripts/github/__init__.py b/nerfstudio/scripts/github/__init__.py new file mode 100644 index 000000000..d926cf95c --- /dev/null +++ b/nerfstudio/scripts/github/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/scripts/github/run_actions.py b/nerfstudio/scripts/github/run_actions.py similarity index 81% rename from scripts/github/run_actions.py rename to nerfstudio/scripts/github/run_actions.py index 19179725c..8f5bb7ce1 100644 --- a/scripts/github/run_actions.py +++ b/nerfstudio/scripts/github/run_actions.py @@ -1,3 +1,17 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #!/usr/bin/env python """Simple yaml debugger""" import subprocess @@ -57,7 +71,9 @@ def run_github_actions_file(filename: str, continue_on_fail: bool = False): # Add checks for building documentation CONSOLE.line() CONSOLE.rule("[bold green]Adding notebook documentation metadata") - success = success and run_command("python scripts/docs/add_nb_tags.py", continue_on_fail=continue_on_fail) + success = success and run_command( + "python nerfstudio/scripts/docs/add_nb_tags.py", continue_on_fail=continue_on_fail + ) CONSOLE.line() CONSOLE.rule("[bold green]Building Documentation") success = success and run_command("cd docs/; make html SPHINXOPTS='-W;'", continue_on_fail=continue_on_fail) diff --git a/scripts/licensing/copyright.txt b/nerfstudio/scripts/licensing/copyright.txt similarity index 100% rename from scripts/licensing/copyright.txt rename to nerfstudio/scripts/licensing/copyright.txt diff --git a/scripts/licensing/license_headers.sh b/nerfstudio/scripts/licensing/license_headers.sh similarity index 80% rename from scripts/licensing/license_headers.sh rename to nerfstudio/scripts/licensing/license_headers.sh index 36482085c..286d7ffb1 100755 --- a/scripts/licensing/license_headers.sh +++ b/nerfstudio/scripts/licensing/license_headers.sh @@ -27,7 +27,7 @@ do echo "$i missing copyright header" check_failed=true else - cat scripts/licensing/copyright.txt $i >$i.new && mv $i.new $i + cat nerfstudio/scripts/licensing/copyright.txt $i >$i.new && mv $i.new $i echo "Adding license header to $i." fi added_headers=true @@ -36,7 +36,7 @@ done if [ "$check_failed" = true ]; then - echo "Run '.scripts/licensing/license_headers.sh to add missing headers.'" + echo "Run '.nerfstudio/scripts/licensing/license_headers.sh to add missing headers.'" exit 1 fi diff --git a/nerfstudio/scripts/process_data.py b/nerfstudio/scripts/process_data.py new file mode 100644 index 000000000..4c2f3829b --- /dev/null +++ b/nerfstudio/scripts/process_data.py @@ -0,0 +1,419 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/usr/bin/env python +"""Processes a video or image sequence to a nerfstudio compatible dataset.""" + + +import sys +import zipfile +from dataclasses import dataclass +from pathlib import Path +from typing import Union + +import numpy as np +import tyro +from rich.console import Console +from typing_extensions import Annotated + +from nerfstudio.process_data import ( + metashape_utils, + polycam_utils, + process_data_utils, + realitycapture_utils, + record3d_utils, +) +from nerfstudio.process_data.colmap_converter_to_nerfstudio_dataset import ( + BaseConverterToNerfstudioDataset, +) +from nerfstudio.process_data.images_to_nerfstudio_dataset import ( + ImagesToNerfstudioDataset, +) +from nerfstudio.process_data.video_to_nerfstudio_dataset import VideoToNerfstudioDataset + +CONSOLE = Console(width=120) + + +@dataclass +class ProcessRecord3D(BaseConverterToNerfstudioDataset): + """Process Record3D data into a nerfstudio dataset. + + This script does the following: + + 1. Scales images to a specified size. + 2. Converts Record3D poses into the nerfstudio format. + """ + + num_downscales: int = 3 + """Number of times to downscale the images. Downscales by 2 each time. For example a value of 3 + will downscale the images by 2x, 4x, and 8x.""" + max_dataset_size: int = 300 + """Max number of images to train on. If the dataset has more, images will be sampled approximately evenly. If -1, + use all images.""" + + def main(self) -> None: + """Process images into a nerfstudio dataset.""" + + self.output_dir.mkdir(parents=True, exist_ok=True) + image_dir = self.output_dir / "images" + image_dir.mkdir(parents=True, exist_ok=True) + + summary_log = [] + + record3d_image_dir = self.data / "rgb" + + if not record3d_image_dir.exists(): + raise ValueError(f"Image directory {record3d_image_dir} doesn't exist") + + record3d_image_filenames = [] + for f in record3d_image_dir.iterdir(): + if f.stem.isdigit(): # removes possible duplicate images (for example, 123(3).jpg) + if f.suffix.lower() in [".jpg", ".jpeg", ".png", ".tif", ".tiff"]: + record3d_image_filenames.append(f) + + record3d_image_filenames = sorted(record3d_image_filenames, key=lambda fn: int(fn.stem)) + num_images = len(record3d_image_filenames) + idx = np.arange(num_images) + if self.max_dataset_size != -1 and num_images > self.max_dataset_size: + idx = np.round(np.linspace(0, num_images - 1, self.max_dataset_size)).astype(int) + + record3d_image_filenames = list(np.array(record3d_image_filenames)[idx]) + # Copy images to output directory + copied_image_paths = process_data_utils.copy_images_list( + record3d_image_filenames, image_dir=image_dir, verbose=self.verbose + ) + num_frames = len(copied_image_paths) + + copied_image_paths = [Path("images/" + copied_image_path.name) for copied_image_path in copied_image_paths] + summary_log.append(f"Used {num_frames} images out of {num_images} total") + if self.max_dataset_size > 0: + summary_log.append( + "To change the size of the dataset add the argument [yellow]--max_dataset_size[/yellow] to " + f"larger than the current value ({self.max_dataset_size}), or -1 to use all images." + ) + + # Downscale images + summary_log.append(process_data_utils.downscale_images(image_dir, self.num_downscales, verbose=self.verbose)) + + metadata_path = self.data / "metadata.json" + record3d_utils.record3d_to_json(copied_image_paths, metadata_path, self.output_dir, indices=idx) + CONSOLE.rule("[bold green]:tada: :tada: :tada: All DONE :tada: :tada: :tada:") + + for summary in summary_log: + CONSOLE.print(summary, justify="center") + CONSOLE.rule() + + +@dataclass +class ProcessPolycam(BaseConverterToNerfstudioDataset): + """Process Polycam data into a nerfstudio dataset. + + To capture data, use the Polycam app on an iPhone or iPad with LiDAR. The capture must be in LiDAR or ROOM mode. + Developer mode must be enabled in the app settings, this will enable a raw data export option in the export menus. + The exported data folder is used as the input to this script. + + This script does the following: + + 1. Scales images to a specified size. + 2. Converts Polycam poses into the nerfstudio format. + """ + + num_downscales: int = 3 + """Number of times to downscale the images. Downscales by 2 each time. For example a value of 3 + will downscale the images by 2x, 4x, and 8x.""" + use_uncorrected_images: bool = False + """If True, use the raw images from the polycam export. If False, use the corrected images.""" + max_dataset_size: int = 600 + """Max number of images to train on. If the dataset has more, images will be sampled approximately evenly. If -1, + use all images.""" + min_blur_score: float = 25 + """Minimum blur score to use an image. If the blur score is below this value, the image will be skipped.""" + crop_border_pixels: int = 15 + """Number of pixels to crop from each border of the image. Useful as borders may be black due to undistortion.""" + use_depth: bool = False + """If True, processes the generated depth maps from Polycam""" + + def main(self) -> None: + """Process images into a nerfstudio dataset.""" + + self.output_dir.mkdir(parents=True, exist_ok=True) + image_dir = self.output_dir / "images" + image_dir.mkdir(parents=True, exist_ok=True) + + summary_log = [] + + if self.data.suffix == ".zip": + with zipfile.ZipFile(self.data, "r") as zip_ref: + zip_ref.extractall(self.output_dir) + extracted_folder = zip_ref.namelist()[0].split("/")[0] + self.data = self.output_dir / extracted_folder + + if (self.data / "keyframes" / "corrected_images").exists() and not self.use_uncorrected_images: + polycam_image_dir = self.data / "keyframes" / "corrected_images" + polycam_cameras_dir = self.data / "keyframes" / "corrected_cameras" + else: + polycam_image_dir = self.data / "keyframes" / "images" + polycam_cameras_dir = self.data / "keyframes" / "cameras" + if not self.use_uncorrected_images: + CONSOLE.print("[bold yellow]Corrected images not found, using raw images.") + + if not polycam_image_dir.exists(): + raise ValueError(f"Image directory {polycam_image_dir} doesn't exist") + + if not (self.data / "keyframes" / "depth").exists(): + depth_dir = self.data / "keyframes" / "depth" + raise ValueError(f"Depth map directory {depth_dir} doesn't exist") + + (image_processing_log, polycam_image_filenames) = polycam_utils.process_images( + polycam_image_dir, + image_dir, + crop_border_pixels=self.crop_border_pixels, + max_dataset_size=self.max_dataset_size, + num_downscales=self.num_downscales, + verbose=self.verbose, + ) + + summary_log.extend(image_processing_log) + + polycam_depth_filenames = [] + if self.use_depth: + polycam_depth_image_dir = self.data / "keyframes" / "depth" + depth_dir = self.output_dir / "depth" + depth_dir.mkdir(parents=True, exist_ok=True) + (depth_processing_log, polycam_depth_filenames) = polycam_utils.process_depth_maps( + polycam_depth_image_dir, + depth_dir, + num_processed_images=len(polycam_image_filenames), + crop_border_pixels=self.crop_border_pixels, + max_dataset_size=self.max_dataset_size, + num_downscales=self.num_downscales, + verbose=self.verbose, + ) + summary_log.extend(depth_processing_log) + + summary_log.extend( + polycam_utils.polycam_to_json( + image_filenames=polycam_image_filenames, + depth_filenames=polycam_depth_filenames, + cameras_dir=polycam_cameras_dir, + output_dir=self.output_dir, + min_blur_score=self.min_blur_score, + crop_border_pixels=self.crop_border_pixels, + ) + ) + + CONSOLE.rule("[bold green]:tada: :tada: :tada: All DONE :tada: :tada: :tada:") + + for summary in summary_log: + CONSOLE.print(summary, justify="center") + CONSOLE.rule() + + +@dataclass +class _NoDefaultProcessMetashape: + """Private class to order the parameters of ProcessMetashape in the right order for default values.""" + + xml: Path + """Path to the Metashape xml file.""" + + +@dataclass +class ProcessMetashape(BaseConverterToNerfstudioDataset, _NoDefaultProcessMetashape): + """Process Metashape data into a nerfstudio dataset. + + This script assumes that cameras have been aligned using Metashape. After alignment, it is necessary to export the + camera poses as a `.xml` file. This option can be found under `File > Export > Export Cameras`. + + This script does the following: + + 1. Scales images to a specified size. + 2. Converts Metashape poses into the nerfstudio format. + """ + + num_downscales: int = 3 + """Number of times to downscale the images. Downscales by 2 each time. For example a value of 3 + will downscale the images by 2x, 4x, and 8x.""" + max_dataset_size: int = 600 + """Max number of images to train on. If the dataset has more, images will be sampled approximately evenly. If -1, + use all images.""" + + def main(self) -> None: + """Process images into a nerfstudio dataset.""" + + if self.xml.suffix != ".xml": + raise ValueError(f"XML file {self.xml} must have a .xml extension") + if not self.xml.exists: + raise ValueError(f"XML file {self.xml} doesn't exist") + + self.output_dir.mkdir(parents=True, exist_ok=True) + image_dir = self.output_dir / "images" + image_dir.mkdir(parents=True, exist_ok=True) + + summary_log = [] + + # Copy images to output directory + image_filenames, num_orig_images = process_data_utils.get_image_filenames(self.data, self.max_dataset_size) + copied_image_paths = process_data_utils.copy_images_list( + image_filenames, + image_dir=image_dir, + verbose=self.verbose, + ) + num_frames = len(copied_image_paths) + + copied_image_paths = [Path("images/" + copied_image_path.name) for copied_image_path in copied_image_paths] + original_names = [image_path.stem for image_path in image_filenames] + image_filename_map = dict(zip(original_names, copied_image_paths)) + + if self.max_dataset_size > 0 and num_frames != num_orig_images: + summary_log.append(f"Started with {num_frames} images out of {num_orig_images} total") + summary_log.append( + "To change the size of the dataset add the argument [yellow]--max_dataset_size[/yellow] to " + f"larger than the current value ({self.max_dataset_size}), or -1 to use all images." + ) + else: + summary_log.append(f"Started with {num_frames} images") + + # Downscale images + summary_log.append(process_data_utils.downscale_images(image_dir, self.num_downscales, verbose=self.verbose)) + + # Save json + if num_frames == 0: + CONSOLE.print("[bold red]No images found, exiting") + sys.exit(1) + summary_log.extend( + metashape_utils.metashape_to_json( + image_filename_map=image_filename_map, + xml_filename=self.xml, + output_dir=self.output_dir, + verbose=self.verbose, + ) + ) + + CONSOLE.rule("[bold green]:tada: :tada: :tada: All DONE :tada: :tada: :tada:") + + for summary in summary_log: + CONSOLE.print(summary, justify="center") + CONSOLE.rule() + + +@dataclass +class _NoDefaultProcessRealityCapture: + """Private class to order the parameters of ProcessRealityCapture in the right order for default values.""" + + csv: Path + """Path to the RealityCapture cameras CSV file.""" + + +@dataclass +class ProcessRealityCapture(BaseConverterToNerfstudioDataset, _NoDefaultProcessRealityCapture): + """Process RealityCapture data into a nerfstudio dataset. + + This script assumes that cameras have been aligned using RealityCapture. After alignment, it is necessary to + export the camera poses as a `.csv` file using the `Internal/External camera parameters` option. + + This script does the following: + + 1. Scales images to a specified size. + 2. Converts RealityCapture poses into the nerfstudio format. + """ + + num_downscales: int = 3 + """Number of times to downscale the images. Downscales by 2 each time. For example a value of 3 + will downscale the images by 2x, 4x, and 8x.""" + max_dataset_size: int = 600 + """Max number of images to train on. If the dataset has more, images will be sampled approximately evenly. If -1, + use all images.""" + + def main(self) -> None: + """Process images into a nerfstudio dataset.""" + + if self.csv.suffix != ".csv": + raise ValueError(f"CSV file {self.csv} must have a .csv extension") + if not self.csv.exists: + raise ValueError(f"CSV file {self.csv} doesn't exist") + + self.output_dir.mkdir(parents=True, exist_ok=True) + image_dir = self.output_dir / "images" + image_dir.mkdir(parents=True, exist_ok=True) + + summary_log = [] + + # Copy images to output directory + image_filenames, num_orig_images = process_data_utils.get_image_filenames(self.data, self.max_dataset_size) + copied_image_paths = process_data_utils.copy_images_list( + image_filenames, + image_dir=image_dir, + verbose=self.verbose, + ) + num_frames = len(copied_image_paths) + + copied_image_paths = [Path("images/" + copied_image_path.name) for copied_image_path in copied_image_paths] + original_names = [image_path.stem for image_path in image_filenames] + image_filename_map = dict(zip(original_names, copied_image_paths)) + + if self.max_dataset_size > 0 and num_frames != num_orig_images: + summary_log.append(f"Started with {num_frames} images out of {num_orig_images} total") + summary_log.append( + "To change the size of the dataset add the argument [yellow]--max_dataset_size[/yellow] to " + f"larger than the current value ({self.max_dataset_size}), or -1 to use all images." + ) + else: + summary_log.append(f"Started with {num_frames} images") + + # Downscale images + summary_log.append(process_data_utils.downscale_images(image_dir, self.num_downscales, verbose=self.verbose)) + + # Save json + if num_frames == 0: + CONSOLE.print("[bold red]No images found, exiting") + sys.exit(1) + summary_log.extend( + realitycapture_utils.realitycapture_to_json( + image_filename_map=image_filename_map, + csv_filename=self.csv, + output_dir=self.output_dir, + ) + ) + + CONSOLE.rule("[bold green]:tada: :tada: :tada: All DONE :tada: :tada: :tada:") + + for summary in summary_log: + CONSOLE.print(summary, justify="center") + CONSOLE.rule() + + +Commands = Union[ + Annotated[ImagesToNerfstudioDataset, tyro.conf.subcommand(name="images")], + Annotated[VideoToNerfstudioDataset, tyro.conf.subcommand(name="video")], + Annotated[ProcessPolycam, tyro.conf.subcommand(name="polycam")], + Annotated[ProcessMetashape, tyro.conf.subcommand(name="metashape")], + Annotated[ProcessRealityCapture, tyro.conf.subcommand(name="realitycapture")], + Annotated[ProcessRecord3D, tyro.conf.subcommand(name="record3d")], +] + + +def entrypoint(): + """Entrypoint for use with pyproject scripts.""" + tyro.extras.set_accent_color("bright_yellow") + try: + tyro.cli(Commands).main() + except RuntimeError as e: + CONSOLE.log("[bold red]" + e.args[0]) + + +if __name__ == "__main__": + entrypoint() + +# For sphinx docs +get_parser_fn = lambda: tyro.extras.get_parser(Commands) # type: ignore diff --git a/scripts/render.py b/nerfstudio/scripts/render.py similarity index 87% rename from scripts/render.py rename to nerfstudio/scripts/render.py index ce2f79dc2..ae6937380 100644 --- a/scripts/render.py +++ b/nerfstudio/scripts/render.py @@ -1,3 +1,17 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #!/usr/bin/env python """ render.py @@ -11,13 +25,15 @@ import sys from contextlib import ExitStack from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Literal, Optional import mediapy as media import numpy as np import torch import tyro +from rich import box, style from rich.console import Console +from rich.panel import Panel from rich.progress import ( BarColumn, Progress, @@ -25,8 +41,9 @@ from rich.progress import ( TextColumn, TimeRemainingColumn, ) +from rich.table import Table from torchtyping import TensorType -from typing_extensions import Literal, assert_never +from typing_extensions import assert_never from nerfstudio.cameras.camera_paths import ( get_interpolated_camera_path, @@ -37,7 +54,7 @@ from nerfstudio.cameras.cameras import Cameras, CameraType from nerfstudio.data.scene_box import SceneBox from nerfstudio.model_components import renderers from nerfstudio.pipelines.base_pipeline import Pipeline -from nerfstudio.utils import install_checks +from nerfstudio.utils import colormaps, install_checks from nerfstudio.utils.eval_utils import eval_setup from nerfstudio.utils.rich_utils import ItersPerSecColumn @@ -54,6 +71,7 @@ def _render_trajectory_video( seconds: float = 5.0, output_format: Literal["images", "video"] = "video", camera_type: CameraType = CameraType.PERSPECTIVE, + colormap_options: colormaps.ColormapOptions = colormaps.ColormapOptions(), ) -> None: """Helper function to create a video of the spiral trajectory. @@ -67,6 +85,7 @@ def _render_trajectory_video( seconds: Length of output video. output_format: How to save output data. camera_type: Camera projection format type. + colormap_options: Options for colormap. """ CONSOLE.print("[bold green]Creating trajectory " + output_format) cameras.rescale_output_resolution(rendered_resolution_scaling_factor) @@ -80,8 +99,8 @@ def _render_trajectory_video( ItersPerSecColumn(suffix="fps"), TimeRemainingColumn(elapsed_when_finished=True, compact=True), ) + output_image_dir = output_filename.parent / output_filename.stem if output_format == "images": - output_image_dir = output_filename.parent / output_filename.stem output_image_dir.mkdir(parents=True, exist_ok=True) if output_format == "video": # make the folder if it doesn't exist @@ -123,9 +142,15 @@ def _render_trajectory_video( f"Please set --rendered_output_name to one of: {outputs.keys()}", justify="center" ) sys.exit(1) - output_image = outputs[rendered_output_name].cpu().numpy() - if output_image.shape[-1] == 1: - output_image = np.concatenate((output_image,) * 3, axis=-1) + output_image = outputs[rendered_output_name] + output_image = ( + colormaps.apply_colormap( + image=output_image, + colormap_options=colormap_options, + ) + .cpu() + .numpy() + ) render_image.append(output_image) render_image = np.concatenate(render_image, axis=1) if output_format == "images": @@ -143,9 +168,19 @@ def _render_trajectory_video( ) writer.add_image(render_image) + table = Table( + title=None, + show_header=False, + box=box.MINIMAL, + title_style=style.Style(bold=True), + ) if output_format == "video": if camera_type == CameraType.EQUIRECTANGULAR: insert_spherical_metadata_into_file(output_filename) + table.add_row("Video", str(output_filename)) + else: + table.add_row("Images", str(output_image_dir)) + CONSOLE.print(Panel(table, title="[bold][green]:tada: Render Complete :tada:[/bold]", expand=False)) def insert_spherical_metadata_into_file( @@ -280,10 +315,12 @@ class RenderTrajectory: """Number of interpolation steps between eval dataset cameras.""" eval_num_rays_per_chunk: Optional[int] = None """Specifies number of rays per chunk during eval.""" + colormap_options: colormaps.ColormapOptions = colormaps.ColormapOptions() + """Colormap options.""" def main(self) -> None: """Main function.""" - _, pipeline, _ = eval_setup( + _, pipeline, _, _ = eval_setup( self.load_config, eval_num_rays_per_chunk=self.eval_num_rays_per_chunk, test_mode="test" if self.traj in ["spiral", "interpolate"] else "inference", @@ -332,6 +369,7 @@ class RenderTrajectory: seconds=seconds, output_format=self.output_format, camera_type=camera_type, + colormap_options=self.colormap_options, ) diff --git a/scripts/texture.py b/nerfstudio/scripts/texture.py similarity index 75% rename from scripts/texture.py rename to nerfstudio/scripts/texture.py index 702242ef2..98f26b846 100644 --- a/scripts/texture.py +++ b/nerfstudio/scripts/texture.py @@ -1,3 +1,17 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ Script to texture an existing mesh file. """ @@ -6,11 +20,10 @@ from __future__ import annotations from dataclasses import dataclass from pathlib import Path -from typing import Optional +from typing import Literal, Optional import tyro from rich.console import Console -from typing_extensions import Literal from nerfstudio.exporter import texture_utils from nerfstudio.exporter.exporter_utils import get_mesh_from_filename @@ -51,7 +64,7 @@ class TextureMesh: mesh = get_mesh_from_filename(str(self.input_mesh_filename), target_num_faces=self.target_num_faces) # load the Pipeline - _, pipeline, _ = eval_setup(self.load_config, test_mode="inference") + _, pipeline, _, _ = eval_setup(self.load_config, test_mode="inference") # texture the mesh with NeRF and export to a mesh.obj file # and a material and texture file diff --git a/scripts/train.py b/nerfstudio/scripts/train.py similarity index 93% rename from scripts/train.py rename to nerfstudio/scripts/train.py index bb3887133..84a3ebfd3 100644 --- a/scripts/train.py +++ b/nerfstudio/scripts/train.py @@ -1,3 +1,17 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #!/usr/bin/env python """Train a radiance field with nerfstudio. For real captures, we recommend using the [bright_yellow]nerfacto[/bright_yellow] model. diff --git a/nerfstudio/scripts/viewer/__init__.py b/nerfstudio/scripts/viewer/__init__.py new file mode 100644 index 000000000..d926cf95c --- /dev/null +++ b/nerfstudio/scripts/viewer/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/nerfstudio/scripts/viewer/run_viewer.py b/nerfstudio/scripts/viewer/run_viewer.py new file mode 100644 index 000000000..62abe8941 --- /dev/null +++ b/nerfstudio/scripts/viewer/run_viewer.py @@ -0,0 +1,122 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/usr/bin/env python +""" +Starts viewer in eval mode. +""" +from __future__ import annotations + +import time +from dataclasses import dataclass, field, fields +from pathlib import Path + +import tyro +from rich.console import Console + +from nerfstudio.configs.base_config import ViewerConfig +from nerfstudio.engine.trainer import TrainerConfig +from nerfstudio.pipelines.base_pipeline import Pipeline +from nerfstudio.utils import writer +from nerfstudio.utils.eval_utils import eval_setup +from nerfstudio.viewer.server.viewer_state import ViewerState + +CONSOLE = Console(width=120) + + +@dataclass +class ViewerConfigWithoutNumRays(ViewerConfig): + """Configuration for viewer instantiation""" + + num_rays_per_chunk: tyro.conf.Suppress[int] = -1 + + def as_viewer_config(self): + """Converts the instance to ViewerConfig""" + return ViewerConfig(**{x.name: getattr(self, x.name) for x in fields(self)}) + + +@dataclass +class RunViewer: + """Load a checkpoint and start the viewer.""" + + load_config: Path + """Path to config YAML file.""" + viewer: ViewerConfigWithoutNumRays = field(default_factory=ViewerConfigWithoutNumRays) + """Viewer configuration""" + + def main(self) -> None: + """Main function.""" + config, pipeline, _, step = eval_setup( + self.load_config, + eval_num_rays_per_chunk=None, + test_mode="test", + ) + num_rays_per_chunk = config.viewer.num_rays_per_chunk + assert self.viewer.num_rays_per_chunk == -1 + config.vis = "viewer" + config.viewer = self.viewer.as_viewer_config() + config.viewer.num_rays_per_chunk = num_rays_per_chunk + + _start_viewer(config, pipeline, step) + + def save_checkpoint(self, *args, **kwargs): + """ + Mock method because we pass this instance to viewer_state.update_scene + """ + + +def _start_viewer(config: TrainerConfig, pipeline: Pipeline, step: int): + """Starts the viewer + + Args: + config: Configuration of pipeline to load + pipeline: Pipeline instance of which to load weights + step: Step at which the pipeline was saved + """ + base_dir = config.get_base_dir() + viewer_log_path = base_dir / config.viewer.relative_log_filename + viewer_state = ViewerState( + config.viewer, + log_filename=viewer_log_path, + datapath=pipeline.datamanager.get_datapath(), + pipeline=pipeline, + ) + banner_messages = [f"Viewer at: {viewer_state.viewer_url}"] + + # We don't need logging, but writer.GLOBAL_BUFFER needs to be populated + config.logging.local_writer.enable = False + writer.setup_local_writer(config.logging, max_iter=config.max_num_iterations, banner_messages=banner_messages) + + assert viewer_state and pipeline.datamanager.train_dataset + viewer_state.init_scene( + dataset=pipeline.datamanager.train_dataset, + train_state="completed", + ) + viewer_state.viser_server.set_training_state("completed") + viewer_state.update_scene(step=step) + while True: + time.sleep(0.01) + + +def entrypoint(): + """Entrypoint for use with pyproject scripts.""" + tyro.extras.set_accent_color("bright_yellow") + tyro.cli(RunViewer).main() + + +if __name__ == "__main__": + entrypoint() + +# For sphinx docs +get_parser_fn = lambda: tyro.extras.get_parser(RunViewer) # noqa diff --git a/nerfstudio/scripts/viewer/sync_viser_message_defs.py b/nerfstudio/scripts/viewer/sync_viser_message_defs.py new file mode 100644 index 000000000..c693db288 --- /dev/null +++ b/nerfstudio/scripts/viewer/sync_viser_message_defs.py @@ -0,0 +1,68 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generate viser message definitions for TypeScript, by parsing Python dataclasses.""" +import json +import pathlib +from datetime import datetime + +import tyro +from viser.infra import generate_typescript_interfaces + +from nerfstudio.viewer.viser import NerfstudioMessage + + +def main() -> None: + """Generate viser message definitions for TypeScript, by parsing Python dataclasses.""" + + # Generate typescript source. + defs = generate_typescript_interfaces(NerfstudioMessage) + + # Write to file. + # Three parents from nerfstudio/scripts/viewer/sync_viser_message_defs.py: + # - nerfstudio/scripts/viewer/ + # - nerfstudio/scripts/ + # - nerfstudio/ + target_path = pathlib.Path(__file__).absolute().parent.parent.parent / pathlib.Path( + "nerfstudio/viewer/app/src/modules/WebSocket/ViserMessages.tsx" + ) + assert target_path.exists() + + old_defs = target_path.read_text(encoding="utf_8") + + if old_defs != defs: + target_path.write_text(defs, encoding="utf_8") + + with open("nerfstudio/viewer/app/package.json", "r", encoding="utf_8") as f: + data = json.load(f) + + now = datetime.now() + data["version"] = now.strftime("%y-%m-%d") + "-0" + + with open("nerfstudio/viewer/app/package.json", "w", encoding="utf_8") as f: + json.dump(data, f, indent=2) + print(f"Wrote updates to {target_path}") + print(f"Current viewer version is now {data['version']}") + else: + print("No update to messages.") + + +def entrypoint() -> None: + """Entrypoint for use with pyproject scripts.""" + # All entrypoints must currently be tyro CLIs. + tyro.cli(main) + + +if __name__ == "__main__": + entrypoint() diff --git a/nerfstudio/utils/colormaps.py b/nerfstudio/utils/colormaps.py index aae1b2a92..a9eb6fc8b 100644 --- a/nerfstudio/utils/colormaps.py +++ b/nerfstudio/utils/colormaps.py @@ -14,27 +14,91 @@ """ Helper functions for visualizing outputs """ -from typing import Optional +from dataclasses import dataclass +from typing import Literal, Optional +import matplotlib import torch -from matplotlib import cm from torchtyping import TensorType from nerfstudio.utils import colors +Colormaps = Literal["default", "turbo", "viridis", "magma", "inferno", "cividis"] -def apply_colormap(image: TensorType["bs":..., 1], cmap="viridis") -> TensorType["bs":..., "rgb":3]: + +@dataclass +class ColormapOptions: + """Options for colormap""" + + colormap: Colormaps = "default" + """ The colormap to use """ + normalize: bool = False + """ Whether to normalize the input tensor image """ + colormap_min: float = 0 + """ Minimum value for the output colormap """ + colormap_max: float = 1 + """ Maximum value for the output colormap """ + invert: bool = False + """ Whether to invert the output colormap """ + + +def apply_colormap( + image: TensorType["bs":..., 1], + colormap_options: ColormapOptions = ColormapOptions(), + eps: float = 1e-9, +) -> TensorType["bs":..., "rgb":3]: + """ + Applies a colormap to a tensor image. Currently only supports 1 and 3 channel inputs + + Args: + image: Input tensor image. + eps: Epsilon value for numerical stability. + + Returns: + Tensor with the colormap applied. + """ + + # default for rgb images + if image.shape[-1] == 3: + return image + + # rendering depth outputs + if image.shape[-1] == 1 and torch.is_floating_point(image): + output = image + if colormap_options.normalize: + output = output - torch.min(output) + output = output / (torch.max(output) + eps) + output = ( + output * (colormap_options.colormap_max - colormap_options.colormap_min) + colormap_options.colormap_min + ) + output = torch.clip(output, 0, 1) + if colormap_options.invert: + output = 1 - output + return apply_float_colormap(output, colormap=colormap_options.colormap) + + # rendering boolean outputs + if image.dtype == torch.bool: + return apply_boolean_colormap(image) + + raise NotImplementedError + + +def apply_float_colormap( + image: TensorType["bs":..., 1], colormap: Colormaps = "viridis" +) -> TensorType["bs":..., "rgb":3]: """Convert single channel to a color image. Args: image: Single channel image. - cmap: Colormap for image. + colormap: Colormap for image. Returns: TensorType: Colored image with colors in [0, 1] """ + if colormap == "default": + colormap = "turbo" - colormap = cm.get_cmap(cmap) + colormap = matplotlib.colormaps[colormap] colormap = torch.tensor(colormap.colors).to(image.device) # type: ignore image = torch.nan_to_num(image, 0) image_long = (image * 255).long() @@ -50,7 +114,7 @@ def apply_depth_colormap( accumulation: Optional[TensorType["bs":..., 1]] = None, near_plane: Optional[float] = None, far_plane: Optional[float] = None, - cmap="turbo", + colormap_options: ColormapOptions = ColormapOptions(), ) -> TensorType["bs":..., "rgb":3]: """Converts a depth image to color for easier analysis. @@ -59,7 +123,7 @@ def apply_depth_colormap( accumulation: Ray accumulation used for masking vis. near_plane: Closest depth to consider. If None, use min image value. far_plane: Furthest depth to consider. If None, use max image value. - cmap: Colormap to apply. + colormap: Colormap to apply. Returns: Colored depth image with colors in [0, 1] @@ -72,7 +136,7 @@ def apply_depth_colormap( depth = torch.clip(depth, 0, 1) # depth = torch.nan_to_num(depth, nan=0.0) # TODO(ethan): remove this - colored_image = apply_colormap(depth, cmap=cmap) + colored_image = apply_colormap(depth, colormap_options=colormap_options) if accumulation is not None: colored_image = colored_image * accumulation + (1 - accumulation) diff --git a/nerfstudio/utils/decorators.py b/nerfstudio/utils/decorators.py index 6f92ac679..356369f82 100644 --- a/nerfstudio/utils/decorators.py +++ b/nerfstudio/utils/decorators.py @@ -42,7 +42,7 @@ def check_profiler_enabled(func: Callable) -> Callable: def wrapper(self, *args, **kwargs): ret = None - if self.config.enable_profiler: + if self.config.profiler != "none": ret = func(self, *args, **kwargs) return ret diff --git a/nerfstudio/utils/eval_utils.py b/nerfstudio/utils/eval_utils.py index 3431b20a3..5b35c33ff 100644 --- a/nerfstudio/utils/eval_utils.py +++ b/nerfstudio/utils/eval_utils.py @@ -20,12 +20,11 @@ from __future__ import annotations import os import sys from pathlib import Path -from typing import Optional, Tuple +from typing import Literal, Optional, Tuple import torch import yaml from rich.console import Console -from typing_extensions import Literal from nerfstudio.engine.trainer import TrainerConfig from nerfstudio.pipelines.base_pipeline import Pipeline @@ -33,13 +32,15 @@ from nerfstudio.pipelines.base_pipeline import Pipeline CONSOLE = Console(width=120) -def eval_load_checkpoint(config: TrainerConfig, pipeline: Pipeline) -> Path: +def eval_load_checkpoint(config: TrainerConfig, pipeline: Pipeline) -> Tuple[Path, int]: ## TODO: ideally eventually want to get this to be the same as whatever is used to load train checkpoint too """Helper function to load checkpointed pipeline Args: config (DictConfig): Configuration of pipeline to load pipeline (Pipeline): Pipeline instance of which to load weights + Returns: + A tuple of the path to the loaded checkpoint and the step at which it was saved. """ assert config.load_dir is not None if config.load_step is None: @@ -61,14 +62,14 @@ def eval_load_checkpoint(config: TrainerConfig, pipeline: Pipeline) -> Path: loaded_state = torch.load(load_path, map_location="cpu") pipeline.load_pipeline(loaded_state["pipeline"], loaded_state["step"]) CONSOLE.print(f":white_check_mark: Done loading checkpoint from {load_path}") - return load_path + return load_path, load_step def eval_setup( config_path: Path, eval_num_rays_per_chunk: Optional[int] = None, test_mode: Literal["test", "val", "inference"] = "test", -) -> Tuple[TrainerConfig, Pipeline, Path]: +) -> Tuple[TrainerConfig, Pipeline, Path, int]: """Shared setup for loading a saved pipeline for evaluation. Args: @@ -81,7 +82,7 @@ def eval_setup( Returns: - Loaded config, pipeline module, and corresponding checkpoint. + Loaded config, pipeline module, corresponding checkpoint, and step """ # load save config config = yaml.load(config_path.read_text(), Loader=yaml.Loader) @@ -102,6 +103,6 @@ def eval_setup( pipeline.eval() # load checkpointed information - checkpoint_path = eval_load_checkpoint(config, pipeline) + checkpoint_path, step = eval_load_checkpoint(config, pipeline) - return config, pipeline, checkpoint_path + return config, pipeline, checkpoint_path, step diff --git a/nerfstudio/utils/math.py b/nerfstudio/utils/math.py index f54b6509a..924556597 100644 --- a/nerfstudio/utils/math.py +++ b/nerfstudio/utils/math.py @@ -15,13 +15,10 @@ """ Math Helper Functions """ from dataclasses import dataclass -from typing import Tuple +from typing import Literal, Tuple import torch from torchtyping import TensorType -from typing_extensions import Literal - -_USE_NERFACC = True def components_from_spherical_harmonics(levels: int, directions: TensorType[..., 3]) -> TensorType[..., "components"]: @@ -198,7 +195,7 @@ def expected_sin(x_means: torch.Tensor, x_vars: torch.Tensor) -> torch.Tensor: @torch.jit.script -def _intersect_aabb( +def intersect_aabb( origins: torch.Tensor, directions: torch.Tensor, aabb: torch.Tensor, @@ -238,40 +235,6 @@ def _intersect_aabb( return t_min, t_max -def intersect_aabb( - origins: TensorType["N", 3], - directions: TensorType["N", 3], - aabb: TensorType[6], -) -> Tuple[TensorType["N"], TensorType["N"]]: - """ - Implementation of ray intersection with AABB box - - Args: - origins: 3d positions - directions: Normalized directions - aabb: array of aabb box in the form of [x_min, y_min, z_min, x_max, y_max, z_max] - max_bound: Maximum value of t_max - invalid_value: Value to return in case of no intersection - - Returns: - t_min, t_max - two tensors of shapes N representing distance of intersection from the origin. - """ - - global _USE_NERFACC # pylint: disable=global-statement - if _USE_NERFACC: - try: - import nerfacc # pylint: disable=import-outside-toplevel - - t_min, t_max = nerfacc.ray_aabb_intersect(origins, directions, aabb) - except: # pylint: disable=bare-except - t_min, t_max = _intersect_aabb(origins, directions, aabb, max_bound=1e10, invalid_value=1e10) - _USE_NERFACC = False - else: - t_min, t_max = _intersect_aabb(origins, directions, aabb, max_bound=1e10, invalid_value=1e10) - - return t_min, t_max - - def safe_normalize( vectors: TensorType["batch_dim":..., "N"], eps: float = 1e-10, diff --git a/nerfstudio/utils/profiler.py b/nerfstudio/utils/profiler.py index c1fc96dcb..5d5599de6 100644 --- a/nerfstudio/utils/profiler.py +++ b/nerfstudio/utils/profiler.py @@ -17,10 +17,17 @@ Profiler base class and functionality """ from __future__ import annotations +import functools +import os import time -from typing import Callable +import typing +from collections import deque +from contextlib import ContextDecorator, contextmanager +from pathlib import Path +from typing import Callable, Dict, List, Optional, Tuple, Union from rich.console import Console +from torch.profiler import ProfilerActivity, profile, record_function from nerfstudio.configs import base_config as cfg from nerfstudio.utils import comms @@ -33,32 +40,124 @@ from nerfstudio.utils.decorators import ( CONSOLE = Console(width=120) PROFILER = [] +PYTORCH_PROFILER = None -def time_function(func: Callable) -> Callable: - """Decorator: time a function call""" +class time_function(ContextDecorator): # pylint: disable=invalid-name + """Decorator/Context manager: time a function call or a block of code""" - def wrapper(*args, **kwargs): - start = time.time() - ret = func(*args, **kwargs) + def __init__(self, name: Union[str, Callable]): + # NOTE: This is a workaround for the fact that the __new__ method of a ContextDecorator + # is not picked up by VSCode intellisense + self.name: str = typing.cast(str, name) + self.start = None + self._profiler_contexts = deque() + self._function_call_args: Optional[Tuple[Tuple, Dict]] = None + + def __new__(cls, func: Union[str, Callable]): + instance = super().__new__(cls) + if isinstance(func, str): + instance.__init__(func) + return instance + if callable(func): + instance.__init__(func.__qualname__) + return instance(func) + raise ValueError(f"Argument func of type {type(func)} is not a string or a callable.") + + def __enter__(self): + self.start = time.time() + if PYTORCH_PROFILER is not None: + args, kwargs = tuple(), {} + if self._function_call_args is not None: + args, kwargs = self._function_call_args + ctx = PYTORCH_PROFILER.record_function(self.name, *args, **kwargs) + ctx.__enter__() # pylint: disable=no-member + self._profiler_contexts.append(ctx) + if self._function_call_args is None: + ctx = record_function(self.name) + ctx.__enter__() + self._profiler_contexts.append(ctx) + + def __exit__(self, *args, **kwargs): + while self._profiler_contexts: + context = self._profiler_contexts.pop() + context.__exit__(*args, **kwargs) if PROFILER: - class_str = func.__qualname__ - PROFILER[0].update_time(class_str, start, time.time()) - return ret + PROFILER[0].update_time(self.name, self.start, time.time()) - return wrapper + def __call__(self, func: Callable): + @functools.wraps(func) + def inner(*args, **kwargs): + self._function_call_args = (args, kwargs) + with self: + out = func(*args, **kwargs) + self._function_call_args = None + return out + + return inner def flush_profiler(config: cfg.LoggingConfig): """Method that checks if profiler is enabled before flushing""" - if config.enable_profiler and PROFILER: + if config.profiler != "none" and PROFILER: PROFILER[0].print_profile() -def setup_profiler(config: cfg.LoggingConfig): +def setup_profiler(config: cfg.LoggingConfig, log_dir: Path): """Initialization of profilers""" + global PYTORCH_PROFILER # pylint: disable=global-statement if comms.is_main_process(): PROFILER.append(Profiler(config)) + if config.profiler == "pytorch": + PYTORCH_PROFILER = PytorchProfiler(log_dir) + + +class PytorchProfiler: # pylint: disable=too-few-public-methods + """ + Wrapper for Pytorch Profiler + """ + + def __init__(self, output_path: Path, trace_steps: Optional[List[int]] = None): + self.output_path = output_path / "profiler_traces" + if trace_steps is None: + # Some arbitrary steps which likely do not overlap with steps usually chosen to run callbacks + trace_steps = [12, 17] + self.trace_steps = trace_steps + + @contextmanager + def record_function(self, function: str, *args, **_kwargs): + """ + Context manager that records a function call and saves the trace to a json file. + Traced functions are: train_iteration, eval_iteration + """ + if function.endswith("train_iteration") or function.endswith("eval_iteration"): + step = args[1] + assert isinstance(step, int) + assert len(args) == 2 + stage = function.split(".")[-1].split("_")[0] + if step in self.trace_steps: + launch_kernel_blocking = self.trace_steps.index(step) % 2 == 0 + backup_lb_var = "" + if launch_kernel_blocking: + backup_lb_var = os.environ.get("CUDA_LAUNCH_BLOCKING", "") + os.environ["CUDA_LAUNCH_BLOCKING"] = "1" + with profile( + activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA], + record_shapes=True, + with_stack=True, + profile_memory=True, + ) as prof: + yield None + if launch_kernel_blocking: + os.environ["CUDA_LAUNCH_BLOCKING"] = backup_lb_var + self.output_path.mkdir(parents=True, exist_ok=True) + prof.export_chrome_trace( + str(self.output_path / f"trace_{stage}_{step}{'_blocking' if launch_kernel_blocking else ''}.json") + ) + return + # Functions are recorded automatically + yield None + return @decorate_all([check_profiler_enabled, check_main_thread]) diff --git a/nerfstudio/utils/writer.py b/nerfstudio/utils/writer.py index 720d1d449..d996a7f36 100644 --- a/nerfstudio/utils/writer.py +++ b/nerfstudio/utils/writer.py @@ -18,6 +18,7 @@ Generic Writer class from __future__ import annotations import enum +import os from abc import abstractmethod from pathlib import Path from time import time @@ -46,7 +47,6 @@ class EventName(enum.Enum): ITER_TRAIN_TIME = "Train Iter (time)" TOTAL_TRAIN_TIME = "Train Total (time)" - ITER_VIS_TIME = "Viewer Rendering (time)" ETA = "ETA (time)" TRAIN_RAYS_PER_SEC = "Train Rays / Sec" TEST_RAYS_PER_SEC = "Test Rays / Sec" @@ -187,10 +187,22 @@ def setup_local_writer(config: cfg.LoggingConfig, max_iter: int, banner_messages GLOBAL_BUFFER["events"] = {} -@check_main_thread -def setup_event_writer(is_wandb_enabled: bool, is_tensorboard_enabled: bool, log_dir: Path) -> None: - """Initialization of all event writers specified in config +def is_initialized(): + """ + Returns True after setup_local_writer was called + """ + return "events" in GLOBAL_BUFFER + +@check_main_thread +def setup_event_writer( + is_wandb_enabled: bool, + is_tensorboard_enabled: bool, + log_dir: Path, + experiment_name: str, + project_name: str = "nerfstudio-project", +) -> None: + """Initialization of all event writers specified in config Args: config: configuration to instantiate loggers max_iter: maximum number of train iterations @@ -198,7 +210,7 @@ def setup_event_writer(is_wandb_enabled: bool, is_tensorboard_enabled: bool, log """ using_event_writer = False if is_wandb_enabled: - curr_writer = WandbWriter(log_dir=log_dir) + curr_writer = WandbWriter(log_dir=log_dir, experiment_name=experiment_name, project_name=project_name) EVENT_WRITERS.append(curr_writer) using_event_writer = True if is_tensorboard_enabled: @@ -268,7 +280,7 @@ class TimeWriter: def __exit__(self, *args): self.duration = time() - self.start update_step = self.step is not None - if self.write: + if self.write and is_initialized(): self.writer.put_time( name=self.name, duration=self.duration, @@ -282,8 +294,13 @@ class TimeWriter: class WandbWriter(Writer): """WandDB Writer Class""" - def __init__(self, log_dir: Path): - wandb.init(project="nerfstudio-project", dir=str(log_dir), reinit=True) + def __init__(self, log_dir: Path, experiment_name: str, project_name: str = "nerfstudio-project"): + wandb.init( + project=os.environ.get("WANDB_PROJECT", project_name), + dir=os.environ.get("WANDB_DIR", str(log_dir)), + name=os.environ.get("WANDB_NAME", experiment_name), + reinit=True, + ) def write_image(self, name: str, image: TensorType["H", "W", "C"], step: int) -> None: image = torch.permute(image, (2, 0, 1)) diff --git a/nerfstudio/viewer/app/.eslintrc.json b/nerfstudio/viewer/app/.eslintrc.json index ecb608b57..f219c3898 100644 --- a/nerfstudio/viewer/app/.eslintrc.json +++ b/nerfstudio/viewer/app/.eslintrc.json @@ -4,10 +4,12 @@ "es2021": true }, "extends": [ + "react-app", "eslint:recommended", "plugin:react/recommended", "airbnb", - "prettier" + "prettier", + "plugin:import/typescript" ], "parser": "@typescript-eslint/parser", "parserOptions": { @@ -27,6 +29,20 @@ "prefer-destructuring": "off", "react/destructuring-assignment": "off", "react/prop-types": 0, - "unused-imports/no-unused-imports-ts": 2 + "react/jsx-filename-extension": [ + 2, + { "extensions": [".js", ".jsx", ".ts", ".tsx"] } + ], + "unused-imports/no-unused-imports-ts": 2, + "import/extensions": [ + "error", + "ignorePackages", + { + "js": "never", + "jsx": "never", + "ts": "never", + "tsx": "never" + } + ] } } diff --git a/nerfstudio/viewer/app/package.json b/nerfstudio/viewer/app/package.json index 3525ebd86..4d159c41e 100644 --- a/nerfstudio/viewer/app/package.json +++ b/nerfstudio/viewer/app/package.json @@ -1,7 +1,11 @@ { "name": "viewer", "homepage": ".", +<<<<<<< HEAD "version": "23-03-9-0", +======= + "version": "23-05-01-0", +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb "private": true, "dependencies": { "@emotion/react": "^11.10.4", @@ -15,6 +19,7 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^14.2.0", + "await-lock": "^2.2.2", "camera-controls": "^1.37.2", "classnames": "^2.3.1", "dat.gui": "^0.7.9", @@ -22,8 +27,10 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-unused-imports": "^2.0.0", "leva": "^0.9.29", + "lodash": "^4.17.21", "meshline": "^2.0.4", "msgpack-lite": "^0.1.26", + "msgpackr": "^1.8.5", "prop-types": "^15.8.1", "re-resizable": "^6.9.9", "react": "^18.1.0", @@ -77,10 +84,17 @@ "eslint-plugin-react-hooks": "^4.3.0", "prettier": "2.7.1", "react-scripts": "^5.0.1", - "typescript": "^4.7.3", + "typescript": "^5.0.3", "wait-on": "^6.0.1" }, "resolutions": { - "nth-check": "^2.0.1" + "babel-loader": "^9.1.2", + "json5": "^2.2.3", + "loader-utils": "^3.2.1", + "nth-check": "^2.0.1", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", + "recursive-readdir": "^2.2.3", + "resolve-url-loader": "^5.0.0", + "yaml": "^2.2.2" } -} +} \ No newline at end of file diff --git a/nerfstudio/viewer/app/run_deploy.py b/nerfstudio/viewer/app/run_deploy.py index 03fd86465..99d5f47a9 100644 --- a/nerfstudio/viewer/app/run_deploy.py +++ b/nerfstudio/viewer/app/run_deploy.py @@ -119,7 +119,6 @@ def main( # update the symlink of latest if branch_name == "main": - # move the build folder to the correct location run_cmd(f"""ssh {host} 'rm -rf {remote_folder}/{version}'""") run_cmd(f"""ssh {host} 'cp -R /home/eweb0124/build {remote_folder}/{version}'""") diff --git a/nerfstudio/viewer/app/src/App.jsx b/nerfstudio/viewer/app/src/App.jsx index 1d2987107..fd42a37f9 100644 --- a/nerfstudio/viewer/app/src/App.jsx +++ b/nerfstudio/viewer/app/src/App.jsx @@ -1,14 +1,11 @@ import { CssBaseline, ThemeProvider } from '@mui/material'; import React from 'react'; -import { - SceneTreeWebSocketListener, - get_scene_tree, -} from './modules/Scene/Scene'; +import { get_scene_tree } from './modules/Scene/Scene'; import Banner from './modules/Banner'; import { BasicTabs } from './modules/SidePanel/SidePanel'; import ViewerWindow from './modules/ViewerWindow/ViewerWindow'; -import { appTheme } from './themes/theme.ts'; +import { appTheme } from './themes/theme'; export default function App() { // The scene tree won't rerender but it will listen to changes @@ -20,8 +17,6 @@ export default function App() {
- {/* Listens for websocket 'write' messages and updates the redux store. */} - {/* The banner at the top of the page. */}
diff --git a/nerfstudio/viewer/app/src/index.jsx b/nerfstudio/viewer/app/src/index.jsx index 966e50e43..b712d1d3d 100644 --- a/nerfstudio/viewer/app/src/index.jsx +++ b/nerfstudio/viewer/app/src/index.jsx @@ -1,16 +1,16 @@ import './index.scss'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import App from './App'; -import WebSocketProvider from './modules/WebSocket/WebSocket'; import store from './store'; +import { ViserWebSocket } from './modules/WebSocket/ViserWebSocket'; -const root = ReactDOM.createRoot(document.getElementById('root')); +const root = createRoot(document.getElementById('root')); root.render( - + - + , ); diff --git a/nerfstudio/viewer/app/src/index.scss b/nerfstudio/viewer/app/src/index.scss index a3af6c244..acdfbdcd2 100644 --- a/nerfstudio/viewer/app/src/index.scss +++ b/nerfstudio/viewer/app/src/index.scss @@ -17,6 +17,7 @@ code { .App { position: relative; height: 100vh; + background-color: #1d222a; } $base-color1: #292d39; @@ -358,8 +359,37 @@ $sidebar-width: 100% !default; border: $custom-border; } +<<<<<<< HEAD .RenderWindow { z-index: 1; +======= +#not-connected-overlay { + position: absolute; + background-color: #1d222a; + height: 100%; + width: 100%; + text-align: center; + margin: 0; + padding: 0; + z-index: 10; + opacity: 0.8; +} + +#not-connected-overlay-text { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + background-color: #393939; + border-radius: 10px; + padding-left: 15px; + padding-right: 15px; + padding-top: 5px; + padding-bottom: 5px; + color: #ff6969; + font-size: 18px; + z-index: 10; +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb } .ViewerWindow-render-crop-container { diff --git a/nerfstudio/viewer/app/src/modules/ConfigPanel/ConfigPanel.jsx b/nerfstudio/viewer/app/src/modules/ConfigPanel/ConfigPanel.jsx index 2bb6d8fd3..f7a2392e7 100644 --- a/nerfstudio/viewer/app/src/modules/ConfigPanel/ConfigPanel.jsx +++ b/nerfstudio/viewer/app/src/modules/ConfigPanel/ConfigPanel.jsx @@ -1,33 +1,155 @@ -import { buttonGroup, useControls } from 'leva'; -import { useContext, useEffect, useState } from 'react'; +import * as React from 'react'; +import { + LevaPanel, + useCreateStore, + Leva, + useControls, + folder, + button, +} from 'leva'; import { useDispatch, useSelector } from 'react-redux'; +import { Box } from '@mui/material'; +import LevaTheme from '../../themes/leva_theme.json'; +import { + ViserWebSocketContext, + sendWebsocketMessage, + makeThrottledMessageSender, +} from '../WebSocket/ViserWebSocket'; -import { WebSocketContext } from '../WebSocket/WebSocket'; +function CustomLeva() { + const viser_websocket = React.useContext(ViserWebSocketContext); + const customGui = useSelector((state) => state.custom_gui); + const guiNames = customGui.guiNames; + const guiConfigFromName = customGui.guiConfigFromName; -const msgpack = require('msgpack-lite'); - -function dispatch_and_send(websocket, dispatch, path, data) { - dispatch({ - type: 'write', - path, - data, - }); - if (websocket.readyState === WebSocket.OPEN) { - const message = msgpack.encode({ - type: 'write', - path, - data, + // We're going to try and build an object that looks like: + // {"folder name": {"input name": leva config}} + const guiConfigTree: { [key: string]: any } = {}; + function getFolderContainer(folderLabels: string[]) { + let guiConfigNode = guiConfigTree; + folderLabels.forEach((label) => { + if (guiConfigNode[label] === undefined) { + guiConfigNode[label] = { _is_folder_marker: true }; + } + guiConfigNode = guiConfigNode[label]; }); - websocket.send(message); + return guiConfigNode; } + + guiNames.forEach((key) => { + const { levaConf, folderLabels, hidden } = guiConfigFromName[key]; + if (hidden) return; + const leafFolder = getFolderContainer(folderLabels); + + // Hacky stuff that lives outside of TypeScript... + if (levaConf.type === 'BUTTON') { + // Add a button. + leafFolder[key] = button(() => { + sendWebsocketMessage(viser_websocket, { + type: 'GuiUpdateMessage', + name: key, + value: true, + }); + }, levaConf.settings); + } else { + // Add any other kind of input. + const throttledSender = makeThrottledMessageSender(viser_websocket, 25); + leafFolder[key] = { + ...levaConf, + onChange: (value: any, _propName: any, options: any) => { + if (options.initial) return; + throttledSender({ + type: 'GuiUpdateMessage', + name: key, + value, + }); + }, + }; + } + }); + + // Recursively wrap folders in a GUI config tree with Leva's `folder()`. + function wrapFoldersInGuiConfigTree( + guiConfigNode: { [key: string]: any }, + root: boolean, + ) { + const { _is_folder_marker, ...rest } = guiConfigNode; + const nodeCopy = { ...rest }; + + if (root || _is_folder_marker === true) { + const out: { [key: string]: any } = {}; + Object.entries(nodeCopy).forEach(([k, v]) => { + out[k] = wrapFoldersInGuiConfigTree(v, false); + }); + return root ? out : folder(out); + } + return guiConfigNode; + } + + // Make Leva controls. + const levaStore = useCreateStore(); + const [, set] = useControls( + () => wrapFoldersInGuiConfigTree(guiConfigTree, true), + { store: levaStore }, + [guiConfigFromName], + ); + + const dispatch = useDispatch(); + const guiSetQueue = customGui.guiSetQueue; + React.useEffect(() => { + // This line is important to prevent looping + if (Object.keys(guiSetQueue).length === 0) return; + Object.entries(guiSetQueue).forEach(([key, value]) => { + if (guiNames.includes(key)) { + set({ [key]: value }); // call the leva function for setting the value of the element + } + }); + // delete the queue + dispatch({ + type: 'write', + path: 'custom_gui/guiSetQueue', + data: {}, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [guiSetQueue, set, guiNames]); + + // Leva theming is a bit limited, so we hack at styles here... + return ( + + ); } export function RenderControls() { - // connection status indicators - const websocket = useContext(WebSocketContext).socket; - const outputOptions = useSelector( - (state) => state.renderingState.output_options, + return ( +
+ + + + +
); +<<<<<<< HEAD const outputChoice = useSelector( (state) => state.renderingState.output_choice, ); @@ -359,4 +481,6 @@ export function RenderControls() { ]); return null; +======= +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb } diff --git a/nerfstudio/viewer/app/src/modules/ConfigPanel/ConfigPanelSlice.js b/nerfstudio/viewer/app/src/modules/ConfigPanel/ConfigPanelSlice.js deleted file mode 100644 index 49f86a065..000000000 --- a/nerfstudio/viewer/app/src/modules/ConfigPanel/ConfigPanelSlice.js +++ /dev/null @@ -1,7 +0,0 @@ -// The function below is called a selector and allows us to select a value from -// the state. Selectors can also be defined inline where they're used instead of -// in the slice file. For example: `useSelector((state) => state.counter.value)` -export const selectTrainingState = (state) => - state.shared.rendering.training_state; -export const selectOutputOptions = (state) => state.shared.output_options; -export const selectColormapOptions = (state) => state.shared.colormap_options; diff --git a/nerfstudio/viewer/app/src/modules/LoadPathModal/LoadPathModal.jsx b/nerfstudio/viewer/app/src/modules/LoadPathModal/LoadPathModal.jsx index 5f79d1e00..ad8c83d7e 100644 --- a/nerfstudio/viewer/app/src/modules/LoadPathModal/LoadPathModal.jsx +++ b/nerfstudio/viewer/app/src/modules/LoadPathModal/LoadPathModal.jsx @@ -24,9 +24,7 @@ export default function LoadPathModal(props: LoadPathModalProps) { const [existingPathSelect, setExistingPathSelect] = React.useState(''); // redux store state - const all_camera_paths = useSelector( - (state) => state.renderingState.all_camera_paths, - ); + const all_camera_paths = useSelector((state) => state.all_camera_paths); let camera_paths_arr = []; if (typeof all_camera_paths === 'object' && all_camera_paths !== null) { @@ -113,9 +111,9 @@ export default function LoadPathModal(props: LoadPathModalProps) {
-

-

OR
-

+
+

OR

+
)} {camera_paths_arr.length === 0 && ( diff --git a/nerfstudio/viewer/app/src/modules/LogPanel/LogPanel.jsx b/nerfstudio/viewer/app/src/modules/LogPanel/LogPanel.jsx deleted file mode 100644 index 9782a9645..000000000 --- a/nerfstudio/viewer/app/src/modules/LogPanel/LogPanel.jsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useContext, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import { WebSocketContext } from '../WebSocket/WebSocket'; - -const msgpack = require('msgpack-lite'); - -export function LogPanel() { - const websocket = useContext(WebSocketContext).socket; - const dispatch = useDispatch(); - const gpu_oom_error_msg = 'GPU out of memory'; - const resolved_msg = 'resolved'; - let local_error = resolved_msg; - // connection status indicators - - const set_max_train_util = () => { - if (websocket.readyState === WebSocket.OPEN) { - dispatch({ - type: 'write', - path: 'renderingState/targetTrainUtil', - data: 0.9, - }); - const cmd = 'write'; - const path = 'renderingState/targetTrainUtil'; - const data = { - type: cmd, - path, - data: 0.9, - }; - const message = msgpack.encode(data); - websocket.send(message); - } - }; - - const set_small_resolution = () => { - if (websocket.readyState === WebSocket.OPEN) { - dispatch({ - type: 'write', - path: 'renderingState/maxResolution', - data: 512, - }); - const cmd = 'write'; - const path = 'renderingState/maxResolution'; - const data = { - type: cmd, - path, - data: 512, - }; - const message = msgpack.encode(data); - websocket.send(message); - } - }; - - const set_log_message = () => { - if (websocket.readyState === WebSocket.OPEN) { - dispatch({ - type: 'write', - path: 'renderingState/log_errors', - data: resolved_msg, - }); - const cmd = 'write'; - const path = 'renderingState/log_errors'; - const data = { - type: cmd, - path, - data: resolved_msg, - }; - const message = msgpack.encode(data); - websocket.send(message); - } - }; - - const check_error = useSelector((state) => { - local_error = state.renderingState.log_errors; - if (local_error.includes(gpu_oom_error_msg)) { - console.log(local_error); - set_log_message(); - set_small_resolution(); - set_max_train_util(); - } - }); - - useEffect(() => {}, [check_error, local_error]); - - return null; -} diff --git a/nerfstudio/viewer/app/src/modules/RenderModal/RenderModal.jsx b/nerfstudio/viewer/app/src/modules/RenderModal/RenderModal.jsx index 8b0a18321..8c6121f68 100644 --- a/nerfstudio/viewer/app/src/modules/RenderModal/RenderModal.jsx +++ b/nerfstudio/viewer/app/src/modules/RenderModal/RenderModal.jsx @@ -16,13 +16,15 @@ export default function RenderModal(props: RenderModalProps) { // redux store state const config_base_dir = useSelector( - (state) => state.renderingState.config_base_dir, + (state) => state.file_path_info.config_base_dir, ); - const export_path = useSelector((state) => state.renderingState.export_path); + const export_path = useSelector( + (state) => state.file_path_info.export_path_name, + ); const data_base_dir = useSelector( - (state) => state.renderingState.data_base_dir, + (state) => state.file_path_info.data_base_dir, ); // react state diff --git a/nerfstudio/viewer/app/src/modules/Scene/Scene.jsx b/nerfstudio/viewer/app/src/modules/Scene/Scene.jsx index 8388f7735..197f6887f 100644 --- a/nerfstudio/viewer/app/src/modules/Scene/Scene.jsx +++ b/nerfstudio/viewer/app/src/modules/Scene/Scene.jsx @@ -1,7 +1,6 @@ /* eslint-disable no-restricted-syntax */ import * as THREE from 'three'; -import { useContext, useEffect } from 'react'; import CameraControls from 'camera-controls'; import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer'; @@ -11,12 +10,9 @@ import { drawCamera, drawSceneBox } from './drawing'; import { CameraHelper } from '../SidePanel/CameraPanel/CameraHelper'; import SceneNode from '../../SceneNode'; -import { WebSocketContext } from '../WebSocket/WebSocket'; import { subscribe_to_changes } from '../../subscriber'; import { snap_to_camera } from '../SidePanel/SidePanel'; -const msgpack = require('msgpack-lite'); - const SCENE_BOX_NAME = 'Scene Box'; const CAMERAS_NAME = 'Training Cameras'; @@ -31,16 +27,17 @@ export function get_scene_tree() { }, setValue(key, value) { this.value.set(key, value); - this.callbacks.forEach((cbk) => { - const i = cbk[1]; - const callback = cbk[0]; - return callback(this.value.get(i)); + this.callbacks.forEach((callback, callbackKey) => { + if (callbackKey === key) { + callback(value); + } }); }, }; const sceneTree = new SceneNode(scene, scene_state); + // eslint-disable-next-line react-hooks/rules-of-hooks const dispatch = useDispatch(); const BANNER_HEIGHT = 50; @@ -164,6 +161,9 @@ export function get_scene_tree() { if (!scene_state.value.get('mouse_in_scene')) { return; } + if (keyMap.Space === true) { + camera_controls.setLookAt(0.7, -0.7, 0.3, 0, 0, 0); + } translate(); rotate(); } @@ -189,12 +189,6 @@ export function get_scene_tree() { window.addEventListener('keydown', onKeyDown, true); window.addEventListener('keyup', onKeyUp, true); - window.addEventListener('keydown', (event) => { - if (event.code === 'Space') { - camera_controls.setLookAt(0.7, -0.7, 0.3, 0, 0, 0); - } - }); - sceneTree.metadata.camera_controls = camera_controls; sceneTree.metadata.moveCamera = moveCamera; @@ -383,24 +377,3 @@ export function get_scene_tree() { window.addEventListener('mouseup', onMouseUp, false); return sceneTree; } - -// manages setting up the scene and other logic for keeping state in sync with the server -export function SceneTreeWebSocketListener() { - const socket = useContext(WebSocketContext).socket; - const dispatch = useDispatch(); - - useEffect(() => { - socket.addEventListener('message', (originalCmd) => { - // set the remote description when the offer is received - const cmd = msgpack.decode(new Uint8Array(originalCmd.data)); - if (cmd.type === 'write') { - // write to the store - dispatch({ - type: 'write', - path: cmd.path, - data: cmd.data, - }); - } - }); - }, [socket]); // dependency to call this whenever the websocket changes -} diff --git a/nerfstudio/viewer/app/src/modules/Scene/drawing.js b/nerfstudio/viewer/app/src/modules/Scene/drawing.js index fb5ef7af5..9de449a5e 100644 --- a/nerfstudio/viewer/app/src/modules/Scene/drawing.js +++ b/nerfstudio/viewer/app/src/modules/Scene/drawing.js @@ -1,10 +1,9 @@ /* eslint-disable no-restricted-syntax */ // ---- code for drawing with three.js ---- import * as THREE from 'three'; +import { SceneBoxMessage } from '../WebSocket/ViserMessages'; -export function drawSceneBox(sceneBox): THREE.Object3D { - console.assert(sceneBox.type === 'aabb', 'The scene box must be an AABB'); - +export function drawSceneBox(sceneBox: SceneBoxMessage): THREE.Object3D { const box = sceneBox; const w = 1.0; @@ -20,8 +19,8 @@ export function drawSceneBox(sceneBox): THREE.Object3D { points = points.concat([baa, bab, baa, bba, bab, bbb, bba, bbb]); points = points.concat([aaa, baa, aab, bab, aba, bba, abb, bbb]); - const maxPoint = new THREE.Vector3(...box.max_point); - const minPoint = new THREE.Vector3(...box.min_point); + const maxPoint = new THREE.Vector3(...box.max); + const minPoint = new THREE.Vector3(...box.min); const lengths = maxPoint.clone(); lengths.sub(minPoint); diff --git a/nerfstudio/viewer/app/src/modules/SidePanel/CameraPanel/CameraPanel.jsx b/nerfstudio/viewer/app/src/modules/SidePanel/CameraPanel/CameraPanel.jsx index 478068e6f..1ea1595cd 100644 --- a/nerfstudio/viewer/app/src/modules/SidePanel/CameraPanel/CameraPanel.jsx +++ b/nerfstudio/viewer/app/src/modules/SidePanel/CameraPanel/CameraPanel.jsx @@ -45,14 +45,16 @@ import Tooltip from '@mui/material/Tooltip'; import VideoCameraBackIcon from '@mui/icons-material/VideoCameraBack'; import { CameraHelper } from './CameraHelper'; import { get_curve_object_from_cameras, get_transform_matrix } from './curve'; -import { WebSocketContext } from '../../WebSocket/WebSocket'; +import { + ViserWebSocketContext, + sendWebsocketMessage, + makeThrottledMessageSender, +} from '../../WebSocket/ViserWebSocket'; import RenderModal from '../../RenderModal'; import LoadPathModal from '../../LoadPathModal'; import CameraPropPanel from './CameraPropPanel'; import LevaTheme from '../../../themes/leva_theme.json'; -const msgpack = require('msgpack-lite'); - const FOV_LABELS = { FOV: 'Β°', MM: 'mm', @@ -65,14 +67,13 @@ function set_camera_position(camera, matrix) { } function RenderTimeSelector(props) { + const throttled_time_message_sender = props.throttled_time_message_sender; const disabled = props.disabled; const isGlobal = props.isGlobal; const camera = props.camera; - const dispatch = props.dispatch; const globalRenderTime = props.globalRenderTime; const setGlobalRenderTime = props.setGlobalRenderTime; const applyAll = props.applyAll; - const changeMain = props.changeMain; const setAllCameraRenderTime = props.setAllCameraRenderTime; const getRenderTimeLabel = () => { @@ -89,10 +90,14 @@ function RenderTimeSelector(props) { const [valid, setValid] = React.useState(true); - useEffect( - () => setUIRenderTime(getRenderTimeLabel()), - [camera, globalRenderTime], - ); + useEffect(() => { + setUIRenderTime(getRenderTimeLabel()); + throttled_time_message_sender({ + type: 'TimeConditionMessage', + time: camera.renderTime, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [camera, globalRenderTime]); const setRndrTime = (val) => { if (!isGlobal) { @@ -105,14 +110,10 @@ function RenderTimeSelector(props) { if (applyAll) { setAllCameraRenderTime(val); } - - if (changeMain) { - dispatch({ - type: 'write', - path: 'renderingState/render_time', - data: camera.renderTime, - }); - } + throttled_time_message_sender({ + type: 'TimeConditionMessage', + time: camera.renderTime, + }); }; const handleValidation = (e) => { @@ -147,9 +148,7 @@ function RenderTimeSelector(props) { disabled={disabled} sx={{ input: { - '-webkit-text-fill-color': `${ - disabled ? '#24B6FF' : '#EBEBEB' - } !important`, + WebkitTextFillColor: `${disabled ? '#24B6FF' : '#EBEBEB'} !important`, color: `${disabled ? '#24B6FF' : '#EBEBEB'} !important`, }, }} @@ -199,6 +198,7 @@ function FovSelector(props) { useEffect( () => setUIFieldOfView(getFovLabel()), + // eslint-disable-next-line react-hooks/exhaustive-deps [camera, fovLabel, globalFov], ); @@ -282,9 +282,7 @@ function FovSelector(props) { }} sx={{ input: { - '-webkit-text-fill-color': `${ - disabled ? '#24B6FF' : '#EBEBEB' - } !important`, + WebkitTextFillColor: `${disabled ? '#24B6FF' : '#EBEBEB'} !important`, color: `${disabled ? '#24B6FF' : '#EBEBEB'} !important`, }, }} @@ -297,6 +295,7 @@ function FovSelector(props) { } function CameraList(props) { + const throttled_time_message_sender = props.throttled_time_message_sender; const sceneTree = props.sceneTree; const cameras = props.cameras; const camera_main = props.camera_main; @@ -308,7 +307,6 @@ function CameraList(props) { const cameraProperties = props.cameraProperties; const setCameraProperties = props.setCameraProperties; const isAnimated = props.isAnimated; - const dispatch = props.dispatch; // eslint-disable-next-line no-unused-vars const slider_value = props.slider_value; const set_slider_value = props.set_slider_value; @@ -508,19 +506,16 @@ function CameraList(props) { fovLabel={fovLabel} setFovLabel={setFovLabel} camera={camera} - dispatch={dispatch} disabled={!isAnimated('FOV')} isGlobal={false} - changeMain={false} /> )} {isAnimated('RenderTime') && ( )} {!isAnimated('FOV') && !isAnimated('RenderTime') && ( @@ -556,9 +551,13 @@ export default function CameraPanel(props) { ]); // redux store state - const export_path = useSelector((state) => state.renderingState.export_path); + const export_path = useSelector( + (state) => state.file_path_info.export_path_name, + ); - const websocket = useContext(WebSocketContext).socket; + const viser_websocket = useContext(ViserWebSocketContext); + const throttled_time_message_sender = + makeThrottledMessageSender(viser_websocket); const DEFAULT_FOV = 50; const DEFAULT_RENDER_TIME = 0.0; @@ -592,6 +591,7 @@ export default function CameraPanel(props) { (value) => setMouseInScene(value), 'mouse_in_scene', ); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const dispatch = useDispatch(); @@ -612,16 +612,9 @@ export default function CameraPanel(props) { const crop_center = useSelector((state) => state.renderingState.crop_center); const crop_scale = useSelector((state) => state.renderingState.crop_scale); - const [display_render_time, set_display_render_time] = React.useState(false); - - const receive_temporal_dist = (e) => { - const msg = msgpack.decode(new Uint8Array(e.data)); - if (msg.path === '/model/has_temporal_distortion') { - set_display_render_time(msg.data === 'true'); - websocket.removeEventListener('message', receive_temporal_dist); - } - }; - websocket.addEventListener('message', receive_temporal_dist); + const display_render_time = useSelector( + (state) => state.renderingState.use_time_conditioning, + ); const setRenderHeight = (value) => { dispatch({ @@ -654,48 +647,6 @@ export default function CameraPanel(props) { }); }; - const setCropEnabled = (value) => { - console.log('setting the crop enabled value to: ', value); - dispatch({ - type: 'write', - path: 'renderingState/crop_enabled', - data: value, - }); - console.log('crop enabled value is now: ', crop_enabled); - }; - - const serCropBgColor = (value) => { - dispatch({ - type: 'write', - path: 'renderingState/crop_bg_color', - data: value, - }); - }; - - const setCropCenter = (value) => { - dispatch({ - type: 'write', - path: 'renderingState/crop_center', - data: value, - }); - }; - - const setCropScale = (value) => { - dispatch({ - type: 'write', - path: 'renderingState/crop_scale', - data: value, - }); - }; - - const setRenderTime = (value) => { - dispatch({ - type: 'write', - path: 'renderingState/render_time', - data: parseFloat(value), - }); - }; - // ui state const [fovLabel, setFovLabel] = React.useState(FOV_LABELS.FOV); @@ -712,7 +663,6 @@ export default function CameraPanel(props) { if (new_camera_list.length >= 1) { set_camera_position(camera_render, new_camera_list[0].matrix); setFieldOfView(new_camera_list[0].fov); - setRenderTime(new_camera_list[0].renderTime); set_slider_value(slider_min); } }; @@ -851,6 +801,7 @@ export default function CameraPanel(props) { camera_helper, ); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [cameras, cameraProperties, render_width, render_height]); // update the camera curve @@ -959,15 +910,20 @@ export default function CameraPanel(props) { let lookat = null; let up = null; let fov = null; + let render_time = null; position = curve_object.curve_positions.getPoint(point); lookat = curve_object.curve_lookats.getPoint(point); up = curve_object.curve_ups.getPoint(point); fov = curve_object.curve_fovs.getPoint(point).z; + render_time = curve_object.curve_render_times.getPoint(point).z; + render_time = Math.max(Math.min(render_time, 1.0), 0.0); // clamp time values to [0, 1] const mat = get_transform_matrix(position, lookat, up); set_camera_position(camera_render, mat); setFieldOfView(fov); setGlobalFov(fov); + setGlobalRenderTime(render_time); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [slider_value, render_height, render_width]); // call this function whenever slider state changes @@ -979,6 +935,7 @@ export default function CameraPanel(props) { return () => clearInterval(interval); } return () => {}; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [is_playing]); // make sure to pause if the slider reaches the end @@ -1035,7 +992,11 @@ export default function CameraPanel(props) { let crop = null; if (crop_enabled) { crop = { - crop_bg_color, + crop_bg_color: { + r: crop_bg_color[0], + g: crop_bg_color[1], + b: crop_bg_color[2], + }, crop_center, crop_scale, }; @@ -1062,6 +1023,8 @@ export default function CameraPanel(props) { // inspired by: // https://stackoverflow.com/questions/55613438/reactwrite-to-json-file-or-export-download-no-server + sendWebsocketMessage(viser_websocket, { type: 'SaveCheckpointMessage' }); + const camera_path_object = get_camera_path(); console.log(camera_render.toJSON()); @@ -1121,11 +1084,14 @@ export default function CameraPanel(props) { reset_slider_render_on_add(new_camera_list); if ('crop' in camera_path_object && camera_path_object.crop !== null) { - console.log('crop' in camera_path_object); - setCropEnabled(true); - serCropBgColor(camera_path_object.crop.crop_bg_color); - setCropCenter(camera_path_object.crop.crop_center); - setCropScale(camera_path_object.crop.crop_scale); + const bg_color = camera_path_object.crop.crop_bg_color; + sendWebsocketMessage(viser_websocket, { + type: 'CropParamsMessage', + crop_enabled: true, + crop_bg_color: [bg_color.r, bg_color.g, bg_color.b], + crop_center: camera_path_object.crop.crop_center, + crop_scale: camera_path_object.crop.crop_scale, + }); } }; @@ -1145,33 +1111,17 @@ export default function CameraPanel(props) { setRenderModalOpen(true); const camera_path_object = get_camera_path(); - const camera_path_payload = { + + sendWebsocketMessage(viser_websocket, { + type: 'CameraPathPayloadMessage', camera_path_filename: export_path, camera_path: camera_path_object, - }; - - // send a command of the websocket to save the trajectory somewhere! - if (websocket.readyState === WebSocket.OPEN) { - const data = { - type: 'write', - path: 'camera_path_payload', - data: camera_path_payload, - }; - const message = msgpack.encode(data); - websocket.send(message); - } + }); + sendWebsocketMessage(viser_websocket, { type: 'SaveCheckpointMessage' }); }; const open_load_path_modal = () => { - if (websocket.readyState === WebSocket.OPEN) { - const data = { - type: 'write', - path: 'populate_paths_payload', - data: true, - }; - const message = msgpack.encode(data); - websocket.send(message); - } + sendWebsocketMessage(viser_websocket, { type: 'CameraPathOptionsRequest' }); setLoadPathModalOpen(true); }; @@ -1303,6 +1253,7 @@ export default function CameraPanel(props) {
state.renderingState.camera_type); - const export_path = useSelector((state) => state.renderingState.export_path); + const export_path = useSelector( + (state) => state.file_path_info.export_path_name, + ); const setExportPath = (value) => { dispatch({ type: 'write', - path: 'renderingState/export_path', + path: 'file_path_info/export_path_name', data: value, }); }; @@ -59,14 +61,12 @@ export default function CameraPropPanel(props) { value: export_path, onChange: (v) => { const valid_filename_reg = /^([a-z]|[A-Z]|[0-9]|-|_)+$/g; - if(!valid_filename_reg.test(v)){ - alert("Please only use letters, numbers, and hyphens"); - } - else { + if (!valid_filename_reg.test(v)) { + alert('Please only use letters, numbers, and hyphens'); + } else { setExportPath(v); } }, - }, camera_resolution: { label: 'Resolution', @@ -109,7 +109,7 @@ export default function CameraPropPanel(props) { { store }, ); - setControls({path: export_path}); + setControls({ path: export_path }); setControls({ video_fps: fps }); setControls({ video_duration: seconds }); setControls({ diff --git a/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/ExportPanel.jsx b/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/ExportPanel.jsx index ac998daa2..30e390b34 100644 --- a/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/ExportPanel.jsx +++ b/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/ExportPanel.jsx @@ -30,7 +30,7 @@ function TabPanel(props: TabPanelProps) { > {value === index && ( - {children} + {children} )}
@@ -49,9 +49,9 @@ const CLIPPING_BOX_NAME = 'Clipping Box'; export default function ExportPanel(props) { // unpack relevant information const sceneTree = props.sceneTree; - const showExportBox = props.showExportBox; + const showExportBox = useSelector((state) => state.show_export_box); - const [type_value, setTypeValue] = React.useState(0); + const [type_value, setTypeValue] = React.useState(false); const handleTypeChange = (event: React.SyntheticEvent, newValue: number) => { setTypeValue(newValue); diff --git a/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/MeshSubPanel.jsx b/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/MeshSubPanel.jsx index 3eb2725af..c322ce971 100644 --- a/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/MeshSubPanel.jsx +++ b/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/MeshSubPanel.jsx @@ -33,7 +33,7 @@ export default function MeshSubPanel(props) { // redux store state const config_base_dir = useSelector( - (state) => state.renderingState.config_base_dir, + (state) => state.file_path_info.config_base_dir, ); const clippingEnabled = useSelector( (state) => state.renderingState.clipping_enabled, @@ -132,9 +132,6 @@ export default function MeshSubPanel(props) { return boolean ? 'True' : 'False'; }; - console.log(controlValues.outputDir); - console.log(getPythonBool(controlValues.removeOutliers)); - const bbox_min = [ clippingCenter[0] - clippingScale[0] / 2, clippingCenter[1] - clippingScale[1] / 2, @@ -147,7 +144,6 @@ export default function MeshSubPanel(props) { ]; const mesh_method_choice = controlValues.mesh_method_options; - console.log(mesh_method_choice); let cmd = ''; if (mesh_method_choice === 'tsdf') { cmd = @@ -192,7 +188,7 @@ export default function MeshSubPanel(props) { inner_html = (
You need to train a model with normals to use poisson meshing. Try tsdf - or retrain the model with normals. You can use nerfacto with
+ or retrain the model with normals. You can use nerfacto with
--pipeline.model.predict-normals True
); diff --git a/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/PointcloudSubPanel.jsx b/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/PointcloudSubPanel.jsx index 480f3694c..9a8f7177e 100644 --- a/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/PointcloudSubPanel.jsx +++ b/nerfstudio/viewer/app/src/modules/SidePanel/ExportPanel/PointcloudSubPanel.jsx @@ -15,7 +15,7 @@ export default function PointcloudSubPanel(props) { // redux store state const config_base_dir = useSelector( - (state) => state.renderingState.config_base_dir, + (state) => state.file_path_info.config_base_dir, ); const clippingEnabled = useSelector( (state) => state.renderingState.clipping_enabled, diff --git a/nerfstudio/viewer/app/src/modules/SidePanel/ScenePanel/ScenePanel.jsx b/nerfstudio/viewer/app/src/modules/SidePanel/ScenePanel/ScenePanel.jsx index 233520125..254d75008 100644 --- a/nerfstudio/viewer/app/src/modules/SidePanel/ScenePanel/ScenePanel.jsx +++ b/nerfstudio/viewer/app/src/modules/SidePanel/ScenePanel/ScenePanel.jsx @@ -64,11 +64,13 @@ function MenuItems(props: ListItemProps) { return null; } + // eslint-disable-next-line react-hooks/rules-of-hooks const [open, setOpen] = React.useState(true); const toggleOpen = () => { setOpen(!open); }; + // eslint-disable-next-line react-hooks/rules-of-hooks const [visible, setVisible] = React.useState(groupVisible); if (terminal) { scene_node.object.visible = visible; @@ -77,16 +79,15 @@ function MenuItems(props: ListItemProps) { const toggleVisible = (e) => { e.stopPropagation(); setVisible(!visible); - scene_node.object.traverse( - (obj) => { - if (obj.name === 'CAMERA_LABEL') { - // eslint-disable-next-line no-param-reassign - obj.visible = !visible; - } + scene_node.object.traverse((obj) => { + if (obj.name === 'CAMERA_LABEL') { + // eslint-disable-next-line no-param-reassign + obj.visible = !visible; } - ); + }); }; + // eslint-disable-next-line react-hooks/rules-of-hooks React.useEffect(() => { setVisible(groupVisible); }, [groupVisible]); diff --git a/nerfstudio/viewer/app/src/modules/SidePanel/SidePanel.jsx b/nerfstudio/viewer/app/src/modules/SidePanel/SidePanel.jsx index 35dd126d6..1e7db37e3 100644 --- a/nerfstudio/viewer/app/src/modules/SidePanel/SidePanel.jsx +++ b/nerfstudio/viewer/app/src/modules/SidePanel/SidePanel.jsx @@ -5,19 +5,16 @@ import * as THREE from 'three'; import Box from '@mui/material/Box'; import Divider from '@mui/material/Divider'; -import { Leva } from 'leva'; import Tab from '@mui/material/Tab'; import Tabs from '@mui/material/Tabs'; -import Typography from '@mui/material/Typography'; import { CameraAltRounded, TuneRounded, WidgetsRounded, ImportExportRounded, } from '@mui/icons-material/'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import StatusPanel from './StatusPanel'; -import LevaTheme from '../../themes/leva_theme.json'; import CameraPanel from './CameraPanel'; import ScenePanel from './ScenePanel'; import { RenderControls } from '../ConfigPanel/ConfigPanel'; @@ -39,31 +36,6 @@ export const snap_to_camera = (sceneTree, camera, matrix) => { ); }; -interface TabPanelProps { - children: React.ReactNode; - index: number; - value: number; -} - -function TabPanel(props: TabPanelProps) { - const { children, value, index, ...other } = props; - - return ( - - ); -} - function a11yProps(index: number) { return { id: `simple-tab-${index}`, @@ -71,6 +43,92 @@ function a11yProps(index: number) { }; } +interface PanelTabContentsProps { + children: React.ReactNode; + index: number; + value: number; +} + +/** One tab in the control panel. */ +function PanelTabContents(props: PanelTabContentsProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +interface PanelContentsProps { + children: React.ReactNode; +} + +function PanelContents(props: PanelContentsProps) { + const dispatch = useDispatch(); + const [tabState, setTabState] = React.useState(0); + const handleChange = (_event: React.SyntheticEvent, newValue: number) => { + setTabState(newValue); + dispatch({ + type: 'write', + path: 'show_export_box', + data: newValue === 3, + }); + }; + const camera_choice = useSelector( + (state) => state.renderingState.camera_choice, + ); + const arrayChildren = React.Children.toArray(props.children); + + React.useEffect(() => { + if (camera_choice === 'Render Camera') { + setTabState(1); + } + }, [camera_choice]); + + return ( + <> + + + } label="Controls" {...a11yProps(0)} /> + } label="Render" {...a11yProps(1)} /> + } + label="Scene" + disabled={camera_choice === 'Render Camera'} + {...a11yProps(2)} + /> + } + label="Export" + disabled={camera_choice === 'Render Camera'} + {...a11yProps(3)} + /> + + + + {arrayChildren.map((child, index) => ( + // eslint-disable-next-line react/no-array-index-key + + {child} + + ))} + + ); +} + interface BasicTabsProps { sceneTree: object; } @@ -78,78 +136,17 @@ interface BasicTabsProps { export function BasicTabs(props: BasicTabsProps) { const sceneTree = props.sceneTree; - const [value, setValue] = React.useState(0); - const [showExportBox, setShowExportBox] = React.useState(false); - - const handleChange = (event: React.SyntheticEvent, newValue: number) => { - setValue(newValue); - setShowExportBox(newValue === 3); - }; - const camera_choice = useSelector( - (state) => state.renderingState.camera_choice, - ); - - React.useEffect(() => { - if (camera_choice === 'Render Camera') { - setValue(1); - } - }, [camera_choice]); - return (
- - - } label="Controls" {...a11yProps(0)} /> - } label="Render" {...a11yProps(1)} /> - } - label="Scene" - disabled={camera_choice === 'Render Camera'} - {...a11yProps(2)} - /> - } - label="Export" - disabled={camera_choice === 'Render Camera'} - {...a11yProps(3)} - /> - - - -
- - -
-
- - - - -
- -
-
- - - - + + + + + +
); diff --git a/nerfstudio/viewer/app/src/modules/SidePanel/StatusPanel/StatusPanel.jsx b/nerfstudio/viewer/app/src/modules/SidePanel/StatusPanel/StatusPanel.jsx index b80446664..d89cb8eeb 100644 --- a/nerfstudio/viewer/app/src/modules/SidePanel/StatusPanel/StatusPanel.jsx +++ b/nerfstudio/viewer/app/src/modules/SidePanel/StatusPanel/StatusPanel.jsx @@ -4,12 +4,13 @@ import Button from '@mui/material/Button'; import { useDispatch, useSelector } from 'react-redux'; import PauseIcon from '@mui/icons-material/Pause'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import CelebrationOutlinedIcon from '@mui/icons-material/CelebrationOutlined'; import ViewInArIcon from '@mui/icons-material/ViewInAr'; import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'; - -import { WebSocketContext } from '../../WebSocket/WebSocket'; - -const msgpack = require('msgpack-lite'); +import { + ViserWebSocketContext, + sendWebsocketMessage, +} from '../../WebSocket/ViserWebSocket'; interface StatusPanelProps { sceneTree: object; @@ -17,17 +18,20 @@ interface StatusPanelProps { export default function StatusPanel(props: StatusPanelProps) { const dispatch = useDispatch(); - const websocket = React.useContext(WebSocketContext).socket; - const isTraining = useSelector((state) => state.renderingState.isTraining); + const viser_websocket = React.useContext(ViserWebSocketContext); + const training_state = useSelector( + (state) => state.renderingState.training_state, + ); const sceneTree = props.sceneTree; const isWebsocketConnected = useSelector( (state) => state.websocketState.isConnected, ); +<<<<<<< HEAD +======= + const step = useSelector((state) => state.renderingState.step); +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb const eval_res = useSelector((state) => state.renderingState.eval_res); - const vis_train_ratio = useSelector( - (state) => state.renderingState.vis_train_ratio, - ); const camera_choice = useSelector( (state) => state.renderingState.camera_choice, ); @@ -51,25 +55,29 @@ export default function StatusPanel(props: StatusPanelProps) { obj.visible = is_scene_visible && camera_choice === 'Main Camera'; } }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [camera_choice, is_scene_visible]); const handlePlayChange = () => { + let new_state = null; + if (training_state === 'training') { + new_state = 'paused'; + } else if (training_state === 'paused') { + new_state = 'training'; + } else { + return; + } dispatch({ type: 'write', - path: 'renderingState/isTraining', - data: !isTraining, + path: 'renderingState/training_state', + data: new_state, + }); + sendWebsocketMessage(viser_websocket, { + type: 'TrainingStateMessage', + training_state: new_state, }); - // write to server - const cmd = 'write'; - const path = 'renderingState/isTraining'; - const data = { - type: cmd, - path, - data: !isTraining, - }; - const message = msgpack.encode(data); - websocket.send(message); }; +<<<<<<< HEAD const is_training_text = isTraining ? 'Pause Training' : 'Resume Training'; const training_icon = isTraining ? : ; @@ -77,6 +85,22 @@ export default function StatusPanel(props: StatusPanelProps) { ? 'Renderer Connected' : 'Renderer Disconnected'; const websocket_connected_color = isWebsocketConnected ? 'success' : 'error'; +======= + let is_training_text = ''; + let training_icon = null; + let color = 'secondary'; + if (training_state === 'training') { + is_training_text = 'Pause Training'; + training_icon = ; + } else if (training_state === 'paused') { + is_training_text = 'Resume Training'; + training_icon = ; + } else { + is_training_text = 'Training Complete'; + color = 'success'; + training_icon = ; + } +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb return (
@@ -84,7 +108,7 @@ export default function StatusPanel(props: StatusPanelProps) { +======= +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb
); } diff --git a/nerfstudio/viewer/app/src/modules/ViewerWindow/ViewerWindow.jsx b/nerfstudio/viewer/app/src/modules/ViewerWindow/ViewerWindow.jsx index ea6196a14..999089f9d 100644 --- a/nerfstudio/viewer/app/src/modules/ViewerWindow/ViewerWindow.jsx +++ b/nerfstudio/viewer/app/src/modules/ViewerWindow/ViewerWindow.jsx @@ -11,10 +11,18 @@ import PublicSharpIcon from '@mui/icons-material/PublicSharp'; import SyncOutlinedIcon from '@mui/icons-material/SyncOutlined'; import ThreeDRotationIcon from '@mui/icons-material/ThreeDRotation'; import VideoCameraBackIcon from '@mui/icons-material/VideoCameraBackOutlined'; +<<<<<<< HEAD import { WebSocketContext } from '../WebSocket/WebSocket'; import RenderWindow from '../RenderWindow/RenderWindow'; const msgpack = require('msgpack-lite'); +======= +import { isEqual } from 'lodash'; +import { + makeThrottledMessageSender, + ViserWebSocketContext, +} from '../WebSocket/ViserWebSocket'; +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb function CameraToggle() { const dispatch = useDispatch(); @@ -108,7 +116,7 @@ export default function ViewerWindow(props) { const labelRenderer = sceneTree.metadata.labelRenderer; const myRef = useRef(null); - const websocket = useContext(WebSocketContext).socket; + const viser_websocket = useContext(ViserWebSocketContext); const field_of_view = useSelector( (state) => state.renderingState.field_of_view, ); @@ -162,32 +170,45 @@ export default function ViewerWindow(props) { }; useEffect(() => { - function update_dimensions() { + const handleNewDimensions = () => { setDimensions({ height: get_window_height(), width: get_window_width(), }); - } + }; - window.addEventListener('resize', update_dimensions); - - // force a rerender setDimensions({ height: get_window_height(), width: get_window_width(), }); render(); + window.addEventListener('resize', handleNewDimensions); return () => { - window.removeEventListener('resize', update_dimensions); + window.removeEventListener('resize', handleNewDimensions); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // start the three.js rendering loop // when the DOM is ready useEffect(() => { + document.getElementById("background-image").onload = function () { + if (scene) { + const oldBackground = scene.background; + const texture = new THREE.Texture(this); + texture.minFilter = THREE.LinearFilter; + texture.magFilter = THREE.NearestFilter; + texture.needsUpdate = true; + scene.background = texture; + if (oldBackground) { + oldBackground.dispose(); + } + } + } myRef.current.append(renderer.domElement); render(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const render_height = useSelector( @@ -201,17 +222,17 @@ export default function ViewerWindow(props) { let crop_h; const render_aspect = render_width / render_height; const viewport_aspect = viewport_width / viewport_height; - let render_viewport_apsect_ratio = null; + let render_viewport_aspect_ratio = null; if (render_aspect > viewport_aspect) { // render width is the limiting factor crop_w = viewport_width; crop_h = viewport_width / render_aspect; - render_viewport_apsect_ratio = viewport_aspect / render_aspect; + render_viewport_aspect_ratio = viewport_aspect / render_aspect; } else { // render height is the limiting factor crop_w = viewport_height * render_aspect; crop_h = viewport_height; - render_viewport_apsect_ratio = 1.0; + render_viewport_aspect_ratio = 1.0; } let display = null; @@ -231,34 +252,41 @@ export default function ViewerWindow(props) { // such that the rendered video will match correctly if (camera_choice !== 'Main Camera') { const fl = 1.0 / Math.tan((field_of_view * Math.PI) / 360); - const fl_new = fl * render_viewport_apsect_ratio; + const fl_new = fl * render_viewport_aspect_ratio; const fov = Math.atan(1 / fl_new) / (Math.PI / 360); sceneTree.metadata.camera.fov = fov; } else { sceneTree.metadata.camera.fov = 50; } + let old_camera_matrix = null; + let is_moving = false; + const sendThrottledCameraMessage = makeThrottledMessageSender( + viser_websocket, + 25, + ); // update the camera information in the python server const sendCamera = () => { - if (websocket.readyState === WebSocket.OPEN) { - const cmd = 'write'; - const path = 'renderingState/camera'; - const data_packet = sceneTree.metadata.camera.toJSON(); - data_packet.object.timestamp = +new Date(); - if (camera_choice === 'Render Camera') { - data_packet.object.camera_type = camera_type; + if (isEqual(old_camera_matrix, sceneTree.metadata.camera.matrix.elements)) { + if (is_moving) { + is_moving = false; } else { - data_packet.object.camera_type = 'perspective'; + return; } - data_packet.object.render_aspect = render_aspect; - const data = { - type: cmd, - path, - data: data_packet, - }; - const message = msgpack.encode(data); - websocket.send(message); + } else { + is_moving = true; } + old_camera_matrix = sceneTree.metadata.camera.matrix.elements.slice(); + sendThrottledCameraMessage({ + type: 'CameraMessage', + aspect: sceneTree.metadata.camera.aspect, + render_aspect, + fov: sceneTree.metadata.camera.fov, + matrix: old_camera_matrix, + camera_type, + is_moving, + timestamp: +new Date(), + }); }; // keep sending the camera often @@ -270,11 +298,45 @@ export default function ViewerWindow(props) { return () => { clearInterval(refreshIntervalId); }; - }, [websocket, camera_choice, camera_type, render_aspect]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [viser_websocket, camera_choice, camera_type, render_aspect]); + + const isWebsocketConnected = useSelector( + (state) => state.websocketState.isConnected, + ); + useEffect(() => { + if (isWebsocketConnected) { + sendThrottledCameraMessage({ + type: 'CameraMessage', + aspect: sceneTree.metadata.camera.aspect, + render_aspect, + fov: sceneTree.metadata.camera.fov, + matrix: sceneTree.metadata.camera.matrix.elements.slice(), + camera_type, + is_moving: false, + timestamp: +new Date(), + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isWebsocketConnected]); return ( <> +<<<<<<< HEAD +======= +
+ +
+ +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb
diff --git a/nerfstudio/viewer/app/src/modules/WebSocket/ViserMessages.tsx b/nerfstudio/viewer/app/src/modules/WebSocket/ViserMessages.tsx new file mode 100644 index 000000000..965e59ab6 --- /dev/null +++ b/nerfstudio/viewer/app/src/modules/WebSocket/ViserMessages.tsx @@ -0,0 +1,128 @@ +// AUTOMATICALLY GENERATED message interfaces, from Python dataclass definitions. +// This file should not be manually modified. +interface BackgroundImageMessage { + type: "BackgroundImageMessage"; + media_type: 'image/jpeg' | 'image/png'; + base64_data: string; +} +interface GuiAddMessage { + type: "GuiAddMessage"; + name: string; + folder_labels: string[]; + leva_conf: any; +} +interface GuiRemoveMessage { + type: "GuiRemoveMessage"; + name: string; +} +interface GuiUpdateMessage { + type: "GuiUpdateMessage"; + name: string; + value: any; +} +interface GuiSetHiddenMessage { + type: "GuiSetHiddenMessage"; + name: string; + hidden: boolean; +} +interface GuiSetValueMessage { + type: "GuiSetValueMessage"; + name: string; + value: any; +} +interface GuiSetLevaConfMessage { + type: "GuiSetLevaConfMessage"; + name: string; + leva_conf: any; +} +interface FilePathInfoMessage { + type: "FilePathInfoMessage"; + config_base_dir: string; + data_base_dir: string; + export_path_name: string; +} +interface CameraMessage { + type: "CameraMessage"; + aspect: number; + render_aspect: number; + fov: number; + matrix: [number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]; + camera_type: 'perspective' | 'fisheye' | 'equirectangular'; + is_moving: boolean; + timestamp: number; +} +interface SceneBoxMessage { + type: "SceneBoxMessage"; + min: [number, number, number]; + max: [number, number, number]; +} +interface DatasetImageMessage { + type: "DatasetImageMessage"; + idx: string; + json: any; +} +interface TrainingStateMessage { + type: "TrainingStateMessage"; + training_state: 'training' | 'paused' | 'completed'; +} +interface CameraPathPayloadMessage { + type: "CameraPathPayloadMessage"; + camera_path_filename: string; + camera_path: any; +} +interface CameraPathOptionsRequest { + type: "CameraPathOptionsRequest"; +} +interface CameraPathsMessage { + type: "CameraPathsMessage"; + payload: any; +} +interface CropParamsMessage { + type: "CropParamsMessage"; + crop_enabled: boolean; + crop_bg_color: [number, number, number]; + crop_center: [number, number, number]; + crop_scale: [number, number, number]; +} +interface StatusMessage { + type: "StatusMessage"; + eval_res: string; + step: number; +} +interface SaveCheckpointMessage { + type: "SaveCheckpointMessage"; +} +interface UseTimeConditioningMessage { + type: "UseTimeConditioningMessage"; +} +interface TimeConditionMessage { + type: "TimeConditionMessage"; + time: number; +} +interface OutputOptionsMessage { + type: "OutputOptionsMessage"; + options: any; +} + +export type Message = + | BackgroundImageMessage + | GuiAddMessage + | GuiRemoveMessage + | GuiUpdateMessage + | GuiSetHiddenMessage + | GuiSetValueMessage + | GuiSetLevaConfMessage + | FilePathInfoMessage + | CameraMessage + | SceneBoxMessage + | DatasetImageMessage + | TrainingStateMessage + | CameraPathPayloadMessage + | CameraPathOptionsRequest + | CameraPathsMessage + | CropParamsMessage + | StatusMessage + | SaveCheckpointMessage + | UseTimeConditioningMessage + | TimeConditionMessage + | OutputOptionsMessage; diff --git a/nerfstudio/viewer/app/src/modules/WebSocket/ViserWebSocket.tsx b/nerfstudio/viewer/app/src/modules/WebSocket/ViserWebSocket.tsx new file mode 100644 index 000000000..0b5b8f2e9 --- /dev/null +++ b/nerfstudio/viewer/app/src/modules/WebSocket/ViserWebSocket.tsx @@ -0,0 +1,387 @@ +import React, { useEffect, MutableRefObject, Dispatch } from 'react'; + +import AwaitLock from 'await-lock'; +import { pack, unpack } from 'msgpackr'; +import { useDispatch, useStore, useSelector } from 'react-redux'; +import { Store } from 'redux'; +import { Message } from './ViserMessages'; + +const ViserWebSocketContext = + React.createContext | null>(null); + +export { ViserWebSocketContext }; + +/** Send message over websocket. */ +export function sendWebsocketMessage( + websocketRef: MutableRefObject, + message: Message, +) { + if (websocketRef.current === null) return; + websocketRef.current!.send(pack(message)); +} + +/** Returns a function for sending messages, with automatic throttling. */ +export function makeThrottledMessageSender( + websocketRef: MutableRefObject, + throttleMilliseconds: number, +) { + let readyToSend = true; + let stale = false; + let latestMessage: Message | null = null; + + function send(message: Message) { + if (websocketRef.current == null) return; + latestMessage = message; + if (readyToSend) { + websocketRef.current!.send(pack(message)); + stale = false; + readyToSend = false; + + setTimeout(() => { + readyToSend = true; + if (!stale) return; + send(latestMessage!); + }, throttleMilliseconds); + } else { + stale = true; + } + } + return send; +} + +function handleMessage( + message: Message, + dispatch: Dispatch, + store: Store, +) { + switch (message.type) { + // Add a background image. + case 'BackgroundImageMessage': { + document + .getElementById('background-image')! + .setAttribute( + 'src', + `data:${message.media_type};base64,${message.base64_data}`, + ); + break; + } + // Add a GUI input. + case 'GuiAddMessage': { + const curGuiNames = store.getState().custom_gui.guiNames; + const curGuiConfigFromName = + store.getState().custom_gui.guiConfigFromName; + dispatch({ + type: 'write', + path: 'custom_gui/guiNames', + data: [...curGuiNames, message.name], + }); + dispatch({ + type: 'write', + path: 'custom_gui/guiConfigFromName', + data: { + ...curGuiConfigFromName, + [message.name]: { + folderLabels: message.folder_labels, + levaConf: message.leva_conf, + hidden: false, + }, + }, + }); + break; + } + // Set the hidden state of a GUI input. + case 'GuiSetHiddenMessage': { + const curGuiConfigFromName = + store.getState().custom_gui.guiConfigFromName; + const currentConf = curGuiConfigFromName[message.name]; + if (currentConf !== undefined) { + dispatch({ + type: 'write', + path: 'custom_gui/guiConfigFromName', + data: { + ...curGuiConfigFromName, + [message.name]: { + ...currentConf, + hidden: message.hidden, + }, + }, + }); + } + break; + } + // Set the value of a GUI input. + case 'GuiSetValueMessage': { + const curGuiConfigFromName = + store.getState().custom_gui.guiConfigFromName; + const currentConf = curGuiConfigFromName[message.name]; + if (currentConf !== undefined) { + dispatch({ + type: 'write', + path: 'custom_gui/guiConfigFromName', + data: { + ...curGuiConfigFromName, + [message.name]: { + ...currentConf, + value: message.value, + }, + }, + }); + // To propagate change to the leva element, need to add to the guiSetQueue + const curSetQueue = store.getState().custom_gui.guiSetQueue; + dispatch({ + type: 'write', + path: 'custom_gui/guiSetQueue', + data: { + ...curSetQueue, + [message.name]: message.value, + }, + }); + } + break; + } + // Set leva conf of element. + case 'GuiSetLevaConfMessage': { + const curGuiConfigFromName = + store.getState().custom_gui.guiConfigFromName; + const currentConf = curGuiConfigFromName[message.name]; + if (currentConf !== undefined) { + dispatch({ + type: 'write', + path: 'custom_gui/guiConfigFromName', + data: { + ...curGuiConfigFromName, + [message.name]: { + ...currentConf, + levaConf: message.leva_conf, + }, + }, + }); + } + break; + } + // Remove a GUI input. + case 'GuiRemoveMessage': { + // TODO: not implemented. + break; + } + // Update scene box. + case 'SceneBoxMessage': { + dispatch({ + type: 'write', + path: 'sceneState/sceneBox', + data: message, + }); + break; + } + // Add dataset image. + case 'DatasetImageMessage': { + const dataset_path = `sceneState/cameras/${message.idx}`; + dispatch({ + type: 'write', + path: dataset_path, + data: message.json, + }); + break; + } + // Set training value. + case 'TrainingStateMessage': { + dispatch({ + type: 'write', + path: 'renderingState/training_state', + data: message.training_state, + }); + break; + } + // Populate camera paths. + case 'CameraPathsMessage': { + dispatch({ + type: 'write', + path: 'all_camera_paths', + data: message.payload, + }); + break; + } + // Set file path info. + case 'FilePathInfoMessage': { + dispatch({ + type: 'write', + path: 'file_path_info/config_base_dir', + data: message.config_base_dir, + }); + dispatch({ + type: 'write', + path: 'file_path_info/data_base_dir', + data: message.data_base_dir, + }); + dispatch({ + type: 'write', + path: 'file_path_info/export_path_name', + data: message.export_path_name, + }); + break; + } + // Set crop parameters + case 'CropParamsMessage': { + dispatch({ + type: 'write', + path: 'renderingState/crop_enabled', + data: message.crop_enabled, + }); + dispatch({ + type: 'write', + path: 'renderingState/crop_bg_color', + data: message.crop_bg_color, + }); + dispatch({ + type: 'write', + path: 'renderingState/crop_scale', + data: message.crop_scale, + }); + dispatch({ + type: 'write', + path: 'renderingState/crop_center', + data: message.crop_center, + }); + break; + } + // Handle status messages. + case 'StatusMessage': { + dispatch({ + type: 'write', + path: 'renderingState/eval_res', + data: message.eval_res, + }); + dispatch({ + type: 'write', + path: 'renderingState/step', + data: message.step, + }); + break; + } + // Handle time conditioning messages. + case 'UseTimeConditioningMessage': { + console.log('HERERERE'); + dispatch({ + type: 'write', + path: 'renderingState/use_time_conditioning', + data: true, + }); + break; + } + case 'TimeConditionMessage': { + dispatch({ + type: 'write', + path: 'renderingState/time_condition', + data: message.time, + }); + break; + } + case 'OutputOptionsMessage': { + dispatch({ + type: 'write', + path: 'renderingState/output_options', + data: message.options, + }); + break; + } + default: { + console.log('Received message did not match any known types:', message); + break; + } + } +} + +export function ViserWebSocket({ children }: { children: React.ReactNode }) { + const dispatch = useDispatch(); + const store = useStore(); + + const ws = React.useRef(null); + + const websocket_url = useSelector( + (state) => state.websocketState.websocket_url, + ); + + useEffect(() => { + // Lock for making sure messages are handled in order. + const orderLock = new AwaitLock(); + + let done = false; + + function tryConnect(): void { + if (done) return; + + ws.current = new WebSocket(websocket_url); + + const connecting_timeout = setTimeout(() => { + if ( + ws.current != null && + ws.current.readyState === WebSocket.CONNECTING + ) { + ws.current.close(); + console.log('WebSocket connection timed out'); + } + }, 5000); // timeout after 5 seconds + + ws.current.onopen = () => { + console.log(`Viser connected! ${websocket_url}`); + clearTimeout(connecting_timeout); + dispatch({ + type: 'write', + path: 'websocketState/isConnected', + data: true, + }); + }; + + ws.current.onclose = () => { + console.log(`Viser disconnected! ${websocket_url}`); + dispatch({ + type: 'write', + path: 'websocketState/isConnected', + data: false, + }); + + // Try to reconnect. + // eslint-disable-next-line no-use-before-define + timeout = setTimeout(tryConnect, 1000); + }; + + ws.current.onmessage = async (event) => { + // Reduce websocket backpressure. + try { + const message = (await unpack( + new Uint8Array(await event.data.arrayBuffer()), + )) as Message; + await orderLock.acquireAsync({ timeout: 1000 }); + handleMessage(message, dispatch, store); + } catch (error) { + console.error(`Error handling message: ${error}`); + } finally { + if (orderLock.acquired) { + orderLock.release(); + } + } + }; + + // add websocket error handling + ws.current.onerror = (err) => { + console.log('Websocket error: ', err); + }; + } + + let timeout = setTimeout(tryConnect, 500); + return () => { + clearTimeout(timeout); + if (ws.current) { + done = true; + ws.current.close(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [websocket_url]); + + return ( + + {children} + + ); +} diff --git a/nerfstudio/viewer/app/src/modules/WebSocket/WebSocket.jsx b/nerfstudio/viewer/app/src/modules/WebSocket/WebSocket.jsx deleted file mode 100644 index 0e265ef60..000000000 --- a/nerfstudio/viewer/app/src/modules/WebSocket/WebSocket.jsx +++ /dev/null @@ -1,81 +0,0 @@ -// Much of this code comes from or is inspired by: -// https://www.pluralsight.com/guides/using-web-sockets-in-your-reactredux-app - -import React, { createContext, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import PropTypes from 'prop-types'; - -const WebSocketContext = createContext(null); - -export { WebSocketContext }; - -export default function WebSocketContextFunction({ children }) { - const dispatch = useDispatch(); - let ws = null; - let socket = null; - - // this code will rerender anytime the websocket url changes - const websocket_url = useSelector( - (state) => state.websocketState.websocket_url, - ); - - const connect = () => { - // of the form wss://ip_address:port - console.log(websocket_url); - try { - socket = new WebSocket(websocket_url); - } catch (error) { - socket = new WebSocket('ws://localhost:7007'); - } - socket.binaryType = 'arraybuffer'; - socket.onopen = () => { - dispatch({ - type: 'write', - path: 'websocketState/isConnected', - data: true, - }); - }; - - socket.onclose = () => { - // when closed, the websocket will try to reconnect every second - dispatch({ - type: 'write', - path: 'websocketState/isConnected', - data: false, - }); - }; - - socket.onerror = (err) => { - console.error( - 'Socket encountered error: ', - err.message, - 'Closing socket', - ); - socket.close(); - }; - return socket; - }; - - useEffect(() => { - // cleanup function to close the websocket on rerender - return () => { - if (socket !== null) { - socket.close(); - } - }; - }, [websocket_url]); - - connect(); - ws = { - socket, - }; - - return ( - {children} - ); -} - -WebSocketContextFunction.propTypes = { - children: PropTypes.node.isRequired, -}; diff --git a/nerfstudio/viewer/app/src/modules/WebSocketUrlField.jsx b/nerfstudio/viewer/app/src/modules/WebSocketUrlField.jsx index f93b528fc..d25b8ba2a 100644 --- a/nerfstudio/viewer/app/src/modules/WebSocketUrlField.jsx +++ b/nerfstudio/viewer/app/src/modules/WebSocketUrlField.jsx @@ -29,6 +29,8 @@ export default function WebSocketUrlField() { } }; + const currentHost = `${window.location.protocol}//${window.location.host}`; + return (
- viewer.nerf.studio?websocket_url={websocket_url} + {currentHost}?websocket_url={websocket_url}
); diff --git a/nerfstudio/viewer/app/src/reducer.js b/nerfstudio/viewer/app/src/reducer.js index 5aefe9c8a..043f1dd98 100644 --- a/nerfstudio/viewer/app/src/reducer.js +++ b/nerfstudio/viewer/app/src/reducer.js @@ -13,41 +13,40 @@ const initialState = { render_img: null, // The rendered images +<<<<<<< HEAD +======= + show_export_box: false, // whether to show the export box + + custom_gui: { + guiNames: [], + guiConfigFromName: {}, + guiSetQueue: {}, + }, + + file_path_info: { + config_base_dir: 'config_base_dir', // the base directory of the config file + data_base_dir: 'data_base_dir', // the base directory of the images for saving camera path with the data + export_path_name: 'export_path_name', // export name for render and camera_path + }, + + all_camera_paths: null, // object containing camera paths and names + +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb // the rendering state renderingState: { // cameras camera_choice: 'Main Camera', // the camera being used to render the scene // camera path information - config_base_dir: 'config_base_dir', // the base directory of the config file render_height: 1080, render_width: 1920, field_of_view: 50, camera_type: 'perspective', - data_base_dir: 'data_base_dir', // the base directory of the images for saving camera path with the data - export_path: 'export_path', // export name for render and camera_path + training_state: 'training', - all_camera_paths: null, // object containing camera paths and names - - isTraining: true, - - // colormap options - output_options: ['rgb'], // populated by the possible Graph outputs - output_choice: 'rgb', // the selected output - colormap_options: ['default'], // populated by the output choice - colormap_choice: 'default', // the selected colormap - colormap_invert: false, // whether to invert the colormap - colormap_normalize: false, // whether to normalize the colormap - colormap_range: [0.0, 1.0], // the range of the colormap - - maxResolution: 1024, - targetTrainUtil: 0.9, + step: 0, eval_res: '?', - train_eta: 'Paused', - vis_train_ratio: 'Paused', - log_errors: '', - renderTime: 0.0, // export options clipping_enabled: true, @@ -59,6 +58,12 @@ const initialState = { crop_bg_color: { r: 38, g: 42, b: 55 }, crop_scale: [2.0, 2.0, 2.0], crop_center: [0.0, 0.0, 0.0], + + // Time options + use_time_conditioning: false, + + // Currently just used for mesh panel, should be removed + output_options: [], }, // the scene state sceneState: { @@ -67,31 +72,42 @@ const initialState = { }, }; +<<<<<<< HEAD function setData(newState, state, path, data) { +======= +// Recursive function to update the state object with new data at a given path +function setData(state, path, data) { + // If we've reached the final level of the path, update the property with the new data +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb if (path.length === 1) { - newState[path[0]] = data; // eslint-disable-line no-param-reassign - } else { - newState[path[0]] = { ...state[path[0]] }; // eslint-disable-line no-param-reassign - setData(newState[path[0]], state[path[0]], path.slice(1), data); + // Use the spread operator to create a shallow copy of the state object + // and update the property with the new data + return { ...state, [path[0]]: data }; } + // If we haven't reached the final level of the path, recursively update the nested object + // by creating a shallow copy of the parent object and updating the relevant property + return { + ...state, + [path[0]]: setData(state[path[0]], path.slice(1), data), + }; } -// Use the initialState as a default value +// Reducer function that handles the state updates // eslint-disable-next-line default-param-last export default function rootReducer(state = initialState, action) { - // The reducer normally looks at the action type field to decide what happens - switch (action.type) { case 'write': { - const path = split_path(action.path); // convert string with "/"s to a list - const data = action.data; - const newState = { ...state }; - setData(newState, state, path, data); + // Destructure the path and data values from the action object + const { path, data } = action; + // Split the path string into an array of path segments + const pathSegments = split_path(path); + // Call the setData function to update the state object with the new data + const newState = setData(state, pathSegments, data); + // Return the updated state object return newState; } + // If the reducer doesn't recognize the action type, return the existing state unchanged default: - // If this reducer doesn't recognize the action type, or doesn't - // care about this specific action, return the existing state unchanged return state; } } diff --git a/nerfstudio/viewer/app/src/subscriber.js b/nerfstudio/viewer/app/src/subscriber.js index 5af1a946c..fbbf44bc3 100644 --- a/nerfstudio/viewer/app/src/subscriber.js +++ b/nerfstudio/viewer/app/src/subscriber.js @@ -1,19 +1,18 @@ import { useContext } from 'react'; import { ReactReduxContext } from 'react-redux'; -export function subscribe_to_changes(selector_fn, fn) { - // selector_fn: returns a value from the redux state - // fn_valid: function to run on a valid input - // fn_null: function to run on a null input +export function subscribe_to_changes(selector_fn, callback_fn) { + // eslint-disable-next-line react-hooks/rules-of-hooks const { store } = useContext(ReactReduxContext); - let current; + let previousState; const handleChange = () => { - const previous = current; - current = selector_fn(store.getState()); - if (previous !== current) { - fn(previous, current); + const currentState = selector_fn(store.getState()); + if (previousState !== currentState) { + callback_fn(previousState, currentState); + previousState = currentState; } }; + store.subscribe(handleChange); } diff --git a/nerfstudio/viewer/app/src/themes/leva_theme.json b/nerfstudio/viewer/app/src/themes/leva_theme.json index e790bcd8a..db09d14c4 100644 --- a/nerfstudio/viewer/app/src/themes/leva_theme.json +++ b/nerfstudio/viewer/app/src/themes/leva_theme.json @@ -8,7 +8,7 @@ "accent3": "#ffd65c", "highlight1": "#d1d4db", "highlight2": "#EEEEEE", - "highlight3": "#222831", + "highlight3": "#EEEEEE", "disabled": "#595959", "vivid1": "#ffcc00" }, @@ -20,7 +20,7 @@ "space": { "sm": "6px", "md": "5px", - "rowGap": "6px", + "rowGap": "3px", "colGap": "7px" }, "fontSizes": { @@ -31,15 +31,15 @@ "controlWidth": "170px", "scrubberWidth": "8px", "scrubberHeight": "16px", - "rowHeight": "24px", - "folderHeight": "20px", + "rowHeight": "22px", + "folderHeight": "10px", "checkboxSize": "16px", "joystickWidth": "100px", "joystickHeight": "100px", "colorPickerWidth": "160px", "colorPickerHeight": "100px", "monitorHeight": "60px", - "titleBarHeight": "39px" + "titleBarHeight": "20px" }, "borderWidths": { "root": "0px", diff --git a/nerfstudio/viewer/app/src/themes/theme.ts b/nerfstudio/viewer/app/src/themes/theme.ts index b789c3899..0f6f2044c 100644 --- a/nerfstudio/viewer/app/src/themes/theme.ts +++ b/nerfstudio/viewer/app/src/themes/theme.ts @@ -4,6 +4,7 @@ export const appTheme = createTheme({ palette: { primary: { main: '#EEEEEE' }, secondary: { main: '#FFD369' }, + success: { main: '#5daf5d' }, text: { primary: '#EEEEEE', secondary: '#FFD369', diff --git a/nerfstudio/viewer/app/tsconfig.json b/nerfstudio/viewer/app/tsconfig.json new file mode 100644 index 000000000..9d379a3c4 --- /dev/null +++ b/nerfstudio/viewer/app/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/nerfstudio/viewer/app/yarn.lock b/nerfstudio/viewer/app/yarn.lock index b57fe28d7..24d156e1f 100644 --- a/nerfstudio/viewer/app/yarn.lock +++ b/nerfstudio/viewer/app/yarn.lock @@ -1034,7 +1034,7 @@ "@babel/runtime@^7.19.0": version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz" integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== dependencies: regenerator-runtime "^0.13.4" @@ -1604,7 +1604,7 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@^3.0.3": +"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": version "3.1.0" resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== @@ -1622,11 +1622,19 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/sourcemap-codec@^1.4.10": +"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/trace-mapping@^0.3.17": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + "@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9": version "0.3.14" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz" @@ -1640,6 +1648,36 @@ resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz#44d752c1a2dc113f15f781b7cc4f53a307e3fa38" + integrity sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ== + +"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz#f954f34355712212a8e06c465bc06c40852c6bb3" + integrity sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw== + +"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz#45c63037f045c2b15c44f80f0393fa24f9655367" + integrity sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg== + +"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz#35707efeafe6d22b3f373caf9e8775e8920d1399" + integrity sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA== + +"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2": + version "3.0.2" + resolved "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz" + integrity sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA== + +"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz#0f164b726869f71da3c594171df5ebc1c4b0a407" + integrity sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ== + "@mui/base@5.0.0-alpha.95": version "5.0.0-alpha.95" resolved "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.95.tgz" @@ -1722,7 +1760,7 @@ "@mui/private-theming@^5.10.6": version "5.10.6" - resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.10.6.tgz#2c6bb2a4b7034cd402a099bd0349f217584e7b25" + resolved "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.10.6.tgz" integrity sha512-I/W0QyTLRdEx6py3lKAquKO/rNF/7j+nIOM/xCyI9kU0fcotVTcTY08mKMsS6vrzdWpi6pAkD0wP0KwWy5R5VA== dependencies: "@babel/runtime" "^7.19.0" @@ -1751,7 +1789,7 @@ "@mui/styled-engine@^5.10.7": version "5.10.7" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.10.7.tgz#0080319577aec765590e3c2272f6629427a65eb0" + resolved "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.10.7.tgz" integrity sha512-CCrtW+vvCKEm6pOE/QcutQ+ORC/iE6D1ghscN4l7LE2JXPvTXO/z0yu8Wxug1JEDlWm4r1Qa0PzJe1P9bjKzNA== dependencies: "@babel/runtime" "^7.19.0" @@ -1789,7 +1827,7 @@ "@mui/system@^5.10.6": version "5.10.7" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.10.7.tgz#c13e5d135967e9689c473a66db4695ff78012701" + resolved "https://registry.npmjs.org/@mui/system/-/system-5.10.7.tgz" integrity sha512-kwyhjjKGsgtBRFl6vSqidDZcNKU5S1juTgm4Xi2fyWxaEbIQb9Sh9y0iVP2bNCJzgDr0alLaENOZOEaDWHISAQ== dependencies: "@babel/runtime" "^7.19.0" @@ -1819,7 +1857,7 @@ "@mui/utils@^5.10.6": version "5.10.6" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.10.6.tgz#98d432d2b05544c46efe356cf095cea3a37c2e59" + resolved "https://registry.npmjs.org/@mui/utils/-/utils-5.10.6.tgz" integrity sha512-g0Qs8xN/MW2M3fLL8197h5J2VB9U+49fLlnKKqC6zy/yus5cZwdT+Gwec+wUMxgwQoxMDn+J8oDWAn28kEOR/Q== dependencies: "@babel/runtime" "^7.19.0" @@ -1867,18 +1905,18 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@pmmmwh/react-refresh-webpack-plugin@^0.5.3": - version "0.5.7" - resolved "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz" - integrity sha512-bcKCAzF0DV2IIROp9ZHkRJa6O4jy7NlnHdWL3GmcUxYWNjLXkK5kfELELwEfSP5hXPfVL/qOGMAROuMQb9GG8Q== +"@pmmmwh/react-refresh-webpack-plugin@^0.5.10", "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": + version "0.5.10" + resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz#2eba163b8e7dbabb4ce3609ab5e32ab63dda3ef8" + integrity sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA== dependencies: ansi-html-community "^0.0.8" common-path-prefix "^3.0.0" - core-js-pure "^3.8.1" + core-js-pure "^3.23.3" error-stack-parser "^2.0.6" find-up "^5.0.0" html-entities "^2.1.0" - loader-utils "^2.0.0" + loader-utils "^2.0.4" schema-utils "^3.0.0" source-map "^0.7.3" @@ -2146,9 +2184,9 @@ "@hapi/hoek" "^9.0.0" "@sideway/formula@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz" - integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== "@sideway/pinpoint@^2.0.0": version "2.0.0" @@ -2444,10 +2482,10 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/estree@^0.0.51": - version "0.0.51" - resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz" - integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/estree@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" + integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": version "4.17.30" @@ -2788,125 +2826,125 @@ dependencies: "@use-gesture/core" "10.2.17" -"@webassemblyjs/ast@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz" - integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== +"@webassemblyjs/ast@1.11.5", "@webassemblyjs/ast@^1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.5.tgz#6e818036b94548c1fb53b754b5cae3c9b208281c" + integrity sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ== dependencies: - "@webassemblyjs/helper-numbers" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-numbers" "1.11.5" + "@webassemblyjs/helper-wasm-bytecode" "1.11.5" -"@webassemblyjs/floating-point-hex-parser@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz" - integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== +"@webassemblyjs/floating-point-hex-parser@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.5.tgz#e85dfdb01cad16b812ff166b96806c050555f1b4" + integrity sha512-1j1zTIC5EZOtCplMBG/IEwLtUojtwFVwdyVMbL/hwWqbzlQoJsWCOavrdnLkemwNoC/EOwtUFch3fuo+cbcXYQ== -"@webassemblyjs/helper-api-error@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz" - integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== +"@webassemblyjs/helper-api-error@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.5.tgz#1e82fa7958c681ddcf4eabef756ce09d49d442d1" + integrity sha512-L65bDPmfpY0+yFrsgz8b6LhXmbbs38OnwDCf6NpnMUYqa+ENfE5Dq9E42ny0qz/PdR0LJyq/T5YijPnU8AXEpA== -"@webassemblyjs/helper-buffer@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz" - integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== +"@webassemblyjs/helper-buffer@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.5.tgz#91381652ea95bb38bbfd270702351c0c89d69fba" + integrity sha512-fDKo1gstwFFSfacIeH5KfwzjykIE6ldh1iH9Y/8YkAZrhmu4TctqYjSh7t0K2VyDSXOZJ1MLhht/k9IvYGcIxg== -"@webassemblyjs/helper-numbers@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz" - integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== +"@webassemblyjs/helper-numbers@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.5.tgz#23380c910d56764957292839006fecbe05e135a9" + integrity sha512-DhykHXM0ZABqfIGYNv93A5KKDw/+ywBFnuWybZZWcuzWHfbp21wUfRkbtz7dMGwGgT4iXjWuhRMA2Mzod6W4WA== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/floating-point-hex-parser" "1.11.5" + "@webassemblyjs/helper-api-error" "1.11.5" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz" - integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== +"@webassemblyjs/helper-wasm-bytecode@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.5.tgz#e258a25251bc69a52ef817da3001863cc1c24b9f" + integrity sha512-oC4Qa0bNcqnjAowFn7MPCETQgDYytpsfvz4ujZz63Zu/a/v71HeCAAmZsgZ3YVKec3zSPYytG3/PrRCqbtcAvA== -"@webassemblyjs/helper-wasm-section@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz" - integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== +"@webassemblyjs/helper-wasm-section@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.5.tgz#966e855a6fae04d5570ad4ec87fbcf29b42ba78e" + integrity sha512-uEoThA1LN2NA+K3B9wDo3yKlBfVtC6rh0i4/6hvbz071E8gTNZD/pT0MsBf7MeD6KbApMSkaAK0XeKyOZC7CIA== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/ast" "1.11.5" + "@webassemblyjs/helper-buffer" "1.11.5" + "@webassemblyjs/helper-wasm-bytecode" "1.11.5" + "@webassemblyjs/wasm-gen" "1.11.5" -"@webassemblyjs/ieee754@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz" - integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== +"@webassemblyjs/ieee754@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.5.tgz#b2db1b33ce9c91e34236194c2b5cba9b25ca9d60" + integrity sha512-37aGq6qVL8A8oPbPrSGMBcp38YZFXcHfiROflJn9jxSdSMMM5dS5P/9e2/TpaJuhE+wFrbukN2WI6Hw9MH5acg== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz" - integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== +"@webassemblyjs/leb128@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.5.tgz#482e44d26b6b949edf042a8525a66c649e38935a" + integrity sha512-ajqrRSXaTJoPW+xmkfYN6l8VIeNnR4vBOTQO9HzR7IygoCcKWkICbKFbVTNMjMgMREqXEr0+2M6zukzM47ZUfQ== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz" - integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== +"@webassemblyjs/utf8@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.5.tgz#83bef94856e399f3740e8df9f63bc47a987eae1a" + integrity sha512-WiOhulHKTZU5UPlRl53gHR8OxdGsSOxqfpqWeA2FmcwBMaoEdz6b2x2si3IwC9/fSPLfe8pBMRTHVMk5nlwnFQ== -"@webassemblyjs/wasm-edit@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz" - integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.5.tgz#93ee10a08037657e21c70de31c47fdad6b522b2d" + integrity sha512-C0p9D2fAu3Twwqvygvf42iGCQ4av8MFBLiTb+08SZ4cEdwzWx9QeAHDo1E2k+9s/0w1DM40oflJOpkZ8jW4HCQ== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/helper-wasm-section" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-opt" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - "@webassemblyjs/wast-printer" "1.11.1" + "@webassemblyjs/ast" "1.11.5" + "@webassemblyjs/helper-buffer" "1.11.5" + "@webassemblyjs/helper-wasm-bytecode" "1.11.5" + "@webassemblyjs/helper-wasm-section" "1.11.5" + "@webassemblyjs/wasm-gen" "1.11.5" + "@webassemblyjs/wasm-opt" "1.11.5" + "@webassemblyjs/wasm-parser" "1.11.5" + "@webassemblyjs/wast-printer" "1.11.5" -"@webassemblyjs/wasm-gen@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz" - integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== +"@webassemblyjs/wasm-gen@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.5.tgz#ceb1c82b40bf0cf67a492c53381916756ef7f0b1" + integrity sha512-14vteRlRjxLK9eSyYFvw1K8Vv+iPdZU0Aebk3j6oB8TQiQYuO6hj9s4d7qf6f2HJr2khzvNldAFG13CgdkAIfA== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" + "@webassemblyjs/ast" "1.11.5" + "@webassemblyjs/helper-wasm-bytecode" "1.11.5" + "@webassemblyjs/ieee754" "1.11.5" + "@webassemblyjs/leb128" "1.11.5" + "@webassemblyjs/utf8" "1.11.5" -"@webassemblyjs/wasm-opt@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz" - integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== +"@webassemblyjs/wasm-opt@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.5.tgz#b52bac29681fa62487e16d3bb7f0633d5e62ca0a" + integrity sha512-tcKwlIXstBQgbKy1MlbDMlXaxpucn42eb17H29rawYLxm5+MsEmgPzeCP8B1Cl69hCice8LeKgZpRUAPtqYPgw== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/ast" "1.11.5" + "@webassemblyjs/helper-buffer" "1.11.5" + "@webassemblyjs/wasm-gen" "1.11.5" + "@webassemblyjs/wasm-parser" "1.11.5" -"@webassemblyjs/wasm-parser@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz" - integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== +"@webassemblyjs/wasm-parser@1.11.5", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.5.tgz#7ba0697ca74c860ea13e3ba226b29617046982e2" + integrity sha512-SVXUIwsLQlc8srSD7jejsfTU83g7pIGr2YYNb9oHdtldSxaOhvA5xwvIiWIfcX8PlSakgqMXsLpLfbbJ4cBYew== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" + "@webassemblyjs/ast" "1.11.5" + "@webassemblyjs/helper-api-error" "1.11.5" + "@webassemblyjs/helper-wasm-bytecode" "1.11.5" + "@webassemblyjs/ieee754" "1.11.5" + "@webassemblyjs/leb128" "1.11.5" + "@webassemblyjs/utf8" "1.11.5" -"@webassemblyjs/wast-printer@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz" - integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== +"@webassemblyjs/wast-printer@1.11.5": + version "1.11.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.5.tgz#7a5e9689043f3eca82d544d7be7a8e6373a6fa98" + integrity sha512-f7Pq3wvg3GSPUPzR0F6bmI89Hdb+u9WXrSKc4v+N0aV0q6r42WoF92Jp2jEorBEBRoRNXgjp53nBniDXcqZYPA== dependencies: - "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/ast" "1.11.5" "@xtuc/long" "4.2.2" "@welldone-software/why-did-you-render@^6.2.3": @@ -2988,7 +3026,7 @@ address@^1.0.1, address@^1.1.2: adjust-sourcemap-loader@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== dependencies: loader-utils "^2.0.0" @@ -3228,6 +3266,11 @@ autoprefixer@^10.4.7: picocolors "^1.0.0" postcss-value-parser "^4.2.0" +await-lock@^2.2.2: + version "2.2.2" + resolved "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz" + integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw== + axe-core@^4.4.3: version "4.4.3" resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.4.3.tgz" @@ -3259,15 +3302,13 @@ babel-jest@^27.4.2, babel-jest@^27.5.1: graceful-fs "^4.2.9" slash "^3.0.0" -babel-loader@^8.2.3: - version "8.2.5" - resolved "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz" - integrity sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ== +babel-loader@^8.2.3, babel-loader@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c" + integrity sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA== dependencies: - find-cache-dir "^3.3.1" - loader-utils "^2.0.0" - make-dir "^3.1.0" - schema-utils "^2.6.5" + find-cache-dir "^3.3.2" + schema-utils "^4.0.0" babel-plugin-dynamic-import-node@^2.3.3: version "2.3.3" @@ -3408,11 +3449,6 @@ bfj@^7.0.2: hoopy "^0.1.4" tryer "^1.0.1" -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" @@ -3555,7 +3591,7 @@ camelcase-css@^2.0.1: camelcase@^5.3.1: version "5.3.1" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== camelcase@^6.2.0, camelcase@^6.2.1: @@ -3579,9 +3615,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001373: - version "1.0.30001373" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001373.tgz" - integrity sha512-pJYArGHrPp3TUqQzFYRmP/lwJlj8RCbVe3Gd3eJQkAV8SAC6b19XS9BjMvRdvaS8RMkaTN8ZhoHP6S1y8zzwEQ== + version "1.0.30001478" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001478.tgz" + integrity sha512-gMhDyXGItTHipJj2ApIvR+iVB5hd0KP3svMWWXDvZOmjzJJassGLMfxRkQCSYgGd2gtdL/ReeiyvMSFD1Ss6Mw== case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" @@ -3590,7 +3626,7 @@ case-sensitive-paths-webpack-plugin@^2.4.0: chalk@^2.0.0, chalk@^2.4.1: version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" @@ -3861,11 +3897,16 @@ core-js-compat@^3.21.0, core-js-compat@^3.22.1: browserslist "^4.21.3" semver "7.0.0" -core-js-pure@^3.20.2, core-js-pure@^3.8.1: +core-js-pure@^3.20.2: version "3.24.1" resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.24.1.tgz" integrity sha512-r1nJk41QLLPyozHUUPmILCEMtMw24NG4oWK6RbsDdjzQgg9ZvrUsPBj1MnG0wXXp1DCDU6j+wUvEmBSrtRbLXg== +core-js-pure@^3.23.3: + version "3.30.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.30.1.tgz#7d93dc89e7d47b8ef05d7e79f507b0e99ea77eec" + integrity sha512-nXBEVpmUnNRhz83cHd9JRQC52cTMcuXAmR56+9dSMpRdpeA4I1PX6yjmhd71Eyc/wXNsdBdUDIj1QTIeZpU5Tg== + core-js@^3.19.2: version "3.24.1" resolved "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz" @@ -4118,7 +4159,7 @@ csstype@^3.0.2, csstype@^3.0.4, csstype@^3.1.0: csstype@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== d@1, d@^1.0.1: @@ -4185,9 +4226,9 @@ decimal.js@^10.2.1: integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz" - integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og== + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== dedent@^0.7.0: version "0.7.0" @@ -4472,11 +4513,6 @@ emoji-regex@^9.2.2: resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" @@ -4498,10 +4534,10 @@ engine.io-parser@~5.0.3: resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz" integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg== -enhanced-resolve@^5.10.0: - version "5.10.0" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz" - integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ== +enhanced-resolve@^5.13.0: + version "5.13.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz#26d1ecc448c02de997133217b5c1053f34a0a275" + integrity sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -4559,10 +4595,10 @@ es-array-method-boxes-properly@^1.0.0: resolved "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz" integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== -es-module-lexer@^0.9.0: - version "0.9.3" - resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-module-lexer@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.2.1.tgz#ba303831f63e6a394983fde2f97ad77b22324527" + integrity sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg== es-shim-unscopables@^1.0.0: version "1.0.0" @@ -4591,7 +4627,7 @@ es5-ext@^0.10.35, es5-ext@^0.10.50: es6-iterator@^2.0.3: version "2.0.3" - resolved "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== dependencies: d "1" @@ -5147,9 +5183,9 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" -find-cache-dir@^3.3.1: +find-cache-dir@^3.3.2: version "3.3.2" - resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== dependencies: commondir "^1.0.1" @@ -6642,17 +6678,10 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json5@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" - integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== - dependencies: - minimist "^1.2.0" - -json5@^2.1.2, json5@^2.2.0, json5@^2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz" - integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== +json5@^1.0.1, json5@^2.2.0, json5@^2.2.1, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonfile@^6.0.1: version "6.1.0" @@ -6757,19 +6786,10 @@ loader-runner@^4.2.0: resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz" integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== -loader-utils@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" - integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -loader-utils@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz" - integrity sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ== +loader-utils@^2.0.0, loader-utils@^2.0.4, loader-utils@^3.2.0, loader-utils@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" + integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== locate-path@^2.0.0: version "2.0.0" @@ -6864,7 +6884,7 @@ magic-string@^0.25.0, magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.8" -make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: +make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -6982,14 +7002,7 @@ minimalistic-assert@^1.0.0: resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -7003,9 +7016,9 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.2.5, minimist@^1.2.6: version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== mixin-deep@^1.2.0: @@ -7048,6 +7061,27 @@ msgpack-lite@^0.1.26: int64-buffer "^0.1.9" isarray "^1.0.0" +msgpackr-extract@^3.0.1: + version "3.0.2" + resolved "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz" + integrity sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A== + dependencies: + node-gyp-build-optional-packages "5.0.7" + optionalDependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.2" + "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.2" + "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.2" + "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.2" + "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.2" + "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.2" + +msgpackr@^1.8.5: + version "1.8.5" + resolved "https://registry.npmjs.org/msgpackr/-/msgpackr-1.8.5.tgz" + integrity sha512-mpPs3qqTug6ahbblkThoUY2DQdNXcm4IapwOS3Vm/87vmpzLVelvp9h3It1y9l1VPpiFLV11vfOXnmeEwiIXwg== + optionalDependencies: + msgpackr-extract "^3.0.1" + multicast-dns@^7.2.5: version "7.2.5" resolved "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz" @@ -7061,6 +7095,11 @@ nanoid@^3.3.4: resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" @@ -7094,6 +7133,11 @@ node-forge@^1: resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== +node-gyp-build-optional-packages@5.0.7: + version "5.0.7" + resolved "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz" + integrity sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w== + node-gyp-build@^4.3.0: version "4.5.0" resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz" @@ -7432,11 +7476,6 @@ performance-now@^2.1.0: resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== -picocolors@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz" - integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" @@ -8004,13 +8043,14 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^ resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^7.0.35: - version "7.0.39" - resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz" - integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== +postcss@^8.2.14: + version "8.4.23" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" + integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== dependencies: - picocolors "^0.2.1" - source-map "^0.6.1" + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" postcss@^8.3.5, postcss@^8.4.14, postcss@^8.4.4, postcss@^8.4.7: version "8.4.14" @@ -8402,12 +8442,12 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -recursive-readdir@^2.2.2: - version "2.2.2" - resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz" - integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== +recursive-readdir@^2.2.2, recursive-readdir@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" + integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== dependencies: - minimatch "3.0.4" + minimatch "^3.0.5" redent@^3.0.0: version "3.0.0" @@ -8554,15 +8594,15 @@ resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-url-loader@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz" - integrity sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA== +resolve-url-loader@^4.0.0, resolve-url-loader@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz#ee3142fb1f1e0d9db9524d539cfa166e9314f795" + integrity sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg== dependencies: adjust-sourcemap-loader "^4.0.0" convert-source-map "^1.7.0" loader-utils "^2.0.0" - postcss "^7.0.35" + postcss "^8.2.14" source-map "0.6.1" resolve.exports@^1.1.0: @@ -8706,16 +8746,7 @@ schema-utils@2.7.0: ajv "^6.12.2" ajv-keywords "^3.4.1" -schema-utils@^2.6.5: - version "2.7.1" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz" - integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== - dependencies: - "@types/json-schema" "^7.0.5" - ajv "^6.12.4" - ajv-keywords "^3.5.2" - -schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: +schema-utils@^3.0.0, schema-utils@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz" integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== @@ -8724,6 +8755,15 @@ schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +schema-utils@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.2.tgz#36c10abca6f7577aeae136c804b0c741edeadc99" + integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + schema-utils@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz" @@ -8796,6 +8836,13 @@ serialize-javascript@^6.0.0: dependencies: randombytes "^2.1.0" +serialize-javascript@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + serve-index@^1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" @@ -9327,7 +9374,7 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" -terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5: +terser-webpack-plugin@^5.2.5: version "5.3.3" resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz" integrity sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ== @@ -9338,6 +9385,17 @@ terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5: serialize-javascript "^6.0.0" terser "^5.7.2" +terser-webpack-plugin@^5.3.7: + version "5.3.7" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz#ef760632d24991760f339fe9290deb936ad1ffc7" + integrity sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.17" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.16.5" + terser@^5.0.0, terser@^5.10.0, terser@^5.7.2: version "5.14.2" resolved "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz" @@ -9348,6 +9406,16 @@ terser@^5.0.0, terser@^5.10.0, terser@^5.7.2: commander "^2.20.0" source-map-support "~0.5.20" +terser@^5.16.5: + version "5.17.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.1.tgz#948f10830454761e2eeedc6debe45c532c83fd69" + integrity sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw== + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" @@ -9530,10 +9598,10 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^4.7.3: - version "4.7.4" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz" - integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== +typescript@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.0.3.tgz" + integrity sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA== unbox-primitive@^1.0.2: version "1.0.2" @@ -9819,21 +9887,21 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.64.4: - version "5.74.0" - resolved "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz" - integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA== + version "5.80.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.80.0.tgz#3e660b4ab572be38c5e954bdaae7e2bf76010fdc" + integrity sha512-OIMiq37XK1rWO8mH9ssfFKZsXg4n6klTEDL7S8/HqbAOBBaiy8ABvXvz0dDCXeEF9gqwxSvVk611zFPjS8hJxA== dependencies: "@types/eslint-scope" "^3.7.3" - "@types/estree" "^0.0.51" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" acorn "^8.7.1" acorn-import-assertions "^1.7.6" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.10.0" - es-module-lexer "^0.9.0" + enhanced-resolve "^5.13.0" + es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" @@ -9842,9 +9910,9 @@ webpack@^5.64.4: loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.1.0" + schema-utils "^3.1.2" tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" + terser-webpack-plugin "^5.3.7" watchpack "^2.4.0" webpack-sources "^3.2.3" @@ -10189,10 +10257,10 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: - version "1.10.2" - resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2, yaml@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073" + integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA== yargs-parser@^20.2.2: version "20.2.9" diff --git a/nerfstudio/viewer/server/README.md b/nerfstudio/viewer/server/README.md deleted file mode 100644 index 307c0a221..000000000 --- a/nerfstudio/viewer/server/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Python Kernel and Client Viewer App communication - -> The purpose of this document is to explain how to communicate from Python with the Client Viewer app. We will eventually move this into the read the docs. - -- Python Kernel (nerfstudio code) -- Bridge Server -- Client Viewer App - -We have two types of components that we want to keep state updated in both locations. - -- Widgets - - The widgets are used to keep track of the -- SceneNode - - The scene nodes are used to represent the three.js objects. The properties relevant to these objects are the following: `"object", "transform", "properties"`. - -# Checklist - -- [ ] Currently using request-reply (REQ, REP with zmq). I.e., Python Kernel -> Bridge Server <-> Client Viewer App. We want a way to update the Python Kernel when the Bridge Server is updated. This requries some form of binding with callbacks. When the Bridge Server is updated, we want to update the binded Python variable. We can take inspiration from [ipywidgets](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Basics.html). - -- [ ] diff --git a/nerfstudio/viewer/server/control_panel.py b/nerfstudio/viewer/server/control_panel.py new file mode 100644 index 000000000..7ee539fa6 --- /dev/null +++ b/nerfstudio/viewer/server/control_panel.py @@ -0,0 +1,331 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Control panel for the viewer """ +from collections import defaultdict +from typing import Callable, DefaultDict, List, Tuple, get_args + +import torch + +from nerfstudio.utils.colormaps import ColormapOptions, Colormaps +from nerfstudio.viewer.server.viewer_elements import ( + ViewerCheckbox, + ViewerDropdown, + ViewerElement, + ViewerNumber, + ViewerRGB, + ViewerSlider, + ViewerVec3, +) +from nerfstudio.viewer.viser import ViserServer + + +class ControlPanel: + """ + Initializes the control panel with all the elements + Args: + time_enabled: whether or not the time slider should be enabled + rerender_cb: a callback that will be called when the user changes a parameter that requires a rerender + (eg train speed, max res, etc) + crop_update_cb: a callback that will be called when the user changes the crop parameters + update_output_cb: a callback that will be called when the user changes the output render + """ + + def __init__( + self, + viser_server: ViserServer, + time_enabled: bool, + rerender_cb: Callable, + crop_update_cb: Callable, + update_output_cb: Callable, + update_split_output_cb: Callable, + ): + # elements holds a mapping from tag: [elements] + self.viser_server = viser_server + self._elements_by_tag: DefaultDict[str, List[ViewerElement]] = defaultdict(lambda: []) + + self._train_speed = ViewerDropdown( + "Train Speed", "Balanced", ["Fast", "Balanced", "Slow"], cb_hook=lambda han: self._train_speed_cb() + ) + self._output_render = ViewerDropdown( + "Output Render", + "not set", + ["not set"], + cb_hook=lambda han: [self.update_control_panel(), update_output_cb(han), rerender_cb(han)], + ) + self._colormap = ViewerDropdown[Colormaps]("Colormap", "default", ["default"], cb_hook=rerender_cb) + self._invert = ViewerCheckbox("Invert", False, cb_hook=rerender_cb) + self._normalize = ViewerCheckbox("Normalize", False, cb_hook=rerender_cb) + self._min = ViewerNumber("Min", 0.0, cb_hook=rerender_cb) + self._max = ViewerNumber("Max", 1.0, cb_hook=rerender_cb) + + self._split = ViewerCheckbox( + "Enable", False, cb_hook=lambda han: [self.update_control_panel(), rerender_cb(han)] + ) + self._split_percentage = ViewerSlider("Split Percentage", 0.5, 0.0, 1.0, 0.01, cb_hook=rerender_cb) + self._split_output_render = ViewerDropdown( + "Output Render Split", + "not set", + ["not set"], + cb_hook=lambda han: [self.update_control_panel(), update_split_output_cb(han), rerender_cb(han)], + ) + # Hack: spaces are after at the end of the names to make them unique + self._split_colormap = ViewerDropdown[Colormaps]("Colormap ", "default", ["default"], cb_hook=rerender_cb) + self._split_invert = ViewerCheckbox("Invert ", False, cb_hook=rerender_cb) + self._split_normalize = ViewerCheckbox("Normalize ", True, cb_hook=rerender_cb) + self._split_min = ViewerNumber("Min ", 0.0, cb_hook=rerender_cb) + self._split_max = ViewerNumber("Max ", 1.0, cb_hook=rerender_cb) + + self._train_util = ViewerSlider("Train Util", 0.85, 0, 1, 0.05) + self._max_res = ViewerSlider("Max Res", 512, 64, 2048, 100, cb_hook=rerender_cb) + self._crop_viewport = ViewerCheckbox( + "Enable ", + False, + cb_hook=lambda han: [self.update_control_panel(), crop_update_cb(han), rerender_cb(han)], + ) + self._background_color = ViewerRGB("Background color", (38, 42, 55), cb_hook=crop_update_cb) + self._crop_min = ViewerVec3("Crop Min", (-1, -1, -1), 0.05, cb_hook=crop_update_cb) + self._crop_max = ViewerVec3("Crop Max", (1, 1, 1), 0.05, cb_hook=crop_update_cb) + self._time = ViewerSlider("Time", 0.0, 0.0, 1.0, 0.01, cb_hook=rerender_cb) + self._time_enabled = time_enabled + + self.add_element(self._train_speed) + self.add_element(self._train_util) + with self.viser_server.gui_folder("Render Options"): + self.add_element(self._max_res) + self.add_element(self._output_render) + self.add_element(self._colormap) + # colormap options + with self.viser_server.gui_folder(" "): + self.add_element(self._invert, additional_tags=("colormap",)) + self.add_element(self._normalize, additional_tags=("colormap",)) + self.add_element(self._min, additional_tags=("colormap",)) + self.add_element(self._max, additional_tags=("colormap",)) + + # split options + with self.viser_server.gui_folder("Split Screen"): + self.add_element(self._split) + + self.add_element(self._split_percentage, additional_tags=("split",)) + self.add_element(self._split_output_render, additional_tags=("split",)) + self.add_element(self._split_colormap, additional_tags=("split",)) + with self.viser_server.gui_folder(" "): + self.add_element(self._split_invert, additional_tags=("split_colormap",)) + self.add_element(self._split_normalize, additional_tags=("split_colormap",)) + self.add_element(self._split_min, additional_tags=("split_colormap",)) + self.add_element(self._split_max, additional_tags=("split_colormap",)) + + with self.viser_server.gui_folder("Crop Viewport"): + self.add_element(self._crop_viewport) + + # Crop options + self.add_element(self._background_color, additional_tags=("crop",)) + self.add_element(self._crop_min, additional_tags=("crop",)) + self.add_element(self._crop_max, additional_tags=("crop",)) + + self.add_element(self._time, additional_tags=("time",)) + + def _train_speed_cb(self) -> None: + """Callback for when the train speed is changed""" + if self.train_speed == "Fast": + self._train_util.value = 0.95 + self._max_res.value = 256 + elif self.train_speed == "Balanced": + self._train_util.value = 0.85 + self._max_res.value = 512 + elif self.train_speed == "Slow": + self._train_util.value = 0.5 + self._max_res.value = 1024 + + def update_output_options(self, new_options: List[str]): + """ + Args: + new_options: a list of new output options + """ + self._output_render.set_options(new_options) + self._split_output_render.set_options(new_options) + self._split_output_render.value = new_options[-1] + + def add_element(self, e: ViewerElement, additional_tags: Tuple[str] = tuple()) -> None: + """Adds an element to the control panel + + Args: + e: the element to add + additional_tags: additional tags to add to the element for selection + """ + self._elements_by_tag["all"].append(e) + for t in additional_tags: + self._elements_by_tag[t].append(e) + e.install(self.viser_server) + + def update_control_panel(self) -> None: + """ + Sets elements to be hidden or not based on the current state of the control panel + """ + self._colormap.set_disabled(self.output_render == "rgb") + for e in self._elements_by_tag["colormap"]: + e.set_hidden(self.output_render == "rgb") + for e in self._elements_by_tag["split_colormap"]: + e.set_hidden(not self._split.value or self.split_output_render == "rgb") + for e in self._elements_by_tag["crop"]: + e.set_hidden(not self.crop_viewport) + self._time.set_hidden(not self._time_enabled) + self._split_percentage.set_hidden(not self._split.value) + self._split_output_render.set_hidden(not self._split.value) + self._split_colormap.set_hidden(not self._split.value) + self._split_colormap.set_disabled(self.split_output_render == "rgb") + + def update_colormap_options(self, dimensions: int, dtype: type) -> None: + """update the colormap options based on the current render + + Args: + dimensions: the number of dimensions of the render + dtype: the data type of the render + """ + self._colormap.set_options(_get_colormap_options(dimensions, dtype)) + + def update_split_colormap_options(self, dimensions: int, dtype: type) -> None: + """update the split colormap options based on the current render + + Args: + dimensions: the number of dimensions of the render + dtype: the data type of the render + """ + self._split_colormap.set_options(_get_colormap_options(dimensions, dtype)) + + @property + def train_speed(self) -> str: + """Returns the current train speed setting""" + return self._train_speed.value + + @property + def output_render(self) -> str: + """Returns the current output render""" + return self._output_render.value + + @property + def split_output_render(self) -> str: + """Returns the current output for the split render""" + return self._split_output_render.value + + @property + def split(self) -> bool: + """Returns whether the split is enabled""" + return self._split.value + + @property + def split_percentage(self) -> float: + """Returns the percentage of the screen to split""" + return self._split_percentage.value + + @property + def train_util(self) -> float: + """Returns the current train util setting""" + return self._train_util.value + + @property + def max_res(self) -> int: + """Returns the current max res setting""" + return self._max_res.value + + @property + def crop_viewport(self) -> bool: + """Returns the current crop viewport setting""" + return self._crop_viewport.value + + @crop_viewport.setter + def crop_viewport(self, value: bool): + """Sets the crop viewport setting""" + self._crop_viewport.value = value + + @property + def crop_min(self) -> Tuple[float, float, float]: + """Returns the current crop min setting""" + return self._crop_min.value + + @crop_min.setter + def crop_min(self, value: Tuple[float, float, float]): + """Sets the crop min setting""" + self._crop_min.value = value + + @property + def crop_max(self) -> Tuple[float, float, float]: + """Returns the current crop max setting""" + return self._crop_max.value + + @crop_max.setter + def crop_max(self, value: Tuple[float, float, float]): + """Sets the crop max setting""" + self._crop_max.value = value + + @property + def background_color(self) -> Tuple[int, int, int]: + """Returns the current background color""" + return self._background_color.value + + @background_color.setter + def background_color(self, value: Tuple[int, int, int]): + """Sets the background color""" + self._background_color.value = value + + @property + def time(self) -> float: + """Returns the current background color""" + return self._time.value + + @time.setter + def time(self, value: float): + """Sets the background color""" + self._time.value = value + + @property + def colormap_options(self) -> ColormapOptions: + """Returns the current colormap options""" + return ColormapOptions( + colormap=self._colormap.value, + normalize=self._normalize.value, + colormap_min=self._min.value, + colormap_max=self._max.value, + invert=self._invert.value, + ) + + @property + def split_colormap_options(self) -> ColormapOptions: + """Returns the current colormap options""" + return ColormapOptions( + colormap=self._split_colormap.value, + normalize=self._split_normalize.value, + colormap_min=self._split_min.value, + colormap_max=self._split_max.value, + invert=self._split_invert.value, + ) + + +def _get_colormap_options(dimensions: int, dtype: type) -> List[Colormaps]: + """ + Given the number of dimensions and data type, returns a list of available colormap options + to use with the visualize() function. + + Args: + dimensions: the number of dimensions of the render + dtype: the data type of the render + Returns: + a list of available colormap options + """ + colormap_options: List[Colormaps] = [] + if dimensions == 3: + colormap_options = ["default"] + if dimensions == 1 and dtype == torch.float: + colormap_options = [c for c in list(get_args(Colormaps)) if c != "default"] + return colormap_options diff --git a/nerfstudio/viewer/server/gui_utils.py b/nerfstudio/viewer/server/gui_utils.py new file mode 100644 index 000000000..a77fc6edb --- /dev/null +++ b/nerfstudio/viewer/server/gui_utils.py @@ -0,0 +1,67 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Utilites for generating custom gui elements in the viewer """ + +from __future__ import annotations + +from typing import Any, List, Tuple + +from torch import nn + + +def parse_object( + obj: Any, + type_check, + tree_stub: str, +) -> List[Tuple[str, Any]]: + """ + obj: the object to parse + type_check: recursively adds instances of this type to the output + tree_stub: the path down the object tree to this object + + Returns: + a list of (path/to/object, obj), which represents the path down the object tree + along with the object itself + """ + + def add(ret: List[Tuple[str, Any]], ts: str, v: Any): + """ + helper that adds to ret, and if v exists already keeps the tree stub with + the shortest path + """ + for i, (t, o) in enumerate(ret): + if o == v: + if len(t.split("/")) > len(ts.split("/")): + ret[i] = (ts, v) + return + ret.append((ts, v)) + + if not hasattr(obj, "__dict__"): + return [] + ret = [] + # get a list of the properties of the object, sorted by whether things are instances of type_check + obj_props = [(k, getattr(obj, k)) for k in dir(obj)] + for k, v in obj_props: + if k[0] == "_": + continue + new_tree_stub = f"{tree_stub}/{k}" + if isinstance(v, type_check): + add(ret, new_tree_stub, v) + elif isinstance(v, nn.Module): + lower_rets = parse_object(v, type_check, new_tree_stub) + # check that the values aren't already in the tree + for ts, o in lower_rets: + add(ret, ts, o) + return ret diff --git a/nerfstudio/viewer/server/render_state_machine.py b/nerfstudio/viewer/server/render_state_machine.py new file mode 100644 index 000000000..1001b4f18 --- /dev/null +++ b/nerfstudio/viewer/server/render_state_machine.py @@ -0,0 +1,304 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This file contains the render state machine, which is responsible for deciding when to render the image """ +from __future__ import annotations + +import contextlib +import threading +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Tuple, get_args + +import torch + +from nerfstudio.cameras.cameras import Cameras, CameraType +from nerfstudio.model_components.renderers import background_color_override_context +from nerfstudio.utils import colormaps, writer +from nerfstudio.utils.writer import GLOBAL_BUFFER, EventName, TimeWriter +from nerfstudio.viewer.server import viewer_utils +from nerfstudio.viewer.server.utils import get_intrinsics_matrix_and_camera_to_world_h +from nerfstudio.viewer.viser.messages import CameraMessage + +if TYPE_CHECKING: + from nerfstudio.viewer.server.viewer_state import ViewerState + +RenderStates = Literal["low_move", "low_static", "high"] +RenderActions = Literal["rerender", "move", "static", "step"] + + +@dataclass +class RenderAction: + """Message to the render state machine""" + + action: RenderActions + """The action to take """ + cam_msg: CameraMessage + """The camera message to render""" + + +class RenderStateMachine(threading.Thread): + """The render state machine is responsible for deciding how to render the image. + It decides the resolution and whether to interrupt the current render. + + Args: + viewer: the viewer state + """ + + def __init__(self, viewer: ViewerState): + threading.Thread.__init__(self) + self.transitions: Dict[RenderStates, Dict[RenderActions, RenderStates]] = { + s: {} for s in get_args(RenderStates) + } + # by default, everything is a self-transition + for a in get_args(RenderActions): + for s in get_args(RenderStates): + self.transitions[s][a] = s + # then define the actions between states + self.transitions["low_move"]["static"] = "low_static" + self.transitions["low_static"]["static"] = "high" + self.transitions["low_static"]["step"] = "high" + self.transitions["low_static"]["move"] = "low_move" + self.transitions["high"]["move"] = "low_move" + self.transitions["high"]["rerender"] = "low_static" + self.next_action: Optional[RenderAction] = None + self.state: RenderStates = "low_static" + self.render_trigger = threading.Event() + self.target_fps = 24 + self.viewer = viewer + self.interrupt_render_flag = False + self.daemon = True + self.output_keys = {} + + def action(self, action: RenderAction): + """Takes an action and updates the state machine + + Args: + action: the action to take + """ + if self.next_action is None: + self.next_action = action + elif action.action == "step" and ( + self.state == "low_move" or self.next_action.action in ("move", "static", "rerender") + ): + # ignore steps if: + # 1. we are in low_moving state + # 2. the current next_action is move, static, or rerender + return + elif self.next_action == "rerender": + # never overwrite rerenders + pass + else: + # monimal use case, just set the next action + self.next_action = action + + # handle interrupt logic + if self.state == "high" and self.next_action.action in ("move", "rerender"): + self.interrupt_render_flag = True + self.render_trigger.set() + + def _render_img(self, cam_msg: CameraMessage): + """Takes the current camera, generates rays, and renders the iamge + + Args: + cam_msg: the camera message to render + """ + + # initialize the camera ray bundle + viewer_utils.update_render_aabb( + crop_viewport=self.viewer.control_panel.crop_viewport, + crop_min=self.viewer.control_panel.crop_min, + crop_max=self.viewer.control_panel.crop_max, + model=self.viewer.get_model(), + ) + + image_height, image_width = self._calculate_image_res(cam_msg.aspect) + + intrinsics_matrix, camera_to_world_h = get_intrinsics_matrix_and_camera_to_world_h( + cam_msg, image_height=image_height, image_width=image_width + ) + + camera_to_world = camera_to_world_h[:3, :] + camera_to_world = torch.stack( + [ + camera_to_world[0, :], + camera_to_world[2, :], + camera_to_world[1, :], + ], + dim=0, + ) + + camera_type_msg = cam_msg.camera_type + if camera_type_msg == "perspective": + camera_type = CameraType.PERSPECTIVE + elif camera_type_msg == "fisheye": + camera_type = CameraType.FISHEYE + elif camera_type_msg == "equirectangular": + camera_type = CameraType.EQUIRECTANGULAR + else: + camera_type = CameraType.PERSPECTIVE + + camera = Cameras( + fx=intrinsics_matrix[0, 0], + fy=intrinsics_matrix[1, 1], + cx=intrinsics_matrix[0, 2], + cy=intrinsics_matrix[1, 2], + camera_type=camera_type, + camera_to_worlds=camera_to_world[None, ...], + times=torch.tensor([self.viewer.control_panel.time], dtype=torch.float32), + ) + camera = camera.to(self.viewer.get_model().device) + + with self.viewer.train_lock if self.viewer.train_lock is not None else contextlib.nullcontext(): + camera_ray_bundle = camera.generate_rays(camera_indices=0, aabb_box=self.viewer.get_model().render_aabb) + + with TimeWriter(None, None, write=False) as vis_t: + self.viewer.get_model().eval() + step = self.viewer.step + if self.viewer.control_panel.crop_viewport: + color = self.viewer.control_panel.background_color + if color is None: + background_color = torch.tensor([0.0, 0.0, 0.0], device=self.viewer.pipeline.model.device) + else: + background_color = torch.tensor( + [color[0] / 255.0, color[1] / 255.0, color[2] / 255.0], + device=self.viewer.get_model().device, + ) + with background_color_override_context(background_color), torch.no_grad(): + outputs = self.viewer.get_model().get_outputs_for_camera_ray_bundle(camera_ray_bundle) + else: + with torch.no_grad(): + outputs = self.viewer.get_model().get_outputs_for_camera_ray_bundle(camera_ray_bundle) + self.viewer.get_model().train() + num_rays = len(camera_ray_bundle) + render_time = vis_t.duration + if writer.is_initialized(): + writer.put_time( + name=EventName.VIS_RAYS_PER_SEC, duration=num_rays / render_time, step=step, avg_over_steps=True + ) + self.viewer.viser_server.send_status_message(eval_res=f"{image_height}x{image_width}px", step=step) + return outputs + + def run(self): + """Main loop for the render thread""" + while True: + self.render_trigger.wait() + self.render_trigger.clear() + action = self.next_action + assert action is not None, "Action should never be None at this point" + self.next_action = None + if self.state == "high" and action.action == "static": + # if we are in high res and we get a static action, we don't need to do anything + continue + self.state = self.transitions[self.state][action.action] + try: + with viewer_utils.SetTrace(self.check_interrupt): + outputs = self._render_img(action.cam_msg) + except viewer_utils.IOChangeException: + # if we got interrupted, don't send the output to the viewer + continue + self._send_output_to_viewer(outputs) + # if we rendered a static low res, we need to self-trigger a static high-res + if self.state == "low_static": + self.action(RenderAction("static", action.cam_msg)) + + def check_interrupt(self, frame, event, arg): # pylint: disable=unused-argument + """Raises interrupt when flag has been set and not already on lowest resolution. + Used in conjunction with SetTrace. + """ + if event == "line": + if self.interrupt_render_flag: + self.interrupt_render_flag = False + raise viewer_utils.IOChangeException + return self.check_interrupt + + def _send_output_to_viewer(self, outputs: Dict[str, Any]): + """Chooses the correct output and sends it to the viewer + + Args: + outputs: the dictionary of outputs to choose from, from the model + """ + output_keys = set(outputs.keys()) + if self.output_keys != output_keys: + self.output_keys = output_keys + self.viewer.viser_server.send_output_options_message(list(outputs.keys())) + self.viewer.control_panel.update_output_options(list(outputs.keys())) + + output_render = self.viewer.control_panel.output_render + self.viewer.update_colormap_options( + dimensions=outputs[output_render].shape[-1], dtype=outputs[output_render].dtype + ) + selected_output = colormaps.apply_colormap( + image=outputs[self.viewer.control_panel.output_render], + colormap_options=self.viewer.control_panel.colormap_options, + ) + + if self.viewer.control_panel.split: + split_output_render = self.viewer.control_panel.split_output_render + self.viewer.update_colormap_options( + dimensions=outputs[split_output_render].shape[-1], dtype=outputs[split_output_render].dtype + ) + split_output = colormaps.apply_colormap( + image=outputs[self.viewer.control_panel.split_output_render], + colormap_options=self.viewer.control_panel.split_colormap_options, + ) + split_index = min( + int(self.viewer.control_panel.split_percentage * selected_output.shape[1]), + selected_output.shape[1] - 1, + ) + selected_output = torch.cat([selected_output[:, :split_index], split_output[:, split_index:]], dim=1) + selected_output[:, split_index] = torch.tensor([0.133, 0.157, 0.192], device=selected_output.device) + + selected_output = (selected_output * 255).type(torch.uint8) + + self.viewer.viser_server.set_background_image( + selected_output.cpu().numpy(), + file_format=self.viewer.config.image_format, + quality=self.viewer.config.jpeg_quality, + ) + + def _calculate_image_res(self, aspect_ratio: float) -> Tuple[int, int]: + """Calculate the maximum image height that can be rendered in the time budget + + Args: + apect_ratio: the aspect ratio of the current view + Returns: + image_height: the maximum image height that can be rendered in the time budget + image_width: the maximum image width that can be rendered in the time budget + """ + max_res = self.viewer.control_panel.max_res + if self.state == "high": + # high res is always static + image_height = max_res + image_width = int(image_height * aspect_ratio) + if image_width > max_res: + image_width = max_res + image_height = int(image_width / aspect_ratio) + elif self.state in ("low_move", "low_static"): + if writer.is_initialized() and EventName.VIS_RAYS_PER_SEC.value in GLOBAL_BUFFER["events"]: + vis_rays_per_sec = GLOBAL_BUFFER["events"][EventName.VIS_RAYS_PER_SEC.value]["avg"] + else: + vis_rays_per_sec = 100000 + target_fps = self.target_fps + num_vis_rays = vis_rays_per_sec / target_fps + image_height = (num_vis_rays / aspect_ratio) ** 0.5 + image_height = int(round(image_height, -1)) + image_height = max(min(max_res, image_height), 30) + image_width = int(image_height * aspect_ratio) + if image_width > max_res: + image_width = max_res + image_height = int(image_width / aspect_ratio) + else: + raise ValueError(f"Invalid state: {self.state}") + + return image_height, image_width diff --git a/nerfstudio/viewer/server/subprocess.py b/nerfstudio/viewer/server/subprocess.py deleted file mode 100644 index 3e07e2d6b..000000000 --- a/nerfstudio/viewer/server/subprocess.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright 2022 The Nerfstudio Team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Code to use the viewer as a subprocess. -""" - -import atexit -import os -import signal -import socket -import subprocess -import sys -import threading -import time -from typing import Optional, Union - -from rich.console import Console - -from nerfstudio.viewer.server import server - -CONSOLE = Console() - - -def run_viewer_bridge_server_as_subprocess( - websocket_port: int, - zmq_port: Optional[int] = None, - ip_address: str = "127.0.0.1", - log_filename: Union[str, None] = None, -): - """Runs the viewer bridge server as a subprocess. - - Args: - zmq_port: Port to use for the ZMQ server. - websocket_port: Port to use for the websocket server. - ip_address: host to connect to - log_filename: Filename to use for the log file. If None, no log file is created. - - Returns: - None - """ - args = [sys.executable, "-u", "-m", server.__name__] - - # find an available port for zmq - if zmq_port is None: - sock = socket.socket() - sock.bind(("", 0)) - zmq_port = sock.getsockname()[1] - string = f"Using ZMQ port: {zmq_port}" - CONSOLE.print(f"[bold yellow]{string}") - - args.append("--zmq-port") - args.append(str(zmq_port)) - args.append("--websocket-port") - args.append(str(websocket_port)) - args.append("--ip-address") - args.append(str(ip_address)) - # supress output if no log filename is specified - logfile = open( # pylint: disable=consider-using-with - log_filename if log_filename else os.devnull, "w", encoding="utf8" - ) - process = subprocess.Popen( # pylint: disable=consider-using-with - args, stdout=logfile, stderr=logfile, start_new_session=True - ) - - def cleanup(process): - process.kill() - process.wait() - - def poll_process(): - """ - Continually check to see if the viewer bridge server process is still running and has not failed. - If it fails, alert the user and exit the entire program. - """ - while process.poll() is None: - time.sleep(0.5) - string = f"\nThe viewer bridge server subprocess failed. Please check the log file {log_filename}.\n" - string += ( - "You likely have to modify --viewer.zmq-port and/or --viewer.websocket-port in the " - "config to avoid conflicting ports.\n" - ) - string += "Try modifying --viewer.websocket-port 7007\n" - CONSOLE.print(f"[bold red]{string}") - cleanup(process) - # This exists the entire program. sys.exit() will only kill the thread that this runs in. - os.kill(os.getpid(), signal.SIGKILL) - - # continually check to see if the process stopped - t1 = threading.Thread(target=poll_process) - t1.daemon = True - t1.start() - atexit.register(cleanup, process) - return zmq_port diff --git a/nerfstudio/viewer/server/utils.py b/nerfstudio/viewer/server/utils.py index b246790ef..3b81cd5d7 100644 --- a/nerfstudio/viewer/server/utils.py +++ b/nerfstudio/viewer/server/utils.py @@ -15,12 +15,16 @@ """Generic utility functions """ -import sys -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Callable, List, Optional, Tuple import numpy as np import torch +<<<<<<< HEAD import zmq +======= + +from nerfstudio.viewer.viser.messages import CameraMessage +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb def get_chunks( @@ -61,7 +65,7 @@ def three_js_perspective_camera_focal_length(fov: float, image_height: int): def get_intrinsics_matrix_and_camera_to_world_h( - camera_object: Dict[str, Any], image_height: int, image_width: Optional[int] = None + camera_message: CameraMessage, image_height: int, image_width: Optional[int] = None ) -> Tuple[torch.Tensor, torch.Tensor]: """Returns the camera intrinsics matrix and the camera to world homogeneous matrix. @@ -70,17 +74,17 @@ def get_intrinsics_matrix_and_camera_to_world_h( image_size: the size of the image (height, width) """ # intrinsics - fov = camera_object["fov"] - aspect = camera_object["aspect"] + fov = camera_message.fov + aspect = camera_message.aspect if image_width is None: image_width = aspect * image_height pp_w = image_width / 2.0 pp_h = image_height / 2.0 - if (camera_object["camera_type"] == "perspective") | (camera_object["camera_type"] == "fisheye"): + if (camera_message.camera_type == "perspective") | (camera_message.camera_type == "fisheye"): focal_length = three_js_perspective_camera_focal_length(fov, image_height) intrinsics_matrix = torch.tensor([[focal_length, 0, pp_w], [0, focal_length, pp_h], [0, 0, 1]]).float() - elif camera_object["camera_type"] == "equirectangular": - render_aspect = camera_object["render_aspect"] + elif camera_message.camera_type == "equirectangular": + render_aspect = camera_message.render_aspect if aspect < render_aspect: intrinsics_matrix = torch.tensor( [[pp_w, 0, pp_w], [0, image_width / render_aspect, pp_h], [0, 0, 1]] @@ -91,7 +95,7 @@ def get_intrinsics_matrix_and_camera_to_world_h( ).float() # extrinsics - camera_to_world_h = torch.tensor(get_chunks(camera_object["matrix"], size_of_chunk=4)).T.float() + camera_to_world_h = torch.tensor(get_chunks(camera_message.matrix, size_of_chunk=4)).T.float() camera_to_world_h = torch.stack( [ camera_to_world_h[0, :], @@ -117,8 +121,6 @@ def find_available_port(func: Callable, default_port: int, max_attempts: int = 1 port = default_port + i try: return func(port, **kwargs), port - except (OSError, zmq.error.ZMQError): - print(f"Port: {port:d} in use, trying another...", file=sys.stderr) except Exception as e: print(type(e)) raise diff --git a/nerfstudio/viewer/server/viewer_elements.py b/nerfstudio/viewer/server/viewer_elements.py new file mode 100644 index 000000000..fbf60bfb6 --- /dev/null +++ b/nerfstudio/viewer/server/viewer_elements.py @@ -0,0 +1,357 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=too-few-public-methods,access-member-before-definition,attribute-defined-outside-init + +""" Viewer GUI elements for the nerfstudio viewer """ + +from __future__ import annotations + +from abc import abstractmethod +from typing import Any, Callable, Generic, List, Optional, Tuple + +from typing_extensions import TypeVar + +from nerfstudio.viewer.viser import GuiHandle, GuiSelectHandle, ViserServer + +TValue = TypeVar("TValue") + + +class ViewerElement(Generic[TValue]): + """Base class for all viewer elements + + Args: + name: The name of the element + disabled: If the element is disabled + """ + + def __init__( + self, + name: str, + disabled: bool = False, + cb_hook: Callable = lambda element: None, + ) -> None: + self.name = name + self.gui_handle: Optional[GuiHandle[TValue]] = None + self.disabled = disabled + self.cb_hook = cb_hook + + @abstractmethod + def _create_gui_handle(self, viser_server: ViserServer) -> None: + """ + Returns the GuiHandle object which actually controls the parameter in the gui. + + Args: + viser_server: The server to install the gui element into. + """ + ... + + def remove(self) -> None: + """Removes the gui element from the viewer""" + if self.gui_handle is not None: + self.gui_handle.remove() + self.gui_handle = None + + def set_hidden(self, hidden: bool) -> None: + """Sets the hidden state of the gui element""" + assert self.gui_handle is not None + self.gui_handle.set_hidden(hidden) + + def set_disabled(self, disabled: bool) -> None: + """Sets the disabled state of the gui element""" + assert self.gui_handle is not None + self.gui_handle.set_disabled(disabled) + + @abstractmethod + def install(self, viser_server: ViserServer) -> None: + """Installs the gui element into the given viser_server""" + ... + + +class ViewerButton(ViewerElement[bool]): + """A button in the viewer + + Args: + name: The name of the button + cb_hook: The function to call when the button is pressed + disabled: If the button is disabled + """ + + def __init__(self, name: str, cb_hook: Callable[[ViewerButton], Any], disabled: bool = False): + super().__init__(name, disabled=disabled, cb_hook=cb_hook) + + def _create_gui_handle(self, viser_server: ViserServer) -> None: + self.gui_handle = viser_server.add_gui_button(self.name) + self.gui_handle.set_disabled(self.disabled) + + def install(self, viser_server: ViserServer) -> None: + self._create_gui_handle(viser_server) + + assert self.gui_handle is not None + self.gui_handle.on_update(lambda _: self.cb_hook(self)) + + +class ViewerParameter(ViewerElement[TValue], Generic[TValue]): + """A viewer element with state + + Args: + name: The name of the element + default_value: The default value of the element + disabled: If the element is disabled + cb_hook: Callback to call on update + """ + + def __init__( + self, + name: str, + default_value: TValue, + disabled: bool = False, + cb_hook: Callable = lambda element: None, + ) -> None: + super().__init__(name, disabled=disabled, cb_hook=cb_hook) + self.default_value = default_value + + def install(self, viser_server: ViserServer) -> None: + """ + Based on the type provided by default_value, installs a gui element inside the given viser_server + + Args: + viser_server: The server to install the gui element into. + """ + self._create_gui_handle(viser_server) + + assert self.gui_handle is not None + self.gui_handle.on_update(lambda _: self.cb_hook(self)) + + @abstractmethod + def _create_gui_handle(self, viser_server: ViserServer) -> None: + ... + + @property + def value(self) -> TValue: + """Returns the current value of the viewer element""" + if self.gui_handle is None: + return self.default_value + return self.gui_handle.get_value() + + @value.setter + def value(self, value: TValue) -> None: + if self.gui_handle is not None: + self.gui_handle.set_value(value) + else: + self.default_value = value + + +IntOrFloat = TypeVar("IntOrFloat", int, float) + + +class ViewerSlider(ViewerParameter[IntOrFloat], Generic[IntOrFloat]): + """A slider in the viewer + + Args: + name: The name of the slider + default_value: The default value of the slider + min_value: The minimum value of the slider + max_value: The maximum value of the slider + step: The step size of the slider + disabled: If the slider is disabled + cb_hook: Callback to call on update + """ + + def __init__( + self, + name: str, + default_value: IntOrFloat, + min_value: IntOrFloat, + max_value: IntOrFloat, + step: IntOrFloat = 0.1, + disabled: bool = False, + cb_hook: Callable[[ViewerSlider], Any] = lambda element: None, + ): + assert isinstance(default_value, (float, int)) + super().__init__(name, default_value, disabled=disabled, cb_hook=cb_hook) + self.min = min_value + self.max = max_value + self.step = step + + def _create_gui_handle(self, viser_server: ViserServer) -> None: + assert self.gui_handle is None, "gui_handle should be initialized once" + self.gui_handle = viser_server.add_gui_slider(self.name, self.min, self.max, self.step, self.default_value) + self.gui_handle.set_disabled(self.disabled) + + +class ViewerText(ViewerParameter[str]): + """A text field in the viewer + + Args: + name: The name of the text field + default_value: The default value of the text field + disabled: If the text field is disabled + cb_hook: Callback to call on update + """ + + def __init__( + self, + name: str, + default_value: str, + disabled: bool = False, + cb_hook: Callable[[ViewerText], Any] = lambda element: None, + ): + assert isinstance(default_value, str) + super().__init__(name, default_value, disabled=disabled, cb_hook=cb_hook) + + def _create_gui_handle(self, viser_server: ViserServer) -> None: + assert self.gui_handle is None, "gui_handle should be initialized once" + self.gui_handle = viser_server.add_gui_text(self.name, self.default_value) + self.gui_handle.set_disabled(self.disabled) + + +class ViewerNumber(ViewerParameter[IntOrFloat], Generic[IntOrFloat]): + """A number field in the viewer + + Args: + name: The name of the number field + default_value: The default value of the number field + disabled: If the number field is disabled + cb_hook: Callback to call on update + """ + + default_value: IntOrFloat + + def __init__( + self, + name: str, + default_value: IntOrFloat, + disabled: bool = False, + cb_hook: Callable[[ViewerNumber], Any] = lambda element: None, + ): + assert isinstance(default_value, (float, int)) + super().__init__(name, default_value, disabled=disabled, cb_hook=cb_hook) + + def _create_gui_handle(self, viser_server: ViserServer) -> None: + assert self.gui_handle is None, "gui_handle should be initialized once" + self.gui_handle = viser_server.add_gui_number(self.name, self.default_value) + + +class ViewerCheckbox(ViewerParameter[bool]): + """A checkbox in the viewer + + Args: + name: The name of the checkbox + default_value: The default value of the checkbox + disabled: If the checkbox is disabled + cb_hook: Callback to call on update + """ + + def __init__( + self, + name: str, + default_value: bool, + disabled: bool = False, + cb_hook: Callable[[ViewerCheckbox], Any] = lambda element: None, + ): + assert isinstance(default_value, bool) + super().__init__(name, default_value, disabled=disabled, cb_hook=cb_hook) + + def _create_gui_handle(self, viser_server: ViserServer) -> None: + assert self.gui_handle is None, "gui_handle should be initialized once" + self.gui_handle = viser_server.add_gui_checkbox(self.name, self.default_value) + self.gui_handle.set_disabled(self.disabled) + + +TString = TypeVar("TString", default=str) + + +class ViewerDropdown(ViewerParameter[TString], Generic[TString]): + """A dropdown in the viewer + + Args: + name: The name of the dropdown + default_value: The default value of the dropdown + options: The options of the dropdown + disabled: If the dropdown is disabled + cb_hook: Callback to call on update + """ + + gui_handle: Optional[GuiSelectHandle[str]] + + def __init__( + self, + name: str, + default_value: TString, + options: List[TString], + disabled: bool = False, + cb_hook: Callable[[ViewerDropdown], Any] = lambda element: None, + ): + assert default_value in options + super().__init__(name, default_value, disabled=disabled, cb_hook=cb_hook) + self.options = options + + def _create_gui_handle(self, viser_server: ViserServer) -> None: + assert self.gui_handle is None, "gui_handle should be initialized once" + self.gui_handle = viser_server.add_gui_select(self.name, self.options, self.default_value) + self.gui_handle.set_disabled(self.disabled) + + def set_options(self, new_options: List[str]) -> None: + """ + Sets the options of the dropdown, + + Args: + new_options: The new options. If the current option isn't in the new options, the first option is selected. + """ + self.options = new_options + if self.gui_handle is not None: + self.gui_handle.set_options(new_options) + + +class ViewerRGB(ViewerParameter[Tuple[int, int, int]]): + """ + An RGB color picker for the viewer + """ + + def __init__( + self, + name, + default_value: Tuple[int, int, int], + disabled=False, + cb_hook: Callable[[ViewerRGB], Any] = lambda element: None, + ): + assert len(default_value) == 3 + super().__init__(name, default_value, disabled=disabled, cb_hook=cb_hook) + + def _create_gui_handle(self, viser_server: ViserServer) -> None: + self.gui_handle = viser_server.add_gui_rgb(self.name, self.default_value) + self.gui_handle.set_disabled(self.disabled) + + +class ViewerVec3(ViewerParameter[Tuple[float, float, float]]): + """ + 3 number boxes in a row to input a vector + """ + + def __init__( + self, + name, + default_value: Tuple[float, float, float], + step=0.1, + disabled=False, + cb_hook: Callable[[ViewerVec3], Any] = lambda element: None, + ): + assert len(default_value) == 3 + super().__init__(name, default_value, disabled=disabled, cb_hook=cb_hook) + self.step = step + + def _create_gui_handle(self, viser_server: ViserServer) -> None: + self.gui_handle = viser_server.add_gui_vector3(self.name, self.default_value, self.step) + self.gui_handle.set_disabled(self.disabled) diff --git a/nerfstudio/viewer/server/viewer_state.py b/nerfstudio/viewer/server/viewer_state.py new file mode 100644 index 000000000..513bc480c --- /dev/null +++ b/nerfstudio/viewer/server/viewer_state.py @@ -0,0 +1,379 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Manage the state of the viewer """ +from __future__ import annotations + +import threading +from pathlib import Path +from typing import TYPE_CHECKING, List, Literal, Optional + +import numpy as np +import torch +from rich import box, style +from rich.console import Console +from rich.panel import Panel +from rich.table import Table + +from nerfstudio.configs import base_config as cfg +from nerfstudio.data.datasets.base_dataset import InputDataset +from nerfstudio.data.scene_box import SceneBox +from nerfstudio.models.base_model import Model +from nerfstudio.pipelines.base_pipeline import Pipeline +from nerfstudio.utils.decorators import check_main_thread, decorate_all +from nerfstudio.utils.io import load_from_json, write_to_json +from nerfstudio.utils.writer import GLOBAL_BUFFER, EventName +from nerfstudio.viewer.server import viewer_utils +from nerfstudio.viewer.server.control_panel import ControlPanel +from nerfstudio.viewer.server.gui_utils import parse_object +from nerfstudio.viewer.server.render_state_machine import ( + RenderAction, + RenderStateMachine, +) +from nerfstudio.viewer.server.viewer_elements import ViewerElement +from nerfstudio.viewer.viser import ViserServer +from nerfstudio.viewer.viser.messages import ( + CameraMessage, + CameraPathOptionsRequest, + CameraPathPayloadMessage, + CropParamsMessage, + NerfstudioMessage, + SaveCheckpointMessage, + TimeConditionMessage, + TrainingStateMessage, +) + +if TYPE_CHECKING: + from nerfstudio.engine.trainer import Trainer + +CONSOLE = Console(width=120) + + +@decorate_all([check_main_thread]) +class ViewerState: + """Class to hold state for viewer variables + + Args: + config: viewer setup configuration + log_filename: filename to log viewer output to + datapath: path to data + pipeline: pipeline object to use + trainer: trainer object to use + + Attributes: + viewer_url: url to open viewer + """ + + viewer_url: str + + def __init__( + self, + config: cfg.ViewerConfig, + log_filename: Path, + datapath: Path, + pipeline: Pipeline, + trainer: Optional[Trainer] = None, + train_lock: Optional[threading.Lock] = None, + ): + self.config = config + self.trainer = trainer + self.last_step = 0 + self.train_lock = train_lock + self.pipeline = pipeline + self.log_filename = log_filename + self.datapath = datapath.parent if datapath.is_file() else datapath + + if self.config.websocket_port is None: + websocket_port = viewer_utils.get_free_port(default_port=self.config.websocket_port_default) + else: + websocket_port = self.config.websocket_port + self.log_filename.parent.mkdir(exist_ok=True) + + self.viewer_url = viewer_utils.get_viewer_url(websocket_port) + table = Table( + title=None, + show_header=False, + box=box.MINIMAL, + title_style=style.Style(bold=True), + ) + table.add_row("HTTP", f"[link={self.viewer_url}][blue]{self.viewer_url}[/link]") + + CONSOLE.print(Panel(table, title="[bold][yellow]Viewer[/bold]", expand=False)) + + self.include_time = self.pipeline.datamanager.includes_time + + # viewer specific variables + self.output_type_changed = True + self.output_split_type_changed = True + self.step = 0 + self.train_btn_state: Literal["training", "paused", "completed"] = "training" + self._prev_train_state: Literal["training", "paused", "completed"] = "training" + + self.camera_message = None + + self.viser_server = ViserServer(host=config.websocket_host, port=websocket_port) + + self.viser_server.register_handler(TrainingStateMessage, self._handle_training_state_message) + self.viser_server.register_handler(SaveCheckpointMessage, self._handle_save_checkpoint) + self.viser_server.register_handler(CameraMessage, self._handle_camera_update) + self.viser_server.register_handler(CameraPathOptionsRequest, self._handle_camera_path_option_request) + self.viser_server.register_handler(CameraPathPayloadMessage, self._handle_camera_path_payload) + self.viser_server.register_handler(CropParamsMessage, self._handle_crop_params_message) + if self.include_time: + self.viser_server.use_time_conditioning() + self.viser_server.register_handler(TimeConditionMessage, self._handle_time_condition_message) + + self.control_panel = ControlPanel( + self.viser_server, + self.include_time, + self._interrupt_render, + self._crop_params_update, + self._output_type_change, + self._output_split_type_change, + ) + + def nested_folder_install(folder_labels: List[str], element: ViewerElement): + if len(folder_labels) == 0: + element.install(self.viser_server) + # also rewire the hook to rerender + prev_cb = element.cb_hook + element.cb_hook = lambda element: [self._interrupt_render(element), prev_cb(element)] + else: + with self.viser_server.gui_folder(folder_labels[0]): + nested_folder_install(folder_labels[1:], element) + + self.viewer_elements = [] + if self.trainer is not None: + self.viewer_elements.extend(parse_object(self.trainer, ViewerElement, "Trainer")) + else: + self.viewer_elements.extend(parse_object(pipeline, ViewerElement, "Pipeline")) + for param_path, element in self.viewer_elements: + folder_labels = param_path.split("/")[:-1] + nested_folder_install(folder_labels, element) + + self.render_statemachine = RenderStateMachine(self) + self.render_statemachine.start() + + def _output_type_change(self, _): + self.output_type_changed = True + + def _output_split_type_change(self, _): + self.output_split_type_changed = True + + def _interrupt_render(self, _) -> None: + """Interrupt current render.""" + if self.camera_message is not None: + self.render_statemachine.action(RenderAction("rerender", self.camera_message)) + + def _crop_params_update(self, _) -> None: + """Update crop parameters""" + self.render_statemachine.action(RenderAction("rerender", self.camera_message)) + crop_min = torch.tensor(self.control_panel.crop_min, dtype=torch.float32) + crop_max = torch.tensor(self.control_panel.crop_max, dtype=torch.float32) + scene_box = SceneBox(aabb=torch.stack([crop_min, crop_max], dim=0)) + self.viser_server.update_scene_box(scene_box) + crop_scale = crop_max - crop_min + crop_center = (crop_max + crop_min) / 2.0 + self.viser_server.send_crop_params( + crop_enabled=self.control_panel.crop_viewport, + crop_bg_color=self.control_panel.background_color, + crop_scale=tuple(crop_scale.tolist()), + crop_center=tuple(crop_center.tolist()), + ) + + def _handle_training_state_message(self, message: NerfstudioMessage) -> None: + """Handle training state message from viewer.""" + assert isinstance(message, TrainingStateMessage) + self.train_btn_state = message.training_state + self.training_state = message.training_state + self.viser_server.set_training_state(message.training_state) + + def _handle_save_checkpoint(self, message: NerfstudioMessage) -> None: + """Handle save checkpoint message from viewer.""" + assert isinstance(message, SaveCheckpointMessage) + if self.trainer is not None: + self.trainer.save_checkpoint(self.step) + + def _handle_camera_update(self, message: NerfstudioMessage) -> None: + """Handle camera update message from viewer.""" + assert isinstance(message, CameraMessage) + self.camera_message = message + if message.is_moving: + self.render_statemachine.action(RenderAction("move", self.camera_message)) + if self.training_state == "training": + self.training_state = "paused" + else: + self.render_statemachine.action(RenderAction("static", self.camera_message)) + self.training_state = self.train_btn_state + + def _handle_camera_path_option_request(self, message: NerfstudioMessage) -> None: + """Handle camera path option request message from viewer.""" + assert isinstance(message, CameraPathOptionsRequest) + camera_path_dir = self.datapath / "camera_paths" + if camera_path_dir.exists(): + all_path_dict = {} + for path in camera_path_dir.iterdir(): + if path.suffix == ".json": + all_path_dict[path.stem] = load_from_json(path) + self.viser_server.send_camera_paths(all_path_dict) + + def _handle_camera_path_payload(self, message: NerfstudioMessage) -> None: + """Handle camera path payload message from viewer.""" + assert isinstance(message, CameraPathPayloadMessage) + camera_path_filename = message.camera_path_filename + ".json" + camera_path = message.camera_path + camera_paths_directory = self.datapath / "camera_paths" + camera_paths_directory.mkdir(parents=True, exist_ok=True) + write_to_json(camera_paths_directory / camera_path_filename, camera_path) + + def _handle_crop_params_message(self, message: NerfstudioMessage) -> None: + """Handle crop parameters message from viewer.""" + assert isinstance(message, CropParamsMessage) + self.control_panel.crop_viewport = message.crop_enabled + self.control_panel.background_color = message.crop_bg_color + center = np.array(message.crop_center) + scale = np.array(message.crop_scale) + crop_min = center - scale / 2.0 + crop_max = center + scale / 2.0 + self.control_panel.crop_min = tuple(crop_min.tolist()) + self.control_panel.crop_max = tuple(crop_max.tolist()) + + def _handle_time_condition_message(self, message: NerfstudioMessage) -> None: + """Handle time conditioning message from viewer.""" + assert isinstance(message, TimeConditionMessage) + self.control_panel.time = message.time + + @property + def training_state(self) -> Literal["training", "paused", "completed"]: + """Get training state flag.""" + if self.trainer is not None: + return self.trainer.training_state + return self.train_btn_state + + @training_state.setter + def training_state(self, training_state: Literal["training", "paused", "completed"]) -> None: + """Set training state flag.""" + if self.trainer is not None: + self.trainer.training_state = training_state + + def _pick_drawn_image_idxs(self, total_num: int) -> list[int]: + """Determine indicies of images to display in viewer. + + Args: + total_num: total number of training images. + + Returns: + List of indices from [0, total_num-1]. + """ + if self.config.max_num_display_images < 0: + num_display_images = total_num + else: + num_display_images = min(self.config.max_num_display_images, total_num) + # draw indices, roughly evenly spaced + return np.linspace(0, total_num - 1, num_display_images, dtype=np.int32).tolist() + + def init_scene(self, dataset: InputDataset, train_state=Literal["training", "paused", "completed"]) -> None: + """Draw some images and the scene aabb in the viewer. + + Args: + dataset: dataset to render in the scene + train_state: Current status of training + """ + self.viser_server.send_file_path_info( + config_base_dir=self.log_filename.parents[0], + data_base_dir=self.datapath, + export_path_name=self.log_filename.parent.stem, + ) + + # draw the training cameras and images + image_indices = self._pick_drawn_image_idxs(len(dataset)) + for idx in image_indices: + image = dataset[idx]["image"] + bgr = image[..., [2, 1, 0]] + camera_json = dataset.cameras.to_json(camera_idx=idx, image=bgr, max_size=100) + self.viser_server.add_dataset_image(idx=f"{idx:06d}", json=camera_json) + + # draw the scene box (i.e., the bounding box) + self.viser_server.update_scene_box(dataset.scene_box) + + # set the initial state whether to train or not + self.train_btn_state = train_state + self.viser_server.set_training_state(train_state) + + def update_scene(self, step: int, num_rays_per_batch: Optional[int] = None) -> None: + """updates the scene based on the graph weights + + Args: + step: iteration step of training + num_rays_per_batch: number of rays per batch, used during training + """ + self.step = step + + if self.camera_message is None: + return + + if ( + self.trainer is not None + and self.trainer.training_state == "training" + and self.control_panel.train_util != 1 + ): + if ( + EventName.TRAIN_RAYS_PER_SEC.value in GLOBAL_BUFFER["events"] + and EventName.VIS_RAYS_PER_SEC.value in GLOBAL_BUFFER["events"] + ): + train_s = GLOBAL_BUFFER["events"][EventName.TRAIN_RAYS_PER_SEC.value]["avg"] + vis_s = GLOBAL_BUFFER["events"][EventName.VIS_RAYS_PER_SEC.value]["avg"] + train_util = self.control_panel.train_util + vis_n = self.control_panel.max_res**2 + train_n = num_rays_per_batch + train_time = train_n / train_s + vis_time = vis_n / vis_s + + render_freq = train_util * vis_time / (train_time - train_util * train_time) + else: + render_freq = 30 + if step > self.last_step + render_freq: + self.last_step = step + self.render_statemachine.action(RenderAction("step", self.camera_message)) + + def update_colormap_options(self, dimensions: int, dtype: type) -> None: + """update the colormap options based on the current render + + Args: + dimensions: the number of dimensions of the render + dtype: the data type of the render + """ + if self.output_type_changed: + self.control_panel.update_colormap_options(dimensions, dtype) + self.output_type_changed = False + + def update_split_colormap_options(self, dimensions: int, dtype: type) -> None: + """update the colormap options based on the current render + + Args: + dimensions: the number of dimensions of the render + dtype: the data type of the render + """ + if self.output_split_type_changed: + self.control_panel.update_split_colormap_options(dimensions, dtype) + self.output_split_type_changed = False + + def get_model(self) -> Model: + """Returns the model.""" + return self.pipeline.model + + def training_complete(self) -> None: + """Called when training is complete.""" + self.training_state = "completed" + self.viser_server.set_training_state("completed") diff --git a/nerfstudio/viewer/server/viewer_utils.py b/nerfstudio/viewer/server/viewer_utils.py index 89d66766f..34448dc52 100644 --- a/nerfstudio/viewer/server/viewer_utils.py +++ b/nerfstudio/viewer/server/viewer_utils.py @@ -14,33 +14,34 @@ # pylint: disable=too-many-lines -"""Code to interface with the `vis/` (the JS viewer). -""" +"""Code to interface with the `vis/` (the JS viewer).""" from __future__ import annotations +<<<<<<< HEAD import base64 import enum import os +======= +import os +import socket +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb import sys -import threading -import time -import warnings from pathlib import Path -from typing import Any, Dict, Optional, Tuple +from typing import TYPE_CHECKING, Optional, Tuple +<<<<<<< HEAD import cv2 import numpy as np import torch from cryptography.utils import CryptographyDeprecationWarning +======= +import torch +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb from rich.console import Console -from nerfstudio.cameras.cameras import Cameras, CameraType -from nerfstudio.cameras.rays import RayBundle -from nerfstudio.configs import base_config as cfg -from nerfstudio.data.datasets.base_dataset import InputDataset from nerfstudio.data.scene_box import SceneBox -from nerfstudio.model_components import renderers from nerfstudio.models.base_model import Model +<<<<<<< HEAD from nerfstudio.utils import colormaps, profiler, writer from nerfstudio.utils.decorators import check_main_thread, decorate_all from nerfstudio.utils.io import load_from_json, write_to_json @@ -48,52 +49,34 @@ from nerfstudio.utils.writer import GLOBAL_BUFFER, EventName, TimeWriter from nerfstudio.viewer.server.subprocess import run_viewer_bridge_server_as_subprocess from nerfstudio.viewer.server.utils import get_intrinsics_matrix_and_camera_to_world_h from nerfstudio.viewer.server.visualizer import Viewer +======= +from nerfstudio.utils.io import load_from_json +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb -warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning) +if TYPE_CHECKING: + from nerfstudio.engine.trainer import Trainer CONSOLE = Console(width=120) def get_viewer_version() -> str: - """Get the version of the viewer.""" + """Return the version of the viewer.""" json_filename = os.path.join(os.path.dirname(__file__), "../app/package.json") version = load_from_json(Path(json_filename))["version"] return version -@check_main_thread -def setup_viewer(config: cfg.ViewerConfig, log_filename: Path, datapath: Path): - """Sets up the viewer if enabled +def get_viewer_url(websocket_port: int) -> str: + """Generate URL for the viewer. Args: - config: the configuration to instantiate viewer - log_filename: the log filename to write to - datapath: the path to the dataset + websocket_port: port to connect to the viewer + Returns: + URL to the viewer """ - viewer_state = ViewerState(config, log_filename=log_filename, datapath=datapath) - banner_messages = [f"Viewer at: {viewer_state.viewer_url}"] - return viewer_state, banner_messages - - -class OutputTypes(str, enum.Enum): - """Noncomprehensive list of output render types""" - - INIT = "init" - RGB = "rgb" - RGB_FINE = "rgb_fine" - ACCUMULATION = "accumulation" - ACCUMULATION_FINE = "accumulation_fine" - - -class ColormapTypes(str, enum.Enum): - """List of colormap render types""" - - DEFAULT = "default" - TURBO = "turbo" - VIRIDIS = "viridis" - MAGMA = "magma" - INFERNO = "inferno" - CIVIDIS = "cividis" + version = get_viewer_version() + websocket_url = f"ws://localhost:{websocket_port}" + return f"https://viewer.nerf.studio/versions/{version}/?websocket_url={websocket_url}" class IOChangeException(Exception): @@ -114,140 +97,57 @@ class SetTrace: sys.settrace(None) -class RenderThread(threading.Thread): - """Thread that does all the rendering calls while listening for interrupts +def is_port_open(port: int): + """Returns True if the port is open. Args: - state: current viewer state object - graph: current checkpoint of model - camera_ray_bundle: input rays to pass through the graph to render out + port: Port to check. + + Returns: + True if the port is open, False otherwise. """ - - def __init__(self, state: "ViewerState", graph: Model, camera_ray_bundle: RayBundle): - threading.Thread.__init__(self) - self.state = state - self.graph = graph - self.camera_ray_bundle = camera_ray_bundle - self.exc = None - self.vis_outputs = None - - def run(self): - """run function that renders out images given the current graph and ray bundles. - Interlaced with a trace function that checks to see if any I/O changes were registered. - Exits and continues program if IOChangeException thrown. - """ - outputs = None - try: - with SetTrace(self.state.check_interrupt): - if self.state.prev_crop_enabled: - color = self.state.prev_crop_bg_color - if color is None: - background_color = torch.tensor([0.0, 0.0, 0.0], device=self.graph.device) - else: - background_color = torch.tensor( - [color["r"] / 255.0, color["g"] / 255.0, color["b"] / 255.0], device=self.graph.device - ) - with renderers.background_color_override_context(background_color), torch.no_grad(): - outputs = self.graph.get_outputs_for_camera_ray_bundle(self.camera_ray_bundle) - else: - with torch.no_grad(): - outputs = self.graph.get_outputs_for_camera_ray_bundle(self.camera_ray_bundle) - except Exception as e: # pylint: disable=broad-except - self.exc = e - - if outputs: - self.vis_outputs = outputs - - self.state.check_done_render = True - self.state.check_interrupt_vis = False - - def join(self, timeout=None): - threading.Thread.join(self) - if self.exc: - raise self.exc + try: + sock = socket.socket() + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + _ = sock.bind(("", port)) + sock.close() + return True + except OSError: + return False -class CheckThread(threading.Thread): - """Thread the constantly checks for io changes and sets a flag indicating interrupt +def get_free_port(default_port: Optional[int] = None): + """Returns a free port on the local machine. Try to use default_port if possible. Args: - state: current viewer state object + default_port: Port to try to use. + + Returns: + A free port on the local machine. """ - - def __init__(self, state): - threading.Thread.__init__(self) - self.state = state - - def run(self): - """Run function that checks to see if any of the existing state has changed - (e.g. camera pose/output type/resolutions). - Sets the viewer state flag to true to signal - to render thread that an interrupt was registered. - """ - self.state.check_done_render = False - while not self.state.check_done_render: - # check camera - data = self.state.vis["renderingState/camera"].read() - render_time = self.state.vis["renderingState/render_time"].read() - if data is not None: - camera_object = data["object"] - if ( - self.state.prev_camera_matrix is None - or ( - not np.allclose(camera_object["matrix"], self.state.prev_camera_matrix) - and not self.state.prev_moving - ) - or (render_time is not None and render_time != self.state.prev_render_time) - ): - self.state.check_interrupt_vis = True - self.state.prev_moving = True - return - self.state.prev_moving = False - - # check output type - output_type = self.state.vis["renderingState/output_choice"].read() - if output_type is None: - output_type = OutputTypes.INIT - if self.state.prev_output_type != output_type: - self.state.check_interrupt_vis = True - return - - # check colormap type - colormap_type = self.state.vis["renderingState/colormap_choice"].read() - if self.state.prev_colormap_type != colormap_type: - self.state.check_interrupt_vis = True - return - - colormap_range = self.state.vis["renderingState/colormap_range"].read() - if self.state.prev_colormap_range != colormap_range: - self.state.check_interrupt_vis = True - return - - # check max render - max_resolution = self.state.vis["renderingState/maxResolution"].read() - if max_resolution is not None: - if self.state.max_resolution != max_resolution: - self.state.check_interrupt_vis = True - return - - # check crop changes - crop_enabled = self.state.vis["renderingState/crop_enabled"].read() - if crop_enabled is not None: - if self.state.prev_crop_enabled != crop_enabled: - self.state.check_interrupt_vis = True - return + if default_port is not None: + if is_port_open(default_port): + return default_port + sock = socket.socket() + sock.bind(("", 0)) + port = sock.getsockname()[1] + return port -@decorate_all([check_main_thread]) -class ViewerState: - """Class to hold state for viewer variables +def update_render_aabb( + crop_viewport: bool, crop_min: Tuple[float, float, float], crop_max: Tuple[float, float, float], model: Model +): + """ + update the render aabb box for the viewer: Args: - config: viewer setup configuration - log_filename: filename to log viewer output to - datapath: path to data + crop_viewport: whether to crop the viewport + crop_min: min of the crop box + crop_max: max of the crop box + model: the model to render """ +<<<<<<< HEAD def __init__(self, config: cfg.ViewerConfig, log_filename: Path, datapath: Path): self.config = config self.vis = None @@ -917,3 +817,16 @@ class ViewerState: self._update_viewer_stats( vis_t.duration, num_rays=len(camera_ray_bundle), image_height=image_height, image_width=image_width ) +======= + if crop_viewport: + crop_min = torch.tensor(crop_min, dtype=torch.float32) + crop_max = torch.tensor(crop_max, dtype=torch.float32) + + if isinstance(model.render_aabb, SceneBox): + model.render_aabb.aabb[0] = crop_min + model.render_aabb.aabb[1] = crop_max + else: + model.render_aabb = SceneBox(aabb=torch.stack([crop_min, crop_max], dim=0)) + else: + model.render_aabb = None +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb diff --git a/nerfstudio/viewer/server/visualizer.py b/nerfstudio/viewer/server/visualizer.py deleted file mode 100644 index a30500771..000000000 --- a/nerfstudio/viewer/server/visualizer.py +++ /dev/null @@ -1,155 +0,0 @@ -# Copyright 2022 The Nerfstudio Team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Code to connect and send commands to the viewer. -""" - -import sys -from threading import Thread -from typing import Dict, Optional, Union - -import umsgpack -import zmq -from rich.console import Console - -from nerfstudio.viewer.server.path import Path - -CONSOLE = Console(width=120) - - -class ViewerWindow: - """The viewer window has the ZMQ connection to the viewer bridge server. - - Args: - zmq_port: Where to connect with ZMQ. - ip_address: The ip address of the bridge server. - """ - - context = zmq.Context() # pylint: disable=abstract-class-instantiated - - def __init__(self, zmq_port, ip_address="127.0.0.1"): - self.zmq_port = zmq_port - self.client = self.context.socket(zmq.REQ) - zmq_url = f"tcp://{ip_address}:{self.zmq_port}" - self.client.connect(zmq_url) - self.assert_connected() - - def send(self, command): - """Sends a command to the viewer bridge server.""" - self.client.send_multipart( - [ - command["type"].encode("utf-8"), - command["path"].encode("utf-8"), - umsgpack.packb(command), - ] - ) - return umsgpack.unpackb(self.client.recv()) - - def send_ping(self): - """Tries to contact the viewer bridge server.""" - type_ = "ping" - path = "" - data = umsgpack.packb({"type": type_, "path": path}) - self.client.send_multipart( - [ - type_.encode("utf-8"), - path.encode("utf-8"), - data, - ] - ) - return umsgpack.unpackb(self.client.recv()) - - def timeout_ping(self, timeout_in_sec: int = 15): - """Timeout if ping fails to complete in timeout_in_secs seconds""" - - res = [Exception(f"Couldn't connect to the viewer Bridge Server in {timeout_in_sec} seconds. Exiting.")] - - def wrapper_func(): - res[0] = self.send_ping() - - t = Thread(target=wrapper_func) - t.daemon = True - try: - t.start() - t.join(timeout_in_sec) - except Exception as je: - CONSOLE.log("Error starting thread") - raise je - ret = res[0] - if isinstance(ret, BaseException): - raise ret - return ret - - def assert_connected(self, timeout_in_sec: int = 15): - """Check if the connection was established properly within some time. - - Args: - timeout_in_sec (int): The maximum time to wait for the connection to be established. - """ - try: - CONSOLE.print("Sending ping to the viewer Bridge Server...") - _ = self.timeout_ping(timeout_in_sec) - CONSOLE.print("Successfully connected.") - - except Exception as e: # pylint: disable=broad-except - CONSOLE.log(e) - sys.exit() - - -class Viewer: - """Viewer class for connecting to the bridge server. - - Args: - zmq_port: Where to connect with ZMQ. - window: An already existing ViewerWindow. - ip_address: The ip address of the bridge server. - """ - - def __init__( - self, zmq_port: Optional[int] = None, window: Optional[ViewerWindow] = None, ip_address: str = "127.0.0.1" - ): - if zmq_port is None and window is None: - raise ValueError("Must specify either zmq_port or window.") - if window is None: - self.window = ViewerWindow(zmq_port=zmq_port, ip_address=ip_address) - else: - self.window = window - self.path = Path(()) - - @staticmethod - def view_into(window: ViewerWindow, path: Path): - """Returns a new Viewer but keeping the same ViewerWindow.""" - vis = Viewer(window=window) - vis.path = path - return vis - - def __getitem__(self, path): - return Viewer.view_into(self.window, self.path.append(path)) - - def __repr__(self): - return f"" - - def write(self, data: Union[Dict, str, None] = None): - """Write data.""" - path = self.path.lower() - return self.window.send({"type": "write", "path": path, "data": data}) - - def read(self): - """Read data.""" - path = self.path.lower() - return self.window.send({"type": "read", "path": path}) - - def delete(self): - """Delete data.""" - return self.write(data=None) diff --git a/nerfstudio/viewer/viser/__init__.py b/nerfstudio/viewer/viser/__init__.py new file mode 100644 index 000000000..9914442ed --- /dev/null +++ b/nerfstudio/viewer/viser/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Viser is used for the nerfstudio viewr backend """ +# pylint: disable=useless-import-alias + +from .message_api import GuiHandle as GuiHandle +from .message_api import GuiSelectHandle as GuiSelectHandle +from .messages import NerfstudioMessage as NerfstudioMessage +from .server import ViserServer as ViserServer diff --git a/nerfstudio/viewer/viser/gui.py b/nerfstudio/viewer/viser/gui.py new file mode 100644 index 000000000..0e8e95e65 --- /dev/null +++ b/nerfstudio/viewer/viser/gui.py @@ -0,0 +1,196 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access +""" Manages GUI communication. + +Should be almost identical to: https://github.com/brentyi/viser/blob/main/viser/_gui.py +""" +from __future__ import annotations + +import dataclasses +import time +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generic, + List, + Optional, + Type, + TypeVar, + Union, +) + +import numpy as onp +from viser.infra import ClientId + +from .messages import ( + GuiRemoveMessage, + GuiSetHiddenMessage, + GuiSetLevaConfMessage, + GuiSetValueMessage, +) + +if TYPE_CHECKING: + from .message_api import MessageApi + + +T = TypeVar("T") + + +@dataclasses.dataclass +class _GuiHandleState(Generic[T]): + """Internal API for GUI elements.""" + + name: str + typ: Type[T] + api: MessageApi + value: T + last_updated: float + + folder_labels: List[str] + """Name of the folders this GUI input was placed into.""" + + update_cb: List[Callable[[GuiHandle[T]], None]] + """Registered functions to call when this input is updated.""" + + leva_conf: Dict[str, Any] + """Input config for Leva.""" + + is_button: bool + """Indicates a button element, which requires special handling.""" + + sync_cb: Optional[Callable[[ClientId, T], None]] = None + """Callback for synchronizing inputs across clients.""" + + cleanup_cb: Optional[Callable[[], Any]] = None + """Function to call when GUI element is removed.""" + + # Encoder: run on outgoing message values. + # Decoder: run on incoming message values. + # + # This helps us handle cases where types used by Leva don't match what we want to + # expose as a Python API. + # + # noqa because ruff --fix currently breaks these lines. + encoder: Callable[[T], Any] = lambda x: x # noqa + decoder: Callable[[Any], T] = lambda x: x # noqa + + +@dataclasses.dataclass(frozen=True) +class GuiHandle(Generic[T]): + """Handle for a particular GUI input in our visualizer. + + Lets us get values, set values, and detect updates.""" + + # Let's shove private implementation details in here... + _impl: _GuiHandleState[T] + + def on_update(self, func: Callable[[GuiHandle[T]], None]) -> Callable[[GuiHandle[T]], None]: + """Attach a function to call when a GUI input is updated. Happens in a thread. + + Callbacks are passed the originating GUI handle, which can be useful in loops. + """ + self._impl.update_cb.append(func) + return func + + def get_value(self) -> T: + """Get the value of the GUI input.""" + return self._impl.value + + def get_update_timestamp(self) -> float: + """Get the last time that this input was updated.""" + return self._impl.last_updated + + def set_value(self, value: Union[T, onp.ndarray]) -> GuiHandle[T]: + """Set the value of the GUI input.""" + if isinstance(value, onp.ndarray): + assert len(value.shape) <= 1, f"{value.shape} should be at most 1D!" + value = tuple(map(float, value)) # type: ignore + + # Send to client, except for buttons. + if not self._impl.is_button: + self._impl.api._queue(GuiSetValueMessage(self._impl.name, self._impl.encoder(value))) # type: ignore + + # Set internal state. We automatically convert numpy arrays to the expected + # internal type. (eg 1D arrays to tuples) + self._impl.value = type(self._impl.value)(value) # type: ignore + self._impl.last_updated = time.time() + + # Call update callbacks. + for cb in self._impl.update_cb: + cb(self) + + return self + + def set_disabled(self, disabled: bool) -> GuiHandle[T]: + """Allow/disallow user interaction with the input.""" + if self._impl.is_button: + self._impl.leva_conf["settings"]["disabled"] = disabled + self._impl.api._queue( + GuiSetLevaConfMessage(self._impl.name, self._impl.leva_conf), + ) + else: + self._impl.leva_conf["disabled"] = disabled + self._impl.api._queue( + GuiSetLevaConfMessage(self._impl.name, self._impl.leva_conf), + ) + + return self + + def set_hidden(self, hidden: bool) -> GuiHandle[T]: + """Temporarily hide this GUI element from the visualizer.""" + self._impl.api._queue(GuiSetHiddenMessage(self._impl.name, hidden=hidden)) + return self + + def remove(self) -> None: + """Permanently remove this GUI element from the visualizer.""" + self._impl.api._queue(GuiRemoveMessage(self._impl.name)) + assert self._impl.cleanup_cb is not None + self._impl.cleanup_cb() + + +TString = TypeVar("TString", bound=str) + + +@dataclasses.dataclass(frozen=True) +class GuiSelectHandle(GuiHandle[TString], Generic[TString]): + """Handle for a particular drop-down input in our visualizer. + + Lets us get values, set values, and detect updates.""" + + def set_options(self, options: List[TString]) -> None: + """Assign a new set of options for the dropdown menu. + + For projects that care about typing: the static type of `options` should be + consistent with the `StringType` associated with a handle. Literal types will be + inferred where possible when handles are instantiated; for the most flexibility, + we can declare handles as `GuiHandle[str]`. + """ + + # Make sure initial value is in options. + self._impl.leva_conf["options"] = options + if self._impl.leva_conf["value"] not in options: + self._impl.leva_conf["value"] = options[0] + + # Update options. + self._impl.api._queue( + GuiSetLevaConfMessage(self._impl.name, self._impl.leva_conf), + ) + + # Make sure current value is in options. + if self.get_value() not in options: + self.set_value(options[0]) diff --git a/nerfstudio/viewer/viser/message_api.py b/nerfstudio/viewer/viser/message_api.py new file mode 100644 index 000000000..38faf5d06 --- /dev/null +++ b/nerfstudio/viewer/viser/message_api.py @@ -0,0 +1,571 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module contains the MessageApi class, which is the interface for sending messages to the Viewer""" +# pylint: disable=protected-access +# pylint: disable=too-many-public-methods + +from __future__ import annotations + +import abc +import base64 +import contextlib +import io +import time +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generator, + List, + Literal, + Optional, + Tuple, + TypeVar, + cast, + overload, +) + +import imageio.v3 as iio +import numpy as onp +import numpy.typing as onpt +from typing_extensions import LiteralString, ParamSpec, assert_never + +from nerfstudio.data.scene_box import SceneBox + +from . import messages +from .gui import GuiHandle, GuiSelectHandle, _GuiHandleState + +if TYPE_CHECKING: + from viser.infra import ClientId + + +P = ParamSpec("P") + + +def _colors_to_uint8(colors: onp.ndarray) -> onpt.NDArray[onp.uint8]: + """Convert intensity values to uint8. We assume the range [0,1] for floats, and + [0,255] for integers.""" + if colors.dtype != onp.uint8: + if onp.issubdtype(colors.dtype, onp.floating): + colors = onp.clip(colors * 255.0, 0, 255).astype(onp.uint8) + if onp.issubdtype(colors.dtype, onp.integer): + colors = onp.clip(colors, 0, 255).astype(onp.uint8) + return colors + + +def _encode_image_base64( + image: onp.ndarray, + file_format: Literal["png", "jpeg"], + quality: Optional[int] = None, +) -> Tuple[Literal["image/png", "image/jpeg"], str]: + """Encode an image as a base64 string. + + Args: + image: The image to encode. + file_format: The format to encode the image as. + quality: The quality to encode the image as. Only used for JPEG. + + Returns: + A tuple of the media type and the base64-encoded image. + """ + + media_type: Literal["image/png", "image/jpeg"] + image = _colors_to_uint8(image) + with io.BytesIO() as data_buffer: + if file_format == "png": + media_type = "image/png" + iio.imwrite(data_buffer, image, extension=".png") + elif file_format == "jpeg": + media_type = "image/jpeg" + iio.imwrite( + data_buffer, + image[..., :3], # Strip alpha. + extension=".jpeg", + quality=75 if quality is None else quality, + ) + else: + assert_never(file_format) + + base64_data = base64.b64encode(data_buffer.getvalue()).decode("ascii") + + return media_type, base64_data + + +TVector = TypeVar("TVector", bound=tuple) + + +def _cast_vector(vector: TVector | onp.ndarray, length: int) -> TVector: + if isinstance(vector, tuple): + assert len(vector) == length + return cast(TVector, vector) + assert cast(onp.ndarray, vector).shape == (length,) + return cast(TVector, tuple(map(float, vector))) + + +T = TypeVar("T") +IntOrFloat = TypeVar("IntOrFloat", int, float) +TLiteralString = TypeVar("TLiteralString", bound=LiteralString) + + +class MessageApi(abc.ABC): + """Interface for all commands we can use to send messages over a websocket connection. + + Should be implemented by both our global server object (for broadcasting) and by + invidividual clients.""" + + def __init__(self) -> None: + self._handle_state_from_gui_name: Dict[str, _GuiHandleState[Any]] = {} + self._gui_folder_labels: List[str] = [] + + @abc.abstractmethod + def _queue(self, message: messages.NerfstudioMessage) -> None: + """Abstract method for sending messages.""" + ... + + @contextlib.contextmanager + def gui_folder(self, label: str) -> Generator[None, None, None]: + """Context for placing all GUI elements into a particular folder. + + We currently only support one folder level. + + Args: + label: The label for the folder. + """ + self._gui_folder_labels.append(label) + yield + assert self._gui_folder_labels.pop() == label + + def add_gui_button(self, name: str) -> GuiHandle[bool]: + """Add a button to the GUI. The value of this input is set to `True` every time + it is clicked; to detect clicks, we can manually set it back to `False`. + + Currently, all button names need to be unique. + + Args: + name: The name of the gui element. + """ + return self._add_gui_impl( + name, + initial_value=False, + leva_conf={"type": "BUTTON", "settings": {}}, + is_button=True, + ) + + def add_gui_checkbox(self, name: str, initial_value: bool) -> GuiHandle[bool]: + """Add a checkbox to the GUI. + + Args: + name: The name of the checkbox. + initial_value: The initial value of the checkbox. + """ + assert isinstance(initial_value, bool) + return self._add_gui_impl( + "/".join(self._gui_folder_labels + [name]), + initial_value, + leva_conf={"value": initial_value, "label": name}, + ) + + def add_gui_text(self, name: str, initial_value: str) -> GuiHandle[str]: + """Add a text input to the GUI. + + Args: + name: The name of the text input. + initial_value: The initial value of the text input. + """ + assert isinstance(initial_value, str) + return self._add_gui_impl( + "/".join(self._gui_folder_labels + [name]), + initial_value, + leva_conf={"value": initial_value, "label": name}, + ) + + def add_gui_number(self, name: str, initial_value: IntOrFloat) -> GuiHandle[IntOrFloat]: + """Add a number input to the GUI. + + Args: + name: The name of the number. + initial_value: The initial value of the number. + """ + assert isinstance(initial_value, (int, float)) + return self._add_gui_impl( + "/".join(self._gui_folder_labels + [name]), + initial_value, + leva_conf={"value": initial_value, "label": name}, + ) + + def add_gui_vector2( + self, + name: str, + initial_value: Tuple[float, float] | onp.ndarray, + step: Optional[float] = None, + ) -> GuiHandle[Tuple[float, float]]: + """Add a length-2 vector input to the GUI. + + Args: + name: The name of the vector. + initial_value: The initial value of the vector. + step: The step size for the vector. + """ + return self._add_gui_impl( + "/".join(self._gui_folder_labels + [name]), + _cast_vector(initial_value, length=2), + leva_conf={ + "value": initial_value, + "label": name, + "step": step, + }, + ) + + def add_gui_vector3( + self, + name: str, + initial_value: Tuple[float, float, float] | onp.ndarray, + step: Optional[float] = None, + lock: bool = False, + ) -> GuiHandle[Tuple[float, float, float]]: + """Add a length-3 vector input to the GUI. + + Args: + name: The name of the vector. + initial_value: The initial value of the vector. + step: The step size for the vector. + lock: Whether the vector is locked. + """ + return self._add_gui_impl( + "/".join(self._gui_folder_labels + [name]), + _cast_vector(initial_value, length=3), + leva_conf={ + "label": name, + "value": initial_value, + "step": step, + "lock": lock, + }, + ) + + # Resolve type of value to a Literal whenever possible. + @overload + def add_gui_select( + self, + name: str, + options: List[TLiteralString], + initial_value: Optional[TLiteralString] = None, + ) -> GuiSelectHandle[TLiteralString]: + ... + + @overload + def add_gui_select( + self, + name: str, + options: List[str], + initial_value: Optional[str] = None, + ) -> GuiSelectHandle[str]: + ... + + def add_gui_select( + self, + name: str, + options: List[TLiteralString] | List[str], + initial_value: Optional[TLiteralString | str] = None, + ) -> GuiSelectHandle[TLiteralString] | GuiSelectHandle[str]: + """Add a dropdown to the GUI. + + Args: + name: The name of the dropdown. + options: The options to choose from. + initial_value: The initial value of the dropdown. + """ + assert len(options) > 0 + if initial_value is None: + initial_value = options[0] + return GuiSelectHandle( + self._add_gui_impl( + "/".join(self._gui_folder_labels + [name]), + initial_value, + leva_conf={ + "value": initial_value, + "label": name, + "options": options, + }, + )._impl + ) + + def add_gui_slider( + self, + name: str, + low: IntOrFloat, + high: IntOrFloat, + step: Optional[IntOrFloat], + initial_value: IntOrFloat, + ) -> GuiHandle[IntOrFloat]: + """Add a slider to the GUI. + + Args: + name: The name of the slider. + low: The minimum value of the slider. + high: The maximum value of the slider. + step: The step size of the slider. + initial_value: The initial value of the slider. + """ + assert high >= low + if step is not None: + assert step <= (high - low) + assert high >= initial_value >= low + + return self._add_gui_impl( + "/".join(self._gui_folder_labels + [name]), + initial_value, + leva_conf={ + "value": initial_value, + "label": name, + "min": low, + "max": high, + "step": step, + }, + ) + + def add_gui_rgb( + self, + name: str, + initial_value: Tuple[int, int, int], + ) -> GuiHandle[Tuple[int, int, int]]: + """Add an RGB picker to the GUI. + + Args: + image: The image to set as the background. Must be a 3D numpy array of shape (H, W, 3). + file_format: The file format to use for the image. + quality: The quality of the image, if using jpeg. Must be an integer between 0 and 100. + """ + return self._add_gui_impl( + "/".join(self._gui_folder_labels + [name]), + initial_value, + leva_conf={ + "value": { + "r": initial_value[0], + "g": initial_value[1], + "b": initial_value[2], + }, + "label": name, + }, + encoder=lambda rgb: dict(zip("rgb", rgb)), + decoder=lambda rgb_dict: (rgb_dict["r"], rgb_dict["g"], rgb_dict["b"]), + ) + + def add_gui_rgba( + self, + name: str, + initial_value: Tuple[int, int, int, int], + ) -> GuiHandle[Tuple[int, int, int, int]]: + """Add an RGBA picker to the GUI.""" + return self._add_gui_impl( + "/".join(self._gui_folder_labels + [name]), + initial_value, + leva_conf={ + "value": { + "r": initial_value[0], + "g": initial_value[1], + "b": initial_value[2], + "a": initial_value[3], + }, + "label": name, + }, + encoder=lambda rgba: dict(zip("rgba", rgba)), + decoder=lambda rgba_dict: ( + rgba_dict["r"], + rgba_dict["g"], + rgba_dict["b"], + rgba_dict["a"], + ), + ) + + def set_background_image( + self, + image: onp.ndarray, + file_format: Literal["png", "jpeg"] = "jpeg", + quality: Optional[int] = None, + ) -> None: + """Set the background image of the scene. + + Args: + image: The image to set as the background. Must be a 3D numpy array of shape (H, W, 3). + file_format: The file format to use for the image. + quality: The quality of the image, if using jpeg. Must be an integer between 0 and 100. + """ + media_type, base64_data = _encode_image_base64(image, file_format, quality=quality) + self._queue(messages.BackgroundImageMessage(media_type=media_type, base64_data=base64_data)) + + def send_file_path_info(self, config_base_dir: Path, data_base_dir: Path, export_path_name: str) -> None: + """Send file path info to the scene. + + Args: + config_base_dir: The base directory for config files. + data_base_dir: The base directory for data files. + export_path_name: The name for the export folder. + """ + self._queue( + messages.FilePathInfoMessage( + config_base_dir=str(config_base_dir), + data_base_dir=str(data_base_dir), + export_path_name=export_path_name, + ) + ) + + def update_scene_box(self, scene_box: SceneBox) -> None: + """Update the scene box. + + Args: + scene_box: The scene box. + """ + self._queue(messages.SceneBoxMessage(min=scene_box.aabb[0].tolist(), max=scene_box.aabb[1].tolist())) + + def add_dataset_image(self, idx: str, json: Dict) -> None: + """Add a dataset image to the scene. + + Args: + idx: The index of the image. + json: The json dict from the camera frustum and image. + """ + self._queue(messages.DatasetImageMessage(idx=idx, json=json)) + + def set_training_state(self, training_state: Literal["training", "paused", "completed"]) -> None: + """Set the training mode. + + Args: + training_state: The training mode. + """ + self._queue(messages.TrainingStateMessage(training_state=training_state)) + + def send_camera_paths(self, camera_paths: Dict[str, Any]) -> None: + """Send camera paths to the scene. + + Args: + camera_paths: A dictionary of camera paths. + """ + self._queue(messages.CameraPathsMessage(payload=camera_paths)) + + def send_crop_params( + self, + crop_enabled: bool, + crop_bg_color: Tuple[int, int, int], + crop_center: Tuple[float, float, float], + crop_scale: Tuple[float, float, float], + ) -> None: + """Send crop parameters to the scene. + + Args: + crop_enabled: Whether crop is enabled. + crop_bg_color: The background color of the crop. + crop_center: The center of the crop. + crop_scale: The scale of the crop. + """ + self._queue( + messages.CropParamsMessage( + crop_enabled=crop_enabled, crop_bg_color=crop_bg_color, crop_center=crop_center, crop_scale=crop_scale + ) + ) + + def send_status_message(self, eval_res: str, step: int): + """Send status message + + Args: + eval_res: The resolution of the render in plain text. + step: The current step. + """ + self._queue(messages.StatusMessage(eval_res=eval_res, step=step)) + + def send_output_options_message(self, options: List[str]): + """Send output options message + + Args: + options: The list of output options + """ + self._queue(messages.OutputOptionsMessage(options=options)) + + def _add_gui_impl( + self, + name: str, + initial_value: T, + leva_conf: dict, + is_button: bool = False, + encoder: Callable[[T], Any] = lambda x: x, + decoder: Callable[[Any], T] = lambda x: x, + ) -> GuiHandle[T]: + """Private helper for adding a simple GUI element.""" + + handle_state = _GuiHandleState( + name, + typ=type(initial_value), + api=self, + value=initial_value, + last_updated=time.time(), + folder_labels=self._gui_folder_labels, + update_cb=[], + leva_conf=leva_conf, + is_button=is_button, + encoder=encoder, + decoder=decoder, + ) + self._handle_state_from_gui_name[name] = handle_state + handle_state.cleanup_cb = lambda: self._handle_state_from_gui_name.pop(name) + + # For broadcasted GUI handles, we should synchronize all clients. + # This will be a no-op for client handles. + if not is_button: + + def sync_other_clients(client_id: ClientId, value: Any) -> None: + message = messages.GuiSetValueMessage(name=name, value=handle_state.encoder(value)) + message.excluded_self_client = client_id + self._queue(message) + + handle_state.sync_cb = sync_other_clients + + self._queue( + messages.GuiAddMessage( + name=name, + folder_labels=tuple(self._gui_folder_labels), + leva_conf=leva_conf, + ) + ) + return GuiHandle(handle_state) + + def use_time_conditioning(self) -> None: + """Use time conditioning.""" + self._queue(messages.UseTimeConditioningMessage()) + + def _handle_gui_updates( + self: MessageApi, + client_id: ClientId, + message: messages.GuiUpdateMessage, + ) -> None: + handle_state = self._handle_state_from_gui_name.get(message.name, None) + if handle_state is None: + return + + value = handle_state.typ(handle_state.decoder(message.value)) + + # Only call update when value has actually changed. + if not handle_state.is_button and value == handle_state.value: + return + + # Update state. + handle_state.value = value + handle_state.last_updated = time.time() + + # Trigger callbacks. + for cb in handle_state.update_cb: + cb(GuiHandle(handle_state)) + if handle_state.sync_cb is not None: + handle_state.sync_cb(client_id, value) diff --git a/nerfstudio/viewer/viser/messages.py b/nerfstudio/viewer/viser/messages.py new file mode 100644 index 000000000..3fcfc7b3a --- /dev/null +++ b/nerfstudio/viewer/viser/messages.py @@ -0,0 +1,253 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Message type definitions. For synchronization with the TypeScript definitions, see +`_typescript_interface_gen.py.`""" + +from __future__ import annotations + +import dataclasses +from typing import Any, Literal, Tuple + +import viser.infra +from typing_extensions import override + + +class NerfstudioMessage(viser.infra.Message): + """Base message type for controlling our viewer.""" + + @override + def redundancy_key(self) -> str: + return type(self).__name__ + + +@dataclasses.dataclass +class BackgroundImageMessage(NerfstudioMessage): + """Message for rendering a background image.""" + + media_type: Literal["image/jpeg", "image/png"] + base64_data: str + + +@dataclasses.dataclass +class GuiAddMessage(NerfstudioMessage): + """Sent server->client to add a new GUI input.""" + + name: str + folder_labels: Tuple[str, ...] + leva_conf: Any + + @override + def redundancy_key(self) -> str: + return f"{type(self).__name__}_{self.name}" + + +@dataclasses.dataclass +class GuiRemoveMessage(NerfstudioMessage): + """Sent server->client to add a new GUI input.""" + + name: str + + @override + def redundancy_key(self) -> str: + return f"{type(self).__name__}_{self.name}" + + +@dataclasses.dataclass +class GuiUpdateMessage(NerfstudioMessage): + """Sent client->server when a GUI input is changed.""" + + name: str + value: Any + + @override + def redundancy_key(self) -> str: + return f"{type(self).__name__}_{self.name}" + + +@dataclasses.dataclass +class GuiSetHiddenMessage(NerfstudioMessage): + """Sent client->server when a GUI input is changed.""" + + name: str + hidden: bool + + @override + def redundancy_key(self) -> str: + return f"{type(self).__name__}_{self.name}" + + +@dataclasses.dataclass +class GuiSetValueMessage(NerfstudioMessage): + """Sent server->client to set the value of a particular input.""" + + name: str + value: Any + + @override + def redundancy_key(self) -> str: + return f"{type(self).__name__}_{self.name}" + + +@dataclasses.dataclass +class GuiSetLevaConfMessage(NerfstudioMessage): + """Sent server->client to override some part of an input's Leva config.""" + + name: str + leva_conf: Any + + @override + def redundancy_key(self) -> str: + return f"{type(self).__name__}_{self.name}" + + +@dataclasses.dataclass +class FilePathInfoMessage(NerfstudioMessage): + """Experiment file path info""" + + config_base_dir: str + """ Base directory for config files """ + data_base_dir: str + """ Base directory for data files """ + export_path_name: str + """ Name of the export folder """ + + +@dataclasses.dataclass +class CameraMessage(NerfstudioMessage): + """Render camera data.""" + + aspect: float + """ Aspect ratio of the camera """ + render_aspect: float + """ Aspect ratio of the render window """ + fov: float + """ Field of view of the camera """ + matrix: Tuple[ + float, float, float, float, float, float, float, float, float, float, float, float, float, float, float, float + ] + """ Camera matrix """ + camera_type: Literal["perspective", "fisheye", "equirectangular"] + """ Camera type """ + is_moving: bool + """ True if the camera is moving, False otherwise """ + timestamp: int + """JSON computed by the camera class""" + + +@dataclasses.dataclass +class SceneBoxMessage(NerfstudioMessage): + """Scene Box data.""" + + min: Tuple[float, float, float] + """ Minimum coordinates of the scene box """ + max: Tuple[float, float, float] + """ Maximum coordinates of the scene box """ + + +@dataclasses.dataclass +class DatasetImageMessage(NerfstudioMessage): + """Message for rendering a dataset image frustum.""" + + idx: str + """Index of the image in the threejs scene""" + json: Any + """JSON computed by the camera class""" + + @override + def redundancy_key(self) -> str: + return f"{type(self).__name__}_{self.idx}" + + +@dataclasses.dataclass +class TrainingStateMessage(NerfstudioMessage): + """Wheather the scene is in training mode or not.""" + + training_state: Literal["training", "paused", "completed"] + """True if the model is currently trianing, False otherwise""" + + +@dataclasses.dataclass +class CameraPathPayloadMessage(NerfstudioMessage): + """Camera path""" + + camera_path_filename: str + """ Camera path filename """ + camera_path: Any + """ Camera path data """ + + +@dataclasses.dataclass +class CameraPathOptionsRequest(NerfstudioMessage): + """Request list of existing camera paths""" + + +@dataclasses.dataclass +class CameraPathsMessage(NerfstudioMessage): + """Dictionary of camera paths""" + + payload: Any + """ Dictionary of camera paths """ + + +@dataclasses.dataclass +class CropParamsMessage(NerfstudioMessage): + """Crop parameters""" + + crop_enabled: bool + """ Crop parameters """ + crop_bg_color: Tuple[int, int, int] + """ Crop background color, range 0-255 """ + crop_center: Tuple[float, float, float] + """ Center of the crop box """ + crop_scale: Tuple[float, float, float] + """ Scale of the crop box """ + + +@dataclasses.dataclass +class StatusMessage(NerfstudioMessage): + """Status message.""" + + eval_res: str + """ Resolution of the viewer display in plain text """ + step: int + """ Current step """ + + +@dataclasses.dataclass +class SaveCheckpointMessage(NerfstudioMessage): + """Save checkpoint message.""" + + +@dataclasses.dataclass +class UseTimeConditioningMessage(NerfstudioMessage): + """Use time conditioning message.""" + + +@dataclasses.dataclass +class TimeConditionMessage(NerfstudioMessage): + """Time conditioning message.""" + + time: float + """ Time conditioning value """ + + +@dataclasses.dataclass +class OutputOptionsMessage(NerfstudioMessage): + """Output options message which are used in the export panel. + TODO: remove when export panel is becomes python defined. + """ + + options: Any + """ List of output option strings""" diff --git a/nerfstudio/viewer/viser/server.py b/nerfstudio/viewer/viser/server.py new file mode 100644 index 000000000..0dbc4c6e1 --- /dev/null +++ b/nerfstudio/viewer/viser/server.py @@ -0,0 +1,68 @@ +# Copyright 2022 The Nerfstudio Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Core Viser Server """ +# pylint: disable=protected-access +# pylint: disable=too-many-statements + + +from __future__ import annotations + +from typing import Callable, Type + +import viser.infra +from typing_extensions import override + +from .message_api import MessageApi +from .messages import GuiUpdateMessage, NerfstudioMessage + + +class ViserServer(MessageApi): + """Core visualization server. Communicates asynchronously with client applications + via websocket connections. + + By default, all messages (eg `server.add_frame()`) are broadcasted to all connected + clients. + + To send messages to an individual client, we can grab a client ID -> handle mapping + via `server.get_clients()`, and then call `client.add_frame()` on the handle. + """ + + def __init__( + self, + host: str = "localhost", + port: int = 8080, + ): + super().__init__() + + self._ws_server = viser.infra.Server(host, port, http_server_root=None, verbose=False) + self._ws_server.register_handler(GuiUpdateMessage, self._handle_gui_updates) + self._ws_server.start() + + @override + def _queue(self, message: NerfstudioMessage) -> None: + """Implements message enqueue required by MessageApi. + + Pushes a message onto a broadcast queue.""" + self._ws_server.broadcast(message) + + def register_handler( + self, message_type: Type[NerfstudioMessage], handler: Callable[[NerfstudioMessage], None] + ) -> None: + """Register a handler for incoming messages. + + Args: + handler: A function that takes a message, and does something + """ + self._ws_server.register_handler(message_type, lambda client_id, msg: handler(msg)) diff --git a/pyproject.toml b/pyproject.toml index e8dbf3e96..6bd4abf74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,11 +4,15 @@ build-backend = "setuptools.build_meta" [project] name = "nerfstudio" +<<<<<<< HEAD version = "0.1.19" +======= +version = "0.2.2" +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb description = "All-in-one repository for state-of-the-art NeRFs" readme = "README.md" license = { text="Apache 2.0"} -requires-python = ">=3.7.3" +requires-python = ">=3.8.0" classifiers = [ "Development Status :: 3 - Alpha", "Programming Language :: Python", @@ -30,7 +34,7 @@ dependencies = [ "mediapy>=1.1.0", "msgpack>=1.0.4", "msgpack_numpy>=0.4.8", - "nerfacc==0.3.5", + "nerfacc==0.5.2", "open3d>=0.16.0", "opencv-python==4.6.0.66", "Pillow>=9.3.0", @@ -46,15 +50,17 @@ dependencies = [ "requests", "rich>=12.5.1", "scikit-image>=0.19.3", - "tensorboard==2.9.0", - "torch>=1.12.1", + "tensorboard>=2.13.0", + "torch>=1.12.1,<2.0.0", "torchmetrics[image]>=0.9.3", "torchtyping>=0.1.4", "torchvision>=0.13.0", - "u-msgpack-python>=2.4.1", + "typing_extensions>=4.4.0", + "viser>=0.0.5", "nuscenes-devkit>=1.1.1", "wandb>=0.13.3", - "xatlas" + "xatlas", + "trimesh>=3.20.2" ] [project.urls] @@ -72,11 +78,11 @@ gen = [ # Development packages dev = [ - "black[jupyter]==22.3.0", + "black[jupyter]==23.3.0", "pylint==2.13.4", "pytest==7.1.2", "pytest-xdist==2.5.0", - "typeguard>=2.13.3", + "typeguard==2.13.3", ] # Documentation related packages @@ -97,6 +103,7 @@ docs = [ ] [project.scripts] +<<<<<<< HEAD # Note, add entrypoint name to scripts/completions/install.py to include CLI completion ns-install-cli = "scripts.completions.install:entrypoint" ns-process-data = "scripts.process_data:entrypoint" @@ -109,6 +116,19 @@ ns-render = "scripts.render:entrypoint" ns-export = "scripts.exporter:entrypoint" ns-dev-test = "scripts.github.run_actions:entrypoint" ns-bridge-server = "nerfstudio.viewer.server.server:entrypoint" +======= +# Note, add entrypoint name to nerfstudio/scripts/completions/install.py to include CLI completion +ns-install-cli = "nerfstudio.scripts.completions.install:entrypoint" +ns-process-data = "nerfstudio.scripts.process_data:entrypoint" +ns-download-data = "nerfstudio.scripts.downloads.download_data:entrypoint" +ns-train = "nerfstudio.scripts.train:entrypoint" +ns-viewer = "nerfstudio.scripts.viewer.run_viewer:entrypoint" +ns-eval = "nerfstudio.scripts.eval:entrypoint" +ns-render = "nerfstudio.scripts.render:entrypoint" +ns-export = "nerfstudio.scripts.exporter:entrypoint" +ns-dev-test = "nerfstudio.scripts.github.run_actions:entrypoint" +ns-dev-sync-viser-message-defs = "nerfstudio.scripts.viewer.sync_viser_message_defs:entrypoint" +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb [options] # equivalent to using --extra-index-url with pip, which is needed for specifying the CUDA version torch and torchvision @@ -117,10 +137,10 @@ dependency_links = [ ] [tool.setuptools.packages.find] -include = ["nerfstudio*","scripts*"] +include = ["nerfstudio*"] [tool.setuptools.package-data] -"*" = ["*.json", "py.typed", "setup.bash", "setup.zsh"] +"*" = ["*.cu", "*.json", "py.typed", "setup.bash", "setup.zsh"] # black [tool.black] @@ -131,7 +151,6 @@ line-length = 120 max-line-length = 120 generated-members = ["numpy.*", "torch.*", "cv2.*", "cv.*"] good-names-rgxs = "^[_a-zA-Z][_a-z0-9]?$" -ignore-paths = ["scripts/colmap2nerf.py"] jobs = 0 ignored-classes = ["TensorDataclass"] @@ -143,6 +162,7 @@ disable = [ "too-many-branches", "too-many-instance-attributes", "too-many-locals", + "too-many-statements", "unnecessary-ellipsis", ] @@ -159,7 +179,6 @@ include = ["nerfstudio"] exclude = ["**/node_modules", "**/__pycache__", ] -ignore = ["nerfstudio/viewer"] defineConstant = { DEBUG = true } reportMissingImports = true @@ -167,5 +186,5 @@ reportMissingTypeStubs = false reportPrivateImportUsage = false reportUndefinedVariable = false -pythonVersion = "3.7" +pythonVersion = "3.8" pythonPlatform = "Linux" diff --git a/scripts/__init__.py b/scripts/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/blender/__init__.py b/scripts/blender/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/completions/__init__.py b/scripts/completions/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/docs/__init__.py b/scripts/docs/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/downloads/__init__.py b/scripts/downloads/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/generative/__init__.py b/scripts/generative/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/github/__init__.py b/scripts/github/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/viewer/__init__.py b/scripts/viewer/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/viewer/view_dataset.py b/scripts/viewer/view_dataset.py deleted file mode 100644 index 4bd526552..000000000 --- a/scripts/viewer/view_dataset.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -""" -view_dataset.py -""" - -import time -from datetime import timedelta -from pathlib import Path - -import torch -import tyro -from rich.console import Console - -from nerfstudio.configs.base_config import ViewerConfig -from nerfstudio.data.datamanagers.base_datamanager import AnnotatedDataParserUnion -from nerfstudio.data.datasets.base_dataset import InputDataset -from nerfstudio.viewer.server import viewer_utils - -DEFAULT_TIMEOUT = timedelta(minutes=30) -CONSOLE = Console(width=120) - -# speedup for when input size to model doesn't change (much) -torch.backends.cudnn.benchmark = True # type: ignore - - -def main( - dataparser: AnnotatedDataParserUnion, - viewer: ViewerConfig, - log_base_dir: Path = Path("/tmp/nerfstudio_viewer_logs"), -) -> None: - """Main function.""" - viewer_state, _ = viewer_utils.setup_viewer( - viewer, - log_filename=log_base_dir / viewer.relative_log_filename, - datapath=dataparser.data, - ) - dataset = InputDataset(dataparser.setup().get_dataparser_outputs(split="train")) - viewer_state.init_scene(dataset=dataset, start_train=False) - CONSOLE.log("Please refresh and load page at: %s", viewer_state.viewer_url) - time.sleep(30) # allowing time to refresh page - - -if __name__ == "__main__": - tyro.extras.set_accent_color("bright_yellow") - tyro.cli(main) diff --git a/tests/data/test_datamanager.py b/tests/data/test_datamanager.py new file mode 100644 index 000000000..e80330183 --- /dev/null +++ b/tests/data/test_datamanager.py @@ -0,0 +1,107 @@ +# pylint: disable=all +from copy import copy +from typing import Any + +import pytest +import torch +import yaml + +from nerfstudio.cameras.cameras import Cameras +from nerfstudio.configs.base_config import InstantiateConfig +from nerfstudio.data.datamanagers.base_datamanager import ( + DataManager, + DataparserOutputs, + VanillaDataManager, + VanillaDataManagerConfig, +) +from nerfstudio.data.datasets.base_dataset import InputDataset +from nerfstudio.data.datasets.depth_dataset import DepthDataset + + +class DummyDataParser: + def __init__(self, *args, **kwargs): + pass + + def __getattr__(self, __name: str) -> Any: + if __name.startswith("_"): + return object.__getattribute__(self, __name) + return None + + def get_dataparser_outputs(self, *args, **kwargs): + return DataparserOutputs( + [], + Cameras( + torch.ones((0, 3, 4)), + torch.ones((0, 1)), + torch.ones((0, 1)), + torch.ones((0, 1)), + torch.ones((0, 1)), + 10, + 10, + ), + metadata={"depth_filenames": [], "depth_unit_scale_factor": 1.0}, + ) + + +@pytest.fixture +def config(): + config = VanillaDataManagerConfig() + setattr(config, "dataparser", InstantiateConfig(_target=DummyDataParser)) + setattr(config.dataparser, "data", None) + return config + + +def test_data_manager_type_inference(config): + # Mock for a faster test + + assert VanillaDataManager(config).dataset_type is InputDataset + assert VanillaDataManager[DepthDataset](config).dataset_type is DepthDataset + + class tmp(VanillaDataManager): + pass + + assert tmp(config).dataset_type is InputDataset + + class tmp2(VanillaDataManager[DepthDataset]): + pass + + assert tmp2(config).dataset_type is DepthDataset + + +def test_data_manager_type_can_be_serialized(config): + # Mock for a faster test + + assert VanillaDataManager(config).dataset_type is InputDataset + obj = yaml.load(yaml.dump(VanillaDataManager(config)), Loader=yaml.UnsafeLoader) + assert obj.dataset_type is InputDataset + assert isinstance(obj, VanillaDataManager) + + assert VanillaDataManager[DepthDataset](config).dataset_type is DepthDataset + obj = yaml.load(yaml.dump(VanillaDataManager[DepthDataset](config)), Loader=yaml.UnsafeLoader) + assert obj.dataset_type is DepthDataset + assert isinstance(obj, VanillaDataManager) + + class tmp(VanillaDataManager): + pass + + try: + globals()["tmp"] = tmp + assert tmp(config).dataset_type is InputDataset + obj = yaml.load(yaml.dump(tmp(config)), Loader=yaml.UnsafeLoader) + assert obj.dataset_type is InputDataset + assert isinstance(obj, tmp) + finally: + globals().pop("tmp") + + class tmp2(VanillaDataManager[DepthDataset]): + pass + + try: + globals()["tmp2"] = tmp2 + + assert tmp2(config).dataset_type is DepthDataset + obj = yaml.load(yaml.dump(tmp2(config)), Loader=yaml.UnsafeLoader) + assert obj.dataset_type is DepthDataset + assert isinstance(obj, tmp2) + finally: + globals().pop("tmp2") diff --git a/tests/field_components/test_encodings.py b/tests/field_components/test_encodings.py index 271adddc9..dc59b79ff 100644 --- a/tests/field_components/test_encodings.py +++ b/tests/field_components/test_encodings.py @@ -171,6 +171,32 @@ def test_tensor_hash_encoder(): assert encoded.shape == (10, out_dim) +def test_kplane_encoder(): + """Test K-Planes encoder""" + + out_dim = 8 + + encoder = encodings.KPlanesEncoding( + resolution=(32, 32, 32, 16), + num_components=out_dim, + reduce="product", + ) + assert encoder.get_out_dim() == out_dim + + in_tensor = torch.randn((10, 4)) + encoded = encoder(in_tensor) + assert encoded.shape == (10, out_dim) + + encoder = encodings.KPlanesEncoding( + resolution=(32, 32, 32), + num_components=out_dim, + reduce="product", + ) + in_tensor = torch.randn((10, 3)) + encoded = encoder(in_tensor) + assert encoded.shape == (10, out_dim) + + if __name__ == "__main__": test_scaling_and_offset() test_nerf_encoder() diff --git a/tests/model_components/test_losses.py b/tests/model_components/test_losses.py new file mode 100644 index 000000000..8dbbbc443 --- /dev/null +++ b/tests/model_components/test_losses.py @@ -0,0 +1,28 @@ +""" +Test losses +""" + +import torch + +from nerfstudio.model_components.losses import tv_loss + + +def test_tv_loss(): + """Test total variation loss""" + + grids = torch.ones([3, 64, 128, 128]) + assert tv_loss(grids).item() == 0 + + grids = torch.zeros([1, 10, 100, 100]) + for i in range(100): + for j in range(100): + grid_location = (i + j) % 2 + for k in range(10): + grids[0, k, i, j] = grid_location + + # TV_row = 1, TV_col = 1. Totol tv should be 2 * (TV_row + TV_col) + assert tv_loss(grids).item() == 4.0 + + +if __name__ == "__main__": + test_tv_loss() diff --git a/tests/model_components/test_renderers.py b/tests/model_components/test_renderers.py index 984a45e7c..a1e098388 100644 --- a/tests/model_components/test_renderers.py +++ b/tests/model_components/test_renderers.py @@ -40,7 +40,7 @@ def test_sh_renderer(): sh_renderer = renderers.SHRenderer() rgb = sh_renderer(sh=sh, directions=directions, weights=weights) - assert torch.max(rgb) > 0.9 + assert torch.max(rgb) > 0.7 def test_acc_renderer(): diff --git a/tests/pipelines/test_vanilla_pipeline.py b/tests/pipelines/test_vanilla_pipeline.py index 69581ce27..c51038135 100644 --- a/tests/pipelines/test_vanilla_pipeline.py +++ b/tests/pipelines/test_vanilla_pipeline.py @@ -57,6 +57,9 @@ def test_load_state_dict(): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.register_parameter("param", nn.Parameter(torch.ones((3,)))) + self.register_module( + "module", Model(*args, **kwargs) + ) # make sure that non-DDP checkpoint won't be processed as DDP one def load_state_dict(self, state_dict, strict=True): """Mocked load_state_dict""" @@ -64,6 +67,7 @@ def test_load_state_dict(): was_called = True assert strict assert "param" in state_dict + assert "module.device_indicator_param" in state_dict config = VanillaPipelineConfig( datamanager=VanillaDataManagerConfig( @@ -77,3 +81,19 @@ def test_load_state_dict(): pipeline.load_pipeline(state_dict, 0) assert was_called assert pipeline.model.param[0].item() == 2 + + # preparation for another test + state_dict["_model.param"].mul_(2) # pylint: disable=unsubscriptable-object + was_called = False + # pretends to be a DDP checkpoint + ddp_state_dict = {} + for key, value in state_dict.items(): + if key.startswith("_model."): + # replace the prefix "_model" with "_model.module" + ddp_state_dict["_model.module." + key[len("_model.") :]] = value + else: + ddp_state_dict[key] = value + # load DDP checkpoint + pipeline.load_pipeline(ddp_state_dict, 0) + assert was_called + assert pipeline.model.param[0].item() == 4 diff --git a/tests/plugins/test_registry.py b/tests/plugins/test_registry.py index fe8361a18..1fb993633 100644 --- a/tests/plugins/test_registry.py +++ b/tests/plugins/test_registry.py @@ -1,6 +1,7 @@ """ Tests for the nerfstudio.plugins.registry module. """ +import os import sys from nerfstudio.engine.trainer import TrainerConfig @@ -49,3 +50,23 @@ def test_discover_methods(): finally: # Revert mock registry.entry_points = entry_points_backup + + +def test_discover_methods_from_environment_variable(): + """Tests if a custom method from env variable gets properly registered using the discover_methods method""" + old_env = None + try: + old_env = os.environ.get("NERFSTUDIO_METHOD_CONFIGS", None) + os.environ["NERFSTUDIO_METHOD_CONFIGS"] = "test-method-env=test_registry:TestConfig" + + # Discover plugins + methods, _ = registry.discover_methods() + assert "test-method-env" in methods + config = methods["test-method-env"] + assert isinstance(config, TrainerConfig) + finally: + # Revert mock + if old_env is not None: + os.environ["NERFSTUDIO_METHOD_CONFIGS"] = old_env + else: + del os.environ["NERFSTUDIO_METHOD_CONFIGS"] diff --git a/tests/process_data/test_process_images.py b/tests/process_data/test_process_images.py index 3e4fa82a9..ae48057e5 100644 --- a/tests/process_data/test_process_images.py +++ b/tests/process_data/test_process_images.py @@ -12,10 +12,37 @@ from nerfstudio.data.dataparsers.nerfstudio_dataparser import NerfstudioDataPars from nerfstudio.data.utils.colmap_parsing_utils import Camera from nerfstudio.data.utils.colmap_parsing_utils import Image as ColmapImage from nerfstudio.data.utils.colmap_parsing_utils import ( +<<<<<<< HEAD write_cameras_binary, write_images_binary, ) from scripts.process_data import ProcessImages +======= + qvec2rotmat, + write_cameras_binary, + write_images_binary, +) +from nerfstudio.process_data.images_to_nerfstudio_dataset import ( + ImagesToNerfstudioDataset, +) + + +def random_quaternion(num_poses: int): + """ + Generates random rotation quatenion. + """ + u, v, w = np.random.uniform(size=(3, num_poses)) + quaternion = np.stack( + ( + np.sqrt(1 - u) * np.sin(2 * np.pi * v), + np.sqrt(1 - u) * np.cos(2 * np.pi * v), + np.sqrt(u) * np.sin(2 * np.pi * w), + np.sqrt(u) * np.cos(2 * np.pi * w), + ), + -1, + ) + return quaternion +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb def test_process_images_skip_colmap(tmp_path: Path): @@ -34,6 +61,7 @@ def test_process_images_skip_colmap(tmp_path: Path): ) frames = {} num_frames = 10 +<<<<<<< HEAD qvecs = np.random.uniform(size=(num_frames, 4)) tvecs = np.random.uniform(size=(num_frames, 3)) # original_poses = np.concatenate( @@ -49,6 +77,23 @@ def test_process_images_skip_colmap(tmp_path: Path): # ), # -2, # ) +======= + qvecs = random_quaternion(num_frames) + tvecs = np.random.uniform(size=(num_frames, 3)) + original_poses = np.concatenate( + ( + np.concatenate( + ( + np.stack(list(map(qvec2rotmat, qvecs))), + tvecs[:, :, None], + ), + -1, + ), + np.array([[[0, 0, 0, 1]]], dtype=qvecs.dtype).repeat(num_frames, 0), + ), + -2, + ) +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb for i in range(num_frames): frames[i + 1] = ColmapImage(i + 1, qvecs[i], tvecs[i], 1, f"image_{i}.png", [], []) Image.new("RGB", (width, height)).save(tmp_path / "images" / f"image_{i}.png") @@ -61,8 +106,15 @@ def test_process_images_skip_colmap(tmp_path: Path): (tmp_path / "mocked_bin" / "colmap").touch(mode=0o777) (tmp_path / "mocked_bin" / "ffmpeg").touch(mode=0o777) +<<<<<<< HEAD # Run ProcessImages cmd = ProcessImages(tmp_path / "images", tmp_path / "nerfstudio", colmap_model_path=sparse_path, skip_colmap=True) +======= + # Convert images into a NerfStudio dataset + cmd = ImagesToNerfstudioDataset( + data=tmp_path / "images", output_dir=tmp_path / "nerfstudio", colmap_model_path=sparse_path, skip_colmap=True + ) +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb cmd.main() os.environ["PATH"] = old_path @@ -77,4 +129,16 @@ def test_process_images_skip_colmap(tmp_path: Path): outputs = parser.get_dataparser_outputs("train") assert len(outputs.image_filenames) == 9 assert torch.is_tensor(outputs.dataparser_transform) +<<<<<<< HEAD # TODO @jkulhanek: Add tests if the loaded poses are the same +======= + + # Test if the original poses can be obtained back + gt_poses = original_poses[[0, 1, 2, 3, 4, 5, 6, 7, 9]] + dataparser_poses = outputs.transform_poses_to_original_space(outputs.cameras.camera_to_worlds, "opencv").numpy() + dataparser_poses = np.concatenate( + (dataparser_poses, np.array([[[0, 0, 0, 1]]]).repeat(len(dataparser_poses), 0)), 1 + ) + dataparser_poses = np.linalg.inv(dataparser_poses) + np.testing.assert_allclose(gt_poses, dataparser_poses, rtol=0, atol=1e-5) +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb diff --git a/tests/test_train.py b/tests/test_train.py index 9d9e8e4fa..1d30f3bc3 100644 --- a/tests/test_train.py +++ b/tests/test_train.py @@ -13,7 +13,7 @@ from nerfstudio.configs.method_configs import method_configs from nerfstudio.data.dataparsers.blender_dataparser import BlenderDataParserConfig from nerfstudio.data.dataparsers.minimal_dataparser import MinimalDataParserConfig from nerfstudio.engine.trainer import TrainerConfig -from scripts.train import train_loop +from nerfstudio.scripts.train import train_loop BLACKLIST = [ "base", @@ -21,13 +21,18 @@ BLACKLIST = [ "instant-ngp", "instant-ngp-bounded", "nerfacto", + "nerfacto-big", "volinga", "phototourism", "depth-nerfacto", "nerfplayer-ngp", "nerfplayer-nerfacto", "neus", +<<<<<<< HEAD "dreamfusion", +======= + "neus-facto", +>>>>>>> b1ddb9e695b789fae47e0f760a5c7f59f25af0bb ] @@ -50,11 +55,7 @@ def set_reduced_config(config: TrainerConfig): if hasattr(config.pipeline.model, "num_importance_samples"): config.pipeline.model.num_importance_samples = 4 # remove viewer - config.viewer.enable = False - - # model specific config settings - if config.method_name == "instant-ngp": - config.pipeline.model.field_implementation = "torch" + config.viewer.quit_on_train_completion = True return config diff --git a/tests/utils/test_aabb_intersection.py b/tests/utils/test_aabb_intersection.py index 1d6ba34b9..ab8e2ce0e 100644 --- a/tests/utils/test_aabb_intersection.py +++ b/tests/utils/test_aabb_intersection.py @@ -168,7 +168,11 @@ def test_equall_nerfacc(): # time2 = time.time() # time3 = time.time() - t_min_nerfacc, t_max_nerfacc = nerfacc.ray_aabb_intersect(origins, directions, aabb) + t_min_nerfacc, t_max_nerfacc, _ = nerfacc.ray_aabb_intersect( + origins, directions, aabb[None, :], near_plane=0, far_plane=1e10, miss_value=1e10 + ) + t_max_nerfacc = t_max_nerfacc.squeeze(-1) + t_min_nerfacc = t_min_nerfacc.squeeze(-1) # time4 = time.time() # print("pytorch ", time2-time1)