Project 9 added.
106
.dockerignore
Normal 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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
./mlruns*
|
||||
43
Dockerfile
Normal 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
|
|
@ -0,0 +1,3 @@
|
|||
# Projet 9
|
||||
|
||||
Documentation à venir
|
||||
427
app/main.py
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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"]
|
||||
}
|
||||
47
docker-compose.yml.example
Normal 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
|
||||
21162
docs/Cordts_The_Cityscapes_Dataset_CVPR_2016_paper.pdf
Normal file
BIN
docs/fpn_resnet34_confusion_matrix.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
docs/fpn_resnet34_training_curves.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
docs/iou_delta_iou_per_class.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/mIoU_accuracy.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
323
docs/note_methodologique.md
Normal 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
|
||||
|
||||

|
||||
*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
|
||||
|
||||

|
||||
*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%.
|
||||
BIN
docs/note_methodologique.pdf
Normal file
58
docs/plan_previsionnel.md
Normal 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
BIN
docs/segformer_b1_confusion_matrix.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
docs/segformer_b1_training_curves.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
57
nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
notebooks/content/data/new_images_to_predict/paris_street.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
1
notebooks/p9_notebook.ipynb
Normal file
671
notebooks/requirements.txt
Normal 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
|
|
@ -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
741
utils/utils.py
Normal 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)
|
||||
|
||||
|
||||