Discord

Checklist OAI 2026 Final (Stage III) · Task 3

Pixels

Polish title: Piksele

Classify MNIST digits from masked images in which only 10 pixels are visible, using a CNN trained on full images.

  • Vision
  • Image classification from sparse pixels
  • Polish original · English translation

The task

Two characters peek at an art exhibition but cannot see everything, yet want to say what the pictures show. The contestant has a pre-trained MNIST digit classifier (a simple CNN trained on full, normalised images) and a set of masked 28×28 images in which only 10 pixels are visible and the rest are zeroed.

The task is to implement a function that takes a masked image and returns the predicted digit (0–9).

Provided are the trained model, 500 masked training images with labels and 2,000 masked validation images with labels; each split also stores the unmasked originals. The hidden test set has the same characteristics and size as the validation set.

Abridged and translated by SOTA from the official Polish materials. The official statement has the exact rules, and it wins wherever this summary differs.

In English

This task was published in Polish. SOTA translated it into English on 16 September 2026. Only the words changed in the notebooks: markdown, code comments, messages and printed output. The code, file names and paths are the original's, so a translated notebook runs with the original data.

Read the task notebook in English 501 words and 7 code cells

Pixels

image

Introduction

Aiga and Bajtek are peeking at an art exhibition. Unfortunately, they cannot make out everything! Even so, they would like to be able to tell what is in the pictures.

Task

You have access to a pre-trained MNIST digit classifier and a set of masked 28×28 images in which only 10 pixels are visible (the rest are set to zero).

Your task is to implement the function solution(img), which takes a masked image and correctly classifies the digit (0-9).

Data

  • A trained CNN model (data/mnist_classifier.pth)
  • Training set: 500 masked images with labels (data/train.npz)
  • Validation set: 2000 masked images with labels (data/val.npz)

Your solution will be scored on a test set with the same characteristics and number of samples as the validation set. You do not have access to it during the contest.

Scoring Criterion

Your solution will be scored on the basis of the accuracy of its predictions on the hidden test set:

  • if the result is lower than 35%, you will receive 0 points,
  • if the result is higher than 60%, you will receive 100 points.

Points for values between these thresholds will be awarded proportionally.

Constraints

  • Your solution will be tested on the Contest Platform without internet access.
  • The evaluation of the final solution must not take longer than 3 minutes (without a GPU).
  • List of permitted libraries: scikit-learn, numpy, pandas, torch, opencv, scipy.

Submission Files

This notebook, completed with your solution (see the function solution).

Evaluation

During grading, the FINAL_EVALUATION_MODE flag will be set to True. The number of points will be calculated on the secret test set on the Contest Platform. If the solution does not meet the criteria or does not run correctly, you will receive 0 points for it.

Hints

  1. The model was trained on full (unmasked), normalised MNIST images. See the function predict.

Starter Code

In this section we initialise the environment by importing the required libraries and loading the model.

######################### DO NOT CHANGE THIS CELL WHEN SUBMITTING ##########################
FINAL_EVALUATION_MODE = False # During grading on the grading system, the flag is automatically changed to True.
# ######################### DO NOT CHANGE THIS CELL WHEN SUBMITTING ##########################

import numpy as np
import torch
import torch.nn as nn
import math

if not FINAL_EVALUATION_MODE:
    import matplotlib.pyplot as plt

