车道检测源码分析系列

http://www.voidcn.com/blog/qq535033459/article/p-1939538.html

车道线检测是辅助驾驶和自动驾驶的重要研究内容,相关的算法研究已经延续了20多年,本系列文章旨在帮助初学者在阅读文献之余快速上手构建自己的项目。OpenCV极大地方便了机器视觉应用的编写,为了能在刚起步时就看得远些,我们需要站在OpenCV巨人的肩膀上。

先看一个比较简单的程序,Github地址

框架

  • class LaneDetect封装核心算法
  • void makeFromVid(string path) 根据视频文件目录读入,对视频中的图像帧进行处理
  • 主函数 调用makeFromVid

核心算法

整个算法的核心在blobRemoval函数中:

  1. findContours函数提取轮廓
  2. 对找到的轮廓进行筛选: 
    1. contourArea轮廓足够大(高于设定的阈值minSize)
    2. minAreaRect拟合矩形的有足够长的边(高于设定的阈值longLane )
    3. 根据矩形的偏角和位置再筛选
  3. 绘制满足条件的轮廓

这个算法非常简单,效果较差。属于利用车道线的基本几何特征和位置的算法。

完整源程序

/*TODO * improve edge linking * remove blobs whose axis direction doesnt point towards vanishing pt * Parallelisation * lane prediction */


#include <opencv2/core/core.hpp>
#include "opencv2/imgproc/imgproc.hpp"
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
#include <vector>
#include <algorithm>
#include <math.h>
#include <time.h>

using namespace std;
using namespace cv;
clock_t start, stop;

class LaneDetect
{
public:
    Mat currFrame; //stores the upcoming frame
    Mat temp;      //stores intermediate results
    Mat temp2;     //stores the final lane segments

    int diff, diffL, diffR;
    int laneWidth;
    int diffThreshTop;
    int diffThreshLow;
    int ROIrows;
    int vertical_left;
    int vertical_right;
    int vertical_top;
    int smallLaneArea;
    int longLane;
    int  vanishingPt;
    float maxLaneWidth;

    //to store various blob properties
    Mat binary_image; //used for blob removal
    int minSize;
    int ratio;
    float  contour_area;
    float blob_angle_deg;
    float bounding_width;
    float bounding_length;
    Size2f sz;
    vector< vector<Point> > contours;
    vector<Vec4i> hierarchy;
    RotatedRect rotated_rect;


    LaneDetect(Mat startFrame)
    {
        //currFrame = startFrame; //if image has to be processed at original size

        currFrame = Mat(320,480,CV_8UC1,0.0);                        //initialised the image size to 320x480
        resize(startFrame, currFrame, currFrame.size());             // resize the input to required size

        temp      = Mat(currFrame.rows, currFrame.cols, CV_8UC1,0.0);//stores possible lane markings
        temp2     = Mat(currFrame.rows, currFrame.cols, CV_8UC1,0.0);//stores finally selected lane marks

        vanishingPt    = currFrame.rows/2;                           //for simplicity right now
        ROIrows        = currFrame.rows - vanishingPt;               //rows in region of interest
        minSize        = 0.00015 * (currFrame.cols*currFrame.rows);  //min size of any region to be selected as lane
        maxLaneWidth   = 0.025 * currFrame.cols;                     //approximate max lane width based on image size
        smallLaneArea  = 7 * minSize;
        longLane       = 0.3 * currFrame.rows;
        ratio          = 4;

        //these mark the possible ROI for vertical lane segments and to filter vehicle glare
        vertical_left  = 2*currFrame.cols/5;
        vertical_right = 3*currFrame.cols/5;
        vertical_top   = 2*currFrame.rows/3;

        namedWindow("lane",2);
        namedWindow("midstep", 2);
        namedWindow("currframe", 2);
        namedWindow("laneBlobs",2);

        getLane();
    }

    void updateSensitivity()
    {
        int total=0, average =0;
        for(int i= vanishingPt; i<currFrame.rows; i++)
            for(int j= 0 ; j<currFrame.cols; j++)
                total += currFrame.at<uchar>(i,j);
        average = total/(ROIrows*currFrame.cols);
        cout<<"average : "<<average<<endl;
    }

