phoebehxf commited on
Commit
f6846ac
·
1 Parent(s): e429374

add tracking display

Browse files
Files changed (1) hide show
  1. app.py +536 -35
app.py CHANGED
@@ -13,6 +13,8 @@ import tempfile
13
  import zipfile
14
  from skimage import measure
15
  from matplotlib import cm
 
 
16
 
17
  # ===== 导入三个推理模块 =====
18
  from inference_seg import load_model as load_seg_model, run as run_seg
@@ -353,55 +355,447 @@ def find_tif_dir(root_dir):
353
  return dirpath
354
  return None
355
 
356
- def track_video_handler(zip_file_obj):
357
- """支持 ZIP 压缩包上传的 Tracking 处理函数"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  if zip_file_obj is None:
359
- return None, "⚠️ 请上传包含视频帧的压缩包 (.zip)"
 
 
 
360
 
361
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  temp_dir = tempfile.mkdtemp()
363
- print(f"📦 解压到临时目录: {temp_dir}")
364
 
365
  with zipfile.ZipFile(zip_file_obj.name, 'r') as zip_ref:
366
- zip_ref.extractall(temp_dir)
367
-
368
- tif_dir = find_tif_dir(temp_dir)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  if tif_dir is None:
370
- return None, "❌ 解压后未找到任何 .tif 图像"
 
 
 
 
 
 
 
 
 
 
 
371
 
372
- print(f"🎬 Tracking - Found .tif in: {tif_dir}")
 
373
 
 
 
 
 
 
374
  result = run_track(
375
  TRACK_MODEL,
376
  video_dir=tif_dir,
377
- box=None,
378
  device=TRACK_DEVICE,
379
- output_dir="tracked_results"
380
  )
381
 
382
  if 'error' in result:
383
- return None, f"❌ 跟踪失败: {result['error']}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
 
385
- output_dir = result['output_dir']
 
 
386
 
387
  result_text = f"""✅ 跟踪完成!
388
 
389
- 📁 结果保存在: {output_dir}
390
 
391
- 包含的文件:
392
- - res_track.txt (CTC格式轨迹)
393
- - 其他跟踪数据文件
394
- """
395
-
396
- print(f"✅ Tracking done")
397
- return None, result_text
 
 
 
 
 
 
 
 
 
 
 
398
 
399
  except zipfile.BadZipFile:
400
- return None, "❌ 上传的文件不是有效的 ZIP 压缩包"
401
  except Exception as e:
402
  import traceback
403
  traceback.print_exc()
404
- return None, f"❌ 跟踪失败: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
405
 
406
  # ===== 示例图像 =====
407
  example_images = ["003_img.png", "1977_Well_F-5_Field_1.png"]
@@ -692,29 +1086,138 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
692
  label="📦 上传视频帧 ZIP 文件",
693
  file_types=[".zip"]
694
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
  track_btn = gr.Button("▶️ 运行跟踪", variant="primary", size="lg")
696
 
697
  gr.Markdown(
698
- """
699
  **使用说明:**
700
- 1. 上传包含 `.tif` 图像的 ZIP 压缩包
701
- 2. 点击 "运行跟踪"
702
- 3. 结果保存到 `tracked_results/` 目录
703
- """
 
 
 
 
 
 
704
  )
705
 
706
  with gr.Column(scale=2):
 
 
 
 
 
 
 
707
  track_output = gr.Textbox(
708
  label="📊 跟踪信息",
709
- lines=12,
710
  interactive=False
711
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
 
713
- dummy = gr.Textbox(visible=False)
 
 
 
 
 
 
 
714
  track_btn.click(
715
  fn=track_video_handler,
716
- inputs=track_zip_upload,
717
- outputs=[dummy, track_output]
718
  )
719
 
720
  gr.Markdown(
@@ -722,9 +1225,7 @@ with gr.Blocks(title="Microscopy Analysis Suite", theme=gr.themes.Soft()) as dem
722
  ---
723
  ### 💡 技术说明
724
 
725
- **分割 (Segmentation)** - 基于 Stable Diffusion 特征的实例分割
726
- **计数 (Counting)** - 密度图估计
727
- **跟踪 (Tracking)** - Trackastra 跟踪算法
728
  """
729
  )
730
 
 
13
  import zipfile
14
  from skimage import measure
15
  from matplotlib import cm
16
+ from glob import glob
17
+ from natsort import natsorted
18
 
19
  # ===== 导入三个推理模块 =====
20
  from inference_seg import load_model as load_seg_model, run as run_seg
 
355
  return dirpath
356
  return None
357
 
358
+ def is_valid_tiff(filepath):
359
+ """Check if a file is a valid TIFF image"""
360
+ try:
361
+ with Image.open(filepath) as img:
362
+ img.verify()
363
+ return True
364
+ except Exception as e:
365
+ return False
366
+
367
+ def find_valid_tif_dir(root_dir):
368
+ """递归查找第一个包含有效 .tif 文件的目录"""
369
+ for dirpath, dirnames, filenames in os.walk(root_dir):
370
+ if '__MACOSX' in dirpath:
371
+ continue
372
+
373
+ potential_tifs = [
374
+ os.path.join(dirpath, f)
375
+ for f in filenames
376
+ if f.lower().endswith(('.tif', '.tiff')) and not f.startswith('._')
377
+ ]
378
+
379
+ if not potential_tifs:
380
+ continue
381
+
382
+ valid_tifs = [f for f in potential_tifs if is_valid_tiff(f)]
383
+
384
+ if valid_tifs:
385
+ print(f"✅ Found {len(valid_tifs)} valid TIFF files in: {dirpath}")
386
+ return dirpath
387
+
388
+ return None
389
+
390
+ def create_ctc_results_zip(output_dir):
391
+ """
392
+ Create a ZIP file with CTC format results
393
+
394
+ Parameters:
395
+ -----------
396
+ output_dir : str
397
+ Directory containing tracking results (res_track.txt, etc.)
398
+
399
+ Returns:
400
+ --------
401
+ zip_path : str
402
+ Path to created ZIP file
403
+ """
404
+ # Create temp directory for ZIP
405
+ temp_zip_dir = tempfile.mkdtemp()
406
+ zip_filename = f"tracking_results_{time.strftime('%Y%m%d_%H%M%S')}.zip"
407
+ zip_path = os.path.join(temp_zip_dir, zip_filename)
408
+
409
+ print(f"📦 Creating results ZIP: {zip_path}")
410
+
411
+ # Create ZIP with all tracking results
412
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
413
+ # Add all files from output directory
414
+ for root, dirs, files in os.walk(output_dir):
415
+ for file in files:
416
+ file_path = os.path.join(root, file)
417
+ arcname = os.path.relpath(file_path, output_dir)
418
+ zipf.write(file_path, arcname)
419
+ print(f" 📄 Added: {arcname}")
420
+
421
+ # Add a README with summary
422
+ readme_content = f"""Tracking Results Summary
423
+ ========================
424
+
425
+ Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}
426
+
427
+ Files:
428
+ ------
429
+ - res_track.txt: CTC format tracking data
430
+ Format: track_id start_frame end_frame parent_id
431
+
432
+ - Segmentation masks
433
+
434
+ For more information on CTC format:
435
+ http://celltrackingchallenge.net/
436
+ """
437
+ zipf.writestr("README.txt", readme_content)
438
+
439
+ print(f"✅ ZIP created: {zip_path} ({os.path.getsize(zip_path) / 1024:.1f} KB)")
440
+ return zip_path
441
+
442
+ def extract_first_frame(tif_dir):
443
+ """
444
+ Extract the first frame from a directory of TIF files
445
+
446
+ Returns:
447
+ --------
448
+ first_frame_path : str
449
+ Path to the first TIF frame
450
+ """
451
+ tif_files = natsorted(glob(os.path.join(tif_dir, "*.tif")) +
452
+ glob(os.path.join(tif_dir, "*.tiff")))
453
+ valid_tif_files = [f for f in tif_files
454
+ if not os.path.basename(f).startswith('._') and is_valid_tiff(f)]
455
+
456
+ if valid_tif_files:
457
+ return valid_tif_files[0]
458
+ return None
459
+
460
+ def create_tracking_visualization(tif_dir, output_dir, valid_tif_files):
461
+ """
462
+ Create an animated GIF/video showing tracked objects with consistent colors
463
+
464
+ Parameters:
465
+ -----------
466
+ tif_dir : str
467
+ Directory containing input TIF frames
468
+ output_dir : str
469
+ Directory containing tracking results (masks)
470
+ valid_tif_files : list
471
+ List of valid TIF file paths
472
+
473
+ Returns:
474
+ --------
475
+ video_path : str
476
+ Path to generated visualization (GIF or first frame)
477
+ """
478
+ import numpy as np
479
+ from matplotlib import colormaps
480
+ from skimage import measure
481
+ import tifffile
482
+
483
+ # Look for tracking mask files in output directory
484
+ # Common CTC formats: man_track*.tif, mask*.tif, or numbered masks
485
+ mask_files = natsorted(glob(os.path.join(output_dir, "mask*.tif")) +
486
+ glob(os.path.join(output_dir, "man_track*.tif")) +
487
+ glob(os.path.join(output_dir, "*.tif")))
488
+
489
+ if not mask_files:
490
+ print("⚠️ No mask files found in output directory")
491
+ # Return first frame as fallback
492
+ return valid_tif_files[0]
493
+
494
+ print(f"📊 Found {len(mask_files)} mask files")
495
+
496
+ # Create color map for consistent track IDs
497
+ # Use a colormap with many distinct colors
498
+ try:
499
+ cmap = colormaps.get_cmap("nipy_spectral")
500
+ except:
501
+ from matplotlib import cm
502
+ cmap = cm.get_cmap("nipy_spectral")
503
+
504
+ frames = []
505
+ alpha = 0.5 # Transparency for overlay
506
+
507
+ # Process each frame
508
+ num_frames = min(len(valid_tif_files), len(mask_files))
509
+ for i in range(num_frames):
510
+ try:
511
+ # Load original image using tifffile (handles ZSTD compression)
512
+ try:
513
+ img_np = tifffile.imread(valid_tif_files[i])
514
+
515
+ # Normalize to [0, 1] range based on actual data type and values
516
+ if img_np.dtype == np.uint8:
517
+ img_np = img_np.astype(np.float32) / 255.0
518
+ elif img_np.dtype == np.uint16:
519
+ # Normalize uint16 to [0, 1] using actual min/max
520
+ img_min, img_max = img_np.min(), img_np.max()
521
+ if img_max > img_min:
522
+ img_np = (img_np.astype(np.float32) - img_min) / (img_max - img_min)
523
+ else:
524
+ img_np = img_np.astype(np.float32) / 65535.0
525
+ else:
526
+ # For float or other types, normalize based on actual range
527
+ img_np = img_np.astype(np.float32)
528
+ img_min, img_max = img_np.min(), img_np.max()
529
+ if img_max > img_min:
530
+ img_np = (img_np - img_min) / (img_max - img_min)
531
+ else:
532
+ img_np = np.clip(img_np, 0, 1)
533
+
534
+ # Convert to RGB if grayscale
535
+ if img_np.ndim == 2:
536
+ img_np = np.stack([img_np]*3, axis=-1)
537
+ img_np = img_np.astype(np.float32)
538
+ if img_np.max() > 1.5:
539
+ img_np = img_np / 255.0
540
+ except Exception as e:
541
+ print(f"⚠️ Error loading image frame {i}: {e}")
542
+ # Fallback to PIL
543
+ img = Image.open(valid_tif_files[i]).convert("RGB")
544
+ img_np = np.array(img, dtype=np.float32) / 255.0
545
+
546
+ # Load tracking mask using tifffile (handles ZSTD compression)
547
+ try:
548
+ mask = tifffile.imread(mask_files[i])
549
+ except Exception as e:
550
+ print(f"⚠️ Error loading mask frame {i}: {e}")
551
+ # Fallback to PIL
552
+ mask = np.array(Image.open(mask_files[i]))
553
+
554
+ # Resize mask to match image if needed
555
+ if mask.shape[:2] != img_np.shape[:2]:
556
+ from scipy.ndimage import zoom
557
+ zoom_factors = [img_np.shape[0] / mask.shape[0], img_np.shape[1] / mask.shape[1]]
558
+ mask = zoom(mask, zoom_factors, order=0).astype(mask.dtype)
559
+
560
+ # Create overlay
561
+ overlay = img_np.copy()
562
+
563
+ # Get unique track IDs (excluding background 0)
564
+ track_ids = np.unique(mask)
565
+ track_ids = track_ids[track_ids != 0]
566
+
567
+ # Color each tracked object
568
+ for track_id in track_ids:
569
+ # Create binary mask for this track
570
+ binary_mask = (mask == track_id)
571
+
572
+ # Get consistent color for this track ID
573
+ color = np.array(cmap(int(track_id) % 256)[:3])
574
+
575
+ # Blend color onto image
576
+ overlay[binary_mask] = (1 - alpha) * overlay[binary_mask] + alpha * color
577
+
578
+ # Draw contours (optional, adds yellow boundaries)
579
+ try:
580
+ contours = measure.find_contours(binary_mask.astype(np.uint8), 0.5)
581
+ for contour in contours:
582
+ contour = contour.astype(np.int32)
583
+ valid_y = np.clip(contour[:, 0], 0, overlay.shape[0] - 1)
584
+ valid_x = np.clip(contour[:, 1], 0, overlay.shape[1] - 1)
585
+ overlay[valid_y, valid_x] = [1.0, 1.0, 0.0] # Yellow contour
586
+ except:
587
+ pass # Skip contours if they fail
588
+
589
+ # Convert to uint8
590
+ overlay_uint8 = np.clip(overlay * 255.0, 0, 255).astype(np.uint8)
591
+ frames.append(Image.fromarray(overlay_uint8))
592
+
593
+ if i % 10 == 0 or i == num_frames - 1:
594
+ print(f" 📸 Processed frame {i+1}/{num_frames}")
595
+
596
+ except Exception as e:
597
+ print(f"⚠️ Error processing frame {i}: {e}")
598
+ import traceback
599
+ traceback.print_exc()
600
+ continue
601
+
602
+ if not frames:
603
+ print("⚠️ No frames were processed successfully")
604
+ return valid_tif_files[0]
605
+
606
+ # Save as animated GIF
607
+ try:
608
+ temp_gif = tempfile.NamedTemporaryFile(delete=False, suffix=".gif")
609
+ frames[0].save(
610
+ temp_gif.name,
611
+ save_all=True,
612
+ append_images=frames[1:],
613
+ duration=200, # 200ms per frame = 5fps
614
+ loop=0
615
+ )
616
+ temp_gif.close() # Close the file handle
617
+ print(f"✅ Created tracking visualization GIF: {temp_gif.name}")
618
+ print(f" Size: {os.path.getsize(temp_gif.name)} bytes, Frames: {len(frames)}")
619
+ return temp_gif.name
620
+ except Exception as e:
621
+ print(f"⚠️ Failed to create GIF: {e}")
622
+ import traceback
623
+ traceback.print_exc()
624
+ # Return first frame as static image fallback
625
+ try:
626
+ temp_img = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
627
+ frames[0].save(temp_img.name)
628
+ temp_img.close()
629
+ return temp_img.name
630
+ except:
631
+ return valid_tif_files[0]
632
+
633
+
634
+ def track_video_handler(use_box_choice, first_frame_annot, zip_file_obj):
635
+ """
636
+ 支持 ZIP 压缩包上传的 Tracking 处理函数 - 支持首帧边界框
637
+
638
+ Parameters:
639
+ -----------
640
+ use_box_choice : str
641
+ "Yes" or "No" - 是否使用边界框
642
+ first_frame_annot : tuple or None
643
+ (image_path, bboxes) from BBoxAnnotator, only used if user annotated first frame
644
+ zip_file_obj : File
645
+ Uploaded ZIP file containing TIF sequence
646
+ """
647
  if zip_file_obj is None:
648
+ return None, "⚠️ 请上传包含视频帧的压缩包 (.zip)", None, None
649
+
650
+ temp_dir = None
651
+ output_temp_dir = None
652
 
653
  try:
654
+ # Parse bounding box if provided
655
+ box_array = None
656
+ if use_box_choice == "Yes" and first_frame_annot is not None:
657
+ if isinstance(first_frame_annot, (list, tuple)) and len(first_frame_annot) > 1:
658
+ bboxes = first_frame_annot[1]
659
+ if bboxes:
660
+ box = parse_first_bbox(bboxes)
661
+ if box:
662
+ xmin, ymin, xmax, ymax = map(int, box)
663
+ box_array = [[xmin, ymin, xmax, ymax]]
664
+ print(f"📦 使用边界框: {box_array}")
665
+
666
+ # Extract input ZIP
667
  temp_dir = tempfile.mkdtemp()
668
+ print(f"\n📦 解压到临时目录: {temp_dir}")
669
 
670
  with zipfile.ZipFile(zip_file_obj.name, 'r') as zip_ref:
671
+ extracted_count = 0
672
+ skipped_count = 0
673
+
674
+ for member in zip_ref.namelist():
675
+ basename = os.path.basename(member)
676
+
677
+ if ('__MACOSX' in member or
678
+ basename.startswith('._') or
679
+ basename.startswith('.DS_Store') or
680
+ member.endswith('/')):
681
+ skipped_count += 1
682
+ continue
683
+
684
+ try:
685
+ zip_ref.extract(member, temp_dir)
686
+ extracted_count += 1
687
+ if basename.lower().endswith(('.tif', '.tiff')):
688
+ print(f"📄 Extracted TIFF: {basename}")
689
+ except Exception as e:
690
+ print(f"⚠️ Failed to extract {member}: {e}")
691
+
692
+ print(f"\n📊 提取: {extracted_count} 文件, 跳过: {skipped_count} 文件")
693
+
694
+ # Find valid TIFF directory
695
+ tif_dir = find_valid_tif_dir(temp_dir)
696
+
697
  if tif_dir is None:
698
+ return None, "❌ 未找到有效的TIFF文件", None, None
699
+
700
+ # Validate TIFF files
701
+ tif_files = sorted(glob(os.path.join(tif_dir, "*.tif")) +
702
+ glob(os.path.join(tif_dir, "*.tiff")))
703
+ valid_tif_files = [f for f in tif_files
704
+ if not os.path.basename(f).startswith('._') and is_valid_tiff(f)]
705
+
706
+ if len(valid_tif_files) == 0:
707
+ return None, "❌ 没有有效的TIFF文件", None, None
708
+
709
+ print(f"📈 使用 {len(valid_tif_files)} 个TIFF文件")
710
 
711
+ # Store paths for later visualization
712
+ first_frame_path = valid_tif_files[0]
713
 
714
+ # Create temporary output directory for CTC results
715
+ output_temp_dir = tempfile.mkdtemp()
716
+ print(f"💾 CTC结果将保存到: {output_temp_dir}")
717
+
718
+ # Run tracking with optional bounding box
719
  result = run_track(
720
  TRACK_MODEL,
721
  video_dir=tif_dir,
722
+ box=box_array, # Pass bounding box if specified
723
  device=TRACK_DEVICE,
724
+ output_dir=output_temp_dir
725
  )
726
 
727
  if 'error' in result:
728
+ return None, f"❌ 跟踪失败: {result['error']}", None, None
729
+
730
+ # Create visualization video of tracked objects
731
+ print("\n🎬 Creating tracking visualization...")
732
+ try:
733
+ tracking_video = create_tracking_visualization(
734
+ tif_dir,
735
+ output_temp_dir,
736
+ valid_tif_files
737
+ )
738
+ except Exception as e:
739
+ print(f"⚠️ Failed to create visualization: {e}")
740
+ import traceback
741
+ traceback.print_exc()
742
+ # Fallback to first frame if visualization fails
743
+ try:
744
+ tracking_video = Image.open(first_frame_path)
745
+ except:
746
+ tracking_video = None
747
+
748
+ # Create downloadable ZIP with results
749
+ try:
750
+ results_zip = create_ctc_results_zip(output_temp_dir)
751
+ except Exception as e:
752
+ print(f"⚠️ Failed to create ZIP: {e}")
753
+ results_zip = None
754
 
755
+ bbox_info = ""
756
+ if box_array:
757
+ bbox_info = f"\n🔲 使用边界框: [{box_array[0][0]}, {box_array[0][1]}, {box_array[0][2]}, {box_array[0][3]}]"
758
 
759
  result_text = f"""✅ 跟踪完成!
760
 
761
+ 🖼️ 处理帧数: {len(valid_tif_files)}{bbox_info}
762
 
763
+ 📥 点击下方按钮下载CTC格式结果
764
+ 结果包含:
765
+ - res_track.txt (CTC格式轨迹数据)
766
+ - 其他跟踪相关文件
767
+ - README.txt (结果说明)
768
+ """
769
+
770
+ print(f"\n✅ Tracking完成")
771
+
772
+ # Clean up input temp directory (keep output temp for download)
773
+ if temp_dir:
774
+ try:
775
+ shutil.rmtree(temp_dir)
776
+ print(f"🗑️ 清理输入临时目录")
777
+ except:
778
+ pass
779
+
780
+ return results_zip, result_text, gr.update(visible=True), tracking_video
781
 
