A quality number is a lie: what ssimulacra2 measures instead
Every image encoder has a quality knob — -q 80, -quality 90, a cq-level. It feels like a promise: turn it to 80 and every image comes out “80% good.” It isn’t. The number sets a compression parameter, not a result, and the result depends entirely on the picture.
We can show exactly how much it lies.
The same setting, wildly different quality
We encoded 1,998 real photographs to JPEG at a fixed -q 80 (libjpeg-turbo) and scored each one with ssimulacra2, a perceptual metric where higher means closer to the original.
vips -q 80 on 1,998 photos |
ssimulacra2 |
|---|---|
| worst image | 58 |
| 10th percentile | 74 |
| median | 77 |
| 90th percentile | 80 |
| best image | 87 |
One setting. A 29-point spread in actual quality. At -q 80, one photo comes out at 58 — visibly degraded, blocking and mush — while another sails through at 87, indistinguishable from the source and carrying bytes it didn’t need. The number on the dial told you nothing about either.
It gets worse across encoders. -q 80 in JPEG is not -q 80 in WebP is not cq-level 30 in AVIF — each scale is defined by its own encoder, so the same digits mean different things. A quality number isn’t even comparable to itself in a different tool.
Why a photo’s content decides its quality
A quality setting picks how aggressively to throw away detail. A flat sky or a soft portrait has little detail to lose, so aggressive settings barely hurt — it scores high. A photo of foliage, gravel, or fabric is nothing but fine detail, so the same setting shreds it — it scores low. The encoder applied the identical rule; the images had wildly different amounts to lose.
That’s why “just use quality 80” quietly ships you two failures at once: some images degraded past the point a user notices, and some images 30% larger than they needed to be. You can’t see it in the settings — only in the output.
What to target instead
Target the result, not the parameter. ssimulacra2 is built for this: it’s calibrated in units of perceived difference (roughly, “just-noticeable differences” from the original), so a target score means the same thing on every image and in every format.
- ~90 — indistinguishable from the original at normal viewing.
- ~85 — high quality; you have to look for the difference.
- ~80 — good; fine for most web imagery.
- below ~70 — the difference is visible.
Pick a target, then let the encoder’s quality be searched per image to hit it. The foliage photo gets a higher-quality setting because it needs one; the flat sky gets a lower one because it can. Every image lands at the same perceived quality, each at the fewest bytes that reach it. No image is quietly ruined, none is quietly bloated.
This is what Patu does on every image: a per-image search on encoder quality to hit a ssimulacra2 target, rather than a fixed -q that means something different on every photo.
curl -X POST "https://patu.dev/v1/compress?target=85" \
-H "X-Api-Key: $PATU_KEY" \
--data-binary @photo.jpg -o photo.avif
The metric is doing the work a quality number pretends to do: describing the picture you actually get.
Method and data: the Patu benchmark harness over the photo category (1,998 real photographs); libjpeg-turbo via vips; scored with ssimulacra2 via the rust-av implementation. The percentiles are across all scored images at the fixed setting.