whyun13's picture
Upload folder using huggingface_hub
882f6e2 verified
"""
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