skarugu commited on
Commit
1f3fffe
Β·
verified Β·
1 Parent(s): 3633771

Updated Streamlit

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +173 -11
src/streamlit_app.py CHANGED
@@ -62,10 +62,42 @@ import matplotlib.patches as mpatches
62
  from huggingface_hub import hf_hub_download
63
 
64
  import scipy.ndimage as ndi
65
- from skimage.morphology import remove_small_objects, disk, closing, opening, binary_dilation, binary_erosion
66
  from skimage import measure
67
  from skimage.segmentation import watershed, find_boundaries
68
- from skimage.feature import peak_local_max
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
 
71
  # ─────────────────────────────────────────────────────────────────────────────
@@ -149,17 +181,17 @@ def postprocess_masks(nuc_mask, myo_mask,
149
  # Nuclei β€” closing fills small gaps, then size filter
150
  nuc_bin = nuc_mask.astype(bool)
151
  if int(nuc_close_radius) > 0:
152
- nuc_bin = closing(nuc_bin, disk(int(nuc_close_radius)))
153
  nuc_clean = remove_small_objects(nuc_bin, min_size=int(min_nuc_area)).astype(np.uint8)
154
 
155
  # Myotubes β€” opening + erode/dilate + size filter + shape filter
156
  myo_bin = myo_mask.astype(bool)
157
  if int(myo_open_radius) > 0:
158
- myo_bin = opening(myo_bin, disk(int(myo_open_radius)))
159
  if int(myo_erode_radius) > 0:
160
  se = disk(int(myo_erode_radius))
161
- myo_bin = binary_erosion(myo_bin, se)
162
- myo_bin = binary_dilation(myo_bin, se) # re-dilate to restore size
163
  myo_bin = remove_small_objects(myo_bin, min_size=int(min_myo_area))
164
  if float(min_myo_aspect_ratio) > 0:
165
  myo_bin = _filter_by_aspect_ratio(myo_bin, float(min_myo_aspect_ratio))
@@ -366,7 +398,7 @@ def classify_nucleus_in_myotube(nuc_coords: np.ndarray,
366
  nuc_footprint = np.zeros((H, W), dtype=bool)
367
  nuc_footprint[rows, cols] = True
368
 
369
- nuc_dilated = binary_dilation(nuc_footprint, footprint=disk(ring_width))
370
  ring_mask = nuc_dilated & ~nuc_footprint & myo_mask_full.astype(bool)
371
 
372
  if ring_mask.sum() < 4:
@@ -596,14 +628,14 @@ def make_outline_overlay(rgb_u8: np.ndarray,
596
  if myo_disp.max() > 0:
597
  myo_bounds = find_boundaries(myo_disp, mode='outer')
598
  if line_width > 1:
599
- myo_bounds = binary_dilation(myo_bounds, footprint=disk(line_width - 1))
600
  out[myo_bounds] = myo_color
601
 
602
  # Nuclei outlines
603
  if nuc_disp.max() > 0:
604
  nuc_bounds = find_boundaries(nuc_disp, mode='outer')
605
  if line_width > 1:
606
- nuc_bounds = binary_dilation(nuc_bounds, footprint=disk(max(line_width - 2, 0)))
607
  out[nuc_bounds] = nuc_color
608
 
609
  return out
@@ -1551,7 +1583,133 @@ h1,h2,h3,h4 { color:#90caf9; }
1551
  """, unsafe_allow_html=True)
1552
 
1553
  st.title("πŸ”¬ MyoSeg β€” Myotube & Nuclei Analyser")
1554
- device = "cuda" if torch.cuda.is_available() else "cpu"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1555
 
1556
  # ─────────────────────────────────────────────────────────────────────────────
1557
  # SIDEBAR
@@ -1746,7 +1904,11 @@ with st.sidebar:
1746
 
1747
 
1748
  # ─────────────────────────────────────────────────────────────────────────────
1749
- # FILE UPLOADER
 
 
 
 
1750
  # ─────────────────────────────────────────────────────────────────────────────
1751
  _uploader_label = (
1752
  "Upload 1+ images (png / jpg / tif). Any filename is accepted. πŸ”’ Private mode is ON β€” images will not be stored."
 
62
  from huggingface_hub import hf_hub_download
63
 
64
  import scipy.ndimage as ndi
65
+ from skimage.morphology import remove_small_objects, disk
66
  from skimage import measure
67
  from skimage.segmentation import watershed, find_boundaries
68
+ from skimage.feature import peak_local_max
69
+
70
+
71
+ def _as_bool_c_array(mask) -> np.ndarray:
72
+ return np.ascontiguousarray(np.asarray(mask, dtype=bool))
73
+
74
+
75
+ def _safe_binary_dilation(mask, footprint):
76
+ return ndi.binary_dilation(
77
+ _as_bool_c_array(mask),
78
+ structure=np.asarray(footprint, dtype=bool),
79
+ )
80
+
81
+
82
+ def _safe_binary_erosion(mask, footprint):
83
+ return ndi.binary_erosion(
84
+ _as_bool_c_array(mask),
85
+ structure=np.asarray(footprint, dtype=bool),
86
+ )
87
+
88
+
89
+ def _safe_binary_opening(mask, footprint):
90
+ return ndi.binary_opening(
91
+ _as_bool_c_array(mask),
92
+ structure=np.asarray(footprint, dtype=bool),
93
+ )
94
+
95
+
96
+ def _safe_binary_closing(mask, footprint):
97
+ return ndi.binary_closing(
98
+ _as_bool_c_array(mask),
99
+ structure=np.asarray(footprint, dtype=bool),
100
+ )
101
 
102
 
103
  # ─────────────────────────────────────────────────────────────────────────────
 
181
  # Nuclei β€” closing fills small gaps, then size filter
182
  nuc_bin = nuc_mask.astype(bool)
183
  if int(nuc_close_radius) > 0:
184
+ nuc_bin = _safe_binary_closing(nuc_bin, disk(int(nuc_close_radius)))
185
  nuc_clean = remove_small_objects(nuc_bin, min_size=int(min_nuc_area)).astype(np.uint8)
186
 
187
  # Myotubes β€” opening + erode/dilate + size filter + shape filter
188
  myo_bin = myo_mask.astype(bool)
189
  if int(myo_open_radius) > 0:
190
+ myo_bin = _safe_binary_opening(myo_bin, disk(int(myo_open_radius)))
191
  if int(myo_erode_radius) > 0:
192
  se = disk(int(myo_erode_radius))
193
+ myo_bin = _safe_binary_erosion(myo_bin, se)
194
+ myo_bin = _safe_binary_dilation(myo_bin, se) # re-dilate to restore size
195
  myo_bin = remove_small_objects(myo_bin, min_size=int(min_myo_area))
196
  if float(min_myo_aspect_ratio) > 0:
197
  myo_bin = _filter_by_aspect_ratio(myo_bin, float(min_myo_aspect_ratio))
 
398
  nuc_footprint = np.zeros((H, W), dtype=bool)
399
  nuc_footprint[rows, cols] = True
400
 
401
+ nuc_dilated = _safe_binary_dilation(nuc_footprint, disk(ring_width))
402
  ring_mask = nuc_dilated & ~nuc_footprint & myo_mask_full.astype(bool)
403
 
404
  if ring_mask.sum() < 4:
 
628
  if myo_disp.max() > 0:
629
  myo_bounds = find_boundaries(myo_disp, mode='outer')
630
  if line_width > 1:
631
+ myo_bounds = _safe_binary_dilation(myo_bounds, disk(line_width - 1))
632
  out[myo_bounds] = myo_color
633
 
634
  # Nuclei outlines
635
  if nuc_disp.max() > 0:
636
  nuc_bounds = find_boundaries(nuc_disp, mode='outer')
637
  if line_width > 1:
638
+ nuc_bounds = _safe_binary_dilation(nuc_bounds, disk(max(line_width - 2, 0)))
639
  out[nuc_bounds] = nuc_color
640
 
641
  return out
 
1583
  """, unsafe_allow_html=True)
1584
 
1585
  st.title("πŸ”¬ MyoSeg β€” Myotube & Nuclei Analyser")
1586
+ device = "cuda" if torch.cuda.is_available() else "cpu"
1587
+
1588
+ V2_RUN_DIR = Path("runs_v2")
1589
+ V2_PID_PATH = V2_RUN_DIR / "train_v2.pid"
1590
+ V2_LOG_PATH = V2_RUN_DIR / "train_v2_latest.log"
1591
+
1592
+
1593
+ def _pid_is_running(pid: int) -> bool:
1594
+ if pid <= 0:
1595
+ return False
1596
+ try:
1597
+ os.kill(pid, 0)
1598
+ return True
1599
+ except OSError:
1600
+ return False
1601
+
1602
+
1603
+ def _v2_training_status():
1604
+ if not V2_PID_PATH.exists():
1605
+ return False, None
1606
+ try:
1607
+ pid = int(V2_PID_PATH.read_text().strip())
1608
+ except Exception:
1609
+ return False, None
1610
+ return _pid_is_running(pid), pid
1611
+
1612
+
1613
+ def _start_v2_training(epochs: int, batch_size: int, base: int, image_size: int):
1614
+ V2_RUN_DIR.mkdir(parents=True, exist_ok=True)
1615
+ cmd = [
1616
+ sys.executable,
1617
+ "train_model_v2.py",
1618
+ "--data_root", "data",
1619
+ "--corrections_dir", "corrections",
1620
+ "--out_dir", str(V2_RUN_DIR),
1621
+ "--epochs", str(int(epochs)),
1622
+ "--batch_size", str(int(batch_size)),
1623
+ "--image_size", str(int(image_size)),
1624
+ "--base", str(int(base)),
1625
+ "--push_to_hub",
1626
+ "--hf_repo", MODEL_REPO_ID,
1627
+ "--hf_filename", "model_v2.pt",
1628
+ ]
1629
+ env = os.environ.copy()
1630
+ env["PYTHONUNBUFFERED"] = "1"
1631
+ with V2_LOG_PATH.open("ab", buffering=0) as log_f:
1632
+ log_f.write(
1633
+ f"\n\n=== MyoSeg v2 training started {datetime.now().isoformat()} ===\n".encode()
1634
+ )
1635
+ log_f.write(("Command: " + " ".join(cmd) + "\n\n").encode())
1636
+ proc = subprocess.Popen(
1637
+ cmd,
1638
+ stdout=log_f,
1639
+ stderr=subprocess.STDOUT,
1640
+ cwd=".",
1641
+ env=env,
1642
+ )
1643
+ V2_PID_PATH.write_text(str(proc.pid))
1644
+ return proc.pid
1645
+
1646
+
1647
+ def render_v2_training_panel(image_size: int, key_prefix: str = "v2_top"):
1648
+ train_script = Path("train_model_v2.py")
1649
+ has_script = train_script.exists()
1650
+ has_token = bool(os.environ.get("HF_TOKEN"))
1651
+ running, pid = _v2_training_status()
1652
+ latest_model = V2_RUN_DIR / "model_v2.pt"
1653
+ latest_metrics = V2_RUN_DIR / "metrics_v2.json"
1654
+
1655
+ s1, s2, s3 = st.columns(3)
1656
+ s1.metric("train_model_v2.py", "Found" if has_script else "Missing")
1657
+ s2.metric("HF_TOKEN", "Ready" if has_token else "Missing")
1658
+ s3.metric("v2 status", f"Running pid {pid}" if running else "Idle")
1659
+
1660
+ if not has_script:
1661
+ st.error("Upload train_model_v2.py to the Space before starting v2 training.")
1662
+ if not has_token:
1663
+ st.warning("Add HF_TOKEN as a Space secret with write access before training.")
1664
+
1665
+ c1, c2, c3 = st.columns(3)
1666
+ with c1:
1667
+ v2_epochs = st.number_input(
1668
+ "v2 epochs", min_value=5, max_value=200, value=80, step=5,
1669
+ key=f"{key_prefix}_epochs",
1670
+ )
1671
+ with c2:
1672
+ v2_batch = st.selectbox("v2 batch size", [2, 4], index=0, key=f"{key_prefix}_batch")
1673
+ with c3:
1674
+ v2_base = st.selectbox("v2 base width", [24, 32], index=1, key=f"{key_prefix}_base")
1675
+
1676
+ st.caption(
1677
+ "This trains attention_resunet_v2 on data/images plus data/masks/Nuclei_m "
1678
+ "and data/masks/Myotubes_m, then uploads model_v2.pt to the model repo."
1679
+ )
1680
+
1681
+ start_disabled = running or not has_script or not has_token
1682
+ if st.button("Start v2 training now", type="primary", disabled=start_disabled, key=f"{key_prefix}_start"):
1683
+ new_pid = _start_v2_training(
1684
+ epochs=int(v2_epochs),
1685
+ batch_size=int(v2_batch),
1686
+ base=int(v2_base),
1687
+ image_size=int(image_size),
1688
+ )
1689
+ st.success(f"v2 training started in the background. Process id: {new_pid}")
1690
+ st.info("Refresh this page to update status. Watch the log preview below for progress.")
1691
+
1692
+ if running:
1693
+ st.info("v2 training is currently running. Avoid restarting the Space until it finishes.")
1694
+
1695
+ if latest_model.exists():
1696
+ st.success(f"Local v2 model exists: {latest_model}")
1697
+ if latest_metrics.exists():
1698
+ try:
1699
+ st.json(json.loads(latest_metrics.read_text()))
1700
+ except Exception:
1701
+ st.caption(f"Metrics file exists: {latest_metrics}")
1702
+
1703
+ if V2_LOG_PATH.exists():
1704
+ log_text = V2_LOG_PATH.read_text(errors="replace")
1705
+ st.download_button(
1706
+ "Download v2 training log",
1707
+ data=log_text,
1708
+ file_name="train_v2_latest.log",
1709
+ mime="text/plain",
1710
+ key=f"{key_prefix}_download_log",
1711
+ )
1712
+ st.text_area("Latest v2 training log", log_text[-6000:], height=260, key=f"{key_prefix}_log")
1713
 
1714
  # ─────────────────────────────────────────────────────────────────────────────
1715
  # SIDEBAR
 
1904
 
1905
 
1906
  # ─────────────────────────────────────────────────────────────────────────────
1907
+ with st.expander("Train v2 model", expanded=True):
1908
+ render_v2_training_panel(image_size=int(image_size), key_prefix="v2_top")
1909
+
1910
+
1911
+ # FILE UPLOADER
1912
  # ─────────────────────────────────────────────────────────────────────────────
1913
  _uploader_label = (
1914
  "Upload 1+ images (png / jpg / tif). Any filename is accepted. πŸ”’ Private mode is ON β€” images will not be stored."