3D Gaussian Splatting代码中的Gaussian_Module和Cameras两个类的代码解读

Gaussian_model

讨论Gaussian_model这个类,是因为里面包含了三维高斯分布的基本信息,里面定义了各种参量的构建方式、用于优化学习的激活函数、学习率设置方法和高斯点优化过程中的增加与删除方式及对应优化器的处理方法。这个类定义在scene文件夹中的gaussian_module.py文件里。

scene/gaussian_model中进行函数构建

在这里插入图片描述

协方差矩阵和各种参数的激活函数

首先这个方法构建了协方差矩阵,而其中旋转、缩放矩阵在以下文件中:

utils/general_utils的build_rotation中构建旋转矩阵

'''
用来计算四元数表示的旋转矩阵(Rotation Matrix)的。。

首先,通过计算每个四元数的模长,来标准化它们。这是为了确保旋转向量(四元数)的单位长度,以便于正确地进行旋转计算。

接着,将标准化后的四元数应用到旋转矩阵上。这里,代码创建了一个大小为 (batch_size, 3, 3) 的零张量 R,然后,通过四元数的各个分量进行矩阵赋值,根据四元数到旋转矩阵的转换公式来填充这个张量。

最后,返回填充完的旋转矩阵 R。

需要注意的是,这段代码是针对批处理的,因此输入 r 是一个张量,其中每一行代表一个四元数。
'''
def build_rotation(r):
    norm = torch.sqrt(r[:,0]*r[:,0] + r[:,1]*r[:,1] + r[:,2]*r[:,2] + r[:,3]*r[:,3])

    q = r / norm[:, None]

    R = torch.zeros((q.size(0), 3, 3), device='cuda')

    r = q[:, 0]
    x = q[:, 1]
    y = q[:, 2]
    z = q[:, 3]

    R[:, 0, 0] = 1 - 2 * (y*y + z*z)
    R[:, 0, 1] = 2 * (x*y - r*z)
    R[:, 0, 2] = 2 * (x*z + r*y)
    R[:, 1, 0] = 2 * (x*y + r*z)
    R[:, 1, 1] = 1 - 2 * (x*x + z*z)
    R[:, 1, 2] = 2 * (y*z - r*x)
    R[:, 2, 0] = 2 * (x*z - r*y)
    R[:, 2, 1] = 2 * (y*z + r*x)
    R[:, 2, 2] = 1 - 2 * (x*x + y*y)
    return R

utils/general_utils的build_scaling_rotation中构建缩放矩阵并将旋转矩阵和缩放矩阵合并

def build_scaling_rotation(s, r):
    L = torch.zeros((s.shape[0], 3, 3), dtype=torch.float, device="cuda")
    R = build_rotation(r)

    L[:,0,0] = s[:,0]
    L[:,1,1] = s[:,1]
    L[:,2,2] = s[:,2]

    L = R @ L
    return L

scene/gaussian_model的setup_functions中用build_covariance_from_scaling_rotation中将旋转矩阵和缩放矩阵合并并处理成协方差矩阵。并在setup_functions中设定激活函数。

def setup_functions(self):
        def build_covariance_from_scaling_rotation(scaling, scaling_modifier, rotation):
            L = build_scaling_rotation(scaling_modifier * scaling, rotation)
            actual_covariance = L @ L.transpose(1, 2)
            symm = strip_symmetric(actual_covariance)
            return symm
        
        self.scaling_activation = torch.exp
        self.scaling_inverse_activation = torch.log

        self.covariance_activation = build_covariance_from_scaling_rotation

        self.opacity_activation = torch.sigmoid
        self.inverse_opacity_activation = inverse_sigmoid

        self.rotation_activation = torch.nn.functional.normalize
'''
self.scaling_activation = torch.exp:这行代码将指数函数 torch.exp 赋值给了 self.scaling_activation。这意味着在网络中,会使用指数函数作为缩放操作的激活函数。

self.scaling_inverse_activation = torch.log:这行代码将对数函数 torch.log 赋值给了 self.scaling_inverse_activation。这表示在网络中,会使用对数函数作为缩放的逆操作的激活函数。

self.covariance_activation = build_covariance_from_scaling_rotation:这行代码将一个函数 build_covariance_from_scaling_rotation 赋值给了 self.covariance_activation。这可能是一个自定义的函数,用于构建协方差矩阵,该函数可能会使用了缩放和旋转操作。

self.opacity_activation = torch.sigmoid:这行代码将 sigmoid 函数 torch.sigmoid 赋值给了 self.opacity_activation。这表示在网络中,会使用 sigmoid 函数作为不透明度的激活函数。

self.inverse_opacity_activation = inverse_sigmoid:这行代码将一个函数 inverse_sigmoid 赋值给了 self.inverse_opacity_activation。这可能是一个自定义的函数,用于计算 sigmoid 函数的逆操作。

self.rotation_activation = torch.nn.functional.normalize:这行代码将归一化函数 torch.nn.functional.normalize 赋值给了 self.rotation_activation。这表示在网络中,会使用归一化函数作为旋转操作的激活函数。
'''

点云数据的处理

初始化参数的含义:

def __init__(self, sh_degree: int):
    # 初始化球谐函数相关的度数
    # 初始化当前活动的球谐函数度数为 0,并将最大球谐函数度数设置为传入的 sh_degree 参数。
    self.active_sh_degree = 0
    self.max_sh_degree = sh_degree

    # 初始化存储点、球谐函数系数、缩放、旋转、不透明度等的张量为空张量
    self._xyz = torch.empty(0)
    self._features_dc = torch.empty(0)
    self._features_rest = torch.empty(0)
    self._scaling = torch.empty(0)
    self._rotation = torch.empty(0)
    self._opacity = torch.empty(0)
    
    # 初始化高斯分布投影后的最大二维半径、梯度累积器(用于辨别是否需要新增和删除高斯)和分母张量(表示统计了多少次累计梯度,最后要把这个分母张量除掉)为空张量
    self.max_radii2D = torch.empty(0)
    self.xyz_gradient_accum = torch.empty(0)
    self.denom = torch.empty(0)

    # 初始化优化器optimizer为 None
    self.optimizer = None
    
    # 初始化密度百分比和空间学习率缩放因子(用于处理不同参数对学习率的要求)
    self.percent_dense = 0
    self.spatial_lr_scale = 0

    # 调用设置函数的方法进行必要的初始化
    self.setup_functions()

