ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Colmap自制3DGS数据集:从图像到相机位姿的完整流程

Colmap自制3DGS数据集:从图像到相机位姿的完整流程

简介:这份源码包面向希望低成本自制3D Gaussian Splatting数据集的开发者与三维重建学习者,围绕Colmap预处理流程提供可运行的项目代码,帮助解决稀疏重建无相机轨迹、稠密点云孔洞多等常见问题。包内共12个文件,以5张jpg示例图片、3个py脚本、1个md说明文档为主,另含html页面、gitignore与inscode配置,压缩包约379KB,结构轻量便于快速上手。其中Python脚本分别对应Colmap配置、稠密重建与格式转换等关键环节,md文档梳理了整体流程与注意事项,图片可用于验证输入数据规范。已有314人学习,适合具备一定三维视觉基础、需要从图像采集到格式转换完整跑通3DGS数据集构建流程的读者参考,也可作为后续训练与应用的数据准备起点。

1. 从一堆手机照片到能训 3DGS 的数据集,中间到底缺了什么

你拍了段环绕视频,抽帧得到几百张图,兴冲冲丢进 3DGS 训练脚本,结果要么直接报错,要么训出来一团糊。问题八成不在 3DGS 本身,而在数据没有经过 SfM 处理——相机位姿是空的,稀疏点云是空的,3DGS 根本不知道每张图是从哪个角度拍的。Colmap 自制 3DGS 数据集这件事,核心就是用 Colmap 把无序图像变成带相机内外参和稀疏点云的规范数据集,再喂给 3DGS 训练。它解决的是「有图但没法训」的断层问题,适合手里有拍摄素材、想跑通 3DGS 全流程的工程师和研究者。整套流程不复杂,但参数和顺序错一步就翻车,下面按我实际跑通的路径拆开讲。

2. Colmap 跑通 SfM:从图像到相机位姿的最小闭环

2.1 为什么 3DGS 必须先过 Colmap 这一关

3DGS 的训练本质是优化一堆三维高斯椭球的位置、协方差、颜色和透明度,而监督信号来自「把高斯投影到每个相机视角后渲染出的图像」与真实图像的差异。这意味着每个训练视角必须知道相机在哪、朝哪看、焦距多少。Colmap 做的就是从多视角图像中同时估计相机位姿和场景稀疏结构,输出 cameras.bin、images.bin、points3D.bin 三件套。没有这三件套,3DGS 的投影环节就是黑匣子,梯度传不回去。

常见做法是先用 Colmap 做一遍完整的 SfM,拿到稀疏重建结果,再转成 3DGS 需要的格式。有人会问能不能跳过 Colmap 直接用 COLMAP-free 的方案,可以,但那是另一条技术路线,稳定性和可控性目前不如 Colmap 成熟。对于自制数据集,Colmap 仍然是首选。

2.2 图像采集与预处理的三个硬指标

在打开 Colmap 之前,素材本身要过关。我一般按三个指标检查:

  • 重叠度:相邻帧之间至少 60% 画面重叠,环绕拍摄时每转 10 到 15 度一张,一圈 24 到 36 张起步。重叠不够,特征匹配直接断链。
  • 清晰度:运动模糊是 SfM 的头号杀手。快门速度不够就补光或降速,糊掉的帧宁可删掉也不要留着凑数。
  • 纹理:纯白墙面、镜面、大面积天空这些弱纹理区域,Colmap 提不出特征点。拍摄时尽量让画面里有报纸、木纹、绿植这类有纹理的参照物。

预处理阶段用 ffmpeg 抽帧,命令如下:

# 从视频按每秒 2 帧抽图,输出到 images 目录 ffmpeg -i input.mp4 -vf "fps=2" -q:v 2 images/frame_%04d.jpg

fps=2表示每秒抽 2 帧,环绕 30 秒的视频大约得到 60 张图,够用。-q:v 2控制 JPEG 质量,数值越小质量越高,2 到 5 之间比较合适。抽完帧后建议人工过一遍,把模糊和重复的删掉,这一步花五分钟,后面省半小时排错。

2.3 Colmap 命令行完整流程与参数逐项说明

Colmap 有 GUI 和命令行两套入口。GUI 适合调试,命令行适合批量复现。我一般先用 GUI 跑通一遍确认参数,再固化成脚本。下面是命令行的完整流程:

# 1. 特征提取 colmap feature_extractor \ --database_path ./database.db \ --image_path ./images \ --ImageReader.camera_model OPENCV \ --ImageReader.single_camera 1 \ --SiftExtraction.max_image_size 2000 # 2. 特征匹配 colmap exhaustive_matcher \ --database_path ./database.db \ --SiftMatching.guided_matching 1 # 3. 稀疏重建 mkdir sparse colmap mapper \ --database_path ./database.db \ --image_path ./images \ --output_path ./sparse # 4. 去畸变导出 colmap image_undistorter \ --image_path ./images \ --input_path ./sparse/0 \ --output_path ./dense \ --output_type COLMAP

逐项说明关键参数。--ImageReader.camera_model OPENCV适用于大多数手机和单反,如果用的是鱼眼镜头就换成 OPENCV_FISHEYE。--ImageReader.single_camera 1表示所有图来自同一台相机、内参一致,手机拍摄基本都设 1。--SiftExtraction.max_image_size 2000把长边压到 2000 像素,太大显存吃不住,太小特征点不够,2000 是经验值。--SiftMatching.guided_matching 1开启引导匹配,能提升弱纹理区域的匹配数,代价是慢一些。

稀疏重建完成后,sparse/0目录下会出现 cameras.bin、images.bin、points3D.bin。用colmap model_analyzer可以查看重建统计:

colmap model_analyzer --path ./sparse/0

重点看两个数:注册图像数和平均重投影误差。注册图像数应该接近输入总数,如果只有一半,说明匹配或重建出了问题。平均重投影误差在 0.5 到 1.0 像素之间算正常,超过 2 像素说明位姿不准,后面 3DGS 训出来会糊。

2.4 判断重建是否合格的四个检查点

跑完不要急着转格式,先做四个检查:

检查项合格标准不合格时的处理
注册图像比例大于 90%降低匹配阈值或补拍弱纹理区域
平均重投影误差小于 1.0 像素剔除误差大的图像后重跑
稀疏点数量与场景复杂度匹配增加图像或提高特征提取数量
轨迹连续性相机位置平滑无跳变检查是否有重复纹理导致误匹配

在 GUI 里打开稀疏重建结果,按Render > Render camera可以看到相机轨迹。轨迹应该是平滑的环绕或推进,如果出现突然跳变,说明某些帧的位姿估计错了,需要把这些帧找出来删掉重跑。

3. 把 Colmap 输出转成 3DGS 能吃的格式

3.1 3DGS 到底需要哪些文件

3DGS 官方实现读取的数据集目录结构是这样的:

dataset/ images/ frame_0001.jpg frame_0002.jpg ... sparse/ 0/ cameras.bin images.bin points3D.bin

注意这里有个容易搞混的点:3DGS 读的是sparse/0下的二进制文件,不是去畸变后的dense目录。很多人跑完image_undistorter后把 dense 目录指给 3DGS,结果报错找不到相机参数。正确做法是把sparse/0整个目录拷到数据集根目录下,图像用原始未去畸变的版本。

3.2 转换脚本与目录组织

如果 Colmap 输出已经是标准格式,其实不需要额外转换脚本,直接组织目录即可。但实际项目中经常需要把图像重命名、把多个子场景合并、或者把 Colmap 的文本格式转成二进制格式。下面这个脚本做三件事:把图像按顺序重命名、拷贝 sparse 目录、生成一个简单的校验文件。

import os import shutil import struct def organize_dataset(src_images, src_sparse, dst_root): """ 将 Colmap 输出组织成 3DGS 可读的数据集结构 src_images: 原始图像目录 src_sparse: Colmap 稀疏重建目录(包含 cameras.bin 等) dst_root: 目标数据集根目录 """ img_dst = os.path.join(dst_root, "images") sparse_dst = os.path.join(dst_root, "sparse", "0") os.makedirs(img_dst, exist_ok=True) os.makedirs(sparse_dst, exist_ok=True) # 拷贝并重命名图像 exts = (".jpg", ".jpeg", ".png") files = sorted([f for f in os.listdir(src_images) if f.lower().endswith(exts)]) for idx, fname in enumerate(files): new_name = f"frame_{idx:04d}{os.path.splitext(fname)[1]}" shutil.copy2(os.path.join(src_images, fname), os.path.join(img_dst, new_name)) # 拷贝稀疏重建文件 for f in ["cameras.bin", "images.bin", "points3D.bin"]: src = os.path.join(src_sparse, f) if os.path.exists(src): shutil.copy2(src, os.path.join(sparse_dst, f)) else: print(f"警告:缺少 {f}") # 校验 images.bin 中的图像名是否与重命名后一致 # 注意:如果重命名了图像,images.bin 里的名字会对不上 # 所以更稳妥的做法是不重命名,或者用 colmap 的 model_converter 同步 print(f"完成:{len(files)} 张图像已组织到 {dst_root}") if __name__ == "__main__": organize_dataset("./images", "./sparse/0", "./dataset_3dgs")

