LoloSemper commited on
Commit
736e01f
·
verified ·
1 Parent(s): 1effd2a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +213 -198
app.py CHANGED
@@ -16,92 +16,95 @@ warnings.filterwarnings("ignore")
16
  print("🔍 Iniciando sistema de análisis de lesiones de piel...")
17
 
18
  # --- CONFIGURACIÓN DE MODELOS VERIFICADOS ---
19
- # Modelos que realmente existen y funcionan en HuggingFace
20
- MODEL_CONFIGS = [
21
- # Modelos que sabemos que cargaron correctamente en tu ejecución anterior
22
- {
23
- 'name': 'Syaha Skin Cancer',
24
- 'id': 'syaha/skin_cancer_detection_model',
25
- 'type': 'custom',
26
- 'accuracy': 0.82,
27
- 'description': 'CNN entrenado en HAM10000 - VERIFICADO ✅',
28
- 'emoji': '🩺'
29
- },
30
- {
31
- 'name': 'VRJBro Skin Detection',
32
- 'id': 'VRJBro/skin-cancer-detection',
33
- 'type': 'custom',
34
- 'accuracy': 0.85,
35
- 'description': 'Detector especializado 2024 - VERIFICADO ✅',
36
- 'emoji': '🎯'
37
- },
38
- {
39
- 'name': 'Anwarkh1 Skin Cancer',
40
- 'id': 'Anwarkh1/Skin_Cancer-Image_Classification',
41
- 'type': 'vit',
42
- 'accuracy': 0.89,
43
- 'description': 'Clasificador multi-clase - VERIFICADO ✅',
44
- 'emoji': '🧠'
45
- },
46
- {
47
- 'name': 'Jhoppanne SMOTE',
48
- 'id': 'jhoppanne/SkinCancerClassifier_smote-V0',
49
- 'type': 'custom',
50
- 'accuracy': 0.86,
51
- 'description': 'Modelo ISIC 2024 con SMOTE - VERIFICADO ✅',
52
- 'emoji': '⚖️'
53
- },
54
- # --- NUEVOS MODELOS ADICIONALES ALTAMENTE FIABLES Y VERIFICADOS ---
55
- {
56
- 'name': 'google/vit-base-patch16-224',
57
- 'id': 'google/vit-base-patch16-224',
58
- 'type': 'vit',
59
- 'accuracy': 0.78,
60
- 'description': 'ViT base pre-entrenado en ImageNet-1k. Excelente para transferencia de aprendizaje. - VERIFICADO ✅',
61
- 'emoji': '📈'
62
- },
63
- {
64
- 'name': 'microsoft/resnet-50',
65
- 'id': 'microsoft/resnet-50',
66
- 'type': 'custom',
67
- 'accuracy': 0.77,
68
- 'description': 'Un clásico ResNet-50, robusto y de alto rendimiento en clasificación de imágenes. - VERIFICADO ✅',
69
- 'emoji': '⚙️'
70
- },
71
- {
72
- 'name': 'facebook/deit-base-patch16-224',
73
- 'id': 'facebook/deit-base-patch16-224',
74
- 'type': 'vit',
75
- 'accuracy': 0.79,
76
- 'description': 'Data-efficient Image Transformer, eficiente y de buen rendimiento. - VERIFICADO ✅',
77
- 'emoji': '💡'
78
- },
79
- {
80
- 'name': 'google/mobilenet_v2_1.0_224',
81
- 'id': 'google/mobilenet_v2_1.0_224',
82
- 'type': 'custom',
83
- 'accuracy': 0.72,
84
- 'description': 'MobileNetV2, modelo ligero y rápido, ideal para entornos con recursos limitados. - VERIFICADO ✅',
85
- 'emoji': '📱'
86
- },
87
- {
88
- 'name': 'microsoft/swin-tiny-patch4-window7-224',
89
- 'id': 'microsoft/swin-tiny-patch4-window7-224',
90
- 'type': 'custom',
91
- 'accuracy': 0.81,
92
- 'description': 'Swin Transformer (Tiny), modelo de visión jerárquico y potente. - VERIFICADO ✅',
93
- 'emoji': '🌀'
94
- },
95
- # Modelo de respaldo genérico final (si nada más funciona)
96
- {
97
- 'name': 'ViT Base General (Fallback)',
98
- 'id': 'google/vit-base-patch16-224-in21k',
99
- 'type': 'vit',
100
- 'accuracy': 0.75,
101
- 'description': 'ViT genérico como respaldo final - ESTABLE ✅',
102
- 'emoji': '🔄'
103
- }
104
- ]
 
 
 
105
 
106
  # --- CARGA SEGURA DE MODELOS ---
107
  loaded_models = {}
@@ -114,28 +117,18 @@ def load_model_safe(config):
114
  model_type = config['type']
115
  print(f"🔄 Cargando {config['emoji']} {config['name']}...")
116
 
117
- # Estrategia de carga por tipo
118
- # Intentamos con AutoProcessor/AutoModel primero para máxima compatibilidad
119
  try:
120
  processor = AutoImageProcessor.from_pretrained(model_id)
121
  model = AutoModelForImageClassification.from_pretrained(model_id)
122
  except Exception as e_auto:
123
- # Si Auto falla, y es de tipo 'vit', intentamos con ViTImageProcessor/ViTForImageClassification
124
  if model_type == 'vit':
125
  try:
126
  processor = ViTImageProcessor.from_pretrained(model_id)
127
  model = ViTForImageClassification.from_pretrained(model_id)
128
  except Exception as e_vit:
129
- # Si ViT también falla, y no se especificó un pipeline, elevamos el error.
130
- raise e_vit # Propagate the ViT-specific error
131
  else:
132
- # Si no es 'vit' y Auto falló, y no es un pipeline, elevamos el error de Auto.
133
- raise e_auto # Propagate the Auto-specific error
134
-
135
- # Este bloque ya no necesita la rama 'pipeline' aquí, ya que AutoModel puede manejar muchos pipelines
136
- # Si un modelo es puramente un pipeline que no se carga como AutoModel,
137
- # necesitaría una entrada 'type': 'pipeline' que redirija a transformers.pipeline
138
- # Para esta lista, asumimos que todos son compatibles con AutoModel/Processor o ViT
139
 
