Shengxiao0709 commited on
Commit
71a7bf5
·
verified ·
1 Parent(s): 9d32f50

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +224 -421
app.py CHANGED
@@ -1,278 +1,3 @@
1
- # import gradio as gr
2
- # from gradio_bbox_annotator import BBoxAnnotator
3
- # from PIL import Image
4
- # import numpy as np
5
- # import torch
6
- # import os
7
- # import shutil
8
- # import subprocess
9
- # import time, json, uuid
10
- # from pathlib import Path
11
- # import tempfile
12
- # from inference import load_model, run
13
- # from skimage import measure
14
- # # === 图像处理依赖 ===
15
- # from scipy.ndimage import label
16
- # from matplotlib import cm
17
- # # ===== 清理缓存目录 =====
18
- # print("===== Space Usage =====")
19
- # subprocess.run("du -sh *", shell=True)
20
- # print("===== ~/.cache =====")
21
- # subprocess.run("ls -lh ~/.cache", shell=True)
22
- # cache_path = os.path.expanduser("~/.cache")
23
- # if os.path.exists(cache_path):
24
- # shutil.rmtree(cache_path)
25
- # print("✅ Deleted ~/.cache to free space.")
26
-
27
- # # ===== 模型初始化 =====
28
- # MODEL = None
29
- # DEVICE = torch.device("cpu")
30
- # CUDA_READY = False
31
-
32
- # def load_model_cpu():
33
- # global MODEL, DEVICE
34
- # MODEL, DEVICE = load_model(use_box=False)
35
- # load_model_cpu()
36
-
37
- # def prepare_cuda():
38
- # global MODEL, DEVICE, CUDA_READY
39
- # if torch.cuda.is_available() and not CUDA_READY:
40
- # MODEL.to("cuda")
41
- # DEVICE = torch.device("cuda")
42
- # CUDA_READY = True
43
- # _ = torch.zeros(1, device=DEVICE)
44
-
45
- # # ===== BBox 解析 =====
46
- # def parse_first_bbox(bboxes):
47
- # if not bboxes:
48
- # return None
49
- # b = bboxes[0]
50
- # if isinstance(b, dict):
51
- # x, y = float(b.get("x", 0)), float(b.get("y", 0))
52
- # w, h = float(b.get("width", 0)), float(b.get("height", 0))
53
- # return x, y, x + w, y + h
54
- # if isinstance(b, (list, tuple)) and len(b) >= 4:
55
- # return float(b[0]), float(b[1]), float(b[2]), float(b[3])
56
- # return None
57
-
58
- # # ===== 保存用户反馈 =====
59
- # DATASET_DIR = Path("solver_cache")
60
- # DATASET_DIR.mkdir(parents=True, exist_ok=True)
61
-
62
- # def save_feedback(query_id, feedback_type, feedback_text=None, img_path=None, bboxes=None):
63
- # feedback_data = {
64
- # "query_id": query_id,
65
- # "feedback_type": feedback_type,
66
- # "feedback_text": feedback_text,
67
- # "image": img_path,
68
- # "bboxes": bboxes,
69
- # "datetime": time.strftime("%Y%m%d_%H%M%S")
70
- # }
71
- # feedback_file = DATASET_DIR / query_id / "feedback.json"
72
- # feedback_file.parent.mkdir(parents=True, exist_ok=True)
73
- # if feedback_file.exists():
74
- # with feedback_file.open("r") as f:
75
- # existing = json.load(f)
76
- # if not isinstance(existing, list):
77
- # existing = [existing]
78
- # existing.append(feedback_data)
79
- # feedback_data = existing
80
- # else:
81
- # feedback_data = [feedback_data]
82
- # with feedback_file.open("w") as f:
83
- # json.dump(feedback_data, f, indent=4, ensure_ascii=False)
84
-
85
- # # ===== 彩色 mask 可视化 =====
86
- # def colorize_mask(mask: np.ndarray, num_colors: int = 512) -> np.ndarray:
87
- # mask = mask.astype(np.int32)
88
-
89
- # def hsv_to_rgb(hh, ss, vv):
90
- # i = int(hh * 6.0)
91
- # f = hh * 6.0 - i
92
- # p = vv * (1.0 - ss)
93
- # q = vv * (1.0 - f * ss)
94
- # t = vv * (1.0 - (1.0 - f) * ss)
95
- # i = i % 6
96
- # if i == 0: r, g, b = vv, t, p
97
- # elif i == 1: r, g, b = q, vv, p
98
- # elif i == 2: r, g, b = p, vv, t
99
- # elif i == 3: r, g, b = p, q, vv
100
- # elif i == 4: r, g, b = t, p, vv
101
- # else: r, g, b = vv, p, q
102
- # return int(r*255), int(g*255), int(b*255)
103
-
104
- # palette = [(0, 0, 0)]
105
- # for k in range(1, num_colors):
106
- # hue = (k % num_colors) / float(num_colors)
107
- # palette.append(hsv_to_rgb(hue, 1.0, 0.95))
108
-
109
- # color_idx = mask % num_colors
110
- # palette_arr = np.array(palette, dtype=np.uint8)
111
- # return palette_arr[color_idx]
112
-
113
- # # ===== 推理 + 实例彩色可视化 =====
114
- # def segment_with_choice(use_box_choice, annot_value, mode="Overlay"):
115
- # prepare_cuda()
116
- # if annot_value is None or len(annot_value) < 1:
117
- # print("❌ No annotation input")
118
- # return None
119
-
120
- # img_path = annot_value[0]
121
- # bboxes = annot_value[1] if len(annot_value) > 1 else []
122
-
123
- # print(f"🖼️ Image path: {img_path}")
124
- # box_array = None
125
- # if use_box_choice == "Yes" and bboxes:
126
- # box = parse_first_bbox(bboxes)
127
- # if box:
128
- # xmin, ymin, xmax, ymax = map(int, box)
129
- # box_array = [[xmin, ymin, xmax, ymax]]
130
- # print(f"📦 Using box: {box_array}")
131
-
132
- # try:
133
- # mask = run(MODEL, img_path, box=box_array, device=DEVICE)
134
- # print("📏 Mask shape:", mask.shape, "dtype:", mask.dtype, "unique:", np.unique(mask))
135
- # except Exception as e:
136
- # print(f"❌ Error during inference: {e}")
137
- # return None
138
-
139
- # try:
140
- # img = Image.open(img_path)
141
- # print("📷 Image mode:", img.mode, "size:", img.size)
142
- # except Exception as e:
143
- # print(f"❌ Failed to open image: {e}")
144
- # return None
145
-
146
- # try:
147
- # img_rgb = img.convert("RGB").resize(mask.shape[::-1], resample=Image.BILINEAR)
148
- # img_np = np.array(img_rgb, dtype=np.float32)
149
- # if img_np.max() > 1.5:
150
- # img_np = img_np / 255.0
151
- # except Exception as e:
152
- # print(f"❌ Error in image conversion/resizing: {e}")
153
- # return None
154
-
155
- # mask_np = np.array(mask)
156
- # inst_mask = mask_np.astype(np.int32)
157
- # unique_ids = np.unique(inst_mask)
158
- # num_instances = len(unique_ids[unique_ids != 0])
159
- # print(f"✅ Instance IDs found: {unique_ids}, Total instances: {num_instances}")
160
-
161
- # if num_instances == 0:
162
- # print("⚠️ No instance found, returning dummy red image")
163
- # return Image.new("RGB", mask.shape[::-1], (255, 0, 0))
164
-
165
- # # ==== Color Overlay (每个实例一个颜色) ====
166
- # overlay = img_np.copy()
167
- # alpha = 0.5
168
- # cmap = cm.get_cmap("nipy_spectral", num_instances + 1)
169
-
170
- # for inst_id in np.unique(inst_mask):
171
- # if inst_id == 0:
172
- # continue
173
- # binary_mask = (inst_mask == inst_id).astype(np.uint8)
174
- # color = np.array(cmap(inst_id / (num_instances + 1))[:3]) # RGB only, ignore alpha
175
- # overlay[binary_mask == 1] = (1 - alpha) * overlay[binary_mask == 1] + alpha * color
176
-
177
- # # 可选:绘制轮廓
178
- # contours = measure.find_contours(binary_mask, 0.5)
179
- # for contour in contours:
180
- # contour = contour.astype(np.int32)
181
- # overlay[contour[:, 0], contour[:, 1]] = [1.0, 1.0, 0.0] # 黄色轮廓
182
-
183
- # overlay = np.clip(overlay * 255.0, 0, 255).astype(np.uint8)
184
-
185
- # if mode == "Instance Mask Only":
186
- # return Image.fromarray(colorize_mask(inst_mask, num_colors=512))
187
-
188
- # return Image.fromarray(overlay)
189
-
190
- # # ===== 示例图像 =====
191
- # example_data = [
192
- # ("003_img.png", [(50, 60, 120, 150, "cell")]),
193
- # ("1977_Well_F-5_Field_1.png", [(30, 40, 100, 130, "cell")]),
194
- # ]
195
- # gallery_images = [p for p, _ in example_data]
196
-
197
- # # ===== Gradio UI =====
198
- # with gr.Blocks(title="Microscopy Cell Segmentation") as demo:
199
- # gr.Markdown("## 🧬 Microscopy Image Segmentation — One Cell, One Color")
200
-
201
- # with gr.Row():
202
- # with gr.Column(scale=1):
203
- # annotator = BBoxAnnotator(label="🖼️ Upload & Annotate", categories=["cell"])
204
-
205
- # example_gallery = gr.Gallery(
206
- # value=gallery_images,
207
- # label="📁 Example Inputs",
208
- # columns=[3], object_fit="cover", height=128
209
- # )
210
-
211
- # image_uploader = gr.Image(label="➕ Upload Image", type="filepath")
212
-
213
- # run_btn = gr.Button("▶️ Run Segmentation")
214
- # use_box_radio = gr.Radio(choices=["Yes", "No"], label="🔲 Use Bounding Box?", visible=False)
215
- # confirm_btn = gr.Button("✅ Confirm", visible=False)
216
- # mode_radio = gr.Radio(choices=["Overlay", "Instance Mask Only"], value="Overlay",
217
- # label="🎨 Display Mode")
218
-
219
- # with gr.Column(scale=2):
220
- # image_output = gr.Image(type="pil", label="📸 Segmentation Result", height=400)
221
- # score = gr.Slider(1, 5, step=1, value=3, label="🌟 Satisfaction (1–5)")
222
- # comment_box = gr.Textbox(placeholder="Type your feedback...", lines=2, label="💬 Feedback")
223
- # submit_score = gr.Button("💾 Submit Rating")
224
-
225
- # user_uploaded_images = gr.State([])
226
-
227
- # def add_uploaded_image(img_path, current_gallery):
228
- # if not img_path:
229
- # return current_gallery
230
- # try:
231
- # img = Image.open(img_path)
232
- # img.thumbnail((128, 128))
233
- # temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
234
- # img.save(temp_file.name, format="PNG")
235
- # thumb_path = temp_file.name
236
- # if thumb_path not in current_gallery:
237
- # current_gallery.append(thumb_path)
238
- # except Exception as e:
239
- # print(f"❌ Failed image: {e}")
240
- # return current_gallery
241
-
242
- # image_uploader.upload(add_uploaded_image, [image_uploader, user_uploaded_images], [example_gallery, user_uploaded_images])
243
-
244
- # def on_gallery_select(evt: gr.SelectData, gallery_images):
245
- # index = evt.index
246
- # if index < len(example_data):
247
- # selected_path, selected_boxes = example_data[index]
248
- # return selected_path, selected_boxes
249
- # else:
250
- # selected_path = gallery_images[index]
251
- # return selected_path, []
252
-
253
- # example_gallery.select(on_gallery_select, inputs=[user_uploaded_images], outputs=[annotator])
254
-
255
- # def show_radio():
256
- # return gr.update(visible=True), gr.update(visible=True)
257
-
258
- # run_btn.click(fn=show_radio, outputs=[use_box_radio, confirm_btn])
259
- # confirm_btn.click(fn=segment_with_choice,
260
- # inputs=[use_box_radio, annotator, mode_radio],
261
- # outputs=image_output)
262
-
263
- # def handle_comment(comment, annot_value):
264
- # save_feedback(time.strftime("%Y%m%d_%H%M%S") + "_" + str(uuid.uuid4())[:8], "comment", comment, annot_value[0], annot_value[1])
265
- # return ""
266
-
267
- # def handle_rating(score, annot_value):
268
- # save_feedback(time.strftime("%Y%m%d_%H%M%S") + "_" + str(uuid.uuid4())[:8], "rating", f"Satisfaction Score: {score}", annot_value[0], annot_value[1])
269
- # return 3
270
-
271
- # comment_box.submit(fn=handle_comment, inputs=[comment_box, annotator], outputs=[comment_box])
272
- # submit_score.click(fn=handle_rating, inputs=[score, annotator], outputs=[score])
273
-
274
- # if __name__ == "__main__":
275
- # demo.queue().launch(server_name="0.0.0.0", server_port=7860, share=True, show_error=True)
276
  import gradio as gr
