学习OpenCV——Kalman滤波

原文:https://blog.csdn.net/yangtrees/article/details/8075911

背景:

卡尔曼滤波是一种高效率的递归滤波器(自回归滤波器), 它能够从一系列的不完全及包含噪声测量中,估计动态系统的状态。卡尔曼滤波的一个典型实例是从一组有限的,包含噪声的,对物体位置的观察序列(可能有偏差)预测出物体的位置的坐标速度

这种滤波方法以它的发明者鲁道夫.E.卡尔曼(Rudolph E. Kalman)命名,但是根据文献可知实际上Peter Swerling在更早之前就提出了一种类似的算法。

斯坦利.施密特(Stanley Schmidt)首次实现了卡尔曼滤波器。卡尔曼在NASA埃姆斯研究中心访问时,发现他的方法对于解决阿波罗计划的轨道预测很有用,后来阿波罗飞船的导航电脑便使用了这种滤波器。 关于这种滤波器的论文由Swerling (1958)、Kalman (1960)与 Kalman and Bucy (1961)发表。

目前,卡尔曼滤波已经有很多不同的实现.卡尔曼最初提出的形式现在一般称为简单卡尔曼滤波器。除此以外,还有施密特扩展滤波器、信息滤波器以及很多Bierman, Thornton 开发的平方根滤波器的变种。也许最常见的卡尔曼滤波器是锁相环,它在收音机、计算机和几乎任何视频或通讯设备中广泛存在。


模型:

基本动态系统模型:

卡尔曼滤波建立在线性代数隐马尔可夫模型(hidden Markov model)上。其基本动态系统可以用一个马尔可夫链表示,该马尔可夫链建立在一个被高斯噪声(即正态分布的噪声)干扰的线性算子上的。系统的状态可以用一个元素为实数的向量表示。 随着离散时间的每一个增加,这个线性算子就会作用在当前状态上,产生一个新的状态,并也会带入一些噪声,同时系统的一些已知的控制器的控制信息也会被加入。同时,另一个受噪声干扰的线性算子产生出这些隐含状态的可见输出。

模型图:

                                                    

为了从一系列有噪声的观察数据中用卡尔曼滤波器估计出被观察过程的内部状态,我们必须把这个过程在卡尔曼滤波的框架下建立模型。也就是说对于每一步k,定义矩阵

例如:KalmanFilter KF(stateNum, measureNum, 0);

F: KF.transitionMatrix

H: KF.measurementMatrix

Qk : KF.processNoiseCov

R: KF.measurementNoiseCov

Pk : KF.errorCovPost

有时也需要定义Bk : KF.ontrolMatrix

如下。

尔曼滤波模型假设k时刻的真实状态是从(k − 1)时刻的状态演化而来,符合下式:

           \textbf{x}_{k} = \textbf{F}_{k} \textbf{x}_{k-1} + \textbf{B}_{k}\textbf{u}_{k}  + \textbf{w}_{k}

其中

  • Fk 是作用在xk−1上的状态变换模型(/矩阵/矢量)。 
  • Bk 是作用在控制器向量uk上的输入-控制模型。 
  • wk 是过程噪声,并假定其符合均值为零,协方差矩阵Qk多元正态分布

\textbf{w}_{k} \sim N(0, \textbf{Q}_k)

时刻k,对真实状态 xk的一个测量zk满足下式:

            \textbf{z}_{k} = \textbf{H}_{k} \textbf{x}_{k} + \textbf{v}_{k}

其中Hk是观测模型,它把真实状态空间映射成观测空间,vk 是观测噪声,其均值为零,协方差矩阵为Rk,且服从正态分布

\textbf{v}_{k} \sim N(0, \textbf{R}_k)

初始状态以及每一时刻的噪声{x0, w1, ..., wk, v1 ...vk} 都认为是互相独立的.

卡尔曼滤波器:

卡尔曼滤波是一种递归的估计,即只要获知上一时刻状态的估计值以及当前状态的观测值就可以计算出当前状态的估计值,因此不需要记录观测或者估计的历史信息。卡尔曼滤波器与大多数滤波器不同之处,在于它是一种纯粹的时域滤波器,它不需要像低通滤波器频域滤波器那样,需要在频域设计再转换到时域实现。