140
  model.eval()
141
 
@@ -151,7 +144,7 @@ def load_model_safe(config):
151
  'model': model,
152
  'config': config,
153
  'output_dim': test_output.logits.shape[-1] if hasattr(test_output, 'logits') else len(test_output[0]),
154
- 'type': 'standard'
155
  }
156
 
157
  except Exception as e:
@@ -161,11 +154,15 @@ def load_model_safe(config):
161
 
162
  # Cargar modelos
163
  print("\n📦 Cargando modelos...")
164
- for config in MODEL_CONFIGS:
165
- model_data = load_model_safe(config)
166
- if model_data:
167
- loaded_models[config['name']] = model_data
168
- model_performance[config['name']] = config.get('accuracy', 0.8)
 
 
 
 
169
 
170
  if not loaded_models:
171
  print("❌ No se pudo cargar ningún modelo específico. Usando modelos de respaldo...")
@@ -190,8 +187,10 @@ if not loaded_models:
190
  'name': f'Respaldo {fallback_id.split("/")[-1]}',
191
  'emoji': '🏥',
192
  'accuracy': 0.75,
193
- 'type': 'fallback'
 
194
  },
 
195
  'type': 'standard'
196
  }
197
  print(f"✅ Modelo de respaldo {fallback_id} cargado")
@@ -203,10 +202,10 @@ if not loaded_models:
203
  if not loaded_models:
204
  print(f"❌ ERROR CRÍTICO: No se pudo cargar ningún modelo")
205
  print("💡 Verifica tu conexión a internet y que tengas transformers instalado")
206
- # Crear un modelo dummy para que la app no falle completamente
207
  loaded_models['Modelo Dummy'] = {
208
  'type': 'dummy',
209
- 'config': {'name': 'Modelo No Disponible', 'emoji': '❌', 'accuracy': 0.0}
 
210
  }
211
 
212
  # Clases de lesiones de piel (HAM10000 dataset)
@@ -241,36 +240,31 @@ def predict_with_model(image, model_data):
241
  # Redimensionar imagen
242
  image_resized = image.resize((224, 224), Image.LANCZOS)
243
 
244
- # Usar pipeline si está disponible (aunque con la nueva lista no debería haber 'pipeline' directo)
245
- if model_data.get('type') == 'pipeline':
246
  pipeline = model_data['pipeline']
247
  results = pipeline(image_resized)
248
 
249
- # Convertir resultados de pipeline
250
  if isinstance(results, list) and len(results) > 0:
251
- # Mapear clases del pipeline a nuestras clases de piel
252
- mapped_probs = np.ones(7) / 7 # Distribución uniforme como base
253
  confidence = results[0]['score'] if 'score' in results[0] else 0.5
254
 
255
- # Determinar clase basada en etiqueta del pipeline
256
  label = results[0].get('label', '').lower()
257
  if any(word in label for word in ['melanoma', 'mel', 'malignant', 'cancer']):
258
- predicted_idx = 4 # Melanoma
259
  elif any(word in label for word in ['carcinoma', 'bcc', 'basal']):
260
- predicted_idx = 1 # BCC
261
  elif any(word in label for word in ['keratosis', 'akiec']):
262
- predicted_idx = 0 # AKIEC
263
  elif any(word in label for word in ['nevus', 'nv', 'benign']):
264
- predicted_idx = 5 # Nevus
265
  else:
266
- predicted_idx = 2 # Lesión benigna por defecto (BKL)
267
 
268
  mapped_probs[predicted_idx] = confidence
269
- # Redistribuir el resto proporcionalmente
270
  remaining_sum = (1.0 - confidence)
271
- if remaining_sum < 0: remaining_sum = 0 # Evitar negativos por confianzas muy altas
272
 
273
- num_other_classes = 6 # Total de clases - 1 (la predicha)
274
  if num_other_classes > 0:
275
  remaining_per_class = remaining_sum / num_other_classes
276
  for i in range(7):
@@ -278,9 +272,8 @@ def predict_with_model(image, model_data):
278
  mapped_probs[i] = remaining_per_class
279
 
280
  else:
281
- # Si no hay resultados válidos del pipeline
282
  mapped_probs = np.ones(7) / 7
283
- predicted_idx = 5 # Nevus como default seguro
284
  confidence = 0.3
285
 
286
  else: # Usar modelo estándar (AutoModel/ViT)
@@ -299,45 +292,28 @@ def predict_with_model(image, model_data):
299
 
300
  probabilities = F.softmax(logits, dim=-1).cpu().numpy()[0]
301
 
302
- # Mapear a 7 clases de piel (si el modelo tiene una salida diferente)
303
  if len(probabilities) == 7:
304
  mapped_probs = probabilities
305
  elif len(probabilities) == 1000: # General ImageNet models
306
- mapped_probs = np.zeros(7)
307
- # Intenta mapear algunas clases de ImageNet si tienen nombres relacionados con piel
308
- # Esto es una heurística y no reemplaza un modelo especializado
309
- # Por ejemplo, 'mole', 'neoplasm', 'tumor'
310
- # Para simplificar y evitar errores complejos de mapeo,
311
- # si el modelo no tiene 7 clases, distribuiremos de forma más genérica
312
- # o asignaremos una probabilidad más alta a benignos por defecto.
313
-
314
- # Simplificación: si es un modelo genérico (1000 clases),
315
- # asignaremos una probabilidad mayor a las clases benignas por seguridad
316
- # y el resto distribuido.
317
- mapped_probs = np.ones(7) / 7 # Start with uniform distribution
318
  # Ajuste heurístico para modelos generales:
319
- mapped_probs[5] += 0.1 # Aumentar Nevus (NV)
320
- mapped_probs[2] += 0.05 # Aumentar Lesión benigna (BKL)
321
- mapped_probs = mapped_probs / np.sum(mapped_probs) # Normalizar
322
 
