使用支持向量机(SVM)检测行人-基于opencv和python的学习笔记(十八)

版权声明:本文为博主原创文章,未经博主允许不得转载。https://blog.csdn.net/weixin_44474718/article/details/87200512

SVM的明确目标是将一个类的数据点和另一个类的数据点的边缘最大化。这也是SVM有时被叫作最大边缘分类器的原因。

一、支持向量机的线性实现

利用sklearn可以自己生成可控大小和复杂度的随机数据集。
  • datasets.make_classification([n_samples, ...]): This function
    generates a random n-class classification problem, where we can
    specify the number of samples, the number of features, and the number
    of target labels
  • datasets.make_regression([n_samples, ...]): This function generates a
    random regression problem
  • datasets.make_blobs([n_samples, n_features, ...]): This function
    generates a number of Gaussian blobs we can use for clustering

svm.setKernel(cv2.ml.SVM_LINEAR) # 可以设置不同的模式(直线)操作

np.c_是按行连接两个矩阵,就是把两矩阵左右相加,要求行数相等。

无论plt.contour还是plt.contourf,都是绘制三维图,其中前两个参数x和y为两个等长一维数组,第三个参数z为二维数组(表示平面点xi,yi映射的函数值)

from sklearn import datasets
import matplotlib.pyplot as plt
import numpy as np
from sklearn import model_selection as ms
import cv2
from sklearn import metrics


# 数据集生成
x , y = datasets.make_classification(n_samples=100,n_features=2,n_redundant=0,n_classes=2,random_state=7816)
# 数据集可视化
plt.scatter(x[:,0],x[:,1],c=y, s=100)          # 目标标签作为色彩值传入 c=y
plt.xlabel('x1 values')
plt.ylabel('x2 values')
plt.show()
# 数据预处理
x = x.astype(np.float32)               # 使数据兼容OpenCv
y = y * 2 - 1
x_train , x_test , y_train , y_test = ms.train_test_split(x , y , test_size= 0.2, random_state=42)
# 构建支持向量机
svm = cv2.ml.SVM_create()
svm.setKernel(cv2.ml.SVM_LINEAR)                    # 可以设置不同的模式(直线)操作
svm.train(x_train, cv2.ml.ROW_SAMPLE, y_train)      # 调用分类器的train方法来找到最优决策边界
_ , y_pred = svm.predict(x_test)                    # 调用分类器的predict方法来预测测试数据集中所有数据样本的目标标签
print(metrics.accuracy_score(y_test, y_pred))
# 决策边界可视化
def plot_decision_boundary (svm, x_test, y_test):
    x_min, x_max = x_test[:,0].min()-1, x_test[:,0].max()+1          # 使得所有的点在方格子内
    y_min, y_max = x_test[:,0].min()-1, x_test[:,0].max()+1
    h = 0.02      # 取样步长
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),np.arange(y_min, y_max, h))    # 创建网格
    # np.c_是按行连接两个矩阵,就是把两矩阵左右相加,要求行数相等。
    # 把这些(xx,yy)坐标值当作假设的数据点。因此可以把它们叠成为一个Nx2的矩阵
    x_hypo = np.c_[xx.ravel().astype(np.float32),yy.ravel().astype(np.float32)]   #最终的数据为一个Nx2的矩阵
    _, zz = svm.predict(x_hypo)
    # 得到的结构目标标签zz将会被用来创建一个特征结构的颜色映射
    zz = zz.reshape(xx.shape)
    plt.contourf(xx, yy, zz, cmap=plt.cm.coolwarm, alpha=0.8)    # 绘制三维图
    plt.scatter(x_test[:,0],x_test[:,1], c=y_test,s=200)
plot_decision_boundary(svm, x_test, y_test)
plt.show()
下图是随机生成的用于二分类问题的数据:

使用支持向量机(SVM)检测行人-基于opencv和python的学习笔记(十八)_第1张图片

下图是线性决策边界可视化:

使用支持向量机(SVM)检测行人-基于opencv和python的学习笔记(十八)_第2张图片

二、支持向量机的非线性实现(创建不同的核函数)

处理线性不可分的数据基本思路是创建原始特征的非线性组合。与把数据投影到更高维空间,在高维空间中的数据突然变得线性可分的说法是一样的。

