土炮自制扑克牌图像数据集

土炮自自制扑克牌图像资料集 (Home-made Poker cards Dataset)

资料集说明
  这个案例主要是希望透过一个简单的示范来带领大家使用一些简单的工具与手法来产生一个可以用来作图像辨别练习的图像资料集。扑克牌图像资料集主要由一张包含了52张扑克牌的大图像來进行切割以及透过简单的影像处理所产生出來。它包含52张扑克牌的图像。 主要的步骤如下:
  (1)下载原始扑克牌的大图像, 例如: http://www.eypo-bet.com/wp-content/uploads/2015/12/poker.cards_.png
  (2)使用Windows內附的"小畫家"打开扑克牌的大图像
  (3)使用Windows內附的"裁剪工作(snipping tool)"将每张牌一张一张的裁剪下来并存成png的格式
  (4)由于扑克牌有四中花样与13个数字,存档时以{花樣}{数字}.png的档名进行儲存, 比如c01.png (黑桃1)
  (5)使用"GIMP"修图软件去除四週边框
  (6)使用"Excel"编写一个资料表來标注每一个图像与标签的关系资讯:
  花樣样(card_type): club, diamond, heart与spear牌面数字(card_num): 1 ~ 13
  标签(card_label): club1club13,diamond1diamond13, heart1~heart13 与 spear1~spear13

扑克牌图像辨识 (Poker cards Recognition)
# 输入相关所需的模组
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os
import glob
from pathlib import PurePath
import cv2

import keras
from keras.preprocessing.image import ImageDataGenerator
from keras.callbacks import LearningRateScheduler, ModelCheckpoint
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.optimizers import SGD, Adam
from keras.utils import to_categorical

# 案例库的根目录路径
ROOT_DIR = os.getcwd()

# 置放资料的路径
DATA_PATH = os.path.join(ROOT_DIR, "data")

# 置放原始图像档案的路径
ORIGIN_IMG_PATH = os.path.join(DATA_PATH, "origin_imgs")

# 置放要用来训练用图像档案的路径
TRAIN_IMG_PATH = os.path.join(DATA_PATH, "train")

# 训练用的图像大小与色階
IMG_HEIGHT = 28
IMG_WIDTH = 28
IMG_CHANNEL = 1
资料预处理 (Data Preprocessing)

  预处理图片的第一步将数据资料转换型別为float32来节省一些记忆的用量并对它们进行归一化(除以255)。然后使用one-hot编码來将标签(label)转换为向量(vector):

进行图像色階转换及大小的修改

  为了能够节省要训练的参数与运算量, 我们要将图像由彩色转为灰階图像, 并且将图像的大小转换成28x28。

# 方法一:取得原始圖像的檔案路徑
#glob.glob()用来进行类似正则表达式的操作,可以用通配符来搜索文件,便于文件的遍历,获取该文件下此类型的所有文件
all_img_paths = glob.glob(os.path.join(ORIGIN_IMG_PATH, "*.PNG"))

# 进行图像色階转换及大小的修改
for img_path in all_img_paths:
    filename = (PurePath(img_path).stem) # 取得原图像档案名称, 例如.c01
    new_filename = filename + ".png" # 更换附档名    
    card_img_grey = cv2.imread(img_path, 0) # 使用OpenCV以灰階读入
    card_img = cv2.resize(card_img_grey, (IMG_HEIGHT, IMG_WIDTH)) # 转换大小  
    cv2.imwrite(os.path.join(TRAIN_IMG_PATH, new_filename), card_img) # 写出

# 方法二:取得原始图像的档案路径
#直接利用os.listdir()获取ORIGIN_IMG_PATH目录下各图片的文件名
img_paths = os.listdir(ORIGIN_IMG_PATH)
# 进行图像色階转换及大小的修改
for img_path in img_paths:
    img_path1 = os.path.join(ORIGIN_IMG_PATH,img_path)#构建路径名
    card_img_grey = cv2.imread(img_path1, 0) # 使用OpenCV以灰階读入
    card_img = cv2.resize(card_img_grey, (IMG_HEIGHT, IMG_WIDTH)) # 转换大小  
    cv2.imwrite(os.path.join('train1', img_path), card_img) # 写出
转换前与转换后的图像資料

转换前的图像:

plt.figure(figsize=(8,8)) # 设定每个图像顯示的大小

# 产生一个3x2网格的组合图像
for i in range(0, 6):
    img_file = '0'+str(i+1)+'.PNG'
    img = cv2.imread(os.path.join(ORIGIN_IMG_PATH, img_file))#cv2.imread()接口读图像,读进来直接是BGR 格式数据格式在 0~255
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)#cv2.cvtColor(p1,p2) 是颜色空间转换函数,p1是需要转换的图片,p2是转换成何种格式
    #cv2.COLOR_BGR2RGB 将BGR格式转换成RGB格式,cv2.COLOR_BGR2GRAY 将BGR格式转换成灰度图片
    plt.subplot(330+1+i) # (331) -> 第一个子图像, (332) -> 第二个子图像
    plt.title(img_file)  # 秀出原图像的档名
    plt.axis('off')      #不顯示坐标
    plt.imshow(img)      # 图像顯示
    
# 展現出图像
plt.show()

image.png

转换后的图像:

plt.figure(figsize=(8,8)) # 設定每个图像顯示的大小

# 产生一個3x2网格的組合图像
for i in range(0, 6):
    img_file = '0'+str(i+1)+'.png'
    img = cv2.imread(os.path.join(TRAIN_IMG_PATH, img_file),0)
    
    plt.subplot(330+1+i) # (331) -> 第一個子图像, (332) -> 第二個子图像
    plt.title(img_file)  # 秀出原图像的档名
    plt.axis('off')      #不顯示坐标
    plt.imshow(img, cmap=plt.get_cmap('gray'))      #图像顯示
    
# 展現出图像
plt.show()

image.png

取得要进行训练的X与标签y

num_classes = 52 # 扑克牌的标签总共有52类
img_rows, img_cols, img_channels = IMG_HEIGHT, IMG_WIDTH, IMG_CHANNEL # 图像是 49像素 x 33像素 (灰色階: 1)
input_shape = (img_rows, img_cols, img_channels) # (图像的height, 图像的width, 图像的顏色通道数channel)

# 载入标签资料档
cards_data = pd.read_excel(os.path.join(DATA_PATH, 'cards_data.xlsx'))

# 取得"card_label"的欄位資料
cards_label = cards_data['card_label']
 
#产生相关的查找的字典物件
idx_to_label = {k:v for k, v in cards_label.iteritems()}
label_to_idx = {v:k for k, v in cards_label.iteritems()}

# 取得所有图像的标签值
y = np.array(cards_label.index.values)

# 进行标签的one-hot編碼
y_train = to_categorical(y, num_classes)
y_test = y_train.copy()

# 将每个图像从档案中读取进來
imgs = []
all_img_paths = glob.glob(os.path.join(TRAIN_IMG_PATH, "*.png"))

#进行图像每個像素值的型別转换与归一化處处理
for img_path in all_img_paths:
    img = cv2.imread(img_path,0) # 以灰階讀入
    img = img.astype('float32')/255.
    imgs.append(img)
    
# 取得要進行训练用的灰階图像
X = np.array(imgs)    

# 将图像数据集的维度進行改变
# 改变前: [样本数, 图像宽, 图像高] -> 改变後: [样本数, 图像宽, 图像高, 图像頻道数]
X_train = X.reshape(X.shape[0], 28, 28, 1)
X_test = X_train.copy()

print("X_train:", X_train.shape)
print("y_train:", y_train.shape)
网络模型

  現在我們来定义我們的模型架构。我們将使用具有6個卷积层的前馈网络,然后是完全连接的隐藏层。 我們也将在两者之间使用Dropout层来防止网络"过拟合overfitting)"。

