Task 3 of 6

What Moved?

Subtract one frame from the one before it. Everything that stayed put cancels to roughly zero; everything that moved does not. Threshold what is left and you have a motion mask — one bit per pixel, "something happened here" — and that is the first step of essentially every "is anything moving?" system ever shipped, from a doorbell camera to a video codec deciding which blocks to re-encode.

Two things make or break it. The first is the absolute value: a pixel that got darker moved exactly as much as one that got brighter, and dropping Math.abs silently throws away half of every edge — the mask still looks plausible, which is what makes it nasty. The second is noise. A raw thresholded difference is speckled with isolated pixels that the sensor invented, so the mask gets a cleanup pass: a 3×3 majority vote, in the spirit of the morphological open you met in Thresholding & Morphology. A lone hot pixel has one vote out of nine and loses. The inside of something that really moved has nine and does not.

And frame 0 has no predecessor. Eight frames give you seven differences, not eight — the classic off-by-one at the start of a sequence, and the reason so many filters flash garbage on their very first frame.

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: finish the motion kernel — the absolute luminance difference between two frames, thresholded to 1 or 0 — and hand the right pair of frames to it.

Requirements

Hint 1 — the difference, both ways

Motion is a change in either direction:

const change = Math.abs(now - before);
if (change > this.constants.threshold) {
  return 1;
}
return 0;

Drop the Math.abs and the trailing edge of every moving object disappears.

Hint 2 — which pair of frames

previous has to be the frame before this one: frames[i - 1]. Hand the kernel frames[i] twice and the difference is zero everywhere — a perfectly quiet, perfectly useless mask.

Hint 3 — where the loop starts

frames[i - 1] only exists from i = 1 onwards, so the loop starts there. Eight frames, seven differences — the log line prints the count so you can see it.

Same idea elsewhere

Frame differencing is the oldest trick in video and still the load-bearing one. Motion estimation in H.264/AV1 starts from exactly this residual; OpenCV ships absdiff plus a threshold as the canonical first example; and every "smart" security camera on the market is this kernel plus a blob counter. On any GPU it is a one-instruction-per-pixel pass whose real cost is getting the two frames resident at once.

All tasks in Video Filters

  1. Sixteen Milliseconds
  2. Averaging Across Time
  3. What Moved?
  4. Learning the Empty Room
  5. The Payoff: A Virtual Background
  6. Going Live, Honestly

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