前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何教电脑玩Spot it!使用OpenCV和深度学习

如何教电脑玩Spot it!使用OpenCV和深度学习

作者头像
代码医生工作室
发布2020-05-09 10:37:35
8340
发布2020-05-09 10:37:35
举报
文章被收录于专栏:相约机器人相约机器人

来源 | Medium

编辑 | 代码医生团队

爱好是玩棋盘游戏,因为对CNN有所了解,所以决定开发一种可以在纸牌游戏中击败人类的应用程序。想使用我自己的数据集从头开始构建模型,以查看使用小数据集从头开始的模型的性能如何。选择从一个不太难的游戏入手!(又称Dobble)。

如果不知道它!这里有一个简短的游戏说明:是一款简单的模式识别游戏,玩家可以尝试查找两张卡上显示的图像。每张卡都在原厂现货中!具有八个不同的符号,符号的大小从一张卡到另一张卡都不同。任何两张卡共有一个符号。如果是第一个找到该符号的人,那么将赢得该卡。55张纸牌用完时,收集最多纸牌的人将获胜。

自己尝试:上面显示的卡上常见的符号是什么?

从哪儿开始?

任何数据科学问题的第一步都是收集数据。用手机拍了一些照片,每张卡有六个。总共制作了330张照片。其中四个如下所示。可能会想:这足以构建完善的卷积神经网络吗?会回到这一点!

处理图像

好的有数据,下一步是什么?这可能是成功的最重要部分:处理图像。需要提取每张卡上显示的符号。这里有一些困难。可以在上面的图片中看到一些符号可能更难提取:雪人和幽灵(第三张图片)和圆顶冰屋(第四张图片)的颜色浅,并且有污点(第二张图片)和感叹号(第四张图片)多个部分。为了处理浅色符号,向图像添加了对比度。之后调整大小并保存图像。

增加对比

使用Lab颜色空间来增加对比度。L代表亮度,a是从绿色到品红色的颜色分量,b是从蓝色到黄色的颜色分量。可以使用OpenCV轻松提取这些组件:

代码语言:javascript
复制
import cv2
import imutils
imgname = 'picture1'
image = cv2.imread(f’{imgname}.jpg’)
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)

从左到右:原始图像,Light组件,a组件和b组件

现在向Light组件添加对比度,将这些组件合并在一起,然后将图像转换回正常状态:

代码语言:javascript
复制
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
cl = clahe.apply(l)
limg = cv2.merge((cl,a,b))
final = cv2.cvtColor(limg, cv2.COLOR_LAB2BGR)

从左到右:原始图像,光分量,增加的对比度,转换回RGB

调整大小

然后调整大小并保存图像:

代码语言:javascript
复制
resized = cv2.resize(final, (800, 800))
# save the image
cv2.imwrite(f'{imgname}processed.jpg', blurred)

检测卡和符号

现在处理图像,可以从检测图像上的卡开始。使用OpenCV可以找到外部轮廓。然后需要将图像转换为灰度,选择一个阈值(在本例中为190)以创建黑白图像,然后找到轮廓。在代码中:

代码语言:javascript
复制
image = cv2.imread(f’{imgname}processed.jpg’)
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
thresh = cv2.threshold(gray, 190, 255, cv2.THRESH_BINARY)[1]
# find contours
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
output = image.copy()
# draw contours on image
for c in cnts:
cv2.drawContours(output, [c], -1, (255, 0, 0), 3)

处理后的图像,转换为灰度,阈值并具有外部轮廓

如果按区域对外部轮廓进行排序,则可以找到面积最大的轮廓:这就是卡片。可以创建一个白色背景来提取符号。

代码语言:javascript
复制
# sort by area, grab the biggest one
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[0]
# create mask with the biggest contour
mask = np.zeros(gray.shape,np.uint8)
mask = cv2.drawContours(mask, [cnts], -1, 255, cv2.FILLED)
# card in foreground
fg_masked = cv2.bitwise_and(image, image, mask=mask)
# white background (use inverted mask)
mask = cv2.bitwise_not(mask)
bk = np.full(image.shape, 255, dtype=np.uint8)
bk_masked = cv2.bitwise_and(bk, bk, mask=mask)
# combine back- and foreground
final = cv2.bitwise_or(fg_masked, bk_masked)

遮罩,背景,前景,组合

现在是符号检测时间!可以使用最后一张图像再次检测外部轮廓,这些轮廓是符号。如果在每个符号周围创建一个正方形,则可以提取该区域。代码更长一些:

代码语言:javascript
复制
# just like before (with detecting the card)
gray = cv2.cvtColor(final, cv2.COLOR_RGB2GRAY)
thresh = cv2.threshold(gray, 195, 255, cv2.THRESH_BINARY)[1]
thresh = cv2.bitwise_not(thresh)
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[:10]
# handle each contour
i = 0
for c in cnts:
    if cv2.contourArea(c) > 1000:
        # draw mask, keep contour
        mask = np.zeros(gray.shape, np.uint8)
        mask = cv2.drawContours(mask, [c], -1, 255, cv2.FILLED)
        # white background
        fg_masked = cv2.bitwise_and(image, image, mask=mask)
        mask = cv2.bitwise_not(mask)
        bk = np.full(image.shape, 255, dtype=np.uint8)
        bk_masked = cv2.bitwise_and(bk, bk, mask=mask)
        finalcont = cv2.bitwise_or(fg_masked, bk_masked)
        # bounding rectangle around contour
        output = finalcont.copy()
        x,y,w,h = cv2.boundingRect(c)
        # squares io rectangles
        if w < h:
            x += int((w-h)/2)
            w = h
        else:
            y += int((h-w)/2)
            h = w
        # take out the square with the symbol
        roi = finalcont[y:y+h, x:x+w]
        roi = cv2.resize(roi, (400,400))
        # save the symbol
        cv2.imwrite(f"{imgname}_icon{i}.jpg", roi)
        i += 1

