diff --git a/app/main.py b/app/main.py index 136db01..5acdbb2 100644 --- a/app/main.py +++ b/app/main.py @@ -72,6 +72,10 @@ class ModelInfo(BaseModel): encoder_name: str input_size: tuple rank: int + architecture: Optional[str] = None # Ajout architecture + test_mean_iou: Optional[float] = None # Ajout mIoU + test_accuracy: Optional[float] = None # Ajout accuracy + class_ious: Optional[dict] = None # Ajout IoU par classe class ComparisonResult(BaseModel): success: bool @@ -104,6 +108,45 @@ best_model_info = {} second_best_model_info = {} mapping_config = None +def get_model_detailed_info(run_id, mapping_config): + """Récupère les informations détaillées d'un modèle depuis MLflow""" + client = mlflow.tracking.MlflowClient() + + try: + run = mlflow.get_run(run_id) + params = run.data.params + metrics = run.data.metrics + + # Architecture depuis le paramètre model_architecture + architecture = params.get("model_architecture", "Unknown").split('_')[0] + + # Métriques de performance + test_mean_iou = metrics.get("test_mean_iou", None) + test_accuracy = metrics.get("test_accuracy", None) + + # IoU par classe depuis les métriques class_X_iou + class_ious = {} + if mapping_config and 'group_names' in mapping_config: + for i, class_name in enumerate(mapping_config['group_names']): + metric_key = f"class_{i}_iou" + if metric_key in metrics: + class_ious[class_name] = round(metrics[metric_key], 3) + + return { + "architecture": architecture, + "test_mean_iou": test_mean_iou, + "test_accuracy": test_accuracy, + "class_ious": class_ious + } + except Exception as e: + print(f"⚠️ Erreur lors de la récupération des infos détaillées: {e}") + return { + "architecture": "Unknown", + "test_mean_iou": None, + "test_accuracy": None, + "class_ious": {} + } + 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 @@ -122,12 +165,20 @@ def initialize_models(): metric="test_mean_iou", top_n=1 ) + + # Récupérer les infos détaillées du meilleur modèle + best_detailed_info = get_model_detailed_info(best_run_id, mapping_config) + best_model_info = { "name": "Meilleur modèle", "run_id": best_run_id, "encoder_name": best_encoder_name, "input_size": best_img_size, - "rank": 1 + "rank": 1, + "architecture": best_detailed_info["architecture"], + "test_mean_iou": best_detailed_info["test_mean_iou"], + "test_accuracy": best_detailed_info["test_accuracy"], + "class_ious": best_detailed_info["class_ious"] } print("🔄 Chargement du deuxième meilleur modèle...") @@ -136,12 +187,20 @@ def initialize_models(): metric="test_mean_iou", top_n=2 ) + + # Récupérer les infos détaillées du deuxième modèle + second_detailed_info = get_model_detailed_info(second_run_id, mapping_config) + 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 + "rank": 2, + "architecture": second_detailed_info["architecture"], + "test_mean_iou": second_detailed_info["test_mean_iou"], + "test_accuracy": second_detailed_info["test_accuracy"], + "class_ious": second_detailed_info["class_ious"] } os.makedirs(TEMP_UPLOAD_DIR, exist_ok=True) @@ -257,40 +316,95 @@ def compare_sample_image(request: SampleImageRequest): mask_path = Path(SAMPLE_IMAGES_DIR) / "mask" / mask_name mask_path_str = str(mask_path) if mask_path.exists() else None - # Inférence modèle 1 - pred1, t1, _ = run_inference_and_visualize( + # Inférence modèle 1 avec sa résolution spécifique + pred1, t1, fig1 = run_inference_and_visualize( image_path=str(image_path), mask_path=mask_path_str, model=best_model, - encoder_name=None, - img_size=best_model_info["input_size"], + encoder_name=None, # Normalisé de la même manière + img_size=best_model_info["input_size"], # Utilise la résolution du modèle mapping_config=mapping_config, save_dir=None, show_stats=False, show_plot=False ) - # Inférence modèle 2 - pred2, t2, _ = run_inference_and_visualize( + # Inférence modèle 2 avec sa résolution spécifique + pred2, t2, fig2 = run_inference_and_visualize( image_path=str(image_path), mask_path=mask_path_str, model=second_best_model, - encoder_name=None, - img_size=second_best_model_info["input_size"], + encoder_name=None, # Normalisé de la même manière + img_size=second_best_model_info["input_size"], # Utilise la résolution du modèle 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') + + # Créer une figure combinée avec les deux modèles + # Nombre de colonnes : 3 si pas de GT, 4 si GT disponible + num_cols = 3 if not (mask_path_str and os.path.exists(mask_path_str)) else 4 + fig, axes = plt.subplots(2, num_cols, figsize=(5*num_cols, 8)) + + # Récupérer les images depuis les figures individuelles + axes1 = fig1.get_axes() + axes2 = fig2.get_axes() + + # Ligne 1 : Modèle 1 + for i in range(min(len(axes1), num_cols)): + ax_src = axes1[i] + ax_dst = axes[0, i] + + # Copier toutes les images de l'axe source + for img_obj in ax_src.get_images(): + ax_dst.imshow( + img_obj.get_array(), + cmap=img_obj.get_cmap(), + alpha=img_obj.get_alpha(), + vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None, + vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None + ) + + # Copier le titre avec nom du modèle + original_title = ax_src.get_title() + if i == 0: + new_title = "Image originale" + elif "Prédit" in original_title: + new_title = f"Prédiction {best_model_info['architecture']} - {best_model_info['encoder_name']}" + else: + new_title = original_title + + ax_dst.set_title(new_title) + ax_dst.axis('off') + + # Ligne 2 : Modèle 2 + for i in range(min(len(axes2), num_cols)): + ax_src = axes2[i] + ax_dst = axes[1, i] + + # Copier toutes les images de l'axe source + for img_obj in ax_src.get_images(): + ax_dst.imshow( + img_obj.get_array(), + cmap=img_obj.get_cmap(), + alpha=img_obj.get_alpha(), + vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None, + vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None + ) + + # Copier le titre avec nom du modèle + original_title = ax_src.get_title() + if i == 0: + new_title = "Image originale" + elif "Prédit" in original_title: + new_title = f"Prédiction {second_best_model_info['architecture']} - {second_best_model_info['encoder_name']}" + else: + new_title = original_title + + ax_dst.set_title(new_title) + ax_dst.axis('off') + plt.tight_layout() + + # Fermer les figures individuelles pour libérer la mémoire + plt.close(fig1) + plt.close(fig2) fig_b64 = base64.b64encode(pickle.dumps(fig)).decode() # Stats def calc(mask): @@ -333,28 +447,82 @@ def compare_uploaded_image(file: UploadFile = File(...)): temp_file_path = os.path.join(TEMP_UPLOAD_DIR, temp_filename) with open(temp_file_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer) - # Inférences - pred1, t1, _ = run_inference_and_visualize( + # Inférences avec résolutions spécifiques + pred1, t1, fig1 = run_inference_and_visualize( image_path=temp_file_path, mask_path=None, - model=best_model, encoder_name=None, + model=best_model, encoder_name=None, # Normalisé de la même manière img_size=best_model_info["input_size"], mapping_config=mapping_config, save_dir=None, show_stats=False, show_plot=False, ) - pred2, t2, _ = run_inference_and_visualize( + pred2, t2, fig2 = run_inference_and_visualize( image_path=temp_file_path, mask_path=None, - model=second_best_model, encoder_name=None, + model=second_best_model, encoder_name=None, # Normalisé de la même manière 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') + + # Figure combinée pour uploads (pas de GT, donc 2 colonnes seulement) + fig, axes = plt.subplots(2, 2, figsize=(10, 8)) + + # Récupérer les images depuis les figures individuelles + axes1 = fig1.get_axes() + axes2 = fig2.get_axes() + + # Ligne 1 : Modèle 1 (Image originale + Prédiction) + for i in range(min(len(axes1), 2)): + ax_src = axes1[i] + ax_dst = axes[0, i] + + # Copier toutes les images de l'axe source + for img_obj in ax_src.get_images(): + ax_dst.imshow( + img_obj.get_array(), + cmap=img_obj.get_cmap(), + alpha=img_obj.get_alpha(), + vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None, + vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None + ) + + # Copier le titre avec nom du modèle + original_title = ax_src.get_title() + if i == 0: + new_title = "Image originale" + else: + new_title = f"Prédiction {best_model_info['architecture']} - {best_model_info['encoder_name']}" + + ax_dst.set_title(new_title) + ax_dst.axis('off') + + # Ligne 2 : Modèle 2 (Image originale + Prédiction) + for i in range(min(len(axes2), 2)): + ax_src = axes2[i] + ax_dst = axes[1, i] + + # Copier toutes les images de l'axe source + for img_obj in ax_src.get_images(): + ax_dst.imshow( + img_obj.get_array(), + cmap=img_obj.get_cmap(), + alpha=img_obj.get_alpha(), + vmin=img_obj.get_clim()[0] if img_obj.get_clim() else None, + vmax=img_obj.get_clim()[1] if img_obj.get_clim() else None + ) + + # Copier le titre avec nom du modèle + original_title = ax_src.get_title() + if i == 0: + new_title = "Image originale" + else: + new_title = f"Prédiction {second_best_model_info['architecture']} - {second_best_model_info['encoder_name']}" + + ax_dst.set_title(new_title) + ax_dst.axis('off') + plt.tight_layout() + + # Fermer les figures individuelles pour libérer la mémoire + plt.close(fig1) + plt.close(fig2) fig_b64 = base64.b64encode(pickle.dumps(fig)).decode() # Stats def calc(mask): @@ -395,4 +563,4 @@ def get_metrics(): "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/streamlit_app.py b/app/streamlit_app.py index 3351498..846f8b1 100644 --- a/app/streamlit_app.py +++ b/app/streamlit_app.py @@ -2,6 +2,7 @@ import os import sys import requests import streamlit as st +import pandas as pd import numpy as np import matplotlib.pyplot as plt from PIL import Image @@ -67,6 +68,47 @@ st.markdown(""" border-radius: 8px; margin: 1rem 0; } +.color-box { + display: inline-block; + width: 14px; + height: 14px; + margin-right: 8px; + border-radius: 2px; + border: 1px solid rgba(0,0,0,0.2); + vertical-align: middle; +} +.comparison-table { + margin: 1rem 0; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} +.comparison-table th { + background-color: #667eea !important; + color: white !important; + font-weight: 600 !important; + text-align: center !important; +} +.comparison-table td { + text-align: center !important; + padding: 0.5rem !important; + vertical-align: middle !important; +} +.diff-positive { + background-color: #d4edda !important; + color: #155724 !important; + font-weight: 600; +} +.diff-negative { + background-color: #f8d7da !important; + color: #721c24 !important; + font-weight: 600; +} +.diff-neutral { + background-color: #fff3cd !important; + color: #856404 !important; + font-weight: 600; +} """, unsafe_allow_html=True) @@ -151,23 +193,33 @@ def display_comparison_results(result, models_info): with col1: model1 = result['model1_info'] + # Formater les métriques séparément + miou1 = f"{model1.get('test_mean_iou'):.4f}" if model1.get('test_mean_iou') is not None else 'N/A' + acc1 = f"{model1.get('test_accuracy'):.4f}" if model1.get('test_accuracy') is not None else 'N/A' + st.markdown(f"""

