LiangLabUMB commited on
Commit
1118e3e
Β·
verified Β·
1 Parent(s): 303e3df

Upload app.py

Browse files

changed layout

Files changed (1) hide show
  1. app.py +366 -537
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
- def build_tab(tab_index, masks_state, image_state, result_state):
1599
- with gr.Tab(f"Tab {tab_index}"):
1600
- gr.Markdown("Run segmentation")
1601
-
1602
- # Per-tab state: list of (x,y) crop polygon points
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
- use_stereo.change(
1808
- fn=toggle_stereological_mode,
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
- def on_exclusion_change(stored_base, stored_image, use_stereo_on,
1838
- left_pct, top_pct):
1839
- """Re-apply the exclusion zones to existing masks β€” no re-segmentation."""
1840
- if stored_base is None or stored_image is None:
1841
- return (gr.update(), gr.update(), gr.update(),
1842
- gr.update(), gr.update())
1843
- base_masks = unpack_array(stored_base)
1844
- image_np = unpack_array(stored_image)
1845
- masks, cell_count, confluency, overlay_pil, excluded = render_segmentation(
1846
- base_masks, image_np, use_stereo_on, left_pct, top_pct
1847
- )
1848
- if use_stereo_on:
1849
- msg = (f"Stereological exclusion (Left: {left_pct}%, Top: {top_pct}%): "
1850
- f"{excluded} cells excluded, {cell_count} counted.\n"
1851
- f"Confluency: {confluency:.1f}%\n"
1852
- f"Re-run viability classification to update live/dead counts.")
1853
- else:
1854
- msg = (f"Stereological exclusion off β€” {cell_count} cells counted.\n"
1855
- f"Confluency: {confluency:.1f}%")
1856
- return cell_count, overlay_pil, confluency, pack_array(masks), msg
1857
-
1858
- exclusion_inputs = [base_masks_state, image_state, use_stereo,
1859
- left_excl, top_excl]
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
- gr.Markdown("For accurate cell confluency, crop the image to display only desired area. Note that some image file types are not yet supported. PNG and JPEG are preferred.")
2066
-
2067
- # Shared mask/image state (one pair per tab so tabs don't clobber each other)
2068
- masks_states = [gr.State(value=None) for _ in range(4)]
2069
- image_states = [gr.State(value=None) for _ in range(4)]
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
- "Run segmentation in one or more tabs, "
2083
- "then click **Refresh Summary** to see the averages."
 
 
2084
  )
2085
 
2086
- refresh_btn = gr.Button("πŸ”„ Refresh Summary", variant="primary", size="lg")
2087
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2088
  with gr.Row():
2089
- avg_count_out = gr.Number(label="Avg Cell Count", precision=1)
2090
- avg_conf_out = gr.Number(label="Avg Confluency (%)", precision=1)
2091
- avg_viab_out = gr.Number(label="Avg Viability (%)", precision=1)
2092
-
2093
- summary_box = gr.Textbox(label="Per-Tab Breakdown", lines=11)
2094
-
2095
- gr.Markdown("### Cell Concentration (from the mean across tabs)")
2096
  with gr.Row():
2097
- summary_conc_2x = gr.Textbox(
2098
- label=f"Concentration β€” {CONC_PRESETS[0][0]}",
2099
- lines=8, interactive=False,
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
- summary_export_btn = gr.Button("⬇️ Export summary CSV",
2116
- variant="secondary")
2117
- summary_export_info = gr.Textbox(label="Export status", lines=2,
2118
- interactive=False)
2119
- summary_export_file = gr.File(label="Download summary CSV", visible=False)
2120
-
2121
- def on_export_summary(*tab_results):
2122
- path, msg = export_summary_csv(*tab_results)
2123
- if path is None:
2124
- return gr.update(visible=False), msg
2125
- return gr.update(value=path, visible=True), msg
2126
-
2127
- summary_export_btn.click(
2128
- fn=on_export_summary,
2129
- inputs=result_states,
2130
- outputs=[summary_export_file, summary_export_info]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2131
  )
2132
-
2133
  with gr.Row():
2134
- summary_pdf_btn = gr.Button("πŸ“„ Export summary PDF (with images)",
2135
- variant="secondary")
2136
- summary_pdf_info = gr.Textbox(label="PDF status", lines=2,
2137
- interactive=False)
2138
- summary_pdf_file = gr.File(label="Download summary PDF", visible=False)
2139
-
2140
- def on_export_summary_pdf(*tab_results):
2141
- path, msg = export_summary_pdf(*tab_results)
2142
- if path is None:
2143
- return gr.update(visible=False), msg
2144
- return gr.update(value=path, visible=True), msg
2145
-
2146
- summary_pdf_btn.click(
2147
- fn=on_export_summary_pdf,
2148
- inputs=result_states,
2149
- outputs=[summary_pdf_file, summary_pdf_info]
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()