# 产生一个Keras序贯模型
def cnn_model():
    model = Sequential()
    
    model.add(Conv2D(32, (3, 3), padding='same', activation='relu', input_shape=input_shape))    
    model.add(Conv2D(32, (3, 3), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2
                                     )))
    model.add(Dropout(0.2))
    
    model.add(Conv2D(64, (3, 3), padding='same', activation='relu'))
    model.add(Conv2D(64, (3, 3), padding='same', activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.2))
    
    model.add(Conv2D(128, (3, 3), padding='same', activation='relu'))
    model.add(Conv2D(128, (3, 3), padding='same', activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.2))
    
    model.add(Flatten())
    model.add(Dense(512, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(num_classes, activation='softmax'))
    
    return model;   

model = cnn_model() # 初始化一个模型
model.summary() # 秀出模型架构

在训练模型之前,我们需要将模型配置为学习算法并进行编译。我们需要指定:
loss: 损失函数,我们要優化。我們不能使用MSE,因为它是不连续的数值。因此,我们使用:categorical_crossentropy
optimizer: 我们使用标准随机梯度下降(Stochastic gradient descent)与涅斯捷羅夫動量(Nesterov momentum)
metric: 由于我們正在处理一个分类问题,我们用度量是accuracy。

# 让我们先配置一个常用的組合來作为后续优化的基准点
lr = 0.01
sgd = SGD(lr=lr, decay=1e-6, momentum=0.9, nesterov=True)

model.compile(loss='categorical_crossentropy',
             optimizer=sgd,
             metrics=['accuracy'])
图像增強 (Data Augmentation)

  因为我们只有52张图像, 因此利用現有的图像来生成新的训练图像,这将是一个很好的方式来增加训练数据集的大小。让我们直接使用keras的內置功能來完成图像增強 (Data Augmentation)。

datagen_train = ImageDataGenerator(rotation_range=3.)
datagen_train.fit(X_train)
训练 (Training)

现在,我们的模型已经准备好了。在训练期间,我们的模型将进行迭代批量训练,每个次的训练资料的大小为batch_size。对于每批次,模型将会计算出梯度(gradient),并自动更新网络的权重。对所有训练集的一次迭代被称为一次的循环(epoch)。训练通常会一直进行到损失收敛于一個常數。

batch_size = 100
steps_per_epoch = 50000
training_epochs = 700

# 透過data generator來產生訓練資料, 由於資料是可持續產生, 我們可以透過設定'steps_per_epoch'的數量來讓模型可以有更多的訓練批次
history = model.fit_generator(datagen_train.flow(X_train, y_train, batch_size=batch_size),
                           steps_per_epoch= steps_per_epoch,
                           epochs=training_epochs)
训练过程可视化
# 透過趨勢圖來觀察訓練與驗證的走向 (特別去觀察是否有"過擬合(overfitting)"的現象)
import matplotlib.pyplot as plt

def plot_train_history(history, train_metrics):
    plt.plot(history.history.get(train_metrics))
    plt.ylabel(train_metrics)
    plt.xlabel('Epochs')
    plt.legend(['train'])
    
    
plt.figure(figsize=(12,4))
plt.subplot(1,2,1)
plot_train_history(history, 'loss')

plt.subplot(1,2,2)
plot_train_history(history, 'acc')

plt.show()
image.png
验证评估(Evaluation)
score = model.evaluate(X_train, y_train, verbose=1)

print('Test loss:', score[0])
print('Test accuracy:', score[1])

在这个案例中, 由与图像資料太少所以并沒有特別在验证资料集去進行验证。但是从训练资料集的检查來看,这个卷积网络模型的确可以有效地识別每一张扑克图像。

每一张扑克牌的预测

我们再来看每一张图像的验证結果:

# 打散图像集的顺序
randomize = np.arange(len(X_test))
np.random.shuffle(randomize)
X_test_randomize = X_test[randomize]
y_test_randomize = y_test[randomize]
# 计算打散后的图像集验证
score = model.evaluate(X_test_randomize, y_test_randomize, verbose=1)

print('Test loss:', score[0])
print('Test accuracy:', score[1])

# 进行每一张牌的图像辨识預測
print("\t[Info] Making prediction of X_test_randomize")  
prediction = model.predict_classes(X_test_randomize)  # Making prediction and save result to prediction  

print()  
print("\t[Info] Show 52 prediction result (From 0):")  
print("%s\n" % (prediction[:52]))

# 把每一张牌的图像及预测结果打印出來
for i in range(X_test_randomize.shape[0]):
    true_class = idx_to_label.get(np.argmax(y_test_randomize[i]))
    predict_class = idx_to_label.get(prediction[i])
    plt.title("Predicted:[{}], True:[{}]".format(predict_class, true_class))
    plt.imshow(X_test_randomize[i].reshape(28,28), cmap=plt.get_cmap('gray'))
    plt.tight_layout()  
    plt.show()
image.png
总结(Conclusion)

在这篇文章中有一些个人学习到的一些有趣的重点:

  • 透过土炮自制的图像来进行深度学习,不仅有趣同时学习起來更有效率
  • 当图像資料不多的時候, 图像增強 (Data Augmentation)可以帮上忙
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容