🥇 Modèle #1

- {model1['encoder_name']}
+ {model1.get('architecture', 'N/A')} - {model1['encoder_name']}
📐 Taille: {model1['input_size']}
- 🆔 Run: {model1['run_id'][:8]}... + 📊 mIoU: {miou1}
+ 🎯 Accuracy: {acc1}
""", unsafe_allow_html=True) with col2: model2 = result['model2_info'] + # Formater les métriques séparément + miou2 = f"{model2.get('test_mean_iou'):.4f}" if model2.get('test_mean_iou') is not None else 'N/A' + acc2 = f"{model2.get('test_accuracy'):.4f}" if model2.get('test_accuracy') is not None else 'N/A' + st.markdown(f"""

🥈 Modèle #2

- {model2['encoder_name']}
+ {model2.get('architecture', 'N/A')} - {model2['encoder_name']}
📐 Taille: {model2['input_size']}
- 🆔 Run: {model2['run_id'][:8]}... + 📊 mIoU: {miou2}
+ 🎯 Accuracy: {acc2}
""", unsafe_allow_html=True) @@ -184,7 +236,7 @@ def display_comparison_results(result, models_info): winner_class = "speed-winner" if speed_comp['faster_model'] == model1['encoder_name'] else "speed-slower" st.markdown(f"""
-
🥇 {model1['encoder_name']}
+
🥇 {model1.get('architecture', 'N/A')} - {model1['encoder_name']}
{model1_time:.1f} ms
""", unsafe_allow_html=True) @@ -203,7 +255,7 @@ def display_comparison_results(result, models_info): winner_class = "speed-winner" if speed_comp['faster_model'] == model2['encoder_name'] else "speed-slower" st.markdown(f"""
-
🥈 {model2['encoder_name']}
+
🥈 {model2.get('architecture', 'N/A')} - {model2['encoder_name']}
{model2_time:.1f} ms
""", unsafe_allow_html=True) @@ -220,23 +272,21 @@ def display_comparison_results(result, models_info): st.error(f"Erreur lors de l'affichage: {e}") # Comparaison des statistiques - st.markdown("### 📊 Comparaison des Statistiques") + st.markdown("### 📊 Répartition des classes par modèle") col1, col2 = st.columns(2) with col1: - st.markdown(f"**📈 {model1['encoder_name']} - Répartition des classes:**") if result.get('model1_stats'): - display_model_stats_chart(result['model1_stats'], models_info, f"Modèle 1 - {model1['encoder_name']}") + display_model_stats_chart(result['model1_stats'], models_info, f"{model1.get('architecture', 'N/A')} - {model1['encoder_name']}") with col2: - st.markdown(f"**📈 {model2['encoder_name']} - Répartition des classes:**") if result.get('model2_stats'): - display_model_stats_chart(result['model2_stats'], models_info, f"Modèle 2 - {model2['encoder_name']}") + display_model_stats_chart(result['model2_stats'], models_info, f"{model2.get('architecture', 'N/A')} - {model2['encoder_name']}") # Analyse comparative des classes if result.get('model1_stats') and result.get('model2_stats'): - display_class_comparison(result['model1_stats'], result['model2_stats'], model1, model2) + display_class_comparison(result['model1_stats'], result['model2_stats'], model1, model2, models_info) def display_model_stats_chart(stats, models_info, title): """Affiche les statistiques d'un modèle sous forme de graphique""" @@ -244,9 +294,15 @@ def display_model_stats_chart(stats, models_info, title): st.warning("Aucune statistique disponible") return - # Préparer les données pour le graphique - class_names = list(stats.keys()) - percentages = [data['percentage'] for data in stats.values()] + # Utiliser l'ordre des classes du modèle pour la cohérence + if models_info and 'class_names' in models_info: + # Ordonner selon l'ordre du modèle + ordered_class_names = [name for name in models_info['class_names'] if name in stats] + else: + # Fallback : ordre alphabétique + ordered_class_names = sorted(stats.keys()) + + percentages = [stats[name]['percentage'] for name in ordered_class_names] # Récupérer les couleurs des classes colors = [] @@ -254,7 +310,7 @@ def display_model_stats_chart(stats, models_info, title): model_class_names = models_info['class_names'] model_class_colors = models_info['class_colors'] - for class_name in class_names: + for class_name in ordered_class_names: try: idx = model_class_names.index(class_name) color = model_class_colors[idx] @@ -262,21 +318,21 @@ def display_model_stats_chart(stats, models_info, title): except (ValueError, IndexError): colors.append([0.5, 0.5, 0.5]) else: - colors = plt.cm.Set3(np.linspace(0, 1, len(class_names))) + colors = plt.cm.Set3(np.linspace(0, 1, len(ordered_class_names))) # Créer le graphique fig, ax = plt.subplots(figsize=(8, 5)) - bars = ax.bar(class_names, percentages, color=colors, alpha=0.8, edgecolor='white', linewidth=1) + bars = ax.bar(ordered_class_names, percentages, color=colors, alpha=0.8, edgecolor='white', linewidth=1) ax.set_xlabel('Classes de Segmentation', fontsize=10, fontweight='bold') ax.set_ylabel('Pourcentage (%)', fontsize=10, fontweight='bold') ax.set_title(title, fontsize=11, fontweight='bold', pad=15) # Rotation des labels selon le nombre de classes - if len(class_names) <= 4: + if len(ordered_class_names) <= 4: plt.xticks(rotation=0, ha='center', fontsize=9) - elif len(class_names) <= 6: + elif len(ordered_class_names) <= 6: plt.xticks(rotation=30, ha='right', fontsize=8) else: plt.xticks(rotation=45, ha='right', fontsize=8) @@ -305,15 +361,22 @@ def display_model_stats_chart(stats, models_info, title): st.pyplot(fig) plt.close(fig) -def display_class_comparison(stats1, stats2, model1, model2): +def display_class_comparison(stats1, stats2, model1, model2, models_info): """Affiche une comparaison détaillée des classes entre les deux modèles""" st.markdown("### 🔍 Analyse Comparative par Classe") - # Combiner toutes les classes présentes - all_classes = set(stats1.keys()) | set(stats2.keys()) + # Utiliser l'ordre des classes du modèle pour la cohérence + if models_info and 'class_names' in models_info: + # Combiner toutes les classes présentes en conservant l'ordre du modèle + all_classes_in_stats = set(stats1.keys()) | set(stats2.keys()) + ordered_all_classes = [name for name in models_info['class_names'] if name in all_classes_in_stats] + else: + # Fallback : ordre alphabétique + all_classes = set(stats1.keys()) | set(stats2.keys()) + ordered_all_classes = sorted(all_classes) comparison_data = [] - for class_name in sorted(all_classes): + for class_name in ordered_all_classes: pct1 = stats1.get(class_name, {}).get('percentage', 0) pct2 = stats2.get(class_name, {}).get('percentage', 0) diff = pct1 - pct2 @@ -325,27 +388,122 @@ def display_class_comparison(stats1, stats2, model1, model2): 'difference': diff }) - # Afficher sous forme de tableau interactif - col1, col2 = st.columns([2, 1]) + # Créer un graphique comparatif des barres + st.markdown("**📊 Comparaison des classes entre les modèles:**") - with col1: - st.markdown("**📋 Tableau Comparatif:**") - for data in comparison_data: - diff_color = "🟢" if abs(data['difference']) < 1 else ("🔴" if abs(data['difference']) > 5 else "🟡") - diff_text = f"{data['difference']:+.1f}%" - - st.markdown(f""" - **{data['class']}** {diff_color} - - {model1['encoder_name']}: {data['model1_pct']:.1f}% - - {model2['encoder_name']}: {data['model2_pct']:.1f}% - - Différence: {diff_text} - """) + # Préparation des données + class_names = [data['class'] for data in comparison_data] + model1_values = [data['model1_pct'] for data in comparison_data] + model2_values = [data['model2_pct'] for data in comparison_data] - with col2: - st.markdown("**🎯 Légende:**") - st.markdown("🟢 Différence < 1%") - st.markdown("🟡 Différence 1-5%") - st.markdown("🔴 Différence > 5%") + # Récupérer les couleurs des classes + colors = [] + if models_info and 'class_colors' in models_info and 'class_names' in models_info: + model_class_names = models_info['class_names'] + model_class_colors = models_info['class_colors'] + + for class_name in class_names: + try: + idx = model_class_names.index(class_name) + color = model_class_colors[idx] + hex_color = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}" + colors.append(hex_color) + except (ValueError, IndexError): + colors.append("#999999") + else: + colors = ['#999999'] * len(class_names) + + # Créer le graphique comparatif + fig, ax = plt.subplots(figsize=(12, 7)) + + x = np.arange(len(class_names)) # Positions des barres + width = 0.35 # Largeur des barres + + bars1 = ax.bar(x - width/2, model1_values, width, label=f"{model1.get('architecture', 'N/A')} - {model1['encoder_name']}", + color=colors, alpha=0.8, edgecolor='white') + + # Barres hachurées pour le modèle 2 + bars2 = ax.bar(x + width/2, model2_values, width, label=f"{model2.get('architecture', 'N/A')} - {model2['encoder_name']}", + color=colors, alpha=0.6, edgecolor='white', hatch='///') + + ax.set_xlabel('Classes', fontsize=11, fontweight='bold') + ax.set_ylabel('Pourcentage (%)', fontsize=11, fontweight='bold') + ax.set_title('Comparaison des classes entre les deux modèles', fontsize=13, fontweight='bold', pad=15) + ax.set_xticks(x) + + # Rotation des labels selon le nombre de classes + if len(class_names) <= 4: + ax.set_xticklabels(class_names, fontsize=10) + elif len(class_names) <= 6: + ax.set_xticklabels(class_names, rotation=30, ha='right', fontsize=9) + else: + ax.set_xticklabels(class_names, rotation=45, ha='right', fontsize=9) + + ax.legend(loc='upper right', fontsize=10) + ax.grid(axis='y', alpha=0.2, linestyle='--') + ax.set_axisbelow(True) + + # Style des axes + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + + # Valeurs sur les barres + for bar1, bar2, val1, val2 in zip(bars1, bars2, model1_values, model2_values): + height1 = bar1.get_height() + height2 = bar2.get_height() + ax.text(bar1.get_x() + bar1.get_width()/2., height1 + 0.5, + f'{val1:.1f}%', ha='center', va='bottom', fontsize=8, fontweight='bold') + ax.text(bar2.get_x() + bar2.get_width()/2., height2 + 0.5, + f'{val2:.1f}%', ha='center', va='bottom', fontsize=8, fontweight='bold') + + plt.tight_layout() + st.pyplot(fig) + plt.close(fig) + + # Tableau détaillé avec un beau format + st.markdown("**🔎 Détails des différences par classe:**") + + # Préparer les données pour le tableau + table_data = [] + for i, data in enumerate(comparison_data): + class_color = colors[i] + diff_value = data['difference'] + + # Classification de la différence + if abs(diff_value) < 1: + diff_category = "Similaire" + diff_emoji = "🟢" + elif abs(diff_value) < 5: + diff_category = "Différence modérée" + diff_emoji = "🟡" + else: + diff_category = "Différence importante" + diff_emoji = "🔴" + + table_data.append({ + "Classe": data['class'], + f"🥇 {model1.get('architecture', 'N/A')[:15]} (%)": f"{data['model1_pct']:.1f}%", + f"🥈 {model2.get('architecture', 'N/A')[:15]} (%)": f"{data['model2_pct']:.1f}%", + "Différence": f"{diff_value:+.1f}%", + "Catégorie": f"{diff_emoji} {diff_category}" + }) + + # Créer le DataFrame et l'afficher + df = pd.DataFrame(table_data) + + # Utiliser st.dataframe avec style personnalisé + st.dataframe( + df, + use_container_width=True, + hide_index=True, + column_config={ + "Classe": st.column_config.TextColumn("🎯 Classe", width="medium"), + f"🥇 {model1.get('architecture', 'N/A')[:15]} (%)": st.column_config.TextColumn(width="medium"), + f"🥈 {model2.get('architecture', 'N/A')[:15]} (%)": st.column_config.TextColumn(width="medium"), + "Différence": st.column_config.TextColumn("📊 Différence", width="small"), + "Catégorie": st.column_config.TextColumn("📈 Évaluation", width="medium") + } + ) # Sidebar avec informations des modèles with st.sidebar: @@ -358,37 +516,54 @@ with st.sidebar: # Modèle 1 best_model = models_info.get('best_model', {}) st.markdown("**🥇 Meilleur Modèle:**") + st.write(f"**Architecture:** {best_model.get('architecture', 'N/A')}") st.write(f"**Encoder:** {best_model.get('encoder_name', 'N/A')}") - st.write(f"**Run ID:** {best_model.get('run_id', 'N/A')[:8]}...") + st.write(f"**Résolution:** {best_model.get('input_size', 'N/A')}") # Modèle 2 second_model = models_info.get('second_best_model', {}) st.markdown("**🥈 Deuxième Modèle:**") + st.write(f"**Architecture:** {second_model.get('architecture', 'N/A')}") st.write(f"**Encoder:** {second_model.get('encoder_name', 'N/A')}") - st.write(f"**Run ID:** {second_model.get('run_id', 'N/A')[:8]}...") - - st.write(f"**Classes:** {models_info.get('num_classes', 'N/A')}") + st.write(f"**Résolution:** {second_model.get('input_size', 'N/A')}") else: st.error("❌ Modèles non disponibles") st.markdown("---") - st.markdown("## 🎯 Classes Segmentées") + st.markdown("## 🎯 Classes Segmentées (IoU)") - # Affichage avec carrés colorés + # Affichage avec carrés colorés et IoU if models_info and 'class_names' in models_info and 'class_colors' in models_info: class_names = models_info['class_names'] class_colors = models_info['class_colors'] + # Récupérer les IoU des modèles + best_ious = best_model.get('class_ious', {}) if models_info else {} + second_ious = second_model.get('class_ious', {}) if models_info else {} + for i, (class_name, color) in enumerate(zip(class_names, class_colors)): color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}" + # Construire la chaîne IoU + iou_str = "" + if best_ious.get(class_name) is not None and second_ious.get(class_name) is not None: + best_iou = best_ious[class_name] + second_iou = second_ious[class_name] + iou_str = f" (🥇{best_iou:.2f} - 🥈{second_iou:.2f})" + elif best_ious.get(class_name) is not None: + best_iou = best_ious[class_name] + iou_str = f" (🥇{best_iou:.2f})" + elif second_ious.get(class_name) is not None: + second_iou = second_ious[class_name] + iou_str = f" (🥈{second_iou:.2f})" + st.markdown(f"""
- {i}. {class_name} + {i}. {class_name}{iou_str}
""", unsafe_allow_html=True) diff --git a/utils/utils.py b/utils/utils.py index 39047f5..0a58a91 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -102,38 +102,6 @@ def colorize_mask(mask, group_colors): return colored_mask -# def get_preprocessing_fn(encoder_name=None): -# """ -# Retourne la fonction de prétraitement adaptée au backbone utilisé. -# ADAPTÉ PYTORCH : utilise smp.encoders.get_preprocessing_params -# """ -# if encoder_name is None: -# print("ℹ️ Modèle vanilla - normalisation simple") -# return lambda x: x / 255.0 -# else: -# print(f"ℹ️ Préprocessing du backbone {encoder_name}") -# try: -# # PyTorch segmentation_models utilise get_preprocessing_params -# preprocess_params = smp.encoders.get_preprocessing_params(encoder_name) -# mean = preprocess_params['mean'] -# std = preprocess_params['std'] - -# def preprocess_fn(x): -# # x doit être dans [0, 1] puis normalisé -# x = x / 255.0 if x.max() > 1.0 else x -# # Normalisation ImageNet ou spécifique au backbone -# if isinstance(x, torch.Tensor): -# for i in range(3): # RGB -# x[:, i, :, :] = (x[:, i, :, :] - mean[i]) / std[i] -# else: # numpy -# for i in range(3): -# x[:, :, i] = (x[:, :, i] - mean[i]) / std[i] -# return x - -# return preprocess_fn -# except Exception as e: -# print(f"⚠️ Erreur preprocessing {encoder_name}: {e}, fallback simple") -# return lambda x: x / 255.0 def get_preprocessing_fn(img_size=(512, 512), encoder=None): """ @@ -200,170 +168,6 @@ def preprocess_image_and_mask(image_path, mask_path=None, img_size=(512, 512), return img_preprocessed, img_display, mask_preprocessed - -# def preprocess_image_and_mask(image_path, mask_path=None, img_size=(512, 512), # ← CHANGÉ: 224→512 -# encoder_name=None, id_to_group_mapping=None): -# """ -# Préprocesse une image (et optionnellement un masque) avec le même pipeline que l'entraînement -# ADAPTÉ PYTORCH : Utilise PIL + torch au lieu de tf -# """ - -# print(f"🔄 Préprocessing de l'image: {os.path.basename(image_path)}") - -# # Chargement et preprocessing de l'image -# img_pil = Image.open(image_path).convert('RGB') -# img_original = img_pil.resize(img_size, Image.BILINEAR) - -# # Conversion en tensor et normalisation -# img_tensor = transforms.ToTensor()(img_original) # [3, H, W] dans [0, 1] - -# # Appliquer le preprocessing du backbone -# preprocessing_fn = get_preprocessing_fn(encoder_name) -# if encoder_name is not None: -# # Pour les backbones pré-entraînés, on applique leur normalisation -# img_preprocessed = preprocessing_fn(img_tensor.permute(1, 2, 0).numpy()) -# img_preprocessed = torch.from_numpy(img_preprocessed).permute(2, 0, 1).float() -# else: -# # Simple normalisation [0, 1] -# img_preprocessed = img_tensor.float() - -# # Preprocessing du masque si fourni -# mask_preprocessed = None -# if mask_path and os.path.exists(mask_path): -# print(f"🔄 Préprocessing du masque: {os.path.basename(mask_path)}") - -# mask_pil = Image.open(mask_path).convert('L') # Grayscale -# mask_resized = mask_pil.resize(img_size, Image.NEAREST) -# mask_tensor = torch.from_numpy(np.array(mask_resized)).long() - -# print(f" Valeurs uniques avant mapping: {torch.unique(mask_tensor)[:10]}...") - -# # Appliquer le mapping si fourni -# if id_to_group_mapping is not None: -# mask_tensor = map_mask_ids(mask_tensor, id_to_group_mapping) -# print(f" Valeurs uniques après mapping: {torch.unique(mask_tensor)}") - -# mask_preprocessed = mask_tensor - -# # img_original en numpy pour affichage -# img_display = np.array(img_original) - -# return img_preprocessed, img_display, mask_preprocessed - -# def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config, -# mask_path=None, save_dir=None, show_stats=True): -# """ -# ADAPTÉ PYTORCH : Inférence avec modèle PyTorch -# Returns: (predicted_mask, inference_time_seconds) -# """ - -# print(f"=== INFÉRENCE PYTORCH SUR {os.path.basename(image_path)} ===") - -# img_preprocessed, img_display, mask_gt = preprocess_image_and_mask( -# image_path=image_path, -# mask_path=mask_path, -# img_size=img_size or (512, 512), -# encoder_name=encoder_name, -# id_to_group_mapping=mapping_config.get('id_to_group') -# ) - -# print("🔮 Exécution de l'inférence PyTorch...") -# model.eval() -# device = next(model.parameters()).device - -# with torch.no_grad(): -# # Ajouter dimension batch -# img_batch = img_preprocessed.unsqueeze(0).to(device) # [1, 3, H, W] - -# # ⏱️ MESURE DU TEMPS D'INFÉRENCE -# start_time = time.time() -# predictions = model(img_batch) # [1, num_classes, H, W] -# end_time = time.time() - -# inference_time = end_time - start_time - -# predicted_mask = torch.argmax(predictions[0], dim=0).cpu().numpy() # [H, W] - -# print(f"✅ Inférence terminée - Shape: {predicted_mask.shape}") -# print(f"⏱️ Temps d'inférence: {inference_time*1000:.1f} ms ({inference_time:.4f} s)") - -# # Visualisation -# num_plots = 2 if mask_gt is None else 4 -# fig, axes = plt.subplots(1, num_plots, figsize=(5*num_plots, 5)) -# if num_plots == 1: -# axes = [axes] - -# axes[0].imshow(img_display) -# axes[0].set_title("Image Originale") -# axes[0].axis('off') - -# predicted_colored = colorize_mask(predicted_mask, mapping_config['group_colors']) -# axes[1].imshow(predicted_colored) -# axes[1].set_title("Masque Prédit") -# axes[1].axis('off') - -# if mask_gt is not None: -# mask_gt_np = mask_gt.numpy() if isinstance(mask_gt, torch.Tensor) else mask_gt -# gt_colored = colorize_mask(mask_gt_np, mapping_config['group_colors']) -# axes[2].imshow(gt_colored) -# axes[2].set_title("Masque de Vérité") -# axes[2].axis('off') - -# # Visualisation des erreurs -# diff_mask = (mask_gt_np != predicted_mask).astype(float) -# axes[3].imshow(img_display) -# axes[3].imshow(diff_mask, alpha=0.5, cmap='Reds') -# axes[3].set_title("Erreurs (en rouge)") -# axes[3].axis('off') - -# colors_normalized = mapping_config['group_colors'] / 255.0 -# custom_cmap = ListedColormap(colors_normalized) - -# legend_elements = [] -# for i, (name, color) in enumerate(zip(mapping_config['group_names'], colors_normalized)): -# legend_elements.append(mpatches.Patch(color=color, label=f'{i}: {name}')) - -# fig.legend(handles=legend_elements, -# loc='center left', -# bbox_to_anchor=(1.01, 0.5), -# fontsize=9, -# frameon=True, -# fancybox=True, -# shadow=True) - -# plt.tight_layout() -# fig.subplots_adjust(right=0.95) - -# if save_dir: -# os.makedirs(save_dir, exist_ok=True) -# img_name = os.path.splitext(os.path.basename(image_path))[0] -# fig_path = os.path.join(save_dir, f'{img_name}_inference.png') -# fig.savefig(fig_path, dpi=150, bbox_inches='tight') -# print(f"💾 Résultat sauvegardé: {fig_path}") - -# plt.show() - -# # Statistiques -# if show_stats: -# print("\n📊 Statistiques du masque prédit:") -# unique_classes, counts = np.unique(predicted_mask, return_counts=True) -# for class_id, count in zip(unique_classes, counts): -# if class_id < len(mapping_config['group_names']): -# percentage = (count / predicted_mask.size) * 100 -# print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)") - -# if mask_gt is not None: -# mask_gt_np = mask_gt.numpy() if isinstance(mask_gt, torch.Tensor) else mask_gt -# print("\n📊 Statistiques du masque de vérité:") -# unique_classes, counts = np.unique(mask_gt_np, return_counts=True) -# for class_id, count in zip(unique_classes, counts): -# if class_id < len(mapping_config['group_names']): -# percentage = (count / mask_gt_np.size) * 100 -# print(f" {mapping_config['group_names'][class_id]}: {count} pixels ({percentage:.1f}%)") - -# return predicted_mask, inference_time - - # Version mise à jour de run_inference_and_visualize def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config, mask_path=None, save_dir=None, show_stats=True, show_plot=True):