Opencv研读笔记:haartraining程序之icvCreateCARTStageClassifier函数详解(强分类器创建)

之前介绍了haartraining程序中的cvCreateMTStumpClassifier函数,这个函数的功能是计算最优弱分类器,这篇文章介绍一下自己对haartraining中关于强分类器计算的一些理解,也就是程序中的icvCreateCARTStageClassifier函数。


在给出代码之前,说几处自认为值得说说的问题:

1. 由于haartraining是基于HAAR特征进行adaboost训练,对于HAAR特征的处理比较繁琐,采用了奇数弱分类器补充针对翻转特征最优弱分类器计算的代码,所以代码看起来较为冗长。

2. 创建强分类器时,其中包含有样本权值的更新,代码中共提供了四种经典adaboost算法版本,它们是Discrete Adaboost、Real Adaboost、Logit Boost、Gentle Adaboost。每种算法的权值更新策略不同,这方面的知识建议大家下载几篇博士论文看看,也可以看看我之前发的博客。

http://blog.csdn.net/wsj998689aa/article/details/42242565

3. 代码较多地通过函数指针的形式(实际上这也是opencv一直常用的手段)对函数进行回调。

4. 小权值样本需要剔除掉,因为小权值样本对训练结果的影响微乎其微,加了它们反而要耗时不少。这边有一处需要提醒大家,当前分类器剔除掉的小权值样本,仍旧参与下一个分类器中样本权值的剔除,换句话说,每一个弱分类器,其实都要对所有的样本权值进行排序,所以会造成实际训练样本比例出现“跳变”的情况,但是总体走势还是始终下降的。如下图所示:


5. 代码中采用了较多的中间结构体变量,例如CvIntHaarClassifier结构体(用于模拟强分类器结构体CvStageHaarClassifier的父类),CvBoostTrainer结构体(用于初始化,更新样本权值等)等等,看起来比较绕。

6. 关于弱分类器的创建,事先创建的其实是CART分类器,CART分类器就是一棵树,每个节点代表一个最优Haar特征,但是一般程序中的节点个数都设置为1,所以一个CART就相当于stump了,此外,CART的创建涉及到节点的分裂,通过icvSplitIndicesCallback函数实现。

7. 在创建CART的时候,最优Haar特征其实就已经被选择好了,至于下面还有一个stumpConstructor函数,是由于Haar特征被翻转而产生了新特征,所以需要重新寻找最优弱分类器。

8. 有意思的是,函数输入了最小正检率和最大误检率,前者决定了判定样本是正类还是负类的阈值,后者决定了强分类器是否能够收敛,具体来说吧,先计算当前最优弱分类器关于每个正样本的置信度,然后对置信度进行排序,这个阈值就是基于最小正检率选择的置信度,然后在根据这个阈值来计算当前最优弱分类器的误检率(计算每个负样本的置信度),如果大于了输入的最大误检率,那么OK!!一串弱分类器所构成的强分类器诞生了。


以上说的就是icvCreateCARTStageClassifier中值得注意的几点,下面上代码,是根据自己的理解添加的注释,请各位不吝批评指正哈!

