来源 | Medium
编辑 | 代码医生团队
爱好是玩棋盘游戏,因为对CNN有所了解,所以决定开发一种可以在纸牌游戏中击败人类的应用程序。想使用我自己的数据集从头开始构建模型,以查看使用小数据集从头开始的模型的性能如何。选择从一个不太难的游戏入手!(又称Dobble)。
如果不知道它!这里有一个简短的游戏说明:是一款简单的模式识别游戏,玩家可以尝试查找两张卡上显示的图像。每张卡都在原厂现货中!具有八个不同的符号,符号的大小从一张卡到另一张卡都不同。任何两张卡共有一个符号。如果是第一个找到该符号的人,那么将赢得该卡。55张纸牌用完时,收集最多纸牌的人将获胜。
自己尝试:上面显示的卡上常见的符号是什么?
从哪儿开始?
任何数据科学问题的第一步都是收集数据。用手机拍了一些照片,每张卡有六个。总共制作了330张照片。其中四个如下所示。可能会想:这足以构建完善的卷积神经网络吗?会回到这一点!
处理图像
好的有数据,下一步是什么?这可能是成功的最重要部分:处理图像。需要提取每张卡上显示的符号。这里有一些困难。可以在上面的图片中看到一些符号可能更难提取:雪人和幽灵(第三张图片)和圆顶冰屋(第四张图片)的颜色浅,并且有污点(第二张图片)和感叹号(第四张图片)多个部分。为了处理浅色符号,向图像添加了对比度。之后调整大小并保存图像。
增加对比
使用Lab颜色空间来增加对比度。L代表亮度,a是从绿色到品红色的颜色分量,b是从蓝色到黄色的颜色分量。可以使用OpenCV轻松提取这些组件:
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组件添加对比度,将这些组件合并在一起,然后将图像转换回正常状态:
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
调整大小
然后调整大小并保存图像:
resized = cv2.resize(final, (800, 800))
# save the image
cv2.imwrite(f'{imgname}processed.jpg', blurred)
检测卡和符号
现在处理图像,可以从检测图像上的卡开始。使用OpenCV可以找到外部轮廓。然后需要将图像转换为灰度,选择一个阈值(在本例中为190)以创建黑白图像,然后找到轮廓。在代码中:
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)
处理后的图像,转换为灰度,阈值并具有外部轮廓
如果按区域对外部轮廓进行排序,则可以找到面积最大的轮廓:这就是卡片。可以创建一个白色背景来提取符号。
# 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)
遮罩,背景,前景,组合
现在是符号检测时间!可以使用最后一张图像再次检测外部轮廓,这些轮廓是符号。如果在每个符号周围创建一个正方形,则可以提取该区域。代码更长一些:
# 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的原因。
最终模型的架构如下所示:
# 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进行数据扩充很容易:
# 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')
如果想知道,一个增强的幽灵看起来像这样:
左侧为原始鬼影,其他图像为增强鬼影
拟合模型
拟合模型,将其保存以用于预测并检查结果。
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%绩效的模型并不难。例如,可以通过使用迁移学习来完成。为了了解模型在做什么,可以可视化测试图像的层。下次可以尝试的事情!