Discord

Checklist OAI 2026 Stage II · Task 1

Painter's Optimiser

Polish title: Optymalizator malarza

Recover the parameters of ten overlapping semi-transparent coloured circles that reproduce a given image through a differentiable renderer.

  • Vision
  • Inverse rendering by optimisation
  • Polish original · English translation

The task

An art student paints abstract compositions of many overlapping semi-transparent coloured circles and asks the inverse question: can the parameters of all circles be recovered from the finished image? The contestant writes YourSolution.solve(), which for each image finds parameters params describing all circles.

A function differentiable_renderer(params, H, W) is provided: given a tensor of shape [N, 7] it returns a [3, H, W] image of N overlapping circles, and it is differentiable, so it can be used by gradient-based optimisation. Each circle has 7 parameters: position_X, position_Y, r, color_R, color_G, color_B and alpha; every image consists of exactly N = 10 circles, and exactly N circles must be returned.

The goal is to find parameters whose rendering is as similar as possible to the input image.

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 6391 words and 8 code cells

Painter's Optimiser

thumbnail.jpg

Image generated with Nano Banana 2 (Gemini 3 PRO).

Introduction

Norbert is a young painting student who is particularly fascinated by abstract art. As part of his latest experiment, he created a composition made up of many semi-transparent, coloured circles overlapping one another. Fascinated by the final effect, Norbert began to wonder about the inverse problem: is it possible, on the basis of a finished picture, to precisely recover the parameters of all the circles that make it up?

Your task is to help the young painter Norbert develop an algorithm that performs such a decomposition and finds all the parameters of the circles from which the painted picture can be reconstructed.

Task

Your main task is to write the function solve() in the class YourSolution. For each image, this function should find a set of parameters params that describe all the coloured circles forming the given image.

You have at your disposal the function differentiable_renderer. It takes:

  • params (torch.Tensor) - A tensor of shape [N, 7], where N is the number of circles and 7 is the number of parameters of a circle.
  • H (int) - The height of the output image.
  • W (int) - The width of the output image.

The function returns a PyTorch tensor of size [3, H, W], which represents a colour image composed of N overlapping circles. The name of the function differentiable_renderer is not accidental. It means that the process of creating the image is mathematically "transparent" to optimisation algorithms. The exact code of this function and example images can be seen in the cells below.

Your goal is to find parameters params such that, after using differentiable_renderer, the resulting image is as similar as possible to the original. The measure of success will be the minimisation of the error between your reconstruction and the original. In other words, your goal is to find circle parameters which, once rendered, reproduce the input image as well as possible. For each image you must return the parameters of exactly N circles.

Data

The dataset contains images generated by this function, each in the form of a tensor with dimensions [3, H, W].

Each image, in both the validation and the test data, consists of N overlapping circles, where N is a constant value equal to 10. Each circle is defined by 7 parameters:

  • position_X - the X coordinate of the circle
  • position_Y - the Y coordinate of the circle
  • r - the radius of the circle
  • color_R - the value of the red colour
  • color_G - the value of the green colour
  • color_B - the value of the blue colour
  • alpha - the transparency of the circle

You have at your disposal two datasets saved in the file train_validation_sets.npz:

  • x_train - 50 training images [50, 3, H, W]
  • x_val - 10 validation images [10, 3, H, W]
  • y_train - the true circle parameters for the training images [50, N, 7]
  • y_val - the true circle parameters for the validation images [10, N, 7]

The test data x_test and y_test, which will be used later for the final scoring of your solution, have shapes [10, 3, H, W] and [10, N, 7]. You do not have access to them, and they are hidden on the grading system.

Scoring Criterion

The quality of your solution will be scored using the MSE (Mean Squared Error) metric, which is computed as the mean of the squared differences between corresponding pixels (tensor elements) of the original and the reconstructed image.

MSE=1Mi=1M(OiRi)2\text{MSE} = \frac{1}{M} \sum_{i=1}^{M} (O_i - R_i)^2

Where MM is the total number of all elements in the tensor representing the image (in our case M=3×H×WM = 3 \times H \times W). OO is the image from the dataset, and RR is the image generated from the parameters returned by the solution, R = differentiable_renderer(params, H, W).

A lower MSE value means a better fit and a higher reconstruction quality; a value of 0.0 means that the images are identical.

