LiangLabUMB commited on
Commit
126faab
·
verified ·
1 Parent(s): efef066

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +245 -50
app.py CHANGED
@@ -5,10 +5,10 @@ import numpy as np
5
  import cv2
6
  import matplotlib.pyplot as plt
7
  import tempfile
8
- from PIL import Image
9
  import io
10
  from huggingface_hub import hf_hub_download
11
- import base64
12
 
13
  HF_REPO_ID = "myang4218/cellposemodel"
14
  MODEL_OPTIONS = {
@@ -18,7 +18,7 @@ MODEL_OPTIONS = {
18
 
19
  loaded_models = {}
20
 
21
- # mobile-safe size limits
22
  MAX_SIDE = 1024
23
  MAX_PIXELS = 1024 * 1024
24
 
@@ -31,11 +31,10 @@ def safe_resize(image_np):
31
  h, w = image_np.shape[:2]
32
  total = h * w
33
 
34
-
35
  if max(h, w) <= MAX_SIDE and total <= MAX_PIXELS:
36
  return image_np
37
 
38
-
39
  scale_side = MAX_SIDE / max(h, w)
40
  scale_pixels = (MAX_PIXELS / total) ** 0.5
41
  scale = min(scale_side, scale_pixels)
@@ -45,6 +44,85 @@ def safe_resize(image_np):
45
 
46
  return cv2.resize(image_np, (new_w, new_h), interpolation=cv2.INTER_AREA)
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  def classify_cells_by_blueness(image_np, masks, blue_threshold):
49
  """
50
  Classify cells as dead (blue) or alive based on single blueness metric
@@ -57,13 +135,13 @@ def classify_cells_by_blueness(image_np, masks, blue_threshold):
57
  Returns:
58
  dead_count, alive_count, colored_overlay
59
  """
60
- # Ensure image_np is RGB for consistency with HSV conversion
61
  if len(image_np.shape) == 2:
62
  image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
63
  elif len(image_np.shape) == 3 and image_np.shape[2] == 4:
64
  image_np = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
65
 
66
- # Convert RGB to HSV
67
  hsv = cv2.cvtColor(image_np, cv2.COLOR_RGB2HSV)
68
 
69
  # Calculate blueness index for each pixel
@@ -81,22 +159,19 @@ def classify_cells_by_blueness(image_np, masks, blue_threshold):
81
  # Convert threshold from 0-100 to 0-1 scale
82
  threshold = blue_threshold / 100.0
83
 
84
- # Get unique cell IDs (excluding background)
85
  cell_ids = np.unique(masks)
86
- cell_ids = cell_ids[cell_ids > 0] # Remove background (0)
87
 
88
  dead_cells = []
89
  alive_cells = []
90
 
91
  # Classify each cell
92
  for cell_id in cell_ids:
93
- # Get mask for this specific cell
94
  cell_mask = (masks == cell_id)
95
-
96
- # Calculate average blueness for this cell
97
  cell_blueness = np.mean(blueness[cell_mask])
98
 
99
- # Classify based on threshold
100
  if cell_blueness > threshold:
101
  dead_cells.append(cell_id)
102
  else:
@@ -108,11 +183,11 @@ def classify_cells_by_blueness(image_np, masks, blue_threshold):
108
  # Color dead cells red, alive cells green
109
  for cell_id in dead_cells:
110
  cell_mask = (masks == cell_id)
111
- overlay[cell_mask] = [255, 0, 0] # Red for dead
112
 
113
  for cell_id in alive_cells:
114
  cell_mask = (masks == cell_id)
115
- overlay[cell_mask] = [0, 255, 0] # Green for alive
116
 
117
  # Blend with original image
118
  alpha = 0.4
@@ -123,7 +198,6 @@ def classify_cells_by_blueness(image_np, masks, blue_threshold):
123
 
124
 
125
  def measure_confluency(masks, image_np):
126
- """Calculate the percentage of image area covered by cells"""
127
  tot_pixels = image_np.shape[0] * image_np.shape[1]
128
  cell_pixels = np.count_nonzero(masks)
129
  confluency = cell_pixels / tot_pixels * 100
@@ -132,7 +206,7 @@ def measure_confluency(masks, image_np):
132
  def filter_mask_by_size(masks, minimum_pixels):
133
  filtered_masks = masks.copy()
134
  cell_ids = np.unique(masks)
135
- cell_ids = cell_ids[cell_ids > 0] #subtract background
136
 
137
  removed_count = 0
138
 
@@ -185,9 +259,24 @@ def rec_min_size(masks, q=25):
185
  return int(round(np.percentile(sizes, q)))
186
 
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  @spaces.GPU
190
- def run_segmentation(image, model_choice, min_cell_size, max_cell_size):
 
191
  image_np = np.array(image)
192
  image_np = safe_resize(image_np)
193
 
@@ -201,9 +290,6 @@ def run_segmentation(image, model_choice, min_cell_size, max_cell_size):
201
  model = models.CellposeModel(gpu=True, pretrained_model=model_path)
202
  loaded_models[model_filename] = model
203
 
204
-
205
-
206
-
207
  # Process image format to RGB
208
  if len(image_np.shape) == 2:
209
  processed_image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
@@ -220,11 +306,15 @@ def run_segmentation(image, model_choice, min_cell_size, max_cell_size):
220
 
221
  sizes = np.array([np.count_nonzero(masks_raw == cid) for cid in ids])
222
 
 
 
 
 
 
223
 
224
  # Compute recommendation from RAW masks
225
  recommend_min = rec_min_size(masks_raw)
226
 
227
-
228
  # If user sets slider to 0, use the recommendation
229
  min_used = recommend_min if (min_cell_size == 0) else int(min_cell_size)
230
 
@@ -239,12 +329,20 @@ def run_segmentation(image, model_choice, min_cell_size, max_cell_size):
239
  if max_cell_size > 0:
240
  masks, removed_large = filter_mask_by_maxsize(masks, int(max_cell_size))
241
 
 
 
 
 
 
 
242
 
243
  filter_msg = ""
244
  if removed_small:
245
  filter_msg += f"Removed {removed_small} small objects (< {min_used} pixels).\n"
246
  if removed_large:
247
  filter_msg += f"Removed {removed_large} large objects (> {int(max_cell_size)} pixels).\n"
 
 
248
 
249
  cell_count = len(np.unique(masks)) - 1
250
  confluency = measure_confluency(masks, processed_image_np)
@@ -252,19 +350,25 @@ def run_segmentation(image, model_choice, min_cell_size, max_cell_size):
252
  # Create a basic segmentation overlay (without viability)
253
  segmentation_overlay = processed_image_np.copy().astype(np.float32)
254
  if masks.max() > 0:
255
- np.random.seed(42)
256
  colors = np.random.randint(0, 255, size=(masks.max() + 1, 3))
257
  colors[0] = [0, 0, 0]
258
  colored_mask = colors[masks]
259
  alpha = 0.4
260
  segmentation_overlay = (1 - alpha) * segmentation_overlay + alpha * colored_mask
261
  segmentation_overlay = np.clip(segmentation_overlay, 0, 255).astype(np.uint8)
 
 
 
 
262
 
263
  info_msg = ""
264
  if filter_msg:
265
  info_msg += filter_msg
266
  info_msg += f"Segmentation complete! Found {cell_count} cells.\n"
267
  info_msg += f"Confluency: {confluency:.1f}%\n"
 
 
268
  info_msg += "Now adjust the Blue Threshold for viability assessment."
269
 
270
  return (
@@ -279,6 +383,8 @@ def run_segmentation(image, model_choice, min_cell_size, max_cell_size):
279
  )
280
 
281
  except Exception as e:
 
 
282
  return (
283
  0,
284
  None,
@@ -291,9 +397,6 @@ def run_segmentation(image, model_choice, min_cell_size, max_cell_size):
291
  )
292
 
293
 
294
-
295
-
296
-
297
  def update_viability_realtime(blue_threshold, stored_masks, stored_image_np):
298
  # avoid unpacking None (e.g. slider moved before segmentation)
299
  if stored_masks is None or stored_image_np is None:
@@ -321,7 +424,6 @@ def update_viability_realtime(blue_threshold, stored_masks, stored_image_np):
321
  return None, 0, 0, 0.0, f"Error updating viability: {str(e)}"
322
 
323
 
324
-
325
  def pack_array(arr):
326
  pil = Image.fromarray(arr.astype(np.uint8))
327
  buf = io.BytesIO()
@@ -350,7 +452,7 @@ with gr.Blocks(
350
 
351
  with gr.Row():
352
  with gr.Column():
353
- img_input=gr.Image(
354
  type="pil",
355
  label="Microscopy image",
356
  image_mode="RGB",
@@ -362,22 +464,63 @@ with gr.Blocks(
362
  label="Select Model",
363
  value="Hemocytometer Model"
364
  )
 
365
  min_size_slider1 = gr.Slider(
366
  minimum=0,
367
  maximum=500,
368
  value=0,
369
  step=10,
370
  label="Minimum Cell Size (pixels). Leave at zero for automated recommendation",
371
-
372
  )
 
373
  max_size_slider1 = gr.Slider(
374
  minimum=0,
375
- maximum=10000,
376
- value=10000,
377
  step=10,
378
  label="Maximum Cell Size (pixels)",
379
  )
380
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
 
382
  segment_btn1 = gr.Button("🔬 Run Segmentation", variant="primary", size="lg")
383
 
@@ -412,13 +555,58 @@ with gr.Blocks(
412
  viability_info1 = gr.Textbox(label="Analysis Results", lines=5)
413
 
414
  # Event handlers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  segment_btn1.click(
416
  fn=run_segmentation,
417
- inputs=[img_input, model_dropdown1, min_size_slider1, max_size_slider1],
418
- outputs=[cell_count_output1, overlay_output1, info_output1, viability_section1, masks_state, image_state, confluency_output1, min_size_slider1]
419
- ).then(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  fn=update_viability_realtime,
421
- inputs=[blue_threshold1, masks_state, image_state], # Pass stored state as inputs
422
  outputs=[viability_overlay1, live_count_output1, dead_count_output1, viability_percent_output1, viability_info1]
423
  )
424
 
@@ -429,8 +617,6 @@ with gr.Blocks(
429
  outputs=[viability_overlay1, live_count_output1, dead_count_output1, viability_percent_output1, viability_info1]
430
  )
431
 
432
-
433
-
434
  # Instructions
435
  with gr.Accordion("Instructions", open=False):
436
  gr.Markdown("""
@@ -439,24 +625,33 @@ with gr.Blocks(
439
  1. **Upload and Segment**:
440
  - Upload your microscopy image.
441
  - Select a Cellpose model (e.g., "Hemocytometer Model" for suspension culture).
 
442
  - Click "Run Segmentation".
443
 
444
- 2. **Analysis Results**:
445
- - **Cell Count**: Total number of detected cells
446
- - **Confluency**: Percentage of image area covered by cells (useful for assessing cell density). Note that cell confluency is calculated per the entire area of the image input.
 
 
 
 
 
 
 
 
 
447
 
448
- 3. **Real-time Viability Assessment (Trypan Blue)**:
449
  - After segmentation, the viability section will become visible.
450
- - This tool is specifically designed for **Trypan Blue stained images**, where dead cells appear blue.
451
- - Adjust the **"Blue Threshold (%)"** slider in real-time. As you change it, the green (live) and red (dead) classification on the overlay will update.
452
- - **Lower values (e.g., 10-20%)** are more sensitive and will classify more cells as blue/dead.
453
- - **Higher values (e.g., 30-50%)** are more selective and will only classify strongly blue cells as dead.
454
  - Green cells = Live, Red cells = Dead.
455
 
456
- 4. **Interpreting Results**:
457
- - The app calculates and displays the total, live, and dead cell counts, along with the viability percentage and confluency.
458
- - **Confluency** helps assess how densely packed your cells are, which is important for cell culture monitoring.
459
  """)
460
 
461
  if __name__ == "__main__":
462
- demo.launch()
 
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 = {
 
18
 
19
  loaded_models = {}
20
 
21
+ # ---- mobile-safe size limits (aggressive for Safari) ----
22
  MAX_SIDE = 1024
23
  MAX_PIXELS = 1024 * 1024
24
 
 
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)
 
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, blue_threshold):
127
  """
128
  Classify cells as dead (blue) or alive based on single blueness metric
 
135
  Returns:
136
  dead_count, alive_count, colored_overlay
137
  """
138
+
139
  if len(image_np.shape) == 2:
140
  image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
141
  elif len(image_np.shape) == 3 and image_np.shape[2] == 4:
142
  image_np = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
143
 
144
+
145
  hsv = cv2.cvtColor(image_np, cv2.COLOR_RGB2HSV)
146
 
147
  # Calculate blueness index for each pixel
 
159
  # Convert threshold from 0-100 to 0-1 scale
160
  threshold = blue_threshold / 100.0
161
 
162
+ # Get unique cell IDs
163
  cell_ids = np.unique(masks)
164
+ cell_ids = cell_ids[cell_ids > 0]
165
 
166
  dead_cells = []
167
  alive_cells = []
168
 
169
  # Classify each cell
170
  for cell_id in cell_ids:
171
+
172
  cell_mask = (masks == cell_id)
 
 
173
  cell_blueness = np.mean(blueness[cell_mask])
174
 
 
175
  if cell_blueness > threshold:
176
  dead_cells.append(cell_id)
177
  else:
 
183
  # Color dead cells red, alive cells green
184
  for cell_id in dead_cells:
185
  cell_mask = (masks == cell_id)
186
+ overlay[cell_mask] = [255, 0, 0]
187
 
188
  for cell_id in alive_cells:
189
  cell_mask = (masks == cell_id)
190
+ overlay[cell_mask] = [0, 255, 0]
191
 
192
  # Blend with original image
193
  alpha = 0.4
 
198
 
199
 
200
  def measure_confluency(masks, image_np):
 
201
  tot_pixels = image_np.shape[0] * image_np.shape[1]
202
  cell_pixels = np.count_nonzero(masks)
203
  confluency = cell_pixels / tot_pixels * 100
 
206
  def filter_mask_by_size(masks, minimum_pixels):
207
  filtered_masks = masks.copy()
208
  cell_ids = np.unique(masks)
209
+ cell_ids = cell_ids[cell_ids > 0]
210
 
211
  removed_count = 0
212
 
 
259
  return int(round(np.percentile(sizes, q)))
260
 
261
 
262
+ def toggle_stereological_mode(use_stereology):
263
+ """Show/hide stereological controls based on checkbox"""
264
+ return gr.update(visible=use_stereology)
265
+
266
+
267
+ def update_exclusion_preview(image, left_width, top_width):
268
+ """Update the preview image with exclusion zone overlay"""
269
+ if image is None:
270
+ return None
271
+
272
+ image_np = np.array(image)
273
+ overlay = draw_exclusion_overlay(image_np, left_width, top_width)
274
+ return Image.fromarray(overlay)
275
+
276
 
277
  @spaces.GPU
278
+ def run_segmentation(image, model_choice, min_cell_size, max_cell_size,
279
+ use_stereology, left_exclusion, top_exclusion):
280
  image_np = np.array(image)
281
  image_np = safe_resize(image_np)
282
 
 
290
  model = models.CellposeModel(gpu=True, pretrained_model=model_path)
291
  loaded_models[model_filename] = model
292
 
 
 
 
293
  # Process image format to RGB
294
  if len(image_np.shape) == 2:
295
  processed_image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
 
306
 
307
  sizes = np.array([np.count_nonzero(masks_raw == cid) for cid in ids])
308
 
309
+ print("num_cells:", len(ids))
310
+ print("mean:", sizes.mean() if len(sizes) > 0 else 0)
311
+ print("median:", np.median(sizes) if len(sizes) > 0 else 0)
312
+ print("p90:", np.percentile(sizes, 90) if len(sizes) > 0 else 0)
313
+ print("max:", sizes.max() if len(sizes) > 0 else 0)
314
 
315
  # Compute recommendation from RAW masks
316
  recommend_min = rec_min_size(masks_raw)
317
 
 
318
  # If user sets slider to 0, use the recommendation
319
  min_used = recommend_min if (min_cell_size == 0) else int(min_cell_size)
320
 
 
329
  if max_cell_size > 0:
330
  masks, removed_large = filter_mask_by_maxsize(masks, int(max_cell_size))
331
 
332
+ # Apply stereological exclusion if enabled
333
+ excluded_count = 0
334
+ if use_stereology:
335
+ masks, excluded_count, included_count = apply_stereological_exclusion(
336
+ masks, left_exclusion, top_exclusion
337
+ )
338
 
339
  filter_msg = ""
340
  if removed_small:
341
  filter_msg += f"Removed {removed_small} small objects (< {min_used} pixels).\n"
342
  if removed_large:
343
  filter_msg += f"Removed {removed_large} large objects (> {int(max_cell_size)} pixels).\n"
344
+ if use_stereology and excluded_count > 0:
345
+ filter_msg += f"Stereological exclusion: {excluded_count} cells excluded (touching left/top zones).\n"
346
 
347
  cell_count = len(np.unique(masks)) - 1
348
  confluency = measure_confluency(masks, processed_image_np)
 
350
  # Create a basic segmentation overlay (without viability)
351
  segmentation_overlay = processed_image_np.copy().astype(np.float32)
352
  if masks.max() > 0:
353
+ np.random.seed(42) # For consistent random colors
354
  colors = np.random.randint(0, 255, size=(masks.max() + 1, 3))
355
  colors[0] = [0, 0, 0]
356
  colored_mask = colors[masks]
357
  alpha = 0.4
358
  segmentation_overlay = (1 - alpha) * segmentation_overlay + alpha * colored_mask
359
  segmentation_overlay = np.clip(segmentation_overlay, 0, 255).astype(np.uint8)
360
+
361
+ # Add exclusion zone overlay if stereology is enabled
362
+ if use_stereology:
363
+ segmentation_overlay = draw_exclusion_overlay(segmentation_overlay, left_exclusion, top_exclusion)
364
 
365
  info_msg = ""
366
  if filter_msg:
367
  info_msg += filter_msg
368
  info_msg += f"Segmentation complete! Found {cell_count} cells.\n"
369
  info_msg += f"Confluency: {confluency:.1f}%\n"
370
+ if use_stereology:
371
+ info_msg += f"Stereological counting enabled (Left: {left_exclusion}%, Top: {top_exclusion}%)\n"
372
  info_msg += "Now adjust the Blue Threshold for viability assessment."
373
 
374
  return (
 
383
  )
384
 
385
  except Exception as e:
386
+ import traceback
387
+ traceback.print_exc()
388
  return (
389
  0,
390
  None,
 
397
  )
398
 
399
 
 
 
 
400
  def update_viability_realtime(blue_threshold, stored_masks, stored_image_np):
401
  # avoid unpacking None (e.g. slider moved before segmentation)
402
  if stored_masks is None or stored_image_np is None:
 
424
  return None, 0, 0, 0.0, f"Error updating viability: {str(e)}"
425
 
426
 
 
427
  def pack_array(arr):
428
  pil = Image.fromarray(arr.astype(np.uint8))
429
  buf = io.BytesIO()
 
452
 
453
  with gr.Row():
454
  with gr.Column():
455
+ img_input = gr.Image(
456
  type="pil",
457
  label="Microscopy image",
458
  image_mode="RGB",
 
464
  label="Select Model",
465
  value="Hemocytometer Model"
466
  )
467
+
468
  min_size_slider1 = gr.Slider(
469
  minimum=0,
470
  maximum=500,
471
  value=0,
472
  step=10,
473
  label="Minimum Cell Size (pixels). Leave at zero for automated recommendation",
 
474
  )
475
+
476
  max_size_slider1 = gr.Slider(
477
  minimum=0,
478
+ maximum=500,
479
+ value=500,
480
  step=10,
481
  label="Maximum Cell Size (pixels)",
482
  )
483
+
484
+ # Stereological counting option
485
+ gr.Markdown("### Stereological Counting")
486
+ use_stereology_checkbox = gr.Checkbox(
487
+ label="Enable Stereological Counting",
488
+ value=False,
489
+ info="Use unbiased stereological rules for cell counting"
490
+ )
491
+
492
+ # Stereological controls (initially hidden)
493
+ with gr.Group(visible=False) as stereology_controls:
494
+ gr.Markdown("""
495
+ **Stereological Counting Rules:**
496
+ - Cells touching LEFT or TOP exclusion zones are EXCLUDED
497
+ - Cells touching RIGHT or BOTTOM edges are INCLUDED
498
+ - This provides unbiased counting for quantification
499
+ """)
500
+
501
+ exclusion_preview = gr.Image(
502
+ type="pil",
503
+ label="Exclusion Zone Preview (Red = Excluded)",
504
+ height=300
505
+ )
506
+
507
+ left_exclusion_slider = gr.Slider(
508
+ minimum=0,
509
+ maximum=50,
510
+ value=10,
511
+ step=1,
512
+ label="Left Exclusion Width (%)",
513
+ info="Width of left exclusion zone"
514
+ )
515
+
516
+ top_exclusion_slider = gr.Slider(
517
+ minimum=0,
518
+ maximum=50,
519
+ value=10,
520
+ step=1,
521
+ label="Top Exclusion Width (%)",
522
+ info="Width of top exclusion zone"
523
+ )
524
 
525
  segment_btn1 = gr.Button("🔬 Run Segmentation", variant="primary", size="lg")
526
 
 
555
  viability_info1 = gr.Textbox(label="Analysis Results", lines=5)
556
 
557
  # Event handlers
558
+
559
+ # Toggle stereological controls visibility
560
+ use_stereology_checkbox.change(
561
+ fn=toggle_stereological_mode,
562
+ inputs=[use_stereology_checkbox],
563
+ outputs=[stereology_controls]
564
+ )
565
+
566
+ # Update exclusion preview when image is uploaded or sliders change
567
+ img_input.change(
568
+ fn=update_exclusion_preview,
569
+ inputs=[img_input, left_exclusion_slider, top_exclusion_slider],
570
+ outputs=[exclusion_preview]
571
+ )
572
+
573
+ left_exclusion_slider.change(
574
+ fn=update_exclusion_preview,
575
+ inputs=[img_input, left_exclusion_slider, top_exclusion_slider],
576
+ outputs=[exclusion_preview]
577
+ )
578
+
579
+ top_exclusion_slider.change(
580
+ fn=update_exclusion_preview,
581
+ inputs=[img_input, left_exclusion_slider, top_exclusion_slider],
582
+ outputs=[exclusion_preview]
583
+ )
584
+
585
+ # Run segmentation
586
  segment_btn1.click(
587
  fn=run_segmentation,
588
+ inputs=[
589
+ img_input,
590
+ model_dropdown1,
591
+ min_size_slider1,
592
+ max_size_slider1,
593
+ use_stereology_checkbox,
594
+ left_exclusion_slider,
595
+ top_exclusion_slider
596
+ ],
597
+ outputs=[
598
+ cell_count_output1,
599
+ overlay_output1,
600
+ info_output1,
601
+ viability_section1,
602
+ masks_state,
603
+ image_state,
604
+ confluency_output1,
605
+ min_size_slider1
606
+ ]
607
+ ).then( # Chain the initial viability assessment after segmentation
608
  fn=update_viability_realtime,
609
+ inputs=[blue_threshold1, masks_state, image_state],
610
  outputs=[viability_overlay1, live_count_output1, dead_count_output1, viability_percent_output1, viability_info1]
611
  )
612
 
 
617
  outputs=[viability_overlay1, live_count_output1, dead_count_output1, viability_percent_output1, viability_info1]
618
  )
619
 
 
 
620
  # Instructions
621
  with gr.Accordion("Instructions", open=False):
622
  gr.Markdown("""
 
625
  1. **Upload and Segment**:
626
  - Upload your microscopy image.
627
  - Select a Cellpose model (e.g., "Hemocytometer Model" for suspension culture).
628
+ - **(Optional)** Enable Stereological Counting for unbiased quantification.
629
  - Click "Run Segmentation".
630
 
631
+ 2. **Stereological Counting** (Optional):
632
+ - Check "Enable Stereological Counting" to use unbiased counting rules.
633
+ - Adjust the Left and Top exclusion zone widths using the sliders.
634
+ - Preview shows excluded areas in red.
635
+ - **Counting Rules**:
636
+ - Cells touching LEFT or TOP exclusion zones are EXCLUDED
637
+ - Cells touching RIGHT or BOTTOM edges are INCLUDED
638
+ - This ensures unbiased, systematic counting
639
+
640
+ 3. **Analysis Results**:
641
+ - **Cell Count**: Total number of detected cells (after exclusions if using stereology)
642
+ - **Confluency**: Percentage of image area covered by cells
643
 
644
+ 4. **Real-time Viability Assessment (Trypan Blue)**:
645
  - After segmentation, the viability section will become visible.
646
+ - Adjust the **"Blue Threshold (%)"** slider in real-time.
647
+ - **Lower values (10-20%)** are more sensitive.
648
+ - **Higher values (30-50%)** are more selective.
 
649
  - Green cells = Live, Red cells = Dead.
650
 
651
+ 5. **Interpreting Results**:
652
+ - The app displays total, live, and dead cell counts, viability percentage, and confluency.
653
+ - If stereological counting is enabled, excluded cells are noted in the processing info.
654
  """)
655
 
656
  if __name__ == "__main__":
657
+ demo.launch()