Project 9 added.

This commit is contained in:
Grégoire Mureau 2025-09-02 08:12:32 +02:00
commit a833332c4a
39 changed files with 24623 additions and 0 deletions

106
.dockerignore Normal file
View file

@ -0,0 +1,106 @@
# Notebooks de développement mais GARDER les samples
notebooks/*.ipynb
notebooks/p8_notebook.ipynb
notebooks/requirements.txt
.ipynb_checkpoints/
# GARDER: notebooks/content/data/test_images_sample/ (nécessaire pour l'app)
# Environment files (variables passées via docker run ou compose)
.env
.env.*
!.env.example
# Python cache et build files
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
venv/
env/
ENV/
env.bak/
venv.bak/
.venv/
# IDE et editor files
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
Thumbs.db
# Git files
.git/
.gitignore
.gitattributes
# Documentation
*.md
!README.MD
docs/
documentation/
# Test files
tests/
test_*/
*_test.py
debug_*.py
pytest.ini
tox.ini
.coverage
.pytest_cache/
# Jupyter notebook outputs
*.nbconvert
# Fichiers temporaires
.cache/
.tmp/
tmp/
temp/
*.tmp
*.temp
*.log
logs/
# OS generated files
.DS_Store?
ehthumbs.db
Icon?
# MLflow artifacts locaux (seront téléchargés depuis le registry)
mlruns/
mlruns.backup/
mlartifacts/
# Backup files
*.bak
*.backup
# Large datasets (samples nécessaires sont gardés)
*.zip
*.tar.gz
*.rar

41
.env.example Normal file
View file

@ -0,0 +1,41 @@
# =============================================================================
# Configuration du serveur
# =============================================================================
# Port sur lequel nginx écoute (défaut: 8080 pour compatibilité Cloud Run)
NGINX_PORT=8080
# =============================================================================
# Configuration MLflow (pour le versioning et le téléchargement des modèles)
# =============================================================================
# URI du serveur MLflow de tracking
MLFLOW_TRACKING_URI=http://localhost:5000
# URL du stockage S3/MinIO pour les artefacts MLflow
MLFLOW_S3_ENDPOINT_URL=http://localhost:9000
# Clés d'accès S3/MinIO
AWS_ACCESS_KEY_ID=minio_access_key
AWS_SECRET_ACCESS_KEY=minio_secret_key
# =============================================================================
# Configuration email (optionnel - pour les notifications)
# =============================================================================
# Serveur SMTP
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=587
# Identifiants email
SMTP_EMAIL=noreply@yourcompany.com
SMTP_PASSWORD=your_app_password
SMTP_FROM_ALIAS=Multi-Segmentation API
# Email de l'administrateur (destinataire des notifications)
ADMIN_EMAIL=admin@yourcompany.com
# =============================================================================
# Configuration de l'API
# =============================================================================
# URL de base de l'API (utilisée par Streamlit pour faire les appels)
# En développement local: http://localhost:8080/api
# En production: https://your-domain.com/api
MULTISEG_API_BASE_URL=http://localhost:8080/api

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
./mlruns*

43
Dockerfile Normal file
View file

@ -0,0 +1,43 @@
FROM python:3.12-slim
WORKDIR /app
# Install system dependencies (ajout de git pour segmentation_models)
RUN apt-get update && \
apt-get install -y nginx curl gettext-base git && \
# Pour OpenCV si nécessaire
apt-get install -y libglib2.0-0 libsm6 libxext6 libxrender-dev libgomp1 && \
adduser --system --no-create-home --group www-data || true && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Copy app/ d'abord pour avoir requirements.txt
COPY app/ ./app/
# Install requirements depuis le bon chemin
RUN pip install --no-cache-dir -r app/requirements.txt
# ✅ CORRECTION: Copy start.sh AVANT les permissions
COPY start.sh ./start.sh
# Copy du reste après installation (optimise les layers Docker)
COPY utils/ ./utils/
COPY cityscapes_config.json .
COPY notebooks/content/data/test_images_sample/ ./notebooks/content/data/test_images_sample/
# Copy nginx config
COPY nginx.conf /etc/nginx/nginx.conf.template
# ✅ CORRECTION: Setup permissions APRÈS avoir copié start.sh + correction fins de ligne
RUN mkdir -p /var/log/nginx /var/lib/nginx /run && \
sed -i 's/\r$//' ./start.sh && \
chmod +x ./start.sh
# Environment variables
ENV NGINX_PORT=8080
ENV MULTISEG_API_BASE_URL=http://localhost:8080/api
EXPOSE $NGINX_PORT
# ✅ CORRECTION: Chemin cohérent (./start.sh au lieu de /app/start.sh)
CMD ["./start.sh"]

3
README.MD Normal file
View file

@ -0,0 +1,3 @@
# Projet 9
Documentation à venir

427
app/main.py Normal file
View file

@ -0,0 +1,427 @@
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()
}

141
app/requirements.txt Normal file
View file

@ -0,0 +1,141 @@
# =============================================================================
# Multi-Class Segmentation API - Requirements
# =============================================================================
# ===== CORE WEB FRAMEWORK =====
fastapi==0.115.14
uvicorn==0.35.0
starlette==0.46.2
pydantic==2.11.7
pydantic_core==2.33.2
# ===== STREAMLIT =====
streamlit==1.46.1
altair==5.5.0 # Utilisé par Streamlit pour les graphiques
blinker==1.9.0 # Streamlit dependency
# ===== HTTP REQUESTS =====
requests==2.32.4
httpx==0.28.1 # Modern HTTP client (utilisé par FastAPI)
httpcore==1.0.9
h11==0.16.0
# ===== MACHINE LEARNING CORE =====
tensorflow==2.19.0
keras==3.10.0
numpy>=1.24.0,<2.0.0
# Note: tensorflow-addons n'est plus nécessaire avec TensorFlow 2.19+
# Les fonctionnalités sont maintenant intégrées dans TensorFlow core
# ===== COMPUTER VISION =====
pillow==11.3.0
opencv-python-headless==4.9.0.80 # Pas dans votre freeze, mais nécessaire pour Docker
scikit-image==0.25.2
imageio==2.37.0
# ===== DATA SCIENCE =====
pandas==2.3.0
matplotlib==3.10.3
scipy==1.16.0
scikit-learn==1.7.0
# ===== MLFLOW & MODEL REGISTRY =====
mlflow==3.2.0
mlflow-skinny==3.2.0
mlflow-tracing==3.2.0
# ===== AWS/S3 SUPPORT =====
boto3==1.39.2
botocore==1.39.2
s3transfer==0.13.0
# ===== SEGMENTATION MODELS =====
# Option 1: Version figée par commit (RECOMMANDÉE pour production)
segmentation_models @ git+https://github.com/qubvel/segmentation_models.git@5d24bbfb28af6134e25e2c0b79e7727f6c0491d0
# Option 2: Si vous préférez une version PyPI (décommentez et commentez la ligne du dessus)
# segmentation-models==1.0.1
# ===== SUPPORTING LIBRARIES =====
# TensorFlow ecosystem
tensorboard==2.19.0
tensorboard-data-server==0.7.2
protobuf==3.20.3
flatbuffers==25.2.10
h5py==3.14.0
astunparse==1.6.3
gast==0.6.0
google-pasta==0.2.0
keras-applications==1.0.8
opt_einsum==3.4.0
termcolor==2.5.0
wrapt==1.17.2
# Data processing
networkx==3.5
tifffile==2025.6.11
lazy_loader==0.4
# Utilities
click==8.2.1
tqdm==4.67.1
joblib==1.5.1
python-dateutil
packaging
typing_extensions==4.14.0
# File handling
python-multipart==0.0.20 # Pour FastAPI file uploads
# ===== AJOUTS SPÉCIFIQUES POUR UTILS/UTILS.PY =====
# Pour la gestion des couleurs et légendes matplotlib
cycler==0.12.1 # Pour matplotlib customization
# Font handling pour matplotlib (nécessaire dans Docker)
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 .

686
app/streamlit_app.py Normal file
View file

@ -0,0 +1,686 @@
import os
import sys
import requests
import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import io
import base64
import pickle
# Configuration de l'API
# prod docker
API_BASE_URL = os.getenv("MULTISEG_API_BASE_URL", "http://127.0.0.1:8080/api")
# test local
# API_BASE_URL = os.getenv("MULTISEG_API_BASE_URL", "http://127.0.0.1:8000") # ← Port 8000 au lieu de 8080
# Import des fonctions utils pour l'affichage local
sys.path.append('/app')
# APRÈS (pour test local)
sys.path.append('.')
from utils.utils import colorize_mask
# Configuration de la page
st.set_page_config(
page_title="Multi-Class Segmentation",
page_icon="🚗",
layout="wide"
)
# CSS personnalisé
st.markdown("""
<style>
.main-header {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
border-radius: 10px;
margin-bottom: 2rem;
color: white;
text-align: center;
}
.metric-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 1rem;
border-radius: 8px;
border-left: 4px solid #667eea;
margin: 0.5rem 0;
color: #2c3e50;
font-weight: 500;
}
.prediction-stats {
background: #e8f4fd;
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
}
</style>
""", unsafe_allow_html=True)
# En-tête principal
st.markdown("""
<div class="main-header">
<h1>🚗 Segmentation Multi-Classes</h1>
<p>Système de segmentation sémantique pour véhicules autonomes</p>
</div>
""", unsafe_allow_html=True)
# Initialisation des états
if "prediction_result" not in st.session_state:
st.session_state.prediction_result = None
if "selected_image" not in st.session_state:
st.session_state.selected_image = None
if "uploaded_file" not in st.session_state:
st.session_state.uploaded_file = None
def reset_state():
"""Reset des états de session"""
st.session_state.prediction_result = None
st.session_state.selected_image = None
st.session_state.uploaded_file = None
def get_model_info():
"""Récupère les informations du modèle"""
try:
response = requests.get(f"{API_BASE_URL}/models")
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur lors de la récupération des infos modèle: {response.status_code}")
return None
except Exception as e:
st.error(f"Erreur de connexion à l'API: {e}")
return None
def get_sample_images():
"""Récupère la liste des images d'exemple"""
try:
response = requests.get(f"{API_BASE_URL}/sample-images")
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur lors de la récupération des images: {response.status_code}")
return None
except Exception as e:
st.error(f"Erreur de connexion à l'API: {e}")
return None
def predict_sample_image(filename):
"""Lance une prédiction sur une image d'exemple"""
try:
# CORRECTION: Envoyer en JSON au lieu de form data
data = {"filename": filename}
response = requests.post(
f"{API_BASE_URL}/predict-sample",
json=data, # ← Changé de 'data=' à 'json='
headers={"Content-Type": "application/json"} # ← Ajout du header explicite
)
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur API: {response.status_code} - {response.text}")
return None
except Exception as e:
st.error(f"Erreur lors de la prédiction: {e}")
return None
def predict_uploaded_image(uploaded_file):
"""Lance une prédiction sur une image uploadée"""
try:
files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)}
response = requests.post(f"{API_BASE_URL}/predict-upload", files=files)
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur API: {response.status_code} - {response.text}")
return None
except Exception as e:
st.error(f"Erreur lors de la prédiction: {e}")
return None
def display_segmentation_plots(fig):
"""Affiche chaque subplot séparément dans des colonnes"""
if fig is None:
st.warning("Aucune figure de segmentation disponible")
return
axes = fig.get_axes()
num_plots = len(axes)
st.markdown("### 🎨 Résultat de la Segmentation")
# Créer les colonnes selon le nombre de subplots
if num_plots <= 4:
cols = st.columns(num_plots)
else:
cols = st.columns(4) # Maximum 4 colonnes
for i, ax in enumerate(axes):
col_idx = i % len(cols)
with cols[col_idx]:
# Récupérer le titre existant
title = ax.get_title()
# st.markdown(f"**{title}:**")
# Créer une figure individuelle avec ce subplot
individual_fig, individual_ax = plt.subplots(figsize=(6, 6))
# Copier toutes les images de l'axe original
for image in ax.get_images():
individual_ax.imshow(
image.get_array(),
cmap=image.get_cmap() if image.get_cmap() else None,
alpha=image.get_alpha() if image.get_alpha() else None,
vmin=image.get_clim()[0] if image.get_clim() else None,
vmax=image.get_clim()[1] if image.get_clim() else None
)
# Copier les propriétés de l'axe
individual_ax.set_title(title)
individual_ax.axis('off')
# Ajuster la mise en page
individual_fig.tight_layout()
# Afficher dans Streamlit
st.pyplot(individual_fig)
# Libérer la mémoire
plt.close(individual_fig)
# Fermer la figure originale pour libérer la mémoire
plt.close(fig)
def display_prediction_stats_with_chart(stats, model_info):
"""Affiche les statistiques avec un graphique en barres"""
st.markdown("### 📊 Statistiques de Segmentation")
if not stats:
st.warning("Aucune statistique disponible")
return
# Ratio 1:2 pour donner plus de place au graphique
col1, col2 = st.columns([1, 2])
# Colonne 1: Cartes de statistiques compactes avec carrés colorés
with col1:
st.markdown("**Détails par classe:**")
# Récupérer les couleurs des classes pour les carrés
class_colors_dict = {}
if model_info and 'class_colors' in model_info and 'class_names' in model_info:
model_class_names = model_info['class_names']
model_class_colors = model_info['class_colors']
for class_name in stats.keys():
try:
idx = model_class_names.index(class_name)
color = model_class_colors[idx]
# Convertir en hex pour CSS
color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
class_colors_dict[class_name] = color_hex
except (ValueError, IndexError):
# Couleur par défaut si pas trouvée
class_colors_dict[class_name] = "#808080"
for class_name, data in stats.items():
# Récupérer la couleur pour cette classe
color_hex = class_colors_dict.get(class_name, "#808080")
# ✨ NOUVEAU: Carré coloré + nom de classe
st.markdown(f"""
<div style="
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 0.5rem 0.8rem;
border-radius: 6px;
border-left: 3px solid #667eea;
margin: 0.3rem 0;
color: #2c3e50;
font-weight: 500;
font-size: 0.85rem;
line-height: 1.3;
">
<div style="
font-weight: 600;
margin-bottom: 0.2rem;
display: flex;
align-items: center;
gap: 0.5rem;
">
<div style="
width: 12px;
height: 12px;
background-color: {color_hex};
border-radius: 2px;
border: 1px solid rgba(0,0,0,0.2);
flex-shrink: 0;
"></div>
<span>{class_name}</span>
</div>
<div style="font-size: 0.8rem; color: #555;">
🔢 {data['pixels']:,} pixels | 📊 {data['percentage']}%
</div>
</div>
""", unsafe_allow_html=True)
# Colonne 2: Graphique en barres (reste identique)
with col2:
st.markdown("**Répartition visuelle:**")
# Préparer les données pour le graphique
class_names = list(stats.keys())
percentages = [data['percentage'] for data in stats.values()]
# Récupérer les couleurs des classes depuis model_info
colors = []
if model_info and 'class_colors' in model_info and 'class_names' in model_info:
model_class_names = model_info['class_names']
model_class_colors = model_info['class_colors']
for class_name in class_names:
# Trouver l'index de cette classe dans le modèle
try:
idx = model_class_names.index(class_name)
color = model_class_colors[idx]
# Convertir RGB en format matplotlib
colors.append([c/255.0 for c in color])
except (ValueError, IndexError):
# Couleur par défaut si pas trouvée
colors.append([0.5, 0.5, 0.5])
else:
# Couleurs par défaut si pas d'info du modèle
colors = plt.cm.Set3(np.linspace(0, 1, len(class_names)))
# Créer un graphique plus grand et mieux adapté
fig, ax = plt.subplots(figsize=(10, 6)) # Plus large
bars = ax.bar(class_names, percentages, color=colors, alpha=0.8, edgecolor='white', linewidth=1)
# Personnalisation du graphique améliorée
ax.set_xlabel('Classes de Segmentation', fontsize=11, fontweight='bold')
ax.set_ylabel('Pourcentage (%)', fontsize=11, fontweight='bold')
ax.set_title('Répartition des Classes Détectées', fontsize=13, fontweight='bold', pad=20)
# Meilleure gestion des labels selon le nombre de classes
if len(class_names) <= 4:
plt.xticks(rotation=0, ha='center', fontsize=10)
elif len(class_names) <= 6:
plt.xticks(rotation=30, ha='right', fontsize=9)
else:
plt.xticks(rotation=45, ha='right', fontsize=8)
plt.yticks(fontsize=10)
# Valeurs sur les barres avec meilleur positionnement
for bar, percentage in zip(bars, percentages):
height = bar.get_height()
# Adapter la position du texte selon la hauteur de la barre
if height < max(percentages) * 0.1:
# Si la barre est très petite, mettre le texte au-dessus
va = 'bottom'
y_pos = height + max(percentages) * 0.02
else:
# Sinon, centrer dans la barre
va = 'center'
y_pos = height / 2
ax.text(bar.get_x() + bar.get_width()/2., y_pos,
f'{percentage:.1f}%',
ha='center', va=va, fontsize=9, fontweight='bold',
color='white' if va == 'center' else 'black',
bbox=dict(boxstyle='round,pad=0.2', facecolor='black', alpha=0.7) if va == 'center' else None)
# Améliorer la mise en page et le style
plt.tight_layout()
# Grille plus subtile
ax.grid(axis='y', alpha=0.2, linestyle='--')
ax.set_axisbelow(True)
# Limites et style des axes
ax.set_ylim(0, max(percentages) * 1.15) # Plus d'espace au-dessus
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color('#666')
ax.spines['bottom'].set_color('#666')
# Couleur de fond
fig.patch.set_facecolor('white')
ax.set_facecolor('#fafafa')
# Afficher dans Streamlit
st.pyplot(fig)
plt.close(fig)
def display_class_legend(model_info):
"""Affiche la légende des classes de segmentation"""
if model_info and 'class_colors' in model_info:
st.markdown("**Légende des classes:**")
class_names = model_info.get('class_names', [])
class_colors = model_info.get('class_colors', [])
# Créer une légende colorée
legend_cols = st.columns(len(class_names))
for i, (name, color) in enumerate(zip(class_names, class_colors)):
with legend_cols[i]:
color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
st.markdown(f"""
<div style="
background-color: {color_hex};
padding: 10px;
border-radius: 5px;
text-align: center;
color: white;
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0,0,0,0.7);
margin: 2px;
">
{name}
</div>
""", unsafe_allow_html=True)
# # Sidebar avec informations du modèle
# with st.sidebar:
# st.markdown("## 🔧 Informations du Modèle")
# model_info = get_model_info()
# if model_info:
# st.success("✅ Modèle chargé")
# st.write(f"**Nom:** {model_info.get('model_name', 'N/A')}")
# st.write(f"**Encoder:** {model_info.get('encoder', 'N/A')}")
# st.write(f"**Classes:** {model_info.get('num_classes', 'N/A')}")
# st.write(f"**Taille d'entrée:** {model_info.get('input_size', 'N/A')}")
# st.write(f"**Run ID:** {model_info.get('run_id', 'N/A')[:8]}...")
# else:
# st.error("❌ Modèle non disponible")
# st.markdown("---")
# st.markdown("## 🎯 Classes Segmentées")
# if model_info and 'class_names' in model_info:
# for i, class_name in enumerate(model_info['class_names']):
# st.write(f"{i}. {class_name}")
# Sidebar avec informations du modèle
with st.sidebar:
st.markdown("## 🔧 Informations du Modèle")
model_info = get_model_info()
if model_info:
st.success("✅ Modèle chargé")
st.write(f"**Nom:** {model_info.get('model_name', 'N/A')}")
st.write(f"**Encoder:** {model_info.get('encoder', 'N/A')}")
st.write(f"**Classes:** {model_info.get('num_classes', 'N/A')}")
st.write(f"**Taille d'entrée:** {model_info.get('input_size', 'N/A')}")
st.write(f"**Run ID:** {model_info.get('run_id', 'N/A')[:8]}...")
else:
st.error("❌ Modèle non disponible")
st.markdown("---")
st.markdown("## 🎯 Classes Segmentées")
# ✨ NOUVEAU: Affichage avec carrés colorés
if model_info and 'class_names' in model_info and 'class_colors' in model_info:
class_names = model_info['class_names']
class_colors = model_info['class_colors']
for i, (class_name, color) in enumerate(zip(class_names, class_colors)):
# Convertir RGB en hex
color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
# Affichage avec carré coloré
st.markdown(f"""
<div style="
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0.3rem 0;
font-size: 0.9rem;
">
<div style="
width: 14px;
height: 14px;
background-color: {color_hex};
border-radius: 2px;
border: 1px solid rgba(0,0,0,0.2);
flex-shrink: 0;
"></div>
<span style="font-weight: 500;">{i}. {class_name}</span>
</div>
""", unsafe_allow_html=True)
else:
# Fallback si pas d'info couleur
if model_info and 'class_names' in model_info:
for i, class_name in enumerate(model_info['class_names']):
st.write(f"{i}. {class_name}")
else:
st.write("Informations des classes non disponibles")
# Initialisation des états - MODIFIÉE pour séparer les résultats par onglet
if "sample_prediction_result" not in st.session_state:
st.session_state.sample_prediction_result = None
if "sample_selected_image" not in st.session_state:
st.session_state.sample_selected_image = None
if "upload_prediction_result" not in st.session_state:
st.session_state.upload_prediction_result = None
if "upload_uploaded_file" not in st.session_state:
st.session_state.upload_uploaded_file = None
# ANCIEN système (gardé pour compatibilité si besoin)
if "prediction_result" not in st.session_state:
st.session_state.prediction_result = None
if "selected_image" not in st.session_state:
st.session_state.selected_image = None
if "uploaded_file" not in st.session_state:
st.session_state.uploaded_file = None
def reset_sample_state():
"""Reset des états de session pour l'onglet Sample"""
st.session_state.sample_prediction_result = None
st.session_state.sample_selected_image = None
def reset_upload_state():
"""Reset des états de session pour l'onglet Upload"""
st.session_state.upload_prediction_result = None
st.session_state.upload_uploaded_file = None
def reset_state():
"""Reset complet (garde l'ancienne fonction pour compatibilité)"""
reset_sample_state()
reset_upload_state()
# Interface principale - MODIFIÉE pour résultats indépendants
tab1, tab2 = st.tabs(["📸 Images d'Exemple", "📤 Upload Personnel"])
# Tab 1: Images d'exemple
with tab1:
st.markdown("## 📸 Sélectionner une Image d'Exemple")
st.markdown("Ces images proviennent du dataset Cityscapes avec masques de vérité disponibles.")
# Récupérer les images d'exemple
sample_data = get_sample_images()
if sample_data and sample_data['images']:
# Sélecteur d'image
image_options = {img['display_name']: img['filename'] for img in sample_data['images']}
selected_display_name = st.selectbox(
"Choisissez une image:",
options=list(image_options.keys()),
help=f"{sample_data['total_count']} images disponibles"
)
selected_filename = image_options[selected_display_name]
# Afficher les infos de l'image sélectionnée
selected_img_info = next(img for img in sample_data['images'] if img['filename'] == selected_filename)
col1, col2 = st.columns([2, 1])
with col1:
st.info(f"📁 **Fichier:** {selected_filename}")
with col2:
ground_truth_status = "✅ Disponible" if selected_img_info['has_ground_truth'] else "❌ Non disponible"
st.info(f"🎯 **Vérité terrain:** {ground_truth_status}")
# Prévisualisation de l'image sélectionnée
if selected_filename:
try:
# Récupérer l'image via l'API
image_url = f"{API_BASE_URL}/sample-image/{selected_filename}"
response = requests.get(image_url, timeout=5)
if response.status_code == 200:
# Charger l'image depuis les bytes
sample_image = Image.open(io.BytesIO(response.content))
st.image(
sample_image,
caption=f"Aperçu: {selected_display_name}",
use_container_width=True
)
elif response.status_code == 404:
st.warning("📷 Image non trouvée sur le serveur")
else:
st.info("📷 Prévisualisation temporairement indisponible")
except requests.exceptions.Timeout:
st.warning("⏱️ Timeout lors du chargement de l'aperçu")
except Exception as e:
st.info("📷 Aperçu non disponible - L'image sera chargée lors de la prédiction")
# Bouton de prédiction
if st.button("🔮 Lancer la Segmentation", key="predict_sample", type="primary"):
if selected_filename:
with st.spinner("Segmentation en cours..."):
result = predict_sample_image(selected_filename)
if result and result['success']:
# ✨ NOUVEAU: Stocker dans les variables Sample
st.session_state.sample_prediction_result = result
st.session_state.sample_selected_image = selected_filename
st.success("✅ Segmentation terminée!")
st.rerun()
else:
st.error("❌ Erreur lors de la segmentation")
else:
st.warning("Aucune image d'exemple disponible")
# ✨ NOUVEAU: Affichage des résultats Sample (indépendants)
if st.session_state.sample_prediction_result and st.session_state.sample_selected_image:
st.markdown("---")
result = st.session_state.sample_prediction_result
st.markdown(f"## 🎯 Résultats pour: {st.session_state.sample_selected_image}")
# Afficher la segmentation
if result.get('figure_data'):
try:
# Désérialiser la figure
fig_bytes = base64.b64decode(result['figure_data'])
fig = pickle.loads(fig_bytes)
display_segmentation_plots(fig)
except Exception as e:
st.error(f"Erreur lors de l'affichage de la segmentation: {e}")
# Fallback sur la légende des classes
display_class_legend(model_info)
else:
# Fallback si pas de figure_data
display_class_legend(model_info)
# Afficher les statistiques
if result.get('prediction_stats'):
display_prediction_stats_with_chart(result['prediction_stats'], model_info)
# Informations additionnelles
if result.get('ground_truth_available'):
st.success("🎯 Masque de vérité terrain disponible pour comparaison")
else:
st.info("ℹ️ Pas de vérité terrain disponible")
# Bouton de reset
if st.button("🔄 Nouvelle Analyse", key="reset_sample", type="secondary"):
reset_sample_state()
st.rerun()
# Tab 2: Upload personnel
with tab2:
st.markdown("## 📤 Uploader Votre Image")
st.markdown("Uploadez une image de rue pour analyse. Formats acceptés: JPG, PNG")
uploaded_file = st.file_uploader(
"Sélectionnez une image:",
type=['jpg', 'jpeg', 'png'],
help="Taille maximale recommandée: 5MB"
)
if uploaded_file is not None:
# Afficher l'image uploadée
image = Image.open(uploaded_file)
st.image(image, caption=f"Image uploadée: {uploaded_file.name}", use_container_width=True)
# Informations sur le fichier
file_size_mb = len(uploaded_file.getvalue()) / 1024 / 1024
st.info(f"📁 **Fichier:** {uploaded_file.name} | 📊 **Taille:** {file_size_mb:.2f} MB")
# Bouton de prédiction
if st.button("🔮 Lancer la Segmentation", key="predict_upload", type="primary"):
with st.spinner("Segmentation en cours..."):
result = predict_uploaded_image(uploaded_file)
if result and result['success']:
# ✨ NOUVEAU: Stocker dans les variables Upload
st.session_state.upload_prediction_result = result
st.session_state.upload_uploaded_file = uploaded_file.name
st.success("✅ Segmentation terminée!")
st.rerun()
else:
st.error("❌ Erreur lors de la segmentation")
# ✨ NOUVEAU: Affichage des résultats Upload (indépendants)
if st.session_state.upload_prediction_result and st.session_state.upload_uploaded_file:
st.markdown("---")
result = st.session_state.upload_prediction_result
st.markdown(f"## 🎯 Résultats pour: {st.session_state.upload_uploaded_file}")
# Afficher la segmentation
if result.get('figure_data'):
try:
# Désérialiser la figure
fig_bytes = base64.b64decode(result['figure_data'])
fig = pickle.loads(fig_bytes)
display_segmentation_plots(fig)
except Exception as e:
st.error(f"Erreur lors de l'affichage de la segmentation: {e}")
# Fallback sur la légende des classes
display_class_legend(model_info)
else:
# Fallback si pas de figure_data
display_class_legend(model_info)
# Afficher les statistiques
if result.get('prediction_stats'):
display_prediction_stats_with_chart(result['prediction_stats'], model_info)
# Informations additionnelles
if result.get('ground_truth_available'):
st.success("🎯 Masque de vérité terrain disponible pour comparaison")
else:
st.info("ℹ️ Pas de vérité terrain disponible (image personnelle)")
# Bouton de reset
if st.button("🔄 Nouvelle Analyse", key="reset_upload", type="secondary"):
reset_upload_state()
st.rerun()
# Footer
st.markdown("---")
st.markdown("""
<div style="text-align: center; color: #666; padding: 1rem;">
🚗 <strong>Multi-Class Segmentation API</strong> -
Système de vision par ordinateur pour véhicules autonomes<br>
<small>Développé avec FastAPI, Streamlit et TensorFlow</small>
</div>
""", unsafe_allow_html=True)