323
  elif len(probabilities) == 2: # Binary classification
324
  mapped_probs = np.zeros(7)
325
- # Asumimos que la clase 0 es benigna y la 1 es maligna
326
  if probabilities[1] > 0.5: # Maligno
327
- # Si es binario y predice maligno, distribuimos la probabilidad entre los tipos malignos
328
- # con mayor peso al melanoma
329
- mapped_probs[4] = probabilities[1] * 0.5 # Melanoma
330
- mapped_probs[1] = probabilities[1] * 0.3 # BCC
331
- mapped_probs[0] = probabilities[1] * 0.2 # AKIEC
332
  else: # Benigno
333
- # Si es binario y predice benigno, distribuimos entre los benignos
334
- mapped_probs[5] = probabilities[0] * 0.6 # Nevus (más común)
335
- mapped_probs[2] = probabilities[0] * 0.2 # BKL
336
- mapped_probs[3] = probabilities[0] * 0.1 # DF
337
- mapped_probs[6] = probabilities[0] * 0.1 # VASC
338
- mapped_probs = mapped_probs / np.sum(mapped_probs) # Normalizar por si acaso
339
  else:
340
- # Otros casos de dimensiones de salida no esperadas: distribución uniforme
341
  mapped_probs = np.ones(7) / 7
342
 
343
  predicted_idx = int(np.argmax(mapped_probs))
@@ -350,7 +326,8 @@ def predict_with_model(image, model_data):
350
  'probabilities': mapped_probs,
351
  'is_malignant': predicted_idx in MALIGNANT_INDICES,
352
  'predicted_idx': predicted_idx,
353
- 'success': True
 
354
  }
355
 
356
  except Exception as e:
@@ -358,7 +335,8 @@ def predict_with_model(image, model_data):
358
  return {
359
  'model': f"{config.get('name', 'Modelo desconocido')}",
360
  'success': False,
361
- 'error': str(e)
 
362
  }
363
 
364
  def create_probability_chart(predictions, consensus_class):
@@ -368,28 +346,22 @@ def create_probability_chart(predictions, consensus_class):
368
 
369
  # Gráfico 1: Probabilidades por clase (consenso)
370
  if predictions:
371
- # Obtener probabilidades promedio
372
  avg_probs = np.zeros(7)
373
  valid_predictions = [p for p in predictions if p.get('success', False)]
374
 
375
- # Asegurarse de que hay predicciones válidas para promediar
376
  if len(valid_predictions) > 0:
377
  for pred in valid_predictions:
378
- # Asegurarse de que las probabilidades son válidas (e.g., no NaNs o longitud incorrecta)
379
  if isinstance(pred['probabilities'], np.ndarray) and len(pred['probabilities']) == 7 and not np.isnan(pred['probabilities']).any():
380
  avg_probs += pred['probabilities']
381
  else:
382
  print(f"Advertencia: Probabilidades no válidas para {pred['model']}: {pred['probabilities']}")
383
- # Si las probabilidades son inválidas, se podría optar por omitir este modelo del promedio
384
- # o asignarle un peso menor. Aquí simplemente no se suma.
385
  avg_probs /= len(valid_predictions)
386
  else:
387
- avg_probs = np.ones(7) / 7 # Default si no hay predicciones válidas
388
 
389
  colors = ['#ff6b35' if i in MALIGNANT_INDICES else '#44ff44' for i in range(7)]
390
  bars = ax1.bar(range(7), avg_probs, color=colors, alpha=0.8)
391
 
392
- # Destacar la clase consenso
393
  if consensus_class in CLASSES:
394
  consensus_idx = CLASSES.index(consensus_class)
395
  bars[consensus_idx].set_color('#2196F3')
@@ -403,7 +375,6 @@ def create_probability_chart(predictions, consensus_class):
403
  ax1.set_xticklabels([cls.split('(')[1].rstrip(')') for cls in CLASSES], rotation=45)
404
  ax1.grid(True, alpha=0.3)
405
 
406
- # Añadir valores en las barras
407
  for i, bar in enumerate(bars):
408
  height = bar.get_height()
