import streamlit as st import pandas as pd import seaborn as sns import numpy as np import os import random import matplotlib.pyplot as plt import matplotlib.image as mpimg import plotly.express as px from plotly.subplots import make_subplots import plotly.graph_objects as go from PIL import Image from custom_functions import calc_mean_intensity, source_extract df_images = pd.read_csv('data/df_images.csv') df_masks = pd.read_csv('data/df_masks.csv') df_combined = pd.read_csv('data/df_combined.csv') df_metadata = pd.read_csv('data/df_metadata.csv') def show_exploration(): # Style des onglets st.markdown(""" """, unsafe_allow_html = True) tab1, tab2 = st.tabs(["🗂️ Métadonnées", "🖼️ Images & masques"]) ### Premier onglet with tab1: st.header("Exploration des métadonnées") st.markdown(''' Nous avons à notre disposition un important jeu de données provenant de [Kaggle](https://www.kaggle.com/datasets/tawsifurrahman/covid19-radiography-database). Il s'agit de 21 165 images de radiographies pulmonaires labellisées dans leur nom, ainsi qu'autant de masques basiques associés aux images. De plus, quatre fichiers de métadonnées au format *.xlxs sont disponibles en accompagnement des quatre catégories. Au regard du peu d'informations présents dans les métadonnées, nous avons cherché à créer un nouveau tableau plus complet, nous permettant d'appréhender plus efficacement le jeu de données. Cette démarche nous a permis de détecter de nombreuses erreurs. ''') col1, col2 = st.columns([1, 1]) with col1: st.subheader("Métadonnées fournies") st.dataframe(df_metadata.head()) ## Valeurs uniques unique_format = df_metadata["FORMAT"].unique() unique_format = ", ".join(map(str, unique_format)) unique_resolution = df_metadata["SIZE"].unique() unique_resolution = ", ".join(map(str, unique_resolution)) data = [ ("Liste des résolutions", unique_resolution), ("Liste des formats", unique_format), ("Nombre d'images", len(df_metadata)) ] # Créer le DataFrame à partir de la liste de tuples df2 = pd.DataFrame(data, columns = ["Titre", "Variable"]) # Convertir le dataframe en HTML avec les styles CSS html_table2 = df2.to_html(index = False, header = False, justify = 'center', classes = 'styled-table', border = 0) # Afficher le HTML dans Streamlit avec la largeur calculée st.markdown(f"
{html_table2}
", unsafe_allow_html=True) with col2: st.subheader("Métadonnées réelles") st.dataframe(df_images.head()) ## Valeurs uniques unique_classes = df_images['LABEL'].unique() unique_classes = ", ".join(map(str, unique_classes)) unique_sources = df_images['SOURCE'].dropna().unique() unique_sources = ", ".join(map(str, unique_sources)) unique_format = df_images["FORMAT"].unique() unique_format = ", ".join(map(str, unique_format)) unique_resolution = df_images["SIZE"].unique() unique_resolution = ", ".join(map(str, unique_resolution)) unique_channel = df_images["CHANNELS"].unique() unique_channel = ", ".join(map(str, unique_channel)) data = [ ("Liste des résolutions", unique_resolution), ("Liste des formats", unique_format), ("Liste des classes", unique_classes), ("Liste des sources", unique_sources), ("Nombre de canaux", unique_channel), ("Nombre d'images", len(df_images)) ] # Créer le DataFrame à partir de la liste de tuples df = pd.DataFrame(data, columns = ["Titre", "Variable"]) # Convertir le dataframe en HTML avec les styles CSS html_table = df.to_html(index = False, header = False, justify = 'center', classes = 'styled-table', border = 0) # Afficher le HTML dans Streamlit avec la largeur calculée st.markdown(f"
{html_table}
", unsafe_allow_html=True) # ================================================================================================== # ================================================================================================== st.header("Analyse du nombre d'images") col1, col2 = st.columns([0.4, 0.6]) with col1: ## Countplot # Preprocess des données pour affichage total_images = df_images.groupby('LABEL').size().reset_index(name='Total Images') max_values = df_images.groupby('LABEL')['LABEL'].count().reset_index(name='Max Images') df_merged = pd.merge(total_images, max_values, on='LABEL') df_sorted = df_merged.sort_values('Total Images', ascending = False) palette = {'Normal': '#A1C9F4', 'Lung_Opacity': '#8DE5A1', 'COVID': '#FFB482', 'Viral Pneumonia': '#D0BBFF'} # Création du graphique Plotly avec affichage des valeurs maximales uniquement au survol fig = px.bar(df_sorted, x = 'LABEL', y = 'Total Images', color = 'LABEL', color_discrete_map = palette, title = "Distribution du nombre d'images par LABEL", labels = {'LABEL': 'Label', 'Total Images': 'Nombre d\'images', 'Max Images': 'Nombre maximal d\'images'}, hover_data = {'LABEL': False, 'Max Images': True, 'Total Images': False} ) fig.update_layout( xaxis_title = 'Label', yaxis_title = "Nombre d'images", showlegend = True, bargap = 0.2, # Espace entre les barres width = 600, height = 500 ) st.plotly_chart(fig) with col2: st.markdown("Les classes sont particulièrement déséquilibrées, il est cependant intéressant de constater que la classe minoritaire contient tout de même 1345 images.") st.header("", divider = 'gray') # ================================================================================================== # ================================================================================================== col1, col2 = st.columns([0.5, 0.5]) with col1: ## Pieplot # Supposons que df_images est votre DataFrame et 'SOURCE' est la colonne d'intérêt source_counts = df_images['SOURCE'].value_counts() # Création d'un graphique pie avec Plotly fig = px.pie( values = source_counts.values, names = source_counts.index, title = 'Répartition des sources des images', hole = 0.5, color_discrete_sequence = px.colors.qualitative.Pastel ) fig.update_traces( textinfo = 'label+percent', marker = dict(line = dict(color = 'black', width = 1)), ) fig.update_layout( showlegend = True, width = 700, height = 700 ) # Affichage du graphique avec Streamlit st.plotly_chart(fig) with col2: ## Barplot df_count = df_images.groupby(['SOURCE', 'LABEL']).size().reset_index(name='NOMBRE_IMAGES') # Créer un graphique empilé avec Plotly fig2 = px.bar(df_count, x='SOURCE', y='NOMBRE_IMAGES', color='LABEL', barmode='stack', title='Nombre d\'images par label et par source', labels={'NOMBRE_IMAGES': 'Nombre d\'images', 'LABEL': 'Label', 'SOURCE': 'Source'}, color_discrete_sequence = px.colors.qualitative.Pastel) fig2.update_traces( marker = dict(line = dict(color = 'black', width = 1)), ) fig2.update_layout( showlegend = True, width = 700, height = 700 ) # Affichage du graphique avec Streamlit st.plotly_chart(fig2) ### Deuxième onglet with tab2: st.header("Exploration des images") ## Affichage d'une image aléatoire pour chaque catégorie dossier_radio = "radios/" sous_dossiers = [d for d in os.listdir(dossier_radio) if os.path.isdir(os.path.join(dossier_radio, d))] st.info("Cliquez sur le boutton ci-dessous pour afficher un échantillon d'images", icon="ℹ️") if st.button("Afficher une image aléatoire de chaque LABEL"): cols = st.columns(len(sous_dossiers)) for i, sous_dossier in enumerate(sous_dossiers): sous_dossier_path = os.path.join(dossier_radio, sous_dossier) fichiers_images = [f for f in os.listdir(sous_dossier_path) if f.endswith(('.png', '.jpg', '.jpeg'))] if fichiers_images: image_selectionnee = random.choice(fichiers_images) image_path = os.path.join(sous_dossier_path, image_selectionnee) with open(image_path, "rb") as f: image = Image.open(f) cols[i].image(image, caption=f"{sous_dossier}", use_column_width = "auto", width = 299) st.success('Images affichées avec succès !', icon = "✅") st.markdown(''' Nous pouvons remarquer que les images sont toutes en nuances de gris, malgré leur nombre de canaux quelques fois différent. De plus, toutes les radiographies semblent avoir été prises selon une méthode standard, mettant bien les poumons au centre de l'image. Quelques variations peuvent cependant apparaître (bras vers le haut, artefacts visuels, annotations, etc.) ''') st.header("", divider = 'gray') # ================================================================================================== # ================================================================================================== col1, col2 = st.columns([0.6, 0.4]) with col1: ## Violintplot palette = { 'Normal (Sans masque)': '#A1C9F4', 'Normal (Avec masque)': '#517EA4', 'Lung_Opacity (Sans masque)': '#8DE5A1', 'Lung_Opacity (Avec masque)': '#4E7D51', 'COVID (Sans masque)': '#FFB482', 'COVID (Avec masque)': '#BF6D41', 'Viral Pneumonia (Sans masque)': '#D0BBFF', 'Viral Pneumonia (Avec masque)': '#7E6CBF' } new_labels = ['COVID\nAvec masque', 'COVID\nSans masque', 'Lung_Opacity\nAvec masque', 'Lung_Opacity\nSans masque', 'Normal\nAvec masque', 'Normal\nSans masque', 'Viral Pneumonia\nAvec masque', 'Viral Pneumonia\nSans masque'] # Création du graphique Plotly fig = px.violin(df_combined, x = 'Label_Masque', y = 'COMBINED_INTENSITY', violinmode = 'overlay', color = 'Label_Masque', color_discrete_map = palette, category_orders = {'Label_Masque': new_labels}, title = "Distribution de l'intensité lumineuse moyenne normalisée", labels = {'Label_Masque': ''}, ) # Mise en forme du graphique fig.update_layout( yaxis_title = 'Intensité lumineuse moyenne', xaxis_title = '', width = 1000, height = 600 ) # Affichage du graphique st.plotly_chart(fig) with col2: st.markdown(''' L'application des masques réduit considérablement l'intensité lumineuse moyenne des images. Ce comportement est tout à fait normal car les masques noircient les parties non pertinentes et font ainsi tendre la moyenne des pixels vers 0. Ainsi, les valeurs restantes sont les valeurs ayant de la pertinence dans ce que nous cherchons à faire observer au modèle. Mais ceci peut aussi entrainer une diminution de la variabilité des images, pouvant entrainer plus de difficultés à la généralisation. De plus, ceci génère une contrainte supplémentaire lors du déploiement du modèle, nécessitant d'appliquer un masque spécifique aux nouvelles données avant introduction dans le modèle. ''') st.header("", divider = 'gray') # ================================================================================================== # ================================================================================================== ## Histogramme de la fréquence de l'intensité des pixels palette_list = ['#A1C9F4','#8DE5A1','#FFB482', '#D0BBFF'] st.info("Cliquez sur le bouton ci-dessous pour afficher un échantillon d'images et leur histogramme", icon="ℹ️") if st.button("Afficher la fréquence de l'intensité des pixels"): # Créer une ligne avec quatre colonnes cols = st.columns(4) for idx, sous_dossier in enumerate(sous_dossiers): sous_dossier_path = os.path.join(dossier_radio, sous_dossier) fichiers_images = [f for f in os.listdir(sous_dossier_path) if f.endswith(('.png', '.jpg', '.jpeg'))] if fichiers_images: image_selectionnee = random.choice(fichiers_images) image_path = os.path.join(sous_dossier_path, image_selectionnee) with open(image_path, "rb") as f: image = Image.open(f) # Afficher l'image avec sa véritable résolution sans agrandissement cols[idx].image(image, caption=f"{sous_dossier}", width = image.width, use_column_width = "auto") image_array = np.array(image) if len(image_array.shape) == 3: image_array = image_array.mean(axis=2) filtered_array = image_array[image_array > 0] hist_values, bin_edges = np.histogram(filtered_array, bins=255, range=(0, 256)) # Création de l'histogramme avec Plotly fig = px.bar( x=bin_edges[:-1], y=hist_values, labels={'x': 'Intensité des pixels', 'y': 'Nombre de pixels'}, color_discrete_sequence=[palette_list[idx]] ) cols[idx].plotly_chart(fig, use_container_width=True) st.success('Histogrammes générés avec succès !', icon="✅") st.header("", divider='gray') # ================================================================================================== # ================================================================================================== col1, col2 = st.columns([0.4, 0.6]) with col1: st.markdown(''' La surface utile des masques est l'ensemble des pixels apportant de l'information dans l'analyse de notre problématique. En pratique, il s'agit de l'ensemble des pixels définissant les poumons sur la radiographie. Des éléments artefactuels peu commun sur la radiographies hors des poumons peut générer de la variabilité que le modèle va prendre en compte, le poussant alors à "observer" des zones qui ne sont pas pertinentes pour notre problématique. C'est là que l'application des masques pour limiter la "vision" du modèle aux poumons peut s'avérer utile. ''') with col2: ## Surface utile # Création de l'histogramme avec Plotly Express fig2 = px.histogram(df_masks, x = 'RATIO', color = 'LABEL', nbins = 70, barmode = 'overlay', # Superpose les distributions color_discrete_sequence = ['#A1C9F4', '#8DE5A1', '#FFB482', '#D0BBFF'], # Palette de couleurs opacity = 0.75) # Personnalisation supplémentaire fig2.update_traces(marker_line_color = 'gray', marker_line_width = 1.5) # Ajouter la bordure de barre fig2.update_layout( title = 'Ratio de la surface utile en appliquant les masques', xaxis_title = 'Ratio', yaxis_title = 'Nombre de cas', legend_title = 'Label', height = 500, width = 800 ) # Affichage du graphique st.plotly_chart(fig2)