From cdd6f9645bb7272b23b92cce02a84d768be48041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Mureau?= Date: Tue, 2 Sep 2025 10:46:13 +0200 Subject: [PATCH] Docker working. Improvements incoming. --- EV3 | 0 app/main.py | 419 ++++++++++----------- app/requirements.txt | 117 ++---- app/streamlit_app.py | 749 +++++++++++++++---------------------- notebooks/requirements.txt | 6 +- utils/utils.py | 20 +- 6 files changed, 530 insertions(+), 781 deletions(-) create mode 100644 EV3 diff --git a/EV3 b/EV3 new file mode 100644 index 0000000..e69de29 diff --git a/app/main.py b/app/main.py index 3487456..136db01 100644 --- a/app/main.py +++ b/app/main.py @@ -1,24 +1,27 @@ import os import sys +import traceback import tempfile import shutil from datetime import datetime from typing import Optional, List from pathlib import Path import mlflow -from fastapi import FastAPI, HTTPException, File, UploadFile, Form +from fastapi import FastAPI, HTTPException, File, UploadFile from fastapi.responses import FileResponse from pydantic import BaseModel import numpy as np import pickle import base64 +import matplotlib.pyplot as plt +import matplotlib.image as mpimg + # Configuration MLflow avec variables d'environnement def configure_mlflow(): """Configure MLflow avec les variables d'environnement""" print("=== DEBUG VARIABLES D'ENVIRONNEMENT ===") - # ✅ 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() @@ -33,11 +36,9 @@ def configure_mlflow(): 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)") @@ -46,66 +47,68 @@ def configure_mlflow(): 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)") + print("✅ Configuration MLflow terminée") # Import des fonctions utils sys.path.append('.') +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from utils.utils import ( load_cityscapes_config, - load_best_model_from_registry, + load_model, 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 +# Modèles de données mis à jour +class ModelInfo(BaseModel): + name: str + run_id: str + encoder_name: str + input_size: tuple + rank: int + +class ComparisonResult(BaseModel): + success: bool + message: str + model1_info: ModelInfo + model2_info: ModelInfo + image_path: Optional[str] = None + mask_path: Optional[str] = None + model1_stats: Optional[dict] = None + model2_stats: Optional[dict] = None + model1_inference_time: float + model2_inference_time: float + speed_comparison: dict + ground_truth_available: bool = False + figure_data: Optional[str] = None + class ImageInfo(BaseModel): filename: str display_name: str has_ground_truth: bool -class 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 +# Variables globales pour les deux modèles +best_model = None +second_best_model = None +best_model_info = {} +second_best_model_info = {} 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 +def initialize_models(): + """Initialise les deux meilleurs modèles au démarrage""" + global best_model, second_best_model, best_model_info, second_best_model_info, mapping_config try: - # Configuration MLflow print("🔄 Configuration de MLflow...") configure_mlflow() @@ -113,76 +116,83 @@ def initialize_model(): 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("🔄 Chargement du meilleur modèle...") + best_model, best_run_id, best_encoder_name, best_img_size = load_model( + experiment_name="OC Projet 9", + metric="test_mean_iou", + top_n=1 + ) + best_model_info = { + "name": "Meilleur modèle", + "run_id": best_run_id, + "encoder_name": best_encoder_name, + "input_size": best_img_size, + "rank": 1 + } - 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']}") + print("🔄 Chargement du deuxième meilleur modèle...") + second_best_model, second_run_id, second_encoder_name, second_img_size = load_model( + experiment_name="OC Projet 9", + metric="test_mean_iou", + top_n=2 + ) + second_best_model_info = { + "name": "Deuxième modèle", + "run_id": second_run_id, + "encoder_name": second_encoder_name, + "input_size": second_img_size, + "rank": 2 + } - # 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" + title="Multi-Class Segmentation Comparison API", + description="API de comparaison de segmentation sémantique avec deux modèles", + version="2.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é!") + initialize_models() + print("🚀 API démarrée avec succès - Modèles chargés!") 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.") + print(f"⚠️ ATTENTION: Impossible de charger les modèles au démarrage: {e}") @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, + "message": "Multi-Class Segmentation Comparison API - Online", + "models_loaded": best_model is not None and second_best_model is not None, + "best_model": best_model_info if best_model else None, + "second_best_model": second_best_model_info if second_best_model else None, "num_classes": mapping_config['num_classes'] if mapping_config else None } @app.get("/health") def health_check(): - """Health check endpoint pour nginx""" - if model is None or mapping_config is None: - raise HTTPException(status_code=503, detail="Model not loaded") - + if best_model is None or second_best_model is None or mapping_config is None: + raise HTTPException(status_code=503, detail="Models not loaded") return { "status": "healthy", - "model_loaded": True, + "models_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") - +def get_models_info(): + if best_model is None or second_best_model is None: + raise HTTPException(status_code=503, detail="Models not loaded") return { - "model_name": MODEL_NAME, - "run_id": run_id, - "encoder": encoder_name, - "input_size": script_img_size, + "best_model": best_model_info, + "second_best_model": second_best_model_info, "num_classes": mapping_config['num_classes'], "class_names": mapping_config['group_names'], "class_colors": mapping_config['group_colors'].tolist() @@ -190,238 +200,199 @@ def get_model_info(): @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) - ) - + 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") - +@app.post("/compare-sample", response_model=ComparisonResult) +def compare_sample_image(request: SampleImageRequest): + if best_model is None or second_best_model is None or mapping_config is None: + raise HTTPException(status_code=503, detail="Models not loaded") try: - # 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( + # Inférence modèle 1 + pred1, t1, _ = 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, + model=best_model, + encoder_name=None, + img_size=best_model_info["input_size"], mapping_config=mapping_config, - save_dir=None, - show_stats=False, - show_plot=False # ← AJOUTÉ pour récupérer la figure + save_dir=None, show_stats=False, show_plot=False ) - - # 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", + # Inférence modèle 2 + pred2, t2, _ = run_inference_and_visualize( image_path=str(image_path), mask_path=mask_path_str, - prediction_stats=stats, - ground_truth_available=mask_path.exists(), + model=second_best_model, + encoder_name=None, + img_size=second_best_model_info["input_size"], + mapping_config=mapping_config, + save_dir=None, show_stats=False, show_plot=False + ) + # Figure combinée + fig, axes = plt.subplots(2, 3, figsize=(15, 8)) + img_arr = mpimg.imread(str(image_path)) + axes[0, 0].imshow(img_arr); axes[0, 0].set_title("Image originale"); axes[0, 0].axis('off') + axes[1, 0].imshow(img_arr); axes[1, 0].set_title("Image originale"); axes[1, 0].axis('off') + axes[0, 1].imshow(colorize_mask(pred1, mapping_config['group_colors'])) + axes[0, 1].set_title(f"Prédiction {best_model_info['encoder_name']}"); axes[0, 1].axis('off') + axes[1, 1].imshow(colorize_mask(pred2, mapping_config['group_colors'])) + axes[1, 1].set_title(f"Prédiction {second_best_model_info['encoder_name']}"); axes[1, 1].axis('off') + if mask_path_str and os.path.exists(mask_path_str): + gt = mpimg.imread(mask_path_str) + axes[0, 2].imshow(gt); axes[0, 2].set_title("Ground Truth"); axes[0, 2].axis('off') + axes[1, 2].imshow(gt); axes[1, 2].set_title("Ground Truth"); axes[1, 2].axis('off') + plt.tight_layout() + fig_b64 = base64.b64encode(pickle.dumps(fig)).decode() + # Stats + def calc(mask): + uc, cnts = np.unique(mask, return_counts=True) + stats = {} + for cid, c in zip(uc, cnts): + if cid < len(mapping_config['group_names']): + stats[mapping_config['group_names'][cid]] = { + "pixels": int(c), + "percentage": round((c/mask.size)*100, 1) + } + return stats + stats1, stats2 = calc(pred1), calc(pred2) + faster = best_model_info['encoder_name'] if t1 < t2 else second_best_model_info['encoder_name'] + speedup = round(max(t1, t2) / min(t1, t2), 2) + speed_comp = {"faster_model": faster, "speedup_ratio": speedup, "time_difference_ms": abs(t1-t2)*1000} + return ComparisonResult( + success=True, message="Comparison completed successfully", + model1_info=ModelInfo(**best_model_info), model2_info=ModelInfo(**second_best_model_info), + image_path=str(image_path), mask_path=mask_path_str, + model1_stats=stats1, model2_stats=stats2, + model1_inference_time=t1, model2_inference_time=t2, + speed_comparison=speed_comp, ground_truth_available=mask_path.exists(), figure_data=fig_b64 ) - except Exception as e: - 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)}") + raise HTTPException(status_code=500, detail=f"Comparison 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 +@app.post("/compare-upload", response_model=ComparisonResult) +def compare_uploaded_image(file: UploadFile = File(...)): + if best_model is None or second_best_model is None or mapping_config is None: + raise HTTPException(status_code=503, detail="Models not loaded") allowed_types = ["image/jpeg", "image/jpg", "image/png"] if file.content_type not in allowed_types: - raise HTTPException( - status_code=400, - detail=f"Invalid file type. Allowed: {', '.join(allowed_types)}" - ) - + 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 + # Inférences + pred1, t1, _ = run_inference_and_visualize( + image_path=temp_file_path, mask_path=None, + model=best_model, encoder_name=None, + img_size=best_model_info["input_size"], mapping_config=mapping_config, + save_dir=None, show_stats=False, show_plot=False, ) - - # 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", + pred2, t2, _ = run_inference_and_visualize( + image_path=temp_file_path, mask_path=None, + model=second_best_model, encoder_name=None, + img_size=second_best_model_info["input_size"], mapping_config=mapping_config, + save_dir=None, show_stats=False, show_plot=False, + ) + # Figure + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + img_arr = mpimg.imread(temp_file_path) + axes[0].imshow(img_arr); axes[0].set_title("Image originale"); axes[0].axis('off') + axes[1].imshow(colorize_mask(pred1, mapping_config['group_colors'])) + axes[1].set_title(f"Prédiction {best_model_info['encoder_name']}"); axes[1].axis('off') + axes[2].imshow(colorize_mask(pred2, mapping_config['group_colors'])) + axes[2].set_title(f"Prédiction {second_best_model_info['encoder_name']}"); axes[2].axis('off') + plt.tight_layout() + fig_b64 = base64.b64encode(pickle.dumps(fig)).decode() + # Stats + def calc(mask): + uc, cnts = np.unique(mask, return_counts=True) + stats = {} + for cid, c in zip(uc, cnts): + if cid < len(mapping_config['group_names']): + stats[mapping_config['group_names'][cid]] = { + "pixels": int(c), + "percentage": round((c/mask.size)*100, 1) + } + return stats + stats1, stats2 = calc(pred1), calc(pred2) + faster = best_model_info['encoder_name'] if t1 < t2 else second_best_model_info['encoder_name'] + speedup = round(max(t1, t2) / min(t1, t2), 2) + speed_comp = {"faster_model": faster, "speedup_ratio": speedup, "time_difference_ms": abs(t1-t2)*1000} + return ComparisonResult( + success=True, message="Comparison completed successfully", + model1_info=ModelInfo(**best_model_info), model2_info=ModelInfo(**second_best_model_info), image_path=temp_file_path, - prediction_stats=stats, - ground_truth_available=False, + model1_stats=stats1, model2_stats=stats2, + model1_inference_time=t1, model2_inference_time=t2, + speed_comparison=speed_comp, ground_truth_available=False, figure_data=fig_b64 ) - except Exception as e: - 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)}") - + raise HTTPException(status_code=500, detail=f"Comparison 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}") + try: os.remove(temp_file_path) + except Exception: pass @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, + "models_loaded": best_model is not None and second_best_model is not None, + "best_model_info": best_model_info, + "second_best_model_info": second_best_model_info, "num_classes": mapping_config['num_classes'] if mapping_config else None, "timestamp": datetime.utcnow().isoformat() - } \ No newline at end of file + } diff --git a/app/requirements.txt b/app/requirements.txt index c78ee04..ef6d4de 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -8,37 +8,44 @@ uvicorn==0.35.0 starlette==0.46.2 pydantic==2.11.7 pydantic_core==2.33.2 +python-multipart==0.0.20 # uploads FastAPI # ===== STREAMLIT ===== streamlit==1.46.1 -altair==5.5.0 # Utilisé par Streamlit pour les graphiques -blinker==1.9.0 # Streamlit dependency +altair==5.5.0 # utilisé par Streamlit +blinker==1.9.0 # dépendance Streamlit -# ===== HTTP REQUESTS ===== +# ===== HTTP CLIENTS ===== requests==2.32.4 -httpx==0.28.1 # Modern HTTP client (utilisé par FastAPI) +httpx==0.28.1 # utilisé en interne 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 +# ===== MACHINE LEARNING CORE (PyTorch) ===== +torch==2.8.0 # version stable PyTorch +torchvision==0.23.0 # utile pour les modèles pré-entraînés +torchaudio==2.8.0 # optionnel (mais recommandé pour compatibilité PyTorch) +segmentation-models-pytorch==0.5.0 +albumentations==2.0.8 +# version PyTorch de ToTensor +albumentations[imgaug,opencv] -# Note: tensorflow-addons n'est plus nécessaire avec TensorFlow 2.19+ -# Les fonctionnalités sont maintenant intégrées dans TensorFlow core +numpy>=1.24.0,<2.0.0 +scipy==1.16.0 +scikit-learn==1.7.0 # ===== COMPUTER VISION ===== pillow==11.3.0 -opencv-python-headless==4.9.0.80 # Pas dans votre freeze, mais nécessaire pour Docker +opencv-python-headless==4.9.0.80 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 +networkx==3.5 +tifffile==2025.6.11 +lazy_loader==0.4 # ===== MLFLOW & MODEL REGISTRY ===== mlflow==3.2.0 @@ -50,34 +57,7 @@ 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 +# ===== UTILITIES ===== click==8.2.1 tqdm==4.67.1 joblib==1.5.1 @@ -85,57 +65,6 @@ 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) +# ===== MATPLOTLIB ADDONS ===== +cycler==0.12.1 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 . diff --git a/app/streamlit_app.py b/app/streamlit_app.py index 6f9a090..3351498 100644 --- a/app/streamlit_app.py +++ b/app/streamlit_app.py @@ -10,25 +10,21 @@ 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="🚗", + page_title="Multi-Class Segmentation Comparison", + page_icon="🔄", layout="wide" ) -# CSS personnalisé +# CSS personnalisé mis à jour st.markdown("""