☰
无人机航拍三维重建实战:NeRF结合COLMAP与姿态优化全链路
2026/10/1 12:27:16 网站建设 项目流程

简介:面向无人机航拍场景的三维重建项目源码包,适合计算机视觉、摄影测量方向的研究者与开发者实战学习。项目覆盖图像获取、特征提取、相机标定、特征匹配、点云生成、网格构建与纹理映射等关键环节,并提供较完整的Python算法实现与可复现的工程目录结构。压缩包共54个文件,以41个Python脚本为核心,另含yaml环境与参数配置、ipynb交互式示例、txt运行记录、png/mp4/gif结果演示及README说明,整体约20.66MB,便于快速部署和研读。目前已有132人学习下载,适合希望在真实航拍数据上复现完整三维重建流程并继续改进算法的学习者。源码中可重点研读姿态对齐、轨迹误差计算、深度估计、正射投影生成、表面重建与体积计算等模块,配套结果图片与视频能直观对照重建效果;从数据预处理、模型训练到评估输出的脚本链条,为扩展城市建模、地形测绘等应用提供了清晰参照。

1. 无人机航拍三维重建:当NeRF遇上低空影像

三维重建一直是计算机视觉里最绕不开的硬骨头,而无人机航拍又把骨头的难度抬高了一个级别:大视角场景、非均匀光照、六自由度相机轨迹,传统 SFM+MVS 管线在弱纹理区域几乎必翻车。这套源码走的是另一条路,用 NeRF 体渲染把航拍图像直接转成带相机姿态估计的三维场景,从图像预处理、姿态对齐、ATE 精度评估到正射投影、体积计算全链路打通。它不是教学 demo,而是能跑真实无人机数据的工程包,适合正在做无人机测绘、工地数字孪生、案场三维建模,或者想把 NeRF 姿态优化搞懂的人。下面按我拆包的实际顺序,从数据准备讲到排坑和进阶用法。

2. 数据准备与预处理:把航拍影像变成模型能吃的训练数据

2.1 无人机影像采集的前置要求

这套算法对输入图像的质量要求比普通航测严。飞行时我建议航向重叠率至少 80%,旁向重叠率不低于 70%,否则后续特征提取和姿态估计在树木、水面这类弱纹理区域直接哑火。云台角度一般控制在 -60° 到 -90° 之间,保证地面有足够纹理覆盖;航高根据地面分辨率倒推,曝光优先用小光圈保证景深一致。光照上挑太阳高度角高的时段,阴影过长的早晚航片会使 NeRF 的颜色场和几何场互相打架。有 RTK 或 PPK 更好,姿态先验越准,后面可微姿态优化收敛越快。

2.2 SFM 姿态估计与数据组织方式

拿到原始航片后,第一步是用 SFM 算出每张图的位姿,这一步决定了后续 NeRF 训练的起点。常见做法是用 COLMAP 跑完整流程:特征提取、特征匹配、稀疏重建、BA 优化,导出相机内参和位姿。数据包里的get_matrix_by_sfm.py就是干这件事的,它把 COLMAP 的images.bin和cameras.bin转成 NeRF 训练需要的poses矩阵和intrinsics配置:

python get_matrix_by_sfm.py \ --colmap_dir /data/colmap_out \ --output_dir /data/nerf_ready \ --image_dir /data/raw_images

--colmap_dir是 COLMAP 工程输出目录,--image_dir是原始影像目录,--output_dir生成poses.npy、intrinsics.npy和划分好的训练/验证图像列表。这个脚本的核心逻辑是把 COLMAP 的相机坐标系到世界坐标系的变换矩阵,转成 NeRF 里相机在世界坐标系下的位姿矩阵,同时完成单位统一。注意 COLMAP 输出的平移向量是世界系到相机系,需要取反再转置,这个坑我在用其他开源库时踩过好几次。

2.3 深度先验与预处理脚本化

纯 NeRF 在无人机大场景上收敛很慢,因为航拍视角之间基线大,光度一致性能提供的约束有限。项目里用 DPT 生成单目深度先验来给几何场加引导,dpt_depth.py和dpt_depth.ipynb都能跑。DPT 是密集预测 Transformer,对无人机影像这种大尺度变化场景鲁棒性比传统立体匹配好很多:

