This commit is contained in:
Grégoire Mureau 2025-09-02 17:10:52 +02:00
commit 133436cc3e
6 changed files with 871 additions and 975 deletions

0
EV3 Normal file
View file

View file

@ -1,24 +1,27 @@
import os import os
import sys import sys
import traceback
import tempfile import tempfile
import shutil import shutil
from datetime import datetime from datetime import datetime
from typing import Optional, List from typing import Optional, List
from pathlib import Path from pathlib import Path
import mlflow import mlflow
from fastapi import FastAPI, HTTPException, File, UploadFile, Form from fastapi import FastAPI, HTTPException, File, UploadFile
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from pydantic import BaseModel from pydantic import BaseModel
import numpy as np import numpy as np
import pickle import pickle
import base64 import base64
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
# Configuration MLflow avec variables d'environnement # Configuration MLflow avec variables d'environnement
def configure_mlflow(): def configure_mlflow():
"""Configure MLflow avec les variables d'environnement""" """Configure MLflow avec les variables d'environnement"""
print("=== DEBUG VARIABLES D'ENVIRONNEMENT ===") print("=== DEBUG VARIABLES D'ENVIRONNEMENT ===")
# ✅ NETTOYAGE automatique des variables d'environnement
mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI", "").strip() mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI", "").strip()
mlflow_s3_endpoint_url = os.getenv("MLFLOW_S3_ENDPOINT_URL", "").strip() mlflow_s3_endpoint_url = os.getenv("MLFLOW_S3_ENDPOINT_URL", "").strip()
aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID", "").strip() aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID", "").strip()
@ -33,11 +36,9 @@ def configure_mlflow():
if not mlflow_tracking_uri: if not mlflow_tracking_uri:
raise ValueError("MLFLOW_TRACKING_URI not found in environment variables") raise ValueError("MLFLOW_TRACKING_URI not found in environment variables")
# Configuration MLflow avec URLs nettoyées
mlflow.set_tracking_uri(mlflow_tracking_uri) mlflow.set_tracking_uri(mlflow_tracking_uri)
print(f"MLflow 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: 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)") 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 os.environ["AWS_SECRET_ACCESS_KEY"] = aws_secret_access_key
print(f"MLflow S3 Endpoint: {mlflow_s3_endpoint_url}") print(f"MLflow S3 Endpoint: {mlflow_s3_endpoint_url}")
print("✅ Identifiants AWS configurés") print("✅ Identifiants AWS configurés")
print("✅ Configuration MLflow terminée")
# ✅ 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)")
# Import des fonctions utils # Import des fonctions utils
sys.path.append('.') sys.path.append('.')
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils.utils import ( from utils.utils import (
load_cityscapes_config, load_cityscapes_config,
load_best_model_from_registry, load_model,
run_inference_and_visualize, run_inference_and_visualize,
colorize_mask 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 # Configuration
MODEL_NAME = "StreetsSegmentation" MODEL_NAME = "StreetsSegmentation"
SAMPLE_IMAGES_DIR = "./notebooks/content/data/test_images_sample" SAMPLE_IMAGES_DIR = "./notebooks/content/data/test_images_sample"
CITYSCAPES_CONFIG_PATH = "./cityscapes_config.json" CITYSCAPES_CONFIG_PATH = "./cityscapes_config.json"
TEMP_UPLOAD_DIR = "/tmp/uploads" 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): class ImageInfo(BaseModel):
filename: str filename: str
display_name: str display_name: str
has_ground_truth: bool 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): class AvailableImagesResponse(BaseModel):
images: List[ImageInfo] images: List[ImageInfo]
total_count: int total_count: int
# Variables globales pour le modèle # Variables globales pour les deux modèles
model = None best_model = None
encoder_name = None second_best_model = None
script_img_size = None best_model_info = {}
second_best_model_info = {}
mapping_config = None mapping_config = None
run_id = None
def initialize_model(): def get_model_detailed_info(run_id, mapping_config):
"""Initialise le modèle et la configuration au démarrage""" """Récupère les informations détaillées d'un modèle depuis MLflow"""
global model, encoder_name, script_img_size, mapping_config, run_id 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: try:
# Configuration MLflow
print("🔄 Configuration de MLflow...") print("🔄 Configuration de MLflow...")
configure_mlflow() configure_mlflow()
@ -113,76 +159,99 @@ def initialize_model():
mapping_config = load_cityscapes_config(CITYSCAPES_CONFIG_PATH, verbose=False) mapping_config = load_cityscapes_config(CITYSCAPES_CONFIG_PATH, verbose=False)
print("✅ Configuration Cityscapes chargée") print("✅ Configuration Cityscapes chargée")
print("🔄 Chargement du modèle depuis MLflow...") print("🔄 Chargement du meilleur modèle...")
print(f"🔍 Recherche du modèle: {MODEL_NAME}") best_model, best_run_id, best_encoder_name, best_img_size = load_model(
model, run_id, encoder_name, script_img_size = load_best_model_from_registry(MODEL_NAME) experiment_name="OC Projet 9",
print("✅ Modèle téléchargé depuis MLflow") metric="test_mean_iou",
top_n=1
)
print(f"✅ Modèle chargé du run {run_id}") # Récupérer les infos détaillées du meilleur modèle
print(f"📐 Encoder: {encoder_name}") best_detailed_info = get_model_detailed_info(best_run_id, mapping_config)
print(f"📐 Input shape: {script_img_size}")
print(f"🎨 Nombre de classes: {mapping_config['num_classes']}") 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) os.makedirs(TEMP_UPLOAD_DIR, exist_ok=True)
except Exception as e: except Exception as e:
print(f"❌ Erreur lors de l'initialisation: {e}") print(f"❌ Erreur lors de l'initialisation: {e}")
import traceback
traceback.print_exc() traceback.print_exc()
raise e raise e
# Initialisation de l'API # Initialisation de l'API
app = FastAPI( app = FastAPI(
title="Multi-Class Segmentation API", title="Multi-Class Segmentation Comparison API",
description="API de segmentation sémantique pour véhicules autonomes", description="API de comparaison de segmentation sémantique avec deux modèles",
version="1.0.0" version="2.0.0"
) )
# ✅ AJOUT: Initialiser le modèle de manière plus robuste
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
"""Initialise le modèle au démarrage de l'API"""
try: try:
initialize_model() initialize_models()
print("🚀 API démarrée avec succès - Modèle chargé!") print("🚀 API démarrée avec succès - Modèles chargés!")
except Exception as e: except Exception as e:
print(f"⚠️ ATTENTION: Impossible de charger le modèle au démarrage: {e}") print(f"⚠️ ATTENTION: Impossible de charger les modèles au démarrage: {e}")
print("L'API démarre quand même mais les prédictions ne fonctionneront pas.")
@app.get("/") @app.get("/")
def root(): def root():
return { return {
"message": "Multi-Class Segmentation API - Online", "message": "Multi-Class Segmentation Comparison API - Online",
"model_loaded": model is not None, "models_loaded": best_model is not None and second_best_model is not None,
"model_run_id": run_id, "best_model": best_model_info if best_model else None,
"encoder": encoder_name, "second_best_model": second_best_model_info if second_best_model else None,
"num_classes": mapping_config['num_classes'] if mapping_config else None "num_classes": mapping_config['num_classes'] if mapping_config else None
} }
@app.get("/health") @app.get("/health")
def health_check(): def health_check():
"""Health check endpoint pour nginx""" if best_model is None or second_best_model is None or mapping_config is None:
if model is None or mapping_config is None: raise HTTPException(status_code=503, detail="Models not loaded")
raise HTTPException(status_code=503, detail="Model not loaded")
return { return {
"status": "healthy", "status": "healthy",
"model_loaded": True, "models_loaded": True,
"timestamp": datetime.utcnow().isoformat() "timestamp": datetime.utcnow().isoformat()
} }
@app.get("/models", response_model=dict) @app.get("/models", response_model=dict)
def get_model_info(): def get_models_info():
"""Retourne les informations du modèle chargé""" if best_model is None or second_best_model is None:
if model is None: raise HTTPException(status_code=503, detail="Models not loaded")
raise HTTPException(status_code=503, detail="Model not loaded")
return { return {
"model_name": MODEL_NAME, "best_model": best_model_info,
"run_id": run_id, "second_best_model": second_best_model_info,
"encoder": encoder_name,
"input_size": script_img_size,
"num_classes": mapping_config['num_classes'], "num_classes": mapping_config['num_classes'],
"class_names": mapping_config['group_names'], "class_names": mapping_config['group_names'],
"class_colors": mapping_config['group_colors'].tolist() "class_colors": mapping_config['group_colors'].tolist()
@ -190,238 +259,308 @@ def get_model_info():
@app.get("/sample-images", response_model=AvailableImagesResponse) @app.get("/sample-images", response_model=AvailableImagesResponse)
def get_sample_images(): def get_sample_images():
"""Retourne la liste des images d'exemple disponibles"""
try: try:
original_dir = Path(SAMPLE_IMAGES_DIR) / "original" original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
mask_dir = Path(SAMPLE_IMAGES_DIR) / "mask" mask_dir = Path(SAMPLE_IMAGES_DIR) / "mask"
if not original_dir.exists(): if not original_dir.exists():
raise HTTPException(status_code=404, detail="Sample images directory not found") raise HTTPException(status_code=404, detail="Sample images directory not found")
images = [] images = []
for img_path in original_dir.glob("*leftImg8bit.png"): for img_path in original_dir.glob("*leftImg8bit.png"):
# Construire le nom du masque correspondant
base_name = img_path.stem.replace("_leftImg8bit", "") base_name = img_path.stem.replace("_leftImg8bit", "")
mask_name = f"{base_name}_gtFine_labelIds.png" mask_name = f"{base_name}_gtFine_labelIds.png"
mask_path = mask_dir / mask_name mask_path = mask_dir / mask_name
# Créer un nom d'affichage plus convivial
display_name = base_name.replace("_", " ").title() display_name = base_name.replace("_", " ").title()
images.append(ImageInfo( images.append(ImageInfo(
filename=img_path.name, filename=img_path.name,
display_name=display_name, display_name=display_name,
has_ground_truth=mask_path.exists() has_ground_truth=mask_path.exists()
)) ))
# Trier par nom pour un affichage cohérent
images.sort(key=lambda x: x.filename) 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: except Exception as e:
raise HTTPException(status_code=500, detail=f"Error listing sample images: {str(e)}") raise HTTPException(status_code=500, detail=f"Error listing sample images: {str(e)}")
@app.get("/sample-image/{filename}") @app.get("/sample-image/{filename}")
def get_sample_image(filename: str): def get_sample_image(filename: str):
"""Sert une image d'exemple pour prévisualisation"""
try: try:
print(f"🔍 DEBUG: Demande d'image: {filename}")
original_dir = Path(SAMPLE_IMAGES_DIR) / "original" original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
image_path = original_dir / filename 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(): 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") 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'): if not filename.endswith('_leftImg8bit.png'):
print(f"❌ DEBUG: Format invalide: {filename}")
raise HTTPException(status_code=400, detail="Invalid image filename format") raise HTTPException(status_code=400, detail="Invalid image filename format")
print(f"✅ DEBUG: Envoi de l'image: {filename}")
return FileResponse( return FileResponse(
image_path, image_path,
media_type="image/png", media_type="image/png",
headers={"Content-Disposition": f"inline; filename={filename}"} headers={"Content-Disposition": f"inline; filename={filename}"}
) )
except HTTPException: except HTTPException:
# Re-raise les HTTPException pour qu'elles soient gérées correctement
raise raise
except Exception as e: 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)}") raise HTTPException(status_code=500, detail=f"Error serving image: {str(e)}")
class SampleImageRequest(BaseModel): class SampleImageRequest(BaseModel):
filename: str filename: str
@app.post("/predict-sample", response_model=PredictionResult) @app.post("/compare-sample", response_model=ComparisonResult)
def predict_sample_image(request: SampleImageRequest): def compare_sample_image(request: SampleImageRequest):
"""Lance une prédiction sur une image d'exemple""" if best_model is None or second_best_model is None or mapping_config is None:
if model is None or mapping_config is None: raise HTTPException(status_code=503, detail="Models not loaded")
raise HTTPException(status_code=503, detail="Model not loaded")
try: try:
# Vérifier que l'image existe
original_dir = Path(SAMPLE_IMAGES_DIR) / "original" original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
image_path = original_dir / request.filename image_path = original_dir / request.filename
if not image_path.exists(): if not image_path.exists():
raise HTTPException(status_code=404, detail=f"Sample image {request.filename} not found") 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", "") base_name = image_path.stem.replace("_leftImg8bit", "")
mask_name = f"{base_name}_gtFine_labelIds.png" mask_name = f"{base_name}_gtFine_labelIds.png"
mask_path = Path(SAMPLE_IMAGES_DIR) / "mask" / mask_name mask_path = Path(SAMPLE_IMAGES_DIR) / "mask" / mask_name
mask_path_str = str(mask_path) if mask_path.exists() else None mask_path_str = str(mask_path) if mask_path.exists() else None
print(f"🔮 Lancement de l'inférence sur {request.filename}") # Inférence modèle 1 avec sa résolution spécifique
pred1, t1, fig1 = run_inference_and_visualize(
# Lancer l'inférence (sans affichage pour l'API)
predicted_mask, fig = run_inference_and_visualize(
image_path=str(image_path), image_path=str(image_path),
mask_path=mask_path_str, mask_path=mask_path_str,
model=model, model=best_model,
encoder_name=encoder_name, encoder_name=None, # Normalisé de la même manière
img_size=script_img_size, img_size=best_model_info["input_size"], # Utilise la résolution du modèle
mapping_config=mapping_config, mapping_config=mapping_config,
save_dir=None, save_dir=None, show_stats=False, show_plot=False
show_stats=False, )
show_plot=False # ← AJOUTÉ pour récupérer la figure # 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 # Créer une figure combinée avec les deux modèles
fig_bytes = pickle.dumps(fig) # Nombre de colonnes : 3 si pas de GT, 4 si GT disponible
fig_b64 = base64.b64encode(fig_bytes).decode() 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 # Récupérer les images depuis les figures individuelles
unique_classes, counts = np.unique(predicted_mask, return_counts=True) axes1 = fig1.get_axes()
total_pixels = predicted_mask.size axes2 = fig2.get_axes()
stats = {} # Ligne 1 : Modèle 1
for class_id, count in zip(unique_classes, counts): for i in range(min(len(axes1), num_cols)):
if class_id < len(mapping_config['group_names']): ax_src = axes1[i]
percentage = (count / total_pixels) * 100 ax_dst = axes[0, i]
stats[mapping_config['group_names'][class_id]] = {
"pixels": int(count),
"percentage": round(percentage, 1)
}
return PredictionResult( # Copier toutes les images de l'axe source
success=True, for img_obj in ax_src.get_images():
message="Prediction completed successfully", ax_dst.imshow(
image_path=str(image_path), img_obj.get_array(),
mask_path=mask_path_str, cmap=img_obj.get_cmap(),
prediction_stats=stats, alpha=img_obj.get_alpha(),
ground_truth_available=mask_path.exists(), vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
)
# Copier le titre avec nom du modèle
original_title = ax_src.get_title()
if i == 0:
new_title = "Image originale"
elif "Prédit" in original_title:
new_title = f"Prédiction {best_model_info['architecture']} - {best_model_info['encoder_name']}"
else:
new_title = original_title
ax_dst.set_title(new_title)
ax_dst.axis('off')
# Ligne 2 : Modèle 2
for i in range(min(len(axes2), num_cols)):
ax_src = axes2[i]
ax_dst = axes[1, i]
# Copier toutes les images de l'axe source
for img_obj in ax_src.get_images():
ax_dst.imshow(
img_obj.get_array(),
cmap=img_obj.get_cmap(),
alpha=img_obj.get_alpha(),
vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
)
# Copier le titre avec nom du modèle
original_title = ax_src.get_title()
if i == 0:
new_title = "Image originale"
elif "Prédit" in original_title:
new_title = f"Prédiction {second_best_model_info['architecture']} - {second_best_model_info['encoder_name']}"
else:
new_title = original_title
ax_dst.set_title(new_title)
ax_dst.axis('off')
plt.tight_layout()
# Fermer les figures individuelles pour libérer la mémoire
plt.close(fig1)
plt.close(fig2)
fig_b64 = base64.b64encode(pickle.dumps(fig)).decode()
# Stats
def calc(mask):
uc, cnts = np.unique(mask, return_counts=True)
stats = {}
for cid, c in zip(uc, cnts):
if cid < len(mapping_config['group_names']):
stats[mapping_config['group_names'][cid]] = {
"pixels": int(c),
"percentage": round((c/mask.size)*100, 1)
}
return stats
stats1, stats2 = calc(pred1), calc(pred2)
faster = best_model_info['encoder_name'] if t1 < t2 else second_best_model_info['encoder_name']
speedup = round(max(t1, t2) / min(t1, t2), 2)
speed_comp = {"faster_model": faster, "speedup_ratio": speedup, "time_difference_ms": abs(t1-t2)*1000}
return ComparisonResult(
success=True, message="Comparison completed successfully",
model1_info=ModelInfo(**best_model_info), model2_info=ModelInfo(**second_best_model_info),
image_path=str(image_path), mask_path=mask_path_str,
model1_stats=stats1, model2_stats=stats2,
model1_inference_time=t1, model2_inference_time=t2,
speed_comparison=speed_comp, ground_truth_available=mask_path.exists(),
figure_data=fig_b64 figure_data=fig_b64
) )
except Exception as e: except Exception as e:
print(f"❌ Erreur lors de la prédiction: {e}") raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Prediction error: {str(e)}")
@app.post("/predict-upload", response_model=PredictionResult) @app.post("/compare-upload", response_model=ComparisonResult)
def predict_uploaded_image(file: UploadFile = File(...)): def compare_uploaded_image(file: UploadFile = File(...)):
"""Lance une prédiction sur une image uploadée""" if best_model is None or second_best_model is None or mapping_config is None:
if model is None or mapping_config is None: raise HTTPException(status_code=503, detail="Models not loaded")
raise HTTPException(status_code=503, detail="Model not loaded")
# Vérifier le type de fichier
allowed_types = ["image/jpeg", "image/jpg", "image/png"] allowed_types = ["image/jpeg", "image/jpg", "image/png"]
if file.content_type not in allowed_types: if file.content_type not in allowed_types:
raise HTTPException( raise HTTPException(status_code=400, detail=f"Invalid file type. Allowed: {', '.join(allowed_types)}")
status_code=400,
detail=f"Invalid file type. Allowed: {', '.join(allowed_types)}"
)
temp_file_path = None temp_file_path = None
try: try:
# Créer un fichier temporaire
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
temp_filename = f"upload_{timestamp}_{file.filename}" temp_filename = f"upload_{timestamp}_{file.filename}"
temp_file_path = os.path.join(TEMP_UPLOAD_DIR, temp_filename) temp_file_path = os.path.join(TEMP_UPLOAD_DIR, temp_filename)
# Sauvegarder le fichier uploadé
with open(temp_file_path, "wb") as buffer: with open(temp_file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer) shutil.copyfileobj(file.file, buffer)
# Inférences avec résolutions spécifiques
print(f"🔮 Lancement de l'inférence sur {file.filename}") pred1, t1, fig1 = run_inference_and_visualize(
image_path=temp_file_path, mask_path=None,
# Lancer l'inférence (sans masque de vérité) model=best_model, encoder_name=None, # Normalisé de la même manière
predicted_mask, fig = run_inference_and_visualize( img_size=best_model_info["input_size"], mapping_config=mapping_config,
image_path=temp_file_path, save_dir=None, show_stats=False, show_plot=False,
mask_path=None, # Pas de masque de vérité pour les uploads )
model=model, pred2, t2, fig2 = run_inference_and_visualize(
encoder_name=encoder_name, image_path=temp_file_path, mask_path=None,
img_size=script_img_size, model=second_best_model, encoder_name=None, # Normalisé de la même manière
mapping_config=mapping_config, img_size=second_best_model_info["input_size"], mapping_config=mapping_config,
save_dir=None, save_dir=None, show_stats=False, show_plot=False,
show_stats=False,
show_plot=False # ← AJOUTÉ pour récupérer la figure
) )
# Sérialiser la figure # Figure combinée pour uploads (pas de GT, donc 2 colonnes seulement)
fig_bytes = pickle.dumps(fig) fig, axes = plt.subplots(2, 2, figsize=(10, 8))
fig_b64 = base64.b64encode(fig_bytes).decode()
# Calculer les statistiques # Récupérer les images depuis les figures individuelles
unique_classes, counts = np.unique(predicted_mask, return_counts=True) axes1 = fig1.get_axes()
total_pixels = predicted_mask.size axes2 = fig2.get_axes()
stats = {} # Ligne 1 : Modèle 1 (Image originale + Prédiction)
for class_id, count in zip(unique_classes, counts): for i in range(min(len(axes1), 2)):
if class_id < len(mapping_config['group_names']): ax_src = axes1[i]
percentage = (count / total_pixels) * 100 ax_dst = axes[0, i]
stats[mapping_config['group_names'][class_id]] = {
"pixels": int(count),
"percentage": round(percentage, 1)
}
return PredictionResult( # Copier toutes les images de l'axe source
success=True, for img_obj in ax_src.get_images():
message="Prediction completed successfully", ax_dst.imshow(
img_obj.get_array(),
cmap=img_obj.get_cmap(),
alpha=img_obj.get_alpha(),
vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
)
# Copier le titre avec nom du modèle
original_title = ax_src.get_title()
if i == 0:
new_title = "Image originale"
else:
new_title = f"Prédiction {best_model_info['architecture']} - {best_model_info['encoder_name']}"
ax_dst.set_title(new_title)
ax_dst.axis('off')
# Ligne 2 : Modèle 2 (Image originale + Prédiction)
for i in range(min(len(axes2), 2)):
ax_src = axes2[i]
ax_dst = axes[1, i]
# Copier toutes les images de l'axe source
for img_obj in ax_src.get_images():
ax_dst.imshow(
img_obj.get_array(),
cmap=img_obj.get_cmap(),
alpha=img_obj.get_alpha(),
vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
)
# Copier le titre avec nom du modèle
original_title = ax_src.get_title()
if i == 0:
new_title = "Image originale"
else:
new_title = f"Prédiction {second_best_model_info['architecture']} - {second_best_model_info['encoder_name']}"
ax_dst.set_title(new_title)
ax_dst.axis('off')
plt.tight_layout()
# Fermer les figures individuelles pour libérer la mémoire
plt.close(fig1)
plt.close(fig2)
fig_b64 = base64.b64encode(pickle.dumps(fig)).decode()
# Stats
def calc(mask):
uc, cnts = np.unique(mask, return_counts=True)
stats = {}
for cid, c in zip(uc, cnts):
if cid < len(mapping_config['group_names']):
stats[mapping_config['group_names'][cid]] = {
"pixels": int(c),
"percentage": round((c/mask.size)*100, 1)
}
return stats
stats1, stats2 = calc(pred1), calc(pred2)
faster = best_model_info['encoder_name'] if t1 < t2 else second_best_model_info['encoder_name']
speedup = round(max(t1, t2) / min(t1, t2), 2)
speed_comp = {"faster_model": faster, "speedup_ratio": speedup, "time_difference_ms": abs(t1-t2)*1000}
return ComparisonResult(
success=True, message="Comparison completed successfully",
model1_info=ModelInfo(**best_model_info), model2_info=ModelInfo(**second_best_model_info),
image_path=temp_file_path, image_path=temp_file_path,
prediction_stats=stats, model1_stats=stats1, model2_stats=stats2,
ground_truth_available=False, model1_inference_time=t1, model2_inference_time=t2,
speed_comparison=speed_comp, ground_truth_available=False,
figure_data=fig_b64 figure_data=fig_b64
) )
except Exception as e: except Exception as e:
print(f"❌ Erreur lors de la prédiction: {e}") raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Prediction error: {str(e)}")
finally: finally:
# Nettoyer le fichier temporaire
if temp_file_path and os.path.exists(temp_file_path): if temp_file_path and os.path.exists(temp_file_path):
try: try: os.remove(temp_file_path)
os.remove(temp_file_path) except Exception: pass
print(f"🗑️ Fichier temporaire supprimé: {temp_filename}")
except Exception as e:
print(f"⚠️ Impossible de supprimer le fichier temporaire: {e}")
@app.get("/metrics") @app.get("/metrics")
def get_metrics(): def get_metrics():
"""Endpoint de métriques pour monitoring"""
return { return {
"model_loaded": model is not None, "models_loaded": best_model is not None and second_best_model is not None,
"model_name": MODEL_NAME, "best_model_info": best_model_info,
"run_id": run_id, "second_best_model_info": second_best_model_info,
"num_classes": mapping_config['num_classes'] if mapping_config else None, "num_classes": mapping_config['num_classes'] if mapping_config else None,
"timestamp": datetime.utcnow().isoformat() "timestamp": datetime.utcnow().isoformat()
} }

View file

@ -8,37 +8,44 @@ uvicorn==0.35.0
starlette==0.46.2 starlette==0.46.2
pydantic==2.11.7 pydantic==2.11.7
pydantic_core==2.33.2 pydantic_core==2.33.2
python-multipart==0.0.20 # uploads FastAPI
# ===== STREAMLIT ===== # ===== STREAMLIT =====
streamlit==1.46.1 streamlit==1.46.1
altair==5.5.0 # Utilisé par Streamlit pour les graphiques altair==5.5.0 # utilisé par Streamlit
blinker==1.9.0 # Streamlit dependency blinker==1.9.0 # dépendance Streamlit
# ===== HTTP REQUESTS ===== # ===== HTTP CLIENTS =====
requests==2.32.4 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 httpcore==1.0.9
h11==0.16.0 h11==0.16.0
# ===== MACHINE LEARNING CORE ===== # ===== MACHINE LEARNING CORE (PyTorch) =====
tensorflow==2.19.0 torch==2.8.0 # version stable PyTorch
keras==3.10.0 torchvision==0.23.0 # utile pour les modèles pré-entraînés
numpy>=1.24.0,<2.0.0 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+ numpy>=1.24.0,<2.0.0
# Les fonctionnalités sont maintenant intégrées dans TensorFlow core scipy==1.16.0
scikit-learn==1.7.0
# ===== COMPUTER VISION ===== # ===== COMPUTER VISION =====
pillow==11.3.0 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 scikit-image==0.25.2
imageio==2.37.0 imageio==2.37.0
# ===== DATA SCIENCE ===== # ===== DATA SCIENCE =====
pandas==2.3.0 pandas==2.3.0
matplotlib==3.10.3 matplotlib==3.10.3
scipy==1.16.0 networkx==3.5
scikit-learn==1.7.0 tifffile==2025.6.11
lazy_loader==0.4
# ===== MLFLOW & MODEL REGISTRY ===== # ===== MLFLOW & MODEL REGISTRY =====
mlflow==3.2.0 mlflow==3.2.0
@ -50,34 +57,7 @@ boto3==1.39.2
botocore==1.39.2 botocore==1.39.2
s3transfer==0.13.0 s3transfer==0.13.0
# ===== SEGMENTATION MODELS ===== # ===== UTILITIES =====
# 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
click==8.2.1 click==8.2.1
tqdm==4.67.1 tqdm==4.67.1
joblib==1.5.1 joblib==1.5.1
@ -85,57 +65,6 @@ python-dateutil
packaging packaging
typing_extensions==4.14.0 typing_extensions==4.14.0
# File handling # ===== MATPLOTLIB ADDONS =====
python-multipart==0.0.20 # Pour FastAPI file uploads cycler==0.12.1
# ===== 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)
fonttools==4.58.5 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 .

File diff suppressed because it is too large Load diff

View file

@ -606,13 +606,13 @@ tokenizers==0.21.4
toml==0.10.2 toml==0.10.2
tomlkit==0.13.3 tomlkit==0.13.3
toolz==0.12.1 toolz==0.12.1
torch==2.8.0+cu126 torch==2.8.0
torchao==0.10.0 torchao==0.10.0
torchaudio==2.8.0+cu126 torchaudio==2.8.0
torchdata==0.11.0 torchdata==0.11.0
torchsummary==1.5.1 torchsummary==1.5.1
torchtune==0.6.1 torchtune==0.6.1
torchvision==0.23.0+cu126 torchvision==0.23.0
tornado==6.4.2 tornado==6.4.2
tqdm==4.67.1 tqdm==4.67.1
traitlets==5.7.1 traitlets==5.7.1

View file

@ -3,7 +3,6 @@ import json
import tempfile import tempfile
import numpy as np import numpy as np
import torch import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import segmentation_models_pytorch as smp import segmentation_models_pytorch as smp
import mlflow import mlflow
@ -16,8 +15,8 @@ import matplotlib.patches as mpatches
from matplotlib.colors import ListedColormap from matplotlib.colors import ListedColormap
# Preprocessing/Images # Preprocessing/Images
from PIL import Image import albumentations as A
import torchvision.transforms as transforms from albumentations.pytorch import ToTensorV2
def load_cityscapes_config(config_path="../cityscapes_config.json", verbose=True): def load_cityscapes_config(config_path="../cityscapes_config.json", verbose=True):
@ -103,48 +102,12 @@ def colorize_mask(mask, group_colors):
return colored_mask 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): def get_preprocessing_fn(img_size=(512, 512), encoder=None):
""" """
Retourne une fonction qui prétraite à la fois les images et les masques Retourne une fonction qui prétraite à la fois les images et les masques
pour l'inférence, en reproduisant exactement le pipeline CityscapesDataset. 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: if encoder is None:
mean, std = [0.0, 0.0, 0.0], [1.0, 1.0, 1.0] 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 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 # Version mise à jour de run_inference_and_visualize
def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config, 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 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') fig.savefig(fig_path, dpi=150, bbox_inches='tight')
print(f"💾 Résultat sauvegardé: {fig_path}") print(f"💾 Résultat sauvegardé: {fig_path}")
plt.show()
# Statistiques # Statistiques
if show_stats: 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 percentage = (count / mask_gt_np.size) * 100
print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)") 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", 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): def load_model_by_run_id(run_id):
"""Charge un modèle par run_id spécifique""" """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)