裸眼3D–原理介绍

裸眼3D基本上都是针对双目视差来说的。

什么是双目视差:人有两只眼睛,它们之间大约相隔65mm。当我们观看一个物体,两眼视轴辐合在这个物体上时,物体的映像将落在两眼网膜的对应点上。这时如果将两眼网膜重叠起来,它们的视像应该重合在一起,即看到单一、清晰的物体。根据这一事实,当两眼辐合到空间中的一点时,我们可以确定一个假想的平面,这个平面上的所有各点都将刺激两眼网膜的对应区域。这个表面就叫做视觉单像区(horopter)。它可以定义为在一定的辐合条件下,在视网膜对应区域的成像空间中所有各点的轨迹。位于视觉单像区的物体,都将落在视网膜对应点而形成单个的映像。

如果两眼成像的网膜部位相差太大,那么人们看到的将是双像,即把同一个物体看成两个。例如,我们用右手举起一支铅笔,让它和远处墙角的直线平行。这时如果我们注视远处墙角的直线,那么近处的铅笔就将出现双像;如果我们注视近处的铅笔,远处的墙角直线就将出现双像。

正因为双目视差,才会让我们看到的物体有纵深感和空间感。

裸眼3D是怎么做到蒙骗双眼来营造空间和纵深感呢,现在的3D视频或者图像都是通过区分左右眼来拍摄的两幅图,视差距约为65mm,通过让你左眼看到左眼的图像,右眼看到右眼的图像就可以让你的大脑合成一副有纵深感的立体画面。

人的两只眼睛相距约6cm,就像两部相距6cm放置的照相机,拍出的照片角度会有一点点不同(侈开)。

这种侈开在大脑里就可以融合成立体的感觉。

我们再抽丝剥茧制作一个最简单的侈开立体图:

越简单的图越容易说明原理,但观看起来越消耗眼睛“内功”。请您用原理一的透视方法,让左眼看左图,右眼看右图,当您能看到三个双圈的时候,中间那个小圆就会凸出纸面呈现立体感

计算机图形学:变换矩阵

最近在研究3D建模和新视点合成,在渲染过程中需要选取新视点去合成新图。一般在接口处需要传递一个变换矩阵,用于控制视点的变化。

成像的过程实质上是几个坐标系的转换。首先空间中的一点由世界坐标系转换到 摄像机坐标系 ,然后再将其投影到成像平面 ( 图像物理坐标系 ) ,最后再将成像平面上的数据转换到图像平面 (图像像素坐标系 ) 。

以AdaMPI的代码为例:

# 定义新视角的角度和渲染的帧数
def gen_swing_path(num_frames=90, r_x=0.14, r_y=0., r_z=0.10):
    "Return a list of matrix [4, 4]"
    t = torch.arange(num_frames) / (num_frames - 1)
    poses = torch.eye(4).repeat(num_frames, 1, 1)
    poses[:, 0, 3] = r_x * torch.sin(2. * math.pi * t)
    poses[:, 1, 3] = r_y * torch.cos(2. * math.pi * t)
    poses[:, 2, 3] = r_z * (torch.cos(2. * math.pi * t) - 1.)
    return poses.unbind()

以Synsin代码为例:

# Parameters for the transformation
theta = -0.15
phi = -0.1
tx = 0
ty = 0
tz = 0.1

RT = torch.eye(4).unsqueeze(0)
# Set up rotation(旋转参数)
RT[0,0:3,0:3] = torch.Tensor(quaternion.as_rotation_matrix(quaternion.from_rotation_vector([phi, theta, 0])))
# Set up translation(平移参数)
RT[0,0:3,3] = torch.Tensor([tx, ty, tz])

一开始其实没有明白为什么需要对 r_x=0.14, r_y=0., r_z=0.10 进行处理,处理成4*4的矩阵形式,而不是直接使用,后来查阅资料发现应该是涉及到计算机图形学的变换矩阵的范畴。

计算机图形学中3D的变换是基于转换矩阵( 仿射空间 )进行的。那么为什么是4维的矩阵而不是3维:用第四维度标识向量 or 点。

模型的变换可以认为是空间中一堆点的变换,三维空间中,(x,y,z)可以认为是点,也可以认为是一个向量,因此,人们引入的第4个维度来标识是点还是向量,这个4维空间就叫 仿射空间,,在仿射空间中,(x,y,z,0)标识向量,而(x,y,z,1)表示点。

在图形学中,在做平移,旋转和缩放时,经常会用到矩阵,有缩放矩阵、平移矩阵和旋转矩阵。在三维空间中,变换矩阵都是一个四维矩阵,每一行分别表示x, y, z, w。

1. 缩放矩阵(scale)

上面的公式,左边的第一个操作数(四维矩阵)就是一个缩放矩阵,s1表示x轴的缩放倍数,s2表示y轴的缩放倍数,s3表示z轴的缩放倍数。第二个操作数表示空间中(x, y, z)点, w分量在缩放矩阵中没有用到,我们将其设为1。由右边的结果,可以看出(x, y, z)点经过缩放矩阵变换后,x、y、z分量都各自缩放了s(s1、s2、s3)倍。需要注意的是矩阵的乘法不具有交换律,这里点是用一维列矩阵表示的,作为矩阵乘法的右操作数。如果将其转换到乘法的左边,那么点应该用一维行矩阵表示:

缩放矩阵比较简单,不需要证明,只需要会简单的乘法,就可以看出x,y,z经过缩放矩阵的变换确实被缩放了。

2.平移矩阵(translation)

平移矩阵也称位移矩阵,平移矩阵用到了第四列(w列),这也是为什么三维空间变换矩阵为什么是四维的。平移矩阵也比较容易理解,因为可以通过结果看出想x 、y、z确实各自平移了T步。

3. 旋转矩阵

旋转矩阵,相对难一些,也不是那么容易理解,我们先看最基础的绕x、y、z轴旋转的旋转矩阵。

沿x轴:

沿y轴:

沿z轴:

引入了三角函数,我们无法从结果看出旋转矩阵是否正确,所以我们需要证明。下面我给出沿z轴旋转的变换矩阵证明过程,其他轴同理可证。

image-20210521112417466

假设有如图的点p1,因为绕z轴旋转,点的z值是不变的,我们将其设为0,这样可以将其模拟成二维平面xy中旋转。假设p1绕原点旋转b角度,初始角度为a。整个证明过程如下:

// 经过旋转后向量的长度不变为L(原点到p1和p2的距离相同)
// 由三角函数得到sin(a + b), cos(a + b)的值
cos(a + b) = x2 / L;
sin(a + b) = y2 / L;

// 展开sin(a + b)和cos(a + b)
cos(a) * cos(b) - sin(a) * sin(b) = x2 / L;
sin(a) * cos(b) + cos(a) * sin(b) = y2 / L;

// 用x和y表示cos(a)和sin(a)
x / L * cos(b) - y / L * sin(b) = x2 / L;
y / L * cos(b) + x / L * sin(b) = y2 / L;

// 等式两边同时乘以L
x * cos(b) - y * sin(b) = x2;
y * cos(b) + x * sin(b) = y2;