阈值图像,找到的轮廓,重影符号和心脏符号(使用遮罩提取的符号)

排序符号

现在是无聊的部分!现在该对符号进行排序了。需要一个训练,测试和验证目录,每个目录包含57个目录(有57个不同的符号)。文件夹结构如下所示:

symbols

├── test

│ ├── anchor

│ ├── apple

│ │ ...

│ └── zebra

├── train

│ ├── anchor

│ ├── apple

│ │ ...

│ └── zebra

└── validation

├── anchor

├── apple

│ ...

└── zebra

将提取的符号(超过2500个)放在正确的目录中需要花费一些时间!有用于在GitHub上创建子文件夹,测试和验证集的代码。也许下次最好使用聚类算法进行排序…

https://github.com/henniedeharder/spotit/tree/master/DeepLearningSpotIt

训练卷积神经网络

在无聊的部分之后又是酷的部分。构建和训练一个CNN。可以在这篇文章中找到有关CNN的信息。

模型架构

这是一个多类,单标签的分类问题。希望每个符号都有一个标签。这就是为什么必须选择具有57个节点和分类交叉熵损失函数的最后一层激活softmax的原因。

最终模型的架构如下所示:

代码语言:javascript
复制
# imports
from keras import layers
from keras import models
from keras import optimizers
from keras.preprocessing.image import ImageDataGenerator
import matplotlib.pyplot as plt
# layers, activation layer with 57 nodes (one for every symbol)
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(400, 400, 3)))
model.add(layers.MaxPooling2D((2, 2)))  
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(256, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(256, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(57, activation='softmax'))
model.compile(loss='categorical_crossentropy',       optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])

资料扩充

为了获得更好的性能,使用了数据扩充。数据扩充是增加输入数据的数量和多样性的过程。这可以通过旋转,移动,缩放,裁剪和翻转现有图像来实现。使用Keras进行数据扩充很容易:

代码语言:javascript
复制
# specify the directories
train_dir = 'symbols/train'
validation_dir = 'symbols/validation'
test_dir = 'symbols/test'
# data augmentation with ImageDataGenerator from Keras (only train)
train_datagen = ImageDataGenerator(rescale=1./255, rotation_range=40, width_shift_range=0.1, height_shift_range=0.1, shear_range=0.1, zoom_range=0.1, horizontal_flip=True, vertical_flip=True)
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(train_dir, target_size=(400,400), batch_size=20, class_mode='categorical')
validation_generator = test_datagen.flow_from_directory(validation_dir, target_size=(400,400), batch_size=20, class_mode='categorical')

如果想知道,一个增强的幽灵看起来像这样:

左侧为原始鬼影,其他图像为增强鬼影

拟合模型

拟合模型,将其保存以用于预测并检查结果。

代码语言:javascript
复制
history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=100, validation_data=validation_generator, validation_steps=50)
# don't forget to save your model!
model.save('models/model.h5')

完美的预测!

结果

训练的基准模型没有数据扩充,丢失和层次减少的问题。该模型得出以下结果:

基线模型的结果

可以清楚地看到此模型过度拟合。最终模型的结果(来自前面各段中的代码)要好得多。在下图中,可以看到训练和验证集的准确性和损失。

最终模型的结果

使用测试集,该模型仅犯了一个错误:它预测炸弹会掉落。决定坚持使用该模型,测试集的准确性为0.995。

预测两张牌的共同符号

现在可以预测两张卡上的通用符号。可以使用两个图像,分别对每个图像进行预测,并使用交集查看两个卡都有什么符号。这提供了三种可能性:

  • 在预测期间出了点问题:找不到常见的符号。
  • 相交处只有一个符号(可以是错误的或正确的)。
  • 相交处有多个符号。在这种情况下,选择了概率最高的符号(两个预测的均值)。

该代码位于GitHub上,用于预测目录main.py文件中两个图像的所有组合。

https://github.com/henniedeharder/spotit/tree/master/DeepLearningSpotIt

一些结果:

结论

这是一个完美的表现模型吗?抱歉不行!当为卡片拍摄新照片并让模型预测通用符号时,雪人遇到了一些问题。有时它预示着眼睛或斑马像个雪人!这给出了一些奇怪的结果:

雪人?哪里?

这个模型比人类好吗?这取决于:人类可以做到完美,但是模型更快!给计算机计算时:给它提供了55张卡,并询问两个卡的每种组合的通用符号。总共有1485个组合。这花费了计算机不到140秒的时间。电脑犯了一些错误,但绝对可以打败任何人!

认为建立100%绩效的模型并不难。例如,可以通过使用迁移学习来完成。为了了解模型在做什么,可以可视化测试图像的层。下次可以尝试的事情!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 相约机器人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档