From cdd6f9645bb7272b23b92cce02a84d768be48041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Mureau?= Date: Tue, 2 Sep 2025 10:46:13 +0200 Subject: [PATCH 1/2] Docker working. Improvements incoming. --- EV3 | 0 app/main.py | 419 ++++++++++----------- app/requirements.txt | 117 ++---- app/streamlit_app.py | 749 +++++++++++++++---------------------- notebooks/requirements.txt | 6 +- utils/utils.py | 20 +- 6 files changed, 530 insertions(+), 781 deletions(-) create mode 100644 EV3 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..136db01 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,68 @@ 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 + +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 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 +116,83 @@ 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 + ) + best_model_info = { + "name": "Meilleur modèle", + "run_id": best_run_id, + "encoder_name": best_encoder_name, + "input_size": best_img_size, + "rank": 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']}") + 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 + ) + 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 + } - # 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 +200,199 @@ 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 + pred1, t1, _ = 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, + img_size=best_model_info["input_size"], 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 ) - - # Sérialiser la figure - fig_bytes = pickle.dumps(fig) - fig_b64 = base64.b64encode(fig_bytes).decode() - - # Calculer les statistiques - unique_classes, counts = np.unique(predicted_mask, return_counts=True) - total_pixels = predicted_mask.size - - 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) - } - - return PredictionResult( - success=True, - message="Prediction completed successfully", + # Inférence modèle 2 + pred2, t2, _ = run_inference_and_visualize( image_path=str(image_path), mask_path=mask_path_str, - prediction_stats=stats, - ground_truth_available=mask_path.exists(), + model=second_best_model, + encoder_name=None, + img_size=second_best_model_info["input_size"], + mapping_config=mapping_config, + save_dir=None, show_stats=False, show_plot=False + ) + # Figure combinée + fig, axes = plt.subplots(2, 3, figsize=(15, 8)) + img_arr = mpimg.imread(str(image_path)) + axes[0, 0].imshow(img_arr); axes[0, 0].set_title("Image originale"); axes[0, 0].axis('off') + axes[1, 0].imshow(img_arr); axes[1, 0].set_title("Image originale"); axes[1, 0].axis('off') + axes[0, 1].imshow(colorize_mask(pred1, mapping_config['group_colors'])) + axes[0, 1].set_title(f"Prédiction {best_model_info['encoder_name']}"); axes[0, 1].axis('off') + axes[1, 1].imshow(colorize_mask(pred2, mapping_config['group_colors'])) + axes[1, 1].set_title(f"Prédiction {second_best_model_info['encoder_name']}"); axes[1, 1].axis('off') + if mask_path_str and os.path.exists(mask_path_str): + gt = mpimg.imread(mask_path_str) + axes[0, 2].imshow(gt); axes[0, 2].set_title("Ground Truth"); axes[0, 2].axis('off') + axes[1, 2].imshow(gt); axes[1, 2].set_title("Ground Truth"); axes[1, 2].axis('off') + plt.tight_layout() + 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 + pred1, t1, _ = run_inference_and_visualize( + image_path=temp_file_path, mask_path=None, + model=best_model, encoder_name=None, + img_size=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() - - # Calculer les statistiques - unique_classes, counts = np.unique(predicted_mask, return_counts=True) - total_pixels = predicted_mask.size - - 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) - } - - return PredictionResult( - success=True, - message="Prediction completed successfully", + pred2, t2, _ = run_inference_and_visualize( + image_path=temp_file_path, mask_path=None, + model=second_best_model, encoder_name=None, + img_size=second_best_model_info["input_size"], mapping_config=mapping_config, + save_dir=None, show_stats=False, show_plot=False, + ) + # Figure + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + img_arr = mpimg.imread(temp_file_path) + axes[0].imshow(img_arr); axes[0].set_title("Image originale"); axes[0].axis('off') + axes[1].imshow(colorize_mask(pred1, mapping_config['group_colors'])) + axes[1].set_title(f"Prédiction {best_model_info['encoder_name']}"); axes[1].axis('off') + axes[2].imshow(colorize_mask(pred2, mapping_config['group_colors'])) + axes[2].set_title(f"Prédiction {second_best_model_info['encoder_name']}"); axes[2].axis('off') + plt.tight_layout() + 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..3351498 100644 --- a/app/streamlit_app.py +++ b/app/streamlit_app.py @@ -10,25 +10,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) @@ -151,23 +193,33 @@ def display_comparison_results(result, models_info): with col1: model1 = result['model1_info'] + # Formater les métriques séparément + miou1 = f"{model1.get('test_mean_iou'):.4f}" if model1.get('test_mean_iou') is not None else 'N/A' + acc1 = f"{model1.get('test_accuracy'):.4f}" if model1.get('test_accuracy') is not None else 'N/A' + st.markdown(f"""

🥇 Modèle #1

- {model1['encoder_name']}
+ {model1.get('architecture', 'N/A')} - {model1['encoder_name']}
📐 Taille: {model1['input_size']}
- 🆔 Run: {model1['run_id'][:8]}... + 📊 mIoU: {miou1}
+ 🎯 Accuracy: {acc1}
""", unsafe_allow_html=True) with col2: model2 = result['model2_info'] + # Formater les métriques séparément + miou2 = f"{model2.get('test_mean_iou'):.4f}" if model2.get('test_mean_iou') is not None else 'N/A' + acc2 = f"{model2.get('test_accuracy'):.4f}" if model2.get('test_accuracy') is not None else 'N/A' + st.markdown(f"""

🥈 Modèle #2

- {model2['encoder_name']}
+ {model2.get('architecture', 'N/A')} - {model2['encoder_name']}
📐 Taille: {model2['input_size']}
- 🆔 Run: {model2['run_id'][:8]}... + 📊 mIoU: {miou2}
+ 🎯 Accuracy: {acc2}
""", unsafe_allow_html=True) @@ -184,7 +236,7 @@ def display_comparison_results(result, models_info): winner_class = "speed-winner" if speed_comp['faster_model'] == model1['encoder_name'] else "speed-slower" st.markdown(f"""
-
🥇 {model1['encoder_name']}
+
🥇 {model1.get('architecture', 'N/A')} - {model1['encoder_name']}
{model1_time:.1f} ms
""", unsafe_allow_html=True) @@ -203,7 +255,7 @@ def display_comparison_results(result, models_info): winner_class = "speed-winner" if speed_comp['faster_model'] == model2['encoder_name'] else "speed-slower" st.markdown(f"""
-
🥈 {model2['encoder_name']}
+
🥈 {model2.get('architecture', 'N/A')} - {model2['encoder_name']}
{model2_time:.1f} ms
""", unsafe_allow_html=True) @@ -220,23 +272,21 @@ def display_comparison_results(result, models_info): st.error(f"Erreur lors de l'affichage: {e}") # Comparaison des statistiques - st.markdown("### 📊 Comparaison des Statistiques") + st.markdown("### 📊 Répartition des classes par modèle") col1, col2 = st.columns(2) with col1: - st.markdown(f"**📈 {model1['encoder_name']} - Répartition des classes:**") if result.get('model1_stats'): - display_model_stats_chart(result['model1_stats'], models_info, f"Modèle 1 - {model1['encoder_name']}") + display_model_stats_chart(result['model1_stats'], models_info, f"{model1.get('architecture', 'N/A')} - {model1['encoder_name']}") with col2: - st.markdown(f"**📈 {model2['encoder_name']} - Répartition des classes:**") if result.get('model2_stats'): - display_model_stats_chart(result['model2_stats'], models_info, f"Modèle 2 - {model2['encoder_name']}") + 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) + 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""" @@ -244,9 +294,15 @@ def display_model_stats_chart(stats, models_info, title): st.warning("Aucune statistique disponible") return - # Préparer les données pour le graphique - class_names = list(stats.keys()) - percentages = [data['percentage'] for data in stats.values()] + # 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 = [] @@ -254,7 +310,7 @@ def display_model_stats_chart(stats, models_info, title): model_class_names = models_info['class_names'] model_class_colors = models_info['class_colors'] - for class_name in class_names: + for class_name in ordered_class_names: try: idx = model_class_names.index(class_name) color = model_class_colors[idx] @@ -262,21 +318,21 @@ def display_model_stats_chart(stats, models_info, title): except (ValueError, IndexError): colors.append([0.5, 0.5, 0.5]) else: - colors = plt.cm.Set3(np.linspace(0, 1, len(class_names))) + 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(class_names, percentages, color=colors, alpha=0.8, edgecolor='white', linewidth=1) + 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(class_names) <= 4: + if len(ordered_class_names) <= 4: plt.xticks(rotation=0, ha='center', fontsize=9) - elif len(class_names) <= 6: + elif len(ordered_class_names) <= 6: plt.xticks(rotation=30, ha='right', fontsize=8) else: plt.xticks(rotation=45, ha='right', fontsize=8) @@ -305,15 +361,22 @@ def display_model_stats_chart(stats, models_info, title): st.pyplot(fig) plt.close(fig) -def display_class_comparison(stats1, stats2, model1, model2): +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") - # Combiner toutes les classes présentes - all_classes = set(stats1.keys()) | set(stats2.keys()) + # 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 sorted(all_classes): + 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 @@ -325,27 +388,122 @@ def display_class_comparison(stats1, stats2, model1, model2): 'difference': diff }) - # Afficher sous forme de tableau interactif - col1, col2 = st.columns([2, 1]) + # Créer un graphique comparatif des barres + st.markdown("**📊 Comparaison des classes entre les modèles:**") - with col1: - st.markdown("**📋 Tableau Comparatif:**") - for data in comparison_data: - diff_color = "🟢" if abs(data['difference']) < 1 else ("🔴" if abs(data['difference']) > 5 else "🟡") - diff_text = f"{data['difference']:+.1f}%" - - st.markdown(f""" - **{data['class']}** {diff_color} - - {model1['encoder_name']}: {data['model1_pct']:.1f}% - - {model2['encoder_name']}: {data['model2_pct']:.1f}% - - Différence: {diff_text} - """) + # 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] - with col2: - st.markdown("**🎯 Légende:**") - st.markdown("🟢 Différence < 1%") - st.markdown("🟡 Différence 1-5%") - st.markdown("🔴 Différence > 5%") + # 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: @@ -358,37 +516,54 @@ with st.sidebar: # 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"**Run ID:** {best_model.get('run_id', 'N/A')[:8]}...") + 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"**Run ID:** {second_model.get('run_id', 'N/A')[:8]}...") - - st.write(f"**Classes:** {models_info.get('num_classes', '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)") - # Affichage avec carrés colorés + # Affichage avec carrés colorés et IoU if models_info and 'class_names' in models_info and 'class_colors' in models_info: class_names = models_info['class_names'] class_colors = models_info['class_colors'] + # Récupérer les IoU des modèles + best_ious = best_model.get('class_ious', {}) if models_info else {} + second_ious = second_model.get('class_ious', {}) if models_info else {} + for i, (class_name, color) in enumerate(zip(class_names, class_colors)): color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}" + # Construire la chaîne IoU + iou_str = "" + if best_ious.get(class_name) is not None and second_ious.get(class_name) is not None: + best_iou = best_ious[class_name] + second_iou = second_ious[class_name] + iou_str = f" (🥇{best_iou:.2f} - 🥈{second_iou:.2f})" + elif best_ious.get(class_name) is not None: + best_iou = best_ious[class_name] + iou_str = f" (🥇{best_iou:.2f})" + elif second_ious.get(class_name) is not None: + second_iou = second_ious[class_name] + iou_str = f" (🥈{second_iou:.2f})" + st.markdown(f"""
- {i}. {class_name} + {i}. {class_name}{iou_str}
""", unsafe_allow_html=True) diff --git a/utils/utils.py b/utils/utils.py index 39047f5..0a58a91 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -102,38 +102,6 @@ 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): """ @@ -200,170 +168,6 @@ 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, show_plot=True):