Shengxiao0709 commited on
Commit
7727a64
·
verified ·
1 Parent(s): 71a7bf5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +177 -203
app.py CHANGED
@@ -5,7 +5,6 @@ import numpy as np
5
  import torch
6
  import os
7
  import shutil
8
- import subprocess
9
  import time
10
  import json
11
  import uuid
@@ -26,9 +25,9 @@ cache_path = os.path.expanduser("~/.cache")
26
  if os.path.exists(cache_path):
27
  try:
28
  shutil.rmtree(cache_path)
29
- print("✅ Deleted ~/.cache to free space.")
30
- except Exception as e:
31
- print(f"⚠️ Could not delete cache: {e}")
32
 
33
  # ===== 全局模型变量 =====
34
  SEG_MODEL = None
@@ -46,19 +45,16 @@ def load_all_models():
46
  global COUNT_MODEL, COUNT_DEVICE
47
  global TRACK_MODEL, TRACK_DEVICE
48
 
49
- # 加载分割模型
50
  print("\n" + "="*60)
51
  print("📦 Loading Segmentation Model")
52
  print("="*60)
53
  SEG_MODEL, SEG_DEVICE = load_seg_model(use_box=False)
54
 
55
- # 加载计数模型
56
  print("\n" + "="*60)
57
  print("📦 Loading Counting Model")
58
  print("="*60)
59
  COUNT_MODEL, COUNT_DEVICE = load_count_model(use_box=False)
60
 
61
- # 加载跟踪模型
62
  print("\n" + "="*60)
63
  print("📦 Loading Tracking Model")
64
  print("="*60)
@@ -68,22 +64,8 @@ def load_all_models():
68
  print("✅ All Models Loaded Successfully")
69
  print("="*60)
70
 
71
- # 启动时加载所有模型
72
  load_all_models()
73
 
74
- # ===== BBox 解析 =====
75
- def parse_first_bbox(bboxes):
76
- if not bboxes:
77
- return None
78
- b = bboxes[0]
79
- if isinstance(b, dict):
80
- x, y = float(b.get("x", 0)), float(b.get("y", 0))
81
- w, h = float(b.get("width", 0)), float(b.get("height", 0))
82
- return x, y, x + w, y + h
83
- if isinstance(b, (list, tuple)) and len(b) >= 4:
84
- return float(b[0]), float(b[1]), float(b[2]), float(b[3])
85
- return None
86
-
87
  # ===== 保存用户反馈 =====
88
  DATASET_DIR = Path("solver_cache")
89
  DATASET_DIR.mkdir(parents=True, exist_ok=True)
@@ -100,6 +82,7 @@ def save_feedback(query_id, feedback_type, feedback_text=None, img_path=None, bb
100
  }
101
  feedback_file = DATASET_DIR / query_id / "feedback.json"
102
  feedback_file.parent.mkdir(parents=True, exist_ok=True)
 
103
  if feedback_file.exists():
104
  with feedback_file.open("r") as f:
105
  existing = json.load(f)
@@ -109,140 +92,168 @@ def save_feedback(query_id, feedback_type, feedback_text=None, img_path=None, bb
109
  feedback_data = existing
110
  else:
111
  feedback_data = [feedback_data]
 
112
  with feedback_file.open("w") as f:
113
  json.dump(feedback_data, f, indent=4, ensure_ascii=False)
114
 
115
- # ===== 彩色 mask 可视化 =====
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  def colorize_mask(mask: np.ndarray, num_colors: int = 512) -> np.ndarray:
117
- mask = mask.astype(np.int32)
118
-
119
- def hsv_to_rgb(hh, ss, vv):
120
- i = int(hh * 6.0)
121
- f = hh * 6.0 - i
122
- p = vv * (1.0 - ss)
123
- q = vv * (1.0 - f * ss)
124
- t = vv * (1.0 - (1.0 - f) * ss)
125
  i = i % 6
126
- if i == 0: r, g, b = vv, t, p
127
- elif i == 1: r, g, b = q, vv, p
128
- elif i == 2: r, g, b = p, vv, t
129
- elif i == 3: r, g, b = p, q, vv
130
- elif i == 4: r, g, b = t, p, vv
131
- else: r, g, b = vv, p, q
132
- return int(r*255), int(g*255), int(b*255)
 
 
 
133
 
134
  palette = [(0, 0, 0)]
135
- for k in range(1, num_colors):
136
- hue = (k % num_colors) / float(num_colors)
137
- palette.append(hsv_to_rgb(hue, 1.0, 0.95))
138
 
139
- color_idx = mask % num_colors
140
  palette_arr = np.array(palette, dtype=np.uint8)
 
141
  return palette_arr[color_idx]
142
 
143
- # ===== 推理 + 实例彩色可视化 =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  def segment_with_choice(use_box_choice, annot_value, mode="Overlay"):
 
145
  if annot_value is None or len(annot_value) < 1:
146
- print("❌ No annotation input")
147
- return None, "❌ 没有输入图像"
148
 
149
  img_path = annot_value[0]
150
  bboxes = annot_value[1] if len(annot_value) > 1 else []
151
 
152
- print(f"🖼️ Image path: {img_path}")
153
  box_array = None
154
  if use_box_choice == "Yes" and bboxes:
155
  box = parse_first_bbox(bboxes)
156
  if box:
157
  xmin, ymin, xmax, ymax = map(int, box)
158
  box_array = [[xmin, ymin, xmax, ymax]]
159
- print(f"📦 Using box: {box_array}")
160
 
 
161
  try:
162
  mask = run_seg(SEG_MODEL, img_path, box=box_array, device=SEG_DEVICE)
163
- print("📏 Mask shape:", mask.shape, "dtype:", mask.dtype, "unique:", np.unique(mask))
164
  except Exception as e:
165
- print(f"❌ Error during inference: {e}")
166
  return None, f"❌ 推理失败: {str(e)}"
167
 
 
168
  try:
169
- img = Image.open(img_path)
170
- print("📷 Image mode:", img.mode, "size:", img.size)
171
- except Exception as e:
172
- print(f"❌ Failed to open image: {e}")
173
- return None, f"❌ 无法打开图像: {str(e)}"
174
-
175
- try:
176
- img_rgb = img.convert("RGB").resize(mask.shape[::-1], resample=Image.BILINEAR)
177
- img_np = np.array(img_rgb, dtype=np.float32)
178
  if img_np.max() > 1.5:
179
- img_np = img_np / 255.0
180
  except Exception as e:
181
- print(f"❌ Error in image conversion/resizing: {e}")
182
- return None, f"❌ 图像转换失败: {str(e)}"
183
 
184
- mask_np = np.array(mask)
185
- inst_mask = mask_np.astype(np.int32)
186
  unique_ids = np.unique(inst_mask)
187
  num_instances = len(unique_ids[unique_ids != 0])
188
- print(f"✅ Instance IDs found: {unique_ids}, Total instances: {num_instances}")
189
 
190
  if num_instances == 0:
191
- print("⚠️ No instance found, returning dummy red image")
192
- return Image.new("RGB", mask.shape[::-1], (255, 0, 0)), "⚠️ 未检测到任何实例"
193
-
194
- # ==== Color Overlay (每个实例一个颜色) ====
195
- overlay = img_np.copy()
196
- alpha = 0.5
197
- cmap = cm.get_cmap("nipy_spectral", num_instances + 1)
198
-
199
- for inst_id in np.unique(inst_mask):
200
- if inst_id == 0:
201
- continue
202
- binary_mask = (inst_mask == inst_id).astype(np.uint8)
203
- color = np.array(cmap(inst_id / (num_instances + 1))[:3]) # RGB only, ignore alpha
204
- overlay[binary_mask == 1] = (1 - alpha) * overlay[binary_mask == 1] + alpha * color
205
-
206
- # 可选:绘制轮廓
207
- contours = measure.find_contours(binary_mask, 0.5)
208
- for contour in contours:
209
- contour = contour.astype(np.int32)
210
- overlay[contour[:, 0], contour[:, 1]] = [1.0, 1.0, 0.0] # 黄色轮廓
211
-
212
- overlay = np.clip(overlay * 255.0, 0, 255).astype(np.uint8)
213
-
214
- status_msg = f"✅ 分割完成! 检测到 {num_instances} 个实例"
215
-
216
- if mode == "Instance Mask Only":
217
- return Image.fromarray(colorize_mask(inst_mask, num_colors=512)), status_msg
218
-
219
- return Image.fromarray(overlay), status_msg
220
-
221
- # ===== Count Handler =====
222
- def count_cells_handler(input_image):
223
- if input_image is None:
224
- return None, "❌ 请先上传图像"
225
 
226
  try:
227
- result_image, cell_count = run_count(COUNT_MODEL, input_image, device=COUNT_DEVICE)
228
- status = f"✅ 计数完成! 检测到 {cell_count} 个细胞"
229
- return result_image, status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  except Exception as e:
 
231
  import traceback
232
  traceback.print_exc()
233
  return None, f"❌ 计数失败: {str(e)}"
234
 
235
- # ===== Track Handler =====
236
  def find_tif_dir(root_dir):
237
- for dirpath, dirnames, filenames in os.walk(root_dir):
238
- tif_files = [f for f in filenames if f.lower().endswith(('.tif', '.tiff'))]
239
- if tif_files:
240
  return dirpath
241
  return None
242
 
243
  def track_video_handler(zip_file_obj):
 
244
  if zip_file_obj is None:
245
- return None, " 请先上传 ZIP 文件"
246
 
247
  try:
248
  temp_dir = tempfile.mkdtemp()
@@ -253,9 +264,9 @@ def track_video_handler(zip_file_obj):
253
 
254
  tif_dir = find_tif_dir(temp_dir)
255
  if tif_dir is None:
256
- return None, f"❌ 跟踪失败: 解压后未找到任何 .tif 图像"
257
 
258
- print(f"🎬 Tracking - Found .tif in folder: {tif_dir}")
259
 
260
  result = run_track(
261
  TRACK_MODEL,
@@ -282,23 +293,17 @@ def track_video_handler(zip_file_obj):
282
  """
283
 
284
  print(f"✅ Tracking done - {num_tracks} tracks")
285
-
286
  return None, result_text
287
 
288
  except zipfile.BadZipFile:
289
  return None, "❌ 上传的文件不是有效的 ZIP 压缩包"
290
-
291
  except Exception as e:
292
  import traceback
293
  traceback.print_exc()
294
  return None, f"❌ 跟踪失败: {str(e)}"
295
 
296
- # ===== 示例图像数据 =====
297
- example_data = [
298
- ("003_img.png", [(50, 60, 120, 150, "cell")]),
299
- ("1977_Well_F-5_Field_1.png", [(30, 40, 100, 130, "cell")]),
300
- ]
301
- gallery_images = [p for p, _ in example_data]
302
 
303
  # ===== Gradio UI =====
304
  with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as demo:
@@ -313,9 +318,9 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
313
  """
314
  )
315
 
316
- # 全局状态: 用于存储当前query_id和用户上传的示例图片
317
  current_query_id = gr.State(str(uuid.uuid4()))
318
- user_uploaded_images = gr.State([])
319
 
320
  with gr.Tabs():
321
  # ===== Tab 1: Segmentation =====
@@ -329,18 +334,17 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
329
  categories=["cell"]
330
  )
