Spaces:
Running
Running
import torch | |
from transformers import ViTImageProcessor, ViTForImageClassification | |
from PIL import Image | |
import matplotlib.pyplot as plt | |
import numpy as np | |
import gradio as gr | |
import io | |
import base64 | |
from torchvision import transforms | |
import torch.nn.functional as F | |
# --- MODELOS VERIFICADOS DISPONIBLES EN HUGGING FACE --- | |
# 1. Google Derm Foundation (VERIFICADO - existe en Hugging Face) | |
try: | |
derm_processor = ViTImageProcessor.from_pretrained("google/derm-foundation") | |
derm_model = ViTForImageClassification.from_pretrained("google/derm-foundation") | |
derm_model.eval() | |
DERM_AVAILABLE = True | |
print("✅ Google Derm Foundation cargado exitosamente") | |
except Exception as e: | |
DERM_AVAILABLE = False | |
print(f"❌ Google Derm Foundation no disponible: {e}") | |
# 2. Modelo HAM10k especializado (VERIFICADO) | |
try: | |
ham_processor = ViTImageProcessor.from_pretrained("bsenst/skin-cancer-HAM10k") | |
ham_model = ViTForImageClassification.from_pretrained("bsenst/skin-cancer-HAM10k") | |
ham_model.eval() | |
HAM_AVAILABLE = True | |
print("✅ HAM10k especializado cargado exitosamente") | |
except Exception as e: | |
HAM_AVAILABLE = False | |
print(f"❌ HAM10k especializado no disponible: {e}") | |
# 3. Modelo ISIC 2024 con SMOTE (VERIFICADO) | |
try: | |
isic_processor = ViTImageProcessor.from_pretrained("jhoppanne/SkinCancerClassifier_smote-V0") | |
isic_model = ViTForImageClassification.from_pretrained("jhoppanne/SkinCancerClassifier_smote-V0") | |
isic_model.eval() | |
ISIC_AVAILABLE = True | |
print("✅ ISIC 2024 SMOTE cargado exitosamente") | |
except Exception as e: | |
ISIC_AVAILABLE = False | |
print(f"❌ ISIC 2024 SMOTE no disponible: {e}") | |
# 4. Modelo genérico de detección (VERIFICADO) | |
try: | |
generic_processor = ViTImageProcessor.from_pretrained("syaha/skin_cancer_detection_model") | |
generic_model = ViTForImageClassification.from_pretrained("syaha/skin_cancer_detection_model") | |
generic_model.eval() | |
GENERIC_AVAILABLE = True | |
print("✅ Modelo genérico cargado exitosamente") | |
except Exception as e: | |
GENERIC_AVAILABLE = False | |
print(f"❌ Modelo genérico no disponible: {e}") | |
# 5. Modelo de melanoma específico (VERIFICADO) | |
try: | |
melanoma_processor = ViTImageProcessor.from_pretrained("milutinNemanjic/Melanoma-detection-model") | |
melanoma_model = ViTForImageClassification.from_pretrained("milutinNemanjic/Melanoma-detection-model") | |
melanoma_model.eval() | |
MELANOMA_AVAILABLE = True | |
print("✅ Modelo melanoma específico cargado exitosamente") | |
except Exception as e: | |
MELANOMA_AVAILABLE = False | |
print(f"❌ Modelo melanoma específico no disponible: {e}") | |
# 6. Tu modelo actual como respaldo | |
try: | |
backup_processor = ViTImageProcessor.from_pretrained("Anwarkh1/Skin_Cancer-Image_Classification") | |
backup_model = ViTForImageClassification.from_pretrained("Anwarkh1/Skin_Cancer-Image_Classification") | |
backup_model.eval() | |
BACKUP_AVAILABLE = True | |
print("✅ Modelo de respaldo cargado exitosamente") | |
except Exception as e: | |
BACKUP_AVAILABLE = False | |
print(f"❌ Modelo de respaldo no disponible: {e}") | |
# Clases HAM10000 estándar | |
CLASSES = [ | |
"Queratosis actínica / Bowen", "Carcinoma células basales", | |
"Lesión queratósica benigna", "Dermatofibroma", | |
"Melanoma maligno", "Nevus melanocítico", "Lesión vascular" | |
] | |
RISK_LEVELS = { | |
0: {'level': 'Alto', 'color': '#ff6b35', 'weight': 0.7}, # akiec | |
1: {'level': 'Crítico', 'color': '#cc0000', 'weight': 0.9}, # bcc | |
2: {'level': 'Bajo', 'color': '#44ff44', 'weight': 0.1}, # bkl | |
3: {'level': 'Bajo', 'color': '#44ff44', 'weight': 0.1}, # df | |
4: {'level': 'Crítico', 'color': '#990000', 'weight': 1.0}, # melanoma | |
5: {'level': 'Bajo', 'color': '#66ff66', 'weight': 0.1}, # nv | |
6: {'level': 'Moderado', 'color': '#ffaa00', 'weight': 0.3} # vasc | |
} | |
MALIGNANT_INDICES = [0, 1, 4] # akiec, bcc, melanoma | |
def safe_predict(image, processor, model, model_name, expected_classes=7): | |
"""Predicción segura que maneja diferentes números de clases""" | |
try: | |
inputs = processor(image, return_tensors="pt") | |
with torch.no_grad(): | |
outputs = model(**inputs) | |
logits = outputs.logits | |
# Manejar diferentes números de clases | |
if logits.shape[1] != expected_classes: | |
print(f"⚠️ {model_name}: Esperaba {expected_classes} clases, obtuvo {logits.shape[1]}") | |
if logits.shape[1] == 2: # Modelo binario (benigno/maligno) | |
probabilities = F.softmax(logits, dim=-1).cpu().numpy()[0] | |
# Convertir a formato de 7 clases (simplificado) | |
expanded_probs = np.zeros(expected_classes) | |
if probabilities[1] > 0.5: # Maligno | |
expanded_probs[4] = probabilities[1] * 0.6 # Melanoma | |
expanded_probs[1] = probabilities[1] * 0.3 # BCC | |
expanded_probs[0] = probabilities[1] * 0.1 # AKIEC | |
else: # Benigno | |
expanded_probs[5] = probabilities[0] * 0.7 # Nevus | |
expanded_probs[2] = probabilities[0] * 0.2 # BKL | |
expanded_probs[3] = probabilities[0] * 0.1 # DF | |
probabilities = expanded_probs | |
else: | |
# Para otros números de clases, normalizar o truncar | |
probabilities = F.softmax(logits, dim=-1).cpu().numpy()[0] | |
if len(probabilities) > expected_classes: | |
probabilities = probabilities[:expected_classes] | |
elif len(probabilities) < expected_classes: | |
temp = np.zeros(expected_classes) | |
temp[:len(probabilities)] = probabilities | |
probabilities = temp | |
else: | |
probabilities = F.softmax(logits, dim=-1).cpu().numpy()[0] | |
predicted_idx = int(np.argmax(probabilities)) | |
predicted_class = CLASSES[predicted_idx] if predicted_idx < len(CLASSES) else "Desconocido" | |
confidence = float(probabilities[predicted_idx]) | |
is_malignant = predicted_idx in MALIGNANT_INDICES | |
return { | |
'model': model_name, | |
'class': predicted_class, | |
'confidence': confidence, | |
'probabilities': probabilities, | |
'is_malignant': is_malignant, | |
'predicted_idx': predicted_idx, | |
'success': True | |
} | |
except Exception as e: | |
print(f"❌ Error en {model_name}: {e}") | |
return { | |
'model': model_name, | |
'error': str(e), | |
'class': 'Error', | |
'confidence': 0.0, | |
'is_malignant': False, | |
'success': False | |
} | |
def ensemble_prediction(predictions): | |
"""Combina múltiples predicciones usando weighted voting inteligente""" | |
valid_preds = [p for p in predictions if p.get('success', False)] | |
if not valid_preds: | |
return None | |
# Weighted ensemble basado en confianza y disponibilidad del modelo | |
ensemble_probs = np.zeros(len(CLASSES)) | |
total_weight = 0 | |
# Pesos específicos por modelo (basado en calidad esperada) | |
model_weights = { | |
"🏥 Google Derm Foundation": 1.0, | |
"🧠 HAM10k Especializado": 0.9, | |
"🆕 ISIC 2024 SMOTE": 0.8, | |
"🔬 Melanoma Específico": 0.7, | |
"🌐 Genérico": 0.6, | |
"🔄 Respaldo Original": 0.5 | |
} | |
for pred in valid_preds: | |
model_weight = model_weights.get(pred['model'], 0.5) | |
confidence_weight = pred['confidence'] | |
final_weight = model_weight * confidence_weight | |
ensemble_probs += pred['probabilities'] * final_weight | |
total_weight += final_weight | |
if total_weight > 0: | |
ensemble_probs /= total_weight | |
ensemble_idx = int(np.argmax(ensemble_probs)) | |
ensemble_class = CLASSES[ensemble_idx] | |
ensemble_confidence = float(ensemble_probs[ensemble_idx]) | |
ensemble_malignant = ensemble_idx in MALIGNANT_INDICES | |
# Calcular consenso de malignidad | |
malignant_votes = sum(1 for p in valid_preds if p.get('is_malignant', False)) | |
malignant_consensus = malignant_votes / len(valid_preds) | |
return { | |
'class': ensemble_class, | |
'confidence': ensemble_confidence, | |
'probabilities': ensemble_probs, | |
'is_malignant': ensemble_malignant, | |
'predicted_idx': ensemble_idx, | |
'malignant_consensus': malignant_consensus, | |
'num_models': len(valid_preds) | |
} | |
def calculate_risk_score(ensemble_result): | |
"""Calcula score de riesgo sofisticado""" | |
if not ensemble_result: | |
return 0.0 | |
# Score base del ensemble | |
base_score = ensemble_result['probabilities'][ensemble_result['predicted_idx']] * \ | |
RISK_LEVELS[ensemble_result['predicted_idx']]['weight'] | |
# Ajuste por consenso de malignidad | |
consensus_boost = ensemble_result['malignant_consensus'] * 0.3 | |
# Bonus por número de modelos | |
model_confidence = min(ensemble_result['num_models'] / 5.0, 1.0) * 0.1 | |
final_score = base_score + consensus_boost + model_confidence | |
return min(final_score, 1.0) | |
def analizar_lesion_verificado(img): | |
"""Análisis con modelos verificados existentes""" | |
predictions = [] | |
# Probar modelos disponibles en orden de preferencia | |
models_to_try = [ | |
(DERM_AVAILABLE, derm_processor, derm_model, "🏥 Google Derm Foundation"), | |
(HAM_AVAILABLE, ham_processor, ham_model, "🧠 HAM10k Especializado"), | |
(ISIC_AVAILABLE, isic_processor, isic_model, "🆕 ISIC 2024 SMOTE"), | |
(MELANOMA_AVAILABLE, melanoma_processor, melanoma_model, "🔬 Melanoma Específico"), | |
(GENERIC_AVAILABLE, generic_processor, generic_model, "🌐 Genérico"), | |
(BACKUP_AVAILABLE, backup_processor, backup_model, "🔄 Respaldo Original") | |
] | |
for available, processor, model, name in models_to_try: | |
if available: | |
pred = safe_predict(img, processor, model, name) | |
predictions.append(pred) | |
if not predictions: | |
return "❌ No hay modelos disponibles", "" | |
# Ensemble de predicciones | |
ensemble_result = ensemble_prediction(predictions) | |
if not ensemble_result: | |
return "❌ Error en el análisis ensemble", "" | |
# Calcular riesgo | |
risk_score = calculate_risk_score(ensemble_result) | |
# Generar visualización | |
colors = [RISK_LEVELS[i]['color'] for i in range(len(CLASSES))] | |
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7)) | |
# Gráfico principal del ensemble | |
bars = ax1.bar(CLASSES, ensemble_result['probabilities'] * 100, color=colors, alpha=0.8) | |
ax1.set_title("🎯 Predicción Ensemble (Modelos Combinados)", fontsize=16, fontweight='bold', pad=20) | |
ax1.set_ylabel("Probabilidad (%)", fontsize=12) | |
ax1.set_xticklabels(CLASSES, rotation=45, ha='right', fontsize=10) | |
ax1.grid(axis='y', alpha=0.3) | |
ax1.set_ylim(0, 100) | |
# Destacar la predicción principal | |
bars[ensemble_result['predicted_idx']].set_edgecolor('black') | |
bars[ensemble_result['predicted_idx']].set_linewidth(3) | |
bars[ensemble_result['predicted_idx']].set_alpha(1.0) | |
# Gráfico de consenso | |
consensus_data = ['Benigno', 'Maligno'] | |
consensus_values = [1 - ensemble_result['malignant_consensus'], ensemble_result['malignant_consensus']] | |
consensus_colors = ['#27ae60', '#e74c3c'] | |
bars2 = ax2.bar(consensus_data, consensus_values, color=consensus_colors, alpha=0.8) | |
ax2.set_title(f"🤝 Consenso Malignidad ({ensemble_result['num_models']} modelos)", | |
fontsize=16, fontweight='bold', pad=20) | |
ax2.set_ylabel("Proporción de Modelos", fontsize=12) | |
ax2.set_ylim(0, 1) | |
ax2.grid(axis='y', alpha=0.3) | |
# Añadir valores en las barras | |
for bar, value in zip(bars2, consensus_values): | |
height = bar.get_height() | |
ax2.text(bar.get_x() + bar.get_width()/2., height + 0.02, | |
f'{value:.1%}', ha='center', va='bottom', fontweight='bold') | |
plt.tight_layout() | |
buf = io.BytesIO() | |
plt.savefig(buf, format="png", dpi=120, bbox_inches='tight') | |
plt.close(fig) | |
chart_html = f'<img src="data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}" style="max-width:100%; border-radius:8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>' | |
# Generar reporte detallado | |
informe = f""" | |
<div style="font-family: 'Segoe UI', Arial, sans-serif; max-width: 1000px; margin: auto; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); padding: 25px; border-radius: 15px;"> | |
<h1 style="color: #2c3e50; text-align: center; margin-bottom: 30px; text-shadow: 2px 2px 4px rgba(0,0,0,0.1);"> | |
🏥 Análisis Dermatológico Multi-Modelo IA | |
</h1> | |
<div style="background: white; padding: 25px; border-radius: 12px; margin-bottom: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);"> | |
<h2 style="color: #34495e; margin-top: 0; border-bottom: 3px solid #3498db; padding-bottom: 10px;"> | |
📊 Resultados Individuales por Modelo | |
</h2> | |
<div style="overflow-x: auto;"> | |
<table style="width: 100%; border-collapse: collapse; font-size: 14px; margin-top: 15px;"> | |
<thead> | |
<tr style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;"> | |
<th style="padding: 15px; text-align: left; border-radius: 8px 0 0 0;">Modelo</th> | |
<th style="padding: 15px; text-align: left;">Diagnóstico</th> | |
<th style="padding: 15px; text-align: left;">Confianza</th> | |
<th style="padding: 15px; text-align: left;">Estado</th> | |
<th style="padding: 15px; text-align: left; border-radius: 0 8px 0 0;">Malignidad</th> | |
</tr> | |
</thead> | |
<tbody> | |
""" | |
for i, pred in enumerate(predictions): | |
row_color = "#f8f9fa" if i % 2 == 0 else "#ffffff" | |
if pred.get('success', False): | |
status_icon = "✅" | |
status_color = "#27ae60" | |
status_text = "Activo" | |
malignant_color = "#e74c3c" if pred.get('is_malignant', False) else "#27ae60" | |
malignant_text = "🚨 Maligno" if pred.get('is_malignant', False) else "✅ Benigno" | |
informe += f""" | |
<tr style="background: {row_color};"> | |
<td style="padding: 12px; border-bottom: 1px solid #ecf0f1; font-weight: bold;">{pred['model']}</td> | |
<td style="padding: 12px; border-bottom: 1px solid #ecf0f1;"><strong>{pred['class']}</strong></td> | |
<td style="padding: 12px; border-bottom: 1px solid #ecf0f1;">{pred['confidence']:.1%}</td> | |
<td style="padding: 12px; border-bottom: 1px solid #ecf0f1; color: {status_color};"><strong>{status_icon} {status_text}</strong></td> | |
<td style="padding: 12px; border-bottom: 1px solid #ecf0f1; color: {malignant_color};"><strong>{malignant_text}</strong></td> | |
</tr> | |
""" | |
else: | |
informe += f""" | |
<tr style="background: {row_color};"> | |
<td style="padding: 12px; border-bottom: 1px solid #ecf0f1; font-weight: bold; color: #7f8c8d;">{pred['model']}</td> | |
<td style="padding: 12px; border-bottom: 1px solid #ecf0f1; color: #e67e22;">❌ No disponible</td> | |
<td style="padding: 12px; border-bottom: 1px solid #ecf0f1;">N/A</td> | |
<td style="padding: 12px; border-bottom: 1px solid #ecf0f1; color: #e74c3c;"><strong>❌ Error</strong></td> | |
<td style="padding: 12px; border-bottom: 1px solid #ecf0f1;">N/A</td> | |
</tr> | |
""" | |
# Resultado del ensemble | |
ensemble_status_color = "#e74c3c" if ensemble_result.get('is_malignant', False) else "#27ae60" | |
ensemble_status_text = "🚨 MALIGNO" if ensemble_result.get('is_malignant', False) else "✅ BENIGNO" | |
informe += f""" | |
</tbody> | |
</table> | |
</div> | |
</div> | |
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 25px; border-radius: 12px; margin-bottom: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.2);"> | |
<h2 style="margin-top: 0; color: white; display: flex; align-items: center;"> | |
🎯 Diagnóstico Final (Consenso de {ensemble_result['num_models']} modelos) | |
</h2> | |
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;"> | |
<div> | |
<p style="font-size: 18px; margin: 8px 0;"><strong>Diagnóstico:</strong> {ensemble_result['class']}</p> | |
<p style="margin: 8px 0;"><strong>Confianza:</strong> {ensemble_result['confidence']:.1%}</p> | |
<p style="margin: 8px 0; color: {ensemble_status_color}; background: rgba(255,255,255,0.2); padding: 8px; border-radius: 5px;"><strong>Estado: {ensemble_status_text}</strong></p> | |
</div> | |
<div> | |
<p style="margin: 8px 0;"><strong>Consenso Malignidad:</strong> {ensemble_result['malignant_consensus']:.1%}</p> | |
<p style="margin: 8px 0;"><strong>Score de Riesgo:</strong> {risk_score:.2f}</p> | |
<p style="margin: 8px 0;"><strong>Modelos Activos:</strong> {ensemble_result['num_models']}/6</p> | |
</div> | |
</div> | |
</div> | |
""" | |
# Recomendación clínica | |
informe += """ | |
<div style="background: white; padding: 25px; border-radius: 12px; border-left: 6px solid #3498db; box-shadow: 0 4px 15px rgba(0,0,0,0.1);"> | |
<h2 style="color: #2c3e50; margin-top: 0; display: flex; align-items: center;"> | |
🩺 Recomendación Clínica Automatizada | |
</h2> | |
""" | |
if risk_score > 0.7: | |
informe += ''' | |
<div style="background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%); color: white; padding: 20px; border-radius: 8px; margin: 15px 0;"> | |
<h3 style="margin: 0; font-size: 18px;">🚨 DERIVACIÓN URGENTE</h3> | |
<p style="margin: 10px 0 0 0; font-size: 16px;">Contactar con oncología dermatológica en 24-48 horas</p> | |
</div>''' | |
elif risk_score > 0.5: | |
informe += ''' | |
<div style="background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%); color: white; padding: 20px; border-radius: 8px; margin: 15px 0;"> | |
<h3 style="margin: 0; font-size: 18px;">⚠️ EVALUACIÓN PRIORITARIA</h3> | |
<p style="margin: 10px 0 0 0; font-size: 16px;">Consulta dermatológica en 1-2 semanas</p> | |
</div>''' | |
elif risk_score > 0.3: | |
informe += ''' | |
<div style="background: linear-gradient(135deg, #42a5f5 0%, #2196f3 100%); color: white; padding: 20px; border-radius: 8px; margin: 15px 0;"> | |
<h3 style="margin: 0; font-size: 18px;">📋 SEGUIMIENTO PROGRAMADO</h3> | |
<p style="margin: 10px 0 0 0; font-size: 16px;">Consulta dermatológica en 4-6 semanas</p> | |
</div>''' | |
else: | |
informe += ''' | |
<div style="background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%); color: white; padding: 20px; border-radius: 8px; margin: 15px 0;"> | |
<h3 style="margin: 0; font-size: 18px;">✅ MONITOREO RUTINARIO</h3> | |
<p style="margin: 10px 0 0 0; font-size: 16px;">Seguimiento en 3-6 meses</p> | |
</div>''' | |
informe += f""" | |
<div style="margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #e67e22;"> | |
<p style="margin: 0; font-style: italic; color: #7f8c8d; font-size: 13px;"> | |
⚠️ <strong>Disclaimer Médico:</strong> Este análisis utiliza {ensemble_result['num_models']} modelos de IA como herramienta de apoyo diagnóstico. | |
El resultado NO sustituye el criterio médico profesional. Siempre consulte con un dermatólogo certificado | |
para un diagnóstico definitivo y plan de tratamiento apropiado. | |
</p> | |
</div> | |
</div> | |
</div> | |
""" | |
return informe, chart_html | |
# Interfaz Gradio mejorada | |
demo = gr.Interface( | |
fn=analizar_lesion_verificado, | |
inputs=gr.Image(type="pil", label="📷 Cargar imagen dermatoscópica o foto de lesión cutánea"), | |
outputs=[ | |
gr.HTML(label="📋 Informe Diagnóstico Completo"), | |
gr.HTML(label="📊 Análisis Visual de Resultados") | |
], | |
title="🏥 Sistema Avanzado de Detección de Cáncer de Piel - Multi-Modelo IA", | |
description=""" | |
Sistema de análisis dermatológico que utiliza múltiples modelos de IA especializados verificados: | |
• Google Derm Foundation (modelo más avanzado de Google Health) | |
• Modelos especializados en HAM10000, ISIC 2024, y detección de melanoma | |
• Ensemble inteligente con weighted voting y análisis de consenso | |
""", | |
theme=gr.themes.Soft(), | |
allow_flagging="never", | |
examples=None | |
) | |
if __name__ == "__main__": | |
print("\n🚀 Iniciando sistema de detección de cáncer de piel...") | |
print("📋 Modelos verificados y disponibles en Hugging Face:") | |
print("✅ google/derm-foundation") | |
print("✅ bsenst/skin-cancer-HAM10k") | |
print("✅ jhoppanne/SkinCancerClassifier_smote-V0") | |
print("✅ syaha/skin_cancer_detection_model") | |
print("✅ milutinNemanjic/Melanoma-detection-model") | |
print("✅ Anwarkh1/Skin_Cancer-Image_Classification") | |
print("\n🌐 Lanzando interfaz web...") | |
demo.launch(share=False) |