277
  from gradio_bbox_annotator import BBoxAnnotator
278
  from PIL import Image
@@ -286,32 +11,24 @@ import json
286
  import uuid
287
  from pathlib import Path
288
  import tempfile
 
289
  from skimage import measure
290
  from matplotlib import cm
291
 
292
-
293
  # ===== 导入三个推理模块 =====
294
  from inference_seg import load_model as load_seg_model, run as run_seg
295
  from inference_count import load_model as load_count_model, run as run_count
296
  from inference_track import load_model as load_track_model, run as run_track
297
- import subprocess
298
-
299
- print("\n===== 🔍 TOP 20 Disk Usage in your Space =====")
300
- subprocess.run("du -sh /* /home/* /home/user/* | sort -hr | head -n 20", shell=True)
301
- print("\n===== 🔍 Inside .cache =====")
302
- subprocess.run("du -sh ~/.cache/* | sort -hr | head -n 10", shell=True)
303
- print("\n===== 🔍 Inside current working dir =====")
304
- subprocess.run("du -sh ./* | sort -hr | head -n 10", shell=True)
305
 
306
  # ===== 清理缓存目录 =====
307
- print("===== Space Usage =====")
308
- subprocess.run("du -sh *", shell=True)
309
- print("===== ~/.cache =====")
310
- subprocess.run("ls -lh ~/.cache", shell=True)
311
  cache_path = os.path.expanduser("~/.cache")
312
  if os.path.exists(cache_path):
313
- shutil.rmtree(cache_path)
314
- print("✅ Deleted ~/.cache to free space.")
 
 
 
315
 
316
  # ===== 全局模型变量 =====
317
  SEG_MODEL = None
@@ -354,9 +71,8 @@ def load_all_models():
354
  # 启动时加载所有模型
355
  load_all_models()
356
 
357
- # ===== 辅助函数 =====
358
  def parse_first_bbox(bboxes):
359
- """解析第一个边界框"""
360
  if not bboxes:
361
  return None
362
  b = bboxes[0]
@@ -368,8 +84,36 @@ def parse_first_bbox(bboxes):
368
  return float(b[0]), float(b[1]), float(b[2]), float(b[3])
369
  return None
370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  def colorize_mask(mask: np.ndarray, num_colors: int = 512) -> np.ndarray:
372
- """将实例掩码转换为彩色图像"""
373
  mask = mask.astype(np.int32)
374
 
375
  def hsv_to_rgb(hh, ss, vv):
@@ -396,173 +140,117 @@ def colorize_mask(mask: np.ndarray, num_colors: int = 512) -> np.ndarray:
396
  palette_arr = np.array(palette, dtype=np.uint8)
397
  return palette_arr[color_idx]
398
 
