import gradio as gr import tensorflow as tf import numpy as np import cv2 from huggingface_hub import hf_hub_download from tensorflow.keras.preprocessing import image # === Importar funciones del pipeline === from preprocessing.zoom import apply_zoom from preprocessing.hair_removal import quitar_pelos from preprocessing.segmentation import segmentar_lesion from preprocessing.metrics import ( calcular_area, calcular_perimetro, calcular_circularidad, calcular_simetria ) # Tamaño de entrada del modelo ROWS, COLS = 224, 224 # === Cargar modelo desde Hugging Face Hub === model_path = hf_hub_download(repo_id="Martinagg/simpleNet", filename="simpleNet.h5") model = tf.keras.models.load_model(model_path) # === Cargar modelo ZoomNet para Grad-CAM === zoom_path = hf_hub_download(repo_id="Martinagg/ZoomNet", filename="ZoomNet.keras") model_zoom = tf.keras.models.load_model(zoom_path) # === Función Grad-CAM === def make_gradcam_heatmap(img_array, model, last_conv_layer_name="conv4", pred_index=None): grad_model = tf.keras.models.Model( [model.inputs], [model.get_layer(last_conv_layer_name).output, model.output] ) with tf.GradientTape() as tape: conv_outputs, predictions = grad_model(img_array) if isinstance(predictions, list): predictions = predictions[0] if pred_index is None: pred_index = tf.argmax(predictions[0]) class_channel = predictions[:, pred_index] grads = tape.gradient(class_channel, conv_outputs) pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2)) conv_outputs = conv_outputs[0] heatmap = conv_outputs @ pooled_grads[..., tf.newaxis] heatmap = tf.squeeze(heatmap) heatmap = tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap) return heatmap.numpy() # === Función principal === def preprocess_and_predict(img_input): img = np.array(img_input)[:, :, ::-1] zoomed = apply_zoom(img, zoom_factor=0.9) rgb_clean = cv2.cvtColor(zoomed, cv2.COLOR_BGR2RGB) clean = quitar_pelos(rgb_clean) mask, lesion_rgb = segmentar_lesion(clean, size=(ROWS, COLS)) lesion_resized = cv2.resize(lesion_rgb, (ROWS, COLS)) img_array = image.img_to_array(lesion_resized) / 255.0 img_array = np.expand_dims(img_array, axis=0) probs = model.predict(img_array)[0] classes = ["Benign", "Malignant"] pred_idx = np.argmax(probs) pred_label = classes[pred_idx] prob_percent = int(probs[pred_idx] * 100) # Color dinámico para la barra color = "green" if pred_label == "Benign" else "red" result_text_html = f"

Predicción: {pred_label}