使用支持向量机(SVM)检测行人-基于opencv和python的学习笔记(十八)_第3张图片

N维空间的线性决策边界叫作超平面。在六维空间中的决策边界时一个五维的超平面,三维空间的决策边界是一个二维平面,在二维空间中,是一条直线。

而在高维空间中,因为它增加了维度之间的数学投影要处理大量的额外单元。因此采用核机制来解决这个问题。

其中一类核函数叫作,径向基函数(Radial basis function):是一个取值仅仅依赖于离原点距离的实值函数。更常见的例子是高斯函数(也叫作贝尔曲线),通过调节高斯函数的标准差(也就是从中心距离下降的程度),可以创建大量复杂的决策边界,尤其是在更高维空间中。

常用的核函数:

- cv2.ml.SVM_LINEAR: 提供一个线性的决策边界。

- cv2.ml.SVM_POLY: 这个核在原始特征空间提供一个多项式的函数作为决策边界,通过 svm.setCoef0 (通常为0) 指定一个系数,通过 svm.setDegree.指定多项式的项数。

- cv2.ml.SVM_RBF: 高斯函数类型

- cv2.ml.SVM_SIGMOID: sigmoid函数类型

- cv2.ml.SVM_INTER: 由 OpenCV 3 新提供的. 它根据类别的直方图的相似性来分类。

# 创建不同的核函数
kernels = [cv2.ml.SVM_LINEAR, cv2.ml.SVM_INTER, cv2.ml.SVM_SIGMOID, cv2.ml.SVM_RBF]
kernels1 = ['LINEAR','INTER','SIGMOID','RBF']
for idx, kernel in enumerate(kernels):
    svm = cv2.ml.SVM_create()                             # 创建SVM
    svm.setKernel(kernel)                                 # 设置核函数
    svm.train(x_train, cv2.ml.ROW_SAMPLE, y_train)        # 训练数据集
    _1, y_pred1 = svm.predict(x_test)                     # 评分
    accuracy1 = metrics.accuracy_score(y_test, y_pred1)
    plt.subplot(2,2,idx + 1)
    plot_decision_boundary(svm, x_test, y_test)
    title1='% s : accuracy = %.2f' % (kernels1[idx], accuracy1)   print(title1)
    plt.title(title1)
plt.show()
不同核函数的边缘分界不一样:

使用支持向量机(SVM)检测行人-基于opencv和python的学习笔记(十八)_第4张图片

三、行人检测

检测和识别的区别,识别:对象是什么,检测:对象是否存在。

The basic idea behind most detection algorithms is to split up an image into many small patches, and then classify each image patch as either containing a pedestrian or not. This is exactly what we are going to do in this section. In order to arrive at our own pedestrian detection algorithm, we need to perform the following steps:

1. Build a database of images containing pedestrians.These will be ourpositivedata samples.

2. Build a database of images not containing pedestrians. These will be our negative data samples.

3. Train an SVM on the dataset.

4. Apply the SVM to every possible patch of a test image in order to decide whether the overall image contains a pedestrian.

获取数据集,从http://cbcl.mit.edu/software-datasets/PedestrianData.html下载,点击下载一个叫作http://cbcl.mit.edu/projects/cbcl/software-datasets/pedestrians128x64.tar.gz的文件。

方向梯度直方图(histogram of oriented gradients ,HOG),基本思想:图像中对象的局部形状和外观可以使用边缘方向的分布来表示。图像被分为多个连接在一起的小区域,然后编译其中的方向梯度直方图(或者边缘直方图),接下来,通过把不同的直方图连接起来就得到了描述符。下图所示:

使用支持向量机(SVM)检测行人-基于opencv和python的学习笔记(十八)_第5张图片

HOG特别适合纹理丰富的数据,为了提升性能,局部直方图可以按照对比度进行归一化,这个操作会让光照和阴影下的变化保持更好的不变性

在OpenCv中HOG描述符可以通过cv2.HOGDescriptor来设置,

win_size = (48, 96) ,检测窗口大小(检测对象的最小尺寸48 * 96)对HOG影响最大
block_size = (16, 16), 块的大小(每个块最大为16 * 16)
block_stride = (8, 8) , 单元格尺寸
cell_size = (8, 8), 单元格步长(从一个单元格移动8*8像素到另外一个单元格)
num_bins = 9 , 对于每一个单元格,统计9个方向的梯度直方图。

