Discord

Checklist IOAI Indonesia 2025 OSN 2025 AI Exhibition – Final · P3 task

Face or Flag?

Indonesian title: Computer Vision Task: Face or Flag?

Classify 64×64 emoji images as faces or flags using hand-designed features.

  • Vision
  • Binary image classification
  • Indonesian original · English translation

The task

The notebook introduces pixels, the RGB model and colour histograms. The training set has 160 emoji images (64×64, RGB) labelled 1 = Faces or 2 = Flags; the test set has 310 unlabelled images, released one hour before the end of the contest.

Contestants complete a prediction function; a template works with nested lists rather than NumPy. Any preprocessing and modelling approach may be chosen, but external data with similar emoji is not allowed.

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

In English

This task was published in Indonesian. SOTA translated it into English on 17 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 958 words and 8 code cells

😄 Computer Vision Task: Face or Flag?

Imagine you are an emoji curator at a large exhibition. Every icon looks similar: small, colourful and uniform in size. In fact, however, they come from two different classes: Face (face) and Flag (flag). Your task is simple: build an automatic emoji sorter that, for each emoji given, decides as accurately as possible whether the emoji belongs to the Face class or the Flag class.

Pixels and Digital Images

A digital image is made up of small squares called pixels (pixel). Each pixel stores colour information. For digital images, colour is usually stored in an RGB model. RGB stands for Red, Green and Blue, so each pixel is represented by three numbers [R, G, B], one for each channel. These three numbers indicate how brightly red, green and blue are “lit” at the same time in each of the RGB channels at that pixel. The value in each RGB channel generally lies between 0-255, where a value of 0 means the channel is off (dark), while a value of 255 means the channel is on at maximum intensity.

RGB image and each of the red, blue and green channels

Examples:

  1. Black = [0, 0, 0] (all off)
  2. White = [255, 255, 255] (all fully on)
  3. Pure red = [255, 0, 0]
  4. Pure green = [0, 255, 0]
  5. Pure blue = [0, 0, 255]
  6. Yellow = [255, 255, 0]
  7. Light blue (cyan) = [0, 255, 255]
  8. Light purple (magenta) = [255, 0, 255]
  9. Orange = [255, 165, 0]

RGB Model

Histogram of an RGB digital image

An RGB channel histogram is a statistical summary that shows how often each intensity value of the colours R, G and B (generally in the range 0–255) occurs across all the pixels of an image. The X-axis represents the intensity value (from dark, with value 0, to bright, with value 255), while the Y-axis shows the number of occurrences (or the probability if normalised, so that the total area = 1). By plotting the three channels for red, green and blue together, we can see which colour is dominant (has a high peak), how “contrasty” the image is (narrow vs wide spread of intensities), whether there is over- or under-exposure (a pile-up at the right or left end of the histogram), and colour-mixing patterns (double/multimodal peaks). Comparing the shape of the histograms between channels helps to understand the colour character of an image without looking at it visually, which is useful for describing data, choosing simple threshold values, and detecting differences between objects in images.

RGB Histogram

Available dataset

  • Train: The emoji images are in the variable X_train, which consists of 160 emoji images of size 64x64 with 3 RGB channels. The class of each image can be found in the variable y_train, where the value 1 = Faces and 2 = Flags.
  • Test: The variable X_test consists of 310 emoji images with the same size and number of channels as X_train. However, the emoji images in X_test have no class information for each emoji. The Test dataset will be released 1 hour before the contest ends.

Rules

  • You are free to choose and use the best pre-processing and modelling approach.
  • You are not allowed to use external data containing similar emojis.
  • The final scoring will use the accuracy on the test set X_test.

Download data (NO NEED TO CHANGE)

!pip -q install gdown

FILE_ID = "1YC1i-DN3DOnV8MGqorKl6yTgSERah0wa"  # WITHOUT THE KEYS
!gdown --id {FILE_ID} -O OSNAI2025_public.zip
!unzip -o OSNAI2025_public.zip -d /content/

