简介:这份资源面向计算机视觉、三维重建方向的学习者与毕业设计开发者,提供一套基于NeRF、用手机拍摄物体图片即可完成三维重建的Python工程。包内包含可本地编译运行的源码、配套数据集与文档说明,评审分达95分以上,难度适中,适合作为课程实践、毕业设计或算法入门项目。压缩包共31个文件,约5.37MB,以21个py源码文件为核心,辅以5个pyc编译文件、2个txt说明、1个md文档及gif、jpg演示素材,覆盖数据加载、位姿估计、渲染路径生成、可视化与HTML导出等模块,并集成COLMAP相关工具脚本。已有196人学习关注。读者可据此掌握从手机多视角照片到NeRF训练、渲染与结果展示的完整流程,理解LLFF、DTU等数据组织方式,并借助现成脚本快速复现实验、排查环境依赖问题,节省自行搭建工程的时间。
1. 手机拍一圈照片就能建模:NeRF 三维重建到底能不能落地
你拿手机围着一个杯子、一双鞋或者一件手办拍上三四十张照片,丢进一段 Python 脚本里跑几个小时,就能得到一个可以任意角度旋转查看的三维模型——这件事在 NeRF(Neural Radiance Fields,神经辐射场)出现之后,从论文里的炫技变成了普通工程师也能复现的流程。它解决的核心问题是:传统摄影测量和结构光三维重建要么依赖昂贵的标定设备,要么对反光、透明、弱纹理表面束手无策,而 NeRF 用一组多视角图片加上隐式神经表示,把「场景长什么样」直接学进一个多层感知机里,渲染时按光线查询密度和颜色即可。这套「NeRF + 手机拍摄 + Python 源码 + 数据集 + 文档」的组合,适合三类人:想入门三维重建但买不起激光扫描仪的独立开发者、需要给电商商品做低成本 3D 展示的工程团队、以及拿它当毕业设计或课程作业的学生。下面我按自己踩过坑的顺序,把从环境配置到训练出可用模型的完整路径讲清楚。
2. 从手机照片到可训练数据集:采集、清洗与坐标约定
2.1 为什么手机拍摄的原始照片不能直接喂给 NeRF
NeRF 的训练输入不是「一堆图片」,而是「每张图片对应的相机位姿 + 内参」。手机相册里的 JPEG 只告诉你像素长什么样,不告诉你拍摄时相机站在哪里、朝向哪个方向。所以第一步永远是把图片转成带位姿的数据集,常见做法是用 COLMAP 做稀疏重建,输出每张图的旋转矩阵、平移向量和焦距。这里有个反直觉的点:照片数量不是越多越好。我试过用 120 张图重建一个马克杯,结果比 40 张图还差,原因是手机自动对焦导致不同帧的焦距轻微变化,COLMAP 匹配时把这些帧当成不同相机,位姿估计直接发散。所以采集阶段要锁死对焦和曝光,宁可少拍、拍稳。
采集时的具体约束我整理成一张表,照着做能省掉后面大量返工:
| 采集参数 | 推荐值 | 不这样做的后果 |
|---|---|---|
| 拍摄张数 | 30~60 张 | 少于 20 张位姿解算失败率高 |
| 环绕层数 | 2~3 层(高/中/低角度) | 单层导致顶部和底部空洞 |
| 重叠度 | 相邻帧画面重叠 70% 以上 | 重叠不足 COLMAP 匹配不上 |
| 对焦/曝光 | 手动锁定 | 自动对焦让内参漂移 |
| 背景 | 纯色、无纹理干扰 | 背景特征点抢走匹配资源 |
| 光照 | 均匀漫射光 | 高光区域在 NeRF 里变成漂浮雾团 |
2.2 用 COLMAP 生成 transforms.json 的完整命令
NeRF 的 Python 实现(无论是最早的 nerf-pytorch 还是后来的 instant-ngp 风格代码)普遍读取一个transforms.json,里面记录每张图的file_path、transform_matrix和相机camera_angle_x。这个文件不是 COLMAP 直接吐出来的,需要中间转换。下面是我常用的流程,先跑 COLMAP 命令行,再用脚本转格式。
# 1. 特征提取,--ImageReader.single_camera 1 强制所有图共用内参 colmap feature_extractor \ --database_path ./colmap.db \ --image_path ./images \ --ImageReader.single_camera 1 \ --ImageReader.camera_model OPENCV # 2. 穷举匹配,手机拍的小物体用 exhaustive_matcher 比 sequential 稳 colmap exhaustive_matcher --database_path ./colmap.db # 3. 稀疏重建,输出到 sparse/0 colmap mapper \ --database_path ./colmap.db \ --image_path ./images \ --output_path ./sparse # 4. 导出为文本格式,方便 Python 读取 colmap model_converter \ --input_path ./sparse/0 \ --output_path ./sparse/0 \ --output_type TXT这几条命令的逻辑是:feature_extractor在每张图上找 SIFT 特征点并写进数据库;exhaustive_matcher对所有图片两两匹配,因为物体小、图片少,穷举匹配比顺序匹配更不容易漏掉跨层视角;mapper做增量式 SfM,逐步加入图片并优化位姿;最后model_converter把二进制模型转成cameras.txt、images.txt、points3D.txt三个文本文件。参数上最需要注意的是--ImageReader.single_camera 1,它假设所有照片来自同一台相机、内参一致,这正是手机拍摄场景该用的设置;如果你混用了两台手机,就得去掉这个参数并给每台相机单独标定。
2.3 把 COLMAP 输出转成 NeRF 能读的 transforms.json
拿到文本文件后,用一段 Python 把四元数和平移向量拼成 4x4 变换矩阵,并计算camera_angle_x。这段代码我改过很多次,核心是坐标系转换:COLMAP 是 Y 轴向下、Z 轴向前,而 NeRF 的渲染代码通常假设相机看向 -Z,所以要对矩阵做一次翻转。
import numpy as np import json from pathlib import Path from scipy.spatial.transform import Rotation def colmap_to_nerf(images_txt, cameras_txt, out_json): # 读取相机内参,取第一个相机的焦距 with open(cameras_txt) as f: cam_line = f.readline().strip().split() width, height, fx = float(cam_line[2]), float(cam_line[3]), float(cam_line[4]) camera_angle_x = 2 * np.arctan(width / (2 * fx)) frames = [] with open(images_txt) as f: lines = [l.strip() for l in f if l.strip() and not l.startswith('#')] # images.txt 每两行描述一张图,第一行是位姿,第二行是特征点 for i in range(0, len(lines), 2): parts = lines[i].split() qw, qx, qy, qz = map(float, parts[1:5]) tx, ty, tz = map(float, parts[5:8]) name = parts[9] rot = Rotation.from_quat([qx, qy, qz, qw]).as_matrix() # COLMAP 到 NeRF 的坐标轴翻转 flip = np.diag([1, -1, -1]) rot = flip @ rot trans = flip @ np.array([tx, ty, tz]) mat = np.eye(4) mat[:3, :3] = rot mat[:3, 3] = trans frames.append({"file_path": f"./images/{name}", "transform_matrix": mat.tolist()}) with open(out_json, 'w') as f: json.dump({"camera_angle_x": camera_angle_x, "frames": frames}, f, indent=2) colmap_to_nerf("./sparse/0/images.txt", "./sparse/0/cameras.txt", "./transforms.json")逻辑说明:camera_angle_x是水平视场角,NeRF 用它配合图片宽高算光线方向,所以必须从 COLMAP 的焦距反推,不能随便填。flip矩阵做的是把 COLMAP 的相机坐标系翻到 NeRF 约定,这一步如果漏掉,训练出来的模型会上下颠倒或者整个场景跑到相机背后。参数上,Rotation.from_quat的输入顺序是[x, y, z, w],而 COLMAP 文本里是qw qx qy qz,顺序写反是新手最常见的翻车点,表现为渲染出来全是噪点。转换完打开transforms.json检查frames数量是否等于图片数,少一张都说明images.txt解析有问题。
3. 搭 NeRF 训练环境:Python 依赖、CUDA 版本与显存估算
3.1 依赖装不对,后面全是玄学报错
NeRF 的 Python 源码通常依赖 PyTorch、tiny-cuda-nn(如果用的是 instant-ngp 系实现)或者纯 PyTorch 的 MLP 版本。我一般先确认显卡驱动和 CUDA 版本,再决定装哪个 PyTorch。用nvidia-smi看驱动支持的最高 CUDA 版本,然后去 PyTorch 官网找对应命令。这里有个血泪经验:不要用pip install torch不带版本号,它可能给你装一个 CPU 版,训练时 GPU 利用率一直是 0,你还以为是代码问题。
# 查看驱动和 CUDA 版本 nvidia-smi # 以 CUDA 11.8 为例,装对应 PyTorch pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 验证 GPU 是否可用 python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"如果输出True和你的显卡型号,环境就通了。接着装numpy、scipy、imageio、opencv-python这些基础库。纯 PyTorch 版 NeRF 对显存的要求可以用公式粗估:显存 ≈ 图片分辨率像素数 × 每批光线数 × 网络层数相关常数。实际经验是 1080p 图片、batch 1024 条光线,8GB 显存能跑,但要把N_rand调小;4GB 显存基本只能降到 512×512 分辨率训练。
3.2 训练命令与关键参数怎么改
假设源码入口是run_nerf.py,典型训练命令如下:
python run_nerf.py \ --config configs/lego.txt \ --datadir ./data/lego \ --basedir ./logs \ --expname my_object \ --N_rand 1024 \ --N_samples 64 \ --N_importance 128 \ --netdepth 8 \ --netwidth 256 \ --learning_rate 5e-4 \ --N_iters 200000参数含义逐个说:N_rand是每次迭代随机采样的光线数,直接决定显存占用和训练速度,显存不够先降它;N_samples是粗网络沿每条光线采的粗采样点数,N_importance是细网络在粗采样高密度区域追加的点数,这两个值越大细节越好但越慢;netdepth和netwidth是 MLP 的层数和每层宽度,8 层 256 宽是原论文配置,物体小可以降到 6 层 128 宽;learning_rate用 5e-4 比较稳,调到 1e-3 容易发散。训练过程中看logs/my_object下的 loss 曲线,正常情况前几千步 loss 快速下降,之后缓慢收敛;如果 loss 一直震荡,先检查transforms.json里的位姿是不是错的。
3.3 数据集目录该长什么样
源码配套的数据集一般按场景分文件夹,每个场景下放images/和transforms.json(或者sparse/0/)。我习惯的目录结构是:
data/ my_object/ images/ IMG_001.jpg IMG_002.jpg ... transforms.json如果拿到的是官方数据集(比如 NeRF 论文的 lego、fern),它可能已经包含transforms_train.json、transforms_val.json、transforms_test.json三个文件,分别对应训练、验证、测试视角。自己拍的数据集只需要一个transforms.json,训练脚本会自动按比例划分。注意图片路径在 json 里是相对路径,跑训练时的工作目录要和 json 里的file_path前缀对得上,否则报FileNotFoundError。
4. 避坑与排查:训练不收敛、渲染有雾、显存爆掉的真实原因
4.1 训练几万步后画面还是糊的
现象:loss 降到某个值就不动了,渲染出来像蒙了一层毛玻璃。原因通常是位姿精度不够或者光线采样范围不对。手机拍摄的图片如果 COLMAP 重投影误差大于 1 像素,NeRF 会把误差学成模糊。解决办法是在 COLMAP 里开--Mapper.ba_global_function_tolerance=1e-6做全局 BA 优化,或者干脆用hloc这类更鲁棒的匹配流程重跑一遍。另一个原因是--near和--far参数设得离物体太远,光线大部分采样点落在空区域,密度网络学不到东西。我一般先量一下物体到相机的距离,把near设成最近距离的 0.8 倍,far设成最远距离的 1.2 倍。
4.2 渲染结果里出现漂浮的彩色雾团
现象:旋转模型时能看到一些半透明的色块悬在空中。原因是训练图片里有高光或者背景纹理,NeRF 把这些区域当成了真实几何。解决分两步:采集时用偏振片或者柔光箱压掉高光;训练时在transforms.json里给每张图加一个 mask,把背景涂黑,让 loss 只计算物体区域。很多源码支持--use_mask参数,配合images_mask/目录使用。如果源码不支持 mask,退而求其次是在 COLMAP 阶段用--ImageReader.mask_path指定掩码,让特征点只落在物体上。
4.3 显存爆掉但显卡明明够大
现象:报CUDA out of memory,但nvidia-smi显示显存还有富余。原因通常是 PyTorch 缓存分配器碎片化,或者N_rand设得太大导致单次前向传播就超了。先降N_rand到 512 试,如果还爆,在训练脚本开头加torch.cuda.empty_cache(),并把--N_importance降到 64。还有一个隐蔽原因是图片分辨率没降,源码默认读原图,手机照片 4000×3000 直接进网络,显存瞬间吃满。解决办法是在数据加载部分加 resize,或者提前用ffmpeg把图片缩到 800×600。
# 批量把图片缩到长边 800 像素 mkdir images_small for f in images/*.jpg; do ffmpeg -i "$f" -vf "scale='min(800,iw)':-1" "images_small/$(basename $f)" done4.4 训练完导出 mesh 全是碎片
现象:用 marching cubes 提取网格后,模型是一堆不连通的碎片。原因是 NeRF 的密度场在物体表面附近不够锐利,等值面提取时阈值选得不好。解决办法是训练时加--raw_noise_std 0.1让密度更平滑,导出时把阈值从默认的 10 降到 5 左右,并且用--N_grid 256提高采样网格分辨率。如果还是碎,说明位姿本身有漂移,回到 4.1 重新优化 COLMAP。
4.5 换一台机器跑就报版本不兼容
现象:在自己电脑上跑通的代码,换到服务器上报undefined symbol或者torch version mismatch。原因是 PyTorch、CUDA、tiny-cuda-nn 三者版本必须严格对应。我的习惯是用conda建独立环境,把environment.yml导出,里面锁死pytorch=2.0.1=py3.10_cuda11.8这种完整版本号。如果源码依赖 tiny-cuda-nn,它需要和 CUDA 版本一起编译,换机器必须重新pip install git+https://github.com/NVlabs/tiny-cuda-nn/#subdirectory=bindings/torch,不能直接拷贝编译好的.so文件。
5. 进阶技巧:用少样本和深度先验把重建时间压到半小时内
原始 NeRF 要跑十几小时甚至几天,这对想快速验证的工程师不友好。我后来固定用两条加速路线:一是少样本训练,把图片降到 20 张以内,配合--N_iters 50000和更大的学习率,半小时能出粗模;二是引入深度先验,用手机的人像模式或者单目深度估计模型(如 MiDaS)生成每张图的深度图,作为额外监督信号加到 loss 里,这样即使位姿有轻微误差,深度也能把几何约束住。
具体做法是在数据加载时多读一个depths/目录,训练 loss 改成:
# 在原有 color loss 基础上加深度监督 color_loss = ((rgb_pred - rgb_gt) ** 2).mean() depth_loss = ((depth_pred - depth_gt) ** 2).mean() loss = color_loss + 0.1 * depth_loss权重 0.1 是我试出来的经验值,太大导致颜色发灰,太小起不到约束作用。深度图不需要很准,MiDaS 输出的相对深度归一化到 [0,1] 就能用。这套组合让我在 RTX 3060 上把一个小物体的重建时间从 8 小时压到 40 分钟,精度损失肉眼几乎看不出来。
验证重建质量我一般看两个指标:PSNR 和 LPIPS。PSNR 高于 25dB 说明颜色还原可以,LPIPS 低于 0.1 说明感知质量接近真实。但指标只是参考,最终还是要旋转模型看有没有空洞和漂浮物。我现在的习惯是每次采集完先跑一遍 COLMAP,把重投影误差图存下来,误差大的区域直接重拍,而不是等 NeRF 训练完再返工。这个习惯帮我省掉了至少一半的无效训练时间。希望帮到你。
本文还有配套的精品资源,点击获取