Task 4 of 6
A fresh mask is never clean. Stray single pixels where a highlight caught the sensor; single missing pixels where a shape had a dark fleck. Morphology is the repair kit, and it is built out of two operations you have, in a real sense, already written.
In Convolution & Filters the 3×3 window read nine samples, multiplied them by nine weights and added them up. Keep the window, keep the nine reads, keep the clamped edges — and replace the weighted sum with a minimum. That is erosion: a pixel survives only if every one of its neighbours is foreground, so shapes lose a one-pixel rind and lone specks vanish. Replace it with a maximum and you have dilation: a pixel lights up if any neighbour does, so shapes gain a rind and small holes close over.
Same access pattern, different reduction operator. That is worth saying out loud, because it generalises: a neighbourhood sweep is a shape, and what you do with the nine values you gathered is a separate decision. Sum them and you have a filter; take their extreme and you have morphology.
The window has a name — the structuring element — and a 3×3 square is
the plainest one there is. Edges: this module clamps, so a
sample that falls off the frame reuses the nearest in-bounds cell, exactly as the box
blur did. Treating out-of-bounds as background is just as defensible, and it is a
different answer: a shape lying flush against the frame erodes away along that edge
instead of surviving it. Four of the shapes in mask run to the frame, so the
tests can tell which rule you picked.
0…this.constants.last1, Math.min); dilation keeps the maximum (start at 0, Math.max)Copy the box blur's double loop verbatim, clamps and all. Replace the three
channel sums with one accumulator, and replace += with
Math.min or Math.max.
Start the minimum at the largest value a mask can hold and the maximum at the smallest, so the first sample always wins:
let lo = 1;
// … inside the loops …
lo = Math.min(lo, mask[sy][sx]);
and the mirror image — let hi = 0; with Math.max — for
dilation.
Say it as a sentence. Erosion: "I stay foreground only if all of my neighbours are" — that is an AND over the window, and the AND of 1s and 0s is their minimum. Dilation: "I become foreground if any neighbour is" — an OR, which is their maximum. If your shapes are growing when you asked them to shrink, these two are the wrong way round.
nppiErode/nppiDilate, Metal Performance Shaders has
MPSImageAreaMin and MPSImageAreaMax, and every WGSL post-process
chain grows one eventually. The optimisation is the same as for a box blur — a rectangular
structuring element is separable, so an n×n erosion is a horizontal
pass followed by a vertical one.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.