63
cityscapes_config.json Normal file
View file

@ -0,0 +1,63 @@
{
"class_groups": {
"flat": ["road", "sidewalk", "parking", "rail track"],
"human": ["person", "rider"],
"vehicle": ["car", "truck", "bus", "train", "motorcycle", "bicycle", "caravan", "trailer"],
"construction": ["building", "wall", "fence", "guard rail", "bridge", "tunnel"],
"object": ["pole", "pole group", "traffic sign", "traffic light"],
"nature": ["vegetation", "terrain"],
"sky": ["sky"],
"void": ["unlabeled", "ego vehicle", "rectification border", "out of roi", "static", "dynamic", "ground"]
},
"cityscapes_mapping": {
"0": 7, "1": 7, "2": 7, "3": 7, "4": 7, "5": 7, "6": 7,
"7": 0, "8": 0, "9": 0, "10": 0,
"11": 3, "12": 3, "13": 3, "14": 3, "15": 3, "16": 3,
"17": 4, "18": 4, "19": 4, "20": 4,
"21": 5, "22": 5,
"23": 6,
"24": 1, "25": 1,
"26": 2, "27": 2, "28": 2, "29": 2, "30": 2, "31": 2, "32": 2, "33": 2
},
"id_to_label": {
"0": "unlabeled",
"1": "ego vehicle",
"2": "rectification border",
"3": "out of roi",
"4": "static",
"5": "dynamic",
"6": "ground",
"7": "road",
"8": "sidewalk",
"9": "parking",
"10": "rail track",
"11": "building",
"12": "wall",
"13": "fence",
"14": "guard rail",
"15": "bridge",
"16": "tunnel",
"17": "pole",
"18": "pole group",
"19": "traffic light",
"20": "traffic sign",
"21": "vegetation",
"22": "terrain",
"23": "sky",
"24": "person",
"25": "rider",
"26": "car",
"27": "truck",
"28": "bus",
"29": "caravan",
"30": "trailer",
"31": "train",
"32": "motorcycle",
"33": "bicycle"
},
"group_colors": [
[128, 64, 128], [220, 20, 60], [0, 0, 142], [70, 70, 70],
[220, 220, 0], [107, 142, 35], [70, 130, 180], [0, 0, 0]
],
"group_names": ["flat", "human", "vehicle", "construction", "object", "nature", "sky", "void"]
}

View file

@ -0,0 +1,47 @@
version: '3.8'
services:
street-vision-api:
# ✅ PRODUCTION: Image depuis Docker Hub
image: grgmdmn/street-vision_api:latest
container_name: street_vision_api
ports:
- "${NGINX_PORT:-8080}:${NGINX_PORT:-8080}" # Port nginx configurable
environment:
# Configuration du port nginx
NGINX_PORT: "${NGINX_PORT:-8080}"
# Configuration MLflow pour le suivi des modèles
MLFLOW_TRACKING_URI: "${MLFLOW_TRACKING_URI}" # ex: https://mlflow.domain.com
MLFLOW_S3_ENDPOINT_URL: "${MLFLOW_S3_ENDPOINT_URL}" # ex: url MinIO ou S3
AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}" # clé ID S3
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}" # clé secrète S3
# Configuration email pour notifications (optionnel)
SMTP_SERVER: "${SMTP_SERVER:-}" # serveur SMTP
SMTP_PORT: "${SMTP_PORT:-587}" # port SMTP (ex: 587)
SMTP_EMAIL: "${SMTP_EMAIL:-}" # email expéditeur
SMTP_PASSWORD: "${SMTP_PASSWORD:-}" # mot de passe SMTP
SMTP_FROM_ALIAS: "${SMTP_FROM_ALIAS:-Street Vision API}" # alias (optionnel)
ADMIN_EMAIL: "${ADMIN_EMAIL:-}" # email admin destinataire
# URL de base de l'API (utilisée par Streamlit pour rediriger vers FastAPI)
MULTISEG_API_BASE_URL: "${MULTISEG_API_BASE_URL}" # ex: https://vision.domain.com/api
# Pour une mise à jour automatique par watchtower
labels:
com.centurylinklabs.watchtower.enable: "true"
# Seulement si vous avez un reverse proxy server
# networks:
# - swag
restart: unless-stopped
# Seulement si vous avez un reverse proxy server
# networks:
# swag:
# external: true

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
docs/mIoU_accuracy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

323
docs/note_methodologique.md Normal file
View file

