331 lines
16 KiB
Markdown
331 lines
16 KiB
Markdown
# 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: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)
|
||
|
||
```mermaid
|
||
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 :
|
||
|
||
```mermaid
|
||
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.
|
||
|
||
```mermaid
|
||
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.
|
||
|
||
```mermaid
|
||
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 :
|
||
|
||
```mermaid
|
||
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`. |