Fix file path and replace streamlit_app.py with v3
Browse files- src/streamlit_app.py +151 -47
src/streamlit_app.py
CHANGED
|
@@ -97,12 +97,19 @@ def hex_to_rgb(h: str):
|
|
| 97 |
|
| 98 |
def postprocess_masks(nuc_mask, myo_mask,
|
| 99 |
min_nuc_area=20, min_myo_area=500,
|
| 100 |
-
myo_close_radius=3):
|
| 101 |
-
"""
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
|
|
|
| 106 |
selem = disk(int(myo_close_radius))
|
| 107 |
myo_bin = closing(myo_mask.astype(bool), selem)
|
| 108 |
myo_bin = opening(myo_bin, selem)
|
|
@@ -231,73 +238,127 @@ def make_instance_overlay(rgb_u8: np.ndarray,
|
|
| 231 |
label_nuclei: bool = True,
|
| 232 |
label_myotubes: bool = True) -> np.ndarray:
|
| 233 |
"""
|
| 234 |
-
Per-instance coloured overlay rendered
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
"""
|
| 239 |
orig_h, orig_w = rgb_u8.shape[:2]
|
| 240 |
nuc_cmap = plt.cm.get_cmap("cool")
|
| 241 |
myo_cmap = plt.cm.get_cmap("autumn")
|
| 242 |
|
|
|
|
| 243 |
def _resize_lab(lab, h, w):
|
| 244 |
-
return np.array(
|
|
|
|
|
|
|
| 245 |
|
| 246 |
nuc_disp = _resize_lab(nuc_lab, orig_h, orig_w)
|
| 247 |
myo_disp = _resize_lab(myo_lab, orig_h, orig_w)
|
| 248 |
-
base = rgb_u8.astype(np.float32).copy()
|
| 249 |
-
n_myo = int(myo_disp.max())
|
| 250 |
n_nuc = int(nuc_disp.max())
|
|
|
|
| 251 |
|
|
|
|
|
|
|
| 252 |
if n_myo > 0:
|
| 253 |
myo_norm = (myo_disp / max(n_myo, 1)).astype(np.float32)
|
| 254 |
myo_rgba = (myo_cmap(myo_norm)[:, :, :3] * 255).astype(np.float32)
|
| 255 |
mask = myo_disp > 0
|
| 256 |
base[mask] = (1 - alpha) * base[mask] + alpha * myo_rgba[mask]
|
| 257 |
-
|
| 258 |
if n_nuc > 0:
|
| 259 |
nuc_norm = (nuc_disp / max(n_nuc, 1)).astype(np.float32)
|
| 260 |
nuc_rgba = (nuc_cmap(nuc_norm)[:, :, :3] * 255).astype(np.float32)
|
| 261 |
mask = nuc_disp > 0
|
| 262 |
base[mask] = (1 - alpha) * base[mask] + alpha * nuc_rgba[mask]
|
| 263 |
-
|
| 264 |
overlay = np.clip(base, 0, 255).astype(np.uint8)
|
| 265 |
|
| 266 |
-
|
| 267 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
ax.imshow(overlay)
|
|
|
|
|
|
|
| 269 |
ax.axis("off")
|
| 270 |
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
|
|
|
| 275 |
|
|
|
|
| 276 |
if label_nuclei:
|
| 277 |
for prop in measure.regionprops(nuc_lab):
|
| 278 |
r, c = prop.centroid
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
|
|
|
|
| 284 |
if label_myotubes:
|
| 285 |
for prop in measure.regionprops(myo_lab):
|
| 286 |
r, c = prop.centroid
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
|
|
|
|
| 292 |
patches = [
|
| 293 |
mpatches.Patch(color=nuc_cmap(0.7), label=f"Nuclei (n={n_nuc})"),
|
| 294 |
mpatches.Patch(color=myo_cmap(0.7), label=f"Myotubes (n={n_myo})"),
|
| 295 |
]
|
| 296 |
-
ax.legend(
|
| 297 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
|
| 299 |
fig.tight_layout(pad=0)
|
| 300 |
buf = io.BytesIO()
|
|
|
|
| 301 |
fig.savefig(buf, format="png", bbox_inches="tight", pad_inches=0, dpi=dpi)
|
| 302 |
plt.close(fig)
|
| 303 |
buf.seek(0)
|
|
@@ -466,6 +527,7 @@ with st.sidebar:
|
|
| 466 |
st.header("Postprocessing")
|
| 467 |
min_nuc_area = st.number_input("Min nucleus area (px)", 0, 10000, 20, 1)
|
| 468 |
min_myo_area = st.number_input("Min myotube area (px)", 0, 200000, 500, 10)
|
|
|
|
| 469 |
myo_close_radius = st.number_input("Myotube close radius", 0, 50, 3, 1)
|
| 470 |
|
| 471 |
st.header("Watershed (nuclei splitting)")
|
|
@@ -568,6 +630,7 @@ if run:
|
|
| 568 |
nuc_raw, myo_raw,
|
| 569 |
min_nuc_area=int(min_nuc_area),
|
| 570 |
min_myo_area=int(min_myo_area),
|
|
|
|
| 571 |
myo_close_radius=int(myo_close_radius),
|
| 572 |
)
|
| 573 |
|
|
@@ -576,16 +639,33 @@ if run:
|
|
| 576 |
rgb_u8, nuc_pp, myo_pp, nuc_rgb, myo_rgb, float(alpha)
|
| 577 |
)
|
| 578 |
|
| 579 |
-
#
|
| 580 |
nuc_lab = label_nuclei_watershed(nuc_pp,
|
| 581 |
min_distance=int(nuc_ws_min_dist),
|
| 582 |
min_nuc_area=int(nuc_ws_min_area))
|
| 583 |
myo_lab = label_cc(myo_pp)
|
|
|
|
|
|
|
| 584 |
inst_ov = make_instance_overlay(rgb_u8, nuc_lab, myo_lab,
|
| 585 |
alpha=float(alpha),
|
| 586 |
label_nuclei=label_nuc,
|
| 587 |
label_myotubes=label_myo)
|
| 588 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 589 |
bio = compute_bio_metrics(
|
| 590 |
nuc_pp, myo_pp,
|
| 591 |
nuc_ws_min_distance=int(nuc_ws_min_dist),
|
|
@@ -598,19 +678,23 @@ if run:
|
|
| 598 |
all_bio_metrics[name] = {**bio, "_per_myotube_areas": per_areas}
|
| 599 |
|
| 600 |
artifacts[name] = {
|
| 601 |
-
"original"
|
| 602 |
-
"overlay"
|
| 603 |
-
"
|
| 604 |
-
"
|
|
|
|
|
|
|
| 605 |
}
|
| 606 |
|
| 607 |
# ZIP contents
|
| 608 |
-
zf.writestr(f"{name}/
|
| 609 |
-
zf.writestr(f"{name}/
|
| 610 |
-
zf.writestr(f"{name}/
|
| 611 |
-
zf.writestr(f"{name}/
|
| 612 |
-
zf.writestr(f"{name}/
|
| 613 |
-
zf.writestr(f"{name}/
|
|
|
|
|
|
|
| 614 |
|
| 615 |
prog.progress((i + 1) / len(uploads))
|
| 616 |
|
|
@@ -663,13 +747,33 @@ pick = st.selectbox("Select image", names)
|
|
| 663 |
col_img, col_metrics = st.columns([3, 2], gap="large")
|
| 664 |
|
| 665 |
with col_img:
|
| 666 |
-
tabs = st.tabs([
|
| 667 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
FIXED_W = 700
|
| 669 |
-
with tabs[0]:
|
| 670 |
-
|
| 671 |
-
with tabs[
|
| 672 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 673 |
|
| 674 |
with col_metrics:
|
| 675 |
st.markdown("#### π Live metrics")
|
|
|
|
| 97 |
|
| 98 |
def postprocess_masks(nuc_mask, myo_mask,
|
| 99 |
min_nuc_area=20, min_myo_area=500,
|
| 100 |
+
nuc_close_radius=2, myo_close_radius=3):
|
| 101 |
+
"""
|
| 102 |
+
Clean up raw predicted masks.
|
| 103 |
+
Nuclei: optional closing to fill gaps, then remove small objects.
|
| 104 |
+
Myotubes: closing + opening to smooth edges, then remove small objects.
|
| 105 |
+
"""
|
| 106 |
+
# Nuclei
|
| 107 |
+
nuc_bin = nuc_mask.astype(bool)
|
| 108 |
+
if int(nuc_close_radius) > 0:
|
| 109 |
+
nuc_bin = closing(nuc_bin, disk(int(nuc_close_radius)))
|
| 110 |
+
nuc_clean = remove_small_objects(nuc_bin, min_size=int(min_nuc_area)).astype(np.uint8)
|
| 111 |
|
| 112 |
+
# Myotubes
|
| 113 |
selem = disk(int(myo_close_radius))
|
| 114 |
myo_bin = closing(myo_mask.astype(bool), selem)
|
| 115 |
myo_bin = opening(myo_bin, selem)
|
|
|
|
| 238 |
label_nuclei: bool = True,
|
| 239 |
label_myotubes: bool = True) -> np.ndarray:
|
| 240 |
"""
|
| 241 |
+
Per-instance coloured overlay rendered at high DPI so labels stay sharp
|
| 242 |
+
when the image is zoomed in.
|
| 243 |
+
|
| 244 |
+
Nuclei β cool colourmap, white numeric IDs on solid dark-blue backing.
|
| 245 |
+
Myotubes β autumn colourmap, white M1/M2β¦ IDs on solid dark-red backing.
|
| 246 |
+
|
| 247 |
+
Font sizes are fixed in data-space pixels so they look the same regardless
|
| 248 |
+
of image resolution. Myotube labels are always 3Γ bigger than nucleus
|
| 249 |
+
labels so the two tiers are visually distinct at any zoom level.
|
| 250 |
"""
|
| 251 |
orig_h, orig_w = rgb_u8.shape[:2]
|
| 252 |
nuc_cmap = plt.cm.get_cmap("cool")
|
| 253 |
myo_cmap = plt.cm.get_cmap("autumn")
|
| 254 |
|
| 255 |
+
# ββ resize label maps to original image resolution βββββββββββββββββββββββ
|
| 256 |
def _resize_lab(lab, h, w):
|
| 257 |
+
return np.array(
|
| 258 |
+
Image.fromarray(lab.astype(np.int32)).resize((w, h), Image.NEAREST)
|
| 259 |
+
)
|
| 260 |
|
| 261 |
nuc_disp = _resize_lab(nuc_lab, orig_h, orig_w)
|
| 262 |
myo_disp = _resize_lab(myo_lab, orig_h, orig_w)
|
|
|
|
|
|
|
| 263 |
n_nuc = int(nuc_disp.max())
|
| 264 |
+
n_myo = int(myo_disp.max())
|
| 265 |
|
| 266 |
+
# ββ colour the mask regions βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 267 |
+
base = rgb_u8.astype(np.float32).copy()
|
| 268 |
if n_myo > 0:
|
| 269 |
myo_norm = (myo_disp / max(n_myo, 1)).astype(np.float32)
|
| 270 |
myo_rgba = (myo_cmap(myo_norm)[:, :, :3] * 255).astype(np.float32)
|
| 271 |
mask = myo_disp > 0
|
| 272 |
base[mask] = (1 - alpha) * base[mask] + alpha * myo_rgba[mask]
|
|
|
|
| 273 |
if n_nuc > 0:
|
| 274 |
nuc_norm = (nuc_disp / max(n_nuc, 1)).astype(np.float32)
|
| 275 |
nuc_rgba = (nuc_cmap(nuc_norm)[:, :, :3] * 255).astype(np.float32)
|
| 276 |
mask = nuc_disp > 0
|
| 277 |
base[mask] = (1 - alpha) * base[mask] + alpha * nuc_rgba[mask]
|
|
|
|
| 278 |
overlay = np.clip(base, 0, 255).astype(np.uint8)
|
| 279 |
|
| 280 |
+
# ββ render at high DPI so the PNG is sharp when zoomed βββββββββββββββββββ
|
| 281 |
+
# We render the figure at the ORIGINAL pixel size Γ a scale factor,
|
| 282 |
+
# then downsample back β this keeps labels crisp at zoom.
|
| 283 |
+
RENDER_SCALE = 2 # render at 2Γ then downsample β no blur
|
| 284 |
+
dpi = 150
|
| 285 |
+
fig_w = orig_w * RENDER_SCALE / dpi
|
| 286 |
+
fig_h = orig_h * RENDER_SCALE / dpi
|
| 287 |
+
|
| 288 |
+
fig, ax = plt.subplots(figsize=(fig_w, fig_h), dpi=dpi)
|
| 289 |
ax.imshow(overlay)
|
| 290 |
+
ax.set_xlim(0, orig_w)
|
| 291 |
+
ax.set_ylim(orig_h, 0)
|
| 292 |
ax.axis("off")
|
| 293 |
|
| 294 |
+
# ββ font sizes: fixed in figure points, independent of image size ββββββββ
|
| 295 |
+
# At RENDER_SCALE=2, dpi=150: 1 data pixel β 1/75 inch.
|
| 296 |
+
# We want nucleus labels ~8β10 pt and myotube labels ~18β22 pt.
|
| 297 |
+
font_nuc = 9 # pt β clearly readable when zoomed, not overwhelming at full view
|
| 298 |
+
font_myo = 20 # pt β dominant, impossible to miss
|
| 299 |
|
| 300 |
+
# ββ nucleus labels ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 301 |
if label_nuclei:
|
| 302 |
for prop in measure.regionprops(nuc_lab):
|
| 303 |
r, c = prop.centroid
|
| 304 |
+
# scale centroid from prediction-space to display-space
|
| 305 |
+
cx = c * (orig_w / nuc_lab.shape[1])
|
| 306 |
+
cy = r * (orig_h / nuc_lab.shape[0])
|
| 307 |
+
ax.text(
|
| 308 |
+
cx, cy, str(prop.label),
|
| 309 |
+
fontsize=font_nuc,
|
| 310 |
+
color="white",
|
| 311 |
+
ha="center", va="center",
|
| 312 |
+
fontweight="bold",
|
| 313 |
+
bbox=dict(
|
| 314 |
+
boxstyle="round,pad=0.25",
|
| 315 |
+
fc="#003366", # solid dark-blue β fully opaque
|
| 316 |
+
ec="none",
|
| 317 |
+
alpha=0.92,
|
| 318 |
+
),
|
| 319 |
+
zorder=2,
|
| 320 |
+
)
|
| 321 |
|
| 322 |
+
# ββ myotube labels ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 323 |
if label_myotubes:
|
| 324 |
for prop in measure.regionprops(myo_lab):
|
| 325 |
r, c = prop.centroid
|
| 326 |
+
cx = c * (orig_w / myo_lab.shape[1])
|
| 327 |
+
cy = r * (orig_h / myo_lab.shape[0])
|
| 328 |
+
ax.text(
|
| 329 |
+
cx, cy, f"M{prop.label}",
|
| 330 |
+
fontsize=font_myo,
|
| 331 |
+
color="white",
|
| 332 |
+
ha="center", va="center",
|
| 333 |
+
fontweight="bold",
|
| 334 |
+
bbox=dict(
|
| 335 |
+
boxstyle="round,pad=0.35",
|
| 336 |
+
fc="#8B0000", # solid dark-red β fully opaque
|
| 337 |
+
ec="#FF6666", # thin bright-red border so it pops
|
| 338 |
+
linewidth=1.5,
|
| 339 |
+
alpha=0.95,
|
| 340 |
+
),
|
| 341 |
+
zorder=3,
|
| 342 |
+
)
|
| 343 |
|
| 344 |
+
# ββ legend ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 345 |
patches = [
|
| 346 |
mpatches.Patch(color=nuc_cmap(0.7), label=f"Nuclei (n={n_nuc})"),
|
| 347 |
mpatches.Patch(color=myo_cmap(0.7), label=f"Myotubes (n={n_myo})"),
|
| 348 |
]
|
| 349 |
+
ax.legend(
|
| 350 |
+
handles=patches,
|
| 351 |
+
loc="upper right",
|
| 352 |
+
fontsize=13,
|
| 353 |
+
framealpha=0.85,
|
| 354 |
+
facecolor="#111111",
|
| 355 |
+
labelcolor="white",
|
| 356 |
+
edgecolor="#444444",
|
| 357 |
+
)
|
| 358 |
|
| 359 |
fig.tight_layout(pad=0)
|
| 360 |
buf = io.BytesIO()
|
| 361 |
+
# Save at same high DPI β this is what makes the PNG sharp when zoomed
|
| 362 |
fig.savefig(buf, format="png", bbox_inches="tight", pad_inches=0, dpi=dpi)
|
| 363 |
plt.close(fig)
|
| 364 |
buf.seek(0)
|
|
|
|
| 527 |
st.header("Postprocessing")
|
| 528 |
min_nuc_area = st.number_input("Min nucleus area (px)", 0, 10000, 20, 1)
|
| 529 |
min_myo_area = st.number_input("Min myotube area (px)", 0, 200000, 500, 10)
|
| 530 |
+
nuc_close_radius = st.number_input("Nuclei close radius", 0, 50, 2, 1)
|
| 531 |
myo_close_radius = st.number_input("Myotube close radius", 0, 50, 3, 1)
|
| 532 |
|
| 533 |
st.header("Watershed (nuclei splitting)")
|
|
|
|
| 630 |
nuc_raw, myo_raw,
|
| 631 |
min_nuc_area=int(min_nuc_area),
|
| 632 |
min_myo_area=int(min_myo_area),
|
| 633 |
+
nuc_close_radius=int(nuc_close_radius),
|
| 634 |
myo_close_radius=int(myo_close_radius),
|
| 635 |
)
|
| 636 |
|
|
|
|
| 639 |
rgb_u8, nuc_pp, myo_pp, nuc_rgb, myo_rgb, float(alpha)
|
| 640 |
)
|
| 641 |
|
| 642 |
+
# Label maps β shared across all three overlays
|
| 643 |
nuc_lab = label_nuclei_watershed(nuc_pp,
|
| 644 |
min_distance=int(nuc_ws_min_dist),
|
| 645 |
min_nuc_area=int(nuc_ws_min_area))
|
| 646 |
myo_lab = label_cc(myo_pp)
|
| 647 |
+
|
| 648 |
+
# Combined instance overlay (both nuclei + myotubes)
|
| 649 |
inst_ov = make_instance_overlay(rgb_u8, nuc_lab, myo_lab,
|
| 650 |
alpha=float(alpha),
|
| 651 |
label_nuclei=label_nuc,
|
| 652 |
label_myotubes=label_myo)
|
| 653 |
|
| 654 |
+
# Nuclei-only overlay
|
| 655 |
+
nuc_only_ov = make_instance_overlay(rgb_u8, nuc_lab,
|
| 656 |
+
np.zeros_like(myo_lab),
|
| 657 |
+
alpha=float(alpha),
|
| 658 |
+
label_nuclei=True,
|
| 659 |
+
label_myotubes=False)
|
| 660 |
+
|
| 661 |
+
# Myotubes-only overlay
|
| 662 |
+
myo_only_ov = make_instance_overlay(rgb_u8,
|
| 663 |
+
np.zeros_like(nuc_lab),
|
| 664 |
+
myo_lab,
|
| 665 |
+
alpha=float(alpha),
|
| 666 |
+
label_nuclei=False,
|
| 667 |
+
label_myotubes=True)
|
| 668 |
+
|
| 669 |
bio = compute_bio_metrics(
|
| 670 |
nuc_pp, myo_pp,
|
| 671 |
nuc_ws_min_distance=int(nuc_ws_min_dist),
|
|
|
|
| 678 |
all_bio_metrics[name] = {**bio, "_per_myotube_areas": per_areas}
|
| 679 |
|
| 680 |
artifacts[name] = {
|
| 681 |
+
"original" : png_bytes(rgb_u8),
|
| 682 |
+
"overlay" : png_bytes(inst_ov),
|
| 683 |
+
"nuc_only_ov" : png_bytes(nuc_only_ov),
|
| 684 |
+
"myo_only_ov" : png_bytes(myo_only_ov),
|
| 685 |
+
"nuc_pp" : png_bytes((nuc_pp * 255).astype(np.uint8)),
|
| 686 |
+
"myo_pp" : png_bytes((myo_pp * 255).astype(np.uint8)),
|
| 687 |
}
|
| 688 |
|
| 689 |
# ZIP contents
|
| 690 |
+
zf.writestr(f"{name}/overlay_combined.png", png_bytes(simple_ov))
|
| 691 |
+
zf.writestr(f"{name}/overlay_instance.png", png_bytes(inst_ov))
|
| 692 |
+
zf.writestr(f"{name}/overlay_nuclei.png", png_bytes(nuc_only_ov))
|
| 693 |
+
zf.writestr(f"{name}/overlay_myotubes.png", png_bytes(myo_only_ov))
|
| 694 |
+
zf.writestr(f"{name}/nuclei_pp.png", artifacts[name]["nuc_pp"])
|
| 695 |
+
zf.writestr(f"{name}/myotube_pp.png", artifacts[name]["myo_pp"])
|
| 696 |
+
zf.writestr(f"{name}/nuclei_raw.png", png_bytes((nuc_raw*255).astype(np.uint8)))
|
| 697 |
+
zf.writestr(f"{name}/myotube_raw.png", png_bytes((myo_raw*255).astype(np.uint8)))
|
| 698 |
|
| 699 |
prog.progress((i + 1) / len(uploads))
|
| 700 |
|
|
|
|
| 747 |
col_img, col_metrics = st.columns([3, 2], gap="large")
|
| 748 |
|
| 749 |
with col_img:
|
| 750 |
+
tabs = st.tabs([
|
| 751 |
+
"π΅ Combined overlay",
|
| 752 |
+
"π£ Nuclei only",
|
| 753 |
+
"π Myotubes only",
|
| 754 |
+
"π· Original",
|
| 755 |
+
"β¬ Nuclei mask",
|
| 756 |
+
"β¬ Myotube mask",
|
| 757 |
+
])
|
| 758 |
+
art = st.session_state.artifacts[pick]
|
| 759 |
+
bio_cur = st.session_state.bio_metrics.get(pick, {})
|
| 760 |
FIXED_W = 700
|
| 761 |
+
with tabs[0]:
|
| 762 |
+
st.image(art["overlay"], width=FIXED_W)
|
| 763 |
+
with tabs[1]:
|
| 764 |
+
n_nuc = bio_cur.get("total_nuclei", "β")
|
| 765 |
+
st.caption(f"**Nuclei count: {n_nuc}** β each nucleus has a unique ID label")
|
| 766 |
+
st.image(art["nuc_only_ov"], width=FIXED_W)
|
| 767 |
+
with tabs[2]:
|
| 768 |
+
n_myo = bio_cur.get("myotube_count", "β")
|
| 769 |
+
st.caption(f"**Myotube count: {n_myo}** β each myotube has a unique M-label")
|
| 770 |
+
st.image(art["myo_only_ov"], width=FIXED_W)
|
| 771 |
+
with tabs[3]:
|
| 772 |
+
st.image(art["original"], width=FIXED_W)
|
| 773 |
+
with tabs[4]:
|
| 774 |
+
st.image(art["nuc_pp"], width=FIXED_W)
|
| 775 |
+
with tabs[5]:
|
| 776 |
+
st.image(art["myo_pp"], width=FIXED_W)
|
| 777 |
|
| 778 |
with col_metrics:
|
| 779 |
st.markdown("#### π Live metrics")
|