@ -0,0 +1,323 @@
# Note Méthodologique : Preuve de concept SegFormer vs FPN
## Dataset retenu
### Cityscapes Dataset : Référence en Segmentation Urbaine
Le **Cityscapes Dataset** constitue la référence internationale pour l'évaluation des algorithmes de segmentation sémantique en environnement urbain. Développé par l'Université de Tübingen et Mercedes-Benz, ce dataset comprend **25,000 images** haute résolution (2048×1024 pixels) capturées dans 50 villes allemandes et suisses.
**Composition du dataset :**
- **Train** : 2,975 images finement annotées
- **Validation** : 500 images avec annotations de référence
- **Test** : 1,525 images (annotations non-publiques)
- **Coarse** : 20,000 images avec annotations grossières
**Architecture de classes hiérarchique :**
Le dataset organise 34 classes détaillées en **8 méta-classes** pour la segmentation :
| Méta-classe | Classes incluses | Importance |
|-------------|------------------|------------|
| **Flat** | Route, trottoir, parkings | Navigation de base |
| **Human** | Piéton, cycliste | **Sécurité critique** |
| **Vehicle** | Voiture, camion, bus, train | Obstacles dynamiques |
| **Construction** | Bâtiment, mur, clôture, pont | Structure urbaine |
| **Object** | Poteau, feu, panneau | Signalétique |
| **Nature** | Végétation, terrain | Environnement |
| **Sky** | Ciel | Contexte |
| **Void** | Zones non-définies | Exclusion |
**Spécificités techniques :**
- **Résolutions d'acquisition** : Données natives 2048×1024 pixels
- **Conditions d'acquisition** : Conditions météo bonnes à moyennes, pas de conditions adverses (pluie, neige)
- **Diversité géographique** : Villes allemandes et suisses (architectures variées)
- **Annotations pixel-perfect** : Masques de segmentation précis au pixel près
Ce dataset permet d'évaluer la capacité des modèles à comprendre les scènes urbaines complexes, élément fondamental pour les applications de conduite autonome et de surveillance urbaine intelligente.
---
## Les concepts de l'algorithme récent
### SegFormer : Architecture Transformer Vision-Optimisée
**SegFormer** représente une avancée majeure dans la segmentation sémantique en introduisant une architecture **Transformer** spécifiquement conçue pour la compréhension visuelle dense, contrairement aux Transformers textuels originaux.
### Innovations Architecturales Clés
**1. Encodeur Hiérarchique Multi-Échelles**
L'encodeur SegFormer **abandonne les encodages positionnels** classiques au profit d'une architecture hiérarchique qui capture naturellement les relations spatiales :
$$\text{Feature Maps} = \{F_1, F_2, F_3, F_4\}$$
avec résolutions $\{\frac{H}{4}, \frac{H}{8}, \frac{H}{16}, \frac{H}{32}\}$
**Efficient Self-Attention :**
Contrairement à l'attention standard $\mathcal{O}(N^2)$, SegFormer utilise une **attention réduite** :
$$\text{Attention}(Q,K,V) = \text{Softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$
où $K$ est sous-échantillonnée par un facteur $R$ pour réduire la complexité :
$$\text{Complexité} = \mathcal{O}\left(\frac{N^2}{R}\right)$$
**2. Mix-FFN : Convolution dans le Transformer**
Innovation majeure : intégration de **convolutions 3×3** dans les couches Feed-Forward :
$$\text{Mix-FFN}(x) = \text{MLP}(\text{GELU}(\text{Conv}_{3×3}(\text{MLP}(x)) + x))$$
Cette hybridation **CNN-Transformer** permet :
- **Capture des patterns locaux** (force des CNN)
- **Compréhension globale** (force des Transformers)
- **Élimination des encodages positionnels** : Contrairement aux ViT classiques qui utilisent des encodages positionnels, SegFormer s'en affranchit totalement
**3. Décodeur MLP Unifié**
SegFormer simplifie drastiquement le décodeur avec un **MLP léger** :
$$\text{Seg} = \text{MLP}\left(\text{Concat}[\text{Upsample}(F_1), F_2, F_3, F_4]\right)$$
**Avantages :**
- **Élimination des décodeurs complexes** (PSP, ASPP)
- **Réduction paramètres** : Focus sur l'encodeur
- **Inférence accélérée** : Décodeur 10× plus rapide
### Comparaison Conceptuelle : SegFormer vs CNN
| Aspect | **CNN (FPN)** | **SegFormer** |
|--------|---------------|----------------|
| **Réceptive Field** | Locale → Globale (graduelle) | **Globale dès L1** |
| **Relations Spatiales** | Convolutions fixes | **Attention adaptative** |
| **Multi-échelles** | Feature Pyramid complexe | **Hiérarchie naturelle** |
| **Paramètres** | Nombreux (convolutions) | **Optimisés (attention)** |
| **Contexte** | Limité (kernels 3×3-5×5) | **Illimité (global)** |
| **Encodage positionnel** | Aucun (spatial intrinsèque) | **Aucun (Mix-FFN)** |
### Mécanisme d'Attention Global
**Force distinctive :** Chaque pixel "voit" tous les autres pixels de l'image simultanément :
$$\text{Output}_i = \sum_{j=1}^{N} \alpha_{ij} \cdot V_j$$
où $\alpha_{ij}$ représente l'**importance du pixel $j$ pour classifier le pixel $i$**.
**Impact :**
- **Classes complexes** (Human, Vehicle) : Gains significatifs vs CNN
- **Cohérence spatiale** : Réduction artefacts de segmentation
- **Robustesse occlusion** : Reconstruction contextuelle
Cette architecture explique la **supériorité empirique** de SegFormer sur les tâches nécessitant une compréhension contextuelle étendue, particulièrement visible sur Cityscapes.
---
## La modélisation
### Méthodologie Expérimentale
**Architecture comparative :**
Nous comparons SegFormer à **FPN** (Feature Pyramid Network), architecture CNN mature représentative des approches traditionnelles, avec deux encodeurs testés : **EfficientNet-B0** et **ResNet34**.
**Configuration expérimentale :**
- **Plateforme** : Google Colab Pro avec GPU T4 (15Go VRAM)
- **Framework** : PyTorch + Segmentation Models PyTorch
- **Contraintes** : Plan payant Google avec unités de calcul limitées
### Preprocessing et Modèles Pré-entraînés
**Résolutions testées :** 512×512 et 768×768 pixels
**Modèles SegFormer (pré-entraînés ADE20K) :**
- **SegFormer-B0** : `smp-hub/segformer-b0-512x512-ade-160k`
- **SegFormer-B1** : `smp-hub/segformer-b1-512x512-ade-160k`
**Modèles FPN (pré-entraînés ImageNet) :**
- **FPN+ResNet34** : `encoder_weights="imagenet"`
- **FPN+EfficientNet-B0** : `encoder_weights="imagenet"`
### Métrique d'Évaluation
**Mean Intersection over Union (mIoU) - Métrique principale :**
L'IoU mesure la précision de segmentation par classe :
$$\text{IoU}_{\text{classe}} = \frac{|\text{Prédiction} \cap \text{Ground Truth}|}{|\text{Prédiction} \cup \text{Ground Truth}|}$$
**Mean IoU (mIoU) :** Moyenne des IoU sur toutes les classes
$$\text{mIoU} = \frac{1}{N} \sum_{i=1}^{N} \text{IoU}_i$$
**Métrique complémentaire :**
- **IoU par classe (IoUc)** : Performance détaillée par méta-classe
### Démarche d'Optimisation
**Configuration SegFormer :**
- **Loss Function** : Jaccard Loss seule
- **Optimiseur** : AdamW (lr=1e-4, weight_decay=1e-4)
- **Scheduler** : Cosine Annealing (min_lr=1e-6)
- **Epochs** : Maximum 30 avec early stopping (patience=5)
**Configuration FPN :**
- **Loss Function** : Focal + Jaccard Loss (focal_weight=1.0, jaccard_weight=1.0)
- **Optimiseur** : Adam (lr=1.5e-4, weight_decay=1e-5)
- **Scheduler** : ReduceLROnPlateau (patience=4, min_lr=1e-6)
- **Epochs** : Maximum 20 avec early stopping (patience=7)
**Stratégie d'entraînement :**
Entraînement séquentiel des 4 modèles à 512×512, puis à 768×768, suivi de la comparaison des métriques entre les deux meilleurs modèles.
---
## Une synthèse des résultats
### Performance Comparative : SegFormer vs Architectures CNN
**Résultats principaux (mIoU sur Cityscapes Validation) :**
*[Insérer ici le graphique Image 1 : "Mean IoU par Expérience" et "Accuracy par Expérience"]*
| **Modèle** | **mIoU** | **Gain vs meilleure baseline** | **Paramètres** | **Taille modèle** | **Temps inférence** |
|------------|----------|-------------------------------|----------------|--------------------|-------------------|
| **SegFormer-B1** | **79.98%** | **+1.31%** | **13.68M** | **52MB** | **1.39s** |
| **FPN+ResNet34** | **79.22%** | **+0.55%** | **23.16M** | **88MB** | **0.73s** |
| **FPN+EfficientNet-B0** | **78.67%** | *baseline* | **5.76M** | **22MB** | **0.48s** |
| **SegFormer-B0** | **77.91%** | **-0.76%** | **3.72M** | **14MB** | **0.87s** |
### Analyse Détaillée par Classes
![IoU Classe et Δ IoU par Classe](iou_delta_iou_per_class.png)
*Fig.1*: IoU et Δ IoU par Classe entre SegFormer-B1 et FPN-ResNet34
**Performance par méta-classes Cityscapes :**
| **Classe** | **SegFormer-B1** | **FPN+ResNet34** | **FPN+EfficientNet-B0** | **SegFormer-B0** | **Meilleur** |
|------------|-------------------|-------------------|-------------------------|------------------|-------------|
| **Flat** | **93.35%** | **93.38%** | **92.73%** | **93.12%** | **FPN+ResNet34** |
| **Human** | **71.13%** | **69.35%** | **68.68%** | **67.45%** | **SegFormer-B1** ⭐ |
| **Vehicle** | **88.80%** | **87.75%** | **87.72%** | **86.87%** | **SegFormer-B1** ⭐ |
| **Construction** | **85.94%** | **85.06%** | **84.92%** | **84.19%** | **SegFormer-B1** ⭐ |
| **Object** | **49.24%** | **48.68%** | **46.71%** | **43.62%** | **SegFormer-B1** ⭐ |
| **Nature** | **89.14%** | **88.13%** | **88.64%** | **88.15%** | **SegFormer-B1** ⭐ |
| **Sky** | **92.35%** | **91.67%** | **91.71%** | **91.37%** | **SegFormer-B1** ⭐ |
| **Void** | **69.89%** | **69.76%** | **68.24%** | **68.51%** | **SegFormer-B1** ⭐ |
**Gains SegFormer-B1 vs FPN+ResNet34 :**
- **Human** : +1.78% (sécurité critique)
- **Vehicle** : +1.05% (objets mobiles)
- **Construction** : +0.88% (structure urbaine)
- **Object** : +0.56% (signalétique)
- **Nature** : +1.01% (environnement)
- **Sky** : +0.68% (contexte global)
- **Void** : +0.13% (zones ambiguës)
- **Flat** : -0.03% (performance équivalente)
### Insights Métier Critiques
**1. Classes Sécurité-Critiques :**
- **Human (+1.78% IoU)** : Amélioration la plus significative avec SegFormer-B1 atteignant 71.13% vs 69.35% pour FPN+ResNet34
- **Vehicle (+1.05% IoU)** : Meilleure identification obstacles mobiles (88.80% vs 87.75%)
- **Impact applicatif** : Réduction risque accidents, fiabilité conduite autonome renforcée
**2. Signalétique et Infrastructure :**
- **Object (+0.56% IoU)** : Panneaux, feux, poteaux mieux segmentés (49.24% vs 48.68%)
- **Construction (+0.88% IoU)** : Amélioration structures urbaines (85.94% vs 85.06%)
- **Conséquences** : Navigation urbaine plus précise, respect signalisation
**3. Efficacité Paramétrique et Déploiement :**
- **SegFormer-B1** : Meilleure performance (79.98% mIoU) avec **41% moins de paramètres** que FPN+ResNet34 (13.68M vs 23.16M) et **41% moins d'espace disque** (52MB vs 88MB)
- **FPN+EfficientNet-B0** : Excellent compromis performance/compacité avec 5.76M paramètres et seulement 22MB de stockage
- **SegFormer-B0** : Modèle ultra-compact (3.72M paramètres, 14MB) mais performances insuffisantes
- **Avantage déploiement** : SegFormer-B1 facilite le déploiement mobile et edge computing avec des contraintes mémoire/stockage réduites
**4. Trade-off Performance vs Vitesse d'Exécution :**
SegFormer-B1 démontre une **supériorité en précision** (+0.76% mIoU) avec un temps d'inférence de 1.39s contre 0.73s pour FPN+ResNet34 (1.92x plus lent) sur **CPU Google Colab**. Cette différence de 668ms par image doit être contextualisée :
- **Limitation CPU** : Les architectures Transformer bénéficient davantage des optimisations GPU spécialisées (TensorRT, CUDA) que les CNN
- **Applications batch** : SegFormer-B1 recommandé pour la précision maximale hors contraintes temps réel
- **Potentiel d'optimisation** : Les temps d'inférence seraient probablement divisés par 3-5x sur hardware dédié avec accélération GPU
### Évolution des Performances
![Matrice de confusion FPN_ResNet34](mIoU_accuracy.png)
*Fig.2*: Évolution de MeanIoU et Accuracy pour les 8 modèles.
### Conclusion Expérimentale
**Analyse nuancée des résultats :** Les résultats montrent que SegFormer-B1 **rivalise** avec les CNN traditionnels dans une configuration d'évaluation statique (images redimensionnées à résolution fixe, quitte à déformer le ratio), avec un gain modeste de +0.76% de mIoU sur FPN+ResNet34.
**Limites méthodologiques identifiées :**
1. **Absence de sliding window** : Contrairement aux auteurs originaux de SegFormer, nous n'avons pas implémenté de sliding window pour couvrir entièrement l'image sans déformer son ratio, ce qui pourrait favoriser les approches CNN plus habituées aux déformations géométriques.
2. **Résolution 512×512** : Les expériences menées à résolution inférieure (512×512) montrent des résultats similaires avec une légère avance pour FPN+ResNet34, suggérant que l'augmentation d'échelle (768×768) a légèrement profité à SegFormer-B1.
3. **Contraintes computationnelles** : Les performances de nos baselines CNN seraient probablement dépassées par les versions plus lourdes de SegFormer (B2, B3, B4) que nous n'avons pas pu entraîner faute de puissance de calcul suffisante.
**ROI technique contextuel :**
- **SegFormer-B1** : Performance comparable aux CNN établis avec une architecture plus moderne
- **SegFormer-B0** : Moins performant que prévu, nécessiterait probablement des ajustements d'hyperparamètres spécifiques
- **Potentiel d'amélioration** : Les versions B2+ de SegFormer promettraient vraisemblablement des gains significatifs
**Facteurs de validation partielle :**
1. **Gains ciblés** : Améliorations cohérentes sur les classes critiques (Human +1.78%, Vehicle +1.05%)
2. **Architecture prometteuse** : Les mécanismes d'attention globale montrent leur potentiel malgré les contraintes expérimentales
3. **Preuve de concept réussie** : Démonstration de la faisabilité technique de l'approche Transformer
Cette preuve de concept valide l'intérêt des architectures Transformer pour la segmentation urbaine, tout en soulignant l'importance des ressources computationnelles et des protocoles d'évaluation appropriés pour exploiter pleinement leur potentiel.
---
## L'analyse de la feature importance globale et locale du nouveau modèle
### Complexité de l'Explicabilité des Transformers
L'analyse d'explicabilité des modèles de deep learning basés sur l'architecture Transformer représente un défi technique considérable, particulièrement pour des tâches de segmentation sémantique dense comme celle étudiée ici. Les méthodes traditionnelles d'explicabilité s'appuient généralement sur les cartes d'attention obtenues ou utilisent des méthodes de propagation heuristiques le long du graphe d'attention, mais ces approches présentent des limitations importantes dans le contexte d'une preuve de concept.
### Approches Disponibles dans la Littérature
Dans le cadre de cette mission d'évaluation technique, une analyse d'explicabilité approfondie dépasse le scope d'une preuve de concept. Néanmoins, les auteurs de SegFormer fournissent des éléments d'analyse via l'**effective receptive field (ERF)** qui permet de visualiser comment l'encodeur produit naturellement des attentions locales dans les couches inférieures (similaires aux convolutions) et des attentions hautement non-locales dans les couches supérieures pour capturer le contexte global.
Cette différence fondamentale avec les CNN explique en partie la supériorité de SegFormer sur les classes nécessitant une compréhension contextuelle étendue (Human, Vehicle, Object) observée dans nos résultats expérimentaux.
### Limitations et Perspectives
Pour une analyse d'explicabilité complète, des méthodes spécialisées comme celle basée sur le **Deep Taylor Decomposition** seraient nécessaires pour propager correctement les scores de pertinence à travers les couches d'attention et les connexions résiduelles. Ces approches, bien que techniquement réalisables, nécessitent des développements spécifiques qui dépassent le cadre temporel d'une preuve de concept.
L'analyse des patterns attentionnels de SegFormer révèle néanmoins que le modèle développe une **"compréhension scénique"** globale des environnements urbains, expliquant sa supériorité sur les classes nécessitant une contextualisation étendue par rapport aux approches CNN traditionnelles.
---
## Limites et améliorations possibles
### Limitations Identifiées
**1. Contraintes Expérimentales**
- **Ressources computationnelles limitées** : Plan payant Google Colab avec 100 unités de calcul, 2/3 des crédits consommés pour l'entraînement sur GPU T4 (entry-level), rendant irréaliste l'entraînement de variantes plus lourdes (SegFormer-B2+) ou l'utilisation de GPU plus puissants
- **Inférence CPU uniquement** : Temps de traitement mesurés sur CPU Colab non-représentatifs des performances GPU optimisées en production
- **Dataset unique** : Généralisation à d'autres environnements urbains non-validée
- **Protocole d'évaluation statique** : Images redimensionnées avec déformation du ratio, contrairement à la méthodologie sliding window du papier original
**2. Limitations Architecturales**
- **Complexité computationnelle** : Attention quadratique, scalabilité limitée pour très hautes résolutions
- **Transfer learning hétérogène** : SegFormer (ADE20K) vs FPN (ImageNet)
**3. Limitations Applicatives**
- **Classes déséquilibrées** : Performance variable sur objets rares
- **Conditions d'acquisition** : Robustesse non-testée (nuit, intempéries)
### Améliorations Prioritaires
**1. Optimisation Computationnelle**
- **Migration GPU + TensorRT** : Réduction estimée ×3-5 temps d'inférence
- **Quantization INT8** : Compression modèle sans perte significative
**2. Validation Étendue**
- **SegFormer-B2/B3 benchmark** : Évaluation ROI performance-coût
- **Multi-dataset validation** : ADE20K + Mapillary Vistas pour robustesse
**3. Données et Robustesse**
- **Synthetic data** : CARLA/GTA pour conditions météo variées
- **Multi-resolution training** : Amélioration adaptabilité résolutions
**Impact estimé :** Performance +2-3% mIoU, temps d'inférence ÷3, robustesse conditions adverses +40%.

Binary file not shown.

58
docs/plan_previsionnel.md Normal file
View file

@ -0,0 +1,58 @@
# Plan Prévisionnel - Preuve de Concept Segmentation Sémantique
**Projet** : Amélioration de la segmentation sémantique urbaine avec SegFormer
---
## Dataset Retenu
## Dataset Retenu
**Cityscapes** : Dataset de référence pour la segmentation sémantique de scènes urbaines comprenant 2975 images haute résolution annotées pixel par pixel. Les images sont capturées dans **50 villes allemandes différentes** avec **34 classes principales** que nous regroupons en **8 catégories pertinentes** (flat, vehicle, human, construction, object, nature, sky, void). La résolution sera progressivement testée à 512×512 puis 768×768 pixels, contre 224×224 dans le projet précédent, pour exploiter pleinement les capacités des architectures modernes.
---
## Modèle Envisagé
### Algorithme : SegFormer
**Justifications du choix :**
SegFormer (Xie et al., 2021) combine l'efficacité des Vision Transformers avec un décodeur MLP simple, atteignant des performances état de l'art sur Cityscapes (**84.0% mIoU avec SegFormer-B5** contre 80.9% pour DeepLabv3+ ResNet-101). Cette architecture résout les limitations des CNN par son contexte global natif et évite les problèmes des Transformers classiques grâce à l'absence d'encodage positionnel fixe. Comparé à notre baseline actuelle FPN + EfficientNetB0 (74.6% mIoU), SegFormer présente un **potentiel d'amélioration de ~10 points mIoU**, particulièrement sur les petits objets critiques pour les classes Human et Object.\
*Note : les résultats de référence utilisent parfois des techniques d'inférence sliding window, différentes de notre protocole à résolution fixe 768×768.*
**Objectif et contexte :**
SegFormer vise la compréhension précise pixel-wise des scènes urbaines. Son architecture hiérarchique capture simultanément détails fins et contexte global, essentiel pour la segmentation d'éléments critiques (piétons, véhicules, signalétique). Le modèle s'applique aux systèmes de perception embarqués nécessitant un équilibre performance/efficacité computationnelle.
---
## Références Bibliographiques
1. **Xie, E., Wang, W., Yu, Z., et al.** (2021). *SegFormer: Simple and Efficient Design for Semantic Segmentation with Transformers*. NeurIPS 2021. [Article de recherche principal]
2. **PyTorch Semantic Segmentation Implementation Guide** - Towards Data Science (2023). *Modern Semantic Segmentation Architectures Comparison*. [Guide pratique d'implémentation]
3. **Cordts, M., Omran, M., Ramos, S., et al.** (2016). *The Cityscapes Dataset for Semantic Urban Scene Understanding*. CVPR 2016. [Dataset de référence]
---
## Explication de la Démarche de Test
### Méthode Baseline
**FPN + EfficientNetB0/ResNet34** : Réentraînement des modèles précédents (initialement 74.6% mIoU en 224×224) aux nouvelles résolutions 512×512 et 768×768. **Migration framework obligatoire** : passage de `segmentation_models` (TensorFlow/Keras) vers `segmentation_models_pytorch` pour accéder aux architectures récentes.
### Méthode de Test SegFormer
**Évaluation comparative progressive** :
1. Test des variantes SegFormer-B0, B1 selon contraintes mémoire GPU
2. Comparaison métriques : Mean IoU, accuracy pixel-wise, analyse par classe
3. Mesure des temps d'inférence pour validation pratique
4. Sélection des **2 meilleurs modèles par famille** (FPN vs SegFormer)
### Preuve de Concept
**Interface Streamlit** déployée sur serveur NAS personnel (CPU N100) permettant :
- Upload d'images urbaines avec **double inférence simultanée** (FPN + SegFormer)
- Comparaison visuelle des masques de segmentation
- Affichage temps d'inférence et métriques de performance
- Téléchargement des modèles optimaux pour usage pratique
**Code réutilisé** : Framework `segmentation_models_pytorch` pour les implémentations. Application originale par le dataset Cityscapes en hautes résolutions et la comparaison architecturale CNN vs Transformer sur notre cas d'usage spécifique.

BIN
docs/plan_previsionnel.pdf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

57
nginx.conf Normal file
View file

@ -0,0 +1,57 @@
# This reverse proxy configuration aims to make calls to different services running under different ports (Streamlit :8501 and FastAPI :8000) by calling only one port : the nginx one (configurable via NGINX_PORT)
# The NGINX_PORT has to be opened (Dockerfile) and used by app.py (streamlit) in order to make right calls.
worker_processes 1;
events { worker_connections 1024; }
http {
# ✅ AJOUT: Limite d'upload augmentée
client_max_body_size 200M; # Permet uploads jusqu'à 200MB
server {
listen ${NGINX_PORT};
# ✅ AJOUT: Timeouts pour gros fichiers
client_body_timeout 60s;
client_header_timeout 60s;
keepalive_timeout 60s;
send_timeout 60s;
location /api/ {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# ✅ AJOUT: Timeouts pour uploads
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
proxy_send_timeout 300s;
}
location / {
proxy_pass http://127.0.0.1:8501/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# ✅ AJOUT: Timeouts pour Streamlit uploads
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
proxy_send_timeout 300s;
}
location /health {
return 200 'OK';
add_header Content-Type text/plain;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

File diff suppressed because one or more lines are too long

671
notebooks/requirements.txt Normal file
View file

@ -0,0 +1,671 @@
absl-py==1.4.0
absolufy-imports==0.3.1
accelerate==1.10.1
aiofiles==24.1.0
aiohappyeyeballs==2.6.1
aiohttp==3.12.15
aiosignal==1.4.0
alabaster==1.0.0
albucore==0.0.24
albumentations==2.0.8
ale-py==0.11.2
alembic==1.16.5
altair==5.5.0
annotated-types==0.7.0
antlr4-python3-runtime==4.9.3
anyio==4.10.0
anywidget==0.9.18
argon2-cffi==25.1.0
argon2-cffi-bindings==25.1.0
array_record==0.8.1
arviz==0.22.0
astropy==7.1.0
astropy-iers-data==0.2025.8.25.0.36.58
astunparse==1.6.3
atpublic==5.1
attrs==25.3.0
audioread==3.0.1
Authlib==1.6.2
autograd==1.8.0
babel==2.17.0
backcall==0.2.0
beartype==0.21.0
beautifulsoup4==4.13.5
betterproto==2.0.0b6
bigframes==2.17.0
bigquery-magics==0.10.3
bleach==6.2.0
blinker==1.9.0
blis==1.3.0
blobfile==3.0.0
blosc2==3.7.2
bokeh==3.7.3
boto3==1.40.21
botocore==1.40.21
Bottleneck==1.4.2
bqplot==0.12.45
branca==0.8.1
Brotli==1.1.0
build==1.3.0
CacheControl==0.14.3
cachetools==5.5.2
catalogue==2.0.10
certifi==2025.8.3
cffi==1.17.1
chardet==5.2.0
charset-normalizer==3.4.3
chex==0.1.90
clarabel==0.11.1
click==8.2.1
cloudpathlib==0.21.1
cloudpickle==3.1.1
cmake==3.31.6
cmdstanpy==1.2.5
colorcet==3.1.0
colorlover==0.3.0
colour==0.1.5
community==1.0.0b1
confection==0.1.5
cons==0.4.7
contourpy==1.3.3
cramjam==2.11.0
cryptography==43.0.3
cuda-python==12.6.2.post1
cudf-cu12 @ https://pypi.nvidia.com/cudf-cu12/cudf_cu12-25.6.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
cudf-polars-cu12==25.6.0
cufflinks==0.17.3
cuml-cu12==25.6.0
cupy-cuda12x==13.3.0
curl_cffi==0.13.0
cuvs-cu12==25.6.1
cvxopt==1.3.2
cvxpy==1.6.7
cycler==0.12.1
cyipopt==1.5.0
cymem==2.0.11
Cython==3.0.12
dask==2025.5.0
dask-cuda==25.6.0
dask-cudf-cu12==25.6.0
databricks-sdk==0.64.0
dataproc-spark-connect==0.8.3
datasets==4.0.0
db-dtypes==1.4.3
dbus-python==1.2.18
debugpy==1.8.15
decorator==4.4.2
defusedxml==0.7.1
diffusers==0.35.1
dill==0.3.8
distributed==2025.5.0
distributed-ucxx-cu12==0.44.0
distro==1.9.0
dlib==19.24.6
dm-tree==0.1.9
docker==7.1.0
docstring_parser==0.17.0
docutils==0.21.2
dopamine_rl==4.1.2
dotenv==0.9.9
duckdb==1.3.2
earthengine-api==1.5.24
easydict==1.13
editdistance==0.8.1
eerepr==0.1.2
einops==0.8.1
en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85
entrypoints==0.4
et_xmlfile==2.0.0
etils==1.13.0
etuples==0.3.10
Farama-Notifications==0.0.4
fastai==2.8.4
fastapi==0.116.1
fastcore==1.8.8
fastdownload==0.0.7
fastjsonschema==2.21.2
fastprogress==1.0.3
fastrlock==0.8.3
fasttransform==0.0.2
ffmpy==0.6.1
filelock==3.19.1
firebase-admin==6.9.0
Flask==3.1.2
flatbuffers==25.2.10
flax==0.10.6
folium==0.20.0
fonttools==4.59.1
frozendict==2.4.6
frozenlist==1.7.0
fsspec==2025.3.0
future==1.0.0
gast==0.6.0
gcsfs==2025.3.0
GDAL==3.8.4
gdown==5.2.0
geemap==0.35.3
geocoder==1.38.1
geographiclib==2.1
geopandas==1.1.1
geopy==2.4.1
gin-config==0.5.0
gitdb==4.0.12
GitPython==3.1.45
glob2==0.7
google==2.0.3
google-adk==1.12.0
google-ai-generativelanguage==0.6.15
google-api-core==2.25.1
google-api-python-client==2.179.0
google-auth==2.38.0
google-auth-httplib2==0.2.0
google-auth-oauthlib==1.2.2
google-cloud-aiplatform==1.110.0
google-cloud-appengine-logging==1.6.2
google-cloud-audit-log==0.3.2
google-cloud-bigquery==3.36.0
google-cloud-bigquery-connection==1.18.3
google-cloud-bigquery-storage==2.32.0
google-cloud-bigtable==2.32.0
google-cloud-core==2.4.3
google-cloud-dataproc==5.21.0
google-cloud-datastore==2.21.0
google-cloud-firestore==2.21.0
google-cloud-functions==1.20.4
google-cloud-language==2.17.2
google-cloud-logging==3.12.1
google-cloud-resource-manager==1.14.2
google-cloud-secret-manager==2.24.0
google-cloud-spanner==3.57.0
google-cloud-speech==2.33.0
google-cloud-storage==2.19.0
google-cloud-trace==1.16.2
google-cloud-translate==3.21.1
google-colab @ file:///colabtools/dist/google_colab-1.0.0.tar.gz
google-crc32c==1.7.1
google-genai==1.31.0
google-generativeai==0.8.5
google-pasta==0.2.0
google-resumable-media==2.7.2
googleapis-common-protos==1.70.0
googledrivedownloader==1.1.0
gradio==5.43.1
gradio_client==1.12.1
graphene==3.4.3
graphql-core==3.2.6
graphql-relay==3.2.0
graphviz==0.21
greenlet==3.2.4
groovy==0.1.2
grpc-google-iam-v1==0.14.2
grpc-interceptor==0.15.4
grpcio==1.74.0
grpcio-status==1.71.2
grpclib==0.4.8
gspread==6.2.1
gspread-dataframe==4.0.0
gunicorn==23.0.0
gym==0.25.2
gym-notices==0.1.0
gymnasium==1.2.0
h11==0.16.0
h2==4.3.0
h5netcdf==1.6.4
h5py==3.14.0
hdbscan==0.8.40
hf-xet==1.1.8
hf_transfer==0.1.9
highspy==1.11.0
holidays==0.79
holoviews==1.21.0
hpack==4.1.0
html5lib==1.1
httpcore==1.0.9
httpimport==1.4.1
httplib2==0.22.0
httpx==0.28.1
httpx-sse==0.4.1
huggingface-hub==0.34.4
humanize==4.13.0
hyperframe==6.1.0
hyperopt==0.2.7
ibis-framework==9.5.0
idna==3.10
imageio==2.37.0
imageio-ffmpeg==0.6.0
imagesize==1.4.1
imbalanced-learn==0.14.0
immutabledict==4.2.1
importlib_metadata==8.7.0
importlib_resources==6.5.2
imutils==0.5.4
inflect==7.5.0
iniconfig==2.1.0
intel-cmplr-lib-ur==2025.2.1
intel-openmp==2025.2.1
ipyevents==2.0.2
ipyfilechooser==0.6.0
ipykernel==6.17.1
ipyleaflet==0.20.0
ipyparallel==8.8.0
ipython==7.34.0
ipython-genutils==0.2.0
ipython-sql==0.5.0
ipytree==0.2.2
ipywidgets==7.7.1
itsdangerous==2.2.0
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.3.0
jax==0.5.3
jax-cuda12-pjrt==0.5.3
jax-cuda12-plugin==0.5.3
jaxlib==0.5.3
jeepney==0.9.0
jieba==0.42.1
Jinja2==3.1.6
jiter==0.10.0
jmespath==1.0.1
joblib==1.5.1
jsonpatch==1.33
jsonpickle==4.1.1
jsonpointer==3.0.0
jsonschema==4.25.1
jsonschema-specifications==2025.4.1
jupyter-client==6.1.12
jupyter-console==6.1.0
jupyter-leaflet==0.20.0
jupyter-server==1.16.0
jupyter_core==5.8.1
jupyter_kernel_gateway @ git+https://github.com/googlecolab/kernel_gateway@b134e9945df25c2dcb98ade9129399be10788671
jupyterlab_pygments==0.3.0
jupyterlab_widgets==3.0.15
jupytext==1.17.2
kaggle==1.7.4.5
kagglehub==0.3.12
keras==3.10.0
keras-hub==0.21.1
keras-nlp==0.21.1
keyring==25.6.0
keyrings.google-artifactregistry-auth==1.1.2
kiwisolver==1.4.9
langchain==0.3.27
langchain-core==0.3.74
langchain-text-splitters==0.3.9
langcodes==3.5.0
langsmith==0.4.16
language_data==1.3.0
launchpadlib==1.10.16
lazr.restfulclient==0.14.4
lazr.uri==1.0.6
lazy_loader==0.4
libclang==18.1.1
libcudf-cu12 @ https://pypi.nvidia.com/libcudf-cu12/libcudf_cu12-25.6.0-py3-none-manylinux_2_28_x86_64.whl
libcugraph-cu12==25.6.0
libcuml-cu12==25.6.0
libcuvs-cu12==25.6.1
libkvikio-cu12==25.6.0
libpysal==4.13.0
libraft-cu12==25.6.0
librmm-cu12==25.6.0
librosa==0.11.0
libucx-cu12==1.18.1
libucxx-cu12==0.44.0
lightgbm @ file:///tmp/lightgbm/LightGBM/dist/lightgbm-4.6.0-py3-none-linux_x86_64.whl
linkify-it-py==2.0.3
llvmlite==0.43.0
locket==1.0.0
logical-unification==0.4.6
lxml==5.4.0
Mako==1.1.3
marisa-trie==1.3.0
Markdown==3.8.2
markdown-it-py==4.0.0
MarkupSafe==3.0.2
matplotlib==3.10.0
matplotlib-inline==0.1.7
matplotlib-venn==1.1.2
mcp==1.13.1
mdit-py-plugins==0.5.0
mdurl==0.1.2
miniKanren==1.0.5
missingno==0.5.2
mistune==3.1.3
mizani==0.13.5
mkl==2025.2.0
ml_dtypes==0.5.3
mlflow==3.3.2
mlflow-skinny==3.3.2
mlflow-tracing==3.3.2
mlxtend==0.23.4
more-itertools==10.7.0
moviepy==1.0.3
mpmath==1.3.0
msgpack==1.1.1
multidict==6.6.4
multipledispatch==1.0.0
multiprocess==0.70.16
multitasking==0.0.12
murmurhash==1.0.13
music21==9.3.0
namex==0.1.0
narwhals==2.2.0
natsort==8.4.0
nbclassic==1.3.1
nbclient==0.10.2
nbconvert==7.16.6
nbformat==5.10.4
ndindex==1.10.0
nest-asyncio==1.6.0
networkx==3.5
nibabel==5.3.2
nltk==3.9.1
notebook==6.5.7
notebook_shim==0.2.4
numba==0.60.0
numba-cuda==0.11.0
numexpr==2.11.0
numpy==2.0.2
nvidia-cublas-cu12==12.6.4.1
nvidia-cuda-cupti-cu12==12.6.80
nvidia-cuda-nvcc-cu12==12.5.82
nvidia-cuda-nvrtc-cu12==12.6.77
nvidia-cuda-runtime-cu12==12.6.77
nvidia-cudnn-cu12==9.10.2.21
nvidia-cufft-cu12==11.3.0.4
nvidia-cufile-cu12==1.11.1.6
nvidia-curand-cu12==10.3.7.77
nvidia-cusolver-cu12==11.7.1.2
nvidia-cusparse-cu12==12.5.4.2
nvidia-cusparselt-cu12==0.7.1
nvidia-ml-py==12.575.51
nvidia-nccl-cu12==2.27.3
nvidia-nvjitlink-cu12==12.6.85
nvidia-nvtx-cu12==12.6.77
nvtx==0.2.13
nx-cugraph-cu12 @ https://pypi.nvidia.com/nx-cugraph-cu12/nx_cugraph_cu12-25.6.0-py3-none-any.whl
oauth2client==4.1.3
oauthlib==3.3.1
omegaconf==2.3.0
openai==1.101.0
opencv-contrib-python==4.12.0.88
opencv-python==4.12.0.88
opencv-python-headless==4.12.0.88
openpyxl==3.1.5
opentelemetry-api==1.36.0
opentelemetry-exporter-gcp-trace==1.9.0
opentelemetry-resourcedetector-gcp==1.9.0a0
opentelemetry-sdk==1.36.0
opentelemetry-semantic-conventions==0.57b0
opt_einsum==3.4.0
optax==0.2.5
optree==0.17.0
orbax-checkpoint==0.11.23
orjson==3.11.2
osqp==1.0.4
packaging==25.0
pandas==2.2.2
pandas-datareader==0.10.0
pandas-gbq==0.29.2
pandas-stubs==2.2.2.240909
pandocfilters==1.5.1
panel==1.7.5
param==2.2.1
parso==0.8.5
parsy==2.1
partd==1.4.2
patsy==1.0.1
peewee==3.18.2
peft==0.17.1
pexpect==4.9.0
pickleshare==0.7.5
pillow==11.3.0
platformdirs==4.3.8
plotly==5.24.1
plotnine==0.14.5
pluggy==1.6.0
plum-dispatch==2.5.7
ply==3.11
polars==1.25.2
pooch==1.8.2
portpicker==1.5.2
preshed==3.0.10
prettytable==3.16.0
proglog==0.1.12
progressbar2==4.5.0
prometheus_client==0.22.1
promise==2.3
prompt_toolkit==3.0.51
propcache==0.3.2
prophet==1.1.7
proto-plus==1.26.1
protobuf==5.29.5
psutil==5.9.5
psycopg2==2.9.10
psygnal==0.14.1
ptyprocess==0.7.0
py-cpuinfo==9.0.0
py4j==0.10.9.7
pyarrow==18.1.0
pyasn1==0.6.1
pyasn1_modules==0.4.2
pycairo==1.28.0
pycocotools==2.0.10
pycparser==2.22
pycryptodomex==3.23.0
pydantic==2.11.7
pydantic-settings==2.10.1
pydantic_core==2.33.2
pydata-google-auth==1.9.1
pydot==3.0.4
pydotplus==2.0.2
PyDrive2==1.21.3
pydub==0.25.1
pyerfa==2.0.1.5
pygame==2.6.1
pygit2==1.18.2
Pygments==2.19.2
PyGObject==3.42.0
PyJWT==2.10.1
pylibcudf-cu12 @ https://pypi.nvidia.com/pylibcudf-cu12/pylibcudf_cu12-25.6.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
pylibcugraph-cu12==25.6.0
pylibraft-cu12==25.6.0
pymc==5.25.1
pynndescent==0.5.13
pynvjitlink-cu12==0.7.0
pynvml==12.0.0
pyogrio==0.11.1
pyomo==6.9.3
PyOpenGL==3.1.10
pyOpenSSL==24.2.1
pyparsing==3.2.3
pyperclip==1.9.0
pyproj==3.7.2
pyproject_hooks==1.2.0
pyshp==2.3.1
PySocks==1.7.1
pyspark==3.5.1
pytensor==2.31.7
pytest==8.4.1
python-apt==0.0.0
python-box==7.3.2
python-dateutil==2.9.0.post0
python-dotenv==1.1.1
python-louvain==0.16
python-multipart==0.0.20
python-slugify==8.0.4
python-snappy==0.7.3
python-utils==3.9.1
pytz==2025.2
pyviz_comms==3.0.6
PyWavelets==1.9.0
PyYAML==6.0.2
pyzmq==26.2.1
raft-dask-cu12==25.6.0
rapids-dask-dependency==25.6.0
rapids-logger==0.1.1
ratelim==0.1.6
referencing==0.36.2
regex==2024.11.6
requests==2.32.4
requests-oauthlib==2.0.0
requests-toolbelt==1.0.0
requirements-parser==0.9.0
rich==13.9.4
rmm-cu12==25.6.0
roman-numerals-py==3.1.0
rpds-py==0.27.0
rpy2==3.5.17
rsa==4.9.1
ruff==0.12.10
s3transfer==0.13.1
safehttpx==0.1.6
safetensors==0.6.2
scikit-image==0.25.2
scikit-learn==1.6.1
scipy==1.16.1
scooby==0.10.1
scs==3.2.8
seaborn==0.13.2
SecretStorage==3.3.3
segmentation_models_pytorch==0.5.0
semantic-version==2.10.0
Send2Trash==1.8.3
sentence-transformers==5.1.0
sentencepiece==0.2.1
sentry-sdk==2.35.0
setuptools==75.2.0
shap==0.48.0
shapely==2.1.1
shellingham==1.5.4
simple-parsing==0.1.7
simplejson==3.20.1
simsimd==6.5.1
six==1.17.0
sklearn-pandas==2.2.0
slicer==0.0.8
smart_open==7.3.0.post1
smmap==5.0.2
sniffio==1.3.1
snowballstemmer==3.0.1
sortedcontainers==2.4.0
soundfile==0.13.1
soupsieve==2.7
soxr==0.5.0.post1
spacy==3.8.7
spacy-legacy==3.0.12
spacy-loggers==1.0.5
spanner-graph-notebook==1.1.7
Sphinx==8.2.3
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
SQLAlchemy==2.0.43
sqlglot==25.20.2
sqlparse==0.5.3
srsly==2.5.1
sse-starlette==3.0.2
stanio==0.5.1
starlette==0.47.3
statsmodels==0.14.5
stringzilla==3.12.6
stumpy==1.13.0
sympy==1.13.3
tables==3.10.2
tabulate==0.9.0
tbb==2022.2.0
tblib==3.1.0
tcmlib==1.4.0
tenacity==8.5.0
tensorboard==2.19.0
tensorboard-data-server==0.7.2
tensorflow==2.19.0
tensorflow-datasets==4.9.9
tensorflow-hub==0.16.1
tensorflow-metadata==1.17.2
tensorflow-probability==0.25.0
tensorflow-text==2.19.0
tensorflow_decision_forests==1.12.0
tensorstore==0.1.76
termcolor==3.1.0
terminado==0.18.1
text-unidecode==1.3
textblob==0.19.0
tf-slim==1.1.0
tf_keras==2.19.0
thinc==8.3.6
threadpoolctl==3.6.0
tifffile==2025.6.11
tiktoken==0.11.0
timm==1.0.19
tinycss2==1.4.0
tokenizers==0.21.4
toml==0.10.2
tomlkit==0.13.3
toolz==0.12.1
torch==2.8.0+cu126
torchao==0.10.0
torchaudio==2.8.0+cu126
torchdata==0.11.0
torchsummary==1.5.1
torchtune==0.6.1
torchvision==0.23.0+cu126
tornado==6.4.2
tqdm==4.67.1
traitlets==5.7.1
traittypes==0.2.1
transformers==4.55.4
treelite==4.4.1
treescope==0.1.10
triton==3.4.0
tsfresh==0.21.0
tweepy==4.16.0
typeguard==4.4.4
typer==0.16.1
types-pytz==2025.2.0.20250809
types-setuptools==80.9.0.20250822
typing-inspection==0.4.1
typing_extensions==4.15.0
tzdata==2025.2
tzlocal==5.3.1
uc-micro-py==1.0.3
ucx-py-cu12==0.44.0
ucxx-cu12==0.44.0
umap-learn==0.5.9.post2
umf==0.11.0
uritemplate==4.2.0
urllib3==2.5.0
uvicorn==0.35.0
vega-datasets==0.9.0
wadllib==1.3.6
wandb==0.21.1
wasabi==1.1.3
watchdog==6.0.0
wcwidth==0.2.13
weasel==0.4.1
webcolors==24.11.1
webencodings==0.5.1
websocket-client==1.8.0
websockets==15.0.1
Werkzeug==3.1.3
wheel==0.45.1
widgetsnbextension==3.6.10
wordcloud==1.9.4
wrapt==1.17.3
wurlitzer==3.1.1
xarray==2025.8.0
xarray-einstats==0.9.1
xgboost==3.0.4
xlrd==2.0.2
xxhash==3.5.0
xyzservices==2025.4.0
yarl==1.20.1
ydf==0.13.0
yellowbrick==1.5
yfinance==0.2.65
zict==3.0.0
zipp==3.23.0
zstandard==0.24.0

52
start.sh Normal file
View file

@ -0,0 +1,52 @@
#!/bin/bash
set -e
# Set default port si non fourni
export NGINX_PORT=${NGINX_PORT:-8080}
export MULTISEG_API_BASE_URL=${MULTISEG_API_BASE_URL:-http://localhost:$NGINX_PORT/api}
echo "=== Multi-Segmentation API Docker Startup ==="
echo "NGINX_PORT: $NGINX_PORT"
echo "API_BASE_URL: $MULTISEG_API_BASE_URL"
# Substitute environment variables dans nginx config
envsubst '${NGINX_PORT}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
# S'assurer que les répertoires nginx sont accessibles
mkdir -p /var/cache/nginx /var/run/nginx /var/log/nginx
chown -R www-data:www-data /var/cache/nginx /var/run/nginx /var/log/nginx
# Créer le dossier uploads temporaire
mkdir -p /tmp/uploads
echo "Starting FastAPI server..."
uvicorn app.main:app --host 0.0.0.0 --port 8000 --loop asyncio &
echo "Starting Streamlit app..."
streamlit run app/streamlit_app.py \
--server.port 8501 \
--server.address 0.0.0.0 \
--server.enableXsrfProtection false \
--server.enableCORS false \
--server.headless true \
&
# Vérifier que FastAPI est prêt
echo "Waiting for FastAPI to be ready..."
until curl -s http://127.0.0.1:8000/health > /dev/null; do
echo " FastAPI not ready yet, waiting..."
sleep 2
done
echo "✅ FastAPI is ready!"
# Vérifier que Streamlit est prêt
echo "Waiting for Streamlit to be ready..."
until curl -s http://127.0.0.1:8501 > /dev/null; do
echo " Streamlit not ready yet, waiting..."
sleep 2
done
echo "✅ Streamlit is ready!"
echo "🚀 Launching NGINX reverse proxy on port $NGINX_PORT..."
nginx -c /etc/nginx/nginx.conf -g 'daemon off;'

0
utils/__init__.py Normal file
View file

741
utils/utils.py Normal file
View file

@ -0,0 +1,741 @@
import os
import json
import tempfile
import numpy as np
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
import segmentation_models_pytorch as smp
import mlflow
import re
import ast
import time
# Visualisation
import matplotlib.patches as mpatches
from matplotlib.colors import ListedColormap
# Preprocessing/Images
from PIL import Image
import torchvision.transforms as transforms
def load_cityscapes_config(config_path="../cityscapes_config.json", verbose=True):
"""
Charge la configuration Cityscapes depuis un fichier JSON.
INCHANGÉ - Compatible PyTorch
"""
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
class_groups = config["class_groups"]
cityscapes_mapping = {int(k): v for k, v in config["cityscapes_mapping"].items()}
id_to_label = {int(k): v for k, v in config["id_to_label"].items()}
group_colors = np.array(config["group_colors"], dtype=np.uint8)
group_names = config["group_names"]
id_to_group = np.zeros(256, dtype=np.uint8)
for id_, group_id in cityscapes_mapping.items():
id_to_group[id_] = group_id
num_classes = len(group_names)
if verbose:
print("== Groupes de classes Cityscapes ==")
for group, classes in class_groups.items():
print(f" - {group} : {classes}")
print("\n== Mapping ID Cityscapes → Groupe ==")
for id_ in sorted(cityscapes_mapping.keys()):
label = id_to_label.get(id_, "Label inconnu")
group_id = cityscapes_mapping[id_]
print(f" ID {id_:2d} ('{label}') → groupe '{group_names[group_id]}' ({group_id})")
print("\n== Liste des groupes et couleurs associées ==")
for i, (name, color) in enumerate(zip(group_names, group_colors)):
print(f" Groupe {i}: {name} - Couleur RGB : {color}")
print(f"\nNombre total de groupes/classes : {num_classes}")
return {
"class_groups": class_groups,
"cityscapes_mapping": cityscapes_mapping,
"id_to_label": id_to_label,
"group_colors": group_colors,
"group_names": group_names,
"id_to_group": id_to_group,
"num_classes": num_classes
}
def map_mask_ids(mask, id_to_group_mapping):
"""
Applique le mapping des IDs Cityscapes vers les groupes
ADAPTÉ PYTORCH : Utilise torch au lieu de tf
"""
if isinstance(mask, np.ndarray):
mask = torch.from_numpy(mask)
if isinstance(id_to_group_mapping, np.ndarray):
if len(id_to_group_mapping) != 256:
raise ValueError(f"id_to_group_mapping doit avoir 256 éléments, reçu: {len(id_to_group_mapping)}")
mapping_tensor = torch.tensor(id_to_group_mapping, dtype=torch.long)
else:
mapping_tensor = torch.tensor(list(id_to_group_mapping), dtype=torch.long)
# S'assurer que les valeurs du masque sont dans la plage valide [0, 255]
mask_clipped = torch.clamp(mask, 0, 255)
mapped_mask = mapping_tensor[mask_clipped.long()]
return mapped_mask
def colorize_mask(mask, group_colors):
"""
Applique la colorisation du masque avec les couleurs définies
INCHANGÉ - Compatible PyTorch (prend numpy en entrée)
"""
if isinstance(mask, torch.Tensor):
mask = mask.numpy()
colored_mask = np.zeros((mask.shape[0], mask.shape[1], 3), dtype=np.uint8)
num_classes = len(group_colors)
for group_id in range(num_classes):
colored_mask[mask == group_id] = group_colors[group_id]
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):
"""
Retourne une fonction qui prétraite à la fois les images et les masques
pour l'inférence, en reproduisant exactement le pipeline CityscapesDataset.
"""
import albumentations as A
from albumentations.pytorch import ToTensorV2
import cv2
import torch
if encoder is None:
mean, std = [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]
else:
mean, std = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]
# Pipeline unifié (gérera image ET masque ensemble)
transform = A.Compose([
A.Resize(height=img_size[0], width=img_size[1]),
A.Normalize(mean=mean, std=std),
ToTensorV2()
])
def preprocess(image_np, mask_np=None):
if mask_np is not None:
# Traitement simultané image + masque
transformed = transform(image=image_np, mask=mask_np)
return transformed['image'], transformed['mask'].long()
else:
# Image seule
transformed = transform(image=image_np)
return transformed['image'], None
return preprocess
# Version simplifiée de preprocess_image_and_mask utilisant la nouvelle get_preprocessing_fn
def preprocess_image_and_mask(image_path, mask_path=None, img_size=(512, 512),
encoder_name=None, id_to_group_mapping=None):
"""
Version simplifiée utilisant get_preprocessing_fn pour la cohérence avec CityscapesDataset
"""
import numpy as np
from PIL import Image
import torch
print(f"🔄 Préprocessing de l'image: {os.path.basename(image_path)}")
# Chargement des données
image_pil = Image.open(image_path).convert('RGB')
image_np = np.array(image_pil)
img_display = np.array(image_pil.resize(img_size, Image.BILINEAR)) # Pour affichage
mask_np = 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')
mask_np = np.array(mask_pil)
print(f" Valeurs uniques avant mapping: {np.unique(mask_np)[:10]}...")
# Utiliser notre preprocessing unifié
preprocess_fn = get_preprocessing_fn(img_size=img_size, encoder=encoder_name)
img_preprocessed, mask_preprocessed = preprocess_fn(image_np, mask_np)
# Appliquer le mapping si fourni ET si on a un masque
if mask_preprocessed is not None and id_to_group_mapping is not None:
mask_preprocessed = map_mask_ids(mask_preprocessed, id_to_group_mapping)
print(f" Valeurs uniques après mapping: {torch.unique(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
def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config,
mask_path=None, save_dir=None, show_stats=True):
"""
Version simplifiée utilisant preprocess_image_and_mask
"""
print(f"=== INFÉRENCE PYTORCH SUR {os.path.basename(image_path)} ===")
# Utiliser la version simplifiée du preprocessing
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)")
# [Le reste du code de visualisation reste identique]
# 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
def load_model(run_id=None, experiment_name="OC Projet 9",
metric="test_mean_iou", top_n=1):
"""
Charge un modèle PyTorch depuis MLflow avec plusieurs méthodes de sélection
Args:
run_id (str, optional): Run ID spécifique (prioritaire si fourni)
experiment_name (str): Nom de l'expérience MLflow
metric (str): Métrique pour classer les runs
top_n (int): Position dans le classement (1=meilleur, 2=deuxième, etc.)
Returns:
tuple: (model, source_run_id, encoder_name, script_img_size)
"""
print(f"\n🔍 Chargement d'un modèle PyTorch depuis MLflow...")
client = mlflow.tracking.MlflowClient()
# 1. Sélection du run selon les paramètres
if run_id:
print(f"🎯 Mode run_id spécifique: {run_id}")
try:
run = mlflow.get_run(run_id)
source_run_id = run_id
print(f"✅ Run trouvé: {run.data.tags.get('mlflow.runName', 'Unknown')}")
except Exception as e:
raise RuntimeError(f"❌ Run {run_id} introuvable: {e}")
else:
print(f"🏆 Mode classement: top_{top_n} de '{metric}' dans '{experiment_name}'")
try:
# Récupérer l'expérience
experiment = client.get_experiment_by_name(experiment_name)
if experiment is None:
raise RuntimeError(f"❌ Expérience '{experiment_name}' non trouvée")
# Rechercher les runs avec la métrique, triés par ordre décroissant
filter_string = f"status = 'FINISHED' and metrics.{metric} >= 0"
order_by = f"metrics.{metric} DESC"
runs_df = mlflow.search_runs(
experiment_ids=[experiment.experiment_id],
filter_string=filter_string,
order_by=[order_by],
max_results=top_n
)
if runs_df.empty:
raise RuntimeError(f"❌ Aucun run avec '{metric}' trouvé dans '{experiment_name}'")
if len(runs_df) < top_n:
raise RuntimeError(f"❌ Seulement {len(runs_df)} runs trouvés, impossible d'obtenir le top_{top_n}")
# Prendre le run à la position top_n
selected_run = runs_df.iloc[top_n - 1]
source_run_id = selected_run['run_id']
metric_value = selected_run[f'metrics.{metric}']
run_name = selected_run.get('tags.mlflow.runName', 'Unknown')
print(f"✅ Run sélectionné (position #{top_n}):")
print(f" • Nom: {run_name}")
print(f" • Run ID: {source_run_id}")
print(f" • {metric}: {metric_value:.4f}")
# Afficher le contexte
print(f"📊 Contexte du classement:")
for i, (_, row) in enumerate(runs_df.head(min(5, len(runs_df))).iterrows()):
indicator = "👑" if i == (top_n - 1) else f"{i+1}."
name = row.get('tags.mlflow.runName', 'Unknown')[:30]
value = row[f'metrics.{metric}']
print(f" {indicator} {name}: {value:.4f}")
run = mlflow.get_run(source_run_id)
except Exception as e:
raise RuntimeError(f"❌ Erreur lors de la sélection: {e}")
# 2-5. [Reste du code identique - extraction params, détection variante, chargement...]
try:
params = run.data.params
encoder_name = params.get("encoder_name", None)
architecture = params.get("architecture", "FPN")
num_classes = int(params.get("num_classes", 8))
script_img_size_raw = params.get("script_img_size", None)
run_name = run.data.tags.get('mlflow.runName', 'Unknown')
# Parser script_img_size
script_img_size = None
if script_img_size_raw:
try:
script_img_size = ast.literal_eval(script_img_size_raw)
if isinstance(script_img_size, (tuple, list)) and len(script_img_size) == 2:
script_img_size = tuple(script_img_size)
except:
script_img_size = None
print(f"\n📋 Paramètres du modèle:")
print(f"🏗️ Architecture: {architecture}")
print(f"🔧 Encodeur: {encoder_name}")
print(f"📊 Nombre de classes: {num_classes}")
print(f"🖼️ Script img size: {script_img_size}")
except Exception as e:
raise RuntimeError(f"❌ Erreur lors de la récupération des paramètres: {e}")
# Détecter la variante SegFormer
def detect_segformer_variant_from_name(run_name, params):
segformer_pattern = r'SegFormer[_-]?b(\d+)'
match = re.search(segformer_pattern, run_name, re.IGNORECASE)
if match:
variant_num = match.group(1)
detected = f"mit_b{variant_num}"
print(f"🔍 Variante détectée: 'SegFormer_b{variant_num}' → {detected}")
return detected
if encoder_name and encoder_name.startswith("mit_b"):
print(f"🔍 Variante depuis paramètres: {encoder_name}")
return encoder_name
b_pattern = r'\bb(\d+)\b'
match = re.search(b_pattern, run_name, re.IGNORECASE)
if match:
variant_num = match.group(1)
detected = f"mit_b{variant_num}"
print(f"🔍 Pattern détecté: 'b{variant_num}' → {detected}")
return detected
print(f"🔍 Aucune variante détectée, utilisation de mit_b0")
return "mit_b0"
detected_variant = detect_segformer_variant_from_name(run_name, params)
# Télécharger et charger le modèle
with tempfile.TemporaryDirectory() as tmpdir:
try:
artifacts = client.list_artifacts(source_run_id)
model_artifact_candidates = [a.path for a in artifacts if a.path.endswith('.pth')]
if not model_artifact_candidates:
raise RuntimeError("❌ Aucun fichier .pth trouvé")
model_artifact = model_artifact_candidates[0]
print(f"⬇️ Téléchargement de {model_artifact}...")
local_path = mlflow.artifacts.download_artifacts(
run_id=source_run_id,
artifact_path=model_artifact,
dst_path=tmpdir
)
checkpoint = torch.load(local_path, map_location='cpu')
if isinstance(checkpoint, dict) and 'state_dict' in checkpoint:
state_dict = checkpoint['state_dict']
else:
state_dict = checkpoint
except Exception as e:
raise RuntimeError(f"❌ Erreur lors du téléchargement: {e}")
# Créer le modèle
try:
is_transformer = any('patch_embed' in key for key in state_dict.keys())
if is_transformer:
print(f"🔧 Création SegFormer ({detected_variant})")
model = smp.Segformer(encoder_name=detected_variant, classes=num_classes)
encoder_name = detected_variant
else:
print("🔧 Création modèle CNN")
if encoder_name is None or encoder_name == "None":
encoder_name = "efficientnet-b0"
if architecture.lower() == "fpn":
model = smp.FPN(encoder_name=encoder_name, classes=num_classes)
elif architecture.lower() == "unet":
model = smp.Unet(encoder_name=encoder_name, classes=num_classes)
elif architecture.lower() == "deeplabv3plus":
model = smp.DeepLabV3Plus(encoder_name=encoder_name, classes=num_classes)
else:
raise ValueError(f"Architecture {architecture} non supportée")
print("🔄 Chargement des poids...")
model.load_state_dict(state_dict, strict=True)
model.eval()
print("✅ Modèle chargé avec succès!")
except Exception as e:
try:
model.load_state_dict(state_dict, strict=False)
model.eval()
print("✅ Chargement partiel réussi!")
except Exception as e2:
raise RuntimeError(f"❌ Erreur lors du chargement: {e2}")
print(f"🎉 Modèle chargé depuis le run {source_run_id}")
return model, source_run_id, encoder_name, script_img_size
def load_best_model_from_registry(model_name):
"""
Charge le meilleur modèle depuis le MLflow Model Registry
(Version refactorisée - wrapper autour de load_model)
Args:
model_name (str): Nom du modèle dans le Registry
Returns:
tuple: (model, source_run_id, encoder_name, script_img_size)
"""
print(f"🏛️ Récupération du modèle '{model_name}' depuis le Registry...")
client = mlflow.tracking.MlflowClient()
try:
# Récupérer la version en Production
versions = client.search_model_versions(f"name='{model_name}'")
prod_versions = [v for v in versions if v.current_stage == "Production"]
if not prod_versions:
raise RuntimeError(f"❌ Aucune version en Production trouvée pour {model_name}")
latest_prod = max(prod_versions, key=lambda v: int(v.version))
print(f"📦 Version Registry: {latest_prod.version} (Stage: {latest_prod.current_stage})")
# Récupérer le source_run_id depuis le tag du Registry
source_run_id = latest_prod.tags.get("source_run_id")
if not source_run_id:
raise RuntimeError(f"❌ Tag 'source_run_id' absent pour la version {latest_prod.version}")
print(f"🔗 Source run détecté: {source_run_id}")
except Exception as e:
raise RuntimeError(f"❌ Erreur lors de l'accès au Registry: {e}")
# Déléguer le chargement à load_model avec le run_id trouvé
print("➡️ Délégation vers load_model...")
return load_model(run_id=source_run_id)
# Fonctions de convenance
def load_best_model(experiment_name="OC Projet 9", metric="test_mean_iou"):
"""Charge le meilleur modèle selon une métrique"""
return load_model(experiment_name=experiment_name, metric=metric, top_n=1)
def load_second_best_model(experiment_name="OC Projet 9", metric="test_mean_iou"):
"""Charge le deuxième meilleur modèle selon une métrique"""
return load_model(experiment_name=experiment_name, metric=metric, top_n=2)
def load_model_by_run_id(run_id):
"""Charge un modèle par run_id spécifique"""
return load_model_by_param(run_id=run_id)