Spaces:
Running on Zero
Running on Zero
Commit ·
88f9ed6
1
Parent(s): 5fb70b0
slide bar
Browse files
app.py
CHANGED
|
@@ -199,12 +199,106 @@ def colorize_mask(mask: np.ndarray, num_colors: int = 512) -> np.ndarray:
|
|
| 199 |
color_idx = mask % num_colors
|
| 200 |
return palette_arr[color_idx]
|
| 201 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
# @spaces.GPU
|
| 203 |
def segment_with_choice(use_box_choice, annot_value, overlay_alpha):
|
| 204 |
"""Segmentation handler - supports bounding box, returns colorized overlay and original mask path"""
|
| 205 |
if annot_value is None or len(annot_value) < 1:
|
| 206 |
print("❌ No annotation input")
|
| 207 |
-
return None, None
|
| 208 |
|
| 209 |
img_path = annot_value[0]
|
| 210 |
bboxes = annot_value[1] if len(annot_value) > 1 else []
|
|
@@ -223,7 +317,7 @@ def segment_with_choice(use_box_choice, annot_value, overlay_alpha):
|
|
| 223 |
print("📏 mask shape:", mask.shape, "dtype:", mask.dtype, "unique:", np.unique(mask))
|
| 224 |
except Exception as e:
|
| 225 |
print(f"❌ Inference failed: {str(e)}")
|
| 226 |
-
return None, None
|
| 227 |
|
| 228 |
temp_mask_file = tempfile.NamedTemporaryFile(delete=False, suffix=".tif")
|
| 229 |
mask_img = Image.fromarray(mask.astype(np.uint16))
|
|
@@ -235,7 +329,7 @@ def segment_with_choice(use_box_choice, annot_value, overlay_alpha):
|
|
| 235 |
print("📷 Image mode:", img.mode, "size:", img.size)
|
| 236 |
except Exception as e:
|
| 237 |
print(f"❌ Failed to open image: {e}")
|
| 238 |
-
return None, None
|
| 239 |
|
| 240 |
try:
|
| 241 |
img_rgb = img.convert("RGB").resize(mask.shape[::-1], resample=Image.BILINEAR)
|
|
@@ -244,7 +338,7 @@ def segment_with_choice(use_box_choice, annot_value, overlay_alpha):
|
|
| 244 |
img_np = img_np / 255.0
|
| 245 |
except Exception as e:
|
| 246 |
print(f"❌ Error in image conversion/resizing: {e}")
|
| 247 |
-
return None, None
|
| 248 |
|
| 249 |
mask_np = np.array(mask)
|
| 250 |
inst_mask = mask_np.astype(np.int32)
|
|
@@ -254,35 +348,18 @@ def segment_with_choice(use_box_choice, annot_value, overlay_alpha):
|
|
| 254 |
|
| 255 |
if num_instances == 0:
|
| 256 |
print("⚠️ No instance found, returning dummy red image")
|
| 257 |
-
return Image.new("RGB", mask.shape[::-1], (255, 0, 0)), None
|
| 258 |
-
|
| 259 |
-
overlay = img_np.copy()
|
| 260 |
-
alpha = float(np.clip(overlay_alpha, 0.0, 1.0))
|
| 261 |
-
|
| 262 |
-
for inst_id in np.unique(inst_mask):
|
| 263 |
-
if inst_id == 0:
|
| 264 |
-
continue
|
| 265 |
-
binary_mask = (inst_mask == inst_id).astype(np.uint8)
|
| 266 |
-
color = get_well_spaced_color(inst_id)
|
| 267 |
-
overlay[binary_mask == 1] = (1 - alpha) * overlay[binary_mask == 1] + alpha * color
|
| 268 |
-
|
| 269 |
-
contours = measure.find_contours(binary_mask, 0.5)
|
| 270 |
-
for contour in contours:
|
| 271 |
-
contour = contour.astype(np.int32)
|
| 272 |
-
valid_y = np.clip(contour[:, 0], 0, overlay.shape[0] - 1)
|
| 273 |
-
valid_x = np.clip(contour[:, 1], 0, overlay.shape[1] - 1)
|
| 274 |
-
overlay[valid_y, valid_x] = [1.0, 1.0, 0.0] # 黄色轮廓
|
| 275 |
-
|
| 276 |
-
overlay = np.clip(overlay * 255.0, 0, 255).astype(np.uint8)
|
| 277 |
|
| 278 |
-
|
|
|
|
|
|
|
| 279 |
|
| 280 |
|
| 281 |
# @spaces.GPU
|
| 282 |
def count_cells_handler(use_box_choice, annot_value, overlay_alpha):
|
| 283 |
"""Counting handler - supports bounding box, returns only density map"""
|
| 284 |
if annot_value is None or len(annot_value) < 1:
|
| 285 |
-
return None, "⚠️ Please provide an image."
|
| 286 |
|
| 287 |
image_path = annot_value[0]
|
| 288 |
bboxes = annot_value[1] if len(annot_value) > 1 else []
|
|
@@ -307,7 +384,7 @@ def count_cells_handler(use_box_choice, annot_value, overlay_alpha):
|
|
| 307 |
)
|
| 308 |
|
| 309 |
if 'error' in result:
|
| 310 |
-
return None, f"❌ Counting failed: {result['error']}"
|
| 311 |
|
| 312 |
count = result['count']
|
| 313 |
density_map = result['density_map']
|
|
@@ -321,7 +398,7 @@ def count_cells_handler(use_box_choice, annot_value, overlay_alpha):
|
|
| 321 |
print("📷 Image mode:", img.mode, "size:", img.size)
|
| 322 |
except Exception as e:
|
| 323 |
print(f"❌ Failed to open image: {e}")
|
| 324 |
-
return None, None
|
| 325 |
|
| 326 |
try:
|
| 327 |
img_rgb = img.convert("RGB").resize(density_map.shape[::-1], resample=Image.BILINEAR)
|
|
@@ -331,31 +408,14 @@ def count_cells_handler(use_box_choice, annot_value, overlay_alpha):
|
|
| 331 |
img_np = img_np / 255.0
|
| 332 |
except Exception as e:
|
| 333 |
print(f"❌ Error in image conversion/resizing: {e}")
|
| 334 |
-
return None, None
|
| 335 |
|
| 336 |
|
| 337 |
density_normalized = density_map.copy()
|
| 338 |
if density_normalized.max() > 0:
|
| 339 |
density_normalized = (density_normalized - density_normalized.min()) / (density_normalized.max() - density_normalized.min())
|
| 340 |
|
| 341 |
-
|
| 342 |
-
alpha = float(np.clip(overlay_alpha, 0.0, 1.0))
|
| 343 |
-
density_colored = cmap(density_normalized)[:, :, :3] # RGB only, ignore alpha
|
| 344 |
-
|
| 345 |
-
overlay = img_np.copy()
|
| 346 |
-
|
| 347 |
-
threshold = 0.01 # Only overlay where density > 1% of max
|
| 348 |
-
significant_mask = density_normalized > threshold
|
| 349 |
-
|
| 350 |
-
overlay[significant_mask] = (1 - alpha) * overlay[significant_mask] + alpha * density_colored[significant_mask]
|
| 351 |
-
|
| 352 |
-
# Clip and convert to uint8
|
| 353 |
-
overlay = np.clip(overlay * 255.0, 0, 255).astype(np.uint8)
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
result_text = f"✅ Detected {round(count)} objects"
|
| 360 |
if use_box_choice == "Yes" and box_array:
|
| 361 |
result_text += f"\n📦 Using bounding box: {box_array}"
|
|
@@ -363,14 +423,15 @@ def count_cells_handler(use_box_choice, annot_value, overlay_alpha):
|
|
| 363 |
|
| 364 |
print(f"✅ Counting done - Count: {count:.1f}")
|
| 365 |
|
| 366 |
-
|
|
|
|
| 367 |
|
| 368 |
|
| 369 |
except Exception as e:
|
| 370 |
print(f"❌ Counting error: {e}")
|
| 371 |
import traceback
|
| 372 |
traceback.print_exc()
|
| 373 |
-
return None, f"❌ Counting failed: {str(e)}"
|
| 374 |
|
| 375 |
|
| 376 |
def find_tif_dir(root_dir):
|
|
@@ -664,7 +725,7 @@ def create_tracking_visualization(tif_dir, output_dir, valid_tif_files, overlay_
|
|
| 664 |
return valid_tif_files[0]
|
| 665 |
|
| 666 |
# @spaces.GPU
|
| 667 |
-
def track_video_handler(use_box_choice, first_frame_annot, zip_file_obj, overlay_alpha):
|
| 668 |
"""
|
| 669 |
Tracking handler - processes a ZIP of TIF frames, supports bounding box, returns visualization and results ZIP
|
| 670 |
|
|
@@ -678,8 +739,9 @@ def track_video_handler(use_box_choice, first_frame_annot, zip_file_obj, overlay
|
|
| 678 |
Uploaded ZIP file containing TIF sequence
|
| 679 |
"""
|
| 680 |
if zip_file_obj is None:
|
| 681 |
-
return None, "⚠️ Please upload a ZIP file containing video frames (.zip)", None, None
|
| 682 |
|
|
|
|
| 683 |
temp_dir = None
|
| 684 |
output_temp_dir = None
|
| 685 |
|
|
@@ -727,7 +789,7 @@ def track_video_handler(use_box_choice, first_frame_annot, zip_file_obj, overlay
|
|
| 727 |
tif_dir = find_valid_tif_dir(temp_dir)
|
| 728 |
|
| 729 |
if tif_dir is None:
|
| 730 |
-
return None, "❌ Did not find valid TIF directory", None, None
|
| 731 |
|
| 732 |
# Validate TIFF files
|
| 733 |
tif_files = natsorted(glob(os.path.join(tif_dir, "*.tif")) +
|
|
@@ -736,7 +798,7 @@ def track_video_handler(use_box_choice, first_frame_annot, zip_file_obj, overlay
|
|
| 736 |
if not os.path.basename(f).startswith('._') and is_valid_tiff(f)]
|
| 737 |
|
| 738 |
if len(valid_tif_files) == 0:
|
| 739 |
-
return None, "❌ Did not find valid TIF files", None, None
|
| 740 |
|
| 741 |
print(f"📈 Using {len(valid_tif_files)} TIF files")
|
| 742 |
|
|
@@ -757,7 +819,7 @@ def track_video_handler(use_box_choice, first_frame_annot, zip_file_obj, overlay
|
|
| 757 |
)
|
| 758 |
|
| 759 |
if 'error' in result:
|
| 760 |
-
return None, f"❌ Tracking failed: {result['error']}", None, None
|
| 761 |
|
| 762 |
# Create visualization video of tracked objects
|
| 763 |
print("\n🎬 Creating tracking visualization...")
|
|
@@ -805,18 +867,17 @@ def track_video_handler(use_box_choice, first_frame_annot, zip_file_obj, overlay
|
|
| 805 |
|
| 806 |
print(f"\n✅ Tracking completed")
|
| 807 |
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
return results_zip, result_text, gr.update(visible=True), tracking_video
|
| 817 |
|
| 818 |
except zipfile.BadZipFile:
|
| 819 |
-
return None, "❌ Not a valid ZIP file", None, None
|
| 820 |
except Exception as e:
|
| 821 |
import traceback
|
| 822 |
traceback.print_exc()
|
|
@@ -828,7 +889,7 @@ def track_video_handler(use_box_choice, first_frame_annot, zip_file_obj, overlay
|
|
| 828 |
shutil.rmtree(d)
|
| 829 |
except:
|
| 830 |
pass
|
| 831 |
-
return None, f"❌ Tracking failed: {str(e)}", None, None
|
| 832 |
|
| 833 |
|
| 834 |
|
|
@@ -890,6 +951,9 @@ with gr.Blocks(
|
|
| 890 |
# 全局状态
|
| 891 |
current_query_id = gr.State(str(uuid.uuid4()))
|
| 892 |
user_uploaded_examples = gr.State(example_images_seg.copy())
|
|
|
|
|
|
|
|
|
|
| 893 |
|
| 894 |
with gr.Tabs():
|
| 895 |
# ===== Tab 1: Segmentation =====
|
|
@@ -992,14 +1056,19 @@ with gr.Blocks(
|
|
| 992 |
run_seg_btn.click(
|
| 993 |
fn=segment_with_choice,
|
| 994 |
inputs=[use_box_radio, annotator, seg_alpha_slider],
|
| 995 |
-
outputs=[seg_output, download_mask_btn]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 996 |
)
|
| 997 |
|
| 998 |
# click event for clear button
|
| 999 |
clear_btn.click(
|
| 1000 |
-
fn=lambda: None,
|
| 1001 |
inputs=None,
|
| 1002 |
-
outputs=annotator
|
| 1003 |
)
|
| 1004 |
|
| 1005 |
# init Gallery with example images
|
|
@@ -1220,14 +1289,19 @@ with gr.Blocks(
|
|
| 1220 |
count_btn.click(
|
| 1221 |
fn=count_cells_handler,
|
| 1222 |
inputs=[count_use_box_radio, count_annotator, count_alpha_slider],
|
| 1223 |
-
outputs=[count_output, download_density_btn, count_status]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1224 |
)
|
| 1225 |
|
| 1226 |
# Clear selection
|
| 1227 |
clear_btn.click(
|
| 1228 |
-
fn=lambda: None,
|
| 1229 |
inputs=None,
|
| 1230 |
-
outputs=count_annotator
|
| 1231 |
)
|
| 1232 |
|
| 1233 |
# Submit feedback
|
|
@@ -1548,15 +1622,20 @@ with gr.Blocks(
|
|
| 1548 |
# Run tracking
|
| 1549 |
track_btn.click(
|
| 1550 |
fn=track_video_handler,
|
| 1551 |
-
inputs=[track_use_box_radio, track_first_frame_annotator, track_zip_upload, track_alpha_slider],
|
| 1552 |
-
outputs=[track_download, track_output, track_download, track_first_frame_preview]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1553 |
)
|
| 1554 |
|
| 1555 |
# Clear selection
|
| 1556 |
clear_btn.click(
|
| 1557 |
-
fn=lambda: None,
|
| 1558 |
inputs=None,
|
| 1559 |
-
outputs=track_first_frame_annotator
|
| 1560 |
)
|
| 1561 |
|
| 1562 |
# Submit feedback
|
|
|
|
| 199 |
color_idx = mask % num_colors
|
| 200 |
return palette_arr[color_idx]
|
| 201 |
|
| 202 |
+
|
| 203 |
+
def render_seg_overlay(img_np, inst_mask, overlay_alpha):
|
| 204 |
+
"""Render segmentation overlay from cached image/mask."""
|
| 205 |
+
if img_np is None or inst_mask is None:
|
| 206 |
+
return None
|
| 207 |
+
|
| 208 |
+
overlay = img_np.copy()
|
| 209 |
+
alpha = float(np.clip(overlay_alpha, 0.0, 1.0))
|
| 210 |
+
|
| 211 |
+
for inst_id in np.unique(inst_mask):
|
| 212 |
+
if inst_id == 0:
|
| 213 |
+
continue
|
| 214 |
+
binary_mask = (inst_mask == inst_id).astype(np.uint8)
|
| 215 |
+
color = get_well_spaced_color(inst_id)
|
| 216 |
+
overlay[binary_mask == 1] = (1 - alpha) * overlay[binary_mask == 1] + alpha * color
|
| 217 |
+
|
| 218 |
+
contours = measure.find_contours(binary_mask, 0.5)
|
| 219 |
+
for contour in contours:
|
| 220 |
+
contour = contour.astype(np.int32)
|
| 221 |
+
valid_y = np.clip(contour[:, 0], 0, overlay.shape[0] - 1)
|
| 222 |
+
valid_x = np.clip(contour[:, 1], 0, overlay.shape[1] - 1)
|
| 223 |
+
overlay[valid_y, valid_x] = [1.0, 1.0, 0.0]
|
| 224 |
+
|
| 225 |
+
overlay = np.clip(overlay * 255.0, 0, 255).astype(np.uint8)
|
| 226 |
+
return Image.fromarray(overlay)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def render_count_overlay(img_np, density_normalized, overlay_alpha):
|
| 230 |
+
"""Render counting heatmap overlay from cached image/density."""
|
| 231 |
+
if img_np is None or density_normalized is None:
|
| 232 |
+
return None
|
| 233 |
+
|
| 234 |
+
alpha = float(np.clip(overlay_alpha, 0.0, 1.0))
|
| 235 |
+
cmap = cm.get_cmap("jet")
|
| 236 |
+
density_colored = cmap(density_normalized)[:, :, :3]
|
| 237 |
+
|
| 238 |
+
overlay = img_np.copy()
|
| 239 |
+
threshold = 0.01
|
| 240 |
+
significant_mask = density_normalized > threshold
|
| 241 |
+
overlay[significant_mask] = (1 - alpha) * overlay[significant_mask] + alpha * density_colored[significant_mask]
|
| 242 |
+
overlay = np.clip(overlay * 255.0, 0, 255).astype(np.uint8)
|
| 243 |
+
return Image.fromarray(overlay)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def update_seg_overlay_alpha(overlay_alpha, seg_vis_cache):
|
| 247 |
+
"""Live update segmentation visualization without rerunning inference."""
|
| 248 |
+
if not seg_vis_cache:
|
| 249 |
+
return None
|
| 250 |
+
return render_seg_overlay(seg_vis_cache.get("img_np"), seg_vis_cache.get("inst_mask"), overlay_alpha)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def update_count_overlay_alpha(overlay_alpha, count_vis_cache):
|
| 254 |
+
"""Live update counting visualization without rerunning inference."""
|
| 255 |
+
if not count_vis_cache:
|
| 256 |
+
return None
|
| 257 |
+
return render_count_overlay(count_vis_cache.get("img_np"), count_vis_cache.get("density_normalized"), overlay_alpha)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def update_tracking_overlay_alpha(overlay_alpha, track_vis_cache):
|
| 261 |
+
"""Regenerate tracking visualization at new opacity using cached outputs."""
|
| 262 |
+
if not track_vis_cache:
|
| 263 |
+
return None
|
| 264 |
+
|
| 265 |
+
tif_dir = track_vis_cache.get("tif_dir")
|
| 266 |
+
output_dir = track_vis_cache.get("output_dir")
|
| 267 |
+
valid_tif_files = track_vis_cache.get("valid_tif_files")
|
| 268 |
+
if not tif_dir or not output_dir or not valid_tif_files:
|
| 269 |
+
return None
|
| 270 |
+
|
| 271 |
+
try:
|
| 272 |
+
return create_tracking_visualization(
|
| 273 |
+
tif_dir=tif_dir,
|
| 274 |
+
output_dir=output_dir,
|
| 275 |
+
valid_tif_files=valid_tif_files,
|
| 276 |
+
overlay_alpha=overlay_alpha
|
| 277 |
+
)
|
| 278 |
+
except Exception as e:
|
| 279 |
+
print(f"⚠️ Failed to update tracking opacity: {e}")
|
| 280 |
+
return None
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def cleanup_tracking_cache(track_vis_cache):
|
| 284 |
+
"""Delete cached tracking temp directories from the previous run."""
|
| 285 |
+
if not track_vis_cache:
|
| 286 |
+
return
|
| 287 |
+
for key in ["input_temp_dir", "output_dir"]:
|
| 288 |
+
path = track_vis_cache.get(key)
|
| 289 |
+
if path and os.path.isdir(path):
|
| 290 |
+
try:
|
| 291 |
+
shutil.rmtree(path)
|
| 292 |
+
except Exception:
|
| 293 |
+
pass
|
| 294 |
+
|
| 295 |
+
|
| 296 |
# @spaces.GPU
|
| 297 |
def segment_with_choice(use_box_choice, annot_value, overlay_alpha):
|
| 298 |
"""Segmentation handler - supports bounding box, returns colorized overlay and original mask path"""
|
| 299 |
if annot_value is None or len(annot_value) < 1:
|
| 300 |
print("❌ No annotation input")
|
| 301 |
+
return None, None, {}
|
| 302 |
|
| 303 |
img_path = annot_value[0]
|
| 304 |
bboxes = annot_value[1] if len(annot_value) > 1 else []
|
|
|
|
| 317 |
print("📏 mask shape:", mask.shape, "dtype:", mask.dtype, "unique:", np.unique(mask))
|
| 318 |
except Exception as e:
|
| 319 |
print(f"❌ Inference failed: {str(e)}")
|
| 320 |
+
return None, None, {}
|
| 321 |
|
| 322 |
temp_mask_file = tempfile.NamedTemporaryFile(delete=False, suffix=".tif")
|
| 323 |
mask_img = Image.fromarray(mask.astype(np.uint16))
|
|
|
|
| 329 |
print("📷 Image mode:", img.mode, "size:", img.size)
|
| 330 |
except Exception as e:
|
| 331 |
print(f"❌ Failed to open image: {e}")
|
| 332 |
+
return None, None, {}
|
| 333 |
|
| 334 |
try:
|
| 335 |
img_rgb = img.convert("RGB").resize(mask.shape[::-1], resample=Image.BILINEAR)
|
|
|
|
| 338 |
img_np = img_np / 255.0
|
| 339 |
except Exception as e:
|
| 340 |
print(f"❌ Error in image conversion/resizing: {e}")
|
| 341 |
+
return None, None, {}
|
| 342 |
|
| 343 |
mask_np = np.array(mask)
|
| 344 |
inst_mask = mask_np.astype(np.int32)
|
|
|
|
| 348 |
|
| 349 |
if num_instances == 0:
|
| 350 |
print("⚠️ No instance found, returning dummy red image")
|
| 351 |
+
return Image.new("RGB", mask.shape[::-1], (255, 0, 0)), None, {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
|
| 353 |
+
overlay_img = render_seg_overlay(img_np, inst_mask, overlay_alpha)
|
| 354 |
+
seg_vis_cache = {"img_np": img_np, "inst_mask": inst_mask}
|
| 355 |
+
return overlay_img, temp_mask_file.name, seg_vis_cache
|
| 356 |
|
| 357 |
|
| 358 |
# @spaces.GPU
|
| 359 |
def count_cells_handler(use_box_choice, annot_value, overlay_alpha):
|
| 360 |
"""Counting handler - supports bounding box, returns only density map"""
|
| 361 |
if annot_value is None or len(annot_value) < 1:
|
| 362 |
+
return None, None, "⚠️ Please provide an image.", {}
|
| 363 |
|
| 364 |
image_path = annot_value[0]
|
| 365 |
bboxes = annot_value[1] if len(annot_value) > 1 else []
|
|
|
|
| 384 |
)
|
| 385 |
|
| 386 |
if 'error' in result:
|
| 387 |
+
return None, None, f"❌ Counting failed: {result['error']}", {}
|
| 388 |
|
| 389 |
count = result['count']
|
| 390 |
density_map = result['density_map']
|
|
|
|
| 398 |
print("📷 Image mode:", img.mode, "size:", img.size)
|
| 399 |
except Exception as e:
|
| 400 |
print(f"❌ Failed to open image: {e}")
|
| 401 |
+
return None, None, f"❌ Failed to open image: {str(e)}", {}
|
| 402 |
|
| 403 |
try:
|
| 404 |
img_rgb = img.convert("RGB").resize(density_map.shape[::-1], resample=Image.BILINEAR)
|
|
|
|
| 408 |
img_np = img_np / 255.0
|
| 409 |
except Exception as e:
|
| 410 |
print(f"❌ Error in image conversion/resizing: {e}")
|
| 411 |
+
return None, None, f"❌ Error in image conversion/resizing: {str(e)}", {}
|
| 412 |
|
| 413 |
|
| 414 |
density_normalized = density_map.copy()
|
| 415 |
if density_normalized.max() > 0:
|
| 416 |
density_normalized = (density_normalized - density_normalized.min()) / (density_normalized.max() - density_normalized.min())
|
| 417 |
|
| 418 |
+
overlay_img = render_count_overlay(img_np, density_normalized, overlay_alpha)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
result_text = f"✅ Detected {round(count)} objects"
|
| 420 |
if use_box_choice == "Yes" and box_array:
|
| 421 |
result_text += f"\n📦 Using bounding box: {box_array}"
|
|
|
|
| 423 |
|
| 424 |
print(f"✅ Counting done - Count: {count:.1f}")
|
| 425 |
|
| 426 |
+
count_vis_cache = {"img_np": img_np, "density_normalized": density_normalized}
|
| 427 |
+
return overlay_img, temp_density_file.name, result_text, count_vis_cache
|
| 428 |
|
| 429 |
|
| 430 |
except Exception as e:
|
| 431 |
print(f"❌ Counting error: {e}")
|
| 432 |
import traceback
|
| 433 |
traceback.print_exc()
|
| 434 |
+
return None, None, f"❌ Counting failed: {str(e)}", {}
|
| 435 |
|
| 436 |
|
| 437 |
def find_tif_dir(root_dir):
|
|
|
|
| 725 |
return valid_tif_files[0]
|
| 726 |
|
| 727 |
# @spaces.GPU
|
| 728 |
+
def track_video_handler(use_box_choice, first_frame_annot, zip_file_obj, overlay_alpha, prev_track_vis_cache):
|
| 729 |
"""
|
| 730 |
Tracking handler - processes a ZIP of TIF frames, supports bounding box, returns visualization and results ZIP
|
| 731 |
|
|
|
|
| 739 |
Uploaded ZIP file containing TIF sequence
|
| 740 |
"""
|
| 741 |
if zip_file_obj is None:
|
| 742 |
+
return None, "⚠️ Please upload a ZIP file containing video frames (.zip)", None, None, {}
|
| 743 |
|
| 744 |
+
cleanup_tracking_cache(prev_track_vis_cache)
|
| 745 |
temp_dir = None
|
| 746 |
output_temp_dir = None
|
| 747 |
|
|
|
|
| 789 |
tif_dir = find_valid_tif_dir(temp_dir)
|
| 790 |
|
| 791 |
if tif_dir is None:
|
| 792 |
+
return None, "❌ Did not find valid TIF directory", None, None, {}
|
| 793 |
|
| 794 |
# Validate TIFF files
|
| 795 |
tif_files = natsorted(glob(os.path.join(tif_dir, "*.tif")) +
|
|
|
|
| 798 |
if not os.path.basename(f).startswith('._') and is_valid_tiff(f)]
|
| 799 |
|
| 800 |
if len(valid_tif_files) == 0:
|
| 801 |
+
return None, "❌ Did not find valid TIF files", None, None, {}
|
| 802 |
|
| 803 |
print(f"📈 Using {len(valid_tif_files)} TIF files")
|
| 804 |
|
|
|
|
| 819 |
)
|
| 820 |
|
| 821 |
if 'error' in result:
|
| 822 |
+
return None, f"❌ Tracking failed: {result['error']}", None, None, {}
|
| 823 |
|
| 824 |
# Create visualization video of tracked objects
|
| 825 |
print("\n🎬 Creating tracking visualization...")
|
|
|
|
| 867 |
|
| 868 |
print(f"\n✅ Tracking completed")
|
| 869 |
|
| 870 |
+
track_vis_cache = {
|
| 871 |
+
"tif_dir": tif_dir,
|
| 872 |
+
"valid_tif_files": valid_tif_files,
|
| 873 |
+
"output_dir": output_temp_dir,
|
| 874 |
+
"input_temp_dir": temp_dir,
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
return results_zip, result_text, gr.update(visible=True), tracking_video, track_vis_cache
|
|
|
|
| 878 |
|
| 879 |
except zipfile.BadZipFile:
|
| 880 |
+
return None, "❌ Not a valid ZIP file", None, None, {}
|
| 881 |
except Exception as e:
|
| 882 |
import traceback
|
| 883 |
traceback.print_exc()
|
|
|
|
| 889 |
shutil.rmtree(d)
|
| 890 |
except:
|
| 891 |
pass
|
| 892 |
+
return None, f"❌ Tracking failed: {str(e)}", None, None, {}
|
| 893 |
|
| 894 |
|
| 895 |
|
|
|
|
| 951 |
# 全局状态
|
| 952 |
current_query_id = gr.State(str(uuid.uuid4()))
|
| 953 |
user_uploaded_examples = gr.State(example_images_seg.copy())
|
| 954 |
+
seg_vis_state = gr.State({})
|
| 955 |
+
count_vis_state = gr.State({})
|
| 956 |
+
track_vis_state = gr.State({})
|
| 957 |
|
| 958 |
with gr.Tabs():
|
| 959 |
# ===== Tab 1: Segmentation =====
|
|
|
|
| 1056 |
run_seg_btn.click(
|
| 1057 |
fn=segment_with_choice,
|
| 1058 |
inputs=[use_box_radio, annotator, seg_alpha_slider],
|
| 1059 |
+
outputs=[seg_output, download_mask_btn, seg_vis_state]
|
| 1060 |
+
)
|
| 1061 |
+
seg_alpha_slider.input(
|
| 1062 |
+
fn=update_seg_overlay_alpha,
|
| 1063 |
+
inputs=[seg_alpha_slider, seg_vis_state],
|
| 1064 |
+
outputs=seg_output
|
| 1065 |
)
|
| 1066 |
|
| 1067 |
# click event for clear button
|
| 1068 |
clear_btn.click(
|
| 1069 |
+
fn=lambda: (None, {}),
|
| 1070 |
inputs=None,
|
| 1071 |
+
outputs=[annotator, seg_vis_state]
|
| 1072 |
)
|
| 1073 |
|
| 1074 |
# init Gallery with example images
|
|
|
|
| 1289 |
count_btn.click(
|
| 1290 |
fn=count_cells_handler,
|
| 1291 |
inputs=[count_use_box_radio, count_annotator, count_alpha_slider],
|
| 1292 |
+
outputs=[count_output, download_density_btn, count_status, count_vis_state]
|
| 1293 |
+
)
|
| 1294 |
+
count_alpha_slider.input(
|
| 1295 |
+
fn=update_count_overlay_alpha,
|
| 1296 |
+
inputs=[count_alpha_slider, count_vis_state],
|
| 1297 |
+
outputs=count_output
|
| 1298 |
)
|
| 1299 |
|
| 1300 |
# Clear selection
|
| 1301 |
clear_btn.click(
|
| 1302 |
+
fn=lambda: (None, {}),
|
| 1303 |
inputs=None,
|
| 1304 |
+
outputs=[count_annotator, count_vis_state]
|
| 1305 |
)
|
| 1306 |
|
| 1307 |
# Submit feedback
|
|
|
|
| 1622 |
# Run tracking
|
| 1623 |
track_btn.click(
|
| 1624 |
fn=track_video_handler,
|
| 1625 |
+
inputs=[track_use_box_radio, track_first_frame_annotator, track_zip_upload, track_alpha_slider, track_vis_state],
|
| 1626 |
+
outputs=[track_download, track_output, track_download, track_first_frame_preview, track_vis_state]
|
| 1627 |
+
)
|
| 1628 |
+
track_alpha_slider.change(
|
| 1629 |
+
fn=update_tracking_overlay_alpha,
|
| 1630 |
+
inputs=[track_alpha_slider, track_vis_state],
|
| 1631 |
+
outputs=track_first_frame_preview
|
| 1632 |
)
|
| 1633 |
|
| 1634 |
# Clear selection
|
| 1635 |
clear_btn.click(
|
| 1636 |
+
fn=lambda: (None, {}),
|
| 1637 |
inputs=None,
|
| 1638 |
+
outputs=[track_first_frame_annotator, track_vis_state]
|
| 1639 |
)
|
| 1640 |
|
| 1641 |
# Submit feedback
|