# Payoff: An Image's Tone Histogram

*Task 5 of 5 · [Histograms & Binning](https://gpu.rocks/learn/histograms-and-binning-dfb254f4.md) · GPU.js Learn*

The payoff, and the histogram everybody has actually seen: an image's
**tone histogram** — how many pixels are dark, how many mid, how many bright.
Every photo editor draws one, because it tells you a shot is underexposed before your eyes
do.

Two kernels, and the reason for two is worth a sentence. Luminance is a per-pixel
calculation and there are 4,096 pixels — but there are 32 bins, so a single histogram kernel
would recompute every pixel's luminance *32 times over*, once per bin thread. Compute
it once into a 64 × 64 map, then histogram the map. Map first, bin second; the map pass is
4,096 luminance evaluations instead of 131,072.

Luminance runs 0 … 1, so 32 bins over that range is a bin every 0.03125 — the same
clamped `floor` as task 3, with `lo = 0` and `span = 1`
doing nothing visible. And the same smoke alarm: 4,096 pixels in, 4,096 counted out.

**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 a 64 × 64 luminance map of `photo`, histogram
it into 32 tone bins, and log the total.

## Requirements

- `luminance`: `output: [64, 64]`, each cell `0.299r + 0.587g + 0.114b` of that pixel
- `histogram`: `output: [32]`, each thread scans the whole map
- Bin with the clamped index from task 3: `Math.min(bins - 1, Math.floor(l * bins))`
- `console.log` the total of the 32 counts — it must be `4096`

## Hint 1 — the map pass

Straight out of any grayscale kernel — read this thread's pixel and return a
number instead of painting it:

```js
const pixel = photo[this.thread.y][this.thread.x];
return 0.299 * pixel[0] + 0.587 * pixel[1] + 0.114 * pixel[2];
```

## Hint 2 — scanning a 2D map from a 1D kernel

The histogram kernel has 32 threads and a 64 × 64 map, so each thread runs two
nested loops over the map. Both bounds are constants, which is what the WebGL backend
needs:

```js
for (let y = 0; y < this.constants.size; y++) {
  for (let x = 0; x < this.constants.size; x++) {
    const bin = Math.min(
      this.constants.bins - 1,
      Math.floor(map[y][x] * this.constants.bins)
    );
    if (bin === this.thread.x) count++;
  }
}
```

## Hint 3 — read the shape of the answer

Once it runs, look at the counts: the first bins and the last bins are empty.
This image never gets truly black or truly white — which is precisely the thing a tone
histogram exists to tell you.

## Same idea elsewhere

Tone histograms are load-bearing infrastructure, not a readout: auto-exposure,
auto-contrast and histogram equalization all start here, and phone ISPs compute one in fixed
function hardware on every frame. The two-pass shape generalizes past images — derive the
quantity once into a buffer, then bin the buffer — and it is the same reason CUDA and WebGPU
pipelines materialize an intermediate rather than recomputing inside an inner loop. Turning
these counts into a cumulative curve (the next step of equalization) is a prefix sum, which
is the one parallel primitive this module does not need.

## Starter code

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

const luminance = gpu.createKernel(function (photo) {
  const pixel = photo[this.thread.y][this.thread.x];
  // TODO: return perceptual luminance — 0.299 R + 0.587 G + 0.114 B
  return pixel[0];
}, { output: [64, 64] });

const histogram = gpu.createKernel(function (map) {
  let count = 0;
  for (let y = 0; y < this.constants.size; y++) {
    for (let x = 0; x < this.constants.size; x++) {
      // TODO: bin map[y][x] into 0 ... bins - 1 with a clamped floor,
      // and count it only when that bin is this thread's own.
      count++;
    }
  }
  return count;
}, {
  output: [32],
  constants: { size: 64, bins: 32 },
});

const map = await luminance(photo);
const counts = await histogram(map);
console.log('counts:', counts);

// TODO: total the counts and log the total. 4096 pixels in, 4096 counted.
```

---

Interactive version: https://gpu.rocks/learn/histograms-and-binning-dfb254f4/5

[Previous task](https://gpu.rocks/learn/histograms-and-binning-dfb254f4/4.md)