将x2和y2的结果与上面z轴旋转矩阵结果比较,发现是完全一样的。

按照上面的方法同理可证绕x轴旋转和绕z轴旋转的矩阵。

那么绕任意轴旋转的矩阵呢?learnOpengl_cn官网直接给出了绕任意轴旋转的矩阵,(Rx, Ry, Rz)表示任意轴,θ表示旋转的矩阵。这个矩阵证明比较复杂。

PyTorch3D:面向3D计算机视觉的PyTorch工具箱

PyTorch3D通过PyTorch为3D计算机视觉研究提供高效,可重复使用的组件。目前已基于此开发了:Mesh R-CNN、SynSin等模型。

Facebook开源了一个专门用于3D模型学习的库pytorch3d,说白了就是将3d中一些常用的操作封装起来了。那这个玩意到底有啥用呢?使用这个库又能完成什么事情呢?个人觉得这个库还是蛮有用的,它将一些常用的3D概念整理在一起,并且通过使用这个库可以完成一些基于3D的创作,对于学习入门3D的视觉生成、渲染、甚至是3d的目标检测、3维的姿态评估都大有裨益。

Pytorch3D_上手学习3D的AI模型

Accelerating 3D Deep Learning with PyTorch3D

文档:Welcome to PyTorch3D’s documentation!
项目链接:facebookresearch/pytorch3d
论文:https://arxiv.org/abs/2007.08501

PyTorch3D

主要功能包括:

  • 用于存储和操作 triangle meshes的数据结构
  • 在 triangle meshes上的有效操作(投影变换,图卷积,采样,损失函数)
  • 可微的mesh渲染器

PyTorch3D旨在与深度学习方法稳定集成,以预测和处理3D数据。 因此,PyTorch3D中的所有运算符:

  • 使用PyTorch张量实现
  • 可以处理小批量的异构数据
  • 可以differentiated
  • 可以利用GPU进行加速

深度学习已大大改善了2D图像识别。扩展到3D可能会推动许多新应用的发展,包括自动驾驶汽车,虚拟现实和增强现实,创作3D内容,甚至改善2D识别。然而,尽管兴趣日益浓厚,但3D深度学习仍相对未得到充分开发。我们认为,这种差异是由于3D深度学习所涉及的工程挑战所致,例如有效处理异构数据和将图形操作重构为可微的。

我们通过引入PyTorch3D来应对这些挑战,PyTorch3D是一个用于3D深度学习的模块化,高效且可微的运算符库。它包括一个用于网格和点云的快速,模块化,可微的渲染器,支持按合成进行分析的方法。

与其他可微的渲染器相比,PyTorch3D更具模块化和效率,允许用户更轻松地扩展它,同时还可以优雅地缩放到较大的网格和图像。我们将PyTorch3D运算符和渲染器与其他实现进行了比较,并展示了显著的速度和内存改进。我们还使用PyTorch3D改进了ShapeNet上2D图像的无监督3D网格和点云预测的最新技术。

PyTorch3D是开源的,我们希望它将有助于加速3D深度学习的研究。

实验结果

D-NeRF—针对动态场景的NeRF

主页:https://www.albertpumarola.com/research/D-NeRF/index.html

https://github.com/albertpumarola/D-NeRF

D-NeRF: Neural Radiance Fields for Dynamic Scenes https://arxiv.org/abs/2011.13961

Abstract


NeRF只能重建静态场景,本文提出的方法可以把神经辐射场扩展到动态领域,可以在单相机围绕场景旋转一周的情况下重建物体的刚性和非刚性运动。由此把时间作为附加维度加到输入中,同时把学习过程分为两个阶段:第一个把场景编码到基准空间,另一个在特定时间把这种基准表达形式map到变形场景中。两个map都用fcn学习,训练完后可以通过控制相机视角和时间变量达到物体运动的效果。

Introduction

和已有的4d方式不同点有:

  • 只需单相机
  • 无需预先算好一个三维重建
  • 可以端到端

想做的就是在原始nerf五维输入上加个时间t,完成

到density和radiance的输出。不过如果直接加,时间冗余没有很好的利用,效果并不好。所以分两个模块:

而且可以产生完整三维mesh,捕捉时变几何。

Pipeline

Formulation

Method


Canonical network(规范网络)

希望找到一种场景的表示能把所有图的相关点的信息都汇聚起来。网络 Ψx 用来编码基准空间中的密度和颜色,给一个坐标x,通过fcn输出256维向量再和d concatenate起来输出density和color


Deformation network(变形网络)

Ψt 训练用来估计某一具体时刻的场景和基准空间中场景的变形场(deformation field),给一个坐标x,输出是这个点和基准空间中的这个点的位移。并且把基准空间场景设为t=0

也用了位置编码,x十维,d,t都是四维

Volume rendering

带时间的渲染公式为:

p(ℎ,t) 表示canonical space中由x(h)变换来的点所以其实渲染的时候还是在canonical space里边进行渲染的离散化形式。

Experiment


可以出mesh,depth:

还可以合成阴影:

相比其他方法:

SynSin:单张图片端到端新视点重建

整体思路:图片-网络-深度图->(直接转换)点云-网络-新视点(CVPR2020)

SynSin: End-to-end View Synthesis from a Single Image

项目主页: https://www.robots.ox.ac.uk/~ow/synsin.html

来自牛津大学、FAIR、Facebook 和密歇根大学的研究者提出了一种单一图像视图合成方法,允许从单一输入图像生成新的场景视图。它被训练在真实的图像上,没有使用任何真实的 3D 信息;引入了一种新的可微点云渲染器,用于将潜在的 3D 点云特征转换为目标视图;细化网络对投影特征进行解码,插入缺失区域,生成逼真的输出图像;生成模型内部的 3D 组件允许在测试时对潜在特征空间进行可解释的操作,例如,可以从单个图像动画轨迹。与以前的工作不同,SynSin 可以生成高分辨率的图像,并推广到其他输入分辨率,在 Matterport、Replica 和 RealEstate10K 数据集上超越基线和前期工作。

整体网络:

首先将图片输入特征和深度网络得到特征map和深度图,接着通过相机参数变换为带特征的点云,接着根据相对变换矩阵T,将带特征的点云渲染到二维像素位置上,接着通过一个GAN生成最终的新视角图片。

该论文的渲染方式值得关注,其并不是简单的使用zbuffer和固定投影像素位置渲染的,在z深度和平面广度上都是软映射的。平面广度上是线性递减的辐射,在深度上取k近邻并排序后,按照次序递减:

这种软渲染的方式估计会牺牲较大的渲染精度,但是对于优化问题来说非常合适不过。

总结:构建了一个先进的端到端模型SynSin,它可以获取单个RGB图像,然后从不同的角度生成同一场景的新图像,无需任何3D监督。我们系统主要是预测一个3D点云,后者通过PyTorch3D使用我们的可微渲染器投射到新的视图上,并且将渲染的点云传递到生成对抗网络(GAN)来合成输出图像。当前的方法通常是使用密集体素网格,它们在单个对象的合成场景中显示出优秀的应用前景,但无法扩展到复杂的真实场景。利用点云的灵活性,SynSin不仅能够实现这一点,而且比体素网格等替代方法更有效地推广到各种分辨率。SynSin的高效率可以帮助我们探索广泛的应用,如生成更好的3D照片和360度视频。

CLIP-NeRF:文本和图像驱动的NeRF编辑框架

论文:(CVPR 2022) CLIP-NeRF: Text-and-Image Driven Manipulation of Neural Radiance Fields

项目主页:https://cassiepython.github.io/clipnerf/

Overview

  • 提出了第一个统一文本和图像驱动的NeRF编辑框架,使得用户可以使用文本提示或示例图像对3D内容进行灵活编辑。
  • Zs 和Za,分别控制形状和外观。
  • 提出feed forward mapper,能够实现快速推理出用户输入对物体形状和外观的改变量。
  • 提出了一种反演方法,利用EM算法从真实的图像中推断出相机位姿、Zs 和 Za,进而实现编辑现有物体的形状、外观、姿态。

Network architecture

  • GRAF不同,该网络并不是将 Zs 直接与positional encoding拼接起来送入神经辐射场,而是先用deformation network(受到Nerfies启发)生成原始位置编码的偏移量并与原始位置编码相加,再送入后续辐射场中。优点:使用tanh函数限制deformation network输出的偏移量的值域,提升了shape操控的鲁棒性与稳定性,同时使得对shape的操纵对于appearance无影响(传统conditional NeRF,如GRAF,实际上改变 Zs会对appearance产生一些影响)。
  • 先预训练好一个解耦条件NeRF,使得NeRF能够充分学习到场景的3D信息以及生成出真实的场景物体。
  • 然后利用CLIP distance训练CLIP分支中的shape mapper and appearance mapper(固定其他模块的参数)。使得mapper能够正确学习到如何将用户输入的modify 信息映射为 Zs, Za 的改变量以使得NeRF正确生成目标结果

Inverse Manipulation

为了获得某物体对应的latent code以便对其实施编辑,作者根据EM算法( Expectation Maximization Algorithm。期望最大算法)设计了一种迭代方法来交替优化 、Zs、Za 和相机位姿 v ,本质是优化各项参数,使得在该组参数下得到的生成结果接近于实际结果。

其中Zn是扰动,用于提升优化过程的鲁棒性, 入n 从1decay到0,表示越往后参数已经优化得差不多了,那么扰动也就相应地减小。

当获得某物体对应的 Zs,Za,v 后,输入文本便可以输出编辑后的物体。

Experiment results

  • Text-Driven
  • Exemplar-Driven Editing Results
  • Convert real image into corresponding latent code and camera pose, then use prompt to edit real image

实现编辑应该就是先反演出目标图像对应的各个latent codes,然后向CLIP encoder输入参考图像/文字,再通过shape/appearancce mapper得到相应的编辑改变量,将改变量和原始推算出的latent codes相加,再利用NeRF前向渲染出最终编辑后的图像。

Limitations

  • 无法进行细粒度的物体修改,比如修改车轮为红色。根源在于隐空间和预训练CLIP的固有局限性,比如CLIP就没有学到轮胎的语义信息。
  • 局限于使用文本和示例图像对于单个物体进行修改,没有扩展到复杂场景(如object-nerf处理的现实场景)。如何实现多物体场景的text/img guided modify?结合object-nerf和clip-nerf
  • 先训练好NeRF,再训练mapping network,那就限定了模型只能用参考文字/参考图像编辑固定场景中的物体,而且通过模型结构不难推测出,该模型迁移到多物体数据集上是不可行的。在多物体场景下,由于文本只能影响全局的 Za,Zs ,因此编辑会影响场景中的所有物体。

PixelNeRF–具有泛化性的NeRF

pixelNeRF: Neural Radiance Fields from One or Few Images

代码链接:https://github.com/sxyu/pixel-nerf

论文链接:https://alexyu.net/pixelnerf/

Nerf提出以来,收到了大量关注。但是,它仍存在以下缺点:

  1. Nerf的训练需要很多标准化的照片
  2. 训练花费大量时间

因此,本文提出了一种基于全卷积的Nerf:pixelNerf。当经过大量训练后,pixelNerf可以仅通过几张(甚至一张)照片进行良好的视图合成,同时这种方式也不需要精确的3D监督(2D监督即可)。

作者提出了pixelNeRF,一个只需要输入单张或多张图像,就能得到连续场景表示的学习框架。由于现存的构建神经辐射场的方法涉及到独立优化每个场景的表示,这需要许多校准的视图和大量的计算时间,因此作者引入了一种新的网络架构。实验结果表明,在所有情况下,pixelNeRF在新视图合成和单图像三维重建方面都优于当前最先进的工作。

该项目主要研究的问题是如何从一个稀疏的输入视图集中合成这个场景的新视图,在可微神经渲染出现之前,这个长期存在的问题一直没有得到进展。同时,最近的神经渲染场NeRF通过编码体积密度和颜色,在特定场景的新视图合成方面表现出很好的效果。虽然NeRF可以渲染非常逼真的新视图,但它通常是不切实际的,因为它需要大量的位姿图像和冗长的场景优化。

在这篇文章中,作者对上述方法进行了改进,与NeRF网络不使用任何图像特征不同的是,pixelNeRF将与每个像素对齐的空间图像特征作为输入。这种图像调节允许框架在一组多视图图像上进行训练,学习场景先验,然后从一个或几个输入图像中合成视图,如下图所示。

PixelNeRF具有很多特点:首先,Pixel可以在多视图图像的数据集上面进行训练,而不需要任何额外的监督;其次,PixelNeRF预测输入图像的摄像机坐标系中的NeRF表示,而不是标准坐标系,这是泛化看不见的场景和物体类别的必要条件,因为在有多个物体的场景中,不存在明确的规范坐标系;第三,它是完全卷积的,这允许它保持图像和输出3D表示之间的空间对齐;最后,PixelNeRF可以在测试时合并任意数量的输入视图,且不需要任何优化。

相关工作

新视图合成:这是一个长期存在的问题,它需要从一组输入视图中构建一个场景的新视图。尽管现在有很多工作都已经取得了逼真的效果,但是存在比较多的问题,例如需要密集的视图和大量的优化时间。其他方法通过学习跨场景共享的先验知识,从单个或少数输入视图进行新的视图合成,但是这些方法使用2.5D表示,因此它们能够合成的摄像机运动范围是有限的。在这项工作中,作者提出了PixelNeRF,能够直接从相当大的基线中合成新视图。

基于学习的三维重建:得益于深度学习的发展,单视图或多视图的三维重建也得到快速的发展。问题是,很多表示方法都需要3D模型进行监督,尽管多视图监督限制更小、更容易获取,其中的很多方法也需要物体的mask。相比之下,PixelNeRF可以单独从图像中训练,允许它应用到含有两个对象的场景而不需要修改。

以观察者为中心的三维重建:对于3D学习任务,可以在以观察者为中心的坐标系(即视图空间)或以对象为中心的坐标系(即规范空间)中进行预测。大多数现存的方法都是在规范空间中进行预测,虽然这使得学习空间规律更加容易,但是会降低不可见对象和具有多个对象场景的预测性能。PixelNeRF在视图空间中操作,这在【2】中已经被证明可以更好地重建看不见的对象类别,并且不鼓励对训练集的记忆。下表是PixelNeRF和其他方法的对比:

NeRF的缺点:

虽然NeRF实现了最新的视图合成,但它是一种基于优化的方法,每个场景必须单独优化,场景之间没有知识共享。这种方法不仅耗时,而且在单个或极稀疏视图的限制下,无法利用任何先验知识来加速重建或完成形状。

基于图像的NeRF:pixelNeRF

为了克服上面提到的关于NeRF的问题,作者提出了一种基于空间图像特征的NeRF结构。该模型由两个部分组成:一个完全卷积的图像编码器E (将输入图像编码为像素对齐的特征网格)和一个NeRF网络f (给定一个空间位置及其对应的编码特征,输出颜色和密度)。

单视图pixelNeRF:首先固定坐标系为输入图像的视图空间,并在这个坐标系中指定位置和摄像机光线。给定场景的输入图像I,首先提取出它的特征量W=E(I)。然后,对于相机光线上的一个点x,通过使用已知的内参,将x投影到图像坐标π(x)上,然后在像素特征之间进行双线性插值来提取相应的图像特征向量W(π(x))。最后把图像特征连同位置和视图方向(都在输入视图坐标系统中)传递到NeRF网络:其中γ()是x上的位置编码。

合并多个视图:多个视图提供了有关场景的附加信息,并解决了单视图固有的三维几何歧义。作者扩展了该模型,不同于现有的在测试时只使用单个输入视图的方法,它允许在测试时有任意数量的视图。

在有多个输入视图的情况下,只假设相对的相机姿态是已知的,为了便于解释,可以为场景任意固定一个世界坐标系。把输入图像记为I,其相关联的摄像机记为P=[R t]。对于新的目标摄影机光线,将视图方向为d的点x转换到每个输入视图i的坐标系,转换如下:

为了获得输出的密度和颜色,作者独立地处理每个视图坐标帧中的坐标和相应的特征,并在NeRF网络中聚合视图。将NeRF网络的初始层表示为f1,它分别处理每个输入视图空间中的输入,并将最终层表示为f2,它处理聚合视图。

和单视图类似,作者将每个输入图像编码成特征体积W(i)=E(I(i))。对于点x(i),在投影图像坐标π(x(i))处从特征体W(i)中提取相应的图像特征,然后将这些输入传递到f1,以获得中间向量:

最后用平均池化算子ψ将中间向量V(i)聚合并传递到最后一层f2,得到预测的密度和颜色:

个人觉得,这篇文章其实就是对原始Nerf做了一个效果很好的改进:将图片进行特征提取后再输入Nerf,(似乎并没有很多的创新),但是从图中展现的效果来看,这一改进是卓有成效的,或许也为我们提供了一种新的思路。

目前的缺陷:

1) Like NeRF, our rendering time is slow, and in fact, our runtime increases linearly when given more input views. Further, some methods (e.g. [28, 21]) can recover a mesh from the image enabling fast rendering and manipulation afterwards, while NeRF based representations cannot be converted to meshes very reliably. Improving NeRF’s efficiency is an important re-search question that can enable real-time applications.

2) As in the vanilla NeRF, we manually tune ray sampling bounds tn,tf and a scale for the positional encoding. Making NeRF-related methods scale-invariant is a crucial challenge.
3) While we have demonstrated our method on real data from the DTU dataset, we acknowledge that this dataset was captured under controlled settings and has matching camera poses across all scenes with limited viewpoints. Ultimately,our approach is bottlenecked by the availability of largescale wide baseline multi-view datasets, limiting the applicability to datasets such as ShapeNet and DTU. Learning
a general prior for 360◦ scenes in-the-wild is an exciting direction for future work

参考文献:

【1】Ben Mildenhall, Pratul P. Srinivasan, Matthew Tancik,Jonathan T. Barron, Ravi Ramamoorthi, and Ren Ng. Nerf: Representing scenes as neural radiance fields for view synthesis. In Eur. Conf. Comput. Vis., 2020

【2】Daeyun Shin, Charless Fowlkes, and Derek Hoiem. Pixels, voxels, and views: A study of shape representations for single view 3d object shape prediction. In IEEE Conf. Comput.Vis. Pattern Recog., 2018.

IBRNet: Learning Multi-View Image-Based Rendering(用NeRF做可泛化视角插值)

IBRNet: Learning Multi-View Image-Based Rendering

主页:https://ibrnet.github.io/

代码链接:https://github.com/googleinterns/IBRNet

NeRF存在的一大问题就是仅仅只能表示一个场景,因此这篇文章就学提出了一个框架可以同时学习多个场景,且可以扩展到没有学习过的场景(提高泛化性)。实验表明,在寻求泛化到新scenes时,我们的方法比其它好。更进一步,如果fine-tuned每一个scene,可以实现和目前SOTA的NVS任务相当的表现。

本文与NeRF最大的不同是输入的数据不仅仅有目标视角,还有对应的所有同一场景的多视角图片,因此理论上的确是可以直接端到端的应用于新场景的。原始NeRF针对每个scene都需要优化(重头训练),而本文方法学习一个通用的view插值函数能够泛化到新的scenes。

overview_image

模型流程:

1. 将同一场景的多视角图片一同输入网络(个数不限),然后使用一个U-Net来抽取每张图片(source view)的特征,特征包括图像颜色,相机参数,图像表征(这里可以就理解成NeRF中的向辐射场发射光线,然后保存对应的光线参数和图片特征)。

2. 之后将每张图片的特征并行的输入一个transformer,用于预测一个共同的颜色和密度。之所以是共同的颜色和密度是因为这多个视角输入的特征我们默认是同一个点在不同视角的特征,因此结果就是用于预测我们目标视角(target view)里此点的结果。

3. 用体渲染的方式将结果渲染出来,之后通过像素的重构损失来优化网络

4. 换一个场景,重复1~3

备注:如果一直用同一个场景训练的话,理论上效果肯定会更好,也就是论文中提到的finetune的情况。

个人理解:本质上这里的模型学习的是“如何插值”,而不是构建一个辐射场,因此可能对于较为稀疏的情形或复杂场景表现的没那么好。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

MINE–利用单张图片做三维重建

端到端类型

用MPI(Multi-Plane Image )代替NeRF的RGBσ作为网络的输出

来自字节跳动视觉技术团队的研究者将 NeRF 和 Multiplane Image(MPI)结合,提出了一种新的三维空间表达方式 MINE。该方法通过对单张图片做三维重建,实现新视角合成和深度估算。

开源了训练代码(基于LLFF数据集的toy example),paper里面数据集的pretrained models,并提供了demo代码:

相关工作

近年来,在新视角合成这个领域里,最火爆的方法无疑是 ECCV 2020 的 NeRF [5]。与传统的一些手工设计的显式三维表达(Light Fields,LDI,MPI 等)不同,NeRF 把整个三维空间的几何信息与 texture 信息全部用一个 MLP 的权重来表达,输入任意一个空间坐标以及观察角度,MLP 会预测一个 RGB 值和 volume density。目标图片的渲染通过 ray tracing 和 volume rendering 的方式来完成。尽管 NeRF 的效果非常惊艳,但它的缺点也非常明显:

  1. 一个模型只能表达一个场景,且优化一个场景耗时久;
  2. per-pixel 渲染较为低效;
  3. 泛化能力较差,一个场景需要较多的照片才能训练好。

另外一个与该研究较相关的是 MPI(Multiplane Image)[1, 2, 3]。MPI 包含了多个平面的 RGB-alpha 图片,其中每个平面表达场景在某个深度中的内容,它的主要缺点在于深度是固定及离散的,这个缺点限制了它对三维空间的表达能力。[1, 2, 3] 都能方便地泛化到不同的场景,然而 MPI 各个平面的深度是固定且离散的,这个缺点严重限制了它的效果。

结合了NeRF和Multiplane Image(MPI),提出了一种新的三维空间表达方式MINE。MINE利用了NeRF的思路,将MPI扩展成了连续深度的形式。输入单张RGB图片,我们的方法会对source相机的视锥(frustum)做稠密的三维重建,同时对被遮挡的部分做inpainting,预测出相机视锥的三维表达。利用这个三维表达,给出target相机相对于source相机的在三维空间中的相对位置和角度变化(rotation and translation),我们可以方便且高效地渲染出在目标相机视图下的RGB图片以及深度图。

MINE在KITTI,RealEstate10K以及Flowers Light Fields数据集上,生成质量大幅超过了当前单视图合成的state-of-the-art。同时,在深度估计benchmark iBims-1和NYU-v2上,虽然我们在训练中只使用了RGB图片和sparse深度监督,MINE在单目深度估计任务上取得了非常接近全监督state-of-the-art的performance,并大幅超越了其他弱监督的方法。

Introduction and Related Works

视图合成(novel view synthesis)需要解决的问题是:在一个场景(scene)下,输入一个或多个图片,它们各自的相机内参和外参(source camera pose),之后对于任意的相机位置和角度(target camera pose),我们想要生成场景在该相机视图下的RGB图片。要解决这个问题,我们的模型需要学会场景的几何结构,同时对被遮挡的部分做inpainting。学术界设计了很多利用learning的方法预测场景的3D/2.5D表达,其中跟我们较相关的是MPI(Multiplane Image)[1, 2, 3]。MPI包含了多个平面的 RGB-α图片,其中每个平面表达场景在某个深度中的内容,它的主要缺点在于深度是固定及离散的,这个缺点限制了它对三维空间的表达能力。

近年来,这个领域的当红炸子鸡无疑是ECCV 2020的NeRF [5]。与传统的一些手工设计的显式三维表达(Light Fields,LDI,MPI等)不同,NeRF把整个三维空间的几何信息与texture信息全部用一个MLP的权重来表达,输入任意一个空间坐标以及观察角度,MLP会预测一个RGB值和volume density。目标图片的渲染通过ray tracing和volume rendering的方式来完成。尽管NeRF的效果非常惊艳,但它的缺点也非常明显:1. 一个模型只能表达一个场景,且优化一个场景耗时久;2. per-pixel渲染较为低效;3. 泛化能力较差,一个场景需要较多的照片才能训练好。

方法综述

该团队采用一个 encoder-decoder 的结构来生成三维表达:

  1. Encoder 是一个全卷积网络,输入为单个 RGB 图片,输出为 feature maps;
  2. Decoder 也是一个全卷积网络,输入为 encoder 输出的 feature map,以及任意深度值(repeat + concat),输出该深度下的 RGB-sigma 图片;
  3. 最终的三维表达由多个平面组成,也就是说在一次完整的 forward 中,encoder 需要 inference 一次,而 decoder 需要 inference N 次获得个 N 平面。

获得三维表达后,不再需要任何的网络 inference,渲染任意 target 相机 pose 下的视角只需要两步:

  1. 利用 homography wrapping 建立像素点间的 correspondence。可以想象,从 target 相机射出一条光线,这条光线与 target 图片的一个像素点相交,然后,研究者延长这条射线,让它与 source 相机视锥的各个平面相交。相交点的 RGB-sigma 值可以通过 bilinear sampling 获得;
  2. 利用 volume rendering 将光线上的点渲染到目标图片像素点上,获得该像素点的 RGB 值与深度。

三维表达与渲染

1. Planar Neural Radiance Field

2. Rendering Process

完成这两步之后,我们就可以通过上面volume rendering的公式渲染任意target camera下的视图了。需要注意的是,在获得3D表达后,渲染任意target camera pose下的视图都只需要这两个步骤,无需再做额外的网络inference

Scale 校正

MINE 可以利用 structure-from-motion 计算的相机参数与点云进行场景的学习,在这种情况下,深度是 ambiguous 的。由于在这个方法中,深度采样的范围是固定的。所以需要计算一个 scale factor,使网络预测的 scale 与 structure-from-motion 的 scale 进行对齐。团队利用通过 Structure from Motion 获得的每个图片的可见 3D 点 P 以及网络预测的深度图 Z 计算 scale factor:

获得 scale factor 后,对相机的位移进行 scale:

需要注意的是,由于需要和 ground truth 比较,所以在训练和测试时需要做 scale calibration。而在部署时不需要做这一步。

端到端的训练

MINE 可以仅通过 RGB 图片学习到场景的三维几何信息,训练 Loss 主要由两部分组成:

1.Reconsturction loss——计算渲染出的 target 图片与 ground truth 的差异:

2.Edge-aware smoothness loss——确保在图片颜色没有突变的地方,深度也不会突变,这里主要参考了 monodepth2 [6] 种的实现:

3.Sparse disparity loss——在训练集各场景的 scale 不一样时,利用 structure-from-motion 获得的稀疏点云辅助场景几何信息的学习:

MINE 与 MPI、NeRF 的比较

MINE 是 MPI 的一种连续深度的扩展,相比于 MPI 和 NeRF,MINE 有几个明显的优势:

  1. 与 NeRF 相比,MINE 能够泛化到训练集没有出现过的场景;
  2. 与 NeRF 的逐点渲染相比,MINE 的渲染非常高效;
  3. 与 MPI 相比,MINE 的深度是连续的,能稠密地表示相机的视锥;
  4. MPI 通过 alpha 合成(alpha compositing)进行渲染,但该方法与射线上点之间的距离无关,而 MINE 利用 volume rendering 解决了这个限制。

然而,MINE 也有一些自身的局限性:

  1. 由于输入是单张图片,MINE 无法表达相机视锥以外的三维空间;
  2. 由于 MINE 的输入里没有观察角度,所以其无法对一些复杂的 view-dependent 效果(如光盘上的彩虹等)进行建模。

