LiangLabUMB commited on
Commit
1320040
·
verified ·
1 Parent(s): e43377b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +583 -0
app.py ADDED
@@ -0,0 +1,583 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import spaces
3
+ from cellpose import models
4
+ import numpy as np
5
+ import cv2
6
+ import matplotlib.pyplot as plt
7
+ import tempfile
8
+ from PIL import Image
9
+ import io
10
+ from huggingface_hub import hf_hub_download
11
+
12
+ HF_REPO_ID = "myang4218/cellposemodel"
13
+ MODEL_OPTIONS = {
14
+ "Hemocytometer Model": "hemocytometermodel.npy",
15
+ "General Model": "generalmodel.npy"
16
+ }
17
+ loaded_models = {}
18
+
19
+ # Global variable for region coordinates (can remain global if only for info display)
20
+ current_region_coords = None
21
+
22
+ """
23
+ # Commented out debugging function
24
+ def debug_editor_data(editor_data):
25
+ \"\"\"Debug function to understand the structure of ImageEditor data\"\"\"
26
+ if editor_data is None:
27
+ return "No data provided"
28
+ info = f"Type: {type(editor_data)}\n"
29
+ if isinstance(editor_data, dict):
30
+ info += f"Keys: {list(editor_data.keys())}\n"
31
+ for key, value in editor_data.items():
32
+ info += f" {key}: {type(value)}\n"
33
+ if key == 'layers' and value:
34
+ info += f" Layers count: {len(value)}\n"
35
+ for i, layer in enumerate(value):
36
+ info += f" Layer {i}: {type(layer)}\n"
37
+ if hasattr(layer, 'size'):
38
+ info += f" Size: {layer.size}\n"
39
+ info += f" Mode: {layer.mode}\n"
40
+ else: # Direct PIL Image
41
+ if hasattr(editor_data, 'size'):
42
+ info += f"Size: {editor_data.size}\n"
43
+ info += f"Mode: {editor_data.mode}\n"
44
+ return info
45
+ """
46
+
47
+ def extract_region_from_editor(editor_data):
48
+ """Extract the selected region from ImageEditor data"""
49
+ if editor_data is None:
50
+ # print("DEBUG: extract_region_from_editor - editor_data is None")
51
+ return None, None
52
+
53
+ # print(f"DEBUG: extract_region_from_editor - editor_data type: {type(editor_data)}")
54
+
55
+ if isinstance(editor_data, dict):
56
+ background = editor_data.get('background')
57
+ layers = editor_data.get('layers', [])
58
+
59
+ if background is None:
60
+ # print("DEBUG: extract_region_from_editor - background is None")
61
+ return None, None
62
+
63
+ background_np = np.array(background)
64
+ # print(f"DEBUG: extract_region_from_editor - background_np shape: {background_np.shape}")
65
+
66
+ if layers and len(layers) > 0:
67
+ selection_layer = layers[0]
68
+ selection_np = np.array(selection_layer)
69
+ # print(f"DEBUG: extract_region_from_editor - selection_np shape: {selection_np.shape}")
70
+
71
+ if len(selection_np.shape) == 3:
72
+ if selection_np.shape[2] == 4: # RGBA
73
+ mask = selection_np[:, :, 3] > 0
74
+ else: # RGB
75
+ mask = np.any(selection_np > 0, axis=2)
76
+ else:
77
+ mask = selection_np > 0
78
+
79
+ coords = np.where(mask)
80
+ if len(coords[0]) > 0:
81
+ y_min, y_max = coords[0].min(), coords[0].max()
82
+ x_min, x_max = coords[1].min(), coords[1].max()
83
+
84
+ pad = 5
85
+ h, w = background_np.shape[:2]
86
+ y_min = max(0, y_min - pad)
87
+ y_max = min(h, y_max + pad)
88
+ x_min = max(0, x_min - pad)
89
+ x_max = min(w, x_max + pad)
90
+
91
+ region = background_np[y_min:y_max+1, x_min:x_max+1]
92
+ # print(f"DEBUG: extract_region_from_editor - Extracted region shape: {region.shape}")
93
+ return region, (x_min, y_min, x_max, y_max)
94
+ # print("DEBUG: extract_region_from_editor - No selection or empty selection, returning full background.")
95
+ return background_np, None
96
+ else: # Assuming it's a direct PIL Image if not dict
97
+ if hasattr(editor_data, 'size'):
98
+ image_np = np.array(editor_data)
99
+ # print(f"DEBUG: extract_region_from_editor - Direct PIL image, shape: {image_np.shape}")
100
+ return image_np, None
101
+ else:
102
+ # print("DEBUG: extract_region_from_editor - editor_data is not dict and no size attribute, returning None.")
103
+ return None, None
104
+
105
+ def calculate_blue_intensity(image_np, masks):
106
+ """Calculate blue intensity for each cell"""
107
+ # Ensure image is in correct format
108
+ if len(image_np.shape) == 2:
109
+ # Convert grayscale to RGB
110
+ image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
111
+ elif len(image_np.shape) == 3 and image_np.shape[2] == 4:
112
+ # Handle RGBA
113
+ image_np = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
114
+
115
+ # Calculate blue intensity for each cell
116
+ cell_blue_intensities = {}
117
+ unique_cells = np.unique(masks)
118
+
119
+ # print(f"DEBUG: Found {len(unique_cells)} unique mask values: {unique_cells}")
120
+ # print(f"DEBUG: Image shape: {image_np.shape}, dtype: {image_np.dtype}")
121
+ # print(f"DEBUG: Masks shape: {masks.shape}, dtype: {masks.dtype}")
122
+
123
+ for cell_id in unique_cells:
124
+ if cell_id == 0: # Skip background
125
+ continue
126
+
127
+ cell_mask = masks == cell_id
128
+ cell_pixels = image_np[cell_mask]
129
+
130
+ if len(cell_pixels) > 0:
131
+ # Calculate mean blue intensity (blue channel is index 2)
132
+ blue_intensity = np.mean(cell_pixels[:, 2]) # Blue channel
133
+ cell_blue_intensities[cell_id] = blue_intensity
134
+ # print(f"DEBUG: Cell {cell_id}: {len(cell_pixels)} pixels, blue intensity: {blue_intensity:.2f}")
135
+ # print(f"DEBUG: Total cells with blue intensity: {len(cell_blue_intensities)}")
136
+ return cell_blue_intensities
137
+
138
+ def create_viability_overlay(image_np, masks, blue_threshold):
139
+ """Create overlay showing live/dead cells based on blue threshold"""
140
+
141
+ if masks is None or image_np is None:
142
+ # print("DEBUG: create_viability_overlay - masks or image_np is None, returning None.")
143
+ return None, 0, 0
144
+
145
+ # print(f"DEBUG: create_viability_overlay called with threshold: {blue_threshold}")
146
+ # print(f"DEBUG: Image shape: {image_np.shape}, Masks shape: {masks.shape}, Masks max: {masks.max()}")
147
+
148
+ # Ensure image is RGB
149
+ if len(image_np.shape) == 2:
150
+ image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
151
+ elif len(image_np.shape) == 3 and image_np.shape[2] == 4:
152
+ image_np = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
153
+
154
+ # Calculate blue intensities
155
+ cell_blue_intensities = calculate_blue_intensity(image_np, masks)
156
+
157
+ # print(f"DEBUG: Got {len(cell_blue_intensities)} cells with blue intensities")
158
+
159
+ # Create overlay - start with original image
160
+ overlay = image_np.copy().astype(np.float32)
161
+
162
+ live_count = 0
163
+ dead_count = 0
164
+
165
+ # Color cells based on viability
166
+ for cell_id, blue_intensity in cell_blue_intensities.items():
167
+ cell_mask = masks == cell_id
168
+
169
+ # print(f"DEBUG: Processing cell {cell_id}, blue intensity: {blue_intensity:.2f}, threshold: {blue_threshold}")
170
+
171
+ if blue_intensity > blue_threshold:
172
+ # Dead cell - color in red
173
+ overlay[cell_mask] = [255, 0, 0] # Red
174
+ dead_count += 1
175
+ # print(f"DEBUG: Cell {cell_id} classified as DEAD")
176
+ else:
177
+ # Live cell - color in green
178
+ overlay[cell_mask] = [0, 255, 0] # Green
179
+ live_count += 1
180
+ # print(f"DEBUG: Cell {cell_id} classified as LIVE")
181
+ # print(f"DEBUG: Final counts - Live: {live_count}, Dead: {dead_count}")
182
+
183
+ # Blend with original image
184
+ alpha = 0.6 # Increased alpha for better visibility
185
+ final_overlay = (1 - alpha) * image_np.astype(np.float32) + alpha * overlay
186
+ final_overlay = np.clip(final_overlay, 0, 255).astype(np.uint8)
187
+
188
+ return Image.fromarray(final_overlay), live_count, dead_count
189
+
190
+ @spaces.GPU
191
+ def segment_cells(editor_data, model_choice): # crop_coords removed from here, handled within extract_region_from_editor implicitly by ImageEditor
192
+ """
193
+ First stage: Segment cells and return results for viability assessment
194
+ """
195
+ global current_region_coords # Keep global for info display if needed later
196
+
197
+ try:
198
+ # print("DEBUG: --- Starting segment_cells ---")
199
+ # debug_info = debug_editor_data(editor_data) # Commented out debug function call
200
+ # print(f"DEBUG: Editor data info:\n{debug_info}")
201
+
202
+ # Load the model
203
+ model_filename = MODEL_OPTIONS[model_choice]
204
+ model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
205
+
206
+ if model_filename in loaded_models:
207
+ model = loaded_models[model_filename]
208
+ else:
209
+ model = models.CellposeModel(gpu=True, pretrained_model=model_path)
210
+ loaded_models[model_filename] = model
211
+
212
+ # Extract region from editor
213
+ region_np, region_coords = extract_region_from_editor(editor_data)
214
+
215
+ if region_np is None:
216
+ # print("DEBUG: region_np is None, returning error and hiding viability section.")
217
+ current_region_coords = None
218
+ # Also return None for masks and image for the gr.State components
219
+ return 0, None, f"No image provided.", gr.update(visible=False), None, None # Removed debug_info
220
+
221
+ # print(f"DEBUG: Image to segment shape (after extraction/cropping): {region_np.shape}, dtype: {region_np.dtype}")
222
+
223
+ # Process image format
224
+ if len(region_np.shape) == 2:
225
+ region_np_processed = cv2.cvtColor(region_np, cv2.COLOR_GRAY2RGB)
226
+ elif len(region_np.shape) == 3 and region_np.shape[2] == 4:
227
+ region_np_processed = cv2.cvtColor(region_np, cv2.COLOR_RGBA2RGB)
228
+ else:
229
+ region_np_processed = region_np # Already RGB or suitable
230
+
231
+ # print(f"DEBUG: Image before model.eval shape: {region_np_processed.shape}, dtype: {region_np_processed.dtype}")
232
+
233
+ # Run Cellpose segmentation
234
+ masks, flows, styles = model.eval(region_np_processed, diameter=None, channels=[0, 0])
235
+ # print(f"DEBUG: After model.eval: masks type: {type(masks)}, shape: {masks.shape if hasattr(masks, 'shape') else 'N/A'}")
236
+
237
+ # Update global region coords for info display
238
+ current_region_coords = region_coords
239
+
240
+ # Count unique cells
241
+ cell_count = len(np.unique(masks)) - 1
242
+ # print(f"DEBUG: Cell count: {cell_count}")
243
+
244
+ # Create basic segmentation overlay
245
+ overlay = region_np_processed.copy().astype(np.float32)
246
+
247
+ if masks.max() > 0:
248
+ np.random.seed(42)
249
+ colors = np.random.randint(0, 255, size=(masks.max() + 1, 3))
250
+ colors[0] = [0, 0, 0] # Background stays black
251
+
252
+ colored_mask = colors[masks]
253
+ alpha = 0.4
254
+ overlay = (1 - alpha) * overlay + alpha * colored_mask
255
+
256
+ overlay = np.clip(overlay, 0, 255).astype(np.uint8)
257
+ overlay_image = Image.fromarray(overlay)
258
+
259
+ info_msg = f"Segmentation complete! Found {cell_count} cells.\n"
260
+ if region_coords:
261
+ info_msg += f"Region: {region_coords}\n"
262
+ info_msg += f"Now use the blue threshold slider below for viability assessment."
263
+
264
+ # Make viability section visible
265
+ viability_section_update = gr.update(visible=True)
266
+ # print("DEBUG: segment_cells returning successfully, viability section visible.")
267
+
268
+ # Return masks and original processed image for state components
269
+ return cell_count, overlay_image, info_msg, viability_section_update, masks, region_np_processed
270
+
271
+ except Exception as e:
272
+ # print(f"ERROR: An exception occurred in segment_cells: {e}")
273
+ current_region_coords = None
274
+ # Return None for masks and image in case of error
275
+ return 0, None, f"Error: {str(e)}", gr.update(visible=False), None, None
276
+
277
+ def update_viability_assessment(blue_threshold, stored_masks, stored_image):
278
+ """
279
+ Real-time viability assessment based on blue threshold
280
+ """
281
+
282
+ # print("DEBUG: --- Starting update_viability_assessment ---")
283
+ # print(f"DEBUG: At start of update_viability_assessment: stored_masks is None? {stored_masks is None}, stored_image is None? {stored_image is None}")
284
+
285
+ if stored_masks is None or stored_image is None:
286
+ # print("DEBUG: Passed-in stored_masks or stored_image is None, returning 'Please run segmentation first'.")
287
+ return None, 0, 0, "Please run segmentation first"
288
+
289
+ try:
290
+ # Debug information (kept for now, but can be removed if not needed for user info)
291
+ unique_cells = np.unique(stored_masks)
292
+ total_cells = len(unique_cells) - 1 # Subtract background
293
+
294
+ debug_info = f"Debug - Total cells found: {total_cells}\n"
295
+ debug_info += f"Image shape: {stored_image.shape}\n"
296
+ debug_info += f"Masks shape: {stored_masks.shape}\n"
297
+ debug_info += f"Unique mask values (first 10): {unique_cells[:10]}...\n"
298
+
299
+ # Create viability overlay
300
+ viability_overlay, live_count, dead_count = create_viability_overlay(
301
+ stored_image, stored_masks, blue_threshold
302
+ )
303
+
304
+ total_count = live_count + dead_count
305
+ viability_info = f"Blue threshold: {blue_threshold}\n"
306
+ viability_info += f"Total cells: {total_count}\nLive (green): {live_count}\nDead (red): {dead_count}\n"
307
+
308
+ if total_count > 0:
309
+ viability_percentage = (live_count / total_count) * 100
310
+ viability_info += f"Viability: {viability_percentage:.1f}%\n"
311
+
312
+ # viability_info += f"\n{debug_info}" # Removed this line to hide debug info from UI
313
+ # print("DEBUG: update_viability_assessment returning successfully.")
314
+
315
+ return viability_overlay, live_count, dead_count, viability_info
316
+
317
+ except Exception as e:
318
+ # print(f"ERROR: An exception occurred in update_viability_assessment: {e}")
319
+ return None, 0, 0, f"Error in viability assessment: {str(e)}\nMasks available: {stored_masks is not None}\nImage available: {stored_image is not None}"
320
+
321
+ @spaces.GPU
322
+ def segment_with_coords(image, model_choice, crop_coords):
323
+ """Alternative function for manual coordinates input"""
324
+ global current_region_coords
325
+
326
+ # print("DEBUG: --- Starting segment_with_coords ---")
327
+ if image is None:
328
+ # print("DEBUG: No image provided for segment_with_coords, returning error.")
329
+ current_region_coords = None
330
+ return 0, None, "No image provided", gr.update(visible=False), None, None
331
+
332
+ try:
333
+ # Load the model
334
+ model_filename = MODEL_OPTIONS[model_choice]
335
+ model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
336
+
337
+ if model_filename in loaded_models:
338
+ model = loaded_models[model_filename]
339
+ else:
340
+ model = models.CellposeModel(gpu=True, pretrained_model=model_path)
341
+ loaded_models[model_filename] = model
342
+
343
+ # Convert PIL Image to numpy array
344
+ image_np = np.array(image)
345
+ # print(f"DEBUG: image_np from PIL: shape={image_np.shape}, dtype={image_np.dtype}")
346
+
347
+ # Apply crop if coordinates are provided
348
+ if crop_coords and crop_coords.strip():
349
+ try:
350
+ coords = [int(x.strip()) for x in crop_coords.split(',')]
351
+ if len(coords) == 4:
352
+ x_min, y_min, x_max, y_max = coords
353
+ h, w = image_np.shape[:2]
354
+ x_min = max(0, min(x_min, w-1))
355
+ y_min = max(0, min(y_min, h-1))
356
+ x_max = max(x_min+1, min(x_max, w))
357
+ y_max = max(y_min+1, min(y_max, h))
358
+
359
+ image_np_processed = image_np[y_min:y_max, x_min:x_max]
360
+ current_region_coords = (x_min, y_min, x_max, y_max)
361
+ # print(f"DEBUG: Applied manual crop in segment_with_coords: {current_region_coords}, new image_np_processed shape: {image_np_processed.shape}")
362
+ else:
363
+ image_np_processed = image_np # No valid crop, use original
364
+ except ValueError:
365
+ # print(f"DEBUG: Invalid crop coordinates format in segment_with_coords: {crop_coords}")
366
+ image_np_processed = image_np # Invalid crop, use original
367
+ else:
368
+ image_np_processed = image_np # No crop coords, use original
369
+
370
+ # Process image format
371
+ if len(image_np_processed.shape) == 2:
372
+ image_np_final = cv2.cvtColor(image_np_processed, cv2.COLOR_GRAY2RGB)
373
+ elif len(image_np_processed.shape) == 3 and image_np_processed.shape[2] == 4:
374
+ image_np_final = cv2.cvtColor(image_np_processed, cv2.COLOR_RGBA2RGB)
375
+ else:
376
+ image_np_final = image_np_processed
377
+
378
+ # print(f"DEBUG: Image before model.eval (segment_with_coords) shape: {image_np_final.shape}, dtype: {image_np_final.dtype}")
379
+ # Run Cellpose
380
+ masks, flows, styles = model.eval(image_np_final, diameter=None, channels=[0, 0])
381
+ # print(f"DEBUG: After model.eval (segment_with_coords): masks type: {type(masks)}, shape: {masks.shape if hasattr(masks, 'shape') else 'N/A'}")
382
+
383
+ # Count unique cells
384
+ cell_count = len(np.unique(masks)) - 1
385
+
386
+ # Create overlay
387
+ overlay = image_np_final.copy().astype(np.float32)
388
+
389
+ if masks.max() > 0:
390
+ np.random.seed(42)
391
+ colors = np.random.randint(0, 255, size=(masks.max() + 1, 3))
392
+ colors[0] = [0, 0, 0]
393
+ colored_mask = colors[masks]
394
+ alpha = 0.4
395
+ overlay = (1 - alpha) * overlay + alpha * colored_mask
396
+
397
+ overlay = np.clip(overlay, 0, 255).astype(np.uint8)
398
+ overlay_image = Image.fromarray(overlay)
399
+
400
+ info_msg = f"Segmentation complete! Found {cell_count} cells.\n"
401
+ if current_region_coords: # Use global if set by crop
402
+ info_msg += f"Region: {current_region_coords}\n"
403
+ info_msg += f"Now use the blue threshold slider below for viability assessment."
404
+ # print("DEBUG: segment_with_coords returning successfully, viability section visible.")
405
+
406
+ return cell_count, overlay_image, info_msg, gr.update(visible=True), masks, image_np_final
407
+
408
+ except Exception as e:
409
+ # print(f"ERROR: An exception occurred in segment_with_coords: {e}")
410
+ current_region_coords = None
411
+ return 0, None, f"Error: {str(e)}", gr.update(visible=False), None, None
412
+
413
+ # Create the Gradio interface
414
+ with gr.Blocks(title="Cell Counter with Real-time Viability Assessment", theme=gr.themes.Soft()) as demo:
415
+ gr.Markdown("# Cell Counter with Real-time Viability Assessment")
416
+ gr.Markdown("Upload a microscopy image, run segmentation, then use the blue threshold slider for real-time viability assessment.")
417
+
418
+ # Define State components to store masks and image data across function calls
419
+ # These are hidden from the UI but persist data per session.
420
+ masks_state = gr.State(value=None)
421
+ image_state = gr.State(value=None)
422
+
423
+ with gr.Tab("Image Editor (Draw Selection)"):
424
+ gr.Markdown("### Step 1: Draw selection and run segmentation")
425
+
426
+ with gr.Row():
427
+ with gr.Column():
428
+ image_editor = gr.ImageEditor(
429
+ label="Draw selection on image",
430
+ type="pil",
431
+ brush=gr.Brush(colors=["#ff0000"], color_mode="fixed", default_size=20),
432
+ eraser=gr.Eraser(default_size=20)
433
+ )
434
+ model_dropdown1 = gr.Dropdown(
435
+ choices=list(MODEL_OPTIONS.keys()),
436
+ label="Select Model",
437
+ value="Hemocytometer Model"
438
+ )
439
+ segment_btn1 = gr.Button("🔬 Run Segmentation", variant="primary", size="lg")
440
+
441
+ with gr.Column():
442
+ cell_count_output1 = gr.Number(label="Total Cells Detected", precision=0)
443
+ overlay_output1 = gr.Image(type="pil", label="Segmentation Result")
444
+ info_output1 = gr.Textbox(label="Processing Info", lines=3)
445
+
446
+ # Viability Assessment Section
447
+ with gr.Group(visible=False) as viability_section1:
448
+ gr.Markdown("### Step 2: Real-time Viability Assessment")
449
+ gr.Markdown("Adjust the blue threshold to classify cells as live (green) or dead (red). Higher values = more blue = more dead.")
450
+
451
+ with gr.Row():
452
+ with gr.Column():
453
+ blue_threshold_slider1 = gr.Slider(
454
+ minimum=0,
455
+ maximum=255,
456
+ value=100,
457
+ step=1,
458
+ label="Blue Threshold",
459
+ info="Higher values = more cells classified as dead"
460
+ )
461
+
462
+ with gr.Column():
463
+ live_count_output1 = gr.Number(label="Live Cells (Green)", precision=0)
464
+ dead_count_output1 = gr.Number(label="Dead Cells (Red)", precision=0)
465
+
466
+ viability_overlay1 = gr.Image(type="pil", label="Viability Assessment (Green=Live, Red=Dead)")
467
+ viability_info1 = gr.Textbox(label="Viability Statistics", lines=4)
468
+
469
+ # Event handlers
470
+ # segment_cells now returns masks and image_np which are stored in masks_state and image_state
471
+ segment_btn1.click(
472
+ fn=segment_cells,
473
+ inputs=[image_editor, model_dropdown1],
474
+ outputs=[cell_count_output1, overlay_output1, info_output1, viability_section1, masks_state, image_state]
475
+ ).then( # Chain the viability assessment after segmentation
476
+ fn=update_viability_assessment,
477
+ inputs=[blue_threshold_slider1, masks_state, image_state], # Pass stored state as inputs
478
+ outputs=[viability_overlay1, live_count_output1, dead_count_output1, viability_info1]
479
+ )
480
+
481
+ # blue_threshold_slider changes also need the stored state
482
+ blue_threshold_slider1.change(
483
+ fn=update_viability_assessment,
484
+ inputs=[blue_threshold_slider1, masks_state, image_state], # Pass stored state as inputs
485
+ outputs=[viability_overlay1, live_count_output1, dead_count_output1, viability_info1]
486
+ )
487
+
488
+ with gr.Tab("Manual Coordinates"):
489
+ gr.Markdown("### Step 1: Upload image and run segmentation")
490
+
491
+ with gr.Row():
492
+ with gr.Column():
493
+ image_input = gr.Image(type="pil", label="Microscopy Image")
494
+ model_dropdown2 = gr.Dropdown(
495
+ choices=list(MODEL_OPTIONS.keys()),
496
+ label="Select Model",
497
+ value="Hemocytometer Model"
498
+ )
499
+ coord_input = gr.Textbox(
500
+ label="Crop Coordinates (optional)",
501
+ placeholder="e.g., 100,100,400,400",
502
+ info="Format: x_min,y_min,x_max,y_max"
503
+ )
504
+ segment_btn2 = gr.Button("🔬 Run Segmentation", variant="primary", size="lg")
505
+
506
+ with gr.Column():
507
+ cell_count_output2 = gr.Number(label="Total Cells Detected", precision=0)
508
+ overlay_output2 = gr.Image(type="pil", label="Segmentation Result")
509
+ info_output2 = gr.Textbox(label="Processing Info", lines=3)
510
+
511
+ # Viability Assessment Section
512
+ with gr.Group(visible=False) as viability_section2:
513
+ gr.Markdown("### Step 2: Real-time Viability Assessment")
514
+ gr.Markdown("Adjust the blue threshold to classify cells as live (green) or dead (red). Higher values = more blue = more dead.")
515
+
516
+ with gr.Row():
517
+ with gr.Column():
518
+ blue_threshold_slider2 = gr.Slider(
519
+ minimum=0,
520
+ maximum=255,
521
+ value=100,
522
+ step=1,
523
+ label="Blue Threshold",
524
+ info="Higher values = more cells classified as dead"
525
+ )
526
+
527
+ with gr.Column():
528
+ live_count_output2 = gr.Number(label="Live Cells (Green)", precision=0)
529
+ dead_count_output2 = gr.Number(label="Dead Cells (Red)", precision=0)
530
+
531
+ viability_overlay2 = gr.Image(type="pil", label="Viability Assessment (Green=Live, Red=Dead)")
532
+ viability_info2 = gr.Textbox(label="Viability Statistics", lines=4)
533
+
534
+ # Event handlers
535
+ # segment_with_coords now returns masks and image_np which are stored in masks_state and image_state
536
+ segment_btn2.click(
537
+ fn=segment_with_coords,
538
+ inputs=[image_input, model_dropdown2, coord_input],
539
+ outputs=[cell_count_output2, overlay_output2, info_output2, viability_section2, masks_state, image_state]
540
+ ).then( # Chain the viability assessment after segmentation
541
+ fn=update_viability_assessment,
542
+ inputs=[blue_threshold_slider2, masks_state, image_state], # Pass stored state as inputs
543
+ outputs=[viability_overlay2, live_count_output2, dead_count_output2, viability_info2]
544
+ )
545
+
546
+ # blue_threshold_slider changes also need the stored state
547
+ blue_threshold_slider2.change(
548
+ fn=update_viability_assessment,
549
+ inputs=[blue_threshold_slider2, masks_state, image_state], # Pass stored state as inputs
550
+ outputs=[viability_overlay2, live_count_output2, dead_count_output2, viability_info2]
551
+ )
552
+
553
+ # Instructions
554
+ with gr.Accordion("Instructions", open=False):
555
+ gr.Markdown("""
556
+ ### How to use:
557
+
558
+ 1. **Upload and Segment**:
559
+ - Upload your microscopy image
560
+ - Select a model (Hemocytometer Model for blood cells)
561
+ - Draw a selection region or specify coordinates
562
+ - Click "Run Segmentation"
563
+
564
+ 2. **Viability Assessment**:
565
+ - After segmentation, the viability section will appear
566
+ - Use the blue threshold slider to classify cells in real-time
567
+ - Green cells = Live (low blue intensity)
568
+ - Red cells = Dead (high blue intensity)
569
+ - Adjust the threshold until the classification looks correct
570
+
571
+ 3. **Interpreting Results**:
572
+ - The app calculates viability percentage automatically
573
+ - Live cells appear green, dead cells appear red
574
+ - Statistics show total counts and viability percentage
575
+
576
+ ### Tips:
577
+ - Start with a threshold around 100 and adjust based on your staining
578
+ - Dead cells typically have higher blue intensity due to trypan blue uptake
579
+ - The threshold is applied in real-time without re-running segmentation
580
+ """)
581
+
582
+ if __name__ == "__main__":
583
+ demo.launch()