《图像处理实例》 之 答题卡检测

提前说明一下:这是“禾路”老师博客上的一个例子,老师在51cto上有课程,大家如果需要可以去看一下http://edu.51cto.com/lecturer/8887491.html

本博文是参考老师的教程,自己消化理解之后进行了部分代码的改进,发表未经原作者允许,如果有侵犯版权请告知立马删除!

 


 目标:检测以下相机没有拍摄好的答题卡:

第一步:定位点检测

        从上图可以看到四个黑圆圈,这个就是定位用的四个角,我们检测这四个角就可以进行答题卡的定位:

          方法一:利用霍夫圆变换,进行圆心的查找。

          方法二:轮廓区域检测

          方法三:模板匹配

          方法四:特征检测匹配

        本文利用方法三的模板匹配,其它方法完全可行的,如果不知道其它方法可以看看我的其它博文,都有例子。

 

--------->>>>模板匹配的黑点截取出来(照一张好的图片去测量截取)

 

 

 

 

上代码:此代码都是原作者发表过的,版权的代码不会发表

 

 1 //--------------------------------注释代码部分为未用掩码操作--------------------------//
 2 void FindAnchorPoint(const Mat& src,const Mat& matchMask,vector<Point2f>& anchorPoint)
 3 {
 4     Mat matchResult;
 5     matchResult.create(Size(src.cols - matchMask.cols, src.rows - matchMask.rows), CV_16SC1);
 6     //----模板匹配找四个定位点,同时得归一化(初始数据范围太大,自己通过image watch 查看)
 7     matchTemplate(src, matchMask, matchResult, TM_CCOEFF, Mat());
 8     normalize(matchResult, matchResult, 0, 1, NORM_MINMAX);
 9     //----查找匹配的四个点,分成四个区域查找,因为一个区域没办法查找四个值
10     /*Mat topleft  = matchResult(Rect(Point(0, 0), Point(matchResult.cols / 2, matchResult.rows / 2)));
11     Mat topright = matchResult(Rect(Point(matchResult.cols / 2, 0), Point(matchResult.cols, matchResult.rows / 2)));
12     Mat botleft  = matchResult(Rect(Point(0, matchResult.rows / 2), Point(matchResult.cols / 2, matchResult.rows)));
13     Mat botright = matchResult(Rect(Point(matchResult.cols / 2, matchResult.rows / 2), Point(matchResult.cols , matchResult.rows)));*/
14     double maxValue[4] = { 0 }, minValue[4] = {0};
15     vector<Point2i> maxPoint(4), minPoint(4);
16     Mat topleftMask  = Mat::zeros(matchResult.size(), CV_8UC1);
17     Mat toprightMask = Mat::zeros(matchResult.size(), CV_8UC1);
18     Mat botleftMask  = Mat::zeros(matchResult.size(), CV_8UC1);
19     Mat botrightMask = Mat::zeros(matchResult.size(), CV_8UC1);
20     topleftMask(Rect(Point(0, 0), Point(matchResult.cols / 2, matchResult.rows / 2))).setTo(255);
21     toprightMask(Rect(Point(matchResult.cols / 2, 0), Point(matchResult.cols, matchResult.rows / 2))).setTo(255);
22     botleftMask(Rect(Point(0, matchResult.rows / 2), Point(matchResult.cols / 2, matchResult.rows))).setTo(255);
23     botrightMask(Rect(Point(matchResult.cols / 2, matchResult.rows / 2), Point(matchResult.cols, matchResult.rows))).setTo(255);
24     vector<Mat> vectorMask;//注意此处如果用vector<Mat> vectorMask(4);对应的下面写法是vectorMask[0]=topleftMask;
25     vectorMask.push_back(topleftMask);
26     vectorMask.push_back(toprightMask);
27     vectorMask.push_back(botleftMask);
28     vectorMask.push_back(botrightMask);
29     for (size_t i = 0; i < vectorMask.size(); i++)
30     {
31         minMaxLoc(matchResult, &minValue[i], &maxValue[i], &minPoint[i], &maxPoint[i], vectorMask[i]);
32     }
33     //minMaxLoc(topleft, &minValue[0], &maxValue[0], &minPoint[0], &maxPoint[0]);
34     //minMaxLoc(topright, &minValue[1], &maxValue[1], &minPoint[1], &maxPoint[1]);
35     //maxPoint[1].x = maxPoint[1].x + matchResult.cols / 2;
36     //minMaxLoc(botleft,  &minValue[2], &maxValue[2], &minPoint[2], &maxPoint[2]);
37     //maxPoint[2].y = maxPoint[2].y + matchResult.rows / 2;
38     //minMaxLoc(botright, &minValue[3], &maxValue[3], &minPoint[3], &maxPoint[3]);
39     //maxPoint[3].x = maxPoint[3].x + matchResult.cols / 2;
40     //maxPoint[3].x = maxPoint[3].y + matchResult.rows / 2;
41 }

