mirror of
https://github.com/nerfstudio-project/nerfstudio.git
synced 2026-11-13 03:14:00 +08:00
Merge pull request #75 from ethanweber/tancik/spherical_harmoincs
Tancik/spherical harmoincs
This commit is contained in:
7 files changed
+366
-30
No files matched your search
@@ -9,6 +9,7 @@ from torch import nn
|
||||
from torchtyping import TensorType
|
||||
|
||||
from mattport.nerf.field_modules.base import FieldModule
|
||||
from mattport.utils.math import components_from_spherical_harmonics
|
||||
|
||||
|
||||
class Encoding(FieldModule):
|
||||
@@ -348,3 +349,26 @@ class TensorVMEncoding(Encoding):
|
||||
)
|
||||
|
||||
self.resolution = resolution
|
||||
|
||||
|
||||
class SHEncoding(Encoding):
|
||||
"""Spherical harmonic encoding"""
|
||||
|
||||
def __init__(self, levels: int = 4) -> None:
|
||||
"""
|
||||
Args:
|
||||
levels (int, optional): Number of spherical hamonic levels to encode. Defaults to 4.
|
||||
"""
|
||||
super().__init__(in_dim=3)
|
||||
|
||||
if levels <= 0 or levels > 4:
|
||||
raise ValueError(f"Spherical harmonic encoding only suports 1 to 4 levels, requested {levels}")
|
||||
|
||||
self.levels = levels
|
||||
|
||||
def get_out_dim(self) -> int:
|
||||
return self.levels**2
|
||||
|
||||
@torch.no_grad()
|
||||
def encode(self, in_tensor: TensorType[..., "input_dim"]) -> TensorType[..., "output_dim"]:
|
||||
return components_from_spherical_harmonics(levels=self.levels, directions=in_tensor)
|
||||
@@ -71,53 +71,56 @@ class FieldHead(FieldModule):
|
||||
class DensityFieldHead(FieldHead):
|
||||
"""Density output"""
|
||||
|
||||
def __init__(
|
||||
self, in_dim: int, field_head_name=FieldHeadNames.DENSITY, activation: Optional[nn.Module] = nn.Softplus()
|
||||
) -> None:
|
||||
super().__init__(in_dim, out_dim=1, field_head_name=field_head_name, activation=activation)
|
||||
def __init__(self, in_dim: int, activation: Optional[nn.Module] = nn.Softplus()) -> None:
|
||||
super().__init__(in_dim, out_dim=1, field_head_name=FieldHeadNames.DENSITY, activation=activation)
|
||||
|
||||
|
||||
class RGBFieldHead(FieldHead):
|
||||
"""RGB output"""
|
||||
|
||||
def __init__(
|
||||
self, in_dim: int, field_head_name=FieldHeadNames.RGB, activation: Optional[nn.Module] = nn.Sigmoid()
|
||||
) -> None:
|
||||
super().__init__(in_dim, out_dim=3, field_head_name=field_head_name, activation=activation)
|
||||
def __init__(self, in_dim: int, activation: Optional[nn.Module] = nn.Sigmoid()) -> None:
|
||||
super().__init__(in_dim, out_dim=3, field_head_name=FieldHeadNames.RGB, activation=activation)
|
||||
|
||||
|
||||
class SHFieldHead(FieldHead):
|
||||
"""Spherical harmonics output"""
|
||||
|
||||
def __init__(self, in_dim: int, levels: int = 3, channels: int = 3, activation: Optional[nn.Module] = None) -> None:
|
||||
"""_summary_
|
||||
|
||||
Args:
|
||||
in_dim (int): Input dimension
|
||||
levels (int, optional): Number of spherical harmonics layers. Defaults to 3.
|
||||
channels (int, optional): Number of channels. Defaults to 3 (ie RGB).
|
||||
activation (Optional[nn.Module], optional): Output activation. Defaults to None.
|
||||
"""
|
||||
out_dim = channels * levels**2
|
||||
super().__init__(in_dim, out_dim=out_dim, field_head_name=FieldHeadNames.RGB, activation=activation)
|
||||
|
||||
|
||||
class UncertaintyFieldHead(FieldHead):
|
||||
"""Uncertainty output"""
|
||||
|
||||
def __init__(
|
||||
self, in_dim: int, field_head_name=FieldHeadNames.UNCERTAINTY, activation: Optional[nn.Module] = nn.Softplus()
|
||||
) -> None:
|
||||
super().__init__(in_dim, out_dim=1, field_head_name=field_head_name, activation=activation)
|
||||
def __init__(self, in_dim: int, activation: Optional[nn.Module] = nn.Softplus()) -> None:
|
||||
super().__init__(in_dim, out_dim=1, field_head_name=FieldHeadNames.UNCERTAINTY, activation=activation)
|
||||
|
||||
|
||||
class TransientRGBHead(RGBFieldHead):
|
||||
class TransientRGBHead(FieldHead):
|
||||
"""Transient RGB output"""
|
||||
|
||||
def __init__(
|
||||
self, in_dim: int, field_head_name=FieldHeadNames.TRANSIENT_RGB, activation: Optional[nn.Module] = nn.Sigmoid()
|
||||
) -> None:
|
||||
super().__init__(in_dim, field_head_name, activation)
|
||||
def __init__(self, in_dim: int, activation: Optional[nn.Module] = nn.Sigmoid()) -> None:
|
||||
super().__init__(in_dim, out_dim=3, field_head_name=FieldHeadNames.TRANSIENT_RGB, activation=activation)
|
||||
|
||||
|
||||
class TransientDensityHead(DensityFieldHead):
|
||||
class TransientDensityHead(FieldHead):
|
||||
"""Transient density output"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
in_dim: int,
|
||||
field_head_name=FieldHeadNames.TRANSIENT_DENSITY,
|
||||
activation: Optional[nn.Module] = nn.Softplus(),
|
||||
) -> None:
|
||||
super().__init__(in_dim, field_head_name, activation)
|
||||
def __init__(self, in_dim: int, activation: Optional[nn.Module] = nn.Softplus()) -> None:
|
||||
super().__init__(in_dim, out_dim=1, field_head_name=FieldHeadNames.TRANSIENT_DENSITY, activation=activation)
|
||||
|
||||
|
||||
class SemanticStuffHead(FieldHead):
|
||||
"""Semantic stuff output"""
|
||||
|
||||
def __init__(self, in_dim: int, num_classes: int, field_head_name=FieldHeadNames.SEMANTICS_STUFF) -> None:
|
||||
super().__init__(in_dim, out_dim=num_classes, field_head_name=field_head_name, activation=None)
|
||||
def __init__(self, in_dim: int, num_classes: int) -> None:
|
||||
super().__init__(in_dim, out_dim=num_classes, field_head_name=FieldHeadNames.SEMANTICS_STUFF, activation=None)
|
||||
@@ -2,12 +2,15 @@
|
||||
Collection of renderers
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from torchtyping import TensorType
|
||||
|
||||
from mattport.utils.math import components_from_spherical_harmonics
|
||||
|
||||
|
||||
@dataclass
|
||||
class RendererOutputs:
|
||||
@@ -62,6 +65,64 @@ class RGBRenderer(nn.Module):
|
||||
return renderer_outputs
|
||||
|
||||
|
||||
class SHRenderer(nn.Module):
|
||||
"""Render RGB value from spherical harmonics."""
|
||||
|
||||
def __init__(
|
||||
self, activation: Optional[nn.Module] = nn.Sigmoid(), background_color: Optional[TensorType[3]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
background_color (TensorType[3], optional): Background color as RGB. Defaults to black.
|
||||
activation (Optional[nn.Module], optional): Output activation. Defaults to Sigmoid().
|
||||
"""
|
||||
super().__init__()
|
||||
self.background_color = background_color
|
||||
self.activation = activation
|
||||
|
||||
def forward(
|
||||
self,
|
||||
sh: TensorType[..., "num_samples", "coeffs"],
|
||||
directions: TensorType[..., "num_samples", 3],
|
||||
weights: TensorType[..., "num_samples"],
|
||||
) -> RendererOutputs:
|
||||
"""Composite samples along ray and render color image
|
||||
|
||||
Args:
|
||||
sh (TensorType[..., "num_samples", "coeffs"]): Spherical hamonics coefficients for each sample
|
||||
directions: (TensorType[..., "num_samples", 3]): Sample direction
|
||||
weights (TensorType[..., "num_samples"]): Weights for each sample
|
||||
|
||||
Returns:
|
||||
RendererOutputs: Outputs with rgb values.
|
||||
"""
|
||||
|
||||
sh = sh.view(*sh.shape[:-1], 3, sh.shape[-1] // 3)
|
||||
|
||||
levels = int(math.sqrt(sh.shape[-1]))
|
||||
components = components_from_spherical_harmonics(levels=levels, directions=directions)
|
||||
|
||||
rgb = sh * components[..., None, :] # [..., num_samples, 3, sh_components]
|
||||
rgb = torch.sum(sh, dim=-1) # [..., num_samples, 3]
|
||||
|
||||
rgb = torch.sum(weights[..., None] * rgb, dim=-2) + 0.5
|
||||
|
||||
if self.activation is not None:
|
||||
self.activation(rgb)
|
||||
|
||||
if self.background_color is not None:
|
||||
rgb = rgb + self.background_color.to(weights.device)[None, ...] * (
|
||||
1.0 - torch.sum(weights, dim=-1)[..., None]
|
||||
)
|
||||
torch.clamp_(rgb, min=0.0, max=1.0)
|
||||
|
||||
assert torch.max(rgb) <= 1.0
|
||||
assert torch.min(rgb) >= 0.0
|
||||
|
||||
renderer_outputs = RendererOutputs(rgb=rgb)
|
||||
return renderer_outputs
|
||||
|
||||
|
||||
class AccumulationRenderer(nn.Module):
|
||||
"""Accumulated value along a ray."""
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
""" Math Helper Functions """
|
||||
|
||||
import torch
|
||||
from torchtyping import TensorType
|
||||
|
||||
|
||||
def components_from_spherical_harmonics(levels: int, directions: TensorType[..., 3]) -> TensorType[..., "components"]:
|
||||
"""
|
||||
Args:
|
||||
levels (int): Number of spherical harmonic levels to compute.
|
||||
coeffs (TensorType[..., "coeffs"]): Spherical hamonic coefficients
|
||||
|
||||
Returns:
|
||||
TensorType[..., "components"]: Value for each component.
|
||||
"""
|
||||
num_components = levels**2
|
||||
components = torch.zeros((*directions.shape[:-1], num_components)).to(directions.device)
|
||||
|
||||
assert 1 <= levels <= 5, f"SH levels must be in [1,4], got {levels}"
|
||||
assert directions.shape[-1] == 3, f"Direction input should have three dimensions. Got {directions.shape[-1]}"
|
||||
|
||||
x = directions[..., 0]
|
||||
y = directions[..., 1]
|
||||
z = directions[..., 2]
|
||||
|
||||
xx = x**2
|
||||
yy = y**2
|
||||
zz = z**2
|
||||
|
||||
# l0
|
||||
components[..., 0] = 0.28209479177387814
|
||||
|
||||
# l1
|
||||
if levels > 1:
|
||||
components[..., 1] = 0.4886025119029199 * y
|
||||
components[..., 2] = 0.4886025119029199 * z
|
||||
components[..., 3] = 0.4886025119029199 * x
|
||||
|
||||
# l2
|
||||
if levels > 2:
|
||||
components[..., 4] = 1.0925484305920792 * x * y
|
||||
components[..., 5] = 1.0925484305920792 * y * z
|
||||
components[..., 6] = 0.9461746957575601 * zz
|
||||
components[..., 7] = 1.0925484305920792 * x * z
|
||||
components[..., 8] = 0.5462742152960396 * (xx - yy)
|
||||
|
||||
# l3
|
||||
if levels > 3:
|
||||
components[..., 9] = 0.5900435899266435 * y * (3 * xx - yy)
|
||||
components[..., 10] = 2.890611442640554 * x * y * z
|
||||
components[..., 11] = 0.4570457994644658 * y * (5 * zz - 1)
|
||||
components[..., 12] = 0.3731763325901154 * z * (5 * zz - 3)
|
||||
components[..., 13] = 0.4570457994644658 * x * (5 * zz - 1)
|
||||
components[..., 14] = 1.445305721320277 * z * (xx - yy)
|
||||
components[..., 15] = 0.5900435899266435 * x * (xx - 3 * yy)
|
||||
|
||||
# l4
|
||||
if levels > 4:
|
||||
components[..., 16] = 2.5033429417967046 * x * y * (xx - yy)
|
||||
components[..., 17] = 1.7701307697799304 * y * z * (3 * xx - yy)
|
||||
components[..., 18] = 0.9461746957575601 * x * y * (7 * zz - 1)
|
||||
components[..., 19] = 0.6690465435572892 * y * (7 * zz - 3 * z)
|
||||
components[..., 20] = 0.10578554691520431 * (35 * zz * zz - 30 * zz + 3)
|
||||
components[..., 21] = 0.6690465435572892 * x * z * (7 * zz - 3)
|
||||
components[..., 22] = 0.47308734787878004 * (xx - yy) * (7 * zz - 1)
|
||||
components[..., 23] = 1.7701307697799304 * x * z * (xx - 3 * yy)
|
||||
components[..., 24] = 0.4425326924449826 * (xx * (xx - 3 * yy) - yy * (3 * xx - yy))
|
||||
|
||||
return components
|
||||
@@ -8,7 +8,7 @@
|
||||
"source": [
|
||||
"import torch\n",
|
||||
"import mediapy as media\n",
|
||||
"from mattport.nerf.field_modules import encoding\n"
|
||||
"from mattport.nerf.field_modules import encoding"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -251,6 +251,118 @@
|
||||
"media.show_images(encoded_images.detach().numpy(), cmap=\"plasma\", border=True)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Spherical Harmonic Encoding\n",
|
||||
"Encode direction using spherical harmoincs. (Mostly used to encode viewing direction)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 66,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Level: 1\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<table class=\"show_images\" style=\"border-spacing:0px;\"><tr><td style=\"padding:1px;\"><img width=\"150\" height=\"100\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAABkCAIAAADrOV6nAAABA0lEQVR4nO3RsQkAIQDAwPcrG+d2bYewkMDdBIGMNfdH2f86gFsW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYZ2GehXkW5lmYdwDkygFkzaTKPwAAAABJRU5ErkJggg==\"/></td></tr></table>"
|
||||
],
|
||||
"text/plain": [
|
||||
"<IPython.core.display.HTML object>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Level: 2\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<table class=\"show_images\" style=\"border-spacing:0px;\"><tr><td style=\"padding:1px;\"><img width=\"150\" height=\"100\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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 truncated
|
||||
],
|
||||
"text/plain": [
|
||||
"<IPython.core.display.HTML object>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Level: 3\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<table class=\"show_images\" style=\"border-spacing:0px;\"><tr><td style=\"padding:1px;\"><img width=\"150\" height=\"100\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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 truncated
|
||||
],
|
||||
"text/plain": [
|
||||
"<IPython.core.display.HTML object>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Level: 4\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<table class=\"show_images\" style=\"border-spacing:0px;\"><tr><td style=\"padding:1px;\"><img width=\"150\" height=\"100\" style=\"border:1px solid black; image-rendering:pixelated; object-fit:cover;\" src=\"data:image/png;base64,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 truncated
|
||||
],
|
||||
"text/plain": [
|
||||
"<IPython.core.display.HTML object>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"levels = 4\n",
|
||||
"\n",
|
||||
"height = 100\n",
|
||||
"width = 150\n",
|
||||
"\n",
|
||||
"encoder = encoding.SHEncoding(levels=levels)\n",
|
||||
"\n",
|
||||
"theta = torch.linspace(-torch.pi, torch.pi, width)\n",
|
||||
"phi = torch.linspace(0, torch.pi, height)\n",
|
||||
"[theta, phi] = torch.meshgrid([theta, phi], indexing=\"xy\")\n",
|
||||
"\n",
|
||||
"directions = torch.stack([torch.cos(theta) * torch.sin(phi), torch.sin(theta) * torch.sin(phi), torch.cos(phi)], dim=-1)\n",
|
||||
"\n",
|
||||
"encoded_values = encoder(directions)\n",
|
||||
"encoded_values = torch.moveaxis(encoded_values, 2, 0)\n",
|
||||
"\n",
|
||||
"for level in range(levels):\n",
|
||||
" print(f\"Level: {level+1}\")\n",
|
||||
" media.show_images(encoded_values[level**2:(level+1)**2,...], cmap=\"plasma\", border=True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
@@ -278,7 +390,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.9.11"
|
||||
"version": "3.9.12"
|
||||
},
|
||||
"orig_nbformat": 4
|
||||
},
|
||||
|
||||
@@ -111,9 +111,28 @@ def test_tensor_cp_encoder():
|
||||
encoder.upsample_grid(resolution=64)
|
||||
|
||||
|
||||
def test_tensor_sh_encoder():
|
||||
"""Test Spherical Harmonic encoder"""
|
||||
|
||||
levels = 4
|
||||
out_dim = levels**2
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
encoder = encoding.SHEncoding(levels=5)
|
||||
|
||||
encoder = encoding.SHEncoding(levels=levels)
|
||||
assert encoder.get_out_dim() == out_dim
|
||||
|
||||
in_tensor = torch.zeros((10, 3))
|
||||
in_tensor[..., 1] = 1
|
||||
encoded = encoder.encode(in_tensor)
|
||||
assert encoded.shape == (10, out_dim)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_scaling_and_offset()
|
||||
test_nerf_encoder()
|
||||
test_rff_encoder()
|
||||
test_tensor_vm_encoder()
|
||||
test_tensor_cp_encoder()
|
||||
test_tensor_sh_encoder()
|
||||
@@ -23,8 +23,56 @@ def test_rgb_renderer():
|
||||
out = rgb_renderer(rgb=rgb * 0, weights=weights)
|
||||
assert torch.max(out.rgb) == pytest.approx(0)
|
||||
|
||||
# TODO Tancik: Add more precise tests
|
||||
|
||||
def test_sh_renderer():
|
||||
"""Test SH volumetric rendering"""
|
||||
|
||||
levels = 4
|
||||
num_samples = 10
|
||||
|
||||
sh = torch.ones((3, num_samples, 3 * levels**2))
|
||||
weights = torch.ones((3, num_samples))
|
||||
weights /= torch.sum(weights, axis=-1, keepdim=True)
|
||||
directions = torch.zeros((3, num_samples, 3))
|
||||
directions[..., 0] = 1
|
||||
|
||||
sh_renderer = renderers.SHRenderer()
|
||||
|
||||
out = sh_renderer(sh=sh, directions=directions, weights=weights)
|
||||
assert torch.max(out.rgb) > 0.9
|
||||
|
||||
|
||||
def test_acc_renderer():
|
||||
"""Test accumulation rendering"""
|
||||
|
||||
num_samples = 10
|
||||
weights = torch.ones((3, num_samples))
|
||||
weights /= torch.sum(weights, axis=-1, keepdim=True)
|
||||
|
||||
acc_renderer = renderers.AccumulationRenderer()
|
||||
|
||||
out = acc_renderer(weights=weights)
|
||||
assert torch.max(out.accumulation) > 0.9
|
||||
|
||||
|
||||
def test_depth_renderer():
|
||||
"""Test depth rendering"""
|
||||
|
||||
num_samples = 10
|
||||
weights = torch.ones((3, num_samples))
|
||||
weights /= torch.sum(weights, axis=-1, keepdim=True)
|
||||
|
||||
ts = torch.linspace(0, 100, num_samples)
|
||||
ts = torch.stack([ts, ts, ts], dim=0)
|
||||
|
||||
depth_renderer = renderers.DepthRenderer()
|
||||
|
||||
out = depth_renderer(weights=weights, ts=ts)
|
||||
assert torch.min(out.depth) > 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_rgb_renderer()
|
||||
test_sh_renderer()
|
||||
test_acc_renderer()
|
||||
test_depth_renderer()
|
||||
Reference in new issue
Block a user