|
|
import gradio as gr |
|
|
import tensorflow as tf |
|
|
import numpy as np |
|
|
import time |
|
|
|
|
|
|
|
|
IMG_SIZE = (224, 224) |
|
|
MODEL_PATH = "dental_classifier_model.keras" |
|
|
CLASS_NAMES = ['no_valido', 'valido'] |
|
|
|
|
|
|
|
|
model = None |
|
|
model_load_message = "Cargando modelo... por favor espera." |
|
|
|
|
|
try: |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
def preprocess_image(img): |
|
|
"""Preprocesa la imagen de entrada al formato que espera el modelo.""" |
|
|
if img is None: |
|
|
return None |
|
|
|
|
|
if len(img.shape) == 2: |
|
|
img = np.stack((img,)*3, axis=-1) |
|
|
elif img.shape[2] == 4: |
|
|
img = img[:, :, :3] |
|
|
|
|
|
img = tf.image.resize(img, IMG_SIZE) |
|
|
img_array = tf.expand_dims(img, 0) |
|
|
img_array = img_array / 255.0 |
|
|
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: |
|
|
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 |
|
|
|
|
|
|
|
|
color_borde = "#4CAF50" if predicted_class == "valido" else "#FF6B6B" |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
with gr.Blocks(theme=gr.themes.Soft(primary_hue=gr.themes.colors.emerald, secondary_hue=gr.themes.colors.slate)) as demo: |
|
|
|
|
|
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>") |
|
|
|
|
|
|
|
|
gr.Textbox(value=model_load_message, interactive=False, container=False, |
|
|
show_label=False, elem_id="model_status_message", |
|
|
label="Estado del Modelo", |
|
|
render=True, |
|
|
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, |
|
|
) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
boton_analizar.click(fn=predecir, inputs=rx_input, outputs=resultado) |
|
|
boton_limpiar.click(lambda: (None, initial_result_html), inputs=[], outputs=[rx_input, resultado]) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch(share=False) |
|
|
|
|
|
|