" result_bar_html = f"""
{prob_percent}%
""" # === Métricas geométricas === area = calcular_area(mask) perim = calcular_perimetro(mask) circ = calcular_circularidad(mask) sim_v, sim_h = calcular_simetria(mask) metrics_data = [ ["Área (px²)", area], ["Perímetro (px)", perim], ["Circularidad", round(circ, 3)], ["Simetría Vertical", round(sim_v, 3)], ["Simetría Horizontal", round(sim_h, 3)] ] # === Grad-CAM === raw_resized = cv2.resize(np.array(img_input), (ROWS, COLS)) raw_array = image.img_to_array(raw_resized) / 255.0 raw_array = np.expand_dims(raw_array, axis=0) heatmap = make_gradcam_heatmap(raw_array, model_zoom, last_conv_layer_name="conv4") heatmap = cv2.resize(heatmap, (ROWS, COLS)) heatmap = np.uint8(255 * heatmap) heatmap_color = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET) overlay = cv2.addWeighted(raw_resized.astype("uint8"), 0.6, heatmap_color, 0.4, 0) return mask, lesion_rgb, result_text_html, result_bar_html, metrics_data, overlay # === Interfaz con estilo === with gr.Blocks(css=""" body, .gradio-container { font-family: 'Inter', sans-serif; background: #ffffff !important; font-weight: bold !important; } h1, h2 { font-weight: 600; color: #111827; margin-bottom: 0.5rem; } .section { background: #f9fafb; /* gris muy claro */ border-radius: 0.75rem; padding: 1.5rem; margin: 1.5rem auto; box-shadow: 0 1px 3px rgba(0,0,0,0.08); max-width: 900px; } .gradio-container { max-width: 900px; margin: auto; } img { border-radius: 0.5rem; } /* === Botón Analizar personalizado === */ #analyze-btn { display: block; margin: 1.5rem auto; width: 90%; padding: 0.75rem 1rem; background-color: #f97316; color: #7c2d12; border: 2px solid #f97316; border-radius: 0.75rem; font-weight: bold; font-size: 1rem; cursor: pointer; transition: background 0.3s ease, color 0.3s ease; } #analyze-btn:hover { background-color: #ea580c; color: white; } """) as demo: # === Título e introducción === gr.HTML("""

DermaScan - Clasificación de Melanomas

Una herramienta de apoyo basada en IA para la detección temprana de cáncer de piel.

El melanoma es un cáncer de piel agresivo que se origina en los melanocitos. Aunque poco frecuente, es el más peligroso por su capacidad de generar metástasis si no se detecta a tiempo.

En esta aplicación hemos implementado una red neuronal convolucional (CNN) entrenada con imágenes dermatoscópicas para estimar la probabilidad de que una lesión sea benigna o maligna.

Además, incorporamos técnicas de interpretabilidad como Grad-CAM y métricas geométricas basadas en el criterio clínico ABCDE, que sirven como apoyo en la exploración médica.

""") # === Subir imagen === with gr.Column(elem_classes="section"): gr.HTML("

Subir imagen

") gr.HTML("

Sube una imagen clara de tu lunar Asegúrate de que esté bien iluminada y centrada.

") img_input = gr.Image(type="pil", label="Imagen de la lesión", elem_id="upload-img") run_btn = gr.Button("Analizar", elem_id="analyze-btn", scale=0) # === Segmentación === with gr.Column(elem_classes="section"): gr.HTML("

Preprocesamiento y Segmentación

") gr.HTML("""

En este apartado verás cómo preparamos tu imagen para que la red neuronal se concentre en la lesión:

""") img_mask = gr.Image(type="numpy", label="Máscara Binaria", elem_id="mask-img") img_segmented = gr.Image(type="numpy", label="Lesión Segmentada", elem_id="seg-img") # === Grad-CAM === with gr.Column(elem_classes="section"): gr.HTML("

Grad-CAM

") gr.HTML("""

El Grad-CAM resalta las zonas en las que la red neuronal ha puesto mayor atención para clasificar la lesión. Los colores cálidos indican mayor relevancia: rojo y amarillo muestran las áreas más importantes.

""") gradcam_img = gr.Image(type="numpy", label="Mapa de activación", elem_id="gradcam-img") gr.HTML("""
Baja relevancia
Media relevancia
Alta relevancia
""") # === Resultados === with gr.Column(elem_classes="section"): gr.HTML("

Resultados del modelo

") result_text = gr.HTML() result_bar = gr.HTML() # === Cuadro amarillo estilo ABCDE === gr.HTML("""

Criterio ABCDE del melanoma

A

Asimetría

B

Bordes irregulares

C

Color variado

D

Diámetro > 6mm

E

Evolución

Estas características aumentan la probabilidad de melanoma. Consulte a un dermatólogo si observa alguna.

""") # === Tabla de métricas debajo === gr.HTML("""

Métricas lunar para consultar en casos

""") metrics_table = gr.Dataframe( headers=["Métrica", "Valor"], datatype=["str", "number"], interactive=False, label="Métricas calculadas", wrap=True, row_count=(5, "fixed"), col_count=(2, "fixed") ) # === Aviso final === gr.HTML("""

Este sistema es solo de apoyo y nunca sustituye la valoración de un experto médico.

""") # === Conexión botón -> función === run_btn.click( fn=preprocess_and_predict, inputs=[img_input], outputs=[img_mask, img_segmented, result_text, result_bar, metrics_table, gradcam_img] ) # === Lanzar en tema claro === if __name__ == "__main__": demo.launch()