The short version

How it works

A photo is not just a picture. It is a record of light hitting a piece of silicon behind a piece of glass, and both leave marks. Grainprint looks for those marks. If they are missing, something else made the image.

The one sentence version: real cameras are imperfect in specific, predictable ways, and image generators are not imperfect in those ways.

Why it asks what you are checking

Before anything is analysed, Grainprint asks whether you are checking a photograph or a piece of art or a graphic. It will not guess, because guessing that question is how detectors produce unfair answers.

Almost everything below depends on the image having come out of a camera. An illustration has no sensor grid, no photon grain and no lens flaws, so it fails every one of those tests. If the tool assumed everything was a photo, it would report a hand drawn illustration as AI generated with total confidence, for the sole crime of not having been photographed.

Photograph mode runs everything: the sensor grid, the grain behaviour, the lens signature, plus all the file record and generator checks.

Art or graphic mode switches the camera tests off. They still appear in the results for reference, but they are not allowed to affect the verdict. What remains is the file's own record, generator size buckets, upsampling grids and resampling traces.

For Nerds
Two independent switches, and conflating them was a real bug during development.

isGraphic decides whether CAMERA tests apply. It is true in artwork mode, and also true whenever the file contains real transparency regardless of the chosen mode, because no sensor can record an alpha channel. Transparency is a physical fact, so it overrides the mode you picked and the result says so.

generatorSignals decides whether GENERATOR tests apply, and it stays true in artwork mode. A diffusion model upsamples its latent the same way whether it is producing a landscape or an illustration, so the FFT grid and resampling autocorrelation are just as valid for art. They are only dropped for cut out graphics, where large flat and transparent regions turn them into noise.

Artwork mode also uses lower verdict thresholds (10 and 6 rather than 14 and 8) because far fewer signals are available, and it removes "real photo" as a possible answer entirely, since nothing in that mode could establish it.

The five things it checks

1

What the file says about itself

Most AI tools now write a small record inside the picture saying what made it. Stable Diffusion stores the entire prompt. Canva, Adobe and OpenAI attach a signed Content Credentials record. Cameras write their own record instead: make, model, shutter speed, lens, sometimes GPS.

For Nerds
Grainprint parses the container directly rather than relying on a library. JPEG APP segments, PNG chunks, WebP RIFF chunks and ISOBMFF boxes are walked by hand. It reads: EXIF/TIFF IFD0, the Exif sub IFD and the GPS IFD; XMP packets; IPTC DigitalSourceType from the cv.iptc.org vocabulary; C2PA manifests stored as JUMBF in APP11, caBX, a C2PA RIFF chunk or a top level uuid box, decoding enough CBOR to lift softwareAgent and the c2pa.actions list; PNG tEXt, zTXt and iTXt chunks including deflate decompression, which is where Automatic1111 writes "parameters" and ComfyUI writes its node graph.

Signature validation is deliberately out of scope. Grainprint tells you a manifest exists and what it says, not that it is cryptographically valid. Use contentcredentials.org/verify for that.
2

The sensor pattern

A camera chip cannot see colour. It sees brightness through a grid of tiny coloured filters, so each pixel physically measures only red, or only green, or only blue. The other two are guessed from the neighbours afterwards. That guessing leaves a faint checkerboard in the picture forever. No generator does this, because it never had a filter grid to work around.

For Nerds
Colour filter array trace, following the Popescu and Farid approach. Interpolated samples are by construction smoother than measured ones, so the prediction residual carries a systematic 2x2 periodicity locked to the sensor lattice. Grainprint computes the Laplacian residual on the green plane, partitions pixels into four classes by (y & 1) * 2 + (x & 1), and takes the ratio of largest to smallest class variance. Above 1.25x is a detection; below 1.06x means no trace.

This runs on an even aligned centre crop at native resolution, because any rescale destroys the phase relationship entirely. It is also computed per 128x128 tile, so a region pasted in from elsewhere shows up as a different ratio from its surroundings.
3

Whether the grain behaves like real light

This is the strongest test Grainprint has. Light arrives as individual particles, and they arrive at random. Counting random arrivals means brighter areas are genuinely noisier than darker ones. Every camera ever built obeys this, because it is a property of light itself rather than of the camera. Generated images have no photons, so their grain, if any, is usually the same everywhere.

For Nerds
Photon arrivals are a Poisson process, so variance equals the mean and standard deviation scales with the square root of intensity. Grainprint measures the Pearson correlation between block mean luminance and per block Immerkaer noise sigma, restricted to the flattest 45 percent of 16x16 blocks so texture does not contaminate the noise estimate, and to blocks with mean between 12 and 243 so clipping does not.

r >= 0.4 across a wide brightness span is treated as strong evidence of a real capture. Noise that is clearly present but has |r| < 0.12 is treated as evidence the grain was added afterwards, which is what a grain filter or a generator produces.

Caveat worth knowing: sRGB gamma compresses the highlights, so the relationship flattens or turns over at the very top end. That is why Grainprint tests for correlation rather than fitting a straight line.
4

Lens flaws

Glass bends red, green and blue light by slightly different amounts. So in a real photo the three colour layers are very slightly out of line, and they drift further apart toward the edges of the frame. Software composites its colour layers perfectly, so this radial drift is missing.

For Nerds
Lateral chromatic aberration. Grainprint splits the frame into eight sectors around the centre, estimates the sub pixel shift of R against G and B against G in each by SSD minimisation over integer lags with parabolic refinement, then projects each shift onto the outward radial direction.

The important part is not the size of the shift but its consistency. Random channel noise splits roughly 50/50 on sign, so sign agreement near 0.5 is chance. A real lens pushes every sector the same way, giving agreement near 1.0. The score multiplies mean radial magnitude by agreement above chance.

Vignetting is measured too, as mean luminance across eight radial bins, but it is weighted low because scene composition fakes it constantly.
5

What has happened to the file since

This does not tell you whether an image is real, but it tells you whether the other tests can be trusted. If the file has been saved several times, cropped, or stitched together from pieces, Grainprint says so and lowers its own confidence accordingly.

For Nerds
Three separate techniques.

Double quantisation. Re encoding a JPEG quantises the DCT coefficients twice with different step sizes, so the second grid lands unevenly on the first and the coefficient histogram grows periodic peaks and gaps. Detected by removing the smooth envelope from the histogram of low frequency AC coefficients and taking a small DFT over candidate periods.

JPEG ghost. The image is re encoded at six qualities and each 16x16 tile records which quality minimises its error. A single history gives one dominant answer across the frame. Two different answers in two regions means two different histories, which is how splices are found.

Block grid and resampling. Mean absolute pixel difference grouped by x mod 8 reveals JPEG block structure and its phase, so a non zero phase proves the image was cropped after compression. Separately, the normalised autocorrelation of the Laplacian residual at lags 1 to 8 reveals interpolation, because resizing makes each pixel a fixed linear combination of its neighbours.

Why the answer comes in tiers

Most detectors give you a single number, something like "87 percent AI". That number is how people get accused over a correlation. Grainprint refuses to produce one. Instead every finding is sorted by how much weight it can actually carry, and each one is printed next to its own innocent explanation.

A Tier 1 result means the file declared its own origin, which is close to proof. A Tier 2 result is a physical trace, strong but destroyable. A Tier 3 result is a hint. "Cannot tell" is a permitted and common answer, and an honest one.

Why no AI

Using an AI classifier to detect AI images has three problems. It cannot explain itself, so you cannot check its reasoning. It goes stale the moment a new generator appears. And it fails silently and confidently, which is the worst possible behaviour for a tool people use to make accusations.

Everything here is either reading a published file format specification, or arithmetic you could do by hand with enough patience. Fourier transforms, variance ratios, correlations, chi square. You can read all of it in the js/ folder and check the work.

Before you trust a result

Read what Grainprint cannot do. It is short, and it matters more than this page.