Max-Tree Ladderv0.2
Study lab · Blin et al., IEEE TPDS 2022

Max-Tree Ladder v0.2

Climb the paper "Max-Tree Computation on GPUs" one rung at a time. Each rung gives you a picture first, then a task you solve yourself, then the paper's own notation.

Version 0.2 covers the whole paper in 13 rungs: the max-tree itself, the 1D algorithm, Union-Find, CONNECT, atomics, the GPU pipeline, the optimizations and the results.

A 1D signal as a landscape, with its max-tree drawn on top.
See it

A concrete picture: pixels as terrain, thresholds as water. You drag and play with it.

Try it

You answer, click and predict. The first problems are worked for you, then the help fades away step by step.

Name it

Only after the picture is clear do we write the paper's symbols, mapped onto the same picture.

The ladder

Tick "I understand this rung" at the end of each one. Your progress stays in this browser.

Rung 1Paper §2.1

An image is a landscape, a threshold is water

Forget images for a moment. Take one row of pixels. Each pixel has a gray value: bigger means brighter. Draw each value as a column of land. Now flood the land with water up to some height λ (lambda). Whatever sticks out above the water forms islands.

SeeMove the water level

Tip: click any pixel to follow it in the notation panel.

What you see, in the paper's words

NameThe three symbols of §2.1

[f ≥ λ] is the upper level set: every pixel whose value is at least λ. In the picture: all land above the water.

CC(X) splits a set of pixels into connected components: groups that touch each other. In the picture: the islands. So CC([f ≥ λ]) is "the list of islands at water level λ".

Pxλ is the island that contains pixel x at level λ. Without the λ, Px means "use x's own value as the water level".

In 1D, "connected" just means "next to each other". In a 2D image a pixel also touches the pixels above and below it (4-connectivity), or also the diagonal ones (8-connectivity). The idea stays the same.

TryYour turn

Rung 2Paper §2.1, Fig. 2

Drain the lake, and the islands build a tree

Start with the water above everything. Lower it one level at a time. First the highest peaks appear. As the water keeps dropping, islands grow, and neighbouring islands join into one bigger island. Write down every island that ever exists, and link each one to the bigger island that swallows it. That family tree is the max-tree.

SeePlay the drain

Left: each black line is one island, drawn at the level where it is born and spanning the pixels it covers. The vertical line connects an island to the island that swallows it.

Right: the same tree drawn the way the paper draws it (Fig. 2, Fig. 3a): root on top, leaves at the bottom. Leaves (ochre) are peaks. The root is the whole signal.

A new node appears only when the island changes: it is born, it grows, or two islands merge. If the water drops and nothing changes, there is no new node.

TryRead the tree yourself

Use the water slider if you need it. The tree is hidden on purpose.

Rung 3Paper §2.2, Fig. 3

Store the whole tree in one array: parent

A computer cannot store "islands". The paper stores the tree in one array called parent, with one entry per pixel. Every pixel points to exactly one other pixel. That is all. The trick is choosing what to point to.

SeeThree rules

Rule 1. Each pixel belongs to one node: the highest island it reaches. Pixel p2 in example A belongs to the node at level 2, not to the root, even though the root also contains it.

