3D Reconstruction Series

引言

更新,已决定停止更新( x


可以看到本文的 publishDate 是 4096-16-64, 实际上的 publishDate 是 2026-02-10 。
本文的初衷是一个长期更新的 3D recon 系列论文阅读,之前其实已经发过了一些该领域的论文的精读了,但是显然精读必然是不可长期持续的。因此,我想以本文——一个系列的形式记录对大多数论文的浅要阅读,当然如果有特别重要的论文,我也会单开一篇文章进行精读的。

本文的 cover image 是一个词云,记录了本文包含的工作的名称,希望它能不断地更新,成为一个 3D recon 领域的词云图谱。

CUT3R

CUT3R 的输入是视频序列,但是也可以 unordered (据作者所言训练的时候是无序训练的,但是推理的时候推理的时候是 dataloader 先计算重合率来进行初步排序。),使用一个 feed forward 网络预测 camera parameters 和点云。

cut3r

然后是一个 recurrent 模型,每一帧输入的时候添加一个 pose token 然后经过 encoder 和 decoder ,之后使用交叉注意力更新,之后再使用不同 head 来从中提取 output 。

显然这样缺少修正,对于长序列容易造成偏移。但是作者似乎也提到了一个 revisit 机制,在输入结束之后拿着全局的来做之前的预测,在 7scene 上的 acc 和 comp 是有改善的,但是 NRGBD 不怎么明显。

此外,作者也说因为数据集质量的原因,采用的 head 即使已经有一个 pose head 和 local points head ,也仍然要加入一个 world ptshead (缺乏高质量的数据集)。

">

是一个相对来说比较有趣的东西,模型结构如下:

pi3

首先与之前的最大不同是它没有显式地选取参考帧和一个特定的 scale factor ,像 VGGT 就是先选取了一个 ref frame 然后做重建,但是重建质量受 ref 影响很大,因此选择了一个方案,就是一次性将所有帧全部输入,所有帧之间均平等,然后 inference 出一组相对位姿和局部点云,这样就能规避确定某一个 frame 作为坐标原点造成的不确定性问题。

但是仔细一想,仍然不怎么好避免一个 ref 的问题,首先,在一个 batch 内部,虽然我们预测的是一组相对位姿,但是直觉上感觉仍然是把某一帧与其他帧不融洽所导致的原先的那种大的,显著的,偶然性的损失转化为了现在的看起来不明显的、高一致的、所有帧都有的系统性损失。但作者通过实验证明了损失会变小,其实这也是比较好解释的,因为原先的可能是依赖依赖……这种单向参考,而则进行了交叉注意力计算,仔细想来确实会更好。

其次,交叉注意力的复杂度大概是,显然对于长序列是不可接受的,作者训练和测试的时候均采用了有限个 batch 内 frame 的做法,但对于实际的长序列的话,感觉并不是很好做。如果切片进行拼接的话,显然也会面临 ref 的选择问题,但是这时候是一个 scene 之间的拼接,感觉确实会降低很多错误,如果分层做的话,也会降低误差,总之感觉似乎确实是一个不错的方案。

DA3

DA3 是字节 seed 的一个项目,可以说是力大飞砖,充分体现了工业界解决问题的规模( x 。

da3
DA3 的主要创新点在于:

  • 更简单的模型,作者的意思是 VGGT 即使结构很简单,但是由于其在 DINO 后接 AA 层的操作,因为 AA layers 是新训练的,因此过程中可能数据的利用率不高。而 DA3 选择了只利用 DINO 这一个方案,通过在 DINO 的层中变形数据完成了 AA 层所做的事情。因此, DA3 的几乎所有参数都是预训练过的,而 vggt 则有 的参数是从头开始训的,这是 DA3 的简洁之处。

  • 预测任务的简洁性。相比于 VGGT 通过不同 head 得出了不同结果, DA3 则使用了一个更新的表达方式: Ray-depth 表达,具体来说就是使用一个 Dual head 来分别输出一个像素的深度信息和光心与之相连的射线的信息,从而天然地同时包含了点云和 pose 信息,而且在设计 loss 的时候是可以加入一致性信息的。相比与 vggt ,这似乎加强了一致性,也提高了数据利用率,感觉 pose 和 pts3d 反而是不容易加入一致性的,作者做的消融实验也证实了这一点。

  • 使用 teacher 标定数据,首先训了一个 teacher 模型用于给深度不好的 frame 重新生成 depth ,之后依照这个 depths 训练。感觉最终效果也很依赖这个 teacher 模型。

但是, DA3 的弊端也有一些,他的效果确实非常好,但是阅读之后才发现他是用 128 x H100 训练的,这个规模确实有点难以复现。小算力情况下上面两条结论似乎很有帮助,可以尝试。

MapAnything

首先是 Meta 的项目,和 VGGT 难道不构成什么竞争关系嘛()

主要创新点在于他的输入很有意思,不同于 VGGT 还有以往的重建工作只输入图像序列, MapAnything 支持多种多样的输入,对于每一个输入都会通过一个 encoder 最后对齐到 DINOv2 输出的 image token 上,然后就是正常处理的流程,不过似乎它多加了一个 scale token ,用于预测 scale 信息。

mapanything

感觉其利用了 nlp 里面的多模态,证明了给定不同类型的输入其预测的准确性与相应的专家模型性能相似,这是很有价值的,因为他减少了很多训练量(虽然也是在 64xH200 上训了 10 天)。

另外一个比较有趣的地方在于,他最后的点云数据不是直接输出的,而是由 depth , ray , pose 联合输出,这解耦了 VGGT 的冗余预测模式,而且在设计 loss 的时候能保持更好的一致性,感觉这个跟 DA3 输出 Depth-ray 的做法还是很像的。

不过其缺点也非常明显,首先对于长序列情况下,其仍然没有摆脱的处理复杂度;其次模型是 offline 的,不过感觉各有各的应用场景;最后就是推理速度和显存占用,推理速度在 100frame 的时候就已经接近 10s ,而且这时的显存占用也已经来到了 65G 左右,即使采用了作者提出的 Mem Efficent 策略,即在 dpt 头采用串行计算策略也是 20G 左右,似乎有点太大了( x

此外,作者表示了在输入过程中模型无法对噪声数据进行处理,也就是说潜在的噪声可能会污染整个 transformer 的内容,另外融合时机是在 encoder 之后进行,而且是简单的相加,可能有更精细的融合方式。

AnySplat

anysplat

与之前讲过的大多数点云重建的工作不同, AnySplat 是 3dgs 重建。具体来讲就是他在 vggt 的基础上进行改造, backbone 与 vggt 相同,但其 head 则是一个 gaussian head, 一个 depth head ,还有一个 Camera head 。然后通过一个可微体素化将原本稠密的高斯球聚合到一起,训练的时候则监督:

  1. 每一帧位置的 rgb loss

  2. depth 的深度与 gaussian depth 的差异损失

  3. 相机参数与 vggt 预测出的损失

  4. 模型预测深度与 vggt 之间的深度差异

首先, 2 的 loss 保证了其几何一致性,也就是让不同视角的深度尽量保持一致,可以避免分层现象。此外,文章作者说他们实现了一个 Differentiable Voxelization ,可以有效解决生成的稠密高斯球产生的复杂度问题。

总体来说,这是一个高度模仿 vggt 的工作,只不过换了一下 head 和输出形式,其余部分都差不多。此外为 offline 的重建,看上去速度似乎还可以,但是同样面临长序列问题。另外,固定世界坐标系为第一张图片,去监督每一个绝对位姿是否正确,似乎也是存在所述的归纳偏置问题的。

RayZer

rayzer

令人耳目一新的自监督模型,训练过程只需要图片而不需要 gt 的 pose 和内参,训练过程大概是这样的:

  • 首先输入张图片,将其分为两个集合。

  • 然后模型通过 Camera Estimator 模块,预测出 pose 和 intrinsics 。

  • 之后对于 ,模型根据其对应的预测出来的 和本身的图片输入,生成场景的 token.

  • 然后对于,模型选择通过 预测出 然后监督 之间的损失,然后更新所有的值。

因此,推理时的大致步骤大概就是先把场景的已知几张图片输入得到 ,之后针对一个特定的 pose ,计算一个光线图,之后输入到 rendering decoder 里得到在这个特定的 pose 下的 rgb 图片。

感觉和 nerf 好像,都是一个隐式的表达整个场景,不过不同的是 RayZer 是一个更直接的模型,图里的三个模块每个都是 8 层 naive transformer , loss 仅由最后的 rgbloss 和 LPIPS loss 决定,感觉挺聪明的。不过感觉 rendering 部分采用的表现形式——类 raymap 形式似乎真的挺好用的。

另外,值得注意的是第一部分,在预测 pose 和 intrinsics 时,直接选取了中间帧作为参考帧,使得模型能跨越更长的距离。此外,如果说我们在第一部分就引入 ,能否实现定位功能?不过作者似乎做了消融实验,发现在训练的时候,从图像特征中提取几何关系比从一个未成形的 中提取容易得多。但是我觉着可以在 rendering 部分再添加一个 decoder 用于定位。

另外,这个模型完全打败了 LVSM (一个有监督的模型),感觉是一个非常惊艳的工作,看项目主页的 demo 视频感觉真的很不错啊。

Spa3R

spa3r

首先是一个自监督的模型,模型的 backbone 设计的有点复杂:

我们给出一个场景的 views ,然后将 views 分为 context view 和 target view ,首先将所有 views 通过一个改造过的 vggt (似乎是只引入了 head 之前的部分),改造内容是在 context Views 的 AA 层那里把 Target Views 给 mask 掉,然后得到 Context Views 的 feature 和 Target Views 的 camera token 和 Feature ,之后,数据流向两条路径:

  • Context views : 与一组可学习的 通往 Encoder ,然后得到 作为空间的隐式表征。

  • Target views : camera tokens 通过 camera head 生成 camera embeds ,然后与 一起输入到 Decoder 里生成对 Target views 的预测过的 feature ,然后将得到的预测 feature 与 进行监督得到 loss 。

推理的阶段我们就只看 Context Views 得到的 ,将与 qwen2.5 vl 得到的 输入到一个Adapter里,然后将这个 adapter 和 text prompt 输入到 llm 里得到最终结果。

首先,肉眼可见这项工作把大量的其他工作缝合到了一起, Target View 阶段用了 DINOv3 和 VGGT , 的后续处理用到了 qwen2.5 vl ,但是这篇文章叫 Spa3R 啊, Dust3R 被放到哪了呢?然后可训练的内容只有 Encoder 和 Decoder ,仅 6 层 Transformer ,而且通过两个作为 loss 进行训练,训练结束之后即丢弃 Decoder ,保留训好的 Encoder 和 q 。然后后续还有一个针对 Adapter 的一个微调,让其学到怎么生成一个合理的融合

模型做了几个消融实验:

  1. Target Views 阶段作者证明了同时使用 VGGT 和 DINO 会更好(包含语义和空间信息),这是一个比较显然的结论。

  2. 提取出一个场景 表征是一个更好的手段,相对于现有的几个类似于 VG-LLM 简单把所有特征输入到 llm 里效果更好(但是只提升了 3 个点,感觉有点低于预期,考虑到第二阶段训练只进行了 1 个 epoch ,有没有可能是训练量不够?我也是第一次读 VLM 相关的文章(),不过看具体的比分, Multi-Choice 涨分了,而 Numerical 几乎没变,确实是 make sense 的)。

  3. pose embedding 的影响, PRoPE 比 plucker 更好。

  4. Mask Ratio ,这也是一个比较显然的消融实验。

  5. Adapter 使用提高了点数,比较 make sense 。

模型只在 ScanNet 和 ScanNetpp 上进行了 pre train ,使用了 8 张 5090 进行训练,在 VSI-Bench 上达到了 58.6 的水平,超过了之前的大部分 model ,查看现在的 VSI-Bench Leaderboard ,其性能也是处于前列的(不过论文里的表格好像有些数据有点不对?可能有更新吧)。算是为领域开了一个新坑(),自监督看上去也不错()。

看上去这篇文章正在投 CVPR ,是笔者写阅读笔记的两天前才登上了 arxiv ,也不知道中没中,方法是很有趣

Spann3R

结构很复杂,首先大部分模型权重继承自 Dust3r ,然后模型的 backbone 大致如下:

spann3r

  • 预编码 :首先将一帧输入到 ViT Encoder 得到一个 ,此时我们手上还有一个上一帧的

  • 查询记忆: 根据 ,我们可以从历史记忆中查询出一个 来作为下一步的输入。

  • 主要推理部分: 之后我们将这两个 feature 输入到 Target Decoder 和 Reference Decoder ,这两个 Decoder 会做 self attention 和 Cross attention 然后分别得到

  • Heads : 对于 ,在推理阶段我们会使用一个 query head 来提取出,然而在训练阶段我们也会加入一个 head 将其转化为点云和置信度来监督训练;对于 ,我们会通过一个 reference head 将其重建出点云和置信度。

  • 记忆: 之后,根据 ,我们将其通过一个 Memory encoder + MLP head 生成一个 ,然后根据这个和点云通过一个 Memory Encoder 生成 ,之后会对已有记忆去重, 如果工作记忆已满剩下的就会进长期记忆然后做进一步处理。

这是一篇 24 年的文章了,主要创新点就在于他改良了 Dust3R ,使得可以对多个图片输出一个一致的全局坐标系下的点云,此外使用记忆方法,分层处理记忆。

但很显然的是,虽然该方法加入了记忆,但是记忆看上去也是近期记忆的方案,客观上因此而存在长距离漂移的现象,此外,如果遇到 reloop 现象,记忆是否能健康提取也会是一个比较大的问题。

做的消融实验大致有这几个:

  1. 关于记忆方面的消融实验,去掉长期记忆会引起很大的漂移现象,而注意力不截断的话也会引发噪声的干扰

  2. 关于长期记忆应该取多大:作者发现 1000-2000token 的过程中漂移得到极大修正,但是 4000+之后就不会有明显的提升,因此最后作者选择了 4000.

  3. Dust3R 采用了 exp confidence function ,本文将其改为了 sigmoid ,事实证明是有所改善的。

Flow4R

一个局限性很大的三维重建追踪方案,不过在表现形式上很有新意。

模型的 backbone 很优雅,首先接收两张图片作为输入,通过共享权重和 cross attention 的两个对称 encoder-ecoder-head 结构得到每张图的 其中, 是相机坐标系下的点云, 是一个场景流,描述每一个像素如何从本张图片移动到下一张点云,之后还有一个 指示哪个像素在求解 pose 的时候最可靠,最后的 是全局的置信度。

flow4r

得到这些元素之后,可以首先将 pose 通过最小二乘法求出:

是由 得到的,得到 pose 之后就可以做位姿流和场景流的分解,然后很多下游任务就可以进行处理了。

针对于长序列数据,作者提出了将第 1 张 frame 作为锚点,后续的每一张都与之输入处理,好处是可以通过 L2 norm 来归一化尺度,但是坏处也非常明显,一是稍微长一点的序列,就会出现遮挡现象,模型目前来看没有一种很好的应对方式;二是极其依赖第一张 frame 的质量,鲁棒性不算太好。观察其论文里呈现的 demo ,看起来也通常是对一个角落 or 一个相似视角区域做的重建,完整场景重建效果存疑。

此外,作者竟然只做了一个消融实验(能中吗?)对比了三种不同的网络预测和监督变体 :

  • 预测场景流 ,并用真实的  进行损失监督 。

  • 预测场景流 ,但用目标帧的真实 3D 点位置  进行监督 。

  • 直接预测目标帧的 3D 点位置 ,并用真实的  监督(场景流则通过简单的减法推导: ) 。

消融结果:实验证明,直接预测并监督 的性能最佳 。因此后来直接预测的实际上是

总体来说,这篇工作证明了一点,可以通过引入流的方式来完成 Dust3R 这种结构从静态到动态的拓展,但确实局限性很大。

这项工作似乎还没有开源()

AMB3R

把三维体素引入到了重建中,使得模型能够真正地从空间角度来考虑重建任务。简而言之就是之前的重建采用的 ViT 将图像分为一个一个 patch 造成隐式几何中缺乏空间紧凑性约束,于是论文作者想了一个办法把空间紧凑性加入到了 backbone 当中。

amb3r

大致的 backbone 分为前端和后端,其中,前端继承了 VGGT 的网络和参数,一张图片进入之后会经过 Encoder 得到一个初步的 feature ,然后数据的主题是向 decoder 移动,但是这部分 feature 也会使用一个 scale head 预测一个绝对尺度。

然后,进入 decoder 的 feature 会对 keyframes 做 cross attention ,这里的 keyframe 就可以理解为场景的隐式表达,经过该过程之后, decoder 就会输出一个 pointmap 和一个 confidence ,在推理阶段,之后会有一个门控机制:如果置信度足够高,那就直接进入下一阶段,反之则会将点云和 feature 变为体素,然后通过一个 point transformer 优化该体素的 feature ,之后再会逆变换变为 2Dfeature ,之后我们会将该 feature 注入到前端的 decoder 中,重新拿到一个高级的点云。

然后我们拿到了当前帧的点云以及物理尺度,然后系统会将该结构放大/缩小,然后根据 keyframes 和 VGGT 预测出的 pose 将该结构拼接到大的全局点云中,最后我们会评测该点云是否可以成为 keyframes ,然后将其处理掉。

将体素引入到点云重建里很厉害,作者做的几个消融实验:

  1. 移除了基于 sparse voxel 的后端,转而使用一个 2D 做 alternate attention 的后端,发现精度不如之前。

  2. 去除了零卷积机制,发现模型短时间内根本就未收敛。

  3. 在算 loss 的时候去除了 scale 发现效果变差,也就是说模型需要去专注思考几何结构。这是在训练阶段做的事情

这篇文章的训练成本非常非常的低,依赖于一个已经训好的 VGGT ,只训练了微调点云特征的一个 point Transformer 和一堆 head ,感觉非常有启发性非常厉害,同时也中了 CVPR2026 ,符合预期(似乎是 Spann3R 的续作)

VGGT-SLAM

我说这是一篇数学论文,文中没有训练任何模型,仅仅是介绍了一种局部点云拼合办法。

vggt-slam

顾名思义,这篇工作基于 VGGT 输出的点云和 pose ,作者认为 VGGT 预测出 pose 和局部点云之后直接进行 Sim(3)变化为全局点云是有问题的,主要灵感来自于传统 CV 里面的双目立体视觉:相机之间的单应性矩阵或者说是本征矩阵并非仅仅包含了 pose 中进行的旋转、平移,更有一些拉伸,透视等等等。具体来讲就是 VGGT 预测出的点云深度包含了相机的射影形变,直接使用 Sim(3)方法来还原是不准确的。

因此,作者转而使用了 SL(4)进行点云的对齐,具体来讲,当 VGGT 得出了点云和 pose 之后,会进行以下几个操作:

  • 对于一个子地图里的帧,作者选择相信 VGGT 的质量,作者在代码里设置了一个 submap_size 参数用于控制子地图的大小。

  • 对于不同子地图之间,因为我们想得到一个在不同坐标系下共享的三维点,所以作者这里采用了一个很聪明的办法,将上一个子地图的最后一帧重复输入到下一个子地图里,这样 VGGT 的输出就包含了相同图片在不同坐标系下的点云,由此可以建立点与点之间的对应关系。

  • 之后根据传统的一些算法,可以计算两个子地图之间的 SL(4)矩阵,到这里第一步就算完成了

  • 下一个步骤就是全局对齐,作者也写得太数学了吧:

  • 具体来讲,作者构建了一个基于最大后验估计的非线性因子图,目标是最小化所有子地图之间的相对单应性误差:

    $\hat{H}=argmin_{H\in SL(4)}\sum_{(i,j)\in\mathcal{L}}||Log(H_{i}^{-1}H_{j}(H_{j}^{i})^{-1})||{\Omega{ij}^{H}}^{2}$

  • 然后引入各种优化器,这里我的数学太烂了( x )根本看不懂,只知道他是需要迭代优化的。

  • 嗯嗯,所以这样我们就可以得到一个后端,对于每一个子地图,都给出了一个将其变换到潜在全局坐标系下的 SL(4)矩阵,从而消除了 Sim(3)变换带来的问题。

此外,文章还提出了一种 reloop 机制,就是说在一个子地图待输入的时候,系统会利用 SALAD 描述子去寻找历史子地图中是否有相似的图片,若有,系统就会选择将那张图片作为共享帧,我们这时候就会有多个相对的信息。

总体来说,这篇工作就是提供了一个偏传统的对齐方法,比较优雅,但是很显然缺点也很明显,首先对于单个子地图,该工作完全信任 VGGT 的输出结果,缺乏鲁棒性;其次,其得出对齐是通过迭代优化得出,相对于直接拼接会慢上很多,另外有太多的查询操作(如 reloop ),感觉复杂度还是有点高的。

不过可以从上图看到,他确实改善了点云拼接时可能产生的分层的质量。但是,查看其 github 里的 issue ,似乎稳定性存疑:

Due to potential randomness in our approach caused by RANSAC, we report the average performance over five runs, which have a low spread (small standard deviation) as shown in Sec. 5.5.

而且那个 issue 到最后作者都没有回答,感觉有点尴尬( x

SF3D 论文阅读记录

引言

mesh construction 是我刚刚开始了解的一个方向, 今天读了SF3D: Scene Fusion for 3D Reconstruction with Transformers这篇论文, 本文笔记记录用于后续翻阅学习。

读完这篇论文之后, 感觉 mesh reconstruction 与 point cloud reconstruction 还是有很大区别的, 尤其是这篇文章中引入的几个新的 mesh 专有的 module, 感觉要比 point cloud reconstruction 更加复杂一些.OK,
废话不多说, 直接进入正题.

Introduction

作者一上来就提出了几个 issue:
SF3D提出的问题

  1. Light bake-in: 现有的模型将光照信息直接 bake 到 texture 里, 使得生成的 mesh 难以利用, 而在 SF3D 中, 作者提出了使用 explicit illumination 和一个不同的使用 Spherical Gaussian 的 shading model 来解决这个问题(如上图第一行所示).
  2. Vertex Coloring: 现有的工作中, 生成的 vertex 的数量过多, 使得性能开销很大. 作者认为一个关键问题就是 UV unwrapping 的额外处理时间, 于是作者提出了一种 highly parallelizable fast box projection-based UV
    unwrapping method 来解决这个问题(如上图第二行所示), 这使得时间从 10-30s 减少到了 0.5s, 而且从图上来看, 细节比 baseline 的 TripoSR 的效果更好.
  3. Marching Cube Artifacts: feed-forward network 通常生成类似与 Triplane NeRFs 的体素网格, 然后使用 marching cube 来提取 mesh, 但是这种方法会引入一些 artifacts,
    作者提出了使用一个对高分辨率 Triplane 更有效的 architecture, 并且使用 DMTet 来对生成的 vetex diplacement 和 normal map 生成最终的 mesh, 这样可以有效减少 marching cube 引入的 artifacts(如上图第三行所示).
  4. Lack of Material Properties: 现有的工作生成的 mesh 在不同光照下都会看起来 dull, 这是因为缺乏 explicit 的 material properties.为解决这个问题, 作者预测了 non-spartially varying material properties
    (如上图第 4, 5 行所示).

通过以上的改进, SF3D 可以从单张图像生成高质量的 mesh, 且生成的 3D 资产体积小(1 MB)并且可以在 0.5s 内生成.

Method

为了解决上面提到的问题, 作者提出了 SF3D.

首先, SF3D 是在 TripoSR 的基础上进行改进的. TripoSR 训练了一个能够生成 Triplane 3D representation 的 transformer. 它使用 DINO encode image, 然后把 token 送入 transformer 中, transformer 输出一个分辨率的
triplane, 然后 triplane feature 之后被 decode 为 color 和渲染成标准 NeRF. TripoSR 只学到了 colors 并且不能处理反射等材质属性.

Overview

SF3D 的整体架构如下图所示:
SF3D架构图
可以看到, SF3D 由 5 个主要模块组成:

  1. Enhanced Transformer: 用于预测高分辨率的 triplane feature.
  2. Merterial Estimation: 用于预测材质属性.
  3. Illumination Modeling: 处理光照问题.
  4. Mesh extraction and refinement: 用于从 triplane 中提取 mesh 并进行细化.
  5. UV Unwrapping and Export: 产生 low-poly mesh 和 高分辨率 texture map.

Enhanced Transformer

为了生成高分辨率的 triplane feature, 作者对 TripoSR 的 transformer 进行了改进, 主要有以下几点:

  • 首先, 作者将 DINO 替换成了 DINOv2, 这样可以获得更好的 image feature.
  • 其次, 作者对 triplane 导致的 aliasing 问题进行了讨论
    aliasing问题
    如上图所示, 低分辨率的 triplane 会导致 aliasing 问题, 但是简单地提高 triplane 的分辨率会导致模型更复杂, 作者说, 他从 PointInfinity 中获得启发,
    (PointInfinity 提供了一个不需要计算 triplane 的 self-attention 的架构), 因此, 作者将分辨率提高到, 从而降低了走样.

Material Estimation

SF3D 输出了 metallic 和 roughness 两个材质属性. 论文中提到, 理想状况下, 人们希望材质属性是 spatially varying 的, 但是这样并不现实. 于是作者简化了这个问题, 为整个物体
预测这两个属性, 作者提到虽然这种非空间变化的材质属性通常适用于同质物体, 但是实际上能显著改善渲染效果.

为了实现这个预测, 作者引入了一个 Material net, 首先将图像通过 CLIP encoder 编码, 然后通过 2 个 MLP 预测 metallic 和 roughness.

Illumination Modeling

作者提出要显式 estimating 光照, 如果不这样做的话, 输出的 RGB 颜色会将光照信息 bake 进去, 使得生成的 mesh 难以利用. 为此, 作者提出了一个 Light net, estimate SG 光照. 因为 triplane encode 了场景的几何信息, 所以可以能够推断光照变化.

具体实现上, 作者使用 Transformer 输出的 分辨率的 triplane 作为输入, 使其通过 2 个 CNN 层, 接着进行 max pool,
最后通过一个 MLP 。 Light Net 输出 24 个 SG 的 grayscale amplitude values, 并使用 Softplus 以确保值为正数。这些 SG 的轴和锐度值保持固定, 其设置旨在覆盖整个球体。
利用这些振幅值, 作者实施了一种类似于 NeRD [4] 中使用的 deferred physically based rendering 方法.

此外, 作者的方法在训练阶段还引入了一个 lighting demodulation loss , 该损失函数旨在确保:一个具有 entirely white albedo 的物体上的光照,
能与输入图像的亮度紧密匹配。 lighting demodulation loss 强制学习到的光照与训练数据中观察到的光照条件保持一致.
这可以被视为一种 bias, 用于解决 appearance 和 shading 之间的 ambiguity.

Mesh Extraction and Refinement

为了从 triplane 中提取 mesh, 作者使用了 DMTet. 作者提出了两个 MLP head 来预测 vertex offsets 和 vertex normals. 这里受 MeshLRM 启发, 作者也单独使用了分离的 decoder MLP 来辅助这两个 head 的训练.
作者发现, vertex offset 能够反走样, 而 vertex normal 则能提升细节表现. 鉴于一开始 normal map 的预测不会太准确, 于是作者使用了 slerp 来稳定训练, 这是在一开始的 5K step 里发生.

然后引入了各种 loss 来训练这个 mesh extraction and refinement 模块:

  • $$\mathcal{L}_{\text{Nrmconsistency}}$$: 法线一致性损失
  • $$\mathcal{L}_{\text{Laplacian}}$$: Laplacian 平滑损失
  • $$\mathcal{L}_{\text{Offset}} = v_o^2$$: 顶点偏移正则化
  • $$\mathcal{L}_{\text{Nrmrepl}} = 1 - n \cdot \hat{n}$$: 法线复制损失
  • $$\mathcal{L}_{\text{Nrmsmooth}} = (\hat{n}(x) - \hat{n}(x + \epsilon))^2$$: 法线平滑损失

UV Unwrapping and Export

SF3D 模型的最终阶段是一个高效的导出流水线, 关键挑战在于传统 UV 展开的计算密集性, 这不符合快速生成的要求. 为此, 作者提出了一个基于立方体投影的展开方法. 该方法利用网格面法线独立决定投影方向, 实现了可并行化的展开过程.
具体实现上, 该方法执行 2D 三角形-三角形相交测试来处理 UV 图集中的遮挡, 并根据深度和接近度对相交面进行重新分配. 同时, 通过遵循径向 切线方向旋转 UV 岛以最小化阴影接缝. 接着, 通过 UV 展开将世界坐标和占用率烘焙到 UV 图集上
, 用于从 triplane 中查询反照率和表面法线. 为防止接缝伪影, 作者采用了一个迭代过程, 使用 部分卷积和最大池化来扩展 UV 边界, 确保纹理平滑向外混合.

之后, 作者将所有文件作为 glb 格式导出.

Overall Training and Loss Functions

由于直接在网格渲染任务上训练方法会产生不满意的结果, 作者首先在 NeRF 任务上进行了预训练. 完成预训练后, 模型过渡到网格训练,
将 NeRF 渲染替换为 differentiable mesh rendering 和基于 SG 的着色.

分步的损失函数如下所示:\begin{split}\mathcal{L}<em>{\rm render}&=\underbrace{ \lambda</em>{\rm MSE}}<em>{ 1 0}\mathcal{L}</em>{\rm MSE}+\underbrace{ \lambda_{\rm LPIPS}}<em>{ 2}\mathcal{L}</em>{\rm LPIPS}+\underbrace{\lambda_{ \rm Mask}}<em>{ 1 0}\mathcal{L}</em>{\rm Mask}\ \mathcal{L}<em>{\rm mesh}&=\underbrace{\lambda</em>{\rm Laplacian }}<em>{ 0.01}\mathcal{L}</em>{\rm Laplacian}+\underbrace{\lambda_{\rm Nrm Consistency}}<em>{ 0.001}\mathcal{L}</em>{\rm Nrm consistency}+\underbrace{\lambda_{\rm Offset}}<em>{ 0.1}\mathcal{L}</em>{\rm Offset}\ \mathcal{L}<em>{\rm shading}&=\underbrace{\lambda</em>{\rm Nrm repl}}<em>{ 0.2}\mathcal{L}</em>{\rm Nrm repl}\underbrace{\lambda_{\rm Nrm smooth}}<em>{ 0.02}\mathcal{L}</em>{\rm Nrm smooth}+\underbrace{\lambda_{\rm Demod}}<em>{ 0.01}\mathcal{L}</em>{\rm Demod}\end{split}
总损失为:

Results

作者在 GSO 和 OminiObject3D 数据集上对 SF3D 进行了评估. 结果如下图所示:
结果图
可以看到, SF3D 在视觉效果上明显优于其他方法, 并且在数值指标上也有显著提升.

在速度方面, 确实如作者所说, SF3D 的 UV 展开非常快, 只需 0.5s, 远快于其他方法的 10-30s.
速度对比

Conclusion

因此, 我似乎大致总结完了 SF3D 的主要结构, 从一张图像生成高质量的 mesh, 能不能对视频进行这样的操作呢? 我们看到这个任务里实际上用了大量生成的先验知识, 我在想一个完全
基于 image 的 3D reconstruction 方法, 能不能做到不依赖于这些先验知识?

SLAM Former 阅读

引言

最近几天读了SLAM-Former: Putting SLAM into One Transformer这篇很近很近的工作,本文笔记记录用于后续翻阅学习

首先, SLAM-Former 与之前读到的所有论文相似,都是致力于从 RGB 图像序列中恢复三维场景结构和相机位姿等属性的工作。但是与之前的工作(包含一个冗长复杂的 pipeline )不同,
SLAM-Former 对已有的 transformer 架构进行了大胆的改进,使之更适合进行重建任务,并在实验中得到了 competitive 的结果。

模型结构

SLAM-Former架构图

据作者所述, SLAM-Former 的主要 pipeline 由 frontend 和 backend 两部分组成,至于模型的 backbone , SLAM-Former 建立在一个 Transformer 架构之上,
而这个 Transformer aggregate 了 intraframe 和 interframe 的信息,并使用 task specific heads 预测不同的三维属性。
值得注意的是, 这个 Transformer 的输入与类似,对所有的输入的 image token 共享一个相同的 register tokens
从而使模型不依赖于一个不稳定的 reference frame 。

模型的 backbone 包含了层组合了 intra-frame attention 和 inter-frame attention
来联合捕捉图像内容和图像之间的关系。

此外, Front end 部分负责增量式的逐帧重建, back end 负责全局的点云对齐和相机优化,他们共享一个
Transformer backbone 。

Front end

图中大部分内容都是 front end 的处理细节,当一个新的 frame 输入时, frontend 首先会
决定其是否为 keyframe ,如果是的话,则会进行进一步处理。

当给定一个 frame sequence 时, frontend 将每一个 frame 映射到一个 map token 集合中:
这里, ${C_k}{K\in S}$表示之前 keyframe 的KV cache
代表着 keyframe 的索引集合,是当前 frame 的 map token, 作为该 frame 的
一个隐式神经表示。 同时新的 KV cache 也通过$C_t = Cache(f(\mathbb{F}t))$产生,
也会视情况被扩充到${C_k}
{K\in S}$中。

Keyframe detection

在上一步中我们已经对当前帧 generated 了 map token ,接下来我们需要决定是否为 keyframe.

作者采用了 pose head 来预测当前帧的 pose :

当当前 frame 的 relative pose 与最近的 keyframe 的 pose 之间的差异大于一个阈值时,
则将当前 frame 标记为 keyframe 。

但是作者在论文里又表明,在检测 frame 是否为 keyframe 时,他们并没有依赖 KV cache
, 而是直接应用了来检测,就相当于之前的 KV cache 是将该图片
与所有的 keyframe 进行 attention 计算,而这里则是只与最近的 keyframe 进行 attention 计算。
这样增加了效率并且避免了选取一个特定的 reference frame 。(这里似乎我没怎么懂跟特定的 reference frame 有什么关系)

Front end tracking and mapping

接着上一步,如果一个新的 frame 已经被认为是一个 keyframe ,我们就可以重新利用全部的 KV cache 来重新
计算他的 map token, 并更新 M, S.

好了, front end 到这里差不多结束了,作者说 frontend 只依赖于过去的 keyframe ,
使得其适合于 online 的 tracking ,然而, 这种处理顺序会导致误差累积和局部不一致,
为了解决这一问题,作者引入了一个 back end 模块来进行 global refinement.

Backend

Backend 的主要任务是 refine 所有的 frame 来达到全局的一致性。传统的
SLAM 系统通常会使用 loop closure 和 bundle adjustment 来实现这一点,
但是这些方法都非常的 costly, 作为对比,作者使用了一个 transformer-based 的
back end 来进行全局的优化。

作者认为这个设计的有效性在于 backend transformer 内部的 full attention 机制,
他的全局感受野使得模型能够完成误差纠正和结构一致性。

此外, 为了继承 backend refinement 的优势, frontend 和 backend 共享了 KV cache ,
使得 frontend 能够受益于 backend 的全局优化。

Training Strategy

与以往的一些论文不同, SLAM-Former 的创新点不止在于模型架构,也在于一些训练策略。

作者的目标是使一个 transformer 同时胜任 frontend 和 backend 的任务,为了达到这个目标,
作者用三种模式联合训练,每一个模式都对应着不同的输入输出对。

训练模式图

Training Frontend

Frontend 用了一个 causal mask 来确保每一个 frame 只能访问之前的 keyframe 。

然而,纯净的使用 causal mask 会自动的将第一帧作为 reference frame ,
作者又注意到党对两帧或更多帧进行联合操作时,没有单一的 refernce frame,
这避免了后续帧需要与 reference frame pose 相似的要求。

因此, 作者对前两帧使用了 full attention ,并同时对所有后续 frame 使用 causal mask,
在这种情况下, inference 时, keyframe detection 将最后一帧关键帧和当前的输入帧进行处理,
tracking and mapping 时, 前两个 keyframe 则会联合处理决定全局坐标。

作者的原文是:

For tracking and mapping, the
first two keyframes are jointly processed to determine the
global coordinate.

取前两帧的做法与之前的 tracking and mapping 部分提到的 use full KV cache 不符,
我感觉不怎么理解。

Training Frontend with Backend Cooperation

为了在 frontend 和 backend 之间建立联系,作者使用 maxed attention 来模拟 backend 和
cache sharing 的过程。

具体来说,采用混合注意力在一个统一的正向传播中同时完成地图精炼(后端/全注意力)和新数据处理,
并且前端的 casual attention 并非独立工作,而是以 KV cache 为条件,实现了高效且信息流一致的前端-后端协作,确保前端的实时处理结果能够立即对齐到后端修正后的全局结构。

woc 这什么花式操作啊

Training Backend

作者最后使用 full attention 来训练 backend transformer ,

Joint Training

在所有的三种模式中,三维属性均是由 task specific heads 预测的:

但值得注意的是, 并不像其他的工作一样, SLAM-Former 只预测每一帧的 local
pointmap 来避免设定一个特定的世界坐标系的需求,这倒是与非常相似。

剩下的 loss 函数都比较常规。
这三种模式都会在一个 batch 中共享权重依次训练。

Pipeline

在图片和叙述过程中, pipeline 已经是显而易见的,于是我便不再赘述。

Experimental Setup

本模型有 36 层 framewise 和 global attention 相结合的 transformer layer, 训了 10 个
epoch, 在 32 个 A100 上训练了 11 小时。可以可以。

Results

模型在 pose , tracking 和 reconstruction 等任务上都达到了很好的指标。数据冗长不再多说。
值得一提的是作者对 Front end 和 back end 的联系的理解。

back end assist front end 无疑是显而易见的,但是作者还发现 back end 同样也
benefit from front end, 作者解释了是因为 back end 使用了来自于 frontend 的
implicit 的顺序信息,从而使得 back end 能够更好地理解 frame 之间的关系。(迷)

总结

总之, SLAM-Former 通过对 transformer 架构的改进和训练策略的设计,
成功地实现了一个统一的模型来处理 SLAM 任务。

但 SLAM-Former 仍然存在一些局限性,比如说作者用 full attention 来替代传统的 loop
closure 和 bundle adjustment ,受限于 full attention 的计算复杂度,模型难以处理非常长的序列,
其次, frontend 不支持一个 local 的 inference , 因为在 inference 之前需要将所有的 KV cache 输入到 frontend 中。

此外, 文章中没有提到的是,我去看他们的 demo , 发现重建结果有很明显的分块化现象,目前不知是否与 transformer 的架构有关。
重建结果

此文撰写的时候, SLAM-Former 的代码尚未开源,期待后续的代码发布。

重返vggt

引言

这是本人在学了一些基础知识并做了一些实验之后, 察觉到之前对于一些经典论文的阅读并不充分, 于是决定重新阅读VGGT一文, 并写下这篇文章, 以供后续查阅.

首先, VGGT 是一个完全的前馈式神经网络用于多目重建任务, 通过 look into 他的代码, 可以看到基本上是没有什么 pipeline 的, 直接将图片输入网络, 然后输出各种三维属性, 并在作者的宣称下, 他们所预测的多个指标在存在 BA 的前提下
均达到
子领域的 SOTA 水平, 这一点非常厉害.

模型结构

VGGT 的 backbone 是一个标准的 transformer 结构, 首先接受大量图片作为输入, 首先通过一个 DINO 提取了分块的 feature, 然后将这些 feature 通过一个主体网络结构(包含了 Alternating frame-wise layer 和 global attention layer)
进行处理, 最后通过多个 task-specific heads 输出不同的三维属性.
VGGT架构图
接下来, 我们详细叙述各个细节部分:

Alternating attention frame-wise layer

据文章作者所述, 该 AA 机制与标准的 transformer attention 机制有所不同, 能够使 Transformer 以交替的方式聚焦每一帧和全局.

  • frame wise attention layer: 该层的 attention 仅在同一帧内进行, 也就是说, 每个 patch 只能与同一帧内的其他 patch 进行 attention 计算. 这样做的好处是能够更好地捕捉每一帧内部的局部特征.
  • global attention layer: 该层的 attention 在所有帧之间进行, 也就是说, 每个 patch 可以与所有帧内的其他 patch 进行 attention 计算. 这样做的好处是能够捕捉不同帧之间的全局特征.

另外值得一提的是, 作者采用了层的 AA 机制, 并通过消融实验证明了 AA 机制的有效性, 此外, 作者声称他们的架构并没有采用 cross attention, 只采用 self attention.

任务特定的heads

将输入的图片通过 backbone 网络处理后, 会得到一个全局的 feature 表示, 然后通过多个 task-specific heads 输出不同的三维属性. 值得注意的是, DINO 编码的 feature 并非直接输入到 AA 中, 而是被添加了一个额外的相机 token
和四个 register tokens进行增强, 然后将作为最终的输入.

此处值得注意的是, 第一帧的输入 token 是, 之后的帧的输入 token 是, 也就是说, 第一帧和之后的帧的 camera token 和 register token 是不同的.
但是作者说他们都是 learnable 的. 这使得模型能够将第一帧和其他帧区分开来, 并在第一个相机的坐标系下表示全局点云以及各种数据.但是, 经过 AA 层之后, 本来被赋予同一初值的 camera token 和 register
token 均会变为帧特定的, 这是因为 AA 层的 frame-wise attention layer 会使得每一帧的 token 在不同的计算中产生不同的表示.

最后遵循常规做法, register token 会被丢弃, camera token 和 image token 会被保留用于预测.

Camera parameter head

这个 head 从上图中的模型的 backbone 就可以看到, 他是将 camera token 通过 4 个 self-attention layers 进行处理, 然后通过一个 MLP 预测出每一帧的相机参数(包含内参和外参).

Dense Prediction

输出的 image token 在这里被使用, 用于预测 depth map , point map 和 tracking features . 更具体地来讲, 首先会通过一个 DPT head 转化为一个 dense feature map
, 之后每一个会通过一个的卷积层解析出 corresponding depth 和 point map. 另外, DPT 头同样也会输出 dense feature map 用于后续的 tracking,
在此同时, vggt 同样也会输出 confidence map 用于表示 depth 和 point 的置信度. 这个置信度用于后续的模型的 loss 计算和
真实预测时的 conf 输出.

Tracking

这一方面我并不打算去深入了解, 因此先跳过.

Training

Loss function

VGGT 的 loss function 包含多个部分, 主要包含以下几种:

  • Camera loss: 这个 loss 监管了相机参数$L_{camera} = \sum_{i=1}^{N} ||\hat{g}i - g_i||{\epsilon}$, 使用了 Huber loss.
  • Depth loss: 这个 loss 沿用了 dust3r 的 loss 设计$\mathcal{L}{\mathrm{depth}}=\sum{i=1}^N|\Sigma_i^D\odot(\hat{D}_i-D_i)|+|\Sigma_i^D\odot(\nabla\hat{D}_i-\nabla D_i)|-\alpha\log\Sigma_i^D$
  • Point loss: 这个 loss 同样沿用了 dust3r 的 loss 设计$\mathcal{L}{\mathrm{point}}=\sum{i=1}^N|\Sigma_i^P\odot(\hat{P}_i-P_i)|+|\Sigma_i^P\odot(\nabla\hat{P}_i-\nabla P_i)|-\beta\log\Sigma_i^P$
  • Tracking loss: 这个 loss 监管了 tracking feature 的质量, 具体细节我并不打算深入了解, 因此先跳过.

因此, 最终的 loss function 为:

坐标Normalization

如果缩放的话, 重建结果应该同样也是正确的, 为了消除这种不确定性, 作者采用了归一化进行处理. 首先将所有量表示在第一个相机的坐标系中, 然后计算所有点的平均欧氏距离, 然后利用该尺度归一化相机平移, 点云坐标和深度值.

值得注意的是, 作者没有对预测结果施加任何归一化, 相反强制模型去学习预测归一化后的值, 这样做的好处是能够使得模型更好地适应不同尺度的场景.

Details

我难以想象训练的规模, 按照作者所述, 这一个 transformer 模型包含了的参数, 在 64 块 A100 上训练了 9 天, 属实是第一次见了.

另外, 训练的数据集之多也是难以想象:
dsfa
有点离谱了.

结论

vggt 的指标基本上达到 SOTA 水平, 但是值得注意的是, 直接的输出并没有达到, 作者加入了 BA 优化之后才达到了 SOTA, 因为 BA 是一个 costly 的优化过程, 因此我觉着这一方面或许还可以改进? 作者在论文中提到了
应用 diffentiable BA 的可行性, 但是也因为 BA 的计算量过大, 因此并没有进行进一步的尝试.

此外, VGGT 向我们展示了不需要一个复杂的 pipeline 也可以进行高质量的多目重建说你呢, SLAM3R, 我 TM 的快改吐了, 再结合最近发布的 SLAM Former, 我觉着这是一个很有意义的方向.

非常重要的是, vggt 证明了联合预测多个任务是有益的, 虽然并没有在 loss 阶段进行互相的监督, 但是通过多个任务的单独监督, 使得模型学到了更好的表示,

此外, vggt 另一个重要的发现是, 通过 depth 和 pose 反解出来的点云比直接预测的点云要好.

ok, 让我们把仓库链接抬出来:

另外, 这是真的可以的嘛?

iasdf

论文阅读记录:Fast3R

引言

OK, 本人昨天又读了一篇 3D reconstruction 方向的论文:Fast3R: Towards 3D Reconstruction of 1000+ Images in One Forward Pass,因此写下此篇 Blog 分享自己的理解与发现。

Fast3R 从本质上来说感觉和 SLAM3R 解决的是一类问题,都是对原本 DUst3R 存在的局限性:一次只能对两张图片进行处理,如果对多张图片进行处理的话, DUst3R 则是选择进行两两配对进行重建,最后进行全局坐标下的对齐,显然这将会是一个
的过程。而 Fast3R 提出了对于打乱序列的多张图片( 1000+)的处理方法, SLAM3R 则是解决了由视频进行重建的方法。感觉两者的本质上的区别就是 input 的图像集是否有序,后续两者的网络结构区别也正是在此。

从论文的 introduction 上来看,他们主要做了以下三方面的贡献:

  • 创建了 Fast3R ,一个基于 Transformer 的对多目图片重建点图的端到端的模型,据论文所述,它在速度上取得显著提升,并且可以规模化计算。
  • 展示了随着训练时视角增多,模型表现也会加强。另外,当推理时视角增多时,每张视角重建结果的精确度也会提升。并且模型可以处理比训练时多得多的模型。
  • 在相机的位姿定位上达到了SOTA水平,另外也展现出了极快的速度。

好的,现在到了我们喜闻乐见的介绍模型环节啦!

模型

Fast3R 给出了一个看起来在推理环境就很庞大的结构图:
Fast3R

问题定义

从图中右边就可以看到, Fast3R 采用了两个头: Global Head 和 Local Head 来处理输出的 token ,因此可见, Fast3R 为每张图片预测了两个点图:本地坐标系下的点图和全局坐标系下的点图,可以用公式表示:
指代的是点图的置信度。

值得注意的是,全局坐标系值得是第一张图片的坐标系,本地坐标系是每个对应图片的坐标系。(虽然 Fast3R 并没有次序的概念,但其也需要一个切入点,所以随机选取了一张图片作为第一张图片)

训练对象

类似于 Dust3R , Fast3R 的损失函数分别采用了同样的处理方法处理本地点图和全局点图两部分:
阅读其论文,发现其与 Dust3R 的损失函数基本一致,因此不多赘述。

模型架构

Image Encoder

由上图所示,我们可以看到每一个输入的图片都会经过一个共享权重的 Vit Encoder 生成对应的 token 序列 ,即:
论文中提到,他们使用了和 Dust3R 相同的 Encoder : CroCo ViT ,但是他们提到了 DINOv2 的表现与之相似。

另外,在把 token 传入 fusion transformer 之前,作者为每一个 token 添加了一个一维的位置编码,目的是让模型知道哪些图像块来自于同一张图片,并且帮助模型认出上文标定的第一张图片。这同样也能让模型隐式地去理解这些图片里反映的相机位姿。

Fusion Transformer

模型中大多数计算都发生在 Fusion Transformer 里面,作者使用了一个类似于ViT-L的 24 层的 transformer 作为这一模块的主体。它将来自所有的视角的 token 作为输入,并且通过全连接的自注意力机制进行处理,使的模型能够理解所有视角的信息,远超 Dust3R 能理解的两个视角的信息。

Pointmap Decoding Heads

最后, Fast3R 使用了两个独立的 DPT 解码头将 Fusion Transformer 的输出解码为点图,即图片中右边部分。

位置编码

论文最后的目标是进行多图片处理,并且实现推理时的可以处理的图片数量远远多于训练时的图片数量,因此我们就要考虑推理时为 token 嵌入位置编码的手段。

  • 一开始,文章尝试使用相同的球谐函数嵌入编码,文章中又提到:在 LLM 中,这种方法导致性能不佳。果不其然,在文章的初步实现中,他们同样发现当输入图像数量超过训练时使用图像的数量时,模型的效果并不好。
  • 因此,文章借鉴了大预言模型中的位置插值方法:在训练时从一个集合均匀随机抽取个索引,这样模型便被迫去学习处理更大范围的索引。

对于 transformer 来说,这种策略感觉和 masking 没什么区别,文章中也说:

This strategy enables Fast3R to handle N = 1000 images during inference, even if only trained with N = 20 images.

有效利用显存

从模型架构的图片来看,这看起来就是一个占用很大显存的模型。但是文章提出,由于模型的特点( meta-architecture ),这个模型可以广泛使用各种并行化以及分片技术。
文章提出他们在训练和推理的时候利用了两种不同形式的并行化和 FlashAttention 技术,并认为随着未来的技术成熟他们的模型会持续受益(废话)。

具体采用的策略来实现高效训练。

首先,使用 FlashAttention 来提高时间和内存效率。即便如此,当 N>16 时,一个朴素的实现即使在批量大小为 1 的情况下也会耗尽内存( 128 x A100-80GB 啊,离大谱)。
因此,后来使用了 DeepSpeed ZeRO stage 2 训练,将优化器状态、动量估计和梯度在不同的机器上进行分区。这样就能够以每个数据样本最多 N=28 个视角进行训练,同时每个 GPU 的批量大小为 1 。

模型效果:

miaomiao
就模型所给出的表格而言,确实是达到了 Sota 水平。

在推理速度上,由于所做的各种优化,它也得到了显著的提升。

但是,其实我更好奇的是它跟同期的 SLAM3R 的性能比较,阅读论文,发现两者并没有过同一个精度指标的比较,通过本人的本地测试,发现对于一个很小的数据集( 82 张有序图片),两者速度上并没有太多差距,但是重建质量上来说
, SLAM3R 的质量远超 Fast3R 。这很好的符合了 SLAM3R 对有序图像序列进行针对性重建的特性,而 fast3R 是对一个随机图像重建的方法。

所以,当我看到 Fast3R 的 demo 里有对视频重建的选项时,我感觉并不适合。因为从直觉上来说,人们从一个没有次序的图像集中理解环境的过程也大致遵循一个先排序再重建的过程,也就是说人们对无次序的图片集中还原 3D 场景的难度远大于从视频中还原场景的难度。

论文中也提到了局限性的存在:

  • 缺少包含大型场景的数据因而缺少在此类场景下的泛化能力。
  • 没有更好的位置嵌入,不过论文提出可以参考那些能处理极长上下文序列的大语言模型。

ok ,关于 Fast3R 我就处理到这里,欸,我觉着或许我以后应该认真去看看训练细节和实验部分,总去看模型结构有种高屋建瓴的感觉,还是应该多看看代码( x

论文阅读记录:MAst3R

引言

经过一周的对SLAM3R进行 online 以及可视化 demo 改造的低效率劳作且工作完成,我终于有时间来补档我这篇早在近两个周之前就读完的论文Grounding Image Matching in 3D with MASt3R

读完这篇论文之后,我的第一感觉就是:这是一个 DUst3R 的修补模型,他并没有太多的像 DUst3R 那样的开创性地将 transformer 运用于双目三维重建那样的举动,而是在 DUst3R 模型上进行了
少许修补,并提出了少许修补中的一些独创性方法,感觉是一篇介绍 small trick 的论文。同时,我们似乎也可以这么说: MAst3R 发现本聚焦于三维重建任务的 DUst3R 在像素匹配问题上同样达到了 SOTA
于是, MAst3R 将 DUst3R 稍加改造,得到了一个在像素匹配上表现更强的模型 MAst3R.

模型介绍

MASt3R 的模型结构与 Dust3R 大致相同:
mast3r

Encoder

与 DUst3R 相同, MAst3R 的 encoder 部分同样是由 ViT 组成的,且与 DUst3R 相同的是, MAst3R 的 encoder 部分也是共享权重的。
就像这样:

Decoder

MASt3R 的 Decoder 同样采用了 cross-attention 的机制,这能使得 MAst3R 能够理解同一像素在不同视角下的信息,有助于后续进行像素匹配。

Heads

对于 Dust3R 来说,他只有一个 head ,直接将 decoder 的输出转化为点图信息和置信度(上图灰色部分)

3D Heads

MASt3R 对这个 head 基本上与 DUst3R 的 head 相同,都是将 decoder 的输出转化为点图信息和置信度。

Matching Heads

MASt3R 在此基础上又增加了一个 head ,专门用于像素匹配任务(上图蓝色部分),这个头部由一个简单的两层的 MLP 组成,使用了 GELU 作为激活函数,另外在处理完后进行归一化处理,负责输出两张密集的特征图:

Loss

Mast3R 的损失函数由两部分组成:

3D Loss

MAst3R 的 3D Loss 与 DUst3R 的 3D Loss 基本相同,都是由点图的 L1 损失和置信度的交叉熵损失组成。
但是, MAst3R 在计算回归损失的时候,原本的 DUst3R 计算公式是这样的:
MAst3R 认为在它的应用场景中,并不鼓励尺度不变性,而更多的是需要绝对的尺度一致性,因此 MAst3R 将上式改为了:
因此, MAst3R 的 3D Loss 计算公式为:

Matching Loss

这个损失函数是对 Matching Head 输出的特征图进行监督的,基本思想是:我们鼓励一个图像中的一个特征匹配符,最多与另一张图像中代表同一个 3D 点的特征匹配符进行匹配,
需要注意的是,这个匹配本质上是一个交叉熵分类损失,当网络猜到正确的像素(而非邻近的像素)时,才会得到奖励。

具体实现上,我们利用了 InfoNCE loss 来实现这个想法,其作用于一组对应关系,具体公式如下:
其中,是一个温度参数,分别是图像 1 和图像 2 中所有像素的集合。

这极大地鼓励了网络进行高精度匹配。

最后,两个损失函数被结合起来,形成了 MAst3R 的总损失函数:
有了上述模型与 Loss 就可以训练了,但是网络的输出还需要经过一些处理,才能得到需要的匹配关系。注意,网络只输出了 PointMap 和每个像素的 LocalFeature ,而期望得到的是两个图像之间的像素点级别的匹配,匹配相关的部分就是图中新增的 NN 模块。

快速互惠匹配

当给定两张特定的预测图时,我们的目标是提取一组可靠的像素对应关系,即互惠最近邻。

数学定义:

  • 互惠最近邻集合由公式定义:̲</li> <li>这里的$…" style="color:#cc0000">\mathcal{M}={(i,j)|j=\mathrm{NN}_2(D_i^1)\mathrm{<del>and</del>}i=\mathrm{NN}_1(D_j^2)} $$</li> <li>这里的表示在特征图中与特征距离最近的特征的索引。其数学定义为:
    \mathrm{NN}_A(D_j^B)=\arg\min_i|D_i^A-D_j^B|</li> </ul> <h3 id="传统方法"><a href="#传统方法" class="headerlink" title="传统方法"></a>传统方法</h3><p>传统上,计算互惠最近邻的方法是通过暴力搜索来实现的,这种方法的时间复杂度为,这在高分辨率图像中是不可行的。</p> <p>虽然优化最近邻搜索是可能的,例如使用 <strong>K-d</strong> 树,但这种优化在高维特征空间中通常会变得非常低效,在某些情况下,其速度甚至比 MASt3R 输出的推理时间慢几个数量级。</p> <h3 id="MASt3R的方法"><a href="#MASt3R的方法" class="headerlink" title="MASt3R的方法"></a>MASt3R的方法</h3><p>MASt3R 提出了一种基于<strong>子采样</strong>*的快速方法。</p> <p>这个方法是从一个稀疏的第一张图片的像素集合出发的,通过找到这个集合中每个像素在第二张图片上的最近邻得到最近邻集合,然后再从这个最近邻集合中找到每个像素在第一张图片上的最近邻,最后通过检查互惠性来得到最终的互惠最近邻集合。</p> <p>整个过程可以表示为:
    U^t\mapsto[\mathrm{NN}2(D_u^1)]{u\in U^t}=V^t\mapsto[\mathrm{NN}1(D_v^2)]{v\in V^t}=U^{t+1}</p> <ul> <li>当 时,这些像素形成了一个闭环,并被收集为一组<strong>互惠匹配</strong> 。</li> <li>对于下一次迭代,那些已经收敛的像素(即 )会被过滤掉,新的 更新为 。</li> <li>这个过程会迭代固定的次数,直到所有的对应关系都收敛到稳定的(互惠)对为止。</li> <li>最终的输出对应关系集合 由所有互惠匹配集合的拼接而成:。</li> </ul> <p>这种快速匹配算法的总体复杂度大概是,相比朴素方法的,有了显著的提升。 <img src="/blog/mst3r/chart.png" alt="chart"></p> <p>具体证明过程可以参考论文的附录部分。</p> <h2 id="个人总结"><a href="#个人总结" class="headerlink" title="个人总结"></a>个人总结</h2><p>MAst3R 这篇论文的阅读,本人自己对 mast3r 的理解,以及对 transformer 在三维重建任务中应用的理解,基本上就到这里了,当然, mast3r 的实验部分我并没有过多地去阅读,因为我觉得 mast3r 的实验部分并没有太多的创新性,基本上都是在验证 mast3r 在各个任务上都达到了 SOTA 的水平。 我个人觉得 mast3r 的创新点主要有以下几点:</p> <ol> <li>在 DUst3R 的基础上,增加了一个匹配头,用于像素匹配任务,这个头部的设计比较简单,但是效果却非常好。</li> <li>在 3D 损失函数中,改变了点图回归损失的计算方式,使其更加适合绝对尺度一致性的任务。</li> <li>提出了一个快速的互惠匹配算法,大大提升了匹配的效率。 总的来说, MAst3R 是一篇比较实用的论文,通过一些小的改动和创新,使得模型在多个任务上都达到了 SOTA 的水平,值得学习和借鉴。</li> </ol> <p>另外, MAst3R 的代码也已经开源:</p> <p>喵喵补坑完毕,虽然感觉说了和没说一样😭</p> </div></article></div><div class="card"><article class="card-content article" role="article"><div class="article-meta is-size-7 is-uppercase level is-mobile"><div class="level-left"><span class="level-item">Posted&nbsp;<time dateTime="2025-08-14T12:04:00.000Z" title="8/14/2025, 12:04:00 PM">Aug 14, 2025</time></span><span class="level-item">Updated&nbsp;<time dateTime="2025-08-14T12:04:00.000Z" title="8/14/2025, 12:04:00 PM">Aug 14, 2025</time></span><span class="level-item"><a class="link-muted" href="/categories/blog/">blog</a></span><span class="level-item">4 minutes read (About 641 words)</span></div></div><p class="title is-3 is-size-4-mobile"><a class="link-muted" href="/blog/VGGT/">VGGT读后有感</a></p><div class="content"><h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>继写完<strong>SLAM3R</strong>的 onlinee 处理后,我又将目光投向了今年 CVPR 的最佳论文:<a target="_blank" rel="noopener" href="https://github.com/facebookresearch/vggt">VGGT:Visual Geometry Grounded Transformer</a> 不要问我研究 3R 为什么不先看 vggt😂, 问就是我太摆了一开始懒得看了。</p> <p><strong>VGGT</strong>主要介绍了一个离线的多视图重建,位姿估计和轨迹追踪的强大的模型,与之前类似于<em>SfM</em>、<strong>DUst3R</strong>的重建方法相比,它的先进之处在于:</p> <ul> <li>摆脱了这些方法所依赖的昂贵的后处理过程(而这通常没有计入到之前模型的性能评估中)</li> <li>将多个任务:深度估计、位姿估计、视图重建、轨迹追踪等全部输出,表现甚至超过了之前单一领域的<strong>SOTA</strong>方法。</li> <li>在将多个任务的结果全部输出的过程中,作者发现了引入不同结果之间的内在数学联系限制后会大幅提高模型的性能。</li> </ul> <h2 id="项目架构"><a href="#项目架构" class="headerlink" title="项目架构"></a>项目架构</h2><p>与之前的模块化解决问题不同,<strong>VGGT</strong>的主要结构是一个大的 Transformer ,它接受一个图片集作为输入,然后输出场景图片的不同三维属性。</p> <p>值得一提的是,它所能解决的多视角三维属性几乎涵盖了三维视觉的方方面面:</p> <ul> <li>相机位姿以及内参</li> <li>点图重建</li> <li>关键区域追踪</li> <li>关于单张图片的深度图</li> </ul> <p>并且, VGGT 通过更加创新的举动,它将输出的多任务成果的内在几何关系作为归纳偏置整合进了模型,并发现了大幅度的性能提升,这个很值得去研究。</p> <h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>感觉<strong>VGGT</strong>就是一个巨大的 transformer ,通过极其暴力的手段解决问题,客观上来说,这确实展示了 transformer 在三维重建领域的应用,但其实我是有一些疑问的: 像自然语言处理这种工作,它是无法定量化去研究的,所以我们引入了 transformer ,似乎是用未知对抗不确定性的手段,但是,在这个三维重建这个领域,它真的有那么多不确定性吗? 还是感觉 transformer 对于三维重建的成果属于是结果能看,但是要达到更高的精度会让人很迷惑。</p> </div></article></div><div class="card"><article class="card-content article" role="article"><div class="article-meta is-size-7 is-uppercase level is-mobile"><div class="level-left"><span class="level-item">Posted&nbsp;<time dateTime="2025-08-12T07:57:00.000Z" title="8/12/2025, 7:57:00 AM">Aug 12, 2025</time></span><span class="level-item">Updated&nbsp;<time dateTime="2025-08-12T07:57:00.000Z" title="8/12/2025, 7:57:00 AM">Aug 12, 2025</time></span><span class="level-item"><a class="link-muted" href="/categories/blog/">blog</a></span><span class="level-item">37 minutes read (About 5476 words)</span></div></div><p class="title is-3 is-size-4-mobile"><a class="link-muted" href="/blog/SLAM3R_online%20edit/">为SLAM3R补充实时处理函数方法</a></p><div class="content"><p>在上个周阅读<strong>SLAM3R</strong>论文结束后,学长让我去看一下它的<a target="_blank" rel="noopener" href="https://github.com/PKU-VCL-3DV/SLAM3R">源代码</a>,读完代码之后,发现虽然论文里讲述的是“可以实时重建”,但是实际上在<code>recon.py</code>文件中的<code>scene_recon_pipeline</code>函数中,代码采取了先对所有<code>input_views</code>进行输入到<code>i2p_model</code>得到<code>res_feats</code>,然后再将所有图片的 token 输入到 l2w 网络中进行重建的大致逻辑。</p> <p>显然,这样的处理方法不是论文里所提出的<strong>online</strong>处理方法,因此,在过去的一个周里,本人一边练着科三显然今天上午刚挂掉,该死的直线行驶😡,同时抽出了一点点时间完成了<code>recon_online.py</code>, 一个把原本的<code>scene_recon_pipeline</code>改成<code>online</code>处理的改动。</p> <h2 id="原函数的处理逻辑"><a href="#原函数的处理逻辑" class="headerlink" title="原函数的处理逻辑"></a>原函数的处理逻辑</h2><p>阅读原函数的代码,我们可以将其分为以下几段:</p> <h3 id="预处理-得到所有view的token"><a href="#预处理-得到所有view的token" class="headerlink" title="预处理&得到所有view的token"></a>预处理&得到所有view的token</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> <span class="line">9</span> <span class="line">10</span> <span class="line">11</span> <span class="line">12</span> <span class="line">13</span> <span class="line">14</span> <span class="line">15</span> <span class="line">16</span> <span class="line">17</span> <span class="line">18</span> <span class="line">19</span> <span class="line">20</span> <span class="line">21</span> <span class="line">22</span> <span class="line">23</span> <span class="line">24</span> </pre></td><td class="code"><pre><span class="line"><span class="comment"># Pre-save the RGB images along with their corresponding masks </span></span> <span class="line"><span class="comment"># in preparation for visualization at last.</span></span> <span class="line">rgb_imgs = []</span> <span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(data_views)): </span> <span class="line"> <span class="keyword">if</span> data_views[i][<span class="string">&#x27; img&#x27; </span>].shape[<span class="number">0</span>] == <span class="number">1</span>: </span> <span class="line"> data_views[i][<span class="string">&#x27; img&#x27; </span>] = data_views[i][<span class="string">&#x27; img&#x27; </span>][<span class="number">0</span>] </span> <span class="line"> rgb_imgs.append(transform_img(<span class="built_in">dict</span>(img=data_views[i][<span class="string">&#x27; img&#x27; </span>][<span class="literal">None</span>]))[..., : :-<span class="number">1</span>])</span> <span class="line"><span class="keyword">if</span> <span class="string">&#x27; valid_mask&#x27; </span> <span class="keyword">not</span> <span class="keyword">in</span> data_views[<span class="number">0</span>]: </span> <span class="line"> valid_masks = <span class="literal">None</span></span> <span class="line"><span class="keyword">else</span>: </span> <span class="line"> valid_masks = [view[<span class="string">&#x27; valid_mask&#x27; </span>] <span class="keyword">for</span> view <span class="keyword">in</span> data_views] </span> <span class="line"></span> <span class="line"><span class="comment">#preprocess data for extracting their img tokens with encoder</span></span> <span class="line"><span class="keyword">for</span> view <span class="keyword">in</span> data_views: </span> <span class="line"> view[<span class="string">&#x27; img&#x27; </span>] = torch.tensor(view[<span class="string">&#x27; img&#x27; </span>][<span class="literal">None</span>])</span> <span class="line"> view[<span class="string">&#x27; true_shape&#x27; </span>] = torch.tensor(view[<span class="string">&#x27; true_shape&#x27; </span>][<span class="literal">None</span>])</span> <span class="line"> <span class="keyword">for</span> key <span class="keyword">in</span> [<span class="string">&#x27; valid_mask&#x27; </span>, <span class="string">&#x27; pts3d_cam&#x27; </span>, <span class="string">&#x27; pts3d&#x27; </span>]: </span> <span class="line"> <span class="keyword">if</span> key <span class="keyword">in</span> view: </span> <span class="line"> <span class="keyword">del</span> view[key]</span> <span class="line"> to_device(view, device=args.device)</span> <span class="line"><span class="comment"># pre-extract img tokens by encoder, which can be reused </span></span> <span class="line"><span class="comment"># in the following inference by both i2p and l2w models</span></span> <span class="line">res_shapes, res_feats, res_poses = get_img_tokens(data_views, i2p_model) <span class="comment"># 300+fps</span></span> <span class="line"><span class="built_in">print</span>(<span class="string">&#x27; finish pre-extracting img tokens&#x27; </span>)</span> </pre></td></tr></table></figure> <p>这里重点就是最后的<code>res_shapes, res_feats, res_poses = get_img_tokens(data_views, i2p_model)</code>,采用<code>i2p_model</code>的<code>_encode_multiview</code>方法批次化地(<em>batchify</em>)对<code>data_views</code>进行处理,从而得到所有的 view 的<code>token</code>。</p> <h3 id="对所有view进行推理得到最合适的key-frame-stride"><a href="#对所有view进行推理得到最合适的key-frame-stride" class="headerlink" title="对所有view进行推理得到最合适的key_frame_stride"></a>对所有view进行推理得到最合适的key_frame_stride</h3><p>这里的核心代码就是:</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> <span class="line">9</span> </pre></td><td class="code"><pre><span class="line"><span class="comment"># decide the stride of sampling keyframes, as well as other related parameters</span></span> <span class="line"><span class="keyword">if</span> args.keyframe_stride == -<span class="number">1</span>: </span> <span class="line"> kf_stride = adapt_keyframe_stride(input_views, i2p_model, </span> <span class="line"> win_r = <span class="number">3</span>, </span> <span class="line"> adapt_min=args.keyframe_adapt_min, </span> <span class="line"> adapt_max=args.keyframe_adapt_max, </span> <span class="line"> adapt_stride=args.keyframe_adapt_stride)</span> <span class="line"><span class="keyword">else</span>: </span> <span class="line"> kf_stride = args.keyframe_stride</span> </pre></td></tr></table></figure> <p>其中,<code>adapt_keyframe_stride</code>函数是一个典型的<strong>offline</strong>处理函数,它的功能是在所有的 input_view 中遍历可能的<code>kf_stride</code>取值,然后对每一个可能的取值随机取样,然后利用<code>i2p_inference_batch</code>函数得出置信度作为相似度?然后选取最高的所对应的<code>kf_stride</code>作为最优的取值。</p> <h3 id="使用初始的几个滑动窗口创建初始的全局scene-初始化buffer-set"><a href="#使用初始的几个滑动窗口创建初始的全局scene-初始化buffer-set" class="headerlink" title="使用初始的几个滑动窗口创建初始的全局scene&初始化buffer set"></a>使用初始的几个滑动窗口创建初始的全局scene&初始化buffer set</h3><p>因为<strong>SLAM3R</strong>初始化时的<a target="_blank" rel="noopener" href="http://localhost:4321/blog/slam3r/slam3r">特殊性</a>:</p> <blockquote> <p>对于第一个帧这种特殊情况,我们采用了重复运行多次 I2P 获取足够多数量的初始帧作为缓冲集</p> </blockquote> <p>在原本的 offline 格式的<code>recon.py</code>中,这种做法以这种样式呈现:</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> <span class="line">9</span> <span class="line">10</span> <span class="line">11</span> <span class="line">12</span> <span class="line">13</span> <span class="line">14</span> <span class="line">15</span> <span class="line">16</span> <span class="line">17</span> <span class="line">18</span> <span class="line">19</span> <span class="line">20</span> <span class="line">21</span> <span class="line">22</span> <span class="line">23</span> <span class="line">24</span> <span class="line">25</span> <span class="line">26</span> <span class="line">27</span> <span class="line">28</span> <span class="line">29</span> <span class="line">30</span> <span class="line">31</span> <span class="line">32</span> <span class="line">33</span> <span class="line">34</span> </pre></td><td class="code"><pre><span class="line">initial_pcds, initial_confs, init_ref_id = initialize_scene(input_views[: initial_winsize*kf_stride: kf_stride], </span> <span class="line"> i2p_model, </span> <span class="line"> winsize=initial_winsize, </span> <span class="line"> return_ref_id=<span class="literal">True</span>) <span class="comment"># 5*(1,224,224,3)</span></span> <span class="line"></span> <span class="line"><span class="comment"># start reconstrution of the whole scene</span></span> <span class="line">init_num = <span class="built_in">len</span>(initial_pcds)</span> <span class="line">per_frame_res = <span class="built_in">dict</span>(i2p_pcds=[], i2p_confs=[], l2w_pcds=[], l2w_confs=[])</span> <span class="line"><span class="keyword">for</span> key <span class="keyword">in</span> per_frame_res: </span> <span class="line"> per_frame_res[key] = [<span class="literal">None</span> <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(num_views)]</span> <span class="line"></span> <span class="line">registered_confs_mean = [_ <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(num_views)]</span> <span class="line"></span> <span class="line"><span class="comment"># set up the world coordinates with the initial window</span></span> <span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(init_num): </span> <span class="line"> per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][i*kf_stride] = initial_confs[i][<span class="number">0</span>].to(args.device) <span class="comment"># 224,224</span></span> <span class="line"> registered_confs_mean[i*kf_stride] = per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][i*kf_stride].mean().cpu()</span> <span class="line"></span> <span class="line"><span class="comment"># initialize the buffering set with the initial window</span></span> <span class="line"><span class="keyword">assert</span> args.buffer_size < = <span class="number">0</span> <span class="keyword">or</span> args.buffer_size > = init_num </span> <span class="line">buffering_set_ids = [i*kf_stride <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(init_num)]</span> <span class="line"></span> <span class="line"><span class="comment"># set up the world coordinates with frames in the initial window</span></span> <span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(init_num): </span> <span class="line"> input_views[i*kf_stride][<span class="string">&#x27; pts3d_world&#x27; </span>] = initial_pcds[i]</span> <span class="line"> </span> <span class="line">initial_valid_masks = [conf > conf_thres_i2p <span class="keyword">for</span> conf <span class="keyword">in</span> initial_confs] <span class="comment"># 1,224,224</span></span> <span class="line">normed_pts = normalize_views([view[<span class="string">&#x27; pts3d_world&#x27; </span>] <span class="keyword">for</span> view <span class="keyword">in</span> input_views[: init_num*kf_stride: kf_stride]], </span> <span class="line"> initial_valid_masks)</span> <span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(init_num): </span> <span class="line"> input_views[i*kf_stride][<span class="string">&#x27; pts3d_world&#x27; </span>] = normed_pts[i]</span> <span class="line"> <span class="comment"># filter out points with low confidence</span></span> <span class="line"> input_views[i*kf_stride][<span class="string">&#x27; pts3d_world&#x27; </span>][~initial_valid_masks[i]] = <span class="number">0</span> </span> <span class="line"> per_frame_res[<span class="string">&#x27; l2w_pcds&#x27; </span>][i*kf_stride] = normed_pts[i] <span class="comment"># 224,224,3</span></span> </pre></td></tr></table></figure> <p>其中,</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> </pre></td><td class="code"><pre><span class="line">initial_pcds, initial_confs, init_ref_id = initialize_scene(input_views[: initial_winsize*kf_stride: kf_stride], </span> <span class="line"> i2p_model, </span> <span class="line"> winsize=initial_winsize, </span> <span class="line"> return_ref_id=<span class="literal">True</span>) <span class="comment"># 5*(1,224,224,3)</span></span> </pre></td></tr></table></figure> <p>这一行是对初始化的几个<code>view_token</code>进行场景重建,并选出一开始的<code>init_ref_id</code></p> <p>然后之后就是把所有初始化的帧放到<code>buffer_set</code>里,然后进行一些归一化处理。</p> <h3 id="对原始的view再继续进行i2p重建点图"><a href="#对原始的view再继续进行i2p重建点图" class="headerlink" title="对原始的view再继续进行i2p重建点图"></a>对原始的view再继续进行i2p重建点图</h3><p>这里我们重新遍历所有图像,对应论文里面通过<code>I2P</code>的<code>decoder</code>重建所有<code>view</code>的点图。此外,注意<code>initial window</code>的关键帧图片基本上已经在上面的初始化中被创建出了点图,因此我们选择略过他们,只对没有被创建点图的帧进行<code>I2P</code>处理 以得到点图,然后就采用论文中的输入窗口多个帧,重建每个帧的点云作为<code>L2W model</code>的输入。</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> <span class="line">9</span> <span class="line">10</span> <span class="line">11</span> <span class="line">12</span> <span class="line">13</span> <span class="line">14</span> <span class="line">15</span> <span class="line">16</span> <span class="line">17</span> <span class="line">18</span> <span class="line">19</span> <span class="line">20</span> <span class="line">21</span> <span class="line">22</span> <span class="line">23</span> <span class="line">24</span> <span class="line">25</span> <span class="line">26</span> <span class="line">27</span> <span class="line">28</span> <span class="line">29</span> <span class="line">30</span> <span class="line">31</span> <span class="line">32</span> </pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> view_id <span class="keyword">in</span> tqdm(<span class="built_in">range</span>(num_views), desc=<span class="string">&quot; I2P resonstruction&quot; </span>): </span> <span class="line"> <span class="comment"># skip the views in the initial window</span></span> <span class="line"> <span class="keyword">if</span> view_id <span class="keyword">in</span> buffering_set_ids: </span> <span class="line"> <span class="comment"># trick to mark the keyframe in the initial window</span></span> <span class="line"> <span class="keyword">if</span> view_id // kf_stride == init_ref_id: </span> <span class="line"> per_frame_res[<span class="string">&#x27; i2p_pcds&#x27; </span>][view_id] = per_frame_res[<span class="string">&#x27; l2w_pcds&#x27; </span>][view_id].cpu()</span> <span class="line"> <span class="keyword">else</span>: </span> <span class="line"> per_frame_res[<span class="string">&#x27; i2p_pcds&#x27; </span>][view_id] = torch.zeros_like(per_frame_res[<span class="string">&#x27; l2w_pcds&#x27; </span>][view_id], device=<span class="string">&quot; cpu&quot; </span>)</span> <span class="line"> per_frame_res[<span class="string">&#x27; i2p_confs&#x27; </span>][view_id] = per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][view_id].cpu()</span> <span class="line"> <span class="keyword">continue</span></span> <span class="line"> <span class="comment"># construct the local window </span></span> <span class="line"> sel_ids = [view_id]</span> <span class="line"> <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">1</span>, win_r+<span class="number">1</span>): </span> <span class="line"> <span class="keyword">if</span> view_id-i*adj_distance > = <span class="number">0</span>: </span> <span class="line"> sel_ids.append(view_id-i*adj_distance)</span> <span class="line"> <span class="keyword">if</span> view_id+i*adj_distance < num_views: </span> <span class="line"> sel_ids.append(view_id+i*adj_distance)</span> <span class="line"> local_views = [input_views[<span class="built_in">id</span>] <span class="keyword">for</span> <span class="built_in">id</span> <span class="keyword">in</span> sel_ids]</span> <span class="line"> ref_id = <span class="number">0</span> </span> <span class="line"> <span class="comment"># recover points in the local window, and save the keyframe points and confs</span></span> <span class="line"> output = i2p_inference_batch([local_views], i2p_model, ref_id=ref_id, </span> <span class="line"> tocpu=<span class="literal">False</span>, unsqueeze=<span class="literal">False</span>)[<span class="string">&#x27; preds&#x27; </span>]</span> <span class="line"> <span class="comment">#save results of the i2p model</span></span> <span class="line"> per_frame_res[<span class="string">&#x27; i2p_pcds&#x27; </span>][view_id] = output[ref_id][<span class="string">&#x27; pts3d&#x27; </span>].cpu() <span class="comment"># 1,224,224,3</span></span> <span class="line"> per_frame_res[<span class="string">&#x27; i2p_confs&#x27; </span>][view_id] = output[ref_id][<span class="string">&#x27; conf&#x27; </span>][<span class="number">0</span>].cpu() <span class="comment"># 224,224</span></span> <span class="line"></span> <span class="line"> <span class="comment"># construct the input for L2W model </span></span> <span class="line"> input_views[view_id][<span class="string">&#x27; pts3d_cam&#x27; </span>] = output[ref_id][<span class="string">&#x27; pts3d&#x27; </span>] <span class="comment"># 1,224,224,3</span></span> <span class="line"> valid_mask = output[ref_id][<span class="string">&#x27; conf&#x27; </span>] > conf_thres_i2p <span class="comment"># 1,224,224</span></span> <span class="line"> input_views[view_id][<span class="string">&#x27; pts3d_cam&#x27; </span>] = normalize_views([input_views[view_id][<span class="string">&#x27; pts3d_cam&#x27; </span>]], </span> <span class="line"> [valid_mask])[<span class="number">0</span>]</span> <span class="line"> input_views[view_id][<span class="string">&#x27; pts3d_cam&#x27; </span>][~valid_mask] = <span class="number">0</span> </span> </pre></td></tr></table></figure> <h3 id="对初始窗口非关键帧进行注册"><a href="#对初始窗口非关键帧进行注册" class="headerlink" title="对初始窗口非关键帧进行注册"></a>对初始窗口非关键帧进行注册</h3><p>显然我们在之前的初始化场景中只注册了关键帧,因此我们现在开始对非关键帧进行注册:</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> <span class="line">9</span> <span class="line">10</span> <span class="line">11</span> <span class="line">12</span> <span class="line">13</span> <span class="line">14</span> <span class="line">15</span> <span class="line">16</span> <span class="line">17</span> <span class="line">18</span> <span class="line">19</span> <span class="line">20</span> <span class="line">21</span> <span class="line">22</span> <span class="line">23</span> <span class="line">24</span> <span class="line">25</span> </pre></td><td class="code"><pre><span class="line"><span class="comment"># Special treatment: register the frames within the range of initial window with L2W model</span></span> <span class="line"><span class="comment"># <span class="doctag">TODO: </span> batchify</span></span> <span class="line"><span class="keyword">if</span> kf_stride > <span class="number">1</span>: </span> <span class="line"> max_conf_mean = -<span class="number">1</span></span> <span class="line"> <span class="keyword">for</span> view_id <span class="keyword">in</span> tqdm(<span class="built_in">range</span>((init_num-<span class="number">1</span>)*kf_stride), desc=<span class="string">&quot; pre-registering&quot; </span>): </span> <span class="line"> <span class="keyword">if</span> view_id % kf_stride == <span class="number">0</span>: </span> <span class="line"> <span class="keyword">continue</span></span> <span class="line"> <span class="comment"># construct the input for L2W model</span></span> <span class="line"> l2w_input_views = [input_views[view_id]] + [input_views[<span class="built_in">id</span>] <span class="keyword">for</span> <span class="built_in">id</span> <span class="keyword">in</span> buffering_set_ids]</span> <span class="line"> <span class="comment"># (for defination of ref_ids, see the doc of l2w_model)</span></span> <span class="line"> output = l2w_inference(l2w_input_views, l2w_model, </span> <span class="line"> ref_ids=<span class="built_in">list</span>(<span class="built_in">range</span>(<span class="number">1</span>, <span class="built_in">len</span>(l2w_input_views))), </span> <span class="line"> device=args.device, </span> <span class="line"> normalize=args.norm_input)</span> <span class="line"> </span> <span class="line"> <span class="comment"># process the output of L2W model</span></span> <span class="line"> input_views[view_id][<span class="string">&#x27; pts3d_world&#x27; </span>] = output[<span class="number">0</span>][<span class="string">&#x27; pts3d_in_other_view&#x27; </span>] <span class="comment"># 1,224,224,3</span></span> <span class="line"> conf_map = output[<span class="number">0</span>][<span class="string">&#x27; conf&#x27; </span>] <span class="comment"># 1,224,224</span></span> <span class="line"> per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][view_id] = conf_map[<span class="number">0</span>] <span class="comment"># 224,224</span></span> <span class="line"> registered_confs_mean[view_id] = conf_map.mean().cpu()</span> <span class="line"> per_frame_res[<span class="string">&#x27; l2w_pcds&#x27; </span>][view_id] = input_views[view_id][<span class="string">&#x27; pts3d_world&#x27; </span>]</span> <span class="line"> </span> <span class="line"> <span class="keyword">if</span> registered_confs_mean[view_id] > max_conf_mean: </span> <span class="line"> max_conf_mean = registered_confs_mean[view_id]</span> <span class="line"> <span class="built_in">print</span>(<span class="string">f&#x27; finish aligning <span class="subst">&#123; (init_num-<span class="number">1</span>)*kf_stride&#125; </span> head frames, with a max mean confidence of <span class="subst">&#123; max_conf_mean: <span class="number">.2</span>f&#125; </span>&#x27; </span>)</span> </pre></td></tr></table></figure> <p>这里正如注释所说,是一个<strong>Special treatment</strong>。也是一个特殊情况处理。</p> <h4 id="缩放confs"><a href="#缩放confs" class="headerlink" title="缩放confs"></a>缩放confs</h4><p>我们发现,我们只用<code>l2w</code>网络对非关键帧进行了置信度预测,关键帧的置信度是由之前的<code>i2p</code>网络进行预测的,作者在这里为了控制计算成本,选择直接将后者乘上一个常数因子进行缩放,大致反映出了场景的置信度分数:</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> <span class="line">9</span> <span class="line">10</span> <span class="line">11</span> <span class="line">12</span> </pre></td><td class="code"><pre><span class="line"><span class="comment"># A problem is that the registered_confs_mean of the initial window is generated by I2P model, </span></span> <span class="line"><span class="comment"># while the registered_confs_mean of the frames within the initial window is generated by L2W model, </span></span> <span class="line"><span class="comment"># so there exists a gap. Here we try to align it.</span></span> <span class="line">max_initial_conf_mean = -<span class="number">1</span></span> <span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(init_num): </span> <span class="line"> <span class="keyword">if</span> registered_confs_mean[i*kf_stride] > max_initial_conf_mean: </span> <span class="line"> max_initial_conf_mean = registered_confs_mean[i*kf_stride]</span> <span class="line">factor = max_conf_mean/max_initial_conf_mean</span> <span class="line"><span class="comment"># print(f&#x27; align register confidence with a factor &#123; factor&#125; &#x27; )</span></span> <span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(init_num): </span> <span class="line"> per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][i*kf_stride] *= factor</span> <span class="line"> registered_confs_mean[i*kf_stride] = per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][i*kf_stride].mean().cpu()</span> </pre></td></tr></table></figure> <h3 id="对剩下的views进行注册"><a href="#对剩下的views进行注册" class="headerlink" title="对剩下的views进行注册"></a>对剩下的views进行注册</h3><p>OK ,经过了以上的对于初始帧的特殊处理,我们终于踏入了正途:在过程中对每个帧进行实时处理</p> <h4 id="从buffer-set里选择最相近的sel-num个帧:"><a href="#从buffer-set里选择最相近的sel-num个帧:" class="headerlink" title="从buffer set里选择最相近的sel_num个帧:"></a>从buffer set里选择最相近的sel_num个帧:</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> </pre></td><td class="code"><pre><span class="line"><span class="comment"># select sccene frames in the buffering set to work as a global reference</span></span> <span class="line">cand_ref_ids = buffering_set_ids</span> <span class="line">ref_views, sel_pool_ids = scene_frame_retrieve(</span> <span class="line"> [input_views[i] <span class="keyword">for</span> i <span class="keyword">in</span> cand_ref_ids], </span> <span class="line"> input_views[ni: ni+num_register: <span class="number">2</span>], </span> <span class="line"> i2p_model, sel_num=num_scene_frame, </span> <span class="line"> <span class="comment"># cand_recon_confs=[per_frame_res[&#x27; l2w_confs&#x27; ][i] for i in cand_ref_ids], </span></span> <span class="line"> depth=<span class="number">2</span>)</span> </pre></td></tr></table></figure> <p>这里正如论文中所述,采用了<code>i2p_model</code>的前 2 个<strong>decoder</strong>进行相似评分。</p> <h4 id="将选取的最相近的几个帧作为参考合并当前帧进行l2w重建"><a href="#将选取的最相近的几个帧作为参考合并当前帧进行l2w重建" class="headerlink" title="将选取的最相近的几个帧作为参考合并当前帧进行l2w重建"></a>将选取的最相近的几个帧作为参考合并当前帧进行l2w重建</h4><p>显而易见,言以概之:</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> <span class="line">9</span> <span class="line">10</span> <span class="line">11</span> <span class="line">12</span> <span class="line">13</span> <span class="line">14</span> <span class="line">15</span> <span class="line">16</span> <span class="line">17</span> <span class="line">18</span> <span class="line">19</span> <span class="line">20</span> <span class="line">21</span> <span class="line">22</span> <span class="line">23</span> </pre></td><td class="code"><pre><span class="line"><span class="comment"># register the source frames in the local coordinates to the world coordinates with L2W model</span></span> <span class="line">l2w_input_views = ref_views + input_views[ni: max_id+<span class="number">1</span>]</span> <span class="line">input_view_num = <span class="built_in">len</span>(ref_views) + max_id - ni + <span class="number">1</span></span> <span class="line"><span class="keyword">assert</span> input_view_num == <span class="built_in">len</span>(l2w_input_views)</span> <span class="line"></span> <span class="line">output = l2w_inference(l2w_input_views, l2w_model, </span> <span class="line"> ref_ids=<span class="built_in">list</span>(<span class="built_in">range</span>(<span class="built_in">len</span>(ref_views))), </span> <span class="line"> device=args.device, </span> <span class="line"> normalize=args.norm_input)</span> <span class="line"></span> <span class="line"><span class="comment"># process the output of L2W model</span></span> <span class="line">src_ids_local = [<span class="built_in">id</span>+<span class="built_in">len</span>(ref_views) <span class="keyword">for</span> <span class="built_in">id</span> <span class="keyword">in</span> <span class="built_in">range</span>(max_id-ni+<span class="number">1</span>)] <span class="comment"># the ids of src views in the local window</span></span> <span class="line">src_ids_global = [<span class="built_in">id</span> <span class="keyword">for</span> <span class="built_in">id</span> <span class="keyword">in</span> <span class="built_in">range</span>(ni, max_id+<span class="number">1</span>)] <span class="comment">#the ids of src views in the whole dataset</span></span> <span class="line">succ_num = <span class="number">0</span></span> <span class="line"><span class="keyword">for</span> <span class="built_in">id</span> <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(src_ids_global)): </span> <span class="line"> output_id = src_ids_local[<span class="built_in">id</span>] <span class="comment"># the id of the output in the output list</span></span> <span class="line"> view_id = src_ids_global[<span class="built_in">id</span>] <span class="comment"># the id of the view in all views</span></span> <span class="line"> conf_map = output[output_id][<span class="string">&#x27; conf&#x27; </span>] <span class="comment"># 1,224,224</span></span> <span class="line"> input_views[view_id][<span class="string">&#x27; pts3d_world&#x27; </span>] = output[output_id][<span class="string">&#x27; pts3d_in_other_view&#x27; </span>] <span class="comment"># 1,224,224,3</span></span> <span class="line"> per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][view_id] = conf_map[<span class="number">0</span>]</span> <span class="line"> registered_confs_mean[view_id] = conf_map[<span class="number">0</span>].mean().cpu()</span> <span class="line"> per_frame_res[<span class="string">&#x27; l2w_pcds&#x27; </span>][view_id] = input_views[view_id][<span class="string">&#x27; pts3d_world&#x27; </span>]</span> <span class="line"> succ_num += <span class="number">1</span></span> </pre></td></tr></table></figure> <blockquote> <p>需要注意的是,这里其实还是有改进空间的,我们可以根据<code>l2w_model</code>的<code>output</code>对参考帧进行微调。</p> </blockquote> <h4 id="通过一些手段更新buffer-set"><a href="#通过一些手段更新buffer-set" class="headerlink" title="通过一些手段更新buffer set"></a>通过一些手段更新buffer set</h4><p><code>buffer_set</code>的选取方法差不多就和论文里面讲的一样,基本上就是随机选取了。</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> <span class="line">9</span> <span class="line">10</span> <span class="line">11</span> <span class="line">12</span> <span class="line">13</span> <span class="line">14</span> <span class="line">15</span> <span class="line">16</span> <span class="line">17</span> <span class="line">18</span> <span class="line">19</span> <span class="line">20</span> <span class="line">21</span> <span class="line">22</span> <span class="line">23</span> <span class="line">24</span> <span class="line">25</span> <span class="line">26</span> <span class="line">27</span> <span class="line">28</span> <span class="line">29</span> <span class="line">30</span> <span class="line">31</span> <span class="line">32</span> <span class="line">33</span> <span class="line">34</span> <span class="line">35</span> <span class="line">36</span> <span class="line">37</span> <span class="line">38</span> <span class="line">39</span> <span class="line">40</span> <span class="line">41</span> <span class="line">42</span> </pre></td><td class="code"><pre><span class="line"><span class="comment"># update the buffering set</span></span> <span class="line"><span class="keyword">if</span> next_register_id - milestone > = update_buffer_intv: </span> <span class="line"> <span class="keyword">while</span>(next_register_id - milestone > = kf_stride): </span> <span class="line"> candi_frame_id += <span class="number">1</span></span> <span class="line"> full_flag = max_buffer_size > <span class="number">0</span> <span class="keyword">and</span> <span class="built_in">len</span>(buffering_set_ids) > = max_buffer_size</span> <span class="line"> insert_flag = (<span class="keyword">not</span> full_flag) <span class="keyword">or</span> ((strategy == <span class="string">&#x27; fifo&#x27; </span>) <span class="keyword">or</span> </span> <span class="line"> (strategy == <span class="string">&#x27; reservoir&#x27; </span> <span class="keyword">and</span> np.random.rand() < max_buffer_size/candi_frame_id))</span> <span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> insert_flag: </span> <span class="line"> milestone += kf_stride</span> <span class="line"> <span class="keyword">continue</span></span> <span class="line"> <span class="comment"># Use offest to ensure the selected view is not too close to the last selected view</span></span> <span class="line"> <span class="comment"># If the last selected view is 0, </span></span> <span class="line"> <span class="comment"># the next selected view should be at least kf_stride*3//4 frames away</span></span> <span class="line"> start_ids_offset = <span class="built_in">max</span>(<span class="number">0</span>, buffering_set_ids[-<span class="number">1</span>]+kf_stride*<span class="number">3</span>//<span class="number">4</span> - milestone)</span> <span class="line"> </span> <span class="line"> <span class="comment"># get the mean confidence of the candidate views</span></span> <span class="line"> mean_cand_recon_confs = torch.stack([registered_confs_mean[i]</span> <span class="line"> <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(milestone+start_ids_offset, milestone+kf_stride)])</span> <span class="line"> mean_cand_local_confs = torch.stack([local_confs_mean[i]</span> <span class="line"> <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(milestone+start_ids_offset, milestone+kf_stride)])</span> <span class="line"> <span class="comment"># normalize the confidence to [0,1], to avoid overconfidence</span></span> <span class="line"> mean_cand_recon_confs = (mean_cand_recon_confs - <span class="number">1</span>)/mean_cand_recon_confs <span class="comment"># transform to sigmoid</span></span> <span class="line"> mean_cand_local_confs = (mean_cand_local_confs - <span class="number">1</span>)/mean_cand_local_confs</span> <span class="line"> <span class="comment"># the final confidence is the product of the two kinds of confidences</span></span> <span class="line"> mean_cand_confs = mean_cand_recon_confs*mean_cand_local_confs</span> <span class="line"> </span> <span class="line"> most_conf_id = mean_cand_confs.argmax().item()</span> <span class="line"> most_conf_id += start_ids_offset</span> <span class="line"> id_to_buffer = milestone + most_conf_id</span> <span class="line"> buffering_set_ids.append(id_to_buffer)</span> <span class="line"> <span class="comment"># print(f&quot; add ref view &#123; id_to_buffer&#125; &quot; ) </span></span> <span class="line"> <span class="comment"># since we have inserted a new frame, overflow must happen when full_flag is True</span></span> <span class="line"> <span class="keyword">if</span> full_flag: </span> <span class="line"> <span class="keyword">if</span> strategy == <span class="string">&#x27; reservoir&#x27; </span>: </span> <span class="line"> buffering_set_ids.pop(np.random.randint(max_buffer_size))</span> <span class="line"> <span class="keyword">elif</span> strategy == <span class="string">&#x27; fifo&#x27; </span>: </span> <span class="line"> buffering_set_ids.pop(<span class="number">0</span>)</span> <span class="line"> <span class="comment"># print(next_register_id, buffering_set_ids)</span></span> <span class="line"> milestone += kf_stride</span> <span class="line"><span class="comment"># transfer the data to cpu if it is not in the buffering set, to save gpu memory</span></span> <span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(next_register_id): </span> <span class="line"> to_device(input_views[i], device=args.device <span class="keyword">if</span> i <span class="keyword">in</span> buffering_set_ids <span class="keyword">else</span> <span class="string">&#x27; cpu&#x27; </span>)</span> </pre></td></tr></table></figure> <h3 id="保存环节"><a href="#保存环节" class="headerlink" title="保存环节"></a>保存环节</h3><p>当我们处理完所有帧后,我们会保存我们的所有帧的点云,把这些所有帧的点云合到一起进行重建,得出最后的场景点云。</p> <h3 id="Review"><a href="#Review" class="headerlink" title="Review"></a>Review</h3><p>显而易见,原<code>recon.py</code>中的这个<code>pipeline</code>是一个完全的<strong>offline</strong>处理方法,因此,我编写了一个真正的(?<strong>online</strong>版本的方法,处理逻辑如下所示:</p> <h2 id="Online-函数的处理逻辑"><a href="#Online-函数的处理逻辑" class="headerlink" title="Online 函数的处理逻辑"></a>Online 函数的处理逻辑</h2><p>既然是要 online ,我们显然第一件要做的事情就是写下:</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> </pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(<span class="built_in">len</span>(data_views)): </span> </pre></td></tr></table></figure> <p>之后我们在进行一系列处理:</p> <h3 id="预处理-得到当前view的token"><a href="#预处理-得到当前view的token" class="headerlink" title="预处理 & 得到当前view的token"></a>预处理 & 得到当前view的token</h3><p>显然,通过对原先<strong>offline</strong>版本的函数分析,这个过程没有初始化的困扰,因此,我们可以大胆对所有遍历到的 view 都进行这一步:</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> <span class="line">9</span> <span class="line">10</span> <span class="line">11</span> <span class="line">12</span> <span class="line">13</span> <span class="line">14</span> <span class="line">15</span> <span class="line">16</span> <span class="line">17</span> <span class="line">18</span> <span class="line">19</span> <span class="line">20</span> <span class="line">21</span> <span class="line">22</span> <span class="line">23</span> <span class="line">24</span> <span class="line">25</span> <span class="line">26</span> <span class="line">27</span> <span class="line">28</span> <span class="line">29</span> <span class="line">30</span> <span class="line">31</span> <span class="line">32</span> <span class="line">33</span> </pre></td><td class="code"><pre><span class="line"><span class="comment"># Pre-save the RGB images along with their corresponding masks</span></span> <span class="line"><span class="comment"># in preparation for visualization at last.</span></span> <span class="line"></span> <span class="line"><span class="keyword">if</span> data_views[i][<span class="string">&#x27; img&#x27; </span>].shape[<span class="number">0</span>] == <span class="number">1</span>: </span> <span class="line"> data_views[i][<span class="string">&#x27; img&#x27; </span>] = data_views[i][<span class="string">&#x27; img&#x27; </span>][<span class="number">0</span>]</span> <span class="line">rgb_imgs.append(transform_img(<span class="built_in">dict</span>(img=data_views[i][<span class="string">&#x27; img&#x27; </span>][<span class="literal">None</span>]))[..., : :-<span class="number">1</span>])</span> <span class="line"></span> <span class="line"><span class="keyword">if</span> is_have_mask_rgb: </span> <span class="line"> valid_masks.append(data_views[i][<span class="string">&#x27; valid_mask&#x27; </span>])</span> <span class="line"></span> <span class="line"><span class="comment"># process now image for extracting its img token with encoder</span></span> <span class="line">data_views[i][<span class="string">&#x27; img&#x27; </span>] = torch.tensor(data_views[i][<span class="string">&#x27; img&#x27; </span>][<span class="literal">None</span>])</span> <span class="line">data_views[i][<span class="string">&#x27; true_shape&#x27; </span>] = torch.tensor(data_views[i][<span class="string">&#x27; true_shape&#x27; </span>][<span class="literal">None</span>])</span> <span class="line"><span class="keyword">for</span> key <span class="keyword">in</span> [<span class="string">&#x27; valid_mask&#x27; </span>, <span class="string">&#x27; pts3d_cam&#x27; </span>, <span class="string">&#x27; pts3d&#x27; </span>]: </span> <span class="line"> <span class="keyword">if</span> key <span class="keyword">in</span> data_views[i]: </span> <span class="line"> <span class="keyword">del</span> data_views[key]</span> <span class="line">to_device(data_views[i], device=args.device)</span> <span class="line"></span> <span class="line"><span class="comment"># pre-extract img tokens by encoder, which can be reused </span></span> <span class="line"><span class="comment"># in the following inference by both i2p and l2w models</span></span> <span class="line">temp_shape, temp_feat, temp_pose = get_single_img_tokens([data_views[i]], i2p_model, <span class="literal">True</span>)</span> <span class="line">res_shapes.append(temp_shape[<span class="number">0</span>])</span> <span class="line">res_feats.append(temp_feat[<span class="number">0</span>])</span> <span class="line">res_poses.append(temp_pose[<span class="number">0</span>])</span> <span class="line"><span class="built_in">print</span>(<span class="string">f&quot; finish pre-extracting img token of view <span class="subst">&#123; i&#125; </span>&quot; </span>)</span> <span class="line"></span> <span class="line">input_views.append(<span class="built_in">dict</span>(label=data_views[i][<span class="string">&#x27; label&#x27; </span>], </span> <span class="line"> img_tokens=temp_feat[<span class="number">0</span>], </span> <span class="line"> true_shape=data_views[i][<span class="string">&#x27; true_shape&#x27; </span>], </span> <span class="line"> img_pos=temp_pose[<span class="number">0</span>]))</span> <span class="line"><span class="keyword">for</span> key <span class="keyword">in</span> per_frame_res: </span> <span class="line"> per_frame_res[key].append(<span class="literal">None</span>)</span> <span class="line">registered_confs_mean.append(i)</span> </pre></td></tr></table></figure> <p>这里我使用了一个<code>get_single_img_tokens</code>函数,与之前的<code>get_img_tokens</code>函数相比,该函数除了不能 batch 化(online 的限制)之外,效果输出别无二致。</p> <h3 id="积累帧以用于场景初始化"><a href="#积累帧以用于场景初始化" class="headerlink" title="积累帧以用于场景初始化"></a>积累帧以用于场景初始化</h3><p>需要注意的是,当帧序数小于初始化所需要的帧数时,我们后续的程序均无法进行,因此在我的代码中,我选择直接跳过,先蓄势待发🤣</p> <p>一旦积累到初始化场景所需帧后,函数会采用一系列操作初始化场景以及初始化 buffer set ,对初始化后的各帧点云进行归一化处理:</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> <span class="line">9</span> <span class="line">10</span> <span class="line">11</span> <span class="line">12</span> <span class="line">13</span> <span class="line">14</span> <span class="line">15</span> <span class="line">16</span> <span class="line">17</span> <span class="line">18</span> <span class="line">19</span> <span class="line">20</span> <span class="line">21</span> <span class="line">22</span> <span class="line">23</span> <span class="line">24</span> <span class="line">25</span> <span class="line">26</span> <span class="line">27</span> <span class="line">28</span> <span class="line">29</span> <span class="line">30</span> </pre></td><td class="code"><pre><span class="line"><span class="comment"># accumulate the initial window frames</span></span> <span class="line"><span class="keyword">if</span> i < (initial_winsize - <span class="number">1</span>)*kf_stride <span class="keyword">and</span> i % kf_stride == <span class="number">0</span>: </span> <span class="line"> <span class="keyword">continue</span></span> <span class="line"><span class="keyword">elif</span> i == (initial_winsize - <span class="number">1</span>)*kf_stride: </span> <span class="line"> initial_pcds, initial_confs, init_ref_id = initialize_scene(input_views[: initial_winsize*kf_stride: kf_stride], </span> <span class="line"> i2p_model, </span> <span class="line"> winsize=initial_winsize, </span> <span class="line"> return_ref_id=<span class="literal">True</span>)</span> <span class="line"> <span class="comment"># set up the world coordinates with the initial window</span></span> <span class="line"> init_num = <span class="built_in">len</span>(initial_pcds)</span> <span class="line"> <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(init_num): </span> <span class="line"> per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][j * kf_stride] = initial_confs[j][<span class="number">0</span>].to(args.device)</span> <span class="line"> registered_confs_mean[j * kf_stride] = per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][j * kf_stride].mean().cpu()</span> <span class="line"> <span class="comment"># initialize the buffering set with the initial window</span></span> <span class="line"> <span class="keyword">assert</span> args.buffer_size < = <span class="number">0</span> <span class="keyword">or</span> args.buffer_size > = init_num </span> <span class="line"> buffering_set_ids = [j*kf_stride <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(init_num)]</span> <span class="line"> <span class="comment"># set ip the woeld coordinates with frames in the initial window</span></span> <span class="line"> <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(init_num): </span> <span class="line"> input_views[j*kf_stride][<span class="string">&#x27; pts3d_world&#x27; </span>] = initial_pcds[j]</span> <span class="line"> initial_valid_masks = [conf > conf_thres_i2p <span class="keyword">for</span> conf <span class="keyword">in</span> initial_confs]</span> <span class="line"> normed_pts = normalize_views([view[<span class="string">&#x27; pts3d_world&#x27; </span>] <span class="keyword">for</span> view <span class="keyword">in</span> input_views[: init_num*kf_stride: kf_stride]], </span> <span class="line"> initial_valid_masks)</span> <span class="line"> <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(init_num): </span> <span class="line"> input_views[j*kf_stride][<span class="string">&#x27; pts3d_world&#x27; </span>] = normed_pts[j]</span> <span class="line"> <span class="comment"># filter out points with low confidence</span></span> <span class="line"> input_views[j*kf_stride][<span class="string">&#x27; pts3d_world&#x27; </span>][~initial_valid_masks[j]] = <span class="number">0</span></span> <span class="line"> per_frame_res[<span class="string">&#x27; l2w_pcds&#x27; </span>][j*kf_stride] = normed_pts[j]</span> <span class="line"></span> <span class="line"><span class="keyword">elif</span> i < (initial_winsize - <span class="number">1</span>) * kf_stride: </span> <span class="line"> <span class="keyword">continue</span></span> </pre></td></tr></table></figure> <p>需要注意的是,这里一旦积累到足够多的初始帧,我们就不会进行 continue 处理了,然后直接进行下一部分。</p> <h3 id="对之前积累的view进行i2p重建点图(包含正在处理的帧)-注册初始窗口非关键帧"><a href="#对之前积累的view进行i2p重建点图(包含正在处理的帧)-注册初始窗口非关键帧" class="headerlink" title="对之前积累的view进行i2p重建点图(包含正在处理的帧) & 注册初始窗口非关键帧"></a>对之前积累的view进行i2p重建点图(包含正在处理的帧) & 注册初始窗口非关键帧</h3><p>这里我们采用类似于之前<strong>offline</strong>的顺序,只不过把外在的表现形式作出了改变,实际上内在的顺序逻辑基本不变:</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> <span class="line">9</span> <span class="line">10</span> <span class="line">11</span> <span class="line">12</span> <span class="line">13</span> <span class="line">14</span> <span class="line">15</span> <span class="line">16</span> <span class="line">17</span> <span class="line">18</span> <span class="line">19</span> <span class="line">20</span> <span class="line">21</span> <span class="line">22</span> <span class="line">23</span> <span class="line">24</span> <span class="line">25</span> <span class="line">26</span> <span class="line">27</span> <span class="line">28</span> <span class="line">29</span> <span class="line">30</span> <span class="line">31</span> <span class="line">32</span> <span class="line">33</span> <span class="line">34</span> <span class="line">35</span> <span class="line">36</span> <span class="line">37</span> <span class="line">38</span> <span class="line">39</span> <span class="line">40</span> <span class="line">41</span> <span class="line">42</span> <span class="line">43</span> <span class="line">44</span> <span class="line">45</span> <span class="line">46</span> <span class="line">47</span> <span class="line">48</span> <span class="line">49</span> <span class="line">50</span> <span class="line">51</span> <span class="line">52</span> <span class="line">53</span> <span class="line">54</span> <span class="line">55</span> <span class="line">56</span> <span class="line">57</span> <span class="line">58</span> <span class="line">59</span> <span class="line">60</span> <span class="line">61</span> <span class="line">62</span> <span class="line">63</span> <span class="line">64</span> <span class="line">65</span> <span class="line">66</span> <span class="line">67</span> <span class="line">68</span> <span class="line">69</span> <span class="line">70</span> <span class="line">71</span> <span class="line">72</span> <span class="line">73</span> <span class="line">74</span> <span class="line">75</span> <span class="line">76</span> <span class="line">77</span> <span class="line">78</span> <span class="line">79</span> <span class="line">80</span> <span class="line">81</span> <span class="line">82</span> <span class="line">83</span> <span class="line">84</span> </pre></td><td class="code"><pre><span class="line"><span class="comment"># first recover the accumulate views</span></span> <span class="line"><span class="keyword">if</span> i == (initial_winsize - <span class="number">1</span>) * kf_stride: </span> <span class="line"> <span class="keyword">for</span> view_id <span class="keyword">in</span> <span class="built_in">range</span>(i + <span class="number">1</span>): </span> <span class="line"> <span class="comment"># skip the views in the initial window</span></span> <span class="line"> <span class="keyword">if</span> view_id <span class="keyword">in</span> buffering_set_ids: </span> <span class="line"> <span class="comment"># trick to mark the keyframe in the initial window</span></span> <span class="line"> <span class="keyword">if</span> view_id // kf_stride == init_ref_id: </span> <span class="line"> per_frame_res[<span class="string">&#x27; i2p_pcds&#x27; </span>][view_id] = per_frame_res[<span class="string">&#x27; l2w_pcds&#x27; </span>][view_id].cpu()</span> <span class="line"> <span class="keyword">else</span>: </span> <span class="line"> per_frame_res[<span class="string">&#x27; i2p_pcds&#x27; </span>][view_id] = torch.zeros_like(per_frame_res[<span class="string">&#x27; l2w_pcds&#x27; </span>][view_id], device=<span class="string">&quot; cpu&quot; </span>)</span> <span class="line"> per_frame_res[<span class="string">&#x27; i2p_confs&#x27; </span>][view_id] = per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][view_id].cpu()</span> <span class="line"> <span class="built_in">print</span>(<span class="string">f&quot; finish revocer pcd of frame <span class="subst">&#123; view_id&#125; </span> in their local coordinates(in buffer set), with a mean confidence of <span class="subst">&#123; per_frame_res[<span class="string">&#x27; i2p_confs&#x27; </span>][view_id].mean(): <span class="number">.2</span>f&#125; </span> up to now.&quot; </span>)</span> <span class="line"> <span class="keyword">continue</span></span> <span class="line"> <span class="comment"># construct the local window with the initial views</span></span> <span class="line"> sel_ids = [view_id]</span> <span class="line"> <span class="keyword">for</span> j <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">1</span>, win_r + <span class="number">1</span>): </span> <span class="line"> <span class="keyword">if</span> view_id - j * adj_distance > = <span class="number">0</span>: </span> <span class="line"> sel_ids.append(view_id - j * adj_distance)</span> <span class="line"> <span class="keyword">if</span> view_id + j * adj_distance < i: </span> <span class="line"> sel_ids.append(view_id + j * adj_distance)</span> <span class="line"> local_views = [input_views[<span class="built_in">id</span>] <span class="keyword">for</span> <span class="built_in">id</span> <span class="keyword">in</span> sel_ids]</span> <span class="line"> ref_id = <span class="number">0</span></span> <span class="line"></span> <span class="line"> <span class="comment"># recover poionts in the initial window, and save the keyframe points and confs</span></span> <span class="line"> output = i2p_inference_batch([local_views], i2p_model, ref_id=ref_id, </span> <span class="line"> tocpu=<span class="literal">False</span>, unsqueeze=<span class="literal">False</span>)[<span class="string">&#x27; preds&#x27; </span>]</span> <span class="line"> <span class="comment"># save results of the i2p model for the initial window</span></span> <span class="line"> per_frame_res[<span class="string">&#x27; i2p_pcds&#x27; </span>][view_id] = output[ref_id][<span class="string">&#x27; pts3d&#x27; </span>].cpu()</span> <span class="line"> per_frame_res[<span class="string">&#x27; i2p_confs&#x27; </span>][view_id] = output[ref_id][<span class="string">&#x27; conf&#x27; </span>][<span class="number">0</span>].cpu()</span> <span class="line"></span> <span class="line"> <span class="comment"># construct the input for L2W model</span></span> <span class="line"> input_views[view_id][<span class="string">&#x27; pts3d_cam&#x27; </span>] = output[ref_id][<span class="string">&#x27; pts3d&#x27; </span>]</span> <span class="line"> valid_mask = output[ref_id][<span class="string">&#x27; conf&#x27; </span>] > conf_thres_i2p</span> <span class="line"> input_views[view_id][<span class="string">&#x27; pts3d_cam&#x27; </span>] = normalize_views([input_views[view_id][<span class="string">&#x27; pts3d_cam&#x27; </span>]], </span> <span class="line"> [valid_mask])[<span class="number">0</span>]</span> <span class="line"> input_views[view_id][<span class="string">&#x27; pts3d_cam&#x27; </span>][~valid_mask] = <span class="number">0</span></span> <span class="line"></span> <span class="line"> local_confs_mean_up2now = [conf.mean() <span class="keyword">for</span> conf <span class="keyword">in</span> per_frame_res[<span class="string">&#x27; i2p_confs&#x27; </span>] <span class="keyword">if</span> conf <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>]</span> <span class="line"> <span class="built_in">print</span>(<span class="string">f&quot; finish revocer pcd of frame <span class="subst">&#123; view_id&#125; </span> in their local coordinates, with a mean confidence of <span class="subst">&#123; torch.stack(local_confs_mean_up2now).mean(): <span class="number">.2</span>f&#125; </span> up to now.&quot; </span>)</span> <span class="line"></span> <span class="line"> <span class="comment"># Special treatment: register the frames within the range of initial window with L2W model</span></span> <span class="line"> <span class="keyword">if</span> kf_stride > <span class="number">1</span>: </span> <span class="line"> max_conf_mean = -<span class="number">1</span></span> <span class="line"> <span class="keyword">for</span> view_id <span class="keyword">in</span> tqdm(<span class="built_in">range</span>((init_num - <span class="number">1</span>) * kf_stride), desc=<span class="string">&quot; pre-registering&quot; </span>): </span> <span class="line"> <span class="keyword">if</span> view_id % kf_stride == <span class="number">0</span>: </span> <span class="line"> <span class="keyword">continue</span></span> <span class="line"> <span class="comment"># construct the input for L2W model</span></span> <span class="line"></span> <span class="line"> l2w_input_views = [input_views[view_id]] + [input_views[<span class="built_in">id</span>] <span class="keyword">for</span> <span class="built_in">id</span> <span class="keyword">in</span> buffering_set_ids]</span> <span class="line"> <span class="comment"># (for defination of ref_ids, seee the doc of l2w_model)</span></span> <span class="line"> output = l2w_inference(l2w_input_views, l2w_model, </span> <span class="line"> ref_ids=<span class="built_in">list</span>(<span class="built_in">range</span>(<span class="number">1</span>, <span class="built_in">len</span>(l2w_input_views))), </span> <span class="line"> device=args.device, </span> <span class="line"> normalize=args.norm_input)</span> <span class="line"> <span class="comment"># process the output of L2W model</span></span> <span class="line"> input_views[view_id][<span class="string">&#x27; pts3d_world&#x27; </span>] = output[<span class="number">0</span>][<span class="string">&#x27; pts3d_in_other_view&#x27; </span>] <span class="comment"># 1,224,224,3</span></span> <span class="line"> conf_map = output[<span class="number">0</span>][<span class="string">&#x27; conf&#x27; </span>] <span class="comment"># 1,224,224</span></span> <span class="line"> per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][view_id] = conf_map[<span class="number">0</span>] <span class="comment"># 224,224</span></span> <span class="line"> registered_confs_mean[view_id] = conf_map.mean().cpu()</span> <span class="line"> per_frame_res[<span class="string">&#x27; l2w_pcds&#x27; </span>][view_id] = input_views[view_id][<span class="string">&#x27; pts3d_world&#x27; </span>]</span> <span class="line"> </span> <span class="line"> <span class="keyword">if</span> registered_confs_mean[view_id] > max_conf_mean: </span> <span class="line"> max_conf_mean = registered_confs_mean[view_id]</span> <span class="line"> <span class="built_in">print</span>(<span class="string">f&#x27; finish aligning <span class="subst">&#123; (init_num)*kf_stride&#125; </span> head frames, with a max mean confidence of <span class="subst">&#123; max_conf_mean: <span class="number">.2</span>f&#125; </span>&#x27; </span>)</span> <span class="line"> <span class="comment"># A problem is that the registered_confs_mean of the initial window is generated by I2P model, </span></span> <span class="line"> <span class="comment"># while the registered_confs_mean of the frames within the initial window is generated by L2W model, </span></span> <span class="line"> <span class="comment"># so there exists a gap. Here we try to align it.</span></span> <span class="line"> max_initial_conf_mean = -<span class="number">1</span></span> <span class="line"> <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(init_num): </span> <span class="line"> <span class="keyword">if</span> registered_confs_mean[i*kf_stride] > max_initial_conf_mean: </span> <span class="line"> max_initial_conf_mean = registered_confs_mean[i*kf_stride]</span> <span class="line"> factor = max_conf_mean/max_initial_conf_mean</span> <span class="line"> <span class="comment"># print(f&#x27; align register confidence with a factor &#123; factor&#125; &#x27; )</span></span> <span class="line"> <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(init_num): </span> <span class="line"> per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][i*kf_stride] *= factor</span> <span class="line"> registered_confs_mean[i*kf_stride] = per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][i*kf_stride].mean().cpu()</span> <span class="line"> <span class="comment"># register the rest frames with L2W model</span></span> <span class="line"> next_register_id = (init_num - <span class="number">1</span>) * kf_stride + <span class="number">1</span></span> <span class="line"> milestone = init_num * kf_stride + <span class="number">1</span></span> <span class="line"> update_buffer_intv = kf_stride*args.update_buffer_intv <span class="comment"># update the buffering set every update_buffer_intv frames</span></span> <span class="line"> max_buffer_size = args.buffer_size</span> <span class="line"> strategy = args.buffer_strategy</span> <span class="line"> candi_frame_id = <span class="built_in">len</span>(buffering_set_ids) <span class="comment"># used for the reservoir sampling strategy</span></span> <span class="line"> <span class="keyword">continue</span></span> </pre></td></tr></table></figure> <p>然后在处理完这么一堆之后我们直接<code>continue</code>到下一个循环。</p> <h3 id="处理新图片"><a href="#处理新图片" class="headerlink" title="处理新图片"></a>处理新图片</h3><p>在下一个循环中,我们拿到了新图片,此时我们也在我们的<strong>online</strong>函数中踏上了正途,可以对每一个帧进行实时处理了。</p> <p>这里,我们的处理逻辑与第一种方法类似,不同的一点是我是一帧一帧地去处理。</p> <h3 id="保存环节-1"><a href="#保存环节-1" class="headerlink" title="保存环节"></a>保存环节</h3><p>与上一个方法略微不同,我提供了参数选项选择是否在线保存/逐几帧保存,因此我重写了一个增量式保存的类:</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> <span class="line">9</span> <span class="line">10</span> <span class="line">11</span> <span class="line">12</span> <span class="line">13</span> <span class="line">14</span> <span class="line">15</span> <span class="line">16</span> <span class="line">17</span> <span class="line">18</span> <span class="line">19</span> <span class="line">20</span> <span class="line">21</span> <span class="line">22</span> <span class="line">23</span> <span class="line">24</span> <span class="line">25</span> <span class="line">26</span> <span class="line">27</span> <span class="line">28</span> <span class="line">29</span> <span class="line">30</span> <span class="line">31</span> <span class="line">32</span> <span class="line">33</span> <span class="line">34</span> <span class="line">35</span> <span class="line">36</span> <span class="line">37</span> <span class="line">38</span> <span class="line">39</span> <span class="line">40</span> <span class="line">41</span> <span class="line">42</span> <span class="line">43</span> <span class="line">44</span> <span class="line">45</span> <span class="line">46</span> <span class="line">47</span> <span class="line">48</span> <span class="line">49</span> <span class="line">50</span> <span class="line">51</span> <span class="line">52</span> <span class="line">53</span> <span class="line">54</span> <span class="line">55</span> <span class="line">56</span> <span class="line">57</span> <span class="line">58</span> <span class="line">59</span> <span class="line">60</span> <span class="line">61</span> <span class="line">62</span> <span class="line">63</span> <span class="line">64</span> <span class="line">65</span> <span class="line">66</span> <span class="line">67</span> <span class="line">68</span> <span class="line">69</span> <span class="line">70</span> <span class="line">71</span> <span class="line">72</span> <span class="line">73</span> <span class="line">74</span> <span class="line">75</span> <span class="line">76</span> <span class="line">77</span> <span class="line">78</span> </pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">IncrementalReconstructor</span>: </span> <span class="line"> <span class="string">&quot; &quot; &quot; </span></span> <span class="line"><span class="string"> A class used for reconstruting the pts incrementally</span></span> <span class="line"><span class="string"> &quot; &quot; &quot; </span></span> <span class="line"> <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self</span>): </span> <span class="line"> <span class="variable language_">self</span>.res_pcds = <span class="literal">None</span></span> <span class="line"> <span class="variable language_">self</span>.res_rgbs = <span class="literal">None</span></span> <span class="line"> <span class="variable language_">self</span>.res_confs = <span class="literal">None</span></span> <span class="line"> <span class="variable language_">self</span>.res_valid_masks = <span class="literal">None</span></span> <span class="line"> <span class="variable language_">self</span>.is_initialized = <span class="literal">False</span></span> <span class="line"></span> <span class="line"> <span class="keyword">def</span> <span class="title function_">add_frame</span>(<span class="params">self, view: <span class="built_in">dict</span>, img: np.ndarray, conf: np.ndarray = <span class="literal">None</span>, valid_mask: np.ndarray = <span class="literal">None</span></span>): </span> <span class="line"> <span class="string">&quot; &quot; &quot; </span></span> <span class="line"><span class="string"> Incrementally add a new frame of view data.</span></span> <span class="line"><span class="string"></span></span> <span class="line"><span class="string"> Args: </span></span> <span class="line"><span class="string"> view (dict): a dictionary for a new view</span></span> <span class="line"><span class="string"> img (np.ndarray): rgb_img</span></span> <span class="line"><span class="string"> conf (np.ndarray, optional): </span></span> <span class="line"><span class="string"> valid_mask (np.ndarray, optional): </span></span> <span class="line"><span class="string"> &quot; &quot; &quot; </span></span> <span class="line"> <span class="keyword">try</span>: </span> <span class="line"> new_pcd = to_numpy(view[<span class="string">&#x27; pts3d_world&#x27; </span>]).reshape(-<span class="number">1</span>, <span class="number">3</span>)</span> <span class="line"> new_rgb = to_numpy(img).reshape(-<span class="number">1</span>, <span class="number">3</span>)</span> <span class="line"> <span class="keyword">except</span> KeyError: </span> <span class="line"> <span class="built_in">print</span>(<span class="string">f&quot; Warning: &#x27; pts3d_world&#x27; not found in the new view. Frame skipped.&quot; </span>)</span> <span class="line"> <span class="keyword">return</span></span> <span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> <span class="variable language_">self</span>.is_initialized: </span> <span class="line"> <span class="variable language_">self</span>.res_pcds = new_pcd</span> <span class="line"> <span class="variable language_">self</span>.res_rgbs = new_rgb</span> <span class="line"> <span class="keyword">if</span> conf <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>: </span> <span class="line"> <span class="variable language_">self</span>.res_confs = to_numpy(conf).reshape(-<span class="number">1</span>)</span> <span class="line"> <span class="keyword">if</span> valid_mask <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>: </span> <span class="line"> <span class="variable language_">self</span>.res_valid_masks = to_numpy(valid_mask).reshape(-<span class="number">1</span>)</span> <span class="line"> <span class="variable language_">self</span>.is_initialized = <span class="literal">True</span></span> <span class="line"> <span class="keyword">else</span>: </span> <span class="line"> <span class="variable language_">self</span>.res_pcds = np.concatenate([<span class="variable language_">self</span>.res_pcds, new_pcd], axis=<span class="number">0</span>)</span> <span class="line"> <span class="variable language_">self</span>.res_rgbs = np.concatenate([<span class="variable language_">self</span>.res_rgbs, new_rgb], axis=<span class="number">0</span>)</span> <span class="line"> <span class="keyword">if</span> conf <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>: </span> <span class="line"> new_conf = to_numpy(conf).reshape(-<span class="number">1</span>)</span> <span class="line"> <span class="variable language_">self</span>.res_confs = np.concatenate([<span class="variable language_">self</span>.res_confs, new_conf], axis=<span class="number">0</span>)</span> <span class="line"> <span class="keyword">if</span> valid_mask <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>: </span> <span class="line"> new_mask = to_numpy(valid_mask).reshape(-<span class="number">1</span>)</span> <span class="line"> <span class="variable language_">self</span>.res_valid_masks = np.concatenate([<span class="variable language_">self</span>.res_valid_masks, new_mask], axis=<span class="number">0</span>)</span> <span class="line"></span> <span class="line"> <span class="keyword">def</span> <span class="title function_">save_snapshot</span>(<span class="params">self, snapshot_id: <span class="built_in">int</span>, save_dir: <span class="built_in">str</span>, num_points_save: <span class="built_in">int</span> = <span class="number">200000</span>, conf_thres_res: <span class="built_in">float</span> = <span class="number">3.0</span></span>): </span> <span class="line"> <span class="string">&quot; &quot; &quot; </span></span> <span class="line"><span class="string"> Just save</span></span> <span class="line"><span class="string"> &quot; &quot; &quot; </span></span> <span class="line"> <span class="keyword">if</span> <span class="keyword">not</span> <span class="variable language_">self</span>.is_initialized: </span> <span class="line"> <span class="built_in">print</span>(<span class="string">&quot; Warning: Reconstructor not initialized. Nothing to save.&quot; </span>)</span> <span class="line"> <span class="keyword">return</span></span> <span class="line"> save_name = <span class="string">f&quot; recon_snapshot_<span class="subst">&#123; snapshot_id: 05d&#125; </span>.ply&quot; </span></span> <span class="line"> pts_count = <span class="built_in">len</span>(<span class="variable language_">self</span>.res_pcds)</span> <span class="line"> final_valid_mask = np.ones(pts_count, dtype=<span class="built_in">bool</span>)</span> <span class="line"></span> <span class="line"> <span class="keyword">if</span> <span class="variable language_">self</span>.res_valid_masks <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>: </span> <span class="line"> final_valid_mask & = <span class="variable language_">self</span>.res_valid_masks</span> <span class="line"> </span> <span class="line"> <span class="keyword">if</span> <span class="variable language_">self</span>.res_confs <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>: </span> <span class="line"> conf_masks = <span class="variable language_">self</span>.res_confs > conf_thres_res</span> <span class="line"> final_valid_mask & = conf_masks</span> <span class="line"></span> <span class="line"> valid_ids = np.where(final_valid_mask)[<span class="number">0</span>]</span> <span class="line"> </span> <span class="line"> <span class="keyword">if</span> <span class="built_in">len</span>(valid_ids) == <span class="number">0</span>: </span> <span class="line"> <span class="built_in">print</span>(<span class="string">f&quot; Warning for snapshot <span class="subst">&#123; snapshot_id&#125; </span>: No valid points left after filtering.&quot; </span>)</span> <span class="line"> <span class="keyword">return</span></span> <span class="line"> </span> <span class="line"> <span class="built_in">print</span>(<span class="string">f&#x27; Snapshot <span class="subst">&#123; snapshot_id&#125; </span>: Ratio of points filtered out: <span class="subst">&#123; (<span class="number">1.</span> - <span class="built_in">len</span>(valid_ids) / pts_count) * <span class="number">100</span>: <span class="number">.2</span>f&#125; </span>%&#x27; </span>)</span> <span class="line"> n_samples = <span class="built_in">min</span>(num_points_save, <span class="built_in">len</span>(valid_ids))</span> <span class="line"> <span class="built_in">print</span>(<span class="string">f&quot; Snapshot <span class="subst">&#123; snapshot_id&#125; </span>: Resampling <span class="subst">&#123; n_samples&#125; </span> points from <span class="subst">&#123; <span class="built_in">len</span>(valid_ids)&#125; </span> valid points.&quot; </span>)</span> <span class="line"> sampled_idx = np.random.choice(valid_ids, n_samples, replace=<span class="literal">False</span>)</span> <span class="line"> sampled_pts = <span class="variable language_">self</span>.res_pcds[sampled_idx]</span> <span class="line"> sampled_rgbs = <span class="variable language_">self</span>.res_rgbs[sampled_idx]</span> <span class="line"> save_path = join(save_dir, save_name)</span> <span class="line"> <span class="built_in">print</span>(<span class="string">f&quot; Saving reconstruction snapshot to <span class="subst">&#123; save_path&#125; </span>&quot; </span>)</span> <span class="line"> save_ply(points=sampled_pts, save_path=save_path, colors=sampled_rgbs)</span> </pre></td></tr></table></figure> <p>在每一个循环最后加以调用:</p> <figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span> <span class="line">2</span> <span class="line">3</span> <span class="line">4</span> <span class="line">5</span> <span class="line">6</span> <span class="line">7</span> <span class="line">8</span> <span class="line">9</span> <span class="line">10</span> <span class="line">11</span> <span class="line">12</span> <span class="line">13</span> <span class="line">14</span> </pre></td><td class="code"><pre><span class="line">reconstructor.add_frame(</span> <span class="line"> view=input_views[i], </span> <span class="line"> img=rgb_imgs[i], </span> <span class="line"> conf=per_frame_res[<span class="string">&#x27; l2w_confs&#x27; </span>][i], </span> <span class="line"> valid_mask=valid_masks</span> <span class="line"> )</span> <span class="line"> <span class="keyword">if</span> args.save_online: </span> <span class="line"> <span class="keyword">if</span> (i + <span class="number">1</span>) % args.save_frequency == <span class="number">0</span>: </span> <span class="line"> reconstructor.save_snapshot(</span> <span class="line"> snapshot_id=i + <span class="number">1</span>, </span> <span class="line"> save_dir=save_dir, </span> <span class="line"> num_points_save=num_points_save, </span> <span class="line"> conf_thres_res=conf_thres_l2w</span> <span class="line"> )</span> </pre></td></tr></table></figure> <p>OK ,到此为止我就写完了原本的处理逻辑的解释和新写的*<em>onlinee</em>处理逻辑介绍,其实要说不说,<strong>online</strong>处理逻辑也并非太过复杂,但是奈何我这几天因为学车耽误了太多时间也没做什么东西( x</p> <p>又水了一篇 blog😋</p> <h2 id="新的仓库:"><a href="#新的仓库:" class="headerlink" title="新的仓库:"></a>新的仓库:</h2></div></article></div><div class="card"><article class="card-content article" role="article"><div class="article-meta is-size-7 is-uppercase level is-mobile"><div class="level-left"><span class="level-item">Posted&nbsp;<time dateTime="2025-08-02T16:00:00.000Z" title="8/2/2025, 4:00:00 PM">Aug 03, 2025</time></span><span class="level-item">Updated&nbsp;<time dateTime="2025-08-02T16:00:00.000Z" title="8/2/2025, 4:00:00 PM">Aug 03, 2025</time></span><span class="level-item"><a class="link-muted" href="/categories/blog/">blog</a></span><span class="level-item">9 minutes read (About 1342 words)</span></div></div><p class="title is-3 is-size-4-mobile"><a class="link-muted" href="/blog/SLAM3R/">SLAM3R读后有感</a></p><div class="content"><p>最近几天读完了<a target="_blank" rel="noopener" href="https://github.com/PKU-VCL-3DV/SLAM3R">SLAM3R</a>的论文,这是 2025 年 CVPR 的一 篇<strong>Highlight</strong>论文,也是我在 3R 方向的读过的第 3 篇论文。</p> <p>这篇论文主要介绍了一个叫做<strong>SLAM3R</strong>的根据视频即时重建的系统,感觉是由<strong>DUst3R</strong>中获得的灵感,不同的是<strong>DUst3R</strong>是根据两张图片重建出三维点图,并且是离线处理;而<strong>SLAM3R</strong>是从一个单目视频中实时在线重建,并且相较于之前的一些方法具有极高的效率。</p> <h2 id="SLAM3R的主要模块"><a href="#SLAM3R的主要模块" class="headerlink" title="SLAM3R的主要模块"></a>SLAM3R的主要模块</h2><p>SLAM3R 主要由<strong>I2P</strong>和<strong>L2W</strong>两大模块组成,分别负责从视频中的关键帧重建点图(Image to Point)和利用点图增量式地重建全局点图( Local to World ), 具体结构如下:</p> <p><img src="/blog/SLAM3R/overalmodule.png" alt="nothing"></p> <h3 id="视频预处理"><a href="#视频预处理" class="headerlink" title="视频预处理"></a>视频预处理</h3><p>首先, SLAM3R 采用了滑动窗口算法将视频拆成多个小片段,把多个小片段输入到 I2P 中进行处理。</p> <h3 id="I2P网络"><a href="#I2P网络" class="headerlink" title="I2P网络"></a>I2P网络</h3><p>I2P 模块接受预处理产生的视频片段,该视频片段由多个帧组成。通常我们从中选取最中间的帧作为关键帧,剩下的个帧作为补充帧输入到 I2P 中。</p> <p>首先,我们将所有帧通过一个由个 ViT encoder 组成的,生成相应的 token ,然后再进行 decoder 操作。具体就是将关键帧的 token 输入到一个特殊处理的 decoder:里(如下图所示),然后剩下的个补充帧共享同一个 decoder 结构(继承自<strong>DUst3R</strong>,由个 ViT decoder 组成),均生成对应的。</p> <p><img src="/blog/SLAM3R/D_key.png" alt="0"></p> <p>然后,我们再使用类似于<strong>DUSt3R</strong>中的方法,将这些帧(尤其是关键帧)做出一个置信度最高的三维重建。从而得到某一个视频片段对应的点图。</p> <h3 id="L2W网络"><a href="#L2W网络" class="headerlink" title="L2W网络"></a>L2W网络</h3><p>这个模块接受 I2P 模块产生的作为输入,因为其是一个在线处理方法,所以我们引入了缓冲集这一关键的组分。</p> <p>首先,我们在已经处理完的关键帧点图中采用<code>reservoir strategy</code>选取个已经注册完的帧作为缓冲集(对于第一个帧这种特殊情况,我们采用了重复运行多次 I2P 获取足够多数量的初始帧作为缓冲集),然后,每当一个新的帧输入时,我们使用一个检索模块(由 I2P 中的 decoder 组成)在缓冲集中将特征的相似度进行匹配,我们然后选取匹配度最高的个关键帧点图,然后将这个关键帧点图 $$ \hat{X}_{i}^{H \times W \times 3},i = 1 , …, K + 1 $$作为这个模块的输入。</p> <p>如前图所示,我们将这个点图输入到我们的 L2W 模块的 encoder 中:
    \mathcal{P}i^{(T\times d)}=E{pts}(\hat{X}_i^{(H\times W\times3)}),i=1,…,K+1.
    \mathcal{F}_i^{(T\times d)}=F_i^{(T\times d)}+\mathcal{P}_i^{(T\times d)},i=1,…,K+1.̲K + 1个点图输入到两个解…" style="color:#cc0000">在这之后,我们便生成了每张点图的位置外观特征序列。</p> <p>紧接着,我们会这个点图输入到两个解码器中:</p> <h4 id="Registration-Decoder"><a href="#Registration-Decoder" class="headerlink" title="Registration Decoder"></a>Registration Decoder</h4><p>Registration Decoder 将所有 token 作为输入,然后目的是将 L2W 的关键帧重建转换到场景坐标系下,它与采用相同的架构。</p> <p>解码过程大概是:
    \mathcal{G}{sce_i}=D{sce}(\mathcal{F}{sce_i},\mathcal{F}{key}),\quad i=1,…,K</p> <h4 id="Scene-Decoder"><a href="#Scene-Decoder" class="headerlink" title="Scene Decoder"></a>Scene Decoder</h4><p>Scene Decoder 同样将所有 token 作为输入,但是它的目的是在不改变场景坐标系的情况下,精化坐标几何。他同样采用与相同的架构,但是他是对每一个在已选中的关键帧点图进行优化:
    \mathcal{G}{sce_i}=D{sce}(\mathcal{F}{sce_i},\mathcal{F}{key}),\quad i=1,…,K
    \tilde{X}_i^{(H\times W\times3)},\tilde{C}_i^{(H\times W\times1)}=\mathrm{H}(\mathcal{G}_i^{(T\times d)}),i=1,…,K+1.
    $$

    得到一个实时的三维表示。

    结论

    本人目前涉猎不深,但是论文最后与其他系统做比较,其展现的效率确实令我印象深刻,感觉以上的这个系统的两大模块也令非常简洁舒适。等我再去阅读其他的 3R 文章来进一步理解这个 SOTA 的含金量吧😋

    github 项目地址:

    喵喵又是充实的一天🥳,本人可能理解有偏差( bushi