399
- # ===== 分割功能 =====
400
- # ===== 彩色 mask 可视化 =====
401
- def colorize_mask(mask: np.ndarray, num_colors: int = 512) -> np.ndarray:
402
- def hsv_to_rgb(h, s, v):
403
- i = int(h * 6.0)
404
- f = h * 6.0 - i
405
- i = i % 6
406
- p = v * (1 - s)
407
- q = v * (1 - f * s)
408
- t = v * (1 - (1 - f) * s)
409
- if i == 0: r, g, b = v, t, p
410
- elif i == 1: r, g, b = q, v, p
411
- elif i == 2: r, g, b = p, v, t
412
- elif i == 3: r, g, b = p, q, v
413
- elif i == 4: r, g, b = t, p, v
414
- else: r, g, b = v, p, q
415
- return int(r * 255), int(g * 255), int(b * 255)
416
-
417
- palette = [(0, 0, 0)] # 背景为黑色
418
- for i in range(1, num_colors):
419
- h = (i % num_colors) / float(num_colors)
420
- palette.append(hsv_to_rgb(h, 1.0, 0.95))
421
-
422
- palette_arr = np.array(palette, dtype=np.uint8)
423
- color_idx = mask % num_colors
424
- return palette_arr[color_idx]
425
-
426
- def overlay_instances(img, mask, alpha=0.5, cmap_name="tab20"):
427
- img = img.astype(np.float32)
428
- if len(img.shape) == 2:
429
- img = np.stack([img]*3, axis=-1)
430
- if img.max() > 1.5:
431
- img = img / 255.0
432
-
433
- overlay = img.copy()
434
- cmap = cm.get_cmap(cmap_name, np.max(mask) + 1)
435
-
436
- for inst_id in np.unique(mask):
437
- if inst_id == 0:
438
- continue
439
- color = np.array(cmap(inst_id)[:3])
440
- overlay[mask == inst_id] = (1 - alpha) * overlay[mask == inst_id] + alpha * color
441
-
442
- return overlay
443
  # ===== 推理 + 实例彩色可视化 =====
