diff --git a/MachineLearning Projects/sudoku_solver/README.md b/MachineLearning Projects/sudoku_solver/README.md new file mode 100644 index 00000000..4829dc59 --- /dev/null +++ b/MachineLearning Projects/sudoku_solver/README.md @@ -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. \ No newline at end of file diff --git a/MachineLearning Projects/sudoku_solver/__pycache__/image.cpython-311.pyc b/MachineLearning Projects/sudoku_solver/__pycache__/image.cpython-311.pyc new file mode 100644 index 00000000..bc46b3c9 Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/__pycache__/image.cpython-311.pyc differ diff --git a/MachineLearning Projects/sudoku_solver/__pycache__/perspective.cpython-312.pyc b/MachineLearning Projects/sudoku_solver/__pycache__/perspective.cpython-312.pyc new file mode 100644 index 00000000..73b55c0f Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/__pycache__/perspective.cpython-312.pyc differ diff --git a/MachineLearning Projects/sudoku_solver/__pycache__/sudoku.cpython-312.pyc b/MachineLearning Projects/sudoku_solver/__pycache__/sudoku.cpython-312.pyc new file mode 100644 index 00000000..d76e94c2 Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/__pycache__/sudoku.cpython-312.pyc differ diff --git a/MachineLearning Projects/sudoku_solver/config.cfg b/MachineLearning Projects/sudoku_solver/config.cfg new file mode 100644 index 00000000..93d8c2b5 --- /dev/null +++ b/MachineLearning Projects/sudoku_solver/config.cfg @@ -0,0 +1,4 @@ +UPLOAD_FOLDER="uploads" +SECRET_KEY="secret" +SOLVER_IP="localhost" +SOLVER_PORT=3535 \ No newline at end of file diff --git a/MachineLearning Projects/sudoku_solver/f1.jpg b/MachineLearning Projects/sudoku_solver/f1.jpg new file mode 100644 index 00000000..12c2702e Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/f1.jpg differ diff --git a/MachineLearning Projects/sudoku_solver/f2.jpg b/MachineLearning Projects/sudoku_solver/f2.jpg new file mode 100644 index 00000000..dc232b54 Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/f2.jpg differ diff --git a/MachineLearning Projects/sudoku_solver/full_stack_http.py b/MachineLearning Projects/sudoku_solver/full_stack_http.py new file mode 100644 index 00000000..d6232b81 --- /dev/null +++ b/MachineLearning Projects/sudoku_solver/full_stack_http.py @@ -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()) \ No newline at end of file diff --git a/MachineLearning Projects/sudoku_solver/image.py b/MachineLearning Projects/sudoku_solver/image.py new file mode 100644 index 00000000..24ca83d4 --- /dev/null +++ b/MachineLearning Projects/sudoku_solver/image.py @@ -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) \ No newline at end of file diff --git a/MachineLearning Projects/sudoku_solver/model.pt b/MachineLearning Projects/sudoku_solver/model.pt new file mode 100644 index 00000000..ba421a06 Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/model.pt differ diff --git a/MachineLearning Projects/sudoku_solver/perspective.py b/MachineLearning Projects/sudoku_solver/perspective.py new file mode 100644 index 00000000..3c79c78d --- /dev/null +++ b/MachineLearning Projects/sudoku_solver/perspective.py @@ -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")) \ No newline at end of file diff --git a/MachineLearning Projects/sudoku_solver/resultfile2_server.png b/MachineLearning Projects/sudoku_solver/resultfile2_server.png new file mode 100644 index 00000000..d6af2f3c Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/resultfile2_server.png differ diff --git a/MachineLearning Projects/sudoku_solver/sudoku.py b/MachineLearning Projects/sudoku_solver/sudoku.py new file mode 100644 index 00000000..fa5dc225 --- /dev/null +++ b/MachineLearning Projects/sudoku_solver/sudoku.py @@ -0,0 +1,373 @@ +"""This python script contains a class to solve a sudoku. +""" + +from copy import deepcopy +import pygame as pg + +VISITED_COLOR = (50,50,50) +AGENT_COLOR = (255,0,0) +BOARD_COLOR = (0,0,0) +WALL_COLOR = (255,255,255) +SOLUTION_COLOR = (0,255,0) +START_CELL_COLOR = (200,0,200) +END_CELL_COLOR = (0,128,128) + +SIZE = (600,600) + +class Cell(): + """Cell element of sudoku. + """ + def __init__(self, name:int|str, domain:list[int]) -> None: + """Initialise a cell of the sudoku. + + Args: + name (int): the actual cell position. + domain (list[int]): list of all the possible values the cell can take. + """ + self.name = name + self.value:int|str = None + self.domain:list[int] = deepcopy(domain) + +class Grid(): + """The actual sudoku grid. + """ + def __init__(self, rows:int|None = None, columns:int|None = None) -> None: + """Initialise the sudoku grid. + + Args: + rows (int | None, optional): The number of rows in a block eg 3 for a 9x9 sudoku. Defaults to None. + columns (int | None, optional): The number of columns in a block. Defaults to None. + """ + self.rows = rows + self.columns = columns + if not self.rows or not self.columns: + return + self.grid_len = self.rows * self.columns + self.domain:list[int] = [i for i in range(1, min(10, self.grid_len+1))] + if self.grid_len >= 10: + self.domain.extend(chr(ord('A') + i - 10) for i in range(10, self.grid_len+1)) + self.cells:list[Cell] = [Cell(i, self.domain) for i in range(self.grid_len * self.grid_len)] + self.unsolved_cells:list[int] = [i for i in range(self.grid_len * self.grid_len)] + self.solved_cells:list[int] = [] + self.initial_solved:list[int] = [] + self.initial_unsolved:list[int] = [i for i in range(self.grid_len * self.grid_len)] + + def preassign(self, values:dict[tuple, int]) -> None: + """Preassigns particular value to the cells already given in the problem. + + Args: + values (dict[tuple, int]): a dictionary with keys of the (row,column) and value of the actual value of the cell. + """ + for i, value in values.items(): + number = int(i[0]*self.grid_len + i[1]) + if number in self.initial_solved: + self.unassign_last(number) + self.cells[number].value = value + self.cells[number].domain = [] + self.unsolved_cells.remove(number) + self.initial_unsolved.remove(number) + self.initial_solved.append(number) + self.solved_cells.append(number) + + def unassign_last(self, number:int|None = None): + """Unassigns either the last value assigned to a cell or a particular cell given by number. + + Args: + number (int | None, optional): The number of the cell in the grid, starting from 0 at the top right and moving left. Defaults to None. + """ + if not number: + number = self.solved_cells.pop() + self.initial_solved.pop() + else: + self.solved_cells.remove(number) + self.initial_solved.remove(number) + self.unsolved_cells.append(number) + self.initial_unsolved.append(number) + self.cells[number].domain = deepcopy(self.domain) + self.cells[number].value = None + + def solve(self) -> None: + """Tries to solve the sudoku. + """ + while len(self.unsolved_cells) > 0: + changed = False + i = 0 + # first update domains based on known cells + while i < len(self.solved_cells): + val = self.cells[self.solved_cells[i]].value + r,c = int(self.solved_cells[i]/self.grid_len), int(self.solved_cells[i]%self.grid_len) + # first check cells on the same row + for j in range(r*self.grid_len, (r+1)*self.grid_len): + try: + self.cells[j].domain.remove(val) + if len(self.cells[j].domain) == 1: + self.cells[j].value = self.cells[j].domain[0] + self.cells[j].domain = [] + self.unsolved_cells.remove(j) + self.solved_cells.append(j) + changed = True + i = -1 + except ValueError: + pass + # next check cells on the same column + for k in range(self.grid_len): + j = k*self.grid_len + c + try: + self.cells[j].domain.remove(val) + if len(self.cells[j].domain) == 1: + self.cells[j].value = self.cells[j].domain[0] + self.cells[j].domain = [] + self.unsolved_cells.remove(j) + self.solved_cells.append(j) + changed = True + i = -1 + except ValueError: + pass + # next check cells on the same block + br = int(r/self.rows) + bc = int(c/self.columns) + for k in range(self.grid_len): + cr = br*self.rows + int(k/self.columns) + cc = bc*self.columns + int(k%self.columns) + j = cr*self.grid_len + cc + try: + self.cells[j].domain.remove(val) + if len(self.cells[j].domain) == 1: + self.cells[j].value = self.cells[j].domain[0] + self.cells[j].domain = [] + self.unsolved_cells.remove(j) + self.solved_cells.append(j) + changed = True + i = -1 + except ValueError: + pass + i += 1 + # next check for unique value in domains of cells in row column or block + # first check rows + to_break = False + for k in range(self.grid_len): + values:dict[int|str, list[int]] = {val:[] for val in self.domain} + for m in range(self.grid_len): + j = k*self.grid_len + m + for v in self.cells[j].domain: + values[v].append(j) + for val,ls in values.items(): + if len(ls) == 1: + self.cells[ls[0]].value = val + self.cells[ls[0]].domain = [] + self.unsolved_cells.remove(ls[0]) + self.solved_cells.append(ls[0]) + to_break = True + break + if to_break: + break + if to_break: + continue + # first check columns + to_break = False + for k in range(self.grid_len): + values:dict[int|str, list[int]] = {val:[] for val in self.domain} + for m in range(self.grid_len): + j = m*self.grid_len + k + for v in self.cells[j].domain: + values[v].append(j) + for val,ls in values.items(): + if len(ls) == 1: + self.cells[ls[0]].value = val + self.cells[ls[0]].domain = [] + self.unsolved_cells.remove(ls[0]) + self.solved_cells.append(ls[0]) + to_break = True + break + if to_break: + break + if to_break: + continue + if not changed: + return + + def render_cells(self, window:pg.Surface) -> None: + """Draws the grid and populates it with the value of the cells. + + Args: + window (pg.Surface): a pygame window to be used to populate the grid and cells. + """ + size = window.get_size() + py = int(size[1] / self.grid_len) + px = int(size[0] / self.grid_len) + ball = pg.Rect(0, 0, size[0], size[1]) + pg.draw.rect(window, BOARD_COLOR, ball) + for i in range(self.grid_len+1): + if i%self.columns: + pg.draw.line(window, VISITED_COLOR, (i*px, 0), (i*px, size[1])) + else: + pg.draw.line(window, WALL_COLOR, (i*px, 0), (i*px, size[1])) + if i%self.rows: + pg.draw.line(window, VISITED_COLOR, (0, i*py), (size[0], i*py)) + else: + pg.draw.line(window, WALL_COLOR, (0, i*py), (size[0], i*py)) + font = pg.font.SysFont(None, min(py, px)) + for i in self.initial_solved: + text = font.render(str(self.cells[i].value), True, AGENT_COLOR, BOARD_COLOR) + textRect = text.get_rect() + y = int(i/self.grid_len) + x = int(i%self.grid_len) + textRect.center = (int((x+0.5)*px),int((y+0.5)*py)) + window.blit(text, textRect) + for i in self.initial_unsolved: + if val:=self.cells[i].value: + text = font.render(str(val), True, SOLUTION_COLOR, BOARD_COLOR) + textRect = text.get_rect() + y = int(i/self.grid_len) + x = int(i%self.grid_len) + textRect.center = (int((x+0.5)*px),int((y+0.5)*py)) + window.blit(text, textRect) + # else: + # for dv in self.cells[i].domain: + # text = font.render(str(val), True, SOLUTION_COLOR, BOARD_COLOR) + # textRect = text.get_rect() + # y = int(i/self.grid_len) + # x = int(i%self.grid_len) + # textRect.center = (int((x+0.5)*px),int((y+0.5)*py)) + # window.blit(text, textRect) + + def render_grid(self, size:tuple[int, int]=SIZE) -> None: + """Creates the grid window and renders it. + + Args: + size (tuple[int, int], optional): The size of the window to be used. Defaults to (600,600). + """ + pg.init() + window = pg.display.set_mode(size) + window.fill(BOARD_COLOR) + while True: + for event in pg.event.get(): + if event.type == pg.QUIT: + pg.display.quit() + return + self.render_cells(window) + pg.display.update() + + def input_to_grid(self, size:tuple[int, int]=SIZE) -> None: + """Allows for input of the value of the grid cells by clicking on a cell and typing the value. + + Args: + size (tuple[int, int], optional): The size of the window to which the grid will be rendered. Defaults to (600,600). + """ + pg.init() + window = pg.display.set_mode(size) + window.fill(BOARD_COLOR) + size = window.get_size() + py = int(size[1] / self.grid_len) + px = int(size[0] / self.grid_len) + clicked_cell = None + while True: + for event in pg.event.get(): + if event.type == pg.QUIT: + pg.display.quit() + return + if event.type == pg.MOUSEBUTTONUP: + clicked_cell = event.dict['pos'] + if event.type == pg.KEYDOWN: + key = event.dict['unicode'] + if key >= '0' and key <= '9': + if clicked_cell: + pos = (int(clicked_cell[1] / py), int(clicked_cell[0] / px)) + if int(key) <= self.grid_len: + self.preassign({pos:int(key)}) + elif key >= 'A' and key <= 'Z': + if clicked_cell: + pos = (int(clicked_cell[1] / py), int(clicked_cell[0] / px)) + if (ord(key) - ord('A') + 10) <= self.grid_len: + self.preassign({pos:key}) + elif key == ' ': + self.unassign_last() + self.render_cells(window) + pg.display.update() + + def save(self, filename:str) -> None: + """Saves the current state of the grid in a file.\n + Save format is:\n + rows,columns\n + (cell_number,cell_value)|(cell_number,cell_value)|...|(cell_number,cell_value)\n + (cell_number,cell_value)|(cell_number,cell_value)|...|(cell_number,cell_value)\n + \n + where the second line is the initial cell values before trying to solve\n + \t the third line is the initially unsolved cell values after solving if Grid.solve() has been run\n + + Args: + filename (str): The path of the file to be saved to. + """ + s = f"{self.rows},{self.columns}\n" + s += "|".join(f"({a},{self.cells[a].value})" for a in self.initial_solved) + s += "\n" + s += "|".join(f"({a},{self.cells[a].value})" for a in self.initial_unsolved) + with open(filename, 'w') as f: + f.write(s) + f.close() + + def load(self, filename:str): + """Loads the grid from a saved state file created by calling Grid.save(filename) + + Args: + filename (str): The path to the file containing the grid status to be loaded. + """ + with open(filename, 'r') as f: + for i,line in enumerate(f): + line = line.replace("\n","") + if i == 0: + rows, columns = line.replace("(","").replace(")","").split(",") + self.rows = int(rows) + self.columns = int(columns) + elif i == 1: + initial_solved_pairs = [tuple(int(i) for i in a.split(",")) for a in line.replace("(","").replace(")","").split("|")] + elif i == 2: + initial_unsolved_pairs = [tuple(eval(i) for i in a.split(",")) for a in line.replace("(","").replace(")","").split("|")] + f.close() + self.grid_len = self.rows * self.columns + self.domain:list[int] = [i for i in range(1, min(10, self.grid_len+1))] + if self.grid_len >= 10: + self.domain.extend(chr(ord('A') + i - 10) for i in range(10, self.grid_len+1)) + self.cells:list[Cell] = [Cell(i, self.domain) for i in range(self.grid_len * self.grid_len)] + self.unsolved_cells:list[int] = [i for i in range(self.grid_len * self.grid_len)] + self.solved_cells:list[int] = [] + self.initial_solved:list[int] = [] + self.initial_unsolved:list[int] = [i for i in range(self.grid_len * self.grid_len)] + for (number,value) in initial_solved_pairs: + self.initial_solved.append(number) + self.solved_cells.append(number) + self.cells[number].value = value + self.cells[number].domain = [] + self.initial_unsolved.remove(number) + self.unsolved_cells.remove(number) + for (number,value) in initial_unsolved_pairs: + if value: + self.solved_cells.append(number) + self.cells[number].value = value + self.cells[number].domain = [] + self.unsolved_cells.remove(number) + + def save_grid_image(self, path:str, size:tuple[int, int]=SIZE) -> None: + pg.init() + window = pg.display.set_mode(size) + window.fill(BOARD_COLOR) + self.render_cells(window) + pg.image.save(window, path) + pg.quit() + +def main(): + r = int(input("Enter number of rows in a block: ")) + c = int(input("Enter number of columns in a block: ")) + grid = Grid(r,c) + # grid = Grid() + # grid.load("s2.txt") + grid.input_to_grid() + grid.save("s3.txt") + grid.solve() + grid.save("s3.txt") + # grid = Grid() + # grid.load("s1.txt") + grid.render_grid() + +if __name__ == "__main__": + main() diff --git a/MachineLearning Projects/sudoku_solver/temp.ipynb b/MachineLearning Projects/sudoku_solver/temp.ipynb new file mode 100644 index 00000000..0737eb93 --- /dev/null +++ b/MachineLearning Projects/sudoku_solver/temp.ipynb @@ -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 +} diff --git a/MachineLearning Projects/sudoku_solver/temp.py b/MachineLearning Projects/sudoku_solver/temp.py new file mode 100644 index 00000000..553f0531 --- /dev/null +++ b/MachineLearning Projects/sudoku_solver/temp.py @@ -0,0 +1,2 @@ +while True: + pass \ No newline at end of file diff --git a/MachineLearning Projects/sudoku_solver/tempfile1.jpg b/MachineLearning Projects/sudoku_solver/tempfile1.jpg new file mode 100644 index 00000000..7e398e54 Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/tempfile1.jpg differ diff --git a/MachineLearning Projects/sudoku_solver/tempfile2.jpg b/MachineLearning Projects/sudoku_solver/tempfile2.jpg new file mode 100644 index 00000000..b0069382 Binary files /dev/null and b/MachineLearning Projects/sudoku_solver/tempfile2.jpg differ diff --git a/MachineLearning Projects/sudoku_solver/templates/index.html b/MachineLearning Projects/sudoku_solver/templates/index.html new file mode 100644 index 00000000..8a812ec4 --- /dev/null +++ b/MachineLearning Projects/sudoku_solver/templates/index.html @@ -0,0 +1,20 @@ + + + + + + Sudoku Solver + + +

Sudoku Solver

+
+

To solve a sudoku select the image of the sudoku and upload it to the page then hit submit.

+

The solution will be returned as an image on the next page.

+
+
+ + +
+
+ + \ No newline at end of file diff --git a/MachineLearning Projects/sudoku_solver/templates/result.html b/MachineLearning Projects/sudoku_solver/templates/result.html new file mode 100644 index 00000000..94fbedb1 --- /dev/null +++ b/MachineLearning Projects/sudoku_solver/templates/result.html @@ -0,0 +1,19 @@ + + + + + + Solution + + +
+ Back to Main Page +
+
+

Solution

+
+
+ img +
+ + \ No newline at end of file diff --git a/MachineLearning Projects/sudoku_solver/test_full_stack.py b/MachineLearning Projects/sudoku_solver/test_full_stack.py new file mode 100644 index 00000000..a24ab068 --- /dev/null +++ b/MachineLearning Projects/sudoku_solver/test_full_stack.py @@ -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)) \ No newline at end of file diff --git a/MachineLearning Projects/sudoku_solver/verify_image.py b/MachineLearning Projects/sudoku_solver/verify_image.py new file mode 100644 index 00000000..1ad57615 --- /dev/null +++ b/MachineLearning Projects/sudoku_solver/verify_image.py @@ -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") \ No newline at end of file diff --git a/MachineLearning Projects/sudoku_solver/web_interface.py b/MachineLearning Projects/sudoku_solver/web_interface.py new file mode 100644 index 00000000..6bc14604 --- /dev/null +++ b/MachineLearning Projects/sudoku_solver/web_interface.py @@ -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 + ) \ No newline at end of file