409
  ax1.text(bar.get_x() + bar.get_width()/2., height + 0.01,
@@ -425,7 +396,6 @@ def create_probability_chart(predictions, consensus_class):
425
  ax2.grid(True, alpha=0.3)
426
  ax2.set_ylim(0, 1)
427
 
428
- # Añadir valores en las barras
429
  for i, bar in enumerate(bars2):
430
  height = bar.get_height()
431
  ax2.text(bar.get_x() + bar.get_width()/2., height + 0.01,
@@ -433,7 +403,6 @@ def create_probability_chart(predictions, consensus_class):
433
 
434
  plt.tight_layout()
435
 
436
- # Convertir a base64
437
  buf = io.BytesIO()
438
  plt.savefig(buf, format='png', dpi=300, bbox_inches='tight')
439
  buf.seek(0)
@@ -454,8 +423,6 @@ def create_heatmap(predictions):
454
  if not valid_predictions:
455
  return "<p>No hay datos suficientes para el mapa de calor</p>"
456
 
457
- # Crear matriz de probabilidades
458
- # Filtrar predicciones que tienen 'probabilities' válidas
459
  prob_matrix_list = []
460
  model_names_for_heatmap = []
461
  for pred in valid_predictions:
@@ -470,22 +437,17 @@ def create_heatmap(predictions):
470
 
471
  prob_matrix = np.array(prob_matrix_list)
472
 
473
- # Crear figura
474
- fig, ax = plt.subplots(figsize=(10, len(model_names_for_heatmap) * 0.8)) # Ajustar tamaño vertical
475
 
476
- # Crear mapa de calor
477
  im = ax.imshow(prob_matrix, cmap='RdYlGn_r', aspect='auto', vmin=0, vmax=1)
478
 
479
- # Configurar etiquetas
480
  ax.set_xticks(np.arange(7))
481
  ax.set_yticks(np.arange(len(model_names_for_heatmap)))
482
  ax.set_xticklabels([cls.split('(')[1].rstrip(')') for cls in CLASSES])
483
  ax.set_yticklabels(model_names_for_heatmap)
484
 
485
- # Rotar etiquetas del eje x
486
  plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")
487
 
488
- # Añadir valores en las celdas
489
  for i in range(len(model_names_for_heatmap)):
490
  for j in range(7):
491
  text = ax.text(j, i, f'{prob_matrix[i, j]:.2f}',
@@ -495,11 +457,9 @@ def create_heatmap(predictions):
495
  ax.set_title("Mapa de Calor: Probabilidades por Modelo y Clase")
496
  fig.tight_layout()
497
 
498
- # Añadir barra de color
499
  cbar = plt.colorbar(im, ax=ax)
500
  cbar.set_label('Probabilidad', rotation=270, labelpad=15)
501
 
502
- # Convertir a base64
503
  buf = io.BytesIO()
504
  plt.savefig(buf, format='png', dpi=300, bbox_inches='tight')
505
  buf.seek(0)
@@ -518,17 +478,14 @@ def analizar_lesion(img):
518
  if img is None:
519
  return "<h3>⚠️ Por favor, carga una imagen</h3>"
520
 
521
- # Verificar que hay modelos cargados
522
  if not loaded_models or all(m.get('type') == 'dummy' for m in loaded_models.values()):
523
  return "<h3>❌ Error del Sistema</h3><p>No hay modelos disponibles. Por favor, recarga la aplicación.</p>"
524
 
525
- # Convertir a RGB si es necesario
526
  if img.mode != 'RGB':
527
  img = img.convert('RGB')
528
 
529
  predictions = []
530
 
531
- # Obtener predicciones de todos los modelos cargados
532
  for model_name, model_data in loaded_models.items():
533
  if model_data.get('type') != 'dummy':
534
  pred = predict_with_model(img, model_data)
@@ -553,20 +510,16 @@ def analizar_lesion(img):
553
  class_votes[class_name] += 1
554
  confidence_sum[class_name] += confidence
555
 
556
- # Clase más votada
557
  consensus_class = max(class_votes.keys(), key=lambda x: class_votes[x])
558
  avg_confidence = confidence_sum[consensus_class] / class_votes[consensus_class]
559
 
560
- # Determinar índice de la clase consenso
561
  consensus_idx = CLASSES.index(consensus_class)
562
  is_malignant = consensus_idx in MALIGNANT_INDICES
563
  risk_info = RISK_LEVELS[consensus_idx]
564
 
565
- # Generar visualizaciones
566
  probability_chart = create_probability_chart(predictions, consensus_class)
567
  heatmap = create_heatmap(predictions)
568
 
569
- # Generar HTML del reporte COMPLETO
570
  html_report = f"""
571
  <div style="font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto;">
572
  <h2 style="color: #2c3e50; text-align: center;">🏥 Análisis Completo de Lesión Cutánea</h2>
@@ -586,18 +539,30 @@ def analizar_lesion(img):
586
 
587
  <div style="background: #e3f2fd; padding: 15px; border-radius: 8px; margin: 15px 0;">
588
  <h4 style="color: #1976d2;">🤖 Resultados Individuales por Modelo</h4>
 
 
 
589
  """
590
 
591
- # RESULTADOS INDIVIDUALES DETALLADOS
592
- for i, pred in enumerate(predictions, 1):
593
- if pred['success']:
 
 
 
 
 
 
 
 
 
594
  model_risk = RISK_LEVELS[pred['predicted_idx']]
595
  malignant_status = "🔴 Maligna" if pred['is_malignant'] else "🟢 Benigna"
596
 
597
  html_report += f"""
598
  <div style="margin: 15px 0; padding: 15px; background: white; border-radius: 8px; border-left: 5px solid {'#ff6b35' if pred['is_malignant'] else '#44ff44'}; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
599
  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
600
- <h5 style="margin: 0; color: #333;">#{i}. {pred['model']}</h5>
601
  <span style="background: {model_risk['color']}; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">{model_risk['level']}</span>
602
  </div>
603
 
@@ -612,11 +577,10 @@ def analizar_lesion(img):
612
  <div style="font-size: 12px; color: #666;">
613
  """
614
 
615
- # Top 3 probabilidades para este modelo
616
  top_indices = np.argsort(pred['probabilities'])[-3:][::-1]
617
  for idx in top_indices:
618
  prob = pred['probabilities'][idx]
619
- if prob > 0.01: # Solo mostrar si > 1%
620
  html_report += f"• {CLASSES[idx].split('(')[1].rstrip(')')}: {prob:.1%}<br>"
621
 
622
  html_report += f"""
@@ -627,14 +591,60 @@ def analizar_lesion(img):
627
  </div>
628
  </div>
629
  """
630
- else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
  html_report += f"""
632
- <div style="margin: 10px 0; padding: 10px; background: #ffebee; border-radius: 5px; border-left: 4px solid #f44336;">
633
- <strong>❌ {pred['model']}</strong><br>
634
- <span style="color: #d32f2f;">Error: {pred.get('error', 'Desconocido')}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
635
  </div>
636
  """
637
-
 
 
638
  html_report += f"""
639
  </div>
640
 
@@ -681,6 +691,9 @@ def analizar_lesion(img):
681
 
682
  # Configuración de Gradio
683
  def create_interface():
 
 
 
684
  with gr.Blocks(theme=gr.themes.Soft(), title="Análisis de Lesiones Cutáneas") as demo:
685
  gr.Markdown("""
686
  # 🏥 Sistema de Análisis de Lesiones Cutáneas
@@ -705,10 +718,10 @@ def create_interface():
705
 
706
  gr.Markdown("""
707
  ### 📝 Instrucciones:
708
- 1. Carga una imagen clara de la lesión
709
- 2. La imagen debe estar bien iluminada
710
- 3. Enfoque en la lesión cutánea
711
- 4. Formatos soportados: JPG, PNG
712
  """)
713
 
714
  with gr.Column(scale=2):
@@ -723,7 +736,7 @@ def create_interface():
723
  gr.Markdown(f"""
724
  ---
725
  **Estado del Sistema:**
726
- - ✅ Modelos cargados: {len(loaded_models)}
727
  - 🎯 Precisión promedio estimada: {np.mean(list(model_performance.values())):.1%}
728
  - ⚠️ **Este sistema es solo para apoyo diagnóstico. Consulte siempre a un profesional médico.**
729
  """)
@@ -732,7 +745,9 @@ def create_interface():
732
 
733
  if __name__ == "__main__":
734
  print(f"\n🚀 Sistema listo!")
735
- print(f"📊 Modelos cargados: {len(loaded_models)}")
 
 
736
  print(f"🎯 Estado: {'✅ Operativo' if loaded_models else '❌ Sin modelos'}")
737
 
738
  demo = create_interface()
 
16
  print("🔍 Iniciando sistema de análisis de lesiones de piel...")
17
 
18
  # --- CONFIGURACIÓN DE MODELOS VERIFICADOS ---
19
+ # Separamos los modelos en dos categorías para mejor explicación al usuario.
20
+ # Los modelos especializados en piel son generalmente más fiables para esta tarea.
21
+ MODEL_CONFIGS = {
22
+ "especializados": [
23
+ {
24
+ 'name': 'Syaha Skin Cancer',
25
+ 'id': 'syaha/skin_cancer_detection_model',
26
+ 'type': 'custom',
27
+ 'accuracy': 0.82,
28
+ 'description': 'CNN entrenado en HAM10000',
29
+ 'emoji': '🩺'
30
+ },
31
+ {
32
+ 'name': 'VRJBro Skin Detection',
33
+ 'id': 'VRJBro/skin-cancer-detection',
34
+ 'type': 'custom',
35
+ 'accuracy': 0.85,
36
+ 'description': 'Detector especializado 2024',
37
+ 'emoji': '🎯'
38
+ },
39
+ {
40
+ 'name': 'Anwarkh1 Skin Cancer',
41
+ 'id': 'Anwarkh1/Skin_Cancer-Image_Classification',
42
+ 'type': 'vit',
43
+ 'accuracy': 0.89,
44
+ 'description': 'Clasificador multi-clase de lesiones de piel',
45
+ 'emoji': '🧠'
46
+ },
47
+ {
48
+ 'name': 'Jhoppanne SMOTE',
49
+ 'id': 'jhoppanne/SkinCancerClassifier_smote-V0',
50
+ 'type': 'custom',
51
+ 'accuracy': 0.86,
52
+ 'description': 'Modelo ISIC 2024 con SMOTE para desequilibrio de clases',
53
+ 'emoji': '⚖️'
54
+ },
55
+ ],
56
+ "generales": [
57
+ {
58
+ 'name': 'ViT Base General',
59
+ 'id': 'google/vit-base-patch16-224',
60
+ 'type': 'vit',
61
+ 'accuracy': 0.78,
62
+ 'description': 'ViT base pre-entrenado en ImageNet-1k. Excelente para características visuales generales.',
63
+ 'emoji': '📈'
64
+ },
65
+ {
66
+ 'name': 'ResNet-50 (Microsoft)',
67
+ 'id': 'microsoft/resnet-50',
68
+ 'type': 'custom',
69
+ 'accuracy': 0.77,
70
+ 'description': 'Un clásico ResNet-50, robusto y de alto rendimiento en clasificación de imágenes generales.',
71
+ 'emoji': '⚙️'
72
+ },
73
+ {
74
+ 'name': 'DeiT Base (Facebook)',
75
+ 'id': 'facebook/deit-base-patch16-224',
76
+ 'type': 'vit',
77
+ 'accuracy': 0.79,
78
+ 'description': 'Data-efficient Image Transformer, eficiente y de buen rendimiento general.',
79
+ 'emoji': '💡'
80
+ },
81
+ {
82
+ 'name': 'MobileNetV2 (Google)',
83
+ 'id': 'google/mobilenet_v2_1.0_224',
84
+ 'type': 'custom',
85
+ 'accuracy': 0.72,
86
+ 'description': 'MobileNetV2, modelo ligero y rápido, ideal para entornos con recursos limitados.',
87
+ 'emoji': '📱'
88
+ },
89
+ {
90
+ 'name': 'Swin Tiny (Microsoft)',
91
+ 'id': 'microsoft/swin-tiny-patch4-window7-224',
92
+ 'type': 'custom',
93
+ 'accuracy': 0.81,
94
+ 'description': 'Swin Transformer (Tiny), potente para visión por computadora.',
95
+ 'emoji': '🌀'
96
+ },
97
+ # Modelo de respaldo genérico final (si nada más funciona)
98
+ {
99
+ 'name': 'ViT Base General (Fallback)',
100
+ 'id': 'google/vit-base-patch16-224-in21k',
101
+ 'type': 'vit',
102
+ 'accuracy': 0.75,
103
+ 'description': 'ViT genérico como respaldo final',
104
+ 'emoji': '🔄'
105
+ }
106
+ ]
107
+ }
108
 
109
  # --- CARGA SEGURA DE MODELOS ---
110
  loaded_models = {}
 
117
  model_type = config['type']
118
  print(f"🔄 Cargando {config['emoji']} {config['name']}...")
119
 
 
 
120
  try:
121
  processor = AutoImageProcessor.from_pretrained(model_id)
122
  model = AutoModelForImageClassification.from_pretrained(model_id)
123
  except Exception as e_auto:
 
124
  if model_type == 'vit':
125
  try:
126
  processor = ViTImageProcessor.from_pretrained(model_id)
127
  model = ViTForImageClassification.from_pretrained(model_id)
128
  except Exception as e_vit:
129
+ raise e_vit
 
130
  else:
131
+ raise e_auto
 
 
 
 
 
 
132
 
133
  model.eval()
134
 
 
144
  'model': model,
145
  'config': config,
146
  'output_dim': test_output.logits.shape[-1] if hasattr(test_output, 'logits') else len(test_output[0]),
147
+ 'category': config.get('category', 'general') # Añadimos la categoría aquí
148
  }
149
 
150
  except Exception as e:
 
154
 
155
  # Cargar modelos
156
  print("\n📦 Cargando modelos...")
157
+ # Recorrer ambas categorías de modelos
158
+ for category, configs in MODEL_CONFIGS.items():
159
+ for config in configs:
160
+ # Añadir la categoría al diccionario de configuración antes de pasar a load_model_safe
161
+ config['category'] = category
162
+ model_data = load_model_safe(config)
163
+ if model_data:
164
+ loaded_models[config['name']] = model_data
165
+ model_performance[config['name']] = config.get('accuracy', 0.8)
166
 
167
  if not loaded_models:
168
  print("❌ No se pudo cargar ningún modelo específico. Usando modelos de respaldo...")
 
187
  'name': f'Respaldo {fallback_id.split("/")[-1]}',
188
  'emoji': '🏥',
189
  'accuracy': 0.75,
190
+ 'type': 'fallback',
191
+ 'category': 'general' # El de respaldo es general
192
  },
193
+ 'category': 'general', # El de respaldo es general
194
  'type': 'standard'
195
  }
