Waarom kleine JPEG's er in Chrome anders uitzien

Dit icoon ziet er op de computer van mijn collega beter uit

Een tijdje geleden merkte ik tijdens een gesprek met een collega dat een logo op diens computer er niet precies hetzelfde uitzag als op die van mij. Op de computer van mijn collega zag het logo er dunner uit en kwam het trouwer overeen met het originele beeld. Het werd weergegeven op 15px; hier is een vergrote versie.

Let op: dit was niet de oorspronkelijke afbeelding. Omdat dit enige tijd geleden gebeurde, heb ik een nieuwe afbeelding gemaakt om het probleem te demonstreren.

Links staat Firefox; rechts staat Chrome.

Als je je ogen een beetje dichtknijpt of een stap terug doet, ziet de versie van Chrome er dikker uit. Dat is wat vreemd, maar het vervangen van de afbeelding door een SVG loste het op. Toch was ik nieuwsgierig: waarom werd het in eerste instantie zo weergegeven?

Na wat onderzoek vond ik een handige optimalisatie die Chrome gebruikt bij het renderen van JPEG's op kleine schalen.

Het verkleinen van afbeeldingen kan inefficiënt zijn

De intuïtieve manier om een kleine afbeelding uit een JPEG te renderen, is door deze volledig in het geheugen te decompresseren en vervolgens te verkleinen. Maar dat is niet altijd efficiënt.

Stel je een JPEG van 2000 × 2000 pixels voor die moet worden weergegeven als 20 × 20 pixels. Eenmaal gedecomprimeerd neemt de afbeelding veel meer geheugen in beslag dan het uiteindelijke resultaat. Een bitmap van de volledige afbeelding gebruikt ongeveer 12 MB, terwijl de uiteindelijke 20 × 20 afbeelding slechts ongeveer 1,2 KB nodig heeft. Het grootste deel van de informatie in de grote versie gaat verloren bij het verkleinen.

Welke informatie gaat er verloren bij het verkleinen?

Een interessant inzicht is dat de informatie die verloren gaat niet willekeurig is. Wanneer een afbeelding sterk wordt verkleind, is de informatie die verdwijnt voornamelijk het detail met een hoge frequentie.

Dit is intuïtief eenvoudig te begrijpen. Denk aan een boom met veel bladeren en ruwe schors: die fijne details veranderen snel van pixel naar pixel, waardoor ze tellen als hoogfrequente informatie. Als je die boom verkleint tot iets heel kleins, zoals 20 × 10, houd je bovenin slechts een groene vlek over voor het gebladerte en onderin een bruine stok voor de stam. De verkleinde versie heeft het fijne detail — de hoogfrequente informatie — weggegooid.

Een deel van die hoogfrequente informatie overleeft tot op zekere hoogte wel, omdat de details met elkaar vermengd raken.

Hoe JPEG beeldata opslaat

Ik houd deze uitleg beperkt in jargon en wiskunde, maar ik noem wel enkele technische termen die goede startpunten zijn als je dieper wilt graven. Ook sla ik een groot deel van de volledige JPEG-transformatie over, omdat dat hier niet nodig is.

Tijdens de JPEG-compressie worden afbeeldingen opgesplitst in blokken van 8 × 8 pixels die worden omgezet naar het frequentiedomein. Deze bewerking wordt een DCT (Discrete Cosine Transform) genoemd.

In een blok van 8 × 8 is de laagste mogelijke frequentie een egale kleur. Strikt genomen is dit geen frequentie omdat er niets verandert; het is de constante component. Aan de andere kant ziet de hoogste frequentie eruit als een schaakbord, waarbij de waarde zo veel mogelijk verandert. Alles daartussenin vertegenwoordigt de rest van het frequentiedomein. Deze worden basisfuncties genoemd.

Het omzetten van een blok van 8 × 8 naar het frequentiedomein is in feite de vraag: hoeveel van elk patroon is aanwezig in dit blok? Die hoeveelheden worden coëfficiënten genoemd.

JPEG-compressie heeft daarna nog enkele stappen om die coëfficiënten efficiënt op te slaan, en dat is waar de lossy compressie plaatsvindt. Maar dat deel is niet belangrijk voor wat we hier bespreken.

Alles samenvoegen: een JPEG renderen op 1/8 schaal

Stel nu dat je een afbeelding met een factor 8 wilt verkleinen. De eerder genoemde blokken van 8 × 8 kunnen nu worden vertegenwoordigd door één enkele pixel in de verkleinde afbeelding. Bij die grootte heeft de afbeelding voornamelijk laagfrequente informatie nodig, omdat — zoals in het voorbeeld van de boom — de hoogfrequente details grotendeels verdwijnen tijdens het schalen.

In plaats van de volledige JPEG te decompresseren, kunnen we dus de coëfficiënten voor de hoogfrequente delen overslaan en alleen die gebruiken die nodig zijn voor de grove versie van de afbeelding. Dit resulteert in een verkleind resultaat zonder dat de oorspronkelijke afbeelding eerst volledig hoeft te worden uitgepakt.

De gedecodeerde afbeelding neemt minder ruimte in beslag en is sneller te decomprimeren, aangezien we een groot deel van de coëfficiënten overslaan.

Dit kan worden uitgebreid naar andere ratio's, zolang dit breuken zijn met een noemer van 8. De technische naam hiervoor is partial IDCT scaling (zie jpegclub.org; als je hierover leest, zul je zien dat deze techniek ook kan worden gebruikt om afbeeldingen te vergroten!).

  • IDCT (Inverse Discrete Cosine Transform): Het terugbrengen van het frequentiedomein naar het beelddomein.

Hoe Chrome dit toepast

Chrome besteedt het decoderen en renderen van afbeeldingen uit aan Skia. Voor JPEG's gebruikt Skia libjpeg-turbo, dat partial IDCT scaling implementeert. Hierdoor kan het alleen de laagfrequente data decoderen wanneer de doelsegrootte klein genoeg is.

Met andere woorden: Chrome/Skia decomprimeert niet altijd de volledige afbeelding om deze daarna te schalen. Het berekent de dichtstbijzijnde breuk met een noemer van 8 en decodeert de afbeelding op die schaal. Vervolgens wordt de afbeelding verder geschaald met een traditioneler downsampling-algoritme totdat de gewenste grootte is bereikt.

Dat is de reden waarom de afbeelding op mijn machine dikker leek. Omdat het zo klein werd gerenderd, werd het op een achtste schaal gedecodeerd via partial IDCT scaling. De enige data uit de frequentierepresentatie die overbleven, waren dus de constante componenten; alle verzachtingen van randen en gradiënten werden niet gebruikt.

De moraal van dit verhaal is dat je JPEG's niet moet gebruiken voor iconen en dergelijke. Het formaat en de bijbehorende optimalisaties zijn ontworpen rondom onze perceptie van foto's.

Het zit immers ook in de naam: Joint Photographic Experts Group.