diff --git a/EV3 b/EV3
new file mode 100644
index 0000000..e69de29
diff --git a/app/main.py b/app/main.py
index 3487456..5acdbb2 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,24 +1,27 @@
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, Form
+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 ===")
- # ✅ NETTOYAGE automatique des 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()
@@ -33,11 +36,9 @@ def configure_mlflow():
if not mlflow_tracking_uri:
raise ValueError("MLFLOW_TRACKING_URI not found in environment variables")
- # Configuration MLflow avec URLs nettoyées
mlflow.set_tracking_uri(mlflow_tracking_uri)
print(f"MLflow Tracking URI: {mlflow_tracking_uri}")
- # ✅ CORRECTION: Configuration EXPLICITE des identifiants avec nettoyage
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)")
@@ -46,66 +47,111 @@ def configure_mlflow():
os.environ["AWS_SECRET_ACCESS_KEY"] = aws_secret_access_key
print(f"MLflow S3 Endpoint: {mlflow_s3_endpoint_url}")
print("✅ Identifiants AWS configurés")
-
- # ✅ SKIP set_experiment pour éviter le problème 404
- # mlflow.set_experiment("OC Projet 8") # Commenté temporairement
- print("✅ Configuration MLflow terminée (sans set_experiment)")
+ 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_best_model_from_registry,
+ load_model,
run_inference_and_visualize,
colorize_mask
)
-# ✅ AJOUT: Import segmentation_models pour le chargement des modèles
-try:
- import segmentation_models as sm
- print("✅ segmentation_models importé - modèles compatibles")
-except ImportError as e:
- print(f"⚠️ segmentation_models non disponible: {e}")
- print("💡 Installez avec: pip install git+https://github.com/qubvel/segmentation_models.git")
-
# Configuration
MODEL_NAME = "StreetsSegmentation"
SAMPLE_IMAGES_DIR = "./notebooks/content/data/test_images_sample"
CITYSCAPES_CONFIG_PATH = "./cityscapes_config.json"
TEMP_UPLOAD_DIR = "/tmp/uploads"
-# Modèles de données
+# 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 # 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
+
+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 PredictionResult(BaseModel):
- success: bool
- message: str
- image_path: Optional[str] = None
- mask_path: Optional[str] = None
- prediction_stats: Optional[dict] = None
- ground_truth_available: bool = False
- figure_data: Optional[str] = None
-
class AvailableImagesResponse(BaseModel):
images: List[ImageInfo]
total_count: int
-# Variables globales pour le modèle
-model = None
-encoder_name = None
-script_img_size = None
+# Variables globales pour les deux modèles
+best_model = None
+second_best_model = None
+best_model_info = {}
+second_best_model_info = {}
mapping_config = None
-run_id = None
-def initialize_model():
- """Initialise le modèle et la configuration au démarrage"""
- global model, encoder_name, script_img_size, mapping_config, run_id
+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:
- # Configuration MLflow
print("🔄 Configuration de MLflow...")
configure_mlflow()
@@ -113,76 +159,99 @@ def initialize_model():
mapping_config = load_cityscapes_config(CITYSCAPES_CONFIG_PATH, verbose=False)
print("✅ Configuration Cityscapes chargée")
- print("🔄 Chargement du modèle depuis MLflow...")
- print(f"🔍 Recherche du modèle: {MODEL_NAME}")
- model, run_id, encoder_name, script_img_size = load_best_model_from_registry(MODEL_NAME)
- print("✅ Modèle téléchargé depuis MLflow")
+ 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
+ )
- print(f"✅ Modèle chargé du run {run_id}")
- print(f"📐 Encoder: {encoder_name}")
- print(f"📐 Input shape: {script_img_size}")
- print(f"🎨 Nombre de classes: {mapping_config['num_classes']}")
+ # 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"]
+ }
- # Créer le dossier temporaire
os.makedirs(TEMP_UPLOAD_DIR, exist_ok=True)
except Exception as e:
print(f"❌ Erreur lors de l'initialisation: {e}")
- import traceback
traceback.print_exc()
raise e
# Initialisation de l'API
app = FastAPI(
- title="Multi-Class Segmentation API",
- description="API de segmentation sémantique pour véhicules autonomes",
- version="1.0.0"
+ title="Multi-Class Segmentation Comparison API",
+ description="API de comparaison de segmentation sémantique avec deux modèles",
+ version="2.0.0"
)
-# ✅ AJOUT: Initialiser le modèle de manière plus robuste
@app.on_event("startup")
async def startup_event():
- """Initialise le modèle au démarrage de l'API"""
try:
- initialize_model()
- print("🚀 API démarrée avec succès - Modèle chargé!")
+ initialize_models()
+ print("🚀 API démarrée avec succès - Modèles chargés!")
except Exception as e:
- print(f"⚠️ ATTENTION: Impossible de charger le modèle au démarrage: {e}")
- print("L'API démarre quand même mais les prédictions ne fonctionneront pas.")
+ print(f"⚠️ ATTENTION: Impossible de charger les modèles au démarrage: {e}")
@app.get("/")
def root():
return {
- "message": "Multi-Class Segmentation API - Online",
- "model_loaded": model is not None,
- "model_run_id": run_id,
- "encoder": encoder_name,
+ "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():
- """Health check endpoint pour nginx"""
- if model is None or mapping_config is None:
- raise HTTPException(status_code=503, detail="Model not loaded")
-
+ 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",
- "model_loaded": True,
+ "models_loaded": True,
"timestamp": datetime.utcnow().isoformat()
}
@app.get("/models", response_model=dict)
-def get_model_info():
- """Retourne les informations du modèle chargé"""
- if model is None:
- raise HTTPException(status_code=503, detail="Model not loaded")
-
+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 {
- "model_name": MODEL_NAME,
- "run_id": run_id,
- "encoder": encoder_name,
- "input_size": script_img_size,
+ "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()
@@ -190,238 +259,308 @@ def get_model_info():
@app.get("/sample-images", response_model=AvailableImagesResponse)
def get_sample_images():
- """Retourne la liste des images d'exemple disponibles"""
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"):
- # 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)
- )
-
+ 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):
- """Sert une image d'exemple pour prévisualisation"""
try:
- print(f"🔍 DEBUG: Demande d'image: {filename}")
-
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
image_path = original_dir / filename
-
- print(f"🔍 DEBUG: Chemin construit: {image_path}")
- print(f"🔍 DEBUG: Fichier existe: {image_path.exists()}")
-
if not image_path.exists():
- print(f"❌ DEBUG: Image non trouvée: {image_path}")
raise HTTPException(status_code=404, detail=f"Sample image {filename} not found")
-
- # Vérifier que c'est bien un fichier image attendu (sécurité)
if not filename.endswith('_leftImg8bit.png'):
- print(f"❌ DEBUG: Format invalide: {filename}")
raise HTTPException(status_code=400, detail="Invalid image filename format")
-
- print(f"✅ DEBUG: Envoi de l'image: {filename}")
return FileResponse(
image_path,
media_type="image/png",
headers={"Content-Disposition": f"inline; filename={filename}"}
)
-
except HTTPException:
- # Re-raise les HTTPException pour qu'elles soient gérées correctement
raise
except Exception as e:
- print(f"❌ Erreur lors du service de l'image {filename}: {e}")
- import traceback
- traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Error serving image: {str(e)}")
class SampleImageRequest(BaseModel):
filename: str
-@app.post("/predict-sample", response_model=PredictionResult)
-def predict_sample_image(request: SampleImageRequest):
- """Lance une prédiction sur une image d'exemple"""
- if model is None or mapping_config is None:
- raise HTTPException(status_code=503, detail="Model not loaded")
-
+@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:
- # Vérifier que l'image existe
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")
-
- # Construire le chemin du masque de vérité
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
- print(f"🔮 Lancement de l'inférence sur {request.filename}")
-
- # Lancer l'inférence (sans affichage pour l'API)
- predicted_mask, fig = run_inference_and_visualize(
+ # 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=model,
- encoder_name=encoder_name,
- img_size=script_img_size,
+ 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 # ← AJOUTÉ pour récupérer la figure
+ 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
)
- # Sérialiser la figure
- fig_bytes = pickle.dumps(fig)
- fig_b64 = base64.b64encode(fig_bytes).decode()
+ # 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))
- # Calculer les statistiques
- unique_classes, counts = np.unique(predicted_mask, return_counts=True)
- total_pixels = predicted_mask.size
+ # Récupérer les images depuis les figures individuelles
+ axes1 = fig1.get_axes()
+ axes2 = fig2.get_axes()
- stats = {}
- for class_id, count in zip(unique_classes, counts):
- if class_id < len(mapping_config['group_names']):
- percentage = (count / total_pixels) * 100
- stats[mapping_config['group_names'][class_id]] = {
- "pixels": int(count),
- "percentage": round(percentage, 1)
- }
+ # 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')
- return PredictionResult(
- success=True,
- message="Prediction completed successfully",
- image_path=str(image_path),
- mask_path=mask_path_str,
- prediction_stats=stats,
- ground_truth_available=mask_path.exists(),
+ # 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:
- print(f"❌ Erreur lors de la prédiction: {e}")
- import traceback
- traceback.print_exc()
- raise HTTPException(status_code=500, detail=f"Prediction error: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
-@app.post("/predict-upload", response_model=PredictionResult)
-def predict_uploaded_image(file: UploadFile = File(...)):
- """Lance une prédiction sur une image uploadée"""
- if model is None or mapping_config is None:
- raise HTTPException(status_code=503, detail="Model not loaded")
-
- # Vérifier le type de fichier
+@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)}"
- )
-
+ raise HTTPException(status_code=400, detail=f"Invalid file type. Allowed: {', '.join(allowed_types)}")
temp_file_path = None
try:
- # Créer un fichier temporaire
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)
-
- # Sauvegarder le fichier uploadé
with open(temp_file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
-
- print(f"🔮 Lancement de l'inférence sur {file.filename}")
-
- # Lancer l'inférence (sans masque de vérité)
- predicted_mask, fig = run_inference_and_visualize(
- image_path=temp_file_path,
- mask_path=None, # Pas de masque de vérité pour les uploads
- model=model,
- encoder_name=encoder_name,
- img_size=script_img_size,
- mapping_config=mapping_config,
- save_dir=None,
- show_stats=False,
- show_plot=False # ← AJOUTÉ pour récupérer la figure
+ # 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,
)
- # Sérialiser la figure
- fig_bytes = pickle.dumps(fig)
- fig_b64 = base64.b64encode(fig_bytes).decode()
+ # Figure combinée pour uploads (pas de GT, donc 2 colonnes seulement)
+ fig, axes = plt.subplots(2, 2, figsize=(10, 8))
- # Calculer les statistiques
- unique_classes, counts = np.unique(predicted_mask, return_counts=True)
- total_pixels = predicted_mask.size
+ # Récupérer les images depuis les figures individuelles
+ axes1 = fig1.get_axes()
+ axes2 = fig2.get_axes()
- stats = {}
- for class_id, count in zip(unique_classes, counts):
- if class_id < len(mapping_config['group_names']):
- percentage = (count / total_pixels) * 100
- stats[mapping_config['group_names'][class_id]] = {
- "pixels": int(count),
- "percentage": round(percentage, 1)
- }
+ # 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')
- return PredictionResult(
- success=True,
- message="Prediction completed successfully",
+ # 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,
- prediction_stats=stats,
- ground_truth_available=False,
+ 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:
- print(f"❌ Erreur lors de la prédiction: {e}")
- import traceback
- traceback.print_exc()
- raise HTTPException(status_code=500, detail=f"Prediction error: {str(e)}")
-
+ raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
finally:
- # Nettoyer le fichier temporaire
if temp_file_path and os.path.exists(temp_file_path):
- try:
- os.remove(temp_file_path)
- print(f"🗑️ Fichier temporaire supprimé: {temp_filename}")
- except Exception as e:
- print(f"⚠️ Impossible de supprimer le fichier temporaire: {e}")
+ try: os.remove(temp_file_path)
+ except Exception: pass
@app.get("/metrics")
def get_metrics():
- """Endpoint de métriques pour monitoring"""
return {
- "model_loaded": model is not None,
- "model_name": MODEL_NAME,
- "run_id": run_id,
+ "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()
}
\ No newline at end of file
diff --git a/app/requirements.txt b/app/requirements.txt
index c78ee04..ef6d4de 100644
--- a/app/requirements.txt
+++ b/app/requirements.txt
@@ -8,37 +8,44 @@ 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 pour les graphiques
-blinker==1.9.0 # Streamlit dependency
+altair==5.5.0 # utilisé par Streamlit
+blinker==1.9.0 # dépendance Streamlit
-# ===== HTTP REQUESTS =====
+# ===== HTTP CLIENTS =====
requests==2.32.4
-httpx==0.28.1 # Modern HTTP client (utilisé par FastAPI)
+httpx==0.28.1 # utilisé en interne par FastAPI
httpcore==1.0.9
h11==0.16.0
-# ===== MACHINE LEARNING CORE =====
-tensorflow==2.19.0
-keras==3.10.0
-numpy>=1.24.0,<2.0.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]
-# Note: tensorflow-addons n'est plus nécessaire avec TensorFlow 2.19+
-# Les fonctionnalités sont maintenant intégrées dans TensorFlow core
+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 # Pas dans votre freeze, mais nécessaire pour Docker
+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
-scipy==1.16.0
-scikit-learn==1.7.0
+networkx==3.5
+tifffile==2025.6.11
+lazy_loader==0.4
# ===== MLFLOW & MODEL REGISTRY =====
mlflow==3.2.0
@@ -50,34 +57,7 @@ boto3==1.39.2
botocore==1.39.2
s3transfer==0.13.0
-# ===== SEGMENTATION MODELS =====
-# Option 1: Version figée par commit (RECOMMANDÉE pour production)
-segmentation_models @ git+https://github.com/qubvel/segmentation_models.git@5d24bbfb28af6134e25e2c0b79e7727f6c0491d0
-
-# Option 2: Si vous préférez une version PyPI (décommentez et commentez la ligne du dessus)
-# segmentation-models==1.0.1
-
-# ===== SUPPORTING LIBRARIES =====
-# TensorFlow ecosystem
-tensorboard==2.19.0
-tensorboard-data-server==0.7.2
-protobuf==3.20.3
-flatbuffers==25.2.10
-h5py==3.14.0
-astunparse==1.6.3
-gast==0.6.0
-google-pasta==0.2.0
-keras-applications==1.0.8
-opt_einsum==3.4.0
-termcolor==2.5.0
-wrapt==1.17.2
-
-# Data processing
-networkx==3.5
-tifffile==2025.6.11
-lazy_loader==0.4
-
-# Utilities
+# ===== UTILITIES =====
click==8.2.1
tqdm==4.67.1
joblib==1.5.1
@@ -85,57 +65,6 @@ python-dateutil
packaging
typing_extensions==4.14.0
-# File handling
-python-multipart==0.0.20 # Pour FastAPI file uploads
-
-# ===== AJOUTS SPÉCIFIQUES POUR UTILS/UTILS.PY =====
-# Pour la gestion des couleurs et légendes matplotlib
-cycler==0.12.1 # Pour matplotlib customization
-
-# Font handling pour matplotlib (nécessaire dans Docker)
+# ===== MATPLOTLIB ADDONS =====
+cycler==0.12.1
fonttools==4.58.5
-
-# ===== REMOVED FROM ORIGINAL =====
-# Les packages suivants ont été supprimés car non utilisés dans votre app:
-# - awsebcli (AWS CLI - pas nécessaire dans le container)
-# - blessed, fabric, invoke (outils dev)
-# - jupyter, ipython, spyder (dev environment)
-# - nltk (pas d'NLP dans votre app)
-# - pytest (tests - à garder seulement pour dev)
-# - flask, gunicorn (vous utilisez FastAPI+uvicorn)
-# - docker (pas besoin dans le container)
-# - git* packages (dev tools)
-
-# =============================================================================
-# VERSION ALTERNATIVE - PLUS MINIMALISTE
-# =============================================================================
-
-# Si vous voulez une version encore plus légère, voici les STRICTEMENT nécessaires:
-# fastapi==0.115.14
-# uvicorn==0.35.0
-# streamlit==1.46.1
-# tensorflow==2.19.0
-# numpy==2.1.3
-# pillow==11.3.0
-# matplotlib==3.10.3
-# pandas==2.3.0
-# requests==2.32.4
-# mlflow==3.2.0
-# boto3==1.39.2
-# segmentation_models @ git+https://github.com/qubvel/segmentation_models.git@5d24bbfb28af6134e25e2c0b79e7727f6c0491d0
-
-# =============================================================================
-# NOTES SUR SEGMENTATION_MODELS
-# =============================================================================
-
-# OPTION A: Git avec commit hash (ACTUEL - RECOMMANDÉ)
-# Avantages: Version exacte, pas de surprises
-# Inconvénients: Nécessite git dans le container de build
-
-# OPTION B: Version PyPI officielle
-# segmentation-models==1.0.1
-# Avantages: Plus simple, pas besoin de git
-# Inconvénients: Peut être différent de votre version dev
-
-# OPTION C: Version locale (si vous avez des modifications)
-# Copier le package dans le container et faire pip install -e .
diff --git a/app/streamlit_app.py b/app/streamlit_app.py
index 6f9a090..846f8b1 100644
--- a/app/streamlit_app.py
+++ b/app/streamlit_app.py
@@ -2,6 +2,7 @@ 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
@@ -10,25 +11,21 @@ import base64
import pickle
# Configuration de l'API
-# prod docker
API_BASE_URL = os.getenv("MULTISEG_API_BASE_URL", "http://127.0.0.1:8080/api")
-# test local
-# API_BASE_URL = os.getenv("MULTISEG_API_BASE_URL", "http://127.0.0.1:8000") # ← Port 8000 au lieu de 8080
# Import des fonctions utils pour l'affichage local
sys.path.append('/app')
-# APRÈS (pour test local)
sys.path.append('.')
from utils.utils import colorize_mask
# Configuration de la page
st.set_page_config(
- page_title="Multi-Class Segmentation",
- page_icon="🚗",
+ page_title="Multi-Class Segmentation Comparison",
+ page_icon="🔄",
layout="wide"
)
-# CSS personnalisé
+# CSS personnalisé mis à jour
st.markdown("""
""", unsafe_allow_html=True)
# En-tête principal
st.markdown("""
-
🚗 Segmentation Multi-Classes
-
Système de segmentation sémantique pour véhicules autonomes
+
🔄 Comparaison de Segmentation Multi-Classes
+
Système de comparaison entre deux modèles de segmentation sémantique
""", unsafe_allow_html=True)
-# Initialisation des états
-if "prediction_result" not in st.session_state:
- st.session_state.prediction_result = None
-if "selected_image" not in st.session_state:
- st.session_state.selected_image = None
-if "uploaded_file" not in st.session_state:
- st.session_state.uploaded_file = None
-
-def reset_state():
- """Reset des états de session"""
- st.session_state.prediction_result = None
- st.session_state.selected_image = None
- st.session_state.uploaded_file = None
-
-def get_model_info():
- """Récupère les informations du modèle"""
+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èle: {response.status_code}")
+ 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}")
@@ -105,15 +146,14 @@ def get_sample_images():
st.error(f"Erreur de connexion à l'API: {e}")
return None
-def predict_sample_image(filename):
- """Lance une prédiction sur une image d'exemple"""
+def compare_sample_image(filename):
+ """Lance une comparaison sur une image d'exemple"""
try:
- # CORRECTION: Envoyer en JSON au lieu de form data
data = {"filename": filename}
response = requests.post(
- f"{API_BASE_URL}/predict-sample",
- json=data, # ← Changé de 'data=' à 'json='
- headers={"Content-Type": "application/json"} # ← Ajout du header explicite
+ f"{API_BASE_URL}/compare-sample",
+ json=data,
+ headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
@@ -122,14 +162,14 @@ def predict_sample_image(filename):
st.error(f"Erreur API: {response.status_code} - {response.text}")
return None
except Exception as e:
- st.error(f"Erreur lors de la prédiction: {e}")
+ st.error(f"Erreur lors de la comparaison: {e}")
return None
-def predict_uploaded_image(uploaded_file):
- """Lance une prédiction sur une image uploadée"""
+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}/predict-upload", files=files)
+ response = requests.post(f"{API_BASE_URL}/compare-upload", files=files)
if response.status_code == 200:
return response.json()
@@ -137,310 +177,393 @@ def predict_uploaded_image(uploaded_file):
st.error(f"Erreur API: {response.status_code} - {response.text}")
return None
except Exception as e:
- st.error(f"Erreur lors de la prédiction: {e}")
+ st.error(f"Erreur lors de la comparaison: {e}")
return None
-def display_segmentation_plots(fig):
- """Affiche chaque subplot séparément dans des colonnes"""
- if fig is None:
- st.warning("Aucune figure de segmentation disponible")
+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
- axes = fig.get_axes()
- num_plots = len(axes)
+ # Informations des modèles
+ st.markdown("### 🏆 Modèles Comparés")
- st.markdown("### 🎨 Résultat de la Segmentation")
+ col1, col2 = st.columns(2)
- # Créer les colonnes selon le nombre de subplots
- if num_plots <= 4:
- cols = st.columns(num_plots)
- else:
- cols = st.columns(4) # Maximum 4 colonnes
-
- for i, ax in enumerate(axes):
- col_idx = i % len(cols)
+ 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'
- with cols[col_idx]:
- # Récupérer le titre existant
- title = ax.get_title()
- # st.markdown(f"**{title}:**")
-
- # Créer une figure individuelle avec ce subplot
- individual_fig, individual_ax = plt.subplots(figsize=(6, 6))
-
- # Copier toutes les images de l'axe original
- for image in ax.get_images():
- individual_ax.imshow(
- image.get_array(),
- cmap=image.get_cmap() if image.get_cmap() else None,
- alpha=image.get_alpha() if image.get_alpha() else None,
- vmin=image.get_clim()[0] if image.get_clim() else None,
- vmax=image.get_clim()[1] if image.get_clim() else None
- )
-
- # Copier les propriétés de l'axe
- individual_ax.set_title(title)
- individual_ax.axis('off')
-
- # Ajuster la mise en page
- individual_fig.tight_layout()
-
- # Afficher dans Streamlit
- st.pyplot(individual_fig)
-
- # Libérer la mémoire
- plt.close(individual_fig)
+ st.markdown(f"""
+
+
🥇 Modèle #1
+ {model1.get('architecture', 'N/A')} - {model1['encoder_name']}
+ 📐 Taille: {model1['input_size']}
+ 📊 mIoU: {miou1}
+ 🎯 Accuracy: {acc1}
+
+ """, unsafe_allow_html=True)
- # Fermer la figure originale pour libérer la mémoire
- plt.close(fig)
+ with col2:
+ model2 = result['model2_info']
+ # Formater les métriques séparément
+ miou2 = f"{model2.get('test_mean_iou'):.4f}" if model2.get('test_mean_iou') is not None else 'N/A'
+ acc2 = f"{model2.get('test_accuracy'):.4f}" if model2.get('test_accuracy') is not None else 'N/A'
+
+ st.markdown(f"""
+
+
🥈 Modèle #2
+ {model2.get('architecture', 'N/A')} - {model2['encoder_name']}
+ 📐 Taille: {model2['input_size']}
+ 📊 mIoU: {miou2}
+ 🎯 Accuracy: {acc2}
+
+ """, unsafe_allow_html=True)
+
+ # Comparaison des temps d'inférence
+ st.markdown("### ⏱️ Comparaison des Performances")
+
+ speed_comp = result['speed_comparison']
+ model1_time = result['model1_inference_time'] * 1000
+ model2_time = result['model2_inference_time'] * 1000
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ winner_class = "speed-winner" if speed_comp['faster_model'] == model1['encoder_name'] else "speed-slower"
+ st.markdown(f"""
+
+
🥇 {model1.get('architecture', 'N/A')} - {model1['encoder_name']}
+ {model1_time:.1f} ms
+
+ """, unsafe_allow_html=True)
+
+ with col2:
+ st.markdown(f"""
+
+
⚡ Comparaison
+ {speed_comp['faster_model']}
+ est {speed_comp['speedup_ratio']}x plus rapide
+ Différence: {speed_comp['time_difference_ms']:.1f} ms
+
+ """, unsafe_allow_html=True)
+
+ with col3:
+ winner_class = "speed-winner" if speed_comp['faster_model'] == model2['encoder_name'] else "speed-slower"
+ st.markdown(f"""
+
+
🥈 {model2.get('architecture', 'N/A')} - {model2['encoder_name']}
+ {model2_time:.1f} ms
+
+ """, unsafe_allow_html=True)
+
+ # Affichage des segmentations
+ 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_prediction_stats_with_chart(stats, model_info):
- """Affiche les statistiques avec un graphique en barres"""
- st.markdown("### 📊 Statistiques de Segmentation")
-
+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
- # Ratio 1:2 pour donner plus de place au graphique
- col1, col2 = st.columns([1, 2])
-
- # Colonne 1: Cartes de statistiques compactes avec carrés colorés
- with col1:
- st.markdown("**Détails par classe:**")
-
- # Récupérer les couleurs des classes pour les carrés
- class_colors_dict = {}
- if model_info and 'class_colors' in model_info and 'class_names' in model_info:
- model_class_names = model_info['class_names']
- model_class_colors = model_info['class_colors']
-
- for class_name in stats.keys():
- try:
- idx = model_class_names.index(class_name)
- color = model_class_colors[idx]
- # Convertir en hex pour CSS
- color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
- class_colors_dict[class_name] = color_hex
- except (ValueError, IndexError):
- # Couleur par défaut si pas trouvée
- class_colors_dict[class_name] = "#808080"
-
- for class_name, data in stats.items():
- # Récupérer la couleur pour cette classe
- color_hex = class_colors_dict.get(class_name, "#808080")
-
- # ✨ NOUVEAU: Carré coloré + nom de classe
- st.markdown(f"""
-
-
-
- 🔢 {data['pixels']:,} pixels | 📊 {data['percentage']}%
-
-
- """, unsafe_allow_html=True)
-
- # Colonne 2: Graphique en barres (reste identique)
- with col2:
- st.markdown("**Répartition visuelle:**")
-
- # Préparer les données pour le graphique
- class_names = list(stats.keys())
- percentages = [data['percentage'] for data in stats.values()]
-
- # Récupérer les couleurs des classes depuis model_info
- colors = []
- if model_info and 'class_colors' in model_info and 'class_names' in model_info:
- model_class_names = model_info['class_names']
- model_class_colors = model_info['class_colors']
-
- for class_name in class_names:
- # Trouver l'index de cette classe dans le modèle
- try:
- idx = model_class_names.index(class_name)
- color = model_class_colors[idx]
- # Convertir RGB en format matplotlib
- colors.append([c/255.0 for c in color])
- except (ValueError, IndexError):
- # Couleur par défaut si pas trouvée
- colors.append([0.5, 0.5, 0.5])
- else:
- # Couleurs par défaut si pas d'info du modèle
- colors = plt.cm.Set3(np.linspace(0, 1, len(class_names)))
-
- # Créer un graphique plus grand et mieux adapté
- fig, ax = plt.subplots(figsize=(10, 6)) # Plus large
-
- bars = ax.bar(class_names, percentages, color=colors, alpha=0.8, edgecolor='white', linewidth=1)
-
- # Personnalisation du graphique améliorée
- ax.set_xlabel('Classes de Segmentation', fontsize=11, fontweight='bold')
- ax.set_ylabel('Pourcentage (%)', fontsize=11, fontweight='bold')
- ax.set_title('Répartition des Classes Détectées', fontsize=13, fontweight='bold', pad=20)
-
- # Meilleure gestion des labels selon le nombre de classes
- if len(class_names) <= 4:
- plt.xticks(rotation=0, ha='center', fontsize=10)
- elif len(class_names) <= 6:
- plt.xticks(rotation=30, ha='right', fontsize=9)
- else:
- plt.xticks(rotation=45, ha='right', fontsize=8)
-
- plt.yticks(fontsize=10)
-
- # Valeurs sur les barres avec meilleur positionnement
- for bar, percentage in zip(bars, percentages):
- height = bar.get_height()
- # Adapter la position du texte selon la hauteur de la barre
- if height < max(percentages) * 0.1:
- # Si la barre est très petite, mettre le texte au-dessus
- va = 'bottom'
- y_pos = height + max(percentages) * 0.02
- else:
- # Sinon, centrer dans la barre
- va = 'center'
- y_pos = height / 2
-
- ax.text(bar.get_x() + bar.get_width()/2., y_pos,
- f'{percentage:.1f}%',
- ha='center', va=va, fontsize=9, fontweight='bold',
- color='white' if va == 'center' else 'black',
- bbox=dict(boxstyle='round,pad=0.2', facecolor='black', alpha=0.7) if va == 'center' else None)
-
- # Améliorer la mise en page et le style
- plt.tight_layout()
-
- # Grille plus subtile
- ax.grid(axis='y', alpha=0.2, linestyle='--')
- ax.set_axisbelow(True)
-
- # Limites et style des axes
- ax.set_ylim(0, max(percentages) * 1.15) # Plus d'espace au-dessus
- ax.spines['top'].set_visible(False)
- ax.spines['right'].set_visible(False)
- ax.spines['left'].set_color('#666')
- ax.spines['bottom'].set_color('#666')
-
- # Couleur de fond
- fig.patch.set_facecolor('white')
- ax.set_facecolor('#fafafa')
-
- # Afficher dans Streamlit
- st.pyplot(fig)
- plt.close(fig)
-
-
-def display_class_legend(model_info):
- """Affiche la légende des classes de segmentation"""
- if model_info and 'class_colors' in model_info:
- st.markdown("**Légende des classes:**")
-
- class_names = model_info.get('class_names', [])
- class_colors = model_info.get('class_colors', [])
-
- # Créer une légende colorée
- legend_cols = st.columns(len(class_names))
- for i, (name, color) in enumerate(zip(class_names, class_colors)):
- with legend_cols[i]:
- color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
- st.markdown(f"""
-
- {name}
-
- """, unsafe_allow_html=True)
-
-# # Sidebar avec informations du modèle
-# with st.sidebar:
-# st.markdown("## 🔧 Informations du Modèle")
-
-# model_info = get_model_info()
-# if model_info:
-# st.success("✅ Modèle chargé")
-# st.write(f"**Nom:** {model_info.get('model_name', 'N/A')}")
-# st.write(f"**Encoder:** {model_info.get('encoder', 'N/A')}")
-# st.write(f"**Classes:** {model_info.get('num_classes', 'N/A')}")
-# st.write(f"**Taille d'entrée:** {model_info.get('input_size', 'N/A')}")
-# st.write(f"**Run ID:** {model_info.get('run_id', 'N/A')[:8]}...")
-# else:
-# st.error("❌ Modèle non disponible")
-
-# st.markdown("---")
-# st.markdown("## 🎯 Classes Segmentées")
-# if model_info and 'class_names' in model_info:
-# for i, class_name in enumerate(model_info['class_names']):
-# st.write(f"{i}. {class_name}")
-
-# Sidebar avec informations du modèle
-with st.sidebar:
- st.markdown("## 🔧 Informations du Modèle")
-
- model_info = get_model_info()
- if model_info:
- st.success("✅ Modèle chargé")
- st.write(f"**Nom:** {model_info.get('model_name', 'N/A')}")
- st.write(f"**Encoder:** {model_info.get('encoder', 'N/A')}")
- st.write(f"**Classes:** {model_info.get('num_classes', 'N/A')}")
- st.write(f"**Taille d'entrée:** {model_info.get('input_size', 'N/A')}")
- st.write(f"**Run ID:** {model_info.get('run_id', 'N/A')[:8]}...")
+ # 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:
- st.error("❌ Modèle non disponible")
+ # 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")
+ }
+ )
+
+# 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")
+ st.markdown("## 🎯 Classes Segmentées (IoU)")
- # ✨ NOUVEAU: Affichage avec carrés colorés
- if model_info and 'class_names' in model_info and 'class_colors' in model_info:
- class_names = model_info['class_names']
- class_colors = model_info['class_colors']
+ # 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)):
- # Convertir RGB en hex
color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
- # Affichage avec carré coloré
+ # Construire la chaîne IoU
+ iou_str = ""
+ if best_ious.get(class_name) is not None and second_ious.get(class_name) is not None:
+ best_iou = best_ious[class_name]
+ second_iou = second_ious[class_name]
+ iou_str = f" (🥇{best_iou:.2f} - 🥈{second_iou:.2f})"
+ elif best_ious.get(class_name) is not None:
+ best_iou = best_ious[class_name]
+ iou_str = f" (🥇{best_iou:.2f})"
+ elif second_ious.get(class_name) is not None:
+ second_iou = second_ious[class_name]
+ iou_str = f" (🥈{second_iou:.2f})"
+
st.markdown(f"""
-
{i}. {class_name}
+
{i}. {class_name}{iou_str}
""", unsafe_allow_html=True)
- else:
- # Fallback si pas d'info couleur
- if model_info and 'class_names' in model_info:
- for i, class_name in enumerate(model_info['class_names']):
- st.write(f"{i}. {class_name}")
- else:
- st.write("Informations des classes non disponibles")
+# États de session
+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
-# Initialisation des états - MODIFIÉE pour séparer les résultats par onglet
-if "sample_prediction_result" not in st.session_state:
- st.session_state.sample_prediction_result = None
-if "sample_selected_image" not in st.session_state:
- st.session_state.sample_selected_image = None
+def reset_sample_comparison():
+ st.session_state.sample_comparison_result = None
-if "upload_prediction_result" not in st.session_state:
- st.session_state.upload_prediction_result = None
-if "upload_uploaded_file" not in st.session_state:
- st.session_state.upload_uploaded_file = None
+def reset_upload_comparison():
+ st.session_state.upload_comparison_result = None
-# ANCIEN système (gardé pour compatibilité si besoin)
-if "prediction_result" not in st.session_state:
- st.session_state.prediction_result = None
-if "selected_image" not in st.session_state:
- st.session_state.selected_image = None
-if "uploaded_file" not in st.session_state:
- st.session_state.uploaded_file = None
-
-def reset_sample_state():
- """Reset des états de session pour l'onglet Sample"""
- st.session_state.sample_prediction_result = None
- st.session_state.sample_selected_image = None
-
-def reset_upload_state():
- """Reset des états de session pour l'onglet Upload"""
- st.session_state.upload_prediction_result = None
- st.session_state.upload_uploaded_file = None
-
-def reset_state():
- """Reset complet (garde l'ancienne fonction pour compatibilité)"""
- reset_sample_state()
- reset_upload_state()
-
-# Interface principale - MODIFIÉE pour résultats indépendants
+# Interface principale
tab1, tab2 = st.tabs(["📸 Images d'Exemple", "📤 Upload Personnel"])
# Tab 1: Images d'exemple
with tab1:
- st.markdown("## 📸 Sélectionner une Image d'Exemple")
- st.markdown("Ces images proviennent du dataset Cityscapes avec masques de vérité disponibles.")
+ st.markdown("## 📸 Comparaison sur Images d'Exemple")
+ st.markdown("Comparez les performances des deux modèles sur des images Cityscapes.")
# Récupérer les images d'exemple
sample_data = get_sample_images()
@@ -530,85 +623,47 @@ with tab1:
# Prévisualisation de l'image sélectionnée
if selected_filename:
try:
- # Récupérer l'image via l'API
image_url = f"{API_BASE_URL}/sample-image/{selected_filename}"
response = requests.get(image_url, timeout=5)
if response.status_code == 200:
- # Charger l'image depuis les bytes
sample_image = Image.open(io.BytesIO(response.content))
st.image(
sample_image,
caption=f"Aperçu: {selected_display_name}",
use_container_width=True
)
- elif response.status_code == 404:
- st.warning("📷 Image non trouvée sur le serveur")
- else:
- st.info("📷 Prévisualisation temporairement indisponible")
-
- except requests.exceptions.Timeout:
- st.warning("⏱️ Timeout lors du chargement de l'aperçu")
- except Exception as e:
- st.info("📷 Aperçu non disponible - L'image sera chargée lors de la prédiction")
+ except:
+ st.info("📷 Aperçu non disponible - L'image sera chargée lors de la comparaison")
- # Bouton de prédiction
- if st.button("🔮 Lancer la Segmentation", key="predict_sample", type="primary"):
+ # Bouton de comparaison
+ if st.button("🔄 Lancer la Comparaison", key="compare_sample", type="primary"):
if selected_filename:
- with st.spinner("Segmentation en cours..."):
- result = predict_sample_image(selected_filename)
+ with st.spinner("Comparaison en cours..."):
+ result = compare_sample_image(selected_filename)
if result and result['success']:
- # ✨ NOUVEAU: Stocker dans les variables Sample
- st.session_state.sample_prediction_result = result
- st.session_state.sample_selected_image = selected_filename
- st.success("✅ Segmentation terminée!")
+ st.session_state.sample_comparison_result = result
+ st.success("✅ Comparaison terminée!")
st.rerun()
else:
- st.error("❌ Erreur lors de la segmentation")
+ st.error("❌ Erreur lors de la comparaison")
else:
st.warning("Aucune image d'exemple disponible")
- # ✨ NOUVEAU: Affichage des résultats Sample (indépendants)
- if st.session_state.sample_prediction_result and st.session_state.sample_selected_image:
+ # Affichage des résultats de comparaison
+ if st.session_state.sample_comparison_result:
st.markdown("---")
- result = st.session_state.sample_prediction_result
-
- st.markdown(f"## 🎯 Résultats pour: {st.session_state.sample_selected_image}")
-
- # Afficher la segmentation
- if result.get('figure_data'):
- try:
- # Désérialiser la figure
- fig_bytes = base64.b64decode(result['figure_data'])
- fig = pickle.loads(fig_bytes)
- display_segmentation_plots(fig)
- except Exception as e:
- st.error(f"Erreur lors de l'affichage de la segmentation: {e}")
- # Fallback sur la légende des classes
- display_class_legend(model_info)
- else:
- # Fallback si pas de figure_data
- display_class_legend(model_info)
-
- # Afficher les statistiques
- if result.get('prediction_stats'):
- display_prediction_stats_with_chart(result['prediction_stats'], model_info)
-
- # Informations additionnelles
- if result.get('ground_truth_available'):
- st.success("🎯 Masque de vérité terrain disponible pour comparaison")
- else:
- st.info("ℹ️ Pas de vérité terrain disponible")
+ display_comparison_results(st.session_state.sample_comparison_result, models_info)
# Bouton de reset
- if st.button("🔄 Nouvelle Analyse", key="reset_sample", type="secondary"):
- reset_sample_state()
+ if st.button("🔄 Nouvelle Comparaison", key="reset_sample", type="secondary"):
+ reset_sample_comparison()
st.rerun()
# Tab 2: Upload personnel
with tab2:
- st.markdown("## 📤 Uploader Votre Image")
- st.markdown("Uploadez une image de rue pour analyse. Formats acceptés: JPG, PNG")
+ 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:",
@@ -625,62 +680,33 @@ with tab2:
file_size_mb = len(uploaded_file.getvalue()) / 1024 / 1024
st.info(f"📁 **Fichier:** {uploaded_file.name} | 📊 **Taille:** {file_size_mb:.2f} MB")
- # Bouton de prédiction
- if st.button("🔮 Lancer la Segmentation", key="predict_upload", type="primary"):
- with st.spinner("Segmentation en cours..."):
- result = predict_uploaded_image(uploaded_file)
+ # 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']:
- # ✨ NOUVEAU: Stocker dans les variables Upload
- st.session_state.upload_prediction_result = result
- st.session_state.upload_uploaded_file = uploaded_file.name
- st.success("✅ Segmentation terminée!")
+ st.session_state.upload_comparison_result = result
+ st.success("✅ Comparaison terminée!")
st.rerun()
else:
- st.error("❌ Erreur lors de la segmentation")
+ st.error("❌ Erreur lors de la comparaison")
- # ✨ NOUVEAU: Affichage des résultats Upload (indépendants)
- if st.session_state.upload_prediction_result and st.session_state.upload_uploaded_file:
+ # Affichage des résultats de comparaison
+ if st.session_state.upload_comparison_result:
st.markdown("---")
- result = st.session_state.upload_prediction_result
-
- st.markdown(f"## 🎯 Résultats pour: {st.session_state.upload_uploaded_file}")
-
- # Afficher la segmentation
- if result.get('figure_data'):
- try:
- # Désérialiser la figure
- fig_bytes = base64.b64decode(result['figure_data'])
- fig = pickle.loads(fig_bytes)
- display_segmentation_plots(fig)
- except Exception as e:
- st.error(f"Erreur lors de l'affichage de la segmentation: {e}")
- # Fallback sur la légende des classes
- display_class_legend(model_info)
- else:
- # Fallback si pas de figure_data
- display_class_legend(model_info)
-
- # Afficher les statistiques
- if result.get('prediction_stats'):
- display_prediction_stats_with_chart(result['prediction_stats'], model_info)
-
- # Informations additionnelles
- if result.get('ground_truth_available'):
- st.success("🎯 Masque de vérité terrain disponible pour comparaison")
- else:
- st.info("ℹ️ Pas de vérité terrain disponible (image personnelle)")
+ display_comparison_results(st.session_state.upload_comparison_result, models_info)
# Bouton de reset
- if st.button("🔄 Nouvelle Analyse", key="reset_upload", type="secondary"):
- reset_upload_state()
+ if st.button("🔄 Nouvelle Comparaison", key="reset_upload", type="secondary"):
+ reset_upload_comparison()
st.rerun()
# Footer
st.markdown("---")
st.markdown("""
- 🚗 Multi-Class Segmentation API -
- Système de vision par ordinateur pour véhicules autonomes
- Développé avec FastAPI, Streamlit et TensorFlow
+ 🔄 Multi-Class Segmentation Comparison API -
+ Système de comparaison de modèles de vision par ordinateur
+ Développé avec FastAPI, Streamlit et PyTorch
""", unsafe_allow_html=True)
\ No newline at end of file
diff --git a/notebooks/requirements.txt b/notebooks/requirements.txt
index d6dc46d..5bb3c2f 100644
--- a/notebooks/requirements.txt
+++ b/notebooks/requirements.txt
@@ -606,13 +606,13 @@ tokenizers==0.21.4
toml==0.10.2
tomlkit==0.13.3
toolz==0.12.1
-torch==2.8.0+cu126
+torch==2.8.0
torchao==0.10.0
-torchaudio==2.8.0+cu126
+torchaudio==2.8.0
torchdata==0.11.0
torchsummary==1.5.1
torchtune==0.6.1
-torchvision==0.23.0+cu126
+torchvision==0.23.0
tornado==6.4.2
tqdm==4.67.1
traitlets==5.7.1
diff --git a/utils/utils.py b/utils/utils.py
index c3cb60e..0a58a91 100644
--- a/utils/utils.py
+++ b/utils/utils.py
@@ -3,7 +3,6 @@ import json
import tempfile
import numpy as np
import torch
-import torch.nn.functional as F
import matplotlib.pyplot as plt
import segmentation_models_pytorch as smp
import mlflow
@@ -16,8 +15,8 @@ import matplotlib.patches as mpatches
from matplotlib.colors import ListedColormap
# Preprocessing/Images
-from PIL import Image
-import torchvision.transforms as transforms
+import albumentations as A
+from albumentations.pytorch import ToTensorV2
def load_cityscapes_config(config_path="../cityscapes_config.json", verbose=True):
@@ -103,48 +102,12 @@ def colorize_mask(mask, group_colors):
return colored_mask
-# def get_preprocessing_fn(encoder_name=None):
-# """
-# Retourne la fonction de prétraitement adaptée au backbone utilisé.
-# ADAPTÉ PYTORCH : utilise smp.encoders.get_preprocessing_params
-# """
-# if encoder_name is None:
-# print("ℹ️ Modèle vanilla - normalisation simple")
-# return lambda x: x / 255.0
-# else:
-# print(f"ℹ️ Préprocessing du backbone {encoder_name}")
-# try:
-# # PyTorch segmentation_models utilise get_preprocessing_params
-# preprocess_params = smp.encoders.get_preprocessing_params(encoder_name)
-# mean = preprocess_params['mean']
-# std = preprocess_params['std']
-
-# def preprocess_fn(x):
-# # x doit être dans [0, 1] puis normalisé
-# x = x / 255.0 if x.max() > 1.0 else x
-# # Normalisation ImageNet ou spécifique au backbone
-# if isinstance(x, torch.Tensor):
-# for i in range(3): # RGB
-# x[:, i, :, :] = (x[:, i, :, :] - mean[i]) / std[i]
-# else: # numpy
-# for i in range(3):
-# x[:, :, i] = (x[:, :, i] - mean[i]) / std[i]
-# return x
-
-# return preprocess_fn
-# except Exception as e:
-# print(f"⚠️ Erreur preprocessing {encoder_name}: {e}, fallback simple")
-# return lambda x: x / 255.0
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.
"""
- import albumentations as A
- from albumentations.pytorch import ToTensorV2
- import cv2
- import torch
if encoder is None:
mean, std = [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]
@@ -205,173 +168,9 @@ def preprocess_image_and_mask(image_path, mask_path=None, img_size=(512, 512),
return img_preprocessed, img_display, mask_preprocessed
-
-# def preprocess_image_and_mask(image_path, mask_path=None, img_size=(512, 512), # ← CHANGÉ: 224→512
-# encoder_name=None, id_to_group_mapping=None):
-# """
-# Préprocesse une image (et optionnellement un masque) avec le même pipeline que l'entraînement
-# ADAPTÉ PYTORCH : Utilise PIL + torch au lieu de tf
-# """
-
-# print(f"🔄 Préprocessing de l'image: {os.path.basename(image_path)}")
-
-# # Chargement et preprocessing de l'image
-# img_pil = Image.open(image_path).convert('RGB')
-# img_original = img_pil.resize(img_size, Image.BILINEAR)
-
-# # Conversion en tensor et normalisation
-# img_tensor = transforms.ToTensor()(img_original) # [3, H, W] dans [0, 1]
-
-# # Appliquer le preprocessing du backbone
-# preprocessing_fn = get_preprocessing_fn(encoder_name)
-# if encoder_name is not None:
-# # Pour les backbones pré-entraînés, on applique leur normalisation
-# img_preprocessed = preprocessing_fn(img_tensor.permute(1, 2, 0).numpy())
-# img_preprocessed = torch.from_numpy(img_preprocessed).permute(2, 0, 1).float()
-# else:
-# # Simple normalisation [0, 1]
-# img_preprocessed = img_tensor.float()
-
-# # Preprocessing du masque si fourni
-# mask_preprocessed = 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') # Grayscale
-# mask_resized = mask_pil.resize(img_size, Image.NEAREST)
-# mask_tensor = torch.from_numpy(np.array(mask_resized)).long()
-
-# print(f" Valeurs uniques avant mapping: {torch.unique(mask_tensor)[:10]}...")
-
-# # Appliquer le mapping si fourni
-# if id_to_group_mapping is not None:
-# mask_tensor = map_mask_ids(mask_tensor, id_to_group_mapping)
-# print(f" Valeurs uniques après mapping: {torch.unique(mask_tensor)}")
-
-# mask_preprocessed = mask_tensor
-
-# # img_original en numpy pour affichage
-# img_display = np.array(img_original)
-
-# return img_preprocessed, img_display, mask_preprocessed
-
-# def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config,
-# mask_path=None, save_dir=None, show_stats=True):
-# """
-# ADAPTÉ PYTORCH : Inférence avec modèle PyTorch
-# Returns: (predicted_mask, inference_time_seconds)
-# """
-
-# print(f"=== INFÉRENCE PYTORCH SUR {os.path.basename(image_path)} ===")
-
-# 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)")
-
-# # 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}")
-
-# plt.show()
-
-# # 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}%)")
-
-# return predicted_mask, inference_time
-
-
# 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):
+ mask_path=None, save_dir=None, show_stats=True, show_plot=True):
"""
Version simplifiée utilisant preprocess_image_and_mask
"""
@@ -461,7 +260,6 @@ def run_inference_and_visualize(image_path, model, encoder_name, img_size, mappi
fig.savefig(fig_path, dpi=150, bbox_inches='tight')
print(f"💾 Résultat sauvegardé: {fig_path}")
- plt.show()
# Statistiques
if show_stats:
@@ -481,7 +279,11 @@ def run_inference_and_visualize(image_path, model, encoder_name, img_size, mappi
percentage = (count / mask_gt_np.size) * 100
print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)")
- return predicted_mask, inference_time
+ 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",
@@ -736,6 +538,6 @@ 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_by_param(run_id=run_id)
+ return load_model(run_id=run_id)