《Python 计算机视觉编程》学习笔记(八)

第 8 章 图像内容分类

引言

本章介绍图像分类和图像内容分类算法。

8.1 K邻近分类法( KNN)

在分类方法中,最简单且用得最多的一种方法之一就是 KNN( K-Nearest Neighbor ,K邻近分类法),这种算法把要分类的对象(例如一个特征向量)与训练集中已知类标记的所有对象进行对比,并由 k 近邻对指派到哪个类进行投票。这种方法通常分类效果较好,但是也有很多弊端:与 K-means 聚类算法一样,需要预先设定 k 值, k 值的选择会影响分类的性能;此外,这种方法要求将整个训练集存储起来,如果训练集非常大,搜索起来就非常慢。对于大训练集,采取某些装箱形式通常会减少对比的次数从积极的一面来看,这种方法在采用何种距离度量方面是没有限制的;实际上,对于你所能想到的东西它都可以奏效,但这并不意味着对任何东西它的分类性能都很好。另外,这种算法的可并行性也很一般。

实现最基本的 KNN 形式非常简单。给定训练样本集和对应的标记列表,下面的代码可以用来完成这一工作。这些训练样本和标记可以在一个数组里成行摆放或者干脆摆放列表里,训练样本可能是数字、字符串等任何你喜欢的形状。将定义的类对象添加到名为 knn.py 的文件里:

class KnnClassifier(object):
    
    def __init__(self,labels,samples):
        """ 使用训练数据初始化分类器 """
        
        self.labels = labels
        self.samples = samples
    
    def classify(self,point,k=3):
        """ 在训练数据上采用k近邻分类,并返回标记 """
        
        # 计算所有训练数据点的距离
        dist = array([L2dist(point,s) for s in self.samples])
        
        # 对它们进行排序
        ndx = dist.argsort()
        
        # 使用字典存储k近邻
        votes = {}
        for i in range(k):
            label = self.labels[ndx[i]]
            votes.setdefault(label,0)
            votes[label] += 1
            
        return max(votes)
 
 
def L2dist(p1,p2):
    return sqrt( sum( (p1-p2)**2) )
 
def L1dist(v1,v2):
    return sum(abs(v1-v2))

定义一个类并用训练数据初始化非常简单 ; 每次想对某些东西进行分类时,用 KNN方法,我们就没有必要存储并将训练数据作为参数来传递。用一个字典来存储邻近标记,我们便可以用文本字符串或数字来表示标记。在这个例子中,我们用欧式距
离 (L2) 进行度量,当然,如果你有其他的度量方式,只需要将其作为函数添加到上面代码的最后。

一个简单的二维示例

我们首先建立一些简单的二维示例数据集来说明并可视化分类器的工作原理,下面的脚本将创建两个不同的二维点集,每个点集有两类,用 Pickle 模块来保存创建的数据:


# 创建二维样本数据
n = 200
# 两个正态分布数据集
class_1 = 0.6 * randn(n, 2)
class_2 = 1.2 * randn(n, 2) + array([5, 1])
labels = hstack((ones(n), -ones(n)))
# 用pickle模块保存
with open('points_normal.pkl', 'wb') as f:
    pickle.dump(class_1, f)
    pickle.dump(class_2, f)
    pickle.dump(labels, f)
# 正态分布,并使数据成环绕状分布
class_1 = 0.6 * randn(n, 2)
r = 0.8 * randn(n, 1) + 5
angle = 2 * pi * randn(n, 1)
class_2 = hstack((r * cos(angle), r * sin(angle)))
labels = hstack((ones(n), -ones(n)))
# 用pickle保存
with open('points_ring.pkl', 'wb') as f:
    pickle.dump(class_1, f)
    pickle.dump(class_2, f)
    pickle.dump(labels, f)

用不同的保存文件名运行该脚本两次,例如第一次用代码中的文件名进行保存,第二次将代码中的 points_normal_t.pkl 和 points_ring_pkl 分别改为 points_normal_test.pkl 和 points_ring_test.pkl 进行保存。得到 4 个二维数据集文件,每个分布都有
两个文件,我们可以将一个用来训练,另一个用来做测试。

让我们看看怎么用 KNN 分类器来完成,用下面的代码来创建一个脚本:

import pickle
from pylab import *
from numpy import *
from PCV.classifiers import knn
from PCV.tools import imtools
 
# 用pickle载入二维数据点
with open('points_normal.pkl', 'rb') as f:
    class_1 = pickle.load(f)
    class_2 = pickle.load(f)
    labels = pickle.load(f)
model = knn.KnnClassifier(labels, vstack((class_1, class_2)))
 
# 用pickle模块载入测试数据
with open('points_normal_test.pkl', 'rb') as f:
    class_1 = pickle.load(f)
    class_2 = pickle.load(f)
    labels = pickle.load(f)
 
# 在测试数据集的第一个数据点上进行测试
print(model.classify(class_1[0]))
 
# 定义绘制函数
def classify(x, y, model=model):
    return array([model.classify([xx, yy]) for (xx, yy) in zip(x,y)])
 
# 绘制分类边界
imtools.plot_2D_boundary([-6, 6, -6, 6],[class_1, class_2],classify,[1,-1])
show()

plot_2D_boundary(plot_range,points,decisionfcn,labels,values=[0])函数我们直接调用PCV中的函数。

上面代码载入另一个数据集(测试数据集),并在你的控制台上打印第一个数据点估计出来的类标记。

为了可视化所有测试数据点的分类,并展示分类器将两个不同的类分开得怎样,我们增加了图像绘制函数。
画出来的结果如下图所示,正如你所看到的, kNN 决策边界适用于没有任何明确模型的类分布。

normal数据集
ring数据集

用 K 邻近分类器分类二维数据。每个示例中,不同颜色代表类标记,正确分类的点用星号表示,分类错误的点用圆点表示,曲线是分类器的决策边界

用稠密SIFT作为图像特征

我们来看如何对图像进行分类。要对图像进行分类,我们需要一个特征向量来表示一幅图像。在聚类一章我们用平均 RGB 像素值和 PCA 系数作为图像的特征向量;这里我们会介绍另外一种表示形式,即稠密 SIFT 特征向量。在整幅图像上用一个规则的网格应用 SIFT 描述子可以得到稠密 SIFT 的表示形式 1,我们可以利用 2.2 节的可执行脚本,通过添加一些额外的参数来得到稠密 SIFT 特征。创建一个名为 dsift.py 的文件(我是直接调用了PCV中的这个文件,所以就不展示代码了)

对比 2.2 节的 process_image() 函数,为了使用命令行处理,我们用 savetxt() 函数将帧数组存储在一个文本文件中,该函数的最后一个参数可以在提取描述子之前对图像的大小进行调整,例如,传递参数 imsize=(100, 100) 会将图像调整为 100× 100
像素的方形图像。最后,如果 force_orientation 为真,则提取出来的描述子会基于局部主梯度方向进行归一化;否则,则所有的描述子的方向只是简单地朝上。

利用类似下面的代码可以计算稠密 SIFT 描述子,并可视化它们的位置:

from PIL import Image
from pylab import *
from numpy import *
from PCV.localdescriptors import dsift,sift
 
dsift.process_image_dsift(
    'C:/hqq/document/python/computervision/ch08/picture/laboratory.jpg', 'empire.sift', 150, 150, True)
# """ 用密集采样的 SIFT 描述子处理一幅图像,并将结果保存在一个文件中。
# 可选的输入:特征的大小 size,位置之间的步长 steps,
# 是否强迫计算描述子的方位force_orientation( False 表示所有的方位都是朝上的),
# 用于调整图像大小的元组 """

l,d = sift.read_features_from_file('empire.sift')
im = array(Image.open('C:/hqq/document/python/computervision/ch08/picture/laboratory.jpg'))
sift.plot_features(im, l, True)
show()

输出结果如下:
在这里插入图片描述
上图为在实验室楼的图像上应用稠密 SIFT 描述子的例子。

图像分类: 手势识别

在这个应用中,我们会用稠密 SIFT 描述子来表示这些手势图像,并建立一个简单的手势识别系统。我们用静态手势( Static Hand Posture)数据库中的一些图像进行演示。在该数据库主页上下载数据较小的测试集 test set 16.3Mb,将下载后的所有图像放在一个名为 uniform 的文件夹里,每一类均分两组,并分别放入名为 train 和 test 的两个文件夹中。用上面的稠密 SIFT 函数对图像进行处理,可以得到所有图像的特征向量。

