UBS-Server / routes /sudoku.py
m-abdur2024's picture
Update routes/sudoku.py
9b1162f verified
raw
history blame contribute delete
No virus
9.12 kB
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.")
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)
largest_contour = max(contours, key=cv2.contourArea)
peri = cv2.arcLength(largest_contour, True)
epsilon = 0.01 * peri
approx = cv2.approxPolyDP(largest_contour, epsilon, True)
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
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')
# Base64 Decode First
run_length_encoded_bytes = base64.b64decode(encoded_str)
logger.info("Base64 decoding successful.")
# Run-Length Decode Second
image_bytes = run_length_decode(run_length_encoded_bytes)
logger.info("Run-length decoding successful.")
# Open image using PIL
image = Image.open(io.BytesIO(image_bytes))
# Save Image for inspection
# image.save("image_main.jpg")
logger.info("Image opened successfully.")
# Extract Sudoku board
board, size = extract_sudoku_board(image, empty_cells, img_length)
logger.info(f"Sudoku board extracted successfully. Size: {size}x{size}")
# Solve Sudoku
logger.info("Sudoku board before solving:")
for row in board:
logger.info(row)
solved = solve_sudoku_general(board, size)
logger.info("Sudoku solved successfully.")
total_sum = calculate_sum(board, empty_cells)
logger.info(f"Sum of specified cells: {total_sum}")
# Prepare response
response = {
"answer": board,
"sum": total_sum
}
logger.info(f"Response prepared successfully for Sudoku ID: {sudoku_id}")
return jsonify(response)