Ujjwal123 commited on
Commit
bcf5fac
·
1 Parent(s): bd11644

Copied all grid and clue extraction logic from EZ-crossword

Browse files
Files changed (4) hide show
  1. Dockerfile +30 -0
  2. extractpuzzle.py +787 -0
  3. main.py +69 -0
  4. requirements.txt +12 -0
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.9
5
+
6
+ RUN apt-get update
7
+ RUN apt-get -y install \
8
+ tesseract-ocr \
9
+ tesseract-ocr-jpn \
10
+ libgl1-mesa-dev;
11
+ RUN apt-get clean
12
+
13
+ WORKDIR /code
14
+
15
+ COPY requirements.txt ./
16
+
17
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
18
+
19
+ RUN useradd -m -u 1000 user
20
+
21
+ USER user
22
+
23
+ ENV HOME=/home/user \
24
+ PATH=/home/user/.local/bin:$PATH
25
+
26
+ WORKDIR $HOME/app
27
+
28
+ COPY --chown=user . $HOME/app/
29
+
30
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
extractpuzzle.py ADDED
@@ -0,0 +1,787 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import math
4
+ from sklearn.linear_model import LinearRegression
5
+ import pytesseract
6
+ import re
7
+
8
+
9
+ pytesseract.pytesseract.tesseract_cmd = "/usr/bin/tesseract"
10
+
11
+ def first_preprocessing(image):
12
+ gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
13
+ canny = cv2.Canny(gray,75,25)
14
+ contours,hierarchies = cv2.findContours(canny,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)
15
+ sorted_contours = sorted(contours,key = cv2.contourArea,reverse = True)
16
+ largest_contour = sorted_contours[0]
17
+ box = cv2.boundingRect(sorted_contours[0])
18
+ x = box[0]
19
+ y = box[1]
20
+ w = box[2]
21
+ h = box[3]
22
+ result = cv2.rectangle(image, (x, y), (x + w, y + h), (255, 255, 255), -1)
23
+ return result
24
+
25
+ def remove_head(image):
26
+ custom_config = r'--oem 3 --psm 6' # Tesseract OCR configuration
27
+ detected_text = pytesseract.image_to_string(image, config=custom_config)
28
+ lines = detected_text.split('\n')
29
+
30
+ # Find the first line containing some text
31
+ line_index = 0
32
+ for i, line in enumerate(lines):
33
+ if line.strip() != '':
34
+ line_index = i
35
+ break
36
+ first_newline_idx = detected_text.find('\n')
37
+ result = cv2.rectangle(image, (0, line_index), (image.shape[1], first_newline_idx), (255,255,255), thickness=cv2.FILLED)
38
+ return result
39
+
40
+ def second_preprocessing(image):
41
+ gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
42
+ canny = cv2.Canny(gray,75,25)
43
+ contours,hierarchies = cv2.findContours(canny,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)
44
+ sorted_contours = sorted(contours,key = cv2.contourArea,reverse = True)
45
+ largest_contour = sorted_contours[0]
46
+ box2 = cv2.boundingRect(sorted_contours[0])
47
+ x = box2[0]
48
+ y = box2[1]
49
+ w = box2[2]
50
+ h = box2[3]
51
+ result2 = cv2.rectangle(image, (x, y), (x + w, y + h), (255, 255, 255), -1)
52
+ return result2
53
+
54
+ def find_vertical_profile(image):
55
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
56
+ _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
57
+ vertical_profile = np.sum(binary, axis=0)
58
+ return vertical_profile
59
+
60
+ def detect_steepest_changes(projection_profile, threshold=0.4, start_idx=0, min_valley_width=10, min_search_width=50):
61
+ differences = np.diff(projection_profile)
62
+ change_points = np.where(np.abs(differences) > threshold * np.max(np.abs(differences)))[0]
63
+ left_boundaries = []
64
+ right_boundaries = []
65
+
66
+ for idx in change_points:
67
+ if idx <= start_idx:
68
+ continue
69
+
70
+ if idx - start_idx >= min_search_width:
71
+ decreasing_profile = projection_profile[idx:]
72
+ if np.any(decreasing_profile > 0):
73
+ right_boundary = idx + np.argmin(decreasing_profile)
74
+ right_boundaries.append(right_boundary)
75
+ else:
76
+ continue
77
+ valley_start = max(start_idx, idx - min_valley_width)
78
+ valley_start = valley_start-40
79
+ valley_end = min(idx + min_valley_width, len(projection_profile) - 1)
80
+ valley = valley_start + np.argmin(projection_profile[valley_start:valley_end])
81
+ left_boundaries.append(valley)
82
+
83
+ break
84
+
85
+ return left_boundaries, right_boundaries
86
+
87
+ def crop_text_columns(image, projection_profile, threshold=0.4):
88
+ start_idx = 0
89
+ text_columns = []
90
+
91
+ while True:
92
+ left_boundaries, right_boundaries = detect_steepest_changes(projection_profile, threshold, start_idx)
93
+ if not left_boundaries or not right_boundaries:
94
+ break
95
+ left = left_boundaries[0]
96
+ right = right_boundaries[0]
97
+ text_column = image[:, left:right]
98
+ text_columns.append(text_column)
99
+
100
+ start_idx = right
101
+
102
+ return text_columns
103
+
104
+
105
+ def parse_clues(clue_text):
106
+ lines = clue_text.split('\n')
107
+ clues = {}
108
+ number = None
109
+ column = 0
110
+ for line in lines:
111
+ if "column separation" in line:
112
+ column += 1
113
+ continue
114
+ pattern = r"^(\d+(?:\.\d+)?)\s*(.+)" # Updated pattern to handle decimal point numbers for clues
115
+ match = re.search(pattern, line)
116
+ if match:
117
+ number = float(match.group(1)) # Convert the matched number to float if there is a decimal point
118
+ if number not in clues:
119
+ clues[number] = [column,match.group(2).strip()]
120
+ else:
121
+ continue
122
+ elif number is None:
123
+ continue
124
+ elif clues[number][0] != column:
125
+ continue
126
+ else:
127
+ clues[number][1] += " " + line.strip() # Append to the previous clue if it's a multiline clue
128
+
129
+ return clues
130
+
131
+ def parse_crossword_clues(text):
132
+ # Check if "Down" clues are present
133
+ match = re.search(r'[dD][oO][wW][nN]\n', text)
134
+ if match:
135
+ across_clues, down_clues = re.split(r'[dD][oO][wW][nN]\n', text)
136
+ else:
137
+ # If "Down" clues are not present, set down_clues to an empty string
138
+ across_clues, down_clues = text, ""
139
+
140
+ across = parse_clues(across_clues)
141
+ down = parse_clues(down_clues)
142
+
143
+ return across, down
144
+
145
+
146
+ def classify_text(filtered_columns):
147
+ text = ""
148
+ custom_config = r'--oem 3 --psm 6'
149
+ for i, column in enumerate(filtered_columns):
150
+ column2 = cv2.cvtColor(column, cv2.COLOR_BGR2RGB)
151
+ scale_factor = 2.0 # You can adjust this value
152
+
153
+ # Calculate the new dimensions after scaling
154
+ new_width = int(column2.shape[1] * scale_factor)
155
+ new_height = int(column2.shape[0] * scale_factor)
156
+
157
+ # Resize the image using OpenCV
158
+ scaled_image = cv2.resize(column2, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
159
+
160
+ # Apply image enhancement techniques
161
+ denoised_image = cv2.fastNlMeansDenoising(scaled_image, None, h=10, templateWindowSize=7, searchWindowSize=21)
162
+ enhanced_image = cv2.cvtColor(denoised_image, cv2.COLOR_BGR2GRAY) # Convert to grayscale # Apply histogram equalization
163
+ detected_text = pytesseract.image_to_string(enhanced_image, config=custom_config)
164
+ # print(detected_text)
165
+ text+=detected_text
166
+ across_clues, down_clues = parse_crossword_clues(text)
167
+ return across_clues,down_clues
168
+
169
+ def get_text(image):
170
+ image = cv2.cvtColor(image,cv2.COLOR_GRAY2BGR)
171
+ result = first_preprocessing(image)
172
+ result1 = remove_head(result)
173
+ result2 = second_preprocessing(result1)
174
+ vertical_profile = find_vertical_profile(result2)
175
+ combined_columns = crop_text_columns(result2,vertical_profile)
176
+ across,down = classify_text(combined_columns)
177
+ return across,down
178
+
179
+
180
+ ################################ Grid Extraction begins here ###########################
181
+ ########################################################################################
182
+
183
+
184
+ # for applying non max suppression of the contours
185
+ def calculate_iou(image, contour1, contour2):
186
+ # Create masks for each contour
187
+ mask1 = np.zeros_like(image, dtype=np.uint8)
188
+ cv2.drawContours(mask1, [contour1], -1, 255, thickness=cv2.FILLED)
189
+
190
+ mask2 = np.zeros_like(image, dtype=np.uint8)
191
+ cv2.drawContours(mask2, [contour2], -1, 255, thickness=cv2.FILLED)
192
+
193
+ # Find the intersection between the two masks
194
+ intersection = cv2.bitwise_and(mask1, mask2)
195
+
196
+ # Calculate the intersection area
197
+ intersection_area = cv2.countNonZero(intersection)
198
+
199
+ # Calculate the union area (Not the accurate one but works alright XD !)
200
+ union_area = cv2.contourArea(cv2.convexHull(np.concatenate((contour1, contour2))))
201
+
202
+ # Calculate the IoU
203
+ iou = intersection_area / union_area
204
+ return iou
205
+
206
+ # remove overlapping contours, non square and not quardatic contours
207
+ # this check every contour with every other contour so be careful
208
+ def filter_contours(img_gray2, contours, iou_threshold = 0.6, asp_ratio = 1,tolerance = 0.5):
209
+ # Remove overlapping contours, removing that are not square
210
+ filtered_contours = []
211
+ epsilon = 0.02
212
+ for contour in contours:
213
+
214
+ # Approximate the contour to reduce the number of points
215
+ epsilon_multiplier = epsilon * cv2.arcLength(contour, True)
216
+ approximated_contour = cv2.approxPolyDP(contour, epsilon_multiplier, True)
217
+
218
+ # find the aspect ratio of the contour, if it is close to 1 then keep it otherwise discard
219
+ _,_,w,h = cv2.boundingRect(approximated_contour)
220
+ if(abs(float(w)/h - asp_ratio) > tolerance ): continue
221
+
222
+ # Calculate the IoU with all existing contours
223
+ iou_values = [calculate_iou(img_gray2,np.array(approximated_contour), np.array(existing_contour)) for existing_contour in filtered_contours]
224
+
225
+ # If the IoU value with all existing contours is below the threshold, add the current contour
226
+ if not any(iou_value > iou_threshold for iou_value in iou_values):
227
+ filtered_contours.append(approximated_contour)
228
+
229
+ return filtered_contours
230
+
231
+ # https://stackoverflow.com/questions/383480/intersection-of-two-lines-defined-in-rho-theta-parameterization/383527#383527
232
+ # Define the parametricIntersect function
233
+ def parametricIntersect(r1, t1, r2, t2):
234
+ ct1 = np.cos(t1)
235
+ st1 = np.sin(t1)
236
+ ct2 = np.cos(t2)
237
+ st2 = np.sin(t2)
238
+ d = ct1 * st2 - st1 * ct2
239
+ if d != 0.0:
240
+ x = int((st2 * r1 - st1 * r2) / d)
241
+ y = int((-ct2 * r1 + ct1 * r2) / d)
242
+ return x, y
243
+ else:
244
+ return None
245
+
246
+ # Group the coordinate to a list such that each point in a list may belong to a line
247
+ def group_lines(coordinates,axis=0,threshold=10):
248
+ sorted_coordinates = list(sorted(coordinates,key=lambda x: x[axis]))
249
+ groups = []
250
+ current_group = []
251
+
252
+ for i in range(len(sorted_coordinates)):
253
+ if i!=0 and abs(current_group[0][axis] - sorted_coordinates[i][axis]) > threshold: # condition to change the group
254
+ if len(current_group) > 4:
255
+ groups.append(current_group)
256
+ current_group = []
257
+ current_group.append(sorted_coordinates[i]) # condition to append to the group
258
+ if(len(current_group) > 4):
259
+ groups.append(current_group)
260
+ return groups
261
+
262
+ # Use the Grouped Lines to Fit a line using Linear Regression
263
+ def fit_lines(grouped_lines,is_horizontal = False):
264
+ actual_lines = []
265
+ for coordinates in grouped_lines:
266
+ # Converting into numpy array
267
+ coordinates_arr = np.array(coordinates)
268
+ # Separate the x and y coordinates
269
+ x = coordinates_arr[:, 0]
270
+ y = coordinates_arr[:, 1]
271
+ # Fit a linear regression model
272
+ regressor = LinearRegression()
273
+ regressor.fit(y.reshape(-1, 1), x)
274
+ # Get the slope and intercept of the fitted line
275
+ slope = regressor.coef_[0]
276
+ intercept = regressor.intercept_
277
+
278
+ if(is_horizontal):
279
+ intercept = np.mean(y)
280
+ actual_lines.append((slope,intercept))
281
+
282
+ return actual_lines
283
+
284
+ # Calculates difference between two consecutive elements in an array
285
+ def average_distance(arr):
286
+ n = len(arr)
287
+ distance_sum = 0
288
+
289
+ for i in range(n - 1):
290
+ distance_sum += abs(arr[i+1] - arr[i])
291
+
292
+ average = distance_sum / (n - 1)
293
+ return average
294
+
295
+ # If two adjacent lines are near than some threshold, then merge them
296
+ # Returns Results in y = mx + b from
297
+ def average_out_similar_lines(lines_m_c,lines_coord,del_threshold,is_horizontal=False):
298
+ averaged_lines = []
299
+ i = 0
300
+ while(i < len(lines_m_c) - 1):
301
+
302
+ _, intercept1 = lines_m_c[i]
303
+ _, intercept2 = lines_m_c[i + 1]
304
+
305
+ if abs(intercept2 - intercept1) < del_threshold:
306
+ new_points = np.array(lines_coord[i] + lines_coord[i+1][:-1])
307
+ # Separate the x and y coordinates
308
+ x = new_points[:, 0]
309
+ y = new_points[:, 1]
310
+
311
+ # Fit a linear regression model
312
+ regressor = LinearRegression()
313
+ regressor.fit(y.reshape(-1, 1), x)
314
+
315
+ # Get the slope and intercept of the fitted line
316
+ slope = regressor.coef_[0]
317
+ intercept = regressor.intercept_
318
+
319
+ if(is_horizontal):
320
+ intercept = np.mean(y)
321
+ averaged_lines.append((slope,intercept))
322
+ i+=2
323
+ else:
324
+ averaged_lines.append(lines_m_c[i])
325
+ i+=1
326
+ if(i < len(lines_m_c)):
327
+ averaged_lines.append(lines_m_c[i])
328
+
329
+ return averaged_lines
330
+
331
+ # If two adjacent lines are near than some threshold, then merge them
332
+ # Returns Results in normalized vector form
333
+ def average_out_similar_lines1(lines_m_c,lines_coord,del_threshold):
334
+ averaged_lines = []
335
+ i = 0
336
+ while(i < len(lines_m_c) - 1):
337
+
338
+ _, intercept1 = lines_m_c[i]
339
+ _, intercept2 = lines_m_c[i + 1]
340
+
341
+ if abs(intercept2 - intercept1) < del_threshold:
342
+ new_points = np.array(lines_coord[i] + lines_coord[i+1][:-1])
343
+ coordinates = np.array(new_points)
344
+ points = coordinates[:, None, :].astype(np.int32)
345
+ # Fit a line using linear regression
346
+ [vx, vy, x, y] = cv2.fitLine(points, cv2.DIST_L2, 0, 0.01, 0.01)
347
+ averaged_lines.append((vx, vy, x, y))
348
+ i+=2
349
+ else:
350
+ new_points = np.array(lines_coord[i])
351
+
352
+ coordinates = np.array(new_points)
353
+ points = coordinates[:, None, :].astype(np.int32)
354
+ # Fit a line using linear regression
355
+ [vx, vy, x, y] = cv2.fitLine(points, cv2.DIST_L2, 0, 0.01, 0.01)
356
+ averaged_lines.append((vx, vy, x, y))
357
+ i+=1
358
+ if(i < len(lines_m_c)):
359
+ new_points = np.array(lines_coord[i])
360
+ coordinates = np.array(new_points)
361
+ points = coordinates[:, None, :].astype(np.int32)
362
+ # Fit a line using linear regression
363
+ [vx, vy, x, y] = cv2.fitLine(points, cv2.DIST_L2, 0, 0.01, 0.01)
364
+ averaged_lines.append((vx, vy, x, y))
365
+
366
+ return averaged_lines
367
+
368
+ def get_square_color(image, box):
369
+
370
+ # Determine the size of the square region
371
+ square_size = (box[1][0] - box[0][0]) / 3
372
+
373
+ # Determine the coordinates of the square region inside the box
374
+ top_left = (box[0][0] + square_size, box[0][1] + square_size)
375
+ bottom_right = (box[0][0] + square_size*2, box[0][1] + square_size*2)
376
+
377
+ # Extract the square region from the image
378
+ square_region = image[int(top_left[1]):int(bottom_right[1]), int(top_left[0]):int(bottom_right[0])]
379
+
380
+ # Calculate the mean pixel value of the square region
381
+ mean_value = np.mean(square_region)
382
+
383
+ # Determine whether the square region is predominantly black or white
384
+ if mean_value < 128:
385
+ square_color = "."
386
+ else:
387
+ square_color = " "
388
+
389
+ return square_color
390
+
391
+ # accepts image in grayscale
392
+ def extract_grid(image):
393
+
394
+ # Apply Gaussian blur to reduce noise and improve edge detection
395
+ blurred = cv2.GaussianBlur(image, (3, 3), 0)
396
+ # Apply Canny edge detection
397
+ edges = cv2.Canny(blurred, 50, 150)
398
+
399
+ # Apply dilation to connect nearby edges and make them more contiguous
400
+ kernel = np.ones((5, 5), np.uint8)
401
+ dilated = cv2.dilate(edges, kernel, iterations=1)
402
+
403
+ # # Applying canny edge detector
404
+ # detecting contours on the canny image
405
+ contours, _ = cv2.findContours(dilated, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
406
+
407
+ # sorting the contours by the descending order area of the contour
408
+ sorted_contours = list(sorted(contours, key=cv2.contourArea,reverse=True))
409
+ # filtering out the top 10 largest by applying NMS and only selecting square ones (Apsect ratio 1)
410
+ filtered_contours = filter_contours(image, sorted_contours[0:10],iou_threshold=0.6,asp_ratio=1,tolerance=0.2)
411
+
412
+ # largest Contour Extraction
413
+ largest_contour = []
414
+ if(len(filtered_contours)):
415
+ largest_contour = filtered_contours[0]
416
+ else:
417
+ largest_contour = sorted_contours[0]
418
+
419
+ # --- Performing Perspective warp of the largest contour ---
420
+ coordinates_list = []
421
+
422
+ if(largest_contour.shape != (4,1,2)):
423
+ largest_contour = cv2.convexHull(largest_contour)
424
+ if(largest_contour.shape != (4,1,2)):
425
+ rect = cv2.minAreaRect(largest_contour)
426
+ largest_contour = cv2.boxPoints(rect)
427
+ largest_contour = largest_contour.astype('int')
428
+
429
+ coordinates_list = largest_contour.reshape(4, 2).tolist()
430
+
431
+ # Convert coordinates_list to a numpy array
432
+ coordinates_array = np.array(coordinates_list)
433
+
434
+ # Find the convex hull of the points
435
+ hull = cv2.convexHull(coordinates_array)
436
+
437
+ # Find the extreme points of the convex hull
438
+ extreme_points = np.squeeze(hull)
439
+
440
+ # Sort the extreme points by their x and y coordinates to determine the order
441
+ sorted_points = extreme_points[np.lexsort((extreme_points[:, 1], extreme_points[:, 0]))]
442
+
443
+ # Extract top left, bottom right, top right, and bottom left points
444
+ tl = sorted_points[0]
445
+ tr = sorted_points[1]
446
+ bl = sorted_points[2]
447
+ br = sorted_points[3]
448
+
449
+ if(tr[1] < tl[1]):
450
+ tl,tr = tr,tl
451
+ if(br[1] < bl[1]):
452
+ bl,br = br,bl
453
+
454
+ # Define pts1
455
+ pts1 = [tl, bl, tr, br]
456
+
457
+ # Calculate the bounding rectangle coordinates
458
+ x, y, w, h = 0,0,400,400
459
+ # Define pts2 as the corners of the bounding rectangle
460
+ pts2 = [[3, 3], [400, 3], [3, 400], [400, 400]]
461
+
462
+ # Calculate the perspective transformation matrix
463
+ matrix = cv2.getPerspectiveTransform(np.float32(pts1), np.float32(pts2))
464
+
465
+ # Apply the perspective transformation to the cropped_image
466
+ transformed_img = cv2.warpPerspective(image, matrix, (403, 403))
467
+ cropped_image = transformed_img.copy()
468
+
469
+ # if the largest contour was not exactly quadilateral
470
+
471
+ # -- Performing Hough Transform --
472
+
473
+ similarity_threshold = math.floor(w/30) # Thresholds for filtering Similar Hough Lines
474
+
475
+ # Applying Gaussian Blur to reduce noice and improve dege detection
476
+ blurred = cv2.GaussianBlur(cropped_image, (5, 5), 0)
477
+ # Perform Canny edge detection on the GrayScale Image
478
+ edges = cv2.Canny(blurred, 50, 150)
479
+ lines = cv2.HoughLines(edges, 1, np.pi/180, 200)
480
+
481
+ # Filter out similar lines
482
+ filtered_lines = []
483
+ for line in lines:
484
+ for r_theta in lines:
485
+ arr = np.array(r_theta[0], dtype=np.float64)
486
+ rho, theta = arr
487
+ is_similar = False
488
+ for filtered_line in filtered_lines:
489
+ filtered_rho, filtered_theta = filtered_line
490
+ # similarity threshold is 10
491
+ if abs(rho - filtered_rho) < similarity_threshold and abs(theta - filtered_theta) < np.pi/180 * similarity_threshold:
492
+ is_similar = True
493
+ break
494
+ if not is_similar:
495
+ filtered_lines.append((rho, theta))
496
+
497
+ # Filter out the horizontal and the vertical lines
498
+ horizontal_lines = []
499
+ vertical_lines = []
500
+ for rho, theta in filtered_lines:
501
+ a = np.cos(theta)
502
+ b = np.sin(theta)
503
+ x0 = a * rho
504
+ y0 = b * rho
505
+ x1 = int(x0 + 1000 * (-b))
506
+ y1 = int(y0 + 1000 * (a))
507
+ x2 = int(x0 - 1000 * (-b))
508
+ y2 = int(y0 - 1000 * (a))
509
+
510
+ slope = (y2 - y1) / (x2 - x1 + 0.0001)
511
+ # do taninv(0.17) it is nearly equal to 10
512
+ if( abs(slope) <= 0.18 ):
513
+ horizontal_lines.append((rho,theta))
514
+ elif (abs(slope) > 6):
515
+ vertical_lines.append((rho,theta))
516
+
517
+ # Find the intersection points of horizontal and vertical lines
518
+ hough_corners = []
519
+ for h_rho, h_theta in horizontal_lines:
520
+ for v_rho, v_theta in vertical_lines:
521
+ x, y = parametricIntersect(h_rho, h_theta, v_rho, v_theta)
522
+ if x is not None and y is not None:
523
+ hough_corners.append((x, y))
524
+
525
+ # -- Performing Harris Corner Detection --
526
+
527
+ # Create CLAHE object with specified clip limit
528
+ clahe = cv2.createCLAHE(clipLimit=3, tileGridSize=(8, 8))
529
+ clahe_image = clahe.apply(cropped_image)
530
+
531
+ # harris corner detection for CLHAE IMAGE
532
+ dst = cv2.cornerHarris(clahe_image,2,3,0.04)
533
+ ret,dst = cv2.threshold(dst,0.1*dst.max(),255,0)
534
+ dst = np.uint8(dst)
535
+ dst = cv2.dilate(dst,None)
536
+ ret, labels, stats, centroids = cv2.connectedComponentsWithStats(dst)
537
+ criteria = (cv2.TERM_CRITERIA_EPS+cv2.TermCriteria_MAX_ITER,100,0.001)
538
+ harris_corners = cv2.cornerSubPix(clahe_image,np.float32(centroids),(5,5),(-1,-1),criteria)
539
+
540
+ drawn_image = cv2.cvtColor(cropped_image, cv2.COLOR_GRAY2BGR)
541
+ for i in harris_corners:
542
+ x,y = i
543
+ image2 = cv2.circle(drawn_image, (int(x),int(y)), radius=0, color=(0, 0, 255), thickness=3)
544
+
545
+ # -- Using Regression Model to approximate horizontal and vertical Lines
546
+
547
+ # reducing to 0 decimal places
548
+ corners1 = list(map(lambda coord: (round(coord[0], 0), round(coord[1], 0)), harris_corners))
549
+
550
+ # adding the corners obtained from hough transform
551
+ corners1 += hough_corners
552
+
553
+ # removing the duplicate corners
554
+ corners_no_dup = list(set(corners1))
555
+
556
+ min_cell_width = w/30
557
+ min_cell_height = h/30
558
+
559
+ # grouping coordinates into probabale array that could fit a horizontal and vertical lien
560
+ vertical_lines = group_lines(corners_no_dup,0,min_cell_height)
561
+ horizontal_lines = group_lines(corners_no_dup,1,min_cell_height)
562
+
563
+ actual_vertical_lines = fit_lines(vertical_lines)
564
+ actual_horizontal_lines = fit_lines(horizontal_lines,is_horizontal=True)
565
+
566
+
567
+ # Lines obtained from above method are not appropriate, we have to refine them
568
+
569
+ x_probable = [i[1] for i in actual_horizontal_lines] # looking at the intercepts
570
+ y_probable = [i[1] for i in actual_vertical_lines]
571
+
572
+ del_x_avg = average_distance(x_probable)
573
+ del_y_avg = average_distance(y_probable)
574
+
575
+ averaged_horizontal_lines1 = [] # This step here is fishy and needs refinement
576
+ averaged_vertical_lines1 = []
577
+ multiplier = 0.95
578
+ i = 0
579
+ while(1):
580
+ averaged_horizontal_lines = average_out_similar_lines(actual_horizontal_lines,horizontal_lines,del_y_avg*multiplier,is_horizontal=True)
581
+ averaged_vertical_lines = average_out_similar_lines(actual_vertical_lines,vertical_lines,del_x_avg*multiplier,is_horizontal=False)
582
+ i += 1
583
+ if(i >= 20 or len(averaged_horizontal_lines) == len(averaged_vertical_lines)):
584
+ break
585
+ else:
586
+ multiplier -= 0.05
587
+
588
+ averaged_horizontal_lines1 = average_out_similar_lines1(actual_horizontal_lines,horizontal_lines,del_y_avg*multiplier)
589
+ averaged_vertical_lines1 = average_out_similar_lines1(actual_vertical_lines,vertical_lines,del_x_avg*multiplier)
590
+
591
+
592
+ # plotting the lines to image to find the intersection points
593
+ drawn_image6 = np.ones_like(cropped_image)*255
594
+ for vx,vy,cx,cy in averaged_horizontal_lines1 + averaged_vertical_lines1:
595
+ w = cropped_image.shape[1]
596
+ cv2.line(drawn_image6, (int(cx-vx*w), int(cy-vy*w)), (int(cx+vx*w), int(cy+vy*w)), (0, 0, 255),1,cv2.LINE_AA)
597
+
598
+ # -- Finding Intersection points --
599
+
600
+ # Applying Harris Corner Detection to find the intersection points
601
+ mesh_image = drawn_image6.copy()
602
+ dst = cv2.cornerHarris(mesh_image,2,3,0.04)
603
+
604
+ ret,dst = cv2.threshold(dst,0.1*dst.max(),255,0)
605
+ dst = np.uint8(dst)
606
+ dst = cv2.dilate(dst,None)
607
+ ret, labels, stats, centroids = cv2.connectedComponentsWithStats(dst)
608
+ criteria = (cv2.TERM_CRITERIA_EPS+cv2.TermCriteria_MAX_ITER,100,0.001)
609
+ harris_corners = cv2.cornerSubPix(mesh_image,np.float32(centroids),(5,5),(-1,-1),criteria)
610
+ drawn_image = cv2.cvtColor(drawn_image6, cv2.COLOR_GRAY2BGR)
611
+ harris_corners = list(sorted(harris_corners[1:],key = lambda x : x[1]))
612
+
613
+ # -- Finding out the grid color --
614
+
615
+
616
+ grayscale = cropped_image.copy()
617
+ # Perform adaptive thresholding to obtain binary image
618
+ _, binary = cv2.threshold(grayscale, 128, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
619
+
620
+ # Perform morphological operations to remove small text regions
621
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
622
+ binary = cv2.morphologyEx(binary, cv2.MORPH_ELLIPSE, kernel, iterations=1)
623
+
624
+ # Invert the binary image
625
+ inverted_binary = cv2.bitwise_not(binary)
626
+
627
+ # Restore the image by blending the inverted binary image with the grayscale image
628
+ restored_image = cv2.bitwise_or(inverted_binary, grayscale)
629
+
630
+ # Apply morphological opening to remove small black dots
631
+ kernel_opening = np.ones((3, 3), np.uint8)
632
+ opened_image = cv2.morphologyEx(restored_image, cv2.MORPH_OPEN, kernel_opening, iterations=1)
633
+
634
+ # Apply morphological closing to further refine the restored image
635
+ kernel_closing = np.ones((5, 5), np.uint8)
636
+ refined_image = cv2.morphologyEx(opened_image, cv2.MORPH_CLOSE, kernel_closing, iterations=1)
637
+
638
+ # finding out the grid corner
639
+ grid = []
640
+ grid_nums = []
641
+ across_clue_num = []
642
+ down_clue_num = []
643
+
644
+ sorted_corners = np.array(list(sorted(harris_corners,key=lambda x:x[1])))
645
+ if(len(sorted_corners) == len(averaged_horizontal_lines1) * len(averaged_vertical_lines1)):
646
+ sorted_corners_grouped = []
647
+ for i in range(0,len(sorted_corners),len(averaged_vertical_lines1)):
648
+ temp_arr = sorted_corners[i:i+len(averaged_vertical_lines1)]
649
+ temp_arr = list(sorted(temp_arr,key=lambda x: x[0]))
650
+ sorted_corners_grouped.append(temp_arr)
651
+
652
+ for h_line_idx in range(0,len(sorted_corners_grouped)-1):
653
+ for corner_idx in range(0,len(sorted_corners_grouped[h_line_idx])-1):
654
+ # grabbing the four box coordinates
655
+ box = [sorted_corners_grouped[h_line_idx][corner_idx],sorted_corners_grouped[h_line_idx][corner_idx+1],
656
+ sorted_corners_grouped[h_line_idx+1][corner_idx],sorted_corners_grouped[h_line_idx+1][corner_idx+1]]
657
+ grid.append(get_square_color(refined_image,box))
658
+
659
+ grid_formatted = []
660
+ for i in range(0, len(grid), len(averaged_vertical_lines1) - 1):
661
+ grid_formatted.append(grid[i:i + len(averaged_vertical_lines1) - 1])
662
+
663
+
664
+ # if (x,y) is present in these array the cell (x,y) is already accounted as a part of answer of across or down
665
+ in_horizontal = []
666
+ in_vertical = []
667
+
668
+ num = 0
669
+
670
+
671
+
672
+ for x in range(0, len(averaged_vertical_lines1) - 1):
673
+ for y in range(0, len(averaged_horizontal_lines1) - 1):
674
+
675
+ # if the cell is black there's no need to number
676
+ if grid_formatted[x][y] == '.':
677
+ grid_nums.append(0)
678
+ continue
679
+
680
+ # if the cell is part of both horizontal and vertical cell then there's no need to number
681
+ horizontal_presence = (x, y) in in_horizontal
682
+ vertical_presence = (x, y) in in_vertical
683
+
684
+ # present in both 1 1
685
+ if horizontal_presence and vertical_presence:
686
+ grid_nums.append(0)
687
+ continue
688
+
689
+ # present in one i.e 1 0
690
+ if not horizontal_presence and vertical_presence:
691
+ horizontal_length = 0
692
+ temp_horizontal_arr = []
693
+ # iterate in x direction until the end of the grid or until a black box is found
694
+ while x + horizontal_length < len(averaged_horizontal_lines1) - 1 and grid_formatted[x + horizontal_length][y] != '.':
695
+ temp_horizontal_arr.append((x + horizontal_length, y))
696
+ horizontal_length += 1
697
+ # if horizontal length is greater than 1, then append the temp_horizontal_arr to in_horizontal array
698
+ if horizontal_length > 1:
699
+ in_horizontal.extend(temp_horizontal_arr)
700
+ num += 1
701
+ across_clue_num.append(num)
702
+ grid_nums.append(num)
703
+ continue
704
+ grid_nums.append(0)
705
+ # present in one 1 0
706
+ if not vertical_presence and horizontal_presence:
707
+ # do the same for vertical
708
+ vertical_length = 0
709
+ temp_vertical_arr = []
710
+ # iterate in y direction until the end of the grid or until a black box is found
711
+ while y + vertical_length < len(averaged_vertical_lines1) - 1 and grid_formatted[x][y+vertical_length] != '.':
712
+ temp_vertical_arr.append((x, y+vertical_length))
713
+ vertical_length += 1
714
+ # if vertical length is greater than 1, then append the temp_vertical_arr to in_vertical array
715
+ if vertical_length > 1:
716
+ in_vertical.extend(temp_vertical_arr)
717
+ num += 1
718
+ down_clue_num.append(num)
719
+ grid_nums.append(num)
720
+ continue
721
+ grid_nums.append(0)
722
+
723
+ if(not horizontal_presence and not vertical_presence):
724
+
725
+ horizontal_length = 0
726
+ temp_horizontal_arr = []
727
+ # iterate in x direction until the end of the grid or until a black box is found
728
+ while x + horizontal_length < len(averaged_horizontal_lines1) - 1 and grid_formatted[x + horizontal_length][y] != '.':
729
+ temp_horizontal_arr.append((x + horizontal_length, y))
730
+ horizontal_length += 1
731
+ # if horizontal length is greater than 1, then append the temp_horizontal_arr to in_horizontal array
732
+
733
+ # do the same for vertical
734
+ vertical_length = 0
735
+ temp_vertical_arr = []
736
+ # iterate in y direction until the end of the grid or until a black box is found
737
+ while y + vertical_length < len(averaged_vertical_lines1) - 1 and grid_formatted[x][y+vertical_length] != '.':
738
+ temp_vertical_arr.append((x, y+vertical_length))
739
+ vertical_length += 1
740
+ # if vertical length is greater than 1, then append the temp_vertical_arr to in_vertical array
741
+
742
+ if horizontal_length > 1 and horizontal_length > 1:
743
+ in_horizontal.extend(temp_horizontal_arr)
744
+ in_vertical.extend(temp_vertical_arr)
745
+ num += 1
746
+ across_clue_num.append(num)
747
+ down_clue_num.append(num)
748
+ grid_nums.append(num)
749
+ elif vertical_length > 1:
750
+ in_vertical.extend(temp_vertical_arr)
751
+ num += 1
752
+ down_clue_num.append(num)
753
+ grid_nums.append(num)
754
+ elif horizontal_length > 1:
755
+ in_horizontal.extend(temp_horizontal_arr)
756
+ num += 1
757
+ across_clue_num.append(num)
758
+ grid_nums.append(num)
759
+ else:
760
+ grid_nums.append(0)
761
+
762
+
763
+ size = { 'rows' : len(averaged_horizontal_lines1)-1,
764
+ 'cols' : len(averaged_vertical_lines1)-1,
765
+ }
766
+
767
+ dict = {
768
+ 'size' : size,
769
+ 'grid' : grid,
770
+ 'gridnums': grid_nums,
771
+ 'across_nums': down_clue_num,
772
+ 'down_nums' : across_clue_num,
773
+ 'clues':{
774
+ 'across' : [],
775
+ 'down': []
776
+ }
777
+ }
778
+
779
+ return dict
780
+
781
+ if __name__ == "__main__":
782
+ img = cv2.imread("D:\\D\\Major Project files\\opencv\\movie.png",0)
783
+ down = extract_grid(img)
784
+ print(down)
785
+ # img = Image.open("chalena3.jpg")
786
+ # img_gray = img.convert("L")
787
+ # print(extract_grid(img_gray))
main.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI,UploadFile,File,status,HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ import aiofiles
4
+ import os
5
+ import cv2
6
+ from extractpuzzle import extract_grid,get_text
7
+
8
+
9
+ app = FastAPI()
10
+ # for reading images in chunk
11
+ CHUNK_SIZE = 1024 * 1024 * 2
12
+
13
+ app.add_middleware(
14
+ CORSMiddleware,
15
+ allow_origins=["*"],
16
+ allow_methods=["*"],
17
+ allow_headers=["*"],
18
+ allow_credentials=True,
19
+ )
20
+
21
+ @app.get("/")
22
+ async def index():
23
+ return {"message": "Hello World"}
24
+
25
+
26
+ @app.post("/parseImage/")
27
+ async def upload(file: UploadFile = File(...)):
28
+
29
+ try:
30
+ filepath = os.path.join('./', os.path.basename(file.filename))
31
+ async with aiofiles.open(filepath, 'wb') as f:
32
+ while chunk := await file.read(CHUNK_SIZE):
33
+ await f.write(chunk)
34
+ except Exception:
35
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
36
+ detail='There was an error uploading the file')
37
+ finally:
38
+ await file.close()
39
+
40
+ img_array = cv2.imread(filepath,0)
41
+
42
+ grid_data = {}
43
+ clue_data = {}
44
+
45
+
46
+ try: # try extracting the grid from the image
47
+ # dict = { 'size' : size, 'grid' : grid, 'gridnums': grid_nums, 'across_nums': down_clue_num,'down_nums' : across_clue_num }
48
+ grid_data = extract_grid(img_array)
49
+ grid_data['gridExtractionStatus'] = "Passed"
50
+ except Exception as e:
51
+ grid_data['gridExtractionStatus'] = "Failed"
52
+
53
+
54
+ try: # try extracting clues
55
+ acrossClues, downClues = get_text(img_array) # { number : [column_of_projection_profile,extracted_text]}
56
+ clue_data['across'] = acrossClues
57
+ clue_data['down'] = downClues
58
+ clue_data['gridExtractionStatus'] = "Passed"
59
+ except Exception as e:
60
+ grid_data['ClueExtractionStatus'] = "Failed"
61
+
62
+ grid_data.update(clue_data)
63
+
64
+ return grid_data
65
+
66
+
67
+
68
+
69
+
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi== 0.104.1
2
+ uvicorn[standard]
3
+ numpy==1.26.2
4
+ scipy==1.11.4
5
+ aiofiles==23.2.1
6
+ python-multipart
7
+ opencv-python-headless==4.6.0.66
8
+ pytesseract==0.3.10
9
+ scikit-learn==1.3.2
10
+
11
+
12
+