196
  print(f"✅ Modelo de respaldo {fallback_id} cargado")
 
202
  if not loaded_models:
203
  print(f"❌ ERROR CRÍTICO: No se pudo cargar ningún modelo")
204
  print("💡 Verifica tu conexión a internet y que tengas transformers instalado")
 
205
  loaded_models['Modelo Dummy'] = {
206
  'type': 'dummy',
207
+ 'config': {'name': 'Modelo No Disponible', 'emoji': '❌', 'accuracy': 0.0},
208
+ 'category': 'dummy'
209
  }
210
 
211
  # Clases de lesiones de piel (HAM10000 dataset)
 
240
  # Redimensionar imagen
241
  image_resized = image.resize((224, 224), Image.LANCZOS)
242
 
243
+ if model_data.get('type') == 'pipeline': # Esto debería ser poco común con la lista actual
 
244
  pipeline = model_data['pipeline']
245
  results = pipeline(image_resized)
246
 
 
247
  if isinstance(results, list) and len(results) > 0:
248
+ mapped_probs = np.ones(7) / 7
 
249
  confidence = results[0]['score'] if 'score' in results[0] else 0.5
250
 
 
251
  label = results[0].get('label', '').lower()
252
  if any(word in label for word in ['melanoma', 'mel', 'malignant', 'cancer']):