[1]. Tinghui Zhou, Richard Tucker, John Flynn, Graham Fyffe, Noah Snavely. Stereo Magnification: Learning View Synthesis using Multiplane Images. (SIGGRAPH 2018)

[2]. Ben Mildenhall, Pratul P. Srinivasan, Rodrigo Ortiz-Cayon, Nima Khademi Kalantari, Ravi Ramamoorthi, Ren Ng, Abhishek Kar. Local Light Field Fusion: Practical View Synthesis with Prescriptive Sampling Guidelines. (SIGGRAPH 2019)

[3]. Richard Tucker, Noah Snavely. Single-View View Synthesis with Multiplane Images. (CVPR 2020)

神经辐射场(NeRF)-代码解析

参考:Dasuda​ and Liwen.site

参考代码:Nerf-pl: https://github.com/kwea123/nerf_pl

位置编码

NeRF 的输入是一个五维向量: (物体)空间点的位置x=(x,y,z) 和 (相机)观测方向d=(θ,ϕ)。NeRF 使用了位置编码(positional encoding)把一维的位置坐标,转换为高维的表征。例如 p∈RL, 通过函数γ(⋅) 映射到R2L 空间中,这里L 指的是编码的数量,对于位置坐标,L=10;对于观测角度,L=4。

代码实现

 # 类的定义
class Embedding(nn.Module):
    def __init__(self, in_channels, N_freqs, logscale=True):
        """
        Defines a function that embeds x to (x, sin(2^k x), cos(2^k x), ...)
        in_channels: number of input channels (3 for both xyz and direction)
        """
        super(Embedding, self).__init__()
        self.N_freqs = N_freqs
        self.in_channels = in_channels
        self.funcs = [torch.sin, torch.cos]
        self.out_channels = in_channels*(len(self.funcs)*N_freqs+1)
 
        if logscale:
            self.freq_bands = 2**torch.linspace(0, N_freqs-1, N_freqs)
        else:
            self.freq_bands = torch.linspace(1, 2**(N_freqs-1), N_freqs)
 
    def forward(self, x):
        """
        Embeds x to (x, sin(2^k x), cos(2^k x), ...) 
        Different from the paper, "x" is also in the output
        See https://github.com/bmild/nerf/issues/12
 
        Inputs:
            x: (B, self.in_channels)
 
        Outputs:
            out: (B, self.out_channels)
        """
        out = [x]
        for freq in self.freq_bands:
            for func in self.funcs:
                out += [func(freq*x)]
 
        return torch.cat(out, -1)
 
# 使用
 
class NeRFSystem(LightningModule):
    def __init__(self, hparams):
        ...
        self.embedding_xyz = Embedding(3, 10) # 10 is the default number
        self.embedding_dir = Embedding(3, 4) # 4 is the default number
        self.embeddings = [self.embedding_xyz, self.embedding_dir]
        ...   

解释

  • 对于位置坐标 (x,y,z), 每一个值都使用 10 个 sin 和 10 个cos 频率进行拓展。例如 Embeds x to (x, sin (2^k x), cos (2^k x), …) 。再连接一个本身。因此每一个值都拓展为 10+10+1=21维。对于位置坐标的三个值,总共有 3×21=63 维。
  • 对于相机角度 (θ,ϕ),也是类似,使用 4 个sin 和 4 个 cos 频率进行拓展。这里输入保留了一位,实际输入是(θ,ϕ,1)。再连接一个本身。因此每一个值都拓展为4+4+1=9 维。对于相机角度的三个值,总共有 3×9=27 维。

NeRF 网络

NeRF 网络默认是一个多层的 MLP。中间第四层有 skip connection,构成了一个 ResNet 的结构。网络的宽度默认为 256。

输入

  1. 位置坐标的表征(in_channels_xyz):63d

输出

  1. 体密度σ:1d
  2. RGB 色彩值C: 3d

网络结构
FC 指的是带 ReLU 的全连接层。Linear 层指的是单纯的线性方程。

quicker_40ae8453-64cb-4645-8a27-e62001a85aa2.png

代码实现

class NeRF(nn.Module):
    def __init__(self,
                 D=8, W=256,
                 in_channels_xyz=63, in_channels_dir=27, 
                 skips=[4]):
        """
        D: number of layers for density (sigma) encoder
        W: number of hidden units in each layer
        in_channels_xyz: number of input channels for xyz (3+3*10*2=63 by default)
        in_channels_dir: number of input channels for direction (3+3*4*2=27 by default)
        skips: add skip connection in the Dth layer
        """
        super(NeRF, self).__init__()
        self.D = D
        self.W = W
        self.in_channels_xyz = in_channels_xyz
        self.in_channels_dir = in_channels_dir
        self.skips = skips
 
        # xyz encoding layers
        for i in range(D):
            if i == 0:
                layer = nn.Linear(in_channels_xyz, W)
            elif i in skips:
                layer = nn.Linear(W+in_channels_xyz, W)
            else:
                layer = nn.Linear(W, W)
            layer = nn.Sequential(layer, nn.ReLU(True))
            setattr(self, f"xyz_encoding_{i+1}", layer)
        self.xyz_encoding_final = nn.Linear(W, W)
 
        # direction encoding layers
        self.dir_encoding = nn.Sequential(
                                nn.Linear(W+in_channels_dir, W//2),
                                nn.ReLU(True))
 
        # output layers
        self.sigma = nn.Linear(W, 1)
        self.rgb = nn.Sequential(
                        nn.Linear(W//2, 3),
                        nn.Sigmoid())
 
    def forward(self, x, sigma_only=False):
        """
        Encodes input (xyz+dir) to rgb+sigma (not ready to render yet).
        For rendering this ray, please see rendering.py
 
        Inputs:
            x: (B, self.in_channels_xyz(+self.in_channels_dir))
               the embedded vector of position and direction
            sigma_only: whether to infer sigma only. If True,
                        x is of shape (B, self.in_channels_xyz)
 
        Outputs:
            if sigma_ony:
                sigma: (B, 1) sigma
            else:
                out: (B, 4), rgb and sigma
        """
        if not sigma_only:
            input_xyz, input_dir = \
                torch.split(x, [self.in_channels_xyz, self.in_channels_dir], dim=-1)
        else:
            input_xyz = x
 
        xyz_ = input_xyz
        for i in range(self.D):
            if i in self.skips:
                xyz_ = torch.cat([input_xyz, xyz_], -1)
            xyz_ = getattr(self, f"xyz_encoding_{i+1}")(xyz_)
 
        sigma = self.sigma(xyz_)
        if sigma_only:
            return sigma
 
        xyz_encoding_final = self.xyz_encoding_final(xyz_)
 
        dir_encoding_input = torch.cat([xyz_encoding_final, input_dir], -1)
        dir_encoding = self.dir_encoding(dir_encoding_input)
        rgb = self.rgb(dir_encoding)
 
        out = torch.cat([rgb, sigma], -1)
 
        return out

体素渲染

假设我们已经得到了一束光线上所有的位置对应的色彩和体密度。我们需要对这束光线进行后处理(体素渲染),得到最终在图片上的像素值。

# z_vals: (N_rays, N_samples_) depths of the sampled positions
# noise_std: factor to perturb the model's prediction of sigma(提升模型鲁棒性??)
 
# Convert these values using volume rendering (Section 4)
deltas = z_vals[:, 1:] - z_vals[:, :-1] # (N_rays, N_samples_-1)
delta_inf = 1e10 * torch.ones_like(deltas[:, :1]) # (N_rays, 1) the last delta is infinity
deltas = torch.cat([deltas, delta_inf], -1)  # (N_rays, N_samples_)
 
# Multiply each distance by the norm of its corresponding direction ray
# to convert to real world distance (accounts for non-unit directions).
deltas = deltas * torch.norm(dir_.unsqueeze(1), dim=-1)
 
noise = torch.randn(sigmas.shape, device=sigmas.device) * noise_std
 
# compute alpha by the formula (3)
alphas = 1-torch.exp(-deltas*torch.relu(sigmas+noise)) # (N_rays, N_samples_)
alphas_shifted = \
    torch.cat([torch.ones_like(alphas[:, :1]), 1-alphas+1e-10], -1) # [1, a1, a2, ...]
weights = \
    alphas * torch.cumprod(alphas_shifted, -1)[:, :-1] # (N_rays, N_samples_)
weights_sum = weights.sum(1) # (N_rays), the accumulated opacity along the rays
                                # equals "1 - (1-a1)(1-a2)...(1-an)" mathematically
if weights_only:
    return weights
 
# compute final weighted outputs
rgb_final = torch.sum(weights.unsqueeze(-1)*rgbs, -2) # (N_rays, 3)
depth_final = torch.sum(weights*z_vals, -1) # (N_rays)

第二轮渲染

对于渲染的结果,会根据 对应的权重,使用 pdf 抽样,得到新的渲染点。例如默认第一轮粗渲染每束光线是 64 个样本点,第二轮再增加 128 个抽样点。

然后使用 finemodel 进行预测,后对所有的样本点(64+128)进行体素渲染。

def sample_pdf(bins, weights, N_importance, det=False, eps=1e-5):
    """
    Sample @N_importance samples from @bins with distribution defined by @weights.
 
    Inputs:
        bins: (N_rays, N_samples_+1) where N_samples_ is "the number of coarse samples per ray - 2"
        weights: (N_rays, N_samples_)
        N_importance: the number of samples to draw from the distribution
        det: deterministic or not
        eps: a small number to prevent division by zero
 
    Outputs:
        samples: the sampled samples
    """
    N_rays, N_samples_ = weights.shape
    weights = weights + eps # prevent division by zero (don't do inplace op!)
    pdf = weights / torch.sum(weights, -1, keepdim=True) # (N_rays, N_samples_)
    cdf = torch.cumsum(pdf, -1) # (N_rays, N_samples), cumulative distribution function
    cdf = torch.cat([torch.zeros_like(cdf[: ,:1]), cdf], -1)  # (N_rays, N_samples_+1) 
                                                               # padded to 0~1 inclusive
 
    if det:
        u = torch.linspace(0, 1, N_importance, device=bins.device)
        u = u.expand(N_rays, N_importance)
    else:
        u = torch.rand(N_rays, N_importance, device=bins.device)
    u = u.contiguous()
 
    inds = searchsorted(cdf, u, side='right')
    below = torch.clamp_min(inds-1, 0)
    above = torch.clamp_max(inds, N_samples_)
 
    inds_sampled = torch.stack([below, above], -1).view(N_rays, 2*N_importance)
    cdf_g = torch.gather(cdf, 1, inds_sampled).view(N_rays, N_importance, 2)
    bins_g = torch.gather(bins, 1, inds_sampled).view(N_rays, N_importance, 2)
 
    denom = cdf_g[...,1]-cdf_g[...,0]
    denom[denom<eps] = 1 # denom equals 0 means a bin has weight 0, in which case it will not be sampled
                         # anyway, therefore any value for it is fine (set to 1 here)
 
    samples = bins_g[...,0] + (u-cdf_g[...,0])/denom * (bins_g[...,1]-bins_g[...,0])
    return samples

Loss

这里直接使用的 MSE loss,对输出的像素值和 ground truth 计算 L2-norm loss.

训练数据

训练数据

quicker_a23cfc59-ae27-4cb6-83fa-5149a4b91f19.png

根据前面的介绍,NeRF 实现的,是从 【位置坐标 katex 和 拍摄角度(θ,ϕ)】 到 【体密度 (σ) 和 RGB 色彩值 (C)】的映射。根据体素渲染理论,图片中的每一个像素,实质上都是从相机发射出的一条光线渲染得到的。 因此,我们首先,需要得到每一个像素对应的光线(ray), 然后,计算光线上每一个点的【体密度σ) 和 RGB 色彩值 (C)】,最后再渲染得到对应的像素值。

对于训练数据,我们需要拍摄一系列的图片(如 100 张)图片和他们的拍摄相机角度、内参、场景边界(可以使用 COLMAP 获得)。我们需要准备每一个像素对应的光线(ray)信息,这样可以组成成对的训练数据【光线信息 <==> 像素值】。

下面以 LLFFDataset (”datasets/llff.py”) 为例,进行分析:

读取的数据(以一张图片为例)

  • 图片:尺寸是 N_img​×C×H×W。 其中 C=3 代表了这是 RGB 三通道图片
  • 拍摄角度信息(从 COLMAP 生成):Nimg​×17。前 15 维可以变形为 3×5,代表了相机的 pose,后 2 维是最近和最远的深度。解释: 3×5 pose matrices and 2 depth bounds for each image. Each pose has [R T] as the left 3×4 matrix and [H W F] as the right 3×1 matrix. R matrix is in the form [down right back] instead of [right up back] . (https://github.com/bmild/nerf/issues/34

拍摄角度预处理

第一步:根据拍摄的尺寸和处理尺寸的关系,缩放相机的焦距。例如:Himg​=3024,Wimg​=4032,Fimg​=3260, 如果我们想处理的尺寸是H=378,W=504 (为了提升训练的速度),我们需要缩放焦距 F:

# "datasets/llff.py", line:188
    # Step 1: rescale focal length according to training resolution
    H, W, self.focal = poses[0, :, -1] # original intrinsics, same for all images
    assert H*self.img_wh[0] == W*self.img_wh[1], \
        f'You must set @img_wh to have the same aspect ratio as ({W}, {H}) !'
 
    self.focal *= self.img_wh[0]/W

第二步:调整 pose 的方向。在 “poses_bounds.npy” 中,pose 的方向是 “下右后”,我们调整到 “右上后”。同时使用 “center_poses(poses)” 函数,对整个 dataset 的坐标轴进行标准化(??)。
解释:“poses_avg computes a “central” pose for the dataset, based on using the mean translation, the mean z axis, and adopting the mean y axis as an “up” direction (so that Up x Z = X and then Z x X = Y). recenter_poses very simply applies the inverse of this average pose to the dataset (a rigid rotation/translation) so that the identity extrinsic matrix is looking at the scene, which is nice because normalizes the orientation of the scene for later rendering from the learned NeRF. This is also important for using NDC (Normalized device coordinates) coordinates, since we assume the scene is centered there too.”(https://github.com/bmild/nerf/issues/34

# "datasets/llff.py", line:195
    # Step 2: correct poses
    # Original poses has rotation in form "down right back", change to "right up back"
    # See https://github.com/bmild/nerf/issues/34
    poses = np.concatenate([poses[..., 1:2], -poses[..., :1], poses[..., 2:4]], -1)
            # (N_images, 3, 4) exclude H, W, focal
    self.poses, self.pose_avg = center_poses(poses)

第三步:令最近的距离约为 1。 解释:“The NDC code takes in a “near” bound and assumes the far bound is infinity (this doesn’t matter too much since NDC space samples in 1/depth so moving from “far” to infinity is only slightly less sample-efficient). You can see here that the “near” bound is hardcoded to 1”。For more details on how to use NDC space see https://github.com/bmild/nerf/files/4451808/ndc_derivation.pdf

# "datasets/llff.py", line:205    # Step 3: correct scale so that the nearest depth is at a little more than 1.0    # See https://github.com/bmild/nerf/issues/34    near_original = self.bounds.min()    scale_factor = near_original*0.75 # 0.75 is the default parameter                                        # the nearest depth is at 1/0.75=1.33    self.bounds /= scale_factor    self.poses[..., 3] /= scale_factor

计算光线角度


接下来就是对每一个像素,使用 “get_ray_directions()” 函数计算所对应的光线。这里只需要使用图像的长宽和焦距即可计算

self.directions = get_ray_directions(self.img_wh[1], self.img_wh[0], self.focal) # (H, W, 3)

调用函数:

def get_ray_directions(H, W, focal):
    """
    Get ray directions for all pixels in camera coordinate.
    Reference: https://www.scratchapixel.com/lessons/3d-basic-rendering/
               ray-tracing-generating-camera-rays/standard-coordinate-systems
 
    Inputs:
        H, W, focal: image height, width and focal length
 
    Outputs:
        directions: (H, W, 3), the direction of the rays in camera coordinate
    """
    grid = create_meshgrid(H, W, normalized_coordinates=False)[0]
    i, j = grid.unbind(-1)
    # the direction here is without +0.5 pixel centering as calibration is not so accurate
    # see https://github.com/bmild/nerf/issues/24
    directions = \
        torch.stack([(i-W/2)/focal, -(j-H/2)/focal, -torch.ones_like(i)], -1) # (H, W, 3)
 
    return directions

世界坐标系下的光线
在拿到每一个像素对应的光线角度后,我们需要得到具体的光线信息。首先,先计算在世界坐标系下的光线信息。主要是一个归一化的操作。

Get ray origin and normalized directions in world coordinate for all pixels in one image. Reference: https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-generating-camera-rays/standard-coordinate-systems

输入:

图像上每一点所对应的光线角度:(H, W, 3) precomputed ray directions in camera coordinate。
相机映射矩阵 c2w:(3, 4) transformation matrix from camera coordinate to world coordinate
输出:

光线原点在世界坐标系中的坐标:(HW, 3), the origin of the rays in world coordinate 在世界坐标系中,归一化的光线角度:(HW, 3), the normalized direction of the rays in world


def get_rays(directions, c2w):
    """
    Get ray origin and normalized directions in world coordinate for all pixels in one image.
    Reference: https://www.scratchapixel.com/lessons/3d-basic-rendering/
               ray-tracing-generating-camera-rays/standard-coordinate-systems
 
    Inputs:
        directions: (H, W, 3) precomputed ray directions in camera coordinate
        c2w: (3, 4) transformation matrix from camera coordinate to world coordinate
 
    Outputs:
        rays_o: (H*W, 3), the origin of the rays in world coordinate
        rays_d: (H*W, 3), the normalized direction of the rays in world coordinate
    """
    # Rotate ray directions from camera coordinate to the world coordinate
    rays_d = directions @ c2w[:, :3].T # (H, W, 3)
    rays_d = rays_d / torch.norm(rays_d, dim=-1, keepdim=True)
    # The origin of all rays is the camera origin in world coordinate
    rays_o = c2w[:, 3].expand(rays_d.shape) # (H, W, 3)
 
    rays_d = rays_d.view(-1, 3)
    rays_o = rays_o.view(-1, 3)
 
    return rays_o, rays_d

NDC 下的光线

NDC (Normalized device coordinates) 归一化的设备坐标系。

首先对光线的边界进行限定:

near, far = 0, 1

然后对坐标进行平移和映射。

def get_ndc_rays(H, W, focal, near, rays_o, rays_d):
    """
    Transform rays from world coordinate to NDC.
    NDC: Space such that the canvas is a cube with sides [-1, 1] in each axis.
    For detailed derivation, please see:
    http://www.songho.ca/opengl/gl_projectionmatrix.html
    https://github.com/bmild/nerf/files/4451808/ndc_derivation.pdf
 
    In practice, use NDC "if and only if" the scene is unbounded (has a large depth).
    See https://github.com/bmild/nerf/issues/18
 
    Inputs:
        H, W, focal: image height, width and focal length
        near: (N_rays) or float, the depths of the near plane
        rays_o: (N_rays, 3), the origin of the rays in world coordinate
        rays_d: (N_rays, 3), the direction of the rays in world coordinate
 
    Outputs:
        rays_o: (N_rays, 3), the origin of the rays in NDC
        rays_d: (N_rays, 3), the direction of the rays in NDC
    """
    # Shift ray origins to near plane
    t = -(near + rays_o[...,2]) / rays_d[...,2]
    rays_o = rays_o + t[...,None] * rays_d
 
    # Store some intermediate homogeneous results
    ox_oz = rays_o[...,0] / rays_o[...,2]
    oy_oz = rays_o[...,1] / rays_o[...,2]
 
    # Projection
    o0 = -1./(W/(2.*focal)) * ox_oz
    o1 = -1./(H/(2.*focal)) * oy_oz
    o2 = 1. + 2. * near / rays_o[...,2]
 
    d0 = -1./(W/(2.*focal)) * (rays_d[...,0]/rays_d[...,2] - ox_oz)
    d1 = -1./(H/(2.*focal)) * (rays_d[...,1]/rays_d[...,2] - oy_oz)
    d2 = 1 - o2
 
    rays_o = torch.stack([o0, o1, o2], -1) # (B, 3)
    rays_d = torch.stack([d0, d1, d2], -1) # (B, 3)
 
    return rays_o, rays_d

训练数据的生成

输出分为两部分:光线的信息,和对应的图片像素值

  • 对于每一束光线,按照 【光线原点 (3d), 光线角度 (3d), 最近的边界 (1d), 最远的边界 (1d)】= 8d 的格式存储。
  • 光线对应的像素,RGB=3d 的格式存储。
self.all_rays += [torch.cat([rays_o, rays_d,                                              near*torch.ones_like(rays_o[:, :1]),                                             far*torch.ones_like(rays_o[:, :1])],                                             1)] # (h*w, 8)