features2d_1_特征点检测与匹配

一角点检测

兴趣点=关键点=特征点,检测足够多的点,同时他们的区分度很高,则可以精确定位稳定的特征
特征类型分为:
[1]边缘
[2]角点(感兴趣关键点)
[3]斑点(Blobs) (感兴趣区域)

角点是在任意方向的一个微小变动都会引起灰度很大的变化的点。
角点:
[1]一阶导数(灰度的梯度)的局部最大所对应的像素点
[2]两条及两条以上边缘的交点
[3]图像中梯度和梯度方向的变化速率都很高的点
[4]角点处的一阶导数最大,二阶导数为0,它指示了物体边缘变化不连续的方向

1,Harris角点检测
基于灰度图像的角点提取算法,稳定性高。采用高斯滤波,速度较慢,角点信息有丢失和位置偏移的现象,有聚簇现象。
针对每一个像素,在blockSize×blockSize的邻域内,计算
这里写图片描述
这里写图片描述
根据设定的阈值threshold,如果R>threshold,则判断为角点

void cornerHarris(InputArray src, OutputArray dst, 
                  int blockSize, int ksize, double k, 
                  int borderType=BORDER_DEFAULT )
//src,单通道8-bit或浮点型图像
//dst,与src同size,type为CV_32FC1,用来存储R值
//blockSize,针对每一个像素的邻域大小
//ksize,进行Sobel边缘检测的ksize
//k,计算公式中的参数k
//borderType,边界拓展类型                  

应用:

//转化成灰度图
cv::cvtColor(image,Image,CV_BGR2GRAY);

//进行角点检测
cv::Mat process;
int blockSize=2;
int ksize=3;
double k=0.04;
cv::cornerHarris(Image,process,blockSize,ksize,k);

//进行归一化,使值落在0~255
cv::normalize(process,process,0,255,cv::NORM_MINMAX,
              CV_32FC1);

//对于大于设定阈值的点,判断为角点
int thresh=120;
for(int i=0;i<result.rows;i++)
for(int j=0;j<result.cols;j++)
if(process.at<float>(i,j)>thresh)
{
cv::circle(result,cv::Point(j,i),5,cv::Scalar(0,0,255),2);
}

效果图:
这里写图片描述

2,Shi-Tomasi角点检测
用于强角点的检测,是Harris算法的改进版,两个特征值中较小的一个大于最小阈值,会得到强角点
角点检测原理:
[1]首先利用cornerHarris()或者cornerMnEigenVal(),计算角点的质量
[2]执行非极大抑制,在3*3邻域内局部最大值被保留
[3]有最小特征值比这里写图片描述还要小的点被抛弃
[4]剩下的角点,根据质量进行递减排序
[5]根据角点间距离minDistance,留下质量好的角点

void goodFeaturesToTrack(InputArray image, 
                         OutputArray corners, 
                         int maxCorners, 
                         double qualityLevel, 
                         double minDistance, 
                         InputArray mask=noArray(), 
                         int blockSize=3, 
                         bool useHarrisDetector=false, 
                         double k=0.04 )
/*image,单通道,8-bit或者32位浮点型
 *corners,输出被检测到的角点向量,vector<Point2f>
 *maxCorners,角点的最大数量
 *qualityLevel,用于计算最小特征值的参数,最小特征值
 *=qualityLevel×最大特征值,一般为0.01/0.1
 *minDistance,角点间的最小距离
 *mask,掩码
 *blockSize,每个像素邻域
 *useHarrisDetector,如果true使用cornerHarris(),否则
 *cornerMnEigenVal()
 *k,Harris的参数
 */                         

应用:

//转化成灰度图
cv::cvtColor(image,Image,CV_BGR2GRAY);

//查找角点
std::vector<cv::Point2f> corners;
cv::goodFeaturesToTrack(Image,corners,100,0.01,8);

cv::Size winSize(5,5);
cv::Size zeroZone(-1,-1);
cv::TermCriteria criteria=
cv::TermCriteria(CV_TERMCRIT_EPS+CV_TERMCRIT_ITER,40,0.01);
//进行亚像素精确化
cv::cornerSubPix(Image,corners,winSize,zeroZone,criteria);
//进行标记
for(int i=0;i<corners.size();i++)
{
cv::circle(result,corners[i],5,cv::Scalar(0,0,255),2);
}

效果图:
这里写图片描述

3,创建自己的角点检测
1>计算角点检测特征值和特征向量
对于每一个像素p,在blockSize×blockSize区域内,通过Sobel计算协变矩阵:
这里写图片描述

这里写图片描述为M的非排序特征值。
x1,y1为这里写图片描述的特征向量
x2,y2为这里写图片描述的特征向量
存储在dst中这里写图片描述

void cornerEigenValsAndVecs(InputArray src, OutputArray dst, 
                            int blockSize, int ksize, 
                            int borderType=BORDER_DEFAULT )
//src,单通道,8-bit或浮点型图像
//dst,与src同size,type为CV_32FC(6)
//blockSize,邻域大小
//ksize,Sobel边缘检测的ksize
//borderType,边界拓展类型                            

2>为角点检测,计算梯度矩阵的最小的特征值
用来寻找这里写图片描述中的最小值

void cornerMinEigenVal(InputArray src, OutputArray dst, 
                       int blockSize, int ksize=3, 
                       int borderType=BORDER_DEFAULT )
//src,单通道,8-bit或浮点型图像
//dst,存储最小的特征值,与src同size,type为CV_32FC1
//blockSize,邻域大小
//ksize,Sobel边缘检测的ksize
//borderType,边界拓展类型                       

[1]应用_Harris角点检测:

//转化成灰度图
cv::cvtColor(image,Image,CV_BGR2GRAY);

//找到特征值
cv::Mat dst;
cv::cornerEigenValsAndVecs(Image,dst,3,3);

//将特征值带入公式,得到Mc
int k=0.04;
cv::Mat Mc(dst.size(),CV_32FC1);
for(int i=0;i<dst.rows;i++)
for(int j=0;j<dst.cols;j++)
{
float lam1=dst.at<cv::Vec6f>(i,j)[0];
float lam2=dst.at<cv::Vec6f>(i,j)[1];
Mc.at<float>(i,j)=lam1*lam2-k*std::pow((lam1+lam2),2);
}

//找到Mc中的最大值,最小值
double minVal,maxVal;
cv::minMaxLoc(Mc,&minVal,&maxVal,0,0);

int max_qualityLevel=100,qualityLevel=30;
//对每一点是否达到角点标准进行判断
for(int i=0;i<Mc.rows;i++)
for(int j=0;j<Mc.cols;j++)
if(Mc.at<float>(i,j)>minVal+(maxVal-minVal)*
                     qualityLevel/max_qualityLevel)
{         cv::circle(result,cv::Point(j,i),4,cv::Scalar(0,0,255),2);
}

效果图:
这里写图片描述

[2]应用_Shi_Tomashi角点检测:

//转化成灰度图
cv::cvtColor(image,Image,CV_BGR2GRAY);

//得到特征值,并且计算得到最小值
cv::Mat dst;
cv::cornerMinEigenVal(Image,dst,3,3);

//找到dst中,最大值,最小值
double minVal,maxVal;
cv::minMaxLoc(dst,&minVal,&maxVal,0,0);

int max_qualityLevel=100,qualityLevel=30;
//对每一点是否达到,角点标准进行判断
for(int i=0;i<dst.rows;i++)
for(int j=0;j<dst.cols;j++)
if(dst.at<float>(i,j)>minVal+(maxVal-minVal)
                     *qualityLevel/max_qualityLevel)
{
cv::circle(result,cv::Point(j,i),4,cv::Scalar(0,0,255),2);
}

效果图:
这里写图片描述

二特征检测与匹配

1,基类和数据结构
1>KeyPoint类

class KeyPoint
{
Point2f pt      //坐标
float size      //特征点的邻域直径
float angle     //特征点的方向
float response  //特征点的强度反应,可用于sort
int octave      //特征点所在的金字塔的组
int class_id    
}

2>FeatureDetector类
二维图像中用于特征探测的抽象基类

[1]关键点探测

//从image中探测关键点
void FeatureDetector::detect(const Mat& image, 
                             vector<KeyPoint>& keypoints, 
                             const Mat& mask=Mat() ) const
//image图像
//keypoints探测到的关键点
//mask,掩码,8-bit                             

