from pathlib import Path import numpy as np import torch from PIL import Image, ImageOps from torch import nn class DigitCNN(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Flatten(), nn.Linear(64 * 7 * 7, 128), nn.ReLU(), nn.Dropout(0.25), nn.Linear(128, 10), ) def forward(self, x): return self.net(x) ARTIFACT = torch.load( Path(__file__).with_name("mnist_cnn.pt"), map_location="cpu", weights_only=False, ) MODEL = DigitCNN() MODEL.load_state_dict(ARTIFACT["model_state_dict"]) MODEL.eval() ACCURACY = ARTIFACT["accuracy"] def _to_image(image): if image is None: raise ValueError("Please draw or upload a digit image.") if isinstance(image, dict): image = image.get("composite") or image.get("image") or image.get("background") if isinstance(image, (str, Path)): return Image.open(image) return Image.fromarray(np.asarray(image)) def preprocess_image(image): img = _to_image(image).convert("L") arr = np.asarray(img).astype("float32") # Convert common black-on-white handwriting into MNIST-style white-on-black. if arr.mean() > 127: arr = 255.0 - arr arr[arr < 25] = 0.0 rows, cols = np.where(arr > 0) if len(rows) == 0 or len(cols) == 0: raise ValueError("No digit-like strokes were detected.") cropped = arr[rows.min() : rows.max() + 1, cols.min() : cols.max() + 1] cropped_img = Image.fromarray(cropped.astype("uint8"), mode="L") cropped_img.thumbnail((20, 20), Image.Resampling.LANCZOS) canvas = Image.new("L", (28, 28), 0) left = (28 - cropped_img.width) // 2 top = (28 - cropped_img.height) // 2 canvas.paste(cropped_img, (left, top)) preview = np.asarray(canvas).astype("float32") / 255.0 features = torch.from_numpy(preview.reshape(1, 1, 28, 28)).float() return features, preview def predict_digit(image): features, preview = preprocess_image(image) with torch.no_grad(): probabilities = torch.softmax(MODEL(features), dim=1).numpy()[0] digit = int(np.argmax(probabilities)) confidence = float(probabilities[digit]) ranking = { str(index): float(score) for index, score in sorted( enumerate(probabilities), key=lambda item: item[1], reverse=True, ) } return str(digit), confidence, ranking, preview