wanhanisah commited on
Commit
ea99d0b
·
verified ·
1 Parent(s): 3d368e7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +181 -56
app.py CHANGED
@@ -6,7 +6,6 @@ import zipfile
6
  import tempfile
7
  from pathlib import Path
8
  from typing import Optional, List, Dict
9
-
10
  import numpy as np
11
  import pandas as pd
12
  import nibabel as nib
@@ -17,6 +16,7 @@ from matplotlib import animation
17
  from PIL import Image
18
  from skimage.measure import label as cc_label, regionprops
19
  import streamlit as st
 
20
 
21
  # ---- Custom layer used by model
22
  from utils.layer_util import ResizeAndConcatenate
@@ -49,7 +49,7 @@ ISLAND_MIN_SLICE_SPAN = 2
49
  ISLAND_MIN_AREA_PER_SLICE = 10
50
  ISLAND_CENTROID_DIST_THRESH = 40
51
 
52
- # Orientation / display (non-destructive)
53
  ORIENT_TARGET = None # None (keep native), "LPS", or "RAS"
54
  DISPLAY_MATCH_DICOM = False
55
  DISPLAY_RULES = {
@@ -69,6 +69,125 @@ MID_A_MYO_MIN = 30
69
  GIF_FPS = 2
70
  GIF_DPI = 300
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  # =========================
73
  # Small utilities
74
  # =========================
@@ -450,68 +569,71 @@ def process_nifti_case(nifti_path: str, model, rows_acc: List[Dict], per_frame_r
450
  'PixelSpacing_col_mm': spacing['col_mm'],
451
  })
452
 
453
- # GIF: ED vs ES (slices animate) — non-fatal display
454
  gif_path = gif_animation_for_patient_pred_only(imgs_4d, preds_4d, pid, ed_idx, es_idx, GIFS_DIR)
455
  try:
456
- # Streamlit uses 'use_column_width'; keep it, but guard against TypeError on older builds
457
  st.image(gif_path, caption="Generated GIF", use_column_width=True)
458
  except TypeError:
459
  st.image(gif_path, caption="Generated GIF")
460
  log(f"GIF saved: {gif_path}")
461
 
 
462
  # =========================
463
  # UI
464
  # =========================
465
- # def main():
466
- # st.header("Data Upload")
467
- # uploaded_zip = st.file_uploader("Upload ZIP of NIfTI files", type="zip")
468
-
469
  def main():
470
- st.set_page_config(layout="wide")
 
471
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  st.markdown(
473
  """
474
- <h1 style='text-align: left; font-size: 32px; line-height: 1.4;'>
475
- This Hugging Face model is developed as part of the publication: <br>
476
- <span style='margin-left: 40px; font-size: 28px; display: block;'>
477
- Open-Source Pre-Clinical Image Segmentation:
478
- </span>
479
- <span style='margin-left: 40px; font-size: 28px; display: block;'>
480
- Mouse cardiac MRI datasets with a deep learning segmentation framework
481
- </span>
482
- submitted to the Journal of Cardiovascular Magnetic Resonance
483
- </h1>
484
  """,
485
  unsafe_allow_html=True
486
  )
487
-
488
- st.markdown(
489
- """
490
- <div style="width:100%; text-align:justify;">
491
- In this paper, we present the first publicly-available pre-clinical cardiac MRI dataset, along with an open-source DL segmentation model
492
- <br>(both available on GitHub: https://github.com/mrphys/Open-Source_Pre-Clinical_Segmentation.git)<br>
493
- and this web-based interface for easy deployment.
494
- <br><br>
495
- The dataset comprises of complete cine short-axis cardiac MRI images from 130 mice with diverse phenotypes.
496
- <br>It also contains expert manual segmentations of left ventricular (LV) blood pool and myocardium at end-diastole, end-systole, as well as additional timeframes with artefacts to improve robustness.
497
-
498
- <br>Using this resource, we developed an open-source DL segmentation model based on the UNet3+ architecture.
499
-
500
- This Streamlit application consits of the inference model to enable easy-to-use interface of our DL segmentation model, without the need for local installation.
501
- <br>The applictaion requires the complete SAX cine image data to be uploaded in NIfTI format, as a zip file using the simple file browser, below.
502
-
503
- Pre-processing and inference is then performed on all 2D images.
504
- <br>The resulting blood-pool and myocardial volumes are combined across all slices at each timeframe and output in a .csv file.
505
- <br>The blood-pool volumes are used to easily identify ED and ES, and these volumes are displayed as a GIF, with the segmentations overlaid.
506
- """,
507
- unsafe_allow_html=True,
508
  )
