Task 3 of 6
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.
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].
motion kernel — the absolute luminance
difference between two frames, thresholded to 1 or 0 — and hand the right pair of frames to
it.motion, take the luminance of both frames and the absolute difference1 when that difference exceeds this.constants.threshold, otherwise 0Motion 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.
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.
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.
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.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.