seed = 42
torch.manual_seed(seed)
np.random.seed(seed)
class SimpleCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Conv2d(1, 16, 3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(16, 32, 3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Flatten(),
            nn.Linear(32 * 7 * 7, 128),
            nn.ReLU(),
            nn.Linear(128, 10)
        )
    def forward(self, x):
        return self.net(x)


def load_trained_model(model_path="mnist_classifier.pth"):
    """Load a trained model."""
    model = SimpleCNN()
    model.load_state_dict(torch.load(model_path, map_location='cpu'))
    model.eval()
    return model


def predict(images, model):
    # normalize input!
    images = images.float()
    images = (images - 0.1307) / 0.3081
    with torch.no_grad():
        logits = model(images)
        probs = torch.softmax(logits, dim=1)
        preds = torch.argmax(probs, dim=1)
    return preds, probs

def round_half_up(number: float) -> int:
    return int(math.floor(number + 0.5))

def compute_score(acc_val: float):
    upper_limit = 0.6
    lower_limit = 0.35
    if acc_val > upper_limit:
        return 100
    elif acc_val < lower_limit:
        return 0
    else:
        return round_half_up((acc_val - lower_limit) / (upper_limit - lower_limit) * 100)

Loading the Data

File format:

  • data/train.npz: images, labels, originals
  • data/val.npz: images, labels, originals

Where:

  • images — masked images (N × 1 × 28 × 28, float16), only 10 pixels visible, the rest set to zero,
  • labels — digit labels (N, int, values 0–9),
# ######################### DO NOT CHANGE THIS CELL WHEN SUBMITTING ##########################

model = load_trained_model("data/mnist_classifier.pth")
model.net.eval()

def load_split(path):
    d = np.load(path)
    return torch.from_numpy(d["images"]), torch.from_numpy(d["labels"]).long()

train_imgs, train_labels = load_split("data/train.npz")
val_imgs, val_labels = load_split("data/val.npz")

print(f"Train: {len(train_imgs)}, Val: {len(val_imgs)}")

Data inspection

A visualisation of selected masked images with their labels. For each example we show the masked image (10 visible pixels) next to the original.

# ######################### DO NOT CHANGE THIS CELL WHEN SUBMITTING ##########################

if not FINAL_EVALUATION_MODE:
    fig, axes = plt.subplots(2, 10, figsize=(20, 4))
    for i in range(20):
        row = (i // 10)
        col = i % 10
        predicted = predict(train_imgs[i:i+1], model)[0].item()
        axes[row, col].imshow(train_imgs[i, 0].numpy(), cmap='gray', vmin=0, vmax=1)
        axes[row, col].set_title(f"y={train_labels[i].item()}, pred={predicted}", fontsize=8)
        axes[row, col].axis('off')
    plt.tight_layout()
    plt.show()

Your Solution

Place your solution in this section. Implement the function

  • predict(img) - the function takes a masked image to classify. It returns the predicted label as a single number 0-9.
def solution(img):
    """
    Classification of masked MNIST images.

    Args:
        img: tensor (1, 28, 28) - the masked image to classify

    Returns:
        the predicted label (0-9) for the given image
    """
    # TODO: write your solution here!

    # Example: direct model prediction on the masked images (baseline)
    img = img.unsqueeze(0)  # add the batch dimension
    predicted_label, _ = predict(img, model)
    return predicted_label.item()

Evaluation

Running the cell below lets you check the accuracy of your solution on the validation set. Before submitting, make sure that the whole notebook runs from start to finish without errors and without any user intervention after selecting the "Run All" option.

During grading, the model will be scored on the hidden test set.

######################### DO NOT CHANGE THIS CELL WHEN SUBMITTING ##########################

if not FINAL_EVALUATION_MODE:
    accs = []
    for img, label in zip(val_imgs, val_labels):
        pred = solution(img)
        accs.append(pred == label.item())
    acc = np.mean(accs)
    print(f"  Accuracy on the validation set: {acc*100:.2f}%")

    score = compute_score(acc)
    print(f"  Number of points: {score}")

Translated by SOTA. The Polish original is the official version and wins wherever the two differ. The data and model are in the data/ folder next to the original notebook. As in the original, the 'Your Solution' section names predict(img), but the function to implement is solution(img). If you organise this olympiad and would like the translation removed, email [email protected] and we will take it down.

At a glance

You get
data/mnist_classifier.pth, data/train.npz and data/val.npz (images of shape N × 1 × 28 × 28 in float16, labels, originals) in the task folder.
You submit
This notebook with the solution function returning a single label.
Scoring
Accuracy on the hidden test set: 0 points below 35%, 100 points above 60%, linear in between.
Rules
  • Tested without Internet access; evaluation must take at most 3 minutes (without a GPU).
  • Allowed libraries: scikit-learn, numpy, pandas, torch, opencv, scipy.
Format
Final (Stage III), 17–20 April 2026, Faculty of Mathematics and Computer Science, Adam Mickiewicz University in Poznań; two 5-hour contest sessions (Saturday and Sunday, i.e. 18 and 19 April 2026); 45 finalists; maximum 400 points. Evaluated automatically on the Competition Platform (Platforma Konkursowa) on a hidden test set; points are rounded to an integer, and a notebook that fails the requirements or does not run scores 0.

Details

Year
2026, Poznań, Poland
Round
Final (Stage III) · Task 3
Language
Polish; English translation by SOTA
License
Not stated by the source