À quoi ça sert

Pillow (anciennement PIL, Python Imaging Library) est le couteau suisse pour manipuler des fichiers image en Python :

  • Ouvrir et sauvegarder — JPEG, PNG, GIF, WebP, TIFF, BMP, ICO et une vingtaine d'autres formats.
  • Redimensionner / recadrer — opérations basiques avant de passer une image à un modèle ML.
  • Convertir — RGB ↔ niveaux de gris, mode 8-bit ↔ 16-bit, etc.
  • Dessiner — texte, formes, ajouter un watermark ou une bounding box sur une image.
  • Appliquer des filtres — flou, netteté, détection de contours basique (mais pour du vrai traitement d'image, on préfère OpenCV).
Pillow vs OpenCV ?

Pillow = manipulation simple de fichiers image, API conviviale, image stockée comme un objet Image. OpenCV = traitement d'image et vision par ordinateur, plus puissant et plus rapide, mais image stockée comme un array NumPy au format BGR (et pas RGB — piège classique). Règle pratique : si tu ouvres juste un fichier et le redonnes à un modèle, Pillow suffit. Si tu fais du filtrage / détection / transformations géométriques avancées, passe à OpenCV.

Un exemple d'usage

Tu reçois un dossier de photos hétérogènes et tu veux toutes les redimensionner à 512×512 en JPEG, prêtes pour un modèle :

python
from pathlib import Path
from PIL import Image

src = Path("raw")
dst = Path("prepared"); dst.mkdir(exist_ok=True)

for path in src.iterdir():
    img = Image.open(path).convert("RGB")
    img.thumbnail((512, 512))             # garde le ratio
    img.save(dst / f"{path.stem}.jpg", quality=85)

.convert("RGB") est l'étape qui sauve la mise : certaines images peuvent être en RGBA (avec canal alpha) ou en mode palette (GIF) — les convertir en RGB évite des erreurs cryptiques quand on sauve en JPEG (qui ne supporte pas l'alpha) ou qu'on les passe à un modèle qui attend 3 canaux.

How-to : installer et utiliser Pillow

  1. Installer Pillow

    Dépendance principale (cf. UV) — note que la lib s'installe sous le nom Pillow mais s'importe sous le nom PIL (héritage de PIL, l'ancêtre) :

    bash
    uv add pillow
    python
    from PIL import Image, ImageDraw, ImageFilter
  2. Ouvrir et inspecter une image

    python
    img = Image.open("photo.jpg")
    
    img.size           # (width, height) en pixels
    img.mode           # "RGB", "RGBA", "L" (gris), "P" (palette)…
    img.format         # "JPEG", "PNG", …
    img.info           # métadonnées (EXIF, DPI…)

    Important : Image.open est paresseux. Il ne lit pas vraiment les pixels avant qu'on en ait besoin (utile pour parcourir un dossier vite). Pour forcer le chargement : img.load().

  3. Redimensionner, recadrer, faire pivoter

    python
    # Redimensionner à taille exacte (peut déformer)
    img.resize((300, 200))
    
    # Vignette : garde le ratio, modifie l'image en place
    img.thumbnail((512, 512))
    
    # Recadrer (left, top, right, bottom)
    img.crop((50, 50, 450, 450))
    
    # Rotation (en degrés)
    img.rotate(90, expand=True)        # expand=True évite le rognage
    
    # Flip / miroir
    img.transpose(Image.FLIP_LEFT_RIGHT)
  4. Convertir entre formats et modes

    python
    # Modes de pixel
    img.convert("RGB")        # couleur
    img.convert("L")          # niveaux de gris
    img.convert("1")          # noir et blanc 1 bit
    
    # Sauvegarde : le format est déduit de l'extension
    img.save("out.jpg", quality=90)
    img.save("out.png", optimize=True)
    img.save("out.webp", quality=80)
  5. Dessiner sur une image (annotations)

    Particulièrement utile pour visualiser une sortie de modèle (bounding boxes YOLO, par exemple) :

    python
    from PIL import Image, ImageDraw, ImageFont
    
    img = Image.open("photo.jpg")
    draw = ImageDraw.Draw(img)
    
    # Rectangle (left, top, right, bottom)
    draw.rectangle([(50, 50), (200, 150)],
                   outline="red", width=3)
    
    # Texte (police par défaut)
    draw.text((55, 55), "chien (92%)", fill="red")
    
    img.save("annotated.jpg")
  6. Aller-retour avec NumPy

    Indispensable quand on passe l'image à un modèle ML :

    python
    import numpy as np
    from PIL import Image
    
    img = Image.open("photo.jpg").convert("RGB")
    
    # Image → array NumPy (H, W, 3) en uint8
    arr = np.array(img)
    print(arr.shape, arr.dtype)  # (H, W, 3) uint8
    
    # Array NumPy → Image
    img2 = Image.fromarray(arr)
  7. Filtres simples

    python
    from PIL import ImageFilter
    
    img.filter(ImageFilter.BLUR)
    img.filter(ImageFilter.SHARPEN)
    img.filter(ImageFilter.FIND_EDGES)
    img.filter(ImageFilter.GaussianBlur(radius=5))

    Pour du traitement plus poussé (convolutions personnalisées, morphologie, transformations géométriques), c'est le moment de passer à OpenCV.

Aide-mémoire

python (base)
from PIL import Image
img = Image.open("in.jpg").convert("RGB")
img.thumbnail((512, 512))
img.save("out.jpg", quality=85)
python (transforms)
img.resize((w, h))
img.crop((l, t, r, b))
img.rotate(90, expand=True)
img.transpose(Image.FLIP_LEFT_RIGHT)
img.convert("L")
python (dessin)
from PIL import ImageDraw
d = ImageDraw.Draw(img)
d.rectangle([(x1, y1), (x2, y2)], outline="red", width=3)
d.text((x, y), "label", fill="red")

Pillow et le reste de l'écosystème

  • NumPy — pont obligatoire dès qu'on quitte le fichier image vers un calcul. np.array(img) et Image.fromarray(arr) forment le pont aller-retour.
  • OpenCV — la lib qu'on sort dès qu'on a besoin de plus que de la manipulation de fichier. Attention au piège du format BGR d'OpenCV : un cv2.imread() renvoie une image en BGR, pas en RGB.
  • YOLO / PyTorch — la plupart des modèles vision en Python acceptent une image PIL directement en entrée. Pas besoin de convertir d'abord en tensor à la main.
  • matplotlibplt.imshow(np.array(img)) pour afficher rapidement une image PIL dans un notebook Jupyter.
  • FastAPI — pour recevoir une image en upload : FastAPI livre un UploadFile, on l'ouvre avec Image.open(file.file), on le passe à un modèle, on renvoie le résultat.
Le piège EXIF / orientation

Beaucoup de photos prises au smartphone embarquent une orientation dans les métadonnées EXIF (le capteur photo prend en paysage, tu tournes le téléphone). Pillow lit les pixels « dans le sens capteur » et n'applique pas l'orientation par défaut → ton image peut sortir tournée à 90°. Utilise ImageOps.exif_transpose(img) pour appliquer la rotation indiquée par l'EXIF.

Pour aller plus loin