OpenCV之YOLOv4 目标检测

6 篇文章 1 订阅
  • 💂 个人主页:风间琉璃
  • 🤟 版权: 本文由【风间琉璃】原创、在CSDN首发、需要转载请联系博主
  • 💬 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)订阅专栏

目录

前言

一、YOLOV4简介

二、预处理

1.获取分类名

2.获取输出层名称

3.图像尺度变换

三、模型加载和推理

四、后处理

五、源码


前言

YOLOv4(You Only Look Once version 4)是计算机视觉领域中的一个物体检测算法,它是 YOLO 系列算法的最新版本之一。YOLOv4 极大地改进了物体检测的性能和准确性,并且在速度和精度之间取得了良好的平衡。

一、YOLOV4简介

yolov4网络结构如下:

yolov4相对于yolov3 spp性能提升的不多,但是相对应yolov3,性能得到了极大的提高。

yolov4基本组成:

1. CBM:Yolov4网络结构中的最小组件,由Conv+Bn+Mish激活函数三者组成。
2. CBL:由Conv+Bn+Leaky_relu激活函数三者组成。
3. Res unit:借鉴Resnet网络中的残差结构,让网络可以构建的更深。
4. CSPX:借鉴CSPNet网络结构,由卷积层和X个Res unint模块Concat组成。
5. SPP:采用1×1,5×5,9×9,13×13的最大池化的方式,进行多尺度融合

 其他操作:

1. Concat:张量拼接,维度会扩充,对应于cfg文件中的route操作。
2. Add:张量相加,不会扩充维度,对应于cfg文件中的shortcut操作。

总得来说,作者将近几年关于深度学习领域最新研究的tricks移植到Yolov4中做验证测试,将yolov3的精度提高了不少。虽然没有全新的创新,但很多改进之处都值得借鉴,借用Yolov4作者的总结 

详细请参考大佬写的笔记:深入浅出Yolo系列之Yolov3&Yolov4&Yolov5&Yolox核心基础知识完整讲解_yolo4理解江大白_江大白*的博客-CSDN博客 

yolov4网络模型和配置文件下载:

yolov4.cfg:https://github.com/AlexeyAB/darknet/blob/master/cfg/yolov4.cfg

yolov4.weights:https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights

coco.names:https://github.com/AlexeyAB/darknet/blob/master/data/coco.names

二、预处理

1.获取分类名

数据集采用的coco数据集,需要将coco.names包含训练模型的所有类名称加载到内存中。

string classsepath = "F:/data/CQU/VS/yolov4/coco.names";


//获取数据集标签
vector<string> getclasssnames(string classnamespath)
{
    ifstream ifs(classnamespath);
    vector<string> names;
    if (ifs.is_open())
    {
        string line;
        while (getline(ifs, line))
        {
            //printf("name:%s\n", line.c_str());
            names.push_back(line);
        }
        
    }
    return names;
}

2.获取输出层名称

YOLOv4由于可以在三个不同的尺度上进行检测,也就是说它有三个输出层,因此我们需要获得它的三个输出层的名称。

//获取未连接的输出层名称
vector<string> getoutputnames(const Net& net)
{
    vector<string>  names = net.getUnconnectedOutLayersNames();
    for (int i = 0; i < names.size(); i++)
    {
        printf("output layer name:%s\n", names.at(i).c_str());
    }
    return names;
}

3.图像尺度变换

神经网络的输入图像需要采用称为blob的特定格式。从输入图像或视频流中读取帧后,将通过blobFromImage函数将其转换为神经网络的输入blob。

在此过程中,它使用比例因子1/255将图像像素值缩放到0到1的目标范围。它还将图像的大小调整为给定大小(416,416)而不进行裁剪。视频处理使用416,小尺寸,帧率会提高一些,图像目标检测是使用608尺寸的。
 

Mat blob = blobFromImage(frame, 1 / 255.0, Size(416, 416));

三、模型加载和推理

