minor changes

This commit is contained in:
Grégoire Mureau 2025-11-11 08:39:23 +01:00
parent 56a77bf6f7
commit d4f513c2f2
60 changed files with 36213 additions and 0 deletions

106
.dockerignore Normal file
View file

@ -0,0 +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

41
.env.example Normal file
View file

@ -0,0 +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

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
./mlruns*

44
Dockerfile Normal file
View file

@ -0,0 +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"]

116
README.MD Normal file
View file

@ -0,0 +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)
---
**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.

108
app/README.MD Normal file
View file

@ -0,0 +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

834
app/main.py Normal file
View file

@ -0,0 +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é"""
return get_accessibility_info()

71
app/requirements.txt Normal file
View file

@ -0,0 +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

1889
app/streamlit_app.py Normal file

File diff suppressed because it is too large Load diff

63
cityscapes_config.json Normal file
View file

@ -0,0 +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"]
}

View file

@ -0,0 +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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,230 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cahier des Charges Dashboard</title>
<style>
body {
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 15px 35px rgba(0,0,0,0.1);
max-width: 950px;
width: 100%;
}
.title {
text-align: center;
color: #2c3e50;
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
color: #7f8c8d;
font-size: 16px;
margin-bottom: 25px;
font-style: italic;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
font-size: 14px;
}
th {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
font-weight: bold;
font-size: 13px;
text-transform: uppercase;
padding: 15px 12px;
text-align: center;
border: 2px solid white;
}
td {
padding: 12px;
border: 2px solid #ecf0f1;
vertical-align: top;
}
.category {
font-weight: bold;
text-align: center;
color: white;
font-size: 13px;
width: 20%;
}
.exploratoire {
background: linear-gradient(135deg, #e74c3c, #c0392b);
}
.comparaison {
background: linear-gradient(135deg, #f39c12, #d68910);
}
.accessibilite {
background: linear-gradient(135deg, #27ae60, #229954);
}
.technique {
background: linear-gradient(135deg, #8e44ad, #7d3c98);
}
.requirement {
font-size: 13px;
line-height: 1.4;
color: #2c3e50;
}
.implementation {
font-size: 13px;
line-height: 1.4;
color: #2c3e50;
font-style: italic;
}
.tech-stack {
margin-top: 20px;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 10px;
padding: 20px;
border-left: 5px solid #6c757d;
}
.tech-stack h3 {
color: #495057;
margin-top: 0;
font-size: 18px;
}
.tech-items {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.tech-item {
background: #6c757d;
color: white;
padding: 5px 12px;
border-radius: 15px;
font-size: 12px;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<div class="title">Cahier des Charges Dashboard Interactif</div>
<div class="subtitle">Spécifications et implémentation - Projet DataSpace</div>
<table>
<thead>
<tr>
<th style="width: 20%;">Catégorie</th>
<th style="width: 45%;">Exigences Projet</th>
<th style="width: 35%;">Implémentation</th>
</tr>
</thead>
<tbody>
<tr>
<td class="category exploratoire">ANALYSE EXPLORATOIRE</td>
<td class="requirement">
• Visualisation contenu dataset<br>
• Analyses statistiques describe()<br>
• 2+ graphiques interactifs<br>
• Distribution des classes
</td>
<td class="implementation">
Streamlit + Plotly interactif<br>
Métriques dataset Cityscapes<br>
Distribution pixel-wise classes<br>
Graphiques responsive
</td>
</tr>
<tr>
<td class="category comparaison">COMPARAISON MODÈLES</td>
<td class="requirement">
• Sélection données entrée<br>
• Upload images personnalisées<br>
• Prédiction temps réel<br>
• Affichage résultats comparatifs
</td>
<td class="implementation">
Interface upload + dropdown<br>
Inférence FPN vs SegFormer<br>
Visualisation masques côte-à-côte<br>
Métriques temps d'exécution
</td>
</tr>
<tr>
<td class="category accessibilite">ACCESSIBILITÉ WCAG</td>
<td class="requirement">
• Critères WCAG essentiels<br>
• Contraste couleurs<br>
• Navigation clavier<br>
• Labels descriptifs
</td>
<td class="implementation">
Palette couleurs accessibles<br>
Alt-text images générées<br>
Semantic HTML structure<br>
Focus indicators
</td>
</tr>
<tr>
<td class="category technique">DÉPLOIEMENT CLOUD</td>
<td class="requirement">
• Application déployée online<br>
• Interface responsive<br>
• Performance acceptable<br>
• Code source accessible
</td>
<td class="implementation">
Serveur NAS personnel (Intel N100)<br>
Streamlit containerisé<br>
Optimisation modèles CPU<br>
Repository GitHub documenté
</td>
</tr>
</tbody>
</table>
<div class="tech-stack">
<h3>🛠️ Stack Technique</h3>
<div class="tech-items">
<div class="tech-item">FastAPI</div>
<div class="tech-item">Streamlit</div>
<div class="tech-item">PyTorch</div>
<div class="tech-item">Plotly</div>
<div class="tech-item">PIL/OpenCV</div>
<div class="tech-item">Docker</div>
<div class="tech-item">Intel N100</div>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
docs/mIoU_accuracy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 MiB

295
docs/note_methodologique.md Normal file
View file

@ -0,0 +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.
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.

Binary file not shown.

BIN
docs/p8_technical_note.pdf Normal file

Binary file not shown.

77
docs/plan_previsionnel.md Normal file
View file

@ -0,0 +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.
---
**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.

BIN
docs/plan_previsionnel.pdf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

57
nginx.conf Normal file
View file

@ -0,0 +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;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

File diff suppressed because one or more lines are too long

671
notebooks/requirements.txt Normal file
View file

@ -0,0 +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

52
start.sh Normal file
View file

@ -0,0 +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;'

0
utils/__init__.py Normal file
View file

658
utils/utils.py Normal file
View file

@ -0,0 +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"""
return load_model(run_id=run_id)