|
from internal import math |
|
import torch |
|
import numpy as np |
|
|
|
|
|
def reflect(viewdirs, normals): |
|
"""Reflect view directions about normals. |
|
|
|
The reflection of a vector v about a unit vector n is a vector u such that |
|
dot(v, n) = dot(u, n), and dot(u, u) = dot(v, v). The solution to these two |
|
equations is u = 2 dot(n, v) n - v. |
|
|
|
Args: |
|
viewdirs: [..., 3] array of view directions. |
|
normals: [..., 3] array of normal directions (assumed to be unit vectors). |
|
|
|
Returns: |
|
[..., 3] array of reflection directions. |
|
""" |
|
return 2.0 * torch.sum(normals * viewdirs, dim=-1, keepdim=True) * normals - viewdirs |
|
|
|
|
|
def l2_normalize(x): |
|
"""Normalize x to unit length along last axis.""" |
|
return torch.nn.functional.normalize(x, dim=-1, eps=torch.finfo(x.dtype).eps) |
|
|
|
|
|
def l2_normalize_np(x): |
|
"""Normalize x to unit length along last axis.""" |
|
return x / np.sqrt(np.maximum(np.sum(x ** 2, axis=-1, keepdims=True), np.finfo(x.dtype).eps)) |
|
|
|
|
|
def compute_weighted_mae(weights, normals, normals_gt): |
|
"""Compute weighted mean angular error, assuming normals are unit length.""" |
|
one_eps = 1 - torch.finfo(weights.dtype).eps |
|
return (weights * torch.arccos(torch.clip((normals * normals_gt).sum(-1), |
|
-one_eps, one_eps))).sum() / weights.sum() * 180.0 / torch.pi |
|
|
|
|
|
def compute_weighted_mae_np(weights, normals, normals_gt): |
|
"""Compute weighted mean angular error, assuming normals are unit length.""" |
|
one_eps = 1 - np.finfo(weights.dtype).eps |
|
return (weights * np.arccos(np.clip((normals * normals_gt).sum(-1), |
|
-one_eps, one_eps))).sum() / weights.sum() * 180.0 / np.pi |
|
|
|
|
|
def generalized_binomial_coeff(a, k): |
|
"""Compute generalized binomial coefficients.""" |
|
return np.prod(a - np.arange(k)) / np.math.factorial(k) |
|
|
|
|
|
def assoc_legendre_coeff(l, m, k): |
|
"""Compute associated Legendre polynomial coefficients. |
|
|
|
Returns the coefficient of the cos^k(theta)*sin^m(theta) term in the |
|
(l, m)th associated Legendre polynomial, P_l^m(cos(theta)). |
|
|
|
Args: |
|
l: associated Legendre polynomial degree. |
|
m: associated Legendre polynomial order. |
|
k: power of cos(theta). |
|
|
|
Returns: |
|
A float, the coefficient of the term corresponding to the inputs. |
|
""" |
|
return ((-1) ** m * 2 ** l * np.math.factorial(l) / np.math.factorial(k) / |
|
np.math.factorial(l - k - m) * |
|
generalized_binomial_coeff(0.5 * (l + k + m - 1.0), l)) |
|
|
|
|
|
def sph_harm_coeff(l, m, k): |
|
"""Compute spherical harmonic coefficients.""" |
|
return (np.sqrt( |
|
(2.0 * l + 1.0) * np.math.factorial(l - m) / |
|
(4.0 * np.pi * np.math.factorial(l + m))) * assoc_legendre_coeff(l, m, k)) |
|
|
|
|
|
def get_ml_array(deg_view): |
|
"""Create a list with all pairs of (l, m) values to use in the encoding.""" |
|
ml_list = [] |
|
for i in range(deg_view): |
|
l = 2 ** i |
|
|
|
for m in range(l + 1): |
|
ml_list.append((m, l)) |
|
|
|
|
|
ml_array = np.array(ml_list).T |
|
return ml_array |
|
|
|
|
|
def generate_ide_fn(deg_view): |
|
"""Generate integrated directional encoding (IDE) function. |
|
|
|
This function returns a function that computes the integrated directional |
|
encoding from Equations 6-8 of arxiv.org/abs/2112.03907. |
|
|
|
Args: |
|
deg_view: number of spherical harmonics degrees to use. |
|
|
|
Returns: |
|
A function for evaluating integrated directional encoding. |
|
|
|
Raises: |
|
ValueError: if deg_view is larger than 5. |
|
""" |
|
if deg_view > 5: |
|
raise ValueError('Only deg_view of at most 5 is numerically stable.') |
|
|
|
ml_array = get_ml_array(deg_view) |
|
l_max = 2 ** (deg_view - 1) |
|
|
|
|
|
|
|
|
|
mat = np.zeros((l_max + 1, ml_array.shape[1])) |
|
for i, (m, l) in enumerate(ml_array.T): |
|
for k in range(l - m + 1): |
|
mat[k, i] = sph_harm_coeff(l, m, k) |
|
mat = torch.from_numpy(mat).float() |
|
ml_array = torch.from_numpy(ml_array).float() |
|
|
|
def integrated_dir_enc_fn(xyz, kappa_inv): |
|
"""Function returning integrated directional encoding (IDE). |
|
|
|
Args: |
|
xyz: [..., 3] array of Cartesian coordinates of directions to evaluate at. |
|
kappa_inv: [..., 1] reciprocal of the concentration parameter of the von |
|
Mises-Fisher distribution. |
|
|
|
Returns: |
|
An array with the resulting IDE. |
|
""" |
|
x = xyz[..., 0:1] |
|
y = xyz[..., 1:2] |
|
z = xyz[..., 2:3] |
|
|
|
|
|
vmz = torch.cat([z ** i for i in range(mat.shape[0])], dim=-1) |
|
|
|
|
|
vmxy = torch.cat([(x + 1j * y) ** m for m in ml_array[0, :]], dim=-1) |
|
|
|
|
|
sph_harms = vmxy * math.matmul(vmz, mat.to(xyz.device)) |
|
|
|
|
|
|
|
sigma = 0.5 * ml_array[1, :] * (ml_array[1, :] + 1) |
|
sigma = sigma.to(sph_harms.device) |
|
ide = sph_harms * torch.exp(-sigma * kappa_inv) |
|
|
|
|
|
return torch.cat([torch.real(ide), torch.imag(ide)], dim=-1) |
|
|
|
return integrated_dir_enc_fn |
|
|
|
|
|
def generate_dir_enc_fn(deg_view): |
|
"""Generate directional encoding (DE) function. |
|
|
|
Args: |
|
deg_view: number of spherical harmonics degrees to use. |
|
|
|
Returns: |
|
A function for evaluating directional encoding. |
|
""" |
|
integrated_dir_enc_fn = generate_ide_fn(deg_view) |
|
|
|
def dir_enc_fn(xyz): |
|
"""Function returning directional encoding (DE).""" |
|
return integrated_dir_enc_fn(xyz, torch.zeros_like(xyz[..., :1])) |
|
|
|
return dir_enc_fn |
|
|