Pourquoi compresser les images par lots
Vous avez 1000 photos haute résolution exportées depuis un appareil photo, chacune d'environ 8 Mo, et le répertoire entier consomme près de 8 Go. Si l'objectif est de les télécharger sur un site web, de les intégrer dans une présentation ou de les envoyer par e-mail, ce volume est totalement inacceptable. Les compresser une par une dans un éditeur graphique est à la fois inefficace et source d'erreurs — c'est précisément là que le traitement par lots scripté excelle.
La bibliothèque Pillow de Python est le standard de fait pour le traitement d'images. Elle offre une prise en charge en lecture et écriture des formats grand public, notamment JPEG, PNG, WebP, BMP et TIFF, ainsi qu'un contrôle fin des facteurs de qualité, des taux d'échantillonnage et des modes de couleur. Avec quelques dizaines de lignes de Python, vous pouvez parcourir une arborescence de répertoires entière, appliquer une stratégie de compression uniforme et préserver la structure des dossiers d'origine dans la sortie.
Cet article part de l'utilisation de base de Pillow et construit progressivement un script de compression par lots complet, exécutable et multithreadé. Il couvre également des détails d'ingénierie pratiques tels que la préservation EXIF, la conversion de format et la sélection intelligente des paramètres.
Installation et utilisation de base de Pillow
Installation
Pillow est le fork activement maintenu de PIL (Python Imaging Library), mais le nom d'import reste PIL. Installez-le avec pip :
pip install Pillow
Si vous devez traiter un grand volume d'images ou souhaitez un décodage plus rapide, installez la version complète avec prise en charge des plugins :
pip install "Pillow[all]"
Après l'installation, vérifiez la version :
from PIL import Image, __version__
print(__version__) # par exemple, 10.4.0
Opérations de base
Le point d'entrée principal de Pillow est la classe Image. Les trois opérations les plus courantes sont ouvrir, modifier et enregistrer :
from PIL import Image
# Ouvrir une image (chargement paresseux ; les données pixel sont lues au premier accès)
img = Image.open("photo.jpg")
print(img.format, img.size, img.mode) # JPEG (4000, 3000) RGB
# Obtenir la valeur d'un pixel
pixel = img.getpixel((100, 100))
# Redimensionner
resized = img.resize((1920, 1080))
# Enregistrer
resized.save("photo_small.jpg", quality=85)
Quelques détails méritent d'être notés : l'objet renvoyé par Image.open() conserve une référence au fichier qui n'est libérée qu'à la fermeture explicite ou à la sortie du bloc with. En traitement par lots, utilisez toujours un gestionnaire de contexte ou appelez close() explicitement, sinon les descripteurs de fichier fuient.
Exemples de compression d'une seule image
La base de la compression par lots est le traitement d'une seule image. Nous implémentons d'abord une fonction qui prend en charge le réglage de la qualité JPEG, la réduction de résolution et la conversion de format.
Réglage de la qualité JPEG
Le paramètre quality du JPEG varie de 1 à 100 et contrôle directement la mise à l'échelle de la table de quantification. C'est le principal levier pour équilibrer la taille du fichier et la qualité de l'image :
from PIL import Image
def compress_jpeg(input_path, output_path, quality=75):
"""Compresser un JPEG avec le facteur de qualité spécifié"""
with Image.open(input_path) as img:
# Le JPEG ne prend pas en charge la transparence ; les modes RGBA/P doivent être convertis d'abord
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, # Activer l'optimisation du codage Huffman
progressive=True, # Générer un JPEG progressif ; légèrement plus grand mais meilleure expérience de chargement
)
# Comparer différents facteurs de qualité
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 amène Pillow à scanner les données une fois de plus lors de l'enregistrement afin de reconstruire une table de codage Huffman optimale. Cela réduit généralement la taille du fichier de 2 à 5 % supplémentaires au prix d'un enregistrement légèrement plus lent.
Ajustement de la résolution
Pour l'affichage web, un original de 4000x3000 est bien plus grand que nécessaire. Utiliser thumbnail() pour réduire proportionnellement est l'approche la plus efficace — diviser la résolution par deux réduit la taille du fichier d'environ 75 % :
from PIL import Image
def resize_image(input_path, output_path, max_size=1920, quality=80):
"""Mettre à l'échelle le plus long côté à max_size en préservant le rapport d'aspect"""
with Image.open(input_path) as img:
# thumbnail modifie sur place et ne dépasse jamais les dimensions spécifiées
# Image.Resampling.LANCZOS est le meilleur algorithme de sous-échantillonnage en qualité
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)
La différence entre thumbnail() et resize() est que thumbnail() préserve le rapport d'aspect et n'agrandit jamais l'image — les dimensions finales sont toujours inférieures ou égales aux valeurs spécifiées. resize() force l'image aux dimensions cibles exactes. En traitement par lots, vous voulez presque toujours thumbnail().
Conversion de format
Convertir un PNG en WebP est une optimisation courante — WebP est 26 % plus petit que le PNG et 25-35 % plus petit que le JPEG à qualité équivalente, tout en prenant en charge la transparence :
from PIL import Image
def convert_to_webp(input_path, output_path, quality=80, lossless=False):
"""Convertir n'importe quel format en WebP"""
with Image.open(input_path) as img:
img.save(
output_path,
format="WebP",
quality=quality,
lossless=lossless, # Si True, utilise le mode sans perte
method=6, # Effort de compression 0-6 ; 6 est le plus lent mais le plus petit
)
convert_to_webp("icon.png", "icon.webp", quality=85)
convert_to_webp("logo.png", "logo_lossless.webp", lossless=True)
Le paramètre method contrôle l'effort de compression, de 0 à 6. Des valeurs plus élevées offrent de meilleurs ratios de compression mais prennent plus de temps. Pour un traitement par lots où la taille finale du fichier compte, réglez-le sur 6 ; pour la vitesse, 4 est un bon compromis.
Script complet de compression par lots
Nous assemblons maintenant la logique d'image unique en un script complet par lots. Ce script parcourt toutes les images du répertoire d'entrée, préserve la structure des dossiers d'origine dans la sortie et affiche la progression du traitement ainsi que les résultats globaux de compression.
L'organigramme ci-dessous illustre le flux de traitement par lots global :
Le script complet :
"""
batch_compress.py - Script de compression d'images par lots
Utilisation : 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):
"""Compresser une seule image"""
with Image.open(input_path) as img:
# 1. Extraire les données EXIF (date de prise de vue, modèle d'appareil, GPS, etc.)
exif_data = img.info.get("exif", b"") if keep_exif else b""
# 2. Conversion de mode de couleur : le JPEG ne prend pas en charge la transparence
out_format = (target_format or img.format or "JPEG").upper()
if out_format in ("JPEG", "JPG") and img.mode in ("RGBA", "P", "LA"):
# Compositage sur fond blanc pour éviter les régions noires après conversion
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. Ajustement de la résolution : uniquement réduire, jamais agrandir
if max_size and max(img.size) > max_size:
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
# 4. Enregistrer
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):
"""Compresser par lots un répertoire entier"""
input_dir = Path(input_dir)
output_dir = Path(output_dir)
# Collecter tous les fichiers à traiter
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
# Ajuster l'extension selon le format cible
if target_format:
dst = dst.with_suffix(f".{target_format.lower()}")
tasks.append((src, dst))
total = len(tasks)
if total == 0:
print("Aucun fichier image pris en charge trouvé")
return
print(f"{total} images trouvées. Démarrage de la compression...\n")
processed = 0
total_original = 0
total_compressed = 0
errors = []
for src, dst in tasks:
# S'assurer que le répertoire de sortie existe
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 # Comptabiliser comme taille d'origine en cas d'échec
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}%)")
# Rapport de synthèse
print(f"\n{'=' * 50}")
print(f"Terminé : {processed} réussis, {len(errors)} échoués")
if total_original > 0:
ratio = total_original / total_compressed if total_compressed else 0
saved_mb = (total_original - total_compressed) / 1024 / 1024
print(f"Taille d'origine : {total_original / 1024 / 1024:.2f} MB")
print(f"Taille compressée : {total_compressed / 1024 / 1024:.2f} MB")
print(f"Espace économisé : {saved_mb:.2f} MB (ratio {ratio:.2f}x)")
for src, err in errors:
print(f" Échec : {src} - {err}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Outil de compression d'images par lots")
parser.add_argument("input_dir", help="Répertoire d'images d'entrée")
parser.add_argument("output_dir", help="Répertoire de sortie")
parser.add_argument("--quality", type=int, default=75, help="Facteur de qualité JPEG/WebP (1-100)")
parser.add_argument("--max-size", type=int, default=1920, help="Plus long côté maximum en pixels")
parser.add_argument("--format", default=None, help="Format cible (JPEG/WebP/PNG)")
parser.add_argument("--no-exif", action="store_true", help="Ignorer les données EXIF")
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,
)
Exemples d'utilisation :
# Utilisation de base
python batch_compress.py ./photos ./output
# Convertir en WebP, qualité 80, plus long côté maximum 2560
python batch_compress.py ./photos ./output --quality 80 --max-size 2560 --format webp
Ce script repose sur trois principes de conception fondamentaux : il utilise os.walk() pour parcourir récursivement et préserver la structure des répertoires ; il utilise relative_to() pour calculer les chemins relatifs afin que la disposition de sortie corresponde à l'entrée ; et il utilise Path.stat() pour obtenir les tailles de fichier afin de calculer l'effet de compression réel.
Techniques avancées
Préservation des informations EXIF
Les données EXIF contiennent l'heure de prise de vue, les coordonnées GPS, les paramètres de l'appareil photo, l'ouverture, la vitesse d'obturation et d'autres métadonnées. Par défaut, img.save() ignore ces informations. Pour les préserver, vous devez les extraire manuellement et les transmettre lors de l'enregistrement :
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"") # Extraire les octets EXIF bruts
if img.mode != "RGB":
img = img.convert("RGB")
img.save(
output_path,
format="JPEG",
quality=quality,
exif=exif if exif else None, # Transmettre None s'il n'y a pas d'EXIF
optimize=True,
)
Si vous ne devez conserver que certains champs EXIF (par exemple, uniquement la balise d'orientation), utilisez la méthode getexif() pour sélectionner les éléments individuellement :
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) # Balise d'orientation
# Faire pivoter l'image selon la balise d'orientation
if orientation:
from PIL import ImageOps
img = ImageOps.exif_transpose(img)
img.save(output_path, format="JPEG", quality=quality, optimize=True)
ImageOps.exif_transpose() fait pivoter automatiquement l'image selon la balise d'orientation EXIF. Cela est particulièrement utile lors du traitement de photos prises avec des smartphones — de nombreux capteurs de téléphone sont montés en orientation paysage et s'appuient sur la balise d'orientation EXIF pour indiquer la bonne direction d'affichage.
PNG vers WebP
Lors de la conversion de PNG en WebP, vous devez choisir entre les modes avec et sans perte selon les caractéristiques de l'image. Pour les images contenant du texte et des lignes nettes (icônes, éléments d'interface), le WebP sans perte préserve des bords nets. Pour le contenu photographique, le WebP avec perte offre un avantage de taille significatif :
from PIL import Image
def png_to_webp(input_path, output_path, lossless_threshold=0.3):
"""Choisir intelligemment entre WebP avec ou sans perte selon les caractéristiques de l'image"""
with Image.open(input_path) as img:
# Compter les couleurs pour déterminer s'il s'agit d'un graphique simple
colors = img.getcolors(maxcolors=65536)
is_simple = colors is not None and len(colors) < 256
if is_simple:
# Peu de couleurs, bords nets : mode sans perte
img.save(output_path, format="WebP", lossless=True, method=6)
print(f"WebP sans perte (nombre de couleurs : {len(colors)})")
else:
# Couleurs riches, photographique : mode avec perte
if img.mode != "RGB":
img = img.convert("RGB")
img.save(output_path, format="WebP", quality=82, method=6)
print("WebP avec perte (photographique)")
getcolors() renvoie une liste de couleurs ou None (lorsque le nombre de couleurs dépasse maxcolors). Un petit nombre de couleurs indique généralement un graphique simple tel qu'une icône ou une capture d'écran, qui convient mieux à la compression sans perte.
Sélection intelligente des paramètres
Différentes images conviennent mieux à différentes stratégies de compression. La fonction suivante sélectionne automatiquement le format et les paramètres en fonction de la résolution, de la transparence et du nombre de couleurs :
from PIL import Image
def smart_compress(input_path, output_path):
"""Sélectionner automatiquement la stratégie de compression optimale selon les caractéristiques de l'image"""
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
)
# Stratégie 1 : A une transparence -> WebP (équilibre transparence et taille)
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 "WebP avec perte (transparent)"
# Stratégie 2 : Très grande photo -> réduire + JPEG qualité moyenne
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 (grande image réduite)"
# Stratégie 3 : Photo normale -> JPEG qualité supérieure
if pixel_count > 500_000:
img = img.convert("RGB")
img.save(output_path, format="JPEG", quality=82, optimize=True)
return "JPEG (haute qualité)"
# Stratégie 4 : Petite image -> garder en PNG
img.save(output_path, format="PNG", optimize=True)
return "PNG (petite image préservée)"
La logique centrale de cette stratégie est : les images avec transparence vont en WebP, les grandes images sont réduites avant compression, les photos normales utilisent JPEG, et les petites images utilisent PNG pour la fidélité. Vous pouvez ajuster les seuils selon vos besoins réels.
Optimisation des performances
Traitement multithreadé
La compression d'images est une tâche qui mélange un travail limité par les E/S et un travail limité par le CPU. La lecture/écriture du disque et l'encodage représentent chacun une partie du temps. Le GIL de Python limite le parallélisme CPU pur, mais Pillow libère le GIL lorsqu'il appelle les bibliothèques C sous-jacentes pour l'encodage JPEG/WebP, de sorte que le multithreading peut apporter des accélérations substantielles :
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)
# Collecter les tâches
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} images au total, traitement avec {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"Échec : {src.name} - {e}")
completed += 1
with _print_lock:
print(f"[{completed / total * 100:5.1f}%] ({completed}/{total}) {src.name}")
print(f"\nTerminé. Espace économisé : {total_saved / 1024 / 1024:.2f} MB")
if __name__ == "__main__":
batch_compress_threaded("./photos", "./output", quality=75, max_size=1920, workers=4)
La règle empirique pour le nombre de threads est le nombre de cœurs logiques du CPU. Sur une machine 8 cœurs, 4 à 6 threads atteignent généralement une accélération quasi optimale. Trop de threads peuvent en réalité dégrader les performances en raison de la contention des E/S disque et de la pression mémoire.
Si vous devez aller plus loin dans le parallélisme, vous pouvez utiliser ProcessPoolExecutor pour contourner le GIL. Le compromis est que les processus ne peuvent pas partager de mémoire, chaque processus doit charger indépendamment la bibliothèque Pillow, et la surcharge de démarrage est plus élevée.
Gestion de la mémoire
La consommation de mémoire devient un sujet de préoccupation lors du traitement d'images haute résolution. Une seule image RGB de 6000x4000 occupe environ 72 Mo de mémoire après décodage (6000x4000x3 octets). Si 10 sont ouvertes simultanément, cela représente 720 Mo.
Plusieurs principes clés de gestion de la mémoire :
from PIL import Image
import gc
# 1. Toujours utiliser l'instruction with pour s'assurer que les descripteurs de fichier et les tampons pixel sont libérés rapidement
def safe_compress(input_path, output_path, quality=75):
with Image.open(input_path) as img:
# 2. Copier l'image si vous devez la modifier, en évitant les opérations sur l'original
work = img.copy()
# img est libéré ; work est toujours dans la portée
if work.mode != "RGB":
work = work.convert("RGB")
work.save(output_path, format="JPEG", quality=quality, optimize=True)
work.close() # 3. Fermer explicitement
# 4. Déclencher le garbage collector périodiquement pendant le traitement par lots
def batch_with_gc(tasks, interval=100):
for i, task in enumerate(tasks):
process(task)
if i % interval == 0:
gc.collect()
De plus, Image.MAX_IMAGE_PIXELS a par défaut une limite d'environ 178 millions de pixels (environ 8900x8900). La dépasser lève une DecompressionBombError. Si vous devez réellement traiter de très grandes images, vous pouvez relever ce seuil, mais soyez attentif à la consommation de mémoire :
Image.MAX_IMAGE_PIXELS = None # Désactiver la limite (à utiliser avec prudence)
Comparaison des résultats de compression
Le tableau ci-dessous présente les données mesurées d'un ensemble de 50 photos de smartphone (moyenne d'origine 6,5 Mo, 4000x3000 pixels). Tous les tests ont été exécutés sur le même matériel, avec la résolution uniformément réduite à un plus long côté de 1920 :
| Facteur de qualité | Format | Taille moyenne | Ratio | Qualité subjective | Cas d'usage |
|---|---|---|---|---|---|
| Original | JPEG | 6,5 Mo | 1,0x | Référence | — |
| 95 | JPEG | 1,8 Mo | 3,6x | Pratiquement identique | Archivage haute qualité |
| 85 | JPEG | 0,92 Mo | 7,1x | Différence imperceptible | Images principales de site web |
| 75 | JPEG | 0,58 Mo | 11,2x | Légers artefacts à l'examen rapproché | Vignettes, pages de liste |
| 65 | JPEG | 0,42 Mo | 15,5x | Artefacts en blocs visibles | Non recommandé |
| 50 | JPEG | 0,28 Mo | 23,2x | Distorsion évidente | Uniquement aperçus minuscules |
| 80 | WebP | 0,51 Mo | 12,7x | Comparable à JPEG 85 | Sites web modernes |
| Sans perte | WebP | 4,2 Mo | 1,5x | Parfaitement sans perte | Quand la transparence est nécessaire |
Plusieurs schémas ressortent des données :
- La qualité 85 est le point d'inflexion coût-efficacité : elle économise près de la moitié de la taille par rapport à 95, avec des différences de qualité virtuellement imperceptibles à l'œil.
- En dessous de la qualité 70, les rendements diminuent rapidement : la taille du fichier continue de baisser, mais la dégradation de la qualité s'accélère nettement.
- Les facteurs de qualité WebP ne sont pas directement équivalents au JPEG : WebP 80 produit une qualité comparable à JPEG 85 mais à une taille inférieure.
- Le WebP sans perte n'a de sens que lorsque la transparence est requise ; pour les photographies, l'avantage de taille est minime.
FAQ
Q1 : Pourquoi les images apparaissent-elles à l'envers ou de côté après compression ?
Cela se produit parce que la balise d'orientation EXIF du smartphone est ignorée lors de la compression, alors que les données pixel réelles ont été stockées en orientation paysage. La solution consiste à appeler ImageOps.exif_transpose(img) avant l'enregistrement. Elle fait pivoter les données pixel selon la balise d'orientation EXIF afin que l'image soit correctement orientée avant d'être enregistrée. Ainsi, même si le logiciel en aval ne lit pas les informations d'orientation EXIF, l'image s'affichera correctement.
Q2 : Pourquoi l'utilisation de la mémoire ne cesse-t-elle d'augmenter lors du traitement de 1000 images ?
La cause la plus courante est de ne pas fermer les objets image rapidement. Utilisez toujours le gestionnaire de contexte with Image.open(...) as img: pour vous assurer que chaque image est libérée immédiatement après le traitement. Deuxièmement, dans la version multithreadée, si toutes les tâches sont soumises d'un coup, les objets future conservent des références aux résultats jusqu'à ce qu'ils soient consommés, ce qui entraîne une accumulation de mémoire. La solution consiste à soumettre les tâches par lots avec chunksize, ou à passer à un modèle de file producteur-consommateur pour contrôler la concurrence. Enfin, appeler gc.collect() périodiquement peut récupérer la mémoire détenue par des références circulaires.
Q3 : Pourquoi les zones transparentes deviennent-elles noires après la conversion de PNG en JPEG ?
Le format JPEG ne prend pas en charge la transparence. Lorsque Pillow convertit RGBA en RGB par défaut, il remplit l'arrière-plan transparent avec du noir. La solution consiste à créer d'abord une image RGB sur fond blanc, puis à utiliser paste() pour superposer l'image d'origine avec le canal alpha comme masque :
background = Image.new("RGB", img.size, (255, 255, 255))
background.paste(img, mask=img.split()[3]) # Le 4e canal est l'alpha
Alternativement, convertissez directement en WebP, qui prend nativement en charge la transparence sans aucun traitement supplémentaire.
Q4 : Pourquoi le multithreading n'offre-t-il pas une accélération linéaire ?
La compression d'images implique à la fois les E/S disque et l'encodage CPU. Une fois que le nombre de threads dépasse un certain point, la bande passante du disque devient le goulot d'étranglement — le CPU ne peut pas aller plus vite que les données ne peuvent être lues et écrites. De plus, les données pixel décodées des images haute résolution sont volumineuses, et le traitement simultané de plusieurs grandes images dans différents threads entraîne une contention de la bande passante mémoire et des taux de réussite de cache réduits. En pratique, 4 à 8 threads offrent généralement une accélération de 2 à 4x, avec des rendements décroissants au-delà. Pour les disques durs mécaniques, 4 threads sont recommandés ; pour les SSD NVMe, le nombre peut être quelque peu augmenté.
Résumé
La compression par lots d'images avec Python Pillow peut être décomposée en trois couches :
- Couche de base : Maîtrisez les trois méthodes
Image.open(),thumbnail()etsave(), combinées aux paramètresqualityetoptimize, et vous pourrez compresser une seule image. C'est la brique de base de tout traitement par lots. - Couche d'ingénierie : Utilisez
os.walk()pour parcourir les répertoires,Path.relative_to()pour préserver la structure etconcurrent.futurespour le traitement parallèle afin de faire passer la logique d'image unique à des milliers d'images. Cette couche se concentre sur l'exactitude, la robustesse et le débit. - Couche d'optimisation : Sélectionnez intelligemment les formats et les paramètres selon les caractéristiques de l'image — les images transparentes vont en WebP, les grandes images sont réduites d'abord, les petites images restent en PNG. Des détails comme la préservation EXIF, la correction d'orientation et la gestion de la mémoire déterminent si le script peut fonctionner de manière fiable dans un environnement de production.
Il existe une règle empirique pour la sélection du facteur de qualité : 85 est le seuil de la qualité visuellement sans perte, 75 est le point d'équilibre entre taille et qualité, et en dessous de 65 n'est pas recommandé pour l'affichage. WebP est désormais pleinement pris en charge par les navigateurs modernes et devrait être le format privilégié pour les nouveaux projets.
La valeur de ce script ne réside pas dans le remplacement des outils professionnels mais dans sa personnalisabilité. Vous pouvez ajuster chaque étape selon vos besoins réels — configurer des paramètres différents pour des répertoires spécifiques, télécharger automatiquement vers un CDN après compression, ou l'intégrer dans un pipeline CI/CD. Comprendre les principes derrière chaque ligne de code vous donne la base pour gérer n'importe quel scénario de traitement d'images.
Lectures associées :
- Guide de compression d'images : comparaison et choix des formats JPG/PNG/WebP
- Why JPEG Quality 75 Is the Magic Number for Image Compression
Besoin de compresser des fichiers ? Essayez SmartSlim
Basé sur un moteur de compression Rust développé en interne, prenant en charge 10 catégories et plus de 40 formats dont PDF, images, vidéo, Office et OFD, avec une compression locale qui conserve vos données sur place.