scene/gaussian_model的create_from_pcd

def create_from_pcd(self, pcd : BasicPointCloud, spatial_lr_scale : float):
    self.spatial_lr_scale = spatial_lr_scale
    
    # 将点云数据中的点坐标转换为 PyTorch 张量,并放置在 GPU 上进行加速处理
    fused_point_cloud = torch.tensor(np.asarray(pcd.points)).float().cuda()
    
    '''
    球谐函数这一堆写在另一个文件里。
    '''
    
    print("Number of points at initialisation : ", fused_point_cloud.shape[0])
    
    # 计算点云中每个点与原点的欧氏距离的平方,并将其限制在一个最小值以上,以避免出现除以零的情况
    '''
    这行代码使用了函数 distCUDA2 来计算点云中每个点之间的距离,并将结果存储在 dist2 变量中。
	torch.from_numpy(np.asarray(pcd.points)).float().cuda() 将点云数据转换为 PyTorch 张量,并将其移到 GPU 上。
	torch.clamp_min 函数用于将 dist2 中的所有元素的最小值限制为 0.0000001,以确保不会出现零距离。	
    '''
    dist2 = torch.clamp_min(distCUDA2(torch.from_numpy(np.asarray(pcd.points)).float().cuda()), 0.0000001)
    
    '''
    这行代码首先计算了 dist2 中每个元素的平方根,然后取其自然对数。
[..., None] 用于在张量的最后一个维度上添加一个新的维度。
repeat(1, 3) 表示沿着第一个维度将张量复制三次,以便将其扩展为与 fused_point_cloud 相同的形状,其中 fused_point_cloud 是点云的坐标。
最终,scales 是一个与 fused_point_cloud 具有相同形状的张量,用于存储每个点的缩放因子。
    '''
    scales = torch.log(torch.sqrt(dist2))[...,None].repeat(1, 3)
    
    '''
    这行代码创建了一个形状为 (点数, 4) 的全零张量 rots,用于存储点云中每个点的旋转信息。
每个点的旋转信息是一个四维向量,其中第一个元素为1,其余元素为0,表示点云中的每个点都没有旋转。
    '''
    rots = torch.zeros((fused_point_cloud.shape[0], 4), device="cuda")
    
    '''
    这行代码将 rots 张量的第一列(即第一个元素)设置为1,以表示每个点的旋转信息中的第一个元素为1,其余元素为0,即单位四元数。
    '''
    rots[:, 0] = 1
    
    # 计算每个点的不透明度,这里使用了一个 sigmoid 函数的逆函数
    opacities = inverse_sigmoid(0.1 * torch.ones((fused_point_cloud.shape[0], 1), dtype=torch.float, device="cuda"))
    
    # 将点云的坐标、特征、尺度、旋转和不透明度分别存储为对象的参数,并设置为可训练
    self._xyz = nn.Parameter(fused_point_cloud.requires_grad_(True))
    self._features_dc = nn.Parameter(features[:,:,0:1].transpose(1, 2).contiguous().requires_grad_(True))
    self._features_rest = nn.Parameter(features[:,:,1:].transpose(1, 2).contiguous().requires_grad_(True))
    self._scaling = nn.Parameter(scales.requires_grad_(True))
    self._rotation = nn.Parameter(rots.requires_grad_(True))
    self._opacity = nn.Parameter(opacities.requires_grad_(True))
    
    # 初始化一个用于存储二维最大半径的张量,其形状与点云的数量相同
    self.max_radii2D = torch.zeros((self.get_xyz.shape[0]), device="cuda")

这里再放一个代码块说一下这个distCUDA2的函数是怎么定义的,在simple-knn/spatial.cu中

// 计算输入张量 `points` 中每个点到其他所有点的距离之和,并返回每个点的平均距离。
torch::Tensor distCUDA2(const torch::Tensor& points)
{
  const int P = points.size(0); // 点的数量
  auto float_opts = points.options().dtype(torch::kFloat32); // Float32 类型选项
  torch::Tensor means = torch::full({P}, 0.0, float_opts); // 用于存储平均距离的张量
  
  // 调用 SimpleKNN::knn 函数计算距离
  SimpleKNN::knn(P, (float3*)points.contiguous().data<float>(), means.contiguous().data<float>());
  
  return means; // 返回包含平均距离的张量
}

这就是点云文件ply以文本形式打开后的变量内容,里面的参量很好理解。重点说一下f_dc是球谐函数的直流分量,而f_rest是球谐函数的高阶分量。nx,ny,nz是每个高斯分布的法向量,但考虑到高斯分布的性质,这里的法向量都设置为0。

在这里插入图片描述

def training_setup(self, training_args):
    # 保存训练参数中的 percent_dense 到对象的属性中,用于后续的训练。
    self.percent_dense = training_args.percent_dense
    # 创建一个全零张量 xyz_gradient_accum,用于累积 xyz 坐标的梯度,张量的形状与 self.get_xyz 的第一维度匹配,并将其放置在 GPU 上。
    self.xyz_gradient_accum = torch.zeros((self.get_xyz.shape[0], 1), device="cuda")
    # 创建一个全零张量 denom,用于存储某种归一化或计数信息,张量的形状与 self.get_xyz 的第一维度匹配,并将其放置在 GPU 上。
    self.denom = torch.zeros((self.get_xyz.shape[0], 1), device="cuda")

    l = [
        {'params': [self._xyz], 'lr': training_args.position_lr_init * self.spatial_lr_scale, "name": "xyz"},
        {'params': [self._features_dc], 'lr': training_args.feature_lr, "name": "f_dc"},
        {'params': [self._features_rest], 'lr': training_args.feature_lr / 20.0, "name": "f_rest"},
        {'params': [self._opacity], 'lr': training_args.opacity_lr, "name": "opacity"},
        {'params': [self._scaling], 'lr': training_args.scaling_lr, "name": "scaling"},
        {'params': [self._rotation], 'lr': training_args.rotation_lr, "name": "rotation"}
    ]
	# 使用 Adam 优化器,将参数列表 l 传递给优化器,并设置优化器的全局学习率为 0.0(具体学习率在每个参数字典中指定),以及非常小的 eps 值 1e-15。
    self.optimizer = torch.optim.Adam(l, lr=0.0, eps=1e-15)
    # lr_init: 初始学习率,按 self.spatial_lr_scale 进行缩放。lr_final: 最终学习率,按 self.spatial_lr_scale 进行缩放。lr_delay_mult: 学习率延迟倍数。max_steps: 最大训练步数。
    self.xyz_scheduler_args = get_expon_lr_func(lr_init=training_args.position_lr_init*self.spatial_lr_scale,                                                   lr_final=training_args.position_lr_final*self.spatial_lr_scale,
                                                   lr_delay_mult=training_args.position_lr_delay_mult,
                                                   max_steps=training_args.position_lr_max_steps)
