Task 1 of 6

One Number for the Whole Image

Everything downstream — counting, measuring, tracking — wants a binary mask: one bit per pixel, foreground or background. The cheapest way to make one is a threshold. Pick a number; call every pixel brighter than it foreground.

Per pixel, no neighbours, no order, nothing shared: the friendliest shape a kernel can have, and exactly the pure map "Thinking in Parallel" calls the easy case. One thread, one pixel, one comparison.

It is also where real images bite back. photo is lit unevenly — bright at the top-left corner, fading away to the bottom-right — with small bright marks scattered over the whole frame. Run the starter and read the ASCII dump it prints: one corner comes back solid, the opposite corner comes back empty, and only a diagonal band across the middle finds the marks at all.

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

one number cannot serve both ends of a lit scene; a number per neighbourhood can
Goal: return a 128×128 mask — 1 where this pixel's luminance is above this.constants.t, 0 everywhere else.

Requirements

Hint 1 — luminance first, comparison second

Two steps, both of which you have written before: reduce the pixel to one number, then compare that number. The image is warm-toned, so red and luminance are genuinely different pictures — thresholding pixel[0] gives a mask that is wrong by a couple of lighting bands.

Hint 2 — returning a bit

A kernel returns a number, so the "bit" is the number 1 or the number 0:

if (lum > this.constants.t) return 1;
return 0;
Hint 3 — the whole body
const p = photo[this.thread.y][this.thread.x];
const lum = 0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2];
if (lum > this.constants.t) return 1;
return 0;

Same idea elsewhere

A threshold is one step() in GLSL/WGSL, one predicated store in CUDA, and a single fused op in every imaging library — it is so cheap that camera ISPs do it in silicon. Which is exactly why the interesting question is never how to compare, but what to compare against.

All tasks in Thresholding & Morphology

  1. One Number for the Whole Image
  2. Let the Histogram Pick the Number
  3. A Threshold Per Neighbourhood
  4. Erode and Dilate: the Sweep, With Min and Max
  5. Opening and Closing: Order Is the Answer
  6. Payoff: Clean the Mask, Count What Is Left

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