从“0”到“1”:Eigen库全面指南

一、Eigen 库简介

在 C++ 编程的广阔天地中,线性代数运算占据着举足轻重的地位,而 Eigen 库就如同一位得力助手,为开发者们提供了强大而高效的线性代数计算支持。Eigen 库是一个基于 C++ 的开源模板库,专注于线性代数领域,涵盖了矩阵运算、向量操作、数值求解以及相关算法等丰富功能。

从性能表现来看,Eigen 库堪称卓越。它利用现代 CPU 的 SIMD(单指令多数据)指令集,如 SSE(Streaming SIMD Extensions)、AVX(Advanced Vector Extensions)等,能够让计算机在一个指令周期内处理多个数据,极大地提升了计算速度。例如在进行大规模矩阵乘法时,借助 SIMD 指令集,Eigen 库可以将原本需要多次执行的操作合并为一次,大大缩短了运算时间。同时,它采用表达式模板技术,能够延迟计算,在运行时对复杂表达式进行优化,减少不必要的计算步骤和内存占用。在计算一系列矩阵的加法和乘法组合时,表达式模板可以将这些操作整合为一个更高效的计算流程,避免了中间结果的频繁存储和读取。

Eigen 库的易用性也十分出色。它的 API 设计简洁直观,对于熟悉线性代数基本概念的开发者而言,几乎可以做到 “所见即所得”。以创建一个简单的矩阵为例,只需使用类似Matrix3d A; A << 1, 2, 3, 4, 5, 6, 7, 8, 9;这样的代码,就能轻松构建一个 3x3 的双精度矩阵,如同在纸上书写矩阵元素一样自然。而且,Eigen 库是一个头文件库,这意味着使用者无需进行繁琐的编译过程,也无需担心动态链接的问题,只需将相关头文件包含在项目中,即可立即使用其丰富的功能,大大降低了集成成本。

跨平台特性使得 Eigen 库能够在不同的操作系统和硬件架构上自由驰骋。无论是 Windows、Linux 还是 macOS 系统,亦或是 x86、ARM 等不同的处理器架构,Eigen 库都能稳定运行,展现出强大的兼容性,为开发者们消除了平台差异带来的困扰,使得基于 Eigen 库开发的程序能够在更广泛的环境中部署和应用。

二、Eigen 基础:搭建开发环境

在开启 Eigen 库的探索之旅前,首先要在开发环境中引入这位得力助手。下面将分别介绍在 Windows、Linux 和 macOS 系统下搭建 Eigen 开发环境的详细步骤。

Windows 系统