    void getLane()
    {
        //medianBlur(currFrame, currFrame,5 );
        // updateSensitivity();
        //ROI = bottom half
        for(int i=vanishingPt; i<currFrame.rows; i++)
            for(int j=0; j<currFrame.cols; j++)
            {
                temp.at<uchar>(i,j)    = 0;
                temp2.at<uchar>(i,j)   = 0;
            }

        imshow("currframe", currFrame);
        blobRemoval();
    }

    void markLane()
    {
        for(int i=vanishingPt; i<currFrame.rows; i++)
        {
            //IF COLOUR IMAGE IS GIVEN then additional check can be done
            // lane markings RGB values will be nearly same to each other(i.e without any hue)

            //min lane width is taken to be 5
            laneWidth =5+ maxLaneWidth*(i-vanishingPt)/ROIrows;
            for(int j=laneWidth; j<currFrame.cols- laneWidth; j++)
            {

                diffL = currFrame.at<uchar>(i,j) - currFrame.at<uchar>(i,j-laneWidth);
                diffR = currFrame.at<uchar>(i,j) - currFrame.at<uchar>(i,j+laneWidth);
                diff  =  diffL + diffR - abs(diffL-diffR);

                //1 right bit shifts to make it 0.5 times
                diffThreshLow = currFrame.at<uchar>(i,j)>>1;
                //diffThreshTop = 1.2*currFrame.at<uchar>(i,j);

                //both left and right differences can be made to contribute
                //at least by certain threshold (which is >0 right now)
                //total minimum Diff should be atleast more than 5 to avoid noise
                if (diffL>0 && diffR >0 && diff>5)
                    if(diff>=diffThreshLow /*&& diff<= diffThreshTop*/ )
                        temp.at<uchar>(i,j)=255;
            }
        }

    }

    void blobRemoval()
    {
        markLane();

        // find all contours in the binary image
        temp.copyTo(binary_image);
        findContours(binary_image, contours,
                     hierarchy, CV_RETR_CCOMP,
                     CV_CHAIN_APPROX_SIMPLE);

        // for removing invalid blobs
        if (!contours.empty())
        {
            for (size_t i=0; i<contours.size(); ++i)
            {
                //====conditions for removing contours====//

                contour_area = contourArea(contours[i]) ;

                //blob size should not be less than lower threshold
                if(contour_area > minSize)
                {
                    rotated_rect    = minAreaRect(contours[i]);
                    sz              = rotated_rect.size;
                    bounding_width  = sz.width;
                    bounding_length = sz.height;


                    //openCV selects length and width based on their orientation
                    //so angle needs to be adjusted accordingly
                    blob_angle_deg = rotated_rect.angle;
                    if (bounding_width < bounding_length)
                        blob_angle_deg = 90 + blob_angle_deg;

                    //if such big line has been detected then it has to be a (curved or a normal)lane
                    if(bounding_length>longLane || bounding_width >longLane)
                    {
                        drawContours(currFrame, contours,i, Scalar(255), CV_FILLED, 8);
                        drawContours(temp2, contours,i, Scalar(255), CV_FILLED, 8);
                    }

                    //angle of orientation of blob should not be near horizontal or vertical
                    //vertical blobs are allowed only near center-bottom region, where centre lane mark is present
                    //length:width >= ratio for valid line segments
                    //if area is very small then ratio limits are compensated
                    else if ((blob_angle_deg <-10 || blob_angle_deg >-10 ) &&
                             ((blob_angle_deg > -70 && blob_angle_deg < 70 ) ||
                              (rotated_rect.center.y > vertical_top &&
                               rotated_rect.center.x > vertical_left && rotated_rect.center.x < vertical_right)))
                    {

                        if ((bounding_length/bounding_width)>=ratio || (bounding_width/bounding_length)>=ratio
                                ||(contour_area< smallLaneArea &&  ((contour_area/(bounding_width*bounding_length)) > .75) &&
                                   ((bounding_length/bounding_width)>=2 || (bounding_width/bounding_length)>=2)))
                        {
                            drawContours(currFrame, contours,i, Scalar(255), CV_FILLED, 8);
                            drawContours(temp2, contours,i, Scalar(255), CV_FILLED, 8);
                        }
                    }
                }
            }
        }
        imshow("midstep", temp);
        imshow("laneBlobs", temp2);
        imshow("lane",currFrame);
    }