正样本:行人(64*128)

负样本:图像看起来像正样本,但不包括行人

import cv2
import matplotlib.pyplot as plt
import numpy as np
import random
import os
from sklearn import model_selection as ms
from sklearn import metrics

random.seed(42)

# 设置正负数据集所在的位置
datadir = "data-new/chapter6"
dataset = "pedestrians128x64"
datafile = "%s/%s.tar.gz" % (datadir, dataset)
extractdir = "%s/%s" % (datadir, dataset)
negset = "pedestrians_neg"
negfile = "%s/%s.tar.gz" % (datadir, negset)
negdir = "%s/%s" % (datadir, negset)

# 解压数据集
def extract_tar(datafile, extractdir):                 # 定义解压缩文件函数   datafile待解压的文件,
    try:
        import tarfile
    except ImportError:
        raise ImportError("You do not have tarfile installed. "
                          "Try unzipping the file outside of Python.")

    tar = tarfile.open(datafile)
    tar.extractall(path=extractdir)
    tar.close()
    print("%s successfully extracted to %s" % (datafile, extractdir))
extract_tar(datafile, datadir)               # 解压正样本数据集至指定位置
extract_tar(negfile, datadir)                # 解压负样本数据集至指定位置
for i in range(5):
    filename = "%s/per0010%d.ppm" % (extractdir, i)     # 读取文件中第100到104的5张图片
    img = cv2.imread(filename)
    plt.subplot(1,5,i+1)
    plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    plt.axis('off')
plt.show()
# 设置HOG描述符参数
win_size = (48, 96)            # 检测窗口大小(检测对象的最小尺寸48 * 96)
block_size = (16, 16)          #(每个块最大为16 * 16)
block_stride = (8, 8)          # 单元格尺寸
cell_size = (8, 8)             #(从一个单元格移动8*8像素到另外一个单元格)
num_bins = 9                   # 对于每一个单元格,统计9个方向的梯度直方图。
hog = cv2.HOGDescriptor(win_size, block_size, block_stride, cell_size, num_bins)     # 设置HOG描述符参数
# 构建正样本
x_pos = []
for i in random.sample(range(900), 400):                   # 在0到900张图片中随机挑选400张图片
    filename = "%s/per%05d.ppm" % (extractdir, i)
    img = cv2.imread(filename)
    if img is None:
        print('Could not find image %s' % filename)
        continue
    x_pos.append(hog.compute(img, (64, 64)))                   # 利用HOG进行计算
x_pos = np.array(x_pos, dtype=np.float32)                      # 数据类型转换,兼容OpenCv
y_pos = np.ones(x_pos.shape[0], dtype=np.int32)                # 将训练样本赋值给y_pos
print (x_pos.shape, y_pos.shape)                               # (399, 1980, 1) (399,)     399个训练样本,1980特征值
# 构建负样本  图像尺寸与正样本一样
hroi = 128
wroi = 64
x_neg = []
for negfile in os.listdir(negdir):
    filename = '%s/%s' % (negdir, negfile)
    img = cv2.imread(filename)
    img = cv2.resize(img, (512, 512))
    for j in range(5):
        rand_y = random.randint(0, img.shape[0] - hroi)    # uniform:随机浮点数 randint:随机整数 randrange:随机偶数
        rand_x = random.randint(0, img.shape[1] - wroi)
        roi = img[rand_y:rand_y + hroi, rand_x:rand_x + wroi, :]    # 随机选择左上角的坐标来切出一个64*128像素的感兴趣区域
        x_neg.append(hog.compute(roi, (64, 64)))                    # 利用HOG进行计算
x_neg = np.array(x_neg, dtype=np.float32)                           # 数据类型转换,兼容OpenCv
y_neg = -np.ones(x_neg.shape[0], dtype=np.int32)                    # 将训练样本赋值给y_neg
print(x_neg.shape, y_neg.shape)                                     # (250, 1980, 1) (250,)   250个训练样本,1980特征值
# 将X 和Y (正样本和负样本)合并
x = np.concatenate((x_pos, x_neg))
y = np.concatenate((y_pos, y_neg))
x_train, x_test, y_train, y_test = ms.train_test_split( x, y, test_size=0.2, random_state=42)
# 实现支持向量机
def train_svm(X_train, y_train):                                  # 把训练过程封装为一个函数
    svm = cv2.ml.SVM_create()
    svm.train(X_train, cv2.ml.ROW_SAMPLE, y_train)
    return svm
