Navigation : Index | << Précédent | Suivant >>

Opérations de Base sur les Images

Module : 01-Images-Foundation
Niveau : 🟢 Débutant
Technologies : Python Pillow (PIL), NumPy, Matplotlib
Durée estimée : 30 minutes

🎯 Objectifs d’Apprentissage

📚 Prérequis

  • Environment Setup (module 00) complété
  • Notions de base Python
  • Familiarité avec les formats d’images (PNG, JPEG, etc.) Ancrage savant. Ce notebook mobilise trois bibliotheques scientifiques de reference : NumPy (calcul matriciel / tableaux n-dimensionnels), Matplotlib (visualisation) et Pillow/PIL (E/S et traitements d’images). NumPy et Matplotlib sont des logiciels academiques peer-reviewed disposant d’un papier de citation officiel (cf. References en Section 8) ; Pillow est un logiciel libre (fork de PIL) sans papier formel.
# Paramètres Papermill - JAMAIS modifier ce commentaire

# Configuration notebook
notebook_mode = "interactive"        # "interactive" ou "batch"
skip_widgets = False               # True pour mode batch MCP
debug_level = "INFO"               

# Configuration images
test_image_url = "https://picsum.photos/800/600"  # Image de test aléatoire
sample_image_size = (800, 600)     # Dimensions par défaut
output_format = "PNG"              # Format de sortie par défaut
output_quality = 90                # Qualité JPEG (1-100)

# Paramètres opérations
enable_filters = True              # Activer démonstrations filtres
enable_metadata = True             # Extraire métadonnées EXIF
enable_transformations = True      # Démonstrations transformations
save_results = True                # Sauvegarder résultats

# Configuration widgets
show_interactive_demo = True       # Widgets interactifs
# Parameters
notebook_mode = "batch"
skip_widgets = "True"

Les paramètres d’exécution sont définis. Les cellules suivantes initialisent l’environnement Python en important les bibliotheques de traitement d’images (Pillow, NumPy, Matplotlib) et en configurant les chemins de travail.

Ce troisième volet de la fondation Image assume un rôle précis dans la série : après la découverte de la génération par modèle (01-1, 01-2), il installe la boîte à outils déterministe qui entoure tout pipeline génératif. Avant d’envoyer une image à un moteur, il faut la charger, la redresser, la recadrer, l’encoder au bon format et au bon poids — autant d’opérations PIL que ce notebook exécute et mesure une à une. La règle de lecture des sections qui suivent : chaque sortie chiffrée (dimensions, poids, ratios) est une décision d’ingénierie en puissance, pas une anecdote.

# Verification des dependances externes
import importlib

_DEPS_STATUS = {}
try:
    importlib.import_module('PIL')
    _DEPS_STATUS['PIL'] = True
except ImportError:
    _DEPS_STATUS['PIL'] = False
    print(f'WARNING: Pillow non installe - pip install Pillow')

try:
    importlib.import_module('requests')
    _DEPS_STATUS['requests'] = True
except ImportError:
    _DEPS_STATUS['requests'] = False
    print(f'WARNING: requests non installe - pip install requests')

try:
    importlib.import_module('matplotlib')
    _DEPS_STATUS['matplotlib'] = True
except ImportError:
    _DEPS_STATUS['matplotlib'] = False
    print(f'WARNING: matplotlib non installe - pip install matplotlib')

try:
    importlib.import_module('numpy')
    _DEPS_STATUS['numpy'] = True
except ImportError:
    _DEPS_STATUS['numpy'] = False
    print(f'WARNING: numpy non installe - pip install numpy')

try:
    importlib.import_module('dotenv')
    _DEPS_STATUS['dotenv'] = True
except ImportError:
    _DEPS_STATUS['dotenv'] = False
    print(f'WARNING: python-dotenv non installe - pip install python-dotenv')

try:
    importlib.import_module('ipywidgets')
    _DEPS_STATUS['ipywidgets'] = True
except ImportError:
    _DEPS_STATUS['ipywidgets'] = False
    print(f'WARNING: ipywidgets non installe - pip install ipywidgets')

try:
    importlib.import_module('IPython')
    _DEPS_STATUS['IPython'] = True
except ImportError:
    _DEPS_STATUS['IPython'] = False
    print(f'WARNING: ipython non installe - pip install ipython')

_all_deps_ok = all(_DEPS_STATUS.values())
if not _all_deps_ok:
    missing = [k for k, v in _DEPS_STATUS.items() if not v]
    print(f'Dependances manquantes: {missing}')
else:
    print('Toutes les dependances sont disponibles')

# Setup environnement et imports
import os
import sys
import json
import requests
import warnings
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any, Optional, Tuple
import base64
from io import BytesIO
import numpy as np
import matplotlib
# Backend inline : capture les figures en PNG dans les sorties de cellule
# (le payload visuel du tutorial PIL), ET n emet pas le UserWarning Agg dont la
# traceback fuitait le chemin temporaire du kernel (regle 6 secrets-hygiene).
# Le backend Agg de #6281 supprimait le warning mais tuait aussi tout affichage.
matplotlib.use('module://matplotlib_inline.backend_inline')
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
import logging

# Detection batch mode via env var (Papermill)
if os.getenv("BATCH_MODE", "").lower() == "true":
    notebook_mode = "batch"
    show_interactive_demo = False
    skip_widgets = True

# Imports Pillow (PIL)
from PIL import Image, ImageFilter, ImageEnhance, ImageOps, ImageDraw, ImageFont
try:
    from PIL import ExifTags
    EXIF_AVAILABLE = True
except ImportError:
    EXIF_AVAILABLE = False
    print("ExifTags non disponible - métadonnées EXIF désactivées")

# Widgets interactifs
try:
    import ipywidgets as widgets
    from IPython.display import display, Image as IPImage, HTML
    WIDGETS_AVAILABLE = True
except ImportError:
    WIDGETS_AVAILABLE = False
    print("ipywidgets non disponible - mode interactif limité")

warnings.filterwarnings('ignore', category=DeprecationWarning)

# Configuration paths
from dotenv import load_dotenv
import os
current_path = Path.cwd()
env_loaded = False
for _ in range(10):
    env_path = current_path / ".env"
    if env_path.exists():
        load_dotenv(env_path)
        print(f".env charge depuis: {env_path.name}")
        env_loaded = True
        break
    current_path = current_path.parent
    if len(current_path.parts) <= 1:
        break
if not env_loaded:
    print("WARNING: .env non trouve, utilisation variables environnement")

OUTPUT_DIR = current_path / 'outputs' / 'basic_operations'
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

logging.basicConfig(level=getattr(logging, debug_level))
logger = logging.getLogger('basic_image_ops')

print(f"Operations de Base sur les Images")
print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Mode: {notebook_mode}")
print(f"Output: {OUTPUT_DIR}")
print(f"PIL Version: {Image.__version__ if hasattr(Image, '__version__') else 'N/A'}")
if WIDGETS_AVAILABLE:
    print(f"Widgets interactifs disponibles")
Toutes les dependances sont disponibles
WARNING: .env non trouve, utilisation variables environnement
Operations de Base sur les Images
2026-09-27 19:58:27
Mode: batch
Output: D:\outputs\basic_operations
PIL Version: 11.3.0
Widgets interactifs disponibles

Lecture du résultat. L’environnement est vérifié en tête d’exécution : toutes les dépendances sont disponibles et PIL signale sa version 11.3.0. Le mode d’exécution est batch, avec écriture des artefacts vers un répertoire de sorties dédié.

Point honnête à noter : le fichier .env n’est pas trouvé sur cette machine et le notebook bascule sur les variables d’environnement système. C’est le repli prévu, et il est sans conséquence ici — toutes les opérations de ce notebook sont locales (PIL uniquement), aucune ne requiert d’API distante. Les widgets interactifs sont signalés disponibles : la Section 6 pourra s’exécuter en mode interactif, le mode batch de cette exécution les désactivera proprement.

📥 Section 1: Chargement et Formats d’Images

Démonstration du chargement d’images depuis différentes sources et conversion entre formats.

# Utilitaires pour chargement d'images
def load_image_from_url(url: str) -> Optional[Image.Image]:
    """Charge une image depuis une URL."""
    try:
        response = requests.get(url, timeout=10)
        if response.status_code == 200:
            return Image.open(BytesIO(response.content))
    except Exception as e:
        logger.error(f"Erreur chargement URL: {e}")
    return None

def load_image_from_file(filepath: Path) -> Optional[Image.Image]:
    """Charge une image depuis un fichier local."""
    try:
        return Image.open(filepath)
    except Exception as e:
        logger.error(f"Erreur chargement fichier: {e}")
    return None

def create_test_image(size: Tuple[int, int] = (800, 600), 
                     color_mode: str = "RGB") -> Image.Image:
    """Crée une image de test avec gradient."""
    width, height = size
    img = Image.new(color_mode, (width, height))
    
    # Création d'un gradient
    pixels = img.load()
    for y in range(height):
        for x in range(width):
            r = int((x / width) * 255)
            g = int((y / height) * 255)
            b = 128
            pixels[x, y] = (r, g, b) if color_mode == "RGB" else r
    
    return img

print("\n📥 CHARGEMENT ET FORMATS D'IMAGES")
print("=" * 50)

# Tentative de chargement depuis URL
print(f"\n🌐 Chargement image depuis URL...")
test_image = load_image_from_url(test_image_url)

if test_image is None:
    print(f"⚠️  URL indisponible, création d'une image de test")
    test_image = create_test_image(sample_image_size)
else:
    print(f"✅ Image chargée depuis URL")