结果图片:

 

 

 第二步:定位线检测

 

        定位线:每一个涂卡区域都是由X、Y两个轴共同定位。

由以上的分析可知,我们这一步的操作是找到这些定位线,再由这些定位线去找每个涂卡区的坐标。

------>>>>>裁剪定位上下左右四个区域,利用投影算法找出定位线。

                                              

 

 

 

 

 

上代码:

 1 //------------------------------------图像投影算法-----------------------------------------------//
 2 //*************@src------------------输入矩阵为单通道********************************************//
 3 //*************@leftUpJumpWave-------上升跳变沿存储**********************************************//
 4 //*************@rightDownJumpWave----下降跳变沿存储**********************************************//
 5 //*************@maxInterval----------允许高电平(像素)最大间隔,也可以说是允许的最大误差********//
 6 //-----------------------------------------------------------------------------------------------//
 7 void projectionAlgorithm(Mat src,vector<int>& UpJumpWave,vector<int>& DownJumpWave,bool Axis,int maxInterval)
 8 {
 9     vector<int> pixNum(src.rows > src.cols ? src.rows : src.cols);
10     //------对X、Y做直方图类似的投影,统计一行或者一列的非零个数--------//
11     if (Axis)
12     {
13         for (size_t i = 0; i < src.cols; i++)
14         {
15             Mat col = src.col(i);//一列数据
16             pixNum[i] = countNonZero(col) > 1 ? countNonZero(col) : 0;
17         }
18     }
19     else
20     {
21         
22         for (size_t i = 0; i < src.rows; i++)
23         {
24             Mat row = src.row(i);//一行数据
25             pixNum[i] = countNonZero(row) > 1 ? countNonZero(row) : 0;
26         }
27     }
28     if (pixNum.size() < maxInterval) return;//防止有空洞(实际没见过,如果有的话那程序架构会奔溃了)
29     //-----对上面的数据进行二值化0-1,同时对于不满足maxInterval的数据进行剔除--------//
30     for (int k = 1; k < pixNum.size()-maxInterval; k++)//去除了第一个和最后一个像素
31     {
32         if (pixNum[k] > 0 && pixNum[k + maxInterval] > 0)
33         {
34             for (size_t j = k; j < k + maxInterval; j++)
35             {
36                 pixNum[j] = 1;
37             }
38             k = k + maxInterval-1;
39         }
40         else
41         {
42             pixNum[k] = 0;
43         }
44     }
45     //----对跳变的电平进行存储,高->低,低->高,-----//
46     for (size_t i = 1 ; i < pixNum.size()-2; i++)//去除了第一个和最后一个像素
47     {
48         if (pixNum[i] == 0 && pixNum[i + 1] == 1) UpJumpWave.push_back(i);
49         if (pixNum[i] == 1 && pixNum[i + 1] == 0) DownJumpWave.push_back(i);
50     }
51     //----对得到的结果进行处理,定位点被误判----//
52     vector<int>::iterator begin = UpJumpWave.begin();    
53     if (UpJumpWave[0] < 15)                    UpJumpWave.erase(begin);
54     vector<int>::iterator end = UpJumpWave.end()-1;
55     if (UpJumpWave[UpJumpWave.size()-1] > 330) UpJumpWave.erase(end);
56 }

 

