# A Threshold Per Neighbourhood

*Task 3 of 6 · [Thresholding & Morphology](https://gpu.rocks/learn/thresholding-and-morphology-670eaafa.md) · GPU.js Learn*

Task 1's failure was not bad luck, and no cleverer *single* number fixes
it: Otsu would pick the best one that exists and the lit corner would still saturate.
The premise is what is wrong. One number cannot describe an image whose brightness
changes across the frame.

So stop asking for one. **Adaptive thresholding** compares every pixel
against the mean of *its own* neighbourhood — a 9×9 box average, which is the
clamped sweep the box blur in Convolution & Filters already makes — plus a small
bias `c`. A pixel is foreground when it is at least `c` brighter
than its surroundings. That is a statement about local contrast, and it says nothing
whatever about the lamp.

The window size is the one real choice. It has to be comfortably bigger than the
things you are hunting, or the mean drowns in them and a mark declares itself average;
and comfortably smaller than the lighting changes, or it stops tracking them and you are
back to task 1. Here the marks are 5 pixels across and the light drifts over tens of
pixels, so 9×9 sits nicely in between.

`gray` is the same scene's luminance, one number per pixel. A luminance
pass produces it in a real pipeline — the finale of **Data In, Data Out** is
exactly that pass — and it
is handed over here so the sweep is the only thing you write.

## Goal

**Goal:** return `1` where `gray[y][x]` exceeds
the mean of its clamped 9×9 neighbourhood by more than `this.constants.c`,
and `0` everywhere else.

## Requirements

- Sum the `this.constants.win` × `this.constants.win` neighbourhood, both coordinates clamped to `0…this.constants.last`
- Centre the window: sample `this.thread.y + dy - this.constants.radius`, likewise for x
- Divide by `this.constants.area` to get the mean
- Return `1` when this pixel is above `mean + this.constants.c`, otherwise `0`

## Hint 1 — it is a box blur that ends in a question

The loop is the one from the 3×3 box blur, widened to 9×9 and reading a
single number per cell instead of three channels. The only new line is the last
one: instead of painting the mean, compare against it.

## Hint 2 — the clamped sample

```js
let sy = this.thread.y + dy - this.constants.radius;
if (sy < 0) sy = 0;
if (sy > this.constants.last) sy = this.constants.last;
```

— the same four lines for `sx`, then `sum += gray[sy][sx];`.

## Hint 3 — the finish

```js
const mean = sum / this.constants.area;
if (gray[this.thread.y][this.thread.x] > mean + this.constants.c) return 1;
return 0;
```

The bias goes on the *mean*, raising the bar. Subtract it instead and flat
ground starts reporting itself as foreground.

## Same idea elsewhere

Every vision toolkit ships this: OpenCV's `adaptiveThreshold`,
Sauvola and Niblack binarisation in document scanning, and the local-contrast test at
the front of most feature detectors. On a GPU the box average is separable and can be
done in two passes, or in one with a summed-area table — the same trick that makes
real-time adaptive thresholding cheap on a phone.

## Starter code

```js
// A threshold per pixel: the box-blur sweep, ending in a comparison.
const gpu = new GPU({ mode });

const adaptive = gpu.createKernel(function (gray) {
  let sum = 0;
  // TODO: sum the 9×9 neighbourhood centred on this thread, clamping both
  // coordinates to 0…this.constants.last. Then return 1 when this pixel is
  // more than this.constants.c above the mean, and 0 when it is not.
  return 0;
}, {
  output: [128, 128],
  constants: { last: 127, win: 9, radius: 4, area: 81, c: 0.03 },
});

const mask = await adaptive(gray);

// A look at the result: every 4th pixel, '#' where the mask says foreground.
for (let y = 0; y < 128; y += 4) {
  let line = '';
  for (let x = 0; x < 128; x += 4) line += mask[y][x] > 0.5 ? '#' : '.';
  console.log(line);
}

let lit = 0;
let dark = 0;
for (let y = 0; y < 32; y++) {
  for (let x = 0; x < 32; x++) {
    lit += mask[y][x];
    dark += mask[y + 96][x + 96];
  }
}
console.log('top-left 32x32 foreground:', lit, 'of 1024');
console.log('bottom-right 32x32 foreground:', dark, 'of 1024');
```

---

Interactive version: https://gpu.rocks/learn/thresholding-and-morphology-670eaafa/3

[Previous task](https://gpu.rocks/learn/thresholding-and-morphology-670eaafa/2.md) · [Next task](https://gpu.rocks/learn/thresholding-and-morphology-670eaafa/4.md)
