Add interactive slice viewer: scroll through slices with slider and navigation buttons
Browse files
app.py
CHANGED
|
@@ -409,6 +409,88 @@ def process_sequence(image_files, prompt_text, modality, window_type):
|
|
| 409 |
else:
|
| 410 |
return [], "❌ No images were processed successfully. Check console for error details."
|
| 411 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
with gr.Blocks() as demo:
|
| 413 |
gr.Markdown("# 🏥 NeuroSAM 3: Medical Image Segmentation")
|
| 414 |
|
|
@@ -488,15 +570,16 @@ with gr.Blocks() as demo:
|
|
| 488 |
interactive=False
|
| 489 |
)
|
| 490 |
|
| 491 |
-
with gr.Tab("
|
| 492 |
-
gr.Markdown("**
|
| 493 |
with gr.Row():
|
| 494 |
with gr.Column():
|
| 495 |
files_input = gr.File(
|
| 496 |
-
label="Upload Multiple Images (Select multiple files)",
|
| 497 |
file_types=[".dcm", ".png", ".jpg", ".jpeg"],
|
| 498 |
file_count="multiple",
|
| 499 |
-
type="filepath"
|
|
|
|
| 500 |
)
|
| 501 |
|
| 502 |
text_input_batch = gr.Textbox(
|
|
@@ -520,11 +603,81 @@ with gr.Blocks() as demo:
|
|
| 520 |
info="CT windowing preset (ignored for MRI)"
|
| 521 |
)
|
| 522 |
|
| 523 |
-
submit_batch_btn = gr.Button("Process
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
|
| 525 |
with gr.Column():
|
| 526 |
gallery_output = gr.Gallery(
|
| 527 |
-
label="Segmentation
|
| 528 |
show_label=True,
|
| 529 |
elem_id="gallery",
|
| 530 |
columns=2,
|
|
@@ -532,12 +685,10 @@ with gr.Blocks() as demo:
|
|
| 532 |
height="auto"
|
| 533 |
)
|
| 534 |
|
| 535 |
-
gr.
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
interactive=False,
|
| 540 |
-
lines=5
|
| 541 |
)
|
| 542 |
|
| 543 |
with gr.Tab("Compare with Ground Truth"):
|
|
@@ -625,11 +776,54 @@ with gr.Blocks() as demo:
|
|
| 625 |
outputs=[image_output, status_text]
|
| 626 |
)
|
| 627 |
|
| 628 |
-
#
|
| 629 |
submit_batch_btn.click(
|
| 630 |
-
fn=
|
| 631 |
inputs=[files_input, text_input_batch, modality_dropdown_batch, window_dropdown_batch],
|
| 632 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
)
|
| 634 |
|
| 635 |
# Ground truth comparison
|
|
|
|
| 409 |
else:
|
| 410 |
return [], "❌ No images were processed successfully. Check console for error details."
|
| 411 |
|
| 412 |
+
# Store processed results for interactive viewer
|
| 413 |
+
processed_results_cache = {}
|
| 414 |
+
|
| 415 |
+
def process_slices_for_viewer(image_files, prompt_text, modality, window_type):
|
| 416 |
+
"""Process all slices and cache results for interactive viewing."""
|
| 417 |
+
if model is None or processor is None:
|
| 418 |
+
return None, 0, "❌ Error: Model not loaded.", "No slices loaded"
|
| 419 |
+
|
| 420 |
+
if not image_files:
|
| 421 |
+
return None, 0, "⚠️ Please upload medical image files.", "No slices loaded"
|
| 422 |
+
|
| 423 |
+
# Handle single file or list of files
|
| 424 |
+
if isinstance(image_files, str):
|
| 425 |
+
image_files = [image_files]
|
| 426 |
+
|
| 427 |
+
# Filter out None files
|
| 428 |
+
image_files = [f for f in image_files if f is not None]
|
| 429 |
+
|
| 430 |
+
if not image_files:
|
| 431 |
+
return None, 0, "⚠️ No valid files uploaded.", "No slices loaded"
|
| 432 |
+
|
| 433 |
+
results = []
|
| 434 |
+
status_messages = []
|
| 435 |
+
|
| 436 |
+
for idx, image_file in enumerate(image_files):
|
| 437 |
+
status_msg = f"Processing slice {idx + 1}/{len(image_files)}..."
|
| 438 |
+
status_messages.append(status_msg)
|
| 439 |
+
|
| 440 |
+
result = process_medical_image(image_file, prompt_text, modality, window_type)
|
| 441 |
+
|
| 442 |
+
if result:
|
| 443 |
+
results.append(result)
|
| 444 |
+
status_messages.append(f"✅ Slice {idx + 1} processed")
|
| 445 |
+
else:
|
| 446 |
+
status_messages.append(f"❌ Failed to process slice {idx + 1}")
|
| 447 |
+
|
| 448 |
+
if results:
|
| 449 |
+
# Cache results with a unique key
|
| 450 |
+
cache_key = f"{len(image_files)}_{prompt_text}_{modality}"
|
| 451 |
+
processed_results_cache[cache_key] = results
|
| 452 |
+
|
| 453 |
+
max_slices = len(results) - 1
|
| 454 |
+
status = f"✅ Processed {len(results)}/{len(image_files)} slices!\nUse slider or buttons to navigate."
|
| 455 |
+
slice_info = f"Slice 1/{len(results)}"
|
| 456 |
+
|
| 457 |
+
return results[0], max_slices, status, slice_info
|
| 458 |
+
else:
|
| 459 |
+
return None, 0, "❌ No slices were processed successfully.", "No slices loaded"
|
| 460 |
+
|
| 461 |
+
def navigate_slice(slice_idx, image_files, prompt_text, modality, window_type):
|
| 462 |
+
"""Navigate to a specific slice in the sequence."""
|
| 463 |
+
if not image_files:
|
| 464 |
+
return None, "No slices loaded"
|
| 465 |
+
|
| 466 |
+
# Handle single file or list of files
|
| 467 |
+
if isinstance(image_files, str):
|
| 468 |
+
image_files = [image_files]
|
| 469 |
+
|
| 470 |
+
# Filter out None files
|
| 471 |
+
image_files = [f for f in image_files if f is not None]
|
| 472 |
+
|
| 473 |
+
if not image_files:
|
| 474 |
+
return None, "No slices loaded"
|
| 475 |
+
|
| 476 |
+
slice_idx = int(slice_idx)
|
| 477 |
+
cache_key = f"{len(image_files)}_{prompt_text}_{modality}"
|
| 478 |
+
|
| 479 |
+
if cache_key in processed_results_cache:
|
| 480 |
+
results = processed_results_cache[cache_key]
|
| 481 |
+
if 0 <= slice_idx < len(results):
|
| 482 |
+
slice_info = f"Slice {slice_idx + 1}/{len(results)}"
|
| 483 |
+
return results[slice_idx], slice_info
|
| 484 |
+
|
| 485 |
+
# If not cached, process on the fly (fallback)
|
| 486 |
+
if 0 <= slice_idx < len(image_files):
|
| 487 |
+
result = process_medical_image(image_files[slice_idx], prompt_text, modality, window_type)
|
| 488 |
+
if result:
|
| 489 |
+
slice_info = f"Slice {slice_idx + 1}/{len(image_files)}"
|
| 490 |
+
return result, slice_info
|
| 491 |
+
|
| 492 |
+
return None, f"Invalid slice index: {slice_idx}"
|
| 493 |
+
|
| 494 |
with gr.Blocks() as demo:
|
| 495 |
gr.Markdown("# 🏥 NeuroSAM 3: Medical Image Segmentation")
|
| 496 |
|
|
|
|
| 570 |
interactive=False
|
| 571 |
)
|
| 572 |
|
| 573 |
+
with gr.Tab("Interactive Slice Viewer"):
|
| 574 |
+
gr.Markdown("**Scroll through multiple slices/images from the same subject interactively**")
|
| 575 |
with gr.Row():
|
| 576 |
with gr.Column():
|
| 577 |
files_input = gr.File(
|
| 578 |
+
label="Upload Multiple Images/Slices (Select multiple files)",
|
| 579 |
file_types=[".dcm", ".png", ".jpg", ".jpeg"],
|
| 580 |
file_count="multiple",
|
| 581 |
+
type="filepath",
|
| 582 |
+
info="Upload multiple slices from the same subject (e.g., axial MRI slices)"
|
| 583 |
)
|
| 584 |
|
| 585 |
text_input_batch = gr.Textbox(
|
|
|
|
| 603 |
info="CT windowing preset (ignored for MRI)"
|
| 604 |
)
|
| 605 |
|
| 606 |
+
submit_batch_btn = gr.Button("Process All Slices", variant="primary", size="lg")
|
| 607 |
+
|
| 608 |
+
gr.Markdown("---")
|
| 609 |
+
gr.Markdown("### 🎛️ Slice Navigator")
|
| 610 |
+
slice_slider = gr.Slider(
|
| 611 |
+
minimum=0,
|
| 612 |
+
maximum=0,
|
| 613 |
+
step=1,
|
| 614 |
+
value=0,
|
| 615 |
+
label="Slice Number",
|
| 616 |
+
info="Use slider or arrow keys to navigate through slices",
|
| 617 |
+
interactive=False
|
| 618 |
+
)
|
| 619 |
+
|
| 620 |
+
with gr.Row():
|
| 621 |
+
prev_btn = gr.Button("⬆️ Previous Slice", size="sm")
|
| 622 |
+
next_btn = gr.Button("⬇️ Next Slice", size="sm")
|
| 623 |
+
auto_play_btn = gr.Button("▶️ Auto-play", size="sm")
|
| 624 |
+
|
| 625 |
+
with gr.Column():
|
| 626 |
+
current_slice_output = gr.Image(
|
| 627 |
+
label="Current Slice Segmentation",
|
| 628 |
+
type="filepath",
|
| 629 |
+
height=600
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
gr.Markdown("### Slice Info")
|
| 633 |
+
slice_info_text = gr.Textbox(
|
| 634 |
+
label="Current Slice",
|
| 635 |
+
value="No slices loaded",
|
| 636 |
+
interactive=False
|
| 637 |
+
)
|
| 638 |
+
|
| 639 |
+
gr.Markdown("### Status")
|
| 640 |
+
status_batch_text = gr.Textbox(
|
| 641 |
+
label="Processing Status",
|
| 642 |
+
value="Ready. Upload multiple medical image files to process a sequence.",
|
| 643 |
+
interactive=False,
|
| 644 |
+
lines=4
|
| 645 |
+
)
|
| 646 |
+
|
| 647 |
+
with gr.Tab("Gallery View"):
|
| 648 |
+
gr.Markdown("**View all segmentations in a gallery grid**")
|
| 649 |
+
with gr.Row():
|
| 650 |
+
with gr.Column():
|
| 651 |
+
files_input_gallery = gr.File(
|
| 652 |
+
label="Upload Multiple Images (Select multiple files)",
|
| 653 |
+
file_types=[".dcm", ".png", ".jpg", ".jpeg"],
|
| 654 |
+
file_count="multiple",
|
| 655 |
+
type="filepath"
|
| 656 |
+
)
|
| 657 |
+
|
| 658 |
+
text_input_gallery = gr.Textbox(
|
| 659 |
+
label="Text Prompt",
|
| 660 |
+
value="brain",
|
| 661 |
+
placeholder="e.g. brain, tumor, skull, eyes"
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
+
with gr.Row():
|
| 665 |
+
modality_dropdown_gallery = gr.Dropdown(
|
| 666 |
+
["CT", "MRI"],
|
| 667 |
+
label="Modality",
|
| 668 |
+
value="MRI"
|
| 669 |
+
)
|
| 670 |
+
window_dropdown_gallery = gr.Dropdown(
|
| 671 |
+
["Brain (Grey Matter)", "Bone (Skull)", "Soft Tissue (Face)"],
|
| 672 |
+
label="Windowing Strategy (CT only)",
|
| 673 |
+
value="Brain (Grey Matter)"
|
| 674 |
+
)
|
| 675 |
+
|
| 676 |
+
submit_gallery_btn = gr.Button("Process & Show Gallery", variant="primary", size="lg")
|
| 677 |
|
| 678 |
with gr.Column():
|
| 679 |
gallery_output = gr.Gallery(
|
| 680 |
+
label="Segmentation Gallery",
|
| 681 |
show_label=True,
|
| 682 |
elem_id="gallery",
|
| 683 |
columns=2,
|
|
|
|
| 685 |
height="auto"
|
| 686 |
)
|
| 687 |
|
| 688 |
+
status_gallery_text = gr.Textbox(
|
| 689 |
+
label="Status",
|
| 690 |
+
value="Ready. Upload multiple images to view in gallery.",
|
| 691 |
+
interactive=False
|
|
|
|
|
|
|
| 692 |
)
|
| 693 |
|
| 694 |
with gr.Tab("Compare with Ground Truth"):
|
|
|
|
| 776 |
outputs=[image_output, status_text]
|
| 777 |
)
|
| 778 |
|
| 779 |
+
# Interactive slice viewer
|
| 780 |
submit_batch_btn.click(
|
| 781 |
+
fn=process_slices_for_viewer,
|
| 782 |
inputs=[files_input, text_input_batch, modality_dropdown_batch, window_dropdown_batch],
|
| 783 |
+
outputs=[current_slice_output, slice_slider, status_batch_text, slice_info_text]
|
| 784 |
+
).then(
|
| 785 |
+
lambda max_val: gr.Slider(maximum=max_val, interactive=True),
|
| 786 |
+
inputs=[slice_slider],
|
| 787 |
+
outputs=[slice_slider]
|
| 788 |
+
)
|
| 789 |
+
|
| 790 |
+
def update_slice(slice_num, files, prompt, mod, window):
|
| 791 |
+
result, info = navigate_slice(int(slice_num), files, prompt, mod, window)
|
| 792 |
+
return result, info
|
| 793 |
+
|
| 794 |
+
slice_slider.change(
|
| 795 |
+
fn=update_slice,
|
| 796 |
+
inputs=[slice_slider, files_input, text_input_batch, modality_dropdown_batch, window_dropdown_batch],
|
| 797 |
+
outputs=[current_slice_output, slice_info_text]
|
| 798 |
+
)
|
| 799 |
+
|
| 800 |
+
def prev_slice(current, files, prompt, mod, window):
|
| 801 |
+
new_val = max(0, current - 1)
|
| 802 |
+
result, info = navigate_slice(new_val, files, prompt, mod, window)
|
| 803 |
+
return new_val, result, info
|
| 804 |
+
|
| 805 |
+
def next_slice(current, max_val, files, prompt, mod, window):
|
| 806 |
+
new_val = min(max_val, current + 1)
|
| 807 |
+
result, info = navigate_slice(new_val, files, prompt, mod, window)
|
| 808 |
+
return new_val, result, info
|
| 809 |
+
|
| 810 |
+
prev_btn.click(
|
| 811 |
+
fn=prev_slice,
|
| 812 |
+
inputs=[slice_slider, files_input, text_input_batch, modality_dropdown_batch, window_dropdown_batch],
|
| 813 |
+
outputs=[slice_slider, current_slice_output, slice_info_text]
|
| 814 |
+
)
|
| 815 |
+
|
| 816 |
+
next_btn.click(
|
| 817 |
+
fn=next_slice,
|
| 818 |
+
inputs=[slice_slider, slice_slider, files_input, text_input_batch, modality_dropdown_batch, window_dropdown_batch],
|
| 819 |
+
outputs=[slice_slider, current_slice_output, slice_info_text]
|
| 820 |
+
)
|
| 821 |
+
|
| 822 |
+
# Gallery view
|
| 823 |
+
submit_gallery_btn.click(
|
| 824 |
+
fn=process_sequence,
|
| 825 |
+
inputs=[files_input_gallery, text_input_gallery, modality_dropdown_gallery, window_dropdown_gallery],
|
| 826 |
+
outputs=[gallery_output, status_gallery_text]
|
| 827 |
)
|
| 828 |
|
| 829 |
# Ground truth comparison
|