Discord

Checklist OAI 2026 Stage I · Task 5

Multispectral Data Segmentation

Polish title: Segmentacja danych multispektralnych

Segment 30×30 twelve-band satellite images into water, land, vegetation and industrial areas while using as few input channels as possible.

  • Vision
  • Semantic segmentation with channel selection
  • Polish original · English translation

The task

Multispectral satellite images record reflectance in many wavelength ranges; each material has a characteristic spectral signature, but the data are affected by atmospheric factors and sensor errors. The task has two stages: a preprocessing class (YourPreprocessing) that transforms the input without using labels (augmentation, channel selection or new derived features are allowed), and a segmentation model (YourModel) that assigns each pixel one of four classes — water, land, vegetation or industrial areas.

Contestants are encouraged to choose a minimal set of spectral bands that still gives high segmentation quality. Each image is 30×30 pixels with 12 channels and a per-pixel class mask. The training set has 48 images and the validation set 16; the hidden test set has 96.

The score combines the number of channels used at the model input and the segmentation quality measured by mean IoU, where for each image only the classes present in the ground truth are averaged.

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 its 2 files 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 1638 words and 13 code cells

Multispectral Data Segmentation

satellite

Image generated with the DeepAI image generation tool.

Introduction

The Earth is constantly changing — cities expand, forest boundaries shift, and glaciers melt. To understand these processes, scientists and engineers increasingly turn to satellite data. Such data allow us to observe our planet from space, monitor the state of the environment and detect changes that are often invisible to the "naked eye".

One of the key types of such data is multispectral imagery, that is, images captured in many different ranges of electromagnetic radiation. Each band provides different information about the Earth's surface, or more precisely, about the way in which a given material absorbs and reflects light. For example, in visible light we perceive the natural colours of plants and soil, whereas infrared makes it possible to assess the condition of vegetation or the moisture of the ground.

This technology enables non-invasive map-making, the analysis of farmland (e.g. with regard to fertility), tracking the progress of drought, as well as monitoring pollution or the effects of natural disasters. Multispectral images are today one of the foundations of modern remote sensing.

Every material on Earth — water, sand, concrete or grass - reflects radiation in its own characteristic way. We call this unique pattern a spectral signature. By analysing these signatures across different bands, we can precisely identify the objects visible in an image.

In practice, however, these data are often disturbed by atmospheric factors (clouds, water vapour, dust) and by small measurement errors of the sensors. Therefore, before multispectral images can be used for analysis, they must undergo appropriate processing and correction.

Task

Your task is to prepare a class that processes the dataset and to train a model that segments multispectral images (terrain maps).

The first stage of the task is to create a function that processes the input data in a way that maximises the model's performance (without using the labels). To increase the diversity of the training set and improve generalisation, you may apply various data augmentation techniques, such as rotation, scaling or adding noise (or other methods you consider appropriate).

While analysing the data, try to select a minimal set of bands (channels) that allows you to obtain high segmentation quality. Consider which channels carry the most information - for example: does infrared affect the detection of vegetation? Using all bands is not always necessary; selecting only those that contain the key information for the classes being distinguished often gives better results. Remember that you do not have to limit yourself to the raw channel values - in the processing step you may generate entirely new feature representations.

The second stage is to create a solution for the segmentation of multispectral images, that is, assigning each pixel one of four classes: water, land, vegetation or industrial areas (as in the example below). For this purpose you may use libraries such as PyTorch and scikit-learn.

segmentation_maps

You have at your disposal a labelled training set and validation set on which you can test your approach. The final evaluation of the model will be carried out on a hidden test set. Each input image consists of 12 spectral bands; however, the decision on how many of them, and which ones, you use is yours.

Data description

The data have been divided into separate sets:

  • training - 4848 samples (images),

  • validation - 1616 samples (images).

The final evaluation will use the test set, consisting of 9696 samples (images), to which you do not have access. Each sample is an image of size 30×3030 \times 30 pixels. Each pixel has an assigned class label (segmentation mask). Each pixel is described by 1212 channels corresponding to measurements of light reflectance in different wavelength ranges.