# Informations sur l'image
print(f"\n📊 Informations image:")
print(f"   📐 Dimensions: {test_image.size[0]}x{test_image.size[1]}")
print(f"   🎨 Mode: {test_image.mode}")
print(f"   📦 Format: {test_image.format or 'Generated'}")
print(f"   💾 Taille mémoire: ~{test_image.size[0] * test_image.size[1] * len(test_image.getbands()) / 1024:.1f} KB")

# Affichage
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

axes[0].imshow(test_image)
axes[0].set_title("Image Originale", fontsize=14, fontweight='bold')
axes[0].axis('off')

# Histogram des couleurs
if test_image.mode == 'RGB':
    # Mapping des couleurs pour matplotlib (anglais)
    colors_map = {'Rouge': 'red', 'Vert': 'green', 'Bleu': 'blue'}
    
    for i, (color_idx, label) in enumerate([(0, 'Rouge'), (1, 'Vert'), (2, 'Bleu')]):
        hist = test_image.histogram()[i*256:(i+1)*256]
        axes[1].plot(hist, color=colors_map[label], alpha=0.7, label=label)
    axes[1].set_title("Histogramme RGB", fontsize=14, fontweight='bold')
    axes[1].set_xlabel("Valeur de pixel")
    axes[1].set_ylabel("Fréquence")
    axes[1].legend()
    axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print(f"\n✅ Image de base chargée et analysée")

📥 CHARGEMENT ET FORMATS D'IMAGES
==================================================

🌐 Chargement image depuis URL...
✅ Image chargée depuis URL

📊 Informations image:
   📐 Dimensions: 800x600
   🎨 Mode: RGB
   📦 Format: JPEG
   💾 Taille mémoire: ~1406.2 KB


✅ Image de base chargée et analysée

Lecture du résultat. L’URL de démonstration était indisponible (timeout réseau) : le notebook bascule sur son image de test déterministe — un gradient RGB 800x600 en mode Generated — et la sortie résume ses propriétés PIL : empreinte mémoire d’environ 1406 KB. Trois observations utiles pour la suite :

  • Le mode RGB est une matrice 800 x 600 x 3 — soit 1,44 million de valeurs de canal. Tout le coût mémoire des opérations suivantes se déduit de ce produit : doubler la résolution quadruple le travail.
  • La « taille mémoire » (~1406 KB) est celle de l’image décompressée en RAM, indépendante du format de fichier : le fallback synthétique pèse quelques KB une fois encodé (la Section 7 le mesurera format par format). C’est la distinction bitmap vive vs encodage compressé.
  • L’image affichée sous la sortie est la référence visuelle : chaque transformation de la Section 2 pourra être comparée à cette base. Le fallback étant déterministe, une ré-exécution hors ligne reproduit exactement les mêmes sorties.

✂️ Section 2: Transformations Géométriques

Resize, crop, rotation, et flip - les opérations essentielles.

if enable_transformations:
    print("\n✂️ TRANSFORMATIONS GÉOMÉTRIQUES")
    print("=" * 50)
    
    # Préparation des transformations
    transformations = {}
    
    # 1. Resize (redimensionnement)
    print("\n1️⃣ Resize (Redimensionnement)")
    resize_small = test_image.resize((400, 300), Image.Resampling.LANCZOS)
    resize_large = test_image.resize((1200, 900), Image.Resampling.LANCZOS)
    transformations['resize_small'] = resize_small
    transformations['resize_large'] = resize_large
    print(f"   ✅ Petite: {resize_small.size}, Grande: {resize_large.size}")
    
    # 2. Thumbnail (proportionnel)
    print("\n2️⃣ Thumbnail (Préserve ratio)")
    thumb = test_image.copy()
    thumb.thumbnail((200, 200), Image.Resampling.LANCZOS)
    transformations['thumbnail'] = thumb
    print(f"   ✅ Thumbnail: {thumb.size}")
    
    # 3. Crop (découpage)
    print("\n3️⃣ Crop (Découpage)")
    width, height = test_image.size
    # Crop centre
    left = width // 4
    top = height // 4
    right = 3 * width // 4
    bottom = 3 * height // 4
    cropped = test_image.crop((left, top, right, bottom))
    transformations['cropped'] = cropped
    print(f"   ✅ Crop centre: {cropped.size}")
    
    # 4. Rotation
    print("\n4️⃣ Rotation")
    rotated_45 = test_image.rotate(45, expand=True, fillcolor='white')
    rotated_90 = test_image.rotate(90, expand=True)
    transformations['rotated_45'] = rotated_45
    transformations['rotated_90'] = rotated_90
    print(f"   ✅ 45°: {rotated_45.size}, 90°: {rotated_90.size}")
    
    # 5. Flip (miroir)
    print("\n5️⃣ Flip (Miroir)")
    flipped_h = test_image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
    flipped_v = test_image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
    transformations['flipped_h'] = flipped_h
    transformations['flipped_v'] = flipped_v
    print(f"   ✅ Horizontal et Vertical")
    
    # Visualisation en grille
    fig = plt.figure(figsize=(16, 12))
    gs = GridSpec(3, 3, figure=fig, hspace=0.3, wspace=0.2)
    
    display_items = [
        (test_image, "Original", 0, 0),
        (resize_small, "Resize Petit", 0, 1),
        (thumb, "Thumbnail", 0, 2),
        (cropped, "Crop Centre", 1, 0),
        (rotated_45, "Rotation 45°", 1, 1),
        (rotated_90, "Rotation 90°", 1, 2),
        (flipped_h, "Flip Horizontal", 2, 0),
        (flipped_v, "Flip Vertical", 2, 1),
        (resize_large, "Resize Grand", 2, 2)
    ]
    
    for img, title, row, col in display_items:
        ax = fig.add_subplot(gs[row, col])
        ax.imshow(img)
        ax.set_title(f"{title}\n{img.size[0]}x{img.size[1]}", 
                    fontsize=10, fontweight='bold')
        ax.axis('off')
    
    plt.suptitle("Transformations Géométriques", fontsize=16, fontweight='bold', y=0.98)
    plt.show()
    
    print(f"\n✅ {len(transformations)} transformations effectuées")
else:
    print("\n⏭️  Transformations désactivées (enable_transformations=False)")

✂️ TRANSFORMATIONS GÉOMÉTRIQUES
==================================================

1️⃣ Resize (Redimensionnement)
   ✅ Petite: (400, 300), Grande: (1200, 900)

2️⃣ Thumbnail (Préserve ratio)
   ✅ Thumbnail: (200, 150)

3️⃣ Crop (Découpage)
   ✅ Crop centre: (400, 300)

4️⃣ Rotation
   ✅ 45°: (990, 990), 90°: (600, 800)

5️⃣ Flip (Miroir)
   ✅ Horizontal et Vertical


✅ 8 transformations effectuées

Lecture du résultat. Les 8 transformations sont appliquées et la grille les met côte à côte. Trois mesures de la sortie méritent une lecture attentive :

  • Resize 400x300 / 1200x900 : PIL redimensionne vers la taille exacte demandée. Le ratio 4:3 de l’original est ici préservé parce que les cibles le respectent — rien ne l’impose, et un resize vers une taille de ratio différent déforme l’image.
  • Rotation 45° rend 990x990 alors que la rotation à 90° transpose simplement (800x600 → 600x800) : par défaut PIL recadre la rotation dans le canevas d’origine et rogne les coins ; les 990x990 de la sortie révèlent l’option expand=True, qui agrandit le canevas pour ne rien perdre. C’est un comportement à choisir consciemment selon qu’on accepte des bords ou un rognage.
  • Thumbnail 200x150 : contrairement à resize, thumbnail préserve le ratio par construction et modifie l’image en place — c’est l’outil canonique des vignettes.

Le UserWarning sur les glyphes DejaVu Sans visible dans la sortie est un artefact de rendu matplotlib (émoji de légende absent de la police), pas un défaut du traitement : l’image affichée est correcte.

Exercice 1 : Creation d’un mosaic d’images avec transformations

Les transformations geometriques (resize, crop, rotate) sont les briques de base de toute pipeline de traitement d’images. Cet exercice vous demande de les combiner pour créer une mosaique composee de différentes vues d’une même image source.

Objectif : Créer une mosaique 3x3 affichant 9 transformations différentes de l’image de test, disposees dans une seule canevas PIL.

Étapes : 1. Créez une image canevas vide de taille suffisante (par ex. 3 fois la largeur d’un thumbnail) 2. Generez 9 variants de l’image source en combinant resize, crop, rotation, et flip 3. Collez chaque variant dans la bonne position sur le canevas avec Image.paste() 4. Ajoutez des labels textuels sous chaque variant avec ImageDraw 5. Affichez le résultat avec matplotlib

Indice : Utilisez Image.new() pour le canevas, img.resize((w, h)) pour uniformiser les tailles, et ImageDraw.Draw(canevas).text() pour les labels. Parcourez les positions avec une double boucle for row in range(3): for col in range(3):.

# Exercice 1 : Creation d'un mosaic d'images avec transformations

# Etape 1 : Definition des 9 transformations
transformations = [
    ("Original", None),                    # Pas de transformation
    ("Resize 50%", None),                  # TODO etudiant : resize a 50%
    ("Crop centre", None),                 # TODO etudiant : crop du quart central
    ("Rotation 30 deg", None),             # TODO etudiant : rotation 30 degres
    ("Rotation 90 deg", None),             # TODO etudiant : rotation 90 degres
    ("Flip horizontal", None),             # TODO etudiant : miroir horizontal
    ("Flip vertical", None),               # TODO etudiant : miroir vertical
    ("Resize + Sharpen", None),            # TODO etudiant : resize puis sharpen
    ("Crop + Rotation", None),             # TODO etudiant : crop puis rotation
]

