opencv 学习笔记

在这里插入图片描述

Mat的概念

Android中对图像是用bitmap格式来进行处理,而OpenCV中是采用Mat格式进行处理。所以我们在Android中使用OpenCV也要将Bitmp转化为Mat格式。Mat类用于表示一个多维的单通道或者多通道的数组。能够用来保存实数或复数的向量、矩阵,灰度或彩色图像,立体元素,张量以及直方图。简而言之, Mat就是用来保存多维的矩阵的。Mat对象中包含了图像的各种基本信息与图像像索数据。Mat是由头部与数据部分组成的,其中头部还包含一个指向数据的指针。我们把Mat可以视作就是图像矩阵。

Bitmap和Mat的转换Bitmap和Mat的转换方法

Bitmap bp =BitmapFactory.decodeResource(getResources(), R. drawable.d01);
Mat src = new Mat();
Utils.bitmapToMat(bp,src);

Mat可以转换为Bitmap在Android UI上进行显示,转换代码如下。

bitmap = Bitmap.createBitmap(width, height, Config.ARGB 8888);
Utils.matToBitmap(mat, bitmap)

2.1.3 Mat的位运算和算术运算
Mat格式的图像可以直接进行位运算和算术运算。位运算主要支持按位非、按位与、按位或、按位异或。算术运算主要支持加减乘除。这些运算其实实际上都是矩阵的运算。相应的OpenCV AP如下表。
在这里插入图片描述
比如要对两种图片进行and并输出结如下代码(注意两张尺寸要一样)

        Mat srcMat1;
        Mat srcMat2 ;
        Mar dstMat;
        try {
              srcMat1 = Utils.loadResource(this,R.drawable.bg_gradient_soft);
              srcMat2 = Utils.loadResource(this,R.drawable.bg_gradient_soft);
        } catch (IOException e) {
            e.printStackTrace();
        }
        Core.bitwise_and(srcMat1,srcMat2,dstMat);
        resultBitmap = Bitmap.createBitmap(dstMat.width(),dstMat.height(),Bitmap.Config.ARGB_8888);
        Utils.matToBitmap(dstMat,resultBitmap);
        dstImg.setImageBitmap(resultBitmap);

用完以后在onDestroy()里进行代码销毁
mat.release();

2.2颜色转换

图像色彩模式图像的色彩模式主要有以下几种:
位图模式位图模式是图像中最基本的格式,图像只有黑色和白色像素,是色彩模式中占有空间最小的,同样也叫做黑白图,它包含的信息量最少,无法包含图像中的细节,相当于只有0或者1。一副彩色图如果要转换成黑白模式,则一般不能直接转換,需要首先将图像转換成灰度模式。

灰度模式灰度模式即使用单一色调来表示图像,与位图模式不同,不像位图只有0和1,使用256级的灰度来表示图像,一个像素相当于占用8为一个字节,每个像素值使用0到255的亮度值代表,其中0为黑色, 255为白色,相当于从黑->灰->白的过度,通常我们所说的黑白照片就是这种模式,与位图模式相比,能表现出一定的细节,占用空间也比位图模式较大。

RGB模式RGB模式为我们经常见到的,被称为真色彩。RGB模式的图像有3个颜色通道,分布为红(Red) ,绿(Green)和蓝(Bule) ,每个都占用8位一个字节来表示颜色信息,这样每个颜色的取值范围为0-255,那么就三种颜色就可以有多种组合,当三种基色的值相等时表现出为灰色,三种颜色都为255即为白色,三种颜色都为0,即为黑色。RGB模式的图像占用空间要比位图,灰度图都要大,但表现出的细节更加明显。

HSV模式是根据日常生活中人眼的视觉对色彩的观察得而制定的一套色彩模式,最接近与人类对色彩的辨认的思考方式,所有的颜色都是用色彩三属性来描述

H:(色相) :是指从物体反射或透过物体传播的颜色

S:(饱和度):是指颜色的强度或纯度,表示色相中灰色成分所占的比例

V:(亮度):是指颜色对相对明暗程度,通常100%定义为白色; 0%为黑色

在这里插入图片描述
2.2.2 cvtColor()颜色转换函数
OpenCV中主要使用cvtColor ()函数进行颜色转换操作。函数原型如下:

Imgproc .cvtcolor(source mat, destination mat1, Color_Conversion_Code);

Color_Conversion_Code提供了丰富的颜色转换模式。
在这里插入图片描述

转换灰度

Bitmap bp = BitmapFactory.decodeResource(getResources(),R.drawable.shap);
        Utils.bitmapToMat(bp,srcMat);
        Imgproc.cvtColor(srcMat,dstMat,Imgproc.COLOR_BGR2HSV);
        Utils.matToBitmap(dstMat,bp);
        mImageView.setImageBitmap(bp);

在这里插入图片描述
在这里插入图片描述
转换二值图实例
图像二值化就是将图像上的像素点的灰度值设置为0或255,也就是将整个图像呈现出明显的黑白效果的过程。转换二值图的关键就是确定一个國值,整张图片中高于國值的点都置为255,低于國值的点都置为0,就可以呈现出明显的二值黑白效果。阈值的确定有两种方法,一种是手动指定,由开发者手动确定一个阈值,在一些专用场景下可以通过实验效果调整。手动闽值法手动國值法所使用的函数的是:
Imgproc.threshold ()

threshold(Mat src, Mat dst, double thresh, double maxval, int type);

参数:
src : Mat输入图像(注意这里要灰度图)
dst: Mat输出图像阈值操作结果填充在此图像
thresh : double阈值
maxval : double当type为THRESH-BINARY或THRESHBINARYINV时的最大值
type: int,阈值类型。对对象取國值的方式0: THRESH-BINARY : src(x,y) > thresh ? maxval : 0。当前像素点的灰度值>thresh,当前像素点值为maxval,反之为0

1: THRESH BINARY-INV : src(xy) > thresh ? 0 : maxval。当前像素点灰度值> thresh,当前像素点值为0反之为maxval

2: THRESH TRUNC : src(x,y) > thresh ? threshold: src(x,y)当前像素点灰度值> thresh,设定为thresh ,反之保持不变

3: THRESH TOZERO : src(x,y) > thresh ? src(x,y) : 0。当前像素点灰度值> thresh,当前像素点值保持不变,其他情况为0

4: THRESH TOZERO-INV : stc(x,y) > thresh ? 0 : src(x,y)。当前像素点灰度值> thresh ,当前像素点值为0,其他情况保持不变

手动阈值二值化转换代码

Imgproc.threshold(src, dst, 125, 255, Imgproc. THRESH BINARY);

其中125就是我们自己指定的國值。
效果如下
在这里插入图片描述
自动阈值法
OpenCV中也可以使用算法来自动计算阈值, OpenCV支持均值算法和高斯均值算法。它不是计算全局图像的阈值,而是根据图像不同区域亮度分布,计算其局部阈值,所以对于图像不同区域,能够自适应计算不同的值,因此被称为自适应國值法。

如果图像的各处亮度不一致在进行全局阈值(指定的阈值)二值化的时候会得到不太理想的结果。
在这里插入图片描述
而采用了局部咸值计算的话是计算某个邻域(局部)的均值、高斯加权平均(高斯滤波)来确定阈值。
对每个区域来说阈值是不一样的。局部阈值通常会得到较好的二值化效果,上图如果采用局部阈值的话可以得到如下的效果。

自动阈值所使用的函数是Imgproc.adaptiveThreshold ()
函数原型如下:

public static void adaptiveThreshold(Mat src, Mat dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C)

参数说明:
Src : Mat输入图像
dst: Mat输出图像阈值操作结果填充在此图像
maxValue : double分配给满足条件的像素的非零值

adaptiveMethod : int自定义使用的國值算法,
ADAPTIVE_THRESH_GAUSSIAN_C(高斯的)、 ADAPTIVE_THRESH_MEAN_C(平均的)

ADAPTIVE THRESHMEAN-C时, T(xy) = blockSize " blockSize [b】 blockSize 【b】 =邻域内(x.y)-C-ADAPTIVE THRESH-GAUSSIAN-C时, Txy) = blockSize * blockSize [b】 blocksize [b] =邻域内(x.y)-C与高斯窗交叉相关的加权总和

thresholdType : int阈值类型,只能是THRESH-BINARY、 THRESH-BINARY-INV

blockSize : int用来计算國值的邻域尺寸3,5,7等等,

奇数C:double减去平均值或加权平均值的常数,通常情况下,它是正的,但也可能是零或负。

代码实现:

Imgproc.adaptiveThreshold(srcMat,dstMat,255,Imgproc.ADAPTIVE_THRESH_MEAN_C,Imgproc.THRESH_BINARY,13,5);

效果如下
在这里插入图片描述

几何图形绘制

直线绘制几何图形绘制主要使用ImgProc类里的line、 rectangle, polylines, circle, ellipse等函数,也可以使用putText函数绘制文字,图形绘制函数一般在APP中起到画面标注的功能。

直线绘制line函数原型

public static void line(Mat img,Point pt1,Point pt2,Scalar color,int thickness);

参数:img:需要绘制的图像Mat
pt1:直线起点坐标。
pt2:直线终点坐标。
color:直线的颜色。
thickness:直线的宽度。
比如我们要在画面的左下到右上画一条蓝色的宽度为4的直线,可以这样调用。

Imgproc.line(srcMat,new Point(0,srcMat.height()),new Point(srcMat.width(),0),new Scalar(255,0,0),4);

效果如下
在这里插入图片描述
Android的坐标圆点在屏幕左上角(0,0),y向右变大,x向下变大

矩形绘制函数原型

public static void rectangle(Mat img, Point pt1, Point pt2, Scalar color, int thickness);

参数:
img,需要绘制的图像Mat
pt1,矩形左上角
pt2,矩形右下角
color,绘制直线的颜色
thickness,直线宽度。若为负值,表示填充

多边形绘制函数原型

public static void polylines(Mat img, List<MatOfPoint> pts, boolean isclosed, scalar color, int thickness);

参数:
img.输入图像
pts,多边形端点坐标列表
isClosed,是否闭合
color,绘制直线的颜色
thickness,直线宽度

圆形绘制函数原型:

public static void circle(Mat img, Point center, int radius, Scalar color, int thickness);

参数:
img,输入图像
center,圆心坐标
radius,圆半径
color,绘制直线的颜色
thickness,直线宽度。若为负值,表示填充

文字绘制函数原型:

public static void putText(Mat ing, string text, Point org, int fontFace, double fontScale, Scalar color, int thickr);

参数:
img,输入图像
text,文字内容.
org,文本字符串的左下角位置
fontFace,字体类型,可取值
在这里插入图片描述
调用代码:

Imgproc . putText(src,"I'm a cat",new Point(src .width() /2,src .height()/3),2,5,new Scalar(0,255,0),3);

效果如下
在这里插入图片描述

图像切割

颜色形状识别的第一步就是将原始图片中央部分的识别区域切割出来,去除干扰的画面元素。
在这里插入图片描述
自动切割和手动切割

切割图片可以自动切割也可以手动切割。自动切割的原理是对整图进行轮廓搜索,将搜索出的结果按轮廓面积进行分析,将面积最适合的那个作为识别区域进行切割。这种方法的优点在于不用手动去确定切割点坐标,缺点在于由于画面内容比较复杂,找到的轮廓可能会很多,分析轮廓的运算量较大会对Android设备带来较大的运算负担,造成识别速度较慢。

手动切割就是按上图的红框人工确定切割点,对每张图片进行切割。优点在于运算量很少,速度快。但对每次的拍摄位置的一致性要求较高。

Mat的切割
OpenCv对图像切割的方法可以用Rect类实现。定义一个Rect类对象,构造时的参数为切割区域左上角坐标和切割区域的宽度和高度,共4个参数。函数原型如下:

Rect (int x,int y,int weight, int height) ;

然后用Mat (Mat mat, Rect rect) ;

构造函数构造一个基于原图的部分切割图像。代码如下:

Rect rect = new Rect(182,82, 278,158)
 dstmat = new Mat(srcmat,rect);

然后可将切割后的dstmat转换为bitmap在界面上进行显示。但是显示完会发现颜色和原图不一致。

resultBitmap = Bitmap. createBitmap(dstmat.width(), dstmat.height(), Bitmap.Config. ARGB_8888);
utils.matToBitmap(dstmat, resultBitmap);
imgView.setImageBitmap (resultBitmap);

代码实例:

Rect rect = new Rect(182,82,300,200);
            dstMat = new Mat(srcMat,rect);
            resultBitmap = Bitmap.createBitmap(dstMat.width(),dstMat.height(),Bitmap.Config.ARGB_8888);
            Imgproc.cvtColor(dstMat,dstMat,Imgproc.COLOR_BGR2RGB);   //转换颜色
            Utils.matToBitmap(dstMat,resultBitmap);
            mImageView.setImageBitmap(resultBitmap);

在这里插入图片描述
颜色空间的转换
颜色不一致的原因在于OpenCV的Mat格式默认是采用BGR格式保存颜色,转成bitmap的时候是RGB模式,造成了R和B的错位。解决的方法就是在转bitmap之前将Mat的BGR颜色空间转成RGB,可采用cvtColor ()函数进行转换。

Imgproc.cvtColor(dstmat,dstmat, Imgproc.COLOR_BGR2RGB);

经测试颜色空间转换后颜色显示正常。

在这里插入图片描述

颜色识别

数字图像处理中常用的采用模型是RGB (红,绿,蓝)模型和HSV (色调,饱和度,亮度) , RGB广泛应用于彩色监视器和彩色视频摄像机,我们平时的图片一般都是RGB模型。而HSV模型更符合人描述和解释颜色的方式, HSV的彩色描述对人来说是自然且非常直观的。

HSV模型
HSV模型中颜色的参数分别是:色调(H: hue) ,饱和度(S: saturation) ,亮度(V: value) 。由A. R. Smith在1978年创建的一种颜色空间,也称六角锥体模型(Hexcone Model)。

色调(H: hue) :用角度度量,取值范围为0" ~ 360",从红色开始按逆时针方向计算,红色为0",绿色为120"蓝色为240"。它们的补色是:黄色为60",青色为180"品红为300";

饱和度(S: saturation) :取值范围为0.0~1.0,值越大,颜色越饱和。

亮度(V: value) :取值范围为0.0(黑色) ~ 1.0(白色)。
在这里插入图片描述

传统的RGB模型可以通过以下的公式转换为HSV模型。
在这里插入图片描述对于颜色识别来说一般我们只关注H分量的值就可以。在OpenCV中H被映射到(2. 180)的区间内, S和V都被映射到(0, 255)的区间内。常用颜色H分量值如下表

在这里插入图片描述
OpenCV中可以通过cvtColor0函数进行RGB到HSV的转换

hsvMat = new Mat();
Imgproc.cvtColor(dstMat,hsvMat,Imgproc.COLOR_BGR2HSV);

颜色检测
确定了需要检测的颜色HSV值以后在OpenCV中可以通过inRange()函数进行颜色检测. inRange)也叫颜色分割函数。函数原型:

public static void inRange(imgHsv, Scalar(ilowH, ilows, iLowV), Scalar(iHighH, iHighs, iHighV), imgThresholded);

参数:imgHSV :HSV颜色空间的源图

Scalar Low:HSV范围下限

Scalar High: HSV范围上限

imgThresholded:输出的图像这个函数的作用就是检测imgHSV图像的每一个像素是不是在Scalar(Low)和Scalar(High)之间,如果是,这个像素就设置为255,并保存在imgThresholded图像中,否则为0

代码:(检测红色)

Core.inRange(hsvMat,new Scalar(90,90,90),new Scalar(150,255,255),hsvMat);

在这里插入图片描述
当然有的时候我们在得到的图片上面会看到一些白色的噪点,或者是轮廓不连续有断开。这种情况我们需要执行一下开运算和闭运算来优化一下图像,将一些错误的连接断开和把一些错误的不连续接上。
在这里插入图片描述

开运算和闭运算

开运算

开运算的原理是通过先进行腐蚀操作,再进行膨胀操作得到,我们在移除小的对象时候很有用(假设物品是亮色,前景色是黑色),开运算可以去除噪声,消除小物体;在纤细点处分离物体;平滑较大物体的边界的同时并不明显改变其面积。

比如在二值化图像没处理好的时候会有一些白色的噪点,可以通过开运算进行消除。

在这里插入图片描述闭运算

