Updated Streamlit
Browse files- 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
|
| 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 =
|
| 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 =
|
| 159 |
if int(myo_erode_radius) > 0:
|
| 160 |
se = disk(int(myo_erode_radius))
|
| 161 |
-
myo_bin =
|
| 162 |
-
myo_bin =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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."
|