331
 
332
- # 示例图片展示
333
  example_gallery = gr.Gallery(
334
- value=gallery_images,
335
  label="📁 示例图片",
336
- columns=[3],
337
  object_fit="cover",
338
- height=128
339
  )
340
 
341
- # 用户上传示例图片
342
  image_uploader = gr.Image(
343
- label="➕ 上传新示例图片到Gallery",
344
  type="filepath"
345
  )
346
 
@@ -361,7 +365,7 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
361
  gr.Markdown(
362
  """
363
  **使用说明:**
364
- 1. 上传图像或选择示例图片
365
  2. (可选) 标注边界框并选择 "Yes"
366
  3. 选择显示模式
367
  4. 点击 "运行分割"
@@ -380,25 +384,26 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
380
  )
381
 
382
  # 满意度评分
383
- score = gr.Slider(
384
- 1, 5,
 
385
  step=1,
386
  value=3,
387
  label="🌟 满意度评分 (1-5)"
388
  )
389
 
390
  # 反馈文本框
391
- comment_box = gr.Textbox(
392
  placeholder="请输入您的反馈意见...",
393
  lines=2,
394
  label="💬 反馈意见"
395
  )
396
 
397
- # 提交评分按钮
398
- submit_score = gr.Button("💾 提交评分", variant="secondary")
399
 
400
  feedback_status = gr.Textbox(
401
- label="✅ 反馈提交状态",
402
  lines=1,
403
  visible=False
404
  )
@@ -410,56 +415,55 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
410
  outputs=[seg_output, seg_status]
411
  )
412
 
413
- # 绑定事件: 上传示例图片到Gallery
414
- def add_uploaded_image(img_path, current_gallery):
 
 
 
 
 
 
415
  if not img_path:
416
- return current_gallery
417
  try:
418
- img = Image.open(img_path)
419
- img.thumbnail((128, 128))
420
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
421
- img.save(temp_file.name, format="PNG")
422
- thumb_path = temp_file.name
423
- if thumb_path not in current_gallery:
424
- current_gallery.append(thumb_path)
425
- return current_gallery
426
- except Exception as e:
427
- print(f"❌ Failed to add image to gallery: {e}")
428
- return current_gallery
429
 
430
  image_uploader.change(
431
- fn=add_uploaded_image,
432
- inputs=[image_uploader, user_uploaded_images],
433
- outputs=user_uploaded_images
434
  ).then(
435
  fn=lambda imgs: imgs,
436
- inputs=user_uploaded_images,
437
  outputs=example_gallery
438
  )
439
 
440
- # 绑定事件: 点击Gallery图片加载到annotator
441
- def load_example(evt: gr.SelectData, examples):
442
- if evt.index is not None and evt.index < len(examples):
443
- img_path = examples[evt.index]
444
- return img_path
445
  return None
446
 
447
  example_gallery.select(
448
- fn=load_example,
449
- inputs=user_uploaded_images,
450
  outputs=annotator
451
  )
452
 
453
- # 绑定事件: 提交评分
454
- def submit_feedback(query_id, score_val, comment_text, annot_value):
455
  try:
456
- img_path = annot_value[0] if annot_value and len(annot_value) > 0 else None
457
- bboxes = annot_value[1] if annot_value and len(annot_value) > 1 else []
458
 
459
  save_feedback(
460
  query_id=query_id,
461
- feedback_type=f"score_{int(score_val)}",
462
- feedback_text=comment_text,
463
  img_path=img_path,
464
  bboxes=bboxes
465
  )
@@ -467,9 +471,9 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
467
  except Exception as e:
468
  return f"❌ 提交失败: {str(e)}", gr.update(visible=True)
469
 
470
- submit_score.click(
471
- fn=submit_feedback,
472
- inputs=[current_query_id, score, comment_box, annotator],
473
  outputs=[feedback_status, feedback_status]
474
  )
475
 
@@ -491,17 +495,12 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
491
  1. 上传细胞图像
492
  2. 点击 "运行计数"
493
  3. 查看密度图和计数结果
494
-
495
- **特点:**
496
- - 基于 Stable Diffusion 特征
497
- - 自动生成密度图
498
- - 无需手动标注
499
  """
500
  )
501
 
502
  with gr.Column(scale=2):
503
  count_output = gr.Image(
504
- label="📸 计数结果 (左: 原图 | 右: 密度图)",
505
  type="filepath",
506
  height=500
507
  )
@@ -510,7 +509,6 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
510
  lines=2
511
  )
512
 
513
- # 绑定事件
514
  count_btn.click(
515
  fn=count_cells_handler,
516
  inputs=count_input,
@@ -532,21 +530,9 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
532
  gr.Markdown(
533
  """
534
  **使用说明:**
535
- 1. 上传包含视频帧序列的压缩包 `.zip`
536
- 2. 压缩包应直接包含 `.tif` 格式图像,如 t000.tif, t001.tif, ...
537
- 3. 点击 "运行跟踪"
538
- 4. 结果将保存到 `tracked_results/` 目录
539
-
540
- **压缩包示例结构:**
541
- ```
542
- frames.zip
543
- ├── t000.tif
544
- ├── t001.tif
545
- ├── t002.tif
546
- └── ...
547
- ```
548
-
549
- **跟踪模式:** Greedy (快速)
550
  """
551
  )
552
 
@@ -557,12 +543,11 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
557
  interactive=False
558
  )
559
 
560
- # 绑定事件:上传zip → 解压 → Tracking
561
- dummy_output = gr.Textbox(visible=False)
562
  track_btn.click(
563
  fn=track_video_handler,
564
  inputs=track_zip_upload,
565
- outputs=[dummy_output, track_output]
566
  )
567
 
568
  gr.Markdown(
@@ -570,20 +555,9 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
570
  ---
571
  ### 💡 技术说明
572
 
573
- **分割 (Segmentation)**
574
- - 模型: 基于 Stable Diffusion 特征的实例分割
575
- - 输出: 每个细胞一个唯一颜色的掩码
576
-
577
- **计数 (Counting)**
578
- - 模型: 密度图估计
579
- - 输出: 密度热力图 + 总计数
580
-
581
- **跟踪 (Tracking)**
582
- - 模型: Trackastra 跟踪算法
583
- - 输出: CTC 格式的轨迹文件
584
-
585
- ---
586
- 📧 问题反馈 | 🌟 GitHub
587
  """
588
  )
589
 
 
5
  import torch
6
  import os
7
  import shutil
 
8
  import time
9
  import json
10
  import uuid
 
25
  if os.path.exists(cache_path):
26
  try:
27
  shutil.rmtree(cache_path)
28
+ print("✅ Deleted ~/.cache")
29
+ except:
30
+ pass
31
 
32
  # ===== 全局模型变量 =====
33
  SEG_MODEL = None
 
45
  global COUNT_MODEL, COUNT_DEVICE
46
  global TRACK_MODEL, TRACK_DEVICE
47
 
 
48
  print("\n" + "="*60)
49
  print("📦 Loading Segmentation Model")
50
  print("="*60)
51
  SEG_MODEL, SEG_DEVICE = load_seg_model(use_box=False)
52
 
 
53
  print("\n" + "="*60)
54
  print("📦 Loading Counting Model")
55
  print("="*60)
56
  COUNT_MODEL, COUNT_DEVICE = load_count_model(use_box=False)
57
 
 
58
  print("\n" + "="*60)
59
  print("📦 Loading Tracking Model")
60
  print("="*60)
 
64
  print("✅ All Models Loaded Successfully")
65
  print("="*60)
66
 
 
67
  load_all_models()
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  # ===== 保存用户反馈 =====
70
  DATASET_DIR = Path("solver_cache")
71
  DATASET_DIR.mkdir(parents=True, exist_ok=True)
 
82
  }
83
  feedback_file = DATASET_DIR / query_id / "feedback.json"
84
  feedback_file.parent.mkdir(parents=True, exist_ok=True)
85
+
86
  if feedback_file.exists():
87
  with feedback_file.open("r") as f:
88
  existing = json.load(f)
 
92
  feedback_data = existing
93
  else:
94
  feedback_data = [feedback_data]
95
+
96
  with feedback_file.open("w") as f:
97
  json.dump(feedback_data, f, indent=4, ensure_ascii=False)
98
 
99
+ # ===== 辅助函数 =====
100
+ def parse_first_bbox(bboxes):
101
+ """解析第一个边界框"""
102
+ if not bboxes:
103
+ return None
104
+ b = bboxes[0]
105
+ if isinstance(b, dict):
106
+ x, y = float(b.get("x", 0)), float(b.get("y", 0))
107
+ w, h = float(b.get("width", 0)), float(b.get("height", 0))
108
+ return x, y, x + w, y + h
109
+ if isinstance(b, (list, tuple)) and len(b) >= 4):
110
+ return float(b[0]), float(b[1]), float(b[2]), float(b[3])
111
+ return None
112
+
113
  def colorize_mask(mask: np.ndarray, num_colors: int = 512) -> np.ndarray:
114
+ """将实例掩码转换为彩色图像"""
115
+ def hsv_to_rgb(h, s, v):
116
+ i = int(h * 6.0)
117
+ f = h * 6.0 - i
 
 
 
 
118
  i = i % 6
119
+ p = v * (1 - s)
120
+ q = v * (1 - f * s)
121
+ t = v * (1 - (1 - f) * s)
122
+ if i == 0: r, g, b = v, t, p
123
+ elif i == 1: r, g, b = q, v, p
124
+ elif i == 2: r, g, b = p, v, t
125
+ elif i == 3: r, g, b = p, q, v
126
+ elif i == 4: r, g, b = t, p, v
127
+ else: r, g, b = v, p, q
128
+ return int(r * 255), int(g * 255), int(b * 255)
129
 
130
  palette = [(0, 0, 0)]
131
+ for i in range(1, num_colors):
132
+ h = (i % num_colors) / float(num_colors)
133
+ palette.append(hsv_to_rgb(h, 1.0, 0.95))
134
 
 
135
  palette_arr = np.array(palette, dtype=np.uint8)
136
+ color_idx = mask % num_colors
137
  return palette_arr[color_idx]
138
 
139
+ def overlay_instances(img, mask, alpha=0.5, cmap_name="tab20"):
140
+ """叠加实例颜色"""
141
+ img = img.astype(np.float32)
142
+ if len(img.shape) == 2:
143
+ img = np.stack([img]*3, axis=-1)
144
+ if img.max() > 1.5:
145
+ img = img / 255.0
146
+
147
+ overlay = img.copy()
148
+ cmap = cm.get_cmap(cmap_name, np.max(mask) + 1)
149
+
150
+ for inst_id in np.unique(mask):
151
+ if inst_id == 0:
152
+ continue
153
+ color = np.array(cmap(inst_id)[:3])
154
+ overlay[mask == inst_id] = (1 - alpha) * overlay[mask == inst_id] + alpha * color
155
+
156
+ return overlay
157
+
158
+ # ===== 分割功能 =====
159
  def segment_with_choice(use_box_choice, annot_value, mode="Overlay"):
160
+ """分割主函数"""
161
  if annot_value is None or len(annot_value) < 1:
162
+ return None, "⚠️ 请上传图像"
 
163
 
164
  img_path = annot_value[0]
165
  bboxes = annot_value[1] if len(annot_value) > 1 else []
166
 
167
+ print(f"🖼️ 图像路径: {img_path}")
168
  box_array = None
169
  if use_box_choice == "Yes" and bboxes:
170
  box = parse_first_bbox(bboxes)
171
  if box:
172
  xmin, ymin, xmax, ymax = map(int, box)
173
  box_array = [[xmin, ymin, xmax, ymax]]
174
+ print(f"📦 使用边界框: {box_array}")
175
 
176
+ # 运行分割模型
177
  try:
178
  mask = run_seg(SEG_MODEL, img_path, box=box_array, device=SEG_DEVICE)
179
+ print("📏 mask shape:", mask.shape, "unique ids:", np.unique(mask))
180
  except Exception as e:
 
181
  return None, f"❌ 推理失败: {str(e)}"
182
 
183
+ # 读取原图
184
  try:
185
+ img = Image.open(img_path).convert("RGB").resize(mask.shape[::-1], resample=Image.BILINEAR)
186
+ img_np = np.array(img).astype(np.float32)
 
 
 
 
 
 
 
187
  if img_np.max() > 1.5:
188
+ img_np /= 255.0
189
  except Exception as e:
190
+ return None, f"❌ 图像读取失败: {str(e)}"
 
191
 
192
+ inst_mask = mask.astype(np.int32)
 
193
  unique_ids = np.unique(inst_mask)
194
  num_instances = len(unique_ids[unique_ids != 0])
195
+ print(f"✅ 实例数量: {num_instances}")
196
 
197
  if num_instances == 0:
198
+ return Image.new("RGB", mask.shape[::-1], (255, 0, 0)), "⚠️ 未检测到实例"
199
+
200
+ # 可视化
201
+ if mode == "Overlay":
202
+ overlay = overlay_instances(img_np, inst_mask, alpha=0.5, cmap_name="tab20")
203
+ overlay_img = Image.fromarray((overlay * 255).astype(np.uint8))
204
+ return overlay_img, f" 检测到 {num_instances} 个细胞"
205
+ elif mode == "Instance Mask Only":
206
+ color_mask = colorize_mask(inst_mask, num_colors=512)
207
+ return Image.fromarray(color_mask), f"✅ 检测到 {num_instances} 个细胞"
208
+
209
+ return None, "❓ 无效显示模式"
210
+
211
+ # ===== 计数功能 =====
212
+ def count_cells_handler(image_path):
213
+ """计数处理函数"""
214
+ if image_path is None:
215
+ return None, "⚠️ 请先上传图像"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
  try:
218
+ print(f"🔢 Counting - Image: {image_path}")
219
+
220
+ result = run_count(
221
+ COUNT_MODEL,
222
+ image_path,
223
+ box=None,
224
+ device=COUNT_DEVICE,
225
+ visualize=True
226
+ )
227
+
228
+ if 'error' in result:
229
+ return None, f"❌ 计数失败: {result['error']}"
230
+
231
+ count = result['count']
232
+ viz_path = result['visualized_path']
233
+ result_text = f"✅ 检测到 {count:.1f} 个细胞"
234
+
235
+ print(f"✅ Counting done - Count: {count:.1f}")
236
+
237
+ return viz_path, result_text
238
+
239
  except Exception as e:
240
+ print(f"❌ Counting error: {e}")
241
  import traceback
242
  traceback.print_exc()
243
  return None, f"❌ 计数失败: {str(e)}"
244
 
245
+ # ===== 跟踪功能 =====
246
  def find_tif_dir(root_dir):
247
+ """递归查找第一个包含 .tif 文件的目录"""
248
+ for dirpath, _, filenames in os.walk(root_dir):
249
+ if any(f.lower().endswith('.tif') for f in filenames):
250
  return dirpath
251
  return None
252
 
253
  def track_video_handler(zip_file_obj):
254
+ """支持 ZIP 压缩包上传的 Tracking 处理函数"""
255
  if zip_file_obj is None:
256
+ return None, "⚠️ 请上传包含视频帧的压缩包 (.zip)"
257
 
258
  try:
259
  temp_dir = tempfile.mkdtemp()
 
264
 
265
  tif_dir = find_tif_dir(temp_dir)
266
  if tif_dir is None:
267
+ return None, "❌ 解压后未找到任何 .tif 图像"
268
 
269
+ print(f"🎬 Tracking - Found .tif in: {tif_dir}")
270
 
271
  result = run_track(
272
  TRACK_MODEL,
 
293
  """
294
 
295
  print(f"✅ Tracking done - {num_tracks} tracks")
 
296
  return None, result_text
297
 
298
  except zipfile.BadZipFile:
299
  return None, "❌ 上传的文件不是有效的 ZIP 压缩包"
 
300
  except Exception as e:
301
  import traceback
302
  traceback.print_exc()
303
  return None, f"❌ 跟踪失败: {str(e)}"
304
 
305
+ # ===== 示例图像 =====
306
+ example_images = ["003_img.png", "1977_Well_F-5_Field_1.png"]
 
 
 
 
307
 
308
  # ===== Gradio UI =====
309
  with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as demo:
 
318
  """
319
  )
320
 
321
+ # 全局状态
322
  current_query_id = gr.State(str(uuid.uuid4()))
323
+ user_uploaded_examples = gr.State(example_images.copy()) # 初始化时包含原始示例
324
 
325
  with gr.Tabs():
326
  # ===== Tab 1: Segmentation =====
 
334
  categories=["cell"]
335
  )
336
 
337
+ # 示例图片Gallery
338
  example_gallery = gr.Gallery(
 
339
  label="📁 示例图片",
340
+ columns=3,
341
  object_fit="cover",
342
+ height=150
343
  )
344
 
345
+ # 上传示例图片
346
  image_uploader = gr.Image(
347
+ label="➕ 上传新示例到Gallery",
348
  type="filepath"
349
  )
350
 
 
365
  gr.Markdown(
366
  """
367
  **使用说明:**
368
+ 1. 上传图像或从Gallery选择示例
369
  2. (可选) 标注边界框并选择 "Yes"
370
  3. 选择显示模式
371
  4. 点击 "运行分割"
 
384
  )
385
 
386
  # 满意度评分
387
+ score_slider = gr.Slider(
388
+ minimum=1,
389
+ maximum=5,
390
  step=1,
391
  value=3,
392
  label="🌟 满意度评分 (1-5)"
393
  )
394
 
395
  # 反馈文本框
396
+ feedback_box = gr.Textbox(
397
  placeholder="请输入您的反馈意见...",
398
  lines=2,
399
  label="💬 反馈意见"
400
  )
401
 
402
+ # 提交按钮
403
+ submit_feedback_btn = gr.Button("💾 提交反馈", variant="secondary")
404
 
405
  feedback_status = gr.Textbox(
406
+ label="✅ 提交状态",
407
  lines=1,
408
  visible=False
409
  )
 
415
  outputs=[seg_output, seg_status]
416
  )
417
 
418
+ # 初始化Gallery显示
419
+ demo.load(
420
+ fn=lambda: example_images.copy(),
421
+ outputs=example_gallery
422
+ )
423
+
424
+ # 绑定事件: 上传示例图片
425
+ def add_to_gallery(img_path, current_imgs):
426
  if not img_path:
427
+ return current_imgs
428
  try:
429
+ if img_path not in current_imgs:
430
+ current_imgs.append(img_path)
431
+ return current_imgs
432
+ except:
433
+ return current_imgs
 
 
 
 
 
 
434
 
435
  image_uploader.change(
436
+ fn=add_to_gallery,
437
+ inputs=[image_uploader, user_uploaded_examples],
438
+ outputs=user_uploaded_examples
439
  ).then(
440
  fn=lambda imgs: imgs,
441
+ inputs=user_uploaded_examples,
442
  outputs=example_gallery
443
  )
444
 
445
+ # 绑定事件: 点击Gallery加载
446
+ def load_from_gallery(evt: gr.SelectData, all_imgs):
447
+ if evt.index is not None and evt.index < len(all_imgs):
448
+ return all_imgs[evt.index]
 
449
  return None
450
 
451
  example_gallery.select(
452
+ fn=load_from_gallery,
453
+ inputs=user_uploaded_examples,
454
  outputs=annotator
455
  )
456
 
457
+ # 绑定事件: 提交反馈
458
+ def submit_user_feedback(query_id, score, comment, annot_val):
459
  try:
460
+ img_path = annot_val[0] if annot_val and len(annot_val) > 0 else None
461
+ bboxes = annot_val[1] if annot_val and len(annot_val) > 1 else []
462
 
463
  save_feedback(
464
  query_id=query_id,
465
+ feedback_type=f"score_{int(score)}",
466
+ feedback_text=comment,
467
  img_path=img_path,
468
  bboxes=bboxes
469
  )
 