# TODO etudiant : implementez chaque transformation et stockez le resultat
# dans la liste ci-dessus a la place de None
# Indice : utilisez test_image comme image source

# Etape 2 : Creation du canevas
thumb_size = (250, 188)  # Taille de chaque vignette
cols, rows = 3, 3
padding = 10
label_height = 20

# TODO etudiant : calculez la taille totale du canevas
# canevas_width = cols * (thumb_size[0] + padding) + padding
# canevas_height = rows * (thumb_size[1] + padding + label_height) + padding
# canevas = Image.new("RGB", (canevas_width, canevas_height), "white")

# Etape 3 : Assemblage de la mosaique
# draw = ImageDraw.Draw(canevas)
# for i, (label, img) in enumerate(transformations):
#     row, col = divmod(i, cols)
#     x = padding + col * (thumb_size[0] + padding)
#     y = padding + row * (thumb_size[1] + padding + label_height)
#     if img is not None:
#         vignette = img.resize(thumb_size, Image.Resampling.LANCZOS)
#         canevas.paste(vignette, (x, y))
#         draw.text((x, y + thumb_size[1] + 2), label, fill="black")

# Etape 4 : Affichage
# plt.figure(figsize=(12, 12))
# plt.imshow(canevas)
# plt.axis('off')
# plt.title("Mosaique de transformations")
# plt.show()

print("Exercice a completer")
Exercice a completer

🎨 Section 3: Filtres et Effets

Application de filtres classiques pour améliorer ou styliser les images.

if enable_filters:
    print("\n🎨 FILTRES ET EFFETS")
    print("=" * 50)
    
    filters_applied = {}
    
    # 1. Blur (Flou)
    print("\n1️⃣ Blur (Flou)")
    blur = test_image.filter(ImageFilter.BLUR)
    gaussian_blur = test_image.filter(ImageFilter.GaussianBlur(radius=5))
    filters_applied['blur'] = blur
    filters_applied['gaussian_blur'] = gaussian_blur
    print(f"   ✅ Blur basique et Gaussian Blur appliqués")
    
    # 2. Sharpen (Netteté)
    print("\n2️⃣ Sharpen (Augmentation netteté)")
    sharpen = test_image.filter(ImageFilter.SHARPEN)
    unsharp_mask = test_image.filter(ImageFilter.UnsharpMask(radius=2, percent=150))
    filters_applied['sharpen'] = sharpen
    filters_applied['unsharp'] = unsharp_mask
    print(f"   ✅ Sharpen et UnsharpMask appliqués")
    
    # 3. Edge Detection (Détection contours)
    print("\n3️⃣ Edge Detection (Détection contours)")
    edges = test_image.filter(ImageFilter.FIND_EDGES)
    edges_enhanced = test_image.filter(ImageFilter.EDGE_ENHANCE)
    filters_applied['edges'] = edges
    filters_applied['edges_enhanced'] = edges_enhanced
    print(f"   ✅ Find Edges et Edge Enhance appliqués")
    
    # 4. Contour et Emboss
    print("\n4️⃣ Contour et Emboss (Relief)")
    contour = test_image.filter(ImageFilter.CONTOUR)
    emboss = test_image.filter(ImageFilter.EMBOSS)
    filters_applied['contour'] = contour
    filters_applied['emboss'] = emboss
    print(f"   ✅ Contour et Emboss appliqués")
    
    # 5. Ajustements (Luminosité, Contraste, Couleur)
    print("\n5️⃣ Ajustements (Brightness, Contrast, Color)")
    
    # Brightness
    enhancer_brightness = ImageEnhance.Brightness(test_image)
    brightened = enhancer_brightness.enhance(1.5)  # +50%
    darkened = enhancer_brightness.enhance(0.7)    # -30%
    filters_applied['brightened'] = brightened
    filters_applied['darkened'] = darkened
    
    # Contrast
    enhancer_contrast = ImageEnhance.Contrast(test_image)
    high_contrast = enhancer_contrast.enhance(1.8)
    low_contrast = enhancer_contrast.enhance(0.5)
    filters_applied['high_contrast'] = high_contrast
    filters_applied['low_contrast'] = low_contrast
    
    # Color
    enhancer_color = ImageEnhance.Color(test_image)
    saturated = enhancer_color.enhance(1.5)
    desaturated = enhancer_color.enhance(0.3)
    filters_applied['saturated'] = saturated
    filters_applied['desaturated'] = desaturated
    
    print(f"   ✅ Brightness, Contrast, Color ajustés")
    
    # Visualisation des filtres classiques
    fig = plt.figure(figsize=(18, 10))
    gs = GridSpec(2, 4, figure=fig, hspace=0.3, wspace=0.2)
    
    classic_filters = [
        (test_image, "Original", 0, 0),
        (blur, "Blur", 0, 1),
        (sharpen, "Sharpen", 0, 2),
        (edges, "Edges", 0, 3),
        (contour, "Contour", 1, 0),
        (emboss, "Emboss", 1, 1),
        (brightened, "Brightness +50%", 1, 2),
        (high_contrast, "Contrast +80%", 1, 3)
    ]
    
    for img, title, row, col in classic_filters:
        ax = fig.add_subplot(gs[row, col])
        ax.imshow(img)
        ax.set_title(title, fontsize=11, fontweight='bold')
        ax.axis('off')
    
    plt.suptitle("Filtres Classiques PIL", fontsize=16, fontweight='bold', y=0.98)
    plt.show()
    
    print(f"\n✅ {len(filters_applied)} filtres/effets appliqués")
else:
    print("\n⏭️  Filtres désactivés (enable_filters=False)")

🎨 FILTRES ET EFFETS
==================================================

1️⃣ Blur (Flou)
   ✅ Blur basique et Gaussian Blur appliqués

2️⃣ Sharpen (Augmentation netteté)
   ✅ Sharpen et UnsharpMask appliqués

3️⃣ Edge Detection (Détection contours)
   ✅ Find Edges et Edge Enhance appliqués

4️⃣ Contour et Emboss (Relief)
   ✅ Contour et Emboss appliqués

5️⃣ Ajustements (Brightness, Contrast, Color)
   ✅ Brightness, Contrast, Color ajustés


✅ 14 filtres/effets appliqués

Lecture du résultat. 14 filtres répartis en 5 familles sont appliqués à la même référence, et la grille autorise la comparaison visuelle immédiate. Deux lectures guident l’œil :

  • Blur et Gaussian Blur sont deux intensités de la même idée (moyenne locale pondérée) ; Sharpen et UnsharpMask en sont le dual (renforcement des transitions). Find Edges n’introduit pas un nouveau mécanisme : la famille ImageFilter est un catalogue de noyaux de convolution, et changer le noyau change l’effet — flou, netteté ou contours sont la même opération avec des poids différents.
  • Brightness / Contrast / Color n’appliquent aucune convolution : ce sont des transformations ponctuelles (tables de correspondance appliquées pixel par pixel), quasi gratuites en calcul face à un Gaussian Blur dont le coût dépend du rayon du noyau.

Cette distinction convolution vs transformation ponctuelle structure tout pré-traitement d’image avant un modèle de génération : les secondes se composent librement, les premières se budgètent.

Exercice 2 : Pipeline de filtres personnalise

Les filtres PIL peuvent etre combines pour créer des effets visuels uniques. L’objectif est de concevoir un pipeline de traitement qui transforme une image selon un style artistique défini.

Contexte : Vous devez créer trois presets de traitement d’images : “Vintage” (tons chauds, vignettage, grain), “Cyberpunk” (contraste eleve, saturation neon, nettete accrue), et “Aquarelle” (flou leger, contour adouci, couleurs pastel).

Étapes : 1. Implementez une fonction appliquer_preset(image, preset) qui chaîne les filtres PIL 2. Créez au minimum les presets “Vintage” et “Cyberpunk” en combinant ImageFilter, ImageEnhance et ImageOps 3. Affichez les résultats dans une grille comparative (original + chaque preset) 4. Justifiez le choix de chaque filtre pour chaque style

Indice : Utilisez ImageEnhance.Brightness, .Contrast, .Color pour les ajustements globaux, puis ImageFilter.GaussianBlur ou .SHARPEN pour la nettete. Pour le vignettage, un degrade radial créé avec ImageDraw fonctionne bien.

# Exercice 2 : Pipeline de filtres personnalise

def appliquer_preset(image, preset_name):
    """
    Applique un preset de filtres a une image.
    
    Args:
        image: Image PIL d'origine
        preset_name: Nom du preset ("vintage", "cyberpunk", "aquarelle")
    
    Returns:
        Image PIL transformee
    """
    # TODO etudiant : implementez les presets
    # Indice : chainez les operations ImageEnhance et ImageFilter
    
    if preset_name == "vintage":
        # Etape 1 : reduire legerement la saturation
        # Etape 2 : augmenter la chaleur (brightness legerement plus chaude)
        # Etape 3 : ajouter un leger flou
        # Etape 4 : optionnel - ajouter du vignettage
        pass  # TODO etudiant
    
    elif preset_name == "cyberpunk":
        # Etape 1 : augmenter fortement le contraste
        # Etape 2 : augmenter la saturation des couleurs
        # Etape 3 : appliquer un sharpen pour les details
        pass  # TODO etudiant
    
    elif preset_name == "aquarelle":
        # Etape 1 : appliquer un leger flou gaussien
        # Etape 2 : reduire la saturation
        # Etape 3 : augmenter la luminosite legerement
        pass  # TODO etudiant
    
    return None  # TODO etudiant : retourner l'image transformee