def score_svm(svm, X, y):                                         # 把评分过程封装为一个函数
    _, y_pred = svm.predict(X)
    return metrics.accuracy_score(y, y_pred)
svm = train_svm(x_train, y_train)                                 # 训练
print('训练集分数:%.5f  ,测试集分数:%.5f' %  (score_svm(svm, x_train, y_train),score_svm(svm, x_test, y_test)))            # 在训练集上的评分

由于使用了HOG特征描述子,虽然在训练数据集上没有错误出现,然而,其泛化能力非常差(64%)。因为比训练表现差太多(100%)。表明模型在数据集上过拟合。为了提升性能,采用模型自举

3.1模型自举

自举:想法很简单,在训练集上训练完SVM后,他们对模型进行评分,发现模型得到了一些假正的结果。记住假正意味着模型预测为一个正样本(+),但实际上是一个负样本(-).在我们的场景中,这就表明SVM错误地认为图像中包含一个行人。如果是在数据集中一张特定的图像上发生这种情况,这个样本显然是有问题的。因此,我们应该把它添加到训练数据集中,并使用这些额外有问题的图像重新训练SVM,这样算法就可以学习到如何把这个图像正确分类。这个过程可以一直重复直到SVM得到一个满意的表现。

# 模型自举
score_train = []
score_test = []
for i in range(3):
    svm = train_svm(x_train, y_train)                    # 训练评估模型
    score_train.append(score_svm(svm, x_train, y_train))
    score_test.append(score_svm(svm, x_test, y_test))

    _, y_pred = svm.predict(x_test)                      # 从测试数据集中找到假正的图片,如果没有,就完成了训练
    false_pos = np.logical_and((y_test.ravel() == -1), (y_pred.ravel() == 1))  # 都为真,即存在假正  false_pos为布尔值
    if not np.any(false_pos):
        print('no more false positives : done')
        break
    x_train = np.concatenate((x_train, x_test[false_pos, :]), axis=0)         # 把假正图片添加到训练数据集中,重复过程
    y_train = np.concatenate((y_train, y_test[false_pos]), axis=0)
print('训练集分数:%s  ,测试集分数:%s' %  (score_train,score_test))

3.2 在更大的图像中检测行人

剩下要做的就是将SVM分类过程与检测过程连接起来。这样做的方法是对图像中的每一个可能的块重复进行我们的分类。这与我们之前可视化决策边界时所做的类似;我们创建了一个精细的网格,并对网格上的每个点进行分类。同样的想法也适用于这里。我们将图像分割成小块,并将每个小块检测是否包含行人。
因此,如果我们想这样做,我们必须在图像中遍历所有可能的块,每次将我们感兴趣的区域移动一个小的stride像素点。

行人可能出现在不同的图像中的区域,且大小不一样需要对图像进行缩放detectMultiScale

# 行人可能出现在不同的图像中的区域,且大小不一样需要对图像进行缩放
hog = cv2.HOGDescriptor(win_size, block_size, block_stride, cell_size, num_bins)
rho, _, _ = svm.getDecisionFunction(0)                     # 返回决策函数的参数,从核响应的加权和减去的一个标量
sv = svm.getSupportVectors()                               # 以浮点矩阵的形式返回所有的支持向量
hog.setSVMDetector(np.append(sv[0, :].ravel(), rho))       # 为线性分类器设置系数

"""
# 使用默认的SVM分类
hogdef = cv2.HOGDescriptor()                               # 设置HOG默认参数
hogdef.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())      # 用于人员检测的分类器(64x128窗口)的简短返回系数
"""
found, _ = hog.detectMultiScale(img_test)               # 调用检测函数 ,返回检测到行人的框的列表

fig = plt.figure(figsize=(10, 6))                          # 画出测试图像
ax = fig.add_subplot(111)
ax.imshow(cv2.cvtColor(img_test, cv2.COLOR_BGR2RGB))
for f in found:                                            # 标记图像中检测到的行人
    ax.add_patch(patches.Rectangle((f[0], f[1]), f[2], f[3], color='y', linewidth=3, fill=False))
