Update app.py
Browse files
app.py
CHANGED
|
@@ -6,12 +6,22 @@ import uuid
|
|
| 6 |
import shutil
|
| 7 |
from pathlib import Path
|
| 8 |
from typing import Tuple, Dict, List, Optional
|
|
|
|
| 9 |
import gradio as gr
|
| 10 |
import SimpleITK as sitk
|
| 11 |
from huggingface_hub import hf_hub_download
|
|
|
|
| 12 |
import spaces
|
| 13 |
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
# =========================
|
| 16 |
# App config
|
| 17 |
# =========================
|
|
@@ -50,6 +60,12 @@ def ensure_dirs() -> None:
|
|
| 50 |
MODEL_ROOT.mkdir(parents=True, exist_ok=True)
|
| 51 |
BIN_ROOT.mkdir(parents=True, exist_ok=True)
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
def is_zip(path: Path) -> bool:
|
| 55 |
return path.suffix.lower() == ".zip"
|
|
@@ -62,7 +78,7 @@ def get_dcm2niix_bin() -> Optional[str]:
|
|
| 62 |
return shutil.which("dcm2niix")
|
| 63 |
|
| 64 |
|
| 65 |
-
# ---- SynthStrip (NiPreps) ----
|
| 66 |
def ensure_synthstrip_available() -> None:
|
| 67 |
"""
|
| 68 |
Ensure SynthStrip from NiPreps is available, either:
|
|
@@ -284,7 +300,6 @@ def register_rigid_affine(prev_stripped: Path, new_stripped: Path, reg_dir: Path
|
|
| 284 |
@spaces.GPU(duration=300)
|
| 285 |
def run_flames_single(input_nii: Path, out_mask_path: Path, device: str = "cuda") -> Path:
|
| 286 |
"""Run FLAMeS (nnUNetv2) on a single input NIfTI and write a mask. Uses shared MODEL_ROOT."""
|
| 287 |
-
# Ensure file is accessible
|
| 288 |
with (Path(input_nii).open("rb")):
|
| 289 |
pass
|
| 290 |
import tempfile
|
|
@@ -493,6 +508,7 @@ def _redact_paths(s: str) -> str:
|
|
| 493 |
return s
|
| 494 |
|
| 495 |
|
|
|
|
| 496 |
def run_pipeline(file1, file2, dilate_prev_radius_vox=1, min_lesion_vol_ml=0.01):
|
| 497 |
"""
|
| 498 |
file1: previous (baseline) FLAIR (.nii/.nii.gz or DICOM .zip)
|
|
@@ -539,7 +555,7 @@ def run_pipeline(file1, file2, dilate_prev_radius_vox=1, min_lesion_vol_ml=0.01)
|
|
| 539 |
registered_path, _, _ = register_rigid_affine(prev_stripped, new_stripped, reg_dir)
|
| 540 |
registered_path = registered_path.resolve()
|
| 541 |
|
| 542 |
-
# FLAMeS segmentation
|
| 543 |
seg_dir = job_dir / SEG_DIR; seg_dir.mkdir(parents=True, exist_ok=True)
|
| 544 |
prev_mask_flames = seg_dir / "prev_flames_mask.nii.gz"
|
| 545 |
new_mask_flames = seg_dir / "new_in_prev_space_flames_mask.nii.gz"
|
|
@@ -610,7 +626,7 @@ def run_pipeline(file1, file2, dilate_prev_radius_vox=1, min_lesion_vol_ml=0.01)
|
|
| 610 |
|
| 611 |
|
| 612 |
# =========================
|
| 613 |
-
# Gradio UI (consistent theme)
|
| 614 |
# =========================
|
| 615 |
with gr.Blocks(
|
| 616 |
title=APP_NAME,
|
|
@@ -629,6 +645,7 @@ with gr.Blocks(
|
|
| 629 |
-webkit-background-clip: text;
|
| 630 |
-webkit-text-fill-color: transparent;
|
| 631 |
}
|
|
|
|
| 632 |
/* ----- Run pipeline button ----- */
|
| 633 |
#run_btn {
|
| 634 |
display: block;
|
|
@@ -644,12 +661,14 @@ with gr.Blocks(
|
|
| 644 |
transition: all 0.25s ease;
|
| 645 |
text-transform: none !important;
|
| 646 |
}
|
|
|
|
| 647 |
#run_btn:hover {
|
| 648 |
background: linear-gradient(90deg, #3da0df, #64eec8);
|
| 649 |
transform: translateY(-1px);
|
| 650 |
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.18);
|
| 651 |
color: #000
|
| 652 |
}
|
|
|
|
| 653 |
/* ----- Status box ----- */
|
| 654 |
#status_box {
|
| 655 |
overflow: visible !important;
|
|
@@ -663,6 +682,7 @@ with gr.Blocks(
|
|
| 663 |
font-size: 16px;
|
| 664 |
line-height: 1.45;
|
| 665 |
}
|
|
|
|
| 666 |
/* ----- Info & reference sections ----- */
|
| 667 |
.info-section {
|
| 668 |
font-size: 18px;
|
|
@@ -675,6 +695,7 @@ with gr.Blocks(
|
|
| 675 |
border: 1px solid var(--border-color-primary);
|
| 676 |
box-shadow: 0 4px 14px rgba(0,0,0,0.06);
|
| 677 |
}
|
|
|
|
| 678 |
.info-section h3 {
|
| 679 |
margin-top: 0;
|
| 680 |
margin-bottom: 12px;
|
|
@@ -683,28 +704,34 @@ with gr.Blocks(
|
|
| 683 |
color: #4cafef;
|
| 684 |
letter-spacing: -0.3px;
|
| 685 |
}
|
|
|
|
| 686 |
.info-section p, .info-section li {
|
| 687 |
color: var(--body-text-color);
|
| 688 |
}
|
|
|
|
| 689 |
.info-section ul {
|
| 690 |
margin-top: 6px;
|
| 691 |
margin-bottom: 6px;
|
| 692 |
padding-left: 24px;
|
| 693 |
list-style-type: disc;
|
| 694 |
}
|
|
|
|
| 695 |
.info-section code {
|
| 696 |
background: var(--background-secondary);
|
| 697 |
padding: 2px 5px;
|
| 698 |
border-radius: 5px;
|
| 699 |
font-size: 90%;
|
| 700 |
}
|
|
|
|
| 701 |
.info-section a {
|
| 702 |
color: #4cafef;
|
| 703 |
text-decoration: none;
|
| 704 |
}
|
|
|
|
| 705 |
.info-section a:hover {
|
| 706 |
text-decoration: underline;
|
| 707 |
}
|
|
|
|
| 708 |
/* ----- Textual report styling ----- */
|
| 709 |
#report {
|
| 710 |
font-size: 20px !important;
|
|
@@ -718,6 +745,7 @@ with gr.Blocks(
|
|
| 718 |
margin: 0 auto 34px auto;
|
| 719 |
box-shadow: 0 6px 18px rgba(0,0,0,0.08);
|
| 720 |
}
|
|
|
|
| 721 |
#report h3 {
|
| 722 |
margin-top: 0;
|
| 723 |
margin-bottom: 16px;
|
|
@@ -727,21 +755,25 @@ with gr.Blocks(
|
|
| 727 |
letter-spacing: -0.3px;
|
| 728 |
text-align: left;
|
| 729 |
}
|
|
|
|
| 730 |
#report ul {
|
| 731 |
margin: 10px 0 0 22px;
|
| 732 |
padding: 0;
|
| 733 |
list-style-type: disc;
|
| 734 |
}
|
|
|
|
| 735 |
#report li {
|
| 736 |
margin-bottom: 10px;
|
| 737 |
font-size: 20px;
|
| 738 |
line-height: 1.8;
|
| 739 |
}
|
|
|
|
| 740 |
#report strong {
|
| 741 |
color: #4cafef;
|
| 742 |
font-weight: 600;
|
| 743 |
font-size: 20px;
|
| 744 |
}
|
|
|
|
| 745 |
#report .footnote {
|
| 746 |
font-size: 16px;
|
| 747 |
color: #999;
|
|
@@ -758,14 +790,18 @@ with gr.Blocks(
|
|
| 758 |
<div class="info-section">
|
| 759 |
<h3>Overview</h3>
|
| 760 |
<p>This tool detects changes in <strong>multiple sclerosis (MS) lesions</strong> between two brain MRI scans.</p>
|
|
|
|
| 761 |
<p>Input sequence must be <strong>isotropic 3D FLAIR</strong> in
|
| 762 |
<code>.nii/.nii.gz</code> (NIfTI) or DICOM (<code>.zip</code>) format. <br>
|
| 763 |
If DICOM is provided, images are automatically converted to NIfTI using
|
| 764 |
<em>dcm2niix</em>.</p>
|
|
|
|
| 765 |
<p>Processing includes skull stripping with <em>NiPreps SynthStrip</em> package,
|
| 766 |
rigid/affine co-registration of the two scans with SimpleITK,
|
| 767 |
and lesion segmentation using <em>FLAMeS</em> deep learning model.</p>
|
|
|
|
| 768 |
<p>Lesion difference masks between the two scans are then calculated and made available for download.</p>
|
|
|
|
| 769 |
<p><strong>Note: This application is a <em>research preview</em>.
|
| 770 |
For clinical reporting, all results should be reviewed and validated by a qualified radiologist.</strong></p>
|
| 771 |
</div>
|
|
@@ -784,6 +820,7 @@ with gr.Blocks(
|
|
| 784 |
<li>After processing, download the ZIP file and open the NIfTI outputs in your preferred neuroimaging viewer
|
| 785 |
(e.g. ITK-SNAP, FSLeyes, 3D Slicer) to inspect the lesions and overlays.</li>
|
| 786 |
</ul>
|
|
|
|
| 787 |
<p style="margin-top:16px;"><strong>Advanced options:</strong></p>
|
| 788 |
<ul>
|
| 789 |
<li><em>Dilate previous mask (voxels):</em> Expands the baseline lesion mask slightly
|
|
@@ -829,12 +866,14 @@ Li X, Morgan PS, Ashburner J, Smith J, Rorden C (2016).
|
|
| 829 |
<strong>J Neurosci Methods</strong> 264:47–56.
|
| 830 |
<a href="https://doi.org/10.1016/j.jneumeth.2016.03.001" target="_blank">📄 DOI: 10.1016/j.jneumeth.2016.03.001</a>
|
| 831 |
</li>
|
|
|
|
| 832 |
<li>
|
| 833 |
Hoopes A, Mora JS, Dalca AV, Fischl B*, Hoffmann M* (2022).
|
| 834 |
<em>SynthStrip: Skull-Stripping for Any Brain Image.</em>
|
| 835 |
<strong>NeuroImage</strong> 260:119474.
|
| 836 |
<a href="https://doi.org/10.1016/j.neuroimage.2022.119474" target="_blank">📄 DOI: 10.1016/j.neuroimage.2022.119474</a>
|
| 837 |
</li>
|
|
|
|
| 838 |
<li>
|
| 839 |
Dereskewicz E, La Rosa F, Dos Santos Silva J, et al. (2025).
|
| 840 |
<em>FLAMeS: A Robust Deep Learning Model for Automated Multiple Sclerosis Lesion Segmentation.</em>
|
|
|
|
| 6 |
import shutil
|
| 7 |
from pathlib import Path
|
| 8 |
from typing import Tuple, Dict, List, Optional
|
| 9 |
+
|
| 10 |
import gradio as gr
|
| 11 |
import SimpleITK as sitk
|
| 12 |
from huggingface_hub import hf_hub_download
|
| 13 |
+
|
| 14 |
import spaces
|
| 15 |
|
| 16 |
|
| 17 |
+
# Dummy function to satisfy HF Spaces GPU detection during startup
|
| 18 |
+
@spaces.GPU
|
| 19 |
+
def _init_gpu():
|
| 20 |
+
"""Dummy function to ensure Spaces detects GPU usage at startup."""
|
| 21 |
+
import torch
|
| 22 |
+
return torch.cuda.is_available() if torch.cuda.is_available() else True
|
| 23 |
+
|
| 24 |
+
|
| 25 |
# =========================
|
| 26 |
# App config
|
| 27 |
# =========================
|
|
|
|
| 60 |
MODEL_ROOT.mkdir(parents=True, exist_ok=True)
|
| 61 |
BIN_ROOT.mkdir(parents=True, exist_ok=True)
|
| 62 |
|
| 63 |
+
# After ensure_dirs(), add:
|
| 64 |
+
try:
|
| 65 |
+
_init_gpu()
|
| 66 |
+
except Exception:
|
| 67 |
+
pass
|
| 68 |
+
|
| 69 |
|
| 70 |
def is_zip(path: Path) -> bool:
|
| 71 |
return path.suffix.lower() == ".zip"
|
|
|
|
| 78 |
return shutil.which("dcm2niix")
|
| 79 |
|
| 80 |
|
| 81 |
+
# ---- SynthStrip (NiPreps) without Docker ----
|
| 82 |
def ensure_synthstrip_available() -> None:
|
| 83 |
"""
|
| 84 |
Ensure SynthStrip from NiPreps is available, either:
|
|
|
|
| 300 |
@spaces.GPU(duration=300)
|
| 301 |
def run_flames_single(input_nii: Path, out_mask_path: Path, device: str = "cuda") -> Path:
|
| 302 |
"""Run FLAMeS (nnUNetv2) on a single input NIfTI and write a mask. Uses shared MODEL_ROOT."""
|
|
|
|
| 303 |
with (Path(input_nii).open("rb")):
|
| 304 |
pass
|
| 305 |
import tempfile
|
|
|
|
| 508 |
return s
|
| 509 |
|
| 510 |
|
| 511 |
+
@spaces.GPU(duration=300)
|
| 512 |
def run_pipeline(file1, file2, dilate_prev_radius_vox=1, min_lesion_vol_ml=0.01):
|
| 513 |
"""
|
| 514 |
file1: previous (baseline) FLAIR (.nii/.nii.gz or DICOM .zip)
|
|
|
|
| 555 |
registered_path, _, _ = register_rigid_affine(prev_stripped, new_stripped, reg_dir)
|
| 556 |
registered_path = registered_path.resolve()
|
| 557 |
|
| 558 |
+
# FLAMeS segmentation
|
| 559 |
seg_dir = job_dir / SEG_DIR; seg_dir.mkdir(parents=True, exist_ok=True)
|
| 560 |
prev_mask_flames = seg_dir / "prev_flames_mask.nii.gz"
|
| 561 |
new_mask_flames = seg_dir / "new_in_prev_space_flames_mask.nii.gz"
|
|
|
|
| 626 |
|
| 627 |
|
| 628 |
# =========================
|
| 629 |
+
# Gradio UI (refined, consistent theme)
|
| 630 |
# =========================
|
| 631 |
with gr.Blocks(
|
| 632 |
title=APP_NAME,
|
|
|
|
| 645 |
-webkit-background-clip: text;
|
| 646 |
-webkit-text-fill-color: transparent;
|
| 647 |
}
|
| 648 |
+
|
| 649 |
/* ----- Run pipeline button ----- */
|
| 650 |
#run_btn {
|
| 651 |
display: block;
|
|
|
|
| 661 |
transition: all 0.25s ease;
|
| 662 |
text-transform: none !important;
|
| 663 |
}
|
| 664 |
+
|
| 665 |
#run_btn:hover {
|
| 666 |
background: linear-gradient(90deg, #3da0df, #64eec8);
|
| 667 |
transform: translateY(-1px);
|
| 668 |
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.18);
|
| 669 |
color: #000
|
| 670 |
}
|
| 671 |
+
|
| 672 |
/* ----- Status box ----- */
|
| 673 |
#status_box {
|
| 674 |
overflow: visible !important;
|
|
|
|
| 682 |
font-size: 16px;
|
| 683 |
line-height: 1.45;
|
| 684 |
}
|
| 685 |
+
|
| 686 |
/* ----- Info & reference sections ----- */
|
| 687 |
.info-section {
|
| 688 |
font-size: 18px;
|
|
|
|
| 695 |
border: 1px solid var(--border-color-primary);
|
| 696 |
box-shadow: 0 4px 14px rgba(0,0,0,0.06);
|
| 697 |
}
|
| 698 |
+
|
| 699 |
.info-section h3 {
|
| 700 |
margin-top: 0;
|
| 701 |
margin-bottom: 12px;
|
|
|
|
| 704 |
color: #4cafef;
|
| 705 |
letter-spacing: -0.3px;
|
| 706 |
}
|
| 707 |
+
|
| 708 |
.info-section p, .info-section li {
|
| 709 |
color: var(--body-text-color);
|
| 710 |
}
|
| 711 |
+
|
| 712 |
.info-section ul {
|
| 713 |
margin-top: 6px;
|
| 714 |
margin-bottom: 6px;
|
| 715 |
padding-left: 24px;
|
| 716 |
list-style-type: disc;
|
| 717 |
}
|
| 718 |
+
|
| 719 |
.info-section code {
|
| 720 |
background: var(--background-secondary);
|
| 721 |
padding: 2px 5px;
|
| 722 |
border-radius: 5px;
|
| 723 |
font-size: 90%;
|
| 724 |
}
|
| 725 |
+
|
| 726 |
.info-section a {
|
| 727 |
color: #4cafef;
|
| 728 |
text-decoration: none;
|
| 729 |
}
|
| 730 |
+
|
| 731 |
.info-section a:hover {
|
| 732 |
text-decoration: underline;
|
| 733 |
}
|
| 734 |
+
|
| 735 |
/* ----- Textual report styling ----- */
|
| 736 |
#report {
|
| 737 |
font-size: 20px !important;
|
|
|
|
| 745 |
margin: 0 auto 34px auto;
|
| 746 |
box-shadow: 0 6px 18px rgba(0,0,0,0.08);
|
| 747 |
}
|
| 748 |
+
|
| 749 |
#report h3 {
|
| 750 |
margin-top: 0;
|
| 751 |
margin-bottom: 16px;
|
|
|
|
| 755 |
letter-spacing: -0.3px;
|
| 756 |
text-align: left;
|
| 757 |
}
|
| 758 |
+
|
| 759 |
#report ul {
|
| 760 |
margin: 10px 0 0 22px;
|
| 761 |
padding: 0;
|
| 762 |
list-style-type: disc;
|
| 763 |
}
|
| 764 |
+
|
| 765 |
#report li {
|
| 766 |
margin-bottom: 10px;
|
| 767 |
font-size: 20px;
|
| 768 |
line-height: 1.8;
|
| 769 |
}
|
| 770 |
+
|
| 771 |
#report strong {
|
| 772 |
color: #4cafef;
|
| 773 |
font-weight: 600;
|
| 774 |
font-size: 20px;
|
| 775 |
}
|
| 776 |
+
|
| 777 |
#report .footnote {
|
| 778 |
font-size: 16px;
|
| 779 |
color: #999;
|
|
|
|
| 790 |
<div class="info-section">
|
| 791 |
<h3>Overview</h3>
|
| 792 |
<p>This tool detects changes in <strong>multiple sclerosis (MS) lesions</strong> between two brain MRI scans.</p>
|
| 793 |
+
|
| 794 |
<p>Input sequence must be <strong>isotropic 3D FLAIR</strong> in
|
| 795 |
<code>.nii/.nii.gz</code> (NIfTI) or DICOM (<code>.zip</code>) format. <br>
|
| 796 |
If DICOM is provided, images are automatically converted to NIfTI using
|
| 797 |
<em>dcm2niix</em>.</p>
|
| 798 |
+
|
| 799 |
<p>Processing includes skull stripping with <em>NiPreps SynthStrip</em> package,
|
| 800 |
rigid/affine co-registration of the two scans with SimpleITK,
|
| 801 |
and lesion segmentation using <em>FLAMeS</em> deep learning model.</p>
|
| 802 |
+
|
| 803 |
<p>Lesion difference masks between the two scans are then calculated and made available for download.</p>
|
| 804 |
+
|
| 805 |
<p><strong>Note: This application is a <em>research preview</em>.
|
| 806 |
For clinical reporting, all results should be reviewed and validated by a qualified radiologist.</strong></p>
|
| 807 |
</div>
|
|
|
|
| 820 |
<li>After processing, download the ZIP file and open the NIfTI outputs in your preferred neuroimaging viewer
|
| 821 |
(e.g. ITK-SNAP, FSLeyes, 3D Slicer) to inspect the lesions and overlays.</li>
|
| 822 |
</ul>
|
| 823 |
+
|
| 824 |
<p style="margin-top:16px;"><strong>Advanced options:</strong></p>
|
| 825 |
<ul>
|
| 826 |
<li><em>Dilate previous mask (voxels):</em> Expands the baseline lesion mask slightly
|
|
|
|
| 866 |
<strong>J Neurosci Methods</strong> 264:47–56.
|
| 867 |
<a href="https://doi.org/10.1016/j.jneumeth.2016.03.001" target="_blank">📄 DOI: 10.1016/j.jneumeth.2016.03.001</a>
|
| 868 |
</li>
|
| 869 |
+
|
| 870 |
<li>
|
| 871 |
Hoopes A, Mora JS, Dalca AV, Fischl B*, Hoffmann M* (2022).
|
| 872 |
<em>SynthStrip: Skull-Stripping for Any Brain Image.</em>
|
| 873 |
<strong>NeuroImage</strong> 260:119474.
|
| 874 |
<a href="https://doi.org/10.1016/j.neuroimage.2022.119474" target="_blank">📄 DOI: 10.1016/j.neuroimage.2022.119474</a>
|
| 875 |
</li>
|
| 876 |
+
|
| 877 |
<li>
|
| 878 |
Dereskewicz E, La Rosa F, Dos Santos Silva J, et al. (2025).
|
| 879 |
<em>FLAMeS: A Robust Deep Learning Model for Automated Multiple Sclerosis Lesion Segmentation.</em>
|