wanhanisah commited on
Commit
ae2f7ce
·
verified ·
1 Parent(s): 4916f13

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +607 -154
app.py CHANGED
@@ -1,9 +1,11 @@
1
- # app.py
2
  import os
3
  import io
4
  import math
5
  import zipfile
6
  import tempfile
 
 
7
  from pathlib import Path
8
  from typing import Optional, List, Dict
9
  import numpy as np
@@ -17,6 +19,7 @@ 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
@@ -72,11 +75,8 @@ GIF_DPI = 300
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
 
@@ -84,9 +84,9 @@ SAFE_INSET_PX = 18
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>
@@ -94,8 +94,27 @@ def _inject_layout_css():
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;
@@ -117,35 +136,79 @@ def _inject_layout_css():
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);
@@ -161,33 +224,52 @@ def _inject_layout_css():
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
  # =========================
@@ -494,6 +576,137 @@ def write_all_in_one_csv(rows, per_frame_rows, csv_dir):
494
  log(f"CSV written: {out_csv}")
495
  return out_csv
496
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  # =========================
498
  # Per-file processing
499
  # =========================
@@ -553,7 +766,7 @@ def process_nifti_case(nifti_path: str, model, rows_acc: List[Dict], per_frame_r
553
  per_frame_df.insert(0, "Patient_ID", pid)
554
  per_frame_rows_acc.append(per_frame_df)
555
 
556
- # -------- Append summary row BEFORE any UI drawing (so UI errors don't drop data)
557
  rows_acc.append({
558
  'Patient_ID': pid,
559
  'EDV_uL': EDV_uL,
@@ -585,129 +798,369 @@ 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:
640
- valid_files = [
641
- f for f in zip_ref.namelist()
642
- if "__MACOSX" not in f and not os.path.basename(f).startswith("._")
643
- ]
644
- zip_ref.extractall(extract_to, members=valid_files)
645
-
646
- if uploaded_zip is None:
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:
655
- f.write(uploaded_zip.read())
656
- extract_zip(zpath, tmpdir)
657
-
658
- # Find NIfTI files inside ZIP
659
- nii_files: List[str] = []
660
- for root, _, files in os.walk(tmpdir):
661
- for fn in files:
662
- low = fn.lower()
663
- if low.endswith(".nii") or low.endswith(".nii.gz"):
664
- nii_files.append(os.path.join(root, fn))
665
-
666
- if not nii_files:
667
- st.error("No NIfTI files (.nii / .nii.gz) found in the uploaded ZIP.")
668
- st.stop()
669
-
670
- # Load model
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,
678
- 'dice_blood': None,
679
- 'dice': None
680
- },
681
- compile=False
682
- )
683
- log("Model loaded.")
684
-
685
- rows: List[Dict] = []
686
- per_frame_rows: List[pd.DataFrame] = []
687
-
688
- for fp in sorted(nii_files):
689
- try:
690
- process_nifti_case(fp, model, rows, per_frame_rows)
691
- except Exception as e:
692
- st.warning(f"Failed: {Path(fp).name} {e}")
693
-
694
- # Write merged CSV and show download
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
-
 
1
+ # App.py
2
  import os
3
  import io
4
  import math
5
  import zipfile
6
  import tempfile
7
+ import streamlit.components.v1 as components
8
+ from textwrap import dedent
9
  from pathlib import Path
10
  from typing import Optional, List, Dict
11
  import numpy as np
 
19
  from skimage.measure import label as cc_label, regionprops
20
  import streamlit as st
21
  from textwrap import dedent
22
+ import base64 # kept for reference; not required when using st.download_button
23
 
24
  # ---- Custom layer used by model
25
  from utils.layer_util import ResizeAndConcatenate
 
75
  # =========================
76
  # Branding / UI
77
  # =========================
 
78
  LOGO_URL = "https://raw.githubusercontent.com/whanisa/Segmentation/main/icon/logo.png"
79
  LOGO_LINK = "https://github.com/whanisa/Segmentation"
 
 
80
  LOGO_HEIGHT_PX = 120
81
  SAFE_INSET_PX = 18
82
 
 
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 (aligns with left gutter)
89
+ UPLOAD_WIDTH_PX = 420 # make uploader column narrower
90
 
91
  st.markdown(f"""
92
  <style>
 
94
  --content-measure: {CONTENT_MEASURE_PX}px;
95
  --left-offset: {LEFT_OFFSET_PX}px;
96
  --upload-width: {UPLOAD_WIDTH_PX}px;
97
+
98
+ /* Fixed-edge logo tunables */
99
+ --logo-height: {LOGO_HEIGHT_PX}px;
100
+ --edge-x: max(12px, env(safe-area-inset-left));
101
+ /* Height of Streamlit/HF header; adjust to 40-56–64px if needed */
102
+ --header-clear: 40px;
103
+ --edge-y: calc(env(safe-area-inset-top) + var(--header-clear) + 0px);
104
+
105
+ /* Gap below the logo before the tabs appear */
106
+ --tabs-top-gap: calc(var(--logo-height) + 16px);
107
+
108
+ /* Shift the tabs a bit to the right */
109
+ --tabs-left-shift: 32px;
110
+
111
+ /* Accent color for the active tab underline/text */
112
+ --accent: #ef4444; /* red-500 */
113
  }}
114
 
115
+ /* Ensure the fixed logo isn't clipped by Streamlit containers */
116
+ .stApp, .appview-container, .main {{ overflow: visible !important; }}
117
+
118
  /* Compact page padding */
119
  .appview-container .main .block-container {{
120
  padding-top: 0.75rem;
 
136
  margin-right: auto;
137
  }}
138
 
139
+ /* ---- Fixed, far-left edge logo ---- */
140
+ #fixed-edge-logo {{
141
+ position: fixed;
142
+ left: var(--edge-x);
143
+ top: var(--edge-y);
144
+ z-index: 1000;
145
+ pointer-events: none;
146
+ }}
147
+ #fixed-edge-logo img {{
148
+ height: var(--logo-height);
149
+ width: auto;
150
+ display: block;
151
+ }}
152
+
153
+ /* Spacer so tabs/hero don't hide under the fixed logo */
154
+ .edge-logo-spacer {{ height: var(--tabs-top-gap); }}
155
+
156
+
157
+ /* Title: justify both sides */
158
  .hero-title {{
159
+ font-size: 40px;
160
+ line-height: 1.25;
 
161
  font-weight: 800;
162
+ margin: 0 0 25px; /* adjust to control gap to first paragraph */
163
+ text-align: justify; /* both edges */
164
+ text-justify: inter-word;
165
  }}
166
+
167
+
168
+ /* Optional subtitle inside the H1 */
169
  .hero-title .sub {{
170
+ display: block;
171
+ font-size: 28px;
172
+ line-height: 1.25;
173
+ margin: 0;
174
  }}
175
+
176
+ /* Body paragraphs: justified, but last line remains ragged-right */
177
  .text-wrap p {{
178
+ margin: 0 0 14px 0; /* paragraph spacing */
179
  font-size: 17px;
180
  line-height: 1.5;
181
  text-align: justify;
182
+ text-justify: inter-word;
183
  color: #333;
184
+ hyphens: auto;
185
+ -webkit-hyphens: auto;
186
+ -ms-hyphens: auto;
187
+ }}
188
+
189
+ /* Let long URLs wrap so they don’t wreck the right edge */
190
+ .text-wrap p a {{
191
+ overflow-wrap: anywhere;
192
+ word-break: break-word;
193
  }}
194
+
195
+ /* Link aesthetics (optional) */
196
  .text-wrap a {{
197
+ color: #0066cc;
198
+ text-decoration: underline;
199
+ text-underline-offset: 2px;
200
+ text-decoration-thickness: 1.5px;
201
+ }}
202
+
203
+ /* Note under last paragraph */
204
+ .note-text {{
205
+ font-size: 14px; /* smaller than normal text */
206
+ color: #333; /* optional: softer gray */
207
+ line-height: 1.4; /* a bit tighter spacing */
208
+ margin-top: 4px; /* space above note */
209
  }}
210
 
211
+ /* Uploader block alignment */
212
  #upload-wrap {{
213
  max-width: var(--upload-width);
214
  margin-left: var(--left-offset);
 
224
  padding-right: 44px !important;
225
  }}
226
 
227
+ /* ---- REAL Streamlit tabs styling and alignment ---- */
228
+ /* Shift the tab strip to align with content */
229
+ div[data-testid="stTabs"] > div[role="tablist"],
230
+ div[data-baseweb="tab-list"],
231
+ .stTabs [role="tablist"] {{
232
+ margin-left: calc(var(--left-offset) + var(--tabs-left-shift)) !important;
233
+ margin-right: 18px !important;
234
+ border-bottom: 0; /* remove gray baseline */
235
+ padding-bottom: 6px;
236
  }}
237
+
238
+ /* Tab buttons */
239
+ div[data-baseweb="tab-list"] button[role="tab"],
240
+ .stTabs [role="tab"] {{
241
+ color: #374151; /* gray-700 */
242
+ background: transparent;
243
+ border: none;
244
+ outline: none;
245
+ padding: 6px 14px 10px 14px;
246
+ margin: 0 4px;
247
+ font-weight: 600;
248
  }}
 
249
 
250
+ /* Active tab: keep ONLY our single orange underline */
251
+ div[data-baseweb="tab-list"] button[aria-selected="true"],
252
+ .stTabs [role="tab"][aria-selected="true"] {{
253
+ color: var(--accent) !important;
254
+ border-bottom: 3px solid var(--accent) !important;
255
+ }}
256
+ /* Hide BaseWeb's moving highlight to avoid double orange lines */
257
+ div[data-baseweb="tab-highlight"] {{ display: none !important; }}
258
+
259
+ /* Small screens */
260
+ @media (max-width: 480px) {{
261
+ :root {{
262
+ --logo-height: {max(48, int(LOGO_HEIGHT_PX*0.7))}px;
263
+ --header-clear: 64px;
264
+ --tabs-left-shift: 16px;
265
+ }}
266
+ }}
267
  </style>
 
 
 
 
 
 
268
  """, unsafe_allow_html=True)
269
 
270
 
271
+
272
+
273
  # =========================
274
  # Small utilities
275
  # =========================
 
576
  log(f"CSV written: {out_csv}")
577
  return out_csv
578
 
579
+
580
+
581
+ # =========================
582
+ # Same-tab download (no /media, no new tab)
583
+ # Styled like Streamlit buttons
584
+ # =========================
585
+ def _same_tab_download_button(label: str, data_bytes: bytes, file_name: str, mime: str = "text/csv", *, key: Optional[str] = None):
586
+ """
587
+ Streamlit-like download button that:
588
+ • hovers as white bg + red text/border
589
+ • turns solid red with white text while pressed
590
+ • downloads in the SAME TAB (Blob + programmatic click)
591
+ """
592
+ import html, hashlib, base64
593
+ import streamlit as st
594
+ import streamlit.components.v1 as components
595
+
596
+ b64 = base64.b64encode(data_bytes).decode("ascii")
597
+ btn_id = f"dl_{(key or file_name)}_{hashlib.sha256((key or file_name).encode()).hexdigest()[:8]}"
598
+
599
+ # CSS: normal -> hover -> pressed (solid red)
600
+ st.markdown(f"""
601
+ <style>
602
+ a#{btn_id} {{
603
+ appearance: none;
604
+ display: inline-flex; align-items: center; justify-content: center;
605
+ padding: 0.5rem 0.75rem;
606
+ border-radius: 0.5rem;
607
+ border: 1px solid rgba(49,51,63,.2);
608
+ background: var(--background-color);
609
+ color: var(--text-color);
610
+ font-weight: 600; text-decoration: none !important;
611
+ box-shadow: 0 1px 2px rgba(0,0,0,0.04);
612
+ transition: color .15s ease, border-color .15s ease,
613
+ box-shadow .15s ease, transform .05s ease, background-color .15s;
614
+ cursor: pointer;
615
+ user-select: none;
616
+ -webkit-tap-highlight-color: transparent;
617
+ }}
618
+ /* Hover: white bg, red border/text */
619
+ a#{btn_id}:hover, a#{btn_id}:focus {{
620
+ background: var(--background-color);
621
+ color: var(--accent) !important;
622
+ border-color: var(--accent) !important;
623
+ box-shadow: 0 2px 6px rgba(239,68,68,0.20);
624
+ transform: translateY(-1px);
625
+ }}
626
+ /* Active/pressed: solid red with white text */
627
+ a#{btn_id}:active,
628
+ a#{btn_id}.pressed {{
629
+ background: var(--accent) !important;
630
+ border-color: var(--accent) !important;
631
+ color: #fff !important;
632
+ box-shadow: 0 3px 10px rgba(239,68,68,0.35);
633
+ transform: translateY(0);
634
+ }}
635
+ a#{btn_id}:focus-visible {{
636
+ outline: none;
637
+ box-shadow: 0 0 0 0.2rem rgba(239,68,68,0.35);
638
+ }}
639
+ </style>
640
+ """, unsafe_allow_html=True)
641
+
642
+ # Render the button (no navigation in href)
643
+ st.markdown(
644
+ f'<a id="{btn_id}" href="#" '
645
+ f' data-b64="{b64}" data-mime="{html.escape(mime)}" data-fname="{html.escape(file_name)}">{html.escape(label)}</a>',
646
+ unsafe_allow_html=True
647
+ )
648
+
649
+ # JS: add a temporary "pressed" class on mousedown/touch, then same-tab download via Blob
650
+ components.html(f"""
651
+ <script>
652
+ (function () {{
653
+ try {{
654
+ const doc = window.parent.document;
655
+ const a = doc.getElementById("{btn_id}");
656
+ if (!a) return;
657
+
658
+ const pressOn = () => a.classList.add("pressed");
659
+ const pressOff = () => a.classList.remove("pressed");
660
+
661
+ a.addEventListener("mousedown", pressOn, true);
662
+ a.addEventListener("mouseup", pressOff, true);
663
+ a.addEventListener("mouseleave",pressOff, true);
664
+ a.addEventListener("touchstart",pressOn, {{passive:true}});
665
+ a.addEventListener("touchend", pressOff, true);
666
+ a.addEventListener("touchcancel",pressOff, true);
667
+
668
+ a.addEventListener("click", function(ev) {{
669
+ ev.preventDefault();
670
+ ev.stopImmediatePropagation();
671
+
672
+ const b64 = a.getAttribute("data-b64");
673
+ const mime = a.getAttribute("data-mime") || "application/octet-stream";
674
+ const fname = a.getAttribute("data-fname") || "download";
675
+
676
+ // base64 → Blob
677
+ const bstr = atob(b64);
678
+ const len = bstr.length;
679
+ const u8 = new Uint8Array(len);
680
+ for (let i = 0; i < len; i++) u8[i] = bstr.charCodeAt(i);
681
+ const blob = new Blob([u8], {{ type: mime }});
682
+ const url = URL.createObjectURL(blob);
683
+
684
+ // programmatic same-tab download
685
+ const tmp = doc.createElement("a");
686
+ tmp.href = url;
687
+ tmp.download = fname;
688
+ tmp.style.display = "none";
689
+ doc.body.appendChild(tmp);
690
+ tmp.click();
691
+
692
+ // keep the red state briefly so it's visible, then clean up
693
+ setTimeout(() => {{
694
+ URL.revokeObjectURL(url);
695
+ tmp.remove();
696
+ pressOff();
697
+ }}, 150);
698
+ }}, true);
699
+ }} catch (err) {{
700
+ console.debug("download handler error:", err);
701
+ }}
702
+ }})();
703
+ </script>
704
+ """, height=0)
705
+
706
+
707
+
708
+
709
+
710
  # =========================
711
  # Per-file processing
712
  # =========================
 
766
  per_frame_df.insert(0, "Patient_ID", pid)
767
  per_frame_rows_acc.append(per_frame_df)
768
 
769
+ # -------- Append summary row BEFORE any UI drawing
770
  rows_acc.append({
771
  'Patient_ID': pid,
772
  'EDV_uL': EDV_uL,
 
798
  st.set_page_config(layout="wide", initial_sidebar_state="collapsed")
799
  _inject_layout_css()
800
 
801
+ # ---- Fixed, far-left logo; spacer prevents overlap with tabs ----
802
+ st.markdown(
803
+ f'''
804
+ <div id="fixed-edge-logo" aria-hidden="true" role="presentation">
805
+ <img src="{LOGO_URL}" alt="Pre-Clinical Cardiac MRI Segmentation">
806
+ </div>
807
+ <div class="edge-logo-spacer"></div>
808
+ ''',
809
+ unsafe_allow_html=True,
810
+ )
811
 
812
+ # ---------- REAL tabs ----------
813
+ tab1, tab2, tab3 = st.tabs(["Segmentation App", "Dataset", "NIfTI converter"])
814
+
815
+
816
+
817
+ # ===== Tab 1: Segmentation App =====
818
+ with tab1:
819
+ # ---------- REMOVE PAPERCLIP ----------
820
+ st.markdown(
821
+ """
822
+ <style>
823
+ /* Hide Streamlit's hover anchor/paperclip on all headings */
824
+ [data-testid="stHeading"] a,
825
+ h1 a[href^="#"],
826
+ h2 a[href^="#"],
827
+ h3 a[href^="#"] {
828
+ display: none !important;
829
+ visibility: hidden !important;
830
+ }
831
+ </style>
832
+ """,
833
+ unsafe_allow_html=True
834
+ )
835
+
836
+
837
+ # ---------- HERO ----------
838
+ HERO_HTML = dedent("""\
839
  <div class="content-wrap">
840
  <div class="measure-wrap">
841
+ <div class="text-wrap">
842
+ <h1 class="hero-title">
843
+ Open-Source Pre-Clinical Image Segmentation:<br>
844
+ Mouse cardiac MRI datasets with a deep learning segmentation framework
845
+ </h1>
 
 
846
  </div>
847
  <div class="text-wrap">
848
+ <p>We present the first publicly-available pre-clinical cardiac MRI dataset, along with an open-source DL segmentation model (both available on GitHub:
849
+ <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>
850
+ <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>
851
+ <p>Using this resource, we developed an open-source DL segmentation model based on the UNet3+ architecture.</p>
852
+ <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>
853
+ <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>
854
+ <p class="note-text">(Note: This Hugging Face model was developed as part of a manuscript submitted to the <em>Journal of Cardiovascular Magnetic Resonance</em>)</p>
855
  </div>
856
  </div>
857
  </div>
858
+ """)
859
+ st.markdown(HERO_HTML, unsafe_allow_html=True)
860
+
861
+
862
+
863
+
864
+ # HERO_HTML = dedent("""
865
+ # <div class="content-wrap">
866
+ # <div class="measure-wrap">
867
+ # <div class="hero-wrap">
868
+ # <h1 class="hero-title">
869
+ # Open-Source Pre-Clinical Image Segmentation:<br>
870
+ # Mouse cardiac MRI datasets with a deep learning segmentation framework
871
+ # </h1>
872
+ # </div>
873
+ # <div class="text-wrap">
874
+ # <p>We present the first publicly-available pre-clinical cardiac MRI dataset, along with an open-source DL segmentation model (both available on GitHub:
875
+ # <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>
876
+ # <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>
877
+ # <p>Using this resource, we developed an open-source DL segmentation model based on the UNet3+ architecture.</p>
878
+ # <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>
879
+ # <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>
880
+ # <p>
881
+ # 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.
882
+ # <br>
883
+ # (Note: This Hugging Face model was developed as part of a manuscript submitted to the <em>Journal of Cardiovascular Magnetic Resonance</em>).
884
+ # </p>
885
+ # </div>
886
+ # </div>
887
+ # </div>
888
+ # """)
889
+ # st.markdown(HERO_HTML, unsafe_allow_html=True)
890
+
891
+ # ---------- DATA UPLOAD (aligned) ----------
892
+ st.markdown('<div class="content-wrap"><div class="measure-wrap" id="upload-wrap">', unsafe_allow_html=True)
893
+ st.markdown(
894
+ """
895
+ <h2 style='margin-bottom:0.2rem;'>
896
+ Data Upload <span style='font-size:33px;'>📤</span>
897
+ </h2>
898
+ """,
899
+ unsafe_allow_html=True
900
+ )
901
+ uploaded_zip = st.file_uploader(
902
+ "Upload ZIP of NIfTI files 🐭",
903
+ type="zip",
904
+ label_visibility="visible"
905
+ )
906
+
907
+
908
+ st.markdown(
909
+ """
910
+ <p style="margin-top:0.3rem; font-size:15px; color:#444;">
911
+ Or download our <a href="https://huggingface.co/spaces/mrphys/Pre-clinical_DL_segmentation/tree/main/NIfTI_dataset" target="_blank" rel="noopener">
912
+ sample NIfTI dataset</a> to try it out!
913
+ </p>
914
+ """,
915
+ unsafe_allow_html=True
916
+ )
917
+
918
+ st.markdown('</div></div>', unsafe_allow_html=True)
919
+
920
+ # ---- Clear stale CSV when a new ZIP is picked ----
921
+ if uploaded_zip is not None:
922
+ if st.session_state.get("_last_zip_name") != uploaded_zip.name:
923
+ st.session_state.pop("csv_bytes", None)
924
+ st.session_state.pop("csv_name", None)
925
+ st.session_state.pop("rows_count", None)
926
+ st.session_state.pop("_dl_token", None) # reset download-button identity
927
+ st.session_state["_last_zip_name"] = uploaded_zip.name
928
+
929
+ # ---- Extract helper ----
930
+ def extract_zip(zip_path, extract_to):
931
+ os.makedirs(extract_to, exist_ok=True)
932
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
933
+ valid_files = [
934
+ f for f in zip_ref.namelist()
935
+ if "__MACOSX" not in f and not os.path.basename(f).startswith("._")
936
+ ]
937
+ zip_ref.extractall(extract_to, members=valid_files)
938
+
939
+ # ---------- PROCESS ----------
940
+ if uploaded_zip and st.button("Process Data"):
941
+ zip_label = uploaded_zip.name or "ZIP"
942
+ with st.spinner(f"Processing {zip_label}..."):
943
+ tmpdir = tempfile.mkdtemp()
944
+ zpath = os.path.join(tmpdir, uploaded_zip.name)
945
+ with open(zpath, "wb") as f:
946
+ f.write(uploaded_zip.read())
947
+ extract_zip(zpath, tmpdir)
948
+
949
+ # Find NIfTI files inside ZIP
950
+ nii_files: List[str] = []
951
+ for root, _, files in os.walk(tmpdir):
952
+ for fn in files:
953
+ low = fn.lower()
954
+ if low.endswith(".nii") or low.endswith(".nii.gz"):
955
+ nii_files.append(os.path.join(root, fn))
956
+
957
+ if not nii_files:
958
+ st.error("No NIfTI files (.nii / .nii.gz) found in the uploaded ZIP.")
959
+ else:
960
+ # Load model
961
+ model = keras.models.load_model(
962
+ MODEL_PATH,
963
+ custom_objects={
964
+ 'focal_tversky_loss': None,
965
+ 'dice_coef_no_bkg': None,
966
+ 'ResizeAndConcatenate': ResizeAndConcatenate,
967
+ 'dice_myo': None,
968
+ 'dice_blood': None,
969
+ 'dice': None
970
+ },
971
+ compile=False
972
+ )
973
+ log("Model loaded.")
974
+
975
+ rows: List[Dict] = []
976
+ per_frame_rows: List[pd.DataFrame] = []
977
+
978
+ for fp in sorted(nii_files):
979
+ try:
980
+ process_nifti_case(fp, model, rows, per_frame_rows)
981
+ except Exception as e:
982
+ st.warning(f"Failed: {Path(fp).name} — {e}")
983
+
984
+ # ---- BELOW the GIF(s): write CSV & persist bytes/name ----
985
+ csv_path = write_all_in_one_csv(rows, per_frame_rows, CSV_DIR)
986
+ csv_download_name = f"{Path(zip_label).stem}_Results.csv"
987
+ with open(csv_path, "rb") as f:
988
+ csv_bytes = f.read()
989
+
990
+ st.session_state["csv_bytes"] = csv_bytes
991
+ st.session_state["csv_name"] = csv_download_name
992
+ st.session_state["rows_count"] = len(rows)
993
+
994
+ # ---- Re-render success + robust download on EVERY run (same tab, no 404) ----
995
+ if "csv_bytes" in st.session_state and "csv_name" in st.session_state:
996
+ st.success(f"Processed {st.session_state.get('rows_count', 0)} NIfTI file(s).")
997
+
998
+ # Use our same-tab data:URI button that looks like Streamlit's and turns red on hover
999
+ _same_tab_download_button(
1000
  label="Download CSV",
1001
+ data_bytes=st.session_state["csv_bytes"],
1002
+ file_name=st.session_state["csv_name"],
1003
  mime="text/csv",
1004
+ key="results"
1005
  )
1006
 
1007
 
1008
+
1009
+ # ===== Tab 2: Dataset =====
1010
+ with tab2:
1011
+ st.markdown(
1012
+ """
1013
+ <style>
1014
+ /* --- Full-width dark hero section --- */
1015
+ .ds-hero-section {
1016
+ background: #082c3a;
1017
+ padding: 30px 10px;
1018
+ text-align: center;
1019
+ margin-left: -100vw;
1020
+ margin-right: -100vw;
1021
+ left: 0; right: 0; position: relative;
1022
+ }
1023
+ .ds-hero-section-inner { max-width: 1100px; margin: 0 auto; }
1024
+
1025
+ /* --- Hero image (centered) --- */
1026
+ .ds-heroimg {
1027
+ max-width: 1000px; width: 100%; height: auto;
1028
+ border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.25);
1029
+ display: block; margin: 0 auto;
1030
+ }
1031
+
1032
+ /* --- Caption (light on dark) --- */
1033
+ .ds-caption {
1034
+ text-align: center; color: #e0f2f1;
1035
+ font-size: 18px; line-height: 1.5;
1036
+ margin: 14px 6px 0; font-style: italic;
1037
+ }
1038
+
1039
+ /* --- Thicker orange divider --- */
1040
+ .ds-hr {
1041
+ height: 8px;
1042
+ border: 0; background: #ea580c;
1043
+ margin: 24px 0 20px;
1044
+ border-radius: 3px;
1045
+ }
1046
+
1047
+ /* --- White background lower content --- */
1048
+ .ds-wrap {
1049
+ max-width: var(--content-measure, 920px);
1050
+ margin-left: var(--left-offset, 40px);
1051
+ margin-right: auto;
1052
+ background: #fff; padding: 16px 24px; border-radius: 6px;
1053
+ }
1054
+
1055
+ /* --- Section headers --- */
1056
+ .ds-section h2 {
1057
+ font-size: 20px; font-weight: 700;
1058
+ margin: 0 0 2px;
1059
+ color: #082c3a;
1060
+ }
1061
+
1062
+ /* --- Text content --- */
1063
+ .ds-section p {
1064
+ font-size: 16px; line-height: 1.6; color: #333;
1065
+ margin: 0 0 6px;
1066
+ }
1067
+ .ds-section ul {
1068
+ margin: 2px 0 8px 18px;
1069
+ padding: 0;
1070
+ }
1071
+ .ds-section li {
1072
+ font-size: 16px; line-height: 1.6; color: #333;
1073
+ margin-bottom: 10px;
1074
+ }
1075
+ .ds-section a {
1076
+ color: #0b66c3 !important; text-decoration: underline !important;
1077
+ }
1078
+
1079
+ /* --- Remove Streamlit paperclip/anchor on headings --- */
1080
+ h2 a, [data-testid="stHeading"] a { display: none !important; }
1081
+ </style>
1082
+
1083
+ <!-- Full-width dark top section -->
1084
+ <div class="ds-hero-section">
1085
+ <div class="ds-hero-section-inner">
1086
+ <img class="ds-heroimg"
1087
+ src="https://raw.githubusercontent.com/whanisa/Segmentation/main/icon/open_source.png"
1088
+ alt="Illustration of mouse with heart representing open-source pre-clinical cardiac MRI dataset" />
1089
+ <p class="ds-caption">
1090
+ The first publicly-available pre-clinical cardiac MRI dataset,<br/>
1091
+ with an open-source segmentation model and an easy-to-use web app.
1092
+ </p>
1093
+ </div>
1094
+ </div>
1095
+
1096
+ <hr class="ds-hr"/>
1097
+
1098
+ <!-- White lower content -->
1099
+ <div class="ds-wrap">
1100
+ <div class="ds-section">
1101
+ <h2>Repository & Paper Resources</h2>
1102
+ <p>GitHub:
1103
+ <a href="https://github.com/mrphys/Open-Source_Pre-Clinical_Segmentation.git" target="_blank">
1104
+ Open-Source_Pre-Clinical_Segmentation
1105
+ </a>
1106
+ </p>
1107
+
1108
+ <h2>📊 Dataset Availability</h2>
1109
+ <ul>
1110
+ <li>
1111
+ <strong>Full dataset (130 mice, HDF5 format):</strong><br/>
1112
+ Available in our
1113
+ <a href="https://github.com/mrphys/Open-Source_Pre-Clinical_Segmentation/tree/master/Data" target="_blank">
1114
+ GitHub repository
1115
+ </a>.<br/>
1116
+ Each .h5 file contains the complete cine SAX MRI and expert manual segmentations.
1117
+ </li>
1118
+ <li>
1119
+ <strong>Sample datasets (3 mice, NIfTI format):</strong><br/>
1120
+ Available here:
1121
+ <a href="https://huggingface.co/spaces/mrphys/Pre-clinical_DL_segmentation/tree/main/NIfTI_dataset" target="_blank">
1122
+ NIfTI Sample Dataset
1123
+ </a>.<br/>
1124
+ We provide 3 example NIfTI datasets for quick download and direct use within the app.
1125
+ </li>
1126
+ </ul>
1127
+ </div>
1128
+
1129
+ <hr class="ds-hr"/>
1130
+
1131
+ <div class="ds-section">
1132
+ <h2>Notes</h2>
1133
+ <ul>
1134
+ <li>Complete SAX cine MRI for 130 mice with expert LV blood & myocardium labels (ED/ES).</li>
1135
+ </ul>
1136
+ </div>
1137
+ </div>
1138
+ """,
1139
+ unsafe_allow_html=True
1140
+ )
1141
+
1142
+
1143
+
1144
+
1145
+ # ===== Tab 3: NIfTI converter =====
1146
+ with tab3:
1147
+ st.subheader("NIfTI converter")
1148
+ st.markdown(
1149
+ """
1150
+ **Working with Agilent data?**
1151
+ Easily convert your fid files to NIfTI using our **fid2niix**.
1152
+
1153
+ ```bash
1154
+ fid2niix -z y -o /path/to/out -f "%p_%s" /path/to/fid_folder
1155
+ ```
1156
+
1157
+ **💡 Tips**
1158
+ - Upload a ZIP file that includes both `fid` and `procpar`.
1159
+ - Conversion outputs **NIfTI-1** format, ready to use with our web app.
1160
+ """
1161
+ )
1162
+
1163
+
1164
+
1165
  if __name__ == "__main__":
1166
  main()