> 技术文档 > 【三维重建】【3DGS系列】【深度学习】3DGS的代码讲解之三维场景数据加载模块解析_readnerfsyntheticinfo

【三维重建】【3DGS系列】【深度学习】3DGS的代码讲解之三维场景数据加载模块解析_readnerfsyntheticinfo


【三维重建】【3DGS系列】【深度学习】3DGS的代码讲解之三维场景数据加载模块解析

文章目录

  • 【三维重建】【3DGS系列】【深度学习】3DGS的代码讲解之三维场景数据加载模块解析
  • 前言
  • sceneLoadTypeCallbacks
  • readColmapSceneInfo
  • readColmapCameras
  • class CameraInfo
  • getNerfppNorm
  • storePly
  • fetchPly
  • SceneInfo
  • readNerfSyntheticInfo
  • readCamerasFromTransforms
  • 总结

前言

在详细解析3DGS代码之前,首要任务是成功运行3DGS代码【理论基础及代码运行(win11下)解析参考教程】,后续学习才有意义。本博文对三维场景数据加载模块代码进行解析,dataset_readers是用于加载和处理不同格式的三维场景数据的核心模块。其他模块代码后续的博文将会陆续讲解。这里只做dataset_readers相关模块的代码解析。
【三维重建】【3DGS系列】【深度学习】3DGS的代码讲解之三维场景数据加载模块解析_readnerfsyntheticinfo


sceneLoadTypeCallbacks

用于根据不同的数据集类型选择对应的场景信息读取函数。