You can score between 0 and 100 points for this task. The score will be scaled linearly depending on the mean MSE achieved on the whole test set:

  • Mean MSE ≥ 0.02: 0 points
  • Mean MSE ≤ 0.01: 100 points
  • Values between 0.01 and 0.02: scaled linearly.

The final score is calculated according to the formula:

Points={0for Mean MSE0.02100×0.02Mean MSE0.020.01for 0.01<Mean MSE<0.02100for Mean MSE0.01\text{Points} = \begin{cases} 0 & \text{for } \text{Mean MSE} \geq 0.02 \\ 100 \times \frac{0.02 - \text{Mean MSE}}{0.02 - 0.01} & \text{for } 0.01 < \text{Mean MSE} < 0.02 \\ 100 & \text{for } \text{Mean MSE} \leq 0.01 \end{cases}

Constraints

  • Your solution will be tested on the Contest Platform without internet access and in an environment without a GPU.
  • The evaluation of your final solution on the Contest Platform must not take longer than 5 minutes without a GPU.
  • List of permitted libraries: scikit-learn, numpy, pandas, pytorch.
  • For each image you must return the parameters of exactly N circles.

Submission Files

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

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

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

######################### 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 random
import numpy as np
import torch
import torch.nn.functional as F

if not FINAL_EVALUATION_MODE:
    import matplotlib.pyplot as plt


# Setting the seed of the pseudo-random number generator to ensure deterministic results
seed = 42
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)


# Global parameters of the images and the number of shapes that will be used in the task
H, W = 64, 64
N_SHAPES = 10

The differentiable_renderer function

We define our function differentiable_renderer

######################### DO NOT CHANGE THIS CELL WHEN SUBMITTING ##########################
def differentiable_renderer(params, H, W):
    """
    Differentiably renders circles onto an image on the basis of the given parameters.

    Args:
        params (torch.Tensor): A tensor of shape [N, 7], where N is the number of circles.
                               Each row contains 7 parameters in the following order:
                               [position_X, position_Y, r, color_R, color_G, color_B, alpha]
        H (int): The height of the output image.
        W (int): The width of the output image.

    Returns:
        torch.Tensor: The rendered image as a tensor of shape [3, H, W].
    """
    sharpness = 20.0
    if params is None or params.shape[0] == 0:
        return torch.ones(3, H, W)
    N = params.shape[0]
    y_coords, x_coords = torch.meshgrid(torch.arange(H, dtype=torch.float32), torch.arange(W, dtype=torch.float32), indexing='ij')
    centers_x, centers_y = params[:, 0].view(N, 1, 1), params[:, 1].view(N, 1, 1)
    radii_sq = params[:, 2].view(N, 1, 1)**2
    colors = params[:, 3:6].view(N, 3, 1, 1)
    alphas = params[:, 6].view(N, 1, 1, 1)
    dist_sq = (x_coords - centers_x)**2 + (y_coords - centers_y)**2
    mask = torch.sigmoid((radii_sq - dist_sq) * sharpness)
    pixel_alpha = mask.unsqueeze(1) * alphas
    image = torch.ones(3, H, W)
    for i in range(N):
        image = colors[i] * pixel_alpha[i] + image * (1.0 - pixel_alpha[i])
    return image

Loading the Data

The code below loads the data and prepares it appropriately.

######################### DO NOT CHANGE THIS CELL WHEN SUBMITTING ##########################
def setup_data_from_npz(path_to_npz: str, mode: str):

    bundle_npz = np.load(path_to_npz, allow_pickle=True)
    if mode == "train":
        x_bundle_name, y_bundle_name = "X_train", "y_train"
    elif mode == "val":
        x_bundle_name, y_bundle_name = "X_validation", "y_validation"

    x = torch.from_numpy(bundle_npz[x_bundle_name]).float()
    y = torch.from_numpy(bundle_npz[y_bundle_name]).float()

    return x, y

if not FINAL_EVALUATION_MODE:
    x_train, y_train = setup_data_from_npz("train_validation_sets.npz", "train")
    x_val, y_val = setup_data_from_npz("train_validation_sets.npz", "val")

Data visualisation

Using the matplotlib library, we can display example images from the training and validation sets

######################### DO NOT CHANGE THIS CELL WHEN SUBMITTING ##########################
import matplotlib.pyplot as plt

