Komprimieren von Fotos ohne sichtbaren Qualitätsverlust: Yelp-Erfahrung

Ursprünglicher Autor: Stephen Arthur
  • Übersetzung
Yelp speichert über 100 Millionen von Nutzern erstellte Fotos, angefangen beim Abendessen über Frisurenbilder bis hin zu einer unserer neuesten Funktionen, #yelfies . Diese Bilder machen den Großteil des Datenverkehrs für die Benutzer der Anwendung und der Website aus. Das Speichern und Übertragen dieser Bilder ist nicht billig. Im Bestreben, den Menschen den besten Service zu bieten, haben wir hart daran gearbeitet, alle Fotos zu optimieren und eine durchschnittliche Größenreduzierung von 30% zu erzielen. Dies spart Zeit und Verkehr und senkt auch die Wartungskosten für diese Bilder. Oh ja, und wir haben es geschafft, ohne die Qualität der Fotos zu beeinträchtigen!

Ausgangsdaten


Yelp speichert seit 12 Jahren benutzerdefinierte Fotos. Wir speichern verlustfreie Formate (PNG, GIF) als PNG und alle anderen Formate in JPEG. Python und Pillow werden zum Speichern von Dateien verwendet , und Foto-Uploads beginnen mit so etwas wie diesem Snippet:

# do a typical thumbnail, preserving aspect ratio
new_photo = photo.copy()
new_photo.thumbnail(
    (width, height),
    resample=PIL.Image.ANTIALIAS,
)
thumbfile = cStringIO.StringIO()
save_args = {'format': format}
if format == 'JPEG':
    save_args['quality'] = 85
new_photo.save(thumbfile, **save_args)

Danach suchen wir nach Optionen, um die Dateigröße zu optimieren, ohne an Qualität zu verlieren.

Optimierungen


Zunächst müssen Sie entscheiden, ob Sie die Dateien selbst verarbeiten möchten oder ob der CDN-Anbieter unsere Fotos magisch ändern soll . Da wir hochwertigen Inhalten Priorität einräumen, ist es sinnvoll, die Optionen und möglichen Kompromisse zwischen Größe und Qualität selbst zu bewerten. Wir haben begonnen, den aktuellen Stand der Dinge im Hinblick auf die Optimierung der Dateigröße zu untersuchen - welche Änderungen können vorgenommen werden und wie sich die Größe / Qualität bei jeder Änderung ändert. Am Ende der Studie beschlossen wir, in drei Hauptbereichen zu arbeiten. Der Rest des Artikels widmet sich der Geschichte, was wir getan haben und welchen Nutzen wir aus jeder Optimierung gezogen haben.

  1. Kissenwechsel
    • Kennzeichen optimieren
    • Progressives JPEG
  2. Änderungen an der Logik der Fotoanwendung
    • Große PNG-Erkennung
    • Dynamische JPEG-Qualität
  3. JPEG-Encoder ändert sich
    • Mozjpeg (Gitterquantisierung, benutzerdefinierte Quantisierungsmatrix)

Kissenwechsel


Kennzeichen optimieren


Dies ist eine der einfachsten Änderungen, die wir vorgenommen haben: Übertragung der Pillow-Verantwortung für zusätzliche Einsparungen bei der Dateigröße aufgrund der CPU ( optimize=True) -Zeit. Per Definition wirkt sich dies nicht auf die Qualität der Fotos aus.

Bei JPEG bedeutet dieses Flag, dass der Encoder den optimalen Huffman-Code finden soll, indem er beim Scannen jedes Bildes einen zusätzlichen Durchgang ausführt. Anstatt in eine Datei zu schreiben, berechnet jeder erste Durchlauf die Statistik der Vorkommen für jeden Wert. Diese Informationen werden für eine perfekte Codierung benötigt. Der PNG-Standard verwendet zlib, daher weist das Optimierungsflag in diesem Fall den Encoder an, gzip -9stattdessen zu verwenden gzip -6.

