Skip to content

Commit 2609693

Browse files
authored
Included a sudoku solver script.
1 parent f1b1de7 commit 2609693

22 files changed

+1321
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Sudoku Solver
2+
3+
* This app was built to allow users to solve their sudokus using a computer.
4+
* 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.
5+
* 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.
6+
* The app relies of Pytesseract to identify the characters in the sudoku image.
7+
8+
# Operation
9+
10+
* The image is first stripped of color.
11+
* It is then cropped to select the section of the sudoku. NOTE: This section is not dependent on the sudoku but has been hardcoded.
12+
* The resulting image is passed to `Pytesseract` to extract the characters and their position.
13+
* Using the characters and their position the grid size is determined.
14+
* The appropriate grid is created and filled with the discovered characters.
15+
* The grid is then solved with an algorithm contained in `sudoku.py`.
16+
* A snapshot of the solved grid is then created and sent back to the user.
17+
* The resultant snapshot is rendered on the browser page.
18+
19+
# To Run
20+
21+
* First install `Pytesseract`
22+
* Install `Flask`
23+
* Then run the `full_stack_http.py` file.
24+
* Then run the `web_interface.py` file.
25+
* Go to the browser and load the URL provided in the previous step.
26+
* Click the upload button.
27+
* Select your image and submit the form.
28+
* Wait for the result to be loaded.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
UPLOAD_FOLDER="uploads"
2+
SECRET_KEY="secret"
3+
SOLVER_IP="localhost"
4+
SOLVER_PORT=3535
178 KB
Loading
185 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import multiprocessing.util
2+
import socket
3+
from perspective import resolve_image
4+
from sudoku import Grid
5+
import argparse
6+
import multiprocessing
7+
import os
8+
9+
temp_result_file = "resultfile.png"
10+
temp_input_file = "tempfile.jpg"
11+
12+
def process_handle_transaction(proc_num:int, sock:socket.socket):
13+
print(f"[{proc_num}] Waiting for client...")
14+
sock2, address2 = sock.accept()
15+
print(f"[{proc_num}] Connected to client with address: {address2}")
16+
sock2.settimeout(1)
17+
rec_buf = b''
18+
split = temp_input_file.split('.')
19+
my_temp_input_file = ".".join(i for i in split[:-1]) + str(proc_num) + "." + split[-1]
20+
split = temp_result_file.split('.')
21+
my_temp_result_file = ".".join(i for i in split[:-1]) + str(proc_num) + "." + split[-1]
22+
try:
23+
while True:
24+
try:
25+
rec = sock2.recv(1)
26+
rec_buf += rec
27+
if len(rec) == 0:
28+
print(f"[{proc_num}] Lost connection")
29+
break
30+
except socket.timeout:
31+
with open(my_temp_input_file, "wb") as f:
32+
f.write(rec_buf)
33+
rec_buf = b''
34+
grid_size, points = resolve_image(my_temp_input_file)
35+
grid = Grid(rows=grid_size[0], columns=grid_size[1])
36+
assignment_values = {}
37+
for val,loc in points:
38+
assignment_values[loc] = val
39+
grid.preassign(assignment_values)
40+
grid.solve()
41+
grid.save_grid_image(path=my_temp_result_file, size=(400,400))
42+
with open(my_temp_result_file, "rb") as f:
43+
sock2.send(f.read())
44+
os.remove(my_temp_input_file)
45+
os.remove(my_temp_result_file)
46+
sock2.close()
47+
print(f"[{proc_num}] Finished!")
48+
break
49+
finally:
50+
sock2.close()
51+
52+
class Manager():
53+
def __init__(self, address:tuple[str,int]):
54+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
55+
self.address = address
56+
57+
def wait_for_connect(self):
58+
print("Waiting for client...")
59+
self.sock2, self.address2 = self.sock.accept()
60+
print(f"Connected to client with address: {self.address2}")
61+
self.sock2.settimeout(1)
62+
63+
def run(self):
64+
self.sock.bind(self.address)
65+
self.sock.listen()
66+
print(f"Listening from address: {self.address}")
67+
try:
68+
while True:
69+
self.wait_for_connect()
70+
rec_buf = b''
71+
while True:
72+
try:
73+
rec = self.sock2.recv(1)
74+
rec_buf += rec
75+
if len(rec) == 0:
76+
print("Lost connection")
77+
break
78+
except socket.timeout:
79+
with open(temp_input_file, "wb") as f:
80+
f.write(rec_buf)
81+
rec_buf = b''
82+
grid_size, points = resolve_image(temp_input_file)
83+
grid = Grid(rows=grid_size[0], columns=grid_size[1])
84+
assignment_values = {}
85+
for val,loc in points:
86+
assignment_values[loc] = val
87+
grid.preassign(assignment_values)
88+
grid.solve()
89+
grid.save_grid_image(path=temp_result_file, size=(400,400))
90+
with open(temp_result_file, "rb") as f:
91+
self.sock2.send(f.read())
92+
os.remove(temp_input_file)
93+
os.remove(temp_result_file)
94+
self.sock2.close()
95+
break
96+
finally:
97+
try:
98+
self.sock2.close()
99+
except socket.error:
100+
pass
101+
except AttributeError:
102+
pass
103+
self.sock.close()
104+
105+
def run_multiprocessing(self, max_clients:int=8):
106+
self.sock.bind(self.address)
107+
self.sock.listen()
108+
print(f"Listening from address: {self.address}")
109+
processes:dict[int,multiprocessing.Process]= {}
110+
proc_num = 0
111+
try:
112+
while True:
113+
if len(processes) <= max_clients:
114+
proc = multiprocessing.Process(target=process_handle_transaction, args=(proc_num, self.sock))
115+
proc.start()
116+
processes[proc_num] = proc
117+
proc_num += 1
118+
proc_num%=(max_clients*2)
119+
keys = list(processes.keys())
120+
for proc_n in keys:
121+
if not processes[proc_n].is_alive():
122+
processes.pop(proc_n)
123+
finally:
124+
if len(processes):
125+
for proc in processes.values():
126+
proc.kill()
127+
self.sock.close()
128+
129+
if "__main__" == __name__:
130+
parser = argparse.ArgumentParser()
131+
parser.add_argument("--port", type=int, default=3535, help="The port to host the server.")
132+
parser.add_argument("--host", type=str, default="localhost", help="The host or ip-address to host the server.")
133+
args = parser.parse_args()
134+
address = (args.host, args.port)
135+
manager = Manager(address)
136+
manager.run_multiprocessing(max_clients=multiprocessing.cpu_count())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import torch
2+
from torch.utils.data import Dataset, DataLoader
3+
import PIL.Image as Image
4+
import pandas as pd
5+
from tqdm import tqdm
6+
import numpy as np
7+
8+
9+
class SudokuDataset(Dataset):
10+
def __init__(self, grid_locations_file:str, input_shape:tuple[int, int]) -> None:
11+
super().__init__()
12+
self.grid_locations = []
13+
self.image_filenames = []
14+
self.input_shape = input_shape
15+
self.all_data = pd.read_csv(grid_locations_file, header=0)
16+
self.image_filenames = list(self.all_data['filepath'].to_numpy())
17+
self.grid_locations = [list(a[1:]) for a in self.all_data.values]
18+
to_pop = []
19+
for i,file in enumerate(self.image_filenames):
20+
try:
21+
Image.open(file)
22+
except FileNotFoundError:
23+
to_pop.append(i)
24+
print(f"{file} not found.")
25+
for i in reversed(to_pop):
26+
self.image_filenames.pop(i)
27+
self.grid_locations.pop(i)
28+
# print(self.all_data.columns)
29+
# print(self.grid_locations)
30+
31+
def __len__(self) -> int:
32+
return len(self.image_filenames)
33+
34+
def __getitem__(self, index) -> dict[str, torch.Tensor]:
35+
image = Image.open(self.image_filenames[index]).convert("L")
36+
size = image.size
37+
image = image.resize(self.input_shape)
38+
image = np.array(image)
39+
image = image.reshape((1,*image.shape))
40+
location = self.grid_locations[index]
41+
for i in range(len(location)):
42+
if i%2:
43+
location[i] /= size[1]
44+
else:
45+
location[i] /= size[0]
46+
return {
47+
"image": torch.tensor(image, dtype=torch.float32)/255.,
48+
"grid": torch.tensor(location, dtype=torch.float32)
49+
}
50+
51+
class Model(torch.nn.Module):
52+
def __init__(self, input_shape:tuple[int,int], number_of_layers:int, dims:int, *args, **kwargs) -> None:
53+
super().__init__(*args, **kwargs)
54+
self.input_shape = input_shape
55+
self.conv_layers:list = []
56+
self.conv_layers.append(torch.nn.Conv2d(1, dims, (3,3), padding='same'))
57+
for _ in range(number_of_layers-1):
58+
self.conv_layers.append(torch.nn.Conv2d(dims, dims, (3,3), padding='same'))
59+
self.conv_layers.append(torch.nn.LeakyReLU(negative_slope=0.01))
60+
self.conv_layers.append(torch.nn.MaxPool2d((2,2)))
61+
self.conv_layers.append(torch.nn.BatchNorm2d(dims))
62+
self.flatten = torch.nn.Flatten()
63+
self.location = [
64+
torch.nn.Linear(4107, 8),
65+
torch.nn.Sigmoid()
66+
]
67+
self.conv_layers = torch.nn.ModuleList(self.conv_layers)
68+
self.location = torch.nn.ModuleList(self.location)
69+
70+
def forward(self, x:torch.Tensor) -> torch.Tensor:
71+
for layer in self.conv_layers:
72+
x = layer(x)
73+
x = self.flatten(x)
74+
location = x
75+
for layer in self.location:
76+
location = layer(location)
77+
return location
78+
79+
def create_model(input_shape:tuple[int,int], number_of_layers:int, dims:int):
80+
model = Model(input_shape, number_of_layers, dims)
81+
for p in model.parameters():
82+
if p.dim() > 1:
83+
torch.nn.init.xavier_uniform_(p)
84+
return model
85+
86+
def get_dataset(filename:str, input_shape:tuple[int,int], batch_size:int) -> DataLoader:
87+
train_dataset = SudokuDataset(filename, input_shape)
88+
train_dataloader = DataLoader(train_dataset, batch_size, shuffle=True)
89+
return train_dataloader
90+
91+
def train(epochs:int, config:dict, model:None|Model = None) -> Model:
92+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
93+
if not model:
94+
print("========== Using new model =========")
95+
model = create_model(config['input_shape'], config['number_of_layers'], config['dims']).to(device)
96+
optimizer = torch.optim.Adam(model.parameters(), lr=config['lr'])
97+
loss = torch.nn.MSELoss().to(device)
98+
dataset = get_dataset(config['filename'], config['input_shape'], config['batch_size'])
99+
prev_error = 0
100+
try:
101+
for epoch in range(1, epochs+1):
102+
batch_iterator = tqdm(dataset, f"Epoch {epoch}/{epochs}:")
103+
for batch in batch_iterator:
104+
x = batch['image'].to(device)
105+
y_true = batch['grid'].to(device)
106+
# print(batch['grid'])
107+
# return
108+
y_pred = model(x)
109+
error = loss(y_true, y_pred)
110+
batch_iterator.set_postfix({"loss":f"Loss: {error.item():6.6f}"})
111+
error.backward()
112+
optimizer.step()
113+
# optimizer.zero_grad()
114+
if abs(error-0.5) < 0.05:# or (prev_error-error)<0.000001:
115+
del(model)
116+
model = create_model(config['input_shape'], config['number_of_layers'], config['dims']).to(device)
117+
print("New model created")
118+
prev_error = error
119+
except KeyboardInterrupt:
120+
torch.save(model, "model.pt")
121+
return model
122+
123+
def test(config:dict, model_filename:str):
124+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
125+
model = torch.load("model.pt").to(device)
126+
loss = torch.nn.MSELoss().to(device)
127+
dataset = get_dataset(config['filename'], config['input_shape'], config['batch_size'])
128+
129+
130+
if __name__ == '__main__':
131+
config = {
132+
"input_shape": (300,300),
133+
"filename": "archive/outlines_sorted.csv",
134+
"number_of_layers": 4,
135+
"dims": 3,
136+
"batch_size": 8,
137+
"lr": 1e-5
138+
}
139+
# model = train(50, config)
140+
model = torch.load("model.pt")
141+
test(config, model)
Binary file not shown.

0 commit comments

Comments
 (0)