Ceres学习笔记应用篇002_使用Ceres求解Powell方程

本文详细介绍了使用Ceres求解器解决Powell方程的方法,包括构建最小二乘问题和求解最小二乘问题的步骤。通过自定义残差计算模型,创建了不同结构的代价函数,如4个独立的1维代价函数或1个4维代价函数,并使用自动微分、数值微分和解析法计算导数。最终展示了各种实现方式下的优化过程和结果,所有实现均得到了接近零的最小值,验证了Ceres求解器的有效性。
摘要由CSDN通过智能技术生成

1 Powell方程

Powell方程如下:

F ( x ) = [ f 1 ( x ) , f 2 ( x ) , f 3 ( x ) , f 4 ( x ) ] F(x)=[f_1(x),f_2(x),f_3(x),f_4(x)] F(x)=[f1(x),f2(x),f3(x),f4(x)] (1)

其中:
{ f 1 ( x ) = x 1 + 10 ∗ x 2 f 2 ( x ) = 5 ∗ ( x 3 − x 4 ) f 3 ( x ) = ( x 2 − 2 ∗ x 3 ) 2 f 4 ( x ) = 10 ∗ ( x 1 − x 4 ) 2 \left\{\begin{matrix} \begin{aligned} &f_1(x)=x_1+10*x_2\\ &f_2(x)=\sqrt{5}*(x_3-x_4)\\ &f_3(x)=(x_2-2*x_3)^2\\ &f_4(x)=\sqrt{10}*(x_1-x_4)^2\\ \end{aligned} \end{matrix}\right. f1(x)=x1+10x2f2(x)=5 (x3x4)f3(x)=(x22x3)2f4(x)=10 (x1x4)2 (2)

Powell方程中,含有4个参数,4个残差项,目标是找到参数块 [ x 1 , x 2 , x 3 , x 4 ] [x_1,x_2,x_3,x_4] [x1,x2,x3,x4],使得下式能够取得最小值(实际上一般是极小值):
1 2 ∗ ∣ ∣ F ( x ) ∣ ∣ 2 = 1 2 ∗ ( ∣ ∣ f 1 ( x ) ∣ ∣ 2 + ∣ ∣ f 2 ( x ) ∣ ∣ 2 + ∣ ∣ f 3 ( x ) ∣ ∣ 2 + ∣ ∣ f 4 ( x ) ∣ ∣ 2 ) \frac{1}{2}*||F(x)||^2=\frac{1}{2}*(||f_1(x)||^2+||f_2(x)||^2+||f_3(x)||^2+||f_4(x)||^2) 21∣∣F(x)2=21(∣∣f1(x)2+∣∣f2(x)2+∣∣f3(x)2+∣∣f4(x)2) (3)

2 Ceres使用基本步骤

Ceres求解过程主要有两大步骤,构建最小二乘问题和求解最小二乘问题,具体步骤如下:
一、 构建最小二乘问题

  1. 用户自定义残差计算模型,可能存在多个;
  2. 构建Ceres代价函数(CostFuntion),将用户自定义残差计算模型添加至CostFuntion,可能存在多个CostFuntion,为每个CostFuntion添加用户自定义残差计算模型,并指定用户自定义残差计算模型的导数计算方法;
  3. 构建Ceres问题(Problem),并在Problem中添加残差块(ResidualBlock),可能存在多个ResidualBlock,为每个ResidualBlock指定CostFuntionLossFuntion以及参数块(ParameterBlock);

二、 求解最小二乘问题

  1. 配置求解器参数Options,即设置Problem求解方法及参数。例如迭代次数、步长等等;
  2. 输出日志内容Summary
  3. 优化求解Solve

构建最小二乘问题时的一些思考:

  1. 目标函数有4个维度,可以拆成4个代价函数,每1个代价函数包含1项残差;也可以合并成1个代价函数,包含4项残差;
  2. 有4个参数,可以拆成4个参数块,每个参数块包含1个参数;也可以合并成1个参数块,包含4个参数;
  3. 计算导数时,可以使用自动微分法、数值微分法、解析解法。

接下来将通过多种方式实现。

3 代码实现

3.1 cost4_x4_auto

  1. 4个代价函数,每个代价函数的输出残差维度为1;
  2. 4个参数块,每个参数块的输入参数维度为1;
  3. 导数计算采用自动微分。

完整代码如下:

#include "ceres/ceres.h"
#include "gflags/gflags.h"
#include "glog/logging.h"

// 用户自定义第1个维度的残差计算模型
// 输入2个参数块,每个参数块的维度为1
// 输出1个残差块,残差块的维度为1
struct F1 
{
    template <typename T>
    bool operator()(const T* const x1, const T* const x2, T* residual) const 
    {
        residual[0] = x1[0] + 10.0 * x2[0];
        return true;
    }
};

// 用户自定义第1个维度的残差计算模型
struct F2 
{
    template <typename T>
    bool operator()(const T* const x3, const T* const x4, T* residual) const 
    {
        residual[0] = sqrt(5.0) * (x3[0] - x4[0]);
        return true;
    }
};

// 用户自定义第1个维度的残差计算模型
struct F3 
{
    template <typename T>
    bool operator()(const T* const x2, const T* const x3, T* residual) const 
    {
        residual[0] = (x2[0] - 2.0 * x3[0]) * (x2[0] - 2.0 * x3[0]);
        return true;
    }
};

// 用户自定义第1个维度的残差计算模型
struct F4 
{
    template <typename T>
    bool operator()(const T* const x1, const T* const x4, T* residual) const 
    {
        residual[0] = sqrt(10.0) * (x1[0] - x4[0]) * (x1[0] - x4[0]);
        return true;
    }
};


int main(int argc, char** argv) {
    GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
    google::InitGoogleLogging(argv[0]);

    // 设置参数初始值,输入4个参数块,每个参数块的维度为1
    double x1 = 3.0;
    double x2 = -1.0;
    double x3 = 0.0;
    double x4 = 1.0;

    // 构建非线性最小二乘问题
    ceres::Problem problem;
    // 添加残差块,需要依次指定代价函数,损失函数,参数块
    // 本例中损失函数为单位函数,每个代价函数需要输入2个参数块
    problem.AddResidualBlock(
        new ceres::AutoDiffCostFunction<F1, 1, 1, 1>(new F1), NULL, &x1, &x2);
    problem.AddResidualBlock(
        new ceres::AutoDiffCostFunction<F2, 1, 1, 1>(new F2), NULL, &x3, &x4);
    problem.AddResidualBlock(
        new ceres::AutoDiffCostFunction<F3, 1, 1, 1>(new F3), NULL, &x2, &x3);
    problem.AddResidualBlock(
        new ceres::AutoDiffCostFunction<F4, 1, 1, 1>(new F4), NULL, &x1, &x4);

    // 配置求解器参数
    ceres::Solver::Options options;
    // 设置最大迭代次数
    options.max_num_iterations = 100;
    // 指定线性求解器来求解问题
    options.linear_solver_type = ceres::DENSE_QR;
    // 输出每次迭代的信息
    options.minimizer_progress_to_stdout = true;

    // 输出日志内容
    ceres::Solver::Summary summary;

    std::cout << "Initial x1 = " << x1
        << ", x2 = " << x2
        << ", x3 = " << x3
        << ", x4 = " << x4
        << "\n";

    // 开始优化求解
    ceres::Solve(options, &problem, &summary);

    // 输出优化过程及结果
    std::cout << summary.FullReport() << "\n";
    std::cout << "Final x1 = " << x1
        << ", x2 = " << x2
        << ", x3 = " << x3
        << ", x4 = " << x4
        << "\n";

    std::system("pause");
    return 0;
}

结果如下:

Initial x1 = 3, x2 = -1, x3 = 0, x4 = 1
iter      cost      cost_change  |gradient|   |step|    tr_ratio  tr_radius  ls_iter  iter_time  total_time
   0  1.075000e+02    0.00e+00    1.55e+02   0.00e+00   0.00e+00  1.00e+04        0    4.77e-04    1.01e-03
   1  5.036190e+00    1.02e+02    2.00e+01   2.16e+00   9.53e-01  3.00e+04        1    8.67e-04    2.00e-03
   2  3.148168e-01    4.72e+00    2.50e+00   6.23e-01   9.37e-01  9.00e+04        1    4.34e-04    2.50e-03
   3  1.967760e-02    2.95e-01    3.13e-01   3.08e-01   9.37e-01  2.70e+05        1    4.35e-04    3.06e-03
   4  1.229900e-03    1.84e-02    3.91e-02   1.54e-01   9.37e-01  8.10e+05        1    4.44e-04    3.56e-03
   5  7.687123e-05    1.15e-03    4.89e-03   7.69e-02   9.37e-01  2.43e+06        1    4.36e-04    4.06e-03
   6  4.804625e-06    7.21e-05    6.11e-04   3.85e-02   9.37e-01  7.29e+06        1    4.54e-04    4.56e-03
   7  3.003028e-07    4.50e-06    7.64e-05   1.92e-02   9.37e-01  2.19e+07        1    4.49e-04    5.07e-03
   8  1.877006e-08    2.82e-07    9.54e-06   9.62e-03   9.37e-01  6.56e+07        1    4.66e-04    5.60e-03
   9  1.173223e-09    1.76e-08    1.19e-06   4.81e-03   9.37e-01  1.97e+08        1    4.58e-04    6.11e-03
  10  7.333425e-11    1.10e-09    1.49e-07   2.40e-03   9.37e-01  5.90e+08        1    4.79e-04    6.65e-03
  11  4.584044e-12    6.88e-11    1.86e-08   1.20e-03   9.37e-01  1.77e+09        1    5.80e-04    7.34e-03
  12  2.865573e-13    4.30e-12    2.33e-09   6.02e-04   9.37e-01  5.31e+09        1    4.97e-04    7.92e-03
  13  1.791438e-14    2.69e-13    2.91e-10   3.01e-04   9.37e-01  1.59e+10        1    4.79e-04    8.46e-03
  14  1.120029e-15    1.68e-14    3.64e-11   1.51e-04   9.37e-01  4.78e+10        1    4.61e-04    8.99e-03

Solver Summary (v 2.0.0-eigen-(3.3.8)-no_lapack-eigensparse-no_openmp)

                                     Original                  Reduced
Parameter blocks                            4                        4
Parameters                                  4                        4
Residual blocks                             4                        4
Residuals                                   4                        4

Minimizer                        TRUST_REGION

Dense linear algebra library            EIGEN
Trust region strategy     LEVENBERG_MARQUARDT

                                        Given                     Used
Linear solver                        DENSE_QR                 DENSE_QR
Threads                                     1                        1
Linear solver ordering              AUTOMATIC                        4

Cost:
Initial                          1.075000e+02
Final                            1.120029e-15
Change                           1.075000e+02

Minimizer iterations                       15
Successful steps                           15
Unsuccessful steps                          0

Time (in seconds):
Preprocessor                         0.000534

  Residual only evaluation           0.000297 (14)
  Jacobian & residual evaluation     0.002385 (15)
  Linear solver                      0.003162 (14)
Minimizer                            0.008568

Postprocessor                        0.000036
Total                                0.009139

Termination:                      CONVERGENCE (Gradient tolerance reached. Gradient max norm: 3.642190e-11 <= 1.000000e-10)
Final x1 = 0.000146222, x2 = -1.46222e-05, x3 = 2.40957e-05, x4 = 2.40957e-05

根据优化结果,当 x 1 = x 2 = x 3 = x 4 = 0 x_1=x_2=x_3=x_4=0 x1=x2=x3=x4=0时,能够得到最小值。

3.2 cost1_x1_auto

  1. 1个代价函数,输出残差维度为4;
  2. 1个参数块,输入参数维度为4;
  3. 导数计算采用自动微分。

完整代码如下:

#include "ceres/ceres.h"
#include "gflags/gflags.h"
#include "glog/logging.h"

// 用户自定义残差计算模型
// 输入1个参数块,参数块维度为4
// 输出1个残差块,残差块的维度为4
struct F1234
{
    template <typename T>
    bool operator()(const T* const x, T* residual) const
    {
        residual[0] = x[0] + 10.0 * x[1];
        residual[1] = sqrt(5.0) * (x[2] - x[3]);
        residual[2] = (x[1] - 2.0 * x[2]) * (x[1] - 2.0 * x[2]);
        residual[3] = sqrt(10.0) * (x[0] - x[3]) * (x[0] - x[3]);
        return true;
    }
};

int main(int argc, char** argv) {
    GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
    google::InitGoogleLogging(argv[0]);

    // 设置参数初始值,输入1个参数块,参数块的维度为4
    double x1234[4] = { 3,-1,0,1 };

    // 构建非线性最小二乘问题
    ceres::Problem problem;
    // 添加残差块,需要依次指定代价函数,损失函数,参数块
    // 本例中损失函数为单位函数,代价函数需要输入1个参数块
    problem.AddResidualBlock(
        new ceres::AutoDiffCostFunction<F1234, 4, 4>(new F1234), NULL, x1234);

    // 配置求解器参数
    ceres::Solver::Options options;
    // 设置最大迭代次数
    options.max_num_iterations = 100;
    // 指定线性求解器来求解问题
    options.linear_solver_type = ceres::DENSE_QR;
    // 输出每次迭代的信息
    options.minimizer_progress_to_stdout = true;

    // 输出日志内容
    ceres::Solver::Summary summary;

    std::cout << "Initial x1 = " << x1234[0]
        << ", x2 = " << x1234[1]
        << ", x3 = " << x1234[2]
        << ", x4 = " << x1234[3]
        << "\n";

    // 开始优化求解
    ceres::Solve(options, &problem, &summary);

    // 输出优化过程及结果
    std::cout << summary.FullReport() << "\n";
    std::cout << "Final x1 = " << x1234[0]
        << ", x2 = " << x1234[1]
        << ", x3 = " << x1234[2]
        << ", x4 = " << x1234[3]
        << "\n";

    std::system("pause");
    return 0;
}

结果与之前的基本一致。

3.3 cost1_x4_auto

  1. 1个代价函数,输出残差维度为4;
  2. 4个参数块,每个参数块的输入参数维度为1;
  3. 导数计算采用自动微分。

完整代码如下:

#include "ceres/ceres.h"
#include "gflags/gflags.h"
#include "glog/logging.h"

// 用户自定义残差计算模型
// 输入1个参数块,参数块维度为4
// 输出1个残差块,残差块的维度为4
struct F1234
{
    template <typename T>
    bool operator()(const T* const x1, const T* const x2, 
        const T* const x3, const T* const x4, T* residual) const
    {
        residual[0] = x1[0] + 10.0 * x2[0];
        residual[1] = sqrt(5.0) * (x3[0] - x4[0]);
        residual[2] = (x2[0] - 2.0 * x3[0]) * (x2[0] - 2.0 * x3[0]);
        residual[3] = sqrt(10.0) * (x1[0] - x4[0]) * (x1[0] - x4[0]);
        return true;
    }
};

int main(int argc, char** argv) {
    GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
    google::InitGoogleLogging(argv[0]);

    // 设置参数初始值,输入4个参数块,每个参数块的维度为1
    double x1 = 3.0;
    double x2 = -1.0;
    double x3 = 0.0;
    double x4 = 1.0;

    // 构建非线性最小二乘问题
    ceres::Problem problem;
    // 添加残差块,需要依次指定代价函数,损失函数,参数块
    // 本例中损失函数为单位函数,代价函数需要输入4个参数块
    problem.AddResidualBlock(
        new ceres::AutoDiffCostFunction<F1234, 4, 1, 1, 1, 1>(new F1234), NULL, &x1, &x2, &x3, &x4);

    // 配置求解器参数
    ceres::Solver::Options options;
    // 设置最大迭代次数
    options.max_num_iterations = 100;
    // 指定线性求解器来求解问题
    options.linear_solver_type = ceres::DENSE_QR;
    // 输出每次迭代的信息
    options.minimizer_progress_to_stdout = true;

    // 输出日志内容
    ceres::Solver::Summary summary;

    std::cout << "Initial x1 = " << x1
        << ", x2 = " << x2
        << ", x3 = " << x3
        << ", x4 = " << x4
        << "\n";

    // 开始优化求解
    ceres::Solve(options, &problem, &summary);

    // 输出优化过程及结果
    std::cout << summary.FullReport() << "\n";
    std::cout << "Final x1 = " << x1
        << ", x2 = " << x2
        << ", x3 = " << x3
        << ", x4 = " << x4
        << "\n";

    std::system("pause");
    return 0;
}

同样,也可以按照cost4_x1_autocost2_x2_auto等多种方式实现,读者可举一反三,自行实现验证,关键是一定要确保ceres::AutoDiffCostFunction<>中,输出残差维度和输入每个参数块维度,要和用户自定义残差计算模型(或代价函数)中,输出残差维度和输入每个参数块维度保持一致(通俗但不严谨的讲,可以理解成x[]residual[]的长度),否则很有可能无法迭代出正确结果。

另外,按照cost1_x1指定输入和输出的维度,分别通过数值法和解析解法求解Powell方程的最小值。

3.4 cost1_x1_numeric

  1. 1个代价函数,输出残差维度为4;
  2. 1个参数块,输入参数维度为4;
  3. 导数计算采用数值微分法。

完整代码如下:

#include "ceres/ceres.h"
#include "gflags/gflags.h"
#include "glog/logging.h"

// 用户自定义残差计算模型
// 输入1个参数块,参数块维度为4
// 输出1个残差块,残差块的维度为4
// 用户自定义残差计算模型
struct F1234
{
    bool operator()(const double* const x, double* residual) const
    {
        residual[0] = x[0] + 10.0 * x[1];
        residual[1] = sqrt(5.0) * (x[2] - x[3]);
        residual[2] = (x[1] - 2.0 * x[2]) * (x[1] - 2.0 * x[2]);
        residual[3] = sqrt(10.0) * (x[0] - x[3]) * (x[0] - x[3]);
        return true;
    }
};

int main(int argc, char** argv) {
    GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
    google::InitGoogleLogging(argv[0]);

    // 设置参数初始值,输入1个参数块,参数块的维度为4
    double x1234[4] = { 3,-1,0,1 };

    // 构建非线性最小二乘问题
    ceres::Problem problem;
    // 添加残差块,需要依次指定代价函数,损失函数,参数块
    // 本例中损失函数为单位函数,代价函数需要输入1个参数块
    // 本例中数值计算导数方法为CENTAL
    problem.AddResidualBlock(
        new ceres::NumericDiffCostFunction<F1234, ceres::CENTRAL, 4, 4>(new F1234), NULL, x1234);

    // 配置求解器参数
    ceres::Solver::Options options;
    // 设置最大迭代次数
    options.max_num_iterations = 100;
    // 指定线性求解器来求解问题
    options.linear_solver_type = ceres::DENSE_QR;
    // 输出每次迭代的信息
    options.minimizer_progress_to_stdout = true;

    // 输出日志内容
    ceres::Solver::Summary summary;

    std::cout << "Initial x1 = " << x1234[0]
        << ", x2 = " << x1234[1]
        << ", x3 = " << x1234[2]
        << ", x4 = " << x1234[3]
        << "\n";

    // 开始优化求解
    ceres::Solve(options, &problem, &summary);

    // 输出优化过程及结果
    std::cout << summary.FullReport() << "\n";
    std::cout << "Final x1 = " << x1234[0]
        << ", x2 = " << x1234[1]
        << ", x3 = " << x1234[2]
        << ", x4 = " << x1234[3]
        << "\n";

    std::system("pause");
    return 0;
}

3.5 cost1_x1_analytic

  1. 1个代价函数,输出残差维度为4;
  2. 1个参数块,输入参数维度为4;
  3. 导数计算采用解析法。

Ceres中,每个雅克比分量表示如下:

jacobian[i][r * parameters_block_size_[i] + c]= ∂ residual [ r ] ∂ parameters [ i ] [ c ] \frac{\partial\text{residual}[r]} {\partial\text{parameters}[i][c]} parameters[i][c]residual[r] (4)

式中, i i i表示参数块索引, c c c表示某个参数块中的参数索引, r r r表示残差的索引。
由此可见,雅克比可以理解成是一个二维的数组,(不同于 C++中的二维数组,因为 C++中的二维数组列数都一样,类似于 std::vector<std::vector<double>>类型),是 double**类型,表示一个数组,数组中的每个元素也是数组,有几个参数块就有几行,每一行对应一个参数块,每行的列数为当前行参数块中参数的数量与残差维度的乘积。
对于本例,只有1个参数块,因此只有 jacobian[0],残差的维度为4,参数块中参数数量为4,因此 jacobian[0]中共有 4*4个分量。

完整代码如下:

#include "ceres/ceres.h"
#include "gflags/gflags.h"
#include "glog/logging.h"

class CostFunction1234
    : public ceres::SizedCostFunction<4, /*输出(resudual)维度大小*/\
    4 /*输入参数块维度大小*/> 
{
public:
    virtual ~CostFunction1234() {}

    // 用户自定义残差计算方法
    virtual bool Evaluate(double const* const* x, /*输入参数块*/\
        double* residuals, /*输出残差*/\
        double** jacobians /*输出雅克比矩阵*/) const
    {
        // 本例中1个参数块中有4个输入参数
        double x1 = x[0][0];
        double x2 = x[0][1];
        double x3 = x[0][2];
        double x4 = x[0][3];

        // 本例中输出残差维度为4
        residuals[0] = x1 + 10.0 * x2;
        residuals[1] = sqrt(5.0) * (x3 - x4);
        residuals[2] = (x2 - 2.0 * x3) * (x2 - 2.0 * x3);
        residuals[3] = sqrt(10.0) * (x1 - x4) * (x1 - x4);

        if (jacobians != NULL && jacobians[0] != NULL) {
            // 第1个维度残差对参数块中的参数依次求偏导
            jacobians[0][0] = 1.0;
            jacobians[0][1] = 10.0;
            jacobians[0][2] = 0.0;
            jacobians[0][3] = 0.0;
            // 第2个维度残差对参数块中的参数依次求偏导
            jacobians[0][4] = 0.0;
            jacobians[0][5] = 0.0;
            jacobians[0][6] = sqrt(5.0);
            jacobians[0][7] = -sqrt(5.0);
            // 第3个维度残差对参数块中的参数依次求偏导
            jacobians[0][8] = 0.0;
            jacobians[0][9] = 2 * (x2 - 2 * x3);
            jacobians[0][10] = -4 * (x2 - 2 * x3);
            jacobians[0][11] = 0.0;
            // 第4个维度残差对参数块中的参数依次求偏导
            jacobians[0][12] = 2 * sqrt(10.0) * (x1 - x4);
            jacobians[0][13] = 0.0;
            jacobians[0][14] = 0.0;
            jacobians[0][15] = -2 * sqrt(10.0) * (x1 - x4);
        }
        return true;
    }
};

int main(int argc, char** argv) {
    GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
    google::InitGoogleLogging(argv[0]);

    // 设置参数初始值,输入1个参数块,参数块的维度为4
    double x1234[4] = { 3,-1,0,1 };

    // 构建非线性最小二乘问题
    ceres::Problem problem;
    // 添加残差块,需要依次指定代价函数,损失函数,参数块
    // 本例中损失函数为单位函数,代价函数需要输入1个参数块
    problem.AddResidualBlock(
        new CostFunction1234, NULL, x1234);

    // 配置求解器参数
    ceres::Solver::Options options;
    // 设置最大迭代次数
    options.max_num_iterations = 100;
    // 指定线性求解器来求解问题
    options.linear_solver_type = ceres::DENSE_QR;
    // 输出每次迭代的信息
    options.minimizer_progress_to_stdout = true;

    // 输出日志内容
    ceres::Solver::Summary summary;

    std::cout << "Initial x1 = " << x1234[0]
        << ", x2 = " << x1234[1]
        << ", x3 = " << x1234[2]
        << ", x4 = " << x1234[3]
        << "\n";

    // 开始优化求解
    ceres::Solve(options, &problem, &summary);

    // 输出优化过程及结果
    std::cout << summary.FullReport() << "\n";
    std::cout << "Final x1 = " << x1234[0]
        << ", x2 = " << x1234[1]
        << ", x3 = " << x1234[2]
        << ", x4 = " << x1234[3]
        << "\n";

    std::system("pause");
    return 0;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值