oc-p09-street-vision-poc/app/main.py

566 lines
No EOL
23 KiB
Python

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
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 ===")
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")
mlflow.set_tracking_uri(mlflow_tracking_uri)
print(f"MLflow Tracking URI: {mlflow_tracking_uri}")
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")
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_model,
run_inference_and_visualize,
colorize_mask
)
# 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 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):
filename: str
display_name: str
has_ground_truth: bool
class AvailableImagesResponse(BaseModel):
images: List[ImageInfo]
total_count: int
# Variables globales pour les deux modèles
best_model = None
second_best_model = None
best_model_info = {}
second_best_model_info = {}
mapping_config = None
def get_model_detailed_info(run_id, mapping_config):
"""Récupère les informations détaillées d'un modèle depuis MLflow"""
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:
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 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
)
# Récupérer les infos détaillées du meilleur modèle
best_detailed_info = get_model_detailed_info(best_run_id, mapping_config)
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"]
}
os.makedirs(TEMP_UPLOAD_DIR, exist_ok=True)
except Exception as e:
print(f"❌ Erreur lors de l'initialisation: {e}")
traceback.print_exc()
raise e
# Initialisation de l'API
app = FastAPI(
title="Multi-Class Segmentation Comparison API",
description="API de comparaison de segmentation sémantique avec deux modèles",
version="2.0.0"
)
@app.on_event("startup")
async def startup_event():
try:
initialize_models()
print("🚀 API démarrée avec succès - Modèles chargés!")
except Exception as e:
print(f"⚠️ ATTENTION: Impossible de charger les modèles au démarrage: {e}")
@app.get("/")
def root():
return {
"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():
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",
"models_loaded": True,
"timestamp": datetime.utcnow().isoformat()
}
@app.get("/models", response_model=dict)
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 {
"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()
}
@app.get("/sample-images", response_model=AvailableImagesResponse)
def get_sample_images():
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"):
base_name = img_path.stem.replace("_leftImg8bit", "")
mask_name = f"{base_name}_gtFine_labelIds.png"
mask_path = mask_dir / mask_name
display_name = base_name.replace("_", " ").title()
images.append(ImageInfo(
filename=img_path.name,
display_name=display_name,
has_ground_truth=mask_path.exists()
))
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):
try:
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
image_path = original_dir / filename
if not image_path.exists():
raise HTTPException(status_code=404, detail=f"Sample image {filename} not found")
if not filename.endswith('_leftImg8bit.png'):
raise HTTPException(status_code=400, detail="Invalid image filename format")
return FileResponse(
image_path,
media_type="image/png",
headers={"Content-Disposition": f"inline; filename={filename}"}
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error serving image: {str(e)}")
class SampleImageRequest(BaseModel):
filename: str
@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:
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")
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
# Inférence modèle 1 avec sa résolution spécifique
pred1, t1, fig1 = run_inference_and_visualize(
image_path=str(image_path),
mask_path=mask_path_str,
model=best_model,
encoder_name=None, # Normalisé de la même manière
img_size=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
)
# 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
)
# Créer une figure combinée avec les deux modèles
# Nombre de colonnes : 3 si pas de GT, 4 si GT disponible
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))
# Récupérer les images depuis les figures individuelles
axes1 = fig1.get_axes()
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 = {}
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:
raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
@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)}")
temp_file_path = None
try:
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)
with open(temp_file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# Inférences avec résolutions spécifiques
pred1, t1, fig1 = run_inference_and_visualize(
image_path=temp_file_path, mask_path=None,
model=best_model, encoder_name=None, # Normalisé de la même manière
img_size=best_model_info["input_size"], mapping_config=mapping_config,
save_dir=None, show_stats=False, show_plot=False,
)
pred2, t2, fig2 = run_inference_and_visualize(
image_path=temp_file_path, mask_path=None,
model=second_best_model, encoder_name=None, # Normalisé de la même manière
img_size=second_best_model_info["input_size"], mapping_config=mapping_config,
save_dir=None, show_stats=False, show_plot=False,
)
# Figure combinée pour uploads (pas de GT, donc 2 colonnes seulement)
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
# Récupérer les images depuis les figures individuelles
axes1 = fig1.get_axes()
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 = {}
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,
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:
raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
finally:
if temp_file_path and os.path.exists(temp_file_path):
try: os.remove(temp_file_path)
except Exception: pass
@app.get("/metrics")
def get_metrics():
return {
"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()
}