Files
ENI-PythonAdvanced_04/README.md
2025-12-16 09:56:21 +01:00

16 KiB
Raw Permalink Blame History

TP : Traitement d'images par lots (multiprocessing)

Informations générales

Cours : Python Avancé > Programmation concurrente > Parallélisation de tâches CPU-bound avec multiprocessing
Objectifs pédagogiques :

  • Python avancé : programmation fonctionnelle
  • Python avancé : utilisation de bibliothèques spécialisées (pathlib, pandas, pillow, etc.)
  • Programmation concurrente : parallélisation de tâches CPU-bound avec multiprocessing
  • Cryptographie et sécurité : hachage et vérification de l'intégrité des données
  • Gestion des erreurs et logging avancé : capture d'exceptions ciblées, logging
  • Tests : découverte de pytest
  • Outils modernes (poetry, PyCharm)
  • Bonnes pratiques de l'entreprise

Prérequis

Installation et configuration de lenvironnement

Installer les dépendances via poetry install depuis un terminal PyCharm.

Pour lancer les tests unitaires :

  • poetry run pytest -p no:warnings
  • poetry run pytest tests/test_calculate_new_height.py -p no:warnings
  • poetry run pytest tests/test_calculate_new_height.py::test_calculate_new_height_various_scenarios

Connaissances préalables

  • Connaissances de base en programmation

Énoncé

L'objectif de ce TP est de concevoir et de réaliser un script Python robuste et performant pour le traitement d'images (redimensionnement) en lots. Vous partirez d'un squelette de script process.py pour progressivement implémenter la logique métier, paralléliser l'exécution pour des performances maximales, et enfin ajouter un système de cache pour optimiser les traitements répétitifs.

Plus tard, nous pourrons utiliser les images générées pour alimenter un site web responsive, afin de charger les images les plus adaptées à la taille de l'écran de l'utilisateur, et ainsi améliorer les performances SEO (référencement naturel), et réduire le coût du SEA (référencement payant type Google Ads).

Ce que l'on cherche à accomplir

Redimensionner des images en plusieurs tailles et formats, en utilisant la bibliothèque Pillow.

Exemples de redimensionnement

Exemple : à partir d'une image source PIA04921.jpg de 5184x3456 pixels, le script va générer les images suivantes :

  • PIA04921_4000x2667.png (4000 pixels de large, format PNG)
  • PIA04921_4000x2667.webp (4000 pixels de large, format WebP)
  • PIA04921_2500x1667.png (2500 pixels de large, format PNG)
  • PIA04921_2500x1667.webp (2500 pixels de large, format WebP)
  • PIA04921_1928x1286.png (1928 pixels de large, format PNG)
  • PIA04921_1928x1286.webp (1928 pixels de large, format WebP)
  • etc.

Version sans hachage (plus simple, mais moins optimisé)

Pour vous donner une idée du résultat final du script côté console, voici des exemples de console d'exécution du script.

Lancement du script, avec le mode d'exécution séquentiel (sans multiprocessing) :

INFO - --- Mode d'exécution sélectionné : SEQUENTIAL ---
INFO - Trouvé 6 fichier(s) dans 'input_images'.
INFO - -> 6 fichier(s) à traiter.
INFO - Démarrage de 'Redimensionnement des images' (132 tâches) en mode séquentiel...
INFO - Tâche 'Redimensionnement des images' terminée en 53.42 secondes.
INFO - --- Résumé des temps ---
INFO - Temps de redimensionnement : 53.42 secondes
INFO - --------------------------

Avec le mode d'exécution parallèle (avec multiprocessing). C'est beaucoup plus rapide :

INFO - --- Mode d'exécution sélectionné : PARALLEL ---
INFO - Trouvé 6 fichier(s) dans 'input_images'.
INFO - -> 6 fichier(s) à traiter.
INFO - Démarrage de 'Redimensionnement des images' (132 tâches) avec 19 processus...
INFO - Tâche 'Redimensionnement des images' terminée en 6.88 secondes.
INFO - --- Résumé des temps ---
INFO - Temps de redimensionnement : 6.88 secondes
INFO - --------------------------

Version avec hachage (plus optimisé)

Lancement du script, avec le mode d'exécution séquentiel (sans multiprocessing) :

