Warum Bilder stapelweise komprimieren
Sie haben 1000 hochauflösende Fotos, die von einer Kamera exportiert wurden, jedes etwa 8 MB groß, und das gesamte Verzeichnis verbraucht fast 8 GB. Wenn das Ziel darin besteht, sie auf eine Website hochzuladen, in eine Präsentation einzubetten oder per E-Mail zu versenden, ist dieses Volumen völlig inakzeptabel. Sie in einem grafischen Editor einzeln zu komprimieren ist sowohl ineffizient als auch fehleranfällig — genau hier glänzt die skriptgesteuerte Stapelverarbeitung.
Die Bibliothek Pillow von Python ist der De-facto-Standard für die Bildverarbeitung. Sie bietet Lese- und Schreibunterstützung für gängige Formate einschließlich JPEG, PNG, WebP, BMP und TIFF sowie feinkörnige Kontrolle über Qualitätsfaktoren, Abtastraten und Farbmodi. Mit einigen Dutzend Zeilen Python können Sie einen gesamten Verzeichnisbaum durchlaufen, eine einheitliche Komprimierungsstrategie anwenden und die ursprüngliche Ordnerstruktur in der Ausgabe beibehalten.
Dieser Artikel beginnt mit der grundlegenden Verwendung von Pillow und erstellt schrittweise ein vollständiges, ausführbares, multithreaded Stapelkomprimierungsskript. Er behandelt auch praktische Engineering-Details wie EXIF-Erhaltung, Formatkonvertierung und intelligente Parameterauswahl.
Pillow-Installation und grundlegende Verwendung
Installation
Pillow ist der aktiv gepflegte Fork von PIL (Python Imaging Library), aber der Importname bleibt PIL. Installieren Sie es mit pip:
pip install Pillow
Wenn Sie ein großes Bildvolumen verarbeiten müssen oder eine schnellere Dekodierung wünschen, installieren Sie die Vollversion mit Plugin-Unterstützung:
pip install "Pillow[all]"
Überprüfen Sie nach der Installation die Version:
from PIL import Image, __version__
print(__version__) # z. B. 10.4.0
Grundlegende Operationen
Der Haupteinstiegspunkt von Pillow ist die Klasse Image. Die drei häufigsten Operationen sind Öffnen, Ändern und Speichern:
from PIL import Image
# Ein Bild öffnen (Lazy Loading; Pixeldaten werden beim ersten Zugriff gelesen)
img = Image.open("photo.jpg")
print(img.format, img.size, img.mode) # JPEG (4000, 3000) RGB
# Einen Pixelwert abrufen
pixel = img.getpixel((100, 100))
# Größe ändern
resized = img.resize((1920, 1080))
# Speichern
resized.save("photo_small.jpg", quality=85)
Einige Details sind erwähnenswert: Das von Image.open() zurückgegebene Objekt hält eine Dateireferenz, die erst freigegeben wird, wenn sie explizit geschlossen wird oder der with-Block verlassen wird. Bei der Stapelverarbeitung sollten Sie immer einen Kontextmanager verwenden oder close() explizit aufrufen, da sonst Datei-Handles lecken.
Beispiele zur Komprimierung einzelner Bilder
Die Grundlage der Stapelkomprimierung ist die Verarbeitung einzelner Bilder. Wir implementieren zunächst eine Funktion, die JPEG-Qualitätsanpassung, Auflösungsverkleinerung und Formatkonvertierung unterstützt.
JPEG-Qualitätsanpassung
Der Parameter quality von JPEG reicht von 1 bis 100 und steuert direkt die Skalierung der Quantisierungstabelle. Er ist der wichtigste Regler zum Ausbalancieren von Dateigröße und Bildqualität:
from PIL import Image
def compress_jpeg(input_path, output_path, quality=75):
"""Ein JPEG mit dem angegebenen Qualitätsfaktor komprimieren"""
with Image.open(input_path) as img:
# JPEG unterstützt keine Transparenz; RGBA/P-Modi müssen zuerst konvertiert werden
if img.mode in ("RGBA", "P", "LA"):
img = img.convert("RGB")
elif img.mode != "RGB":
img = img.convert("RGB")
img.save(
output_path,
format="JPEG",
quality=quality,
optimize=True, # Huffman-Kodierungsoptimierung aktivieren
progressive=True, # Progressives JPEG erzeugen; etwas größer aber besseres Ladeerlebnis
)
# Verschiedene Qualitätsfaktoren vergleichen
compress_jpeg("photo.jpg", "q95.jpg", quality=95)
compress_jpeg("photo.jpg", "q75.jpg", quality=75)
compress_jpeg("photo.jpg", "q50.jpg", quality=50)
optimize=True bewirkt, dass Pillow die Daten beim Speichern ein weiteres Mal scannt, um eine optimale Huffman-Kodierungstabelle zu erstellen. Dies reduziert die Dateigröße typischerweise um weitere 2-5 % auf Kosten eines etwas langsameren Speicherns.
Auflösungsanpassung
Für die Webanzeige ist ein Original von 4000x3000 weit größer als nötig. thumbnail() für die proportionale Verkleinerung zu verwenden ist der effektivste Ansatz — eine Halbierung der Auflösung reduziert die Dateigröße um etwa 75 %:
from PIL import Image
def resize_image(input_path, output_path, max_size=1920, quality=80):
"""Die längste Kante auf max_size skalieren, wobei das Seitenverhältnis erhalten bleibt"""
with Image.open(input_path) as img:
# thumbnail ändert das Bild an Ort und Stelle und überschreitet nie die angegebenen Abmessungen
# Image.Resampling.LANCZOS ist der Algorithmus mit der besten Qualität beim Downsampling
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
if img.mode != "RGB":
img = img.convert("RGB")
img.save(output_path, format="JPEG", quality=quality, optimize=True)
resize_image("photo.jpg", "photo_1920.jpg", max_size=1920, quality=80)
Der Unterschied zwischen thumbnail() und resize() besteht darin, dass thumbnail() das Seitenverhältnis bewahrt und das Bild nie vergrößert — die endgültigen Abmessungen sind immer kleiner oder gleich den angegebenen Werten. resize() zwingt das Bild auf die exakten Zielabmessungen. Bei der Stapelverarbeitung wollen Sie fast immer thumbnail().
Formatkonvertierung
Das Konvertieren von PNG zu WebP ist eine häufige Optimierung — WebP ist 26 % kleiner als PNG und 25-35 % kleiner als JPEG bei vergleichbarer Qualität, während es zudem Transparenz unterstützt:
from PIL import Image
def convert_to_webp(input_path, output_path, quality=80, lossless=False):
"""Beliebiges Format in WebP konvertieren"""
with Image.open(input_path) as img:
img.save(
output_path,
format="WebP",
quality=quality,
lossless=lossless, # Wenn True, wird der verlustfreie Modus verwendet
method=6, # Komprimierungsaufwand 0-6; 6 ist am langsamsten, aber am kleinsten
)
convert_to_webp("icon.png", "icon.webp", quality=85)
convert_to_webp("logo.png", "logo_lossless.webp", lossless=True)
Der Parameter method steuert den Komprimierungsaufwand von 0 bis 6. Höhere Werte liefern bessere Komprimierungsverhältnisse, dauern aber länger. Für die Stapelverarbeitung, bei der die endgültige Dateigröße zählt, stellen Sie ihn auf 6; für Geschwindigkeit ist 4 ein guter Kompromiss.
Vollständiges Stapelkomprimierungsskript
Jetzt fügen wir die Einzelbild-Logik zu einem vollständigen Stapelskript zusammen. Dieses Skript durchläuft alle Bilder im Eingabeverzeichnis, behält die ursprüngliche Ordnerstruktur in der Ausgabe bei und zeigt den Verarbeitungsfortschritt sowie die gesamten Komprimierungsergebnisse an.
Das folgende Flussdiagramm veranschaulicht den gesamten Stapelverarbeitungsablauf:
Das vollständige Skript:
"""
batch_compress.py - Skript zur stapelweisen Bildkomprimierung
Verwendung: python batch_compress.py <input_dir> <output_dir> [--quality 75] [--max-size 1920]
"""
import os
import sys
import argparse
from pathlib import Path
from PIL import Image
SUPPORTED_FORMATS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff", ".tif"}
def compress_single(input_path, output_path, quality=75, max_size=1920,
target_format=None, keep_exif=True):
"""Ein einzelnes Bild komprimieren"""
with Image.open(input_path) as img:
# 1. EXIF-Daten extrahieren (Aufnahmezeit, Kameramodell, GPS usw.)
exif_data = img.info.get("exif", b"") if keep_exif else b""
# 2. Farbmodus-Konvertierung: JPEG unterstützt keine Transparenz
out_format = (target_format or img.format or "JPEG").upper()
if out_format in ("JPEG", "JPG") and img.mode in ("RGBA", "P", "LA"):
# Auf weißen Hintergrund komponieren, um schwarze Bereiche nach der Konvertierung zu vermeiden
background = Image.new("RGB", img.size, (255, 255, 255))
if img.mode == "P":
img = img.convert("RGBA")
background.paste(img, mask=img.split()[-1] if img.mode == "RGBA" else None)
img = background
elif img.mode not in ("RGB", "RGBA", "L"):
img = img.convert("RGB")
# 3. Auflösungsanpassung: nur verkleinern, niemals vergrößern
if max_size and max(img.size) > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# 4. Speichern
save_kwargs = {"optimize": True}
if out_format in ("JPEG", "JPG"):
save_kwargs["quality"] = quality
save_kwargs["progressive"] = True
if exif_data:
save_kwargs["exif"] = exif_data
elif out_format == "WEBP":
save_kwargs["quality"] = quality
save_kwargs["method"] = 6
elif out_format == "PNG":
save_kwargs["optimize"] = True
img.save(output_path, format=out_format, **save_kwargs)
def batch_compress(input_dir, output_dir, quality=75, max_size=1920,
target_format=None, keep_exif=True):
"""Ein gesamtes Verzeichnis stapelweise komprimieren"""
input_dir = Path(input_dir)
output_dir = Path(output_dir)
# Alle zu verarbeitenden Dateien sammeln
tasks = []
for root, _, files in os.walk(input_dir):
for filename in files:
if Path(filename).suffix.lower() in SUPPORTED_FORMATS:
src = Path(root) / filename
rel = src.relative_to(input_dir)
dst = output_dir / rel
# Erweiterung basierend auf dem Zielformat anpassen
if target_format:
dst = dst.with_suffix(f".{target_format.lower()}")
tasks.append((src, dst))
total = len(tasks)
if total == 0:
print("Keine unterstützten Bilddateien gefunden")
return
print(f"{total} Bilder gefunden. Komprimierung wird gestartet...\n")
processed = 0
total_original = 0
total_compressed = 0
errors = []
for src, dst in tasks:
# Sicherstellen, dass das Ausgabeverzeichnis existiert
dst.parent.mkdir(parents=True, exist_ok=True)
original_size = src.stat().st_size
try:
compress_single(src, dst, quality, max_size, target_format, keep_exif)
new_size = dst.stat().st_size
total_original += original_size
total_compressed += new_size
except Exception as e:
errors.append((src, str(e)))
new_size = original_size # Bei Fehlschlag als Originalgröße zählen
processed += 1
pct = processed / total * 100
saved = (1 - new_size / original_size) * 100 if original_size else 0
print(f"[{pct:5.1f}%] ({processed}/{total}) {src.relative_to(input_dir)} "
f"{original_size // 1024}KB -> {new_size // 1024}KB (-{saved:.1f}%)")
# Zusammenfassungsbericht
print(f"\n{'=' * 50}")
print(f"Fertig: {processed} erfolgreich, {len(errors)} fehlgeschlagen")
if total_original > 0:
ratio = total_original / total_compressed if total_compressed else 0
saved_mb = (total_original - total_compressed) / 1024 / 1024
print(f"Originalgröße: {total_original / 1024 / 1024:.2f} MB")
print(f"Komprimierte Größe: {total_compressed / 1024 / 1024:.2f} MB")
print(f"Eingesparter Speicherplatz: {saved_mb:.2f} MB (Verhältnis {ratio:.2f}x)")
for src, err in errors:
print(f" Fehlgeschlagen: {src} - {err}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Werkzeug zur stapelweisen Bildkomprimierung")
parser.add_argument("input_dir", help="Eingabeverzeichnis für Bilder")
parser.add_argument("output_dir", help="Ausgabeverzeichnis")
parser.add_argument("--quality", type=int, default=75, help="JPEG/WebP-Qualitätsfaktor (1-100)")
parser.add_argument("--max-size", type=int, default=1920, help="Maximale längste Kante in Pixeln")
parser.add_argument("--format", default=None, help="Zielformat (JPEG/WebP/PNG)")
parser.add_argument("--no-exif", action="store_true", help="EXIF-Daten verwerfen")
args = parser.parse_args()
batch_compress(
args.input_dir,
args.output_dir,
quality=args.quality,
max_size=args.max_size,
target_format=args.format,
keep_exif=not args.no_exif,
)
Verwendungsbeispiele:
# Grundlegende Verwendung
python batch_compress.py ./photos ./output
# In WebP konvertieren, Qualität 80, maximale längste Kante 2560
python batch_compress.py ./photos ./output --quality 80 --max-size 2560 --format webp
Dieses Skript beruht auf drei Kern-Designprinzipien: Es verwendet os.walk() zum rekursiven Durchlaufen und Erhalten der Verzeichnisstruktur; es verwendet relative_to() zum Berechnen relativer Pfade, um sicherzustellen, dass das Ausgabelayout mit der Eingabe übereinstimmt; und es verwendet Path.stat() zum Abrufen von Dateigrößen zur Berechnung der tatsächlichen Komprimierungswirkung.
Erweiterte Techniken
EXIF-Informationen erhalten
EXIF-Daten enthalten Aufnahmezeit, GPS-Koordinaten, Kameraparameter, Blende, Verschlusszeit und andere Metadaten. Standardmäßig verwirft img.save() diese Informationen. Um sie zu erhalten, müssen Sie sie manuell extrahieren und beim Speichern übergeben:
from PIL import Image
def compress_with_exif(input_path, output_path, quality=75):
with Image.open(input_path) as img:
exif = img.info.get("exif", b"") # Rohe EXIF-Bytes extrahieren
if img.mode != "RGB":
img = img.convert("RGB")
img.save(
output_path,
format="JPEG",
quality=quality,
exif=exif if exif else None, # None übergeben, wenn kein EXIF vorhanden
optimize=True,
)
Wenn Sie nur bestimmte EXIF-Felder behalten müssen (z. B. nur das Orientierungs-Tag), verwenden Sie die Methode getexif(), um Elemente einzeln auszuwählen:
from PIL import ExifTags
def keep_orientation_only(input_path, output_path, quality=75):
with Image.open(input_path) as img:
exif = img.getexif()
orientation = exif.get(0x0112) # Orientierungs-Tag
# Bild basierend auf dem Orientierungs-Tag drehen
if orientation:
from PIL import ImageOps
img = ImageOps.exif_transpose(img)
img.save(output_path, format="JPEG", quality=quality, optimize=True)
ImageOps.exif_transpose() dreht das Bild automatisch gemäß dem EXIF-Orientierungs-Tag. Dies ist besonders nützlich beim Verarbeiten von Fotos, die mit Smartphones aufgenommen wurden — viele Telefonsensoren sind im Querformat montiert und verlassen sich auf das EXIF-Orientierungs-Tag, um die korrekte Anzeigerichtung anzugeben.
PNG zu WebP
Beim Konvertieren von PNG zu WebP müssen Sie basierend auf den Bildmerkmalen zwischen verlustbehafteten und verlustfreien Modi wählen. Für Bilder mit Text und scharfen Linien (Icons, UI-Elemente) erhält verlustfreies WebP knackige Kanten. Für fotografische Inhalte bietet verlustbehaftetes WebP einen erheblichen Größenvorteil:
from PIL import Image
def png_to_webp(input_path, output_path, lossless_threshold=0.3):
"""Intelligent zwischen verlustbehaftetem oder verlustfreiem WebP basierend auf Bildmerkmalen wählen"""
with Image.open(input_path) as img:
# Farben zählen, um festzustellen, ob es sich um eine einfache Grafik handelt
colors = img.getcolors(maxcolors=65536)
is_simple = colors is not None and len(colors) < 256
if is_simple:
# Wenige Farben, scharfe Kanten: verlustfreier Modus
img.save(output_path, format="WebP", lossless=True, method=6)
print(f"Verlustfreies WebP (Farbanzahl: {len(colors)})")
else:
# Reichhaltige Farben, fotografisch: verlustbehafteter Modus
if img.mode != "RGB":
img = img.convert("RGB")
img.save(output_path, format="WebP", quality=82, method=6)
print("Verlustbehaftetes WebP (fotografisch)")
getcolors() gibt eine Liste von Farben oder None zurück (wenn die Farbanzahl maxcolors überschreitet). Eine geringe Farbanzahl weist meist auf eine einfache Grafik wie ein Icon oder einen Screenshot hin, die besser für die verlustfreie Komprimierung geeignet ist.
Intelligente Parameterauswahl
Verschiedene Bilder eignen sich am besten für unterschiedliche Komprimierungsstrategien. Die folgende Funktion wählt automatisch Format und Parameter basierend auf Auflösung, Transparenz und Farbanzahl aus:
from PIL import Image
def smart_compress(input_path, output_path):
"""Automatisch die optimale Komprimierungsstrategie basierend auf Bildmerkmalen auswählen"""
with Image.open(input_path) as img:
width, height = img.size
pixel_count = width * height
mode = img.mode
has_transparency = mode in ("RGBA", "LA") or (
mode == "P" and "transparency" in img.info
)
# Strategie 1: Hat Transparenz -> WebP (balanciert Transparenz und Größe)
if has_transparency:
if pixel_count > 2_000_000:
q = 80
else:
q = 88
img.save(output_path, format="WebP", quality=q, method=6)
return "Verlustbehaftetes WebP (transparent)"
# Strategie 2: Sehr großes Foto -> verkleinern + JPEG mittlere Qualität
if pixel_count > 8_000_000 or width > 4000:
img = img.convert("RGB")
img.thumbnail((2560, 2560), Image.Resampling.LANCZOS)
img.save(output_path, format="JPEG", quality=75, optimize=True)
return "JPEG (großes Bild verkleinert)"
# Strategie 3: Normales Foto -> JPEG höhere Qualität
if pixel_count > 500_000:
img = img.convert("RGB")
img.save(output_path, format="JPEG", quality=82, optimize=True)
return "JPEG (hohe Qualität)"
# Strategie 4: Kleines Bild -> als PNG belassen
img.save(output_path, format="PNG", optimize=True)
return "PNG (kleines Bild erhalten)"
Die Kernlogik dieser Strategie ist: Bilder mit Transparenz gehen zu WebP, große Bilder werden vor der Komprimierung verkleinert, normale Fotos verwenden JPEG und kleine Bilder verwenden PNG für Detailtreue. Sie können die Schwellenwerte an Ihre tatsächlichen Anforderungen anpassen.
Leistungsoptimierung
Multithreaded-Verarbeitung
Bildkomprimierung ist eine Aufgabe, die E/A-gebundene und CPU-gebundene Arbeit mischt. Lese-/Schreibvorgänge auf der Festplatte und Kodierung machen jeweils einen Teil der Zeit aus. Pythons GIL begrenzt reine CPU-Parallelität, aber Pillow gibt den GIL frei, wenn es die zugrunde liegenden C-Bibliotheken für die JPEG-/WebP-Kodierung aufruft, sodass Multithreading erhebliche Beschleunigungen bringen kann:
import os
from pathlib import Path
from PIL import Image
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
SUPPORTED_FORMATS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff"}
_print_lock = threading.Lock()
def compress_single(input_path, output_path, quality=75, max_size=1920):
with Image.open(input_path) as img:
exif = img.info.get("exif", b"")
if img.mode != "RGB":
img = img.convert("RGB")
if max_size and max(img.size) > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
kwargs = {"quality": quality, "optimize": True, "progressive": True}
if exif:
kwargs["exif"] = exif
img.save(output_path, format="JPEG", **kwargs)
def batch_compress_threaded(input_dir, output_dir, quality=75, max_size=1920,
workers=4):
input_dir = Path(input_dir)
output_dir = Path(output_dir)
# Aufgaben sammeln
tasks = []
for root, _, files in os.walk(input_dir):
for f in files:
if Path(f).suffix.lower() in SUPPORTED_FORMATS:
src = Path(root) / f
rel = src.relative_to(input_dir)
dst = output_dir / rel
dst.parent.mkdir(parents=True, exist_ok=True)
tasks.append((src, dst))
total = len(tasks)
print(f"{total} Bilder insgesamt, Verarbeitung mit {workers} Threads\n")
completed = 0
total_saved = 0
with ThreadPoolExecutor(max_workers=workers) as executor:
futures = {
executor.submit(compress_single, src, dst, quality, max_size): (src, dst)
for src, dst in tasks
}
for future in as_completed(futures):
src, dst = futures[future]
try:
future.result()
saved = src.stat().st_size - dst.stat().st_size
total_saved += saved
except Exception as e:
with _print_lock:
print(f"Fehlgeschlagen: {src.name} - {e}")
completed += 1
with _print_lock:
print(f"[{completed / total * 100:5.1f}%] ({completed}/{total}) {src.name}")
print(f"\nFertig. Eingesparter Speicherplatz: {total_saved / 1024 / 1024:.2f} MB")
if __name__ == "__main__":
batch_compress_threaded("./photos", "./output", quality=75, max_size=1920, workers=4)
Die Faustregel für die Thread-Anzahl ist die Anzahl der logischen CPU-Kerne. Auf einer 8-Kern-Maschine erreichen 4-6 Threads in der Regel eine nahezu optimale Beschleunigung. Zu viele Threads können die Leistung aufgrund von E/A-Konflikten und Speicherdruck tatsächlich verschlechtern.
Wenn Sie beim Parallelismus noch weiter gehen möchten, können Sie ProcessPoolExecutor verwenden, um den GIL zu umgehen. Der Kompromiss ist, dass Prozesse keinen Speicher teilen können, jeder Prozess die Pillow-Bibliothek unabhängig laden muss und der Start-Overhead höher ist.
Speicherverwaltung
Der Speicherverbrauch wird beim Verarbeiten hochauflösender Bilder zu einem Anliegen. Ein einzelnes 6000x4000 RGB-Bild belegt nach der Dekodierung etwa 72 MB Speicher (6000x4000x3 Bytes). Wenn 10 gleichzeitig geöffnet sind, sind das 720 MB.
Mehrere Schlüsselprinzipien der Speicherverwaltung:
from PIL import Image
import gc
# 1. Immer die with-Anweisung verwenden, um sicherzustellen, dass Datei-Handles und Pixel-Puffer promptly freigegeben werden
def safe_compress(input_path, output_path, quality=75):
with Image.open(input_path) as img:
# 2. Das Bild kopieren, wenn Sie es ändern müssen, um Operationen am Original zu vermeiden
work = img.copy()
# img wird freigegeben; work ist noch im Gültigkeitsbereich
if work.mode != "RGB":
work = work.convert("RGB")
work.save(output_path, format="JPEG", quality=quality, optimize=True)
work.close() # 3. Explizit schließen
# 4. Während der Stapelverarbeitung regelmäßig Garbage Collection auslösen
def batch_with_gc(tasks, interval=100):
for i, task in enumerate(tasks):
process(task)
if i % interval == 0:
gc.collect()
Zusätzlich hat Image.MAX_IMAGE_PIXELS standardmäßig ein Limit von etwa 178 Millionen Pixeln (etwa 8900x8900). Wird es überschritten, wird eine DecompressionBombError ausgelöst. Wenn Sie wirklich sehr große Bilder verarbeiten müssen, können Sie diesen Schwellenwert erhöhen, beachten Sie aber den Speicherverbrauch:
Image.MAX_IMAGE_PIXELS = None # Limit deaktivieren (mit Vorsicht verwenden)
Vergleich der Komprimierungsergebnisse
Die folgende Tabelle zeigt gemessene Daten aus einem Satz von 50 Smartphone-Fotos (Originaldurchschnitt 6,5 MB, 4000x3000 Pixel). Alle Tests liefen auf derselben Hardware, mit einer einheitlich auf eine längste Kante von 1920 verkleinerten Auflösung:
| Qualitätsfaktor | Format | Durchschnittliche Größe | Verhältnis | Subjektive Qualität | Anwendungsfall |
|---|---|---|---|---|---|
| Original | JPEG | 6,5 MB | 1,0x | Referenz | — |
| 95 | JPEG | 1,8 MB | 3,6x | Praktisch identisch | Hochwertige Archivierung |
| 85 | JPEG | 0,92 MB | 7,1x | Unmerklicher Unterschied | Hero-Bilder der Website |
| 75 | JPEG | 0,58 MB | 11,2x | Leichte Artefakte bei Nahbetrachtung | Miniaturansichten, Listenseiten |
| 65 | JPEG | 0,42 MB | 15,5x | Sichtbare Blockartefakte | Nicht empfohlen |
| 50 | JPEG | 0,28 MB | 23,2x | Offensichtliche Verzerrung | Nur winzige Vorschauen |
| 80 | WebP | 0,51 MB | 12,7x | Vergleichbar mit JPEG 85 | Moderne Websites |
| Verlustfrei | WebP | 4,2 MB | 1,5x | Perfekt verlustfrei | Wenn Transparenz benötigt wird |
Mehrere Muster gehen aus den Daten hervor:
- Qualität 85 ist der Kosten-Nutzen-Wendepunkt: Sie spart fast die Hälfte der Größe gegenüber 95, mit Qualitätsunterschieden, die für das Auge praktisch unmerklich sind.
- Unter Qualität 70 nehmen die Erträge schnell ab: Die Dateigröße continues sinkt weiter, aber die Qualitätsverschlechterung beschleunigt sich spürbar.
- WebP-Qualitätsfaktoren sind nicht direkt mit JPEG vergleichbar: WebP 80 erzeugt eine Qualität vergleichbar mit JPEG 85, aber bei kleinerer Größe.
- Verlustfreies WebP ist nur sinnvoll, wenn Transparenz erforderlich ist; für Fotografien ist der Größenvorteil minimal.
FAQ
F1: Warum erscheinen Bilder nach der Komprimierung kopfständig oder quer?
Dies passiert, weil das EXIF-Orientierungs-Tag des Smartphones während der Komprimierung verworfen wird, während die tatsächlichen Pixeldaten im Querformat gespeichert wurden. Die Lösung besteht darin, vor dem Speichern ImageOps.exif_transpose(img) aufzurufen. Es dreht die Pixeldaten gemäß dem EXIF-Orientierungs-Tag, sodass das Bild vor dem Speichern korrekt ausgerichtet ist. Auf diese Weise wird das Bild auch dann korrekt angezeigt, wenn nachgelagerte Software die EXIF-Orientierungsinformationen nicht liest.
F2: Warum wächst die Speichernutzung beim Verarbeiten von 1000 Bildern ständig weiter?
Die häufigste Ursache ist das nicht rechtzeitige Schließen von Bildobjekten. Verwenden Sie immer den Kontextmanager with Image.open(...) as img:, um sicherzustellen, dass jedes Bild sofort nach der Verarbeitung freigegeben wird. Zweitens: In der Multithread-Version, wenn alle Aufgaben auf einmal übermittelt werden, halten die Future-Objekte Referenzen auf Ergebnisse, bis sie verbraucht sind, was zu Speicheransammlung führt. Die Lösung besteht darin, Aufgaben in Chargen mit chunksize zu übermitteln oder zu einem Producer-Consumer-Warteschlangenmuster zu wechseln, um die Nebenläufigkeit zu steuern. Schließlich kann das regelmäßige Aufrufen von gc.collect() Speicher zurückgewinnen, der durch zirkuläre Referenzen gehalten wird.
F3: Warum werden transparente Bereiche nach der Konvertierung von PNG zu JPEG schwarz?
Das JPEG-Format unterstützt keine Transparenz. Wenn Pillow RGBA standardmäßig in RGB konvertiert, füllt es den transparenten Hintergrund mit Schwarz. Die Lösung besteht darin, zuerst ein RGB-Bild mit weißem Hintergrund zu erstellen und dann paste() zu verwenden, um das Originalbild mit dem Alpha-Kanal als Maske zu überlagern:
background = Image.new("RGB", img.size, (255, 255, 255))
background.paste(img, mask=img.split()[3]) # Der 4. Kanal ist Alpha
Alternativ konvertieren Sie direkt zu WebP, das nativ Transparenz ohne zusätzliche Behandlung unterstützt.
F4: Warum bietet Multithreading keine lineare Beschleunigung?
Bildkomprimierung umfasst sowohl Festplatten-E/A als auch CPU-Kodierung. Sobald die Thread-Anzahl einen bestimmten Punkt überschreitet, wird die Festplattenbandbreite zum Engpass — die CPU kann nicht schneller sein, als Daten gelesen und geschrieben werden können. Außerdem sind die dekodierten Pixeldaten hochauflösender Bilder groß, und das gleichzeitige Verarbeiten mehrerer großer Bilder in verschiedenen Threads führt zu Speicherbandbreiten-Konflikten und reduzierten Cache-Trefferraten. In der Praxis liefern 4-8 Threads typischerweise eine 2-4x-Beschleunigung, mit abnehmendem Nutzen darüber hinaus. Für mechanische Festplatten werden 4 Threads empfohlen; für NVMe-SSDs kann die Anzahl etwas erhöht werden.
Zusammenfassung
Die stapelweise Bildkomprimierung mit Python Pillow lässt sich in drei Schichten unterteilen:
- Grundschicht: Beherrschen Sie die drei Methoden
Image.open(),thumbnail()undsave(), kombiniert mit den Parameternqualityundoptimize, und Sie können ein einzelnes Bild komprimieren. Dies ist der Baustein für die gesamte Stapelverarbeitung. - Engineering-Schicht: Verwenden Sie
os.walk()zum Durchlaufen von Verzeichnissen,Path.relative_to()zum Erhalten der Struktur undconcurrent.futuresfür die parallele Verarbeitung, um die Einzelbild-Logik auf Tausende von Bildern zu skalieren. Diese Schicht konzentriert sich auf Korrektheit, Robustheit und Durchsatz. - Optimierungsschicht: Wählen Sie intelligent Formate und Parameter basierend auf Bildmerkmalen — transparente Bilder gehen zu WebP, große Bilder werden zuerst verkleinert, kleine Bilder bleiben PNG. Details wie EXIF-Erhaltung, Orientierungskorrektur und Speicherverwaltung bestimmen, ob das Skript zuverlässig in einer Produktionsumgebung laufen kann.
Es gibt eine Faustregel für die Auswahl des Qualitätsfaktors: 85 ist die Schwelle für visuell verlustfreie Qualität, 75 ist der Balancepunkt zwischen Größe und Qualität, und unter 65 wird für Anzeigezwecke nicht empfohlen. WebP wird nun von modernen Browsern vollständig unterstützt und sollte das bevorzugte Format für neue Projekte sein.
Der Wert dieses Skripts liegt nicht im Ersetzen professioneller Tools, sondern in seiner Anpassbarkeit. Sie können jede Phase an Ihre tatsächlichen Bedürfnisse anpassen — verschiedene Parameter für bestimmte Verzeichnisse konfigurieren, nach der Komprimierung automatisch zu einem CDN hochladen oder es in eine CI/CD-Pipeline integrieren. Das Verständnis der Prinzipien hinter jeder Codezeile gibt Ihnen die Grundlage, jedes Bildverarbeitungsszenario zu bewältigen.
Weiterführende Lektüre:
- Leitfaden zur Bildkomprimierung: Vergleich und Auswahl der Formate JPG/PNG/WebP
- Why JPEG Quality 75 Is the Magic Number for Image Compression
Dateien komprimieren? Probieren Sie SmartSlim
Basierend auf einer selbstentwickelten Rust-Kompressions-Engine, unterstützt über 40 Formate in 10 Kategorien wie PDF, Bilder, Video, Office und OFD. Die Komprimierung erfolgt lokal, ohne dass Daten Ihre Umgebung verlassen.