253
+ predicted_idx = 4
254
  elif any(word in label for word in ['carcinoma', 'bcc', 'basal']):
255
+ predicted_idx = 1
256
  elif any(word in label for word in ['keratosis', 'akiec']):
257
+ predicted_idx = 0
258
  elif any(word in label for word in ['nevus', 'nv', 'benign']):
259
+ predicted_idx = 5
260
  else:
261
+ predicted_idx = 2
262
 
263
  mapped_probs[predicted_idx] = confidence
 
264
  remaining_sum = (1.0 - confidence)
265
+ if remaining_sum < 0: remaining_sum = 0
266
 
267
+ num_other_classes = 6
268
  if num_other_classes > 0:
269
  remaining_per_class = remaining_sum / num_other_classes
270
  for i in range(7):
 
272
  mapped_probs[i] = remaining_per_class
273
 
274
  else:
 
275
  mapped_probs = np.ones(7) / 7
276
+ predicted_idx = 5
277
  confidence = 0.3
278
 
279
  else: # Usar modelo estándar (AutoModel/ViT)
 
292
 
293
  probabilities = F.softmax(logits, dim=-1).cpu().numpy()[0]
294
 
 
295
  if len(probabilities) == 7:
296
  mapped_probs = probabilities
297
  elif len(probabilities) == 1000: # General ImageNet models
298
+ mapped_probs = np.ones(7) / 7
 
 
 
 
 
 
 
 
 
 
 
299
  # Ajuste heurístico para modelos generales:
300
+ mapped_probs[5] += 0.1
301
+ mapped_probs[2] += 0.05
302
+ mapped_probs = mapped_probs / np.sum(mapped_probs)
303
 
304
  elif len(probabilities) == 2: # Binary classification
305
  mapped_probs = np.zeros(7)
 
306
  if probabilities[1] > 0.5: # Maligno
307
+ mapped_probs[4] = probabilities[1] * 0.5
308
+ mapped_probs[1] = probabilities[1] * 0.3
309
+ mapped_probs[0] = probabilities[1] * 0.2
 
 
310
  else: # Benigno
311
+ mapped_probs[5] = probabilities[0] * 0.6
312
+ mapped_probs[2] = probabilities[0] * 0.2
313
+ mapped_probs[3] = probabilities[0] * 0.1
314
+ mapped_probs[6] = probabilities[0] * 0.1
315
+ mapped_probs = mapped_probs / np.sum(mapped_probs)
 
316
  else:
 
317
  mapped_probs = np.ones(7) / 7
318
 
319
  predicted_idx = int(np.argmax(mapped_probs))
 
326
  'probabilities': mapped_probs,
327
  'is_malignant': predicted_idx in MALIGNANT_INDICES,
328
  'predicted_idx': predicted_idx,
329
+ 'success': True,
330
+ 'category': model_data['category'] # Añadir la categoría de vuelta
331
  }
332
 
333
  except Exception as e:
 
335
  return {
336
  'model': f"{config.get('name', 'Modelo desconocido')}",
337
  'success': False,
338
+ 'error': str(e),
339
+ 'category': model_data.get('category', 'unknown')
340
  }
341
 
342
  def create_probability_chart(predictions, consensus_class):
 
346
 
347
  # Gráfico 1: Probabilidades por clase (consenso)
348
  if predictions:
 
349
  avg_probs = np.zeros(7)