INFO - --- Mode d'exécution sélectionné : SEQUENTIAL ---
INFO - Trouvé 6 fichier(s) dans 'input_images'.
INFO - Démarrage de 'Calcul des hashes' (6 tâches) en mode séquentiel...
INFO - Tâche 'Calcul des hashes' terminée en 0.08 secondes.
INFO - Filtrage des fichiers modifiés...
INFO - Fichier de hash 'hashes.csv' mis à jour.
INFO - -> 6 fichier(s) à traiter.
INFO - Démarrage de 'Redimensionnement des images' (132 tâches) en mode séquentiel...
INFO - Tâche 'Redimensionnement des images' terminée en 53.42 secondes.
INFO - --- Résumé des temps ---
INFO - Temps de hachage :           0.08 secondes
INFO - Temps de redimensionnement : 53.42 secondes
INFO - --------------------------
INFO - Traitement complet terminé en 53.51 secondes.

Avec le mode d'exécution parallèle (avec multiprocessing). C'est toujours beaucoup plus rapide :

INFO - --- Mode d'exécution sélectionné : PARALLEL ---
INFO - Trouvé 6 fichier(s) dans 'input_images'.
INFO - Démarrage de 'Calcul des hashes' (6 tâches) avec 19 processus...
INFO - Tâche 'Calcul des hashes' terminée en 0.52 secondes.
INFO - Filtrage des fichiers modifiés...
INFO - Fichier de hash 'hashes.csv' mis à jour.
INFO - -> 6 fichier(s) à traiter.
INFO - Démarrage de 'Redimensionnement des images' (132 tâches) avec 19 processus...
INFO - Tâche 'Redimensionnement des images' terminée en 6.88 secondes.
INFO - --- Résumé des temps ---
INFO - Temps de hachage :           0.52 secondes
INFO - Temps de redimensionnement : 6.88 secondes
INFO - --------------------------
INFO - Traitement complet terminé en 7.40 secondes.

Dans le cas où le hachage des images n'a pas changé, le script ira très vite (on ne redimensionne rien, on ne fait que calculer les hashes) :

INFO - --- Mode d'exécution sélectionné : PARALLEL ---
INFO - Trouvé 6 fichier(s) dans 'input_images'.
INFO - Démarrage de 'Calcul des hashes' (6 tâches) avec 19 processus...
INFO - Tâche 'Calcul des hashes' terminée en 0.52 secondes.
INFO - Filtrage des fichiers modifiés...
INFO - Fichier de hash 'hashes.csv' mis à jour.
INFO - -> 0 fichier(s) à traiter.
INFO - Aucun fichier à redimensionner.
INFO - --- Résumé des temps ---
INFO - Temps de hachage :           0.52 secondes
INFO - Temps de redimensionnement : 0.00 secondes
INFO - --------------------------
INFO - Traitement complet terminé en 0.52 secondes.

Vision d'ensemble du script (sans la partie hachage)

graph TD
    A[Démarrage du script] --> B["Lister les fichiers source .jpg"]
    B --> G[Générer les tâches de redimensionnement pour tous les fichiers]
    G --> H{Aucune tâche de redimensionnement?}
    H -- Non --> I["Exécuter le redimensionnement (parallèle ou séquentiel)"]
    I --> J[Sauvegarder les nouvelles images]
    J --> L[Fin]
    H -- Oui --> L[Fin]

Partie 1 : le redimensionnement d'images séquentiel

Le squelette du script process.py vous est fourni. Votre première tâche est de compléter les sections marquées par des TODO pour implémenter la logique de base du traitement séquentiel.

  1. Prise en main de la structure :

    • Ouvrez le fichier process.py et prenez connaissance de sa structure : les imports, les constantes globales (SOURCE_DIRECTORY, TARGET_DIRECTORY, etc.) et les fonctions déjà définies.
    • Remarquez que la fonction calculate_new_height est déjà implémentée.
  2. Création de la logique de redimensionnement :

    • Votre premier objectif est de compléter la fonction resize_single_image. Suivez les instructions laissées dans le commentaire TODO pour ouvrir, redimensionner et sauvegarder une image.
    • Ensuite, dans la fonction main, complétez la section TODO qui liste les fichiers sources (all_source_files).

Partie 2 : flexibilité et traitement par lots

