LiangLabUMB commited on
Commit
14d2aef
·
verified ·
1 Parent(s): dce4ab8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +15 -164
app.py CHANGED
@@ -35,16 +35,13 @@ try:
35
  except Exception as e:
36
  print(f"Viability classifier not found or failed to load: {e}")
37
 
38
- # ---- mobile-safe size limits (aggressive for Safari) ----
39
  MAX_SIDE = 1024
40
  MAX_PIXELS = 1024 * 1024
41
 
42
 
43
  def safe_resize(image_np):
44
- """
45
- Downscale image to fit within MAX_SIDE and MAX_PIXELS while
46
- preserving aspect ratio. Works for RGB / RGBA / grayscale.
47
- """
48
  h, w = image_np.shape[:2]
49
  total = h * w
50
 
@@ -152,11 +149,7 @@ FEATURE_COLS_INFERENCE = [
152
 
153
 
154
  def classify_cells_by_model(image_np, masks):
155
- """
156
- Run the trained LogisticRegression classifier to predict live/dead per cell.
157
- Returns (dead_count, alive_count, overlay_np, {cell_id: label}).
158
- Requires VIABILITY_CLF and VIABILITY_SCALER to be loaded.
159
- """
160
  import numpy as np
161
  cell_ids = np.unique(masks)
162
  cell_ids = cell_ids[cell_ids > 0]
@@ -188,11 +181,7 @@ def classify_cells_by_model(image_np, masks):
188
 
189
 
190
  def draw_viability_overlay(image_np, masks, label_map):
191
- """
192
- Draw coloured contours + cell-number labels onto image_np.
193
- label_map: {cell_id: 0=live, 1=dead}
194
- Returns a uint8 numpy array.
195
- """
196
  overlay = image_np.copy()
197
  cell_ids = np.unique(masks)
198
  cell_ids = cell_ids[cell_ids > 0]
@@ -224,112 +213,6 @@ def draw_viability_overlay(image_np, masks, label_map):
224
  return overlay
225
 
226
 
227
- def classify_cells_by_blueness(image_np, masks, threshold_bias):
228
- """
229
- Classify cells as dead (blue) or alive using an adaptive Otsu threshold
230
- on per-cell blueness scores, with a user bias to fine-tune.
231
-
232
- Args:
233
- image_np: RGB image array
234
- masks: Cellpose segmentation masks
235
- threshold_bias: Slider value -50..+50; shifts Otsu threshold up/down.
236
- Negative = more cells classified dead (looser).
237
- Positive = fewer cells classified dead (stricter).
238
- 0 = pure Otsu (fully automatic).
239
-
240
- Returns:
241
- dead_count, alive_count, colored_overlay, otsu_threshold, final_threshold
242
- """
243
-
244
- if len(image_np.shape) == 2:
245
- image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
246
- elif len(image_np.shape) == 3 and image_np.shape[2] == 4:
247
- image_np = cv2.cvtColor(image_np, cv2.COLOR_RGBA2RGB)
248
-
249
- hsv = cv2.cvtColor(image_np, cv2.COLOR_RGB2HSV)
250
-
251
- hue = hsv[:, :, 0].astype(np.float32)
252
- saturation = hsv[:, :, 1].astype(np.float32)
253
-
254
- # Raw blueness: hue proximity to 115° × saturation
255
- hue_distance = np.minimum(np.abs(hue - 115), 180 - np.abs(hue - 115))
256
- hue_score = np.maximum(0, 1 - hue_distance / 65)
257
- blueness = hue_score * (saturation / 255.0)
258
-
259
- # --- Compute per-cell mean blueness scores ---
260
- cell_ids = np.unique(masks)
261
- cell_ids = cell_ids[cell_ids > 0]
262
-
263
- if len(cell_ids) == 0:
264
- blank = image_np.copy()
265
- return 0, 0, blank, 0.0, 0.0
266
-
267
- cell_scores = np.array([np.mean(blueness[masks == cid]) for cid in cell_ids])
268
-
269
- # --- Otsu on the distribution of per-cell scores ---
270
- # cv2.threshold expects uint8; scale 0-1 → 0-255
271
- scores_u8 = (np.clip(cell_scores, 0, 1) * 255).astype(np.uint8)
272
-
273
- if scores_u8.max() == scores_u8.min():
274
- # All cells identical → Otsu is undefined; use midpoint
275
- otsu_threshold = float(scores_u8[0]) / 255.0
276
- else:
277
- # Reshape to a single-column image so cv2.threshold works
278
- thresh_val, _ = cv2.threshold(
279
- scores_u8.reshape(-1, 1), 0, 255,
280
- cv2.THRESH_BINARY + cv2.THRESH_OTSU
281
- )
282
- otsu_threshold = thresh_val / 255.0
283
-
284
- # --- Apply user bias: slider -50..+50 maps to ±0.20 shift ---
285
- bias = (threshold_bias / 50.0) * 0.20
286
- final_threshold = float(np.clip(otsu_threshold + bias, 0.0, 1.0))
287
-
288
- # --- Classify ---
289
- dead_cells = [cid for cid, s in zip(cell_ids, cell_scores) if s > final_threshold]
290
- alive_cells = [cid for cid, s in zip(cell_ids, cell_scores) if s <= final_threshold]
291
-
292
- # --- Outline-only overlay on raw image with enumerated labels ---
293
- final_overlay = image_np.copy()
294
-
295
- # Compute a consistent enumeration order (cell_ids is already sorted ascending)
296
- cell_enum = {cid: idx + 1 for idx, cid in enumerate(cell_ids)}
297
-
298
- dead_set = set(dead_cells)
299
- alive_set = set(alive_cells)
300
-
301
- for cid in cell_ids:
302
- cell_mask = (masks == cid).astype(np.uint8)
303
- contours, _ = cv2.findContours(cell_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
304
- color = (220, 50, 50) if cid in dead_set else (50, 220, 80)
305
- cv2.drawContours(final_overlay, contours, -1, color, thickness=2)
306
-
307
- # Draw enumeration label at centroid
308
- ys, xs = np.where(cell_mask)
309
- if len(ys) > 0:
310
- cx, cy = int(xs.mean()), int(ys.mean())
311
- label_str = str(cell_enum[cid])
312
- font = cv2.FONT_HERSHEY_SIMPLEX
313
- font_scale = 0.35
314
- thickness = 1
315
- (tw, th), _ = cv2.getTextSize(label_str, font, font_scale, thickness)
316
- # Dark background rectangle for readability
317
- cv2.rectangle(
318
- final_overlay,
319
- (cx - tw // 2 - 1, cy - th // 2 - 1),
320
- (cx + tw // 2 + 1, cy + th // 2 + 1),
321
- (0, 0, 0),
322
- -1
323
- )
324
- cv2.putText(
325
- final_overlay, label_str,
326
- (cx - tw // 2, cy + th // 2),
327
- font, font_scale, color, thickness, cv2.LINE_AA
328
- )
329
-
330
- return len(dead_cells), len(alive_cells), final_overlay, otsu_threshold, final_threshold
331
-
332
-
333
  def measure_confluency(masks, image_np):
334
  tot_pixels = image_np.shape[0] * image_np.shape[1]
335
  cell_pixels = np.count_nonzero(masks)
@@ -451,12 +334,10 @@ def warp_polygon_to_square(image_np, points):
451
 
452
 
453
  def toggle_stereological_mode(use_stereology):
454
- """Show/hide stereological controls based on checkbox"""
455
  return gr.update(visible=use_stereology)
456
 
457
 
458
  def update_exclusion_preview(image, left_width, top_width):
459
- """Update the preview image with exclusion zone overlay"""
460
  if image is None:
461
  return None
462
 
@@ -465,9 +346,8 @@ def update_exclusion_preview(image, left_width, top_width):
465
  return Image.fromarray(overlay)
466
 
467
 
468
- # ---------------------------------------------------------------------------
469
  # Patch segmentation
470
- # ---------------------------------------------------------------------------
471
  PATCH_SIZE = 512 # target patch side length
472
  PATCH_OVERLAP = 64 # overlap border on each edge (pixels)
473
  MIN_PATCH_DIM = 256 # don't bother patching if image fits comfortably
@@ -750,7 +630,6 @@ def run_segmentation(image, model_choice, min_cell_size, max_cell_size,
750
 
751
 
752
  def run_viability(stored_masks, stored_image_np):
753
- """Run model-based viability classification. Returns overlay + counts + label_map."""
754
  if stored_masks is None or stored_image_np is None:
755
  return None, 0, 0, 0.0, "Please run segmentation first.", {}
756
  if VIABILITY_CLF is None:
@@ -820,29 +699,12 @@ def compute_summary(r1, r2, r3, r4):
820
  return avg_count, avg_conf, avg_viab, "\n".join(lines)
821
 
822
 
823
- # ---------------------------------------------------------------------------
824
  # Training data export — feature extraction per cell
825
- # ---------------------------------------------------------------------------
826
 
827
  def extract_cell_features(image_np, masks):
828
- """
829
- For every segmented cell, extract a fixed feature vector from the pixels
830
- inside its mask. Returns a list of dicts, one per cell.
831
-
832
- Features:
833
- RGB channels — mean_r, mean_g, mean_b, std_r, std_g, std_b
834
- HSV channels — mean_h, mean_s, mean_v, std_s, std_v
835
- Ratios — blue_red_ratio, blue_green_ratio, rg_ratio
836
- Morphology — area_px, circularity
837
- Centre/edge profile — inner_brightness, peak_brightness,
838
- bright_spot_fraction, ring_darkness,
839
- centre_periphery_ratio, brightness_std_normalised
840
-
841
- Profile zones are tuned to hemocytometer live-cell morphology:
842
- a small intense specular highlight at the centre surrounded by a dark
843
- navy membrane ring. Dead cells are pale blue-grey blobs with no ring
844
- and no bright spot.
845
- """
846
  if len(image_np.shape) == 2:
847
  image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
848
  elif image_np.shape[2] == 4:
@@ -1003,9 +865,8 @@ def prepare_export(stored_masks, stored_image, threshold_bias):
1003
  return path, msg
1004
 
1005
 
1006
- # ---------------------------------------------------------------------------
1007
  # Tab builder
1008
- # ---------------------------------------------------------------------------
1009
 
1010
  def draw_polygon_overlay(image_pil, points):
1011
  """
@@ -1063,14 +924,12 @@ def clear_crop_points(image_pil):
1063
 
1064
 
1065
 
1066
- # ---------------------------------------------------------------------------
1067
  # Label correction grid
1068
- # ---------------------------------------------------------------------------
1069
 
1070
- THUMB_SIZE = 80 # each cell thumbnail is THUMB_SIZE × THUMB_SIZE px
1071
- GRID_COLS = 6 # thumbnails per row
1072
- BORDER = 4 # coloured border thickness in px
1073
- LABEL_H = 16 # height of the text label strip at the bottom of each thumb
1074
 
1075
  def _crop_cell_thumb(image_np, masks, cid):
1076
  """
@@ -1102,14 +961,7 @@ def _crop_cell_thumb(image_np, masks, cid):
1102
 
1103
 
1104
  def build_correction_grid(image_np, masks, labelled_features, raw_image_np=None):
1105
- """
1106
- Render all cell thumbnails into a single PIL image grid.
1107
- Each thumbnail has a coloured border: green=live(0), red=dead(1).
1108
- A small number in the corner identifies the cell_id.
1109
-
1110
- Returns the PIL grid image.
1111
- Cell order in the grid matches the order of labelled_features.
1112
- """
1113
  if not labelled_features:
1114
  placeholder = Image.fromarray(
1115
  np.zeros((THUMB_SIZE, THUMB_SIZE, 3), dtype=np.uint8)
@@ -1496,9 +1348,8 @@ def build_tab(tab_index, masks_state, image_state, result_state):
1496
 
1497
 
1498
 
1499
- # ---------------------------------------------------------------------------
1500
  # Gradio interface
1501
- # ---------------------------------------------------------------------------
1502
  with gr.Blocks(
1503
  title="CellposeCellCounter",
1504
  theme=gr.themes.Soft(),
 
35
  except Exception as e:
36
  print(f"Viability classifier not found or failed to load: {e}")
37
 
38
+ # mobile safe resize limits
39
  MAX_SIDE = 1024
40
  MAX_PIXELS = 1024 * 1024
41
 
42
 
43
  def safe_resize(image_np):
44
+
 
 
 
45
  h, w = image_np.shape[:2]
46
  total = h * w
47
 
 
149
 
150
 
151
  def classify_cells_by_model(image_np, masks):
152
+
 
 
 
 
153
  import numpy as np
154
  cell_ids = np.unique(masks)
155
  cell_ids = cell_ids[cell_ids > 0]
 
181
 
182
 
183
  def draw_viability_overlay(image_np, masks, label_map):
184
+
 
 
 
 
185
  overlay = image_np.copy()
186
  cell_ids = np.unique(masks)
187
  cell_ids = cell_ids[cell_ids > 0]
 
213
  return overlay
214
 
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  def measure_confluency(masks, image_np):
217
  tot_pixels = image_np.shape[0] * image_np.shape[1]
218
  cell_pixels = np.count_nonzero(masks)
 
334
 
335
 
336
  def toggle_stereological_mode(use_stereology):
 
337
  return gr.update(visible=use_stereology)
338
 
339
 
340
  def update_exclusion_preview(image, left_width, top_width):
 
341
  if image is None:
342
  return None
343
 
 
346
  return Image.fromarray(overlay)
347
 
348
 
 
349
  # Patch segmentation
350
+
351
  PATCH_SIZE = 512 # target patch side length
352
  PATCH_OVERLAP = 64 # overlap border on each edge (pixels)
353
  MIN_PATCH_DIM = 256 # don't bother patching if image fits comfortably
 
630
 
631
 
632
  def run_viability(stored_masks, stored_image_np):
 
633
  if stored_masks is None or stored_image_np is None:
634
  return None, 0, 0, 0.0, "Please run segmentation first.", {}
635
  if VIABILITY_CLF is None:
 
699
  return avg_count, avg_conf, avg_viab, "\n".join(lines)
700
 
701
 
702
+
703
  # Training data export — feature extraction per cell
704
+
705
 
706
  def extract_cell_features(image_np, masks):
707
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
708
  if len(image_np.shape) == 2:
709
  image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
710
  elif image_np.shape[2] == 4:
 
865
  return path, msg
866
 
867
 
868
+
869
  # Tab builder
 
870
 
871
  def draw_polygon_overlay(image_pil, points):
872
  """
 
924
 
925
 
926
 
 
927
  # Label correction grid
 
928
 
929
+ THUMB_SIZE = 80
930
+ GRID_COLS = 6
931
+ BORDER = 4
932
+ LABEL_H = 16
933
 
934
  def _crop_cell_thumb(image_np, masks, cid):
935
  """
 
961
 
962
 
963
  def build_correction_grid(image_np, masks, labelled_features, raw_image_np=None):
964
+
 
 
 
 
 
 
 
965
  if not labelled_features:
966
  placeholder = Image.fromarray(
967
  np.zeros((THUMB_SIZE, THUMB_SIZE, 3), dtype=np.uint8)
 
1348
 
1349
 
1350
 
 
1351
  # Gradio interface
1352
+
1353
  with gr.Blocks(
1354
  title="CellposeCellCounter",
1355
  theme=gr.themes.Soft(),