def update_learning_rate(self, iteration):
    ''' Learning rate scheduling per step '''
    # 遍历 self.optimizer 中的所有参数组。self.optimizer.param_groups 是一个包含多个字典的列表,每个字典包含了一组参数及其相关的优化信息(例如学习率)。
    for param_group in self.optimizer.param_groups:
        # 检查当前参数组的名称是否为 "xyz"。只有名称为 "xyz" 的参数组才会进行学习率更新。
        if param_group["name"] == "xyz":
            # 调用 self.xyz_scheduler_args 函数,并传入当前的迭代次数 iteration。self.xyz_scheduler_args 是一个学习率调度函数,根据当前的迭代次数计算并返回新的学习率 lr。 
            lr = self.xyz_scheduler_args(iteration)
            # 将计算得到的新学习率 lr 更新到当前的参数组中,使其在接下来的训练步骤中使用新的学习率。
            param_group['lr'] = lr
            return lr
def construct_list_of_attributes(self):
    # 初始化一个列表 l,包含一些固定的属性名称:'x', 'y', 'z' 表示点的坐标,'nx', 'ny', 'nz' 表示法线的分量
    l = ['x', 'y', 'z', 'nx', 'ny', 'nz']
    # All channels except the 3 DC
    # 遍历 _features_dc 的通道和特征维度,生成特征属性名称
    for i in range(self._features_dc.shape[1] * self._features_dc.shape[2]):
        l.append('f_dc_{}'.format(i))
    # 遍历 _features_rest 的通道和特征维度,生成特征属性名称
    for i in range(self._features_rest.shape[1] * self._features_rest.shape[2]):
        l.append('f_rest_{}'.format(i))
    # 添加透明度属性
    l.append('opacity')
    # 遍历 _scaling 张量的所有通道,生成缩放因子属性名称
    for i in range(self._scaling.shape[1]):
        l.append('scale_{}'.format(i))
    # 遍历 _rotation 张量的所有通道,生成旋转信息属性名称
    for i in range(self._rotation.shape[1]):
        l.append('rot_{}'.format(i))
    return l
def save_ply(self, path):
    # 创建保存路径的目录,如果目录不存在
    mkdir_p(os.path.dirname(path))

    # 获取 xyz 坐标,并将其从 GPU 上移至 CPU,并转换为 numpy 数组
    xyz = self._xyz.detach().cpu().numpy()
    
    # 初始化法向量数组,大小与 xyz 坐标相同
    normals = np.zeros_like(xyz)
    
    # 获取并处理特征数据,将其从 GPU 移至 CPU,并转换为 numpy 数组
    f_dc = self._features_dc.detach().transpose(1, 2).flatten(start_dim=1).contiguous().cpu().numpy()
    f_rest = self._features_rest.detach().transpose(1, 2).flatten(start_dim=1).contiguous().cpu().numpy()
    
    # 获取并处理不透明度数据
    opacities = self._opacity.detach().cpu().numpy()
    
    # 获取并处理缩放数据
    scale = self._scaling.detach().cpu().numpy()
    
    # 获取并处理旋转数据
    rotation = self._rotation.detach().cpu().numpy()

    # 构建 dtype,用于 numpy 的结构化数组,包含所有属性名称及其类型
    dtype_full = [(attribute, 'f4') for attribute in self.construct_list_of_attributes()]

    # 创建一个空的结构化数组,用于存储所有顶点的数据
    elements = np.empty(xyz.shape[0], dtype=dtype_full)
    
    # 将所有属性组合到一个二维数组中
    attributes = np.concatenate((xyz, normals, f_dc, f_rest, opacities, scale, rotation), axis=1)
    
    # 将属性数据逐个赋值到结构化数组中
    elements[:] = list(map(tuple, attributes))
    
    # 创建 PlyElement 并将其描述为 'vertex'
    el = PlyElement.describe(elements, 'vertex')
    
    # 将 PlyData 写入到指定路径
    PlyData([el]).write(path)

def reset_opacity(self):
    '''
	获取当前透明度张量self.get_opacity。
	将所有透明度值限制在不超过0.01。
	对限制后的透明度值应用反Sigmoid函数。
	将新的透明度张量opacities_new替换到优化器中,并更新对象的透明度属性self._opacity。
	'''
    opacities_new = inverse_sigmoid(torch.min(self.get_opacity, torch.ones_like(self.get_opacity) * 0.01))
    optimizable_tensors = self.replace_tensor_to_optimizer(opacities_new, "opacity")
    self._opacity = optimizable_tensors["opacity"]