卡尔曼滤波器的状态由以下两个变量表示:

  • \hat{\textbf{x}}_{k|k},在时刻k 的状态的估计;
  • \textbf{P}_{k|k},误差相关矩阵,度量估计值的精确程度。

卡尔曼滤波器的操作包括两个阶段:预测更新。在预测阶段,滤波器使用上一状态的估计,做出对当前状态的估计。在更新阶段,滤波器利用对当前状态的观测值优化在预测阶段获得的预测值,以获得一个更精确的新估计值。

预测 predict:      Mat prediction = KF.predict();

\hat{\textbf{x}}_{k|k-1} = \textbf{F}_{k}\hat{\textbf{x}}_{k-1|k-1} + \textbf{B}_{k} \textbf{u}_{k} (预测状态)

\textbf{P}_{k|k-1} =  \textbf{F}_{k} \textbf{P}_{k-1|k-1} \textbf{F}_{k}^{T} + \textbf{Q}_{k} (预测估计协方差矩阵)

可参考:http://www.cs.unc.edu/~welch/media/pdf/kalman_intro.pdf

更新 updata:     KF.correct(measurement);

\tilde{\textbf{y}}_{k} = \textbf{z}_{k} - \textbf{H}_{k}\hat{\textbf{x}}_{k|k-1} (测量余量,measurement residual)

\textbf{S}_{k} = \textbf{H}_{k}\textbf{P}_{k|k-1}\textbf{H}_{k}^{T} + \textbf{R}_{k} (测量余量协方差)

\textbf{K}_{k} = \textbf{P}_{k|k-1}\textbf{H}_{k}^{T}\textbf{S}_{k}^{-1} (最优卡尔曼增益)

\hat{\textbf{x}}_{k|k} = \hat{\textbf{x}}_{k|k-1} + \textbf{K}_{k}\tilde{\textbf{y}}_{k} (更新的状态估计)

\textbf{P}_{k|k} = (I - \textbf{K}_{k} \textbf{H}_{k}) \textbf{P}_{k|k-1} (更新的协方差估计)

使用上述公式计算\textbf{P}_{k|k}仅在最优卡尔曼增益的时候有效。使用其他增益的话,公式要复杂一些,请参见推导

不变量(Invariant)

如果模型准确,而且\hat{\textbf{x}}_{0|0}\textbf{P}_{0|0} 的值准确的反映了最初状态的分布,那么以下不变量就保持不变:所有估计的误差均值为零

  • \textrm{E}[\textbf{x}_k - \hat{\textbf{x}}_{k|k}] = \textrm{E}[\textbf{x}_k - \hat{\textbf{x}}_{k|k-1}] = 0
  • \textrm{E}[\tilde{\textbf{y}}_k] = 0

协方差矩阵 准确的反映了估计的协方差:

  • \textbf{P}_{k|k} = \textrm{cov}(\textbf{x}_k - \hat{\textbf{x}}_{k|k})
  • \textbf{P}_{k|k-1} = \textrm{cov}(\textbf{x}_k - \hat{\textbf{x}}_{k|k-1})
  • \textbf{S}_{k} = \textrm{cov}(\tilde{\textbf{y}}_k)

请注意,其中\textrm{E}[\textbf{a}]表示{a}的期望值,\textrm{cov}(\textbf{a}) = \textrm{E}[\textbf{a}\textbf{a}^T]

 


 

 

代码:

class Kalman:

 
  1. class CV_EXPORTS_W KalmanFilter

  2. {

  3. public:

  4. //! the default constructor

  5. CV_WRAP KalmanFilter();

  6. //! the full constructor taking the dimensionality of the state, of the measurement and of the control vector

  7. CV_WRAP KalmanFilter(int dynamParams, int measureParams, int controlParams=0, int type=CV_32F);

  8. //! re-initializes Kalman filter. The previous content is destroyed.

  9. void init(int dynamParams, int measureParams, int controlParams=0, int type=CV_32F);

  10.  
  11. //! computes predicted state

  12. CV_WRAP const Mat& predict(const Mat& control=Mat());

  13. //! updates the predicted state from the measurement

  14. CV_WRAP const Mat& correct(const Mat& measurement);

  15.  
  16. Mat statePre; //!< predicted state (x'(k)): x(k)=A*x(k-1)+B*u(k)

  17. Mat statePost; //!< corrected state (x(k)): x(k)=x'(k)+K(k)*(z(k)-H*x'(k))

  18. Mat transitionMatrix; //!< state transition matrix (A)

  19. Mat controlMatrix; //!< control matrix (B) (not used if there is no control)

  20. Mat measurementMatrix; //!< measurement matrix (H)

  21. Mat processNoiseCov; //!< process noise covariance matrix (Q)

  22. Mat measurementNoiseCov;//!< measurement noise covariance matrix (R)

  23. Mat errorCovPre; //!< priori error estimate covariance matrix (P'(k)): P'(k)=A*P(k-1)*At + Q)*/

  24. Mat gain; //!< Kalman gain matrix (K(k)): K(k)=P'(k)*Ht*inv(H*P'(k)*Ht+R)

  25. Mat errorCovPost; //!< posteriori error estimate covariance matrix (P(k)): P(k)=(I-K(k)*H)*P'(k)

  26.  
  27. // temporary matrices

  28. Mat temp1;

  29. Mat temp2;

  30. Mat temp3;

  31. Mat temp4;

  32. Mat temp5;

  33. };


sample\kalman.cpp

