| import gradio as gr |
| import numpy as np |
| from PIL import Image |
| |
| |
| from layers import Conv, MaxPool, Flatten, Dense |
| from activations import GELU |
| |
| |
| conv = Conv(input_shape=(1, 28, 28), kernel_size=5, num_kernels=12) |
| gelu = GELU() |
| pool = MaxPool(2, 2) |
| flatten = Flatten() |
| dense = Dense(1728, 10) |
| |
| |
| weights_loaded = False |
| try: |
| data = np.load("model_weights.npz") |
| conv.kernels = data["conv_kernels"] |
| conv.biases = data["conv_biases"] |
| dense.weights = data["dense_weights"] |
| dense.biases = data["dense_biases"] |
| weights_loaded = True |
| print("Weights loaded successfully!") |
| except FileNotFoundError: |
| print("Warning: model_weights.npz not found.") |
| except Exception as e: |
| print(f"Warning: error loading weights: {e}") |
| |
| |
| def make_grid(feature_maps, cols=4): |
| n, h, w = feature_maps.shape |
| rows = (n + cols - 1) // cols |
| |
| |
| padding = 2 |
| grid_h = rows * h + (rows - 1) * padding |
| grid_w = cols * w + (cols - 1) * padding |
| grid = np.zeros((grid_h, grid_w), dtype=np.uint8) |
| |
| for idx in range(n): |
| r = idx // cols |
| c = idx % cols |
| |
| f_map = feature_maps[idx] |
| f_min, f_max = f_map.min(), f_map.max() |
| |
| if f_max > f_min: |
| f_map = 255.0 * (f_map - f_min) / (f_max - f_min) |
| else: |
| f_map = f_map * 0 |
| |
| f_map = f_map.astype(np.uint8) |
| |
| y_start = r * (h + padding) |
| x_start = c * (w + padding) |
| grid[y_start:y_start+h, x_start:x_start+w] = f_map |
| |
| img = Image.fromarray(grid) |
| |
| img = img.resize((grid_w * 12, grid_h * 12), Image.Resampling.NEAREST) |
| return img |
| |
| |
| def predict(input_image): |
| if not weights_loaded: |
| return {"Error: please upload 'model_weights.npz'": 1.0}, None, None |
| |
| if input_image is None: |
| return "No image drawn", None, None |
| |
| if isinstance(input_image, dict): |
| img = input_image['composite'] |
| else: |
| img = input_image |
| |
| |
| img = img.convert('L').resize((100, 100)) |
| arr = np.array(img) |
| |
| |
| if arr[0, 0] > 128: |
| bg_noise = max(arr[0, 0], arr[-1, -1], arr[0, -1], arr[-1, 0]) |
| arr[arr > bg_noise - 10] = bg_noise |
| arr = bg_noise - arr |
| else: |
| bg_noise = min(arr[0, 0], arr[-1, -1], arr[0, -1], arr[-1, 0]) |
| arr[arr < bg_noise + 10] = bg_noise |
| arr = arr - bg_noise |
| |
| |
| non_zero = np.argwhere(arr > 35) |
| if len(non_zero) > 0: |
| min_y, min_x = non_zero.min(axis=0) |
| max_y, max_x = non_zero.max(axis=0) |
| cropped = arr[min_y:max_y+1, min_x:max_x+1] |
| cropped[cropped < 45] = 0 |
| |
| h, w = cropped.shape |
| cropped_img = Image.fromarray(cropped) |
| if h > w: |
| new_h = 20 |
| new_w = int(20 * w / h) |
| else: |
| new_w = 20 |
| new_h = int(20 * h / w) |
| new_w = max(1, new_w) |
| new_h = max(1, new_h) |
| |
| resized = cropped_img.resize((new_w, new_h), Image.Resampling.LANCZOS) |
| |
| canvas = Image.new('L', (28, 28), 0) |
| offset_x = (28 - new_w) // 2 |
| offset_y = (28 - new_h) // 2 |
| canvas.paste(resized, (offset_x, offset_y)) |
| arr = np.array(canvas) |
| |
| x = (arr / 255.0) - 0.5 |
| x = x[np.newaxis, :, :] |
| |
| |
| out_conv = conv.forward(x) |
| out_gelu = gelu.forward(out_conv) |
| out_pool = pool.forward(out_gelu) |
| |
| out_flat = flatten.forward(out_pool[np.newaxis, :, :, :]) |
| logits = dense.forward(out_flat)[0] |
| |
| |
| probs = np.exp(logits - np.max(logits)) |
| probs /= np.sum(probs) |
| |
| |
| conv_grid = make_grid(out_conv) |
| pool_grid = make_grid(out_pool) |
| |
| |
| class_probs = {str(i): float(probs[i]) for i in range(10)} |
| return class_probs, conv_grid, pool_grid |
| |
| |
| demo = gr.Interface( |
| fn=predict, |
| inputs=gr.Sketchpad(type="pil", image_mode="L"), |
| outputs=[ |
| gr.Label(num_top_classes=3, label="Prediction"), |
| gr.Image(label="Layer 1: Convolutional Activations (12 Filters)", type="pil"), |
| gr.Image(label="Layer 2: Max Pooling Outputs (Downsampled Features)", type="pil") |
| ], |
| title="CNN from Scratch - Digit Classifier", |
| description="Draw a digit in the box to predict its value and see inside the model's 'brain' in real-time!" |
| ) |
| |
| if __name__ == "__main__": |
| demo.launch() |