Eine solche Änderung war einfach durchzuführen, es stellte sich jedoch heraus, dass dies keine ideale Lösung war und die Dateigröße nur um einige Prozent verringerte.

Progressives JPEG


Beim Speichern von JPEG können Sie verschiedene Typen auswählen:

  • Grundlegende JPEGs, die von oben nach unten geladen werden
  • Progressive JPEGs, die von verschwommen bis gestochen scharf geladen werden. Die progressive Bildoption ist in Pillow ( progressive=True) einfach zu aktivieren . Infolgedessen wird die Qualität subjektiv erhöht (es ist leichter, einen teilweisen Mangel an Bild zu bemerken als seine unvollkommene Schärfe).

Darüber hinaus ist die Methode zum Packen von progressiven Bildern so, dass dies normalerweise zu einer kleineren Dateigröße führt. Wie ausführlicher im Wikipedia-Artikel erklärtIm JPEG-Format wird eine Zickzack-Penetration eines 8 × 8-Pixelblocks für die Entropiecodierung verwendet. Wenn die Werte dieser Pixelblöcke nicht gepackt und in der richtigen Reihenfolge angeordnet sind, gibt es normalerweise Werte ungleich Null und dann Folgen von Nullen, und dieses Muster wird für jeden 8 × 8-Block im Bild wiederholt und abgewechselt. Mit fortschreitender Codierung ändert sich die Verarbeitungsreihenfolge der Pixelblöcke. Die ersten in der Datei sind die großen Werte für jeden Block (was den ersten Abtastungen des progressiven Bildes eine solche charakteristische Blockierung verleiht), und am Ende werden große Bereiche kleiner Werte, einschließlich mehr Nullen, gespeichert, wobei diese Bereiche feine Details liefern. Eine solche Neuverteilung von Daten in der Datei ändert nicht das Bild selbst, sondern erhöht die Anzahl der Nullen in einer Reihe nacheinander (die einfacher zu komprimieren sind).

Vergleich von Baseline JPEG und Progressive JPEG

Ein Beispiel für die Funktionsweise von Baseline-JPEG-Rendering


Ein Beispiel für die Funktionsweise von Progressive-JPEG-Rendering

Änderungen an der Logik der Fotoanwendung


Große PNG-Erkennung


Yelp arbeitet mit zwei Formaten für benutzerdefinierte Inhalte - JPEG und PNG. JPEG eignet sich hervorragend für Fotos, ist jedoch in der Regel für kontrastreiche Designerinhalte (z. B. Logos) nicht geeignet. Im Gegensatz dazu komprimiert PNG das Bild absolut verlustfrei, ideal für Grafiken, aber zu umständlich für Fotos, bei denen kleine Verzerrungen immer noch nicht auffallen. In Fällen, in denen Benutzer Fotos im PNG-Format hochladen, können wir viel Platz sparen, wenn wir solche Dateien erkennen und im JPEG-Format speichern. Eine der Hauptquellen für PNG-Fotos auf Yelp sind Screenshots von Mobilgeräten und Anwendungen, mit denen Fotos bearbeitet, Effekte angewendet und Rahmen hinzugefügt werden.


Links: Typisches kombiniertes PNG mit Logo und Rahmen. Rechts: typisches PNG aus dem Screenshot.

Wir wollten die Anzahl solcher optionalen PNGs reduzieren, aber es war wichtig, diese nicht zu übertreiben, indem wir die Formate ändern oder die Qualität von Logos, Grafiken usw. herabsetzen. Wie können wir feststellen, ob ein Bild ein Foto ist? In Pixeln?

Nach Überprüfung einer experimentellen Stichprobe von 2500 Bildern stellten wir fest, dass die Kombination aus Dateigröße und Anzahl der eindeutigen Pixel es uns ermöglicht, die Fotos genau zu bestimmen. Wir erstellen eine kleine Kopie mit maximaler Auflösung und prüfen, ob die Dateigröße mehr als 300 KiB beträgt. Wenn ja, überprüfen Sie die Bildpixel auf mehr als 2 16 eindeutige Farben. (Yelp konvertiert die heruntergeladenen RGBA-Bilder in RGB. Andernfalls wird dies jedoch weiterhin überprüft.)

