# Payoff: What Colour Is This Picture?

*Task 5 of 5 · [Colour Spaces](https://gpu.rocks/learn/colour-spaces-8d79c6af.md) · GPU.js Learn*

The payoff, and a question a person can answer in a glance: what colour is this
picture, mostly? Cut the wheel into 12 bins of 30°, count how many pixels fall in each, and
read off the fullest one.

The counting is a histogram, and the one-thread-per-bin shape it has to take when you have
no atomics is exactly what Histograms & Binning derives — so that kernel comes ready
made below, along with the two you wrote in task 2. What is left for you is the part that is
about colour: turning each pixel into a bin number, and refusing to answer for the pixels
that have no colour to report.

That refusal is the difference between an answer and a rumour. The stones along the bottom
of this picture are grey to within a rounding error, and the direction of a rounding error is
still a perfectly valid-looking angle. Bin them and they smear a plausible-looking 96 pixels
of nonsense across the whole wheel. Drop anything below a saturation floor and the histogram
only counts pixels that actually have a hue — which is why its counts come to fewer than
4,096, on purpose.

**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:** finish `hueBin` — the bin index for each pixel, or
`-1` for a pixel with no usable hue — then find the fullest bin in JavaScript and
log it.

## Requirements

- Return `-1` when the saturation is below `this.constants.floor`
- Otherwise `Math.floor(h / this.constants.width)`, clamped to `this.constants.bins - 1`
- Find the fullest bin in plain JavaScript and `console.log` its index
- Check the counts: they should total `4000`, not 4,096 — the 96 stones are excluded on purpose

## Hint 1 — the floor first

The saturation test comes before anything else, because a pixel that fails it
has no angle worth binning:

```js
if (sat[this.thread.y][this.thread.x] < this.constants.floor) {
  return -1;
}
```

## Hint 2 — the bin

30° per bin, so the index is the hue divided by the width and floored. The
clamp is the same one Histograms & Binning needed: a hue of exactly 360 would
otherwise land in bin 12, which no thread owns.

```js
const h = hue[this.thread.y][this.thread.x];
return Math.min(this.constants.bins - 1, Math.floor(h / this.constants.width));
```

## Hint 3 — reading the answer

The fullest bin is a plain loop over 12 numbers — not worth a kernel. Bin
*b* covers `b * 30` to `(b + 1) * 30` degrees, so printing
that range alongside the index tells you what colour the picture actually is.

## Same idea elsewhere

Hue histograms are the backbone of colour-based tracking: the CAMShift tracker that
ships with OpenCV builds one over a target region and then back-projects it into each new
frame, precisely because hue survives the target walking through a shadow. The two-pass
shape — derive a per-pixel quantity into a map, then bin the map — is the same one every
GPU histogram uses, on every platform, and for the same reason: binning has to read the data
many times, so you want it reading something cheap.

## Starter code

```js
// Map first (a bin per pixel), bin second (a thread per bin).
const gpu = new GPU({ mode });

// The two kernels from task 2, unchanged.
const hue = gpu.createKernel(function (photo) {
  const pixel = photo[this.thread.y][this.thread.x];
  const v = Math.max(pixel[0], Math.max(pixel[1], pixel[2]));
  const m = Math.min(pixel[0], Math.min(pixel[1], pixel[2]));
  const c = v - m;
  if (c === 0) {
    return -1;
  }
  if (v === pixel[0]) {
    const h = 60 * ((pixel[1] - pixel[2]) / c);
    if (h < 0) {
      return h + 360;
    }
    return h;
  }
  if (v === pixel[1]) {
    return 60 * ((pixel[2] - pixel[0]) / c + 2);
  }
  return 60 * ((pixel[0] - pixel[1]) / c + 4);
}, { output: [64, 64] });

const saturation = gpu.createKernel(function (photo) {
  const pixel = photo[this.thread.y][this.thread.x];
  const v = Math.max(pixel[0], Math.max(pixel[1], pixel[2]));
  const m = Math.min(pixel[0], Math.min(pixel[1], pixel[2]));
  if (v === 0) {
    return 0;
  }
  return (v - m) / v;
}, { output: [64, 64] });

const hueBin = gpu.createKernel(function (hue, sat) {
  // TODO: -1 when this pixel's saturation is below this.constants.floor,
  // otherwise its bin: the hue divided by this.constants.width, floored,
  // and clamped to this.constants.bins - 1.
  return 0;
}, {
  output: [64, 64],
  constants: { bins: 12, width: 30, floor: 0.15 },
});

// One thread per bin, each scanning the whole map — the shape a GPU histogram
// has to take when nobody can increment anybody else's counter.
const histogram = gpu.createKernel(function (bins) {
  let count = 0;
  for (let y = 0; y < this.constants.size; y++) {
    for (let x = 0; x < this.constants.size; x++) {
      if (bins[y][x] === this.thread.x) {
        count++;
      }
    }
  }
  return count;
}, { output: [12], constants: { size: 64 } });

const counts = await histogram(await hueBin(await hue(photo), await saturation(photo)));
console.log('counts:', counts);

// TODO: find the fullest bin and log it. Bin b covers b * 30 ... (b + 1) * 30 degrees.
```

---

Interactive version: https://gpu.rocks/learn/colour-spaces-8d79c6af/5

[Previous task](https://gpu.rocks/learn/colour-spaces-8d79c6af/4.md)