这里有个血泪教训:不要随意重命名图像。Colmap 的images.bin里存的是原始文件名,重命名后 3DGS 按名字去索引图像会找不到。如果确实需要重命名,要么在 Colmap 之前就统一命名,要么用colmap model_converter把二进制转成文本,手动改完再转回去。我一般选择在抽帧阶段就用frame_%04d.jpg的格式,后面全程不动文件名。

3.3 用 model_converter 做格式互转与校验

Colmap 的二进制格式读写快但不可读,文本格式方便检查但体积大。model_converter可以在两者之间转换:

# 二进制转文本,方便查看相机参数 colmap model_converter \ --input_path ./sparse/0 \ --output_path ./sparse_txt \ --output_type TXT # 文本转二进制,用于最终训练 colmap model_converter \ --input_path ./sparse_txt \ --output_path ./sparse/0 \ --output_type BIN

转成文本后,cameras.txt里能看到每台相机的内参,images.txt里能看到每张图的位姿四元数和平移向量。检查images.txt时注意:如果某张图的四元数全是 0 或者平移向量异常大,说明这张图的位姿估计失败了,应该从数据集中剔除。

3.4 数据集划分与分辨率对齐

3DGS 训练通常需要留出测试视角来评估渲染质量。常见做法是按 8:1:1 划分训练、测试、验证集,但 3DGS 官方脚本默认用llffhold参数每隔 8 张取一张做测试。如果自己划分,需要在数据集目录下放一个split.json或者按 3DGS 的约定命名。

分辨率方面,3DGS 训练时会自动按图像长边缩放,但 Colmap 的特征提取分辨率要和训练分辨率匹配。如果 Colmap 用 2000 像素提取特征,而 3DGS 训练时缩到 800,位姿精度是够的,但稀疏点的尺度会偏。我一般让 Colmap 和 3DGS 用同一套图像,不做额外缩放,显存不够就减图像数量而不是降分辨率。

4. 避坑与排查:Colmap 自制 3DGS 数据集最常见的五个翻车点

4.1 重建只注册了一半图像

现象:model_analyzer显示注册图像数只有输入的一半甚至更少,稀疏点云稀稀拉拉。

原因:最常见的是图像重叠度不够,或者场景中有大面积弱纹理区域导致特征匹配断裂。另一个容易被忽略的原因是single_camera设置错误——如果图像来自不同相机或不同焦距,设成 1 会导致内参估计混乱。

解决:先检查图像序列,把模糊和重复的删掉。然后在feature_extractor阶段把--SiftExtraction.max_image_size提到 2400 或 3200,增加特征点数量。如果还是不行,改用sequential_matcher代替exhaustive_matcher,因为环绕拍摄的图像顺序本身就是连续的,顺序匹配更稳。

4.2 重投影误差过大导致 3DGS 训出来糊

现象:Colmap 重建看起来正常,但 3DGS 训练几百轮后渲染结果仍然模糊,PSNR 上不去。

原因:平均重投影误差超过 2 像素,说明相机位姿不够准。3DGS 对位姿误差很敏感,位姿偏一点,高斯投影就偏一片。

解决:在 Colmap 里用--Mapper.ba_global_function_tolerance收紧 BA 的收敛容差,默认是 1e-6,可以试 1e-7。另外在image_undistorter之后,用colmap bundle_adjuster做一次全局 BA 精化:

colmap bundle_adjuster \ --input_path ./sparse/0 \ --output_path ./sparse/0_refined \ --BundleAdjustment.max_num_iterations 100

跑完后再看重投影误差,通常能降到 0.5 像素以下。

4.3 图像文件名不匹配导致 3DGS 报 FileNotFoundError

现象:3DGS 训练脚本启动后报错,说找不到某张图像。

原因:Colmap 的images.bin里存的是原始文件名,如果中间用脚本重命名过图像,或者抽帧时的命名和 Colmap 读取时的命名不一致,就会对不上。

解决:统一命名规范。我习惯在抽帧阶段就用frame_%04d.jpg,Colmap 的image_path指向这个目录,后面所有步骤都不再改名。如果已经改了名,用model_converter转成文本,手动同步images.txt里的文件名,再转回二进制。

4.4 显存不足导致训练中断

