Checklist OAI 2026 Stage II · Task 4
Colourisation with a GAN
Polish title: Kolorowanie z GANem
Colourise 256×256 greyscale face photographs by exploiting a pre-trained StyleGAN face generator, without any training data.
The task
The introduction covers image enhancement, the greyscale conversion x_gray = 0.299·R + 0.587·G + 0.114·B, the ill-posed nature of colourisation, and generative adversarial networks, including the StyleGAN architecture with a mapping network f (z → w, 512 dimensions) and a synthesis network g conditioned on w that produces 256×256 faces.
Given only the pre-trained StyleGAN generator, the contestant proposes a method to colourise black-and-white face photographs of the same resolution, extracting the knowledge contained in the generator's weights for a task the generator was not trained for. The solution is a YourModel class whose fit method prepares the colourising model and whose predict method colourises x_gray.
There is no training data; only validation data are provided, with 500 paired greyscale and colour images.
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

The images were generated with the Flux-dev model and then edited by the author of the task.
Introduction
The subject of this task is image quality improvement (image enhancement), a collective term covering activities such as:
- increasing the resolution (image super-resolution),
- denoising (image denoising),
- sharpening (image deblurring),
- brightening (low-light image enhancement),
- colourisation (image colorization),
- and many others.
The aim of these techniques is to obtain a high-quality image from a low-quality photograph. This process often requires filling in missing information in the image on the basis of its context, which is why deep learning methods are commonly used.
Thanks to such techniques, we can in a sense "look" into the past from a new perspective. An example is the photograph of Warsaw below, from the early interwar period – on the left in the original, and on the right in a version colourised by Mariusz Zając.

