# Strong, Weak, Gone

*Task 4 of 6 · [The Canny Edge Pipeline](https://gpu.rocks/learn/canny-edges-6901c51a.md) · GPU.js Learn*

One threshold forces a bad choice. Set it high and long edges break into dashes
wherever the contrast dips; set it low and the picture fills with noise. Canny's answer is
to refuse to choose: use **two** thresholds and admit that the middle band is
undecided.

Above `high` a pixel is **strong** — it is an edge, no
argument. Below `low` it is **gone**. Between them it is
**weak**: it might be the faint continuation of a real edge, or it might be
nothing, and this stage deliberately does not decide. It just labels. The next stage
decides, and it decides by asking who the pixel's neighbours are.

The labels are numbers, because a kernel returns a number: `1` for strong,
`0.5` for weak, `0` for gone. Order matters — test `high`
first. Written the other way round, `low` catches everything and the strong
branch is the only one that ever fires, which turns three classes back into one.

## Goal

**Goal:** classify every cell of `thin` into
`1` (at or above `this.constants.high`), `0.5` (at or
above `this.constants.low`) or `0`.

## Requirements

- Compare against `this.constants.high` *first*, then `this.constants.low`
- Return exactly `1`, `0.5` or `0` — the next stage tests for those values
- Both comparisons are `>=`, so a pixel exactly on a threshold takes the higher class

## Hint 1 — three lines

```js
const m = thin[this.thread.y][this.thread.x];
if (m >= this.constants.high) {
  return 1;
}
```

…then the same shape for `low` returning `0.5`, and a bare
`return 0;` at the end.

## Hint 2 — why 0.5 and not 2

The value has to survive a float texture and a `>` comparison in
the next kernel, so the three labels want to be far apart and exactly representable.
`0`, `0.5` and `1` are all exact in binary floating
point, and the propagation kernel can then test `> 0.75` for "strong"
and `< 0.25` for "gone" without ever comparing floats for equality.

## Same idea elsewhere

This stage is the most boring kernel in the module and the most universally fast
one: a pure elementwise map, one read and one write per thread, no neighbours, no
coordination — the shape a GPU is happiest with. In CUDA it is a `thrust::transform`,
in WebGPU a one-line compute shader, and in a fused production Canny it does not exist as
a separate pass at all: the comparison gets folded into the tail of the suppression
kernel, because the memory traffic of a whole extra pass costs more than the arithmetic
it saves.

## Starter code

```js
// Stage 4 of Canny: three classes, two thresholds, no decisions.
const gpu = new GPU({ mode });

const classify = gpu.createKernel(function (thin) {
  const m = thin[this.thread.y][this.thread.x];
  // TODO: 1 when m is at or above this.constants.high,
  //       0.5 when it is at or above this.constants.low,
  //       0 otherwise. Mind which one you test first.
  return m;
}, {
  output: [64, 64],
  constants: { low: 0.3, high: 0.7 },
});

const labels = await classify(thin);

let strong = 0;
let weak = 0;
for (let y = 0; y < 64; y++) {
  for (let x = 0; x < 64; x++) {
    if (labels[y][x] === 1) strong++;
    if (labels[y][x] === 0.5) weak++;
  }
}
console.log('strong:', strong, ' weak:', weak, ' gone:', 64 * 64 - strong - weak);
```

---

Interactive version: https://gpu.rocks/learn/canny-edges-6901c51a/4

[Previous task](https://gpu.rocks/learn/canny-edges-6901c51a/3.md) · [Next task](https://gpu.rocks/learn/canny-edges-6901c51a/5.md)
