forked from guille0/songoku
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathimage_parsing.py
237 lines (186 loc) · 9.56 KB
/
image_parsing.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
import numpy as np
import cv2
from helpers import crop_from_points, perspective_transform, resize_to_square
from helpers import blend_non_transparent
from sudoku_guessing import Sudoku
# MAIN
def sudoku_master(img_original):
# Tries to find the part of the image with the sudoku
## corners are top left, top right, bottom left, bottom right
_, corners = find_sudoku(img_original, draw_contours=False ,test=False)
# If we got a sudoku image
if corners is not None:
# We crop out the sudoku and get the info needed to paste it back (matrix)
img_cropped_sudoku, transformation = crop_from_points(img_original, corners)
transformation_matrix = transformation['matrix']
original_shape = transformation['original_shape']
# We inverse the matrix so we can do the opposite transformation later
transformation_matrix = np.linalg.pinv(transformation_matrix)
# We crop out each number from the sudoku and create a Sudoku instance
sudoku = build_sudoku(img_cropped_sudoku, test=False)
# We pass the image of each case in the sudoku to a neural network to read
## NOTE: NUMBER READING THRESHOLD
## Minimum confidence the neural network needs to have about its guess (from 0 to 1)
sudoku.guess_sudoku(confidence_threshold=0)
# Now that we have processed the sudoku, we can solve it with a normal sudoku algorithm
# Also writes the results into the cropped sudoku
sudoku.solve(img_cropped_sudoku, approximate=0.95)
# sudoku.write_test(img_cropped_sudoku)
# We paste the cropped sudoku which is now solved into the camera image
img_sudoku_final = perspective_transform(img_cropped_sudoku, transformation_matrix, original_shape)
img_final = blend_non_transparent(img_original, img_sudoku_final)
else:
# If there is no sudoku to write on
img_final = img_original
return img_final
def find_sudoku(img, draw_contours=False, test=False):
'''Finds the biggest object in the image and returns its 4 corners (to crop it)'''
# Preprocessing:
edges = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.GaussianBlur(edges, (7, 7), 0)
kernel = np.ones((3, 3), np.uint8)
edges = cv2.morphologyEx(edges, cv2.MORPH_OPEN, kernel)
edges = cv2.adaptiveThreshold(edges, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 19, 2)
# Get contours:
contours, _ = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# Extracting the image of what we think might be a sudoku:
topbottom_edges = (0, img.shape[0]-1)
leftright_edges = (0, img.shape[1]-1)
# NOTE change this to 0?
# NOTE in my webcam contours[0] is always the whole image, so i just ignore it
if len(contours) > 1:
conts = sorted(contours, key=cv2.contourArea, reverse=True)
# Loops through the found objects
# for something with at least 4 corners and kinda big (>10_000 pixels)
# NOTE change the 10000 if different webcam
for cnt in conts:
epsilon = 0.025*cv2.arcLength(cnt, True)
cnt = cv2.approxPolyDP(cnt, epsilon, True)
if len(cnt) > 3:
# Gets the 4 corners of the object (assume it's a square)
topleft = min(cnt, key=lambda x: x[0,0]+x[0,1])
bottomright = max(cnt, key=lambda x: x[0,0]+x[0,1])
topright = max(cnt, key=lambda x: x[0,0]-x[0,1])
bottomleft = min(cnt, key=lambda x: x[0,0]-x[0,1])
corners = (topleft, topright, bottomleft, bottomright)
# Sometimes it finds 'objects' which are just parts of the screen
# Ignore those
badobject = False
for corner in corners:
if corner[0][0] in leftright_edges or corner[0][1] in topbottom_edges:
badobject = True
if badobject is True:
continue
# Just a test, ignore
if test is True:
cv2.drawContours(img, [cnt], 0, (0,255,0), 2)
# TESTING CORNERS
# cv2.circle(img, (topleft[0][0], topleft[0][1]), 5, 0, thickness=5, lineType=8, shift=0)
# cv2.circle(img, (topright[0][0], topright[0][1]), 5, 0, thickness=5, lineType=8, shift=0)
# cv2.circle(img, (bottomleft[0][0], bottomleft[0][1]), 5, 0, thickness=5, lineType=8, shift=0)
# cv2.circle(img, (bottomright[0][0], bottomright[0][1]), 5, 0, thickness=5, lineType=8, shift=0)
else:
# If it has less than 4 corners its not a sudoku
return edges, None
# NOTE edit this for different webcams, I found at least size 10k is good
if cv2.contourArea(cnt) > 10000:
rect = cv2.minAreaRect(cnt)
box = cv2.boxPoints(rect)
box = np.int0(box)
if draw_contours is True:
cv2.drawContours(edges, [box], 0, (0,255,0), 2)
# Returns the 4 corners of an object with 4+ corners and area of >10k
return edges, corners
else:
return edges, None
return edges, None
def build_sudoku(sudoku_image, test=False):
# Different preprocessings
# can dilate/open if numbers are small or blur if there's noise
edges = cv2.cvtColor(sudoku_image, cv2.COLOR_BGR2GRAY)
# edges = cv2.dilate(edges, np.ones((2, 2)))
# edges = cv2.morphologyEx(edges, cv2.MORPH_OPEN, np.ones((1,5),np.uint8))
# edges = cv2.GaussianBlur(edges,(5,5),0)
# NOTE 5,7 is normal or 7,7 V
edges = cv2.adaptiveThreshold(edges, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 7, 7)
# Just a test, ignore
if test is True:
cv2.imshow('edgys', edges)
# Divide the sudoku in cases and load its data
h, w = sudoku_image.shape[0], sudoku_image.shape[1]
# Sudoku object that will contain all the information
sudoku = Sudoku.instance()
# NOTE Change border for different kinds of sudoku (bigger/smaller numbers, thick lines...)
# TODO find a way to find this automatically?
# The borders of the whole puzzle (4?)
sudoku_border = 4
# The lines inbetween the cases (2? 4?)
border = 4
x = w/9
y = h/9
for i in range(9):
for j in range(9):
# We get the position of each case (simply dividing the image in 9)
top = int(round(y*i+border))
left = int(round(x*j+border))
right = int(round(x*(j+1)-border))
bottom = int(round(y*(i+1)-border))
if i == 0:
top+=sudoku_border
if i == 8:
bottom-=sudoku_border
if j == 0:
left+=sudoku_border
if j == 8:
right-=sudoku_border
point = [
[[left, top]],
[[right, top]],
[[left, bottom]],
[[right, bottom]]
]
# Crop out the preprocessed case (edges)
square, _ = crop_from_points(edges, point)
if test is True:
if i == 0 and j == 3:
cv2.imshow('square', square)
if i == 1 and j == 0:
cv2.imshow('ss', square)
# Making the number fatter so it's easier to extract
fat_square = square.copy()
contours, _ = cv2.findContours(fat_square, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(fat_square, contours, -1, (255, 255, 255), 2)
# Get the contour of the number (biggest object in a case)
contours, _ = cv2.findContours(fat_square, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
physical_position = [top, right, bottom, left]
if contours:
conts = sorted(contours, key=cv2.contourArea, reverse=True)
# Get the biggest object in the case (assume it's a number)
cnt = conts[0]
# minarea is an arbitrary size that the number must be to be considered valid
# NOTE change it if it detects noise/doesn't detect numbers (0.04)
minarea = x*y*0.03
if cv2.contourArea(cnt) > minarea:
# Crop out the number
rect = cv2.minAreaRect(cnt)
box = cv2.boxPoints(rect)
box = np.int0(box)
minx = max(min(box, key=lambda g: g[0])[0], 0)
miny = max(min(box, key=lambda g: g[1])[1], 0)
maxx = min(max(box, key=lambda g: g[0])[0], int(x))
maxy = min(max(box, key=lambda g: g[1])[1], int(y))
number_image = square[miny:maxy, minx:maxx]
if number_image is None or number_image.shape[0] < 2 or number_image.shape[1] < 2:
# If there's not a number in there
sudoku.update_case(None, (i, j), physical_position)
else:
# If we get a valid number image:
# Resize it to 28x28 for neural network purposes
final = resize_to_square(number_image)
# Send the data to the Sudoku object
sudoku.update_case(final, (i, j), physical_position)
else:
sudoku.update_case(None, (i, j), physical_position)
else:
sudoku.update_case(None, (i, j), physical_position)
return sudoku