LiangLabUMB commited on
Commit
e120ea8
·
verified ·
1 Parent(s): 050acf9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +156 -115
app.py CHANGED
@@ -8,8 +8,7 @@ import tempfile
8
  from PIL import Image
9
  import io
10
  from huggingface_hub import hf_hub_download
11
- import base64
12
-
13
 
14
  HF_REPO_ID = "myang4218/cellposemodel"
15
  MODEL_OPTIONS = {
@@ -19,6 +18,34 @@ MODEL_OPTIONS = {
19
 
20
  loaded_models = {}
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  def extract_region_from_editor(editor_data):
23
  """Extract selected region from ImageEditor data (mobile-safe)."""
24
  if editor_data is None:
@@ -79,13 +106,13 @@ def extract_region_from_editor(editor_data):
79
  else:
80
  try:
81
  sel_np = np.array(sel)
82
- except:
83
  sel_np = None
84
 
85
  # If selection exists: compute bounding box
86
  if sel_np is not None:
87
  if sel_np.ndim == 3:
88
- if sel_np.shape[2] == 4:
89
  mask = sel_np[:, :, 3] > 0
90
  else:
91
  mask = np.any(sel_np > 0, axis=2)
@@ -96,7 +123,7 @@ def extract_region_from_editor(editor_data):
96
  if coords[0].size > 0:
97
  y0, y1 = coords[0].min(), coords[0].max()
98
  x0, x1 = coords[1].min(), coords[1].max()
99
- return background_np[y0:y1+1, x0:x1+1], (x0, y0, x1, y1)
100
 
101
  # No selection → return full image
102
  return background_np, None
@@ -108,12 +135,12 @@ def extract_region_from_editor(editor_data):
108
  def classify_cells_by_blueness(image_np, masks, blue_threshold):
109
  """
110
  Classify cells as dead (blue) or alive based on single blueness metric
111
-
112
  Args:
113
  image_np: RGB image array
114
  masks: Cellpose segmentation masks
115
  blue_threshold: Single threshold value (0-100) for blueness detection
116
-
117
  Returns:
118
  dead_count, alive_count, colored_overlay
119
  """
@@ -125,62 +152,63 @@ def classify_cells_by_blueness(image_np, masks, blue_threshold):
125
 
126
  # Convert RGB to HSV
127
  hsv = cv2.cvtColor(image_np, cv2.COLOR_RGB2HSV)
128
-
129
  # Calculate blueness index for each pixel
130
  hue = hsv[:, :, 0].astype(np.float32)
131
  saturation = hsv[:, :, 1].astype(np.float32)
132
-
133
  # Hue score: peaks around 115 (blue in HSV), drops off towards edges
134
  # Handle hue wrap-around for blue detection (100-130 range)
135
  hue_distance = np.minimum(np.abs(hue - 115), 180 - np.abs(hue - 115))
136
  hue_score = np.maximum(0, 1 - hue_distance / 65) # 65 gives good blue range
137
-
138
  # Combine hue proximity with saturation intensity
139
  blueness = hue_score * (saturation / 255.0)
140
-
141
  # Convert threshold from 0-100 to 0-1 scale
142
  threshold = blue_threshold / 100.0
143
-
144
  # Get unique cell IDs (excluding background)
145
  cell_ids = np.unique(masks)
146
  cell_ids = cell_ids[cell_ids > 0] # Remove background (0)
147
-
148
  dead_cells = []
149
  alive_cells = []
150
-
151
  # Classify each cell
152
  for cell_id in cell_ids:
153
  # Get mask for this specific cell
154
  cell_mask = (masks == cell_id)
155
-
156
  # Calculate average blueness for this cell
157
  cell_blueness = np.mean(blueness[cell_mask])
158
-
159
  # Classify based on threshold
160
  if cell_blueness > threshold:
161
  dead_cells.append(cell_id)
162
  else:
163
  alive_cells.append(cell_id)
164
-
165
  # Create colored overlay
166
- overlay = image_np.copy().astype(np.float32) # Ensure float for blending
167
-
168
  # Color dead cells red, alive cells green
169
  for cell_id in dead_cells:
170
  cell_mask = (masks == cell_id)
171
  overlay[cell_mask] = [255, 0, 0] # Red for dead
172
-
173
  for cell_id in alive_cells:
174
  cell_mask = (masks == cell_id)
175
  overlay[cell_mask] = [0, 255, 0] # Green for alive
176
-
177
  # Blend with original image
178
  alpha = 0.4
179
  final_overlay = (1 - alpha) * image_np.astype(np.float32) + alpha * overlay
180
  final_overlay = np.clip(final_overlay, 0, 255).astype(np.uint8)
181
-
182
  return len(dead_cells), len(alive_cells), final_overlay
183
-
 
184
  def measure_confluency(masks, image_np):
185
  """Calculate the percentage of image area covered by cells"""
186
  tot_pixels = image_np.shape[0] * image_np.shape[1]
@@ -198,28 +226,21 @@ def run_segmentation_editor(editor_data, model_choice):
198
  try:
199
  model_filename = MODEL_OPTIONS[model_choice]
200
  model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
201
-
202
  if model_filename in loaded_models:
203
  model = loaded_models[model_filename]
204
  else:
205
  model = models.CellposeModel(gpu=True, pretrained_model=model_path)
206
  loaded_models[model_filename] = model
207
-
208
  region_np, region_coords = extract_region_from_editor(editor_data)
209
-
210
  if region_np is None:
211
  return 0, None, f"No image provided.", gr.update(visible=False), None, None, 0.0
212
-
213
- # Resize large images to prevent crashes
214
- max_size = 256
215
- if region_np.shape[0] > max_size or region_np.shape[1] > max_size:
216
- h, w = region_np.shape[:2]
217
- if h > w:
218
- new_h, new_w = max_size, int(w * max_size / h)
219
- else:
220
- new_h, new_w = int(h * max_size / w), max_size
221
- region_np = cv2.resize(region_np, (new_w, new_h), interpolation=cv2.INTER_AREA)
222
-
223
  # Process image format to RGB
224
  if len(region_np.shape) == 2:
225
  processed_image_np = cv2.cvtColor(region_np, cv2.COLOR_GRAY2RGB)
@@ -227,36 +248,45 @@ def run_segmentation_editor(editor_data, model_choice):
227
  processed_image_np = cv2.cvtColor(region_np, cv2.COLOR_RGBA2RGB)
228
  else:
229
  processed_image_np = region_np
230
-
231
  # Run Cellpose segmentation
232
  masks, flows, styles = model.eval(processed_image_np, diameter=None, channels=[0, 0])
233
-
234
  cell_count = len(np.unique(masks)) - 1
235
  confluency = measure_confluency(masks, processed_image_np)
236
-
237
  # Create a basic segmentation overlay (without viability)
238
  segmentation_overlay = processed_image_np.copy().astype(np.float32)
239
  if masks.max() > 0:
240
- np.random.seed(42) # For consistent random colors
241
  colors = np.random.randint(0, 255, size=(masks.max() + 1, 3))
242
- colors[0] = [0, 0, 0] # Background color
243
  colored_mask = colors[masks]
244
  alpha = 0.4
245
  segmentation_overlay = (1 - alpha) * segmentation_overlay + alpha * colored_mask
246
  segmentation_overlay = np.clip(segmentation_overlay, 0, 255).astype(np.uint8)
247
-
248
  info_msg = f"Segmentation complete! Found {cell_count} cells.\n"
249
  info_msg += f"Confluency: {confluency:.1f}%\n"
250
  if region_coords:
251
  info_msg += f"Processed region: {region_coords[0]},{region_coords[1]} to {region_coords[2]},{region_coords[3]}\n"
252
  info_msg += f"Now adjust the Blue Threshold for viability assessment."
253
-
254
- # Return initial segmentation display and state variables
255
- return cell_count, Image.fromarray(segmentation_overlay), info_msg, gr.update(visible=True), pack_array(masks), pack_array(processed_image_np), confluency
256
-
 
 
 
 
 
 
 
 
257
  except Exception as e:
258
  return 0, None, f"Error during segmentation: {str(e)}", gr.update(visible=False), None, None, 0.0
259
 
 
260
  @spaces.GPU
261
  def run_segmentation_manual(image, model_choice, crop_coords):
262
  """
@@ -265,29 +295,22 @@ def run_segmentation_manual(image, model_choice, crop_coords):
265
  """
266
  if image is None:
267
  return 0, None, "No image provided", gr.update(visible=False), None, None, 0.0
268
-
269
  try:
270
  model_filename = MODEL_OPTIONS[model_choice]
271
  model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
272
-
273
  if model_filename in loaded_models:
274
  model = loaded_models[model_filename]
275
  else:
276
  model = models.CellposeModel(gpu=True, pretrained_model=model_path)
277
  loaded_models[model_filename] = model
278
-
279
  image_np = np.array(image)
280
-
281
- # Resize large images to prevent crashes
282
- max_size = 256
283
- if image_np.shape[0] > max_size or image_np.shape[1] > max_size:
284
- h, w = image_np.shape[:2]
285
- if h > w:
286
- new_h, new_w = max_size, int(w * max_size / h)
287
- else:
288
- new_h, new_w = int(h * max_size / w), max_size
289
- image_np = cv2.resize(image_np, (new_w, new_h), interpolation=cv2.INTER_AREA)
290
-
291
  # Apply crop if coordinates are provided
292
  if crop_coords and crop_coords.strip():
293
  try:
@@ -295,81 +318,89 @@ def run_segmentation_manual(image, model_choice, crop_coords):
295
  if len(coords) == 4:
296
  x_min, y_min, x_max, y_max = coords
297
  h, w = image_np.shape[:2]
298
- x_min = max(0, min(x_min, w-1))
299
- y_min = max(0, min(y_min, h-1))
300
- x_max = max(x_min+1, min(x_max, w))
301
- y_max = max(y_min+1, min(y_max, h))
302
-
303
  processed_image_np = image_np[y_min:y_max, x_min:x_max]
304
  else:
305
- processed_image_np = image_np # No valid crop, use original
306
  except ValueError:
307
- processed_image_np = image_np # Invalid crop, use original
308
  else:
309
- processed_image_np = image_np # No crop coords, use original
310
-
311
  # Process image format to RGB
312
  if len(processed_image_np.shape) == 2:
313
  processed_image_np = cv2.cvtColor(processed_image_np, cv2.COLOR_GRAY2RGB)
314
  elif len(processed_image_np.shape) == 3 and processed_image_np.shape[2] == 4:
315
  processed_image_np = cv2.cvtColor(processed_image_np, cv2.COLOR_RGBA2RGB)
316
-
317
  # Run Cellpose
318
  masks, flows, styles = model.eval(processed_image_np, diameter=None, channels=[0, 0])
319
-
320
- cell_count = len(np.unique(masks)) - 1
321
  confluency = measure_confluency(masks, processed_image_np)
322
-
323
  # Create a basic segmentation overlay
324
  segmentation_overlay = processed_image_np.copy().astype(np.float32)
325
  if masks.max() > 0:
326
  np.random.seed(42)
327
  colors = np.random.randint(0, 255, size=(masks.max() + 1, 3))
328
- colors[0] = [0, 0, 0]
329
  colored_mask = colors[masks]
330
  alpha = 0.4
331
  segmentation_overlay = (1 - alpha) * segmentation_overlay + alpha * colored_mask
332
  segmentation_overlay = np.clip(segmentation_overlay, 0, 255).astype(np.uint8)
333
-
334
  info_msg = f"Segmentation complete! Found {cell_count} cells.\n"
335
  info_msg += f"Confluency: {confluency:.1f}%\n"
336
  if crop_coords:
337
  info_msg += f"Processed with coordinates: {crop_coords}\n"
338
  info_msg += f"Now adjust the Blue Threshold for viability assessment."
339
-
340
- return cell_count, Image.fromarray(segmentation_overlay), info_msg, gr.update(visible=True), pack_array(masks), pack_array(processed_image_np), confluency
341
-
 
 
 
 
 
 
 
 
342
  except Exception as e:
343
  return 0, None, f"Error during segmentation: {str(e)}", gr.update(visible=False), None, None, 0.0
344
 
 
345
  def update_viability_realtime(blue_threshold, stored_masks, stored_image_np):
 
346
  if stored_masks is None or stored_image_np is None:
347
  return None, 0, 0, 0.0, "Please run segmentation first."
348
-
349
  stored_masks = unpack_array(stored_masks)
350
  stored_image_np = unpack_array(stored_image_np)
351
 
352
- if stored_masks is None or stored_image_np is None:
353
- return None, 0, 0, 0.0, "Please run segmentation first."
354
-
355
  try:
356
  dead_count, alive_count, viability_overlay_np = classify_cells_by_blueness(
357
  stored_image_np, stored_masks, blue_threshold
358
  )
359
-
360
  total_count = alive_count + dead_count
361
  viability_percent = (alive_count / total_count * 100) if total_count > 0 else 0.0
362
  confluency = measure_confluency(stored_masks, stored_image_np)
363
-
364
  overlay_image = Image.fromarray(viability_overlay_np)
365
  info_msg = f"Total cells: {total_count}\nLive (green): {alive_count}\nDead (red): {dead_count}\n"
366
  info_msg += f"Viability: {viability_percent:.1f}%\nConfluency: {confluency:.1f}%\nBlue threshold: {blue_threshold}%"
367
-
368
  return overlay_image, alive_count, dead_count, viability_percent, info_msg
369
-
370
  except Exception as e:
371
  return None, 0, 0, 0.0, f"Error updating viability: {str(e)}"
372
 
 
373
  # PWA Head HTML - Updated for nested zip structure
374
  pwa_head = """
375
  <link rel="manifest" href="/file=static.zip/static/manifest.json">
@@ -392,36 +423,42 @@ if ('serviceWorker' in navigator) {
392
  }
393
  </script>
394
  """
 
 
395
  def pack_array(arr):
396
  pil = Image.fromarray(arr.astype(np.uint8))
397
  buf = io.BytesIO()
398
  pil.save(buf, format="PNG")
399
  return buf.getvalue()
400
 
 
401
  def unpack_array(data):
402
  return np.array(Image.open(io.BytesIO(data)))
403
 
 
404
  # Gradio interface
405
  with gr.Blocks(
406
- title="CellposeCellCounter",
407
  theme=gr.themes.Soft(),
408
  head=pwa_head # Add PWA head elements
409
  ) as demo:
410
  gr.Markdown("# CellposeCellCounter")
411
  gr.Markdown("For accurate cell confluency, crop the image to display only desired area.")
412
-
 
413
  masks_state = gr.State(value=None)
414
  image_state = gr.State(value=None)
415
 
416
-
417
  with gr.Tab("Image Editor (Draw Selection)"):
418
  gr.Markdown("### Draw selection and run segmentation")
419
-
420
  with gr.Row():
421
  with gr.Column():
422
  image_editor = gr.ImageEditor(
423
  label="Draw selection on image",
424
  type="pil",
 
 
425
  brush=gr.Brush(colors=["#ff0000"], color_mode="fixed", default_size=20),
426
  eraser=gr.Eraser(default_size=20)
427
  )
@@ -431,18 +468,18 @@ with gr.Blocks(
431
  value="Hemocytometer Model"
432
  )
433
  segment_btn1 = gr.Button("🔬 Run Segmentation", variant="primary", size="lg")
434
-
435
  with gr.Column():
436
  cell_count_output1 = gr.Number(label="Total Cells Detected", precision=0)
437
  confluency_output1 = gr.Number(label="Confluency (%)", precision=1)
438
  overlay_output1 = gr.Image(type="pil", label="Segmentation Result")
439
  info_output1 = gr.Textbox(label="Processing Info", lines=4)
440
-
441
  # Viability Assessment Section
442
  with gr.Group(visible=False) as viability_section1:
443
  gr.Markdown("### Viability Assessment (Trypan Blue)")
444
  gr.Markdown("Adjust the threshold to classify cells as live (green) or dead (red).")
445
-
446
  with gr.Row():
447
  with gr.Column():
448
  blue_threshold1 = gr.Slider(
@@ -453,27 +490,26 @@ with gr.Blocks(
453
  label="Blue Threshold (%)",
454
  info="Higher values = more selective for blue cells"
455
  )
456
-
457
  with gr.Column():
458
  live_count_output1 = gr.Number(label="Live Cells (Green)", precision=0)
459
  dead_count_output1 = gr.Number(label="Dead Cells (Red)", precision=0)
460
-
461
  viability_overlay1 = gr.Image(type="pil", label="Viability Assessment (Green=Live, Red=Dead)")
462
  viability_percent_output1 = gr.Number(label="Viability (%)", precision=1)
463
  viability_info1 = gr.Textbox(label="Analysis Results", lines=5)
464
-
465
  # Event handlers
466
- # segment_cells now returns masks and image_np which are stored in masks_state and image_state
467
  segment_btn1.click(
468
  fn=run_segmentation_editor,
469
  inputs=[image_editor, model_dropdown1],
470
  outputs=[cell_count_output1, overlay_output1, info_output1, viability_section1, masks_state, image_state, confluency_output1]
471
  ).then( # Chain the initial viability assessment after segmentation
472
  fn=update_viability_realtime,
473
- inputs=[blue_threshold1, masks_state, image_state], # Pass stored state as inputs
474
  outputs=[viability_overlay1, live_count_output1, dead_count_output1, viability_percent_output1, viability_info1]
475
  )
476
-
477
  # Slider changes update viability in real-time
478
  blue_threshold1.change(
479
  fn=update_viability_realtime,
@@ -483,10 +519,15 @@ with gr.Blocks(
483
 
484
  with gr.Tab("Manual Coordinates"):
485
  gr.Markdown("### Upload image and run segmentation")
486
-
487
  with gr.Row():
488
  with gr.Column():
489
- image_input = gr.Image(type="pil", label="Microscopy Image")
 
 
 
 
 
490
  model_dropdown2 = gr.Dropdown(
491
  choices=list(MODEL_OPTIONS.keys()),
492
  label="Select Model",
@@ -498,18 +539,18 @@ with gr.Blocks(
498
  info="Format: x_min,y_min,x_max,y_max"
499
  )
500
  segment_btn2 = gr.Button("🔬 Run Segmentation", variant="primary", size="lg")
501
-
502
  with gr.Column():
503
  cell_count_output2 = gr.Number(label="Total Cells Detected", precision=0)
504
  confluency_output2 = gr.Number(label="Confluency (%)", precision=1)
505
  overlay_output2 = gr.Image(type="pil", label="Segmentation Result")
506
  info_output2 = gr.Textbox(label="Processing Info", lines=4)
507
-
508
  # Viability Assessment Section
509
  with gr.Group(visible=False) as viability_section2:
510
  gr.Markdown("### Viability Assessment (Trypan Blue)")
511
  gr.Markdown("Adjust the threshold to classify cells as live (green) or dead (red).")
512
-
513
  with gr.Row():
514
  with gr.Column():
515
  blue_threshold2 = gr.Slider(
@@ -520,15 +561,15 @@ with gr.Blocks(
520
  label="Blue Threshold (%)",
521
  info="Higher values = more selective for blue cells"
522
  )
523
-
524
  with gr.Column():
525
  live_count_output2 = gr.Number(label="Live Cells (Green)", precision=0)
526
  dead_count_output2 = gr.Number(label="Dead Cells (Red)", precision=0)
527
-
528
  viability_overlay2 = gr.Image(type="pil", label="Viability Assessment (Green=Live, Red=Dead)")
529
  viability_percent_output2 = gr.Number(label="Viability (%)", precision=1)
530
  viability_info2 = gr.Textbox(label="Analysis Results", lines=5)
531
-
532
  # Event handlers
533
  segment_btn2.click(
534
  fn=run_segmentation_manual,
@@ -536,10 +577,10 @@ with gr.Blocks(
536
  outputs=[cell_count_output2, overlay_output2, info_output2, viability_section2, masks_state, image_state, confluency_output2]
537
  ).then( # Chain the initial viability assessment after segmentation
538
  fn=update_viability_realtime,
539
- inputs=[blue_threshold2, masks_state, image_state],
540
  outputs=[viability_overlay2, live_count_output2, dead_count_output2, viability_percent_output2, viability_info2]
541
  )
542
-
543
  # Slider changes update viability in real-time
544
  blue_threshold2.change(
545
  fn=update_viability_realtime,
@@ -551,17 +592,17 @@ with gr.Blocks(
551
  with gr.Accordion("Instructions", open=False):
552
  gr.Markdown("""
553
  ### How to use:
554
-
555
  1. **Upload and Segment**:
556
  - Upload your microscopy image.
557
  - Select a Cellpose model (e.g., "Hemocytometer Model" for blood cells).
558
  - Draw a selection region using the Image Editor, or specify coordinates manually.
559
  - Click "Run Segmentation".
560
-
561
  2. **Analysis Results**:
562
  - **Cell Count**: Total number of detected cells
563
  - **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.
564
-
565
  3. **Real-time Viability Assessment (Trypan Blue)**:
566
  - After segmentation, the viability section will become visible.
567
  - This tool is specifically designed for **Trypan Blue stained images**, where dead cells appear blue.
@@ -569,11 +610,11 @@ with gr.Blocks(
569
  - **Lower values (e.g., 10-20%)** are more sensitive and will classify more cells as blue/dead.
570
  - **Higher values (e.g., 30-50%)** are more selective and will only classify strongly blue cells as dead.
571
  - Green cells = Live, Red cells = Dead.
572
-
573
  4. **Interpreting Results**:
574
  - The app calculates and displays the total, live, and dead cell counts, along with the viability percentage and confluency.
575
  - **Confluency** helps assess how densely packed your cells are, which is important for cell culture monitoring.
576
  """)
577
 
578
  if __name__ == "__main__":
579
- demo.launch()
 
8
  from PIL import Image
9
  import io
10
  from huggingface_hub import hf_hub_download
11
+ import base64 # needed for mobile ImageEditor base64 handling
 
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 = 512 # shrink any side larger than this
23
+ MAX_PIXELS = 512 * 512 # ~0.26 megapixels
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
+ # already small enough
35
+ if max(h, w) <= MAX_SIDE and total <= MAX_PIXELS:
36
+ return image_np
37
+
38
+ # compute scale so BOTH constraints are satisfied
39
+ scale_side = MAX_SIDE / max(h, w)
40
+ scale_pixels = (MAX_PIXELS / total) ** 0.5
41
+ scale = min(scale_side, scale_pixels)
42
+
43
+ new_w = max(1, int(w * scale))
44
+ new_h = max(1, int(h * scale))
45
+
46
+ return cv2.resize(image_np, (new_w, new_h), interpolation=cv2.INTER_AREA)
47
+
48
+
49
  def extract_region_from_editor(editor_data):
50
  """Extract selected region from ImageEditor data (mobile-safe)."""
51
  if editor_data is None:
 
106
  else:
107
  try:
108
  sel_np = np.array(sel)
109
+ except Exception:
110
  sel_np = None
111
 
112
  # If selection exists: compute bounding box
113
  if sel_np is not None:
114
  if sel_np.ndim == 3:
115
+ if sel_np.shape[2] == 4:
116
  mask = sel_np[:, :, 3] > 0
117
  else:
118
  mask = np.any(sel_np > 0, axis=2)
 
123
  if coords[0].size > 0:
124
  y0, y1 = coords[0].min(), coords[0].max()
125
  x0, x1 = coords[1].min(), coords[1].max()
126
+ return background_np[y0:y1 + 1, x0:x1 + 1], (x0, y0, x1, y1)
127
 
128
  # No selection → return full image
129
  return background_np, None
 
135
  def classify_cells_by_blueness(image_np, masks, blue_threshold):
136
  """
137
  Classify cells as dead (blue) or alive based on single blueness metric
138
+
139
  Args:
140
  image_np: RGB image array
141
  masks: Cellpose segmentation masks
142
  blue_threshold: Single threshold value (0-100) for blueness detection
143
+
144
  Returns:
145
  dead_count, alive_count, colored_overlay
146
  """
 
152
 
153
  # Convert RGB to HSV
154
  hsv = cv2.cvtColor(image_np, cv2.COLOR_RGB2HSV)
155
+
156
  # Calculate blueness index for each pixel
157
  hue = hsv[:, :, 0].astype(np.float32)
158
  saturation = hsv[:, :, 1].astype(np.float32)
159
+
160
  # Hue score: peaks around 115 (blue in HSV), drops off towards edges
161
  # Handle hue wrap-around for blue detection (100-130 range)
162
  hue_distance = np.minimum(np.abs(hue - 115), 180 - np.abs(hue - 115))
163
  hue_score = np.maximum(0, 1 - hue_distance / 65) # 65 gives good blue range
164
+
165
  # Combine hue proximity with saturation intensity
166
  blueness = hue_score * (saturation / 255.0)
167
+
168
  # Convert threshold from 0-100 to 0-1 scale
169
  threshold = blue_threshold / 100.0
170
+
171
  # Get unique cell IDs (excluding background)
172
  cell_ids = np.unique(masks)
173
  cell_ids = cell_ids[cell_ids > 0] # Remove background (0)
174
+
175
  dead_cells = []
176
  alive_cells = []
177
+
178
  # Classify each cell
179
  for cell_id in cell_ids:
180
  # Get mask for this specific cell
181
  cell_mask = (masks == cell_id)
182
+
183
  # Calculate average blueness for this cell
184
  cell_blueness = np.mean(blueness[cell_mask])
185
+
186
  # Classify based on threshold
187
  if cell_blueness > threshold:
188
  dead_cells.append(cell_id)
189
  else:
190
  alive_cells.append(cell_id)
191
+
192
  # Create colored overlay
193
+ overlay = image_np.copy().astype(np.float32) # Ensure float for blending
194
+
195
  # Color dead cells red, alive cells green
196
  for cell_id in dead_cells:
197
  cell_mask = (masks == cell_id)
198
  overlay[cell_mask] = [255, 0, 0] # Red for dead
199
+
200
  for cell_id in alive_cells:
201
  cell_mask = (masks == cell_id)
202
  overlay[cell_mask] = [0, 255, 0] # Green for alive
203
+
204
  # Blend with original image
205
  alpha = 0.4
206
  final_overlay = (1 - alpha) * image_np.astype(np.float32) + alpha * overlay
207
  final_overlay = np.clip(final_overlay, 0, 255).astype(np.uint8)
208
+
209
  return len(dead_cells), len(alive_cells), final_overlay
210
+
211
+
212
  def measure_confluency(masks, image_np):
213
  """Calculate the percentage of image area covered by cells"""
214
  tot_pixels = image_np.shape[0] * image_np.shape[1]
 
226
  try:
227
  model_filename = MODEL_OPTIONS[model_choice]
228
  model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
229
+
230
  if model_filename in loaded_models:
231
  model = loaded_models[model_filename]
232
  else:
233
  model = models.CellposeModel(gpu=True, pretrained_model=model_path)
234
  loaded_models[model_filename] = model
235
+
236
  region_np, region_coords = extract_region_from_editor(editor_data)
237
+
238
  if region_np is None:
239
  return 0, None, f"No image provided.", gr.update(visible=False), None, None, 0.0
240
+
241
+ # Enforce mobile-safe size limit immediately
242
+ region_np = safe_resize(region_np)
243
+
 
 
 
 
 
 
 
244
  # Process image format to RGB
245
  if len(region_np.shape) == 2:
246
  processed_image_np = cv2.cvtColor(region_np, cv2.COLOR_GRAY2RGB)
 
248
  processed_image_np = cv2.cvtColor(region_np, cv2.COLOR_RGBA2RGB)
249
  else:
250
  processed_image_np = region_np
251
+
252
  # Run Cellpose segmentation
253
  masks, flows, styles = model.eval(processed_image_np, diameter=None, channels=[0, 0])
254
+
255
  cell_count = len(np.unique(masks)) - 1
256
  confluency = measure_confluency(masks, processed_image_np)
257
+
258
  # Create a basic segmentation overlay (without viability)
259
  segmentation_overlay = processed_image_np.copy().astype(np.float32)
260
  if masks.max() > 0:
261
+ np.random.seed(42) # For consistent random colors
262
  colors = np.random.randint(0, 255, size=(masks.max() + 1, 3))
263
+ colors[0] = [0, 0, 0] # Background color
264
  colored_mask = colors[masks]
265
  alpha = 0.4
266
  segmentation_overlay = (1 - alpha) * segmentation_overlay + alpha * colored_mask
267
  segmentation_overlay = np.clip(segmentation_overlay, 0, 255).astype(np.uint8)
268
+
269
  info_msg = f"Segmentation complete! Found {cell_count} cells.\n"
270
  info_msg += f"Confluency: {confluency:.1f}%\n"
271
  if region_coords:
272
  info_msg += f"Processed region: {region_coords[0]},{region_coords[1]} to {region_coords[2]},{region_coords[3]}\n"
273
  info_msg += f"Now adjust the Blue Threshold for viability assessment."
274
+
275
+ # Return initial segmentation display and state variables (packed for Gradio State)
276
+ return (
277
+ cell_count,
278
+ Image.fromarray(segmentation_overlay),
279
+ info_msg,
280
+ gr.update(visible=True),
281
+ pack_array(masks),
282
+ pack_array(processed_image_np),
283
+ confluency,
284
+ )
285
+
286
  except Exception as e:
287
  return 0, None, f"Error during segmentation: {str(e)}", gr.update(visible=False), None, None, 0.0
288
 
289
+
290
  @spaces.GPU
291
  def run_segmentation_manual(image, model_choice, crop_coords):
292
  """
 
295
  """
296
  if image is None:
297
  return 0, None, "No image provided", gr.update(visible=False), None, None, 0.0
298
+
299
  try:
300
  model_filename = MODEL_OPTIONS[model_choice]
301
  model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
302
+
303
  if model_filename in loaded_models:
304
  model = loaded_models[model_filename]
305
  else:
306
  model = models.CellposeModel(gpu=True, pretrained_model=model_path)
307
  loaded_models[model_filename] = model
308
+
309
  image_np = np.array(image)
310
+
311
+ # Enforce mobile-safe size limit immediately
312
+ image_np = safe_resize(image_np)
313
+
 
 
 
 
 
 
 
314
  # Apply crop if coordinates are provided
315
  if crop_coords and crop_coords.strip():
316
  try:
 
318
  if len(coords) == 4:
319
  x_min, y_min, x_max, y_max = coords
320
  h, w = image_np.shape[:2]
321
+ x_min = max(0, min(x_min, w - 1))
322
+ y_min = max(0, min(y_min, h - 1))
323
+ x_max = max(x_min + 1, min(x_max, w))
324
+ y_max = max(y_min + 1, min(y_max, h))
325
+
326
  processed_image_np = image_np[y_min:y_max, x_min:x_max]
327
  else:
328
+ processed_image_np = image_np # No valid crop, use original
329
  except ValueError:
330
+ processed_image_np = image_np # Invalid crop, use original
331
  else:
332
+ processed_image_np = image_np # No crop coords, use original
333
+
334
  # Process image format to RGB
335
  if len(processed_image_np.shape) == 2:
336
  processed_image_np = cv2.cvtColor(processed_image_np, cv2.COLOR_GRAY2RGB)
337
  elif len(processed_image_np.shape) == 3 and processed_image_np.shape[2] == 4:
338
  processed_image_np = cv2.cvtColor(processed_image_np, cv2.COLOR_RGBA2RGB)
339
+
340
  # Run Cellpose
341
  masks, flows, styles = model.eval(processed_image_np, diameter=None, channels=[0, 0])
342
+
343
+ cell_count = len(np.unique(masks)) - 1
344
  confluency = measure_confluency(masks, processed_image_np)
345
+
346
  # Create a basic segmentation overlay
347
  segmentation_overlay = processed_image_np.copy().astype(np.float32)
348
  if masks.max() > 0:
349
  np.random.seed(42)
350
  colors = np.random.randint(0, 255, size=(masks.max() + 1, 3))
351
+ colors[0] = [0, 0, 0]
352
  colored_mask = colors[masks]
353
  alpha = 0.4
354
  segmentation_overlay = (1 - alpha) * segmentation_overlay + alpha * colored_mask
355
  segmentation_overlay = np.clip(segmentation_overlay, 0, 255).astype(np.uint8)
356
+
357
  info_msg = f"Segmentation complete! Found {cell_count} cells.\n"
358
  info_msg += f"Confluency: {confluency:.1f}%\n"
359
  if crop_coords:
360
  info_msg += f"Processed with coordinates: {crop_coords}\n"
361
  info_msg += f"Now adjust the Blue Threshold for viability assessment."
362
+
363
+ return (
364
+ cell_count,
365
+ Image.fromarray(segmentation_overlay),
366
+ info_msg,
367
+ gr.update(visible=True),
368
+ pack_array(masks),
369
+ pack_array(processed_image_np),
370
+ confluency,
371
+ )
372
+
373
  except Exception as e:
374
  return 0, None, f"Error during segmentation: {str(e)}", gr.update(visible=False), None, None, 0.0
375
 
376
+
377
  def update_viability_realtime(blue_threshold, stored_masks, stored_image_np):
378
+ # avoid unpacking None (e.g. slider moved before segmentation)
379
  if stored_masks is None or stored_image_np is None:
380
  return None, 0, 0, 0.0, "Please run segmentation first."
381
+
382
  stored_masks = unpack_array(stored_masks)
383
  stored_image_np = unpack_array(stored_image_np)
384
 
 
 
 
385
  try:
386
  dead_count, alive_count, viability_overlay_np = classify_cells_by_blueness(
387
  stored_image_np, stored_masks, blue_threshold
388
  )
389
+
390
  total_count = alive_count + dead_count
391
  viability_percent = (alive_count / total_count * 100) if total_count > 0 else 0.0
392
  confluency = measure_confluency(stored_masks, stored_image_np)
393
+
394
  overlay_image = Image.fromarray(viability_overlay_np)
395
  info_msg = f"Total cells: {total_count}\nLive (green): {alive_count}\nDead (red): {dead_count}\n"
396
  info_msg += f"Viability: {viability_percent:.1f}%\nConfluency: {confluency:.1f}%\nBlue threshold: {blue_threshold}%"
397
+
398
  return overlay_image, alive_count, dead_count, viability_percent, info_msg
399
+
400
  except Exception as e:
401
  return None, 0, 0, 0.0, f"Error updating viability: {str(e)}"
402
 
403
+
404
  # PWA Head HTML - Updated for nested zip structure
405
  pwa_head = """
406
  <link rel="manifest" href="/file=static.zip/static/manifest.json">
 
423
  }
424
  </script>
425
  """
426
+
427
+
428
  def pack_array(arr):
429
  pil = Image.fromarray(arr.astype(np.uint8))
430
  buf = io.BytesIO()
431
  pil.save(buf, format="PNG")
432
  return buf.getvalue()
433
 
434
+
435
  def unpack_array(data):
436
  return np.array(Image.open(io.BytesIO(data)))
437
 
438
+
439
  # Gradio interface
440
  with gr.Blocks(
441
+ title="CellposeCellCounter",
442
  theme=gr.themes.Soft(),
443
  head=pwa_head # Add PWA head elements
444
  ) as demo:
445
  gr.Markdown("# CellposeCellCounter")
446
  gr.Markdown("For accurate cell confluency, crop the image to display only desired area.")
447
+
448
+ # Define State components to store masks and image data across function calls
449
  masks_state = gr.State(value=None)
450
  image_state = gr.State(value=None)
451
 
 
452
  with gr.Tab("Image Editor (Draw Selection)"):
453
  gr.Markdown("### Draw selection and run segmentation")
454
+
455
  with gr.Row():
456
  with gr.Column():
457
  image_editor = gr.ImageEditor(
458
  label="Draw selection on image",
459
  type="pil",
460
+ image_mode="RGB",
461
+ height=512, # limit canvas size on client (important for mobile)
462
  brush=gr.Brush(colors=["#ff0000"], color_mode="fixed", default_size=20),
463
  eraser=gr.Eraser(default_size=20)
464
  )
 
468
  value="Hemocytometer Model"
469
  )
470
  segment_btn1 = gr.Button("🔬 Run Segmentation", variant="primary", size="lg")
471
+
472
  with gr.Column():
473
  cell_count_output1 = gr.Number(label="Total Cells Detected", precision=0)
474
  confluency_output1 = gr.Number(label="Confluency (%)", precision=1)
475
  overlay_output1 = gr.Image(type="pil", label="Segmentation Result")
476
  info_output1 = gr.Textbox(label="Processing Info", lines=4)
477
+
478
  # Viability Assessment Section
479
  with gr.Group(visible=False) as viability_section1:
480
  gr.Markdown("### Viability Assessment (Trypan Blue)")
481
  gr.Markdown("Adjust the threshold to classify cells as live (green) or dead (red).")
482
+
483
  with gr.Row():
484
  with gr.Column():
485
  blue_threshold1 = gr.Slider(
 
490
  label="Blue Threshold (%)",
491
  info="Higher values = more selective for blue cells"
492
  )
493
+
494
  with gr.Column():
495
  live_count_output1 = gr.Number(label="Live Cells (Green)", precision=0)
496
  dead_count_output1 = gr.Number(label="Dead Cells (Red)", precision=0)
497
+
498
  viability_overlay1 = gr.Image(type="pil", label="Viability Assessment (Green=Live, Red=Dead)")
499
  viability_percent_output1 = gr.Number(label="Viability (%)", precision=1)
500
  viability_info1 = gr.Textbox(label="Analysis Results", lines=5)
501
+
502
  # Event handlers
 
503
  segment_btn1.click(
504
  fn=run_segmentation_editor,
505
  inputs=[image_editor, model_dropdown1],
506
  outputs=[cell_count_output1, overlay_output1, info_output1, viability_section1, masks_state, image_state, confluency_output1]
507
  ).then( # Chain the initial viability assessment after segmentation
508
  fn=update_viability_realtime,
509
+ inputs=[blue_threshold1, masks_state, image_state], # Pass stored state as inputs
510
  outputs=[viability_overlay1, live_count_output1, dead_count_output1, viability_percent_output1, viability_info1]
511
  )
512
+
513
  # Slider changes update viability in real-time
514
  blue_threshold1.change(
515
  fn=update_viability_realtime,
 
519
 
520
  with gr.Tab("Manual Coordinates"):
521
  gr.Markdown("### Upload image and run segmentation")
522
+
523
  with gr.Row():
524
  with gr.Column():
525
+ image_input = gr.Image(
526
+ type="pil",
527
+ label="Microscopy Image",
528
+ image_mode="RGB",
529
+ height=512 # limit canvas size here too
530
+ )
531
  model_dropdown2 = gr.Dropdown(
532
  choices=list(MODEL_OPTIONS.keys()),
533
  label="Select Model",
 
539
  info="Format: x_min,y_min,x_max,y_max"
540
  )
541
  segment_btn2 = gr.Button("🔬 Run Segmentation", variant="primary", size="lg")
542
+
543
  with gr.Column():
544
  cell_count_output2 = gr.Number(label="Total Cells Detected", precision=0)
545
  confluency_output2 = gr.Number(label="Confluency (%)", precision=1)
546
  overlay_output2 = gr.Image(type="pil", label="Segmentation Result")
547
  info_output2 = gr.Textbox(label="Processing Info", lines=4)
548
+
549
  # Viability Assessment Section
550
  with gr.Group(visible=False) as viability_section2:
551
  gr.Markdown("### Viability Assessment (Trypan Blue)")
552
  gr.Markdown("Adjust the threshold to classify cells as live (green) or dead (red).")
553
+
554
  with gr.Row():
555
  with gr.Column():
556
  blue_threshold2 = gr.Slider(
 
561
  label="Blue Threshold (%)",
562
  info="Higher values = more selective for blue cells"
563
  )
564
+
565
  with gr.Column():
566
  live_count_output2 = gr.Number(label="Live Cells (Green)", precision=0)
567
  dead_count_output2 = gr.Number(label="Dead Cells (Red)", precision=0)
568
+
569
  viability_overlay2 = gr.Image(type="pil", label="Viability Assessment (Green=Live, Red=Dead)")
570
  viability_percent_output2 = gr.Number(label="Viability (%)", precision=1)
571
  viability_info2 = gr.Textbox(label="Analysis Results", lines=5)
572
+
573
  # Event handlers
574
  segment_btn2.click(
575
  fn=run_segmentation_manual,
 
577
  outputs=[cell_count_output2, overlay_output2, info_output2, viability_section2, masks_state, image_state, confluency_output2]
578
  ).then( # Chain the initial viability assessment after segmentation
579
  fn=update_viability_realtime,
580
+ inputs=[blue_threshold2, masks_state, image_state],
581
  outputs=[viability_overlay2, live_count_output2, dead_count_output2, viability_percent_output2, viability_info2]
582
  )
583
+
584
  # Slider changes update viability in real-time
585
  blue_threshold2.change(
586
  fn=update_viability_realtime,
 
592
  with gr.Accordion("Instructions", open=False):
593
  gr.Markdown("""
594
  ### How to use:
595
+
596
  1. **Upload and Segment**:
597
  - Upload your microscopy image.
598
  - Select a Cellpose model (e.g., "Hemocytometer Model" for blood cells).
599
  - Draw a selection region using the Image Editor, or specify coordinates manually.
600
  - Click "Run Segmentation".
601
+
602
  2. **Analysis Results**:
603
  - **Cell Count**: Total number of detected cells
604
  - **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.
605
+
606
  3. **Real-time Viability Assessment (Trypan Blue)**:
607
  - After segmentation, the viability section will become visible.
608
  - This tool is specifically designed for **Trypan Blue stained images**, where dead cells appear blue.
 
610
  - **Lower values (e.g., 10-20%)** are more sensitive and will classify more cells as blue/dead.
611
  - **Higher values (e.g., 30-50%)** are more selective and will only classify strongly blue cells as dead.
612
  - Green cells = Live, Red cells = Dead.
613
+
614
  4. **Interpreting Results**:
615
  - The app calculates and displays the total, live, and dead cell counts, along with the viability percentage and confluency.
616
  - **Confluency** helps assess how densely packed your cells are, which is important for cell culture monitoring.
617
  """)
618
 
619
  if __name__ == "__main__":
620
+ demo.launch()