1. ScanNet数据集核心文件全景解析
第一次接触ScanNet数据集时,面对几十种文件格式确实容易让人摸不着头脑。我刚开始研究时,光是区分各种.ply文件就花了整整两天时间。不过别担心,经过多次实战踩坑后,我把这些文件整理成了清晰的逻辑框架。
ScanNet的核心文件可以分为三大类:几何数据、标注数据和传感器数据。几何数据主要包括_vh_clean.ply和_vh_clean_2.ply这两个点云文件,前者是原始高精度网格,后者是优化后的版本。实测发现_vh_clean_2.ply在保持95%精度的同时,文件大小只有前者的60%,更适合日常实验使用。
标注数据是ScanNet的精华所在,包括:
- .aggregation.json:实例级标注的"百科全书"
- _vh_clean_2.labels.ply:每个点的语义标签
- _vh_clean_2.0.010000.segs.json:点云分割索引 这三个文件构成了从语义到实例的完整标注体系。
传感器数据则以.sens文件为代表,它像是个时间胶囊,保存了原始采集时的RGB-D序列、相机位姿等信息。有趣的是,这个二进制文件解压后平均包含3000+帧数据,但通过特殊编码压缩后体积只有原始数据的1/10。
2. 点云文件深度解析与实战读取
2.1 PLY文件结构与Python读取
_vh_clean_2.ply是使用频率最高的文件,它采用标准的PLY格式存储。用文本编辑器打开文件头部,你会看到这样的元信息:
ply format binary_little_endian 1.0 element vertex 124371 property float x property float y property float z property uchar red property uchar green property uchar blue这告诉我们文件包含124371个顶点,每个顶点有坐标(x,y,z)和颜色(r,g,b)信息。
读取PLY文件最稳妥的方式是使用PlyData库:
from plyfile import PlyData import numpy as np def load_ply(ply_path): with open(ply_path, 'rb') as f: plydata = PlyData.read(f) vertices = plydata['vertex'] points = np.vstack([vertices['x'], vertices['y'], vertices['z']]).T colors = np.vstack([vertices['red'], vertices['green'], vertices['blue']]).T return points, colors这里有个坑要注意:不同版本的PlyData对属性名的处理可能不同,有些数据集会把颜色通道命名为'r','g','b'而不是'red','green','blue'。
2.2 全局坐标对齐实战
ScanNet的每个场景都有独立的坐标系,需要通过.txt文件中的axisAlignment矩阵进行统一。这个4x4矩阵实际上包含了旋转和平移变换。我曾因为忽略这个步骤导致两个场景的点云怎么都对不齐,浪费了半天时间。
正确的对齐代码应该是:
def apply_alignment(coords, scan_dir, scan_id): align_matrix = np.eye(4) txt_path = os.path.join(scan_dir, scan_id, f"{scan_id}.txt") if os.path.exists(txt_path): with open(txt_path, 'r') as f: for line in f: if line.startswith('axisAlignment'): align_matrix = np.array( [float(x) for x in line.strip().split()[-16:]] ).reshape(4, 4) break homog_coords = np.hstack([coords, np.ones((coords.shape[0], 1))]) return (homog_coords @ align_matrix.T)[:, :3]特别注意对齐操作要在所有几何变换之前进行,否则会导致后续处理全部基于错误的坐标系。
3. 标注文件系统解读与联合使用
3.1 语义标签与实例标注的关联
ScanNet的标注系统设计非常精妙,但也容易让人困惑。关键在于理解这三个文件的层级关系:
- .labels.ply:给每个点打上语义标签(如"椅子")
- .segs.json:将点分组为物体部件
- .aggregation.json:将部件组装成完整实例
举个例子,一个办公椅可能有:
- 50个点被标记为"椅子"(语义层)
- 这些点分为5个segments:底座、轮子、靠背等(部件层)
- 最终聚合成1个椅子实例(实例层)
3.2 实例标注的完整读取流程
要将原始标注转换为可用的实例标签,需要经过以下步骤:
def load_instance_labels(scan_dir, scan_id): # 加载基础点云 coords, _ = load_ply(f"{scan_id}_vh_clean_2.ply") # 读取语义标签 with open(f"{scan_id}_vh_clean_2.labels.ply", 'rb') as f: sem_labels = np.array(PlyData.read(f)['vertex']['label']) # 建立segment到点的映射 with open(f"{scan_id}_vh_clean_2.0.010000.segs.json", 'r') as f: seg_indices = json.load(f)['segIndices'] seg_to_points = defaultdict(list) for point_idx, seg_idx in enumerate(seg_indices): seg_to_points[seg_idx].append(point_idx) # 聚合实例信息 instance_labels = np.full(len(coords), -1) with open(f"{scan_id}.aggregation.json", 'r') as f: agg_data = json.load(f) for instance in agg_data['segGroups']: for seg_id in instance['segments']: point_indices = seg_to_points[seg_id] instance_labels[point_indices] = instance['id'] return sem_labels, instance_labels这个过程看似复杂,但实际反映了真实世界中物体的组成方式 - 由部件到整体。
4. 传感器数据处理技巧
4.1 .sens文件解析实战
.sens文件是二进制格式,需要使用专门的读取工具。推荐使用ScanNet提供的官方工具包:
from liblzfse import decompress import struct def read_sens_file(file_path): with open(file_path, 'rb') as f: # 读取文件头 version = struct.unpack('I', f.read(4))[0] if version != 1: raise ValueError("Unsupported version") # 读取传感器参数 color_width, color_height = struct.unpack('II', f.read(8)) depth_width, depth_height = struct.unpack('II', f.read(8)) # 读取帧数据 frames = [] while True: try: # 解压缩颜色帧 color_size = struct.unpack('I', f.read(4))[0] color_data = decompress(f.read(color_size)) # 解压缩深度帧 depth_size = struct.unpack('I', f.read(4))[0] depth_data = decompress(f.read(depth_size)) # 读取位姿 pose = struct.unpack('f'*16, f.read(16*4)) frames.append({ 'color': np.frombuffer(color_data, dtype=np.uint8), 'depth': np.frombuffer(depth_data, dtype=np.uint16), 'pose': np.array(pose).reshape(4,4) }) except: break return frames处理.sens文件时最常遇到的问题是内存不足,因为一个完整的.sens文件可能包含数千帧数据。建议按需读取,或者先转换为更高效的格式如HDF5。
4.2 从传感器数据重建点云
利用相机位姿和深度图可以重建原始点云,这对理解数据采集过程很有帮助:
def depth_to_pointcloud(depth, pose, intrinsics): # 创建像素网格 v, u = np.indices(depth.shape) z = depth.astype(float) / 1000.0 # 转换为米 # 反投影到相机坐标系 x = (u - intrinsics[0,2]) * z / intrinsics[0,0] y = (v - intrinsics[1,2]) * z / intrinsics[1,1] # 转换到世界坐标系 points = np.vstack([x.flatten(), y.flatten(), z.flatten()]) points = np.vstack([points, np.ones(points.shape[1])]) world_points = (pose @ points)[:3,:] return world_points.T这个方法可以帮助验证标注点云的准确性,我在调试时发现有些标注点云与重建结果存在厘米级的偏差,这是传感器噪声和重建算法共同导致的结果。