第三步:检测涂卡区域的状态

        这一步是我自己写的,没有参考别人程序,如果有错误的地方请不吝指教!

        思路:找到检测的点,然后利用非零区域进行判断,想法很简单但是实现完全实现很多小技巧,具体看代码。

 上代码:

 

 1 //-----------------------------------------------------------------------------------//
 2 //************************************检测涂卡区域函数***********************************//
 3 void checkKeypoint(Mat& _src,vector<Point2f>& allPoint,vector<Point2f>& testkeyPoint)
 4 {
 5     Mat src = _src.clone();
 6     Mat show = Mat::zeros(src.size(), CV_8UC3);
 7     morphologyEx(src, src, MORPH_DILATE, Mat::ones(3, 3, CV_8UC1));
 8     for (size_t i = 0; i < allPoint.size(); i++)
 9     {
10         //------判断检测点的正方形涂卡区的非零个数---------//
11         if (allPoint[i].x == 0 || allPoint[i].y == 0)
12         {
13             allPoint[i].x += 1;
14             allPoint[i].y += 1;
15         }
16         Mat rec = src(Rect(static_cast<int>(allPoint[i].x - 1 ), static_cast<int>(allPoint[i].y - 1 ), 12, 5));
17         int count = countNonZero(rec);
18         if (count > 15)
19         {
20             testkeyPoint.push_back(allPoint[i]);
21             rectangle(show, Rect(allPoint[i], Point(allPoint[i].x + 13, allPoint[i].y + 5)), Scalar(0, 0, 255));
22         }
23     }
24 }

 

 

 整体代码:(再次申明:核心是参考禾路老师的,细节处理和部分代码是自己加的,如有侵权请告知,立马删除)

  1 #include <opencv2/opencv.hpp>
  2 #include <iostream>
  3 #include "math.h"
  4 using namespace cv;
  5 using namespace std;
  6 
  7 #if 1
  8 const bool X_Axis = true;
  9 const bool Y_Axis = false;
 10 
 11 void FindAnchorPoint(const Mat& src, const Mat& matchMask, vector<Point2f>& anchorPoint);
 12 void projectionAlgorithm(Mat src, vector<int>& UpJumpWave, vector<int>& DownJumpWave, bool Axis, int maxInterval);
 13 void checkKeypoint( Mat& _src, vector<Point2f>& allPoint, vector<Point2f>& testkeyPoint);
 14 int main(int argc,char** argv)
 15 {
 16     //变量
 17     //读取图片
 18     Mat standImage = imread("SheetStand.jpg");
 19     Mat perImage   = imread("perspective3.bmp");
 20     //Mat perImage = imread("perspective.jpg");
 21     //Mat matchMask  = imread("Circle.jpg");
 22     //-----------生成模板图片R = 11
 23     Mat matchMask;
 24     matchMask.create(Size(24, 24), CV_8UC3);
 25     matchMask.setTo(255);
 26     circle(matchMask, Point(11, 11), 11, Scalar(0), -1);
 27 
 28     resize(perImage, perImage, Size(600, 600));
 29     vector<Point2f> stdAncherPoint(4);
 30     vector<Point2f> perAncherPoint(4);
 31     FindAnchorPoint(standImage, matchMask, stdAncherPoint);
 32     FindAnchorPoint(perImage,   matchMask, perAncherPoint);
 33     Mat change = getPerspectiveTransform(perAncherPoint, stdAncherPoint);
 34     Mat resultPerImage;
 35     warpPerspective(perImage, resultPerImage, change, resultPerImage.size());
 36     FindAnchorPoint(resultPerImage, matchMask, perAncherPoint);
 37 
 38     Mat grayImage = resultPerImage;//.clone();
 39     Mat show = resultPerImage.clone();
 40     cvtColor(grayImage, grayImage, CV_BGR2GRAY);
 41     threshold(grayImage, grayImage,90,255, THRESH_BINARY_INV);
 42     vector<Mat> vectorGrayImage(4);
 43     vectorGrayImage[0] = grayImage(Rect(perAncherPoint[0].x+4, 0, 15, standImage.rows));//LEFT
 44     vectorGrayImage[1] = grayImage(Rect(perAncherPoint[1].x+4 , 0, 15, standImage.rows));//RIGHT
 45     vectorGrayImage[2] = grayImage(Rect(0,perAncherPoint[0].y+4, standImage.cols, 15));//TOP
 46     vectorGrayImage[3] = grayImage(Rect(0,perAncherPoint[2].y+4, standImage.cols, 15));//BOTTOM
 47     vector<vector<int>> upJumpWave(4);
 48     vector<vector<int>> downJumpWave(4);    
 49     for (size_t i = 0; i < 4; i++)
 50     {
 51         if (i<2) projectionAlgorithm(vectorGrayImage[i], upJumpWave[i], downJumpWave[i], Y_Axis, 2);
 52         else     projectionAlgorithm(vectorGrayImage[i], upJumpWave[i], downJumpWave[i], X_Axis, 2);
 53     }
 54     //-----------------------绘制检测的跳变线-------------------------//
 55     for (size_t i = 0; i < upJumpWave[0].size(); i++)
 56     {
 57         line(grayImage, Point(perAncherPoint[0].x + 11, upJumpWave[0][i]), Point(perAncherPoint[0].x + 22, upJumpWave[0][i]), Scalar(255, 255, 255));
 58     }
 59     for (size_t i = 0; i < upJumpWave[3].size(); i++)
 60     {
 61         line(grayImage, Point(upJumpWave[3][i], perAncherPoint[3].y), Point(upJumpWave[3][i], perAncherPoint[3].y + 11), Scalar(255, 255, 255));
 62     }
 63     for (size_t i = 0; i < upJumpWave[1].size(); i++)
 64     {
 65         line(grayImage, Point(perAncherPoint[1].x, upJumpWave[1][i]), Point(perAncherPoint[1].x + 11, upJumpWave[1][i]), Scalar(255, 255, 255));
 66     }
 67     for (size_t i = 0; i < upJumpWave[2].size(); i++)
 68     {
 69         line(grayImage, Point(upJumpWave[2][i], perAncherPoint[0].y + 11), Point(upJumpWave[2][i], perAncherPoint[0].y + 22), Scalar(255, 255, 255));
 70     }
 71     //-------------把所有的点存储在容器里,以供下面的函数调用--------------//
 72     vector<Point2f> allPoint;
 73     for (size_t i = 1; i < upJumpWave[2].size(); i++)//存储上半部分图卡点(准考证号区+旁边那个看不清的区域)
 74     {
 75         for (size_t j = 0; j < 10; j++)
 76         {
 77             allPoint.push_back(Point(upJumpWave[2][i], upJumpWave[1][j]));
 78         }
 79     }
 80     for (size_t i = 0; i < upJumpWave[3].size(); i++)//存储下半部分图卡点(答题区)
 81     {
 82         for (size_t j = 10; j < upJumpWave[1].size(); j++)
 83         {
 84             allPoint.push_back(Point(upJumpWave[3][i], upJumpWave[1][j]));
 85         }
 86     }
 87     //--------------检测涂上铅笔的区域--------------//
 88     vector<Point2f> testKeyPoint;
 89     checkKeypoint(grayImage, allPoint, testKeyPoint);
 90     for (size_t i = 0; i < testKeyPoint.size(); i++)
 91     {
 92         rectangle(show, Rect(testKeyPoint.at(i), Point(testKeyPoint.at(i).x + 12, testKeyPoint.at(i).y + 5)), Scalar(0, 0, 255));
 93     }
 94     waitKey();
 95     return 0;
 96 }
 97 //--------------------------------注释代码部分为未用掩码操作--------------------------//
 98 void FindAnchorPoint(const Mat& src,const Mat& matchMask,vector<Point2f>& anchorPoint)
 99 {
100     Mat matchResult;
101     matchResult.create(Size(src.cols - matchMask.cols, src.rows - matchMask.rows), CV_16SC1);
102     //----模板匹配找四个定位点,同时得归一化(初始数据范围太大,自己通过image watch 查看)
103     matchTemplate(src, matchMask, matchResult, TM_CCOEFF_NORMED, Mat());
104     normalize(matchResult, matchResult, 0, 1, NORM_MINMAX);
105     //----查找匹配的四个点,分成四个区域查找,因为一个区域没办法查找四个值
106     /*Mat topleft  = matchResult(Rect(Point(0, 0), Point(matchResult.cols / 2, matchResult.rows / 2)));
107     Mat topright = matchResult(Rect(Point(matchResult.cols / 2, 0), Point(matchResult.cols, matchResult.rows / 2)));
108     Mat botleft  = matchResult(Rect(Point(0, matchResult.rows / 2), Point(matchResult.cols / 2, matchResult.rows)));
109     Mat botright = matchResult(Rect(Point(matchResult.cols / 2, matchResult.rows / 2), Point(matchResult.cols , matchResult.rows)));*/
110     double maxValue[4] = { 0 }, minValue[4] = {0};
111     vector<Point2i> maxPoint(4), minPoint(4);
112     Mat topleftMask  = Mat::zeros(matchResult.size(), CV_8UC1);
113     Mat toprightMask = Mat::zeros(matchResult.size(), CV_8UC1);
114     Mat botleftMask  = Mat::zeros(matchResult.size(), CV_8UC1);
115     Mat botrightMask = Mat::zeros(matchResult.size(), CV_8UC1);
116     topleftMask(Rect(Point(0, 0), Point(matchResult.cols / 2, matchResult.rows / 2))).setTo(255);
117     toprightMask(Rect(Point(matchResult.cols / 2, 0), Point(matchResult.cols, matchResult.rows / 2))).setTo(255);
118     botleftMask(Rect(Point(0, matchResult.rows / 2), Point(matchResult.cols / 2, matchResult.rows))).setTo(255);
119     botrightMask(Rect(Point(matchResult.cols / 2, matchResult.rows / 2), Point(matchResult.cols, matchResult.rows))).setTo(255);
120     vector<Mat> vectorMask;//注意此处如果用vector<Mat> vectorMask(4);对应的下面写法是vectorMask[0]=topleftMask;
121     vectorMask.push_back(topleftMask);
122     vectorMask.push_back(toprightMask);
123     vectorMask.push_back(botleftMask);
124     vectorMask.push_back(botrightMask);
125     for (size_t i = 0; i < vectorMask.size(); i++)
126     {
127         minMaxLoc(matchResult, &minValue[i], &maxValue[i], &minPoint[i], &maxPoint[i], vectorMask[i]);
128     }
129     anchorPoint.assign(maxPoint.begin(), maxPoint.end());
130     //minMaxLoc(topleft, &minValue[0], &maxValue[0], &minPoint[0], &maxPoint[0]);
131     //minMaxLoc(topright, &minValue[1], &maxValue[1], &minPoint[1], &maxPoint[1]);
132     //maxPoint[1].x = maxPoint[1].x + matchResult.cols / 2;
133     //minMaxLoc(botleft,  &minValue[2], &maxValue[2], &minPoint[2], &maxPoint[2]);
134     //maxPoint[2].y = maxPoint[2].y + matchResult.rows / 2;
135     //minMaxLoc(botright, &minValue[3], &maxValue[3], &minPoint[3], &maxPoint[3]);
136     //maxPoint[3].x = maxPoint[3].x + matchResult.cols / 2;
137     //maxPoint[3].x = maxPoint[3].y + matchResult.rows / 2;
138 }
139 //------------------------------------图像投影算法-----------------------------------------------//
140 //*************@src------------------输入矩阵为单通道********************************************//
141 //*************@leftUpJumpWave-------上升跳变沿存储**********************************************//
142 //*************@rightDownJumpWave----下降跳变沿存储**********************************************//
143 //*************@maxInterval----------允许高电平(像素)最大间隔,也可以说是允许的最大误差********//
144 //-----------------------------------------------------------------------------------------------//
145 void projectionAlgorithm(Mat src,vector<int>& UpJumpWave,vector<int>& DownJumpWave,bool Axis,int maxInterval)
146 {
147     vector<int> pixNum(src.rows > src.cols ? src.rows : src.cols);
148     //------对X、Y做直方图类似的投影,统计一行或者一列的非零个数--------//
149     if (Axis)
150     {
151         for (size_t i = 0; i < src.cols; i++)
152         {
153             Mat col = src.col(i);//一列数据
154             pixNum[i] = countNonZero(col) > 1 ? countNonZero(col) : 0;
155         }
156     }
157     else
158     {
159         
160         for (size_t i = 0; i < src.rows; i++)
161         {
162             Mat row = src.row(i);//一行数据
163             pixNum[i] = countNonZero(row) > 1 ? countNonZero(row) : 0;
164         }
165     }
166     if (pixNum.size() < maxInterval) return;//防止有空洞(实际没见过,如果有的话那程序架构会奔溃了)
167     //-----对上面的数据进行二值化0-1,同时对于不满足maxInterval的数据进行剔除--------//
168     for (int k = 1; k < pixNum.size()-maxInterval; k++)//去除了第一个和最后一个像素
169     {
170         if (pixNum[k] > 0 && pixNum[k + maxInterval] > 0)
171         {
172             for (size_t j = k; j < k + maxInterval; j++)
173             {
174                 pixNum[j] = 1;
175             }
176             k = k + maxInterval-1;
177         }
178         else
179         {
180             pixNum[k] = 0;
181         }
182     }
183     //----对跳变的电平进行存储,高->低,低->高,-----//
184     for (size_t i = 1 ; i < pixNum.size()-2; i++)//去除了第一个和最后一个像素
185     {
186         if (pixNum[i] == 0 && pixNum[i + 1] == 1) UpJumpWave.push_back(i);
187         if (pixNum[i] == 1 && pixNum[i + 1] == 0) DownJumpWave.push_back(i);
188     }
189     //----对得到的结果进行处理,定位点被误判----//
190     vector<int>::iterator begin = UpJumpWave.begin();    
191     if (UpJumpWave[0] < 15)                    UpJumpWave.erase(begin);
192     vector<int>::iterator end = UpJumpWave.end()-1;
193     if (UpJumpWave[UpJumpWave.size()-1] > 330) UpJumpWave.erase(end);
194 }
195 //-----------------------------------------------------------------------------------//
196 //************************************检测涂卡区域函数***********************************//
197 void checkKeypoint(Mat& _src,vector<Point2f>& allPoint,vector<Point2f>& testkeyPoint)
198 {
199     Mat src = _src.clone();
200     Mat show = Mat::zeros(src.size(), CV_8UC3);
201     morphologyEx(src, src, MORPH_DILATE, Mat::ones(3, 3, CV_8UC1));
202     for (size_t i = 0; i < allPoint.size(); i++)
203     {
204         //------判断检测点的正方形涂卡区的非零个数---------//
205         if (allPoint[i].x == 0 || allPoint[i].y == 0)
206         {
207             allPoint[i].x += 1;
208             allPoint[i].y += 1;
209         }
210         Mat rec = src(Rect(static_cast<int>(allPoint[i].x - 1 ), static_cast<int>(allPoint[i].y - 1 ), 12, 5));
211         int count = countNonZero(rec);
212         if (count > 15)
213         {
214             testkeyPoint.push_back(allPoint[i]);
215             rectangle(show, Rect(allPoint[i], Point(allPoint[i].x + 13, allPoint[i].y + 5)), Scalar(0, 0, 255));
216         }
217     }
218 }
219 #endif

 


 

补充:

    1.此代码无法识别旋转的答题卡,只能识别透视的答题卡。原因是模板匹配不具有旋转和尺度的不变性。

    2.随便拿来一张答题卡,主要是改里内部的参数,其中包括:

           A. threshold(grayImage, grayImage,90,255, THRESH_BINARY_INV);//阈值更改,一般不用OTSU算法,因为要求得很准确。

       B.   matchTemplate(src, matchMask, matchResult, TM_CCOEFF_NORMED, Mat());//模板匹配方法更改

       C.   vectorGrayImage[0] = grayImage(Rect(perAncherPoint[0].x+4, 0, 15, standImage.rows));//区域的大小更改,其它三个等同

       D. void projectionAlgorithm(Mat src,vector<int>& UpJumpWave,vector<int>& DownJumpWave,bool Axis,int maxInterval)//maxInterval最大误差更改

       E.   if (UpJumpWave[0] < 15) UpJumpWave.erase(begin);//对误判定位线进行筛选,另一个等同

       F.  if (pixNum[i] == 0 && pixNum[i + 1] == 1) UpJumpWave.push_back(i);//跳变沿的两边宽度可以进行调整

       G.   Mat rec = src(Rect(static_cast<int>(allPoint[i].x - 1 ), static_cast<int>(allPoint[i].y - 1 ), 12, 5));//检测涂卡区域大小更改

       H.   if (count > 15)//涂卡区域的程度判断更改

 

转载于:https://www.cnblogs.com/wjy-lulu/p/6958987.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值