# Magnitude, and the Angle Nobody Mentions

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

The Sobel pass you wrote in Convolution & Filters answered one question:
*how strong* is the change here, `√(gx² + gy²)`. Canny needs a second
answer from the same eight reads, and it is the one that usually gets skipped:
*which way* does the change point. `Math.atan2(gy, gx)` — and the next
stage is built entirely on it. Get the angle wrong and non-maximum suppression compares
the wrong two neighbours, silently, on every pixel.

Two things about `atan2` worth saying out loud. It takes the
**vertical component first**: `Math.atan2(gy, gx)`, not the other
way round — swap them and every angle is reflected about 45°. And it returns
**radians** in −π…π, which is why the result can be negative: a gradient
pointing up-and-right and one pointing down-and-left are 180° apart and describe the same
edge. Stage 3 is where that gets sorted out.

`gray` here is already smoothed — it is what stage 1 hands over. Both
kernels read the same 3×3 neighbourhood; the starter has pulled the nine cells into
locals for you.

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

## Goal

**Goal:** compute `gx` and `gy` from the Sobel
grids in both kernels, then return the gradient's **length** from
`magnitude` and its **angle in radians** from
`direction`.

## Requirements

- `gx` is the right column minus the left, middle row counted double; `gy` is the bottom row minus the top
- `magnitude` returns `Math.sqrt(gx * gx + gy * gy)` — the length, not its square
- `direction` returns `Math.atan2(gy, gx)` — vertical component first, in radians
- Border pixels have no full neighbourhood: both kernels already return `0` there

## Hint 1 — the two grids

Same pair Convolution & Filters used:

```js
const gx = (tr + 2 * mr + br) - (tl + 2 * ml + bl);
const gy = (bl + 2 * bm + br) - (tl + 2 * tm + tr);
```

`gy` is bottom minus top because `y` runs *down* the image.
Flip that sign and the magnitude will not notice — it squares everything — but every
angle will.

## Hint 2 — the two returns

```js
return Math.sqrt(gx * gx + gy * gy);   // magnitude
return Math.atan2(gy, gx);             // direction, radians
```

Leaving the `Math.sqrt` off is tempting — comparisons on squares sort the
same way — but every threshold in the rest of this module is calibrated against a
length, and squaring bends the scale.

## Same idea elsewhere

`atan2` is a hardware instruction's worth of work on every GPU:
CUDA has `atan2f` (and `__fdividef` for the cheap path), WGSL and
Metal both spell it `atan2`, and gpu.js compiles `Math.atan2`
straight to GLSL's `atan(y, x)`. OpenCV's `cv::Canny` famously
avoids it altogether — it compares `|gy|` against `tan(22.5°)·|gx|`
with integer arithmetic — which is the same quantisation you are about to write, with the
trigonometry folded away.

## Starter code

```js
// Stage 2 of Canny: two answers from one 3x3 neighbourhood.
//
//        Gx              Gy
//    -1   0  +1      -1  -2  -1
//    -2   0  +2       0   0   0
//    -1   0  +1      +1  +2  +1
//
const gpu = new GPU({ mode });

const magnitude = gpu.createKernel(function (gray) {
  const x = this.thread.x;
  const y = this.thread.y;
  if (x === 0 || y === 0 || x === this.constants.last || y === this.constants.last) {
    return 0;
  }
  const tl = gray[y - 1][x - 1];
  const tm = gray[y - 1][x];
  const tr = gray[y - 1][x + 1];
  const ml = gray[y][x - 1];
  const mr = gray[y][x + 1];
  const bl = gray[y + 1][x - 1];
  const bm = gray[y + 1][x];
  const br = gray[y + 1][x + 1];
  // TODO: gx and gy from the grids above, then return the gradient's LENGTH.
  return 0;
}, {
  output: [64, 64],
  constants: { last: 63 },
});

const direction = gpu.createKernel(function (gray) {
  const x = this.thread.x;
  const y = this.thread.y;
  if (x === 0 || y === 0 || x === this.constants.last || y === this.constants.last) {
    return 0;
  }
  const tl = gray[y - 1][x - 1];
  const tm = gray[y - 1][x];
  const tr = gray[y - 1][x + 1];
  const ml = gray[y][x - 1];
  const mr = gray[y][x + 1];
  const bl = gray[y + 1][x - 1];
  const bm = gray[y + 1][x];
  const br = gray[y + 1][x + 1];
  // TODO: the same gx and gy, then return the gradient's ANGLE in radians.
  return 0;
}, {
  output: [64, 64],
  constants: { last: 63 },
});

const mag = await magnitude(gray);
const dir = await direction(gray);
console.log('on a vertical edge — magnitude:', mag[20][8], ' angle:', dir[20][8]);
console.log('on a horizontal edge — magnitude:', mag[8][20], ' angle:', dir[8][20]);
```

---

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

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