LiangLabUMB commited on
Commit
98a07a0
·
verified ·
1 Parent(s): a3c88f8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +229 -196
app.py CHANGED
@@ -17,55 +17,37 @@ MODEL_OPTIONS = {
17
 
18
  loaded_models = {}
19
 
20
- # Removed the debug_editor_data function completely as per request
21
-
22
  def extract_region_from_editor(editor_data):
23
  """Extract the selected region from ImageEditor data"""
24
  if editor_data is None:
25
  return None, None
26
 
27
- # ImageEditor can return different formats depending on version
28
- # Let's handle the most common cases
29
-
30
  if isinstance(editor_data, dict):
31
- # Case 1: Dictionary with 'background' and 'layers'
32
  background = editor_data.get('background')
33
  layers = editor_data.get('layers', [])
34
 
35
  if background is None:
36
  return None, None
37
 
38
- # Convert background to numpy array
39
  background_np = np.array(background)
40
 
41
- # If there are drawn layers, try to extract selection
42
  if layers and len(layers) > 0:
43
- # Get the first layer
44
  selection_layer = layers[0]
45
-
46
- # Convert to numpy array
47
  selection_np = np.array(selection_layer)
48
 
49
- # Find non-transparent/non-black pixels as selection
50
  if len(selection_np.shape) == 3:
51
- # For RGB, look for non-black pixels
52
  if selection_np.shape[2] == 4: # RGBA
53
- # Use alpha channel
54
  mask = selection_np[:, :, 3] > 0
55
  else: # RGB
56
- # Use non-black pixels
57
  mask = np.any(selection_np > 0, axis=2)
58
  else:
59
- # Grayscale
60
  mask = selection_np > 0
61
 
62
- # Find bounding box of the selection
63
  coords = np.where(mask)
64
  if len(coords[0]) > 0:
65
  y_min, y_max = coords[0].min(), coords[0].max()
66
  x_min, x_max = coords[1].min(), coords[1].max()
67
 
68
- # Add some padding to ensure we don't get tiny regions
69
  pad = 5
70
  h, w = background_np.shape[:2]
71
  y_min = max(0, y_min - pad)
@@ -73,29 +55,18 @@ def extract_region_from_editor(editor_data):
73
  x_min = max(0, x_min - pad)
74
  x_max = min(w, x_max + pad)
75
 
76
- # Extract the region
77
  region = background_np[y_min:y_max+1, x_min:x_max+1]
78
  return region, (x_min, y_min, x_max, y_max)
79
 
80
- # If no selection, return the full image
81
  return background_np, None
82
 
83
  else:
84
- # Case 2: Direct PIL Image
85
- if hasattr(editor_data, 'size'): # Check if it's a PIL Image
86
  image_np = np.array(editor_data)
87
  return image_np, None
88
  else:
89
  return None, None
90
 
91
- def crop_image_with_coords(image_np, coords):
92
- """Crop image using provided coordinates"""
93
- if coords is None:
94
- return image_np
95
-
96
- x_min, y_min, x_max, y_max = coords
97
- return image_np[y_min:y_max+1, x_min:x_max+1]
98
-
99
  def classify_cells_by_blueness(image_np, masks, blue_threshold):
100
  """
101
  Classify cells as dead (blue) or alive based on single blueness metric
@@ -108,6 +79,12 @@ def classify_cells_by_blueness(image_np, masks, blue_threshold):
108
  Returns:
109
  dead_count, alive_count, colored_overlay
110
  """
 
 
 
 
 
 
111
  # Convert RGB to HSV
112
  hsv = cv2.cvtColor(image_np, cv2.COLOR_RGB2HSV)
113
 
@@ -148,7 +125,7 @@ def classify_cells_by_blueness(image_np, masks, blue_threshold):
148
  alive_cells.append(cell_id)
149
 
150
  # Create colored overlay
151
- overlay = image_np.copy()
152
 
153
  # Color dead cells red, alive cells green
154
  for cell_id in dead_cells:
@@ -161,18 +138,18 @@ def classify_cells_by_blueness(image_np, masks, blue_threshold):
161
 
162
  # Blend with original image
163
  alpha = 0.4
164
- overlay = (1 - alpha) * image_np + alpha * overlay
165
- overlay = np.clip(overlay, 0, 255).astype(np.uint8)
166
 
167
- return len(dead_cells), len(alive_cells), overlay
168
 
169
  @spaces.GPU
170
- def segment_and_count_viability(editor_data, model_choice, blue_threshold, crop_coords=None):
171
  """
172
- Segment cells and analyze viability using single blueness threshold
 
173
  """
174
  try:
175
- # Load the model
176
  model_filename = MODEL_OPTIONS[model_choice]
177
  model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
178
 
@@ -182,134 +159,157 @@ def segment_and_count_viability(editor_data, model_choice, blue_threshold, crop_
182
  model = models.CellposeModel(gpu=True, pretrained_model=model_path)
183
  loaded_models[model_filename] = model
184
 
185
- # Extract region from editor
186
  region_np, region_coords = extract_region_from_editor(editor_data)
187
 
188
  if region_np is None:
189
- return 0, 0, 0, 0.0, None, f"No image provided."
190
 
191
- # If manual crop coordinates are provided, use them instead
192
- if crop_coords and crop_coords.strip():
193
- try:
194
- coords = [int(x.strip()) for x in crop_coords.split(',')]
195
- if len(coords) == 4:
196
- x_min, y_min, x_max, y_max = coords
197
- # Ensure coordinates are within image bounds
198
- h, w = region_np.shape[:2]
199
- x_min = max(0, min(x_min, w-1))
200
- y_min = max(0, min(y_min, h-1))
201
- x_max = max(x_min+1, min(x_max, w))
202
- y_max = max(y_min+1, min(y_max, h))
203
-
204
- region_np = region_np[y_min:y_max, x_min:x_max]
205
- region_coords = (x_min, y_min, x_max, y_max)
206
- except ValueError:
207
- pass # Invalid coordinates, continue with current region
208
-
209
- # If grayscale, convert to RGB
210
  if len(region_np.shape) == 2:
211
- region_np = cv2.cvtColor(region_np, cv2.COLOR_GRAY2RGB)
212
  elif len(region_np.shape) == 3 and region_np.shape[2] == 4:
213
- # Handle RGBA images
214
- region_np = cv2.cvtColor(region_np, cv2.COLOR_RGBA2RGB)
215
-
216
- # Run Cellpose on the selected region
217
- masks, flows, styles = model.eval(region_np, diameter=None, channels=[0, 0])
218
 
219
- # Classify cells by blueness
220
- dead_count, alive_count, viability_overlay = classify_cells_by_blueness(
221
- region_np, masks, blue_threshold
222
- )
223
 
224
- total_count = dead_count + alive_count
225
- viability_percent = (alive_count / total_count * 100) if total_count > 0 else 0.0
226
 
227
- # Convert result to PIL Image for output
228
- overlay_image = Image.fromarray(viability_overlay)
 
 
 
 
 
 
 
 
229
 
230
- # Create info message
231
- info_msg = f"Total cells: {total_count}\nAlive cells: {alive_count}\nDead cells: {dead_count}\nViability: {viability_percent:.1f}%\nBlue threshold: {blue_threshold}%"
232
  if region_coords:
233
- info_msg += f"\nProcessed region: {region_coords[0]},{region_coords[1]} to {region_coords[2]},{region_coords[3]}"
234
-
235
- return total_count, alive_count, dead_count, viability_percent, overlay_image, info_msg
236
-
 
 
237
  except Exception as e:
238
- return 0, 0, 0, 0.0, None, f"Error occurred: {str(e)}"
239
 
240
  @spaces.GPU
241
- def segment_with_coords_viability(image, model_choice, blue_threshold, crop_coords):
242
- """Segment using regular image input with manual coordinate specification and viability analysis"""
 
 
 
243
  if image is None:
244
- return 0, 0, 0, 0.0, None, "No image provided"
245
-
246
  try:
247
- # Load the model
248
  model_filename = MODEL_OPTIONS[model_choice]
249
  model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
250
-
251
  if model_filename in loaded_models:
252
  model = loaded_models[model_filename]
253
  else:
254
  model = models.CellposeModel(gpu=True, pretrained_model=model_path)
255
  loaded_models[model_filename] = model
256
-
257
- # Convert PIL Image to numpy array
258
  image_np = np.array(image)
259
-
260
  # Apply crop if coordinates are provided
261
  if crop_coords and crop_coords.strip():
262
  try:
263
  coords = [int(x.strip()) for x in crop_coords.split(',')]
264
  if len(coords) == 4:
265
  x_min, y_min, x_max, y_max = coords
266
- # Ensure coordinates are within image bounds
267
  h, w = image_np.shape[:2]
268
  x_min = max(0, min(x_min, w-1))
269
  y_min = max(0, min(y_min, h-1))
270
  x_max = max(x_min+1, min(x_max, w))
271
  y_max = max(y_min+1, min(y_max, h))
272
-
273
- image_np = image_np[y_min:y_max, x_min:x_max]
 
 
274
  except ValueError:
275
- pass # Invalid coordinates, use full image
 
 
276
 
277
- # Process image format
278
- if len(image_np.shape) == 2:
279
- image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
280
- elif len(image_np.shape) == 3 and image_np.shape[2] == 4:
281
- image_np = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
282
 
283
  # Run Cellpose
284
- masks, flows, styles = model.eval(image_np, diameter=None, channels=[0, 0])
285
 
286
- # Classify cells by blueness
287
- dead_count, alive_count, viability_overlay = classify_cells_by_blueness(
288
- image_np, masks, blue_threshold
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  )
290
 
291
- total_count = dead_count + alive_count
292
  viability_percent = (alive_count / total_count * 100) if total_count > 0 else 0.0
293
 
294
- overlay_image = Image.fromarray(viability_overlay)
295
-
296
- info_msg = f"Total cells: {total_count}\nAlive cells: {alive_count}\nDead cells: {dead_count}\nViability: {viability_percent:.1f}%\nBlue threshold: {blue_threshold}%"
297
- if crop_coords:
298
- info_msg += f"\nProcessed with coordinates: {crop_coords}"
299
-
300
- return total_count, alive_count, dead_count, viability_percent, overlay_image, info_msg
301
 
 
 
302
  except Exception as e:
303
- return 0, 0, 0, 0.0, None, f"Error occurred: {str(e)}"
304
 
305
- # Create the Gradio interface with tabs for different input methods
306
- with gr.Blocks(title="Cell Viability Counter with Trypan Blue Analysis") as demo:
 
307
  gr.Markdown("# Cell Viability Counter with Trypan Blue Analysis")
308
  gr.Markdown("Upload a trypan blue stained microscopy image and analyze cell viability. Dead cells appear blue, alive cells are unstained.")
309
-
310
- with gr.Tab("Image Editor (Draw Selection)"):
311
- gr.Markdown("Use the drawing tools to select a region of the image for segmentation and viability analysis.")
312
 
 
 
 
 
 
 
 
313
  with gr.Row():
314
  with gr.Column():
315
  image_editor = gr.ImageEditor(
@@ -318,114 +318,147 @@ with gr.Blocks(title="Cell Viability Counter with Trypan Blue Analysis") as demo
318
  brush=gr.Brush(colors=["#ff0000"], color_mode="fixed", default_size=20),
319
  eraser=gr.Eraser(default_size=20)
320
  )
321
-
322
  model_dropdown1 = gr.Dropdown(
323
  choices=list(MODEL_OPTIONS.keys()),
324
  label="Select Model",
325
  value="Hemocytometer Model"
326
  )
327
-
328
- # Single blue threshold control
329
- gr.Markdown("### Trypan Blue Threshold")
330
- blue_threshold1 = gr.Slider(
331
- 0, 100, value=25, step=1,
332
- label="Blue Threshold (%)",
333
- info="Higher values = more selective for blue cells"
334
- )
335
-
336
- segment_btn1 = gr.Button("Analyze Viability", variant="primary")
337
-
338
  with gr.Column():
339
- with gr.Row():
340
- total_count_output1 = gr.Number(label="Total Cells")
341
- alive_count_output1 = gr.Number(label="Alive Cells")
342
- dead_count_output1 = gr.Number(label="Dead Cells")
343
 
344
- viability_output1 = gr.Number(label="Viability (%)", precision=1)
345
- overlay_output1 = gr.Image(type="pil", label="Viability Analysis (Green=Alive, Red=Dead)")
346
- info_output1 = gr.Textbox(label="Analysis Results")
347
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  segment_btn1.click(
349
- fn=segment_and_count_viability,
350
- inputs=[image_editor, model_dropdown1, blue_threshold1],
351
- outputs=[total_count_output1, alive_count_output1, dead_count_output1, viability_output1, overlay_output1, info_output1]
 
 
 
 
352
  )
353
-
 
 
 
 
 
 
 
354
  with gr.Tab("Manual Coordinates"):
355
- gr.Markdown("Upload an image and specify coordinates manually (format: x_min,y_min,x_max,y_max)")
356
-
357
  with gr.Row():
358
  with gr.Column():
359
- image_input = gr.Image(type="pil", label="Trypan Blue Stained Image")
360
-
361
  model_dropdown2 = gr.Dropdown(
362
  choices=list(MODEL_OPTIONS.keys()),
363
  label="Select Model",
364
  value="Hemocytometer Model"
365
  )
366
-
367
  coord_input = gr.Textbox(
368
  label="Crop Coordinates (optional)",
369
  placeholder="e.g., 100,100,400,400",
370
  info="Format: x_min,y_min,x_max,y_max"
371
  )
372
-
373
- # Single blue threshold control
374
- gr.Markdown("### Trypan Blue Threshold")
375
- blue_threshold2 = gr.Slider(
376
- 0, 100, value=25, step=1,
377
- label="Blue Threshold (%)",
378
- info="Higher values = more selective for blue cells"
379
- )
380
-
381
- segment_btn2 = gr.Button("Analyze Viability", variant="primary")
382
-
383
  with gr.Column():
384
- with gr.Row():
385
- total_count_output2 = gr.Number(label="Total Cells")
386
- alive_count_output2 = gr.Number(label="Alive Cells")
387
- dead_count_output2 = gr.Number(label="Dead Cells")
388
 
389
- viability_output2 = gr.Number(label="Viability (%)", precision=1)
390
- overlay_output2 = gr.Image(type="pil", label="Viability Analysis (Green=Alive, Red=Dead)")
391
- info_output2 = gr.Textbox(label="Analysis Results")
392
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  segment_btn2.click(
394
- fn=segment_with_coords_viability,
395
- inputs=[image_input, model_dropdown2, blue_threshold2, coord_input],
396
- outputs=[total_count_output2, alive_count_output2, dead_count_output2, viability_output2, overlay_output2, info_output2]
 
 
 
 
397
  )
398
-
399
- # Add usage instructions
400
- with gr.Tab("Usage Instructions"):
 
 
 
 
 
 
 
401
  gr.Markdown("""
402
- ## How to Use This Tool
403
-
404
- ### 1. Image Preparation
405
- - Upload a trypan blue stained microscopy image
406
- - Dead cells should appear blue, alive cells should be unstained (clear)
407
-
408
- ### 2. Blue Threshold Adjustment
409
- - **Single Slider (0-100%)**: Adjusts sensitivity to blue coloration
410
- - **Lower values (10-20%)**: More sensitive, detects lightly stained cells
411
- - **Higher values (30-50%)**: More selective, only detects strongly blue cells
412
- - **Default (25%)**: Works well for most standard trypan blue staining
413
-
414
- ### 3. How the Blue Detection Works
415
- - Analyzes hue (color) and saturation (intensity) in each cell
416
- - Focuses on blue range around 115° in HSV color space
417
- - Combines color proximity with saturation strength
418
- - Calculates average \"blueness\" for each detected cell
419
-
420
- ### 4. Output
421
- - **Green overlay**: Live cells
422
- - **Red overlay**: Dead cells
423
- - **Viability %**: (Alive cells / Total cells) × 100
424
-
425
- ### 5. Tips
426
- - Use consistent lighting and staining protocols for best results
427
- - Adjust thresholds if cells are misclassified
428
- - The tool works best with clear contrast between stained and unstained cells
429
  """)
430
 
431
  if __name__ == "__main__":
 
17
 
18
  loaded_models = {}
19
 
 
 
20
  def extract_region_from_editor(editor_data):
21
  """Extract the selected region from ImageEditor data"""
22
  if editor_data is None:
23
  return None, None
24
 
 
 
 
25
  if isinstance(editor_data, dict):
 
26
  background = editor_data.get('background')
27
  layers = editor_data.get('layers', [])
28
 
29
  if background is None:
30
  return None, None
31
 
 
32
  background_np = np.array(background)
33
 
 
34
  if layers and len(layers) > 0:
 
35
  selection_layer = layers[0]
 
 
36
  selection_np = np.array(selection_layer)
37
 
 
38
  if len(selection_np.shape) == 3:
 
39
  if selection_np.shape[2] == 4: # RGBA
 
40
  mask = selection_np[:, :, 3] > 0
41
  else: # RGB
 
42
  mask = np.any(selection_np > 0, axis=2)
43
  else:
 
44
  mask = selection_np > 0
45
 
 
46
  coords = np.where(mask)
47
  if len(coords[0]) > 0:
48
  y_min, y_max = coords[0].min(), coords[0].max()
49
  x_min, x_max = coords[1].min(), coords[1].max()
50
 
 
51
  pad = 5
52
  h, w = background_np.shape[:2]
53
  y_min = max(0, y_min - pad)
 
55
  x_min = max(0, x_min - pad)
56
  x_max = min(w, x_max + pad)
57
 
 
58
  region = background_np[y_min:y_max+1, x_min:x_max+1]
59
  return region, (x_min, y_min, x_max, y_max)
60
 
 
61
  return background_np, None
62
 
63
  else:
64
+ if hasattr(editor_data, 'size'):
 
65
  image_np = np.array(editor_data)
66
  return image_np, None
67
  else:
68
  return None, None
69
 
 
 
 
 
 
 
 
 
70
  def classify_cells_by_blueness(image_np, masks, blue_threshold):
71
  """
72
  Classify cells as dead (blue) or alive based on single blueness metric
 
79
  Returns:
80
  dead_count, alive_count, colored_overlay
81
  """
82
+ # Ensure image_np is RGB for consistency with HSV conversion
83
+ if len(image_np.shape) == 2:
84
+ image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
85
+ elif len(image_np.shape) == 3 and image_np.shape[2] == 4:
86
+ image_np = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
87
+
88
  # Convert RGB to HSV
89
  hsv = cv2.cvtColor(image_np, cv2.COLOR_RGB2HSV)
90
 
 
125
  alive_cells.append(cell_id)
126
 
127
  # Create colored overlay
128
+ overlay = image_np.copy().astype(np.float32) # Ensure float for blending
129
 
130
  # Color dead cells red, alive cells green
131
  for cell_id in dead_cells:
 
138
 
139
  # Blend with original image
140
  alpha = 0.4
141
+ final_overlay = (1 - alpha) * image_np.astype(np.float32) + alpha * overlay
142
+ final_overlay = np.clip(final_overlay, 0, 255).astype(np.uint8)
143
 
144
+ return len(dead_cells), len(alive_cells), final_overlay
145
 
146
  @spaces.GPU
147
+ def run_segmentation_editor(editor_data, model_choice):
148
  """
149
+ Runs cell segmentation using ImageEditor data.
150
+ Returns initial segmentation overlay, counts, and also masks/image for state.
151
  """
152
  try:
 
153
  model_filename = MODEL_OPTIONS[model_choice]
154
  model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
155
 
 
159
  model = models.CellposeModel(gpu=True, pretrained_model=model_path)
160
  loaded_models[model_filename] = model
161
 
 
162
  region_np, region_coords = extract_region_from_editor(editor_data)
163
 
164
  if region_np is None:
165
+ return 0, None, f"No image provided.", gr.update(visible=False), None, None
166
 
167
+ # Process image format to RGB
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  if len(region_np.shape) == 2:
169
+ processed_image_np = cv2.cvtColor(region_np, cv2.COLOR_GRAY2RGB)
170
  elif len(region_np.shape) == 3 and region_np.shape[2] == 4:
171
+ processed_image_np = cv2.cvtColor(region_np, cv2.COLOR_RGBA2RGB)
172
+ else:
173
+ processed_image_np = region_np
 
 
174
 
175
+ # Run Cellpose segmentation
176
+ masks, flows, styles = model.eval(processed_image_np, diameter=None, channels=[0, 0])
 
 
177
 
178
+ cell_count = len(np.unique(masks)) - 1
 
179
 
180
+ # Create a basic segmentation overlay (without viability)
181
+ segmentation_overlay = processed_image_np.copy().astype(np.float32)
182
+ if masks.max() > 0:
183
+ np.random.seed(42) # For consistent random colors
184
+ colors = np.random.randint(0, 255, size=(masks.max() + 1, 3))
185
+ colors[0] = [0, 0, 0] # Background color
186
+ colored_mask = colors[masks]
187
+ alpha = 0.4
188
+ segmentation_overlay = (1 - alpha) * segmentation_overlay + alpha * colored_mask
189
+ segmentation_overlay = np.clip(segmentation_overlay, 0, 255).astype(np.uint8)
190
 
191
+ info_msg = f"Segmentation complete! Found {cell_count} cells.\n"
 
192
  if region_coords:
193
+ info_msg += f"Processed region: {region_coords[0]},{region_coords[1]} to {region_coords[2]},{region_coords[3]}\n"
194
+ info_msg += f"Now adjust the Blue Threshold for viability assessment."
195
+
196
+ # Return initial segmentation display and state variables
197
+ return cell_count, Image.fromarray(segmentation_overlay), info_msg, gr.update(visible=True), masks, processed_image_np
198
+
199
  except Exception as e:
200
+ return 0, None, f"Error during segmentation: {str(e)}", gr.update(visible=False), None, None
201
 
202
  @spaces.GPU
203
+ def run_segmentation_manual(image, model_choice, crop_coords):
204
+ """
205
+ Runs cell segmentation using manual image input and coordinates.
206
+ Returns initial segmentation overlay, counts, and also masks/image for state.
207
+ """
208
  if image is None:
209
+ return 0, None, "No image provided", gr.update(visible=False), None, None
210
+
211
  try:
 
212
  model_filename = MODEL_OPTIONS[model_choice]
213
  model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
214
+
215
  if model_filename in loaded_models:
216
  model = loaded_models[model_filename]
217
  else:
218
  model = models.CellposeModel(gpu=True, pretrained_model=model_path)
219
  loaded_models[model_filename] = model
220
+
 
221
  image_np = np.array(image)
222
+
223
  # Apply crop if coordinates are provided
224
  if crop_coords and crop_coords.strip():
225
  try:
226
  coords = [int(x.strip()) for x in crop_coords.split(',')]
227
  if len(coords) == 4:
228
  x_min, y_min, x_max, y_max = coords
 
229
  h, w = image_np.shape[:2]
230
  x_min = max(0, min(x_min, w-1))
231
  y_min = max(0, min(y_min, h-1))
232
  x_max = max(x_min+1, min(x_max, w))
233
  y_max = max(y_min+1, min(y_max, h))
234
+
235
+ processed_image_np = image_np[y_min:y_max, x_min:x_max]
236
+ else:
237
+ processed_image_np = image_np # No valid crop, use original
238
  except ValueError:
239
+ processed_image_np = image_np # Invalid crop, use original
240
+ else:
241
+ processed_image_np = image_np # No crop coords, use original
242
 
243
+ # Process image format to RGB
244
+ if len(processed_image_np.shape) == 2:
245
+ processed_image_np = cv2.cvtColor(processed_image_np, cv2.COLOR_GRAY2RGB)
246
+ elif len(processed_image_np.shape) == 3 and processed_image_np.shape[2] == 4:
247
+ processed_image_np = cv2.cvtColor(processed_image_np, cv2.COLOR_RGBA2RGB)
248
 
249
  # Run Cellpose
250
+ masks, flows, styles = model.eval(processed_image_np, diameter=None, channels=[0, 0])
251
 
252
+ cell_count = len(np.unique(masks)) - 1
253
+
254
+ # Create a basic segmentation overlay
255
+ segmentation_overlay = processed_image_np.copy().astype(np.float32)
256
+ if masks.max() > 0:
257
+ np.random.seed(42)
258
+ colors = np.random.randint(0, 255, size=(masks.max() + 1, 3))
259
+ colors[0] = [0, 0, 0]
260
+ colored_mask = colors[masks]
261
+ alpha = 0.4
262
+ segmentation_overlay = (1 - alpha) * segmentation_overlay + alpha * colored_mask
263
+ segmentation_overlay = np.clip(segmentation_overlay, 0, 255).astype(np.uint8)
264
+
265
+ info_msg = f"Segmentation complete! Found {cell_count} cells.\n"
266
+ if crop_coords:
267
+ info_msg += f"Processed with coordinates: {crop_coords}\n"
268
+ info_msg += f"Now adjust the Blue Threshold for viability assessment."
269
+
270
+ return cell_count, Image.fromarray(segmentation_overlay), info_msg, gr.update(visible=True), masks, processed_image_np
271
+
272
+ except Exception as e:
273
+ return 0, None, f"Error during segmentation: {str(e)}", gr.update(visible=False), None, None
274
+
275
+ def update_viability_realtime(blue_threshold, stored_masks, stored_image_np):
276
+ """
277
+ Updates viability assessment in real-time based on blue threshold.
278
+ Takes stored masks and image_np from state.
279
+ """
280
+ if stored_masks is None or stored_image_np is None:
281
+ return None, 0, 0, 0.0, "Please run segmentation first."
282
+
283
+ try:
284
+ dead_count, alive_count, viability_overlay_np = classify_cells_by_blueness(
285
+ stored_image_np, stored_masks, blue_threshold
286
  )
287
 
288
+ total_count = alive_count + dead_count
289
  viability_percent = (alive_count / total_count * 100) if total_count > 0 else 0.0
290
 
291
+ overlay_image = Image.fromarray(viability_overlay_np)
292
+ info_msg = f"Total cells: {total_count}\nLive (green): {alive_count}\nDead (red): {dead_count}\n"
293
+ info_msg += f"Viability: {viability_percent:.1f}%\nBlue threshold: {blue_threshold}%"
 
 
 
 
294
 
295
+ return overlay_image, alive_count, dead_count, viability_percent, info_msg
296
+
297
  except Exception as e:
298
+ return None, 0, 0, 0.0, f"Error updating viability: {str(e)}"
299
 
300
+
301
+ # Create the Gradio interface
302
+ with gr.Blocks(title="Cell Viability Counter with Trypan Blue Analysis", theme=gr.themes.Soft()) as demo:
303
  gr.Markdown("# Cell Viability Counter with Trypan Blue Analysis")
304
  gr.Markdown("Upload a trypan blue stained microscopy image and analyze cell viability. Dead cells appear blue, alive cells are unstained.")
 
 
 
305
 
306
+ # Define State components to store masks and image data across function calls
307
+ masks_state = gr.State(value=None)
308
+ image_state = gr.State(value=None)
309
+
310
+ with gr.Tab("Image Editor (Draw Selection)"):
311
+ gr.Markdown("### Step 1: Draw selection and run segmentation")
312
+
313
  with gr.Row():
314
  with gr.Column():
315
  image_editor = gr.ImageEditor(
 
318
  brush=gr.Brush(colors=["#ff0000"], color_mode="fixed", default_size=20),
319
  eraser=gr.Eraser(default_size=20)
320
  )
 
321
  model_dropdown1 = gr.Dropdown(
322
  choices=list(MODEL_OPTIONS.keys()),
323
  label="Select Model",
324
  value="Hemocytometer Model"
325
  )
326
+ segment_btn1 = gr.Button("🔬 Run Segmentation", variant="primary", size="lg")
327
+
 
 
 
 
 
 
 
 
 
328
  with gr.Column():
329
+ cell_count_output1 = gr.Number(label="Total Cells Detected", precision=0)
330
+ overlay_output1 = gr.Image(type="pil", label="Segmentation Result")
331
+ info_output1 = gr.Textbox(label="Processing Info", lines=3)
 
332
 
333
+ # Viability Assessment Section
334
+ with gr.Group(visible=False) as viability_section1:
335
+ gr.Markdown("### Step 2: Real-time Viability Assessment (Trypan Blue)")
336
+ gr.Markdown("Adjust the threshold to classify cells as live (green) or dead (red).")
337
+
338
+ with gr.Row():
339
+ with gr.Column():
340
+ blue_threshold1 = gr.Slider(
341
+ minimum=0,
342
+ maximum=100,
343
+ value=25,
344
+ step=1,
345
+ label="Blue Threshold (%)",
346
+ info="Higher values = more selective for blue cells"
347
+ )
348
+
349
+ with gr.Column():
350
+ live_count_output1 = gr.Number(label="Live Cells (Green)", precision=0)
351
+ dead_count_output1 = gr.Number(label="Dead Cells (Red)", precision=0)
352
+
353
+ viability_overlay1 = gr.Image(type="pil", label="Viability Assessment (Green=Live, Red=Dead)")
354
+ viability_percent_output1 = gr.Number(label="Viability (%)", precision=1)
355
+ viability_info1 = gr.Textbox(label="Viability Statistics", lines=4)
356
+
357
+ # Event handlers
358
+ # segment_cells now returns masks and image_np which are stored in masks_state and image_state
359
  segment_btn1.click(
360
+ fn=run_segmentation_editor,
361
+ inputs=[image_editor, model_dropdown1],
362
+ outputs=[cell_count_output1, overlay_output1, info_output1, viability_section1, masks_state, image_state]
363
+ ).then( # Chain the initial viability assessment after segmentation
364
+ fn=update_viability_realtime,
365
+ inputs=[blue_threshold1, masks_state, image_state], # Pass stored state as inputs
366
+ outputs=[viability_overlay1, live_count_output1, dead_count_output1, viability_percent_output1, viability_info1]
367
  )
368
+
369
+ # Slider changes update viability in real-time
370
+ blue_threshold1.change(
371
+ fn=update_viability_realtime,
372
+ inputs=[blue_threshold1, masks_state, image_state],
373
+ outputs=[viability_overlay1, live_count_output1, dead_count_output1, viability_percent_output1, viability_info1]
374
+ )
375
+
376
  with gr.Tab("Manual Coordinates"):
377
+ gr.Markdown("### Step 1: Upload image and run segmentation")
378
+
379
  with gr.Row():
380
  with gr.Column():
381
+ image_input = gr.Image(type="pil", label="Microscopy Image")
 
382
  model_dropdown2 = gr.Dropdown(
383
  choices=list(MODEL_OPTIONS.keys()),
384
  label="Select Model",
385
  value="Hemocytometer Model"
386
  )
 
387
  coord_input = gr.Textbox(
388
  label="Crop Coordinates (optional)",
389
  placeholder="e.g., 100,100,400,400",
390
  info="Format: x_min,y_min,x_max,y_max"
391
  )
392
+ segment_btn2 = gr.Button("🔬 Run Segmentation", variant="primary", size="lg")
393
+
 
 
 
 
 
 
 
 
 
394
  with gr.Column():
395
+ cell_count_output2 = gr.Number(label="Total Cells Detected", precision=0)
396
+ overlay_output2 = gr.Image(type="pil", label="Segmentation Result")
397
+ info_output2 = gr.Textbox(label="Processing Info", lines=3)
 
398
 
399
+ # Viability Assessment Section
400
+ with gr.Group(visible=False) as viability_section2:
401
+ gr.Markdown("### Step 2: Real-time Viability Assessment (Trypan Blue)")
402
+ gr.Markdown("Adjust the threshold to classify cells as live (green) or dead (red).")
403
+
404
+ with gr.Row():
405
+ with gr.Column():
406
+ blue_threshold2 = gr.Slider(
407
+ minimum=0,
408
+ maximum=100,
409
+ value=25,
410
+ step=1,
411
+ label="Blue Threshold (%)",
412
+ info="Higher values = more selective for blue cells"
413
+ )
414
+
415
+ with gr.Column():
416
+ live_count_output2 = gr.Number(label="Live Cells (Green)", precision=0)
417
+ dead_count_output2 = gr.Number(label="Dead Cells (Red)", precision=0)
418
+
419
+ viability_overlay2 = gr.Image(type="pil", label="Viability Assessment (Green=Live, Red=Dead)")
420
+ viability_percent_output2 = gr.Number(label="Viability (%)", precision=1)
421
+ viability_info2 = gr.Textbox(label="Viability Statistics", lines=4)
422
+
423
+ # Event handlers
424
  segment_btn2.click(
425
+ fn=run_segmentation_manual,
426
+ inputs=[image_input, model_dropdown2, coord_input],
427
+ outputs=[cell_count_output2, overlay_output2, info_output2, viability_section2, masks_state, image_state]
428
+ ).then( # Chain the initial viability assessment after segmentation
429
+ fn=update_viability_realtime,
430
+ inputs=[blue_threshold2, masks_state, image_state],
431
+ outputs=[viability_overlay2, live_count_output2, dead_count_output2, viability_percent_output2, viability_info2]
432
  )
433
+
434
+ # Slider changes update viability in real-time
435
+ blue_threshold2.change(
436
+ fn=update_viability_realtime,
437
+ inputs=[blue_threshold2, masks_state, image_state],
438
+ outputs=[viability_overlay2, live_count_output2, dead_count_output2, viability_percent_output2, viability_info2]
439
+ )
440
+
441
+ # Instructions
442
+ with gr.Accordion("Instructions", open=False):
443
  gr.Markdown("""
444
+ ### How to use:
445
+
446
+ 1. **Upload and Segment**:
447
+ - Upload your microscopy image.
448
+ - Select a Cellpose model (e.g., "Hemocytometer Model" for blood cells).
449
+ - Draw a selection region using the Image Editor, or specify coordinates manually.
450
+ - Click "Run Segmentation".
451
+
452
+ 2. **Real-time Viability Assessment (Trypan Blue)**:
453
+ - After segmentation, the viability section will become visible.
454
+ - This tool is specifically designed for **Trypan Blue stained images**, where dead cells appear blue.
455
+ - Adjust the **"Blue Threshold (%)"** slider in real-time. As you change it, the green (live) and red (dead) classification on the overlay will update.
456
+ - **Lower values (e.g., 10-20%)** are more sensitive and will classify more cells as blue/dead.
457
+ - **Higher values (e.g., 30-50%)** are more selective and will only classify strongly blue cells as dead.
458
+ - Green cells = Live, Red cells = Dead.
459
+
460
+ 3. **Interpreting Results**:
461
+ - The app calculates and displays the total, live, and dead cell counts, along with the viability percentage.
 
 
 
 
 
 
 
 
 
462
  """)
463
 
464
  if __name__ == "__main__":