import os import sys 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.responses import FileResponse from pydantic import BaseModel import numpy as np import pickle import base64 # 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() aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY", "").strip() print(f"MLFLOW_TRACKING_URI: {mlflow_tracking_uri}") print(f"MLFLOW_S3_ENDPOINT_URL: {mlflow_s3_endpoint_url}") print(f"AWS_ACCESS_KEY_ID: {aws_access_key_id}") print(f"AWS_SECRET_ACCESS_KEY: {'***' if aws_secret_access_key else None} ({'set' if aws_secret_access_key else 'not set'})") print("============================================") 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)") os.environ["MLFLOW_S3_ENDPOINT_URL"] = mlflow_s3_endpoint_url os.environ["AWS_ACCESS_KEY_ID"] = aws_access_key_id 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)") # Import des fonctions utils sys.path.append('.') from utils.utils import ( load_cityscapes_config, load_best_model_from_registry, 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 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 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 try: # Configuration MLflow print("🔄 Configuration de MLflow...") configure_mlflow() print("🔄 Chargement de la configuration Cityscapes...") 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(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']}") # 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" ) # ✅ 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é!") 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.") @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, "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") return { "status": "healthy", "model_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") return { "model_name": MODEL_NAME, "run_id": run_id, "encoder": encoder_name, "input_size": script_img_size, "num_classes": mapping_config['num_classes'], "class_names": mapping_config['group_names'], "class_colors": mapping_config['group_colors'].tolist() } @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) ) 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") 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( image_path=str(image_path), mask_path=mask_path_str, 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 ) # 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", image_path=str(image_path), mask_path=mask_path_str, prediction_stats=stats, 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)}") @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 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)}" ) 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 ) # 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", image_path=temp_file_path, prediction_stats=stats, 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)}") 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}") @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, "num_classes": mapping_config['num_classes'] if mapping_config else None, "timestamp": datetime.utcnow().isoformat() }