Docker development done. Pushing to dockerhub following cd github instructions

This commit is contained in:
Grégoire Mureau 2025-09-02 15:31:13 +02:00
parent cdd6f9645b
commit bb13d48c77
3 changed files with 431 additions and 284 deletions

View file

@ -72,6 +72,10 @@ class ModelInfo(BaseModel):
encoder_name: str encoder_name: str
input_size: tuple input_size: tuple
rank: int 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): class ComparisonResult(BaseModel):
success: bool success: bool
@ -104,6 +108,45 @@ best_model_info = {}
second_best_model_info = {} second_best_model_info = {}
mapping_config = None 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(): def initialize_models():
"""Initialise les deux meilleurs modèles au démarrage""" """Initialise les deux meilleurs modèles au démarrage"""
global best_model, second_best_model, best_model_info, second_best_model_info, mapping_config 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", metric="test_mean_iou",
top_n=1 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 = { best_model_info = {
"name": "Meilleur modèle", "name": "Meilleur modèle",
"run_id": best_run_id, "run_id": best_run_id,
"encoder_name": best_encoder_name, "encoder_name": best_encoder_name,
"input_size": best_img_size, "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...") print("🔄 Chargement du deuxième meilleur modèle...")
@ -136,12 +187,20 @@ def initialize_models():
metric="test_mean_iou", metric="test_mean_iou",
top_n=2 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 = { second_best_model_info = {
"name": "Deuxième modèle", "name": "Deuxième modèle",
"run_id": second_run_id, "run_id": second_run_id,
"encoder_name": second_encoder_name, "encoder_name": second_encoder_name,
"input_size": second_img_size, "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) 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 = Path(SAMPLE_IMAGES_DIR) / "mask" / mask_name
mask_path_str = str(mask_path) if mask_path.exists() else None mask_path_str = str(mask_path) if mask_path.exists() else None
# Inférence modèle 1 # Inférence modèle 1 avec sa résolution spécifique
pred1, t1, _ = run_inference_and_visualize( pred1, t1, fig1 = run_inference_and_visualize(
image_path=str(image_path), image_path=str(image_path),
mask_path=mask_path_str, mask_path=mask_path_str,
model=best_model, model=best_model,
encoder_name=None, encoder_name=None, # Normalisé de la même manière
img_size=best_model_info["input_size"], img_size=best_model_info["input_size"], # Utilise la résolution du modèle
mapping_config=mapping_config, mapping_config=mapping_config,
save_dir=None, show_stats=False, show_plot=False save_dir=None, show_stats=False, show_plot=False
) )
# Inférence modèle 2 # Inférence modèle 2 avec sa résolution spécifique
pred2, t2, _ = run_inference_and_visualize( pred2, t2, fig2 = run_inference_and_visualize(
image_path=str(image_path), image_path=str(image_path),
mask_path=mask_path_str, mask_path=mask_path_str,
model=second_best_model, model=second_best_model,
encoder_name=None, encoder_name=None, # Normalisé de la même manière
img_size=second_best_model_info["input_size"], img_size=second_best_model_info["input_size"], # Utilise la résolution du modèle
mapping_config=mapping_config, mapping_config=mapping_config,
save_dir=None, show_stats=False, show_plot=False save_dir=None, show_stats=False, show_plot=False
) )
# Figure combinée
fig, axes = plt.subplots(2, 3, figsize=(15, 8)) # Créer une figure combinée avec les deux modèles
img_arr = mpimg.imread(str(image_path)) # Nombre de colonnes : 3 si pas de GT, 4 si GT disponible
axes[0, 0].imshow(img_arr); axes[0, 0].set_title("Image originale"); axes[0, 0].axis('off') num_cols = 3 if not (mask_path_str and os.path.exists(mask_path_str)) else 4
axes[1, 0].imshow(img_arr); axes[1, 0].set_title("Image originale"); axes[1, 0].axis('off') fig, axes = plt.subplots(2, num_cols, figsize=(5*num_cols, 8))
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') # Récupérer les images depuis les figures individuelles
axes[1, 1].imshow(colorize_mask(pred2, mapping_config['group_colors'])) axes1 = fig1.get_axes()
axes[1, 1].set_title(f"Prédiction {second_best_model_info['encoder_name']}"); axes[1, 1].axis('off') axes2 = fig2.get_axes()
if mask_path_str and os.path.exists(mask_path_str):
gt = mpimg.imread(mask_path_str) # Ligne 1 : Modèle 1
axes[0, 2].imshow(gt); axes[0, 2].set_title("Ground Truth"); axes[0, 2].axis('off') for i in range(min(len(axes1), num_cols)):
axes[1, 2].imshow(gt); axes[1, 2].set_title("Ground Truth"); axes[1, 2].axis('off') 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() 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() fig_b64 = base64.b64encode(pickle.dumps(fig)).decode()
# Stats # Stats
def calc(mask): 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) temp_file_path = os.path.join(TEMP_UPLOAD_DIR, temp_filename)
with open(temp_file_path, "wb") as buffer: with open(temp_file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer) shutil.copyfileobj(file.file, buffer)
# Inférences # Inférences avec résolutions spécifiques
pred1, t1, _ = run_inference_and_visualize( pred1, t1, fig1 = run_inference_and_visualize(
image_path=temp_file_path, mask_path=None, 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, img_size=best_model_info["input_size"], mapping_config=mapping_config,
save_dir=None, show_stats=False, show_plot=False, 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, 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, img_size=second_best_model_info["input_size"], mapping_config=mapping_config,
save_dir=None, show_stats=False, show_plot=False, save_dir=None, show_stats=False, show_plot=False,
) )
# Figure
fig, axes = plt.subplots(1, 3, figsize=(15, 5)) # Figure combinée pour uploads (pas de GT, donc 2 colonnes seulement)
img_arr = mpimg.imread(temp_file_path) fig, axes = plt.subplots(2, 2, figsize=(10, 8))
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'])) # Récupérer les images depuis les figures individuelles
axes[1].set_title(f"Prédiction {best_model_info['encoder_name']}"); axes[1].axis('off') axes1 = fig1.get_axes()
axes[2].imshow(colorize_mask(pred2, mapping_config['group_colors'])) axes2 = fig2.get_axes()
axes[2].set_title(f"Prédiction {second_best_model_info['encoder_name']}"); axes[2].axis('off')
# 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() 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() fig_b64 = base64.b64encode(pickle.dumps(fig)).decode()
# Stats # Stats
def calc(mask): def calc(mask):
@ -395,4 +563,4 @@ def get_metrics():
"second_best_model_info": second_best_model_info, "second_best_model_info": second_best_model_info,
"num_classes": mapping_config['num_classes'] if mapping_config else None, "num_classes": mapping_config['num_classes'] if mapping_config else None,
"timestamp": datetime.utcnow().isoformat() "timestamp": datetime.utcnow().isoformat()
} }