[2]特征探测器的创建

Ptr<FeatureDetector> 
FeatureDetector::create(const string& detectorType)
/*返回相应名字的特征探测器的指针
 *有以下特征探测名称:
 *"FAST"FastFeatureDetector
 *"STAR"StarFeatureDetector
 *"SIFT"SIFT (nonfree module)
 *"SURF"SURF (nonfree module)
 *"ORB"ORB
 *"BRISK"BRISK
 *"MSER"MSER
 *"GFTT"GoodFeaturesToTrackDetector
 *"HARRIS"GoodFeaturesToTrackDetector(Harris)   
 *"Dense"DenseFeatureDetector
 *"SimpleBlob"SimpleBlobDetector
 */

3>DescriptorExtractor类
用于计算图像关键点描述符的抽象基类

[1]计算图像中关键点的描述符

void DescriptorExtractor::compute(const Mat& image, 
                                 vector<KeyPoint>& keypoints, 
                                 Mat& descriptors) const
//image,图像
//keypoints,输入的关键点,不能计算描述符的关键点被删除,剩下的被
//排序,还有部分点被添加进来
//descriptors,描述符                                 

[2]描述符提取类的创建

Ptr<DescriptorExtractor> DescriptorExtractor
              ::create(const string& descriptorExtractorType)
/*返回相应描述符提取类的指针
 *有以下提取类名称:
 *"SIFT" – SIFT
 *"SURF" – SURF
 *"BRIEF" – BriefDescriptorExtractor
 *"BRISK" – BRISK
 *"ORB" – ORB
 *"FREAK" – FREAK
 */              

4>DMatch类
用于匹配关键点描述符的类

struct DMatch
{
    int queryIdx;   //查找描述索引
    int trainIdx;   //训练描述索引
    int imgIdx;     //训练图像索引
    float distance; //描述符之间的距离
};

5>DescriptorMatcher类
用于图像匹配的抽象基类

[1]从查询集中为每个描述符找到最好的匹配

void DescriptorMatcher::match(const Mat& queryDescriptors, 
                              const Mat& trainDescriptors, 
                              vector<DMatch>& matches, 
                              const Mat& mask=Mat() ) const
//queryDescriptors,查询集的描述符
//trainDescriptors,训练集的描述符,这一部分是没有添加到类中的描述符
//matches,匹配结果
//mask,掩码                              

[2]从查询集中为每个描述符找到k个最好的匹配

void DescriptorMatcher
     ::knnMatch(const Mat& queryDescriptors, 
                const Mat& trainDescriptors, 
                vector<vector<DMatch>>& matches, int k, 
                const Mat& mask=Mat(), 
                bool compactResult=false ) const
//queryDescriptors,查询集的描述符
//trainDescriptors,训练集的描述符,这一部分是没有添加到类中的描述符
//matches,匹配结果,每一个matches[i]都有k个或者少于k个的匹配,
//针对相应的queryDescriptors
//k,找到k个最好结果,并且以质量递减返回
//compactResult,if false,matches的size与
//queryDescriptors的rows相同,if true,不包含完全掩盖查询符的
//匹配结果

[3]针对每一个查询集描述符,查找不超过最远距离的匹配

void DescriptorMatcher
     ::radiusMatch(const Mat& queryDescriptors, 
                   const Mat& trainDescriptors,           
                   vector<vector<DMatch>>& matches, 
                   float maxDistance, 
                   const Mat& mask=Mat(), 
                   bool compactResult=false ) const
//queryDescriptors,查询集的描述符
//trainDescriptors,训练集的描述符,这一部分是没有添加到类中的描述符
//matches,匹配结果,matches[i]是queryDescriptors中对应的匹
//配结果,并且匹配描述符间的距离小于maxDistance,返回的顺序是以距
//离递减排序
//maxDistance,匹配描述符间的最远距离

[4]描述符匹配类的创建

Ptr<DescriptorMatcher> DescriptorMatcher
::create(const string& descriptorMatcherType)
/*返回相应描述符匹配类的指针
 *描述符匹配类的名称:
 *BruteForce(默认L2)
 *BruteForce-L1
 *BruteForce-Hamming
 *BruteForce-Hamming(2)
 *FlannBased
 */

[5]向类中增加用于训练的描述符

void DescriptorMatcher::add(const vector<Mat>& descriptors)
//descriptors,每一个descriptors[i]来自同一张train image的描述符

[6]训练描述符匹配类

void DescriptorMatcher::train()
//训练描述符匹配类,针对所有匹配方法,在matching之前train都会运
//行

6>DrawMatchesFlags
matches和Keypoints的flag类

struct DrawMatchesFlags
{
    enum
    {
        DEFAULT = 0, 
        //输出图像会被创建,源图像,matches,keypoints会被绘制
        DRAW_OVER_OUTIMG = 1,
        //输出图像不会被创建,在已有图像上绘制            
        NOT_DRAW_SINGLE_POINTS = 2, 
        //未匹配关键点不会被绘制
        DRAW_RICH_KEYPOINTS = 4
        //关键点的方向和大小都会被绘制 
    };
};                   

2,特征点检测
1>drawKeypoints
绘制检测到的关键点

void drawKeypoints(const Mat& image, 
                   const vector<KeyPoint>& keypoints,   
                   Mat& outImage, 
                   const Scalar& color=Scalar::all(-1), 
                   int flags=DrawMatchesFlags::DEFAULT )
//image,源图像
//keypoints,源图像中的关键点
//outImage,输出图像
//color,颜色
//flags,绘制标记

2>FastFeatureDetector
使用FAST算法检测关键点
检测步骤:
[1]以某个点为中心做一个圆,根据圆上的像素值判断该店是否为关键点
[2]如果存在这样一段圆弧,它的连续长度超过周长的3/4,并且他上面的所有像素强度都与圆心强度值明显不同,则认定这是一个关键点

class FastFeatureDetector : public FeatureDetector

应用:

//创建关键点探测器
cv::Ptr<cv::FeatureDetector> detector=
cv::FeatureDetector::create("FAST");
//也可以(效果相同):
//FastFeatureDetector dector

//关键点检测
std::vector<cv::KeyPoint> keypoint;
detector->detect(Image,keypoint);

//关键点绘制
cv::drawKeypoints(Image,keypoint,result,cv::Scalar::all(-1));

原图与效果图:
这里写图片描述 这里写图片描述
time(s):0.00469645

2>BRISK
多尺度FAST特征检测,既可以快速检测,又不随尺度改变而改变。
检测步骤:
[1]首先通过两个下采样过程构建一个图像金字塔
[2]在图像金字塔上应用FAST特征检测器,只有局部最大值的像素才可能称为关键点
[3]这个点与上下两层的相邻像素比较评分,如果它的评分在尺度上也更高,则它是个关键点

应用:

//另外一种特征点探测器的创建方法
cv::Ptr<cv::FeatureDetector> detector=
new cv::BRISK(1,3);
//通过BRISK的构造函数,来创建特征探测器
//有两个参数:
//[1]thresh,为FAST/AGAST的阈值,是中央像素与周围像素之间的区别
//[2]octaves,检测八度,金字塔的多尺度数目

效果图:
这里写图片描述
time(s):0.793073

3>SIFT
尺度不变特征转换,采用图像空间和尺度空间的局部最大值,使用拉普拉斯滤波器响应。
SIFT基于浮点内核计算特征,则在空间和尺度上更加精确,计算效率更低。
效果图:
这里写图片描述
time(s):0.0790308

4>SURF
是SIFT的加速版,尺度不变的特征检测,不仅在任何尺度下拍摄的物体都能够检测到相同的特征点,而且每个被检测的特征都对应一个尺度因子。
检测步骤:
[1]针对每个像素计算Hessian矩阵,该矩阵衡量了一个函数的局部曲率。
[2]根据矩阵的行列式,得到曲率的强度,在普通空间和尺度空间,Hessian行列式达到了局部最大值,则认为是尺度不变特征
应用:

 cv::Ptr<cv::FeatureDetector> detector=
                              new cv::SURF(900);
//hessianThreshold,为hessian矩阵的阈值 

效果图:
这里写图片描述
time(s):0.0227669

5>ORB
定向FAST和旋转BRISK
检测步骤:
[1]创建图像金字塔,在具有关键点评分的位置接受N个强度最大的关键点(使用Harris角点强度衡量方法)
[2]每个被检测的关键点总是关联了一个方向,ORB使用关键点周围的圆形邻域的重心方向