    void nextFrame(Mat &nxt)
    {
        //currFrame = nxt; //if processing is to be done at original size

        resize(nxt ,currFrame, currFrame.size()); //resizing the input image for faster processing
        getLane();
    }

    Mat getResult()
    {
        return temp2;
    }

};//end of class LaneDetect


void makeFromVid(string path)
{
    Mat frame;
    VideoCapture cap(path); // open the video file for reading

    if ( !cap.isOpened() )  // if not success, exit program
        cout << "Cannot open the video file" << endl;

    //cap.set(CV_CAP_PROP_POS_MSEC, 300); //start the video at 300ms

    double fps = cap.get(CV_CAP_PROP_FPS); //get the frames per seconds of the video
    cout << "Input video's Frame per seconds : " << fps << endl;

    cap.read(frame);
    LaneDetect detect(frame);

    while(1)
    {
        bool bSuccess = cap.read(frame); // read a new frame from video
        if (!bSuccess)                   //if not success, break loop
        {
            cout << "Cannot read the frame from video file" << endl;
            break;
        }

        cvtColor(frame, frame, CV_BGR2GRAY);

        //start = clock();
        detect.nextFrame(frame);
        //stop =clock();
        // cout<<"fps : "<<1.0/(((double)(stop-start))/ CLOCKS_PER_SEC)<<endl;

        if(waitKey(10) == 27) //wait for 'esc' key press for 10 ms. If 'esc' key is pressed, break loop
        {
            cout<<"video paused!, press q to quit, any other key to continue"<<endl;
            if(waitKey(0) == 'q')
            {
                cout << "terminated by user" << endl;
                break;
            }
        }
    }
}

int main()
{
    makeFromVid("/home/yash/opencv-2.4.10/programs/output.avi");
    // makeFromVid("/home/yash/opencv-2.4.10/programs/road.m4v");
    waitKey(0);
    destroyAllWindows();
}

本节分析一个国人开发的简单的车道检测程序(不涉及跟踪)

概述

  • 采用opencv2编写 C++
  • 算法核心步骤 
    1. 提取车道标记特征,封装在laneExtraction类中
    2. 车道建模(两条边,单车道),封装在laneModeling类中
  • 对于断断续续的斑点车道标记(虚线)使用RANSAC匹配直线,对每幅图像,检测结果可能是感兴趣的左右车道都检测到或者全都没检测到

主程序框架

track.cpp 
主程序依次读入源文件夹下的图片,进行处理后输出到目标文件夹。由于图片上有固定的遮挡物,所以添加了mask进行去除。 
构建laneExtraction类的实例,设置参数路宽,添加mask去除固定遮挡物的干扰,然后调用getLaneCandidates得到备选车道线。

/* * extract line segments */
        aps::laneExtraction le;
        le.setLaneWidth(15);
        le.compute(img_org, carmask / 255);
        std::vector<aps::line> laneSegCandidates = le.getLaneCandidates();

构建laneModeling类的实例,对备选车道线参数化后,验证左右车道是否检测到,如果检测到则标记线段。

/* * model the lane */
        aps::laneModeling lm;
        lm.compute(laneSegCandidates);

        std::vector<aps::line> lanes = lm.getLanes();

        cv::cvtColor(img_org, img_org, CV_GRAY2BGR);
        ofs << fname << ",";
        if (lm.verifyLanes())
        {
            cv::line(img_org, lanes[0].getEndPoint1(), lanes[0].getEndPoint2(),
                    cv::Scalar(0, 255, 255), 5, CV_AA);
            cv::line(img_org, lanes[1].getEndPoint1(), lanes[1].getEndPoint2(),
                    cv::Scalar(0, 255, 255), 5, CV_AA);
            int x_left =
                    (lanes[0].getEndPoint2().x < lanes[1].getEndPoint2().x) ?
                            lanes[0].getEndPoint2().x :
                            lanes[1].getEndPoint2().x;
            int x_right =
                    (lanes[0].getEndPoint2().x > lanes[1].getEndPoint2().x) ?
                            lanes[0].getEndPoint2().x :
                            lanes[1].getEndPoint2().x;

            ofs << x_left << "," << x_right << std::endl;
        }
        else
        {
            ofs << "NONE,NONE\n";
        }

