Spaces:
Sleeping
Sleeping
File size: 11,833 Bytes
0d3476b |
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 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 |
from flask import Flask, request, jsonify
import base64
import io
from PIL import Image
import cv2
import numpy as np
import easyocr
import logging
from routes import app
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize EasyOCR Reader
reader = easyocr.Reader(['en'], gpu=False)
def run_length_decode(encoded_bytes):
if len(encoded_bytes) % 2 != 0:
logger.error("Run-length encoded data length is not even.")
raise ValueError("Run-length encoded data length is not even.")
decoded = bytearray()
for i in range(0, len(encoded_bytes), 2):
count = encoded_bytes[i]
byte = encoded_bytes[i + 1]
decoded.extend([byte] * count)
return bytes(decoded)
def remove_green_lines(cv_image):
hsv = cv2.cvtColor(cv_image, cv2.COLOR_BGR2HSV)
lower_green = np.array([40, 40, 40])
upper_green = np.array([80, 255, 255])
mask = cv2.inRange(hsv, lower_green, upper_green)
mask_inv = cv2.bitwise_not(mask)
img_no_green = cv2.bitwise_and(cv_image, cv_image, mask=mask_inv)
return img_no_green
def preprocess_image(cv_image):
gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (7, 7), 0)
thresh = cv2.adaptiveThreshold(
blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, blockSize=15, C=3)
thresh = cv2.bitwise_not(thresh)
kernel = np.ones((5,5), np.uint8)
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
return thresh
def extract_sudoku_board(image, emptyCells, image_size):
cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
cv_image = remove_green_lines(cv_image)
thresh = preprocess_image(cv_image)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
raise ValueError("No contours found in image")
largest_contour = max(contours, key=cv2.contourArea)
peri = cv2.arcLength(largest_contour, True)
epsilon = 0.01 * peri
approx = cv2.approxPolyDP(largest_contour, epsilon, True)
if len(approx) != 4:
raise ValueError("Sudoku grid not found (did not find 4 corners)")
pts = approx.reshape(4, 2)
rect = np.zeros((4, 2), dtype="float32")
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
(tl, tr, br, bl) = rect
widthA = np.linalg.norm(br - bl)
widthB = np.linalg.norm(tr - tl)
maxWidth = max(int(widthA), int(widthB))
heightA = np.linalg.norm(tr - br)
heightB = np.linalg.norm(tl - bl)
maxHeight = max(int(heightA), int(heightB))
dst = np.array([
[0, 0],
[maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1],
[0, maxHeight - 1]
], dtype="float32")
M = cv2.getPerspectiveTransform(rect, dst)
warp = cv2.warpPerspective(cv_image, M, (maxWidth, maxHeight))
# Determine Sudoku size based on image dimensions and emptyCells
size = 4 # Default to 4x4
if image_size > 40000:
if image_size < 100000:
size = 9
else:
size = 16
# Secondary check based on emptyCells
empty_cell_check = any(cell.get('x', 0) > 9 or cell.get('y', 0) > 9 for cell in emptyCells)
if empty_cell_check:
size = 16
# Calculate cell size
cell_size = maxWidth // size
# Resize the warped grid to the expected size
expected_size = size * cell_size
warp = cv2.resize(warp, (expected_size, expected_size))
# Convert warped image to grayscale and threshold
warp_gray = cv2.cvtColor(warp, cv2.COLOR_BGR2GRAY)
warp_blurred = cv2.GaussianBlur(warp_gray, (5, 5), 0)
warp_thresh = cv2.adaptiveThreshold(
warp_blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)
warp_thresh = cv2.bitwise_not(warp_thresh)
board = []
for y in range(size):
row = []
for x in range(size):
start_x = x * cell_size
start_y = y * cell_size
end_x = (x + 1) * cell_size
end_y = (y + 1) * cell_size
cell = warp_thresh[start_y:end_y, start_x:end_x]
digit = recognize_digit(cell, size)
row.append(digit)
board.append(row)
return board, size
def count_connected_black_pixels(image, min_pixels=14):
# Label the connected components in the binary image
image = cv2.resize(image, (100, 100))
num_labels, labels_im = cv2.connectedComponents(image, connectivity=8)
# Save Image for inspection in img folder with randome name
# cv2.imwrite("img/img" + str(np.random.randint(0, 1000)) + ".jpg", image)
# Subtract 1 to ignore the background label (label=0 is the background)
valid_components = 0
# Iterate over all the labels and count those with at least min_pixels pixels
for label in range(0, num_labels):
if np.sum(labels_im == label) >= min_pixels:
valid_components += 1
valid_components = valid_components - 2
if valid_components < 0:
valid_components = 0
return valid_components
# Test the function on a preprocessed image
def recognize_digit(cell_image, slots = 4):
"""
Recognizes the digit in a Sudoku cell. If no digit is detected, it counts black pixel groups (dots).
"""
# Preprocess cell image for digit detection
if slots == 4:
cell_resized = cv2.resize(cell_image, (100, 100))
elif slots == 9:
cell_resized = cv2.resize(cell_image, (300, 300))
else:
cell_resized = cv2.resize(cell_image, (500, 500))
cell_padded = cv2.copyMakeBorder(
cell_resized, 10, 10, 10, 10, cv2.BORDER_CONSTANT, value=255)
cell_rgb = cv2.cvtColor(cell_padded, cv2.COLOR_GRAY2RGB)
# Use EasyOCR to detect text
result = reader.readtext(cell_rgb, detail=0, allowlist='0123456789')
if result:
digit = max(result, key=len)
if digit.isdigit():
return int(digit)
# If no digit is detected, count connected black pixel components (dots)
logger.info("No digit detected. Counting black pixel groups.")
# Save Image for inspection
dots_count = count_connected_black_pixels(cell_padded)
logger.info(f"Detected {dots_count} connected black pixel groups (dots).")
return dots_count if dots_count > 0 else 0 # Default to 0 if nothing is recognized
def is_valid_general(board, row, col, num, size):
# Check row and column
for i in range(size):
if board[row][i] == num or board[i][col] == num:
return False
# Check subgrid
subgrid_size = int(np.sqrt(size))
start_row, start_col = subgrid_size * (row // subgrid_size), subgrid_size * (col // subgrid_size)
for y in range(start_row, start_row + subgrid_size):
for x in range(start_col, start_col + subgrid_size):
if board[y][x] == num:
return False
return True
def solve_sudoku_general(board, size):
for row in range(size):
for col in range(size):
if board[row][col] == 0:
for num in range(1, size + 1):
if is_valid_general(board, row, col, num, size):
board[row][col] = num
if solve_sudoku_general(board, size):
return True
board[row][col] = 0
return False
return True
def calculate_sum(board, empty_cells):
total = 0
for cell in empty_cells:
x = cell.get('x')
y = cell.get('y')
# Validate the coordinates
if x is None or y is None:
raise ValueError(f"Empty cell coordinates missing: {cell}")
if not (0 <= y < len(board)) or not (0 <= x < len(board[0])):
raise ValueError(f"Cell location out of bounds: x={x}, y={y}")
total += board[y][x]
return total
@app.route('/sudoku', methods=['POST'])
def sudoku_endpoint():
payload = request.json
logger.info("Received payload: %s", payload)
# Extract fields
sudoku_id = payload.get('id')
encoded_str = payload.get('encoded')
img_length = payload.get('imgLength')
empty_cells = payload.get('emptyCells')
# Validate required fields
if not all([sudoku_id, encoded_str, img_length, empty_cells]):
missing = [field for field in ['id', 'encoded', 'imgLength', 'emptyCells'] if not payload.get(field)]
logger.error(f"Missing required fields: {missing}")
return jsonify({"error": f"Missing required fields: {missing}"}), 400
# Base64 Decode First
try:
run_length_encoded_bytes = base64.b64decode(encoded_str)
logger.info("Base64 decoding successful.")
except Exception as e:
logger.error(f"Base64 decoding failed: {e}")
return jsonify({"error": f"Base64 decoding failed: {str(e)}"}), 400
# Run-Length Decode Second
try:
image_bytes = run_length_decode(run_length_encoded_bytes)
logger.info("Run-length decoding successful.")
except Exception as e:
logger.error(f"Run-length decoding failed: {e}")
return jsonify({"error": f"Run-length decoding failed: {str(e)}"}), 400
# Verify imgLength
if len(image_bytes) != img_length:
logger.error(f"Decoded image length mismatch: expected {img_length}, got {len(image_bytes)}")
return jsonify({"error": f"Decoded image length mismatch: expected {img_length}, got {len(image_bytes)}"}), 400
# Open image using PIL
try:
image = Image.open(io.BytesIO(image_bytes))
# Save Image for inspection
image.save("image_main.jpg")
logger.info("Image opened successfully.")
except Exception as e:
logger.error(f"Failed to open image: {e}")
return jsonify({"error": f"Failed to open image: {str(e)}"}), 400
# Extract Sudoku board
try:
board, size = extract_sudoku_board(image, empty_cells, img_length)
logger.info(f"Sudoku board extracted successfully. Size: {size}x{size}")
except Exception as e:
logger.error(f"Failed to extract Sudoku board: {e}")
return jsonify({"error": f"Failed to extract Sudoku board: {str(e)}"}), 400
# Solve Sudoku
try:
logger.info("Sudoku board before solving:")
for row in board:
logger.info(row)
solved = solve_sudoku_general(board, size)
if not solved:
logger.error("Sudoku cannot be solved.")
return jsonify({"error": "Sudoku cannot be solved"}), 400
logger.info("Sudoku solved successfully.")
except Exception as e:
logger.error(f"Sudoku solving failed: {e}")
return jsonify({"error": f"Sudoku solving failed: {str(e)}"}), 400
# Calculate sum
try:
total_sum = calculate_sum(board, empty_cells)
logger.info(f"Sum of specified cells: {total_sum}")
except Exception as e:
logger.error(f"Sum calculation failed: {e}")
return jsonify({"error": f"Sum calculation failed: {str(e)}"}), 400
# Prepare response
response = {
"answer": board,
"sum": total_sum
}
logger.info(f"Response prepared successfully for Sudoku ID: {sudoku_id}")
return jsonify(response), 200 |