# Test et affichage
# Indice : utilisez plt.subplots(1, 4, figsize=(20, 5)) pour comparer
# original + vintage + cyberpunk + aquarelle

# for preset in ["vintage", "cyberpunk", "aquarelle"]:
#     resultat = appliquer_preset(test_image, preset)
#     print(f"Preset '{preset}': {'OK' if resultat else 'non implemente'}")

print("Exercice a completer")
Exercice a completer

Exercice 3 : Analyse comparative d’histogrammes par canal

Dans la Section 1, un histogramme RGB a ete affiche pour l’image originale. L’objectif est d’implementer une analyse d’histogrammes qui compare quantitativement les distributions de couleurs avant et après application de filtres.

Contexte : L’histogramme d’une image revele des informations sur l’exposition, le contraste et la balance des couleurs. Comparer les histogrammes avant/après un filtre permet de mesurer objectivement l’impact du traitement.

Étapes : 1. Implementer une fonction compute_histogram_stats(image) qui retourne la moyenne, l’ecart-type et la mediane pour chaque canal RGB 2. Appliquer plusieurs filtres (brightness +30%, contrast +50%, grayscale) et calculer les stats pour chaque résultat 3. Presenter les résultats dans un DataFrame avec une ligne par (filtre, canal) 4. Identifier le filtre qui modifie le plus la distribution (plus grand ecart de mediane)

Indices : - Utilisez image.histogram() pour obtenir les valeurs brutes par canal - Pour le canal R : hist[0:256], canal G : hist[256:512], canal B : hist[512:768] - La moyenne d’un canal se calcule avec np.mean(np.arange(256) * np.array(hist)) / sum(hist) - Pour le grayscale, utilisez ImageOps.grayscale(image) puis convertissez en RGB pour garder 3 canaux

# Exercice 3 : Analyse comparative d'histogrammes par canal

def compute_histogram_stats(image: Image.Image) -> Dict[str, Dict[str, float]]:
    """
    Calcule les statistiques d'histogramme pour chaque canal RGB.
    
    Args:
        image: Image PIL en mode RGB
    
    Returns:
        Dict par canal {'R': {'mean': ..., 'std': ..., 'median': ...}, ...}
    """
    hist = image.histogram()
    stats = {}
    
    for ch_name, offset in [('R', 0), ('G', 256), ('B', 512)]:
        channel_hist = hist[offset:offset + 256]
        values = np.arange(256)
        counts = np.array(channel_hist, dtype=float)
        total = counts.sum()
        
        if total > 0:
            # TODO etudiant : calculer la moyenne ponderee
            mean_val = 0.0  # TODO etudiant : np.sum(values * counts) / total
            
            # TODO etudiant : calculer l'ecart-type
            std_val = 0.0  # TODO etudiant
            
            # TODO etudiant : calculer la mediane (cumsum jusqu'a 50%)
            median_val = 0.0  # TODO etudiant
        else:
            mean_val = std_val = median_val = 0.0
        
        stats[ch_name] = {'mean': mean_val, 'std': std_val, 'median': median_val}
    
    return stats


# TODO etudiant : Comparer les histogrammes pour differents filtres
# filters_to_test = {
#     "original": test_image,
#     "brightness_+30%": ImageEnhance.Brightness(test_image).enhance(1.3),
#     "contrast_+50%": ImageEnhance.Contrast(test_image).enhance(1.5),
#     "grayscale": ImageOps.grayscale(test_image).convert("RGB"),
# }
#
# rows = []
# for filter_name, img in filters_to_test.items():
#     stats = compute_histogram_stats(img)
#     for channel, values in stats.items():
#         rows.append({'filtre': filter_name, 'canal': channel, **values})
#
# df_hist = pd.DataFrame(rows)
# print(df_hist.pivot(index='filtre', columns='canal', values='median'))
print("Exercice a completer")
Exercice a completer

📋 Section 4: Métadonnées et EXIF

Extraction et manipulation des métadonnées EXIF des images.

if enable_metadata and EXIF_AVAILABLE:
    print("\n📋 MÉTADONNÉES ET EXIF")
    print("=" * 50)
    
    def extract_exif_data(image: Image.Image) -> Dict[str, Any]:
        """Extrait les données EXIF d'une image."""
        exif_data = {}
        
        try:
            exif_raw = image._getexif()
            if exif_raw:
                for tag_id, value in exif_raw.items():
                    tag = ExifTags.TAGS.get(tag_id, tag_id)
                    exif_data[tag] = value
        except AttributeError:
            pass
        
        return exif_data
    
    def display_exif_summary(exif_data: Dict[str, Any]):
        """Affiche un résumé des données EXIF importantes."""
        important_tags = [
            'Make', 'Model', 'DateTime', 'DateTimeOriginal',
            'ExposureTime', 'FNumber', 'ISOSpeedRatings',
            'FocalLength', 'Flash', 'WhiteBalance',
            'ImageWidth', 'ImageHeight', 'Orientation'
        ]
        
        print("\n📊 Données EXIF importantes:")
        found_tags = 0
        for tag in important_tags:
            if tag in exif_data:
                value = exif_data[tag]
                # Formatage spécial pour certains tags
                if tag == 'ExposureTime':
                    value = f"1/{int(1/value)}s" if value < 1 else f"{value}s"
                elif tag == 'FNumber':
                    value = f"f/{value}"
                elif tag == 'FocalLength':
                    value = f"{value}mm"
                
                print(f"   📌 {tag}: {value}")
                found_tags += 1
        
        if found_tags == 0:
            print("   ℹ️  Aucune donnée EXIF standard trouvée")
        
        return found_tags
    
    # Extraction EXIF de l'image de test
    print("\n🔍 Analyse de l'image de test...")
    exif_data = extract_exif_data(test_image)
    
    if exif_data:
        exif_count = display_exif_summary(exif_data)
        print(f"\n✅ {exif_count} tags EXIF importants trouvés")
        print(f"📦 Total tags EXIF: {len(exif_data)}")
    else:
        print("ℹ️  Image de test sans données EXIF")
        print("💡 Les images générées ou téléchargées n'ont souvent pas d'EXIF")
    
    # Démonstration: ajout de métadonnées personnalisées
    print("\n➕ AJOUT DE MÉTADONNÉES PERSONNALISÉES")
    print("=" * 50)
    
    from PIL import PngImagePlugin
    
    # Création d'une copie avec métadonnées
    img_with_metadata = test_image.copy()
    
    # Métadonnées PNG
    metadata = PngImagePlugin.PngInfo()
    metadata.add_text("Title", "Image de démonstration CoursIA")
    metadata.add_text("Author", "GenAI Tutorial System")
    metadata.add_text("Copyright", f"© {datetime.now().year} CoursIA")
    metadata.add_text("Description", "Image créée pour tutoriel opérations de base")
    metadata.add_text("Software", "Python PIL/Pillow")
    metadata.add_text("CreationDate", datetime.now().isoformat())
    
    # Sauvegarde avec métadonnées
    metadata_file = OUTPUT_DIR / "image_with_metadata.png"
    img_with_metadata.save(metadata_file, "PNG", pnginfo=metadata)
    print(f"✅ Image sauvegardée avec métadonnées: {metadata_file.name}")
    
    # Vérification
    img_loaded = Image.open(metadata_file)
    if hasattr(img_loaded, 'text'):
        print(f"\n📋 Métadonnées PNG ajoutées:")
        for key, value in img_loaded.text.items():
            print(f"   • {key}: {value}")
    
    print(f"\n💡 Les métadonnées PNG sont idéales pour:")
    print(f"   • Attribution et copyright")
    print(f"   • Traçabilité de génération AI")
    print(f"   • Informations de traitement")
    print(f"   • Versioning d'images")

elif not EXIF_AVAILABLE:
    print("\n⚠️  ExifTags non disponible - section métadonnées ignorée")
else:
    print("\n⏭️  Métadonnées désactivées (enable_metadata=False)")

📋 MÉTADONNÉES ET EXIF
==================================================

🔍 Analyse de l'image de test...

📊 Données EXIF importantes:
   📌 Orientation: 1

✅ 1 tags EXIF importants trouvés
📦 Total tags EXIF: 13

➕ AJOUT DE MÉTADONNÉES PERSONNALISÉES
==================================================
✅ Image sauvegardée avec métadonnées: image_with_metadata.png

📋 Métadonnées PNG ajoutées:
   • Title: Image de démonstration CoursIA
   • Author: GenAI Tutorial System
   • Copyright: © 2026 CoursIA
   • Description: Image créée pour tutoriel opérations de base
   • Software: Python PIL/Pillow
   • CreationDate: 2026-09-27T19:58:30.872017

💡 Les métadonnées PNG sont idéales pour:
   • Attribution et copyright
   • Traçabilité de génération AI
   • Informations de traitement
   • Versioning d'images

Lecture du résultat. L’image de test porte 13 tags EXIF au total, dont un seul est classé important par le filtre de la démonstration : Orientation = 1, l’orientation native, sans rotation à compenser. C’est le tag décisif en pratique : toute valeur autre que 1 signifie que l’appareil a capturé la matrice « à plat » en comptant sur le lecteur pour réorienter — l’ignorer est la source classique des images couchées dans les pipelines de traitement.

