|
|
@@ -5,6 +5,25 @@ import copy
|
|
5
|
5
|
import argparse
|
|
6
|
6
|
|
|
7
|
7
|
|
|
|
8
|
+def non_greys_to_white(img, threshold=48):
|
|
|
9
|
+ b, g, r = cv2.split(img)
|
|
|
10
|
+ rgb_diff = cv2.subtract(cv2.max(cv2.max(b, g), r), cv2.min(cv2.min(b, g), r))
|
|
|
11
|
+ filtered = img.copy()
|
|
|
12
|
+ filtered[np.where(rgb_diff > threshold)] = (255, 255, 255)
|
|
|
13
|
+ return filtered
|
|
|
14
|
+
|
|
|
15
|
+
|
|
|
16
|
+def load_image_as_greyscale(file_name, filter_colours, colour_filter_threshold):
|
|
|
17
|
+ img = cv2.imread(file_name)
|
|
|
18
|
+ if img is None:
|
|
|
19
|
+ raise RuntimeError("Failed to load image")
|
|
|
20
|
+
|
|
|
21
|
+ if filter_colours:
|
|
|
22
|
+ img = non_greys_to_white(img, colour_filter_threshold)
|
|
|
23
|
+
|
|
|
24
|
+ return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
|
25
|
+
|
|
|
26
|
+
|
|
8
|
27
|
def preprocess_image(original, gaussian_blur_size, adaptive_threshold_block_size, adaptive_threshold_mean_adjustment, num_dilations):
|
|
9
|
28
|
img = cv2.GaussianBlur(original, (gaussian_blur_size, gaussian_blur_size), 0)
|
|
10
|
29
|
img = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, adaptive_threshold_block_size, adaptive_threshold_mean_adjustment)
|
|
|
@@ -136,25 +155,41 @@ def extract_grid_colours(img, num_rows, num_cols, sampling_block_size_ratio):
|
|
136
|
155
|
def grid_colours_to_blocks(grid_colours, num_rows, num_cols, sampling_threshold):
|
|
137
|
156
|
grid = copy.deepcopy(grid_colours)
|
|
138
|
157
|
warning = False
|
|
139
|
|
- for row in range(round(num_rows / 2)):
|
|
|
158
|
+
|
|
|
159
|
+ midpoint = num_rows // 2 + (0 if num_rows % 2 == 0 else 1)
|
|
|
160
|
+ for row in range(midpoint):
|
|
140
|
161
|
for col in range(num_cols):
|
|
|
162
|
+ # If there is an odd number of rows then row and row2 will point to
|
|
|
163
|
+ # the same row when we reach the middle. Doesn't seem worth adding a
|
|
|
164
|
+ # special case.
|
|
141
|
165
|
row2 = num_rows - row - 1
|
|
142
|
166
|
col2 = num_cols - col - 1
|
|
143
|
167
|
delta1 = grid_colours[row][col] - sampling_threshold
|
|
144
|
168
|
delta2 = grid_colours[row2][col2] - sampling_threshold
|
|
145
|
169
|
|
|
146
|
170
|
if (delta1 > 0) and (delta2 > 0):
|
|
147
|
|
- block = 0
|
|
|
171
|
+ filled = False
|
|
148
|
172
|
elif (delta1 < 0) and (delta2 < 0):
|
|
149
|
|
- block = 1
|
|
|
173
|
+ filled = True
|
|
150
|
174
|
else:
|
|
151
|
175
|
warning = True
|
|
152
|
176
|
if abs(delta1) > abs(delta2):
|
|
153
|
|
- block = 1 if delta1 < 0 else 0
|
|
|
177
|
+ filled = delta1 < 0
|
|
154
|
178
|
else:
|
|
155
|
|
- block = 1 if delta2 < 0 else 0
|
|
|
179
|
+ filled = delta2 < 0
|
|
|
180
|
+
|
|
|
181
|
+ grid[row][col] = {'filled': filled}
|
|
|
182
|
+ grid[row2][col2] = {'filled': filled}
|
|
156
|
183
|
|
|
157
|
|
- grid[row][col] = grid[row2][col2] = block
|
|
|
184
|
+ number = 1
|
|
|
185
|
+ for row in range(num_rows):
|
|
|
186
|
+ for col in range(num_cols):
|
|
|
187
|
+ if (not grid[row][col]['filled'] and (
|
|
|
188
|
+ (((col == 0) or grid[row][col - 1]['filled']) and (col < num_cols - 1) and not grid[row][col + 1]['filled']) or
|
|
|
189
|
+ (((row == 0) or grid[row - 1][col]['filled']) and (row < num_rows - 1) and not grid[row + 1][col]['filled'])
|
|
|
190
|
+ )):
|
|
|
191
|
+ grid[row][col]['number'] = number
|
|
|
192
|
+ number += 1
|
|
158
|
193
|
|
|
159
|
194
|
return warning, grid
|
|
160
|
195
|
|
|
|
@@ -169,38 +204,32 @@ def draw_point(image, point, colour):
|
|
169
|
204
|
image[y, x] = colour
|
|
170
|
205
|
|
|
171
|
206
|
|
|
172
|
|
-def show_image(image):
|
|
173
|
|
- cv2.namedWindow('xword', cv2.WINDOW_NORMAL)
|
|
174
|
|
- cv2.imshow('xword', image)
|
|
175
|
|
- while cv2.waitKey() & 0xFF != ord('q'):
|
|
176
|
|
- pass
|
|
177
|
|
- cv2.destroyAllWindows()
|
|
178
|
|
-
|
|
179
|
|
-
|
|
180
|
|
-def extract_crossword(
|
|
|
207
|
+def extract_crossword_grid(
|
|
181
|
208
|
file_name,
|
|
|
209
|
+ callback=None,
|
|
|
210
|
+ remove_colours=False,
|
|
|
211
|
+ colour_removal_threshold=48,
|
|
182
|
212
|
gaussian_blur_size=11,
|
|
183
|
213
|
adaptive_threshold_block_size=11,
|
|
184
|
214
|
adaptive_threshold_mean_adjustment=2,
|
|
185
|
215
|
square=True,
|
|
186
|
216
|
num_dilations=1,
|
|
187
|
217
|
contour_erosion_kernel_size=5,
|
|
188
|
|
- contour_erosion_iterations=6,
|
|
|
218
|
+ contour_erosion_iterations=5,
|
|
189
|
219
|
line_detector_element_size=51,
|
|
190
|
220
|
sampling_block_size_ratio=0.25,
|
|
191
|
221
|
sampling_threshold_quantile=0.3,
|
|
192
|
|
- sampling_threshold=None,
|
|
193
|
|
- grid_line_thickness=4,
|
|
194
|
|
- grid_square_size=64,
|
|
195
|
|
- grid_border_size=20,
|
|
|
222
|
+ sampling_threshold=None
|
|
196
|
223
|
):
|
|
197
|
224
|
warnings = []
|
|
198
|
225
|
|
|
199
|
|
- original = cv2.imread(file_name, cv2.IMREAD_GRAYSCALE)
|
|
200
|
|
- if original is None:
|
|
201
|
|
- raise RuntimeError("Failed to load image")
|
|
|
226
|
+ original = load_image_as_greyscale(file_name, remove_colours, colour_removal_threshold)
|
|
|
227
|
+ if callback is not None:
|
|
|
228
|
+ callback('original', original)
|
|
202
|
229
|
|
|
203
|
230
|
img = preprocess_image(original, gaussian_blur_size, adaptive_threshold_block_size, adaptive_threshold_mean_adjustment, num_dilations)
|
|
|
231
|
+ if callback is not None:
|
|
|
232
|
+ callback('preprocessed', img)
|
|
204
|
233
|
|
|
205
|
234
|
biggest = find_biggest_contour(img)
|
|
206
|
235
|
biggest = erode_contour(img.shape, biggest, contour_erosion_kernel_size, contour_erosion_iterations)
|
|
|
@@ -208,6 +237,8 @@ def extract_crossword(
|
|
208
|
237
|
top_left, top_right, bottom_right, bottom_left = get_contour_corners(img, biggest)
|
|
209
|
238
|
|
|
210
|
239
|
img = extract_square(img, top_left, top_right, bottom_right, bottom_left)
|
|
|
240
|
+ if callback is not None:
|
|
|
241
|
+ callback('pre-fft', img)
|
|
211
|
242
|
|
|
212
|
243
|
num_rows = get_line_frequency(img, line_detector_element_size, 1)
|
|
213
|
244
|
num_cols = get_line_frequency(img, line_detector_element_size, 0)
|
|
|
@@ -226,6 +257,17 @@ def extract_crossword(
|
|
226
|
257
|
if warning:
|
|
227
|
258
|
warnings.append("Some blocks may be the wrong colour")
|
|
228
|
259
|
|
|
|
260
|
+ return warnings, grid, num_rows, num_cols, block_img
|
|
|
261
|
+
|
|
|
262
|
+
|
|
|
263
|
+def draw_grid(
|
|
|
264
|
+ grid,
|
|
|
265
|
+ num_rows,
|
|
|
266
|
+ num_cols,
|
|
|
267
|
+ grid_line_thickness=4,
|
|
|
268
|
+ grid_square_size=64,
|
|
|
269
|
+ grid_border_size=20
|
|
|
270
|
+):
|
|
229
|
271
|
step = grid_square_size + grid_line_thickness
|
|
230
|
272
|
grid_height = num_rows * step + grid_line_thickness
|
|
231
|
273
|
grid_width = num_cols * step + grid_line_thickness
|
|
|
@@ -234,9 +276,7 @@ def extract_crossword(
|
|
234
|
276
|
for row in range(num_rows):
|
|
235
|
277
|
y = row * step + grid_line_thickness + grid_border_size
|
|
236
|
278
|
for col in range(num_cols):
|
|
237
|
|
- if grid[row][col] == 0:
|
|
|
279
|
+ if not grid[row][col]['filled']:
|
|
238
|
280
|
x = col * step + grid_line_thickness + grid_border_size
|
|
239
|
281
|
cv2.rectangle(output, (x, y), (x + grid_square_size - 1, y + grid_square_size - 1), 255, -1)
|
|
240
|
|
-
|
|
241
|
|
- _, png = cv2.imencode('.png', output)
|
|
242
|
|
- return png.tobytes(), warnings
|
|
|
282
|
+ return output
|