API development finished + Docker is working locally
This commit is contained in:
parent
6a4a546f7b
commit
763f2ec538
15 changed files with 2384 additions and 37 deletions
106
.dockerignore
Normal file
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
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
|
||||
43
Dockerfile
Normal file
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"]
|
||||
202
README.MD
202
README.MD
|
|
@ -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
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
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 .
|
||||
631
app/streamlit_app.py
Normal file
631
app/streamlit_app.py
Normal 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)
|
||||
38
docker-compose.yml.example
Normal file
38
docker-compose.yml.example
Normal 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
|
||||
6
mlruns.backup/0/meta.yaml
Normal file
6
mlruns.backup/0/meta.yaml
Normal 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
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
654
notebooks/requirements.txt
Normal file
654
notebooks/requirements.txt
Normal 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
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;'
|
||||
1
test.env
Normal file
1
test.env
Normal file
|
|
@ -0,0 +1 @@
|
|||
TEST_VAR='hello'
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue