VoxelMap(LIO)原理介绍

一、VoxelMap

VoxelMap是一种紧耦合融合的LIO里程计,本文档旨在介绍其核心创新点地图维护部分以及公式推导部分;

二、整体方案

激光惯性里程计通常将历史点云信息按照KD-Tree进行维护,前面两节也介绍了KD-Tree和体素图的计算方式,KD-Tree可以实现O(logn)级别的KNN搜索。在搜索到足够的最近点之后,进行平面/线拟合,然后计算点面残差,这其中平面拟合相对还是比较耗时,并且KD-tree的KNN搜索也比较耗时。Voxel map地图更新方案如下图所示
在这里插入图片描述
VoxelMap维护体素地图,每个体素地图内部维护八叉树,深度一般为3,根据实际情况自行调整,八叉树节点为平面信息。具体为:

  • 体素图的key是世界坐标系下的体素ID,value是体素内信息,用八叉树进行表示;八叉树内存着点云的参数化表示,我们这里主要是平面信息,平面包含质心位置q以及法向量n 共6个参数(注:可以优化使用最近点表示,只有3dof,可以降低地图存储),这就意味着根据位置检索到体素图就可以获取到参数化的平面信息。
  • 此外,当体素无法成功拟合平面,需要进行八叉树分割,将大体素分成很多小的体素,这里限制最大深度为3,当然,如果可以拟合平面,就不需要进行分割了。(注:这里也有优化空间,假设体素是3m*3m,那么不同大体素可能维护相同平面,其实可以对多个大体素进行合并,进一步减小地图存储空间,同时可以提高大场景平面精度)
  • 论文中的地图方案相对于KD-tree的方案计算耗时更低,整体耗时可以降低30%,CPU占用和内存占用也会大幅下降(30%和50%)

个人认为VoxelMap核心内容分为两部分,一部分是如何维护自适应体素地图,一部分是其理论推导,尤其是平面法向量置信度推导,本文对这两部分进行详细介绍。

三、地图维护方案

VoxelMap维护的可以理解为一个全局地图,如果不进行地图删减,那么地图当中是包含全局信息的。

1.地图初始化

在地图初次建立时进行调用,涉及体素地图和八叉树地图的初始化。

a) 体素图初始化

其中VoxelLoc是点云投影在体素中的键值,OctoTree指的是当前体素“键”对应的“值”。

算法1-1 体素地图初始化

输入:世界坐标系下的去畸变点云pvList,其他初始化参数
输出:面体素地图PlaneVoxelMap

for all pv 属于 pvList do{
	VoxelLoc = round(pv/maxVoxelSize)
	if(pv是面特征){
		if(VoxelLoc属于PlaneVoxelMap.key)
			将点缓存在对应的OctoTree的根节点TempPoint中
		else
			创建新的Octotree并载入参数
	}else{
		Error:输入异常特征
	}
}
for( all PlaneOctoTree属于PlaneVoxelMap.values){
	调用1.2进行八叉树PlaneOctoTree初始化
}

b) 八叉树初始化

算法1-2 八叉树初始化

输入:八叉树点云类型(面or线,此处只有面),PlaneVoxelMap.values点云
输出:八叉树根节点

if(点数 > maxPlaneUpdateThreshold){
	调用算法4-1进行平面拟合
	if(成功拟合平面){
		达到树底,不调用1-3所示的节点分割
		if(点数 > maxPointsSize){
			updateEnable = False,这个体素不再更新平面
			释放TempPoint的缓存点云
		}
	}else{
		调用算法1-3进行OctoTree分割
		直到成功生成平面or达到maxLayer
	}
}
return RootNode;

c) 八叉树分割算法

当父节点调用算法4-1(或者4-2)拟合平面(线)失败,且父节点尚未到达底层时,需要进行父节点分割。节点分割会将父体素分割成8个子体素,子体素的信息会保存在八叉树的叶子节点LeafNodeArr中,当叶子节点也拟合失败,会递归调用知道到大最大深度。

算法1-3 八叉树分割

输入:根节点RootNode
输出:子节点数组LeafNodeArr[8]

获得RootNode中的点云数据纪委pvList
if(到达底层){
	不再进行节点分割
	return nullptr;
}else{
	for(auto& pv: pvList){
		根据体素中心center以及pv,判断八叉树节点索引i
		载入pv至LeafNodeArr[i]
	}
}
for(auto& LeafNode:LeafNodeArr){
	if(点数 > maxPlaneUpdateThreshold){
		调用4-1进行平面拟合;
		if(拟合成功)
			continue;
		else
			递归调用1-3,对LeafNode再次进行OctoTree分割
	}
}
return LeafNodeArr[8];

2. 地图更新

地图创建成功之后,需要使用新的激光scan对地图点进行更新,使其能表示新区域的信息。

a) 体素地图更新

将点云放到对应的体素,然后调用2-2更新八叉树,所有点都更新之后,就完成了体素地图的更新。

算法2-1 体素图更新

输入:世界坐标系下的去畸变点云pvList,其他初始化参数
输出:面体素地图PlaneVoxelMap

for all pv 属于 pvList do{
	VoxelLoc = round(pv/maxVoxelSize)
	
	if(VoxelLoc属于PlaneVoxelMap.key)
		调用2-2进行OctoTree更新
	else
		创建Octotree并载入参数
	
}
return PlaneVoxelMap;

b)八叉树更新

算法2-2 八叉树更新

输入:单个扫描点pv, 八叉树根节点RootNode
输出:八叉树根节点

pv载入RootNode的tempPoint
if(进行RootNode尚未初始化){
	if(点数 > maxPlaneUpdateThreshold){
		调用算法1-2进行RootNode初始化
	}
}else{
	if(当前节点已成功拟合平面){
		if(可以进行平面更新){
			if(newPointSize > updateSizeThreshold){
				调用算法4-1或者4-2重新拟合平面or直线
				newPointSize = 0	
			}
			if(totalPointSize > maPointsSize){
				不再拟合并释放资源tempPoint
			}
		}
	}else{
		根据体素中心center以及pv,判断八叉树节点索引i
		递归调用2-2,进行LeafNode的更新直到最底层
	}
}

return RootNode;

3.点云配准及残差计算

根据Hash找到当前点云对应体素,如果体素拟合平面成功,返回点到平面距离等观测量和计算的不确定度;如果当前大体素没有拟合平面成功,则需要进入到八叉树每个叶子节点计算一遍残差,最终选择残差最小的返回,如果都没有找到平面,那就在相邻体素中计算。

a) 点云配准

算法3-1 点云配准

输入:世界坐标系下去畸变点云pvList
输出:观测量MatchingList(残差,不确定度)

for(auto& pv: pvList){
	VoxelMap = PlaneVoxelMap
	VoxelLoc = round(pv/maxVoxelSize);
	if(VoxelLoc 属于VoxelMap.key){
		调用算法3-2进行单个观测量MatchingValue计算
		if(MatchingValue计算失败)
			使用最近邻体素,调用算法3-2进行观测量MatchingValue计算
		else
			将MatchingValue并入MatchingList尾部
	}
}
return MatchingList;

b) 单个观测量计算

算法3-2 单个观测量MatchingValue计算

输入:点云在世界坐标系位置pv, 八叉树根节点RootNode
输出:观测量MatchingList(残差,不确定度)

将pv向量化,获取本节点法向量n和质心q
pq = pv -q;
if(当前节点已成功拟合平面){
	计算点到平面的距离d = n*qp
	缓存法向量、质心、距离至MatchingValue
}else{
	进入“所有”LeafNode递归调用算法3-2进行观测量计算
	选择LeafNode中距离d最小的MatchingValue返回
}

return MatchingValue;

4、平面的拟合

前面已经介绍过标准的平面拟合,这里是VoxelMap用的平面拟合方法

算法4-1 平面拟合

输入:点云pointsVec
输出:平面参数

计算质心q
计算点云pointsVec协方差Cov
对Cov进行特征分解L_min, L_middle, L_max,三个特征向量对应点云的正交分解
选取最小特征值对应特征向量作为法向量n
if(L_min < planeThreshold){
	plane = [n q]
}else{
	平面拟合失败
}

return plane;

四、理论推导

1、Local系下点云 L p i ^Lp_i Lpi位置协方差