Im experimentellen Beispiel zeigen solche manuellen Einstellungen für die Definition von "großen Bildern" 88% aller Dateien, die möglicherweise zur Optimierung geeignet sind, ohne dass die Grafik falsch positiv ist.

Dynamische JPEG-Qualität


Die erste und bekannteste Möglichkeit, die Größe von JPEG-Dateien zu verringern, ist die Verwendung einer Einstellung namens quality. Viele Anwendungen, die im JPEG-Format gespeichert werden können, sind qualityals Zahl definiert .

Qualität ist eine Art Abstraktion. Tatsächlich gibt es für jeden der Farbkanäle eines JPEG-Bildes separate Qualitätsstufen. Qualitätsstufen von 0 bis 100 entsprechen verschiedenen Quantisierungstabellen für Farbkanäle und bestimmen, wie viele Daten verloren gehen (normalerweise bei hohen Frequenzen). Die Signalquantisierung ist einer der Schritte beim JPEG-Codierungsprozess, wenn Informationen verloren gehen.

Die einfachste Möglichkeit, die Dateigröße zu verringern, besteht darin, die Bildqualität zu verringern, indem mehr Rauschen zugelassen wird. Es geht jedoch nicht für jedes Bild die gleiche Informationsmenge bei gleicher Qualitätsstufe verloren.

Wir können die Qualitätseinstellungen dynamisch ändern und für jedes einzelne Bild optimieren, um die perfekte Balance zwischen Qualität und Größe zu erreichen. Hierfür gibt es zwei Möglichkeiten:

  • Bottom-up: Diese Algorithmen generieren benutzerdefinierte Quantisierungstabellen und verarbeiten das Bild auf einer Blockebene von 8 × 8 Pixeln. Sie berechnen gleichzeitig, wie viel theoretische Qualität verloren gegangen ist und wie diese verlorenen Daten das Auftreten von Verzerrungen für das menschliche Auge verbessern oder verringern.
  • Top-down: Diese Algorithmen vergleichen das gesamte Bild mit der Originalversion und stellen fest, wie viele Informationen verloren gegangen sind. Je nach verwendetem Bewertungsalgorithmus können wir Kandidaten mit unterschiedlichen Qualitätseinstellungen auswählen, die dem Mindestbewertungsniveau entsprechen.

Wir haben die Funktionsweise des Bottom-Up-Algorithmus bewertet und sind zu dem Ergebnis gekommen, dass er bei den von uns gewünschten Einstellungen für die höchste Qualität keine ordnungsgemäßen Ergebnisse liefert (obwohl es den Anschein hat, dass er Potenzial im mittleren Qualitätsbereich hat, in dem der Encoder in Bezug auf die Auswahl verworfener Elemente mutiger sein kann Bytes). Viele wissenschaftliche Arbeiten zu dieser Strategie wurden in den frühen 90er Jahren veröffentlicht, als es an Rechenressourcen mangelte. Daher war es schwierig, die von Option B verwendeten ressourcenintensiven Methoden zu verwenden, z. B. die Bewertung der Beziehungen zwischen Blöcken.

Daher wandten wir uns dem zweiten Ansatz zu: der Verwendung eines zweigeteilten Algorithmus zur Erzeugung von Kandidatenbildern mit unterschiedlichen Qualitätsstufen und der Bewertung des Qualitätsverlusts jedes Bildes durch Berechnung seines strukturellen Ähnlichkeitsindex ( SSIM ) mit Pyssim , solange dieser Wert im Bereich der benutzerdefinierten Werte liegt aber eine statische Schwelle. Dies ermöglichte es uns, die durchschnittliche Dateigröße (und die durchschnittliche Qualität) selektiv nur für Bilder zu verringern, die über dem wahrgenommenen Schwellenwert lagen.