# Import path
import sys
sys.path.append("/content/OSNAI2025_pack")

from emoji_train import X_train, y_train
from emoji_test import X_test

print("Shapes:", X_train.shape, y_train.shape, X_test.shape)
print("Unique labels in train:", sorted(set(y_train.tolist())))

Utility functions for evaluation (NO NEED TO CHANGE)

# ------------------------------
# Simple utilities for evaluation
# ------------------------------
def _safe_div(a, b):
    return (a / b) if b != 0 else 0.0

def evaluate_binary_12(y_true, y_pred):
    """
    Evaluation for labels {1, 2}:
    - Accuracy
    - Macro F1-score
    """
    C11 = C12 = C21 = C22 = 0
    n = min(len(y_true), len(y_pred))
    for i in range(n):
        t = int(y_true[i])
        p = int(y_pred[i])
        if   t == 1 and p == 1: C11 += 1
        elif t == 1 and p == 2: C12 += 1
        elif t == 2 and p == 1: C21 += 1
        elif t == 2 and p == 2: C22 += 1
        # values outside {1,2} are automatically not counted

    support1 = C11 + C12
    support2 = C21 + C22
    total    = support1 + support2

    acc = _safe_div(C11 + C22, total)

    # Class 1
    prec1 = _safe_div(C11, (C11 + C21))   # TP1 / (TP1 + FP1)
    rec1  = _safe_div(C11, (C11 + C12))   # TP1 / (TP1 + FN1)
    f1_1  = _safe_div(2*prec1*rec1, (prec1 + rec1)) if (prec1 + rec1) > 0 else 0.0

    # Class 2
    prec2 = _safe_div(C22, (C22 + C12))   # TP2 / (TP2 + FP2)
    rec2  = _safe_div(C22, (C22 + C21))   # TP2 / (TP2 + FN2)
    f1_2  = _safe_div(2*prec2*rec2, (prec2 + rec2)) if (prec2 + rec2) > 0 else 0.0

    f1_macro = (f1_1 + f1_2) / 2.0

    return acc, f1_macro

Showing sample emoji images and their classes in a grid (NO NEED TO CHANGE)

print("Sizes (shapes) of the dataset:")
print("  X_train:", X_train.shape, " y_train:", y_train.shape)
print("  X_test :", X_test.shape)

# Labels available in train (must be 1..5)
print("Unique labels available in the dataset:", sorted(set(y_train.tolist())))

# Show some sample images + labels (matplotlib)
import numpy as np
import matplotlib.pyplot as plt

def show_grid(images, labels=None, n=12, cols=6):
    n = min(n, len(images))
    rows = (n + cols - 1) // cols
    plt.figure(figsize=(1.8*cols, 1.8*rows))
    for i in range(n):
        ax = plt.subplot(rows, cols, i+1)
        ax.imshow(images[i])
        ax.axis("off")
        if labels is not None:
            ax.set_title(str(labels[i]))
    plt.tight_layout()
    plt.show()

# Shuffle indices for visualisation only
rng = np.random.RandomState(0)
idx = rng.choice(len(X_train), size=12, replace=False)
show_grid(X_train[idx], y_train[idx])

EXAMPLE: Accessing 1 emoji image and showing each RGB channel

# Example: view 1 TRAIN image + split the R, G, B channels
from emoji_train import X_train, y_train
import matplotlib.pyplot as plt

# --- Choose the index of the image to view ---
INDEX = 0  # change as needed, 0 <= INDEX < len(X_train)

# --- Take one image & its label ---
# X_train[INDEX] is an (H, W, 3) array of type uint8; we convert it to a nested list so it is easy to understand
img = X_train[INDEX].tolist()  # size: H x W x 3; each pixel = [R, G, B] (0..255)
label = int(y_train[INDEX])    # integer label (1 or 2)

# --- Image dimension information ---
H = len(img)                                  # number of rows (height)
W = len(img[0]) if H > 0 else 0               # number of columns (width)
C = len(img[0][0]) if (H > 0 and W > 0) else 0  # number of channels (should be 3 = R,G,B)

print(f"Index: {INDEX} | Label: {label} | Size: {H}x{W} | Channels: {C}")

# --- Access a sample pixel (row=10, column=20; adjust so it stays within range) ---
y, x = min(10, H-1), min(20, W-1)
Rpx, Gpx, Bpx = img[y][x]  # these are the RGB values 0..255 at that pixel
print(f"Sample pixel at (row={y}, column={x}): R={Rpx}, G={Gpx}, B={Bpx}")

# --- Split the channels into 2D (grayscale) ---
# Each channel will be a 2D list of size H x W containing numbers 0..255
R_plane = [[img[r][c][0] for c in range(W)] for r in range(H)]
G_plane = [[img[r][c][1] for c in range(W)] for r in range(H)]
B_plane = [[img[r][c][2] for c in range(W)] for r in range(H)]

# --- Build coloured "single-channel" visualisations ---
# R-only: copy the R value into the red channel, set G and B to zero
R_only_color = [[[img[r][c][0], 0, 0] for c in range(W)] for r in range(H)]
# G-only: copy the G value into the green channel, set R and B to zero
G_only_color = [[[0, img[r][c][1], 0] for c in range(W)] for r in range(H)]
# B-only: copy the B value into the blue channel, set R and G to zero
B_only_color = [[[0, 0, img[r][c][2]] for c in range(W)] for r in range(H)]


# If you also want to see B-only, show it separately:
# Original image
plt.figure(figsize=(4, 4))
plt.imshow(img)
plt.axis('off')
plt.title("Original Image (RGB)")
plt.show()

# --- Show everything in a subplot grid ---
fig, axes = plt.subplots(2, 3, figsize=(12, 8))

# (a) Coloured R-only visualisation
axes[0, 0].imshow(R_only_color)
axes[0, 0].set_title("R-only (RGB)")
axes[0, 0].axis("off")

# (b) G-only & B-only visualisation
axes[0, 1].imshow(G_only_color)
axes[0, 1].set_title("G-only (RGB)")
axes[0, 1].axis("off")

# (c) B-only visualisation
axes[0, 2].imshow(B_only_color)
axes[0, 2].set_title("B-only (RGB)")
axes[0, 2].axis("off")

# (d) Channel R as grayscale
axes[1, 0].imshow(R_plane, cmap="gray", vmin=0, vmax=255)
axes[1, 0].set_title("Channel R (grayscale)")
axes[1, 0].axis("off")

# (e) Channel G as grayscale
axes[1, 1].imshow(G_plane, cmap="gray", vmin=0, vmax=255)
axes[1, 1].set_title("Channel G (grayscale)")
axes[1, 1].axis("off")

# (f) Channel B as grayscale
axes[1, 2].imshow(B_plane, cmap="gray", vmin=0, vmax=255)
axes[1, 2].set_title("Channel B (grayscale)")
axes[1, 2].axis("off")

plt.tight_layout()
plt.show()

EXAMPLE: Accessing 1 emoji and showing the intensity histogram for each channel

# Show 1 emoji + R/G/B channel histograms in one figure
# - The image data are treated as a nested list: img[row][column] -> [R, G, B] (0..255)
# - The histogram is computed manually (without NumPy)

from emoji_train import X_train, y_train
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec

# --- Choose the index of the image to view ---
INDEX = 0  # change as needed (0 <= INDEX < len(X_train))

# Take the image & label; convert to a nested list (HxWx3)
img = X_train[INDEX].tolist()
label = int(y_train[INDEX])

# Dimension info
H = len(img)
W = len(img[0]) if H > 0 else 0
C = len(img[0][0]) if (H > 0 and W > 0) else 0
print(f"Index: {INDEX} | Label: {label} | Size: {H}x{W} | Channels: {C}")