Rule 2. Each node picks one of its pixels as its representative. The paper calls it the canonical element. Here we pick the leftmost one, because that is what the 1D algorithm in Rung 4 produces. (The paper's 2D section picks the bottom-right one. Any fixed rule works, it just has to be the same rule everywhere.)

Rule 3. Pointers: a representative points to the representative of its parent node. Any other pixel points to its own node's representative. The root's representative points to null.

Teal cells are representatives. Click a cell in the array to hear why it points where it does.

Click a cell in the array.
NameWhat "canonical" means in the paper

A tree is canonical when every pixel points straight at a representative, never at an ordinary pixel. The paper's Fig. 3b is not canonical, Fig. 3c is. "Flattening" or "canonicalization" means fixing the pointers so this is true.

The paper also defines an order p ≺ q ("p is lower in the tree than q"): lower gray value first, and pixel position to break ties. It gives two rules: (a) parent(p) ≼ p, your parent is never higher than you, and (b) parent(p) is always a representative. You will need this order again on the GPU rungs, because it is what lets many threads agree on a winner.

TryFill the parent array
Rung 4Paper §3.1, Algorithm 2

Watch the 1D algorithm build the tree in one walk

Draining the lake needs many passes. The 1D algorithm does the same job in a single walk from left to right, using only a small stack. This is the brick the GPU version is built on: each GPU thread runs it on one column of pixels.

Think of a hiker with a notebook. Walk along the landscape. Each time you climb, you write the ground you are leaving behind in your notebook, because you will come back down to it later (push). Each time you descend, you pass back through the levels you wrote down, the latest one first (pop). Last written, first crossed out: that is a stack. Programmers know this pattern as the "monotonic stack" from problems like "next smaller element": the notebook always holds levels that increase from bottom to top.
SeeThe three cases

At each step, compare the new pixel p with r, the representative of the island you are standing on:

UP  f(p) > f(r). You climb. Push r on the stack, because its island is not finished. p becomes the new r.

SAME  f(p) = f(r). Same flat island. parent[p] = r.

DOWN  f(p) < f(r). Islands above p are ending. Pop every stack entry that is ≥ f(p), linking each one under the next. Then either p lands on a level you already know (join it), or p is a brand-new lower island (it becomes the parent of r).

SeeThe machine, step by step

The pseudocode is the paper's Algorithm 2 rewritten in plain steps. The highlighted lines are what the machine just did; the chip next to the step counter tells you which lines of the paper they match.

Rung 5Paper §3.1, Algorithm 2

Now you run the machine

Three stages. In Stage A you only decide the case; the machine does the rest. In Stage B you also count the pops. In Stage C you make every decision yourself. A wrong answer first gets a hint; a second wrong answer shows the solution with the reason.

Rung 6Bridge to §4–5

Explain it back

Write a short answer in your own words first, then open the model answer and compare. Writing the explanation is where the understanding sets. Your drafts stay in this browser.

CheckBefore you climb on
Rung 7Paper §2.1–2.2, §3.1, Algorithm 1, Fig. 2–3

From a row to an image: sort first, then join

Real images are 2D. Two things change. First, a pixel now has more neighbours. Second, we meet the classic algorithm most CPU code uses, Union-Find. It works well, but it needs a sort before anything else, and that sort is exactly what the GPU paper wants to get rid of.

SeeNeighbours in 2D

This is the paper's 𝒩 from §2.1. With 4-connectivity a pixel touches the pixels left, right, above and below. With 8-connectivity it also touches the four diagonal ones. Click any pixel.

Pixels are numbered row by row: p0 is top-left, the last pixel is bottom-right. That number is the pixel's index.

SeeThe paper's own example (Fig. 2 and 3)

Same picture as the paper: nine pixels A to I. Left: each colour is one node of the max-tree. Right: the tree, root on top. The bold letter in each node is its representative.

The paper's representative rule. In the 1D rungs we picked the leftmost pixel of a node. From here on we use the paper's rule (§2.2): the representative is the pixel that comes last in reading order (the bottom-right-most one). That is why H represents the node {D, E, H}.

This rule comes from the order p ≺ q: "p is lower than q" when f(p) < f(q), or when the values are equal and p has the larger index. Every pixel is either lower or higher than any other pixel, never "tied". Remember this: in Rung 9, many threads will use it to agree on a winner.

SeeAlgorithm 1, step by step
The idea. Sort the pixels from brightest to darkest. Walk down that list. Each new pixel looks at its neighbours that are already placed, finds the top of each neighbour's tree, and hangs those trees under itself. Brighter things always end up above darker things, because they were placed first.
S, sorted by ≺, processed from the right
START

Arrows point from a pixel to its parent. The ochre arrow is the one just written. Pixels not placed yet are pale.

NameThe four operations, and why the GPU dislikes them

make-set(p): p becomes a tree of its own. find-root(n): follow parent arrows from n until you reach a pixel with no parent. union(p, r): hang the tree whose root is r under p. FLATTEN: a final pass, darkest first, that makes every pixel point straight at a representative.

Why this is hard on a GPU. The sort is global: you cannot sort one tile without knowing every other tile. And the joins must happen in sorted order, one after the other. A GPU wants thousands of independent jobs, not one long ordered queue. Rung 8 removes the sort completely.

TryRun Union-Find yourself
Processing order (brightest first)
Rung 8Paper §3.2, §4.1, Algorithms 3 and 5, Fig. 5

Merge two finished trees: CONNECT

This is the heart of the paper. Imagine the image is cut into two tiles and each tile already has its own tree. One pair of neighbouring pixels, a on the left and b on the right, touches across the border. CONNECT(a, b) turns the two trees into one correct tree.

You already know this from merge sort. Start at any pixel and follow parent arrows to the root: the levels only go down. So that path is a sorted list. Because a and b touch, their paths must become one path. Merging two sorted lists into one sorted list is the merge step of merge sort: always take the higher of the two tops. CONNECT does exactly that, one link at a time.
SeeCONNECT, step by step
START

Blue lines are islands that so far only know the left tile, orange ones only the right tile, black ones span both. The two rows of chips are the paths from a and from b down to their roots: watch them become one path.

NameThe pieces of Algorithm 3

find-peak-root(x, λ): walk down from x while the next node is still at level λ or above. It stops on the lowest node that is still at or above λ. In the picture: the biggest island containing x that is still above water at level λ.

find-level-root(x) is find-peak-root with λ = f(x): it finds the representative of x's own node.

EXCHANGE(parent(b), a): write a into parent(b) and hand back the value that was there before. That old value is where the zipping continues.

if b ≺ a then swap: when a and b have the same level they are the same node. The order ≺ decides which one stays the representative, so the result is always the same.

TryZip two paths yourself

Two paths, each sorted from high to low. Click the nodes in the order of the merged path, highest first. This is all CONNECT does.

TryRun CONNECT yourself
SeeAlgorithm 5: no sorting at all

Here is the paper's big move (§4.1). Start with every pixel as its own one-pixel tree. Then call CONNECT on every pair of neighbours, in any order. Merging sorted lists gives the same answer whatever order you merge them in, so the final tree is always correct. No sort is needed.

Edges in the order they will be connected

Try "Shuffle" several times: the final tree is identical every time, and it matches Algorithm 1. The price: each CONNECT may walk a long path, so the work is O(G·N) where G is the number of gray levels. Keep that in mind for Rung 11.

Rung 9Paper §3.3, §4.2, Algorithms 4, 6, 7, Fig. 6

Many threads, one memory: atomics

On a GPU, thousands of threads run CONNECT at the same moment, all writing into the same parent array. CONNECT gives the right answer in any order, but only if each write is not destroyed by another thread's write.

The shared booking sheet. Two staff members book the last seat on a shared sheet. Both look and see "seat 12 is free". Both write a name. The second write erases the first, and one booking is silently lost. Programmers call this the lost-update problem. The fix is an atomic operation: "look and write" happens as one step that nobody can split.
SeeThe race lab: you are the GPU scheduler

Node b (level 4) hangs under x (level 1). Two threads want to insert nodes under b at the same time: thread 1 runs CONNECT(b, m) with m at level 3, thread 2 runs CONNECT(b, n) with n at level 2. The correct result is the path b → m → n → x. You decide which thread runs its next step.

Thread 1 · CONNECT(b, m)

    Thread 2 · CONNECT(b, n)

      Try the unlucky timing in each mode: both threads read before either one writes. With a plain write, m is lost. With atomicMax, thread 2's write fails safely and it carries on from what thread 1 wrote.

      NameWhat each atomic does

      atomicMax(parent(b), a) (Algorithm 6): in one unbreakable step, write a into parent(b) only if a is a better parent (higher in the order ≺) than what is there, and always return the old value. Succeed or fail, the thread gets back "the next node I must merge with" and keeps going. Several threads can make progress in the same round.

      atomicCAS, compare-and-swap (Algorithm 7): write only if the cell still holds the value I saw earlier. If someone changed it, my write fails and I retry from the start. Only one thread wins per conflict, so there are more retries (paper Fig. 6a vs 6b).

      Where the idea comes from (§3.3, Algorithm 4): GPU connected-component labelling (Komura, 2015) already joins labels with atomicMin, "always point to the smaller label". A fixed rule that every thread obeys means no cycles and no lost joins. The max-tree version uses the order ≺ as its rule.

      This is also why the paper calls the method lock-free: no thread ever waits for a lock, and some thread always makes progress.

      SeeOne thousand random schedules

      Every edge of the paper's 3×3 example gets its own thread (12 threads). The scheduler picks a random thread for each step. We run 1,000 schedules in each mode and check every final tree against the true one.

      SeePacking a parent into one 32-bit number

      GPU atomics only compare plain integers. The order ≺ compares a gray level first, then an index. The trick is to store each parent entry as one 32-bit record, highest bits first, so that comparing the integers compares the parents the right way.

      TryCheck yourself
      Rung 10Paper §4.3, §5.1, §6.1, Fig. 7–8, Table 2

      The GPU pipeline: tiles, borders, flatten

      Everything comes together here. The image is cut into tiles. The work runs as three kernels, one after another. Inside each kernel, thousands of threads run at the same time.

      A kitchen with several stoves. Each tile is one dish, cooked on one stove (a streaming multiprocessor, SM). If there are more dishes than stoves, the rest wait in a queue and start when a stove is free. Only when every dish is ready does the next course start. Between kernels, the GPU waits for everyone.
      SeeRun the pipeline
        START

        Each colour is one island above the water level, according to the tree built so far. After Stage 1, islands stop at tile borders. After Stage 2, they cross them. Move λ to look at different levels.

        NameGPU words, and what the paper measured

        Kernel: one program launched on the GPU; the three stages are three kernels. Thread block: a group of threads that share fast shared memory; here one block per tile. SM: the hardware unit that runs blocks. Warp: 32 threads that execute the same instruction together. Global memory: the big, slow memory every block can see.

        Stage 1 in shared memory: tile indices fit in 16 bits, so the packed 32-bit record from Rung 9 works with atomicMax. At the end, the tile is copied to global memory and its indices converted to 32 bits. Stage 2 in global memory: one thread per pair of pixels across a tile border. Stage 3: flatten every pixel.

        The 1D optimization (§5.1, Fig. 8): each thread first builds the tree of one 16-pixel column with the stack algorithm from Rungs 4–5. Each thread owns its column, so no atomics are needed. Then the columns are merged with CONNECT. This halves the CONNECT calls. The paper measured local-tree time cut by about 50% and the whole algorithm about 30% faster (Table 2). Fewer threads doing more work each was faster, because fewer atomic writes collided. The opposite design, one thread per pixel, was slower.

        TryCount and order
        Rung 11Paper §5.2–5.3, §6.2–6.3, Fig. 9–10, 13–14, Table 3

        Three hard cases

        The pipeline works. Three situations make it slow, and the paper answers each one: diagonal neighbours, images with many gray levels, and threads that wait for each other.

        A. 8-connectivity: drop the edges that change nothing

        With diagonals, each border pixel touches three pixels on the other side instead of one, so the border needs three times as many CONNECT calls. Without a fix, the paper measured the Base algorithm about 2.2× slower (+120%) and the 1D-optimized one 2.5× slower (§6.2).

        The fix rests on one observation: an edge between u and v is useless if u and v are already joined by another path that never dips lower than both of them. Removing that edge changes no island at any water level, so the tree stays the same.

        In the "all edges" views, click any border edge to remove it yourself. The checker tells you at once whether the max-tree changed.

        Propositions 1 and 2 (8-connectivity) keep at most one edge per border position, chosen by comparing each pixel with its neighbour along the border. Proposition 3 (4-connectivity) drops the straight edge at position k when a neighbouring position is a higher bridge. With the rules applied, each thread makes at most one CONNECT, and 8-connectivity costs about the same as 4-connectivity (Fig. 13).

        Reading note: a typo in Proposition 3

        As printed, Proposition 3 drops edge k when the neighbouring pair's minimum is smaller than this pair's minimum. Tested against brute force, that version changes the tree in most random cases. The reversed comparison (drop edge k when a neighbouring pair's minimum is larger) never changes it, and it is exactly what the paper's own Fig. 9c shows: edges u1–v1, u4–v4 and u6–v6 kept. This page uses the reversed, tested version. Propositions 1 and 2 are used exactly as printed. On the paper example the 8-connectivity result differs slightly from Fig. 9c where values tie (v3 = v4 = v5 = 9); the checker confirms the printed rules keep the same tree.

        A trade-off the paper points out: with 4-connectivity, removing edges also removes parallelism. In the extreme, one thread merges a whole branch while the others sit idle.

        B. 16-bit images: long paths in slow memory

        More gray levels means more nodes and longer paths. Each CONNECT walks further, through scattered places in slow global memory. On 16-bit images the paper measured the Base algorithm about 30 times slower than on 8-bit images. Merging in global memory took 90% of the time, and the L2 cache hit rate fell below 40% (§6.3).

        Live: the same image at different bit depths

        A 32×32 image cut into 8×8 tiles. The chart shows the average number of parent reads each border thread makes in Stage 2. A small image saturates early; real images with millions of pixels keep growing.

        Paper, Table 3 (Desktop 1)

        8-bit, Base16-bit, Base16-bit, with halo

        MPix/s, higher is better. The halo (Fig. 10) duplicates the pixels along tile borders, so each tile already holds a copy of its neighbour's border. That shortens the first walk of every border CONNECT. It costs about 2% extra time and helps, but 16-bit stays up to 20 times slower.

        C. Threads that wait for each other

        The 32 threads of a warp move together. If one thread must walk a long path while its neighbours finish quickly, the whole warp waits for the slowest one (Fig. 14). On some images fewer than 3 of the 32 threads in a warp were active.

        TryCheck yourself
        Rung 12Paper §1, §6, §7, Fig. 1, 11–12, Table 2

        What max-trees are for, and how fast the GPU builds them

        A. A max-tree filter you can drive

        Once the tree exists, many filters are just "remove some nodes". Here every node smaller than a minimum area is merged into its parent: small bright specks vanish, big bright shapes stay with their exact outline. The paper's Fig. 1a removes text noise the same way, and astronomers use it to separate stars from galaxies.

        Input
        Filtered

        Other uses named in the paper: background removal in medical images, document cleaning, salient object detection (Fig. 1b), MSER features for matching images, and the tree of shapes, which is built from max-trees.

        B. The speed results (Fig. 12)

        CPUGPU, including memory transferGPU, kernel time only

        The headline: on a single image, memory transfer included, the GPU is at least 5 times faster than the best parallel CPU algorithm on comparable hardware. On a stream of images, the transfer of the next image overlaps the work on the current one, so only kernel time counts: about 10 times faster.

        C. Where the time goes (Table 2)

        Stage 1 · local treesStage 2 · merge bordersStage 3 · flatten

        Desktop 1, in milliseconds. The paper gives only percentages for Stages 2 and 3 of the 1D-optimized version; those segments are computed from the percentages and the total. The local stage is the biggest cost, and the 1D optimization roughly halves it.

        D. What comes next (§7)

        3D images: build 2D slices, then merge them in depth like tiles. Full 3D 26-connectivity would mean a huge number of CONNECT calls in global memory. Tree of shapes: it is built with a max-tree as its last step, so this GPU algorithm could speed it up. Alpha-tree: closely related to a min-tree on the image's edge graph, so the method looks adaptable.

        Rung 13Whole paper

        Explain it all back

        Write first, then compare. These are the questions a committee or a reviewer would ask about this paper.

        QuizMixed review across all rungs
        CheckCan you say yes to all of these?

        Map and glossary

        Where each rung lives in the paper

        Glossary · مسرد المصطلحات