加载网络直接使用readNetFromDarknet或者readNet都行。

 //加载网络
 Net net = readNetFromDarknet(config, weights);
 if (net.empty())
 {
     printf("Could not load yolov4....\n");
     return;
 }

这里可以根据个人的情况设置是否使用CUDA加速,我编译过CUDA的OpenCV,所以这里利用第二种方式

    //设置计算平台
#if 0
    //cpu:
    net.setPreferableBackend(DNN_BACKEND_OPENCV);
    net.setPreferableTarget(DNN_TARGET_CPU);

#elif 1
    //cuda
    net.setPreferableBackend(DNN_BACKEND_CUDA);
    net.setPreferableTarget(DNN_TARGET_CUDA_FP16);

#endif

  预处理和网络模型都完成后就可以进行图像的预测。

 net.setInput(blob);

 vector<Mat> outs;
 net.forward(outs, outputnamse);

预测的结果保存在outs的Mat类型矩阵中。接下来就需要对这个预测结果进行后处理。  

四、后处理

yolov4的后处理基本上和yolov3的后处理操作是一样的,具体可以参考yolov3的处理:OpenCV之YOLOv3目标检测_风间琉璃•的博客-CSDN博客

//后处理
float* data;  //存储每一层输出结果
Mat scores;   //存储分类的概率,大小80
vector<Rect> boxes;  //边框信息”x,y,w,h
vector<int> classIds; //存储所有分类索引值
vector<float> confidences;  //置信度存储用于NMS

int centerX, centerY, width, height, left, top;
float confidenceThreshold = 0.5;
float nmsThreashold = 0.4;
double confidence; //获取自信度
Point classIdPoint;//获取分类索引值 

//对每一个输出层找到所有目标以及位置
for (int i = 0; i < outs.size(); i++)
{
    data = (float*)outs[i].data; //分别处理每一层
    for (int j = 0; j < outs[i].rows; j++, data += outs[i].cols)
    {
        //分类概率
        scores = outs[i].row(j).colRange(5, outs[i].cols);
        minMaxLoc(scores, 0, &confidence, 0, &classIdPoint);
        if (confidence > confidenceThreshold)
        {
            centerX = (int)(data[0] * frame.cols);
            centerY = (int)(data[1] * frame.rows);
            width = (int)(data[2] * frame.cols);
            height = (int)(data[3] * frame.rows);
            left = centerX - width / 2;
            top = centerY - height / 2;

            classIds.push_back(classIdPoint.x);
            confidences.push_back((float)confidence);
            boxes.push_back(Rect(left, top, width, height));
        }

    }

}

//非极大值抑制消除重叠度较高但置信度低的边界框
vector<int> indices; //NMSBoxes输出,包含经过非极大值抑制后的所选边界框的索引
NMSBoxes(boxes, confidences, confidenceThreshold, nmsThreashold, indices);
for (int i = 0; i < indices.size(); i++)
{
    int idx = indices[i];
    Rect box = boxes[idx];
    //在图像上绘制检测结果,包括类别标签、置信度和边界框
    drawPred(classesnames, classIds[idx], confidences[idx], box.x, box.y, box.x + box.width, box.y + box.height, frame);
}

//FPS计算
t = ((double)getTickCount() - t) / getTickFrequency();//求输入帧后经过的周期数/每秒系统计的周期数=一帧用时多少秒
//求倒数得到每秒经过多少帧,即帧率
double fps = 1.0 / t; 
string text = format("FPS:%.2f", fps);
cv::putText(frame, text, Point(10, 50), FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0), 2, 8, 0);

imshow("yolov4", frame);
int c = waitKey(1);
if (c == 27)
{
    break;
}

 绘制边界框:

//画预测的目标bounding box
void drawPred(vector<string> classesnames,int classId, float conf, int left, int top, int right, int bottom, Mat& frame)
{
    //获取类别名称及其置信度
    string label = format("%.2f", conf);
    if (!classesnames.empty())
    {
        CV_Assert(classId < (int)classesnames.size());
        label = classesnames[classId] + ":" + label;
    }


    //box 和 text 的颜色
    Scalar rectColor, textColor;
    //在边界框的顶部显示标签
    int baseLine;
    Size labelSize = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
    //确保顶部位置足够高以容纳标签
    top = max(top, labelSize.height);

    // 创建随机数生成器
    random_device rd;
    mt19937 generator(rd());

    // 创建均匀分布对象,范围是1到50
    uniform_int_distribution<int> distribution(1, 80);
    // 生成随机数
    int random_number = distribution(generator);

    //设置颜色
    rectColor = Scalar(random_number * 11 % 256, random_number * 22 % 256, random_number * 33 % 256);
    textColor = Scalar(255 - random_number * 11 % 256, 255 - random_number * 22 % 256, 255 - random_number * 33 % 256);

  
    
    //绘制检测目标的矩形框
    rectangle(frame, Point(left, top), Point(right, bottom), rectColor, 2);
    //文字边框
    rectangle(frame, Point(left, top - round(1.5 * labelSize.height)), Point(left + round(1.5 * labelSize.width), top + baseLine), textColor, FILLED);
    putText(frame, label, Point(left, top), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0), 1);
}

运行结果:

图片:

视频:

YOLOv4:下一代目标检测算法的革命性突破

五、源码

资源下载:https://download.csdn.net/download/qq_53144843/88357523

// yolov4.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <fstream>
#include <random>


#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>

using namespace std;
using namespace cv;
using namespace cv::dnn;


void image_detection(string config, string weights, string classespath, string image_path);
void video_detection(string config, string weights, string classespath, string video_path);


//获取数据集标签
vector<string> getclasssnames(string classnamespath)
{
    ifstream ifs(classnamespath);
    vector<string> names;
    if (ifs.is_open())
    {
        string line;
        while (getline(ifs, line))
        {
            //printf("name:%s\n", line.c_str());
            names.push_back(line);
        }
        
    }
    return names;
}

//获取未连接的输出层名称
vector<string> getoutputnames(const Net& net)
{
    vector<string>  names = net.getUnconnectedOutLayersNames();
    for (int i = 0; i < names.size(); i++)
    {
        printf("output layer name:%s\n", names.at(i).c_str());
    }
    return names;
}



//画预测的目标bounding box
void drawPred(vector<string> classesnames,int classId, float conf, int left, int top, int right, int bottom, Mat& frame)
{
    //获取类别名称及其置信度
    string label = format("%.2f", conf);
    if (!classesnames.empty())
    {
        CV_Assert(classId < (int)classesnames.size());
        label = classesnames[classId] + ":" + label;
    }


    //box 和 text 的颜色
    Scalar rectColor, textColor;
    //在边界框的顶部显示标签
    int baseLine;
    Size labelSize = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
    //确保顶部位置足够高以容纳标签
    top = max(top, labelSize.height);

    // 创建随机数生成器
    random_device rd;
    mt19937 generator(rd());

    // 创建均匀分布对象,范围是1到50
    uniform_int_distribution<int> distribution(1, 80);
    // 生成随机数
    int random_number = distribution(generator);

    //设置颜色
    rectColor = Scalar(random_number * 11 % 256, random_number * 22 % 256, random_number * 33 % 256);
    textColor = Scalar(255 - random_number * 11 % 256, 255 - random_number * 22 % 256, 255 - random_number * 33 % 256);

  
    
    //绘制检测目标的矩形框
    rectangle(frame, Point(left, top), Point(right, bottom), rectColor, 2);
    //文字边框
    rectangle(frame, Point(left, top - round(1.5 * labelSize.height)), Point(left + round(1.5 * labelSize.width), top + baseLine), textColor, FILLED);
    putText(frame, label, Point(left, top), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0), 1);
}


