Sorry Leute, heute wird es technisch. Also ignoriert das hier einfach, wenn's nicht euer Ding ist.
Die Geschichte geht so: Vor einer Weile hab ich dem Blog ja äußerlich eine neue Gestalt und unter der Haube ein technisch etwas zeitgemäßeres Theme spendiert. Davor war es ganz einfach mit den Bildern: In voller Auflösung hochgeladen, hat Imsanity das Zeug dann von selbst auf die damalige Breite von 604 Pixeln skaliert. Mit der WordPress-Voreinstellung für die JPEG-Kompression (am Anfang "-quality 90", dann in neueren Versionen auf "-quality 82" reduziert), sah das üblicherweise auch halbwegs ansehnlich aus, die Dateigrößen hielten sich mit durchschnittlich weniger als 100 kB im Rahmen.
Mit dem moderneren Theme hielten dann nicht nur etwas großzügigere Abmessungen sondern natürlich auch ein gewisses Maß an Responsiveness Einzug. Den Bildern lediglich ein paar zusätzliche Pixel spendieren und "One size fits all" für jedes Display reicht da nicht mehr. Also lieber mal die neuen Bilder mit aktuell 960 Pixeln etwas großzügiger dimensionieren als nötig, vielleicht zahlt sich das ja bei der nächsten großen Umstellung noch mal aus; außerdem sieht's dann mit etwas Glück auch auf hochauflösenden Displays etwas weniger Scheiße aus (wobei manche Retina-Displays wohlbemerkt ein vielfaches davon vertragen könnten).
Jetzt muss ich also mit ziemlich genau zweieinhalb mal so vielen Pixeln pro Bild hantieren. Das macht sich selbstverständlich in der Dateigröße bemerkbar und so kommt der Wunsch auf, das Bytes/Pixel-Verhältnis etwas zu optimieren. Schließlich will ich wenn's geht noch ein paar Jahre so weiterbloggen, ohne meinem (noch) vergleichsweise günstigen Hoster mehr Geld abdrücken oder eine neue Heimat suchen zu müssen. (Auch wenn mich die Limitierungen eines 08/15 shared Webspace langsam nerven und ich zumindest mit 'nem ordentlichen VServer liebäugle, bin ich doch gerade nicht wirklich in der Stimmung für einen Serverumzug.)
Außerdem mag Google flott ladende Seiten und effizient komprimierte Bilder sind da ein großer Faktor (und denke ich dann an SEO und die ganze damit einhergehende Bauernfängerei, kommt mir gleich das Kotzen… aber das ist nochmal ein ganz anderes Thema). Zu guter letzt muss ich dann noch ordentlich Speicherplatz freihalten, etwa zum zwischenspeichern von Backups, die dann automatisch per FTP auf einen Raspberry Pi bei mir Zuhause geschoben werden. Alles gute Gründe, ein wenig sparsam mit dem Datenaufkommen zu sein.
Meine Zielsetzung: Akzebtables Bildmaterial mit einer durchschnittlichen Dateigröße von grob 100 kB pro Bild.
Man kann's ja mal versuchen: Online-Bildoptimierer
Diese Dienste werden derzeit an allen Ecken und Enden angepriesen mit teilweise recht magisch anmutenden Verprechungen. Die meisten davon sind Freemium Angebote, mit einem in Funktion oder Durchsatz stark eingeschränkten kostenlosen Tier und kostenpflichtigen Angeboten, deren monatliche Gebühren für kleine Blogger wie mich in keinem Verhältnis zum Nutzen stehen. Da muss man schon Pitchfork sein, damit sich das lohnt.
Cheetao ist einer der wenigen Anbieter, die auch einen bezahlbaren Volumentarif anbieten (einmal fünf Dollar rausrücken für 3000 Bilder, das würde bei mir für 3-4 Jahre ausreichen) und deshalb hab ich der Bude mal 'ne Chance gegeben, nebenbei aber auch ein paar kostenlose Krümel anderer Anbieter aufgepickt. Und tatsächlich waren die meisten davon in der Lage mit gutem Ausgangsmaterial dem Endergebnis ein paar Kilobytes abzuluchsen. Bei subjektiv gleicher Bildqualität, verglichen mit den üblichen, auf der sehr alten libjpeg basierenden Hausmitteln.
Nebenher hab ich noch ein paar andere Sachen versucht und bin letztendlich zum Schluss gekommen, dass die Online-Bildschrumpfer auch nur mit Wasser kochen und ihre ach so bahnbrechenden Verfahren gar nicht so geil sind. Mit etwas Gaffatape und ein paar Infos ist es kein Problem, auf dem heimischen Rechner und ohne wahnsinnigen Aufwand bessere Ergebnisse zu erzielen. (Hier sei angemerkt, dass auf meinem Alltags-Laptop Ubuntu läuft. Die angesprochenen Tools sollte man auch auf nicht-*nix Systemen ans Kacken kriegen, das könnte potenziell aber mit mehr Frickelei verbunden sein.)
Stufe eins: Mozilla's MozJPEG und eine sehr langsame Alternative
Zwei Projekte haben in den letzten Jahren kleinere Wellen geschlagen mit dem Versuch, einen zeitgemäßeren JPEG-Encoder zu bauen. Immerhin ist die fast überall eingebundene libjpeg und deren Performance-optimierter Fork libjpeg-turbo schon ziemlich in die Jahre gekommen.
Der jüngere Versuch kommt aus dem Hause Google und nennt sich Guetzli. Der überwiegende Teil aller JPEG-Encoder ist auf die SSIM-Metrik optimiert, eines von vielen Wahrnehmungspsychologischen Modellen mit dem Ziel, sich mit mathematischen Mitteln der menschlichen Wahrnehmung anzunähern und somit einen Wert zu ermitteln, welcher halbwegs mit der von Menschen empfundenen, subjektiven Qualität eines Bildes korreliert. SSIM vergleicht immer zwei Bilder miteinander, im Fall von JPEG die Quell- mit der Zieldatei. Der SSIM-Wert besagt, wie groß der unterschied zwischen beiden ist. Keine dieser Metriken ist perfekt, es wird auch noch fleißig dran geforscht und entwickelt.
Google hat eine eigene Metrik entwickelt. Sie hört auf den Namen Butteraugli und behauptet von sich, neuere, konkretere wissenschaftliche Erkenntnisse über das menschliche Sehvermögen und -empfinden abzubilden. Guetzli ist noch in einem frühen Entwicklungsstadium und in seiner Funktion etwas eingeschränkt. Er lässt derzeit nur relativ hohe Qualitätsstufen zu und kann in diesem Bereich auch durchaus beachtliche Ergebnisse erzielen. Ich bin als Blogger allerdings nicht an höchster Qualität interessiert, sondern an einem guten Kompromiss, bei dem die Bildqualität nicht perfekt sein muss. Im direkten Vergleich mit dem Quellmaterial dürfen ruhig Unterschiede feststellbar sein, aber für sich genommen sollen die Bilder nicht negativ auffallen. Das kann Guetzli einfach nicht und fliegt somit aus meiner Auswahl.
Außerdem ist Guetzli unglaublich speicherhungrig (größere Bilder fressen auch mehr RAM) und arbeitet dabei im Schneckentempo; je nach Größe kann die Kodierung eines Bildes einige Minuten dauern. Ein vielversprechender proof of concept ist das, aber in der Praxis derzeit nicht zu gebrauchen.
Vergleichsweise gereift ist dagegen schon das Mozilla-Projekt MozJPEG, dessen erste Version Anfang 2014 erschien. Es handelt sich um einen Fork der bereits angesprochenen libjpeg-turbo. Einerseits macht sich der Encoder einige schon länger bekannte, zum Teil verlustfreie Methoden zur Optimierung zunutze, die in der normalen libjpeg entweder nicht implementiert oder per default deaktiviert sind. Außerdem kommt hier zum ersten mal bei einer JPEG-library die sogenannte Trellis-Quantisierung zum Einsatz, die man auch in modernen Videocodecs wiederfindet.
Das Kommandozeilen-Tool steht im Quellcode zur Verfügung, es gibt auch inoffizielle Binaries für Linux und Windows. Ob letztere mit den gängigen Distros auch problemlos funzen habe ich bisher nicht ausprobiert.
MozJPEG ist weitgehend kompatibel mit der libjpeg-turbo, kann aber außer JPEG komprimieren nicht viel. Also muss Imagemagick für Skalierung und Pre-Processing herhalten. Das ahnungslos zusammengeklaute Basis-Skript sieht bei mir so aus:
for img in `ls *.jpg`
do
convert $img -resize 960 -filter lanczos -define filter:blur=1.2 pnm:- | /opt/mozjpeg/bin/cjpeg -quality 72 -progressive -smooth 10 -optimize > M$img
done
Die Qualitätsstufen sind nicht eins zu eins von der normalen libjpeg übertragbar, daher muss etwas experimentiert werden, bis ein guter Kompromiss von Qualität und Kompression gefunden ist.
Mit dem Zusatz -define filter:blur=1.2
bringe ich dem an sich sehr crispen Lanczos-Skalierer ein wenig Unschärfe bei und lasse den JPEG-Encoder mit -smooth 10
nochmal etwas weichzeichnen. So tausche ich ein wenig Bildschärfe für eine bessere Kompression ein, außerdem reduzieren sich so im Ergebnis sehr deutlich die Ringing-Artefakte, die besonders an harten Kanten auftreten.
Damit lassen sich bei brauchbarer Qualität schon einige Prozent an Daten einsparen. In etwa auf einer Höhe mit den erwähnten Online-Optimierern, die vermutlich auch gar nichts anderes machen.
Auf unnötige Bildgrößen verzichten
Um responsives Verhalten zu unterstützen, legt WordPress automatisch Kopien aller Bilder in unterschiedlichen Auflösungen an. Mit großem Erstaunen stellte ich fest, dass das neue Theme sage und schreibe sechs neue Bildgrößen in unterschiedlichen Seitenverhältnissen definiert. Keine einzige dieser Kopien braucht man für ein reguläres Blog, wenn man sich in der Gestaltung der Posts auf die WordPress-Bordmittel beschränkt. Was man sowieso tun sollte, sonst macht man sich von Zusatzfunktionen des Themes abhängig und beim nächsten Relaunch hat man ein Problem. Also weg mit dem Ballast.
Wegen der höheren Auflösung werden jetzt auch von WordPress vorgegebene Kopien mit einer Breite von 768 Pixeln angelegt. Weil WordPress ja nur auf die von meinem Hoster verfügbare Version der libjpeg zugreifen kann, sind diese Bilder, wenn überhaupt, nur sehr geringfügig schlanker als das optimierte Original mit 960px. Daher schlucken sie nur unnötig Speicherplatz und helfen kein bisschen bei Ladezeiten und Responsive-Klimbim. Also auch weg damit. Das lässt sich mit einem Plugin bewerkstelligen oder von Hand, üblicherweise findet sich der Code dazu in der functions.php. Übrig bleiben dann die Größen "Medium" mit 300px Breite und "Thumbnail" mit 150px. Beide ergeben in meinen Augen Sinn und fallen Speichermäßig kaum ins Gewicht.
So, das war der Stand in den letzten Wochen und so richtig zufrieden war ich damit nicht. Geht es noch besser? Aber sicher doch.
Stufe zwei: Konstante Qualität mit jpeg-recompress
MozJPEG hat zwar einen deutlichen Kompressionsgewinn, schleppt aber auch eine große Schwäche der alten libjpeg mit sich herum: Bei einer gegebenen Kompressionsstufe schwankt die erzielte Qualität erheblich. Manche Bilder könnte man viel härter anpacken ohne dass es groß auffält, andere bräuchten einfach ein paar kB mehr um zu überzeugen.
Erinnert ihr euch, was ich über SSIM geschrieben hab? Die Mozilla-Entwickler benutzten auch SSIM um die Effizienz von MozJPEG zu tunen, aber die eingestellte Qualitätsstufe verändert nur diverse Parameter im Encoder, bestimmt damit letztendlich nur die Stärke der Kompression. Und das Maß an Kompression ist nicht gleichzusetzen mit subjektiver Qualität.
Deshalb musste ich in den vergangenen Wochen jedes Bild erst mal kontrollieren und bei Bedarf eine höhere Qualitätsstufe anwenden.
Hier setzt ein geniales Tool namens jpeg-recompress an. Ursprunglich für den Einsatz mit libjpeg-turbo gedacht, steht inzwischen auch eine Version zur Verfügung, die mit MozJPEG zusammenarbeitet.
Jpeg-recompress hat es sich zum Ziel gesetzt, nicht Bilder von konstanter Kompression, sondern von konstanter Qualität zu liefern. Deshalb will jpeg-reencode auch nichts von Kompressionsstufen wissen, stattdessen wählt man ein Preset oder - in meinen Augen sinnvoller - einen Ziel-Wert in SSIM oder einer anderen vom Tool unterstützten Metrik.
Also nehme ich mir als erstes mal einige mit MozJPEG erzeugte Bilder vor und mache mich schlau, was für SSIM-Werte die denn erreichen. Dazu ist das mitgelieferte Tool jpeg-compare hilfreich. Damit dieser Test sinnvolle Ergebnisse liefert, muss das Quellmaterial bereits auf die Zielauflösung herunterskaliert sein. Jetzt kann ich daraus meine Schlüsse ziehen und mich langsam an den Wert herantasten, der für mich durchweg zufriedenstellende Ergebnisse liefert.
Um diesen zu erreichen ruft jpeg-recompress wiederholt MozJPEG auf und komprimiert das Bild im ersten Durchgang mit der Qualitätsstufe 80. Dann checkt er den SSIM-Wert und verwirft das Bild. Ist der Wert höher als gewünscht (höher ist besser), wird im nächsten Durchgang die Kompression erhöht. Ist er niedriger als gewünscht, wird die kompression verringert. Das macht er per default acht mal und nähert sich so dem gewünschten Wert an. Erst im letzten Durchgang wird das Bild gespeichert, das jetzt sehr nah an der gewünschten Qualität ist.
In der Shell sieht das so aus:
Metadata size is 0kb
ms-ssim at q=80 (60 - 100): 0.967018
ms-ssim at q=69 (60 - 79): 0.955776
ms-ssim at q=74 (70 - 79): 0.963294
ms-ssim at q=71 (70 - 73): 0.954818
ms-ssim at q=72 (72 - 73): 0.954625
ms-ssim at q=73 (73 - 73): 0.956991
ms-ssim at q=74 (74 - 73): 0.963294
ms-ssim at q=74 (74 - 73): 0.963294
ms-ssim at q=74 (74 - 73): 0.963294
Final optimized ms-ssim at q=74: 0.963497
New size is 4% of original (saved 2588 kb)
Und das Skript dazu sieht so aus:
for img in `ls *.jpg`
do
convert $img -resize 960 -filter lanczos ppm:- | jpeg-recompress --ppm --method ms-ssim --accurate --target 0.96000 --min 60 --max 100 --loops 10 - J$img
done
Das alles funktioniert ganz hervorragend. Viele Bilder werden jetzt stärker komprimiert und sehen trotzdem einwandfrei aus. Andere werden deutlich größer als bisher und ein kleiner Check mit dem nackten MozJPEG bestätigt, dass sie die zusätzlichen Bits auch brauchen. Große Überraschungen kommen nicht mehr vor. Insgesamt ist die Ersparnis so groß, dass ich komplett auf die Weichzeichner verzichten kann. Mit einer durchschnittlich immer noch etwas geringeren Dateigröße.
Das komprimieren eines Bildes dauert mit den erwähnten Einstellungen ein paar Sekunden, das finde ich vertretbar. Aber mann kann den Vorgang auch stark beschleunigen, wenn man anstatt der rechenintensiven MS-SSIM Metrik die schneller zu errechnenden, normalen SSIM-Werte benutzt. Auch der Verzicht auf den --accurate
Switch hat keinen großen Einfluss auf das Ergebnis und beschleunigt den Vorgang sehr.
Den --min
Switch darf man nicht zu niedrig ansetzen: Mit einem zu niedrigen Wert wird es bei einigen Bildern hässlich. Besser bei der sehr sinnvollen Voreinstellung von 60 bleiben. Nach oben hin setze ich keine Grenze, hier gilt: --max 100
.
Also, das Fazit fällt gut aus. Ordentliche Quali? Check! Durchschnittlich knapp unter 100 kB? Yupp.
So lässt es sich wieder arbeiten.
Update 11.8.2017
Smallfry vs. MS-SSIM
Ich hab in der Zwischenzeit auch mal der ebenfalls in Jpeg-Recompress implementierten Smallfry-Metrik eine Chance gegeben, weil einige User gute Erfahrungen damit berichtet hatten. Ich kann die Begeisterung nicht wirklich teilen. Vielleicht performt die schön flott zu errechnende Metrik besser für reine Fotos oder bei geringerer Kompression. Ich hatte aber wenig Glück mit dem doch sehr unterschiedlichen Bildmaterial für dieses Blog.
Einmal den passenden Zielwert gefunden, der bei einer zufälligen Auswahl von 100 Bildern die gleiche durchschnittliche Dateigröße erreicht wie die bisherigen Einstellungen (Metrik: MS-SSIM, Zielwert: 0,95), zeigen sich dann doch recht viele Qualitäts-Ausreißer im Vergleich mit MS-SSIM. Besonders bei detaillierten Objekten vor einem "sauberen", uniformen Hintergrund und besonders bei farbigem Bildmaterial scheint die Gewichtung nicht so recht mit dem subjektiven Eindruck zu korrelieren; Smallfry scheint sich dabei an für's menschliche Auge sehr auffälliger Blockbildung nicht zu stören. Von den verfügbaren Metriken erzielt MS-SSIM hier immer noch die zuverlässigsten Ergebnisse.
Es sei erwähnt, dass derzeit einige interessante Entwicklungen stattfinden. Insbesondere bei neuen Metriken, die speziell darauf optimiert sind, Artefakte blockbasierter Kompression (Blockbildung, Ringing, Mosquito-Noise) zu erkennen und zu bewerten. Unter anderem erscheint mir SSIMulacra, sehr vielversprechend. Da es leider (noch?) keine gebrauchsfertige Software-Implementierung gibt, konnte ich den Krempel bisher nicht ausprobieren.
Farbe und Graustufen
Ich hab mich von Anfang an gewundert, dass JPEG-Recompress keinen greyscale-Switch hat. Den benötigt MozJPEG laut Dokumentation, weil der Encoder von sich aus (noch?) kein monochromes Material erkennt.
Bei genauerer Inspektion der Ergebnisse zeigt sich aber, dass eins von beiden Tools definitiv mit Graustufen umgehen kann. Entweder erkennt JPEG-Recompress den nicht vorhandenen Farbkanal und setzt dann den --greyscale
Switch, oder MozJPEG geht sehr wohl von selbst in den Graustufenmodus, wenn es "echtes" monochromes Material gefüttert bekommt.
Voraussetzung ist, dass im Quellmaterial auch wirklich kein Farbkanal vorhanden ist. In meiner Imagemagick-Pipe sorgt der Switch -colorspace gray
für die interne Farbraumkonvertierung und -type grayscale
stellt dann sicher, dass auch der Output keinen Farbkanal verpasst bekommt.
Es stellt sich heraus, dass sich die erzielten MS-SSIM-Werte und damit die Kompressionsstufen und Dateigrößen doch erheblich unterscheiden können zwischen einem Graustufen-kodierten Jpeg und einem aus der gleichen monochromen Quelle stammenden, in Farbe kodierten Bild. Das finde ich besonders deshalb interessant, weil nach meinem Wissen MS-SSIM sowieso nur mit monochromem Material arbeitet, Farbinformationen vor der Analyse also entweder verworfen oder konvertiert werden. Mögliche Erklärungen wären das Chroma Subsampling des JPEG-Formates oder Unterschiede im Verhalten von MozJPEG - vielleicht in der Trellis-Quantisierung - die von der MS-SSIM Metrik sehr unterschiedlich bewertet werden.
Außerdem stellt sich heraus, dass monochrom kodierte Bilder etwas härter angepackt werden können. Auch hier kann ich nur spekulieren warum. Es erscheint mir aber plausibel, dass die typischen JPEG-Artefakte im normalerweise eh deutlich stärker komprimierten Farbkanal für die menschliche Wahrnehmung auffälliger sind.
Wie dem auch sei, ich hab jetzt also zwei getrennte Skripte; eins für farbiges und eins für monochromes Bildmaterial. Neben den erwähnten Imagemagick-Tweaks kann die minimale Kompressionsstufe von monochromen Bildern ruhig von --min 60
auf --min 50
verringert werden, ohne dass sich das bisher in negativen Ausreißern bemerkbar gemacht hätte.