# Blur First: a Separable Gaussian

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

Canny's first move looks like vandalism: before you go looking for edges, you
**throw detail away**. The reason is that every later stage is built on a
derivative, and the derivative of noise is enormous. A pixel that wobbles by ±0.12 against
its neighbours has no visible brightness to speak of — but a difference operator reads that
wobble at full strength, because a difference is exactly what it is looking for.

The numbers on this task's own picture: run the rest of this module on `gray`
unsmoothed and you get **596 edge pixels, 154 of them in flat background** —
pure noise, promoted to structure. Smooth it first and the same pipeline reports
**299 edge pixels and not one spurious**. That is what the blur buys.

Convolution & Filters already taught the sliding window, the box blur, clamped
edges, and the fact that a box blur is *separable*. Both facts come due here. A
Gaussian beats a box for this job because it has no corners: a box filter's response
oscillates as the window slides, so it manufactures small ridges of its own — precisely
the thing stage 3 is about to hunt for. And a Gaussian is separable too, so a 5×5 window is
**two 5-tap passes, not one 25-tap pass**: 10 reads per pixel instead of 25.

**Array layout in gpu.js**
Image data comes in row-major: `image[y][x]` is the pixel in row *y*,
column *x*, and each pixel is an `[r, g, b, a]` array with channels from
0 to 1. Mind the inversion that catches everyone — sizes are given width-first
(`output: [width, height]`), but indexing runs row-first, so this thread's own
pixel is `image[this.thread.y][this.thread.x]`. Swap those two and you read the
transpose of your image. Three-dimensional data follows the same rule:
`output: [w, h, d]` is indexed `[z][y][x]`.

## Figures

- **five kernels in a row — you build them left to right, then chain them in task 6** — The five stages of Canny as a strip of pictures: noisy photo, blurred photo, a thick gradient ridge, that ridge thinned to one pixel, the thin line broken into strong and weak segments, and one unbroken edge after hysteresis.

## Goal

**Goal:** finish the vertical half of the blur. `blurX` is
written for you; write `blurY` so the pair applies the weights
`[1, 4, 6, 4, 1] / 16` along *each* axis, indexes clamped at the edges.

## Requirements

- Weight five samples `1, 4, 6, 4, 1` and divide the total by `16`
- `blurY` walks **rows** — offset `this.thread.y`, not `this.thread.x`
- Clamp every sample index into `0…this.constants.last`

## Hint 1 — the same filter, turned ninety degrees

`blurX` holds `y` still and moves `x`.
`blurY` does the mirror image: hold `x` still, move
`y`. Copying the body is fine — copying its *axis* is the mistake
the tests are watching for.

## Hint 2 — the clamps

```js
let y0 = y - 2;
if (y0 < 0) y0 = 0;
let y4 = y + 2;
if (y4 > this.constants.last) y4 = this.constants.last;
```

— and the same for `y1` and `y3` at distance 1.

## Hint 3 — the whole return

```js
return (map[y0][x] + 4 * map[y1][x] + 6 * map[y][x]
      + 4 * map[y3][x] + map[y4][x]) / 16;
```

The weights sum to 16, so the divide is what keeps a flat area flat.

## Same idea elsewhere

Separability is not a gpu.js trick — it is why production blurs are fast
everywhere. Metal Performance Shaders' `MPSImageGaussianBlur` and NVIDIA NPP's
`nppiFilterGaussBorder` both decompose internally; a WebGPU post-processing
chain does horizontal-then-vertical into a ping-pong pair of textures. The saving grows
with the kernel: a 15×15 Gaussian is 225 taps as one pass and 30 as two.

## Starter code

```js
// Stage 1 of Canny: smooth, so the derivative that follows is a
// derivative of the picture and not of the noise.
const gpu = new GPU({ mode });

// Pass 1 — horizontal. Weights 1, 4, 6, 4, 1 over five columns.
const blurX = gpu.createKernel(function (gray) {
  const x = this.thread.x;
  const y = this.thread.y;
  let x0 = x - 2;
  if (x0 < 0) x0 = 0;
  let x1 = x - 1;
  if (x1 < 0) x1 = 0;
  let x3 = x + 1;
  if (x3 > this.constants.last) x3 = this.constants.last;
  let x4 = x + 2;
  if (x4 > this.constants.last) x4 = this.constants.last;
  return (gray[y][x0] + 4 * gray[y][x1] + 6 * gray[y][x] + 4 * gray[y][x3] + gray[y][x4]) / 16;
}, {
  output: [64, 64],
  constants: { last: 63 },
});

// Pass 2 — vertical. Same weights, other axis.
const blurY = gpu.createKernel(function (map) {
  const x = this.thread.x;
  const y = this.thread.y;
  // TODO: the same five weighted samples, walking DOWN the column:
  // rows y-2, y-1, y, y+1, y+2, each index clamped to 0…this.constants.last.
  return map[y][x];
}, {
  output: [64, 64],
  constants: { last: 63 },
});

const smooth = await blurY(await blurX(gray));
console.log('noisy background pixel:', gray[4][40], ' smoothed:', smooth[4][40]);
```

---

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

[Next task](https://gpu.rocks/learn/canny-edges-6901c51a/2.md)
