Task 4 of 5

Select by Colour

Here is the whole module's argument, in one exercise. You want every pixel of the red ball in frame — all of it, top to bottom, shadow included.

In RGB that is a threshold on numbers that move when the light moves. "Red" comes out as something like r > 0.5 and r − g > 0.3, and it works beautifully on the lit top of the ball. Turn the light down and every one of those numbers falls with it, until the test stops being true — for a pixel that is exactly as red as it ever was. That kernel is written for you below, so you can watch it happen.

In HSV, brightness lives in V and nowhere else. Multiply a pixel's r, g and b by the same factor and H and S do not move at all. So the test becomes "hue near red, saturated enough", and the shadow costs you nothing. One wrinkle, and it is task 3's wrinkle: red sits at 0°, so a 15° wedge around it runs from 345° up over the seam to 15°. h > 345 && h < 15 is true for no angle whatsoever. Measure the distance round the wheel instead.

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

turn the light down and RGB loses the colour; HSV only loses the V
Goal: finish hsvMask1 for pixels within this.constants.tol degrees of this.constants.target on the wheel and at least this.constants.minSat saturated, 0 for everything else — and log how many pixels each mask found.

Requirements

Hint 1 — distance round the wheel

Exactly the fold from task 3, then drop the sign:

let d = h - this.constants.target;
if (d > 180) { d = d - 360; }
if (d < -180) { d = d + 360; }
if (d < 0) { d = -d; }

Now d is a distance in degrees, 0 … 180, and it does not care where the seam is.

Hint 2 — the test itself

Two conditions, and both matter:

if (d <= this.constants.tol && s >= this.constants.minSat) {
  return 1;
}
return 0;
Hint 3 — reading the counts

Total each mask with a plain nested loop in JavaScript after the kernels have run. The HSV count should be comfortably the larger — and the gap between them is the part of the ball that RGB gave up on because a lamp was dimmer there.

Same idea elsewhere

This is chroma keying, and the reason a green screen is green: it is the channel a sensor samples most finely, and it is nowhere near skin. Real compositors go further into spaces built for exactly this — YCbCr, or CIE L*a*b* — where the two chromaticity axes are perpendicular to lightness by construction, so a key becomes a distance in a plane rather than a wedge with a seam in it. On the GPU it stays what you just wrote: one thread per pixel, no communication, and the mask is a texture the next pass reads.

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.