Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Included a sudoku solver script. #398

Merged
merged 1 commit into from
Sep 9, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions MachineLearning Projects/sudoku_solver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Sudoku Solver

* This app was built to allow users to solve their sudokus using a computer.
* There is a Flask based webserver `web_interface.py` which when run gives a web interface to upload an image of a sudoku to be solved. The response is a solved sudoku.
* There is a file `full_stack_http.py` which needs to be run alongside the webserver for the full app to run. This is in charge of opening multiple process channels to process the images that are sent to the webserver.
* The app relies of Pytesseract to identify the characters in the sudoku image.

# Operation

* The image is first stripped of color.
* It is then cropped to select the section of the sudoku. NOTE: This section is not dependent on the sudoku but has been hardcoded.
* The resulting image is passed to `Pytesseract` to extract the characters and their position.
* Using the characters and their position the grid size is determined.
* The appropriate grid is created and filled with the discovered characters.
* The grid is then solved with an algorithm contained in `sudoku.py`.
* A snapshot of the solved grid is then created and sent back to the user.
* The resultant snapshot is rendered on the browser page.

# To Run

* First install `Pytesseract`
* Install `Flask`
* Then run the `full_stack_http.py` file.
* Then run the `web_interface.py` file.
* Go to the browser and load the URL provided in the previous step.
* Click the upload button.
* Select your image and submit the form.
* Wait for the result to be loaded.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4 changes: 4 additions & 0 deletions MachineLearning Projects/sudoku_solver/config.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
UPLOAD_FOLDER="uploads"
SECRET_KEY="secret"
SOLVER_IP="localhost"
SOLVER_PORT=3535
Binary file added MachineLearning Projects/sudoku_solver/f1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added MachineLearning Projects/sudoku_solver/f2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
136 changes: 136 additions & 0 deletions MachineLearning Projects/sudoku_solver/full_stack_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import multiprocessing.util
import socket
from perspective import resolve_image
from sudoku import Grid
import argparse
import multiprocessing
import os

temp_result_file = "resultfile.png"
temp_input_file = "tempfile.jpg"

def process_handle_transaction(proc_num:int, sock:socket.socket):
print(f"[{proc_num}] Waiting for client...")
sock2, address2 = sock.accept()
print(f"[{proc_num}] Connected to client with address: {address2}")
sock2.settimeout(1)
rec_buf = b''
split = temp_input_file.split('.')
my_temp_input_file = ".".join(i for i in split[:-1]) + str(proc_num) + "." + split[-1]
split = temp_result_file.split('.')
my_temp_result_file = ".".join(i for i in split[:-1]) + str(proc_num) + "." + split[-1]
try:
while True:
try:
rec = sock2.recv(1)
rec_buf += rec
if len(rec) == 0:
print(f"[{proc_num}] Lost connection")
break
except socket.timeout:
with open(my_temp_input_file, "wb") as f:
f.write(rec_buf)
rec_buf = b''
grid_size, points = resolve_image(my_temp_input_file)
grid = Grid(rows=grid_size[0], columns=grid_size[1])
assignment_values = {}
for val,loc in points:
assignment_values[loc] = val
grid.preassign(assignment_values)
grid.solve()
grid.save_grid_image(path=my_temp_result_file, size=(400,400))
with open(my_temp_result_file, "rb") as f:
sock2.send(f.read())
os.remove(my_temp_input_file)
os.remove(my_temp_result_file)
sock2.close()
print(f"[{proc_num}] Finished!")
break
finally:
sock2.close()

class Manager():
def __init__(self, address:tuple[str,int]):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.address = address

def wait_for_connect(self):
print("Waiting for client...")
self.sock2, self.address2 = self.sock.accept()
print(f"Connected to client with address: {self.address2}")
self.sock2.settimeout(1)