Scoring criterion

Your score depends on two factors: the segmentation quality on the hidden test set and the number of channels you decide to use as the model's input.

The first component of the scoring function relates to the number of channels used, N_channelsN\_channels. Originally, each pixel consists of 12 channels, and for using all of the channels you will receive 0 points for this part of the task. All solutions using at most 3 channels will receive full points for this component of the scoring function, whereas solutions using {4,5,6,...,11}\lbrace 4, 5, 6, ..., 11\rbrace channels will be scored according to a quadratic scale function:

channel_evaluation={0if N_channels12100if N_channels3100(12N_channels123)2otherwise.\mathtt{channel\_evaluation} = \begin{cases} 0 &\quad \text{if } N\_channels \geq 12 \\ 100 &\quad \text{if } N\_channels \leq 3 \\ 100 \cdot \left( \dfrac{12 - N\_channels}{12 - 3} \right)^2 &\quad \text{otherwise}. \end{cases}

For example, for using 10 channels you will receive only 5 points for this part of the task, multiplied by a coefficient of 0.250.25. The second component of the scoring function relates to the segmentation quality for the maps used in the task. For this we will use the IoU\text{IoU} (Intersection over Union) measure, which evaluates the ratio of the number of correctly classified pixels of a given class in a given image to the number of all pixels of that class present in the actual map (ground truth) or in the model's prediction. Finally, the mean over all classes and over all images in the test set will be computed. For each image, the mean will be taken only over the classes that appear in the ground truth.

IoU=1MNi=1Nj=1Minumber of correctly classified pixels of the j-th class in the i-th mapnumber of pixels of the j-th class in the combined ground-truth area and model prediction for the i-th map \text{IoU} = \dfrac{1}{M \cdot N} \cdot \sum\limits_{i=1}^{N} \sum\limits_{j=1}^{M_i} \dfrac{\text{number of correctly classified pixels of the j-th class in the i-th map}}{\text{number of pixels of the j-th class in the combined ground-truth area and model prediction for the i-th map }}

where NN is the number of images in a given set (e.g. the test set), and MiM_i is the number of classes actually present in the ii-th map. If the mean IoU\text{IoU} is no more than 0.6, you will receive 0 points for this part of the task, and if it is at least 0.8, you will receive the full number of points for this part of the task.

segmentation_evaluation={0if IoU0.6100if IoU0.8100IoU0.60.80.6otherwise\mathtt{segmentation\_evaluation} = \begin{cases} 0 &\quad \text{if } \text{IoU} \leq 0.6 \\ 100 &\quad \text{if } \text{IoU} \geq 0.8 \\ 100 \cdot \dfrac{\text{IoU} - 0.6}{0.8 - 0.6} &\quad \text{otherwise} \end{cases}

An example of computing IoU\text{IoU} for one of the classes (represented by blue squares) is shown in the image below. The final IoU\text{IoU} result is averaged over all classes present in a given image, and also over all images in the set.

IoU

The final score for the task will consist of 25%25\% of the score for the number of channels used and 75%75\% of the points for segmentation quality, according to the formula:

score=0.25channel_evaluation+0.75segmentation_evaluation\text{score} = 0.25 \cdot \mathtt{channel\_evaluation} + 0.75 \cdot \mathtt{segmentation\_evaluation}

WARNING! If IoU\text{IoU} is less than 0.5, you will receive 0 points for the whole task, regardless of the number of channels used!

Constraints

  • Your solution will be tested on the Contest Platform without Internet access, in an environment with a GPU.
  • Training the model and evaluating your final solution on the Contest Platform must not take longer than 5 minutes using the GPU.
  • The class performing the preliminary data transformation, YourPreprocessing, must not use the dataset labels in any way (it may only process the samples themselves), and the segmentation must be performed directly in the YourModel class.

Submission Files

This notebook, completed with your solution (see the YourModel class and the YourPreprocessing class), in which you prepare a pipeline consisting of a data transformation, with potential reduction and modification of channels, and a model that performs segmentation on the processed data.

Evaluation

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

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

Starter Code

In this section we initialise the environment by importing the required libraries and functions. The code provided will make it easier for you to work with the data efficiently and to build your solution.

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

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

import os
import random
from tqdm import tqdm

import numpy as np

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, TensorDataset, DataLoader

# Additional libraries that you may use in your solution
import xgboost
import sklearn

DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
DATA_DIR = "./data"
TRAIN_DATA_PATH = os.path.join(DATA_DIR, "train.npz")
VALID_DATA_PATH = os.path.join(DATA_DIR, "valid.npz")

assert torch.cuda.is_available(), "No graphics card (GPU) was found!"
######################### DO NOT CHANGE THIS CELL ###########################

seed = 12345

random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
os.environ["PYTHONHASHSEED"] = str(seed)
Loading the Data

Using the code below, we load the data containing the multispectral images and their corresponding segmentation masks. These data will be the basis for training and validating your segmentation model.

######################### DO NOT CHANGE THIS CELL ###########################
# Cell containing helper functions for preparing the data.

class BaseDataset(Dataset):
    """
    Multispectral dataset class.
    """
    def __init__(self, data_path: str):
        data = np.load(data_path)
        self.bands = data["bands"]
        self.segmentations = data["segmentations"]

    def __len__(self):
        """Returns the number of samples in the dataset."""
        return self.bands.shape[0]

    def __getitem__(self, idx):
        """Returns the sample with index idx - its multispectral bands and its segmentation."""
        bands = self.bands[idx]
        segmentations = self.segmentations[idx]

        bands = torch.from_numpy(bands).float()
        segmentations = torch.from_numpy(segmentations).long()

        return bands, segmentations

def setup_data():
    """
    Downloads the datasets used in the task.
    """
    import gdown
    os.makedirs(DATA_DIR, exist_ok=True)

    if not os.path.exists(TRAIN_DATA_PATH):
        url = "https://drive.google.com/uc?id=1Nfv3Kd9W8ypjr8RL1jY3VF3yTKU934lz"
        gdown.download(url, TRAIN_DATA_PATH, fuzzy=True)

    if not os.path.exists(VALID_DATA_PATH):
        url = "https://drive.google.com/uc?id=1WUwayYErm4CXI7zHUAmZmi23doIKmDm3"
        gdown.download(url, VALID_DATA_PATH, fuzzy=True)

if not FINAL_EVALUATION_MODE:
    setup_data()
Code with the Scoring Criterion

Code similar to the code below will be used to evaluate the solution on the test set.

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

def calculate_miou(y_true, y_pred):
    """
    Computes the mIoU (mean Intersection over Union) metric used to evaluate the model.
    Helper function used when evaluating the solution.
    """
    assert y_true.shape == y_pred.shape
    assert y_true.device == y_pred.device

    num_classes = 4
    device = y_true.device
    batch_size = y_true.shape[0]
    y_true_flat = y_true.reshape(batch_size, y_true.shape[1] * y_true.shape[2])
    y_pred_flat = y_pred.reshape(batch_size, y_pred.shape[1] * y_pred.shape[2])

    # The final mean is computed only over the classes present in the reference data (ground truth)
    intersections = torch.zeros((batch_size, num_classes), dtype=torch.float32, device=device)
    unions = torch.zeros((batch_size, num_classes), dtype=torch.float32, device=device)
    true_present = torch.zeros((batch_size, num_classes), dtype=torch.bool, device=device)

    for cls in range(num_classes):
        y_true_c = (y_true_flat == cls)
        y_pred_c = (y_pred_flat == cls)
        true_present[:, cls] = torch.any(y_true_c, dim=1)

        intersections[:, cls] = torch.sum(y_true_c & y_pred_c, dim=1).to(torch.float32)
        unions[:, cls] = torch.sum(y_true_c | y_pred_c, dim=1).to(torch.float32)

    # Computes the number of relevant classes for each sample
    num_present_classes = torch.sum(true_present, dim=1).to(torch.float32)

    # Although the computations are performed for all classes, we sum only the classes that actually occur (in the reference data)
    iou_per_class = torch.nan_to_num(intersections / unions, nan=0.0)
    sum_iou = torch.sum(iou_per_class * true_present, dim=1)
    miou_scores = torch.nan_to_num(sum_iou / num_present_classes, nan=0.0).tolist()
    assert len(miou_scores) == batch_size

    return miou_scores


def calculate_channel_count(dataloader):
    """
    Helper function used when evaluating the solution.
    Computes the number of bands used in training and evaluating the model.
    """
    tensors = dataloader.dataset.tensors

    if len(tensors) != 2:
        raise ValueError("The dataset should contain only labels and images (__getitem__ should return a tuple of size 2)")

    x, _ = tensors

    # x.shape [dataset size, channels, height, width]
    if x.ndim != 4:
        raise ValueError("The processed data must have 4 dimensions [batch size, channels, height, width]")

    if x.shape[0] < 15: #the validation set is the smallest and has 15 samples
        raise ValueError(f"The first dimension is reserved for the dataset size; your code should not change it!")

    if x.shape[2] != 30 or x.shape[3] != 30:
        raise ValueError("The processed data must have size 30x30 in the 3rd and 4th dimensions (.shape[2] == .shape[3] == 30)")

    channels_count = x.shape[1]
    return channels_count

def transform_dataset(preprocessing, dataset:BaseDataset) -> TensorDataset:
    """
    Processes the given dataset and keeps it in the computer's memory (RAM).
    Calls the .transform function, implemented by the participant, of the data preparation class.
    """
    processed_labels = []
    processed_images = []

    for raw_image, label in dataset:
        processed_labels.append(label.clone())

        transformed_image = preprocessing.transform(raw_image.clone())
        processed_images.append(transformed_image)

    labels_tensor = torch.stack(processed_labels)
    images_tensor = torch.stack(processed_images)
    memory_dataset = TensorDataset(images_tensor, labels_tensor)
    return memory_dataset

def evaluate(train_model, preprocessing, data_path: str):
    """
    Main function that evaluates the task.
    The same function will be called on the Contest Platform.

    1. Fits the data processing class on the training set.
    2. Processes the given dataset using the fitted class.
    3. Evaluates the model with the mIoU metric on the given processed dataset.
    4. Evaluates the number of bands used in evaluating the model - checks the shape of the data.
    """

    # Loads the training set and fits the data preparation class on it
    train_ds = BaseDataset(data_path=TRAIN_DATA_PATH)
    preprocessing = preprocessing()
    assert hasattr(preprocessing, "fit"), "The data preparation function must implement the .fit method"
    preprocessing.fit(train_ds)

    # Loads the target dataset and processes it using the data preparation class
    target_ds = BaseDataset(data_path=data_path)
    assert hasattr(preprocessing, "transform"), "The data preparation function must implement the .transform method"
    transformed_dataset = transform_dataset(preprocessing=preprocessing, dataset=target_ds)
    dataloader = DataLoader(transformed_dataset, batch_size=8, shuffle=False)

    model = train_model()

    if hasattr(model, "to") and callable(getattr(model, "to", None)):
        model = model.to(DEVICE)

    if hasattr(model, "eval") and callable(getattr(model, "eval", None)):
        model.eval()

    mious = []

    # Evaluates the model with the mIoU metric
    with torch.no_grad():

        for x, y in dataloader:
            x = x.to(DEVICE)
            y = y.to(DEVICE)

            y_pred = model(x)
            if hasattr(y_pred, "to") and callable(getattr(y_pred, "to", None)):
                y_pred = y_pred.to(DEVICE)
            if not torch.is_tensor(y_pred):
                y_pred = torch.tensor(y_pred)
            y_pred = torch.argmax(y_pred, dim=1)

            assert y_pred.shape == y.shape
            assert y_pred.max() <= 4 and y_pred.min() >= 0

            miou = calculate_miou(y, y_pred)
            mious.extend(miou)

    # Evaluates the number of bands used in evaluating the model
    channels_count = calculate_channel_count(dataloader=dataloader)

    # Computes the mean mIoU over all samples
    miou = sum(mious) / len(mious)

    return miou, channels_count

def compute_score(miou, channels_count):
    """
    Computes the score for the task: calculates the final number of points for the task using the formula given in the task statement.
    The same function will be called on the Contest Platform.
    """
    band_score = max(0, min(100, 100 * ((12 - channels_count) / (12 - 3))**2))
    miou_score = max(0, min(100, 100 * (miou - 0.6) / (0.8 - 0.6)))

    print(f"Number of channels: {channels_count}")
    print(f"mIoU: {miou:.3f} \n")

    print(f"Channel score: {band_score:.3f}")
    print(f"mIoU score: {miou_score:.3f}")

    if miou < 0.5:
        total_score = int(0)
    else:
        total_score = 0.25 * band_score + 0.75 * miou_score
        total_score = int(round(total_score))
    print(f"Estimated number of points for the task: {total_score}")
    return total_score

Example Solution

Below we present a simplified solution based on linear regression, which can serve both as an example demonstrating how the notebook works and as a starting point for creating your solution.

######################### DO NOT CHANGE THIS CELL ###########################
class BasicPreprocessing():
  """
  Example implementation of a dataset processing class.

  This is only an example solution, and it does not reduce the number of channels.
  The fewer channels you use, the more points you will receive for this part of the task.
  """

  def __init__(self):
    self.std_per_channel = None

  def fit(self, dataset:BaseDataset):

    # Collects all images into a single matrix [N_samples, 12, 30, 30]
    images = []
    for item in dataset:
        image_tensor, _ = item
        images.append(image_tensor)
    images_tensor = torch.stack(images)  # shape: [N, 12, 30, 30]

    # Computes the std (standard deviation) over the dimensions: (0: sample, 2: H, 3: W)
    self.std_per_channel = images_tensor.std(dim=(0, 2, 3))

    return self

  def transform(self, image_tensor: torch.Tensor) -> torch.Tensor:
      # Reshapes the mean to [12, 1, 1] to allow broadcasting against [12, 30, 30].
      # Remember, this is only an example solution; merely dividing by the std is not the most effective solution.
      std_reshaped = self.std_per_channel.view(-1, 1, 1)

      return image_tensor / std_reshaped
######################### DO NOT CHANGE THIS CELL ###########################

class BasicModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(12, 4)

    def forward(self, bands):
        # bands.shape [b, c, h, w]
        return self.linear(bands.permute(0, 2, 3, 1)).permute(0, 3, 1, 2)
Training the Example Model
######################### DO NOT CHANGE THIS CELL ###########################

def train_basic_model():

    epochs = 40
    lr = 0.001
    batch_size = 8
    model = BasicModel()
    model = model.to(DEVICE)

    # Prepares the data preprocessing parameters using only
    # the training set.

    raw_train_ds = BaseDataset(data_path=TRAIN_DATA_PATH)
    raw_valid_ds = BaseDataset(data_path=VALID_DATA_PATH)

    preprocessing = BasicPreprocessing()
    preprocessing.fit(raw_train_ds)

    train_ds = transform_dataset(preprocessing=preprocessing, dataset=raw_train_ds)
    valid_ds = transform_dataset(preprocessing=preprocessing, dataset=raw_valid_ds)
    train_dataloader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)
    valid_dataloader = DataLoader(valid_ds, batch_size=batch_size, shuffle=False)

    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    criterion = nn.CrossEntropyLoss()

    for epoch in range(epochs):
        model.train()
        for x, y in tqdm(train_dataloader, total=len(train_dataloader), desc="Training"):
            x = x.to(DEVICE)
            y = y.to(DEVICE)

            y_pred = model(x)
            loss = criterion(y_pred, y)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

        model.eval()
        with torch.no_grad():
            valid_loss = 0
            mious = []
            for x, y in tqdm(valid_dataloader, total=len(valid_dataloader), desc="Validation"):
                x = x.to(DEVICE)
                y = y.to(DEVICE)

                y_pred = model(x)
                loss = criterion(y_pred, y)

                valid_loss += loss.item()

                y_pred = torch.argmax(y_pred, dim=1)
                miou = calculate_miou(y, y_pred)
                mious.extend(miou)

            valid_loss = valid_loss / len(valid_dataloader)
            print(f"Epoch {epoch+1} loss: {valid_loss}, mIoU: {sum(mious) / len(mious)}")

    return model
Evaluation of the Example Solution
######################### DO NOT CHANGE THIS CELL ###########################

if not FINAL_EVALUATION_MODE:
    miou, channels_count = evaluate(train_basic_model, BasicPreprocessing, VALID_DATA_PATH)
    print("-"*50)
    compute_score(miou, channels_count)

Your solution

Place your solution in this section. Make changes only here!

# Do not change the name of the class
# This class may only process the samples; it must not use the data labels.
# No method may perform segmentation either.

class YourPreprocessing():
  def __init__(self):
    pass

  def fit(self, dataset: BaseDataset):
    return dataset

  def transform(self, image_tensor: torch.Tensor) -> torch.Tensor:
    return image_tensor
class YourModel(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, bands):
        # random predictions
        segmentation = torch.randint(0, 4, (bands.shape[0], 1, 30, 30))
        # Performs one-hot encoding because evaluate uses torch.argmax
        one_hot = torch.zeros(bands.shape[0], 4, 30, 30)
        one_hot.scatter_(1, segmentation, 1)
        return one_hot
# Do not change the name of the function
def train_your_model():
    return YourModel()

Evaluation

Running the cell below lets you check how many points your solution would earn on the validation data. On the Contest Platform, your solution will be evaluated on the test set.

Before submitting, make sure that the whole notebook runs from start to finish without errors and without user intervention after executing the Run All command.

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

if not FINAL_EVALUATION_MODE:
    miou, channels_count = evaluate(train_your_model, YourPreprocessing, VALID_DATA_PATH)
    print("-"*50)
    compute_score(miou, channels_count)

Remember: During grading, the model (the model training function) and the data processing function will be evaluated on the test set, not the validation set!

Translated by SOTA. The Polish original is the official version and wins wherever the two differ. In the official solution, the class names and the 'Klasa' (class) column used by the analysis code stay in Polish and are glossed in comments. The saved plot images come from the original run, so their labels are in Polish. 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
Training and validation arrays (bands and masks), downloaded from Google Drive.
You submit
This notebook with YourPreprocessing and YourModel (training and segmentation).
Scoring
channel_evaluation = 0 if N_channels ≥ 12, 100 if N_channels ≤ 3, else 100·((12 − N_channels)/9)². segmentation_evaluation = 0 if mIoU ≤ 0.6, 100 if mIoU ≥ 0.8, else 100·(mIoU − 0.6)/0.2. score = 0.25·channel_evaluation + 0.75·segmentation_evaluation; the whole task scores 0 if mIoU < 0.5.
Rules
  • Tested without Internet access, with a GPU; training and evaluation must take at most 5 minutes with a GPU.
  • YourPreprocessing may not use the labels in any way; segmentation must be performed directly in YourModel.
  • Processed data must keep spatial dimensions 30×30.
Format
Stage I (online), 1 December 2025 – 25 January 2026; up to 100 points per task (500 in total; the qualification threshold for Stage II was 350 points). Tasks are ordered by intended increasing difficulty. 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, Online
Round
Stage I · Task 5
Language
Polish; English translation by SOTA
License
Not stated by the source