Docker updated. some WCAG rules added

This commit is contained in:
Grégoire Mureau 2025-09-03 20:50:24 +02:00
parent f598e16752
commit 142623267d
28 changed files with 3441 additions and 348 deletions

View file

@ -24,6 +24,7 @@ COPY start.sh ./start.sh
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

111
README.MD
View file

@ -1,3 +1,112 @@
# Projet 9
Documentation à venir
Documentation à venir
# 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

View file

@ -62,6 +62,7 @@ from utils.utils import (
# 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"
@ -72,10 +73,10 @@ class ModelInfo(BaseModel):
encoder_name: str
input_size: tuple
rank: int
architecture: Optional[str] = None # Ajout architecture
test_mean_iou: Optional[float] = None # Ajout mIoU
test_accuracy: Optional[float] = None # Ajout accuracy
class_ious: Optional[dict] = None # Ajout IoU par classe
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
@ -101,6 +102,21 @@ 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
@ -209,6 +225,34 @@ def initialize_models():
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(
@ -245,16 +289,228 @@ def health_check():
"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")
# Couleurs accessibles pour les classes
accessible_class_colors = [
[0, 102, 204], # primary_blue
[204, 0, 0], # primary_red
[0, 102, 0], # primary_green
[204, 102, 0], # primary_orange
[102, 51, 153], # secondary_purple
[0, 102, 102], # secondary_teal
[51, 51, 51], # neutral_dark
[102, 102, 102] # neutral_medium
]
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": mapping_config['group_colors'].tolist()
"class_colors": accessible_class_colors,
"accessibility_info": get_accessibility_info(),
"preprocessing_resolution": best_model_info.get("input_size", (512, 512)) # Ajout pour clarté
}
@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)
@ -299,9 +555,6 @@ def get_sample_image(filename: str):
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error serving image: {str(e)}")
class SampleImageRequest(BaseModel):
filename: str
@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:
@ -405,6 +658,7 @@ def compare_sample_image(request: SampleImageRequest):
# 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):
@ -523,6 +777,7 @@ def compare_uploaded_image(file: UploadFile = File(...)):
# 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):
@ -563,4 +818,10 @@ def get_metrics():
"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()

File diff suppressed because it is too large Load diff

755
app_2/main.py Normal file
View file

