Spaces:
Running on Zero
Running on Zero
Upload app.py
Browse fileschanged layout
app.py
CHANGED
|
@@ -1595,562 +1595,391 @@ def prepare_export_corrected(stored_masks, stored_image, labelled_features, labe
|
|
| 1595 |
f"{corrected} label(s) manually corrected.")
|
| 1596 |
return path, msg
|
| 1597 |
|
| 1598 |
-
|
| 1599 |
-
|
| 1600 |
-
|
| 1601 |
-
|
| 1602 |
-
|
| 1603 |
-
crop_points_state = gr.State(value=[])
|
| 1604 |
-
# Clean copy of the uploaded image (no polygon drawn on it)
|
| 1605 |
-
base_image_state = gr.State(value=None)
|
| 1606 |
-
# preview->original scale, so taps on the small picker map back to
|
| 1607 |
-
# full-resolution coordinates for the segmentation warp
|
| 1608 |
-
preview_scale_state = gr.State(value=1.0)
|
| 1609 |
-
# None = showing the overview; dict = showing a zoomed window
|
| 1610 |
-
zoom_state = gr.State(value=None)
|
| 1611 |
-
# segmentation wall time and the overlay frame, both needed by Tab 5
|
| 1612 |
-
seg_time_state = gr.State(value=0.0)
|
| 1613 |
-
seg_overlay_state = gr.State(value=None)
|
| 1614 |
-
#raw image state
|
| 1615 |
-
raw_image_state = gr.State(value=None)
|
| 1616 |
-
# Masks as segmented, before stereological exclusion β lets the zones
|
| 1617 |
-
# be re-chosen after segmentation without re-running the model.
|
| 1618 |
-
base_masks_state = gr.State(value=None)
|
| 1619 |
|
| 1620 |
-
with gr.Row():
|
| 1621 |
-
with gr.Column():
|
| 1622 |
-
img_input = gr.Image(
|
| 1623 |
-
type="pil",
|
| 1624 |
-
label="Upload image",
|
| 1625 |
-
image_mode="RGB",
|
| 1626 |
-
height=512
|
| 1627 |
-
)
|
| 1628 |
-
|
| 1629 |
-
gr.Markdown(
|
| 1630 |
-
"### Crop region (optional)\n"
|
| 1631 |
-
"For each corner, tap **twice**: once roughly near it, then "
|
| 1632 |
-
"again precisely in the zoomed view that appears. The rough "
|
| 1633 |
-
"tap needs no accuracy β your finger can cover it entirely. "
|
| 1634 |
-
"Four corners define the region to segment; leave empty to "
|
| 1635 |
-
"use the whole image."
|
| 1636 |
-
)
|
| 1637 |
-
|
| 1638 |
-
crop_display = gr.Image(
|
| 1639 |
-
type="pil",
|
| 1640 |
-
label="Tap to set crop vertices (up to 4)",
|
| 1641 |
-
interactive=True,
|
| 1642 |
-
height=400,
|
| 1643 |
-
format="jpeg", # PNG costs ~3 s per redraw at 12 MP
|
| 1644 |
-
show_download_button=False,
|
| 1645 |
-
)
|
| 1646 |
-
|
| 1647 |
-
crop_status = gr.Markdown("*Upload an image to enable cropping*")
|
| 1648 |
-
|
| 1649 |
-
clear_crop_btn = gr.Button("β Clear crop points", size="sm")
|
| 1650 |
-
|
| 1651 |
-
cancel_zoom_btn = gr.Button("β© Back to full view", size="sm",
|
| 1652 |
-
visible=False)
|
| 1653 |
-
|
| 1654 |
-
model_dropdown = gr.Dropdown(
|
| 1655 |
-
choices=list(MODEL_OPTIONS.keys()),
|
| 1656 |
-
label="Select Model",
|
| 1657 |
-
value="Hemocytometer Model"
|
| 1658 |
-
)
|
| 1659 |
-
|
| 1660 |
-
gr.Markdown("### Size Filters")
|
| 1661 |
-
|
| 1662 |
-
use_min_filter = gr.Checkbox(
|
| 1663 |
-
label="Enable minimum size filter",
|
| 1664 |
-
value=True,
|
| 1665 |
-
info="Remove objects smaller than the threshold below. "
|
| 1666 |
-
"At 0 the app uses its own recommendation "
|
| 1667 |
-
"(25th percentile of detected object sizes)."
|
| 1668 |
-
)
|
| 1669 |
-
min_size_slider = gr.Slider(
|
| 1670 |
-
minimum=0,
|
| 1671 |
-
maximum=500,
|
| 1672 |
-
value=0,
|
| 1673 |
-
step=10,
|
| 1674 |
-
label="Minimum Cell Size (pixels)",
|
| 1675 |
-
interactive=True,
|
| 1676 |
-
)
|
| 1677 |
-
min_size_recommendation = gr.Markdown(
|
| 1678 |
-
value="*Run segmentation to see recommended minimum*",
|
| 1679 |
-
)
|
| 1680 |
-
|
| 1681 |
-
use_max_filter = gr.Checkbox(
|
| 1682 |
-
label="Enable maximum size filter",
|
| 1683 |
-
value=False,
|
| 1684 |
-
info="Remove objects larger than the threshold below"
|
| 1685 |
-
)
|
| 1686 |
-
max_size_slider = gr.Slider(
|
| 1687 |
-
minimum=0,
|
| 1688 |
-
maximum=10000,
|
| 1689 |
-
value=10000,
|
| 1690 |
-
step=10,
|
| 1691 |
-
label="Maximum Cell Size (pixels)",
|
| 1692 |
-
interactive=False,
|
| 1693 |
-
)
|
| 1694 |
-
|
| 1695 |
-
segment_btn = gr.Button("π¬ Run Segmentation", variant="primary", size="lg")
|
| 1696 |
-
|
| 1697 |
-
with gr.Column():
|
| 1698 |
-
cell_count_out = gr.Number(label="Total Cells Detected", precision=0)
|
| 1699 |
-
confluency_out = gr.Number(label="Confluency (%)", precision=1)
|
| 1700 |
-
overlay_out = gr.Image(type="pil", label="Segmentation Result")
|
| 1701 |
-
info_out = gr.Textbox(label="Processing Info", lines=4)
|
| 1702 |
-
|
| 1703 |
-
# Applied after segmentation, so it lives beside the result it
|
| 1704 |
-
# modifies rather than with the pre-segmentation settings.
|
| 1705 |
-
gr.Markdown("### Stereological Counting")
|
| 1706 |
-
use_stereo = gr.Checkbox(
|
| 1707 |
-
label="Enable Stereological Counting",
|
| 1708 |
-
value=True,
|
| 1709 |
-
info="Applied to the segmentation above β no re-segmenting needed"
|
| 1710 |
-
)
|
| 1711 |
-
|
| 1712 |
-
with gr.Group(visible=True) as stereo_controls:
|
| 1713 |
-
gr.Markdown("""
|
| 1714 |
-
**Stereological Counting Rules:**
|
| 1715 |
-
- Cells touching LEFT or TOP exclusion zones are EXCLUDED
|
| 1716 |
-
- Cells touching RIGHT or BOTTOM edges are INCLUDED
|
| 1717 |
-
- This provides unbiased counting for quantification
|
| 1718 |
-
|
| 1719 |
-
Zone widths are percentages of the **segmented image** β
|
| 1720 |
-
the cropped and perspective-corrected region, not the
|
| 1721 |
-
original upload. The red zones are drawn on the
|
| 1722 |
-
segmentation result above and update as you drag.
|
| 1723 |
-
""")
|
| 1724 |
-
|
| 1725 |
-
left_excl = gr.Slider(
|
| 1726 |
-
minimum=0,
|
| 1727 |
-
maximum=50,
|
| 1728 |
-
value=1,
|
| 1729 |
-
step=1,
|
| 1730 |
-
label="Left Exclusion Width (%)",
|
| 1731 |
-
info="Width of left exclusion zone"
|
| 1732 |
-
)
|
| 1733 |
-
|
| 1734 |
-
top_excl = gr.Slider(
|
| 1735 |
-
minimum=0,
|
| 1736 |
-
maximum=50,
|
| 1737 |
-
value=1,
|
| 1738 |
-
step=1,
|
| 1739 |
-
label="Top Exclusion Width (%)",
|
| 1740 |
-
info="Width of top exclusion zone"
|
| 1741 |
-
)
|
| 1742 |
-
|
| 1743 |
-
with gr.Group(visible=False) as viability_section:
|
| 1744 |
-
gr.Markdown("### Viability Assessment (Trypan Blue)")
|
| 1745 |
-
|
| 1746 |
-
viab_run_btn = gr.Button("Run Viability Analysis", variant="primary")
|
| 1747 |
-
|
| 1748 |
-
with gr.Row():
|
| 1749 |
-
live_count_out = gr.Number(label="Live Cells (Green)", precision=0)
|
| 1750 |
-
dead_count_out = gr.Number(label="Dead Cells (Red)", precision=0)
|
| 1751 |
-
|
| 1752 |
-
viab_overlay = gr.Image(type="pil", label="Viability (Green=Live Β· Red=Dead)")
|
| 1753 |
-
viab_percent_out = gr.Number(label="Viability (%)", precision=1)
|
| 1754 |
-
with gr.Row():
|
| 1755 |
-
viab_info = gr.Textbox(label="Analysis Results", lines=5)
|
| 1756 |
-
conc_1_1 = gr.Textbox(
|
| 1757 |
-
label=f"Concentration β {CONC_PRESETS[0][0]}",
|
| 1758 |
-
lines=5, interactive=False,
|
| 1759 |
-
)
|
| 1760 |
-
conc_1_10 = gr.Textbox(
|
| 1761 |
-
label=f"Concentration β {CONC_PRESETS[1][0]}",
|
| 1762 |
-
lines=5, interactive=False,
|
| 1763 |
-
)
|
| 1764 |
-
|
| 1765 |
-
gr.Markdown("### Label Correction & Export")
|
| 1766 |
-
gr.Markdown(
|
| 1767 |
-
"After running viability, click **Build correction grid** to review every cell. "
|
| 1768 |
-
"**Green border = Live, Red border = Dead** (model predictions). "
|
| 1769 |
-
"Tap any thumbnail to flip its label β the counts and overlay update instantly. "
|
| 1770 |
-
"Export the corrected CSV for retraining."
|
| 1771 |
-
)
|
| 1772 |
-
|
| 1773 |
-
build_grid_btn = gr.Button("π² Build correction grid", variant="secondary")
|
| 1774 |
-
labelled_state = gr.State(value=[])
|
| 1775 |
-
label_map_state = gr.State(value={})
|
| 1776 |
-
|
| 1777 |
-
correction_grid = gr.Image(
|
| 1778 |
-
type="pil",
|
| 1779 |
-
label="Tap a cell to flip its label (green=live Β· red=dead)",
|
| 1780 |
-
interactive=True,
|
| 1781 |
-
visible=False,
|
| 1782 |
-
)
|
| 1783 |
-
correction_status = gr.Markdown(visible=False)
|
| 1784 |
-
|
| 1785 |
-
with gr.Row():
|
| 1786 |
-
export_btn = gr.Button("β¬οΈ Export corrected CSV", variant="secondary")
|
| 1787 |
-
export_info = gr.Textbox(label="Export status", lines=2, interactive=False)
|
| 1788 |
-
export_file = gr.File(label="Download CSV", visible=False)
|
| 1789 |
-
|
| 1790 |
-
# ---- Event handlers ------------------------------------------------
|
| 1791 |
-
|
| 1792 |
-
def on_model_change(model_choice):
|
| 1793 |
-
"""Hemocytometer counting is only unbiased with the stereological
|
| 1794 |
-
rules applied, so selecting that model turns them on. Left as a
|
| 1795 |
-
normal checkbox rather than locked, so the setting can still be
|
| 1796 |
-
switched off deliberately."""
|
| 1797 |
-
if model_choice == "Hemocytometer Model":
|
| 1798 |
-
return gr.update(value=True), gr.update(visible=True)
|
| 1799 |
-
return gr.update(), gr.update()
|
| 1800 |
-
|
| 1801 |
-
model_dropdown.change(
|
| 1802 |
-
fn=on_model_change,
|
| 1803 |
-
inputs=[model_dropdown],
|
| 1804 |
-
outputs=[use_stereo, stereo_controls]
|
| 1805 |
-
)
|
| 1806 |
|
| 1807 |
-
|
| 1808 |
-
|
| 1809 |
-
inputs=[use_stereo],
|
| 1810 |
-
outputs=[stereo_controls]
|
| 1811 |
-
)
|
| 1812 |
-
|
| 1813 |
-
def on_image_upload(img):
|
| 1814 |
-
if img is None:
|
| 1815 |
-
return None, None, 1.0, "*Upload an image to enable cropping*"
|
| 1816 |
-
preview, scale = make_preview(img)
|
| 1817 |
-
return (preview, preview, scale,
|
| 1818 |
-
"*Image loaded β tap up to 4 points to define crop region*")
|
| 1819 |
-
|
| 1820 |
-
img_input.change(
|
| 1821 |
-
fn=on_image_upload,
|
| 1822 |
-
inputs=[img_input],
|
| 1823 |
-
outputs=[crop_display, base_image_state, preview_scale_state, crop_status]
|
| 1824 |
-
).then(fn=lambda: ([], None), outputs=[crop_points_state, zoom_state])
|
| 1825 |
-
|
| 1826 |
-
# Grey the size sliders out while their filter is disabled, so the
|
| 1827 |
-
# checkbox state is visible rather than silently inferred.
|
| 1828 |
-
use_min_filter.change(
|
| 1829 |
-
fn=lambda on: gr.update(interactive=bool(on)),
|
| 1830 |
-
inputs=[use_min_filter], outputs=[min_size_slider]
|
| 1831 |
-
)
|
| 1832 |
-
use_max_filter.change(
|
| 1833 |
-
fn=lambda on: gr.update(interactive=bool(on)),
|
| 1834 |
-
inputs=[use_max_filter], outputs=[max_size_slider]
|
| 1835 |
-
)
|
| 1836 |
|
| 1837 |
-
|
| 1838 |
-
|
| 1839 |
-
|
| 1840 |
-
|
| 1841 |
-
|
| 1842 |
-
|
| 1843 |
-
|
| 1844 |
-
|
| 1845 |
-
|
| 1846 |
-
|
| 1847 |
-
|
| 1848 |
-
|
| 1849 |
-
|
| 1850 |
-
|
| 1851 |
-
|
| 1852 |
-
|
| 1853 |
-
|
| 1854 |
-
|
| 1855 |
-
|
| 1856 |
-
|
| 1857 |
-
|
| 1858 |
-
|
| 1859 |
-
|
| 1860 |
-
exclusion_outputs = [cell_count_out, overlay_out, confluency_out,
|
| 1861 |
-
masks_state, info_out]
|
| 1862 |
-
for _component in (use_stereo, left_excl, top_excl):
|
| 1863 |
-
_component.change(fn=on_exclusion_change,
|
| 1864 |
-
inputs=exclusion_inputs, outputs=exclusion_outputs)
|
| 1865 |
-
|
| 1866 |
-
def _overview(preview, points, scale):
|
| 1867 |
-
pv = [(int(x * scale), int(y * scale)) for x, y in points]
|
| 1868 |
-
return draw_polygon_overlay(preview, pv)
|
| 1869 |
-
|
| 1870 |
-
def on_crop_click(full_img, preview, points, scale, zoom,
|
| 1871 |
-
evt: gr.SelectData):
|
| 1872 |
-
"""Two stages per corner: rough tap -> zoomed view -> precise tap."""
|
| 1873 |
-
points = list(points or [])
|
| 1874 |
-
if preview is None or full_img is None:
|
| 1875 |
-
return (gr.update(), points, zoom, gr.update(), gr.update())
|
| 1876 |
-
|
| 1877 |
-
# Gradio can deliver an empty selection (gradio-app/gradio#5945)
|
| 1878 |
-
if evt is None or evt.index is None or evt.index[0] is None:
|
| 1879 |
-
return (gr.update(), points, zoom,
|
| 1880 |
-
"*Tap not registered β try again inside the image*",
|
| 1881 |
-
gr.update())
|
| 1882 |
-
|
| 1883 |
-
tx, ty = int(evt.index[0]), int(evt.index[1])
|
| 1884 |
-
|
| 1885 |
-
if zoom is None:
|
| 1886 |
-
# ---- stage 1: rough tap on the overview ----------------------
|
| 1887 |
-
if len(points) >= 4:
|
| 1888 |
-
return (_overview(preview, points, scale), points, None,
|
| 1889 |
-
"*4 points set β β **β Clear** to redo, or run segmentation*",
|
| 1890 |
-
gr.update(visible=False))
|
| 1891 |
-
view, z = make_zoom_view(full_img, tx / scale, ty / scale)
|
| 1892 |
-
return (view, points, z,
|
| 1893 |
-
f"*Zoomed in β now tap corner {len(points) + 1} precisely "
|
| 1894 |
-
f"(the red cross is your rough tap)*",
|
| 1895 |
-
gr.update(visible=True))
|
| 1896 |
-
|
| 1897 |
-
# ---- stage 2: precise tap inside the zoomed view -----------------
|
| 1898 |
-
x_full = zoom["x0"] + tx / zoom["zscale"]
|
| 1899 |
-
y_full = zoom["y0"] + ty / zoom["zscale"]
|
| 1900 |
-
W, H = full_img.size
|
| 1901 |
-
new_points = points + [(int(min(max(0, x_full), W - 1)),
|
| 1902 |
-
int(min(max(0, y_full), H - 1)))]
|
| 1903 |
-
n = len(new_points)
|
| 1904 |
-
status = (f"*{n} / 4 corners set β tap roughly near corner {n + 1}*"
|
| 1905 |
-
if n < 4 else
|
| 1906 |
-
"*4 points set β β **β Clear** to redo, or run segmentation*")
|
| 1907 |
-
return (_overview(preview, new_points, scale), new_points, None,
|
| 1908 |
-
status, gr.update(visible=False))
|
| 1909 |
-
|
| 1910 |
-
crop_display.select(fn=on_crop_click,
|
| 1911 |
-
inputs=[img_input, base_image_state, crop_points_state,
|
| 1912 |
-
preview_scale_state, zoom_state],
|
| 1913 |
-
outputs=[crop_display, crop_points_state, zoom_state, crop_status,
|
| 1914 |
-
cancel_zoom_btn])
|
| 1915 |
-
|
| 1916 |
-
def on_cancel_zoom(preview, points, scale):
|
| 1917 |
-
n = len(points or [])
|
| 1918 |
-
return (_overview(preview, points or [], scale), None,
|
| 1919 |
-
f"*Back to full view β {n} / 4 corners set*",
|
| 1920 |
-
gr.update(visible=False))
|
| 1921 |
-
|
| 1922 |
-
cancel_zoom_btn.click(fn=on_cancel_zoom,
|
| 1923 |
-
inputs=[base_image_state, crop_points_state, preview_scale_state],
|
| 1924 |
-
outputs=[crop_display, zoom_state, crop_status, cancel_zoom_btn])
|
| 1925 |
-
|
| 1926 |
-
def on_clear_crop(base_img):
|
| 1927 |
-
img, pts = clear_crop_points(base_img)
|
| 1928 |
-
return (img, pts, None, "*Points cleared β tap roughly near corner 1*",
|
| 1929 |
-
gr.update(visible=False))
|
| 1930 |
-
|
| 1931 |
-
clear_crop_btn.click(fn=on_clear_crop,
|
| 1932 |
-
inputs=[base_image_state],
|
| 1933 |
-
outputs=[crop_display, crop_points_state, zoom_state, crop_status,
|
| 1934 |
-
cancel_zoom_btn])
|
| 1935 |
-
|
| 1936 |
-
|
| 1937 |
-
# ---- Viability ------------------------------------------------------
|
| 1938 |
-
def on_run_viability(stored_masks, stored_image, seg_seconds=None):
|
| 1939 |
-
overlay, alive, dead, viab_pct, info, label_map = run_viability(
|
| 1940 |
-
stored_masks, stored_image)
|
| 1941 |
-
if seg_seconds:
|
| 1942 |
-
info = f"{info}\nSegmentation time: {float(seg_seconds):.2f} s"
|
| 1943 |
-
return (overlay, alive, dead, viab_pct, info, label_map,
|
| 1944 |
-
format_concentration(alive, CONC_PRESETS[0][1]),
|
| 1945 |
-
format_concentration(alive, CONC_PRESETS[1][1]))
|
| 1946 |
-
|
| 1947 |
-
viab_outputs = [viab_overlay, live_count_out, dead_count_out,
|
| 1948 |
-
viab_percent_out, viab_info, label_map_state,
|
| 1949 |
-
conc_1_1, conc_1_10]
|
| 1950 |
-
|
| 1951 |
-
viab_run_btn.click(
|
| 1952 |
-
fn=on_run_viability,
|
| 1953 |
-
inputs=[masks_state, image_state, seg_time_state],
|
| 1954 |
-
outputs=viab_outputs
|
| 1955 |
-
).then(
|
| 1956 |
-
fn=save_tab_result,
|
| 1957 |
-
inputs=[cell_count_out, confluency_out, viab_percent_out,
|
| 1958 |
-
live_count_out, dead_count_out, seg_time_state,
|
| 1959 |
-
raw_image_state, seg_overlay_state, viab_overlay],
|
| 1960 |
-
outputs=[result_state]
|
| 1961 |
-
)
|
| 1962 |
-
|
| 1963 |
-
segment_btn.click(
|
| 1964 |
-
fn=run_segmentation,
|
| 1965 |
-
inputs=[img_input, model_dropdown, min_size_slider, max_size_slider,
|
| 1966 |
-
use_stereo, left_excl, top_excl, crop_points_state,
|
| 1967 |
-
use_min_filter, use_max_filter],
|
| 1968 |
-
outputs=[cell_count_out, overlay_out, info_out, viability_section,
|
| 1969 |
-
masks_state, image_state, confluency_out,
|
| 1970 |
-
min_size_recommendation, raw_image_state, base_masks_state,
|
| 1971 |
-
seg_time_state, seg_overlay_state]
|
| 1972 |
-
).then(
|
| 1973 |
-
# Viability is wanted on essentially every run, so do it without a
|
| 1974 |
-
# second button press. The button stays for re-running after the
|
| 1975 |
-
# exclusion sliders or the label corrections change.
|
| 1976 |
-
fn=on_run_viability,
|
| 1977 |
-
inputs=[masks_state, image_state, seg_time_state],
|
| 1978 |
-
outputs=viab_outputs
|
| 1979 |
-
).then(
|
| 1980 |
-
fn=save_tab_result,
|
| 1981 |
-
inputs=[cell_count_out, confluency_out, viab_percent_out,
|
| 1982 |
-
live_count_out, dead_count_out, seg_time_state,
|
| 1983 |
-
raw_image_state, seg_overlay_state, viab_overlay],
|
| 1984 |
-
outputs=[result_state]
|
| 1985 |
-
)
|
| 1986 |
-
|
| 1987 |
-
# ---- Build correction grid -----------------------------------------
|
| 1988 |
-
def on_build_grid(stored_masks, stored_image, label_map, stored_raw_image):
|
| 1989 |
-
if stored_masks is None or stored_image is None or not label_map:
|
| 1990 |
-
return (gr.update(visible=False), [],
|
| 1991 |
-
gr.update(value="*Run viability analysis first.*", visible=True))
|
| 1992 |
-
masks = unpack_array(stored_masks)
|
| 1993 |
-
image_np = unpack_array(stored_image)
|
| 1994 |
-
raw_image_np = unpack_array(stored_raw_image) if stored_raw_image is not None else None
|
| 1995 |
-
features = extract_cell_features(image_np, masks)
|
| 1996 |
-
labelled = attach_viability_labels(features, masks, image_np, label_map)
|
| 1997 |
-
if not labelled:
|
| 1998 |
-
return (gr.update(visible=False), [],
|
| 1999 |
-
gr.update(value="*No cells found.*", visible=True))
|
| 2000 |
-
grid = build_correction_grid(image_np, masks, labelled, raw_image_np)
|
| 2001 |
-
n = len(labelled)
|
| 2002 |
-
dead = sum(1 for r in labelled if r["label"] == 1)
|
| 2003 |
-
msg = (f"*{n} cells β {n-dead} live (green), {dead} dead (red). "
|
| 2004 |
-
f"Tap any thumbnail to flip its label.*")
|
| 2005 |
-
return gr.update(value=grid, visible=True), labelled, gr.update(value=msg, visible=True)
|
| 2006 |
-
|
| 2007 |
-
build_grid_btn.click(
|
| 2008 |
-
fn=on_build_grid,
|
| 2009 |
-
inputs=[masks_state, image_state, label_map_state, raw_image_state],
|
| 2010 |
-
outputs=[correction_grid, labelled_state, correction_status]
|
| 2011 |
-
)
|
| 2012 |
-
|
| 2013 |
-
# ---- Grid tap β flip label, update overlay + counts ----------------
|
| 2014 |
-
def on_grid_tap(labelled, stored_masks, stored_image, stored_raw_image, evt: gr.SelectData):
|
| 2015 |
-
if not labelled or stored_masks is None:
|
| 2016 |
-
return None, labelled, "", 0, 0, 0.0, None, {}
|
| 2017 |
-
masks = unpack_array(stored_masks)
|
| 2018 |
-
image_np = unpack_array(stored_image)
|
| 2019 |
-
raw_image_np = unpack_array(stored_raw_image) if stored_raw_image is not None else None
|
| 2020 |
-
grid, updated, msg = toggle_cell_label(labelled, image_np, masks, raw_image_np, evt)
|
| 2021 |
-
|
| 2022 |
-
# Rebuild label_map from corrected labelled list
|
| 2023 |
-
new_label_map = {int(f["cell_id"]): int(f["label"]) for f in updated}
|
| 2024 |
-
overlay_np = draw_viability_overlay(image_np, masks, new_label_map)
|
| 2025 |
-
dead = sum(1 for f in updated if f["label"] == 1)
|
| 2026 |
-
alive = len(updated) - dead
|
| 2027 |
-
total = alive + dead
|
| 2028 |
-
viab_pct = (alive / total * 100) if total > 0 else 0.0
|
| 2029 |
-
|
| 2030 |
-
return (grid, updated, f"*{msg}*",
|
| 2031 |
-
alive, dead, viab_pct,
|
| 2032 |
-
Image.fromarray(overlay_np), new_label_map)
|
| 2033 |
-
|
| 2034 |
-
correction_grid.select(
|
| 2035 |
-
fn=on_grid_tap,
|
| 2036 |
-
inputs=[labelled_state, masks_state, image_state, raw_image_state],
|
| 2037 |
-
outputs=[correction_grid, labelled_state, correction_status,
|
| 2038 |
-
live_count_out, dead_count_out, viab_percent_out,
|
| 2039 |
-
viab_overlay, label_map_state]
|
| 2040 |
-
)
|
| 2041 |
-
|
| 2042 |
-
# ---- Export --------------------------------------------------------
|
| 2043 |
-
def on_export(stored_masks, stored_image, labelled, label_map):
|
| 2044 |
-
path, msg = prepare_export_corrected(stored_masks, stored_image, labelled, label_map)
|
| 2045 |
-
if path is None:
|
| 2046 |
-
return gr.update(visible=False), msg
|
| 2047 |
-
return gr.update(value=path, visible=True), msg
|
| 2048 |
-
|
| 2049 |
-
export_btn.click(
|
| 2050 |
-
fn=on_export,
|
| 2051 |
-
inputs=[masks_state, image_state, labelled_state, label_map_state],
|
| 2052 |
-
outputs=[export_file, export_info]
|
| 2053 |
-
)
|
| 2054 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2055 |
|
| 2056 |
|
| 2057 |
-
|
| 2058 |
-
# Gradio interface
|
| 2059 |
-
# ---------------------------------------------------------------------------
|
| 2060 |
-
with gr.Blocks(
|
| 2061 |
-
title="CellposeCellCounter",
|
| 2062 |
-
theme=gr.themes.Soft(),
|
| 2063 |
-
) as demo:
|
| 2064 |
gr.Markdown("# CellposeCellCounter")
|
| 2065 |
-
|
| 2066 |
-
|
| 2067 |
-
#
|
| 2068 |
-
|
| 2069 |
-
|
| 2070 |
-
result_states = [gr.State(value=None) for _ in range(4)]
|
| 2071 |
-
|
| 2072 |
-
# Build Tabs 1β4 with a loop
|
| 2073 |
-
for i in range(4):
|
| 2074 |
-
build_tab(i + 1, masks_states[i], image_states[i], result_states[i])
|
| 2075 |
-
|
| 2076 |
-
# -------------------------------------------------------------------------
|
| 2077 |
-
# Tab 5 β Summary
|
| 2078 |
-
# -------------------------------------------------------------------------
|
| 2079 |
-
with gr.Tab("Tab 5 β Summary"):
|
| 2080 |
-
gr.Markdown("## Average Results Across All Tabs")
|
| 2081 |
gr.Markdown(
|
| 2082 |
-
"
|
| 2083 |
-
"
|
|
|
|
|
|
|
| 2084 |
)
|
| 2085 |
|
| 2086 |
-
|
| 2087 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2088 |
with gr.Row():
|
| 2089 |
-
|
| 2090 |
-
|
| 2091 |
-
|
| 2092 |
-
|
| 2093 |
-
|
| 2094 |
-
|
| 2095 |
-
gr.Markdown("### Cell Concentration (from the mean across tabs)")
|
| 2096 |
with gr.Row():
|
| 2097 |
-
|
| 2098 |
-
|
| 2099 |
-
|
| 2100 |
-
|
| 2101 |
-
summary_conc_10x = gr.Textbox(
|
| 2102 |
-
label=f"Concentration β {CONC_PRESETS[1][0]}",
|
| 2103 |
-
lines=8, interactive=False,
|
| 2104 |
-
)
|
| 2105 |
-
|
| 2106 |
-
refresh_btn.click(
|
| 2107 |
-
fn=compute_summary,
|
| 2108 |
-
inputs=result_states, # list of 4 gr.State components
|
| 2109 |
-
outputs=[avg_count_out, avg_conf_out, avg_viab_out, summary_box,
|
| 2110 |
-
summary_conc_2x, summary_conc_10x]
|
| 2111 |
-
)
|
| 2112 |
|
| 2113 |
gr.Markdown("### Export")
|
| 2114 |
with gr.Row():
|
| 2115 |
-
|
| 2116 |
-
|
| 2117 |
-
|
| 2118 |
-
|
| 2119 |
-
|
| 2120 |
-
|
| 2121 |
-
|
| 2122 |
-
|
| 2123 |
-
|
| 2124 |
-
|
| 2125 |
-
|
| 2126 |
-
|
| 2127 |
-
|
| 2128 |
-
|
| 2129 |
-
|
| 2130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2131 |
)
|
| 2132 |
-
|
| 2133 |
with gr.Row():
|
| 2134 |
-
|
| 2135 |
-
|
| 2136 |
-
|
| 2137 |
-
|
| 2138 |
-
|
| 2139 |
-
|
| 2140 |
-
|
| 2141 |
-
|
| 2142 |
-
|
| 2143 |
-
|
| 2144 |
-
|
| 2145 |
-
|
| 2146 |
-
|
| 2147 |
-
|
| 2148 |
-
|
| 2149 |
-
|
| 2150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2151 |
|
|
|
|
|
|
|
|
|
|
| 2152 |
|
| 2153 |
|
| 2154 |
if __name__ == "__main__":
|
| 2155 |
_prefetch_models()
|
| 2156 |
-
demo.launch()
|
|
|
|
| 1595 |
f"{corrected} label(s) manually corrected.")
|
| 1596 |
return path, msg
|
| 1597 |
|
| 1598 |
+
# ===========================================================================
|
| 1599 |
+
# UI
|
| 1600 |
+
# ===========================================================================
|
| 1601 |
+
HEMO_MODEL = "Hemocytometer Model"
|
| 1602 |
+
GENERAL_MODEL = "General Model"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1603 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1604 |
|
| 1605 |
+
def _crop_picker(label):
|
| 1606 |
+
"""Upload + tap-to-zoom corner picker. Returns the components and states.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1607 |
|
| 1608 |
+
Factored out because Tab 1 needs four of these and Tab 2 needs one; the
|
| 1609 |
+
wiring is identical, only the label differs.
|
| 1610 |
+
"""
|
| 1611 |
+
img_input = gr.Image(type="pil", label=label, image_mode="RGB", height=220)
|
| 1612 |
+
crop_display = gr.Image(
|
| 1613 |
+
type="pil", label="Tap roughly, then precisely in the zoom",
|
| 1614 |
+
interactive=True, height=340, format="jpeg",
|
| 1615 |
+
show_download_button=False,
|
| 1616 |
+
)
|
| 1617 |
+
crop_status = gr.Markdown("*Upload an image to set the counting square*")
|
| 1618 |
+
with gr.Row():
|
| 1619 |
+
clear_btn = gr.Button("β Clear", size="sm")
|
| 1620 |
+
cancel_btn = gr.Button("β© Back to full view", size="sm", visible=False)
|
| 1621 |
+
|
| 1622 |
+
st = {
|
| 1623 |
+
"img": img_input,
|
| 1624 |
+
"display": crop_display,
|
| 1625 |
+
"status": crop_status,
|
| 1626 |
+
"points": gr.State(value=[]),
|
| 1627 |
+
"base": gr.State(value=None),
|
| 1628 |
+
"scale": gr.State(value=1.0),
|
| 1629 |
+
"zoom": gr.State(value=None),
|
| 1630 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1631 |
|
| 1632 |
+
def on_upload(img):
|
| 1633 |
+
if img is None:
|
| 1634 |
+
return None, None, 1.0, "*Upload an image to set the counting square*"
|
| 1635 |
+
preview, scale = make_preview(img)
|
| 1636 |
+
return preview, preview, scale, "*Tap roughly near corner 1*"
|
| 1637 |
+
|
| 1638 |
+
img_input.change(
|
| 1639 |
+
fn=on_upload, inputs=[img_input],
|
| 1640 |
+
outputs=[crop_display, st["base"], st["scale"], crop_status]
|
| 1641 |
+
).then(fn=lambda: ([], None), outputs=[st["points"], st["zoom"]])
|
| 1642 |
+
|
| 1643 |
+
def _overview(preview, points, scale):
|
| 1644 |
+
return draw_polygon_overlay(
|
| 1645 |
+
preview, [(int(x * scale), int(y * scale)) for x, y in points])
|
| 1646 |
+
|
| 1647 |
+
def on_click(full_img, preview, points, scale, zoom, evt: gr.SelectData):
|
| 1648 |
+
points = list(points or [])
|
| 1649 |
+
if preview is None or full_img is None:
|
| 1650 |
+
return gr.update(), points, zoom, gr.update(), gr.update()
|
| 1651 |
+
if evt is None or evt.index is None or evt.index[0] is None:
|
| 1652 |
+
return (gr.update(), points, zoom,
|
| 1653 |
+
"*Tap not registered β try again inside the image*", gr.update())
|
| 1654 |
+
tx, ty = int(evt.index[0]), int(evt.index[1])
|
| 1655 |
+
|
| 1656 |
+
if zoom is None:
|
| 1657 |
+
if len(points) >= 4:
|
| 1658 |
+
return (_overview(preview, points, scale), points, None,
|
| 1659 |
+
"*4 corners set β β β Clear to redo*",
|
| 1660 |
+
gr.update(visible=False))
|
| 1661 |
+
view, z = make_zoom_view(full_img, tx / scale, ty / scale)
|
| 1662 |
+
return (view, points, z,
|
| 1663 |
+
f"*Zoomed β tap corner {len(points) + 1} precisely*",
|
| 1664 |
+
gr.update(visible=True))
|
| 1665 |
+
|
| 1666 |
+
x = zoom["x0"] + tx / zoom["zscale"]
|
| 1667 |
+
y = zoom["y0"] + ty / zoom["zscale"]
|
| 1668 |
+
W, H = full_img.size
|
| 1669 |
+
pts = points + [(int(min(max(0, x), W - 1)), int(min(max(0, y), H - 1)))]
|
| 1670 |
+
n = len(pts)
|
| 1671 |
+
msg = (f"*{n} / 4 corners β tap roughly near corner {n + 1}*" if n < 4
|
| 1672 |
+
else "*4 corners set β*")
|
| 1673 |
+
return (_overview(preview, pts, scale), pts, None, msg,
|
| 1674 |
+
gr.update(visible=False))
|
| 1675 |
+
|
| 1676 |
+
crop_display.select(
|
| 1677 |
+
fn=on_click,
|
| 1678 |
+
inputs=[img_input, st["base"], st["points"], st["scale"], st["zoom"]],
|
| 1679 |
+
outputs=[crop_display, st["points"], st["zoom"], crop_status, cancel_btn])
|
| 1680 |
+
|
| 1681 |
+
cancel_btn.click(
|
| 1682 |
+
fn=lambda preview, pts, sc: (
|
| 1683 |
+
_overview(preview, pts or [], sc), None,
|
| 1684 |
+
f"*Back to full view β {len(pts or [])} / 4 corners*",
|
| 1685 |
+
gr.update(visible=False)),
|
| 1686 |
+
inputs=[st["base"], st["points"], st["scale"]],
|
| 1687 |
+
outputs=[crop_display, st["zoom"], crop_status, cancel_btn])
|
| 1688 |
+
|
| 1689 |
+
clear_btn.click(
|
| 1690 |
+
fn=lambda base: (base, [], None, "*Cleared β tap roughly near corner 1*",
|
| 1691 |
+
gr.update(visible=False)),
|
| 1692 |
+
inputs=[st["base"]],
|
| 1693 |
+
outputs=[crop_display, st["points"], st["zoom"], crop_status, cancel_btn])
|
| 1694 |
+
|
| 1695 |
+
return st
|
| 1696 |
+
|
| 1697 |
+
|
| 1698 |
+
def _segment_one(image, points, model_name, use_min, min_px,
|
| 1699 |
+
use_stereo, left_pct, top_pct, want_viability):
|
| 1700 |
+
"""Segment (and optionally classify) a single image. Returns a dict."""
|
| 1701 |
+
if image is None:
|
| 1702 |
+
return None
|
| 1703 |
+
out = run_segmentation(image, model_name, min_px, 10000,
|
| 1704 |
+
use_stereo, left_pct, top_pct, points,
|
| 1705 |
+
use_min, False)
|
| 1706 |
+
(cell_count, seg_overlay, info_msg, _vis, packed_masks, packed_img,
|
| 1707 |
+
confluency, _rec, packed_raw, _base, seg_seconds, _ov) = out
|
| 1708 |
+
if packed_masks is None:
|
| 1709 |
+
return {"error": info_msg}
|
| 1710 |
+
|
| 1711 |
+
res = {"count": cell_count, "confluency": confluency,
|
| 1712 |
+
"seg_overlay": seg_overlay, "seconds": seg_seconds,
|
| 1713 |
+
"info": info_msg, "raw": packed_raw,
|
| 1714 |
+
"masks": packed_masks, "img": packed_img, "label_map": None,
|
| 1715 |
+
"alive": None, "dead": None, "viab": None, "viab_overlay": None}
|
| 1716 |
+
|
| 1717 |
+
if want_viability:
|
| 1718 |
+
v_overlay, alive, dead, viab_pct, _vinfo, lm = run_viability(
|
| 1719 |
+
packed_masks, packed_img)
|
| 1720 |
+
res.update(alive=alive, dead=dead, viab=viab_pct,
|
| 1721 |
+
viab_overlay=v_overlay, label_map=lm)
|
| 1722 |
+
return res
|
| 1723 |
+
|
| 1724 |
+
|
| 1725 |
+
def _build_correction(res):
|
| 1726 |
+
"""Per-cell thumbnails for manual live/dead correction.
|
| 1727 |
+
|
| 1728 |
+
Only called when the user has ticked the correction checkbox: the grid costs
|
| 1729 |
+
one crop per cell, so building it for four squares that nobody inspects is
|
| 1730 |
+
pure waste.
|
| 1731 |
+
"""
|
| 1732 |
+
try:
|
| 1733 |
+
masks = unpack_array(res["masks"])
|
| 1734 |
+
img = unpack_array(res["img"])
|
| 1735 |
+
raw = unpack_array(res["raw"]) if res.get("raw") is not None else None
|
| 1736 |
+
feats = extract_cell_features(img, masks)
|
| 1737 |
+
labelled = attach_viability_labels(feats, masks, img, res.get("label_map"))
|
| 1738 |
+
return build_correction_grid(img, masks, labelled, raw), labelled
|
| 1739 |
+
except Exception as e:
|
| 1740 |
+
print("correction grid failed:", e)
|
| 1741 |
+
return None, []
|
| 1742 |
|
| 1743 |
|
| 1744 |
+
with gr.Blocks(title="CellposeCellCounter", theme=gr.themes.Soft()) as demo:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1745 |
gr.Markdown("# CellposeCellCounter")
|
| 1746 |
+
|
| 1747 |
+
# =======================================================================
|
| 1748 |
+
# Tab 1 β hemocytometer counting (4 squares, one run)
|
| 1749 |
+
# =======================================================================
|
| 1750 |
+
with gr.Tab("tab1_hemacyto_counting"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1751 |
gr.Markdown(
|
| 1752 |
+
"### Hemocytometer counting\n"
|
| 1753 |
+
"Upload the **four corner squares**, set four corners on each, then "
|
| 1754 |
+
"press **Run segmentation** once. Uses the hemocytometer model and "
|
| 1755 |
+
"runs viability automatically."
|
| 1756 |
)
|
| 1757 |
|
| 1758 |
+
with gr.Accordion("Settings", open=False):
|
| 1759 |
+
h_use_min = gr.Checkbox(
|
| 1760 |
+
label="Enable minimum size filter", value=True,
|
| 1761 |
+
info="At 0 the app uses the 25th percentile of each image β "
|
| 1762 |
+
"set a fixed value for reproducible counts.")
|
| 1763 |
+
h_min_px = gr.Slider(0, 500, value=0, step=10,
|
| 1764 |
+
label="Minimum Cell Size (pixels)")
|
| 1765 |
+
h_use_stereo = gr.Checkbox(
|
| 1766 |
+
label="Enable stereological counting", value=True,
|
| 1767 |
+
info="Cells touching the left/top exclusion lines are excluded.")
|
| 1768 |
+
h_left = gr.Slider(0, 50, value=1, step=1, label="Left exclusion (%)")
|
| 1769 |
+
h_top = gr.Slider(0, 50, value=1, step=1, label="Top exclusion (%)")
|
| 1770 |
+
h_correct = gr.Checkbox(
|
| 1771 |
+
label="Enable manual label correction", value=False,
|
| 1772 |
+
info="Adds a tap-to-flip thumbnail grid per square. Off by "
|
| 1773 |
+
"default because building it costs one crop per cell.")
|
| 1774 |
+
|
| 1775 |
+
pickers, seg_imgs, viab_imgs, stat_boxes = [], [], [], []
|
| 1776 |
+
corr_accordions, corr_grids, corr_btns, corr_infos, corr_files = [], [], [], [], []
|
| 1777 |
+
masks_sts, img_sts, raw_sts, lab_sts, lmap_sts = [], [], [], [], []
|
| 1778 |
+
for _i in range(4):
|
| 1779 |
+
with gr.Accordion(f"Square {_i + 1}", open=(_i == 0)):
|
| 1780 |
+
with gr.Row():
|
| 1781 |
+
with gr.Column(scale=1):
|
| 1782 |
+
pickers.append(_crop_picker(f"Square {_i + 1} image"))
|
| 1783 |
+
with gr.Column(scale=1):
|
| 1784 |
+
seg_imgs.append(gr.Image(type="pil", label="Segmentation",
|
| 1785 |
+
height=260))
|
| 1786 |
+
viab_imgs.append(gr.Image(type="pil",
|
| 1787 |
+
label="Viability (green=live Β· red=dead)",
|
| 1788 |
+
height=260))
|
| 1789 |
+
stat_boxes.append(gr.Textbox(label="Results", lines=7,
|
| 1790 |
+
interactive=False))
|
| 1791 |
+
with gr.Accordion("Label correction", open=False,
|
| 1792 |
+
visible=False) as _acc:
|
| 1793 |
+
corr_accordions.append(_acc)
|
| 1794 |
+
gr.Markdown("Tap a thumbnail to flip live β dead "
|
| 1795 |
+
"(green = live, red = dead).")
|
| 1796 |
+
corr_grids.append(gr.Image(
|
| 1797 |
+
type="pil", label="Correction grid",
|
| 1798 |
+
interactive=True, height=420, format="jpeg",
|
| 1799 |
+
show_download_button=False))
|
| 1800 |
+
with gr.Row():
|
| 1801 |
+
corr_btns.append(gr.Button("β¬οΈ Export corrected CSV",
|
| 1802 |
+
size="sm"))
|
| 1803 |
+
corr_infos.append(gr.Textbox(label="Export status",
|
| 1804 |
+
lines=2, interactive=False))
|
| 1805 |
+
corr_files.append(gr.File(label="Download CSV", visible=False))
|
| 1806 |
+
masks_sts.append(gr.State(value=None))
|
| 1807 |
+
img_sts.append(gr.State(value=None))
|
| 1808 |
+
raw_sts.append(gr.State(value=None))
|
| 1809 |
+
lab_sts.append(gr.State(value=None))
|
| 1810 |
+
lmap_sts.append(gr.State(value=None))
|
| 1811 |
+
|
| 1812 |
+
run_all_btn = gr.Button("π¬ Run segmentation (all 4 squares)",
|
| 1813 |
+
variant="primary", size="lg")
|
| 1814 |
+
run_status = gr.Textbox(label="Progress", lines=2, interactive=False)
|
| 1815 |
+
|
| 1816 |
+
gr.Markdown("## Summary across the four squares")
|
| 1817 |
with gr.Row():
|
| 1818 |
+
s_count = gr.Number(label="Mean total cells", precision=1)
|
| 1819 |
+
s_conf = gr.Number(label="Mean confluency (%)", precision=1)
|
| 1820 |
+
s_viab = gr.Number(label="Mean viability (%)", precision=1)
|
| 1821 |
+
s_break = gr.Textbox(label="Per-square breakdown", lines=11,
|
| 1822 |
+
interactive=False)
|
|
|
|
|
|
|
| 1823 |
with gr.Row():
|
| 1824 |
+
s_c2 = gr.Textbox(label=f"Concentration β {CONC_PRESETS[0][0]}",
|
| 1825 |
+
lines=8, interactive=False)
|
| 1826 |
+
s_c10 = gr.Textbox(label=f"Concentration β {CONC_PRESETS[1][0]}",
|
| 1827 |
+
lines=8, interactive=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1828 |
|
| 1829 |
gr.Markdown("### Export")
|
| 1830 |
with gr.Row():
|
| 1831 |
+
s_csv_btn = gr.Button("β¬οΈ Export summary CSV", variant="secondary")
|
| 1832 |
+
s_pdf_btn = gr.Button("π Export summary PDF (with images)",
|
| 1833 |
+
variant="secondary")
|
| 1834 |
+
s_export_info = gr.Textbox(label="Export status", lines=2,
|
| 1835 |
+
interactive=False)
|
| 1836 |
+
s_csv_file = gr.File(label="Download CSV", visible=False)
|
| 1837 |
+
s_pdf_file = gr.File(label="Download PDF", visible=False)
|
| 1838 |
+
|
| 1839 |
+
result_states = [gr.State(value=None) for _ in range(4)]
|
| 1840 |
+
|
| 1841 |
+
def run_all(i1, p1, i2, p2, i3, p3, i4, p4,
|
| 1842 |
+
use_min, min_px, use_stereo, left_pct, top_pct, want_correct):
|
| 1843 |
+
imgs = [(i1, p1), (i2, p2), (i3, p3), (i4, p4)]
|
| 1844 |
+
outs, sts, results, done, notes = [], [], [], 0, []
|
| 1845 |
+
|
| 1846 |
+
def _blank(msg):
|
| 1847 |
+
outs.extend([None, None, msg, None])
|
| 1848 |
+
sts.extend([None, None, None, None, None])
|
| 1849 |
+
results.append(None)
|
| 1850 |
+
|
| 1851 |
+
for idx, (img, pts) in enumerate(imgs, start=1):
|
| 1852 |
+
r = _segment_one(img, pts, HEMO_MODEL, use_min, min_px,
|
| 1853 |
+
use_stereo, left_pct, top_pct, True)
|
| 1854 |
+
if r is None:
|
| 1855 |
+
_blank("No image uploaded.")
|
| 1856 |
+
continue
|
| 1857 |
+
if "error" in r:
|
| 1858 |
+
_blank(r["error"])
|
| 1859 |
+
notes.append(f"Square {idx} failed")
|
| 1860 |
+
continue
|
| 1861 |
+
if not (pts and len(pts) >= 3):
|
| 1862 |
+
notes.append(f"Square {idx}: no crop set β whole image used")
|
| 1863 |
+
|
| 1864 |
+
grid, labelled = (_build_correction(r) if want_correct
|
| 1865 |
+
else (None, None))
|
| 1866 |
+
|
| 1867 |
+
txt = (f"Total cells: {r['count']}\n"
|
| 1868 |
+
f"Live (green): {r['alive']}\n"
|
| 1869 |
+
f"Dead (red): {r['dead']}\n"
|
| 1870 |
+
f"Viability: {(r['viab'] or 0):.1f}%\n"
|
| 1871 |
+
f"Confluency: {r['confluency']:.1f}%\n"
|
| 1872 |
+
f"Segmentation time: {r['seconds']:.2f} s")
|
| 1873 |
+
outs.extend([r["seg_overlay"], r["viab_overlay"], txt, grid])
|
| 1874 |
+
sts.extend([r["masks"], r["img"], r["raw"], labelled,
|
| 1875 |
+
r["label_map"]])
|
| 1876 |
+
results.append(save_tab_result(
|
| 1877 |
+
r["count"], r["confluency"], r["viab"], r["alive"], r["dead"],
|
| 1878 |
+
r["seconds"], r["raw"], r["seg_overlay"], r["viab_overlay"]))
|
| 1879 |
+
done += 1
|
| 1880 |
+
|
| 1881 |
+
avg_c, avg_f, avg_v, brk, c2, c10 = compute_summary(*results)
|
| 1882 |
+
status = f"Segmented {done} of 4 squares."
|
| 1883 |
+
if notes:
|
| 1884 |
+
status += " β " + "; ".join(notes)
|
| 1885 |
+
return outs + sts + results + [avg_c, avg_f, avg_v, brk, c2, c10, status]
|
| 1886 |
+
|
| 1887 |
+
run_all_outputs = []
|
| 1888 |
+
for _i in range(4):
|
| 1889 |
+
run_all_outputs += [seg_imgs[_i], viab_imgs[_i], stat_boxes[_i],
|
| 1890 |
+
corr_grids[_i]]
|
| 1891 |
+
for _i in range(4):
|
| 1892 |
+
run_all_outputs += [masks_sts[_i], img_sts[_i], raw_sts[_i],
|
| 1893 |
+
lab_sts[_i], lmap_sts[_i]]
|
| 1894 |
+
run_all_outputs += result_states
|
| 1895 |
+
run_all_outputs += [s_count, s_conf, s_viab, s_break, s_c2, s_c10,
|
| 1896 |
+
run_status]
|
| 1897 |
+
|
| 1898 |
+
h_correct.change(
|
| 1899 |
+
fn=lambda on: [gr.update(visible=bool(on))] * 4,
|
| 1900 |
+
inputs=[h_correct], outputs=corr_accordions)
|
| 1901 |
+
|
| 1902 |
+
for _i in range(4):
|
| 1903 |
+
corr_grids[_i].select(
|
| 1904 |
+
fn=toggle_cell_label,
|
| 1905 |
+
inputs=[lab_sts[_i], img_sts[_i], masks_sts[_i], raw_sts[_i]],
|
| 1906 |
+
outputs=[corr_grids[_i], lab_sts[_i]])
|
| 1907 |
+
|
| 1908 |
+
def _make_export(k):
|
| 1909 |
+
def _export(masks, img, labelled, lmap):
|
| 1910 |
+
path, msg = prepare_export_corrected(masks, img, labelled, lmap)
|
| 1911 |
+
return ((gr.update(visible=False) if path is None
|
| 1912 |
+
else gr.update(value=path, visible=True)), msg)
|
| 1913 |
+
return _export
|
| 1914 |
+
|
| 1915 |
+
corr_btns[_i].click(
|
| 1916 |
+
fn=_make_export(_i),
|
| 1917 |
+
inputs=[masks_sts[_i], img_sts[_i], lab_sts[_i], lmap_sts[_i]],
|
| 1918 |
+
outputs=[corr_files[_i], corr_infos[_i]])
|
| 1919 |
+
|
| 1920 |
+
run_all_btn.click(
|
| 1921 |
+
fn=run_all,
|
| 1922 |
+
inputs=[pickers[0]["img"], pickers[0]["points"],
|
| 1923 |
+
pickers[1]["img"], pickers[1]["points"],
|
| 1924 |
+
pickers[2]["img"], pickers[2]["points"],
|
| 1925 |
+
pickers[3]["img"], pickers[3]["points"],
|
| 1926 |
+
h_use_min, h_min_px, h_use_stereo, h_left, h_top,
|
| 1927 |
+
h_correct],
|
| 1928 |
+
outputs=run_all_outputs)
|
| 1929 |
+
|
| 1930 |
+
def _csv(*rs):
|
| 1931 |
+
path, msg = export_summary_csv(*rs)
|
| 1932 |
+
return (gr.update(visible=False) if path is None
|
| 1933 |
+
else gr.update(value=path, visible=True)), msg
|
| 1934 |
+
|
| 1935 |
+
def _pdf(*rs):
|
| 1936 |
+
path, msg = export_summary_pdf(*rs)
|
| 1937 |
+
return (gr.update(visible=False) if path is None
|
| 1938 |
+
else gr.update(value=path, visible=True)), msg
|
| 1939 |
+
|
| 1940 |
+
s_csv_btn.click(fn=_csv, inputs=result_states,
|
| 1941 |
+
outputs=[s_csv_file, s_export_info])
|
| 1942 |
+
s_pdf_btn.click(fn=_pdf, inputs=result_states,
|
| 1943 |
+
outputs=[s_pdf_file, s_export_info])
|
| 1944 |
+
|
| 1945 |
+
# =======================================================================
|
| 1946 |
+
# Tab 2 β confluency only (general model, no viability)
|
| 1947 |
+
# =======================================================================
|
| 1948 |
+
with gr.Tab("tab2_confluency"):
|
| 1949 |
+
gr.Markdown(
|
| 1950 |
+
"### Confluency estimation\n"
|
| 1951 |
+
"Uses the **general** model. Cropping is optional β leave the corners "
|
| 1952 |
+
"unset to measure the whole field. No viability classification."
|
| 1953 |
)
|
|
|
|
| 1954 |
with gr.Row():
|
| 1955 |
+
with gr.Column(scale=1):
|
| 1956 |
+
c_pick = _crop_picker("Image")
|
| 1957 |
+
c_run = gr.Button("π¬ Run segmentation", variant="primary",
|
| 1958 |
+
size="lg")
|
| 1959 |
+
with gr.Column(scale=1):
|
| 1960 |
+
c_overlay = gr.Image(type="pil", label="Segmentation", height=340)
|
| 1961 |
+
with gr.Row():
|
| 1962 |
+
c_count = gr.Number(label="Cells detected", precision=0)
|
| 1963 |
+
c_conf = gr.Number(label="Confluency (%)", precision=2)
|
| 1964 |
+
c_info = gr.Textbox(label="Processing info", lines=6,
|
| 1965 |
+
interactive=False)
|
| 1966 |
+
|
| 1967 |
+
def run_confluency(img, pts):
|
| 1968 |
+
if img is None:
|
| 1969 |
+
return None, 0, 0.0, "Upload an image first."
|
| 1970 |
+
r = _segment_one(img, pts, GENERAL_MODEL, False, 0,
|
| 1971 |
+
False, 0, 0, False)
|
| 1972 |
+
if r is None or "error" in r:
|
| 1973 |
+
return None, 0, 0.0, (r or {}).get("error", "Segmentation failed.")
|
| 1974 |
+
note = "" if (pts and len(pts) >= 3) else "\nWhole image used (no crop set)."
|
| 1975 |
+
return (r["seg_overlay"], r["count"], round(r["confluency"], 2),
|
| 1976 |
+
r["info"] + note)
|
| 1977 |
|
| 1978 |
+
c_run.click(fn=run_confluency,
|
| 1979 |
+
inputs=[c_pick["img"], c_pick["points"]],
|
| 1980 |
+
outputs=[c_overlay, c_count, c_conf, c_info])
|
| 1981 |
|
| 1982 |
|
| 1983 |
if __name__ == "__main__":
|
| 1984 |
_prefetch_models()
|
| 1985 |
+
demo.launch()
|