782
  except zipfile.BadZipFile:
783
+ return None, "❌ 不是有效的ZIP文件", None, None
784
  except Exception as e:
785
  import traceback
786
  traceback.print_exc()
787
+
788
+ # Clean up on error
789
+ for d in [temp_dir, output_temp_dir]:
790
+ if d:
791
+ try:
792
+ shutil.rmtree(d)
793
+ except:
794
+ pass
795
+
796
+ return None, f"❌ 跟踪失败: {str(e)}", None, None
797
+
798
+
799
 
800
  # ===== 示例图像 =====
801
  example_images = ["003_img.png", "1977_Well_F-5_Field_1.png"]
 
1086
  label="📦 上传视频帧 ZIP 文件",
1087
  file_types=[".zip"]
1088
  )
1089
+
1090
+ # First frame annotation for bounding box
1091
+ track_first_frame_annotator = BBoxAnnotator(
1092
+ label="🖼️ 首帧边界框标注 (可选)",
1093
+ categories=["cell"]
1094
+ )
1095
+
1096
+ with gr.Row():
1097
+ track_use_box_radio = gr.Radio(
1098
+ choices=["Yes", "No"],
1099
+ value="No",
1100
+ label="🔲 使用边界框?"
1101
+ )
1102
+
1103
  track_btn = gr.Button("▶️ 运行跟踪", variant="primary", size="lg")
1104
 
1105
  gr.Markdown(
1106
+ '''
1107
  **使用说明:**
1108
+ 1. 上传包含 `.tif` 图像序列的 ZIP 文件
1109
+ 2. (可选) 在首帧上标注边界框并选择 "Yes"
1110
+ 3. 点击 "运行跟踪"
1111
+ 4. 下载CTC格式结果
1112
+
1113
+ **注意:**
1114
+ - 确保TIFF文件按时间顺序命名 (如: t000.tif, t001.tif...)
1115
+ - 边界框将应用于整个视频序列
1116
+ - 避免使用macOS的压缩工具
1117
+ '''
1118
  )
1119
 
1120
  with gr.Column(scale=2):
1121
+ # Preview of first frame
1122
+ track_first_frame_preview = gr.Image(
1123
+ label="📸 首帧预览",
1124
+ type="pil",
1125
+ height=300
1126
+ )
1127
+
1128
  track_output = gr.Textbox(
1129
  label="📊 跟踪信息",
1130
+ lines=8,
1131
  interactive=False
1132
  )
