API development finished + Docker is working locally

This commit is contained in:
Grégoire Mureau 2025-08-13 11:46:15 +02:00
parent 6a4a546f7b
commit 763f2ec538
15 changed files with 2384 additions and 37 deletions

106
.dockerignore Normal file
View file

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

41
.env.example Normal file
View file

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

43
Dockerfile Normal file
View file

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

202
README.MD
View file

@ -10,17 +10,24 @@ Le système s'inscrit dans une chaîne complète de vision par ordinateur embarq
```
├── notebooks/
│ ├── p8_notebook.py # Notebook principal d'entraînement des modèles
│ ├── requirements.txt # Dépendances pour l'entraînement et expérimentation
│ ├── p8_notebook.ipynb # Notebook principal d'entraînement des modèles
│ └── content/
│ └── data/
│ ├── new_images_to_predict/ # Images diverses pour tests d'inférence
│ └── test_images_sample/ # Échantillon Cityscapes avec masques
├── utils/
│ └── utils.py # Fonctions communes (prétraitement, chargement, inférence)
├── api/ # [En développement] API FastAPI
│ └── requirements.txt # Dépendances légères pour l'API uniquement
├── .env # Variables d'environnement (MLFlow, MinIO)
├── app/ # API et Interface Web
│ ├── main.py # API FastAPI avec routes de segmentation
│ ├── streamlit_app.py # Interface web interactive
│ └── requirements.txt # Dépendances pour l'API et l'interface
├── cityscapes_config.json # Configuration des classes Cityscapes
├── .env.example # Template de variables d'environnement
├── Dockerfile # Configuration Docker
├── docker-compose.yml.example # Template Docker Compose
├── nginx.conf # Configuration reverse proxy
├── start.sh # Script de démarrage des services
├── .dockerignore # Optimisation du build Docker
└── README.md
```
@ -29,8 +36,9 @@ Le système s'inscrit dans une chaîne complète de vision par ordinateur embarq
**État actuel du développement :**
- ✅ **Entraînement des modèles** : Complet (notebook + MLFlow + MinIO)
- ✅ **Fonctions d'inférence** : Développées et prêtes
- 🔄 **API FastAPI** : En cours de développement
- 🔄 **Déploiement** : Conteneurisation Docker + NAS personnel
- ✅ **API FastAPI** : API complète avec endpoints de segmentation
- ✅ **Interface Web** : Interface Streamlit pour upload et visualisation
- ✅ **Déploiement** : Conteneurisation Docker complète + reverse proxy nginx
## 🏷️ Classes de Segmentation
@ -83,7 +91,9 @@ Utilisation de la bibliothèque **[segmentation_models](https://github.com/qubve
- **Architectures** : [segmentation_models](https://github.com/qubvel/segmentation_models) (qubvel)
- **Gestion des expériences** : MLFlow
- **Stockage des artefacts** : MinIO (S3-compatible)
- **API** : FastAPI *(en développement)*
- **API** : FastAPI avec validation Pydantic
- **Interface** : Streamlit pour l'interaction utilisateur
- **Reverse Proxy** : nginx pour routage API/Interface
- **Déploiement** : Docker + NAS personnel *(solutions souveraines)*
## 📊 Dataset
@ -95,46 +105,174 @@ Utilisation de la bibliothèque **[segmentation_models](https://github.com/qubve
## 🚀 Installation et Configuration
### Installation
### Option 1: Déploiement Docker (Recommandé)
#### Configuration rapide
```bash
# Cloner le projet
git clone https://github.com/GrgMdmn/OpenClassrooms_AI_Projects.git
cd OpenClassrooms_AI_Projects/Project_8
# Pour l'expérimentation et l'entraînement
# Copier et adapter la configuration
cp .env.example .env
cp docker-compose.yml.example docker-compose.yml
# Éditer les variables d'environnement
nano .env
```
#### Variables d'environnement requises
```bash
# Configuration du serveur
NGINX_PORT=8080
# Configuration MLflow
MLFLOW_TRACKING_URI=http://localhost:5000
MLFLOW_S3_ENDPOINT_URL=http://localhost:9000
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
# URL de base de l'API (pour Streamlit)
MULTISEG_API_BASE_URL=http://localhost:8080/api
```
#### Déploiement
```bash
# Construction et lancement
docker-compose up --build
# Ou en arrière-plan
docker-compose up -d --build
# Accès à l'application
# Interface web: http://localhost:8080
# API docs: http://localhost:8080/api/docs
# Health check: http://localhost:8080/health
```
#### Test local sans docker-compose
```bash
# Lancement avec port personnalisé
docker build -t multiseg-api .
docker run -p 3000:3000 \
-e NGINX_PORT=3000 \
-e MLFLOW_TRACKING_URI=http://host.docker.internal:5000 \
-e MULTISEG_API_BASE_URL=http://localhost:3000/api \
multiseg-api
```
### Option 2: Installation Locale (Développement)
#### Pour l'expérimentation et l'entraînement
```bash
cd notebooks
pip install -r requirements.txt
# Pour l'API uniquement (plus léger)
cd api
pip install -r requirements.txt
```
### Configuration MLFlow et MinIO
Créer un fichier `.env` à la racine du projet :
#### Pour l'API uniquement
```bash
MLFLOW_TRACKING_URI=http://localhost:5000 # mlflow url
MLFLOW_S3_ENDPOINT_URL=http://localhost:9000 # minio url
AWS_ACCESS_KEY_ID=your_access_key # minio login
AWS_SECRET_ACCESS_KEY=your_secret_key # minio password
cd app
pip install -r requirements.txt
# Lancement manuel des services
# Terminal 1: FastAPI
uvicorn app.main:app --host 0.0.0.0 --port 8000
# Terminal 2: Streamlit
streamlit run app/streamlit_app.py --server.port 8501
```
Le chargement des variables se fait via `python-dotenv` dans le notebook et l'API.
## 🌐 Utilisation de l'API
## 🌐 API et Déploiement
### Endpoints Principaux
### API FastAPI (En développement)
L'API utilisera les fonctions de `utils.py` et sera déployée via Docker sur NAS personnel pour maintenir la souveraineté des données.
#### Segmentation d'image
```bash
# Upload et segmentation via curl
curl -X POST "http://localhost:8080/api/segment" \
-F "file=@image.jpg" \
-F "model_name=unet_mobilenetv2" \
-F "confidence_threshold=0.5"
```
#### Informations sur les modèles
```bash
# Liste des modèles disponibles
curl "http://localhost:8080/api/models"
# Détails d'un modèle
curl "http://localhost:8080/api/models/unet_mobilenetv2"
```
#### Monitoring
```bash
# Health check
curl "http://localhost:8080/health"
# Métriques API
curl "http://localhost:8080/api/metrics"
```
### Interface Web
L'interface Streamlit accessible sur `http://localhost:8080` permet :
- **Upload d'images** par glisser-déposer ou sélection
- **Sélection du modèle** parmi les modèles disponibles
- **Ajustement des paramètres** (seuil de confiance, etc.)
- **Visualisation** des résultats avec overlay des segmentations
- **Téléchargement** des masques de segmentation
## 🔧 Architecture de Déploiement
### Structure des Services
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ nginx:8080 │────│ FastAPI:8000 │────│ MLFlow/MinIO │
│ (Reverse Proxy)│ │ (API Backend) │ │ (Model Storage) │
└─────────┬───────┘ └──────────────────┘ └─────────────────┘
│
┌─────────▼───────┐
│ Streamlit:8501 │
│ (Web Interface) │
└─────────────────┘
```
### Volumes et Persistance
- **Modèles** : Téléchargés depuis MLFlow/MinIO au démarrage
- **Uploads temporaires** : Stockage en mémoire (pas de persistance)
- **Logs** : Accessibles via `docker-compose logs`
### Monitoring et Logs
```bash
# Logs en temps réel
docker-compose logs -f
# Logs d'un service spécifique
docker-compose logs -f multiseg-api
# Métriques de performance
curl http://localhost:8080/api/metrics
```
## 🔮 Améliorations Futures
- **Prétraitement adaptatif** : Prétraitement spécifique à chaque backbone au lieu de la normalisation générique (division par 255)
- **Généralisation des conditions** : Extension au-delà des conditions urbaines de Cityscapes (météo, environnements variés)
- **Optimisation temps réel** : Évaluation et amélioration des performances d'inférence
- **Résolution variable** : Support de tailles d'images variables au-delà du format fixe 224x224
- **Transfer learning ciblé** : Utilisation de backbones pré-entraînés sur Mapillary Vistas plutôt qu'ImageNet pour un meilleur alignement domaine
- **Segmentation des personnes** : Amélioration de l'IoU sur les humains via data augmentation (IA générative)
- **Limitation du dataset** : Les performances similaires entre modèles suggèrent une limite liée à la qualité/diversité des données d'entraînement
### Modèles et Performance
- **Prétraitement adaptatif** : Prétraitement spécifique à chaque backbone au lieu de la normalisation générique
- **Optimisation temps réel** : Amélioration des performances d'inférence pour déploiement embarqué
- **Résolution d'entraînement améliorée** : L'API accepte toutes les résolutions d'images, mais l'entraînement a été effectué sur des images redimensionnées à 224x224. Un réentraînement sur des résolutions supérieures (512x512, 1024x1024) pourrait améliorer la précision en capturant plus de détails fins, au prix d'une augmentation des besoins en puissance de calcul
- **Transfer learning ciblé** : Utilisation de backbones pré-entraînés sur Mapillary Vistas
### Infrastructure et Déploiement
- **Scaling horizontal** : Support Kubernetes pour déploiement multi-instances
- **Cache intelligent** : Mise en cache des prédictions pour images similaires
- **API Gateway** : Intégration d'un gateway pour authentification et rate limiting
- **Monitoring avancé** : Intégration Prometheus/Grafana pour métriques détaillées
### Données et Généralisation
- **Généralisation des conditions** : Extension au-delà des conditions urbaines de Cityscapes
- **Segmentation des personnes** : Amélioration de l'IoU via data augmentation (IA générative)
- **Datasets additionnels** : Intégration de datasets complémentaires pour robustesse
---
*Projet développé dans une approche de souveraineté technologique, privilégiant les solutions open-source et l'hébergement sur infrastructure personnelle.*
*Projet développé dans une approche de souveraineté technologique, privilégiant les solutions open-source et l'hébergement sur infrastructure personnelle.*

427
app/main.py Normal file
View file

@ -0,0 +1,427 @@
import os
import sys
import tempfile
import shutil
from datetime import datetime
from typing import Optional, List
from pathlib import Path
import mlflow
from fastapi import FastAPI, HTTPException, File, UploadFile, Form
from fastapi.responses import FileResponse
from pydantic import BaseModel
import numpy as np
import pickle
import base64
# Configuration MLflow avec variables d'environnement
def configure_mlflow():
"""Configure MLflow avec les variables d'environnement"""
print("=== DEBUG VARIABLES D'ENVIRONNEMENT ===")
# ✅ NETTOYAGE automatique des variables d'environnement
mlflow_tracking_uri = os.getenv("MLFLOW_TRACKING_URI", "").strip()
mlflow_s3_endpoint_url = os.getenv("MLFLOW_S3_ENDPOINT_URL", "").strip()
aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID", "").strip()
aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY", "").strip()
print(f"MLFLOW_TRACKING_URI: {mlflow_tracking_uri}")
print(f"MLFLOW_S3_ENDPOINT_URL: {mlflow_s3_endpoint_url}")
print(f"AWS_ACCESS_KEY_ID: {aws_access_key_id}")
print(f"AWS_SECRET_ACCESS_KEY: {'***' if aws_secret_access_key else None} ({'set' if aws_secret_access_key else 'not set'})")
print("============================================")
if not mlflow_tracking_uri:
raise ValueError("MLFLOW_TRACKING_URI not found in environment variables")
# Configuration MLflow avec URLs nettoyées
mlflow.set_tracking_uri(mlflow_tracking_uri)
print(f"MLflow Tracking URI: {mlflow_tracking_uri}")
# ✅ CORRECTION: Configuration EXPLICITE des identifiants avec nettoyage
if not mlflow_s3_endpoint_url or not aws_access_key_id or not aws_secret_access_key:
raise ValueError("Variables d'environnement MLflow manquantes (S3_ENDPOINT, ACCESS_KEY, SECRET_KEY)")
os.environ["MLFLOW_S3_ENDPOINT_URL"] = mlflow_s3_endpoint_url
os.environ["AWS_ACCESS_KEY_ID"] = aws_access_key_id
os.environ["AWS_SECRET_ACCESS_KEY"] = aws_secret_access_key
print(f"MLflow S3 Endpoint: {mlflow_s3_endpoint_url}")
print("✅ Identifiants AWS configurés")
# ✅ SKIP set_experiment pour éviter le problème 404
# mlflow.set_experiment("OC Projet 8") # Commenté temporairement
print("✅ Configuration MLflow terminée (sans set_experiment)")
# Import des fonctions utils
sys.path.append('.')
from utils.utils import (
load_cityscapes_config,
load_best_model_from_registry,
run_inference_and_visualize,
colorize_mask
)
# ✅ AJOUT: Import segmentation_models pour le chargement des modèles
try:
import segmentation_models as sm
print("✅ segmentation_models importé - modèles compatibles")
except ImportError as e:
print(f"⚠️ segmentation_models non disponible: {e}")
print("💡 Installez avec: pip install git+https://github.com/qubvel/segmentation_models.git")
# Configuration
MODEL_NAME = "StreetsSegmentation"
SAMPLE_IMAGES_DIR = "./notebooks/content/data/test_images_sample"
CITYSCAPES_CONFIG_PATH = "./cityscapes_config.json"
TEMP_UPLOAD_DIR = "/tmp/uploads"
# Modèles de données
class ImageInfo(BaseModel):
filename: str
display_name: str
has_ground_truth: bool
class PredictionResult(BaseModel):
success: bool
message: str
image_path: Optional[str] = None
mask_path: Optional[str] = None
prediction_stats: Optional[dict] = None
ground_truth_available: bool = False
figure_data: Optional[str] = None
class AvailableImagesResponse(BaseModel):
images: List[ImageInfo]
total_count: int
# Variables globales pour le modèle
model = None
encoder_name = None
script_img_size = None
mapping_config = None
run_id = None
def initialize_model():
"""Initialise le modèle et la configuration au démarrage"""
global model, encoder_name, script_img_size, mapping_config, run_id
try:
# Configuration MLflow
print("🔄 Configuration de MLflow...")
configure_mlflow()
print("🔄 Chargement de la configuration Cityscapes...")
mapping_config = load_cityscapes_config(CITYSCAPES_CONFIG_PATH, verbose=False)
print("✅ Configuration Cityscapes chargée")
print("🔄 Chargement du modèle depuis MLflow...")
print(f"🔍 Recherche du modèle: {MODEL_NAME}")
model, run_id, encoder_name, script_img_size = load_best_model_from_registry(MODEL_NAME)
print("✅ Modèle téléchargé depuis MLflow")
print(f"✅ Modèle chargé du run {run_id}")
print(f"📐 Encoder: {encoder_name}")
print(f"📐 Input shape: {script_img_size}")
print(f"🎨 Nombre de classes: {mapping_config['num_classes']}")
# Créer le dossier temporaire
os.makedirs(TEMP_UPLOAD_DIR, exist_ok=True)
except Exception as e:
print(f"❌ Erreur lors de l'initialisation: {e}")
import traceback
traceback.print_exc()
raise e
# Initialisation de l'API
app = FastAPI(
title="Multi-Class Segmentation API",
description="API de segmentation sémantique pour véhicules autonomes",
version="1.0.0"
)
# ✅ AJOUT: Initialiser le modèle de manière plus robuste
@app.on_event("startup")
async def startup_event():
"""Initialise le modèle au démarrage de l'API"""
try:
initialize_model()
print("🚀 API démarrée avec succès - Modèle chargé!")
except Exception as e:
print(f"⚠️ ATTENTION: Impossible de charger le modèle au démarrage: {e}")
print("L'API démarre quand même mais les prédictions ne fonctionneront pas.")
@app.get("/")
def root():
return {
"message": "Multi-Class Segmentation API - Online",
"model_loaded": model is not None,
"model_run_id": run_id,
"encoder": encoder_name,
"num_classes": mapping_config['num_classes'] if mapping_config else None
}
@app.get("/health")
def health_check():
"""Health check endpoint pour nginx"""
if model is None or mapping_config is None:
raise HTTPException(status_code=503, detail="Model not loaded")
return {
"status": "healthy",
"model_loaded": True,
"timestamp": datetime.utcnow().isoformat()
}
@app.get("/models", response_model=dict)
def get_model_info():
"""Retourne les informations du modèle chargé"""
if model is None:
raise HTTPException(status_code=503, detail="Model not loaded")
return {
"model_name": MODEL_NAME,
"run_id": run_id,
"encoder": encoder_name,
"input_size": script_img_size,
"num_classes": mapping_config['num_classes'],
"class_names": mapping_config['group_names'],
"class_colors": mapping_config['group_colors'].tolist()
}
@app.get("/sample-images", response_model=AvailableImagesResponse)
def get_sample_images():
"""Retourne la liste des images d'exemple disponibles"""
try:
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
mask_dir = Path(SAMPLE_IMAGES_DIR) / "mask"
if not original_dir.exists():
raise HTTPException(status_code=404, detail="Sample images directory not found")
images = []
for img_path in original_dir.glob("*leftImg8bit.png"):
# Construire le nom du masque correspondant
base_name = img_path.stem.replace("_leftImg8bit", "")
mask_name = f"{base_name}_gtFine_labelIds.png"
mask_path = mask_dir / mask_name
# Créer un nom d'affichage plus convivial
display_name = base_name.replace("_", " ").title()
images.append(ImageInfo(
filename=img_path.name,
display_name=display_name,
has_ground_truth=mask_path.exists()
))
# Trier par nom pour un affichage cohérent
images.sort(key=lambda x: x.filename)
return AvailableImagesResponse(
images=images,
total_count=len(images)
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error listing sample images: {str(e)}")
@app.get("/sample-image/{filename}")
def get_sample_image(filename: str):
"""Sert une image d'exemple pour prévisualisation"""
try:
print(f"🔍 DEBUG: Demande d'image: {filename}")
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
image_path = original_dir / filename
print(f"🔍 DEBUG: Chemin construit: {image_path}")
print(f"🔍 DEBUG: Fichier existe: {image_path.exists()}")
if not image_path.exists():
print(f"❌ DEBUG: Image non trouvée: {image_path}")
raise HTTPException(status_code=404, detail=f"Sample image {filename} not found")
# Vérifier que c'est bien un fichier image attendu (sécurité)
if not filename.endswith('_leftImg8bit.png'):
print(f"❌ DEBUG: Format invalide: {filename}")
raise HTTPException(status_code=400, detail="Invalid image filename format")
print(f"✅ DEBUG: Envoi de l'image: {filename}")
return FileResponse(
image_path,
media_type="image/png",
headers={"Content-Disposition": f"inline; filename={filename}"}
)
except HTTPException:
# Re-raise les HTTPException pour qu'elles soient gérées correctement
raise
except Exception as e:
print(f"❌ Erreur lors du service de l'image {filename}: {e}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Error serving image: {str(e)}")
class SampleImageRequest(BaseModel):
filename: str
@app.post("/predict-sample", response_model=PredictionResult)
def predict_sample_image(request: SampleImageRequest):
"""Lance une prédiction sur une image d'exemple"""
if model is None or mapping_config is None:
raise HTTPException(status_code=503, detail="Model not loaded")
try:
# Vérifier que l'image existe
original_dir = Path(SAMPLE_IMAGES_DIR) / "original"
image_path = original_dir / request.filename
if not image_path.exists():
raise HTTPException(status_code=404, detail=f"Sample image {request.filename} not found")
# Construire le chemin du masque de vérité
base_name = image_path.stem.replace("_leftImg8bit", "")
mask_name = f"{base_name}_gtFine_labelIds.png"
mask_path = Path(SAMPLE_IMAGES_DIR) / "mask" / mask_name
mask_path_str = str(mask_path) if mask_path.exists() else None
print(f"🔮 Lancement de l'inférence sur {request.filename}")
# Lancer l'inférence (sans affichage pour l'API)
predicted_mask, fig = run_inference_and_visualize(
image_path=str(image_path),
mask_path=mask_path_str,
model=model,
encoder_name=encoder_name,
img_size=script_img_size,
mapping_config=mapping_config,
save_dir=None,
show_stats=False,
show_plot=False # ← AJOUTÉ pour récupérer la figure
)
# Sérialiser la figure
fig_bytes = pickle.dumps(fig)
fig_b64 = base64.b64encode(fig_bytes).decode()
# Calculer les statistiques
unique_classes, counts = np.unique(predicted_mask, return_counts=True)
total_pixels = predicted_mask.size
stats = {}
for class_id, count in zip(unique_classes, counts):
if class_id < len(mapping_config['group_names']):
percentage = (count / total_pixels) * 100
stats[mapping_config['group_names'][class_id]] = {
"pixels": int(count),
"percentage": round(percentage, 1)
}
return PredictionResult(
success=True,
message="Prediction completed successfully",
image_path=str(image_path),
mask_path=mask_path_str,
prediction_stats=stats,
ground_truth_available=mask_path.exists(),
figure_data=fig_b64
)
except Exception as e:
print(f"❌ Erreur lors de la prédiction: {e}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Prediction error: {str(e)}")
@app.post("/predict-upload", response_model=PredictionResult)
def predict_uploaded_image(file: UploadFile = File(...)):
"""Lance une prédiction sur une image uploadée"""
if model is None or mapping_config is None:
raise HTTPException(status_code=503, detail="Model not loaded")
# Vérifier le type de fichier
allowed_types = ["image/jpeg", "image/jpg", "image/png"]
if file.content_type not in allowed_types:
raise HTTPException(
status_code=400,
detail=f"Invalid file type. Allowed: {', '.join(allowed_types)}"
)
temp_file_path = None
try:
# Créer un fichier temporaire
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
temp_filename = f"upload_{timestamp}_{file.filename}"
temp_file_path = os.path.join(TEMP_UPLOAD_DIR, temp_filename)
# Sauvegarder le fichier uploadé
with open(temp_file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
print(f"🔮 Lancement de l'inférence sur {file.filename}")
# Lancer l'inférence (sans masque de vérité)
predicted_mask, fig = run_inference_and_visualize(
image_path=temp_file_path,
mask_path=None, # Pas de masque de vérité pour les uploads
model=model,
encoder_name=encoder_name,
img_size=script_img_size,
mapping_config=mapping_config,
save_dir=None,
show_stats=False,
show_plot=False # ← AJOUTÉ pour récupérer la figure
)
# Sérialiser la figure
fig_bytes = pickle.dumps(fig)
fig_b64 = base64.b64encode(fig_bytes).decode()
# Calculer les statistiques
unique_classes, counts = np.unique(predicted_mask, return_counts=True)
total_pixels = predicted_mask.size
stats = {}
for class_id, count in zip(unique_classes, counts):
if class_id < len(mapping_config['group_names']):
percentage = (count / total_pixels) * 100
stats[mapping_config['group_names'][class_id]] = {
"pixels": int(count),
"percentage": round(percentage, 1)
}
return PredictionResult(
success=True,
message="Prediction completed successfully",
image_path=temp_file_path,
prediction_stats=stats,
ground_truth_available=False,
figure_data=fig_b64
)
except Exception as e:
print(f"❌ Erreur lors de la prédiction: {e}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Prediction error: {str(e)}")
finally:
# Nettoyer le fichier temporaire
if temp_file_path and os.path.exists(temp_file_path):
try:
os.remove(temp_file_path)
print(f"🗑️ Fichier temporaire supprimé: {temp_filename}")
except Exception as e:
print(f"⚠️ Impossible de supprimer le fichier temporaire: {e}")
@app.get("/metrics")
def get_metrics():
"""Endpoint de métriques pour monitoring"""
return {
"model_loaded": model is not None,
"model_name": MODEL_NAME,
"run_id": run_id,
"num_classes": mapping_config['num_classes'] if mapping_config else None,
"timestamp": datetime.utcnow().isoformat()
}

141
app/requirements.txt Normal file
View file

@ -0,0 +1,141 @@
# =============================================================================
# Multi-Class Segmentation API - Requirements
# =============================================================================
# ===== CORE WEB FRAMEWORK =====
fastapi==0.115.14
uvicorn==0.35.0
starlette==0.46.2
pydantic==2.11.7
pydantic_core==2.33.2
# ===== STREAMLIT =====
streamlit==1.46.1
altair==5.5.0 # Utilisé par Streamlit pour les graphiques
blinker==1.9.0 # Streamlit dependency
# ===== HTTP REQUESTS =====
requests==2.32.4
httpx==0.28.1 # Modern HTTP client (utilisé par FastAPI)
httpcore==1.0.9
h11==0.16.0
# ===== MACHINE LEARNING CORE =====
tensorflow==2.19.0
keras==3.10.0
numpy>=1.24.0,<2.0.0
# Note: tensorflow-addons n'est plus nécessaire avec TensorFlow 2.19+
# Les fonctionnalités sont maintenant intégrées dans TensorFlow core
# ===== COMPUTER VISION =====
pillow==11.3.0
opencv-python-headless==4.9.0.80 # Pas dans votre freeze, mais nécessaire pour Docker
scikit-image==0.25.2
imageio==2.37.0
# ===== DATA SCIENCE =====
pandas==2.3.0
matplotlib==3.10.3
scipy==1.16.0
scikit-learn==1.7.0
# ===== MLFLOW & MODEL REGISTRY =====
mlflow==3.2.0
mlflow-skinny==3.2.0
mlflow-tracing==3.2.0
# ===== AWS/S3 SUPPORT =====
boto3==1.39.2
botocore==1.39.2
s3transfer==0.13.0
# ===== SEGMENTATION MODELS =====
# Option 1: Version figée par commit (RECOMMANDÉE pour production)
segmentation_models @ git+https://github.com/qubvel/segmentation_models.git@5d24bbfb28af6134e25e2c0b79e7727f6c0491d0
# Option 2: Si vous préférez une version PyPI (décommentez et commentez la ligne du dessus)
# segmentation-models==1.0.1
# ===== SUPPORTING LIBRARIES =====
# TensorFlow ecosystem
tensorboard==2.19.0
tensorboard-data-server==0.7.2
protobuf==3.20.3
flatbuffers==25.2.10
h5py==3.14.0
astunparse==1.6.3
gast==0.6.0
google-pasta==0.2.0
keras-applications==1.0.8
opt_einsum==3.4.0
termcolor==2.5.0
wrapt==1.17.2
# Data processing
networkx==3.5
tifffile==2025.6.11
lazy_loader==0.4
# Utilities
click==8.2.1
tqdm==4.67.1
joblib==1.5.1
python-dateutil
packaging
typing_extensions==4.14.0
# File handling
python-multipart==0.0.20 # Pour FastAPI file uploads
# ===== AJOUTS SPÉCIFIQUES POUR UTILS/UTILS.PY =====
# Pour la gestion des couleurs et légendes matplotlib
cycler==0.12.1 # Pour matplotlib customization
# Font handling pour matplotlib (nécessaire dans Docker)
fonttools==4.58.5
# ===== REMOVED FROM ORIGINAL =====
# Les packages suivants ont été supprimés car non utilisés dans votre app:
# - awsebcli (AWS CLI - pas nécessaire dans le container)
# - blessed, fabric, invoke (outils dev)
# - jupyter, ipython, spyder (dev environment)
# - nltk (pas d'NLP dans votre app)
# - pytest (tests - à garder seulement pour dev)
# - flask, gunicorn (vous utilisez FastAPI+uvicorn)
# - docker (pas besoin dans le container)
# - git* packages (dev tools)
# =============================================================================
# VERSION ALTERNATIVE - PLUS MINIMALISTE
# =============================================================================
# Si vous voulez une version encore plus légère, voici les STRICTEMENT nécessaires:
# fastapi==0.115.14
# uvicorn==0.35.0
# streamlit==1.46.1
# tensorflow==2.19.0
# numpy==2.1.3
# pillow==11.3.0
# matplotlib==3.10.3
# pandas==2.3.0
# requests==2.32.4
# mlflow==3.2.0
# boto3==1.39.2
# segmentation_models @ git+https://github.com/qubvel/segmentation_models.git@5d24bbfb28af6134e25e2c0b79e7727f6c0491d0
# =============================================================================
# NOTES SUR SEGMENTATION_MODELS
# =============================================================================
# OPTION A: Git avec commit hash (ACTUEL - RECOMMANDÉ)
# Avantages: Version exacte, pas de surprises
# Inconvénients: Nécessite git dans le container de build
# OPTION B: Version PyPI officielle
# segmentation-models==1.0.1
# Avantages: Plus simple, pas besoin de git
# Inconvénients: Peut être différent de votre version dev
# OPTION C: Version locale (si vous avez des modifications)
# Copier le package dans le container et faire pip install -e .

631
app/streamlit_app.py Normal file
View file

@ -0,0 +1,631 @@
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}")
# 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)

View file

@ -0,0 +1,38 @@
services:
multiseg-api:
build:
context: .
dockerfile: Dockerfile
container_name: multisegmentation_api
ports:
- "${NGINX_PORT:-8080}:${NGINX_PORT:-8080}" # Port configurable, défaut 8080
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: http://mlflow:5000
MLFLOW_S3_ENDPOINT_URL: "${MLFLOW_S3_ENDPOINT_URL}" # ex: http://minio:9000 ou https://s3.amazonaws.com
AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}" # clé d'accès S3/MinIO
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}" # clé secrète S3/MinIO
# Configuration email pour notifications (optionnel)
SMTP_SERVER: "${SMTP_SERVER}" # ex: smtp.gmail.com
SMTP_PORT: "${SMTP_PORT}" # ex: 587
SMTP_EMAIL: "${SMTP_EMAIL}" # ex: noreply@company.com
SMTP_PASSWORD: "${SMTP_PASSWORD}" # mot de passe d'application SMTP
SMTP_FROM_ALIAS: "${SMTP_FROM_ALIAS}" # ex: "Multi-Segmentation API"
ADMIN_EMAIL: "${ADMIN_EMAIL}" # ex: admin@company.com
# URL de base de l'API (utilisée par Streamlit pour les appels API)
MULTISEG_API_BASE_URL: "${MULTISEG_API_BASE_URL}" # ex: http://localhost:8080/api ou https://your-domain.com/api
# only if you have a reverse proxy server (like nginx-proxy or Traefik)
# networks:
# - proxy
restart: always
# only if you have a reverse proxy server
# networks:
# proxy:
# external: true

View file

@ -0,0 +1,6 @@
artifact_location: file:///home/madfuckinman/Documents/python_dev/ml-workspace/OpenClassrooms_AI_Projects/Project_8/mlruns/0
creation_time: 1755008866771
experiment_id: '0'
last_update_time: 1755008866771
lifecycle_stage: active
name: Default

57
nginx.conf Normal file
View file

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

File diff suppressed because one or more lines are too long

654
notebooks/requirements.txt Normal file
View file

@ -0,0 +1,654 @@
absl-py==1.4.0
accelerate==1.9.0
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.4
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.7.2
arviz==0.22.0
astropy==7.1.0
astropy-iers-data==0.2025.8.4.0.42.59
astunparse==1.6.3
atpublic==5.1
attrs==25.3.0
audioread==3.0.1
autograd==1.8.0
babel==2.17.0
backcall==0.2.0
backports.tarfile==1.2.0
beautifulsoup4==4.13.4
betterproto==2.0.0b6
bigframes==2.13.0
bigquery-magics==0.10.2
bleach==6.2.0
blinker==1.9.0
blis==1.3.0
blobfile==3.0.0
blosc2==3.6.1
bokeh==3.7.3
boto3==1.40.7
botocore==1.40.7
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.2
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-cp311-cp311-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.62.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.34.0
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
efficientnet==1.1.1
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.7.19
fastapi==0.116.1
fastcore==1.7.29
fastdownload==0.0.7
fastjsonschema==2.21.1
fastprogress==1.0.3
fastrlock==0.8.3
ffmpy==0.6.1
filelock==3.18.0
firebase-admin==6.9.0
Flask==3.1.1
flatbuffers==25.2.10
flax==0.10.6
folium==0.20.0
fonttools==4.59.0
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.0
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-ai-generativelanguage==0.6.15
google-api-core==2.25.1
google-api-python-client==2.177.0
google-auth==2.38.0
google-auth-httplib2==0.2.0
google-auth-oauthlib==1.2.2
google-cloud-aiplatform==1.106.0
google-cloud-bigquery==3.35.1
google-cloud-bigquery-connection==1.18.3
google-cloud-bigquery-storage==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-resource-manager==1.14.2
google-cloud-spanner==3.56.0
google-cloud-storage==2.19.0
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.28.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.41.0
gradio_client==1.11.0
graphene==3.4.3
graphql-core==3.2.6
graphql-relay==3.2.0
graphviz==0.21
greenlet==3.2.3
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.2.0
h5netcdf==1.6.4
h5py==3.14.0
hdbscan==0.8.40
hf-xet==1.1.7
hf_transfer==0.1.9
highspy==1.11.0
holidays==0.78
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
huggingface-hub==0.34.3
humanize==4.12.3
hyperframe==6.1.0
hyperopt==0.2.7
ibis-framework==9.5.0
idna==3.10
image-classifiers==1.0.0
imageio==2.37.0
imageio-ffmpeg==0.6.0
imagesize==1.4.1
imbalanced-learn==0.13.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.0
intel-openmp==2025.2.0
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.2.1
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.0
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-Applications==1.0.8
keras-hub==0.21.1
keras-nlp==0.21.1
keyring==25.6.0
keyrings.google-artifactregistry-auth==1.1.2
kiwisolver==1.4.8
langchain==0.3.27
langchain-core==0.3.72
langchain-text-splitters==0.3.9
langcodes==3.5.0
langsmith==0.4.12
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.2.1
Markdown==3.8.2
markdown-it-py==3.0.0
MarkupSafe==3.0.2
matplotlib==3.10.0
matplotlib-inline==0.1.7
matplotlib-venn==1.1.2
mdit-py-plugins==0.4.2
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.2.0
mlflow-skinny==3.2.0
mlflow-tracing==3.2.0
mlxtend==0.23.4
more-itertools==10.7.0
moviepy==1.0.3
mpmath==1.3.0
msgpack==1.1.1
multidict==6.6.3
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.0.1
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.5.3.2
nvidia-cuda-cupti-cu12==12.5.82
nvidia-cuda-nvcc-cu12==12.5.82
nvidia-cuda-nvrtc-cu12==12.5.82
nvidia-cuda-runtime-cu12==12.5.82
nvidia-cudnn-cu12==9.3.0.75
nvidia-cufft-cu12==11.2.3.61
nvidia-curand-cu12==10.3.6.82
nvidia-cusolver-cu12==11.6.3.83
nvidia-cusparse-cu12==12.5.1.3
nvidia-cusparselt-cu12==0.6.2
nvidia-ml-py==12.575.51
nvidia-nccl-cu12==2.23.4
nvidia-nvjitlink-cu12==12.5.82
nvidia-nvtx-cu12==12.4.127
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.99.1
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-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.20
orjson==3.11.1
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.4
parsy==2.1
partd==1.4.2
patsy==1.0.1
peewee==3.18.2
peft==0.17.0
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
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.0
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_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.1
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-cp311-cp311-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.2
PyOpenGL==3.1.9
pyOpenSSL==24.2.1
pyparsing==3.2.3
pyperclip==1.9.0
pyproj==3.7.1
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.3
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.26.0
rpy2==3.5.17
rsa==4.9.1
ruff==0.12.7
s3transfer==0.13.1
safehttpx==0.1.6
safetensors==0.6.1
scikit-image==0.25.2
scikit-learn==1.6.1
scipy==1.16.1
scooby==0.10.1
scs==3.2.7.post2
seaborn==0.13.2
SecretStorage==3.3.3
segmentation_models @ git+https://github.com/qubvel/segmentation_models.git@5d24bbfb28af6134e25e2c0b79e7727f6c0491d0
semantic-version==2.10.0
Send2Trash==1.8.3
sentence-transformers==5.0.0
sentencepiece==0.2.0
sentry-sdk==2.34.1
shap==0.48.0
shapely==2.1.1
shellingham==1.5.4
simple-parsing==0.1.7
simplejson==3.20.1
simsimd==6.5.0
six==1.17.0
sklearn-compat==0.1.3
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.42
sqlglot==25.20.2
sqlparse==0.5.3
srsly==2.5.1
stanio==0.5.1
starlette==0.47.2
statsmodels==0.14.5
stringzilla==3.12.5
stumpy==1.13.0
sympy==1.13.1
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-io-gcs-filesystem==0.37.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.10.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 @ https://download.pytorch.org/whl/cu124/torch-2.6.0%2Bcu124-cp311-cp311-linux_x86_64.whl
torchao==0.10.0
torchaudio @ https://download.pytorch.org/whl/cu124/torchaudio-2.6.0%2Bcu124-cp311-cp311-linux_x86_64.whl
torchdata==0.11.0
torchsummary==1.5.1
torchtune==0.6.1
torchvision @ https://download.pytorch.org/whl/cu124/torchvision-0.21.0%2Bcu124-cp311-cp311-linux_x86_64.whl
tornado==6.4.2
tqdm==4.67.1
traitlets==5.7.1
traittypes==0.2.1
transformers==4.55.0
treelite==4.4.1
treescope==0.1.9
triton==3.2.0
tsfresh==0.21.0
tweepy==4.16.0
typeguard==4.4.4
typer==0.16.0
types-pytz==2025.2.0.20250516
types-setuptools==80.9.0.20250801
typing-inspection==0.4.1
typing_extensions==4.14.1
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.0
wasabi==1.1.3
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
widgetsnbextension==3.6.10
wordcloud==1.9.4
wrapt==1.17.2
wurlitzer==3.1.1
xarray==2025.7.1
xarray-einstats==0.9.1
xgboost==3.0.3
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.23.0

52
start.sh Normal file
View file

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

1
test.env Normal file
View file

@ -0,0 +1 @@
TEST_VAR='hello'

View file

@ -8,6 +8,14 @@ import matplotlib.pyplot as plt
import mlflow
# Au début de utils.py, après les imports
import segmentation_models as sm
import tensorflow.keras.backend as K
# Configuration du backend segmentation_models (comme dans le notebook)
sm.set_framework('tf.keras')
sm.backend = K
def load_cityscapes_config(config_path="../cityscapes_config.json", verbose=True):
"""
@ -80,6 +88,7 @@ def load_best_model_from_registry(model_name):
# Récupérer la dernière version en Production (votre logique)
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}")
@ -216,7 +225,7 @@ def colorize_mask(mask, group_colors):
def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config,
mask_path=None, save_dir=None, show_stats=True):
mask_path=None, save_dir=None, show_stats=True, show_plot=True):
print(f"=== INFÉRENCE SUR {os.path.basename(image_path)} ===")
img_preprocessed, img_display, mask_gt = preprocess_image_and_mask(
@ -290,7 +299,6 @@ def run_inference_and_visualize(image_path, model, encoder_name, img_size, mappi
fig.savefig(fig_path, dpi=150, bbox_inches='tight')
print(f"💾 Résultat sauvegardé: {fig_path}")
plt.show()
if show_stats:
print("\n📊 Statistiques du masque prédit:")
@ -307,5 +315,9 @@ def run_inference_and_visualize(image_path, model, encoder_name, img_size, mappi
if class_id < len(mapping_config['group_names']):
percentage = (count / mask_gt.numpy().size) * 100
print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)")
return predicted_mask
if show_plot:
plt.show()
return predicted_mask
else:
return predicted_mask, fig