# 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`.