def load_ply(self, path):
    # 读取给定路径的PLY文件,并将数据加载到类的属性中。
    # 使用PlyData.read方法读取PLY文件的数据。
    plydata = PlyData.read(path)
	
    # 从PLY文件中提取x, y, z坐标并堆叠成一个二维数组xyz。
    xyz = np.stack((np.asarray(plydata.elements[0]["x"]),
                    np.asarray(plydata.elements[0]["y"]),
                    np.asarray(plydata.elements[0]["z"])), axis=1)
    # 从PLY文件中提取opacity值,并添加一个新轴,以便与其他属性的维度一致。
    opacities = np.asarray(plydata.elements[0]["opacity"])[..., np.newaxis]

    # 创建一个形状为(顶点数, 3, 1)的零数组features_dc。
    # 从PLY文件中提取f_dc_0, f_dc_1, f_dc_2特征,并分别填充到features_dc中。
    features_dc = np.zeros((xyz.shape[0], 3, 1))
    features_dc[:, 0, 0] = np.asarray(plydata.elements[0]["f_dc_0"])
    features_dc[:, 1, 0] = np.asarray(plydata.elements[0]["f_dc_1"])
    features_dc[:, 2, 0] = np.asarray(plydata.elements[0]["f_dc_2"])
	
    # 提取所有以"f_rest_"开头的属性名,并按后缀数字排序。
	# 断言这些属性的数量符合期望。
	# 创建一个形状为(顶点数, 特征数)的零数组features_extra。
	# 从PLY文件中提取相应的特征,并填充到features_extra中。
	# 重新调整数组的形状,以便特征维度与SH系数一致。
    extra_f_names = [p.name for p in plydata.elements[0].properties if p.name.startswith("f_rest_")]
    extra_f_names = sorted(extra_f_names, key=lambda x: int(x.split('_')[-1]))
    assert len(extra_f_names) == 3 * (self.max_sh_degree + 1) ** 2 - 3
    features_extra = np.zeros((xyz.shape[0], len(extra_f_names)))
    for idx, attr_name in enumerate(extra_f_names):
        features_extra[:, idx] = np.asarray(plydata.elements[0][attr_name])
    features_extra = features_extra.reshape((features_extra.shape[0], 3, (self.max_sh_degree + 1) ** 2 - 1))

    # 提取所有以"scale_"和"rot"开头的属性名,并按后缀数字排序。
	# 创建零数组scales和rots,并从PLY文件中提取相应的属性值,填充到数组中。
    scale_names = [p.name for p in plydata.elements[0].properties if p.name.startswith("scale_")]
    scale_names = sorted(scale_names, key=lambda x: int(x.split('_')[-1]))
    scales = np.zeros((xyz.shape[0], len(scale_names)))
    for idx, attr_name in enumerate(scale_names):
        scales[:, idx] = np.asarray(plydata.elements[0][attr_name])

    rot_names = [p.name for p in plydata.elements[0].properties if p.name.startswith("rot")]
    rot_names = sorted(rot_names, key=lambda x: int(x.split('_')[-1]))
    rots = np.zeros((xyz.shape[0], len(rot_names)))
    for idx, attr_name in enumerate(rot_names):
        rots[:, idx] = np.asarray(plydata.elements[0][attr_name])
		
    # 将所有提取的数据转换为PyTorch张量,并将其设置为可优化的参数(requires_grad_(True))。
	# 使用transpose(1, 2)和contiguous()确保张量的内存布局适合后续计算。
    self._xyz = nn.Parameter(torch.tensor(xyz, dtype=torch.float, device="cuda").requires_grad_(True))
    self._features_dc = nn.Parameter(torch.tensor(features_dc, dtype=torch.float, device="cuda").transpose(1, 2).contiguous().requires_grad_(True))
    self._features_rest = nn.Parameter(torch.tensor(features_extra, dtype=torch.float, device="cuda").transpose(1, 2).contiguous().requires_grad_(True))
    self._opacity = nn.Parameter(torch.tensor(opacities, dtype=torch.float, device="cuda").requires_grad_(True))
    self._scaling = nn.Parameter(torch.tensor(scales, dtype=torch.float, device="cuda").requires_grad_(True))
    self._rotation = nn.Parameter(torch.tensor(rots, dtype=torch.float, device="cuda").requires_grad_(True))
	
    # 将active_sh_degree设置为max_sh_degree,表示当前使用的SH度数。
    self.active_sh_degree = self.max_sh_degree
   

接下来的内容是关于自适应密度控制

这些代码同样在scene/gaussian_model中。

def replace_tensor_to_optimizer(self, tensor, name):
    # 将新的张量替换为优化器中的参数,同时保留优化器的状态。这个很重要,后面处理参数都要靠这个方法!!!
    	# 用于存储新的可优化张量。
        optimizable_tensors = {}
        # 优化器的参数组包含了所有要优化的参数和相关设置。
        for group in self.optimizer.param_groups:
            # 检查参数组的名字是否与给定的名字匹配。
            if group["name"] == name:
                # 获取当前参数的状态,包括动量项和二次动量项。
                stored_state = self.optimizer.state.get(group['params'][0], None)
                # 将动量项和二次动量项重置为与新张量相同形状的零张量。
                stored_state["exp_avg"] = torch.zeros_like(tensor)
                stored_state["exp_avg_sq"] = torch.zeros_like(tensor)
				
                # 删除旧参数的状态。
                del self.optimizer.state[group['params'][0]]
                # 将参数组中的第一个参数替换为新的张量,并设置为需要梯度。
                group["params"][0] = nn.Parameter(tensor.requires_grad_(True))
                # 将存储的状态重新设置到新的参数上。
                self.optimizer.state[group['params'][0]] = stored_state
	
    			# 将新的参数添加到字典中。
                optimizable_tensors[group["name"]] = group["params"][0]
         # 返回包含新的可优化张量的字典。
        return optimizable_tensors
