RxValidas / app.py
SergioI1991's picture
Update app.py
1b156ec verified
import gradio as gr
import tensorflow as tf
import numpy as np
import time # Para simular un retraso en la carga del modelo
# --- Configuración ---
IMG_SIZE = (224, 224)
MODEL_PATH = "dental_classifier_model.keras" # Asegúrate de que esta ruta sea correcta
CLASS_NAMES = ['no_valido', 'valido']
# --- Cargar Modelo con mensaje de carga ---
model = None
model_load_message = "Cargando modelo... por favor espera."
try:
# Simular una carga lenta para ver el mensaje (puedes quitar esto en producción)
time.sleep(2)
model = tf.keras.models.load_model(MODEL_PATH)
model_load_message = "Modelo cargado exitosamente."
print("Modelo cargado exitosamente.")
except Exception as e:
model_load_message = f"Error cargando el modelo: {e}. Asegúrate que 'dental_classifier_model.keras' existe."
print(model_load_message)
# --- Funciones de Procesamiento ---
def preprocess_image(img):
"""Preprocesa la imagen de entrada al formato que espera el modelo."""
if img is None:
return None
# Asegurarse de que la imagen tiene 3 canales si es en escala de grises
if len(img.shape) == 2:
img = np.stack((img,)*3, axis=-1)
elif img.shape[2] == 4: #RGBA a RGB
img = img[:, :, :3]
img = tf.image.resize(img, IMG_SIZE)
img_array = tf.expand_dims(img, 0)
img_array = img_array / 255.0 # Normalizar
return img_array
def predecir(rx_image):
"""Realiza la predicción y formatea la salida HTML."""
if model is None:
return "<div class='result-box' style='color:#FF6B6B; font-weight: bold;'>Error: El modelo no se ha cargado.</div>"
if rx_image is None:
return "<div class='result-box' style='color:#FF6B6B; font-weight: bold;'>Por favor, sube una imagen RX para analizar.</div>"
img_array = preprocess_image(rx_image)
if img_array is None: # Si preprocess_image devuelve None por algún motivo
return "<div class='result-box' style='color:#FF6B6B; font-weight: bold;'>Error: No se pudo procesar la imagen.</div>"
try:
preds = model.predict(img_array)
except Exception as e:
print(f"Error durante la predicción: {e}")
return f"<div class='result-box' style='color:#FF6B6B; font-weight: bold;'>Error al realizar la predicción: {e}</div>"
score = tf.nn.softmax(preds[0])
predicted_index = np.argmax(score)
confidence = np.max(score) * 100
predicted_class = CLASS_NAMES[predicted_index]
other_index = 1 - predicted_index
other_class = CLASS_NAMES[other_index]
other_confidence = score[other_index] * 100
# Colores para el borde. El color del texto se heredará del CSS.
color_borde = "#4CAF50" if predicted_class == "valido" else "#FF6B6B" # Verde para válido, Rojo para no válido
# HTML para el resultado con estilos mejorados
# Eliminamos los 'style' de color de texto para que se hereden de la clase 'result-box'
resultado_texto = f"""
<div class='result-box' style='
border: 3px solid {color_borde};
box-shadow: 0 6px 20px rgba(0,0,0,0.15); /* Sombra más pronunciada */
'>
<div style='font-size:42px; font-weight:bold;'>
Resultado: <span style='color: {color_borde};'>{predicted_class.upper()}</span>
</div>
<div style='font-size:30px; margin-top:15px;'>
Confianza: <span style='font-weight:bold;'>{confidence:.2f}%</span>
</div>
<div style='font-size:24px; margin-top:10px;'>
(Probabilidad {other_class}: <span style='font-weight:bold;'>{other_confidence:.2f}%</span>)
</div>
</div>
"""
return resultado_texto
# --- Interfaz de Gradio ---
with gr.Blocks(theme=gr.themes.Soft(primary_hue=gr.themes.colors.emerald, secondary_hue=gr.themes.colors.slate)) as demo:
# --- Estilos CSS personalizados (Adaptados a Dark Mode) ---
gr.HTML(f"""
<style>
body {{
/* Se elimina el background-color y color fijos */
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}}
.gradio-container {{
max-width: 1200px;
margin: auto;
padding: 20px;
background-color: var(--background-fill-primary); /* Variable de Gradio para fondo */
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}}
h2 {{
color: var(--text-color-primary); /* Variable de Gradio para texto */
font-size: 2.8em;
font-weight: 700;
text-align: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 2px solid var(--border-color-accent); /* Variable de Gradio para bordes */
}}
/* Estilos de botones (se mantienen, ya que definen su propio color y fondo) */
.gr-button.primary {{
background-color: #28a745 !important;
border-color: #28a745 !important;
color: white !important;
font-weight: bold;
padding: 12px 25px;
font-size: 1.1em;
border-radius: 8px;
transition: all 0.3s ease;
}}
.gr-button.primary:hover {{
background-color: #218838 !important;
border-color: #1e7e34 !important;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
}}
.gr-button.secondary {{
background-color: #6c757d !important;
border-color: #6c757d !important;
color: white !important;
font-weight: bold;
padding: 12px 25px;
font-size: 1.1em;
border-radius: 8px;
transition: all 0.3s ease;
}}
.gr-button.secondary:hover {{
background-color: #5a6268 !important;
border-color: #545b62 !important;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
}}
.result-box {{
font-size: 28px;
text-align: center;
padding: 50px;
min-height: 380px;
border-radius: 20px;
background-color: var(--background-fill-secondary); /* Variable de Gradio */
display: flex;
flex-direction: column;
justify-content: center;
color: var(--text-color-primary); /* Variable de Gradio */
transition: all 0.3s ease;
}}
.result-box:hover {{
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.2);
}}
.gr-image {{
border-radius: 12px;
border: 1px solid var(--border-color-primary); /* Variable de Gradio */
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
}}
.gr-label {{
font-weight: 600;
color: var(--text-color-secondary); /* Variable de Gradio */
font-size: 1.2em;
margin-bottom: 8px;
}}
/* Clase para el subtítulo */
.subtitle {{
text-align:center;
font-size:1.1em;
color: var(--text-color-secondary); /* Variable de Gradio */
}}
</style>
""")
gr.Markdown("## Clasificador RX LAB 🦷 V1(529NV-348V) TFG Marta B.")
gr.Markdown("<p class='subtitle'>Sube una imagen de una radiografía dental para clasificarla como válida o no válida.</p>")
# Mensaje de carga del modelo
gr.Textbox(value=model_load_message, interactive=False, container=False,
show_label=False, elem_id="model_status_message",
label="Estado del Modelo",
render=True, # Asegura que se renderiza inicialmente
info="El modelo está cargando..." if "Cargando" in model_load_message else None,
visible=True if "Cargando" in model_load_message or "Error" in model_load_message else False,
)
# Texto de estado inicial para la caja de resultados
initial_result_html = f"<div class='result-box'><div style='font-size:24px; color: var(--text-color-secondary);'>Esperando imagen...</div></div>"
with gr.Row(variant="panel", scale=1):
with gr.Column(scale=1, min_width=400):
gr.Markdown("### Sube tu Radiografía")
rx_input = gr.Image(type="numpy", label="Imagen de Radiografía Dental", show_label=True, height=450)
with gr.Row():
boton_limpiar = gr.Button("Limpiar", variant="secondary", size="lg", )
boton_analizar = gr.Button("Analizar RX", variant="primary", size="lg",)
with gr.Column(scale=1, min_width=400):
gr.Markdown("### Resultado del Análisis")
resultado = gr.HTML(label="Análisis de Radiografía", show_label=True, value=initial_result_html)
# --- Conexiones de Eventos ---
boton_analizar.click(fn=predecir, inputs=rx_input, outputs=resultado)
boton_limpiar.click(lambda: (None, initial_result_html), inputs=[], outputs=[rx_input, resultado])
# --- Lanzar la App ---
if __name__ == "__main__":
demo.launch(share=False)