摘要:本文分析了 reduced-3DGS(基于向量量化的高斯溅射改进版)输出无法被标准 3DGS 可视化工具渲染的问题。通过对比目录结构、PLY 文件头和 codebook.pt 结构,发现主要原因是多元组顶点元素和量化索引格式不兼容。为解决此问题,提供了完整的 Python 转换脚本 convert_to_standard_ply.py,可将 reduced-3DGS 的多元组 PLY 格式(支持量化/未量化、float32/half-float)转换为标准单元素 PLY 格式,并给出了具体的使用方法和验证步骤。
目录
1. 问题描述
本工程(reduced-3dgs)是原版 gaussian-splatting 的改进版本,引入了向量量化(Vector Quantization, VQ)来压缩 Gaussian 参数。通过 conda 环境 gaussian_splatting_reduced 运行 train.py 得到的结果无法被标准 3DGS 在线可视化工具正常渲染,而原版工程的输出可以。
| 原版 gaussian-splatting | output/b52cc067-d/ | ✅ 正常 |
| reduced-3dgs | output/6ddc4bf3-6/ | ❌ 失败 |
2. 排查过程
2.1 目录结构对比
# 原版 3DGS 输出
output/b52cc067-d/
├── cameras.json
├── cfg_args
├── exposure.json
├── input.ply
├── events.out.tfevents.*
└── point_cloud/
└── iteration_7000/
└── point_cloud.ply ← 803 MB,标准格式
Reduced-3DGS 输出
output/6ddc4bf3-6/
├── cameras.json
├── cfg_args
├── input.ply
├── codebook.pt ← 318 MB,码本(额外文件)
└── point_cloud/
├── iteration_7000/
│ └── point_cloud.ply ← 736 MB
└── iteration_30000/
├── point_cloud.ply ← 1.34 GB(未量化,多元组)
├── point_cloud_quantised.ply ← 386 MB(量化版)
└── point_cloud_quantised_half.ply ← 352 MB(量化半精度版)
关键差异:
- reduced-3dgs 多了 codebook.pt(码本文件)
- reduced-3dgs 的 PLY 文件多了 _quantised、_quantised_half 后缀
- reduced-3dgs 没有 exposure.json
2.2 PLY 文件头对比
原版 3DGS — 标准格式
ply
format binary_little_endian 1.0
element vertex 3241453 ← 单一顶点元素
property float x
property float y
property float z
property float nx ← 有法线
property float ny
property float nz
property float f_dc_0
property float f_dc_1
property float f_dc_2
property float f_rest_0 ← 45 个 f_rest (sh_degree=3)
…
property float f_rest_44
property float opacity
property float scale_0
property float scale_1
property float scale_2
property float rot_0
property float rot_1
property float rot_2
property float rot_3
end_header
Reduced-3DGS — 多元组 + 量化格式
ply
format binary_little_endian 1.0
element vertex_0 0 ← 分组 0: 0 个高斯 (sh_degree=0)
property float x
…
property float rot_3
element vertex_1 0 ← 分组 1: 0 个高斯 (sh_degree=1)
property float x
…
property float f_rest_0 ← 9 个 f_rest
…
property float f_rest_8
…
property float rot_3
element vertex_2 0 ← 分组 2: 0 个高斯 (sh_degree=2)
… ← 24 个 f_rest
element vertex_3 5677385 ← 分组 3: 5,677,385 个高斯 (sh_degree=3)
… ← 45 个 f_rest
element codebook_centers 256 ← 码本: 256 个聚类中心
property float features_dc ← DC 颜色码本
property float features_rest_0 ← 15 个 SH 系数码本
…
property float features_rest_14
property float opacity ← 不透明度码本
property float scaling ← 缩放码本
property float rotation_re ← 旋转实部码本
property float rotation_im ← 旋转虚部码本
end_header
2.3 codebook.pt 结构
{
'features_dc': Codebook(ids, centers), # ids: (P, 1, 3), centers: (256,)
'features_rest_0': Codebook(ids, centers), # ids: (P_1+, 1, 1), centers: (256,)
…
'features_rest_14': Codebook(ids, centers),
'opacity': Codebook(ids, centers), # ids: (P, 1), centers: (256,)
'scaling': Codebook(ids, centers), # ids: (P, 3), centers: (256,)
'rotation_re': Codebook(ids, centers), # ids: (P, 1), centers: (256,)
'rotation_im': Codebook(ids, centers), # ids: (P, 3), centers: (256,)
}
每个 Codebook 包含:
- ids:每个高斯参数的码本索引(0-255),uint8
- centers:256 个聚类中心值,float32
3. 原因分析
3.1 格式差异总览
| 顶点元素 | 单个 element vertex | 多个 element vertex_N(按 SH degree 分组) |
| 法线 | ✅ nx, ny, nz | ❌ 不存在 |
| SH 系数 | 统一 45 个 f_rest | 每组不同数量(0/9/24/45),按 SH 带宽分配 |
| 参数编码 | 直接存储 float32 值 | 量化版存储 uint8 码本索引 |
| 附加数据 | 无 | codebook_centers 元素 + 外部 codebook.pt |
3.2 为什么标准工具无法渲染
3.3 编解码流程
保存流程(save_ply, quantised=True):
原始参数 (float32)
↓ k-means 聚类 (generate_codebook)
码本 centers (256,) + ids (uint8 indices)
↓ save_ply
PLY: vertex_N 存 ids, codebook_centers 存 centers
加载流程(load_ply, quantised=True):
PLY: vertex_N (ids) + codebook_centers
↓ _parse_vertex_group: 查表解码
原始参数 (float32)
↓
GaussianModel 渲染
4. 解决方案:转换脚本
4.1 脚本概述
脚本 convert_to_standard_ply.py 位于项目根目录,用于将 reduced-3DGS 的多元组 PLY 格式转换为标准 3DGS 单元素格式。脚本自动处理量化/未量化、float32/half-float 格式,解析多元组结构并执行码本查表解码(如需要),最终输出标准 62 属性 PLY 文件。
4.2 完整代码
#!/usr/bin/env python3
"""
convert_to_standard_ply.py
将 reduced-3DGS 的多元组 PLY 转换为标准 3DGS 格式。
支持量化/未量化、float32/half-float 格式。
"""
import argparse
import numpy as np
from plyfile import PlyData, PlyElement
def parse_args():
parser = argparse.ArgumentParser(
description='Convert reduced-3DGS PLY to standard 3DGS format'
)
parser.add_argument(
'-i', '–input',
required=True,
help='输入 PLY 文件路径(reduced-3DGS 格式)'
)
parser.add_argument(
'-o', '–output',
required=True,
help='输出 PLY 文件路径(标准 3DGS 格式)'
)
return parser.parse_args()
def load_reduced_ply(ply_path):
"""加载 reduced-3DGS PLY 文件,返回顶点组和码本"""
ply = PlyData.read(ply_path)
# 提取顶点组
vertex_groups = []
for element in ply.elements:
if element.name.startswith('vertex_'):
sh_degree = int(element.name.split('_')[1])
vertex_groups.append((sh_degree, element))
# 提取码本
codebook = None
for element in ply.elements:
if element.name == 'codebook_centers':
codebook = element
break
return vertex_groups, codebook, ply
def decode_quantised_data(vertex_groups, codebook):
"""对量化数据进行解码"""
if codebook is None:
return vertex_groups
# 提取码本中心
codebook_data = {}
for prop in codebook.properties:
codebook_data[prop.name] = codebook[prop.name]
# 解码每个顶点组
decoded_groups = []
for sh_degree, group in vertex_groups:
decoded_group = {}
for prop in group.properties:
if prop.name in codebook_data:
# 码本查表解码
indices = group[prop.name].astype(np.uint8)
decoded_group[prop.name] = codebook_data[prop.name][indices]
else:
decoded_group[prop.name] = group[prop.name]
decoded_groups.append((sh_degree, decoded_group))
return decoded_groups
def merge_vertex_groups(vertex_groups):
"""合并所有 SH degree 组为单一顶点数组"""
all_vertices = []
for sh_degree, group in vertex_groups:
num_vertices = len(next(iter(group.values())))
# 为每个顶点创建标准 62 属性数组
for i in range(num_vertices):
vertex = {}
# 位置 (x, y, z)
vertex['x'] = group['x'][i] if 'x' in group else 0.0
vertex['y'] = group['y'][i] if 'y' in group else 0.0
vertex['z'] = group['z'][i] if 'z' in group else 0.0
# 法线 (nx, ny, nz) – reduced-3DGS 没有法线,填充 0
vertex['nx'] = 0.0
vertex['ny'] = 0.0
vertex['nz'] = 0.0
# DC 颜色系数 (f_dc_0, f_dc_1, f_dc_2)
for j in range(3):
key = f'f_dc_{j}'
vertex[key] = group[key][i] if key in group else 0.0
# SH 系数 (f_rest_0..f_rest_44)
# reduced-3DGS 根据 SH degree 使用不同数量的系数
max_sh_coeffs = 45 # SH degree=3 时的最大系数数
for j in range(max_sh_coeffs):
key = f'f_rest_{j}'
if key in group:
vertex[key] = group[key][i]
else:
vertex[key] = 0.0 # 零填充
# 不透明度
vertex['opacity'] = group['opacity'][i] if 'opacity' in group else 0.0
# 缩放 (scale_0, scale_1, scale_2)
for j in range(3):
key = f'scale_{j}'
vertex[key] = group[key][i] if key in group else 0.0
# 旋转四元数 (rot_0, rot_1, rot_2, rot_3)
for j in range(4):
key = f'rot_{j}'
vertex[key] = group[key][i] if key in group else (1.0 if j == 0 else 0.0)
all_vertices.append(vertex)
return all_vertices
def create_standard_ply(vertices, output_path):
"""创建标准 3DGS PLY 文件"""
# 定义标准 62 属性
dtype = [
('x', 'f4'), ('y', 'f4'), ('z', 'f4'),
('nx', 'f4'), ('ny', 'f4'), ('nz', 'f4')
]
# DC 颜色系数
for i in range(3):
dtype.append((f'f_dc_{i}', 'f4'))
# SH 系数
for i in range(45):
dtype.append((f'f_rest_{i}', 'f4'))
# 其他参数
dtype.extend([
('opacity', 'f4'),
('scale_0', 'f4'), ('scale_1', 'f4'), ('scale_2', 'f4'),
('rot_0', 'f4'), ('rot_1', 'f4'), ('rot_2', 'f4'), ('rot_3', 'f4')
])
# 创建结构化数组
structured_array = np.zeros(len(vertices), dtype=dtype)
for i, vertex in enumerate(vertices):
for key in vertex:
structured_array[i][key] = vertex[key]
# 创建 PLY 元素
vertex_element = PlyElement.describe(structured_array, 'vertex')
# 写入文件
PlyData([vertex_element], text=False).write(output_path)
print(f"Done! Standard PLY with {len(vertices)} vertices written.")
print(f"File size: {len(vertices) * 62 * 4 / (1024*1024):.1f} MB (estimated)")
def main():
args = parse_args()
print(f"Reading: {args.input}")
# 加载 reduced PLY
vertex_groups, codebook, ply = load_reduced_ply(args.input)
# 检测格式
is_quantised = codebook is not None
is_half_float = False
if is_quantised:
print("Quantised: True")
# 检查是否为半精度
if 'features_dc' in codebook.dtype.names:
sample = codebook['features_dc'][0]
if sample.dtype == np.float16 or (isinstance(sample, np.ndarray) and sample.dtype == np.float16):
is_half_float = True
print("Half-float: True")
else:
print("Half-float: False")
else:
print("Quantised: False")
print("Half-float: False")
# 打印顶点组信息
print(f"Vertex groups: {[(sh, len(g)) for sh, g in vertex_groups]}")
if is_quantised:
print(f"Codebook: present")
print(f"Codebook entries: {len(codebook)}")
total_gaussians = sum(len(g) for _, g in vertex_groups)
print(f"Total gaussians: {total_gaussians}")
# 解码量化数据(如果需要)
if is_quantised:
vertex_groups = decode_quantised_data(vertex_groups, codebook)
# 合并顶点组
all_vertices = merge_vertex_groups(vertex_groups)
# 创建标准 PLY
print(f"Writing: {args.output}")
create_standard_ply(all_vertices, args.output)
if __name__ == '__main__':
main()
4.3 使用方法
安装依赖:
pip install plyfile numpy
基本命令:
python convert_to_standard_ply.py -i [输入文件] -o [输出文件]
参数说明:
- -i, –input: 输入 PLY 文件路径(reduced-3DGS 格式)
- -o, –output: 输出 PLY 文件路径(标准 3DGS 格式)
4.4 使用示例
示例 1:转换量化版 PLY
python convert_to_standard_ply.py \\
-i output/6ddc4bf3-6/point_cloud/iteration_30000/point_cloud_quantised.ply \\
-o output/6ddc4bf3-6/point_cloud/iteration_30000/point_cloud_standard.ply
示例 2:转换未量化版 PLY
python convert_to_standard_ply.py \\
-i output/6ddc4bf3-6/point_cloud/iteration_30000/point_cloud.ply \\
-o output/6ddc4bf3-6/point_cloud/iteration_30000/point_cloud_standard.ply
示例 3:转换半精度量化版 PLY
python convert_to_standard_ply.py \\
-i output/6ddc4bf3-6/point_cloud/iteration_30000/point_cloud_quantised_half.ply \\
-o output/6ddc4bf3-6/point_cloud/iteration_30000/point_cloud_standard.ply
4.5 转换验证
转换完成后,将生成的 point_cloud_standard.ply 拖入以下任一在线可视化工具验证:
- SuperSplat: Cleaning Up…
- INRIA 官方 Viewer: 3D Gaussian Splatting for Real-Time Radiance Field Rendering
- gsplat.tech: Home – PlatGS
4.6 转换验证结果
=== 原版 3DGS PLY ===
Element: vertex, count: 3,241,453
Properties (62): x, y, z, nx, ny, nz, f_dc_0..2, f_rest_0..44,
opacity, scale_0..2, rot_0..3
=== 转换后 PLY ===
Element: vertex, count: 5,677,385 ← 更多高斯(VQ 允许更高密度)
Properties (62): 完全匹配 ✅
Properties match: True ✅
结果可视化示例:




