COMP90086 Computer Vision · Workshop 11

Image Segmentation: from Pixel Clustering to Encoder–Decoders

A primer on lectures 19 and 20, then the Workshop 11 notebook cell by cell. Every algorithm the notebook calls from a library also runs live on this page.

region found by the algorithm algorithm state (centroid, window, prediction) ground truth boundary, cut or error
Mean-shift windows climbing to density modes
Prologue · Lecture 19, slides 3–6 and 40–41 · Lecture 20, slides 5–6

A1 · Problem definitions: semantic, instance and panoptic segmentation

Segmentation assigns a label to every pixel.

The lectures distinguish problems by what that label means:

  • Unlabelled regions. The label is only an ID: pixels with the same ID belong together. This is what clustering, superpixels and graph methods produce (lecture 19).
  • Semantic segmentation. The label is a category such as cow, grass or sky. Two cows get the same label.
  • Instance segmentation. Each countable object gets its own ID: cow 1, cow 2.
  • Panoptic segmentation. Both at once: every pixel gets a category, and pixels of countable objects also get an instance ID.1

Slide 41 adds the vocabulary for this split. Things are countable objects (cow, person, car). Stuff is regions or materials (road, grass, sky).2 Instance segmentation focuses on things.

InteractiveOne scene, four definitions of the label
Hover over a pixel to read its label.
"Regions" gives arbitrary IDs and no names. "Semantic" merges the two cows. "Instance" separates the cows and ignores stuff. "Panoptic" does both.
Prologue · Lecture 19, slides 10–12

A2 · Pixel clustering with k-means

The simplest segmenter ignores where pixels are and looks only at their colour.

Each pixel becomes a point in colour space, for example \((R, G, B)\) or \((L, a, b)\). Clusters of points become segments.

k-means looks for \(k\) centres \(\boldsymbol\mu_1, \dots, \boldsymbol\mu_k\) and an assignment \(c_i\) of each point that minimise the within-cluster sum of squares:3

$$J = \sum_{i=1}^{n} \big\lVert \mathbf x_i - \boldsymbol\mu_{c_i} \big\rVert^2 .$$

Lloyd's algorithm alternates two steps until nothing changes:

  1. Assignment step. Give each point the label of its nearest centre.
  2. Update step. Move each centre to the mean of the points assigned to it.
InteractiveLloyd's algorithm in a two-dimensional colour plane
horizontal axis \(a^*\) (green to red) · vertical axis \(b^*\) (blue to yellow)
Shaded cells show which centre is nearest (the assignment rule). Crosses are the centres \(\boldsymbol\mu_j\). Try \(k = 4\) on three blobs, and several initialisations: k-means always splits the data into exactly \(k\) groups, and different starts can reach different local minima.
Derivation · why neither step can increase \(J\)

Assignment step. Each term \(\lVert\mathbf x_i - \boldsymbol\mu_{c_i}\rVert^2\) is replaced by the smallest of its \(k\) options, so no term grows.

Update step. With the assignments fixed, consider one cluster \(S\). Its contribution, as a function of a single centre \(\boldsymbol\mu\), is

$$J_S(\boldsymbol\mu) = \sum_{i \in S} \lVert \mathbf x_i - \boldsymbol\mu \rVert^2 .$$

Setting the derivative to zero gives the minimiser:

$$\frac{\partial J_S}{\partial \boldsymbol\mu} = -2\sum_{i\in S}(\mathbf x_i - \boldsymbol\mu) = 0 \quad\Longrightarrow\quad \boldsymbol\mu = \frac{1}{|S|}\sum_{i \in S}\mathbf x_i .$$

So moving the centre to the mean can only lower \(J_S\).

\(J \ge 0\), it never increases, and there are finitely many assignments, so the algorithm stops. It stops at a local minimum, not necessarily the global one.

Slide 12 replaces hard assignments with a Gaussian mixture model. Each cluster becomes a Gaussian with its own covariance, and each pixel gets a probability of belonging to each cluster. k-means is the limiting case where every cluster has the same tiny spherical covariance.

Prologue · Lecture 19, slides 13–26

A3 · Mean shift and the modes of a density

Mean shift treats the points as samples from an unknown probability density, and looks for its peaks (modes).4

Each point is then labelled by the peak it climbs to. The number of clusters is not chosen in advance: it is the number of peaks.

The procedure on slide 24 is:

  1. Centre a window of radius \(h\) (the bandwidth) on a point.
  2. Compute the mean of the data inside the window.
  3. Move the window to that mean, and repeat until it stops moving.
  4. Points whose windows end at the same peak form one cluster.

The move from \(\mathbf x\) to the weighted mean is the mean-shift vector (slide 23). The weights come from a kernel profile \(g\):

$$\mathbf m(\mathbf x) = \frac{\displaystyle\sum_{i=1}^{n} \mathbf x_i\, g\!\left(\left\lVert \frac{\mathbf x - \mathbf x_i}{h}\right\rVert^2\right)}{\displaystyle\sum_{i=1}^{n} g\!\left(\left\lVert \frac{\mathbf x - \mathbf x_i}{h}\right\rVert^2\right)} - \mathbf x .$$

A flat kernel gives \(g = 1\) inside the window and \(0\) outside, so the update is the plain mean of the points in the window. A Gaussian kernel weights every point by its distance, with \(g(u) = e^{-u/2}\).

InteractiveMean shift on a kernel density estimate
window of radius \(h\) and its pathcontours of the density estimate \(\hat f_h\)
Drag the window anywhere. "Cluster every point" runs mean shift from every data point and colours each point by the peak it reaches. A small \(h\) finds many peaks, and a large \(h\) merges them: the bandwidth replaces \(k\) as the parameter to choose (slide 27).
Derivation · mean shift climbs the density estimate

A kernel density estimate puts a bump of width \(h\) on every point. With profile \(k\):

$$\hat f_h(\mathbf x) = \frac{C}{n}\sum_{i=1}^{n} k\!\left(\left\lVert \frac{\mathbf x - \mathbf x_i}{h}\right\rVert^2\right).$$