444
  def segment_with_choice(use_box_choice, annot_value, mode="Overlay"):
445
- prepare_cuda()
446
  if annot_value is None or len(annot_value) < 1:
447
  print("❌ No annotation input")
448
- return None, "请上传图像"
449
 
450
  img_path = annot_value[0]
451
  bboxes = annot_value[1] if len(annot_value) > 1 else []
452
 
453
- print(f"🖼️ 图像路径: {img_path}")
454
  box_array = None
455
  if use_box_choice == "Yes" and bboxes:
456
  box = parse_first_bbox(bboxes)
457
  if box:
458
  xmin, ymin, xmax, ymax = map(int, box)
459
  box_array = [[xmin, ymin, xmax, ymax]]
460
- print(f"📦 使用边界框: {box_array}")
461
 
462
- # === Run model
463
  try:
464
- mask = run(MODEL, img_path, box=box_array, device=DEVICE)
465
- print("📏 mask shape:", mask.shape, "unique ids:", np.unique(mask))
466
  except Exception as e:
 
467
  return None, f"❌ 推理失败: {str(e)}"
468
 
469
- # === 读取原图
470
  try:
471
- img = Image.open(img_path).convert("RGB").resize(mask.shape[::-1], resample=Image.BILINEAR)
472
- img_np = np.array(img).astype(np.float32)
 
 
 
 
 
 
 
473
  if img_np.max() > 1.5:
474
- img_np /= 255.0
475
  except Exception as e:
476
- return None, f"❌ 图像读取失败: {str(e)}"
 
477
 
478
- inst_mask = mask.astype(np.int32)
 
479
  unique_ids = np.unique(inst_mask)
480
  num_instances = len(unique_ids[unique_ids != 0])
481
- print(f"✅ 实例数量: {num_instances}")
482
 
483
  if num_instances == 0:
484
- return Image.new("RGB", mask.shape[::-1], (255, 0, 0)), "⚠️ 未检测到实例"
485
-
486
- # === 可视化:Overlay 模式
487
- if mode == "Overlay":
488
- overlay = overlay_instances(img_np, inst_mask, alpha=0.5, cmap_name="tab20")
489
- overlay_img = Image.fromarray((overlay * 255).astype(np.uint8))
490
- return overlay_img, f" 检测到 {num_instances} 个细胞"
491
-
492
- # === 可视化:纯彩色 mask
493
- elif mode == "Instance Mask Only":
494
- color_mask = colorize_mask(inst_mask, num_colors=512)
495
- return Image.fromarray(color_mask), f"✅ 检测到 {num_instances} 个细胞"
496
-
497
- return None, "❓ 无效显示模式"
498
-
499
- # ===== 计数功能 =====
500
- def count_cells_handler(image_path):
501
- """计数处理函数"""
502
- if image_path is None:
503
- return None, "⚠️ 请先上传图像"
 
 
 
 
504
 
505
- if COUNT_MODEL is None:
506
- return None, "❌ 计数模型未加载"
 
 
 
 
 
 
 
507
 
508
  try:
509
- print(f"🔢 Counting - Image: {image_path}")
510
-
511
- result = run_count(
512
- COUNT_MODEL,
513
- image_path,
514
- box=None,
515
- device=COUNT_DEVICE,
516
- visualize=True
517
- )
518
-
519
- if 'error' in result:
520
- return None, f"❌ 计数失败: {result['error']}"
521
-
522
- count = result['count']
523
- viz_path = result['visualized_path']
524
- result_text = f"✅ 检测到 {count:.1f} 个细胞"
525
-
526
- print(f"✅ Counting done - Count: {count:.1f}")
527
-
528
- return viz_path, result_text
529
-
530
  except Exception as e:
531
- print(f"❌ Counting error: {e}")
532
  import traceback
533
  traceback.print_exc()
534
  return None, f"❌ 计数失败: {str(e)}"
535
 
536
- # ===== 跟踪功能 =====
537
- import zipfile
538
- import tempfile
539
- import shutil
540
-
541
  def find_tif_dir(root_dir):
542
- """递归查找第一个包含 .tif 文件的目录"""
543
- for dirpath, _, filenames in os.walk(root_dir):
544
- if any(f.lower().endswith('.tif') for f in filenames):
545
  return dirpath
546
  return None
547
 
548
  def track_video_handler(zip_file_obj):
549
- """支持 ZIP 压缩包上传的 Tracking 处理函数"""
550
  if zip_file_obj is None:
551
- return None, "⚠️ 请上传包含视频帧的压缩包 (.zip)"
552
-
553
- if TRACK_MODEL is None:
554
- return None, "❌ 跟踪模型未加载"
555
 
556
  try:
557
- # 创建临时目录
558
  temp_dir = tempfile.mkdtemp()
559
  print(f"📦 解压到临时目录: {temp_dir}")
560
 
561
- # 解压 zip 文件
562
  with zipfile.ZipFile(zip_file_obj.name, 'r') as zip_ref:
563
  zip_ref.extractall(temp_dir)
564
 
565
- # 自动查找含 .tif 的子目录
566
  tif_dir = find_tif_dir(temp_dir)
567
  if tif_dir is None:
568
  return None, f"❌ 跟踪失败: 解压后未找到任何 .tif 图像"
@@ -571,7 +259,7 @@ def track_video_handler(zip_file_obj):
571
 
572
  result = run_track(
573
  TRACK_MODEL,
574
- video_dir=tif_dir, # 使用真正含图像的目录
575
  box=None,
576
  device=TRACK_DEVICE,
577
  output_dir="tracked_results"
@@ -604,6 +292,14 @@ def track_video_handler(zip_file_obj):
604
  import traceback
605
  traceback.print_exc()
606
  return None, f"❌ 跟踪失败: {str(e)}"
 
 
 
 
 
 
 
 
607
  # ===== Gradio UI =====
608
  with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as demo:
609
  gr.Markdown(
@@ -611,12 +307,16 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
611
  # 🔬 显微图像分析工具套件
612
 
613
  支持三种分析模式:
614
- - 🎨 **分割 (Segmentation)**: 实例分割,每个细胞不同颜色
615
  - 🔢 **计数 (Counting)**: 基于密度图的细胞计数
616
  - 🎬 **跟踪 (Tracking)**: 视频序列中的细胞运动跟踪
617
  """
618
  )
619
 
 
 
 
 
620
  with gr.Tabs():
621
  # ===== Tab 1: Segmentation =====
622
  with gr.Tab("🎨 分割 (Segmentation)"):
@@ -629,6 +329,21 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
629
  categories=["cell"]
630
  )
631
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  with gr.Row():
633
  use_box_radio = gr.Radio(
634
  choices=["Yes", "No"],
@@ -646,7 +361,7 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
646
  gr.Markdown(
647
  """
648
  **使用说明:**
649
- 1. 上传图像
650
  2. (可选) 标注边界框并选择 "Yes"
651
  3. 选择显示模式
652
  4. 点击 "运行分割"
@@ -657,19 +372,106 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
657
  seg_output = gr.Image(
658
  type="pil",
659
  label="📸 分割结果",
660
- height=500
661
  )
662
  seg_status = gr.Textbox(
663
  label="📊 状态信息",
664
  lines=2
665
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
 
667
- # 绑定事件
668
  run_seg_btn.click(
669
  fn=segment_with_choice,
670
  inputs=[use_box_radio, annotator, mode_radio],
671
  outputs=[seg_output, seg_status]
672
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
 
674
  # ===== Tab 2: Counting =====
675
  with gr.Tab("🔢 计数 (Counting)"):
@@ -731,7 +533,7 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
731
  """
732
  **使用说明:**
733
  1. 上传包含视频帧序列的压缩包 `.zip`
734
- 2. 压缩包应直接包含 `.tif` 格式图像,如 t000.tif, t001.tif, ...
735
  3. 点击 "运行跟踪"
736
  4. 结果将保存到 `tracked_results/` 目录
737
 
@@ -755,13 +557,14 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
755
  interactive=False
756
  )
757
 
758
- # 绑定事件:上传zip → 解压 → Tracking
759
  dummy_output = gr.Textbox(visible=False)
760
  track_btn.click(
761
- fn=track_video_handler, # 你刚才改好的函数
762
- inputs=track_zip_upload, # 文件上传
763
- outputs=[dummy_output, track_output] # 第二个是 Textbox 输出
764
  )
 
765
  gr.Markdown(
766
  """
767
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  from gradio_bbox_annotator import BBoxAnnotator
3
  from PIL import Image
 
11
  import uuid
12
  from pathlib import Path
13
  import tempfile
14
+ import zipfile
15
  from skimage import measure
16
  from matplotlib import cm
17
 
 
18
  # ===== 导入三个推理模块 =====
19
  from inference_seg import load_model as load_seg_model, run as run_seg
20
  from inference_count import load_model as load_count_model, run as run_count
21
  from inference_track import load_model as load_track_model, run as run_track
 
 
 
 
 
 
 
 
22
 
23
  # ===== 清理缓存目录 =====
24
+ print("===== 清理缓存 =====")
 
 
 
25
  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
 
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]
 
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)
90
+
91
+ def save_feedback(query_id, feedback_type, feedback_text=None, img_path=None, bboxes=None):
92
+ """保存用户反馈到JSON文件"""
93
+ feedback_data = {
94
+ "query_id": query_id,
95
+ "feedback_type": feedback_type,
96
+ "feedback_text": feedback_text,
97
+ "image": img_path,
98
+ "bboxes": bboxes,
99
+ "datetime": time.strftime("%Y%m%d_%H%M%S")
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)
106
+ if not isinstance(existing, list):
107
+ existing = [existing]
108
+ existing.append(feedback_data)
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):
 
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()
249
  print(f"📦 解压到临时目录: {temp_dir}")
250
 
 
251
  with zipfile.ZipFile(zip_file_obj.name, 'r') as zip_ref:
252
  zip_ref.extractall(temp_dir)
253
 
 
254
  tif_dir = find_tif_dir(temp_dir)
255
  if tif_dir is None:
256
  return None, f"❌ 跟踪失败: 解压后未找到任何 .tif 图像"
 
259
 
260
  result = run_track(
261
  TRACK_MODEL,
262
+ video_dir=tif_dir,
263
  box=None,
264
  device=TRACK_DEVICE,
265
  output_dir="tracked_results"
 
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:
305
  gr.Markdown(
 
307
  # 🔬 显微图像分析工具套件
308
 
309
  支持三种分析模式:
310
+ - 🎨 **分割 (Segmentation)**: 实例分割,每个细胞不同颜色
311
  - 🔢 **计数 (Counting)**: 基于密度图的细胞计数
312
  - 🎬 **跟踪 (Tracking)**: 视频序列中的细胞运动跟踪
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 =====
322
  with gr.Tab("🎨 分割 (Segmentation)"):
 
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
+
347
  with gr.Row():
348
  use_box_radio = gr.Radio(
349
  choices=["Yes", "No"],
 
361
  gr.Markdown(
362
  """
363
  **使用说明:**
364
+ 1. 上传图像或选择示例图片
365
  2. (可选) 标注边界框并选择 "Yes"
366
  3. 选择显示模式
367
  4. 点击 "运行分割"
 
372
  seg_output = gr.Image(
373
  type="pil",
374
  label="📸 分割结果",
375
+ height=400
376
  )
377
  seg_status = gr.Textbox(
378
  label="📊 状态信息",
379
  lines=2
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
+ )
405
 
406
+ # 绑定事件: 运行分割
407
  run_seg_btn.click(
408
  fn=segment_with_choice,
409
  inputs=[use_box_radio, annotator, mode_radio],
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
+ )
466
+ return "✅ 反馈已提交,感谢您的评价!", gr.update(visible=True)
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
 
476
  # ===== Tab 2: Counting =====
477
  with gr.Tab("🔢 计数 (Counting)"):
 
533
  """
534
  **使用说明:**
535
  1. 上传包含视频帧序列的压缩包 `.zip`
536
+ 2. 压缩包应直接包含 `.tif` 格式图像,如 t000.tif, t001.tif, ...
537
  3. 点击 "运行跟踪"
538
  4. 结果将保存到 `tracked_results/` 目录
539
 
 
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(
569
  """
570
  ---