闭运算是开运算的一个相反的操作,具体是先进行膨胀然后进行腐蚀操作。通常是被用来填充前景物体中的小洞,或者抹去前最物体上的小黑点。因为可以想象,其就是先将白色部分变大,把小的黑色部分挤掉,然后再将一些大的黑色的部分还原回来,整体得到的效果就是:抹去前景物体上的小黑点了。
在这里插入图片描述
在执行开运算和闭运算之前我们要确定一个运算核,这个运算核是一个小矩阵。圈蚀运算就是在整张图像上计算给定内核区域的局部最小值,用最小值替换对应的像索值。而膨胀运算就是在整张图像上计算给定内核区域的局部最大值,用最大值替换对应的像索值。

我们在优化图像时可以先执行开运算消除背景上的白色噪点,再运行闭运算消除前景上的黑色杂点。代码如下:

Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT,new Size(3,3));
Imgproc.morphologyEx(hsvMat,hsvMat,Imgproc.MORPH_OPEN,kernel);
Imgproc.morphologyEx(hsvMat,hsvMat,Imgproc.MORPH_CLOSE,kernel);

轮廓识别

轮廓识别
在OpenCV中,轮廓对应看一系列的点的集合,OpenCV提供了一个findContours()函数用来获得这些点的集合。函数原型如下:

public static void findContours(Mat image,List<MatofPoint> contours,Mat hierarchy, int mode,int method)

image一个8位的单通道图像(二值化的图)。非零像素被视为1, 0像素仍然是0。图像被当作二进制来处理。如果使用的mode是RETR-CCOMP或者RETR-FLOODFILL,那么输入的图像类型也可以是32位单通道整型,即CV-32SC1

contours检测到的轮廓。一个MatOfPoint保存一个轮廓,所有轮廓放在List中。

hierarchy可选的输出。包含轮廓之间的联系。4通道矩阵,元素个数为轮廓数量。通道【0】 ~通道【3】对应保存:后个轮廓下标,前一个轮廓下标,父轮廓下标,内嵌轮廓下标。如果没有后一个,前一个,父轮廓,内轮廓,那么该通道的值为-1

mode 轮廓检索模式。
在这里插入图片描述
●method 轮廓近似法。

在这里插入图片描述调用findContours()函数对进行过颜色识别的二值图进行轮廓识别。

Mat outMat = new Mat();
                Imgproc.findContours(hsvMat,contours,outMat,Imgproc.RETR_EXTERNAL,Imgproc.CHAIN_APPROX_SIMPLE);
                contoursSize = contours.size();
                Toast.makeText(TestActivity.this,"轮廓" + contoursSize,Toast.LENGTH_LONG).show();

轮廓绘制

为了更直观的查看轮廓识别的效果,我们可以通过绘制轮廓函数drawContours()将识别到的轮廓绘制出来。函数原型如下:

public static void drawContours(Mat src,List<MatofPoint> contours,int contourIdx, Scalar & color, int thickness)

参数:
src:目标图像

contours:输入的所有轮廓(每个轮廓以点集的方式存储)

contoursldx:指定绘制轮廓的下标(若为负数,则绘制所有轮廓)

color:绘制轮廓的颜色

thickness:绘制轮廓的线的宽度(若为负数,则填充轮廓内部)轮廓线会直接绘制在src上,但是如果src是二值图的话轮廓线也会二值化,所以建议把轮廓线绘制在原图上。

代码如下: (绘制宽度为4的蓝色的轮廓线)

Imgproc.drawContours(dstMat,contours,-1,new Scalar(0,0,255),4);
                Utils.matToBitmap(dstMat,resultBitmap);
                mImageView.setImageBitmap(resultBitmap);

在这里插入图片描述

形状识别

多边形拟合

轮廓点集合找到以后我们可以通过多边形拟合的方式来寻找由轮廓点所组成的多边形的顶点。 approxPolyDP()函数功能是把一个连续光滑曲线折线化,对图像轮廓点进行多边形拟合。简单来说就是该函数是用一条具有较少顶点的曲线/多边形去逼近另一条具有较多顶点的曲线或多边形。 approxPolyDP函数的原理如下:

(1)在曲线首尾两点A, B之间连接一条直线AB,该直线为曲线的弦;

(2)得到曲线上离该直线段距离最大的点C,计算其与AB的距离d

(3)比较该距离与预先给定的阈值threshold的大小,如果小于threshold,则该直线段作为曲线的近似,该段曲线处理完毕

