整个解数独问题可以大致分为3个部分:
- 从图片中提取出完整的数独
- 从数独中提取出数字并传入神经网络进行预测【本文的部分】
- 解出数独
【环境】
- Python:3.8.5
- OpenCV:4.5.1
- Keras:2.4.3
【第二部分】数字的识别:
这部分流程比较简单清晰,步骤如下:
- 使用 MNIST 数据集训练模型并保存
- 读取模型,使用本地数据集继续训练
- 切分数独图片,传入模型进行预测
【一】使用 MNIST 数据集训练模型
这部分相当于深度学习的 "Hello world" ,整体比较简单,所以直接贴出代码,每一行都有注释:
整体分为 4 部分:
- 构建训练、测试数据集
- 构建模型
- 把数据喂入模型进行训练
- 保存模型
import numpy
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import Flatten
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.utils import np_utils
from keras import backend as K
# 设置图片数据格式为 "channels_last",即 NHWC
K.set_image_data_format('channels_last')
# 固定随机种子,使具有可重复性
seed = 7
numpy.random.seed(seed)
(X_train, Y_train), (X_test, Y_test) = mnist.load_data()
# 把数据集变为 NHWC 格式
X_train = X_train.reshape(X_train.shape[0], 28, 28, 1).astype('float32')
X_test = X_test.reshape(X_test.shape[0], 28, 28, 1).astype('float32')
# 数据归一化 from 0-255 to 0-1
X_train = X_train / 255
X_test = X_test / 255
# 将标签转化为二值序列
Y_train = np_utils.to_categorical(Y_train)
Y_test = np_utils.to_categorical(Y_test)
# 获取类别数量
num_classes = Y_test.shape[1]
# 创建模型
model = Sequential()
# 2个卷积层
model.add(Conv2D(32, (5, 5), input_shape=(28, 28, 1), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(16, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
# Dropout层
model.add(Dropout(0.2))
# Flatten层
model.add(Flatten())
# 3个Dense层
model.add(Dense(128, activation='relu'))
model.add(Dense(64, activation='relu'))
model.add(Dense(num_classes, activation='softmax'))
# 编译模型,指定损失函数为 categorical_crossentropy,优化器为 adam,模型评估标准为 accuracy
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
# 模型训练,传入训练集,验证集,指定 epochs 和 batch_size
model.fit(X_train, Y_train, validation_data=(X_test, Y_test), epochs=10, batch_size=200)
# 评估模型
scores = model.evaluate(X_test, Y_test, verbose=0)
print("CNN Error: %.2f%%" % (100 - scores[1] * 100))
# - - - - - - - 保存模型 - - - - - - - -
# 把模型保存到 JSON 文件中
model_json = model.to_json()
with open("model.json", "w") as json_file:
json_file.write(model_json)
# 把权重参数保存到 HDF5文件中
model.save_weights("model.h5")
print("Saved model to disk")
【二】读取模型,使用本地数据集继续训练
因为只使用 MNIST 数据集进行训练得到的模型,用来预测提取的数独数字图片准确率不高,所以我又从网上下载了额外的数据集来继续训练,提高准确率。 (也可能是因为我的模型或者参数问题,大家有兴趣的话可以自己调试)。
整体分为 4 部分:
- 处理本地数据,并构建训练、测试数据集
- 读取模型
- 把数据喂入模型继续训练
- 保存模型
from sklearn.model_selection import train_test_split
from imutils import paths
import random
import cv2
import os
import numpy as np
from keras.models import model_from_json
from keras.utils import np_utils
print("loading images")
data = []
labels = []
# 列出路径下的所有文件名并存入 list 列表,以便 for 循环时使用
imagePaths = sorted(list(paths.list_images(".\\digits\\")))
# 固定随机种子,使具有可重复性
random.seed(42)
# 洗牌
random.shuffle(imagePaths)
# 循环遍历所有图片
for imagePath in imagePaths:
img = cv2.imread(imagePath, 0)
# 图片大小预处理
img_resize = cv2.resize(img, (28, 28))
img_resize = img_resize.reshape(28, 28, 1)
# 保存图片数据
data.append(img_resize)
# 从图像路径中提取分类标签并存储到分类标签数组中
label = imagePath.split(os.path.sep)[-2]
labels.append(label)
# 数据归一化
data = np.array(data, dtype="float") / 255.0
# 标签转化为array数组
labels = np.array(labels)
# 构建训练和测试数据集
(X_train, X_test, Y_train, Y_test) = train_test_split(data, labels,
test_size=0.25, random_state=42)
# 将标签转化为二值序列
Y_train = np_utils.to_categorical(Y_train)
Y_test = np_utils.to_categorical(Y_test)
# 获取类别数量
num_classes = Y_test.shape[1]
# 读取预训练模型
json_file = open('model.json', 'r')
loaded_model_json = json_file.read()
json_file.close()
loaded_model = model_from_json(loaded_model_json)
# 读取预训练权重参数
loaded_model.load_weights("model.h5")
print("Loaded saved model from disk.")
# 编译模型,指定损失函数为 categorical_crossentropy,优化器为 adam,模型评估标准为 accuracy
loaded_model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
# 模型训练,传入训练集,验证集,指定 epochs 和 batch_size
loaded_model.fit(X_train, Y_train, validation_data=(X_test, Y_test), epochs=10, batch_size=32)
# - - - - - - - 保存模型 - - - - - - - -
# 把模型保存到 JSON 文件中
model_json = loaded_model.to_json()
with open("model.json", "w") as json_file:
json_file.write(model_json)
# 把权重参数保存到 HDF5文件中
loaded_model.save_weights("model.h5")
print("Saved model to disk")
【三】切分数独图片,传入模型进行预测
这里有一个注意的点是,把数独图片进行 9 * 9 切分后,因为方格中有数字时的像素总值总是比没有数字时的像素总值大很多,所以可以直接计算该方格的像素总值,如果大于某个阈值,则认为有数字,否则认为没有数字。结果如下:
from SudokuExtractor import extract_sudoku
import cv2
import numpy as np
from keras.models import model_from_json
import sys
import os
# 读取训练模型
json_file = open('model.json', 'r')
loaded_model_json = json_file.read()
json_file.close()
loaded_model = model_from_json(loaded_model_json)
# 读取训练权重参数
loaded_model.load_weights("model.h5")
print("Loaded saved model from disk.")
# 识别数字图片
def identify_number(img):
# 图片大小处理
img_show = cv2.resize(img, (28, 28))
img_resize = img_show.reshape(1, 28, 28, 1)
# 把图片输入模型进行预测
loaded_model_pred = np.argmax(loaded_model.predict(img_resize), axis=-1)
# 返回预测值
return loaded_model_pred[0]
# 提取数字图片
def extract_number(sudoku):
# 把图片大小转为450 * 450,然后以50 * 50切分为81张图片进行识别
sudoku = cv2.resize(sudoku, (450, 450))
grid = np.zeros([9, 9])
for i in range(9):
for j in range(9):
img = sudoku[i * 50:(i + 1) * 50, j * 50:(j + 1) * 50]
'''
如果像素总值大于 100000,那么认为图片中有数字;
100000的由来和图片大小450 * 450有关,是经过本地测试得到的;
有数字的图片像素总值总是大于 100000,无数字的图片像素总值总是小于 100000;
'''
if img.sum() > 100000:
grid[i][j] = identify_number(img)
else:
grid[i][j] = 0
return grid.astype(int)
def output(a):
sys.stdout.write(str(a))
# 打印数独
def display_sudoku(sudoku):
output("--------+----------+---------\n")
for i in range(9):
for j in range(9):
cell = sudoku[i][j]
if cell == 0 or isinstance(cell, set):
output('.')
else:
output(cell)
if (j + 1) % 3 == 0 and j < 8:
output(' |')
if j != 8:
output(' ')
output('\n')
if (i + 1) % 3 == 0 and i < 8:
output("--------+----------+---------\n")
output("--------+----------+---------\n")
output("\n")
# 读图
filePath = ".\\imgs\\"
files = os.listdir(filePath)
for file in files:
path = filePath + file
image = extract_sudoku(path)
result = extract_number(image)
display_sudoku(result)
【第二部分总结】
这部分主要是识别 1-9 数字,整体准确率在 99% 以上,一般来说只要第一部分能够较好地提取出数独,那么第二部分就能准确识别出数字。当然很难保证该模型对所有数字图片都能准确识别。
【源码】
【GitHub链接】:GitHub - ITACHI142857/SudokuSolving: sudoku solver using python\opencv\deep learning