509
-
510
- st.header("Data Upload 🐭")
511
-
512
- uploaded_zip = st.file_uploader("Upload ZIP of NIfTI files", type="zip")
513
-
514
 
 
515
  def extract_zip(zip_path, extract_to):
516
  os.makedirs(extract_to, exist_ok=True)
517
  with zipfile.ZipFile(zip_path, 'r') as zip_ref:
@@ -525,10 +647,8 @@ def main():
525
  st.stop()
526
 
527
  if st.button("Process Data"):
528
- # Show the real filename in the spinner and use it for the temp file name
529
  zip_label = uploaded_zip.name or "ZIP"
530
  with st.spinner(f"Processing {zip_label}..."):
531
- # Save & extract
532
  tmpdir = tempfile.mkdtemp()
533
  zpath = os.path.join(tmpdir, uploaded_zip.name)
534
  with open(zpath, "wb") as f:
@@ -551,7 +671,7 @@ def main():
551
  model = keras.models.load_model(
552
  MODEL_PATH,
553
  custom_objects={
554
- 'focal_tversky_loss': None, # loss/metrics not needed for inference
555
  'dice_coef_no_bkg': None,
556
  'ResizeAndConcatenate': ResizeAndConcatenate,
557
  'dice_myo': None,
@@ -575,14 +695,19 @@ def main():
575
  csv_path = write_all_in_one_csv(rows, per_frame_rows, CSV_DIR)
576
  st.success(f"Processed {len(rows)} NIfTI file(s).")
577
  csv_download_name = f"{Path(zip_label).stem}_Results.csv"
 
 
578
  with open(csv_path, "rb") as f:
579
- st.download_button(
580
- label="Download CSV",
581
- data=f,
582
- file_name=csv_download_name,
583
- mime="text/csv"
584
- )
585
-
586
-
 
 
587
  if __name__ == "__main__":
588
  main()
 
 
6
  import tempfile
7
  from pathlib import Path
8
  from typing import Optional, List, Dict
 
9
  import numpy as np
10
  import pandas as pd
11
  import nibabel as nib
 
16
  from PIL import Image
17
  from skimage.measure import label as cc_label, regionprops
18
  import streamlit as st
19
+ from textwrap import dedent
20
 
21
  # ---- Custom layer used by model
22
  from utils.layer_util import ResizeAndConcatenate
 
49
  ISLAND_MIN_AREA_PER_SLICE = 10
50
  ISLAND_CENTROID_DIST_THRESH = 40
51
 
52
+ # Orientation / display
53
  ORIENT_TARGET = None # None (keep native), "LPS", or "RAS"
54
  DISPLAY_MATCH_DICOM = False
55
  DISPLAY_RULES = {
 
69
  GIF_FPS = 2
70
  GIF_DPI = 300
71
 
72
+ # =========================
73
+ # Branding / UI
74
+ # =========================
75
+ # Raw logo URL so Streamlit can fetch it
76
+ LOGO_URL = "https://raw.githubusercontent.com/whanisa/Segmentation/main/icon/logo.png"
77
+ LOGO_LINK = "https://github.com/whanisa/Segmentation"
78
+
79
+ # Optional: adjust fixed logo size and safe top spacing
80
+ LOGO_HEIGHT_PX = 120
81
+ SAFE_INSET_PX = 18
82
+
83
+
84
+ # --- CSS
85
+ def _inject_layout_css():
86
+ # Tune these three knobs as needed
87
+ CONTENT_MEASURE_PX = 920 # width of the hero + paragraphs column
88
+ LEFT_OFFSET_PX = 40 # push BOTH text and uploader to the right
89
+ UPLOAD_WIDTH_PX = 420 # make uploader column narrower
90
+
91
+ st.markdown(f"""
92
+ <style>
93
+ :root {{
94
+ --content-measure: {CONTENT_MEASURE_PX}px;
95
+ --left-offset: {LEFT_OFFSET_PX}px;
96
+ --upload-width: {UPLOAD_WIDTH_PX}px;
97
+ }}
98
+
99
+ /* Compact page padding */
100
+ .appview-container .main .block-container {{
101
+ padding-top: 0.75rem;
102
+ padding-bottom: 1rem;
103
+ }}
104
+
105
+ /* Outer wrapper */
106
+ .content-wrap {{
107
+ width: min(1300px, 100%);
108
+ margin: 0 auto;
109
+ padding: 0 18px;
110
+ box-sizing: border-box;
111
+ }}
112
+
113
+ /* Text column (hero + paragraphs): same width + same left offset as uploader */
114
+ .measure-wrap {{
115
+ max-width: var(--content-measure);
116
+ margin-left: var(--left-offset);
117
+ margin-right: auto;
118
+ }}
119
+
120
+ /* HERO: one H1 + inline spans; remove all extra margins */
121
+ .hero-title {{
122
+ text-align: left;
123
+ font-size: 32px;
124
+ line-height: 1.25; /* exact line-height */
125
+ font-weight: 800;
126
+ margin: 0; /* kill default h1 margins */
127
+ }}
128
+ .hero-title .sub {{
129
+ display: block; /* forces each to its own line */
130
+ font-size: 28px; /* exact size */
131
+ line-height: 1.25; /* keep same rhythm */
132
+ margin: 0; /* kill gaps between lines */
133
+ }}
134
+
135
+ /* Body text */
136
+ .text-wrap p {{
137
+ margin: 0 0 14px 0;
138
+ font-size: 17px;
139
+ line-height: 1.5;
140
+ text-align: justify;
141
+ color: #333;
142
+ }}
143
+ .text-wrap a {{
144
+ color: #0066cc !important;
145
+ text-decoration: underline !important;
146
+ }}
147
+
148
+ /* Uploader block: same left offset; narrower width so it doesn't drift right */
149
+ #upload-wrap {{
150
+ max-width: var(--upload-width);
151
+ margin-left: var(--left-offset);
152
+ margin-right: auto;
153
+ }}
154
+ #upload-wrap [data-testid="stFileUploader"] {{
155
+ width: 100% !important;
156
+ margin-left: 0 !important;
157
+ margin-right: 0 !important;
158
+ }}
159
+ #upload-wrap [data-testid="stFileUploaderDropzone"] {{
160
+ padding-left: 0 !important;
161
+ padding-right: 44px !important;
162
+ }}
163
+
164
+ /* Fixed, NON-INTERACTIVE logo (won't steal clicks) */
165
+ #fixed-logo {{
166
+ position: fixed;
167
+ top: 40px;
168
+ left: 25px;
169
+ z-index: 5; /* lower than big overlays; high enough to be visible */
170
+ pointer-events: none; /* <-- key: pass clicks through */
171
+ }}
172
+ #fixed-logo img {{
173
+ height: 120px;
174
+ display: block;
175
+ }}
176
+ .top-spacer {{ height: 120px; }}
177
+
178
+ /* Remove Streamlit paperclip/anchor icons on headings */
179
+ [data-testid="stHeaderAction"] {{ display: none !important; }}
180
+ .stMarkdown h1 a, .stMarkdown h2 a, .stMarkdown h3 a {{ display: none !important; }}
181
+ </style>
182
+
183
+ <!-- Fixed, NON-clickable logo (no <a> wrapper) -->
184
+ <div id="fixed-logo" aria-hidden="true" role="presentation">
185
+ <img src="{LOGO_URL}" alt="">
186
+ </div>
187
+ <div class="top-spacer"></div>
188
+ """, unsafe_allow_html=True)
189
+
190
+
191
  # =========================
192
  # Small utilities
193
  # =========================
 
569
  'PixelSpacing_col_mm': spacing['col_mm'],
570
  })
571
 
572
+ # GIF: ED vs ES (slices animate)
573
  gif_path = gif_animation_for_patient_pred_only(imgs_4d, preds_4d, pid, ed_idx, es_idx, GIFS_DIR)
574
  try:
 
575
  st.image(gif_path, caption="Generated GIF", use_column_width=True)
576
  except TypeError:
577
  st.image(gif_path, caption="Generated GIF")
578
  log(f"GIF saved: {gif_path}")
579
 
580
+
581
  # =========================
582
  # UI
583
  # =========================
 
 
 
 
584
  def main():
585
+ st.set_page_config(layout="wide", initial_sidebar_state="collapsed")
586
+ _inject_layout_css()
587
 
588
+ # Optional: also register the logo with Streamlit (for favicon/header in newer builds)
589
+ try:
590
+ st.logo(LOGO_URL, size="large", link=LOGO_LINK, icon_image=LOGO_URL)
591
+ except Exception:
592
+ pass
593
+
594
+ # ---------- HERO (kept indentation; dedent removes it safely) ----------
595
+ HERO_HTML = dedent("""
596
+ <div class="content-wrap">
597
+ <div class="measure-wrap">
598
+ <div class="hero-wrap">
599
+ <h1 class="hero-title">
600
+ This Hugging Face model is developed as part of the publication:<br>
601
+ <span class="sub">Open-Source Pre-Clinical Image Segmentation:</span>
602
+ <span class="sub">Mouse cardiac MRI datasets with a deep learning segmentation framework</span>
603
+ submitted to the Journal of Cardiovascular Magnetic Resonance
604
+ </h1>
605
+ </div>
606
+ <div class="text-wrap">
607
+ <p>In this paper, we present the first publicly-available pre-clinical cardiac MRI dataset, along with an open-source DL segmentation model (both available on GitHub:
608
+ <a href="https://github.com/mrphys/Open-Source_Pre-Clinical_Segmentation.git" target="_blank" rel="noopener">https://github.com/mrphys/Open-Source_Pre-Clinical_Segmentation.git</a>) and this web-based interface for easy deployment.</p>
609
+ <p>The dataset comprises complete cine short-axis cardiac MRI images from 130 mice with diverse phenotypes. It also contains expert manual segmentations of left ventricular (LV) blood pool and myocardium at end-diastole, end-systole, as well as additional timeframes with artefacts to improve robustness.</p>
610
+ <p>Using this resource, we developed an open-source DL segmentation model based on the UNet3+ architecture.</p>
611
+ <p>This Streamlit application consists of the inference model to provide an easy-to-use interface for our DL segmentation model, without the need for local installation. The application requires the complete SAX cine image data to be uploaded in NIfTI format, as a ZIP file using the simple file browser below.</p>
612
+ <p>Pre-processing and inference are then performed on all 2D images. The resulting blood-pool and myocardial volumes are combined across all slices at each timeframe and output to a .csv file. The blood-pool volumes are used to identify ED and ES, and these volumes are displayed as a GIF with the segmentations overlaid.</p>
613
+ </div>
614
+ </div>
615
+ </div>
616
+ """)
617
+ st.markdown(HERO_HTML, unsafe_allow_html=True)
618
+
619
+ # ---------- DATA UPLOAD (same column; perfect left alignment) ----------
620
+ st.markdown('<div class="content-wrap"><div class="measure-wrap" id="upload-wrap">', unsafe_allow_html=True)
621
  st.markdown(
622
  """
623
+ <h2 style='margin-bottom:0.2rem;'>
624
+ Data Upload <span style='font-size:31px;'>📤</span>
625
+ </h2>
 
 
 
 
 
 
 
626
  """,
627
  unsafe_allow_html=True
628
  )
629
+ uploaded_zip = st.file_uploader(
630
+ "Upload ZIP of NIfTI files 🐭",
631
+ type="zip",
632
+ label_visibility="visible"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
633
  )
634
+ st.markdown('</div></div>', unsafe_allow_html=True)
 
 
 
 
635
 
636
+ # ---- Extract helper ----
637
  def extract_zip(zip_path, extract_to):
638
  os.makedirs(extract_to, exist_ok=True)
639
  with zipfile.ZipFile(zip_path, 'r') as zip_ref:
 
647
  st.stop()
648
 
649
  if st.button("Process Data"):
 
650
  zip_label = uploaded_zip.name or "ZIP"
651
  with st.spinner(f"Processing {zip_label}..."):
 
652
  tmpdir = tempfile.mkdtemp()
653
  zpath = os.path.join(tmpdir, uploaded_zip.name)
654
  with open(zpath, "wb") as f:
 
671
  model = keras.models.load_model(
672
  MODEL_PATH,
673
  custom_objects={
674
+ 'focal_tversky_loss': None,
675
  'dice_coef_no_bkg': None,
676
  'ResizeAndConcatenate': ResizeAndConcatenate,
677
  'dice_myo': None,
 
695
  csv_path = write_all_in_one_csv(rows, per_frame_rows, CSV_DIR)
696
  st.success(f"Processed {len(rows)} NIfTI file(s).")
697
  csv_download_name = f"{Path(zip_label).stem}_Results.csv"
698
+
699
+ # -------- PASS BYTES (not a file object) --------
700
  with open(csv_path, "rb") as f:
701
+ csv_bytes = f.read()
702
+ st.download_button(
703
+ label="Download CSV",
704
+ data=csv_bytes, # <-- bytes, best practice on HuggingFace/iframes
705
+ file_name=csv_download_name,
706
+ mime="text/csv",
707
+ key="dl_csv_results" # stable key helps HF embed
708
+ )
709
+
710
+
711
  if __name__ == "__main__":
712
  main()
713
+