plt.savefig('detected.png')
plt.show()

使用支持向量机(SVM)检测行人-基于opencv和python的学习笔记(十八)_第6张图片

完整代码:

import cv2
import matplotlib.pyplot as plt
import numpy as np
import random
import os
from sklearn import model_selection as ms
from sklearn import metrics
from matplotlib import patches

random.seed(42)

# 设置正负数据集所在的位置
datadir = "data-new/chapter6"
dataset = "pedestrians128x64"
datafile = "%s/%s.tar.gz" % (datadir, dataset)
extractdir = "%s/%s" % (datadir, dataset)
negset = "pedestrians_neg"
negfile = "%s/%s.tar.gz" % (datadir, negset)
negdir = "%s/%s" % (datadir, negset)

# 解压数据集
def extract_tar(datafile, extractdir):                 # 定义解压缩文件函数   datafile待解压的文件,
    try:
        import tarfile
    except ImportError:
        raise ImportError("You do not have tarfile installed. "
                          "Try unzipping the file outside of Python.")

    tar = tarfile.open(datafile)
    tar.extractall(path=extractdir)
    tar.close()
    print("%s successfully extracted to %s" % (datafile, extractdir))
extract_tar(datafile, datadir)               # 解压正样本数据集至指定位置
extract_tar(negfile, datadir)                # 解压负样本数据集至指定位置
for i in range(5):
    filename = "%s/per0010%d.ppm" % (extractdir, i)     # 读取文件中第100到104的5张图片
    img = cv2.imread(filename)
    plt.subplot(1,5,i+1)
    plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    plt.axis('off')
plt.show()
# 设置HOG描述符参数
win_size = (48, 96)            # 检测窗口大小(检测对象的最小尺寸48 * 96)
block_size = (16, 16)          #(每个块最大为16 * 16)
block_stride = (8, 8)          # 单元格尺寸
cell_size = (8, 8)             #(从一个单元格移动8*8像素到另外一个单元格)
num_bins = 9                   # 对于每一个单元格,统计9个方向的梯度直方图。
hog = cv2.HOGDescriptor(win_size, block_size, block_stride, cell_size, num_bins)     # 设置HOG描述符参数
# 构建正样本
x_pos = []
for i in random.sample(range(900), 400):                   # 在0到900张图片中随机挑选400张图片
    filename = "%s/per%05d.ppm" % (extractdir, i)
    img = cv2.imread(filename)
    if img is None:
        print('Could not find image %s' % filename)
        continue
    x_pos.append(hog.compute(img, (64, 64)))                   # 利用HOG进行计算
x_pos = np.array(x_pos, dtype=np.float32)                      # 数据类型转换,兼容OpenCv
y_pos = np.ones(x_pos.shape[0], dtype=np.int32)                # 将训练样本赋值为1给y_pos
print (x_pos.shape, y_pos.shape)                               # (399, 1980, 1) (399,)     399个训练样本,1980特征值
# 构建负样本  图像尺寸与正样本一样
hroi = 128
wroi = 64
x_neg = []
for negfile in os.listdir(negdir):
    filename = '%s/%s' % (negdir, negfile)
    img = cv2.imread(filename)
    img = cv2.resize(img, (512, 512))
    for j in range(5):
        rand_y = random.randint(0, img.shape[0] - hroi)    # uniform:随机浮点数 randint:随机整数 randrange:随机偶数
        rand_x = random.randint(0, img.shape[1] - wroi)
        roi = img[rand_y:rand_y + hroi, rand_x:rand_x + wroi, :]    # 随机选择左上角的坐标来切出一个64*128像素的感兴趣区域
        x_neg.append(hog.compute(roi, (64, 64)))                    # 利用HOG进行计算