View file

@ -2,6 +2,7 @@ import os
import sys import sys
import requests import requests
import streamlit as st import streamlit as st
import pandas as pd
import numpy as np import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from PIL import Image from PIL import Image
@ -67,6 +68,47 @@ st.markdown("""
border-radius: 8px; border-radius: 8px;
margin: 1rem 0; 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;
}
</style> </style>
""", unsafe_allow_html=True) """, unsafe_allow_html=True)
@ -151,23 +193,33 @@ def display_comparison_results(result, models_info):
with col1: with col1:
model1 = result['model1_info'] 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""" st.markdown(f"""
<div class="comparison-card"> <div class="comparison-card">
<h4>🥇 Modèle #1</h4> <h4>🥇 Modèle #1</h4>
<strong>{model1['encoder_name']}</strong><br> <strong>{model1.get('architecture', 'N/A')} - {model1['encoder_name']}</strong><br>
📐 Taille: {model1['input_size']}<br> 📐 Taille: {model1['input_size']}<br>
🆔 Run: {model1['run_id'][:8]}... 📊 mIoU: {miou1}<br>
🎯 Accuracy: {acc1}
</div> </div>
""", unsafe_allow_html=True) """, unsafe_allow_html=True)
with col2: with col2:
model2 = result['model2_info'] 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""" st.markdown(f"""
<div class="comparison-card"> <div class="comparison-card">
<h4>🥈 Modèle #2</h4> <h4>🥈 Modèle #2</h4>
<strong>{model2['encoder_name']}</strong><br> <strong>{model2.get('architecture', 'N/A')} - {model2['encoder_name']}</strong><br>
📐 Taille: {model2['input_size']}<br> 📐 Taille: {model2['input_size']}<br>
🆔 Run: {model2['run_id'][:8]}... 📊 mIoU: {miou2}<br>
🎯 Accuracy: {acc2}
</div> </div>
""", unsafe_allow_html=True) """, 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" winner_class = "speed-winner" if speed_comp['faster_model'] == model1['encoder_name'] else "speed-slower"
st.markdown(f""" st.markdown(f"""
<div class="comparison-card {winner_class}"> <div class="comparison-card {winner_class}">
<h5>🥇 {model1['encoder_name']}</h5> <h5>🥇 {model1.get('architecture', 'N/A')} - {model1['encoder_name']}</h5>
<strong>{model1_time:.1f} ms</strong> <strong>{model1_time:.1f} ms</strong>
</div> </div>
""", unsafe_allow_html=True) """, 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" winner_class = "speed-winner" if speed_comp['faster_model'] == model2['encoder_name'] else "speed-slower"
st.markdown(f""" st.markdown(f"""
<div class="comparison-card {winner_class}"> <div class="comparison-card {winner_class}">
<h5>🥈 {model2['encoder_name']}</h5> <h5>🥈 {model2.get('architecture', 'N/A')} - {model2['encoder_name']}</h5>
<strong>{model2_time:.1f} ms</strong> <strong>{model2_time:.1f} ms</strong>
</div> </div>
""", unsafe_allow_html=True) """, unsafe_allow_html=True)
@ -220,23 +272,21 @@ def display_comparison_results(result, models_info):
st.error(f"Erreur lors de l'affichage: {e}") st.error(f"Erreur lors de l'affichage: {e}")
# Comparaison des statistiques # Comparaison des statistiques
st.markdown("### 📊 Comparaison des Statistiques") st.markdown("### 📊 Répartition des classes par modèle")
col1, col2 = st.columns(2) col1, col2 = st.columns(2)
with col1: with col1:
st.markdown(f"**📈 {model1['encoder_name']} - Répartition des classes:**")
if result.get('model1_stats'): 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: with col2:
st.markdown(f"**📈 {model2['encoder_name']} - Répartition des classes:**")
if result.get('model2_stats'): 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 # Analyse comparative des classes
if result.get('model1_stats') and result.get('model2_stats'): 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): def display_model_stats_chart(stats, models_info, title):
"""Affiche les statistiques d'un modèle sous forme de graphique""" """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") st.warning("Aucune statistique disponible")
return return
# Préparer les données pour le graphique # Utiliser l'ordre des classes du modèle pour la cohérence
class_names = list(stats.keys()) if models_info and 'class_names' in models_info:
percentages = [data['percentage'] for data in stats.values()] # 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 # Récupérer les couleurs des classes
colors = [] colors = []
@ -254,7 +310,7 @@ def display_model_stats_chart(stats, models_info, title):
model_class_names = models_info['class_names'] model_class_names = models_info['class_names']
model_class_colors = models_info['class_colors'] model_class_colors = models_info['class_colors']
for class_name in class_names: for class_name in ordered_class_names:
try: try:
idx = model_class_names.index(class_name) idx = model_class_names.index(class_name)
color = model_class_colors[idx] color = model_class_colors[idx]
@ -262,21 +318,21 @@ def display_model_stats_chart(stats, models_info, title):
except (ValueError, IndexError): except (ValueError, IndexError):
colors.append([0.5, 0.5, 0.5]) colors.append([0.5, 0.5, 0.5])
else: 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 # Créer le graphique
fig, ax = plt.subplots(figsize=(8, 5)) 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_xlabel('Classes de Segmentation', fontsize=10, fontweight='bold')
ax.set_ylabel('Pourcentage (%)', fontsize=10, fontweight='bold') ax.set_ylabel('Pourcentage (%)', fontsize=10, fontweight='bold')
ax.set_title(title, fontsize=11, fontweight='bold', pad=15) ax.set_title(title, fontsize=11, fontweight='bold', pad=15)
# Rotation des labels selon le nombre de classes # 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) 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) plt.xticks(rotation=30, ha='right', fontsize=8)
else: else:
plt.xticks(rotation=45, ha='right', fontsize=8) plt.xticks(rotation=45, ha='right', fontsize=8)
@ -305,15 +361,22 @@ def display_model_stats_chart(stats, models_info, title):
st.pyplot(fig) st.pyplot(fig)
plt.close(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""" """Affiche une comparaison détaillée des classes entre les deux modèles"""
st.markdown("### 🔍 Analyse Comparative par Classe") st.markdown("### 🔍 Analyse Comparative par Classe")
# Combiner toutes les classes présentes # Utiliser l'ordre des classes du modèle pour la cohérence
all_classes = set(stats1.keys()) | set(stats2.keys()) 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 = [] comparison_data = []
for class_name in sorted(all_classes): for class_name in ordered_all_classes:
pct1 = stats1.get(class_name, {}).get('percentage', 0) pct1 = stats1.get(class_name, {}).get('percentage', 0)
pct2 = stats2.get(class_name, {}).get('percentage', 0) pct2 = stats2.get(class_name, {}).get('percentage', 0)
diff = pct1 - pct2 diff = pct1 - pct2
@ -325,27 +388,122 @@ def display_class_comparison(stats1, stats2, model1, model2):
'difference': diff 'difference': diff
}) })
# Afficher sous forme de tableau interactif # Créer un graphique comparatif des barres
col1, col2 = st.columns([2, 1]) st.markdown("**📊 Comparaison des classes entre les modèles:**")
with col1: # Préparation des données
st.markdown("**📋 Tableau Comparatif:**") class_names = [data['class'] for data in comparison_data]
for data in comparison_data: model1_values = [data['model1_pct'] for data in comparison_data]
diff_color = "🟢" if abs(data['difference']) < 1 else ("🔴" if abs(data['difference']) > 5 else "🟡") model2_values = [data['model2_pct'] for data in comparison_data]
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}
""")
with col2: # Récupérer les couleurs des classes
st.markdown("**🎯 Légende:**") colors = []
st.markdown("🟢 Différence < 1%") if models_info and 'class_colors' in models_info and 'class_names' in models_info:
st.markdown("🟡 Différence 1-5%") model_class_names = models_info['class_names']
st.markdown("🔴 Différence > 5%") 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 # Sidebar avec informations des modèles
with st.sidebar: with st.sidebar:
@ -358,37 +516,54 @@ with st.sidebar:
# Modèle 1 # Modèle 1
best_model = models_info.get('best_model', {}) best_model = models_info.get('best_model', {})
st.markdown("**🥇 Meilleur Modèle:**") 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"**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 # Modèle 2
second_model = models_info.get('second_best_model', {}) second_model = models_info.get('second_best_model', {})
st.markdown("**🥈 Deuxième Modèle:**") 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"**Encoder:** {second_model.get('encoder_name', 'N/A')}")
st.write(f"**Run ID:** {second_model.get('run_id', 'N/A')[:8]}...") st.write(f"**Résolution:** {second_model.get('input_size', 'N/A')}")
st.write(f"**Classes:** {models_info.get('num_classes', 'N/A')}")
else: else:
st.error("❌ Modèles non disponibles") st.error("❌ Modèles non disponibles")
st.markdown("---") 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: if models_info and 'class_names' in models_info and 'class_colors' in models_info:
class_names = models_info['class_names'] class_names = models_info['class_names']
class_colors = models_info['class_colors'] 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)): for i, (class_name, color) in enumerate(zip(class_names, class_colors)):
color_hex = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}" 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""" st.markdown(f"""
<div style=" <div style="
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
margin: 0.3rem 0; margin: 0.3rem 0;
font-size: 0.9rem; font-size: 0.85rem;
"> ">
<div style=" <div style="
width: 14px; width: 14px;
@ -398,7 +573,7 @@ with st.sidebar:
border: 1px solid rgba(0,0,0,0.2); border: 1px solid rgba(0,0,0,0.2);
flex-shrink: 0; flex-shrink: 0;
"></div> "></div>
<span style="font-weight: 500;">{i}. {class_name}</span> <span style="font-weight: 500;">{i}. {class_name}{iou_str}</span>
</div> </div>
""", unsafe_allow_html=True) """, unsafe_allow_html=True)

View file

@ -102,38 +102,6 @@ def colorize_mask(mask, group_colors):
return colored_mask 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): 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 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 # Version mise à jour de run_inference_and_visualize
def run_inference_and_visualize(image_path, model, encoder_name, img_size, mapping_config, 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): mask_path=None, save_dir=None, show_stats=True, show_plot=True):