471
  except Exception as e:
472
  return f"❌ 提交失败: {str(e)}", gr.update(visible=True)
473
 
474
+ submit_feedback_btn.click(
475
+ fn=submit_user_feedback,
476
+ inputs=[current_query_id, score_slider, feedback_box, annotator],
477
  outputs=[feedback_status, feedback_status]
478
  )
479
 
 
495
  1. 上传细胞图像
496
  2. 点击 "运行计数"
497
  3. 查看密度图和计数结果
 
 
 
 
 
498
  """
499
  )
500
 
501
  with gr.Column(scale=2):
502
  count_output = gr.Image(
503
+ label="📸 计数结果",
504
  type="filepath",
505
  height=500
506
  )
 
509
  lines=2
510
  )
511
 
 
512
  count_btn.click(
513
  fn=count_cells_handler,
514
  inputs=count_input,
 
530
  gr.Markdown(
531
  """
532
  **使用说明:**
533
+ 1. 上传包含 `.tif` 图像的 ZIP 压缩包
534
+ 2. 点击 "运行跟踪"
535
+ 3. 结果保存到 `tracked_results/` 目录
 
 
 
 
 
 
 
 
 
 
 
 
536
  """
537
  )
538
 
 
543
  interactive=False
544
  )
545
 
546
+ dummy = gr.Textbox(visible=False)
 
547
  track_btn.click(
548
  fn=track_video_handler,
549
  inputs=track_zip_upload,
550
+ outputs=[dummy, track_output]
551
  )
552
 
553
  gr.Markdown(
 
555
  ---
556
  ### 💡 技术说明
557
 
558
+ **分割 (Segmentation)** - 基于 Stable Diffusion 特征的实例分割
559
+ **计数 (Counting)** - 密度图估计
560
+ **跟踪 (Tracking)** - Trackastra 跟踪算法
 
 
 
 
 
 
 
 
 
 
 
561
  """
562
  )
563