import os
import sys
import requests
import streamlit as st
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import plotly.express as px
import plotly.graph_objects as go
from PIL import Image
import io
import base64
import pickle
# Configuration de l'API
API_BASE_URL = os.getenv("MULTISEG_API_BASE_URL", "http://127.0.0.1:8080/api")
# Import des fonctions utils pour l'affichage local
sys.path.append('/app')
sys.path.append('.')
from utils.utils import colorize_mask
# Configuration de la page avec titre approprié (WCAG 2.4.2)
st.set_page_config(
page_title="Street Vision - Segmentation Comparison | Dashboard Accessible",
page_icon="🚗",
layout="wide"
)
# Ajouts à placer au début de streamlit_app.py après les imports
# Palettes de couleurs conformes WCAG AA (ratio contraste ≥ 3:1)
WCAG_ACCESSIBLE_COLORS = {
'primary_blue': '#0066CC', # [0, 102, 204]
'primary_red': '#CC0000', # [204, 0, 0]
'primary_green': '#006600', # [0, 102, 0]
'primary_orange': '#CC6600', # [204, 102, 0]
'secondary_purple': '#663399', # [102, 51, 153]
'secondary_teal': '#006666', # [0, 102, 102]
'neutral_dark': '#333333', # [51, 51, 51]
'neutral_medium': '#666666', # [102, 102, 102]
'neutral_light': '#CCCCCC', # Pour éléments désactivés
'background_alt': '#F5F5F5' # Fond alternatif accessible
}
# Convertir hex vers RGB
def hex_to_rgb(hex_color):
"""Convertit une couleur hex en tuple RGB"""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
# Couleurs RGB pour les masques (dans le même ordre que les classes)
WCAG_RGB_COLORS = [
hex_to_rgb(WCAG_ACCESSIBLE_COLORS['neutral_medium']), # Flat: gris moyen [102, 102, 102]
hex_to_rgb(WCAG_ACCESSIBLE_COLORS['primary_red']), # Human: rouge [204, 0, 0]
hex_to_rgb(WCAG_ACCESSIBLE_COLORS['secondary_purple']), # Vehicle: violet [102, 51, 153]
hex_to_rgb(WCAG_ACCESSIBLE_COLORS['primary_orange']), # Construction: orange [204, 102, 0]
hex_to_rgb(WCAG_ACCESSIBLE_COLORS['secondary_teal']), # Object: teal [0, 102, 102]
hex_to_rgb(WCAG_ACCESSIBLE_COLORS['primary_green']), # Nature: vert [0, 102, 0]
hex_to_rgb(WCAG_ACCESSIBLE_COLORS['primary_blue']), # Sky: bleu [0, 102, 204]
hex_to_rgb(WCAG_ACCESSIBLE_COLORS['neutral_dark']) # Void: gris foncé [51, 51, 51]
]
# Motifs pour différencier sans couleur uniquement
ACCESSIBILITY_PATTERNS = {
'solid': None,
'diagonal_lines': '///',
'vertical_lines': '|||',
'horizontal_lines': '---',
'dots': '...',
'crosses': 'xxx'
}
def create_accessible_pie_chart_conditional(sizes, labels, title, use_patterns=False):
"""Version corrigée qui règle le problème de lisibilité avec les motifs"""
fig, ax = plt.subplots(figsize=(6, 6), tight_layout=True)
accessible_colors = [
WCAG_ACCESSIBLE_COLORS['primary_blue'], WCAG_ACCESSIBLE_COLORS['primary_red'],
WCAG_ACCESSIBLE_COLORS['primary_green'], WCAG_ACCESSIBLE_COLORS['primary_orange'],
WCAG_ACCESSIBLE_COLORS['secondary_purple'], WCAG_ACCESSIBLE_COLORS['secondary_teal']
]
colors = accessible_colors[:len(sizes)]
# SOLUTION : texte avec fond contrasté
uniform_textprops = {
'fontsize': 10, 'fontweight': 'bold', 'color': 'white',
'bbox': dict(boxstyle="round,pad=0.3", facecolor='black', alpha=0.8)
}
try:
wedges, texts, autotexts = ax.pie(
sizes, labels=labels, colors=colors, autopct='%1.0f%%', startangle=90, shadow=True,
textprops=uniform_textprops, wedgeprops={'edgecolor': 'white', 'linewidth': 2},
pctdistance=0.85 # Éloigner les pourcentages
)
# Améliorer la lisibilité des pourcentages
for autotext in autotexts:
autotext.set_bbox(dict(boxstyle="round,pad=0.4", facecolor='black', alpha=0.9))
autotext.set_color('white')
autotext.set_fontweight('bold')
autotext.set_fontsize(9)
# Motifs APRÈS configuration du texte
if use_patterns:
patterns = ['///', '|||', '---', '...', 'xxx', '+++']
for wedge, pattern in zip(wedges, patterns[:len(wedges)]):
if pattern:
wedge.set_hatch(pattern)
ax.set_title(title, fontsize=10, fontweight='bold', pad=15, color='#333333')
ax.axis('equal')
chart_displayed = True
except Exception as e:
ax.text(0.5, 0.5, f"Erreur d'affichage du graphique\n{str(e)}",
ha='center', va='center', transform=ax.transAxes, fontsize=10, color='red', weight='bold')
chart_displayed = False
return fig, ax, {'colors': colors, 'patterns': patterns if use_patterns else None}, chart_displayed
# Fonctions Plotly pour graphiques interactifs
def create_interactive_pie_chart(sizes, labels, title, models_info=None):
"""Crée un pie chart interactif avec les couleurs des classes définies dans models_info"""
# Utiliser les couleurs des classes si disponibles
if models_info and 'class_colors' in models_info and 'class_names' in models_info:
colors = []
class_names = models_info['class_names']
class_colors = models_info['class_colors']
for label in labels:
if label in class_names:
idx = class_names.index(label)
color_rgb = class_colors[idx]
color_hex = f"#{color_rgb[0]:02x}{color_rgb[1]:02x}{color_rgb[2]:02x}"
colors.append(color_hex)
else:
# Couleur par défaut si classe non trouvée
colors.append('#666666')
else:
# Fallback vers les couleurs WCAG
accessible_colors_list = list(WCAG_ACCESSIBLE_COLORS.values())
colors = accessible_colors_list[:len(sizes)]
df = pd.DataFrame({'labels': labels, 'values': sizes, 'colors': colors})
fig = go.Figure(data=[go.Pie(
labels=df['labels'],
values=df['values'],
marker=dict(
colors=df['colors'],
line=dict(color='#FFFFFF', width=3),
pattern=dict(
shape=['', '/', '\\', '|', '-', '+', 'x', '.'][:len(sizes)],
size=8,
solidity=0.3
)
),
textinfo='label+percent',
textposition='auto',
textfont=dict(
size=13,
color='white',
family="Arial Black"
),
hovertemplate='%{label}
Valeur: %{value}
Pourcentage: %{percent}
',
hoverlabel=dict(
bgcolor="rgba(255,255,255,0.95)",
bordercolor="#333333",
font=dict(size=14, color="#333333")
)
)])
fig.update_layout(
title={
'text': title,
'x': 0.5,
'xanchor': 'center',
'font': {'size': 16, 'color': '#333333', 'family': 'Arial Black'}
},
showlegend=True,
legend=dict(
orientation="v",
yanchor="middle",
y=0.5,
xanchor="left",
x=1.02,
font=dict(size=12, color='#333333'),
bgcolor='rgba(255,255,255,0.9)',
bordercolor='#333333',
),
margin=dict(l=20, r=120, t=60, b=20),
height=400,
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)',
font=dict(color='#333333')
)
return fig
def create_interactive_comparison_chart(model1_stats, model2_stats, model1_name, model2_name):
"""Crée un graphique en barres interactif WCAG-compliant pour comparer les deux modèles"""
classes = list(set(model1_stats.keys()) | set(model2_stats.keys()))
data = []
for class_name in classes:
val1 = model1_stats.get(class_name, {}).get('percentage', 0)
val2 = model2_stats.get(class_name, {}).get('percentage', 0)
data.append({'Classe': class_name, 'Modèle': model1_name, 'Pourcentage': val1, 'Pixels': model1_stats.get(class_name, {}).get('pixels', 0)})
data.append({'Classe': class_name, 'Modèle': model2_name, 'Pourcentage': val2, 'Pixels': model2_stats.get(class_name, {}).get('pixels', 0)})
df = pd.DataFrame(data)
# Filtrer les données pour supprimer les lignes vides potentielles
df = df[df['Modèle'].notna() & (df['Modèle'] != '')]
fig = px.bar(
df, x='Classe', y='Pourcentage', color='Modèle', barmode='group',
title='Comparaison interactive des classes détectées',
hover_data=['Pixels'],
color_discrete_map={model1_name: '#0066CC', model2_name: '#CC0000'}
# PAS de pattern_shape du tout
)
fig.update_layout(
title={'x': 0.5, 'xanchor': 'center', 'font': {'size': 16, 'color': '#333333', 'family': 'Arial Black'}},
xaxis=dict(
title='Classes de segmentation',
titlefont=dict(size=14, color='#666666'), # Plus clair comme vos pie charts
tickfont=dict(size=12, color='#666666'), # Plus clair
tickangle=45,
showgrid=True,
gridcolor='#E8E8E8', # Grille beaucoup plus claire
gridwidth=1,
linecolor='#AAAAAA', # Axe gris moyen
linewidth=1,
showline=True
),
yaxis=dict(
title='Pourcentage de l\'image (%)',
titlefont=dict(size=14, color='#666666'), # Plus clair
tickfont=dict(size=12, color='#666666'), # Plus clair
showgrid=True,
gridcolor='#E8E8E8', # Grille beaucoup plus claire
gridwidth=1,
linecolor='#AAAAAA', # Axe gris moyen
linewidth=1,
showline=True
),
legend=dict(
title='Modèles',
font=dict(size=12, color='#333333'), # Comme vos pie charts
bgcolor='rgba(255,255,255,0.9)',
bordercolor='#333333',
),
hovermode='x unified',
height=500,
plot_bgcolor='rgba(0,0,0,0)', # Transparent
paper_bgcolor='rgba(0,0,0,0)', # Transparent
font=dict(color='#333333') # Sans spécifier size qui peut poser problème
)
fig.update_traces(
texttemplate='%{y:.1f}%',
textposition='outside',
textfont=dict(size=11, color='#666666'), # Plus clair pour lisibilité
marker=dict(line=dict(color='#FFFFFF', width=1)) # Bordures blanches comme vos pie charts
)
return fig
def create_dataset_interactive_overview():
"""Vue d'ensemble interactive du dataset avec les MÊMES couleurs que 'Visualisations accessibles'"""
# RÉORGANISER les données pour que l'ordre soit logique et que les couleurs correspondent
dataset_data = {
'Split': ['Train (notre projet)', 'Validation (notre projet)', 'Test final', 'Non utilisé'],
'Images': [2380, 595, 500, 21525],
'Utilisation': ['Entraînement', 'Validation', 'Test', 'Non utilisé']
}
df = pd.DataFrame(dataset_data)
# Couleurs dans l'ordre exact des données
colors = [
WCAG_ACCESSIBLE_COLORS['primary_blue'], # Train (notre projet) - bleu
WCAG_ACCESSIBLE_COLORS['primary_red'], # Validation (notre projet) - rouge
WCAG_ACCESSIBLE_COLORS['primary_orange'], # Test final - orange
WCAG_ACCESSIBLE_COLORS['neutral_dark'] # Non utilisé - gris foncé
]
# FORCER l'ordre avec category_orders
fig = px.pie(
df,
values='Images',
names='Split',
title='Répartition interactive du dataset Cityscapes',
color_discrete_sequence=colors,
hover_data=['Utilisation'],
category_orders={'Split': ['Train (notre projet)', 'Validation (notre projet)', 'Test final', 'Non utilisé']}
)
# FORCER la correspondance couleur-catégorie avec color_discrete_map
color_map = {
'Train (notre projet)': WCAG_ACCESSIBLE_COLORS['primary_blue'],
'Validation (notre projet)': WCAG_ACCESSIBLE_COLORS['primary_red'],
'Test final': WCAG_ACCESSIBLE_COLORS['primary_orange'],
'Non utilisé': WCAG_ACCESSIBLE_COLORS['neutral_dark']
}
fig.update_traces(
marker=dict(
colors=[color_map[split] for split in df['Split']], # Forcer les couleurs exactes
line=dict(color='#FFFFFF', width=3),
pattern=dict(
shape=['|', '/', '\\', ''],
size=8,
solidity=0.3
)
),
textposition='auto',
textinfo='percent+label',
textfont=dict(size=12, color='white', family="Arial Black"),
hovertemplate='%{label}
Images: %{value}
Usage: %{customdata[0]}'
)
fig.update_layout(
title={'x': 0.5, 'xanchor': 'center', 'font': {'size': 16, 'color': '#333333'}},
height=500,
legend=dict(
font=dict(size=12, color='#333333'),
bgcolor='rgba(255,255,255,0.9)',
bordercolor='#333333',
),
font=dict(color='#333333')
)
return fig
def create_accessible_bar_chart_conditional(data, labels, title, colors=None, use_patterns=False):
"""
Crée un bar chart accessible - descriptions uniquement si erreur d'affichage
"""
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10, 6))
if colors is None:
colors = [WCAG_ACCESSIBLE_COLORS['primary_blue']] * len(data)
# Motifs pour différenciation
patterns = None
if use_patterns:
pattern_list = list(ACCESSIBILITY_PATTERNS.values())
patterns = pattern_list[:len(data)]
try:
bars = ax.bar(
labels,
data,
color=colors,
alpha=0.8,
edgecolor='#333333',
linewidth=1
)
# Ajouter des motifs
if patterns:
for bar, pattern in zip(bars, patterns):
if pattern:
bar.set_hatch(pattern)
# Style accessible
ax.set_ylabel('Valeur', fontweight='bold', color='#333333')
ax.set_xlabel('Catégories', fontweight='bold', color='#333333')
ax.set_title(title, fontsize=14, fontweight='bold', pad=20, color='#333333')
# Grille pour faciliter la lecture
ax.grid(axis='y', alpha=0.3, linestyle='--', color='#666666')
ax.set_axisbelow(True)
# Valeurs sur les barres pour accessibilité
for bar, value in zip(bars, data):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height + max(data) * 0.01,
f'{value:.1f}' if isinstance(value, float) else str(value),
ha='center', va='bottom', fontsize=10, fontweight='bold', color='#333333')
# Style des axes
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color('#666666')
ax.spines['bottom'].set_color('#666666')
plt.xticks(rotation=45, ha='right', color='#333333')
plt.yticks(color='#333333')
chart_displayed = True
except Exception as e:
# En cas d'erreur, message d'erreur
ax.text(0.5, 0.5, f"Erreur d'affichage du graphique\n{str(e)}",
ha='center', va='center', transform=ax.transAxes,
fontsize=12, color='red', weight='bold')
chart_displayed = False
return fig, ax, chart_displayed
# Fonction pour générer du texte alternatif (WCAG 1.1.1)
def generate_alt_text_for_pie(sizes, labels, title):
"""
Génère un texte alternatif détaillé pour un pie chart
"""
total = sum(sizes)
descriptions = []
for size, label in zip(sizes, labels):
percentage = (size / total) * 100
descriptions.append(f"{label}: {size} éléments ({percentage:.1f}%)")
alt_text = f"Graphique en secteurs: {title}. "
alt_text += f"Total de {total} éléments répartis comme suit: "
alt_text += "; ".join(descriptions) + "."
return alt_text
# Fonction pour générer du texte alternatif (WCAG 1.1.1)
def generate_alt_text_for_images(image_type, model_name=None, stats=None):
"""Génère un texte alternatif détaillé pour les images de segmentation"""
if image_type == "original":
return "Image originale en couleur montrant une scène urbaine typique du dataset Cityscapes"
elif image_type == "segmentation":
base_text = f"Masque de segmentation sémantique prédit par le modèle {model_name or 'IA'}"
if stats:
# Ajouter les statistiques principales
main_classes = sorted(stats.items(), key=lambda x: x[1]['percentage'], reverse=True)[:3]
stats_text = ". Classes principales détectées: " + ", ".join([
f"{name} ({data['percentage']:.1f}%)" for name, data in main_classes
])
return base_text + stats_text
return base_text
elif image_type == "comparison":
return "Visualisation comparative des prédictions de deux modèles de segmentation sémantique"
else:
return "Image de segmentation sémantique"
def generate_alt_text_for_bar(data, labels, title, unit=""):
"""
Génère un texte alternatif détaillé pour un bar chart
"""
descriptions = []
for value, label in zip(data, labels):
descriptions.append(f"{label}: {value}{unit}")
alt_text = f"Graphique en barres: {title}. "
alt_text += "Valeurs: " + "; ".join(descriptions) + "."
return alt_text
def generate_comprehensive_alt_text(image_type, model_name=None, stats=None, comparison_data=None):
"""Génère un texte alternatif complet selon WCAG 1.1.1"""
if image_type == "preprocessing_original":
return "Image urbaine originale redimensionnée à la résolution d'entraînement du modèle, montrant une scène typique du dataset Cityscapes avec véhicules, piétons et infrastructure urbaine"
elif image_type == "preprocessing_normalized":
return "Même image après normalisation des pixels pour l'entraînement du modèle IA, les couleurs peuvent paraître légèrement différentes"
elif image_type == "preprocessing_augmented":
return "Image avec augmentations de données appliquées (rotation, changement de luminosité, etc.) pour améliorer la robustesse du modèle"
elif image_type == "segmentation_prediction":
base_text = f"Masque de segmentation sémantique coloré prédit par le modèle {model_name or 'IA'}"
if stats:
main_classes = sorted(stats.items(), key=lambda x: x[1]['percentage'], reverse=True)[:3]
stats_text = ". Classes principales détectées : " + ", ".join([
f"{name} ({data['percentage']:.1f}% de l'image)" for name, data in main_classes
])
return base_text + stats_text
return base_text
elif image_type == "comparison_grid":
return "Grille comparative montrant l'image originale et les prédictions de segmentation de deux modèles d'IA différents côte à côte pour évaluation"
else:
return "Image de segmentation sémantique urbaine générée par intelligence artificielle"
def create_data_table(data, columns, title, description=""):
"""
Crée un tableau de données accessible complémentaire aux graphiques
"""
import pandas as pd
df = pd.DataFrame(data, columns=columns)
st.markdown(f"**Tableau de données: {title}**")
if description:
st.markdown(f"*{description}*")
# Configuration accessible du dataframe
st.dataframe(
df,
use_container_width=True,
hide_index=True,
column_config={
col: st.column_config.TextColumn(col, width="medium")
for col in columns
}
)
return df
# CSS amélioré pour l'accessibilité complète
ENHANCED_WCAG_CSS = """
"""
st.markdown(ENHANCED_WCAG_CSS, unsafe_allow_html=True)
# En-tête principal
st.markdown("""
🚗 Street Vision - Comparaison de Modèles
Évaluation comparative SegFormer vs FPN sur le dataset Cityscapes
""", unsafe_allow_html=True)
# Fonctions utilitaires
def get_models_info():
"""Récupère les informations des deux modèles"""
try:
response = requests.get(f"{API_BASE_URL}/models")
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur lors de la récupération des infos modèles: {response.status_code}")
return None
except Exception as e:
st.error(f"Erreur de connexion à l'API: {e}")
return None
def get_dataset_info():
"""Récupère les informations sur le dataset"""
try:
response = requests.get(f"{API_BASE_URL}/dataset-info")
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur lors de la récupération des infos dataset: {response.status_code}")
return None
except Exception as e:
st.error(f"Erreur de connexion à l'API: {e}")
return None
def get_train_images():
"""Récupère la liste des images d'entraînement"""
try:
response = requests.get(f"{API_BASE_URL}/train-images")
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur lors de la récupération des images: {response.status_code}")
return None
except Exception as e:
st.error(f"Erreur de connexion à l'API: {e}")
return None
def get_class_repartition():
"""Récupère le graphique de répartition des classes"""
try:
response = requests.get(f"{API_BASE_URL}/class-repartition", timeout=10)
if response.status_code == 200:
return response.content
else:
return None
except Exception as e:
st.warning(f"Graphique de répartition non disponible: {e}")
return None
def preprocess_train_image(filename, apply_augmentation=False):
"""Lance le preprocessing sur une image d'entraînement"""
try:
data = {
"filename": filename,
"apply_augmentation": apply_augmentation
}
response = requests.post(
f"{API_BASE_URL}/preprocess-train-image",
json=data,
headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur API: {response.status_code} - {response.text}")
return None
except Exception as e:
st.error(f"Erreur lors du preprocessing: {e}")
return None
def get_sample_images():
"""Récupère la liste des images d'exemple"""
try:
response = requests.get(f"{API_BASE_URL}/sample-images")
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur lors de la récupération des images: {response.status_code}")
return None
except Exception as e:
st.error(f"Erreur de connexion à l'API: {e}")
return None
def compare_sample_image(filename):
"""Lance une comparaison sur une image d'exemple"""
try:
data = {"filename": filename}
response = requests.post(
f"{API_BASE_URL}/compare-sample",
json=data,
headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur API: {response.status_code} - {response.text}")
return None
except Exception as e:
st.error(f"Erreur lors de la comparaison: {e}")
return None
def compare_uploaded_image(uploaded_file):
"""Lance une comparaison sur une image uploadée"""
try:
files = {"file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)}
response = requests.post(f"{API_BASE_URL}/compare-upload", files=files)
if response.status_code == 200:
return response.json()
else:
st.error(f"Erreur API: {response.status_code} - {response.text}")
return None
except Exception as e:
st.error(f"Erreur lors de la comparaison: {e}")
return None
def display_comparison_results(result, models_info, context=""):
"""Affiche les résultats de comparaison avec graphiques interactifs et accessibilité complète"""
if not result or not result['success']:
st.error("Aucun résultat de comparaison disponible")
return
# Informations des modèles
st.markdown("### 🏆 Modèles Comparés")
col1, col2 = st.columns(2)
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.get('architecture', 'N/A')} - {model1['encoder_name']}
📐 Taille: {model1['input_size']}
📊 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.get('architecture', 'N/A')} - {model2['encoder_name']}
📐 Taille: {model2['input_size']}
📊 mIoU: {miou2}
🎯 Accuracy: {acc2}
""", unsafe_allow_html=True)
# Comparaison des temps d'inférence
st.markdown("### ⏱️ Comparaison des Performances")
speed_comp = result['speed_comparison']
model1_time = result['model1_inference_time'] * 1000
model2_time = result['model2_inference_time'] * 1000
col1, col2, col3 = st.columns(3)
with col1:
winner_class = "speed-winner" if speed_comp['faster_model'] == model1['encoder_name'] else "speed-slower"
st.markdown(f"""
🥇 {model1.get('architecture', 'N/A')} - {model1['encoder_name']}
{model1_time:.1f} ms
""", unsafe_allow_html=True)
with col2:
st.markdown(f"""
⚡ Comparaison
{speed_comp['faster_model']}
est {speed_comp['speedup_ratio']}x plus rapide
Différence: {speed_comp['time_difference_ms']:.1f} ms
""", unsafe_allow_html=True)
with col3:
winner_class = "speed-winner" if speed_comp['faster_model'] == model2['encoder_name'] else "speed-slower"
st.markdown(f"""
🥈 {model2.get('architecture', 'N/A')} - {model2['encoder_name']}
{model2_time:.1f} ms
""", unsafe_allow_html=True)
# Affichage des segmentations avec descriptions alternatives CONDITIONNELLES
if result.get('figure_data'):
st.markdown("### 🎨 Résultats de Segmentation")
try:
fig_bytes = base64.b64decode(result['figure_data'])
fig = pickle.loads(fig_bytes)
st.pyplot(fig)
plt.close(fig)
except Exception as e:
st.error(f"Erreur lors de l'affichage: {e}")
# Description alternative SEULEMENT en cas d'erreur
alt_text = generate_comprehensive_alt_text("comparison_grid")
st.warning(f"**Description alternative:** {alt_text}")
# Section interactive (CE2 - Graphiques interactifs) - SEULEMENT 2 ONGLETS
st.markdown("### 📊 Analyse Interactive des Résultats")
if result.get('model1_stats') and result.get('model2_stats'):
# SUPPRIMER l'onglet "Performances"
tab1, tab2 = st.tabs(["🥧 Répartitions", "📊 Comparaison"])
with tab1:
col1, col2 = st.columns(2)
with col1:
st.markdown("#### Répartition Modèle 1")
sizes1 = [data['percentage'] for data in result['model1_stats'].values()]
labels1 = list(result['model1_stats'].keys())
fig_pie1 = create_interactive_pie_chart(sizes1, labels1,
f"Distribution - {result['model1_info'].get('architecture', 'N/A')} {result['model1_info']['encoder_name']}",
models_info)
# AJOUTER key unique pour éviter l'erreur Streamlit
st.plotly_chart(fig_pie1, use_container_width=True, key=f"pie_model1_{context}")
with col2:
st.markdown("#### Répartition Modèle 2")
sizes2 = [data['percentage'] for data in result['model2_stats'].values()]
labels2 = list(result['model2_stats'].keys())
fig_pie2 = create_interactive_pie_chart(sizes2, labels2,
f"Distribution - {result['model2_info'].get('architecture', 'N/A')} {result['model2_info']['encoder_name']}",
models_info)
# AJOUTER key unique pour éviter l'erreur Streamlit
st.plotly_chart(fig_pie2, use_container_width=True, key=f"pie_model2_{context}")
with tab2:
st.markdown("#### Comparaison Interactive des Classes")
fig_comparison = create_interactive_comparison_chart(
result['model1_stats'], result['model2_stats'],
f"{result['model1_info'].get('architecture', 'N/A')} {result['model1_info']['encoder_name']}",
f"{result['model2_info'].get('architecture', 'N/A')} {result['model2_info']['encoder_name']}"
)
# AJOUTER key unique pour éviter l'erreur Streamlit
st.plotly_chart(fig_comparison, use_container_width=True, key=f"comparison_chart_{context}")
st.info("💡 **Interactivité :** Survolez les barres, cliquez sur la légende, zoomez en sélectionnant une zone.")
# SUPPRIMER la description systématique
# Garder seulement le tableau détaillé (sans graphique redondant)
if result.get('model1_stats') and result.get('model2_stats'):
display_class_comparison(result['model1_stats'], result['model2_stats'],
result['model1_info'], result['model2_info'], models_info)
# def display_model_stats_chart(stats, models_info, title):
# """Affiche les statistiques d'un modèle sous forme de graphique accessible"""
# if not stats:
# st.warning("Aucune statistique disponible")
# return
# # Utiliser l'ordre des classes du modèle pour la cohérence
# if models_info and 'class_names' in models_info:
# ordered_class_names = [name for name in models_info['class_names'] if name in stats]
# else:
# ordered_class_names = sorted(stats.keys())
# percentages = [stats[name]['percentage'] for name in ordered_class_names]
# # Couleurs accessibles WCAG
# accessible_colors = [
# WCAG_ACCESSIBLE_COLORS['neutral_medium'], # Flat: gris moyen
# WCAG_ACCESSIBLE_COLORS['primary_red'], # Human: rouge
# WCAG_ACCESSIBLE_COLORS['secondary_purple'], # Vehicle: violet
# WCAG_ACCESSIBLE_COLORS['primary_orange'], # Construction: orange
# WCAG_ACCESSIBLE_COLORS['secondary_teal'], # Object: teal
# WCAG_ACCESSIBLE_COLORS['primary_green'], # Nature: vert
# WCAG_ACCESSIBLE_COLORS['primary_blue'], # Sky: bleu
# WCAG_ACCESSIBLE_COLORS['neutral_dark'] # Void: gris foncé
# ]
# colors = accessible_colors[:len(ordered_class_names)]
# # Créer le graphique accessible
# fig, ax, displayed = create_accessible_bar_chart_conditional(
# percentages, ordered_class_names, title,
# colors=colors, use_patterns=True
# )
# # Description alternative SEULEMENT si erreur d'affichage
# if not displayed:
# alt_text = generate_alt_text_for_bar(
# percentages, ordered_class_names, title, unit="%"
# )
# st.warning(f"**Description du graphique:** {alt_text}")
# st.pyplot(fig)
# plt.close(fig)
# # Tableau de données complémentaire (toujours affiché)
# table_data = []
# for name, percentage in zip(ordered_class_names, percentages):
# pixels = stats[name]['pixels']
# table_data.append([name, f"{percentage:.1f}%", f"{pixels:,} pixels"])
# create_data_table(
# table_data,
# ["Classe", "Pourcentage", "Nombre de pixels"],
# f"Données - {title}",
# "Répartition détaillée des classes de segmentation"
# )
def display_class_comparison(stats1, stats2, model1, model2, models_info):
"""Affiche uniquement le tableau détaillé de comparaison des classes (sans graphique redondant)"""
st.markdown("### 🔎 Tableau Détaillé des Comparaisons")
# Préparer les données
if models_info and 'class_names' in models_info:
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:
all_classes = set(stats1.keys()) | set(stats2.keys())
ordered_all_classes = sorted(all_classes)
class_names = ordered_all_classes
model1_values = [stats1.get(name, {}).get('percentage', 0) for name in class_names]
model2_values = [stats2.get(name, {}).get('percentage', 0) for name in class_names]
# SUPPRIMER tout le code de génération du graphique matplotlib
# GARDER seulement le tableau
table_data = []
for i, name in enumerate(class_names):
val1 = model1_values[i]
val2 = model2_values[i]
diff = val1 - val2
if abs(diff) < 1:
category = "🟢 Similaire"
elif abs(diff) < 5:
category = "🟡 Différence modérée"
else:
category = "🔴 Différence importante"
table_data.append([
name,
f"{val1:.1f}%",
f"{val2:.1f}%",
f"{diff:+.1f}%",
category
])
create_data_table(
table_data,
[
"Classe",
f"🥇 {model1.get('architecture', 'N/A')[:15]} (%)",
f"🥈 {model2.get('architecture', 'N/A')[:15]} (%)",
"Différence",
"Évaluation"
],
"Comparaison détaillée par classe",
"Analyse des écarts de performance entre les deux modèles pour chaque classe de segmentation"
)
# def display_class_comparison(stats1, stats2, model1, model2, models_info):
# """Affiche une comparaison accessible des classes entre les deux modèles"""
# st.markdown("### 🔍 Analyse Comparative par Classe")
# # Préparer les données
# if models_info and 'class_names' in models_info:
# 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:
# all_classes = set(stats1.keys()) | set(stats2.keys())
# ordered_all_classes = sorted(all_classes)
# class_names = ordered_all_classes
# model1_values = [stats1.get(name, {}).get('percentage', 0) for name in class_names]
# model2_values = [stats2.get(name, {}).get('percentage', 0) for name in class_names]
# # Couleurs accessibles avec motifs différents
# fig, ax = plt.subplots(figsize=(12, 7))
# x = np.arange(len(class_names))
# width = 0.35
# # Couleurs accessibles
# color1 = WCAG_ACCESSIBLE_COLORS['primary_blue']
# color2 = WCAG_ACCESSIBLE_COLORS['primary_red']
# bars1 = ax.bar(x - width/2, model1_values, width,
# label=f"{model1.get('architecture', 'N/A')} - {model1['encoder_name']}",
# color=color1, alpha=0.8, edgecolor='#333333')
# bars2 = ax.bar(x + width/2, model2_values, width,
# label=f"{model2.get('architecture', 'N/A')} - {model2['encoder_name']}",
# color=color2, alpha=0.8, edgecolor='#333333', hatch='///')
# # Style accessible
# ax.set_xlabel('Classes', fontsize=11, fontweight='bold', color='#333333')
# ax.set_ylabel('Pourcentage (%)', fontsize=11, fontweight='bold', color='#333333')
# ax.set_title('Comparaison des classes entre les deux modèles',
# fontsize=13, fontweight='bold', pad=15, color='#333333')
# ax.set_xticks(x)
# if len(class_names) <= 4:
# ax.set_xticklabels(class_names, fontsize=10, color='#333333')
# elif len(class_names) <= 6:
# ax.set_xticklabels(class_names, rotation=30, ha='right', fontsize=9, color='#333333')
# else:
# ax.set_xticklabels(class_names, rotation=45, ha='right', fontsize=9, color='#333333')
# ax.legend(loc='upper right', fontsize=10)
# ax.grid(axis='y', alpha=0.3, linestyle='--', color='#666666')
# ax.set_axisbelow(True)
# # Style des axes
# ax.spines['top'].set_visible(False)
# ax.spines['right'].set_visible(False)
# ax.spines['left'].set_color('#666666')
# ax.spines['bottom'].set_color('#666666')
# # Valeurs sur les barres
# for bar1, bar2, val1, val2 in zip(bars1, bars2, model1_values, model2_values):
# height1, height2 = bar1.get_height(), 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', color='#333333')
# ax.text(bar2.get_x() + bar2.get_width()/2., height2 + 0.5,
# f'{val2:.1f}%', ha='center', va='bottom', fontsize=8,
# fontweight='bold', color='#333333')
# plt.tight_layout()
# # Description alternative
# alt_text = f"Graphique comparatif en barres des classes entre {model1['encoder_name']} et {model2['encoder_name']}. "
# comparisons = []
# for name, val1, val2 in zip(class_names, model1_values, model2_values):
# comparisons.append(f"{name}: {val1:.1f}% vs {val2:.1f}%")
# alt_text += "Comparaisons: " + "; ".join(comparisons)
# st.markdown(f"**Description du graphique:** {alt_text}")
# st.pyplot(fig)
# plt.close(fig)
# # Tableau détaillé accessible
# st.markdown("**🔎 Tableau détaillé des comparaisons:**")
# table_data = []
# for i, name in enumerate(class_names):
# val1 = model1_values[i]
# val2 = model2_values[i]
# diff = val1 - val2
# if abs(diff) < 1:
# category = "🟢 Similaire"
# elif abs(diff) < 5:
# category = "🟡 Différence modérée"
# else:
# category = "🔴 Différence importante"
# table_data.append([
# name,
# f"{val1:.1f}%",
# f"{val2:.1f}%",
# f"{diff:+.1f}%",
# category
# ])
# create_data_table(
# table_data,
# [
# "Classe",
# f"🥇 {model1.get('architecture', 'N/A')[:15]} (%)",
# f"🥈 {model2.get('architecture', 'N/A')[:15]} (%)",
# "Différence",
# "Évaluation"
# ],
# "Comparaison détaillée par classe",
# "Analyse des écarts de performance entre les deux modèles pour chaque classe de segmentation"
# )
# Onglets principaux
main_tab1, main_tab2 = st.tabs(["📊 Présentation Jeu de données Cityscapes", "🔄 Comparaison de performances"])
# ===== ONGLET 1: PRÉSENTATION DATASET =====
with main_tab1:
st.markdown("## 📊 Analyse Exploratoire - Dataset Cityscapes")
# Informations générales sur le dataset
dataset_info = get_dataset_info()
if dataset_info:
# Section méthodologique MODIFIÉE
st.markdown("""
### 🎯 Présentation générale du dataset Cityscapes
#### Dataset retenu
**Cityscapes Dataset : Référence en Segmentation Urbaine**
""")
# Spécificités techniques
st.markdown("""
### ⚙️ Spécificités techniques
- **Taille Datase**: 25 000 images haute résolution
- **Résolutions d'acquisition** : Données natives 2048×1024 pixels
- **Conditions d'acquisition** : Conditions météo bonnes à moyennes, pas de conditions adverses (pluie, neige)
- **Diversité géographique** : Villes allemandes et suisses (architectures variées)
- **Annotations pixel-perfect** : Masques de segmentation précis au pixel près (Train + Val)
Ce dataset permet d'évaluer la capacité des modèles à comprendre les scènes urbaines complexes, élément fondamental pour les applications de conduite autonome et de surveillance urbaine intelligente.
""")
# Composition du dataset avec pie charts NOUVEAU
st.markdown("### 🗂️ Composition et utilisation du dataset")
# Version finale qui combine : tailles uniformes + couleurs WCAG + descriptions conditionnelles
if dataset_info.get('splits'):
splits_info = dataset_info['splits']
project_splits = dataset_info.get('project_splits', {})
st.markdown("#### 📊 Visualisations accessibles")
# Option pour activer les motifs (accessibilité)
use_patterns = st.checkbox(
"🎨 Activer les motifs (accessibilité visuelle)",
help="Active les motifs en plus des couleurs pour faciliter la distinction des données",
value=True
)
# Organiser en 1 ligne de 4 colonnes
col1, col2, col3, col4 = st.columns(4)
# Configuration uniforme pour tous les graphiques (SOLUTION QUI MARCHAIT)
uniform_figsize = (6, 6)
uniform_textprops = {'fontsize': 8, 'fontweight': 'bold', 'color': '#333333'}
uniform_title_props = {'fontsize': 10, 'fontweight': 'bold', 'pad': 15}
# Couleurs WCAG accessibles
accessible_colors = {
'blue': WCAG_ACCESSIBLE_COLORS['primary_blue'],
'red': WCAG_ACCESSIBLE_COLORS['primary_red'],
'orange': WCAG_ACCESSIBLE_COLORS['primary_orange'],
'gray': WCAG_ACCESSIBLE_COLORS['neutral_medium'],
'light_gray': WCAG_ACCESSIBLE_COLORS['neutral_light']
}
# Motifs pour accessibilité
patterns = ['///', '|||', '---', '...', 'xxx', None] if use_patterns else [None] * 6
# Données pour le tableau récapitulatif
pie_data_for_table = []
with col1:
# Pie chart Train (avec répartition projet)
fig_train, ax_train = plt.subplots(figsize=uniform_figsize, tight_layout=True)
train_total = splits_info['train']['images']
train_our_train = project_splits['train']['images']
train_our_val = project_splits['validation']['images']
sizes = [train_our_train, train_our_val]
labels = [f'Notre Entraînement\n{train_our_train} images (80%)',
f'Notre Validation\n{train_our_val} images (20%)']
colors = [accessible_colors['blue'], accessible_colors['red']]
explode = (0.05, 0.05)
try:
wedges, texts, autotexts = ax_train.pie(
sizes, labels=labels, colors=colors, autopct='%1.0f%%',
startangle=90, explode=explode, shadow=True,
textprops=uniform_textprops,
wedgeprops={'edgecolor': 'white', 'linewidth': 2}
)
# Ajouter des motifs si demandé
if use_patterns:
for wedge, pattern in zip(wedges, patterns[:2]):
if pattern:
wedge.set_hatch(pattern)
ax_train.set_title(f'Split Train Original\n{train_total} images total',
**uniform_title_props, color='#333333')
ax_train.axis('equal')
chart_displayed = True
except Exception as e:
ax_train.text(0.5, 0.5, f"Erreur d'affichage\n{str(e)}",
ha='center', va='center', transform=ax_train.transAxes,
fontsize=10, color='red', weight='bold')
chart_displayed = False
# Description SEULEMENT si erreur
if not chart_displayed:
alt_text = generate_alt_text_for_pie(sizes, labels, "Split Train")
st.warning(f"**Description du graphique:** {alt_text}")
st.pyplot(fig_train)
plt.close(fig_train)
pie_data_for_table.extend([
["Train - Entraînement", train_our_train, "80%", "Entraînement des modèles"],
["Train - Validation", train_our_val, "20%", "Validation pendant entraînement"]
])
with col2:
# Pie chart Validation (utilisation complète)
fig_val, ax_val = plt.subplots(figsize=uniform_figsize, tight_layout=True)
val_total = splits_info['validation']['images']
val_our_test = project_splits['test']['images']
sizes = [val_our_test]
labels = [f'Notre Test Final\n{val_our_test} images (100%)']
colors = [accessible_colors['orange']]
try:
wedges, texts, autotexts = ax_val.pie(
sizes, labels=labels, colors=colors, autopct='%1.0f%%',
startangle=90, shadow=True, textprops=uniform_textprops,
wedgeprops={'edgecolor': 'white', 'linewidth': 2}
)
if use_patterns and patterns[2]:
wedges[0].set_hatch(patterns[2])
ax_val.set_title(f'Split Validation Original\n{val_total} images total',
**uniform_title_props, color='#333333')
ax_val.axis('equal')
chart_displayed = True
except Exception as e:
ax_val.text(0.5, 0.5, f"Erreur d'affichage\n{str(e)}",
ha='center', va='center', transform=ax_val.transAxes,
fontsize=10, color='red', weight='bold')
chart_displayed = False
if not chart_displayed:
alt_text = generate_alt_text_for_pie(sizes, labels, "Split Validation")
st.warning(f"**Description du graphique:** {alt_text}")
st.pyplot(fig_val)
plt.close(fig_val)
pie_data_for_table.append(["Validation - Test", val_our_test, "100%", "Test final des modèles"])
with col3:
# Pie chart Test (non utilisé)
fig_test, ax_test = plt.subplots(figsize=uniform_figsize, tight_layout=True)
test_total = splits_info['test']['images']
sizes = [test_total]
labels = [f'Non utilisé dans notre projet\n{test_total} images\n(annotations privées)']
colors = [accessible_colors['gray']]
try:
wedges, texts, autotexts = ax_test.pie(
sizes, labels=labels, colors=colors, autopct='',
startangle=90, shadow=True, textprops=uniform_textprops,
wedgeprops={'edgecolor': 'white', 'linewidth': 2}
)
if use_patterns and patterns[3]:
wedges[0].set_hatch(patterns[3])
ax_test.set_title(f'Split Test Original\n{test_total} images total',
**uniform_title_props, color='#333333')
ax_test.axis('equal')
chart_displayed = True
except Exception as e:
ax_test.text(0.5, 0.5, f"Erreur d'affichage\n{str(e)}",
ha='center', va='center', transform=ax_test.transAxes,
fontsize=10, color='red', weight='bold')
chart_displayed = False
if not chart_displayed:
alt_text = generate_alt_text_for_pie(sizes, labels, "Split Test")
st.warning(f"**Description du graphique:** {alt_text}")
st.pyplot(fig_test)
plt.close(fig_test)
pie_data_for_table.append(["Test", 0, "0%", "Non utilisé (annotations privées)"])
with col4:
# Pie chart Coarse (non utilisé)
fig_coarse, ax_coarse = plt.subplots(figsize=uniform_figsize, tight_layout=True)
coarse_total = splits_info['coarse']['images']
sizes = [coarse_total]
labels = [f'Non utilisé dans notre projet\n{coarse_total} images\n(annotations grossières)']
colors = [accessible_colors['light_gray']]
try:
wedges, texts, autotexts = ax_coarse.pie(
sizes, labels=labels, colors=colors, autopct='',
startangle=90, shadow=True, textprops=uniform_textprops,
wedgeprops={'edgecolor': 'white', 'linewidth': 2}
)
if use_patterns and patterns[4]:
wedges[0].set_hatch(patterns[4])
ax_coarse.set_title(f'Split Coarse Original\n{coarse_total} images total',
**uniform_title_props, color='#333333')
ax_coarse.axis('equal')
chart_displayed = True
except Exception as e:
ax_coarse.text(0.5, 0.5, f"Erreur d'affichage\n{str(e)}",
ha='center', va='center', transform=ax_coarse.transAxes,
fontsize=10, color='red', weight='bold')
chart_displayed = False
if not chart_displayed:
alt_text = generate_alt_text_for_pie(sizes, labels, "Split Coarse")
st.warning(f"**Description du graphique:** {alt_text}")
st.pyplot(fig_coarse)
plt.close(fig_coarse)
pie_data_for_table.append(["Coarse", 0, "0%", "Non utilisé (annotations grossières)"])
st.markdown("### 📊 Exploration Interactive du Dataset")
fig_dataset = create_dataset_interactive_overview()
try:
st.plotly_chart(fig_dataset, use_container_width=True, key="dataset_overview")
except Exception as e:
st.warning("**Description :** Graphique circulaire interactif montrant la répartition de l'utilisation du dataset Cityscapes dans notre projet.")
st.info("💡 **Interactivité :** Survolez les sections pour voir les détails, cliquez sur la légende pour masquer/afficher des catégories.")
# Tableau de données accessible (toujours affiché)
st.markdown("#### 📋 Tableau récapitulatif des données")
create_data_table(
pie_data_for_table,
["Split", "Images utilisées", "Pourcentage", "Utilisation"],
"Répartition du dataset Cityscapes",
"Détail de l'utilisation de chaque split du dataset original dans notre projet"
)
# Architecture des classes
st.markdown("### 🏗️ Architecture de classes hiérarchique")
st.markdown("""
Le dataset organise 34 classes détaillées en **8 méta-classes** pour la segmentation :
| Méta-classe | Description | Importance |
|-------------|-------------|------------|
| **Flat** | Route, trottoir, parkings | Navigation de base |
| **Human** | Piéton, cycliste | **Sécurité critique** |
| **Vehicle** | Voiture, camion, bus, train | Obstacles dynamiques |
| **Construction** | Bâtiment, mur, clôture, pont | Structure urbaine |
| **Object** | Poteau, feu, panneau | Signalétique |
| **Nature** | Végétation, terrain | Environnement |
| **Sky** | Ciel | Contexte |
| **Void** | Zones non-définies | Exclusion |
""")
# Graphique de répartition des classes
st.markdown("### 📊 Répartition des classes dans le dataset d'entraînement")
class_repartition_data = get_class_repartition()
if class_repartition_data:
try:
class_repartition_image = Image.open(io.BytesIO(class_repartition_data))
st.image(
class_repartition_image,
caption="Distribution des classes dans les images d'entraînement Cityscapes",
use_container_width=True
)
except Exception as e:
st.warning("Impossible d'afficher le graphique de répartition des classes")
else:
st.info("Graphique de répartition des classes non disponible")
# Section preprocessing des images
st.markdown("---")
st.markdown("### 🔧 Exemple de Preprocessing d'Images")
# Récupérer les images d'entraînement disponibles
train_data = get_train_images()
if train_data and train_data['images']:
# Sélecteur d'image
image_options = {img['display_name']: img['filename'] for img in train_data['images']}
selected_display_name = st.selectbox(
"Choisissez une image d'entraînement pour voir les transformations:",
options=list(image_options.keys()),
help=f"{train_data['total_count']} images disponibles",
key="train_image_selector"
)
selected_filename = image_options[selected_display_name]
# Preview de l'image sélectionnée (avant preprocessing)
if selected_filename and "preprocessing_result" not in st.session_state:
try:
image_url = f"{API_BASE_URL}/train-image/{selected_filename}"
response = requests.get(image_url, timeout=5)
if response.status_code == 200:
preview_image = Image.open(io.BytesIO(response.content))
st.image(
preview_image,
caption=f"Aperçu de l'image sélectionnée : {selected_display_name}",
use_container_width=True
)
except:
st.info("Aperçu non disponible")
# Options de preprocessing
col1, col2 = st.columns(2)
with col1:
show_augmentation = st.checkbox(
"Inclure l'augmentation aléatoire",
help="Affiche un exemple d'augmentation de données utilisée pendant l'entraînement. Cliquez plusieurs fois sur 'Appliquer le Preprocessing' pour tester différentes augmentations aléatoires !",
key="augmentation_checkbox"
)
with col2:
if st.button("🔧 Appliquer le Preprocessing", type="primary", key="apply_preprocessing"):
with st.spinner("Application des transformations..."):
result = preprocess_train_image(selected_filename, show_augmentation)
if result and result['success']:
st.session_state.preprocessing_result = result
st.success("Preprocessing terminé!")
st.rerun()
else:
st.error("Erreur lors du preprocessing")
# Bouton pour revenir à l'aperçu
if "preprocessing_result" in st.session_state:
if st.button("🔄 Choisir une nouvelle image", type="secondary", key="reset_preprocessing"):
del st.session_state.preprocessing_result
st.rerun()
# Affichage des résultats de preprocessing
if "preprocessing_result" in st.session_state:
result = st.session_state.preprocessing_result
# Affichage des images transformées sur toute la largeur
st.markdown("#### Résultats des transformations :")
if show_augmentation and result.get('augmented_image'):
cols = st.columns(3)
col_titles = ["Image Originale (redimensionnée)", "Après Normalisation", "Avec Augmentation Aléatoire"]
col_data = [result.get('original_image'), result.get('normalized_image'), result.get('augmented_image')]
else:
cols = st.columns(2)
col_titles = ["Image Originale (redimensionnée)", "Après Normalisation"]
col_data = [result.get('original_image'), result.get('normalized_image')]
for i, (col, title, data) in enumerate(zip(cols, col_titles, col_data)):
with col:
st.markdown(f"**{title}**")
if data:
img_data = base64.b64decode(data)
img = Image.open(io.BytesIO(img_data))
st.image(img, use_container_width=True)
# Explications des transformations appliquées
if result.get('transformations_applied'):
st.markdown("#### 📋 Transformations appliquées :")
# Préparer les transformations pour l'alignement
transforms = result['transformations_applied']
# Extraire les transformations de base (toujours présentes)
base_transforms = [t for t in transforms if "Resize" in t or "Normalization" in t]
augmentation_transforms = [t for t in transforms if "Resize" not in t and "Normalization" not in t]
if show_augmentation and result.get('augmented_image'):
# 3 colonnes quand augmentation active
col1, col2, col3 = st.columns(3)
with col1:
st.markdown("**Image Originale (redimensionnée)**")
resize_transform = next((t for t in base_transforms if "Resize" in t), "1. Resize (512x512)")
st.write(f"• {resize_transform}")
with col2:
st.markdown("**Après Normalisation**")
norm_transform = next((t for t in base_transforms if "Normalization" in t), "2. Normalization (0-1)")
st.write(f"• {norm_transform}")
with col3:
st.markdown("**Avec Augmentation Aléatoire**")
if augmentation_transforms:
for i, transform in enumerate(augmentation_transforms, 3):
st.write(f"• {i}. {transform}")
else:
st.write("• 3. Aucune augmentation appliquée (aléatoire)")
else:
# 2 colonnes quand pas d'augmentation
col1, col2 = st.columns(2)
with col1:
st.markdown("**Image Originale (redimensionnée)**")
resize_transform = next((t for t in base_transforms if "Resize" in t), "1. Resize (512x512)")
st.write(f"• {resize_transform}")
with col2:
st.markdown("**Après Normalisation**")
norm_transform = next((t for t in base_transforms if "Normalization" in t), "2. Normalization (0-1)")
st.write(f"• {norm_transform}")
# Explications détaillées avec description complète des augmentations
with st.expander("ℹ️ Détails des transformations possibles"):
st.markdown("""
**Normalisation standard (toujours appliquée) :**
- **Redimensionnement** : Toutes les images sont redimensionnées à 512×512 pixels
- **Normalisation** : Les valeurs de pixels sont normalisées de [0-255] vers [0-1]
- Cette normalisation uniforme est appliquée à tous les modèles (paramètre encoder non utilisé)
**Augmentations de données possibles (si activées) :**
Le système sélectionne aléatoirement entre 1 et 4 transformations parmi les suivantes :
1. **Rotation aléatoire** : Rotation de l'image entre -15° et +15°
2. **Flip horizontal** : Miroir horizontal avec 50% de probabilité
3. **Changement de luminosité/contraste** : Ajustement de ±20% de la luminosité et du contraste
4. **Flou gaussien léger** : Application d'un flou gaussien subtil (blur 1-3 pixels)
5. **Décalage couleur** : Modification de la teinte, saturation et valeur (±10-15%)
6. **Zoom aléatoire** : Zoom de -10% à +10% (avec probabilité réduite)
**Important :** Les augmentations sont **aléatoires** à chaque application. Cliquez plusieurs fois sur
"Appliquer le Preprocessing" pour voir différentes combinaisons ! Le résultat peut inclure une ou
plusieurs transformations, ou même aucune selon le hasard.
Ces transformations permettent d'améliorer la robustesse et la généralisation du modèle pendant
l'entraînement en exposant le modèle à des variations réalistes des données.
""")
else:
st.warning("Aucune image d'entraînement disponible pour la démonstration")
# ===== ONGLET 2: COMPARAISON DE PERFORMANCES =====
with main_tab2:
# Variables de session pour les résultats
if "sample_comparison_result" not in st.session_state:
st.session_state.sample_comparison_result = None
if "upload_comparison_result" not in st.session_state:
st.session_state.upload_comparison_result = None
def reset_sample_comparison():
st.session_state.sample_comparison_result = None
def reset_upload_comparison():
st.session_state.upload_comparison_result = None
# Sous-onglets pour la comparaison
sub_tab1, sub_tab2 = st.tabs(["🏙️ Images Test Cityscapes", "📤 Upload Personnel"])
# SUB-TAB 1: Images Test Cityscapes
with sub_tab1:
st.markdown("## 🏙️ Comparaison sur Images Test Cityscapes")
st.markdown("Comparez les performances des deux modèles sur des images Cityscapes avec masques de vérité.")
# Récupérer les images d'exemple
sample_data = get_sample_images()
if sample_data and sample_data['images']:
# Sélecteur d'image
image_options = {img['display_name']: img['filename'] for img in sample_data['images']}
selected_display_name = st.selectbox(
"Choisissez une image:",
options=list(image_options.keys()),
help=f"{sample_data['total_count']} images disponibles"
)
selected_filename = image_options[selected_display_name]
# Afficher les infos de l'image sélectionnée
selected_img_info = next(img for img in sample_data['images'] if img['filename'] == selected_filename)
col1, col2 = st.columns([2, 1])
with col1:
st.info(f"📁 **Fichier:** {selected_filename}")
with col2:
ground_truth_status = "✅ Disponible" if selected_img_info['has_ground_truth'] else "❌ Non disponible"
st.info(f"🎯 **Vérité terrain:** {ground_truth_status}")
# Prévisualisation de l'image sélectionnée
if selected_filename:
try:
image_url = f"{API_BASE_URL}/sample-image/{selected_filename}"
response = requests.get(image_url, timeout=5)
if response.status_code == 200:
sample_image = Image.open(io.BytesIO(response.content))
st.image(
sample_image,
caption=f"Aperçu: {selected_display_name}",
use_container_width=True
)
except:
st.info("📷 Aperçu non disponible - L'image sera chargée lors de la comparaison")
# Bouton de comparaison
if st.button("🔄 Lancer la Comparaison", key="compare_sample", type="primary"):
if selected_filename:
with st.spinner("Comparaison en cours..."):
result = compare_sample_image(selected_filename)
if result and result['success']:
st.session_state.sample_comparison_result = result
st.success("✅ Comparaison terminée!")
st.rerun()
else:
st.error("❌ Erreur lors de la comparaison")
else:
st.warning("Aucune image d'exemple disponible")
# Affichage des résultats de comparaison
if st.session_state.sample_comparison_result:
st.markdown("---")
models_info = get_models_info()
display_comparison_results(st.session_state.sample_comparison_result, models_info, "sample")
# Bouton de reset
if st.button("🔄 Nouvelle Comparaison", key="reset_sample", type="secondary"):
reset_sample_comparison()
st.rerun()
# SUB-TAB 2: Upload Personnel
with sub_tab2:
st.markdown("## 📤 Comparaison sur Image Personnelle")
st.markdown("Uploadez une image pour comparer les performances des deux modèles.")
uploaded_file = st.file_uploader(
"Sélectionnez une image:",
type=['jpg', 'jpeg', 'png'],
help="Taille maximale recommandée: 5MB"
)
if uploaded_file is not None:
# Afficher l'image uploadée
image = Image.open(uploaded_file)
st.image(image, caption=f"Image uploadée: {uploaded_file.name}", use_container_width=True)
# Informations sur le fichier
file_size_mb = len(uploaded_file.getvalue()) / 1024 / 1024
st.info(f"📁 **Fichier:** {uploaded_file.name} | 📊 **Taille:** {file_size_mb:.2f} MB")
# Bouton de comparaison
if st.button("🔄 Lancer la Comparaison", key="compare_upload", type="primary"):
with st.spinner("Comparaison en cours..."):
result = compare_uploaded_image(uploaded_file)
if result and result['success']:
st.session_state.upload_comparison_result = result
st.success("✅ Comparaison terminée!")
st.rerun()
else:
st.error("❌ Erreur lors de la comparaison")
# Affichage des résultats de comparaison
if st.session_state.upload_comparison_result:
st.markdown("---")
models_info = get_models_info()
display_comparison_results(st.session_state.upload_comparison_result, models_info, "upload")
# Bouton de reset
if st.button("🔄 Nouvelle Comparaison", key="reset_upload", type="secondary"):
reset_upload_comparison()
st.rerun()
# Sidebar avec informations des modèles
with st.sidebar:
st.markdown("## 🔧 Informations des Modèles")
models_info = get_models_info()
if models_info:
st.success("✅ Modèles chargés")
# 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"**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"**Résolution:** {second_model.get('input_size', 'N/A')}")
else:
st.error("❌ Modèles non disponibles")
st.markdown("---")
st.markdown("## 🎯 Classes Segmentées (IoU)")
# 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}{iou_str}
""", unsafe_allow_html=True)
# Footer
st.markdown("---")
st.markdown("""
🚗 Street Vision - Comparaison de Modèles -
Évaluation comparative de modèles de segmentation sémantique
Développé avec FastAPI, Streamlit et PyTorch
""", unsafe_allow_html=True)
with st.sidebar:
st.markdown("---")
st.markdown("## ♿ Conformité WCAG 2.1")
criteria_status = {
"1.1.1 Contenu non textuel": "✅",
"1.4.1 Utilisation de la couleur": "✅",
"1.4.3 Contraste (minimum)": "✅",
"1.4.4 Redimensionnement du texte": "✅",
"2.4.2 Titre de page": "✅"
}
for criterion, status in criteria_status.items():
st.write(f"{status} {criterion}")
st.markdown("**Niveau :** AA | **Référence :** [WCAG 2.1](https://www.w3.org/WAI/WCAG21/quickref/)")