Spaces:
Runtime error
Runtime error
""" | |
Copyright (c) Meta Platforms, Inc. and affiliates. | |
All rights reserved. | |
This source code is licensed under the license found in the | |
LICENSE file in the root directory of this source tree. | |
""" | |
import logging | |
from logging import Logger | |
from typing import Any, Dict, Optional, Tuple, Union | |
import igl | |
import numpy as np | |
import torch as th | |
import torch.nn as nn | |
import torch.nn.functional as F | |
from visualize.ca_body.utils.geom import ( | |
index_image_impaint, | |
make_uv_barys, | |
make_uv_vert_index, | |
) | |
from trimesh import Trimesh | |
from trimesh.triangles import points_to_barycentric | |
logger: Logger = logging.getLogger(__name__) | |
def face_normals_v2(v: th.Tensor, vi: th.Tensor, eps: float = 1e-5) -> th.Tensor: | |
pts = v[:, vi] | |
v0 = pts[:, :, 1] - pts[:, :, 0] | |
v1 = pts[:, :, 2] - pts[:, :, 0] | |
n = th.cross(v0, v1, dim=-1) | |
norm = th.norm(n, dim=-1, keepdim=True) | |
norm[norm < eps] = 1 | |
n /= norm | |
return n | |
def vert_normals_v2(v: th.Tensor, vi: th.Tensor, eps: float = 1.0e-5) -> th.Tensor: | |
fnorms = face_normals_v2(v, vi) | |
fnorms = fnorms[:, :, None].expand(-1, -1, 3, -1).reshape(fnorms.shape[0], -1, 3) | |
vi_flat = vi.view(1, -1).expand(v.shape[0], -1) | |
vnorms = th.zeros_like(v) | |
for j in range(3): | |
vnorms[..., j].scatter_add_(1, vi_flat, fnorms[..., j]) | |
norm = th.norm(vnorms, dim=-1, keepdim=True) | |
norm[norm < eps] = 1 | |
vnorms /= norm | |
return vnorms | |
def compute_neighbours( | |
n_verts: int, vi: th.Tensor, n_max_values: int = 10 | |
) -> Tuple[th.Tensor, th.Tensor]: | |
"""Computes first-ring neighbours given vertices and faces.""" | |
n_vi = vi.shape[0] | |
adj = {i: set() for i in range(n_verts)} | |
for i in range(n_vi): | |
for idx in vi[i]: | |
adj[idx] |= set(vi[i]) - {idx} | |
nbs_idxs = np.tile(np.arange(n_verts)[:, np.newaxis], (1, n_max_values)) | |
nbs_weights = np.zeros((n_verts, n_max_values), dtype=np.float32) | |
for idx in range(n_verts): | |
n_values = min(len(adj[idx]), n_max_values) | |
nbs_idxs[idx, :n_values] = np.array(list(adj[idx]))[:n_values] | |
nbs_weights[idx, :n_values] = -1.0 / n_values | |
return nbs_idxs, nbs_weights | |
def compute_v2uv(n_verts: int, vi: th.Tensor, vti: th.Tensor, n_max: int = 4) -> th.Tensor: | |
"""Computes mapping from vertex indices to texture indices. | |
Args: | |
vi: [F, 3], triangles | |
vti: [F, 3], texture triangles | |
n_max: int, max number of texture locations | |
Returns: | |
[n_verts, n_max], texture indices | |
""" | |
v2uv_dict = {} | |
for i_v, i_uv in zip(vi.reshape(-1), vti.reshape(-1)): | |
v2uv_dict.setdefault(i_v, set()).add(i_uv) | |
assert len(v2uv_dict) == n_verts | |
v2uv = np.zeros((n_verts, n_max), dtype=np.int32) | |
for i in range(n_verts): | |
vals = sorted(v2uv_dict[i]) | |
v2uv[i, :] = vals[0] | |
v2uv[i, : len(vals)] = np.array(vals) | |
return v2uv | |
def values_to_uv(values: th.Tensor, index_img: th.Tensor, bary_img: th.Tensor) -> th.Tensor: | |
uv_size = index_img.shape[0] | |
index_mask = th.all(index_img != -1, dim=-1) | |
idxs_flat = index_img[index_mask].to(th.int64) | |
bary_flat = bary_img[index_mask].to(th.float32) | |
# NOTE: here we assume | |
values_flat = th.sum(values[:, idxs_flat].permute(0, 3, 1, 2) * bary_flat, dim=-1) | |
values_uv = th.zeros( | |
values.shape[0], | |
values.shape[-1], | |
uv_size, | |
uv_size, | |
dtype=values.dtype, | |
device=values.device, | |
) | |
values_uv[:, :, index_mask] = values_flat | |
return values_uv | |
def sample_uv( | |
values_uv: th.Tensor, | |
uv_coords: th.Tensor, | |
v2uv: Optional[th.Tensor] = None, | |
mode: str = "bilinear", | |
align_corners: bool = False, | |
flip_uvs: bool = False, | |
) -> th.Tensor: | |
batch_size = values_uv.shape[0] | |
if flip_uvs: | |
uv_coords = uv_coords.clone() | |
uv_coords[:, 1] = 1.0 - uv_coords[:, 1] | |
uv_coords_norm = (uv_coords * 2.0 - 1.0)[np.newaxis, :, np.newaxis].expand( | |
batch_size, -1, -1, -1 | |
) | |
values = ( | |
F.grid_sample(values_uv, uv_coords_norm, align_corners=align_corners, mode=mode) | |
.squeeze(-1) | |
.permute((0, 2, 1)) | |
) | |
if v2uv is not None: | |
values_duplicate = values[:, v2uv] | |
values = values_duplicate.mean(2) | |
# if return_var: | |
# values_var = values_duplicate.var(2) | |
# return values, values_var | |
return values | |
def compute_tbn_uv( | |
tri_xyz: th.Tensor, tri_uv: th.Tensor, eps: float = 1e-5 | |
) -> Tuple[th.Tensor, th.Tensor, th.Tensor]: | |
"""Compute tangents, bitangents, normals. | |
Args: | |
tri_xyz: [B,N,3,3] vertex coordinates | |
tri_uv: [N,2] texture coordinates | |
Returns: | |
tangents, bitangents, normals | |
""" | |
tri_uv = tri_uv[np.newaxis] | |
v01 = tri_xyz[:, :, 1] - tri_xyz[:, :, 0] | |
v02 = tri_xyz[:, :, 2] - tri_xyz[:, :, 0] | |
normals = th.cross(v01, v02, dim=-1) | |
normals = normals / th.norm(normals, dim=-1, keepdim=True).clamp(min=eps) | |
vt01 = tri_uv[:, :, 1] - tri_uv[:, :, 0] | |
vt02 = tri_uv[:, :, 2] - tri_uv[:, :, 0] | |
f = th.tensor([1.0], device=tri_xyz.device) / ( | |
vt01[..., 0] * vt02[..., 1] - vt01[..., 1] * vt02[..., 0] | |
) | |
tangents = f[..., np.newaxis] * ( | |
v01 * vt02[..., 1][..., np.newaxis] - v02 * vt01[..., 1][..., np.newaxis] | |
) | |
tangents = tangents / th.norm(tangents, dim=-1, keepdim=True).clamp(min=eps) | |
bitangents = th.cross(normals, tangents, dim=-1) | |
bitangents = bitangents / th.norm(bitangents, dim=-1, keepdim=True).clamp(min=eps).clamp( | |
min=eps | |
) | |
return tangents, bitangents, normals | |
class GeometryModule(nn.Module): | |
"""This module encapsulates uv correspondences and vertex images.""" | |
def __init__( | |
self, | |
vi: th.Tensor, | |
vt: th.Tensor, | |
vti: th.Tensor, | |
v2uv: th.Tensor, | |
uv_size: int, | |
flip_uv: bool = False, | |
impaint: bool = False, | |
impaint_threshold: float = 100.0, | |
device=None, | |
) -> None: | |
super().__init__() | |
self.register_buffer("vi", th.as_tensor(vi)) | |
self.register_buffer("vt", th.as_tensor(vt)) | |
self.register_buffer("vti", th.as_tensor(vti)) | |
self.register_buffer("v2uv", th.as_tensor(v2uv)) | |
self.uv_size: int = uv_size | |
index_image = make_uv_vert_index( | |
self.vt, | |
self.vi, | |
self.vti, | |
uv_shape=uv_size, | |
flip_uv=flip_uv, | |
).cpu() | |
face_index, bary_image = make_uv_barys(self.vt, self.vti, uv_shape=uv_size, flip_uv=flip_uv) | |
if impaint: | |
# TODO: have an option to pre-compute this? | |
assert isinstance(uv_size, int) | |
if uv_size >= 1024: | |
logger.info("impainting index image might take a while for sizes >= 1024") | |
index_image, bary_image = index_image_impaint( | |
index_image, bary_image, impaint_threshold | |
) | |
self.register_buffer("index_image", index_image.cpu()) | |
self.register_buffer("bary_image", bary_image.cpu()) | |
self.register_buffer("face_index_image", face_index.cpu()) | |
def render_index_images( | |
self, uv_size: Union[Tuple[int, int], int], flip_uv: bool = False, impaint: bool = False | |
) -> Tuple[th.Tensor, th.Tensor]: | |
index_image = make_uv_vert_index( | |
self.vt, self.vi, self.vti, uv_shape=uv_size, flip_uv=flip_uv | |
) | |
_, bary_image = make_uv_barys(self.vt, self.vti, uv_shape=uv_size, flip_uv=flip_uv) | |
if impaint: | |
index_image, bary_image = index_image_impaint( | |
index_image, | |
bary_image, | |
) | |
return index_image, bary_image | |
def vn(self, verts: th.Tensor) -> th.Tensor: | |
return vert_normals_v2(verts, self.vi[np.newaxis].to(th.long)) | |
def to_uv(self, values: th.Tensor) -> th.Tensor: | |
return values_to_uv(values, self.index_image, self.bary_image) | |
def from_uv(self, values_uv: th.Tensor) -> th.Tensor: | |
# TODO: we need to sample this | |
return sample_uv(values_uv, self.vt, self.v2uv.to(th.long)) | |
def compute_view_cos(verts: th.Tensor, faces: th.Tensor, camera_pos: th.Tensor) -> th.Tensor: | |
vn = F.normalize(vert_normals_v2(verts, faces), dim=-1) | |
v2c = F.normalize(verts - camera_pos[:, np.newaxis], dim=-1) | |
return th.einsum("bnd,bnd->bn", vn, v2c) | |
def interpolate_values_mesh( | |
src_values: th.Tensor, src_faces: th.Tensor, idxs: th.Tensor, weights: th.Tensor | |
) -> th.Tensor: | |
"""Interpolate values on the mesh.""" | |
assert src_faces.dtype == th.long, "index should be torch.long" | |
assert len(src_values.shape) in [2, 3], "supporting [N, F] and [B, N, F] only" | |
if src_values.shape == 2: | |
return (src_values[src_faces[idxs]] * weights[..., np.newaxis]).sum(dim=1) | |
else: # src.verts.shape == 3: | |
return (src_values[:, src_faces[idxs]] * weights[np.newaxis, ..., np.newaxis]).sum(dim=2) | |
def depth_discontuity_mask( | |
depth: th.Tensor, threshold: float = 40.0, kscale: float = 4.0, pool_ksize: int = 3 | |
) -> th.Tensor: | |
device = depth.device | |
with th.no_grad(): | |
# TODO: pass the kernel? | |
kernel = th.as_tensor( | |
[ | |
[[[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]], | |
[[[-1, -2, -1], [0, 0, 0], [1, 2, 1]]], | |
], | |
dtype=th.float32, | |
device=device, | |
) | |
disc_mask = (th.norm(F.conv2d(depth, kernel, bias=None, padding=1), dim=1) > threshold)[ | |
:, np.newaxis | |
] | |
disc_mask = ( | |
F.avg_pool2d(disc_mask.float(), pool_ksize, stride=1, padding=pool_ksize // 2) > 0.0 | |
) | |
return disc_mask | |
def convert_camera_parameters(Rt: th.Tensor, K: th.Tensor) -> Dict[str, th.Tensor]: | |
R = Rt[:, :3, :3] | |
t = -R.permute(0, 2, 1).bmm(Rt[:, :3, 3].unsqueeze(2)).squeeze(2) | |
return { | |
"campos": t, | |
"camrot": R, | |
"focal": K[:, :2, :2], | |
"princpt": K[:, :2, 2], | |
} | |
def closest_point(mesh: Trimesh, points: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: | |
v = mesh.vertices | |
vi = mesh.faces | |
# pyre-ignore | |
dist, face_idxs, p = igl.point_mesh_squared_distance(points, v, vi) | |
return p, dist, face_idxs | |
def closest_point_barycentrics( | |
v: np.ndarray, vi: np.ndarray, points: np.ndarray | |
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: | |
"""Given a 3D mesh and a set of query points, return closest point barycentrics | |
Args: | |
v: np.array (float) | |
[N, 3] mesh vertices | |
vi: np.array (int) | |
[N, 3] mesh triangle indices | |
points: np.array (float) | |
[M, 3] query points | |
Returns: | |
Tuple[approx, barys, interp_idxs, face_idxs] | |
approx: [M, 3] approximated (closest) points on the mesh | |
barys: [M, 3] barycentric weights that produce "approx" | |
interp_idxs: [M, 3] vertex indices for barycentric interpolation | |
face_idxs: [M] face indices for barycentric interpolation. interp_idxs = vi[face_idxs] | |
""" | |
mesh = Trimesh(vertices=v, faces=vi) | |
p, _, face_idxs = closest_point(mesh, points) | |
barys = points_to_barycentric(mesh.triangles[face_idxs], p) | |
b0, b1, b2 = np.split(barys, 3, axis=1) | |
interp_idxs = vi[face_idxs] | |
v0 = v[interp_idxs[:, 0]] | |
v1 = v[interp_idxs[:, 1]] | |
v2 = v[interp_idxs[:, 2]] | |
approx = b0 * v0 + b1 * v1 + b2 * v2 | |
return approx, barys, interp_idxs, face_idxs | |
def make_closest_uv_barys( | |
vt: np.ndarray, | |
vti: np.ndarray, | |
uv_shape: Union[Tuple[int, int], int], | |
flip_uv: bool = True, | |
return_approx_dist: bool = False, | |
) -> Union[Tuple[th.Tensor, th.Tensor], Tuple[th.Tensor, th.Tensor, th.Tensor]]: | |
"""Compute a UV-space barycentric map where each texel contains barycentric | |
coordinates for the closest point on a UV triangle. | |
Args: | |
vt: th.Tensor | |
Texture coordinates. Shape = [n_texcoords, 2] | |
vti: th.Tensor | |
Face texture coordinate indices. Shape = [n_faces, 3] | |
uv_shape: Tuple[int, int] or int | |
Shape of the texture map. (HxW) | |
flip_uv: bool | |
Whether or not to flip UV coordinates along the V axis (OpenGL -> numpy/pytorch convention). | |
return_approx_dist: bool | |
Whether or not to include the distance to the nearest point. | |
Returns: | |
th.Tensor: index_img: Face index image, shape [uv_shape[0], uv_shape[1]] | |
th.Tensor: Barycentric coordinate map, shape [uv_shape[0], uv_shape[1], 3] | |
""" | |
if isinstance(uv_shape, int): | |
uv_shape = (uv_shape, uv_shape) | |
if flip_uv: | |
# Flip here because texture coordinates in some of our topo files are | |
# stored in OpenGL convention with Y=0 on the bottom of the texture | |
# unlike numpy/torch arrays/tensors. | |
vt = vt.clone() | |
vt[:, 1] = 1 - vt[:, 1] | |
# Texel to UV mapping (as per OpenGL linear filtering) | |
# https://www.khronos.org/registry/OpenGL/specs/gl/glspec46.core.pdf | |
# Sect. 8.14, page 261 | |
# uv=(0.5,0.5)/w is at the center of texel [0,0] | |
# uv=(w-0.5, w-0.5)/w is the center of texel [w-1,w-1] | |
# texel = floor(u*w - 0.5) | |
# u = (texel+0.5)/w | |
uv_grid = th.meshgrid( | |
th.linspace(0.5, uv_shape[0] - 1 + 0.5, uv_shape[0]) / uv_shape[0], | |
th.linspace(0.5, uv_shape[1] - 1 + 0.5, uv_shape[1]) / uv_shape[1], | |
) # HxW, v,u | |
uv_grid = th.stack(uv_grid[::-1], dim=2) # HxW, u, v | |
uv = uv_grid.reshape(-1, 2).data.to("cpu").numpy() | |
vth = np.hstack((vt, vt[:, 0:1] * 0 + 1)) | |
uvh = np.hstack((uv, uv[:, 0:1] * 0 + 1)) | |
approx, barys, interp_idxs, face_idxs = closest_point_barycentrics(vth, vti, uvh) | |
index_img = th.from_numpy(face_idxs.reshape(uv_shape[0], uv_shape[1])).long() | |
bary_img = th.from_numpy(barys.reshape(uv_shape[0], uv_shape[1], 3)).float() | |
if return_approx_dist: | |
dist = np.linalg.norm(approx - uvh, axis=1) | |
dist = th.from_numpy(dist.reshape(uv_shape[0], uv_shape[1])).float() | |
return index_img, bary_img, dist | |
else: | |
return index_img, bary_img | |
def compute_tbn( | |
geom: th.Tensor, vt: th.Tensor, vi: th.Tensor, vti: th.Tensor | |
) -> Tuple[th.Tensor, th.Tensor, th.Tensor]: | |
"""Computes tangent, bitangent, and normal vectors given a mesh. | |
Args: | |
geom: [N, n_verts, 3] th.Tensor | |
Vertex positions. | |
vt: [n_uv_coords, 2] th.Tensor | |
UV coordinates. | |
vi: [..., 3] th.Tensor | |
Face vertex indices. | |
vti: [..., 3] th.Tensor | |
Face UV indices. | |
Returns: | |
[..., 3] th.Tensors for T, B, N. | |
""" | |
v0 = geom[:, vi[..., 0]] | |
v1 = geom[:, vi[..., 1]] | |
v2 = geom[:, vi[..., 2]] | |
vt0 = vt[vti[..., 0]] | |
vt1 = vt[vti[..., 1]] | |
vt2 = vt[vti[..., 2]] | |
v01 = v1 - v0 | |
v02 = v2 - v0 | |
vt01 = vt1 - vt0 | |
vt02 = vt2 - vt0 | |
f = th.tensor([1.0], device=geom.device) / ( | |
vt01[None, ..., 0] * vt02[None, ..., 1] - vt01[None, ..., 1] * vt02[None, ..., 0] | |
) | |
tangent = f[..., None] * th.stack( | |
[ | |
v01[..., 0] * vt02[None, ..., 1] - v02[..., 0] * vt01[None, ..., 1], | |
v01[..., 1] * vt02[None, ..., 1] - v02[..., 1] * vt01[None, ..., 1], | |
v01[..., 2] * vt02[None, ..., 1] - v02[..., 2] * vt01[None, ..., 1], | |
], | |
dim=-1, | |
) | |
tangent = F.normalize(tangent, dim=-1) | |
normal = F.normalize(th.cross(v01, v02, dim=3), dim=-1) | |
bitangent = F.normalize(th.cross(tangent, normal, dim=3), dim=-1) | |
return tangent, bitangent, normal | |
def make_postex(v: th.Tensor, idxim: th.Tensor, barim: th.Tensor) -> th.Tensor: | |
return ( | |
barim[None, :, :, 0, None] * v[:, idxim[:, :, 0]] | |
+ barim[None, :, :, 1, None] * v[:, idxim[:, :, 1]] | |
+ barim[None, :, :, 2, None] * v[:, idxim[:, :, 2]] | |
).permute( | |
0, 3, 1, 2 | |
) # B x 3 x H x W | |
def acos_safe_th(x: th.Tensor, eps: float = 1e-4) -> th.Tensor: | |
slope = th.arccos(th.as_tensor(1 - eps)) / th.as_tensor(eps) | |
# TODO: stop doing this allocation once sparse gradients with NaNs (like in | |
# th.where) are handled differently. | |
buf = th.empty_like(x) | |
good = abs(x) <= 1 - eps | |
bad = ~good | |
sign = th.sign(x.data[bad]) | |
buf[good] = th.acos(x[good]) | |
buf[bad] = th.acos(sign * (1 - eps)) - slope * sign * (abs(x[bad]) - 1 + eps) | |
return buf | |
def invRodrigues(R: th.Tensor, eps: float = 1e-8) -> th.Tensor: | |
"""Computes the Rodrigues vectors r from the rotation matrices `R`""" | |
# t = trace(R) | |
# theta = rotational angle | |
# [omega]_x = (R-R^T)/2 | |
# r = theta/sin(theta)*omega | |
assert R.shape[-2:] == (3, 3) | |
t = R[..., 0, 0] + R[..., 1, 1] + R[..., 2, 2] | |
theta = acos_safe_th((t - 1) / 2) | |
omega = ( | |
th.stack( | |
( | |
R[..., 2, 1] - R[..., 1, 2], | |
R[..., 0, 2] - R[..., 2, 0], | |
R[..., 1, 0] - R[..., 0, 1], | |
), | |
-1, | |
) | |
/ 2 | |
) | |
# Edge Case 1: t >= 3 - eps | |
inv_sinc = theta / th.sin(theta) | |
inv_sinc_taylor_expansion = ( | |
1 | |
+ (1.0 / 6.0) * th.pow(theta, 2) | |
+ (7.0 / 360.0) * th.pow(theta, 4) | |
+ (31.0 / 15120.0) * th.pow(theta, 6) | |
) | |
# Edge Case 2: t <= -1 + eps | |
# From: https://math.stackexchange.com/questions/83874/efficient-and-accurate-numerical | |
# -implementation-of-the-inverse-rodrigues-rotatio | |
a = th.diagonal(R, 0, -2, -1).argmax(dim=-1) | |
b = (a + 1) % 3 | |
c = (a + 2) % 3 | |
s = th.sqrt(R[..., a, a] - R[..., b, b] - R[..., c, c] + 1 + 1e-4) | |
v = th.zeros_like(omega) | |
v[..., a] = s / 2 | |
v[..., b] = (R[..., b, a] + R[..., a, b]) / (2 * s) | |
v[..., c] = (R[..., c, a] + R[..., a, c]) / (2 * s) | |
norm = th.norm(v, dim=-1, keepdim=True).to(v.dtype).clamp(min=eps) | |
pi_vnorm = np.pi * (v / norm) | |
# use taylor expansion when R is close to a identity matrix (trace(R) ~= 3) | |
r = th.where( | |
t[:, None] > (3 - 1e-3), | |
inv_sinc_taylor_expansion[..., None] * omega, | |
th.where(t[:, None] < -1 + 1e-3, pi_vnorm, inv_sinc[..., None] * omega), | |
) | |
return r | |
def EulerXYZ_to_matrix(xyz: th.Tensor) -> th.Tensor: | |
# R = Rz(φ)Ry(θ)Rx(ψ) = [ | |
# cos θ cos φ sin ψ sin θ cos φ − cos ψ sin φ cos ψ sin θ cos φ + sin ψ sin φ | |
# cos θ sin φ sin ψ sin θ sin φ + cos ψ cos φ cos ψ sin θ sin φ − sin ψ cos φ | |
# − sin θ sin ψ cos θ cos ψ cos θ | |
# ] | |
( | |
x, | |
y, | |
z, | |
) = ( | |
xyz[..., 0:1], | |
xyz[..., 1:2], | |
xyz[..., 2:3], | |
) | |
sinx, cosx = th.sin(x), th.cos(x) | |
siny, cosy = th.sin(y), th.cos(y) | |
sinz, cosz = th.sin(z), th.cos(z) | |
r1 = th.cat( | |
( | |
cosy * cosz, | |
sinx * siny * cosz | |
- cosx * sinz, # th.sin(x) * th.sin(y) * th.cos(z) - th.cos(x) * th.sin(z), | |
cosx * siny * cosz | |
+ sinx * sinz, # th.cos(x) * th.sin(y) * th.cos(z) + th.sin(x) * th.sin(z) | |
), | |
-1, | |
) # [..., 3] | |
r3 = th.cat( | |
( | |
-siny, # -th.sin(y), | |
sinx * cosy, # th.sin(x) * th.cos(y), | |
cosx * cosy, # th.cos(x) * th.cos(y) | |
), | |
-1, | |
) # [..., 3] | |
r2 = th.cross(r3, r1, dim=-1) | |
R = th.cat((r1.unsqueeze(-2), r2.unsqueeze(-2), r3.unsqueeze(-2)), -2) | |
return R | |
def axisangle_to_matrix(rvec: th.Tensor) -> th.Tensor: | |
theta = th.sqrt(1e-5 + th.sum(th.pow(rvec, 2), dim=-1)) | |
rvec = rvec / theta[..., None] | |
costh = th.cos(theta) | |
sinth = th.sin(theta) | |
return th.stack( | |
( | |
th.stack( | |
( | |
th.pow(rvec[..., 0], 2) + (1.0 - th.pow(rvec[..., 0], 2)) * costh, | |
rvec[..., 0] * rvec[..., 1] * (1.0 - costh) - rvec[..., 2] * sinth, | |
rvec[..., 0] * rvec[..., 2] * (1.0 - costh) + rvec[..., 1] * sinth, | |
), | |
dim=-1, | |
), | |
th.stack( | |
( | |
rvec[..., 0] * rvec[..., 1] * (1.0 - costh) + rvec[..., 2] * sinth, | |
th.pow(rvec[..., 1], 2) + (1.0 - th.pow(rvec[..., 1], 2)) * costh, | |
rvec[..., 1] * rvec[..., 2] * (1.0 - costh) - rvec[..., 0] * sinth, | |
), | |
dim=-1, | |
), | |
th.stack( | |
( | |
rvec[..., 0] * rvec[..., 2] * (1.0 - costh) - rvec[..., 1] * sinth, | |
rvec[..., 1] * rvec[..., 2] * (1.0 - costh) + rvec[..., 0] * sinth, | |
th.pow(rvec[..., 2], 2) + (1.0 - th.pow(rvec[..., 2], 2)) * costh, | |
), | |
dim=-1, | |
), | |
), | |
dim=-2, | |
) | |
def compute_view_cond_tbnrefl( | |
geom: th.Tensor, campos: th.Tensor, geo_fn: GeometryModule | |
) -> th.Tensor: | |
B = int(geom.shape[0]) | |
S = geo_fn.uv_size | |
device = geom.device | |
# TODO: this can be pre-computed, or we can assume no invalid pixels? | |
mask = (geo_fn.index_image != -1).any(dim=-1) | |
idxs = geo_fn.index_image[mask] | |
tri_uv = geo_fn.vt[geo_fn.v2uv[idxs, 0].to(th.long)] | |
tri_xyz = geom[:, idxs] | |
t, b, n = compute_tbn_uv(tri_xyz, tri_uv) | |
tbn_rot = th.stack((t, -b, n), dim=-2) | |
tbn_rot_uv = th.zeros( | |
(B, S, S, 3, 3), | |
dtype=th.float32, | |
device=device, | |
) | |
tbn_rot_uv[:, mask] = tbn_rot | |
view = F.normalize(campos[:, np.newaxis] - geom, dim=-1) | |
v_uv = geo_fn.to_uv(values=view) | |
tbn_uv = th.einsum("bhwij,bjhw->bihw", tbn_rot_uv, v_uv) | |
# reflectance vector | |
n_uv = th.zeros((B, 3, S, S), dtype=th.float32, device=device) | |
n_uv[..., mask] = n.permute(0, 2, 1) | |
n_dot_v = (v_uv * n_uv).sum(dim=1, keepdim=True) | |
r_uv = 2.0 * n_uv * n_dot_v - v_uv | |
return th.cat([tbn_uv, r_uv], dim=1) | |
def get_barys_for_uvs( | |
topology: Dict[str, Any], uv_correspondences: np.ndarray | |
) -> Tuple[np.ndarray, np.ndarray]: | |
""" | |
Given a topology along with uv correspondences for the topology (eg. keypoints correspondences in uv space), | |
this function will produce a tuple with the bary coordinates for each uv correspondece along with the vertex index. | |
Parameters: | |
---------- | |
topology: Input mesh that contains vertices, faces and texture coordinates info. | |
uv_correspondences: N X 2 uv locations that describe the uv correspondence to the topology | |
Returns: | |
------- | |
bary: (N X 3 float) | |
For each uv correspondence returns the bary corrdinates for the uv pixel | |
triangles: (N X 3 int) | |
For each uv correspondence returns the face (i.e vertices of the faces) for that pixel. | |
""" | |
vi: np.ndarray = topology["vi"] | |
vt: np.ndarray = topology["vt"] | |
vti: np.ndarray = topology["vti"] | |
# # No up-down flip here | |
# Here we pad the texture cordinates and correspondences with a 0 | |
vth = np.hstack((vt[:, :2], vt[:, :1] * 0)) | |
kp_uv_h = np.hstack((uv_correspondences, uv_correspondences[:, :1] * 0)) | |
_, kp_barys, _, face_indices = closest_point_barycentrics(vth, vti, kp_uv_h) | |
kp_verts = vi[face_indices] | |
return kp_barys, kp_verts | |