'''
后面做自适应密度控制的时候,所用的方法如果需要替换优化器中的参数,都需要使用这个replace_tensor_to_optimizer方法。这个方法可以确保新的参数替换后,优化器的状态(例如动量和二次动量)能够被正确保留,使得训练过程平稳进行。通过这种方式,可以有效地管理和更新优化器中的参数,而不会丢失已有的优化状态。
'''
def _prune_optimizer(self, mask):
    # 通过应用掩码(mask)来选择需要保留的参数,并相应地更新优化器的状态。
    # 初始化优化参数字典
    optimizable_tensors = {}

    # 遍历优化器参数组
    for group in self.optimizer.param_groups:
        # 如果有存储状态(例如动量和二次动量),则更新这些状态,使其只包含被掩码保留的部分。
        stored_state = self.optimizer.state.get(group['params'][0], None)
        if stored_state is not None:
            stored_state["exp_avg"] = stored_state["exp_avg"][mask]
            stored_state["exp_avg_sq"] = stored_state["exp_avg_sq"][mask]

            # 删除旧参数并用掩码后的参数替换,无论是否有存储状态
            del self.optimizer.state[group['params'][0]]
            group["params"][0] = nn.Parameter((group["params"][0][mask].requires_grad_(True)))

            # 如果有存储状态,则将其重新分配到新的参数上
            self.optimizer.state[group['params'][0]] = stored_state

            # 记录更新后的参数
            optimizable_tensors[group["name"]] = group["params"][0]
        else:
            # 如果没有存储状态,仅更新参数组中的参数
            group["params"][0] = nn.Parameter(group["params"][0][mask].requires_grad_(True))
            optimizable_tensors[group["name"]] = group["params"][0]

    # 返回更新后的参数字典
    return optimizable_tensors

 def prune_points(self, mask):
    # 对输入掩码取反,得到有效点的掩码
    valid_points_mask = ~mask

    # 调用 _prune_optimizer 方法,使用有效点的掩码来更新优化器中的参数
    optimizable_tensors = self._prune_optimizer(valid_points_mask)

    # 更新类中的各个属性,使其只包含有效点
    self._xyz = optimizable_tensors["xyz"]
    self._features_dc = optimizable_tensors["f_dc"]
    self._features_rest = optimizable_tensors["f_rest"]
    self._opacity = optimizable_tensors["opacity"]
    self._scaling = optimizable_tensors["scaling"]
    self._rotation = optimizable_tensors["rotation"]

    # 更新 xyz_gradient_accum 以仅包含有效点
    self.xyz_gradient_accum = self.xyz_gradient_accum[valid_points_mask]

    # 更新 denom 和 max_radii2D 以仅包含有效点
    self.denom = self.denom[valid_points_mask]
    self.max_radii2D = self.max_radii2D[valid_points_mask]

def cat_tensors_to_optimizer(self, tensors_dict):
    # 初始化优化参数字典
    optimizable_tensors = {}

    # 遍历优化器参数组
    for group in self.optimizer.param_groups:
        assert len(group["params"]) == 1
        # 从字典中获取要扩展的张量
        extension_tensor = tensors_dict[group["name"]]

        # 获取参数组的存储状态(例如动量和二次动量)
        stored_state = self.optimizer.state.get(group['params'][0], None)
        if stored_state is not None:
            # 扩展动量和二次动量
            stored_state["exp_avg"] = torch.cat((stored_state["exp_avg"], torch.zeros_like(extension_tensor)), dim=0)
            stored_state["exp_avg_sq"] = torch.cat((stored_state["exp_avg_sq"], torch.zeros_like(extension_tensor)), dim=0)

            # 更新参数组中的参数,并将其设置为需要梯度计算的模式
            del self.optimizer.state[group['params'][0]]
            group["params"][0] = nn.Parameter(torch.cat((group["params"][0], extension_tensor), dim=0).requires_grad_(True))
            self.optimizer.state[group['params'][0]] = stored_state

            # 将扩展后的参数存储到优化参数字典中
            optimizable_tensors[group["name"]] = group["params"][0]
        else:
            # 如果没有存储状态,只更新参数组中的参数
            group["params"][0] = nn.Parameter(torch.cat((group["params"][0], extension_tensor), dim=0).requires_grad_(True))
            optimizable_tensors[group["name"]] = group["params"][0]

    return optimizable_tensors

def densification_postfix(self, new_xyz, new_features_dc, new_features_rest, new_opacities, new_scaling, new_rotation):
    # 将新的数据构建成一个字典
    d = {
        "xyz": new_xyz,
        "f_dc": new_features_dc,
        "f_rest": new_features_rest,
        "opacity": new_opacities,
        "scaling": new_scaling,
        "rotation": new_rotation
    }

    # 使用 cat_tensors_to_optimizer 方法,将新的数据添加到优化器中
    optimizable_tensors = self.cat_tensors_to_optimizer(d)

    # 更新实例变量,使其指向新的优化张量
    self._xyz = optimizable_tensors["xyz"]
    self._features_dc = optimizable_tensors["f_dc"]
    self._features_rest = optimizable_tensors["f_rest"]
    self._opacity = optimizable_tensors["opacity"]
    self._scaling = optimizable_tensors["scaling"]
    self._rotation = optimizable_tensors["rotation"]

    # 初始化梯度累积器和相关变量
    self.xyz_gradient_accum = torch.zeros((self.get_xyz.shape[0], 1), device="cuda")
    self.denom = torch.zeros((self.get_xyz.shape[0], 1), device="cuda")
    self.max_radii2D = torch.zeros((self.get_xyz.shape[0]), device="cuda")

def densify_and_split(self, grads, grad_threshold, scene_extent, N=2):
    # 获取初始点数
    n_init_points = self.get_xyz.shape[0]

    # 初始化一个与初始点数相同大小的全零张量,用于存储梯度
    padded_grad = torch.zeros((n_init_points), device="cuda")
    
    # 将输入的梯度值填充到全零张量中
    padded_grad[:grads.shape[0]] = grads.squeeze()
    
    # 选取满足梯度阈值的点,生成布尔掩码
    selected_pts_mask = torch.where(padded_grad >= grad_threshold, True, False)
    
    # 进一步筛选,确保缩放因子大于特定比例的场景范围
    selected_pts_mask = torch.logical_and(selected_pts_mask,
                                          torch.max(self.get_scaling, dim=1).values > self.percent_dense * scene_extent)

    # 获取选中的点的缩放因子,并重复 N 次
    stds = self.get_scaling[selected_pts_mask].repeat(N, 1)
    
    # 初始化均值为零的张量
    means = torch.zeros((stds.size(0), 3), device="cuda")
    
    # 生成符合正态分布的噪声采样
    samples = torch.normal(mean=means, std=stds)
    
    # 构建旋转矩阵,并重复 N 次
    rots = build_rotation(self._rotation[selected_pts_mask]).repeat(N, 1, 1)
    
    # 计算新的点的坐标
    new_xyz = torch.bmm(rots, samples.unsqueeze(-1)).squeeze(-1) + self.get_xyz[selected_pts_mask].repeat(N, 1)
    
    # 计算新的缩放因子,并进行缩放处理
    new_scaling = self.scaling_inverse_activation(self.get_scaling[selected_pts_mask].repeat(N, 1) / (0.8 * N))
    
    # 复制旋转信息
    new_rotation = self._rotation[selected_pts_mask].repeat(N, 1)
    
    # 复制特征信息
    new_features_dc = self._features_dc[selected_pts_mask].repeat(N, 1, 1)
    new_features_rest = self._features_rest[selected_pts_mask].repeat(N, 1, 1)
    
    # 复制透明度信息
    new_opacity = self._opacity[selected_pts_mask].repeat(N, 1)

    # 调用函数,将新生成的数据传入
    self.densification_postfix(new_xyz, new_features_dc, new_features_rest, new_opacity, new_scaling, new_rotation)

    # 构建修剪掩码,将新生成的点也包含在内
    prune_filter = torch.cat((selected_pts_mask, torch.zeros(N * selected_pts_mask.sum(), device="cuda", dtype=bool)))
    
    # 调用修剪函数,移除不需要的点
    self.prune_points(prune_filter)

