I thought I was changing one threshold.
Instead, I found two different problems:
- I was measuring a second-generation encode against a reference that had already been compressed.
- My code could report the stricter threshold correctly while the adaptive encoder still made its decision using the old global one.
The first problem changed how I think about perceptual metrics.
The second changed how I design validation logic.
My original AVIF policy was simple:
target = 60
single allowed worst sample = 58
For small image sets, every representative sample had to reach 60.
For larger sets, I allowed one sample to fall as low as 58 while requiring every other sample to remain at 60 or above.
That worked well enough when I was encoding from the source I actually cared about.
Then the input started looking like this:
higher-quality original: ~2 MB
↓
lossy WebP: ~100 KB
↓
AVIF
The original was gone.
The WebP was all I had.
The obvious question was:
If SSIMULACRA2 60 is acceptable for source → AVIF, why shouldn't the same 60 be acceptable for lossy WebP → AVIF?
The answer depends on what SSIMULACRA2 can actually see.
SSIMULACRA2 measures the reference you give it
SSIMULACRA2 is a full-reference perceptual image metric.
Conceptually, it receives:
reference
candidate
and estimates the perceptual difference between those two images.
Its published quality scale includes approximate anchors around:
50 ≈ medium / fair
70 ≈ high / good
80 ≈ very high
85 ≈ excellent
90 ≈ visually lossless
100 = mathematically lossless
These are useful orientation points, not universal encoding thresholds.
The critical word is reference.
Suppose I still have the real source:
original
↓
AVIF
The comparison is:
original ↔ AVIF
If the AVIF scores 60, the metric is describing the perceptual change between the source I care about and the output I am about to ship.
That is exactly the question I want answered.
Now consider this:
original
↓ lossy encode
WebP
↓ lossy encode
AVIF
If the original no longer exists, the metric sees only:
WebP ↔ AVIF
The first lossy conversion is no longer part of the comparison.
That loss has not disappeared from the pixels.
It has disappeared from the measurement history.
That distinction is the foundation of the whole problem.
Once the derivative becomes the reference, its defects become reference content
Imagine the original contains a perfectly smooth gradient.
The first lossy encoder introduces a little banding.
The WebP still looks acceptable.
Now that WebP becomes the reference for the second conversion.
From SSIMULACRA2's perspective, the existing banding is no longer an error.
It is part of the reference.
If the AVIF introduces more banding, more blur, ringing, edge damage, or other distortion, the metric can react to that additional change.
But it cannot say:
This region already lost information one generation ago.
The earlier image is not available.
So a high score has different meanings depending on the reference.
For a direct encode:
high score
≈
close to the real source
For a second-generation encode:
high score
≈
close to the already-compressed derivative
Those statements are not equivalent.
This does not mean WebP→AVIF transcoding is inherently bad.
It does not mean AVIF automatically amplifies existing artifacts.
It means the second encoder is operating on pixels that may already contain irreversible decisions made by the first encoder.
Source provenance matters more than the extension
This led me away from rules based purely on .webp, .jpg, or .png.
The property I really care about is:
What happened to these pixels before they reached me?
A simplistic implementation would be:
if (extension === ".webp") {
target = 65;
}
I no longer like that rule.
A better model is closer to:
if (sample.isKnownLossyDerivative) {
target = 65;
} else {
target = 60;
}
Why?
Because WebP itself can be lossless.
And there is an important subtlety here:
lossless WebP does not automatically mean clean provenance.
Lossless WebP means that the WebP encoding itself can reproduce its input pixels exactly.
But imagine this chain:
original
↓
lossy JPEG
↓
lossless WebP
The WebP step did not introduce new loss.
But the pixels inside the WebP already came from a lossy derivative.
So the strongest rule is not:
lossless WebP = clean source
It is:
judge the image by the best provenance information available
For my operational policy, that means roughly:
canonical / trusted source
→ 60 / 58
known already-lossy derivative
→ 65 / 63
A lossless WebP created directly from trusted source pixels can stay on the normal policy.
A lossless WebP wrapping pixels that are already known to be a downstream lossy derivative should not magically erase that history.
The container is not the provenance.
Why I use 65 instead of 60
This part needs a disclaimer.
There is no official SSIMULACRA2 rule saying:
second lossy generation
=
add exactly 5 points
There is no formula where:
60 + previous compression = 65
SSIMULACRA2 scores are not a linear distortion budget that can be added across generations.
So 65 is not a scientific constant.
It is an engineering policy.
My reasoning is simpler.
A target of 60 is already more aggressive than the published high/good anchor around 70.
For a first encode from a trusted source, I am willing to spend that quality budget because reducing bytes is part of the objective.
For a source that I know has already gone through lossy compression, I want the next encoder to have less room to introduce additional distortion.
I could use 70.
That would be more conservative.
But it would also push more outputs toward higher AVIF qualities and larger files.
Without controlled evidence that 70 produces enough additional visual benefit for my workload to justify those bytes, I do not want to force it globally.
So I currently use:
trusted source
target = 60
known lossy derivative
target = 65
I think of 65 as a guardrail.
Not a law.
Why 65/63 instead of 65/62
The original rule was:
target = 60
floor = 58
The one permitted outlier therefore had a two-point allowance:
60 - 58 = 2
If I raise the normal target to 65 while preserving the same semantics:
65 - 63 = 2
So the corresponding policy becomes:
target = 65
floor = 63
Using 62 would change the exception to three points:
65 - 62 = 3
That would be odd.
I would be saying:
This source is already lossy, so I want to be stricter.
while also saying:
The worst sample is now allowed to miss its target by more than before.
I could not justify that.
Neither 65 nor 63 is magical.
The useful property is preserving the meaning of the existing policy.
File size cannot tell me how much perceptual damage already exists
A transformation like this looks dramatic:
2 MB → 100 KB
That is roughly a 20× reduction.
It is tempting to turn that number into a quality judgment.
For example:
if (compressionRatio >= 20) {
target = 70;
}
I do not think that is defensible.
Compression efficiency depends on the content.
A clean illustration with flat regions can sometimes compress extremely well.
A noisy photograph may behave very differently.
Gradients, line art, texture, resolution, chroma, entropy, encoder settings, and source format all matter.
So:
compression ratio
!=
perceptual quality
The question I care about is not:
How impressive was the byte reduction?
It is:
Do I know that this image has already passed through a lossy generation?
Then I found the bug that mattered more than 60 vs. 65
Once I decided on the stricter policy, I expected the implementation change to be trivial.
The code already had the concept of source-specific targets.
Conceptually:
const SOURCE_TARGET = 60;
const LOSSY_DERIVATIVE_TARGET = 65;
There was also logic capable of reporting the correct target for an individual sample.
At first glance, everything looked fine.
But the adaptive search that actually chose the AVIF quality still used a global acceptance condition equivalent to:
const GLOBAL_TARGET = 60;
const GLOBAL_FLOOR = 58;
passed =
worstScore >= GLOBAL_FLOOR &&
secondWorstScore >= GLOBAL_TARGET;
The stricter target appeared later in the reporting layer.
So the system could effectively produce this:
sample:
known lossy derivative
required target:
65
actual score:
61.2
log:
BELOW TARGET
while the adaptive search itself still concluded:
61.2 >= 60
PASS
The logs knew about 65.
The encoder was still optimizing against 60.
That was the real bug.
Nothing crashed.
There was no exception.
The build remained green.
The output image looked plausible.
The system could even print the correct stricter threshold.
But the code responsible for selecting the output was solving a different problem.
A threshold that only exists in reporting is not a threshold
That bug gave me a rule I now use beyond image compression:
A configuration value only matters if it reaches the decision that chooses the output.
Defining this:
const LOSSY_TARGET = 65;
does not enforce anything.
Logging this:
required target: 65
does not enforce anything either.
The value has to participate in the actual PASS/FAIL predicate used by the adaptive search.
So instead of keeping the quality policy far away from the measurement, I prefer attaching it directly to each sample.
For example:
function policyForSample(sample) {
if (sample.isKnownLossyDerivative) {
return {
target: 65,
floor: 63,
};
}
return {
target: 60,
floor: 58,
};
}
Then every measured result can be assessed against its own policy:
function assessSample(result) {
const policy = policyForSample(result.sample);
return {
...result,
target: policy.target,
floor: policy.floor,
targetMargin:
result.score - policy.target,
floorMargin:
result.score - policy.floor,
meetsTarget:
result.score >= policy.target,
meetsFloor:
result.score >= policy.floor,
};
}
Now the measurement carries its own context:
score
target
floor
target margin
floor margin
That makes the next step much safer.
The one-outlier rule should be expressed directly
Suppose the policy is:
For smaller collections:
every sample must reach its own target
For larger collections:
at most one sample may miss its target
but
no sample may fall below its own floor
Then I would implement exactly those semantics instead of reconstructing them indirectly from global score ordering.
function evaluateCandidate(
results,
allowSingleOutlier,
) {
const assessed =
results.map(assessSample);
const belowFloor =
assessed.filter(
(item) => !item.meetsFloor,
);
if (belowFloor.length > 0) {
return {
passed: false,
reason: "below-floor",
assessed,
};
}
const belowTarget =
assessed.filter(
(item) => !item.meetsTarget,
);
const allowedMisses =
allowSingleOutlier ? 1 : 0;
const passed =
belowTarget.length <= allowedMisses;
return {
passed,
reason: passed
? "passed"
: "too-many-target-misses",
assessed,
};
}
The rule is visible in the code.
There is no hidden assumption that every sample shares one target.
Mixed thresholds break raw "worst score" ordering
This was another important consequence.
Imagine:
Sample A
score = 63
target = 60
margin = +3
and:
Sample B
score = 64
target = 65
margin = -1
If I sort by raw SSIMULACRA2 score:
63 < 64
Sample A looks worse.
But relative to the actual requirements:
Sample A
PASS
Sample B
FAIL
The meaningful quantity is no longer just:
score
It is closer to:
score - target
A normalized target margin makes heterogeneous policies comparable:
const targetMargin =
score - target;
Now:
margin >= 0
→ target passed
and:
margin < 0
→ target missed
The floor can be represented the same way:
const floorMargin =
score - floor;
The one permitted outlier then has a very clear state:
targetMargin < 0
floorMargin >= 0
This is much safer than sorting raw scores and comparing the two lowest values against global constants.
The adaptive search must use the same predicate
Fixing final validation is not enough.
Suppose the encoder searches for the minimum passing AVIF quality:
q=35
q=43
q=39
q=41
q=40
The definition of passing drives the entire search.
The correct flow is:
encode candidate
↓
measure samples
↓
apply per-sample policies
↓
PASS / FAIL
↓
choose next quality
The dangerous version is:
encode candidate
↓
compare everything to global 60
↓
choose next quality
↓
later report that some samples wanted 65
If the search predicate is wrong, correct reporting afterward cannot repair the quality that was already selected.
Historical quality predictions need policy versioning too
There is another subtle issue if the encoder learns from previous runs.
Suppose historical data says:
similar images usually pass around q=38
That prediction only makes sense together with the policy under which those images passed.
If the historical runs used:
60 / 58
and the current source now requires:
65 / 63
the old history is solving an easier problem.
A correct final acceptance predicate still protects correctness.
The search may simply start too low and climb.
But efficiency suffers, and debugging becomes harder.
So I prefer making the policy part of the history version.
For example:
sim-v1-60-58
becomes:
sim-v2-source60-58-lossy65-63
It is not glamorous.
It makes optimization results reproducible.
If the real original exists, I skip the second lossy generation
Whenever I still have the true source, the best solution is simpler.
Suppose I have:
original: 2 MB
and:
WebP derivative: 100 KB
I would rather do:
original
↓
AVIF
than:
original
↓
WebP
↓
AVIF
The original becomes the reference again.
A stricter second-generation threshold cannot recover information that disappeared during the first lossy encode.
This distinction matters:
WebP → AVIF at target 65
means:
preserve this WebP more faithfully
It does not mean:
reconstruct the image that existed
before the WebP was created
Once information is gone from the pixels, SSIMULACRA2 cannot bring it back.
Neither can a higher AVIF quality setting.
Passing the quality gate does not mean transcoding is worth it
There is another decision SSIMULACRA2 cannot make for me.
Suppose:
source WebP = 100 KB
AVIF = 96 KB
and the AVIF passes 65/63.
The quality requirement is satisfied.
But I saved only 4 KB.
For that saving I added another encoding operation, another lossy generation, processing cost, and another transformation path to maintain.
Maybe the correct optimization is to keep the WebP.
Now consider:
source WebP = 100 KB
AVIF = 65 KB
with the same perceptual policy still passing.
That is a much more interesting trade-off.
So I separate two questions:
Quality gate: Is the additional distortion acceptable?
Size gate: Is the byte saving large enough to justify another encode?
A candidate can pass the first and still fail the second.
What would actually validate the 65 threshold?
The strongest experiment requires preserved originals.
I would build a representative corpus and compare these chains:
A
original → AVIF
target 60
B
original → lossy WebP → AVIF
target 60
C
original → lossy WebP → AVIF
target 63
D
original → lossy WebP → AVIF
target 65
E
original → lossy WebP → AVIF
target 70
For each final image I would record:
final byte size
selected AVIF quality
SSIMULACRA2:
WebP → final AVIF
SSIMULACRA2:
true original → final AVIF
The last comparison is the important one.
It restores the context that disappears when the WebP derivative becomes the only available reference.
I would also manually inspect difficult content: smooth gradients, fine line art, small text, dense texture, dark gradients, saturated transitions, and sources that already contain visible compression artifacts.
I have not run that controlled experiment across a sufficiently representative corpus of preserved originals.
So I cannot honestly claim:
65 is scientifically optimal
What I can defend is:
For a known already-lossy derivative,
I want a stricter additional-loss budget
than I use for a trusted source.
My current policy is 65/63.
That is an engineering policy.
Not a universal SSIMULACRA2 rule.
The rule I use now
My current mental model is:
-
Trusted / canonical source:
60 / 58 -
Known already-lossy derivative:
65 / 63 - Lossless encoding does not erase earlier provenance: classify the pixels by their known history
- Per-sample policy must participate in the real PASS/FAIL predicate
- Mixed thresholds should be evaluated by policy margin, not raw score ordering
- Adaptive search and historical prediction must understand the same policy
- If the true original exists, encode from it
- If AVIF barely saves bytes, consider keeping the current format
The exact thresholds may change as better controlled data becomes available.
The architecture should not.
The metric was not the bug
SSIMULACRA2 was doing exactly what it was supposed to do.
It compared the reference I gave it with the candidate I gave it.
My first mistake was expecting that score to describe quality loss that happened before the reference existed.
My second mistake was adding a stricter threshold without tracing whether that value actually reached the acceptance condition used by the adaptive search.
Both mistakes came from the same habit:
looking at a number without tracing the decision path around it.
A perceptual score has context.
A reference has history.
A threshold has semantics.
And a configuration value only matters if the code that chooses the output actually obeys it.
For me, that lesson ended up being more important than whether the threshold was 60, 63, or 65.
Top comments (0)