sceneLoadTypeCallbacks = { \"Colmap\": readColmapSceneInfo, # Colmap格式数据集 \"Blender\": readNerfSyntheticInfo # Blender(NeRF合成)格式数据集}

readColmapSceneInfo

读取并处理 COLMAP 输出的相机参数和点云数据,构建一个包含训练/测试相机信息、点云数据和场景归一化参数的统一场景描述对象,用于后续任务。

def readColmapSceneInfo(path, images, eval, llffhold=8): \"\"\" 用于读取、处理和封装由Colmap软件生成的场景信息 @param path:场景文件夹路径 @param images:图像所在目录 @param eval:布尔值,用于指示是否需要划分训练集和测试集 @param llffhold:当eval=True时决定测试集间隔,默认为8 @return:返回场景信息对象 \"\"\" # Colmap支持两种输出格式 try: # 找到指定场景文件路径下sparse/0子目录中二进制格式的相机外参和内参文件 cameras_extrinsic_file = os.path.join(path, \"sparse/0\", \"images.bin\") # 相机外参文件 cameras_intrinsic_file = os.path.join(path, \"sparse/0\", \"cameras.bin\") # 相机内参文件 # 读取二进制格式的相机外参和内参文件 cam_extrinsics = read_extrinsics_binary(cameras_extrinsic_file) # 读取相机外参 cam_intrinsics = read_intrinsics_binary(cameras_intrinsic_file) # 读取相机内参 except: # 找到文本格式的相机外参和内参的文件 cameras_extrinsic_file = os.path.join(path, \"sparse/0\", \"images.txt\") # 相机外参文件 cameras_intrinsic_file = os.path.join(path, \"sparse/0\", \"cameras.txt\") # 相机内参文件 # 读取文本格式的相机外参和内参文件 cam_extrinsics = read_extrinsics_text(cameras_extrinsic_file) # 读取相机外参 cam_intrinsics = read_intrinsics_text(cameras_intrinsic_file) # 读取相机内参 # 用户未指定图像目录则使用默认的\"images\" reading_dir = \"images\" if images == None else images # 确定图像所在的文件夹名称(默认或者指定) # 相机信息的对象列表:整合图像数据集及其对应内外参信息相关信息的对象 cam_infos_unsorted = readColmapCameras(cam_extrinsics=cam_extrinsics, cam_intrinsics=cam_intrinsics,  images_folder=os.path.join(path, reading_dir)) # 对图像相机信息对象按照图像文件名进行排序 cam_infos = sorted(cam_infos_unsorted.copy(), key = lambda x : x.image_name) # 评估模式:需要划分数据集和验证集 if eval: train_cam_infos = [c for idx, c in enumerate(cam_infos) if idx % llffhold != 0] test_cam_infos = [c for idx, c in enumerate(cam_infos) if idx % llffhold == 0] # 非评估模式:所有数据都是训练集 else: train_cam_infos = cam_infos test_cam_infos = [] nerf_normalization = getNerfppNorm(train_cam_infos) # 获取相机位姿的归一化参数(场景中心到原点的平移向量和场景包围球的半径) # 定义几种可能存在的点云文件路径: ply_path = os.path.join(path, \"sparse/0/points3D.ply\") bin_path = os.path.join(path, \"sparse/0/points3D.bin\") txt_path = os.path.join(path, \"sparse/0/points3D.txt\") if not os.path.exists(ply_path): # 没有现成的.ply文件 print(\"Converting point3d.bin to .ply, will happen only the first time you open the scene.\") try: xyz, rgb, _ = read_points3D_binary(bin_path) # 从.bin文件读取点云数据 except: xyz, rgb, _ = read_points3D_text(txt_path) # 从.txt文件读取点云数据 storePly(ply_path, xyz, rgb) # 将读取到的点云数据以.ply格式保存下来,方便下次快速加载 try: pcd = fetchPly(ply_path) # 从.ply文件读取点云数据 except: pcd = None # 封装一个3D场景的完整信息对象 scene_info = SceneInfo(point_cloud=pcd, train_cameras=train_cam_infos, test_cameras=test_cam_infos, nerf_normalization=nerf_normalization, ply_path=ply_path) return scene_info

readColmapCameras

从COLMAP输出的相机内外参数中读取信息,并将每张图像及其对应的相机信息整理成一个结构化的对象列表返回。

NeRF/Gaussian Splatting 数据预处理:准备多视角图像及其相机内外参数。

def readColmapCameras(cam_extrinsics, cam_intrinsics, images_folder): \"\"\" 从COLMAP输出的相机内外参数中读取信息,并将每张图像及其对应的相机信息整理成一个图像相机信息对象列表返回 @param cam_extrinsics:外参对象字典 @param cam_intrinsics:内参对象字典 @param images_folder:图像文件夹路径 @return:返回包含所有图像极其相机信息的对象列表 \"\"\" cam_infos = [] # 存储处理后的所有图像的相机信息对象列表 for idx, key in enumerate(cam_extrinsics): # 遍历所有外参对象 # 实时显示当前正在读取的图像进度数及总数 sys.stdout.write(\'\\r\') sys.stdout.write(\"Reading camera {}/{}\".format(idx+1, len(cam_extrinsics))) sys.stdout.flush() extr = cam_extrinsics[key] # 获得当前图像的外参相关数据对象 intr = cam_intrinsics[extr.camera_id] # 获得当前图像的相机内参相关数据对象 # 获取当前图像尺寸和相机ID height = intr.height width = intr.width uid = intr.id # 将四元数转换为旋转矩阵,并进行转置,转置是为了符合Gaussian Splatting对旋转矩阵形式的要求 R = np.transpose(qvec2rotmat(extr.qvec)) # 获得当前图像的相机的旋转矩阵 T = np.array(extr.tvec) # 获得当前图像的相机的平移向量 # 不同相机类型计算视场角 if intr.model == \"SIMPLE_PINHOLE\": focal_length_x = intr.params[0] # 垂直和水平方向的视场角 FovY = focal2fov(focal_length_x, height) FovX = focal2fov(focal_length_x, width) elif intr.model == \"SIMPLE_RADIAL\": focal_length_x = intr.params[0] # 垂直和水平方向的视场角 FovY = focal2fov(focal_length_x, height) FovX = focal2fov(focal_length_x, width) elif intr.model == \"PINHOLE\": # 垂直和水平方向的视场角 focal_length_x = intr.params[0] focal_length_y = intr.params[1] FovY = focal2fov(focal_length_y, height) FovX = focal2fov(focal_length_x, width) else: assert False, \"Colmap camera model not handled: only undistorted datasets (PINHOLE or SIMPLE_PINHOLE cameras) supported!\" # 图像路径 image_path = os.path.join(images_folder, os.path.basename(extr.name)) # image_path = image_path.replace(\".jpg\", \".png\") # 图像名称 image_name = os.path.basename(image_path).split(\".\")[0] # 读取图像 image = Image.open(image_path) # 整合的图像及其相机内外参信息 # 包含当前图像的相机ID,旋转平移矩阵,垂直水平视场角,图像及其路径和名称已经图像尺寸 cam_info = CameraInfo(uid=uid, R=R, T=T, FovY=FovY, FovX=FovX, image=image, image_path=image_path, image_name=image_name, width=width, height=height) cam_infos.append(cam_info) sys.stdout.write(\'\\n\') # 返回包含所有图像极其相机信息的对象列表 return cam_infos

class CameraInfo

将图像极对应相机的所有相关内外参信息组织成一个结构化的数据类型。

方便在三维重建、渲染、SLAM、神经渲染(如 NeRF、Gaussian Splatting)等任务中传递相机参数。

# 名为CameraInfo的类:将图像极对应相机的所有相关内外参信息组织成一个结构化的数据类型class CameraInfo(NamedTuple): uid: int # 相机ID R: np.array # 3x3旋转矩阵 T: np.array # 3x31移向量 FovY: np.array # 垂直方向视场角,用于渲染或投影计算时确定相机视角范围 FovX: np.array # 水平方向视场角 image: np.array # 图像数据 image_path: str # 图像文件的路径 image_name: str # 图像文件名 width: int # 图像的宽度 height: int # 图像的高度

getNerfppNorm

计算场景归一化参数,返回将场景中心移至原点的平移向量和包含所有相机的包围球半径。

def getNerfppNorm(cam_info): \"\"\" 根据一组图像相机信息对象计算出一个标准化场景中心的平移向量和场景半径,NeRF++中常用的归一化策略 @param cam_info:一组图像相机信息对象列表 @return:字典{平移向量,用于将场景中心移到原点;场景包围球的半径,用于缩放或规范化空间范围} \"\"\" def get_center_and_diag(cam_centers): # 给定一组相机中心坐标cam_centers,计算平均中心点和平移最大距离,即包围球的直径的一半 # 每个(3,1)拼接成一个(3,N)的矩阵 cam_centers = np.hstack(cam_centers) # 将所有相机中心点拼接 # axis=1:对每个坐标轴x/y/z求平均;keepdims=True:保持维度为(3,1) avg_cam_center = np.mean(cam_centers, axis=1, keepdims=True) # 计算所有相机中心在世界坐标下的平均位置(质心) center = avg_cam_center dist = np.linalg.norm(cam_centers - center, axis=0, keepdims=True) # 计算每个相机中心到平均中心的距离(L2范数) diagonal = np.max(dist) # 取最大距离作为包围球的半径,目的是确保这个球能完全包裹住所有相机的位置 # 返回平均中心点和最大距离 return center.flatten(), diagonal cam_centers = [] # 用于存储所有相机的世界坐标中心(相机位置) for cam in cam_info: # 遍历图像相机信息对象列表 W2C = getWorld2View2(cam.R, cam.T) # 世界坐标系到相机坐标的变换矩阵 C2W = np.linalg.inv(W2C) # 求逆,相机坐标系到世界坐标系的变换矩阵 cam_centers.append(C2W[:3, 3:4]) # 获取每个相机在世界坐标中的位置 center, diagonal = get_center_and_diag(cam_centers) # 获取平均相机中心和最大平移距离 radius = diagonal * 1.1 # 增加一个安全边界,确保所有相机都位于包围球内部 translate = -center # 构造一个平移向量,将场景中心移到原点的平移向量 return {\"translate\": translate, \"radius\": radius}

storePly

将带有颜色的点云数据保存为PLY格式文件。

def storePly(path, xyz, rgb): \"\"\" 将给定的点云坐标xyz和颜色rgb保存为一个.ply格式的点云文件 @param path:输出.ply文件路径 @param xyz:所有点云的(x,y,z)坐标,形状(n,3) @param rgb:对应点云的颜色(r,g,b),形状(n,3),值范围通常是[0,255] \"\"\" # 定义了一个结构化np数组的数据类型 dtype = [(\'x\', \'f4\'), (\'y\', \'f4\'), (\'z\', \'f4\'), # x,y,z:点云三维坐标,使用float32类型 (\'nx\', \'f4\'), (\'ny\', \'f4\'), (\'nz\', \'f4\'),  # nx,ny,nz:法向量,默认为 0 (\'red\', \'u1\'), (\'green\', \'u1\'), (\'blue\', \'u1\')] # red,green,blue:颜色通道,使用uint8类型(\'u1\') normals = np.zeros_like(xyz) # 没有提供法向量信息,默认将法向量设置为全零 elements = np.empty(xyz.shape[0], dtype=dtype) # 长度为点的数量n的空结构化np数组 # 每一行的顺序为[x y z nx ny nz r g b] attributes = np.concatenate((xyz, normals, rgb), axis=1) # xyz,normals,rgb在列方向上拼接成一个(n,9)的二维数组 # 将每行转换为元组,然后赋值给结构化数组,每个字段就对应上了定义好的dtype elements[:] = list(map(tuple, attributes)) # 填充结构化数组 # PLY是一种用于存储3D模型数据的文件格式, # vertex表示顶点;face表示面片 vertex_element = PlyElement.describe(elements, \'vertex\') # 将结构化数组描述为PLY格式的顶点数据 ply_data = PlyData([vertex_element]) # 构建一个PlyData对象,包含一个顶点元素列表 ply_data.write(path) # 将数据写入磁盘文件,默认二进制格式 # ply_data.write_ascii(path) # 将数据写入磁盘文件,ASCII格式

fetchPly

从 .ply 文件中读取三维点云数据,并将其解析为包含坐标、颜色和法向量的结构化点云对象返回。

def fetchPly(path): \"\"\" 从一个.ply格式的点云文件中读取数据 @param path:要读取的3D点云.ply文件路径 @return:点云对象(封装了点坐标,颜色和法向量的数据结构) \"\"\" plydata = PlyData.read(path) # 使用.read()方法读取.ply文件 vertices = plydata[\'vertex\'] # 提取PlyData对象的vertex的元素,即所有点云顶点数据 # 从顶点数据中提取x,y,z坐标,将它们堆叠成(3,n)的数组再通过.T转置为(n,3) positions = np.vstack([vertices[\'x\'], vertices[\'y\'], vertices[\'z\']]).T # 读取三维坐标 # 提取r,g,b颜色通道转置为(n,3),将[0,255]的整数转换为[0,1]浮点范围 colors = np.vstack([vertices[\'red\'], vertices[\'green\'], vertices[\'blue\']]).T / 255.0 # 读取颜色信息并归一化 # # 提取法向量转置为(n,3) normals = np.vstack([vertices[\'nx\'], vertices[\'ny\'], vertices[\'nz\']]).T # 读取法向量信息 # 将提取的点坐标,颜色,法向量封装为一个统一的点云对象返回 return BasicPointCloud(points=positions, colors=colors, normals=normals)

SceneInfo

统一管理一个完整3D场景的数据结构。

# 名为SceneInfo的类:用于封装一个 3D 场景的所有相关信息class SceneInfo(NamedTuple): point_cloud: BasicPointCloud # 封装整个场景的点云信息对象,包含点云坐标、颜色和法向量 train_cameras: list # 所有训练用的图像相机信息对象列表,整合的图像及对应的相机内外参信息 test_cameras: list  # 所有测试用的图像相机信息对象列表,整合的图像及对应的相机内外参信息 nerf_normalization: dict # 场景中心到原点的平移向量和场景包围球的半径 ply_path: str  # 原始点云.ply文件的位置

readNerfSyntheticInfo

读取并处理 NeRF 合成数据集的训练和测试相机参数,生成或加载点云数据,构建一个包含训练/测试相机信息、点云数据和场景归一化参数的统一场景描述对象,用于后续任务。

def readNerfSyntheticInfo(path, white_background, eval, extension=\".png\"): \"\"\" 用来读取、处理和封装NERF合成数据集的场景信息 @param path:场景文件夹路径 @param white_background:布尔值,是否将背景设置为白色 @param eval:布尔值,用于指示是否需要划分训练集和测试集 @param extension:图像文件扩展名,默认为.png @return:返回场景信息对象 \"\"\" print(\"Reading Training Transforms\") # 打印消息,表示开始读取训练变换信息 # 相机信息的对象训练列表:整合图像数据集及其对应内外参信息相关信息的对象 train_cam_infos = readCamerasFromTransforms(path, \"transforms_train.json\", white_background, extension) print(\"Reading Test Transforms\") # 打印消息,表示开始读取测试变换信息 # 相机信息的对象测试列表:整合图像数据集及其对应内外参信息相关信息的对象 test_cam_infos = readCamerasFromTransforms(path, \"transforms_test.json\", white_background, extension) if not eval: # 非评估模式:所有数据都是训练集 train_cam_infos.extend(test_cam_infos) # 将测试集合并到训练集中 test_cam_infos = [] # 清空测试集 nerf_normalization = getNerfppNorm(train_cam_infos) # 获取相机位姿的归一化参数(场景中心到原点的平移向量和场景包围球的半径) # NeRF合成数据集通常没有真实的点云,所以可能需要生成一个随机点云作为初始化 ply_path = os.path.join(path, \"points3d.ply\") # 定义点云文件路径 # # 检查点云文件是否存在 if not os.path.exists(ply_path): # 不存在点云文件则生成一个随机点云 num_pts = 100_000 # 定义要生成的点的数量为100000 print(f\"Generating random point cloud ({num_pts})...\") # 打印消息通知用户正在生成随机点云 xyz = np.random.random((num_pts, 3)) * 2.6 - 1.3 # 生成随机点的位置x,y,z坐标 shs = np.random.random((num_pts, 3)) / 255.0 # 生成随机点的颜色,根据球谐系数转换为RGB颜色 # pcd = BasicPointCloud(points=xyz, colors=SH2RGB(shs), normals=np.zeros((num_pts, 3))) storePly(ply_path, xyz, SH2RGB(shs) * 255) # 将读取到的点云数据以.ply格式保存下来,方便下次快速加载 try: pcd = fetchPly(ply_path) # 从.ply文件读取点云数据 except: pcd = None # 封装一个3D场景的完整信息对象 scene_info = SceneInfo(point_cloud=pcd, train_cameras=train_cam_infos, test_cameras=test_cam_infos, nerf_normalization=nerf_normalization, ply_path=ply_path) return scene_info

readCamerasFromTransforms

从NeRF合成数据集的JSON变换文件中读取相机内外参数,并将每张图像及其对应的相机信息整理成一个结构化的对象列表返回。

def readCamerasFromTransforms(path, transformsfile, white_background, extension=\".png\"): \"\"\" 从NeRF合成数据集的JSON变换文件中读取相机内外参数,并将每张图像及其对应的相机信息整理成一个结构化的对象列表返回 @param path:场景文件夹路径 @param transformsfile:相机变换信息JSON文件名 @param white_background:布尔值,是否使用白色背景 @param extension:图片文件扩展名,默认为.png @return:返回包含所有图像极其相机信息的对象列表 \"\"\" cam_infos = [] # 存储处理后的所有图像的相机信息对象列表 with open(os.path.join(path, transformsfile)) as json_file: # 打开并读取JSON文件 contents = json.load(json_file) # 加载JSON文件内容为字典,包含整个变换文件的数据结构 fovx = contents[\"camera_angle_x\"] # 提取相机的水平视角 frames = contents[\"frames\"] # 提取图像信息,包括图像路径及其变换矩阵 for idx, frame in enumerate(frames): # 遍历所有图像信息 cam_name = os.path.join(path, frame[\"file_path\"] + extension) # 构造当前图像文件的完整路径 c2w = np.array(frame[\"transform_matrix\"]) # 获取当前图像相机坐标系到世界坐标系的变换矩阵 # NeRF坐标系: X向右,Y向下,Z向前; COLMAP坐标系:X向右,Y向上,Z向前 c2w[:3, 1:3] *= -1 # 调整坐标轴方向以匹配COLMAP坐标系 w2c = np.linalg.inv(c2w) # 求逆,计算世界坐标系到相机坐标系的变换矩阵 R = np.transpose(w2c[:3,:3]) # 提取3×3旋转矩阵R,转置是为了符合Gaussian Splatting对旋转矩阵形式的要求 T = w2c[:3, 3]  # 提取3×1平移向量T image_path = os.path.join(path, cam_name) # 构造图像文件的实际路径 # image_path = image_path.replace(\".jpg\", \".png\") image_path = image_path.replace(\".png\", \".jpg\") image_name = Path(cam_name).stem # 提取不带扩展名的图像名 image = Image.open(image_path) # 读取图像 im_data = np.array(image.convert(\"RGBA\")) # 转换为带有透明度通道的格式,后续用于背景融合操作 bg = np.array([1,1,1]) if white_background else np.array([0, 0, 0]) # 根据用户设定决定使用白色还是黑色背景 norm_data = im_data / 255.0 # 图像归一化 # 公式:color = foreground * alpha + background * (1 - alpha) arr = norm_data[:, :, :3] * norm_data[:, :, 3:4] + bg * (1 - norm_data[:, :, 3:4]) # 使用透明度通道将图像与背景融合 image = Image.fromarray(np.array(arr*255.0, dtype=np.byte), \"RGB\") # 换回RGB格式 # 先通过fov2focal()由FOV得到焦距,再通过focal2fov()得到垂直方向的视场角 fovy = focal2fov(fov2focal(fovx, image.size[0]), image.size[1]) # 计算垂直方向的视场角 FovY = fovy # 垂直方向视角fovy FovX = fovx # 水平方向视角fovx # 整合的图像及其相机内外参信息 # 包含当前图像的相机ID,旋转平移矩阵,垂直水平视场角,图像及其路径和名称已经图像尺寸 cam_infos.append(CameraInfo(uid=idx, R=R, T=T, FovY=FovY, FovX=FovX, image=image, image_path=image_path, image_name=image_name, width=image.size[0], height=image.size[1])) return cam_infos

总结

尽可能简单、详细的介绍了三维场景数据加载模块的功能和作用。