LiangLabUMB commited on
Commit
a78e921
·
verified ·
1 Parent(s): cde2d53

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -525
app.py DELETED
@@ -1,525 +0,0 @@
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
- import base64 # needed for mobile ImageEditor base64 handling
12
-
13
- HF_REPO_ID = "myang4218/cellposemodel"
14
- MODEL_OPTIONS = {
15
- "Hemocytometer Model": "hemocytometermodel.npy",
16
- "General Model": "generalmodel.npy"
17
- }
18
-
19
- loaded_models = {}
20
-
21
- # ---- mobile-safe size limits (aggressive for Safari) ----
22
- MAX_SIDE = 384
23
- MAX_PIXELS = 384 * 384
24
-
25
-
26
- def safe_resize(image_np):
27
- h, w = image_np.shape[:2]
28
- total = h * w
29
-
30
- # already small enough
31
- if max(h, w) <= MAX_SIDE and total <= MAX_PIXELS:
32
- return image_np
33
-
34
- # compute scale so BOTH constraints are satisfied
35
- scale_side = MAX_SIDE / max(h, w)
36
- scale_pixels = (MAX_PIXELS / total) ** 0.5
37
- scale = min(scale_side, scale_pixels)
38
-
39
- new_w = max(1, int(w * scale))
40
- new_h = max(1, int(h * scale))
41
-
42
- return cv2.resize(image_np, (new_w, new_h), interpolation=cv2.INTER_AREA)
43
-
44
-
45
- def extract_region_from_editor(editor_data):
46
- """Extract selected region from ImageEditor data (mobile-safe)."""
47
- if editor_data is None:
48
- return None, None
49
-
50
- # CASE 1 — Editor returns a PIL Image directly
51
- if isinstance(editor_data, Image.Image):
52
- return np.array(editor_data), None
53
-
54
- # CASE 2 — Editor returns a NumPy array
55
- if isinstance(editor_data, np.ndarray):
56
- return editor_data, None
57
-
58
- # CASE 3 — Mobile browser returns dict structure
59
- if isinstance(editor_data, dict):
60
-
61
- # --- Get background from multiple possible formats ---
62
- bg = editor_data.get("background")
63
-
64
- if bg is None:
65
- return None, None
66
-
67
- # Mobile ImageEditor sometimes wraps image like: {'image': base64, 'mask': None}
68
- if isinstance(bg, dict) and "image" in bg:
69
- try:
70
- # Decode base64 → PIL → NumPy
71
- img_bytes = io.BytesIO(base64.b64decode(bg["image"].split(",")[-1]))
72
- bg_img = Image.open(img_bytes).convert("RGB")
73
- background_np = np.array(bg_img)
74
- except Exception:
75
- return None, None
76
-
77
- # Desktop / some mobile: bg is a PIL image
78
- elif isinstance(bg, Image.Image):
79
- background_np = np.array(bg)
80
-
81
- # Final fallback
82
- else:
83
- try:
84
- background_np = np.array(bg)
85
- except Exception:
86
- return None, None
87
-
88
- # --- Layers (selection mask) ---
89
- layers = editor_data.get("layers", [])
90
- if layers:
91
- sel = layers[0]
92
-
93
- # Convert to NumPy if needed
94
- if isinstance(sel, Image.Image):
95
- sel_np = np.array(sel)
96
- elif isinstance(sel, dict) and "image" in sel:
97
- try:
98
- sel_bytes = io.BytesIO(base64.b64decode(sel["image"].split(",")[-1]))
99
- sel_np = np.array(Image.open(sel_bytes).convert("RGBA"))
100
- except Exception:
101
- sel_np = None
102
- else:
103
- try:
104
- sel_np = np.array(sel)
105
- except Exception:
106
- sel_np = None
107
-
108
- # If selection exists: compute bounding box
109
- if sel_np is not None:
110
- if sel_np.ndim == 3:
111
- if sel_np.shape[2] == 4:
112
- mask = sel_np[:, :, 3] > 0
113
- else:
114
- mask = np.any(sel_np > 0, axis=2)
115
- else:
116
- mask = sel_np > 0
117
-
118
- coords = np.where(mask)
119
- if coords[0].size > 0:
120
- y0, y1 = coords[0].min(), coords[0].max()
121
- x0, x1 = coords[1].min(), coords[1].max()
122
- return background_np[y0:y1 + 1, x0:x1 + 1], (x0, y0, x1, y1)
123
-
124
- # No selection → return full image
125
- return background_np, None
126
-
127
- # CASE 4 — Unknown structure
128
- return None, None
129
-
130
-
131
- def classify_cells_by_blueness(image_np, masks, blue_threshold):
132
- """
133
- Classify cells as dead (blue) or alive based on single blueness metric
134
-
135
- Args:
136
- image_np: RGB image array
137
- masks: Cellpose segmentation masks
138
- blue_threshold: Single threshold value (0-100) for blueness detection
139
-
140
- Returns:
141
- dead_count, alive_count, colored_overlay
142
- """
143
- # Ensure image_np is RGB for consistency with HSV conversion
144
- if len(image_np.shape) == 2:
145
- image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
146
- elif len(image_np.shape) == 3 and image_np.shape[2] == 4:
147
- image_np = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
148
-
149
- # Convert RGB to HSV
150
- hsv = cv2.cvtColor(image_np, cv2.COLOR_RGB2HSV)
151
-
152
- # Calculate blueness index for each pixel
153
- hue = hsv[:, :, 0].astype(np.float32)
154
- saturation = hsv[:, :, 1].astype(np.float32)
155
-
156
- # Hue score: peaks around 115 (blue in HSV), drops off towards edges
157
- # Handle hue wrap-around for blue detection (100-130 range)
158
- hue_distance = np.minimum(np.abs(hue - 115), 180 - np.abs(hue - 115))
159
- hue_score = np.maximum(0, 1 - hue_distance / 65) # 65 gives good blue range
160
-
161
- # Combine hue proximity with saturation intensity
162
- blueness = hue_score * (saturation / 255.0)
163
-
164
- # Convert threshold from 0-100 to 0-1 scale
165
- threshold = blue_threshold / 100.0
166
-
167
- # Get unique cell IDs (excluding background)
168
- cell_ids = np.unique(masks)
169
- cell_ids = cell_ids[cell_ids > 0] # Remove background (0)
170
-
171
- dead_cells = []
172
- alive_cells = []
173
-
174
- # Classify each cell
175
- for cell_id in cell_ids:
176
- # Get mask for this specific cell
177
- cell_mask = (masks == cell_id)
178
-
179
- # Calculate average blueness for this cell
180
- cell_blueness = np.mean(blueness[cell_mask])
181
-
182
- # Classify based on threshold
183
- if cell_blueness > threshold:
184
- dead_cells.append(cell_id)
185
- else:
186
- alive_cells.append(cell_id)
187
-
188
- # Create colored overlay
189
- overlay = image_np.copy().astype(np.float32) # Ensure float for blending
190
-
191
- # Color dead cells red, alive cells green
192
- for cell_id in dead_cells:
193
- cell_mask = (masks == cell_id)
194
- overlay[cell_mask] = [255, 0, 0] # Red for dead
195
-
196
- for cell_id in alive_cells:
197
- cell_mask = (masks == cell_id)
198
- overlay[cell_mask] = [0, 255, 0] # Green for alive
199
-
200
- # Blend with original image
201
- alpha = 0.4
202
- final_overlay = (1 - alpha) * image_np.astype(np.float32) + alpha * overlay
203
- final_overlay = np.clip(final_overlay, 0, 255).astype(np.uint8)
204
-
205
- return len(dead_cells), len(alive_cells), final_overlay
206
-
207
-
208
- def measure_confluency(masks, image_np):
209
- """Calculate the percentage of image area covered by cells"""
210
- tot_pixels = image_np.shape[0] * image_np.shape[1]
211
- cell_pixels = np.count_nonzero(masks)
212
- confluency = cell_pixels / tot_pixels * 100
213
- return confluency
214
-
215
-
216
- @spaces.GPU
217
- def run_segmentation_editor(editor_data, model_choice):
218
-
219
- try:
220
- image_np = np.array(image)
221
- image_np = safe_resize(image_np)
222
-
223
- model_filename = MODEL_OPTIONS[model_choice]
224
- model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=model_filename)
225
-
226
- if model_filename in loaded_models:
227
- model = loaded_models[model_filename]
228
- else:
229
- model = models.CellposeModel(gpu=True, pretrained_model=model_path)
230
- loaded_models[model_filename] = model
231
-
232
- region_np, region_coords = extract_region_from_editor(editor_data)
233
-
234
- if region_np is None:
235
- return 0, None, f"No image provided.", gr.update(visible=False), None, None, 0.0
236
-
237
- # Enforce mobile-safe size limit immediately
238
- region_np = safe_resize(region_np)
239
-
240
- # Process image format to RGB
241
- if len(region_np.shape) == 2:
242
- processed_image_np = cv2.cvtColor(region_np, cv2.COLOR_GRAY2RGB)
243
- elif len(region_np.shape) == 3 and region_np.shape[2] == 4:
244
- processed_image_np = cv2.cvtColor(region_np, cv2.COLOR_RGBA2RGB)
245
- else:
246
- processed_image_np = region_np
247
-
248
- # Run Cellpose segmentation
249
- masks, flows, styles = model.eval(processed_image_np, diameter=None, channels=[0, 0])
250
-
251
- cell_count = len(np.unique(masks)) - 1
252
- confluency = measure_confluency(masks, processed_image_np)
253
-
254
- # Create a basic segmentation overlay (without viability)
255
- segmentation_overlay = processed_image_np.copy().astype(np.float32)
256
- if masks.max() > 0:
257
- np.random.seed(42) # For consistent random colors
258
- colors = np.random.randint(0, 255, size=(masks.max() + 1, 3))
259
- colors[0] = [0, 0, 0] # Background color
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
- info_msg += f"Confluency: {confluency:.1f}%\n"
267
- if region_coords:
268
- info_msg += f"Processed region: {region_coords[0]},{region_coords[1]} to {region_coords[2]},{region_coords[3]}\n"
269
- info_msg += f"Now adjust the Blue Threshold for viability assessment."
270
-
271
- # Return initial segmentation display and state variables (packed for Gradio State)
272
- return (
273
- cell_count,
274
- Image.fromarray(segmentation_overlay),
275
- info_msg,
276
- gr.update(visible=True),
277
- pack_array(masks),
278
- pack_array(processed_image_np),
279
- confluency,
280
- )
281
-
282
- except Exception as e:
283
- return 0, None, f"Error during segmentation: {str(e)}", gr.update(visible=False), None, None, 0.0
284
-
285
- def run_segmentation_with_cleanup(func):
286
- def wrapper(*args, **kwargs):
287
- try:
288
- return func(*args, **kwargs)
289
- except Exception as e:
290
- # Force garbage collection on error
291
- import gc
292
- gc.collect()
293
- return 0, None, f"Error: {str(e)}", gr.update(visible=False), None, None, 0.0
294
- return wrapper
295
-
296
- run_segmentation_editor = run_segmentation_with_cleanup(run_segmentation_editor)
297
-
298
- def validate_and_prepare_image(image_np, max_side=384, max_pixels=384*384):
299
- """
300
- Validate image and prepare for processing with strict size limits
301
- Returns: (processed_image, error_message)
302
- """
303
- if image_np is None:
304
- return None, "No image provided"
305
-
306
- # Check if image is too small
307
- h, w = image_np.shape[:2]
308
- if h < 50 or w < 50:
309
- return None, "Image too small (minimum 50x50 pixels)"
310
-
311
- # Check file size before processing
312
- if h * w > 4000 * 4000: # Reject images over 16 megapixels before resize
313
- return None, "Image too large. Please resize to under 4000x4000 before uploading."
314
-
315
- # Apply safe resize
316
- resized = safe_resize(image_np)
317
-
318
- # Ensure RGB format
319
- if len(resized.shape) == 2:
320
- resized = cv2.cvtColor(resized, cv2.COLOR_GRAY2RGB)
321
- elif len(resized.shape) == 3 and resized.shape[2] == 4:
322
- resized = cv2.cvtColor(resized, cv2.COLOR_RGBA2RGB)
323
-
324
- return resized, None
325
-
326
-
327
- def update_viability_realtime(blue_threshold, stored_masks, stored_image_np):
328
- # avoid unpacking None (e.g. slider moved before segmentation)
329
- if stored_masks is None or stored_image_np is None:
330
- return None, 0, 0, 0.0, "Please run segmentation first."
331
-
332
- stored_masks = unpack_array(stored_masks)
333
- stored_image_np = unpack_array(stored_image_np)
334
-
335
- try:
336
- dead_count, alive_count, viability_overlay_np = classify_cells_by_blueness(
337
- stored_image_np, stored_masks, blue_threshold
338
- )
339
-
340
- total_count = alive_count + dead_count
341
- viability_percent = (alive_count / total_count * 100) if total_count > 0 else 0.0
342
- confluency = measure_confluency(stored_masks, stored_image_np)
343
-
344
- overlay_image = Image.fromarray(viability_overlay_np)
345
- info_msg = f"Total cells: {total_count}\nLive (green): {alive_count}\nDead (red): {dead_count}\n"
346
- info_msg += f"Viability: {viability_percent:.1f}%\nConfluency: {confluency:.1f}%\nBlue threshold: {blue_threshold}%"
347
-
348
- return overlay_image, alive_count, dead_count, viability_percent, info_msg
349
-
350
- except Exception as e:
351
- return None, 0, 0, 0.0, f"Error updating viability: {str(e)}"
352
-
353
-
354
- def diagnose_image_upload(image):
355
- """Helper function to diagnose upload issues"""
356
- try:
357
- if image is None:
358
- return "No image received"
359
-
360
- img_np = np.array(image)
361
- h, w = img_np.shape[:2]
362
- channels = img_np.shape[2] if len(img_np.shape) == 3 else 1
363
- dtype = img_np.dtype
364
- size_mb = img_np.nbytes / (1024 * 1024)
365
-
366
- info = f"Image received successfully!\n"
367
- info += f"Dimensions: {w}x{h}\n"
368
- info += f"Channels: {channels}\n"
369
- info += f"Data type: {dtype}\n"
370
- info += f"Memory size: {size_mb:.2f} MB\n"
371
-
372
- if size_mb > 10:
373
- info += "\n⚠️ WARNING: Image is large and may cause issues on mobile"
374
-
375
- return info
376
- except Exception as e:
377
- return f"Error analyzing image: {str(e)}"
378
-
379
-
380
- def pack_array(arr):
381
- pil = Image.fromarray(arr.astype(np.uint8))
382
- buf = io.BytesIO()
383
- pil.save(buf, format="PNG")
384
- return buf.getvalue()
385
-
386
-
387
- def unpack_array(data):
388
- return np.array(Image.open(io.BytesIO(data)))
389
-
390
-
391
- # Gradio interface
392
- with gr.Blocks(
393
- title="CellposeCellCounter",
394
- theme=gr.themes.Soft(),
395
- ) as demo:
396
- gr.Markdown("# CellposeCellCounter")
397
- gr.Markdown("For accurate cell confluency, crop the image to display only desired area.")
398
-
399
- # Define State components to store masks and image data across function calls
400
- masks_state = gr.State(value=None)
401
- image_state = gr.State(value=None)
402
-
403
- with gr.Tab("Image Editor (Draw Selection)"):
404
- gr.Markdown("### Draw selection and run segmentation")
405
-
406
- with gr.Row():
407
- with gr.Column():
408
- image_editor = gr.ImageEditor(
409
- label="Draw selection on image",
410
- type="pil",
411
- image_mode="RGB",
412
- height=384, # Reduced from 512
413
- width=384, # ADD explicit width
414
- brush=gr.Brush(colors=["#ff0000"], color_mode="fixed", default_size=20),
415
- eraser=gr.Eraser(default_size=20)
416
- )
417
-
418
- image_input = gr.Image(
419
- type="pil",
420
- label="Microscopy Image",
421
- image_mode="RGB",
422
- height=384, # Reduced from 512
423
- width=384 # ADD explicit width
424
- )
425
-
426
- model_dropdown1 = gr.Dropdown(
427
- choices=list(MODEL_OPTIONS.keys()),
428
- label="Select Model",
429
- value="Hemocytometer Model"
430
- )
431
-
432
- segment_btn1 = gr.Button("🔬 Run Segmentation", variant="primary", size="lg")
433
-
434
- with gr.Column():
435
- cell_count_output1 = gr.Number(label="Total Cells Detected", precision=0)
436
- confluency_output1 = gr.Number(label="Confluency (%)", precision=1)
437
- overlay_output1 = gr.Image(type="pil", label="Segmentation Result")
438
- info_output1 = gr.Textbox(label="Processing Info", lines=4)
439
-
440
- # Viability Assessment Section
441
- with gr.Group(visible=False) as viability_section1:
442
- gr.Markdown("### Viability Assessment (Trypan Blue)")
443
- gr.Markdown("Adjust the threshold to classify cells as live (green) or dead (red).")
444
-
445
- with gr.Row():
446
- with gr.Column():
447
- blue_threshold1 = gr.Slider(
448
- minimum=0,
449
- maximum=100,
450
- value=25,
451
- step=1,
452
- label="Blue Threshold (%)",
453
- info="Higher values = more selective for blue cells"
454
- )
455
-
456
- with gr.Column():
457
- live_count_output1 = gr.Number(label="Live Cells (Green)", precision=0)
458
- dead_count_output1 = gr.Number(label="Dead Cells (Red)", precision=0)
459
-
460
- viability_overlay1 = gr.Image(type="pil", label="Viability Assessment (Green=Live, Red=Dead)")
461
- viability_percent_output1 = gr.Number(label="Viability (%)", precision=1)
462
- viability_info1 = gr.Textbox(label="Analysis Results", lines=5)
463
-
464
- # Event handlers
465
- diagnose_btn1 = gr.Button("🔍 Diagnose Image", variant="secondary", size="sm")
466
- diagnose_output1 = gr.Textbox(label="Diagnostic Info", lines=3, visible=True)
467
-
468
- # Diagnostic handler
469
- diagnose_btn1.click(
470
- fn=diagnose_image_upload,
471
- inputs=[image_editor],
472
- outputs=[diagnose_output1]
473
- )
474
- segment_btn1.click(
475
- fn=run_segmentation_editor,
476
- inputs=[image_editor, model_dropdown1],
477
- outputs=[cell_count_output1, overlay_output1, info_output1, viability_section1, masks_state, image_state, confluency_output1]
478
- ).then( # Chain the initial viability assessment after segmentation
479
- fn=update_viability_realtime,
480
- inputs=[blue_threshold1, masks_state, image_state], # Pass stored state as inputs
481
- outputs=[viability_overlay1, live_count_output1, dead_count_output1, viability_percent_output1, viability_info1]
482
- )
483
-
484
- # Slider changes update viability in real-time
485
- blue_threshold1.change(
486
- fn=update_viability_realtime,
487
- inputs=[blue_threshold1, masks_state, image_state],
488
- outputs=[viability_overlay1, live_count_output1, dead_count_output1, viability_percent_output1, viability_info1]
489
- )
490
-
491
- # Instructions
492
- with gr.Accordion("Instructions", open=False):
493
- gr.Markdown("""
494
- ### How to use:
495
-
496
- 1. **Upload and Segment**:
497
- - Upload your microscopy image.
498
- - Select a Cellpose model (e.g., "Hemocytometer Model" for suspenion culture).
499
- - Draw a selection region using the Image Editor.
500
- - Click "Run Segmentation".
501
-
502
- 2. **Analysis Results**:
503
- - **Cell Count**: Total number of detected cells
504
- - **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.
505
-
506
- 3. **Real-time Viability Assessment (Trypan Blue)**:
507
- - After segmentation, the viability section will become visible.
508
- - This tool is specifically designed for **Trypan Blue stained images**, where dead cells appear blue.
509
- - Adjust the **"Blue Threshold (%)"** slider in real-time. As you change it, the green (live) and red (dead) classification on the overlay will update.
510
- - **Lower values (e.g., 10-20%)** are more sensitive and will classify more cells as blue/dead.
511
- - **Higher values (e.g., 30-50%)** are more selective and will only classify strongly blue cells as dead.
512
- - Green cells = Live, Red cells = Dead.
513
-
514
- 4. **Interpreting Results**:
515
- - The app calculates and displays the total, live, and dead cell counts, along with the viability percentage and confluency.
516
- - **Confluency** helps assess how densely packed your cells are, which is important for cell culture monitoring.
517
- """)
518
-
519
- if __name__ == "__main__":
520
- demo.launch(
521
- share=False,
522
- show_error=True, # Show detailed errors
523
- max_file_size="10mb", # Limit upload size
524
- favicon_path="favicon.png"
525
- )