LiangLabUMB commited on
Commit
de7263b
·
verified ·
1 Parent(s): 5629399

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +139 -37
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": float(cell_count) if cell_count is not None else None,
835
- "confluency": float(confluency) if confluency is not None else None,
836
- "viab_percent": float(viab_percent) if viab_percent is not None else None,
 
 
837
  }
838
 
839
 
840
  def compute_summary(r1, r2, r3, r4):
841
- """Average cell count, confluency, and viability across tabs that have data."""
 
 
 
 
 
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
- return (
848
- 0.0, 0.0, 0.0,
849
- "No data yet — run segmentation in at least one tab, then click Refresh Summary."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
850
  )
851
-
852
- avg_count = sum(r["cell_count"] for _, r in valid) / len(valid)
853
- avg_conf = sum(r["confluency"] for _, r in valid) / len(valid)
854
- avg_viab = sum(r["viab_percent"] for _, r in valid) / len(valid)
855
-
856
- lines = [f"Tab {tab_num}: {r['cell_count']:.0f} cells | "
857
- f"{r['confluency']:.1f}% confluency | "
858
- f"{r['viab_percent']:.1f}% viability"
859
- for tab_num, r in valid]
860
- lines.append(f"\nAverages ({len(valid)} tab{'s' if len(valid) > 1 else ''}):")
861
- lines.append(f" Cell count: {avg_count:.1f}")
862
- lines.append(f" Confluency: {avg_conf:.1f}%")
863
- lines.append(f" Viability: {avg_viab:.1f}%")
864
-
865
- return avg_count, avg_conf, avg_viab, "\n".join(lines)
 
 
 
 
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
- viab_info = gr.Textbox(label="Analysis Results", lines=4)
 
 
 
 
 
 
 
 
 
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
- # ---- Run Viability button -------------------------------------------
1631
- def on_run_viability(stored_masks, stored_image):
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=[viab_overlay, live_count_out, dead_count_out,
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=10)
 
 
 
 
 
 
 
 
 
 
 
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