Docker working. Improvements incoming.

This commit is contained in:
Grégoire Mureau 2025-09-02 10:46:13 +02:00
parent 9377a77448
commit cdd6f9645b
6 changed files with 530 additions and 781 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,68 @@ 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
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 initialize_models():
"""Initialise le modèle et la configuration au démarrage""" """Initialise les deux meilleurs modèles au démarrage"""
global model, encoder_name, script_img_size, mapping_config, run_id 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 +116,83 @@ 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
)
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("🔄 Chargement du deuxième meilleur modèle...")
print(f"📐 Encoder: {encoder_name}") second_best_model, second_run_id, second_encoder_name, second_img_size = load_model(
print(f"📐 Input shape: {script_img_size}") experiment_name="OC Projet 9",
print(f"🎨 Nombre de classes: {mapping_config['num_classes']}") 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) 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 +200,199 @@ 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
pred1, t1, _ = 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,
img_size=script_img_size, img_size=best_model_info["input_size"],
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
# Sérialiser la figure pred2, t2, _ = run_inference_and_visualize(
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",
image_path=str(image_path), image_path=str(image_path),
mask_path=mask_path_str, mask_path=mask_path_str,
prediction_stats=stats, model=second_best_model,
ground_truth_available=mask_path.exists(), 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 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
print(f"🔮 Lancement de l'inférence sur {file.filename}") pred1, t1, _ = 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,
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,
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
) )
pred2, t2, _ = run_inference_and_visualize(
# Sérialiser la figure image_path=temp_file_path, mask_path=None,
fig_bytes = pickle.dumps(fig) model=second_best_model, encoder_name=None,
fig_b64 = base64.b64encode(fig_bytes).decode() img_size=second_best_model_info["input_size"], mapping_config=mapping_config,
save_dir=None, show_stats=False, show_plot=False,
# Calculer les statistiques )
unique_classes, counts = np.unique(predicted_mask, return_counts=True) # Figure
total_pixels = predicted_mask.size fig, axes = plt.subplots(1, 3, figsize=(15, 5))
img_arr = mpimg.imread(temp_file_path)
stats = {} axes[0].imshow(img_arr); axes[0].set_title("Image originale"); axes[0].axis('off')
for class_id, count in zip(unique_classes, counts): axes[1].imshow(colorize_mask(pred1, mapping_config['group_colors']))
if class_id < len(mapping_config['group_names']): axes[1].set_title(f"Prédiction {best_model_info['encoder_name']}"); axes[1].axis('off')
percentage = (count / total_pixels) * 100 axes[2].imshow(colorize_mask(pred2, mapping_config['group_colors']))
stats[mapping_config['group_names'][class_id]] = { axes[2].set_title(f"Prédiction {second_best_model_info['encoder_name']}"); axes[2].axis('off')
"pixels": int(count), plt.tight_layout()
"percentage": round(percentage, 1) fig_b64 = base64.b64encode(pickle.dumps(fig)).decode()
} # Stats
def calc(mask):
return PredictionResult( uc, cnts = np.unique(mask, return_counts=True)
success=True, stats = {}
message="Prediction completed successfully", 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 .

View file

@ -10,25 +10,21 @@ import base64
import pickle import pickle
# Configuration de l'API # Configuration de l'API
# prod docker
API_BASE_URL = os.getenv("MULTISEG_API_BASE_URL", "http://127.0.0.1:8080/api") 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 # Import des fonctions utils pour l'affichage local
sys.path.append('/app') sys.path.append('/app')
# APRÈS (pour test local)
sys.path.append('.') sys.path.append('.')
from utils.utils import colorize_mask from utils.utils import colorize_mask
# Configuration de la page # Configuration de la page
st.set_page_config( st.set_page_config(
page_title="Multi-Class Segmentation", page_title="Multi-Class Segmentation Comparison",
page_icon="🚗", page_icon="🔄",
layout="wide" layout="wide"
) )
# CSS personnalisé # CSS personnalisé mis à jour
st.markdown(""" st.markdown("""
<style> <style>
.main-header { .main-header {
@ -39,6 +35,23 @@ st.markdown("""
color: white; color: white;
text-align: center; text-align: center;
} }
.comparison-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 1.5rem;
border-radius: 10px;
border-left: 4px solid #667eea;
margin: 1rem 0;
color: #2c3e50;
font-weight: 500;
}
.speed-winner {
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
border-left-color: #28a745;
}
.speed-slower {
background: linear-gradient(135deg, #f8d7da 0%, #f1b0b7 100%);
border-left-color: #dc3545;
}
.metric-card { .metric-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 1rem; padding: 1rem;
@ -60,33 +73,19 @@ st.markdown("""
# En-tête principal # En-tête principal
st.markdown(""" st.markdown("""
<div class="main-header"> <div class="main-header">
<h1>🚗 Segmentation Multi-Classes</h1> <h1>🔄 Comparaison de Segmentation Multi-Classes</h1>
<p>Système de segmentation sémantique pour véhicules autonomes</p> <p>Système de comparaison entre deux modèles de segmentation sémantique</p>
</div> </div>
""", unsafe_allow_html=True) """, unsafe_allow_html=True)
# Initialisation des états def get_models_info():
if "prediction_result" not in st.session_state: """Récupère les informations des deux modèles"""
st.session_state.prediction_result = None
if "selected_image" not in st.session_state:
st.session_state.selected_image = None
if "uploaded_file" not in st.session_state:
st.session_state.uploaded_file = None
def reset_state():
"""Reset des états de session"""
st.session_state.prediction_result = None
st.session_state.selected_image = None
st.session_state.uploaded_file = None
def get_model_info():
"""Récupère les informations du modèle"""
try: try:
response = requests.get(f"{API_BASE_URL}/models") response = requests.get(f"{API_BASE_URL}/models")
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
else: else:
st.error(f"Erreur lors de la récupération des infos modèle: {response.status_code}") st.error(f"Erreur lors de la récupération des infos modèles: {response.status_code}")
return None return None
except Exception as e: except Exception as e:
st.error(f"Erreur de connexion à l'API: {e}") st.error(f"Erreur de connexion à l'API: {e}")
@ -105,15 +104,14 @@ def get_sample_images():
st.error(f"Erreur de connexion à l'API: {e}") st.error(f"Erreur de connexion à l'API: {e}")
return None return None
def predict_sample_image(filename): def compare_sample_image(filename):
"""Lance une prédiction sur une image d'exemple""" """Lance une comparaison sur une image d'exemple"""
try: try:
# CORRECTION: Envoyer en JSON au lieu de form data
data = {"filename": filename} data = {"filename": filename}
response = requests.post( response = requests.post(
f"{API_BASE_URL}/predict-sample", f"{API_BASE_URL}/compare-sample",
json=data, # ← Changé de 'data=' à 'json=' json=data,
headers={"Content-Type": "application/json"} # ← Ajout du header explicite headers={"Content-Type": "application/json"}
) )
if response.status_code == 200: if response.status_code == 200:
@ -122,14 +120,14 @@ def predict_sample_image(filename):
st.error(f"Erreur API: {response.status_code} - {response.text}") st.error(f"Erreur API: {response.status_code} - {response.text}")
return None return None
except Exception as e: except Exception as e:
st.error(f"Erreur lors de la prédiction: {e}") st.error(f"Erreur lors de la comparaison: {e}")
return None return None
def predict_uploaded_image(uploaded_file): def compare_uploaded_image(uploaded_file):
"""Lance une prédiction sur une image uploadée""" """Lance une comparaison sur une image uploadée"""
try: try:
files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)} files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)}
response = requests.post(f"{API_BASE_URL}/predict-upload", files=files) response = requests.post(f"{API_BASE_URL}/compare-upload", files=files)
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
@ -137,303 +135,253 @@ def predict_uploaded_image(uploaded_file):
st.error(f"Erreur API: {response.status_code} - {response.text}") st.error(f"Erreur API: {response.status_code} - {response.text}")
return None return None
except Exception as e: except Exception as e:
st.error(f"Erreur lors de la prédiction: {e}") st.error(f"Erreur lors de la comparaison: {e}")
return None return None
def display_segmentation_plots(fig): def display_comparison_results(result, models_info):
"""Affiche chaque subplot séparément dans des colonnes""" """Affiche les résultats de comparaison entre les deux modèles"""
if fig is None: if not result or not result['success']:
st.warning("Aucune figure de segmentation disponible") st.error("Aucun résultat de comparaison disponible")
return return
axes = fig.get_axes() # Informations des modèles
num_plots = len(axes) st.markdown("### 🏆 Modèles Comparés")
st.markdown("### 🎨 Résultat de la Segmentation") col1, col2 = st.columns(2)
# Créer les colonnes selon le nombre de subplots with col1:
if num_plots <= 4: model1 = result['model1_info']
cols = st.columns(num_plots) st.markdown(f"""
else: <div class="comparison-card">
cols = st.columns(4) # Maximum 4 colonnes <h4>🥇 Modèle #1</h4>
<strong>{model1['encoder_name']}</strong><br>
📐 Taille: {model1['input_size']}<br>
🆔 Run: {model1['run_id'][:8]}...
</div>
""", unsafe_allow_html=True)
for i, ax in enumerate(axes): with col2:
col_idx = i % len(cols) model2 = result['model2_info']
st.markdown(f"""
<div class="comparison-card">
<h4>🥈 Modèle #2</h4>
<strong>{model2['encoder_name']}</strong><br>
📐 Taille: {model2['input_size']}<br>
🆔 Run: {model2['run_id'][:8]}...
</div>
""", unsafe_allow_html=True)
with cols[col_idx]: # Comparaison des temps d'inférence
# Récupérer le titre existant st.markdown("### ⏱️ Comparaison des Performances")
title = ax.get_title()
# st.markdown(f"**{title}:**")
# Créer une figure individuelle avec ce subplot speed_comp = result['speed_comparison']
individual_fig, individual_ax = plt.subplots(figsize=(6, 6)) model1_time = result['model1_inference_time'] * 1000
model2_time = result['model2_inference_time'] * 1000
# Copier toutes les images de l'axe original col1, col2, col3 = st.columns(3)
for image in ax.get_images():
individual_ax.imshow(
image.get_array(),
cmap=image.get_cmap() if image.get_cmap() else None,
alpha=image.get_alpha() if image.get_alpha() else None,
vmin=image.get_clim()[0] if image.get_clim() else None,
vmax=image.get_clim()[1] if image.get_clim() else None
)
# Copier les propriétés de l'axe with col1:
individual_ax.set_title(title) winner_class = "speed-winner" if speed_comp['faster_model'] == model1['encoder_name'] else "speed-slower"
individual_ax.axis('off') st.markdown(f"""
<div class="comparison-card {winner_class}">
<h5>🥇 {model1['encoder_name']}</h5>
<strong>{model1_time:.1f} ms</strong>
</div>
""", unsafe_allow_html=True)
# Ajuster la mise en page with col2:
individual_fig.tight_layout() st.markdown(f"""
<div class="comparison-card">
<h5>⚡ Comparaison</h5>
<strong>{speed_comp['faster_model']}</strong><br>
est {speed_comp['speedup_ratio']}x plus rapide<br>
<small>Différence: {speed_comp['time_difference_ms']:.1f} ms</small>
</div>
""", unsafe_allow_html=True)
# Afficher dans Streamlit with col3:
st.pyplot(individual_fig) winner_class = "speed-winner" if speed_comp['faster_model'] == model2['encoder_name'] else "speed-slower"
st.markdown(f"""
<div class="comparison-card {winner_class}">
<h5>🥈 {model2['encoder_name']}</h5>
<strong>{model2_time:.1f} ms</strong>
</div>
""", unsafe_allow_html=True)
# Libérer la mémoire # Affichage des segmentations
plt.close(individual_fig) if result.get('figure_data'):
st.markdown("### 🎨 Résultats de Segmentation")
try:
fig_bytes = base64.b64decode(result['figure_data'])
fig = pickle.loads(fig_bytes)
st.pyplot(fig)
plt.close(fig)
except Exception as e:
st.error(f"Erreur lors de l'affichage: {e}")
# Fermer la figure originale pour libérer la mémoire # Comparaison des statistiques
plt.close(fig) st.markdown("### 📊 Comparaison des Statistiques")
col1, col2 = st.columns(2)
def display_prediction_stats_with_chart(stats, model_info): with col1:
"""Affiche les statistiques avec un graphique en barres""" st.markdown(f"**📈 {model1['encoder_name']} - Répartition des classes:**")
st.markdown("### 📊 Statistiques de Segmentation") if result.get('model1_stats'):
display_model_stats_chart(result['model1_stats'], models_info, f"Modèle 1 - {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']}")
# 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)
def display_model_stats_chart(stats, models_info, title):
"""Affiche les statistiques d'un modèle sous forme de graphique"""
if not stats: if not stats:
st.warning("Aucune statistique disponible") st.warning("Aucune statistique disponible")
return return
# Ratio 1:2 pour donner plus de place au graphique # Préparer les données pour le graphique
col1, col2 = st.columns([1, 2]) class_names = list(stats.keys())
percentages = [data['percentage'] for data in stats.values()]
# Colonne 1: Cartes de statistiques compactes avec carrés colorés # Récupérer les couleurs des classes
with col1: colors = []
st.markdown("**Détails par classe:**") 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']
# Récupérer les couleurs des classes pour les carrés for class_name in class_names:
class_colors_dict = {} try:
if model_info and 'class_colors' in model_info and 'class_names' in model_info: idx = model_class_names.index(class_name)
model_class_names = model_info['class_names'] color = model_class_colors[idx]
model_class_colors = model_info['class_colors'] colors.append([c/255.0 for c in color])
except (ValueError, IndexError):
for class_name in stats.keys(): colors.append([0.5, 0.5, 0.5])
try:
idx = model_class_names.index(class_name)
color = model_class_colors[idx]
# Convertir en hex pour CSS
color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
class_colors_dict[class_name] = color_hex
except (ValueError, IndexError):
# Couleur par défaut si pas trouvée
class_colors_dict[class_name] = "#808080"
for class_name, data in stats.items():
# Récupérer la couleur pour cette classe
color_hex = class_colors_dict.get(class_name, "#808080")
# ✨ NOUVEAU: Carré coloré + nom de classe
st.markdown(f"""
<div style="
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 0.5rem 0.8rem;
border-radius: 6px;
border-left: 3px solid #667eea;
margin: 0.3rem 0;
color: #2c3e50;
font-weight: 500;
font-size: 0.85rem;
line-height: 1.3;
">
<div style="
font-weight: 600;
margin-bottom: 0.2rem;
display: flex;
align-items: center;
gap: 0.5rem;
">
<div style="
width: 12px;
height: 12px;
background-color: {color_hex};
border-radius: 2px;
border: 1px solid rgba(0,0,0,0.2);
flex-shrink: 0;
"></div>
<span>{class_name}</span>
</div>
<div style="font-size: 0.8rem; color: #555;">
🔢 {data['pixels']:,} pixels | 📊 {data['percentage']}%
</div>
</div>
""", unsafe_allow_html=True)
# Colonne 2: Graphique en barres (reste identique)
with col2:
st.markdown("**Répartition visuelle:**")
# Préparer les données pour le graphique
class_names = list(stats.keys())
percentages = [data['percentage'] for data in stats.values()]
# Récupérer les couleurs des classes depuis model_info
colors = []
if model_info and 'class_colors' in model_info and 'class_names' in model_info:
model_class_names = model_info['class_names']
model_class_colors = model_info['class_colors']
for class_name in class_names:
# Trouver l'index de cette classe dans le modèle
try:
idx = model_class_names.index(class_name)
color = model_class_colors[idx]
# Convertir RGB en format matplotlib
colors.append([c/255.0 for c in color])
except (ValueError, IndexError):
# Couleur par défaut si pas trouvée
colors.append([0.5, 0.5, 0.5])
else:
# Couleurs par défaut si pas d'info du modèle
colors = plt.cm.Set3(np.linspace(0, 1, len(class_names)))
# Créer un graphique plus grand et mieux adapté
fig, ax = plt.subplots(figsize=(10, 6)) # Plus large
bars = ax.bar(class_names, percentages, color=colors, alpha=0.8, edgecolor='white', linewidth=1)
# Personnalisation du graphique améliorée
ax.set_xlabel('Classes de Segmentation', fontsize=11, fontweight='bold')
ax.set_ylabel('Pourcentage (%)', fontsize=11, fontweight='bold')
ax.set_title('Répartition des Classes Détectées', fontsize=13, fontweight='bold', pad=20)
# Meilleure gestion des labels selon le nombre de classes
if len(class_names) <= 4:
plt.xticks(rotation=0, ha='center', fontsize=10)
elif len(class_names) <= 6:
plt.xticks(rotation=30, ha='right', fontsize=9)
else:
plt.xticks(rotation=45, ha='right', fontsize=8)
plt.yticks(fontsize=10)
# Valeurs sur les barres avec meilleur positionnement
for bar, percentage in zip(bars, percentages):
height = bar.get_height()
# Adapter la position du texte selon la hauteur de la barre
if height < max(percentages) * 0.1:
# Si la barre est très petite, mettre le texte au-dessus
va = 'bottom'
y_pos = height + max(percentages) * 0.02
else:
# Sinon, centrer dans la barre
va = 'center'
y_pos = height / 2
ax.text(bar.get_x() + bar.get_width()/2., y_pos,
f'{percentage:.1f}%',
ha='center', va=va, fontsize=9, fontweight='bold',
color='white' if va == 'center' else 'black',
bbox=dict(boxstyle='round,pad=0.2', facecolor='black', alpha=0.7) if va == 'center' else None)
# Améliorer la mise en page et le style
plt.tight_layout()
# Grille plus subtile
ax.grid(axis='y', alpha=0.2, linestyle='--')
ax.set_axisbelow(True)
# Limites et style des axes
ax.set_ylim(0, max(percentages) * 1.15) # Plus d'espace au-dessus
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color('#666')
ax.spines['bottom'].set_color('#666')
# Couleur de fond
fig.patch.set_facecolor('white')
ax.set_facecolor('#fafafa')
# Afficher dans Streamlit
st.pyplot(fig)
plt.close(fig)
def display_class_legend(model_info):
"""Affiche la légende des classes de segmentation"""
if model_info and 'class_colors' in model_info:
st.markdown("**Légende des classes:**")
class_names = model_info.get('class_names', [])
class_colors = model_info.get('class_colors', [])
# Créer une légende colorée
legend_cols = st.columns(len(class_names))
for i, (name, color) in enumerate(zip(class_names, class_colors)):
with legend_cols[i]:
color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
st.markdown(f"""
<div style="
background-color: {color_hex};
padding: 10px;
border-radius: 5px;
text-align: center;
color: white;
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0,0,0,0.7);
margin: 2px;
">
{name}
</div>
""", unsafe_allow_html=True)
# # Sidebar avec informations du modèle
# with st.sidebar:
# st.markdown("## 🔧 Informations du Modèle")
# model_info = get_model_info()
# if model_info:
# st.success("✅ Modèle chargé")
# st.write(f"**Nom:** {model_info.get('model_name', 'N/A')}")
# st.write(f"**Encoder:** {model_info.get('encoder', 'N/A')}")
# st.write(f"**Classes:** {model_info.get('num_classes', 'N/A')}")
# st.write(f"**Taille d'entrée:** {model_info.get('input_size', 'N/A')}")
# st.write(f"**Run ID:** {model_info.get('run_id', 'N/A')[:8]}...")
# else:
# st.error("❌ Modèle non disponible")
# st.markdown("---")
# st.markdown("## 🎯 Classes Segmentées")
# if model_info and 'class_names' in model_info:
# for i, class_name in enumerate(model_info['class_names']):
# st.write(f"{i}. {class_name}")
# Sidebar avec informations du modèle
with st.sidebar:
st.markdown("## 🔧 Informations du Modèle")
model_info = get_model_info()
if model_info:
st.success("✅ Modèle chargé")
st.write(f"**Nom:** {model_info.get('model_name', 'N/A')}")
st.write(f"**Encoder:** {model_info.get('encoder', 'N/A')}")
st.write(f"**Classes:** {model_info.get('num_classes', 'N/A')}")
st.write(f"**Taille d'entrée:** {model_info.get('input_size', 'N/A')}")
st.write(f"**Run ID:** {model_info.get('run_id', 'N/A')[:8]}...")
else: else:
st.error("❌ Modèle non disponible") colors = plt.cm.Set3(np.linspace(0, 1, len(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)
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:
plt.xticks(rotation=0, ha='center', fontsize=9)
elif len(class_names) <= 6:
plt.xticks(rotation=30, ha='right', fontsize=8)
else:
plt.xticks(rotation=45, ha='right', fontsize=8)
plt.yticks(fontsize=9)
# Valeurs sur les barres
for bar, percentage in zip(bars, percentages):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height + max(percentages) * 0.01,
f'{percentage:.1f}%',
ha='center', va='bottom', fontsize=8, fontweight='bold')
plt.tight_layout()
ax.grid(axis='y', alpha=0.2, linestyle='--')
ax.set_axisbelow(True)
# Style des axes
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# Couleur de fond
fig.patch.set_facecolor('white')
ax.set_facecolor('#fafafa')
st.pyplot(fig)
plt.close(fig)
def display_class_comparison(stats1, stats2, model1, model2):
"""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())
comparison_data = []
for class_name in sorted(all_classes):
pct1 = stats1.get(class_name, {}).get('percentage', 0)
pct2 = stats2.get(class_name, {}).get('percentage', 0)
diff = pct1 - pct2
comparison_data.append({
'class': class_name,
'model1_pct': pct1,
'model2_pct': pct2,
'difference': diff
})
# Afficher sous forme de tableau interactif
col1, col2 = st.columns([2, 1])
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}
""")
with col2:
st.markdown("**🎯 Légende:**")
st.markdown("🟢 Différence < 1%")
st.markdown("🟡 Différence 1-5%")
st.markdown("🔴 Différence > 5%")
# Sidebar avec informations des modèles
with st.sidebar:
st.markdown("## 🔧 Informations des Modèles")
models_info = get_models_info()
if models_info:
st.success("✅ Modèles chargés")
# Modèle 1
best_model = models_info.get('best_model', {})
st.markdown("**🥇 Meilleur Modèle:**")
st.write(f"**Encoder:** {best_model.get('encoder_name', 'N/A')}")
st.write(f"**Run ID:** {best_model.get('run_id', 'N/A')[:8]}...")
# Modèle 2
second_model = models_info.get('second_best_model', {})
st.markdown("**🥈 Deuxième Modèle:**")
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')}")
else:
st.error("❌ Modèles non disponibles")
st.markdown("---") st.markdown("---")
st.markdown("## 🎯 Classes Segmentées") st.markdown("## 🎯 Classes Segmentées")
# ✨ NOUVEAU: Affichage avec carrés colorés # Affichage avec carrés colorés
if model_info and 'class_names' in model_info and 'class_colors' in model_info: if models_info and 'class_names' in models_info and 'class_colors' in models_info:
class_names = model_info['class_names'] class_names = models_info['class_names']
class_colors = model_info['class_colors'] class_colors = models_info['class_colors']
for i, (class_name, color) in enumerate(zip(class_names, class_colors)): for i, (class_name, color) in enumerate(zip(class_names, class_colors)):
# Convertir RGB en hex
color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}" color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
# Affichage avec carré coloré
st.markdown(f""" st.markdown(f"""
<div style=" <div style="
display: flex; display: flex;
@ -453,56 +401,26 @@ with st.sidebar:
<span style="font-weight: 500;">{i}. {class_name}</span> <span style="font-weight: 500;">{i}. {class_name}</span>
</div> </div>
""", unsafe_allow_html=True) """, unsafe_allow_html=True)
else:
# Fallback si pas d'info couleur
if model_info and 'class_names' in model_info:
for i, class_name in enumerate(model_info['class_names']):
st.write(f"{i}. {class_name}")
else:
st.write("Informations des classes non disponibles")
# États de session
if "sample_comparison_result" not in st.session_state:
st.session_state.sample_comparison_result = None
if "upload_comparison_result" not in st.session_state:
st.session_state.upload_comparison_result = None
# Initialisation des états - MODIFIÉE pour séparer les résultats par onglet def reset_sample_comparison():
if "sample_prediction_result" not in st.session_state: st.session_state.sample_comparison_result = None
st.session_state.sample_prediction_result = None
if "sample_selected_image" not in st.session_state:
st.session_state.sample_selected_image = None
if "upload_prediction_result" not in st.session_state: def reset_upload_comparison():
st.session_state.upload_prediction_result = None st.session_state.upload_comparison_result = None
if "upload_uploaded_file" not in st.session_state:
st.session_state.upload_uploaded_file = None
# ANCIEN système (gardé pour compatibilité si besoin) # Interface principale
if "prediction_result" not in st.session_state:
st.session_state.prediction_result = None
if "selected_image" not in st.session_state:
st.session_state.selected_image = None
if "uploaded_file" not in st.session_state:
st.session_state.uploaded_file = None
def reset_sample_state():
"""Reset des états de session pour l'onglet Sample"""
st.session_state.sample_prediction_result = None
st.session_state.sample_selected_image = None
def reset_upload_state():
"""Reset des états de session pour l'onglet Upload"""
st.session_state.upload_prediction_result = None
st.session_state.upload_uploaded_file = None
def reset_state():
"""Reset complet (garde l'ancienne fonction pour compatibilité)"""
reset_sample_state()
reset_upload_state()
# Interface principale - MODIFIÉE pour résultats indépendants
tab1, tab2 = st.tabs(["📸 Images d'Exemple", "📤 Upload Personnel"]) tab1, tab2 = st.tabs(["📸 Images d'Exemple", "📤 Upload Personnel"])
# Tab 1: Images d'exemple # Tab 1: Images d'exemple
with tab1: with tab1:
st.markdown("## 📸 Sélectionner une Image d'Exemple") st.markdown("## 📸 Comparaison sur Images d'Exemple")
st.markdown("Ces images proviennent du dataset Cityscapes avec masques de vérité disponibles.") st.markdown("Comparez les performances des deux modèles sur des images Cityscapes.")
# Récupérer les images d'exemple # Récupérer les images d'exemple
sample_data = get_sample_images() sample_data = get_sample_images()
@ -530,85 +448,47 @@ with tab1:
# Prévisualisation de l'image sélectionnée # Prévisualisation de l'image sélectionnée
if selected_filename: if selected_filename:
try: try:
# Récupérer l'image via l'API
image_url = f"{API_BASE_URL}/sample-image/{selected_filename}" image_url = f"{API_BASE_URL}/sample-image/{selected_filename}"
response = requests.get(image_url, timeout=5) response = requests.get(image_url, timeout=5)
if response.status_code == 200: if response.status_code == 200:
# Charger l'image depuis les bytes
sample_image = Image.open(io.BytesIO(response.content)) sample_image = Image.open(io.BytesIO(response.content))
st.image( st.image(
sample_image, sample_image,
caption=f"Aperçu: {selected_display_name}", caption=f"Aperçu: {selected_display_name}",
use_container_width=True use_container_width=True
) )
elif response.status_code == 404: except:
st.warning("📷 Image non trouvée sur le serveur") st.info("📷 Aperçu non disponible - L'image sera chargée lors de la comparaison")
else:
st.info("📷 Prévisualisation temporairement indisponible")
except requests.exceptions.Timeout: # Bouton de comparaison
st.warning("⏱️ Timeout lors du chargement de l'aperçu") if st.button("🔄 Lancer la Comparaison", key="compare_sample", type="primary"):
except Exception as e:
st.info("📷 Aperçu non disponible - L'image sera chargée lors de la prédiction")
# Bouton de prédiction
if st.button("🔮 Lancer la Segmentation", key="predict_sample", type="primary"):
if selected_filename: if selected_filename:
with st.spinner("Segmentation en cours..."): with st.spinner("Comparaison en cours..."):
result = predict_sample_image(selected_filename) result = compare_sample_image(selected_filename)
if result and result['success']: if result and result['success']:
# ✨ NOUVEAU: Stocker dans les variables Sample st.session_state.sample_comparison_result = result
st.session_state.sample_prediction_result = result st.success("✅ Comparaison terminée!")
st.session_state.sample_selected_image = selected_filename
st.success("✅ Segmentation terminée!")
st.rerun() st.rerun()
else: else:
st.error("❌ Erreur lors de la segmentation") st.error("❌ Erreur lors de la comparaison")
else: else:
st.warning("Aucune image d'exemple disponible") st.warning("Aucune image d'exemple disponible")
# ✨ NOUVEAU: Affichage des résultats Sample (indépendants) # Affichage des résultats de comparaison
if st.session_state.sample_prediction_result and st.session_state.sample_selected_image: if st.session_state.sample_comparison_result:
st.markdown("---") st.markdown("---")
result = st.session_state.sample_prediction_result display_comparison_results(st.session_state.sample_comparison_result, models_info)
st.markdown(f"## 🎯 Résultats pour: {st.session_state.sample_selected_image}")
# Afficher la segmentation
if result.get('figure_data'):
try:
# Désérialiser la figure
fig_bytes = base64.b64decode(result['figure_data'])
fig = pickle.loads(fig_bytes)
display_segmentation_plots(fig)
except Exception as e:
st.error(f"Erreur lors de l'affichage de la segmentation: {e}")
# Fallback sur la légende des classes
display_class_legend(model_info)
else:
# Fallback si pas de figure_data
display_class_legend(model_info)
# Afficher les statistiques
if result.get('prediction_stats'):
display_prediction_stats_with_chart(result['prediction_stats'], model_info)
# Informations additionnelles
if result.get('ground_truth_available'):
st.success("🎯 Masque de vérité terrain disponible pour comparaison")
else:
st.info("ℹ️ Pas de vérité terrain disponible")
# Bouton de reset # Bouton de reset
if st.button("🔄 Nouvelle Analyse", key="reset_sample", type="secondary"): if st.button("🔄 Nouvelle Comparaison", key="reset_sample", type="secondary"):
reset_sample_state() reset_sample_comparison()
st.rerun() st.rerun()
# Tab 2: Upload personnel # Tab 2: Upload personnel
with tab2: with tab2:
st.markdown("## 📤 Uploader Votre Image") st.markdown("## 📤 Comparaison sur Image Personnelle")
st.markdown("Uploadez une image de rue pour analyse. Formats acceptés: JPG, PNG") st.markdown("Uploadez une image pour comparer les performances des deux modèles.")
uploaded_file = st.file_uploader( uploaded_file = st.file_uploader(
"Sélectionnez une image:", "Sélectionnez une image:",
@ -625,62 +505,33 @@ with tab2:
file_size_mb = len(uploaded_file.getvalue()) / 1024 / 1024 file_size_mb = len(uploaded_file.getvalue()) / 1024 / 1024
st.info(f"📁 **Fichier:** {uploaded_file.name} | 📊 **Taille:** {file_size_mb:.2f} MB") st.info(f"📁 **Fichier:** {uploaded_file.name} | 📊 **Taille:** {file_size_mb:.2f} MB")
# Bouton de prédiction # Bouton de comparaison
if st.button("🔮 Lancer la Segmentation", key="predict_upload", type="primary"): if st.button("🔄 Lancer la Comparaison", key="compare_upload", type="primary"):
with st.spinner("Segmentation en cours..."): with st.spinner("Comparaison en cours..."):
result = predict_uploaded_image(uploaded_file) result = compare_uploaded_image(uploaded_file)
if result and result['success']: if result and result['success']:
# ✨ NOUVEAU: Stocker dans les variables Upload st.session_state.upload_comparison_result = result
st.session_state.upload_prediction_result = result st.success("✅ Comparaison terminée!")
st.session_state.upload_uploaded_file = uploaded_file.name
st.success("✅ Segmentation terminée!")
st.rerun() st.rerun()
else: else:
st.error("❌ Erreur lors de la segmentation") st.error("❌ Erreur lors de la comparaison")
# ✨ NOUVEAU: Affichage des résultats Upload (indépendants) # Affichage des résultats de comparaison
if st.session_state.upload_prediction_result and st.session_state.upload_uploaded_file: if st.session_state.upload_comparison_result:
st.markdown("---") st.markdown("---")
result = st.session_state.upload_prediction_result display_comparison_results(st.session_state.upload_comparison_result, models_info)
st.markdown(f"## 🎯 Résultats pour: {st.session_state.upload_uploaded_file}")
# Afficher la segmentation
if result.get('figure_data'):
try:
# Désérialiser la figure
fig_bytes = base64.b64decode(result['figure_data'])
fig = pickle.loads(fig_bytes)
display_segmentation_plots(fig)
except Exception as e:
st.error(f"Erreur lors de l'affichage de la segmentation: {e}")
# Fallback sur la légende des classes
display_class_legend(model_info)
else:
# Fallback si pas de figure_data
display_class_legend(model_info)
# Afficher les statistiques
if result.get('prediction_stats'):
display_prediction_stats_with_chart(result['prediction_stats'], model_info)
# Informations additionnelles
if result.get('ground_truth_available'):
st.success("🎯 Masque de vérité terrain disponible pour comparaison")
else:
st.info("ℹ️ Pas de vérité terrain disponible (image personnelle)")
# Bouton de reset # Bouton de reset
if st.button("🔄 Nouvelle Analyse", key="reset_upload", type="secondary"): if st.button("🔄 Nouvelle Comparaison", key="reset_upload", type="secondary"):
reset_upload_state() reset_upload_comparison()
st.rerun() st.rerun()
# Footer # Footer
st.markdown("---") st.markdown("---")
st.markdown(""" st.markdown("""
<div style="text-align: center; color: #666; padding: 1rem;"> <div style="text-align: center; color: #666; padding: 1rem;">
🚗 <strong>Multi-Class Segmentation API</strong> - 🔄 <strong>Multi-Class Segmentation Comparison API</strong> -
Système de vision par ordinateur pour véhicules autonomes<br> Système de comparaison de modèles de vision par ordinateur<br>
<small>Développé avec FastAPI, Streamlit et TensorFlow</small> <small>Développé avec FastAPI, Streamlit et PyTorch</small>
</div> </div>
""", unsafe_allow_html=True) """, unsafe_allow_html=True)

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):
@ -141,10 +140,6 @@ 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]
@ -371,7 +366,7 @@ def preprocess_image_and_mask(image_path, mask_path=None, img_size=(512, 512),
# 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 +456,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 +475,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 +734,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)