Upload app.py
Browse files
app.py
CHANGED
|
@@ -805,6 +805,28 @@ def run_viability(stored_masks, stored_image_np):
|
|
| 805 |
return None, 0, 0, 0.0, f"Error: {str(e)}", {}
|
| 806 |
|
| 807 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 808 |
def pack_array(arr):
|
| 809 |
"""
|
| 810 |
Serialise an array for gr.State.
|
|
@@ -828,41 +850,80 @@ def unpack_array(data):
|
|
| 828 |
return np.array(Image.open(buf))
|
| 829 |
|
| 830 |
|
| 831 |
-
def save_tab_result(cell_count, confluency, viab_percent):
|
| 832 |
"""Package per-tab results into a dict for Tab 5 summary."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 833 |
return {
|
| 834 |
-
"cell_count":
|
| 835 |
-
"confluency":
|
| 836 |
-
"viab_percent":
|
|
|
|
|
|
|
| 837 |
}
|
| 838 |
|
| 839 |
|
| 840 |
def compute_summary(r1, r2, r3, r4):
|
| 841 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 842 |
all_results = [r1, r2, r3, r4]
|
| 843 |
valid = [(i + 1, r) for i, r in enumerate(all_results)
|
| 844 |
if r is not None and r.get("cell_count") is not None]
|
| 845 |
|
| 846 |
if not valid:
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 850 |
)
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
|
| 867 |
|
| 868 |
# ---------------------------------------------------------------------------
|
|
@@ -1444,7 +1505,16 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1444 |
|
| 1445 |
viab_overlay = gr.Image(type="pil", label="Viability (Green=Live · Red=Dead)")
|
| 1446 |
viab_percent_out = gr.Number(label="Viability (%)", precision=1)
|
| 1447 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1448 |
|
| 1449 |
gr.Markdown("### Label Correction & Export")
|
| 1450 |
gr.Markdown(
|
|
@@ -1617,6 +1687,30 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1617 |
outputs=[crop_display, crop_points_state, zoom_state, crop_status,
|
| 1618 |
cancel_zoom_btn])
|
| 1619 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1620 |
segment_btn.click(
|
| 1621 |
fn=run_segmentation,
|
| 1622 |
inputs=[img_input, model_dropdown, min_size_slider, max_size_slider,
|
|
@@ -1625,21 +1719,17 @@ def build_tab(tab_index, masks_state, image_state, result_state):
|
|
| 1625 |
outputs=[cell_count_out, overlay_out, info_out, viability_section,
|
| 1626 |
masks_state, image_state, confluency_out,
|
| 1627 |
min_size_recommendation, raw_image_state, base_masks_state]
|
| 1628 |
-
)
|
| 1629 |
-
|
| 1630 |
-
|
| 1631 |
-
|
| 1632 |
-
overlay, alive, dead, viab_pct, info, label_map = run_viability(stored_masks, stored_image)
|
| 1633 |
-
return overlay, alive, dead, viab_pct, info, label_map
|
| 1634 |
-
|
| 1635 |
-
viab_run_btn.click(
|
| 1636 |
fn=on_run_viability,
|
| 1637 |
inputs=[masks_state, image_state],
|
| 1638 |
-
outputs=
|
| 1639 |
-
viab_percent_out, viab_info, label_map_state]
|
| 1640 |
).then(
|
| 1641 |
fn=save_tab_result,
|
| 1642 |
-
inputs=[cell_count_out, confluency_out, viab_percent_out
|
|
|
|
| 1643 |
outputs=[result_state]
|
| 1644 |
)
|
| 1645 |
|
|
@@ -1749,12 +1839,24 @@ with gr.Blocks(
|
|
| 1749 |
avg_conf_out = gr.Number(label="Avg Confluency (%)", precision=1)
|
| 1750 |
avg_viab_out = gr.Number(label="Avg Viability (%)", precision=1)
|
| 1751 |
|
| 1752 |
-
summary_box = gr.Textbox(label="Per-Tab Breakdown", lines=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1753 |
|
| 1754 |
refresh_btn.click(
|
| 1755 |
fn=compute_summary,
|
| 1756 |
inputs=result_states, # list of 4 gr.State components
|
| 1757 |
-
outputs=[avg_count_out, avg_conf_out, avg_viab_out, summary_box
|
|
|
|
| 1758 |
)
|
| 1759 |
|
| 1760 |
|
|
|
|
| 805 |
return None, 0, 0, 0.0, f"Error: {str(e)}", {}
|
| 806 |
|
| 807 |
|
| 808 |
+
# Hemocytometer: cells/mL = cells per large square x 10,000 x dilution factor.
|
| 809 |
+
# Counting one large square, so the two presets below fold the 10,000 chamber
|
| 810 |
+
# constant and the dilution together into a single multiplier.
|
| 811 |
+
CONC_PRESETS = (
|
| 812 |
+
("1:1 dilution (2x, trypan blue)", 20_000),
|
| 813 |
+
("1:10 dilution (10x, trypan blue)", 100_000),
|
| 814 |
+
)
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def format_concentration(live_cells, multiplier):
|
| 818 |
+
"""Human-readable concentration, showing the arithmetic for the lab record."""
|
| 819 |
+
try:
|
| 820 |
+
live = int(live_cells or 0)
|
| 821 |
+
except (TypeError, ValueError):
|
| 822 |
+
return ""
|
| 823 |
+
conc = live * multiplier
|
| 824 |
+
return (f"Live cells: {live:,}\n"
|
| 825 |
+
f"x {multiplier:,}\n"
|
| 826 |
+
f"= {conc:,} cells/mL\n"
|
| 827 |
+
f"= {conc / 1e6:.2f} x 10^6 cells/mL")
|
| 828 |
+
|
| 829 |
+
|
| 830 |
def pack_array(arr):
|
| 831 |
"""
|
| 832 |
Serialise an array for gr.State.
|
|
|
|
| 850 |
return np.array(Image.open(buf))
|
| 851 |
|
| 852 |
|
| 853 |
+
def save_tab_result(cell_count, confluency, viab_percent, live_cells, dead_cells):
|
| 854 |
"""Package per-tab results into a dict for Tab 5 summary."""
|
| 855 |
+
def _f(v):
|
| 856 |
+
try:
|
| 857 |
+
return float(v) if v is not None else None
|
| 858 |
+
except (TypeError, ValueError):
|
| 859 |
+
return None
|
| 860 |
return {
|
| 861 |
+
"cell_count": _f(cell_count),
|
| 862 |
+
"confluency": _f(confluency),
|
| 863 |
+
"viab_percent": _f(viab_percent),
|
| 864 |
+
"live": _f(live_cells),
|
| 865 |
+
"dead": _f(dead_cells),
|
| 866 |
}
|
| 867 |
|
| 868 |
|
| 869 |
def compute_summary(r1, r2, r3, r4):
|
| 870 |
+
"""Per-tab counts, their averages, and concentrations from those averages.
|
| 871 |
+
|
| 872 |
+
Standard hemocytometer practice is to count the four corner squares, average
|
| 873 |
+
them, then multiply by the chamber constant and the dilution factor -- so the
|
| 874 |
+
averaging happens BEFORE the multiplication, not after.
|
| 875 |
+
"""
|
| 876 |
all_results = [r1, r2, r3, r4]
|
| 877 |
valid = [(i + 1, r) for i, r in enumerate(all_results)
|
| 878 |
if r is not None and r.get("cell_count") is not None]
|
| 879 |
|
| 880 |
if not valid:
|
| 881 |
+
msg = ("No data yet — run segmentation in at least one tab, "
|
| 882 |
+
"then click Refresh Summary.")
|
| 883 |
+
return 0.0, 0.0, 0.0, msg, "", ""
|
| 884 |
+
|
| 885 |
+
n = len(valid)
|
| 886 |
+
|
| 887 |
+
def _avg(key):
|
| 888 |
+
vals = [r.get(key) for _, r in valid if r.get(key) is not None]
|
| 889 |
+
return (sum(vals) / len(vals)) if vals else 0.0
|
| 890 |
+
|
| 891 |
+
avg_count = _avg("cell_count")
|
| 892 |
+
avg_conf = _avg("confluency")
|
| 893 |
+
avg_viab = _avg("viab_percent")
|
| 894 |
+
avg_live = _avg("live")
|
| 895 |
+
avg_dead = _avg("dead")
|
| 896 |
+
|
| 897 |
+
header = f"{'Tab':<6}{'Total':>8}{'Live':>8}{'Dead':>8}{'Viab %':>9}{'Confl %':>9}"
|
| 898 |
+
lines = [header, "-" * len(header)]
|
| 899 |
+
for tab_num, r in valid:
|
| 900 |
+
lines.append(
|
| 901 |
+
f"{tab_num:<6}"
|
| 902 |
+
f"{(r.get('cell_count') or 0):>8.0f}"
|
| 903 |
+
f"{(r.get('live') or 0):>8.0f}"
|
| 904 |
+
f"{(r.get('dead') or 0):>8.0f}"
|
| 905 |
+
f"{(r.get('viab_percent') or 0):>9.1f}"
|
| 906 |
+
f"{(r.get('confluency') or 0):>9.1f}"
|
| 907 |
)
|
| 908 |
+
lines.append("-" * len(header))
|
| 909 |
+
lines.append(f"{'Mean':<6}{avg_count:>8.1f}{avg_live:>8.1f}{avg_dead:>8.1f}"
|
| 910 |
+
f"{avg_viab:>9.1f}{avg_conf:>9.1f}")
|
| 911 |
+
lines.append("")
|
| 912 |
+
lines.append(f"Averaged over {n} tab{'s' if n > 1 else ''}"
|
| 913 |
+
+ ("" if n == 4 else f" ⚠ hemocytometer convention uses all 4 squares"))
|
| 914 |
+
|
| 915 |
+
def _block(multiplier):
|
| 916 |
+
return (f"Mean of {n} tab{'s' if n > 1 else ''}, x {multiplier:,}\n"
|
| 917 |
+
f"\n"
|
| 918 |
+
f"Live: {avg_live:.1f} -> {avg_live * multiplier:,.0f} cells/mL\n"
|
| 919 |
+
f" ({avg_live * multiplier / 1e6:.2f} x 10^6)\n"
|
| 920 |
+
f"Dead: {avg_dead:.1f} -> {avg_dead * multiplier:,.0f} cells/mL\n"
|
| 921 |
+
f" ({avg_dead * multiplier / 1e6:.2f} x 10^6)\n"
|
| 922 |
+
f"Total: {avg_count:.1f} -> {avg_count * multiplier:,.0f} cells/mL\n"
|
| 923 |
+
f" ({avg_count * multiplier / 1e6:.2f} x 10^6)")
|
| 924 |
+
|
| 925 |
+
return (avg_count, avg_conf, avg_viab, "\n".join(lines),
|
| 926 |
+
_block(CONC_PRESETS[0][1]), _block(CONC_PRESETS[1][1]))
|
| 927 |
|
| 928 |
|
| 929 |
# ---------------------------------------------------------------------------
|
|
|
|
| 1505 |
|
| 1506 |
viab_overlay = gr.Image(type="pil", label="Viability (Green=Live · Red=Dead)")
|
| 1507 |
viab_percent_out = gr.Number(label="Viability (%)", precision=1)
|
| 1508 |
+
with gr.Row():
|
| 1509 |
+
viab_info = gr.Textbox(label="Analysis Results", lines=5)
|
| 1510 |
+
conc_1_1 = gr.Textbox(
|
| 1511 |
+
label=f"Concentration — {CONC_PRESETS[0][0]}",
|
| 1512 |
+
lines=5, interactive=False,
|
| 1513 |
+
)
|
| 1514 |
+
conc_1_10 = gr.Textbox(
|
| 1515 |
+
label=f"Concentration — {CONC_PRESETS[1][0]}",
|
| 1516 |
+
lines=5, interactive=False,
|
| 1517 |
+
)
|
| 1518 |
|
| 1519 |
gr.Markdown("### Label Correction & Export")
|
| 1520 |
gr.Markdown(
|
|
|
|
| 1687 |
outputs=[crop_display, crop_points_state, zoom_state, crop_status,
|
| 1688 |
cancel_zoom_btn])
|
| 1689 |
|
| 1690 |
+
|
| 1691 |
+
# ---- Viability ------------------------------------------------------
|
| 1692 |
+
def on_run_viability(stored_masks, stored_image):
|
| 1693 |
+
overlay, alive, dead, viab_pct, info, label_map = run_viability(
|
| 1694 |
+
stored_masks, stored_image)
|
| 1695 |
+
return (overlay, alive, dead, viab_pct, info, label_map,
|
| 1696 |
+
format_concentration(alive, CONC_PRESETS[0][1]),
|
| 1697 |
+
format_concentration(alive, CONC_PRESETS[1][1]))
|
| 1698 |
+
|
| 1699 |
+
viab_outputs = [viab_overlay, live_count_out, dead_count_out,
|
| 1700 |
+
viab_percent_out, viab_info, label_map_state,
|
| 1701 |
+
conc_1_1, conc_1_10]
|
| 1702 |
+
|
| 1703 |
+
viab_run_btn.click(
|
| 1704 |
+
fn=on_run_viability,
|
| 1705 |
+
inputs=[masks_state, image_state],
|
| 1706 |
+
outputs=viab_outputs
|
| 1707 |
+
).then(
|
| 1708 |
+
fn=save_tab_result,
|
| 1709 |
+
inputs=[cell_count_out, confluency_out, viab_percent_out,
|
| 1710 |
+
live_count_out, dead_count_out],
|
| 1711 |
+
outputs=[result_state]
|
| 1712 |
+
)
|
| 1713 |
+
|
| 1714 |
segment_btn.click(
|
| 1715 |
fn=run_segmentation,
|
| 1716 |
inputs=[img_input, model_dropdown, min_size_slider, max_size_slider,
|
|
|
|
| 1719 |
outputs=[cell_count_out, overlay_out, info_out, viability_section,
|
| 1720 |
masks_state, image_state, confluency_out,
|
| 1721 |
min_size_recommendation, raw_image_state, base_masks_state]
|
| 1722 |
+
).then(
|
| 1723 |
+
# Viability is wanted on essentially every run, so do it without a
|
| 1724 |
+
# second button press. The button stays for re-running after the
|
| 1725 |
+
# exclusion sliders or the label corrections change.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1726 |
fn=on_run_viability,
|
| 1727 |
inputs=[masks_state, image_state],
|
| 1728 |
+
outputs=viab_outputs
|
|
|
|
| 1729 |
).then(
|
| 1730 |
fn=save_tab_result,
|
| 1731 |
+
inputs=[cell_count_out, confluency_out, viab_percent_out,
|
| 1732 |
+
live_count_out, dead_count_out],
|
| 1733 |
outputs=[result_state]
|
| 1734 |
)
|
| 1735 |
|
|
|
|
| 1839 |
avg_conf_out = gr.Number(label="Avg Confluency (%)", precision=1)
|
| 1840 |
avg_viab_out = gr.Number(label="Avg Viability (%)", precision=1)
|
| 1841 |
|
| 1842 |
+
summary_box = gr.Textbox(label="Per-Tab Breakdown", lines=11)
|
| 1843 |
+
|
| 1844 |
+
gr.Markdown("### Cell Concentration (from the mean across tabs)")
|
| 1845 |
+
with gr.Row():
|
| 1846 |
+
summary_conc_2x = gr.Textbox(
|
| 1847 |
+
label=f"Concentration — {CONC_PRESETS[0][0]}",
|
| 1848 |
+
lines=8, interactive=False,
|
| 1849 |
+
)
|
| 1850 |
+
summary_conc_10x = gr.Textbox(
|
| 1851 |
+
label=f"Concentration — {CONC_PRESETS[1][0]}",
|
| 1852 |
+
lines=8, interactive=False,
|
| 1853 |
+
)
|
| 1854 |
|
| 1855 |
refresh_btn.click(
|
| 1856 |
fn=compute_summary,
|
| 1857 |
inputs=result_states, # list of 4 gr.State components
|
| 1858 |
+
outputs=[avg_count_out, avg_conf_out, avg_viab_out, summary_box,
|
| 1859 |
+
summary_conc_2x, summary_conc_10x]
|
| 1860 |
)
|
| 1861 |
|
| 1862 |
|