How QR Scanners Decode: Reed-Solomon in Plain English — featured illustration
Technology

How QR Scanners Decode: Reed-Solomon in Plain English

6 min read
Create a QR code

Hold a phone over a receipt in a dim restaurant, tilted, with a shadow falling across half the code, and the link still appears in about a second. The camera did not see a crisp grid of black and white squares. It saw a gray trapezoid with a brightness gradient across it and a fair amount of sensor noise.

Between that frame and the URL on your screen sit several distinct stages, each of which can quietly fail. Most camera apps run the entire decode attempt on every frame at video rate and throw away the ones that do not resolve, which is why a marginal code appears to think for a moment before catching. It is not thinking. It is failing repeatedly and retrying until one frame happens to be good enough.

That also means anything degrading the frame degrades everything downstream: motion blur, a smeared lens, an autofocus that hunts. A blurry code does not fail at the end of the process. It fails before the process starts.

Binarization: deciding which pixels count as dark

The decoder has to sort every pixel into one of two buckets. The naive approach picks a single brightness value for the whole image and splits there, which works in a lab and falls apart on a real table where a lamp on the left makes the left side of the code brighter than the white paper on the right.

So decoders generally threshold locally. The image is divided into small regions and each region gets its own cutoff, derived from the brightness around it. A shadow across the symbol then stops mattering, because the modules inside the shadow are still darker than their immediate neighbors.

This stage is also where color failures happen. Hue is discarded here. What survives is relative lightness, which is why two colors that look strikingly different to a person can land in the same bucket and produce a code that never scans.

Three corners give the decoder its bearings

Next comes the hunt for the finder patterns, the three nested squares in the corners. They are findable because a line drawn through one in any direction crosses dark and light runs in a 1:1:3:1:1 ratio. The decoder can sweep rows of the binarized image looking for that signature without knowing where the code is, how large it is, or how it is rotated.

Three corners, not four. The empty fourth corner is what tells the decoder which way is up. Larger symbols also carry smaller alignment squares distributed through the grid, which serve as extra reference points when the surface curves or the photograph is badly skewed. The structural side of all this is covered in how QR codes work.

Turning a photograph back into a square grid

Almost nobody scans a code straight on. The finder patterns supply known reference points, and from those the decoder computes a perspective transform: the mapping that would turn the photographed quadrilateral back into a square. Apply it and the skew is gone.

Then the decoder needs the module count. The timing patterns, alternating dark and light runs stretched between the finder patterns, let it count. With the grid resolved it samples the center of each cell and finally holds what everyone imagines it had from the beginning: a matrix of ones and zeros.

The format bits describe how to read the rest

Sitting beside the finder patterns is a small block of format information. It carries two things the decoder cannot proceed without: the error correction level in use (L, M, Q, or H) and which mask pattern was applied when the code was generated.

Losing that block would make the entire symbol unreadable, so it is written twice in separate locations and carries its own error correction. It is the most heavily defended region of the code.

Un-masking and putting the codewords back in order

Encoders do not write data straight into the grid. A mask pattern is XORed over the data area first, selected to break up large solid blocks and to avoid accidentally producing something that resembles a finder pattern. Even mixtures of dark and light threshold more reliably, so masking is a scannability feature rather than obfuscation. The decoder XORs the same pattern back off.

It then walks the modules in the specified zigzag order, groups the bits into eight-bit codewords, and de-interleaves them. Interleaving matters more than it sounds. In larger symbols the data is split into blocks and those blocks are woven together across the grid, so a coffee ring that wipes out one physical area takes a few codewords from every block instead of annihilating one block completely. Damage is spread thin on purpose.

Reed-Solomon, without the algebra

Now the repair itself. Alongside the codewords holding your data, the encoder stored extra ones computed from them, and the useful mental picture is a system of simultaneous equations. You have more equations than unknowns, so you can lose several and still solve for every value, including the ones that never arrived.

That is close to what the decoder does. It checks whether the received codewords are consistent with the redundancy. If they are, it is finished. If they are not, it works out where the corruption sits and what the original values must have been for everything to balance again. Two flavors of damage exist and they cost differently: a codeword the decoder already knows is missing is cheap to fix, while a codeword that is present but wrong costs roughly twice as much, because its position has to be solved for as well.

The four levels set how much redundancy exists, approximately seven, fifteen, twenty-five, and thirty percent of codewords recoverable at L, M, Q, and H. Those are ceilings for the symbol as a whole, and they are shared with every other source of damage: print defects, scuffs, glare, a thumb, a logo. Nothing is set aside.

Past the correction limit there is no graceful degradation. The decoder either reconstructs the data exactly or reports failure. A code that scans is bit-for-bit correct, which is why a half-worn sticker tends to work perfectly right up until it does not work at all. Most reasons a code stops scanning trace back to the earlier stages rather than to this one, since binarization and finder detection have no error correction behind them.

All of it happens on the device

Every step described here is arithmetic performed locally on pixels the phone already has. No lookup, no server, no round trip. A scanner decodes in airplane mode because there is nothing to fetch: the text is sitting in the pattern, and the math to recover it runs in a few milliseconds on hardware you are holding.

What the code contains and what the code then does are separate questions, and the second one is covered in what happens when you scan.

Share this article