LiangLabUMB commited on
Commit
19e1cd7
·
verified ·
1 Parent(s): 904db41

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +458 -0
app.py ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
18
+ loaded_models = {}
19
+
20
+ def debug_editor_data(editor_data):
21
+ """Debug function to understand the structure of ImageEditor data"""
22
+ if editor_data is None:
23
+ return "No data provided"
24
+
25
+ info = f"Type: {type(editor_data)}\n"
26
+
27
+ if isinstance(editor_data, dict):
28
+ info += f"Keys: {list(editor_data.keys())}\n"
29
+ for key, value in editor_data.items():
30
+ info += f" {key}: {type(value)}\n"
31
+ if key == 'layers' and value:
32
+ info += f" Layers count: {len(value)}\n"
33
+ for i, layer in enumerate(value):
34
+ info += f" Layer {i}: {type(layer)}\n"
35
+ if hasattr(layer, 'size'):
36
+ info += f" Size: {layer.size}\n"
37
+ info += f" Mode: {layer.mode}\n"
38
+ else:
39
+ if hasattr(editor_data, 'size'):
40
+ info += f"Size: {editor_data.size}\n"
41
+ info += f"Mode: {editor_data.mode}\n"
42
+
43
+ return info
44
+
45
+ def extract_region_from_editor(editor_data):
46
+ """Extract the selected region from ImageEditor data"""
47
+ if editor_data is None:
48
+ return None, None
49
+
50
+ # ImageEditor can return different formats depending on version
51
+ # Let's handle the most common cases
52
+
53
+ if isinstance(editor_data, dict):
54
+ # Case 1: Dictionary with 'background' and 'layers'
55
+ background = editor_data.get('background')
56
+ layers = editor_data.get('layers', [])
57
+
58
+ if background is None:
59
+ return None, None
60
+
61
+ # Convert background to numpy array
62
+ background_np = np.array(background)
63
+
64
+ # If there are drawn layers, try to extract selection
65
+ if layers and len(layers) > 0:
66
+ # Get the first layer
67
+ selection_layer = layers[0]
68
+
69
+ # Convert to numpy array
70
+ selection_np = np.array(selection_layer)
71
+
72
+ # Find non-transparent/non-black pixels as selection
73
+ if len(selection_np.shape) == 3:
74
+ # For RGB, look for non-black pixels
75
+ if selection_np.shape[2] == 4: # RGBA
76
+ # Use alpha channel
77
+ mask = selection_np[:, :, 3] > 0
78
+ else: # RGB
79
+ # Use non-black pixels
80
+ mask = np.any(selection_np > 0, axis=2)
81
+ else:
82
+ # Grayscale
83
+ mask = selection_np > 0
84
+
85
+ # Find bounding box of the selection
86
+ coords = np.where(mask)
87
+ if len(coords[0]) > 0:
88
+ y_min, y_max = coords[0].min(), coords[0].max()
89
+ x_min, x_max = coords[1].min(), coords[1].max()
90
+
91
+ # Add some padding to ensure we don't get tiny regions
92
+ pad = 5
93
+ h, w = background_np.shape[:2]
94
+ y_min = max(0, y_min - pad)
95
+ y_max = min(h, y_max + pad)
96
+ x_min = max(0, x_min - pad)
97
+ x_max = min(w, x_max + pad)
98
+
99
+ # Extract the region
100
+ region = background_np[y_min:y_max+1, x_min:x_max+1]
101
+ return region, (x_min, y_min, x_max, y_max)
102
+
103
+ # If no selection, return the full image
104
+ return background_np, None
105
+
106
+ else:
107
+ # Case 2: Direct PIL Image
108
+ if hasattr(editor_data, 'size'): # Check if it's a PIL Image
109
+ image_np = np.array(editor_data)
110
+ return image_np, None
111
+ else:
112
+ return None, None
113
+
114
+ def crop_image_with_coords(image_np, coords):
115
+ """Crop image using provided coordinates"""
116
+ if coords is None:
117
+ return image_np
118
+
119
+ x_min, y_min, x_max, y_max = coords
120
+ return image_np[y_min:y_max+1, x_min:x_max+1]
121
+
122
+ def classify_cells_by_color(image_np, masks, hue_min, hue_max, sat_min, val_min, val_max):
123
+ """
124
+ Classify cells as dead (blue) or alive based on trypan blue color threshold
125
+
126
+ Args:
127
+ image_np: RGB image array
128
+ masks: Cellpose segmentation masks
129
+ hue_min, hue_max: Hue range for blue color (0-179 in OpenCV)
130
+ sat_min: Minimum saturation (0-255)
131
+ val_min, val_max: Value (brightness) range (0-255)
132
+
133
+ Returns:
134
+ dead_count, alive_count, colored_overlay
135
+ """
136
+ # Convert RGB to HSV
137
+ hsv = cv2.cvtColor(image_np, cv2.COLOR_RGB2HSV)
138
+
139
+ # Create color mask for trypan blue
140
+ if hue_min <= hue_max:
141
+ color_mask = cv2.inRange(hsv, (hue_min, sat_min, val_min), (hue_max, 255, val_max))
142
+ else:
143
+ # Handle hue wrap-around (e.g., 170-10 for red-blue boundary)
144
+ mask1 = cv2.inRange(hsv, (hue_min, sat_min, val_min), (179, 255, val_max))
145
+ mask2 = cv2.inRange(hsv, (0, sat_min, val_min), (hue_max, 255, val_max))
146
+ color_mask = cv2.bitwise_or(mask1, mask2)
147
+
148
+ # Get unique cell IDs (excluding background)
149
+ cell_ids = np.unique(masks)
150
+ cell_ids = cell_ids[cell_ids > 0] # Remove background (0)
151
+
152
+ dead_cells = []
153
+ alive_cells = []
154
+
155
+ # Classify each cell
156
+ for cell_id in cell_ids:
157
+ # Get mask for this specific cell
158
+ cell_mask = (masks == cell_id)
159
+
160
+ # Calculate overlap with color mask
161
+ overlap = np.sum(color_mask[cell_mask] > 0)
162
+ total_pixels = np.sum(cell_mask)
163
+
164
+ # If >30% of cell pixels are blue, classify as dead
165
+ if overlap / total_pixels > 0.3:
166
+ dead_cells.append(cell_id)
167
+ else:
168
+ alive_cells.append(cell_id)
169
+
170
+ # Create colored overlay
171
+ overlay = image_np.copy()
172
+
173
+ # Color dead cells red, alive cells green
174
+ for cell_id in dead_cells:
175
+ cell_mask = (masks == cell_id)
176
+ overlay[cell_mask] = [255, 0, 0] # Red for dead
177
+
178
+ for cell_id in alive_cells:
179
+ cell_mask = (masks == cell_id)
180
+ overlay[cell_mask] = [0, 255, 0] # Green for alive
181
+
182
+ # Blend with original image
183
+ alpha = 0.4
184
+ overlay = (1 - alpha) * image_np + alpha * overlay
185
+ overlay = np.clip(overlay, 0, 255).astype(np.uint8)
186
+
187
+ return len(dead_cells), len(alive_cells), overlay
188
+
189
+ @spaces.GPU
190
+ def segment_and_count_viability(editor_data, model_choice, hue_min, hue_max, sat_min, val_min, val_max, crop_coords=None):
191
+ """
192
+ Segment cells and analyze viability using trypan blue color threshold
193
+ """
194
+ try:
195
+ # Debug info
196
+ debug_info = debug_editor_data(editor_data)
197
+
198
+ # Load the model
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
+ # Extract region from editor
209
+ region_np, region_coords = extract_region_from_editor(editor_data)
210
+
211
+ if region_np is None:
212
+ return 0, 0, 0, 0.0, None, f"No image provided. Debug info:\n{debug_info}"
213
+
214
+ # If manual crop coordinates are provided, use them instead
215
+ if crop_coords and crop_coords.strip():
216
+ try:
217
+ coords = [int(x.strip()) for x in crop_coords.split(',')]
218
+ if len(coords) == 4:
219
+ x_min, y_min, x_max, y_max = coords
220
+ # Ensure coordinates are within image bounds
221
+ h, w = region_np.shape[:2]
222
+ x_min = max(0, min(x_min, w-1))
223
+ y_min = max(0, min(y_min, h-1))
224
+ x_max = max(x_min+1, min(x_max, w))
225
+ y_max = max(y_min+1, min(y_max, h))
226
+
227
+ region_np = region_np[y_min:y_max, x_min:x_max]
228
+ region_coords = (x_min, y_min, x_max, y_max)
229
+ except ValueError:
230
+ pass # Invalid coordinates, continue with current region
231
+
232
+ # If grayscale, convert to RGB
233
+ if len(region_np.shape) == 2:
234
+ region_np = cv2.cvtColor(region_np, cv2.COLOR_GRAY2RGB)
235
+ elif len(region_np.shape) == 3 and region_np.shape[2] == 4:
236
+ # Handle RGBA images
237
+ region_np = cv2.cvtColor(region_np, cv2.COLOR_RGBA2RGB)
238
+
239
+ # Run Cellpose on the selected region
240
+ masks, flows, styles = model.eval(region_np, diameter=None, channels=[0, 0])
241
+
242
+ # Classify cells by color
243
+ dead_count, alive_count, viability_overlay = classify_cells_by_color(
244
+ region_np, masks, hue_min, hue_max, sat_min, val_min, val_max
245
+ )
246
+
247
+ total_count = dead_count + alive_count
248
+ viability_percent = (alive_count / total_count * 100) if total_count > 0 else 0.0
249
+
250
+ # Convert result to PIL Image for output
251
+ overlay_image = Image.fromarray(viability_overlay)
252
+
253
+ # Create info message
254
+ info_msg = f"Total cells: {total_count}\nAlive cells: {alive_count}\nDead cells: {dead_count}\nViability: {viability_percent:.1f}%"
255
+ if region_coords:
256
+ info_msg += f"\nProcessed region: {region_coords[0]},{region_coords[1]} to {region_coords[2]},{region_coords[3]}"
257
+
258
+ return total_count, alive_count, dead_count, viability_percent, overlay_image, info_msg
259
+
260
+ except Exception as e:
261
+ return 0, 0, 0, 0.0, None, f"Error occurred: {str(e)}\nDebug info:\n{debug_info if 'debug_info' in locals() else 'No debug info'}"
262
+
263
+ @spaces.GPU
264
+ def segment_with_coords_viability(image, model_choice, hue_min, hue_max, sat_min, val_min, val_max, crop_coords):
265
+ """Segment using regular image input with manual coordinate specification and viability analysis"""
266
+ if image is None:
267
+ return 0, 0, 0, 0.0, None, "No image provided"
268
+
269
+ try:
270
+ # Load the model
271
+ model_filename = MODEL_OPTIONS[model_choice]
272
+ model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
273
+
274
+ if model_filename in loaded_models:
275
+ model = loaded_models[model_filename]
276
+ else:
277
+ model = models.CellposeModel(gpu=True, pretrained_model=model_path)
278
+ loaded_models[model_filename] = model
279
+
280
+ # Convert PIL Image to numpy array
281
+ image_np = np.array(image)
282
+
283
+ # Apply crop if coordinates are provided
284
+ if crop_coords and crop_coords.strip():
285
+ try:
286
+ coords = [int(x.strip()) for x in crop_coords.split(',')]
287
+ if len(coords) == 4:
288
+ x_min, y_min, x_max, y_max = coords
289
+ # Ensure coordinates are within image bounds
290
+ h, w = image_np.shape[:2]
291
+ x_min = max(0, min(x_min, w-1))
292
+ y_min = max(0, min(y_min, h-1))
293
+ x_max = max(x_min+1, min(x_max, w))
294
+ y_max = max(y_min+1, min(y_max, h))
295
+
296
+ image_np = image_np[y_min:y_max, x_min:x_max]
297
+ except ValueError:
298
+ pass # Invalid coordinates, use full image
299
+
300
+ # Process image format
301
+ if len(image_np.shape) == 2:
302
+ image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
303
+ elif len(image_np.shape) == 3 and image_np.shape[2] == 4:
304
+ image_np = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
305
+
306
+ # Run Cellpose
307
+ masks, flows, styles = model.eval(image_np, diameter=None, channels=[0, 0])
308
+
309
+ # Classify cells by color
310
+ dead_count, alive_count, viability_overlay = classify_cells_by_color(
311
+ image_np, masks, hue_min, hue_max, sat_min, val_min, val_max
312
+ )
313
+
314
+ total_count = dead_count + alive_count
315
+ viability_percent = (alive_count / total_count * 100) if total_count > 0 else 0.0
316
+
317
+ overlay_image = Image.fromarray(viability_overlay)
318
+
319
+ info_msg = f"Total cells: {total_count}\nAlive cells: {alive_count}\nDead cells: {dead_count}\nViability: {viability_percent:.1f}%"
320
+ if crop_coords:
321
+ info_msg += f"\nProcessed with coordinates: {crop_coords}"
322
+
323
+ return total_count, alive_count, dead_count, viability_percent, overlay_image, info_msg
324
+
325
+ except Exception as e:
326
+ return 0, 0, 0, 0.0, None, f"Error occurred: {str(e)}"
327
+
328
+ # Create the Gradio interface with tabs for different input methods
329
+ with gr.Blocks(title="Cell Viability Counter with Trypan Blue Analysis") as demo:
330
+ gr.Markdown("# Cell Viability Counter with Trypan Blue Analysis")
331
+ gr.Markdown("Upload a trypan blue stained microscopy image and analyze cell viability. Dead cells appear blue, alive cells are unstained.")
332
+
333
+ with gr.Tab("Image Editor (Draw Selection)"):
334
+ gr.Markdown("Use the drawing tools to select a region of the image for segmentation and viability analysis.")
335
+
336
+ with gr.Row():
337
+ with gr.Column():
338
+ image_editor = gr.ImageEditor(
339
+ label="Draw selection on image",
340
+ type="pil",
341
+ brush=gr.Brush(colors=["#ff0000"], color_mode="fixed", default_size=20),
342
+ eraser=gr.Eraser(default_size=20)
343
+ )
344
+
345
+ model_dropdown1 = gr.Dropdown(
346
+ choices=list(MODEL_OPTIONS.keys()),
347
+ label="Select Model",
348
+ value="Hemocytometer Model"
349
+ )
350
+
351
+ # Color threshold controls
352
+ gr.Markdown("### Trypan Blue Color Threshold")
353
+ with gr.Row():
354
+ hue_min1 = gr.Slider(0, 179, value=100, step=1, label="Hue Min (Blue)")
355
+ hue_max1 = gr.Slider(0, 179, value=130, step=1, label="Hue Max (Blue)")
356
+
357
+ with gr.Row():
358
+ sat_min1 = gr.Slider(0, 255, value=50, step=1, label="Saturation Min")
359
+ val_min1 = gr.Slider(0, 255, value=50, step=1, label="Value Min")
360
+ val_max1 = gr.Slider(0, 255, value=255, step=1, label="Value Max")
361
+
362
+ segment_btn1 = gr.Button("Analyze Viability", variant="primary")
363
+
364
+ with gr.Column():
365
+ with gr.Row():
366
+ total_count_output1 = gr.Number(label="Total Cells")
367
+ alive_count_output1 = gr.Number(label="Alive Cells")
368
+ dead_count_output1 = gr.Number(label="Dead Cells")
369
+
370
+ viability_output1 = gr.Number(label="Viability (%)", precision=1)
371
+ overlay_output1 = gr.Image(type="pil", label="Viability Analysis (Green=Alive, Red=Dead)")
372
+ info_output1 = gr.Textbox(label="Analysis Results")
373
+
374
+ segment_btn1.click(
375
+ fn=segment_and_count_viability,
376
+ inputs=[image_editor, model_dropdown1, hue_min1, hue_max1, sat_min1, val_min1, val_max1],
377
+ outputs=[total_count_output1, alive_count_output1, dead_count_output1, viability_output1, overlay_output1, info_output1]
378
+ )
379
+
380
+ with gr.Tab("Manual Coordinates"):
381
+ gr.Markdown("Upload an image and specify coordinates manually (format: x_min,y_min,x_max,y_max)")
382
+
383
+ with gr.Row():
384
+ with gr.Column():
385
+ image_input = gr.Image(type="pil", label="Trypan Blue Stained Image")
386
+
387
+ model_dropdown2 = gr.Dropdown(
388
+ choices=list(MODEL_OPTIONS.keys()),
389
+ label="Select Model",
390
+ value="Hemocytometer Model"
391
+ )
392
+
393
+ coord_input = gr.Textbox(
394
+ label="Crop Coordinates (optional)",
395
+ placeholder="e.g., 100,100,400,400",
396
+ info="Format: x_min,y_min,x_max,y_max"
397
+ )
398
+
399
+ # Color threshold controls
400
+ gr.Markdown("### Trypan Blue Color Threshold")
401
+ with gr.Row():
402
+ hue_min2 = gr.Slider(0, 179, value=100, step=1, label="Hue Min (Blue)")
403
+ hue_max2 = gr.Slider(0, 179, value=130, step=1, label="Hue Max (Blue)")
404
+
405
+ with gr.Row():
406
+ sat_min2 = gr.Slider(0, 255, value=50, step=1, label="Saturation Min")
407
+ val_min2 = gr.Slider(0, 255, value=50, step=1, label="Value Min")
408
+ val_max2 = gr.Slider(0, 255, value=255, step=1, label="Value Max")
409
+
410
+ segment_btn2 = gr.Button("Analyze Viability", variant="primary")
411
+
412
+ with gr.Column():
413
+ with gr.Row():
414
+ total_count_output2 = gr.Number(label="Total Cells")
415
+ alive_count_output2 = gr.Number(label="Alive Cells")
416
+ dead_count_output2 = gr.Number(label="Dead Cells")
417
+
418
+ viability_output2 = gr.Number(label="Viability (%)", precision=1)
419
+ overlay_output2 = gr.Image(type="pil", label="Viability Analysis (Green=Alive, Red=Dead)")
420
+ info_output2 = gr.Textbox(label="Analysis Results")
421
+
422
+ segment_btn2.click(
423
+ fn=segment_with_coords_viability,
424
+ inputs=[image_input, model_dropdown2, hue_min2, hue_max2, sat_min2, val_min2, val_max2, coord_input],
425
+ outputs=[total_count_output2, alive_count_output2, dead_count_output2, viability_output2, overlay_output2, info_output2]
426
+ )
427
+
428
+ # Add usage instructions
429
+ with gr.Tab("Usage Instructions"):
430
+ gr.Markdown("""
431
+ ## How to Use This Tool
432
+
433
+ ### 1. Image Preparation
434
+ - Upload a trypan blue stained microscopy image
435
+ - Dead cells should appear blue, alive cells should be unstained (clear)
436
+
437
+ ### 2. Color Threshold Adjustment
438
+ - **Hue Min/Max**: Adjust to capture the blue color range of trypan blue (typically 100-130)
439
+ - **Saturation Min**: Minimum color saturation to distinguish from gray areas (typically 50+)
440
+ - **Value Min/Max**: Brightness range for blue detection (typically 50-255)
441
+
442
+ ### 3. Default Settings
443
+ - The default settings work well for most trypan blue images
444
+ - Fine-tune based on your specific staining conditions and lighting
445
+
446
+ ### 4. Output
447
+ - **Green overlay**: Live cells
448
+ - **Red overlay**: Dead cells
449
+ - **Viability %**: (Alive cells / Total cells) × 100
450
+
451
+ ### 5. Tips
452
+ - Use consistent lighting and staining protocols for best results
453
+ - Adjust thresholds if cells are misclassified
454
+ - The tool works best with clear contrast between stained and unstained cells
455
+ """)
456
+
457
+ if __name__ == "__main__":
458
+ demo.launch()