├── README.md ├── Test.gif ├── digit_model.h5 ├── sudoku.py └── sudoku_solver.py /README.md: -------------------------------------------------------------------------------- 1 | # Sudoku-Solver-AI 2 | 3 | I made a real time Sudoku solver using the camera, it looks for the edges of the Sudoku in the frame, extracts it, solves it and overlays the solution on the puzzle itself. For the digit recognition, I used a CNN, trained using Keras with printed characters from the different ubuntu fonts. For the image processing part, I used OpenCV for the edge detection. 4 | 5 | ![](Test.gif) 6 | 7 | this youtube channel helped me a lot : https://www.youtube.com/channel/UCn09iU3hS5Fpxv0XniGv2FQ 8 | -------------------------------------------------------------------------------- /Test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1nfinityLoop/Sudoku-Solver-AI/ae19a33ec6f736016216f76213ffb47759d4e949/Test.gif -------------------------------------------------------------------------------- /digit_model.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1nfinityLoop/Sudoku-Solver-AI/ae19a33ec6f736016216f76213ffb47759d4e949/digit_model.h5 -------------------------------------------------------------------------------- /sudoku.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | import operator 4 | from keras.models import load_model 5 | from keras.models import model_from_json 6 | import sudoku_solver as sol 7 | 8 | classifier = load_model("./digit_model.h5") 9 | 10 | marge = 4 11 | case = 28 + 2 * marge 12 | taille_grille = 9 * case 13 | 14 | cap = cv2.VideoCapture(0) 15 | fourcc = cv2.VideoWriter_fourcc(*'XVID') 16 | flag = 0 17 | out = cv2.VideoWriter('output.avi', fourcc, 30.0, (1080, 620)) 18 | 19 | 20 | while True: 21 | 22 | ret, frame = cap.read() 23 | 24 | gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) 25 | gray = cv2.GaussianBlur(gray, (7, 7), 0) 26 | thresh = cv2.adaptiveThreshold( 27 | gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 9, 2) 28 | 29 | contours, hierarchy = cv2.findContours( 30 | thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) 31 | contour_grille = None 32 | maxArea = 0 33 | 34 | for c in contours: 35 | area = cv2.contourArea(c) 36 | if area > 25000: 37 | peri = cv2.arcLength(c, True) 38 | polygone = cv2.approxPolyDP(c, 0.01 * peri, True) 39 | if area > maxArea and len(polygone) == 4: 40 | contour_grille = polygone 41 | maxArea = area 42 | 43 | if contour_grille is not None: 44 | cv2.drawContours(frame, [contour_grille], 0, (0, 255, 0), 2) 45 | points = np.vstack(contour_grille).squeeze() 46 | points = sorted(points, key=operator.itemgetter(1)) 47 | if points[0][0] < points[1][0]: 48 | if points[3][0] < points[2][0]: 49 | pts1 = np.float32([points[0], points[1], points[3], points[2]]) 50 | else: 51 | pts1 = np.float32([points[0], points[1], points[2], points[3]]) 52 | else: 53 | if points[3][0] < points[2][0]: 54 | pts1 = np.float32([points[1], points[0], points[3], points[2]]) 55 | else: 56 | pts1 = np.float32([points[1], points[0], points[2], points[3]]) 57 | pts2 = np.float32([[0, 0], [taille_grille, 0], [0, taille_grille], [ 58 | taille_grille, taille_grille]]) 59 | M = cv2.getPerspectiveTransform(pts1, pts2) 60 | grille = cv2.warpPerspective(frame, M, (taille_grille, taille_grille)) 61 | grille = cv2.cvtColor(grille, cv2.COLOR_BGR2GRAY) 62 | grille = cv2.adaptiveThreshold( 63 | grille, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 7, 3) 64 | 65 | cv2.imshow("grille", grille) 66 | if flag == 0: 67 | 68 | grille_txt = [] 69 | for y in range(9): 70 | ligne = "" 71 | for x in range(9): 72 | y2min = y * case + marge 73 | y2max = (y + 1) * case - marge 74 | x2min = x * case + marge 75 | x2max = (x + 1) * case - marge 76 | cv2.imwrite("mat" + str(y) + str(x) + ".png", 77 | grille[y2min:y2max, x2min:x2max]) 78 | img = grille[y2min:y2max, x2min:x2max] 79 | x = img.reshape(1, 28, 28, 1) 80 | if x.sum() > 10000: 81 | prediction = classifier.predict_classes(x) 82 | ligne += "{:d}".format(prediction[0]) 83 | else: 84 | ligne += "{:d}".format(0) 85 | grille_txt.append(ligne) 86 | print(grille_txt) 87 | result = sol.sudoku(grille_txt) 88 | print("Resultat:", result) 89 | 90 | if result is not None: 91 | flag = 1 92 | fond = np.zeros( 93 | shape=(taille_grille, taille_grille, 3), dtype=np.float32) 94 | for y in range(len(result)): 95 | for x in range(len(result[y])): 96 | if grille_txt[y][x] == "0": 97 | cv2.putText(fond, "{:d}".format(result[y][x]), (( 98 | x) * case + marge + 3, (y + 1) * case - marge - 3), cv2.FONT_HERSHEY_SCRIPT_COMPLEX, 0.9, (0, 0, 255), 1) 99 | M = cv2.getPerspectiveTransform(pts2, pts1) 100 | h, w, c = frame.shape 101 | fondP = cv2.warpPerspective(fond, M, (w, h)) 102 | img2gray = cv2.cvtColor(fondP, cv2.COLOR_BGR2GRAY) 103 | ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY) 104 | mask = mask.astype('uint8') 105 | mask_inv = cv2.bitwise_not(mask) 106 | img1_bg = cv2.bitwise_and(frame, frame, mask=mask_inv) 107 | img2_fg = cv2.bitwise_and(fondP, fondP, mask=mask).astype('uint8') 108 | dst = cv2.add(img1_bg, img2_fg) 109 | dst = cv2.resize(dst, (1080, 620)) 110 | cv2.imshow("frame", dst) 111 | out.write(dst) 112 | 113 | else: 114 | frame = cv2.resize(frame, (1080, 620)) 115 | cv2.imshow("frame", frame) 116 | out.write(frame) 117 | 118 | else: 119 | flag = 0 120 | frame = cv2.resize(frame, (1080, 620)) 121 | cv2.imshow("frame", frame) 122 | out.write(frame) 123 | 124 | key = cv2.waitKey(1) & 0xFF 125 | if key == ord('q'): 126 | break 127 | 128 | 129 | out.release() 130 | cap.release() 131 | cv2.destroyAllWindows() 132 | -------------------------------------------------------------------------------- /sudoku_solver.py: -------------------------------------------------------------------------------- 1 | def sudoku(f): 2 | 3 | def af(g): 4 | for n, l in enumerate(g): 5 | for m, c in enumerate(l): 6 | P(str(c).replace("0", "."), end="") 7 | if m in {2, 5}: 8 | P("+", end="") 9 | P() 10 | if n in {2, 5}: 11 | P("+" * 11) 12 | 13 | def cp(q, s): 14 | l = set(s[q[0]]) 15 | l |= {s[i][q[1]] for i in range(9)} 16 | k = q[0] // 3, q[1] // 3 17 | for i in range(3): 18 | l |= set(s[k[0] * 3 + i][k[1] * 3:(k[1] + 1) * 3]) 19 | return set(range(1, 10)) - l 20 | 21 | def ec(l): 22 | q = set(l) - {0} 23 | for c in q: 24 | if l.count(c) != 1: 25 | return True 26 | return False 27 | 28 | P = print 29 | af(f) 30 | 31 | s = [] 32 | t = [] 33 | for nl, l in enumerate(f): 34 | try: 35 | n = list(map(int, l)) 36 | except: 37 | P("La ligne " + str(nl + 1) + " contient autre chose qu'un chiffre.") 38 | return 39 | if len(n) != 9: 40 | P("La ligne " + str(nl + 1) + " ne contient pas 9 chiffres.") 41 | return 42 | t += [[nl, i] for i in range(9) if n[i] == 0] 43 | s.append(n) 44 | if nl != 8: 45 | P("Le jeu contient " + str(nl + 1) + " lignes au lieu de 9.") 46 | return 47 | 48 | for l in range(9): 49 | if ec(s[l]): 50 | P("La ligne " + str(l + 1) + " est contradictoire.") 51 | return 52 | for c in range(9): 53 | k = [s[l][c] for l in range(9)] 54 | if ec(k): 55 | P("La colonne " + str(c + 1) + " est contradictoire.") 56 | return 57 | for l in range(3): 58 | for c in range(3): 59 | q = [] 60 | for i in range(3): 61 | q += s[l * 3 + i][c * 3:(c + 1) * 3] 62 | if ec(q): 63 | P("La cellule (" + str(l + 1) + ";" + 64 | str(c + 1) + ") est contradictoire.") 65 | return 66 | 67 | p = [[] for i in t] 68 | cr = 0 69 | 70 | while cr < len(t): 71 | p[cr] = cp(t[cr], s) 72 | try: 73 | while not p[cr]: 74 | s[t[cr][0]][t[cr][1]] = 0 75 | cr -= 1 76 | except: 77 | P("Le sudoku n'a pas de solution.") 78 | return 79 | s[t[cr][0]][t[cr][1]] = p[cr].pop() 80 | cr += 1 81 | 82 | af(s) 83 | return(s) 84 | --------------------------------------------------------------------------------