1个1维点的运动跟踪,虽然这个点是在2维平面中运动,但由于它是在一个圆弧上运动,只有一个自由度,角度,所以还是1维的。还是一个匀速运动,建立匀速运动模型,设定状态变量x = [ x1, x2 ] = [ 角度,角速度 ]

  

 
  1. #include "opencv2/video/tracking.hpp"

  2. #include "opencv2/highgui/highgui.hpp"

  3.  
  4. #include <stdio.h>

  5.  
  6. using namespace cv;

  7.  
  8. static inline Point calcPoint(Point2f center, double R, double angle)

  9. {

  10. return center + Point2f((float)cos(angle), (float)-sin(angle))*(float)R;

  11. }

  12.  
  13. void help()

  14. {

  15. printf( "\nExamle of c calls to OpenCV's Kalman filter.\n"

  16. " Tracking of rotating point.\n"

  17. " Rotation speed is constant.\n"

  18. " Both state and measurements vectors are 1D (a point angle),\n"

  19. " Measurement is the real point angle + gaussian noise.\n"

  20. " The real and the estimated points are connected with yellow line segment,\n"

  21. " the real and the measured points are connected with red line segment.\n"

  22. " (if Kalman filter works correctly,\n"

  23. " the yellow segment should be shorter than the red one).\n"

  24. "\n"

  25. " Pressing any key (except ESC) will reset the tracking with a different speed.\n"

  26. " Pressing ESC will stop the program.\n"

  27. );

  28. }

  29.  
  30. int main(int, char**)

  31. {

  32. help();

  33. Mat img(500, 500, CV_8UC3);

  34. KalmanFilter KF(2, 1, 0);

  35. Mat state(2, 1, CV_32F); /* (phi, delta_phi) */

  36. Mat processNoise(2, 1, CV_32F);

  37. Mat measurement = Mat::zeros(1, 1, CV_32F);

  38. char code = (char)-1;

  39.  
  40. for(;;)

  41. {

  42. randn( state, Scalar::all(0), Scalar::all(0.1) );//随机生成一个矩阵,期望是0,标准差为0.1;

  43. KF.transitionMatrix = *(Mat_<float>(2, 2) << 1, 1, 0, 1);//元素导入矩阵,按行;

  44.  
  45. //setIdentity: 缩放的单位对角矩阵;

  46. //!< measurement matrix (H) 观测模型

  47. setIdentity(KF.measurementMatrix);

  48. //!< process noise covariance matrix (Q)

  49. // wk 是过程噪声,并假定其符合均值为零,协方差矩阵为Qk(Q)的多元正态分布;

  50. setIdentity(KF.processNoiseCov, Scalar::all(1e-5));

  51. //!< measurement noise covariance matrix (R)

  52. //vk 是观测噪声,其均值为零,协方差矩阵为Rk,且服从正态分布;

  53. setIdentity(KF.measurementNoiseCov, Scalar::all(1e-1));

  54. //!< priori error estimate covariance matrix (P'(k)): P'(k)=A*P(k-1)*At + Q)*/ A代表F: transitionMatrix

  55. //预测估计协方差矩阵;

  56. setIdentity(KF.errorCovPost, Scalar::all(1));

  57. //!< corrected state (x(k)): x(k)=x'(k)+K(k)*(z(k)-H*x'(k))

  58. randn(KF.statePost, Scalar::all(0), Scalar::all(0.1));

  59.  
  60. for(;;)

  61. {

  62. Point2f center(img.cols*0.5f, img.rows*0.5f);

  63. float R = img.cols/3.f;

  64. double stateAngle = state.at<float>(0);

  65. Point statePt = calcPoint(center, R, stateAngle);

  66.  
  67. Mat prediction = KF.predict();

  68. double predictAngle = prediction.at<float>(0);

  69. Point predictPt = calcPoint(center, R, predictAngle);

  70.  
  71. randn( measurement, Scalar::all(0), Scalar::all(KF.measurementNoiseCov.at<float>(0)));

  72.  
  73. // generate measurement

  74. measurement += KF.measurementMatrix*state;

  75.  
  76. double measAngle = measurement.at<float>(0);

  77. Point measPt = calcPoint(center, R, measAngle);

  78.  
  79. // plot points

  80. #define drawCross( center, color, d ) \

  81. line( img, Point( center.x - d, center.y - d ), \

  82. Point( center.x + d, center.y + d ), color, 1, CV_AA, 0); \

  83. line( img, Point( center.x + d, center.y - d ), \

  84. Point( center.x - d, center.y + d ), color, 1, CV_AA, 0 )

  85.  
  86. img = Scalar::all(0);

  87. drawCross( statePt, Scalar(255,255,255), 3 );

  88. drawCross( measPt, Scalar(0,0,255), 3 );

  89. drawCross( predictPt, Scalar(0,255,0), 3 );

  90. //line( img, statePt, measPt, Scalar(0,0,255), 3, CV_AA, 0 );

  91. //line( img, statePt, predictPt, Scalar(0,255,255), 3, CV_AA, 0 );

  92.  
  93. KF.correct(measurement);

  94.  
  95. randn( processNoise, Scalar(0), Scalar::all(sqrt(KF.processNoiseCov.at<float>(0, 0))));

  96. state = KF.transitionMatrix*state + processNoise;

  97.  
  98. imshow( "Kalman", img );

  99. code = (char)waitKey(100);

  100.  
  101. if( code > 0 )

  102. break;

  103. }

  104. if( code == 27 || code == 'q' || code == 'Q' )

  105. break;

  106. }

  107.  
  108. return 0;

  109. }

  110.  

鼠标跟踪(详解):

1.初始化状态:

onst int stateNum=4;//状态数,包括(x,y,dx,dy)坐标及速度(每次移动的距离)

const int measureNum=2;//观测量,能看到的是坐标值,当然也可以自己计算速度,但没必要

转移矩阵或者说增益矩阵的值好像有点莫名其妙

 

[cpp] view plain copy

 

  1. float A[stateNum][stateNum] ={//transition matrix  
  2.         1,0,1,0,  
  3.         0,1,0,1,  
  4.         0,0,1,0,  
  5.         0,0,0,1  
  6.     };  

 

 

 

看下图就清楚了

X1=X+dx,依次类推
所以这个矩阵还是很容易却确定的,可以根据自己的实际情况定制转移矩阵

同样的方法,三维坐标的转移矩阵可以如下

 

[cpp] view plain copy

 

  1. float A[stateNum][stateNum] ={//transition matrix  
  2.         1,0,0,1,0,0,  
  3.         0,1,0,0,1,0,  
  4.         0,0,1,0,0,1,  
  5.         0,0,0,1,0,0,  
  6.         0,0,0,0,1,0,  
  7.         0,0,0,0,0,1  
  8.     };  

 

 

当然并不一定得是1和0

 

 

 

KalmanFilter KF(stateNum, measureNum, 0);

//KalmanFilter::KalmanFilter(intdynamParams, intmeasureParams, int controlParams=0, inttype=CV_32F)

 

Parameters:
  • dynamParams – Dimensionality of the state.
  • measureParams – Dimensionality of the measurement.
  • controlParams – Dimensionality of the control vector.
  • type – Type of the created matrices that should beCV_32F orCV_64F.

 

Mat state (stateNum, 1, CV_32FC1);         //  state(x,y,detaX,detaY)

Mat processNoise(stateNum, 1, CV_32F); //  processNoise(x,y,detaX,detaY)

Mat measurement = Mat::zeros(measureNum, 1, CV_32F); //measurement(x,y)

 

需定义的参数矩阵:

F: KF.transitionMatrix

 KF.transitionMatrix = *(Mat_<float>(2, 2) << 1, 0, 1, 0......);

 

H: KF.measurementMatrix:

setIdentity(KF.measurementMatrix);


Qk : KF.processNoiseCov

setIdentity(KF.processNoiseCov, Scalar::all(1e-5));

R: KF.measurementNoiseCov

setIdentity(KF.measurementNoiseCov, Scalar::all(1e-1));

Pk : KF.errorCovPost

setIdentity(KF.errorCovPost, Scalar::all(1));

 

注意:其中: KF.transitionMatrix  :

                                         (1,0,1,0, 
                                          0,1,0,1,
                                          0,0,1,0,
                                          0,0,0,1 );

 

2.预测predict,读出预测的位置

3.更新updata

   3.1.更新观测矩阵:updata/generate measurement

  • 对于鼠标跟踪:直接使用鼠标的实际位置更新,真实位置即为观测位置
  • 对于自动更新:
     
    1. randn( measurement, Scalar::all(0), Scalar::all(KF.measurementNoiseCov.at<float>(0)));

    2.  
    3. // generate measurement

    4. measurement += KF.measurementMatrix*state;

    3.2.更新KF:KF.correct(measurement);

 

分别显示前一状态的位置:Point statePt = Point( (int)KF.statePost.at<float>(0), (int)KF.statePost.at<float>(1));;

预测位置:Point predictPt = Point( (int)prediction.at<float>(0), (int)prediction.at<float>(1));

观测位置(真实位置):mousePosition(由setMouseCallback("Kalman", mouseEvent);得到,递归方式通过measurement计算得到)

 

 
  1. #include <opencv/cv.h>

  2. #include <opencv/highgui.h>

  3.  
  4. #include <iostream>

  5.  
  6. using namespace cv;

  7. using namespace std;

  8.  
  9. const int winWidth = 800;

  10. const int winHeight = 600;

  11.  
  12. Point mousePosition = Point(winWidth>>1, winHeight>>1);

  13.  
  14. //mouse call back

  15. void mouseEvent(int event, int x, int y, int flags, void *param)

  16. {

  17. if(event==CV_EVENT_MOUSEMOVE)

  18. {

  19. mousePosition=Point(x,y);

  20. }

  21. }

  22.  
  23. int main()

  24. {

  25. //1.kalman filter setup

  26. const int stateNum=4;

  27. const int measureNum=2;

  28.  
  29. KalmanFilter KF(stateNum, measureNum, 0);

  30. Mat state (stateNum, 1, CV_32FC1); //state(x,y,detaX,detaY)

  31. Mat processNoise(stateNum, 1, CV_32F);

  32. Mat measurement = Mat::zeros(measureNum, 1, CV_32F); //measurement(x,y)

  33.  
  34.  
  35. randn( state, Scalar::all(0), Scalar::all(0.1) ); //随机生成一个矩阵,期望是0,标准差为0.1;

  36. KF.transitionMatrix = *(Mat_<float>(4, 4) <<

  37. 1,0,1,0,

  38. 0,1,0,1,

  39. 0,0,1,0,

  40. 0,0,0,1 );//元素导入矩阵,按行;

  41.  
  42. //setIdentity: 缩放的单位对角矩阵;

  43. //!< measurement matrix (H) 观测模型

  44. setIdentity(KF.measurementMatrix);

  45.  
  46. //!< process noise covariance matrix (Q)

  47. // wk 是过程噪声,并假定其符合均值为零,协方差矩阵为Qk(Q)的多元正态分布;

  48. setIdentity(KF.processNoiseCov, Scalar::all(1e-5));

  49.  
  50. //!< measurement noise covariance matrix (R)

  51. //vk 是观测噪声,其均值为零,协方差矩阵为Rk,且服从正态分布;

  52. setIdentity(KF.measurementNoiseCov, Scalar::all(1e-1));

  53.  
  54. //!< priori error estimate covariance matrix (P'(k)): P'(k)=A*P(k-1)*At + Q)*/ A代表F: transitionMatrix

  55. //预测估计协方差矩阵;

  56. setIdentity(KF.errorCovPost, Scalar::all(1));

  57.  
  58.  
  59. //!< corrected state (x(k)): x(k)=x'(k)+K(k)*(z(k)-H*x'(k))

  60. //initialize post state of kalman filter at random

  61. randn(KF.statePost, Scalar::all(0), Scalar::all(0.1));

  62. Mat showImg(winWidth, winHeight,CV_8UC3);

  63.  
  64. for(;;)

  65. {

  66. setMouseCallback("Kalman", mouseEvent);

  67. showImg.setTo(0);

  68.  
  69. Point statePt = Point( (int)KF.statePost.at<float>(0), (int)KF.statePost.at<float>(1));

  70.  
  71. //2.kalman prediction

  72. Mat prediction = KF.predict();

  73. Point predictPt = Point( (int)prediction.at<float>(0), (int)prediction.at<float>(1));

  74.  
  75. //3.update measurement

  76. measurement.at<float>(0)= (float)mousePosition.x;

  77. measurement.at<float>(1) = (float)mousePosition.y;

  78.  
  79. //4.update

  80. KF.correct(measurement);

  81.  
  82. //randn( processNoise, Scalar(0), Scalar::all(sqrt(KF.processNoiseCov.at<float>(0, 0))));

  83. //state = KF.transitionMatrix*state + processNoise;

  84. //draw

  85. circle(showImg, statePt, 5, CV_RGB(255,0,0),1);//former point

  86. circle(showImg, predictPt, 5, CV_RGB(0,255,0),1);//predict point

  87. circle(showImg, mousePosition, 5, CV_RGB(0,0,255),1);//ture point

  88.  
  89.  
  90. // CvFont font;//字体

  91. // cvInitFont(&font, CV_FONT_HERSHEY_SCRIPT_COMPLEX, 0.5f, 0.5f, 0, 1, 8);

  92. putText(showImg, "Red: Former Point", cvPoint(10,30), FONT_HERSHEY_SIMPLEX, 1 ,Scalar :: all(255));

  93. putText(showImg, "Green: Predict Point", cvPoint(10,60), FONT_HERSHEY_SIMPLEX, 1 ,Scalar :: all(255));

  94. putText(showImg, "Blue: Ture Point", cvPoint(10,90), FONT_HERSHEY_SIMPLEX, 1 ,Scalar :: all(255));

  95.  
  96. imshow( "Kalman", showImg );

  97. int key = waitKey(3);

  98. if (key == 27)

  99. {

  100. break;

  101. }

  102. }

  103. }

主要资料:

1.维基百科:http://en.wikipedia.org/wiki/Kalman_filter(英文)

    中文:http://zh.wikipedia.org/wiki/%E5%8D%A1%E5%B0%94%E6%9B%BC%E6%BB%A4%E6%B3%A2#.E5.8D.A1.E5.B0.94.E6.9B.BC.E6.BB.A4.E6.B3.A2.E5.99.A8

2.OpenCV文档:http://docs.opencv.org/modules/video/doc/motion_analysis_and_object_tracking.html?highlight=kalman#kalmanfilter

3.博客园kalman详解blog:http://www.cnblogs.com/feisky/archive/2009/12/04/1617287.html

4.CSDN kalman.cpp讲解yangxian:http://blog.csdn.net/yang_xian521/article/details/7050398

5.CSDN 鼠标跟踪:http://blog.csdn.net/onezeros/article/details/6318944

6.模型论文:http://www.cs.unc.edu/~welch/media/pdf/kalman_intro.pdf

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值