# Payoff: Clean the Mask, Count What Is Left

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

The whole module in one run. `noisy` is a mask straight off a
threshold: sixteen solid rectangles and a confetti of stray one- and two-pixel specks.
Open it once to clear the confetti, then count what survived.

Counting connected blobs sounds like it needs a real labelling algorithm — and in
general it does. But every shape here is an axis-aligned rectangle, and a rectangle has
exactly one **top-left corner**: a foreground pixel whose neighbour above
and whose neighbour to the left are both background. So count corners and you have
counted shapes, with a per-pixel predicate and a sum — the same map-then-reduce shape
Reductions is built on.

Be straight about the caveat. A U-shaped blob has two top-left corners and this would
count it twice. The trick is exact for *this* mask, not for all masks; real
connected-component labelling is a different and much heavier algorithm.

One more border note. This mask has a clear frame — nothing touches the edge — so a
clamped read of a missing neighbour lands on background either way and the count comes
out exact. Had a shape run to the edge, the clamped read would have returned the shape
itself and that corner would have gone uncounted: one more place where the border rule is
a decision, not a detail.

## Goal

**Goal:** open `noisy` once, write a `corners`
kernel that marks each rectangle's top-left pixel, and log the blob count before and
after the cleanup.

## Requirements

- Clean the mask with one opening: erode, then dilate
- `corners` returns `1` only for a foreground pixel whose neighbour above *and* neighbour to the left are background
- Clamp both neighbour indexes — a negative index reads outside the mask
- Sum the corner grid in JavaScript and log both counts with the starter's labels

## Hint 1 — three conditions

A cell is a corner when all three hold: it is foreground, the cell above is
not, and the cell to its left is not. Any one of them failing means 0 — which reads
nicely as three early returns.

## Hint 2 — clamping just the two you need

Only the low side can go out of bounds here, so two clamps are enough:

```js
let up = this.thread.y - 1;
if (up < 0) up = 0;
let left = this.thread.x - 1;
if (left < 0) left = 0;
```

## Hint 3 — the whole body

```js
const y = this.thread.y;
const x = this.thread.x;
if (mask[y][x] < 0.5) return 0;
let up = y - 1;
if (up < 0) up = 0;
let left = x - 1;
if (left < 0) left = 0;
if (mask[up][x] > 0.5) return 0;
if (mask[y][left] > 0.5) return 0;
return 1;
```

## Same idea elsewhere

Cleaning a mask and then reducing it to a handful of numbers is what a vision
pipeline actually does — the mask is never the product. The corner predicate is a
*stencil* in CUDA/ROCm terms and the sum is a standard reduction, so on any
platform this is one filter pass feeding one reduction: precisely the two primitives
this course keeps coming back to.

## Starter code

```js
// Threshold, clean, count. The whole module in one run.
const gpu = new GPU({ mode });

// Given: the two sweeps from the previous task, unchanged.
const erode = gpu.createKernel(function (mask) {
  let lo = 1;
  for (let dy = 0; dy < 3; dy++) {
    for (let dx = 0; dx < 3; dx++) {
      let sy = this.thread.y + dy - 1;
      let sx = this.thread.x + dx - 1;
      if (sy < 0) sy = 0;
      if (sy > this.constants.last) sy = this.constants.last;
      if (sx < 0) sx = 0;
      if (sx > this.constants.last) sx = this.constants.last;
      lo = Math.min(lo, mask[sy][sx]);
    }
  }
  return lo;
}, { output: [128, 128], constants: { last: 127 } });

const dilate = gpu.createKernel(function (mask) {
  let hi = 0;
  for (let dy = 0; dy < 3; dy++) {
    for (let dx = 0; dx < 3; dx++) {
      let sy = this.thread.y + dy - 1;
      let sx = this.thread.x + dx - 1;
      if (sy < 0) sy = 0;
      if (sy > this.constants.last) sy = this.constants.last;
      if (sx < 0) sx = 0;
      if (sx > this.constants.last) sx = this.constants.last;
      hi = Math.max(hi, mask[sy][sx]);
    }
  }
  return hi;
}, { output: [128, 128], constants: { last: 127 } });

const corners = gpu.createKernel(function (mask) {
  // TODO: return 1 only when this pixel is foreground AND the pixels above it
  // and to its left are both background. Clamp both neighbour indexes.
  return 0;
}, {
  output: [128, 128],
  constants: { last: 127 },
});

// Plain JavaScript: how many cells of a mask are foreground.
function count(grid) {
  let n = 0;
  for (let y = 0; y < 128; y++) {
    for (let x = 0; x < 128; x++) n += grid[y][x];
  }
  return n;
}

// TODO: one opening — erode first, then dilate.
const clean = noisy;

console.log('blobs before cleaning:', count(await corners(noisy)));
console.log('blobs after cleaning:', count(await corners(clean)));
```

---

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

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