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 l’environnement
Installer les dépendances via poetry install depuis un terminal PyCharm.
Pour lancer les tests unitaires :
poetry run pytest -p no:warningspoetry run pytest tests/test_calculate_new_height.py -p no:warningspoetry 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.
-
Prise en main de la structure :
- Ouvrez le fichier
process.pyet 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_heightest déjà implémentée.
- Ouvrez le fichier
-
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 commentaireTODOpour ouvrir, redimensionner et sauvegarder une image. - Ensuite, dans la fonction
main, complétez la sectionTODOqui liste les fichiers sources (all_source_files).
- Votre premier objectif est de compléter la fonction
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.
-
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"]).
- Repérez en haut du script les deux listes de configuration déjà définies :
-
Mise à jour de la logique :
- Dans la fonction
main, modifiez la sectionTODOpour construire la listeresize_tasks. Vous devez maintenant construire cette liste pour que chaque image source soit traitée pour chaque largeur deRESIZE_WIDTHSet sauvegardée dans chaque format deFORMATS. - Adaptez le nommage des fichiers de sortie pour qu'il inclue la taille, par exemple :
{nom_original}_{largeur}x{hauteur}.{format}.
- Dans la fonction
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éfiniall_source_files:files_to_process = all_source_filesVous 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]
-
Refactorisation du code :
- L'architecture du code est déjà prête pour la parallélisation. Remarquez que la fonction "worker"
resize_single_imageest déjà conçue pour accepter un seul argument de typeTuple, ce qui est une condition requise pourProcessPoolExecutor.map.
- L'architecture du code est déjà prête pour la parallélisation. Remarquez que la fonction "worker"
-
Implémentation des exécuteurs :
- Votre mission est de compléter les deux fonctions "runner" :
run_sequentialetrun_parallel. - Suivez les
TODOdansrun_sequentialpour implémenter une simple boucleforqui exécute les tâches les unes après les autres. - Suivez les
TODOdansrun_parallelpour utiliserconcurrent.futures.ProcessPoolExecutorafin de distribuer les tâches sur plusieurs processus. - Les deux fonctions doivent mesurer et retourner le temps d'exécution.
- Votre mission est de compléter les deux fonctions "runner" :
-
Intégration et comparaison :
- Dans
main, la logique pour sélectionner le bon "runner" en fonction de la constanteEXECUTION_MODEest 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 !
- Dans
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
-
Hachage de fichiers :
- La fonction
compute_sha3_512qui 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, commeresize_single_image, prête pour la parallélisation.
- La fonction
-
Mise en place du cache :
- Complétez la fonction
get_hashes_from_csven suivant leTODO. Elle doit lire un fichierhashes.csvet retourner un dictionnaire des hashes existants. L'utilisation de la bibliothèquepandasest recommandée à cet effet. - Dans la fonction
main, suivez lesTODOpour :- Préparer les
hash_taskspour tous les fichiers sources. - Appeler le "runner" pour exécuter ces tâches. Vous pourrez immédiatement appliquer le
run_parallelque vous venez de créer pour accélérer également cette étape de hachage ! - 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. - Sauvegarder les nouveaux hashes dans le fichier CSV.
- Préparer les
- Complétez la fonction
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.
-
Examen d'un test existant :
- Le test
test_calculate_new_height_various_scenariosvous est intégralement fourni. - Analysez sa structure, notamment l'utilisation de
@pytest.mark.parametrizeetpytest.raises. - Exécutez-le pour vous assurer que tout fonctionne.
- Le test
-
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 exceptionValueErrorest bien levée pour des largeurs (orig_width) incorrectes :0et-100.
- En s'inspirant du test existant, ajoutez un autre test