# dpt_depth.py 关键调用示意 from dpt import DPTDepthModel model = DPTDepthModel(backbone="vitb16", head="dpt") model.load_state_dict(torch.load("dpt_hybrid_384.pt")) model.eval().to("cuda") for img_path in image_list: img = load_image(img_path, normalize=True) # 归一化到 [0,1],resize 到 384 with torch.no_grad(): depth = model.forward(img) # 输出逆深度,值越大代表越近 save_depth(depth, img_path)

这里backbone="vitb16"是 ViT-B/16 的编码器,dpt_hybrid_384是混合骨干网络在 384 分辨率下预训练的权重。生成的是逆深度,数值范围不固定,后续训练时会被归一化到当前场景的 near-far 区间。我一般会把深度图的采样步长和 NeRF 的光线采样范围对齐,避免深度先验和体渲染区间错位。

2.4 配置文件 default.yaml 与 preprocess2.yaml 解读

数据预处理参数集中在configs/default.yaml和configs/preprocess2.yaml里,二者是复现时最需要改的两个文件:

参数项典型值含义
resolution0.5影像下采样比例,显存紧张时调小
near_plane/far_plane2.0 / 200.0光线采样范围,按场景尺度设置
min_depth/max_depth0.1 / 1000.0深度先验的截断范围
train_ratio0.9训练集划分比例,剩余做验证
pose_optimizetrue是否开启姿态优化
rand_rays1024每帧随机采样光线数

preprocess2.yaml还多一组crop_ratio和resize_long_side参数,用于去掉航片边缘的无效区域。无人机照片四周常有桨影和云台遮挡,不裁剪会把大量无效像素喂给网络。Tanks 配置则对应 Tanks-and-Temples 数据集的格式,说明这套源码也兼容公开数据集,不一定非要自己飞一遍。

3. 核心网络与几何:NeRF 体渲染里的相机参数与姿态优化

3.1 采样策略与体渲染积分

模型主体是标准的 NeRF 结构,model/official_nerf.py里实现了射线采样、位置编码和体渲染积分。无人机场景相比室内场景最大的区别是 near-far 距离大,同一个场景里可能既有几十米外的山体,又有几米内的建筑立面。所以分层采样是刚需:粗网络在整段[near, far]上均匀采样 64 个点,拿到密度分布后,细网络再在密度高的区域二次采样 128 个点,这样能把计算资源集中在真正有几何的位置。

体渲染积分的核心是沿着射线累积密度和颜色:

# 体渲染积分示意,official_nerf.py 中已实现 def render_rays(network, rays, near, far, n_samples): z_vals = torch.linspace(near, far, n_samples) # 分层采样:在相邻采样点之间加均匀扰动 z_vals = z_vals + torch.rand_like(z_vals) * (far - near) / n_samples pts = rays.o[..., None, :] + rays.d[..., None, :] * z_vals[..., None] raw = network(pts) # 密度转透射率,颜色做加权求和 alpha = 1 - torch.exp(-raw["sigma"] * delta) transmittance = torch.cumprod(1 - alpha + 1e-10, dim=-1) rgb = torch.sum(transmittance * alpha * raw["rgb"], dim=-1) return rgb

torch.cumprod累积的是射线一路打过来没被遮挡的概率,即透射率。delta是相邻采样点间的距离,真正物理意义的体密度要通过指数映射1 - exp(-sigma * delta)来计算,不是直接用网络输出的 sigma。这套代码把粗网络和细网络放在同一个 forward 里,粗网络预测的深度分布会作为细网络采样区间采样的依据。

3.2 可微姿态优化:无人机场景为什么特别需要

无人机靠 GPS/IMU 提供的位姿精度远达不到 NeRF 的收敛要求,尤其是低空近景拍摄时,姿态角度的微小偏差会导致渲染的投影误差被放大。项目在训练阶段把相机外参作为可学习变量,与网络权重一起优化,pose_optimize开关就在model/training.py里控制。

姿态优化不是简单地把 4x4 矩阵丢给反传,而是要在 SE(3) 流形上更新,防止旋转和平移的梯度互相污染。utils_poses/lie_group_helper.py实现了李群和李代数的映射:

# 姿态增量更新:用李代数扰动表示,而不是直接加在矩阵上 def se3_exp_update(pose, delta): # delta: [6],前3维是旋转向量,后3维是平移 axis_angle = delta[:3] translation = delta[3:] R = axis_angle_to_rotation_matrix(axis_angle) # Rodrigues 公式 pose = torch.cat([R, translation.unsqueeze(-1)], dim=-1) return pose

实际更新时,每一轮迭代先由优化器给出 6 维扰动(3 维旋转向量 + 3 维平移),再通过指数映射作用到当前位姿上。这样做的好处是旋转和平移在各自的参数空间里被控制在合适尺度,不会出现平移梯度主导、旋转几乎没有更新的情况。我一开始直接把位姿矩阵当普通张量去优化,加了几天结果严重倾斜,后来切成李代数扰动才正常收敛。

3.3 损失函数与训练策略

损失函数在model/losses.py里,主体是渲染 RGB 和真实图像的 L1 损失,数据集里loss.txt记录了实际训练损失曲线。关键是在基础颜色损失之上,还有两个对无人机场景非常重要的正则项:

第一个是畸变正则,在model/distortions.py中实现,它惩罚沿射线的密度分布过于分散,促使网络把密度集中在真实表面附近。无人机航拍场景里天空占比大,不加这个正则时,网络常把蓝天也拟合出一层浅密度,导致远处地面整体浮起来。第二个是深度先验正则,把 DPT 生成的单目深度作为软约束,让体渲染的期望深度接近先验值,这个损失权重一般设置在 0.1 到 0.5 之间,太大会被错误深度带偏,太小等于没加。

训练策略上,学习率采用 warmup + 指数衰减,前 2k 步用 1e-3 的线性增长,之后按步数指数衰减到 1e-5。姿态优化器用单独的学习率,通常比网络权重低一个数量级,避免刚开始训练时姿态抖动太大把几何场搅乱。

4. 训练、评估与轨迹对齐:ATE 是如何算出来的

4.1 训练脚本与核心参数

入口是train.py,跑起来之前先把上面的 YAML 配置路径改对:

python train.py --config configs/default.yaml --data_dir /data/nerf_ready --log_dir logs/drone1

几个关键的训练参数需要按场景调整:

参数推荐范围说明
batch_size512 ~ 2048每批次采样的光线数,直接影响显存
num_iterations30k ~ 60k航拍场景建议 50k 起步
learning_rate1e-4 ~ 5e-4网络权重初始学习率
pose_lr1e-5 ~ 1e-4姿态优化学习率
n_samples_coarse64粗采样点数
n_samples_fine128细采样点数

训练日志会输出每个 batch 的颜色损失和深度损失,同时定期保存checkpoints和渲染预览图。数据包里的get_log_to_txt.py可以把 TensorBoard 日志转成纯文本,方便在服务器上无界面环境里监控收敛情况。

4.2 ATE 评估工具链

ATE(Absolute Trajectory Error)是衡量估计相机轨迹与真实轨迹差异的指标,评估代码集中在项目根目录和evaluation/下。完整链路依赖transformations.py提供四元数、旋转矩阵、平移向量的格式转换,align_trajectory.py实现轨迹对齐,compute_trajectory_errors.py计算各帧误差,最后由results_writer.py汇总输出。

运行评估的方式是:

python eval_poses.py \ --gt_poses /data/gt_poses.npy \ --est_poses /logs/drone1/est_poses.npy \ --output results/ate_result.json

--gt_poses是真实位姿,一般来自 RTK 后处理结果或 COLMAP 的 BA 输出;--est_poses是网络训练过程中保存的优化后位姿。这里有个评估规范问题:ATE 要求先把估计轨迹和真实轨迹对齐到同一坐标系,常用是 Umeyama 算法的 SE(3) 对齐,只计算相对轨迹误差,而不是直接比较绝对坐标。因为这不仅是衡量姿态优化精度的需要,也能减小不同采集场景之间的坐标系漂移影响。

4.3 轨迹可视化与检查

vis_cam_traj.py和align_traj.py提供轨迹可视化与对齐结果输出。我习惯先看三维轨迹图,再去看 ATE 数字,因为数字只能告诉你精度,图能告诉你错在哪。无人机航拍常见的轨迹问题是绕飞时姿态漂移呈螺旋状,这在数值指标里可能被平均掉,但可视化后一眼就能看到。

results_writer.py除了写指标,还会把逐帧误差按时间轴排序,检查误差是不是集中在某些转弯大的帧上。如果逐帧误差分布不均匀,大概率是某些帧的特征约束太弱,回头补飞那个区域比盲目调参有效得多。

5. 避坑与常见问题排查:无人机数据集训练 NeRF 的四类典型翻车

5.1 训练到一半 loss 突然变 NaN

现象:前几千步损失正常下降,突然某个 iteration 后 loss 变成 nan,继续跑也回不来。

原因:最常见是深度先验里有异常值,DPT 在纯白色建筑墙面或强反光玻璃上会输出接近无穷的深度,归一化后这些异常点产生的梯度异常大,直接把网络权重和姿态参数推到数值溢出区。另一个原因是学习率过高,姿态优化器和网络优化器共用学习率导致姿态在 SE(3) 上震荡过大。

解决:打开get_depth.py里对深度先验的截断逻辑,将min_depth和max_depth按场景实测距离收紧到正常范围,比如航高 100 米的场景就设1.0到150.0。同时按第 4 章建议把姿态优化的学习率独立设置为网络学习率的十分之一。改了这两处之后,同样的数据能稳定跑完 50k 步。

5.2 重建结果整体扭曲、地面呈曲面状

现象:训练正常收敛,渲染图像锐利清晰,但把重建出的深度图拉成三维网格后,地面不是平面而是明显拱起或凹陷。

原因:这是无人机场景最隐蔽的一个坑——姿态优化在缺乏绝对尺度约束时,会把场景几何和相机轨迹一起缩放或弯曲。单目深度先验只提供相对尺度,没有绝对尺度对齐;如果 SFM 阶段没有正确进行尺度恢复,NeRF 姿态优化会把尺度漂移吸收进几何场,造成地面弯曲。

解决:检查 SFM 输出是否有全局一致的尺度。建议用 RTK 测几个地面控制点,在get_matrix_by_sfm.py里加入控制点约束,或者用已知高度的地物(比如一栋楼的高度)对重建结果做后验缩放矫正。场景里至少保证有三个不共线的控制点,效果立竿见影。

5.3 ATE 指标很好但渲染图像模糊

现象:ATE 只有几厘米,跑出来的轨迹精度很高,但渲染图像细节丢失,瓦片状或雾状模糊。

原因:ATE 只评估相机位姿,不评估几何场质量。模糊一般是光线采样数量不足或最大频率的位置编码不够。无人机场景视场大,高频细节多,默认的位置编码只能到 10 级频率,远处地物的纹理被压平了。另外粗采样点只有 64 个,对近处物体表面太稀疏。

解决:把n_samples_coarse提到 128,n_samples_fine提到 256,位置编码最高频率从 10 级提到 12 级,同时把rand_rays提高到 2048。显存吃紧就优先保采样点数,分辨率可以降一点。

5.4 轨迹对齐参数不一致导致指标虚高

现象:自己复现时算出的 ATE 比 README 里报的低很多,甚至低一个数量级。

原因:README 评估用的是先做 SE(3) 对齐再算 RMSE,而我第一次评估时直接比较原始绝对位姿,算出来的误差大得离谱。反向也有坑——如果对齐时只对齐了旋转没有对齐平移,或对齐用的参考帧数太少,误差会被算法强行压低。ATE 是对齐后指标,对齐本身的质量直接影响数字。

解决:严格按align_trajectory.py的参数来,确认对齐至少使用轨迹上均匀分布的 10 帧以上,不要只用首尾两帧做对齐。如果想和图里指标对比,必须确认训练集、验证集划分完全一致,姿态优化的随机种子也要固定。

5.5 显存不够但降低分辨率后特征丢失

现象:16G 显存跑 2K 分辨率炸显存,降到 1K 后重建结果细节明显变差,树木和电线杆糊成一片。

原因:无人机影像下采样后,小地物在图像上只有几个像素,特征点密度不足,NeRF 的高频细节直接丢失。盲目降分辨率治标不治本。

解决:我一般先把原始航片按 1/2 下采样做第一轮粗训练,确认几何结构合理后,再加载粗模型做第二阶段精修,第二阶段用原始分辨率但只训练有限的迭代次数。这种 coarse-to-fine 方式既稳又省显存,比单次高分辨率训练效果好得多。数据包里的preprocess2.yaml就是为两阶段预处理设计的。

6. 进阶用法:从重建结果到正射投影与体积测算

6.1 基于 SFM 矩阵的正射投影

重建完成后除了看渲染视频,还可以直接出正射影像。scripts/get_orthographic_by_arcgis.ipynb把网络优化后的相机矩阵和深度图结合起来,把每个像素投影到地平面网格上,生成正射投影图,数据包里的正射投影.png就是生成结果之一。

核心思路是:对每个渲染出的深度图,利用相机内参和位姿矩阵,把图像像素反投影到世界系下的三维点,再按地平面网格做插值填色:

# 正射投影生成示意 for i, (depth, pose, intrinsics) in enumerate(rendered_data): K = intrinsics # [3,3] R, t = pose[:3, :3], pose[:3, 3] u, v = meshgrid(w, h) coords = stack([u, v, ones]) rays = inv(K) @ coords # 相机系下的方向向量 world_pts = R.T @ (rays * depth - t.T) # 反投影到世界系 ortho_img = scatter_add(ortho_img, world_pts[:, :2], rgb)

这里inv(K)是把像素坐标转成相机系下的射线方向,乘以深度后得到相机系下的三维点,再用旋转矩阵R.T和平移t转到世界系。正射影像平面通常是 XY 平面,Z 方向是高度。生成后要检查边缘是否有孔洞,无人机转弯处覆盖不足导致空洞是正常的,用插值或补飞填补即可。

6.2 体积估算与表面重建

有了深度图和位姿,还能直接算土方量。scripts/get_volume.py和get_volume2.py提供两种方案:一种直接把密集深度图按网格化成体素计数,另一种用get_volume_by_surface_reconstruction.py先做表面重建再算体积。后者更稳,因为深度噪声被网格化过程平滑掉了。

python scripts/get_volume_by_surface_reconstruction.py \ --depth_dir /logs/drone1/depth \ --pose_file /logs/drone1/est_poses.npy \ --intrinsics_file /data/nerf_ready/intrinsics.npy \ --height_range 50.0 120.0 \ --output_volume results/volume.json

--height_range是体积计算的垂直范围,按场景实际地物高度设置。输出会给出总立方体积和按高度分层的表。注意体积计算的精度上限不会超过深度图的精度,一般误差在 10% 以内属于正常。项目里自带的mountain2.gif和gaoqing.mp4就是从重建结果渲染出来的旋转观察和清晰度检查效果,跑完后应该能复现同样的可视化形态。

6.3 深度验证与质量控制

最后一步建议做深度一致性验证,不要只看渲染画面。用scripts/get_depth.py导出训练好的深度图,和 DPT 先验深度做逐像素对比,两者偏差大的区域往往就是几何塌陷的位置。如果某块区域偏差超过 30%,先补图像再重训局部,比全局重训便宜得多。

我踩过最深的一个坑就是这个验证环节:第一次做完正射投影,表面网格看着精致,实际放到 GIS 软件里和 RTK 实测高程一比,局部误差超过半米。后来养成习惯,每次跑完全流程都要用控制点高程做交叉验证,从那以后每次交付重建结果都强制走一遍正射投影 + 控点检查,这套流水线已经成了我处理无人机数据的基本盘。希望这套源码和这些经验能帮到你落地自己的无人机三维重建项目。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询