Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
#
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
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 |
-
|
130 |
-
raise e_vit # Propagate the ViT-specific error
|
131 |
else:
|
132 |
-
|
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 |
-
'
|
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 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
-
|
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
|
259 |
elif any(word in label for word in ['carcinoma', 'bcc', 'basal']):
|
260 |
-
predicted_idx = 1
|
261 |
elif any(word in label for word in ['keratosis', 'akiec']):
|
262 |
-
predicted_idx = 0
|
263 |
elif any(word in label for word in ['nevus', 'nv', 'benign']):
|
264 |
-
predicted_idx = 5
|
265 |
else:
|
266 |
-
predicted_idx = 2
|
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
|
272 |
|
273 |
-
num_other_classes = 6
|
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
|
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.
|
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
|
320 |
-
mapped_probs[2] += 0.05
|
321 |
-
mapped_probs = mapped_probs / np.sum(mapped_probs)
|
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 |
-
|
328 |
-
|
329 |
-
mapped_probs[
|
330 |
-
mapped_probs[1] = probabilities[1] * 0.3 # BCC
|
331 |
-
mapped_probs[0] = probabilities[1] * 0.2 # AKIEC
|
332 |
else: # Benigno
|
333 |
-
|
334 |
-
mapped_probs[
|
335 |
-
mapped_probs[
|
336 |
-
mapped_probs[
|
337 |
-
|
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
|
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 |
-
|
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 |
-
|
593 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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;"
|
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:
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
631 |
html_report += f"""
|
632 |
-
<div style="margin:
|
633 |
-
<
|
634 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
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 |
-
|
|
|
|
|
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()
|