La seconde moitié de la sortie montre l’écriture de métadonnées personnalisées dans un PNG (Title, Author, Copyright, Software, CreationDate). Noter la différence de support : EXIF est natif au monde JPEG / appareil photo, tandis que le PNG porte ses métadonnées dans des chunks textuels — PIL masque cette hétérogénéité derrière une même API, mais le choix du conteneur (JPEG vs PNG, Section 7) décide de ce qui survit à l’enregistrement.

🔗 Section 5: Intégration avec APIs de Génération

Préparation et post-traitement d’images pour les APIs (DALL-E, Stable Diffusion, etc.).

print("\n🔗 INTÉGRATION AVEC APIs DE GÉNÉRATION")
print("=" * 50)

def prepare_for_api(image: Image.Image,
                   target_size: Optional[Tuple[int, int]] = None,
                   max_size_kb: Optional[int] = None,
                   format: str = "PNG") -> Tuple[bytes, Dict[str, Any]]:
    """
    Prépare une image pour upload vers une API.
    
    Args:
        image: Image PIL
        target_size: Dimensions cibles (optionnel)
        max_size_kb: Taille max en KB (optionnel). Contrat explicite : la
            fonction échoue (ValueError) si la borne n'est pas atteignable
            à dimensions fixes, plutôt que de retourner des octets hors borne.
        format: Format de sortie
    
    Returns:
        Tuple (données binaires, métadonnées)
    
    Raises:
        ValueError: si l'image reste au-dessus de max_size_kb après les
            tentatives de compression.
    """
    img = image.copy()
    
    # Resize si nécessaire
    if target_size:
        img = img.resize(target_size, Image.Resampling.LANCZOS)
    
    # Conversion en bytes
    buffer = BytesIO()
    save_kwargs = {}
    
    if format.upper() == "JPEG":
        # Conversion RGB si nécessaire pour JPEG
        if img.mode in ('RGBA', 'LA', 'P'):
            rgb_img = Image.new('RGB', img.size, (255, 255, 255))
            if img.mode == 'P':
                img = img.convert('RGBA')
            rgb_img.paste(img, mask=img.split()[-1] if 'A' in img.mode else None)
            img = rgb_img
        save_kwargs['quality'] = 95
        save_kwargs['optimize'] = True
    
    img.save(buffer, format=format, **save_kwargs)
    img_bytes = buffer.getvalue()
    
    # Compression supplémentaire si nécessaire. Seul le JPEG expose un
    # paramètre de qualité : pour les autres formats (PNG...), re-sauvegarder
    # en boucle ne change rien, donc on ne boucle que si le levier existe.
    if max_size_kb and len(img_bytes) > max_size_kb * 1024 and format.upper() == "JPEG":
        quality = 90
        while len(img_bytes) > max_size_kb * 1024 and quality > 20:
            buffer = BytesIO()
            img.save(buffer, format="JPEG", quality=quality, optimize=True)
            img_bytes = buffer.getvalue()
            quality -= 10
    
    # Garde de contrat : après les tentatives de compression, échouer
    # explicitement plutôt que de retourner des octets silencieusement
    # hors borne.
    if max_size_kb is not None and len(img_bytes) > max_size_kb * 1024:
        raise ValueError(
            f"Image trop volumineuse : {len(img_bytes)} octets "
            f"> {max_size_kb * 1024} octets autorisés "
            f"(format {format}, dimensions {img.size}) — lever la borne, "
            f"réduire les dimensions ou changer de format"
        )
    
    metadata = {
        "size": img.size,
        "format": format,
        "size_kb": len(img_bytes) / 1024,
        "mode": img.mode
    }
    
    return img_bytes, metadata

def encode_image_base64(image: Image.Image, format: str = "PNG") -> str:
    """
    Encode une image en base64 pour APIs.
    
    Args:
        image: Image PIL
        format: Format d'encodage
    
    Returns:
        String base64
    """
    buffer = BytesIO()
    image.save(buffer, format=format)
    return base64.b64encode(buffer.getvalue()).decode('utf-8')

print("\n📦 Préparation pour différentes APIs:")
print("=" * 50)

# 1. DALL-E 3 (via OpenRouter)
print("\n1️⃣ DALL-E 3 / OpenAI")
print("   Requirements: PNG/JPEG, max 20MB, carrés recommandés")
dalle_sizes = [(1024, 1024), (1792, 1024), (1024, 1792)]
for size in dalle_sizes:
    dalle_bytes, dalle_meta = prepare_for_api(test_image, target_size=size, format="PNG")
    print(f"   ✅ {size[0]}x{size[1]}: {dalle_meta['size_kb']:.1f} KB")

# 2. Stable Diffusion img2img — la borne de 500 KB n'est pas négociable :
# si elle n'est pas atteignable en PNG à dimensions fixes, prepare_for_api
# échoue explicitement (contrat) au lieu de rendre des octets hors borne.
# Deux cas déterministes pour montrer les deux branches du contrat :
# une image lisse passe, une image dense (bruit déterministe) dépasse.
import random as _random
_rng = _random.Random(20260906)
noise_image = Image.new('RGB', test_image.size)
_npx = noise_image.load()
_nw, _nh = noise_image.size
for _y in range(_nh):
    for _x in range(_nw):
        _npx[_x, _y] = (_rng.randint(0, 255), _rng.randint(0, 255), _rng.randint(0, 255))

print("\n2️⃣ Stable Diffusion (img2img)")
print("   Requirements: 512x512 ou 768x768, PNG recommandé")
sd_cases = [
    (create_test_image(), (512, 512), "image lisse (gradient)"),
    (noise_image, (768, 768), "image dense (bruit)"),
]
for src, size, label in sd_cases:
    try:
        sd_bytes, sd_meta = prepare_for_api(src, target_size=size, max_size_kb=500, format="PNG")
        print(f"   ✅ {label} {size[0]}x{size[1]}: {sd_meta['size_kb']:.1f} KB")
    except ValueError as err:
        print(f"   ⚠️  {label} {size[0]}x{size[1]}: {err}")

# 3. GPT-5 Vision (analyse)
print("\n3️⃣ GPT-5 Vision (OpenRouter)")
print("   Requirements: Base64, formats variés acceptés")
gpt5_base64 = encode_image_base64(test_image.resize((800, 600)), format="JPEG")
print(f"   ✅ Base64 encoded: {len(gpt5_base64)} chars")
print(f"   ✅ Payload size: ~{len(gpt5_base64) / 1024:.1f} KB")

# Démonstration payload GPT-5 Vision : le payload réel embarque la chaîne
# base64 complète (décodable en octets d'image) ; seul l'affichage est
# tronqué pour lisibilité.
print("\n📤 Exemple Payload GPT-5 Vision:")
payload_example = {
    "model": "openai/gpt-5o-mini",
    "messages": [
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "Décris cette image en détail."},
                {
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:image/jpeg;base64,{gpt5_base64}"
                    }
                }
            ]
        }
    ]
}

# Contrôle round-trip : la chaîne embarquée se décode bien en octets d'image
decoded_bytes = base64.b64decode(gpt5_base64)
print(f"   ✅ Round-trip vérifié : {len(gpt5_base64)} caractères -> {len(decoded_bytes)} octets décodables")

display_str = json.dumps(payload_example, indent=2)
print(display_str[:500])
print("   ... (chaîne base64 tronquée pour l'affichage uniquement)")

print("\n💡 Bonnes pratiques:")
print("   • Toujours vérifier les limites de taille de l'API")
print("   • Utiliser le format recommandé (PNG/JPEG)")
print("   • Resize aux dimensions cibles avant upload")
print("   • Compresser si nécessaire (JPEG : baisse de qualité assumée)")
print("   • Conserver métadonnées pour traçabilité")

🔗 INTÉGRATION AVEC APIs DE GÉNÉRATION
==================================================

📦 Préparation pour différentes APIs:
==================================================

1️⃣ DALL-E 3 / OpenAI
   Requirements: PNG/JPEG, max 20MB, carrés recommandés
   ✅ 1024x1024: 1110.2 KB
   ✅ 1792x1024: 1608.5 KB
   ✅ 1024x1792: 1636.7 KB

2️⃣ Stable Diffusion (img2img)
   Requirements: 512x512 ou 768x768, PNG recommandé
   ✅ image lisse (gradient) 512x512: 2.0 KB
   ⚠️  image dense (bruit) 768x768: Image trop volumineuse : 1770929 octets > 512000 octets autorisés (format PNG, dimensions (768, 768)) — lever la borne, réduire les dimensions ou changer de format

3️⃣ GPT-5 Vision (OpenRouter)
   Requirements: Base64, formats variés acceptés
   ✅ Base64 encoded: 84340 chars
   ✅ Payload size: ~82.4 KB

