土炮自自制扑克牌图像资料集 (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()
转换后的图像:
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()
取得要进行训练的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()
验证评估(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()
总结(Conclusion)
在这篇文章中有一些个人学习到的一些有趣的重点:
- 透过土炮自制的图像来进行深度学习,不仅有趣同时学习起來更有效率
- 当图像資料不多的時候, 图像增強 (Data Augmentation)可以帮上忙