Wróć do bloga
20 sierpnia 2026Sergei Solod8 min czytania

SSIMULACRA2 przy transkodowaniu WebP do AVIF: dlaczego używam 60 dla źródeł i 65 dla stratnych pochodnych

Gdy AVIF powstaje z WebP, który już wcześniej skompresowano stratnie, SSIMULACRA2 mierzy tylko drugą generację strat. Dlatego dla czystych źródeł stosuję 60/58, a dla znanych stratnych pochodnych 65/63.

AVIFWebPSSIMULACRA2Kompresja obrazówWydajność WWW

Moja zasada kompresji początkowo była bardzo prosta: kodować AVIF z najniższą jakością, która nadal przechodzi target SSIMULACRA2 równy 60. W większych zestawach obrazów pozwalałem jednemu reprezentatywnemu sample spaść do 58, podczas gdy wszystkie pozostałe musiały osiągnąć co najmniej 60.

Dla PNG i zwykłych plików źródłowych ta zasada mi odpowiadała. Później zacząłem dostawać pliki WebP, które jeszcze przed trafieniem do mnie zostały stratnie skompresowane z oryginałów wyższej jakości.

oryginał wyższej jakości: ~2 MB
          ↓
      stratny WebP: ~100 KB
          ↓
          AVIF

Czy druga konwersja nadal powinna móc przejść z wynikiem 60 względem WebP? Początkowo uważałem, że tak. 60 to przecież nadal 60. Problem polegał jednak na tym, że zmienił się obraz referencyjny.

Metryka nie była błędna. Zmieniła się referencja.

SSIMULACRA2 porównuje obraz referencyjny z obrazem zniekształconym i ocenia percepcyjną różnicę pomiędzy dokładnie tymi dwoma wejściami. Metryka reaguje między innymi na rozmycie, ringing i sztucznie wprowadzone krawędzie, a opublikowane materiały ewaluacyjne obejmują zniekształcenia JPEG, WebP, AVIF i innych kodeków. Dokumentacja](https://github.com/cloudinary/ssimulacra2/blob/main/README.md%22>Dokumentacja) SSIMULACRA2 opisuje samą metrykę oraz przybliżone punkty odniesienia jakości.

Jeśli koduję AVIF bezpośrednio z dobrego źródła, porównanie jest w praktyce source → AVIF. Wynik 60 opisuje wtedy uszkodzenia wprowadzone przez tę konwersję.

Przy już stratnym WebP rzeczywista historia wygląda inaczej:

original
   ↓ first lossy encode
WebP
   ↓ second lossy encode
AVIF

SSIMULACRA2 widzi jednak wyłącznie WebP → AVIF. Nie wie nic o oryginale istniejącym przed WebP. Artefakty już obecne w WebP stają się częścią referencji.

Wynik 60 może więc powiedzieć, że AVIF nie oddalił się zbytnio od WebP. Nie mówi, jak daleko finalny AVIF znajduje się od utraconego mastera.

Stratne transkodowanie tworzy drugi budżet jakości

Załóżmy, że oryginał zawiera gładki gradient. Pierwszy encoder dodaje niewielki banding, ale WebP nadal wygląda akceptowalnie. Następnie koduję ten WebP do AVIF. SSIMULACRA2 może ukarać dodatkowe pogorszenie dodane przez encoder AVIF, ale nie może ukarać uszkodzeń, które już są częścią referencji.

Dlatego kodowanie z obrazu już wcześniej skompresowanego stratnie nie jest tym samym co bezpośrednie kodowanie z najlepszego dostępnego źródła. Dyskusja w projekcie](https://github.com/AOMediaCodec/libavif/discussions/2640%22>projekcie) libavif opisuje ten sam ogólny mechanizm: istniejące artefakty kompresji mogą zostać przeniesione do nowego AVIF, jeśli wejście jest już skompresowane.

Nie oznacza to, że AVIF automatycznie wzmacnia każdy artefakt WebP ani że transkodowania zawsze należy unikać. Oznacza jedynie, że drugi encoder zaczyna pracę po zużyciu części pierwotnego budżetu jakości.

Polityka, na której ostatecznie się zatrzymałem

  • Źródło kanoniczne lub wysokiej jakości: target 60, floor 58 dla jednego sample.
  • Lossless WebP: target 60, floor 58.
  • Znana stratna pochodna: target 65, floor 63.

Najważniejsze rozróżnienie nie brzmi JPEG kontra WebP, lecz źródło kanoniczne kontra znana stratna pochodna.

WebP może być lossless. Specyfikacja](https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification%22>Specyfikacja) WebP lossless opisuje tryb, który dokładnie rekonstruuje wartości pikseli, więc nie ma wcześniejszej stratnej generacji. Z drugiej strony JPEG, o którym wiem, że przeszedł już kilka stratnych transformacji, zasługuje na taką samą ostrożność jak wcześniej skompresowany WebP.