def run(self):
self.sock.bind(self.address)
self.sock.listen()
print(f"Listening from address: {self.address}")
try:
while True:
self.wait_for_connect()
rec_buf = b''
while True:
try:
rec = self.sock2.recv(1)
rec_buf += rec
if len(rec) == 0:
print("Lost connection")
break
except socket.timeout:
with open(temp_input_file, "wb") as f:
f.write(rec_buf)
rec_buf = b''
grid_size, points = resolve_image(temp_input_file)
grid = Grid(rows=grid_size[0], columns=grid_size[1])
assignment_values = {}
for val,loc in points:
assignment_values[loc] = val
grid.preassign(assignment_values)
grid.solve()
grid.save_grid_image(path=temp_result_file, size=(400,400))
with open(temp_result_file, "rb") as f:
self.sock2.send(f.read())
os.remove(temp_input_file)
os.remove(temp_result_file)
self.sock2.close()
break
finally:
try:
self.sock2.close()
except socket.error:
pass
except AttributeError:
pass
self.sock.close()

def run_multiprocessing(self, max_clients:int=8):
self.sock.bind(self.address)
self.sock.listen()
print(f"Listening from address: {self.address}")
processes:dict[int,multiprocessing.Process]= {}
proc_num = 0
try:
while True:
if len(processes) <= max_clients:
proc = multiprocessing.Process(target=process_handle_transaction, args=(proc_num, self.sock))
proc.start()
processes[proc_num] = proc
proc_num += 1
proc_num%=(max_clients*2)
keys = list(processes.keys())
for proc_n in keys:
if not processes[proc_n].is_alive():
processes.pop(proc_n)
finally:
if len(processes):
for proc in processes.values():
proc.kill()
self.sock.close()

if "__main__" == __name__:
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=3535, help="The port to host the server.")
parser.add_argument("--host", type=str, default="localhost", help="The host or ip-address to host the server.")
args = parser.parse_args()
address = (args.host, args.port)
manager = Manager(address)
manager.run_multiprocessing(max_clients=multiprocessing.cpu_count())
141 changes: 141 additions & 0 deletions MachineLearning Projects/sudoku_solver/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import torch
from torch.utils.data import Dataset, DataLoader
import PIL.Image as Image
import pandas as pd
from tqdm import tqdm
import numpy as np


class SudokuDataset(Dataset):
def __init__(self, grid_locations_file:str, input_shape:tuple[int, int]) -> None:
super().__init__()
self.grid_locations = []
self.image_filenames = []
self.input_shape = input_shape
self.all_data = pd.read_csv(grid_locations_file, header=0)
self.image_filenames = list(self.all_data['filepath'].to_numpy())
self.grid_locations = [list(a[1:]) for a in self.all_data.values]
to_pop = []
for i,file in enumerate(self.image_filenames):
try:
Image.open(file)
except FileNotFoundError:
to_pop.append(i)
print(f"{file} not found.")
for i in reversed(to_pop):
self.image_filenames.pop(i)
self.grid_locations.pop(i)
# print(self.all_data.columns)
# print(self.grid_locations)

def __len__(self) -> int:
return len(self.image_filenames)

def __getitem__(self, index) -> dict[str, torch.Tensor]:
image = Image.open(self.image_filenames[index]).convert("L")
size = image.size
image = image.resize(self.input_shape)
image = np.array(image)
image = image.reshape((1,*image.shape))
location = self.grid_locations[index]
for i in range(len(location)):
if i%2:
location[i] /= size[1]
else:
location[i] /= size[0]
return {
"image": torch.tensor(image, dtype=torch.float32)/255.,
"grid": torch.tensor(location, dtype=torch.float32)
}

class Model(torch.nn.Module):
def __init__(self, input_shape:tuple[int,int], number_of_layers:int, dims:int, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.input_shape = input_shape
self.conv_layers:list = []
self.conv_layers.append(torch.nn.Conv2d(1, dims, (3,3), padding='same'))
for _ in range(number_of_layers-1):
self.conv_layers.append(torch.nn.Conv2d(dims, dims, (3,3), padding='same'))
self.conv_layers.append(torch.nn.LeakyReLU(negative_slope=0.01))
self.conv_layers.append(torch.nn.MaxPool2d((2,2)))
self.conv_layers.append(torch.nn.BatchNorm2d(dims))
self.flatten = torch.nn.Flatten()
self.location = [
torch.nn.Linear(4107, 8),
torch.nn.Sigmoid()
]
self.conv_layers = torch.nn.ModuleList(self.conv_layers)
self.location = torch.nn.ModuleList(self.location)

def forward(self, x:torch.Tensor) -> torch.Tensor:
for layer in self.conv_layers:
x = layer(x)
x = self.flatten(x)
location = x
for layer in self.location:
location = layer(location)
return location

def create_model(input_shape:tuple[int,int], number_of_layers:int, dims:int):
model = Model(input_shape, number_of_layers, dims)
for p in model.parameters():
if p.dim() > 1:
torch.nn.init.xavier_uniform_(p)
return model

def get_dataset(filename:str, input_shape:tuple[int,int], batch_size:int) -> DataLoader:
train_dataset = SudokuDataset(filename, input_shape)
train_dataloader = DataLoader(train_dataset, batch_size, shuffle=True)
return train_dataloader

def train(epochs:int, config:dict, model:None|Model = None) -> Model:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
if not model:
print("========== Using new model =========")
model = create_model(config['input_shape'], config['number_of_layers'], config['dims']).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=config['lr'])
loss = torch.nn.MSELoss().to(device)
dataset = get_dataset(config['filename'], config['input_shape'], config['batch_size'])
prev_error = 0
try:
for epoch in range(1, epochs+1):
batch_iterator = tqdm(dataset, f"Epoch {epoch}/{epochs}:")
for batch in batch_iterator:
x = batch['image'].to(device)
y_true = batch['grid'].to(device)
# print(batch['grid'])
# return
y_pred = model(x)
error = loss(y_true, y_pred)
batch_iterator.set_postfix({"loss":f"Loss: {error.item():6.6f}"})
error.backward()
optimizer.step()
# optimizer.zero_grad()
if abs(error-0.5) < 0.05:# or (prev_error-error)<0.000001:
del(model)
model = create_model(config['input_shape'], config['number_of_layers'], config['dims']).to(device)
print("New model created")
prev_error = error
except KeyboardInterrupt:
torch.save(model, "model.pt")
return model

def test(config:dict, model_filename:str):
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = torch.load("model.pt").to(device)
loss = torch.nn.MSELoss().to(device)
dataset = get_dataset(config['filename'], config['input_shape'], config['batch_size'])


if __name__ == '__main__':
config = {
"input_shape": (300,300),
"filename": "archive/outlines_sorted.csv",
"number_of_layers": 4,
"dims": 3,
"batch_size": 8,
"lr": 1e-5
}
# model = train(50, config)
model = torch.load("model.pt")
test(config, model)
Binary file added MachineLearning Projects/sudoku_solver/model.pt
Binary file not shown.
98 changes: 98 additions & 0 deletions MachineLearning Projects/sudoku_solver/perspective.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import cv2
import numpy as np
from pytesseract import pytesseract as pt

def resolve_perspective(source_image:np.ndarray, points:np.ndarray, target_shape:tuple[int,int]) -> np.ndarray:
"""Takes an source image and transforms takes the region demarkated by points and creates a rectangular image of target.
Args:
source_image (np.ndarray): the source image.
points (np.ndarray): a numpy array of 4 points that will demarkate the vertices of the region to be transformed.\n
\tShould be in the form of points from the point that would be transformed to the top left of the rectangle, clockwise
target_shape (tuple[int,int]): the target shape of the rectangular output image. Format [height, width].
Returns:
np.ndarray: the output image transformed
"""
output_points:np.ndarray = np.array([
[0,0],
[target_shape[0]-1, 0],
[target_shape[0]-1, target_shape[1]-1],
[0,target_shape[1]-1]
], dtype=np.float32)
transformation_matrix:cv2.typing.MatLike = cv2.getPerspectiveTransform(points.astype(np.float32), output_points)
output:cv2.typing.MatLike = cv2.warpPerspective(source_image, transformation_matrix, (target_shape[1], target_shape[0]), flags=cv2.INTER_LINEAR)
return output

def get_grid_size(image:np.ndarray, boxes:list[list[int]], allowed_sizes:list[tuple[int,int]]=[(2,3),(3,3),(4,4)]) -> tuple[int,int]:
h,w = image.shape
for size in allowed_sizes:
s1 = float(w)/float(size[0])
s2 = float(h)/float(size[1])
for box in boxes:
_,x1,y1,x2,y2 = box
if (abs(int(x1/s1) - int(x2/s1)) + abs(int((h - y1)/s2) - int((h - y2)/s2))) > 0:
break
else:
return size

def get_points(image:np.ndarray, boxes:list[list[int]], grid_size:tuple[int,int]) -> list[tuple[int,tuple]]:
h,w = image.shape
size = grid_size[0] * grid_size[1]
s1 = float(w)/float(size)
s2 = float(h)/float(size)
results = []
for box in boxes:
val,x1,y1,x2,y2 = box
center_x = int((x1+x2)/2)
center_y = int((y1+y2)/2)
results.append((val, (int((h-center_y)/s2), int(center_x/s1))))
return results

def resolve_image(path:str) -> tuple[tuple,list[tuple[int,tuple]]]:
# img = cv2.imread("images/image210.jpg")
img = cv2.imread(path)
numbers = [str(i) for i in range(10)]
max_size = 500
min_area = 150
*img_shape,_ = img.shape
max_ind = np.argmax(img_shape)
min_ind = np.argmin(img_shape)
next_shape = [0,0]
if max_ind != min_ind:
next_shape[max_ind] = max_size
next_shape[min_ind] = int(img_shape[min_ind]*max_size/img_shape[max_ind])
else:
next_shape = [max_size, max_size]
img = cv2.resize(img, tuple(reversed(next_shape)))
points = np.array([6,97,219,99,216,309,7,310])
points = points.reshape((4,2))
target_shape = (400,400)
output = resolve_perspective(img, points, target_shape)
output = cv2.cvtColor(output, cv2.COLOR_BGR2GRAY)
norm_img = np.zeros((output.shape[0], output.shape[1]))
output = cv2.normalize(output, norm_img, 0, 255, cv2.NORM_MINMAX)
output1 = cv2.threshold(output, 140, 255, cv2.THRESH_BINARY_INV)[1]
if np.average(output1.flatten()) > 128:
output = cv2.threshold(output, 140, 255, cv2.THRESH_BINARY)[1]
else:
output = output1
output = cv2.GaussianBlur(output, (1,1), 0)
boxes = pt.image_to_boxes(output, "eng", config=r'-c tessedit_char_whitelist=0123456789 --psm 13 --oem 3')
print(boxes)
h,w = output.shape
new_boxes_str = ""
new_boxes = []
for bt in boxes.splitlines():
b = bt.split(' ')
area = (int(b[1]) - int(b[3]))*(int(b[2]) - int(b[4]))
if b[0] in numbers and area > min_area:
output = cv2.rectangle(output, (int(b[1]), h - int(b[2])), (int(b[3]), h - int(b[4])), (255, 255, 255), 2)
new_boxes_str += bt + "\n"
new_boxes.append(list(int(i) for i in b[:5]))
grid_size = get_grid_size(output, new_boxes)
final_points = get_points(output, new_boxes, grid_size)
return grid_size,final_points

if "__main__" == __name__:
print(resolve_image("f2.jpg"))
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
373 changes: 373 additions & 0 deletions MachineLearning Projects/sudoku_solver/sudoku.py

Large diffs are not rendered by default.

230 changes: 230 additions & 0 deletions MachineLearning Projects/sudoku_solver/temp.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [],
"source": [
"class Node:\n",
" def __init__(self,val):\n",
" self.val = val\n",
" self.to = {}"
]
},
{
"cell_type": "code",
"execution_count": 137,
"metadata": {},
"outputs": [],
"source": [
"class Node:\n",
" def __init__(self,val):\n",
" self.val:int = val\n",
" self.to:dict[Node,tuple[int,int]] = {} # destinationNode:(steps,price)\n",
" \n",
" def __str__(self) -> str:\n",
" children = ','.join(str(i.val) for i in self.to.keys())\n",
" return f\"Node({self.val})\"\n",
" \n",
" def __repr__(self) -> str:\n",
" children = ','.join(str(i.val) for i in self.to.keys())\n",
" return f\"Node({self.val})\"\n",
" \n",
" def full(self) -> str:\n",
" children = ','.join(str(i.val) for i in self.to.keys())\n",
" return f\"Node({self.val})->[{children}]\"\n",
"\n",
"def update(node:Node, start:list[int]):\n",
" # print(\"iter\", node, start)\n",
" if node.val in start:\n",
" # print(\"found: \", node, \" => \", start)\n",
" return {}\n",
" ret:dict[Node,set[tuple[int,int]]] = {\n",
" i:set([tuple(node.to[i]),]) for i in node.to.keys()\n",
" } # destinationNode:[(steps1,price1), (steps2,price2), ...]\n",
" for destinationNode,(steps,price) in node.to.items():\n",
" # print(f\"step {node} to {destinationNode}\")\n",
" returned = update(destinationNode, [*start,node.val])\n",
" # print(f\"{node.val} going to {destinationNode.val} got {returned}\")\n",
" if returned == {}:\n",
" # print(f\"here on\")\n",
" ret[destinationNode].add((steps,price))\n",
" continue\n",
" for v,mylist in returned.items():\n",
" # v is the a possible destination from our destination node\n",
" # my list is a list of the steps and prices to that possible destination\n",
" for (stp,prc) in mylist:\n",
" newTuple = (stp+steps,prc+price)\n",
" if ret.get(v):\n",
" ret[v].add(newTuple)\n",
" else:\n",
" ret[v] = set([newTuple,])\n",
" return ret"
]
},
{
"cell_type": "code",
"execution_count": 176,
"metadata": {},
"outputs": [],
"source": [
"from cmath import inf\n",
"\n",
"def findCheapestPrice(n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int:\n",
" nodes:dict[int,Node] = {}\n",
" for s,d,p in flights:\n",
" dnode = nodes.get(d)\n",
" if dnode:\n",
" snode = nodes.get(s)\n",
" if snode:\n",
" snode.to[dnode] = (1,p)\n",
" else:\n",
" nd = Node(s)\n",
" nd.to[dnode] = (1,p)\n",
" nodes[s] = nd\n",
" else:\n",
" snode = nodes.get(s)\n",
" if snode:\n",
" nd = Node(d)\n",
" snode.to[nd] = (1,p)\n",
" nodes[d] = nd\n",
" else:\n",
" nd1 = Node(s)\n",
" nd2 = Node(d)\n",
" nd1.to[nd2] = (1,p)\n",
" nodes[s] = nd1\n",
" nodes[d] = nd2\n",
" for _,node in nodes.items():\n",
" print(node.full())\n",
" return method2(nodes, src, dst, k)\n",
"\n",
"def method1(nodes:dict[int,Node], src:int, dst:int, k:int) -> int:\n",
" results = {}\n",
" for val,node in nodes.items():\n",
" ret = update(node, [])\n",
" results[val] = ret\n",
" desired = results[src].get(nodes[dst])\n",
" if not desired:\n",
" return -1\n",
" filtered = []\n",
" k = k + 1\n",
" for d in desired:\n",
" if d[0] <= k:\n",
" filtered.append(d)\n",
" return min(filtered, key=lambda x:x[1])\n",
"\n",
"def method2(nodes:dict[int,Node], src:int, dst:int, k:int) -> int:\n",
" def recurse(node:Node, dst:int, k:int, visited:list[int]):\n",
" results = []\n",
" if k == 1:\n",
" for nd in node.to.keys():\n",
" if nd.val == dst:\n",
" return node.to[nd][1]\n",
" return inf\n",
" if node.val in visited:\n",
" return inf\n",
" for nd in node.to.keys():\n",
" if nd.val == dst:\n",
" results.append(node.to[nd][1])\n",
" else:\n",
" temp = recurse(nd, dst, k-1, [*visited, node.val]) + node.to[nd][1]\n",
" results.append(temp)\n",
" if len(results):\n",
" return min(results)\n",
" return inf\n",
" \n",
" k = k+1\n",
" node = nodes[src]\n",
" result = recurse(node, dst, k, [])\n",
" if result == inf:\n",
" return -1\n",
" return result"
]
},
{
"cell_type": "code",
"execution_count": 157,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"100"
]
},
"execution_count": 157,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"findCheapestPrice(n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1)"
]
},
{
"cell_type": "code",
"execution_count": 178,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Node(0)->[12,8,15,10]\n",
"Node(12)->[4,3,14,13,9,0,16,6]\n",
"Node(5)->[6,14,13,16,10,9,7]\n",
"Node(6)->[14,10,2,12]\n",
"Node(8)->[6,10,11,9,2,13,3]\n",
"Node(13)->[15,12,6,16,0,5,11,7,8]\n",
"Node(15)->[3,0,6,13,12,11,14,2]\n",
"Node(10)->[12,2,15,11,5,4,9,0,7]\n",
"Node(3)->[4,12,5,6,7,10]\n",
"Node(7)->[11,3,1,14,0,12,2]\n",
"Node(11)->[16,1,0,2,6,9]\n",
"Node(9)->[4,6,1,12,7,10,15,5]\n",
"Node(4)->[7,9,8,5,11,10]\n",
"Node(2)->[12,0,11,5,13,10,7]\n",
"Node(14)->[15,1,9,7,11,6]\n",
"Node(16)->[4,12,1,3,8,11,9,14]\n",
"Node(1)->[11,4,3,7]\n"
]
},
{
"data": {
"text/plain": [
"47"
]
},
"execution_count": 178,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"findCheapestPrice(n = 4, flights = [[0,12,28],[5,6,39],[8,6,59],[13,15,7],[13,12,38],[10,12,35],[15,3,23],[7,11,26],[9,4,65],[10,2,38],[4,7,7],[14,15,31],[2,12,44],[8,10,34],[13,6,29],[5,14,89],[11,16,13],[7,3,46],[10,15,19],[12,4,58],[13,16,11],[16,4,76],[2,0,12],[15,0,22],[16,12,13],[7,1,29],[7,14,100],[16,1,14],[9,6,74],[11,1,73],[2,11,60],[10,11,85],[2,5,49],[3,4,17],[4,9,77],[16,3,47],[15,6,78],[14,1,90],[10,5,95],[1,11,30],[11,0,37],[10,4,86],[0,8,57],[6,14,68],[16,8,3],[13,0,65],[2,13,6],[5,13,5],[8,11,31],[6,10,20],[6,2,33],[9,1,3],[14,9,58],[12,3,19],[11,2,74],[12,14,48],[16,11,100],[3,12,38],[12,13,77],[10,9,99],[15,13,98],[15,12,71],[1,4,28],[7,0,83],[3,5,100],[8,9,14],[15,11,57],[3,6,65],[1,3,45],[14,7,74],[2,10,39],[4,8,73],[13,5,77],[10,0,43],[12,9,92],[8,2,26],[1,7,7],[9,12,10],[13,11,64],[8,13,80],[6,12,74],[9,7,35],[0,15,48],[3,7,87],[16,9,42],[5,16,64],[4,5,65],[15,14,70],[12,0,13],[16,14,52],[3,10,80],[14,11,85],[15,2,77],[4,11,19],[2,7,49],[10,7,78],[14,6,84],[13,7,50],[11,6,75],[5,10,46],[13,8,43],[9,10,49],[7,12,64],[0,10,76],[5,9,77],[8,3,28],[11,9,28],[12,16,87],[12,6,24],[9,15,94],[5,7,77],[4,10,18],[7,2,11],[9,5,41]], src = 13, dst = 4, k = 13)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "base",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.4"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
2 changes: 2 additions & 0 deletions MachineLearning Projects/sudoku_solver/temp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
while True:
pass
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions MachineLearning Projects/sudoku_solver/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sudoku Solver</title>
</head>
<body>
<h1>Sudoku Solver</h1>
<hr>
<h3>To solve a sudoku select the image of the sudoku and upload it to the page then hit submit.</h3>
<h3>The solution will be returned as an image on the next page.</h3>
<div>
<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="image" id="image" value={{request.files.image}}>
<input type="submit" value="Submit" >
</form>
</div>
</body>
</html>
19 changes: 19 additions & 0 deletions MachineLearning Projects/sudoku_solver/templates/result.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solution</title>
</head>
<body>
<hr>
<a href="/">Back to Main Page</a>
<hr>
<div>
<h1>Solution</h1>
</div>
<div>
<img src="{{img}}" alt="img" style="height: max-content; width: max-content;">
</div>
</body>
</html>
31 changes: 31 additions & 0 deletions MachineLearning Projects/sudoku_solver/test_full_stack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import socket

result_file = "resultfile2_server.png"
input_file = "f1.jpg"

def main(address:tuple[str,int]):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(address)
sock.settimeout(10)
with open(input_file, "rb") as f:
sock.send(f.read())
res_buf = b''
try:
while True:
try:
res = sock.recv(1)
res_buf += res
if 0 == len(res):
sock.close()
with open(result_file, "wb") as f:
f.write(res_buf)
break
except socket.timeout:
with open(result_file, "wb") as f:
f.write(res_buf)
break
finally:
sock.close()

if "__main__" == __name__:
main(("localhost", 3535))
110 changes: 110 additions & 0 deletions MachineLearning Projects/sudoku_solver/verify_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""This code is to verify the image dataset and check that all the labels of the grid location are in the correct place.
"""

import PIL.Image as Image
from matplotlib import pyplot as plt
import numpy as np
from image import SudokuDataset, get_dataset, tqdm, Model
import torch

img_size = (300,300)

def mark(positions, image, color_value):
print(positions)
print(image.shape)
x0,y0,x1,y1,x2,y2,x3,y3 = positions
image = image.transpose()
grad = (y1 - y0)/(x1 - x0)
if x1 > x0:
for i in range(x1 - x0):
image[x0 + i, int(y0 + i * grad)] = color_value
else:
for i in range(x0 - x1):
image[x0 - i, int(y0 - i * grad)] = color_value

grad = (y2 - y1)/(x2 - x1)
if x2 > x1:
for i in range(x2 - x1):
image[x1 + i, int(y1 + i * grad)] = color_value
else:
for i in range(x1 - x2):
image[x1 - i, int(y1 - i * grad)] = color_value

grad = (y3 - y2)/(x3 - x2)
if x3 > x2:
for i in range(x3 - x2):
image[x2 + i, int(y2 + i * grad)] = color_value
else:
for i in range(x2 - x3):
image[x2 - i, int(y2 - i * grad)] = color_value

grad = (y0 - y3)/(x0 - x3)
if x0 > x3:
for i in range(x0 - x3):
image[x3 + i, int(y3 + i * grad)] = color_value
else:
for i in range(x3 - x0):
image[x3 - i, int(y3 - i * grad)] = color_value
return image.transpose()

# dataset = SudokuDataset("./archive/outlines_sorted.csv", img_size)
# for item in dataset:
# try:
# image = item['image']
# grid = item['grid']
# x0,y0,x1,y1,x2,y2,x3,y3 = list(grid.numpy())
# x0 = int(x0 * img_size[0])
# x1 = int(x1 * img_size[0])
# x2 = int(x2 * img_size[0])
# x3 = int(x3 * img_size[0])
# y0 = int(y0 * img_size[1])
# y1 = int(y1 * img_size[1])
# y2 = int(y2 * img_size[1])
# y3 = int(y3 * img_size[1])
# image = mark((x0,y0,x1,y1,x2,y2,x3,y3), image.numpy()[0], 0.7)
# plt.imshow(image)
# plt.colorbar()
# plt.show()
# except KeyboardInterrupt:
# break

def test(config:dict, model_filename:str):
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = torch.load(model_filename).to(device)
model.eval()
loss = torch.nn.MSELoss().to(device)
dataset = get_dataset(config['filename'], config['input_shape'], config['batch_size'])
batch_iterator = tqdm(dataset)
for batch in batch_iterator:
x = batch['image'].to(device)
y_true = batch['grid'].to(device)
# print(batch['grid'])
# return
y_pred = model(x)
error = loss(y_true, y_pred)
batch_iterator.set_postfix({"loss":f"Loss: {error.item():6.6f}"})
x0,y0,x1,y1,x2,y2,x3,y3 = list(y_pred.detach().numpy()[1])
print(x0,y0,x1,y1,x2,y2,x3,y3)
x0 = int(x0 * img_size[0])
x1 = int(x1 * img_size[0])
x2 = int(x2 * img_size[0])
x3 = int(x3 * img_size[0])
y0 = int(y0 * img_size[1])
y1 = int(y1 * img_size[1])
y2 = int(y2 * img_size[1])
y3 = int(y3 * img_size[1])
image = mark((x0,y0,x1,y1,x2,y2,x3,y3), x.detach().numpy()[0][0], 0.7)
plt.imshow(image)
plt.colorbar()
plt.show()

config = {
"input_shape": (300,300),
"filename": "archive/outlines_sorted.csv",
"number_of_layers": 4,
"dims": 3,
"batch_size": 8,
"lr": 1e-5
}
# model = train(50, config)
test(config, "model.pt")
129 changes: 129 additions & 0 deletions MachineLearning Projects/sudoku_solver/web_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from flask import Flask, render_template, redirect, url_for, request, flash, session
from werkzeug.utils import secure_filename
import os
from random import choices, choice
from string import ascii_letters, digits
from time import sleep
from datetime import datetime
import socket

app = Flask(__name__)

app.config.from_pyfile("config.cfg")

def manage_solution(input_file, result_file) -> int:
def send(input_file:str, sock:socket.socket) -> int:
try:
with open(input_file, "rb") as f:
sock.send(f.read())
return 1
except FileNotFoundError:
return -2
except socket.error:
return -1

def connect() -> socket.socket:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((app.config['SOLVER_IP'], int(app.config['SOLVER_PORT'])))
sock.settimeout(10)
return sock

def manage_full_send(input_file:str, sock:socket.socket):
tries = 0
while tries < 5:
send_state = send(input_file, sock)
if send_state == 1:
break
elif send_state == -2:
return -2
elif send_state == -1:
sock = connect()
tries += 1
return send_state

sock = connect()
send_state = manage_full_send(input_file, sock)
if send_state == -1:
return -1
elif send_state == -2:
return -2
res_buf = b''
try:
while True:
try:
res = sock.recv(1)
res_buf += res
if 0 == len(res):
sock.close()
with open(result_file, "wb") as f:
f.write(res_buf)
break
except socket.timeout:
with open(result_file, "wb") as f:
f.write(res_buf)
break
finally:
sock.close()
return 0

@app.route('/', methods=['POST', 'GET'])
def index():
if "POST" == request.method:
print(request)
if 'image' not in request.files:
flash('No file part.', "danger")
else:
file = request.files['image']
if '' == file.filename:
flash("No file selected.", "danger")
else:
ext = "." + file.filename.split('.')[-1]
filename = datetime.now().strftime("%d%m%y%H%M%S") + "_" + "".join(i for i in choices(ascii_letters+digits, k=3)) + ext
filename = os.path.join(app.config['UPLOAD_FOLDER'], filename)
print(filename)
file.save(filename)
session['filename'] = filename
return redirect(url_for('result'))
else:
if session.get('solved'):
session.pop('solved')
if session.get('filename'):
try:
os.remove(session['filename'])
session.pop('filename')
except FileNotFoundError:
pass
return render_template('index.html', request=request)

@app.route('/result', methods=['GET'])
def result():
if not session.get('solved'):
filename = session.get('filename')
if not filename:
return redirect(url_for('/'))
solution = ""
result_file = ".".join(i for i in filename.split(".")[:-1]) + "_sol.png"
result_file = result_file.split("/")[-1]
full_result_file = "static/" + result_file
result_file = f"../static/{result_file}"
result = manage_solution(filename, full_result_file)
os.remove(session['filename'])
if result == 0:
session['filename'] = full_result_file
print("solved")
solution = result_file
session['solved'] = solution
else:
session.pop('filename')
flash(f"There was an issue, Error {result}", "danger")
redirect(url_for('/'))
else:
solution = session['solved']
return render_template('result.html', img=solution)

if "__main__" == __name__:
app.run(
host="192.168.1.88",
port=5000,
debug=True
)