📜  用计算机视觉和约束满足算法解决数独问题

📅  最后修改于: 2021-05-04 20:54:44             🧑  作者: Mango

本文介绍了Python 2.7中的一个程序,用于解决genina.com的Android应用“ Sudoku”的Sudoku 9×9。为了解决genina.com的Android应用程序“ Sudoku”的数独问题,请对游戏进行截图(获得720×1280的图像),然后使用KNN算法获得在81个方格中找到的数字确定每个元素后,使用带有回溯的约束满足算法来解决数独问题。

rsz_sudoku1

左边是我们的输入:我们将要分析的屏幕截图。右边是解决方案。

这个如何运作?
步骤1:图像预处理
第一步,图像预处理:分别提取每个数独方块,并将其依次保存为照片#.png(其中#从0到80)。获得80×75像素的图像。

代码:

rsz_sudoku2

输入: photo0.png。这是我们要分析的照片。

代码:

#/Preprocessing.py / import cv2
import numpy as np
import Functions
  
# Relative path
path ="./Screenshots/"
  
# Image to analize
number = input("Enter image number: ")
globalPath = path+"photo"+str(number)+".png"
image = cv2.imread(globalPath)
  
# Save the name of the image to analize after in Main.py
file = open("image.txt", "w")
file.write(globalPath)
file.close()
  
# MAIN
if __name__ == '__main__':    
      
    # PREPROCESSING -> Crop the edges, ads and all 
    # the images outside the sudoku board
    image = Functions.cropImage(image, 218)
    image = Functions.rotateImage(image, 180)
    image = Functions.cropImage(image, 348)
    image = Functions.rotateImage(image, 180)
      
    # Crop each box in the sudoku board
    cont = 0
    w = 0
    for j in range(9):
        h = 0
  
        for i in range(9):
  
            nombre = "image"+ str(cont) + ".png"
            image1 = Functions.cropBox(image, w, h, 75, 80)
  
            # Save the image
            Functions.saveImage(image1, nombre)
            h = h + 80
            cont = cont + 1
  
        # Position of the pixel where start the image
        w = 80*(j + 1)

代码:创建一个仅具有用于预处理和图像转换的函数的库,称为“函数”。

#/Functions.py / import cv2
import numpy as np
  
# Function to rotate the image
def rotateImage(image, angle):
     image_center = tuple(np.array(image.shape[1::-1]) / 2)
     rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0)
     result = cv2.warpAffine(image, rot_mat, image.shape[1::-1], flags = cv2.INTER_LINEAR)
     return result
   
# Function to crop top border in the image
def cropImage(image, x):
  
    # x determine how far to cut the image
    # fileb determines with what name we are going to save the image
    # Determine image dimensions
    height, width, channels = image.shape
    crop_img = image[x:height, 0:width]
    return crop_img
  
# Function to crop every box (there are 81 boxes in total) 
def cropBox(image, x, y, h, w):
    # Each side of the square / box has a side of length 10
    crop_img = image[x:(x + h), y:(y + w)]
    return crop_img
  
# Function to save the image 
def saveImage(image, fileb):
    new_path = "./Images/"
    cv2.imwrite(new_path + fileb, image)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
  
# Function to crop all borders of each box
def cropBorder(image):
  
    # Determine image dimensions
    height, width, channels = image.shape
    crop_img = image[12:height-12, 12:width-12]
    return crop_img

步骤2:影像转换
切出每个框的边框,以防在我们的分析中可以推断出任何黑色边框。每个图像都有56×51像素。

代码:

#/Transformation.py / import cv2
import numpy as np
import Functions
  
# Relative path 
path ="./Images/"
  
if __name__ == '__main__':
      
    for x in range(81):
  
        # Image to analize
        nameImage = "image" + str(x) + ".png"
        image = cv2.imread(path + nameImage)
        image = Functions.cropBorder(image)
        Functions.saveImage(image, nameImage)

步骤3:KNN分类

分析框中的数字。在这种情况下,可以使用Canny算法确定是否有数字或它是一个空盒子。然后通过KNN算法确定框中有哪个数字。为了提取特征,使用了Hu:1和2的矩,进行了高斯滤波器滤波和无监督阈值处理。

代码:

#/Main.py / import numpy as np
from matplotlib import pyplot as plt
import matplotlib.patches as mpatches
plt.rcParams['image.cmap'] = 'gray'
from mpl_toolkits.mplot3d import Axes3D
from skimage import io, color, img_as_float, filters
from skimage.feature import hog
import cv2
import mahotas
      
# Function to extract characteristics of the images
# to later use them in the Knn algorithm
def extraction(image):
      
    # PREPROCESSING -> Convert image to grayscale
    aux = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 
  
    # FILTERING -> Apply Gauss Filter
    aux = cv2.GaussianBlur(aux, (3, 3), 0)   
  
    # SEGMENTATION -> Apply Thresholding simple
    ret, th = cv2.threshold(aux, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    aux = th
  
    # FEATURE EXTRACTION -> Obtain Hu Moments
    hu = cv2.HuMoments(cv2.moments(aux)).flatten()
  
    # Analysis the features (Hu Moments)
    return aux, [hu[0], hu[1]]
  
# Training Data Base (YTrain)
# Load all images of each numbers that appears in sudoku board 
number1 = io.ImageCollection('./Images / Train / Y1/*.png:./Images / Train / Y1/*.jpg')
number2 = io.ImageCollection('./Images / Train / Y2/*.png:./Images / Train / Y2/*.jpg')
number3 = io.ImageCollection('./Images / Train / Y3/*.png:./Images / Train / Y3/*.jpg')
number4 = io.ImageCollection('./Images / Train / Y4/*.png:./Images / Train / Y4/*.jpg')
number5 = io.ImageCollection('./Images / Train / Y5/*.png:./Images / Train / Y5/*.jpg')
number6 = io.ImageCollection('./Images / Train / Y6/*.png:./Images / Train / Y6/*.jpg')
number7 = io.ImageCollection('./Images / Train / Y7/*.png:./Images / Train / Y7/*.jpg')
number8 = io.ImageCollection('./Images / Train / Y8/*.png:./Images / Train / Y8/*.jpg')
number9 = io.ImageCollection('./Images / Train / Y9/*.png:./Images / Train / Y9/*.jpg')
      
# Create a class for each element
class Element:
    def __init__(self):
        self.number = None
        self.image = None
        self.feature = []
        self.distance = 0
          
# Analize data
data = []
i = 0
  
# Analize number 1
iter = 0
  
for object in number1:
    data.append(Element())
    data[i].number = '1'
    data[i].image, data[i].feature = extraction(object)
    i += 1
    iter += 1
print("number1 is OK")
  
# Analize number 2
iter = 0
  
for object in number2:
    data.append(Element())
    data[i].number = '2'
    data[i].image, data[i].feature = extraction(object)
    i += 1
    iter += 1
print("number2 is OK")
  
# Analize number 3
iter = 0
  
for object in number3:
    data.append(Element())
    data[i].number = '3'
    data[i].image, data[i].feature = extraction(object)
    i += 1
    iter += 1
print("number3 is OK")
  
# Analize number 4
iter = 0
  
for object in number4:
    data.append(Element())
    data[i].number = '4'
    data[i].image, data[i].feature = extraction(object)
    i += 1
    iter += 1
print("number4 is OK")
  
# Analize number 5
iter = 0
  
for object in number5:
    data.append(Element())
    data[i].number = '5'
    data[i].image, data[i].feature = extraction(object)
    i += 1
    iter += 1
print("number5 is OK")
  
# Analize number 6
iter = 0
  
for object in number6:
    data.append(Element())
    data[i].number = '6'
    data[i].image, data[i].feature = extraction(object)
    i += 1
    iter += 1
print("number6 is OK")
  
# Analize number 7
iter = 0
  
for object in number7:
    data.append(Element())
    data[i].number = '7'
    data[i].image, data[i].feature = extraction(object)
    i += 1
    iter += 1
print("number7 is OK")
  
# Analize number 8
iter = 0
  
for object in number8:
    data.append(Element())
    data[i].number = '8'
    data[i].image, data[i].feature = extraction(object)
    i += 1
    iter += 1
print("number8 is OK")
  
# Analize number 9
iter = 0
  
for object in number9:
    data.append(Element())
    data[i].number = '9'
    data[i].image, data[i].feature = extraction(object)
    i += 1
    iter += 1
print("number9 is OK")
  
print("Complete analysis of the Train database")
  
# KNN
print("\nInitialization KNN")
  
# Element to analize
# Remember to apply Transformation.py when you 
# want to evaluate a new image.
test = Element()
  
for aux in range(81):
  
    name = './Images / image'+str(aux)+'.png'
    image = io.imread(name)
      
    # COUNTING OBJECTS WITHIN THE IMAGE WITH CANNY ALGORITHM
    borders = cv2.Canny(image, 10, 140)                                            
  
    # OpenCV4    
    ctns, _ = cv2.findContours(borders, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    contours = len(ctns)
      
    # If it is different from an empty box -> in empty boxes the algorithm 
    # marks zero because it does not find anything
    if (contours != 0): 
        test.image, test.feature = extraction(image)
        test.number = '1' # label initial 
  
        i = 0
        sum = 0
  
        for ft in data[0].feature:
            sum = sum + np.power(np.abs(test.feature[i] - ft), 2)
            i += 1
        d = np.sqrt(sum)
  
        for element in data:
            sum = 0
            i = 0
  
            for ft in (element.feature):
                sum = sum + np.power(np.abs((test.feature[i]) - ft), 2)
                i += 1    
            element.distance = np.sqrt(sum)
            if (sum < d):
                d = sum
                test.number = element.number
    else:
        test.number = '.'
          
    if (aux == 0): vector = str(test.number)
    else: vector = vector + str(test.number)
          
print(vector)
  
# Save in a string all the boxes in the sudoku board 
archivo = open("vector.txt", "w")
archivo.write(vector)
archivo.close()

显示性能KNN算法

Vector.txt包含从屏幕截图中提取的所有元素(框从左到右,从上到下滚动)。在该项目中,对于测试中分析的所有图像,KNN算法的性能提供了97%的准确性。如果数字识别出现错误,可以选择在vector.txt中手动更改框的预测。

识别图像photo0.jpg的数独网格的所有数字的结果

步骤4:现在解决数独!
提出了一种带有回溯的约束满足算法来解决数独问题。
代码:

#/Solver.py / import numpy as np
  
# Dictionary with grid numbers
def solverGrid(grid):
      
    values = valuesGrid(grid)
    return searchValues(values)
  
# Exchange of items
def exchangeValues(A, B):
      
    return [a + b for a in A for b in B]
  
# Define initial values
def initialValues(grid):
    return dict(zip(sections, grid))
  
# Define values in the grid
def valuesGrid(grid):
    numbers = []
  
    for c in grid:
        if c == '.':
            numbers.append('123456789')
        elif c in '123456789':
            numbers.append(c)
  
    return dict(zip(sections, numbers))
  
# Delete the values that are already inside the grid
def eliminateValues(numbers):
      
    solved_values = [box for box in numbers.keys() if len(numbers[box]) == 1]
    for box in solved_values:
        digit = numbers[box]
        for vecino in neighbors[box]:
            numbers[vecino] = numbers[vecino].replace(digit, '')
    return numbers
  
def onlyOption(numbers):
    for unit in unitlist:
        for digit in '123456789':
            dplaces = [box for box in unit if digit in numbers[box]]
            if len(dplaces) == 1:
                numbers[dplaces[0]] = digit
    return numbers
  
def reduceSudoku(numbers):
    stalled = False
    while not stalled:
        # Check how many boxes have a determined value
        solved_values_before = len([box for box in numbers.keys() if len(numbers[box]) == 1])
  
        # Set the Eliminate Strategy
        numbers = eliminateValues(numbers)
  
        # Use the Only Choice Strategy
        numbers = onlyOption(numbers)
  
        # Check how many boxes have a determined value, to compare
        solved_values_after = len([box for box in numbers.keys() if len(numbers[box]) == 1])
  
        # If no new values were added, stop the loop.
        stalled = solved_values_before == solved_values_after
  
        # Sanity check, return False if there is a box with zero available values:
        if len([box for box in numbers.keys() if len(numbers[box]) == 0]):
            return False
  
    return numbers
  
def searchValues(numbers):
  
    numbers = reduceSudoku(numbers)
  
    if numbers is False:
        return False    ## Failure
    if all(len(numbers[s]) == 1 for s in sections): 
        return numbers  ## Ok
      
    # Choose one of the unfilled boxes
    unfilled_squares = [(len(numbers[s]), s) for s in sections if len(numbers[s]) > 1]
    n, s = min(unfilled_squares)
      
    # Solve the next boxes
    for value in numbers[s]:
        nova_sudoku = numbers.copy()
        nova_sudoku[s] = value
        attempt = searchValues(nova_sudoku)
        if attempt:
            return attempt
  
# Define values
rows = 'ABCDEFGHI'
columns = '123456789'
  
sections = exchangeValues(rows, columns)
rowsUnit = [exchangeValues(r, columns) for r in rows]
columnUnits = [exchangeValues(rows, c) for c in columns]
boxUnits = [exchangeValues(rs, cs) for rs in ('ABC', 'DEF', 'GHI') for cs in ('123', '456', '789')]
  
unitlist = rowsUnit + columnUnits + boxUnits
  
units = dict((s, [u for u in unitlist if s in u]) for s in sections)
neighbors = dict((s, set(sum(units[s], []))-set([s])) for s in sections)
  
# MAIN
if __name__ == '__main__':
      
    # With file manager to read the file vector.txt 
    # that has all the values of the screenshot
    file = open("vector.txt", "r")
    lines = file.read()
    file.close() 
  
    # Access the dictionary
    a = solverGrid(lines)
    b = sorted(a.items())
      
    # Save the dictionary solution
    np.save('Solution', b) 

步骤5:界面
与原始屏幕截图相比,改进了解决方案的显示方式。

代码:

#/Interface.py / 
import numpy as np
import matplotlib.pyplot as plt
import cv2
  
# Read dictionary from Solution.npy
readDictionary = np.load('Solution.npy')
values = (readDictionary[:, 1])
  
# Read vector.txt
file = open("vector.txt", "r")
lines = file.read()
file.close()
  
# Read the path of the image the we want to analize
fileTxt = open("image.txt", "r")
pathGlobal = fileTxt.read()
fileTxt.close()
  
# Obtain the coordinates to be able to 
# locate them in the image
row = ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
column = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
  
# Assign the coordinates of each number within the image plane
def coordinate():
    positionx = list()
    positiony = list()
      
    for k in range(9):
        for i in range(9):
          
            if (row[k] == "A"): y = 270 
            elif (row[k] == "B"): y = 350
            elif (row[k] == "C"): y = 430
            elif (row[k] == "D"): y = 510
            elif (row[k] == "E"): y = 590
            elif (row[k] == "F"): y = 670
            elif (row[k] == "G"): y = 750
            elif (row[k] == "H"): y = 830
            elif (row[k] == "I"): y = 915
          
            if (column[i] == "1"): x = 19
            elif (column[i] == "2"): x = 98
            elif (column[i] == "3"): x = 182
            elif (column[i] == "4"): x = 261
            elif (column[i] == "5"): x = 335
            elif (column[i] == "6"): x = 419
            elif (column[i] == "7"): x = 499
            elif (column[i] == "8"): x = 580
            elif (column[i] == "9"): x = 660
  
            positionx.append(x)
            positiony.append(y)
          
    return (positionx, positiony)        
  
# Function to write value in each box in the image
def writeValue(image, valor, x, y):
          
    font = cv2.FONT_HERSHEY_SIMPLEX
    text = str(valor)
      
    # Write text in the image
    cv2.putText(image, text, (x, y), font, 2, (255, 0, 0), 5)
    # cv2.putText(image, text, (coordinates), size font, (color RGB), thickness)
      
    return image
  
# Load image
image = cv2.imread(pathGlobal)
image2 = image.copy()
  
# Load coordinates
positionx, positiony = coordinate()
  
for i in range(81):
    if (lines[i] == "."):
        image = writeValue(image, values[i], positionx[i], positiony[i])
  
# Concatenate images horizontally
image = np.concatenate((image2, image), axis = 1)
  
# Show image concatenation   
plt.imshow(image)
plt.axis("off")
plt.show()
  
# Save image
cv2.imwrite("./Interface / example.png", image)

输出:

rsz_sudoku3

photo1.png的结果

数独

photo2.png的结果

可以在给定的存储库中找到所有用于KNN算法训练的图像和示例的屏幕截图