kstember commited on
Commit
d7f36bb
·
verified ·
1 Parent(s): 9094fca

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +43 -4
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 (only this part uses GPU/zeroGPU)
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>