这里,再次假设列表 imlist 中包含了所有图像的文件名,可以通过下面的代码得到每幅图像的稠密 SIFT 特征

from PCV.localdescriptors import sift, dsift
from PCV.tools import imtools 

from pylab import *
import os
from PIL import Image

def getppmfile(ppmfilepath):
    """ 返回路径下后缀为ppm文件的路径 """
    return [os.path.join(ppmfilepath,f) for f in os.listdir(ppmfilepath) if f.endswith('.ppm')]

imlist = ['C:/hqq/document/python/computervision/ch08/gesture/train/C-uniform02.ppm', 
          'C:/hqq/document/python/computervision/ch08/gesture/train/B-uniform01.ppm',
          'C:/hqq/document/python/computervision/ch08/gesture/train//A-uniform01.ppm', 
          'C:/hqq/document/python/computervision/ch08/gesture/train/Five-uniform01.ppm',
          'C:/hqq/document/python/computervision/ch08/gesture/train/Point-uniform01.ppm',
          'C:/hqq/document/python/computervision/ch08/gesture/train/V-uniform01.ppm',
          ]
# imlist = getppmfile('C:/hqq/document/python/computervision/ch08/gesture/train')


figure()
for i, im in enumerate(imlist):
    print(im)
    dsift.process_image_dsift(im, im[:-3] + 'dsift', 10, 5, True, resize=(50,50))
    l, d = sift.read_features_from_file(im[:-3] + 'dsift')
    dirpath, filename = os.path.split(im)
    im = array(Image.open(im))
    # 显示手势含义title
    titlename = filename[:-14]
    subplot(2, 3, i + 1)
    sift.plot_features(im, l, True)
    title(titlename)
show()

只取了6幅图像的稠密 SIFT 描述子,如下所示:
在这里插入图片描述
定义一个辅助函数,用于从文件中读取稠密 SIFT 描述子,如下


