NW' Blog
2026年8月13日 · 内容页面   |   📝编辑于2026年8月13日

RealityScan 2.2 + NerfStudio
数据转换排错记录

使用 RealityScan 2.2 对齐照片并导出 CSV 与 COLMAP 格式,再用 NerfStudio 的 ns-process-data realitycapture 转换为 transforms.json 训练 3DGS。记录"CSV 列名不一致"类报错的完整排查与修复过程。

RealityScan + NerfStudio 预览

结论速览

适用场景:使用 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)。

CSV 表头映射示意
CSV 表头映射:yaw→heading、f_35mm→f、px_norm→px、py_norm→py

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/ 中的原始文件名不对应

原始手机照片
原始输入:手机拍摄照片(IMG_20260702_170007.jpg)

2. 问题现象

执行标准转换命令后报错:

bash
ns-process-data realitycapture --data {data directory} --csv {csv file} --output-dir {output directory}

2.1 第一层现象(Windows 特有)

直接运行时报出 UnicodeEncodeError,掩盖了真实错误:

log
UnicodeEncodeError: 'gbk' codec can't encode character '\U0001f389' in position 0: illegal multibyte sequence

原因:Windows 控制台默认 GBK 编码,无法输出 rich 打印的 🎉 emoji。NerfStudio 捕获异常后再打印错误信息时再次触发该编码异常,导致看不到真正的错误内容。

处置:加环境变量后重跑,暴露真实错误:

bash
PYTHONIOENCODING=utf-8 ns-process-data realitycapture --data orin-images --csv 3.csv --output-dir out_test

2.2 第二层现象(数据目录选错时)

--data images(RC 重命名后的图像目录)时,不报错但结果为空:

log
Missing image data for 16 cameras.
Missing camera data for 16 frames.
Final dataset is 0 frames.

原因见 3.2

2.3 第三层现象(核心报错)

--data orin-images(原始图像目录)时,真正的错误暴露:

log
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
位置 Xxx
位置 Yyy
高度altalt
偏航角headingyaw
俯仰角pitchpitch
翻滚角rollroll
35mm 等效焦距ff_35mm
归一化主点 xpxpx_norm
归一化主点 ypypy_norm
径向畸变k1~k4k1~k4
切向畸变t1, t2t1, t2

源码中的关键取值代码(realitycapture_utils.py 第 65~98 行):

python
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'

为什么可以断定只是改名、语义未变?

  1. RealityCapture 校准模板(calibration.xml)中,旧版表头写的就是 heading,而其取值变量一直是 $(yaw);RealityScan 2.2 只是把表头改成了 yaw
  2. 通过稀疏点云 PnP 重投影独立验证(见第 5 章),映射列名后生成的姿态与 RealityScan 的 COLMAP 稀疏重建几何一致,证明 yaw/pitch/rollx/y/alt 的数值语义与旧版完全一致。

3.2 --data 目录必须指向原始图像

NerfStudio 处理流程(process_data.py::ProcessRealityCapture.main):

  1. --data 目录收集图像,按文件名 stem(去掉扩展名)建立 image_filename_map
  2. 把图像复制到输出目录并重命名为 frame_00000.jpg 等(不影响匹配,因为匹配发生在复制之前的原始名称)。
  3. 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(不修改原始文件)。

python
"""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()

映射前后表头对比:

diff
- #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 标准操作流程

bash
# 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

预期输出:

log
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.109px_norm=-7.9339e-3):

公式(NerfStudio 实现)结果
fl_x = fl_yf_35mm × max(w,h) / 3627.109 × 4524 / 36 = 3406.71
cxpx_norm × max(w,h) + w/2-0.007934 × 4524 + 2262 = 2226.11
cypy_norm × max(w,h) + h/20.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/rollx/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.txttvec 满足标准 COLMAP 关系 t = -R·C(数值验证残差 ~1e-15),数据自洽。NerfStudio 训练时会执行 auto_orient_and_center_poses 归一化,绝对坐标系差异不影响训练。


6. 后续操作(训练 3D Gaussian Splatting)

6.1 训练

bash
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 导出高斯模型 / 点云

bash
# 导出训练完成的模型为点云 / 网格等
ns-export gaussian-splat --load-config runs/myscan/splatfacto/2025-xxxx/xxxx/config.yml --output-dir exports

6.3 查看结果

bash
ns-viewer --load-config runs/myscan/splatfacto/2025-xxxx/xxxx/config.yml

7. 经验总结与注意事项

  1. 版本差异是此类报错的高发区:RealityScan 2.2 修改了 CSV 表头(旧版为 heading/f/px/py),而 NerfStudio 1.1.5 仍按旧版列名解析。升级任何一端前先核对双方文档。
  2. 报错要"剥洋葱":Windows 下 rich emoji 会掩盖真实异常,先 PYTHONIOENCODING=utf-8 再看;数据目录不匹配时程序不报错而是输出 0 帧,别被"成功"误导。
  3. 用数据说话:列名映射后,建议用稀疏点云做一次 PnP 重投影验证姿态正确性,避免"能跑但结果是错的"。
  4. 保持原始导出文件:脚本只生成新 CSV(3_nerfstudio.csv),不改动 3.csv,便于回溯。
  5. 后续版本:如升级 NerfStudio,可先检查其 realitycapture_utils.py 是否已兼容 yaw/f_35mm/px_norm/py_norm 新列名(部分新版本已适配);若已适配则无需再映射。

附录

附录 A:关键文件与命令速查

工具脚本与文档均位于项目 readme/ 目录(fix_rc_csv.pycolmap_txt2bin.pyprepare_gs_data.pybuild_all.bat 等)。

文件说明
3.csvRealityScan 2.2 原始导出(16 行,含表头)
3_nerfstudio.csv列名映射后的 CSV(供 ns-process-data 使用)
fix_rc_csv.py列名映射脚本(可复用,参数化输入输出,位于 readme/)
output/transforms.json转换产物,供 ns-train 使用

核心命令:

bash
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.py
    • realitycapture_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.py
    • ProcessRealityCapture.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 Toolkit11.8(系统预装)nvcc 11.8.89
torch / torchvision2.4.1+cu118 / 0.19.1+cu118与 nvcc 11.8 匹配(此前为跑 nerfstudio 装的 cu124 版本不匹配,需重装)
numpy1.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 镜像)

bash
# 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)

build_all.bat
@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.pyextra_compile_args["nvcc"] 需追加(MSVC 14.44 过新、CUDA 11.8 过旧的两个版本检查):

python
"-allow-unsupported-compiler", "-D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH"

4) 数据准备(RealityScan 的 colmap 导出是文本格式,3DGS 只读二进制)

bash
# 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) 训练与渲染验证

bash
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 帧渲染平均 PSNR37.24 dB(min 30.17 / max 39.92)
输出gs_output/point_cloud/iteration_7000/point_cloud.ply

7) 完整训练(可选)

bash
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 availablegsplat 不支持 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 newerMSVC 14.44 新 STL 拒绝旧 CUDAnvcc 加 -D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH
unsupported Microsoft Visual Studio versionCUDA 11.8 不认 MSVC 14.44nvcc 加 -allow-unsupported-compiler
_ARRAY_API not found(cv2 导入失败)numpy 被升到 2.xpip install "numpy<2"
UnicodeDecodeError ... image_namecolmap images.bin 的 qvec/tvec 写成 float32修正为 float64(<4d/<3d)
GaussianRasterizationSettings ... unexpected keyword 'antialiasing'rasterizer 用了 main 分支,主仓库代码需要 dr_aa换 dr_aa 分支重新编译
路径反斜杠丢失bash 转义传参用正斜杠 D:/temp/...

Happy Reconstructing! 🎉

评论区 留下您的想法
教程