diff --git a/docs/guides/model_components/visualize_encoders.ipynb b/docs/guides/model_components/visualize_encoders.ipynb index 8ed2dbf97..6ef2c0950 100644 --- a/docs/guides/model_components/visualize_encoders.ipynb +++ b/docs/guides/model_components/visualize_encoders.ipynb @@ -208,7 +208,7 @@ { "data": { "text/html": [ - "
" + "
" ], "text/plain": [ "" @@ -228,7 +228,7 @@ { "data": { "text/html": [ - "
" + "
" ], "text/plain": [ "" @@ -247,7 +247,7 @@ { "data": { "text/html": [ - "
" + "
" ], "text/plain": [ "" @@ -266,7 +266,7 @@ { "data": { "text/html": [ - "
" + "
" ], "text/plain": [ "" @@ -318,7 +318,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 18, "metadata": { "tags": [ "hide-input" @@ -329,6 +329,8 @@ "name": "stdout", "output_type": "stream", "text": [ + "tensor(0.0010, grad_fn=)\n", + "tensor(-0.0010, grad_fn=)\n", "Input Values:\n" ] }, @@ -354,7 +356,7 @@ { "data": { "text/html": [ - "
" + "
" ], "text/plain": [ "" @@ -373,7 +375,7 @@ "num_levels = 8\n", "min_res = 2\n", "max_res = 128\n", - "hash_table_size = 2**4 # Typically much larger tables are used\n", + "log2_hashmap_size = 4 # Typically much larger tables are used\n", "\n", "resolution = 128\n", "slice = 0\n", @@ -385,15 +387,18 @@ " num_levels=num_levels,\n", " min_res=min_res,\n", " max_res=max_res,\n", - " hash_table_size=hash_table_size,\n", + " log2_hashmap_size=log2_hashmap_size,\n", " features_per_level=features_per_level,\n", " hash_init_scale=0.001,\n", + " implementation=\"torch\",\n", ")\n", "\n", "x_samples = torch.linspace(0, 1, resolution)\n", "grid = torch.stack(torch.meshgrid([x_samples, x_samples, x_samples], indexing=\"ij\"), dim=-1)\n", "\n", "encoded_values = encoder(grid)\n", + "print(torch.max(encoded_values))\n", + "print(torch.min(encoded_values))\n", "\n", "grid_slice = grid[slice, ...]\n", "encoded_values_slice = encoded_values[slice, ...]\n", @@ -420,7 +425,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "tags": [ "hide-input" diff --git a/nerfstudio/utils/activations.py b/nerfstudio/field_components/activations.py similarity index 100% rename from nerfstudio/utils/activations.py rename to nerfstudio/field_components/activations.py diff --git a/nerfstudio/field_components/encoding.py b/nerfstudio/field_components/encoding.py index 1e4fedf58..3e33cae30 100644 --- a/nerfstudio/field_components/encoding.py +++ b/nerfstudio/field_components/encoding.py @@ -17,7 +17,7 @@ Encoding functions """ from abc import abstractmethod -from typing import Optional +from typing import Literal, Optional import numpy as np import torch @@ -26,8 +26,16 @@ from torch import nn from torchtyping import TensorType from nerfstudio.field_components.base import FieldModule +from nerfstudio.utils.comments import print_tcnn_speed_warning from nerfstudio.utils.math import components_from_spherical_harmonics, expected_sin +try: + import tinycudann as tcnn + + TCNN_EXISTS = True +except ImportError: + TCNN_EXISTS = False + class Encoding(FieldModule): """Encode an input tensor. Intended to be subclassed @@ -215,9 +223,10 @@ class HashEncoding(Encoding): num_levels: Number of feature grids. min_res: Resolution of smallest feature grid. max_res: Resolution of largest feature grid. - hash_table_size: Size of hash table. + log2_hashmap_size: Size of hash map is 2^log2_hashmap_size. features_per_level: Number of features per level. hash_init_scale: Value to initialize hash grid. + implementation: Implementation of hash encoding. Fallback to torch if tcnn not available. """ def __init__( @@ -225,25 +234,43 @@ class HashEncoding(Encoding): num_levels: int = 16, min_res: int = 16, max_res: int = 1024, - hash_table_size: int = 2**19, + log2_hashmap_size: int = 19, features_per_level: int = 2, hash_init_scale: float = 0.001, + implementation: Literal["tcnn", "torch"] = "tcnn", ) -> None: super().__init__(in_dim=3) self.num_levels = num_levels self.features_per_level = features_per_level - self.hash_table_size = hash_table_size + self.log2_hashmap_size = log2_hashmap_size + self.hash_table_size = 2**log2_hashmap_size levels = torch.arange(num_levels) growth_factor = np.exp((np.log(max_res) - np.log(min_res)) / (num_levels - 1)) self.scalings = torch.floor(min_res * growth_factor**levels) - self.hash_offset = levels * hash_table_size - self.hash_table = torch.rand(size=(hash_table_size * num_levels, features_per_level)) * 2 - 1 + self.hash_offset = levels * self.hash_table_size + self.hash_table = torch.rand(size=(self.hash_table_size * num_levels, features_per_level)) * 2 - 1 self.hash_table *= hash_init_scale self.hash_table = nn.Parameter(self.hash_table) + self.tcnn_encoding = None + if not TCNN_EXISTS and implementation == "tcnn": + print_tcnn_speed_warning("HashEncoding") + elif implementation == "tcnn": + self.tcnn_encoding = tcnn.Encoding( + n_input_dims=3, + encoding_config={ + "otype": "HashGrid", + "n_levels": self.num_levels, + "n_features_per_level": self.features_per_level, + "log2_hashmap_size": self.log2_hashmap_size, + "base_resolution": min_res, + "per_level_scale": growth_factor, + }, + ) + def get_out_dim(self) -> int: return self.num_levels * self.features_per_level @@ -266,9 +293,10 @@ class HashEncoding(Encoding): x += self.hash_offset.to(x.device) return x - def forward(self, in_tensor: TensorType["bs":..., "input_dim"]) -> TensorType["bs":..., "output_dim"]: - assert in_tensor.shape[-1] == 3 + def pytorch_fwd(self, in_tensor: TensorType["bs":..., "input_dim"]) -> TensorType["bs":..., "output_dim"]: + """Forward pass using pytorch. Significantly slower than TCNN implementation.""" + assert in_tensor.shape[-1] == 3 in_tensor = in_tensor[..., None, :] # [..., 1, 3] scaled = in_tensor * self.scalings.view(-1, 1).to(in_tensor.device) # [..., L, 3] scaled_c = torch.ceil(scaled).type(torch.int32) @@ -308,6 +336,11 @@ class HashEncoding(Encoding): return torch.flatten(encoded_value, start_dim=-2, end_dim=-1) # [..., num_levels * features_per_level] + def forward(self, in_tensor: TensorType["bs":..., "input_dim"]) -> TensorType["bs":..., "output_dim"]: + if TCNN_EXISTS and self.tcnn_encoding is not None: + return self.tcnn_encoding(in_tensor) + return self.pytorch_fwd(in_tensor) + class TensorCPEncoding(Encoding): """Learned CANDECOMP/PARFAC (CP) decomposition encoding used in TensoRF diff --git a/nerfstudio/fields/density_fields.py b/nerfstudio/fields/density_fields.py index e807213ac..c2fad1e17 100644 --- a/nerfstudio/fields/density_fields.py +++ b/nerfstudio/fields/density_fields.py @@ -26,9 +26,9 @@ from torchtyping import TensorType from nerfstudio.cameras.rays import RaySamples from nerfstudio.data.scene_box import SceneBox +from nerfstudio.field_components.activations import trunc_exp from nerfstudio.field_components.spatial_distortions import SpatialDistortion from nerfstudio.fields.base_field import Field -from nerfstudio.utils.activations import trunc_exp try: import tinycudann as tcnn diff --git a/nerfstudio/fields/instant_ngp_field.py b/nerfstudio/fields/instant_ngp_field.py index dc0ec8c71..de88d2714 100644 --- a/nerfstudio/fields/instant_ngp_field.py +++ b/nerfstudio/fields/instant_ngp_field.py @@ -26,10 +26,10 @@ from torchtyping import TensorType from nerfstudio.cameras.rays import RaySamples 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.fields.base_field import Field -from nerfstudio.utils.activations import trunc_exp try: import tinycudann as tcnn diff --git a/nerfstudio/fields/nerfacto_field.py b/nerfstudio/fields/nerfacto_field.py index 3a5cb2682..b3c0f6cc4 100644 --- a/nerfstudio/fields/nerfacto_field.py +++ b/nerfstudio/fields/nerfacto_field.py @@ -27,6 +27,7 @@ from torchtyping import TensorType from nerfstudio.cameras.rays import RaySamples 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.encoding import Encoding, HashEncoding, SHEncoding from nerfstudio.field_components.field_heads import ( @@ -41,7 +42,6 @@ from nerfstudio.field_components.spatial_distortions import ( SpatialDistortion, ) from nerfstudio.fields.base_field import Field -from nerfstudio.utils.activations import trunc_exp try: import tinycudann as tcnn diff --git a/nerfstudio/utils/comments.py b/nerfstudio/utils/comments.py new file mode 100644 index 000000000..8291de25c --- /dev/null +++ b/nerfstudio/utils/comments.py @@ -0,0 +1,31 @@ +# Copyright 2022 The Plenoptix 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. + +"""A collection of common strings and print statements used throughout the codebase.""" + +from rich.console import Console + +console = Console(width=120) + + +def print_tcnn_speed_warning(method_name: str): + """Prints a warning about the speed of the TCNN.""" + console.line() + console.print(f"[bold yellow]WARNING: Using a slow implementation of {method_name}. ") + console.print( + "[bold yellow]:person_running: :person_running: " + + "Install tcnn for speedups :person_running: :person_running:" + ) + console.print("[yellow]pip install git+https://github.com/NVlabs/tiny-cuda-nn/#subdirectory=bindings/torch") + console.line() diff --git a/nerfstudio/utils/rich.py b/nerfstudio/utils/rich_utils.py similarity index 100% rename from nerfstudio/utils/rich.py rename to nerfstudio/utils/rich_utils.py diff --git a/scripts/eval.py b/scripts/eval.py index 9bfe8c338..098740556 100755 --- a/scripts/eval.py +++ b/scripts/eval.py @@ -30,7 +30,7 @@ from nerfstudio.cameras.camera_paths import get_path_from_json, get_spiral_path from nerfstudio.cameras.cameras import Cameras from nerfstudio.configs import base_config as cfg from nerfstudio.pipelines.base import Pipeline -from nerfstudio.utils.rich import ItersPerSecColumn +from nerfstudio.utils.rich_utils import ItersPerSecColumn console = Console(width=120) diff --git a/tests/fields/modules/test_encodings.py b/tests/fields/modules/test_encodings.py index 3b8b350d9..fe79f7799 100644 --- a/tests/fields/modules/test_encodings.py +++ b/tests/fields/modules/test_encodings.py @@ -139,6 +139,38 @@ def test_tensor_sh_encoder(): assert encoded.shape == (10, out_dim) +def test_tensor_hash_encoder(): + """Test Spherical Harmonic encoder""" + + num_levels = 4 + features_per_level = 4 + out_dim = num_levels * features_per_level + + encoder = encoding.HashEncoding( + num_levels=num_levels, + features_per_level=features_per_level, + log2_hashmap_size=5, + implementation="torch", + ) + assert encoder.get_out_dim() == out_dim + + in_tensor = torch.zeros((10, 3)) + in_tensor[..., 1] = 1 + encoded = encoder(in_tensor) + assert encoded.shape == (10, out_dim) + + encoder = encoding.HashEncoding( + num_levels=num_levels, + features_per_level=features_per_level, + log2_hashmap_size=5, + implementation="tcnn", + ) + in_tensor = torch.zeros((10, 3)) + in_tensor[..., 1] = 1 + encoded = encoder(in_tensor) + assert encoded.shape == (10, out_dim) + + if __name__ == "__main__": test_scaling_and_offset() test_nerf_encoder() @@ -146,3 +178,4 @@ if __name__ == "__main__": test_tensor_vm_encoder() test_tensor_cp_encoder() test_tensor_sh_encoder() + test_tensor_hash_encoder()