int main()
{
    string config = "F:/data/CQU/VS/yolov4/yolov4.cfg";
    string weights = "F:/data/CQU/VS/yolov4/yolov4.weights";
    string classsepath = "F:/data/CQU/VS/yolov4/coco.names";

    string image_path = "F:/data/CQU/VS/yolov4/dog.jpg";
    string video_path = "F:/data/CQU/VS/yolov4/car.mp4";

    //image_detection(config, weights, classsepath, image_path);
    video_detection(config, weights, classsepath, video_path);


}

void image_detection(string config, string weights, string classespath, string image_path)
{
    //加载网络
    Net net = readNetFromDarknet(config, weights);
    if (net.empty())
    {
        printf("Could not load yolov4....\n");
        return;
    }

    //设置计算平台
#if 1 
    //cpu:
    net.setPreferableBackend(DNN_BACKEND_OPENCV);
    net.setPreferableTarget(DNN_TARGET_CPU);

#elif 0
    //cuda
    net.setPreferableBackend(DNN_BACKEND_CUDA);
    net.setPreferableTarget(DNN_TARGET_CUDA_FP16);

#endif

    //获取标签
    vector<string> classesnames = getclasssnames(classespath);
    //获取输出层名称
    vector<string> outputnamse = getoutputnames(net);

    Mat image = imread(image_path);
    if (image.empty())
    {
        printf("Colud not load image...\n");
        return;
    }

    Mat blob = blobFromImage(image, 1 / 255.0, Size(608, 608));

    net.setInput(blob);
    vector<Mat> outs;
    net.forward(outs, outputnamse);

    //后处理
    float* data;  //存储每一层输出结果
    Mat scores;   //存储分类的概率,大小80
    vector<Rect> boxes;  //边框信息”x,y,w,h
    vector<int> classIds; //存储所有分类索引值
    vector<float> confidences;  //置信度存储用于NMS

    int centerX, centerY, width, height, left, top;
    float confidenceThreshold = 0.2;
    float nmsThreashold = 0.4;
    double confidence; //获取自信度
    Point classIdPoint;//获取分类索引值 

    //对每一个输出层找到所有目标以及位置
    for (int i = 0; i < outs.size(); i++)
    {
        data = (float*)outs[i].data; //分别处理每一层
        for (int j = 0; j < outs[i].rows; j++, data += outs[i].cols)
        {
            //分类概率
            scores = outs[i].row(j).colRange(5, outs[i].cols);
            minMaxLoc(scores, 0, &confidence, 0, &classIdPoint);
            if (confidence > confidenceThreshold)
            {
                centerX = (int)(data[0] * image.cols);
                centerY = (int)(data[1] * image.rows);
                width = (int)(data[2] * image.cols);
                height = (int)(data[3] * image.rows);
                left = centerX - width / 2;
                top = centerY - height / 2;

                classIds.push_back(classIdPoint.x);
                confidences.push_back((float)confidence);
                boxes.push_back(Rect(left, top, width, height));
            }

        }

    }

    //非极大值抑制消除重叠度较高但置信度低的边界框
    vector<int> indices; //NMSBoxes输出,包含经过非极大值抑制后的所选边界框的索引
    NMSBoxes(boxes, confidences, confidenceThreshold, nmsThreashold, indices);
    for (int i = 0; i < indices.size(); i++)
    {
        int idx = indices[i];
        Rect box = boxes[idx];
        //在图像上绘制检测结果,包括类别标签、置信度和边界框
       drawPred(classesnames,classIds[idx], confidences[idx], box.x, box.y, box.x + box.width, box.y + box.height, image);
    }
    imshow("yolov4", image);
    waitKey(0);
}