(4)如果距离大于阈值,则用C将曲线分为两段AC和BC,并分别对两段取信进行1~3的处理。

(5)当所有曲线都处理完毕时,依次连接各个分割点形成的折线,即可以作为曲线的近似。

效果如下

在这里插入图片描述
approxPolyDP()函数原型如下:

public static void approxPolyDP( MatOfPoint2f curve ,MatOfPoint2f approxcurve, double epsilon,boolean closed)

参数

curve:输入的轮廓点集合

approxCurve:输出的轮廓点集合。最小包容指定点集,保存的是多边形的顶点。

epsilon:拟合的精度,原始曲线和拟合曲线间的最大值

closed:是否为封闭曲线。如果为true,表示遍近曲线为封闭曲线。

其中逼近精度epsilon可以手动指定,也可以通过curve轮廓点的个数进行计算。

epsilon = a * Imgproc.arcLength(curve, true);

其中arcLength是计算轮廓点的个数,也就是周长。a可按不同的图像测试取得最佳值。

对单个轮廓进行操作

MatOfPoint2f contours2f = new MatOfPoint2f(contours.get(1).toArray());
            double epsilon = 0.04 * Imgproc.arcLength(contours2f,true);
            MatOfPoint2f approxCurve = new MatOfPoint2f();
            Imgproc.approxPolyDP(contours2f,approxCurve,epsilon,true);

集合 approxCurve中存放着多边形页点坐标的列表。列表的行值就是顶点的个数。

形状的识别方法,

顶点判断法

形状识别的方法有很多种,本案例采用最简单的一种就是直接根据多边形顶点的个数进行判断。这种方法最简单但精度不高,只能识别差别较大的几种形状。
在这里插入图片描述

if (approxCurve.rows() == 3) {
                Toast.makeText(TestActivity.this,"三角形",Toast.LENGTH_LONG).show();
            }
            if (approxCurve.rows() == 4) {
                Toast.makeText(TestActivity.this,"正方形",Toast.LENGTH_LONG).show();
            }
            if (approxCurve.rows() > 4) {
                Toast.makeText(TestActivity.this,"圆形",Toast.LENGTH_LONG).show();
            }

自身面积与外接矩形面积比

也可以采用自身面积与外接矩形面积比的方法,不同的形状的自身面积和外接矩形面积比通常区别较大。

在这里插入图片描述
从图上可以看出圆形的外接矩形面积与自身面积之比大约在85%左右,三角形通常在50%左右,矩形大约在95%。

信号分析法
使用Moments ()函数计算多边形的重点,求绕多边形一周重心到多边形轮廓线的距离。把距离值形成信号曲线图,我们可以看到不同的形状信号曲线图区别很大。信号分析法可以识别多种类型的多边形形状。

在这里插入图片描述

在这里插入图片描述

在开和闭的运算后还是有杂点
在这里插入图片描述我们会发现有一些杂点, HSV值范围只是一个理论值,在实际应用的时候可能会包含进一些错误的点。我们下一步去识别轮廓的话会发现识别出3个轮廓,很显然有2个是错误的。去除这样的错误一方面可以用前期的图像优化(开运算、闭运算) ,也可以用后期的杂点过滤。

这里介绍一种杂点过滤方法。我们可以使用lmgproc.contourArea(Mat mat)函数计算多边形的面积(也就是内部点的个数) ,在判断形状时将面积过小的杂点去除点。可参考如下的代码:

 if (Imgproc.contourArea(contours.get(i)) > 10) {  //判断是不是杂点
                MatOfPoint2f contours2f = new MatOfPoint2f(contours.get(i).toArray());
                double epsilon = 0.04 * Imgproc.arcLength(contours2f,true);
                MatOfPoint2f approxCurve = new MatOfPoint2f();
                Imgproc.approxPolyDP(contours2f,approxCurve,epsilon,true);
                if (approxCurve.rows() == 3) {
                    Toast.makeText(TestActivity.this,"三角形",Toast.LENGTH_LONG).show();
                }
                if (approxCurve.rows() == 4) {
                    Toast.makeText(TestActivity.this,"正方形",Toast.LENGTH_LONG).show();
                }
                if (approxCurve.rows() > 4) {
                    Toast.makeText(TestActivity.this,"圆形",Toast.LENGTH_LONG).show();
                }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值