Differentiate with the chain rule, and write \(g = -k'\):

$$\nabla \hat f_h(\mathbf x) = \frac{2C}{nh^2}\sum_{i=1}^{n} (\mathbf x_i - \mathbf x)\, g\!\left(\left\lVert \frac{\mathbf x - \mathbf x_i}{h}\right\rVert^2\right).$$

Pull out the sum of the weights \(g_i\):

$$\nabla \hat f_h(\mathbf x) = \frac{2C}{nh^2}\Big(\sum_{i} g_i\Big)\left[\frac{\sum_i \mathbf x_i g_i}{\sum_i g_i} - \mathbf x\right] = \frac{2C}{nh^2}\Big(\sum_{i} g_i\Big)\, \mathbf m(\mathbf x).$$

The bracket is exactly the mean-shift vector. So \(\mathbf m(\mathbf x)\) always points uphill on \(\hat f_h\), and the window stops where the gradient is zero: at a peak.

For the Gaussian profile \(k(u) = e^{-u/2}\), we get \(g(u) = \tfrac12 e^{-u/2}\), again Gaussian. The flat window corresponds to the Epanechnikov profile \(k(u) = 1 - u\) for \(u \le 1\), whose derivative is constant inside the window.

Clustering in colour and position. Slide 25 appends the pixel coordinates to the colour, giving five-dimensional features \((x, y, L, a, b)\).

Each dimension has its own bandwidth, or equivalently, the coordinates are rescaled before one shared bandwidth is used (the notebook does this in cell 25). A larger spatial bandwidth relative to the colour bandwidth gives smoother, more blob-like regions (slide 26).

Prologue · Lecture 19, slides 29–35

A4 · Superpixels and the SLIC algorithm

Over-segmentation splits the image into regions that are smaller than objects. Objects are still separated from the background, but each object is broken into many parts.

These parts are superpixels: groups of adjacent pixels with similar colour. They make a compact intermediate representation: a \(600 \times 800\) image of \(480\,000\) pixels becomes about \(200\) superpixels (slide 35).

SLIC (simple linear iterative clustering) is k-means in \((l, a, b, x, y)\) space with two changes:5

  1. Initialisation. Place \(k\) centres on a regular grid with spacing \(S\), then move each to the lowest-gradient pixel in its \(3 \times 3\) neighbourhood, so it does not start on an edge.
  2. Local search. Each centre only competes for pixels in a \(2S \times 2S\) window around it. This makes the cost linear in the number of pixels.

The distance mixes colour and position (slide 32):

$$D = d_{lab} + \frac{\alpha}{S}\, d_{xy},$$ $$d_{lab} = \sqrt{(l - l_k)^2 + (a - a_k)^2 + (b - b_k)^2}, \qquad d_{xy} = \sqrt{(x - x_k)^2 + (y - y_k)^2}.$$

The weight \(\alpha\) is the compactness. A large \(\alpha\) gives regular, grid-like cells. A small \(\alpha\) lets cells follow colour edges closely.

LiveSLIC on the notebook's parrot image
boundaries and centres
each superpixel filled with its mean colour
Iteration 0 is the grid initialisation. The readout tracks the average change in the centres' positions, which slide 32 uses as the stopping rule. Without the connectivity clean-up of slide 33, a few cells can split into disconnected pieces: look for small islands at low compactness.
Prologue · Lecture 19, slides 36–37

A5 · Region adjacency graphs and greedy merging

Superpixels are usually a first step. To find whole objects, neighbouring superpixels with similar colour are merged.

The region adjacency graph (RAG) organises this:

  • Vertices are regions (pixels or superpixels).
  • Edges join regions that touch.
  • Edge weights measure how different two regions are, for example the distance between their mean colours.

Iterative merging (slide 36) repeats one move:

  1. Find the lowest-weight edge.
  2. If its weight is below a threshold, merge its two regions into one.
  3. Recompute the merged region's mean colour and the weights of its edges, and repeat.
InteractiveGreedy merging on a six-region toy image
regions with their mean grey value
region adjacency graph, edge weight \(= |\bar g_i - \bar g_j|\)
The merged region's mean is the pixel-weighted average of its parts, so its edges must be recomputed after every merge. Merging stops when the lowest remaining edge is not below \(\tau\).
Prologue · Notebook section 3.2 · Shi and Malik (2000)

A6 · Normalised cuts

Merging builds regions bottom-up. A cut works top-down: split the graph into two sets \(A\) and \(B\) by deleting edges.

Here edge weights measure similarity (large means alike). The cost of a split is the weight of the deleted edges:

$$\operatorname{cut}(A, B) = \sum_{i \in A,\ j \in B} w_{ij}.$$

The minimum cut has a flaw: cutting off one weakly connected node is cheap, so it favours tiny segments.

The normalised cut fixes this by dividing by each side's total connection to the whole graph, \(\operatorname{assoc}(A, V) = \sum_{i \in A} d_i\), where \(d_i\) is the weighted degree of node \(i\):6

$$\operatorname{Ncut}(A, B) = \frac{\operatorname{cut}(A, B)}{\operatorname{assoc}(A, V)} + \frac{\operatorname{cut}(A, B)}{\operatorname{assoc}(B, V)} .$$

A tiny \(A\) has a tiny \(\operatorname{assoc}(A, V)\), so its first term is large. Balanced splits along weak edges score best.

InteractiveMinimum cut versus minimum normalised cut
set \(A\)set \(B\)cut edge
Click a node to move it to the other side. The minimum cut isolates node \(g\), which hangs on by a single weak edge. The normalised cut instead separates the two triangles.
Footnote · how normalised cuts are computed in practice

Trying every split is exponential in the number of nodes. Shi and Malik relax the binary labels to real numbers. The relaxed problem becomes a generalised eigenvalue problem with the degree matrix \(D\) and the weight matrix \(W\):

$$(D - W)\,\mathbf y = \lambda D\, \mathbf y .$$

The eigenvector with the second-smallest eigenvalue is thresholded to give the split, and splitting is applied recursively. skimage.graph.cut_normalized does this on the superpixel RAG of cell 37.

Prologue · Lecture 19, slides 40–49

A7 · Instance segmentation: Mask R-CNN and GrabCut

Clustering finds colour regions, and those regions often do not correspond to objects (slide 27).

The standard approach to instance segmentation (slide 42) has two stages:

  1. Find a region of interest: a patch that is likely to contain one object.
  2. Segment that patch into two classes: foreground (the object) and background.

Slide 43 gives two examples:

  • Mask R-CNN uses a learned detector to propose boxes. For each box it predicts a class, a refined box, and a binary mask from a small fully-convolutional head. RoIAlign resamples each box to a fixed grid without rounding coordinates, so the mask lines up with the pixels.7 Section 4 of the notebook runs this model.
  • GrabCut takes the box from a user. It models the colours of the foreground and of the background each as a Gaussian mixture, and labels pixels to maximise the likelihood plus a penalty for colour boundaries.8

GrabCut minimises an energy over the labels \(\alpha_p \in \{\text{fg}, \text{bg}\}\):

$$E(\boldsymbol\alpha) = \underbrace{\sum_{p} -\log p\big(\mathbf z_p \mid \alpha_p\big)}_{\text{colour likelihood}} \; + \; \lambda \underbrace{\sum_{(p,q)\ \text{neighbours}} [\alpha_p \ne \alpha_q]}_{\text{boundary length}} .$$

It alternates between refitting the two colour models and relabelling the pixels, and the energy falls at every round (slide 47).

InteractiveBox-prompted foreground extraction (a simplified GrabCut)
drag to draw the box (the prompt)
energy \(E\) over iterations
Each colour model is a five-component mixture with a shared spherical variance, fitted by k-means. Pixels outside the box are fixed as background. To keep the page light, labels are updated by a few sweeps of iterated conditional modes rather than an exact graph cut, so this is an approximation of GrabCut, not the real algorithm. Draw a box that also covers the yellow distractor and watch it stay foreground: colour alone cannot tell the two objects apart.
Prologue · Lecture 20, slides 8–13 and 25

A8 · Segmentation as pixel classification

Lecture 20 turns to methods that use labels. The first idea (slide 8) is to classify every pixel from an \(N \times N\) window around it.

Run naively, that is one CNN evaluation per pixel: \(480\,000\) evaluations for a small \(600 \times 800\) image (slide 9).

A fully-convolutional network (FCN) has no fully-connected layers. It produces a spatial map of class scores in one pass, so it classifies all windows in parallel and accepts any input size (slides 10–11).9

Slide 12 names two problems with a plain stack of convolutions:

  • The receptive field grows only linearly. With \(3 \times 3\) kernels and stride 1, an output pixel sees \(2L + 1\) input pixels per side after \(L\) layers.
  • Downsampling fixes that but loses resolution. Max pooling makes the receptive field grow geometrically, but the output map gets smaller.

The solution is an encoder–decoder (slide 13): the encoder downsamples, and the decoder upsamples back to \(H \times W\).

InteractiveReceptive field of one output pixel
pixels the output depends onoutput pixel
Shown in one dimension. Each row is a layer, the input at the bottom. With pooling, each layer's output pixel covers twice as much of the layer below, so the receptive field roughly doubles per layer.
Derivation · the receptive-field recursion

Let \(r_\ell\) be the receptive field after layer \(\ell\), and \(j_\ell\) the distance in input pixels between neighbouring units of layer \(\ell\) (the product of the strides so far). Start from \(r_0 = 1\) and \(j_0 = 1\).

A layer with kernel size \(k\) and stride \(s\) adds \(k - 1\) units of the layer below, and each of those spans \(j_{\ell-1}\) input pixels:

$$r_\ell = r_{\ell-1} + (k - 1)\, j_{\ell-1}, \qquad j_\ell = j_{\ell-1}\, s .$$

For \(3 \times 3\) convolutions with stride 1, every step adds 2, so \(r_L = 2L + 1\): linear growth.

A \(2 \times 2\) pooling layer adds \(j_{\ell-1}\) and doubles \(j\). After that, the next convolution adds twice as much, which is why the growth becomes geometric.

Prologue · Lecture 20, slides 14–24

A9 · Upsampling layers: max unpooling and transposed convolution

The decoder needs layers that make feature maps larger. Lecture 20 gives two.

Max unpooling

Each upsampling layer is paired with a max-pooling layer in the encoder (slide 15).10

  1. When the encoder pools, it records where each maximum came from (the "switches").
  2. When the decoder unpools, it writes each value back into that remembered position, and fills the rest of the block with zeros.

The simpler alternatives on slide 14 copy each value into the whole block (nearest neighbour) or always into the top-left corner (bed of nails).

InteractiveMax pooling with switches, then max unpooling (the numbers from slide 15)
The encoder input is the \(4\times4\) grid from slide 15, with the maxima of each \(2\times2\) block outlined. "…" stands for the rest of the network: the decoder receives a new \(2\times2\) map and puts its values back where the encoder's maxima were.

Transposed convolution

A convolution with stride 2 is a learnable downsampler: the filter moves 2 pixels in the input for every 1 pixel in the output (slide 18).

A transposed convolution reverses the roles. The filter moves 2 pixels in the output for every 1 pixel in the input:

  1. Multiply the whole kernel by one input value.
  2. Stamp the result into the output at stride 2.
  3. Where stamps overlap, add them (slide 20).
  4. Trim the border to reach the target size (slide 21).

Some papers call this "deconvolution", "upconvolution" or "fractionally strided convolution".11

InteractiveOne-dimensional transposed convolution (the practice example on slide 22)
Input \([5, 8]\), kernel \([1, 2, 1]\). Without trimming, the output length is \(s\,(n - 1) + k\). With stride 2 that is \(5\), which matches the five empty cells on slide 22.
Quick check
With stride 2, what is the middle output value of slide 22?
An input of length 3, kernel 3, stride 2, no trimming: how long is the output?

Why the name "transposed"

Slides 23–24 write convolution as a matrix product. For kernel \(\vec x = [x, y, z]\), stride 2 and padding 1, convolving a length-4 signal \(\vec a\) (padded to length 6) is

$$\vec x * \vec a = X \vec a = \begin{bmatrix} x & y & z & 0 & 0 & 0 \\ 0 & 0 & x & y & z & 0 \end{bmatrix}\begin{bmatrix} 0 \\ a \\ b \\ c \\ d \\ 0 \end{bmatrix} = \begin{bmatrix} ay + bz \\ bx + cy + dz \end{bmatrix}.$$

The transposed convolution multiplies by the transpose of the same matrix. A length-2 input becomes a length-6 output, two of which are padding:

$$\vec x *^{T} \vec a = X^{T} \vec a = \begin{bmatrix} x & 0 \\ y & 0 \\ z & x \\ 0 & y \\ 0 & z \\ 0 & 0 \end{bmatrix}\begin{bmatrix} a \\ b \end{bmatrix} = \begin{bmatrix} ax \\ ay \\ az + bx \\ by \\ bz \\ 0 \end{bmatrix}.$$

Read down a column of \(X^{T}\): it is the kernel, shifted by the stride. That is exactly the "stamp and add" rule above.

Derivation · the transpose is the backward pass of the convolution

Let \(\mathbf u = X\mathbf a\) be a strided convolution and \(L\) any loss. By the chain rule:

$$\frac{\partial L}{\partial a_j} = \sum_i \frac{\partial L}{\partial u_i}\,\frac{\partial u_i}{\partial a_j} = \sum_i X_{ij}\,\frac{\partial L}{\partial u_i} \quad\Longrightarrow\quad \nabla_{\mathbf a} L = X^{T}\, \nabla_{\mathbf u} L .$$

So a transposed convolution is the operation that carries gradients back through a strided convolution. It maps a short vector back to the long one, which is why it upsamples. It is not the inverse of the convolution: \(X^{T}X \ne I\) in general.

Prologue · Lecture 20, slides 26–36

A10 · U-Net and the per-pixel loss

The last layer. A \(1 \times 1\) convolution mixes channels at each pixel without looking at neighbours. With \(C\) output channels it turns every pixel's feature vector into \(C\) class scores (slide 26).

The loss. Training compares the predicted class distribution \(\hat{\mathbf y}_p\) with the true class at every pixel \(p\), and averages the cross-entropy over the \(N\) pixels (slide 27):

$$E = -\frac{1}{N}\sum_{p=1}^{N} \sum_{c=1}^{C} y_{p,c}\, \log \hat y_{p,c} .$$

U-Net was built for biomedical images.12 It is an encoder–decoder with one extra idea: each decoder stage concatenates the upsampled map with the feature map from the matching encoder stage (slides 33–34).

The encoder copy carries fine spatial detail that pooling threw away, and the decoder supplies context. For cell segmentation, U-Net also weights the loss so that the thin gaps between touching cells count more.

The original network uses unpadded ("valid") \(3 \times 3\) convolutions, so each one shrinks the map by 2 pixels. The encoder maps are cropped before they are concatenated, which is the "copy and crop" arrow.

InteractiveU-Net shape arithmetic (slide 33)
Each box shows the side length after two valid \(3\times3\) convolutions (each subtracts 2). Red arrows are \(2\times2\) max pools (halve), green arrows are \(2\times2\) up-convolutions (double), and dashed grey arrows copy and crop. Some input sizes are invalid because a pooling step would receive an odd size.
Quick check
For a \(572 \times 572\) input tile, what is the side of the output segmentation map?
The first encoder map is \(568 \times 568\). To what side is it cropped for the top skip connection?
Prologue · Lecture 20, slides 37–47

A11 · Segment Anything and the overlap metrics

The Segment Anything Model (SAM) takes an image plus a prompt and returns a binary mask (slide 38).13 A prompt can be a point, a box, a mask or a text embedding.

Its encoder–decoder structure (slides 39–41) has three parts:

  • Image encoder. A vision transformer pre-trained as a masked autoencoder14 turns the image into a \(64 \times 64\) grid of 256-dimensional vectors. It runs once per image.
  • Prompt encoder. Points and boxes become positional embeddings, text goes through CLIP, and mask prompts go through a small convolution.
  • Mask decoder. A few transformer layers let the prompt tokens and the image embedding attend to each other. The decoder then upsamples \(2\times\) with transposed convolutions, and outputs a mask logit at every pixel plus a predicted IoU score.

Ambiguity. A point on a person's shirt could mean the shirt or the whole person. SAM outputs three masks (for example subpart, part and whole), and is trained by back-propagating only the best of the three (slide 42).

Training. The mask loss combines focal loss and Dice loss, and the IoU head is trained with the mean squared error to the true IoU (slide 43). The SA-1B dataset has 11 million images and 1.1 billion masks, 99% generated automatically (slide 44).

Measuring overlap

Let \(P\) be the predicted pixels and \(G\) the ground-truth pixels. Two scores compare them:

$$\operatorname{IoU}(P, G) = \frac{|P \cap G|}{|P \cup G|}, \qquad \operatorname{Dice}(P, G) = \frac{2\,|P \cap G|}{|P| + |G|}.$$

Both are 1 for a perfect match and 0 for no overlap. Dice is always at least as large as IoU.

Focal loss down-weights pixels that are already easy. If \(p_t\) is the predicted probability of the true class at a pixel, it multiplies the cross-entropy by \((1 - p_t)^\gamma\):15

$$\operatorname{FL}(p_t) = -(1 - p_t)^{\gamma}\, \log p_t .$$

The Dice loss is \(1 - \operatorname{Dice}\), computed with soft probabilities.16 Because it only counts the object and the prediction, a huge background does not swamp it.

InteractiveIoU and Dice of two masks
prediction \(P\) (drag it, or its corner)ground truth \(G\)
horizontal \(\operatorname{IoU}\) · vertical \(\operatorname{Dice} = 2\operatorname{IoU}/(1 + \operatorname{IoU})\)
Derivation · Dice as a function of IoU

Write \(I = |P \cap G|\) and \(S = |P| + |G|\). Inclusion–exclusion gives the union:

$$|P \cup G| = S - I \quad\Longrightarrow\quad \operatorname{IoU} = \frac{I}{S - I}.$$

Solve for \(I\) in terms of \(S\): \(I = \operatorname{IoU}\,(S - I)\), so

$$I = \frac{\operatorname{IoU}}{1 + \operatorname{IoU}}\, S .$$

Substitute into the Dice score:

$$\operatorname{Dice} = \frac{2I}{S} = \frac{2\operatorname{IoU}}{1 + \operatorname{IoU}} .$$

Since \(1 + \operatorname{IoU} \le 2\), we get \(\operatorname{Dice} \ge \operatorname{IoU}\), with equality only at 0 and 1.

Quick check

Ground truth is the square with corners \((0,0)\) and \((4,4)\). The prediction is the square with corners \((2,1)\) and \((6,5)\).

IoU:
Dice:

Lecture 20 ends with the trade-off for encoder–decoder methods (slide 47). They handle complex objects and backgrounds, and can take text as input or output. But their masks may miss precise boundaries, and they may miss background or hard-to-name objects.


Notebook · Section 1 · cells 2–23

1 · Pixel clustering with k-means

The notebook starts by clustering the RGB colours of an aerial photo of Albert Park into \(K\) groups, then repainting each pixel with its cluster centre.

Cell numbers count every cell of worksheet11_solution-old-24.ipynb from 0, markdown included. Code is shown verbatim.

1.1 · Set-up and loading the image

Cell 2Pin scikit-image (Colab)
## For colab user, run
! pip install scikit-image==0.21.0
## After the update, You must restart the runtime in order to use newly installed versions.
Cell 4Imports
import matplotlib.pyplot as plt
import numpy as np
import cv2
import tensorflow as tf
import logging

# Control the logging verbosity
# Suppress verbose Tensorflow logging
tf.get_logger().setLevel(logging.ERROR)
# Sets the AutoGraph verbosity level.
tf.autograph.set_verbosity(0)
# Verbosity is now 0

from sklearn.cluster import KMeans
from skimage.data import astronaut, coffee
from skimage.color import rgb2gray, label2rgb
from skimage.filters import sobel
from skimage import graph
from skimage.segmentation import felzenszwalb, slic, quickshift, watershed
from skimage.segmentation import mark_boundaries
from skimage.util import img_as_float
from sklearn.feature_extraction import image
from sklearn.cluster import spectral_clustering
Cell 8Load the image as RGB
# Read an image into BGR Format
!wget https://raw.githubusercontent.com/saraao/COMP90086_image/main/Albert_Park.jpg -q -O Albert_Park.jpg
img =  cv2.imread('Albert_Park.jpg')
# Convert to RGB
img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)

print("Image shape:", img.shape)

plt.imshow(img) # as RGB Format
plt.axis("off")
plt.show()
Plot from cell 8 (the solution notebook's saved output)Plot produced by cell 8
Output of cell 8
Image shape: (800, 640, 3)

OpenCV reads images in BGR channel order, which is why cell 8 converts to RGB before plotting.

KMeans expects one row per sample, so cell 10 flattens the \(800 \times 640 \times 3\) array into \(512\,000\) rows of 3 colour values:

Cell 10Flatten to one row per pixel
# Reshape the image to a 2D array of pixels and 3 color values
flat_image = img.reshape((-1,3))
# Convert to float
flat_image = np.float32(flat_image)
print(flat_image.shape)
Output of cell 10
(512000, 3)

1.2 · Clustering and repainting

Cell 12Fit k-means with K = 4
K = 4
kmeans = KMeans(n_clusters=K)
kmeans.fit(flat_image)
Cell 13Cluster centres
kmeans.cluster_centers_
Output of cell 13
array([[125.390465, 162.35413 , 170.89093 ],
       [189.08794 , 198.64023 , 204.28804 ],
       [159.2623  , 154.95505 ,  88.992035],
       [ 60.432976,  83.3275  ,  75.4993  ]], dtype=float32)
Cell 14Centres as 8-bit RGB
# Convert to Integer format
centers = np.uint8(kmeans.cluster_centers_)
print(centers) # In RGB Format
Cell 16Colour swatches
plt.figure(0,figsize=(8,2))

i = 1
for each_col in centers:
    plt.subplot(1,4,i)
    plt.axis("off")
    i+=1

    # Color Swatch
    a = np.zeros((100,100,3),dtype='uint8')
    a[:,:,:] = each_col
    plt.imshow(a)

plt.show()
Plot from cell 16 (the solution notebook's saved output)Plot produced by cell 16
Cell 18Repaint every pixel with its centre
res = centers[kmeans.labels_]
result_image1 = res.reshape((img.shape))
Cell 19Show the result
plt.imshow(result_image1)
plt.title("Segmented Image when K = 4")
plt.axis("off")
plt.show()
Plot from cell 19 (the solution notebook's saved output)Plot produced by cell 19
Cell 21K = 10 and K = 2
# Use different K

# image 2
K = 10
kmeans = KMeans(n_clusters=K)
kmeans.fit(flat_image)
centers = np.uint8(kmeans.cluster_centers_)
res = centers[kmeans.labels_]
result_image2 = res.reshape((img.shape))

# image 3
K = 2
kmeans = KMeans(n_clusters=K)
kmeans.fit(flat_image)
centers = np.uint8(kmeans.cluster_centers_)
res = centers[kmeans.labels_]
result_image3 = res.reshape((img.shape))
Cell 22Compare K = 10, 4, 2
# Plot them out
figure_size = 8
plt.figure(figsize=(figure_size,figure_size))
#original image
plt.subplot(2,2,1),plt.imshow(img)
plt.title('Original Image'), plt.xticks([]), plt.yticks([])
#image 2
plt.subplot(2,2,2),plt.imshow(result_image2)
plt.title('Segmented Image when K = 10'), plt.xticks([]), plt.yticks([])
#image 1
plt.subplot(2,2,3),plt.imshow(result_image1)
plt.title('Segmented Image when K = 4'), plt.xticks([]), plt.yticks([])
#image 3
plt.subplot(2,2,4),plt.imshow(result_image3)
plt.title('Segmented Image when K = 2'), plt.xticks([]), plt.yticks([])
plt.show()
Plot from cell 22 (the solution notebook's saved output)Plot produced by cell 22

Cell 18 is the key line. kmeans.labels_ holds one cluster index per pixel, and indexing centers with it replaces every pixel by its centre's colour. This is "colour quantisation".

LiveCells 12–22: k-means on the Albert Park photo
original
segmented
The photo here is recovered from the notebook's own output of cell 8, at \(160 \times 200\) pixels, so the centres differ from cell 13 by a few levels. With a fixed seed and \(K = 4\), offline scikit-learn on the recovered photo gives centres within 2 levels of cell 14's: \((126, 162, 171)\), \((188, 197, 203)\), \((158, 153, 88)\) and \((61, 85, 79)\).

Cell 23 sums it up: more detail needs more clusters. With \(K = 2\), the water and the grass merge. With \(K = 10\), the tree shadows and the road markings appear.

The clusters are colour groups, not objects. The road, the path and the brightest water share one cluster because their colours are close.

Notebook · Exercise 1 · cells 24–29

Exercise 1 · Pixel clustering with mean shift

Problem statement

Implement pixel clustering using the mean-shift clustering algorithm (sklearn.cluster.MeanShift) on the parrot image. Unlike k-means, mean shift does not need the number of clusters in advance.

Use features that combine colour with the pixel's \((x, y)\) position. Try different values for xy_scaling and bandwidth, and see how they change the result.

Prerequisite 1.1

Colour-and-position features, and one shared bandwidth

Cell 25 builds a five-dimensional feature vector for every pixel:

$$\mathbf f = \big(R,\ G,\ B,\ s\,x,\ s\,y\big), \qquad s = \texttt{xy\_scaling}.$$

scikit-learn's MeanShift uses one flat window of radius \(h\) for all five dimensions. Scaling the coordinates by \(s\) has the same effect as giving position its own bandwidth \(h / s\).

With \(h = 25\) and \(s = 0.005\), the spatial bandwidth is \(25 / 0.005 = 5000\) pixels, far larger than the \(256 \times 171\) image. So in the notebook's setting, position barely matters.

Derivation · why scaling the coordinates is the same as a separate spatial bandwidth

A pixel \(j\) is inside the window around \(i\) when

$$\lVert \mathbf c_i - \mathbf c_j \rVert^2 + s^2 \lVert \mathbf p_i - \mathbf p_j \rVert^2 \le h^2 ,$$

where \(\mathbf c\) is the colour and \(\mathbf p\) the position. Divide by \(h^2\) and write \(s^2/h^2 = 1/(h/s)^2\):

$$\frac{\lVert \mathbf c_i - \mathbf c_j \rVert^2}{h^2} + \frac{\lVert \mathbf p_i - \mathbf p_j \rVert^2}{(h/s)^2} \le 1 .$$

That is an elliptical window with colour bandwidth \(h\) and spatial bandwidth \(h / s\), as on lecture 19, slide 25.

InteractiveThe five feature channels of cell 25, shown as images
Each tile maps one feature to grey levels. In image order, \(x\) is a smooth left-to-right ramp and \(y\) a top-to-bottom ramp. Switch to cell 25's construction to see what the notebook actually feeds to mean shift (see the note after the solution).
Prerequisite 1.2

What MeanShift(bin_seeding=True) does

Running mean shift from all \(43\,776\) pixels would be slow. scikit-learn's implementation saves work in three steps:

  1. Bin seeding. Round every feature vector to a grid of spacing \(h\), and start one window from each occupied grid cell. In the notebook's setting this leaves only a few hundred seeds.
  2. Shift. Move each window to the mean of the points within distance \(h\) (a flat kernel), until it moves less than \(10^{-3} h\).
  3. Remove duplicates. Sort the converged windows by how many points they contain. Keep one, and drop every other window within distance \(h\) of a kept one.

With cluster_all=True, every pixel then joins its nearest surviving centre.

Warm-up exercise · before Exercise 1

Six grey-level pixels have intensities

$$\{\,10,\ 20,\ 30,\ 70,\ 80,\ 200\,\}.$$

Run mean shift by hand with a flat window of radius \(h = 20\): the window around \(x\) contains every value in \([x - 20,\ x + 20]\).

1. Start a window at \(x = 30\). Where is it after one shift?
2. Shift again. Where does the window converge?
3. Start at \(x = 70\). Where does it converge?
4. How many clusters does mean shift find in total?
5. Now use \(h = 200\). How many clusters?
Show solution

From 30, the window holds \(\{10, 20, 30\}\), whose mean is 20. From 20 it holds the same three values, so it stays at 20.

From 70 it holds \(\{70, 80\}\), so it moves to 75 and stays there. From 200 it holds only 200.

The peaks are 20, 75 and 200: three clusters, and nobody chose "3". With \(h = 200\) the single window covers everything and converges to the overall mean \(\approx 68.3\): one cluster.

Bridge to the question. The parrot image is the same computation in five dimensions. The bandwidth decides how many colour peaks survive, and xy_scaling decides how far apart in the image two pixels can be and still share a window.

Notebook cells

Cell 25Load the parrots and build 5-D features
from sklearn.cluster import MeanShift, estimate_bandwidth

# Read an image into BGR Format
!wget https://raw.githubusercontent.com/saraao/COMP90086_image/main/256kodim23.png -q -O 256kodim23.png
img =  cv2.imread('256kodim23.png')
# Convert to RGB
img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)

# Show the image
plt.imshow(img)
plt.axis("off")
plt.show()

# Reshape the image to a 2D array of pixels and 3 color values
flat_image = img.reshape((-1,3))
# Convert to float
flat_image = np.float32(flat_image)

# Get x,y coordinate values of pixels
x, y = np.meshgrid(np.arange(img.shape[0]),np.arange(img.shape[1]))
x = x.reshape((-1,1))
y = y.reshape((-1,1))
xy = np.concatenate((x,y),axis=1)

# Since the sklearn function only allows one value for bandwidth, we will scale
# the (x,y) values relative to the colour values. This has the same effect as
# using different bandwidth values for space and colour.
xy_scaling = 0.005
xy = xy_scaling*xy

# Combine colour and x,y values
flat_image = np.concatenate((flat_image,xy),axis=1)

print(flat_image.shape)
Plot from cell 25 (the solution notebook's saved output)Plot produced by cell 25
Output of cell 25
(43776, 5)
Cell 28Estimate a bandwidth
# Estimate the bandwidth to use with the mean-shift algorithm.
bandwidth = estimate_bandwidth(flat_image,
                                 quantile=.04,
                                 n_jobs=-1, #use all processors your computer has
                                 n_samples=500) #The number of samples to use. Set a small value to reduce the time
Cell 29Mean shift, repaint and plot
bandwidth = 25

# Compute clustering with MeanShift
ms = MeanShift(bandwidth = bandwidth,
               n_jobs=-1,  #use all processors which your computer has
               bin_seeding=True, #True to speed up the algorithm because fewer seeds will be initialized
               cluster_all=True) #If true, then all points are clustered.
ms.fit(flat_image)

# Segmentation of the original image
centers = np.uint8(ms.cluster_centers_) # Convert centers to Integer format
res = centers[ms.labels_]
res = res[:,:3] # For visualisation, take just the color values (first 3 columns)
result_image = res.reshape((img.shape))

# Plot result
figure_size = 8
plt.figure(figsize=(figure_size,figure_size))
# original image
plt.subplot(1,2,1),plt.imshow(img)
plt.title('Original Image'), plt.xticks([]), plt.yticks([])
# final image
plt.subplot(1,2,2),plt.imshow(result_image)
plt.title(str(len(ms.cluster_centers_))+" colors,Mean-Shift"), plt.xticks([]), plt.yticks([])
plt.show()
Plot from cell 29 (the solution notebook's saved output)Plot produced by cell 29

Worked solution: live mean shift on the parrot image

The figure below reruns cells 25 and 29 in your browser. It is a JavaScript port of scikit-learn's algorithm (bin seeding, flat kernel, duplicate removal, cluster_all=True).

On the same pixels, it finds the same number of clusters as scikit-learn 1.7 for every setting we tested: 14, 6, 67 and 32 clusters for \((h, s) = (25, 0.005), (40, 0.005), (15, 0.005), (25, 0.3)\).

LiveCells 25 and 29: mean-shift segmentation
original
mean shift
Runs in a background thread. Small bandwidths take a few seconds. Raise \(\texttt{xy\_scaling}\) and the segments become spatially compact, because the spatial bandwidth \(h/s\) shrinks (lecture 19, slide 26). False colours give every cluster its own colour, which makes the regions' shapes visible.
Official solution, step by step
  1. Step 1 · Build colour-and-position features (cell 25)
  2. Step 2 · Estimate a bandwidth (cell 28)
  3. Step 3 · Fit mean shift (cell 29)
  4. Step 4 · Repaint with the centres' colours
  5. Step 5 · Read the result
Worth discussing
  • The coordinates in cell 25 do not match the pixels. np.meshgrid(np.arange(H), np.arange(W)) returns arrays of shape \((W, H) = (256, 171)\). Flattening them does not follow the image's row-major order, so pixel 300 (row 1, column 44) is given \((x, y) = (129, 1)\). The fix is y, x = np.mgrid[0:H, 0:W]. At \(s = 0.005\) the spatial term is so weak that the bug changes nothing. Raise xy_scaling in the live figure and switch on false colours: at \(s = 0.3\) the notebook's coordinates give 41 clusters instead of 32, and at \(s = 1\) the image breaks into horizontal stripes (294 clusters instead of 131).
  • The spatial term is almost switched off. With \(s = 0.005\), the largest coordinate becomes \(0.005 \times 255 \approx 1.3\), against a colour bandwidth of 25. The notebook's result is effectively colour-only clustering.
  • Cell 28's estimate is overwritten. Cell 29 sets bandwidth = 25 straight after estimating it. The estimate happens to be about 25, but the code never uses it.
  • Our count differs from cell 29's. We get 14 clusters where the notebook prints 17. The parrot image here is recovered from the notebook's plotted output, so its pixel values differ slightly from the original 256kodim23.png.
Notebook · Section 2 · cells 30–32

2 · Superpixel over-segmentation

Cell 31 compares four over-segmentation methods on the astronaut image, downsampled to \(256 \times 256\):

  • Felzenszwalb–Huttenlocher. Treats pixels as a graph and merges greedily, using a scale parameter that favours larger regions.17
  • Quickshift. An approximation of kernelised mean shift in the 5-D colour-and-position space.18 max_dist cuts its hierarchy at a chosen level.
  • SLIC. Local k-means in \((l, a, b, x, y)\), as in A4.
  • Compact watershed. Floods the Sobel gradient image from 250 markers. compactness penalises flooding far from a marker.19
Cell 31Four superpixel methods on the astronaut
# Reshape the astronaut image and convert the image to floating point format.
img = img_as_float(astronaut()[::2, ::2])

# Segments image using different methods
segments_fz = felzenszwalb(img, scale=100, sigma=0.5, min_size=50)
segments_slic = slic(img, n_segments=250, compactness=10, sigma=1,
                     start_label=1)
segments_quick = quickshift(img, kernel_size=3, max_dist=6, ratio=0.5)
gradient = sobel(rgb2gray(img))
segments_watershed = watershed(gradient, markers=250, compactness=0.001)

# Count the number of segments
print(f'Felzenszwalb number of segments: {len(np.unique(segments_fz))}')
print(f'SLIC number of segments: {len(np.unique(segments_slic))}')
print(f'Quickshift number of segments: {len(np.unique(segments_quick))}')
print(f'Watershed number of segments: {len(np.unique(segments_watershed))}')

# Plot them out
figure_size = 8
plt.figure(figsize=(figure_size,figure_size))
# Felzenszwalbs's method
# mark_boundaries allows us to see where we get the end of border of each segment
plt.subplot(2,2,1),plt.imshow(mark_boundaries(img, segments_fz))
plt.title("Felzenszwalbs's method"), plt.axis('off')
# SLIC
plt.subplot(2,2,2),plt.imshow(mark_boundaries(img, segments_slic))
plt.title('SLIC'), plt.axis('off')
# Quickshift
plt.subplot(2,2,3),plt.imshow(mark_boundaries(img, segments_quick))
plt.title('Quickshift'), plt.axis('off')
# Compact watershed
plt.subplot(2,2,4),plt.imshow(mark_boundaries(img, segments_watershed))
plt.title('Compact watershed'), plt.axis('off')
plt.show()
Plot from cell 31 (the solution notebook's saved output)Plot produced by cell 31
Output of cell 31
Felzenszwalb number of segments: 194
SLIC number of segments: 196
Quickshift number of segments: 695
Watershed number of segments: 256
PrecomputedCell 31 across parameter settings
All label maps were computed offline with scikit-image 0.25.2 and embedded in the page. The notebook's settings, marked "cell 31", reproduce its printed counts exactly: 194, 196, 695 and 256. Hover to highlight a single segment.
Worth discussing
  • SLIC's compactness is defined differently in scikit-image. The lecture writes \(D = d_{lab} + \tfrac{\alpha}{S} d_{xy}\). scikit-image follows the SLIC paper, \(D = \sqrt{d_{lab}^2 + (d_{xy}/S)^2\, m^2}\), with \(m\) = compactness. Both trade colour against distance, but the same number does not mean the same trade-off.
  • Watershed always returns exactly the number of markers. The other three methods only aim for a size or a number of segments.
Notebook · Section 3 · cells 33–37

3 · Graph-based segmentation

Section 3 over-segments the coffee-cup image with SLIC (about 400 superpixels), builds a region adjacency graph, and then either merges regions (3.1) or cuts the graph (3.2).

3.1 · Region merging

Cell 34Callbacks: recompute the mean colour and edge weights
def _weight_mean_color(g, src, dst, n):
    """Callback to handle merging nodes by recomputing mean color.

    The method expects that the mean color of `dst` is already computed.

    Parameters
    ----------
    g : RAG
        The graph under consideration.
    src, dst : int
        The vertices in `graph` to be merged.
    n : int
        A neighbor of `src` or `dst` or both.

    Returns
    -------
    data : dict
        A dictionary with the `"weight"` attribute set as the absolute
        difference of the mean color between node `dst` and `n`.
    """

    diff = g.nodes[dst]['mean color'] - g.nodes[n]['mean color']
    diff = np.linalg.norm(diff)
    return {'weight': diff}


def merge_mean_color(g, src, dst):
    """Callback called before merging two nodes of a mean color distance graph.

    This method computes the mean color of `dst`.

    Parameters
    ----------
    graph : RAG
        The graph under consideration.
    src, dst : int
        The vertices in `graph` to be merged.
    """
    g.nodes[dst]['total color'] += g.nodes[src]['total color']
    g.nodes[dst]['pixel count'] += g.nodes[src]['pixel count']
    g.nodes[dst]['mean color'] = (g.nodes[dst]['total color'] /
                                      g.nodes[dst]['pixel count'])

The two callbacks define the merge rule of A5:

  • merge_mean_color runs just before two nodes merge. It adds up their total colour and pixel counts, so the merged region's mean is the pixel-weighted mean.
  • _weight_mean_color sets the new weight between the merged node and each neighbour \(n\): the Euclidean distance between their mean RGB colours.
$$w(\text{dst}, n) = \big\lVert \bar{\mathbf c}_{\text{dst}} - \bar{\mathbf c}_{n} \big\rVert_2 .$$
Cell 35SLIC, RAG and hierarchical merging
# Load the Coffee cup image
img = coffee()

# Use the SLIC algorithm to segment the input image and obtain the region labels
labels = slic(img, compactness=30, n_segments=400, start_label=1)
# Compute the Region Adjacency Graph using mean colors.
g = graph.rag_mean_color(img, labels)
out = label2rgb(labels, img, kind='avg', bg_label=0)
out = np.uint8(out)

# Perform hierarchical merging of a RAG.
# Greedily merges the most similar pair of nodes until no edges lower than 'thresh' remain.
labels2 = graph.merge_hierarchical(labels, g,
                                   thresh=35, #Regions connected by an edge with weight smaller than thresh are merged.
                                   rag_copy=False,
                                   in_place_merge=True,
                                   merge_func=merge_mean_color,#This function is called before merging two nodes.
                                   weight_func=_weight_mean_color)#The function to compute the new weights of the nodes adjacent to the merged node.

# Return an RGB image where color-coded labels are painted over the image.
out2 = label2rgb(labels2, img, kind='avg', bg_label=0)
out2 = np.uint8(out2)

fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True, figsize=(6, 8))

ax[0].imshow(out)
ax[1].imshow(out2)

for a in ax:
    a.axis('off')

plt.tight_layout()
Plot from cell 35 (the solution notebook's saved output)Plot produced by cell 35

merge_hierarchical keeps a priority queue of edges. It repeatedly pops the lowest-weight edge, merges its two regions if the weight is below thresh, and pushes the recomputed edges back. The live figure in Exercise 2 is a JavaScript port of this loop.

3.2 · Normalised cuts

Cell 37SLIC, similarity RAG and normalised cut
img = coffee()

labels = slic(img, compactness=30, n_segments=400,
                            start_label=1)
out = label2rgb(labels, img, kind='avg', bg_label=0)

g = graph.rag_mean_color(img, labels, mode='similarity')
labels_nc = graph.cut_normalized(labels, g)
out_nc = label2rgb(labels_nc, img, kind='avg', bg_label=0)

out = np.uint8(out)
out_nc = np.uint8(out_nc)

# Plot results
fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True, figsize=(6, 8))

ax[0].imshow(out)
ax[1].imshow(out_nc)

for a in ax:
    a.axis('off')

plt.tight_layout()
Plot from cell 37 (the solution notebook's saved output)Plot produced by cell 37

Here mode='similarity' turns colour distances into similarities, \(w_{ij} = e^{-d_{ij}^2/\sigma}\) with \(\sigma = 255\), because a cut needs weights where large means alike (A6).

PrecomputedCell 37: superpixels versus the normalised cut
Computed offline with scikit-image 0.25.2 and np.random.seed(0). Each region is filled with its mean colour, as label2rgb(kind='avg') does. cut_normalized uses a randomly initialised eigensolver, so another run can give slightly different regions.
Notebook · Exercise 2 · cells 38–39

Exercise 2 · The merging threshold

Problem statement

Try different values for the threshold (thresh) in the hierarchical merge function. What is the effect of changing the merging threshold?

Prerequisite 2.1

What the threshold is compared with

The threshold has the units of the edge weights: a distance between two mean RGB colours on the 0–255 scale.

The largest possible distance, from black to white, is

$$\lVert (255, 255, 255) - (0, 0, 0) \rVert_2 = 255\sqrt 3 \approx 441.7 .$$

So thresh=35 means: merge neighbours whose average colours differ by less than about 8% of the full range.

Prerequisite 2.2

Over-segmentation and under-segmentation

A segmentation can fail in two opposite ways:

  • Over-segmentation. One object is split into many regions. Every boundary of the object is probably present, plus many false ones.
  • Under-segmentation. Different objects share one region. Some true boundaries are lost, and later steps cannot recover them.

A merging threshold moves along this spectrum: \(\tau = 0\) keeps every superpixel, and a very large \(\tau\) merges the whole image into one region.

Warm-up exercise · before Exercise 2

Four regions sit in a row, each touching only its neighbours: \(A - B - C - D\). Their mean grey values and pixel counts are:

$$A: 10\ (2\text{ px}), \quad B: 14\ (2\text{ px}), \quad C: 40\ (1\text{ px}), \quad D: 46\ (3\text{ px}).$$

Edge weights are absolute differences of the means. Merge greedily: always the lowest edge, while it is below \(\tau\).

1. Which edge weight is merged first?
2. What is the mean of the merged region \(AB\)?
3. What is the new weight of the edge \(AB - C\)?
4. How many regions remain with \(\tau = 10\)?
5. And with \(\tau = 5\)?
6. Above what threshold does everything become one region?
Show solution

The edges are \(AB = 4\), \(BC = 26\), \(CD = 6\). The lowest is 4, so \(A\) and \(B\) merge into a region with mean \((20 + 28)/4 = 12\).

The edge to \(C\) is recomputed as \(|12 - 40| = 28\). The lowest edge is now \(CD = 6\). If \(\tau > 6\), \(C\) and \(D\) merge to mean \((40 + 138)/4 = 44.5\).

The last edge is \(|12 - 44.5| = 32.5\). So:

  • \(\tau = 5\): 3 regions.
  • \(\tau = 10\): 2 regions.
  • \(\tau > 32.5\): 1 region.

Bridge to the question. The coffee image is the same loop on 417 superpixels. The number of regions is a step function of \(\tau\): it drops each time \(\tau\) passes the weight of the next merge.

Worked solution: the threshold sweep

LiveCell 35 with any threshold
merged regions, mean colour
regions against \(\tau\) (log scale)
this pagescikit-image merge_hierarchical, run offlinecurrent \(\tau\)
The RAG, its 417 superpixels and their colour totals come from the real image and scikit-image's SLIC. The merging runs here, and matches scikit-image's region count at all six reference thresholds (the plum dots).
Worked answer, step by step

The solution notebook leaves cell 39 empty, so this answer is ours.

  1. Step 1 · The threshold sets when merging stops
  2. Step 2 · Low threshold: over-segmentation
  3. Step 3 · Moderate threshold: object-level regions
  4. Step 4 · High threshold: under-segmentation
  5. Step 5 · Conclusion
Notebook · Section 4 · cells 40–55

4 · Instance and panoptic segmentation with Detectron2

Section 4 runs pre-trained models from Detectron2,20 Meta's detection library, on a COCO validation image.21 It needs a GPU runtime on Colab.

4.1 · Set-up

Cell 41Check the GPU
#@title Check gpu
!nvidia-smi
Cell 42Install Detectron2
#@title Install detectron2
!python -m pip install 'git+https://github.com/facebookresearch/detectron2.git'
Cell 43Check versions
#@title Check PyTorch, CUDA and Detectron2 Versions
import torch, detectron2
!nvcc --version
TORCH_VERSION = ".".join(torch.__version__.split(".")[:2])
CUDA_VERSION = torch.__version__.split("+")[-1]
print("torch: ", TORCH_VERSION, "; cuda: ", CUDA_VERSION)
print("detectron2:", detectron2.__version__)
Cell 45Imports
# You may need to restart your runtime prior to this, to let your installation take effect
# Some basic setup:
# Setup detectron2 logger
import detectron2
from detectron2.utils.logger import setup_logger
setup_logger()

# import some common libraries
import numpy as np
import os, json, cv2, random
from google.colab.patches import cv2_imshow

# import some common detectron2 utilities
from detectron2 import model_zoo
from detectron2.engine import DefaultPredictor
from detectron2.config import get_cfg
from detectron2.utils.visualizer import Visualizer
from detectron2.data import MetadataCatalog, DatasetCatalog
Cell 47Download a COCO image
!wget http://images.cocodataset.org/val2017/000000439715.jpg -q -O input.jpg
im = cv2.imread("./input.jpg")
cv2_imshow(im)
Plot from cell 47 (the solution notebook's saved output)Plot produced by cell 47

4.2 · Instance segmentation with Mask R-CNN

Cell 49 loads a Mask R-CNN with a ResNet-50 feature-pyramid backbone, trained on COCO's 80 categories (A7). Detections with a score below 0.5 are discarded.

Cell 49Configure and run Mask R-CNN
cfg = get_cfg()
# add project-specific config (e.g., TensorMask) here if you're not running a model in detectron2's core library
cfg.merge_from_file(model_zoo.get_config_file("COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml"))
cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.5  # set threshold for this model
# Find a model from detectron2's model zoo. You can use the https://dl.fbaipublicfiles... url as well
cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url("COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml")
predictor = DefaultPredictor(cfg)
outputs = predictor(im)
Cell 50Inspect the predicted classes and boxes
# look at the outputs. See https://detectron2.readthedocs.io/tutorials/models.html#model-output-format for specification
print(outputs["instances"].pred_classes)
print(outputs["instances"].pred_boxes)
Output of cell 50
tensor([17,  0,  0,  0,  0,  0,  0,  0, 25,  0, 25, 25,  0,  0, 24],
       device='cuda:0')
Boxes(tensor([[126.6035, 244.8977, 459.8291, 480.0000],
        [251.1083, 157.8127, 338.9731, 413.6379],
        [114.8496, 268.6864, 148.2352, 398.8111],
        [  0.8217, 281.0327,  78.6072, 478.4210],
        [ 49.3953, 274.1229,  80.1545, 342.9808],
        [561.2248, 271.5816, 596.2755, 385.2552],
        [385.9072, 270.3125, 413.7130, 304.0397],
        [515.9295, 278.3743, 562.2792, 389.3803],
        [335.2409, 251.9167, 414.7491, 275.9375],
        [350.9300, 269.2060, 386.0984, 297.9081],
        [331.6292, 230.9996, 393.2759, 257.2009],
        [510.7349, 263.2656, 570.9865, 295.9194],
        [409.0841, 271.8646, 460.5582, 356.8722],
        [506.8767, 283.3257, 529.9403, 324.0392],
        [594.5663, 283.4820, 609.0577, 311.4124]], device='cuda:0'))

Boxes are \((x_1, y_1, x_2, y_2)\) in pixels. Classes are zero-based COCO indices without the background class: 0 is person, 17 horse, 24 backpack and 25 umbrella.

InteractiveCell 50's output drawn on the image
Click two boxes (on the image or in the list) to compute their IoU. The boxes are the exact numbers printed by cell 50. The masks are not printed, so the coloured masks are only visible in the notebook's own rendering below.
Cell 51Draw the instance predictions
# We can use `Visualizer` to draw the predictions on the image.
v = Visualizer(im[:, :, ::-1], MetadataCatalog.get(cfg.DATASETS.TRAIN[0]), scale=1.2)
out = v.draw_instance_predictions(outputs["instances"].to("cpu"))
cv2_imshow(out.get_image()[:, :, ::-1])
Plot from cell 51 (the solution notebook's saved output)Plot produced by cell 51

4.3 · Panoptic segmentation

A panoptic model labels every pixel. Things (person, horse, umbrella) get instance masks, and stuff (tree, fence, dirt) gets one region per class (A1).

Cell 52Panoptic FPN
# Inference with a panoptic segmentation model
cfg = get_cfg()
cfg.merge_from_file(model_zoo.get_config_file("COCO-PanopticSegmentation/panoptic_fpn_R_101_3x.yaml"))
cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url("COCO-PanopticSegmentation/panoptic_fpn_R_101_3x.yaml")
predictor = DefaultPredictor(cfg)
panoptic_seg, segments_info = predictor(im)["panoptic_seg"]
v = Visualizer(im[:, :, ::-1], MetadataCatalog.get(cfg.DATASETS.TRAIN[0]), scale=1.2)
out = v.draw_panoptic_seg_predictions(panoptic_seg.to("cpu"), segments_info)
cv2_imshow(out.get_image()[:, :, ::-1])
Plot from cell 52 (the solution notebook's saved output)Plot produced by cell 52

Cell 54 below repeats the same code and produces the same picture.

Cell 54Panoptic FPN, repeated
# Inference with a panoptic segmentation model
cfg = get_cfg()
cfg.merge_from_file(model_zoo.get_config_file("COCO-PanopticSegmentation/panoptic_fpn_R_101_3x.yaml"))
cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url("COCO-PanopticSegmentation/panoptic_fpn_R_101_3x.yaml")
predictor = DefaultPredictor(cfg)
panoptic_seg, segments_info = predictor(im)["panoptic_seg"]
v = Visualizer(im[:, :, ::-1], MetadataCatalog.get(cfg.DATASETS.TRAIN[0]), scale=1.2)
out = v.draw_panoptic_seg_predictions(panoptic_seg.to("cpu"), segments_info)
cv2_imshow(out.get_image()[:, :, ::-1])
Plot from cell 54 (the solution notebook's saved output)Plot produced by cell 54
Notebook · Exercise 3 · cells 56–57

Exercise 3 · Failure analysis of the classification-based approach

Problem statement

Try using other sample images and see the results of image segmentation. Try to find cases where the model fails. Why do you think these cases are difficult for the classification-based approach?

Prerequisite 3.1

A closed vocabulary of 80 classes

Mask R-CNN segments only what its detector proposes and its classifier accepts:

  1. The region proposal network suggests boxes that look like COCO objects.
  2. Each box is classified into one of 80 classes or background, with a score.
  3. Only boxes with a score above SCORE_THRESH_TEST = 0.5 survive, and only those get a mask.

Anything outside the 80 classes can only be "background", or be forced into the nearest class. A low score means no mask at all, even when the object is plainly visible.

Prerequisite 3.2

Box overlap and non-maximum suppression

Detectors produce many overlapping boxes for one object. Non-maximum suppression keeps the highest-scoring box and deletes any other box of the same class whose IoU with it exceeds a threshold (0.5 in Detectron2's test configuration).

For two axis-aligned boxes, the intersection is itself a box:

$$|P \cap G| = \max\!\big(0,\ \min(x_2, x_2') - \max(x_1, x_1')\big)\cdot \max\!\big(0,\ \min(y_2, y_2') - \max(y_1, y_1')\big).$$

In a crowd, two different people can overlap by more than the threshold, and one of them is suppressed.

Warm-up exercise · before Exercise 3

Use the output of cell 50.

1. How many people did Mask R-CNN find?
2. How many umbrellas?
3. The IoU between box 0 (the horse) and box 1 (its rider):
Show solution

The classes are \([17, 0, 0, 0, 0, 0, 0, 0, 25, 0, 25, 25, 0, 0, 24]\): ten zeros (people), three 25s (umbrellas), one horse and one backpack.

The intersection of boxes 0 and 1 is

$$(338.97 - 251.11) \times (413.64 - 244.90) = 87.86 \times 168.74 \approx 14\,826 .$$

So the IoU is

$$\frac{14\,826}{78\,342 + 22\,478 - 14\,826} \approx 0.172 .$$

They are different classes, so NMS would not compare them anyway. Even if it did, an IoU of 0.17 is far below 0.5.

Bridge to the question. The rider and the horse are easy: both are COCO classes, large and well separated. Failure cases are the images that break one of the three stages of Prerequisite 3.1.

Worked answer, step by step

The solution notebook leaves cell 57 empty, so this answer is ours.

  1. Step 1 · Objects outside the vocabulary
  2. Step 2 · Crowds and occlusion
  3. Step 3 · Small, thin or unusual-looking objects
  4. Step 4 · Stuff and ambiguous boundaries
  5. Step 5 · Why this is hard for classification
Notebook · Exercise 4 · cells 59–60

Exercise 4 · Predicting the output of two segmentation pipelines

Problem statement
  1. Explain how a segmentation approach based on mean-shift clustering of colour would operate on the penguin image. What regions or objects would you expect to be segmented, and why?
  2. Explain how a superpixel segmentation combined with a region-merging approach would operate on the same image. What regions or objects would you expect to be segmented, and why?

The photo (open the original) shows five gentoo penguins walking across a white sandy beach dotted with dark pebbles, with a green-blue sea behind them. Each penguin has a black back and head, a white belly and an orange beak and feet.

Prerequisite 4.1

What colour-only clustering can and cannot see

Mean shift on colour (with no position features) groups pixels by colour alone. Two pixels anywhere in the image join the same cluster if their colours climb to the same peak.

So a cluster is a set of colours, not a connected object. An object made of two very different colours, like a black-and-white penguin, is split between two clusters, and each of those clusters also contains every background pixel of the same colour.

Prerequisite 4.2

What superpixels plus merging adds

Superpixels are spatially compact, and merging only joins adjacent regions. So separate black patches never merge with each other unless they touch, which is an improvement.

But the merge test is still colour similarity. A white belly touching white sand looks like one region.

Warm-up exercise · before Exercise 4

A grey-level version of the scene has five kinds of pixels:

$$\text{back } 25, \quad \text{pebbles } 35, \quad \text{sea } 110, \quad \text{sand } 220, \quad \text{belly } 230 .$$

Each kind is common. Run mean shift on the intensities with a flat window of radius \(h = 30\).

1. How many clusters?
2. The belly (230) shares its cluster with which other intensity?
3. In a RAG, what is the edge weight between a belly region and the sand it touches?
Show solution

The windows find three peaks: near 30 (back and pebbles), at 110 (sea) and near 225 (sand and belly). Three clusters, and neither of them is "penguin".

The belly–sand edge weighs only 10, while the belly–back edge weighs 205. Any threshold that merges anything sensible merges the belly into the sand first.

Bridge to the question. The questions ask for exactly these two predictions on the real colour image. The live figure below runs both pipelines on a redrawn scene.

LiveBoth pipelines on a redrawn penguin scene
redrawn scene (schematic)
(1) mean shift on colour
(2) SLIC + merging
The scene is a schematic redrawing of the photo, made of the same ingredients: sea, white sand, dark pebbles and black-and-white penguins with orange beaks. It is not the photo. Turn on false colours to see which pixels share a label.
Official answer, step by step
  1. Step 1 · (1) How mean shift operates
  2. Step 2 · (1) Expected result
  3. Step 3 · (2) How superpixels plus merging operate
  4. Step 4 · (2) Expected result
  5. Step 5 · The common lesson

Wrap-up

Notes on the notebook

Points worth raising in the tutorial, found while porting the notebook:

  1. Cell 25 builds the coordinates in the wrong order. np.meshgrid(np.arange(H), np.arange(W)) has shape \((W, H)\), so after flattening the \((x, y)\) values do not belong to the pixels they are attached to (the image is \(171 \times 256\), not square). Use y, x = np.mgrid[0:H, 0:W]. It is invisible at xy_scaling = 0.005; at 1 the clusters turn into horizontal stripes (Exercise 1 figure, with false colours).
  2. With xy_scaling = 0.005 the spatial term does almost nothing: the coordinates span at most about 1.3 units against a bandwidth of 25.
  3. Cell 28's bandwidth estimate is overwritten by bandwidth = 25 in cell 29.
  4. Cells 52 and 54 are identical. The markdown in cell 53 promises "other types of models", but the panoptic model simply runs again.
  5. Cell 12 creates KMeans without n_init or random_state, so it prints a FutureWarning and the cluster order (and the swatch order in cell 16) changes from run to run. Cell 16 also hard-codes 4 subplots.
  6. The docstring of _weight_mean_color says "absolute difference", but the code takes the Euclidean norm of the RGB difference.
  7. Small typos in cell 30 and 33: "Felsenszwalb" for Felzenszwalb, and the label2rgb link points to lab2rgb.
  8. cut_normalized is not deterministic (random eigensolver start), so cell 37 can differ between runs.
  9. Exercises 2 and 3 have no solution cells (cells 39 and 57 are empty). The answers on this page are ours.
Wrap-up

Summary of key results

MethodKey formula or ruleParameter that mattersOutput
k-means\(J = \sum_i \lVert\mathbf x_i - \boldsymbol\mu_{c_i}\rVert^2\), assign then update\(k\)exactly \(k\) colour groups
Mean shift\(\mathbf m(\mathbf x) = \dfrac{\sum_i \mathbf x_i g_i}{\sum_i g_i} - \mathbf x \propto \nabla \hat f_h\)bandwidth \(h\) (and spatial \(h/s\))one cluster per density peak
SLIC\(D = d_{lab} + \tfrac{\alpha}{S} d_{xy}\), search in \(2S \times 2S\)\(k\), compactness \(\alpha\)compact superpixels
RAG mergingmerge the lowest edge while \(w < \tau\), recompute meansthreshold \(\tau\)fewer, larger regions as \(\tau\) grows
Normalised cut\(\dfrac{\operatorname{cut}}{\operatorname{assoc}(A,V)} + \dfrac{\operatorname{cut}}{\operatorname{assoc}(B,V)}\)similarity scale \(\sigma\)balanced splits along weak edges
GrabCutcolour GMM likelihood \(+\ \lambda \cdot\) boundary lengthbox prompt, \(\lambda\)one foreground mask
Mask R-CNNdetect boxes, then a binary mask per boxscore thresholdinstances of 80 classes
FCN / U-Netper-pixel cross-entropy, skip connections, \(1\times1\) classifierdepth, input sizea class at every pixel
Transposed convmultiply by \(X^{T}\); length \(s(n-1) + k\)stride \(s\)learned upsampling
SAMfocal \(+\) Dice loss, three masks per promptthe promptclass-agnostic masks
Overlap\(\operatorname{Dice} = 2\operatorname{IoU}/(1 + \operatorname{IoU})\)\(\operatorname{Dice} \ge \operatorname{IoU}\)
Wrap-up

References

  1. Alexander Kirillov, Kaiming He, Ross B. Girshick, Carsten Rother and Piotr Dollár. Panoptic segmentation. In IEEE Conference on Computer Vision and Pattern Recognition (CVPR), pages 9404–9413, 2019. doi:10.1109/CVPR.2019.00963
  2. Edward H. Adelson. On seeing stuff: the perception of materials by humans and machines. In Human Vision and Electronic Imaging VI, Proceedings of SPIE 4299, pages 1–12, 2001. doi:10.1117/12.429489
  3. Stuart P. Lloyd. Least squares quantization in PCM. IEEE Transactions on Information Theory, 28(2):129–136, 1982. doi:10.1109/TIT.1982.1056489
  4. Dorin Comaniciu and Peter Meer. Mean shift: a robust approach toward feature space analysis. IEEE Transactions on Pattern Analysis and Machine Intelligence, 24(5):603–619, 2002. doi:10.1109/34.1000236
  5. Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurélien Lucchi, Pascal Fua and Sabine Süsstrunk. SLIC superpixels compared to state-of-the-art superpixel methods. IEEE Transactions on Pattern Analysis and Machine Intelligence, 34(11):2274–2282, 2012. doi:10.1109/TPAMI.2012.120
  6. Jianbo Shi and Jitendra Malik. Normalized cuts and image segmentation. IEEE Transactions on Pattern Analysis and Machine Intelligence, 22(8):888–905, 2000. doi:10.1109/34.868688
  7. Kaiming He, Georgia Gkioxari, Piotr Dollár and Ross B. Girshick. Mask R-CNN. In IEEE International Conference on Computer Vision (ICCV), pages 2980–2988, 2017. doi:10.1109/ICCV.2017.322
  8. Carsten Rother, Vladimir Kolmogorov and Andrew Blake. "GrabCut": interactive foreground extraction using iterated graph cuts. ACM Transactions on Graphics, 23(3):309–314, 2004 (SIGGRAPH). doi:10.1145/1015706.1015720
  9. Jonathan Long, Evan Shelhamer and Trevor Darrell. Fully convolutional networks for semantic segmentation. In IEEE Conference on Computer Vision and Pattern Recognition (CVPR), pages 3431–3440, 2015. doi:10.1109/CVPR.2015.7298965
  10. Hyeonwoo Noh, Seunghoon Hong and Bohyung Han. Learning deconvolution network for semantic segmentation. In IEEE International Conference on Computer Vision (ICCV), pages 1520–1528, 2015. doi:10.1109/ICCV.2015.178
  11. Vincent Dumoulin and Francesco Visin. A guide to convolution arithmetic for deep learning. arXiv:1603.07285, 2016. arxiv.org/abs/1603.07285
  12. Olaf Ronneberger, Philipp Fischer and Thomas Brox. U-Net: convolutional networks for biomedical image segmentation. In Medical Image Computing and Computer-Assisted Intervention (MICCAI), Part III, LNCS 9351, pages 234–241, 2015. doi:10.1007/978-3-319-24574-4_28
  13. Alexander Kirillov, Eric Mintun, Nikhila Ravi, Hanzi Mao, Chloé Rolland, Laura Gustafson, Tete Xiao, Spencer Whitehead, Alexander C. Berg, Wan-Yen Lo, Piotr Dollár and Ross B. Girshick. Segment anything. In IEEE/CVF International Conference on Computer Vision (ICCV), pages 3992–4003, 2023. doi:10.1109/ICCV51070.2023.00371
  14. Kaiming He, Xinlei Chen, Saining Xie, Yanghao Li, Piotr Dollár and Ross B. Girshick. Masked autoencoders are scalable vision learners. In IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), pages 15979–15988, 2022. doi:10.1109/CVPR52688.2022.01553
  15. Tsung-Yi Lin, Priya Goyal, Ross B. Girshick, Kaiming He and Piotr Dollár. Focal loss for dense object detection. In IEEE International Conference on Computer Vision (ICCV), pages 2999–3007, 2017. doi:10.1109/ICCV.2017.324
  16. Fausto Milletari, Nassir Navab and Seyed-Ahmad Ahmadi. V-Net: fully convolutional neural networks for volumetric medical image segmentation. In International Conference on 3D Vision (3DV), pages 565–571, 2016. doi:10.1109/3DV.2016.79
  17. Pedro F. Felzenszwalb and Daniel P. Huttenlocher. Efficient graph-based image segmentation. International Journal of Computer Vision, 59(2):167–181, 2004. doi:10.1023/B:VISI.0000022288.19776.77
  18. Andrea Vedaldi and Stefano Soatto. Quick shift and kernel methods for mode seeking. In European Conference on Computer Vision (ECCV), Part IV, LNCS 5305, pages 705–718, 2008. doi:10.1007/978-3-540-88693-8_52
  19. Peer Neubert and Peter Protzel. Compact watershed and preemptive SLIC: on improving trade-offs of superpixel segmentation algorithms. In International Conference on Pattern Recognition (ICPR), pages 996–1001, 2014. doi:10.1109/ICPR.2014.181
  20. Yuxin Wu, Alexander Kirillov, Francisco Massa, Wan-Yen Lo and Ross Girshick. Detectron2. Software, 2019. github.com/facebookresearch/detectron2
  21. Tsung-Yi Lin, Michael Maire, Serge J. Belongie, James Hays, Pietro Perona, Deva Ramanan, Piotr Dollár and C. Lawrence Zitnick. Microsoft COCO: common objects in context. In European Conference on Computer Vision (ECCV), Part V, LNCS 8693, pages 740–755, 2014. doi:10.1007/978-3-319-10602-1_48

Lecture slides: COMP90086 Computer Vision, Image Segmentation I (lecture 19) and II (lecture 20), Kris Ehinger, University of Melbourne. Notebook: worksheet11_solution-old-24.ipynb.