激光雷达通过扫描电机的编码器测量方向 w w w,通过计算激光飞行时间来测量深度 d d d 。这两部分相互独立,最后相乘得到点云在惯性系下的坐标 L p i = d i ∗ w i ^Lp_i = d_i * w_i Lpi=diwi。因此可以对这他们分开建立误差模型。

  • 测距距离建模相对比较简单。假设 d i d_i di是深度测量值并且深度误差 δ d i ∼ N ( 0 , ∑ d i ) \delta_{d_i}\sim N(0, {\textstyle \sum_{d_i}} ) δdiN(0,di),那么测距真值可通过简单线性相加得到 d i g t = d i + δ d i d_i^{gt}=d_i +\delta_{d_i} digt=di+δdi

  • 测量方向建模涉及S2流形。假设 w i w_i wi是方向测量值并且为 δ w i ∼ N ( 0 , ∑ w i ) \delta_{w_i} \sim N(0,{\textstyle \sum_{w_i}}) δwiN(0,wi)测量量 w i w_i wi在切平面下的方向误差,那么方向真值可以通过SO2上的加法实现,其中 N ( w i ) 3 ∗ 2 N(w_i)_{3*2} N(wi)32 w i w_i wi切平面下的标准正交基。

  • 在这里插入图片描述

  • 在这里插入图片描述
    结合方向和距离测量,可以对Local系下的3D点云的误差进行建模,如下:
    在这里插入图片描述
    据此,可以得到 3D 点云的误差建模模型 δ L p i ∼ N ( 0 , ∑ L p i ) \delta_{^Lp_i} \sim N(0,{\textstyle \sum_{^Lp_i}}) δLpiN(0,Lpi),为深度误差和方向误差线性变换之后的高斯分布。
    在这里插入图片描述

对应代码实现如下:注意,这里方向向量的选择,是选择了N1,然后N1.cross(w)得到N2,此外,这部分协方差随着距离的增加而增加,因此越远的点置信度越低

void calcBodyCov(Eigen::Vector3d &pb, const float range_inc,
                 const float degree_inc, Eigen::Matrix3d &cov) {
  float range = sqrt(pb[0] * pb[0] + pb[1] * pb[1] + pb[2] * pb[2]);
  float range_var = range_inc * range_inc;
  Eigen::Matrix2d direction_var;
  direction_var << pow(sin(DEG2RAD(degree_inc)), 2), 0, 0,
      pow(sin(DEG2RAD(degree_inc)), 2);
  Eigen::Vector3d direction(pb);
  direction.normalize();
  Eigen::Matrix3d direction_hat;
  direction_hat << 0, -direction(2), direction(1), direction(2), 0,
      -direction(0), -direction(1), direction(0), 0;
  Eigen::Vector3d base_vector1(1, 1,
                               -(direction(0) + direction(1)) / direction(2));
  base_vector1.normalize();
  Eigen::Vector3d base_vector2 = base_vector1.cross(direction);
  base_vector2.normalize();
  Eigen::Matrix<double, 3, 2> N;
  N << base_vector1(0), base_vector2(0), base_vector1(1), base_vector2(1),
      base_vector1(2), base_vector2(2);
  Eigen::Matrix<double, 3, 2> A = range * direction_hat * N;
  cov = direction * range_var * direction.transpose() +
        A * direction_var * A.transpose();
};

2、世界系下点云 W p i ^Wp_i Wpi位置协方差

这部分比较简单,转换过程 W p i = L W R i ∗ L p i + L W t i ^Wp_i = ^W_LR_i*^Lp_i + ^W_Lt_i Wpi=LWRiLpi+LWti,这里转换时需要考虑位姿 L W T i ^W_LT_i LWTi的不确定度,计算过程如下:
在这里插入图片描述
因此,世界系下点云的误差 δ w p i ∼ N ( 0 , ∑ w p i ) \delta_{^wp_i} \sim N(0,{\textstyle \sum_{^wp_i}}) δwpiN(0,wpi)为:
在这里插入图片描述
这个不确定度已经和滤波器状态相关了,因此是系统状态不确定度+Lidar测量的总不确定度,因此会比局部坐标系下点的误差更大一些。

3、VoxelMap中拟合平面 [ W n , W q ] [^Wn,^Wq] [Wn,Wq]的协方差

