Docker working. Improvements incoming.
This commit is contained in:
parent
9377a77448
commit
cdd6f9645b
6 changed files with 530 additions and 781 deletions
0
EV3
Normal file
0
EV3
Normal file
417
app/main.py
417
app/main.py
|
|
@ -1,24 +1,27 @@
|
|||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import tempfile
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from pathlib import Path
|
||||
import mlflow
|
||||
from fastapi import FastAPI, HTTPException, File, UploadFile, Form
|
||||
from fastapi import FastAPI, HTTPException, File, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
import numpy as np
|
||||
import pickle
|
||||
import base64
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.image as mpimg
|
||||
|
||||
|
||||
# Configuration MLflow avec variables d'environnement
|
||||
def configure_mlflow():
|
||||
"""Configure MLflow avec les variables d'environnement"""
|
||||
print("=== DEBUG VARIABLES D'ENVIRONNEMENT ===")
|
||||
|
||||
# ✅ NETTOYAGE automatique des variables d'environnement
|
||||
mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI", "").strip()
|
||||
mlflow_s3_endpoint_url = os.getenv("MLFLOW_S3_ENDPOINT_URL", "").strip()
|
||||
aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID", "").strip()
|
||||
|
|
@ -33,11 +36,9 @@ def configure_mlflow():
|
|||
if not mlflow_tracking_uri:
|
||||
raise ValueError("MLFLOW_TRACKING_URI not found in environment variables")
|
||||
|
||||
# Configuration MLflow avec URLs nettoyées
|
||||
mlflow.set_tracking_uri(mlflow_tracking_uri)
|
||||
print(f"MLflow Tracking URI: {mlflow_tracking_uri}")
|
||||
|
||||
# ✅ CORRECTION: Configuration EXPLICITE des identifiants avec nettoyage
|
||||
if not mlflow_s3_endpoint_url or not aws_access_key_id or not aws_secret_access_key:
|
||||
raise ValueError("Variables d'environnement MLflow manquantes (S3_ENDPOINT, ACCESS_KEY, SECRET_KEY)")
|
||||
|
||||
|
|
@ -46,66 +47,68 @@ def configure_mlflow():
|
|||
os.environ["AWS_SECRET_ACCESS_KEY"] = aws_secret_access_key
|
||||
print(f"MLflow S3 Endpoint: {mlflow_s3_endpoint_url}")
|
||||
print("✅ Identifiants AWS configurés")
|
||||
|
||||
# ✅ SKIP set_experiment pour éviter le problème 404
|
||||
# mlflow.set_experiment("OC Projet 8") # Commenté temporairement
|
||||
print("✅ Configuration MLflow terminée (sans set_experiment)")
|
||||
print("✅ Configuration MLflow terminée")
|
||||
|
||||
# Import des fonctions utils
|
||||
sys.path.append('.')
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from utils.utils import (
|
||||
load_cityscapes_config,
|
||||
load_best_model_from_registry,
|
||||
load_model,
|
||||
run_inference_and_visualize,
|
||||
colorize_mask
|
||||
)
|
||||
|
||||
# ✅ AJOUT: Import segmentation_models pour le chargement des modèles
|
||||
try:
|
||||
import segmentation_models as sm
|
||||
print("✅ segmentation_models importé - modèles compatibles")
|
||||
except ImportError as e:
|
||||
print(f"⚠️ segmentation_models non disponible: {e}")
|
||||
print("💡 Installez avec: pip install git+https://github.com/qubvel/segmentation_models.git")
|
||||
|
||||
# Configuration
|
||||
MODEL_NAME = "StreetsSegmentation"
|
||||
SAMPLE_IMAGES_DIR = "./notebooks/content/data/test_images_sample"
|
||||
CITYSCAPES_CONFIG_PATH = "./cityscapes_config.json"
|
||||
TEMP_UPLOAD_DIR = "/tmp/uploads"
|
||||
|
||||
# Modèles de données
|
||||
# Modèles de données mis à jour
|
||||
class ModelInfo(BaseModel):
|
||||
name: str
|
||||
run_id: str
|
||||
encoder_name: str
|
||||
input_size: tuple
|
||||
rank: int
|
||||
|
||||
class ComparisonResult(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
model1_info: ModelInfo
|
||||
model2_info: ModelInfo
|
||||
image_path: Optional[str] = None
|
||||
mask_path: Optional[str] = None
|
||||
model1_stats: Optional[dict] = None
|
||||
model2_stats: Optional[dict] = None
|
||||
model1_inference_time: float
|
||||
model2_inference_time: float
|
||||
speed_comparison: dict
|
||||
ground_truth_available: bool = False
|
||||
figure_data: Optional[str] = None
|
||||
|
||||
class ImageInfo(BaseModel):
|
||||
filename: str
|
||||
display_name: str
|
||||
has_ground_truth: bool
|
||||
|
||||
class PredictionResult(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
image_path: Optional[str] = None
|
||||
mask_path: Optional[str] = None
|
||||
prediction_stats: Optional[dict] = None
|
||||
ground_truth_available: bool = False
|
||||
figure_data: Optional[str] = None
|
||||
|
||||
class AvailableImagesResponse(BaseModel):
|
||||
images: List[ImageInfo]
|
||||
total_count: int
|
||||
|
||||
# Variables globales pour le modèle
|
||||
model = None
|
||||
encoder_name = None
|
||||
script_img_size = None
|
||||
# Variables globales pour les deux modèles
|
||||
best_model = None
|
||||
second_best_model = None
|
||||
best_model_info = {}
|
||||
second_best_model_info = {}
|
||||
mapping_config = None
|
||||
run_id = None
|
||||
|
||||
def initialize_model():
|
||||
"""Initialise le modèle et la configuration au démarrage"""
|
||||
global model, encoder_name, script_img_size, mapping_config, run_id
|
||||
def initialize_models():
|
||||
"""Initialise les deux meilleurs modèles au démarrage"""
|
||||
global best_model, second_best_model, best_model_info, second_best_model_info, mapping_config
|
||||
|
||||
try:
|
||||
# Configuration MLflow
|
||||
print("🔄 Configuration de MLflow...")
|
||||
configure_mlflow()
|
||||
|
||||
|
|
@ -113,76 +116,83 @@ def initialize_model():
|
|||
mapping_config = load_cityscapes_config(CITYSCAPES_CONFIG_PATH, verbose=False)
|
||||
print("✅ Configuration Cityscapes chargée")
|
||||
|
||||
print("🔄 Chargement du modèle depuis MLflow...")
|
||||
print(f"🔍 Recherche du modèle: {MODEL_NAME}")
|
||||
model, run_id, encoder_name, script_img_size = load_best_model_from_registry(MODEL_NAME)
|
||||
print("✅ Modèle téléchargé depuis MLflow")
|
||||
print("🔄 Chargement du meilleur modèle...")
|
||||
best_model, best_run_id, best_encoder_name, best_img_size = load_model(
|
||||
experiment_name="OC Projet 9",
|
||||
metric="test_mean_iou",
|
||||
top_n=1
|
||||
)
|
||||
best_model_info = {
|
||||
"name": "Meilleur modèle",
|
||||
"run_id": best_run_id,
|
||||
"encoder_name": best_encoder_name,
|
||||
"input_size": best_img_size,
|
||||
"rank": 1
|
||||
}
|
||||
|
||||
print(f"✅ Modèle chargé du run {run_id}")
|
||||
print(f"📐 Encoder: {encoder_name}")
|
||||
print(f"📐 Input shape: {script_img_size}")
|
||||
print(f"🎨 Nombre de classes: {mapping_config['num_classes']}")
|
||||
print("🔄 Chargement du deuxième meilleur modèle...")
|
||||
second_best_model, second_run_id, second_encoder_name, second_img_size = load_model(
|
||||
experiment_name="OC Projet 9",
|
||||
metric="test_mean_iou",
|
||||
top_n=2
|
||||
)
|
||||
second_best_model_info = {
|
||||
"name": "Deuxième modèle",
|
||||
"run_id": second_run_id,
|
||||
"encoder_name": second_encoder_name,
|
||||
"input_size": second_img_size,
|
||||
"rank": 2
|
||||
}
|
||||
|
||||
# Créer le dossier temporaire
|
||||
os.makedirs(TEMP_UPLOAD_DIR, exist_ok=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors de l'initialisation: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise e
|
||||
|
||||
# Initialisation de l'API
|
||||
app = FastAPI(
|
||||
title="Multi-Class Segmentation API",
|
||||
description="API de segmentation sémantique pour véhicules autonomes",
|
||||
version="1.0.0"
|
||||
title="Multi-Class Segmentation Comparison API",
|
||||
description="API de comparaison de segmentation sémantique avec deux modèles",
|
||||
version="2.0.0"
|
||||
)
|
||||
|
||||
# ✅ AJOUT: Initialiser le modèle de manière plus robuste
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Initialise le modèle au démarrage de l'API"""
|
||||
try:
|
||||
initialize_model()
|
||||
print("🚀 API démarrée avec succès - Modèle chargé!")
|
||||
initialize_models()
|
||||
print("🚀 API démarrée avec succès - Modèles chargés!")
|
||||
except Exception as e:
|
||||
print(f"⚠️ ATTENTION: Impossible de charger le modèle au démarrage: {e}")
|
||||
print("L'API démarre quand même mais les prédictions ne fonctionneront pas.")
|
||||
print(f"⚠️ ATTENTION: Impossible de charger les modèles au démarrage: {e}")
|
||||
|
||||
@app.get("/")
|
||||
def root():
|
||||
return {
|
||||
"message": "Multi-Class Segmentation API - Online",
|
||||
"model_loaded": model is not None,
|
||||
"model_run_id": run_id,
|
||||
"encoder": encoder_name,
|
||||
"message": "Multi-Class Segmentation Comparison API - Online",
|
||||
"models_loaded": best_model is not None and second_best_model is not None,
|
||||
"best_model": best_model_info if best_model else None,
|
||||
"second_best_model": second_best_model_info if second_best_model else None,
|
||||
"num_classes": mapping_config['num_classes'] if mapping_config else None
|
||||
}
|
||||
|
||||
@app.get("/health")
|
||||
def health_check():
|
||||
"""Health check endpoint pour nginx"""
|
||||
if model is None or mapping_config is None:
|
||||
raise HTTPException(status_code=503, detail="Model not loaded")
|
||||
|
||||
if best_model is None or second_best_model is None or mapping_config is None:
|
||||
raise HTTPException(status_code=503, detail="Models not loaded")
|
||||
return {
|
||||
"status": "healthy",
|
||||
"model_loaded": True,
|
||||
"models_loaded": True,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
@app.get("/models", response_model=dict)
|
||||
def get_model_info():
|
||||
"""Retourne les informations du modèle chargé"""
|
||||
if model is None:
|
||||
raise HTTPException(status_code=503, detail="Model not loaded")
|
||||
|
||||
def get_models_info():
|
||||
if best_model is None or second_best_model is None:
|
||||
raise HTTPException(status_code=503, detail="Models not loaded")
|
||||
return {
|
||||
"model_name": MODEL_NAME,
|
||||
"run_id": run_id,
|
||||
"encoder": encoder_name,
|
||||
"input_size": script_img_size,
|
||||
"best_model": best_model_info,
|
||||
"second_best_model": second_best_model_info,
|
||||
"num_classes": mapping_config['num_classes'],
|
||||
"class_names": mapping_config['group_names'],
|
||||
"class_colors": mapping_config['group_colors'].tolist()
|
||||
|
|
@ -190,238 +200,199 @@ def get_model_info():
|
|||
|
||||
@app.get("/sample-images", response_model=AvailableImagesResponse)
|
||||
def get_sample_images():
|
||||
"""Retourne la liste des images d'exemple disponibles"""
|
||||
try:
|
||||
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
|
||||
mask_dir = Path(SAMPLE_IMAGES_DIR) / "mask"
|
||||
|
||||
if not original_dir.exists():
|
||||
raise HTTPException(status_code=404, detail="Sample images directory not found")
|
||||
|
||||
images = []
|
||||
for img_path in original_dir.glob("*leftImg8bit.png"):
|
||||
# Construire le nom du masque correspondant
|
||||
base_name = img_path.stem.replace("_leftImg8bit", "")
|
||||
mask_name = f"{base_name}_gtFine_labelIds.png"
|
||||
mask_path = mask_dir / mask_name
|
||||
|
||||
# Créer un nom d'affichage plus convivial
|
||||
display_name = base_name.replace("_", " ").title()
|
||||
|
||||
images.append(ImageInfo(
|
||||
filename=img_path.name,
|
||||
display_name=display_name,
|
||||
has_ground_truth=mask_path.exists()
|
||||
))
|
||||
|
||||
# Trier par nom pour un affichage cohérent
|
||||
images.sort(key=lambda x: x.filename)
|
||||
|
||||
return AvailableImagesResponse(
|
||||
images=images,
|
||||
total_count=len(images)
|
||||
)
|
||||
|
||||
return AvailableImagesResponse(images=images, total_count=len(images))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error listing sample images: {str(e)}")
|
||||
|
||||
@app.get("/sample-image/{filename}")
|
||||
def get_sample_image(filename: str):
|
||||
"""Sert une image d'exemple pour prévisualisation"""
|
||||
try:
|
||||
print(f"🔍 DEBUG: Demande d'image: {filename}")
|
||||
|
||||
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
|
||||
image_path = original_dir / filename
|
||||
|
||||
print(f"🔍 DEBUG: Chemin construit: {image_path}")
|
||||
print(f"🔍 DEBUG: Fichier existe: {image_path.exists()}")
|
||||
|
||||
if not image_path.exists():
|
||||
print(f"❌ DEBUG: Image non trouvée: {image_path}")
|
||||
raise HTTPException(status_code=404, detail=f"Sample image {filename} not found")
|
||||
|
||||
# Vérifier que c'est bien un fichier image attendu (sécurité)
|
||||
if not filename.endswith('_leftImg8bit.png'):
|
||||
print(f"❌ DEBUG: Format invalide: {filename}")
|
||||
raise HTTPException(status_code=400, detail="Invalid image filename format")
|
||||
|
||||
print(f"✅ DEBUG: Envoi de l'image: {filename}")
|
||||
return FileResponse(
|
||||
image_path,
|
||||
media_type="image/png",
|
||||
headers={"Content-Disposition": f"inline; filename={filename}"}
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
# Re-raise les HTTPException pour qu'elles soient gérées correctement
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors du service de l'image {filename}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise HTTPException(status_code=500, detail=f"Error serving image: {str(e)}")
|
||||
|
||||
class SampleImageRequest(BaseModel):
|
||||
filename: str
|
||||
|
||||
@app.post("/predict-sample", response_model=PredictionResult)
|
||||
def predict_sample_image(request: SampleImageRequest):
|
||||
"""Lance une prédiction sur une image d'exemple"""
|
||||
if model is None or mapping_config is None:
|
||||
raise HTTPException(status_code=503, detail="Model not loaded")
|
||||
|
||||
@app.post("/compare-sample", response_model=ComparisonResult)
|
||||
def compare_sample_image(request: SampleImageRequest):
|
||||
if best_model is None or second_best_model is None or mapping_config is None:
|
||||
raise HTTPException(status_code=503, detail="Models not loaded")
|
||||
try:
|
||||
# Vérifier que l'image existe
|
||||
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
|
||||
image_path = original_dir / request.filename
|
||||
|
||||
if not image_path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Sample image {request.filename} not found")
|
||||
|
||||
# Construire le chemin du masque de vérité
|
||||
base_name = image_path.stem.replace("_leftImg8bit", "")
|
||||
mask_name = f"{base_name}_gtFine_labelIds.png"
|
||||
mask_path = Path(SAMPLE_IMAGES_DIR) / "mask" / mask_name
|
||||
|
||||
mask_path_str = str(mask_path) if mask_path.exists() else None
|
||||
|
||||
print(f"🔮 Lancement de l'inférence sur {request.filename}")
|
||||
|
||||
# Lancer l'inférence (sans affichage pour l'API)
|
||||
predicted_mask, fig = run_inference_and_visualize(
|
||||
# Inférence modèle 1
|
||||
pred1, t1, _ = run_inference_and_visualize(
|
||||
image_path=str(image_path),
|
||||
mask_path=mask_path_str,
|
||||
model=model,
|
||||
encoder_name=encoder_name,
|
||||
img_size=script_img_size,
|
||||
model=best_model,
|
||||
encoder_name=None,
|
||||
img_size=best_model_info["input_size"],
|
||||
mapping_config=mapping_config,
|
||||
save_dir=None,
|
||||
show_stats=False,
|
||||
show_plot=False # ← AJOUTÉ pour récupérer la figure
|
||||
save_dir=None, show_stats=False, show_plot=False
|
||||
)
|
||||
|
||||
# Sérialiser la figure
|
||||
fig_bytes = pickle.dumps(fig)
|
||||
fig_b64 = base64.b64encode(fig_bytes).decode()
|
||||
|
||||
# Calculer les statistiques
|
||||
unique_classes, counts = np.unique(predicted_mask, return_counts=True)
|
||||
total_pixels = predicted_mask.size
|
||||
|
||||
stats = {}
|
||||
for class_id, count in zip(unique_classes, counts):
|
||||
if class_id < len(mapping_config['group_names']):
|
||||
percentage = (count / total_pixels) * 100
|
||||
stats[mapping_config['group_names'][class_id]] = {
|
||||
"pixels": int(count),
|
||||
"percentage": round(percentage, 1)
|
||||
}
|
||||
|
||||
return PredictionResult(
|
||||
success=True,
|
||||
message="Prediction completed successfully",
|
||||
# Inférence modèle 2
|
||||
pred2, t2, _ = run_inference_and_visualize(
|
||||
image_path=str(image_path),
|
||||
mask_path=mask_path_str,
|
||||
prediction_stats=stats,
|
||||
ground_truth_available=mask_path.exists(),
|
||||
model=second_best_model,
|
||||
encoder_name=None,
|
||||
img_size=second_best_model_info["input_size"],
|
||||
mapping_config=mapping_config,
|
||||
save_dir=None, show_stats=False, show_plot=False
|
||||
)
|
||||
# Figure combinée
|
||||
fig, axes = plt.subplots(2, 3, figsize=(15, 8))
|
||||
img_arr = mpimg.imread(str(image_path))
|
||||
axes[0, 0].imshow(img_arr); axes[0, 0].set_title("Image originale"); axes[0, 0].axis('off')
|
||||
axes[1, 0].imshow(img_arr); axes[1, 0].set_title("Image originale"); axes[1, 0].axis('off')
|
||||
axes[0, 1].imshow(colorize_mask(pred1, mapping_config['group_colors']))
|
||||
axes[0, 1].set_title(f"Prédiction {best_model_info['encoder_name']}"); axes[0, 1].axis('off')
|
||||
axes[1, 1].imshow(colorize_mask(pred2, mapping_config['group_colors']))
|
||||
axes[1, 1].set_title(f"Prédiction {second_best_model_info['encoder_name']}"); axes[1, 1].axis('off')
|
||||
if mask_path_str and os.path.exists(mask_path_str):
|
||||
gt = mpimg.imread(mask_path_str)
|
||||
axes[0, 2].imshow(gt); axes[0, 2].set_title("Ground Truth"); axes[0, 2].axis('off')
|
||||
axes[1, 2].imshow(gt); axes[1, 2].set_title("Ground Truth"); axes[1, 2].axis('off')
|
||||
plt.tight_layout()
|
||||
fig_b64 = base64.b64encode(pickle.dumps(fig)).decode()
|
||||
# Stats
|
||||
def calc(mask):
|
||||
uc, cnts = np.unique(mask, return_counts=True)
|
||||
stats = {}
|
||||
for cid, c in zip(uc, cnts):
|
||||
if cid < len(mapping_config['group_names']):
|
||||
stats[mapping_config['group_names'][cid]] = {
|
||||
"pixels": int(c),
|
||||
"percentage": round((c/mask.size)*100, 1)
|
||||
}
|
||||
return stats
|
||||
stats1, stats2 = calc(pred1), calc(pred2)
|
||||
faster = best_model_info['encoder_name'] if t1 < t2 else second_best_model_info['encoder_name']
|
||||
speedup = round(max(t1, t2) / min(t1, t2), 2)
|
||||
speed_comp = {"faster_model": faster, "speedup_ratio": speedup, "time_difference_ms": abs(t1-t2)*1000}
|
||||
return ComparisonResult(
|
||||
success=True, message="Comparison completed successfully",
|
||||
model1_info=ModelInfo(**best_model_info), model2_info=ModelInfo(**second_best_model_info),
|
||||
image_path=str(image_path), mask_path=mask_path_str,
|
||||
model1_stats=stats1, model2_stats=stats2,
|
||||
model1_inference_time=t1, model2_inference_time=t2,
|
||||
speed_comparison=speed_comp, ground_truth_available=mask_path.exists(),
|
||||
figure_data=fig_b64
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors de la prédiction: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise HTTPException(status_code=500, detail=f"Prediction error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
|
||||
|
||||
@app.post("/predict-upload", response_model=PredictionResult)
|
||||
def predict_uploaded_image(file: UploadFile = File(...)):
|
||||
"""Lance une prédiction sur une image uploadée"""
|
||||
if model is None or mapping_config is None:
|
||||
raise HTTPException(status_code=503, detail="Model not loaded")
|
||||
|
||||
# Vérifier le type de fichier
|
||||
@app.post("/compare-upload", response_model=ComparisonResult)
|
||||
def compare_uploaded_image(file: UploadFile = File(...)):
|
||||
if best_model is None or second_best_model is None or mapping_config is None:
|
||||
raise HTTPException(status_code=503, detail="Models not loaded")
|
||||
allowed_types = ["image/jpeg", "image/jpg", "image/png"]
|
||||
if file.content_type not in allowed_types:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid file type. Allowed: {', '.join(allowed_types)}"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=400, detail=f"Invalid file type. Allowed: {', '.join(allowed_types)}")
|
||||
temp_file_path = None
|
||||
try:
|
||||
# Créer un fichier temporaire
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
temp_filename = f"upload_{timestamp}_{file.filename}"
|
||||
temp_file_path = os.path.join(TEMP_UPLOAD_DIR, temp_filename)
|
||||
|
||||
# Sauvegarder le fichier uploadé
|
||||
with open(temp_file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
print(f"🔮 Lancement de l'inférence sur {file.filename}")
|
||||
|
||||
# Lancer l'inférence (sans masque de vérité)
|
||||
predicted_mask, fig = run_inference_and_visualize(
|
||||
image_path=temp_file_path,
|
||||
mask_path=None, # Pas de masque de vérité pour les uploads
|
||||
model=model,
|
||||
encoder_name=encoder_name,
|
||||
img_size=script_img_size,
|
||||
mapping_config=mapping_config,
|
||||
save_dir=None,
|
||||
show_stats=False,
|
||||
show_plot=False # ← AJOUTÉ pour récupérer la figure
|
||||
# Inférences
|
||||
pred1, t1, _ = run_inference_and_visualize(
|
||||
image_path=temp_file_path, mask_path=None,
|
||||
model=best_model, encoder_name=None,
|
||||
img_size=best_model_info["input_size"], mapping_config=mapping_config,
|
||||
save_dir=None, show_stats=False, show_plot=False,
|
||||
)
|
||||
|
||||
# Sérialiser la figure
|
||||
fig_bytes = pickle.dumps(fig)
|
||||
fig_b64 = base64.b64encode(fig_bytes).decode()
|
||||
|
||||
# Calculer les statistiques
|
||||
unique_classes, counts = np.unique(predicted_mask, return_counts=True)
|
||||
total_pixels = predicted_mask.size
|
||||
|
||||
stats = {}
|
||||
for class_id, count in zip(unique_classes, counts):
|
||||
if class_id < len(mapping_config['group_names']):
|
||||
percentage = (count / total_pixels) * 100
|
||||
stats[mapping_config['group_names'][class_id]] = {
|
||||
"pixels": int(count),
|
||||
"percentage": round(percentage, 1)
|
||||
}
|
||||
|
||||
return PredictionResult(
|
||||
success=True,
|
||||
message="Prediction completed successfully",
|
||||
pred2, t2, _ = run_inference_and_visualize(
|
||||
image_path=temp_file_path, mask_path=None,
|
||||
model=second_best_model, encoder_name=None,
|
||||
img_size=second_best_model_info["input_size"], mapping_config=mapping_config,
|
||||
save_dir=None, show_stats=False, show_plot=False,
|
||||
)
|
||||
# Figure
|
||||
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||||
img_arr = mpimg.imread(temp_file_path)
|
||||
axes[0].imshow(img_arr); axes[0].set_title("Image originale"); axes[0].axis('off')
|
||||
axes[1].imshow(colorize_mask(pred1, mapping_config['group_colors']))
|
||||
axes[1].set_title(f"Prédiction {best_model_info['encoder_name']}"); axes[1].axis('off')
|
||||
axes[2].imshow(colorize_mask(pred2, mapping_config['group_colors']))
|
||||
axes[2].set_title(f"Prédiction {second_best_model_info['encoder_name']}"); axes[2].axis('off')
|
||||
plt.tight_layout()
|
||||
fig_b64 = base64.b64encode(pickle.dumps(fig)).decode()
|
||||
# Stats
|
||||
def calc(mask):
|
||||
uc, cnts = np.unique(mask, return_counts=True)
|
||||
stats = {}
|
||||
for cid, c in zip(uc, cnts):
|
||||
if cid < len(mapping_config['group_names']):
|
||||
stats[mapping_config['group_names'][cid]] = {
|
||||
"pixels": int(c),
|
||||
"percentage": round((c/mask.size)*100, 1)
|
||||
}
|
||||
return stats
|
||||
stats1, stats2 = calc(pred1), calc(pred2)
|
||||
faster = best_model_info['encoder_name'] if t1 < t2 else second_best_model_info['encoder_name']
|
||||
speedup = round(max(t1, t2) / min(t1, t2), 2)
|
||||
speed_comp = {"faster_model": faster, "speedup_ratio": speedup, "time_difference_ms": abs(t1-t2)*1000}
|
||||
return ComparisonResult(
|
||||
success=True, message="Comparison completed successfully",
|
||||
model1_info=ModelInfo(**best_model_info), model2_info=ModelInfo(**second_best_model_info),
|
||||
image_path=temp_file_path,
|
||||
prediction_stats=stats,
|
||||
ground_truth_available=False,
|
||||
model1_stats=stats1, model2_stats=stats2,
|
||||
model1_inference_time=t1, model2_inference_time=t2,
|
||||
speed_comparison=speed_comp, ground_truth_available=False,
|
||||
figure_data=fig_b64
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors de la prédiction: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise HTTPException(status_code=500, detail=f"Prediction error: {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
|
||||
finally:
|
||||
# Nettoyer le fichier temporaire
|
||||
if temp_file_path and os.path.exists(temp_file_path):
|
||||
try:
|
||||
os.remove(temp_file_path)
|
||||
print(f"🗑️ Fichier temporaire supprimé: {temp_filename}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Impossible de supprimer le fichier temporaire: {e}")
|
||||
try: os.remove(temp_file_path)
|
||||
except Exception: pass
|
||||
|
||||
@app.get("/metrics")
|
||||
def get_metrics():
|
||||
"""Endpoint de métriques pour monitoring"""
|
||||
return {
|
||||
"model_loaded": model is not None,
|
||||
"model_name": MODEL_NAME,
|
||||
"run_id": run_id,
|
||||
"models_loaded": best_model is not None and second_best_model is not None,
|
||||
"best_model_info": best_model_info,
|
||||
"second_best_model_info": second_best_model_info,
|
||||
"num_classes": mapping_config['num_classes'] if mapping_config else None,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
|
@ -8,37 +8,44 @@ uvicorn==0.35.0
|
|||
starlette==0.46.2
|
||||
pydantic==2.11.7
|
||||
pydantic_core==2.33.2
|
||||
python-multipart==0.0.20 # uploads FastAPI
|
||||
|
||||
# ===== STREAMLIT =====
|
||||
streamlit==1.46.1
|
||||
altair==5.5.0 # Utilisé par Streamlit pour les graphiques
|
||||
blinker==1.9.0 # Streamlit dependency
|
||||
altair==5.5.0 # utilisé par Streamlit
|
||||
blinker==1.9.0 # dépendance Streamlit
|
||||
|
||||
# ===== HTTP REQUESTS =====
|
||||
# ===== HTTP CLIENTS =====
|
||||
requests==2.32.4
|
||||
httpx==0.28.1 # Modern HTTP client (utilisé par FastAPI)
|
||||
httpx==0.28.1 # utilisé en interne par FastAPI
|
||||
httpcore==1.0.9
|
||||
h11==0.16.0
|
||||
|
||||
# ===== MACHINE LEARNING CORE =====
|
||||
tensorflow==2.19.0
|
||||
keras==3.10.0
|
||||
numpy>=1.24.0,<2.0.0
|
||||
# ===== MACHINE LEARNING CORE (PyTorch) =====
|
||||
torch==2.8.0 # version stable PyTorch
|
||||
torchvision==0.23.0 # utile pour les modèles pré-entraînés
|
||||
torchaudio==2.8.0 # optionnel (mais recommandé pour compatibilité PyTorch)
|
||||
segmentation-models-pytorch==0.5.0
|
||||
albumentations==2.0.8
|
||||
# version PyTorch de ToTensor
|
||||
albumentations[imgaug,opencv]
|
||||
|
||||
# Note: tensorflow-addons n'est plus nécessaire avec TensorFlow 2.19+
|
||||
# Les fonctionnalités sont maintenant intégrées dans TensorFlow core
|
||||
numpy>=1.24.0,<2.0.0
|
||||
scipy==1.16.0
|
||||
scikit-learn==1.7.0
|
||||
|
||||
# ===== COMPUTER VISION =====
|
||||
pillow==11.3.0
|
||||
opencv-python-headless==4.9.0.80 # Pas dans votre freeze, mais nécessaire pour Docker
|
||||
opencv-python-headless==4.9.0.80
|
||||
scikit-image==0.25.2
|
||||
imageio==2.37.0
|
||||
|
||||
# ===== DATA SCIENCE =====
|
||||
pandas==2.3.0
|
||||
matplotlib==3.10.3
|
||||
scipy==1.16.0
|
||||
scikit-learn==1.7.0
|
||||
networkx==3.5
|
||||
tifffile==2025.6.11
|
||||
lazy_loader==0.4
|
||||
|
||||
# ===== MLFLOW & MODEL REGISTRY =====
|
||||
mlflow==3.2.0
|
||||
|
|
@ -50,34 +57,7 @@ boto3==1.39.2
|
|||
botocore==1.39.2
|
||||
s3transfer==0.13.0
|
||||
|
||||
# ===== SEGMENTATION MODELS =====
|
||||
# Option 1: Version figée par commit (RECOMMANDÉE pour production)
|
||||
segmentation_models @ git+https://github.com/qubvel/segmentation_models.git@5d24bbfb28af6134e25e2c0b79e7727f6c0491d0
|
||||
|
||||
# Option 2: Si vous préférez une version PyPI (décommentez et commentez la ligne du dessus)
|
||||
# segmentation-models==1.0.1
|
||||
|
||||
# ===== SUPPORTING LIBRARIES =====
|
||||
# TensorFlow ecosystem
|
||||
tensorboard==2.19.0
|
||||
tensorboard-data-server==0.7.2
|
||||
protobuf==3.20.3
|
||||
flatbuffers==25.2.10
|
||||
h5py==3.14.0
|
||||
astunparse==1.6.3
|
||||
gast==0.6.0
|
||||
google-pasta==0.2.0
|
||||
keras-applications==1.0.8
|
||||
opt_einsum==3.4.0
|
||||
termcolor==2.5.0
|
||||
wrapt==1.17.2
|
||||
|
||||
# Data processing
|
||||
networkx==3.5
|
||||
tifffile==2025.6.11
|
||||
lazy_loader==0.4
|
||||
|
||||
# Utilities
|
||||
# ===== UTILITIES =====
|
||||
click==8.2.1
|
||||
tqdm==4.67.1
|
||||
joblib==1.5.1
|
||||
|
|
@ -85,57 +65,6 @@ python-dateutil
|
|||
packaging
|
||||
typing_extensions==4.14.0
|
||||
|
||||
# File handling
|
||||
python-multipart==0.0.20 # Pour FastAPI file uploads
|
||||
|
||||
# ===== AJOUTS SPÉCIFIQUES POUR UTILS/UTILS.PY =====
|
||||
# Pour la gestion des couleurs et légendes matplotlib
|
||||
cycler==0.12.1 # Pour matplotlib customization
|
||||
|
||||
# Font handling pour matplotlib (nécessaire dans Docker)
|
||||
# ===== MATPLOTLIB ADDONS =====
|
||||
cycler==0.12.1
|
||||
fonttools==4.58.5
|
||||
|
||||
# ===== REMOVED FROM ORIGINAL =====
|
||||
# Les packages suivants ont été supprimés car non utilisés dans votre app:
|
||||
# - awsebcli (AWS CLI - pas nécessaire dans le container)
|
||||
# - blessed, fabric, invoke (outils dev)
|
||||
# - jupyter, ipython, spyder (dev environment)
|
||||
# - nltk (pas d'NLP dans votre app)
|
||||
# - pytest (tests - à garder seulement pour dev)
|
||||
# - flask, gunicorn (vous utilisez FastAPI+uvicorn)
|
||||
# - docker (pas besoin dans le container)
|
||||
# - git* packages (dev tools)
|
||||
|
||||
# =============================================================================
|
||||
# VERSION ALTERNATIVE - PLUS MINIMALISTE
|
||||
# =============================================================================
|
||||
|
||||
# Si vous voulez une version encore plus légère, voici les STRICTEMENT nécessaires:
|
||||
# fastapi==0.115.14
|
||||
# uvicorn==0.35.0
|
||||
# streamlit==1.46.1
|
||||
# tensorflow==2.19.0
|
||||
# numpy==2.1.3
|
||||
# pillow==11.3.0
|
||||
# matplotlib==3.10.3
|
||||
# pandas==2.3.0
|
||||
# requests==2.32.4
|
||||
# mlflow==3.2.0
|
||||
# boto3==1.39.2
|
||||
# segmentation_models @ git+https://github.com/qubvel/segmentation_models.git@5d24bbfb28af6134e25e2c0b79e7727f6c0491d0
|
||||
|
||||
# =============================================================================
|
||||
# NOTES SUR SEGMENTATION_MODELS
|
||||
# =============================================================================
|
||||
|
||||
# OPTION A: Git avec commit hash (ACTUEL - RECOMMANDÉ)
|
||||
# Avantages: Version exacte, pas de surprises
|
||||
# Inconvénients: Nécessite git dans le container de build
|
||||
|
||||
# OPTION B: Version PyPI officielle
|
||||
# segmentation-models==1.0.1
|
||||
# Avantages: Plus simple, pas besoin de git
|
||||
# Inconvénients: Peut être différent de votre version dev
|
||||
|
||||
# OPTION C: Version locale (si vous avez des modifications)
|
||||
# Copier le package dans le container et faire pip install -e .
|
||||
|
|
|
|||
|
|
@ -10,25 +10,21 @@ import base64
|
|||
import pickle
|
||||
|
||||
# Configuration de l'API
|
||||
# prod docker
|
||||
API_BASE_URL = os.getenv("MULTISEG_API_BASE_URL", "http://127.0.0.1:8080/api")
|
||||
# test local
|
||||
# API_BASE_URL = os.getenv("MULTISEG_API_BASE_URL", "http://127.0.0.1:8000") # ← Port 8000 au lieu de 8080
|
||||
|
||||
# Import des fonctions utils pour l'affichage local
|
||||
sys.path.append('/app')
|
||||
# APRÈS (pour test local)
|
||||
sys.path.append('.')
|
||||
from utils.utils import colorize_mask
|
||||
|
||||
# Configuration de la page
|
||||
st.set_page_config(
|
||||
page_title="Multi-Class Segmentation",
|
||||
page_icon="🚗",
|
||||
page_title="Multi-Class Segmentation Comparison",
|
||||
page_icon="🔄",
|
||||
layout="wide"
|
||||
)
|
||||
|
||||
# CSS personnalisé
|
||||
# CSS personnalisé mis à jour
|
||||
st.markdown("""
|
||||
<style>
|
||||
.main-header {
|
||||
|
|
@ -39,6 +35,23 @@ st.markdown("""
|
|||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
.comparison-card {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
border-left: 4px solid #667eea;
|
||||
margin: 1rem 0;
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
}
|
||||
.speed-winner {
|
||||
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
|
||||
border-left-color: #28a745;
|
||||
}
|
||||
.speed-slower {
|
||||
background: linear-gradient(135deg, #f8d7da 0%, #f1b0b7 100%);
|
||||
border-left-color: #dc3545;
|
||||
}
|
||||
.metric-card {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 1rem;
|
||||
|
|
@ -60,33 +73,19 @@ st.markdown("""
|
|||
# En-tête principal
|
||||
st.markdown("""
|
||||
<div class="main-header">
|
||||
<h1>🚗 Segmentation Multi-Classes</h1>
|
||||
<p>Système de segmentation sémantique pour véhicules autonomes</p>
|
||||
<h1>🔄 Comparaison de Segmentation Multi-Classes</h1>
|
||||
<p>Système de comparaison entre deux modèles de segmentation sémantique</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Initialisation des états
|
||||
if "prediction_result" not in st.session_state:
|
||||
st.session_state.prediction_result = None
|
||||
if "selected_image" not in st.session_state:
|
||||
st.session_state.selected_image = None
|
||||
if "uploaded_file" not in st.session_state:
|
||||
st.session_state.uploaded_file = None
|
||||
|
||||
def reset_state():
|
||||
"""Reset des états de session"""
|
||||
st.session_state.prediction_result = None
|
||||
st.session_state.selected_image = None
|
||||
st.session_state.uploaded_file = None
|
||||
|
||||
def get_model_info():
|
||||
"""Récupère les informations du modèle"""
|
||||
def get_models_info():
|
||||
"""Récupère les informations des deux modèles"""
|
||||
try:
|
||||
response = requests.get(f"{API_BASE_URL}/models")
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
st.error(f"Erreur lors de la récupération des infos modèle: {response.status_code}")
|
||||
st.error(f"Erreur lors de la récupération des infos modèles: {response.status_code}")
|
||||
return None
|
||||
except Exception as e:
|
||||
st.error(f"Erreur de connexion à l'API: {e}")
|
||||
|
|
@ -105,15 +104,14 @@ def get_sample_images():
|
|||
st.error(f"Erreur de connexion à l'API: {e}")
|
||||
return None
|
||||
|
||||
def predict_sample_image(filename):
|
||||
"""Lance une prédiction sur une image d'exemple"""
|
||||
def compare_sample_image(filename):
|
||||
"""Lance une comparaison sur une image d'exemple"""
|
||||
try:
|
||||
# CORRECTION: Envoyer en JSON au lieu de form data
|
||||
data = {"filename": filename}
|
||||
response = requests.post(
|
||||
f"{API_BASE_URL}/predict-sample",
|
||||
json=data, # ← Changé de 'data=' à 'json='
|
||||
headers={"Content-Type": "application/json"} # ← Ajout du header explicite
|
||||
f"{API_BASE_URL}/compare-sample",
|
||||
json=data,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
|
|
@ -122,14 +120,14 @@ def predict_sample_image(filename):
|
|||
st.error(f"Erreur API: {response.status_code} - {response.text}")
|
||||
return None
|
||||
except Exception as e:
|
||||
st.error(f"Erreur lors de la prédiction: {e}")
|
||||
st.error(f"Erreur lors de la comparaison: {e}")
|
||||
return None
|
||||
|
||||
def predict_uploaded_image(uploaded_file):
|
||||
"""Lance une prédiction sur une image uploadée"""
|
||||
def compare_uploaded_image(uploaded_file):
|
||||
"""Lance une comparaison sur une image uploadée"""
|
||||
try:
|
||||
files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)}
|
||||
response = requests.post(f"{API_BASE_URL}/predict-upload", files=files)
|
||||
response = requests.post(f"{API_BASE_URL}/compare-upload", files=files)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
|
|
@ -137,303 +135,253 @@ def predict_uploaded_image(uploaded_file):
|
|||
st.error(f"Erreur API: {response.status_code} - {response.text}")
|
||||
return None
|
||||
except Exception as e:
|
||||
st.error(f"Erreur lors de la prédiction: {e}")
|
||||
st.error(f"Erreur lors de la comparaison: {e}")
|
||||
return None
|
||||
|
||||
def display_segmentation_plots(fig):
|
||||
"""Affiche chaque subplot séparément dans des colonnes"""
|
||||
if fig is None:
|
||||
st.warning("Aucune figure de segmentation disponible")
|
||||
def display_comparison_results(result, models_info):
|
||||
"""Affiche les résultats de comparaison entre les deux modèles"""
|
||||
if not result or not result['success']:
|
||||
st.error("Aucun résultat de comparaison disponible")
|
||||
return
|
||||
|
||||
axes = fig.get_axes()
|
||||
num_plots = len(axes)
|
||||
# Informations des modèles
|
||||
st.markdown("### 🏆 Modèles Comparés")
|
||||
|
||||
st.markdown("### 🎨 Résultat de la Segmentation")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
# Créer les colonnes selon le nombre de subplots
|
||||
if num_plots <= 4:
|
||||
cols = st.columns(num_plots)
|
||||
else:
|
||||
cols = st.columns(4) # Maximum 4 colonnes
|
||||
with col1:
|
||||
model1 = result['model1_info']
|
||||
st.markdown(f"""
|
||||
<div class="comparison-card">
|
||||
<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):
|
||||
col_idx = i % len(cols)
|
||||
with col2:
|
||||
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]:
|
||||
# Récupérer le titre existant
|
||||
title = ax.get_title()
|
||||
# st.markdown(f"**{title}:**")
|
||||
# Comparaison des temps d'inférence
|
||||
st.markdown("### ⏱️ Comparaison des Performances")
|
||||
|
||||
# Créer une figure individuelle avec ce subplot
|
||||
individual_fig, individual_ax = plt.subplots(figsize=(6, 6))
|
||||
speed_comp = result['speed_comparison']
|
||||
model1_time = result['model1_inference_time'] * 1000
|
||||
model2_time = result['model2_inference_time'] * 1000
|
||||
|
||||
# Copier toutes les images de l'axe original
|
||||
for image in ax.get_images():
|
||||
individual_ax.imshow(
|
||||
image.get_array(),
|
||||
cmap=image.get_cmap() if image.get_cmap() else None,
|
||||
alpha=image.get_alpha() if image.get_alpha() else None,
|
||||
vmin=image.get_clim()[0] if image.get_clim() else None,
|
||||
vmax=image.get_clim()[1] if image.get_clim() else None
|
||||
)
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
# Copier les propriétés de l'axe
|
||||
individual_ax.set_title(title)
|
||||
individual_ax.axis('off')
|
||||
with col1:
|
||||
winner_class = "speed-winner" if speed_comp['faster_model'] == model1['encoder_name'] else "speed-slower"
|
||||
st.markdown(f"""
|
||||
<div class="comparison-card {winner_class}">
|
||||
<h5>🥇 {model1['encoder_name']}</h5>
|
||||
<strong>{model1_time:.1f} ms</strong>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Ajuster la mise en page
|
||||
individual_fig.tight_layout()
|
||||
with col2:
|
||||
st.markdown(f"""
|
||||
<div class="comparison-card">
|
||||
<h5>⚡ Comparaison</h5>
|
||||
<strong>{speed_comp['faster_model']}</strong><br>
|
||||
est {speed_comp['speedup_ratio']}x plus rapide<br>
|
||||
<small>Différence: {speed_comp['time_difference_ms']:.1f} ms</small>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Afficher dans Streamlit
|
||||
st.pyplot(individual_fig)
|
||||
with col3:
|
||||
winner_class = "speed-winner" if speed_comp['faster_model'] == model2['encoder_name'] else "speed-slower"
|
||||
st.markdown(f"""
|
||||
<div class="comparison-card {winner_class}">
|
||||
<h5>🥈 {model2['encoder_name']}</h5>
|
||||
<strong>{model2_time:.1f} ms</strong>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Libérer la mémoire
|
||||
plt.close(individual_fig)
|
||||
# Affichage des segmentations
|
||||
if result.get('figure_data'):
|
||||
st.markdown("### 🎨 Résultats de Segmentation")
|
||||
try:
|
||||
fig_bytes = base64.b64decode(result['figure_data'])
|
||||
fig = pickle.loads(fig_bytes)
|
||||
st.pyplot(fig)
|
||||
plt.close(fig)
|
||||
except Exception as e:
|
||||
st.error(f"Erreur lors de l'affichage: {e}")
|
||||
|
||||
# Fermer la figure originale pour libérer la mémoire
|
||||
plt.close(fig)
|
||||
# Comparaison des statistiques
|
||||
st.markdown("### 📊 Comparaison des Statistiques")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
def display_prediction_stats_with_chart(stats, model_info):
|
||||
"""Affiche les statistiques avec un graphique en barres"""
|
||||
st.markdown("### 📊 Statistiques de Segmentation")
|
||||
with col1:
|
||||
st.markdown(f"**📈 {model1['encoder_name']} - Répartition des classes:**")
|
||||
if result.get('model1_stats'):
|
||||
display_model_stats_chart(result['model1_stats'], models_info, f"Modèle 1 - {model1['encoder_name']}")
|
||||
|
||||
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:
|
||||
st.warning("Aucune statistique disponible")
|
||||
return
|
||||
|
||||
# Ratio 1:2 pour donner plus de place au graphique
|
||||
col1, col2 = st.columns([1, 2])
|
||||
# Préparer les données pour le graphique
|
||||
class_names = list(stats.keys())
|
||||
percentages = [data['percentage'] for data in stats.values()]
|
||||
|
||||
# Colonne 1: Cartes de statistiques compactes avec carrés colorés
|
||||
with col1:
|
||||
st.markdown("**Détails par classe:**")
|
||||
# Récupérer les couleurs des classes
|
||||
colors = []
|
||||
if models_info and 'class_colors' in models_info and 'class_names' in models_info:
|
||||
model_class_names = models_info['class_names']
|
||||
model_class_colors = models_info['class_colors']
|
||||
|
||||
# Récupérer les couleurs des classes pour les carrés
|
||||
class_colors_dict = {}
|
||||
if model_info and 'class_colors' in model_info and 'class_names' in model_info:
|
||||
model_class_names = model_info['class_names']
|
||||
model_class_colors = model_info['class_colors']
|
||||
|
||||
for class_name in stats.keys():
|
||||
try:
|
||||
idx = model_class_names.index(class_name)
|
||||
color = model_class_colors[idx]
|
||||
# Convertir en hex pour CSS
|
||||
color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
|
||||
class_colors_dict[class_name] = color_hex
|
||||
except (ValueError, IndexError):
|
||||
# Couleur par défaut si pas trouvée
|
||||
class_colors_dict[class_name] = "#808080"
|
||||
|
||||
for class_name, data in stats.items():
|
||||
# Récupérer la couleur pour cette classe
|
||||
color_hex = class_colors_dict.get(class_name, "#808080")
|
||||
|
||||
# ✨ NOUVEAU: Carré coloré + nom de classe
|
||||
st.markdown(f"""
|
||||
<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]}...")
|
||||
for class_name in class_names:
|
||||
try:
|
||||
idx = model_class_names.index(class_name)
|
||||
color = model_class_colors[idx]
|
||||
colors.append([c/255.0 for c in color])
|
||||
except (ValueError, IndexError):
|
||||
colors.append([0.5, 0.5, 0.5])
|
||||
else:
|
||||
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("## 🎯 Classes Segmentées")
|
||||
|
||||
# ✨ NOUVEAU: Affichage avec carrés colorés
|
||||
if model_info and 'class_names' in model_info and 'class_colors' in model_info:
|
||||
class_names = model_info['class_names']
|
||||
class_colors = model_info['class_colors']
|
||||
# Affichage avec carrés colorés
|
||||
if models_info and 'class_names' in models_info and 'class_colors' in models_info:
|
||||
class_names = models_info['class_names']
|
||||
class_colors = models_info['class_colors']
|
||||
|
||||
for i, (class_name, color) in enumerate(zip(class_names, class_colors)):
|
||||
# Convertir RGB en hex
|
||||
color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
|
||||
|
||||
# Affichage avec carré coloré
|
||||
st.markdown(f"""
|
||||
<div style="
|
||||
display: flex;
|
||||
|
|
@ -453,56 +401,26 @@ with st.sidebar:
|
|||
<span style="font-weight: 500;">{i}. {class_name}</span>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
else:
|
||||
# Fallback si pas d'info couleur
|
||||
if model_info and 'class_names' in model_info:
|
||||
for i, class_name in enumerate(model_info['class_names']):
|
||||
st.write(f"{i}. {class_name}")
|
||||
else:
|
||||
st.write("Informations des classes non disponibles")
|
||||
|
||||
# États de session
|
||||
if "sample_comparison_result" not in st.session_state:
|
||||
st.session_state.sample_comparison_result = None
|
||||
if "upload_comparison_result" not in st.session_state:
|
||||
st.session_state.upload_comparison_result = None
|
||||
|
||||
# Initialisation des états - MODIFIÉE pour séparer les résultats par onglet
|
||||
if "sample_prediction_result" not in st.session_state:
|
||||
st.session_state.sample_prediction_result = None
|
||||
if "sample_selected_image" not in st.session_state:
|
||||
st.session_state.sample_selected_image = None
|
||||
def reset_sample_comparison():
|
||||
st.session_state.sample_comparison_result = None
|
||||
|
||||
if "upload_prediction_result" not in st.session_state:
|
||||
st.session_state.upload_prediction_result = None
|
||||
if "upload_uploaded_file" not in st.session_state:
|
||||
st.session_state.upload_uploaded_file = None
|
||||
def reset_upload_comparison():
|
||||
st.session_state.upload_comparison_result = None
|
||||
|
||||
# ANCIEN système (gardé pour compatibilité si besoin)
|
||||
if "prediction_result" not in st.session_state:
|
||||
st.session_state.prediction_result = None
|
||||
if "selected_image" not in st.session_state:
|
||||
st.session_state.selected_image = None
|
||||
if "uploaded_file" not in st.session_state:
|
||||
st.session_state.uploaded_file = None
|
||||
|
||||
def reset_sample_state():
|
||||
"""Reset des états de session pour l'onglet Sample"""
|
||||
st.session_state.sample_prediction_result = None
|
||||
st.session_state.sample_selected_image = None
|
||||
|
||||
def reset_upload_state():
|
||||
"""Reset des états de session pour l'onglet Upload"""
|
||||
st.session_state.upload_prediction_result = None
|
||||
st.session_state.upload_uploaded_file = None
|
||||
|
||||
def reset_state():
|
||||
"""Reset complet (garde l'ancienne fonction pour compatibilité)"""
|
||||
reset_sample_state()
|
||||
reset_upload_state()
|
||||
|
||||
# Interface principale - MODIFIÉE pour résultats indépendants
|
||||
# Interface principale
|
||||
tab1, tab2 = st.tabs(["📸 Images d'Exemple", "📤 Upload Personnel"])
|
||||
|
||||
# Tab 1: Images d'exemple
|
||||
with tab1:
|
||||
st.markdown("## 📸 Sélectionner une Image d'Exemple")
|
||||
st.markdown("Ces images proviennent du dataset Cityscapes avec masques de vérité disponibles.")
|
||||
st.markdown("## 📸 Comparaison sur Images d'Exemple")
|
||||
st.markdown("Comparez les performances des deux modèles sur des images Cityscapes.")
|
||||
|
||||
# Récupérer les images d'exemple
|
||||
sample_data = get_sample_images()
|
||||
|
|
@ -530,85 +448,47 @@ with tab1:
|
|||
# Prévisualisation de l'image sélectionnée
|
||||
if selected_filename:
|
||||
try:
|
||||
# Récupérer l'image via l'API
|
||||
image_url = f"{API_BASE_URL}/sample-image/{selected_filename}"
|
||||
response = requests.get(image_url, timeout=5)
|
||||
|
||||
if response.status_code == 200:
|
||||
# Charger l'image depuis les bytes
|
||||
sample_image = Image.open(io.BytesIO(response.content))
|
||||
st.image(
|
||||
sample_image,
|
||||
caption=f"Aperçu: {selected_display_name}",
|
||||
use_container_width=True
|
||||
)
|
||||
elif response.status_code == 404:
|
||||
st.warning("📷 Image non trouvée sur le serveur")
|
||||
else:
|
||||
st.info("📷 Prévisualisation temporairement indisponible")
|
||||
except:
|
||||
st.info("📷 Aperçu non disponible - L'image sera chargée lors de la comparaison")
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
st.warning("⏱️ Timeout lors du chargement de l'aperçu")
|
||||
except Exception as e:
|
||||
st.info("📷 Aperçu non disponible - L'image sera chargée lors de la prédiction")
|
||||
|
||||
# Bouton de prédiction
|
||||
if st.button("🔮 Lancer la Segmentation", key="predict_sample", type="primary"):
|
||||
# Bouton de comparaison
|
||||
if st.button("🔄 Lancer la Comparaison", key="compare_sample", type="primary"):
|
||||
if selected_filename:
|
||||
with st.spinner("Segmentation en cours..."):
|
||||
result = predict_sample_image(selected_filename)
|
||||
with st.spinner("Comparaison en cours..."):
|
||||
result = compare_sample_image(selected_filename)
|
||||
if result and result['success']:
|
||||
# ✨ NOUVEAU: Stocker dans les variables Sample
|
||||
st.session_state.sample_prediction_result = result
|
||||
st.session_state.sample_selected_image = selected_filename
|
||||
st.success("✅ Segmentation terminée!")
|
||||
st.session_state.sample_comparison_result = result
|
||||
st.success("✅ Comparaison terminée!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("❌ Erreur lors de la segmentation")
|
||||
st.error("❌ Erreur lors de la comparaison")
|
||||
else:
|
||||
st.warning("Aucune image d'exemple disponible")
|
||||
|
||||
# ✨ NOUVEAU: Affichage des résultats Sample (indépendants)
|
||||
if st.session_state.sample_prediction_result and st.session_state.sample_selected_image:
|
||||
# Affichage des résultats de comparaison
|
||||
if st.session_state.sample_comparison_result:
|
||||
st.markdown("---")
|
||||
result = st.session_state.sample_prediction_result
|
||||
|
||||
st.markdown(f"## 🎯 Résultats pour: {st.session_state.sample_selected_image}")
|
||||
|
||||
# Afficher la segmentation
|
||||
if result.get('figure_data'):
|
||||
try:
|
||||
# Désérialiser la figure
|
||||
fig_bytes = base64.b64decode(result['figure_data'])
|
||||
fig = pickle.loads(fig_bytes)
|
||||
display_segmentation_plots(fig)
|
||||
except Exception as e:
|
||||
st.error(f"Erreur lors de l'affichage de la segmentation: {e}")
|
||||
# Fallback sur la légende des classes
|
||||
display_class_legend(model_info)
|
||||
else:
|
||||
# Fallback si pas de figure_data
|
||||
display_class_legend(model_info)
|
||||
|
||||
# Afficher les statistiques
|
||||
if result.get('prediction_stats'):
|
||||
display_prediction_stats_with_chart(result['prediction_stats'], model_info)
|
||||
|
||||
# Informations additionnelles
|
||||
if result.get('ground_truth_available'):
|
||||
st.success("🎯 Masque de vérité terrain disponible pour comparaison")
|
||||
else:
|
||||
st.info("ℹ️ Pas de vérité terrain disponible")
|
||||
display_comparison_results(st.session_state.sample_comparison_result, models_info)
|
||||
|
||||
# Bouton de reset
|
||||
if st.button("🔄 Nouvelle Analyse", key="reset_sample", type="secondary"):
|
||||
reset_sample_state()
|
||||
if st.button("🔄 Nouvelle Comparaison", key="reset_sample", type="secondary"):
|
||||
reset_sample_comparison()
|
||||
st.rerun()
|
||||
|
||||
# Tab 2: Upload personnel
|
||||
with tab2:
|
||||
st.markdown("## 📤 Uploader Votre Image")
|
||||
st.markdown("Uploadez une image de rue pour analyse. Formats acceptés: JPG, PNG")
|
||||
st.markdown("## 📤 Comparaison sur Image Personnelle")
|
||||
st.markdown("Uploadez une image pour comparer les performances des deux modèles.")
|
||||
|
||||
uploaded_file = st.file_uploader(
|
||||
"Sélectionnez une image:",
|
||||
|
|
@ -625,62 +505,33 @@ with tab2:
|
|||
file_size_mb = len(uploaded_file.getvalue()) / 1024 / 1024
|
||||
st.info(f"📁 **Fichier:** {uploaded_file.name} | 📊 **Taille:** {file_size_mb:.2f} MB")
|
||||
|
||||
# Bouton de prédiction
|
||||
if st.button("🔮 Lancer la Segmentation", key="predict_upload", type="primary"):
|
||||
with st.spinner("Segmentation en cours..."):
|
||||
result = predict_uploaded_image(uploaded_file)
|
||||
# Bouton de comparaison
|
||||
if st.button("🔄 Lancer la Comparaison", key="compare_upload", type="primary"):
|
||||
with st.spinner("Comparaison en cours..."):
|
||||
result = compare_uploaded_image(uploaded_file)
|
||||
if result and result['success']:
|
||||
# ✨ NOUVEAU: Stocker dans les variables Upload
|
||||
st.session_state.upload_prediction_result = result
|
||||
st.session_state.upload_uploaded_file = uploaded_file.name
|
||||
st.success("✅ Segmentation terminée!")
|
||||
st.session_state.upload_comparison_result = result
|
||||
st.success("✅ Comparaison terminée!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("❌ Erreur lors de la segmentation")
|
||||
st.error("❌ Erreur lors de la comparaison")
|
||||
|
||||
# ✨ NOUVEAU: Affichage des résultats Upload (indépendants)
|
||||
if st.session_state.upload_prediction_result and st.session_state.upload_uploaded_file:
|
||||
# Affichage des résultats de comparaison
|
||||
if st.session_state.upload_comparison_result:
|
||||
st.markdown("---")
|
||||
result = st.session_state.upload_prediction_result
|
||||
|
||||
st.markdown(f"## 🎯 Résultats pour: {st.session_state.upload_uploaded_file}")
|
||||
|
||||
# Afficher la segmentation
|
||||
if result.get('figure_data'):
|
||||
try:
|
||||
# Désérialiser la figure
|
||||
fig_bytes = base64.b64decode(result['figure_data'])
|
||||
fig = pickle.loads(fig_bytes)
|
||||
display_segmentation_plots(fig)
|
||||
except Exception as e:
|
||||
st.error(f"Erreur lors de l'affichage de la segmentation: {e}")
|
||||
# Fallback sur la légende des classes
|
||||
display_class_legend(model_info)
|
||||
else:
|
||||
# Fallback si pas de figure_data
|
||||
display_class_legend(model_info)
|
||||
|
||||
# Afficher les statistiques
|
||||
if result.get('prediction_stats'):
|
||||
display_prediction_stats_with_chart(result['prediction_stats'], model_info)
|
||||
|
||||
# Informations additionnelles
|
||||
if result.get('ground_truth_available'):
|
||||
st.success("🎯 Masque de vérité terrain disponible pour comparaison")
|
||||
else:
|
||||
st.info("ℹ️ Pas de vérité terrain disponible (image personnelle)")
|
||||
display_comparison_results(st.session_state.upload_comparison_result, models_info)
|
||||
|
||||
# Bouton de reset
|
||||
if st.button("🔄 Nouvelle Analyse", key="reset_upload", type="secondary"):
|
||||
reset_upload_state()
|
||||
if st.button("🔄 Nouvelle Comparaison", key="reset_upload", type="secondary"):
|
||||
reset_upload_comparison()
|
||||
st.rerun()
|
||||
|
||||
# Footer
|
||||
st.markdown("---")
|
||||
st.markdown("""
|
||||
<div style="text-align: center; color: #666; padding: 1rem;">
|
||||
🚗 <strong>Multi-Class Segmentation API</strong> -
|
||||
Système de vision par ordinateur pour véhicules autonomes<br>
|
||||
<small>Développé avec FastAPI, Streamlit et TensorFlow</small>
|
||||
🔄 <strong>Multi-Class Segmentation Comparison API</strong> -
|
||||
Système de comparaison de modèles de vision par ordinateur<br>
|
||||
<small>Développé avec FastAPI, Streamlit et PyTorch</small>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
|
@ -606,13 +606,13 @@ tokenizers==0.21.4
|
|||
toml==0.10.2
|
||||
tomlkit==0.13.3
|
||||
toolz==0.12.1
|
||||
torch==2.8.0+cu126
|
||||
torch==2.8.0
|
||||
torchao==0.10.0
|
||||
torchaudio==2.8.0+cu126
|
||||
torchaudio==2.8.0
|
||||
torchdata==0.11.0
|
||||
torchsummary==1.5.1
|
||||
torchtune==0.6.1
|
||||
torchvision==0.23.0+cu126
|
||||
torchvision==0.23.0
|
||||
tornado==6.4.2
|
||||
tqdm==4.67.1
|
||||
traitlets==5.7.1
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import json
|
|||
import tempfile
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import matplotlib.pyplot as plt
|
||||
import segmentation_models_pytorch as smp
|
||||
import mlflow
|
||||
|
|
@ -16,8 +15,8 @@ import matplotlib.patches as mpatches
|
|||
from matplotlib.colors import ListedColormap
|
||||
|
||||
# Preprocessing/Images
|
||||
from PIL import Image
|
||||
import torchvision.transforms as transforms
|
||||
import albumentations as A
|
||||
from albumentations.pytorch import ToTensorV2
|
||||
|
||||
|
||||
def load_cityscapes_config(config_path="../cityscapes_config.json", verbose=True):
|
||||
|
|
@ -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
|
||||
pour l'inférence, en reproduisant exactement le pipeline CityscapesDataset.
|
||||
"""
|
||||
import albumentations as A
|
||||
from albumentations.pytorch import ToTensorV2
|
||||
import cv2
|
||||
import torch
|
||||
|
||||
if encoder is None:
|
||||
mean, std = [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]
|
||||
|
|
@ -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
|
||||
def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config,
|
||||
mask_path=None, save_dir=None, show_stats=True):
|
||||
mask_path=None, save_dir=None, show_stats=True, show_plot=True):
|
||||
"""
|
||||
Version simplifiée utilisant preprocess_image_and_mask
|
||||
"""
|
||||
|
|
@ -461,7 +456,6 @@ def run_inference_and_visualize(image_path, model, encoder_name, img_size, mappi
|
|||
fig.savefig(fig_path, dpi=150, bbox_inches='tight')
|
||||
print(f"💾 Résultat sauvegardé: {fig_path}")
|
||||
|
||||
plt.show()
|
||||
|
||||
# Statistiques
|
||||
if show_stats:
|
||||
|
|
@ -481,7 +475,11 @@ def run_inference_and_visualize(image_path, model, encoder_name, img_size, mappi
|
|||
percentage = (count / mask_gt_np.size) * 100
|
||||
print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)")
|
||||
|
||||
return predicted_mask, inference_time
|
||||
if show_plot:
|
||||
plt.show()
|
||||
return predicted_mask, inference_time
|
||||
else:
|
||||
return predicted_mask, inference_time, fig
|
||||
|
||||
|
||||
def load_model(run_id=None, experiment_name="OC Projet 9",
|
||||
|
|
@ -736,6 +734,6 @@ def load_second_best_model(experiment_name="OC Projet 9", metric="test_mean_iou"
|
|||
|
||||
def load_model_by_run_id(run_id):
|
||||
"""Charge un modèle par run_id spécifique"""
|
||||
return load_model_by_param(run_id=run_id)
|
||||
return load_model(run_id=run_id)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue