Ceres-Solver

欢迎访问我的博客首页


  ceres-solver 简称 ceres,它和 g2o 都是 SLAM 中常用的非线性优化库。比如,cartographer 使用的是 ceres,ORB-SLAM 使用的是 g2o。ceres 有官方文档

1. 安装 Ceres-Solver


  安装 Ceres-Solver 有点麻烦,因为它的依赖较多。按照顺序依次安装下面的库,选中 BUILD_SHARED_LIBS:

  1. gflags
  2. gtest
  3. glog。依赖 gflags 和 gtest。
  4. lapack。会安装 lapack 和 blas。出现 multiple definition of `_gfortran_concat_string’ 错误时,参考它的 issues/305
  5. suitesparse。该安装包中有一个编译好了的 lapack_windows 文件夹。先点击 cmake-gui 的 configure,出现 LAPACK_DIR,然后把这个目录设置为上面我们自己编译的 lapack,最后删除 lapack_windows 文件夹。会同时安装 amd、btf、camd、ccolamd、colamd、cholmod、cxsparse、klu、ldl、umfpack、spqr、suitesparseconfig。INCLUDE 目录选这些库的 .h 文件所在目录,库文件选择它们的 .a 文件。编译时不要选择 METIS,否则需要先安装 GKlib。
  6. Eigen。在官网下载或从官网去 gitlab 都可以下载。
  7. ceres-solver。按照需要设置上面的依赖路径。我安装的是版本是 2.1.0rc2。

2. 曲线拟合


  使用 Ceres 拟合函数 y = x 2 + 2 x + 3 y = x^2 + 2x + 3 y=x2+2x+3,即,已知若干对 ( x , y ) (x, y) (x,y) 值,求其系数 a , b , c a, b, c a,b,c。参考《视觉 SLAM 十四讲》第 6.3.3 节 《使用 Ceres 拟合曲线》,其源码在 Github

2.1 拟合算法


  使用数组 double abc[3] 存放待估计参数,即一元二次方程的 3 个系数。

#include <ceres/ceres.h>
#include <iostream>
#include <random>
#include <vector>

// 误差类。
struct Function {
	Function(double _x, double _y) : x(_x), y(_y) {}
	template <typename T>
	bool operator()(const T* const abc, T* residual) const {
		residual[0] = T(y) - (abc[0] * T(x) * T(x) + abc[1] * T(x) + abc[2]);
		return true;
	}
	const double x, y;
};

// 使用误差类创建代价函数。
ceres::CostFunction* create(double x, double y) {
	Function* function = new Function(x, y);
	return new ceres::AutoDiffCostFunction<Function, 1, 3>(function);
}

int main(void) {
    // 设置随机数产生方法。
    std::random_device rd;
    std::mt19937 gen(rd());
    std::normal_distribution<double> normal(0.0, 1.0);
    // 1.产生 N 组观测数据,y = x^2 + 2x + c + 噪声。
    const int N = 100;
    std::vector<double> data_x;
    std::vector<double> data_y;
    data_x.reserve(N);
    data_y.reserve(N);
    for (int i = 0; i < N; i++) {
        data_x[i] = i;
        data_y[i] = 1 * i * i + 2 * i + 3 + normal(gen);
    }
    // 2.待优化参数块,随意的初始值。
    double abc[] = {0, 0, 0};
    // double tmp[] = {0, 0, 0};
    // std::vector<double *> abc = {tmp + 0};
    // 3.定义问题,添加残差。
    ceres::Problem problem;
    for (int i = 0; i < N; i++) {
        ceres::CostFunction *cost_function = create(data_x[i], data_y[i]);
        problem.AddResidualBlock(cost_function, nullptr, abc);
    }
    // 4.配置求解器,优化并输出日志。
    ceres::Solver::Options options;
    options.linear_solver_type = ceres::DENSE_NORMAL_CHOLESKY;
    options.minimizer_progress_to_stdout = true;
    ceres::Solver::Summary summary;
    ceres::Solve(options, &problem, &summary);
    std::cout << summary.FullReport() << std::endl;
    // 5.获取优化结果。
    std::cout << abc[0] << ' ' << abc[1] << ' ' << abc[2] << std::endl;
    // std::cout << *(abc[0] + 0) << ' ' << *(abc[0] + 1) << ' ' << *(abc[0] + 2) << std::endl;
    return 0;
}

  代码开头先定义了一个 Function 类,这是一个函数对象,因为它定义了调用运算符。我们把它称为误差类。然后 main 函数的开头定义了噪声的产生方法:使用 C++ 标准库产生高斯噪声,normal 的两个参数分别是均值和标准差。接下来可以分为 5 部分:

  第 1 部分产生观测数据。x 为 0 到 N 的整数, y = a x 2 + b x + c + n o i s e y = a x^2 + b x + c + noise y=ax2+bx+c+noise,其中 a = 1,b=2,c=3,noise 是高斯噪声。

  第 2 部分是待估计的系数 a,b,c,初始值都是 0。

  第 3 部分定义问题和损失函数,添加残差。这部分又分为 3 步:

  1. 创建误差对象,参数是一组观测数据 ( x , y ) (x, y) (x,y)。误差类的类名是随意的,但它必须有一个模板类型的括号运算符重载函数,用于计算残差 r e s i d u a l i = y i − ( a x i 2 + b x i + c ) residual_i = y_i - (a x_i^2 + b x_i + c) residuali=yi(axi2+bxi+c)该重载函数可以有多个参数,但最后一个参数必须是残差

  2. 创建代价函数对象。我们使用 Ceres 的自动求导代价函数 AutoDiffCostFunction,它的模板参数分别是:Function 类,残差的维度(Function 类重载函数的最后一个参数维度),各参数块的维度(Function 类重载函数第 1 到倒数第 2 个参数的维度)。

  3. 添加残差块。参数分别是上一步创建的代价函数,损失函数,待优化参数。

  第 4、5 部分开始优化并获取优化结果。BriefReport 输出简单摘要,FullReport 输出详细摘要。

2.2 参数维度


  现在我们来详细说一下 2.1 第 3 部分的红色文字。在 2.1 中,我们相当于把待估计参数看成 1 个三维数据。现在我们把待估计参数看成是 1 个一维数据和 2 个二维数据。

// 误差类。
struct Function {
	Function(double _x, double _y) : x(_x), y(_y) {}
	template <typename T>
	bool operator()(const T* const a, const T* const bc, T* residual) const {
		residual[0] = T(y) - (a[0] * T(x) * T(x) + bc[0] * T(x) + bc[1]);
		return true;
	}
	const double x, y;
};

// 使用误差类创建代价函数。
ceres::CostFunction* create(double x, double y) {
	Function* function = new Function(x, y);
	return new ceres::AutoDiffCostFunction<Function, 1, 1, 2>(function);
}

int main(void) {
		// ...
		problem.AddResidualBlock(cost_function, nullptr, abc, abc + 1);
		// ...
}

  下面我们用一幅图来理清三部分的关系。紫色直线连接的二个框从上到下分别是指向残差变量的指针、残差的维度,绿色直线连接的三个框从上到下分别是指向第一个待估计参数的指针、第一个待估计参数的维度、第一个待估计参数的首地址,蓝色直线连接的三个框从上到下分别是指向第二个待估计参数的指针、第二个待估计参数的维度、第二个待估计参数的首地址。

请添加图片描述

图 2.1 Ceres 参数维度对应关系

2.3 配置文件


  配置文件 CMakeLists.txt:

cmake_minimum_required(VERSION 3.5.1)
project(demo)

# 1. 查找依赖。
find_package(Ceres REQUIRED)

# 2. 设置包含目录和库目录。
include_directories(
	${CERES_INCLUDE_DIRS}
)

# 3. 生成可执行程序。
add_executable(main
    main.cc
)
target_link_libraries(main
	${CERES_LIBRARIES}
)

3. 使用方法


  Ceres-Solver 有三种求导方式,上面使用最简单的自动求导。

SetParameterBlockConstant
AddParameterBlock
AddResidualBlock

3.1 AddResidualBlock


  该函数的声明如下,它的参数分别是代价函数类指针、损失函数、待优化参数。

ResidualBlockId Problem::AddResidualBlock(CostFunction *cost_function,
                                          LossFunction *loss_function,
                                          const vector<double *> parameter_blocks)

template <typename Ts...>
ResidualBlockId Problem::AddResidualBlock(CostFunction *cost_function,
                                          LossFunction *loss_function,
                                          double *x0,
                                          Ts... xs)

  代价函数类定义了误差的计算方法。代价函数还需要定义误差对优化对象的求导方法,有自动求导、数值求导和解析求导三种可选。

  优化问题的目的是使代价函数值趋向 0,因此代价函数的绝对值与梯度的绝对值呈正相关。为了使代价函数值很大时梯度不至于过分的大,代价函数值很小时梯度不至于过分的小,引入了损失函数。AddResidualBlock 中的损失函数可以是 nullptr,此时代价函数值就是误差向量各项的平方之和。

  待优化参数可以是 vector<double*> 或 double* 类型。

4. 求导


  为 ceres 的代价函数定义误差类时,有自动求导、数值求导和解析求导 3 种求导方式可选。下面我们还用第 2 节拟合曲线的例子,讲述 3 种求导方法的使用。

4.1 自动求导


  第 2 节拟合曲线的例子使用的是自动求导,我们把创建代价函数的代码复制过来。

// 误差类。
struct Function {
	Function(double _x, double _y) : x(_x), y(_y) {}
	template <typename T>
	bool operator()(const T* const abc, T* residual) const {
		residual[0] = T(y) - (abc[0] * T(x) * T(x) + abc[1] * T(x) + abc[2]);
		return true;
	}
	const double x, y;
};

// 使用误差类创建代价函数。
ceres::CostFunction* create(double x, double y) {
	Function* function = new Function(x, y);
	return new ceres::AutoDiffCostFunction<Function, 1, 3>(function);
}

4.2 数值求导


  自动求导需要使用模板,而有时我们无法定义模板类型的误差类,比如计算误差时用到第三方库。此时可以使用数值求导。

// 误差类。
struct Function {
	Function(double _x, double _y) : x(_x), y(_y) {}
	bool operator()(const double* const abc, double* residual) const {
		residual[0] = y - (abc[0] * x * x + abc[1] * x + abc[2]);
		return true;
	}
	const double x, y;
};

// 使用误差类创建代价函数。
ceres::CostFunction* create(double x, double y) {
	Function* function = new Function(x, y);
	return new ceres::NumericDiffCostFunction<Function, ceres::CENTRAL, 1, 3>(function);
}

  可以看出,使用数值求导无需定义模板函数,因此较自动求导更简便。但使用 C++ 模板的自动求导更高效,数值求导却代价高昂,容易出现数值错误,并导致收敛速度较慢。因此应该优先选择自动求导。

4.3 解析求导


  解析求导需要自定义雅可比矩阵。我们的误差 e 是关于优化对象 a, b, c 的函数,假设观测数据是 (x, y),那么

e = f ( a , b , c ) = y − ( a x 2 + b x + c ) , e = f(a, b, c) = y - (ax^2 + bx +c), e=f(a,b,c)=y(ax2+bx+c),

于是雅可比矩阵

J = [ d e d a d e d b d e d c ] = [ − x 2 − x − 1 ] . {\bf J} = \begin{bmatrix} \frac{de}{da} & \frac{de}{db} & \frac{de}{dc} \end{bmatrix} = \begin{bmatrix} -x^2 & -x & -1 \end{bmatrix}. J=[dadedbdedcde]=[x2x1].

据此,我们定义如下代价类。

// 误差类。
struct Function : public ceres::SizedCostFunction<1, 3> {
	Function(double _x, double _y) : x(_x), y(_y) {}
	virtual bool Evaluate(double const* const* parameters, double* residuals, double** jacobians) const {
		residuals[0] = y - (parameters[0][0] * x * x + parameters[0][1] * x + parameters[0][2]);
		if (jacobians != NULL && jacobians[0] != NULL) {
			jacobians[0][0] = -x * x;
			jacobians[0][1] = -x;
			jacobians[0][2] = -1;
		}
		return true;
	}
	virtual ~Function() {}
	const double x, y;
};

// 使用误差类创建代价函数。
ceres::CostFunction* create(double x, double y) {
	Function* function = new Function(x, y);
	return function;
}

  与 ceres::AutoDiffCostFunction 和 ceres::NumericDiffCostFunction 一样,ceres::SizedCostFunction 的模板参数也是残差的维度和各参数块的维度。

  当误差的维度和各参数块的维度已知且固定不变时,我们可以使用 ceres::SizedCostFunction。否则我们可以使用 ceres::CostFunction,通过 set_num_residuals 指定残差的维度,通过 mutable_parameter_block_sizes 指定各参数块的维度。继承 ceres::CostFunction 的代价函数类可以处理多种维度的误差和参数块,灵活性更高。

struct Function : public ceres::CostFunction {
    Function(int num_residuals, std::vector<int32_t> parameter_block_sizes, double _x, double _y)
        : x(_x)
        , y(_y) {
        set_num_residuals(num_residuals);
        // *mutable_parameter_block_sizes() = parameter_block_sizes;
        for (int32_t x : parameter_block_sizes) {
            mutable_parameter_block_sizes()->push_back(x);
        }
    }
    virtual bool Evaluate(double const *const *parameters, double *residuals, double **jacobians) const {
        residuals[0] = y - (parameters[0][0] * x * x + parameters[0][1] * x + parameters[0][2]);
        if (jacobians != NULL && jacobians[0] != NULL) {
            jacobians[0][0] = -x * x;
            jacobians[0][1] = -x;
            jacobians[0][2] = -1;
        }
        return true;
    }
    virtual ~Function() {}
    const double x, y;
};

ceres::CostFunction *create(double x, double y) {
    Function *function = new Function(1, {3}, x, y);
    return function;
}

4.4 求导总结


  自动求导:代价函数须是模板类型。相比非模板类型,模板类型使得求导较高效且健壮。Ceres 推荐我们使用自动求导,因为速度快且准确性高。

  数值求导:代价函数的定义最为简单,但效率和健壮性不如自动求导。因此只有在无法定义模板类型的代价函数时才考虑使用数值求导。

  解析求导:明确定义雅可比矩阵,求导效率高。需要计算雅可比矩阵,因此代价函数的定义变得复杂。因此只有对性能有较高要求时才考虑解析求导。

5. 求解 ICP


  ICP 问题:已知两组三维点,求它们的相对位姿。我们要验证相对位姿是不是

{ R = [ 0 1 0 − 1 0 0 0 0 1 ] t = [ 0 , − 1 , 0 ] . \left\{\begin{aligned} R &= \begin{bmatrix} 0 & 1 & 0 \\ -1 & 0 & 0 \\ 0 & 0 & 1 \end{bmatrix} \\ t &= [0, -1, 0] \end{aligned}\right.. Rt= 010100001 =[0,1,0].

本文的例子参照 A-LOAM。本例中,旋转矩阵用四元数 Eigen::Quaternion 表示,平移向量用矩阵 Eigen::Matrix 表示。

5.1 定义误差类


class Function {
public:
	Function(Eigen::Vector3d _point_a, Eigen::Vector3d _point_b) {
		point_a = _point_a;
		point_b = _point_b;
	}

	template<typename T>
	bool operator()(const T* para_q, const T* para_t, T* residual) const {
		Eigen::Matrix<T, 3, 1> matrix_a{ T(point_a.x()), T(point_a.y()) ,T(point_a.z()) };
		Eigen::Matrix<T, 3, 1> matrix_b{ T(point_b.x()), T(point_b.y()) ,T(point_b.z()) };

		Eigen::Quaternion<T> rotation{ para_q[3],para_q[0], para_q[1], para_q[2] };
		Eigen::Matrix<T, 3, 1> translation{ para_t[0],para_t[1],para_t[2] };

		Eigen::Matrix<T, 3, 1> temp = rotation * matrix_a + translation;
		residual[0] = temp[0] - matrix_b[0];
		residual[1] = temp[1] - matrix_b[1];
		residual[2] = temp[2] - matrix_b[2];
		return true;
	}

private:
	Eigen::Vector3d point_a;
	Eigen::Vector3d point_b;
};

  和第一个拟合函数的例子一样,该误差类的构造函数接收一个观测值 point_a 和一个实际值 point_b。重载函数的参数分别是表示旋转的四元数、平移向量、误差。在重载函数体内,利用观测值 point_a、旋转矩阵、平移向量得到测量值 temp,然后把测量值与实际值的误差赋值给重载函数的第三个参数。

5.2 创建代价函数对象


ceres::CostFunction* create(Eigen::Vector3d point_a, Eigen::Vector3d point_b) {
	Function* function = new Function(point_a, point_b);
	return new ceres::AutoDiffCostFunction<Function, 3, 4, 3>(function);
}

  和第一个拟合函数的例子一样,首先使用 new 操作符创建一个我们刚才定义的误差类的对象。然后用指针作为参数创建 ceres::AutoDiffCostFunction 对象。

5.3 求解 ICP


#include <iostream>
#include <cmath>
#include <iomanip>
#include <Eigen/Core>
#include <Eigen/Eigen>
#include <Eigen/Geometry>
#include <ceres/ceres.h>
using namespace std;

int main() {
	cout << setiosflags(ios::fixed) << setiosflags(ios::right) << setprecision(2);
	// 1. 观测数据。
	vector<Eigen::Vector3d> points_a = { {-4,2,1},{1,2,3},{1,3,2},{2,1,1},{-1,4,2},{7,0,3} };
	vector<Eigen::Vector3d> points_b = { {2,3,1},{2,-2,3},{3,-2,2},{1,-3,1},{4,0,2},{0,-8,3} };
	// 2.待优化的参数。
	double para_q[4] = { 0,0,0,1 };
	double para_t[3] = { 0,0,0 };
	// 3.定义问题和损失函数,添加残差。
	ceres::Problem::Options problem_options;
	ceres::Problem problem(problem_options);
	ceres::LossFunction* loss_function = new ceres::HuberLoss(0.1);
	// 3.1添加优化对象。
	ceres::LocalParameterization* q_parameterization = new ceres::EigenQuaternionParameterization();
	problem.AddParameterBlock(para_q, 4, q_parameterization);
	problem.AddParameterBlock(para_t, 3);
	// 3.2.定义损失函数,添加残差。
	for (size_t i = 0; i < points_a.size(); i++) {
		ceres::CostFunction* cost_function = create(points_a[i], points_b[i]);
		problem.AddResidualBlock(cost_function, loss_function, para_q, para_t);
	}
	// 4.配置求解器,优化并输出日志。
	ceres::Solver::Options options;
	options.linear_solver_type = ceres::DENSE_QR;
	options.max_num_iterations = 10;
	options.minimizer_progress_to_stdout = false;
	ceres::Solver::Summary summary;
	ceres::Solve(options, &problem, &summary);
	cout << summary.BriefReport() << endl;
	// 5.获取优化结果。
	cout << para_q[0] << " " << para_q[1] << " " << para_q[2] << " " << para_q[3] << endl;
	cout << para_t[0] << " " << para_t[1] << " " << para_t[2] << endl;
	// 6.四元数转旋转矩阵。
	Eigen::Quaterniond q(Eigen::Vector4d(para_q[0], para_q[1], para_q[2], para_q[3]));
	Eigen::Matrix3d R = q.toRotationMatrix();
	cout << R << endl;
}

  其中 3.1 部分不是必须的,可以直接删掉。这部分的内容可以查找资料看一下。

6.协方差


ceres::Covariance

7. 参考


  1. Ceres-Solver 下载,github。
  2. Ceres-Solver 安装,CSDN。
  3. Ceres-Solver 编译错误 “(”:“::”右边的非法标记,CSDN。
  4. Ceres-Solver 三种求导,博客园。
  5. Ceres 官方曲线拟合、损失函数的作用,博客园,2021。
  6. Ceres 三种求导方法,古月居,2021。
  7. Ceres求解直接法BA实现自动求导,CSDN,2020。
  8. Ceres Solver 协方差的求解思路,CSDN,2022。
  9. Ceres学习笔记建模篇,CSDN,2022。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 如果您在Ubuntu 18.04系统上安装ceres-solver,您可以使用以下步骤进行安装: 1. 更新您的系统: ``` sudo apt-get update sudo apt-get upgrade ``` 2. 安装必要的软件包: ``` sudo apt-get install libatlas-base-dev libsuitesparse-dev ``` 3. 下载并安装Ceres Solver: ``` wget http://ceres-solver.org/ceres-solver-1.14.0.tar.gz tar xvf ceres-solver-1.14.0.tar.gz cd ceres-solver-1.14.0 mkdir build cd build cmake .. make sudo make install ``` 4. 检查安装是否成功: ``` ceres_version ``` 如果它输出了Ceres Solver的版本号,则说明安装成功。 请注意,在安装过程中,您可能需要先安装一些其他的依赖项,如果您在安装过程中遇到问题,请检查您的系统是否缺少任何必要的软件包。 ### 回答2: Ubuntu18.04是一种广泛使用的开源操作系统,而Ceres-solver则是一种用于最小化非线性代数方程的C ++库,被广泛应用于计算机视觉和机器人领域。在本文中,我们将探讨如何在Ubuntu18.04上安装Ceres-solver。 方法一:通过软件包管理器安装 Ubuntu18.04已经包含了Ceres-solver,因此我们可以通过软件包管理器轻松地安装它。具体步骤如下: 1. 通过终端窗口打开软件包管理器: ``` sudo apt-get update sudo apt-get install libceres-dev ``` 2. 安装后,我们可以检查Ceres-solver是否成功安装。通过终端窗口输入以下命令进行检查: ``` pkg-config --modversion ceres ``` 如果Ceres-solver成功安装,则会显示Ceres-solver的版本号。 方法二:通过源代码安装 如果您想安装最新版本的Ceres-solver,或者如果软件包管理器无法提供所需的版本,则可以通过源代码安装。具体步骤如下: 1. 从Ceres-solver官方网站下载 Ceres-solver的源代码:http://ceres-solver.org/installation.html 2. 下载后,将压缩文件解压缩。在终端窗口中进入解压缩后的文件夹,并运行以下命令: ``` sudo mkdir build cd build sudo cmake .. sudo make sudo make install ``` 3. 安装完成后,我们可以检查Ceres-solver是否成功安装。通过终端窗口输入以下命令进行检查: ``` pkg-config --modversion ceres ``` 如果Ceres-solver成功安装,则会显示Ceres-solver的版本号。 总结: 通过软件包管理器安装Ceres-solver可能更为简单,但这并不意味着源代码安装不是好选择。如果您想安装最新版本的软件,或者 Ubuntu 软件包管理器没有所需软件包。我们希望这篇文章对您有所帮助,让您成功在Ubuntu18.04上安装Ceres-solver。 ### 回答3: ceres-solver是一个广泛应用于计算机视觉、机器人和自动驾驶领域的C++库,它提供了许多优秀的优化算法。在ubuntu18.04中安装ceres-solver非常简单,下面给出步骤: 1. 打开终端,安装必要的依赖库:sudo apt-get install cmake libgoogle-glog-dev libatlas-base-dev 2. 从官方网站下载ceres-solver的源代码包,解压tar.gz包到/home/user/文件夹中(user为当前用户名),并进入解压后的文件夹 3. 在终端中进入解压后的文件夹,先建立build文件夹:`mkdir build`, 然后进入该文件夹:`cd build` 4. 对于64位系统的用户,可以在终端中使用如下命令设置编译器参数:cmake .. -DCMAKE_CXX_FLAGS="-std=c++11"【注:-std=c++11是C++11标准,用于支持新的特性】;对于32位系统的用户,使用cmake .. 就可以了 5. 在终端中输入:make -j4(其中- j 后面的数字是并行编译的数量,按实际情况进行调整),编译源代码,此时电脑会开始编译并生成ceres-solver库,时间较长,请耐心等待。 6. 输入如下命令进行安装:sudo make install 7. 检查是否安装成功,在终端中输入:pkg-config --modversion ceres。若显示版本号,则代表安装成功。 这样,ceres-solver已经成功安装到了ubuntu18.04系统中。此时您可以在自己的程序中使用ceres-solver库,实现自动优化和计算。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值