asmadeyi's picture
Upload 72 files
c29de8d
Raw
History Blame Contribute Delete
16.5 kB
'''
-----------------------------------------------------------------------------
Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved.
NVIDIA CORPORATION and its licensors retain all intellectual property
and proprietary rights in and to this software, related documentation
and any modifications thereto. Any use, reproduction, disclosure or
distribution of this software and related documentation without an express
license agreement from NVIDIA CORPORATION is strictly prohibited.
-----------------------------------------------------------------------------
'''
from functools import partial
import torch
import torch.nn.functional as torch_F
from collections import defaultdict
from imaginaire.models.base import Model as BaseModel
from projects.nerf.utils import nerf_util, camera, render
from projects.neuralangelo.utils import misc
from projects.neuralangelo.utils.modules import NeuralSDF, NeuralRGB, BackgroundNeRF
class Model(BaseModel):
def __init__(self, cfg_model, cfg_data):
super().__init__(cfg_model, cfg_data)
self.cfg_render = cfg_model.render
self.white_background = cfg_model.background.white
self.with_background = cfg_model.background.enabled
self.with_appear_embed = cfg_model.appear_embed.enabled
self.anneal_end = cfg_model.object.s_var.anneal_end
self.outside_val = 1000. * (-1 if cfg_model.object.sdf.mlp.inside_out else 1)
self.image_size_train = cfg_data.train.image_size
self.image_size_val = cfg_data.val.image_size
# Define models.
self.build_model(cfg_model, cfg_data)
# Define functions.
self.ray_generator = partial(nerf_util.ray_generator,
camera_ndc=False,
num_rays=cfg_model.render.rand_rays)
self.sample_dists_from_pdf = partial(nerf_util.sample_dists_from_pdf,
intvs_fine=cfg_model.render.num_samples.fine)
self.to_full_val_image = partial(misc.to_full_image, image_size=cfg_data.val.image_size)
def build_model(self, cfg_model, cfg_data):
# appearance encoding
if cfg_model.appear_embed.enabled:
assert cfg_data.num_images is not None
self.appear_embed = torch.nn.Embedding(cfg_data.num_images, cfg_model.appear_embed.dim)
if cfg_model.background.enabled:
self.appear_embed_outside = torch.nn.Embedding(cfg_data.num_images, cfg_model.appear_embed.dim)
else:
self.appear_embed_outside = None
else:
self.appear_embed = self.appear_embed_outside = None
self.neural_sdf = NeuralSDF(cfg_model.object.sdf)
self.neural_rgb = NeuralRGB(cfg_model.object.rgb, feat_dim=cfg_model.object.sdf.mlp.hidden_dim,
appear_embed=cfg_model.appear_embed)
if cfg_model.background.enabled:
self.background_nerf = BackgroundNeRF(cfg_model.background, appear_embed=cfg_model.appear_embed)
else:
self.background_nerf = None
self.s_var = torch.nn.Parameter(torch.tensor(cfg_model.object.s_var.init_val, dtype=torch.float32))
def forward(self, data):
# Randomly sample and render the pixels.
output = self.render_pixels(data["pose"], data["intr"], image_size=self.image_size_train,
stratified=self.cfg_render.stratified, sample_idx=data["idx"],
ray_idx=data["ray_idx"])
return output
@torch.no_grad()
def inference(self, data):
self.eval()
# Render the full images.
output = self.render_image(data["pose"], data["intr"], image_size=self.image_size_val,
stratified=False, sample_idx=data["idx"]) # [B,N,C]
# Get full rendered RGB and depth images.
rot = data["pose"][..., :3, :3] # [B,3,3]
normal_cam = -output["gradient"] @ rot.transpose(-1, -2) # [B,HW,3]
output.update(
rgb_map=self.to_full_val_image(output["rgb"]), # [B,3,H,W]
opacity_map=self.to_full_val_image(output["opacity"]), # [B,1,H,W]
depth_map=self.to_full_val_image(output["depth"]), # [B,1,H,W]
normal_map=self.to_full_val_image(normal_cam), # [B,3,H,W]
)
return output
def render_image(self, pose, intr, image_size, stratified=False, sample_idx=None):
""" Render the rays given the camera intrinsics and poses.
Args:
pose (tensor [batch,3,4]): Camera poses ([R,t]).
intr (tensor [batch,3,3]): Camera intrinsics.
stratified (bool): Whether to stratify the depth sampling.
sample_idx (tensor [batch]): Data sample index.
Returns:
output: A dictionary containing the outputs.
"""
output = defaultdict(list)
for center, ray, _ in self.ray_generator(pose, intr, image_size, full_image=True):
ray_unit = torch_F.normalize(ray, dim=-1) # [B,R,3]
output_batch = self.render_rays(center, ray_unit, sample_idx=sample_idx, stratified=stratified)
if not self.training:
dist = render.composite(output_batch["dists"], output_batch["weights"]) # [B,R,1]
depth = dist / ray.norm(dim=-1, keepdim=True)
output_batch.update(depth=depth)
for key, value in output_batch.items():
if value is not None:
output[key].append(value.detach())
# Concat each item (list) in output into one tensor. Concatenate along the ray dimension (1)
for key, value in output.items():
output[key] = torch.cat(value, dim=1)
return output
def render_pixels(self, pose, intr, image_size, stratified=False, sample_idx=None, ray_idx=None):
center, ray = camera.get_center_and_ray(pose, intr, image_size) # [B,HW,3]
center = nerf_util.slice_by_ray_idx(center, ray_idx) # [B,R,3]
ray = nerf_util.slice_by_ray_idx(ray, ray_idx) # [B,R,3]
ray_unit = torch_F.normalize(ray, dim=-1) # [B,R,3]
output = self.render_rays(center, ray_unit, sample_idx=sample_idx, stratified=stratified)
return output
def render_rays(self, center, ray_unit, sample_idx=None, stratified=False):
with torch.no_grad():
near, far, outside = self.get_dist_bounds(center, ray_unit)
app, app_outside = self.get_appearance_embedding(sample_idx, ray_unit.shape[1])
output_object = self.render_rays_object(center, ray_unit, near, far, outside, app, stratified=stratified)
if self.with_background:
output_background = self.render_rays_background(center, ray_unit, far, app_outside, stratified=stratified)
# Concatenate object and background samples.
rgbs = torch.cat([output_object["rgbs"], output_background["rgbs"]], dim=2) # [B,R,No+Nb,3]
dists = torch.cat([output_object["dists"], output_background["dists"]], dim=2) # [B,R,No+Nb,1]
alphas = torch.cat([output_object["alphas"], output_background["alphas"]], dim=2) # [B,R,No+Nb]
else:
rgbs = output_object["rgbs"] # [B,R,No,3]
dists = output_object["dists"] # [B,R,No,1]
alphas = output_object["alphas"] # [B,R,No]
weights = render.alpha_compositing_weights(alphas) # [B,R,No+Nb,1]
# Compute weights and composite samples.
rgb = render.composite(rgbs, weights) # [B,R,3]
if self.white_background:
opacity_all = render.composite(1., weights) # [B,R,1]
rgb = rgb + (1 - opacity_all)
# Collect output.
output = dict(
rgb=rgb, # [B,R,3]
opacity=output_object["opacity"], # [B,R,1]/None
outside=outside, # [B,R,1]
dists=dists, # [B,R,No+Nb,1]
weights=weights, # [B,R,No+Nb,1]
gradient=output_object["gradient"], # [B,R,3]/None
gradients=output_object["gradients"], # [B,R,No,3]
hessians=output_object["hessians"], # [B,R,No,3]/None
)
return output
def render_rays_object(self, center, ray_unit, near, far, outside, app, stratified=False):
with torch.no_grad():
dists = self.sample_dists_all(center, ray_unit, near, far, stratified=stratified) # [B,R,N,3]
points = camera.get_3D_points_from_dist(center, ray_unit, dists) # [B,R,N,3]
sdfs, feats = self.neural_sdf.forward(points) # [B,R,N,1],[B,R,N,K]
sdfs[outside[..., None].expand_as(sdfs)] = self.outside_val
# Compute 1st- and 2nd-order gradients.
rays_unit = ray_unit[..., None, :].expand_as(points).contiguous() # [B,R,N,3]
gradients, hessians = self.neural_sdf.compute_gradients(points, compute_hessian=self.training, sdf=sdfs)
normals = torch_F.normalize(gradients, dim=-1) # [B,R,N,3]
rgbs = self.neural_rgb.forward(points, normals, rays_unit, feats, app=app) # [B,R,N,3]
# SDF volume rendering.
alphas = self.compute_neus_alphas(ray_unit, sdfs, gradients, dists, dist_far=far[..., None],
progress=self.progress) # [B,R,N]
if not self.training:
weights = render.alpha_compositing_weights(alphas) # [B,R,N,1]
opacity = render.composite(1., weights) # [B,R,1]
gradient = render.composite(gradients, weights) # [B,R,3]
else:
opacity = None
gradient = None
# Collect output.
output = dict(
rgbs=rgbs, # [B,R,N,3]
sdfs=sdfs[..., 0], # [B,R,N]
dists=dists, # [B,R,N,1]
alphas=alphas, # [B,R,N]
opacity=opacity, # [B,R,3]/None
gradient=gradient, # [B,R,3]/None
gradients=gradients, # [B,R,N,3]
hessians=hessians, # [B,R,N,3]/None
)
return output
def render_rays_background(self, center, ray_unit, far, app_outside, stratified=False):
with torch.no_grad():
dists = self.sample_dists_background(ray_unit, far, stratified=stratified)
points = camera.get_3D_points_from_dist(center, ray_unit, dists) # [B,R,N,3]
rays_unit = ray_unit[..., None, :].expand_as(points) # [B,R,N,3]
rgbs, densities = self.background_nerf.forward(points, rays_unit, app_outside) # [B,R,N,3]
alphas = render.volume_rendering_alphas_dist(densities, dists) # [B,R,N]
# Collect output.
output = dict(
rgbs=rgbs, # [B,R,3]
dists=dists, # [B,R,N,1]
alphas=alphas, # [B,R,N]
)
return output
@torch.no_grad()
def get_dist_bounds(self, center, ray_unit):
dist_near, dist_far = nerf_util.intersect_with_sphere(center, ray_unit, radius=1.)
dist_near.relu_() # Distance (and thus depth) should be non-negative.
outside = dist_near.isnan()
dist_near[outside], dist_far[outside] = 1, 1.2 # Dummy distances. Density will be set to 0.
return dist_near, dist_far, outside
def get_appearance_embedding(self, sample_idx, num_rays):
if self.with_appear_embed:
# Object appearance embedding.
num_samples_all = self.cfg_render.num_samples.coarse + \
self.cfg_render.num_samples.fine * self.cfg_render.num_sample_hierarchy
app = self.appear_embed(sample_idx)[:, None, None] # [B,1,1,C]
app = app.expand(-1, num_rays, num_samples_all, -1) # [B,R,N,C]
# Background appearance embedding.
if self.with_background:
app_outside = self.appear_embed_outside(sample_idx)[:, None, None] # [B,1,1,C]
app_outside = app_outside.expand(-1, num_rays, self.cfg_render.num_samples.background, -1) # [B,R,N,C]
else:
app_outside = None
else:
app = app_outside = None
return app, app_outside
@torch.no_grad()
def sample_dists_all(self, center, ray_unit, near, far, stratified=False):
dists = nerf_util.sample_dists(ray_unit.shape[:2], dist_range=(near[..., None], far[..., None]),
intvs=self.cfg_render.num_samples.coarse, stratified=stratified)
if self.cfg_render.num_sample_hierarchy > 0:
points = camera.get_3D_points_from_dist(center, ray_unit, dists) # [B,R,N,3]
sdfs = self.neural_sdf.sdf(points) # [B,R,N]
for h in range(self.cfg_render.num_sample_hierarchy):
dists_fine = self.sample_dists_hierarchical(dists, sdfs, inv_s=(64 * 2 ** h)) # [B,R,Nf,1]
dists = torch.cat([dists, dists_fine], dim=2) # [B,R,N+Nf,1]
dists, sort_idx = dists.sort(dim=2)
if h != self.cfg_render.num_sample_hierarchy - 1:
points_fine = camera.get_3D_points_from_dist(center, ray_unit, dists_fine) # [B,R,Nf,3]
sdfs_fine = self.neural_sdf.sdf(points_fine) # [B,R,Nf]
sdfs = torch.cat([sdfs, sdfs_fine], dim=2) # [B,R,N+Nf]
sdfs = sdfs.gather(dim=2, index=sort_idx.expand_as(sdfs)) # [B,R,N+Nf,1]
return dists
def sample_dists_hierarchical(self, dists, sdfs, inv_s, robust=True, eps=1e-5):
sdfs = sdfs[..., 0] # [B,R,N]
prev_sdfs, next_sdfs = sdfs[..., :-1], sdfs[..., 1:] # [B,R,N-1]
prev_dists, next_dists = dists[..., :-1, 0], dists[..., 1:, 0] # [B,R,N-1]
mid_sdfs = (prev_sdfs + next_sdfs) * 0.5 # [B,R,N-1]
cos_val = (next_sdfs - prev_sdfs) / (next_dists - prev_dists + 1e-5) # [B,R,N-1]
if robust:
prev_cos_val = torch.cat([torch.zeros_like(cos_val)[..., :1], cos_val[..., :-1]], dim=-1) # [B,R,N-1]
cos_val = torch.stack([prev_cos_val, cos_val], dim=-1).min(dim=-1).values # [B,R,N-1]
dist_intvs = dists[..., 1:, 0] - dists[..., :-1, 0] # [B,R,N-1]
est_prev_sdf = mid_sdfs - cos_val * dist_intvs * 0.5 # [B,R,N-1]
est_next_sdf = mid_sdfs + cos_val * dist_intvs * 0.5 # [B,R,N-1]
prev_cdf = (est_prev_sdf * inv_s).sigmoid() # [B,R,N-1]
next_cdf = (est_next_sdf * inv_s).sigmoid() # [B,R,N-1]
alphas = ((prev_cdf - next_cdf) / (prev_cdf + eps)).clip_(0.0, 1.0) # [B,R,N-1]
weights = render.alpha_compositing_weights(alphas) # [B,R,N-1,1]
dists_fine = self.sample_dists_from_pdf(dists, weights=weights[..., 0]) # [B,R,Nf,1]
return dists_fine
def sample_dists_background(self, ray_unit, far, stratified=False, eps=1e-5):
inv_dists = nerf_util.sample_dists(ray_unit.shape[:2], dist_range=(1, 0),
intvs=self.cfg_render.num_samples.background, stratified=stratified)
dists = far[..., None] / (inv_dists + eps) # [B,R,N,1]
return dists
def compute_neus_alphas(self, ray_unit, sdfs, gradients, dists, dist_far=None, progress=1., eps=1e-5):
sdfs = sdfs[..., 0] # [B,R,N]
# SDF volume rendering in NeuS.
inv_s = self.s_var.exp()
true_cos = (ray_unit[..., None, :] * gradients).sum(dim=-1, keepdim=False) # [B,R,N]
iter_cos = self._get_iter_cos(true_cos, progress=progress) # [B,R,N]
# Estimate signed distances at section points
if dist_far is None:
dist_far = torch.empty_like(dists[..., :1, :]).fill_(1e10) # [B,R,1,1]
dists = torch.cat([dists, dist_far], dim=2) # [B,R,N+1,1]
dist_intvs = dists[..., 1:, 0] - dists[..., :-1, 0] # [B,R,N]
est_prev_sdf = sdfs - iter_cos * dist_intvs * 0.5 # [B,R,N]
est_next_sdf = sdfs + iter_cos * dist_intvs * 0.5 # [B,R,N]
prev_cdf = (est_prev_sdf * inv_s).sigmoid() # [B,R,N]
next_cdf = (est_next_sdf * inv_s).sigmoid() # [B,R,N]
alphas = ((prev_cdf - next_cdf) / (prev_cdf + eps)).clip_(0.0, 1.0) # [B,R,N]
# weights = render.alpha_compositing_weights(alphas) # [B,R,N,1]
return alphas
def _get_iter_cos(self, true_cos, progress=1.):
anneal_ratio = min(progress / self.anneal_end, 1.)
# The anneal strategy below keeps the cos value alive at the beginning of training iterations.
return -((-true_cos * 0.5 + 0.5).relu() * (1.0 - anneal_ratio) +
(-true_cos).relu() * anneal_ratio) # always non-positive