车道标记特征提取

laneExtraction.cpp

先看类的内部方法检测车道标记,输出结果是一个特征图:关键就是一个源图像点到特征图点的计算公式。

void
    laneExtraction::detectMarkers(const cv::Mat img, int tau)
    {
        m_IMMarker.create(img.rows, img.cols, CV_8UC1);
        m_IMMarker.setTo(0);

        int aux = 0;
        for (int j = 0; j < img.rows; j++)
        {
            for (int i = tau; i < img.cols - tau; i++)
            {
                if (img.at<uchar>(j, i) != 0)
                {
                    aux = 2 * img.at<uchar>(j, i);
                    aux += -img.at<uchar>(j, i - tau);
                    aux += -img.at<uchar>(j, i + tau);

                    aux += -abs(
                            (int) (img.at<uchar>(j, i - tau)
                                    - img.at<uchar>(j, i + tau)));

                    aux = (aux < 0) ? (0) : (aux);
                    aux = (aux > 255) ? (255) : (aux);

                    m_IMMarker.at<uchar>(j, i) = (unsigned char) aux;
                }
            }
        }
        return;
    }

得到特征图后,去掉mask固定遮挡物部分,然后通过固定阈值进行二值化,得到二值图,再通过概率hough变换检测直线,最后遍历检测到的直线,如果检测到的直线偏角在某个固定范围内,就算作备选车道线。

void
    laneExtraction::compute(const cv::Mat img, const cv::Mat mask)
    {
        detectMarkers(img, m_laneWidth);

        m_IMMarker = m_IMMarker.mul(mask);
        cv::threshold(m_IMMarker, m_IMMarker, 25, 255, CV_THRESH_BINARY);

        std::vector<cv::Vec4i> lines;
        cv::HoughLinesP(m_IMMarker, lines, 1, CV_PI / 180, 40, 20, 10);

        for (size_t i = 0; i < lines.size(); i++)
        {
            cv::Vec4i l = lines[i];
            aps::line seg;
            seg.set(cv::Point(l[0], l[1]), cv::Point(l[2], l[3]));

            if ((seg.getOrientation() > 10 && seg.getOrientation() < 80)
                    || (seg.getOrientation() > 100 && seg.getOrientation() < 170))
            {
                m_laneSegCandid.push_back(seg);
            }
        }
        return;
    }

车道建模

下面看建模部分:laneModeling类 
如果检测到的直线不够2条,则返回失败。对检测到的直线通过kmeans分类方法分成两类,分类依据是方向信息。 
lines–>lanes 
收集各个类别的直线上的点并用RANSAC拟合成直线,得到最终的车道线,如果得到的lanes少于2条,则失败。

void
    laneModeling::compute(const std::vector<aps::line>& lines)
    {
        int clusterCount = 2;
        if (lines.empty() || lines.size() < clusterCount)
        {
            m_success = false;
            return;
        }

        cv::Mat points(lines.size(), 1, CV_32FC1);
        cv::Mat labels;
        cv::Mat centers(clusterCount, 1, CV_32FC1);

        for (size_t i = 0; i < lines.size(); i++)
        {
            points.at<float>(i, 0) = lines[i].getOrientation();
        }

        /* * put all line candidates into two clusters based on their orientations */
        cv::kmeans(points, clusterCount, labels,
                cv::TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 10, 1.0),
                3, cv::KMEANS_PP_CENTERS, centers);

        for (int k = 0; k < clusterCount; k++)
        {
            std::vector<cv::Point> candid_points;

            for (size_t i = 0; i < lines.size(); i++) // 遍历所有备选直线
            {
                if (labels.at<int>(i, 0) == k) // 如果该直线属于当前的类别K
                {
                    std::vector<cv::Point> pts = lines[i].interpolatePoints(10);
                    candid_points.insert(candid_points.end(), pts.begin(),
                            pts.end()); // 将直线上的点收集起来,用于RANSAC
                }
            }

            if (candid_points.empty())
                continue; // 如果没有收集到,则继续看下一类

            /* * fit the perfect line */
            aps::LineModel model;
            aps::RansacLine2D Ransac;
            Ransac.setObservationSet(candid_points);
            Ransac.setIterations(500);
            Ransac.setRequiredInliers(2);
            Ransac.setTreshold(1);
            Ransac.computeModel();
            Ransac.getBestModel(model);

            double m = model.mSlope, b = model.mIntercept;
            if (fabs(m) <= 1e-10 && fabs(b) <= 1e-10)
                continue;

            cv::Point p1((700 - b) / (m + 1e-5), 700);
            cv::Point p2((1200 - b) / (m + 1e-5), 1200);
            aps::line laneSide;
            laneSide.set(p1, p2);
            lanes.push_back(laneSide);
        }

        if (lanes.size() < 2)
        {
            m_success = false;
        }
        else
        {
            m_success = true;
        }

        return;
    }