In this task we will focus on colourising black-and-white photographs showing human faces.
Colourising photographs
A colour photograph is described by three RGB channels (red, green, blue). It can easily be converted into a monochrome image described by a single channel. However, the conversion to greyscale does not use a simple arithmetic mean of the RGB values, because that does not correspond to the way the human eye perceives the brightness of individual colours (e.g. we perceive blue light as darker than green light). For this reason, the pixels of a greyscale image are computed according to the formula:
It is worth noting that the conversion from the RGB space to greyscale is unambiguous and easy to perform. The reverse operation – recovering the colour information – is not unambiguous, because many different combinations of RGB values can lead to the same greyscale value. This means that image colourisation is an ill-posed task, which implies that many correct colour versions can be generated for a single black-and-white photograph.
For example, a black-and-white photograph of a car is difficult to colourise unambiguously – the vehicle could have been almost any colour. On the other hand, models trained on suitable data are able to exploit statistical regularities – such as the fact that grass is usually green, the sky blue, and a tiger orange and black – in order to generate the most probable colour versions.
Formally, we define the colourisation task as learning a model with parameters , which takes a greyscale image as input and generates its colour version as output:
We would like the model's predictions to resemble the original colour images as closely as possible. To this end, we seek values of the parameters that minimise some measure of difference (e.g. the mean squared error) between the predictions and the real images:
where denotes the number of examples in the training set.
Generative adversarial networks
Generative adversarial networks (GANs) were one of the first approaches to address the problem of generating data using deep learning techniques. Since the generation process does not require any input to the network, it is not possible to propose a loss function in a simple way, because we do not know what output to expect from the network. For example, the network may generate a plausible image of a face, but when we compare it with another random photograph of a face, the penalty will be very large, even though the network's output is correct.
For this reason, the GAN architecture consists of two networks: a generator and a discriminator.
- The role of the generator is to generate new, random images.
- The role of the discriminator is to predict whether the input image comes from the dataset or was generated by the generator.
The generator tries to fool the discriminator (it maximises the discriminator's classification error), whereas the discriminator tries to guess whether the image is artificial or not (it minimises its classification error). The two networks are trained alternately until convergence is reached.
In this task we will not be training a GAN; instead, we will have at our disposal a previously trained generator model which, from noise, can generate a random image of a face with a resolution of . The architecture of this network is called StyleGAN and is presented in the diagram below. It is divided into two modules: a mapping network and a generating network .
The network is an MLP architecture that transforms a random vector , with elements whose values are drawn from the normal distribution , into a -dimensional vector , which is used to condition the network . The vector can be interpreted as a latent representation of the generated image.
The network is a convolutional network with a constant input, which progressively increases its resolution up to . In each block, the network is conditioned on the vector , which ensures the diversity of the generated images. In addition, some noise is also fed into the network, which increases the diversity even further.

Task
In this task, given a trained generator capable of generating face images with a resolution of , you must propose a method for colourising black-and-white face photographs of the same resolution. The aim here is to extract the knowledge that lies dormant in the generator's weights and to use it in a completely different task, previously unknown to the generator.
Data
No training data are available in this task, only validation data for a preliminary assessment of the approach you propose. The validation data contain 500 black-and-white photographs paired with their colour counterparts.
Scoring criterion
To score the quality of your solution, a metric consisting of two sub-metrics will be used: PSNR and LPIPS.
PSNR is inversely proportional to the mean squared error between the generated sample and the real colour image . It is given by the formula
where is the range of possible values that the images and can take. We want to maximise PSNR. The scored range of values of this metric is - points are awarded proportionally.
LPIPS is the mean distance between the feature maps after selected layers of the AlexNet network, obtained for the inputs and . We want to minimise LPIPS.
The scored range of values of this metric is - points are awarded proportionally.
The final point value is the weighted mean of the points obtained from the PSNR and LPIPS metrics, with weights (PSNR) and (LPIPS).
Constraints
- Your solution will be tested on the Contest Platform without internet access and in an environment with a GPU.
- The evaluation of your final solution on the Contest Platform must not take longer than 5 minutes with a GPU.
- List of permitted libraries: torch, numpy, torchvision, pillow.
Submission files
You must submit only this notebook, completed with your solution (see the class YourModel).
Evaluation
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 ##########################
FINAL_EVALUATION_MODE = False
######################### DO NOT CHANGE THIS CELL ##########################
import os
import torch
import tarfile
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import torchvision.transforms as T
from PIL import Image
from io import BytesIO
from torch.utils.data import Dataset, DataLoader
from torchmetrics.image import PeakSignalNoiseRatio as PSNR
from torchmetrics.image.lpip import LearnedPerceptualImagePatchSimilarity as LPIPS
from stylegan import load_generator
RANDOM_SEED = 1
os.environ["PYTHONHASHSEED"] = str(RANDOM_SEED)
np.random.seed(RANDOM_SEED)
torch.manual_seed(RANDOM_SEED)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
The cell below contains the functions that score and visualise your solutions, as well as the validation dataset.
######################### DO NOT CHANGE THIS CELL ##########################
import math
def round_half_up(number: float) -> int:
return int(math.floor(number + 0.5))
def plot_batch(batch):
""" Function that displays a batch of images (generated or real). It shows at most 8 images """
to_show = min(len(batch), 8)
batch = batch.to('cpu')
batch = batch * 0.5 + 0.5
batch = batch.clamp(0, 1)
batch = batch.permute(0, 2, 3, 1).numpy()
fig, axes = plt.subplots(1, to_show, figsize=(to_show * 3, 3))
if to_show == 1:
axes = [axes]
for ax, img in zip(axes, batch[:to_show]):
ax.imshow(img)
ax.axis('off')
plt.tight_layout()
plt.show()
class TarImageDataset(Dataset):
""" Dataset class used to validate your method """
def __init__(self, tar_path):
self.tar_path = tar_path
self.tar = tarfile.open(tar_path, 'r')
self.gt_paths = sorted([m.name for m in self.tar.getmembers() if m.name.startswith('data/GT/') and m.name.endswith('.jpeg')])
self.gray_paths = [p.replace('GT', 'GRAY') for p in self.gt_paths]
self.transform = T.Compose([T.ToTensor(), T.Normalize(mean=[0.5], std=[0.5])])
def __len__(self):
return len(self.gt_paths)
def __getitem__(self, idx):
""" returns the monochrome image and the colour image """
gt_member = self.tar.getmember(self.gt_paths[idx])
gray_member = self.tar.getmember(self.gray_paths[idx])
gt_image = Image.open(BytesIO(self.tar.extractfile(gt_member).read())).convert('RGB')
gray_image = Image.open(BytesIO(self.tar.extractfile(gray_member).read())).convert('L')
return (
self.transform(gray_image),
self.transform(gt_image)
)
def __del__(self):
""" Closes the file when an object of this class is deleted """
if hasattr(self, 'tar') and self.tar:
self.tar.close()
def validate_solution(your_model, device, split="val"):
dataset = TarImageDataset(f"./data/{split}.data")
dataloader = DataLoader(dataset, batch_size=16, shuffle=False)
lpips_metric = LPIPS(net_type='alex').to(device)
psnr_metric = PSNR(data_range=2.0).to(device)
your_model.eval()
with torch.no_grad():
for x_gray, x_gt in dataloader:
x_gray = x_gray.to(device)
x_gt = x_gt.to(device)
x_pred = your_model.predict(x_gray).clamp_(-1, 1)
psnr_metric.update(x_pred, x_gt)
lpips_metric.update(x_pred, x_gt)
avg_psnr = psnr_metric.compute()
avg_lpips = lpips_metric.compute()
print("PSNR: ", avg_psnr)
print("LPIPS:", avg_lpips)
PSNR_MIN, PSNR_MAX = 22, 26
LPIPS_MIN, LPIPS_MAX = 0.11, 0.15
psnr_points = ((torch.clamp(avg_psnr, PSNR_MIN, PSNR_MAX) - PSNR_MIN) / (PSNR_MAX - PSNR_MIN)).item()
lpips_points = ((LPIPS_MAX - torch.clamp(avg_lpips, LPIPS_MIN, LPIPS_MAX)) / (LPIPS_MAX - LPIPS_MIN)).item()
total_points = psnr_points * 0.25 + lpips_points * 0.75
psnr_points = round_half_up(psnr_points * 100)
lpips_points = round_half_up(lpips_points * 100)
total_points = round_half_up(total_points * 100)
return psnr_points, lpips_points, total_points
def show_image_grid(inputs, predictions, targets):
"""
function that visualises the inputs, the predictions and the target images
"""
def tensor_to_numpy(img):
img = (img.clamp(-1, 1) + 1) / 2
img = img.cpu().numpy()
if img.shape[0] == 1:
return img.squeeze(0)
return np.transpose(img, (1, 2, 0))
titles = ["Inputs", "Model predictions", "Target images"]
images = [inputs, predictions, targets]
fig, axes = plt.subplots(3, 4, figsize=(16, 10))
for row in range(3):
for col in range(4):
img = tensor_to_numpy(images[row][col])
cmap = 'gray' if images[row].shape[1] == 1 else None
axes[row, col].imshow(img, cmap=cmap)
axes[row, col].axis('off')
axes[row, 0].text(-0.2, 0.5, titles[row], va='center', ha='right',
fontsize=14, transform=axes[row, 0].transAxes)
plt.suptitle("Example images from the validation set")
plt.tight_layout()
plt.show()
def show_score_bars(psnr_score, lpips_score, task_score):
"""
Function that visualises the points obtained in the form of bar charts
"""
labels = ["PSNR points", "LPIPS points", "task points"][::-1]
values = [psnr_score, lpips_score, task_score][::-1]
plt.style.use('ggplot')
fig, ax = plt.subplots(figsize=(16, 4))
y = np.arange(len(labels))
norm = mcolors.Normalize(vmin=0, vmax=100)
cmap = plt.get_cmap('RdYlGn')
colors = [cmap(norm(v)) for v in values]
ax.barh(y, values, color=colors)
ax.set_xlim(0, 100)
ax.set_yticks(y)
ax.set_yticklabels(labels)
ax.set_xlabel("Points")
ax.set_title("Model scores")
for i, v in enumerate(values):
ax.text(v + 1, i, f"{v:.1f}", va='center', fontsize=10)
plt.tight_layout()
plt.show()
# Reset to default style
plt.style.use('default')
def benchmark_solution(your_model, device, split="val"):
"""
Function that validates the model on the validation data and visualises the results obtained
"""
dataset = TarImageDataset(f"./data/{split}.data")
dataloader = DataLoader(dataset, batch_size=4, shuffle=False)
x_gray, x_gt = next(iter(dataloader))
x_pred = your_model.predict(x_gray.to(device)).cpu().clamp_(-1, 1)
psnr_points, lpips_points, total_points = validate_solution(your_model, device, split)
show_image_grid(x_gray, x_pred, x_gt)
show_score_bars(psnr_points, lpips_points, total_points)
######################### DO NOT CHANGE THIS CELL ##########################
latent_dim = 512 # size of the latent representation of the StyleGAN model
size = 256 # resolution of the images generated by the StyleGAN model
device = 'cuda' if torch.cuda.is_available() else 'cpu'
if device == 'cpu':
print('Warning: no GPU on the machine!')
# Loading the StyleGAN generator.
# Its implementation is in the file stylegan.py.
# A detailed analysis of the model code is not forbidden, but it is not recommended
generator = load_generator()
generator.to(device)
print('The model has been loaded correctly')
The cell below contains code for generating faces with the StyleGAN model. The generation is presented in two versions. The first version is more detailed and consists of the following steps:
- first, we generate from the normal distribution
- next, through the
get_latentmethod, we use the model to generate the vectors - finally, we call the
style_to_imagemethod, which uses the network to generate face images conditioned on the vector .
Alternatively, we can generate images with the forward method, which combines steps 2 and 3.
######################### DO NOT CHANGE THIS CELL ##########################
if not FINAL_EVALUATION_MODE:
with torch.no_grad():
z = torch.randn(8, latent_dim, device=device)
w = generator.get_latent(z)
sample = generator.style_to_image(w)
plot_batch(sample)
z = torch.randn(8, latent_dim, device=device)
sample = generator.forward(z)
plot_batch(sample)
Your solution
Implement your solution in the cell below. Make sure that the fit method prepares the model that colourises the images, while the predict method uses the model to colourise the input data x_gray.
class YourModel(torch.nn.Module):
def __init__(self, generator):
super().__init__()
self.generator = generator
def fit(self):
# Fit the image colourisation method here
pass
def predict(self, x_gray):
# Use your method here to colourise the images x_gray
return x_gray.repeat(1, 3, 1, 1) # by default, returns the input batch as RGB
######################### DO NOT CHANGE THIS CELL ##########################
your_model = YourModel(generator)
your_model.fit()
if not FINAL_EVALUATION_MODE:
benchmark_solution(your_model, device)
Translated by SOTA. The Polish original is the official version and wins wherever the two differ. The notebook needs stylegan.py and data/val.data, which sit next to the original notebook; stylegan.py downloads the generator weights itself. 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/val.data(validation pairs) andstylegan.pyin the task folder; the generator checkpoint is downloaded from Google Drive bystylegan.py.- You submit
- This notebook with the YourModel class.
- Scoring
- PSNR (
data_range2.0) scored linearly over (22.0, 26.0) and LPIPS (AlexNet) scored linearly over (0.15, 0.11); final points = 100 × (0.25·PSNR points + 0.75·LPIPS points), each component clamped to [0, 1]. - Rules
- Tested without Internet access, with a GPU; evaluation must take at most 5 minutes with a GPU.
- Allowed libraries: torch, numpy, torchvision, pillow.
- 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.