Améliorez le script pour qu'il ne soit plus limité à une seule taille et un seul format.

  1. Configuration avancée :

    • Repérez en haut du script les deux listes de configuration déjà définies :
      • RESIZE_WIDTHS: une liste d'entiers contenant toutes les largeurs de sortie souhaitées (ex: [4000, 2500, 1928, 992, 768, 576, 480, 260, 150, 100, 50] inspirée des breakpoints Bootstrap).
      • FORMATS: une liste de chaînes de caractères pour les formats de sortie (ex: ["png", "webp"]).
  2. Mise à jour de la logique :

    • Dans la fonction main, modifiez la section TODO pour construire la liste resize_tasks. Vous devez maintenant construire cette liste pour que chaque image source soit traitée pour chaque largeur de RESIZE_WIDTHS et sauvegardée dans chaque format de FORMATS.
    • Adaptez le nommage des fichiers de sortie pour qu'il inclue la taille, par exemple : {nom_original}_{largeur}x{hauteur}.{format}.

Le schéma ci-dessous illustre comment une seule image source génère une multitude de tâches de redimensionnement :

graph TD
    subgraph "Entrées"
        A["Fichier img.jpg"]
        subgraph "Configuration"
            B1[Largeur 4000]
            B2[Largeur 2500]
            B3[...]
            C1["Format png"]
            C2["Format webp"]
        end
    end

    subgraph "Tâches générées"
        D1["Tâche: img, 4000, png"]
        D2["Tâche: img, 4000, webp"]
        D3["Tâche: img, 2500, png"]
        D4["Tâche: img, 2500, webp"]
        D5[...]
    end

    A --> D1 & D2 & D3 & D4 & D5
    B1 & C1 --> D1
    B1 & C2 --> D2
    B2 & C1 --> D3
    B2 & C2 --> D4

ASTUCE POUR TESTER : À ce stade, la logique de hachage n'est pas encore faite. Pour que votre script traite les images, vous pouvez temporairement forcer le traitement de tous les fichiers en ajoutant cette ligne dans main(), juste après avoir défini all_source_files : files_to_process = all_source_files Vous pourrez retirer cette ligne lorsque vous implémenterez la Partie 4.

À ce stade, votre script est fonctionnel, mais probablement lent si vous avez beaucoup d'images et de tailles cibles.

Partie 3 : Parallélisation des tâches (Multiprocessing)

C'est le cœur du TP. Vous allez drastiquement accélérer le script en utilisant la programmation parallèle pour exploiter tous les cœurs de votre CPU.

La différence entre l'exécution séquentielle et parallèle est la suivante :

Mode Séquentiel : Les tâches sont exécutées les unes après les autres.

graph TD
    A[Démarrage] --> B[Tâche 1]
    B --> C[Tâche 2]
    C --> D[Tâche 3]
    D --> E[...]
    E --> F[Fin]

Mode Parallèle : Les tâches sont distribuées à un pool de processus (workers) et s'exécutent en même temps.

graph TD
    A[Démarrage] --> B[Pool de processus]
    B --> T1[Tâche 1]
    B --> T2[Tâche 2]
    B --> T3[Tâche 3]
    B --> T4[Tâche 4]
    B --> T...[...]
    
    subgraph "Exécution concurrente"
        T1
        T2
        T3
        T4
        T...
    end

    T1 & T2 & T3 & T4 & T... --> E[Attente de la fin de toutes les tâches]
    E --> F[Fin]
  1. Refactorisation du code :

    • L'architecture du code est déjà prête pour la parallélisation. Remarquez que la fonction "worker" resize_single_image est déjà conçue pour accepter un seul argument de type Tuple, ce qui est une condition requise pour ProcessPoolExecutor.map.
  2. Implémentation des exécuteurs :

    • Votre mission est de compléter les deux fonctions "runner" : run_sequential et run_parallel.
    • Suivez les TODO dans run_sequential pour implémenter une simple boucle for qui exécute les tâches les unes après les autres.
    • Suivez les TODO dans run_parallel pour utiliser concurrent.futures.ProcessPoolExecutor afin de distribuer les tâches sur plusieurs processus.
    • Les deux fonctions doivent mesurer et retourner le temps d'exécution.
  3. Intégration et comparaison :

    • Dans main, la logique pour sélectionner le bon "runner" en fonction de la constante EXECUTION_MODE est déjà en place. Une fois vos runners implémentés, vous pourrez basculer entre "parallel" et "sequential" pour comparer les performances.
    • Assurez-vous que l'appel au runner pour le redimensionnement est correctement effectué. Vous devriez constater un gain de performance spectaculaire en mode parallèle !

Partie 4 (bonus) : optimisation avec un cache de hachage

Votre script est rapide, mais il retraite toutes les images à chaque lancement. Vous allez maintenant implémenter un système de cache pour ne traiter que les fichiers nouveaux ou modifiés.

Voici un schéma représentant la logique complète du script que vous allez construire :

graph TD
    A[Démarrage du script] --> B["Lister les fichiers source .jpg"]
    B --> C[Calculer les nouveaux hashes SHA3 des fichiers]
    C --> D["Lire les anciens hashes depuis hashes.csv"]
    D --> E{Fichiers modifiés ou nouveaux?}
    E -- Oui --> F[Filtrer la liste des fichiers à traiter]
    F --> G[Générer les tâches de redimensionnement]
    G --> H{Aucune tâche de redimensionnement?}
    H -- Non --> I["Exécuter le redimensionnement (parallèle ou séquentiel)"]
    I --> J[Sauvegarder les nouvelles images]
    J --> K["Mettre à jour hashes.csv avec les nouveaux hashes"]
    K --> L[Fin]
    E -- Non --> H
    H -- Oui --> K
  1. Hachage de fichiers :

    • La fonction compute_sha3_512 qui calcule l'empreinte numérique d'un fichier vous est déjà fournie. Notez qu'elle est optimisée pour lire les fichiers par blocs et qu'elle est, comme resize_single_image, prête pour la parallélisation.
  2. Mise en place du cache :

    • Complétez la fonction get_hashes_from_csv en suivant le TODO. Elle doit lire un fichier hashes.csv et retourner un dictionnaire des hashes existants. L'utilisation de la bibliothèque pandas est recommandée à cet effet.
    • Dans la fonction main, suivez les TODO pour :
      1. Préparer les hash_tasks pour tous les fichiers sources.
      2. Appeler le "runner" pour exécuter ces tâches. Vous pourrez immédiatement appliquer le run_parallel que vous venez de créer pour accélérer également cette étape de hachage !
      3. Implémenter la logique de filtrage pour ne garder que les fichiers modifiés, c'est à dire en comparant les nouveaux hashes aux anciens et ne gardant que les fichiers modifiés dans la liste files_to_process.
      4. Sauvegarder les nouveaux hashes dans le fichier CSV.

Exemple de fichier hashes.csv généré par votre programme :

filename,hash
GSFC_20171208.jpg,c84a997754ef10a2b8a66d793372983180bdb961753d16ffa9037c32ef09db483ed67ff774863c6591b9120f05e31ddd9893d7e5ac59c1049a993c396af6baa4
ISS070E034016.jpg,82ffdf4f9c0b13bd42626f6026f268b4db078b3e26324038b925c5e563149b0d5e40124251378d0c6f085a4d877fb38a91465934d0f9c51aa34e82740168f7ee
ISS070E052303.jpg,4f8d0614830652a19e53571b26fa44d6f08cee386e19c69ab1347d2afc591e67cdbb48640e050fdf03ce44b1ce8ad8d43f26a2e407cd854205fb23de70ca52fc
PIA04921.jpg,775e80e7782f98cded5e4be2ee5933e7df73ef02470dbd017e1782bba7e01ad30b6c01e78ba825e218db316c8839233b5e9a0a7e8642a5c69e9949aa1f23a00b
PIA18033.jpg,2b6e0212d9185f58da5d7c2d37a732edb055a4032c1679857f00f2cd26bba16d91b5b54944333686bd72d79f0a33421c6edffad2f3c75ea30317a83d5b71b44f
PIA25163.jpg,c3f5d3268b2bc4ec810e084d2156492aee02f6d10d2950d52a5d7471a76de96cb9f5505e565b2b064b4d81cd14814416579e8ebb430d3e9a72346a3a175bf619

Partie 5 : Tests unitaires

Pour garantir la fiabilité de votre code, nous allons examiner et utiliser des tests unitaires.

  1. Examen d'un test existant :

    • Le test test_calculate_new_height_various_scenarios vous est intégralement fourni.
    • Analysez sa structure, notamment l'utilisation de @pytest.mark.parametrize et pytest.raises.
    • Exécutez-le pour vous assurer que tout fonctionne.
  2. Ajout d'un test unitaire :

    • En s'inspirant du test existant, ajoutez un autre test test_calculate_new_height_with_invalid_width_raises_error, qui vérifie qu'une exception ValueError est bien levée pour des largeurs (orig_width) incorrectes : 0 et -100.