| ''' |
| ----------------------------------------------------------------------------- |
| 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 |
| |
| self.build_model(cfg_model, cfg_data) |
| |
| 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): |
| |
| 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): |
| |
| 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() |
| |
| output = self.render_image(data["pose"], data["intr"], image_size=self.image_size_val, |
| stratified=False, sample_idx=data["idx"]) |
| |
| rot = data["pose"][..., :3, :3] |
| normal_cam = -output["gradient"] @ rot.transpose(-1, -2) |
| output.update( |
| rgb_map=self.to_full_val_image(output["rgb"]), |
| opacity_map=self.to_full_val_image(output["opacity"]), |
| depth_map=self.to_full_val_image(output["depth"]), |
| normal_map=self.to_full_val_image(normal_cam), |
| ) |
| 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) |
| 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"]) |
| 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()) |
| |
| 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) |
| center = nerf_util.slice_by_ray_idx(center, ray_idx) |
| ray = nerf_util.slice_by_ray_idx(ray, ray_idx) |
| ray_unit = torch_F.normalize(ray, dim=-1) |
| 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) |
| |
| rgbs = torch.cat([output_object["rgbs"], output_background["rgbs"]], dim=2) |
| dists = torch.cat([output_object["dists"], output_background["dists"]], dim=2) |
| alphas = torch.cat([output_object["alphas"], output_background["alphas"]], dim=2) |
| else: |
| rgbs = output_object["rgbs"] |
| dists = output_object["dists"] |
| alphas = output_object["alphas"] |
| weights = render.alpha_compositing_weights(alphas) |
| |
| rgb = render.composite(rgbs, weights) |
| if self.white_background: |
| opacity_all = render.composite(1., weights) |
| rgb = rgb + (1 - opacity_all) |
| |
| output = dict( |
| rgb=rgb, |
| opacity=output_object["opacity"], |
| outside=outside, |
| dists=dists, |
| weights=weights, |
| gradient=output_object["gradient"], |
| gradients=output_object["gradients"], |
| hessians=output_object["hessians"], |
| ) |
| 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) |
| points = camera.get_3D_points_from_dist(center, ray_unit, dists) |
| sdfs, feats = self.neural_sdf.forward(points) |
| sdfs[outside[..., None].expand_as(sdfs)] = self.outside_val |
| |
| rays_unit = ray_unit[..., None, :].expand_as(points).contiguous() |
| gradients, hessians = self.neural_sdf.compute_gradients(points, compute_hessian=self.training, sdf=sdfs) |
| normals = torch_F.normalize(gradients, dim=-1) |
| rgbs = self.neural_rgb.forward(points, normals, rays_unit, feats, app=app) |
| |
| alphas = self.compute_neus_alphas(ray_unit, sdfs, gradients, dists, dist_far=far[..., None], |
| progress=self.progress) |
| if not self.training: |
| weights = render.alpha_compositing_weights(alphas) |
| opacity = render.composite(1., weights) |
| gradient = render.composite(gradients, weights) |
| else: |
| opacity = None |
| gradient = None |
| |
| output = dict( |
| rgbs=rgbs, |
| sdfs=sdfs[..., 0], |
| dists=dists, |
| alphas=alphas, |
| opacity=opacity, |
| gradient=gradient, |
| gradients=gradients, |
| hessians=hessians, |
| ) |
| 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) |
| rays_unit = ray_unit[..., None, :].expand_as(points) |
| rgbs, densities = self.background_nerf.forward(points, rays_unit, app_outside) |
| alphas = render.volume_rendering_alphas_dist(densities, dists) |
| |
| output = dict( |
| rgbs=rgbs, |
| dists=dists, |
| alphas=alphas, |
| ) |
| 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_() |
| outside = dist_near.isnan() |
| dist_near[outside], dist_far[outside] = 1, 1.2 |
| return dist_near, dist_far, outside |
|
|
| def get_appearance_embedding(self, sample_idx, num_rays): |
| if self.with_appear_embed: |
| |
| 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] |
| app = app.expand(-1, num_rays, num_samples_all, -1) |
| |
| if self.with_background: |
| app_outside = self.appear_embed_outside(sample_idx)[:, None, None] |
| app_outside = app_outside.expand(-1, num_rays, self.cfg_render.num_samples.background, -1) |
| 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) |
| sdfs = self.neural_sdf.sdf(points) |
| for h in range(self.cfg_render.num_sample_hierarchy): |
| dists_fine = self.sample_dists_hierarchical(dists, sdfs, inv_s=(64 * 2 ** h)) |
| dists = torch.cat([dists, dists_fine], dim=2) |
| 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) |
| sdfs_fine = self.neural_sdf.sdf(points_fine) |
| sdfs = torch.cat([sdfs, sdfs_fine], dim=2) |
| sdfs = sdfs.gather(dim=2, index=sort_idx.expand_as(sdfs)) |
| return dists |
|
|
| def sample_dists_hierarchical(self, dists, sdfs, inv_s, robust=True, eps=1e-5): |
| sdfs = sdfs[..., 0] |
| prev_sdfs, next_sdfs = sdfs[..., :-1], sdfs[..., 1:] |
| prev_dists, next_dists = dists[..., :-1, 0], dists[..., 1:, 0] |
| mid_sdfs = (prev_sdfs + next_sdfs) * 0.5 |
| cos_val = (next_sdfs - prev_sdfs) / (next_dists - prev_dists + 1e-5) |
| if robust: |
| prev_cos_val = torch.cat([torch.zeros_like(cos_val)[..., :1], cos_val[..., :-1]], dim=-1) |
| cos_val = torch.stack([prev_cos_val, cos_val], dim=-1).min(dim=-1).values |
| dist_intvs = dists[..., 1:, 0] - dists[..., :-1, 0] |
| est_prev_sdf = mid_sdfs - cos_val * dist_intvs * 0.5 |
| est_next_sdf = mid_sdfs + cos_val * dist_intvs * 0.5 |
| prev_cdf = (est_prev_sdf * inv_s).sigmoid() |
| next_cdf = (est_next_sdf * inv_s).sigmoid() |
| alphas = ((prev_cdf - next_cdf) / (prev_cdf + eps)).clip_(0.0, 1.0) |
| weights = render.alpha_compositing_weights(alphas) |
| dists_fine = self.sample_dists_from_pdf(dists, weights=weights[..., 0]) |
| 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) |
| return dists |
|
|
| def compute_neus_alphas(self, ray_unit, sdfs, gradients, dists, dist_far=None, progress=1., eps=1e-5): |
| sdfs = sdfs[..., 0] |
| |
| inv_s = self.s_var.exp() |
| true_cos = (ray_unit[..., None, :] * gradients).sum(dim=-1, keepdim=False) |
| iter_cos = self._get_iter_cos(true_cos, progress=progress) |
| |
| if dist_far is None: |
| dist_far = torch.empty_like(dists[..., :1, :]).fill_(1e10) |
| dists = torch.cat([dists, dist_far], dim=2) |
| dist_intvs = dists[..., 1:, 0] - dists[..., :-1, 0] |
| est_prev_sdf = sdfs - iter_cos * dist_intvs * 0.5 |
| est_next_sdf = sdfs + iter_cos * dist_intvs * 0.5 |
| prev_cdf = (est_prev_sdf * inv_s).sigmoid() |
| next_cdf = (est_next_sdf * inv_s).sigmoid() |
| alphas = ((prev_cdf - next_cdf) / (prev_cdf + eps)).clip_(0.0, 1.0) |
| |
| return alphas |
|
|
| def _get_iter_cos(self, true_cos, progress=1.): |
| anneal_ratio = min(progress / self.anneal_end, 1.) |
| |
| return -((-true_cos * 0.5 + 0.5).relu() * (1.0 - anneal_ratio) + |
| (-true_cos).relu() * anneal_ratio) |
|
|