Dlaczego 65?

Nie istnieje reguła SSIMULACRA2 mówiąca, że druga stratna generacja wymaga dokładnie pięciu dodatkowych punktów. Nie znalazłem takiej reguły, ponieważ jej nie ma. 65 to polityka inżynierska, a nie właściwość metryki.

Opublikowane punkty odniesienia jakości pomagają umieścić ten wybór w kontekście. W przybliżeniu 50 odpowiada jakości średniej lub fair, a 70 wysokiej lub dobrej. To oznacza, że 60 leży w dość agresywnym obszarze kompresji webowej, a nie w zakresie visually lossless.

Przy bezpośredniej konwersji z dobrego źródła akceptuję taki budżet percepcyjny w zamian za mniejsze pliki. Przy drugiej stratnej generacji chciałem dopuścić mniejsze dodatkowe zniekształcenie.

Rozważałem 70, ale przesunęłoby to wszystkie transkodowane obrazy do znacznie bardziej rygorystycznego zakresu jakości. Na stronach ładujących wiele obrazów, szczególnie przez sieć mobilną, dodatkowe bajty mają znaczenie. Nie miałem dowodów, że wymuszanie 70 dla każdego już skompresowanego obrazu uzasadnia koszt. Dlatego wybrałem 65 jako konserwatywny środek.

Dlaczego 65/63, a nie 65/62?

Moja pierwotna polityka to 60/58: jeden reprezentatywny outlier mógł spaść o dwa punkty poniżej głównego targetu. Po podniesieniu targetu do 65 zachowanie tej samej polityki naturalnie daje 65/63.

60 - 58 = 2
65 - 63 = 2

62 tworzyłoby wyjątek o szerokości trzech punktów. Normalne samples stałyby się bardziej rygorystyczne, podczas gdy najgorszy sample dostałby większy margines. Nie znalazłem technicznego uzasadnienia, aby rozszerzać wyjątek właśnie dla wejść, które już przeszły stratną kompresję.

Ani 63, ani 65 nie są magicznymi liczbami. Wartością jest spójność polityki.

Spadek z 2 MB do 100 KB nie mówi, jaka jest jakość wizualna

Celowo nie wyznaczam progów SSIMULACRA2 na podstawie współczynnika kompresji. Redukcja z 2 MB do 100 KB brzmi spektakularnie, ale sama wielkość pliku mówi zaskakująco mało o degradacji percepcyjnej.