在 Windows 系统下安装 Eigen 库主要有两种方法。第一种方法是通过压缩文件安装,首先前往 Eigen 官网(https://eigen.tuxfamily.org/index.php?title=Main_Page )下载 zip 格式的安装包,然后在任意位置解压文件。接着,将解压后的 Eigen 文件夹路径添加到 Visual Studio 项目的附加包含目录中。具体操作是在项目属性中,找到 “C/C++” -> “常规” -> “附加包含目录”,添加 Eigen 的解压路径。

第二种方法是借助 VS 的 NuGet 包管理器安装,依次选择 “工具” -> “NuGet 包管理器” -> “管理解决方案的 Nuget 程序包”,在搜索框中输入 “eigen3”,选择合适的版本,勾选项目后点击 “安装” 即可自动完成安装与配置,若后续不再需要,也可通过同样的方式选择卸载。

Linux 系统

对于 Linux 系统,以 Ubuntu 为例,通常使用源码编译安装的方式。先从 Eigen 官网下载最新版本的源码压缩包,假设下载后的文件存放在 “Downloads” 目录下,在终端中执行以下命令:

 

cd ~/Downloads # 切换到下载目录

tar xvf eigen-3.4.0.tar.gz # 解压下载的压缩包,这里的版本号需根据实际下载的版本调整

cd eigen-3.4.0 # 进入解压后的Eigen目录

mkdir build # 创建一个build文件夹用于编译

cd build # 进入build文件夹

cmake.. # 配置编译选项,生成Makefile文件

make # 执行编译操作

sudo make install # 将编译好的Eigen库安装到系统中

安装完成后,Eigen 库的头文件会被安装到系统默认的路径(如/usr/local/include/eigen3),后续在项目中使用时,编译器就能顺利找到这些头文件。

macOS 系统

在 macOS 系统下,借助 Homebrew 这个强大的包管理器可以轻松完成 Eigen 库的安装。首先确保已经安装了 Homebrew,如果未安装,可在终端中执行以下命令进行安装:

 

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

安装好 Homebrew 后,在终端中输入以下命令来安装 Eigen 库:

 

brew install eigen

默认情况下,Eigen 库会被安装到 “/usr/local/Cellar/eigen/ 版本号 /include/eigen3” 路径下。为了方便使用,还可以通过以下命令将其链接到系统文件夹 “/usr/local/include”:

 

brew link --overwrite eigen

这样,在后续的项目开发中,就可以直接引用 Eigen 库的头文件,无需再指定复杂的路径。

三、深入 Eigen 核心:矩阵与向量操作

(一)矩阵与向量的定义

在 Eigen 库中,矩阵和向量的定义都依赖于强大的 Matrix 模板类。Matrix 模板类犹如一个灵活的模具,能够根据不同的参数塑造出各种类型和大小的矩阵与向量。它的模板参数如下:

 

template <typename _Scalar, int _Rows, int _Cols, int _Options = 0, int _MaxRows = _Rows, int _MaxCols = _Cols>

class Matrix;

其中,前三个参数是最为关键的。_Scalar用于指定矩阵或向量元素的数据类型,它可以是double、float、int等常见的数据类型,以满足不同精度和计算需求。例如,在进行高精度的科学计算时,常常会选择double类型;而在对内存和计算速度要求较高,精度要求相对较低的场景中,float类型则更为合适。_Rows和_Cols分别表示矩阵的行数和列数,通过这两个参数,能够精确地定义矩阵的维度。

当_Rows和_Cols被指定为固定的常量时,就得到了静态矩阵。静态矩阵在编译时就确定了大小,这使得编译器能够在编译阶段进行一些优化,提高代码的执行效率。比如Matrix3d表示一个 3x3 的双精度浮点型矩阵,其定义为typedef Matrix<double, 3, 3> Matrix3d; ,在编译阶段,编译器就知道该矩阵的大小是固定的 3x3,从而可以对相关的运算进行针对性的优化,减少运行时的开销。

而当_Rows或_Cols被设置为Eigen::Dynamic时,就创建了动态矩阵。动态矩阵的大小在运行时才确定,这为程序带来了更大的灵活性。例如MatrixXd表示动态大小的双精度浮点型矩阵,其定义为typedef Matrix<double, Dynamic, Dynamic> MatrixXd; ,在实际应用中,当需要处理大小不确定的数据时,动态矩阵就派上了用场。比如在读取图像数据时,图像的尺寸可能因来源不同而各异,此时使用动态矩阵就可以方便地存储和处理这些图像数据。

向量本质上是一种特殊的矩阵,当矩阵的行数或列数为 1 时,它就成为了向量。列向量可以看作是行数为n,列数为 1 的矩阵,而行向量则是行数为 1,列数为n的矩阵。Eigen 库提供了一些 typedef 定义,方便用户创建常见的向量类型,如Vector3d表示一个 3 维的双精度列向量,其定义为typedef Matrix<double, 3, 1> Vector3d; ,RowVector3f表示一个 3 维的单精度行向量,定义为typedef Matrix<float, 1, 3> RowVector3f; 。这些预定义的向量类型使得代码更加简洁易读,在实际编程中,使用Vector3d来表示一个三维空间中的点或向量,比直接使用Matrix<double, 3, 1>更加直观和方便。

(二)初始化与访问

矩阵和向量的初始化方式丰富多样,以满足不同的应用场景。默认构造函数会创建一个指定大小的矩阵或向量,但其中的元素不会被初始化,其值是未定义的。例如:

 

Matrix3d A;

VectorXd v(5);

这里创建了一个 3x3 的双精度矩阵A和一个长度为 5 的动态双精度向量v,但它们的元素值是不确定的,在使用前需要进行初始化。

赋值构造函数则可以在创建矩阵或向量的同时进行初始化。对于向量,可以直接在构造函数中传入元素值,例如:

 

Vector3d v(1.0, 2.0, 3.0);

这就创建了一个三维向量v,其三个元素分别为 1.0、2.0 和 3.0。对于矩阵,也可以通过类似的方式进行初始化,不过需要按照行优先的顺序传入所有元素,例如:

 

Matrix2d B(1.0, 2.0, 3.0, 4.0);

这样就创建了一个 2x2 的矩阵B,其元素依次为 1.0、2.0、3.0 和 4.0。

使用特殊函数也是一种常见的初始化方式。MatrixXd::Zero()函数可以创建一个全零矩阵,例如:

 

MatrixXd C = MatrixXd::Zero(3, 4);

这将创建一个 3 行 4 列的全零矩阵C。MatrixXd::Ones()函数用于创建全 1 矩阵,MatrixXd::Identity()函数用于创建单位矩阵,例如:

 

Matrix2d I = Matrix2d::Identity();

这会创建一个 2x2 的单位矩阵I。MatrixXd::Random()函数则能生成一个随机矩阵,矩阵中的元素是在 [-1, 1] 之间均匀分布的随机数,例如:

 

Matrix3f R = Matrix3f::Random();

这将生成一个 3x3 的随机单精度矩阵R。

在访问矩阵和向量的元素时,可以使用类似于数组下标的方式。对于矩阵A,A(i, j)表示访问第i行第j列的元素,索引从 0 开始。例如:

 

Matrix3d A;

A(0, 0) = 1.0;

A(1, 2) = 3.0;

这里将矩阵A的第一行第一列元素赋值为 1.0,第二行第三列元素赋值为 3.0。对于向量v,v(i)表示访问第i个元素,例如:

 

Vector3d v;

v(1) = 2.0;

这将向量v的第二个元素赋值为 2.0。

需要注意的是,在访问动态矩阵或向量时,要确保索引不超出其大小范围,否则可能会导致程序崩溃或未定义行为。同时,对于一些固定大小的矩阵,Eigen 库可能会进行一些优化,例如将其存储在栈上而不是堆上,以提高访问速度。在实际应用中,要根据矩阵和向量的特点选择合适的访问方式,以确保程序的正确性和高效性。

(三)基本运算

Eigen 库重载了一系列算术运算符,使得矩阵和向量的基本运算变得简洁直观。这些运算符包括加(+)、减(-)、乘(*)、除(/)等,它们遵循线性代数的基本规则。

矩阵与矩阵相加或相减时,要求两个矩阵的大小必须相同,即行数和列数都相等。例如:

 

Matrix2d A << 1, 2, 3, 4;

Matrix2d B << 5, 6, 7, 8;

Matrix2d C = A + B;

这里定义了两个 2x2 的矩阵A和B,并通过+运算符将它们相加,结果存储在矩阵C中。C的元素为对应位置上A和B元素之和,即C的第一行第一列元素为1 + 5 = 6,以此类推。

矩阵与向量的乘法运算有着严格的规则。当矩阵A与向量v相乘时,矩阵A的列数必须与向量v的行数相等。例如,一个m x n的矩阵A可以与一个n x 1的列向量v相乘,结果是一个m x 1的列向量。代码示例如下:

 

Matrix3d A << 1, 2, 3, 4, 5, 6, 7, 8, 9;

Vector3d v << 1, 2, 3;

Vector3d w = A * v;

在这个例子中,3x3 的矩阵A与 3x1 的向量v相乘,得到一个 3x1 的向量w。w的每个元素是矩阵A对应行与向量v的点积,例如w(0) = 1*1 + 2*2 + 3*3 = 14。

矩阵与矩阵相乘时,前一个矩阵的列数必须与后一个矩阵的行数相等。例如,一个m x n的矩阵A可以与一个n x p的矩阵B相乘,结果是一个m x p的矩阵。示例代码如下:

 

Matrix2d A << 1, 2, 3, 4;

Matrix2d B << 5, 6, 7, 8;

Matrix2d C = A * B;

这里 2x2 的矩阵A与 2x2 的矩阵B相乘,得到 2x2 的矩阵C。C的元素计算遵循矩阵乘法规则,例如C(0, 0) = 1*5 + 2*7 = 19。

在进行这些运算时,Eigen 库会自动进行类型检查,确保参与运算的矩阵和向量类型兼容。如果类型不匹配,编译器会报错。例如,不能直接将一个整数矩阵与一个浮点数矩阵相加,除非进行显式的类型转换。Eigen 库还支持标量与矩阵或向量的乘法和除法运算,标量会与矩阵或向量的每个元素进行相应的运算。例如:

 

Matrix2d A << 1, 2, 3, 4;

Matrix2d B = A * 2.0;

这里将矩阵A的每个元素都乘以 2.0,得到矩阵B。这些基本运算的重载,使得 Eigen 库在处理线性代数计算时,代码简洁明了,易于理解和编写。

(四)高级运算

除了基本运算,Eigen 库还提供了丰富的高级运算功能,以满足更复杂的数学计算需求。

矩阵的转置操作是将矩阵的行和列进行互换。在 Eigen 库中,可以使用transpose()函数来实现矩阵的转置。例如:

 

Matrix3d A << 1, 2, 3, 4, 5, 6, 7, 8, 9;

Matrix3d B = A.transpose();

这里将矩阵A进行转置,得到矩阵B。转置后的矩阵B中,B(i, j) = A(j, i),例如B(0, 1) = A(1, 0) = 4。

共轭操作主要用于复数矩阵,它会对矩阵中的每个复数元素取共轭。在 Eigen 库中,使用conjugate()函数进行共轭操作。例如对于一个复数矩阵C:

 

MatrixXcd C = MatrixXcd::Random(2, 2);

MatrixXcd D = C.conjugate();

这里MatrixXcd表示动态大小的复数矩阵,C中的每个复数元素的实部不变,虚部取相反数,得到共轭矩阵D。

伴随矩阵是共轭转置矩阵,对于实矩阵,伴随矩阵就是转置矩阵。在 Eigen 库中,使用adjoint()函数获取伴随矩阵。例如:

 

Matrix3d A << 1, 2, 3, 4, 5, 6, 7, 8, 9;

Matrix3d B = A.adjoint();

由于A是实矩阵,所以B就是A的转置矩阵。

求逆操作是计算矩阵的逆矩阵,只有方阵才可以求逆。在 Eigen 库中,可以使用inverse()函数求逆。例如:

 

Matrix2d A << 1, 2, 3, 4;

Matrix2d B = A.inverse();

这里计算矩阵A的逆矩阵B,满足A * B = B * A = I,其中I是单位矩阵。

行列式计算用于求解方阵的行列式值,在 Eigen 库中,使用determinant()函数。例如:

 

Matrix2d A << 1, 2, 3, 4;

double det = A.determinant();

这里计算矩阵A的行列式值,并将结果存储在det变量中。

求解矩阵的特征值和特征向量是线性代数中的重要操作。在 Eigen 库中,可以使用SelfAdjointEigenSolver类来求解实对称矩阵的特征值和特征向量,使用EigenSolver类来求解一般矩阵的特征值和特征向量。例如,对于一个实对称矩阵A:

 

Matrix3d A << 1, 2, 3, 2, 4, 5, 3, 5, 6;

SelfAdjointEigenSolver<Matrix3d> eigensolver(A);

if (eigensolver.info() == Success) {

Vector3d eigenvalues = eigensolver.eigenvalues();

Matrix3d eigenvectors = eigensolver.eigenvectors();

}

这里通过SelfAdjointEigenSolver类求解矩阵A的特征值和特征向量,eigenvalues存储特征值,eigenvectors的每一列存储对应的特征向量。特征值和特征向量在许多领域都有广泛应用,如在数据分析中,主成分分析(PCA)就是基于矩阵的特征值和特征向量来实现数据降维;在物理中,求解振动系统的频率和模态也会用到这些概念。这些高级运算功能使得 Eigen 库成为处理复杂线性代数问题的有力工具。

四、Eigen 进阶:特殊矩阵与功能

(一)特殊矩阵类型

在 Eigen 库中,除了常见的普通矩阵,还提供了多种特殊矩阵类型,每种类型都有其独特的定义、特性以及在 Eigen 库中的表示和操作方法。

对角矩阵是一种除了主对角线上的元素外,其余元素均为零的矩阵。在数学表示上,一个\(n \times n\)的对角矩阵\(D\)可以表示为:\( D = \begin{pmatrix} d_{11} & 0 & \cdots & 0 \\ 0 & d_{22} & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \cdots & d_{nn} \end{pmatrix} \)

在 Eigen 库中,可以使用MatrixXd::Diagonal()函数来创建对角矩阵。例如,创建一个 3x3 的对角矩阵,对角元素分别为 1、2、3:

 

#include <Eigen/Dense>

#include <iostream>

int main() {

Eigen::VectorXd diag_elements(3);

diag_elements << 1, 2, 3;

Eigen::MatrixXd D = Eigen::MatrixXd::Diagonal(3, 3);

D.diagonal() = diag_elements;

std::cout << "对角矩阵 D:\n" << D << std::endl;

return 0;

}

对角矩阵在实际应用中常用于简化矩阵运算,例如矩阵乘法,当一个矩阵与对角矩阵相乘时,运算可以简化为矩阵元素与对角元素的对应相乘,大大提高了计算效率。在图像处理中,对角矩阵可以用于图像的缩放操作,对角元素表示各个方向上的缩放因子。

三角矩阵分为上三角矩阵和下三角矩阵。上三角矩阵是指主对角线以下(不包括主对角线)的元素都为零的矩阵,数学表示为:\( U = \begin{pmatrix} u_{11} & u_{12} & \cdots & u_{1n} \\ 0 & u_{22} & \cdots & u_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \cdots & u_{nn} \end{pmatrix} \)

下三角矩阵则是主对角线以上(不包括主对角线)的元素都为零的矩阵。在 Eigen 库中,可以通过triangularView()函数来获取矩阵的三角视图。例如,获取一个矩阵的上三角视图:

 

#include <Eigen/Dense>

#include <iostream>

int main() {

Eigen::MatrixXd A = Eigen::MatrixXd::Random(3, 3);

Eigen::MatrixXd U = A.triangularView<Eigen::Upper>();

std::cout << "上三角矩阵 U:\n" << U << std::endl;

return 0;

}

三角矩阵在求解线性方程组时具有重要作用,通过将矩阵分解为三角矩阵的形式,可以使用回代法高效地求解方程组。在数值分析中,LU 分解就是将一个矩阵分解为一个下三角矩阵和一个上三角矩阵的乘积,从而简化线性方程组的求解过程。

对称矩阵是指矩阵关于主对角线对称,即\(A_{ij} = A_{ji}\),对于所有的\(i\)和\(j\)。在 Eigen 库中,可以使用selfadjointView()函数来处理对称矩阵。例如:

 

#include <Eigen/Dense>

#include <iostream>

int main() {

Eigen::MatrixXd A = Eigen::MatrixXd::Random(3, 3);

Eigen::MatrixXd symmetric_A = A.selfadjointView<Eigen::Lower>();

std::cout << "对称矩阵 symmetric_A:\n" << symmetric_A << std::endl;

return 0;

}

对称矩阵在许多领域都有广泛应用,如在物理学中,描述物体的惯性张量就是一个对称矩阵;在机器学习的主成分分析(PCA)算法中,协方差矩阵也是对称矩阵,通过对协方差矩阵进行特征分解,可以提取数据的主要特征,实现数据降维。

(二)稀疏矩阵处理

稀疏矩阵是指矩阵中大部分元素为零的矩阵。在实际应用中,如在大规模线性方程组求解、图论、信号处理等领域,经常会遇到稀疏矩阵。以图论中的邻接矩阵为例,对于一个大规模的稀疏图,其邻接矩阵中大部分元素为零,只有表示存在边的位置元素为非零值;在信号处理中,某些信号的特征矩阵也可能呈现稀疏性。

Eigen 库中,稀疏矩阵的存储格式采用压缩列存储(Compressed Sparse Column,CSC)或压缩行存储(Compressed Sparse Row,CSR)。以压缩列存储为例,它主要包含三个数组:Values数组存储非零元素的值;InnerIndices数组存储非零元素在其所在列(或行)中的索引;OuterStarts数组存储每一列(或行)的第一个非零元素在Values和InnerIndices数组中的起始索引。这种存储方式大大节省了存储空间,提高了存储效率。

在 Eigen 库中,使用SparseMatrix类来表示稀疏矩阵。下面是一个创建稀疏矩阵并进行简单操作的示例:

 

#include <Eigen/Sparse>

#include <iostream>

#include <vector>

int main() {

// 定义稀疏矩阵的大小

int rows = 3, cols = 3;

// 使用三元组来存储非零元素

std::vector<Eigen::Triplet<double>> triplets;

// 添加非零元素 (i, j, value)

triplets.emplace_back(0, 0, 1.0);

triplets.emplace_back(1, 1, 2.0);

triplets.emplace_back(2, 2, 3.0);

// 创建稀疏矩阵

Eigen::SparseMatrix<double> A(rows, cols);

A.setFromTriplets(triplets.begin(), triplets.end());

// 输出稀疏矩阵

std::cout << "稀疏矩阵 A:\n" << A << std::endl;

return 0;

}

在这个示例中,首先定义了稀疏矩阵的大小,然后使用std::vector<Eigen::Triplet<double>>来存储非零元素的三元组(行索引,列索引,值),接着通过setFromTriplets()函数将这些三元组赋值给稀疏矩阵A,最后输出稀疏矩阵。

Eigen 库还提供了丰富的操作函数来处理稀疏矩阵。例如,可以进行稀疏矩阵的加法、减法、乘法等基本运算。在进行稀疏矩阵乘法时,Eigen 库会利用稀疏矩阵的特性,跳过零元素的计算,从而大大提高计算效率。在求解线性方程组\(Ax = b\)时,如果系数矩阵\(A\)是稀疏矩阵,可以使用 Eigen 库提供的稀疏求解器,如SimplicialCholesky、SparseLU等,这些求解器针对稀疏矩阵进行了优化,能够快速有效地求解方程组。

(三)几何变换

在三维空间中,几何变换是对物体的位置、方向和形状进行改变的操作,Eigen 库提供了一系列强大的类和函数来实现这些变换,使得在计算机图形学、机器人学、计算机视觉等领域中处理几何变换变得更加便捷高效。

旋转矩阵是描述物体旋转的重要工具,它是一个正交矩阵,其行列式的值为 1。在三维空间中,绕\(x\)轴旋转\(\theta\)角度的旋转矩阵\(R_x\)可以表示为:\( R_x = \begin{pmatrix} 1 & 0 & 0 \\ 0 & \cos\theta & -\sin\theta \\ 0 & \sin\theta & \cos\theta \end{pmatrix} \)

绕\(y\)轴和\(z\)轴的旋转矩阵\(R_y\)和\(R_z\)也有类似的形式。在 Eigen 库中,可以使用AngleAxis类来创建旋转矩阵。例如,创建一个绕\(z\)轴旋转\(\frac{\pi}{2}\)的旋转矩阵:

 

#include <Eigen/Geometry>

#include <iostream>

int main() {

double angle = M_PI / 2;

Eigen::AngleAxisd rotation_z(angle, Eigen::Vector3d::UnitZ());

Eigen::Matrix3d R = rotation_z.toRotationMatrix();

std::cout << "绕z轴旋转的旋转矩阵 R:\n" << R << std::endl;

return 0;

}

在计算机图形学中,旋转矩阵常用于对三维模型进行旋转操作,实现模型的不同视角展示;在机器人学中,旋转矩阵可以描述机器人关节的旋转,从而确定机器人末端执行器的姿态。

平移向量用于描述物体在空间中的位置移动,在三维空间中,一个平移向量\(\vec{t} = (t_x, t_y, t_z)\)表示物体在\(x\)、\(y\)、\(z\)方向上的位移。在 Eigen 库中,使用Translation类来表示平移向量。例如,创建一个沿\(x\)轴平移 1 个单位,\(y\)轴平移 2 个单位,\(z\)轴平移 3 个单位的平移向量:

 

#include <Eigen/Geometry>

#include <iostream>

int main() {

Eigen::Translation3d translation(1, 2, 3);

Eigen::Matrix4d T = translation.matrix();

std::cout << "平移矩阵 T:\n" << T << std::endl;

return 0;

}

这里得到的T是一个 4x4 的齐次变换矩阵,它将平移向量与单位矩阵相结合,方便在统一的框架下进行几何变换的组合。在实际应用中,平移向量常用于移动机器人的位置、调整物体在场景中的位置等。

为了实现更复杂的几何变换,通常需要将旋转矩阵和平移向量组合起来。在 Eigen 库中,可以使用Affine3d类来表示三维空间中的仿射变换,它包含了旋转和平移两种变换。例如,先进行旋转,再进行平移:

 

#include <Eigen/Geometry>

#include <iostream>

int main() {

// 旋转

double angle = M_PI / 2;

Eigen::AngleAxisd rotation_z(angle, Eigen::Vector3d::UnitZ());

// 平移

Eigen::Translation3d translation(1, 2, 3);

// 组合仿射变换

Eigen::Affine3d transform = translation * rotation_z;

Eigen::Vector3d point(1, 0, 0);

Eigen::Vector3d transformed_point = transform * point;

std::cout << "变换后的点 transformed_point:\n" << transformed_point << std::endl;

return 0;

}

在这个例子中,首先定义了一个绕\(z\)轴的旋转和一个平移,然后通过乘法将它们组合成一个仿射变换transform,最后将一个点point应用这个变换,得到变换后的点transformed_point。这种组合变换在机器人运动规划中非常常见,机器人需要同时进行旋转和移动来到达目标位置;在计算机图形学中,也常用于对场景中的物体进行复杂的变换操作。

(四)核心库手册

Eigen 库的核心库手册是深入了解和使用 Eigen 库的重要资源,它全面涵盖了 Eigen 库的各个方面,为开发者提供了详细的参考和指导。

手册中对矩阵和向量的操作进行了详尽的说明。对于矩阵和向量的定义,手册中详细介绍了Matrix模板类的各种参数设置,以及如何根据不同的需求创建静态矩阵和动态矩阵,还列举了各种预定义的矩阵和向量类型,如Matrix3d、VectorXd等,方便开发者快速选择合适的类型。在初始化和访问方面,手册详细讲解了多种初始化方式,包括默认构造函数、赋值构造函数、特殊函数初始化等,以及使用下标访问元素的方法和注意事项。对于基本运算和高级运算,手册不仅介绍了各种运算符的重载和使用方法,还给出了详细的数学原理和代码示例,帮助开发者理解运算的本质和实现方式。

在特殊矩阵类型和稀疏矩阵处理方面,手册详细阐述了对角矩阵、三角矩阵、对称矩阵等特殊矩阵的特性和在 Eigen 库中的表示方法,以及如何进行相关的操作。对于稀疏矩阵,手册介绍了其存储格式、创建方法和各种操作函数,还提供了一些优化技巧和注意事项,以帮助开发者在处理大规模稀疏矩阵时提高效率。

关于几何变换,手册详细介绍了旋转矩阵、平移向量、仿射变换等相关的类和函数,包括它们的定义、构造方法和使用场景,还给出了大量的代码示例,展示如何在三维空间中实现各种复杂的几何变换。

此外,手册还包含了一些高级主题,如表达式模板技术、与其他库的集成等内容。表达式模板技术是 Eigen 库实现高效计算的关键技术之一,手册中对其原理和应用进行了深入的讲解,帮助开发者更好地理解 Eigen 库的内部机制,从而编写更高效的代码。在与其他库的集成方面,手册介绍了 Eigen 库与一些常见库(如 OpenCV、Boost 等)的接口和使用方法,方便开发者在不同的项目中综合运用多个库的功能。

Eigen 库的核心库手册是一本内容丰富、详尽实用的参考资料,无论是初学者还是有经验的开发者,都能从手册中获取到所需的信息,从而更好地使用 Eigen 库进行线性代数计算和相关应用开发。

五、Eigen 实战:案例分析

(一)机器学习中的应用

在机器学习领域,Eigen 库凭借其强大的线性代数运算能力,为众多算法的实现提供了坚实的支持,其中线性回归和主成分分析是两个典型的应用场景。

线性回归是一种基本的机器学习算法,旨在通过建立一个线性模型来预测连续型变量。其核心目标是找到一组最优的权重参数,使得模型的预测值与真实值之间的误差最小化。以多变量线性回归为例,假设我们有\(n\)个样本,每个样本包含\(m\)个特征,以及对应的目标值\(y\)。我们可以将特征矩阵表示为\(X\),它是一个\(n \times m\)的矩阵,权重向量表示为\(w\),是一个\(m \times 1\)的向量,目标值向量为\(y\),是一个\(n \times 1\)的向量。那么线性回归模型可以表示为\(\hat{y} = Xw\),其中\(\hat{y}\)是预测值向量。

在使用 Eigen 库实现线性回归时,首先需要定义和初始化相关的矩阵和向量。利用 Eigen 库的矩阵和向量定义方式,我们可以轻松创建这些数据结构。例如:

 

#include <Eigen/Dense>

#include <iostream>

int main() {

// 假设我们有5个样本,每个样本有3个特征

Eigen::MatrixXd X(5, 3);

X << 1, 2, 3,

4, 5, 6,

7, 8, 9,

10, 11, 12,

13, 14, 15;

// 对应的目标值

Eigen::VectorXd y(5);

y << 2, 4, 6, 8, 10;

// 初始化权重向量

Eigen::VectorXd w = (X.transpose() * X).inverse() * X.transpose() * y;

std::cout << "计算得到的权重向量 w: " << w << std::endl;

return 0;

}

在这段代码中,我们首先创建了特征矩阵\(X\)和目标值向量\(y\),然后利用 Eigen 库的矩阵运算功能,通过公式\(w = (X^T X)^{-1} X^T y\)计算出权重向量\(w\)。这里,X.transpose()用于计算矩阵\(X\)的转置,(X.transpose() * X).inverse()计算矩阵\((X^T X)\)的逆矩阵,最后通过矩阵乘法得到权重向量\(w\)。

主成分分析(PCA)是一种常用的数据降维技术,它通过线性变换将原始数据转换为一组线性不相关的变量,即主成分。这些主成分按照方差从大到小排列,方差越大表示该主成分包含的信息越多。在实际应用中,通常只保留前几个方差较大的主成分,从而在保留数据主要特征的同时降低数据的维度。

使用 Eigen 库实现 PCA 的过程如下:首先对数据进行标准化处理,使每个特征的均值为 0,方差为 1。然后计算数据的协方差矩阵,协方差矩阵反映了各个特征之间的相关性。接着,利用 Eigen 库的特征值分解函数求解协方差矩阵的特征值和特征向量。最后,根据特征值的大小对特征向量进行排序,选取前\(k\)个特征向量组成变换矩阵,将原始数据投影到这个变换矩阵上,得到降维后的数据。

以下是一个简单的使用 Eigen 库实现 PCA 的示例代码:

 

#include <Eigen/Dense>

#include <iostream>

void pca(Eigen::MatrixXd& data, int k) {

// 数据标准化

Eigen::VectorXd mean = data.colwise().mean();

data = data.rowwise() - mean.transpose();

// 计算协方差矩阵

Eigen::MatrixXd covariance = data.adjoint() * data / (data.rows() - 1);

// 求解协方差矩阵的特征值和特征向量

Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> eigensolver(covariance);

if (eigensolver.info()!= Eigen::Success) {

throw std::runtime_error("Eigen decomposition failed");

}

// 按特征值从大到小排序特征向量

Eigen::MatrixXd eigenvectors = eigensolver.eigenvectors();

Eigen::VectorXd eigenvalues = eigensolver.eigenvalues();

for (int i = 0; i < eigenvalues.size() - 1; ++i) {

for (int j = i + 1; j < eigenvalues.size(); ++j) {

if (eigenvalues(i) < eigenvalues(j)) {

eigenvalues.swap(i, j);

eigenvectors.col(i).swap(eigenvectors.col(j));

}

}

}

// 选取前k个特征向量组成变换矩阵

Eigen::MatrixXd transformation_matrix = eigenvectors.block(0, 0, eigenvectors.rows(), k);

// 将原始数据投影到变换矩阵上,得到降维后的数据

data = data * transformation_matrix;

}

int main() {

// 假设我们有4个样本,每个样本有3个特征

Eigen::MatrixXd data(4, 3);

data << 1, 2, 3,

4, 5, 6,

7, 8, 9,

10, 11, 12;

int k = 2; // 降维到2维

pca(data, k);

std::cout << "降维后的数据: " << std::endl << data << std::endl;

return 0;

}

在这个示例中,pca函数实现了 PCA 的主要步骤。首先通过data.colwise().mean()计算每列的均值,然后将数据进行中心化处理。接着计算协方差矩阵,利用SelfAdjointEigenSolver求解协方差矩阵的特征值和特征向量,并对特征向量按特征值大小进行排序。最后选取前k个特征向量组成变换矩阵,将原始数据投影到该矩阵上,完成降维操作。

(二)计算机视觉中的应用

在计算机视觉领域,Eigen 库同样发挥着不可或缺的作用,尤其是在图像特征提取和相机标定等关键任务中。

图像特征提取是计算机视觉中的重要环节,它旨在从图像中提取出具有代表性的特征,以便后续进行图像识别、目标检测、图像匹配等任务。以尺度不变特征变换(SIFT)算法为例,该算法通过检测图像中的关键点,并计算这些关键点的特征描述子,来实现对图像特征的提取。在 SIFT 算法中,需要进行大量的矩阵运算,如高斯滤波、梯度计算、特征向量的构建等,而 Eigen 库正好为这些运算提供了高效的实现方式。

在计算图像的梯度时,通常会使用卷积核与图像进行卷积运算。可以将图像表示为一个矩阵,卷积核也表示为一个矩阵,利用 Eigen 库的矩阵乘法运算来实现卷积操作。假设我们有一个\(m \times n\)的图像矩阵image和一个\(3 \times 3\)的卷积核矩阵kernel,计算梯度的代码示例如下:

 

#include <Eigen/Dense>

#include <iostream>

// 假设图像是一个二维矩阵,数据类型为double

typedef Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic> ImageMatrix;

// 计算图像在x和y方向上的梯度

void computeGradient(const ImageMatrix& image, ImageMatrix& gradient_x, ImageMatrix& gradient_y) {

int rows = image.rows();

int cols = image.cols();

gradient_x.resize(rows, cols);

gradient_y.resize(rows, cols);

// 定义x方向和y方向的卷积核

Eigen::Matrix3d kernel_x;

kernel_x << -1, 0, 1,

-2, 0, 2,

-1, 0, 1;

Eigen::Matrix3d kernel_y;

kernel_y << -1, -2, -1,

0, 0, 0,

1, 2, 1;

for (int i = 1; i < rows - 1; ++i) {

for (int j = 1; j < cols - 1; ++j) {

// 提取3x3的图像块

Eigen::Matrix3d patch = image.block(i - 1, j - 1, 3, 3);

// 计算x方向的梯度

gradient_x(i, j) = (patch * kernel_x).sum();

// 计算y方向的梯度

gradient_y(i, j) = (patch * kernel_y).sum();

}

}

}

int main() {

// 假设我们有一个5x5的图像

ImageMatrix image(5, 5);

image << 1, 2, 3, 4, 5,

6, 7, 8, 9, 10,

11, 12, 13, 14, 15,

16, 17, 18, 19, 20,

21, 22, 23, 24, 25;

ImageMatrix gradient_x, gradient_y;

computeGradient(image, gradient_x, gradient_y);

std::cout << "x方向的梯度: " << std::endl << gradient_x << std::endl;

std::cout << "y方向的梯度: " << std::endl << gradient_y << std::endl;

return 0;

}

在这段代码中,computeGradient函数实现了计算图像在\(x\)和\(y\)方向上梯度的功能。通过定义\(x\)方向和\(y\)方向的卷积核,利用image.block(i - 1, j - 1, 3, 3)提取图像中的\(3 \times 3\)图像块,然后通过矩阵乘法计算梯度值,并存储在gradient_x和gradient_y矩阵中。

相机标定是确定相机内部参数(如焦距、主点坐标等)和外部参数(如旋转矩阵、平移向量等)的过程,这些参数对于将图像中的像素坐标转换为世界坐标至关重要。在相机标定中,通常会使用张正友标定法等经典方法,该方法涉及到大量的矩阵运算,如矩阵的乘法、求逆、特征值分解等,Eigen 库能够高效地完成这些运算。

假设我们已经获取了棋盘格角点在世界坐标系和图像坐标系中的坐标,利用 Eigen 库进行相机标定的大致步骤如下:首先,构建世界坐标系下的点矩阵和图像坐标系下的点矩阵;然后,根据张正友标定法的原理,通过一系列的矩阵运算,包括计算投影矩阵、求解线性方程组等,来估计相机的内参矩阵和外参矩阵。

以下是一个简化的相机标定示例代码,展示了如何使用 Eigen 库进行基本的矩阵运算来求解相机内参矩阵:

 

#include <Eigen/Dense>

#include <iostream>

// 假设我们已经获取了棋盘格角点在世界坐标系和图像坐标系中的坐标

// 世界坐标系下的点,假设为3D点,每个点有3个坐标值,这里假设有5个点

Eigen::MatrixXd world_points(5, 3);

world_points << 0, 0, 0,

1, 0, 0,

1, 1, 0,

0, 1, 0,

0.5, 0.5, 0;

// 图像坐标系下的点,假设为2D点,每个点有2个坐标值,对应上面的5个点

Eigen::MatrixXd image_points(5, 2);

image_points << 100, 100,

200, 100,

200, 200,

100, 200,

150, 150;

// 假设已经知道相机的畸变系数为0,这里简化处理

Eigen::VectorXd distortion_coeffs(5);

distortion_coeffs.setZero();

// 构建线性方程组AX = b

Eigen::MatrixXd A(10, 11);

Eigen::VectorXd b(10);

// 根据张正友标定法的原理填充A和b矩阵,这里省略具体的填充过程,只展示大致框架

// ......

// 求解线性方程组

Eigen::MatrixXd X = A.colPivHouseholderQr().solve(b);

// 从解向量X中提取相机内参矩阵

Eigen::Matrix3d K;

K << X(0), X(1), X(2),

X(3), X(4), X(5),

0, 0, 1;

std::cout << "计算得到的相机内参矩阵 K: " << std::endl << K << std::endl;

在这个示例中,首先定义了世界坐标系下的点矩阵world_points和图像坐标系下的点矩阵image_points,以及畸变系数向量distortion_coeffs。然后根据张正友标定法的原理构建线性方程组的系数矩阵A和常数向量b,这里省略了具体的填充过程。通过A.colPivHouseholderQr().solve(b)求解线性方程组,得到解向量X,最后从解向量中提取相机内参矩阵K。

(三)机器人运动学中的应用

在机器人领域,运动学是研究机器人运动的基础,它主要关注机器人的位置、姿态与关节角度之间的关系。而 Eigen 库在机器人运动学中扮演着关键角色,特别是在机器人正逆运动学求解方面。

机器人正运动学是根据机器人各关节的角度,计算末端执行器在空间中的位置和姿态。以常见的 6 自由度工业机器人为例,每个关节的运动都会引起末端执行器位置和姿态的变化。通过建立机器人的运动学模型,利用齐次变换矩阵来描述关节之间的相对运动关系,从而可以计算出末端执行器的位姿。

假设机器人的每个关节都可以用一个齐次变换矩阵来表示,从基座到末端执行器的总变换矩阵就是各个关节变换矩阵的乘积。利用 Eigen 库的矩阵乘法功能,可以方便地实现这一计算过程。以下是一个简单的 6 自由度机器人正运动学计算示例:

 

#include <Eigen/Dense>

#include <Eigen/Geometry>

#include <iostream>

// 定义关节角度(弧度)

Eigen::VectorXd joint_angles(6);

joint_angles << 0.5, 0.3, 0.2, 0.1, -0.1, -0.2;

// 定义连杆长度等参数(这里简化假设)

Eigen::VectorXd link_lengths(6);

link_lengths << 0.1, 0.2, 0.3, 0.4, 0.5, 0.6;

// 计算每个关节的齐次变换矩阵

Eigen::Matrix4d T01 = Eigen::AngleAxisd(joint_angles(0), Eigen::Vector3d::UnitZ()) *

Eigen::Translation3d(link_lengths(0), 0, 0).matrix();

Eigen::Matrix4d T12 = Eigen::AngleAxisd(joint_angles(1), Eigen::Vector3d::UnitY()) *

Eigen::Translation3d(0, link_lengths(1), 0).matrix();

Eigen::Matrix4d T23 = Eigen::AngleAxisd(joint_angles(2), Eigen::Vector3d::UnitZ()) *

Eigen::Translation3d(0, 0, link_lengths(2)).matrix();

Eigen::Matrix4d T34 = Eigen::AngleAxisd(joint_angles(3), Eigen::Vector3d::UnitY()) *

Eigen::Translation3d(0, 0, link_lengths(3)).matrix();

Eigen::Matrix4d T45 = Eigen::AngleAxisd(joint_angles(4), Eigen::Vector3d::UnitZ()) *

Eigen::Translation3d(0, 0, link_lengths(4)).matrix();

Eigen::Matrix4d T56 = Eigen::AngleAxisd(joint_angles(5), Eigen::Vector3d::UnitY()) *

Eigen::Translation3d(0, 0, link_lengths(5)).matrix();

// 计算从基座到末端执行器的总变换矩阵

Eigen::Matrix4d T = T01 * T12 * T23 * T34 * T45 * T56;

// 提取末端执行器的位置和姿态

Eigen::Vector3d position = T.block(0, 3, 3, 1);

Eigen::Quaterniond orientation(T.block(0, 0, 3, 3));

std::cout << "末端执行器的位置: " << position.transpose() << std::endl;

std::cout << "末端执行器的姿态(四元数): " << orientation.coeffs().transpose() << std::endl;

在这段代码中,首先定义了关节角度向量joint_angles和连杆长度向量link_lengths。然后根据每个关节的旋转轴和连杆长度,利用Eigen::AngleAxisd和Eigen::Translation3d来构建每个关节的齐次变换矩阵T01、T12等。通过矩阵乘法将这些变换矩阵依次相乘,得到从基座到末端执行器的总变换矩阵T。

六、Eigen 优化与技巧

(一)性能优化

Eigen 库的高性能得益于其对现代 CPU 特性的充分利用以及一系列优化策略。向量化和并行计算是其中的关键技术,合理运用这些技术可以显著提升 Eigen 代码的执行效率。

向量化运算利用现代 CPU 的 SIMD(单指令多数据)指令集,如 SSE(Streaming SIMD Extensions)、AVX(Advanced Vector Extensions)等,使一条指令能够同时对多个数据进行操作,从而大大提高计算吞吐量。在矩阵乘法中,Eigen 库会自动检测 CPU 是否支持 SIMD 指令集,并在合适的情况下使用向量化计算。假设我们有两个矩阵A和B,在支持 AVX 指令集的 CPU 上,Eigen 库会将矩阵元素按 AVX 寄存器的宽度进行分组,然后使用 AVX 指令一次性计算多个元素的乘积和累加,相较于传统的标量计算,计算速度得到大幅提升。

并行计算则充分发挥多核 CPU 的优势,将计算任务分解为多个子任务,分配到不同的核心上同时执行。Eigen 库通过内置的线程池实现并行计算。在进行大规模矩阵运算时,如矩阵的特征值分解,Eigen 库会根据系统的 CPU 核心数,将计算任务划分为多个部分,每个部分由一个线程在不同的 CPU 核心上执行,从而加快计算速度。

为了编写高效的 Eigen 代码,首先要充分利用 Eigen 库的表达式模板技术。在进行复杂的矩阵运算时,尽量避免中间变量的显式创建,让 Eigen 库在最后一步才进行实际的计算,这样可以减少不必要的内存访问和计算开销。例如,在计算C = A * B + D * E时,直接使用C = A * B + D * E这样的表达式,而不是先计算A * B和D * E并存储在中间变量中,再进行加法运算。

合理选择矩阵的存储方式也至关重要。对于稀疏矩阵,使用 Eigen 库提供的稀疏矩阵类型(如SparseMatrix),并采用合适的稀疏存储格式(如压缩列存储 CSC 或压缩行存储 CSR),可以大大减少内存占用,提高计算效率。在处理大规模稀疏矩阵时,这种存储方式能够避免对大量零元素的无效操作,显著提升计算速度。

此外,根据矩阵的特性选择合适的算法也能优化性能。对于对称矩阵,使用 Eigen 库提供的针对对称矩阵的特殊算法和函数,如selfadjointView()函数,这些算法利用了对称矩阵的对称性,减少了计算量,提高了计算效率。在求解线性方程组时,如果系数矩阵是对称正定矩阵,使用共轭梯度法等迭代算法,比直接求逆矩阵的方法更加高效。

(二)常见问题与解决

在使用 Eigen 库的过程中,可能会遇到一些常见问题,了解这些问题并掌握相应的解决方法和调试技巧,能够帮助开发者更高效地使用 Eigen 库。

类型不匹配是一个常见问题。Eigen 库中的矩阵和向量都是模板类,在进行运算时,要求参与运算的对象类型必须一致。当使用MatrixXd(动态双精度矩阵)和MatrixXf(动态单精度矩阵)进行运算时,就会出现类型不匹配的错误。解决这个问题的方法是确保参与运算的矩阵和向量类型相同,可以通过显式类型转换来实现。例如,将MatrixXf转换为MatrixXd,可以使用cast()函数:

 

MatrixXf A = MatrixXf::Random(3, 3);

MatrixXd B = A.cast<double>();

内存管理问题也是需要关注的重点。虽然 Eigen 库在内存管理方面做了很多优化,但在处理动态矩阵时,仍然需要注意内存的分配和释放。在创建大量动态矩阵时,如果不及时释放不再使用的矩阵所占用的内存,可能会导致内存泄漏。在使用动态矩阵时,要确保在不再需要矩阵时,及时释放内存。对于局部变量矩阵,在函数结束时,它们会自动释放内存;对于动态分配的矩阵(如使用new创建的矩阵),要记得使用delete来释放内存。

矩阵维度不匹配也是常见错误。在进行矩阵运算时,如矩阵乘法,要求前一个矩阵的列数必须等于后一个矩阵的行数。当进行A * B运算时,如果A的列数和B的行数不相等,就会出现维度不匹配的错误。为了避免这种错误,在进行矩阵运算前,要仔细检查矩阵的维度。可以使用rows()和cols()函数来获取矩阵的行数和列数,进行必要的维度检查。在进行矩阵乘法前,可以添加如下检查代码:

 

if (A.cols()!= B.rows()) {

throw std::invalid_argument("矩阵维度不匹配,无法进行乘法运算");

}

在调试 Eigen 库相关代码时,可以利用 Eigen 库提供的调试宏。例如,定义EIGEN_DEBUG宏,可以输出更多的调试信息,帮助定位问题。在代码中添加#define EIGEN_DEBUG,然后重新编译,Eigen 库在运行时会输出一些内部的计算信息,如矩阵运算的中间结果等,有助于分析代码中的错误。同时,使用调试工具(如 GDB、Visual Studio 调试器等)进行单步调试,查看变量的值和程序的执行流程,也能有效地找出问题所在。

七、总结与展望

Eigen 库作为 C++ 编程领域中线性代数计算的得力助手,以其卓越的性能、简洁易用的接口和丰富多样的功能,在机器学习、计算机视觉、机器人运动学等众多领域展现出了强大的应用价值。从基础的矩阵与向量操作,到进阶的特殊矩阵处理、稀疏矩阵运算以及几何变换,再到实际项目中的应用案例,我们深入领略了 Eigen 库的魅力与实力。

在机器学习中,它助力线性回归和主成分分析等算法的高效实现,为数据建模和降维提供了有力支持;在计算机视觉领域,图像特征提取和相机标定等任务借助 Eigen 库得以顺利完成,推动了图像识别和理解的发展;在机器人运动学中,正逆运动学的求解依赖于 Eigen 库的矩阵运算,实现了机器人的精确运动控制。同时,通过性能优化策略和对常见问题的解决,我们能够进一步提升 Eigen 库的使用效率和稳定性。

展望未来,随着计算机硬件技术的不断发展,如 CPU 性能的持续提升、新型硬件架构的出现,Eigen 库有望进一步优化其底层实现,更加充分地利用硬件特性,实现更高的计算效率。在功能拓展方面,可能会针对新兴的应用领域,如量子计算中的量子态表示与运算、生物信息学中的分子结构分析等,开发出更具针对性的功能。此外,随着开源社区的不断壮大,Eigen 库将吸引更多开发者的参与和贡献,从而不断完善和发展,为更多领域的创新应用提供坚实的基础。希望读者能够通过本文深入了解 Eigen 库,并在实际项目中充分发挥其优势,探索更多创新的应用场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值