Task 5 of 5

Payoff: What Colour Is This Picture?

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: 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

Hint 1 — the floor first

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

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.

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.

All tasks in Colour Spaces

  1. Three Greys, One Pixel
  2. Hue, Saturation, Value
  3. The Midpoint of 350° and 10°
  4. Select by Colour
  5. Payoff: What Colour Is This Picture?

This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.