# --- Compute the R/G/B channel histograms (0..255) with plain loops ---
def channel_histograms(img_3d):
    """Return (histR, histG, histB), each a list of length 256."""
    H = len(img_3d)
    W = len(img_3d[0]) if H > 0 else 0
    histR = [0]*256
    histG = [0]*256
    histB = [0]*256
    for r in range(H):
        row = img_3d[r]
        for c in range(W):
            R, G, B = row[c]  # [R,G,B] 0..255
            histR[int(R)] += 1
            histG[int(G)] += 1
            histB[int(B)] += 1
    return histR, histG, histB

histR, histG, histB = channel_histograms(img)

# (Optional) Normalise to probabilities so that the three are on a comparable scale
total_pixels = H * W if (H > 0 and W > 0) else 1
probR = [h/total_pixels for h in histR]
probG = [h/total_pixels for h in histG]
probB = [h/total_pixels for h in histB]
bins  = list(range(256))  # 0..255

# --- Figure layout: RGB image on top, one axes with 3 histogram curves below ---
fig = plt.figure(figsize=(12, 8))
gs = GridSpec(nrows=2, ncols=1, height_ratios=[2.2, 1.5], hspace=0.28)

# Top: original image
ax_img = fig.add_subplot(gs[0, 0])
ax_img.imshow(img)
ax_img.set_title("Original Image (RGB)")
ax_img.axis("off")

# Bottom: R/G/B channel histograms (overlaid)
ax_hist = fig.add_subplot(gs[1, 0])
ax_hist.plot(bins, probR, label="Channel R", color="red")
ax_hist.plot(bins, probG, label="Channel G", color="green")
ax_hist.plot(bins, probB, label="Channel B", color="blue")
ax_hist.set_xlim(0, 255)
ax_hist.set_xlabel("Intensity Value (0..255)")
ax_hist.set_ylabel("Probability (p)")
ax_hist.set_title("RGB Channel Histogram (normalised)")
ax_hist.grid(alpha=0.25)
ax_hist.legend()

plt.tight_layout()
plt.show()

# (Optional) If you want a 'bar' version instead of lines (careful, it can be dense):
plt.figure(figsize=(12,4))
plt.bar(bins, probR, color="red", alpha=0.4, label="R")
plt.bar(bins, probG, color="green", alpha=0.4, label="G")
plt.bar(bins, probB, color="blue", alpha=0.4, label="B")
plt.xlim(0, 255); plt.xlabel("Intensity Value"); plt.ylabel("Probability"); plt.legend(); plt.show()

YOUR TASK

Complete this function to predict whether an emoji image belongs to the Faces class (class = 1) or the Flags class (class = 2).

def prediksi_gambar_img(img):
    """
    Template prediction function for 1 image.
    Parameters
    ----------
    img : list[list[list[int]]] or an equivalent array
        Image of size HxWx3 with values 0..255 (RGB).
        If the input is a NumPy array, the function will try to convert it to a list.

    Return
    ------
    int
        Predicted label {1, 2}, where 1 = Faces, 2 = Flags.
    """
    # Make sure it is a nested list (if it is still an array, convert it to a list)
    if hasattr(img, "tolist"):
        img = img.tolist()

    # Validate the basic data shape
    H = len(img)
    W = len(img[0]) if H > 0 else 0
    C = len(img[0][0]) if (H > 0 and W > 0) else 0
    if H == 0 or W == 0 or C != 3:
        raise ValueError(f"Invalid image shape. Found: H={H}, W={W}, C={C} (must be HxWx3)")

    # -------------------- YOUR CODE STARTS HERE --------------------
    # Write your logic below to compute features/decisions from img (nested list).
    # Example usage pattern:
    #
    # some_value = 0.0
    # for r in range(H):
    #     for c in range(W):
    #         R, G, B = img[r][c]   # access the pixel at row=r, column=c
    #         # ... write your logic here ...
    #
    # pred_label = 1 (for Faces) # or 2 (for Flags), according to your decision
    # -------------------- YOUR CODE ENDS HERE ------------------

    # Validate & return the label
    try:
        pred_label
    except NameError:
        raise RuntimeError("You have not set the variable 'pred_label' in the YOUR CODE block.")

    if pred_label not in (1, 2):
        raise ValueError("pred_label must have the value 1 or 2.")

    return int(pred_label)

Making Predictions on the Training Data

Your answer will be scored partly on the training data, which consists of 160 emoji images. This training data can be used to decide on the feature extraction for the emoji images and to create rules for predicting whether an image is a Faces or a Flags emoji. The feature extraction and the prediction rules for an image can be implemented in the function prediksi_gambar_img (the cell before this one).

Note:

  1. You do not need to change the code below, as long as you implement the function prediksi_gambar_img above. The code below will run correctly on its own as long as you implement the function prediksi_gambar_img.
# TRAIN template (without NumPy in your code):
# - Iterates over the training examples one by one
# - Converts each image into a nested list: img[row][column] -> [R, G, B] (values 0..255)
# - Shows size/channel information + a visualisation
# - Provides an empty place to write your own features/decisions
#
# Note:
#   X_train: array of images of size (N, H, W, 3) of type uint8
#   y_train: labels of size (N,) with values {1, 2}

from emoji_train import X_train, y_train
import matplotlib.pyplot as plt

THRESH_N = 160  # limit how many samples are processed (to keep exploration fast)

# (Optional) list to store your predictions (if you want to evaluate/submit later)
preds = []  # fill with 1 or 2 for each sample (its length is free during exploration)

N = min(THRESH_N, len(X_train))
for i in range(N):
    # Take one image and convert it to a nested list (without depending on NumPy in your logic)
    img = X_train[i].tolist()  # size: H x W x 3, each pixel = [R, G, B] (0..255)

    # Take the label (1 or 2) for the TRAIN data
    label = int(y_train[i])

    # Image structure information
    H = len(img)                             # number of rows (height)
    W = len(img[0]) if H > 0 else 0          # number of columns (width)
    C = len(img[0][0]) if (H > 0 and W > 0) else 0  # number of channels (should be 3 = RGB)

    # Call the function `prediksi_gambar_img` that you wrote to predict the class of image img.
    prediksi_label = prediksi_gambar_img(img)
    preds.append(prediksi_label)

    print(f"[TRAIN] idx={i} | size={H}x{W} | channels={C} | label={label} | prediction={prediksi_label}")

    # (Optional) Visualisation for a quick check
    plt.imshow(img)
    plt.axis('off')
    plt.title(f"train idx={i} | label={label} | prediction={prediksi_label}")
    plt.show()

# You can use the variable 'preds' to evaluate TRAIN,
# by comparing 'preds' with 'y_train'.

# ------------------------------
# CHECK & EVALUATION
# ------------------------------
if len(preds) != N:
    raise RuntimeError(
        f"Predictions are incomplete: len(preds)={len(preds)} while N={N}. "
        "Please fill in the 'YOUR CODE STARTS HERE' block to produce predictions."
    )

acc, f1_macro = evaluate_binary_12(y_train[:N], preds)

print(f"Evaluation on {N} test samples:")
print(f"Accuracy    : {acc:.4f}")
print(f"F1 Macro    : {f1_macro:.4f}")

SCORING: Making Predictions on the Test Data

Your answer will be scored partly on the test data, which consists of 310 emoji images. The feature extraction and the prediction rules for an image can be implemented in the function prediksi_gambar_img, which is also used to predict the training data.

Notes:

  1. The test file will only be opened 1 hour before the contest ends!
  2. You do not need to change the code below, as long as you implement the function prediksi_gambar_img above. The code below will run correctly on its own as long as you implement the function prediksi_gambar_img.
