1
+ import cv2
2
+ import numpy
3
+ from matplotlib import pyplot
4
+ import sys
5
+ import json
6
+ import util
7
+
8
+
9
+ """
10
+ Main routine to process the image and extract the sudoku matrix from it
11
+ """
12
+
13
+
14
+ def extract_matrix_from_image (image , x_metrics , y_metrics ):
15
+
16
+ x_coords , y_coords = util .get_cell_boundaries (image )
17
+
18
+ # Now extract the images of the cells (between the lines), based on the coordinates we just
19
+ # calculated. When we have these extracted images, sum the pixel counts along the horizontal
20
+ # and vertical axes. This will be used to compare with training data to identify the digits
21
+ # in the cells
22
+
23
+ pyplot .rcParams ['figure.dpi' ] = 50
24
+ bw_threshold = 160
25
+ # Make the image monochrome. If the original pixel value > threshold, 1, otherwise 0.
26
+ (thresh , monochrome_image ) = cv2 .threshold (image , bw_threshold , 1 , cv2 .THRESH_BINARY_INV )
27
+
28
+ puzzle_matrix = []
29
+ for y_coord in y_coords :
30
+ new_matrix_row = []
31
+ for x_coord in x_coords :
32
+ raw_image = monochrome_image [y_coord [0 ]:y_coord [1 ], x_coord [0 ]:x_coord [1 ]]
33
+ image_height , image_width = raw_image .shape
34
+ image_sum = raw_image .sum ()
35
+ image_density = image_sum / (image_width * image_height )
36
+ # If the image density (% of black pixels in the image) is less than a certain threshold
37
+ # we assume the cell is empty and return 0. This is not a test for 0 % since there can be
38
+ # noise in the image. If above the threshold, then determine the number from training data
39
+ if image_density < 0.001 :
40
+ number = 0
41
+ else :
42
+ # show_image(raw_image,
43
+ # title="Y - %s:%s. X - %s:%s" % (y_coord[0], y_coord[1], x_coord[0], x_coord[1]))
44
+ cell_image = util .trim (raw_image )
45
+ if cell_image is None :
46
+ number = 0
47
+ else :
48
+ number = get_number_from_image (cell_image , x_metrics , y_metrics )
49
+ print ("Number: %s" % number )
50
+ new_matrix_row .append (number )
51
+ puzzle_matrix .append (new_matrix_row )
52
+ print ("\n " )
53
+ return puzzle_matrix
54
+
55
+
56
+ """
57
+ This routine finds the digit from a cell image based on the training data
58
+ """
59
+
60
+
61
+ def get_number_from_image (cell_image , x_metrics , y_metrics ):
62
+ # This is summing columns vertically
63
+ x_counts = cell_image .sum (axis = 0 )
64
+ # This is summing rows horizontally
65
+ y_counts = cell_image .sum (axis = 1 )
66
+ x_metrics_size = len (x_metrics ["1" ])
67
+ y_metrics_size = len (y_metrics ["1" ])
68
+ #normalize the size of the image arrays to match the metrics data
69
+ x_histogram = morph_array_to_size (x_counts , x_metrics_size )
70
+ y_histogram = morph_array_to_size (y_counts , y_metrics_size )
71
+ # Now get percentages
72
+ x_sum = sum (x_histogram )
73
+ y_sum = sum (y_histogram )
74
+ for n in range (x_histogram .size ):
75
+ x_histogram [n ] = x_histogram [n ]/ x_sum
76
+ for n in range (y_histogram .size ):
77
+ y_histogram [n ] = y_histogram [n ]/ y_sum
78
+ # Now calculate difference between x and y pixel distribution for this image and metrics data
79
+ # the 1000 is there for readability
80
+ x_distance = numpy .zeros ((len (x_metrics )+ 1 ), dtype = float )
81
+ for n in range (0 , len (x_metrics )):
82
+ x_distance [n ] = diff_between_arrays (x_histogram , x_metrics [str (n )]) * 1000
83
+ y_distance = numpy .zeros ((len (y_metrics )+ 1 ), dtype = float )
84
+ for n in range (0 , len (y_metrics )):
85
+ y_distance [n ] = diff_between_arrays (y_histogram , y_metrics [str (n )]) * 1000
86
+
87
+ # x_distance is an array that has the least squares distance between the x-axis histogram
88
+ # of this image and the x-axis histograms of the training data [1..9]. y_distance is the
89
+ # same thing along the y-axis. The algorithm to determine the digit in the image goes like this:
90
+ # We first match using the y-axis histogram. Experiments have shown this is accurate for
91
+ # everything except some confusion when a '1' is incorrectly recognized as a '2'. So,
92
+ # when we see a '2', perform a second check using the x-axis to see which distance is less,
93
+ # '1' or '2'.
94
+ #
95
+ # Other algorithms attempted:
96
+ # - x_distance
97
+ # - y_distance
98
+ # - x_distance * y_distance
99
+ current_y_distance_min = y_distance [0 ]
100
+ number = 0
101
+ # print("x histogram: %s" % x_histogram)
102
+ # print("y histogram: %s" % y_histogram)
103
+ for n in range (0 , len (x_metrics )):
104
+ print ('n: %s\t x_distance: %.2f\t y_distance: %.2f' %
105
+ (n , x_distance [n ], y_distance [n ]))
106
+ if y_distance [n ] < current_y_distance_min :
107
+ current_y_distance_min = y_distance [n ]
108
+ number = n
109
+ if number == 2 :
110
+ if x_distance [1 ] < x_distance [2 ]:
111
+ number = 1
112
+
113
+ return number
114
+
115
+ """
116
+ The sizes of the images for the matrix cells vary, meaning the length of the image histogram arrays of the
117
+ pixel distributions vary in length. This routine changes the length of the image histogram array to
118
+ match the length of the training data array. It also strips leading and trailing 0s from the input
119
+ array, effectively trimming blank space from around the digit (to match what's in the training data).
120
+
121
+ Because detecting lines is imperfect, the images can have some noise around the edges, parts of the lines
122
+ we weren't able to remove, the trimming starts in the middle and works its way out. Note, this assumes
123
+ that the number in the cell is roughly centered (pretty safe for computer generate Sudoku puzzles).
124
+ """
125
+
126
+ def morph_array_to_size (array , to_size ):
127
+ from_array = array
128
+ from_size = len (from_array )
129
+ to_array = numpy .zeros ((to_size ), dtype = float )
130
+
131
+ if from_size >= to_size :
132
+ # for f in range(from_size):
133
+ # to_array[f] = from_array[f]
134
+ for f in range (from_size ):
135
+ starting_to_element = int (to_size / from_size * f )
136
+ ending_to_element = int (to_size / from_size * (f + 1 ))
137
+ if starting_to_element == ending_to_element or \
138
+ ending_to_element - to_size / from_size * (f + 1 ) == 0 :
139
+ to_array [starting_to_element ] += from_array [f ]
140
+ else :
141
+ amt1_pct = from_size / to_size * f - int (from_size / to_size * f )
142
+ amt2_pct = 1 - amt1_pct
143
+ to_array [starting_to_element ] += from_array [f ] * amt1_pct
144
+ to_array [ending_to_element ] += from_array [f ] * amt2_pct
145
+ else :
146
+ for t in range (to_size ):
147
+ starting_from_element = int (from_size / to_size * t )
148
+ ending_from_element = int (from_size / to_size * (t + 1 ))
149
+ if starting_from_element == ending_from_element or \
150
+ ending_from_element - from_size / to_size * (t + 1 ) == 0 :
151
+ to_array [t ] += from_array [starting_from_element ] * from_size / to_size
152
+ else :
153
+ amt1_pct = int (from_size / to_size * t ) + 1 - from_size / to_size * t
154
+ amt2_pct = from_size / to_size * (t + 1 ) - int (from_size / to_size * (t + 1 ))
155
+ to_array [t ] += from_array [starting_from_element ] * amt1_pct
156
+ to_array [t ] += from_array [ending_from_element ] * amt2_pct
157
+
158
+ return to_array
159
+
160
+ def diff_between_arrays (a1 , a2 ):
161
+ diff = 0
162
+ for n in range (len (a1 )):
163
+ diff += (a1 [n ] - a2 [n ]) * (a1 [n ] - a2 [n ])
164
+ return diff
165
+
166
+ def main ():
167
+ if len (sys .argv ) < 3 :
168
+ raise Exception ("Inputs:\n \t parameter 1: Test image to use for processing.\n \t parameter 2: folder for image metrics json." )
169
+ input_image = sys .argv [1 ]
170
+ print ("Image: %s" % input_image )
171
+ metrics_folder = sys .argv [2 ]
172
+ x_metrics_file = open (metrics_folder + "x.json" , "r" )
173
+ y_metrics_file = open (metrics_folder + "y.json" , "r" )
174
+ x_metrics = json .loads (x_metrics_file .read ())
175
+ y_metrics = json .loads (y_metrics_file .read ())
176
+ image = cv2 .imread (input_image , cv2 .IMREAD_GRAYSCALE )
177
+ matrix = extract_matrix_from_image (image , x_metrics , y_metrics )
178
+ print (matrix )
179
+
180
+ if __name__ == '__main__' :
181
+ main ()
0 commit comments