Rozdzielczość, entropia obrazu, szum, płaskie obszary, line art, chroma subsampling i poprzedni format silnie wpływają na skuteczność kompresji. Badanie](https://developers.google.com/speed/webp/docs/webp_study%22>Badanie) kompresji WebP firmy Google porównuje kodeki przy zbliżonej jakości, zamiast zakładać, że taki sam rozmiar oznacza taką samą jakość wizualną.

Nie stosuję więc reguł typu 20× mniejszy → target 65. Dla mojej polityki liczy się to, czy bieżący plik jest znaną stratną pochodną, a nie jak imponujący wydaje się współczynnik redukcji.

Jeśli mam jeszcze oryginał, nie transkoduję WebP

Jeśli istnieją zarówno wysokiej jakości oryginał, jak i mały stratny WebP, koduję AVIF bezpośrednio z oryginału i stosuję normalną politykę 60/58.

preferred:
original → AVIF

avoid when possible:
original → lossy WebP → AVIF

Bardziej rygorystyczny target drugiej generacji nie odzyska informacji utraconej podczas pierwszego encode. 65 jedynie utrzymuje AVIF bliżej WebP; 70 utrzymałoby go jeszcze bliżej. Żaden z tych wyników nie odtworzy utraconego oryginału.

Błąd implementacji był ważniejszy niż 62 kontra 63

Podczas przeglądu polityki znalazłem groźniejszy problem w logice encodera. Kod miał już targets zależne od formatu oraz helper zwracający inny target dla WebP. Zmiana WebP z 60 na 65 wyglądała więc banalnie.

Nie była. Adaptive quality decision nadal używał globalnego targetu i globalnego worst-score threshold. Target zależny od formatu był później używany do oznaczania wyników jako pass lub below-target, ale niekoniecznie sterował decyzją wybierającą finalną quality AVIF.

To tworzy subtelny błąd: sample WebP może zostać poprawnie oznaczony jako poniżej targetu 65, a mimo to adaptive search zaakceptuje quality, ponieważ globalny warunek pass nadal wynosi 60.

intended WebP target: 65
actual score:         61.2

format-aware label:  below target
global search rule:  pass if target is still 60

Próg jest bez znaczenia, jeśli nie bierze udziału w decyzji wybierającej rzeczywisty output.

Bezpieczniej jest uczynić progi częścią polityki sample

Obecnie wolę traktować progi jako właściwości źródła, a nie dekoracyjne stałe formatu. W uproszczonym pseudokodzie:

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

Najważniejsze jest to, aby te same progi, które opisują wynik, sterowały również jego akceptacją.

Polityka samplowania również ma znaczenie

Nie muszę testować każdego obrazu przy każdej możliwej quality AVIF. Pipeline wybiera do dziesięciu reprezentatywnych sampli JPEG, PNG lub WebP z rozkładu bytes-per-pixel.

Dla kolekcji do dziesięciu obrazów każdy sample musi osiągnąć normalny target. W większych kolekcjach jeden sample może skorzystać z niższego floor, podczas gdy wszystkie pozostałe nadal muszą osiągnąć główny target.

Sampling sprawia, że wyszukiwanie jest praktyczne, ale jest też powodem, aby nie luzować nadmiernie reguły outliera. Wybrane samples są reprezentatywne, lecz nie dowodzą, że każdy niesamplowany obraz zachowa się identycznie.

Eksperyment, który mógłby zastąpić heurystykę

Najmocniejszą odpowiedź dałoby zachowanie prawdziwych oryginałów reprezentatywnego korpusu i przetestowanie pełnych łańcuchów:

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

Dla każdego wariantu zapisałbym finalny rozmiar, SSIMULACRA2 względem prawdziwego oryginału, SSIMULACRA2 względem pośredniego WebP oraz quality wybraną przez encoder. Trudne obrazy sprawdziłbym też ręcznie.

Nie wykonałem tego kontrolowanego eksperymentu na wystarczająco reprezentatywnym zbiorze zachowanych oryginałów, więc nie mogę twierdzić, że 65 jest globalnie optymalne. To ważne ograniczenie.

AVIF nie zawsze uzasadnia kolejny encode

Jeśli jedynym pozostałym źródłem jest WebP 100 KB, a AVIF przechodzący 65/63 ma 96 KB, zakwestionowałbym sens konwersji. Oszczędność 4 KB może nie uzasadniać kolejnej stratnej generacji i dodatkowej złożoności przetwarzania.

Jeżeli jednak ten sam WebP 100 KB staje się AVIF 65 KB i nadal spełnia politykę jakości, trade-off jest znacznie atrakcyjniejszy na stronach z dużą liczbą obrazów.

Konwersja kodeka musi odpowiedzieć na dwa osobne pytania: czy dodatkowe zniekształcenie jest akceptowalne i czy redukcja rozmiaru jest wystarczająco duża, aby miała znaczenie? Spełnienie pierwszego warunku nie gwarantuje drugiego.

Reguła, której używam teraz

Jeśli mam oryginał najwyższej jakości, koduję bezpośrednio z niego i dla tego typu obciążenia webowego używam 60/58. Jeśli WebP jest lossless, także stosuję 60/58. Jeśli jedynym pozostałym plikiem jest znana stratna pochodna, używam bardziej rygorystycznego budżetu dla drugiej generacji, obecnie 65/63. Jeśli AVIF prawie nie zmniejsza rozmiaru, rozważam pozostawienie istniejącego WebP.

Głębsza lekcja nie polega na tym, że WebP potrzebuje specjalnej liczby SSIMULACRA2. Full-reference quality metric odpowiada tylko na pytanie reprezentowane przez jego obraz referencyjny.

Jeśli referencja już utraciła informacje, wysoki score oznacza „blisko tej referencji”, a nie „blisko obrazu, który istniał wcześniej”. Gdy zacząłem traktować pochodzenie obrazu jako część polityki kompresji, progi przestały wyglądać jak arbitralne ustawienia kodeka. Stały się budżetami dla różnych generacji strat.