427 lines
No EOL
16 KiB
Python
427 lines
No EOL
16 KiB
Python
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()
|
|
} |