RealityScan 2.2 + NerfStudio
数据转换排错记录
使用 RealityScan 2.2 对齐照片并导出 CSV 与 COLMAP 格式,再用 NerfStudio 的 ns-process-data realitycapture 转换为 transforms.json 训练 3DGS。记录"CSV 列名不一致"类报错的完整排查与修复过程。
结论速览
适用场景:使用 RealityScan / RealityCapture 2.2 对齐照片并导出
Internal/External camera parameters(CSV)与 COLMAP 格式(sparse/、images/),再通过 NerfStudio 的ns-process-data realitycapture转换为transforms.json以训练 3D Gaussian Splatting。根本原因:RealityScan 2.2 导出的 CSV 表头列名与 NerfStudio 1.1.5 源码期望的列名不一致(仅改名、数值语义未变)。通过一个列名映射脚本修复后,转换成功,且经稀疏点云 PnP 重投影独立验证,生成的相机姿态与 RealityScan 对齐结果几何一致(重投影误差 ~0.6 px)。

1. 环境信息
| 项目 | 值 |
|---|---|
| 操作系统 | Windows(命令行编码 GBK,bash shell) |
| 显卡 | NVIDIA P104 |
| 采集软件 | RealityScan 2.2(RealityCapture 2.x 系列) |
| 重建框架 | NerfStudio 1.1.5(Python 3.10.11) |
| 转换命令 | ns-process-data realitycapture |
1.1 数据目录结构
D:\temp\ReasonixChat\3\
├── orin-images\ # 原始图像(手机拍摄):IMG_20260702_165925.jpg ~ IMG_20260702_170007.jpg,共 16 张
├── images\ # RealityScan 导出的 COLMAP 配套图像:00000.png ~ 00015.png(已重命名,共 16 张)
├── sparse\0\ # RealityScan 导出的 COLMAP 稀疏重建(文本格式)
│ ├── cameras.txt # 16 个相机,PINHOLE 模型(无畸变)
│ ├── images.txt # 16 张图像的位姿(qvec + tvec)+ 2D 特征点
│ └── points3D.txt # 23513 个稀疏 3D 点
└── 3.csv # RealityScan 2.2 "Internal/External camera parameters" 导出,16 行 注意:
images/是 RealityScan 导出 COLMAP 时重命名过的图像(00000.png…),与orin-images/中的原始文件名不对应。