1133
+
1134
+ # Download button for CTC results
1135
+ track_download = gr.File(
1136
+ label="📥 下载跟踪结果 (CTC格式)",
1137
+ visible=False
1138
+ )
1139
+
1140
+ def load_first_frame_for_annotation(zip_file_obj):
1141
+ '''Load and normalize first frame from ZIP for annotation'''
1142
+ if zip_file_obj is None:
1143
+ return None
1144
+
1145
+ import tifffile
1146
+
1147
+ try:
1148
+ temp_dir = tempfile.mkdtemp()
1149
+ with zipfile.ZipFile(zip_file_obj.name, 'r') as zip_ref:
1150
+ for member in zip_ref.namelist():
1151
+ basename = os.path.basename(member)
1152
+ if ('__MACOSX' not in member and
1153
+ not basename.startswith('._') and
1154
+ basename.lower().endswith(('.tif', '.tiff'))):
1155
+ zip_ref.extract(member, temp_dir)
1156
+
1157
+ tif_dir = find_valid_tif_dir(temp_dir)
1158
+ if tif_dir:
1159
+ first_frame = extract_first_frame(tif_dir)
1160
+ if first_frame:
1161
+ # ===== NORMALIZATION STARTS HERE =====
1162
+ try:
1163
+ img_np = tifffile.imread(first_frame)
1164
+
1165
+ # Normalize to [0, 255] uint8 range for display
1166
+ if img_np.dtype == np.uint8:
1167
+ pass # Already uint8
1168
+ elif img_np.dtype == np.uint16:
1169
+ # Normalize uint16 using actual min/max
1170
+ img_min, img_max = img_np.min(), img_np.max()
1171
+ if img_max > img_min:
1172
+ img_np = ((img_np.astype(np.float32) - img_min) / (img_max - img_min) * 255).astype(np.uint8)
1173
+ else:
1174
+ img_np = (img_np.astype(np.float32) / 65535.0 * 255).astype(np.uint8)
1175
+ else:
1176
+ # Float or other types
1177
+ img_np = img_np.astype(np.float32)
1178
+ img_min, img_max = img_np.min(), img_np.max()
1179
+ if img_max > img_min:
1180
+ img_np = ((img_np - img_min) / (img_max - img_min) * 255).astype(np.uint8)
1181
+ else:
1182
+ img_np = np.clip(img_np * 255, 0, 255).astype(np.uint8)
1183
+
1184
+ # Convert to RGB if grayscale
1185
+ if img_np.ndim == 2:
1186
+ img_np = np.stack([img_np]*3, axis=-1)
1187
+ elif img_np.ndim == 3 and img_np.shape[2] > 3:
1188
+ img_np = img_np[:, :, :3]
1189
+
1190
+ # Save normalized image to temp file
1191
+ temp_img = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
1192
+ Image.fromarray(img_np).save(temp_img.name)
1193
+
1194
+ print(f"✅ Loaded and normalized first frame: {first_frame}")
1195
+ print(f" Original dtype: {tifffile.imread(first_frame).dtype}")
1196
+ print(f" Normalized to uint8 RGB for annotation")
1197
+
1198
+ return temp_img.name
1199
+ # ===== NORMALIZATION ENDS HERE =====
1200
+ except Exception as e:
1201
+ print(f"⚠️ Error normalizing first frame: {e}")
1202
+ # Fallback to original file
1203
+ return first_frame
1204
+ except Exception as e:
1205
+ print(f"⚠️ Error loading first frame: {e}")
1206
+ pass
1207
+ return None
1208
 
1209
+ # Load first frame into annotator when ZIP uploaded
1210
+ track_zip_upload.change(
1211
+ fn=load_first_frame_for_annotation,
1212
+ inputs=track_zip_upload,
1213
+ outputs=track_first_frame_annotator
1214
+ )
1215
+
1216
+ # Run tracking
1217
  track_btn.click(
1218
  fn=track_video_handler,
1219
+ inputs=[track_use_box_radio, track_first_frame_annotator, track_zip_upload],
1220
+ outputs=[track_download, track_output, track_download, track_first_frame_preview]
1221
  )
1222
 
1223
  gr.Markdown(
 
1225
  ---
1226
  ### 💡 技术说明
1227
 
1228
+ **MicroscopyMatching** - 基于 Stable Diffusion 的显微图像分析工具套件
 
 
1229
  """
1230
  )
1231