Task 4 of 6
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.
thin into
1 (at or above this.constants.high), 0.5 (at or
above this.constants.low) or 0.this.constants.high first, then this.constants.low1, 0.5 or 0 — the next stage tests for those values>=, so a pixel exactly on a threshold takes the higher classconst 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.
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.
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.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.