Discord

Checklist OAI 2026 Final (Stage III) · Task 1

Decision Localisation

Polish title: Lokalizacja decyzji

Produce saliency heatmaps for a fixed binary ResNet-18 classifier that overlap as much as possible with the COCO segmentation mask of the detected object.

  • Vision
  • Explainability (saliency maps / weakly supervised localisation)
  • Polish original · English translation

The task

A classifier may recognise objects for the right reasons or rely on incidental cues such as background or colour, so it is useful to check which image regions drive its decisions. The contestant receives a trained binary ResNet-18 that recognises whether one specific object is present and must generate saliency heatmaps showing the regions with the greatest influence on its decision.

The data are a subset of COCO 2017 in which the positive class is always present; only a validation set of 1,200 images of size 224×224 is provided, and the test set also has 1,200 images. The same model is used during testing.

Each heatmap (treated as a fuzzy set) is compared with the object's ground-truth mask from the COCO annotations using IoU = |H ∩ G| / |H ∪ G|.

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 522 words and 11 code cells

Decision Localisation

Image

Image generated with ChatGPT

Introduction

A model can recognise objects in images, but it is not known what exactly it is looking at when it makes a decision. Sometimes it really does detect the correct object, and sometimes it relies on incidental features, such as the background or colours. We therefore want to find out which parts of the image have the greatest influence on its answer.

Task

You are given a trained binary classifier. Your task is to generate saliency maps (heatmaps) indicating the regions of the image that had the greatest influence on its decision.

Data

The dataset is a subset of COCO2017, containing images in which the class that the model recognises as positive is always present.

As data you will receive only the validation set – 1200 images.

All images have a size of 224 × 224 pixels.

You also receive a ResNet-18 model, trained for a binary classification task. The model recognises whether one specific object is present in the image.

The same model will be used during testing. The test set consists of 1200 images.

Scoring criterion

Your solution will be scored on the basis of the quality of the generated saliency maps.

For each heatmap, the Intersection over Union (IoU) metric is computed between:

  • the region indicated by the heatmap (a fuzzy set),
  • the actual location of the object, defined on the basis of the mask.

We define IoU as:

IoU=HGHGIoU = \frac{|H \cap G|}{|H \cup G|}

where:

  • HH – the salient area indicated by the model,
  • GG – the object area from the COCO annotation.

The final score will be calculated according to the formula:

results_evaluation={0if IoU0.25100IoU0.1910.2450.191if 0.191IoU_mean0.245100otherwiseresults\_evaluation = \begin{cases} 0 &\quad \text{if IoU} \le 0.25 \\ 100 \cdot \dfrac{\text{IoU} - 0.191}{0.245 - 0.191} &\quad \text{if } 0.191 \leq \text{IoU\_{mean}} \le 0.245 \\ 100 &\quad \text{otherwise} \end{cases}

Constraints

  • The solution will be run without internet access
  • A GPU is available
  • The entire evaluation must not take longer than 3 minutes
  • You must not modify the model weights
  • Permitted libraries: torch, torchvision, numpy, cv2

Your solution will be tested on the Contest Platform without internet access and in an environment with a GPU.

Submission Files

This notebook, completed with your solution.

Evaluation

Remember that during grading the FINAL_EVALUATION_MODE flag will be set to True.

You can score between 0 and 100 points for this task. The number of points you receive will be calculated on the (secret) test set on the Contest Platform according to the formula given above, rounded to an integer. If your solution does not meet the criteria above or does not run correctly, you will receive 0 points for the task.

Starter code

######################### DO NOT CHANGE THIS CELL ##########################

import os
import random
import numpy as np
import cv2
import torch
from torch import nn
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms, models

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

seed = 42
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
np.random.seed(seed)
random.seed(seed)

FINAL_EVALUATION_MODE = False  # We will set this flag to True during grading.
######################### DO NOT CHANGE THIS CELL ##########################

def download_data():
    """Downloads the dataset from Google Drive and saves it in the 'data' folder."""
    import shutil
    import gdown

    # Create or reset the 'data' folder
    if not os.path.exists('data'):
        os.makedirs('data')
    else:
        shutil.rmtree('data')
        os.makedirs('data')

    # Download the file from Google Drive and save it in the 'data' folder
    url_model = "https://drive.google.com/file/d/1b6WhjG_GDihBNKLIXtFcaclJKkTWbZZk/view?usp=drive_link"
    gdown.download(url_model, f'data/model.pth', quiet=True, fuzzy=True)

    url_data = "https://drive.google.com/file/d/19jSIGHXp_BY_gkyGdYKKc7ls-QU7_Rcn/view?usp=share_link"
    gdown.download(url_data, f'data/val_data.npz', quiet=True, fuzzy=True)


if not FINAL_EVALUATION_MODE:
    download_data()

Model definition

######################### DO NOT CHANGE THIS CELL ##########################

# Definition of the model architecture.
class ResNet18Classifier(nn.Module):
    def __init__(self, pretrained=False, num_classes=1):
        super().__init__()

        if pretrained:
            weights = models.ResNet18_Weights.DEFAULT
        else:
            weights = None

        resnet = models.resnet18(weights=weights)

        self.encoder = nn.Sequential(
            resnet.conv1,
            resnet.bn1,
            resnet.relu,
            resnet.maxpool,
            resnet.layer1,
            resnet.layer2,
            resnet.layer3,
            resnet.layer4,
        )

        self.pool = nn.AdaptiveAvgPool2d((1, 1))
        self.head = nn.Linear(512, num_classes)

    def forward(self, x):
        x = self.encoder(x)
        x = self.pool(x)
        x = torch.flatten(x, 1)
        x = self.head(x)
        return x
######################### DO NOT CHANGE THIS CELL ##########################

# Model initialisation
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

model_A = ResNet18Classifier(num_classes=1).to(device)

# Loading the trained weights
model_A.load_state_dict(torch.load("data/model.pth", map_location=device))
model_A.eval()

Loading the data

######################### DO NOT CHANGE THIS CELL ##########################

class CocoSegmentationDataset(Dataset):
    def __init__(self, npz_path, mode="val"):
        """
        npz_path: Path to the .npz file (e.g. 'data/val_data.npz')
        mode: Key inside the npz
        """
        if not os.path.exists(npz_path):
            raise FileNotFoundError(f"File not found: {npz_path}")

        # Loading the data from the NPZ
        data = np.load(npz_path, allow_pickle=True)

        # Key mapping
        key = f"{mode}_ds"
        if key not in data:
            raise KeyError(f"The file {npz_path} does not contain the key {key}")

        self.samples = data[key].tolist()

        # Definition of the ImageNet transformation
        self.normalize = transforms.Normalize(
            mean=[0.485, 0.456, 0.406],
            std=[0.229, 0.224, 0.225]
        )

    def __len__(self):
        return len(self.samples)

    def __getitem__(self, idx):
        sample = self.samples[idx]

        # Image conversion (uint8 [H,W,C] -> float32 [C,H,W])
        # 'image' is your JPG from the original dataset
        img_np = sample["image"]
        img_tensor = torch.from_numpy(img_np).to(torch.float32) / 255.0
        img_tensor = img_tensor.permute(2, 0, 1) # Change to [C, H, W]
        img_tensor = self.normalize(img_tensor)

        # 4. Mask conversion (uint8 [H,W] -> float32 [1,H,W])
        # 'mask' is your PNG mask
        mask_np = sample["mask"]
        mask_tensor = torch.tensor(mask_np / 255.0, dtype=torch.float32)

        # Adding a channel dimension if the mask is 2D
        if mask_tensor.ndim == 2:
            mask_tensor = mask_tensor.unsqueeze(0)

        return img_tensor, mask_tensor
######################### DO NOT CHANGE THIS CELL ##########################

# Initialisation
val_dataset = CocoSegmentationDataset("data/val_data.npz")
val_loader = DataLoader(val_dataset, batch_size=1, shuffle=True)

Your solution

# Your solution - make changes only here.
# Do not change the function arguments or the number of returned values.
# The function has been filled in with an example solution that is far
# from optimal - in particular, it does not use the given models at all.
# It is meant to serve as a starting point for building a solution.

def your_solution(img, model):
    _, H, W = img.shape

    # Randomly generated mask.
    mask = torch.randint(0, 2, (H, W), dtype=torch.float32)

    return mask
######################### DO NOT CHANGE THIS CELL ##########################

# Displaying an example image from the dataset.
if not FINAL_EVALUATION_MODE:
    import matplotlib.pyplot as plt

    def plot_example(dataset, model, your_solution):
        img, mask = dataset[0]
        pred = your_solution(img, model)
        if torch.is_tensor(pred):
            pred = pred.detach().cpu().numpy()
        mask_to_show = mask.squeeze().cpu().numpy()

        # Channel permutation
        img_display = img.permute(1, 2, 0).numpy()
        # Reversing the normalisation for the image
        img_display = img_display * np.array([0.229, 0.224, 0.225]) + np.array([0.485, 0.456, 0.406])
        # Clipping the values to the range [0, 1] for correct visualisation
        img_display = np.clip(img_display, 0, 1)

        plt.figure(figsize=(12, 4))
        plt.subplot(1, 3, 1)
        plt.title("Original")
        plt.imshow(img_display)

        plt.subplot(1, 3, 2)
        plt.title("Mask (GT)")
        plt.imshow(mask_to_show, cmap='gray')

        plt.subplot(1, 3, 3)
        plt.title("Prediction")
        plt.imshow(pred, cmap='jet', alpha=0.5)
        plt.imshow(img_display, alpha=0.5)
        plt.show()

    # Running the function that visualises an example image from the dataset together with the GT mask and the prediction.
    plot_example(val_dataset, model_A, your_solution)
######################### DO NOT CHANGE THIS CELL ##########################

# Computes the IoU metric between two masks.

def compute_iou(pred, target):
    # We make sure the tensors are binary (0 or 1)
    pred = (pred > 0.5).float()
    target = (target > 0.5).float()

    intersection = (pred * target).sum()
    union = (pred + target).clamp(0, 1).sum()
    return (intersection + 1e-8) / (union + 1e-8)

Evaluation

Running the cell below lets you check how many points your solution would score on the training data. Before submitting, make sure that the whole notebook runs from start to finish without errors after setting the flag FINAL_EVALUATION_MODE = True, and without any user intervention after selecting the "Run All" option.

######################### DO NOT CHANGE THIS CELL ##########################

# Computing the score for participants (public, uses val_loader)
def compute_score(loader, model):
    model.eval()
    ious = []
    device = next(model.parameters()).device

    with torch.no_grad():
        for img, mask in loader:
            img, mask = img.to(device), mask.to(device)

            # We pass a single image (C, H, W) to the participant's solution
            # We assume that your_solution returns a mask of shape (H, W) or (1, H, W)
            pred = your_solution(img[0], model)
            mask = mask.to(pred.device)

            # Matching the shapes for IoU
            ious.append(compute_iou(pred.squeeze(), mask.squeeze()))

    mIoU = sum(ious) / len(ious)

    # Scaling the score: 0.191 -> 0 pts, 0.245 -> 100 pts
    raw_score = 100 * (mIoU - 0.191) / (0.245 - 0.191)
    score = int(torch.clamp(raw_score, min=0, max=100).round().item())

    return mIoU, score
######################### DO NOT CHANGE THIS CELL ##########################

# Evaluates your solution on the validation set by computing the mean IoU.
# It should obtain a similar value on the secret test set.

if not FINAL_EVALUATION_MODE:
    mIoU, score = compute_score(val_loader, model_A)
    print(f"mIoU: {mIoU:.4f} | Estimated number of points: {score}/100")

Translated by SOTA. The Polish original is the official version and wins wherever the two differ. The scoring formula is translated as published: its first case reads 'IoU ≤ 0.25', while the text and the scoring code scale the mean IoU linearly from 0.191 (0 points) to 0.245 (100 points). 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
model.pth and val_data.npz, downloaded from Google Drive.
You submit
This notebook with your_solution(img, model) returning a heatmap.
Scoring
Mean IoU over the test set; points = clamp(100 × (mIoU − 0.191)/(0.245 − 0.191), 0, 100), rounded (as in compute_score).
Rules
  • Run without Internet access, with a GPU; the whole evaluation must take at most 3 minutes.
  • The model weights must not be modified.
  • Allowed libraries: torch, torchvision, numpy, cv2.
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 1
Language
Polish; English translation by SOTA
License
Not stated by the source