| """
|
| ===================================================================
|
| APP.PY - Neural Feature Visualization Dashboard (REFACTORIZADO)
|
| ===================================================================
|
|
|
| Aplicación Streamlit refactorizada con:
|
| - Manejo robusto de session state
|
| - Sin st.rerun() innecesarios
|
| - Caché de objetos pesados
|
| - Separación lógica vs UI
|
| - Persistencia correcta entre interacciones
|
|
|
| Autor: Neural Viz Team
|
| Fecha: 2025-01-15 (Refactorizado)
|
| ===================================================================
|
| """
|
|
|
| import streamlit as st
|
| import torch
|
| import numpy as np
|
| from PIL import Image
|
| import matplotlib.pyplot as plt
|
| from pathlib import Path
|
| import sys
|
|
|
|
|
| from modules.model_manager import ModelManager
|
| from modules.image_processor import ImageProcessor
|
| from modules.neuron_analyzer import NeuronAnalyzer
|
| from modules.feature_generator import FeatureGenerator
|
| from modules.visualizer import Visualizer
|
| from utils.cache_manager import get_cache
|
| from utils.helpers import format_number, format_layer_name, format_model_name
|
|
|
|
|
| from config import (
|
| PAGE_TITLE, PAGE_ICON, LAYOUT, SIDEBAR_STATE,
|
| WELCOME_MESSAGE, MAX_NEURONS_DISPLAY,
|
| AVAILABLE_MODELS, IMAGE_SIZE, ROI_SIZE
|
| )
|
|
|
|
|
|
|
|
|
|
|
|
|
| st.set_page_config(
|
| page_title=PAGE_TITLE,
|
| page_icon=PAGE_ICON,
|
| layout=LAYOUT,
|
| initial_sidebar_state=SIDEBAR_STATE
|
| )
|
|
|
|
|
|
|
|
|
|
|
|
|
| def init_session_state():
|
| """Inicializa session state con valores por defecto."""
|
|
|
| defaults = {
|
|
|
| 'image_loaded': False,
|
| 'model_loaded': False,
|
| 'heatmap_generated': False,
|
| 'comparison_generated': False,
|
|
|
|
|
| 'image_tensor': None,
|
| 'image_visual': None,
|
| 'image_pil': None,
|
| 'current_image_id': None,
|
| 'model': None,
|
| 'model_manager': None,
|
| 'activations': None,
|
| 'neuron_stats': None,
|
| 'heatmap': None,
|
| 'roi_center': None,
|
| 'synthetic_image': None,
|
| 'roi_real': None,
|
|
|
|
|
| 'analyzer': None,
|
| 'generator': None,
|
| 'processor': None,
|
| 'visualizer': None,
|
|
|
|
|
| 'device': torch.device('cuda' if torch.cuda.is_available() else 'cpu'),
|
| 'current_model_name': None,
|
| 'selected_layer': None,
|
| 'selected_neuron': 0,
|
| 'available_layers': [],
|
|
|
|
|
| 'debug_mode': False,
|
| 'show_logs': False,
|
| }
|
|
|
| for key, default_value in defaults.items():
|
| if key not in st.session_state:
|
| st.session_state[key] = default_value
|
|
|
|
|
| def get_state(key: str, default=None):
|
| """Helper seguro para obtener valores del session state."""
|
| return st.session_state.get(key, default)
|
|
|
|
|
| def set_state(key: str, value):
|
| """Helper para setear valores en session state."""
|
| st.session_state[key] = value
|
|
|
|
|
|
|
|
|
|
|
|
|
| def reset_all():
|
| """Resetea TODO el estado (como reiniciar la app)."""
|
| keys_to_keep = ['device']
|
| keys_to_delete = [k for k in st.session_state.keys()
|
| if k not in keys_to_keep]
|
|
|
| for key in keys_to_delete:
|
| del st.session_state[key]
|
|
|
| init_session_state()
|
|
|
|
|
| def reset_from_image():
|
| """Resetea estado cuando cambia la imagen."""
|
| reset_keys = [
|
| 'heatmap_generated', 'comparison_generated',
|
| 'activations', 'neuron_stats', 'heatmap',
|
| 'roi_center', 'synthetic_image', 'roi_real',
|
| 'analyzer', 'generator', 'selected_neuron'
|
| ]
|
|
|
| for key in reset_keys:
|
| if key in st.session_state:
|
| st.session_state[key] = None if key not in [
|
| 'selected_neuron'] else 0
|
|
|
| st.session_state.heatmap_generated = False
|
| st.session_state.comparison_generated = False
|
|
|
|
|
| def reset_from_model():
|
| """Resetea estado cuando cambia modelo o capa."""
|
| reset_keys = [
|
| 'heatmap_generated', 'comparison_generated',
|
| 'activations', 'neuron_stats', 'heatmap',
|
| 'roi_center', 'synthetic_image', 'roi_real',
|
| 'analyzer', 'generator', 'selected_neuron'
|
| ]
|
|
|
| for key in reset_keys:
|
| if key in st.session_state:
|
| st.session_state[key] = None if key not in [
|
| 'selected_neuron'] else 0
|
|
|
| st.session_state.heatmap_generated = False
|
| st.session_state.comparison_generated = False
|
|
|
|
|
|
|
|
|
|
|
|
|
| def load_and_process_image(image_source, source_type='upload'):
|
| """
|
| Carga y procesa una imagen.
|
|
|
| Args:
|
| image_source: Archivo subido, PIL Image, o URL
|
| source_type: 'upload', 'pil', 'url'
|
|
|
| Returns:
|
| bool: True si tuvo éxito
|
| """
|
| try:
|
|
|
| if source_type == 'upload':
|
| image_pil = Image.open(image_source).convert('RGB')
|
| elif source_type == 'pil':
|
| image_pil = image_source
|
| elif source_type == 'url':
|
| import requests
|
| from io import BytesIO
|
| response = requests.get(image_source, timeout=10)
|
| response.raise_for_status()
|
| image_pil = Image.open(BytesIO(response.content)).convert('RGB')
|
| else:
|
| return False
|
|
|
|
|
| if st.session_state.processor is None:
|
| st.session_state.processor = ImageProcessor(
|
| device=st.session_state.device)
|
|
|
| img_tensor, img_visual = st.session_state.processor.load_and_preprocess(
|
| image_pil)
|
|
|
|
|
| st.session_state.image_pil = image_pil
|
| st.session_state.image_tensor = img_tensor
|
| st.session_state.image_visual = img_visual
|
| st.session_state.image_loaded = True
|
|
|
|
|
| reset_from_image()
|
|
|
| return True
|
|
|
| except Exception as e:
|
| st.error(f"❌ Error al procesar imagen: {e}")
|
| if st.session_state.debug_mode:
|
| import traceback
|
| st.code(traceback.format_exc())
|
| return False
|
|
|
|
|
| @st.cache_resource
|
| def load_model_cached(model_name: str):
|
| """Carga modelo con caché de Streamlit."""
|
| manager = ModelManager()
|
| model = manager.load_model(model_name)
|
| return model, manager
|
|
|
|
|
| def generate_heatmap():
|
| """
|
| Genera el mapa de calor de activaciones.
|
|
|
| Returns:
|
| bool: True si tuvo éxito
|
| """
|
| try:
|
|
|
| if (st.session_state.analyzer is None or
|
| st.session_state.analyzer.target_layer != st.session_state.selected_layer):
|
|
|
|
|
| if st.session_state.analyzer is not None:
|
| st.session_state.analyzer.cleanup()
|
|
|
|
|
| st.session_state.analyzer = NeuronAnalyzer(
|
| st.session_state.model,
|
| st.session_state.selected_layer,
|
| st.session_state.device
|
| )
|
|
|
| analyzer = st.session_state.analyzer
|
|
|
|
|
| activations = analyzer.extract_activations(
|
| st.session_state.image_tensor)
|
| st.session_state.activations = activations
|
|
|
|
|
| stats = analyzer.compute_neuron_statistics(activations)
|
| st.session_state.neuron_stats = stats
|
|
|
|
|
| heatmap = analyzer.compute_heatmap(activations, method='max')
|
| heatmap_resized = analyzer.resize_heatmap(heatmap, IMAGE_SIZE)
|
| st.session_state.heatmap = heatmap_resized
|
|
|
|
|
| roi_center = analyzer.find_max_activation_region(heatmap)
|
|
|
|
|
| scale_y = IMAGE_SIZE[0] / heatmap.shape[0]
|
| scale_x = IMAGE_SIZE[1] / heatmap.shape[1]
|
| roi_center_scaled = (
|
| int(roi_center[0] * scale_y),
|
| int(roi_center[1] * scale_x)
|
| )
|
| st.session_state.roi_center = roi_center_scaled
|
|
|
|
|
| st.session_state.heatmap_generated = True
|
|
|
| return True
|
|
|
| except Exception as e:
|
| st.error(f"❌ Error al generar mapa de calor: {e}")
|
| if st.session_state.debug_mode:
|
| import traceback
|
| st.code(traceback.format_exc())
|
| return False
|
|
|
|
|
| def generate_comparison(neuron_idx: int):
|
| """
|
| Genera la comparación Real vs Sintética.
|
|
|
| Args:
|
| neuron_idx: Índice de la neurona
|
|
|
| Returns:
|
| bool: True si tuvo éxito
|
| """
|
| try:
|
| analyzer = st.session_state.analyzer
|
|
|
|
|
| neuron_activation_map = analyzer.get_neuron_activation_map(
|
| st.session_state.activations,
|
| neuron_idx
|
| )
|
|
|
|
|
| roi_center_neuron = analyzer.find_max_activation_region(
|
| neuron_activation_map)
|
|
|
|
|
|
|
| act_shape = st.session_state.activations.shape[2:]
|
| scale_y = IMAGE_SIZE[0] / act_shape[0]
|
| scale_x = IMAGE_SIZE[1] / act_shape[1]
|
|
|
| roi_center_scaled = (
|
| int(roi_center_neuron[0] * scale_y),
|
| int(roi_center_neuron[1] * scale_x)
|
| )
|
|
|
|
|
| st.session_state.roi_center = roi_center_scaled
|
|
|
|
|
| roi_real = analyzer.extract_roi(
|
| st.session_state.image_visual,
|
| roi_center_scaled,
|
| ROI_SIZE
|
| )
|
|
|
|
|
| real_activation = st.session_state.neuron_stats[neuron_idx]['mean']
|
|
|
|
|
| if (st.session_state.generator is None or
|
| st.session_state.generator.target_layer != st.session_state.selected_layer):
|
|
|
|
|
| if st.session_state.generator is not None:
|
| st.session_state.generator.cleanup()
|
|
|
|
|
| st.session_state.generator = FeatureGenerator(
|
| st.session_state.model,
|
| st.session_state.selected_layer,
|
| st.session_state.device
|
| )
|
|
|
| generator = st.session_state.generator
|
|
|
|
|
| synthetic_img, history = generator.generate_pattern(
|
| neuron_idx=neuron_idx,
|
| verbose=False
|
| )
|
|
|
| synthetic_activation = history['activation'][-1]
|
|
|
|
|
| from skimage.transform import resize
|
| synthetic_resized = resize(
|
| synthetic_img / 255.0,
|
| ROI_SIZE,
|
| anti_aliasing=True
|
| )
|
|
|
|
|
|
|
| st.session_state.synthetic_image_full = synthetic_img / 255.0
|
| st.session_state.synthetic_image = synthetic_resized
|
| st.session_state.roi_real = roi_real
|
| st.session_state.real_activation = real_activation
|
| st.session_state.synthetic_activation = synthetic_activation
|
| st.session_state.selected_neuron_for_comparison = neuron_idx
|
| st.session_state.roi_center = roi_center_scaled
|
| st.session_state.comparison_generated = True
|
|
|
| return True
|
|
|
| except Exception as e:
|
| st.error(f"❌ Error al generar comparación: {e}")
|
| if st.session_state.debug_mode:
|
| import traceback
|
| st.code(traceback.format_exc())
|
| return False
|
|
|
|
|
|
|
|
|
|
|
|
|
| def section_image_upload():
|
| """Sección de carga de imagen."""
|
|
|
| st.header("📤 1. Carga de Imagen")
|
|
|
| upload_option = st.radio(
|
| "Selecciona una opción:",
|
| ["Subir imagen", "Usar imagen de muestra"],
|
| horizontal=True,
|
| key="upload_option_radio"
|
| )
|
|
|
| image_to_load = None
|
| source_type = None
|
|
|
| if upload_option == "Subir imagen":
|
| uploaded_file = st.file_uploader(
|
| "Sube una imagen (JPG, PNG)",
|
| type=['jpg', 'jpeg', 'png'],
|
| key="file_uploader"
|
| )
|
|
|
| if uploaded_file is not None:
|
| image_to_load = uploaded_file
|
| source_type = 'upload'
|
|
|
| else:
|
|
|
| if st.button("📥 Cargar Imagen de Muestra", key="load_sample"):
|
| with st.spinner("Descargando imagen..."):
|
| url = 'https://images.unsplash.com/photo-1574158622682-e40e69881006?w=400'
|
| success = load_and_process_image(url, 'url')
|
|
|
| if success:
|
| st.success("✅ Imagen de muestra cargada")
|
| else:
|
| st.warning("⚠️ Fallo descarga, usando imagen de respaldo")
|
| fallback = Image.new(
|
| 'RGB', (224, 224), color=(100, 150, 200))
|
| load_and_process_image(fallback, 'pil')
|
|
|
|
|
| if image_to_load is not None and source_type is not None:
|
|
|
|
|
| new_image_id = None
|
|
|
| if source_type == 'upload':
|
| new_image_id = f"upload_{image_to_load.name}_{image_to_load.size}"
|
|
|
| current_image_id = st.session_state.get('current_image_id', None)
|
|
|
|
|
| if new_image_id != current_image_id:
|
| success = load_and_process_image(image_to_load, source_type)
|
| if success:
|
| st.session_state.current_image_id = new_image_id
|
| st.success("✅ Imagen procesada correctamente")
|
|
|
|
|
| if st.session_state.image_loaded and st.session_state.image_visual is not None:
|
| st.write("---")
|
| col1, col2, col3 = st.columns([1, 2, 1])
|
| with col2:
|
| st.image(
|
| st.session_state.image_visual,
|
| caption="Imagen Cargada",
|
| use_column_width=True
|
| )
|
|
|
| if st.session_state.debug_mode:
|
| with st.expander("🔍 Debug Info - Imagen"):
|
| st.write(
|
| f"Tensor shape: {st.session_state.image_tensor.shape}")
|
| st.write(
|
| f"Visual shape: {st.session_state.image_visual.shape}")
|
| st.write(f"Device: {st.session_state.image_tensor.device}")
|
|
|
| elif not st.session_state.image_loaded:
|
| st.info("👆 Sube una imagen o carga la muestra para comenzar")
|
|
|
|
|
| def section_model_config():
|
| """Sección de configuración de modelo y capa."""
|
|
|
| st.header("⚙️ 2. Configuración del Modelo")
|
|
|
| if not st.session_state.image_loaded:
|
| st.warning("⚠️ Primero debes cargar una imagen")
|
| return
|
|
|
| col1, col2 = st.columns(2)
|
|
|
|
|
| with col1:
|
| st.subheader("Modelo")
|
|
|
| model_options = list(AVAILABLE_MODELS.keys())
|
| model_display = [format_model_name(m) for m in model_options]
|
|
|
|
|
| current_idx = 0
|
| if st.session_state.current_model_name:
|
| try:
|
| current_idx = model_options.index(
|
| st.session_state.current_model_name)
|
| except ValueError:
|
| pass
|
|
|
| selected_model_idx = st.selectbox(
|
| "Selecciona el modelo:",
|
| range(len(model_options)),
|
| index=current_idx,
|
| format_func=lambda i: model_display[i],
|
| key="model_selector"
|
| )
|
|
|
| selected_model = model_options[selected_model_idx]
|
|
|
|
|
| if selected_model != st.session_state.current_model_name:
|
| with st.spinner(f"Cargando {format_model_name(selected_model)}..."):
|
| model, manager = load_model_cached(selected_model)
|
| st.session_state.model = model
|
| st.session_state.model_manager = manager
|
| st.session_state.current_model_name = selected_model
|
| st.session_state.model_loaded = True
|
|
|
|
|
| layers = manager.get_conv_layers(model)
|
| st.session_state.available_layers = layers
|
| st.session_state.selected_layer = layers[0] if layers else None
|
|
|
|
|
| reset_from_model()
|
|
|
| st.success(f"✅ {format_model_name(selected_model)} cargado")
|
|
|
|
|
| model_info = AVAILABLE_MODELS[selected_model]
|
| st.caption(f"📝 {model_info['description']}")
|
| st.caption(f"💾 {model_info['size']}")
|
|
|
|
|
| with col2:
|
| st.subheader("Capa")
|
|
|
| if st.session_state.model_loaded and st.session_state.available_layers:
|
| layers = st.session_state.available_layers
|
|
|
|
|
| current_layer_idx = 0
|
| if st.session_state.selected_layer in layers:
|
| current_layer_idx = layers.index(
|
| st.session_state.selected_layer)
|
|
|
| selected_layer_idx = st.selectbox(
|
| "Selecciona la capa:",
|
| range(len(layers)),
|
| index=current_layer_idx,
|
| format_func=lambda i: f"{format_layer_name(layers[i])} ({layers[i]})",
|
| key="layer_selector"
|
| )
|
|
|
| selected_layer = layers[selected_layer_idx]
|
|
|
|
|
| if selected_layer != st.session_state.selected_layer:
|
| st.session_state.selected_layer = selected_layer
|
| reset_from_model()
|
|
|
|
|
| if st.session_state.model_manager:
|
| layer_info = st.session_state.model_manager.get_layer_info(
|
| st.session_state.model,
|
| selected_layer
|
| )
|
| st.caption(
|
| f"📊 Canales: {layer_info.get('num_channels', 'N/A')}")
|
| st.caption(f"🔢 Kernel: {layer_info.get('kernel_size', 'N/A')}")
|
|
|
|
|
| def section_heatmap_generation():
|
| """Sección de generación de mapa de calor."""
|
|
|
| st.header("🔥 3. Mapa de Activación")
|
|
|
| if not st.session_state.image_loaded or not st.session_state.model_loaded:
|
| st.warning("⚠️ Completa las secciones anteriores primero")
|
| return
|
|
|
|
|
| if st.button("🔍 Generar Mapa de Calor", type="primary", key="gen_heatmap_btn"):
|
| with st.spinner("Analizando activaciones..."):
|
| success = generate_heatmap()
|
|
|
| if success:
|
| st.success("✅ Mapa de calor generado")
|
|
|
|
|
| if st.session_state.heatmap_generated:
|
| st.write("---")
|
|
|
|
|
| if st.session_state.visualizer is None:
|
| st.session_state.visualizer = Visualizer()
|
|
|
| viz = st.session_state.visualizer
|
|
|
|
|
| fig = viz.create_heatmap_with_neuron_markers(
|
| st.session_state.image_visual,
|
| st.session_state.activations,
|
| st.session_state.neuron_stats,
|
| top_n=MAX_NEURONS_DISPLAY,
|
| title="Mapa de Activación Neuronal"
|
| )
|
|
|
| st.pyplot(fig)
|
| plt.close(fig)
|
|
|
|
|
| st.subheader("📊 Neuronas Más Activas")
|
|
|
| ranked = sorted(
|
| st.session_state.neuron_stats,
|
| key=lambda x: x['mean'],
|
| reverse=True
|
| )[:MAX_NEURONS_DISPLAY]
|
|
|
|
|
| if ranked:
|
|
|
| max_activation = max(ranked[0]['mean'], 1e-8)
|
| min_activation = min(s['mean'] for s in ranked)
|
| activation_range = max_activation - min_activation
|
|
|
|
|
| if activation_range < 1e-8:
|
| activation_range = 1.0
|
| else:
|
| max_activation = 1.0
|
| min_activation = 0.0
|
| activation_range = 1.0
|
|
|
| for i, stat in enumerate(ranked, 1):
|
| cols = st.columns([0.3, 2, 1, 1])
|
|
|
| with cols[0]:
|
| emoji = "⭐" if i == 1 else f"{i}."
|
| st.write(emoji)
|
|
|
| with cols[1]:
|
|
|
| if activation_range > 0:
|
| progress = (stat['mean'] - min_activation) / \
|
| activation_range
|
| else:
|
| progress = 1.0 if i == 1 else 0.0
|
|
|
|
|
| progress = max(0.0, min(1.0, progress))
|
|
|
| st.progress(progress, text=f"Neurona {stat['neuron_idx']}")
|
|
|
| with cols[2]:
|
| st.caption(f"μ: {stat['mean']:.3f}")
|
|
|
| with cols[3]:
|
| st.caption(f"max: {stat['max']:.3f}")
|
|
|
|
|
| def section_comparison():
|
| """Sección de comparación Real vs Sintética."""
|
|
|
| st.header("🔬 4. Comparación: Real vs Ideal")
|
|
|
| if not st.session_state.heatmap_generated:
|
| st.warning("⚠️ Primero debes generar el mapa de calor")
|
| return
|
|
|
|
|
| st.subheader("Selección de Neurona")
|
|
|
|
|
| ranked = sorted(
|
| st.session_state.neuron_stats,
|
| key=lambda x: x['mean'],
|
| reverse=True
|
| )[:MAX_NEURONS_DISPLAY]
|
|
|
| neuron_options = [s['neuron_idx'] for s in ranked]
|
| neuron_labels = [
|
| f"Neurona {s['neuron_idx']} (Act: {s['mean']:.3f})"
|
| for s in ranked
|
| ]
|
|
|
|
|
| selected_idx = st.selectbox(
|
| "Selecciona una neurona:",
|
| range(len(neuron_options)),
|
| format_func=lambda i: neuron_labels[i],
|
| key="neuron_selector"
|
| )
|
|
|
| selected_neuron = neuron_options[selected_idx]
|
|
|
| st.caption(f"📍 ROI: {st.session_state.roi_center}")
|
|
|
|
|
| if st.button("🎨 Generar Comparación", type="primary", key="gen_comparison_btn"):
|
| with st.spinner("Generando patrón sintético (10-30 seg)..."):
|
|
|
|
|
| progress_bar = st.progress(0)
|
| progress_bar.progress(30)
|
|
|
| success = generate_comparison(selected_neuron)
|
|
|
| progress_bar.progress(100)
|
|
|
| if success:
|
| st.success("✅ Comparación generada")
|
|
|
|
|
| if st.session_state.comparison_generated:
|
| st.write("---")
|
|
|
| viz = st.session_state.visualizer
|
|
|
|
|
| roi_to_use = st.session_state.get(
|
| 'roi_center_used', st.session_state.roi_center)
|
|
|
|
|
| st.subheader("🔬 Comparación Detallada: ROI vs Patrón Ideal")
|
|
|
| fig1 = viz.create_4panel_comparison(
|
| st.session_state.image_visual,
|
| st.session_state.roi_real,
|
| st.session_state.synthetic_image,
|
| roi_to_use,
|
| st.session_state.selected_neuron_for_comparison,
|
| st.session_state.real_activation,
|
| st.session_state.synthetic_activation
|
| )
|
|
|
| st.pyplot(fig1)
|
| plt.close(fig1)
|
|
|
|
|
| st.write("---")
|
| st.subheader("🎨 Visualización Global: Patrón Superpuesto")
|
| st.caption(
|
| "El patrón ideal se repite sobre toda la imagen para mostrar coincidencias globales")
|
|
|
|
|
| from skimage.transform import resize
|
| synthetic_full = st.session_state.synthetic_image
|
|
|
|
|
| if hasattr(st.session_state, 'synthetic_image_full'):
|
| synthetic_for_tile = st.session_state.synthetic_image_full
|
| else:
|
|
|
| synthetic_for_tile = synthetic_full
|
|
|
| fig2 = viz.create_pattern_overlay_comparison(
|
| st.session_state.image_visual,
|
| synthetic_for_tile,
|
| roi_to_use,
|
| st.session_state.selected_neuron_for_comparison,
|
| st.session_state.real_activation,
|
| st.session_state.synthetic_activation
|
| )
|
|
|
| st.pyplot(fig2)
|
| plt.close(fig2)
|
|
|
|
|
| st.subheader("📈 Métricas")
|
|
|
| col1, col2, col3 = st.columns(3)
|
|
|
| with col1:
|
| st.metric(
|
| "Activación Real",
|
| f"{st.session_state.real_activation:.3f}"
|
| )
|
|
|
| with col2:
|
| st.metric(
|
| "Activación Sintética",
|
| f"{st.session_state.synthetic_activation:.3f}"
|
| )
|
|
|
| with col3:
|
| improvement = (
|
| st.session_state.synthetic_activation /
|
| max(st.session_state.real_activation, 1e-8)
|
| )
|
| st.metric("Mejora", f"{improvement:.2f}x")
|
|
|
|
|
| def inject_custom_css():
|
| """
|
| Inyecta CSS personalizado para eliminar el scroll del contenido principal.
|
|
|
| Comportamiento:
|
| - El contenido principal NO tiene scroll (overflow hidden)
|
| - El sidebar mantiene su scroll natural
|
| - La altura del main se fija al 100% del viewport
|
| """
|
| st.markdown("""
|
| <style>
|
| /* Eliminar scroll del contenido principal */
|
| .main .block-container {
|
| overflow-y: hidden !important;
|
| max-height: 100vh !important;
|
| padding-top: 2rem !important;
|
| padding-bottom: 2rem !important;
|
| }
|
|
|
| /* Asegurar que el main ocupe todo el viewport sin scroll */
|
| section[data-testid="stMain"] {
|
| overflow-y: hidden !important;
|
| height: 100vh !important;
|
| }
|
|
|
| /* El sidebar mantiene su scroll natural */
|
| section[data-testid="stSidebar"] {
|
| overflow-y: auto !important;
|
| }
|
|
|
| /* Ajustar padding para mejor visualización */
|
| .main {
|
| padding: 0 !important;
|
| }
|
| </style>
|
| """, unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| def render_sidebar():
|
| """Renderiza el sidebar."""
|
|
|
| with st.sidebar:
|
| st.title("🧠 Neural Viz")
|
| st.caption("Feature Visualization")
|
|
|
| st.divider()
|
|
|
|
|
| st.subheader("📊 Estado")
|
|
|
| device_icon = "🟢" if torch.cuda.is_available() else "🔵"
|
| st.write(f"{device_icon} Device: `{st.session_state.device}`")
|
|
|
| status_icon = "✅" if st.session_state.image_loaded else "⏳"
|
| st.write(
|
| f"{status_icon} Imagen: {'Cargada' if st.session_state.image_loaded else 'Pendiente'}")
|
|
|
| status_icon = "✅" if st.session_state.model_loaded else "⏳"
|
| model_name = st.session_state.current_model_name or 'N/A'
|
| st.write(f"{status_icon} Modelo: `{model_name}`")
|
|
|
| if st.session_state.selected_layer:
|
| st.write(f"📍 Capa: `{st.session_state.selected_layer}`")
|
|
|
| st.divider()
|
|
|
|
|
| st.session_state.debug_mode = st.checkbox(
|
| "🐛 Modo Debug",
|
| value=st.session_state.debug_mode,
|
| help="Mostrar información detallada"
|
| )
|
|
|
| st.divider()
|
|
|
|
|
| with st.expander("❓ Ayuda"):
|
| st.markdown("""
|
| **Pasos:**
|
| 1. Carga una imagen
|
| 2. Selecciona modelo y capa
|
| 3. Genera mapa de calor
|
| 4. Selecciona neurona
|
| 5. Genera comparación
|
| """)
|
|
|
| st.divider()
|
|
|
|
|
| if st.button("🔄 Reiniciar Todo"):
|
| reset_all()
|
| st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
| def main():
|
| """Función principal."""
|
|
|
|
|
| init_session_state()
|
|
|
|
|
| inject_custom_css()
|
|
|
|
|
| render_sidebar()
|
|
|
|
|
| st.title(PAGE_TITLE)
|
|
|
|
|
| with st.expander("👋 Bienvenida", expanded=False):
|
| st.markdown(WELCOME_MESSAGE)
|
|
|
| st.divider()
|
|
|
|
|
| section_image_upload()
|
| st.divider()
|
|
|
| section_model_config()
|
| st.divider()
|
|
|
| section_heatmap_generation()
|
| st.divider()
|
|
|
| section_comparison()
|
|
|
|
|
| st.divider()
|
| st.caption("Desarrollado por @gaxoblanco | PyTorch & Streamlit")
|
|
|
|
|
|
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
| main()
|
|
|