diff --git a/.dockerignore b/.dockerignore
index 4628bdd..5ab9f6d 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,106 +1,106 @@
-# Notebooks de développement mais GARDER les samples
-notebooks/*.ipynb
-notebooks/p8_notebook.ipynb
-notebooks/requirements.txt
-.ipynb_checkpoints/
-# GARDER: notebooks/content/data/test_images_sample/ (nécessaire pour l'app)
-
-# Environment files (variables passées via docker run ou compose)
-.env
-.env.*
-!.env.example
-
-# Python cache et build files
-__pycache__/
-*.py[cod]
-*$py.class
-*.so
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-pip-wheel-metadata/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# Virtual environments
-venv/
-env/
-ENV/
-env.bak/
-venv.bak/
-.venv/
-
-# IDE et editor files
-.vscode/
-.idea/
-*.swp
-*.swo
-*~
-.DS_Store
-Thumbs.db
-
-# Git files
-.git/
-.gitignore
-.gitattributes
-
-# Documentation
-*.md
-!README.MD
-docs/
-documentation/
-
-# Test files
-tests/
-test_*/
-*_test.py
-debug_*.py
-pytest.ini
-tox.ini
-.coverage
-.pytest_cache/
-
-# Jupyter notebook outputs
-*.nbconvert
-
-# Fichiers temporaires
-.cache/
-.tmp/
-tmp/
-temp/
-*.tmp
-*.temp
-*.log
-logs/
-
-# OS generated files
-.DS_Store?
-ehthumbs.db
-Icon?
-
-# MLflow artifacts locaux (seront téléchargés depuis le registry)
-mlruns/
-mlruns.backup/
-mlartifacts/
-
-# Backup files
-*.bak
-*.backup
-
-# Large datasets (samples nécessaires sont gardés)
-*.zip
-*.tar.gz
-*.rar
+# Notebooks de développement mais GARDER les samples
+notebooks/*.ipynb
+notebooks/p8_notebook.ipynb
+notebooks/requirements.txt
+.ipynb_checkpoints/
+# GARDER: notebooks/content/data/test_images_sample/ (nécessaire pour l'app)
+
+# Environment files (variables passées via docker run ou compose)
+.env
+.env.*
+!.env.example
+
+# Python cache et build files
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# Virtual environments
+venv/
+env/
+ENV/
+env.bak/
+venv.bak/
+.venv/
+
+# IDE et editor files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+.DS_Store
+Thumbs.db
+
+# Git files
+.git/
+.gitignore
+.gitattributes
+
+# Documentation
+*.md
+!README.MD
+docs/
+documentation/
+
+# Test files
+tests/
+test_*/
+*_test.py
+debug_*.py
+pytest.ini
+tox.ini
+.coverage
+.pytest_cache/
+
+# Jupyter notebook outputs
+*.nbconvert
+
+# Fichiers temporaires
+.cache/
+.tmp/
+tmp/
+temp/
+*.tmp
+*.temp
+*.log
+logs/
+
+# OS generated files
+.DS_Store?
+ehthumbs.db
+Icon?
+
+# MLflow artifacts locaux (seront téléchargés depuis le registry)
+mlruns/
+mlruns.backup/
+mlartifacts/
+
+# Backup files
+*.bak
+*.backup
+
+# Large datasets (samples nécessaires sont gardés)
+*.zip
+*.tar.gz
+*.rar
diff --git a/.env.example b/.env.example
index 7c6bb04..49f2004 100644
--- a/.env.example
+++ b/.env.example
@@ -1,41 +1,41 @@
-# =============================================================================
-# Configuration du serveur
-# =============================================================================
-# Port sur lequel nginx écoute (défaut: 8080 pour compatibilité Cloud Run)
-NGINX_PORT=8080
-
-# =============================================================================
-# Configuration MLflow (pour le versioning et le téléchargement des modèles)
-# =============================================================================
-# URI du serveur MLflow de tracking
-MLFLOW_TRACKING_URI=http://localhost:5000
-
-# URL du stockage S3/MinIO pour les artefacts MLflow
-MLFLOW_S3_ENDPOINT_URL=http://localhost:9000
-
-# Clés d'accès S3/MinIO
-AWS_ACCESS_KEY_ID=minio_access_key
-AWS_SECRET_ACCESS_KEY=minio_secret_key
-
-# =============================================================================
-# Configuration email (optionnel - pour les notifications)
-# =============================================================================
-# Serveur SMTP
-SMTP_SERVER=smtp.gmail.com
-SMTP_PORT=587
-
-# Identifiants email
-SMTP_EMAIL=noreply@yourcompany.com
-SMTP_PASSWORD=your_app_password
-SMTP_FROM_ALIAS=Multi-Segmentation API
-
-# Email de l'administrateur (destinataire des notifications)
-ADMIN_EMAIL=admin@yourcompany.com
-
-# =============================================================================
-# Configuration de l'API
-# =============================================================================
-# URL de base de l'API (utilisée par Streamlit pour faire les appels)
-# En développement local: http://localhost:8080/api
-# En production: https://your-domain.com/api
-MULTISEG_API_BASE_URL=http://localhost:8080/api
+# =============================================================================
+# Configuration du serveur
+# =============================================================================
+# Port sur lequel nginx écoute (défaut: 8080 pour compatibilité Cloud Run)
+NGINX_PORT=8080
+
+# =============================================================================
+# Configuration MLflow (pour le versioning et le téléchargement des modèles)
+# =============================================================================
+# URI du serveur MLflow de tracking
+MLFLOW_TRACKING_URI=http://localhost:5000
+
+# URL du stockage S3/MinIO pour les artefacts MLflow
+MLFLOW_S3_ENDPOINT_URL=http://localhost:9000
+
+# Clés d'accès S3/MinIO
+AWS_ACCESS_KEY_ID=minio_access_key
+AWS_SECRET_ACCESS_KEY=minio_secret_key
+
+# =============================================================================
+# Configuration email (optionnel - pour les notifications)
+# =============================================================================
+# Serveur SMTP
+SMTP_SERVER=smtp.gmail.com
+SMTP_PORT=587
+
+# Identifiants email
+SMTP_EMAIL=noreply@yourcompany.com
+SMTP_PASSWORD=your_app_password
+SMTP_FROM_ALIAS=Multi-Segmentation API
+
+# Email de l'administrateur (destinataire des notifications)
+ADMIN_EMAIL=admin@yourcompany.com
+
+# =============================================================================
+# Configuration de l'API
+# =============================================================================
+# URL de base de l'API (utilisée par Streamlit pour faire les appels)
+# En développement local: http://localhost:8080/api
+# En production: https://your-domain.com/api
+MULTISEG_API_BASE_URL=http://localhost:8080/api
diff --git a/Dockerfile b/Dockerfile
index 37d075c..925177b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,44 +1,44 @@
-FROM python:3.12-slim
-
-WORKDIR /app
-
-# Install system dependencies (ajout de git pour segmentation_models)
-RUN apt-get update && \
- apt-get install -y nginx curl gettext-base git && \
- # Pour OpenCV si nécessaire
- apt-get install -y libglib2.0-0 libsm6 libxext6 libxrender-dev libgomp1 && \
- adduser --system --no-create-home --group www-data || true && \
- apt-get clean && \
- rm -rf /var/lib/apt/lists/*
-
-# Copy app/ d'abord pour avoir requirements.txt
-COPY app/ ./app/
-
-# Install requirements depuis le bon chemin
-RUN pip install --no-cache-dir -r app/requirements.txt
-
-# ✅ CORRECTION: Copy start.sh AVANT les permissions
-COPY start.sh ./start.sh
-
-# Copy du reste après installation (optimise les layers Docker)
-COPY utils/ ./utils/
-COPY cityscapes_config.json .
-COPY notebooks/content/data/test_images_sample/ ./notebooks/content/data/test_images_sample/
-COPY notebooks/content/data/train_images_sample/ ./notebooks/content/data/train_images_sample/
-
-# Copy nginx config
-COPY nginx.conf /etc/nginx/nginx.conf.template
-
-# ✅ CORRECTION: Setup permissions APRÈS avoir copié start.sh + correction fins de ligne
-RUN mkdir -p /var/log/nginx /var/lib/nginx /run && \
- sed -i 's/\r$//' ./start.sh && \
- chmod +x ./start.sh
-
-# Environment variables
-ENV NGINX_PORT=8080
-ENV MULTISEG_API_BASE_URL=http://localhost:8080/api
-
-EXPOSE $NGINX_PORT
-
-# ✅ CORRECTION: Chemin cohérent (./start.sh au lieu de /app/start.sh)
-CMD ["./start.sh"]
+FROM python:3.12-slim
+
+WORKDIR /app
+
+# Install system dependencies (ajout de git pour segmentation_models)
+RUN apt-get update && \
+ apt-get install -y nginx curl gettext-base git && \
+ # Pour OpenCV si nécessaire
+ apt-get install -y libglib2.0-0 libsm6 libxext6 libxrender-dev libgomp1 && \
+ adduser --system --no-create-home --group www-data || true && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
+
+# Copy app/ d'abord pour avoir requirements.txt
+COPY app/ ./app/
+
+# Install requirements depuis le bon chemin
+RUN pip install --no-cache-dir -r app/requirements.txt
+
+# ✅ CORRECTION: Copy start.sh AVANT les permissions
+COPY start.sh ./start.sh
+
+# Copy du reste après installation (optimise les layers Docker)
+COPY utils/ ./utils/
+COPY cityscapes_config.json .
+COPY notebooks/content/data/test_images_sample/ ./notebooks/content/data/test_images_sample/
+COPY notebooks/content/data/train_images_sample/ ./notebooks/content/data/train_images_sample/
+
+# Copy nginx config
+COPY nginx.conf /etc/nginx/nginx.conf.template
+
+# ✅ CORRECTION: Setup permissions APRÈS avoir copié start.sh + correction fins de ligne
+RUN mkdir -p /var/log/nginx /var/lib/nginx /run && \
+ sed -i 's/\r$//' ./start.sh && \
+ chmod +x ./start.sh
+
+# Environment variables
+ENV NGINX_PORT=8080
+ENV MULTISEG_API_BASE_URL=http://localhost:8080/api
+
+EXPOSE $NGINX_PORT
+
+# ✅ CORRECTION: Chemin cohérent (./start.sh au lieu de /app/start.sh)
+CMD ["./start.sh"]
diff --git a/README.MD b/README.MD
index 3ea4ed0..6e14ca9 100644
--- a/README.MD
+++ b/README.MD
@@ -1,116 +1,116 @@
-# Street Vision PoC - Segmentation Sémantique Urbaine
-
-## 🎯 Description du Projet
-
-**Street Vision PoC** est une preuve de concept démontrant l'amélioration des performances de segmentation sémantique urbaine en comparant les architectures CNN traditionnelles (FPN) aux approches Transformer modernes (SegFormer).
-
-Ce projet évalue empiriquement le potentiel des **Vision Transformers** pour la compréhension de scènes urbaines complexes, avec un focus sur les classes critiques pour la sécurité (piétons, véhicules, signalétique).
-
-### 🔬 Contexte Technique
-
-- **Dataset** : Cityscapes (2,975 images haute résolution annotées pixel-par-pixel)
-- **Architectures comparées** :
- - **CNN traditionnel** : FPN + EfficientNet-B0/ResNet34
- - **Vision Transformer** : SegFormer-B0/B1
-- **Résolutions d'évaluation** : 512×512 et 768×768 pixels
-- **Métrique principale** : Mean Intersection over Union (mIoU)
-
-## 📊 Résultats Principaux
-
-| **Modèle** | **mIoU** | **Paramètres** | **Taille** | **Performance** |
-|------------|----------|----------------|------------|-----------------|
-| **SegFormer-B1** | **79.98%** | 13.68M | 52MB | ⭐ **Meilleur** |
-| **FPN+ResNet34** | 79.22% | 23.16M | 88MB | 🔄 Baseline forte |
-| **FPN+EfficientNet-B0** | 78.67% | 5.76M | 22MB | 💡 Compact |
-| **SegFormer-B0** | 77.91% | 3.72M | 14MB | ⚡ Ultra-léger |
-
-**Analyse comparative** : SegFormer-B1 et FPN+ResNet34 atteignent des performances similaires (~79-80% mIoU), mais SegFormer-B1 présente l'avantage d'être plus compact et probablement plus rapide sur GPU. Les versions SegFormer-B2 à B5 promettraient des gains significatifs, mais mes contraintes matérielles n'ont pas permis leur évaluation.
-
-### 🎯 Gains Significatifs par Classes Critiques
-
-- **Human** : +1.78% IoU (sécurité critique)
-- **Vehicle** : +1.05% IoU (obstacles mobiles)
-- **Object** : +0.56% IoU (signalétique urbaine)
-
-## 🚀 Déploiement Rapide
-
-### Docker Hub
-
-L'application est containerisée et disponible sur Docker Hub :
-
-```bash
-docker pull grgmdmn/street-vision-poc_api
-```
-
-### Configuration avec Docker Compose
-
-1. Copiez le fichier d'exemple de configuration :
-```bash
-cp docker-compose.yml.example docker-compose.yml
-```
-
-2. Configurez les variables d'environnement dans `docker-compose.yml`.
-
-3. Lancez l'application :
-```bash
-docker-compose up -d
-```
-
-4. Accédez à l'interface : `http://localhost:$(port)`
-
-## 🎮 Fonctionnalités de l'Interface
-
-### Dashboard Interactif
-
-- **Upload d'images** : Glisser-déposer d'images urbaines
-- **Double inférence** : Comparaison simultanée FPN vs SegFormer
-- **Visualisation** : Masques de segmentation colorés par classe
-- **Métriques temps réel** : Temps d'inférence et scores de confiance
-- **Export** : Téléchargement des résultats de segmentation
-- **Accessibilité WCAG** : Interface conforme aux standards d'accessibilité
-
-### Analyse Exploratoire des Données
-
-- **Statistiques Cityscapes** : Distribution des classes, résolutions natives
-- **Graphiques interactifs** : Répartition pixel par méta-classe
-- **Exemples annotés** : Visualisation échantillons du dataset
-
-## 📈 Méthodologie d'Évaluation
-
-### Approche Comparative
-
-1. **Baseline CNN** : Réentraînement FPN+EfficientNet/ResNet aux résolutions supérieures
-2. **Nouvelle approche** : Évaluation SegFormer avec attention globale
-3. **Protocole rigoureux** : Validation croisée, métriques standardisées
-4. **Analyse par classe** : Performance détaillée sur 8 méta-classes urbaines
-
-### Limites Identifiées
-
-- **Résolution fixe** : Évaluation sans sliding window (vs protocole original)
-- **Ressources limitées** : Tests uniquement sur SegFormer-B0/B1
-- **Conditions contrôlées** : Dataset Cityscapes uniquement (météo favorable)
-
-## 🔬 Insights Techniques
-
-### Avantages SegFormer
-
-- **Attention globale** : Chaque pixel "voit" toute l'image simultanément
-- **Architecture plus légère** : Modèle compact avec moins de paramètres
-- **Robustesse contextuelle** : Meilleure gestion occlusions et objets complexes
-- **Potentiel d'amélioration** : Versions B2-B5 promettraient des gains significatifs
-
-### Trade-offs Identifiés
-
-- **Vitesse CPU classique** : ~2× plus lent que FPN sur processeurs haute performance (Xeon, 9600X)
-- **Vitesse CPU économe** : 3× plus rapide sur processeurs économes en énergie (N100)
-- **Optimisation GPU** : Architecture Transformer favorisée par accélération matérielle
-
-## 📚 Références
-
-- **Xie, E. et al.** (2021). *SegFormer: Simple and Efficient Design for Semantic Segmentation with Transformers*. NeurIPS 2021.
-- **Cordts, M. et al.** (2016). *The Cityscapes Dataset for Semantic Urban Scene Understanding*. CVPR 2016.
-- **Segmentation Models PyTorch** - [Framework d'implémentation utilisé](https://github.com/qubvel/segmentation_models.pytorch)
-
----
-
+# Street Vision PoC - Segmentation Sémantique Urbaine
+
+## 🎯 Description du Projet
+
+**Street Vision PoC** est une preuve de concept démontrant l'amélioration des performances de segmentation sémantique urbaine en comparant les architectures CNN traditionnelles (FPN) aux approches Transformer modernes (SegFormer).
+
+Ce projet évalue empiriquement le potentiel des **Vision Transformers** pour la compréhension de scènes urbaines complexes, avec un focus sur les classes critiques pour la sécurité (piétons, véhicules, signalétique).
+
+### 🔬 Contexte Technique
+
+- **Dataset** : Cityscapes (2,975 images haute résolution annotées pixel-par-pixel)
+- **Architectures comparées** :
+ - **CNN traditionnel** : FPN + EfficientNet-B0/ResNet34
+ - **Vision Transformer** : SegFormer-B0/B1
+- **Résolutions d'évaluation** : 512×512 et 768×768 pixels
+- **Métrique principale** : Mean Intersection over Union (mIoU)
+
+## 📊 Résultats Principaux
+
+| **Modèle** | **mIoU** | **Paramètres** | **Taille** | **Performance** |
+|------------|----------|----------------|------------|-----------------|
+| **SegFormer-B1** | **79.98%** | 13.68M | 52MB | ⭐ **Meilleur** |
+| **FPN+ResNet34** | 79.22% | 23.16M | 88MB | 🔄 Baseline forte |
+| **FPN+EfficientNet-B0** | 78.67% | 5.76M | 22MB | 💡 Compact |
+| **SegFormer-B0** | 77.91% | 3.72M | 14MB | ⚡ Ultra-léger |
+
+**Analyse comparative** : SegFormer-B1 et FPN+ResNet34 atteignent des performances similaires (~79-80% mIoU), mais SegFormer-B1 présente l'avantage d'être plus compact et probablement plus rapide sur GPU. Les versions SegFormer-B2 à B5 promettraient des gains significatifs, mais mes contraintes matérielles n'ont pas permis leur évaluation.
+
+### 🎯 Gains Significatifs par Classes Critiques
+
+- **Human** : +1.78% IoU (sécurité critique)
+- **Vehicle** : +1.05% IoU (obstacles mobiles)
+- **Object** : +0.56% IoU (signalétique urbaine)
+
+## 🚀 Déploiement Rapide
+
+### Docker Hub
+
+L'application est containerisée et disponible sur Docker Hub :
+
+```bash
+docker pull grgmdmn/street-vision-poc_api
+```
+
+### Configuration avec Docker Compose
+
+1. Copiez le fichier d'exemple de configuration :
+```bash
+cp docker-compose.yml.example docker-compose.yml
+```
+
+2. Configurez les variables d'environnement dans `docker-compose.yml`.
+
+3. Lancez l'application :
+```bash
+docker-compose up -d
+```
+
+4. Accédez à l'interface : `http://localhost:$(port)`
+
+## 🎮 Fonctionnalités de l'Interface
+
+### Dashboard Interactif
+
+- **Upload d'images** : Glisser-déposer d'images urbaines
+- **Double inférence** : Comparaison simultanée FPN vs SegFormer
+- **Visualisation** : Masques de segmentation colorés par classe
+- **Métriques temps réel** : Temps d'inférence et scores de confiance
+- **Export** : Téléchargement des résultats de segmentation
+- **Accessibilité WCAG** : Interface conforme aux standards d'accessibilité
+
+### Analyse Exploratoire des Données
+
+- **Statistiques Cityscapes** : Distribution des classes, résolutions natives
+- **Graphiques interactifs** : Répartition pixel par méta-classe
+- **Exemples annotés** : Visualisation échantillons du dataset
+
+## 📈 Méthodologie d'Évaluation
+
+### Approche Comparative
+
+1. **Baseline CNN** : Réentraînement FPN+EfficientNet/ResNet aux résolutions supérieures
+2. **Nouvelle approche** : Évaluation SegFormer avec attention globale
+3. **Protocole rigoureux** : Validation croisée, métriques standardisées
+4. **Analyse par classe** : Performance détaillée sur 8 méta-classes urbaines
+
+### Limites Identifiées
+
+- **Résolution fixe** : Évaluation sans sliding window (vs protocole original)
+- **Ressources limitées** : Tests uniquement sur SegFormer-B0/B1
+- **Conditions contrôlées** : Dataset Cityscapes uniquement (météo favorable)
+
+## 🔬 Insights Techniques
+
+### Avantages SegFormer
+
+- **Attention globale** : Chaque pixel "voit" toute l'image simultanément
+- **Architecture plus légère** : Modèle compact avec moins de paramètres
+- **Robustesse contextuelle** : Meilleure gestion occlusions et objets complexes
+- **Potentiel d'amélioration** : Versions B2-B5 promettraient des gains significatifs
+
+### Trade-offs Identifiés
+
+- **Vitesse CPU classique** : ~2× plus lent que FPN sur processeurs haute performance (Xeon, 9600X)
+- **Vitesse CPU économe** : 3× plus rapide sur processeurs économes en énergie (N100)
+- **Optimisation GPU** : Architecture Transformer favorisée par accélération matérielle
+
+## 📚 Références
+
+- **Xie, E. et al.** (2021). *SegFormer: Simple and Efficient Design for Semantic Segmentation with Transformers*. NeurIPS 2021.
+- **Cordts, M. et al.** (2016). *The Cityscapes Dataset for Semantic Urban Scene Understanding*. CVPR 2016.
+- **Segmentation Models PyTorch** - [Framework d'implémentation utilisé](https://github.com/qubvel/segmentation_models.pytorch)
+
+---
+
**Note** : Cette preuve de concept démontre le potentiel des architectures Transformer pour la segmentation urbaine, tout en soulignant l'importance des ressources computationnelles pour exploiter pleinement leurs capacités.
\ No newline at end of file
diff --git a/app/README.MD b/app/README.MD
index b300824..7b27e38 100644
--- a/app/README.MD
+++ b/app/README.MD
@@ -1,108 +1,108 @@
-
-# Dashboard de Comparaison de Modèles
-
-## Conformité aux Exigences
-
-### Analyse Exploratoire des Données Non Structurées (Images)
-
-**Présentation d'exemples d'images selon les catégories :**
-- Visualisation d'images du dataset Cityscapes organisées par 8 méta-classes de segmentation
-- Sélection interactive d'images d'entraînement et de test avec prévisualisation
-- Interface de navigation par catégories avec noms d'affichage conviviaux
-
-**Comptage et répartition des données :**
-- Graphiques en secteurs (pie charts) détaillant la répartition des 25,000 images Cityscapes
-- Visualisation de l'utilisation projet : 80% train / 20% validation / 100% test
-- Graphique de distribution des classes dans le dataset d'entraînement
-- Tableaux récapitulatifs avec statistiques détaillées par split
-
-**Transformations des images :**
-- Démonstration interactive du preprocessing : redimensionnement et normalisation
-- Visualisation des augmentations de données aléatoires (rotation, flip, luminosité, etc.)
-- Comparaison avant/après transformations avec alignement visuel
-- Documentation complète des 6 types d'augmentations possibles
-
-### Sélection de Données d'Entrée
-
-**Interface de sélection multiple :**
-- Liste déroulante pour images test Cityscapes avec informations sur la vérité terrain
-- Upload de fichiers personnels (JPEG, PNG) avec validation de format
-- Prévisualisation des images sélectionnées avant traitement
-- Gestion des erreurs et feedback utilisateur en temps réel
-
-### Résultats de Prédiction
-
-**Comparaison complète de modèles :**
-- Affichage côte à côte des prédictions des deux meilleurs modèles
-- Visualisations avec image originale, masques prédits et vérité terrain
-- Détection et mise en évidence des erreurs de segmentation
-- Statistiques détaillées par classe avec pourcentages et nombre de pixels
-
-**Métriques de performance :**
-- Temps d'inférence comparatif avec analyse de vitesse
-- Scores de performance (mIoU, Accuracy) par modèle
-- IoU par classe pour analyse fine des performances
-- Graphiques comparatifs interactifs des résultats
-
-### Déploiement Cloud
-
-**Architecture déployée :**
-- Application FastAPI pour l'API de comparaison de modèles
-- Interface Streamlit pour le dashboard utilisateur
-- Déploiement sur infrastructure cloud avec haute disponibilité
-- Gestion automatique des ressources et montée en charge
-
-### Accessibilité WCAG
-
-**Conformité aux critères essentiels :**
-
-#### Contraste des Couleurs (WCAG AA)
-- Palette de couleurs avec ratios de contraste ≥ 4.1:1 pour éléments graphiques
-- Ratios de contraste ≥ 4.5:1 pour le texte sur fond
-- 8 couleurs principales validées : bleu (#0066CC), rouge (#CC0000), vert (#006600), etc.
-- Remplacement des dégradés par des couleurs unies à contraste élevé
-
-#### Alternatives Textuelles
-- Descriptions alternatives détaillées pour tous les graphiques générées automatiquement
-- Textes explicatifs pour chaque visualisation (pie charts, bar charts)
-- Labels ARIA appropriés pour les éléments interactifs
-- Documentation textuelle complète des transformations d'images
-
-#### Indication Non-Couleur Uniquement
-- Motifs et hachures complémentaires aux couleurs (optionnel via checkbox)
-- 6 motifs différents : diagonales, verticales, horizontales, points, croix
-- Différenciation par forme et texture en plus des couleurs
-- Bordures et contours renforcés pour la distinction visuelle
-
-#### Navigation Clavier
-- CSS focus visible avec contours bleus (#0066CC) de 2px
-- Support complet de la navigation par tabulation
-- Indicateurs visuels clairs pour les éléments focusés
-- Respect de l'ordre logique de navigation
-
-#### Tableaux de Données Accessibles
-- Tableaux structurés complémentaires à tous les graphiques
-- En-têtes appropriés avec scope et descriptions
-- Données exportables et lisibles par lecteurs d'écran
-- Configuration responsive pour tous les écrans
-
-**Fonctionnalités d'assistance intégrées :**
-- Checkbox "Activer les motifs (accessibilité visuelle)" pour personnalisation
-- Descriptions automatiques des graphiques avec statistiques complètes
-- Interface adaptable aux besoins des utilisateurs en situation de handicap
-- Documentation complète des fonctionnalités d'accessibilité via endpoint `/accessibility-info`
-
-## Architecture Technique
-
-**Stack technologique :**
-- Backend : FastAPI + PyTorch pour l'inférence des modèles
-- Frontend : Streamlit avec composants accessibles
-- Visualisations : Matplotlib avec palettes WCAG
-- API REST documentée avec validation Pydantic
-- Gestion d'état optimisée pour les comparaisons de modèles
-
-**Performance et fiabilité :**
-- Temps d'inférence optimisés (< 1 seconde par image)
-- Gestion mémoire efficace avec fermeture automatique des figures
-- Validation des formats et tailles de fichiers
-- Nettoyage automatique des fichiers temporaires
+
+# Dashboard de Comparaison de Modèles
+
+## Conformité aux Exigences
+
+### Analyse Exploratoire des Données Non Structurées (Images)
+
+**Présentation d'exemples d'images selon les catégories :**
+- Visualisation d'images du dataset Cityscapes organisées par 8 méta-classes de segmentation
+- Sélection interactive d'images d'entraînement et de test avec prévisualisation
+- Interface de navigation par catégories avec noms d'affichage conviviaux
+
+**Comptage et répartition des données :**
+- Graphiques en secteurs (pie charts) détaillant la répartition des 25,000 images Cityscapes
+- Visualisation de l'utilisation projet : 80% train / 20% validation / 100% test
+- Graphique de distribution des classes dans le dataset d'entraînement
+- Tableaux récapitulatifs avec statistiques détaillées par split
+
+**Transformations des images :**
+- Démonstration interactive du preprocessing : redimensionnement et normalisation
+- Visualisation des augmentations de données aléatoires (rotation, flip, luminosité, etc.)
+- Comparaison avant/après transformations avec alignement visuel
+- Documentation complète des 6 types d'augmentations possibles
+
+### Sélection de Données d'Entrée
+
+**Interface de sélection multiple :**
+- Liste déroulante pour images test Cityscapes avec informations sur la vérité terrain
+- Upload de fichiers personnels (JPEG, PNG) avec validation de format
+- Prévisualisation des images sélectionnées avant traitement
+- Gestion des erreurs et feedback utilisateur en temps réel
+
+### Résultats de Prédiction
+
+**Comparaison complète de modèles :**
+- Affichage côte à côte des prédictions des deux meilleurs modèles
+- Visualisations avec image originale, masques prédits et vérité terrain
+- Détection et mise en évidence des erreurs de segmentation
+- Statistiques détaillées par classe avec pourcentages et nombre de pixels
+
+**Métriques de performance :**
+- Temps d'inférence comparatif avec analyse de vitesse
+- Scores de performance (mIoU, Accuracy) par modèle
+- IoU par classe pour analyse fine des performances
+- Graphiques comparatifs interactifs des résultats
+
+### Déploiement Cloud
+
+**Architecture déployée :**
+- Application FastAPI pour l'API de comparaison de modèles
+- Interface Streamlit pour le dashboard utilisateur
+- Déploiement sur infrastructure cloud avec haute disponibilité
+- Gestion automatique des ressources et montée en charge
+
+### Accessibilité WCAG
+
+**Conformité aux critères essentiels :**
+
+#### Contraste des Couleurs (WCAG AA)
+- Palette de couleurs avec ratios de contraste ≥ 4.1:1 pour éléments graphiques
+- Ratios de contraste ≥ 4.5:1 pour le texte sur fond
+- 8 couleurs principales validées : bleu (#0066CC), rouge (#CC0000), vert (#006600), etc.
+- Remplacement des dégradés par des couleurs unies à contraste élevé
+
+#### Alternatives Textuelles
+- Descriptions alternatives détaillées pour tous les graphiques générées automatiquement
+- Textes explicatifs pour chaque visualisation (pie charts, bar charts)
+- Labels ARIA appropriés pour les éléments interactifs
+- Documentation textuelle complète des transformations d'images
+
+#### Indication Non-Couleur Uniquement
+- Motifs et hachures complémentaires aux couleurs (optionnel via checkbox)
+- 6 motifs différents : diagonales, verticales, horizontales, points, croix
+- Différenciation par forme et texture en plus des couleurs
+- Bordures et contours renforcés pour la distinction visuelle
+
+#### Navigation Clavier
+- CSS focus visible avec contours bleus (#0066CC) de 2px
+- Support complet de la navigation par tabulation
+- Indicateurs visuels clairs pour les éléments focusés
+- Respect de l'ordre logique de navigation
+
+#### Tableaux de Données Accessibles
+- Tableaux structurés complémentaires à tous les graphiques
+- En-têtes appropriés avec scope et descriptions
+- Données exportables et lisibles par lecteurs d'écran
+- Configuration responsive pour tous les écrans
+
+**Fonctionnalités d'assistance intégrées :**
+- Checkbox "Activer les motifs (accessibilité visuelle)" pour personnalisation
+- Descriptions automatiques des graphiques avec statistiques complètes
+- Interface adaptable aux besoins des utilisateurs en situation de handicap
+- Documentation complète des fonctionnalités d'accessibilité via endpoint `/accessibility-info`
+
+## Architecture Technique
+
+**Stack technologique :**
+- Backend : FastAPI + PyTorch pour l'inférence des modèles
+- Frontend : Streamlit avec composants accessibles
+- Visualisations : Matplotlib avec palettes WCAG
+- API REST documentée avec validation Pydantic
+- Gestion d'état optimisée pour les comparaisons de modèles
+
+**Performance et fiabilité :**
+- Temps d'inférence optimisés (< 1 seconde par image)
+- Gestion mémoire efficace avec fermeture automatique des figures
+- Validation des formats et tailles de fichiers
+- Nettoyage automatique des fichiers temporaires
diff --git a/app/main.py b/app/main.py
index ad66b93..a0c8cc8 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,834 +1,834 @@
-import os
-import sys
-import traceback
-import tempfile
-import shutil
-from datetime import datetime
-from typing import Optional, List
-from pathlib import Path
-import mlflow
-from fastapi import FastAPI, HTTPException, File, UploadFile
-from fastapi.responses import FileResponse
-from pydantic import BaseModel
-import numpy as np
-import pickle
-import base64
-import matplotlib.pyplot as plt
-import matplotlib.image as mpimg
-
-
-# Configuration MLflow avec variables d'environnement
-def configure_mlflow():
- """Configure MLflow avec les variables d'environnement"""
- print("=== DEBUG VARIABLES D'ENVIRONNEMENT ===")
-
- mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI", "").strip()
- mlflow_s3_endpoint_url = os.getenv("MLFLOW_S3_ENDPOINT_URL", "").strip()
- aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID", "").strip()
- aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY", "").strip()
-
- print(f"MLFLOW_TRACKING_URI: {mlflow_tracking_uri}")
- print(f"MLFLOW_S3_ENDPOINT_URL: {mlflow_s3_endpoint_url}")
- print(f"AWS_ACCESS_KEY_ID: {aws_access_key_id}")
- print(f"AWS_SECRET_ACCESS_KEY: {'***' if aws_secret_access_key else None} ({'set' if aws_secret_access_key else 'not set'})")
- print("============================================")
-
- if not mlflow_tracking_uri:
- raise ValueError("MLFLOW_TRACKING_URI not found in environment variables")
-
- mlflow.set_tracking_uri(mlflow_tracking_uri)
- print(f"MLflow Tracking URI: {mlflow_tracking_uri}")
-
- if not mlflow_s3_endpoint_url or not aws_access_key_id or not aws_secret_access_key:
- raise ValueError("Variables d'environnement MLflow manquantes (S3_ENDPOINT, ACCESS_KEY, SECRET_KEY)")
-
- os.environ["MLFLOW_S3_ENDPOINT_URL"] = mlflow_s3_endpoint_url
- os.environ["AWS_ACCESS_KEY_ID"] = aws_access_key_id
- os.environ["AWS_SECRET_ACCESS_KEY"] = aws_secret_access_key
- print(f"MLflow S3 Endpoint: {mlflow_s3_endpoint_url}")
- print("✅ Identifiants AWS configurés")
- print("✅ Configuration MLflow terminée")
-
-# Import des fonctions utils
-sys.path.append('.')
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-from utils.utils import (
- load_cityscapes_config,
- load_model,
- run_inference_and_visualize,
- colorize_mask
-)
-
-# Configuration
-MODEL_NAME = "StreetsSegmentation"
-SAMPLE_IMAGES_DIR = "./notebooks/content/data/test_images_sample"
-TRAIN_IMAGES_DIR = "./notebooks/content/data/train_images_sample"
-CITYSCAPES_CONFIG_PATH = "./cityscapes_config.json"
-TEMP_UPLOAD_DIR = "/tmp/uploads"
-
-# Modèles de données mis à jour
-class ModelInfo(BaseModel):
- name: str
- run_id: str
- encoder_name: str
- input_size: tuple
- rank: int
- architecture: Optional[str] = None
- test_mean_iou: Optional[float] = None
- test_accuracy: Optional[float] = None
- class_ious: Optional[dict] = None
-
-class ComparisonResult(BaseModel):
- success: bool
- message: str
- model1_info: ModelInfo
- model2_info: ModelInfo
- image_path: Optional[str] = None
- mask_path: Optional[str] = None
- model1_stats: Optional[dict] = None
- model2_stats: Optional[dict] = None
- model1_inference_time: float
- model2_inference_time: float
- speed_comparison: dict
- ground_truth_available: bool = False
- figure_data: Optional[str] = None
-
-class ImageInfo(BaseModel):
- filename: str
- display_name: str
- has_ground_truth: bool
-
-class AvailableImagesResponse(BaseModel):
- images: List[ImageInfo]
- total_count: int
-
-class PreprocessingRequest(BaseModel):
- filename: str
- apply_augmentation: bool = False
-
-class PreprocessingResult(BaseModel):
- success: bool
- message: str
- original_image: Optional[str] = None
- normalized_image: Optional[str] = None
- augmented_image: Optional[str] = None
- transformations_applied: Optional[list] = None
-
-class SampleImageRequest(BaseModel):
- filename: str
-
-# Variables globales pour les deux modèles
-best_model = None
-second_best_model = None
-best_model_info = {}
-second_best_model_info = {}
-mapping_config = None
-
-def get_model_detailed_info(run_id, mapping_config):
- """Récupère les informations détaillées d'un modèle depuis MLflow"""
- client = mlflow.tracking.MlflowClient()
-
- try:
- run = mlflow.get_run(run_id)
- params = run.data.params
- metrics = run.data.metrics
-
- # Architecture depuis le paramètre model_architecture
- architecture = params.get("model_architecture", "Unknown").split('_')[0]
-
- # Métriques de performance
- test_mean_iou = metrics.get("test_mean_iou", None)
- test_accuracy = metrics.get("test_accuracy", None)
-
- # IoU par classe depuis les métriques class_X_iou
- class_ious = {}
- if mapping_config and 'group_names' in mapping_config:
- for i, class_name in enumerate(mapping_config['group_names']):
- metric_key = f"class_{i}_iou"
- if metric_key in metrics:
- class_ious[class_name] = round(metrics[metric_key], 3)
-
- return {
- "architecture": architecture,
- "test_mean_iou": test_mean_iou,
- "test_accuracy": test_accuracy,
- "class_ious": class_ious
- }
- except Exception as e:
- print(f"⚠️ Erreur lors de la récupération des infos détaillées: {e}")
- return {
- "architecture": "Unknown",
- "test_mean_iou": None,
- "test_accuracy": None,
- "class_ious": {}
- }
-
-def initialize_models():
- """Initialise les deux meilleurs modèles au démarrage"""
- global best_model, second_best_model, best_model_info, second_best_model_info, mapping_config
-
- try:
- print("🔄 Configuration de MLflow...")
- configure_mlflow()
-
- print("🔄 Chargement de la configuration Cityscapes avec couleurs WCAG...")
- mapping_config = load_cityscapes_config(CITYSCAPES_CONFIG_PATH, verbose=False, use_wcag_colors=True)
- print("✅ Configuration Cityscapes chargée avec couleurs WCAG")
-
-
- print("🔄 Chargement du meilleur modèle...")
- best_model, best_run_id, best_encoder_name, best_img_size = load_model(
- experiment_name="OC Projet 9",
- metric="test_mean_iou",
- top_n=1
- )
-
- # Récupérer les infos détaillées du meilleur modèle
- best_detailed_info = get_model_detailed_info(best_run_id, mapping_config)
-
- best_model_info = {
- "name": "Meilleur modèle",
- "run_id": best_run_id,
- "encoder_name": best_encoder_name,
- "input_size": best_img_size,
- "rank": 1,
- "architecture": best_detailed_info["architecture"],
- "test_mean_iou": best_detailed_info["test_mean_iou"],
- "test_accuracy": best_detailed_info["test_accuracy"],
- "class_ious": best_detailed_info["class_ious"]
- }
-
- print("🔄 Chargement du deuxième meilleur modèle...")
- second_best_model, second_run_id, second_encoder_name, second_img_size = load_model(
- experiment_name="OC Projet 9",
- metric="test_mean_iou",
- top_n=2
- )
-
- # Récupérer les infos détaillées du deuxième modèle
- second_detailed_info = get_model_detailed_info(second_run_id, mapping_config)
-
- second_best_model_info = {
- "name": "Deuxième modèle",
- "run_id": second_run_id,
- "encoder_name": second_encoder_name,
- "input_size": second_img_size,
- "rank": 2,
- "architecture": second_detailed_info["architecture"],
- "test_mean_iou": second_detailed_info["test_mean_iou"],
- "test_accuracy": second_detailed_info["test_accuracy"],
- "class_ious": second_detailed_info["class_ious"]
- }
-
- os.makedirs(TEMP_UPLOAD_DIR, exist_ok=True)
-
- except Exception as e:
- print(f"❌ Erreur lors de l'initialisation: {e}")
- traceback.print_exc()
- raise e
-
-def get_accessibility_info():
- """Retourne les informations d'accessibilité pour le frontend"""
- return {
- "wcag_compliant": True,
- "color_palette": {
- "primary_blue": "#0066CC",
- "primary_red": "#CC0000",
- "primary_green": "#006600",
- "primary_orange": "#CC6600",
- "secondary_purple": "#663399",
- "secondary_teal": "#006666",
- "neutral_dark": "#333333",
- "neutral_medium": "#666666"
- },
- "contrast_ratios": {
- "primary_colors_vs_white": "≥ 4.1:1 (WCAG AA)",
- "text_vs_background": "≥ 4.5:1 (WCAG AA)"
- },
- "features": [
- "Couleurs à contraste élevé",
- "Motifs alternatifs aux couleurs",
- "Descriptions textuelles alternatives en cas d'absence d'images",
- "Tableaux de données complémentaires",
- "Navigation clavier supportée"
- ]
- }
-
-
-# Initialisation de l'API
-app = FastAPI(
- title="Multi-Class Segmentation Comparison API",
- description="API de comparaison de segmentation sémantique avec deux modèles",
- version="2.0.0"
-)
-
-@app.on_event("startup")
-async def startup_event():
- try:
- initialize_models()
- print("🚀 API démarrée avec succès - Modèles chargés!")
- except Exception as e:
- print(f"⚠️ ATTENTION: Impossible de charger les modèles au démarrage: {e}")
-
-@app.get("/")
-def root():
- return {
- "message": "Multi-Class Segmentation Comparison API - Online",
- "models_loaded": best_model is not None and second_best_model is not None,
- "best_model": best_model_info if best_model else None,
- "second_best_model": second_best_model_info if second_best_model else None,
- "num_classes": mapping_config['num_classes'] if mapping_config else None
- }
-
-@app.get("/health")
-def health_check():
- if best_model is None or second_best_model is None or mapping_config is None:
- raise HTTPException(status_code=503, detail="Models not loaded")
- return {
- "status": "healthy",
- "models_loaded": True,
- "timestamp": datetime.utcnow().isoformat()
- }
-
-
-# Modifier le endpoint /models pour inclure des couleurs accessibles :
-@app.get("/models", response_model=dict)
-def get_models_info():
- if best_model is None or second_best_model is None:
- raise HTTPException(status_code=503, detail="Models not loaded")
-
- # NOUVELLE RÉPARTITION DES COULEURS selon votre mapping final
- wcag_accessible_colors = [
- [102, 102, 102], # Flat: neutral_medium (gris moyen pour routes/trottoirs)
- [204, 0, 0], # Human: primary_red (rouge pour piétons/cyclistes)
- [102, 51, 153], # Vehicle: secondary_purple (violet pour véhicules)
- [204, 102, 0], # Construction: primary_orange (orange pour bâtiments)
- [0, 102, 102], # Object: secondary_teal (teal pour objets/signalétique)
- [0, 102, 0], # Nature: primary_green (vert pour végétation)
- [0, 102, 204], # Sky: primary_blue (bleu pour ciel)
- [51, 51, 51] # Void: neutral_dark (gris foncé pour zones non-définies)
- ]
-
- # Mettre à jour la configuration avec les nouvelles couleurs
- if mapping_config:
- mapping_config['group_colors'] = np.array(wcag_accessible_colors, dtype=np.uint8)
-
- return {
- "best_model": best_model_info,
- "second_best_model": second_best_model_info,
- "num_classes": mapping_config['num_classes'],
- "class_names": mapping_config['group_names'],
- "class_colors": wcag_accessible_colors,
- "accessibility_info": get_accessibility_info(),
- "preprocessing_resolution": best_model_info.get("input_size", (512, 512))
- }
-
-
-
-@app.get("/dataset-info")
-def get_dataset_info():
- """Retourne les informations sur le dataset"""
- return {
- "name": "Cityscapes Dataset",
- "description": "Référence internationale pour l'évaluation des algorithmes de segmentation sémantique en environnement urbain",
- "total_images": 25000,
- "resolution": "2048×1024 pixels",
- "cities": 50,
- "countries": ["Allemagne", "Suisse"],
- "splits": {
- "train": {"images": 2975, "usage": "Entraînement avec annotations fines"},
- "validation": {"images": 500, "usage": "Validation avec annotations de référence"},
- "test": {"images": 1525, "usage": "Test (annotations non-publiques)"},
- "coarse": {"images": 20000, "usage": "Entraînement avec annotations grossières"}
- },
- "project_splits": {
- "train": {"images": 2380, "percentage": 80, "source": "Cityscapes train"},
- "validation": {"images": 595, "percentage": 20, "source": "Cityscapes train"},
- "test": {"images": 500, "percentage": 100, "source": "Cityscapes val"}
- },
- "meta_classes": mapping_config['group_names'] if mapping_config else [],
- "num_classes": mapping_config['num_classes'] if mapping_config else 0
- }
-
-@app.get("/train-images", response_model=AvailableImagesResponse)
-def get_train_images():
- """Retourne la liste des images d'entraînement disponibles"""
- try:
- original_dir = Path(TRAIN_IMAGES_DIR) / "original"
- mask_dir = Path(TRAIN_IMAGES_DIR) / "mask"
-
- if not original_dir.exists():
- raise HTTPException(status_code=404, detail="Train images directory not found")
-
- images = []
- for img_path in original_dir.glob("*leftImg8bit.png"):
- # Construire le nom du masque correspondant
- base_name = img_path.stem.replace("_leftImg8bit", "")
- mask_name = f"{base_name}_gtFine_labelIds.png"
- mask_path = mask_dir / mask_name
-
- # Créer un nom d'affichage plus convivial
- display_name = base_name.replace("_", " ").title()
-
- images.append(ImageInfo(
- filename=img_path.name,
- display_name=display_name,
- has_ground_truth=mask_path.exists()
- ))
-
- # Trier par nom pour un affichage cohérent
- images.sort(key=lambda x: x.filename)
-
- return AvailableImagesResponse(
- images=images,
- total_count=len(images)
- )
-
- except Exception as e:
- raise HTTPException(status_code=500, detail=f"Error listing train images: {str(e)}")
-
-@app.get("/train-image/{filename}")
-def get_train_image(filename: str):
- """Sert une image d'entraînement pour prévisualisation"""
- try:
- original_dir = Path(TRAIN_IMAGES_DIR) / "original"
- image_path = original_dir / filename
-
- if not image_path.exists():
- raise HTTPException(status_code=404, detail=f"Train image {filename} not found")
-
- # Vérifier que c'est bien un fichier image attendu
- if not filename.endswith('_leftImg8bit.png'):
- raise HTTPException(status_code=400, detail="Invalid image filename format")
-
- return FileResponse(
- image_path,
- media_type="image/png",
- headers={"Content-Disposition": f"inline; filename={filename}"}
- )
-
- except HTTPException:
- raise
- except Exception as e:
- raise HTTPException(status_code=500, detail=f"Error serving train image: {str(e)}")
-
-@app.get("/class-repartition")
-def get_class_repartition():
- """Sert le graphique de répartition des classes"""
- try:
- graph_path = Path(TRAIN_IMAGES_DIR) / "class_repartition.png"
-
- if not graph_path.exists():
- raise HTTPException(status_code=404, detail="Class repartition graph not found")
-
- return FileResponse(
- graph_path,
- media_type="image/png",
- headers={"Content-Disposition": "inline; filename=class_repartition.png"}
- )
-
- except HTTPException:
- raise
- except Exception as e:
- raise HTTPException(status_code=500, detail=f"Error serving class repartition graph: {str(e)}")
-
-@app.post("/preprocess-train-image", response_model=PreprocessingResult)
-def preprocess_train_image(request: PreprocessingRequest):
- """Prétraite une image d'entraînement et retourne les versions transformées"""
- if mapping_config is None:
- raise HTTPException(status_code=503, detail="Configuration not loaded")
-
- # Vérifier que le meilleur modèle est chargé
- if best_model is None or not best_model_info:
- raise HTTPException(status_code=503, detail="Best model not loaded")
-
- try:
- original_dir = Path(TRAIN_IMAGES_DIR) / "original"
- image_path = original_dir / request.filename
-
- if not image_path.exists():
- raise HTTPException(status_code=404, detail=f"Train image {request.filename} not found")
-
- # Importer les fonctions nécessaires
- import numpy as np
- import matplotlib.pyplot as plt
- from PIL import Image
- import io
- import base64
- from utils.utils import get_preprocessing_fn, apply_random_augmentation
-
- # Charger l'image originale
- original_pil = Image.open(image_path).convert('RGB')
- original_np = np.array(original_pil)
-
- # UTILISER LA RÉSOLUTION DU MEILLEUR MODÈLE au lieu de (512, 512)
- target_size = best_model_info.get("input_size", (512, 512))
- print(f"🎯 Utilisation de la taille du meilleur modèle: {target_size}")
-
- # Appliquer le preprocessing avec la résolution du meilleur modèle
- preprocess_fn = get_preprocessing_fn(img_size=target_size, encoder=None)
- normalized_tensor, _ = preprocess_fn(original_np, None)
-
- # Convertir le tensor en image pour affichage
- normalized_np = normalized_tensor.permute(1, 2, 0).numpy()
- # Dénormaliser pour l'affichage (tensor était normalisé 0-1)
- normalized_display = (normalized_np * 255).astype(np.uint8)
-
- # Créer les images en base64 pour l'envoi
- def img_to_base64(img_array):
- img_pil = Image.fromarray(img_array)
- buffer = io.BytesIO()
- img_pil.save(buffer, format='PNG')
- return base64.b64encode(buffer.getvalue()).decode()
-
- # Image originale redimensionnée pour comparaison (avec la taille du meilleur modèle)
- original_resized = np.array(original_pil.resize(target_size))
-
- result = PreprocessingResult(
- success=True,
- message="Preprocessing completed successfully",
- original_image=img_to_base64(original_resized),
- normalized_image=img_to_base64(normalized_display),
- transformations_applied=[f"Resize {target_size}", "Normalization (0-1)"]
- )
-
- # Appliquer l'augmentation si demandée
- if request.apply_augmentation:
- augmented_np, transforms_applied = apply_random_augmentation(original_np)
- # Appliquer aussi le preprocessing à l'image augmentée
- augmented_tensor, _ = preprocess_fn(augmented_np, None)
- augmented_display = (augmented_tensor.permute(1, 2, 0).numpy() * 255).astype(np.uint8)
-
- result.augmented_image = img_to_base64(augmented_display)
- result.transformations_applied.extend(transforms_applied)
-
- return result
-
- except Exception as e:
- raise HTTPException(status_code=500, detail=f"Preprocessing error: {str(e)}")
-
-@app.get("/best-model-resolution")
-def get_best_model_resolution():
- """Retourne la résolution utilisée par le meilleur modèle"""
- if best_model is None or not best_model_info:
- raise HTTPException(status_code=503, detail="Best model not loaded")
-
- return {
- "model_name": best_model_info.get("name", "Unknown"),
- "encoder_name": best_model_info.get("encoder_name", "Unknown"),
- "input_size": best_model_info.get("input_size", (512, 512)),
- "architecture": best_model_info.get("architecture", "Unknown")
- }
-
-@app.get("/sample-images", response_model=AvailableImagesResponse)
-def get_sample_images():
- try:
- original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
- mask_dir = Path(SAMPLE_IMAGES_DIR) / "mask"
- if not original_dir.exists():
- raise HTTPException(status_code=404, detail="Sample images directory not found")
- images = []
- for img_path in original_dir.glob("*leftImg8bit.png"):
- base_name = img_path.stem.replace("_leftImg8bit", "")
- mask_name = f"{base_name}_gtFine_labelIds.png"
- mask_path = mask_dir / mask_name
- display_name = base_name.replace("_", " ").title()
- images.append(ImageInfo(
- filename=img_path.name,
- display_name=display_name,
- has_ground_truth=mask_path.exists()
- ))
- images.sort(key=lambda x: x.filename)
- return AvailableImagesResponse(images=images, total_count=len(images))
- except Exception as e:
- raise HTTPException(status_code=500, detail=f"Error listing sample images: {str(e)}")
-
-@app.get("/sample-image/{filename}")
-def get_sample_image(filename: str):
- try:
- original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
- image_path = original_dir / filename
- if not image_path.exists():
- raise HTTPException(status_code=404, detail=f"Sample image {filename} not found")
- if not filename.endswith('_leftImg8bit.png'):
- raise HTTPException(status_code=400, detail="Invalid image filename format")
- return FileResponse(
- image_path,
- media_type="image/png",
- headers={"Content-Disposition": f"inline; filename={filename}"}
- )
- except HTTPException:
- raise
- except Exception as e:
- raise HTTPException(status_code=500, detail=f"Error serving image: {str(e)}")
-
-@app.post("/compare-sample", response_model=ComparisonResult)
-def compare_sample_image(request: SampleImageRequest):
- if best_model is None or second_best_model is None or mapping_config is None:
- raise HTTPException(status_code=503, detail="Models not loaded")
- try:
- original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
- image_path = original_dir / request.filename
- if not image_path.exists():
- raise HTTPException(status_code=404, detail=f"Sample image {request.filename} not found")
- base_name = image_path.stem.replace("_leftImg8bit", "")
- mask_name = f"{base_name}_gtFine_labelIds.png"
- mask_path = Path(SAMPLE_IMAGES_DIR) / "mask" / mask_name
- mask_path_str = str(mask_path) if mask_path.exists() else None
-
- # Inférence modèle 1 avec sa résolution spécifique
- pred1, t1, fig1 = run_inference_and_visualize(
- image_path=str(image_path),
- mask_path=mask_path_str,
- model=best_model,
- encoder_name=None, # Normalisé de la même manière
- img_size=best_model_info["input_size"], # Utilise la résolution du modèle
- mapping_config=mapping_config,
- save_dir=None, show_stats=False, show_plot=False
- )
- # Inférence modèle 2 avec sa résolution spécifique
- pred2, t2, fig2 = run_inference_and_visualize(
- image_path=str(image_path),
- mask_path=mask_path_str,
- model=second_best_model,
- encoder_name=None, # Normalisé de la même manière
- img_size=second_best_model_info["input_size"], # Utilise la résolution du modèle
- mapping_config=mapping_config,
- save_dir=None, show_stats=False, show_plot=False
- )
-
- # Créer une figure combinée avec les deux modèles
- # Nombre de colonnes : 3 si pas de GT, 4 si GT disponible
- num_cols = 3 if not (mask_path_str and os.path.exists(mask_path_str)) else 4
- fig, axes = plt.subplots(2, num_cols, figsize=(5*num_cols, 8))
-
- # Récupérer les images depuis les figures individuelles
- axes1 = fig1.get_axes()
- axes2 = fig2.get_axes()
-
- # Ligne 1 : Modèle 1
- for i in range(min(len(axes1), num_cols)):
- ax_src = axes1[i]
- ax_dst = axes[0, i]
-
- # Copier toutes les images de l'axe source
- for img_obj in ax_src.get_images():
- ax_dst.imshow(
- img_obj.get_array(),
- cmap=img_obj.get_cmap(),
- alpha=img_obj.get_alpha(),
- vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
- vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
- )
-
- # Copier le titre avec nom du modèle
- original_title = ax_src.get_title()
- if i == 0:
- new_title = "Image originale"
- elif "Prédit" in original_title:
- new_title = f"Prédiction {best_model_info['architecture']} - {best_model_info['encoder_name']}"
- else:
- new_title = original_title
-
- ax_dst.set_title(new_title)
- ax_dst.axis('off')
-
- # Ligne 2 : Modèle 2
- for i in range(min(len(axes2), num_cols)):
- ax_src = axes2[i]
- ax_dst = axes[1, i]
-
- # Copier toutes les images de l'axe source
- for img_obj in ax_src.get_images():
- ax_dst.imshow(
- img_obj.get_array(),
- cmap=img_obj.get_cmap(),
- alpha=img_obj.get_alpha(),
- vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
- vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
- )
-
- # Copier le titre avec nom du modèle
- original_title = ax_src.get_title()
- if i == 0:
- new_title = "Image originale"
- elif "Prédit" in original_title:
- new_title = f"Prédiction {second_best_model_info['architecture']} - {second_best_model_info['encoder_name']}"
- else:
- new_title = original_title
-
- ax_dst.set_title(new_title)
- ax_dst.axis('off')
-
- plt.tight_layout()
-
- # Fermer les figures individuelles pour libérer la mémoire
- plt.close(fig1)
- plt.close(fig2)
-
- fig_b64 = base64.b64encode(pickle.dumps(fig)).decode()
- # Stats
- def calc(mask):
- uc, cnts = np.unique(mask, return_counts=True)
- stats = {}
- for cid, c in zip(uc, cnts):
- if cid < len(mapping_config['group_names']):
- stats[mapping_config['group_names'][cid]] = {
- "pixels": int(c),
- "percentage": round((c/mask.size)*100, 1)
- }
- return stats
- stats1, stats2 = calc(pred1), calc(pred2)
- faster = best_model_info['encoder_name'] if t1 < t2 else second_best_model_info['encoder_name']
- speedup = round(max(t1, t2) / min(t1, t2), 2)
- speed_comp = {"faster_model": faster, "speedup_ratio": speedup, "time_difference_ms": abs(t1-t2)*1000}
- return ComparisonResult(
- success=True, message="Comparison completed successfully",
- model1_info=ModelInfo(**best_model_info), model2_info=ModelInfo(**second_best_model_info),
- image_path=str(image_path), mask_path=mask_path_str,
- model1_stats=stats1, model2_stats=stats2,
- model1_inference_time=t1, model2_inference_time=t2,
- speed_comparison=speed_comp, ground_truth_available=mask_path.exists(),
- figure_data=fig_b64
- )
- except Exception as e:
- raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
-
-@app.post("/compare-upload", response_model=ComparisonResult)
-def compare_uploaded_image(file: UploadFile = File(...)):
- if best_model is None or second_best_model is None or mapping_config is None:
- raise HTTPException(status_code=503, detail="Models not loaded")
- allowed_types = ["image/jpeg", "image/jpg", "image/png"]
- if file.content_type not in allowed_types:
- raise HTTPException(status_code=400, detail=f"Invalid file type. Allowed: {', '.join(allowed_types)}")
- temp_file_path = None
- try:
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- temp_filename = f"upload_{timestamp}_{file.filename}"
- temp_file_path = os.path.join(TEMP_UPLOAD_DIR, temp_filename)
- with open(temp_file_path, "wb") as buffer:
- shutil.copyfileobj(file.file, buffer)
- # Inférences avec résolutions spécifiques
- pred1, t1, fig1 = run_inference_and_visualize(
- image_path=temp_file_path, mask_path=None,
- model=best_model, encoder_name=None, # Normalisé de la même manière
- img_size=best_model_info["input_size"], mapping_config=mapping_config,
- save_dir=None, show_stats=False, show_plot=False,
- )
- pred2, t2, fig2 = run_inference_and_visualize(
- image_path=temp_file_path, mask_path=None,
- model=second_best_model, encoder_name=None, # Normalisé de la même manière
- img_size=second_best_model_info["input_size"], mapping_config=mapping_config,
- save_dir=None, show_stats=False, show_plot=False,
- )
-
- # Figure combinée pour uploads (pas de GT, donc 2 colonnes seulement)
- fig, axes = plt.subplots(2, 2, figsize=(10, 8))
-
- # Récupérer les images depuis les figures individuelles
- axes1 = fig1.get_axes()
- axes2 = fig2.get_axes()
-
- # Ligne 1 : Modèle 1 (Image originale + Prédiction)
- for i in range(min(len(axes1), 2)):
- ax_src = axes1[i]
- ax_dst = axes[0, i]
-
- # Copier toutes les images de l'axe source
- for img_obj in ax_src.get_images():
- ax_dst.imshow(
- img_obj.get_array(),
- cmap=img_obj.get_cmap(),
- alpha=img_obj.get_alpha(),
- vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
- vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
- )
-
- # Copier le titre avec nom du modèle
- original_title = ax_src.get_title()
- if i == 0:
- new_title = "Image originale"
- else:
- new_title = f"Prédiction {best_model_info['architecture']} - {best_model_info['encoder_name']}"
-
- ax_dst.set_title(new_title)
- ax_dst.axis('off')
-
- # Ligne 2 : Modèle 2 (Image originale + Prédiction)
- for i in range(min(len(axes2), 2)):
- ax_src = axes2[i]
- ax_dst = axes[1, i]
-
- # Copier toutes les images de l'axe source
- for img_obj in ax_src.get_images():
- ax_dst.imshow(
- img_obj.get_array(),
- cmap=img_obj.get_cmap(),
- alpha=img_obj.get_alpha(),
- vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
- vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
- )
-
- # Copier le titre avec nom du modèle
- original_title = ax_src.get_title()
- if i == 0:
- new_title = "Image originale"
- else:
- new_title = f"Prédiction {second_best_model_info['architecture']} - {second_best_model_info['encoder_name']}"
-
- ax_dst.set_title(new_title)
- ax_dst.axis('off')
-
- plt.tight_layout()
-
- # Fermer les figures individuelles pour libérer la mémoire
- plt.close(fig1)
- plt.close(fig2)
-
- fig_b64 = base64.b64encode(pickle.dumps(fig)).decode()
- # Stats
- def calc(mask):
- uc, cnts = np.unique(mask, return_counts=True)
- stats = {}
- for cid, c in zip(uc, cnts):
- if cid < len(mapping_config['group_names']):
- stats[mapping_config['group_names'][cid]] = {
- "pixels": int(c),
- "percentage": round((c/mask.size)*100, 1)
- }
- return stats
- stats1, stats2 = calc(pred1), calc(pred2)
- faster = best_model_info['encoder_name'] if t1 < t2 else second_best_model_info['encoder_name']
- speedup = round(max(t1, t2) / min(t1, t2), 2)
- speed_comp = {"faster_model": faster, "speedup_ratio": speedup, "time_difference_ms": abs(t1-t2)*1000}
- return ComparisonResult(
- success=True, message="Comparison completed successfully",
- model1_info=ModelInfo(**best_model_info), model2_info=ModelInfo(**second_best_model_info),
- image_path=temp_file_path,
- model1_stats=stats1, model2_stats=stats2,
- model1_inference_time=t1, model2_inference_time=t2,
- speed_comparison=speed_comp, ground_truth_available=False,
- figure_data=fig_b64
- )
- except Exception as e:
- raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
- finally:
- if temp_file_path and os.path.exists(temp_file_path):
- try: os.remove(temp_file_path)
- except Exception: pass
-
-@app.get("/metrics")
-def get_metrics():
- return {
- "models_loaded": best_model is not None and second_best_model is not None,
- "best_model_info": best_model_info,
- "second_best_model_info": second_best_model_info,
- "num_classes": mapping_config['num_classes'] if mapping_config else None,
- "timestamp": datetime.utcnow().isoformat()
- }
-
-# Ajouter cette route après les autres routes :
-@app.get("/accessibility-info")
-def get_accessibility_information():
- """Retourne les informations sur la conformité d'accessibilité"""
+import os
+import sys
+import traceback
+import tempfile
+import shutil
+from datetime import datetime
+from typing import Optional, List
+from pathlib import Path
+import mlflow
+from fastapi import FastAPI, HTTPException, File, UploadFile
+from fastapi.responses import FileResponse
+from pydantic import BaseModel
+import numpy as np
+import pickle
+import base64
+import matplotlib.pyplot as plt
+import matplotlib.image as mpimg
+
+
+# Configuration MLflow avec variables d'environnement
+def configure_mlflow():
+ """Configure MLflow avec les variables d'environnement"""
+ print("=== DEBUG VARIABLES D'ENVIRONNEMENT ===")
+
+ mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI", "").strip()
+ mlflow_s3_endpoint_url = os.getenv("MLFLOW_S3_ENDPOINT_URL", "").strip()
+ aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID", "").strip()
+ aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY", "").strip()
+
+ print(f"MLFLOW_TRACKING_URI: {mlflow_tracking_uri}")
+ print(f"MLFLOW_S3_ENDPOINT_URL: {mlflow_s3_endpoint_url}")
+ print(f"AWS_ACCESS_KEY_ID: {aws_access_key_id}")
+ print(f"AWS_SECRET_ACCESS_KEY: {'***' if aws_secret_access_key else None} ({'set' if aws_secret_access_key else 'not set'})")
+ print("============================================")
+
+ if not mlflow_tracking_uri:
+ raise ValueError("MLFLOW_TRACKING_URI not found in environment variables")
+
+ mlflow.set_tracking_uri(mlflow_tracking_uri)
+ print(f"MLflow Tracking URI: {mlflow_tracking_uri}")
+
+ if not mlflow_s3_endpoint_url or not aws_access_key_id or not aws_secret_access_key:
+ raise ValueError("Variables d'environnement MLflow manquantes (S3_ENDPOINT, ACCESS_KEY, SECRET_KEY)")
+
+ os.environ["MLFLOW_S3_ENDPOINT_URL"] = mlflow_s3_endpoint_url
+ os.environ["AWS_ACCESS_KEY_ID"] = aws_access_key_id
+ os.environ["AWS_SECRET_ACCESS_KEY"] = aws_secret_access_key
+ print(f"MLflow S3 Endpoint: {mlflow_s3_endpoint_url}")
+ print("✅ Identifiants AWS configurés")
+ print("✅ Configuration MLflow terminée")
+
+# Import des fonctions utils
+sys.path.append('.')
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+from utils.utils import (
+ load_cityscapes_config,
+ load_model,
+ run_inference_and_visualize,
+ colorize_mask
+)
+
+# Configuration
+MODEL_NAME = "StreetsSegmentation"
+SAMPLE_IMAGES_DIR = "./notebooks/content/data/test_images_sample"
+TRAIN_IMAGES_DIR = "./notebooks/content/data/train_images_sample"
+CITYSCAPES_CONFIG_PATH = "./cityscapes_config.json"
+TEMP_UPLOAD_DIR = "/tmp/uploads"
+
+# Modèles de données mis à jour
+class ModelInfo(BaseModel):
+ name: str
+ run_id: str
+ encoder_name: str
+ input_size: tuple
+ rank: int
+ architecture: Optional[str] = None
+ test_mean_iou: Optional[float] = None
+ test_accuracy: Optional[float] = None
+ class_ious: Optional[dict] = None
+
+class ComparisonResult(BaseModel):
+ success: bool
+ message: str
+ model1_info: ModelInfo
+ model2_info: ModelInfo
+ image_path: Optional[str] = None
+ mask_path: Optional[str] = None
+ model1_stats: Optional[dict] = None
+ model2_stats: Optional[dict] = None
+ model1_inference_time: float
+ model2_inference_time: float
+ speed_comparison: dict
+ ground_truth_available: bool = False
+ figure_data: Optional[str] = None
+
+class ImageInfo(BaseModel):
+ filename: str
+ display_name: str
+ has_ground_truth: bool
+
+class AvailableImagesResponse(BaseModel):
+ images: List[ImageInfo]
+ total_count: int
+
+class PreprocessingRequest(BaseModel):
+ filename: str
+ apply_augmentation: bool = False
+
+class PreprocessingResult(BaseModel):
+ success: bool
+ message: str
+ original_image: Optional[str] = None
+ normalized_image: Optional[str] = None
+ augmented_image: Optional[str] = None
+ transformations_applied: Optional[list] = None
+
+class SampleImageRequest(BaseModel):
+ filename: str
+
+# Variables globales pour les deux modèles
+best_model = None
+second_best_model = None
+best_model_info = {}
+second_best_model_info = {}
+mapping_config = None
+
+def get_model_detailed_info(run_id, mapping_config):
+ """Récupère les informations détaillées d'un modèle depuis MLflow"""
+ client = mlflow.tracking.MlflowClient()
+
+ try:
+ run = mlflow.get_run(run_id)
+ params = run.data.params
+ metrics = run.data.metrics
+
+ # Architecture depuis le paramètre model_architecture
+ architecture = params.get("model_architecture", "Unknown").split('_')[0]
+
+ # Métriques de performance
+ test_mean_iou = metrics.get("test_mean_iou", None)
+ test_accuracy = metrics.get("test_accuracy", None)
+
+ # IoU par classe depuis les métriques class_X_iou
+ class_ious = {}
+ if mapping_config and 'group_names' in mapping_config:
+ for i, class_name in enumerate(mapping_config['group_names']):
+ metric_key = f"class_{i}_iou"
+ if metric_key in metrics:
+ class_ious[class_name] = round(metrics[metric_key], 3)
+
+ return {
+ "architecture": architecture,
+ "test_mean_iou": test_mean_iou,
+ "test_accuracy": test_accuracy,
+ "class_ious": class_ious
+ }
+ except Exception as e:
+ print(f"⚠️ Erreur lors de la récupération des infos détaillées: {e}")
+ return {
+ "architecture": "Unknown",
+ "test_mean_iou": None,
+ "test_accuracy": None,
+ "class_ious": {}
+ }
+
+def initialize_models():
+ """Initialise les deux meilleurs modèles au démarrage"""
+ global best_model, second_best_model, best_model_info, second_best_model_info, mapping_config
+
+ try:
+ print("🔄 Configuration de MLflow...")
+ configure_mlflow()
+
+ print("🔄 Chargement de la configuration Cityscapes avec couleurs WCAG...")
+ mapping_config = load_cityscapes_config(CITYSCAPES_CONFIG_PATH, verbose=False, use_wcag_colors=True)
+ print("✅ Configuration Cityscapes chargée avec couleurs WCAG")
+
+
+ print("🔄 Chargement du meilleur modèle...")
+ best_model, best_run_id, best_encoder_name, best_img_size = load_model(
+ experiment_name="OC Projet 9",
+ metric="test_mean_iou",
+ top_n=1
+ )
+
+ # Récupérer les infos détaillées du meilleur modèle
+ best_detailed_info = get_model_detailed_info(best_run_id, mapping_config)
+
+ best_model_info = {
+ "name": "Meilleur modèle",
+ "run_id": best_run_id,
+ "encoder_name": best_encoder_name,
+ "input_size": best_img_size,
+ "rank": 1,
+ "architecture": best_detailed_info["architecture"],
+ "test_mean_iou": best_detailed_info["test_mean_iou"],
+ "test_accuracy": best_detailed_info["test_accuracy"],
+ "class_ious": best_detailed_info["class_ious"]
+ }
+
+ print("🔄 Chargement du deuxième meilleur modèle...")
+ second_best_model, second_run_id, second_encoder_name, second_img_size = load_model(
+ experiment_name="OC Projet 9",
+ metric="test_mean_iou",
+ top_n=2
+ )
+
+ # Récupérer les infos détaillées du deuxième modèle
+ second_detailed_info = get_model_detailed_info(second_run_id, mapping_config)
+
+ second_best_model_info = {
+ "name": "Deuxième modèle",
+ "run_id": second_run_id,
+ "encoder_name": second_encoder_name,
+ "input_size": second_img_size,
+ "rank": 2,
+ "architecture": second_detailed_info["architecture"],
+ "test_mean_iou": second_detailed_info["test_mean_iou"],
+ "test_accuracy": second_detailed_info["test_accuracy"],
+ "class_ious": second_detailed_info["class_ious"]
+ }
+
+ os.makedirs(TEMP_UPLOAD_DIR, exist_ok=True)
+
+ except Exception as e:
+ print(f"❌ Erreur lors de l'initialisation: {e}")
+ traceback.print_exc()
+ raise e
+
+def get_accessibility_info():
+ """Retourne les informations d'accessibilité pour le frontend"""
+ return {
+ "wcag_compliant": True,
+ "color_palette": {
+ "primary_blue": "#0066CC",
+ "primary_red": "#CC0000",
+ "primary_green": "#006600",
+ "primary_orange": "#CC6600",
+ "secondary_purple": "#663399",
+ "secondary_teal": "#006666",
+ "neutral_dark": "#333333",
+ "neutral_medium": "#666666"
+ },
+ "contrast_ratios": {
+ "primary_colors_vs_white": "≥ 4.1:1 (WCAG AA)",
+ "text_vs_background": "≥ 4.5:1 (WCAG AA)"
+ },
+ "features": [
+ "Couleurs à contraste élevé",
+ "Motifs alternatifs aux couleurs",
+ "Descriptions textuelles alternatives en cas d'absence d'images",
+ "Tableaux de données complémentaires",
+ "Navigation clavier supportée"
+ ]
+ }
+
+
+# Initialisation de l'API
+app = FastAPI(
+ title="Multi-Class Segmentation Comparison API",
+ description="API de comparaison de segmentation sémantique avec deux modèles",
+ version="2.0.0"
+)
+
+@app.on_event("startup")
+async def startup_event():
+ try:
+ initialize_models()
+ print("🚀 API démarrée avec succès - Modèles chargés!")
+ except Exception as e:
+ print(f"⚠️ ATTENTION: Impossible de charger les modèles au démarrage: {e}")
+
+@app.get("/")
+def root():
+ return {
+ "message": "Multi-Class Segmentation Comparison API - Online",
+ "models_loaded": best_model is not None and second_best_model is not None,
+ "best_model": best_model_info if best_model else None,
+ "second_best_model": second_best_model_info if second_best_model else None,
+ "num_classes": mapping_config['num_classes'] if mapping_config else None
+ }
+
+@app.get("/health")
+def health_check():
+ if best_model is None or second_best_model is None or mapping_config is None:
+ raise HTTPException(status_code=503, detail="Models not loaded")
+ return {
+ "status": "healthy",
+ "models_loaded": True,
+ "timestamp": datetime.utcnow().isoformat()
+ }
+
+
+# Modifier le endpoint /models pour inclure des couleurs accessibles :
+@app.get("/models", response_model=dict)
+def get_models_info():
+ if best_model is None or second_best_model is None:
+ raise HTTPException(status_code=503, detail="Models not loaded")
+
+ # NOUVELLE RÉPARTITION DES COULEURS selon votre mapping final
+ wcag_accessible_colors = [
+ [102, 102, 102], # Flat: neutral_medium (gris moyen pour routes/trottoirs)
+ [204, 0, 0], # Human: primary_red (rouge pour piétons/cyclistes)
+ [102, 51, 153], # Vehicle: secondary_purple (violet pour véhicules)
+ [204, 102, 0], # Construction: primary_orange (orange pour bâtiments)
+ [0, 102, 102], # Object: secondary_teal (teal pour objets/signalétique)
+ [0, 102, 0], # Nature: primary_green (vert pour végétation)
+ [0, 102, 204], # Sky: primary_blue (bleu pour ciel)
+ [51, 51, 51] # Void: neutral_dark (gris foncé pour zones non-définies)
+ ]
+
+ # Mettre à jour la configuration avec les nouvelles couleurs
+ if mapping_config:
+ mapping_config['group_colors'] = np.array(wcag_accessible_colors, dtype=np.uint8)
+
+ return {
+ "best_model": best_model_info,
+ "second_best_model": second_best_model_info,
+ "num_classes": mapping_config['num_classes'],
+ "class_names": mapping_config['group_names'],
+ "class_colors": wcag_accessible_colors,
+ "accessibility_info": get_accessibility_info(),
+ "preprocessing_resolution": best_model_info.get("input_size", (512, 512))
+ }
+
+
+
+@app.get("/dataset-info")
+def get_dataset_info():
+ """Retourne les informations sur le dataset"""
+ return {
+ "name": "Cityscapes Dataset",
+ "description": "Référence internationale pour l'évaluation des algorithmes de segmentation sémantique en environnement urbain",
+ "total_images": 25000,
+ "resolution": "2048×1024 pixels",
+ "cities": 50,
+ "countries": ["Allemagne", "Suisse"],
+ "splits": {
+ "train": {"images": 2975, "usage": "Entraînement avec annotations fines"},
+ "validation": {"images": 500, "usage": "Validation avec annotations de référence"},
+ "test": {"images": 1525, "usage": "Test (annotations non-publiques)"},
+ "coarse": {"images": 20000, "usage": "Entraînement avec annotations grossières"}
+ },
+ "project_splits": {
+ "train": {"images": 2380, "percentage": 80, "source": "Cityscapes train"},
+ "validation": {"images": 595, "percentage": 20, "source": "Cityscapes train"},
+ "test": {"images": 500, "percentage": 100, "source": "Cityscapes val"}
+ },
+ "meta_classes": mapping_config['group_names'] if mapping_config else [],
+ "num_classes": mapping_config['num_classes'] if mapping_config else 0
+ }
+
+@app.get("/train-images", response_model=AvailableImagesResponse)
+def get_train_images():
+ """Retourne la liste des images d'entraînement disponibles"""
+ try:
+ original_dir = Path(TRAIN_IMAGES_DIR) / "original"
+ mask_dir = Path(TRAIN_IMAGES_DIR) / "mask"
+
+ if not original_dir.exists():
+ raise HTTPException(status_code=404, detail="Train images directory not found")
+
+ images = []
+ for img_path in original_dir.glob("*leftImg8bit.png"):
+ # Construire le nom du masque correspondant
+ base_name = img_path.stem.replace("_leftImg8bit", "")
+ mask_name = f"{base_name}_gtFine_labelIds.png"
+ mask_path = mask_dir / mask_name
+
+ # Créer un nom d'affichage plus convivial
+ display_name = base_name.replace("_", " ").title()
+
+ images.append(ImageInfo(
+ filename=img_path.name,
+ display_name=display_name,
+ has_ground_truth=mask_path.exists()
+ ))
+
+ # Trier par nom pour un affichage cohérent
+ images.sort(key=lambda x: x.filename)
+
+ return AvailableImagesResponse(
+ images=images,
+ total_count=len(images)
+ )
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error listing train images: {str(e)}")
+
+@app.get("/train-image/{filename}")
+def get_train_image(filename: str):
+ """Sert une image d'entraînement pour prévisualisation"""
+ try:
+ original_dir = Path(TRAIN_IMAGES_DIR) / "original"
+ image_path = original_dir / filename
+
+ if not image_path.exists():
+ raise HTTPException(status_code=404, detail=f"Train image {filename} not found")
+
+ # Vérifier que c'est bien un fichier image attendu
+ if not filename.endswith('_leftImg8bit.png'):
+ raise HTTPException(status_code=400, detail="Invalid image filename format")
+
+ return FileResponse(
+ image_path,
+ media_type="image/png",
+ headers={"Content-Disposition": f"inline; filename={filename}"}
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error serving train image: {str(e)}")
+
+@app.get("/class-repartition")
+def get_class_repartition():
+ """Sert le graphique de répartition des classes"""
+ try:
+ graph_path = Path(TRAIN_IMAGES_DIR) / "class_repartition.png"
+
+ if not graph_path.exists():
+ raise HTTPException(status_code=404, detail="Class repartition graph not found")
+
+ return FileResponse(
+ graph_path,
+ media_type="image/png",
+ headers={"Content-Disposition": "inline; filename=class_repartition.png"}
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error serving class repartition graph: {str(e)}")
+
+@app.post("/preprocess-train-image", response_model=PreprocessingResult)
+def preprocess_train_image(request: PreprocessingRequest):
+ """Prétraite une image d'entraînement et retourne les versions transformées"""
+ if mapping_config is None:
+ raise HTTPException(status_code=503, detail="Configuration not loaded")
+
+ # Vérifier que le meilleur modèle est chargé
+ if best_model is None or not best_model_info:
+ raise HTTPException(status_code=503, detail="Best model not loaded")
+
+ try:
+ original_dir = Path(TRAIN_IMAGES_DIR) / "original"
+ image_path = original_dir / request.filename
+
+ if not image_path.exists():
+ raise HTTPException(status_code=404, detail=f"Train image {request.filename} not found")
+
+ # Importer les fonctions nécessaires
+ import numpy as np
+ import matplotlib.pyplot as plt
+ from PIL import Image
+ import io
+ import base64
+ from utils.utils import get_preprocessing_fn, apply_random_augmentation
+
+ # Charger l'image originale
+ original_pil = Image.open(image_path).convert('RGB')
+ original_np = np.array(original_pil)
+
+ # UTILISER LA RÉSOLUTION DU MEILLEUR MODÈLE au lieu de (512, 512)
+ target_size = best_model_info.get("input_size", (512, 512))
+ print(f"🎯 Utilisation de la taille du meilleur modèle: {target_size}")
+
+ # Appliquer le preprocessing avec la résolution du meilleur modèle
+ preprocess_fn = get_preprocessing_fn(img_size=target_size, encoder=None)
+ normalized_tensor, _ = preprocess_fn(original_np, None)
+
+ # Convertir le tensor en image pour affichage
+ normalized_np = normalized_tensor.permute(1, 2, 0).numpy()
+ # Dénormaliser pour l'affichage (tensor était normalisé 0-1)
+ normalized_display = (normalized_np * 255).astype(np.uint8)
+
+ # Créer les images en base64 pour l'envoi
+ def img_to_base64(img_array):
+ img_pil = Image.fromarray(img_array)
+ buffer = io.BytesIO()
+ img_pil.save(buffer, format='PNG')
+ return base64.b64encode(buffer.getvalue()).decode()
+
+ # Image originale redimensionnée pour comparaison (avec la taille du meilleur modèle)
+ original_resized = np.array(original_pil.resize(target_size))
+
+ result = PreprocessingResult(
+ success=True,
+ message="Preprocessing completed successfully",
+ original_image=img_to_base64(original_resized),
+ normalized_image=img_to_base64(normalized_display),
+ transformations_applied=[f"Resize {target_size}", "Normalization (0-1)"]
+ )
+
+ # Appliquer l'augmentation si demandée
+ if request.apply_augmentation:
+ augmented_np, transforms_applied = apply_random_augmentation(original_np)
+ # Appliquer aussi le preprocessing à l'image augmentée
+ augmented_tensor, _ = preprocess_fn(augmented_np, None)
+ augmented_display = (augmented_tensor.permute(1, 2, 0).numpy() * 255).astype(np.uint8)
+
+ result.augmented_image = img_to_base64(augmented_display)
+ result.transformations_applied.extend(transforms_applied)
+
+ return result
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Preprocessing error: {str(e)}")
+
+@app.get("/best-model-resolution")
+def get_best_model_resolution():
+ """Retourne la résolution utilisée par le meilleur modèle"""
+ if best_model is None or not best_model_info:
+ raise HTTPException(status_code=503, detail="Best model not loaded")
+
+ return {
+ "model_name": best_model_info.get("name", "Unknown"),
+ "encoder_name": best_model_info.get("encoder_name", "Unknown"),
+ "input_size": best_model_info.get("input_size", (512, 512)),
+ "architecture": best_model_info.get("architecture", "Unknown")
+ }
+
+@app.get("/sample-images", response_model=AvailableImagesResponse)
+def get_sample_images():
+ try:
+ original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
+ mask_dir = Path(SAMPLE_IMAGES_DIR) / "mask"
+ if not original_dir.exists():
+ raise HTTPException(status_code=404, detail="Sample images directory not found")
+ images = []
+ for img_path in original_dir.glob("*leftImg8bit.png"):
+ base_name = img_path.stem.replace("_leftImg8bit", "")
+ mask_name = f"{base_name}_gtFine_labelIds.png"
+ mask_path = mask_dir / mask_name
+ display_name = base_name.replace("_", " ").title()
+ images.append(ImageInfo(
+ filename=img_path.name,
+ display_name=display_name,
+ has_ground_truth=mask_path.exists()
+ ))
+ images.sort(key=lambda x: x.filename)
+ return AvailableImagesResponse(images=images, total_count=len(images))
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error listing sample images: {str(e)}")
+
+@app.get("/sample-image/{filename}")
+def get_sample_image(filename: str):
+ try:
+ original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
+ image_path = original_dir / filename
+ if not image_path.exists():
+ raise HTTPException(status_code=404, detail=f"Sample image {filename} not found")
+ if not filename.endswith('_leftImg8bit.png'):
+ raise HTTPException(status_code=400, detail="Invalid image filename format")
+ return FileResponse(
+ image_path,
+ media_type="image/png",
+ headers={"Content-Disposition": f"inline; filename={filename}"}
+ )
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error serving image: {str(e)}")
+
+@app.post("/compare-sample", response_model=ComparisonResult)
+def compare_sample_image(request: SampleImageRequest):
+ if best_model is None or second_best_model is None or mapping_config is None:
+ raise HTTPException(status_code=503, detail="Models not loaded")
+ try:
+ original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
+ image_path = original_dir / request.filename
+ if not image_path.exists():
+ raise HTTPException(status_code=404, detail=f"Sample image {request.filename} not found")
+ base_name = image_path.stem.replace("_leftImg8bit", "")
+ mask_name = f"{base_name}_gtFine_labelIds.png"
+ mask_path = Path(SAMPLE_IMAGES_DIR) / "mask" / mask_name
+ mask_path_str = str(mask_path) if mask_path.exists() else None
+
+ # Inférence modèle 1 avec sa résolution spécifique
+ pred1, t1, fig1 = run_inference_and_visualize(
+ image_path=str(image_path),
+ mask_path=mask_path_str,
+ model=best_model,
+ encoder_name=None, # Normalisé de la même manière
+ img_size=best_model_info["input_size"], # Utilise la résolution du modèle
+ mapping_config=mapping_config,
+ save_dir=None, show_stats=False, show_plot=False
+ )
+ # Inférence modèle 2 avec sa résolution spécifique
+ pred2, t2, fig2 = run_inference_and_visualize(
+ image_path=str(image_path),
+ mask_path=mask_path_str,
+ model=second_best_model,
+ encoder_name=None, # Normalisé de la même manière
+ img_size=second_best_model_info["input_size"], # Utilise la résolution du modèle
+ mapping_config=mapping_config,
+ save_dir=None, show_stats=False, show_plot=False
+ )
+
+ # Créer une figure combinée avec les deux modèles
+ # Nombre de colonnes : 3 si pas de GT, 4 si GT disponible
+ num_cols = 3 if not (mask_path_str and os.path.exists(mask_path_str)) else 4
+ fig, axes = plt.subplots(2, num_cols, figsize=(5*num_cols, 8))
+
+ # Récupérer les images depuis les figures individuelles
+ axes1 = fig1.get_axes()
+ axes2 = fig2.get_axes()
+
+ # Ligne 1 : Modèle 1
+ for i in range(min(len(axes1), num_cols)):
+ ax_src = axes1[i]
+ ax_dst = axes[0, i]
+
+ # Copier toutes les images de l'axe source
+ for img_obj in ax_src.get_images():
+ ax_dst.imshow(
+ img_obj.get_array(),
+ cmap=img_obj.get_cmap(),
+ alpha=img_obj.get_alpha(),
+ vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
+ vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
+ )
+
+ # Copier le titre avec nom du modèle
+ original_title = ax_src.get_title()
+ if i == 0:
+ new_title = "Image originale"
+ elif "Prédit" in original_title:
+ new_title = f"Prédiction {best_model_info['architecture']} - {best_model_info['encoder_name']}"
+ else:
+ new_title = original_title
+
+ ax_dst.set_title(new_title)
+ ax_dst.axis('off')
+
+ # Ligne 2 : Modèle 2
+ for i in range(min(len(axes2), num_cols)):
+ ax_src = axes2[i]
+ ax_dst = axes[1, i]
+
+ # Copier toutes les images de l'axe source
+ for img_obj in ax_src.get_images():
+ ax_dst.imshow(
+ img_obj.get_array(),
+ cmap=img_obj.get_cmap(),
+ alpha=img_obj.get_alpha(),
+ vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
+ vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
+ )
+
+ # Copier le titre avec nom du modèle
+ original_title = ax_src.get_title()
+ if i == 0:
+ new_title = "Image originale"
+ elif "Prédit" in original_title:
+ new_title = f"Prédiction {second_best_model_info['architecture']} - {second_best_model_info['encoder_name']}"
+ else:
+ new_title = original_title
+
+ ax_dst.set_title(new_title)
+ ax_dst.axis('off')
+
+ plt.tight_layout()
+
+ # Fermer les figures individuelles pour libérer la mémoire
+ plt.close(fig1)
+ plt.close(fig2)
+
+ fig_b64 = base64.b64encode(pickle.dumps(fig)).decode()
+ # Stats
+ def calc(mask):
+ uc, cnts = np.unique(mask, return_counts=True)
+ stats = {}
+ for cid, c in zip(uc, cnts):
+ if cid < len(mapping_config['group_names']):
+ stats[mapping_config['group_names'][cid]] = {
+ "pixels": int(c),
+ "percentage": round((c/mask.size)*100, 1)
+ }
+ return stats
+ stats1, stats2 = calc(pred1), calc(pred2)
+ faster = best_model_info['encoder_name'] if t1 < t2 else second_best_model_info['encoder_name']
+ speedup = round(max(t1, t2) / min(t1, t2), 2)
+ speed_comp = {"faster_model": faster, "speedup_ratio": speedup, "time_difference_ms": abs(t1-t2)*1000}
+ return ComparisonResult(
+ success=True, message="Comparison completed successfully",
+ model1_info=ModelInfo(**best_model_info), model2_info=ModelInfo(**second_best_model_info),
+ image_path=str(image_path), mask_path=mask_path_str,
+ model1_stats=stats1, model2_stats=stats2,
+ model1_inference_time=t1, model2_inference_time=t2,
+ speed_comparison=speed_comp, ground_truth_available=mask_path.exists(),
+ figure_data=fig_b64
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
+
+@app.post("/compare-upload", response_model=ComparisonResult)
+def compare_uploaded_image(file: UploadFile = File(...)):
+ if best_model is None or second_best_model is None or mapping_config is None:
+ raise HTTPException(status_code=503, detail="Models not loaded")
+ allowed_types = ["image/jpeg", "image/jpg", "image/png"]
+ if file.content_type not in allowed_types:
+ raise HTTPException(status_code=400, detail=f"Invalid file type. Allowed: {', '.join(allowed_types)}")
+ temp_file_path = None
+ try:
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ temp_filename = f"upload_{timestamp}_{file.filename}"
+ temp_file_path = os.path.join(TEMP_UPLOAD_DIR, temp_filename)
+ with open(temp_file_path, "wb") as buffer:
+ shutil.copyfileobj(file.file, buffer)
+ # Inférences avec résolutions spécifiques
+ pred1, t1, fig1 = run_inference_and_visualize(
+ image_path=temp_file_path, mask_path=None,
+ model=best_model, encoder_name=None, # Normalisé de la même manière
+ img_size=best_model_info["input_size"], mapping_config=mapping_config,
+ save_dir=None, show_stats=False, show_plot=False,
+ )
+ pred2, t2, fig2 = run_inference_and_visualize(
+ image_path=temp_file_path, mask_path=None,
+ model=second_best_model, encoder_name=None, # Normalisé de la même manière
+ img_size=second_best_model_info["input_size"], mapping_config=mapping_config,
+ save_dir=None, show_stats=False, show_plot=False,
+ )
+
+ # Figure combinée pour uploads (pas de GT, donc 2 colonnes seulement)
+ fig, axes = plt.subplots(2, 2, figsize=(10, 8))
+
+ # Récupérer les images depuis les figures individuelles
+ axes1 = fig1.get_axes()
+ axes2 = fig2.get_axes()
+
+ # Ligne 1 : Modèle 1 (Image originale + Prédiction)
+ for i in range(min(len(axes1), 2)):
+ ax_src = axes1[i]
+ ax_dst = axes[0, i]
+
+ # Copier toutes les images de l'axe source
+ for img_obj in ax_src.get_images():
+ ax_dst.imshow(
+ img_obj.get_array(),
+ cmap=img_obj.get_cmap(),
+ alpha=img_obj.get_alpha(),
+ vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
+ vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
+ )
+
+ # Copier le titre avec nom du modèle
+ original_title = ax_src.get_title()
+ if i == 0:
+ new_title = "Image originale"
+ else:
+ new_title = f"Prédiction {best_model_info['architecture']} - {best_model_info['encoder_name']}"
+
+ ax_dst.set_title(new_title)
+ ax_dst.axis('off')
+
+ # Ligne 2 : Modèle 2 (Image originale + Prédiction)
+ for i in range(min(len(axes2), 2)):
+ ax_src = axes2[i]
+ ax_dst = axes[1, i]
+
+ # Copier toutes les images de l'axe source
+ for img_obj in ax_src.get_images():
+ ax_dst.imshow(
+ img_obj.get_array(),
+ cmap=img_obj.get_cmap(),
+ alpha=img_obj.get_alpha(),
+ vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
+ vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
+ )
+
+ # Copier le titre avec nom du modèle
+ original_title = ax_src.get_title()
+ if i == 0:
+ new_title = "Image originale"
+ else:
+ new_title = f"Prédiction {second_best_model_info['architecture']} - {second_best_model_info['encoder_name']}"
+
+ ax_dst.set_title(new_title)
+ ax_dst.axis('off')
+
+ plt.tight_layout()
+
+ # Fermer les figures individuelles pour libérer la mémoire
+ plt.close(fig1)
+ plt.close(fig2)
+
+ fig_b64 = base64.b64encode(pickle.dumps(fig)).decode()
+ # Stats
+ def calc(mask):
+ uc, cnts = np.unique(mask, return_counts=True)
+ stats = {}
+ for cid, c in zip(uc, cnts):
+ if cid < len(mapping_config['group_names']):
+ stats[mapping_config['group_names'][cid]] = {
+ "pixels": int(c),
+ "percentage": round((c/mask.size)*100, 1)
+ }
+ return stats
+ stats1, stats2 = calc(pred1), calc(pred2)
+ faster = best_model_info['encoder_name'] if t1 < t2 else second_best_model_info['encoder_name']
+ speedup = round(max(t1, t2) / min(t1, t2), 2)
+ speed_comp = {"faster_model": faster, "speedup_ratio": speedup, "time_difference_ms": abs(t1-t2)*1000}
+ return ComparisonResult(
+ success=True, message="Comparison completed successfully",
+ model1_info=ModelInfo(**best_model_info), model2_info=ModelInfo(**second_best_model_info),
+ image_path=temp_file_path,
+ model1_stats=stats1, model2_stats=stats2,
+ model1_inference_time=t1, model2_inference_time=t2,
+ speed_comparison=speed_comp, ground_truth_available=False,
+ figure_data=fig_b64
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
+ finally:
+ if temp_file_path and os.path.exists(temp_file_path):
+ try: os.remove(temp_file_path)
+ except Exception: pass
+
+@app.get("/metrics")
+def get_metrics():
+ return {
+ "models_loaded": best_model is not None and second_best_model is not None,
+ "best_model_info": best_model_info,
+ "second_best_model_info": second_best_model_info,
+ "num_classes": mapping_config['num_classes'] if mapping_config else None,
+ "timestamp": datetime.utcnow().isoformat()
+ }
+
+# Ajouter cette route après les autres routes :
+@app.get("/accessibility-info")
+def get_accessibility_information():
+ """Retourne les informations sur la conformité d'accessibilité"""
return get_accessibility_info()
\ No newline at end of file
diff --git a/app/requirements.txt b/app/requirements.txt
index 6fe45e3..eff910c 100644
--- a/app/requirements.txt
+++ b/app/requirements.txt
@@ -1,71 +1,71 @@
-# =============================================================================
-# Multi-Class Segmentation API - Requirements
-# =============================================================================
-
-# ===== CORE WEB FRAMEWORK =====
-fastapi==0.115.14
-uvicorn==0.35.0
-starlette==0.46.2
-pydantic==2.11.7
-pydantic_core==2.33.2
-python-multipart==0.0.20 # uploads FastAPI
-
-# ===== STREAMLIT =====
-streamlit==1.46.1
-altair==5.5.0 # utilisé par Streamlit
-blinker==1.9.0 # dépendance Streamlit
-
-# ===== HTTP CLIENTS =====
-requests==2.32.4
-httpx==0.28.1 # utilisé en interne par FastAPI
-httpcore==1.0.9
-h11==0.16.0
-
-# ===== MACHINE LEARNING CORE (PyTorch) =====
-torch==2.8.0 # version stable PyTorch
-torchvision==0.23.0 # utile pour les modèles pré-entraînés
-torchaudio==2.8.0 # optionnel (mais recommandé pour compatibilité PyTorch)
-segmentation-models-pytorch==0.5.0
-albumentations==2.0.8
-# version PyTorch de ToTensor
-albumentations[imgaug,opencv]
-
-numpy>=1.24.0,<2.0.0
-scipy==1.16.0
-scikit-learn==1.7.0
-
-# ===== COMPUTER VISION =====
-pillow==11.3.0
-opencv-python-headless==4.9.0.80
-scikit-image==0.25.2
-imageio==2.37.0
-
-# ===== DATA SCIENCE =====
-pandas==2.3.0
-matplotlib==3.10.3
-networkx==3.5
-tifffile==2025.6.11
-lazy_loader==0.4
-
-# ===== MLFLOW & MODEL REGISTRY =====
-mlflow==3.2.0
-mlflow-skinny==3.2.0
-mlflow-tracing==3.2.0
-
-# ===== AWS/S3 SUPPORT =====
-boto3==1.39.2
-botocore==1.39.2
-s3transfer==0.13.0
-
-# ===== UTILITIES =====
-click==8.2.1
-tqdm==4.67.1
-joblib==1.5.1
-python-dateutil
-packaging
-typing_extensions==4.14.0
-
-# ===== MATPLOTLIB ADDONS =====
-cycler==0.12.1
-fonttools==4.58.5
-plotly==5.24.1
+# =============================================================================
+# Multi-Class Segmentation API - Requirements
+# =============================================================================
+
+# ===== CORE WEB FRAMEWORK =====
+fastapi==0.115.14
+uvicorn==0.35.0
+starlette==0.46.2
+pydantic==2.11.7
+pydantic_core==2.33.2
+python-multipart==0.0.20 # uploads FastAPI
+
+# ===== STREAMLIT =====
+streamlit==1.46.1
+altair==5.5.0 # utilisé par Streamlit
+blinker==1.9.0 # dépendance Streamlit
+
+# ===== HTTP CLIENTS =====
+requests==2.32.4
+httpx==0.28.1 # utilisé en interne par FastAPI
+httpcore==1.0.9
+h11==0.16.0
+
+# ===== MACHINE LEARNING CORE (PyTorch) =====
+torch==2.8.0 # version stable PyTorch
+torchvision==0.23.0 # utile pour les modèles pré-entraînés
+torchaudio==2.8.0 # optionnel (mais recommandé pour compatibilité PyTorch)
+segmentation-models-pytorch==0.5.0
+albumentations==2.0.8
+# version PyTorch de ToTensor
+albumentations[imgaug,opencv]
+
+numpy>=1.24.0,<2.0.0
+scipy==1.16.0
+scikit-learn==1.7.0
+
+# ===== COMPUTER VISION =====
+pillow==11.3.0
+opencv-python-headless==4.9.0.80
+scikit-image==0.25.2
+imageio==2.37.0
+
+# ===== DATA SCIENCE =====
+pandas==2.3.0
+matplotlib==3.10.3
+networkx==3.5
+tifffile==2025.6.11
+lazy_loader==0.4
+
+# ===== MLFLOW & MODEL REGISTRY =====
+mlflow==3.2.0
+mlflow-skinny==3.2.0
+mlflow-tracing==3.2.0
+
+# ===== AWS/S3 SUPPORT =====
+boto3==1.39.2
+botocore==1.39.2
+s3transfer==0.13.0
+
+# ===== UTILITIES =====
+click==8.2.1
+tqdm==4.67.1
+joblib==1.5.1
+python-dateutil
+packaging
+typing_extensions==4.14.0
+
+# ===== MATPLOTLIB ADDONS =====
+cycler==0.12.1
+fonttools==4.58.5
+plotly==5.24.1
diff --git a/app/streamlit_app.py b/app/streamlit_app.py
index db9527a..c80c8dd 100644
--- a/app/streamlit_app.py
+++ b/app/streamlit_app.py
@@ -1,1889 +1,1889 @@
-import os
-import sys
-import requests
-import streamlit as st
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-import plotly.express as px
-import plotly.graph_objects as go
-from PIL import Image
-import io
-import base64
-import pickle
-
-# Configuration de l'API
-API_BASE_URL = os.getenv("MULTISEG_API_BASE_URL", "http://127.0.0.1:8080/api")
-
-# Import des fonctions utils pour l'affichage local
-sys.path.append('/app')
-sys.path.append('.')
-from utils.utils import colorize_mask
-
-# Configuration de la page avec titre approprié (WCAG 2.4.2)
-st.set_page_config(
- page_title="Street Vision - Segmentation Comparison | Dashboard Accessible",
- page_icon="🚗",
- layout="wide"
-)
-
-
-# Ajouts à placer au début de streamlit_app.py après les imports
-
-# Palettes de couleurs conformes WCAG AA (ratio contraste ≥ 3:1)
-WCAG_ACCESSIBLE_COLORS = {
- 'primary_blue': '#0066CC', # [0, 102, 204]
- 'primary_red': '#CC0000', # [204, 0, 0]
- 'primary_green': '#006600', # [0, 102, 0]
- 'primary_orange': '#CC6600', # [204, 102, 0]
- 'secondary_purple': '#663399', # [102, 51, 153]
- 'secondary_teal': '#006666', # [0, 102, 102]
- 'neutral_dark': '#333333', # [51, 51, 51]
- 'neutral_medium': '#666666', # [102, 102, 102]
- 'neutral_light': '#CCCCCC', # Pour éléments désactivés
- 'background_alt': '#F5F5F5' # Fond alternatif accessible
-}
-
-# Convertir hex vers RGB
-def hex_to_rgb(hex_color):
- """Convertit une couleur hex en tuple RGB"""
- hex_color = hex_color.lstrip('#')
- return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
-
-# Couleurs RGB pour les masques (dans le même ordre que les classes)
-WCAG_RGB_COLORS = [
- hex_to_rgb(WCAG_ACCESSIBLE_COLORS['neutral_medium']), # Flat: gris moyen [102, 102, 102]
- hex_to_rgb(WCAG_ACCESSIBLE_COLORS['primary_red']), # Human: rouge [204, 0, 0]
- hex_to_rgb(WCAG_ACCESSIBLE_COLORS['secondary_purple']), # Vehicle: violet [102, 51, 153]
- hex_to_rgb(WCAG_ACCESSIBLE_COLORS['primary_orange']), # Construction: orange [204, 102, 0]
- hex_to_rgb(WCAG_ACCESSIBLE_COLORS['secondary_teal']), # Object: teal [0, 102, 102]
- hex_to_rgb(WCAG_ACCESSIBLE_COLORS['primary_green']), # Nature: vert [0, 102, 0]
- hex_to_rgb(WCAG_ACCESSIBLE_COLORS['primary_blue']), # Sky: bleu [0, 102, 204]
- hex_to_rgb(WCAG_ACCESSIBLE_COLORS['neutral_dark']) # Void: gris foncé [51, 51, 51]
-]
-
-
-# Motifs pour différencier sans couleur uniquement
-ACCESSIBILITY_PATTERNS = {
- 'solid': None,
- 'diagonal_lines': '///',
- 'vertical_lines': '|||',
- 'horizontal_lines': '---',
- 'dots': '...',
- 'crosses': 'xxx'
-}
-
-def create_accessible_pie_chart_conditional(sizes, labels, title, use_patterns=False):
- """Version corrigée qui règle le problème de lisibilité avec les motifs"""
- fig, ax = plt.subplots(figsize=(6, 6), tight_layout=True)
-
- accessible_colors = [
- WCAG_ACCESSIBLE_COLORS['primary_blue'], WCAG_ACCESSIBLE_COLORS['primary_red'],
- WCAG_ACCESSIBLE_COLORS['primary_green'], WCAG_ACCESSIBLE_COLORS['primary_orange'],
- WCAG_ACCESSIBLE_COLORS['secondary_purple'], WCAG_ACCESSIBLE_COLORS['secondary_teal']
- ]
- colors = accessible_colors[:len(sizes)]
-
- # SOLUTION : texte avec fond contrasté
- uniform_textprops = {
- 'fontsize': 10, 'fontweight': 'bold', 'color': 'white',
- 'bbox': dict(boxstyle="round,pad=0.3", facecolor='black', alpha=0.8)
- }
-
- try:
- wedges, texts, autotexts = ax.pie(
- sizes, labels=labels, colors=colors, autopct='%1.0f%%', startangle=90, shadow=True,
- textprops=uniform_textprops, wedgeprops={'edgecolor': 'white', 'linewidth': 2},
- pctdistance=0.85 # Éloigner les pourcentages
- )
-
- # Améliorer la lisibilité des pourcentages
- for autotext in autotexts:
- autotext.set_bbox(dict(boxstyle="round,pad=0.4", facecolor='black', alpha=0.9))
- autotext.set_color('white')
- autotext.set_fontweight('bold')
- autotext.set_fontsize(9)
-
- # Motifs APRÈS configuration du texte
- if use_patterns:
- patterns = ['///', '|||', '---', '...', 'xxx', '+++']
- for wedge, pattern in zip(wedges, patterns[:len(wedges)]):
- if pattern:
- wedge.set_hatch(pattern)
-
- ax.set_title(title, fontsize=10, fontweight='bold', pad=15, color='#333333')
- ax.axis('equal')
- chart_displayed = True
-
- except Exception as e:
- ax.text(0.5, 0.5, f"Erreur d'affichage du graphique\n{str(e)}",
- ha='center', va='center', transform=ax.transAxes, fontsize=10, color='red', weight='bold')
- chart_displayed = False
-
- return fig, ax, {'colors': colors, 'patterns': patterns if use_patterns else None}, chart_displayed
-
-# Fonctions Plotly pour graphiques interactifs
-def create_interactive_pie_chart(sizes, labels, title, models_info=None):
- """Crée un pie chart interactif avec les couleurs des classes définies dans models_info"""
-
- # Utiliser les couleurs des classes si disponibles
- if models_info and 'class_colors' in models_info and 'class_names' in models_info:
- colors = []
- class_names = models_info['class_names']
- class_colors = models_info['class_colors']
-
- for label in labels:
- if label in class_names:
- idx = class_names.index(label)
- color_rgb = class_colors[idx]
- color_hex = f"#{color_rgb[0]:02x}{color_rgb[1]:02x}{color_rgb[2]:02x}"
- colors.append(color_hex)
- else:
- # Couleur par défaut si classe non trouvée
- colors.append('#666666')
- else:
- # Fallback vers les couleurs WCAG
- accessible_colors_list = list(WCAG_ACCESSIBLE_COLORS.values())
- colors = accessible_colors_list[:len(sizes)]
-
- df = pd.DataFrame({'labels': labels, 'values': sizes, 'colors': colors})
-
- fig = go.Figure(data=[go.Pie(
- labels=df['labels'],
- values=df['values'],
- marker=dict(
- colors=df['colors'],
- line=dict(color='#FFFFFF', width=3),
- pattern=dict(
- shape=['', '/', '\\', '|', '-', '+', 'x', '.'][:len(sizes)],
- size=8,
- solidity=0.3
- )
- ),
- textinfo='label+percent',
- textposition='auto',
- textfont=dict(
- size=13,
- color='white',
- family="Arial Black"
- ),
- hovertemplate='%{label}
Valeur: %{value}
Pourcentage: %{percent}
',
- hoverlabel=dict(
- bgcolor="rgba(255,255,255,0.95)",
- bordercolor="#333333",
- font=dict(size=14, color="#333333")
- )
- )])
-
- fig.update_layout(
- title={
- 'text': title,
- 'x': 0.5,
- 'xanchor': 'center',
- 'font': {'size': 16, 'color': '#333333', 'family': 'Arial Black'}
- },
- showlegend=True,
- legend=dict(
- orientation="v",
- yanchor="middle",
- y=0.5,
- xanchor="left",
- x=1.02,
- font=dict(size=12, color='#333333'),
- bgcolor='rgba(255,255,255,0.9)',
- bordercolor='#333333',
- ),
- margin=dict(l=20, r=120, t=60, b=20),
- height=400,
- plot_bgcolor='rgba(0,0,0,0)',
- paper_bgcolor='rgba(0,0,0,0)',
- font=dict(color='#333333')
- )
-
- return fig
-
-
-def create_interactive_comparison_chart(model1_stats, model2_stats, model1_name, model2_name):
- """Crée un graphique en barres interactif WCAG-compliant pour comparer les deux modèles"""
- classes = list(set(model1_stats.keys()) | set(model2_stats.keys()))
-
- data = []
- for class_name in classes:
- val1 = model1_stats.get(class_name, {}).get('percentage', 0)
- val2 = model2_stats.get(class_name, {}).get('percentage', 0)
-
- data.append({'Classe': class_name, 'Modèle': model1_name, 'Pourcentage': val1, 'Pixels': model1_stats.get(class_name, {}).get('pixels', 0)})
- data.append({'Classe': class_name, 'Modèle': model2_name, 'Pourcentage': val2, 'Pixels': model2_stats.get(class_name, {}).get('pixels', 0)})
-
- df = pd.DataFrame(data)
-
- # Filtrer les données pour supprimer les lignes vides potentielles
- df = df[df['Modèle'].notna() & (df['Modèle'] != '')]
-
- fig = px.bar(
- df, x='Classe', y='Pourcentage', color='Modèle', barmode='group',
- title='Comparaison interactive des classes détectées',
- hover_data=['Pixels'],
- color_discrete_map={model1_name: '#0066CC', model2_name: '#CC0000'}
- # PAS de pattern_shape du tout
- )
-
- fig.update_layout(
- title={'x': 0.5, 'xanchor': 'center', 'font': {'size': 16, 'color': '#333333', 'family': 'Arial Black'}},
- xaxis=dict(
- title='Classes de segmentation',
- titlefont=dict(size=14, color='#666666'), # Plus clair comme vos pie charts
- tickfont=dict(size=12, color='#666666'), # Plus clair
- tickangle=45,
- showgrid=True,
- gridcolor='#E8E8E8', # Grille beaucoup plus claire
- gridwidth=1,
- linecolor='#AAAAAA', # Axe gris moyen
- linewidth=1,
- showline=True
- ),
- yaxis=dict(
- title='Pourcentage de l\'image (%)',
- titlefont=dict(size=14, color='#666666'), # Plus clair
- tickfont=dict(size=12, color='#666666'), # Plus clair
- showgrid=True,
- gridcolor='#E8E8E8', # Grille beaucoup plus claire
- gridwidth=1,
- linecolor='#AAAAAA', # Axe gris moyen
- linewidth=1,
- showline=True
- ),
- legend=dict(
- title='Modèles',
- font=dict(size=12, color='#333333'), # Comme vos pie charts
- bgcolor='rgba(255,255,255,0.9)',
- bordercolor='#333333',
- ),
- hovermode='x unified',
- height=500,
- plot_bgcolor='rgba(0,0,0,0)', # Transparent
- paper_bgcolor='rgba(0,0,0,0)', # Transparent
- font=dict(color='#333333') # Sans spécifier size qui peut poser problème
- )
-
- fig.update_traces(
- texttemplate='%{y:.1f}%',
- textposition='outside',
- textfont=dict(size=11, color='#666666'), # Plus clair pour lisibilité
- marker=dict(line=dict(color='#FFFFFF', width=1)) # Bordures blanches comme vos pie charts
- )
-
- return fig
-
-
-def create_dataset_interactive_overview():
- """Vue d'ensemble interactive du dataset avec les MÊMES couleurs que 'Visualisations accessibles'"""
-
- # RÉORGANISER les données pour que l'ordre soit logique et que les couleurs correspondent
- dataset_data = {
- 'Split': ['Train (notre projet)', 'Validation (notre projet)', 'Test final', 'Non utilisé'],
- 'Images': [2380, 595, 500, 21525],
- 'Utilisation': ['Entraînement', 'Validation', 'Test', 'Non utilisé']
- }
-
- df = pd.DataFrame(dataset_data)
-
- # Couleurs dans l'ordre exact des données
- colors = [
- WCAG_ACCESSIBLE_COLORS['primary_blue'], # Train (notre projet) - bleu
- WCAG_ACCESSIBLE_COLORS['primary_red'], # Validation (notre projet) - rouge
- WCAG_ACCESSIBLE_COLORS['primary_orange'], # Test final - orange
- WCAG_ACCESSIBLE_COLORS['neutral_dark'] # Non utilisé - gris foncé
- ]
-
- # FORCER l'ordre avec category_orders
- fig = px.pie(
- df,
- values='Images',
- names='Split',
- title='Répartition interactive du dataset Cityscapes',
- color_discrete_sequence=colors,
- hover_data=['Utilisation'],
- category_orders={'Split': ['Train (notre projet)', 'Validation (notre projet)', 'Test final', 'Non utilisé']}
- )
-
- # FORCER la correspondance couleur-catégorie avec color_discrete_map
- color_map = {
- 'Train (notre projet)': WCAG_ACCESSIBLE_COLORS['primary_blue'],
- 'Validation (notre projet)': WCAG_ACCESSIBLE_COLORS['primary_red'],
- 'Test final': WCAG_ACCESSIBLE_COLORS['primary_orange'],
- 'Non utilisé': WCAG_ACCESSIBLE_COLORS['neutral_dark']
- }
-
- fig.update_traces(
- marker=dict(
- colors=[color_map[split] for split in df['Split']], # Forcer les couleurs exactes
- line=dict(color='#FFFFFF', width=3),
- pattern=dict(
- shape=['|', '/', '\\', ''],
- size=8,
- solidity=0.3
- )
- ),
- textposition='auto',
- textinfo='percent+label',
- textfont=dict(size=12, color='white', family="Arial Black"),
- hovertemplate='%{label}
Images: %{value}
Usage: %{customdata[0]}'
- )
-
- fig.update_layout(
- title={'x': 0.5, 'xanchor': 'center', 'font': {'size': 16, 'color': '#333333'}},
- height=500,
- legend=dict(
- font=dict(size=12, color='#333333'),
- bgcolor='rgba(255,255,255,0.9)',
- bordercolor='#333333',
- ),
- font=dict(color='#333333')
- )
-
- return fig
-
-
-def create_accessible_bar_chart_conditional(data, labels, title, colors=None, use_patterns=False):
- """
- Crée un bar chart accessible - descriptions uniquement si erreur d'affichage
- """
- import matplotlib.pyplot as plt
-
- fig, ax = plt.subplots(figsize=(10, 6))
-
- if colors is None:
- colors = [WCAG_ACCESSIBLE_COLORS['primary_blue']] * len(data)
-
- # Motifs pour différenciation
- patterns = None
- if use_patterns:
- pattern_list = list(ACCESSIBILITY_PATTERNS.values())
- patterns = pattern_list[:len(data)]
-
- try:
- bars = ax.bar(
- labels,
- data,
- color=colors,
- alpha=0.8,
- edgecolor='#333333',
- linewidth=1
- )
-
- # Ajouter des motifs
- if patterns:
- for bar, pattern in zip(bars, patterns):
- if pattern:
- bar.set_hatch(pattern)
-
- # Style accessible
- ax.set_ylabel('Valeur', fontweight='bold', color='#333333')
- ax.set_xlabel('Catégories', fontweight='bold', color='#333333')
- ax.set_title(title, fontsize=14, fontweight='bold', pad=20, color='#333333')
-
- # Grille pour faciliter la lecture
- ax.grid(axis='y', alpha=0.3, linestyle='--', color='#666666')
- ax.set_axisbelow(True)
-
- # Valeurs sur les barres pour accessibilité
- for bar, value in zip(bars, data):
- height = bar.get_height()
- ax.text(bar.get_x() + bar.get_width()/2., height + max(data) * 0.01,
- f'{value:.1f}' if isinstance(value, float) else str(value),
- ha='center', va='bottom', fontsize=10, fontweight='bold', color='#333333')
-
- # Style des axes
- ax.spines['top'].set_visible(False)
- ax.spines['right'].set_visible(False)
- ax.spines['left'].set_color('#666666')
- ax.spines['bottom'].set_color('#666666')
-
- plt.xticks(rotation=45, ha='right', color='#333333')
- plt.yticks(color='#333333')
-
- chart_displayed = True
-
- except Exception as e:
- # En cas d'erreur, message d'erreur
- ax.text(0.5, 0.5, f"Erreur d'affichage du graphique\n{str(e)}",
- ha='center', va='center', transform=ax.transAxes,
- fontsize=12, color='red', weight='bold')
- chart_displayed = False
-
- return fig, ax, chart_displayed
-
-# Fonction pour générer du texte alternatif (WCAG 1.1.1)
-def generate_alt_text_for_pie(sizes, labels, title):
- """
- Génère un texte alternatif détaillé pour un pie chart
- """
- total = sum(sizes)
- descriptions = []
-
- for size, label in zip(sizes, labels):
- percentage = (size / total) * 100
- descriptions.append(f"{label}: {size} éléments ({percentage:.1f}%)")
-
- alt_text = f"Graphique en secteurs: {title}. "
- alt_text += f"Total de {total} éléments répartis comme suit: "
- alt_text += "; ".join(descriptions) + "."
-
- return alt_text
-
-# Fonction pour générer du texte alternatif (WCAG 1.1.1)
-def generate_alt_text_for_images(image_type, model_name=None, stats=None):
- """Génère un texte alternatif détaillé pour les images de segmentation"""
- if image_type == "original":
- return "Image originale en couleur montrant une scène urbaine typique du dataset Cityscapes"
- elif image_type == "segmentation":
- base_text = f"Masque de segmentation sémantique prédit par le modèle {model_name or 'IA'}"
- if stats:
- # Ajouter les statistiques principales
- main_classes = sorted(stats.items(), key=lambda x: x[1]['percentage'], reverse=True)[:3]
- stats_text = ". Classes principales détectées: " + ", ".join([
- f"{name} ({data['percentage']:.1f}%)" for name, data in main_classes
- ])
- return base_text + stats_text
- return base_text
- elif image_type == "comparison":
- return "Visualisation comparative des prédictions de deux modèles de segmentation sémantique"
- else:
- return "Image de segmentation sémantique"
-
-def generate_alt_text_for_bar(data, labels, title, unit=""):
- """
- Génère un texte alternatif détaillé pour un bar chart
- """
- descriptions = []
-
- for value, label in zip(data, labels):
- descriptions.append(f"{label}: {value}{unit}")
-
- alt_text = f"Graphique en barres: {title}. "
- alt_text += "Valeurs: " + "; ".join(descriptions) + "."
-
- return alt_text
-
-def generate_comprehensive_alt_text(image_type, model_name=None, stats=None, comparison_data=None):
- """Génère un texte alternatif complet selon WCAG 1.1.1"""
- if image_type == "preprocessing_original":
- return "Image urbaine originale redimensionnée à la résolution d'entraînement du modèle, montrant une scène typique du dataset Cityscapes avec véhicules, piétons et infrastructure urbaine"
-
- elif image_type == "preprocessing_normalized":
- return "Même image après normalisation des pixels pour l'entraînement du modèle IA, les couleurs peuvent paraître légèrement différentes"
-
- elif image_type == "preprocessing_augmented":
- return "Image avec augmentations de données appliquées (rotation, changement de luminosité, etc.) pour améliorer la robustesse du modèle"
-
- elif image_type == "segmentation_prediction":
- base_text = f"Masque de segmentation sémantique coloré prédit par le modèle {model_name or 'IA'}"
- if stats:
- main_classes = sorted(stats.items(), key=lambda x: x[1]['percentage'], reverse=True)[:3]
- stats_text = ". Classes principales détectées : " + ", ".join([
- f"{name} ({data['percentage']:.1f}% de l'image)" for name, data in main_classes
- ])
- return base_text + stats_text
- return base_text
-
- elif image_type == "comparison_grid":
- return "Grille comparative montrant l'image originale et les prédictions de segmentation de deux modèles d'IA différents côte à côte pour évaluation"
-
- else:
- return "Image de segmentation sémantique urbaine générée par intelligence artificielle"
-
-def create_data_table(data, columns, title, description=""):
- """
- Crée un tableau de données accessible complémentaire aux graphiques
- """
- import pandas as pd
-
- df = pd.DataFrame(data, columns=columns)
-
- st.markdown(f"**Tableau de données: {title}**")
- if description:
- st.markdown(f"*{description}*")
-
- # Configuration accessible du dataframe
- st.dataframe(
- df,
- use_container_width=True,
- hide_index=True,
- column_config={
- col: st.column_config.TextColumn(col, width="medium")
- for col in columns
- }
- )
-
- return df
-
-# CSS amélioré pour l'accessibilité complète
-ENHANCED_WCAG_CSS = """
-
-"""
-
-st.markdown(ENHANCED_WCAG_CSS, unsafe_allow_html=True)
-
-# En-tête principal
-st.markdown("""
-
-
🚗 Street Vision - Comparaison de Modèles
-
Évaluation comparative SegFormer vs FPN sur le dataset Cityscapes
-
-""", unsafe_allow_html=True)
-
-# Fonctions utilitaires
-def get_models_info():
- """Récupère les informations des deux modèles"""
- try:
- response = requests.get(f"{API_BASE_URL}/models")
- if response.status_code == 200:
- return response.json()
- else:
- st.error(f"Erreur lors de la récupération des infos modèles: {response.status_code}")
- return None
- except Exception as e:
- st.error(f"Erreur de connexion à l'API: {e}")
- return None
-
-def get_dataset_info():
- """Récupère les informations sur le dataset"""
- try:
- response = requests.get(f"{API_BASE_URL}/dataset-info")
- if response.status_code == 200:
- return response.json()
- else:
- st.error(f"Erreur lors de la récupération des infos dataset: {response.status_code}")
- return None
- except Exception as e:
- st.error(f"Erreur de connexion à l'API: {e}")
- return None
-
-def get_train_images():
- """Récupère la liste des images d'entraînement"""
- try:
- response = requests.get(f"{API_BASE_URL}/train-images")
- if response.status_code == 200:
- return response.json()
- else:
- st.error(f"Erreur lors de la récupération des images: {response.status_code}")
- return None
- except Exception as e:
- st.error(f"Erreur de connexion à l'API: {e}")
- return None
-
-def get_class_repartition():
- """Récupère le graphique de répartition des classes"""
- try:
- response = requests.get(f"{API_BASE_URL}/class-repartition", timeout=10)
- if response.status_code == 200:
- return response.content
- else:
- return None
- except Exception as e:
- st.warning(f"Graphique de répartition non disponible: {e}")
- return None
-
-def preprocess_train_image(filename, apply_augmentation=False):
- """Lance le preprocessing sur une image d'entraînement"""
- try:
- data = {
- "filename": filename,
- "apply_augmentation": apply_augmentation
- }
- response = requests.post(
- f"{API_BASE_URL}/preprocess-train-image",
- json=data,
- headers={"Content-Type": "application/json"}
- )
-
- if response.status_code == 200:
- return response.json()
- else:
- st.error(f"Erreur API: {response.status_code} - {response.text}")
- return None
- except Exception as e:
- st.error(f"Erreur lors du preprocessing: {e}")
- return None
-
-def get_sample_images():
- """Récupère la liste des images d'exemple"""
- try:
- response = requests.get(f"{API_BASE_URL}/sample-images")
- if response.status_code == 200:
- return response.json()
- else:
- st.error(f"Erreur lors de la récupération des images: {response.status_code}")
- return None
- except Exception as e:
- st.error(f"Erreur de connexion à l'API: {e}")
- return None
-
-def compare_sample_image(filename):
- """Lance une comparaison sur une image d'exemple"""
- try:
- data = {"filename": filename}
- response = requests.post(
- f"{API_BASE_URL}/compare-sample",
- json=data,
- headers={"Content-Type": "application/json"}
- )
-
- if response.status_code == 200:
- return response.json()
- else:
- st.error(f"Erreur API: {response.status_code} - {response.text}")
- return None
- except Exception as e:
- st.error(f"Erreur lors de la comparaison: {e}")
- return None
-
-def compare_uploaded_image(uploaded_file):
- """Lance une comparaison sur une image uploadée"""
- try:
- files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)}
- response = requests.post(f"{API_BASE_URL}/compare-upload", files=files)
-
- if response.status_code == 200:
- return response.json()
- else:
- st.error(f"Erreur API: {response.status_code} - {response.text}")
- return None
- except Exception as e:
- st.error(f"Erreur lors de la comparaison: {e}")
- return None
-
-def display_comparison_results(result, models_info, context=""):
- """Affiche les résultats de comparaison avec graphiques interactifs et accessibilité complète"""
- if not result or not result['success']:
- st.error("Aucun résultat de comparaison disponible")
- return
-
- # Informations des modèles
- st.markdown("### 🏆 Modèles Comparés")
-
- col1, col2 = st.columns(2)
-
- with col1:
- model1 = result['model1_info']
- # Formater les métriques séparément
- miou1 = f"{model1.get('test_mean_iou'):.4f}" if model1.get('test_mean_iou') is not None else 'N/A'
- acc1 = f"{model1.get('test_accuracy'):.4f}" if model1.get('test_accuracy') is not None else 'N/A'
-
- st.markdown(f"""
-
-
🥇 Modèle #1
- {model1.get('architecture', 'N/A')} - {model1['encoder_name']}
- 📐 Taille: {model1['input_size']}
- 📊 mIoU: {miou1}
- 🎯 Accuracy: {acc1}
-
- """, unsafe_allow_html=True)
-
- with col2:
- model2 = result['model2_info']
- # Formater les métriques séparément
- miou2 = f"{model2.get('test_mean_iou'):.4f}" if model2.get('test_mean_iou') is not None else 'N/A'
- acc2 = f"{model2.get('test_accuracy'):.4f}" if model2.get('test_accuracy') is not None else 'N/A'
-
- st.markdown(f"""
-
-
🥈 Modèle #2
- {model2.get('architecture', 'N/A')} - {model2['encoder_name']}
- 📐 Taille: {model2['input_size']}
- 📊 mIoU: {miou2}
- 🎯 Accuracy: {acc2}
-
- """, unsafe_allow_html=True)
-
- # Comparaison des temps d'inférence
- st.markdown("### ⏱️ Comparaison des Performances")
-
- speed_comp = result['speed_comparison']
- model1_time = result['model1_inference_time'] * 1000
- model2_time = result['model2_inference_time'] * 1000
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- winner_class = "speed-winner" if speed_comp['faster_model'] == model1['encoder_name'] else "speed-slower"
- st.markdown(f"""
-
-
🥇 {model1.get('architecture', 'N/A')} - {model1['encoder_name']}
- {model1_time:.1f} ms
-
- """, unsafe_allow_html=True)
-
- with col2:
- st.markdown(f"""
-
-
⚡ Comparaison
- {speed_comp['faster_model']}
- est {speed_comp['speedup_ratio']}x plus rapide
- Différence: {speed_comp['time_difference_ms']:.1f} ms
-
- """, unsafe_allow_html=True)
-
- with col3:
- winner_class = "speed-winner" if speed_comp['faster_model'] == model2['encoder_name'] else "speed-slower"
- st.markdown(f"""
-
-
🥈 {model2.get('architecture', 'N/A')} - {model2['encoder_name']}
- {model2_time:.1f} ms
-
- """, unsafe_allow_html=True)
-
- # Affichage des segmentations avec descriptions alternatives CONDITIONNELLES
- if result.get('figure_data'):
- st.markdown("### 🎨 Résultats de Segmentation")
-
- try:
- fig_bytes = base64.b64decode(result['figure_data'])
- fig = pickle.loads(fig_bytes)
-
- st.pyplot(fig)
- plt.close(fig)
-
- except Exception as e:
- st.error(f"Erreur lors de l'affichage: {e}")
- # Description alternative SEULEMENT en cas d'erreur
- alt_text = generate_comprehensive_alt_text("comparison_grid")
- st.warning(f"**Description alternative:** {alt_text}")
-
- # Section interactive (CE2 - Graphiques interactifs) - SEULEMENT 2 ONGLETS
- st.markdown("### 📊 Analyse Interactive des Résultats")
-
- if result.get('model1_stats') and result.get('model2_stats'):
- # SUPPRIMER l'onglet "Performances"
- tab1, tab2 = st.tabs(["🥧 Répartitions", "📊 Comparaison"])
-
- with tab1:
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown("#### Répartition Modèle 1")
- sizes1 = [data['percentage'] for data in result['model1_stats'].values()]
- labels1 = list(result['model1_stats'].keys())
-
- fig_pie1 = create_interactive_pie_chart(sizes1, labels1,
- f"Distribution - {result['model1_info'].get('architecture', 'N/A')} {result['model1_info']['encoder_name']}",
- models_info)
- # AJOUTER key unique pour éviter l'erreur Streamlit
- st.plotly_chart(fig_pie1, use_container_width=True, key=f"pie_model1_{context}")
-
- with col2:
- st.markdown("#### Répartition Modèle 2")
- sizes2 = [data['percentage'] for data in result['model2_stats'].values()]
- labels2 = list(result['model2_stats'].keys())
-
- fig_pie2 = create_interactive_pie_chart(sizes2, labels2,
- f"Distribution - {result['model2_info'].get('architecture', 'N/A')} {result['model2_info']['encoder_name']}",
- models_info)
- # AJOUTER key unique pour éviter l'erreur Streamlit
- st.plotly_chart(fig_pie2, use_container_width=True, key=f"pie_model2_{context}")
-
- with tab2:
- st.markdown("#### Comparaison Interactive des Classes")
-
- fig_comparison = create_interactive_comparison_chart(
- result['model1_stats'], result['model2_stats'],
- f"{result['model1_info'].get('architecture', 'N/A')} {result['model1_info']['encoder_name']}",
- f"{result['model2_info'].get('architecture', 'N/A')} {result['model2_info']['encoder_name']}"
- )
- # AJOUTER key unique pour éviter l'erreur Streamlit
- st.plotly_chart(fig_comparison, use_container_width=True, key=f"comparison_chart_{context}")
-
- st.info("💡 **Interactivité :** Survolez les barres, cliquez sur la légende, zoomez en sélectionnant une zone.")
- # SUPPRIMER la description systématique
-
- # Garder seulement le tableau détaillé (sans graphique redondant)
- if result.get('model1_stats') and result.get('model2_stats'):
- display_class_comparison(result['model1_stats'], result['model2_stats'],
- result['model1_info'], result['model2_info'], models_info)
-
-# def display_model_stats_chart(stats, models_info, title):
-# """Affiche les statistiques d'un modèle sous forme de graphique accessible"""
-# if not stats:
-# st.warning("Aucune statistique disponible")
-# return
-
-# # Utiliser l'ordre des classes du modèle pour la cohérence
-# if models_info and 'class_names' in models_info:
-# ordered_class_names = [name for name in models_info['class_names'] if name in stats]
-# else:
-# ordered_class_names = sorted(stats.keys())
-
-# percentages = [stats[name]['percentage'] for name in ordered_class_names]
-
-# # Couleurs accessibles WCAG
-# accessible_colors = [
-# WCAG_ACCESSIBLE_COLORS['neutral_medium'], # Flat: gris moyen
-# WCAG_ACCESSIBLE_COLORS['primary_red'], # Human: rouge
-# WCAG_ACCESSIBLE_COLORS['secondary_purple'], # Vehicle: violet
-# WCAG_ACCESSIBLE_COLORS['primary_orange'], # Construction: orange
-# WCAG_ACCESSIBLE_COLORS['secondary_teal'], # Object: teal
-# WCAG_ACCESSIBLE_COLORS['primary_green'], # Nature: vert
-# WCAG_ACCESSIBLE_COLORS['primary_blue'], # Sky: bleu
-# WCAG_ACCESSIBLE_COLORS['neutral_dark'] # Void: gris foncé
-# ]
-
-# colors = accessible_colors[:len(ordered_class_names)]
-
-# # Créer le graphique accessible
-# fig, ax, displayed = create_accessible_bar_chart_conditional(
-# percentages, ordered_class_names, title,
-# colors=colors, use_patterns=True
-# )
-
-# # Description alternative SEULEMENT si erreur d'affichage
-# if not displayed:
-# alt_text = generate_alt_text_for_bar(
-# percentages, ordered_class_names, title, unit="%"
-# )
-# st.warning(f"**Description du graphique:** {alt_text}")
-
-# st.pyplot(fig)
-# plt.close(fig)
-
-# # Tableau de données complémentaire (toujours affiché)
-# table_data = []
-# for name, percentage in zip(ordered_class_names, percentages):
-# pixels = stats[name]['pixels']
-# table_data.append([name, f"{percentage:.1f}%", f"{pixels:,} pixels"])
-
-# create_data_table(
-# table_data,
-# ["Classe", "Pourcentage", "Nombre de pixels"],
-# f"Données - {title}",
-# "Répartition détaillée des classes de segmentation"
-# )
-
-def display_class_comparison(stats1, stats2, model1, model2, models_info):
- """Affiche uniquement le tableau détaillé de comparaison des classes (sans graphique redondant)"""
- st.markdown("### 🔎 Tableau Détaillé des Comparaisons")
-
- # Préparer les données
- if models_info and 'class_names' in models_info:
- all_classes_in_stats = set(stats1.keys()) | set(stats2.keys())
- ordered_all_classes = [name for name in models_info['class_names'] if name in all_classes_in_stats]
- else:
- all_classes = set(stats1.keys()) | set(stats2.keys())
- ordered_all_classes = sorted(all_classes)
-
- class_names = ordered_all_classes
- model1_values = [stats1.get(name, {}).get('percentage', 0) for name in class_names]
- model2_values = [stats2.get(name, {}).get('percentage', 0) for name in class_names]
-
- # SUPPRIMER tout le code de génération du graphique matplotlib
- # GARDER seulement le tableau
-
- table_data = []
- for i, name in enumerate(class_names):
- val1 = model1_values[i]
- val2 = model2_values[i]
- diff = val1 - val2
-
- if abs(diff) < 1:
- category = "🟢 Similaire"
- elif abs(diff) < 5:
- category = "🟡 Différence modérée"
- else:
- category = "🔴 Différence importante"
-
- table_data.append([
- name,
- f"{val1:.1f}%",
- f"{val2:.1f}%",
- f"{diff:+.1f}%",
- category
- ])
-
- create_data_table(
- table_data,
- [
- "Classe",
- f"🥇 {model1.get('architecture', 'N/A')[:15]} (%)",
- f"🥈 {model2.get('architecture', 'N/A')[:15]} (%)",
- "Différence",
- "Évaluation"
- ],
- "Comparaison détaillée par classe",
- "Analyse des écarts de performance entre les deux modèles pour chaque classe de segmentation"
- )
-
-# def display_class_comparison(stats1, stats2, model1, model2, models_info):
-# """Affiche une comparaison accessible des classes entre les deux modèles"""
-# st.markdown("### 🔍 Analyse Comparative par Classe")
-
-# # Préparer les données
-# if models_info and 'class_names' in models_info:
-# all_classes_in_stats = set(stats1.keys()) | set(stats2.keys())
-# ordered_all_classes = [name for name in models_info['class_names'] if name in all_classes_in_stats]
-# else:
-# all_classes = set(stats1.keys()) | set(stats2.keys())
-# ordered_all_classes = sorted(all_classes)
-
-# class_names = ordered_all_classes
-# model1_values = [stats1.get(name, {}).get('percentage', 0) for name in class_names]
-# model2_values = [stats2.get(name, {}).get('percentage', 0) for name in class_names]
-
-# # Couleurs accessibles avec motifs différents
-# fig, ax = plt.subplots(figsize=(12, 7))
-
-# x = np.arange(len(class_names))
-# width = 0.35
-
-# # Couleurs accessibles
-# color1 = WCAG_ACCESSIBLE_COLORS['primary_blue']
-# color2 = WCAG_ACCESSIBLE_COLORS['primary_red']
-
-# bars1 = ax.bar(x - width/2, model1_values, width,
-# label=f"{model1.get('architecture', 'N/A')} - {model1['encoder_name']}",
-# color=color1, alpha=0.8, edgecolor='#333333')
-
-# bars2 = ax.bar(x + width/2, model2_values, width,
-# label=f"{model2.get('architecture', 'N/A')} - {model2['encoder_name']}",
-# color=color2, alpha=0.8, edgecolor='#333333', hatch='///')
-
-# # Style accessible
-# ax.set_xlabel('Classes', fontsize=11, fontweight='bold', color='#333333')
-# ax.set_ylabel('Pourcentage (%)', fontsize=11, fontweight='bold', color='#333333')
-# ax.set_title('Comparaison des classes entre les deux modèles',
-# fontsize=13, fontweight='bold', pad=15, color='#333333')
-# ax.set_xticks(x)
-
-# if len(class_names) <= 4:
-# ax.set_xticklabels(class_names, fontsize=10, color='#333333')
-# elif len(class_names) <= 6:
-# ax.set_xticklabels(class_names, rotation=30, ha='right', fontsize=9, color='#333333')
-# else:
-# ax.set_xticklabels(class_names, rotation=45, ha='right', fontsize=9, color='#333333')
-
-# ax.legend(loc='upper right', fontsize=10)
-# ax.grid(axis='y', alpha=0.3, linestyle='--', color='#666666')
-# ax.set_axisbelow(True)
-
-# # Style des axes
-# ax.spines['top'].set_visible(False)
-# ax.spines['right'].set_visible(False)
-# ax.spines['left'].set_color('#666666')
-# ax.spines['bottom'].set_color('#666666')
-
-# # Valeurs sur les barres
-# for bar1, bar2, val1, val2 in zip(bars1, bars2, model1_values, model2_values):
-# height1, height2 = bar1.get_height(), bar2.get_height()
-# ax.text(bar1.get_x() + bar1.get_width()/2., height1 + 0.5,
-# f'{val1:.1f}%', ha='center', va='bottom', fontsize=8,
-# fontweight='bold', color='#333333')
-# ax.text(bar2.get_x() + bar2.get_width()/2., height2 + 0.5,
-# f'{val2:.1f}%', ha='center', va='bottom', fontsize=8,
-# fontweight='bold', color='#333333')
-
-# plt.tight_layout()
-
-# # Description alternative
-# alt_text = f"Graphique comparatif en barres des classes entre {model1['encoder_name']} et {model2['encoder_name']}. "
-# comparisons = []
-# for name, val1, val2 in zip(class_names, model1_values, model2_values):
-# comparisons.append(f"{name}: {val1:.1f}% vs {val2:.1f}%")
-# alt_text += "Comparaisons: " + "; ".join(comparisons)
-
-# st.markdown(f"**Description du graphique:** {alt_text}")
-# st.pyplot(fig)
-# plt.close(fig)
-
-# # Tableau détaillé accessible
-# st.markdown("**🔎 Tableau détaillé des comparaisons:**")
-
-# table_data = []
-# for i, name in enumerate(class_names):
-# val1 = model1_values[i]
-# val2 = model2_values[i]
-# diff = val1 - val2
-
-# if abs(diff) < 1:
-# category = "🟢 Similaire"
-# elif abs(diff) < 5:
-# category = "🟡 Différence modérée"
-# else:
-# category = "🔴 Différence importante"
-
-# table_data.append([
-# name,
-# f"{val1:.1f}%",
-# f"{val2:.1f}%",
-# f"{diff:+.1f}%",
-# category
-# ])
-
-# create_data_table(
-# table_data,
-# [
-# "Classe",
-# f"🥇 {model1.get('architecture', 'N/A')[:15]} (%)",
-# f"🥈 {model2.get('architecture', 'N/A')[:15]} (%)",
-# "Différence",
-# "Évaluation"
-# ],
-# "Comparaison détaillée par classe",
-# "Analyse des écarts de performance entre les deux modèles pour chaque classe de segmentation"
-# )
-
-
-# Onglets principaux
-main_tab1, main_tab2 = st.tabs(["📊 Présentation Jeu de données Cityscapes", "🔄 Comparaison de performances"])
-
-# ===== ONGLET 1: PRÉSENTATION DATASET =====
-with main_tab1:
- st.markdown("## 📊 Analyse Exploratoire - Dataset Cityscapes")
-
- # Informations générales sur le dataset
- dataset_info = get_dataset_info()
-
- if dataset_info:
- # Section méthodologique MODIFIÉE
- st.markdown("""
- ### 🎯 Présentation générale du dataset Cityscapes
-
- #### Dataset retenu
-
- **Cityscapes Dataset : Référence en Segmentation Urbaine**
- """)
-
- # Spécificités techniques
- st.markdown("""
- ### ⚙️ Spécificités techniques
-
- - **Taille Datase**: 25 000 images haute résolution
- - **Résolutions d'acquisition** : Données natives 2048×1024 pixels
- - **Conditions d'acquisition** : Conditions météo bonnes à moyennes, pas de conditions adverses (pluie, neige)
- - **Diversité géographique** : Villes allemandes et suisses (architectures variées)
- - **Annotations pixel-perfect** : Masques de segmentation précis au pixel près (Train + Val)
-
- Ce dataset permet d'évaluer la capacité des modèles à comprendre les scènes urbaines complexes, élément fondamental pour les applications de conduite autonome et de surveillance urbaine intelligente.
- """)
-
- # Composition du dataset avec pie charts NOUVEAU
- st.markdown("### 🗂️ Composition et utilisation du dataset")
-
- # Version finale qui combine : tailles uniformes + couleurs WCAG + descriptions conditionnelles
- if dataset_info.get('splits'):
- splits_info = dataset_info['splits']
- project_splits = dataset_info.get('project_splits', {})
-
- st.markdown("#### 📊 Visualisations accessibles")
-
- # Option pour activer les motifs (accessibilité)
- use_patterns = st.checkbox(
- "🎨 Activer les motifs (accessibilité visuelle)",
- help="Active les motifs en plus des couleurs pour faciliter la distinction des données",
- value=True
- )
-
- # Organiser en 1 ligne de 4 colonnes
- col1, col2, col3, col4 = st.columns(4)
-
- # Configuration uniforme pour tous les graphiques (SOLUTION QUI MARCHAIT)
- uniform_figsize = (6, 6)
- uniform_textprops = {'fontsize': 8, 'fontweight': 'bold', 'color': '#333333'}
- uniform_title_props = {'fontsize': 10, 'fontweight': 'bold', 'pad': 15}
-
- # Couleurs WCAG accessibles
- accessible_colors = {
- 'blue': WCAG_ACCESSIBLE_COLORS['primary_blue'],
- 'red': WCAG_ACCESSIBLE_COLORS['primary_red'],
- 'orange': WCAG_ACCESSIBLE_COLORS['primary_orange'],
- 'gray': WCAG_ACCESSIBLE_COLORS['neutral_medium'],
- 'light_gray': WCAG_ACCESSIBLE_COLORS['neutral_light']
- }
-
- # Motifs pour accessibilité
- patterns = ['///', '|||', '---', '...', 'xxx', None] if use_patterns else [None] * 6
-
- # Données pour le tableau récapitulatif
- pie_data_for_table = []
-
- with col1:
- # Pie chart Train (avec répartition projet)
- fig_train, ax_train = plt.subplots(figsize=uniform_figsize, tight_layout=True)
-
- train_total = splits_info['train']['images']
- train_our_train = project_splits['train']['images']
- train_our_val = project_splits['validation']['images']
-
- sizes = [train_our_train, train_our_val]
- labels = [f'Notre Entraînement\n{train_our_train} images (80%)',
- f'Notre Validation\n{train_our_val} images (20%)']
- colors = [accessible_colors['blue'], accessible_colors['red']]
- explode = (0.05, 0.05)
-
- try:
- wedges, texts, autotexts = ax_train.pie(
- sizes, labels=labels, colors=colors, autopct='%1.0f%%',
- startangle=90, explode=explode, shadow=True,
- textprops=uniform_textprops,
- wedgeprops={'edgecolor': 'white', 'linewidth': 2}
- )
-
- # Ajouter des motifs si demandé
- if use_patterns:
- for wedge, pattern in zip(wedges, patterns[:2]):
- if pattern:
- wedge.set_hatch(pattern)
-
- ax_train.set_title(f'Split Train Original\n{train_total} images total',
- **uniform_title_props, color='#333333')
- ax_train.axis('equal')
-
- chart_displayed = True
-
- except Exception as e:
- ax_train.text(0.5, 0.5, f"Erreur d'affichage\n{str(e)}",
- ha='center', va='center', transform=ax_train.transAxes,
- fontsize=10, color='red', weight='bold')
- chart_displayed = False
-
- # Description SEULEMENT si erreur
- if not chart_displayed:
- alt_text = generate_alt_text_for_pie(sizes, labels, "Split Train")
- st.warning(f"**Description du graphique:** {alt_text}")
-
- st.pyplot(fig_train)
- plt.close(fig_train)
-
- pie_data_for_table.extend([
- ["Train - Entraînement", train_our_train, "80%", "Entraînement des modèles"],
- ["Train - Validation", train_our_val, "20%", "Validation pendant entraînement"]
- ])
-
- with col2:
- # Pie chart Validation (utilisation complète)
- fig_val, ax_val = plt.subplots(figsize=uniform_figsize, tight_layout=True)
-
- val_total = splits_info['validation']['images']
- val_our_test = project_splits['test']['images']
-
- sizes = [val_our_test]
- labels = [f'Notre Test Final\n{val_our_test} images (100%)']
- colors = [accessible_colors['orange']]
-
- try:
- wedges, texts, autotexts = ax_val.pie(
- sizes, labels=labels, colors=colors, autopct='%1.0f%%',
- startangle=90, shadow=True, textprops=uniform_textprops,
- wedgeprops={'edgecolor': 'white', 'linewidth': 2}
- )
-
- if use_patterns and patterns[2]:
- wedges[0].set_hatch(patterns[2])
-
- ax_val.set_title(f'Split Validation Original\n{val_total} images total',
- **uniform_title_props, color='#333333')
- ax_val.axis('equal')
-
- chart_displayed = True
-
- except Exception as e:
- ax_val.text(0.5, 0.5, f"Erreur d'affichage\n{str(e)}",
- ha='center', va='center', transform=ax_val.transAxes,
- fontsize=10, color='red', weight='bold')
- chart_displayed = False
-
- if not chart_displayed:
- alt_text = generate_alt_text_for_pie(sizes, labels, "Split Validation")
- st.warning(f"**Description du graphique:** {alt_text}")
-
- st.pyplot(fig_val)
- plt.close(fig_val)
-
- pie_data_for_table.append(["Validation - Test", val_our_test, "100%", "Test final des modèles"])
-
- with col3:
- # Pie chart Test (non utilisé)
- fig_test, ax_test = plt.subplots(figsize=uniform_figsize, tight_layout=True)
-
- test_total = splits_info['test']['images']
-
- sizes = [test_total]
- labels = [f'Non utilisé dans notre projet\n{test_total} images\n(annotations privées)']
- colors = [accessible_colors['gray']]
-
- try:
- wedges, texts, autotexts = ax_test.pie(
- sizes, labels=labels, colors=colors, autopct='',
- startangle=90, shadow=True, textprops=uniform_textprops,
- wedgeprops={'edgecolor': 'white', 'linewidth': 2}
- )
-
- if use_patterns and patterns[3]:
- wedges[0].set_hatch(patterns[3])
-
- ax_test.set_title(f'Split Test Original\n{test_total} images total',
- **uniform_title_props, color='#333333')
- ax_test.axis('equal')
-
- chart_displayed = True
-
- except Exception as e:
- ax_test.text(0.5, 0.5, f"Erreur d'affichage\n{str(e)}",
- ha='center', va='center', transform=ax_test.transAxes,
- fontsize=10, color='red', weight='bold')
- chart_displayed = False
-
- if not chart_displayed:
- alt_text = generate_alt_text_for_pie(sizes, labels, "Split Test")
- st.warning(f"**Description du graphique:** {alt_text}")
-
- st.pyplot(fig_test)
- plt.close(fig_test)
-
- pie_data_for_table.append(["Test", 0, "0%", "Non utilisé (annotations privées)"])
-
- with col4:
- # Pie chart Coarse (non utilisé)
- fig_coarse, ax_coarse = plt.subplots(figsize=uniform_figsize, tight_layout=True)
-
- coarse_total = splits_info['coarse']['images']
-
- sizes = [coarse_total]
- labels = [f'Non utilisé dans notre projet\n{coarse_total} images\n(annotations grossières)']
- colors = [accessible_colors['light_gray']]
-
- try:
- wedges, texts, autotexts = ax_coarse.pie(
- sizes, labels=labels, colors=colors, autopct='',
- startangle=90, shadow=True, textprops=uniform_textprops,
- wedgeprops={'edgecolor': 'white', 'linewidth': 2}
- )
-
- if use_patterns and patterns[4]:
- wedges[0].set_hatch(patterns[4])
-
- ax_coarse.set_title(f'Split Coarse Original\n{coarse_total} images total',
- **uniform_title_props, color='#333333')
- ax_coarse.axis('equal')
-
- chart_displayed = True
-
- except Exception as e:
- ax_coarse.text(0.5, 0.5, f"Erreur d'affichage\n{str(e)}",
- ha='center', va='center', transform=ax_coarse.transAxes,
- fontsize=10, color='red', weight='bold')
- chart_displayed = False
-
- if not chart_displayed:
- alt_text = generate_alt_text_for_pie(sizes, labels, "Split Coarse")
- st.warning(f"**Description du graphique:** {alt_text}")
-
- st.pyplot(fig_coarse)
- plt.close(fig_coarse)
-
- pie_data_for_table.append(["Coarse", 0, "0%", "Non utilisé (annotations grossières)"])
-
-
-
-
- st.markdown("### 📊 Exploration Interactive du Dataset")
-
- fig_dataset = create_dataset_interactive_overview()
-
- try:
- st.plotly_chart(fig_dataset, use_container_width=True, key="dataset_overview")
- except Exception as e:
- st.warning("**Description :** Graphique circulaire interactif montrant la répartition de l'utilisation du dataset Cityscapes dans notre projet.")
- st.info("💡 **Interactivité :** Survolez les sections pour voir les détails, cliquez sur la légende pour masquer/afficher des catégories.")
-
- # Tableau de données accessible (toujours affiché)
- st.markdown("#### 📋 Tableau récapitulatif des données")
-
- create_data_table(
- pie_data_for_table,
- ["Split", "Images utilisées", "Pourcentage", "Utilisation"],
- "Répartition du dataset Cityscapes",
- "Détail de l'utilisation de chaque split du dataset original dans notre projet"
- )
-
-
-
- # Architecture des classes
- st.markdown("### 🏗️ Architecture de classes hiérarchique")
- st.markdown("""
- Le dataset organise 34 classes détaillées en **8 méta-classes** pour la segmentation :
-
- | Méta-classe | Description | Importance |
- |-------------|-------------|------------|
- | **Flat** | Route, trottoir, parkings | Navigation de base |
- | **Human** | Piéton, cycliste | **Sécurité critique** |
- | **Vehicle** | Voiture, camion, bus, train | Obstacles dynamiques |
- | **Construction** | Bâtiment, mur, clôture, pont | Structure urbaine |
- | **Object** | Poteau, feu, panneau | Signalétique |
- | **Nature** | Végétation, terrain | Environnement |
- | **Sky** | Ciel | Contexte |
- | **Void** | Zones non-définies | Exclusion |
- """)
-
- # Graphique de répartition des classes
- st.markdown("### 📊 Répartition des classes dans le dataset d'entraînement")
-
- class_repartition_data = get_class_repartition()
- if class_repartition_data:
- try:
- class_repartition_image = Image.open(io.BytesIO(class_repartition_data))
- st.image(
- class_repartition_image,
- caption="Distribution des classes dans les images d'entraînement Cityscapes",
- use_container_width=True
- )
- except Exception as e:
- st.warning("Impossible d'afficher le graphique de répartition des classes")
- else:
- st.info("Graphique de répartition des classes non disponible")
-
- # Section preprocessing des images
- st.markdown("---")
- st.markdown("### 🔧 Exemple de Preprocessing d'Images")
-
- # Récupérer les images d'entraînement disponibles
- train_data = get_train_images()
- if train_data and train_data['images']:
- # Sélecteur d'image
- image_options = {img['display_name']: img['filename'] for img in train_data['images']}
- selected_display_name = st.selectbox(
- "Choisissez une image d'entraînement pour voir les transformations:",
- options=list(image_options.keys()),
- help=f"{train_data['total_count']} images disponibles",
- key="train_image_selector"
- )
-
- selected_filename = image_options[selected_display_name]
-
- # Preview de l'image sélectionnée (avant preprocessing)
- if selected_filename and "preprocessing_result" not in st.session_state:
- try:
- image_url = f"{API_BASE_URL}/train-image/{selected_filename}"
- response = requests.get(image_url, timeout=5)
-
- if response.status_code == 200:
- preview_image = Image.open(io.BytesIO(response.content))
- st.image(
- preview_image,
- caption=f"Aperçu de l'image sélectionnée : {selected_display_name}",
- use_container_width=True
- )
- except:
- st.info("Aperçu non disponible")
-
- # Options de preprocessing
- col1, col2 = st.columns(2)
- with col1:
- show_augmentation = st.checkbox(
- "Inclure l'augmentation aléatoire",
- help="Affiche un exemple d'augmentation de données utilisée pendant l'entraînement. Cliquez plusieurs fois sur 'Appliquer le Preprocessing' pour tester différentes augmentations aléatoires !",
- key="augmentation_checkbox"
- )
-
- with col2:
- if st.button("🔧 Appliquer le Preprocessing", type="primary", key="apply_preprocessing"):
- with st.spinner("Application des transformations..."):
- result = preprocess_train_image(selected_filename, show_augmentation)
-
- if result and result['success']:
- st.session_state.preprocessing_result = result
- st.success("Preprocessing terminé!")
- st.rerun()
- else:
- st.error("Erreur lors du preprocessing")
-
- # Bouton pour revenir à l'aperçu
- if "preprocessing_result" in st.session_state:
- if st.button("🔄 Choisir une nouvelle image", type="secondary", key="reset_preprocessing"):
- del st.session_state.preprocessing_result
- st.rerun()
-
- # Affichage des résultats de preprocessing
- if "preprocessing_result" in st.session_state:
- result = st.session_state.preprocessing_result
-
- # Affichage des images transformées sur toute la largeur
- st.markdown("#### Résultats des transformations :")
-
- if show_augmentation and result.get('augmented_image'):
- cols = st.columns(3)
- col_titles = ["Image Originale (redimensionnée)", "Après Normalisation", "Avec Augmentation Aléatoire"]
- col_data = [result.get('original_image'), result.get('normalized_image'), result.get('augmented_image')]
- else:
- cols = st.columns(2)
- col_titles = ["Image Originale (redimensionnée)", "Après Normalisation"]
- col_data = [result.get('original_image'), result.get('normalized_image')]
-
- for i, (col, title, data) in enumerate(zip(cols, col_titles, col_data)):
- with col:
- st.markdown(f"**{title}**")
- if data:
- img_data = base64.b64decode(data)
- img = Image.open(io.BytesIO(img_data))
- st.image(img, use_container_width=True)
-
-
- # Explications des transformations appliquées
- if result.get('transformations_applied'):
- st.markdown("#### 📋 Transformations appliquées :")
-
- # Préparer les transformations pour l'alignement
- transforms = result['transformations_applied']
-
- # Extraire les transformations de base (toujours présentes)
- base_transforms = [t for t in transforms if "Resize" in t or "Normalization" in t]
- augmentation_transforms = [t for t in transforms if "Resize" not in t and "Normalization" not in t]
-
- if show_augmentation and result.get('augmented_image'):
- # 3 colonnes quand augmentation active
- col1, col2, col3 = st.columns(3)
-
- with col1:
- st.markdown("**Image Originale (redimensionnée)**")
- resize_transform = next((t for t in base_transforms if "Resize" in t), "1. Resize (512x512)")
- st.write(f"• {resize_transform}")
-
- with col2:
- st.markdown("**Après Normalisation**")
- norm_transform = next((t for t in base_transforms if "Normalization" in t), "2. Normalization (0-1)")
- st.write(f"• {norm_transform}")
-
- with col3:
- st.markdown("**Avec Augmentation Aléatoire**")
- if augmentation_transforms:
- for i, transform in enumerate(augmentation_transforms, 3):
- st.write(f"• {i}. {transform}")
- else:
- st.write("• 3. Aucune augmentation appliquée (aléatoire)")
-
- else:
- # 2 colonnes quand pas d'augmentation
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown("**Image Originale (redimensionnée)**")
- resize_transform = next((t for t in base_transforms if "Resize" in t), "1. Resize (512x512)")
- st.write(f"• {resize_transform}")
-
- with col2:
- st.markdown("**Après Normalisation**")
- norm_transform = next((t for t in base_transforms if "Normalization" in t), "2. Normalization (0-1)")
- st.write(f"• {norm_transform}")
-
- # Explications détaillées avec description complète des augmentations
- with st.expander("ℹ️ Détails des transformations possibles"):
- st.markdown("""
- **Normalisation standard (toujours appliquée) :**
- - **Redimensionnement** : Toutes les images sont redimensionnées à 512×512 pixels
- - **Normalisation** : Les valeurs de pixels sont normalisées de [0-255] vers [0-1]
- - Cette normalisation uniforme est appliquée à tous les modèles (paramètre encoder non utilisé)
-
- **Augmentations de données possibles (si activées) :**
-
- Le système sélectionne aléatoirement entre 1 et 4 transformations parmi les suivantes :
-
- 1. **Rotation aléatoire** : Rotation de l'image entre -15° et +15°
- 2. **Flip horizontal** : Miroir horizontal avec 50% de probabilité
- 3. **Changement de luminosité/contraste** : Ajustement de ±20% de la luminosité et du contraste
- 4. **Flou gaussien léger** : Application d'un flou gaussien subtil (blur 1-3 pixels)
- 5. **Décalage couleur** : Modification de la teinte, saturation et valeur (±10-15%)
- 6. **Zoom aléatoire** : Zoom de -10% à +10% (avec probabilité réduite)
-
- **Important :** Les augmentations sont **aléatoires** à chaque application. Cliquez plusieurs fois sur
- "Appliquer le Preprocessing" pour voir différentes combinaisons ! Le résultat peut inclure une ou
- plusieurs transformations, ou même aucune selon le hasard.
-
- Ces transformations permettent d'améliorer la robustesse et la généralisation du modèle pendant
- l'entraînement en exposant le modèle à des variations réalistes des données.
- """)
- else:
- st.warning("Aucune image d'entraînement disponible pour la démonstration")
-
-# ===== ONGLET 2: COMPARAISON DE PERFORMANCES =====
-with main_tab2:
- # Variables de session pour les résultats
- if "sample_comparison_result" not in st.session_state:
- st.session_state.sample_comparison_result = None
- if "upload_comparison_result" not in st.session_state:
- st.session_state.upload_comparison_result = None
-
- def reset_sample_comparison():
- st.session_state.sample_comparison_result = None
-
- def reset_upload_comparison():
- st.session_state.upload_comparison_result = None
-
- # Sous-onglets pour la comparaison
- sub_tab1, sub_tab2 = st.tabs(["🏙️ Images Test Cityscapes", "📤 Upload Personnel"])
-
- # SUB-TAB 1: Images Test Cityscapes
- with sub_tab1:
- st.markdown("## 🏙️ Comparaison sur Images Test Cityscapes")
- st.markdown("Comparez les performances des deux modèles sur des images Cityscapes avec masques de vérité.")
-
- # Récupérer les images d'exemple
- sample_data = get_sample_images()
- if sample_data and sample_data['images']:
- # Sélecteur d'image
- image_options = {img['display_name']: img['filename'] for img in sample_data['images']}
- selected_display_name = st.selectbox(
- "Choisissez une image:",
- options=list(image_options.keys()),
- help=f"{sample_data['total_count']} images disponibles"
- )
-
- selected_filename = image_options[selected_display_name]
-
- # Afficher les infos de l'image sélectionnée
- selected_img_info = next(img for img in sample_data['images'] if img['filename'] == selected_filename)
-
- col1, col2 = st.columns([2, 1])
- with col1:
- st.info(f"📁 **Fichier:** {selected_filename}")
- with col2:
- ground_truth_status = "✅ Disponible" if selected_img_info['has_ground_truth'] else "❌ Non disponible"
- st.info(f"🎯 **Vérité terrain:** {ground_truth_status}")
-
- # Prévisualisation de l'image sélectionnée
- if selected_filename:
- try:
- image_url = f"{API_BASE_URL}/sample-image/{selected_filename}"
- response = requests.get(image_url, timeout=5)
-
- if response.status_code == 200:
- sample_image = Image.open(io.BytesIO(response.content))
- st.image(
- sample_image,
- caption=f"Aperçu: {selected_display_name}",
- use_container_width=True
- )
- except:
- st.info("📷 Aperçu non disponible - L'image sera chargée lors de la comparaison")
-
- # Bouton de comparaison
- if st.button("🔄 Lancer la Comparaison", key="compare_sample", type="primary"):
- if selected_filename:
- with st.spinner("Comparaison en cours..."):
- result = compare_sample_image(selected_filename)
- if result and result['success']:
- st.session_state.sample_comparison_result = result
- st.success("✅ Comparaison terminée!")
- st.rerun()
- else:
- st.error("❌ Erreur lors de la comparaison")
- else:
- st.warning("Aucune image d'exemple disponible")
-
- # Affichage des résultats de comparaison
- if st.session_state.sample_comparison_result:
- st.markdown("---")
- models_info = get_models_info()
- display_comparison_results(st.session_state.sample_comparison_result, models_info, "sample")
-
- # Bouton de reset
- if st.button("🔄 Nouvelle Comparaison", key="reset_sample", type="secondary"):
- reset_sample_comparison()
- st.rerun()
-
- # SUB-TAB 2: Upload Personnel
- with sub_tab2:
- st.markdown("## 📤 Comparaison sur Image Personnelle")
- st.markdown("Uploadez une image pour comparer les performances des deux modèles.")
-
- uploaded_file = st.file_uploader(
- "Sélectionnez une image:",
- type=['jpg', 'jpeg', 'png'],
- help="Taille maximale recommandée: 5MB"
- )
-
- if uploaded_file is not None:
- # Afficher l'image uploadée
- image = Image.open(uploaded_file)
- st.image(image, caption=f"Image uploadée: {uploaded_file.name}", use_container_width=True)
-
- # Informations sur le fichier
- file_size_mb = len(uploaded_file.getvalue()) / 1024 / 1024
- st.info(f"📁 **Fichier:** {uploaded_file.name} | 📊 **Taille:** {file_size_mb:.2f} MB")
-
- # Bouton de comparaison
- if st.button("🔄 Lancer la Comparaison", key="compare_upload", type="primary"):
- with st.spinner("Comparaison en cours..."):
- result = compare_uploaded_image(uploaded_file)
- if result and result['success']:
- st.session_state.upload_comparison_result = result
- st.success("✅ Comparaison terminée!")
- st.rerun()
- else:
- st.error("❌ Erreur lors de la comparaison")
-
- # Affichage des résultats de comparaison
- if st.session_state.upload_comparison_result:
- st.markdown("---")
- models_info = get_models_info()
- display_comparison_results(st.session_state.upload_comparison_result, models_info, "upload")
-
- # Bouton de reset
- if st.button("🔄 Nouvelle Comparaison", key="reset_upload", type="secondary"):
- reset_upload_comparison()
- st.rerun()
-
-# Sidebar avec informations des modèles
-with st.sidebar:
- st.markdown("## 🔧 Informations des Modèles")
-
- models_info = get_models_info()
- if models_info:
- st.success("✅ Modèles chargés")
-
- # Modèle 1
- best_model = models_info.get('best_model', {})
- st.markdown("**🥇 Meilleur Modèle:**")
- st.write(f"**Architecture:** {best_model.get('architecture', 'N/A')}")
- st.write(f"**Encoder:** {best_model.get('encoder_name', 'N/A')}")
- st.write(f"**Résolution:** {best_model.get('input_size', 'N/A')}")
-
- # Modèle 2
- second_model = models_info.get('second_best_model', {})
- st.markdown("**🥈 Deuxième Modèle:**")
- st.write(f"**Architecture:** {second_model.get('architecture', 'N/A')}")
- st.write(f"**Encoder:** {second_model.get('encoder_name', 'N/A')}")
- st.write(f"**Résolution:** {second_model.get('input_size', 'N/A')}")
- else:
- st.error("❌ Modèles non disponibles")
-
- st.markdown("---")
- st.markdown("## 🎯 Classes Segmentées (IoU)")
-
- # Affichage avec carrés colorés et IoU
- if models_info and 'class_names' in models_info and 'class_colors' in models_info:
- class_names = models_info['class_names']
- class_colors = models_info['class_colors']
-
- # Récupérer les IoU des modèles
- best_ious = best_model.get('class_ious', {}) if models_info else {}
- second_ious = second_model.get('class_ious', {}) if models_info else {}
-
- for i, (class_name, color) in enumerate(zip(class_names, class_colors)):
- color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
-
- # Construire la chaîne IoU
- iou_str = ""
- if best_ious.get(class_name) is not None and second_ious.get(class_name) is not None:
- best_iou = best_ious[class_name]
- second_iou = second_ious[class_name]
- iou_str = f" (🥇{best_iou:.2f} - 🥈{second_iou:.2f})"
- elif best_ious.get(class_name) is not None:
- best_iou = best_ious[class_name]
- iou_str = f" (🥇{best_iou:.2f})"
- elif second_ious.get(class_name) is not None:
- second_iou = second_ious[class_name]
- iou_str = f" (🥈{second_iou:.2f})"
-
- st.markdown(f"""
-
-
-
{i}. {class_name}{iou_str}
-
- """, unsafe_allow_html=True)
-
-# Footer
-st.markdown("---")
-st.markdown("""
-
- 🚗 Street Vision - Comparaison de Modèles -
- Évaluation comparative de modèles de segmentation sémantique
- Développé avec FastAPI, Streamlit et PyTorch
-
-""", unsafe_allow_html=True)
-
-
-with st.sidebar:
- st.markdown("---")
- st.markdown("## ♿ Conformité WCAG 2.1")
-
- criteria_status = {
- "1.1.1 Contenu non textuel": "✅",
- "1.4.1 Utilisation de la couleur": "✅",
- "1.4.3 Contraste (minimum)": "✅",
- "1.4.4 Redimensionnement du texte": "✅",
- "2.4.2 Titre de page": "✅"
- }
-
- for criterion, status in criteria_status.items():
- st.write(f"{status} {criterion}")
-
- st.markdown("**Niveau :** AA | **Référence :** [WCAG 2.1](https://www.w3.org/WAI/WCAG21/quickref/)")
-
+import os
+import sys
+import requests
+import streamlit as st
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import plotly.express as px
+import plotly.graph_objects as go
+from PIL import Image
+import io
+import base64
+import pickle
+
+# Configuration de l'API
+API_BASE_URL = os.getenv("MULTISEG_API_BASE_URL", "http://127.0.0.1:8080/api")
+
+# Import des fonctions utils pour l'affichage local
+sys.path.append('/app')
+sys.path.append('.')
+from utils.utils import colorize_mask
+
+# Configuration de la page avec titre approprié (WCAG 2.4.2)
+st.set_page_config(
+ page_title="Street Vision - Segmentation Comparison | Dashboard Accessible",
+ page_icon="🚗",
+ layout="wide"
+)
+
+
+# Ajouts à placer au début de streamlit_app.py après les imports
+
+# Palettes de couleurs conformes WCAG AA (ratio contraste ≥ 3:1)
+WCAG_ACCESSIBLE_COLORS = {
+ 'primary_blue': '#0066CC', # [0, 102, 204]
+ 'primary_red': '#CC0000', # [204, 0, 0]
+ 'primary_green': '#006600', # [0, 102, 0]
+ 'primary_orange': '#CC6600', # [204, 102, 0]
+ 'secondary_purple': '#663399', # [102, 51, 153]
+ 'secondary_teal': '#006666', # [0, 102, 102]
+ 'neutral_dark': '#333333', # [51, 51, 51]
+ 'neutral_medium': '#666666', # [102, 102, 102]
+ 'neutral_light': '#CCCCCC', # Pour éléments désactivés
+ 'background_alt': '#F5F5F5' # Fond alternatif accessible
+}
+
+# Convertir hex vers RGB
+def hex_to_rgb(hex_color):
+ """Convertit une couleur hex en tuple RGB"""
+ hex_color = hex_color.lstrip('#')
+ return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
+
+# Couleurs RGB pour les masques (dans le même ordre que les classes)
+WCAG_RGB_COLORS = [
+ hex_to_rgb(WCAG_ACCESSIBLE_COLORS['neutral_medium']), # Flat: gris moyen [102, 102, 102]
+ hex_to_rgb(WCAG_ACCESSIBLE_COLORS['primary_red']), # Human: rouge [204, 0, 0]
+ hex_to_rgb(WCAG_ACCESSIBLE_COLORS['secondary_purple']), # Vehicle: violet [102, 51, 153]
+ hex_to_rgb(WCAG_ACCESSIBLE_COLORS['primary_orange']), # Construction: orange [204, 102, 0]
+ hex_to_rgb(WCAG_ACCESSIBLE_COLORS['secondary_teal']), # Object: teal [0, 102, 102]
+ hex_to_rgb(WCAG_ACCESSIBLE_COLORS['primary_green']), # Nature: vert [0, 102, 0]
+ hex_to_rgb(WCAG_ACCESSIBLE_COLORS['primary_blue']), # Sky: bleu [0, 102, 204]
+ hex_to_rgb(WCAG_ACCESSIBLE_COLORS['neutral_dark']) # Void: gris foncé [51, 51, 51]
+]
+
+
+# Motifs pour différencier sans couleur uniquement
+ACCESSIBILITY_PATTERNS = {
+ 'solid': None,
+ 'diagonal_lines': '///',
+ 'vertical_lines': '|||',
+ 'horizontal_lines': '---',
+ 'dots': '...',
+ 'crosses': 'xxx'
+}
+
+def create_accessible_pie_chart_conditional(sizes, labels, title, use_patterns=False):
+ """Version corrigée qui règle le problème de lisibilité avec les motifs"""
+ fig, ax = plt.subplots(figsize=(6, 6), tight_layout=True)
+
+ accessible_colors = [
+ WCAG_ACCESSIBLE_COLORS['primary_blue'], WCAG_ACCESSIBLE_COLORS['primary_red'],
+ WCAG_ACCESSIBLE_COLORS['primary_green'], WCAG_ACCESSIBLE_COLORS['primary_orange'],
+ WCAG_ACCESSIBLE_COLORS['secondary_purple'], WCAG_ACCESSIBLE_COLORS['secondary_teal']
+ ]
+ colors = accessible_colors[:len(sizes)]
+
+ # SOLUTION : texte avec fond contrasté
+ uniform_textprops = {
+ 'fontsize': 10, 'fontweight': 'bold', 'color': 'white',
+ 'bbox': dict(boxstyle="round,pad=0.3", facecolor='black', alpha=0.8)
+ }
+
+ try:
+ wedges, texts, autotexts = ax.pie(
+ sizes, labels=labels, colors=colors, autopct='%1.0f%%', startangle=90, shadow=True,
+ textprops=uniform_textprops, wedgeprops={'edgecolor': 'white', 'linewidth': 2},
+ pctdistance=0.85 # Éloigner les pourcentages
+ )
+
+ # Améliorer la lisibilité des pourcentages
+ for autotext in autotexts:
+ autotext.set_bbox(dict(boxstyle="round,pad=0.4", facecolor='black', alpha=0.9))
+ autotext.set_color('white')
+ autotext.set_fontweight('bold')
+ autotext.set_fontsize(9)
+
+ # Motifs APRÈS configuration du texte
+ if use_patterns:
+ patterns = ['///', '|||', '---', '...', 'xxx', '+++']
+ for wedge, pattern in zip(wedges, patterns[:len(wedges)]):
+ if pattern:
+ wedge.set_hatch(pattern)
+
+ ax.set_title(title, fontsize=10, fontweight='bold', pad=15, color='#333333')
+ ax.axis('equal')
+ chart_displayed = True
+
+ except Exception as e:
+ ax.text(0.5, 0.5, f"Erreur d'affichage du graphique\n{str(e)}",
+ ha='center', va='center', transform=ax.transAxes, fontsize=10, color='red', weight='bold')
+ chart_displayed = False
+
+ return fig, ax, {'colors': colors, 'patterns': patterns if use_patterns else None}, chart_displayed
+
+# Fonctions Plotly pour graphiques interactifs
+def create_interactive_pie_chart(sizes, labels, title, models_info=None):
+ """Crée un pie chart interactif avec les couleurs des classes définies dans models_info"""
+
+ # Utiliser les couleurs des classes si disponibles
+ if models_info and 'class_colors' in models_info and 'class_names' in models_info:
+ colors = []
+ class_names = models_info['class_names']
+ class_colors = models_info['class_colors']
+
+ for label in labels:
+ if label in class_names:
+ idx = class_names.index(label)
+ color_rgb = class_colors[idx]
+ color_hex = f"#{color_rgb[0]:02x}{color_rgb[1]:02x}{color_rgb[2]:02x}"
+ colors.append(color_hex)
+ else:
+ # Couleur par défaut si classe non trouvée
+ colors.append('#666666')
+ else:
+ # Fallback vers les couleurs WCAG
+ accessible_colors_list = list(WCAG_ACCESSIBLE_COLORS.values())
+ colors = accessible_colors_list[:len(sizes)]
+
+ df = pd.DataFrame({'labels': labels, 'values': sizes, 'colors': colors})
+
+ fig = go.Figure(data=[go.Pie(
+ labels=df['labels'],
+ values=df['values'],
+ marker=dict(
+ colors=df['colors'],
+ line=dict(color='#FFFFFF', width=3),
+ pattern=dict(
+ shape=['', '/', '\\', '|', '-', '+', 'x', '.'][:len(sizes)],
+ size=8,
+ solidity=0.3
+ )
+ ),
+ textinfo='label+percent',
+ textposition='auto',
+ textfont=dict(
+ size=13,
+ color='white',
+ family="Arial Black"
+ ),
+ hovertemplate='%{label}
Valeur: %{value}
Pourcentage: %{percent}
',
+ hoverlabel=dict(
+ bgcolor="rgba(255,255,255,0.95)",
+ bordercolor="#333333",
+ font=dict(size=14, color="#333333")
+ )
+ )])
+
+ fig.update_layout(
+ title={
+ 'text': title,
+ 'x': 0.5,
+ 'xanchor': 'center',
+ 'font': {'size': 16, 'color': '#333333', 'family': 'Arial Black'}
+ },
+ showlegend=True,
+ legend=dict(
+ orientation="v",
+ yanchor="middle",
+ y=0.5,
+ xanchor="left",
+ x=1.02,
+ font=dict(size=12, color='#333333'),
+ bgcolor='rgba(255,255,255,0.9)',
+ bordercolor='#333333',
+ ),
+ margin=dict(l=20, r=120, t=60, b=20),
+ height=400,
+ plot_bgcolor='rgba(0,0,0,0)',
+ paper_bgcolor='rgba(0,0,0,0)',
+ font=dict(color='#333333')
+ )
+
+ return fig
+
+
+def create_interactive_comparison_chart(model1_stats, model2_stats, model1_name, model2_name):
+ """Crée un graphique en barres interactif WCAG-compliant pour comparer les deux modèles"""
+ classes = list(set(model1_stats.keys()) | set(model2_stats.keys()))
+
+ data = []
+ for class_name in classes:
+ val1 = model1_stats.get(class_name, {}).get('percentage', 0)
+ val2 = model2_stats.get(class_name, {}).get('percentage', 0)
+
+ data.append({'Classe': class_name, 'Modèle': model1_name, 'Pourcentage': val1, 'Pixels': model1_stats.get(class_name, {}).get('pixels', 0)})
+ data.append({'Classe': class_name, 'Modèle': model2_name, 'Pourcentage': val2, 'Pixels': model2_stats.get(class_name, {}).get('pixels', 0)})
+
+ df = pd.DataFrame(data)
+
+ # Filtrer les données pour supprimer les lignes vides potentielles
+ df = df[df['Modèle'].notna() & (df['Modèle'] != '')]
+
+ fig = px.bar(
+ df, x='Classe', y='Pourcentage', color='Modèle', barmode='group',
+ title='Comparaison interactive des classes détectées',
+ hover_data=['Pixels'],
+ color_discrete_map={model1_name: '#0066CC', model2_name: '#CC0000'}
+ # PAS de pattern_shape du tout
+ )
+
+ fig.update_layout(
+ title={'x': 0.5, 'xanchor': 'center', 'font': {'size': 16, 'color': '#333333', 'family': 'Arial Black'}},
+ xaxis=dict(
+ title='Classes de segmentation',
+ titlefont=dict(size=14, color='#666666'), # Plus clair comme vos pie charts
+ tickfont=dict(size=12, color='#666666'), # Plus clair
+ tickangle=45,
+ showgrid=True,
+ gridcolor='#E8E8E8', # Grille beaucoup plus claire
+ gridwidth=1,
+ linecolor='#AAAAAA', # Axe gris moyen
+ linewidth=1,
+ showline=True
+ ),
+ yaxis=dict(
+ title='Pourcentage de l\'image (%)',
+ titlefont=dict(size=14, color='#666666'), # Plus clair
+ tickfont=dict(size=12, color='#666666'), # Plus clair
+ showgrid=True,
+ gridcolor='#E8E8E8', # Grille beaucoup plus claire
+ gridwidth=1,
+ linecolor='#AAAAAA', # Axe gris moyen
+ linewidth=1,
+ showline=True
+ ),
+ legend=dict(
+ title='Modèles',
+ font=dict(size=12, color='#333333'), # Comme vos pie charts
+ bgcolor='rgba(255,255,255,0.9)',
+ bordercolor='#333333',
+ ),
+ hovermode='x unified',
+ height=500,
+ plot_bgcolor='rgba(0,0,0,0)', # Transparent
+ paper_bgcolor='rgba(0,0,0,0)', # Transparent
+ font=dict(color='#333333') # Sans spécifier size qui peut poser problème
+ )
+
+ fig.update_traces(
+ texttemplate='%{y:.1f}%',
+ textposition='outside',
+ textfont=dict(size=11, color='#666666'), # Plus clair pour lisibilité
+ marker=dict(line=dict(color='#FFFFFF', width=1)) # Bordures blanches comme vos pie charts
+ )
+
+ return fig
+
+
+def create_dataset_interactive_overview():
+ """Vue d'ensemble interactive du dataset avec les MÊMES couleurs que 'Visualisations accessibles'"""
+
+ # RÉORGANISER les données pour que l'ordre soit logique et que les couleurs correspondent
+ dataset_data = {
+ 'Split': ['Train (notre projet)', 'Validation (notre projet)', 'Test final', 'Non utilisé'],
+ 'Images': [2380, 595, 500, 21525],
+ 'Utilisation': ['Entraînement', 'Validation', 'Test', 'Non utilisé']
+ }
+
+ df = pd.DataFrame(dataset_data)
+
+ # Couleurs dans l'ordre exact des données
+ colors = [
+ WCAG_ACCESSIBLE_COLORS['primary_blue'], # Train (notre projet) - bleu
+ WCAG_ACCESSIBLE_COLORS['primary_red'], # Validation (notre projet) - rouge
+ WCAG_ACCESSIBLE_COLORS['primary_orange'], # Test final - orange
+ WCAG_ACCESSIBLE_COLORS['neutral_dark'] # Non utilisé - gris foncé
+ ]
+
+ # FORCER l'ordre avec category_orders
+ fig = px.pie(
+ df,
+ values='Images',
+ names='Split',
+ title='Répartition interactive du dataset Cityscapes',
+ color_discrete_sequence=colors,
+ hover_data=['Utilisation'],
+ category_orders={'Split': ['Train (notre projet)', 'Validation (notre projet)', 'Test final', 'Non utilisé']}
+ )
+
+ # FORCER la correspondance couleur-catégorie avec color_discrete_map
+ color_map = {
+ 'Train (notre projet)': WCAG_ACCESSIBLE_COLORS['primary_blue'],
+ 'Validation (notre projet)': WCAG_ACCESSIBLE_COLORS['primary_red'],
+ 'Test final': WCAG_ACCESSIBLE_COLORS['primary_orange'],
+ 'Non utilisé': WCAG_ACCESSIBLE_COLORS['neutral_dark']
+ }
+
+ fig.update_traces(
+ marker=dict(
+ colors=[color_map[split] for split in df['Split']], # Forcer les couleurs exactes
+ line=dict(color='#FFFFFF', width=3),
+ pattern=dict(
+ shape=['|', '/', '\\', ''],
+ size=8,
+ solidity=0.3
+ )
+ ),
+ textposition='auto',
+ textinfo='percent+label',
+ textfont=dict(size=12, color='white', family="Arial Black"),
+ hovertemplate='%{label}
Images: %{value}
Usage: %{customdata[0]}'
+ )
+
+ fig.update_layout(
+ title={'x': 0.5, 'xanchor': 'center', 'font': {'size': 16, 'color': '#333333'}},
+ height=500,
+ legend=dict(
+ font=dict(size=12, color='#333333'),
+ bgcolor='rgba(255,255,255,0.9)',
+ bordercolor='#333333',
+ ),
+ font=dict(color='#333333')
+ )
+
+ return fig
+
+
+def create_accessible_bar_chart_conditional(data, labels, title, colors=None, use_patterns=False):
+ """
+ Crée un bar chart accessible - descriptions uniquement si erreur d'affichage
+ """
+ import matplotlib.pyplot as plt
+
+ fig, ax = plt.subplots(figsize=(10, 6))
+
+ if colors is None:
+ colors = [WCAG_ACCESSIBLE_COLORS['primary_blue']] * len(data)
+
+ # Motifs pour différenciation
+ patterns = None
+ if use_patterns:
+ pattern_list = list(ACCESSIBILITY_PATTERNS.values())
+ patterns = pattern_list[:len(data)]
+
+ try:
+ bars = ax.bar(
+ labels,
+ data,
+ color=colors,
+ alpha=0.8,
+ edgecolor='#333333',
+ linewidth=1
+ )
+
+ # Ajouter des motifs
+ if patterns:
+ for bar, pattern in zip(bars, patterns):
+ if pattern:
+ bar.set_hatch(pattern)
+
+ # Style accessible
+ ax.set_ylabel('Valeur', fontweight='bold', color='#333333')
+ ax.set_xlabel('Catégories', fontweight='bold', color='#333333')
+ ax.set_title(title, fontsize=14, fontweight='bold', pad=20, color='#333333')
+
+ # Grille pour faciliter la lecture
+ ax.grid(axis='y', alpha=0.3, linestyle='--', color='#666666')
+ ax.set_axisbelow(True)
+
+ # Valeurs sur les barres pour accessibilité
+ for bar, value in zip(bars, data):
+ height = bar.get_height()
+ ax.text(bar.get_x() + bar.get_width()/2., height + max(data) * 0.01,
+ f'{value:.1f}' if isinstance(value, float) else str(value),
+ ha='center', va='bottom', fontsize=10, fontweight='bold', color='#333333')
+
+ # Style des axes
+ ax.spines['top'].set_visible(False)
+ ax.spines['right'].set_visible(False)
+ ax.spines['left'].set_color('#666666')
+ ax.spines['bottom'].set_color('#666666')
+
+ plt.xticks(rotation=45, ha='right', color='#333333')
+ plt.yticks(color='#333333')
+
+ chart_displayed = True
+
+ except Exception as e:
+ # En cas d'erreur, message d'erreur
+ ax.text(0.5, 0.5, f"Erreur d'affichage du graphique\n{str(e)}",
+ ha='center', va='center', transform=ax.transAxes,
+ fontsize=12, color='red', weight='bold')
+ chart_displayed = False
+
+ return fig, ax, chart_displayed
+
+# Fonction pour générer du texte alternatif (WCAG 1.1.1)
+def generate_alt_text_for_pie(sizes, labels, title):
+ """
+ Génère un texte alternatif détaillé pour un pie chart
+ """
+ total = sum(sizes)
+ descriptions = []
+
+ for size, label in zip(sizes, labels):
+ percentage = (size / total) * 100
+ descriptions.append(f"{label}: {size} éléments ({percentage:.1f}%)")
+
+ alt_text = f"Graphique en secteurs: {title}. "
+ alt_text += f"Total de {total} éléments répartis comme suit: "
+ alt_text += "; ".join(descriptions) + "."
+
+ return alt_text
+
+# Fonction pour générer du texte alternatif (WCAG 1.1.1)
+def generate_alt_text_for_images(image_type, model_name=None, stats=None):
+ """Génère un texte alternatif détaillé pour les images de segmentation"""
+ if image_type == "original":
+ return "Image originale en couleur montrant une scène urbaine typique du dataset Cityscapes"
+ elif image_type == "segmentation":
+ base_text = f"Masque de segmentation sémantique prédit par le modèle {model_name or 'IA'}"
+ if stats:
+ # Ajouter les statistiques principales
+ main_classes = sorted(stats.items(), key=lambda x: x[1]['percentage'], reverse=True)[:3]
+ stats_text = ". Classes principales détectées: " + ", ".join([
+ f"{name} ({data['percentage']:.1f}%)" for name, data in main_classes
+ ])
+ return base_text + stats_text
+ return base_text
+ elif image_type == "comparison":
+ return "Visualisation comparative des prédictions de deux modèles de segmentation sémantique"
+ else:
+ return "Image de segmentation sémantique"
+
+def generate_alt_text_for_bar(data, labels, title, unit=""):
+ """
+ Génère un texte alternatif détaillé pour un bar chart
+ """
+ descriptions = []
+
+ for value, label in zip(data, labels):
+ descriptions.append(f"{label}: {value}{unit}")
+
+ alt_text = f"Graphique en barres: {title}. "
+ alt_text += "Valeurs: " + "; ".join(descriptions) + "."
+
+ return alt_text
+
+def generate_comprehensive_alt_text(image_type, model_name=None, stats=None, comparison_data=None):
+ """Génère un texte alternatif complet selon WCAG 1.1.1"""
+ if image_type == "preprocessing_original":
+ return "Image urbaine originale redimensionnée à la résolution d'entraînement du modèle, montrant une scène typique du dataset Cityscapes avec véhicules, piétons et infrastructure urbaine"
+
+ elif image_type == "preprocessing_normalized":
+ return "Même image après normalisation des pixels pour l'entraînement du modèle IA, les couleurs peuvent paraître légèrement différentes"
+
+ elif image_type == "preprocessing_augmented":
+ return "Image avec augmentations de données appliquées (rotation, changement de luminosité, etc.) pour améliorer la robustesse du modèle"
+
+ elif image_type == "segmentation_prediction":
+ base_text = f"Masque de segmentation sémantique coloré prédit par le modèle {model_name or 'IA'}"
+ if stats:
+ main_classes = sorted(stats.items(), key=lambda x: x[1]['percentage'], reverse=True)[:3]
+ stats_text = ". Classes principales détectées : " + ", ".join([
+ f"{name} ({data['percentage']:.1f}% de l'image)" for name, data in main_classes
+ ])
+ return base_text + stats_text
+ return base_text
+
+ elif image_type == "comparison_grid":
+ return "Grille comparative montrant l'image originale et les prédictions de segmentation de deux modèles d'IA différents côte à côte pour évaluation"
+
+ else:
+ return "Image de segmentation sémantique urbaine générée par intelligence artificielle"
+
+def create_data_table(data, columns, title, description=""):
+ """
+ Crée un tableau de données accessible complémentaire aux graphiques
+ """
+ import pandas as pd
+
+ df = pd.DataFrame(data, columns=columns)
+
+ st.markdown(f"**Tableau de données: {title}**")
+ if description:
+ st.markdown(f"*{description}*")
+
+ # Configuration accessible du dataframe
+ st.dataframe(
+ df,
+ use_container_width=True,
+ hide_index=True,
+ column_config={
+ col: st.column_config.TextColumn(col, width="medium")
+ for col in columns
+ }
+ )
+
+ return df
+
+# CSS amélioré pour l'accessibilité complète
+ENHANCED_WCAG_CSS = """
+
+"""
+
+st.markdown(ENHANCED_WCAG_CSS, unsafe_allow_html=True)
+
+# En-tête principal
+st.markdown("""
+
+
🚗 Street Vision - Comparaison de Modèles
+
Évaluation comparative SegFormer vs FPN sur le dataset Cityscapes
+
+""", unsafe_allow_html=True)
+
+# Fonctions utilitaires
+def get_models_info():
+ """Récupère les informations des deux modèles"""
+ try:
+ response = requests.get(f"{API_BASE_URL}/models")
+ if response.status_code == 200:
+ return response.json()
+ else:
+ st.error(f"Erreur lors de la récupération des infos modèles: {response.status_code}")
+ return None
+ except Exception as e:
+ st.error(f"Erreur de connexion à l'API: {e}")
+ return None
+
+def get_dataset_info():
+ """Récupère les informations sur le dataset"""
+ try:
+ response = requests.get(f"{API_BASE_URL}/dataset-info")
+ if response.status_code == 200:
+ return response.json()
+ else:
+ st.error(f"Erreur lors de la récupération des infos dataset: {response.status_code}")
+ return None
+ except Exception as e:
+ st.error(f"Erreur de connexion à l'API: {e}")
+ return None
+
+def get_train_images():
+ """Récupère la liste des images d'entraînement"""
+ try:
+ response = requests.get(f"{API_BASE_URL}/train-images")
+ if response.status_code == 200:
+ return response.json()
+ else:
+ st.error(f"Erreur lors de la récupération des images: {response.status_code}")
+ return None
+ except Exception as e:
+ st.error(f"Erreur de connexion à l'API: {e}")
+ return None
+
+def get_class_repartition():
+ """Récupère le graphique de répartition des classes"""
+ try:
+ response = requests.get(f"{API_BASE_URL}/class-repartition", timeout=10)
+ if response.status_code == 200:
+ return response.content
+ else:
+ return None
+ except Exception as e:
+ st.warning(f"Graphique de répartition non disponible: {e}")
+ return None
+
+def preprocess_train_image(filename, apply_augmentation=False):
+ """Lance le preprocessing sur une image d'entraînement"""
+ try:
+ data = {
+ "filename": filename,
+ "apply_augmentation": apply_augmentation
+ }
+ response = requests.post(
+ f"{API_BASE_URL}/preprocess-train-image",
+ json=data,
+ headers={"Content-Type": "application/json"}
+ )
+
+ if response.status_code == 200:
+ return response.json()
+ else:
+ st.error(f"Erreur API: {response.status_code} - {response.text}")
+ return None
+ except Exception as e:
+ st.error(f"Erreur lors du preprocessing: {e}")
+ return None
+
+def get_sample_images():
+ """Récupère la liste des images d'exemple"""
+ try:
+ response = requests.get(f"{API_BASE_URL}/sample-images")
+ if response.status_code == 200:
+ return response.json()
+ else:
+ st.error(f"Erreur lors de la récupération des images: {response.status_code}")
+ return None
+ except Exception as e:
+ st.error(f"Erreur de connexion à l'API: {e}")
+ return None
+
+def compare_sample_image(filename):
+ """Lance une comparaison sur une image d'exemple"""
+ try:
+ data = {"filename": filename}
+ response = requests.post(
+ f"{API_BASE_URL}/compare-sample",
+ json=data,
+ headers={"Content-Type": "application/json"}
+ )
+
+ if response.status_code == 200:
+ return response.json()
+ else:
+ st.error(f"Erreur API: {response.status_code} - {response.text}")
+ return None
+ except Exception as e:
+ st.error(f"Erreur lors de la comparaison: {e}")
+ return None
+
+def compare_uploaded_image(uploaded_file):
+ """Lance une comparaison sur une image uploadée"""
+ try:
+ files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)}
+ response = requests.post(f"{API_BASE_URL}/compare-upload", files=files)
+
+ if response.status_code == 200:
+ return response.json()
+ else:
+ st.error(f"Erreur API: {response.status_code} - {response.text}")
+ return None
+ except Exception as e:
+ st.error(f"Erreur lors de la comparaison: {e}")
+ return None
+
+def display_comparison_results(result, models_info, context=""):
+ """Affiche les résultats de comparaison avec graphiques interactifs et accessibilité complète"""
+ if not result or not result['success']:
+ st.error("Aucun résultat de comparaison disponible")
+ return
+
+ # Informations des modèles
+ st.markdown("### 🏆 Modèles Comparés")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ model1 = result['model1_info']
+ # Formater les métriques séparément
+ miou1 = f"{model1.get('test_mean_iou'):.4f}" if model1.get('test_mean_iou') is not None else 'N/A'
+ acc1 = f"{model1.get('test_accuracy'):.4f}" if model1.get('test_accuracy') is not None else 'N/A'
+
+ st.markdown(f"""
+
+
🥇 Modèle #1
+ {model1.get('architecture', 'N/A')} - {model1['encoder_name']}
+ 📐 Taille: {model1['input_size']}
+ 📊 mIoU: {miou1}
+ 🎯 Accuracy: {acc1}
+
+ """, unsafe_allow_html=True)
+
+ with col2:
+ model2 = result['model2_info']
+ # Formater les métriques séparément
+ miou2 = f"{model2.get('test_mean_iou'):.4f}" if model2.get('test_mean_iou') is not None else 'N/A'
+ acc2 = f"{model2.get('test_accuracy'):.4f}" if model2.get('test_accuracy') is not None else 'N/A'
+
+ st.markdown(f"""
+
+
🥈 Modèle #2
+ {model2.get('architecture', 'N/A')} - {model2['encoder_name']}
+ 📐 Taille: {model2['input_size']}
+ 📊 mIoU: {miou2}
+ 🎯 Accuracy: {acc2}
+
+ """, unsafe_allow_html=True)
+
+ # Comparaison des temps d'inférence
+ st.markdown("### ⏱️ Comparaison des Performances")
+
+ speed_comp = result['speed_comparison']
+ model1_time = result['model1_inference_time'] * 1000
+ model2_time = result['model2_inference_time'] * 1000
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ winner_class = "speed-winner" if speed_comp['faster_model'] == model1['encoder_name'] else "speed-slower"
+ st.markdown(f"""
+
+
🥇 {model1.get('architecture', 'N/A')} - {model1['encoder_name']}
+ {model1_time:.1f} ms
+
+ """, unsafe_allow_html=True)
+
+ with col2:
+ st.markdown(f"""
+
+
⚡ Comparaison
+ {speed_comp['faster_model']}
+ est {speed_comp['speedup_ratio']}x plus rapide
+ Différence: {speed_comp['time_difference_ms']:.1f} ms
+
+ """, unsafe_allow_html=True)
+
+ with col3:
+ winner_class = "speed-winner" if speed_comp['faster_model'] == model2['encoder_name'] else "speed-slower"
+ st.markdown(f"""
+
+
🥈 {model2.get('architecture', 'N/A')} - {model2['encoder_name']}
+ {model2_time:.1f} ms
+
+ """, unsafe_allow_html=True)
+
+ # Affichage des segmentations avec descriptions alternatives CONDITIONNELLES
+ if result.get('figure_data'):
+ st.markdown("### 🎨 Résultats de Segmentation")
+
+ try:
+ fig_bytes = base64.b64decode(result['figure_data'])
+ fig = pickle.loads(fig_bytes)
+
+ st.pyplot(fig)
+ plt.close(fig)
+
+ except Exception as e:
+ st.error(f"Erreur lors de l'affichage: {e}")
+ # Description alternative SEULEMENT en cas d'erreur
+ alt_text = generate_comprehensive_alt_text("comparison_grid")
+ st.warning(f"**Description alternative:** {alt_text}")
+
+ # Section interactive (CE2 - Graphiques interactifs) - SEULEMENT 2 ONGLETS
+ st.markdown("### 📊 Analyse Interactive des Résultats")
+
+ if result.get('model1_stats') and result.get('model2_stats'):
+ # SUPPRIMER l'onglet "Performances"
+ tab1, tab2 = st.tabs(["🥧 Répartitions", "📊 Comparaison"])
+
+ with tab1:
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown("#### Répartition Modèle 1")
+ sizes1 = [data['percentage'] for data in result['model1_stats'].values()]
+ labels1 = list(result['model1_stats'].keys())
+
+ fig_pie1 = create_interactive_pie_chart(sizes1, labels1,
+ f"Distribution - {result['model1_info'].get('architecture', 'N/A')} {result['model1_info']['encoder_name']}",
+ models_info)
+ # AJOUTER key unique pour éviter l'erreur Streamlit
+ st.plotly_chart(fig_pie1, use_container_width=True, key=f"pie_model1_{context}")
+
+ with col2:
+ st.markdown("#### Répartition Modèle 2")
+ sizes2 = [data['percentage'] for data in result['model2_stats'].values()]
+ labels2 = list(result['model2_stats'].keys())
+
+ fig_pie2 = create_interactive_pie_chart(sizes2, labels2,
+ f"Distribution - {result['model2_info'].get('architecture', 'N/A')} {result['model2_info']['encoder_name']}",
+ models_info)
+ # AJOUTER key unique pour éviter l'erreur Streamlit
+ st.plotly_chart(fig_pie2, use_container_width=True, key=f"pie_model2_{context}")
+
+ with tab2:
+ st.markdown("#### Comparaison Interactive des Classes")
+
+ fig_comparison = create_interactive_comparison_chart(
+ result['model1_stats'], result['model2_stats'],
+ f"{result['model1_info'].get('architecture', 'N/A')} {result['model1_info']['encoder_name']}",
+ f"{result['model2_info'].get('architecture', 'N/A')} {result['model2_info']['encoder_name']}"
+ )
+ # AJOUTER key unique pour éviter l'erreur Streamlit
+ st.plotly_chart(fig_comparison, use_container_width=True, key=f"comparison_chart_{context}")
+
+ st.info("💡 **Interactivité :** Survolez les barres, cliquez sur la légende, zoomez en sélectionnant une zone.")
+ # SUPPRIMER la description systématique
+
+ # Garder seulement le tableau détaillé (sans graphique redondant)
+ if result.get('model1_stats') and result.get('model2_stats'):
+ display_class_comparison(result['model1_stats'], result['model2_stats'],
+ result['model1_info'], result['model2_info'], models_info)
+
+# def display_model_stats_chart(stats, models_info, title):
+# """Affiche les statistiques d'un modèle sous forme de graphique accessible"""
+# if not stats:
+# st.warning("Aucune statistique disponible")
+# return
+
+# # Utiliser l'ordre des classes du modèle pour la cohérence
+# if models_info and 'class_names' in models_info:
+# ordered_class_names = [name for name in models_info['class_names'] if name in stats]
+# else:
+# ordered_class_names = sorted(stats.keys())
+
+# percentages = [stats[name]['percentage'] for name in ordered_class_names]
+
+# # Couleurs accessibles WCAG
+# accessible_colors = [
+# WCAG_ACCESSIBLE_COLORS['neutral_medium'], # Flat: gris moyen
+# WCAG_ACCESSIBLE_COLORS['primary_red'], # Human: rouge
+# WCAG_ACCESSIBLE_COLORS['secondary_purple'], # Vehicle: violet
+# WCAG_ACCESSIBLE_COLORS['primary_orange'], # Construction: orange
+# WCAG_ACCESSIBLE_COLORS['secondary_teal'], # Object: teal
+# WCAG_ACCESSIBLE_COLORS['primary_green'], # Nature: vert
+# WCAG_ACCESSIBLE_COLORS['primary_blue'], # Sky: bleu
+# WCAG_ACCESSIBLE_COLORS['neutral_dark'] # Void: gris foncé
+# ]
+
+# colors = accessible_colors[:len(ordered_class_names)]
+
+# # Créer le graphique accessible
+# fig, ax, displayed = create_accessible_bar_chart_conditional(
+# percentages, ordered_class_names, title,
+# colors=colors, use_patterns=True
+# )
+
+# # Description alternative SEULEMENT si erreur d'affichage
+# if not displayed:
+# alt_text = generate_alt_text_for_bar(
+# percentages, ordered_class_names, title, unit="%"
+# )
+# st.warning(f"**Description du graphique:** {alt_text}")
+
+# st.pyplot(fig)
+# plt.close(fig)
+
+# # Tableau de données complémentaire (toujours affiché)
+# table_data = []
+# for name, percentage in zip(ordered_class_names, percentages):
+# pixels = stats[name]['pixels']
+# table_data.append([name, f"{percentage:.1f}%", f"{pixels:,} pixels"])
+
+# create_data_table(
+# table_data,
+# ["Classe", "Pourcentage", "Nombre de pixels"],
+# f"Données - {title}",
+# "Répartition détaillée des classes de segmentation"
+# )
+
+def display_class_comparison(stats1, stats2, model1, model2, models_info):
+ """Affiche uniquement le tableau détaillé de comparaison des classes (sans graphique redondant)"""
+ st.markdown("### 🔎 Tableau Détaillé des Comparaisons")
+
+ # Préparer les données
+ if models_info and 'class_names' in models_info:
+ all_classes_in_stats = set(stats1.keys()) | set(stats2.keys())
+ ordered_all_classes = [name for name in models_info['class_names'] if name in all_classes_in_stats]
+ else:
+ all_classes = set(stats1.keys()) | set(stats2.keys())
+ ordered_all_classes = sorted(all_classes)
+
+ class_names = ordered_all_classes
+ model1_values = [stats1.get(name, {}).get('percentage', 0) for name in class_names]
+ model2_values = [stats2.get(name, {}).get('percentage', 0) for name in class_names]
+
+ # SUPPRIMER tout le code de génération du graphique matplotlib
+ # GARDER seulement le tableau
+
+ table_data = []
+ for i, name in enumerate(class_names):
+ val1 = model1_values[i]
+ val2 = model2_values[i]
+ diff = val1 - val2
+
+ if abs(diff) < 1:
+ category = "🟢 Similaire"
+ elif abs(diff) < 5:
+ category = "🟡 Différence modérée"
+ else:
+ category = "🔴 Différence importante"
+
+ table_data.append([
+ name,
+ f"{val1:.1f}%",
+ f"{val2:.1f}%",
+ f"{diff:+.1f}%",
+ category
+ ])
+
+ create_data_table(
+ table_data,
+ [
+ "Classe",
+ f"🥇 {model1.get('architecture', 'N/A')[:15]} (%)",
+ f"🥈 {model2.get('architecture', 'N/A')[:15]} (%)",
+ "Différence",
+ "Évaluation"
+ ],
+ "Comparaison détaillée par classe",
+ "Analyse des écarts de performance entre les deux modèles pour chaque classe de segmentation"
+ )
+
+# def display_class_comparison(stats1, stats2, model1, model2, models_info):
+# """Affiche une comparaison accessible des classes entre les deux modèles"""
+# st.markdown("### 🔍 Analyse Comparative par Classe")
+
+# # Préparer les données
+# if models_info and 'class_names' in models_info:
+# all_classes_in_stats = set(stats1.keys()) | set(stats2.keys())
+# ordered_all_classes = [name for name in models_info['class_names'] if name in all_classes_in_stats]
+# else:
+# all_classes = set(stats1.keys()) | set(stats2.keys())
+# ordered_all_classes = sorted(all_classes)
+
+# class_names = ordered_all_classes
+# model1_values = [stats1.get(name, {}).get('percentage', 0) for name in class_names]
+# model2_values = [stats2.get(name, {}).get('percentage', 0) for name in class_names]
+
+# # Couleurs accessibles avec motifs différents
+# fig, ax = plt.subplots(figsize=(12, 7))
+
+# x = np.arange(len(class_names))
+# width = 0.35
+
+# # Couleurs accessibles
+# color1 = WCAG_ACCESSIBLE_COLORS['primary_blue']
+# color2 = WCAG_ACCESSIBLE_COLORS['primary_red']
+
+# bars1 = ax.bar(x - width/2, model1_values, width,
+# label=f"{model1.get('architecture', 'N/A')} - {model1['encoder_name']}",
+# color=color1, alpha=0.8, edgecolor='#333333')
+
+# bars2 = ax.bar(x + width/2, model2_values, width,
+# label=f"{model2.get('architecture', 'N/A')} - {model2['encoder_name']}",
+# color=color2, alpha=0.8, edgecolor='#333333', hatch='///')
+
+# # Style accessible
+# ax.set_xlabel('Classes', fontsize=11, fontweight='bold', color='#333333')
+# ax.set_ylabel('Pourcentage (%)', fontsize=11, fontweight='bold', color='#333333')
+# ax.set_title('Comparaison des classes entre les deux modèles',
+# fontsize=13, fontweight='bold', pad=15, color='#333333')
+# ax.set_xticks(x)
+
+# if len(class_names) <= 4:
+# ax.set_xticklabels(class_names, fontsize=10, color='#333333')
+# elif len(class_names) <= 6:
+# ax.set_xticklabels(class_names, rotation=30, ha='right', fontsize=9, color='#333333')
+# else:
+# ax.set_xticklabels(class_names, rotation=45, ha='right', fontsize=9, color='#333333')
+
+# ax.legend(loc='upper right', fontsize=10)
+# ax.grid(axis='y', alpha=0.3, linestyle='--', color='#666666')
+# ax.set_axisbelow(True)
+
+# # Style des axes
+# ax.spines['top'].set_visible(False)
+# ax.spines['right'].set_visible(False)
+# ax.spines['left'].set_color('#666666')
+# ax.spines['bottom'].set_color('#666666')
+
+# # Valeurs sur les barres
+# for bar1, bar2, val1, val2 in zip(bars1, bars2, model1_values, model2_values):
+# height1, height2 = bar1.get_height(), bar2.get_height()
+# ax.text(bar1.get_x() + bar1.get_width()/2., height1 + 0.5,
+# f'{val1:.1f}%', ha='center', va='bottom', fontsize=8,
+# fontweight='bold', color='#333333')
+# ax.text(bar2.get_x() + bar2.get_width()/2., height2 + 0.5,
+# f'{val2:.1f}%', ha='center', va='bottom', fontsize=8,
+# fontweight='bold', color='#333333')
+
+# plt.tight_layout()
+
+# # Description alternative
+# alt_text = f"Graphique comparatif en barres des classes entre {model1['encoder_name']} et {model2['encoder_name']}. "
+# comparisons = []
+# for name, val1, val2 in zip(class_names, model1_values, model2_values):
+# comparisons.append(f"{name}: {val1:.1f}% vs {val2:.1f}%")
+# alt_text += "Comparaisons: " + "; ".join(comparisons)
+
+# st.markdown(f"**Description du graphique:** {alt_text}")
+# st.pyplot(fig)
+# plt.close(fig)
+
+# # Tableau détaillé accessible
+# st.markdown("**🔎 Tableau détaillé des comparaisons:**")
+
+# table_data = []
+# for i, name in enumerate(class_names):
+# val1 = model1_values[i]
+# val2 = model2_values[i]
+# diff = val1 - val2
+
+# if abs(diff) < 1:
+# category = "🟢 Similaire"
+# elif abs(diff) < 5:
+# category = "🟡 Différence modérée"
+# else:
+# category = "🔴 Différence importante"
+
+# table_data.append([
+# name,
+# f"{val1:.1f}%",
+# f"{val2:.1f}%",
+# f"{diff:+.1f}%",
+# category
+# ])
+
+# create_data_table(
+# table_data,
+# [
+# "Classe",
+# f"🥇 {model1.get('architecture', 'N/A')[:15]} (%)",
+# f"🥈 {model2.get('architecture', 'N/A')[:15]} (%)",
+# "Différence",
+# "Évaluation"
+# ],
+# "Comparaison détaillée par classe",
+# "Analyse des écarts de performance entre les deux modèles pour chaque classe de segmentation"
+# )
+
+
+# Onglets principaux
+main_tab1, main_tab2 = st.tabs(["📊 Présentation Jeu de données Cityscapes", "🔄 Comparaison de performances"])
+
+# ===== ONGLET 1: PRÉSENTATION DATASET =====
+with main_tab1:
+ st.markdown("## 📊 Analyse Exploratoire - Dataset Cityscapes")
+
+ # Informations générales sur le dataset
+ dataset_info = get_dataset_info()
+
+ if dataset_info:
+ # Section méthodologique MODIFIÉE
+ st.markdown("""
+ ### 🎯 Présentation générale du dataset Cityscapes
+
+ #### Dataset retenu
+
+ **Cityscapes Dataset : Référence en Segmentation Urbaine**
+ """)
+
+ # Spécificités techniques
+ st.markdown("""
+ ### ⚙️ Spécificités techniques
+
+ - **Taille Datase**: 25 000 images haute résolution
+ - **Résolutions d'acquisition** : Données natives 2048×1024 pixels
+ - **Conditions d'acquisition** : Conditions météo bonnes à moyennes, pas de conditions adverses (pluie, neige)
+ - **Diversité géographique** : Villes allemandes et suisses (architectures variées)
+ - **Annotations pixel-perfect** : Masques de segmentation précis au pixel près (Train + Val)
+
+ Ce dataset permet d'évaluer la capacité des modèles à comprendre les scènes urbaines complexes, élément fondamental pour les applications de conduite autonome et de surveillance urbaine intelligente.
+ """)
+
+ # Composition du dataset avec pie charts NOUVEAU
+ st.markdown("### 🗂️ Composition et utilisation du dataset")
+
+ # Version finale qui combine : tailles uniformes + couleurs WCAG + descriptions conditionnelles
+ if dataset_info.get('splits'):
+ splits_info = dataset_info['splits']
+ project_splits = dataset_info.get('project_splits', {})
+
+ st.markdown("#### 📊 Visualisations accessibles")
+
+ # Option pour activer les motifs (accessibilité)
+ use_patterns = st.checkbox(
+ "🎨 Activer les motifs (accessibilité visuelle)",
+ help="Active les motifs en plus des couleurs pour faciliter la distinction des données",
+ value=True
+ )
+
+ # Organiser en 1 ligne de 4 colonnes
+ col1, col2, col3, col4 = st.columns(4)
+
+ # Configuration uniforme pour tous les graphiques (SOLUTION QUI MARCHAIT)
+ uniform_figsize = (6, 6)
+ uniform_textprops = {'fontsize': 8, 'fontweight': 'bold', 'color': '#333333'}
+ uniform_title_props = {'fontsize': 10, 'fontweight': 'bold', 'pad': 15}
+
+ # Couleurs WCAG accessibles
+ accessible_colors = {
+ 'blue': WCAG_ACCESSIBLE_COLORS['primary_blue'],
+ 'red': WCAG_ACCESSIBLE_COLORS['primary_red'],
+ 'orange': WCAG_ACCESSIBLE_COLORS['primary_orange'],
+ 'gray': WCAG_ACCESSIBLE_COLORS['neutral_medium'],
+ 'light_gray': WCAG_ACCESSIBLE_COLORS['neutral_light']
+ }
+
+ # Motifs pour accessibilité
+ patterns = ['///', '|||', '---', '...', 'xxx', None] if use_patterns else [None] * 6
+
+ # Données pour le tableau récapitulatif
+ pie_data_for_table = []
+
+ with col1:
+ # Pie chart Train (avec répartition projet)
+ fig_train, ax_train = plt.subplots(figsize=uniform_figsize, tight_layout=True)
+
+ train_total = splits_info['train']['images']
+ train_our_train = project_splits['train']['images']
+ train_our_val = project_splits['validation']['images']
+
+ sizes = [train_our_train, train_our_val]
+ labels = [f'Notre Entraînement\n{train_our_train} images (80%)',
+ f'Notre Validation\n{train_our_val} images (20%)']
+ colors = [accessible_colors['blue'], accessible_colors['red']]
+ explode = (0.05, 0.05)
+
+ try:
+ wedges, texts, autotexts = ax_train.pie(
+ sizes, labels=labels, colors=colors, autopct='%1.0f%%',
+ startangle=90, explode=explode, shadow=True,
+ textprops=uniform_textprops,
+ wedgeprops={'edgecolor': 'white', 'linewidth': 2}
+ )
+
+ # Ajouter des motifs si demandé
+ if use_patterns:
+ for wedge, pattern in zip(wedges, patterns[:2]):
+ if pattern:
+ wedge.set_hatch(pattern)
+
+ ax_train.set_title(f'Split Train Original\n{train_total} images total',
+ **uniform_title_props, color='#333333')
+ ax_train.axis('equal')
+
+ chart_displayed = True
+
+ except Exception as e:
+ ax_train.text(0.5, 0.5, f"Erreur d'affichage\n{str(e)}",
+ ha='center', va='center', transform=ax_train.transAxes,
+ fontsize=10, color='red', weight='bold')
+ chart_displayed = False
+
+ # Description SEULEMENT si erreur
+ if not chart_displayed:
+ alt_text = generate_alt_text_for_pie(sizes, labels, "Split Train")
+ st.warning(f"**Description du graphique:** {alt_text}")
+
+ st.pyplot(fig_train)
+ plt.close(fig_train)
+
+ pie_data_for_table.extend([
+ ["Train - Entraînement", train_our_train, "80%", "Entraînement des modèles"],
+ ["Train - Validation", train_our_val, "20%", "Validation pendant entraînement"]
+ ])
+
+ with col2:
+ # Pie chart Validation (utilisation complète)
+ fig_val, ax_val = plt.subplots(figsize=uniform_figsize, tight_layout=True)
+
+ val_total = splits_info['validation']['images']
+ val_our_test = project_splits['test']['images']
+
+ sizes = [val_our_test]
+ labels = [f'Notre Test Final\n{val_our_test} images (100%)']
+ colors = [accessible_colors['orange']]
+
+ try:
+ wedges, texts, autotexts = ax_val.pie(
+ sizes, labels=labels, colors=colors, autopct='%1.0f%%',
+ startangle=90, shadow=True, textprops=uniform_textprops,
+ wedgeprops={'edgecolor': 'white', 'linewidth': 2}
+ )
+
+ if use_patterns and patterns[2]:
+ wedges[0].set_hatch(patterns[2])
+
+ ax_val.set_title(f'Split Validation Original\n{val_total} images total',
+ **uniform_title_props, color='#333333')
+ ax_val.axis('equal')
+
+ chart_displayed = True
+
+ except Exception as e:
+ ax_val.text(0.5, 0.5, f"Erreur d'affichage\n{str(e)}",
+ ha='center', va='center', transform=ax_val.transAxes,
+ fontsize=10, color='red', weight='bold')
+ chart_displayed = False
+
+ if not chart_displayed:
+ alt_text = generate_alt_text_for_pie(sizes, labels, "Split Validation")
+ st.warning(f"**Description du graphique:** {alt_text}")
+
+ st.pyplot(fig_val)
+ plt.close(fig_val)
+
+ pie_data_for_table.append(["Validation - Test", val_our_test, "100%", "Test final des modèles"])
+
+ with col3:
+ # Pie chart Test (non utilisé)
+ fig_test, ax_test = plt.subplots(figsize=uniform_figsize, tight_layout=True)
+
+ test_total = splits_info['test']['images']
+
+ sizes = [test_total]
+ labels = [f'Non utilisé dans notre projet\n{test_total} images\n(annotations privées)']
+ colors = [accessible_colors['gray']]
+
+ try:
+ wedges, texts, autotexts = ax_test.pie(
+ sizes, labels=labels, colors=colors, autopct='',
+ startangle=90, shadow=True, textprops=uniform_textprops,
+ wedgeprops={'edgecolor': 'white', 'linewidth': 2}
+ )
+
+ if use_patterns and patterns[3]:
+ wedges[0].set_hatch(patterns[3])
+
+ ax_test.set_title(f'Split Test Original\n{test_total} images total',
+ **uniform_title_props, color='#333333')
+ ax_test.axis('equal')
+
+ chart_displayed = True
+
+ except Exception as e:
+ ax_test.text(0.5, 0.5, f"Erreur d'affichage\n{str(e)}",
+ ha='center', va='center', transform=ax_test.transAxes,
+ fontsize=10, color='red', weight='bold')
+ chart_displayed = False
+
+ if not chart_displayed:
+ alt_text = generate_alt_text_for_pie(sizes, labels, "Split Test")
+ st.warning(f"**Description du graphique:** {alt_text}")
+
+ st.pyplot(fig_test)
+ plt.close(fig_test)
+
+ pie_data_for_table.append(["Test", 0, "0%", "Non utilisé (annotations privées)"])
+
+ with col4:
+ # Pie chart Coarse (non utilisé)
+ fig_coarse, ax_coarse = plt.subplots(figsize=uniform_figsize, tight_layout=True)
+
+ coarse_total = splits_info['coarse']['images']
+
+ sizes = [coarse_total]
+ labels = [f'Non utilisé dans notre projet\n{coarse_total} images\n(annotations grossières)']
+ colors = [accessible_colors['light_gray']]
+
+ try:
+ wedges, texts, autotexts = ax_coarse.pie(
+ sizes, labels=labels, colors=colors, autopct='',
+ startangle=90, shadow=True, textprops=uniform_textprops,
+ wedgeprops={'edgecolor': 'white', 'linewidth': 2}
+ )
+
+ if use_patterns and patterns[4]:
+ wedges[0].set_hatch(patterns[4])
+
+ ax_coarse.set_title(f'Split Coarse Original\n{coarse_total} images total',
+ **uniform_title_props, color='#333333')
+ ax_coarse.axis('equal')
+
+ chart_displayed = True
+
+ except Exception as e:
+ ax_coarse.text(0.5, 0.5, f"Erreur d'affichage\n{str(e)}",
+ ha='center', va='center', transform=ax_coarse.transAxes,
+ fontsize=10, color='red', weight='bold')
+ chart_displayed = False
+
+ if not chart_displayed:
+ alt_text = generate_alt_text_for_pie(sizes, labels, "Split Coarse")
+ st.warning(f"**Description du graphique:** {alt_text}")
+
+ st.pyplot(fig_coarse)
+ plt.close(fig_coarse)
+
+ pie_data_for_table.append(["Coarse", 0, "0%", "Non utilisé (annotations grossières)"])
+
+
+
+
+ st.markdown("### 📊 Exploration Interactive du Dataset")
+
+ fig_dataset = create_dataset_interactive_overview()
+
+ try:
+ st.plotly_chart(fig_dataset, use_container_width=True, key="dataset_overview")
+ except Exception as e:
+ st.warning("**Description :** Graphique circulaire interactif montrant la répartition de l'utilisation du dataset Cityscapes dans notre projet.")
+ st.info("💡 **Interactivité :** Survolez les sections pour voir les détails, cliquez sur la légende pour masquer/afficher des catégories.")
+
+ # Tableau de données accessible (toujours affiché)
+ st.markdown("#### 📋 Tableau récapitulatif des données")
+
+ create_data_table(
+ pie_data_for_table,
+ ["Split", "Images utilisées", "Pourcentage", "Utilisation"],
+ "Répartition du dataset Cityscapes",
+ "Détail de l'utilisation de chaque split du dataset original dans notre projet"
+ )
+
+
+
+ # Architecture des classes
+ st.markdown("### 🏗️ Architecture de classes hiérarchique")
+ st.markdown("""
+ Le dataset organise 34 classes détaillées en **8 méta-classes** pour la segmentation :
+
+ | Méta-classe | Description | Importance |
+ |-------------|-------------|------------|
+ | **Flat** | Route, trottoir, parkings | Navigation de base |
+ | **Human** | Piéton, cycliste | **Sécurité critique** |
+ | **Vehicle** | Voiture, camion, bus, train | Obstacles dynamiques |
+ | **Construction** | Bâtiment, mur, clôture, pont | Structure urbaine |
+ | **Object** | Poteau, feu, panneau | Signalétique |
+ | **Nature** | Végétation, terrain | Environnement |
+ | **Sky** | Ciel | Contexte |
+ | **Void** | Zones non-définies | Exclusion |
+ """)
+
+ # Graphique de répartition des classes
+ st.markdown("### 📊 Répartition des classes dans le dataset d'entraînement")
+
+ class_repartition_data = get_class_repartition()
+ if class_repartition_data:
+ try:
+ class_repartition_image = Image.open(io.BytesIO(class_repartition_data))
+ st.image(
+ class_repartition_image,
+ caption="Distribution des classes dans les images d'entraînement Cityscapes",
+ use_container_width=True
+ )
+ except Exception as e:
+ st.warning("Impossible d'afficher le graphique de répartition des classes")
+ else:
+ st.info("Graphique de répartition des classes non disponible")
+
+ # Section preprocessing des images
+ st.markdown("---")
+ st.markdown("### 🔧 Exemple de Preprocessing d'Images")
+
+ # Récupérer les images d'entraînement disponibles
+ train_data = get_train_images()
+ if train_data and train_data['images']:
+ # Sélecteur d'image
+ image_options = {img['display_name']: img['filename'] for img in train_data['images']}
+ selected_display_name = st.selectbox(
+ "Choisissez une image d'entraînement pour voir les transformations:",
+ options=list(image_options.keys()),
+ help=f"{train_data['total_count']} images disponibles",
+ key="train_image_selector"
+ )
+
+ selected_filename = image_options[selected_display_name]
+
+ # Preview de l'image sélectionnée (avant preprocessing)
+ if selected_filename and "preprocessing_result" not in st.session_state:
+ try:
+ image_url = f"{API_BASE_URL}/train-image/{selected_filename}"
+ response = requests.get(image_url, timeout=5)
+
+ if response.status_code == 200:
+ preview_image = Image.open(io.BytesIO(response.content))
+ st.image(
+ preview_image,
+ caption=f"Aperçu de l'image sélectionnée : {selected_display_name}",
+ use_container_width=True
+ )
+ except:
+ st.info("Aperçu non disponible")
+
+ # Options de preprocessing
+ col1, col2 = st.columns(2)
+ with col1:
+ show_augmentation = st.checkbox(
+ "Inclure l'augmentation aléatoire",
+ help="Affiche un exemple d'augmentation de données utilisée pendant l'entraînement. Cliquez plusieurs fois sur 'Appliquer le Preprocessing' pour tester différentes augmentations aléatoires !",
+ key="augmentation_checkbox"
+ )
+
+ with col2:
+ if st.button("🔧 Appliquer le Preprocessing", type="primary", key="apply_preprocessing"):
+ with st.spinner("Application des transformations..."):
+ result = preprocess_train_image(selected_filename, show_augmentation)
+
+ if result and result['success']:
+ st.session_state.preprocessing_result = result
+ st.success("Preprocessing terminé!")
+ st.rerun()
+ else:
+ st.error("Erreur lors du preprocessing")
+
+ # Bouton pour revenir à l'aperçu
+ if "preprocessing_result" in st.session_state:
+ if st.button("🔄 Choisir une nouvelle image", type="secondary", key="reset_preprocessing"):
+ del st.session_state.preprocessing_result
+ st.rerun()
+
+ # Affichage des résultats de preprocessing
+ if "preprocessing_result" in st.session_state:
+ result = st.session_state.preprocessing_result
+
+ # Affichage des images transformées sur toute la largeur
+ st.markdown("#### Résultats des transformations :")
+
+ if show_augmentation and result.get('augmented_image'):
+ cols = st.columns(3)
+ col_titles = ["Image Originale (redimensionnée)", "Après Normalisation", "Avec Augmentation Aléatoire"]
+ col_data = [result.get('original_image'), result.get('normalized_image'), result.get('augmented_image')]
+ else:
+ cols = st.columns(2)
+ col_titles = ["Image Originale (redimensionnée)", "Après Normalisation"]
+ col_data = [result.get('original_image'), result.get('normalized_image')]
+
+ for i, (col, title, data) in enumerate(zip(cols, col_titles, col_data)):
+ with col:
+ st.markdown(f"**{title}**")
+ if data:
+ img_data = base64.b64decode(data)
+ img = Image.open(io.BytesIO(img_data))
+ st.image(img, use_container_width=True)
+
+
+ # Explications des transformations appliquées
+ if result.get('transformations_applied'):
+ st.markdown("#### 📋 Transformations appliquées :")
+
+ # Préparer les transformations pour l'alignement
+ transforms = result['transformations_applied']
+
+ # Extraire les transformations de base (toujours présentes)
+ base_transforms = [t for t in transforms if "Resize" in t or "Normalization" in t]
+ augmentation_transforms = [t for t in transforms if "Resize" not in t and "Normalization" not in t]
+
+ if show_augmentation and result.get('augmented_image'):
+ # 3 colonnes quand augmentation active
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.markdown("**Image Originale (redimensionnée)**")
+ resize_transform = next((t for t in base_transforms if "Resize" in t), "1. Resize (512x512)")
+ st.write(f"• {resize_transform}")
+
+ with col2:
+ st.markdown("**Après Normalisation**")
+ norm_transform = next((t for t in base_transforms if "Normalization" in t), "2. Normalization (0-1)")
+ st.write(f"• {norm_transform}")
+
+ with col3:
+ st.markdown("**Avec Augmentation Aléatoire**")
+ if augmentation_transforms:
+ for i, transform in enumerate(augmentation_transforms, 3):
+ st.write(f"• {i}. {transform}")
+ else:
+ st.write("• 3. Aucune augmentation appliquée (aléatoire)")
+
+ else:
+ # 2 colonnes quand pas d'augmentation
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown("**Image Originale (redimensionnée)**")
+ resize_transform = next((t for t in base_transforms if "Resize" in t), "1. Resize (512x512)")
+ st.write(f"• {resize_transform}")
+
+ with col2:
+ st.markdown("**Après Normalisation**")
+ norm_transform = next((t for t in base_transforms if "Normalization" in t), "2. Normalization (0-1)")
+ st.write(f"• {norm_transform}")
+
+ # Explications détaillées avec description complète des augmentations
+ with st.expander("ℹ️ Détails des transformations possibles"):
+ st.markdown("""
+ **Normalisation standard (toujours appliquée) :**
+ - **Redimensionnement** : Toutes les images sont redimensionnées à 512×512 pixels
+ - **Normalisation** : Les valeurs de pixels sont normalisées de [0-255] vers [0-1]
+ - Cette normalisation uniforme est appliquée à tous les modèles (paramètre encoder non utilisé)
+
+ **Augmentations de données possibles (si activées) :**
+
+ Le système sélectionne aléatoirement entre 1 et 4 transformations parmi les suivantes :
+
+ 1. **Rotation aléatoire** : Rotation de l'image entre -15° et +15°
+ 2. **Flip horizontal** : Miroir horizontal avec 50% de probabilité
+ 3. **Changement de luminosité/contraste** : Ajustement de ±20% de la luminosité et du contraste
+ 4. **Flou gaussien léger** : Application d'un flou gaussien subtil (blur 1-3 pixels)
+ 5. **Décalage couleur** : Modification de la teinte, saturation et valeur (±10-15%)
+ 6. **Zoom aléatoire** : Zoom de -10% à +10% (avec probabilité réduite)
+
+ **Important :** Les augmentations sont **aléatoires** à chaque application. Cliquez plusieurs fois sur
+ "Appliquer le Preprocessing" pour voir différentes combinaisons ! Le résultat peut inclure une ou
+ plusieurs transformations, ou même aucune selon le hasard.
+
+ Ces transformations permettent d'améliorer la robustesse et la généralisation du modèle pendant
+ l'entraînement en exposant le modèle à des variations réalistes des données.
+ """)
+ else:
+ st.warning("Aucune image d'entraînement disponible pour la démonstration")
+
+# ===== ONGLET 2: COMPARAISON DE PERFORMANCES =====
+with main_tab2:
+ # Variables de session pour les résultats
+ if "sample_comparison_result" not in st.session_state:
+ st.session_state.sample_comparison_result = None
+ if "upload_comparison_result" not in st.session_state:
+ st.session_state.upload_comparison_result = None
+
+ def reset_sample_comparison():
+ st.session_state.sample_comparison_result = None
+
+ def reset_upload_comparison():
+ st.session_state.upload_comparison_result = None
+
+ # Sous-onglets pour la comparaison
+ sub_tab1, sub_tab2 = st.tabs(["🏙️ Images Test Cityscapes", "📤 Upload Personnel"])
+
+ # SUB-TAB 1: Images Test Cityscapes
+ with sub_tab1:
+ st.markdown("## 🏙️ Comparaison sur Images Test Cityscapes")
+ st.markdown("Comparez les performances des deux modèles sur des images Cityscapes avec masques de vérité.")
+
+ # Récupérer les images d'exemple
+ sample_data = get_sample_images()
+ if sample_data and sample_data['images']:
+ # Sélecteur d'image
+ image_options = {img['display_name']: img['filename'] for img in sample_data['images']}
+ selected_display_name = st.selectbox(
+ "Choisissez une image:",
+ options=list(image_options.keys()),
+ help=f"{sample_data['total_count']} images disponibles"
+ )
+
+ selected_filename = image_options[selected_display_name]
+
+ # Afficher les infos de l'image sélectionnée
+ selected_img_info = next(img for img in sample_data['images'] if img['filename'] == selected_filename)
+
+ col1, col2 = st.columns([2, 1])
+ with col1:
+ st.info(f"📁 **Fichier:** {selected_filename}")
+ with col2:
+ ground_truth_status = "✅ Disponible" if selected_img_info['has_ground_truth'] else "❌ Non disponible"
+ st.info(f"🎯 **Vérité terrain:** {ground_truth_status}")
+
+ # Prévisualisation de l'image sélectionnée
+ if selected_filename:
+ try:
+ image_url = f"{API_BASE_URL}/sample-image/{selected_filename}"
+ response = requests.get(image_url, timeout=5)
+
+ if response.status_code == 200:
+ sample_image = Image.open(io.BytesIO(response.content))
+ st.image(
+ sample_image,
+ caption=f"Aperçu: {selected_display_name}",
+ use_container_width=True
+ )
+ except:
+ st.info("📷 Aperçu non disponible - L'image sera chargée lors de la comparaison")
+
+ # Bouton de comparaison
+ if st.button("🔄 Lancer la Comparaison", key="compare_sample", type="primary"):
+ if selected_filename:
+ with st.spinner("Comparaison en cours..."):
+ result = compare_sample_image(selected_filename)
+ if result and result['success']:
+ st.session_state.sample_comparison_result = result
+ st.success("✅ Comparaison terminée!")
+ st.rerun()
+ else:
+ st.error("❌ Erreur lors de la comparaison")
+ else:
+ st.warning("Aucune image d'exemple disponible")
+
+ # Affichage des résultats de comparaison
+ if st.session_state.sample_comparison_result:
+ st.markdown("---")
+ models_info = get_models_info()
+ display_comparison_results(st.session_state.sample_comparison_result, models_info, "sample")
+
+ # Bouton de reset
+ if st.button("🔄 Nouvelle Comparaison", key="reset_sample", type="secondary"):
+ reset_sample_comparison()
+ st.rerun()
+
+ # SUB-TAB 2: Upload Personnel
+ with sub_tab2:
+ st.markdown("## 📤 Comparaison sur Image Personnelle")
+ st.markdown("Uploadez une image pour comparer les performances des deux modèles.")
+
+ uploaded_file = st.file_uploader(
+ "Sélectionnez une image:",
+ type=['jpg', 'jpeg', 'png'],
+ help="Taille maximale recommandée: 5MB"
+ )
+
+ if uploaded_file is not None:
+ # Afficher l'image uploadée
+ image = Image.open(uploaded_file)
+ st.image(image, caption=f"Image uploadée: {uploaded_file.name}", use_container_width=True)
+
+ # Informations sur le fichier
+ file_size_mb = len(uploaded_file.getvalue()) / 1024 / 1024
+ st.info(f"📁 **Fichier:** {uploaded_file.name} | 📊 **Taille:** {file_size_mb:.2f} MB")
+
+ # Bouton de comparaison
+ if st.button("🔄 Lancer la Comparaison", key="compare_upload", type="primary"):
+ with st.spinner("Comparaison en cours..."):
+ result = compare_uploaded_image(uploaded_file)
+ if result and result['success']:
+ st.session_state.upload_comparison_result = result
+ st.success("✅ Comparaison terminée!")
+ st.rerun()
+ else:
+ st.error("❌ Erreur lors de la comparaison")
+
+ # Affichage des résultats de comparaison
+ if st.session_state.upload_comparison_result:
+ st.markdown("---")
+ models_info = get_models_info()
+ display_comparison_results(st.session_state.upload_comparison_result, models_info, "upload")
+
+ # Bouton de reset
+ if st.button("🔄 Nouvelle Comparaison", key="reset_upload", type="secondary"):
+ reset_upload_comparison()
+ st.rerun()
+
+# Sidebar avec informations des modèles
+with st.sidebar:
+ st.markdown("## 🔧 Informations des Modèles")
+
+ models_info = get_models_info()
+ if models_info:
+ st.success("✅ Modèles chargés")
+
+ # Modèle 1
+ best_model = models_info.get('best_model', {})
+ st.markdown("**🥇 Meilleur Modèle:**")
+ st.write(f"**Architecture:** {best_model.get('architecture', 'N/A')}")
+ st.write(f"**Encoder:** {best_model.get('encoder_name', 'N/A')}")
+ st.write(f"**Résolution:** {best_model.get('input_size', 'N/A')}")
+
+ # Modèle 2
+ second_model = models_info.get('second_best_model', {})
+ st.markdown("**🥈 Deuxième Modèle:**")
+ st.write(f"**Architecture:** {second_model.get('architecture', 'N/A')}")
+ st.write(f"**Encoder:** {second_model.get('encoder_name', 'N/A')}")
+ st.write(f"**Résolution:** {second_model.get('input_size', 'N/A')}")
+ else:
+ st.error("❌ Modèles non disponibles")
+
+ st.markdown("---")
+ st.markdown("## 🎯 Classes Segmentées (IoU)")
+
+ # Affichage avec carrés colorés et IoU
+ if models_info and 'class_names' in models_info and 'class_colors' in models_info:
+ class_names = models_info['class_names']
+ class_colors = models_info['class_colors']
+
+ # Récupérer les IoU des modèles
+ best_ious = best_model.get('class_ious', {}) if models_info else {}
+ second_ious = second_model.get('class_ious', {}) if models_info else {}
+
+ for i, (class_name, color) in enumerate(zip(class_names, class_colors)):
+ color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
+
+ # Construire la chaîne IoU
+ iou_str = ""
+ if best_ious.get(class_name) is not None and second_ious.get(class_name) is not None:
+ best_iou = best_ious[class_name]
+ second_iou = second_ious[class_name]
+ iou_str = f" (🥇{best_iou:.2f} - 🥈{second_iou:.2f})"
+ elif best_ious.get(class_name) is not None:
+ best_iou = best_ious[class_name]
+ iou_str = f" (🥇{best_iou:.2f})"
+ elif second_ious.get(class_name) is not None:
+ second_iou = second_ious[class_name]
+ iou_str = f" (🥈{second_iou:.2f})"
+
+ st.markdown(f"""
+
+
+
{i}. {class_name}{iou_str}
+
+ """, unsafe_allow_html=True)
+
+# Footer
+st.markdown("---")
+st.markdown("""
+
+ 🚗 Street Vision - Comparaison de Modèles -
+ Évaluation comparative de modèles de segmentation sémantique
+ Développé avec FastAPI, Streamlit et PyTorch
+
+""", unsafe_allow_html=True)
+
+
+with st.sidebar:
+ st.markdown("---")
+ st.markdown("## ♿ Conformité WCAG 2.1")
+
+ criteria_status = {
+ "1.1.1 Contenu non textuel": "✅",
+ "1.4.1 Utilisation de la couleur": "✅",
+ "1.4.3 Contraste (minimum)": "✅",
+ "1.4.4 Redimensionnement du texte": "✅",
+ "2.4.2 Titre de page": "✅"
+ }
+
+ for criterion, status in criteria_status.items():
+ st.write(f"{status} {criterion}")
+
+ st.markdown("**Niveau :** AA | **Référence :** [WCAG 2.1](https://www.w3.org/WAI/WCAG21/quickref/)")
+
diff --git a/cityscapes_config.json b/cityscapes_config.json
index 62b5034..4b61838 100644
--- a/cityscapes_config.json
+++ b/cityscapes_config.json
@@ -1,63 +1,63 @@
-{
- "class_groups": {
- "flat": ["road", "sidewalk", "parking", "rail track"],
- "human": ["person", "rider"],
- "vehicle": ["car", "truck", "bus", "train", "motorcycle", "bicycle", "caravan", "trailer"],
- "construction": ["building", "wall", "fence", "guard rail", "bridge", "tunnel"],
- "object": ["pole", "pole group", "traffic sign", "traffic light"],
- "nature": ["vegetation", "terrain"],
- "sky": ["sky"],
- "void": ["unlabeled", "ego vehicle", "rectification border", "out of roi", "static", "dynamic", "ground"]
- },
- "cityscapes_mapping": {
- "0": 7, "1": 7, "2": 7, "3": 7, "4": 7, "5": 7, "6": 7,
- "7": 0, "8": 0, "9": 0, "10": 0,
- "11": 3, "12": 3, "13": 3, "14": 3, "15": 3, "16": 3,
- "17": 4, "18": 4, "19": 4, "20": 4,
- "21": 5, "22": 5,
- "23": 6,
- "24": 1, "25": 1,
- "26": 2, "27": 2, "28": 2, "29": 2, "30": 2, "31": 2, "32": 2, "33": 2
- },
- "id_to_label": {
- "0": "unlabeled",
- "1": "ego vehicle",
- "2": "rectification border",
- "3": "out of roi",
- "4": "static",
- "5": "dynamic",
- "6": "ground",
- "7": "road",
- "8": "sidewalk",
- "9": "parking",
- "10": "rail track",
- "11": "building",
- "12": "wall",
- "13": "fence",
- "14": "guard rail",
- "15": "bridge",
- "16": "tunnel",
- "17": "pole",
- "18": "pole group",
- "19": "traffic light",
- "20": "traffic sign",
- "21": "vegetation",
- "22": "terrain",
- "23": "sky",
- "24": "person",
- "25": "rider",
- "26": "car",
- "27": "truck",
- "28": "bus",
- "29": "caravan",
- "30": "trailer",
- "31": "train",
- "32": "motorcycle",
- "33": "bicycle"
- },
- "group_colors": [
- [128, 64, 128], [220, 20, 60], [0, 0, 142], [70, 70, 70],
- [220, 220, 0], [107, 142, 35], [70, 130, 180], [0, 0, 0]
- ],
- "group_names": ["flat", "human", "vehicle", "construction", "object", "nature", "sky", "void"]
-}
+{
+ "class_groups": {
+ "flat": ["road", "sidewalk", "parking", "rail track"],
+ "human": ["person", "rider"],
+ "vehicle": ["car", "truck", "bus", "train", "motorcycle", "bicycle", "caravan", "trailer"],
+ "construction": ["building", "wall", "fence", "guard rail", "bridge", "tunnel"],
+ "object": ["pole", "pole group", "traffic sign", "traffic light"],
+ "nature": ["vegetation", "terrain"],
+ "sky": ["sky"],
+ "void": ["unlabeled", "ego vehicle", "rectification border", "out of roi", "static", "dynamic", "ground"]
+ },
+ "cityscapes_mapping": {
+ "0": 7, "1": 7, "2": 7, "3": 7, "4": 7, "5": 7, "6": 7,
+ "7": 0, "8": 0, "9": 0, "10": 0,
+ "11": 3, "12": 3, "13": 3, "14": 3, "15": 3, "16": 3,
+ "17": 4, "18": 4, "19": 4, "20": 4,
+ "21": 5, "22": 5,
+ "23": 6,
+ "24": 1, "25": 1,
+ "26": 2, "27": 2, "28": 2, "29": 2, "30": 2, "31": 2, "32": 2, "33": 2
+ },
+ "id_to_label": {
+ "0": "unlabeled",
+ "1": "ego vehicle",
+ "2": "rectification border",
+ "3": "out of roi",
+ "4": "static",
+ "5": "dynamic",
+ "6": "ground",
+ "7": "road",
+ "8": "sidewalk",
+ "9": "parking",
+ "10": "rail track",
+ "11": "building",
+ "12": "wall",
+ "13": "fence",
+ "14": "guard rail",
+ "15": "bridge",
+ "16": "tunnel",
+ "17": "pole",
+ "18": "pole group",
+ "19": "traffic light",
+ "20": "traffic sign",
+ "21": "vegetation",
+ "22": "terrain",
+ "23": "sky",
+ "24": "person",
+ "25": "rider",
+ "26": "car",
+ "27": "truck",
+ "28": "bus",
+ "29": "caravan",
+ "30": "trailer",
+ "31": "train",
+ "32": "motorcycle",
+ "33": "bicycle"
+ },
+ "group_colors": [
+ [128, 64, 128], [220, 20, 60], [0, 0, 142], [70, 70, 70],
+ [220, 220, 0], [107, 142, 35], [70, 130, 180], [0, 0, 0]
+ ],
+ "group_names": ["flat", "human", "vehicle", "construction", "object", "nature", "sky", "void"]
+}
diff --git a/docker-compose.yml.example b/docker-compose.yml.example
index b5f8514..f05e457 100644
--- a/docker-compose.yml.example
+++ b/docker-compose.yml.example
@@ -1,47 +1,47 @@
-version: '3.8'
-
-services:
- street-vision-api:
- # ✅ PRODUCTION: Image depuis Docker Hub
- image: grgmdmn/street-vision_api:latest
-
- container_name: street_vision_api
-
- ports:
- - "${NGINX_PORT:-8080}:${NGINX_PORT:-8080}" # Port nginx configurable
-
- environment:
- # Configuration du port nginx
- NGINX_PORT: "${NGINX_PORT:-8080}"
-
- # Configuration MLflow pour le suivi des modèles
- MLFLOW_TRACKING_URI: "${MLFLOW_TRACKING_URI}" # ex: https://mlflow.domain.com
- MLFLOW_S3_ENDPOINT_URL: "${MLFLOW_S3_ENDPOINT_URL}" # ex: url MinIO ou S3
- AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}" # clé ID S3
- AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}" # clé secrète S3
-
- # Configuration email pour notifications (optionnel)
- SMTP_SERVER: "${SMTP_SERVER:-}" # serveur SMTP
- SMTP_PORT: "${SMTP_PORT:-587}" # port SMTP (ex: 587)
- SMTP_EMAIL: "${SMTP_EMAIL:-}" # email expéditeur
- SMTP_PASSWORD: "${SMTP_PASSWORD:-}" # mot de passe SMTP
- SMTP_FROM_ALIAS: "${SMTP_FROM_ALIAS:-Street Vision API}" # alias (optionnel)
- ADMIN_EMAIL: "${ADMIN_EMAIL:-}" # email admin destinataire
-
- # URL de base de l'API (utilisée par Streamlit pour rediriger vers FastAPI)
- MULTISEG_API_BASE_URL: "${MULTISEG_API_BASE_URL}" # ex: https://vision.domain.com/api
-
- # Pour une mise à jour automatique par watchtower
- labels:
- com.centurylinklabs.watchtower.enable: "true"
-
- # Seulement si vous avez un reverse proxy server
- # networks:
- # - swag
-
- restart: unless-stopped
-
-# Seulement si vous avez un reverse proxy server
-# networks:
-# swag:
-# external: true
+version: '3.8'
+
+services:
+ street-vision-api:
+ # ✅ PRODUCTION: Image depuis Docker Hub
+ image: grgmdmn/street-vision_api:latest
+
+ container_name: street_vision_api
+
+ ports:
+ - "${NGINX_PORT:-8080}:${NGINX_PORT:-8080}" # Port nginx configurable
+
+ environment:
+ # Configuration du port nginx
+ NGINX_PORT: "${NGINX_PORT:-8080}"
+
+ # Configuration MLflow pour le suivi des modèles
+ MLFLOW_TRACKING_URI: "${MLFLOW_TRACKING_URI}" # ex: https://mlflow.domain.com
+ MLFLOW_S3_ENDPOINT_URL: "${MLFLOW_S3_ENDPOINT_URL}" # ex: url MinIO ou S3
+ AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}" # clé ID S3
+ AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}" # clé secrète S3
+
+ # Configuration email pour notifications (optionnel)
+ SMTP_SERVER: "${SMTP_SERVER:-}" # serveur SMTP
+ SMTP_PORT: "${SMTP_PORT:-587}" # port SMTP (ex: 587)
+ SMTP_EMAIL: "${SMTP_EMAIL:-}" # email expéditeur
+ SMTP_PASSWORD: "${SMTP_PASSWORD:-}" # mot de passe SMTP
+ SMTP_FROM_ALIAS: "${SMTP_FROM_ALIAS:-Street Vision API}" # alias (optionnel)
+ ADMIN_EMAIL: "${ADMIN_EMAIL:-}" # email admin destinataire
+
+ # URL de base de l'API (utilisée par Streamlit pour rediriger vers FastAPI)
+ MULTISEG_API_BASE_URL: "${MULTISEG_API_BASE_URL}" # ex: https://vision.domain.com/api
+
+ # Pour une mise à jour automatique par watchtower
+ labels:
+ com.centurylinklabs.watchtower.enable: "true"
+
+ # Seulement si vous avez un reverse proxy server
+ # networks:
+ # - swag
+
+ restart: unless-stopped
+
+# Seulement si vous avez un reverse proxy server
+# networks:
+# swag:
+# external: true
diff --git a/docs/Multi Segmentation Project - Dashboard Specs html code b/docs/Multi Segmentation Project - Dashboard Specs html code
index 2380bc5..7977b54 100644
--- a/docs/Multi Segmentation Project - Dashboard Specs html code
+++ b/docs/Multi Segmentation Project - Dashboard Specs html code
@@ -1,230 +1,230 @@
-
-
-
-
-
- Cahier des Charges Dashboard
-
-
-
-
-
Cahier des Charges Dashboard Interactif
-
Spécifications et implémentation - Projet DataSpace
-
-
-
-
- | Catégorie |
- Exigences Projet |
- Implémentation |
-
-
-
-
- | ANALYSE EXPLORATOIRE |
-
- • Visualisation contenu dataset
- • Analyses statistiques describe()
- • 2+ graphiques interactifs
- • Distribution des classes
- |
-
- Streamlit + Plotly interactif
- Métriques dataset Cityscapes
- Distribution pixel-wise classes
- Graphiques responsive
- |
-
-
-
- | COMPARAISON MODÈLES |
-
- • Sélection données entrée
- • Upload images personnalisées
- • Prédiction temps réel
- • Affichage résultats comparatifs
- |
-
- Interface upload + dropdown
- Inférence FPN vs SegFormer
- Visualisation masques côte-à-côte
- Métriques temps d'exécution
- |
-
-
-
- | ACCESSIBILITÉ WCAG |
-
- • Critères WCAG essentiels
- • Contraste couleurs
- • Navigation clavier
- • Labels descriptifs
- |
-
- Palette couleurs accessibles
- Alt-text images générées
- Semantic HTML structure
- Focus indicators
- |
-
-
-
- | DÉPLOIEMENT CLOUD |
-
- • Application déployée online
- • Interface responsive
- • Performance acceptable
- • Code source accessible
- |
-
- Serveur NAS personnel (Intel N100)
- Streamlit containerisé
- Optimisation modèles CPU
- Repository GitHub documenté
- |
-
-
-
-
-
-
🛠️ Stack Technique
-
-
FastAPI
-
Streamlit
-
PyTorch
-
Plotly
-
PIL/OpenCV
-
Docker
-
Intel N100
-
-
-
-
+
+
+
+
+
+ Cahier des Charges Dashboard
+
+
+
+
+
Cahier des Charges Dashboard Interactif
+
Spécifications et implémentation - Projet DataSpace
+
+
+
+
+ | Catégorie |
+ Exigences Projet |
+ Implémentation |
+
+
+
+
+ | ANALYSE EXPLORATOIRE |
+
+ • Visualisation contenu dataset
+ • Analyses statistiques describe()
+ • 2+ graphiques interactifs
+ • Distribution des classes
+ |
+
+ Streamlit + Plotly interactif
+ Métriques dataset Cityscapes
+ Distribution pixel-wise classes
+ Graphiques responsive
+ |
+
+
+
+ | COMPARAISON MODÈLES |
+
+ • Sélection données entrée
+ • Upload images personnalisées
+ • Prédiction temps réel
+ • Affichage résultats comparatifs
+ |
+
+ Interface upload + dropdown
+ Inférence FPN vs SegFormer
+ Visualisation masques côte-à-côte
+ Métriques temps d'exécution
+ |
+
+
+
+ | ACCESSIBILITÉ WCAG |
+
+ • Critères WCAG essentiels
+ • Contraste couleurs
+ • Navigation clavier
+ • Labels descriptifs
+ |
+
+ Palette couleurs accessibles
+ Alt-text images générées
+ Semantic HTML structure
+ Focus indicators
+ |
+
+
+
+ | DÉPLOIEMENT CLOUD |
+
+ • Application déployée online
+ • Interface responsive
+ • Performance acceptable
+ • Code source accessible
+ |
+
+ Serveur NAS personnel (Intel N100)
+ Streamlit containerisé
+ Optimisation modèles CPU
+ Repository GitHub documenté
+ |
+
+
+
+
+
+
🛠️ Stack Technique
+
+
FastAPI
+
Streamlit
+
PyTorch
+
Plotly
+
PIL/OpenCV
+
Docker
+
Intel N100
+
+
+
+
\ No newline at end of file
diff --git a/docs/note_methodologique.md b/docs/note_methodologique.md
index 4cb5266..c280fd0 100644
--- a/docs/note_methodologique.md
+++ b/docs/note_methodologique.md
@@ -1,295 +1,295 @@
-# Note Méthodologique : Preuve de concept SegFormer vs FPN
-
-## Dataset retenu
-
-### Cityscapes Dataset : Référence en Segmentation Urbaine
-
-Le **Cityscapes Dataset** constitue la référence internationale pour l'évaluation des algorithmes de segmentation sémantique en environnement urbain. Développé par l'Université de Tübingen et Mercedes-Benz, ce dataset comprend **25,000 images** haute résolution (2048×1024 pixels) capturées dans 50 villes allemandes et suisses.
-
-**Composition du dataset :**
-- **Train** : 2,975 images finement annotées
-- **Validation** : 500 images avec annotations de référence
-- **Test** : 1,525 images (annotations non-publiques)
-- **Coarse** : 20,000 images avec annotations grossières
-
-**Architecture de classes hiérarchique :**
-Le dataset organise 34 classes détaillées en **8 méta-classes** pour la segmentation :
-
-| Méta-classe | Classes incluses | Importance |
-|-------------|------------------|------------|
-| **Flat** | Route, trottoir, parkings | Navigation de base |
-| **Human** | Piéton, cycliste | **Sécurité critique** |
-| **Vehicle** | Voiture, camion, bus, train | Obstacles dynamiques |
-| **Construction** | Bâtiment, mur, clôture, pont | Structure urbaine |
-| **Object** | Poteau, feu, panneau | Signalétique |
-| **Nature** | Végétation, terrain | Environnement |
-| **Sky** | Ciel | Contexte |
-| **Void** | Zones non-définies | Exclusion |
-
-**Particularité méthodologique critique :**
-Contrairement au protocole original préservant le ratio 2:1 natif (2048×1024), nos expérimentations utilisent des **images carrées (1:1)** aux résolutions 512×512 et 768×768. Cette déformation géométrique, motivée par les contraintes GPU T4, permet d'analyser l'impact du ratio d'image sur les différentes philosophies architecturales (CNN vs Transformer).
-
----
-
-## Les concepts de l'algorithme récent
-
-### SegFormer : Architecture Transformer Vision-Optimisée Temps Réel
-
-**SegFormer** représente une avancée dans la segmentation sémantique en introduisant une architecture **Transformer** spécifiquement conçue pour l'efficacité computationnelle, contrairement aux approches Transformer classiques gourmandes en ressources.
-
-### Innovations Architecturales Clés
-
-**1. Encodeur Hiérarchique Sans Encodage Positionnel**
-
-L'encodeur SegFormer **abandonne les encodages positionnels** au profit d'une architecture hiérarchique qui capture naturellement les relations spatiales :
-
-$$\text{Feature Maps} = \{F_1, F_2, F_3, F_4\}$$
-
-avec résolutions $\{\frac{H}{4}, \frac{H}{8}, \frac{H}{16}, \frac{H}{32}\}$
-
-**Efficient Self-Attention :**
-Réduction de complexité par sous-échantillonnage des clés :
-
-$$\text{Attention}(Q,K,V) = \text{Softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$
-
-avec complexité réduite de $\mathcal{O}(N^2)$ à $\mathcal{O}\left(\frac{N^2}{R}\right)$ où $R$ est le facteur de réduction.
-
-**2. Mix-FFN : Hybridation CNN-Transformer**
-
-Innovation clé : intégration de **convolutions 3×3** dans les couches Feed-Forward :
-
-$$\text{Mix-FFN}(x) = \text{MLP}(\text{GELU}(\text{Conv}_{3×3}(\text{MLP}(x)) + x))$$
-
-Cette approche permet :
-- **Élimination totale des encodages positionnels**
-- **Capture locale + compréhension globale** simultanées
-- **Robustesse aux variations de résolution** d'entrée
-
-**3. Décodeur MLP Ultra-Léger**
-
-Simplification drastique vs décodeurs complexes traditionnels :
-
-$$\text{Seg} = \text{MLP}\left(\text{Concat}[\text{Upsample}(F_1), F_2, F_3, F_4]\right)$$
-
-**Avantages :**
-- **Réduction massive des paramètres** : Focus sur l'encodeur performant
-- **Inférence accélérée** : Décodeur 10× plus rapide que ASPP/PSP
-- **Architecture unifiée** : Traitement multi-échelle sans modules ad-hoc
-
-### Positionnement Temps Réel vs État de l'Art
-
-**Analyse critique du benchmark original :**
-Le papier SegFormer révèle que **SegFormer-B0** (3.7M paramètres) atteint **75.3% mIoU** sur Cityscapes à résolution réduite (côté court 768px), égalant **DeepLabv3+ + MobileNetV2** (75.2% mIoU) à résolution pleine. Cette **efficacité computationnelle supérieure** constitue l'argument central de notre hypothèse de recherche.
-
-**Hypothèse de transfert :**
-Si DeepLabv3+ surpasse théoriquement FPN, alors SegFormer-B0 devrait également surpasser notre baseline FPN+EfficientNetB0 (78.67% mIoU), particulièrement à 768×768 pixels.
-
----
-
-## La modélisation
-
-### Méthodologie Expérimentale Adaptée
-
-**Contraintes et adaptations :**
-Notre protocole d'évaluation diffère substantiellement du papier original pour s'adapter aux contraintes pratiques :
-
-**Configuration matérielle :**
-- **Plateforme** : Google Colab Pro avec GPU T4 (15Go VRAM)
-- **Framework** : Migration TensorFlow/Keras → PyTorch + Segmentation Models PyTorch
-- **Contraintes** : Plan payant avec unités de calcul limitées
-
-**Déformation géométrique :**
-- **Protocole original** : Préservation ratio 2:1 natif Cityscapes
-- **Notre protocole** : Déformation vers ratio 1:1 (images carrées)
-- **Résolutions testées** : 512×512 et 768×768 pixels
-- **Impact attendu** : Analyse différentielle CNN vs Transformer sous contrainte géométrique
-
-### Preprocessing et Modèles Pré-entraînés
-
-**Modèles SegFormer (pré-entraînés ADE20K) :**
-- **SegFormer-B0** : `segformer-b0-512x512-ade-160k` (3.7M paramètres)
-- **SegFormer-B1** : `segformer-b1-512x512-ade-160k` (13.7M paramètres, exploration scaling)
-
-**Modèles FPN (pré-entraînés ImageNet) :**
-- **FPN+ResNet34** : 23.16M paramètres
-- **FPN+EfficientNet-B0** : 5.76M paramètres (baseline de référence @224x224 : 74.6% mIoU)
-
-### Configuration d'Entraînement Optimisée
-
-**Configuration SegFormer :**
-- **Loss Function** : Jaccard Loss pour optimisation IoU directe
-- **Optimiseur** : AdamW (lr=1e-4, weight_decay=1e-4, spécialisé Transformers)
-- **Scheduler** : Cosine Annealing (convergence douce, min_lr=1e-6)
-- **Epochs** : Maximum 30 avec early stopping (patience=5)
-
-**Configuration FPN :**
-- **Loss Function** : Focal + Jaccard Loss combinées (poids 1:1)
-- **Optimiseur** : Adam (lr=1.5e-4, weight_decay=1e-5, optimisé CNN)
-- **Scheduler** : ReduceLROnPlateau (adaptation réactive, patience=4)
-- **Epochs** : Maximum 20 avec early stopping (patience=7)
-
-**Stratégie d'entraînement :**
-Entraînement séquentiel des 4 modèles principaux à 512×512, puis scaling à 768×768 pour analyse comparative résolution/performance.
-
----
-
-## Une synthèse des résultats
-
-
-*Fig.1: Mean IoU et Accuracy par expérience*
-
-### Performance Comparative : SegFormer vs FPN sous Contrainte Géométrique
-
-**Résultats principaux (mIoU sur Cityscapes Validation) :**
-
-| **Modèle** | **mIoU** | **Gain vs Baseline** | **Paramètres** | **Taille** | **Temps CPU** |
-|------------|----------|----------------------|----------------|------------|---------------|
-| **SegFormer-B1** | **79.98%** | **+1.31%** | **13.68M** | **52MB** | **1.39s** |
-| **FPN+ResNet34** | **79.22%** | **+0.55%** | **23.16M** | **88MB** | **0.73s** |
-| **FPN+EfficientNet-B0** | **78.67%** | *baseline* | **5.76M** | **22MB** | **0.48s** |
-| **SegFormer-B0** | **77.91%** | **-0.76%** | **3.72M** | **14MB** | **0.87s** |
-
-*Temps d'inférence : CPU Intel Xeon @ 2.20GHz (2 vCPUs) Google Colab*
-
-### Révélations Critiques : Impact du Ratio d'Image
-
-**Résultat majeur inattendu :**
-Contrairement aux prédictions du benchmark original, **SegFormer-B0 sous-performe face aux baselines FPN** sur images carrées (77.91% vs 78.67% mIoU). Cette observation révèle une **sensibilité architecturale différentielle** aux déformations géométriques.
-
-**Hypothèse explicative - Robustesse géométrique :**
-
-1. **CNN (FPN)** : Les convolutions avec leurs kernels fixes et pooling local gèrent naturellement les déformations géométriques
-2. **Transformer (SegFormer)** : L'attention globale, optimisée sur ratio 2:1 natif, souffre de la perte d'information spatiale lors de la déformation 1:1
-3. **Scaling architectural** : SegFormer-B1 compense cette limitation par sa capacité supérieure (+1.31% mIoU)
-
-### Analyse Détaillée par Classes Sémantiques
-
-
-*Fig.2: IoU et Δ IoU par Classe entre SegFormer-B1 et FPN-ResNet34*
-
-**Performance par méta-classes Cityscapes :**
-
-| **Classe** | **SegFormer-B1** | **FPN+ResNet34** | **Gain** | **Analyse** |
-|------------|-------------------|-------------------|----------|-------------|
-| **Flat** | **93.35%** | **93.38%** | **-0.03%** | Performance équivalente |
-| **Human** | **71.13%** | **69.35%** | **+1.78%** | ⭐ **Attention globale critique** |
-| **Vehicle** | **88.80%** | **87.75%** | **+1.05%** | ⭐ **Contexte multi-échelle** |
-| **Construction** | **85.94%** | **85.06%** | **+0.88%** | Structures complexes |
-| **Object** | **49.24%** | **48.68%** | **+0.56%** | ⭐ **Signalétique petite taille** |
-| **Nature** | **89.14%** | **88.13%** | **+1.01%** | Textures variées |
-| **Sky** | **92.35%** | **91.67%** | **+0.68%** | Régions homogènes |
-| **Void** | **69.89%** | **69.76%** | **+0.13%** | Zones ambiguës |
-
-**Patterns d'amélioration SegFormer-B1 :**
-- **Classes sécuritaires critiques** (Human +1.78%, Vehicle +1.05%) : Validation de l'attention globale pour objets contextuels
-- **Objets petite taille** (Object +0.56%) : Meilleure capture multi-échelle vs convolutions locales
-- **Performance équivalente** sur surfaces homogènes (Flat -0.03%)
-
-### Performance Surprenante vs Benchmark Original
-
-**Observation contre-intuitive :**
-Nos modèles atteignent des performances **supérieures au papier original** malgré la contrainte géométrique @768x768 :
-- **SegFormer-B0 : 77.91%** vs 75.3%
-- **FPN baselines : 78.67-79.22%** vs DeepLabV3+ 75.2%
-- **SegFormer-B1 : 79.98%** vs ~76-78% (en effectuant un scaling grossier B0→B1 en se basant sur l'écart de performances de B0-->B5)
-
-
-**Hypothèses explicatives :**
-1. **Optimisation du ratio 1:1** : Réduction de l'aire d'image (×2) concentre l'information spatiale
-2. **Optimisation d'hyperparamètres** : Par exemple, utilisation de fonctions de perte Jaccard (+ Focal) ? Non mentionné dans SegFormer (2021) ni DeepLabV3 (2018)
-3. **Encodeurs** : EfficientNetB0 à peine plus long que MobileNetV2 et performances au-dessus. ResNet34 au-dessus également mais plus lourd (peut-être pertinent en temps réel...?)
-3. **Framework PyTorch** : Optimisations modernes vs implémentations TensorFlow du papier DeepLabV3+ (2018)
-
----
-
-## L'analyse de la feature importance globale et locale du nouveau modèle
-
-### Limitations Méthodologiques de l'Explicabilité Transformer
-
-L'analyse d'explicabilité des architectures Transformer pour la segmentation dense représente un **défi technique majeur** dépassant le cadre de cette preuve de concept. Les mécanismes d'attention multi-têtes, Mix-FFN, et fusion hiérarchique créent des interactions complexes difficiles à décomposer causalement.
-
-### Insights Architecturaux via Patterns de Performance
-
-**Analyse indirecte par performance différentielle :**
-
-Bien que l'explicabilité fine soit inaccessible, les patterns de performance révèlent les mécanismes sous-jacents :
-
-1. **Attention globale validée** : Gains significatifs sur classes contextuelles (Human +1.78%, Vehicle +1.05%) suggèrent une **compréhension scénique** supérieure aux CNN
-2. **Sensibilité géométrique** : Sous-performance SegFormer-B0 vs FPN sur images déformées révèle une **dépendance aux relations spatiales natives**
-3. **Scaling architectural efficace** : Bond B0→B1 (+2.07% mIoU) confirme l'efficacité du scaling Transformer
-
-**Limitation fondamentale :**
-Les cartes d'attention ne correspondent pas nécessairement à l'importance causale réelle. Une explicabilité rigoureuse nécessiterait des approches spécialisées comme **Deep Taylor Decomposition** ou **Integrated Gradients** adaptées aux architectures hybrides CNN-Transformer.
-
----
-
-## Limites et améliorations possibles
-
-### Limitations Expérimentales Critiques
-
-**1. Contraintes Protocolaires Majeures**
-- **Déformation géométrique forcée** : Ratio 2:1→1:1 non-représentatif du déploiement réel
-- **Résolutions limitées** : GPU T4 contraint l'exploration des résolutions supérieures (>768px). Augmentation importante de l'utilisation de VRAM et réduction drastique du `batch_size` --> entraînements plus longs (plus d'époques)
-- **Framework migration** : TensorFlow→PyTorch peut introduire des biais de comparaison
-- **Métriques d'évaluation** : Temps d'inférence CPU non-représentatifs du déploiement GPU optimisé
-
-**2. Généralisation Limitée**
-- **Dataset unique** : Évaluation exclusive sur Cityscapes urbain allemand
-- **Conditions météo** : Uniquement conditions favorables (pas de nuit/intempéries)
-- **Scaling incomplet** : SegFormer-B2+ non-testés faute de ressources computationnelles
-
-### Améliorations Prioritaires Identifiées
-
-**1. Robustesse Protocolaire**
-- **Préservation ratio natif** : Tests sur images 2:1 sans déformation géométrique (contraintes budgétaires/temps limitant cette approche)
-- **Résolutions supérieures** : Évaluation à 1024×512 ou résolutions natives pour validation écologique
-- **Evaluation multi-dataset** : Validation BDD100K, Mapillary Vistas pour généralisation
-
-**2. Scaling Architectural Complet**
-- **SegFormer variants complets** : B2, B3, B4 pour validation des gains à tous les niveaux de complexité vs papier original
-- **Évaluation différentielle** : Déterminer si les améliorations observées sont spécifiques aux modèles légers (B0-B1) ou généralisables aux variants lourds
-- **Optimisations déploiement** : TensorRT, quantization FP16/INT8 pour inférence temps réel selon niveau de complexité
-
-**3. Analyse Causale Approfondie**
-1. **Prototypage rapide** : SegFormer-B1 comme référence architecturale moderne pour projets nécessitant performance maximale
-2. **Contraintes embarquées** : FPN+EfficientNet-B0 optimal pour déploiement edge ultra-contraint à 768×768, avec évaluation requise à résolutions supérieures pour validation complète
-3. **Adaptabilité clients** : Gamme SegFormer (B0-B1+) offrant scaling performance/ressources selon besoins spécifiques
-
-### Perspectives de Déploiement Pratique
-
-**Applications industrielles validées :**
-1. **Prototypage rapide** : SegFormer-B1 comme référence architecturale moderne pour projets nécessitant performance maximale
-2. **Contraintes embarquées** : FPN+EfficientNet-B0 optimal pour déploiement edge ultra-contraint à 768×768, avec évaluation requise à résolutions supérieures pour validation complète
-3. **Adaptabilité clients** : Gamme SegFormer (B0-B1+) offrant scaling performance/ressources selon besoins spécifiques
-
-**ROI technique contextuel :**
-Cette preuve de concept valide l'intérêt des architectures Transformer tout en révélant leurs **sensibilités pratiques** (ratio d'image, scaling nécessaire). L'approche constitue une **base méthodologique** solide pour futurs développements production avec contraintes réelles explicitées.
-
----
-
-## Conclusion Méthodologique
-
-### Contribution Analytique Principale
-
-Cette étude apporte une **perspective complémentaire critique** aux évaluations académiques standard en révélant :
-
-1. **Impact géométrique différentiel** : Sensibilité CNN vs Transformer aux déformations d'image
-2. **Scaling architectural requis** : Nécessité SegFormer-B1+ pour surpasser baselines CNN établies en ratio 1:1
-3. **Trade-offs pratiques** : Compromis performance/efficacité sous contraintes réelles
-
-### Validation des Hypothèses de Recherche
-
-**Hypothèses confirmées :**
-- ✅ **SegFormer-B1 surpasse FPN** (+1.31% mIoU) avec efficacité paramétrique supérieure
-- ✅ **Amélioration classes critiques** validée (Human +1.78%, Vehicle +1.05%)
-- ✅ **Architecture Transformer viable** pour segmentation temps réel avec scaling approprié
-
-**Hypothèses infirmées :**
-- ❌ **SegFormer-B0 sous-performant** vs FPN sur images 1:1
-- ❌ **Robustesse géométrique** Transformer surestimée vs CNN
-
-### Positionnement Scientifique
-
-Cette preuve de concept se distingue par son **réalisme méthodologique** : elle expose les limitations pratiques des architectures d'avant-garde sous contraintes déploiement réelles, offrant une analyse **nuancée et reproductible** aux équipes techniques devant opérer des choix architecturaux informés.
-
+# Note Méthodologique : Preuve de concept SegFormer vs FPN
+
+## Dataset retenu
+
+### Cityscapes Dataset : Référence en Segmentation Urbaine
+
+Le **Cityscapes Dataset** constitue la référence internationale pour l'évaluation des algorithmes de segmentation sémantique en environnement urbain. Développé par l'Université de Tübingen et Mercedes-Benz, ce dataset comprend **25,000 images** haute résolution (2048×1024 pixels) capturées dans 50 villes allemandes et suisses.
+
+**Composition du dataset :**
+- **Train** : 2,975 images finement annotées
+- **Validation** : 500 images avec annotations de référence
+- **Test** : 1,525 images (annotations non-publiques)
+- **Coarse** : 20,000 images avec annotations grossières
+
+**Architecture de classes hiérarchique :**
+Le dataset organise 34 classes détaillées en **8 méta-classes** pour la segmentation :
+
+| Méta-classe | Classes incluses | Importance |
+|-------------|------------------|------------|
+| **Flat** | Route, trottoir, parkings | Navigation de base |
+| **Human** | Piéton, cycliste | **Sécurité critique** |
+| **Vehicle** | Voiture, camion, bus, train | Obstacles dynamiques |
+| **Construction** | Bâtiment, mur, clôture, pont | Structure urbaine |
+| **Object** | Poteau, feu, panneau | Signalétique |
+| **Nature** | Végétation, terrain | Environnement |
+| **Sky** | Ciel | Contexte |
+| **Void** | Zones non-définies | Exclusion |
+
+**Particularité méthodologique critique :**
+Contrairement au protocole original préservant le ratio 2:1 natif (2048×1024), nos expérimentations utilisent des **images carrées (1:1)** aux résolutions 512×512 et 768×768. Cette déformation géométrique, motivée par les contraintes GPU T4, permet d'analyser l'impact du ratio d'image sur les différentes philosophies architecturales (CNN vs Transformer).
+
+---
+
+## Les concepts de l'algorithme récent
+
+### SegFormer : Architecture Transformer Vision-Optimisée Temps Réel
+
+**SegFormer** représente une avancée dans la segmentation sémantique en introduisant une architecture **Transformer** spécifiquement conçue pour l'efficacité computationnelle, contrairement aux approches Transformer classiques gourmandes en ressources.
+
+### Innovations Architecturales Clés
+
+**1. Encodeur Hiérarchique Sans Encodage Positionnel**
+
+L'encodeur SegFormer **abandonne les encodages positionnels** au profit d'une architecture hiérarchique qui capture naturellement les relations spatiales :
+
+$$\text{Feature Maps} = \{F_1, F_2, F_3, F_4\}$$
+
+avec résolutions $\{\frac{H}{4}, \frac{H}{8}, \frac{H}{16}, \frac{H}{32}\}$
+
+**Efficient Self-Attention :**
+Réduction de complexité par sous-échantillonnage des clés :
+
+$$\text{Attention}(Q,K,V) = \text{Softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$
+
+avec complexité réduite de $\mathcal{O}(N^2)$ à $\mathcal{O}\left(\frac{N^2}{R}\right)$ où $R$ est le facteur de réduction.
+
+**2. Mix-FFN : Hybridation CNN-Transformer**
+
+Innovation clé : intégration de **convolutions 3×3** dans les couches Feed-Forward :
+
+$$\text{Mix-FFN}(x) = \text{MLP}(\text{GELU}(\text{Conv}_{3×3}(\text{MLP}(x)) + x))$$
+
+Cette approche permet :
+- **Élimination totale des encodages positionnels**
+- **Capture locale + compréhension globale** simultanées
+- **Robustesse aux variations de résolution** d'entrée
+
+**3. Décodeur MLP Ultra-Léger**
+
+Simplification drastique vs décodeurs complexes traditionnels :
+
+$$\text{Seg} = \text{MLP}\left(\text{Concat}[\text{Upsample}(F_1), F_2, F_3, F_4]\right)$$
+
+**Avantages :**
+- **Réduction massive des paramètres** : Focus sur l'encodeur performant
+- **Inférence accélérée** : Décodeur 10× plus rapide que ASPP/PSP
+- **Architecture unifiée** : Traitement multi-échelle sans modules ad-hoc
+
+### Positionnement Temps Réel vs État de l'Art
+
+**Analyse critique du benchmark original :**
+Le papier SegFormer révèle que **SegFormer-B0** (3.7M paramètres) atteint **75.3% mIoU** sur Cityscapes à résolution réduite (côté court 768px), égalant **DeepLabv3+ + MobileNetV2** (75.2% mIoU) à résolution pleine. Cette **efficacité computationnelle supérieure** constitue l'argument central de notre hypothèse de recherche.
+
+**Hypothèse de transfert :**
+Si DeepLabv3+ surpasse théoriquement FPN, alors SegFormer-B0 devrait également surpasser notre baseline FPN+EfficientNetB0 (78.67% mIoU), particulièrement à 768×768 pixels.
+
+---
+
+## La modélisation
+
+### Méthodologie Expérimentale Adaptée
+
+**Contraintes et adaptations :**
+Notre protocole d'évaluation diffère substantiellement du papier original pour s'adapter aux contraintes pratiques :
+
+**Configuration matérielle :**
+- **Plateforme** : Google Colab Pro avec GPU T4 (15Go VRAM)
+- **Framework** : Migration TensorFlow/Keras → PyTorch + Segmentation Models PyTorch
+- **Contraintes** : Plan payant avec unités de calcul limitées
+
+**Déformation géométrique :**
+- **Protocole original** : Préservation ratio 2:1 natif Cityscapes
+- **Notre protocole** : Déformation vers ratio 1:1 (images carrées)
+- **Résolutions testées** : 512×512 et 768×768 pixels
+- **Impact attendu** : Analyse différentielle CNN vs Transformer sous contrainte géométrique
+
+### Preprocessing et Modèles Pré-entraînés
+
+**Modèles SegFormer (pré-entraînés ADE20K) :**
+- **SegFormer-B0** : `segformer-b0-512x512-ade-160k` (3.7M paramètres)
+- **SegFormer-B1** : `segformer-b1-512x512-ade-160k` (13.7M paramètres, exploration scaling)
+
+**Modèles FPN (pré-entraînés ImageNet) :**
+- **FPN+ResNet34** : 23.16M paramètres
+- **FPN+EfficientNet-B0** : 5.76M paramètres (baseline de référence @224x224 : 74.6% mIoU)
+
+### Configuration d'Entraînement Optimisée
+
+**Configuration SegFormer :**
+- **Loss Function** : Jaccard Loss pour optimisation IoU directe
+- **Optimiseur** : AdamW (lr=1e-4, weight_decay=1e-4, spécialisé Transformers)
+- **Scheduler** : Cosine Annealing (convergence douce, min_lr=1e-6)
+- **Epochs** : Maximum 30 avec early stopping (patience=5)
+
+**Configuration FPN :**
+- **Loss Function** : Focal + Jaccard Loss combinées (poids 1:1)
+- **Optimiseur** : Adam (lr=1.5e-4, weight_decay=1e-5, optimisé CNN)
+- **Scheduler** : ReduceLROnPlateau (adaptation réactive, patience=4)
+- **Epochs** : Maximum 20 avec early stopping (patience=7)
+
+**Stratégie d'entraînement :**
+Entraînement séquentiel des 4 modèles principaux à 512×512, puis scaling à 768×768 pour analyse comparative résolution/performance.
+
+---
+
+## Une synthèse des résultats
+
+
+*Fig.1: Mean IoU et Accuracy par expérience*
+
+### Performance Comparative : SegFormer vs FPN sous Contrainte Géométrique
+
+**Résultats principaux (mIoU sur Cityscapes Validation) :**
+
+| **Modèle** | **mIoU** | **Gain vs Baseline** | **Paramètres** | **Taille** | **Temps CPU** |
+|------------|----------|----------------------|----------------|------------|---------------|
+| **SegFormer-B1** | **79.98%** | **+1.31%** | **13.68M** | **52MB** | **1.39s** |
+| **FPN+ResNet34** | **79.22%** | **+0.55%** | **23.16M** | **88MB** | **0.73s** |
+| **FPN+EfficientNet-B0** | **78.67%** | *baseline* | **5.76M** | **22MB** | **0.48s** |
+| **SegFormer-B0** | **77.91%** | **-0.76%** | **3.72M** | **14MB** | **0.87s** |
+
+*Temps d'inférence : CPU Intel Xeon @ 2.20GHz (2 vCPUs) Google Colab*
+
+### Révélations Critiques : Impact du Ratio d'Image
+
+**Résultat majeur inattendu :**
+Contrairement aux prédictions du benchmark original, **SegFormer-B0 sous-performe face aux baselines FPN** sur images carrées (77.91% vs 78.67% mIoU). Cette observation révèle une **sensibilité architecturale différentielle** aux déformations géométriques.
+
+**Hypothèse explicative - Robustesse géométrique :**
+
+1. **CNN (FPN)** : Les convolutions avec leurs kernels fixes et pooling local gèrent naturellement les déformations géométriques
+2. **Transformer (SegFormer)** : L'attention globale, optimisée sur ratio 2:1 natif, souffre de la perte d'information spatiale lors de la déformation 1:1
+3. **Scaling architectural** : SegFormer-B1 compense cette limitation par sa capacité supérieure (+1.31% mIoU)
+
+### Analyse Détaillée par Classes Sémantiques
+
+
+*Fig.2: IoU et Δ IoU par Classe entre SegFormer-B1 et FPN-ResNet34*
+
+**Performance par méta-classes Cityscapes :**
+
+| **Classe** | **SegFormer-B1** | **FPN+ResNet34** | **Gain** | **Analyse** |
+|------------|-------------------|-------------------|----------|-------------|
+| **Flat** | **93.35%** | **93.38%** | **-0.03%** | Performance équivalente |
+| **Human** | **71.13%** | **69.35%** | **+1.78%** | ⭐ **Attention globale critique** |
+| **Vehicle** | **88.80%** | **87.75%** | **+1.05%** | ⭐ **Contexte multi-échelle** |
+| **Construction** | **85.94%** | **85.06%** | **+0.88%** | Structures complexes |
+| **Object** | **49.24%** | **48.68%** | **+0.56%** | ⭐ **Signalétique petite taille** |
+| **Nature** | **89.14%** | **88.13%** | **+1.01%** | Textures variées |
+| **Sky** | **92.35%** | **91.67%** | **+0.68%** | Régions homogènes |
+| **Void** | **69.89%** | **69.76%** | **+0.13%** | Zones ambiguës |
+
+**Patterns d'amélioration SegFormer-B1 :**
+- **Classes sécuritaires critiques** (Human +1.78%, Vehicle +1.05%) : Validation de l'attention globale pour objets contextuels
+- **Objets petite taille** (Object +0.56%) : Meilleure capture multi-échelle vs convolutions locales
+- **Performance équivalente** sur surfaces homogènes (Flat -0.03%)
+
+### Performance Surprenante vs Benchmark Original
+
+**Observation contre-intuitive :**
+Nos modèles atteignent des performances **supérieures au papier original** malgré la contrainte géométrique @768x768 :
+- **SegFormer-B0 : 77.91%** vs 75.3%
+- **FPN baselines : 78.67-79.22%** vs DeepLabV3+ 75.2%
+- **SegFormer-B1 : 79.98%** vs ~76-78% (en effectuant un scaling grossier B0→B1 en se basant sur l'écart de performances de B0-->B5)
+
+
+**Hypothèses explicatives :**
+1. **Optimisation du ratio 1:1** : Réduction de l'aire d'image (×2) concentre l'information spatiale
+2. **Optimisation d'hyperparamètres** : Par exemple, utilisation de fonctions de perte Jaccard (+ Focal) ? Non mentionné dans SegFormer (2021) ni DeepLabV3 (2018)
+3. **Encodeurs** : EfficientNetB0 à peine plus long que MobileNetV2 et performances au-dessus. ResNet34 au-dessus également mais plus lourd (peut-être pertinent en temps réel...?)
+3. **Framework PyTorch** : Optimisations modernes vs implémentations TensorFlow du papier DeepLabV3+ (2018)
+
+---
+
+## L'analyse de la feature importance globale et locale du nouveau modèle
+
+### Limitations Méthodologiques de l'Explicabilité Transformer
+
+L'analyse d'explicabilité des architectures Transformer pour la segmentation dense représente un **défi technique majeur** dépassant le cadre de cette preuve de concept. Les mécanismes d'attention multi-têtes, Mix-FFN, et fusion hiérarchique créent des interactions complexes difficiles à décomposer causalement.
+
+### Insights Architecturaux via Patterns de Performance
+
+**Analyse indirecte par performance différentielle :**
+
+Bien que l'explicabilité fine soit inaccessible, les patterns de performance révèlent les mécanismes sous-jacents :
+
+1. **Attention globale validée** : Gains significatifs sur classes contextuelles (Human +1.78%, Vehicle +1.05%) suggèrent une **compréhension scénique** supérieure aux CNN
+2. **Sensibilité géométrique** : Sous-performance SegFormer-B0 vs FPN sur images déformées révèle une **dépendance aux relations spatiales natives**
+3. **Scaling architectural efficace** : Bond B0→B1 (+2.07% mIoU) confirme l'efficacité du scaling Transformer
+
+**Limitation fondamentale :**
+Les cartes d'attention ne correspondent pas nécessairement à l'importance causale réelle. Une explicabilité rigoureuse nécessiterait des approches spécialisées comme **Deep Taylor Decomposition** ou **Integrated Gradients** adaptées aux architectures hybrides CNN-Transformer.
+
+---
+
+## Limites et améliorations possibles
+
+### Limitations Expérimentales Critiques
+
+**1. Contraintes Protocolaires Majeures**
+- **Déformation géométrique forcée** : Ratio 2:1→1:1 non-représentatif du déploiement réel
+- **Résolutions limitées** : GPU T4 contraint l'exploration des résolutions supérieures (>768px). Augmentation importante de l'utilisation de VRAM et réduction drastique du `batch_size` --> entraînements plus longs (plus d'époques)
+- **Framework migration** : TensorFlow→PyTorch peut introduire des biais de comparaison
+- **Métriques d'évaluation** : Temps d'inférence CPU non-représentatifs du déploiement GPU optimisé
+
+**2. Généralisation Limitée**
+- **Dataset unique** : Évaluation exclusive sur Cityscapes urbain allemand
+- **Conditions météo** : Uniquement conditions favorables (pas de nuit/intempéries)
+- **Scaling incomplet** : SegFormer-B2+ non-testés faute de ressources computationnelles
+
+### Améliorations Prioritaires Identifiées
+
+**1. Robustesse Protocolaire**
+- **Préservation ratio natif** : Tests sur images 2:1 sans déformation géométrique (contraintes budgétaires/temps limitant cette approche)
+- **Résolutions supérieures** : Évaluation à 1024×512 ou résolutions natives pour validation écologique
+- **Evaluation multi-dataset** : Validation BDD100K, Mapillary Vistas pour généralisation
+
+**2. Scaling Architectural Complet**
+- **SegFormer variants complets** : B2, B3, B4 pour validation des gains à tous les niveaux de complexité vs papier original
+- **Évaluation différentielle** : Déterminer si les améliorations observées sont spécifiques aux modèles légers (B0-B1) ou généralisables aux variants lourds
+- **Optimisations déploiement** : TensorRT, quantization FP16/INT8 pour inférence temps réel selon niveau de complexité
+
+**3. Analyse Causale Approfondie**
+1. **Prototypage rapide** : SegFormer-B1 comme référence architecturale moderne pour projets nécessitant performance maximale
+2. **Contraintes embarquées** : FPN+EfficientNet-B0 optimal pour déploiement edge ultra-contraint à 768×768, avec évaluation requise à résolutions supérieures pour validation complète
+3. **Adaptabilité clients** : Gamme SegFormer (B0-B1+) offrant scaling performance/ressources selon besoins spécifiques
+
+### Perspectives de Déploiement Pratique
+
+**Applications industrielles validées :**
+1. **Prototypage rapide** : SegFormer-B1 comme référence architecturale moderne pour projets nécessitant performance maximale
+2. **Contraintes embarquées** : FPN+EfficientNet-B0 optimal pour déploiement edge ultra-contraint à 768×768, avec évaluation requise à résolutions supérieures pour validation complète
+3. **Adaptabilité clients** : Gamme SegFormer (B0-B1+) offrant scaling performance/ressources selon besoins spécifiques
+
+**ROI technique contextuel :**
+Cette preuve de concept valide l'intérêt des architectures Transformer tout en révélant leurs **sensibilités pratiques** (ratio d'image, scaling nécessaire). L'approche constitue une **base méthodologique** solide pour futurs développements production avec contraintes réelles explicitées.
+
+---
+
+## Conclusion Méthodologique
+
+### Contribution Analytique Principale
+
+Cette étude apporte une **perspective complémentaire critique** aux évaluations académiques standard en révélant :
+
+1. **Impact géométrique différentiel** : Sensibilité CNN vs Transformer aux déformations d'image
+2. **Scaling architectural requis** : Nécessité SegFormer-B1+ pour surpasser baselines CNN établies en ratio 1:1
+3. **Trade-offs pratiques** : Compromis performance/efficacité sous contraintes réelles
+
+### Validation des Hypothèses de Recherche
+
+**Hypothèses confirmées :**
+- ✅ **SegFormer-B1 surpasse FPN** (+1.31% mIoU) avec efficacité paramétrique supérieure
+- ✅ **Amélioration classes critiques** validée (Human +1.78%, Vehicle +1.05%)
+- ✅ **Architecture Transformer viable** pour segmentation temps réel avec scaling approprié
+
+**Hypothèses infirmées :**
+- ❌ **SegFormer-B0 sous-performant** vs FPN sur images 1:1
+- ❌ **Robustesse géométrique** Transformer surestimée vs CNN
+
+### Positionnement Scientifique
+
+Cette preuve de concept se distingue par son **réalisme méthodologique** : elle expose les limitations pratiques des architectures d'avant-garde sous contraintes déploiement réelles, offrant une analyse **nuancée et reproductible** aux équipes techniques devant opérer des choix architecturaux informés.
+
L'approche constitue une **base technique solide** pour futurs développements industriels nécessitant des compromis éclairés entre innovation architecturale et contraintes opérationnelles.
\ No newline at end of file
diff --git a/docs/plan_previsionnel.md b/docs/plan_previsionnel.md
index 7446b34..8abc278 100644
--- a/docs/plan_previsionnel.md
+++ b/docs/plan_previsionnel.md
@@ -1,77 +1,77 @@
-# Plan Prévisionnel - Preuve de Concept Segmentation Sémantique
-
-**Projet** : Amélioration de la segmentation sémantique temps réel avec SegFormer
-
----
-
-## Dataset Retenu
-
-**Cityscapes** : Dataset de référence pour la segmentation sémantique de scènes urbaines comprenant 2975 images haute résolution (2048×1024) annotées pixel par pixel. Les images sont capturées dans **50 villes allemandes différentes** avec **34 classes principales** que nous regroupons en **8 catégories pertinentes** (flat, vehicle, human, construction, object, nature, sky, void).
-
-**Particularité méthodologique** : Les images seront testées aux résolutions 512×512 et 768×768 avec **déformation du ratio d'origine** (passage de 2:1 à 1:1) pour optimiser les contraintes computationnelles GPU T4. Cette modification permettra d'analyser l'impact du ratio d'image sur les différentes architectures.
-
----
-
-## Modèle Envisagé
-
-### Algorithme : SegFormer (Focus Temps Réel)
-
-**Justifications du choix :**
-SegFormer (Xie et al., 2021) représente une approche Transformer vision-optimisée pour la segmentation sémantique. **L'analyse du benchmark Cityscapes révèle que SegFormer-B0 égale les performances de DeepLabv3+** (75.3% vs 75.2% mIoU) tout en fonctionnant à **résolution réduite** (côté court 768px vs résolution pleine). Cette efficacité computationnelle supérieure suggère un potentiel d'amélioration face à notre baseline FPN, DeepLabv3+ étant théoriquement plus performant que FPN.
-
-**Objectif et hypothèses de recherche :**
-1. **SegFormer-B0** devrait surpasser FPN+EfficientNetB0 (78.67% mIoU baseline) grâce à l'attention globale native
-2. **SegFormer-B1** permettra d'explorer le scaling architectural (non documenté dans le papier original)
-3. **Impact du ratio d'image** : Analyser comment la déformation 2:1→1:1 affecte les architectures CNN vs Transformer
-
-**Contraintes et adaptations :**
-- Migration framework obligatoire : TensorFlow/Keras → PyTorch pour accès aux modèles SegFormer récents
-- Résolutions contraintes par GPU T4 : 512×512 et 768×768 maximum
-- Protocole différent du papier original : images carrées vs rectangulaires natives
-
----
-
-## Références Bibliographiques
-
-1. **Xie, E., Wang, W., Yu, Z., et al.** (2021). *SegFormer: Simple and Efficient Design for Semantic Segmentation with Transformers*. NeurIPS 2021. [Architecture Transformer de référence]
-
-2. **Chen, L.C., Zhu, Y., Papandreou, G., et al.** (2018). *Encoder-Decoder with Atrous Separable Convolution for Semantic Image Segmentation*. ECCV 2018. [Baseline comparative DeepLabv3+]
-
-3. **Cordts, M., Omran, M., Ramos, S., et al.** (2016). *The Cityscapes Dataset for Semantic Urban Scene Understanding*. CVPR 2016. [Dataset de référence]
-
----
-
-## Explication de la Démarche de Test
-
-### Méthode Baseline
-**FPN + EfficientNetB0/ResNet34** : Réentraînement des modèles du projet précédent aux nouvelles résolutions 512×512 et 768×768 avec déformation du ratio d'image. Performance baseline FPN+EfficientNetB0 : 78.67% mIoU (baseline de référence).
-
-### Méthode de Test SegFormer
-**Évaluation comparative temps réel** :
-1. **SegFormer-B0** : Modèle compact ciblé (3.7M paramètres) pour validation du concept temps réel
-2. **SegFormer-B1** : Exploration scaling architectural (13.7M paramètres) non documenté dans le papier
-3. **Comparaison 512×512 vs 768×768** : Trade-off résolution/performance sur images déformées
-4. **Analyse différentielle** : Impact du ratio d'image 1:1 vs 2:1 natif Cityscapes
-
-**Métriques d'évaluation** :
-- Mean IoU (métrique principale) et accuracy pixel-wise
-- Performance par classe sémantique (focus classes critiques : Human, Vehicle)
-- Temps d'inférence CPU/GPU pour validation déploiement pratique
-
-### Preuve de Concept
-**Interface Streamlit** déployée sur serveur NAS personnel (CPU Intel N100) permettant :
-- Upload d'images urbaines avec **inférence comparative multi-modèles**
-- **Visualisation côte-à-côte** : FPN+ResNet34 vs SegFormer-B1 (meilleurs représentants)
-- Affichage temps d'inférence réel et métriques de performance détaillées
-- **Analyse exploratoire dataset** : Distribution classes, statistiques pixel-wise interactives
-
-**Hypothèses de validation** :
-1. **SegFormer-B1 > FPN baselines** malgré contraintes ratio d'image
-2. **SegFormer-B0 vs FPN** : Révélation possible de limitations Transformer sur images déformées
-3. **Performances générales** potentiellement supérieures au papier original grâce à l'optimisation ratio 1:1
-
-**Code réutilisé** : Framework `segmentation_models_pytorch` pour les implémentations. **Application originale** : protocole d'évaluation sur images carrées et analyse comparative CNN vs Transformer sous contraintes géométriques spécifiques.
-
----
-
+# Plan Prévisionnel - Preuve de Concept Segmentation Sémantique
+
+**Projet** : Amélioration de la segmentation sémantique temps réel avec SegFormer
+
+---
+
+## Dataset Retenu
+
+**Cityscapes** : Dataset de référence pour la segmentation sémantique de scènes urbaines comprenant 2975 images haute résolution (2048×1024) annotées pixel par pixel. Les images sont capturées dans **50 villes allemandes différentes** avec **34 classes principales** que nous regroupons en **8 catégories pertinentes** (flat, vehicle, human, construction, object, nature, sky, void).
+
+**Particularité méthodologique** : Les images seront testées aux résolutions 512×512 et 768×768 avec **déformation du ratio d'origine** (passage de 2:1 à 1:1) pour optimiser les contraintes computationnelles GPU T4. Cette modification permettra d'analyser l'impact du ratio d'image sur les différentes architectures.
+
+---
+
+## Modèle Envisagé
+
+### Algorithme : SegFormer (Focus Temps Réel)
+
+**Justifications du choix :**
+SegFormer (Xie et al., 2021) représente une approche Transformer vision-optimisée pour la segmentation sémantique. **L'analyse du benchmark Cityscapes révèle que SegFormer-B0 égale les performances de DeepLabv3+** (75.3% vs 75.2% mIoU) tout en fonctionnant à **résolution réduite** (côté court 768px vs résolution pleine). Cette efficacité computationnelle supérieure suggère un potentiel d'amélioration face à notre baseline FPN, DeepLabv3+ étant théoriquement plus performant que FPN.
+
+**Objectif et hypothèses de recherche :**
+1. **SegFormer-B0** devrait surpasser FPN+EfficientNetB0 (78.67% mIoU baseline) grâce à l'attention globale native
+2. **SegFormer-B1** permettra d'explorer le scaling architectural (non documenté dans le papier original)
+3. **Impact du ratio d'image** : Analyser comment la déformation 2:1→1:1 affecte les architectures CNN vs Transformer
+
+**Contraintes et adaptations :**
+- Migration framework obligatoire : TensorFlow/Keras → PyTorch pour accès aux modèles SegFormer récents
+- Résolutions contraintes par GPU T4 : 512×512 et 768×768 maximum
+- Protocole différent du papier original : images carrées vs rectangulaires natives
+
+---
+
+## Références Bibliographiques
+
+1. **Xie, E., Wang, W., Yu, Z., et al.** (2021). *SegFormer: Simple and Efficient Design for Semantic Segmentation with Transformers*. NeurIPS 2021. [Architecture Transformer de référence]
+
+2. **Chen, L.C., Zhu, Y., Papandreou, G., et al.** (2018). *Encoder-Decoder with Atrous Separable Convolution for Semantic Image Segmentation*. ECCV 2018. [Baseline comparative DeepLabv3+]
+
+3. **Cordts, M., Omran, M., Ramos, S., et al.** (2016). *The Cityscapes Dataset for Semantic Urban Scene Understanding*. CVPR 2016. [Dataset de référence]
+
+---
+
+## Explication de la Démarche de Test
+
+### Méthode Baseline
+**FPN + EfficientNetB0/ResNet34** : Réentraînement des modèles du projet précédent aux nouvelles résolutions 512×512 et 768×768 avec déformation du ratio d'image. Performance baseline FPN+EfficientNetB0 : 78.67% mIoU (baseline de référence).
+
+### Méthode de Test SegFormer
+**Évaluation comparative temps réel** :
+1. **SegFormer-B0** : Modèle compact ciblé (3.7M paramètres) pour validation du concept temps réel
+2. **SegFormer-B1** : Exploration scaling architectural (13.7M paramètres) non documenté dans le papier
+3. **Comparaison 512×512 vs 768×768** : Trade-off résolution/performance sur images déformées
+4. **Analyse différentielle** : Impact du ratio d'image 1:1 vs 2:1 natif Cityscapes
+
+**Métriques d'évaluation** :
+- Mean IoU (métrique principale) et accuracy pixel-wise
+- Performance par classe sémantique (focus classes critiques : Human, Vehicle)
+- Temps d'inférence CPU/GPU pour validation déploiement pratique
+
+### Preuve de Concept
+**Interface Streamlit** déployée sur serveur NAS personnel (CPU Intel N100) permettant :
+- Upload d'images urbaines avec **inférence comparative multi-modèles**
+- **Visualisation côte-à-côte** : FPN+ResNet34 vs SegFormer-B1 (meilleurs représentants)
+- Affichage temps d'inférence réel et métriques de performance détaillées
+- **Analyse exploratoire dataset** : Distribution classes, statistiques pixel-wise interactives
+
+**Hypothèses de validation** :
+1. **SegFormer-B1 > FPN baselines** malgré contraintes ratio d'image
+2. **SegFormer-B0 vs FPN** : Révélation possible de limitations Transformer sur images déformées
+3. **Performances générales** potentiellement supérieures au papier original grâce à l'optimisation ratio 1:1
+
+**Code réutilisé** : Framework `segmentation_models_pytorch` pour les implémentations. **Application originale** : protocole d'évaluation sur images carrées et analyse comparative CNN vs Transformer sous contraintes géométriques spécifiques.
+
+---
+
**Note méthodologique** : Cette approche permet d'analyser les trade-offs pratiques architecture/performance dans un contexte de contraintes réelles (hardware limité, déformation d'images), offrant une perspective complémentaire aux évaluations académiques standard.
\ No newline at end of file
diff --git a/nginx.conf b/nginx.conf
index 6482d58..129d895 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -1,57 +1,57 @@
-# This reverse proxy configuration aims to make calls to different services running under different ports (Streamlit :8501 and FastAPI :8000) by calling only one port : the nginx one (configurable via NGINX_PORT)
-# The NGINX_PORT has to be opened (Dockerfile) and used by app.py (streamlit) in order to make right calls.
-worker_processes 1;
-
-events { worker_connections 1024; }
-
-http {
- # ✅ AJOUT: Limite d'upload augmentée
- client_max_body_size 200M; # Permet uploads jusqu'à 200MB
-
- server {
- listen ${NGINX_PORT};
-
- # ✅ AJOUT: Timeouts pour gros fichiers
- client_body_timeout 60s;
- client_header_timeout 60s;
- keepalive_timeout 60s;
- send_timeout 60s;
-
- location /api/ {
- proxy_pass http://127.0.0.1:8000/;
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
-
- # ✅ AJOUT: Timeouts pour uploads
- proxy_read_timeout 300s;
- proxy_connect_timeout 75s;
- proxy_send_timeout 300s;
- }
-
- location / {
- proxy_pass http://127.0.0.1:8501/;
-
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
-
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
-
- # ✅ AJOUT: Timeouts pour Streamlit uploads
- proxy_read_timeout 300s;
- proxy_connect_timeout 75s;
- proxy_send_timeout 300s;
- }
-
- location /health {
- return 200 'OK';
- add_header Content-Type text/plain;
- }
- }
-}
-
+# This reverse proxy configuration aims to make calls to different services running under different ports (Streamlit :8501 and FastAPI :8000) by calling only one port : the nginx one (configurable via NGINX_PORT)
+# The NGINX_PORT has to be opened (Dockerfile) and used by app.py (streamlit) in order to make right calls.
+worker_processes 1;
+
+events { worker_connections 1024; }
+
+http {
+ # ✅ AJOUT: Limite d'upload augmentée
+ client_max_body_size 200M; # Permet uploads jusqu'à 200MB
+
+ server {
+ listen ${NGINX_PORT};
+
+ # ✅ AJOUT: Timeouts pour gros fichiers
+ client_body_timeout 60s;
+ client_header_timeout 60s;
+ keepalive_timeout 60s;
+ send_timeout 60s;
+
+ location /api/ {
+ proxy_pass http://127.0.0.1:8000/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # ✅ AJOUT: Timeouts pour uploads
+ proxy_read_timeout 300s;
+ proxy_connect_timeout 75s;
+ proxy_send_timeout 300s;
+ }
+
+ location / {
+ proxy_pass http://127.0.0.1:8501/;
+
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # ✅ AJOUT: Timeouts pour Streamlit uploads
+ proxy_read_timeout 300s;
+ proxy_connect_timeout 75s;
+ proxy_send_timeout 300s;
+ }
+
+ location /health {
+ return 200 'OK';
+ add_header Content-Type text/plain;
+ }
+ }
+}
+
diff --git a/notebooks/requirements.txt b/notebooks/requirements.txt
index 5bb3c2f..28066ac 100644
--- a/notebooks/requirements.txt
+++ b/notebooks/requirements.txt
@@ -1,671 +1,671 @@
-absl-py==1.4.0
-absolufy-imports==0.3.1
-accelerate==1.10.1
-aiofiles==24.1.0
-aiohappyeyeballs==2.6.1
-aiohttp==3.12.15
-aiosignal==1.4.0
-alabaster==1.0.0
-albucore==0.0.24
-albumentations==2.0.8
-ale-py==0.11.2
-alembic==1.16.5
-altair==5.5.0
-annotated-types==0.7.0
-antlr4-python3-runtime==4.9.3
-anyio==4.10.0
-anywidget==0.9.18
-argon2-cffi==25.1.0
-argon2-cffi-bindings==25.1.0
-array_record==0.8.1
-arviz==0.22.0
-astropy==7.1.0
-astropy-iers-data==0.2025.8.25.0.36.58
-astunparse==1.6.3
-atpublic==5.1
-attrs==25.3.0
-audioread==3.0.1
-Authlib==1.6.2
-autograd==1.8.0
-babel==2.17.0
-backcall==0.2.0
-beartype==0.21.0
-beautifulsoup4==4.13.5
-betterproto==2.0.0b6
-bigframes==2.17.0
-bigquery-magics==0.10.3
-bleach==6.2.0
-blinker==1.9.0
-blis==1.3.0
-blobfile==3.0.0
-blosc2==3.7.2
-bokeh==3.7.3
-boto3==1.40.21
-botocore==1.40.21
-Bottleneck==1.4.2
-bqplot==0.12.45
-branca==0.8.1
-Brotli==1.1.0
-build==1.3.0
-CacheControl==0.14.3
-cachetools==5.5.2
-catalogue==2.0.10
-certifi==2025.8.3
-cffi==1.17.1
-chardet==5.2.0
-charset-normalizer==3.4.3
-chex==0.1.90
-clarabel==0.11.1
-click==8.2.1
-cloudpathlib==0.21.1
-cloudpickle==3.1.1
-cmake==3.31.6
-cmdstanpy==1.2.5
-colorcet==3.1.0
-colorlover==0.3.0
-colour==0.1.5
-community==1.0.0b1
-confection==0.1.5
-cons==0.4.7
-contourpy==1.3.3
-cramjam==2.11.0
-cryptography==43.0.3
-cuda-python==12.6.2.post1
-cudf-cu12 @ https://pypi.nvidia.com/cudf-cu12/cudf_cu12-25.6.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
-cudf-polars-cu12==25.6.0
-cufflinks==0.17.3
-cuml-cu12==25.6.0
-cupy-cuda12x==13.3.0
-curl_cffi==0.13.0
-cuvs-cu12==25.6.1
-cvxopt==1.3.2
-cvxpy==1.6.7
-cycler==0.12.1
-cyipopt==1.5.0
-cymem==2.0.11
-Cython==3.0.12
-dask==2025.5.0
-dask-cuda==25.6.0
-dask-cudf-cu12==25.6.0
-databricks-sdk==0.64.0
-dataproc-spark-connect==0.8.3
-datasets==4.0.0
-db-dtypes==1.4.3
-dbus-python==1.2.18
-debugpy==1.8.15
-decorator==4.4.2
-defusedxml==0.7.1
-diffusers==0.35.1
-dill==0.3.8
-distributed==2025.5.0
-distributed-ucxx-cu12==0.44.0
-distro==1.9.0
-dlib==19.24.6
-dm-tree==0.1.9
-docker==7.1.0
-docstring_parser==0.17.0
-docutils==0.21.2
-dopamine_rl==4.1.2
-dotenv==0.9.9
-duckdb==1.3.2
-earthengine-api==1.5.24
-easydict==1.13
-editdistance==0.8.1
-eerepr==0.1.2
-einops==0.8.1
-en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85
-entrypoints==0.4
-et_xmlfile==2.0.0
-etils==1.13.0
-etuples==0.3.10
-Farama-Notifications==0.0.4
-fastai==2.8.4
-fastapi==0.116.1
-fastcore==1.8.8
-fastdownload==0.0.7
-fastjsonschema==2.21.2
-fastprogress==1.0.3
-fastrlock==0.8.3
-fasttransform==0.0.2
-ffmpy==0.6.1
-filelock==3.19.1
-firebase-admin==6.9.0
-Flask==3.1.2
-flatbuffers==25.2.10
-flax==0.10.6
-folium==0.20.0
-fonttools==4.59.1
-frozendict==2.4.6
-frozenlist==1.7.0
-fsspec==2025.3.0
-future==1.0.0
-gast==0.6.0
-gcsfs==2025.3.0
-GDAL==3.8.4
-gdown==5.2.0
-geemap==0.35.3
-geocoder==1.38.1
-geographiclib==2.1
-geopandas==1.1.1
-geopy==2.4.1
-gin-config==0.5.0
-gitdb==4.0.12
-GitPython==3.1.45
-glob2==0.7
-google==2.0.3
-google-adk==1.12.0
-google-ai-generativelanguage==0.6.15
-google-api-core==2.25.1
-google-api-python-client==2.179.0
-google-auth==2.38.0
-google-auth-httplib2==0.2.0
-google-auth-oauthlib==1.2.2
-google-cloud-aiplatform==1.110.0
-google-cloud-appengine-logging==1.6.2
-google-cloud-audit-log==0.3.2
-google-cloud-bigquery==3.36.0
-google-cloud-bigquery-connection==1.18.3
-google-cloud-bigquery-storage==2.32.0
-google-cloud-bigtable==2.32.0
-google-cloud-core==2.4.3
-google-cloud-dataproc==5.21.0
-google-cloud-datastore==2.21.0
-google-cloud-firestore==2.21.0
-google-cloud-functions==1.20.4
-google-cloud-language==2.17.2
-google-cloud-logging==3.12.1
-google-cloud-resource-manager==1.14.2
-google-cloud-secret-manager==2.24.0
-google-cloud-spanner==3.57.0
-google-cloud-speech==2.33.0
-google-cloud-storage==2.19.0
-google-cloud-trace==1.16.2
-google-cloud-translate==3.21.1
-google-colab @ file:///colabtools/dist/google_colab-1.0.0.tar.gz
-google-crc32c==1.7.1
-google-genai==1.31.0
-google-generativeai==0.8.5
-google-pasta==0.2.0
-google-resumable-media==2.7.2
-googleapis-common-protos==1.70.0
-googledrivedownloader==1.1.0
-gradio==5.43.1
-gradio_client==1.12.1
-graphene==3.4.3
-graphql-core==3.2.6
-graphql-relay==3.2.0
-graphviz==0.21
-greenlet==3.2.4
-groovy==0.1.2
-grpc-google-iam-v1==0.14.2
-grpc-interceptor==0.15.4
-grpcio==1.74.0
-grpcio-status==1.71.2
-grpclib==0.4.8
-gspread==6.2.1
-gspread-dataframe==4.0.0
-gunicorn==23.0.0
-gym==0.25.2
-gym-notices==0.1.0
-gymnasium==1.2.0
-h11==0.16.0
-h2==4.3.0
-h5netcdf==1.6.4
-h5py==3.14.0
-hdbscan==0.8.40
-hf-xet==1.1.8
-hf_transfer==0.1.9
-highspy==1.11.0
-holidays==0.79
-holoviews==1.21.0
-hpack==4.1.0
-html5lib==1.1
-httpcore==1.0.9
-httpimport==1.4.1
-httplib2==0.22.0
-httpx==0.28.1
-httpx-sse==0.4.1
-huggingface-hub==0.34.4
-humanize==4.13.0
-hyperframe==6.1.0
-hyperopt==0.2.7
-ibis-framework==9.5.0
-idna==3.10
-imageio==2.37.0
-imageio-ffmpeg==0.6.0
-imagesize==1.4.1
-imbalanced-learn==0.14.0
-immutabledict==4.2.1
-importlib_metadata==8.7.0
-importlib_resources==6.5.2
-imutils==0.5.4
-inflect==7.5.0
-iniconfig==2.1.0
-intel-cmplr-lib-ur==2025.2.1
-intel-openmp==2025.2.1
-ipyevents==2.0.2
-ipyfilechooser==0.6.0
-ipykernel==6.17.1
-ipyleaflet==0.20.0
-ipyparallel==8.8.0
-ipython==7.34.0
-ipython-genutils==0.2.0
-ipython-sql==0.5.0
-ipytree==0.2.2
-ipywidgets==7.7.1
-itsdangerous==2.2.0
-jaraco.classes==3.4.0
-jaraco.context==6.0.1
-jaraco.functools==4.3.0
-jax==0.5.3
-jax-cuda12-pjrt==0.5.3
-jax-cuda12-plugin==0.5.3
-jaxlib==0.5.3
-jeepney==0.9.0
-jieba==0.42.1
-Jinja2==3.1.6
-jiter==0.10.0
-jmespath==1.0.1
-joblib==1.5.1
-jsonpatch==1.33
-jsonpickle==4.1.1
-jsonpointer==3.0.0
-jsonschema==4.25.1
-jsonschema-specifications==2025.4.1
-jupyter-client==6.1.12
-jupyter-console==6.1.0
-jupyter-leaflet==0.20.0
-jupyter-server==1.16.0
-jupyter_core==5.8.1
-jupyter_kernel_gateway @ git+https://github.com/googlecolab/kernel_gateway@b134e9945df25c2dcb98ade9129399be10788671
-jupyterlab_pygments==0.3.0
-jupyterlab_widgets==3.0.15
-jupytext==1.17.2
-kaggle==1.7.4.5
-kagglehub==0.3.12
-keras==3.10.0
-keras-hub==0.21.1
-keras-nlp==0.21.1
-keyring==25.6.0
-keyrings.google-artifactregistry-auth==1.1.2
-kiwisolver==1.4.9
-langchain==0.3.27
-langchain-core==0.3.74
-langchain-text-splitters==0.3.9
-langcodes==3.5.0
-langsmith==0.4.16
-language_data==1.3.0
-launchpadlib==1.10.16
-lazr.restfulclient==0.14.4
-lazr.uri==1.0.6
-lazy_loader==0.4
-libclang==18.1.1
-libcudf-cu12 @ https://pypi.nvidia.com/libcudf-cu12/libcudf_cu12-25.6.0-py3-none-manylinux_2_28_x86_64.whl
-libcugraph-cu12==25.6.0
-libcuml-cu12==25.6.0
-libcuvs-cu12==25.6.1
-libkvikio-cu12==25.6.0
-libpysal==4.13.0
-libraft-cu12==25.6.0
-librmm-cu12==25.6.0
-librosa==0.11.0
-libucx-cu12==1.18.1
-libucxx-cu12==0.44.0
-lightgbm @ file:///tmp/lightgbm/LightGBM/dist/lightgbm-4.6.0-py3-none-linux_x86_64.whl
-linkify-it-py==2.0.3
-llvmlite==0.43.0
-locket==1.0.0
-logical-unification==0.4.6
-lxml==5.4.0
-Mako==1.1.3
-marisa-trie==1.3.0
-Markdown==3.8.2
-markdown-it-py==4.0.0
-MarkupSafe==3.0.2
-matplotlib==3.10.0
-matplotlib-inline==0.1.7
-matplotlib-venn==1.1.2
-mcp==1.13.1
-mdit-py-plugins==0.5.0
-mdurl==0.1.2
-miniKanren==1.0.5
-missingno==0.5.2
-mistune==3.1.3
-mizani==0.13.5
-mkl==2025.2.0
-ml_dtypes==0.5.3
-mlflow==3.3.2
-mlflow-skinny==3.3.2
-mlflow-tracing==3.3.2
-mlxtend==0.23.4
-more-itertools==10.7.0
-moviepy==1.0.3
-mpmath==1.3.0
-msgpack==1.1.1
-multidict==6.6.4
-multipledispatch==1.0.0
-multiprocess==0.70.16
-multitasking==0.0.12
-murmurhash==1.0.13
-music21==9.3.0
-namex==0.1.0
-narwhals==2.2.0
-natsort==8.4.0
-nbclassic==1.3.1
-nbclient==0.10.2
-nbconvert==7.16.6
-nbformat==5.10.4
-ndindex==1.10.0
-nest-asyncio==1.6.0
-networkx==3.5
-nibabel==5.3.2
-nltk==3.9.1
-notebook==6.5.7
-notebook_shim==0.2.4
-numba==0.60.0
-numba-cuda==0.11.0
-numexpr==2.11.0
-numpy==2.0.2
-nvidia-cublas-cu12==12.6.4.1
-nvidia-cuda-cupti-cu12==12.6.80
-nvidia-cuda-nvcc-cu12==12.5.82
-nvidia-cuda-nvrtc-cu12==12.6.77
-nvidia-cuda-runtime-cu12==12.6.77
-nvidia-cudnn-cu12==9.10.2.21
-nvidia-cufft-cu12==11.3.0.4
-nvidia-cufile-cu12==1.11.1.6
-nvidia-curand-cu12==10.3.7.77
-nvidia-cusolver-cu12==11.7.1.2
-nvidia-cusparse-cu12==12.5.4.2
-nvidia-cusparselt-cu12==0.7.1
-nvidia-ml-py==12.575.51
-nvidia-nccl-cu12==2.27.3
-nvidia-nvjitlink-cu12==12.6.85
-nvidia-nvtx-cu12==12.6.77
-nvtx==0.2.13
-nx-cugraph-cu12 @ https://pypi.nvidia.com/nx-cugraph-cu12/nx_cugraph_cu12-25.6.0-py3-none-any.whl
-oauth2client==4.1.3
-oauthlib==3.3.1
-omegaconf==2.3.0
-openai==1.101.0
-opencv-contrib-python==4.12.0.88
-opencv-python==4.12.0.88
-opencv-python-headless==4.12.0.88
-openpyxl==3.1.5
-opentelemetry-api==1.36.0
-opentelemetry-exporter-gcp-trace==1.9.0
-opentelemetry-resourcedetector-gcp==1.9.0a0
-opentelemetry-sdk==1.36.0
-opentelemetry-semantic-conventions==0.57b0
-opt_einsum==3.4.0
-optax==0.2.5
-optree==0.17.0
-orbax-checkpoint==0.11.23
-orjson==3.11.2
-osqp==1.0.4
-packaging==25.0
-pandas==2.2.2
-pandas-datareader==0.10.0
-pandas-gbq==0.29.2
-pandas-stubs==2.2.2.240909
-pandocfilters==1.5.1
-panel==1.7.5
-param==2.2.1
-parso==0.8.5
-parsy==2.1
-partd==1.4.2
-patsy==1.0.1
-peewee==3.18.2
-peft==0.17.1
-pexpect==4.9.0
-pickleshare==0.7.5
-pillow==11.3.0
-platformdirs==4.3.8
-plotly==5.24.1
-plotnine==0.14.5
-pluggy==1.6.0
-plum-dispatch==2.5.7
-ply==3.11
-polars==1.25.2
-pooch==1.8.2
-portpicker==1.5.2
-preshed==3.0.10
-prettytable==3.16.0
-proglog==0.1.12
-progressbar2==4.5.0
-prometheus_client==0.22.1
-promise==2.3
-prompt_toolkit==3.0.51
-propcache==0.3.2
-prophet==1.1.7
-proto-plus==1.26.1
-protobuf==5.29.5
-psutil==5.9.5
-psycopg2==2.9.10
-psygnal==0.14.1
-ptyprocess==0.7.0
-py-cpuinfo==9.0.0
-py4j==0.10.9.7
-pyarrow==18.1.0
-pyasn1==0.6.1
-pyasn1_modules==0.4.2
-pycairo==1.28.0
-pycocotools==2.0.10
-pycparser==2.22
-pycryptodomex==3.23.0
-pydantic==2.11.7
-pydantic-settings==2.10.1
-pydantic_core==2.33.2
-pydata-google-auth==1.9.1
-pydot==3.0.4
-pydotplus==2.0.2
-PyDrive2==1.21.3
-pydub==0.25.1
-pyerfa==2.0.1.5
-pygame==2.6.1
-pygit2==1.18.2
-Pygments==2.19.2
-PyGObject==3.42.0
-PyJWT==2.10.1
-pylibcudf-cu12 @ https://pypi.nvidia.com/pylibcudf-cu12/pylibcudf_cu12-25.6.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
-pylibcugraph-cu12==25.6.0
-pylibraft-cu12==25.6.0
-pymc==5.25.1
-pynndescent==0.5.13
-pynvjitlink-cu12==0.7.0
-pynvml==12.0.0
-pyogrio==0.11.1
-pyomo==6.9.3
-PyOpenGL==3.1.10
-pyOpenSSL==24.2.1
-pyparsing==3.2.3
-pyperclip==1.9.0
-pyproj==3.7.2
-pyproject_hooks==1.2.0
-pyshp==2.3.1
-PySocks==1.7.1
-pyspark==3.5.1
-pytensor==2.31.7
-pytest==8.4.1
-python-apt==0.0.0
-python-box==7.3.2
-python-dateutil==2.9.0.post0
-python-dotenv==1.1.1
-python-louvain==0.16
-python-multipart==0.0.20
-python-slugify==8.0.4
-python-snappy==0.7.3
-python-utils==3.9.1
-pytz==2025.2
-pyviz_comms==3.0.6
-PyWavelets==1.9.0
-PyYAML==6.0.2
-pyzmq==26.2.1
-raft-dask-cu12==25.6.0
-rapids-dask-dependency==25.6.0
-rapids-logger==0.1.1
-ratelim==0.1.6
-referencing==0.36.2
-regex==2024.11.6
-requests==2.32.4
-requests-oauthlib==2.0.0
-requests-toolbelt==1.0.0
-requirements-parser==0.9.0
-rich==13.9.4
-rmm-cu12==25.6.0
-roman-numerals-py==3.1.0
-rpds-py==0.27.0
-rpy2==3.5.17
-rsa==4.9.1
-ruff==0.12.10
-s3transfer==0.13.1
-safehttpx==0.1.6
-safetensors==0.6.2
-scikit-image==0.25.2
-scikit-learn==1.6.1
-scipy==1.16.1
-scooby==0.10.1
-scs==3.2.8
-seaborn==0.13.2
-SecretStorage==3.3.3
-segmentation_models_pytorch==0.5.0
-semantic-version==2.10.0
-Send2Trash==1.8.3
-sentence-transformers==5.1.0
-sentencepiece==0.2.1
-sentry-sdk==2.35.0
-setuptools==75.2.0
-shap==0.48.0
-shapely==2.1.1
-shellingham==1.5.4
-simple-parsing==0.1.7
-simplejson==3.20.1
-simsimd==6.5.1
-six==1.17.0
-sklearn-pandas==2.2.0
-slicer==0.0.8
-smart_open==7.3.0.post1
-smmap==5.0.2
-sniffio==1.3.1
-snowballstemmer==3.0.1
-sortedcontainers==2.4.0
-soundfile==0.13.1
-soupsieve==2.7
-soxr==0.5.0.post1
-spacy==3.8.7
-spacy-legacy==3.0.12
-spacy-loggers==1.0.5
-spanner-graph-notebook==1.1.7
-Sphinx==8.2.3
-sphinxcontrib-applehelp==2.0.0
-sphinxcontrib-devhelp==2.0.0
-sphinxcontrib-htmlhelp==2.1.0
-sphinxcontrib-jsmath==1.0.1
-sphinxcontrib-qthelp==2.0.0
-sphinxcontrib-serializinghtml==2.0.0
-SQLAlchemy==2.0.43
-sqlglot==25.20.2
-sqlparse==0.5.3
-srsly==2.5.1
-sse-starlette==3.0.2
-stanio==0.5.1
-starlette==0.47.3
-statsmodels==0.14.5
-stringzilla==3.12.6
-stumpy==1.13.0
-sympy==1.13.3
-tables==3.10.2
-tabulate==0.9.0
-tbb==2022.2.0
-tblib==3.1.0
-tcmlib==1.4.0
-tenacity==8.5.0
-tensorboard==2.19.0
-tensorboard-data-server==0.7.2
-tensorflow==2.19.0
-tensorflow-datasets==4.9.9
-tensorflow-hub==0.16.1
-tensorflow-metadata==1.17.2
-tensorflow-probability==0.25.0
-tensorflow-text==2.19.0
-tensorflow_decision_forests==1.12.0
-tensorstore==0.1.76
-termcolor==3.1.0
-terminado==0.18.1
-text-unidecode==1.3
-textblob==0.19.0
-tf-slim==1.1.0
-tf_keras==2.19.0
-thinc==8.3.6
-threadpoolctl==3.6.0
-tifffile==2025.6.11
-tiktoken==0.11.0
-timm==1.0.19
-tinycss2==1.4.0
-tokenizers==0.21.4
-toml==0.10.2
-tomlkit==0.13.3
-toolz==0.12.1
-torch==2.8.0
-torchao==0.10.0
-torchaudio==2.8.0
-torchdata==0.11.0
-torchsummary==1.5.1
-torchtune==0.6.1
-torchvision==0.23.0
-tornado==6.4.2
-tqdm==4.67.1
-traitlets==5.7.1
-traittypes==0.2.1
-transformers==4.55.4
-treelite==4.4.1
-treescope==0.1.10
-triton==3.4.0
-tsfresh==0.21.0
-tweepy==4.16.0
-typeguard==4.4.4
-typer==0.16.1
-types-pytz==2025.2.0.20250809
-types-setuptools==80.9.0.20250822
-typing-inspection==0.4.1
-typing_extensions==4.15.0
-tzdata==2025.2
-tzlocal==5.3.1
-uc-micro-py==1.0.3
-ucx-py-cu12==0.44.0
-ucxx-cu12==0.44.0
-umap-learn==0.5.9.post2
-umf==0.11.0
-uritemplate==4.2.0
-urllib3==2.5.0
-uvicorn==0.35.0
-vega-datasets==0.9.0
-wadllib==1.3.6
-wandb==0.21.1
-wasabi==1.1.3
-watchdog==6.0.0
-wcwidth==0.2.13
-weasel==0.4.1
-webcolors==24.11.1
-webencodings==0.5.1
-websocket-client==1.8.0
-websockets==15.0.1
-Werkzeug==3.1.3
-wheel==0.45.1
-widgetsnbextension==3.6.10
-wordcloud==1.9.4
-wrapt==1.17.3
-wurlitzer==3.1.1
-xarray==2025.8.0
-xarray-einstats==0.9.1
-xgboost==3.0.4
-xlrd==2.0.2
-xxhash==3.5.0
-xyzservices==2025.4.0
-yarl==1.20.1
-ydf==0.13.0
-yellowbrick==1.5
-yfinance==0.2.65
-zict==3.0.0
-zipp==3.23.0
-zstandard==0.24.0
+absl-py==1.4.0
+absolufy-imports==0.3.1
+accelerate==1.10.1
+aiofiles==24.1.0
+aiohappyeyeballs==2.6.1
+aiohttp==3.12.15
+aiosignal==1.4.0
+alabaster==1.0.0
+albucore==0.0.24
+albumentations==2.0.8
+ale-py==0.11.2
+alembic==1.16.5
+altair==5.5.0
+annotated-types==0.7.0
+antlr4-python3-runtime==4.9.3
+anyio==4.10.0
+anywidget==0.9.18
+argon2-cffi==25.1.0
+argon2-cffi-bindings==25.1.0
+array_record==0.8.1
+arviz==0.22.0
+astropy==7.1.0
+astropy-iers-data==0.2025.8.25.0.36.58
+astunparse==1.6.3
+atpublic==5.1
+attrs==25.3.0
+audioread==3.0.1
+Authlib==1.6.2
+autograd==1.8.0
+babel==2.17.0
+backcall==0.2.0
+beartype==0.21.0
+beautifulsoup4==4.13.5
+betterproto==2.0.0b6
+bigframes==2.17.0
+bigquery-magics==0.10.3
+bleach==6.2.0
+blinker==1.9.0
+blis==1.3.0
+blobfile==3.0.0
+blosc2==3.7.2
+bokeh==3.7.3
+boto3==1.40.21
+botocore==1.40.21
+Bottleneck==1.4.2
+bqplot==0.12.45
+branca==0.8.1
+Brotli==1.1.0
+build==1.3.0
+CacheControl==0.14.3
+cachetools==5.5.2
+catalogue==2.0.10
+certifi==2025.8.3
+cffi==1.17.1
+chardet==5.2.0
+charset-normalizer==3.4.3
+chex==0.1.90
+clarabel==0.11.1
+click==8.2.1
+cloudpathlib==0.21.1
+cloudpickle==3.1.1
+cmake==3.31.6
+cmdstanpy==1.2.5
+colorcet==3.1.0
+colorlover==0.3.0
+colour==0.1.5
+community==1.0.0b1
+confection==0.1.5
+cons==0.4.7
+contourpy==1.3.3
+cramjam==2.11.0
+cryptography==43.0.3
+cuda-python==12.6.2.post1
+cudf-cu12 @ https://pypi.nvidia.com/cudf-cu12/cudf_cu12-25.6.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
+cudf-polars-cu12==25.6.0
+cufflinks==0.17.3
+cuml-cu12==25.6.0
+cupy-cuda12x==13.3.0
+curl_cffi==0.13.0
+cuvs-cu12==25.6.1
+cvxopt==1.3.2
+cvxpy==1.6.7
+cycler==0.12.1
+cyipopt==1.5.0
+cymem==2.0.11
+Cython==3.0.12
+dask==2025.5.0
+dask-cuda==25.6.0
+dask-cudf-cu12==25.6.0
+databricks-sdk==0.64.0
+dataproc-spark-connect==0.8.3
+datasets==4.0.0
+db-dtypes==1.4.3
+dbus-python==1.2.18
+debugpy==1.8.15
+decorator==4.4.2
+defusedxml==0.7.1
+diffusers==0.35.1
+dill==0.3.8
+distributed==2025.5.0
+distributed-ucxx-cu12==0.44.0
+distro==1.9.0
+dlib==19.24.6
+dm-tree==0.1.9
+docker==7.1.0
+docstring_parser==0.17.0
+docutils==0.21.2
+dopamine_rl==4.1.2
+dotenv==0.9.9
+duckdb==1.3.2
+earthengine-api==1.5.24
+easydict==1.13
+editdistance==0.8.1
+eerepr==0.1.2
+einops==0.8.1
+en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85
+entrypoints==0.4
+et_xmlfile==2.0.0
+etils==1.13.0
+etuples==0.3.10
+Farama-Notifications==0.0.4
+fastai==2.8.4
+fastapi==0.116.1
+fastcore==1.8.8
+fastdownload==0.0.7
+fastjsonschema==2.21.2
+fastprogress==1.0.3
+fastrlock==0.8.3
+fasttransform==0.0.2
+ffmpy==0.6.1
+filelock==3.19.1
+firebase-admin==6.9.0
+Flask==3.1.2
+flatbuffers==25.2.10
+flax==0.10.6
+folium==0.20.0
+fonttools==4.59.1
+frozendict==2.4.6
+frozenlist==1.7.0
+fsspec==2025.3.0
+future==1.0.0
+gast==0.6.0
+gcsfs==2025.3.0
+GDAL==3.8.4
+gdown==5.2.0
+geemap==0.35.3
+geocoder==1.38.1
+geographiclib==2.1
+geopandas==1.1.1
+geopy==2.4.1
+gin-config==0.5.0
+gitdb==4.0.12
+GitPython==3.1.45
+glob2==0.7
+google==2.0.3
+google-adk==1.12.0
+google-ai-generativelanguage==0.6.15
+google-api-core==2.25.1
+google-api-python-client==2.179.0
+google-auth==2.38.0
+google-auth-httplib2==0.2.0
+google-auth-oauthlib==1.2.2
+google-cloud-aiplatform==1.110.0
+google-cloud-appengine-logging==1.6.2
+google-cloud-audit-log==0.3.2
+google-cloud-bigquery==3.36.0
+google-cloud-bigquery-connection==1.18.3
+google-cloud-bigquery-storage==2.32.0
+google-cloud-bigtable==2.32.0
+google-cloud-core==2.4.3
+google-cloud-dataproc==5.21.0
+google-cloud-datastore==2.21.0
+google-cloud-firestore==2.21.0
+google-cloud-functions==1.20.4
+google-cloud-language==2.17.2
+google-cloud-logging==3.12.1
+google-cloud-resource-manager==1.14.2
+google-cloud-secret-manager==2.24.0
+google-cloud-spanner==3.57.0
+google-cloud-speech==2.33.0
+google-cloud-storage==2.19.0
+google-cloud-trace==1.16.2
+google-cloud-translate==3.21.1
+google-colab @ file:///colabtools/dist/google_colab-1.0.0.tar.gz
+google-crc32c==1.7.1
+google-genai==1.31.0
+google-generativeai==0.8.5
+google-pasta==0.2.0
+google-resumable-media==2.7.2
+googleapis-common-protos==1.70.0
+googledrivedownloader==1.1.0
+gradio==5.43.1
+gradio_client==1.12.1
+graphene==3.4.3
+graphql-core==3.2.6
+graphql-relay==3.2.0
+graphviz==0.21
+greenlet==3.2.4
+groovy==0.1.2
+grpc-google-iam-v1==0.14.2
+grpc-interceptor==0.15.4
+grpcio==1.74.0
+grpcio-status==1.71.2
+grpclib==0.4.8
+gspread==6.2.1
+gspread-dataframe==4.0.0
+gunicorn==23.0.0
+gym==0.25.2
+gym-notices==0.1.0
+gymnasium==1.2.0
+h11==0.16.0
+h2==4.3.0
+h5netcdf==1.6.4
+h5py==3.14.0
+hdbscan==0.8.40
+hf-xet==1.1.8
+hf_transfer==0.1.9
+highspy==1.11.0
+holidays==0.79
+holoviews==1.21.0
+hpack==4.1.0
+html5lib==1.1
+httpcore==1.0.9
+httpimport==1.4.1
+httplib2==0.22.0
+httpx==0.28.1
+httpx-sse==0.4.1
+huggingface-hub==0.34.4
+humanize==4.13.0
+hyperframe==6.1.0
+hyperopt==0.2.7
+ibis-framework==9.5.0
+idna==3.10
+imageio==2.37.0
+imageio-ffmpeg==0.6.0
+imagesize==1.4.1
+imbalanced-learn==0.14.0
+immutabledict==4.2.1
+importlib_metadata==8.7.0
+importlib_resources==6.5.2
+imutils==0.5.4
+inflect==7.5.0
+iniconfig==2.1.0
+intel-cmplr-lib-ur==2025.2.1
+intel-openmp==2025.2.1
+ipyevents==2.0.2
+ipyfilechooser==0.6.0
+ipykernel==6.17.1
+ipyleaflet==0.20.0
+ipyparallel==8.8.0
+ipython==7.34.0
+ipython-genutils==0.2.0
+ipython-sql==0.5.0
+ipytree==0.2.2
+ipywidgets==7.7.1
+itsdangerous==2.2.0
+jaraco.classes==3.4.0
+jaraco.context==6.0.1
+jaraco.functools==4.3.0
+jax==0.5.3
+jax-cuda12-pjrt==0.5.3
+jax-cuda12-plugin==0.5.3
+jaxlib==0.5.3
+jeepney==0.9.0
+jieba==0.42.1
+Jinja2==3.1.6
+jiter==0.10.0
+jmespath==1.0.1
+joblib==1.5.1
+jsonpatch==1.33
+jsonpickle==4.1.1
+jsonpointer==3.0.0
+jsonschema==4.25.1
+jsonschema-specifications==2025.4.1
+jupyter-client==6.1.12
+jupyter-console==6.1.0
+jupyter-leaflet==0.20.0
+jupyter-server==1.16.0
+jupyter_core==5.8.1
+jupyter_kernel_gateway @ git+https://github.com/googlecolab/kernel_gateway@b134e9945df25c2dcb98ade9129399be10788671
+jupyterlab_pygments==0.3.0
+jupyterlab_widgets==3.0.15
+jupytext==1.17.2
+kaggle==1.7.4.5
+kagglehub==0.3.12
+keras==3.10.0
+keras-hub==0.21.1
+keras-nlp==0.21.1
+keyring==25.6.0
+keyrings.google-artifactregistry-auth==1.1.2
+kiwisolver==1.4.9
+langchain==0.3.27
+langchain-core==0.3.74
+langchain-text-splitters==0.3.9
+langcodes==3.5.0
+langsmith==0.4.16
+language_data==1.3.0
+launchpadlib==1.10.16
+lazr.restfulclient==0.14.4
+lazr.uri==1.0.6
+lazy_loader==0.4
+libclang==18.1.1
+libcudf-cu12 @ https://pypi.nvidia.com/libcudf-cu12/libcudf_cu12-25.6.0-py3-none-manylinux_2_28_x86_64.whl
+libcugraph-cu12==25.6.0
+libcuml-cu12==25.6.0
+libcuvs-cu12==25.6.1
+libkvikio-cu12==25.6.0
+libpysal==4.13.0
+libraft-cu12==25.6.0
+librmm-cu12==25.6.0
+librosa==0.11.0
+libucx-cu12==1.18.1
+libucxx-cu12==0.44.0
+lightgbm @ file:///tmp/lightgbm/LightGBM/dist/lightgbm-4.6.0-py3-none-linux_x86_64.whl
+linkify-it-py==2.0.3
+llvmlite==0.43.0
+locket==1.0.0
+logical-unification==0.4.6
+lxml==5.4.0
+Mako==1.1.3
+marisa-trie==1.3.0
+Markdown==3.8.2
+markdown-it-py==4.0.0
+MarkupSafe==3.0.2
+matplotlib==3.10.0
+matplotlib-inline==0.1.7
+matplotlib-venn==1.1.2
+mcp==1.13.1
+mdit-py-plugins==0.5.0
+mdurl==0.1.2
+miniKanren==1.0.5
+missingno==0.5.2
+mistune==3.1.3
+mizani==0.13.5
+mkl==2025.2.0
+ml_dtypes==0.5.3
+mlflow==3.3.2
+mlflow-skinny==3.3.2
+mlflow-tracing==3.3.2
+mlxtend==0.23.4
+more-itertools==10.7.0
+moviepy==1.0.3
+mpmath==1.3.0
+msgpack==1.1.1
+multidict==6.6.4
+multipledispatch==1.0.0
+multiprocess==0.70.16
+multitasking==0.0.12
+murmurhash==1.0.13
+music21==9.3.0
+namex==0.1.0
+narwhals==2.2.0
+natsort==8.4.0
+nbclassic==1.3.1
+nbclient==0.10.2
+nbconvert==7.16.6
+nbformat==5.10.4
+ndindex==1.10.0
+nest-asyncio==1.6.0
+networkx==3.5
+nibabel==5.3.2
+nltk==3.9.1
+notebook==6.5.7
+notebook_shim==0.2.4
+numba==0.60.0
+numba-cuda==0.11.0
+numexpr==2.11.0
+numpy==2.0.2
+nvidia-cublas-cu12==12.6.4.1
+nvidia-cuda-cupti-cu12==12.6.80
+nvidia-cuda-nvcc-cu12==12.5.82
+nvidia-cuda-nvrtc-cu12==12.6.77
+nvidia-cuda-runtime-cu12==12.6.77
+nvidia-cudnn-cu12==9.10.2.21
+nvidia-cufft-cu12==11.3.0.4
+nvidia-cufile-cu12==1.11.1.6
+nvidia-curand-cu12==10.3.7.77
+nvidia-cusolver-cu12==11.7.1.2
+nvidia-cusparse-cu12==12.5.4.2
+nvidia-cusparselt-cu12==0.7.1
+nvidia-ml-py==12.575.51
+nvidia-nccl-cu12==2.27.3
+nvidia-nvjitlink-cu12==12.6.85
+nvidia-nvtx-cu12==12.6.77
+nvtx==0.2.13
+nx-cugraph-cu12 @ https://pypi.nvidia.com/nx-cugraph-cu12/nx_cugraph_cu12-25.6.0-py3-none-any.whl
+oauth2client==4.1.3
+oauthlib==3.3.1
+omegaconf==2.3.0
+openai==1.101.0
+opencv-contrib-python==4.12.0.88
+opencv-python==4.12.0.88
+opencv-python-headless==4.12.0.88
+openpyxl==3.1.5
+opentelemetry-api==1.36.0
+opentelemetry-exporter-gcp-trace==1.9.0
+opentelemetry-resourcedetector-gcp==1.9.0a0
+opentelemetry-sdk==1.36.0
+opentelemetry-semantic-conventions==0.57b0
+opt_einsum==3.4.0
+optax==0.2.5
+optree==0.17.0
+orbax-checkpoint==0.11.23
+orjson==3.11.2
+osqp==1.0.4
+packaging==25.0
+pandas==2.2.2
+pandas-datareader==0.10.0
+pandas-gbq==0.29.2
+pandas-stubs==2.2.2.240909
+pandocfilters==1.5.1
+panel==1.7.5
+param==2.2.1
+parso==0.8.5
+parsy==2.1
+partd==1.4.2
+patsy==1.0.1
+peewee==3.18.2
+peft==0.17.1
+pexpect==4.9.0
+pickleshare==0.7.5
+pillow==11.3.0
+platformdirs==4.3.8
+plotly==5.24.1
+plotnine==0.14.5
+pluggy==1.6.0
+plum-dispatch==2.5.7
+ply==3.11
+polars==1.25.2
+pooch==1.8.2
+portpicker==1.5.2
+preshed==3.0.10
+prettytable==3.16.0
+proglog==0.1.12
+progressbar2==4.5.0
+prometheus_client==0.22.1
+promise==2.3
+prompt_toolkit==3.0.51
+propcache==0.3.2
+prophet==1.1.7
+proto-plus==1.26.1
+protobuf==5.29.5
+psutil==5.9.5
+psycopg2==2.9.10
+psygnal==0.14.1
+ptyprocess==0.7.0
+py-cpuinfo==9.0.0
+py4j==0.10.9.7
+pyarrow==18.1.0
+pyasn1==0.6.1
+pyasn1_modules==0.4.2
+pycairo==1.28.0
+pycocotools==2.0.10
+pycparser==2.22
+pycryptodomex==3.23.0
+pydantic==2.11.7
+pydantic-settings==2.10.1
+pydantic_core==2.33.2
+pydata-google-auth==1.9.1
+pydot==3.0.4
+pydotplus==2.0.2
+PyDrive2==1.21.3
+pydub==0.25.1
+pyerfa==2.0.1.5
+pygame==2.6.1
+pygit2==1.18.2
+Pygments==2.19.2
+PyGObject==3.42.0
+PyJWT==2.10.1
+pylibcudf-cu12 @ https://pypi.nvidia.com/pylibcudf-cu12/pylibcudf_cu12-25.6.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
+pylibcugraph-cu12==25.6.0
+pylibraft-cu12==25.6.0
+pymc==5.25.1
+pynndescent==0.5.13
+pynvjitlink-cu12==0.7.0
+pynvml==12.0.0
+pyogrio==0.11.1
+pyomo==6.9.3
+PyOpenGL==3.1.10
+pyOpenSSL==24.2.1
+pyparsing==3.2.3
+pyperclip==1.9.0
+pyproj==3.7.2
+pyproject_hooks==1.2.0
+pyshp==2.3.1
+PySocks==1.7.1
+pyspark==3.5.1
+pytensor==2.31.7
+pytest==8.4.1
+python-apt==0.0.0
+python-box==7.3.2
+python-dateutil==2.9.0.post0
+python-dotenv==1.1.1
+python-louvain==0.16
+python-multipart==0.0.20
+python-slugify==8.0.4
+python-snappy==0.7.3
+python-utils==3.9.1
+pytz==2025.2
+pyviz_comms==3.0.6
+PyWavelets==1.9.0
+PyYAML==6.0.2
+pyzmq==26.2.1
+raft-dask-cu12==25.6.0
+rapids-dask-dependency==25.6.0
+rapids-logger==0.1.1
+ratelim==0.1.6
+referencing==0.36.2
+regex==2024.11.6
+requests==2.32.4
+requests-oauthlib==2.0.0
+requests-toolbelt==1.0.0
+requirements-parser==0.9.0
+rich==13.9.4
+rmm-cu12==25.6.0
+roman-numerals-py==3.1.0
+rpds-py==0.27.0
+rpy2==3.5.17
+rsa==4.9.1
+ruff==0.12.10
+s3transfer==0.13.1
+safehttpx==0.1.6
+safetensors==0.6.2
+scikit-image==0.25.2
+scikit-learn==1.6.1
+scipy==1.16.1
+scooby==0.10.1
+scs==3.2.8
+seaborn==0.13.2
+SecretStorage==3.3.3
+segmentation_models_pytorch==0.5.0
+semantic-version==2.10.0
+Send2Trash==1.8.3
+sentence-transformers==5.1.0
+sentencepiece==0.2.1
+sentry-sdk==2.35.0
+setuptools==75.2.0
+shap==0.48.0
+shapely==2.1.1
+shellingham==1.5.4
+simple-parsing==0.1.7
+simplejson==3.20.1
+simsimd==6.5.1
+six==1.17.0
+sklearn-pandas==2.2.0
+slicer==0.0.8
+smart_open==7.3.0.post1
+smmap==5.0.2
+sniffio==1.3.1
+snowballstemmer==3.0.1
+sortedcontainers==2.4.0
+soundfile==0.13.1
+soupsieve==2.7
+soxr==0.5.0.post1
+spacy==3.8.7
+spacy-legacy==3.0.12
+spacy-loggers==1.0.5
+spanner-graph-notebook==1.1.7
+Sphinx==8.2.3
+sphinxcontrib-applehelp==2.0.0
+sphinxcontrib-devhelp==2.0.0
+sphinxcontrib-htmlhelp==2.1.0
+sphinxcontrib-jsmath==1.0.1
+sphinxcontrib-qthelp==2.0.0
+sphinxcontrib-serializinghtml==2.0.0
+SQLAlchemy==2.0.43
+sqlglot==25.20.2
+sqlparse==0.5.3
+srsly==2.5.1
+sse-starlette==3.0.2
+stanio==0.5.1
+starlette==0.47.3
+statsmodels==0.14.5
+stringzilla==3.12.6
+stumpy==1.13.0
+sympy==1.13.3
+tables==3.10.2
+tabulate==0.9.0
+tbb==2022.2.0
+tblib==3.1.0
+tcmlib==1.4.0
+tenacity==8.5.0
+tensorboard==2.19.0
+tensorboard-data-server==0.7.2
+tensorflow==2.19.0
+tensorflow-datasets==4.9.9
+tensorflow-hub==0.16.1
+tensorflow-metadata==1.17.2
+tensorflow-probability==0.25.0
+tensorflow-text==2.19.0
+tensorflow_decision_forests==1.12.0
+tensorstore==0.1.76
+termcolor==3.1.0
+terminado==0.18.1
+text-unidecode==1.3
+textblob==0.19.0
+tf-slim==1.1.0
+tf_keras==2.19.0
+thinc==8.3.6
+threadpoolctl==3.6.0
+tifffile==2025.6.11
+tiktoken==0.11.0
+timm==1.0.19
+tinycss2==1.4.0
+tokenizers==0.21.4
+toml==0.10.2
+tomlkit==0.13.3
+toolz==0.12.1
+torch==2.8.0
+torchao==0.10.0
+torchaudio==2.8.0
+torchdata==0.11.0
+torchsummary==1.5.1
+torchtune==0.6.1
+torchvision==0.23.0
+tornado==6.4.2
+tqdm==4.67.1
+traitlets==5.7.1
+traittypes==0.2.1
+transformers==4.55.4
+treelite==4.4.1
+treescope==0.1.10
+triton==3.4.0
+tsfresh==0.21.0
+tweepy==4.16.0
+typeguard==4.4.4
+typer==0.16.1
+types-pytz==2025.2.0.20250809
+types-setuptools==80.9.0.20250822
+typing-inspection==0.4.1
+typing_extensions==4.15.0
+tzdata==2025.2
+tzlocal==5.3.1
+uc-micro-py==1.0.3
+ucx-py-cu12==0.44.0
+ucxx-cu12==0.44.0
+umap-learn==0.5.9.post2
+umf==0.11.0
+uritemplate==4.2.0
+urllib3==2.5.0
+uvicorn==0.35.0
+vega-datasets==0.9.0
+wadllib==1.3.6
+wandb==0.21.1
+wasabi==1.1.3
+watchdog==6.0.0
+wcwidth==0.2.13
+weasel==0.4.1
+webcolors==24.11.1
+webencodings==0.5.1
+websocket-client==1.8.0
+websockets==15.0.1
+Werkzeug==3.1.3
+wheel==0.45.1
+widgetsnbextension==3.6.10
+wordcloud==1.9.4
+wrapt==1.17.3
+wurlitzer==3.1.1
+xarray==2025.8.0
+xarray-einstats==0.9.1
+xgboost==3.0.4
+xlrd==2.0.2
+xxhash==3.5.0
+xyzservices==2025.4.0
+yarl==1.20.1
+ydf==0.13.0
+yellowbrick==1.5
+yfinance==0.2.65
+zict==3.0.0
+zipp==3.23.0
+zstandard==0.24.0
diff --git a/start.sh b/start.sh
index 95c20a5..3ebc9b6 100644
--- a/start.sh
+++ b/start.sh
@@ -1,52 +1,52 @@
-#!/bin/bash
-
-set -e
-
-# Set default port si non fourni
-export NGINX_PORT=${NGINX_PORT:-8080}
-export MULTISEG_API_BASE_URL=${MULTISEG_API_BASE_URL:-http://localhost:$NGINX_PORT/api}
-
-echo "=== Multi-Segmentation API Docker Startup ==="
-echo "NGINX_PORT: $NGINX_PORT"
-echo "API_BASE_URL: $MULTISEG_API_BASE_URL"
-
-# Substitute environment variables dans nginx config
-envsubst '${NGINX_PORT}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
-
-# S'assurer que les répertoires nginx sont accessibles
-mkdir -p /var/cache/nginx /var/run/nginx /var/log/nginx
-chown -R www-data:www-data /var/cache/nginx /var/run/nginx /var/log/nginx
-
-# Créer le dossier uploads temporaire
-mkdir -p /tmp/uploads
-
-echo "Starting FastAPI server..."
-uvicorn app.main:app --host 0.0.0.0 --port 8000 --loop asyncio &
-
-echo "Starting Streamlit app..."
-streamlit run app/streamlit_app.py \
- --server.port 8501 \
- --server.address 0.0.0.0 \
- --server.enableXsrfProtection false \
- --server.enableCORS false \
- --server.headless true \
- &
-
-# Vérifier que FastAPI est prêt
-echo "Waiting for FastAPI to be ready..."
-until curl -s http://127.0.0.1:8000/health > /dev/null; do
- echo " FastAPI not ready yet, waiting..."
- sleep 2
-done
-echo "✅ FastAPI is ready!"
-
-# Vérifier que Streamlit est prêt
-echo "Waiting for Streamlit to be ready..."
-until curl -s http://127.0.0.1:8501 > /dev/null; do
- echo " Streamlit not ready yet, waiting..."
- sleep 2
-done
-echo "✅ Streamlit is ready!"
-
-echo "🚀 Launching NGINX reverse proxy on port $NGINX_PORT..."
-nginx -c /etc/nginx/nginx.conf -g 'daemon off;'
+#!/bin/bash
+
+set -e
+
+# Set default port si non fourni
+export NGINX_PORT=${NGINX_PORT:-8080}
+export MULTISEG_API_BASE_URL=${MULTISEG_API_BASE_URL:-http://localhost:$NGINX_PORT/api}
+
+echo "=== Multi-Segmentation API Docker Startup ==="
+echo "NGINX_PORT: $NGINX_PORT"
+echo "API_BASE_URL: $MULTISEG_API_BASE_URL"
+
+# Substitute environment variables dans nginx config
+envsubst '${NGINX_PORT}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
+
+# S'assurer que les répertoires nginx sont accessibles
+mkdir -p /var/cache/nginx /var/run/nginx /var/log/nginx
+chown -R www-data:www-data /var/cache/nginx /var/run/nginx /var/log/nginx
+
+# Créer le dossier uploads temporaire
+mkdir -p /tmp/uploads
+
+echo "Starting FastAPI server..."
+uvicorn app.main:app --host 0.0.0.0 --port 8000 --loop asyncio &
+
+echo "Starting Streamlit app..."
+streamlit run app/streamlit_app.py \
+ --server.port 8501 \
+ --server.address 0.0.0.0 \
+ --server.enableXsrfProtection false \
+ --server.enableCORS false \
+ --server.headless true \
+ &
+
+# Vérifier que FastAPI est prêt
+echo "Waiting for FastAPI to be ready..."
+until curl -s http://127.0.0.1:8000/health > /dev/null; do
+ echo " FastAPI not ready yet, waiting..."
+ sleep 2
+done
+echo "✅ FastAPI is ready!"
+
+# Vérifier que Streamlit est prêt
+echo "Waiting for Streamlit to be ready..."
+until curl -s http://127.0.0.1:8501 > /dev/null; do
+ echo " Streamlit not ready yet, waiting..."
+ sleep 2
+done
+echo "✅ Streamlit is ready!"
+
+echo "🚀 Launching NGINX reverse proxy on port $NGINX_PORT..."
+nginx -c /etc/nginx/nginx.conf -g 'daemon off;'
diff --git a/utils/utils.py b/utils/utils.py
index cd47f12..17d24bc 100644
--- a/utils/utils.py
+++ b/utils/utils.py
@@ -1,658 +1,658 @@
-import os
-import json
-import tempfile
-import numpy as np
-import torch
-import matplotlib.pyplot as plt
-import segmentation_models_pytorch as smp
-import mlflow
-import re
-import ast
-import time
-import random
-
-# Visualisation
-import matplotlib.patches as mpatches
-from matplotlib.colors import ListedColormap
-
-# Preprocessing/Images
-import albumentations as A
-from albumentations.pytorch import ToTensorV2
-
-
-def update_colors_to_wcag(mapping_config):
- """
- Met à jour les couleurs dans mapping_config pour utiliser les couleurs WCAG accessibles
- avec redistribution sémantique intuitive finale
- """
- # Redistribution sémantique finale :
- # Flat → gris moyen, Human → rouge, Vehicle → violet, Construction → orange,
- # Object → teal, Nature → vert, Sky → bleu, Void → gris foncé
- wcag_colors = np.array([
- [102, 102, 102], # Flat: gris moyen (routes, trottoirs)
- [204, 0, 0], # Human: rouge (piétons, cyclistes)
- [102, 51, 153], # Vehicle: violet (véhicules)
- [204, 102, 0], # Construction: orange (bâtiments)
- [0, 102, 102], # Object: teal (objets, signalétique)
- [0, 102, 0], # Nature: vert (végétation)
- [0, 102, 204], # Sky: bleu (ciel)
- [51, 51, 51] # Void: gris foncé (zones non-définies)
- ], dtype=np.uint8)
-
- # S'assurer qu'on a assez de couleurs pour le nombre de classes
- num_classes = mapping_config['num_classes']
- if len(wcag_colors) < num_classes:
- print(f"⚠️ Attention: {num_classes} classes mais seulement {len(wcag_colors)} couleurs WCAG disponibles")
- # Compléter avec les couleurs originales si nécessaire
- original_colors = mapping_config['group_colors']
- while len(wcag_colors) < num_classes:
- wcag_colors = np.vstack([wcag_colors, original_colors[len(wcag_colors):len(wcag_colors)+1]])
-
- mapping_config['group_colors'] = wcag_colors[:num_classes]
- print(f"✅ Couleurs mises à jour vers WCAG pour {num_classes} classes")
- return mapping_config
-
-
-def load_cityscapes_config(config_path="../cityscapes_config.json", verbose=True, use_wcag_colors=False):
- """
- Charge la configuration Cityscapes depuis un fichier JSON.
-
- Args:
- config_path: Chemin vers le fichier de configuration
- verbose: Affichage détaillé
- use_wcag_colors: Utiliser les couleurs WCAG accessibles
- """
- with open(config_path, "r", encoding="utf-8") as f:
- config = json.load(f)
-
- class_groups = config["class_groups"]
- cityscapes_mapping = {int(k): v for k, v in config["cityscapes_mapping"].items()}
- id_to_label = {int(k): v for k, v in config["id_to_label"].items()}
- group_colors = np.array(config["group_colors"], dtype=np.uint8)
- group_names = config["group_names"]
-
- id_to_group = np.zeros(256, dtype=np.uint8)
- for id_, group_id in cityscapes_mapping.items():
- id_to_group[id_] = group_id
-
- num_classes = len(group_names)
-
- mapping_config = {
- "class_groups": class_groups,
- "cityscapes_mapping": cityscapes_mapping,
- "id_to_label": id_to_label,
- "group_colors": group_colors,
- "group_names": group_names,
- "id_to_group": id_to_group,
- "num_classes": num_classes
- }
-
- # Appliquer les couleurs WCAG si demandé
- if use_wcag_colors:
- mapping_config = update_colors_to_wcag(mapping_config)
-
- if verbose:
- print("== Groupes de classes Cityscapes ==")
- for group, classes in class_groups.items():
- print(f" - {group} : {classes}")
-
- print("\n== Mapping ID Cityscapes → Groupe ==")
- for id_ in sorted(cityscapes_mapping.keys()):
- label = id_to_label.get(id_, "Label inconnu")
- group_id = cityscapes_mapping[id_]
- print(f" ID {id_:2d} ('{label}') → groupe '{group_names[group_id]}' ({group_id})")
-
- print("\n== Liste des groupes et couleurs associées ==")
- for i, (name, color) in enumerate(zip(group_names, mapping_config['group_colors'])):
- print(f" Groupe {i}: {name} - Couleur RGB : {color}")
-
- print(f"\nNombre total de groupes/classes : {num_classes}")
-
- return mapping_config
-
-
-def map_mask_ids(mask, id_to_group_mapping):
- """
- Applique le mapping des IDs Cityscapes vers les groupes
- ADAPTÉ PYTORCH : Utilise torch au lieu de tf
- """
- if isinstance(mask, np.ndarray):
- mask = torch.from_numpy(mask)
-
- if isinstance(id_to_group_mapping, np.ndarray):
- if len(id_to_group_mapping) != 256:
- raise ValueError(f"id_to_group_mapping doit avoir 256 éléments, reçu: {len(id_to_group_mapping)}")
- mapping_tensor = torch.tensor(id_to_group_mapping, dtype=torch.long)
- else:
- mapping_tensor = torch.tensor(list(id_to_group_mapping), dtype=torch.long)
-
- # S'assurer que les valeurs du masque sont dans la plage valide [0, 255]
- mask_clipped = torch.clamp(mask, 0, 255)
- mapped_mask = mapping_tensor[mask_clipped.long()]
- return mapped_mask
-
-def colorize_mask(mask, group_colors):
- """
- Applique la colorisation du masque avec les couleurs définies
- INCHANGÉ - Compatible PyTorch (prend numpy en entrée)
- """
- if isinstance(mask, torch.Tensor):
- mask = mask.numpy()
-
- colored_mask = np.zeros((mask.shape[0], mask.shape[1], 3), dtype=np.uint8)
- num_classes = len(group_colors)
-
- for group_id in range(num_classes):
- colored_mask[mask == group_id] = group_colors[group_id]
-
- return colored_mask
-
-
-def get_preprocessing_fn(img_size=(512, 512), encoder=None):
- """
- Retourne une fonction qui prétraite à la fois les images et les masques
- pour l'inférence, en reproduisant exactement le pipeline CityscapesDataset.
- """
-
- if encoder is None:
- mean, std = [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]
- else:
- mean, std = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]
-
- # Pipeline unifié (gérera image ET masque ensemble)
- transform = A.Compose([
- A.Resize(height=img_size[0], width=img_size[1]),
- A.Normalize(mean=mean, std=std),
- ToTensorV2()
- ])
-
- def preprocess(image_np, mask_np=None):
- if mask_np is not None:
- # Traitement simultané image + masque
- transformed = transform(image=image_np, mask=mask_np)
- return transformed['image'], transformed['mask'].long()
- else:
- # Image seule
- transformed = transform(image=image_np)
- return transformed['image'], None
-
- return preprocess
-
-def apply_random_augmentation(image_np, prob=0.7):
- """
- Applique des transformations d'augmentation aléatoires à une image,
- similaires à celles utilisées pendant l'entraînement.
-
- Args:
- image_np (np.ndarray): Image numpy array (H, W, 3)
- prob (float): Probabilité d'appliquer chaque transformation
-
- Returns:
- tuple: (image_augmentée, liste_des_transformations_appliquées)
- """
- transforms_applied = []
-
- # Définir les transformations possibles
- possible_transforms = [
- {
- "name": "Rotation aléatoire",
- "transform": A.Rotate(limit=15, p=prob),
- "description": "Rotation de -15° à +15°"
- },
- {
- "name": "Flip horizontal",
- "transform": A.HorizontalFlip(p=0.5),
- "description": "Miroir horizontal (50% chance)"
- },
- {
- "name": "Changement de luminosité",
- "transform": A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=prob),
- "description": "Luminosité et contraste ±20%"
- },
- {
- "name": "Flou gaussien léger",
- "transform": A.GaussianBlur(blur_limit=(1, 3), p=prob*0.3),
- "description": "Flou gaussien subtil"
- },
- {
- "name": "Décalage couleur",
- "transform": A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=15, val_shift_limit=10, p=prob),
- "description": "Ajustement teinte/saturation/valeur"
- },
- {
- "name": "Zoom aléatoire",
- "transform": A.RandomScale(scale_limit=0.1, p=prob*0.5),
- "description": "Zoom de -10% à +10%"
- }
- ]
-
- # Sélectionner aléatoirement quelques transformations
- num_transforms = random.randint(1, min(4, len(possible_transforms)))
- selected_transforms = random.sample(possible_transforms, num_transforms)
-
- # Créer le pipeline d'augmentation
- augmentation_pipeline = []
- for transform_info in selected_transforms:
- augmentation_pipeline.append(transform_info["transform"])
- transforms_applied.append(f"{transform_info['name']}: {transform_info['description']}")
-
- # Appliquer les transformations
- compose = A.Compose(augmentation_pipeline)
-
- try:
- augmented = compose(image=image_np)
- augmented_image = augmented['image']
- except Exception as e:
- print(f"Erreur lors de l'augmentation: {e}")
- # En cas d'erreur, retourner l'image originale
- augmented_image = image_np.copy()
- transforms_applied = ["Erreur - image originale conservée"]
-
- return augmented_image, transforms_applied
-
-# Version simplifiée de preprocess_image_and_mask utilisant la nouvelle get_preprocessing_fn
-def preprocess_image_and_mask(image_path, mask_path=None, img_size=(512, 512),
- encoder_name=None, id_to_group_mapping=None):
- """
- Version simplifiée utilisant get_preprocessing_fn pour la cohérence avec CityscapesDataset
- """
- import numpy as np
- from PIL import Image
- import torch
-
- print(f"🔄 Préprocessing de l'image: {os.path.basename(image_path)}")
-
- # Chargement des données
- image_pil = Image.open(image_path).convert('RGB')
- image_np = np.array(image_pil)
- img_display = np.array(image_pil.resize(img_size, Image.BILINEAR)) # Pour affichage
-
- mask_np = None
- if mask_path and os.path.exists(mask_path):
- print(f"🔄 Préprocessing du masque: {os.path.basename(mask_path)}")
- mask_pil = Image.open(mask_path).convert('L')
- mask_np = np.array(mask_pil)
- print(f" Valeurs uniques avant mapping: {np.unique(mask_np)[:10]}...")
-
- # Utiliser notre preprocessing unifié
- preprocess_fn = get_preprocessing_fn(img_size=img_size, encoder=encoder_name)
- img_preprocessed, mask_preprocessed = preprocess_fn(image_np, mask_np)
-
- # Appliquer le mapping si fourni ET si on a un masque
- if mask_preprocessed is not None and id_to_group_mapping is not None:
- mask_preprocessed = map_mask_ids(mask_preprocessed, id_to_group_mapping)
- print(f" Valeurs uniques après mapping: {torch.unique(mask_preprocessed)}")
-
- return img_preprocessed, img_display, mask_preprocessed
-
-# Version mise à jour de run_inference_and_visualize
-def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config,
- mask_path=None, save_dir=None, show_stats=True, show_plot=True):
- """
- Version simplifiée utilisant preprocess_image_and_mask
- """
- print(f"=== INFÉRENCE PYTORCH SUR {os.path.basename(image_path)} ===")
-
- # Utiliser la version simplifiée du preprocessing
- img_preprocessed, img_display, mask_gt = preprocess_image_and_mask(
- image_path=image_path,
- mask_path=mask_path,
- img_size=img_size or (512, 512),
- encoder_name=encoder_name,
- id_to_group_mapping=mapping_config.get('id_to_group')
- )
-
- print("🔮 Exécution de l'inférence PyTorch...")
- model.eval()
- device = next(model.parameters()).device
-
- with torch.no_grad():
- # Ajouter dimension batch
- img_batch = img_preprocessed.unsqueeze(0).to(device) # [1, 3, H, W]
-
- # ⏱️ MESURE DU TEMPS D'INFÉRENCE
- start_time = time.time()
- predictions = model(img_batch) # [1, num_classes, H, W]
- end_time = time.time()
-
- inference_time = end_time - start_time
-
- predicted_mask = torch.argmax(predictions[0], dim=0).cpu().numpy() # [H, W]
-
- print(f"✅ Inférence terminée - Shape: {predicted_mask.shape}")
- print(f"⏱️ Temps d'inférence: {inference_time*1000:.1f} ms ({inference_time:.4f} s)")
-
- # [Le reste du code de visualisation reste identique]
- # Visualisation
- num_plots = 2 if mask_gt is None else 4
- fig, axes = plt.subplots(1, num_plots, figsize=(5*num_plots, 5))
- if num_plots == 1:
- axes = [axes]
-
- axes[0].imshow(img_display)
- axes[0].set_title("Image Originale")
- axes[0].axis('off')
-
- predicted_colored = colorize_mask(predicted_mask, mapping_config['group_colors'])
- axes[1].imshow(predicted_colored)
- axes[1].set_title("Masque Prédit")
- axes[1].axis('off')
-
- if mask_gt is not None:
- mask_gt_np = mask_gt.numpy() if isinstance(mask_gt, torch.Tensor) else mask_gt
- gt_colored = colorize_mask(mask_gt_np, mapping_config['group_colors'])
- axes[2].imshow(gt_colored)
- axes[2].set_title("Masque de Vérité")
- axes[2].axis('off')
-
- # Visualisation des erreurs
- diff_mask = (mask_gt_np != predicted_mask).astype(float)
- axes[3].imshow(img_display)
- axes[3].imshow(diff_mask, alpha=0.5, cmap='Reds')
- axes[3].set_title("Erreurs (en rouge)")
- axes[3].axis('off')
-
- colors_normalized = mapping_config['group_colors'] / 255.0
- custom_cmap = ListedColormap(colors_normalized)
-
- legend_elements = []
- for i, (name, color) in enumerate(zip(mapping_config['group_names'], colors_normalized)):
- legend_elements.append(mpatches.Patch(color=color, label=f'{i}: {name}'))
-
- fig.legend(handles=legend_elements,
- loc='center left',
- bbox_to_anchor=(1.01, 0.5),
- fontsize=9,
- frameon=True,
- fancybox=True,
- shadow=True)
-
- plt.tight_layout()
- fig.subplots_adjust(right=0.95)
-
- if save_dir:
- os.makedirs(save_dir, exist_ok=True)
- img_name = os.path.splitext(os.path.basename(image_path))[0]
- fig_path = os.path.join(save_dir, f'{img_name}_inference.png')
- fig.savefig(fig_path, dpi=150, bbox_inches='tight')
- print(f"💾 Résultat sauvegardé: {fig_path}")
-
-
- # Statistiques
- if show_stats:
- print("\n📊 Statistiques du masque prédit:")
- unique_classes, counts = np.unique(predicted_mask, return_counts=True)
- for class_id, count in zip(unique_classes, counts):
- if class_id < len(mapping_config['group_names']):
- percentage = (count / predicted_mask.size) * 100
- print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)")
-
- if mask_gt is not None:
- mask_gt_np = mask_gt.numpy() if isinstance(mask_gt, torch.Tensor) else mask_gt
- print("\n📊 Statistiques du masque de vérité:")
- unique_classes, counts = np.unique(mask_gt_np, return_counts=True)
- for class_id, count in zip(unique_classes, counts):
- if class_id < len(mapping_config['group_names']):
- percentage = (count / mask_gt_np.size) * 100
- print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)")
-
- if show_plot:
- plt.show()
- return predicted_mask, inference_time
- else:
- return predicted_mask, inference_time, fig
-
-
-def load_model(run_id=None, experiment_name="OC Projet 9",
- metric="test_mean_iou", top_n=1):
- """
- Charge un modèle PyTorch depuis MLflow avec plusieurs méthodes de sélection
-
- Args:
- run_id (str, optional): Run ID spécifique (prioritaire si fourni)
- experiment_name (str): Nom de l'expérience MLflow
- metric (str): Métrique pour classer les runs
- top_n (int): Position dans le classement (1=meilleur, 2=deuxième, etc.)
-
- Returns:
- tuple: (model, source_run_id, encoder_name, script_img_size)
- """
-
- print(f"\n🔍 Chargement d'un modèle PyTorch depuis MLflow...")
-
- client = mlflow.tracking.MlflowClient()
-
- # 1. Sélection du run selon les paramètres
- if run_id:
- print(f"🎯 Mode run_id spécifique: {run_id}")
- try:
- run = mlflow.get_run(run_id)
- source_run_id = run_id
- print(f"✅ Run trouvé: {run.data.tags.get('mlflow.runName', 'Unknown')}")
- except Exception as e:
- raise RuntimeError(f"❌ Run {run_id} introuvable: {e}")
-
- else:
- print(f"🏆 Mode classement: top_{top_n} de '{metric}' dans '{experiment_name}'")
- try:
- # Récupérer l'expérience
- experiment = client.get_experiment_by_name(experiment_name)
- if experiment is None:
- raise RuntimeError(f"❌ Expérience '{experiment_name}' non trouvée")
-
- # Rechercher les runs avec la métrique, triés par ordre décroissant
- filter_string = f"status = 'FINISHED' and metrics.{metric} >= 0"
- order_by = f"metrics.{metric} DESC"
-
- runs_df = mlflow.search_runs(
- experiment_ids=[experiment.experiment_id],
- filter_string=filter_string,
- order_by=[order_by],
- max_results=top_n
- )
-
- if runs_df.empty:
- raise RuntimeError(f"❌ Aucun run avec '{metric}' trouvé dans '{experiment_name}'")
-
- if len(runs_df) < top_n:
- raise RuntimeError(f"❌ Seulement {len(runs_df)} runs trouvés, impossible d'obtenir le top_{top_n}")
-
- # Prendre le run à la position top_n
- selected_run = runs_df.iloc[top_n - 1]
- source_run_id = selected_run['run_id']
- metric_value = selected_run[f'metrics.{metric}']
- run_name = selected_run.get('tags.mlflow.runName', 'Unknown')
-
- print(f"✅ Run sélectionné (position #{top_n}):")
- print(f" • Nom: {run_name}")
- print(f" • Run ID: {source_run_id}")
- print(f" • {metric}: {metric_value:.4f}")
-
- # Afficher le contexte
- print(f"📊 Contexte du classement:")
- for i, (_, row) in enumerate(runs_df.head(min(5, len(runs_df))).iterrows()):
- indicator = "👑" if i == (top_n - 1) else f"{i+1}."
- name = row.get('tags.mlflow.runName', 'Unknown')[:30]
- value = row[f'metrics.{metric}']
- print(f" {indicator} {name}: {value:.4f}")
-
- run = mlflow.get_run(source_run_id)
-
- except Exception as e:
- raise RuntimeError(f"❌ Erreur lors de la sélection: {e}")
-
- # 2-5. [Reste du code identique - extraction params, détection variante, chargement...]
- try:
- params = run.data.params
-
- encoder_name = params.get("encoder_name", None)
- architecture = params.get("architecture", "FPN")
- num_classes = int(params.get("num_classes", 8))
- script_img_size_raw = params.get("script_img_size", None)
- run_name = run.data.tags.get('mlflow.runName', 'Unknown')
-
- # Parser script_img_size
- script_img_size = None
- if script_img_size_raw:
- try:
- script_img_size = ast.literal_eval(script_img_size_raw)
- if isinstance(script_img_size, (tuple, list)) and len(script_img_size) == 2:
- script_img_size = tuple(script_img_size)
- except:
- script_img_size = None
-
- print(f"\n📋 Paramètres du modèle:")
- print(f"🏗️ Architecture: {architecture}")
- print(f"🔧 Encodeur: {encoder_name}")
- print(f"📊 Nombre de classes: {num_classes}")
- print(f"🖼️ Script img size: {script_img_size}")
-
- except Exception as e:
- raise RuntimeError(f"❌ Erreur lors de la récupération des paramètres: {e}")
-
- # Détecter la variante SegFormer
- def detect_segformer_variant_from_name(run_name, params):
- segformer_pattern = r'SegFormer[_-]?b(\d+)'
- match = re.search(segformer_pattern, run_name, re.IGNORECASE)
- if match:
- variant_num = match.group(1)
- detected = f"mit_b{variant_num}"
- print(f"🔍 Variante détectée: 'SegFormer_b{variant_num}' → {detected}")
- return detected
-
- if encoder_name and encoder_name.startswith("mit_b"):
- print(f"🔍 Variante depuis paramètres: {encoder_name}")
- return encoder_name
-
- b_pattern = r'\bb(\d+)\b'
- match = re.search(b_pattern, run_name, re.IGNORECASE)
- if match:
- variant_num = match.group(1)
- detected = f"mit_b{variant_num}"
- print(f"🔍 Pattern détecté: 'b{variant_num}' → {detected}")
- return detected
-
- print(f"🔍 Aucune variante détectée, utilisation de mit_b0")
- return "mit_b0"
-
- detected_variant = detect_segformer_variant_from_name(run_name, params)
-
- # Télécharger et charger le modèle
- with tempfile.TemporaryDirectory() as tmpdir:
- try:
- artifacts = client.list_artifacts(source_run_id)
- model_artifact_candidates = [a.path for a in artifacts if a.path.endswith('.pth')]
-
- if not model_artifact_candidates:
- raise RuntimeError("❌ Aucun fichier .pth trouvé")
-
- model_artifact = model_artifact_candidates[0]
- print(f"⬇️ Téléchargement de {model_artifact}...")
-
- local_path = mlflow.artifacts.download_artifacts(
- run_id=source_run_id,
- artifact_path=model_artifact,
- dst_path=tmpdir
- )
-
- checkpoint = torch.load(local_path, map_location='cpu')
- if isinstance(checkpoint, dict) and 'state_dict' in checkpoint:
- state_dict = checkpoint['state_dict']
- else:
- state_dict = checkpoint
-
- except Exception as e:
- raise RuntimeError(f"❌ Erreur lors du téléchargement: {e}")
-
- # Créer le modèle
- try:
- is_transformer = any('patch_embed' in key for key in state_dict.keys())
-
- if is_transformer:
- print(f"🔧 Création SegFormer ({detected_variant})")
- model = smp.Segformer(encoder_name=detected_variant, classes=num_classes)
- encoder_name = detected_variant
- else:
- print("🔧 Création modèle CNN")
- if encoder_name is None or encoder_name == "None":
- encoder_name = "efficientnet-b0"
-
- if architecture.lower() == "fpn":
- model = smp.FPN(encoder_name=encoder_name, classes=num_classes)
- elif architecture.lower() == "unet":
- model = smp.Unet(encoder_name=encoder_name, classes=num_classes)
- elif architecture.lower() == "deeplabv3plus":
- model = smp.DeepLabV3Plus(encoder_name=encoder_name, classes=num_classes)
- else:
- raise ValueError(f"Architecture {architecture} non supportée")
-
- print("🔄 Chargement des poids...")
- model.load_state_dict(state_dict, strict=True)
- model.eval()
- print("✅ Modèle chargé avec succès!")
-
- except Exception as e:
- try:
- model.load_state_dict(state_dict, strict=False)
- model.eval()
- print("✅ Chargement partiel réussi!")
- except Exception as e2:
- raise RuntimeError(f"❌ Erreur lors du chargement: {e2}")
-
- print(f"🎉 Modèle chargé depuis le run {source_run_id}")
- return model, source_run_id, encoder_name, script_img_size
-
-
-def load_best_model_from_registry(model_name):
- """
- Charge le meilleur modèle depuis le MLflow Model Registry
- (Version refactorisée - wrapper autour de load_model)
-
- Args:
- model_name (str): Nom du modèle dans le Registry
-
- Returns:
- tuple: (model, source_run_id, encoder_name, script_img_size)
- """
- print(f"🏛️ Récupération du modèle '{model_name}' depuis le Registry...")
-
- client = mlflow.tracking.MlflowClient()
-
- try:
- # Récupérer la version en Production
- versions = client.search_model_versions(f"name='{model_name}'")
- prod_versions = [v for v in versions if v.current_stage == "Production"]
-
- if not prod_versions:
- raise RuntimeError(f"❌ Aucune version en Production trouvée pour {model_name}")
-
- latest_prod = max(prod_versions, key=lambda v: int(v.version))
- print(f"📦 Version Registry: {latest_prod.version} (Stage: {latest_prod.current_stage})")
-
- # Récupérer le source_run_id depuis le tag du Registry
- source_run_id = latest_prod.tags.get("source_run_id")
- if not source_run_id:
- raise RuntimeError(f"❌ Tag 'source_run_id' absent pour la version {latest_prod.version}")
-
- print(f"🔗 Source run détecté: {source_run_id}")
-
- except Exception as e:
- raise RuntimeError(f"❌ Erreur lors de l'accès au Registry: {e}")
-
- # Déléguer le chargement à load_model avec le run_id trouvé
- print("➡️ Délégation vers load_model...")
- return load_model(run_id=source_run_id)
-
-
-# Fonctions de convenance
-def load_best_model(experiment_name="OC Projet 9", metric="test_mean_iou"):
- """Charge le meilleur modèle selon une métrique"""
- return load_model(experiment_name=experiment_name, metric=metric, top_n=1)
-
-def load_second_best_model(experiment_name="OC Projet 9", metric="test_mean_iou"):
- """Charge le deuxième meilleur modèle selon une métrique"""
- return load_model(experiment_name=experiment_name, metric=metric, top_n=2)
-
-def load_model_by_run_id(run_id):
- """Charge un modèle par run_id spécifique"""
+import os
+import json
+import tempfile
+import numpy as np
+import torch
+import matplotlib.pyplot as plt
+import segmentation_models_pytorch as smp
+import mlflow
+import re
+import ast
+import time
+import random
+
+# Visualisation
+import matplotlib.patches as mpatches
+from matplotlib.colors import ListedColormap
+
+# Preprocessing/Images
+import albumentations as A
+from albumentations.pytorch import ToTensorV2
+
+
+def update_colors_to_wcag(mapping_config):
+ """
+ Met à jour les couleurs dans mapping_config pour utiliser les couleurs WCAG accessibles
+ avec redistribution sémantique intuitive finale
+ """
+ # Redistribution sémantique finale :
+ # Flat → gris moyen, Human → rouge, Vehicle → violet, Construction → orange,
+ # Object → teal, Nature → vert, Sky → bleu, Void → gris foncé
+ wcag_colors = np.array([
+ [102, 102, 102], # Flat: gris moyen (routes, trottoirs)
+ [204, 0, 0], # Human: rouge (piétons, cyclistes)
+ [102, 51, 153], # Vehicle: violet (véhicules)
+ [204, 102, 0], # Construction: orange (bâtiments)
+ [0, 102, 102], # Object: teal (objets, signalétique)
+ [0, 102, 0], # Nature: vert (végétation)
+ [0, 102, 204], # Sky: bleu (ciel)
+ [51, 51, 51] # Void: gris foncé (zones non-définies)
+ ], dtype=np.uint8)
+
+ # S'assurer qu'on a assez de couleurs pour le nombre de classes
+ num_classes = mapping_config['num_classes']
+ if len(wcag_colors) < num_classes:
+ print(f"⚠️ Attention: {num_classes} classes mais seulement {len(wcag_colors)} couleurs WCAG disponibles")
+ # Compléter avec les couleurs originales si nécessaire
+ original_colors = mapping_config['group_colors']
+ while len(wcag_colors) < num_classes:
+ wcag_colors = np.vstack([wcag_colors, original_colors[len(wcag_colors):len(wcag_colors)+1]])
+
+ mapping_config['group_colors'] = wcag_colors[:num_classes]
+ print(f"✅ Couleurs mises à jour vers WCAG pour {num_classes} classes")
+ return mapping_config
+
+
+def load_cityscapes_config(config_path="../cityscapes_config.json", verbose=True, use_wcag_colors=False):
+ """
+ Charge la configuration Cityscapes depuis un fichier JSON.
+
+ Args:
+ config_path: Chemin vers le fichier de configuration
+ verbose: Affichage détaillé
+ use_wcag_colors: Utiliser les couleurs WCAG accessibles
+ """
+ with open(config_path, "r", encoding="utf-8") as f:
+ config = json.load(f)
+
+ class_groups = config["class_groups"]
+ cityscapes_mapping = {int(k): v for k, v in config["cityscapes_mapping"].items()}
+ id_to_label = {int(k): v for k, v in config["id_to_label"].items()}
+ group_colors = np.array(config["group_colors"], dtype=np.uint8)
+ group_names = config["group_names"]
+
+ id_to_group = np.zeros(256, dtype=np.uint8)
+ for id_, group_id in cityscapes_mapping.items():
+ id_to_group[id_] = group_id
+
+ num_classes = len(group_names)
+
+ mapping_config = {
+ "class_groups": class_groups,
+ "cityscapes_mapping": cityscapes_mapping,
+ "id_to_label": id_to_label,
+ "group_colors": group_colors,
+ "group_names": group_names,
+ "id_to_group": id_to_group,
+ "num_classes": num_classes
+ }
+
+ # Appliquer les couleurs WCAG si demandé
+ if use_wcag_colors:
+ mapping_config = update_colors_to_wcag(mapping_config)
+
+ if verbose:
+ print("== Groupes de classes Cityscapes ==")
+ for group, classes in class_groups.items():
+ print(f" - {group} : {classes}")
+
+ print("\n== Mapping ID Cityscapes → Groupe ==")
+ for id_ in sorted(cityscapes_mapping.keys()):
+ label = id_to_label.get(id_, "Label inconnu")
+ group_id = cityscapes_mapping[id_]
+ print(f" ID {id_:2d} ('{label}') → groupe '{group_names[group_id]}' ({group_id})")
+
+ print("\n== Liste des groupes et couleurs associées ==")
+ for i, (name, color) in enumerate(zip(group_names, mapping_config['group_colors'])):
+ print(f" Groupe {i}: {name} - Couleur RGB : {color}")
+
+ print(f"\nNombre total de groupes/classes : {num_classes}")
+
+ return mapping_config
+
+
+def map_mask_ids(mask, id_to_group_mapping):
+ """
+ Applique le mapping des IDs Cityscapes vers les groupes
+ ADAPTÉ PYTORCH : Utilise torch au lieu de tf
+ """
+ if isinstance(mask, np.ndarray):
+ mask = torch.from_numpy(mask)
+
+ if isinstance(id_to_group_mapping, np.ndarray):
+ if len(id_to_group_mapping) != 256:
+ raise ValueError(f"id_to_group_mapping doit avoir 256 éléments, reçu: {len(id_to_group_mapping)}")
+ mapping_tensor = torch.tensor(id_to_group_mapping, dtype=torch.long)
+ else:
+ mapping_tensor = torch.tensor(list(id_to_group_mapping), dtype=torch.long)
+
+ # S'assurer que les valeurs du masque sont dans la plage valide [0, 255]
+ mask_clipped = torch.clamp(mask, 0, 255)
+ mapped_mask = mapping_tensor[mask_clipped.long()]
+ return mapped_mask
+
+def colorize_mask(mask, group_colors):
+ """
+ Applique la colorisation du masque avec les couleurs définies
+ INCHANGÉ - Compatible PyTorch (prend numpy en entrée)
+ """
+ if isinstance(mask, torch.Tensor):
+ mask = mask.numpy()
+
+ colored_mask = np.zeros((mask.shape[0], mask.shape[1], 3), dtype=np.uint8)
+ num_classes = len(group_colors)
+
+ for group_id in range(num_classes):
+ colored_mask[mask == group_id] = group_colors[group_id]
+
+ return colored_mask
+
+
+def get_preprocessing_fn(img_size=(512, 512), encoder=None):
+ """
+ Retourne une fonction qui prétraite à la fois les images et les masques
+ pour l'inférence, en reproduisant exactement le pipeline CityscapesDataset.
+ """
+
+ if encoder is None:
+ mean, std = [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]
+ else:
+ mean, std = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]
+
+ # Pipeline unifié (gérera image ET masque ensemble)
+ transform = A.Compose([
+ A.Resize(height=img_size[0], width=img_size[1]),
+ A.Normalize(mean=mean, std=std),
+ ToTensorV2()
+ ])
+
+ def preprocess(image_np, mask_np=None):
+ if mask_np is not None:
+ # Traitement simultané image + masque
+ transformed = transform(image=image_np, mask=mask_np)
+ return transformed['image'], transformed['mask'].long()
+ else:
+ # Image seule
+ transformed = transform(image=image_np)
+ return transformed['image'], None
+
+ return preprocess
+
+def apply_random_augmentation(image_np, prob=0.7):
+ """
+ Applique des transformations d'augmentation aléatoires à une image,
+ similaires à celles utilisées pendant l'entraînement.
+
+ Args:
+ image_np (np.ndarray): Image numpy array (H, W, 3)
+ prob (float): Probabilité d'appliquer chaque transformation
+
+ Returns:
+ tuple: (image_augmentée, liste_des_transformations_appliquées)
+ """
+ transforms_applied = []
+
+ # Définir les transformations possibles
+ possible_transforms = [
+ {
+ "name": "Rotation aléatoire",
+ "transform": A.Rotate(limit=15, p=prob),
+ "description": "Rotation de -15° à +15°"
+ },
+ {
+ "name": "Flip horizontal",
+ "transform": A.HorizontalFlip(p=0.5),
+ "description": "Miroir horizontal (50% chance)"
+ },
+ {
+ "name": "Changement de luminosité",
+ "transform": A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=prob),
+ "description": "Luminosité et contraste ±20%"
+ },
+ {
+ "name": "Flou gaussien léger",
+ "transform": A.GaussianBlur(blur_limit=(1, 3), p=prob*0.3),
+ "description": "Flou gaussien subtil"
+ },
+ {
+ "name": "Décalage couleur",
+ "transform": A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=15, val_shift_limit=10, p=prob),
+ "description": "Ajustement teinte/saturation/valeur"
+ },
+ {
+ "name": "Zoom aléatoire",
+ "transform": A.RandomScale(scale_limit=0.1, p=prob*0.5),
+ "description": "Zoom de -10% à +10%"
+ }
+ ]
+
+ # Sélectionner aléatoirement quelques transformations
+ num_transforms = random.randint(1, min(4, len(possible_transforms)))
+ selected_transforms = random.sample(possible_transforms, num_transforms)
+
+ # Créer le pipeline d'augmentation
+ augmentation_pipeline = []
+ for transform_info in selected_transforms:
+ augmentation_pipeline.append(transform_info["transform"])
+ transforms_applied.append(f"{transform_info['name']}: {transform_info['description']}")
+
+ # Appliquer les transformations
+ compose = A.Compose(augmentation_pipeline)
+
+ try:
+ augmented = compose(image=image_np)
+ augmented_image = augmented['image']
+ except Exception as e:
+ print(f"Erreur lors de l'augmentation: {e}")
+ # En cas d'erreur, retourner l'image originale
+ augmented_image = image_np.copy()
+ transforms_applied = ["Erreur - image originale conservée"]
+
+ return augmented_image, transforms_applied
+
+# Version simplifiée de preprocess_image_and_mask utilisant la nouvelle get_preprocessing_fn
+def preprocess_image_and_mask(image_path, mask_path=None, img_size=(512, 512),
+ encoder_name=None, id_to_group_mapping=None):
+ """
+ Version simplifiée utilisant get_preprocessing_fn pour la cohérence avec CityscapesDataset
+ """
+ import numpy as np
+ from PIL import Image
+ import torch
+
+ print(f"🔄 Préprocessing de l'image: {os.path.basename(image_path)}")
+
+ # Chargement des données
+ image_pil = Image.open(image_path).convert('RGB')
+ image_np = np.array(image_pil)
+ img_display = np.array(image_pil.resize(img_size, Image.BILINEAR)) # Pour affichage
+
+ mask_np = None
+ if mask_path and os.path.exists(mask_path):
+ print(f"🔄 Préprocessing du masque: {os.path.basename(mask_path)}")
+ mask_pil = Image.open(mask_path).convert('L')
+ mask_np = np.array(mask_pil)
+ print(f" Valeurs uniques avant mapping: {np.unique(mask_np)[:10]}...")
+
+ # Utiliser notre preprocessing unifié
+ preprocess_fn = get_preprocessing_fn(img_size=img_size, encoder=encoder_name)
+ img_preprocessed, mask_preprocessed = preprocess_fn(image_np, mask_np)
+
+ # Appliquer le mapping si fourni ET si on a un masque
+ if mask_preprocessed is not None and id_to_group_mapping is not None:
+ mask_preprocessed = map_mask_ids(mask_preprocessed, id_to_group_mapping)
+ print(f" Valeurs uniques après mapping: {torch.unique(mask_preprocessed)}")
+
+ return img_preprocessed, img_display, mask_preprocessed
+
+# Version mise à jour de run_inference_and_visualize
+def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config,
+ mask_path=None, save_dir=None, show_stats=True, show_plot=True):
+ """
+ Version simplifiée utilisant preprocess_image_and_mask
+ """
+ print(f"=== INFÉRENCE PYTORCH SUR {os.path.basename(image_path)} ===")
+
+ # Utiliser la version simplifiée du preprocessing
+ img_preprocessed, img_display, mask_gt = preprocess_image_and_mask(
+ image_path=image_path,
+ mask_path=mask_path,
+ img_size=img_size or (512, 512),
+ encoder_name=encoder_name,
+ id_to_group_mapping=mapping_config.get('id_to_group')
+ )
+
+ print("🔮 Exécution de l'inférence PyTorch...")
+ model.eval()
+ device = next(model.parameters()).device
+
+ with torch.no_grad():
+ # Ajouter dimension batch
+ img_batch = img_preprocessed.unsqueeze(0).to(device) # [1, 3, H, W]
+
+ # ⏱️ MESURE DU TEMPS D'INFÉRENCE
+ start_time = time.time()
+ predictions = model(img_batch) # [1, num_classes, H, W]
+ end_time = time.time()
+
+ inference_time = end_time - start_time
+
+ predicted_mask = torch.argmax(predictions[0], dim=0).cpu().numpy() # [H, W]
+
+ print(f"✅ Inférence terminée - Shape: {predicted_mask.shape}")
+ print(f"⏱️ Temps d'inférence: {inference_time*1000:.1f} ms ({inference_time:.4f} s)")
+
+ # [Le reste du code de visualisation reste identique]
+ # Visualisation
+ num_plots = 2 if mask_gt is None else 4
+ fig, axes = plt.subplots(1, num_plots, figsize=(5*num_plots, 5))
+ if num_plots == 1:
+ axes = [axes]
+
+ axes[0].imshow(img_display)
+ axes[0].set_title("Image Originale")
+ axes[0].axis('off')
+
+ predicted_colored = colorize_mask(predicted_mask, mapping_config['group_colors'])
+ axes[1].imshow(predicted_colored)
+ axes[1].set_title("Masque Prédit")
+ axes[1].axis('off')
+
+ if mask_gt is not None:
+ mask_gt_np = mask_gt.numpy() if isinstance(mask_gt, torch.Tensor) else mask_gt
+ gt_colored = colorize_mask(mask_gt_np, mapping_config['group_colors'])
+ axes[2].imshow(gt_colored)
+ axes[2].set_title("Masque de Vérité")
+ axes[2].axis('off')
+
+ # Visualisation des erreurs
+ diff_mask = (mask_gt_np != predicted_mask).astype(float)
+ axes[3].imshow(img_display)
+ axes[3].imshow(diff_mask, alpha=0.5, cmap='Reds')
+ axes[3].set_title("Erreurs (en rouge)")
+ axes[3].axis('off')
+
+ colors_normalized = mapping_config['group_colors'] / 255.0
+ custom_cmap = ListedColormap(colors_normalized)
+
+ legend_elements = []
+ for i, (name, color) in enumerate(zip(mapping_config['group_names'], colors_normalized)):
+ legend_elements.append(mpatches.Patch(color=color, label=f'{i}: {name}'))
+
+ fig.legend(handles=legend_elements,
+ loc='center left',
+ bbox_to_anchor=(1.01, 0.5),
+ fontsize=9,
+ frameon=True,
+ fancybox=True,
+ shadow=True)
+
+ plt.tight_layout()
+ fig.subplots_adjust(right=0.95)
+
+ if save_dir:
+ os.makedirs(save_dir, exist_ok=True)
+ img_name = os.path.splitext(os.path.basename(image_path))[0]
+ fig_path = os.path.join(save_dir, f'{img_name}_inference.png')
+ fig.savefig(fig_path, dpi=150, bbox_inches='tight')
+ print(f"💾 Résultat sauvegardé: {fig_path}")
+
+
+ # Statistiques
+ if show_stats:
+ print("\n📊 Statistiques du masque prédit:")
+ unique_classes, counts = np.unique(predicted_mask, return_counts=True)
+ for class_id, count in zip(unique_classes, counts):
+ if class_id < len(mapping_config['group_names']):
+ percentage = (count / predicted_mask.size) * 100
+ print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)")
+
+ if mask_gt is not None:
+ mask_gt_np = mask_gt.numpy() if isinstance(mask_gt, torch.Tensor) else mask_gt
+ print("\n📊 Statistiques du masque de vérité:")
+ unique_classes, counts = np.unique(mask_gt_np, return_counts=True)
+ for class_id, count in zip(unique_classes, counts):
+ if class_id < len(mapping_config['group_names']):
+ percentage = (count / mask_gt_np.size) * 100
+ print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)")
+
+ if show_plot:
+ plt.show()
+ return predicted_mask, inference_time
+ else:
+ return predicted_mask, inference_time, fig
+
+
+def load_model(run_id=None, experiment_name="OC Projet 9",
+ metric="test_mean_iou", top_n=1):
+ """
+ Charge un modèle PyTorch depuis MLflow avec plusieurs méthodes de sélection
+
+ Args:
+ run_id (str, optional): Run ID spécifique (prioritaire si fourni)
+ experiment_name (str): Nom de l'expérience MLflow
+ metric (str): Métrique pour classer les runs
+ top_n (int): Position dans le classement (1=meilleur, 2=deuxième, etc.)
+
+ Returns:
+ tuple: (model, source_run_id, encoder_name, script_img_size)
+ """
+
+ print(f"\n🔍 Chargement d'un modèle PyTorch depuis MLflow...")
+
+ client = mlflow.tracking.MlflowClient()
+
+ # 1. Sélection du run selon les paramètres
+ if run_id:
+ print(f"🎯 Mode run_id spécifique: {run_id}")
+ try:
+ run = mlflow.get_run(run_id)
+ source_run_id = run_id
+ print(f"✅ Run trouvé: {run.data.tags.get('mlflow.runName', 'Unknown')}")
+ except Exception as e:
+ raise RuntimeError(f"❌ Run {run_id} introuvable: {e}")
+
+ else:
+ print(f"🏆 Mode classement: top_{top_n} de '{metric}' dans '{experiment_name}'")
+ try:
+ # Récupérer l'expérience
+ experiment = client.get_experiment_by_name(experiment_name)
+ if experiment is None:
+ raise RuntimeError(f"❌ Expérience '{experiment_name}' non trouvée")
+
+ # Rechercher les runs avec la métrique, triés par ordre décroissant
+ filter_string = f"status = 'FINISHED' and metrics.{metric} >= 0"
+ order_by = f"metrics.{metric} DESC"
+
+ runs_df = mlflow.search_runs(
+ experiment_ids=[experiment.experiment_id],
+ filter_string=filter_string,
+ order_by=[order_by],
+ max_results=top_n
+ )
+
+ if runs_df.empty:
+ raise RuntimeError(f"❌ Aucun run avec '{metric}' trouvé dans '{experiment_name}'")
+
+ if len(runs_df) < top_n:
+ raise RuntimeError(f"❌ Seulement {len(runs_df)} runs trouvés, impossible d'obtenir le top_{top_n}")
+
+ # Prendre le run à la position top_n
+ selected_run = runs_df.iloc[top_n - 1]
+ source_run_id = selected_run['run_id']
+ metric_value = selected_run[f'metrics.{metric}']
+ run_name = selected_run.get('tags.mlflow.runName', 'Unknown')
+
+ print(f"✅ Run sélectionné (position #{top_n}):")
+ print(f" • Nom: {run_name}")
+ print(f" • Run ID: {source_run_id}")
+ print(f" • {metric}: {metric_value:.4f}")
+
+ # Afficher le contexte
+ print(f"📊 Contexte du classement:")
+ for i, (_, row) in enumerate(runs_df.head(min(5, len(runs_df))).iterrows()):
+ indicator = "👑" if i == (top_n - 1) else f"{i+1}."
+ name = row.get('tags.mlflow.runName', 'Unknown')[:30]
+ value = row[f'metrics.{metric}']
+ print(f" {indicator} {name}: {value:.4f}")
+
+ run = mlflow.get_run(source_run_id)
+
+ except Exception as e:
+ raise RuntimeError(f"❌ Erreur lors de la sélection: {e}")
+
+ # 2-5. [Reste du code identique - extraction params, détection variante, chargement...]
+ try:
+ params = run.data.params
+
+ encoder_name = params.get("encoder_name", None)
+ architecture = params.get("architecture", "FPN")
+ num_classes = int(params.get("num_classes", 8))
+ script_img_size_raw = params.get("script_img_size", None)
+ run_name = run.data.tags.get('mlflow.runName', 'Unknown')
+
+ # Parser script_img_size
+ script_img_size = None
+ if script_img_size_raw:
+ try:
+ script_img_size = ast.literal_eval(script_img_size_raw)
+ if isinstance(script_img_size, (tuple, list)) and len(script_img_size) == 2:
+ script_img_size = tuple(script_img_size)
+ except:
+ script_img_size = None
+
+ print(f"\n📋 Paramètres du modèle:")
+ print(f"🏗️ Architecture: {architecture}")
+ print(f"🔧 Encodeur: {encoder_name}")
+ print(f"📊 Nombre de classes: {num_classes}")
+ print(f"🖼️ Script img size: {script_img_size}")
+
+ except Exception as e:
+ raise RuntimeError(f"❌ Erreur lors de la récupération des paramètres: {e}")
+
+ # Détecter la variante SegFormer
+ def detect_segformer_variant_from_name(run_name, params):
+ segformer_pattern = r'SegFormer[_-]?b(\d+)'
+ match = re.search(segformer_pattern, run_name, re.IGNORECASE)
+ if match:
+ variant_num = match.group(1)
+ detected = f"mit_b{variant_num}"
+ print(f"🔍 Variante détectée: 'SegFormer_b{variant_num}' → {detected}")
+ return detected
+
+ if encoder_name and encoder_name.startswith("mit_b"):
+ print(f"🔍 Variante depuis paramètres: {encoder_name}")
+ return encoder_name
+
+ b_pattern = r'\bb(\d+)\b'
+ match = re.search(b_pattern, run_name, re.IGNORECASE)
+ if match:
+ variant_num = match.group(1)
+ detected = f"mit_b{variant_num}"
+ print(f"🔍 Pattern détecté: 'b{variant_num}' → {detected}")
+ return detected
+
+ print(f"🔍 Aucune variante détectée, utilisation de mit_b0")
+ return "mit_b0"
+
+ detected_variant = detect_segformer_variant_from_name(run_name, params)
+
+ # Télécharger et charger le modèle
+ with tempfile.TemporaryDirectory() as tmpdir:
+ try:
+ artifacts = client.list_artifacts(source_run_id)
+ model_artifact_candidates = [a.path for a in artifacts if a.path.endswith('.pth')]
+
+ if not model_artifact_candidates:
+ raise RuntimeError("❌ Aucun fichier .pth trouvé")
+
+ model_artifact = model_artifact_candidates[0]
+ print(f"⬇️ Téléchargement de {model_artifact}...")
+
+ local_path = mlflow.artifacts.download_artifacts(
+ run_id=source_run_id,
+ artifact_path=model_artifact,
+ dst_path=tmpdir
+ )
+
+ checkpoint = torch.load(local_path, map_location='cpu')
+ if isinstance(checkpoint, dict) and 'state_dict' in checkpoint:
+ state_dict = checkpoint['state_dict']
+ else:
+ state_dict = checkpoint
+
+ except Exception as e:
+ raise RuntimeError(f"❌ Erreur lors du téléchargement: {e}")
+
+ # Créer le modèle
+ try:
+ is_transformer = any('patch_embed' in key for key in state_dict.keys())
+
+ if is_transformer:
+ print(f"🔧 Création SegFormer ({detected_variant})")
+ model = smp.Segformer(encoder_name=detected_variant, classes=num_classes)
+ encoder_name = detected_variant
+ else:
+ print("🔧 Création modèle CNN")
+ if encoder_name is None or encoder_name == "None":
+ encoder_name = "efficientnet-b0"
+
+ if architecture.lower() == "fpn":
+ model = smp.FPN(encoder_name=encoder_name, classes=num_classes)
+ elif architecture.lower() == "unet":
+ model = smp.Unet(encoder_name=encoder_name, classes=num_classes)
+ elif architecture.lower() == "deeplabv3plus":
+ model = smp.DeepLabV3Plus(encoder_name=encoder_name, classes=num_classes)
+ else:
+ raise ValueError(f"Architecture {architecture} non supportée")
+
+ print("🔄 Chargement des poids...")
+ model.load_state_dict(state_dict, strict=True)
+ model.eval()
+ print("✅ Modèle chargé avec succès!")
+
+ except Exception as e:
+ try:
+ model.load_state_dict(state_dict, strict=False)
+ model.eval()
+ print("✅ Chargement partiel réussi!")
+ except Exception as e2:
+ raise RuntimeError(f"❌ Erreur lors du chargement: {e2}")
+
+ print(f"🎉 Modèle chargé depuis le run {source_run_id}")
+ return model, source_run_id, encoder_name, script_img_size
+
+
+def load_best_model_from_registry(model_name):
+ """
+ Charge le meilleur modèle depuis le MLflow Model Registry
+ (Version refactorisée - wrapper autour de load_model)
+
+ Args:
+ model_name (str): Nom du modèle dans le Registry
+
+ Returns:
+ tuple: (model, source_run_id, encoder_name, script_img_size)
+ """
+ print(f"🏛️ Récupération du modèle '{model_name}' depuis le Registry...")
+
+ client = mlflow.tracking.MlflowClient()
+
+ try:
+ # Récupérer la version en Production
+ versions = client.search_model_versions(f"name='{model_name}'")
+ prod_versions = [v for v in versions if v.current_stage == "Production"]
+
+ if not prod_versions:
+ raise RuntimeError(f"❌ Aucune version en Production trouvée pour {model_name}")
+
+ latest_prod = max(prod_versions, key=lambda v: int(v.version))
+ print(f"📦 Version Registry: {latest_prod.version} (Stage: {latest_prod.current_stage})")
+
+ # Récupérer le source_run_id depuis le tag du Registry
+ source_run_id = latest_prod.tags.get("source_run_id")
+ if not source_run_id:
+ raise RuntimeError(f"❌ Tag 'source_run_id' absent pour la version {latest_prod.version}")
+
+ print(f"🔗 Source run détecté: {source_run_id}")
+
+ except Exception as e:
+ raise RuntimeError(f"❌ Erreur lors de l'accès au Registry: {e}")
+
+ # Déléguer le chargement à load_model avec le run_id trouvé
+ print("➡️ Délégation vers load_model...")
+ return load_model(run_id=source_run_id)
+
+
+# Fonctions de convenance
+def load_best_model(experiment_name="OC Projet 9", metric="test_mean_iou"):
+ """Charge le meilleur modèle selon une métrique"""
+ return load_model(experiment_name=experiment_name, metric=metric, top_n=1)
+
+def load_second_best_model(experiment_name="OC Projet 9", metric="test_mean_iou"):
+ """Charge le deuxième meilleur modèle selon une métrique"""
+ return load_model(experiment_name=experiment_name, metric=metric, top_n=2)
+
+def load_model_by_run_id(run_id):
+ """Charge un modèle par run_id spécifique"""
return load_model(run_id=run_id)
\ No newline at end of file