这部分是重点,地图中保存世界系下的平面,通常表示为法向量 n p n_p np和质心 W q p ^Wq_p Wqp,也会保存并更新平面协方差,平面协方差就是估计的n和q的协方差。前面可以看到n和q的计算过程,是对点云最新范围的一簇历史点进行特征分解得到,假设拟合平面使用N个点, W p 1 , W p 2 , . . . . , W p N ^Wp_1,^Wp_2,....,^Wp_N Wp1,Wp2,....,WpN, 平面的法向量 n q n_q nq为点云协方差矩阵最小特征值 λ m \lambda_m λm对应的特征向量 u m \textbf{u}_m um.
显然[n,q]能表示为点云的函数
在这里插入图片描述
一阶泰勒展开:
[ n g t , q g t ] T ≈ [ n , q ] T + ∑ i = 1 N ∂ f ∂ W p i δ w p i [n^{gt},q^{gt}]^T\approx [n,q]^T+\sum_{i=1}^{N}\frac{\partial f}{\partial ^Wp_i}\delta _{^wp_i} [ngt,qgt]T[n,q]T+i=1NWpifδwpi

最终的协方差为:
在这里插入图片描述
下面介绍 ∂ f ∂ W p i \frac{\partial f}{\partial ^Wp_i} Wpif怎么求。

  • 质心 W q ^Wq Wq的协方差计算:质心是所有点云的平均值
    在这里插入图片描述
    可以得到质心的误差服从 δ q ∼ N ( 0 , ∑ q ) \delta_{q} \sim N(0,{\textstyle \sum_{q}}) δqN(0,q)
    为这些3D点covariance的线性组合:
    在这里插入图片描述

  • 特征向量 u m \textbf{u}_m um的协方差计算:平面的法向量 n q n_q nq为点云协方差矩阵最小特征值 λ m \lambda_m λm对应的特征向量,与点云 W p 1 , W p 2 , . . . . , W p N ^Wp_1,^Wp_2,....,^Wp_N Wp1,Wp2,....,WpN之间的关系比较复杂,我们一步一步来进行推导:
    对于点云 W p 1 , W p 2 , . . . . , W p N ^Wp_1,^Wp_2,....,^Wp_N Wp1,Wp2,....,WpN的协方差矩阵A
    在这里插入图片描述
    A有如下特征分解:

在这里插入图片描述
由于A为实对称矩阵,因此特征向量 u 1 \textbf{u}_1 u1 u 2 \textbf{u}_2 u2 u 3 \textbf{u}_3 u3彼此正交,因此 U T U = I U^TU=I UTU=I成立, U T U = I U^TU=I UTU=I两边对 W p i ^Wp_i Wpi的三维坐标 ( x i , y i , z i ) (x_i, y_i,z_i) (xi,yi,zi)中任意 v v v求偏导,则下式成立:
在这里插入图片描述
对于特征分解 Λ = U T A U \Lambda=U^TAU Λ=UTAU两边也对 v v v求偏导,得到:
在这里插入图片描述
为了表示成 v C ^vC vC的方程,将 U Λ = A U U\Lambda=AU UΛ=AU带入,可得:
在这里插入图片描述
带入 v C ^vC vC得到:
![在这里插入图片描述](https://img-blog.csdnimg.cn/4d1fe7bdf0f34b179899b5495fb9fd63.pn

这里 Λ \Lambda Λ是对角矩阵,因此 ∂ Λ ∂ v \frac{\partial \Lambda }{\partial v} vΛ的所有非对角元素为0。 因此上述方程的非对角元素为0。因此下等式恒成立:

在这里插入图片描述
其中 v C m , n ^vC_{m,n} vCm,n v C ^vC vC的m行n列元素。对 v C m , n ^vC_{m,n} vCm,n合并同类项,并结合 v C + v C T = 0 ^vC+^vC^T=0 vC+vCT=0,可得 v C ^vC vC中所有元素的解析表达式:
在这里插入图片描述
结合 V C = U T ∂ U ∂ v ^VC=U^{T} \frac{\partial U}{\partial v} VC=UTvU,则下式成立:
在这里插入图片描述
其中 e k e_k ek代表第k个元素为1其他为0的方向向量。则带入v为 x , y , z {x,y,z} x,y,z的任意值,则:
在这里插入图片描述
为了使上式更加简洁,引入行向量
在这里插入图片描述
∂ u k ∂ p j \frac{\partial \textbf{u}_k}{\partial \textbf{p}_j} pjuk为:

在这里插入图片描述
对于 F m , n p j \textbf{F}_{m,n}^{p_j} Fm,npj,可以近似为:
在这里插入图片描述
对于 ∂ u m T A u n ∂ p j \frac{\partial \textbf{u}_m^T\textbf{A}\textbf{u}_n}{\partial \textbf{p}_j} pjumTAun的求解如下:
在这里插入图片描述

最后,代入 ∂ u m T A u n ∂ p j \frac{\partial \textbf{u}_m^T\textbf{A}\textbf{u}_n}{\partial \textbf{p}_j} pjumTAun F m , n p j \textbf{F}_{m,n}^{p_j} Fm,npj,可以得到 F m , n p j \textbf{F}_{m,n}^{p_j} Fm,npj的解析表达式:
在这里插入图片描述
这样,我们就可以得到特征向量 u \textbf{u} u关于点云 W p i ^Wp_i Wpi的偏导数:
在这里插入图片描述
对平面法向量来说,令 u k \textbf{u}_k uk为最小特征值对应的特征向量 u m \textbf{u}_m um即可。最后平面法向量 u m \textbf{u}_m um的协方差如下:
在这里插入图片描述
注:这里可以很方便的扩展到直线,直线方向向量为最大特征值对应特征向量,替换 u k \textbf{u}_k uk为最大特征向量即可。
此外,如果新增几个点之后,需要重新拟合平面,这部分可以进行增量计算,因此上述计算也只需要增量计算即可,可以避免一些重复计算。
增量更新一个高斯分布:
在这里插入图片描述
均值显而易见,下面简单说下方差推导
在这里插入图片描述
中间两项为零,于是
在这里插入图片描述
合并后:
在这里插入图片描述

对应代码:

// check is plane , calc plane parameters including plane covariance
  void init_plane(const std::vector<pointWithCov> &points, Plane *plane) {
    plane->plane_cov = Eigen::Matrix<double, 6, 6>::Zero();
    plane->covariance = Eigen::Matrix3d::Zero();
    plane->center = Eigen::Vector3d::Zero();
    plane->normal = Eigen::Vector3d::Zero();
    plane->points_size = points.size();
    plane->radius = 0;
    for (auto pv : points) {
      plane->covariance += pv.point * pv.point.transpose();
      plane->center += pv.point;
    }
    plane->center = plane->center / plane->points_size;
    plane->covariance = plane->covariance / plane->points_size -
                        plane->center * plane->center.transpose();
    Eigen::EigenSolver<Eigen::Matrix3d> es(plane->covariance);
    Eigen::Matrix3cd evecs = es.eigenvectors();
    Eigen::Vector3cd evals = es.eigenvalues();
    Eigen::Vector3d evalsReal;
    evalsReal = evals.real();
    Eigen::Matrix3f::Index evalsMin, evalsMax;
    evalsReal.rowwise().sum().minCoeff(&evalsMin);
    evalsReal.rowwise().sum().maxCoeff(&evalsMax);
    int evalsMid = 3 - evalsMin - evalsMax;
    Eigen::Vector3d evecMin = evecs.real().col(evalsMin);
    Eigen::Vector3d evecMid = evecs.real().col(evalsMid);
    Eigen::Vector3d evecMax = evecs.real().col(evalsMax);
    // plane covariance calculation
    Eigen::Matrix3d J_Q;
    J_Q << 1.0 / plane->points_size, 0, 0, 0, 1.0 / plane->points_size, 0, 0, 0,
        1.0 / plane->points_size;
    if (evalsReal(evalsMin) < planer_threshold_) {
      std::vector<int> index(points.size());
      std::vector<Eigen::Matrix<double, 6, 6>> temp_matrix(points.size());
      for (int i = 0; i < points.size(); i++) {
        Eigen::Matrix<double, 6, 3> J;
        Eigen::Matrix3d F;
        for (int m = 0; m < 3; m++) {
          if (m != (int)evalsMin) {
            Eigen::Matrix<double, 1, 3> F_m =
                (points[i].point - plane->center).transpose() /
                ((plane->points_size) * (evalsReal[evalsMin] - evalsReal[m])) *
                (evecs.real().col(m) * evecs.real().col(evalsMin).transpose() +
                 evecs.real().col(evalsMin) * evecs.real().col(m).transpose());
            F.row(m) = F_m;
          } else {
            Eigen::Matrix<double, 1, 3> F_m;
            F_m << 0, 0, 0;
            F.row(m) = F_m;
          }
        }
        J.block<3, 3>(0, 0) = evecs.real() * F;
        J.block<3, 3>(3, 0) = J_Q;
        plane->plane_cov += J * points[i].cov * J.transpose();
      }
}

4、观测更新

a) 平面匹配- 3 σ 3\sigma 3σ校验

给定一个Lidar 3D点 W p i ^Wp_i Wpi(通过位姿先验预测到世界坐标系),我们首先找到其root Voxel(使用coarse地图分辨率,大体素)位于的Hash Kay. 然后所有子体素都有可能是一个有效的匹配,这里假设某个子体素包含平面[n,q],我们可以计算点到平面距离的不确定度,当前scan的点到VoxelMap中获取的平面的距离如下:
在这里插入图片描述
对于 f p f_p fp 进行协方差传递:
在这里插入图片描述
当系统没有误差时有:
在这里插入图片描述

则上式可以简化为:
在这里插入图片描述
则点到平面距离d的方差为:
在这里插入图片描述
这里需要判断点到平面的距离是否< 3 σ 3\sigma 3σ(注:上式写错了结果是 σ 2 \sigma ^2 σ2),满足这个条件的被认为是有效的匹配,如果大体素里面的多个平面都满足 3 σ 3\sigma 3σ准则,那么就选择概率最高的平面进行匹配。如果没有平面满足该准则,把这个点扔掉。

b) 测量更新

点到平面距离观测方程如下:
在这里插入图片描述
测量噪声为:

在这里插入图片描述
其中:
在这里插入图片描述
T k \textbf{T}_k Tk是状态变量,观测矩阵为 J T i \textbf{J}_{\textbf{T}_i} JTi. 知道观测矩阵和观测噪声,然后基于IEKF进行更新即可。

五、优化思路

  • 这篇论文只考虑了面特征,感兴趣的可以对线特征进行类似的推导,原理并不复杂,可以复用面计算的一些结果
  • 目前VoxelMap地图还是存在冗余,如果是一个大的平面,地图中还是会存在多个相同的平面,可以对这些平面进行合并,减小地图尺寸
  • 11
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
您好!要使用Prometheus监控LIO,您需要执行以下步骤: 1. 安装Prometheus:您可以从Prometheus官方网站下载适用于您的操作系统的二进制文件,并按照官方文档中的说明进行安装。 2. 配置Prometheus:在Prometheus的配置文件 `prometheus.yml` 中,添加一个用于监控LIO的目标(target)。例如: ```yaml scrape_configs: - job_name: 'lio' static_configs: - targets: ['localhost:9000'] # 替换为LIO的地址和端口 ``` 3. 重新启动Prometheus:在完成配置后,重新启动Prometheus服务器以使更改生效。 4. 创建LIO监控指标:您需要编写一个用于从LIO获取指标的Exporter。Exporter是一个独立的程序,可以将LIO的指标转换为Prometheus可识别的格式。您可以使用Python或其他编程语言编写Exporter。 5. 注册Exporter:在Prometheus配置文件中,添加一个用于监控LIO的Exporter实例。例如: ```yaml scrape_configs: - job_name: 'lio' static_configs: - targets: ['localhost:9000'] # 替换为LIO的地址和端口 metrics_path: '/metrics' # 替换为Exporter的metrics路径 relabel_configs: - source_labels: [__address__] target_label: instance replacement: lio-exporter ``` 6. 重新启动Prometheus:确保重新启动Prometheus服务器以使更改生效,并开始收集LIO的指标。 现在,您可以使用Prometheus的查询语言(PromQL)来查询和可视化LIO的监控指标。希望这个回答对您有帮助!如果您有任何进一步的问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值