2. 问题现象
执行标准转换命令后报错:
ns-process-data realitycapture --data {data directory} --csv {csv file} --output-dir {output directory} 2.1 第一层现象(Windows 特有)
直接运行时报出 UnicodeEncodeError,掩盖了真实错误:
UnicodeEncodeError: 'gbk' codec can't encode character '\U0001f389' in position 0: illegal multibyte sequence 原因:Windows 控制台默认 GBK 编码,无法输出 rich 打印的 🎉 emoji。NerfStudio 捕获异常后再打印错误信息时再次触发该编码异常,导致看不到真正的错误内容。
处置:加环境变量后重跑,暴露真实错误:
PYTHONIOENCODING=utf-8 ns-process-data realitycapture --data orin-images --csv 3.csv --output-dir out_test 2.2 第二层现象(数据目录选错时)
--data images(RC 重命名后的图像目录)时,不报错但结果为空:
Missing image data for 16 cameras.
Missing camera data for 16 frames.
Final dataset is 0 frames. 原因见 3.2。
2.3 第三层现象(核心报错)
--data orin-images(原始图像目录)时,真正的错误暴露:
File "...\nerfstudio\process_data\realitycapture_utils.py", line 82, in realitycapture_to_json
frame["fl_x"] = float(cameras["f"][i]) * scale / 36.0
KeyError: 'f' 即 CSV 中不存在名为 f 的列。
3. 问题根源
3.1 核心根源:CSV 列名与 NerfStudio 源码期望不一致
NerfStudio 1.1.5 的 realitycapture_utils.py::realitycapture_to_json() 通过 csv.DictReader 按列名取值:
| 用途 | NerfStudio 1.1.5 期望列名 | RealityScan 2.2 实际导出列名 |
|---|---|---|
| 图像文件名 | #name | #name ✅ |
| 位置 X | x | x ✅ |
| 位置 Y | y | y ✅ |
| 高度 | alt | alt ✅ |
| 偏航角 | heading | yaw ❌ |
| 俯仰角 | pitch | pitch ✅ |
| 翻滚角 | roll | roll ✅ |
| 35mm 等效焦距 | f | f_35mm ❌ |
| 归一化主点 x | px | px_norm ❌ |
| 归一化主点 y | py | py_norm ❌ |
| 径向畸变 | k1~k4 | k1~k4 ✅ |
| 切向畸变 | t1, t2 | t1, t2 ✅ |
源码中的关键取值代码(realitycapture_utils.py 第 65~98 行):
for i, name in enumerate(cameras["#name"]): # 行 65
...
frame["fl_x"] = float(cameras["f"][i]) * scale / 36.0 # 行 82 → KeyError: 'f'
frame["cx"] = float(cameras["px"][i]) * scale + width / 2.0 # 行 84
frame["cy"] = float(cameras["py"][i]) * scale + height / 2.0 # 行 85
...
rot = _get_rotation_matrix(-float(cameras["heading"][i]), # 行 94
float(cameras["pitch"][i]),
float(cameras["roll"][i])) 由于 16 帧全部在访问 cameras["f"] 之前因图像名匹配失败而 continue(见 3.2),程序会先表现出"0 frames";一旦图像名匹配成功,立即触发 KeyError: 'f'。
为什么可以断定只是改名、语义未变?
- RealityCapture 校准模板(calibration.xml)中,旧版表头写的就是
heading,而其取值变量一直是$(yaw);RealityScan 2.2 只是把表头改成了yaw。- 通过稀疏点云 PnP 重投影独立验证(见第 5 章),映射列名后生成的姿态与 RealityScan 的 COLMAP 稀疏重建几何一致,证明
yaw/pitch/roll、x/y/alt的数值语义与旧版完全一致。
3.2 --data 目录必须指向原始图像
NerfStudio 处理流程(process_data.py::ProcessRealityCapture.main):
- 从
--data目录收集图像,按文件名 stem(去掉扩展名)建立image_filename_map。 - 把图像复制到输出目录并重命名为
frame_00000.jpg等(不影响匹配,因为匹配发生在复制之前的原始名称)。 - 在
realitycapture_to_json()中用 CSV 的#name去掉扩展名后到image_filename_map中查找。
因此:
- CSV 的
#name是原始文件名IMG_20260702_165925.jpg→--data必须指向orin-images/。 - 若指向
images/(00000.png),IMG_20260702_165925查不到 → 全部帧被跳过 →Missing image data for 16 cameras / Final dataset is 0 frames。
3.3 次要问题:Windows 控制台 GBK 编码
NerfStudio 使用 rich 输出含 emoji 的日志,Windows 默认 GBK 控制台无法编码,抛 UnicodeEncodeError 并覆盖真正的异常信息。排查此类问题时建议统一加 PYTHONIOENCODING=utf-8。
4. 解决方法
4.1 一键修复脚本:fix_rc_csv.py
将 RealityScan 2.2 表头映射为 NerfStudio 期望的表头,生成新 CSV(不修改原始文件)。
"""Map RealityScan 2.2 CSV column names to the names nerfstudio 1.1.5 expects.
Usage:
python fix_rc_csv.py <input.csv> [output.csv]
nerfstudio 1.1.5 realitycapture_utils.py expects:
#name, x, y, alt, heading, pitch, roll, f, px, py, k1, k2, k3, k4, t1, t2
RealityScan 2.2 exports:
#name, x, y, alt, yaw, pitch, roll, f_35mm, px_norm, py_norm, k1, k2, k3, k4, t1, t2
"""
import csv
import sys
COLUMN_MAP = {
"yaw": "heading",
"f_35mm": "f",
"px_norm": "px",
"py_norm": "py",
}
def main() -> None:
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
src = sys.argv[1]
dst = sys.argv[2] if len(sys.argv) > 2 else src.replace(".csv", "_nerfstudio.csv")
with open(src, encoding="utf-8-sig", newline="") as fin, open(dst, "w", encoding="utf-8", newline="") as fout:
reader = csv.DictReader(fin)
fieldnames = [COLUMN_MAP.get(c, c) for c in reader.fieldnames]
writer = csv.DictWriter(fout, fieldnames=fieldnames)
writer.writeheader()
for row in reader:
writer.writerow({COLUMN_MAP.get(k, k): v for k, v in row.items()})
print(f"written: {dst}")
print("header:", fieldnames)
if __name__ == "__main__":
main() 映射前后表头对比:
- #name,x,y,alt,yaw,pitch,roll,f_35mm,px_norm,py_norm,k1,k2,k3,k4,t1,t2
+ #name,x,y,alt,heading,pitch,roll,f,px,py,k1,k2,k3,k4,t1,t2 4.2 标准操作流程
# 1) 列名映射(生成 3_nerfstudio.csv)
python fix_rc_csv.py 3.csv
# 2) 格式转换(--data 必须指向【原始】图像目录;加 PYTHONIOENCODING 规避 GBK 报错)
PYTHONIOENCODING=utf-8 ns-process-data realitycapture \
--data orin-images \
--csv 3_nerfstudio.csv \
--output-dir output 预期输出:
Started with 16 images
Final dataset is 16 frames. 输出产物:output/transforms.json + output/images/frame_00000.jpg …(含 2x/4x/8x 下采样)。
4.3 常见错误对照表
| 现象 | 原因 | 处理 |
|---|---|---|
UnicodeEncodeError: 'gbk' codec can't encode ... '\U0001f389' | Windows 控制台 GBK 无法显示 rich emoji,掩盖真实错误 | 加 PYTHONIOENCODING=utf-8 重跑 |
Missing image data for N cameras. / Final dataset is 0 frames. | --data 指向了 RC 重命名后的图像目录,CSV 的 #name 匹配不上 | --data 改为指向原始图像目录 |
KeyError: 'f' | CSV 表头是 f_35mm 而非 f | 先运行 fix_rc_csv.py 映射列名 |
KeyError: 'heading' / 'px' / 'py' | 同上,依次触发的同类列名缺失 | 同上 |
5. 验证过程(技术严谨性)
修复后,对生成的 output/transforms.json 做了两层独立验证,确认转换公式本身正确,问题仅在列名。
5.1 数值合理性检查
以第 0 帧为例(原始图像 4524×2034,CSV 第 0 行 f_35mm=27.109、px_norm=-7.9339e-3):
| 项 | 公式(NerfStudio 实现) | 结果 |
|---|---|---|
fl_x = fl_y | f_35mm × max(w,h) / 36 | 27.109 × 4524 / 36 = 3406.71 |
cx | px_norm × max(w,h) + w/2 | -0.007934 × 4524 + 2262 = 2226.11 |
cy | py_norm × max(w,h) + h/2 | 0.002863 × 4524 + 1017 = 1029.95 |
| 平移 | transform[:3,3] = (x, y, alt) | (-33.518, -27.171, 20.402) |
与 CSV 原始数据逐项一致。
5.2 与 COLMAP 稀疏重建的交叉验证(PnP 重投影)
利用 sparse/0 的 3D 点云与 2D 特征点,对每张图用 cv2.solvePnP 求解相机位姿,与 CSV 转换出的位姿对比:
- 重投影误差:mean 0.61 px(max 0.89 px)——稀疏重建与姿态高度吻合;
- 相机中心:PnP 解出的光心与 CSV 位置
(x, y, alt)偏差 ~0.01 m; - 旋转一致性:CSV 姿态旋转与 COLMAP 姿态旋转相差一个固定旋转矩阵(残差 0.000°),即 16 帧之间的相对姿态完全一致。
结论:CSV 的 yaw/pitch/roll、x/y/alt 与 RealityScan 对齐结果几何一致,NerfStudio 的 _get_rotation_matrix(-yaw, pitch, roll) 约定(含 yaw 取负)对该版本导出数据完全适用。
5.3 坐标系说明(旁证)
验证中发现 RC 的 COLMAP 导出与 CSV 导出使用不同坐标系,属正常现象:
| 导出 | 世界坐标系 |
|---|---|
CSV(Internal/External camera parameters) | (E, N, U) 右手系(x 东、y 北、z 上/alt) |
COLMAP(sparse/) | (E, -U, N) 右手系(相机中心 = (x, -alt, y)) |
两者差一个固定旋转,sparse/0/images.txt 中 tvec 满足标准 COLMAP 关系 t = -R·C(数值验证残差 ~1e-15),数据自洽。NerfStudio 训练时会执行 auto_orient_and_center_poses 归一化,绝对坐标系差异不影响训练。
6. 后续操作(训练 3D Gaussian Splatting)
6.1 训练
PYTHONIOENCODING=utf-8 ns-train splatfacto \
--data output \
--output-dir ./runs \
--experiment-name myscan \
--viewer.quit-on-train-completion True \
nerfstudio-data --downscale-factor 2 - 原始图像 4524×2034,对 P104 显卡偏大,
--downscale-factor 2使用 2x 下采样(约 2262×1017)训练,兼顾质量与显存; - 训练数据目录用
ns-process-data的输出output/(内含transforms.json)。
6.2 导出高斯模型 / 点云
# 导出训练完成的模型为点云 / 网格等
ns-export gaussian-splat --load-config runs/myscan/splatfacto/2025-xxxx/xxxx/config.yml --output-dir exports 6.3 查看结果
ns-viewer --load-config runs/myscan/splatfacto/2025-xxxx/xxxx/config.yml 7. 经验总结与注意事项
- 版本差异是此类报错的高发区:RealityScan 2.2 修改了 CSV 表头(旧版为
heading/f/px/py),而 NerfStudio 1.1.5 仍按旧版列名解析。升级任何一端前先核对双方文档。 - 报错要"剥洋葱":Windows 下 rich emoji 会掩盖真实异常,先
PYTHONIOENCODING=utf-8再看;数据目录不匹配时程序不报错而是输出 0 帧,别被"成功"误导。 - 用数据说话:列名映射后,建议用稀疏点云做一次 PnP 重投影验证姿态正确性,避免"能跑但结果是错的"。
- 保持原始导出文件:脚本只生成新 CSV(
3_nerfstudio.csv),不改动3.csv,便于回溯。 - 后续版本:如升级 NerfStudio,可先检查其
realitycapture_utils.py是否已兼容yaw/f_35mm/px_norm/py_norm新列名(部分新版本已适配);若已适配则无需再映射。
附录
附录 A:关键文件与命令速查
工具脚本与文档均位于项目
readme/目录(fix_rc_csv.py、colmap_txt2bin.py、prepare_gs_data.py、build_all.bat等)。
| 文件 | 说明 |
|---|---|
3.csv | RealityScan 2.2 原始导出(16 行,含表头) |
3_nerfstudio.csv | 列名映射后的 CSV(供 ns-process-data 使用) |
fix_rc_csv.py | 列名映射脚本(可复用,参数化输入输出,位于 readme/) |
output/transforms.json | 转换产物,供 ns-train 使用 |
核心命令:
python fix_rc_csv.py 3.csv
PYTHONIOENCODING=utf-8 ns-process-data realitycapture --data orin-images --csv 3_nerfstudio.csv --output-dir output 附录 B:NerfStudio 源码关键位置(1.1.5)
nerfstudio/process_data/realitycapture_utils.pyrealitycapture_to_json():第 30~118 行,CSV 解析与transforms.json生成;- 列名访问点:第 65(
#name)、82~91(f/px/py/k1~k4/t1/t2)、94~98(heading/pitch/roll/x/y/alt)。
nerfstudio/scripts/process_data.pyProcessRealityCapture.main():第 338~415 行,图像复制与调用入口;--data即原始图像目录。
附录 C:训练运行记录(P104 + 原版 3DGS,2026-08-13)
本章为训练简录。完整的《3DGS 训练全流程记录》(含逐步骤教程、原理说明、踩坑清单)见同系列文章。
C.1 硬件限制:nerfstudio splatfacto 无法在 P104 上运行
- P104-100 是 Pascal 架构(sm_61),而 nerfstudio 1.1.5 的 splatfacto 依赖的
gsplat要求 GPU 架构 ≥ sm_70(Volta 及以上)。gsplat 源码构建配置明确注释:build against architectures >= 7.0 (required by cooperative_groups::labeled_partition in gsplat)。 - 官方预编译 wheel 仅含
sm_70/75/80/86/90,无sm_61;该特性是代码级依赖,自行编译也无法绕过。 - 结论:splatfacto 在 P104 上不可运行,需换用支持 Pascal 的方案。
C.2 替代方案:原版 3DGS(INRIA gaussian-splatting)
用户选择用原版 3DGS(其 CUDA 光栅化代码支持 Pascal)。完整搭建过程见 《3DGS 训练全流程记录》。核心要点:
1) Python/CUDA 环境对齐(torch 与 nvcc 版本必须一致)
| 组件 | 版本 | 说明 |
|---|---|---|
| CUDA Toolkit | 11.8(系统预装) | nvcc 11.8.89 |
| torch / torchvision | 2.4.1+cu118 / 0.19.1+cu118 | 与 nvcc 11.8 匹配(此前为跑 nerfstudio 装的 cu124 版本不匹配,需重装) |
| numpy | 1.26.4 | 必须 <2(open-cv 4.8.1 等二进制依赖 numpy 1.x) |
注意:
pip install --force-reinstall会把依赖(如 numpy、torch)从默认源升级,曾两次导致 numpy 变回 2.x、torch 变回 CPU 版,务必用--no-deps或装完后复核。
2) 源码与子模块(GitHub 直连不稳定,使用 ghfast.top 镜像)
# gaussian-splatting(main)+ 三个 submodule + glm
git clone 失败时改用:curl -L -o gs.zip "https://ghfast.top/https://github.com/graphdeco-inria/gaussian-splatting/archive/refs/heads/main.zip"
# 注意 diff-gaussian-rasterization 必须用 dr_aa 分支(主仓库代码调用 antialiasing 参数):
curl -L -o dgr.zip "https://ghfast.top/https://github.com/graphdeco-inria/diff-gaussian-rasterization/archive/refs/heads/dr_aa.zip"
# simple-knn 在 INRIA GitLab:https://gitlab.inria.fr/bkerbl/simple-knn
# fused-ssim(metrics 用):https://github.com/rahul-goel/fused-ssim
# glm(光栅化依赖):https://github.com/g-truc/glm → submodules/diff-gaussian-rasterization/third_party/glm 3) 编译三个 CUDA 扩展(vcvars64 + nvcc 11.8)
@echo off
call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
set DISTUTILS_USE_SDK=1
cd /d D:\temp\gaussian-splatting\submodules\diff-gaussian-rasterization
python setup.py install
cd /d D:\temp\gaussian-splatting\submodules\simple-knn
python setup.py install
cd /d D:\temp\gaussian-splatting\submodules\fused-ssim
python setup.py install 三个 setup.py 的 extra_compile_args["nvcc"] 需追加(MSVC 14.44 过新、CUDA 11.8 过旧的两个版本检查):
"-allow-unsupported-compiler", "-D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH" 4) 数据准备(RealityScan 的 colmap 导出是文本格式,3DGS 只读二进制)
# txt → bin(colmap 官方二进制格式;qvec/tvec 是 float64,容易写错)
python tmp/colmap_txt2bin.py
# 图像+内参同步缩放到 0.5x(P104 显存/速度考虑;3DGS 无内置缩放)
python tmp/prepare_gs_data.py 0.5
# 产物:D:\temp\ReasonixChat\3\gs_data(images/ + sparse/0/{cameras,images,points3D}.bin) 5) 训练与渲染验证
cd D:\temp\gaussian-splatting
python train.py -s D:/temp/ReasonixChat/3/gs_data -m D:/temp/ReasonixChat/3/gs_output --iterations 7000
python render.py -m D:/temp/ReasonixChat/3/gs_output -s D:/temp/ReasonixChat/3/gs_data --iteration 7000 6) 实测结果(P104-100,8GB,0.5x 分辨率 ≈2250×1000)
| 指标 | 数值 |
|---|---|
| 训练速度 | ~9.5 it/s,7000 步耗时 11 分 21 秒 |
| Loss 收敛 | 0.16 → 0.015 |
| 训练集评估 | PSNR 36.6 dB,L1 0.0097 |
| 16 帧渲染平均 PSNR | 37.24 dB(min 30.17 / max 39.92) |
| 输出 | gs_output/point_cloud/iteration_7000/point_cloud.ply |
7) 完整训练(可选)
python train.py -s D:/temp/ReasonixChat/3/gs_data -m D:/temp/ReasonixChat/3/gs_output_full --iterations 30000 预计耗时 ~50 分钟,产出 point_cloud/iteration_30000/point_cloud.ply;查看器可用官方 SIBR viewer 或直接渲染/导出。
C.3 踩坑清单(训练环节)
| 现象 | 原因 | 处理 |
|---|---|---|
torch.cuda.is_available()=False | 装的是 CPU 版 torch | 从 PyTorch 官方源装 cu118/cu124 wheel |
gsplat no kernel image is available | gsplat 不支持 sm_61 | 放弃 splatfacto,换原版 3DGS |
The detected CUDA version (11.8) mismatches ... PyTorch (12.4) | torch CUDA 与 nvcc 版本不一致 | torch 换成 cu118,与 nvcc 11.8 对齐 |
STL1002: expected CUDA 12.4 or newer | MSVC 14.44 新 STL 拒绝旧 CUDA | nvcc 加 -D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH |
unsupported Microsoft Visual Studio version | CUDA 11.8 不认 MSVC 14.44 | nvcc 加 -allow-unsupported-compiler |
_ARRAY_API not found(cv2 导入失败) | numpy 被升到 2.x | pip install "numpy<2" |
UnicodeDecodeError ... image_name | colmap images.bin 的 qvec/tvec 写成 float32 | 修正为 float64(<4d/<3d) |
GaussianRasterizationSettings ... unexpected keyword 'antialiasing' | rasterizer 用了 main 分支,主仓库代码需要 dr_aa | 换 dr_aa 分支重新编译 |
| 路径反斜杠丢失 | bash 转义 | 传参用正斜杠 D:/temp/... |
Happy Reconstructing! 🎉