In der folgenden Abbildung werden die SSIM-Werte für 2.500 Bilder angezeigt, die mit drei verschiedenen Qualitätseinstellungen neu generiert wurden.

  1. Originalbilder, die mit der aktuellen Methode erstellt wurden, quality = 85werden in Blau angezeigt.
  2. Ein alternativer Ansatz zum Verringern der Dateigröße mit einer Verringerung der Qualitätseinstellung auf quality = 80wird in Rot angezeigt.
  3. Und schließlich ist der Ansatz, für den wir uns entschieden haben, die dynamische Qualität SSIM 80-85, in Orange dargestellt. Hier wird die Qualität aus dem Bereich von 80 bis 85 (einschließlich) ausgewählt, abhängig von der Übereinstimmung oder Überschreitung des Verhältnisses von SSIM: ein vorberechneter statischer Wert, der diesen Übergang irgendwo in der Mitte des Bildbereichs macht. Dies ermöglicht es uns, die durchschnittliche Dateigröße zu reduzieren, ohne die Qualität von schlecht aussehenden Bildern zu beeinträchtigen.



SSIM-Indizes für 2500 Bilder mit drei verschiedenen Strategien zum Ändern der

SSIM- Qualitätseinstellungen ?
Es gibt verschiedene Algorithmen zum Ändern der Bildqualität, die versuchen, das menschliche Sichtsystem zu imitieren. Wir haben viele von ihnen geschätzt und sind der Meinung, dass SSIM, obwohl älter, aufgrund seiner Eigenschaften für eine solche iterative Optimierung am besten geeignet ist:

  1. Empfindlichkeit gegenüber JPEG-Quantisierungsfehlern
  2. Schneller, einfacher Algorithmus
  3. Es kann für native PIL-Objekte berechnet werden, ohne dass Bilder in PNG konvertiert und in CLI-Anwendungen übertragen werden müssen (siehe Nr. 2).

Beispielcode für dynamische Qualität:

import cStringIO
import PIL.Image
from ssim import compute_ssim
def get_ssim_at_quality(photo, quality):
    """Return the ssim for this JPEG image saved at the specified quality"""
    ssim_photo = cStringIO.StringIO()
    # optimize is omitted here as it doesn't affect
    # quality but requires additional memory and cpu
    photo.save(ssim_photo, format="JPEG", quality=quality, progressive=True)
    ssim_photo.seek(0)
    ssim_score = compute_ssim(photo, PIL.Image.open(ssim_photo))
    return ssim_score
def _ssim_iteration_count(lo, hi):
    """Return the depth of the binary search tree for this range"""
    if lo >= hi:
        return 0
    else:
        return int(log(hi - lo, 2)) + 1
def jpeg_dynamic_quality(original_photo):
    """Return an integer representing the quality that this JPEG image should be
    saved at to attain the quality threshold specified for this photo class.
    Args:
        original_photo - a prepared PIL JPEG image (only JPEG is supported)
    """
    ssim_goal = 0.95
    hi = 85
    lo = 80
    # working on a smaller size image doesn't give worse results but is faster
    # changing this value requires updating the calculated thresholds
    photo = original_photo.resize((400, 400))
    if not _should_use_dynamic_quality():
        default_ssim = get_ssim_at_quality(photo, hi)
        return hi, default_ssim
    # 95 is the highest useful value for JPEG. Higher values cause different behavior
    # Used to establish the image's intrinsic ssim without encoder artifacts
    normalized_ssim = get_ssim_at_quality(photo, 95)
    selected_quality = selected_ssim = None
    # loop bisection. ssim function increases monotonically so this will converge
    for i in xrange(_ssim_iteration_count(lo, hi)):
        curr_quality = (lo + hi) // 2
        curr_ssim = get_ssim_at_quality(photo, curr_quality)
        ssim_ratio = curr_ssim / normalized_ssim
        if ssim_ratio >= ssim_goal:
            # continue to check whether a lower quality level also exceeds the goal
            selected_quality = curr_quality
            selected_ssim = curr_ssim
            hi = curr_quality
        else:
            lo = curr_quality
    if selected_quality:
        return selected_quality, selected_ssim
    else:
        default_ssim = get_ssim_at_quality(photo, hi)
        return hi, default_ssim

Es gibt mehrere andere Blog-Artikel über diese Technik, hier ist einer von Colt Macanlis. Und wenn wir veröffentlicht werden würden, Etsy auch veröffentlicht sein! High Five, schnelles Internet!

JPEG-Encoder ändert sich


Mozjpeg


Mozjpeg ist ein Open-Source- Zweig von libjpeg-turbo , der die Laufzeit aus Gründen der Dateigröße einbüßte. Dieser Ansatz ist gut mit der Offline-Pipeline zur Wiederherstellung von Dateien kompatibel. Mit einem Ressourcenverbrauch, der 3-5 mal größer ist als der von libjpeg-turbo, verkleinert dieser Algorithmus die Bilder!

Einer der Unterschiede zwischen Mozjpeg besteht darin, dass eine alternative Quantisierungstabelle verwendet wird. Wie oben erwähnt, ist Qualität eine Abstraktion von Quantisierungstabellen für jeden Farbkanal. Alles deutet darauf hin, dass die Standard-JPEG-Quantisierungstabellen ziemlich einfach zu übertreffen sind. Wie die JPEG-Spezifikationen sagen :

Diese Tabellen dienen nur als Beispiele und sind nicht unbedingt für eine bestimmte Anwendung geeignet.

Sie sollten sich also nicht wundern, dass diese Tabellen in den meisten Encoder-Implementierungen standardmäßig verwendet werden.

Mozjpeg hat sich die Mühe gemacht, alternative Tabellen für uns zu vergleichen und alternative Tabellen zu verwenden, die beim Generieren von Bildern die besten Ergebnisse erzielen.

Mozjpeg + Kissen


Die meisten Linux-Distributionen haben standardmäßig libjpeg installiert. Mozjpeg unter Kissen funktioniert also nicht standardmäßig , aber es ist nicht zu schwierig, in der Konfiguration zu konfigurieren. Verwenden Sie beim Zusammenbauen von Mozjpeg die Flagge --with-jpeg8und stellen Sie sicher, dass sie mit Pillow verknüpft werden kann. Wenn Sie Docker verwenden, können Sie eine solche Docker-Datei erstellen:

FROM ubuntu:xenial
RUN apt-get update \
	&& DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install \
	# build tools
	nasm \
	build-essential \
	autoconf \
	automake \
	libtool \
	pkg-config \
	# python tools
	python \
	python-dev \
	python-pip \
	python-setuptools \
	# cleanup
	&& apt-get clean \
	&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Download and compile mozjpeg
ADD https://github.com/mozilla/mozjpeg/archive/v3.2-pre.tar.gz /mozjpeg-src/v3.2-pre.tar.gz
RUN tar -xzf /mozjpeg-src/v3.2-pre.tar.gz -C /mozjpeg-src/
WORKDIR /mozjpeg-src/mozjpeg-3.2-pre
RUN autoreconf -fiv \
	&& ./configure --with-jpeg8 \
	&& make install prefix=/usr libdir=/usr/lib64
RUN echo "/usr/lib64\n" > /etc/ld.so.conf.d/mozjpeg.conf
RUN ldconfig
# Build Pillow
RUN pip install virtualenv \
	&& virtualenv /virtualenv_run \
	&& /virtualenv_run/bin/pip install --upgrade pip \
	&& /virtualenv_run/bin/pip install --no-binary=:all: Pillow==4.0.0

Das ist alles! Sammeln und verwenden Sie Pillow mit Mozjpeg im normalen Bildverarbeitungsprozess.

Wirkung


Wie wichtig waren uns diese Verbesserungen? Wir haben mit einer zufälligen Stichprobe von 2.500 Yelp-Geschäftsfotos begonnen, diese durch unsere Verarbeitungspipeline geleitet und die Größenänderung gemessen.

  1. Änderungen an den Kisseneinstellungen sparen 4,5%
  2. Identifizierung großer PNGs gespart 6,2%
  3. Dynamische Qualität spart 4,5%
  4. Umstellung auf Mozjpeg-Encoder spart 13,8%

Insgesamt wurde die durchschnittliche Bildgröße um etwa 30% reduziert, was wir für unsere größten und häufigsten Fotoauflösungen verwendeten, um die Website für Benutzer schneller zu machen und Terabyte pro Tag bei der Datenübertragung einzusparen. Wie auf CDN-Ebene festgelegt:


Änderung der durchschnittlichen Dateigröße für eine CDN im Zeitverlauf (zusammen mit anderen Dateien, die keine Bilder sind)

Was wir nicht gemacht haben


In diesem Abschnitt werden einige andere typische Optimierungen beschrieben, die Sie verwenden können, die jedoch aufgrund der Standardeinstellungen unserer Tools oder aufgrund einer bewussten Weigerung, einen solchen Kompromiss einzugehen, nicht für Yelp geeignet waren.

Downsampling


Die Unterabtastung ist ein Schlüsselfaktor, der sowohl die Qualität als auch die Größe von Webbilddateien bestimmt. Eine detailliertere Beschreibung des Downsamplings finden Sie im Internet. Für diesen Artikel reicht es jedoch zu erwähnen, dass bereits ein Downsampling durchgeführt wird 4:1:1(dies sind die Standardeinstellungen von Pillow, sofern Sie keine anderen Einstellungen angeben). Daher ist eine weitere Optimierung unwahrscheinlich.

Verlustbehaftete PNG-Codierung


Zu wissen, was wir mit PNG machen, ist die Option, diese Bilder im gleichen Format zu speichern, aber einen verlustbehafteten Encoder wie pngmini zu verwenden , sinnvoll, aber wir haben uns trotzdem für die JPEG-Komprimierungsoption entschieden. Trotzdem sagt der Autor des Encoders über die Dateikomprimierung von 72-85%, so dass dies eine Alternative mit vernünftigen Ergebnissen ist.

Modernere Formate


Unterstützung für modernere Formate wie WebP oder JPEG2k wurde von uns definitiv in Betracht gezogen. Aber selbst wenn wir dieses hypothetische Projekt umsetzen würden, würde es immer noch einen langen Schwanz von Benutzern geben, die JPEG / PNG-Bilder benötigen, so dass die Bemühungen, sie auf jeden Fall zu optimieren, nicht umsonst waren.

Svg


Wir verwenden SVG an vielen Stellen auf der Website, zum Beispiel für statische Bilder, die unsere Designer für den Styleguide erstellt haben . Obwohl dieses Format und Optimierungswerkzeuge wie svgo die Seitengröße gut reduzieren, sind sie für unsere Aufgabe nicht geeignet.

Vendor Magic


Es gibt zu viele Unternehmen, die die Bereitstellung, Größenänderung, Zuschneidung und Transkodierung von Bildern als Service anbieten. Einschließlich Open-Source- Thumbor . Vielleicht ist dies für uns in Zukunft die einfachste Möglichkeit, die Unterstützung für reaktionsschnelle Bilder und dynamische Inhaltstypen zu implementieren und auf dem neuesten Stand des Fortschritts zu bleiben. Aber jetzt machen wir es alleine.

Weitere Lektüre


Die beiden hier erwähnten Bücher sind außerhalb des Kontextes dieses Artikels völlig autark und werden dringend zur weiteren Lektüre zu diesem Thema empfohlen.


Jetzt auch beliebt: