Opencv_04 图像的数据类型Mat详解

一. Mat数据类型介绍

  1. 首先Mat数据你不需要手动管理它的内存,如果你传递了一个已经存在的Mat对象,它已经为矩阵分配了所需的内存空间,它将被重用.
  2. Mat包含两个数据部分的类:矩阵头(包含诸如矩阵大小,用于存储的方法,存储的矩阵地址等信息)和指向包含像素值矩阵(根据选择的存储方法而有不同的维度)的指针.矩阵头大小是个常量,不同大小的图像的矩阵大小各不相同,通常矩阵大小要比图像大小大几个数量级.
  3. opencv是一个图像处理库,其中包含大量图像处理函数.为了解决计算难题,多数情况下选用库中的多个函数来实现计算功能,常见的做法是将图像传递给函数.而图像处理算法的计算量往往非常大,所以要通过避免不必要的图像复制来进一步提升程序的运行速度.为了解决上述问题,Opencv采用了一种引用计数系统.
    具体做法是,每个Mat对象有其各自的头,两个Mat对象可以通过将矩阵指针指向同一块地址来共享一个矩阵,复制操作只复制Mat头和指向矩阵的指针,而不是复制数据本身
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include "MyOpencv.h"
string imagePath = IMAGE_PATH + "\\lena.jpg";
int main()
{
	Mat A, C;// 这里是仅仅创建了头部部分和data的指针,图像矩阵并没有创建
	cout << "A.size = " << sizeof(A) << " C.size = " << sizeof(C) << endl;
	A = imread(imagePath); // 这里创建了数据域
	cout << "A.size = " << sizeof(A) << " A.data = " <<
	 A.cols * A.rows * A.channels() * A.elemSize() << endl;
	Mat B(A); // 使用拷贝构造函数
	C = A; // 赋值操作符函数
	return 0;
}

解析:

上述所有的对象均指向同一个数据矩阵,对矩阵的任何变动均会影响所有的对象.在实际的示例中,不同的对象只是对同一数据的不同方式的访问,尽管如此,不同MAT对象的头各不相同.问题来了,如果像素矩阵可以属于多个MAT对象,那么当它不需要再次被使用时,由谁来负责清空?答案是:通过引用计数机制来实现,由最后一个使用它的对象来清空.每次拷贝MAT对象头时,计数器便会加1;当对MAT对象头进行清空时,此计数会减一.当计数器值为零的时候,矩阵会被释放.当需要对矩阵本身进行复制的时候,Opencv提供了如下两个方法

Mat F = A.clone();
Mat G;
A.copyTo(G);

修改F或者G不会影响A所指向的矩阵,需要记住以下几点:

  1. Opencv函数,输出图像分配时是自动的(除非另行规定)
  2. 无需考虑OPencv中的C++的接口的内存管理
  3. 赋值操作符和拷贝构造函数仅仅复制MAT对象头和那个data指针
  4. 图像的基本矩阵可以利用cv::Mat::clone() 和cv::Mat::copyTo()两个函数进行复制

二. Mat的常用操作

① 创建Mat对象,常用的Mat构造函数

基本: 尺寸+类型

// 创建一个rows行cols列,类型为type的Mat对象
Mat(int rows,int cols,int type);
// 创建一个size大小(宽,高)->(cols,rows)类型为type的Mat对象
Mat(Size size,int type);
// 创建一个维度为n,数据根据数组去创建类型为type的Mat对象
Mat(int ndims,const int * sizes,int type);
// 创建一个固定维度,数据根据容器里面的size去创建类型为type的Mat独享
Mat(cosnt std::vector<int>& sizes,int type);

首先解释下type变量的含义

在构造哈数中,通常可以看到需要输入一个整型的变量type,这个参数通过宏定义在相关头文件中.用来指明Mat对象的存储的数据类型(主要是data部分)

CV_[位数][带符号与否][类型前缀]C[通道数] CV_8UC1 -> 表示8位单通道无符号char类型数组,
CV_32FC2表示一个2通道的32位浮点型数组.

depth:深度

  • CV_8U: bool或者uchar
  • CV_8S: schar或者char
  • CV_16U:ushort
  • CV_16S:short
  • CV_32S:int 或者unsigned int
  • CV_32F: float
  • CV_64F:double

Mat有一个type()函数可以返回该Mat的类型.类型表示了矩阵中元素的类型以及矩阵通道个数,它是一系列的预定义的常量,其命名规则为CV_(位数) + (数据类型) + (通道数)

实例:

#include"MyOpencv.h"
#include <vector>
int main()
{
	// 创建3行3列无符号8位的Mat矩阵
	Mat m1 = Mat(3, 3, CV_8UC1);
	Size size(3, 4);
	// 创建宽为3,高为4的Mat矩阵,4行3列的矩阵
	Mat m2 = Mat(size, CV_8UC1);
	// 创建二维的2行3列的矩阵
	int sizes[] = { 2,3 };
	Mat m3 = Mat(2, sizes, CV_8UC1);
	// 创建3维的2*3*3的三通道的矩阵
	vector<int> v;
	v.push_back(2);
	v.push_back(3);
	v.push_back(3);
	Mat m4 = Mat(v, CV_8UC3);
	return 0;
}

尺寸 + 颜色

Mat(int rows,int cols,int type,const Scalar& s);

Mat(Size size,int type,const Scalar& s);

Mat(int ndims,const int* sizes,int type,const Scalar& s);

Mat(const std::vector<int>& sizes,int type,cosnt Scalar& s);

测试代码:

#include "MyOpencv.h"
#include <vector>

int main()
{
	// 创建3行3列的值为128的单通道矩阵
	Mat m1 = Mat(3, 3, CV_8UC1, cv::Scalar(0));

	// 创建4*4三通道矩阵,并且用cv::Scalar(0,0,255)填充
	Mat m2 = Mat(4, 4, CV_8UC3, cv::Scalar(0,0, 255));

	// 创建3*4三通道矩阵,并且用(1,2,3)填充
	Mat m3 = Mat(cv::Size(3, 4), CV_8UC3, cv::Scalar(1, 2, 3));

	//创建2行3列的单通道矩阵,用(1)填充
	int sizes[] = { 2,3 };
	Mat m4 = Mat(2, sizes, CV_8UC1, cv::Scalar(1));

	// 创建4行3列的单通道矩阵,用(2)填充
	vector<int> v;
	v.push_back(4);
	v.push_back(3);
	Mat m5 = Mat(v, CV_8UC1, cv::Scalar(8));
	cout << "m1 = \n" << m1 << endl;
	cout << "m2 = \n" << m2 << endl;
	cout << "m3 = \n" << m3 << endl;
	cout << "m4 = \n" << m4 << endl;
	cout << "m5 = \n" << m5 << endl;

	system("pause");
	return 0;
}

结果:

基本+数据

第三种类型就是基本类型中添加数据data,也就是尺寸+类型+数据,其中表示数据的是:void* data

Mat(int rows,int cols,int type,void *data,size_t step=AUTO_SETP);

Mat(Size size,int type,void * data,size_t step=AUTO_STEP);

Mat(int ndims,const int * sizes,int type,void * data,const size_t* steps = 0);

Mat(cosnt std::vector<int>& sizes,int type,void* data,const size_t* steps = 0);

参数说明:

  • void* data: 指向实现准备好的用户的数据的指针.矩阵构造器将会取数据和步长参数,而不会分配矩阵数据.取而代之的是,仅仅初始化矩阵的头去指向具体的数据,这意味着数据不会被拷贝.这个操作将会非常的高效,用来处理外部的数据,值得注意的是,这些外部的数据不会被自动释放

  • step: 矩阵每行占据的字节数.这个值必须包含每行补齐的数据.如果不提供这个参数,就假设没有补齐,实际的步长就等于cols*elemSize();

