Skip to content

Lab

Sobel edge detection, in a shader

The same 3×3 convolution I built in Verilog for a Basys 3 FPGA, running here as a WebGL2 fragment shader on your own camera. Nothing is uploaded — the frames go from the camera to your GPU and are discarded. There is no server involved in this page at all.

Frames are processed on your GPU and never leave the page. Nothing is recorded, stored or uploaded.

What it is doing

An edge is a place where brightness changes quickly. Sobel estimates that rate of change by sliding two 3×3 kernels over the image — one measuring the horizontal gradient, one the vertical — and combining them into a magnitude, √(Gx² + Gy²). Bright output means a steep change; flat regions, however bright, come out dark. The middle row and column carry double weight, which is a cheap way of smoothing along the edge while differentiating across it, so the operator is far less twitchy about sensor noise than a bare difference would be.

Gx — vertical edges

-1  0  +1
-2  0  +2
-1  0  +1

Gy — horizontal edges

+1 +2 +1
 0  0  0
-1 -2 -1

The shader runs once per output pixel, gathering that pixel’s eight neighbours and producing its gradient magnitude. Every pixel is independent of every other, which is exactly the shape of problem a GPU exists for — a 1280×720 frame is nine hundred thousand of these, and they all finish inside a frame budget without effort. Instead of a hard threshold the magnitude passes through a smoothstep: a binary cut looks crisp on a test chart and disintegrates into speckle on a noisy phone camera.

The same algorithm, two very different machines

I built this operator before, in Verilog on an Artix-7, and the interesting part is how little the two implementations have in common. On the FPGA there is no image — pixels arrive from an OV7670 one per clock, and a 3×3 neighbourhood only exists if you have already spent block RAM buffering the previous two rows. The whole design is that line buffer, plus reconciling three clock domains that do not agree. It is sequential, and the constraint is memory and timing.

On a GPU the frame is already sitting in memory and every output pixel can read whatever it likes, so the line buffer — the hard part, the part the FPGA design was mostly about — simply does not exist. What costs nothing on one machine is the entire problem on the other. Neither is the “real” implementation; the algorithm is the same fifteen multiply-accumulates either way.

Why it runs in your browser

The obvious architecture for a demo like this is a Python service behind an API. It would also be worse in every way that matters here: a round trip per frame makes real time impossible, an idle server costs money every month, and a cold start greets the one recruiter who clicks the link with a thirty-second spinner. Running client-side costs nothing, scales to any number of visitors, and means your camera frames never travel anywhere. It is also the more honest demonstration — nothing is hidden behind an endpoint.

It is written in plain WebGL2 rather than three.js. This page needs two triangles and one shader; a scene graph would have added the best part of a megabyte to save about forty lines. The Airframe Explorer pays for three.js because it genuinely needs one — that cost stays on that route.

The test target

If you would rather not turn on a camera, the fallback is a synthetic scene drawn in code rather than a bundled video clip — a few kilobytes instead of the heaviest asset on the site. It is built to be read rather than admired: a contrast staircase showing roughly where the operator stops calling a step an edge, a wedge of converging lines that blurs into nothing once neighbouring edges fall inside the same 3×3 window, a smooth gradient that stays almost invisible no matter how bright it gets, and hard-edged shapes moving against all of it.