Spaces:
Running on Zero
Running on Zero
Upload 4 files
Browse files
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🚀
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: pink
|
|
|
|
| 1 |
---
|
| 2 |
+
title: CellposeCellCounter_Mobile
|
| 3 |
emoji: 🚀
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: pink
|
app.py
CHANGED
|
@@ -548,53 +548,77 @@ def _merge_patch_masks(patch_results, full_h, full_w, overlap=PATCH_OVERLAP):
|
|
| 548 |
id_offset += max_id + 1
|
| 549 |
|
| 550 |
# --- Renumber to compact sequential IDs ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
unique_ids = np.unique(full_mask)
|
| 552 |
unique_ids = unique_ids[unique_ids > 0]
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
|
|
|
|
|
|
| 556 |
|
| 557 |
return renumbered
|
| 558 |
|
| 559 |
|
| 560 |
def _segment_patch(args):
|
| 561 |
-
"""Worker: run cellpose on a single patch. Called from a thread pool.
|
| 562 |
-
patch_np, row_start, col_start, model_filename, hf_repo = args
|
| 563 |
-
# Each thread uses the shared loaded_models cache (GIL-safe for reads;
|
| 564 |
-
# model.eval() releases the GIL during GPU work so threads overlap.)
|
| 565 |
-
model_path = hf_hub_download(repo_id=hf_repo, filename=model_filename)
|
| 566 |
-
if model_filename in loaded_models:
|
| 567 |
-
model = loaded_models[model_filename]
|
| 568 |
-
else:
|
| 569 |
-
model = models.CellposeModel(gpu=True, pretrained_model=model_path)
|
| 570 |
-
loaded_models[model_filename] = model
|
| 571 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
mask, _, _ = model.eval(patch_np, diameter=None, channels=[0, 0])
|
| 573 |
return mask, row_start, col_start
|
| 574 |
|
| 575 |
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 589 |
"""
|
| 590 |
_t_gpu = time.perf_counter()
|
| 591 |
h, w = image_np.shape[:2]
|
| 592 |
-
model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
|
| 593 |
-
if model_filename in loaded_models:
|
| 594 |
-
model = loaded_models[model_filename]
|
| 595 |
-
else:
|
| 596 |
-
model = models.CellposeModel(gpu=True, pretrained_model=model_path)
|
| 597 |
-
loaded_models[model_filename] = model
|
| 598 |
|
| 599 |
# Small images: no benefit from patching
|
| 600 |
if max(h, w) <= MIN_PATCH_DIM * 2:
|
|
@@ -604,13 +628,10 @@ def run_segmentation_patched(image_np, model_filename):
|
|
| 604 |
patches = _split_patches(image_np)
|
| 605 |
n_patches = len(patches)
|
| 606 |
|
| 607 |
-
#
|
| 608 |
-
args_list = [
|
| 609 |
-
(patch, r, c, model_filename, HF_REPO_ID)
|
| 610 |
-
for patch, r, c in patches
|
| 611 |
-
]
|
| 612 |
|
| 613 |
-
patch_results = [] # (mask, row_start, col_start) in
|
| 614 |
|
| 615 |
# ThreadPoolExecutor: GPU kernels release the GIL so threads overlap on GPU
|
| 616 |
with ThreadPoolExecutor(max_workers=min(n_patches, 4)) as pool:
|
|
@@ -626,6 +647,34 @@ def run_segmentation_patched(image_np, model_filename):
|
|
| 626 |
return full_mask, n_patches, time.perf_counter() - _t_gpu
|
| 627 |
|
| 628 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
def render_segmentation(base_masks, processed_image_np,
|
| 630 |
use_stereology, left_exclusion, top_exclusion):
|
| 631 |
"""
|
|
@@ -661,16 +710,19 @@ def render_segmentation(base_masks, processed_image_np,
|
|
| 661 |
return masks, cell_count, confluency, Image.fromarray(overlay), excluded_count
|
| 662 |
|
| 663 |
|
| 664 |
-
def
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
|
|
|
|
|
|
|
|
|
| 668 |
image_np = np.array(image)
|
| 669 |
|
| 670 |
# Crop BEFORE downscaling, so the ROI is sampled from the original pixels
|
| 671 |
-
# rather than upscaled out of a
|
| 672 |
# clicked on the full-resolution upload, so they are already in this space.
|
| 673 |
-
# (Need
|
| 674 |
if crop_points and len(crop_points) >= 3:
|
| 675 |
import json
|
| 676 |
if len(crop_points) == 4:
|
|
@@ -682,7 +734,7 @@ def run_segmentation(image, model_choice, min_cell_size, max_cell_size,
|
|
| 682 |
image_pil_masked = apply_polygon_mask(Image.fromarray(image_np), pts_json)
|
| 683 |
image_np = np.array(image_pil_masked)
|
| 684 |
|
| 685 |
-
# Cap the working image only after cropping
|
| 686 |
# native detail, and a large one is still bounded for segmentation.
|
| 687 |
image_np = safe_resize(image_np)
|
| 688 |
|
|
@@ -690,21 +742,48 @@ def run_segmentation(image, model_choice, min_cell_size, max_cell_size,
|
|
| 690 |
# Must be taken after the crop so it stays index-compatible with the masks.
|
| 691 |
raw_image_np = image_np.copy()
|
| 692 |
|
|
|
|
| 693 |
|
| 694 |
-
|
| 695 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 696 |
|
| 697 |
-
|
| 698 |
-
if
|
| 699 |
-
processed_image_np
|
| 700 |
-
elif len(image_np.shape) == 3 and image_np.shape[2] == 4:
|
| 701 |
-
processed_image_np = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
|
| 702 |
else:
|
| 703 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
|
| 705 |
# Run patch-parallel Cellpose segmentation
|
| 706 |
-
|
| 707 |
-
|
|
|
|
|
|
|
|
|
|
| 708 |
|
| 709 |
# Same single pass feeds the log line and the recommendation below;
|
| 710 |
# this used to be computed from scratch twice.
|
|
@@ -1807,14 +1886,8 @@ def _crop_picker(label):
|
|
| 1807 |
return st
|
| 1808 |
|
| 1809 |
|
| 1810 |
-
def
|
| 1811 |
-
|
| 1812 |
-
"""Segment (and optionally classify) a single image. Returns a dict."""
|
| 1813 |
-
if image is None:
|
| 1814 |
-
return None
|
| 1815 |
-
out = run_segmentation(image, model_name, min_px, 10000,
|
| 1816 |
-
use_stereo, left_pct, top_pct, points,
|
| 1817 |
-
use_min, False)
|
| 1818 |
(cell_count, seg_overlay, info_msg, _vis, packed_masks, packed_img,
|
| 1819 |
confluency, _rec, packed_raw, _base, seg_seconds, _ov) = out
|
| 1820 |
if packed_masks is None:
|
|
@@ -1827,6 +1900,7 @@ def _segment_one(image, points, model_name, use_min, min_px,
|
|
| 1827 |
"alive": None, "dead": None, "viab": None, "viab_overlay": None}
|
| 1828 |
|
| 1829 |
if want_viability:
|
|
|
|
| 1830 |
v_overlay, alive, dead, viab_pct, _vinfo, lm = run_viability(
|
| 1831 |
packed_masks, packed_img)
|
| 1832 |
res.update(alive=alive, dead=dead, viab=viab_pct,
|
|
@@ -1834,6 +1908,75 @@ def _segment_one(image, points, model_name, use_min, min_px,
|
|
| 1834 |
return res
|
| 1835 |
|
| 1836 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1837 |
def _build_correction(res):
|
| 1838 |
"""Per-cell thumbnails for manual live/dead correction.
|
| 1839 |
|
|
@@ -2021,20 +2164,43 @@ with gr.Blocks(title="CellposeCellCounter", theme=gr.themes.Soft()) as demo:
|
|
| 2021 |
imgs, ptss = [i1, i2, i3, i4], [p1, p2, p3, p4]
|
| 2022 |
t0 = time.time()
|
| 2023 |
rows, results, notes = [], [], []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2024 |
for k, (im, pts) in enumerate(zip(imgs, ptss), start=1):
|
| 2025 |
-
blank = ["Square %d" % k, "—", "—", "—", "—"]
|
| 2026 |
if im is None:
|
| 2027 |
-
rows.append(blank); results.append(None)
|
| 2028 |
continue
|
| 2029 |
if not (pts and len(pts) >= 3):
|
| 2030 |
# Deliberately skipped, not segmented whole-frame: the whole
|
| 2031 |
# frame is a much larger area than one block, so counting it
|
| 2032 |
# would silently corrupt the concentration.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2033 |
rows.append(blank); results.append(None)
|
| 2034 |
-
notes.append(
|
| 2035 |
continue
|
| 2036 |
-
r =
|
| 2037 |
-
if r is None
|
|
|
|
|
|
|
|
|
|
| 2038 |
rows.append(blank); results.append(None)
|
| 2039 |
notes.append("Square %d failed" % k)
|
| 2040 |
continue
|
|
|
|
| 548 |
id_offset += max_id + 1
|
| 549 |
|
| 550 |
# --- Renumber to compact sequential IDs ---
|
| 551 |
+
# Lookup table, not a loop over ids. The old form was
|
| 552 |
+
# for new_id, old_id in enumerate(unique_ids, 1):
|
| 553 |
+
# renumbered[full_mask == old_id] = new_id
|
| 554 |
+
# i.e. one full-array scan PER CELL -- O(cells x pixels). At ~450 cells on a
|
| 555 |
+
# 1024 px mask that is ~950 M element ops (measured 158 ms vs 12 ms here),
|
| 556 |
+
# and it ran inside the @spaces.GPU window, so it burnt ZeroGPU quota doing
|
| 557 |
+
# CPU integer relabelling. Output is bit-identical.
|
| 558 |
unique_ids = np.unique(full_mask)
|
| 559 |
unique_ids = unique_ids[unique_ids > 0]
|
| 560 |
+
if unique_ids.size == 0:
|
| 561 |
+
return np.zeros_like(full_mask)
|
| 562 |
+
lut = np.zeros(int(full_mask.max()) + 1, dtype=full_mask.dtype)
|
| 563 |
+
lut[unique_ids] = np.arange(1, unique_ids.size + 1, dtype=full_mask.dtype)
|
| 564 |
+
renumbered = lut[full_mask]
|
| 565 |
|
| 566 |
return renumbered
|
| 567 |
|
| 568 |
|
| 569 |
def _segment_patch(args):
|
| 570 |
+
"""Worker: run cellpose on a single patch. Called from a thread pool.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
|
| 572 |
+
The model is built ONCE by the caller and handed in. It used to be looked up
|
| 573 |
+
here, which meant an hf_hub_download cache round-trip per patch (nine of them
|
| 574 |
+
on a 1024 px image) plus a check-then-set race on loaded_models between the
|
| 575 |
+
pool threads. Both were pure overhead inside the GPU window.
|
| 576 |
+
"""
|
| 577 |
+
patch_np, row_start, col_start, model = args
|
| 578 |
mask, _, _ = model.eval(patch_np, diameter=None, channels=[0, 0])
|
| 579 |
return mask, row_start, col_start
|
| 580 |
|
| 581 |
|
| 582 |
+
# ---------------------------------------------------------------------------
|
| 583 |
+
# ZeroGPU duration budget
|
| 584 |
+
#
|
| 585 |
+
# Quota is charged on the duration you REQUEST, not the time you actually use,
|
| 586 |
+
# and remaining quota is what sets your place in the queue -- so over-requesting
|
| 587 |
+
# is paid for twice, once in quota and again in waiting. Tune these two numbers
|
| 588 |
+
# from the "GPU x.xx s" figure the app already prints in the processing info box.
|
| 589 |
+
# ---------------------------------------------------------------------------
|
| 590 |
+
GPU_SETUP_S = 20 # model construction + GPU attach: paid ONCE per call
|
| 591 |
+
GPU_PER_IMAGE_S = 18 # marginal cost of each image in the batch
|
| 592 |
+
GPU_MAX_S = 180 # ceiling: ZeroGPU rejects very long requests outright
|
| 593 |
+
|
| 594 |
+
|
| 595 |
+
def _gpu_duration(images, model_filename):
|
| 596 |
+
"""Dynamic duration, so a 1-image call does not reserve a 4-image budget."""
|
| 597 |
+
try:
|
| 598 |
+
n = max(1, len(images))
|
| 599 |
+
except TypeError:
|
| 600 |
+
n = 1
|
| 601 |
+
return int(min(GPU_MAX_S, GPU_SETUP_S + GPU_PER_IMAGE_S * n))
|
| 602 |
+
|
| 603 |
+
|
| 604 |
+
def _get_model(model_filename):
|
| 605 |
+
"""Build (or reuse) the Cellpose model. Called once per GPU acquisition."""
|
| 606 |
+
if model_filename in loaded_models:
|
| 607 |
+
return loaded_models[model_filename]
|
| 608 |
+
model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
|
| 609 |
+
model = models.CellposeModel(gpu=True, pretrained_model=model_path)
|
| 610 |
+
loaded_models[model_filename] = model
|
| 611 |
+
return model
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
def _segment_image_with_model(image_np, model):
|
| 615 |
+
"""Patch, segment and stitch ONE image using an already-built model.
|
| 616 |
+
|
| 617 |
+
Falls back to whole-image segmentation if the image is small enough that
|
| 618 |
+
patching adds overhead without benefit.
|
| 619 |
"""
|
| 620 |
_t_gpu = time.perf_counter()
|
| 621 |
h, w = image_np.shape[:2]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
|
| 623 |
# Small images: no benefit from patching
|
| 624 |
if max(h, w) <= MIN_PATCH_DIM * 2:
|
|
|
|
| 628 |
patches = _split_patches(image_np)
|
| 629 |
n_patches = len(patches)
|
| 630 |
|
| 631 |
+
# The model goes in the args now -- see _segment_patch.
|
| 632 |
+
args_list = [(patch, r, c, model) for patch, r, c in patches]
|
|
|
|
|
|
|
|
|
|
| 633 |
|
| 634 |
+
patch_results = [] # (mask, row_start, col_start) in completion order
|
| 635 |
|
| 636 |
# ThreadPoolExecutor: GPU kernels release the GIL so threads overlap on GPU
|
| 637 |
with ThreadPoolExecutor(max_workers=min(n_patches, 4)) as pool:
|
|
|
|
| 647 |
return full_mask, n_patches, time.perf_counter() - _t_gpu
|
| 648 |
|
| 649 |
|
| 650 |
+
@spaces.GPU(duration=_gpu_duration)
|
| 651 |
+
def run_segmentation_patched_many(images, model_filename):
|
| 652 |
+
"""Segment a LIST of images inside ONE ZeroGPU acquisition.
|
| 653 |
+
|
| 654 |
+
This is the point of the batching change. The four hemocytometer squares
|
| 655 |
+
used to take four separate GPU allocations, which meant four waits in the
|
| 656 |
+
ZeroGPU queue and four constructions of the Cellpose model -- module state
|
| 657 |
+
does not survive from one allocation to the next, so `loaded_models` never
|
| 658 |
+
actually helped across squares.
|
| 659 |
+
|
| 660 |
+
The decorator sits HERE rather than on run_segmentation because ZeroGPU
|
| 661 |
+
quota is charged for the whole time the GPU is attached, and the caller does
|
| 662 |
+
a lot of CPU-only work -- decoding a 12 MP photo, the perspective warp, size
|
| 663 |
+
filtering, overlay rendering. Holding an A10G through all of that burnt
|
| 664 |
+
visitors' daily quota on NumPy.
|
| 665 |
+
|
| 666 |
+
Returns a list of (mask, n_patches, gpu_seconds), one per input image.
|
| 667 |
+
"""
|
| 668 |
+
model = _get_model(model_filename)
|
| 669 |
+
return [_segment_image_with_model(im, model) for im in images]
|
| 670 |
+
|
| 671 |
+
|
| 672 |
+
def run_segmentation_patched(image_np, model_filename):
|
| 673 |
+
"""Single-image wrapper. One image, one GPU acquisition -- unchanged for the
|
| 674 |
+
comprehensive and confluency tabs, which segment one image at a time."""
|
| 675 |
+
return run_segmentation_patched_many([image_np], model_filename)[0]
|
| 676 |
+
|
| 677 |
+
|
| 678 |
def render_segmentation(base_masks, processed_image_np,
|
| 679 |
use_stereology, left_exclusion, top_exclusion):
|
| 680 |
"""
|
|
|
|
| 710 |
return masks, cell_count, confluency, Image.fromarray(overlay), excluded_count
|
| 711 |
|
| 712 |
|
| 713 |
+
def _prep_for_segmentation(image, model_choice, crop_points):
|
| 714 |
+
"""CPU-only preparation: crop / perspective-warp, downscale, RGB-normalise.
|
| 715 |
+
|
| 716 |
+
Split out of run_segmentation so a whole batch of images can be prepared
|
| 717 |
+
BEFORE a single GPU acquisition, instead of one acquisition per image.
|
| 718 |
+
Returns (processed_image_np, raw_image_np, model_filename).
|
| 719 |
+
"""
|
| 720 |
image_np = np.array(image)
|
| 721 |
|
| 722 |
# Crop BEFORE downscaling, so the ROI is sampled from the original pixels
|
| 723 |
+
# rather than upscaled out of a <=MAX_SIDE working copy. The crop points were
|
| 724 |
# clicked on the full-resolution upload, so they are already in this space.
|
| 725 |
+
# (Need >=3 points for a polygon.)
|
| 726 |
if crop_points and len(crop_points) >= 3:
|
| 727 |
import json
|
| 728 |
if len(crop_points) == 4:
|
|
|
|
| 734 |
image_pil_masked = apply_polygon_mask(Image.fromarray(image_np), pts_json)
|
| 735 |
image_np = np.array(image_pil_masked)
|
| 736 |
|
| 737 |
+
# Cap the working image only after cropping -- a small ROI now keeps its
|
| 738 |
# native detail, and a large one is still bounded for segmentation.
|
| 739 |
image_np = safe_resize(image_np)
|
| 740 |
|
|
|
|
| 742 |
# Must be taken after the crop so it stays index-compatible with the masks.
|
| 743 |
raw_image_np = image_np.copy()
|
| 744 |
|
| 745 |
+
model_filename = MODEL_OPTIONS[model_choice]
|
| 746 |
|
| 747 |
+
# Process image format to RGB
|
| 748 |
+
if len(image_np.shape) == 2:
|
| 749 |
+
processed_image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
|
| 750 |
+
elif len(image_np.shape) == 3 and image_np.shape[2] == 4:
|
| 751 |
+
processed_image_np = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
|
| 752 |
+
else:
|
| 753 |
+
processed_image_np = image_np
|
| 754 |
+
|
| 755 |
+
return processed_image_np, raw_image_np, model_filename
|
| 756 |
+
|
| 757 |
+
|
| 758 |
+
def run_segmentation(image, model_choice, min_cell_size, max_cell_size,
|
| 759 |
+
use_stereology, left_exclusion, top_exclusion,
|
| 760 |
+
crop_points=None, use_min_filter=False, use_max_filter=False,
|
| 761 |
+
_prepped=None, _gpu_result=None, _t_start_override=None):
|
| 762 |
+
"""Segment one image and build the full result tuple.
|
| 763 |
+
|
| 764 |
+
_prepped / _gpu_result / _t_start_override are used by the batched path
|
| 765 |
+
(_segment_many): they let the same post-processing tail run against work
|
| 766 |
+
that has ALREADY been prepared and segmented, so the four squares can share
|
| 767 |
+
one GPU acquisition. Left unset, behaviour is exactly as before.
|
| 768 |
+
"""
|
| 769 |
+
_t_start = time.perf_counter() if _t_start_override is None else _t_start_override
|
| 770 |
|
| 771 |
+
try:
|
| 772 |
+
if _prepped is not None:
|
| 773 |
+
processed_image_np, raw_image_np, model_filename = _prepped
|
|
|
|
|
|
|
| 774 |
else:
|
| 775 |
+
# Now inside the try: a warp failure used to propagate out of
|
| 776 |
+
# run_segmentation and take the whole four-square run down with it.
|
| 777 |
+
# It now fails that one square and leaves the others countable.
|
| 778 |
+
processed_image_np, raw_image_np, model_filename = _prep_for_segmentation(
|
| 779 |
+
image, model_choice, crop_points)
|
| 780 |
|
| 781 |
# Run patch-parallel Cellpose segmentation
|
| 782 |
+
if _gpu_result is not None:
|
| 783 |
+
masks_raw, n_patches, gpu_seconds = _gpu_result
|
| 784 |
+
else:
|
| 785 |
+
masks_raw, n_patches, gpu_seconds = run_segmentation_patched(
|
| 786 |
+
processed_image_np, model_filename)
|
| 787 |
|
| 788 |
# Same single pass feeds the log line and the recommendation below;
|
| 789 |
# this used to be computed from scratch twice.
|
|
|
|
| 1886 |
return st
|
| 1887 |
|
| 1888 |
|
| 1889 |
+
def _result_from_out(out, want_viability):
|
| 1890 |
+
"""Turn the run_segmentation tuple into the dict the tabs consume."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1891 |
(cell_count, seg_overlay, info_msg, _vis, packed_masks, packed_img,
|
| 1892 |
confluency, _rec, packed_raw, _base, seg_seconds, _ov) = out
|
| 1893 |
if packed_masks is None:
|
|
|
|
| 1900 |
"alive": None, "dead": None, "viab": None, "viab_overlay": None}
|
| 1901 |
|
| 1902 |
if want_viability:
|
| 1903 |
+
# CPU-only (sklearn), so it stays outside the GPU window on purpose.
|
| 1904 |
v_overlay, alive, dead, viab_pct, _vinfo, lm = run_viability(
|
| 1905 |
packed_masks, packed_img)
|
| 1906 |
res.update(alive=alive, dead=dead, viab=viab_pct,
|
|
|
|
| 1908 |
return res
|
| 1909 |
|
| 1910 |
|
| 1911 |
+
def _segment_one(image, points, model_name, use_min, min_px,
|
| 1912 |
+
use_stereo, left_pct, top_pct, want_viability):
|
| 1913 |
+
"""Segment (and optionally classify) a single image. Returns a dict."""
|
| 1914 |
+
if image is None:
|
| 1915 |
+
return None
|
| 1916 |
+
out = run_segmentation(image, model_name, min_px, 10000,
|
| 1917 |
+
use_stereo, left_pct, top_pct, points,
|
| 1918 |
+
use_min, False)
|
| 1919 |
+
return _result_from_out(out, want_viability)
|
| 1920 |
+
|
| 1921 |
+
|
| 1922 |
+
def _segment_many(items, model_name, use_min, min_px,
|
| 1923 |
+
use_stereo, left_pct, top_pct, want_viability):
|
| 1924 |
+
"""Segment several (image, points) pairs with ONE ZeroGPU acquisition.
|
| 1925 |
+
|
| 1926 |
+
Produces the same per-image dicts as calling _segment_one in a loop, but the
|
| 1927 |
+
squares share a single trip through the ZeroGPU queue and a single Cellpose
|
| 1928 |
+
model construction instead of one each. All the CPU work -- warping,
|
| 1929 |
+
filtering, overlays, viability -- stays outside the GPU window as before.
|
| 1930 |
+
|
| 1931 |
+
Returns a list the same length as `items`; an entry is None if that image
|
| 1932 |
+
was None, or {"error": ...} if that one square failed. One square failing
|
| 1933 |
+
never takes the others down.
|
| 1934 |
+
"""
|
| 1935 |
+
t0 = time.perf_counter()
|
| 1936 |
+
n = len(items)
|
| 1937 |
+
results = [None] * n
|
| 1938 |
+
prepped = [None] * n
|
| 1939 |
+
|
| 1940 |
+
# --- CPU: prepare everything first, no GPU held ---
|
| 1941 |
+
for i, (image, points) in enumerate(items):
|
| 1942 |
+
if image is None:
|
| 1943 |
+
continue
|
| 1944 |
+
try:
|
| 1945 |
+
prepped[i] = _prep_for_segmentation(image, model_name, points)
|
| 1946 |
+
except Exception as e:
|
| 1947 |
+
import traceback; traceback.print_exc()
|
| 1948 |
+
results[i] = {"error": "Could not prepare image: %s" % e}
|
| 1949 |
+
|
| 1950 |
+
live = [i for i in range(n) if prepped[i] is not None]
|
| 1951 |
+
if not live:
|
| 1952 |
+
return results
|
| 1953 |
+
|
| 1954 |
+
# --- GPU: one acquisition for the whole batch ---
|
| 1955 |
+
model_filename = prepped[live[0]][2]
|
| 1956 |
+
try:
|
| 1957 |
+
gpu_out = run_segmentation_patched_many(
|
| 1958 |
+
[prepped[i][0] for i in live], model_filename)
|
| 1959 |
+
except Exception as e:
|
| 1960 |
+
import traceback; traceback.print_exc()
|
| 1961 |
+
for i in live:
|
| 1962 |
+
results[i] = {"error": "Segmentation failed: %s" % e}
|
| 1963 |
+
return results
|
| 1964 |
+
|
| 1965 |
+
# --- CPU: the unchanged post-processing tail, per image ---
|
| 1966 |
+
for k, i in enumerate(live):
|
| 1967 |
+
try:
|
| 1968 |
+
out = run_segmentation(
|
| 1969 |
+
None, model_name, min_px, 10000,
|
| 1970 |
+
use_stereo, left_pct, top_pct, items[i][1], use_min, False,
|
| 1971 |
+
_prepped=prepped[i], _gpu_result=gpu_out[k], _t_start_override=t0)
|
| 1972 |
+
results[i] = _result_from_out(out, want_viability)
|
| 1973 |
+
except Exception as e:
|
| 1974 |
+
import traceback; traceback.print_exc()
|
| 1975 |
+
results[i] = {"error": "Post-processing failed: %s" % e}
|
| 1976 |
+
|
| 1977 |
+
return results
|
| 1978 |
+
|
| 1979 |
+
|
| 1980 |
def _build_correction(res):
|
| 1981 |
"""Per-cell thumbnails for manual live/dead correction.
|
| 1982 |
|
|
|
|
| 2164 |
imgs, ptss = [i1, i2, i3, i4], [p1, p2, p3, p4]
|
| 2165 |
t0 = time.time()
|
| 2166 |
rows, results, notes = [], [], []
|
| 2167 |
+
|
| 2168 |
+
# Work out which squares are eligible BEFORE touching the GPU, then
|
| 2169 |
+
# segment all of them in ONE acquisition. Previously this loop
|
| 2170 |
+
# called _segment_one per square, so a four-square run queued for a
|
| 2171 |
+
# GPU four separate times and rebuilt the Cellpose model each time.
|
| 2172 |
+
skipped, eligible = {}, []
|
| 2173 |
for k, (im, pts) in enumerate(zip(imgs, ptss), start=1):
|
|
|
|
| 2174 |
if im is None:
|
|
|
|
| 2175 |
continue
|
| 2176 |
if not (pts and len(pts) >= 3):
|
| 2177 |
# Deliberately skipped, not segmented whole-frame: the whole
|
| 2178 |
# frame is a much larger area than one block, so counting it
|
| 2179 |
# would silently corrupt the concentration.
|
| 2180 |
+
skipped[k - 1] = "Square %d: no block tapped, skipped" % k
|
| 2181 |
+
continue
|
| 2182 |
+
eligible.append(k - 1)
|
| 2183 |
+
|
| 2184 |
+
seg = [None, None, None, None]
|
| 2185 |
+
if eligible:
|
| 2186 |
+
batch = _segment_many([(imgs[i], ptss[i]) for i in eligible],
|
| 2187 |
+
HEMO_MODEL, True, 0, True, 1, 1, True)
|
| 2188 |
+
for slot, r in zip(eligible, batch):
|
| 2189 |
+
seg[slot] = r
|
| 2190 |
+
|
| 2191 |
+
# Rebuild rows in square order so the table and the notes read 1..4.
|
| 2192 |
+
for k in range(1, 5):
|
| 2193 |
+
slot = k - 1
|
| 2194 |
+
blank = ["Square %d" % k, "\u2014", "\u2014", "\u2014", "\u2014"]
|
| 2195 |
+
if slot in skipped:
|
| 2196 |
rows.append(blank); results.append(None)
|
| 2197 |
+
notes.append(skipped[slot])
|
| 2198 |
continue
|
| 2199 |
+
r = seg[slot]
|
| 2200 |
+
if r is None:
|
| 2201 |
+
rows.append(blank); results.append(None)
|
| 2202 |
+
continue
|
| 2203 |
+
if "error" in r:
|
| 2204 |
rows.append(blank); results.append(None)
|
| 2205 |
notes.append("Square %d failed" % k)
|
| 2206 |
continue
|