def display_sample(data_tensor, title="Example image"):
    """
    Displays a single image from a PyTorch tensor.
    Matplotlib expects images in the [H, W, C] format, so we have to transform the tensor from the [C, H, W] format,
    where C is the number of channels (in our case 3 - RGB), H is the height and W is the width of the image.
    """
    img = data_tensor.permute(1, 2, 0).numpy()
    plt.figure(figsize=(5, 5))
    plt.imshow(img)
    plt.title(title)
    plt.axis('off')
    plt.show()


if not FINAL_EVALUATION_MODE:
    display_sample(x_train[0], title="Example image from the training set")
    display_sample(x_val[0], title="Example image from the validation set")

Code with the Scoring Criterion

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

######################### DO NOT CHANGE THIS CELL WHEN SUBMITTING ##########################
def compute_score(mse: float) -> float:
    """
    - MSE <= 0.01 receives 100 points.
    - MSE >= 0.02 receives 0 points.
    - The score is linearly interpolated between these thresholds.
    """
    lower_bound_mse = 0.01
    upper_bound_mse = 0.02

    if mse <= lower_bound_mse:
        return 100.0
    if mse >= upper_bound_mse:
        return 0.0

    # Linear interpolation
    score = 100.0 * (upper_bound_mse - mse) / (upper_bound_mse - lower_bound_mse)
    return score


def evaluate_solution(solution, ds) -> float:
    """Scores the solution on the given dataset."""
    total_mse = 0.0
    for i in range(ds.shape[0]):
        target_image = ds[i]

        # Running the algorithm
        final_params = solution.solve(target_image.clone())
        assert final_params.shape == (N_SHAPES, 7), \
            f"The returned tensor has an incorrect shape: {final_params.shape}, expected ({N_SHAPES}, 7)"

        # Scoring
        final_image = differentiable_renderer(final_params, H, W)
        mse = F.mse_loss(final_image, target_image).item()
        total_mse += mse

    average_mse = total_mse / ds.shape[0]
    return average_mse

Your Solution

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

class YourSolution():
    def solve(self, target_image: torch.Tensor) -> torch.Tensor:
        """
        A method which, for a given target image, should run an optimisation
        algorithm and return the final tensor with the circle parameters.

        Args:
            target_image (torch.Tensor): The target image tensor of shape [3, H, W].

        Returns:
            torch.Tensor: A tensor with the optimised circle parameters, of shape [N_SHAPES, 7].
        """
        # An example solution that returns random parameters for each individual image
        # Remove all the code below in order to implement your solution
        with torch.no_grad():
            params = torch.rand(N_SHAPES, 7) # Random parameter values
        return params

Evaluation

Running the cell below lets you check how many points your solution would score on the validation data. 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.

######################### DO NOT CHANGE THIS CELL WHEN SUBMITTING ##########################
if not FINAL_EVALUATION_MODE:
    your_solution = YourSolution()
    mse = evaluate_solution(your_solution, x_val)
    score = compute_score(mse)
    print("Evaluation of the model's performance on the validation set for YourSolution:")
    print(f"Mean squared error (MSE): {mse: .6f}")
    print(f"Score: {score} pts")

Translated by SOTA. The Polish original is the official version and wins wherever the two differ. The data file train_validation_sets.npz sits next to the original notebook. Its arrays are named X_train, y_train, X_validation and y_validation; the loading code turns them into x_train, y_train, x_val and y_val. 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
train_validation_sets.npz in the task folder: x_train [50, 3, H, W], y_train [50, N, 7], x_val [10, 3, H, W], y_val [10, N, 7]; the hidden test set has 10 images.
You submit
This notebook with YourSolution.solve returning a [10, 7] tensor per image.
Scoring
Mean over test images of the pixel-wise MSE between the image and differentiable_renderer(params, H, W). Points = 0 at MSE ≥ 0.02, 100 at MSE ≤ 0.01, 100 × (0.02 − MSE)/(0.02 − 0.01) in between.
Rules
  • Tested without Internet access and without a GPU; evaluation must take at most 5 minutes without a GPU.
  • Allowed libraries: scikit-learn, numpy, pandas, pytorch.
  • Exactly N circles must be returned for every image.
Format
Stage II (regional, on site in Kraków, Poznań, Warsaw and Wrocław), 13–15 March 2026; two tasks per day in 5-hour sessions. 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, Kraków, Poznań, Warsaw and Wrocław, Poland
Round
Stage II · Task 1
Language
Polish; English translation by SOTA
License
Not stated by the source