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.