LiangLabUMB commited on
Commit
fe19ee5
·
verified ·
1 Parent(s): 8165065

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -730
app.py DELETED
@@ -1,730 +0,0 @@
1
- import gradio as gr
2
- import spaces
3
- from cellpose import models
4
- import numpy as np
5
- import cv2
6
- import matplotlib.pyplot as plt
7
- import tempfile
8
- from PIL import Image, ImageDraw
9
- import io
10
- from huggingface_hub import hf_hub_download
11
- import base64
12
-
13
- HF_REPO_ID = "myang4218/cellposemodel"
14
- MODEL_OPTIONS = {
15
- "Hemocytometer Model": "hemocytometermodel.npy",
16
- "General Model": "generalmodel.npy"
17
- }
18
-
19
- loaded_models = {}
20
-
21
- # ---- mobile-safe size limits (aggressive for Safari) ----
22
- MAX_SIDE = 1024
23
- MAX_PIXELS = 1024 * 1024
24
-
25
-
26
- def safe_resize(image_np):
27
- """
28
- Downscale image to fit within MAX_SIDE and MAX_PIXELS while
29
- preserving aspect ratio. Works for RGB / RGBA / grayscale.
30
- """
31
- h, w = image_np.shape[:2]
32
- total = h * w
33
-
34
- if max(h, w) <= MAX_SIDE and total <= MAX_PIXELS:
35
- return image_np
36
-
37
- # compute scale
38
- scale_side = MAX_SIDE / max(h, w)
39
- scale_pixels = (MAX_PIXELS / total) ** 0.5
40
- scale = min(scale_side, scale_pixels)
41
-
42
- new_w = max(1, int(w * scale))
43
- new_h = max(1, int(h * scale))
44
-
45
- return cv2.resize(image_np, (new_w, new_h), interpolation=cv2.INTER_AREA)
46
-
47
-
48
- def draw_exclusion_overlay(image_np, left_width_pct, top_width_pct):
49
-
50
- h, w = image_np.shape[:2]
51
-
52
- # Convert to PIL for drawing
53
- img_pil = Image.fromarray(image_np)
54
- draw = ImageDraw.Draw(img_pil, 'RGBA')
55
-
56
- # Calculate pixel widths from percentages
57
- left_px = int(w * left_width_pct / 100)
58
- top_px = int(h * top_width_pct / 100)
59
-
60
- # Draw overlays for exclusion zones
61
- if left_px > 0:
62
- # Left exclusion zone
63
- draw.rectangle(
64
- [(0, 0), (left_px, h)],
65
- fill=(255, 0, 0, 80) # Semi-transparent red
66
- )
67
- # border line
68
- draw.line([(left_px, 0), (left_px, h)], fill=(255, 0, 0, 255), width=3)
69
-
70
- if top_px > 0:
71
- # Top exclusion zone
72
- draw.rectangle(
73
- [(0, 0), (w, top_px)],
74
- fill=(255, 0, 0, 80) # Semi-transparent red
75
- )
76
- # border line
77
- draw.line([(0, top_px), (w, top_px)], fill=(255, 0, 0, 255), width=3)
78
-
79
- return np.array(img_pil)
80
-
81
-
82
- def apply_stereological_exclusion(masks, left_width_pct, top_width_pct):
83
- h, w = masks.shape
84
-
85
- # Calculate pixel widths from percentages
86
- left_px = int(w * left_width_pct / 100)
87
- top_px = int(h * top_width_pct / 100)
88
-
89
- filtered_masks = masks.copy()
90
- cell_ids = np.unique(masks)
91
- cell_ids = cell_ids[cell_ids > 0]
92
-
93
- excluded_cells = []
94
- included_cells = []
95
-
96
- for cell_id in cell_ids:
97
- cell_mask = (masks == cell_id)
98
-
99
- # Get cell boundary coordinates
100
- rows, cols = np.where(cell_mask)
101
-
102
- # Check if cell touches left exclusion zone
103
- touches_left = np.any(cols < left_px) if left_px > 0 else False
104
-
105
- # Check if cell touches top exclusion zone
106
- touches_top = np.any(rows < top_px) if top_px > 0 else False
107
-
108
- # Exclude if touching left or top
109
- if touches_left or touches_top:
110
- filtered_masks[cell_mask] = 0
111
- excluded_cells.append(cell_id)
112
- else:
113
- included_cells.append(cell_id)
114
-
115
- # Renumber remaining cells
116
- unique_ids = np.unique(filtered_masks)
117
- unique_ids = unique_ids[unique_ids > 0]
118
-
119
- renumbered_masks = np.zeros_like(filtered_masks)
120
- for new_id, old_id in enumerate(unique_ids, start=1):
121
- renumbered_masks[filtered_masks == old_id] = new_id
122
-
123
- return renumbered_masks, len(excluded_cells), len(included_cells)
124
-
125
-
126
- def classify_cells_by_blueness(image_np, masks, threshold_bias):
127
- """
128
- Classify cells as dead (blue) or alive using an adaptive Otsu threshold
129
- on per-cell blueness scores, with a user bias to fine-tune.
130
-
131
- Args:
132
- image_np: RGB image array
133
- masks: Cellpose segmentation masks
134
- threshold_bias: Slider value -50..+50; shifts Otsu threshold up/down.
135
- Negative = more cells classified dead (looser).
136
- Positive = fewer cells classified dead (stricter).
137
- 0 = pure Otsu (fully automatic).
138
-
139
- Returns:
140
- dead_count, alive_count, colored_overlay, otsu_threshold, final_threshold
141
- """
142
-
143
- if len(image_np.shape) == 2:
144
- image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
145
- elif len(image_np.shape) == 3 and image_np.shape[2] == 4:
146
- image_np = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
147
-
148
- hsv = cv2.cvtColor(image_np, cv2.COLOR_RGB2HSV)
149
-
150
- hue = hsv[:, :, 0].astype(np.float32)
151
- saturation = hsv[:, :, 1].astype(np.float32)
152
-
153
- # Raw blueness: hue proximity to 115° × saturation
154
- hue_distance = np.minimum(np.abs(hue - 115), 180 - np.abs(hue - 115))
155
- hue_score = np.maximum(0, 1 - hue_distance / 65)
156
- blueness = hue_score * (saturation / 255.0)
157
-
158
- # --- Compute per-cell mean blueness scores ---
159
- cell_ids = np.unique(masks)
160
- cell_ids = cell_ids[cell_ids > 0]
161
-
162
- if len(cell_ids) == 0:
163
- blank = image_np.copy()
164
- return 0, 0, blank, 0.0, 0.0
165
-
166
- cell_scores = np.array([np.mean(blueness[masks == cid]) for cid in cell_ids])
167
-
168
- # --- Otsu on the distribution of per-cell scores ---
169
- # cv2.threshold expects uint8; scale 0-1 → 0-255
170
- scores_u8 = (np.clip(cell_scores, 0, 1) * 255).astype(np.uint8)
171
-
172
- if scores_u8.max() == scores_u8.min():
173
- # All cells identical → Otsu is undefined; use midpoint
174
- otsu_threshold = float(scores_u8[0]) / 255.0
175
- else:
176
- # Reshape to a single-column image so cv2.threshold works
177
- thresh_val, _ = cv2.threshold(
178
- scores_u8.reshape(-1, 1), 0, 255,
179
- cv2.THRESH_BINARY + cv2.THRESH_OTSU
180
- )
181
- otsu_threshold = thresh_val / 255.0
182
-
183
- # --- Apply user bias: slider -50..+50 maps to ±0.20 shift ---
184
- bias = (threshold_bias / 50.0) * 0.20
185
- final_threshold = float(np.clip(otsu_threshold + bias, 0.0, 1.0))
186
-
187
- # --- Classify ---
188
- dead_cells = [cid for cid, s in zip(cell_ids, cell_scores) if s > final_threshold]
189
- alive_cells = [cid for cid, s in zip(cell_ids, cell_scores) if s <= final_threshold]
190
-
191
- # --- Colored overlay ---
192
- overlay = image_np.copy().astype(np.float32)
193
- for cid in dead_cells:
194
- overlay[masks == cid] = [255, 0, 0]
195
- for cid in alive_cells:
196
- overlay[masks == cid] = [0, 255, 0]
197
-
198
- alpha = 0.4
199
- final_overlay = (1 - alpha) * image_np.astype(np.float32) + alpha * overlay
200
- final_overlay = np.clip(final_overlay, 0, 255).astype(np.uint8)
201
-
202
- return len(dead_cells), len(alive_cells), final_overlay, otsu_threshold, final_threshold
203
-
204
-
205
- def measure_confluency(masks, image_np):
206
- tot_pixels = image_np.shape[0] * image_np.shape[1]
207
- cell_pixels = np.count_nonzero(masks)
208
- confluency = cell_pixels / tot_pixels * 100
209
- return confluency
210
-
211
- def filter_mask_by_size(masks, minimum_pixels):
212
- filtered_masks = masks.copy()
213
- cell_ids = np.unique(masks)
214
- cell_ids = cell_ids[cell_ids > 0]
215
-
216
- removed_count = 0
217
-
218
- for cell_id in cell_ids:
219
- cell_mask = (masks == cell_id)
220
- cell_pixels = np.count_nonzero(cell_mask)
221
- if cell_pixels < minimum_pixels:
222
- filtered_masks[cell_mask] = 0
223
- removed_count += 1
224
-
225
- unique_ids = np.unique(filtered_masks)
226
- unique_ids = unique_ids[unique_ids > 0]
227
-
228
- renumbered_masks = np.zeros_like(filtered_masks)
229
- for new_id, old_id in enumerate(unique_ids, start=1):
230
- renumbered_masks[filtered_masks == old_id] = new_id
231
-
232
- return renumbered_masks, removed_count
233
-
234
-
235
- def filter_mask_by_maxsize(masks, maximum_pixels):
236
- filtered_masks = masks.copy()
237
- cell_ids = np.unique(masks)
238
- cell_ids = cell_ids[cell_ids > 0]
239
-
240
- removed_count = 0
241
- for cell_id in cell_ids:
242
- cell_mask = (masks == cell_id)
243
- cell_pixels = np.count_nonzero(cell_mask)
244
- if cell_pixels > maximum_pixels:
245
- filtered_masks[cell_mask] = 0
246
- removed_count += 1
247
-
248
- unique_ids = np.unique(filtered_masks)
249
- unique_ids = unique_ids[unique_ids > 0]
250
-
251
- renumbered_masks = np.zeros_like(filtered_masks)
252
- for new_id, old_id in enumerate(unique_ids, start=1):
253
- renumbered_masks[filtered_masks == old_id] = new_id
254
-
255
- return renumbered_masks, removed_count
256
-
257
-
258
- def rec_min_size(masks, q=25):
259
- ids = np.unique(masks)
260
- ids = ids[ids > 0]
261
- if len(ids) == 0:
262
- return 0
263
- sizes = np.array([np.count_nonzero(masks == cid) for cid in ids])
264
- return int(round(np.percentile(sizes, q)))
265
-
266
-
267
- def toggle_stereological_mode(use_stereology):
268
- """Show/hide stereological controls based on checkbox"""
269
- return gr.update(visible=use_stereology)
270
-
271
-
272
- def update_exclusion_preview(image, left_width, top_width):
273
- """Update the preview image with exclusion zone overlay"""
274
- if image is None:
275
- return None
276
-
277
- image_np = np.array(image)
278
- overlay = draw_exclusion_overlay(image_np, left_width, top_width)
279
- return Image.fromarray(overlay)
280
-
281
-
282
- @spaces.GPU
283
- def run_segmentation(image, model_choice, min_cell_size, max_cell_size,
284
- use_stereology, left_exclusion, top_exclusion):
285
- image_np = np.array(image)
286
- image_np = safe_resize(image_np)
287
-
288
- try:
289
- model_filename = MODEL_OPTIONS[model_choice]
290
- model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
291
-
292
- if model_filename in loaded_models:
293
- model = loaded_models[model_filename]
294
- else:
295
- model = models.CellposeModel(gpu=True, pretrained_model=model_path)
296
- loaded_models[model_filename] = model
297
-
298
- # Process image format to RGB
299
- if len(image_np.shape) == 2:
300
- processed_image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
301
- elif len(image_np.shape) == 3 and image_np.shape[2] == 4:
302
- processed_image_np = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
303
- else:
304
- processed_image_np = image_np
305
-
306
- # Run Cellpose segmentation
307
- masks_raw, flows, styles = model.eval(processed_image_np, diameter=None, channels=[0, 0])
308
-
309
- ids = np.unique(masks_raw)
310
- ids = ids[ids > 0]
311
-
312
- sizes = np.array([np.count_nonzero(masks_raw == cid) for cid in ids])
313
-
314
- print("num_cells:", len(ids))
315
- print("mean:", sizes.mean() if len(sizes) > 0 else 0)
316
- print("median:", np.median(sizes) if len(sizes) > 0 else 0)
317
- print("p90:", np.percentile(sizes, 90) if len(sizes) > 0 else 0)
318
- print("max:", sizes.max() if len(sizes) > 0 else 0)
319
-
320
- # Compute recommendation from RAW masks
321
- recommend_min = rec_min_size(masks_raw)
322
-
323
- # If user sets slider to 0, use the recommendation
324
- min_used = recommend_min if (min_cell_size == 0) else int(min_cell_size)
325
-
326
- # Apply filters
327
- masks = masks_raw.copy()
328
- removed_small = 0
329
- removed_large = 0
330
-
331
- if min_used > 0:
332
- masks, removed_small = filter_mask_by_size(masks, min_used)
333
-
334
- if max_cell_size > 0:
335
- masks, removed_large = filter_mask_by_maxsize(masks, int(max_cell_size))
336
-
337
- # Apply stereological exclusion if enabled
338
- excluded_count = 0
339
- if use_stereology:
340
- masks, excluded_count, included_count = apply_stereological_exclusion(
341
- masks, left_exclusion, top_exclusion
342
- )
343
-
344
- filter_msg = ""
345
- if removed_small:
346
- filter_msg += f"Removed {removed_small} small objects (< {min_used} pixels).\n"
347
- if removed_large:
348
- filter_msg += f"Removed {removed_large} large objects (> {int(max_cell_size)} pixels).\n"
349
- if use_stereology and excluded_count > 0:
350
- filter_msg += f"Stereological exclusion: {excluded_count} cells excluded (touching left/top zones).\n"
351
-
352
- cell_count = len(np.unique(masks)) - 1
353
- confluency = measure_confluency(masks, processed_image_np)
354
-
355
- # Create a basic segmentation overlay (without viability)
356
- segmentation_overlay = processed_image_np.copy().astype(np.float32)
357
- if masks.max() > 0:
358
- np.random.seed(42) # For consistent random colors
359
- colors = np.random.randint(0, 255, size=(masks.max() + 1, 3))
360
- colors[0] = [0, 0, 0]
361
- colored_mask = colors[masks]
362
- alpha = 0.4
363
- segmentation_overlay = (1 - alpha) * segmentation_overlay + alpha * colored_mask
364
- segmentation_overlay = np.clip(segmentation_overlay, 0, 255).astype(np.uint8)
365
-
366
- # Add exclusion zone overlay if stereology is enabled
367
- if use_stereology:
368
- segmentation_overlay = draw_exclusion_overlay(segmentation_overlay, left_exclusion, top_exclusion)
369
-
370
- info_msg = ""
371
- if filter_msg:
372
- info_msg += filter_msg
373
- info_msg += f"Segmentation complete! Found {cell_count} cells.\n"
374
- info_msg += f"Confluency: {confluency:.1f}%\n"
375
- if use_stereology:
376
- info_msg += f"Stereological counting enabled (Left: {left_exclusion}%, Top: {top_exclusion}%)\n"
377
- info_msg += "Now adjust the Blue Threshold for viability assessment."
378
-
379
- return (
380
- cell_count,
381
- Image.fromarray(segmentation_overlay),
382
- info_msg,
383
- gr.update(visible=True),
384
- pack_array(masks),
385
- pack_array(processed_image_np),
386
- confluency,
387
- gr.update(value=recommend_min), # update slider display to recommended
388
- )
389
-
390
- except Exception as e:
391
- import traceback
392
- traceback.print_exc()
393
- return (
394
- 0,
395
- None,
396
- f"Error during segmentation: {str(e)}",
397
- gr.update(visible=False),
398
- None,
399
- None,
400
- 0.0,
401
- gr.update(),
402
- )
403
-
404
-
405
- def update_viability_realtime(threshold_bias, stored_masks, stored_image_np):
406
- # avoid unpacking None (e.g. slider moved before segmentation)
407
- if stored_masks is None or stored_image_np is None:
408
- return None, 0, 0, 0.0, "Please run segmentation first."
409
-
410
- stored_masks = unpack_array(stored_masks)
411
- stored_image_np = unpack_array(stored_image_np)
412
-
413
- try:
414
- dead_count, alive_count, viability_overlay_np, otsu_thresh, final_thresh = \
415
- classify_cells_by_blueness(stored_image_np, stored_masks, threshold_bias)
416
-
417
- total_count = alive_count + dead_count
418
- viability_percent = (alive_count / total_count * 100) if total_count > 0 else 0.0
419
- confluency = measure_confluency(stored_masks, stored_image_np)
420
-
421
- overlay_image = Image.fromarray(viability_overlay_np)
422
- info_msg = f"Total cells: {total_count}\nLive (green): {alive_count}\nDead (red): {dead_count}\n"
423
- info_msg += f"Viability: {viability_percent:.1f}%\nConfluency: {confluency:.1f}%\n"
424
- info_msg += f"Otsu threshold (auto): {otsu_thresh:.4f}\n"
425
- info_msg += f"Final threshold (with bias): {final_thresh:.4f}\n"
426
- info_msg += f"Sensitivity bias: {threshold_bias:+d}"
427
-
428
- return overlay_image, alive_count, dead_count, viability_percent, info_msg
429
-
430
- except Exception as e:
431
- return None, 0, 0, 0.0, f"Error updating viability: {str(e)}"
432
-
433
-
434
- def pack_array(arr):
435
- pil = Image.fromarray(arr.astype(np.uint8))
436
- buf = io.BytesIO()
437
- pil.save(buf, format="PNG")
438
- return buf.getvalue()
439
-
440
-
441
- def unpack_array(data):
442
- return np.array(Image.open(io.BytesIO(data)))
443
-
444
-
445
- def save_tab_result(cell_count, confluency, viab_percent):
446
- """Package per-tab results into a dict for Tab 5 summary."""
447
- return {
448
- "cell_count": float(cell_count) if cell_count is not None else None,
449
- "confluency": float(confluency) if confluency is not None else None,
450
- "viab_percent": float(viab_percent) if viab_percent is not None else None,
451
- }
452
-
453
-
454
- def compute_summary(r1, r2, r3, r4):
455
- """Average cell count, confluency, and viability across tabs that have data."""
456
- all_results = [r1, r2, r3, r4]
457
- valid = [(i + 1, r) for i, r in enumerate(all_results)
458
- if r is not None and r.get("cell_count") is not None]
459
-
460
- if not valid:
461
- return (
462
- 0.0, 0.0, 0.0,
463
- "No data yet — run segmentation in at least one tab, then click Refresh Summary."
464
- )
465
-
466
- avg_count = sum(r["cell_count"] for _, r in valid) / len(valid)
467
- avg_conf = sum(r["confluency"] for _, r in valid) / len(valid)
468
- avg_viab = sum(r["viab_percent"] for _, r in valid) / len(valid)
469
-
470
- lines = [f"Tab {tab_num}: {r['cell_count']:.0f} cells | "
471
- f"{r['confluency']:.1f}% confluency | "
472
- f"{r['viab_percent']:.1f}% viability"
473
- for tab_num, r in valid]
474
- lines.append(f"\nAverages ({len(valid)} tab{'s' if len(valid) > 1 else ''}):")
475
- lines.append(f" Cell count: {avg_count:.1f}")
476
- lines.append(f" Confluency: {avg_conf:.1f}%")
477
- lines.append(f" Viability: {avg_viab:.1f}%")
478
-
479
- return avg_count, avg_conf, avg_viab, "\n".join(lines)
480
-
481
-
482
- # ---------------------------------------------------------------------------
483
- # Tab builder — creates one full analysis tab and wires all event handlers.
484
- # Returns the per-tab gr.State used by Tab 5 to collect summary data.
485
- # ---------------------------------------------------------------------------
486
- def build_tab(tab_index, masks_state, image_state, result_state):
487
- with gr.Tab(f"Tab {tab_index}"):
488
- gr.Markdown("Run segmentation")
489
-
490
- with gr.Row():
491
- with gr.Column():
492
- img_input = gr.Image(
493
- type="pil",
494
- label="Microscopy image",
495
- image_mode="RGB",
496
- height=512
497
- )
498
-
499
- model_dropdown = gr.Dropdown(
500
- choices=list(MODEL_OPTIONS.keys()),
501
- label="Select Model",
502
- value="Hemocytometer Model"
503
- )
504
-
505
- min_size_slider = gr.Slider(
506
- minimum=0,
507
- maximum=500,
508
- value=0,
509
- step=10,
510
- label="Minimum Cell Size (pixels). Leave at zero for automated recommendation",
511
- )
512
-
513
- max_size_slider = gr.Slider(
514
- minimum=0,
515
- maximum=10000,
516
- value=10000,
517
- step=10,
518
- label="Maximum Cell Size (pixels)",
519
- )
520
-
521
- gr.Markdown("### Stereological Counting")
522
- use_stereo = gr.Checkbox(
523
- label="Enable Stereological Counting",
524
- value=False,
525
- info="Use unbiased stereological rules for cell counting"
526
- )
527
-
528
- with gr.Group(visible=False) as stereo_controls:
529
- gr.Markdown("""
530
- **Stereological Counting Rules:**
531
- - Cells touching LEFT or TOP exclusion zones are EXCLUDED
532
- - Cells touching RIGHT or BOTTOM edges are INCLUDED
533
- - This provides unbiased counting for quantification
534
- """)
535
-
536
- excl_preview = gr.Image(
537
- type="pil",
538
- label="Exclusion Zone Preview (Red = Excluded)",
539
- height=500
540
- )
541
-
542
- left_excl = gr.Slider(
543
- minimum=0,
544
- maximum=50,
545
- value=10,
546
- step=1,
547
- label="Left Exclusion Width (%)",
548
- info="Width of left exclusion zone"
549
- )
550
-
551
- top_excl = gr.Slider(
552
- minimum=0,
553
- maximum=50,
554
- value=10,
555
- step=1,
556
- label="Top Exclusion Width (%)",
557
- info="Width of top exclusion zone"
558
- )
559
-
560
- segment_btn = gr.Button("🔬 Run Segmentation", variant="primary", size="lg")
561
-
562
- with gr.Column():
563
- cell_count_out = gr.Number(label="Total Cells Detected", precision=0)
564
- confluency_out = gr.Number(label="Confluency (%)", precision=1)
565
- overlay_out = gr.Image(type="pil", label="Segmentation Result")
566
- info_out = gr.Textbox(label="Processing Info", lines=4)
567
-
568
- with gr.Group(visible=False) as viability_section:
569
- gr.Markdown("### Viability Assessment (Trypan Blue)")
570
- gr.Markdown(
571
- "The threshold is set **automatically per image** using Otsu's method. "
572
- "Use the sensitivity slider to fine-tune if needed."
573
- )
574
-
575
- with gr.Row():
576
- with gr.Column():
577
- sensitivity = gr.Slider(
578
- minimum=-50,
579
- maximum=50,
580
- value=0,
581
- step=1,
582
- label="Sensitivity",
583
- info="← more dead cells · auto (0) · fewer dead cells →"
584
- )
585
- with gr.Column():
586
- live_count_out = gr.Number(label="Live Cells (Green)", precision=0)
587
- dead_count_out = gr.Number(label="Dead Cells (Red)", precision=0)
588
-
589
- viab_overlay = gr.Image(type="pil", label="Viability Assessment (Green=Live, Red=Dead)")
590
- viab_percent_out = gr.Number(label="Viability (%)", precision=1)
591
- viab_info = gr.Textbox(label="Analysis Results", lines=6)
592
-
593
- # ---- Event handlers ------------------------------------------------
594
-
595
- use_stereo.change(
596
- fn=toggle_stereological_mode,
597
- inputs=[use_stereo],
598
- outputs=[stereo_controls]
599
- )
600
-
601
- img_input.change(
602
- fn=update_exclusion_preview,
603
- inputs=[img_input, left_excl, top_excl],
604
- outputs=[excl_preview]
605
- )
606
- left_excl.change(
607
- fn=update_exclusion_preview,
608
- inputs=[img_input, left_excl, top_excl],
609
- outputs=[excl_preview]
610
- )
611
- top_excl.change(
612
- fn=update_exclusion_preview,
613
- inputs=[img_input, left_excl, top_excl],
614
- outputs=[excl_preview]
615
- )
616
-
617
- segment_btn.click(
618
- fn=run_segmentation,
619
- inputs=[img_input, model_dropdown, min_size_slider, max_size_slider,
620
- use_stereo, left_excl, top_excl],
621
- outputs=[cell_count_out, overlay_out, info_out, viability_section,
622
- masks_state, image_state, confluency_out, min_size_slider]
623
- ).then(
624
- fn=update_viability_realtime,
625
- inputs=[sensitivity, masks_state, image_state],
626
- outputs=[viab_overlay, live_count_out, dead_count_out, viab_percent_out, viab_info]
627
- ).then(
628
- fn=save_tab_result,
629
- inputs=[cell_count_out, confluency_out, viab_percent_out],
630
- outputs=[result_state]
631
- )
632
-
633
- sensitivity.change(
634
- fn=update_viability_realtime,
635
- inputs=[sensitivity, masks_state, image_state],
636
- outputs=[viab_overlay, live_count_out, dead_count_out, viab_percent_out, viab_info]
637
- ).then(
638
- fn=save_tab_result,
639
- inputs=[cell_count_out, confluency_out, viab_percent_out],
640
- outputs=[result_state]
641
- )
642
-
643
-
644
- # ---------------------------------------------------------------------------
645
- # Gradio interface
646
- # ---------------------------------------------------------------------------
647
- with gr.Blocks(
648
- title="CellposeCellCounter",
649
- theme=gr.themes.Soft(),
650
- ) as demo:
651
- gr.Markdown("# CellposeCellCounter")
652
- 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.")
653
-
654
- # Shared mask/image state (one pair per tab so tabs don't clobber each other)
655
- masks_states = [gr.State(value=None) for _ in range(4)]
656
- image_states = [gr.State(value=None) for _ in range(4)]
657
- result_states = [gr.State(value=None) for _ in range(4)]
658
-
659
- # Build Tabs 1–4 with a loop
660
- for i in range(4):
661
- build_tab(i + 1, masks_states[i], image_states[i], result_states[i])
662
-
663
- # -------------------------------------------------------------------------
664
- # Tab 5 — Summary
665
- # -------------------------------------------------------------------------
666
- with gr.Tab("Tab 5 — Summary"):
667
- gr.Markdown("## Average Results Across All Tabs")
668
- gr.Markdown(
669
- "Run segmentation (and optionally adjust the blue threshold) in one or more tabs, "
670
- "then click **Refresh Summary** to see the averages."
671
- )
672
-
673
- refresh_btn = gr.Button("🔄 Refresh Summary", variant="primary", size="lg")
674
-
675
- with gr.Row():
676
- avg_count_out = gr.Number(label="Avg Cell Count", precision=1)
677
- avg_conf_out = gr.Number(label="Avg Confluency (%)", precision=1)
678
- avg_viab_out = gr.Number(label="Avg Viability (%)", precision=1)
679
-
680
- summary_box = gr.Textbox(label="Per-Tab Breakdown", lines=10)
681
-
682
- refresh_btn.click(
683
- fn=compute_summary,
684
- inputs=result_states, # list of 4 gr.State components
685
- outputs=[avg_count_out, avg_conf_out, avg_viab_out, summary_box]
686
- )
687
-
688
- # Instructions
689
- with gr.Accordion("Instructions", open=False):
690
- gr.Markdown("""
691
- ### How to use:
692
-
693
- 1. **Upload and Segment**:
694
- - Upload your microscopy image.
695
- - Select a Cellpose model (e.g., "Hemocytometer Model" for suspension culture).
696
- - **(Optional)** Enable Stereological Counting for unbiased quantification.
697
- - Click "Run Segmentation".
698
-
699
- 2. **Stereological Counting** (Optional):
700
- - Check "Enable Stereological Counting" to use unbiased counting rules.
701
- - Adjust the Left and Top exclusion zone widths using the sliders.
702
- - Preview shows excluded areas in red.
703
- - **Counting Rules**:
704
- - Cells touching LEFT or TOP exclusion zones are EXCLUDED
705
- - Cells touching RIGHT or BOTTOM edges are INCLUDED
706
- - This ensures unbiased, systematic counting
707
-
708
- 3. **Analysis Results**:
709
- - **Cell Count**: Total number of detected cells (after exclusions if using stereology)
710
- - **Confluency**: Percentage of image area covered by cells
711
-
712
- 4. **Real-time Viability Assessment (Trypan Blue)**:
713
- - After segmentation, the viability section will become visible.
714
- - Adjust the **"Blue Threshold (%)"** slider in real-time.
715
- - **Lower values (10-20%)** are more sensitive.
716
- - **Higher values (30-50%)** are more selective.
717
- - Green cells = Live, Red cells = Dead.
718
-
719
- 5. **Summary (Tab 5)**:
720
- - After running segmentation in one or more tabs, switch to **Tab 5**.
721
- - Click **Refresh Summary** to compute averages across all completed tabs.
722
- - Displays average cell count, confluency, and viability, plus a per-tab breakdown.
723
-
724
- 6. **Interpreting Results**:
725
- - The app displays total, live, and dead cell counts, viability percentage, and confluency.
726
- - If stereological counting is enabled, excluded cells are noted in the processing info.
727
- """)
728
-
729
- if __name__ == "__main__":
730
- demo.launch()