Upload app.py
Browse files
app.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
Cephalometric Landmark Detection API
|
| 3 |
HRNet-W32 - 19 Landmarks Cefalométricos
|
| 4 |
-
Versión
|
| 5 |
"""
|
| 6 |
|
| 7 |
import os
|
|
@@ -67,18 +67,6 @@ def get_color_for_landmark(idx):
|
|
| 67 |
return group_data["color"]
|
| 68 |
return (255, 255, 255)
|
| 69 |
|
| 70 |
-
def get_confidence_indicator(confidence):
|
| 71 |
-
"""Retorna color e indicador según nivel de confianza"""
|
| 72 |
-
# Normalizar confianza (los heatmaps pueden tener valores > 1)
|
| 73 |
-
conf_pct = min(confidence * 100, 100)
|
| 74 |
-
|
| 75 |
-
if conf_pct >= 80:
|
| 76 |
-
return (80, 255, 80), "✓", conf_pct # Verde - Alta
|
| 77 |
-
elif conf_pct >= 50:
|
| 78 |
-
return (255, 255, 80), "~", conf_pct # Amarillo - Media
|
| 79 |
-
else:
|
| 80 |
-
return (255, 80, 80), "!", conf_pct # Rojo - Baja
|
| 81 |
-
|
| 82 |
# ============================================================================
|
| 83 |
# ARQUITECTURA HRNET-W32
|
| 84 |
# ============================================================================
|
|
@@ -377,7 +365,7 @@ def detect_landmarks(image):
|
|
| 377 |
"abbrev": LANDMARK_ABBREV[i],
|
| 378 |
"x": round(float(preds[0, i, 0] * scale_x), 1),
|
| 379 |
"y": round(float(preds[0, i, 1] * scale_y), 1),
|
| 380 |
-
"confidence": round(float(maxvals[0, i]),
|
| 381 |
})
|
| 382 |
|
| 383 |
return landmarks
|
|
@@ -404,21 +392,8 @@ def draw_landmarks_clean(image, landmarks, show_labels=True):
|
|
| 404 |
x, y = lm['x'], lm['y']
|
| 405 |
color = get_color_for_landmark(lm['id'])
|
| 406 |
|
| 407 |
-
#
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
# Círculo con borde según confianza
|
| 411 |
-
# Borde más grueso y de color diferente para baja confianza
|
| 412 |
-
if conf_pct < 50:
|
| 413 |
-
# Borde rojo grueso para baja confianza
|
| 414 |
-
draw.ellipse([x-radius-3, y-radius-3, x+radius+3, y+radius+3], fill=(255, 80, 80))
|
| 415 |
-
elif conf_pct < 80:
|
| 416 |
-
# Borde amarillo para confianza media
|
| 417 |
-
draw.ellipse([x-radius-2, y-radius-2, x+radius+2, y+radius+2], fill=(255, 255, 80))
|
| 418 |
-
else:
|
| 419 |
-
# Borde negro normal para alta confianza
|
| 420 |
-
draw.ellipse([x-radius-1, y-radius-1, x+radius+1, y+radius+1], fill=(0, 0, 0))
|
| 421 |
-
|
| 422 |
draw.ellipse([x-radius, y-radius, x+radius, y+radius], fill=color)
|
| 423 |
|
| 424 |
if show_labels:
|
|
@@ -494,20 +469,18 @@ def find_label_position(x, y, text, existing_positions, font, draw, img_size):
|
|
| 494 |
return (x + 12, y - 5)
|
| 495 |
|
| 496 |
def create_legend_image(landmarks):
|
| 497 |
-
"""Crea imagen con leyenda de landmarks
|
| 498 |
-
width =
|
| 499 |
-
height =
|
| 500 |
legend = Image.new('RGB', (width, height), (30, 30, 30))
|
| 501 |
draw = ImageDraw.Draw(legend)
|
| 502 |
|
| 503 |
try:
|
| 504 |
font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14)
|
| 505 |
font_text = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 11)
|
| 506 |
-
font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 9)
|
| 507 |
except:
|
| 508 |
font_title = ImageFont.load_default()
|
| 509 |
font_text = font_title
|
| 510 |
-
font_small = font_title
|
| 511 |
|
| 512 |
y_pos = 10
|
| 513 |
draw.text((10, y_pos), "LANDMARKS DETECTADOS", fill=(255, 255, 255), font=font_title)
|
|
@@ -522,42 +495,15 @@ def create_legend_image(landmarks):
|
|
| 522 |
|
| 523 |
for idx in group_data["indices"]:
|
| 524 |
lm = landmarks[idx]
|
| 525 |
-
|
| 526 |
-
conf_color, conf_indicator, conf_pct = get_confidence_indicator(conf)
|
| 527 |
-
|
| 528 |
-
# Círculo de color del grupo
|
| 529 |
draw.ellipse([15, y_pos+2, 23, y_pos+10], fill=group_data["color"])
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
draw.text((30, y_pos), coord_text, fill=(220, 220, 220), font=font_text)
|
| 534 |
-
|
| 535 |
-
# Indicador de confianza con color
|
| 536 |
-
conf_text = f"{conf_pct:.0f}%"
|
| 537 |
-
# Posicionar a la derecha
|
| 538 |
-
conf_x = width - 60
|
| 539 |
-
draw.text((conf_x, y_pos), conf_text, fill=conf_color, font=font_text)
|
| 540 |
-
|
| 541 |
-
# Indicador visual
|
| 542 |
-
indicator_x = width - 20
|
| 543 |
-
draw.text((indicator_x, y_pos), conf_indicator, fill=conf_color, font=font_text)
|
| 544 |
-
|
| 545 |
y_pos += 16
|
| 546 |
|
| 547 |
y_pos += 8
|
| 548 |
|
| 549 |
-
# Leyenda de confianza al final
|
| 550 |
-
y_pos += 5
|
| 551 |
-
draw.line([(10, y_pos), (width-10, y_pos)], fill=(100, 100, 100), width=1)
|
| 552 |
-
y_pos += 8
|
| 553 |
-
draw.text((10, y_pos), "Confianza:", fill=(200, 200, 200), font=font_small)
|
| 554 |
-
y_pos += 14
|
| 555 |
-
draw.text((15, y_pos), "✓ ≥80% Alta", fill=(80, 255, 80), font=font_small)
|
| 556 |
-
y_pos += 12
|
| 557 |
-
draw.text((15, y_pos), "~ 50-79% Media", fill=(255, 255, 80), font=font_small)
|
| 558 |
-
y_pos += 12
|
| 559 |
-
draw.text((15, y_pos), "! <50% Baja (revisar)", fill=(255, 80, 80), font=font_small)
|
| 560 |
-
|
| 561 |
return legend
|
| 562 |
|
| 563 |
def combine_images(main_image, legend):
|
|
@@ -591,27 +537,14 @@ def process_image(image, show_labels):
|
|
| 591 |
legend = create_legend_image(landmarks)
|
| 592 |
combined = combine_images(annotated, legend)
|
| 593 |
|
| 594 |
-
#
|
| 595 |
-
confidences = [lm['confidence'] for lm in landmarks]
|
| 596 |
-
avg_conf = np.mean(confidences) * 100
|
| 597 |
-
min_conf = np.min(confidences) * 100
|
| 598 |
-
low_conf_count = sum(1 for c in confidences if c * 100 < 50)
|
| 599 |
-
|
| 600 |
-
# JSON output con confianza
|
| 601 |
result = {
|
| 602 |
"num_landmarks": len(landmarks),
|
| 603 |
-
"confidence_stats": {
|
| 604 |
-
"average": round(avg_conf, 1),
|
| 605 |
-
"minimum": round(min_conf, 1),
|
| 606 |
-
"low_confidence_count": low_conf_count
|
| 607 |
-
},
|
| 608 |
"landmarks": [{
|
| 609 |
"id": lm["id"],
|
| 610 |
"name": lm["name"],
|
| 611 |
-
"abbrev": lm["abbrev"],
|
| 612 |
"x": lm["x"],
|
| 613 |
-
"y": lm["y"]
|
| 614 |
-
"confidence": round(lm["confidence"] * 100, 1)
|
| 615 |
} for lm in landmarks]
|
| 616 |
}
|
| 617 |
|
|
@@ -622,18 +555,15 @@ def process_image(image, show_labels):
|
|
| 622 |
return None, f"Error: {e}\n{traceback.format_exc()}"
|
| 623 |
|
| 624 |
print("=" * 50)
|
| 625 |
-
print("Cephalometric Landmark Detection v1.
|
| 626 |
-
print("Con indicadores de confianza")
|
| 627 |
print("=" * 50)
|
| 628 |
load_model()
|
| 629 |
|
| 630 |
-
with gr.Blocks(title="Cephalometric Landmark Detection") as demo:
|
| 631 |
gr.Markdown("""
|
| 632 |
# 🦷 Detección de Landmarks Cefalométricos
|
| 633 |
|
| 634 |
Detección automática de **19 puntos anatómicos** en radiografías cefalométricas laterales usando HRNet-W32.
|
| 635 |
-
|
| 636 |
-
**Nuevo:** Indicadores de confianza para cada punto detectado.
|
| 637 |
""")
|
| 638 |
|
| 639 |
with gr.Row():
|
|
@@ -646,7 +576,7 @@ with gr.Blocks(title="Cephalometric Landmark Detection") as demo:
|
|
| 646 |
output_image = gr.Image(label="📍 Resultado con Leyenda", height=500)
|
| 647 |
|
| 648 |
with gr.Accordion("📋 Datos JSON", open=False):
|
| 649 |
-
output_json = gr.Code(label="Coordenadas
|
| 650 |
|
| 651 |
gr.Markdown("""
|
| 652 |
---
|
|
@@ -660,12 +590,8 @@ with gr.Blocks(title="Cephalometric Landmark Detection") as demo:
|
|
| 660 |
| 🟡 | Dental | Upper Incisor (U1), Lower Incisor (L1) |
|
| 661 |
| 🩵 | Tejido Blando | Upper Lip, Lower Lip, Subnasale, Soft Tissue Pog |
|
| 662 |
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|-----------|-------------|
|
| 666 |
-
| ✓ (verde) | Alta confianza (≥80%) - Punto bien detectado |
|
| 667 |
-
| ~ (amarillo) | Confianza media (50-79%) - Verificar posición |
|
| 668 |
-
| ! (rojo) | Baja confianza (<50%) - Requiere revisión manual |
|
| 669 |
""")
|
| 670 |
|
| 671 |
detect_btn.click(fn=process_image, inputs=[input_image, show_labels], outputs=[output_image, output_json])
|
|
|
|
| 1 |
"""
|
| 2 |
Cephalometric Landmark Detection API
|
| 3 |
HRNet-W32 - 19 Landmarks Cefalométricos
|
| 4 |
+
Versión Final con Visualización Mejorada
|
| 5 |
"""
|
| 6 |
|
| 7 |
import os
|
|
|
|
| 67 |
return group_data["color"]
|
| 68 |
return (255, 255, 255)
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
# ============================================================================
|
| 71 |
# ARQUITECTURA HRNET-W32
|
| 72 |
# ============================================================================
|
|
|
|
| 365 |
"abbrev": LANDMARK_ABBREV[i],
|
| 366 |
"x": round(float(preds[0, i, 0] * scale_x), 1),
|
| 367 |
"y": round(float(preds[0, i, 1] * scale_y), 1),
|
| 368 |
+
"confidence": round(float(maxvals[0, i]), 2)
|
| 369 |
})
|
| 370 |
|
| 371 |
return landmarks
|
|
|
|
| 392 |
x, y = lm['x'], lm['y']
|
| 393 |
color = get_color_for_landmark(lm['id'])
|
| 394 |
|
| 395 |
+
# Círculo con borde negro
|
| 396 |
+
draw.ellipse([x-radius-1, y-radius-1, x+radius+1, y+radius+1], fill=(0, 0, 0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
draw.ellipse([x-radius, y-radius, x+radius, y+radius], fill=color)
|
| 398 |
|
| 399 |
if show_labels:
|
|
|
|
| 469 |
return (x + 12, y - 5)
|
| 470 |
|
| 471 |
def create_legend_image(landmarks):
|
| 472 |
+
"""Crea imagen con leyenda de landmarks"""
|
| 473 |
+
width = 280
|
| 474 |
+
height = 520
|
| 475 |
legend = Image.new('RGB', (width, height), (30, 30, 30))
|
| 476 |
draw = ImageDraw.Draw(legend)
|
| 477 |
|
| 478 |
try:
|
| 479 |
font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14)
|
| 480 |
font_text = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 11)
|
|
|
|
| 481 |
except:
|
| 482 |
font_title = ImageFont.load_default()
|
| 483 |
font_text = font_title
|
|
|
|
| 484 |
|
| 485 |
y_pos = 10
|
| 486 |
draw.text((10, y_pos), "LANDMARKS DETECTADOS", fill=(255, 255, 255), font=font_title)
|
|
|
|
| 495 |
|
| 496 |
for idx in group_data["indices"]:
|
| 497 |
lm = landmarks[idx]
|
| 498 |
+
# Círculo de color
|
|
|
|
|
|
|
|
|
|
| 499 |
draw.ellipse([15, y_pos+2, 23, y_pos+10], fill=group_data["color"])
|
| 500 |
+
# Texto
|
| 501 |
+
text = f"{lm['abbrev']}: ({lm['x']:.0f}, {lm['y']:.0f})"
|
| 502 |
+
draw.text((30, y_pos), text, fill=(220, 220, 220), font=font_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
y_pos += 16
|
| 504 |
|
| 505 |
y_pos += 8
|
| 506 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
return legend
|
| 508 |
|
| 509 |
def combine_images(main_image, legend):
|
|
|
|
| 537 |
legend = create_legend_image(landmarks)
|
| 538 |
combined = combine_images(annotated, legend)
|
| 539 |
|
| 540 |
+
# JSON output
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
result = {
|
| 542 |
"num_landmarks": len(landmarks),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
"landmarks": [{
|
| 544 |
"id": lm["id"],
|
| 545 |
"name": lm["name"],
|
|
|
|
| 546 |
"x": lm["x"],
|
| 547 |
+
"y": lm["y"]
|
|
|
|
| 548 |
} for lm in landmarks]
|
| 549 |
}
|
| 550 |
|
|
|
|
| 555 |
return None, f"Error: {e}\n{traceback.format_exc()}"
|
| 556 |
|
| 557 |
print("=" * 50)
|
| 558 |
+
print("Cephalometric Landmark Detection v1.0")
|
|
|
|
| 559 |
print("=" * 50)
|
| 560 |
load_model()
|
| 561 |
|
| 562 |
+
with gr.Blocks(title="Cephalometric Landmark Detection", theme=gr.themes.Soft()) as demo:
|
| 563 |
gr.Markdown("""
|
| 564 |
# 🦷 Detección de Landmarks Cefalométricos
|
| 565 |
|
| 566 |
Detección automática de **19 puntos anatómicos** en radiografías cefalométricas laterales usando HRNet-W32.
|
|
|
|
|
|
|
| 567 |
""")
|
| 568 |
|
| 569 |
with gr.Row():
|
|
|
|
| 576 |
output_image = gr.Image(label="📍 Resultado con Leyenda", height=500)
|
| 577 |
|
| 578 |
with gr.Accordion("📋 Datos JSON", open=False):
|
| 579 |
+
output_json = gr.Code(label="Coordenadas", language="json", lines=10)
|
| 580 |
|
| 581 |
gr.Markdown("""
|
| 582 |
---
|
|
|
|
| 590 |
| 🟡 | Dental | Upper Incisor (U1), Lower Incisor (L1) |
|
| 591 |
| 🩵 | Tejido Blando | Upper Lip, Lower Lip, Subnasale, Soft Tissue Pog |
|
| 592 |
|
| 593 |
+
---
|
| 594 |
+
> ⚠️ **Nota:** Los puntos detectados son una aproximación inicial. En pacientes en crecimiento y desarrollo o con condiciones anatómicas atípicas, se recomienda ajuste manual.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
""")
|
| 596 |
|
| 597 |
detect_btn.click(fn=process_image, inputs=[input_image, show_labels], outputs=[output_image, output_json])
|