赞
踩
目录
介绍图像分类和图像内容分类算法,这里将介绍一些简单而有效的方法和目前一些性能最好的分类器,并运用它们解决两类和多类分类的问题。
分类算法中,KNN(K临近分类法)是最简单且用的最多的一种方法。通过将要分类的对象与训练集中已知类标记的所有对象进行对比,并由k近邻对指派到哪个类进行投票。这种方法分类效果较好但还是会出现很多弊端,如同K-means聚类算法一样,需要进行k值的预设,k值的选择将会影响分类的性能;此外这种方法还要求将整个训练集存储起来,当训练集非常大时,搜索将会变的很慢。因此对于大的训练集采取某些装箱形式通常会减少对比的次数。
实现最基本的KNN形式非常简单。给定训练样本集和对应的标记列表,下面的代码可以用来完成这一工作,这些训练集样本和标记可以在一个数组里成行摆放或者干脆摆放在列表里。
- 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方法就没必要存储并将训练数据作为参数来传递。用一个字典来存储邻近标记,我们就可以使用文本字符串或数字来表示标记。
建立一些简单的二维示例数据集来说明并可视化分类器的工作原理:
- from numpy.random import randn
- from numpy import *
- import 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_test.pkl', 'w') 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_test', 'w') as f:
- pickle.dump(class_1, f)
- pickle.dump(class_2, f)
- pickle.dump(labels, f)
根据书中给出代码运行程序,出现报错:
这是由于python3给open函数添加了名为encoding的新参数,默认值为utf-8,不在接受包含二进制数据bytes的参数。这里需要将
with open('points_normal_test.pkl', 'w') as f:
改成
with open('points_normal_test.pkl', 'wb') as f:
此时就能成功运行了,生成两个pkl文件。
现在观察如何用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()
使用Pickle模块创建一个KNN分类器模型,载入两个数据集,并在控制台上打印第一个数据点估计出来的类标记。利用classify函数可视化所有测试数据点的分类,并展示分类器将两个不同的类分开的怎么样。
输出结果为:
这里使用了plot_2D_boundary()函数:
- def plot_2D_boundary(plot_range,points,decisionfcn,labels,values=[0]):
- """ Plot_range是(xmin,xmax,ymin,ymax), points是类数据点列表, decisionfcn为评估函数,
- labels是函数decidionfcn关于每个类返回的标记列表,
- """
-
- clist = ['b','r','g','k','m','y'] # 不同类用不同颜色
-
- # 在网格上进行评估,并画出决策函数的边界
- x = arange(plot_range[0],plot_range[1],.1)
- y = arange(plot_range[2],plot_range[3],.1)
- xx,yy = meshgrid(x,y)
- xxx,yyy = xx.flatten(),yy.flatten() # 网格中的x,y坐标点列表
- zz = array(decisionfcn(xxx,yyy))
- zz = zz.reshape(xx.shape)
- # 以value画出边界
- contour(xx,yy,zz,values)
-
- # 对于每个类,用*画出分类正确的点,用o画出分类不正确的点
- for i in range(len(points)):
- d = decisionfcn(points[i][:,0],points[i][:,1])
- correct_ndx = labels[i]==d
- incorrect_ndx = labels[i]!=d
- plot(points[i][correct_ndx,0],points[i][correct_ndx,1],'*',color=clist[i])
- plot(points[i][incorrect_ndx,0],points[i][incorrect_ndx,1],'o',color=clist[i])
-
- axis('equal')
从上图的结果中我们可以看到分界线将两个数据集分割开,其中星号为正确的分类的点,圈点为分类错误的点,分割线又可以称为决策边界。
要对图像进行分类时,需要一个特征向量来表示一幅图像,前面有学过用平均RGB像素值和PCA系数作为图像的特征向量,这里将用稠密SIFT特征向量。
在整幅图像上用一个规划的网格应用SIFT描述子可以得到稠密SIFT的表达形式。
- def process_image_dsift(imagename,resultname,size=20,steps=10,force_orientation=False,resize=None):
- """ 用密集采样的SIFT描述子处理一幅图像,并将结果保存在一个文件里,可选的输入:
- size: 特征的大小
- steps: 位置之间的步长
- force_orientation:是否强迫计算描述子的方位(False表示所有方位都是朝上的),用于调整图像大小的元组
- """
-
- im = Image.open(imagename).convert('L')
- if resize!=None:
- im = im.resize(resize)
- m,n = im.size
-
- if imagename[-3:] != 'pgm':
- # 创建一个pgm文件
- im.save('tmp.pgm')
- imagename = 'tmp.pgm'
-
- # 创建帧,并保存到临时文件
- scale = size/3.0
- x,y = meshgrid(range(steps,m,steps),range(steps,n,steps))
- xx,yy = x.flatten(),y.flatten()
- frame = array([xx,yy,scale*ones(xx.shape[0]),zeros(xx.shape[0])])
- savetxt('tmp.frame',frame.T,fmt='%03.3f')
-
- if force_orientation:
- cmmd = str("sift "+imagename+" --output="+resultname+
- " --read-frames=tmp.frame --orientations")
- else:
- cmmd = str("sift "+imagename+" --output="+resultname+
- " --read-frames=tmp.frame")
- os.system(cmmd)
- print ('processed', imagename, 'to', resultname)
不同于先前的process_image()函数,为了使用命令行处理,用savetext()将帧数组存储在一个文本文件中,函数的最后一个参数可以在提取描述子之前对图像的大小进行调整。当force_orientation为真,则提取出的描述子会基于局部主梯度方向进行归一化,否则只能简单的朝上。
计算稠密SIFT描述子的具体代码如下:
- from PIL import Image
- from pylab import *
- from numpy import *
- from PCV.localdescriptors import dsift,sift
-
- dsift.process_image_dsift('D:\\picture\\test4.jpg', 'empire.sift', 90, 40, True)
- l,d = sift.read_features_from_file('empire.sift')
- im = array(Image.open('D:\\picture\\test4.jpg'))
- sift.plot_features(im, l, True)
- show()
在这个应用中,通过使用稠密SIFT描述子来表示这些手势图像,并建立一个简单的手势识别系统,使用的是书中给出的图像集。
- from PCV.localdescriptors import sift, dsift
- from pylab import *
- import os
- from PIL import Image
-
- imlist = ['D:\\picture\\train\\C-uniform02.ppm', 'D:\\picture\\train\\B-uniform01.ppm',
- 'D:\\picture\\train\\A-uniform01.ppm', 'D:\\picture\\train\\Five-uniform01.ppm',
- 'D:\\picture\\train\\Point-uniform01.ppm', 'D:\\picture\\train\\V-uniform01.ppm']
-
- 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()
结果图如下:
首先对每幅图像创建一个特征文件,文件名后缀为.dift,同时还将图片的分辨率调成了常见的固定大小。
这里定义一个复制函数,用以从文件中读取稠密SIFT描述子:
- from pylab import *
- import os
- from PCV.classifiers import knn
- from PCV.localdescriptors import sift, dsift
-
-
- def read_gesture_features_labels(path):
- featlist = [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.dsift')]
- features = []
- for featfile in featlist:
- l,d = sift.read_features_from_file(featfile)
- features.append(d.flatten())
- features = array(features)
- labels = [featfile.split('/')[-1][0] for featfile in featlist]
- return features,array(labels)
-
-
- def print_confusion(res,test_labels,classnames):
- n = len(classnames)
- class_ind=dict([(classnames[i],i)for i in range(n)])
- confuse = zeros((n,n))
- for i in range(len(test_labels)):
- confuse[class_ind[res[i]],class_ind[test_labels[i]]]+=1
- print('Confusion matrix for')
- print(classnames)
- print(confuse)
-
-
-
- def get_imlist(path):
-
- return [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.ppm')]
-
- features,labels = read_gesture_features_labels('D:\\picture\\train\\')
- test_features,test_labels = read_gesture_features_labels('D:\\picture\\test\\')
- classnames = unique(labels)
-
- # 测试 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)
除了KNN外另一种简单有效的分类器叫做贝叶斯分类器,这个分类器是一种基于贝叶斯条件概率定理的概率分类器,假设特征是彼此独立不相关的。它可以被非常有效的训练出来,原因在于每一个特征模型都是独立选取的。
尽管贝叶斯分类器很简单,但在实际应用中很有效尤其是对垃圾邮件的过滤,另外,一旦学习了这个模型就没有必要存储训练数据了,只需存储模型参数。
使用高斯概率分布模型的贝叶斯分类器的基本实现:
- 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()
输出结果如下图,该脚本将前10个二维数据点的分类结果打印输出到控制台。
使用PCA降维
由于稠密SIFT描述子的特征向量非常庞大,用数据拟合模型进行降维处理就是一个很好的处理方法了。PCA非常适合用于降维处理。下面是利用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)
SVM是一类强大的分类器,可以在很多分类问题中给出出现有水准很高的分类结果,最简单的SVM通过在高维空间中寻找一个最优线性分类面,并尽可能的将两类数据分开。
对于特征向量x的决策函数:
w是常规超平面,b是偏移量常数。函数的月阈值为0,能够很好地将两类数据分开,使其一类为正数,另一类为负数。通过在训练集上求解带有标记的特征向量的最优化问题,使得超平面在两类间具有最大分开间隔,从而找到上面决策函数中的参数w和b。 决策函数的常规解是训练集上某些特征向量的线性组合:
所以决策函数又可以写成:
i是从训练集中选出的部分样本,这个样本称为支持向量。
SVM的一个优势是可以使用核函数,该函数能够将特征向量映射到另一个不同维度的空间中,比如说高维度空间。使用核函数映射可以很有效的解决非线性或者很难的分类问题。
常见的核函数:
1、线性是最简单的情况,即在特征空间中的超平面是线性的,
2、多项式用次数为d的多项式对特征进行映射,
3、径向基函数,通常指数函数是一种极其有效的选择,
4、Sigmoid函数,一个光滑的超平面替代方案,
LibSVM是最好的、使用最为广泛的SVM实现工具包,可以在https://www.lfd.uci.edu/~gohlke/pythonlibs/#libsvm中下在对应版本的whl文件通过pip install文件名的指令进行安装。
下面为LibSVM在二维样本数据点上的运行:
- 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)
使用的是径向基函数训练SVM分类器。以下为得到的结果。
不同于前面的例子,这里再载入数据集后要将数组转换成列表,这是因为LibSVM不支持数组对象的输入,使用使用了内建函数map()进行转换,map()函数中用到了对角一个元素都会进行转换的list()函数。创建svm_problem对象,并为期设置一些参数,调用svm_train()求解该优化问题用以确定模型参数,之后就对其进行预测。
下面是核函数类型的介绍:
载入其他数据集进行测试:
- import pickle
-
- from pylab import *
- from numpy import *
- from libsvm.svmutil import *
- from PCV.tools import imtools
-
- # 用pickle模块在途二维样本点
- with open('points_normal_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()
结果如下图所示,显示了两个不同数据集在二维平面上的分布情况
要注意的是:
书中给出的代码中:
return array(svm_predict([0]*len(x),zip(x,y),model)[0])
在运行后会报出:
TypeError: type of x: <class 'zip'> is not supported! 的错误
这里要对zip函数添加限制,将其转换为list()
return array(svm_predict([0]*len(x),list(zip(x,y)),model)[0])
此时就能成功运行了。
OCR(光学字符识别)是一个理解手写或机写文本图像的处理过程,常见的例子九三通过扫码文件来提取文本。
对于这类分类问题,有10个类:数字1...9,以及一些什么也没有的单元格。给定没有东西的单元格类标号为0,则所有类标号是0-9。
首先确定选取怎样的特征向量来表示每一个单元格里的图像。这里将会用一些简单而有效的特征。输入一个特征,使用如下函数使得返回一个拉成一组数组后的灰度值特征向量:
- def scipy_misc_imresize(arr, size, interp='bilinear', mode=None):
- im = Image.fromarray(arr, mode=mode)
- ts = type(size)
- if np.issubdtype(ts, np.signedinteger):
- percent = size / 100.0
- size = tuple((np.array(im.size)*percent).astype(int))
- elif np.issubdtype(type(size), np.floating):
- size = tuple((np.array(im.size)*size).astype(int))
- else:
- size = (size[1], size[0])
- func = {'nearest': 0, 'lanczos': 1, 'bilinear': 2, 'bicubic': 3, 'cubic': 3}
- imnew = im.resize(size, resample=func[interp]) # 调用PIL库中的resize函数
- return np.array(imnew)
-
-
- def compute_feature(im):
- """ 对一个ocr图像块返回一个特征向量 """
- # 调整大小并去除边界
- norm_im = scipy_misc_imresize(im, (30,30))
- norm_im = norm_im[3:-3,3:-3]
- return norm_im.flatten()
imresize()用于减少特征向量的长度。接下来用函数训练数据:
- def load_ocr_data(path):
- """ 返回路径中所有图像的标记及OCR特征 """
- # 对以.jpg为后缀的所有文件创建一个列表
- imlist = [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.jpg')]
- # 创建标记
- labels = [int(imfile.split('\\')[-1][0]) for imfile in imlist]
- # 从图像中创建特征
- features = []
- for imname in imlist:
- im = array(Image.open(imname).convert('L'))
- features.append(compute_feature(im))
- return array(features), labels
该函数是将每个jpeg文件的文件名的第一个字母提取出来做类标记,并将其作为整形数据存储在labels列表里。
得到训练数据后,学习一个分类器,这里使用多类支持向量机。
- from pylab import *
- import numpy as np
- from PIL import Image
- from libsvm.svmutil import *
- from PCV.tools import imtools
- import os
- def scipy_misc_imresize(arr, size, interp='bilinear', mode=None):
- im = Image.fromarray(arr, mode=mode)
- ts = type(size)
- if np.issubdtype(ts, np.signedinteger):
- percent = size / 100.0
- size = tuple((np.array(im.size)*percent).astype(int))
- elif np.issubdtype(type(size), np.floating):
- size = tuple((np.array(im.size)*size).astype(int))
- else:
- size = (size[1], size[0])
- func = {'nearest': 0, 'lanczos': 1, 'bilinear': 2, 'bicubic': 3, 'cubic': 3}
- imnew = im.resize(size, resample=func[interp]) # 调用PIL库中的resize函数
- return np.array(imnew)
-
-
- def compute_feature(im):
- """ 对一个ocr图像块返回一个特征向量 """
- # 调整大小并去除边界
- norm_im = scipy_misc_imresize(im, (30,30))
- norm_im = norm_im[3:-3,3:-3]
- return norm_im.flatten()
-
- def load_ocr_data(path):
- """ 返回路径中所有图像的标记及OCR特征 """
- # 对以.jpg为后缀的所有文件创建一个列表
- imlist = [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.jpg')]
- # 创建标记
- labels = [int(imfile.split('\\')[-1][0]) for imfile in imlist]
- # 从图像中创建特征
- features = []
- for imname in imlist:
- im = array(Image.open(imname).convert('L'))
- features.append(compute_feature(im))
- return array(features), labels
- # 训练数据
- features, labels = load_ocr_data('D:\\BaiduNetdiskDownload\\PCV-book-data\\data\\sudoku_images\\sudoku_images\\ocr_data\\training\\')
- # 测试数据
- test_features, test_labels = load_ocr_data('D:\\BaiduNetdiskDownload\\PCV-book-data\\data\\sudoku_images\\sudoku_images\\ocr_data\\testing\\')
- # 训练一个线性SVM分类器
- features = list(map(list, features))
- test_features = map(list, test_labels)
- prob = svm_problem(labels, features)
- param = svm_parameter('-t 0')
- m = svm_train(prob, param)
- res = svm_predict(labels, features, m)
- res = svm_predict(test_labels, test_features, m)
代码会训练出一个线性SVM分类器,并在测试集上对该分类器的性能进行测试,理论上可以输出结果:
不过在运行后却出现了报错:
目前还不知道如何处理,有待进一步理解。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。