# Erode and Dilate: the Sweep, With Min and Max

*Task 4 of 6 · [Thresholding & Morphology](https://gpu.rocks/learn/thresholding-and-morphology-670eaafa.md) · GPU.js Learn*

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.

## Figures

- **gather the same nine samples, then decide what to do with them**

## Goal

**Goal:** two kernels over the same clamped 3×3 sweep — an
**eroder** that returns the smallest sample in the window, then a
**dilator** that returns the largest.

## Requirements

- Create the eroder *first* and the dilator *second* — the tests read them in that order
- Sweep the 3×3 neighbourhood with both coordinates clamped to `0…this.constants.last`
- Erosion keeps the minimum (start at `1`, `Math.min`); dilation keeps the maximum (start at `0`, `Math.max`)
- Nothing else changes — a min or max of 1s and 0s is still exactly 1 or 0

## Hint 1 — the same nine reads

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`.

## Hint 2 — the accumulator

Start the minimum at the largest value a mask can hold and the maximum at the
smallest, so the first sample always wins:

```js
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.

## Hint 3 — which way round?

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.

## Same idea elsewhere

Morphology is a first-class citizen everywhere: NVIDIA's NPP has
`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.

## Starter code

```js
// One sweep, two reduction operators.
const gpu = new GPU({ mode });

const erode = gpu.createKernel(function (mask) {
  let lo = 1;
  // TODO: sweep the 3×3 neighbourhood with both coordinates clamped to
  // 0…this.constants.last, and keep the SMALLEST sample you saw.
  return lo;
}, {
  output: [128, 128],
  constants: { last: 127 },
});

const dilate = gpu.createKernel(function (mask) {
  let hi = 0;
  // TODO: the same sweep, keeping the LARGEST sample.
  return hi;
}, {
  output: [128, 128],
  constants: { last: 127 },
});

// Plain JavaScript: how many cells of a mask are foreground.
function count(grid) {
  let n = 0;
  for (let y = 0; y < 128; y++) {
    for (let x = 0; x < 128; x++) n += grid[y][x];
  }
  return n;
}

console.log('mask      :', count(mask));
console.log('eroded    :', count(await erode(mask)));
console.log('dilated   :', count(await dilate(mask)));
```

---

Interactive version: https://gpu.rocks/learn/thresholding-and-morphology-670eaafa/4

[Previous task](https://gpu.rocks/learn/thresholding-and-morphology-670eaafa/3.md) · [Next task](https://gpu.rocks/learn/thresholding-and-morphology-670eaafa/5.md)
