Spaces:
Sleeping
Sleeping
phoebehxf
commited on
Commit
·
f6846ac
1
Parent(s):
e429374
add tracking display
Browse files
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
|
| 357 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 367 |
-
|
| 368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
if tif_dir is None:
|
| 370 |
-
return None, "❌
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
|
| 372 |
-
|
|
|
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
result = run_track(
|
| 375 |
TRACK_MODEL,
|
| 376 |
video_dir=tif_dir,
|
| 377 |
-
box=
|
| 378 |
device=TRACK_DEVICE,
|
| 379 |
-
output_dir=
|
| 380 |
)
|
| 381 |
|
| 382 |
if 'error' in result:
|
| 383 |
-
return None, f"❌ 跟踪失败: {result['error']}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
|
| 385 |
-
|
|
|
|
|
|
|
| 386 |
|
| 387 |
result_text = f"""✅ 跟踪完成!
|
| 388 |
|
| 389 |
-
|
| 390 |
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
|
| 399 |
except zipfile.BadZipFile:
|
| 400 |
-
return None, "❌
|
| 401 |
except Exception as e:
|
| 402 |
import traceback
|
| 403 |
traceback.print_exc()
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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`
|
| 701 |
-
2.
|
| 702 |
-
3.
|
| 703 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
)
|
| 705 |
|
| 706 |
with gr.Column(scale=2):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
track_output = gr.Textbox(
|
| 708 |
label="📊 跟踪信息",
|
| 709 |
-
lines=
|
| 710 |
interactive=False
|
| 711 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
|
| 713 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
track_btn.click(
|
| 715 |
fn=track_video_handler,
|
| 716 |
-
inputs=track_zip_upload,
|
| 717 |
-
outputs=[
|
| 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 |
-
|
| 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 |
|