def read_gesture_features_labels(path):
    # create list of all files ending in .dsift
    featlist = [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.dsift')]
    # read the features
    features = []
    for featfile in featlist:
        l,d = sift.read_features_from_file(featfile)
        features.append(d.flatten())
    features = array(features)
    # create labels
    labels = [featfile.split('/')[-1][0] for featfile in featlist]
    return features,array(labels)

注意,这里将图像分辨率调成了常见的固定大小。 这是非常重要的,否则这些图像会有不同数量的描述子,从而每幅图像的特征向量长度也不一样,这将导致在后面比较它们时出错。

我调成一样的了还是会产生不一样长度的特征向量。所以程序就死了,没想明白是哪里的问题。


def get_imlist(path):
    """    Returns a list of filenames for
        all jpg images in a directory. """

    return [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.ppm')]

imlist1 = get_imlist('C:/hqq/document/python/computervision/ch08/gesture/train')
imlist2 = get_imlist('C:/hqq/document/python/computervision/ch08/gesture/test')

# enumerate利用它可以同时获得索引和值
# 得到训练集的sift
for i, im in enumerate(imlist1):
    print(im)
    # 已经生成过了就注释掉
    dsift.process_image_dsift(im, im[:-3] + 'dsift', 10, 5, True, resize=(50,50))
# 得到测试集的sift
for i, im in enumerate(imlist2):
    print(im)
    dsift.process_image_dsift(im, im[:-3] + 'dsift', 10, 5, True, resize=(50,50))
        
#得到的测试集与训练集的特征个数不同,与图像尺寸有关,但我不知道设置多少合适、、、
features,labels = read_gesture_features_labels('C:/hqq/document/python/computervision/ch08/gesture/train/')
test_features,test_labels = read_gesture_features_labels('C:/hqq/document/python/computervision/ch08/gesture/test/')
classnames = unique(labels)

# test kNN
k = 1
knn_classifier = knn.KnnClassifier(labels,features)
res = array([knn_classifier.classify(test_features[i],k) for i in range(len(test_labels))])
# accuracy
acc = sum(1.0*(res==test_labels)) / len(test_labels)
print ('Accuracy:', acc)
print_confusion(res,test_labels,classnames)

上面代码为输出准确度和混淆矩阵。我的就是在转换为sift时,测试集图片输出的特征向量长度与训练集图片输出的特征向量长度不一样。所以报错了。但是不知道为什么?我按照书中说的将图片大小设置为(50,50)但是sift得到的特征向量长度还是不一样长。

问题先放着。后面再回来调。
在这里插入图片描述

8.2 贝叶斯分类器

另一个简单却有效的分类器是贝叶斯分类器(或称朴素贝叶斯分类器)。贝叶斯分类器是一种基于贝叶斯条件概率定理的概率分类器,它假设特征是彼此独立不相关的(这就是它“朴素”的部分)。贝叶斯分类器可以非常有效地被训练出来,原因在于
每一个特征模型都是独立选取的。尽管它们的假设非常简单,但是贝叶斯分类器已经在实际应用中获得显著成效,尤其是对垃圾邮件的过滤。贝叶斯分类器的另一个好处是,一旦学习了这个模型,就没有必要存储训练数据了,只需存储模型的参数。

该分类器是通过将各个特征的条件概率相乘得到一个类的总概率,然后选取概率最高的那个类构造出来的。

首先让我们看一个使用高斯概率分布模型的贝叶斯分类器基本实现,也就是用从训练数据集计算得到的特征均值和方差来对每个特征单独建模。

class BayesClassifier(object):
    
    def __init__(self):
        """ 使用训练集初始化分类器 """
        
        self.labels = []    # 类标签
        self.mean = []        # 类均值
        self.var = []        # 类方差
        self.n = 0            # 类别数
        
    def train(self,data,labels=None):
        """ 在数据data上训练,标记labels是可选的,默认0....n-1 """
 
        
        if labels==None:
            labels = range(len(data))
        self.labels = labels
        self.n = len(labels)
                
        for c in data:
            self.mean.append(mean(c,axis=0))
            self.var.append(var(c,axis=0))
        
    def classify(self,points):
        """ 通过计算得出的每一类的概率对数据点进行分类,并返回最可能的标记 """
        
        # 计算每一类的概率
        est_prob = array([gauss(m,v,points) for m,v in zip(self.mean,self.var)])
                
        print ('est prob',est_prob.shape,self.labels)
        # get index of highest probability, this gives class label
        ndx = est_prob.argmax(axis=0)
        
        est_labels = array([self.labels[n] for n in ndx])
        
        return est_labels, est_prob

该模型每一类都有两个变量,即类均值和协方差。 train() 方法获取特征数组列表(每个类对应一个特征数组),并计算每个特征数组的均值和协方差。 classify() 方法计算数据点构成的数组的类概率,并选概率最高的那个类,最终返回预测的类标记及概率值,同时需要一个高斯辅助函数:

def gauss(m,v,x):
    """ 用独立均值m和方差v评估d维高斯分布 """
    
    if len(x.shape)==1:
        n,d = 1,x.shape[0]
    else:
        n,d = x.shape
            
    # 协方差矩阵,减去均值
    S = diag(1/v)
    x = x-m
    # 概率的乘积
    y = exp(-0.5*diag(dot(x,dot(S,x.T))))
    
    # 归一化并返回
    return y * (2*pi)**(-d/2.0) / ( sqrt(prod(v)) + 1e-6)

该函数用来计算单个高斯分布的乘积,返回给定一组模型参数 m 和 v 的概率。
将该贝叶斯分类器用于上一节的二维数据,下面的脚本将载入上一节中的二维数据,并训练出一个分类器:

import pickle
from numpy import *
from pylab import *
from PCV.classifiers import bayes
from PCV.tools import imtools
 
# 用pickle模块在途二维样本点
with open('points_normal.pkl','rb') as f:
    class_1 = pickle.load(f)
    class_2 = pickle.load(f)
    labels = pickle.load(f)
# 训练贝叶斯分类器
bc = bayes.BayesClassifier()
bc.train([class_1, class_2], [1, -1])
# pickle模块载入测试数据
with open('points_normal_test.pkl','rb') as f:
    class_1 = pickle.load(f)
    class_2 = pickle.load(f)
    labels = pickle.load(f)
# 在某些数据点上进行测试
print(bc.classify(class_1[:10])[0])
# 绘制这些二维数据点及决策边界
def classify(x,y,bc=bc):
    points = vstack((x,y))
    return bc.classify(points.T)[0]
imtools.plot_2D_boundary([-6,6,-6,6],[class_1,class_2],classify,[1,-1])
show()

输出结果如下:
在这里插入图片描述
十个 1 是该脚本将前 10 个二维数据点的分类结果打印输出的。
可视化这一分类结果后得到下图:

normal数据集
ring数据集
每个例子中的颜色代表了类标记。正确分类的点用星号表示,误错分类的点用圆点表示,曲线是分类器的决策边界。可以看到基本分类准确。

用PCA降维

现在,我们尝试手势识别问题。由于稠密 SIFT 描述子的特征向量十分庞大(从前面的例子可以看到,参数的选取超过了 10 000),在用数据拟合模型之前进行降维处理是一个很好的想法。主成分分析,即 PCA(见 1.3 节),非常适合用于降维。下
面的脚本就是用 pca.py 中的 PCA 进行降维:

features, labels = read_gesture_features_labels('D:\\picture\\train\\')
test_features, test_labels = read_gesture_features_labels('D:\\picture\\test\\')
classnames = unique(labels)
 
 
V, S, m = pca.pca(features)
# 保持最重要的成分
V = V[:50]
features = array([dot(V, f - m) for f in features])
test_features = array([dot(V, f - m) for f in test_features])
 
# 测试贝叶斯分类器
bc = bayes.BayesClassifier()
blist = [features[where(labels == c)[0]] for c in classnames]
 
bc.train(blist, classnames)
res = bc.classify(test_features)[0]
acc = sum(1.0 * (res == test_labels)) / len(test_labels)
print('Accuracy:', acc)
print_confusion(res, test_labels, classnames)

还是之前的问题,产生的图片的特征向量长度不一致,导致后面没办法进行。换了几个版本的sift.exe,但是还是一样。这个问题应该是出在这个软件上了。或者是图片有问题???
在这里插入图片描述

8.3 支持向量机

SVM( Support Vector Machine,支持向量机)是一类强大的分类器,可以在很多分类问题中给出现有水准很高的分类结果。最简单的 SVM 通过在高维空间中寻找一个最优线性分类面,尽可能地将两类数据分开。对于一特征向量 x 的决策函数为:
f ( x ) = w ⋅ x − b f(\boldsymbol{x})=\boldsymbol{w} \cdot \boldsymbol{x}-b f(x)=wxb

其中 w 是常规的超平面, b 是偏移量常数。该函数月阈值为 0,它能够很好地将两类数据分开,使其一类为正数,另一类为负数。通过在训练集上求解那些带有标记 y i ∈ { − 1 , 1 } y_{i} \in\{-1,1\} yi{1,1}的特征向量 x i x_i xi 的最优化问题,使超平面在两类间具有最大分开间隔,从而找到上面决策函数中的参数 w 和 b。该决策函数的常规解是训练集上某些特征向量的线性组合:
w = ∑ i α i y i x i \boldsymbol{w}=\sum_{i} \alpha_{i} y_{i} \boldsymbol{x}_{i} w=iαiyixi
所以决策函数可以写为:
f ( x ) = ∑ α i y i x i ⋅ x − b f(\boldsymbol{x})=\sum \alpha_{i} y_{i} \boldsymbol{x}_{i} \cdot \boldsymbol{x}-b f(x)=αiyixixb
这里的 i 是从训练集中选出的部分样本,这里选择的样本称为支持向量,因为它们可以帮助定义分类的边界。

SVM 的一个优势是可以使用核函数( kernel function) ;核函数能够将特征向量映射到另外一个不同维度的空间中,比如高维度空间。通过核函数映射,依然可以保持对决策函数的控制,从而可以有效地解决非线性或者很难的分类问题。用核函数 K ( x i , x ) K(x_i , x) K(xi,x)替代上面决策函数中的内积 x i ⋅ x x_i · x xix

下面是一些最常见的核函数:

  • 线性是最简单的情况,即在特征空间中的超平面是线性的, K ( x i , x ) = x i ⋅ x ; K(x_i , x)=x_i · x; K(xi,x)=xix
  • 多项式用次数为 d的多项式对特征进行映射, K ( x i , x ) = ( γ x i ⋅ x + r ) d , γ > 0 ; K(x_i , x)=(γx_i · x+r)d, γ>0; K(xi,x)=(γxix+r)dγ>0
  • 径向基函数,通常指数函数是一种极其有效的选择, K ( x i , x ) = e ( − γ ∣ ∣ x i − x ∣ ∣ 2 ) , γ > 0 ; K(x_i , x)=e(-γ||x_i - x||2), γ>0; K(xi,x)=e(γ∣∣xix∣∣2)γ>0
  • Sigmoid 函数,一个更光滑的超平面替代方案, K ( x i , x ) = t a n h ( γ x i ⋅ x + r ) 。 K(x_i , x)=tanh(γx_i · x+r)。 K(xi,x)=tanh(γxix+r)

每个核函数的参数都是在训练阶段确定的。

对于多分类问题,通常训练多个 SVM,使每一个 SVM 可以将其中一类与其余类分开,这样的分类器也称为“ one-versus-all”分类器。

使用LibSVM

LibSVM 是最好的、使用最广泛的 SVM 实现工具包。 LibSVM 为 Python 提供了一个良好的接口(也为其他编程语言提供了接口)。

我们看看 LibSVM 在二维样本数据点上是怎样工作的。下面的脚本会载入在前面kNN 范例分类中用到的数据点,并用径向基函数训练一个 SVM 分类器:

import pickle
from libsvm.svmutil import *
from PCV.tools import imtools
 
# 用pickle模块在途二维样本点
with open('points_normal.pkl','rb') as f:
    class_1 = pickle.load(f)
    class_2 = pickle.load(f)
    labels = pickle.load(f)
 
# 转化成列表,便于使用LibSVM
class_1 = list(map(list, class_1))
class_2 = list(map(list, class_2))
labels = list(labels)
samples = class_1 + class_2  # 连接两个列表
# 创建SVM
prob = svm_problem(labels,samples)
param = svm_parameter('-t 2')
# 在数据上训练SVM
m = svm_train(prob,param)
# 在数据上分类效果如何
res = svm_predict(labels,samples,m)

在这里插入图片描述
结果表明该分类器完全分开了训练数据, 400 个数据点全部分类正确。

我们在调用方法训练分类器时添加了一个参数选择字符串,这些参数用于控制核函数的类型、等级及其他选项。尽管其中大部分超出了本书范围,但是需要知道两个重要的参数 t 和 k。参数 t 决定了所用核函数的类型,该选项是:
在这里插入图片描述
参数 k 决定了多项式的次数(默认为 3)。

现在,载入其他数据集,并对该分类器进行测试:

 
# 用pickle模块在途二维样本点
with open('points_ring_test.pkl','rb') as f:
    class_1 = pickle.load(f)
    class_2 = pickle.load(f)
    labels = pickle.load(f)
 
# 转化成列表,便于使用LibSVM
class_1 = list(map(list, class_1))
class_2 = list(map(list, class_2))
labels = list(labels)
samples = class_1 + class_2  # 连接两个列表
# 创建SVM
prob = svm_problem(labels,samples)
param = svm_parameter('-t 2')
# 在数据上训练SVM
m = svm_train(prob,param)
 
 
# 定义绘制函数
def predict(x,y,model=m):
    return array(svm_predict([0]*len(x),list(zip(x,y)),model)[0])
 
 
# 绘制分类边界
imtools.plot_2D_boundary([-6,6,-6,6],[array(class_1),array(class_2)],predict,[-1,1])
show()
normal数据集(SVM)
ring数据集(SVM)
normal数据集(bayes)
ring数据集(bayes)

对比SVM分类结果与贝叶斯分类结果,不难看出,SVM有更好的分类效果,他将类别做到完全的划分了开来。

8.4 小结

本章有点像是机器学习实战的浓缩版,介绍了KNN分类器、贝叶斯分类器、支持向量机三种分类器。将他们从分类数据集,引入到了分类图片。其实也是可以看作对数据集的分类,因为在分类图片时,也是将图片的特征通过使用sift得到。之后再对这些特征进行分类。为了更好处理,所以引入了稠密SIFT,由于这个稠密SIFT的特征向量特别庞大,我们又引入了PCA降维。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值