diff --git a/app/requirements.txt b/app/requirements.txt
index ef6d4de..6fe45e3 100644
--- a/app/requirements.txt
+++ b/app/requirements.txt
@@ -68,3 +68,4 @@ typing_extensions==4.14.0
# ===== MATPLOTLIB ADDONS =====
cycler==0.12.1
fonttools==4.58.5
+plotly==5.24.1
diff --git a/app/streamlit_app.py b/app/streamlit_app.py
index d0142a5..db9527a 100644
--- a/app/streamlit_app.py
+++ b/app/streamlit_app.py
@@ -5,6 +5,8 @@ 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
@@ -18,9 +20,9 @@ sys.path.append('/app')
sys.path.append('.')
from utils.utils import colorize_mask
-# Configuration de la page
+# Configuration de la page avec titre approprié (WCAG 2.4.2)
st.set_page_config(
- page_title="Street Vision - Segmentation Comparison",
+ page_title="Street Vision - Segmentation Comparison | Dashboard Accessible",
page_icon="🚗",
layout="wide"
)
@@ -72,68 +74,276 @@ ACCESSIBILITY_PATTERNS = {
}
def create_accessible_pie_chart_conditional(sizes, labels, title, use_patterns=False):
- """
- Crée un pie chart accessible - descriptions uniquement si erreur d'affichage
- """
- import matplotlib.pyplot as plt
- import matplotlib.patches as patches
-
- # Configuration uniforme (comme dans la solution qui marchait)
+ """Version corrigée qui règle le problème de lisibilité avec les motifs"""
fig, ax = plt.subplots(figsize=(6, 6), tight_layout=True)
- # Couleurs accessibles
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']
+ 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)]
- # Configuration uniforme des propriétés
- uniform_textprops = {'fontsize': 8, 'fontweight': 'bold', 'color': '#333333'}
-
- # Motifs pour accessibilité
- patterns = None
- if use_patterns:
- pattern_list = list(ACCESSIBILITY_PATTERNS.values())
- patterns = pattern_list[: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, # Propriétés uniformes
- wedgeprops={'edgecolor': 'white', 'linewidth': 2}
+ 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
)
- # Ajouter des motifs si demandé
- if patterns:
- for wedge, pattern in zip(wedges, patterns):
+ # 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') # Assure un cercle parfait
-
+ ax.axis('equal')
chart_displayed = True
except Exception as e:
- # En cas d'erreur, afficher la description alternative
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')
+ 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):
"""
@@ -204,7 +414,7 @@ def create_accessible_bar_chart_conditional(data, labels, title, colors=None, us
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
@@ -222,6 +432,26 @@ def generate_alt_text_for_pie(sizes, labels, title):
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
@@ -236,6 +466,33 @@ def generate_alt_text_for_bar(data, labels, title, unit=""):
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
@@ -261,10 +518,49 @@ def create_data_table(data, columns, title, description=""):
return df
-# CSS personnalisé pour l'accessibilité WCAG
-WCAG_CSS = """
+# CSS amélioré pour l'accessibilité complète
+ENHANCED_WCAG_CSS = """
"""
-st.markdown(WCAG_CSS, unsafe_allow_html=True)
+st.markdown(ENHANCED_WCAG_CSS, unsafe_allow_html=True)
# En-tête principal
st.markdown("""
@@ -493,8 +811,8 @@ def compare_uploaded_image(uploaded_file):
st.error(f"Erreur lors de la comparaison: {e}")
return None
-def display_comparison_results(result, models_info):
- """Affiche les résultats de comparaison entre les deux modèles"""
+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
@@ -573,94 +891,134 @@ def display_comparison_results(result, models_info):
""", unsafe_allow_html=True)
- # Affichage des segmentations
+ # 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}")
- # Comparaison des statistiques
- st.markdown("### 📊 Répartition des classes par modèle")
+ # Section interactive (CE2 - Graphiques interactifs) - SEULEMENT 2 ONGLETS
+ st.markdown("### 📊 Analyse Interactive des Résultats")
- col1, col2 = st.columns(2)
-
- with col1:
- if result.get('model1_stats'):
- display_model_stats_chart(result['model1_stats'], models_info, f"{model1.get('architecture', 'N/A')} - {model1['encoder_name']}")
-
- with col2:
- if result.get('model2_stats'):
- display_model_stats_chart(result['model2_stats'], models_info, f"{model2.get('architecture', 'N/A')} - {model2['encoder_name']}")
-
- # Analyse comparative des classes
if result.get('model1_stats') and result.get('model2_stats'):
- display_class_comparison(result['model1_stats'], result['model2_stats'], model1, model2, models_info)
+ # 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
-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
+ # 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)
- # 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())
+# 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
- percentages = [stats[name]['percentage'] for name in ordered_class_names]
+# # 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())
- # 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é
- ]
+# percentages = [stats[name]['percentage'] for name in ordered_class_names]
- colors = accessible_colors[:len(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é
+# ]
- # Créer le graphique accessible
- fig, ax, displayed = create_accessible_bar_chart_conditional(
- percentages, ordered_class_names, title,
- colors=colors, use_patterns=True
- )
+# colors = accessible_colors[:len(ordered_class_names)]
- # 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}")
+# # Créer le graphique accessible
+# fig, ax, displayed = create_accessible_bar_chart_conditional(
+# percentages, ordered_class_names, title,
+# colors=colors, use_patterns=True
+# )
- st.pyplot(fig)
- plt.close(fig)
+# # 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}")
- # 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"])
+# st.pyplot(fig)
+# plt.close(fig)
- create_data_table(
- table_data,
- ["Classe", "Pourcentage", "Nombre de pixels"],
- f"Données - {title}",
- "Répartition détaillée des classes de segmentation"
- )
+# # 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 une comparaison accessible des classes entre les deux modèles"""
- st.markdown("### 🔍 Analyse Comparative par Classe")
+ """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:
@@ -674,73 +1032,8 @@ def display_class_comparison(stats1, stats2, model1, model2, models_info):
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:**")
+ # SUPPRIMER tout le code de génération du graphique matplotlib
+ # GARDER seulement le tableau
table_data = []
for i, name in enumerate(class_names):
@@ -776,6 +1069,124 @@ def display_class_comparison(stats1, stats2, model1, model2, models_info):
"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"])
@@ -795,211 +1206,24 @@ with main_tab1:
#### Dataset retenu
**Cityscapes Dataset : Référence en Segmentation Urbaine**
-
- Le **Cityscapes Dataset** constitue la référence internationale pour l'évaluation des algorithmes de segmentation sémantique en environnement urbain. Développé par l'Université de Tübingen et Mercedes-Benz, ce dataset comprend **25,000 images** haute résolution (2048×1024 pixels) capturées dans 50 villes allemandes et suisses.
""")
+ # 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 avec tailles forcées absolument uniformes
- # 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 STRICTEMENT uniforme pour tous les graphiques
- # CHART_WIDTH, CHART_HEIGHT = 400, 400 # Taille en pixels
- # uniform_figsize = (5, 5) # Taille figure identique
- # uniform_dpi = 100 # DPI identique
-
- # # Style uniforme pour TOUS les éléments
- # uniform_style = {
- # 'textprops': {'fontsize': 8, 'fontweight': 'bold', 'color': '#333333'},
- # 'wedgeprops': {'edgecolor': 'white', 'linewidth': 2},
- # 'title_props': {'fontsize': 10, 'fontweight': 'bold', 'pad': 15, 'color': '#333333'},
- # 'startangle': 90,
- # 'shadow': True
- # }
-
- # # 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
-
- # # Fonction helper pour créer des pie charts uniformes
- # def create_uniform_pie(ax, sizes, labels, colors, explode=None, patterns_slice=None, autopct='%1.0f%%'):
- # try:
- # wedges, texts, autotexts = ax.pie(
- # sizes, labels=labels, colors=colors,
- # autopct=autopct, explode=explode,
- # **uniform_style
- # )
-
- # # Ajouter des motifs si demandé
- # if patterns_slice and use_patterns:
- # for wedge, pattern in zip(wedges, patterns_slice):
- # if pattern:
- # wedge.set_hatch(pattern)
-
- # # FORCER les limites d'axes identiques pour TOUS
- # ax.set_xlim(-1.5, 1.5)
- # ax.set_ylim(-1.5, 1.5)
- # ax.axis('equal')
- # ax.set_aspect('equal', 'box')
-
- # return True, wedges
- # except Exception as e:
- # ax.text(0.5, 0.5, f"Erreur d'affichage\n{str(e)}",
- # ha='center', va='center', transform=ax.transAxes,
- # fontsize=10, color='red', weight='bold')
- # return False, None
-
- # # 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, dpi=uniform_dpi, 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)
-
- # chart_displayed, wedges = create_uniform_pie(
- # ax_train, sizes, labels, colors, explode, patterns[:2]
- # )
-
- # ax_train.set_title(f'Split Train Original\n{train_total} images total',
- # **uniform_style['title_props'])
-
- # 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, dpi=uniform_dpi, 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']]
-
- # chart_displayed, wedges = create_uniform_pie(
- # ax_val, sizes, labels, colors, None, [patterns[2]]
- # )
-
- # ax_val.set_title(f'Split Validation Original\n{val_total} images total',
- # **title_style)
-
- # 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, dpi=uniform_dpi, 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']]
-
- # chart_displayed, wedges = create_uniform_pie(
- # ax_test, sizes, labels, colors, None, [patterns[3]], autopct=''
- # )
-
- # ax_test.set_title(f'Split Test Original\n{test_total} images total',
- # **title_style)
-
- # 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, dpi=uniform_dpi, 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']]
-
- # chart_displayed, wedges = create_uniform_pie(
- # ax_coarse, sizes, labels, colors, None, [patterns[4]], autopct=''
- # )
-
- # ax_coarse.set_title(f'Split Coarse Original\n{coarse_total} images total',
- # **title_style)
-
- # 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)"])
-
- # # 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"
- # )
-
-
# Version finale qui combine : tailles uniformes + couleurs WCAG + descriptions conditionnelles
if dataset_info.get('splits'):
splits_info = dataset_info['splits']
@@ -1214,6 +1438,19 @@ with main_tab1:
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")
@@ -1223,6 +1460,7 @@ with main_tab1:
"Répartition du dataset Cityscapes",
"Détail de l'utilisation de chaque split du dataset original dans notre projet"
)
+
# Architecture des classes
@@ -1258,18 +1496,6 @@ with main_tab1:
st.warning("Impossible d'afficher le graphique de répartition des classes")
else:
st.info("Graphique de répartition des classes non disponible")
-
- # Spécificités techniques
- st.markdown("""
- ### ⚙️ Spécificités techniques
-
- - **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
-
- 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.
- """)
# Section preprocessing des images
st.markdown("---")
@@ -1512,7 +1738,7 @@ with main_tab2:
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)
+ 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"):
@@ -1554,7 +1780,7 @@ with main_tab2:
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)
+ 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"):
@@ -1641,4 +1867,23 @@ st.markdown("""
Évaluation comparative de modèles de segmentation sémantique
Développé avec FastAPI, Streamlit et PyTorch
-""", unsafe_allow_html=True)
\ No newline at end of file
+""", 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/)")
+