缺点:
[1]不具备旋转不变性
[2]对噪声敏感
[3]不具备尺度不变性

应用:

cv::Ptr<cv::FeatureDetector> detector=
            new cv::ORB(200,1.2,8);
//参数:
//[1]nfeatures,最多的关键点总数
//[2]scaleFactor,图层之间的缩放因子
//[3]nlevels,金字塔的图层数量            

效果图:
这里写图片描述
time(s):0.00400557

3,特征匹配
1>drawMatches
两张image中匹配结果的绘制

void drawMatches
(const Mat& img1, const vector<KeyPoint>& keypoints1, 
 const Mat& img2, const vector<KeyPoint>& keypoints2, 
 const vector<DMatch>& matches1to2, Mat& outImg, 
 const Scalar& matchColor=Scalar::all(-1), 
 const Scalar& singlePointColor=Scalar::all(-1), 
 const vector<char>& matchesMask=vector<char>(), 
 int flags=DrawMatchesFlags::DEFAULT )
 /*img1,第一张图像,keypoints1,第一张图像的关键点
  *img2,第二张图像,keypoints2,第二张图像的关键点
  *matches1to2,从第一张图像到第二张图像的匹配结果
  *outImg,输出图像
  *matchColor,匹配上的颜色,singlePointColor,关键点没有匹配
  *上的颜色
  */

2>图像匹配

[1]BFMatcher类
蛮力描述符匹配器,针对第一组每一个descriptor,在第二组与每一个descriptor进行比较得到最有可能的

class BFMatcher : public DescriptorMatcher

//构造函数
BFMatcher::BFMatcher(int normType=NORM_L2, 
                     bool crossCheck=false)
/*normType(其中之一):
 *NORM_L1/NORM_L2,针对SIFT/SURF的descriptors
 *NORM_HAMMING,针对ORB/BRISK/BRIEF的descriptors
 *NORM_HAMMING2,针对ORB,并且WTA_K==3/4(ORB构造函数)
 *crossCheck,if false,针对每个描述符,找到k个最近邻
 *if true,knnMatch()方法k=1时,返回pair(i,j),则两组描述符中
 *第i,和第j个最相似
 */                     

[2]FlannBasedMatcher类
基于Flann的描述符匹配器,当描述符很多时比BFMatcher更有效,但是不支持允许匹配的描述符集

[3]说明
SIFT,SURF描述符类型float
ORB,BRIFT描述符类型uchar

支持float有FlannBasedMatcher,BFMatcher
支持uchar有BFMatcher

所以ORB,BRIFT的描述符类型只可使用BFMatcher

应用:
[1]常规match

int min=1000;
cv::SurfFeatureDetector detector(min);
std::vector<cv::KeyPoint> keypoint1;
std::vector<cv::KeyPoint> keypoint2;

//image均为灰度图
detector.detect(Image1,keypoint1);
detector.detect(Image2,keypoint2);

//计算关键点描述符
cv::SurfDescriptorExtractor extractor;
cv::Mat descriptor1,descriptor2;
extractor.compute(Image1,keypoint1,descriptor1);
extractor.compute(Image2,keypoint2,descriptor2);

//进行匹配
cv::BFMatcher matcher(cv::NORM_L2);
std::vector<cv::DMatch> matches;
matcher.match(descriptor1,descriptor2,matches);

//绘制匹配结果
cv::drawMatches(Image1,keypoint1,Image2,keypoint2,
                 matches,result);

效果图:
这里写图片描述
错误匹配较多,速度较慢

[2]match匹配改进

//利用Flann进行第一步匹配SURF的描述符
cv::FlannBasedMatcher matcher;
std::vector<cv::DMatch> matches;
matcher.match(descriptor1,descriptor2,matches);

double min_distance=1000,max_distance=0;
for(int i=0;i<matches.size();i++)
{
  //在已经match好的结果中,找到描述符之间距离的最大值和最小值
  double dist=matches[i].distance;
  if(dist>max_distance) max_distance=dist;
  if(dist<min_distance) min_distance=dist;
}
//对于描述符之间距离超过2倍最小距离的进行排除
std::vector<cv::DMatch> rmatches;
for(int i=0;i<matches.size();i++)
if(matches[i].distance<std::max(2*min_distance,0.02))
{
 rmatches.push_back(matches[i]);
}

效果图:
这里写图片描述

[3]knnMatch

//knnMatch针对每一个描述符,找到2个最好的匹配,并且距离递增
std::vector<std::vector<cv::DMatch>> matches;
matcher.knnMatch(descriptor1,descriptor2,matches,2);

//对于matches[i][0].distance<0.7*matches[i][1].distance进行采纳
std::vector<cv::DMatch > rmatches;
for(int i=0;i<matches.size();i++)
if(matches[i][0].distance<0.7*matches[i][1].distance)
rmatches.push_back(matches[i][0]);

//进行绘制
cv::drawMatches(Image1,keypoint1,Image2,keypoint2,
                rmatches,result);

效果图:
这里写图片描述

3>寻找已知物体

[1]findHomography
通过匹配的关键点,找出相应变换矩阵H
这里写图片描述
反向投影误差:
这里写图片描述

Mat findHomography(InputArray srcPoints, 
                   InputArray dstPoints, int method=0, 
                   double ransacReprojThreshold=3, 
                   OutputArray mask=noArray() )
/*srcPoints,dstPoints,分别为源图像和目标图像上的对应点,为Mat
 *的CV_32FC2或者vector<Point2f>
 *method,用于计算H矩阵的方法:
 *0,常规方法使用所有点
 *CV_RANSAC,使用RANSAC鲁棒性方法
 *CV_LMEDS,最小中值鲁棒性方法
 *ransacReprojThreshold,处理点对为内围层时,允许重投影误差的最
 *大值。
 */                   

RANSAC:处理几乎任何比例异常,但需要阈值
LMEDS:只有超过50%的可利用点,才能确定
如果影响很小就用default

[2]perspectiveTransform
执行向量的透视矩阵变换
变换每一个src元素
这里写图片描述
这里写图片描述
这里写图片描述

void perspectiveTransform(InputArray src, OutputArray dst, 
                          InputArray m)
//src,dst同size和type
//m,变换矩阵,3*3或者4*4矩阵

应用:

//在匹配完成的基础上,如果已匹配数目达到一定数量,则允许查找
//防止出现bug
if(rmatches.size()>4)
{
  std::vector<cv::Point2f> obj;
  std::vector<cv::Point2f> scene;
  //对于已匹配点,对点进行分组
  for(int i=0;i<rmatches.size();i++)
  {
   obj.push_back(keypoint1[rmatches[i].queryIdx].pt);
   scene.push_back(keypoint2[rmatches[i].trainIdx].pt);
  }

  //得到变换矩阵H
  cv::Mat H=cv::findHomography(obj,scene,CV_RANSAC);
  std::vector<cv::Point2f> obj_corner(4);

  //将obj的图像的四个角进行存储
  obj_corner[0]=cv::Point2f(0,0);
  obj_corner[1]=cv::Point2f(Image1.cols,0);
  obj_corner[2]=cv::Point2f(Image1.cols,Image1.rows);
  obj_corner[3]=cv::Point2f(0,Image1.rows);

  //进行转换,得到scene中的查找物体
  std::vector<cv::Point2f> scene_corner(4);
  cv::perspectiveTransform(obj_corner,scene_corner,H);

//进行标记
cv::line(result,scene_corner[0]+cv::Point2f(Image1.cols,0),
         scene_corner[1]+cv::Point2f(Image1.cols,0),
         cv::Scalar(0,0,255),2);
cv::line(result,scene_corner[1]+cv::Point2f(Image1.cols,0),
         scene_corner[2]+cv::Point2f(Image1.cols,0),
         cv::Scalar(0,0,255),2);
cv::line(result,scene_corner[2]+cv::Point2f(Image1.cols,0),
         scene_corner[3]+cv::Point2f(Image1.cols,0),
         cv::Scalar(0,0,255),2);
cv::line(result,scene_corner[3]+cv::Point2f(Image1.cols,0),
         scene_corner[0]+cv::Point2f(Image1.cols,0),
         cv::Scalar(0,0,255),2);
}

效果图:
这里写图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值