Merge branch 'main' of https://github.com/GrgMdmn/OpenClassrooms_AI_Projects
This commit is contained in:
commit
133436cc3e
6 changed files with 871 additions and 975 deletions
0
EV3
Normal file
0
EV3
Normal file
569
app/main.py
569
app/main.py
|
|
@ -1,24 +1,27 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
import tempfile
|
import tempfile
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import mlflow
|
import mlflow
|
||||||
from fastapi import FastAPI, HTTPException, File, UploadFile, Form
|
from fastapi import FastAPI, HTTPException, File, UploadFile
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pickle
|
import pickle
|
||||||
import base64
|
import base64
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.image as mpimg
|
||||||
|
|
||||||
|
|
||||||
# Configuration MLflow avec variables d'environnement
|
# Configuration MLflow avec variables d'environnement
|
||||||
def configure_mlflow():
|
def configure_mlflow():
|
||||||
"""Configure MLflow avec les variables d'environnement"""
|
"""Configure MLflow avec les variables d'environnement"""
|
||||||
print("=== DEBUG VARIABLES D'ENVIRONNEMENT ===")
|
print("=== DEBUG VARIABLES D'ENVIRONNEMENT ===")
|
||||||
|
|
||||||
# ✅ NETTOYAGE automatique des variables d'environnement
|
|
||||||
mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI", "").strip()
|
mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI", "").strip()
|
||||||
mlflow_s3_endpoint_url = os.getenv("MLFLOW_S3_ENDPOINT_URL", "").strip()
|
mlflow_s3_endpoint_url = os.getenv("MLFLOW_S3_ENDPOINT_URL", "").strip()
|
||||||
aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID", "").strip()
|
aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID", "").strip()
|
||||||
|
|
@ -33,11 +36,9 @@ def configure_mlflow():
|
||||||
if not mlflow_tracking_uri:
|
if not mlflow_tracking_uri:
|
||||||
raise ValueError("MLFLOW_TRACKING_URI not found in environment variables")
|
raise ValueError("MLFLOW_TRACKING_URI not found in environment variables")
|
||||||
|
|
||||||
# Configuration MLflow avec URLs nettoyées
|
|
||||||
mlflow.set_tracking_uri(mlflow_tracking_uri)
|
mlflow.set_tracking_uri(mlflow_tracking_uri)
|
||||||
print(f"MLflow Tracking URI: {mlflow_tracking_uri}")
|
print(f"MLflow Tracking URI: {mlflow_tracking_uri}")
|
||||||
|
|
||||||
# ✅ CORRECTION: Configuration EXPLICITE des identifiants avec nettoyage
|
|
||||||
if not mlflow_s3_endpoint_url or not aws_access_key_id or not aws_secret_access_key:
|
if not mlflow_s3_endpoint_url or not aws_access_key_id or not aws_secret_access_key:
|
||||||
raise ValueError("Variables d'environnement MLflow manquantes (S3_ENDPOINT, ACCESS_KEY, SECRET_KEY)")
|
raise ValueError("Variables d'environnement MLflow manquantes (S3_ENDPOINT, ACCESS_KEY, SECRET_KEY)")
|
||||||
|
|
||||||
|
|
@ -46,66 +47,111 @@ def configure_mlflow():
|
||||||
os.environ["AWS_SECRET_ACCESS_KEY"] = aws_secret_access_key
|
os.environ["AWS_SECRET_ACCESS_KEY"] = aws_secret_access_key
|
||||||
print(f"MLflow S3 Endpoint: {mlflow_s3_endpoint_url}")
|
print(f"MLflow S3 Endpoint: {mlflow_s3_endpoint_url}")
|
||||||
print("✅ Identifiants AWS configurés")
|
print("✅ Identifiants AWS configurés")
|
||||||
|
print("✅ Configuration MLflow terminée")
|
||||||
# ✅ SKIP set_experiment pour éviter le problème 404
|
|
||||||
# mlflow.set_experiment("OC Projet 8") # Commenté temporairement
|
|
||||||
print("✅ Configuration MLflow terminée (sans set_experiment)")
|
|
||||||
|
|
||||||
# Import des fonctions utils
|
# Import des fonctions utils
|
||||||
sys.path.append('.')
|
sys.path.append('.')
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from utils.utils import (
|
from utils.utils import (
|
||||||
load_cityscapes_config,
|
load_cityscapes_config,
|
||||||
load_best_model_from_registry,
|
load_model,
|
||||||
run_inference_and_visualize,
|
run_inference_and_visualize,
|
||||||
colorize_mask
|
colorize_mask
|
||||||
)
|
)
|
||||||
|
|
||||||
# ✅ AJOUT: Import segmentation_models pour le chargement des modèles
|
|
||||||
try:
|
|
||||||
import segmentation_models as sm
|
|
||||||
print("✅ segmentation_models importé - modèles compatibles")
|
|
||||||
except ImportError as e:
|
|
||||||
print(f"⚠️ segmentation_models non disponible: {e}")
|
|
||||||
print("💡 Installez avec: pip install git+https://github.com/qubvel/segmentation_models.git")
|
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
MODEL_NAME = "StreetsSegmentation"
|
MODEL_NAME = "StreetsSegmentation"
|
||||||
SAMPLE_IMAGES_DIR = "./notebooks/content/data/test_images_sample"
|
SAMPLE_IMAGES_DIR = "./notebooks/content/data/test_images_sample"
|
||||||
CITYSCAPES_CONFIG_PATH = "./cityscapes_config.json"
|
CITYSCAPES_CONFIG_PATH = "./cityscapes_config.json"
|
||||||
TEMP_UPLOAD_DIR = "/tmp/uploads"
|
TEMP_UPLOAD_DIR = "/tmp/uploads"
|
||||||
|
|
||||||
# Modèles de données
|
# Modèles de données mis à jour
|
||||||
|
class ModelInfo(BaseModel):
|
||||||
|
name: str
|
||||||
|
run_id: str
|
||||||
|
encoder_name: str
|
||||||
|
input_size: tuple
|
||||||
|
rank: int
|
||||||
|
architecture: Optional[str] = None # Ajout architecture
|
||||||
|
test_mean_iou: Optional[float] = None # Ajout mIoU
|
||||||
|
test_accuracy: Optional[float] = None # Ajout accuracy
|
||||||
|
class_ious: Optional[dict] = None # Ajout IoU par classe
|
||||||
|
|
||||||
|
class ComparisonResult(BaseModel):
|
||||||
|
success: bool
|
||||||
|
message: str
|
||||||
|
model1_info: ModelInfo
|
||||||
|
model2_info: ModelInfo
|
||||||
|
image_path: Optional[str] = None
|
||||||
|
mask_path: Optional[str] = None
|
||||||
|
model1_stats: Optional[dict] = None
|
||||||
|
model2_stats: Optional[dict] = None
|
||||||
|
model1_inference_time: float
|
||||||
|
model2_inference_time: float
|
||||||
|
speed_comparison: dict
|
||||||
|
ground_truth_available: bool = False
|
||||||
|
figure_data: Optional[str] = None
|
||||||
|
|
||||||
class ImageInfo(BaseModel):
|
class ImageInfo(BaseModel):
|
||||||
filename: str
|
filename: str
|
||||||
display_name: str
|
display_name: str
|
||||||
has_ground_truth: bool
|
has_ground_truth: bool
|
||||||
|
|
||||||
class PredictionResult(BaseModel):
|
|
||||||
success: bool
|
|
||||||
message: str
|
|
||||||
image_path: Optional[str] = None
|
|
||||||
mask_path: Optional[str] = None
|
|
||||||
prediction_stats: Optional[dict] = None
|
|
||||||
ground_truth_available: bool = False
|
|
||||||
figure_data: Optional[str] = None
|
|
||||||
|
|
||||||
class AvailableImagesResponse(BaseModel):
|
class AvailableImagesResponse(BaseModel):
|
||||||
images: List[ImageInfo]
|
images: List[ImageInfo]
|
||||||
total_count: int
|
total_count: int
|
||||||
|
|
||||||
# Variables globales pour le modèle
|
# Variables globales pour les deux modèles
|
||||||
model = None
|
best_model = None
|
||||||
encoder_name = None
|
second_best_model = None
|
||||||
script_img_size = None
|
best_model_info = {}
|
||||||
|
second_best_model_info = {}
|
||||||
mapping_config = None
|
mapping_config = None
|
||||||
run_id = None
|
|
||||||
|
|
||||||
def initialize_model():
|
def get_model_detailed_info(run_id, mapping_config):
|
||||||
"""Initialise le modèle et la configuration au démarrage"""
|
"""Récupère les informations détaillées d'un modèle depuis MLflow"""
|
||||||
global model, encoder_name, script_img_size, mapping_config, run_id
|
client = mlflow.tracking.MlflowClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
run = mlflow.get_run(run_id)
|
||||||
|
params = run.data.params
|
||||||
|
metrics = run.data.metrics
|
||||||
|
|
||||||
|
# Architecture depuis le paramètre model_architecture
|
||||||
|
architecture = params.get("model_architecture", "Unknown").split('_')[0]
|
||||||
|
|
||||||
|
# Métriques de performance
|
||||||
|
test_mean_iou = metrics.get("test_mean_iou", None)
|
||||||
|
test_accuracy = metrics.get("test_accuracy", None)
|
||||||
|
|
||||||
|
# IoU par classe depuis les métriques class_X_iou
|
||||||
|
class_ious = {}
|
||||||
|
if mapping_config and 'group_names' in mapping_config:
|
||||||
|
for i, class_name in enumerate(mapping_config['group_names']):
|
||||||
|
metric_key = f"class_{i}_iou"
|
||||||
|
if metric_key in metrics:
|
||||||
|
class_ious[class_name] = round(metrics[metric_key], 3)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"architecture": architecture,
|
||||||
|
"test_mean_iou": test_mean_iou,
|
||||||
|
"test_accuracy": test_accuracy,
|
||||||
|
"class_ious": class_ious
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Erreur lors de la récupération des infos détaillées: {e}")
|
||||||
|
return {
|
||||||
|
"architecture": "Unknown",
|
||||||
|
"test_mean_iou": None,
|
||||||
|
"test_accuracy": None,
|
||||||
|
"class_ious": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize_models():
|
||||||
|
"""Initialise les deux meilleurs modèles au démarrage"""
|
||||||
|
global best_model, second_best_model, best_model_info, second_best_model_info, mapping_config
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Configuration MLflow
|
|
||||||
print("🔄 Configuration de MLflow...")
|
print("🔄 Configuration de MLflow...")
|
||||||
configure_mlflow()
|
configure_mlflow()
|
||||||
|
|
||||||
|
|
@ -113,76 +159,99 @@ def initialize_model():
|
||||||
mapping_config = load_cityscapes_config(CITYSCAPES_CONFIG_PATH, verbose=False)
|
mapping_config = load_cityscapes_config(CITYSCAPES_CONFIG_PATH, verbose=False)
|
||||||
print("✅ Configuration Cityscapes chargée")
|
print("✅ Configuration Cityscapes chargée")
|
||||||
|
|
||||||
print("🔄 Chargement du modèle depuis MLflow...")
|
print("🔄 Chargement du meilleur modèle...")
|
||||||
print(f"🔍 Recherche du modèle: {MODEL_NAME}")
|
best_model, best_run_id, best_encoder_name, best_img_size = load_model(
|
||||||
model, run_id, encoder_name, script_img_size = load_best_model_from_registry(MODEL_NAME)
|
experiment_name="OC Projet 9",
|
||||||
print("✅ Modèle téléchargé depuis MLflow")
|
metric="test_mean_iou",
|
||||||
|
top_n=1
|
||||||
|
)
|
||||||
|
|
||||||
print(f"✅ Modèle chargé du run {run_id}")
|
# Récupérer les infos détaillées du meilleur modèle
|
||||||
print(f"📐 Encoder: {encoder_name}")
|
best_detailed_info = get_model_detailed_info(best_run_id, mapping_config)
|
||||||
print(f"📐 Input shape: {script_img_size}")
|
|
||||||
print(f"🎨 Nombre de classes: {mapping_config['num_classes']}")
|
best_model_info = {
|
||||||
|
"name": "Meilleur modèle",
|
||||||
|
"run_id": best_run_id,
|
||||||
|
"encoder_name": best_encoder_name,
|
||||||
|
"input_size": best_img_size,
|
||||||
|
"rank": 1,
|
||||||
|
"architecture": best_detailed_info["architecture"],
|
||||||
|
"test_mean_iou": best_detailed_info["test_mean_iou"],
|
||||||
|
"test_accuracy": best_detailed_info["test_accuracy"],
|
||||||
|
"class_ious": best_detailed_info["class_ious"]
|
||||||
|
}
|
||||||
|
|
||||||
|
print("🔄 Chargement du deuxième meilleur modèle...")
|
||||||
|
second_best_model, second_run_id, second_encoder_name, second_img_size = load_model(
|
||||||
|
experiment_name="OC Projet 9",
|
||||||
|
metric="test_mean_iou",
|
||||||
|
top_n=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Récupérer les infos détaillées du deuxième modèle
|
||||||
|
second_detailed_info = get_model_detailed_info(second_run_id, mapping_config)
|
||||||
|
|
||||||
|
second_best_model_info = {
|
||||||
|
"name": "Deuxième modèle",
|
||||||
|
"run_id": second_run_id,
|
||||||
|
"encoder_name": second_encoder_name,
|
||||||
|
"input_size": second_img_size,
|
||||||
|
"rank": 2,
|
||||||
|
"architecture": second_detailed_info["architecture"],
|
||||||
|
"test_mean_iou": second_detailed_info["test_mean_iou"],
|
||||||
|
"test_accuracy": second_detailed_info["test_accuracy"],
|
||||||
|
"class_ious": second_detailed_info["class_ious"]
|
||||||
|
}
|
||||||
|
|
||||||
# Créer le dossier temporaire
|
|
||||||
os.makedirs(TEMP_UPLOAD_DIR, exist_ok=True)
|
os.makedirs(TEMP_UPLOAD_DIR, exist_ok=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Erreur lors de l'initialisation: {e}")
|
print(f"❌ Erreur lors de l'initialisation: {e}")
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
# Initialisation de l'API
|
# Initialisation de l'API
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Multi-Class Segmentation API",
|
title="Multi-Class Segmentation Comparison API",
|
||||||
description="API de segmentation sémantique pour véhicules autonomes",
|
description="API de comparaison de segmentation sémantique avec deux modèles",
|
||||||
version="1.0.0"
|
version="2.0.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ✅ AJOUT: Initialiser le modèle de manière plus robuste
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup_event():
|
async def startup_event():
|
||||||
"""Initialise le modèle au démarrage de l'API"""
|
|
||||||
try:
|
try:
|
||||||
initialize_model()
|
initialize_models()
|
||||||
print("🚀 API démarrée avec succès - Modèle chargé!")
|
print("🚀 API démarrée avec succès - Modèles chargés!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ ATTENTION: Impossible de charger le modèle au démarrage: {e}")
|
print(f"⚠️ ATTENTION: Impossible de charger les modèles au démarrage: {e}")
|
||||||
print("L'API démarre quand même mais les prédictions ne fonctionneront pas.")
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def root():
|
def root():
|
||||||
return {
|
return {
|
||||||
"message": "Multi-Class Segmentation API - Online",
|
"message": "Multi-Class Segmentation Comparison API - Online",
|
||||||
"model_loaded": model is not None,
|
"models_loaded": best_model is not None and second_best_model is not None,
|
||||||
"model_run_id": run_id,
|
"best_model": best_model_info if best_model else None,
|
||||||
"encoder": encoder_name,
|
"second_best_model": second_best_model_info if second_best_model else None,
|
||||||
"num_classes": mapping_config['num_classes'] if mapping_config else None
|
"num_classes": mapping_config['num_classes'] if mapping_config else None
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health_check():
|
def health_check():
|
||||||
"""Health check endpoint pour nginx"""
|
if best_model is None or second_best_model is None or mapping_config is None:
|
||||||
if model is None or mapping_config is None:
|
raise HTTPException(status_code=503, detail="Models not loaded")
|
||||||
raise HTTPException(status_code=503, detail="Model not loaded")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"model_loaded": True,
|
"models_loaded": True,
|
||||||
"timestamp": datetime.utcnow().isoformat()
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/models", response_model=dict)
|
@app.get("/models", response_model=dict)
|
||||||
def get_model_info():
|
def get_models_info():
|
||||||
"""Retourne les informations du modèle chargé"""
|
if best_model is None or second_best_model is None:
|
||||||
if model is None:
|
raise HTTPException(status_code=503, detail="Models not loaded")
|
||||||
raise HTTPException(status_code=503, detail="Model not loaded")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"model_name": MODEL_NAME,
|
"best_model": best_model_info,
|
||||||
"run_id": run_id,
|
"second_best_model": second_best_model_info,
|
||||||
"encoder": encoder_name,
|
|
||||||
"input_size": script_img_size,
|
|
||||||
"num_classes": mapping_config['num_classes'],
|
"num_classes": mapping_config['num_classes'],
|
||||||
"class_names": mapping_config['group_names'],
|
"class_names": mapping_config['group_names'],
|
||||||
"class_colors": mapping_config['group_colors'].tolist()
|
"class_colors": mapping_config['group_colors'].tolist()
|
||||||
|
|
@ -190,238 +259,308 @@ def get_model_info():
|
||||||
|
|
||||||
@app.get("/sample-images", response_model=AvailableImagesResponse)
|
@app.get("/sample-images", response_model=AvailableImagesResponse)
|
||||||
def get_sample_images():
|
def get_sample_images():
|
||||||
"""Retourne la liste des images d'exemple disponibles"""
|
|
||||||
try:
|
try:
|
||||||
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
|
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
|
||||||
mask_dir = Path(SAMPLE_IMAGES_DIR) / "mask"
|
mask_dir = Path(SAMPLE_IMAGES_DIR) / "mask"
|
||||||
|
|
||||||
if not original_dir.exists():
|
if not original_dir.exists():
|
||||||
raise HTTPException(status_code=404, detail="Sample images directory not found")
|
raise HTTPException(status_code=404, detail="Sample images directory not found")
|
||||||
|
|
||||||
images = []
|
images = []
|
||||||
for img_path in original_dir.glob("*leftImg8bit.png"):
|
for img_path in original_dir.glob("*leftImg8bit.png"):
|
||||||
# Construire le nom du masque correspondant
|
|
||||||
base_name = img_path.stem.replace("_leftImg8bit", "")
|
base_name = img_path.stem.replace("_leftImg8bit", "")
|
||||||
mask_name = f"{base_name}_gtFine_labelIds.png"
|
mask_name = f"{base_name}_gtFine_labelIds.png"
|
||||||
mask_path = mask_dir / mask_name
|
mask_path = mask_dir / mask_name
|
||||||
|
|
||||||
# Créer un nom d'affichage plus convivial
|
|
||||||
display_name = base_name.replace("_", " ").title()
|
display_name = base_name.replace("_", " ").title()
|
||||||
|
|
||||||
images.append(ImageInfo(
|
images.append(ImageInfo(
|
||||||
filename=img_path.name,
|
filename=img_path.name,
|
||||||
display_name=display_name,
|
display_name=display_name,
|
||||||
has_ground_truth=mask_path.exists()
|
has_ground_truth=mask_path.exists()
|
||||||
))
|
))
|
||||||
|
|
||||||
# Trier par nom pour un affichage cohérent
|
|
||||||
images.sort(key=lambda x: x.filename)
|
images.sort(key=lambda x: x.filename)
|
||||||
|
return AvailableImagesResponse(images=images, total_count=len(images))
|
||||||
return AvailableImagesResponse(
|
|
||||||
images=images,
|
|
||||||
total_count=len(images)
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Error listing sample images: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Error listing sample images: {str(e)}")
|
||||||
|
|
||||||
@app.get("/sample-image/{filename}")
|
@app.get("/sample-image/{filename}")
|
||||||
def get_sample_image(filename: str):
|
def get_sample_image(filename: str):
|
||||||
"""Sert une image d'exemple pour prévisualisation"""
|
|
||||||
try:
|
try:
|
||||||
print(f"🔍 DEBUG: Demande d'image: {filename}")
|
|
||||||
|
|
||||||
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
|
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
|
||||||
image_path = original_dir / filename
|
image_path = original_dir / filename
|
||||||
|
|
||||||
print(f"🔍 DEBUG: Chemin construit: {image_path}")
|
|
||||||
print(f"🔍 DEBUG: Fichier existe: {image_path.exists()}")
|
|
||||||
|
|
||||||
if not image_path.exists():
|
if not image_path.exists():
|
||||||
print(f"❌ DEBUG: Image non trouvée: {image_path}")
|
|
||||||
raise HTTPException(status_code=404, detail=f"Sample image {filename} not found")
|
raise HTTPException(status_code=404, detail=f"Sample image {filename} not found")
|
||||||
|
|
||||||
# Vérifier que c'est bien un fichier image attendu (sécurité)
|
|
||||||
if not filename.endswith('_leftImg8bit.png'):
|
if not filename.endswith('_leftImg8bit.png'):
|
||||||
print(f"❌ DEBUG: Format invalide: {filename}")
|
|
||||||
raise HTTPException(status_code=400, detail="Invalid image filename format")
|
raise HTTPException(status_code=400, detail="Invalid image filename format")
|
||||||
|
|
||||||
print(f"✅ DEBUG: Envoi de l'image: {filename}")
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
image_path,
|
image_path,
|
||||||
media_type="image/png",
|
media_type="image/png",
|
||||||
headers={"Content-Disposition": f"inline; filename={filename}"}
|
headers={"Content-Disposition": f"inline; filename={filename}"}
|
||||||
)
|
)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
# Re-raise les HTTPException pour qu'elles soient gérées correctement
|
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Erreur lors du service de l'image {filename}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
raise HTTPException(status_code=500, detail=f"Error serving image: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Error serving image: {str(e)}")
|
||||||
|
|
||||||
class SampleImageRequest(BaseModel):
|
class SampleImageRequest(BaseModel):
|
||||||
filename: str
|
filename: str
|
||||||
|
|
||||||
@app.post("/predict-sample", response_model=PredictionResult)
|
@app.post("/compare-sample", response_model=ComparisonResult)
|
||||||
def predict_sample_image(request: SampleImageRequest):
|
def compare_sample_image(request: SampleImageRequest):
|
||||||
"""Lance une prédiction sur une image d'exemple"""
|
if best_model is None or second_best_model is None or mapping_config is None:
|
||||||
if model is None or mapping_config is None:
|
raise HTTPException(status_code=503, detail="Models not loaded")
|
||||||
raise HTTPException(status_code=503, detail="Model not loaded")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Vérifier que l'image existe
|
|
||||||
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
|
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
|
||||||
image_path = original_dir / request.filename
|
image_path = original_dir / request.filename
|
||||||
|
|
||||||
if not image_path.exists():
|
if not image_path.exists():
|
||||||
raise HTTPException(status_code=404, detail=f"Sample image {request.filename} not found")
|
raise HTTPException(status_code=404, detail=f"Sample image {request.filename} not found")
|
||||||
|
|
||||||
# Construire le chemin du masque de vérité
|
|
||||||
base_name = image_path.stem.replace("_leftImg8bit", "")
|
base_name = image_path.stem.replace("_leftImg8bit", "")
|
||||||
mask_name = f"{base_name}_gtFine_labelIds.png"
|
mask_name = f"{base_name}_gtFine_labelIds.png"
|
||||||
mask_path = Path(SAMPLE_IMAGES_DIR) / "mask" / mask_name
|
mask_path = Path(SAMPLE_IMAGES_DIR) / "mask" / mask_name
|
||||||
|
|
||||||
mask_path_str = str(mask_path) if mask_path.exists() else None
|
mask_path_str = str(mask_path) if mask_path.exists() else None
|
||||||
|
|
||||||
print(f"🔮 Lancement de l'inférence sur {request.filename}")
|
# Inférence modèle 1 avec sa résolution spécifique
|
||||||
|
pred1, t1, fig1 = run_inference_and_visualize(
|
||||||
# Lancer l'inférence (sans affichage pour l'API)
|
|
||||||
predicted_mask, fig = run_inference_and_visualize(
|
|
||||||
image_path=str(image_path),
|
image_path=str(image_path),
|
||||||
mask_path=mask_path_str,
|
mask_path=mask_path_str,
|
||||||
model=model,
|
model=best_model,
|
||||||
encoder_name=encoder_name,
|
encoder_name=None, # Normalisé de la même manière
|
||||||
img_size=script_img_size,
|
img_size=best_model_info["input_size"], # Utilise la résolution du modèle
|
||||||
mapping_config=mapping_config,
|
mapping_config=mapping_config,
|
||||||
save_dir=None,
|
save_dir=None, show_stats=False, show_plot=False
|
||||||
show_stats=False,
|
)
|
||||||
show_plot=False # ← AJOUTÉ pour récupérer la figure
|
# Inférence modèle 2 avec sa résolution spécifique
|
||||||
|
pred2, t2, fig2 = run_inference_and_visualize(
|
||||||
|
image_path=str(image_path),
|
||||||
|
mask_path=mask_path_str,
|
||||||
|
model=second_best_model,
|
||||||
|
encoder_name=None, # Normalisé de la même manière
|
||||||
|
img_size=second_best_model_info["input_size"], # Utilise la résolution du modèle
|
||||||
|
mapping_config=mapping_config,
|
||||||
|
save_dir=None, show_stats=False, show_plot=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sérialiser la figure
|
# Créer une figure combinée avec les deux modèles
|
||||||
fig_bytes = pickle.dumps(fig)
|
# Nombre de colonnes : 3 si pas de GT, 4 si GT disponible
|
||||||
fig_b64 = base64.b64encode(fig_bytes).decode()
|
num_cols = 3 if not (mask_path_str and os.path.exists(mask_path_str)) else 4
|
||||||
|
fig, axes = plt.subplots(2, num_cols, figsize=(5*num_cols, 8))
|
||||||
|
|
||||||
# Calculer les statistiques
|
# Récupérer les images depuis les figures individuelles
|
||||||
unique_classes, counts = np.unique(predicted_mask, return_counts=True)
|
axes1 = fig1.get_axes()
|
||||||
total_pixels = predicted_mask.size
|
axes2 = fig2.get_axes()
|
||||||
|
|
||||||
|
# Ligne 1 : Modèle 1
|
||||||
|
for i in range(min(len(axes1), num_cols)):
|
||||||
|
ax_src = axes1[i]
|
||||||
|
ax_dst = axes[0, i]
|
||||||
|
|
||||||
|
# Copier toutes les images de l'axe source
|
||||||
|
for img_obj in ax_src.get_images():
|
||||||
|
ax_dst.imshow(
|
||||||
|
img_obj.get_array(),
|
||||||
|
cmap=img_obj.get_cmap(),
|
||||||
|
alpha=img_obj.get_alpha(),
|
||||||
|
vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
|
||||||
|
vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copier le titre avec nom du modèle
|
||||||
|
original_title = ax_src.get_title()
|
||||||
|
if i == 0:
|
||||||
|
new_title = "Image originale"
|
||||||
|
elif "Prédit" in original_title:
|
||||||
|
new_title = f"Prédiction {best_model_info['architecture']} - {best_model_info['encoder_name']}"
|
||||||
|
else:
|
||||||
|
new_title = original_title
|
||||||
|
|
||||||
|
ax_dst.set_title(new_title)
|
||||||
|
ax_dst.axis('off')
|
||||||
|
|
||||||
|
# Ligne 2 : Modèle 2
|
||||||
|
for i in range(min(len(axes2), num_cols)):
|
||||||
|
ax_src = axes2[i]
|
||||||
|
ax_dst = axes[1, i]
|
||||||
|
|
||||||
|
# Copier toutes les images de l'axe source
|
||||||
|
for img_obj in ax_src.get_images():
|
||||||
|
ax_dst.imshow(
|
||||||
|
img_obj.get_array(),
|
||||||
|
cmap=img_obj.get_cmap(),
|
||||||
|
alpha=img_obj.get_alpha(),
|
||||||
|
vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
|
||||||
|
vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copier le titre avec nom du modèle
|
||||||
|
original_title = ax_src.get_title()
|
||||||
|
if i == 0:
|
||||||
|
new_title = "Image originale"
|
||||||
|
elif "Prédit" in original_title:
|
||||||
|
new_title = f"Prédiction {second_best_model_info['architecture']} - {second_best_model_info['encoder_name']}"
|
||||||
|
else:
|
||||||
|
new_title = original_title
|
||||||
|
|
||||||
|
ax_dst.set_title(new_title)
|
||||||
|
ax_dst.axis('off')
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
# Fermer les figures individuelles pour libérer la mémoire
|
||||||
|
plt.close(fig1)
|
||||||
|
plt.close(fig2)
|
||||||
|
fig_b64 = base64.b64encode(pickle.dumps(fig)).decode()
|
||||||
|
# Stats
|
||||||
|
def calc(mask):
|
||||||
|
uc, cnts = np.unique(mask, return_counts=True)
|
||||||
stats = {}
|
stats = {}
|
||||||
for class_id, count in zip(unique_classes, counts):
|
for cid, c in zip(uc, cnts):
|
||||||
if class_id < len(mapping_config['group_names']):
|
if cid < len(mapping_config['group_names']):
|
||||||
percentage = (count / total_pixels) * 100
|
stats[mapping_config['group_names'][cid]] = {
|
||||||
stats[mapping_config['group_names'][class_id]] = {
|
"pixels": int(c),
|
||||||
"pixels": int(count),
|
"percentage": round((c/mask.size)*100, 1)
|
||||||
"percentage": round(percentage, 1)
|
|
||||||
}
|
}
|
||||||
|
return stats
|
||||||
return PredictionResult(
|
stats1, stats2 = calc(pred1), calc(pred2)
|
||||||
success=True,
|
faster = best_model_info['encoder_name'] if t1 < t2 else second_best_model_info['encoder_name']
|
||||||
message="Prediction completed successfully",
|
speedup = round(max(t1, t2) / min(t1, t2), 2)
|
||||||
image_path=str(image_path),
|
speed_comp = {"faster_model": faster, "speedup_ratio": speedup, "time_difference_ms": abs(t1-t2)*1000}
|
||||||
mask_path=mask_path_str,
|
return ComparisonResult(
|
||||||
prediction_stats=stats,
|
success=True, message="Comparison completed successfully",
|
||||||
ground_truth_available=mask_path.exists(),
|
model1_info=ModelInfo(**best_model_info), model2_info=ModelInfo(**second_best_model_info),
|
||||||
|
image_path=str(image_path), mask_path=mask_path_str,
|
||||||
|
model1_stats=stats1, model2_stats=stats2,
|
||||||
|
model1_inference_time=t1, model2_inference_time=t2,
|
||||||
|
speed_comparison=speed_comp, ground_truth_available=mask_path.exists(),
|
||||||
figure_data=fig_b64
|
figure_data=fig_b64
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Erreur lors de la prédiction: {e}")
|
raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
raise HTTPException(status_code=500, detail=f"Prediction error: {str(e)}")
|
|
||||||
|
|
||||||
@app.post("/predict-upload", response_model=PredictionResult)
|
@app.post("/compare-upload", response_model=ComparisonResult)
|
||||||
def predict_uploaded_image(file: UploadFile = File(...)):
|
def compare_uploaded_image(file: UploadFile = File(...)):
|
||||||
"""Lance une prédiction sur une image uploadée"""
|
if best_model is None or second_best_model is None or mapping_config is None:
|
||||||
if model is None or mapping_config is None:
|
raise HTTPException(status_code=503, detail="Models not loaded")
|
||||||
raise HTTPException(status_code=503, detail="Model not loaded")
|
|
||||||
|
|
||||||
# Vérifier le type de fichier
|
|
||||||
allowed_types = ["image/jpeg", "image/jpg", "image/png"]
|
allowed_types = ["image/jpeg", "image/jpg", "image/png"]
|
||||||
if file.content_type not in allowed_types:
|
if file.content_type not in allowed_types:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=400, detail=f"Invalid file type. Allowed: {', '.join(allowed_types)}")
|
||||||
status_code=400,
|
|
||||||
detail=f"Invalid file type. Allowed: {', '.join(allowed_types)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
temp_file_path = None
|
temp_file_path = None
|
||||||
try:
|
try:
|
||||||
# Créer un fichier temporaire
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
temp_filename = f"upload_{timestamp}_{file.filename}"
|
temp_filename = f"upload_{timestamp}_{file.filename}"
|
||||||
temp_file_path = os.path.join(TEMP_UPLOAD_DIR, temp_filename)
|
temp_file_path = os.path.join(TEMP_UPLOAD_DIR, temp_filename)
|
||||||
|
|
||||||
# Sauvegarder le fichier uploadé
|
|
||||||
with open(temp_file_path, "wb") as buffer:
|
with open(temp_file_path, "wb") as buffer:
|
||||||
shutil.copyfileobj(file.file, buffer)
|
shutil.copyfileobj(file.file, buffer)
|
||||||
|
# Inférences avec résolutions spécifiques
|
||||||
print(f"🔮 Lancement de l'inférence sur {file.filename}")
|
pred1, t1, fig1 = run_inference_and_visualize(
|
||||||
|
image_path=temp_file_path, mask_path=None,
|
||||||
# Lancer l'inférence (sans masque de vérité)
|
model=best_model, encoder_name=None, # Normalisé de la même manière
|
||||||
predicted_mask, fig = run_inference_and_visualize(
|
img_size=best_model_info["input_size"], mapping_config=mapping_config,
|
||||||
image_path=temp_file_path,
|
save_dir=None, show_stats=False, show_plot=False,
|
||||||
mask_path=None, # Pas de masque de vérité pour les uploads
|
)
|
||||||
model=model,
|
pred2, t2, fig2 = run_inference_and_visualize(
|
||||||
encoder_name=encoder_name,
|
image_path=temp_file_path, mask_path=None,
|
||||||
img_size=script_img_size,
|
model=second_best_model, encoder_name=None, # Normalisé de la même manière
|
||||||
mapping_config=mapping_config,
|
img_size=second_best_model_info["input_size"], mapping_config=mapping_config,
|
||||||
save_dir=None,
|
save_dir=None, show_stats=False, show_plot=False,
|
||||||
show_stats=False,
|
|
||||||
show_plot=False # ← AJOUTÉ pour récupérer la figure
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sérialiser la figure
|
# Figure combinée pour uploads (pas de GT, donc 2 colonnes seulement)
|
||||||
fig_bytes = pickle.dumps(fig)
|
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
|
||||||
fig_b64 = base64.b64encode(fig_bytes).decode()
|
|
||||||
|
|
||||||
# Calculer les statistiques
|
# Récupérer les images depuis les figures individuelles
|
||||||
unique_classes, counts = np.unique(predicted_mask, return_counts=True)
|
axes1 = fig1.get_axes()
|
||||||
total_pixels = predicted_mask.size
|
axes2 = fig2.get_axes()
|
||||||
|
|
||||||
|
# Ligne 1 : Modèle 1 (Image originale + Prédiction)
|
||||||
|
for i in range(min(len(axes1), 2)):
|
||||||
|
ax_src = axes1[i]
|
||||||
|
ax_dst = axes[0, i]
|
||||||
|
|
||||||
|
# Copier toutes les images de l'axe source
|
||||||
|
for img_obj in ax_src.get_images():
|
||||||
|
ax_dst.imshow(
|
||||||
|
img_obj.get_array(),
|
||||||
|
cmap=img_obj.get_cmap(),
|
||||||
|
alpha=img_obj.get_alpha(),
|
||||||
|
vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
|
||||||
|
vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copier le titre avec nom du modèle
|
||||||
|
original_title = ax_src.get_title()
|
||||||
|
if i == 0:
|
||||||
|
new_title = "Image originale"
|
||||||
|
else:
|
||||||
|
new_title = f"Prédiction {best_model_info['architecture']} - {best_model_info['encoder_name']}"
|
||||||
|
|
||||||
|
ax_dst.set_title(new_title)
|
||||||
|
ax_dst.axis('off')
|
||||||
|
|
||||||
|
# Ligne 2 : Modèle 2 (Image originale + Prédiction)
|
||||||
|
for i in range(min(len(axes2), 2)):
|
||||||
|
ax_src = axes2[i]
|
||||||
|
ax_dst = axes[1, i]
|
||||||
|
|
||||||
|
# Copier toutes les images de l'axe source
|
||||||
|
for img_obj in ax_src.get_images():
|
||||||
|
ax_dst.imshow(
|
||||||
|
img_obj.get_array(),
|
||||||
|
cmap=img_obj.get_cmap(),
|
||||||
|
alpha=img_obj.get_alpha(),
|
||||||
|
vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None,
|
||||||
|
vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copier le titre avec nom du modèle
|
||||||
|
original_title = ax_src.get_title()
|
||||||
|
if i == 0:
|
||||||
|
new_title = "Image originale"
|
||||||
|
else:
|
||||||
|
new_title = f"Prédiction {second_best_model_info['architecture']} - {second_best_model_info['encoder_name']}"
|
||||||
|
|
||||||
|
ax_dst.set_title(new_title)
|
||||||
|
ax_dst.axis('off')
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
# Fermer les figures individuelles pour libérer la mémoire
|
||||||
|
plt.close(fig1)
|
||||||
|
plt.close(fig2)
|
||||||
|
fig_b64 = base64.b64encode(pickle.dumps(fig)).decode()
|
||||||
|
# Stats
|
||||||
|
def calc(mask):
|
||||||
|
uc, cnts = np.unique(mask, return_counts=True)
|
||||||
stats = {}
|
stats = {}
|
||||||
for class_id, count in zip(unique_classes, counts):
|
for cid, c in zip(uc, cnts):
|
||||||
if class_id < len(mapping_config['group_names']):
|
if cid < len(mapping_config['group_names']):
|
||||||
percentage = (count / total_pixels) * 100
|
stats[mapping_config['group_names'][cid]] = {
|
||||||
stats[mapping_config['group_names'][class_id]] = {
|
"pixels": int(c),
|
||||||
"pixels": int(count),
|
"percentage": round((c/mask.size)*100, 1)
|
||||||
"percentage": round(percentage, 1)
|
|
||||||
}
|
}
|
||||||
|
return stats
|
||||||
return PredictionResult(
|
stats1, stats2 = calc(pred1), calc(pred2)
|
||||||
success=True,
|
faster = best_model_info['encoder_name'] if t1 < t2 else second_best_model_info['encoder_name']
|
||||||
message="Prediction completed successfully",
|
speedup = round(max(t1, t2) / min(t1, t2), 2)
|
||||||
|
speed_comp = {"faster_model": faster, "speedup_ratio": speedup, "time_difference_ms": abs(t1-t2)*1000}
|
||||||
|
return ComparisonResult(
|
||||||
|
success=True, message="Comparison completed successfully",
|
||||||
|
model1_info=ModelInfo(**best_model_info), model2_info=ModelInfo(**second_best_model_info),
|
||||||
image_path=temp_file_path,
|
image_path=temp_file_path,
|
||||||
prediction_stats=stats,
|
model1_stats=stats1, model2_stats=stats2,
|
||||||
ground_truth_available=False,
|
model1_inference_time=t1, model2_inference_time=t2,
|
||||||
|
speed_comparison=speed_comp, ground_truth_available=False,
|
||||||
figure_data=fig_b64
|
figure_data=fig_b64
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Erreur lors de la prédiction: {e}")
|
raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
raise HTTPException(status_code=500, detail=f"Prediction error: {str(e)}")
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Nettoyer le fichier temporaire
|
|
||||||
if temp_file_path and os.path.exists(temp_file_path):
|
if temp_file_path and os.path.exists(temp_file_path):
|
||||||
try:
|
try: os.remove(temp_file_path)
|
||||||
os.remove(temp_file_path)
|
except Exception: pass
|
||||||
print(f"🗑️ Fichier temporaire supprimé: {temp_filename}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Impossible de supprimer le fichier temporaire: {e}")
|
|
||||||
|
|
||||||
@app.get("/metrics")
|
@app.get("/metrics")
|
||||||
def get_metrics():
|
def get_metrics():
|
||||||
"""Endpoint de métriques pour monitoring"""
|
|
||||||
return {
|
return {
|
||||||
"model_loaded": model is not None,
|
"models_loaded": best_model is not None and second_best_model is not None,
|
||||||
"model_name": MODEL_NAME,
|
"best_model_info": best_model_info,
|
||||||
"run_id": run_id,
|
"second_best_model_info": second_best_model_info,
|
||||||
"num_classes": mapping_config['num_classes'] if mapping_config else None,
|
"num_classes": mapping_config['num_classes'] if mapping_config else None,
|
||||||
"timestamp": datetime.utcnow().isoformat()
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
}
|
}
|
||||||
|
|
@ -8,37 +8,44 @@ uvicorn==0.35.0
|
||||||
starlette==0.46.2
|
starlette==0.46.2
|
||||||
pydantic==2.11.7
|
pydantic==2.11.7
|
||||||
pydantic_core==2.33.2
|
pydantic_core==2.33.2
|
||||||
|
python-multipart==0.0.20 # uploads FastAPI
|
||||||
|
|
||||||
# ===== STREAMLIT =====
|
# ===== STREAMLIT =====
|
||||||
streamlit==1.46.1
|
streamlit==1.46.1
|
||||||
altair==5.5.0 # Utilisé par Streamlit pour les graphiques
|
altair==5.5.0 # utilisé par Streamlit
|
||||||
blinker==1.9.0 # Streamlit dependency
|
blinker==1.9.0 # dépendance Streamlit
|
||||||
|
|
||||||
# ===== HTTP REQUESTS =====
|
# ===== HTTP CLIENTS =====
|
||||||
requests==2.32.4
|
requests==2.32.4
|
||||||
httpx==0.28.1 # Modern HTTP client (utilisé par FastAPI)
|
httpx==0.28.1 # utilisé en interne par FastAPI
|
||||||
httpcore==1.0.9
|
httpcore==1.0.9
|
||||||
h11==0.16.0
|
h11==0.16.0
|
||||||
|
|
||||||
# ===== MACHINE LEARNING CORE =====
|
# ===== MACHINE LEARNING CORE (PyTorch) =====
|
||||||
tensorflow==2.19.0
|
torch==2.8.0 # version stable PyTorch
|
||||||
keras==3.10.0
|
torchvision==0.23.0 # utile pour les modèles pré-entraînés
|
||||||
numpy>=1.24.0,<2.0.0
|
torchaudio==2.8.0 # optionnel (mais recommandé pour compatibilité PyTorch)
|
||||||
|
segmentation-models-pytorch==0.5.0
|
||||||
|
albumentations==2.0.8
|
||||||
|
# version PyTorch de ToTensor
|
||||||
|
albumentations[imgaug,opencv]
|
||||||
|
|
||||||
# Note: tensorflow-addons n'est plus nécessaire avec TensorFlow 2.19+
|
numpy>=1.24.0,<2.0.0
|
||||||
# Les fonctionnalités sont maintenant intégrées dans TensorFlow core
|
scipy==1.16.0
|
||||||
|
scikit-learn==1.7.0
|
||||||
|
|
||||||
# ===== COMPUTER VISION =====
|
# ===== COMPUTER VISION =====
|
||||||
pillow==11.3.0
|
pillow==11.3.0
|
||||||
opencv-python-headless==4.9.0.80 # Pas dans votre freeze, mais nécessaire pour Docker
|
opencv-python-headless==4.9.0.80
|
||||||
scikit-image==0.25.2
|
scikit-image==0.25.2
|
||||||
imageio==2.37.0
|
imageio==2.37.0
|
||||||
|
|
||||||
# ===== DATA SCIENCE =====
|
# ===== DATA SCIENCE =====
|
||||||
pandas==2.3.0
|
pandas==2.3.0
|
||||||
matplotlib==3.10.3
|
matplotlib==3.10.3
|
||||||
scipy==1.16.0
|
networkx==3.5
|
||||||
scikit-learn==1.7.0
|
tifffile==2025.6.11
|
||||||
|
lazy_loader==0.4
|
||||||
|
|
||||||
# ===== MLFLOW & MODEL REGISTRY =====
|
# ===== MLFLOW & MODEL REGISTRY =====
|
||||||
mlflow==3.2.0
|
mlflow==3.2.0
|
||||||
|
|
@ -50,34 +57,7 @@ boto3==1.39.2
|
||||||
botocore==1.39.2
|
botocore==1.39.2
|
||||||
s3transfer==0.13.0
|
s3transfer==0.13.0
|
||||||
|
|
||||||
# ===== SEGMENTATION MODELS =====
|
# ===== UTILITIES =====
|
||||||
# Option 1: Version figée par commit (RECOMMANDÉE pour production)
|
|
||||||
segmentation_models @ git+https://github.com/qubvel/segmentation_models.git@5d24bbfb28af6134e25e2c0b79e7727f6c0491d0
|
|
||||||
|
|
||||||
# Option 2: Si vous préférez une version PyPI (décommentez et commentez la ligne du dessus)
|
|
||||||
# segmentation-models==1.0.1
|
|
||||||
|
|
||||||
# ===== SUPPORTING LIBRARIES =====
|
|
||||||
# TensorFlow ecosystem
|
|
||||||
tensorboard==2.19.0
|
|
||||||
tensorboard-data-server==0.7.2
|
|
||||||
protobuf==3.20.3
|
|
||||||
flatbuffers==25.2.10
|
|
||||||
h5py==3.14.0
|
|
||||||
astunparse==1.6.3
|
|
||||||
gast==0.6.0
|
|
||||||
google-pasta==0.2.0
|
|
||||||
keras-applications==1.0.8
|
|
||||||
opt_einsum==3.4.0
|
|
||||||
termcolor==2.5.0
|
|
||||||
wrapt==1.17.2
|
|
||||||
|
|
||||||
# Data processing
|
|
||||||
networkx==3.5
|
|
||||||
tifffile==2025.6.11
|
|
||||||
lazy_loader==0.4
|
|
||||||
|
|
||||||
# Utilities
|
|
||||||
click==8.2.1
|
click==8.2.1
|
||||||
tqdm==4.67.1
|
tqdm==4.67.1
|
||||||
joblib==1.5.1
|
joblib==1.5.1
|
||||||
|
|
@ -85,57 +65,6 @@ python-dateutil
|
||||||
packaging
|
packaging
|
||||||
typing_extensions==4.14.0
|
typing_extensions==4.14.0
|
||||||
|
|
||||||
# File handling
|
# ===== MATPLOTLIB ADDONS =====
|
||||||
python-multipart==0.0.20 # Pour FastAPI file uploads
|
cycler==0.12.1
|
||||||
|
|
||||||
# ===== AJOUTS SPÉCIFIQUES POUR UTILS/UTILS.PY =====
|
|
||||||
# Pour la gestion des couleurs et légendes matplotlib
|
|
||||||
cycler==0.12.1 # Pour matplotlib customization
|
|
||||||
|
|
||||||
# Font handling pour matplotlib (nécessaire dans Docker)
|
|
||||||
fonttools==4.58.5
|
fonttools==4.58.5
|
||||||
|
|
||||||
# ===== REMOVED FROM ORIGINAL =====
|
|
||||||
# Les packages suivants ont été supprimés car non utilisés dans votre app:
|
|
||||||
# - awsebcli (AWS CLI - pas nécessaire dans le container)
|
|
||||||
# - blessed, fabric, invoke (outils dev)
|
|
||||||
# - jupyter, ipython, spyder (dev environment)
|
|
||||||
# - nltk (pas d'NLP dans votre app)
|
|
||||||
# - pytest (tests - à garder seulement pour dev)
|
|
||||||
# - flask, gunicorn (vous utilisez FastAPI+uvicorn)
|
|
||||||
# - docker (pas besoin dans le container)
|
|
||||||
# - git* packages (dev tools)
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# VERSION ALTERNATIVE - PLUS MINIMALISTE
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# Si vous voulez une version encore plus légère, voici les STRICTEMENT nécessaires:
|
|
||||||
# fastapi==0.115.14
|
|
||||||
# uvicorn==0.35.0
|
|
||||||
# streamlit==1.46.1
|
|
||||||
# tensorflow==2.19.0
|
|
||||||
# numpy==2.1.3
|
|
||||||
# pillow==11.3.0
|
|
||||||
# matplotlib==3.10.3
|
|
||||||
# pandas==2.3.0
|
|
||||||
# requests==2.32.4
|
|
||||||
# mlflow==3.2.0
|
|
||||||
# boto3==1.39.2
|
|
||||||
# segmentation_models @ git+https://github.com/qubvel/segmentation_models.git@5d24bbfb28af6134e25e2c0b79e7727f6c0491d0
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# NOTES SUR SEGMENTATION_MODELS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# OPTION A: Git avec commit hash (ACTUEL - RECOMMANDÉ)
|
|
||||||
# Avantages: Version exacte, pas de surprises
|
|
||||||
# Inconvénients: Nécessite git dans le container de build
|
|
||||||
|
|
||||||
# OPTION B: Version PyPI officielle
|
|
||||||
# segmentation-models==1.0.1
|
|
||||||
# Avantages: Plus simple, pas besoin de git
|
|
||||||
# Inconvénients: Peut être différent de votre version dev
|
|
||||||
|
|
||||||
# OPTION C: Version locale (si vous avez des modifications)
|
|
||||||
# Copier le package dans le container et faire pip install -e .
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -606,13 +606,13 @@ tokenizers==0.21.4
|
||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
tomlkit==0.13.3
|
tomlkit==0.13.3
|
||||||
toolz==0.12.1
|
toolz==0.12.1
|
||||||
torch==2.8.0+cu126
|
torch==2.8.0
|
||||||
torchao==0.10.0
|
torchao==0.10.0
|
||||||
torchaudio==2.8.0+cu126
|
torchaudio==2.8.0
|
||||||
torchdata==0.11.0
|
torchdata==0.11.0
|
||||||
torchsummary==1.5.1
|
torchsummary==1.5.1
|
||||||
torchtune==0.6.1
|
torchtune==0.6.1
|
||||||
torchvision==0.23.0+cu126
|
torchvision==0.23.0
|
||||||
tornado==6.4.2
|
tornado==6.4.2
|
||||||
tqdm==4.67.1
|
tqdm==4.67.1
|
||||||
traitlets==5.7.1
|
traitlets==5.7.1
|
||||||
|
|
|
||||||
214
utils/utils.py
214
utils/utils.py
|
|
@ -3,7 +3,6 @@ import json
|
||||||
import tempfile
|
import tempfile
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
import torch.nn.functional as F
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import segmentation_models_pytorch as smp
|
import segmentation_models_pytorch as smp
|
||||||
import mlflow
|
import mlflow
|
||||||
|
|
@ -16,8 +15,8 @@ import matplotlib.patches as mpatches
|
||||||
from matplotlib.colors import ListedColormap
|
from matplotlib.colors import ListedColormap
|
||||||
|
|
||||||
# Preprocessing/Images
|
# Preprocessing/Images
|
||||||
from PIL import Image
|
import albumentations as A
|
||||||
import torchvision.transforms as transforms
|
from albumentations.pytorch import ToTensorV2
|
||||||
|
|
||||||
|
|
||||||
def load_cityscapes_config(config_path="../cityscapes_config.json", verbose=True):
|
def load_cityscapes_config(config_path="../cityscapes_config.json", verbose=True):
|
||||||
|
|
@ -103,48 +102,12 @@ def colorize_mask(mask, group_colors):
|
||||||
|
|
||||||
return colored_mask
|
return colored_mask
|
||||||
|
|
||||||
# def get_preprocessing_fn(encoder_name=None):
|
|
||||||
# """
|
|
||||||
# Retourne la fonction de prétraitement adaptée au backbone utilisé.
|
|
||||||
# ADAPTÉ PYTORCH : utilise smp.encoders.get_preprocessing_params
|
|
||||||
# """
|
|
||||||
# if encoder_name is None:
|
|
||||||
# print("ℹ️ Modèle vanilla - normalisation simple")
|
|
||||||
# return lambda x: x / 255.0
|
|
||||||
# else:
|
|
||||||
# print(f"ℹ️ Préprocessing du backbone {encoder_name}")
|
|
||||||
# try:
|
|
||||||
# # PyTorch segmentation_models utilise get_preprocessing_params
|
|
||||||
# preprocess_params = smp.encoders.get_preprocessing_params(encoder_name)
|
|
||||||
# mean = preprocess_params['mean']
|
|
||||||
# std = preprocess_params['std']
|
|
||||||
|
|
||||||
# def preprocess_fn(x):
|
|
||||||
# # x doit être dans [0, 1] puis normalisé
|
|
||||||
# x = x / 255.0 if x.max() > 1.0 else x
|
|
||||||
# # Normalisation ImageNet ou spécifique au backbone
|
|
||||||
# if isinstance(x, torch.Tensor):
|
|
||||||
# for i in range(3): # RGB
|
|
||||||
# x[:, i, :, :] = (x[:, i, :, :] - mean[i]) / std[i]
|
|
||||||
# else: # numpy
|
|
||||||
# for i in range(3):
|
|
||||||
# x[:, :, i] = (x[:, :, i] - mean[i]) / std[i]
|
|
||||||
# return x
|
|
||||||
|
|
||||||
# return preprocess_fn
|
|
||||||
# except Exception as e:
|
|
||||||
# print(f"⚠️ Erreur preprocessing {encoder_name}: {e}, fallback simple")
|
|
||||||
# return lambda x: x / 255.0
|
|
||||||
|
|
||||||
def get_preprocessing_fn(img_size=(512, 512), encoder=None):
|
def get_preprocessing_fn(img_size=(512, 512), encoder=None):
|
||||||
"""
|
"""
|
||||||
Retourne une fonction qui prétraite à la fois les images et les masques
|
Retourne une fonction qui prétraite à la fois les images et les masques
|
||||||
pour l'inférence, en reproduisant exactement le pipeline CityscapesDataset.
|
pour l'inférence, en reproduisant exactement le pipeline CityscapesDataset.
|
||||||
"""
|
"""
|
||||||
import albumentations as A
|
|
||||||
from albumentations.pytorch import ToTensorV2
|
|
||||||
import cv2
|
|
||||||
import torch
|
|
||||||
|
|
||||||
if encoder is None:
|
if encoder is None:
|
||||||
mean, std = [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]
|
mean, std = [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]
|
||||||
|
|
@ -205,173 +168,9 @@ def preprocess_image_and_mask(image_path, mask_path=None, img_size=(512, 512),
|
||||||
|
|
||||||
return img_preprocessed, img_display, mask_preprocessed
|
return img_preprocessed, img_display, mask_preprocessed
|
||||||
|
|
||||||
|
|
||||||
# def preprocess_image_and_mask(image_path, mask_path=None, img_size=(512, 512), # ← CHANGÉ: 224→512
|
|
||||||
# encoder_name=None, id_to_group_mapping=None):
|
|
||||||
# """
|
|
||||||
# Préprocesse une image (et optionnellement un masque) avec le même pipeline que l'entraînement
|
|
||||||
# ADAPTÉ PYTORCH : Utilise PIL + torch au lieu de tf
|
|
||||||
# """
|
|
||||||
|
|
||||||
# print(f"🔄 Préprocessing de l'image: {os.path.basename(image_path)}")
|
|
||||||
|
|
||||||
# # Chargement et preprocessing de l'image
|
|
||||||
# img_pil = Image.open(image_path).convert('RGB')
|
|
||||||
# img_original = img_pil.resize(img_size, Image.BILINEAR)
|
|
||||||
|
|
||||||
# # Conversion en tensor et normalisation
|
|
||||||
# img_tensor = transforms.ToTensor()(img_original) # [3, H, W] dans [0, 1]
|
|
||||||
|
|
||||||
# # Appliquer le preprocessing du backbone
|
|
||||||
# preprocessing_fn = get_preprocessing_fn(encoder_name)
|
|
||||||
# if encoder_name is not None:
|
|
||||||
# # Pour les backbones pré-entraînés, on applique leur normalisation
|
|
||||||
# img_preprocessed = preprocessing_fn(img_tensor.permute(1, 2, 0).numpy())
|
|
||||||
# img_preprocessed = torch.from_numpy(img_preprocessed).permute(2, 0, 1).float()
|
|
||||||
# else:
|
|
||||||
# # Simple normalisation [0, 1]
|
|
||||||
# img_preprocessed = img_tensor.float()
|
|
||||||
|
|
||||||
# # Preprocessing du masque si fourni
|
|
||||||
# mask_preprocessed = None
|
|
||||||
# if mask_path and os.path.exists(mask_path):
|
|
||||||
# print(f"🔄 Préprocessing du masque: {os.path.basename(mask_path)}")
|
|
||||||
|
|
||||||
# mask_pil = Image.open(mask_path).convert('L') # Grayscale
|
|
||||||
# mask_resized = mask_pil.resize(img_size, Image.NEAREST)
|
|
||||||
# mask_tensor = torch.from_numpy(np.array(mask_resized)).long()
|
|
||||||
|
|
||||||
# print(f" Valeurs uniques avant mapping: {torch.unique(mask_tensor)[:10]}...")
|
|
||||||
|
|
||||||
# # Appliquer le mapping si fourni
|
|
||||||
# if id_to_group_mapping is not None:
|
|
||||||
# mask_tensor = map_mask_ids(mask_tensor, id_to_group_mapping)
|
|
||||||
# print(f" Valeurs uniques après mapping: {torch.unique(mask_tensor)}")
|
|
||||||
|
|
||||||
# mask_preprocessed = mask_tensor
|
|
||||||
|
|
||||||
# # img_original en numpy pour affichage
|
|
||||||
# img_display = np.array(img_original)
|
|
||||||
|
|
||||||
# return img_preprocessed, img_display, mask_preprocessed
|
|
||||||
|
|
||||||
# def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config,
|
|
||||||
# mask_path=None, save_dir=None, show_stats=True):
|
|
||||||
# """
|
|
||||||
# ADAPTÉ PYTORCH : Inférence avec modèle PyTorch
|
|
||||||
# Returns: (predicted_mask, inference_time_seconds)
|
|
||||||
# """
|
|
||||||
|
|
||||||
# print(f"=== INFÉRENCE PYTORCH SUR {os.path.basename(image_path)} ===")
|
|
||||||
|
|
||||||
# img_preprocessed, img_display, mask_gt = preprocess_image_and_mask(
|
|
||||||
# image_path=image_path,
|
|
||||||
# mask_path=mask_path,
|
|
||||||
# img_size=img_size or (512, 512),
|
|
||||||
# encoder_name=encoder_name,
|
|
||||||
# id_to_group_mapping=mapping_config.get('id_to_group')
|
|
||||||
# )
|
|
||||||
|
|
||||||
# print("🔮 Exécution de l'inférence PyTorch...")
|
|
||||||
# model.eval()
|
|
||||||
# device = next(model.parameters()).device
|
|
||||||
|
|
||||||
# with torch.no_grad():
|
|
||||||
# # Ajouter dimension batch
|
|
||||||
# img_batch = img_preprocessed.unsqueeze(0).to(device) # [1, 3, H, W]
|
|
||||||
|
|
||||||
# # ⏱️ MESURE DU TEMPS D'INFÉRENCE
|
|
||||||
# start_time = time.time()
|
|
||||||
# predictions = model(img_batch) # [1, num_classes, H, W]
|
|
||||||
# end_time = time.time()
|
|
||||||
|
|
||||||
# inference_time = end_time - start_time
|
|
||||||
|
|
||||||
# predicted_mask = torch.argmax(predictions[0], dim=0).cpu().numpy() # [H, W]
|
|
||||||
|
|
||||||
# print(f"✅ Inférence terminée - Shape: {predicted_mask.shape}")
|
|
||||||
# print(f"⏱️ Temps d'inférence: {inference_time*1000:.1f} ms ({inference_time:.4f} s)")
|
|
||||||
|
|
||||||
# # Visualisation
|
|
||||||
# num_plots = 2 if mask_gt is None else 4
|
|
||||||
# fig, axes = plt.subplots(1, num_plots, figsize=(5*num_plots, 5))
|
|
||||||
# if num_plots == 1:
|
|
||||||
# axes = [axes]
|
|
||||||
|
|
||||||
# axes[0].imshow(img_display)
|
|
||||||
# axes[0].set_title("Image Originale")
|
|
||||||
# axes[0].axis('off')
|
|
||||||
|
|
||||||
# predicted_colored = colorize_mask(predicted_mask, mapping_config['group_colors'])
|
|
||||||
# axes[1].imshow(predicted_colored)
|
|
||||||
# axes[1].set_title("Masque Prédit")
|
|
||||||
# axes[1].axis('off')
|
|
||||||
|
|
||||||
# if mask_gt is not None:
|
|
||||||
# mask_gt_np = mask_gt.numpy() if isinstance(mask_gt, torch.Tensor) else mask_gt
|
|
||||||
# gt_colored = colorize_mask(mask_gt_np, mapping_config['group_colors'])
|
|
||||||
# axes[2].imshow(gt_colored)
|
|
||||||
# axes[2].set_title("Masque de Vérité")
|
|
||||||
# axes[2].axis('off')
|
|
||||||
|
|
||||||
# # Visualisation des erreurs
|
|
||||||
# diff_mask = (mask_gt_np != predicted_mask).astype(float)
|
|
||||||
# axes[3].imshow(img_display)
|
|
||||||
# axes[3].imshow(diff_mask, alpha=0.5, cmap='Reds')
|
|
||||||
# axes[3].set_title("Erreurs (en rouge)")
|
|
||||||
# axes[3].axis('off')
|
|
||||||
|
|
||||||
# colors_normalized = mapping_config['group_colors'] / 255.0
|
|
||||||
# custom_cmap = ListedColormap(colors_normalized)
|
|
||||||
|
|
||||||
# legend_elements = []
|
|
||||||
# for i, (name, color) in enumerate(zip(mapping_config['group_names'], colors_normalized)):
|
|
||||||
# legend_elements.append(mpatches.Patch(color=color, label=f'{i}: {name}'))
|
|
||||||
|
|
||||||
# fig.legend(handles=legend_elements,
|
|
||||||
# loc='center left',
|
|
||||||
# bbox_to_anchor=(1.01, 0.5),
|
|
||||||
# fontsize=9,
|
|
||||||
# frameon=True,
|
|
||||||
# fancybox=True,
|
|
||||||
# shadow=True)
|
|
||||||
|
|
||||||
# plt.tight_layout()
|
|
||||||
# fig.subplots_adjust(right=0.95)
|
|
||||||
|
|
||||||
# if save_dir:
|
|
||||||
# os.makedirs(save_dir, exist_ok=True)
|
|
||||||
# img_name = os.path.splitext(os.path.basename(image_path))[0]
|
|
||||||
# fig_path = os.path.join(save_dir, f'{img_name}_inference.png')
|
|
||||||
# fig.savefig(fig_path, dpi=150, bbox_inches='tight')
|
|
||||||
# print(f"💾 Résultat sauvegardé: {fig_path}")
|
|
||||||
|
|
||||||
# plt.show()
|
|
||||||
|
|
||||||
# # Statistiques
|
|
||||||
# if show_stats:
|
|
||||||
# print("\n📊 Statistiques du masque prédit:")
|
|
||||||
# unique_classes, counts = np.unique(predicted_mask, return_counts=True)
|
|
||||||
# for class_id, count in zip(unique_classes, counts):
|
|
||||||
# if class_id < len(mapping_config['group_names']):
|
|
||||||
# percentage = (count / predicted_mask.size) * 100
|
|
||||||
# print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)")
|
|
||||||
|
|
||||||
# if mask_gt is not None:
|
|
||||||
# mask_gt_np = mask_gt.numpy() if isinstance(mask_gt, torch.Tensor) else mask_gt
|
|
||||||
# print("\n📊 Statistiques du masque de vérité:")
|
|
||||||
# unique_classes, counts = np.unique(mask_gt_np, return_counts=True)
|
|
||||||
# for class_id, count in zip(unique_classes, counts):
|
|
||||||
# if class_id < len(mapping_config['group_names']):
|
|
||||||
# percentage = (count / mask_gt_np.size) * 100
|
|
||||||
# print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)")
|
|
||||||
|
|
||||||
# return predicted_mask, inference_time
|
|
||||||
|
|
||||||
|
|
||||||
# Version mise à jour de run_inference_and_visualize
|
# Version mise à jour de run_inference_and_visualize
|
||||||
def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config,
|
def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config,
|
||||||
mask_path=None, save_dir=None, show_stats=True):
|
mask_path=None, save_dir=None, show_stats=True, show_plot=True):
|
||||||
"""
|
"""
|
||||||
Version simplifiée utilisant preprocess_image_and_mask
|
Version simplifiée utilisant preprocess_image_and_mask
|
||||||
"""
|
"""
|
||||||
|
|
@ -461,7 +260,6 @@ def run_inference_and_visualize(image_path, model, encoder_name, img_size, mappi
|
||||||
fig.savefig(fig_path, dpi=150, bbox_inches='tight')
|
fig.savefig(fig_path, dpi=150, bbox_inches='tight')
|
||||||
print(f"💾 Résultat sauvegardé: {fig_path}")
|
print(f"💾 Résultat sauvegardé: {fig_path}")
|
||||||
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
# Statistiques
|
# Statistiques
|
||||||
if show_stats:
|
if show_stats:
|
||||||
|
|
@ -481,7 +279,11 @@ def run_inference_and_visualize(image_path, model, encoder_name, img_size, mappi
|
||||||
percentage = (count / mask_gt_np.size) * 100
|
percentage = (count / mask_gt_np.size) * 100
|
||||||
print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)")
|
print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)")
|
||||||
|
|
||||||
|
if show_plot:
|
||||||
|
plt.show()
|
||||||
return predicted_mask, inference_time
|
return predicted_mask, inference_time
|
||||||
|
else:
|
||||||
|
return predicted_mask, inference_time, fig
|
||||||
|
|
||||||
|
|
||||||
def load_model(run_id=None, experiment_name="OC Projet 9",
|
def load_model(run_id=None, experiment_name="OC Projet 9",
|
||||||
|
|
@ -736,6 +538,6 @@ def load_second_best_model(experiment_name="OC Projet 9", metric="test_mean_iou"
|
||||||
|
|
||||||
def load_model_by_run_id(run_id):
|
def load_model_by_run_id(run_id):
|
||||||
"""Charge un modèle par run_id spécifique"""
|
"""Charge un modèle par run_id spécifique"""
|
||||||
return load_model_by_param(run_id=run_id)
|
return load_model(run_id=run_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue