Изначально правило сжатия у меня было очень простым: кодировать AVIF на минимальном quality, который ещё проходит SSIMULACRA2 target 60. Для больших наборов изображений я разрешал одному репрезентативному sample опуститься до 58, а все остальные должны были оставаться на 60 или выше.
Для PNG и обычных исходных изображений меня это устраивало. Проблема появилась, когда я начал получать WebP-файлы, которые ещё до меня уже были lossy-сжаты из оригиналов более высокого качества.
оригинал более высокого качества: ~2 MB
↓
lossy WebP: ~100 KB
↓
AVIF
Должна ли и вторая конвертация по-прежнему проходить с 60 относительно WebP? Сначала я думал, что да. 60 остаётся 60. Но изменилось не число — изменилась reference image.
Метрика не ошибалась. Изменилась reference.
SSIMULACRA2 сравнивает reference image с искажённым изображением и оценивает perceptual difference именно между этими двумя входами. Метрика рассчитана на реакцию на такие артефакты компрессии, как blur, ringing и искусственно появившиеся границы; опубликованные материалы оценки включают искажения JPEG, WebP, AVIF и других кодеков. Документация](https://github.com/cloudinary/ssimulacra2/blob/main/README.md%22>Документация) SSIMULACRA2 описывает саму метрику и примерные quality anchors.
Если я кодирую AVIF напрямую из хорошего source, сравнение фактически выглядит как source → AVIF. Score 60 тогда описывает потери, которые внесла именно эта конвертация.
У уже lossy-сжатого WebP реальная история другая:
original
↓ first lossy encode
WebP
↓ second lossy encode
AVIF
Но SSIMULACRA2 видит только WebP → AVIF. Она ничего не знает об оригинале, существовавшем до WebP. Артефакты, которые уже есть в WebP, становятся частью reference.
Поэтому score 60 может сказать мне, что AVIF не слишком далеко ушёл от WebP. Но он не говорит, насколько финальный AVIF далёк от потерянного master.
Lossy-transcoding создаёт второй бюджет качества
Представим, что в оригинале был чистый градиент. Первый encoder добавил немного banding, но WebP всё ещё выглядит приемлемо. После этого я кодирую этот WebP в AVIF. SSIMULACRA2 может наказать дополнительную деградацию, которую добавил AVIF encoder, но не может наказать повреждения, уже присутствующие в reference.
Именно поэтому encode из уже lossy-изображения не равен encode напрямую из лучшего доступного source. В обсуждении проекта](https://github.com/AOMediaCodec/libavif/discussions/2640%22>проекта) libavif отмечается тот же общий принцип: уже существующие compression artifacts могут перейти в новый AVIF, если вход сам ранее был сжат.
Это не означает, что AVIF автоматически усиливает каждый артефакт WebP, и не означает, что transcoding всегда запрещён. Это означает, что второй encoder начинает работу после того, как часть исходного quality budget уже потрачена.
Политика, на которой я остановился
- Canonical или качественный source: target 60, floor для одного sample 58.
- Lossless WebP: target 60, floor 58.
- Заведомо lossy-производный файл: target 65, floor 63.
Главное различие здесь не JPEG против WebP, а canonical source против заведомо lossy-производного файла.
WebP может быть lossless. Спецификация](https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification%22>Спецификация) WebP lossless описывает режим, который точно восстанавливает значения пикселей, поэтому предыдущего lossy-поколения там нет. И наоборот, JPEG, про который я знаю, что он уже прошёл несколько lossy-преобразований, заслуживает той же осторожности, что и ранее сжатый WebP.
Почему 65?
В SSIMULACRA2 нет правила, согласно которому второе lossy-поколение требует ровно плюс пять пунктов. Я не нашёл такого правила, потому что его не существует. 65 — инженерная policy, а не свойство метрики.
Опубликованные quality anchors помогают понять диапазон. Грубо говоря, 50 соответствует medium/fair quality, а 70 — high/good quality. Поэтому 60 находится в довольно агрессивной зоне web-compression, а не в visually lossless диапазоне.
При прямой конвертации из хорошего source я готов потратить такой perceptual budget ради меньшего размера файлов. Для второго lossy-поколения мне хотелось оставить меньший бюджет на дополнительное искажение.
Я рассматривал 70, но тогда все перекодированные изображения пришлось бы загнать в заметно более строгий диапазон качества. На страницах, где грузится много изображений, особенно через мобильное соединение, дополнительные байты имеют значение. У меня не было доказательств, что обязательные 70 для всех уже сжатых изображений оправдывают цену. Поэтому я выбрал 65 как консервативный компромисс.
Почему 65/63, а не 65/62?
Изначальная policy была 60/58: один репрезентативный outlier мог быть на два пункта ниже основного target. Если поднять target до 65 и сохранить ту же логику, естественно получается 65/63.
60 - 58 = 2
65 - 63 = 2
62 создаёт исключение уже в три пункта. Обычные samples становятся строже, но худшему sample одновременно даётся больше свободы. Я не нашёл технической причины расширять исключение именно для файлов, которые уже прошли lossy-сжатие.
Ни 63, ни 65 не являются магическими числами. Полезное свойство здесь — внутренняя последовательность policy.
Переход с 2 MB до 100 KB ничего надёжного не говорит о визуальном качестве
Я сознательно не вывожу SSIMULACRA2 thresholds из коэффициента сжатия. Переход с 2 MB до 100 KB выглядит радикально, но размер файла сам по себе удивительно мало говорит о perceptual degradation.
Разрешение, image entropy, шум, однотонные области, line art, chroma subsampling и предыдущий формат сильно влияют на эффективность сжатия. Исследование](https://developers.google.com/speed/webp/docs/webp_study%22>Исследование) Google по WebP сравнивает кодеки при примерно сопоставимом качестве, а не предполагает, что одинаковый размер означает одинаковое визуальное качество.
Поэтому у меня нет правил вроде 20× меньше → target 65. Для моей policy важно, является ли текущий файл известным lossy-derivative, а не насколько впечатляюще выглядит коэффициент уменьшения.
Если оригинал сохранился, я не перекодирую WebP
Если у меня есть и качественный оригинал, и маленький lossy WebP, я кодирую AVIF напрямую из оригинала и использую обычную policy 60/58.
предпочтительно:
original → AVIF
по возможности избегать:
original → lossy WebP → AVIF
Более строгий target второго поколения не восстановит информацию, потерянную при первом encode. 65 только заставляет AVIF оставаться ближе к WebP; 70 оставит его ещё ближе. Ни то ни другое не восстановит потерянный оригинал.
Ошибка реализации оказалась важнее спора 62 против 63
Во время проверки policy я нашёл более опасную проблему в логике encoder. В коде уже были отдельные targets по форматам и helper, способный вернуть другой target для WebP. Поэтому изменение WebP с 60 на 65 выглядело тривиальным.
Но это было не так. Adaptive quality decision продолжал использовать глобальный target и глобальный worst-score threshold. Форматный target позже применялся для маркировки отдельных результатов как pass или below-target, но не обязательно управлял решением, которое выбирало финальный AVIF quality.
Из-за этого возможен скрытый failure: WebP sample корректно помечается как ниже своего target 65, но adaptive search всё равно принимает этот quality, потому что глобальное условие pass остаётся 60.
intended WebP target: 65
actual score: 61.2
format-aware label: below target
global search rule: pass if target is still 60
Threshold бессмыслен, если он не участвует в решении, которое реально выбирает encoded output.
Безопаснее сделать thresholds частью policy каждого sample
Сейчас я предпочитаю считать thresholds свойствами source, а не декоративными константами формата. В упрощённом pseudocode:
if sample is a known lossy derivative:
target = 65
floor = 63
else:
target = 60
floor = 58
reject if any sample is below its floor
allow at most one sample below its target
Главное, чтобы те же thresholds, которыми описывается результат, управляли и решением о его принятии.
Sampling policy тоже имеет значение
Мне не нужно проверять каждое изображение на каждом возможном AVIF quality. Pipeline выбирает до десяти репрезентативных JPEG, PNG или WebP samples по распределению bytes-per-pixel.
В коллекциях из десяти изображений или меньше каждый sample должен пройти обычный target. В более крупных коллекциях один sample может использовать нижний floor, а все остальные обязаны пройти основной target.
Sampling делает поиск практичным, но это ещё одна причина не делать outlier rule слишком мягким. Выбранные samples репрезентативны, но они не доказывают, что каждое несэмплированное изображение поведёт себя так же.
Эксперимент, который может заменить heuristic
Самый сильный ответ можно получить, если сохранить настоящие оригиналы репрезентативного корпуса и проверить полные цепочки:
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
Для каждого варианта я бы записывал финальный размер, SSIMULACRA2 относительно настоящего оригинала, SSIMULACRA2 относительно промежуточного WebP и выбранный encoder quality. Сложные изображения я бы также проверял вручную.
Я не проводил такой контролируемый эксперимент на достаточно репрезентативном наборе сохранённых оригиналов, поэтому не могу утверждать, что 65 глобально оптимален. Это важное ограничение.
AVIF не автоматически оправдывает ещё один encode
Если единственный оставшийся source — WebP размером 100 KB, а AVIF, проходящий 65/63, весит 96 KB, я бы поставил конвертацию под сомнение. Экономия 4 KB может не оправдывать ещё одно lossy-поколение и дополнительную сложность обработки.
Если тот же WebP размером 100 KB превращается в AVIF размером 65 KB и при этом проходит quality policy, trade-off становится гораздо интереснее для страниц с большим количеством изображений.
Codec conversion должна отвечать на два отдельных вопроса: допустимо ли дополнительное искажение и достаточно ли сильно уменьшается размер? Успех по первому вопросу не гарантирует успех по второму.
Правило, которое я использую сейчас
Если у меня есть оригинал максимального качества, я кодирую напрямую из него и для такого web workload использую 60/58. Если WebP lossless, тоже использую 60/58. Если единственный оставшийся файл — заведомо lossy derivative, применяю более строгий бюджет второго поколения, сейчас 65/63. Если AVIF почти не уменьшает размер, рассматриваю вариант оставить существующий WebP.
Главный вывод не в том, что WebP нужен особый номер SSIMULACRA2. Full-reference quality metric отвечает только на вопрос, который задаёт её reference image.
Если reference уже потеряла информацию, высокий score означает «близко к этой reference», а не «близко к изображению, которое существовало до неё». Когда я начал учитывать происхождение изображения как часть compression policy, thresholds перестали выглядеть как произвольные настройки кодека. Они превратились в бюджеты для разных поколений потерь.