这种带void*data的函数在使用时要注意:

  1. 这个函数在执行的时候,只是初始化了mat的头,不拷贝数据,也就是使得mat->data指向了这个外部数据的data
  2. 外部数据data需要另外释放,opencv的内存管理不会对这一部分内存进行释放
  3. 如果传入的data是一个局部对象的data,后期在传出时必须要进行深拷贝可以传出正确的数据
  4. 如果传入的数据是用户data,需要自己手动释放掉,如果是opencv的Mat的data会进行自动内存管理.

举例:

#include "MyOpencv.h"
#include <vector>
int main()
{
	Mat m1 = Mat(3, 4, CV_8UC1, cv::Scalar(2));
	// 将m1的数据转换为4行3列的矩阵
	Mat m2 = Mat(4, 3, CV_8UC1, (void *)m1.data); 
	// 将m1的数据转换为3行2列的双通道矩阵
	Mat m3 = Mat(cv::Size(2, 3), CV_8UC2, (void *)m1.data);
	// 将m1的数据转换为2行2列的三通道矩阵
	int sizes[] = { 2,2 };
	Mat m4 = Mat(2, sizes, CV_8UC3, (void *)m1.data);
	// 将m1的数据转换为4行1列的三通道矩阵
	vector<int> v;
	v.push_back(4);
	v.push_back(1);
	Mat m5 = Mat(v, CV_8UC3, (void *)m1.data);

	cout << "m1 = \n" << endl;
	cout << m1 << endl;
	cout << "m2 = \n" << endl;
	cout << m2 << endl;
	cout << "m3 = \n" << endl;
	cout << m3 << endl;
	cout << "m4 = \n" << endl;
	cout << m4 << endl;
	cout << "m5 = \n" << endl;
	cout << m5 << endl;
	return 0;
}

结果:

Mat + 其他

Mat(const Mat& m);

Mat(const Mat& m,const Range& rowRange,const Range& colRange=Range::all());

Mat(const Mat& m,const Rect& rio);

Mat(const Mat& m,const Range& ranges);

Mat(const Mat& m,const std::vector<Range>& ranges);

参数解释:
Range:

用来指明一个序列的连续的子序列.拥有两个公共成员:startend.可以使用这两个成员来表示子序列的范围,左开右闭.可以使用Range.all()表示所有.

Rect类型:

创建一个矩形区域,可以用来提取感兴趣区域.前两位为一个坐标,后两位表示偏移量.

注意:

这些构造函数,没有复制数据,构造指向m数据,如果有引用计数器,则递增.当你修改矩阵的首,通过这样的构造函数,对应的m的数据也会被修改,如果你想要不修改,请使用Mat::clone()出一个副本.

/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include "MyOpencv.h"

int main()
{
	Mat m1 = Mat(10, 10, CV_8UC1,cv::Scalar(6));
	// 拷贝构造
	Mat m2 = Mat(m1);
	// 截取前三行
	Mat m3 = Mat(m1, cv::Range(0, 3), cv::Range::all());
	// 截取前三列
	Mat m4 = Mat(m1, cv::Range::all(), cv::Range(0, 3));

	// 截取中间的部分2行2列.从第4行第4列开始截取
	Mat m5 = Mat(m1, cv::Rect(4, 4, 2, 2));


	cout << "m1 = m2 = \n" << endl;
	cout << m2 << endl;
	cout << "m3 = \n" << endl;
	cout << m1 << endl;
	cout << "m4 = \n" << endl;
	cout << m4 << endl;
	cout << "m5 = \n" << endl;
	cout << m5 << endl;

	return 0;
}

结果:

② Mat的行与列相关的操作
// 返回某一行的数据
Mat row(int y) const;
/*
inline
Mat Mat::row(int y) const
{
    return Mat(*this, Range(y, y + 1), Range::all());
}
*/
// 返回某一列的数据
Mat col(int x) const;

// 返回某几行的数据
Mat rowRange(int startrow,int endrow) const;

// 返回某几行的数据
Mat rowRange(const Range& r) const;

// 返回某几列的数据
Mat colRange(int startcol,int endcol) const;

// 返回某几列的数据
Mat colRange(const Range& r) const;

示例:

#include "MyOpencv.h"


int main()
{
	Mat m1 = Mat(5, 5, CV_8UC1, cv::Scalar(1));
	// 获取第一行的数据
	Mat m2 = m1.row(0);
	// 获取第二列的数据
	Mat m3 = m1.col(1);
	// 获取前两行的数据
	Mat m4 = m1.rowRange(0, 2);
	Mat m5 = m1.rowRange(cv::Range(0, 2));

	// 获取第二列和第三列的数据
	Mat m6 = m1.colRange(1, 3);
	Mat m7 = m1.colRange(cv::Range(1, 3));
	cout << "m1 = \n" << endl;
	cout << m1 << endl;
	cout << "m2 = \n" << endl;
	cout << m2 << endl;
	cout << "m3 = \n" << endl;
	cout << m3 << endl;
	cout << "m4 = \n" << endl;
	cout << m4 << endl;
	cout << "m5 = \n" << endl;
	cout << m5 << endl;
	cout << "m6 = \n" << endl;
	cout << m6 << endl;
	cout << "m7 = \n" << endl;
	cout << m7 << endl;

	system("pause");
	return 0;
}

结果:

③ 拷贝和转换
// 拷贝一份和当前矩阵一样的矩阵返回.会为目标矩阵重新分配内存
Mat clone() const;

// 当目标矩阵与源矩阵具有相同的type和size的时候,copyTo不会为目标矩阵重新分配内存.
void copyTo(OutputArray m) const;

// 不仅把图像复制到m上,并且通过掩码进行过滤,如果源图像的像素值非0,则会拷贝到输出图像上.
// 如果为0,则保留输出图像上的原来的像素值
void copyTo(OutputArray m,inputArray mask) const;

// 图像转换函数,可以实现像素类型改变,缩放和平移操作
void convertTo(OutputArray m,int rtype,double alpha=1,double beta=0)const;

void assignTo(Mat& m,int type=-1) const;

Mat& setTo(inputArray value,InputArray mask=noArray());
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include "MyOpencv.h"

int main()
{
	Mat m1 = Mat::ones(1, 4, CV_32F);
	Mat m2 = m1; // m2和m1指向同一内存地址
	Mat m3 = Mat::zeros(1, 4, CV_32F); 
	m3.copyTo(m1); // m1未被重新分配内存,通过m1可以改变m2的内容
	// 因为m1和m3的size和type一致,所以不会分配内存
	cout << m1 << endl; // [0,0,0,0]
	cout << m2 << endl; // [0,0,0,0]


	Mat m4 = Mat::ones(1, 5, CV_32F);
	m4.copyTo(m1);// m4和m1的size不一致,所以会分配内存,改变了m1,不会影响m2
	cout << m1 << endl; // [1,1,1,1,1]
	cout << m2 << endl; // [0,0,0,0]

	return 0;
}

结果:

掩码的使用:

/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include "MyOpencv.h"


int main()
{
	Mat m1 = Mat(3, 3, CV_8UC1, cv::Scalar(2));
	m1.at<uchar>(0, 0) = 0; // 将(0,0)位置置为0
	m1.at<uchar>(2, 2) = 0; // 将(2,2)位置置为0
	Mat m2 = Mat(3, 3, CV_8UC1, cv::Scalar(1));
	// 因为(0,0)位置和(2,2)位置为0,所以拷贝的时候,不会拷贝源Mat的数据,会保留目标矩阵里面的数据
	m1.copyTo(m2, m1);
	cout << "m2 = " << endl;
	cout << m2 << endl;
	return 0;
}

结果:

convertTo()使用:

  • m: 目标矩阵.如果m在运算前没有合适的尺寸或者类型,将被重新分配
  • rtype: 目标矩阵的类型.因为目标矩阵的通道数与源矩阵一样,所以rtype也可以看做是目标矩阵的位深度.如果rtype为负值(一般为-1),目标矩阵(输出矩阵)将使用和源矩阵(输入矩阵)相同的类型.
  • alpha: 尺度变换因子(缩放)
  • beta: 附加到尺度变换后的值上的偏移量(可选),即将输入数组元素按比例缩放后添加的值dst(i) = src(i) * scale + (beta,beta,...);如果没有规定,则默认是0.

说明:

  1. 如果scale = 1,beta = 0,则不进行比例缩放,也即目标矩阵和源矩阵没有区别
  2. 如果输入数组与输出数组的类型相同,则函数可以被用于缩放和平移操作
  3. 如果输入数组和输出数组的类型不同,则用于做类型上的转换
  4. converTo()在进行转换的时候,输出的通道数与输入的通道数相同,即使你填入的转换类型通道数不同,输出的通道数还是与输入的通道数相同
  5. converTo()支持就地(in-place)操作
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include "MyOpencv.h"
string imagePath = IMAGE_PATH + "\\lena.jpg";
int main()
{
	Mat imgColor = imread(imagePath, IMREAD_COLOR);
	Mat imgGray = imread(imagePath, IMREAD_GRAYSCALE);
	cout << "Original: " << endl;
	cout << "ImageColor.type() = " << imgColor.type() << endl; // CV_8UC3
	cout << "ImageGray.type() = " << imgGray.type() << endl; // CV_8UC1
	cout << "--------------------------------------------------" << endl;

	// 是否转换为16位深度为3通道
	Mat imgColorC3;
	Mat imgGrayC3;
	imgColor.convertTo(imgColorC3, CV_16UC3);
	imgGray.convertTo(imgGrayC3, CV_16UC3);
	cout << "convertTo CV_16UC3: " << endl;
	cout << "ImageColorC3.type() = " << imgColorC3.type() << endl;// CV_16UC3
	cout << "ImageGrayC3.type() = " << imgGrayC3.type() << endl; // CV_16UC1

	cout << "--------------------------------------------------" << endl;
	// 是否转换为16位深度的单通道
	Mat imgColorC1;
	Mat imgGrayC1;
	imgColor.convertTo(imgColorC1, CV_16UC1); 
	imgGray.convertTo(imgGrayC1, CV_16UC1); 

	cout << "convertTo CV_16UC1:" << endl;
	cout << "ImageColorC1.type() = " << imgColorC1.type() << endl;// CV_16UC3
	cout << "ImageColorC2.type() = " << imgGrayC1.type() << endl;// CV_16UC1
	
	cout << "--------------------------------------------------" << endl;

	// 是否支持就地操作
	imgColor.convertTo(imgColor, CV_16UC3);
	imgGray.convertTo(imgGray, CV_16UC3);
	cout << "In place: " << endl;
	cout << "imageColor.type() = " << imgColor.type() << endl; // CV_16UC3
	cout << "imageGray.type() = " << imgGray.type() << endl; // CV_16UC1

	return 0;
}
④ Mat类常用的成员属性
// 用来作为标志用的,主要包含magic signature,submat flag,number of channels,depth
int flags;

// 数据的维度 
int dims;

// 行和列的数量,数组超过2维时,为(-1,-1)
int rows,cols;

// 图像数据
uchar* data; 

flags详解

  • 0-2位代表depth即数据类型(如CV_8U),Opencv的数据类型一共有7种,所以3位即可表示.
  • 3-11位代表通道数channels,因为Opencv的最大通道数为512,只需要9位即可表示全部
  • 0-11位共同代表type即通道数和数据类型(如 CV_8UC3)
  • 12-13位未使用
  • 14位代表Mat的内存是否连续,一般由creat创建的mat均是连续的,如果是连续的,将加快数据的访问
  • 15位代表改Mat是否为某一个Mat的submatrix,一般通过ROI以及row(),col(),rowRange,colRange()得到的mat均为submatrix.
  • 16-31代表magic signature,暂时理解为用来区分Mat的类型,如Mat和SparseMat
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include "MyOpencv.h"

int main()
{
	Mat m = Mat(3, 4, CV_8UC3,cv::Scalar(1,2,3));
	cout << "m.flags = " << m.flags << endl;
	cout << "m.dims = " << m.dims << endl;
	cout << "m.rows = " << m.rows << " m.cols = " << m.cols << endl;
	cout << "m = " << endl;
	cout << m << endl;

	// 可以直接更改行数和列数
	m.rows = 2;
	m.cols = 2;
	cout << "2行2列: " << endl;
	cout << m << endl;

	// 更改通道数,变成灰度图像
	m.flags = 0;
	cout << "flags = 0之后: m = " << endl;
	cout << m << endl;
	
	return 0;
}

结果:

⑤ 图像的基本信息
// 类型
int type() const; 

// 图像深度
int depth() const; 

// 图像的通道数
int channels() const; 

// 图像是否为空
bool empty() const 

解析:
类型返回的是一个int类型,定位为宏数据,其对应的值表示的意思如下:

在这里插入图片描述
比如0 -> CV_8UC1, 1-> CV_8S_C1,依次类推

⑥ 按照类型生成图像矩阵

这些函数可以生成全是1,全是0,对角矩阵或者按照类型生成矩阵,主要有:

// 返回指定大小和类型的值全部是0的Matlab式的数组.
static MatExpr zeros(int rows,int cols,int type);
Mat A;
A = Mat::zeros(3,3,CV_32F);
// 只要A不是3*3浮点矩阵它就会被分配新的矩阵,否则就使用现有的.
// 其他的函数和这个函数类似
static MatExpr zeros(Size size,int type);

static MatExpr zeros(int ndims,const int* sz,int type);

static MatExpr ones(int rows,int cols,int type);

static MatExpr ones(Size size,int type);

static MatExpr ones(int ndims,const int * sz,int type);

static MatExpr eye(int rows,int cols,int type);

static MatExpr eye(Size size,int type);

void create(int rows,int cols,int type);

void create(Size size,int type);

void create(int ndims,const int* sizes,int type);

void create(const std::vector<int>& sizes,int type);

实例解析:

#include"MyOpencv.h"

int main()
{
	// 两行两列的单通道全部为0的图像矩阵
	Mat m1 = Mat::zeros(2, 2, CV_8UC1);
	// 两行三列的,三通道矩阵,全部为0
	Mat m2 = Mat::zeros(cv::Size(3, 2), CV_8UC3);
	// 3行4列的单通道矩阵,全部为0
	int sizes[] = { 3,4 };
	Mat m3 = Mat::zeros(2, sizes, CV_8UC1);

	// 创建全部为1的单通道矩阵
	Mat m4 = Mat::ones(2, 3, CV_8UC1);

	// 创建单位对角矩阵
	Mat m5 = Mat::eye(3, 3, CV_8UC1);

	cout << "m1 = " << endl;
	cout << m1 << endl;
	cout << "m2 = " << endl;
	cout << m2 << endl;
	cout << "m3 = " << endl;
	cout << m3 << endl;
	cout << "m4 = " << endl;
	cout << m4 << endl;
	cout << "m5 = " << endl;
	cout << m5 << endl;

	return 0;
}

结果:

关于create函数的说明

1)这个是Mat的关键方法之一,大多数新型的Opencv函数和方法对每个输出数组的创建都会调动此方法.
2)这个方法采用的算法如下:

  • 如果当前的数组和形状匹配新的,立即返回.否则释放之前的数据的引用,初始化新的头,为数据分配新的数据,并且引用计数器设置为1.这种设计,使得用户不用显示的指定输出数组的大小和类型,使得变成很方便
  • 5
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值