对车道最后的验证步骤比较简单,如果得到的两条车道线方向近似,则失败;如果相交,则失败。laneModeling::verifyLanes()部分代码略。

线段操作的封装

封装在line类里

  • set 根据两个点的坐标指定一条线段
  • is_Set 返回线段是否已经通过set设定
  • getOrientation 返回线段方向
  • getEndPoint1,getEndPoint2返回两个端点
  • getLength 返回线段长度
  • is_Headup 端点1的纵坐标比端点2的纵坐标小?
  • getDistFromLine 线段和线段的距离
  • point2Line 点到线段的距离
  • interpolatePoints 返回线段上的点

抛物线掩码

parabolicMask用作去除图像上不必要的噪声,包括固定遮挡物和抛物线划分的ROI

/* * create the mask that can help remove unwanted noise */
    cv::Mat carmask = cv::imread("carmask.png", CV_LOAD_IMAGE_GRAYSCALE);
    aps::parabolicMask pmask(carmask.cols, carmask.rows,
            1.0 / carmask.rows);
    cv::Mat M = pmask.mkMask();
    M.convertTo(M, CV_8UC1);
    carmask.convertTo(carmask, CV_8UC1);
    carmask = carmask.mul(M);

RANSAC拟合直线

封装在LineModel类里面, 
关于RANSAC的讲解见 随机抽样一致性算法(RANSAC)

RANSAC是“RANdom SAmple Consensus(随机抽样一致)”的缩写。它可以从一组包含“局外点”的观测数据集中,通过迭代方式估计数学模型的参数。它是一种不确定的算法——它有一定的概率得出一个合理的结果;为了提高概率必须提高迭代次数。该算法最早由Fischler和Bolles于1981年提出。

这里不再展开。

总结

该程序的结构较好,对象设计合理,提供了一个适合扩展的车道检测框架。


下面看一个出自国外某教授的基于OpenCVSharp的车辆视觉框架,虽然包含车道检测和车辆识别,但源程序仅100行左右,非常精巧。

算法描述

检测部分的步骤很经典,是比较通用的步骤:

  1. 将图像上半部分无关内容截掉,设定ROI
  2. 转为灰度图
  3. Canny算子提取边缘
  4. Hough检测直线
  5. 通过直线角度位置信息筛选出车道线,标记到图上

效果如下图:

源程序

有博客将其中车道检测部分转为了OpenCV2,以下的源码转自该博客。原文链接

另外,国外有基于此程序进行扩展的,后续会介绍。

OpenCVSharp版本

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

using OpenCvSharp;