@ -0,0 +1,755 @@
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...")
mapping_config = load_cityscapes_config(CITYSCAPES_CONFIG_PATH, verbose=False)
print("✅ Configuration Cityscapes chargée")
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
# 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()
}
@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")
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": mapping_config['group_colors'].tolist()
}
@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")
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)
# Appliquer le preprocessing standard (resize + normalisation)
preprocess_fn = get_preprocessing_fn(img_size=(512, 512), 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
original_resized = np.array(original_pil.resize((512, 512)))
result = PreprocessingResult(
success=True,
message="Preprocessing completed successfully",
original_image=img_to_base64(original_resized),
normalized_image=img_to_base64(normalized_display),
transformations_applied=["Resize (512x512)", "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("/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()
}

70
app_2/requirements.txt Normal file
View file

@ -0,0 +1,70 @@
# =============================================================================
# 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

913
app_2/streamlit_app.py Normal file
View file

@ -0,0 +1,913 @@
import os
import sys
import requests
import streamlit as st
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import io
import base64
import pickle
# Configuration de l'API
API_BASE_URL = os.getenv("MULTISEG_API_BASE_URL", "http://127.0.0.1:8080/api")
# Import des fonctions utils pour l'affichage local
sys.path.append('/app')
sys.path.append('.')
from utils.utils import colorize_mask
# Configuration de la page
st.set_page_config(
page_title="Street Vision - Segmentation Comparison",
page_icon="🚗",
layout="wide"
)
# CSS personnalisé mis à jour
st.markdown("""
<style>
.main-header {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
border-radius: 10px;
margin-bottom: 2rem;
color: white;
text-align: center;
}
.comparison-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 1.5rem;
border-radius: 10px;
border-left: 4px solid #667eea;
margin: 1rem 0;
color: #2c3e50;
font-weight: 500;
}
.speed-winner {
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
border-left-color: #28a745;
}
.speed-slower {
background: linear-gradient(135deg, #f8d7da 0%, #f1b0b7 100%);
border-left-color: #dc3545;
}
.metric-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 1rem;
border-radius: 8px;
border-left: 4px solid #667eea;
margin: 0.5rem 0;
color: #2c3e50;
font-weight: 500;
}
.dataset-info {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 10px;
margin: 1rem 0;
border-left: 4px solid #17a2b8;
}
.preprocessing-card {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
padding: 1rem;
border-radius: 8px;
margin: 0.5rem 0;
border-left: 4px solid #2196f3;
}
</style>
""", unsafe_allow_html=True)
# En-tête principal
st.markdown("""
<div class="main-header">
<h1>🚗 Street Vision - Comparaison de Modèles</h1>
<p>Évaluation comparative SegFormer vs FPN sur le dataset Cityscapes</p>
</div>
""", unsafe_allow_html=True)
# Fonctions utilitaires
def get_models_info():
"""Récupère les informations des deux modèles"""
try:
response = requests.get(f"{API_BASE_URL}/models")
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur lors de la récupération des infos modèles: {response.status_code}")
return None
except Exception as e:
st.error(f"Erreur de connexion à l'API: {e}")
return None
def get_dataset_info():
"""Récupère les informations sur le dataset"""
try:
response = requests.get(f"{API_BASE_URL}/dataset-info")
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur lors de la récupération des infos dataset: {response.status_code}")
return None
except Exception as e:
st.error(f"Erreur de connexion à l'API: {e}")
return None
def get_train_images():
"""Récupère la liste des images d'entraînement"""
try:
response = requests.get(f"{API_BASE_URL}/train-images")
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur lors de la récupération des images: {response.status_code}")
return None
except Exception as e:
st.error(f"Erreur de connexion à l'API: {e}")
return None
def get_class_repartition():
"""Récupère le graphique de répartition des classes"""
try:
response = requests.get(f"{API_BASE_URL}/class-repartition", timeout=10)
if response.status_code == 200:
return response.content
else:
return None
except Exception as e:
st.warning(f"Graphique de répartition non disponible: {e}")
return None
def preprocess_train_image(filename, apply_augmentation=False):
"""Lance le preprocessing sur une image d'entraînement"""
try:
data = {
"filename": filename,
"apply_augmentation": apply_augmentation
}
response = requests.post(
f"{API_BASE_URL}/preprocess-train-image",
json=data,
headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur API: {response.status_code} - {response.text}")
return None
except Exception as e:
st.error(f"Erreur lors du preprocessing: {e}")
return None
def get_sample_images():
"""Récupère la liste des images d'exemple"""
try:
response = requests.get(f"{API_BASE_URL}/sample-images")
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur lors de la récupération des images: {response.status_code}")
return None
except Exception as e:
st.error(f"Erreur de connexion à l'API: {e}")
return None
def compare_sample_image(filename):
"""Lance une comparaison sur une image d'exemple"""
try:
data = {"filename": filename}
response = requests.post(
f"{API_BASE_URL}/compare-sample",
json=data,
headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur API: {response.status_code} - {response.text}")
return None
except Exception as e:
st.error(f"Erreur lors de la comparaison: {e}")
return None
def compare_uploaded_image(uploaded_file):
"""Lance une comparaison sur une image uploadée"""
try:
files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)}
response = requests.post(f"{API_BASE_URL}/compare-upload", files=files)
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur API: {response.status_code} - {response.text}")
return None
except Exception as e:
st.error(f"Erreur lors de la comparaison: {e}")
return None
def display_comparison_results(result, models_info):
"""Affiche les résultats de comparaison entre les deux modèles"""
if not result or not result['success']:
st.error("Aucun résultat de comparaison disponible")
return
# Informations des modèles
st.markdown("### 🏆 Modèles Comparés")
col1, col2 = st.columns(2)
with col1:
model1 = result['model1_info']
# Formater les métriques séparément
miou1 = f"{model1.get('test_mean_iou'):.4f}" if model1.get('test_mean_iou') is not None else 'N/A'
acc1 = f"{model1.get('test_accuracy'):.4f}" if model1.get('test_accuracy') is not None else 'N/A'
st.markdown(f"""
<div class="comparison-card">
<h4>🥇 Modèle #1</h4>
<strong>{model1.get('architecture', 'N/A')} - {model1['encoder_name']}</strong><br>
📐 Taille: {model1['input_size']}<br>
📊 mIoU: {miou1}<br>
🎯 Accuracy: {acc1}
</div>
""", unsafe_allow_html=True)
with col2:
model2 = result['model2_info']
# Formater les métriques séparément
miou2 = f"{model2.get('test_mean_iou'):.4f}" if model2.get('test_mean_iou') is not None else 'N/A'
acc2 = f"{model2.get('test_accuracy'):.4f}" if model2.get('test_accuracy') is not None else 'N/A'
st.markdown(f"""
<div class="comparison-card">
<h4>🥈 Modèle #2</h4>
<strong>{model2.get('architecture', 'N/A')} - {model2['encoder_name']}</strong><br>
📐 Taille: {model2['input_size']}<br>
📊 mIoU: {miou2}<br>
🎯 Accuracy: {acc2}
</div>
""", unsafe_allow_html=True)
# Comparaison des temps d'inférence
st.markdown("### ⏱️ Comparaison des Performances")
speed_comp = result['speed_comparison']
model1_time = result['model1_inference_time'] * 1000
model2_time = result['model2_inference_time'] * 1000
col1, col2, col3 = st.columns(3)
with col1:
winner_class = "speed-winner" if speed_comp['faster_model'] == model1['encoder_name'] else "speed-slower"
st.markdown(f"""
<div class="comparison-card {winner_class}">
<h5>🥇 {model1.get('architecture', 'N/A')} - {model1['encoder_name']}</h5>
<strong>{model1_time:.1f} ms</strong>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown(f"""
<div class="comparison-card">
<h5>⚡ Comparaison</h5>
<strong>{speed_comp['faster_model']}</strong><br>
est {speed_comp['speedup_ratio']}x plus rapide<br>
<small>Différence: {speed_comp['time_difference_ms']:.1f} ms</small>
</div>
""", unsafe_allow_html=True)
with col3:
winner_class = "speed-winner" if speed_comp['faster_model'] == model2['encoder_name'] else "speed-slower"
st.markdown(f"""
<div class="comparison-card {winner_class}">
<h5>🥈 {model2.get('architecture', 'N/A')} - {model2['encoder_name']}</h5>
<strong>{model2_time:.1f} ms</strong>
</div>
""", unsafe_allow_html=True)
# Affichage des segmentations
if result.get('figure_data'):
st.markdown("### 🎨 Résultats de Segmentation")
try:
fig_bytes = base64.b64decode(result['figure_data'])
fig = pickle.loads(fig_bytes)
st.pyplot(fig)
plt.close(fig)
except Exception as e:
st.error(f"Erreur lors de l'affichage: {e}")
# Comparaison des statistiques
st.markdown("### 📊 Répartition des classes par modèle")
col1, col2 = st.columns(2)
with col1:
if result.get('model1_stats'):
display_model_stats_chart(result['model1_stats'], models_info, f"{model1.get('architecture', 'N/A')} - {model1['encoder_name']}")
with col2:
if result.get('model2_stats'):
display_model_stats_chart(result['model2_stats'], models_info, f"{model2.get('architecture', 'N/A')} - {model2['encoder_name']}")
# Analyse comparative des classes
if result.get('model1_stats') and result.get('model2_stats'):
display_class_comparison(result['model1_stats'], result['model2_stats'], model1, model2, models_info)
def display_model_stats_chart(stats, models_info, title):
"""Affiche les statistiques d'un modèle sous forme de graphique"""
if not stats:
st.warning("Aucune statistique disponible")
return
# Utiliser l'ordre des classes du modèle pour la cohérence
if models_info and 'class_names' in models_info:
# Ordonner selon l'ordre du modèle
ordered_class_names = [name for name in models_info['class_names'] if name in stats]
else:
# Fallback : ordre alphabétique
ordered_class_names = sorted(stats.keys())
percentages = [stats[name]['percentage'] for name in ordered_class_names]
# Récupérer les couleurs des classes
colors = []
if models_info and 'class_colors' in models_info and 'class_names' in models_info:
model_class_names = models_info['class_names']
model_class_colors = models_info['class_colors']
for class_name in ordered_class_names:
try:
idx = model_class_names.index(class_name)
color = model_class_colors[idx]
colors.append([c/255.0 for c in color])
except (ValueError, IndexError):
colors.append([0.5, 0.5, 0.5])
else:
colors = plt.cm.Set3(np.linspace(0, 1, len(ordered_class_names)))
# Créer le graphique
fig, ax = plt.subplots(figsize=(8, 5))
bars = ax.bar(ordered_class_names, percentages, color=colors, alpha=0.8, edgecolor='white', linewidth=1)
ax.set_xlabel('Classes de Segmentation', fontsize=10, fontweight='bold')
ax.set_ylabel('Pourcentage (%)', fontsize=10, fontweight='bold')
ax.set_title(title, fontsize=11, fontweight='bold', pad=15)
# Rotation des labels selon le nombre de classes
if len(ordered_class_names) <= 4:
plt.xticks(rotation=0, ha='center', fontsize=9)
elif len(ordered_class_names) <= 6:
plt.xticks(rotation=30, ha='right', fontsize=8)
else:
plt.xticks(rotation=45, ha='right', fontsize=8)
plt.yticks(fontsize=9)
# Valeurs sur les barres
for bar, percentage in zip(bars, percentages):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height + max(percentages) * 0.01,
f'{percentage:.1f}%',
ha='center', va='bottom', fontsize=8, fontweight='bold')
plt.tight_layout()
ax.grid(axis='y', alpha=0.2, linestyle='--')
ax.set_axisbelow(True)
# Style des axes
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# Couleur de fond
fig.patch.set_facecolor('white')
ax.set_facecolor('#fafafa')
st.pyplot(fig)
plt.close(fig)
def display_class_comparison(stats1, stats2, model1, model2, models_info):
"""Affiche une comparaison détaillée des classes entre les deux modèles"""
st.markdown("### 🔍 Analyse Comparative par Classe")
# Utiliser l'ordre des classes du modèle pour la cohérence
if models_info and 'class_names' in models_info:
# Combiner toutes les classes présentes en conservant l'ordre du modèle
all_classes_in_stats = set(stats1.keys()) | set(stats2.keys())
ordered_all_classes = [name for name in models_info['class_names'] if name in all_classes_in_stats]
else:
# Fallback : ordre alphabétique
all_classes = set(stats1.keys()) | set(stats2.keys())
ordered_all_classes = sorted(all_classes)
comparison_data = []
for class_name in ordered_all_classes:
pct1 = stats1.get(class_name, {}).get('percentage', 0)
pct2 = stats2.get(class_name, {}).get('percentage', 0)
diff = pct1 - pct2
comparison_data.append({
'class': class_name,
'model1_pct': pct1,
'model2_pct': pct2,
'difference': diff
})
# Créer un graphique comparatif des barres
st.markdown("**📊 Comparaison des classes entre les modèles:**")
# Préparation des données
class_names = [data['class'] for data in comparison_data]
model1_values = [data['model1_pct'] for data in comparison_data]
model2_values = [data['model2_pct'] for data in comparison_data]
# Récupérer les couleurs des classes
colors = []
if models_info and 'class_colors' in models_info and 'class_names' in models_info:
model_class_names = models_info['class_names']
model_class_colors = models_info['class_colors']
for class_name in class_names:
try:
idx = model_class_names.index(class_name)
color = model_class_colors[idx]
hex_color = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
colors.append(hex_color)
except (ValueError, IndexError):
colors.append("#999999")
else:
colors = ['#999999'] * len(class_names)
# Créer le graphique comparatif
fig, ax = plt.subplots(figsize=(12, 7))
x = np.arange(len(class_names)) # Positions des barres
width = 0.35 # Largeur des barres
bars1 = ax.bar(x - width/2, model1_values, width, label=f"{model1.get('architecture', 'N/A')} - {model1['encoder_name']}",
color=colors, alpha=0.8, edgecolor='white')
# Barres hachurées pour le modèle 2
bars2 = ax.bar(x + width/2, model2_values, width, label=f"{model2.get('architecture', 'N/A')} - {model2['encoder_name']}",
color=colors, alpha=0.6, edgecolor='white', hatch='///')
ax.set_xlabel('Classes', fontsize=11, fontweight='bold')
ax.set_ylabel('Pourcentage (%)', fontsize=11, fontweight='bold')
ax.set_title('Comparaison des classes entre les deux modèles', fontsize=13, fontweight='bold', pad=15)
ax.set_xticks(x)
# Rotation des labels selon le nombre de classes
if len(class_names) <= 4:
ax.set_xticklabels(class_names, fontsize=10)
elif len(class_names) <= 6:
ax.set_xticklabels(class_names, rotation=30, ha='right', fontsize=9)
else:
ax.set_xticklabels(class_names, rotation=45, ha='right', fontsize=9)
ax.legend(loc='upper right', fontsize=10)
ax.grid(axis='y', alpha=0.2, linestyle='--')
ax.set_axisbelow(True)
# Style des axes
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# Valeurs sur les barres
for bar1, bar2, val1, val2 in zip(bars1, bars2, model1_values, model2_values):
height1 = bar1.get_height()
height2 = bar2.get_height()
ax.text(bar1.get_x() + bar1.get_width()/2., height1 + 0.5,
f'{val1:.1f}%', ha='center', va='bottom', fontsize=8, fontweight='bold')
ax.text(bar2.get_x() + bar2.get_width()/2., height2 + 0.5,
f'{val2:.1f}%', ha='center', va='bottom', fontsize=8, fontweight='bold')
plt.tight_layout()
st.pyplot(fig)
plt.close(fig)
# Tableau détaillé avec un beau format
st.markdown("**🔎 Détails des différences par classe:**")
# Préparer les données pour le tableau
table_data = []
for i, data in enumerate(comparison_data):
class_color = colors[i]
diff_value = data['difference']
# Classification de la différence
if abs(diff_value) < 1:
diff_category = "Similaire"
diff_emoji = "🟢"
elif abs(diff_value) < 5:
diff_category = "Différence modérée"
diff_emoji = "🟡"
else:
diff_category = "Différence importante"
diff_emoji = "🔴"
table_data.append({
"Classe": data['class'],
f"🥇 {model1.get('architecture', 'N/A')[:15]} (%)": f"{data['model1_pct']:.1f}%",
f"🥈 {model2.get('architecture', 'N/A')[:15]} (%)": f"{data['model2_pct']:.1f}%",
"Différence": f"{diff_value:+.1f}%",
"Catégorie": f"{diff_emoji} {diff_category}"
})
# Créer le DataFrame et l'afficher
df = pd.DataFrame(table_data)
# Utiliser st.dataframe avec style personnalisé
st.dataframe(
df,
use_container_width=True,
hide_index=True,
column_config={
"Classe": st.column_config.TextColumn("🎯 Classe", width="medium"),
f"🥇 {model1.get('architecture', 'N/A')[:15]} (%)": st.column_config.TextColumn(width="medium"),
f"🥈 {model2.get('architecture', 'N/A')[:15]} (%)": st.column_config.TextColumn(width="medium"),
"Différence": st.column_config.TextColumn("📊 Différence", width="small"),
"Catégorie": st.column_config.TextColumn("📈 Évaluation", width="medium")
}
)
# Onglets principaux
main_tab1, main_tab2 = st.tabs(["📊 Présentation Jeu de données Cityscapes", "🔄 Comparaison de performances"])
# ===== ONGLET 1: PRÉSENTATION DATASET =====
with main_tab1:
st.markdown("## 📊 Analyse Exploratoire - Dataset Cityscapes")
# Informations générales sur le dataset
dataset_info = get_dataset_info()
if dataset_info:
# Section méthodologique
st.markdown("""
### 🎯 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
col1, col2 = st.columns(2)
with col1:
st.markdown("**🗂️ Composition du dataset :**")
splits_info = dataset_info.get('splits', {})
for split_name, split_info in splits_info.items():
st.write(f"• **{split_name.title()}** : {split_info['images']} images - {split_info['usage']}")
with col2:
st.markdown("**🔬 Utilisation pour ce projet :**")
project_splits = dataset_info.get('project_splits', {})
for split_name, split_info in project_splits.items():
st.write(f"• **{split_name.title()}** : {split_info['images']} images ({split_info['percentage']}%) - {split_info['source']}")
# Architecture des classes
st.markdown("### 🏗️ Architecture de classes hiérarchique")
st.markdown("""
Le dataset organise 34 classes détaillées en **8 méta-classes** pour la segmentation :
| Méta-classe | Description | Importance |
|-------------|-------------|------------|
| **Flat** | Route, trottoir, parkings | Navigation de base |
| **Human** | Piéton, cycliste | **Sécurité critique** |
| **Vehicle** | Voiture, camion, bus, train | Obstacles dynamiques |
| **Construction** | Bâtiment, mur, clôture, pont | Structure urbaine |
| **Object** | Poteau, feu, panneau | Signalétique |
| **Nature** | Végétation, terrain | Environnement |
| **Sky** | Ciel | Contexte |
| **Void** | Zones non-définies | Exclusion |
""")
# Graphique de répartition des classes
st.markdown("### 📊 Répartition des classes dans le dataset d'entraînement")
class_repartition_data = get_class_repartition()
if class_repartition_data:
try:
class_repartition_image = Image.open(io.BytesIO(class_repartition_data))
st.image(
class_repartition_image,
caption="Distribution des classes dans les images d'entraînement Cityscapes",
use_container_width=True
)
except Exception as e:
st.warning("Impossible d'afficher le graphique de répartition des classes")
else:
st.info("Graphique de répartition des classes non disponible")
# Spécificités techniques
st.markdown("""
### ⚙️ Spécificités techniques
- **Résolutions d'acquisition** : Données natives 2048×1024 pixels
- **Conditions d'acquisition** : Conditions météo bonnes à moyennes, pas de conditions adverses (pluie, neige)
- **Diversité géographique** : Villes allemandes et suisses (architectures variées)
- **Annotations pixel-perfect** : Masques de segmentation précis au pixel près
Ce dataset permet d'évaluer la capacité des modèles à comprendre les scènes urbaines complexes, élément fondamental pour les applications de conduite autonome et de surveillance urbaine intelligente.
""")
# Section preprocessing des images
st.markdown("---")
st.markdown("### 🔧 Exemple de Preprocessing d'Images")
# Récupérer les images d'entraînement disponibles
train_data = get_train_images()
if train_data and train_data['images']:
# Sélecteur d'image
image_options = {img['display_name']: img['filename'] for img in train_data['images']}
selected_display_name = st.selectbox(
"Choisissez une image d'entraînement pour voir les transformations:",
options=list(image_options.keys()),
help=f"{train_data['total_count']} images disponibles"
)
selected_filename = image_options[selected_display_name]
# Options de preprocessing
col1, col2 = st.columns(2)
with col1:
show_augmentation = st.checkbox(
"Inclure l'augmentation aléatoire",
help="Affiche un exemple d'augmentation de données utilisée pendant l'entraînement"
)
with col2:
if st.button("🔧 Appliquer le Preprocessing", type="primary"):
with st.spinner("Application des transformations..."):
result = preprocess_train_image(selected_filename, show_augmentation)
if result and result['success']:
st.success("Preprocessing terminé!")
# Affichage des images transformées
st.markdown("#### Résultats des transformations :")
cols = st.columns(3 if show_augmentation else 2)
# Image originale
with cols[0]:
st.markdown("**Image Originale (redimensionnée)**")
if result.get('original_image'):
original_img_data = base64.b64decode(result['original_image'])
original_img = Image.open(io.BytesIO(original_img_data))
st.image(original_img, use_container_width=True)
# Image normalisée
with cols[1]:
st.markdown("**Après Normalisation**")
if result.get('normalized_image'):
normalized_img_data = base64.b64decode(result['normalized_image'])
normalized_img = Image.open(io.BytesIO(normalized_img_data))
st.image(normalized_img, use_container_width=True)
# Image avec augmentation (si demandée)
if show_augmentation and result.get('augmented_image'):
with cols[2]:
st.markdown("**Avec Augmentation Aléatoire**")
augmented_img_data = base64.b64decode(result['augmented_image'])
augmented_img = Image.open(io.BytesIO(augmented_img_data))
st.image(augmented_img, use_container_width=True)
# Explications des transformations
if result.get('transformations_applied'):
st.markdown("#### 📋 Transformations appliquées :")
for i, transform in enumerate(result['transformations_applied'], 1):
st.markdown(f"{i}. {transform}")
# Explications détaillées
with st.expander("ℹ️ Détails des transformations"):
st.markdown("""
**Normalisation standard :**
- Redimensionnement à 512×512 pixels
- Normalisation des valeurs de pixels de [0-255] vers [0-1]
- Cette normalisation uniforme est appliquée à tous les modèles (paramètre encoder non utilisé)
**Augmentation de données (si activée) :**
- Transformations aléatoires appliquées pendant l'entraînement
- Permet d'améliorer la robustesse et la généralisation du modèle
- Chaque image d'entraînement peut être transformée différemment à chaque époque
""")
else:
st.error("Erreur lors du preprocessing")
else:
st.warning("Aucune image d'entraînement disponible pour la démonstration")
# ===== ONGLET 2: COMPARAISON DE PERFORMANCES =====
with main_tab2:
# Variables de session pour les résultats
if "sample_comparison_result" not in st.session_state:
st.session_state.sample_comparison_result = None
if "upload_comparison_result" not in st.session_state:
st.session_state.upload_comparison_result = None
def reset_sample_comparison():
st.session_state.sample_comparison_result = None
def reset_upload_comparison():
st.session_state.upload_comparison_result = None
# Sous-onglets pour la comparaison
sub_tab1, sub_tab2 = st.tabs(["🏙️ Images Test Cityscapes", "📤 Upload Personnel"])
# SUB-TAB 1: Images Test Cityscapes
with sub_tab1:
st.markdown("## 🏙️ Comparaison sur Images Test Cityscapes")
st.markdown("Comparez les performances des deux modèles sur des images Cityscapes avec masques de vérité.")
# Récupérer les images d'exemple
sample_data = get_sample_images()
if sample_data and sample_data['images']:
# Sélecteur d'image
image_options = {img['display_name']: img['filename'] for img in sample_data['images']}
selected_display_name = st.selectbox(
"Choisissez une image:",
options=list(image_options.keys()),
help=f"{sample_data['total_count']} images disponibles"
)
selected_filename = image_options[selected_display_name]
# Afficher les infos de l'image sélectionnée
selected_img_info = next(img for img in sample_data['images'] if img['filename'] == selected_filename)
col1, col2 = st.columns([2, 1])
with col1:
st.info(f"📁 **Fichier:** {selected_filename}")
with col2:
ground_truth_status = "✅ Disponible" if selected_img_info['has_ground_truth'] else "❌ Non disponible"
st.info(f"🎯 **Vérité terrain:** {ground_truth_status}")
# Prévisualisation de l'image sélectionnée
if selected_filename:
try:
image_url = f"{API_BASE_URL}/sample-image/{selected_filename}"
response = requests.get(image_url, timeout=5)
if response.status_code == 200:
sample_image = Image.open(io.BytesIO(response.content))
st.image(
sample_image,
caption=f"Aperçu: {selected_display_name}",
use_container_width=True
)
except:
st.info("📷 Aperçu non disponible - L'image sera chargée lors de la comparaison")
# Bouton de comparaison
if st.button("🔄 Lancer la Comparaison", key="compare_sample", type="primary"):
if selected_filename:
with st.spinner("Comparaison en cours..."):
result = compare_sample_image(selected_filename)
if result and result['success']:
st.session_state.sample_comparison_result = result
st.success("✅ Comparaison terminée!")
st.rerun()
else:
st.error("❌ Erreur lors de la comparaison")
else:
st.warning("Aucune image d'exemple disponible")
# Affichage des résultats de comparaison
if st.session_state.sample_comparison_result:
st.markdown("---")
models_info = get_models_info()
display_comparison_results(st.session_state.sample_comparison_result, models_info)
# Bouton de reset
if st.button("🔄 Nouvelle Comparaison", key="reset_sample", type="secondary"):
reset_sample_comparison()
st.rerun()
# SUB-TAB 2: Upload Personnel
with sub_tab2:
st.markdown("## 📤 Comparaison sur Image Personnelle")
st.markdown("Uploadez une image pour comparer les performances des deux modèles.")
uploaded_file = st.file_uploader(
"Sélectionnez une image:",
type=['jpg', 'jpeg', 'png'],
help="Taille maximale recommandée: 5MB"
)
if uploaded_file is not None:
# Afficher l'image uploadée
image = Image.open(uploaded_file)
st.image(image, caption=f"Image uploadée: {uploaded_file.name}", use_container_width=True)
# Informations sur le fichier
file_size_mb = len(uploaded_file.getvalue()) / 1024 / 1024
st.info(f"📁 **Fichier:** {uploaded_file.name} | 📊 **Taille:** {file_size_mb:.2f} MB")
# Bouton de comparaison
if st.button("🔄 Lancer la Comparaison", key="compare_upload", type="primary"):
with st.spinner("Comparaison en cours..."):
result = compare_uploaded_image(uploaded_file)
if result and result['success']:
st.session_state.upload_comparison_result = result
st.success("✅ Comparaison terminée!")
st.rerun()
else:
st.error("❌ Erreur lors de la comparaison")
# Affichage des résultats de comparaison
if st.session_state.upload_comparison_result:
st.markdown("---")
models_info = get_models_info()
display_comparison_results(st.session_state.upload_comparison_result, models_info)
# Bouton de reset
if st.button("🔄 Nouvelle Comparaison", key="reset_upload", type="secondary"):
reset_upload_comparison()
st.rerun()
# Sidebar avec informations des modèles
with st.sidebar:
st.markdown("## 🔧 Informations des Modèles")
models_info = get_models_info()
if models_info:
st.success("✅ Modèles chargés")
# Modèle 1
best_model = models_info.get('best_model', {})
st.markdown("**🥇 Meilleur Modèle:**")
st.write(f"**Architecture:** {best_model.get('architecture', 'N/A')}")
st.write(f"**Encoder:** {best_model.get('encoder_name', 'N/A')}")
st.write(f"**Résolution:** {best_model.get('input_size', 'N/A')}")
# Modèle 2
second_model = models_info.get('second_best_model', {})
st.markdown("**🥈 Deuxième Modèle:**")
st.write(f"**Architecture:** {second_model.get('architecture', 'N/A')}")
st.write(f"**Encoder:** {second_model.get('encoder_name', 'N/A')}")
st.write(f"**Résolution:** {second_model.get('input_size', 'N/A')}")
else:
st.error("❌ Modèles non disponibles")
st.markdown("---")
st.markdown("## 🎯 Classes Segmentées (IoU)")
# Affichage avec carrés colorés et IoU
if models_info and 'class_names' in models_info and 'class_colors' in models_info:
class_names = models_info['class_names']
class_colors = models_info['class_colors']
# Récupérer les IoU des modèles
best_ious = best_model.get('class_ious', {}) if models_info else {}
second_ious = second_model.get('class_ious', {}) if models_info else {}
for i, (class_name, color) in enumerate(zip(class_names, class_colors)):
color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
# Construire la chaîne IoU
iou_str = ""
if best_ious.get(class_name) is not None and second_ious.get(class_name) is not None:
best_iou = best_ious[class_name]
second_iou = second_ious[class_name]
iou_str = f" (🥇{best_iou:.2f} - 🥈{second_iou:.2f})"
elif best_ious.get(class_name) is not None:
best_iou = best_ious[class_name]
iou_str = f" (🥇{best_iou:.2f})"
elif second_ious.get(class_name) is not None:
second_iou = second_ious[class_name]
iou_str = f" (🥈{second_iou:.2f})"
st.markdown(f"""
<div style="
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0.3rem 0;
font-size: 0.85rem;
">
<div style="
width: 14px;
height: 14px;
background-color: {color_hex};
border-radius: 2px;
border: 1px solid rgba(0,0,0,0.2);
flex-shrink: 0;
"></div>
<span style="font-weight: 500;">{i}. {class_name}{iou_str}</span>
</div>
""", unsafe_allow_html=True)
# Footer
st.markdown("---")
st.markdown("""
<div style="text-align: center; color: #666; padding: 1rem;">
🚗 <strong>Street Vision - Comparaison de Modèles</strong> -
Évaluation comparative de modèles de segmentation sémantique<br>
<small>Développé avec FastAPI, Streamlit et PyTorch</small>
</div>
""", unsafe_allow_html=True)

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.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

View file

@ -9,6 +9,7 @@ import mlflow
import re
import ast
import time
import random
# Visualisation
import matplotlib.patches as mpatches
@ -132,6 +133,78 @@ def get_preprocessing_fn(img_size=(512, 512), encoder=None):
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),
@ -538,6 +611,4 @@ def load_second_best_model(experiment_name="OC Projet 9", metric="test_mean_iou"
def load_model_by_run_id(run_id):
"""Charge un modèle par run_id spécifique"""
return load_model(run_id=run_id)
return load_model(run_id=run_id)