void video_detection(string config, string weights, string classespath, string video_path)
{
    //加载网络
    Net net = readNetFromDarknet(config, weights);
    if (net.empty())
    {
        printf("Could not load yolov4....\n");
        return;
    }

    //设置计算平台
#if 0
    //cpu:
    net.setPreferableBackend(DNN_BACKEND_OPENCV);
    net.setPreferableTarget(DNN_TARGET_CPU);

#elif 1
    //cuda
    net.setPreferableBackend(DNN_BACKEND_CUDA);
    net.setPreferableTarget(DNN_TARGET_CUDA_FP16);

#endif

    //获取标签
    vector<string> classesnames = getclasssnames(classespath);
    //获取输出层名称
    vector<string> outputnamse = getoutputnames(net);


    VideoCapture capture;
    capture.open(video_path);
    if (!capture.isOpened())
    {
        printf("Colud not open video...\n");
        return;
    }

    Mat frame;
    while (capture.read(frame))
    {
        Mat blob = blobFromImage(frame, 1 / 255.0, Size(416, 416));

        net.setInput(blob);

        //获得当前系统的计时间周期数,求FPS
        double t = (double)getTickCount();

        vector<Mat> outs;
        net.forward(outs, outputnamse);

        //后处理
        float* data;  //存储每一层输出结果
        Mat scores;   //存储分类的概率,大小80
        vector<Rect> boxes;  //边框信息”x,y,w,h
        vector<int> classIds; //存储所有分类索引值
        vector<float> confidences;  //置信度存储用于NMS

        int centerX, centerY, width, height, left, top;
        float confidenceThreshold = 0.5;
        float nmsThreashold = 0.4;
        double confidence; //获取自信度
        Point classIdPoint;//获取分类索引值 

        //对每一个输出层找到所有目标以及位置
        for (int i = 0; i < outs.size(); i++)
        {
            data = (float*)outs[i].data; //分别处理每一层
            for (int j = 0; j < outs[i].rows; j++, data += outs[i].cols)
            {
                //分类概率
                scores = outs[i].row(j).colRange(5, outs[i].cols);
                minMaxLoc(scores, 0, &confidence, 0, &classIdPoint);
                if (confidence > confidenceThreshold)
                {
                    centerX = (int)(data[0] * frame.cols);
                    centerY = (int)(data[1] * frame.rows);
                    width = (int)(data[2] * frame.cols);
                    height = (int)(data[3] * frame.rows);
                    left = centerX - width / 2;
                    top = centerY - height / 2;

                    classIds.push_back(classIdPoint.x);
                    confidences.push_back((float)confidence);
                    boxes.push_back(Rect(left, top, width, height));
                }

            }

        }

        //非极大值抑制消除重叠度较高但置信度低的边界框
        vector<int> indices; //NMSBoxes输出,包含经过非极大值抑制后的所选边界框的索引
        NMSBoxes(boxes, confidences, confidenceThreshold, nmsThreashold, indices);
        for (int i = 0; i < indices.size(); i++)
        {
            int idx = indices[i];
            Rect box = boxes[idx];
            //在图像上绘制检测结果,包括类别标签、置信度和边界框
            drawPred(classesnames, classIds[idx], confidences[idx], box.x, box.y, box.x + box.width, box.y + box.height, frame);
        }

        //FPS计算
        t = ((double)getTickCount() - t) / getTickFrequency();//求输入帧后经过的周期数/每秒系统计的周期数=一帧用时多少秒
        //求倒数得到每秒经过多少帧,即帧率
        double fps = 1.0 / t; 
        string text = format("FPS:%.2f", fps);
        cv::putText(frame, text, Point(10, 50), FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0), 2, 8, 0);

        imshow("yolov4", frame);
        int c = waitKey(1);
        if (c == 27)
        {
            break;
        }
    }
    capture.release();//释放资源
    waitKey(0);
}

结束语
感谢你观看我的文章呐~本次航班到这里就结束啦 🛬

希望本篇文章有对你带来帮助 🎉,有学习到一点知识~

躲起来的星星🍥也在努力发光,你也要努力加油(让我们一起努力叭)。

最后,博主要一下你们的三连呀(点赞、评论、收藏),不要钱的还是可以搞一搞的嘛~