namespace LaneDetection
{
    class Program
    {
        [STAThread]
        static void Main()
        {
            CvCapture cap = CvCapture.FromFile("test1.mp4");
            CvWindow w = new CvWindow("Lane Detection");
            CvWindow canny = new CvWindow("Canny");
            IplImage src, gray, dstCanny, halfFrame, smallImg;
            CvMemStorage storage = new CvMemStorage();
            CvSeq lines;

            while (CvWindow.WaitKey(10) < 0)
            {
                src = cap.QueryFrame();
                halfFrame = new IplImage(new CvSize(src.Size.Width / 2, src.Size.Height / 2), BitDepth.U8, 3);
                Cv.PyrDown(src, halfFrame, CvFilter.Gaussian5x5);

                gray = new IplImage(src.Size, BitDepth.U8, 1);
                dstCanny = new IplImage(src.Size, BitDepth.U8, 1);
                storage.Clear();

                // Crop off top half of image since we're only interested in the lower portion of the video
                int halfWidth = src.Width / 2;
                int halfHeight = src.Height / 2;
                int startX = halfWidth - (halfWidth / 2);
                src.SetROI(new CvRect(0, halfHeight - 0, src.Width - 1, src.Height - 1));

                gray.SetROI(src.GetROI());
                dstCanny.SetROI(src.GetROI());

                src.CvtColor(gray, ColorConversion.BgrToGray);
                Cv.Smooth(gray, gray, SmoothType.Gaussian, 5, 5);
                Cv.Canny(gray, dstCanny, 50, 200, ApertureSize.Size3);
                canny.Image = dstCanny;
                storage.Clear();
                lines = dstCanny.HoughLines2(storage, HoughLinesMethod.Probabilistic, 1, Math.PI / 180, 50, 50, 100);

                for (int i = 0; i < lines.Total; i++)
                {
                    CvLineSegmentPoint elem = lines.GetSeqElem<CvLineSegmentPoint>(i).Value;

                    int dx = elem.P2.X - elem.P1.X;
                    int dy = elem.P2.Y - elem.P1.Y;
                    double angle = Math.Atan2(dy, dx) * 180 / Math.PI;

                    if (Math.Abs(angle) <= 10)
                        continue;

                    if (elem.P1.Y > elem.P2.Y + 50  || elem.P1.Y < elem.P2.Y -50 )
                    {
                        src.Line(elem.P1, elem.P2, CvColor.Red, 2, LineType.AntiAlias, 0);
                    }
                }
                src.ResetROI();
                storage.Clear();
                w.Image = src;
            }
        }
    }
}

OpenCV2版本

#include "stdafx.h"
#include <highgui.h>
#include <cv.h>
#include <math.h>

using namespace cv;
using namespace std;

#define INF 99999999
CvCapture* g_capture = NULL;

int g_slider_pos = 0 ;
int frame_count = 0;
CvSeq* lines;


int main(int argc,char* argv[])
{                  
    cvNamedWindow( "show");      
    g_capture=cvCreateFileCapture( "D:\\road.avi");
    IplImage* frame;
    while(1)
    {  
        CvMemStorage* storage = cvCreateMemStorage();
        frame=cvQueryFrame(g_capture);

        //set the ROI of the original image
        int x = 0,y = frame->height/2;
        int width = frame->width,height = frame->height/2;

        if(!frame) 
            break; 

        cvSetImageROI(frame,cvRect(x,y,width,height));
        IplImage* gray = cvCreateImage(cvGetSize(frame),8,1);
        cvCvtColor(frame,gray,CV_BGR2GRAY);

        cvCanny(gray,gray,50,100);
        cvShowImage("canny",gray);
        cvSmooth(gray,gray,CV_GAUSSIAN,3,1,0);

        //Hough
        lines = cvHoughLines2(gray,storage,CV_HOUGH_PROBABILISTIC,1,CV_PI/180,50,90,50);

        //select approprivate lines acoording to the slope
        for (int i = 0;i < lines->total;i ++)
        {
            double k =INF;
            CvPoint* line = (CvPoint*)cvGetSeqElem(lines,i);
            int dx = line[1].x - line[0].x;
            int dy = line[1].x - line[0].y;
            double angle = atan2(dy,dx) * 180 /CV_PI;
            if (abs(angle) <= 10)
                continue;
            if (line[0].y > line[1].y + 50 || line[0].y < line[1].y - 50)
            {
                cvLine(frame,line[0],line[1],CV_RGB(255,0,0),2,CV_AA);
            }
        }
        cvResetImageROI(frame);     
        cvShowImage( "show",frame);
        char c = cvWaitKey(33);            
        if(c==27)
            break;
    } 
    cvReleaseCapture(&g_capture);
    cvDestroyWindow( "show");               
    return 0;
}

参考资料




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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值