x_neg = np.array(x_neg, dtype=np.float32)                           # 数据类型转换,兼容OpenCv
y_neg = -np.ones(x_neg.shape[0], dtype=np.int32)                    # 将训练样本赋值给y_neg   np.ones 填充1
print(x_neg.shape, y_neg.shape)                                     # (250, 1980, 1) (250,)   250个训练样本,1980特征值
# 将X 和Y (正样本和负样本)合并
x = np.concatenate((x_pos, x_neg))
y = np.concatenate((y_pos, y_neg))
x_train, x_test, y_train, y_test = ms.train_test_split( x, y, test_size=0.2, random_state=42)
# 实现支持向量机
def train_svm(X_train, y_train):                                  # 把训练过程封装为一个函数
    svm = cv2.ml.SVM_create()
    svm.train(X_train, cv2.ml.ROW_SAMPLE, y_train)
    return svm
def score_svm(svm, X, y):                                         # 把评分过程封装为一个函数
    _, y_pred = svm.predict(X)
    return metrics.accuracy_score(y, y_pred)
svm = train_svm(x_train, y_train)                                 # 训练
print('训练集分数:%.5f  ,测试集分数:%.5f' %  (score_svm(svm, x_train, y_train),score_svm(svm, x_test, y_test)))            # 在训练集上的评分

# 模型自举
score_train = []
score_test = []
for i in range(3):
    svm = train_svm(x_train, y_train)                    # 训练评估模型
    score_train.append(score_svm(svm, x_train, y_train))
    score_test.append(score_svm(svm, x_test, y_test))

    _, y_pred = svm.predict(x_test)                      # 从测试数据集中找到假正的图片,如果没有,就完成了训练
    false_pos = np.logical_and((y_test.ravel() == -1), (y_pred.ravel() == 1))  # 都为真,即存在假正  false_pos为布尔值
    if not np.any(false_pos):
        print('no more false positives : done')
        break
    x_train = np.concatenate((x_train, x_test[false_pos, :]), axis=0)         # 把假正图片添加到训练数据集中,重复过程
    y_train = np.concatenate((y_train, y_test[false_pos]), axis=0)
print('训练集分数:%s  ,测试集分数:%s' %  (score_train,score_test))

# 在更大的图像中检测行人
img_test = cv2.imread('data-new/chapter6/pedestrian_test.jpg')
stride = 16                            # 设置需要移动的像素点
found = []                             # 检测列表
for ystart in np.arange(0, img_test.shape[0], stride):
    for xstart in np.arange(0, img_test.shape[1], stride):      # 每次将感兴趣的区域移动一个小的stride像素点
        if ystart + hroi > img_test.shape[0]:                   # 确保不会超过图像的边界
            continue
        if xstart + wroi > img_test.shape[1]:      #当xstart + wroi > img_test.shape[1]时,结束当前循环进入下一个循环
            continue
        roi = img_test[ystart:ystart + hroi, xstart:xstart + wroi, :]    # 切下来roi区域预处理并且分类
        feat = np.array([hog.compute(roi, (64, 64))])
        _2, ypred = svm.predict(feat)
        if np.allclose(ypred, 1):                                        # 如果检测到是行人则添加至列表
            found.append((ystart, xstart, hroi, wroi))

# 行人可能出现在不同的图像中的区域,且大小不一样需要对图像进行缩放
hog = cv2.HOGDescriptor(win_size, block_size, block_stride, cell_size, num_bins)
rho, _, _ = svm.getDecisionFunction(0)                     # 返回决策函数的参数,从核响应的加权和减去的一个标量
sv = svm.getSupportVectors()                               # 以浮点矩阵的形式返回所有的支持向量
hog.setSVMDetector(np.append(sv[0, :].ravel(), rho))       # 为线性分类器设置系数

"""
# 使用默认的SVM分类
hogdef = cv2.HOGDescriptor()                               # 设置HOG默认参数
hogdef.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())      # 用于人员检测的分类器(64x128窗口)的简短返回系数
"""
found, _ = hog.detectMultiScale(img_test)               # 调用检测函数 ,返回检测到行人的框的列表

fig = plt.figure(figsize=(10, 6))                          # 画出测试图像
ax = fig.add_subplot(111)
ax.imshow(cv2.cvtColor(img_test, cv2.COLOR_BGR2RGB))
for f in found:                                            # 标记图像中检测到的行人
    ax.add_patch(patches.Rectangle((f[0], f[1]), f[2], f[3], color='y', linewidth=3, fill=False))
plt.savefig('detected.png')
plt.show()

你可能感兴趣的:(机器学习)