不知道评论啥的,即使扣个666也是对博主的鼓舞吖 💞 感谢 💐

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
需要学习Windows系统YOLOv4的同学请前往《Windows版YOLOv4目标检测实战:原理与源码解析》,课程链接 https://edu.csdn.net/course/detail/29865【为什么要学习这门课】 Linux创始人Linus Torvalds有一句名言:Talk is cheap. Show me the code. 冗谈不够,放码过来!  代码阅读是从基础到提高的必由之路。尤其对深度学习,许多框架隐藏了神经网络底层的实现,只能在上层调包使用,对其内部原理很难认识清晰,不利于进一步优化和创新。YOLOv4是最近推出的基于深度学习的端到端实时目标检测方法。YOLOv4的实现darknet是使用C语言开发的轻型开源深度学习框架,依赖少,可移植性好,可以作为很好的代码阅读案例,让我们深入探究其实现原理。【课程内容与收获】 本课程将解析YOLOv4的实现原理和源码,具体内容包括:- YOLOv4目标检测原理- 神经网络及darknet的C语言实现,尤其是反向传播的梯度求解和误差计算- 代码阅读工具及方法- 深度学习计算的利器:BLAS和GEMM- GPU的CUDA编程方法及在darknet的应用- YOLOv4的程序流程- YOLOv4各层及关键技术的源码解析本课程将提供注释后的darknet的源码程序文件。【相关课程】 除本课程《YOLOv4目标检测:原理与源码解析》外,本人推出了有关YOLOv4目标检测的系列课程,包括:《YOLOv4目标检测实战:训练自己的数据集》《YOLOv4-tiny目标检测实战:训练自己的数据集》《YOLOv4目标检测实战:人脸口罩佩戴检测》《YOLOv4目标检测实战:中国交通标志识别》建议先学习一门YOLOv4实战课程,对YOLOv4的使用方法了解以后再学习本课程。【YOLOv4网络模型架构图】 下图由白勇老师绘制  
要使用OpenCV YOLOv5目标检测,您需要安装OpenCV、NumPy和YOLOv5模型。以下是一些基本步骤: 1. 安装OpenCV和NumPy: ``` pip install opencv-python numpy ``` 2. 下载YOLOv5模型: ``` git clone https://github.com/ultralytics/yolov5.git ``` 3. 加载YOLOv5模型: ```python import cv2 import numpy as np net = cv2.dnn.readNet("yolov5s.pt", "yolov5s.yaml") classes = [] with open("coco.names", "r") as f: classes = [line.strip() for line in f.readlines()] layer_names = net.getLayerNames() output_layers = [layer_names[i[0] - 1] for i in net.getUnconnectedOutLayers()] ``` 4. 运行目标检测: ```python img = cv2.imread("test.jpg") height, width, channels = img.shape blob = cv2.dnn.blobFromImage(img, 0.00392, (416, 416), (0, 0, 0), True, crop=False) net.setInput(blob) outs = net.forward(output_layers) class_ids = [] confidences = [] boxes = [] for out in outs: for detection in out: scores = detection[5:] class_id = np.argmax(scores) confidence = scores[class_id] if confidence > 0.5: center_x = int(detection[0] * width) center_y = int(detection[1] * height) w = int(detection[2] * width) h = int(detection[3] * height) x = int(center_x - w / 2) y = int(center_y - h / 2) boxes.append([x, y, w, h]) confidences.append(float(confidence)) class_ids.append(class_id) indexes = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4) font = cv2.FONT_HERSHEY_SIMPLEX for i in range(len(boxes)): if i in indexes: x, y, w, h = boxes[i] label = str(classes[class_ids[i]]) color = (0, 255, 0) cv2.rectangle(img, (x, y), (x + w, y + h), color, 2) cv2.putText(img, label, (x, y - 5), font, 0.5, color, 2) cv2.imshow("Image", img) cv2.waitKey(0) cv2.destroyAllWindows() ``` 这将加载YOLOv5模型,读取输入图像,运行目标检测并在图像上绘制边界框。您可以将此代码作为脚本运行或将其集成到您的项目中。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Super.Bear

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值