现象:3DGS 训练到一半报 CUDA out of memory。

原因:图像分辨率太高或者稀疏点太多,导致高斯数量爆炸。

解决:在 3DGS 的训练配置里降低resolution参数,或者用--densify_grad_threshold提高致密化门槛,减少高斯数量。另一个办法是在 Colmap 阶段就用--SiftExtraction.max_image_size限制图像尺寸,从源头控制数据量。

4.5 稀疏点云尺度异常导致相机轨迹错乱

现象:在 3DGS 的 viewer 里看相机轨迹,发现相机位置分布在一个异常大或异常小的范围内。

原因:Colmap 重建的尺度是不确定的,如果没有做尺度对齐,不同次重建的尺度可能差几个数量级。

解决:3DGS 内部会自动做归一化,一般不需要手动处理。但如果发现轨迹明显错乱,检查images.bin里的平移向量是否在合理范围。如果某张图的平移向量是其他图的几百倍,说明这张图的位姿估计错了,删掉重跑。

5. 进阶技巧:用 Colmap 的文本模型做精细控制和批量验证

跑通基本流程后,真正拉开差距的是对 Colmap 中间结果的控制能力。我一般会在稀疏重建完成后,把模型转成文本格式,用脚本做一轮自动校验,把异常图像筛掉再重跑。这个习惯帮我省了很多次「训到一半发现数据有问题」的后悔药。

具体做法是:先用model_converter把sparse/0转成 TXT,然后读images.txt,计算每张图的重投影误差和轨迹平滑度。下面这段脚本做的是轨迹平滑度检查——相邻帧的相机中心距离如果突变超过阈值,就标记为可疑帧。

import numpy as np def quat_to_rot(q): """四元数转旋转矩阵,q 格式为 (w, x, y, z)""" w, x, y, z = q return np.array([ [1-2*(y*y+z*z), 2*(x*y-w*z), 2*(x*z+w*y)], [2*(x*y+w*z), 1-2*(x*x+z*z), 2*(y*z-w*x)], [2*(x*z-w*y), 2*(y*z+w*x), 1-2*(x*x+y*y)] ]) def check_trajectory(images_txt_path, jump_threshold=0.5): """ 检查相机轨迹平滑度 images_txt_path: Colmap 文本格式的 images.txt jump_threshold: 相邻帧距离突变的阈值(相对于平均距离的倍数) """ centers = [] names = [] with open(images_txt_path, "r") as f: for line in f: if line.startswith("#") or len(line.strip()) == 0: continue parts = line.strip().split() if len(parts) < 10: continue # images.txt 格式:IMAGE_ID QW QX QY QZ TX TY TZ CAMERA_ID NAME q = list(map(float, parts[1:5])) t = np.array(list(map(float, parts[5:8]))) rot = quat_to_rot(q) center = -rot.T @ t # 相机中心 = -R^T * t centers.append(center) names.append(parts[9]) centers = np.array(centers) dists = np.linalg.norm(np.diff(centers, axis=0), axis=1) mean_dist = np.mean(dists) suspicious = [] for i, d in enumerate(dists): if d > jump_threshold * mean_dist * 3: suspicious.append((names[i], names[i+1], d)) print(f"平均帧间距:{mean_dist:.4f}") print(f"可疑跳变:{len(suspicious)} 处") for a, b, d in suspicious: print(f" {a} -> {b}: {d:.4f}") return suspicious if __name__ == "__main__": check_trajectory("./sparse_txt/images.txt")

这段脚本的核心逻辑是:从images.txt里读出每张图的四元数和平移向量,算出相机中心坐标,然后看相邻帧之间的距离是否突变。jump_threshold设 0.5 表示超过平均间距 1.5 倍就标记。实际用的时候可以先把阈值调大跑一遍看分布,再收紧。

另一个进阶技巧是分块重建再合并。如果场景很大,一次性跑 Colmap 容易内存不够,可以按拍摄顺序切成几段分别重建,再用colmap model_merger合并。合并的前提是相邻段之间有足够的重叠图像,否则合并会失败。我一般会在切分处多留 5 到 10 张重叠帧。

最后说一个验证数据集质量的小技巧:在 3DGS 训练启动后,先看前 100 轮的 loss 曲线。如果 loss 在前 100 轮内降到 0.05 以下,说明位姿和稀疏点质量不错;如果 loss 震荡或者下降很慢,大概率是 Colmap 阶段出了问题,别急着调 3DGS 的超参,回去检查重投影误差和注册图像比例。这个习惯让我少走了很多弯路,希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进