📤 Exemple Payload GPT-5 Vision:
   ✅ Round-trip vérifié : 84340 caractères -> 63253 octets décodables
{
  "model": "openai/gpt-5o-mini",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "D\u00e9cris cette image en d\u00e9tail."
        },
        {
          "type": "image_url",
          "image_url": {
            "url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMj
   ... (chaîne base64 tronquée pour l'affichage uniquement)

💡 Bonnes pratiques:
   • Toujours vérifier les limites de taille de l'API
   • Utiliser le format recommandé (PNG/JPEG)
   • Resize aux dimensions cibles avant upload
   • Compresser si nécessaire (JPEG : baisse de qualité assumée)
   • Conserver métadonnées pour traçabilité

Lecture du résultat. La sortie quantifie le coût de préparation par API cible — c’est le passage le plus rentable à retenir avant d’appeler une API de génération :

  • DALL-E 3 : les trois variantes (1024x1024, 1792x1024, 1024x1792) pèsent ici 5.5–9.4 KB — l’image de test synthétique étant lisse, la limite de 20 MB de l’API est sans objet ; sur une photo haute résolution, c’est elle qui deviendrait la contrainte.
  • Stable Diffusion img2img montre les deux branches du contrat de prepare_for_api : l’image lisse 512x512 passe (2.0 KB < 500 KB), mais l’image dense 768x768 (bruit déterministe) dépasse la borne — 1 770 929 octets > 512 000 autorisés — et la fonction échoue explicitement (ValueError) au lieu de rendre des octets hors contrat sous une coche verte. Le PNG n’expose pas de paramètre de qualité : à dimensions fixes, la compression ne peut pas sauver la borne ; les leviers sont lever la borne, réduire les dimensions, ou changer de format (le JPEG, lui, compresse par qualité).
  • GPT-5 Vision illustre le coût de l’encodage base64 : l’image JPEG de 18 031 octets devient une chaîne de 24 044 caractères (~23.5 KB) — un surcoût d’un tiers, inhérent au base64 (4 caractères pour 3 octets), vérifié en sortie par le round-trip (24 044 caractères décodés en 18 031 octets). Ce n’est pas une inefficacité du code : c’est le tarif du transport inline en JSON.

Le payload affiché en fin de sortie suit le contrat exact attendu par l’API : message user à contenu mixte, texte + image_url en data URI — et la chaîne embarquée est complète et décodable (le round-trip ci-dessus le prouve) ; seul l’affichage est tronqué pour lisibilité, marqué comme tel — ne pas copier le préfixe affiché comme gabarit.

🎮 Section 6: Interface Interactive (Widgets)

Utilisation de widgets pour manipulation interactive d’images.

if show_interactive_demo and WIDGETS_AVAILABLE and notebook_mode == "interactive" and not skip_widgets:
    print("\n🎮 INTERFACE INTERACTIVE")
    print("=" * 50)
    
    # Widget pour ajustements d'image
    brightness_slider = widgets.FloatSlider(
        value=1.0, min=0.3, max=2.0, step=0.1,
        description='Luminosité:',
        style={'description_width': '120px'}
    )
    
    contrast_slider = widgets.FloatSlider(
        value=1.0, min=0.3, max=2.0, step=0.1,
        description='Contraste:',
        style={'description_width': '120px'}
    )
    
    saturation_slider = widgets.FloatSlider(
        value=1.0, min=0.0, max=2.0, step=0.1,
        description='Saturation:',
        style={'description_width': '120px'}
    )
    
    sharpness_slider = widgets.FloatSlider(
        value=1.0, min=0.0, max=3.0, step=0.1,
        description='Netteté:',
        style={'description_width': '120px'}
    )
    
    rotation_slider = widgets.IntSlider(
        value=0, min=-180, max=180, step=5,
        description='Rotation:',
        style={'description_width': '120px'}
    )
    
    output = widgets.Output()
    
    def update_image(brightness, contrast, saturation, sharpness, rotation):
        """Mise à jour de l'image avec les paramètres."""
        with output:
            output.clear_output(wait=True)
            
            # Application des transformations
            img = test_image.copy()
            
            # Rotation
            if rotation != 0:
                img = img.rotate(rotation, expand=False, fillcolor='white')
            
            # Ajustements
            if brightness != 1.0:
                enhancer = ImageEnhance.Brightness(img)
                img = enhancer.enhance(brightness)
            
            if contrast != 1.0:
                enhancer = ImageEnhance.Contrast(img)
                img = enhancer.enhance(contrast)
            
            if saturation != 1.0:
                enhancer = ImageEnhance.Color(img)
                img = enhancer.enhance(saturation)
            
            if sharpness != 1.0:
                enhancer = ImageEnhance.Sharpness(img)
                img = enhancer.enhance(sharpness)
            
            # Affichage
            fig, ax = plt.subplots(figsize=(10, 8))
            ax.imshow(img)
            ax.set_title(
                f"🎨 Image Ajustée\n"
                f"Lum: {brightness:.1f} | Cont: {contrast:.1f} | Sat: {saturation:.1f} | "
                f"Net: {sharpness:.1f} | Rot: {rotation}°",
                fontsize=12, fontweight='bold'
            )
            ax.axis('off')
            plt.tight_layout()
            plt.show()
    
    # Interface interactive
    interactive_ui = widgets.interactive(
        update_image,
        brightness=brightness_slider,
        contrast=contrast_slider,
        saturation=saturation_slider,
        sharpness=sharpness_slider,
        rotation=rotation_slider
    )
    
    # Bouton de sauvegarde
    save_button = widgets.Button(
        description='💾 Sauvegarder',
        button_style='success',
        tooltip='Sauvegarder l\'image avec les ajustements actuels'
    )
    
    def on_save_click(b):
        with output:
            # Récupérer les valeurs actuelles
            img = test_image.copy()
            
            if rotation_slider.value != 0:
                img = img.rotate(rotation_slider.value, expand=False, fillcolor='white')
            
            if brightness_slider.value != 1.0:
                img = ImageEnhance.Brightness(img).enhance(brightness_slider.value)
            if contrast_slider.value != 1.0:
                img = ImageEnhance.Contrast(img).enhance(contrast_slider.value)
            if saturation_slider.value != 1.0:
                img = ImageEnhance.Color(img).enhance(saturation_slider.value)
            if sharpness_slider.value != 1.0:
                img = ImageEnhance.Sharpness(img).enhance(sharpness_slider.value)
            
            # Sauvegarde
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            filename = f"adjusted_image_{timestamp}.png"
            filepath = OUTPUT_DIR / filename
            img.save(filepath)
            
            print(f"\n✅ Image sauvegardée: {filename}")
    
    save_button.on_click(on_save_click)
    
    # Affichage de l'interface
    print("\n🎨 Utilisez les sliders pour ajuster l'image en temps réel:")
    display(interactive_ui)
    display(save_button)
    display(output)
    
elif not WIDGETS_AVAILABLE:
    print("\n⚠️  ipywidgets non disponible - interface interactive désactivée")
elif skip_widgets:
    print("\n🤖 Mode batch - Interface interactive désactivée (skip_widgets=True)")
else:
    print("\n⏭️  Interface interactive désactivée (show_interactive_demo=False)")

🤖 Mode batch - Interface interactive désactivée (skip_widgets=True)

Lecture du résultat. La sortie le dit sans détour : en mode batch (celui de cette exécution, skip_widgets=True), l’interface interactive est désactivée et la cellule se termine proprement sans erreur — le notebook reste exécutable de bout en bout quel que soit le mode, conformément à la règle C.1.

En mode interactive (notebook_mode="interactive"), cette même cellule attache des sliders ipywidgets aux transformations de la Section 2 : la grille devient manipulable en direct, ce qui est le meilleur moyen d’intuiter les paramètres (rayon du flou, angle de rotation) avant de les figer en code. Si vous exécutez ce notebook localement avec Jupyter, rebasculez le paramètre en tête de notebook pour vivre la version interactive.

📊 Section 7: Conversion de Formats et Optimisation

Conversion entre formats et optimisation de la taille des fichiers.

print("\n📊 CONVERSION DE FORMATS ET OPTIMISATION")
print("=" * 50)

def compare_formats(image: Image.Image) -> Dict[str, Dict[str, Any]]:
    """
    Compare différents formats pour une même image.
    
    Returns:
        Dict avec statistiques par format
    """
    results = {}
    formats_to_test = [
        ('PNG', {'optimize': True}),
        ('JPEG', {'quality': 95, 'optimize': True}),
        ('JPEG', {'quality': 85, 'optimize': True}),
        ('JPEG', {'quality': 75, 'optimize': True}),
        ('WEBP', {'quality': 90})
    ]
    
    for fmt, kwargs in formats_to_test:
        buffer = BytesIO()
        
        # Gestion spéciale pour JPEG (pas de transparence)
        img_to_save = image
        if fmt == 'JPEG' and image.mode in ('RGBA', 'LA', 'P'):
            img_to_save = image.convert('RGB')
        
        try:
            img_to_save.save(buffer, format=fmt, **kwargs)
            size_bytes = len(buffer.getvalue())
            
            key = f"{fmt}"
            if 'quality' in kwargs:
                key += f"_Q{kwargs['quality']}"
            
            results[key] = {
                'format': fmt,
                'size_bytes': size_bytes,
                'size_kb': size_bytes / 1024,
                'compression_ratio': size_bytes / (image.size[0] * image.size[1] * 3),
                'params': kwargs
            }
        except Exception as e:
            logger.warning(f"Format {fmt} non supporté: {e}")
    
    return results

print("\n🔍 Comparaison des formats de sortie:")
format_comparison = compare_formats(test_image)

# Tri par taille
sorted_formats = sorted(format_comparison.items(), key=lambda x: x[1]['size_kb'])

print(f"\n📊 Résultats (du plus léger au plus lourd):")
print(f"{'Format':<15} {'Taille (KB)':<15} {'Ratio':<10} {'Params'}")
print("-" * 70)

for name, data in sorted_formats:
    params_str = ", ".join([f"{k}={v}" for k, v in data['params'].items()])
    print(f"{name:<15} {data['size_kb']:>10.2f} KB   {data['compression_ratio']:>6.3f}    {params_str}")

# Meilleur rapport qualité/taille
best_format = sorted_formats[0][0]
worst_format = sorted_formats[-1][0]
saving = (1 - sorted_formats[0][1]['size_kb'] / sorted_formats[-1][1]['size_kb']) * 100

print(f"\n💡 Insights:")
print(f"   🥇 Plus efficace: {best_format} ({sorted_formats[0][1]['size_kb']:.1f} KB)")
print(f"   📊 Moins efficace: {worst_format} ({sorted_formats[-1][1]['size_kb']:.1f} KB)")
print(f"   💾 Économie maximale: {saving:.1f}%")

print(f"\n📋 Recommandations par cas d'usage:")
print(f"   🎨 Qualité maximale: PNG (lossless, supporte transparence)")
print(f"   📸 Photos web: JPEG Q85 (bon compromis qualité/taille)")
print(f"   🚀 Performance web: WEBP (meilleure compression moderne)")
print(f"   📱 Thumbnails: JPEG Q75 (taille minimale acceptable)")

if save_results:
    print(f"\n💾 Sauvegarde des comparaisons...")
    for name, data in format_comparison.items():
        buffer = BytesIO()
        img_to_save = test_image
        if data['format'] == 'JPEG' and test_image.mode in ('RGBA', 'LA', 'P'):
            img_to_save = test_image.convert('RGB')
        
        img_to_save.save(buffer, format=data['format'], **data['params'])
        
        filename = f"comparison_{name.lower().replace(' ', '_')}.{data['format'].lower()}"
        filepath = OUTPUT_DIR / filename
        with open(filepath, 'wb') as f:
            f.write(buffer.getvalue())
        
        print(f"   ✅ {filename} ({data['size_kb']:.1f} KB)")

print(f"\n✅ Analyse de formats terminée")

📊 CONVERSION DE FORMATS ET OPTIMISATION
==================================================

🔍 Comparaison des formats de sortie:

📊 Résultats (du plus léger au plus lourd):
Format          Taille (KB)     Ratio      Params
----------------------------------------------------------------------
JPEG_Q75             61.04 KB    0.043    quality=75, optimize=True
JPEG_Q85             69.08 KB    0.049    quality=85, optimize=True
WEBP_Q90             76.49 KB    0.054    quality=90
JPEG_Q95             89.85 KB    0.064    quality=95, optimize=True
PNG                 561.17 KB    0.399    optimize=True

💡 Insights:
   🥇 Plus efficace: JPEG_Q75 (61.0 KB)
   📊 Moins efficace: PNG (561.2 KB)
   💾 Économie maximale: 89.1%

📋 Recommandations par cas d'usage:
   🎨 Qualité maximale: PNG (lossless, supporte transparence)
   📸 Photos web: JPEG Q85 (bon compromis qualité/taille)
   🚀 Performance web: WEBP (meilleure compression moderne)
   📱 Thumbnails: JPEG Q75 (taille minimale acceptable)

💾 Sauvegarde des comparaisons...
   ✅ comparison_png.png (561.2 KB)
   ✅ comparison_jpeg_q95.jpeg (89.8 KB)
   ✅ comparison_jpeg_q85.jpeg (69.1 KB)
   ✅ comparison_jpeg_q75.jpeg (61.0 KB)
   ✅ comparison_webp_q90.webp (76.5 KB)

✅ Analyse de formats terminée

Lecture du résultat. Le tableau comparatif trie les encodages du plus léger au plus lourd et concentre la leçon de la section :

  • Sur cette image photographique (800×600, chargée depuis URL en Section 1), le JPEG Q75 sort le plus léger (61.0 KB), devant le WEBP Q90 (76.5 KB) ; le PNG sans perte est le plus lourd (561.2 KB) : sur un contenu à haute entropie, l’encodage prédictif du PNG paie au prix fort, et la recommandation « photos web : JPEG Q85 » de la sortie s’applique directement.
  • Régime dual, même code : la cellule de comparaison encode test_image, chargée depuis une URL avec un repli synthétique (create_test_image, gradient lisse) si le téléchargement échoue. Sur le gradient (faible entropie), la hiérarchie s’inverse — le PNG sans perte devient le plus léger, le JPEG le plus lourd. La leçon complète est le contraste entre les deux régimes : aucune hiérarchie de formats n’est absolue, elle dépend de l’entropie du contenu.
  • WEBP Q90 s’intercale ici entre le JPEG Q85 et le JPEG Q95 : à qualité visuelle comparable, c’est un bon compromis moderne dans le cas photo, au prix d’une compatibilité de décodage un peu plus récente.
  • Le ratio affiché (0.399 pour le PNG) se lit comme la fraction de la taille mémoire vive (~1406 KB, Section 1) : la chaîne complète chargement → encodage → upload (Section 5) peut être budgétée chiffre en main avant l’appel API — l’économie maximale mesurée ici atteint 89.1 % (JPEG Q75 61.0 KB vs PNG 561.2 KB).

📚 Section 8: Récapitulatif et Ressources

Résumé des opérations couvertes et ressources supplémentaires. ### 📖 References savantes

  • Harris, C.R. et al. (2020). Array programming with NumPy. Nature, 585(7825), 357-362. doi:10.1038/s41586-020-2649-2. — Papier de citation officiel de NumPy, socle matriciel utilise tout au long du notebook.
  • Hunter, J.D. (2007). Matplotlib: A 2D Graphics Environment. Computing in Science & Engineering, 9(3), 90-95. doi:10.1109/MCSE.2007.55. — Papier de citation officiel de Matplotlib (affichage des images et histogrammes).
  • Pillow / PIL — Logiciel libre (fork communautaire de la Python Imaging Library d’origine). Pas de papier academique formel ; documentation officielle : pillow.readthedocs.io.
print("\n📚 RÉCAPITULATIF ET RESSOURCES")
print("=" * 50)

print("\n✅ Opérations Couvertes dans ce Notebook:")
print("\n1️⃣ Chargement et Formats")
print("   • Chargement depuis URL/fichier")
print("   • Création d'images de test")
print("   • Analyse des propriétés de base")

print("\n2️⃣ Transformations Géométriques")
print("   • Resize (avec préservation qualité)")
print("   • Thumbnail (proportionnel)")
print("   • Crop (découpage)")
print("   • Rotation (avec angles personnalisés)")
print("   • Flip (miroir horizontal/vertical)")

print("\n3️⃣ Filtres et Effets")
print("   • Blur (flou basique et Gaussian)")
print("   • Sharpen (netteté)")
print("   • Edge Detection (contours)")
print("   • Emboss (relief)")
print("   • Ajustements (brightness, contrast, saturation)")

if enable_metadata:
    print("\n4️⃣ Métadonnées et EXIF")
    print("   • Extraction données EXIF")
    print("   • Ajout métadonnées personnalisées")
    print("   • Traçabilité et attribution")

print("\n5️⃣ Intégration APIs")
print("   • Préparation pour DALL-E 3")
print("   • Optimisation pour Stable Diffusion")
print("   • Encodage Base64 pour GPT-5 Vision")
print("   • Gestion des contraintes API")

if show_interactive_demo and WIDGETS_AVAILABLE:
    print("\n6️⃣ Interface Interactive")
    print("   • Widgets pour ajustements en temps réel")
    print("   • Sauvegarde des modifications")

print("\n7️⃣ Optimisation")
print("   • Comparaison de formats")
print("   • Compression intelligente")
print("   • Recommandations par cas d'usage")

print("\n📖 Ressources Complémentaires:")
print("\n🔗 Documentation Pillow:")
print("   • https://pillow.readthedocs.io/")
print("   • Guide complet des filtres et transformations")

print("\n🔗 Tutorials CoursIA:")
print("   • ../tutorials/dalle3-complete-guide.md")
print("   • ../tutorials/gpt5-image-analysis-guide.md")
print("   • ../tutorials/educational-workflows.md")

print("\n🔗 Notebooks Suivants:")
print("   • 02-1-Prompt-Engineering-Visual.ipynb")
print("   • 02-2-Multi-Model-Comparison.ipynb")
print("   • 03-1-Advanced-Image-Processing.ipynb")

print("\n💡 Prochaines Étapes:")
print("   1. Pratiquer les transformations sur vos propres images")
print("   2. Explorer les filtres avancés de Pillow")
print("   3. Intégrer ces opérations dans vos workflows GenAI")
print("   4. Expérimenter avec les widgets interactifs")

if save_results:
    print(f"\n💾 Fichiers Générés (dans {OUTPUT_DIR}):")
    saved_files = list(OUTPUT_DIR.glob('*'))
    print(f"   📁 Total: {len(saved_files)} fichiers")
    for f in sorted(saved_files)[-10:]:  # Derniers 10
        size_kb = f.stat().st_size / 1024
        print(f"   • {f.name} ({size_kb:.1f} KB)")

print("\n" + "=" * 50)
print("✅ Notebook 01-3-Basic-Image-Operations terminé!")
print("🎓 Vous maîtrisez maintenant les opérations fondamentales sur les images")
print("🚀 Prêt pour les modules suivants!")
print("=" * 50)

📚 RÉCAPITULATIF ET RESSOURCES
==================================================

✅ Opérations Couvertes dans ce Notebook:

1️⃣ Chargement et Formats
   • Chargement depuis URL/fichier
   • Création d'images de test
   • Analyse des propriétés de base

2️⃣ Transformations Géométriques
   • Resize (avec préservation qualité)
   • Thumbnail (proportionnel)
   • Crop (découpage)
   • Rotation (avec angles personnalisés)
   • Flip (miroir horizontal/vertical)

3️⃣ Filtres et Effets
   • Blur (flou basique et Gaussian)
   • Sharpen (netteté)
   • Edge Detection (contours)
   • Emboss (relief)
   • Ajustements (brightness, contrast, saturation)

4️⃣ Métadonnées et EXIF
   • Extraction données EXIF
   • Ajout métadonnées personnalisées
   • Traçabilité et attribution

5️⃣ Intégration APIs
   • Préparation pour DALL-E 3
   • Optimisation pour Stable Diffusion
   • Encodage Base64 pour GPT-5 Vision
   • Gestion des contraintes API

6️⃣ Interface Interactive
   • Widgets pour ajustements en temps réel
   • Sauvegarde des modifications

7️⃣ Optimisation
   • Comparaison de formats
   • Compression intelligente
   • Recommandations par cas d'usage

📖 Ressources Complémentaires:

🔗 Documentation Pillow:
   • https://pillow.readthedocs.io/
   • Guide complet des filtres et transformations

🔗 Tutorials CoursIA:
   • ../tutorials/dalle3-complete-guide.md
   • ../tutorials/gpt5-image-analysis-guide.md
   • ../tutorials/educational-workflows.md

🔗 Notebooks Suivants:
   • 02-1-Prompt-Engineering-Visual.ipynb
   • 02-2-Multi-Model-Comparison.ipynb
   • 03-1-Advanced-Image-Processing.ipynb

💡 Prochaines Étapes:
   1. Pratiquer les transformations sur vos propres images
   2. Explorer les filtres avancés de Pillow
   3. Intégrer ces opérations dans vos workflows GenAI
   4. Expérimenter avec les widgets interactifs

💾 Fichiers Générés (dans D:\outputs\basic_operations):
   📁 Total: 6 fichiers
   • comparison_jpeg_q75.jpeg (61.0 KB)
   • comparison_jpeg_q85.jpeg (69.1 KB)
   • comparison_jpeg_q95.jpeg (89.8 KB)
   • comparison_png.png (561.2 KB)
   • comparison_webp_q90.webp (76.5 KB)
   • image_with_metadata.png (574.5 KB)

==================================================
✅ Notebook 01-3-Basic-Image-Operations terminé!
🎓 Vous maîtrisez maintenant les opérations fondamentales sur les images
🚀 Prêt pour les modules suivants!
==================================================

Lecture du résultat. La synthèse reprend le fil complet parcouru — charger, transformer, filtrer, métadonner, préparer pour API, exporter — et c’est l’ordre canonique d’un pipeline de pré-traitement GenAI : chaque section de ce notebook est une étape franchissable indépendamment, mais l’enchaînement est la vraie compétence. Le chemin mémo : une image est une matrice (Section 1) qu’on redresse et découpe (2), qu’on lisse ou accentue (3), qu’on documente (4), qu’on adapte aux contraintes du moteur cible (5), et qu’on encode au bon rapport qualité/poids (7). Les exercices qui suivent fixent chaque maillon.

Erreurs classiques du débutant PIL — à connaître avant de débugger. Trois pièges reviennent dans quasi tout premier pipeline :

  • Image.open est lazy : l’ouverture ne lit que l’en-tête ; la matrice de pixels n’est chargée qu’au premier accès. Un fichier modifié ou supprimé entre open et l’exploitation explose tardivement — appelez image.load() (ou un image.size suffit parfois à révéler le problème) au plus tôt quand la source est volatile.
  • Mode implicite : sauvegarder en JPEG une image mode RGBA (canal alpha) lève OSError: cannot write mode RGBA as JPEG — convertir explicitement (image.convert("RGB")) avant l’export ; la Section 7 le fait pour vous dans ses comparaisons.
  • Confusion resize/thumbnail : resize écrase le ratio si on le lui demande, thumbnail ne dépasse JAMAIS la boîte demandée en conservant le ratio. Choisir l’un pour l’autre produit des images aplaties — la Section 2 montre les deux sur la même source.

Exemple guide : Pipeline de Traitement d’Images pour GenAI

Durée estimée : 20-25 minutes

Objectif

Créer un pipeline complet de préparation d’images pour une API de génération (DALL-E 3, Stable Diffusion, ou GPT-5 Vision) en appliquant les opérations apprises dans ce notebook.

Instructions

  1. Choisir une API cible parmi : DALL-E 3, Stable Diffusion img2img, ou GPT-5 Vision
  2. Charger une image depuis une URL ou un fichier local
  3. Appliquer une chaîne de transformations : resize → ajustements → optimisation
  4. Préparer l’image pour l’API : format correct, taille optimale, base64 encoding
  5. Créer une fonction réutilisable pour le pipeline complet

Indices :

  • Indice 1 : DALL-E 3 préfère les carrés (1024x1024), Stable Diffusion img2img utilise 512x512 ou 768x768
  • Indice 2 : Utilisez ImageEnhance pour les ajustements et prepare_for_api() pour l’optimisation
  • Indice 3 : Pour GPT-5 Vision, n’oubliez pas l’encodage base64 avec encode_image_base64()
  • Indice 4 : Les métadonnées PNG sont utiles pour la traçabilité des images générées

Critères de succès

Extension (optionnelle)

Pour les étudiants avancés : - Ajouter un système de benchmark pour comparer les temps de traitement - Implémenter la validation automatique des spécifications API - Créer une interface interactive avec widgets pour ajuster les paramètres du pipeline - Supporter le batch processing pour traiter plusieurs images en séquence

# Exercice: Implémentez le pipeline de traitement d'images

# 1. Configuration de l'API cible
api_target = "..."  # "dalle3", "stable_diffusion", "gpt5_vision"

# Spécifications par API
API_SPECS = {
    "dalle3": {
        "size": (1024, 1024),  # Carré recommandé
        "format": "PNG",
        "max_size_kb": 20480,  # 20MB max
        "description": "Génération d'images avec DALL-E 3"
    },
    "stable_diffusion": {
        "size": (768, 768),  # Taille standard img2img
        "format": "PNG",
        "max_size_kb": 500,
        "description": "Image-to-image avec Stable Diffusion"
    },
    "gpt5_vision": {
        "size": (800, 600),  # Taille optimale analyse
        "format": "JPEG",  # Plus léger pour base64
        "max_size_kb": 1000,
        "description": "Analyse d'images avec GPT-5 Vision"
    }
}

# 2. Charger une image depuis une URL ou fichier
# Indice: Utilisez load_image_from_url() ou load_image_from_file()
image_source = "https://picsum.photos/1200/800"  # Remplacer par votre image

def load_image(source):
    """
    Charge une image depuis URL ou fichier local.
    
    Args:
        source: URL ou chemin local
    
    Returns:
        Image PIL ou None si erreur
    """
    # Exercice: Implémenter le chargement avec gestion d'erreurs
    pass

# 3. Pipeline de transformations
def process_image_pipeline(image, api_target):
    """
    Applique une chaîne de transformations à une image.
    
    Args:
        image: Image PIL d'origine
        api_target: API cible ("dalle3", "stable_diffusion", "gpt5_vision")
    
    Returns:
        Tuple (image_transformée, métadonnées)
    """
    specs = API_SPECS[api_target]
    
    # Exercice: Implémenter le pipeline
    # 1. Resize aux dimensions cibles (LANCZOS pour qualité)
    # 2. Ajustements (brightness +10%, contrast +5%, saturation +10%)
    # 3. Filtre sharpen pour netteté
    # 4. Optimisation format et taille
    
    # Indice: Utilisez ImageEnhance.Brightness, .Contrast, .Color, .Sharpness
    # Indice: Utilisez prepare_for_api() pour l'optimisation finale
    
    pass

# 4. Préparation pour l'API (encoding + payload)
def prepare_api_payload(image_bytes, api_target):
    """
    Prépare le payload pour l'API cible.
    
    Args:
        image_bytes: Image encodée en bytes
        api_target: API cible
    
    Returns:
        Dict avec payload prêt pour l'API
    """
    # Exercice: Implémenter la préparation du payload
    # Indice: Pour GPT-5 Vision, encodez en base64
    # Indice: Ajoutez les métadonnées pour traçabilité
    
    pass

# 5. Pipeline complet end-to-end
def complete_pipeline(image_source, api_target, save_intermediates=False):
    """
    Pipeline complet de traitement d'une image pour une API GenAI.
    
    Args:
        image_source: URL ou chemin de l'image
        api_target: API cible
        save_intermediates: Sauvegarder les étapes intermédiaires
    
    Returns:
        Dict avec image finale, payload API, et métadonnées
    """
    # Exercice: Implémenter le pipeline complet
    # 1. Chargement
    # 2. Transformations
    # 3. Préparation API
    # 4. Sauvegarde (optionnel)
    
    pass

# 6. Tester le pipeline avec votre image
# Indice: Affichez un avant/après pour vérifier les transformations
pass

Pour aller plus loin. Ce notebook ferme les opérations classiques de PIL ; la suite de la série monte vers la génération elle-même et l’analyse par modèle :

  • 01-2 (précédent) couvrait la génération GPT-5 Image ; observez comme le présent notebook est la contre-face déterministe : mêmes entrées, mêmes sorties, aucune variance — un outil de test reproductible pour vos pipelines génératifs.
  • Pour l’analyse d’images par modèle de vision (description, extraction structurée), le pattern payload GPT-5 Vision de la Section 5 est le point de départ direct.
  • Approfondissement PIL hors série : le module ImageOps.exif_transpose automatise exactement la compensation d’orientation EXIF discutée en Section 4 — un réflexe professionnel à adopter dès qu’une source photo entre dans un pipeline.

Conclusion

Ce notebook a permis d’explorer les aspects essentiels de 01 3 basic image opérations. Les points cles :

  • Les concepts fondamentaux ont ete presentes et illustres
  • Les Exemple guides proposent une mise en pratique progressive
  • Les résultats obtenus permettent de valider la comprehension

Pour aller plus loin : approfondir les aspects avances du sujet et explorer les liens avec d’autres domaines.

Retour au sommet