import tkinter as tk import torch import torch.nn as nn from torchvision import transforms from PIL import Image, ImageDraw from pathlib import Path import torch class CNN_MNIST(nn.Module): def __init__(self): super().__init__() self.convolutional_block = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), #28+1*2-3+1 = 28x28 nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), #14x14 nn.Conv2d(32, 64, kernel_size=3, padding=1), #14x14 nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), #7x7 nn.Dropout2d(0.25) #prevents overfitting ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 7 * 7, 128), nn.ReLU(), nn.Dropout(0.5), nn.Linear(128, 10) ) def forward(self, x): x = self.convolutional_block(x) x = self.classifier(x) return x model = CNN_MNIST() BASE_DIR = Path(__file__).resolve().parent WEIGHTS_PATH = BASE_DIR / "MNIST_CNNmodel_weights.pth" try: weights = torch.load(WEIGHTS_PATH, weights_only=True) model.load_state_dict(weights) print("Model weights loaded successfully") except FileNotFoundError: print(f"Error: '{WEIGHTS_PATH.name}' not found at {WEIGHTS_PATH}") model.eval() class MNISTDrawer: def __init__(self, root): self.root = root self.root.title("MNIST Digit Predictor") self.last_x, self.last_y = None, None # visible canvas self.canvas = tk.Canvas(root, width=400, height=400, bg="black") self.canvas.pack(pady=10) # pillow image for processing self.pil_image = Image.new("L", (400, 400), "black") self.pil_draw = ImageDraw.Draw(self.pil_image) self.canvas.bind("", self.start_drawing) self.canvas.bind("", self.draw) self.canvas.bind("", self.stop_drawing) btn_frame = tk.Frame(root) btn_frame.pack(pady=10) self.predict_btn = tk.Button(btn_frame, text="Predict", command=self.predict, font=("Arial", 14), bg="#4CAF50", fg="white") self.predict_btn.pack(side=tk.LEFT, padx=10) self.clear_btn = tk.Button(btn_frame, text="Clear", command=self.clear_canvas, font=("Arial", 14), bg="#f44336", fg="white") self.clear_btn.pack(side=tk.LEFT, padx=10) self.result_label = tk.Label(root, text="Draw a digit and click Predict", font=("Arial", 18, "bold")) self.result_label.pack(pady=15) def start_drawing(self, event): self.last_x, self.last_y = event.x, event.y self.draw(event) def draw(self, event): brush_size = 30 if self.last_x is not None and self.last_y is not None: # draw on tkinter self.canvas.create_line( self.last_x, self.last_y, event.x, event.y, width=brush_size, fill="white", capstyle=tk.ROUND, smooth=True ) # copy to pillow image self.pil_draw.line( [self.last_x, self.last_y, event.x, event.y], fill=255, width=brush_size ) self.last_x, self.last_y = event.x, event.y def stop_drawing(self, event): self.last_x, self.last_y = None, None def clear_canvas(self): self.canvas.delete("all") self.result_label.config(text="Draw a digit and click Predict!") # reset pillow image self.pil_image = Image.new("L", (400, 400), "black") self.pil_draw = ImageDraw.Draw(self.pil_image) def predict(self): # mnist size img_28x28 = self.pil_image.resize((28, 28), Image.Resampling.LANCZOS) transformer = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) tensor_img = transformer(img_28x28).unsqueeze(0) # Add batch dimension [1, 1, 28, 28] # model inference with torch.no_grad(): output = model(tensor_img) probabilities = torch.softmax(output, dim=1) prediction = probabilities.argmax(dim=1).item() confidence = probabilities[0][prediction].item() * 100 self.result_label.config(text=f"Prediction: {prediction} ({confidence:.2f}%)") if __name__ == "__main__": root = tk.Tk() app = MNISTDrawer(root) root.mainloop()