转载请注明:http://blog.csdn.net/wsj998689aa/article/details/42398235

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. static  
  2. CvIntHaarClassifier* icvCreateCARTStageClassifier( CvHaarTrainingData* data,        // 全部训练样本  
  3.                                                    CvMat* sampleIdx,                // 实际训练样本序列  
  4.                                                    CvIntHaarFeatures* haarFeatures, // 全部HAAR特征  
  5.                                                    float minhitrate,    // 最小正检率(用于确定强分类器阈值)  
  6.                                                    float maxfalsealarm, // 最大误检率(用于确定是否收敛)              
  7.                                                    int   symmetric,     // HAAR是否对称  
  8.                                                    float weightfraction,    // 样本剔除比例(用于剔除小权值样本)  
  9.                                                    int numsplits,           // 每个弱分类器特征个数(一般为1)  
  10.                                                    CvBoostType boosttype,   // adaboost类型  
  11.                                                    CvStumpError stumperror, // Discrete AdaBoost中的阈值计算方式  
  12.                                                    int maxsplits )          // 弱分类器最大个数  
  13. {  
  14.   
  15. #ifdef CV_COL_ARRANGEMENT  
  16.     int flags = CV_COL_SAMPLE;  
  17. #else  
  18.     int flags = CV_ROW_SAMPLE;  
  19. #endif  
  20.   
  21.     CvStageHaarClassifier* stage = NULL;                    // 强分类器  
  22.     CvBoostTrainer* trainer;                                // 临时训练器,用于更新样本权值  
  23.     CvCARTClassifier* cart = NULL;                          // 弱分类器  
  24.     CvCARTTrainParams trainParams;                          // 训练参数  
  25.     CvMTStumpTrainParams stumpTrainParams;                  // 弱分类器参数  
  26.     //CvMat* trainData = NULL;  
  27.     //CvMat* sortedIdx = NULL;  
  28.     CvMat eval;                                             // 临时矩阵  
  29.     int n = 0;                                              // 特征总数  
  30.     int m = 0;                                              // 总样本个数  
  31.     int numpos = 0;                                         // 正样本个数  
  32.     int numneg = 0;                                         // 负样本个数  
  33.     int numfalse = 0;                                       // 误检样本个数  
  34.     float sum_stage = 0.0F;                                 // 置信度累积和                                
  35.     float threshold = 0.0F;                                 // 强分类器阈值  
  36.     float falsealarm = 0.0F;                                // 误检率  
  37.       
  38.     //CvMat* sampleIdx = NULL;  
  39.     CvMat* trimmedIdx;                                      // 剔除小权值之后的样本序列  
  40.     //float* idxdata = NULL;  
  41.     //float* tempweights = NULL;  
  42.     //int    idxcount = 0;  
  43.     CvUserdata userdata;                                    // 训练数据  
  44.   
  45.     int i = 0;  
  46.     int j = 0;  
  47.     int idx;  
  48.     int numsamples;                                         // 实际样本个数  
  49.     int numtrimmed;                                         // 剔除小权值之后的样本个数  
  50.       
  51.     CvCARTHaarClassifier* classifier;                       // 弱分类器  
  52.     CvSeq* seq = NULL;  
  53.     CvMemStorage* storage = NULL;  
  54.     CvMat* weakTrainVals;                                   // 样本类别,只有logitboost才会用到  
  55.     float alpha;  
  56.     float sumalpha;  
  57.     int num_splits;                                         // 弱分类器个数                                      
  58.   
  59. #ifdef CV_VERBOSE  
  60.     printf( "+----+----+-+---------+---------+---------+---------+\n" );  
  61.     printf( "|  N |%%SMP|F|  ST.THR |    HR   |    FA   | EXP. ERR|\n" );  
  62.     printf( "+----+----+-+---------+---------+---------+---------+\n" );  
  63. #endif /* CV_VERBOSE */  
  64.       
  65.     n = haarFeatures->count;  
  66.     m = data->sum.rows;  
  67.     numsamples = (sampleIdx) ? MAX( sampleIdx->rows, sampleIdx->cols ) : m;  
  68.   
  69.     // 样本与HAAR特征  
  70.     userdata = cvUserdata( data, haarFeatures );  
  71.   
  72.   
  73.     /* 弱分类参数设置 */  
  74.     stumpTrainParams.type = ( boosttype == CV_DABCLASS )  
  75.         ? CV_CLASSIFICATION_CLASS : CV_REGRESSION;                              // 分类或者回归  
  76.     stumpTrainParams.error = ( boosttype == CV_LBCLASS || boosttype == CV_GABCLASS )  
  77.         ? CV_SQUARE : stumperror;                                               // 弱分类器阈值计算方式  
  78.     stumpTrainParams.portion = CV_STUMP_TRAIN_PORTION;                          // 每组特征个数  
  79.     stumpTrainParams.getTrainData = icvGetTrainingDataCallback;                 // 计算样本的haar值  
  80.     stumpTrainParams.numcomp = n;                                               // 特征个数              
  81.     stumpTrainParams.userdata = &userdata;   
  82.     stumpTrainParams.sortedIdx = data->idxcache;                                // 特征-样本序号矩阵(排序之后)  
  83.   
  84.   
  85.     // 由于参数众多,所以创建参数结构体  
  86.     trainParams.count = numsplits;                                              // 弱分类器特征树  
  87.     trainParams.stumpTrainParams = (CvClassifierTrainParams*) &stumpTrainParams;// 弱分类参数  
  88.     trainParams.stumpConstructor = cvCreateMTStumpClassifier;                   // 筛选最优弱分类器  
  89.     trainParams.splitIdx = icvSplitIndicesCallback;                             // CART节点分裂函数  
  90.     trainParams.userdata = &userdata;                                                 
  91.   
  92.     // 临时向量,用于存放样本haar特征值  
  93.     eval = cvMat( 1, m, CV_32FC1, cvAlloc( sizeoffloat ) * m ) );  
  94.       
  95.     storage = cvCreateMemStorage();  
  96.   
  97.     // 最优弱分类器存储序列  
  98.     seq = cvCreateSeq( 0, sizeof( *seq ), sizeof( classifier ), storage );  
  99.   
  100.     // 样本类别,只有logitboost才会用到  
  101.     weakTrainVals = cvCreateMat( 1, m, CV_32FC1 );  
  102.   
  103.     // 初始化样本类别与权重,weakTrainVals为{-1, 1},权重都一样  
  104.     trainer = cvBoostStartTraining( &data->cls, weakTrainVals, &data->weights,  
  105.                                     sampleIdx, boosttype );  
  106.     num_splits = 0;  
  107.     sumalpha = 0.0F;  
  108.     do  
  109.     {       
  110.   
  111. #ifdef CV_VERBOSE  
  112.         int v_wt = 0;  
  113.         int v_flipped = 0;  
  114. #endif /* CV_VERBOSE */  
  115.   
  116.         // 剔除小权值样本  
  117.         trimmedIdx = cvTrimWeights( &data->weights, sampleIdx, weightfraction );  
  118.   
  119.         // 实际样本总数  
  120.         numtrimmed = (trimmedIdx) ? MAX( trimmedIdx->rows, trimmedIdx->cols ) : m;  
  121.   
  122. #ifdef CV_VERBOSE  
  123.         v_wt = 100 * numtrimmed / numsamples;  
  124.         v_flipped = 0;  
  125.   
  126. #endif /* CV_VERBOSE */  
  127.   
  128.         // 重要函数,创建CART树的同时,当前最优弱分类器出炉,一般只有根节点  
  129.         cart = (CvCARTClassifier*) cvCreateCARTClassifier( data->valcache,  
  130.                         flags,  
  131.                         weakTrainVals, 0, 0, 0, trimmedIdx,  
  132.                         &(data->weights),  
  133.                         (CvClassifierTrainParams*) &trainParams );  
  134.   
  135.         // 创建弱分类器  
  136.         classifier = (CvCARTHaarClassifier*) icvCreateCARTHaarClassifier( numsplits );  
  137.   
  138.         // 将CART树转化为弱分类器  
  139.         icvInitCARTHaarClassifier( classifier, cart, haarFeatures );  
  140.   
  141.         num_splits += classifier->count;  
  142.   
  143.         cart->release( (CvClassifier**) &cart );  
  144.           
  145.         // 为何一定要在奇数个弱分类器处计算?  
  146.         if( symmetric && (seq->total % 2) )  
  147.         {  
  148.             float normfactor = 0.0F;  
  149.             CvStumpClassifier* stump;  
  150.               
  151.             /* 翻转HAAR特征 */  
  152.             for( i = 0; i < classifier->count; i++ )  
  153.             {  
  154.                 if( classifier->feature[i].desc[0] == 'h' )  
  155.                 {  
  156.                     for( j = 0; j < CV_HAAR_FEATURE_MAX &&  
  157.                                     classifier->feature[i].rect[j].weight != 0.0F; j++ )  
  158.                     {  
  159.                         classifier->feature[i].rect[j].r.x = data->winsize.width -   
  160.                             classifier->feature[i].rect[j].r.x -  
  161.                             classifier->feature[i].rect[j].r.width;                  
  162.                     }  
  163.                 }  
  164.                 else  
  165.                 {  
  166.                     int tmp = 0;  
  167.   
  168.                     /* (x,y) -> (24-x,y) */  
  169.                     /* w -> h; h -> w    */  
  170.                     for( j = 0; j < CV_HAAR_FEATURE_MAX &&  
  171.                                     classifier->feature[i].rect[j].weight != 0.0F; j++ )  
  172.                     {  
  173.                         classifier->feature[i].rect[j].r.x = data->winsize.width -   
  174.                             classifier->feature[i].rect[j].r.x;  
  175.                         CV_SWAP( classifier->feature[i].rect[j].r.width,  
  176.                                  classifier->feature[i].rect[j].r.height, tmp );  
  177.                     }  
  178.                 }  
  179.             }  
  180.   
  181.             // 转化为基于积分图计算的特征  
  182.             icvConvertToFastHaarFeature( classifier->feature,  
  183.                                          classifier->fastfeature,  
  184.                                          classifier->count, data->winsize.width + 1 );  
  185.   
  186.             // 为了验证最新翻转特征是否为最优特征  
  187.             stumpTrainParams.getTrainData = NULL;  
  188.             stumpTrainParams.numcomp = 1;  
  189.             stumpTrainParams.userdata = NULL;  
  190.             stumpTrainParams.sortedIdx = NULL;  
  191.   
  192.             // 验证是否新生成的特征可作为最优弱分类器  
  193.             for( i = 0; i < classifier->count; i++ )  
  194.             {  
  195.                 for( j = 0; j < numtrimmed; j++ )  
  196.                 {  
  197.                     // 获取训练样本  
  198.                     idx = icvGetIdxAt( trimmedIdx, j );  
  199.   
  200.                     // 对每个训练样本计算Haar特征  
  201.                     eval.data.fl[idx] = cvEvalFastHaarFeature( &classifier->fastfeature[i],  
  202.                                         (sum_type*) (data->sum.data.ptr + idx * data->sum.step),  
  203.                                         (sum_type*) (data->tilted.data.ptr + idx * data->tilted.step) );   
  204.   
  205.                     // 归一化因子  
  206.                     normfactor = data->normfactor.data.fl[idx];  
  207.   
  208.                     // 对Haar特征归一化  
  209.                     eval.data.fl[idx] = ( normfactor == 0.0F )  
  210.                         ? 0.0F : (eval.data.fl[idx] / normfactor);  
  211.                 }  
  212.   
  213.                 // 计算最优弱分类器  
  214.                 stump = (CvStumpClassifier*) trainParams.stumpConstructor( &eval,  
  215.                     CV_COL_SAMPLE,  
  216.                     weakTrainVals, 0, 0, 0, trimmedIdx,  
  217.                     &(data->weights),  
  218.                     trainParams.stumpTrainParams );  
  219.               
  220.                 classifier->threshold[i] = stump->threshold;                // 阈值  
  221.                 if( classifier->left[i] <= 0 )  
  222.                 {  
  223.                     classifier->val[-classifier->left[i]] = stump->left;    // 左分支输出置信度  
  224.                 }  
  225.                 if( classifier->right[i] <= 0 )  
  226.                 {  
  227.                     classifier->val[-classifier->right[i]] = stump->right;  // 右分支输出置信度  
  228.                 }  
  229.   
  230.                 stump->release( (CvClassifier**) &stump );          
  231.                   
  232.             }  
  233.   
  234.             // 还原参数,参数支持cvCreateCARTClassifier函数  
  235.             stumpTrainParams.getTrainData = icvGetTrainingDataCallback;  
  236.             stumpTrainParams.numcomp = n;  
  237.             stumpTrainParams.userdata = &userdata;  
  238.             stumpTrainParams.sortedIdx = data->idxcache;  
  239.   
  240. #ifdef CV_VERBOSE  
  241.             v_flipped = 1;  
  242. #endif /* CV_VERBOSE */  
  243.   
  244.         } /* if symmetric */  
  245.         if( trimmedIdx != sampleIdx )  
  246.         {  
  247.             cvReleaseMat( &trimmedIdx );  
  248.             trimmedIdx = NULL;  
  249.         }  
  250.           
  251.         // 调用icvEvalCARTHaarClassifier函数,计算每个样本的当前最优弱分类器置信度  
  252.         for( i = 0; i < numsamples; i++ )  
  253.         {  
  254.             idx = icvGetIdxAt( sampleIdx, i );  
  255.   
  256.             eval.data.fl[idx] = classifier->eval( (CvIntHaarClassifier*) classifier,  
  257.                 (sum_type*) (data->sum.data.ptr + idx * data->sum.step),  
  258.                 (sum_type*) (data->tilted.data.ptr + idx * data->tilted.step),  
  259.                 data->normfactor.data.fl[idx] );  
  260.         }  
  261.   
  262.         // 更新样本权重,如果是LogitBoost,也会更新weakTrainVals,函数返回的是弱分类器权重  
  263.         alpha = cvBoostNextWeakClassifier( &eval, &data->cls, weakTrainVals,  
  264.                                            &data->weights, trainer );  
  265.           
  266.         // 这个变量没什么用  
  267.         sumalpha += alpha;  
  268.           
  269.         for( i = 0; i <= classifier->count; i++ )  
  270.         {  
  271.             if( boosttype == CV_RABCLASS )   
  272.             {  
  273.                 classifier->val[i] = cvLogRatio( classifier->val[i] );  
  274.             }  
  275.             classifier->val[i] *= alpha;  
  276.         }  
  277.   
  278.         // 添加弱分类器  
  279.         cvSeqPush( seq, (void*) &classifier );  
  280.   
  281.         // 正样本个数  
  282.         numpos = 0;  
  283.   
  284.         // 遍历sampleIdx中所有样本,计算每个样本的弱分类器置信度和  
  285.         for( i = 0; i < numsamples; i++ )  
  286.         {  
  287.             // 获得样本序号  
  288.             idx = icvGetIdxAt( sampleIdx, i );  
  289.   
  290.             // 如果样本为正样本  
  291.             if( data->cls.data.fl[idx] == 1.0F )  
  292.             {  
  293.                 // 初始化置信度值  
  294.                 eval.data.fl[numpos] = 0.0F;  
  295.   
  296.                 // 遍历seq中所有弱分类器  
  297.                 for( j = 0; j < seq->total; j++ )  
  298.                 {  
  299.                     // 获取弱分类器  
  300.                     classifier = *((CvCARTHaarClassifier**) cvGetSeqElem( seq, j ));  
  301.   
  302.                     // 累积当前正样本的弱分类器置信度和  
  303.                     eval.data.fl[numpos] += classifier->eval(   
  304.                         (CvIntHaarClassifier*) classifier,  
  305.                         (sum_type*) (data->sum.data.ptr + idx * data->sum.step),  
  306.                         (sum_type*) (data->tilted.data.ptr + idx * data->tilted.step),  
  307.                         data->normfactor.data.fl[idx] );  
  308.                 }  
  309.                 /* eval.data.fl[numpos] = 2.0F * eval.data.fl[numpos] - seq->total; */  
  310.                 numpos++;  
  311.             }  
  312.         }  
  313.   
  314.         // 对弱分类器输出置信度和进行排序  
  315.         icvSort_32f( eval.data.fl, numpos, 0 );  
  316.   
  317.         // 计算阈值,应该是大于threshold则为正类,小于threshold则为负类  
  318.         threshold = eval.data.fl[(int) ((1.0F - minhitrate) * numpos)];  
  319.   
  320.         numneg = 0;  
  321.         numfalse = 0;  
  322.   
  323.         // 遍历所有样本,统计错分负样本个数  
  324.         for( i = 0; i < numsamples; i++ )  
  325.         {  
  326.             idx = icvGetIdxAt( sampleIdx, i );  
  327.   
  328.             // 如果样本为负样本  
  329.             if( data->cls.data.fl[idx] == 0.0F )  
  330.             {  
  331.                 numneg++;  
  332.                 sum_stage = 0.0F;  
  333.   
  334.                 // 遍历seq中所有弱分类器  
  335.                 for( j = 0; j < seq->total; j++ )  
  336.                 {  
  337.                    classifier = *((CvCARTHaarClassifier**) cvGetSeqElem( seq, j ));  
  338.   
  339.                    // 累积当前负样本的分类器输出结果  
  340.                    sum_stage += classifier->eval( (CvIntHaarClassifier*) classifier,  
  341.                         (sum_type*) (data->sum.data.ptr + idx * data->sum.step),  
  342.                         (sum_type*) (data->tilted.data.ptr + idx * data->tilted.step),  
  343.                         data->normfactor.data.fl[idx] );  
  344.                 }  
  345.                 /* sum_stage = 2.0F * sum_stage - seq->total; */  
  346.   
  347.                 // 因为小于threshold为负类,所以下面是分类错误的情况  
  348.                 if( sum_stage >= (threshold - CV_THRESHOLD_EPS) )  
  349.                 {  
  350.                     numfalse++;  
  351.                 }  
  352.             }  
  353.         }  
  354.   
  355.         // 计算虚警率  
  356.         falsealarm = ((float) numfalse) / ((float) numneg);  
  357.   
  358. // 输出内容  
  359. #ifdef CV_VERBOSE  
  360.         {  
  361.             // 正样本检出率  
  362.             float v_hitrate    = 0.0F;  
  363.   
  364.             // 负样本误检率  
  365.             float v_falsealarm = 0.0F;  
  366.             /* expected error of stage classifier regardless threshold */  
  367.   
  368.             // 这是什么?  
  369.             float v_experr = 0.0F;  
  370.   
  371.             // 遍历所有样本  
  372.             for( i = 0; i < numsamples; i++ )  
  373.             {  
  374.                 idx = icvGetIdxAt( sampleIdx, i );  
  375.   
  376.                 sum_stage = 0.0F;  
  377.   
  378.                 // 遍历seq中所有弱分类器  
  379.                 for( j = 0; j < seq->total; j++ )  
  380.                 {  
  381.                     classifier = *((CvCARTHaarClassifier**) cvGetSeqElem( seq, j ));  
  382.                     sum_stage += classifier->eval( (CvIntHaarClassifier*) classifier,  
  383.                         (sum_type*) (data->sum.data.ptr + idx * data->sum.step),  
  384.                         (sum_type*) (data->tilted.data.ptr + idx * data->tilted.step),  
  385.                         data->normfactor.data.fl[idx] );  
  386.                 }  
  387.                 /* sum_stage = 2.0F * sum_stage - seq->total; */  
  388.   
  389.                 // 只需要判断单一分支即可  
  390.                 if( sum_stage >= (threshold - CV_THRESHOLD_EPS) )  
  391.                 {  
  392.                     if( data->cls.data.fl[idx] == 1.0F )  
  393.                     {  
  394.                         v_hitrate += 1.0F;  
  395.                     }  
  396.                     else  
  397.                     {  
  398.                         v_falsealarm += 1.0F;  
  399.                     }  
  400.                 }  
  401.   
  402.                 // 正类样本的sum_stage必须大于0  
  403.                 if( ( sum_stage >= 0.0F ) != (data->cls.data.fl[idx] == 1.0F) )  
  404.                 {  
  405.                     v_experr += 1.0F;  
  406.                 }  
  407.             }  
  408.             v_experr /= numsamples;  
  409.             printf( "|%4d|%3d%%|%c|%9f|%9f|%9f|%9f|\n",  
  410.                 seq->total, v_wt, ( (v_flipped) ? '+' : '-' ),  
  411.                 threshold, v_hitrate / numpos, v_falsealarm / numneg,  
  412.                 v_experr );  
  413.             printf( "+----+----+-+---------+---------+---------+---------+\n" );  
  414.             fflush( stdout );  
  415.         }  
  416. #endif /* CV_VERBOSE */  
  417.           
  418.     // 两种收敛方式,一种是误检率小于规定阈值,另一种是弱分类器个数小于规定阈值  
  419.     } while( falsealarm > maxfalsealarm && (!maxsplits || (num_splits < maxsplits) ) );  
  420.     cvBoostEndTraining( &trainer );  
  421.   
  422.     if( falsealarm > maxfalsealarm )          
  423.     {  
  424.         // 如果弱分类器达到上限而收敛,则放弃当前强分类器  
  425.         stage = NULL;  
  426.     }  
  427.     else  
  428.     {  
  429.         // 创建当前强分类器  
  430.         stage = (CvStageHaarClassifier*) icvCreateStageHaarClassifier( seq->total,  
  431.                                                                        threshold );  
  432.         // 保存当前强分类器  
  433.         cvCvtSeqToArray( seq, (CvArr*) stage->classifier );  
  434.     }  
  435.       
  436.     /* CLEANUP */  
  437.     cvReleaseMemStorage( &storage );  
  438.     cvReleaseMat( &weakTrainVals );  
  439.     cvFree( &(eval.data.ptr) );  
  440.       
  441.     return (CvIntHaarClassifier*) stage;  
  442. }  


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值