# TRAIN template (without NumPy in your code):
# - Iterates over the training examples one by one
# - Converts each image into a nested list: img[row][column] -> [R, G, B] (values 0..255)
# - Shows size/channel information + a visualisation
# - Provides an empty place to write your own features/decisions

FILE_ID = "LINK_REMOVED"  # WITHOUT THE KEYS  [SOTA note: the original links here to the private test answers (OSNAI2025_privateGT.zip); that link is left out of this translation]
!gdown --id {FILE_ID} -O OSNAI2025_privateGT.zip
!unzip -o OSNAI2025_privateGT.zip -d /content/

# Import path
import sys
sys.path.append("/content/OSNAI2025_pack")

from emoji_test_gt import X_test, y_test  # private ground-truth module

import matplotlib.pyplot as plt

THRESH_N = 310  # limit how many samples are processed (to keep exploration fast)

# (Optional) list to store your predictions (if you want to evaluate/submit later)
preds = []  # fill with 1 or 2 for each sample (its length is free during exploration)

N = min(THRESH_N, len(X_test))
for i in range(N):
    # Take one image and convert it to a nested list (without depending on NumPy in your logic)
    img = X_test[i].tolist()  # size: H x W x 3, each pixel = [R, G, B] (0..255)

    # Take the label (1 or 2) for the TRAIN data
    label = int(y_test[i])

    # Image structure information
    H = len(img)                             # number of rows (height)
    W = len(img[0]) if H > 0 else 0          # number of columns (width)
    C = len(img[0][0]) if (H > 0 and W > 0) else 0  # number of channels (should be 3 = RGB)

    # Call the function `prediksi_gambar_img` that you wrote to predict the class of image img.
    prediksi_label = prediksi_gambar_img(img)
    preds.append(prediksi_label)

    # (Optional) Visualisation for a quick check
    print(f"[TEST] size={H}x{W} | channels={C} | label={label} | prediction={prediksi_label}")
    plt.imshow(img)
    plt.axis('off')
    plt.title(f"label={label} | prediction={prediksi_label}")
    plt.show()

# You can use the variable 'preds' to evaluate TRAIN,
# by comparing 'preds' with 'y_test'.

# ------------------------------
# CHECK & EVALUATION
# ------------------------------
if len(preds) != N:
    raise RuntimeError(
        f"Predictions are incomplete: len(preds)={len(preds)} while N={N}. "
        "Please fill in the 'YOUR CODE STARTS HERE' block to produce predictions."
    )

acc, f1_macro = evaluate_binary_12(y_test[:N], preds)

print(f"Evaluation on {N} test samples:")
print(f"Accuracy    : {acc:.4f}")
print(f"F1 Macro    : {f1_macro:.4f}")

Scoring

The participant with the best accuracy will receive a perfect score (100). The baseline accuracy is 50%. Participants whose accuracy is equal to or lower than the baseline will receive 0.

Otherwise, participants receive a normalised score:

100 * (participant_accuracy - baseline) / (best_participant - baseline)

The baseline is 50%

Translated by SOTA. The Indonesian original is the official version and wins wherever the two differ. The original's final scoring cell downloads the private test answers; that link is left out of this translation, so the cell will not run as given. Function and variable names stay in Indonesian (prediksi_gambar_img = predict image, prediksi_label = predicted label). 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
X_train (160, 64, 64, 3), y_train, X_test (310, 64, 64, 3) from OSNAI2025_public.zip (Google Drive).
You submit
Predicted class for each test image.
Scoring
Accuracy on X_test (the notebook also scores part of the answer on the training set).
Rules
  • No external emoji data.
Format
Final (on-site) of the AI Exhibition at OSN 2025, Universitas Muhammadiyah Malang, 6–10 October 2025: an essay paper and programming tasks in Google Colab.

Details

Year
2025, Universitas Muhammadiyah Malang, Malang, Indonesia
Round
OSN 2025 AI Exhibition – Final · P3 task
Language
Indonesian; English translation by SOTA
License
Not stated by the source