From 6bdda85438ea540aedc61765b837f1d5f67521e2 Mon Sep 17 00:00:00 2001 From: GrgMdmn Date: Mon, 10 Nov 2025 16:40:12 +0100 Subject: [PATCH] Initial commit of scripts --- .dockerignore | 212 +- .env.example | 82 +- Dockerfile | 88 +- README.MD | 230 +- app/README.MD | 216 +- app/main.py | 1666 ++++---- app/requirements.txt | 142 +- app/streamlit_app.py | 3778 ++++++++--------- cityscapes_config.json | 126 +- docker-compose.yml.example | 94 +- ...tation Project - Dashboard Specs html code | 458 +- docs/note_methodologique.md | 588 +-- docs/plan_previsionnel.md | 152 +- nginx.conf | 114 +- notebooks/requirements.txt | 1342 +++--- start.sh | 104 +- utils/utils.py | 1314 +++--- 17 files changed, 5353 insertions(+), 5353 deletions(-) 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égorieExigences ProjetImplé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égorieExigences ProjetImplé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 - -![mIoU + Accuracy](mIoU_accuracy.png) -*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 - -![IoU Classe et Δ IoU par Classe](iou_delta_iou_per_class.png) -*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 + +![mIoU + Accuracy](mIoU_accuracy.png) +*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 + +![IoU Classe et Δ IoU par Classe](iou_delta_iou_per_class.png) +*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