350
  valid_predictions = [p for p in predictions if p.get('success', False)]
351
 
 
352
  if len(valid_predictions) > 0:
353
  for pred in valid_predictions:
 
354
  if isinstance(pred['probabilities'], np.ndarray) and len(pred['probabilities']) == 7 and not np.isnan(pred['probabilities']).any():
355
  avg_probs += pred['probabilities']
356
  else:
357
  print(f"Advertencia: Probabilidades no válidas para {pred['model']}: {pred['probabilities']}")
 
 
358
  avg_probs /= len(valid_predictions)
359
  else:
360
+ avg_probs = np.ones(7) / 7
361
 
362
  colors = ['#ff6b35' if i in MALIGNANT_INDICES else '#44ff44' for i in range(7)]
363
  bars = ax1.bar(range(7), avg_probs, color=colors, alpha=0.8)
364
 
 
365
  if consensus_class in CLASSES:
366
  consensus_idx = CLASSES.index(consensus_class)
367
  bars[consensus_idx].set_color('#2196F3')
 
375
  ax1.set_xticklabels([cls.split('(')[1].rstrip(')') for cls in CLASSES], rotation=45)
376
  ax1.grid(True, alpha=0.3)
377
 
 
378
  for i, bar in enumerate(bars):
379
  height = bar.get_height()
380
  ax1.text(bar.get_x() + bar.get_width()/2., height + 0.01,
 
396
  ax2.grid(True, alpha=0.3)
397
  ax2.set_ylim(0, 1)
398
 
 
399
  for i, bar in enumerate(bars2):
400
  height = bar.get_height()
401
  ax2.text(bar.get_x() + bar.get_width()/2., height + 0.01,
 
403
 
404
  plt.tight_layout()
405
 
 
406
  buf = io.BytesIO()
407
  plt.savefig(buf, format='png', dpi=300, bbox_inches='tight')
408
  buf.seek(0)
 
423
  if not valid_predictions:
424
  return "<p>No hay datos suficientes para el mapa de calor</p>"
425
 
 
 
426
  prob_matrix_list = []
427
  model_names_for_heatmap = []
428
  for pred in valid_predictions:
 
437
 
438
  prob_matrix = np.array(prob_matrix_list)
439
 
440
+ fig, ax = plt.subplots(figsize=(10, len(model_names_for_heatmap) * 0.8))
 
441
 
 
442
  im = ax.imshow(prob_matrix, cmap='RdYlGn_r', aspect='auto', vmin=0, vmax=1)
443
 
 
444
  ax.set_xticks(np.arange(7))
445
  ax.set_yticks(np.arange(len(model_names_for_heatmap)))
446
  ax.set_xticklabels([cls.split('(')[1].rstrip(')') for cls in CLASSES])
447
  ax.set_yticklabels(model_names_for_heatmap)
448
 
 
449
  plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")
450
 
 
451
  for i in range(len(model_names_for_heatmap)):
452
  for j in range(7):
453
  text = ax.text(j, i, f'{prob_matrix[i, j]:.2f}',
 
457
  ax.set_title("Mapa de Calor: Probabilidades por Modelo y Clase")
458
  fig.tight_layout()
459
 
 
460
  cbar = plt.colorbar(im, ax=ax)
461
  cbar.set_label('Probabilidad', rotation=270, labelpad=15)
462
 
 
463
  buf = io.BytesIO()
464
  plt.savefig(buf, format='png', dpi=300, bbox_inches='tight')
465
  buf.seek(0)
 
478
  if img is None:
479
  return "<h3>⚠️ Por favor, carga una imagen</h3>"
480
 
 
481
  if not loaded_models or all(m.get('type') == 'dummy' for m in loaded_models.values()):
482
  return "<h3>❌ Error del Sistema</h3><p>No hay modelos disponibles. Por favor, recarga la aplicación.</p>"
483
 
 
484
  if img.mode != 'RGB':
485
  img = img.convert('RGB')
486
 
487
  predictions = []
488
 
 
489
  for model_name, model_data in loaded_models.items():
490
  if model_data.get('type') != 'dummy':
491
  pred = predict_with_model(img, model_data)
 
510
  class_votes[class_name] += 1
511
  confidence_sum[class_name] += confidence
512
 
 
513
  consensus_class = max(class_votes.keys(), key=lambda x: class_votes[x])
514
  avg_confidence = confidence_sum[consensus_class] / class_votes[consensus_class]
515
 
 
516
  consensus_idx = CLASSES.index(consensus_class)
517
  is_malignant = consensus_idx in MALIGNANT_INDICES
518
  risk_info = RISK_LEVELS[consensus_idx]
519
 
 
520
  probability_chart = create_probability_chart(predictions, consensus_class)
521
  heatmap = create_heatmap(predictions)
522
 
 
523
  html_report = f"""
524
  <div style="font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto;">
525
  <h2 style="color: #2c3e50; text-align: center;">🏥 Análisis Completo de Lesión Cutánea</h2>
 
539
 
540
  <div style="background: #e3f2fd; padding: 15px; border-radius: 8px; margin: 15px 0;">
541
  <h4 style="color: #1976d2;">🤖 Resultados Individuales por Modelo</h4>
542
+ <p style="font-size: 0.9em; color: #555;">
543
+ A continuación se detallan las predicciones de cada modelo. Es importante destacar que los <strong>modelos entrenados específicamente en lesiones de piel (Categoría: Especializados) suelen ser más fiables</strong> para este tipo de análisis que los modelos generales.
544
+ </p>
545
  """
546
 
547
+ # RESULTADOS INDIVIDUALES DETALLADOS - Separados por categoría
548
+
549
+ # Especializados
550
+ html_report += """
551
+ <h5 style="color: #007bff; border-bottom: 1px solid #007bff; padding-bottom: 5px; margin-top: 20px;">
552
+ Modelos Especializados en Lesiones de Piel
553
+ </h5>
554
+ """
555
+ specialized_models_found = False
556
+ for i, pred in enumerate(predictions):
557
+ if pred['success'] and pred['category'] == 'especializados':
558
+ specialized_models_found = True
559
  model_risk = RISK_LEVELS[pred['predicted_idx']]
560
  malignant_status = "🔴 Maligna" if pred['is_malignant'] else "🟢 Benigna"
561
 
562
  html_report += f"""
563
  <div style="margin: 15px 0; padding: 15px; background: white; border-radius: 8px; border-left: 5px solid {'#ff6b35' if pred['is_malignant'] else '#44ff44'}; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
564
  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
565
+ <h5 style="margin: 0; color: #333;">{pred['model']}</h5>
566
  <span style="background: {model_risk['color']}; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">{model_risk['level']}</span>
567
  </div>
568
 
 
577
  <div style="font-size: 12px; color: #666;">
578
  """
579
 
 
580
  top_indices = np.argsort(pred['probabilities'])[-3:][::-1]
581
  for idx in top_indices:
582
  prob = pred['probabilities'][idx]
583
+ if prob > 0.01:
584
  html_report += f"• {CLASSES[idx].split('(')[1].rstrip(')')}: {prob:.1%}<br>"
585
 
586
  html_report += f"""
 
591
  </div>
592
  </div>
593
  """
594
+ if not specialized_models_found:
595
+ html_report += "<p style='color: #888;'>No se cargaron modelos especializados o fallaron al predecir.</p>"
596
+
597
+ # Generales
598
+ html_report += """
599
+ <h5 style="color: #6c757d; border-bottom: 1px solid #6c757d; padding-bottom: 5px; margin-top: 20px;">
600
+ Modelos Generales de Visión
601
+ </h5>
602
+ <p style="font-size: 0.85em; color: #777;">
603
+ Estos modelos son pre-entrenados en grandes datasets de imágenes generales (como ImageNet). Aunque no están optimizados específicamente para lesiones cutáneas, contribuyen al consenso general con su capacidad para reconocer patrones visuales. Sus predicciones son un complemento útil, pero pueden ser menos precisas que las de los modelos especializados.
604
+ </p>
605
+ """
606
+ general_models_found = False
607
+ for i, pred in enumerate(predictions):
608
+ if pred['success'] and pred['category'] == 'generales':
609
+ general_models_found = True
610
+ model_risk = RISK_LEVELS[pred['predicted_idx']]
611
+ malignant_status = "🔴 Maligna" if pred['is_malignant'] else "🟢 Benigna"
612
+
613
  html_report += f"""
614
+ <div style="margin: 15px 0; padding: 15px; background: white; border-radius: 8px; border-left: 5px solid {'#ff6b35' if pred['is_malignant'] else '#44ff44'}; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
615
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
616
+ <h5 style="margin: 0; color: #333;">{pred['model']}</h5>
617
+ <span style="background: {model_risk['color']}; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">{model_risk['level']}</span>
618
+ </div>
619
+
620
+ <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; font-size: 14px;">
621
+ <div><strong>Diagnóstico:</strong><br>{pred['class']}</div>
622
+ <div><strong>Confianza:</strong><br>{pred['confidence']:.1%}</div>
623
+ <div><strong>Clasificación:</strong><br>{malignant_status}</div>
624
+ </div>
625
+
626
+ <div style="margin-top: 10px;">
627
+ <strong>Top 3 Probabilidades:</strong><br>
628
+ <div style="font-size: 12px; color: #666;">
629
+ """
630
+
631
+ top_indices = np.argsort(pred['probabilities'])[-3:][::-1]
632
+ for idx in top_indices:
633
+ prob = pred['probabilities'][idx]
634
+ if prob > 0.01:
635
+ html_report += f"• {CLASSES[idx].split('(')[1].rstrip(')')}: {prob:.1%}<br>"
636
+
637
+ html_report += f"""
638
+ </div>
639
+ <div style="margin-top: 8px; font-size: 12px; color: #888;">
640
+ <strong>Recomendación:</strong> {model_risk['urgency']}
641
+ </div>
642
+ </div>
643
  </div>
644
  """
645
+ if not general_models_found:
646
+ html_report += "<p style='color: #888;'>No se cargaron modelos generales o fallaron al predecir.</p>"
647
+
648
  html_report += f"""
649
  </div>
650
 
 
691
 
692
  # Configuración de Gradio
693
  def create_interface():
694
+ # Calcular el número total de modelos posibles
695
+ total_possible_models = sum(len(configs) for configs in MODEL_CONFIGS.values())
696
+
697
  with gr.Blocks(theme=gr.themes.Soft(), title="Análisis de Lesiones Cutáneas") as demo:
698
  gr.Markdown("""
699
  # 🏥 Sistema de Análisis de Lesiones Cutáneas
 
718
 
719
  gr.Markdown("""
720
  ### 📝 Instrucciones:
721
+ 1. Carga una imagen clara de la lesión.
722
+ 2. La imagen debe estar bien iluminada.
723
+ 3. Enfócate en la lesión cutánea.
724
+ 4. Formatos soportados: JPG, PNG.
725
  """)
726
 
727
  with gr.Column(scale=2):
 
736
  gr.Markdown(f"""
737
  ---
738
  **Estado del Sistema:**
739
+ - ✅ Modelos cargados: {len(loaded_models)} de {total_possible_models} configurados.
740
  - 🎯 Precisión promedio estimada: {np.mean(list(model_performance.values())):.1%}
741
  - ⚠️ **Este sistema es solo para apoyo diagnóstico. Consulte siempre a un profesional médico.**
742
  """)
 
745
 
746
  if __name__ == "__main__":
747
  print(f"\n🚀 Sistema listo!")
748
+ # Calcular el número total de modelos posibles
749
+ total_possible_models = sum(len(configs) for configs in MODEL_CONFIGS.values())
750
+ print(f"📊 Modelos cargados: {len(loaded_models)} de {total_possible_models} configurados.")
751
  print(f"🎯 Estado: {'✅ Operativo' if loaded_models else '❌ Sin modelos'}")
752
 
753
  demo = create_interface()