# Opening and Closing: Order Is the Answer

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

Erosion on its own is a blunt instrument: it kills the specks and takes a rind
off everything else. Dilation on its own is the same mistake in reverse. Run them back to
back and the size change cancels while the repair survives — and which repair you get
depends entirely on which one goes first.

**Opening** is erode *then* dilate. The erosion wipes anything
thinner than the structuring element, the dilation grows the survivors back to size:
small bright specks are gone for good and everything else ends up roughly where it
started. **Closing** is dilate *then* erode: the dilation swallows
small dark holes, the erosion pulls the outlines back in, so pinholes fill and the specks
stay exactly where they were.

They are not inverses and they are not interchangeable. Opening removes; closing
fills. Ask for one and write the other and you get precisely the opposite of what you
wanted — which is the single most reliable way to lose an afternoon to morphology.

Both kernels are given below, so this task is about the plumbing: chain them, and
chain them twice. Two erosions followed by two dilations is an opening with a radius-2
element — it clears out the 3×3 clumps that a single pass is too gentle to touch.

## Figures

- **run them the other way round and you repair the other defect**

## Goal

**Goal:** build an opening, a closing and a two-pass opening from the
given kernels, and report what each one changed with the exact labels the starter uses.

## Requirements

- Opening is `await dilate(await erode(mask))`; closing is `await erode(await dilate(mask))`
- The two-pass opening runs both erosions before either dilation
- Write the `removed` kernel: `1` where `before` is foreground and `after` is not
- Log the three counts with the labels already in the starter

## Hint 1 — chaining kernels

A kernel's result is an ordinary 2D array, so it goes straight back into
another kernel: `await dilate(await erode(mask))` is the whole opening. Every pass is
a separate launch, which is exactly how a real pipeline does it (and Pipelines &
Textures shows how to keep the intermediate on the GPU).

## Hint 2 — which is which

Read the name outwards. An *opening* opens gaps up: it must start by
shrinking, so erosion goes first. A *closing* closes gaps: it starts by
growing. If your "opening" is filling holes instead of clearing specks, you have
written a closing.

## Hint 3 — the difference kernel

```js
const removed = gpu.createKernel(function (before, after) {
  if (before[this.thread.y][this.thread.x] > after[this.thread.y][this.thread.x]) return 1;
  return 0;
}, { output: [128, 128] });
```

Feed it `(mask, opened)` to see what the opening threw away, and
`(closed, mask)` to see what the closing filled in.

## Same idea elsewhere

Opening and closing are the standard pre-processing pair in OpenCV
(`MORPH_OPEN`, `MORPH_CLOSE`) and in every medical- and
satellite-imaging toolchain. On a GPU each is a fixed chain of launches with no readback
in between — the ping-pong between two buffers that WebGPU and CUDA pipelines are built
around.

## Starter code

```js
// Two orders, two completely different repairs.
const gpu = new GPU({ mode });

// Given: the two sweeps from the previous task, unchanged.
const erode = gpu.createKernel(function (mask) {
  let lo = 1;
  for (let dy = 0; dy < 3; dy++) {
    for (let dx = 0; dx < 3; dx++) {
      let sy = this.thread.y + dy - 1;
      let sx = this.thread.x + dx - 1;
      if (sy < 0) sy = 0;
      if (sy > this.constants.last) sy = this.constants.last;
      if (sx < 0) sx = 0;
      if (sx > this.constants.last) sx = this.constants.last;
      lo = Math.min(lo, mask[sy][sx]);
    }
  }
  return lo;
}, { output: [128, 128], constants: { last: 127 } });

const dilate = gpu.createKernel(function (mask) {
  let hi = 0;
  for (let dy = 0; dy < 3; dy++) {
    for (let dx = 0; dx < 3; dx++) {
      let sy = this.thread.y + dy - 1;
      let sx = this.thread.x + dx - 1;
      if (sy < 0) sy = 0;
      if (sy > this.constants.last) sy = this.constants.last;
      if (sx < 0) sx = 0;
      if (sx > this.constants.last) sx = this.constants.last;
      hi = Math.max(hi, mask[sy][sx]);
    }
  }
  return hi;
}, { output: [128, 128], constants: { last: 127 } });

const removed = gpu.createKernel(function (before, after) {
  // TODO: 1 where before is foreground and after is not; 0 otherwise.
  return 0;
}, { output: [128, 128] });

// 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;
}

// TODO: opening is erode then dilate; closing is dilate then erode;
// the two-pass opening erodes twice before dilating twice.
const opened = mask;
const closed = mask;
const openedTwice = mask;

console.log('opening removed:', count(await removed(mask, opened)));
console.log('closing added:', count(await removed(closed, mask)));
console.log('two passes removed:', count(await removed(mask, openedTwice)));
```

---

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

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