城市尺度的三维重建如果沿用单模型NeRF,几乎会在训练开始前就撞上显存墙。几十万张倾斜摄影图像、上亿条采样射线,全部挤进一个多层感知机,不是收敛慢的问题,而是根本塞不进去。Mega-NeRF给出的解法是把整个城市按空间切成多个单元,每个单元用一个小型NeRF独立建模,渲染时根据射线穿过哪些单元来动态调度。这样做既降低了单模型容量压力,也让训练和推理可以天然分布到多块GPU上。

空间分区:把城市拆成可独立拟合的单元
Mega-NeRF并没有改变NeRF最核心的体渲染方程,而是从系统架构上做拆分。传统NeRF使用一个全局MLP映射三维位置和观察方向到颜色与密度,当场景扩大到城市范围时,这个MLP需要同时表达不同街区的几何细节和外观变化,容量很快耗尽。Mega-NeRF会在训练前先根据COLMAP等工具计算得到的稀疏点云确定场景边界,然后把整个边界框切成若干个空间单元。每个单元对应一个独立的轻量子网络,只负责预测落入该单元内部的采样点。
空间划分听起来简单,但实现时需要处理几个细节。首先是单元粒度如何选择,过粗会导致子网络退化回全局模型的困境,过细则会让某些单元看到的训练射线过少,容易过拟合。Mega-NeRF通常采用非均匀网格,建筑密集区域划分得更细,空旷区域划分得更粗。其次,射线可能横跨多个单元,因此需要根据采样点坐标动态路由到对应子网络。下面是一段简化的三维坐标到单元索引的映射逻辑,真实系统中还会加入边界加权来减少接缝。
def spatial_partition(points, grid_res, bounds_min, bounds_max):
# points: (N, 3), 将坐标归一化到[0,1]区间
norm = (points - bounds_min) / (bounds_max - bounds_min)
cell_idx = (norm * grid_res).long()
cell_idx = cell_idx.clamp(0, grid_res - 1)
return cell_idx[:, 0] * grid_res * grid_res + cell_idx[:, 1] * grid_res + cell_idx[:, 2]
这份代码展示的是均匀网格划分,生产环境中会根据点云密度或相机分布调整网格边界。另一个容易忽略的问题是边界采样:如果一条射线刚好穿过两个单元的交界处,只把采样点分配给其中一个单元会造成颜色跳变。Mega-NeRF的常见做法是让相邻单元之间保留一定的重叠区域,训练时对重叠区域内的采样点同时查询相邻子网络,最后按距离权重融合结果。这种方式虽然增加了少量计算,但能显著改善漫游时的视觉连续性。
户外外观建模与可见性过滤
城市户外数据有一个绕不开的问题:同一栋建筑在不同时间、不同天气条件下拍摄,外观差异很大。直接把所有图像混在一起训练,子NeRF会尝试用一个静态颜色场解释所有变化,最终结果往往偏灰或者出现漂浮伪影。Mega-NeRF为此给每个输入图像引入一个低维外观嵌入向量,与位置和方向一起送入网络。该向量让模型可以解释曝光差异、阴影变化甚至传感器差异,同时不牺牲几何一致性。
外观嵌入可以是训练中可学习的参数,也可以通过预训练图像编码器获得。实际工程中,给每个子模型维护一个外观编码表是更灵活的做法,加载某张训练图像时查询对应嵌入即可。渲染新视角时,可以选择最近邻图像的嵌入,也可以在潜空间做插值来获得期望的光照风格。这种机制对无人机倾斜摄影和街景数据尤为重要,因为同一区域经常由不同设备在不同时段拍摄。
class SpatialNeRF(nn.Module):
def __init__(self, d_input=3, d_dir=3, d_latent=16):
super().__init__()
self.density_head = nn.Sequential(nn.Linear(d_input + d_latent, 256), nn.ReLU(), nn.Linear(256, 1))
self.color_head = nn.Sequential(nn.Linear(256 + d_dir, 128), nn.ReLU(), nn.Linear(128, 3))
self.latent_table = nn.Embedding(20000, d_latent)
def forward(self, x, d, img_id):
z = self.latent_table(img_id)
h = torch.cat([x, z], dim=-1)
sigma = self.density_head(h).squeeze(-1)
color = self.color_head(torch.cat([h, d], dim=-1))
return sigma, color
可见性过滤是另一个决定Mega-NeRF能否真正落地的关键优化。城市场景中一条射线可能穿过很多空间单元,但大部分单元其实与这条射线没有交集。如果让所有子网络都推理一遍,计算量会成倍增加。Mega-NeRF通过为每个子模型建立包围体或BVH结构,渲染时先计算射线与包围体的交点,只把采样点发送给真正相关的子网络。经过过滤后,单次渲染的推理量可以减少数倍,这对于批量生成倾斜摄影三维模型非常重要。
外观编码和可见性过滤常常配合使用。外观编码解决的是颜色一致性,可见性过滤解决的是计算效率。两者叠加之后,Mega-NeRF才有能力在有限算力下处理几十平方公里的场景。需要注意的是,外观嵌入维度不宜设置过高,否则子模型会倾向于用外观编码来解释几何误差,导致训练初期可以快速降低损失,但重建后的表面出现模糊或孔洞。
分布式训练与工程落地细节
Mega-NeRF的空间分区设计天然契合分布式训练。每个空间子模型可以分配到不同GPU甚至不同机器上,数据加载器根据相机位姿和射线方向筛选出与该子模型相关的像素,本地计算梯度后通过参数服务器或AllReduce同步。与单模型NeRF相比,这种并行方式避免了所有数据都经过同一张显卡,也减少了通信开销,因为每个worker只处理局部数据。
实际部署时会遇到不少工程坑。首先是内存管理,城市级数据集往往包含几十万张图片,如果一次性加载会直接撑爆内存。Mega-NeRF通常采用按需加载和缓存策略,优先读取当前训练子模型附近的图像,训练完再释放。其次是位姿精度,如果COLMAP结果存在漂移,分区边界会出现重影或几何断裂,最好在训练前做一次全局光束法平差和闭环优化。另外,外观嵌入表的更新频率需要控制,频繁更新容易导致外观漂移,一般会降低学习率或冻结一段轮次。
# 用多进程启动多个空间子模型训练 python train_mega_nerf.py \ --grid_res 4 3 3 \ --data_root /data/city_oblique \ --cache_dir /tmp/mega_cache \ --workers 8 \ --appearance_dim 16 \ --overlap_ratio 0.08
渲染阶段的调度同样重要。给定一个新视角,先根据相机参数计算每条射线穿过的子模型列表,然后按子模型分批查询。最后把所有子模型的输出按采样权重合并,合并时处理边界重叠常用的方式是距离加权平均,也可以使用学习到的融合权重。对于城市级数字孪生项目,渲染通常不是实时在线进行,而是离线烘焙成网格或点云,因此推理延迟可以接受,重点放在几何精度和外观一致性上。
对比单模型NeRF:收益、代价与适用边界
与单模型NeRF相比,Mega-NeRF最直接的收益是可扩展性。单模型在城市场景下往往只能大幅降低采样频率,导致远处建筑细节丢失,而Mega-NeRF可以用多个子模型分别负责不同区域,每个子模型保持较高采样密度,整体细节更丰富。训练时间上,尽管子模型数量增加,但每个子模型收敛更快,配合多卡并行,实际总训练时间可能反而更短。
代价是分区间的一致性维护。边界处的几何和外观可能出现轻微不连续,需要额外的后处理或重叠融合来缓解。此外,Mega-NeRF引入了更多超参数,比如网格分辨率、重叠宽度、外观编码维度、学习率衰减策略等,这些参数对最终效果有显著影响,调参成本明显高于单模型。它还假设场景是静态的,对于动态物体频繁出现的城市道路,需要额外的时间维度建模或先做动态物体移除。
适用边界方面,如果场景规模只有几百平方米,单模型NeRF仍然是最简单稳定的选择。当面积扩大到几平方公里甚至几十平方公里,并且需要保留建筑立面细节时,Mega-NeRF的分区策略才体现出明显优势。对于园区、校园或城市街区级别的实景三维重建,Mega-NeRF提供了一条工程上可行的路线,尤其适合与无人机倾斜摄影、激光雷达点云融合,为数字孪生底座提供高保真几何和外观数据。