def densify_and_clone(self, grads, grad_threshold, scene_extent):
    # 根据梯度值和场景范围,选取满足条件的点
    selected_pts_mask = torch.where(torch.norm(grads, dim=-1) >= grad_threshold, True, False)
    selected_pts_mask = torch.logical_and(selected_pts_mask,
                                          torch.max(self.get_scaling, dim=1).values <= self.percent_dense * scene_extent)
    
    # 克隆选中的点
    new_xyz = self._xyz[selected_pts_mask]
    new_features_dc = self._features_dc[selected_pts_mask]
    new_features_rest = self._features_rest[selected_pts_mask]
    new_opacities = self._opacity[selected_pts_mask]
    new_scaling = self._scaling[selected_pts_mask]
    new_rotation = self._rotation[selected_pts_mask]

    # 调用函数,将克隆的数据传入
    self.densification_postfix(new_xyz, new_features_dc, new_features_rest, new_opacities, new_scaling, new_rotation)

    
def densify_and_prune(self, max_grad, min_opacity, extent, max_screen_size):
    # 计算每个点的梯度值
    grads = self.xyz_gradient_accum / self.denom
    grads[grads.isnan()] = 0.0

    # 调用密化和克隆函数
    self.densify_and_clone(grads, max_grad, extent)
    
    # 调用密化和分裂函数
    self.densify_and_split(grads, max_grad, extent)

    # 根据透明度生成修剪掩码
    prune_mask = (self.get_opacity < min_opacity).squeeze()
    
    # 如果指定了最大屏幕尺寸,根据屏幕尺寸生成修剪掩码
    if max_screen_size:
        big_points_vs = self.max_radii2D > max_screen_size
        big_points_ws = self.get_scaling.max(dim=1).values > 0.1 * extent
        prune_mask = torch.logical_or(torch.logical_or(prune_mask, big_points_vs), big_points_ws)
    
    # 调用修剪函数,移除不需要的点
    self.prune_points(prune_mask)

    # 清理 CUDA 缓存
    torch.cuda.empty_cache()
def add_densification_stats(self, viewspace_point_tensor, update_filter):
    # 累积选中点的梯度值
    self.xyz_gradient_accum[update_filter] += torch.norm(viewspace_point_tensor.grad[update_filter, :2], dim=-1, keepdim=True)
    
    # 更新选中点的计数
    self.denom[update_filter] += 1

Camera

以下代码定义在scene/cameras.py中。

class Camera(nn.Module):
    def __init__(self, colmap_id, R, T, FoVx, FoVy, image, gt_alpha_mask, image_name, uid, trans=np.array([0.0, 0.0, 0.0]), scale=1.0, data_device="cuda"):
        """
        初始化 Camera 类

        :param colmap_id: Colmap 中相机的 ID
        :param R: 旋转矩阵
        :param T: 平移向量
        :param FoVx: 水平方向的视场角
        :param FoVy: 垂直方向的视场角
        :param image: 图像数据 (torch.Tensor)
        :param gt_alpha_mask: ground truth alpha mask (torch.Tensor)
        :param image_name: 图像名称
        :param uid: 相机的唯一标识符
        :param trans: 平移向量,默认为 [0.0, 0.0, 0.0]
        :param scale: 缩放因子,默认为 1.0
        :param data_device: 数据存储设备,默认为 "cuda"
        """
        super(Camera, self).__init__()

        # 初始化相机参数
        self.uid = uid 
        self.colmap_id = colmap_id
        self.R = R
        self.T = T
        self.FoVx = FoVx
        self.FoVy = FoVy
        self.image_name = image_name

        # 尝试将数据设备设置为指定的设备
        try:
            self.data_device = torch.device(data_device)
        except Exception as e:
            print(e)
            print(f"[Warning] Custom device {data_device} failed, fallback to default cuda device")
            self.data_device = torch.device("cuda")

        # 将图像数据限制在 [0.0, 1.0] 范围内并移动到指定设备
        self.original_image = image.clamp(0.0, 1.0).to(self.data_device)
        self.image_width = self.original_image.shape[2]
        self.image_height = self.original_image.shape[1]

        # 如果提供了 alpha mask,则将图像数据乘以 alpha mask
        if gt_alpha_mask is not None:
            self.original_image *= gt_alpha_mask.to(self.data_device)
        else:
            self.original_image *= torch.ones((1, self.image_height, self.image_width), device=self.data_device)

        # 设置相机的近平面和远平面
        self.zfar = 100.0
        self.znear = 0.01

        # 设置平移和缩放参数
        self.trans = trans
        self.scale = scale

        # 计算世界坐标到视图坐标的变换矩阵,并将其移动到 GPU
        self.world_view_transform = torch.tensor(getWorld2View2(R, T, trans, scale)).transpose(0, 1).cuda()

        # 计算投影矩阵,并将其移动到 GPU
        self.projection_matrix = getProjectionMatrix(znear=self.znear, zfar=self.zfar, fovX=self.FoVx, fovY=self.FoVy).transpose(0, 1).cuda()

        # 计算完整的投影变换矩阵
        self.full_proj_transform = (self.world_view_transform.unsqueeze(0).bmm(self.projection_matrix.unsqueeze(0))).squeeze(0)

        # 计算相机中心位置
        self.camera_center = self.world_view_transform.inverse()[3, :3]
# 这个不是很重要
class MiniCam:
    def __init__(self, width, height, fovy, fovx, znear, zfar, world_view_transform, full_proj_transform):
        """
        初始化 MiniCam 类

        :param width: 图像宽度
        :param height: 图像高度
        :param fovy: 垂直方向的视场角
        :param fovx: 水平方向的视场角
        :param znear: 近平面
        :param zfar: 远平面
        :param world_view_transform: 世界坐标到视图坐标的变换矩阵
        :param full_proj_transform: 完整的投影变换矩阵
        """
        self.image_width = width
        self.image_height = height
        self.FoVy = fovy
        self.FoVx = fovx
        self.znear = znear
        self.zfar = zfar
        self.world_view_transform = world_view_transform
        self.full_proj_transform = full_proj_transform

        # 计算相机中心位置
        view_inv = torch.inverse(self.world_view_transform)
        self.camera_center = view_inv[3][:3]

其中的转换矩阵

def getWorld2View2(R, t, translate=np.array([.0, .0, .0]), scale=1.0):
    """
    计算从世界坐标系到相机视图坐标系的变换矩阵。

    :param R: 旋转矩阵 (3x3 numpy array)
    :param t: 平移向量 (3-dimensional numpy array)
    :param translate: 额外的平移向量,用于对相机中心进行平移 (默认为 [0.0, 0.0, 0.0])
    :param scale: 缩放因子,用于对相机中心进行缩放 (默认为 1.0)
    :return: 从世界坐标系到相机视图坐标系的变换矩阵 (4x4 numpy array)
    """
    # 初始化一个 4x4 矩阵 Rt,全为零
    Rt = np.zeros((4, 4))
    
    # 将旋转矩阵的转置赋值给 Rt 的左上 3x3 子矩阵
    Rt[:3, :3] = R.transpose()
    
    # 将平移向量赋值给 Rt 的第4列的前3个元素
    Rt[:3, 3] = t
    
    # 将 Rt 的右下角元素赋值为 1.0
    Rt[3, 3] = 1.0

    # 计算相机从相机坐标系到世界坐标系的变换矩阵 C2W
    C2W = np.linalg.inv(Rt)
    
    # 提取相机中心坐标
    cam_center = C2W[:3, 3]
    
    # 对相机中心进行平移和缩放
    cam_center = (cam_center + translate) * scale
    
    # 更新 C2W 的第4列的前3个元素为新的相机中心坐标
    C2W[:3, 3] = cam_center
    
    # 计算从世界坐标系到相机坐标系的变换矩阵 Rt
    Rt = np.linalg.inv(C2W)
    
    # 返回浮点类型的变换矩阵 Rt
    return np.float32(Rt)

其中的投影矩阵

def getProjectionMatrix(znear, zfar, fovX, fovY):
    '''
    投影矩阵用于将三维坐标变换为二维屏幕坐标。这通常涉及将相机坐标系中的点投影到一个称为规范化设备坐标(NDC)系的空间中,然后再映射到屏幕坐标系。
    '''
    # 计算垂直视场角的一半的切线值
    tanHalfFovY = math.tan((fovY / 2))
    # 计算水平视场角的一半的切线值
    tanHalfFovX = math.tan((fovX / 2))

    # 计算近平面的上边界位置
    top = tanHalfFovY * znear
    # 计算近平面的下边界位置
    bottom = -top
    # 计算近平面的右边界位置
    right = tanHalfFovX * znear
    # 计算近平面的左边界位置
    left = -right

    # 初始化一个 4x4 的投影矩阵 P,所有元素初始为零
    P = torch.zeros(4, 4)

    # 控制 z 方向的符号
    z_sign = 1.0

    # 设置投影矩阵中的元素
    P[0, 0] = 2.0 * znear / (right - left)  # x 方向上的缩放因子
    P[1, 1] = 2.0 * znear / (top - bottom)  # y 方向上的缩放因子
    P[0, 2] = (right + left) / (right - left)  # x 方向上的偏移量
    P[1, 2] = (top + bottom) / (top - bottom)  # y 方向上的偏移量
    P[3, 2] = z_sign  # 齐次坐标的 w 分量
    P[2, 2] = z_sign * zfar / (zfar - znear)  # z 方向上的缩放因子
    P[2, 3] = -(zfar * znear) / (zfar - znear)  # z 方向上的偏移量

    # 返回计算好的投影矩阵
    return P

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/762749.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

UE5(c++)开发日志(3):将前面写的输出日志的方法进行封装

Public下新增一个c类&#xff1a; 选择无属性&#xff0c;因为不需要添加任何东西进去, 也不需要借助里面任何东西。 创建一个命名空间Debug&#xff0c;可以在命名空间内写一点静态方法 &#xff1a; namespace Debug{} static void Print(const FString& message, con…

41、web基础和http协议

web基础与http协议 一、web web&#xff1a;就是我们所说得页面&#xff0c;打开网页展示得页面。&#xff08;全球广域网&#xff0c;万维网&#xff09; world wide webwww 分布式图形信息系统 http&#xff1a;超文本传输协议 https&#xff1a;加密的超文本传输协议…

身边的故事(十二):阿文的故事:消失

那以后就再也没有任何阿文的消息。刚开始还打过几次电话&#xff0c;他都没接。后来也就慢慢的淡忘了&#xff0c;为自己的工作生活而奔波&#xff0c;时间冲淡一切。在那几年里&#xff0c;阿文就像消失了一样。直到2021的某一天&#xff0c;电话那端传来了熟悉但是有点陌生的…

Ubuntu DNS服务配置 深度解析

测试方法 resolvectl status dig alidns.com 修改实践 直接用接口配置&#xff0c;没用 /etc/resolv.conf&#xff0c;有效 /etc/netplan/01-network-manager-all.yaml,无效 /etc/systemd/resolved.conf&#xff0c;见link&#xff0c;为全局配置 [Resolve] DNS1.1.1.1 Fa…

基于SpringBoot扶农助农政策管理系统设计和实现(源码+LW+调试文档+讲解等)

&#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN作者、博客专家、全栈领域优质创作者&#xff0c;博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f31f;文末获取源码数据库&#x1f31f; 感兴趣的可以先收藏起来&#xff0c;…

Python容器 之 列表--定义

1.什么是列表呢&#xff1f; 列表(list)是 Python 中使用最频繁的数据类型, 在其他语言中通常叫做数组, 专门用来存储一组数据 列表,list, 使用 [ ] 列表可以存放任意多个数据 列表中可以存放任意类型的数据 列表中数据之间 使用 逗号隔开 2.列表如何定义&#xff1f; &#…

springcloud-config服务器,同样的配置在linux环境下不生效

原本在windows下能争取的获取远程配置但是部署到linux上死活都没有内容&#xff0c;然后开始了远程调试&#xff0c;这里顺带讲解下获取配置文件如果使用的是Git源&#xff0c;config service是如何响应接口并返回配置信息的。先说问题&#xff0c;我的服务名原本是abc-abc-abc…

【数据结构】栈和队列-相互实现OJ题

前言&#xff1a; 本题目是关于栈和队列的OJ题目&#xff0c;需对栈和队列有一定了解再进行做题&#xff0c;若不了解可以根据我之前这篇文章进行学习&#xff1a;【数据结构】栈和队列-CSDN博客,题中需要的栈和队列的实现也在该文章中有源代码 目录 前言&#xff1a; 一.用…

科普文:一文搞懂jvm原理(三)执行引擎

概叙 科普文&#xff1a;一文搞懂jvm(一)jvm概叙-CSDN博客 科普文&#xff1a;一文搞懂jvm原理(二)类加载器-CSDN博客 前面我们介绍了jvm&#xff0c;jvm主要包括两个子系统和两个组件&#xff1a; Class loader(类装载器) 子系统&#xff0c;Execution engine(执行引擎) 子系…

扩展学习|风险评估和风险管理:回顾其基础上的最新进展

文献来源&#xff1a;[1]Aven, T. (2016). Risk assessment and risk management: Review of recent advances on their foundation. European journal of operational research, 253(1), 1-13. 文章简介&#xff1a;大约30-40年前&#xff0c;风险评估和管理被确立为一个科学领…

如何使用 SPM 插件从 Pkl 配置文件生成 Swift 接口

文章目录 前言示例展示 Pkl 配置生成 Swift 绑定手动安装和使用 pkl-gen-swift创建 SPM 命令插件加载 Pkl 配置总结前言 Pkl(全称为 Pickle)是苹果推出的一种全新的专用于配置的编程语言。它允许开发人员通过类型和内置验证安全、直观地设计数据模型。 作为苹果语言,Pkl 有…

小阿轩yx-Nginx 网站服务

小阿轩yx-Nginx 网站服务 由俄罗斯的 lgor Sysoev 开发其稳定、高效的特性逐渐被越来越多的用户认可 Nginx 服务基础 Nginx (发音为[engine x])专为性能优化而开发 最知名的优点 稳定性低系统资源消耗以及对 HTTP 并发连接的高处理能力(单台物理服务器可支持 30000~50000个…

Mysql面试合集

概念 是一个开源的关系型数据库。 数据库事务及其特性 事务&#xff1a;是一系列的数据库操作&#xff0c;是数据库应用的基本逻辑单位。 事务特性&#xff1a; &#xff08;1&#xff09;原子性&#xff1a;即不可分割性&#xff0c;事务要么全部被执行&#xff0c;要么就…

文件操作~

目录 1.为什么使用文件&#xff1f; 2.什么是文件&#xff1f; 2.1 程序文件 2.2 数据文件 2.3 文件名 3.⼆进制文件和文本文件&#xff1f; 4.文件的打开和关闭 4.1 流和标准流 4.1.1 流 4.1.2 标准流 4.2 文件指针 4.3 ⽂件的打开和关闭 5.文件的顺序读写 5.1 …

Optional类方法

Optional类 方法empty()方法of(T value)ofNullable(T value)filter(Predicate<? super T> predicate)get()ifPresent(Consumer<? super T> consumer)isPresent()map(Function<? super T,? extends U> mapper)orElse(T other)orElseGet(Supplier<? ex…

PostgreSQL介绍与安装

一、PostgreSQL数据库介绍 1、什么是数据库&#xff1f; 数据库&#xff08;Database&#xff09;是按照数据结构来组织、存储和管理数据的仓库。每个数据库都有一个或多个不同的 API 用于创建&#xff0c;访问&#xff0c;管理&#xff0c;搜索和复制所保存的数据。 我们也…

论文复现---基于随机蕨的快速相位差DOA估计

本篇文章是博主在通信等领域学习时&#xff0c;用于个人学习、研究或者欣赏使用&#xff0c;并基于博主对通信等领域的一些理解而记录的学习摘录和笔记&#xff0c;若有不当和侵权之处&#xff0c;指出后将会立即改正&#xff0c;还望谅解。文章分类在通信领域笔记&#xff1a;…

#笔记# 写给自己用的小爬虫

最近完成了一个文旅行业信息聚合的小应用&#xff0c;实现仅从一个入口了解全行业的信息动态&#xff0c;不用一个一个翻看各网站&#xff0c;节省了不少检索时间。 一、基本思路 明确数据来源。基于前述目标&#xff0c;确定数据源为文化和旅游部管理部门官网&#xff0c;比…

二维数组-----螺旋性矩阵输出

题目有点难&#xff0c;ok其实是很难。。。 观察样例输出&#xff0c;不难发现&#xff0c;螺旋数组中元素的递增轨迹为&#xff1a;右右右、下下下、左左左、上上上 简明为&#xff1a;右、下、左、上。可以设开始递增的元素1的位置为&#xff08;x&#xff0c;y)&#xff0c…

如何用大模型RAG做医疗问答系统

代码参考 https://github.com/honeyandme/RAGQnASystemhttps://github.com/LongxingTan/open-retrievals TLDR if 疾病症状 in entities and 疾病 not in entities:sql_q "match (a:疾病)-[r:疾病的症状]->(b:疾病症状 {名称:%s}) return a.名称" % (entitie…