VisionLanguageGroup commited on
Commit
f129ffb
Β·
1 Parent(s): 5c4d9d8

update on ui

Browse files
Files changed (3) hide show
  1. _utils/image_io.py +20 -0
  2. app.py +869 -475
  3. tracking_one.py +10 -0
_utils/image_io.py CHANGED
@@ -144,6 +144,26 @@ def _shape_axes(path):
144
  return (h, w), "YX"
145
 
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  def _plan_axes(shape, axes):
148
  """Drop size-1 axes, infer a channel axis when the file names none, and move
149
  it last.
 
144
  return (h, w), "YX"
145
 
146
 
147
+ _PIL_BITS = {"1": 1, "L": 8, "P": 8, "LA": 8, "PA": 8, "RGB": 8, "RGBA": 8,
148
+ "CMYK": 8, "YCbCr": 8, "LAB": 8, "HSV": 8,
149
+ "I;16": 16, "I;16B": 16, "I;16L": 16, "I": 32, "F": 32}
150
+
151
+
152
+ def bit_depth(path):
153
+ """Bits per sample, read from metadata only (0 if it cannot be determined)."""
154
+ if tifffile is not None:
155
+ try:
156
+ with _open_tiff(path) as tif:
157
+ return int(np.dtype(tif.series[0].dtype).itemsize) * 8
158
+ except Exception: # noqa: BLE001 - fall through to PIL
159
+ pass
160
+ try:
161
+ with Image.open(path) as img:
162
+ return int(_PIL_BITS.get(img.mode, 0))
163
+ except Exception: # noqa: BLE001
164
+ return 0
165
+
166
+
167
  def _plan_axes(shape, axes):
168
  """Drop size-1 axes, infer a channel axis when the file names none, and move
169
  it last.
app.py CHANGED
@@ -11,6 +11,7 @@ import uuid
11
  from pathlib import Path
12
  import tempfile
13
  import zipfile
 
14
  from skimage import measure
15
  from matplotlib import cm
16
  from glob import glob
@@ -22,7 +23,7 @@ from inference_seg import load_model as load_seg_model, run as run_seg
22
  from inference_count import load_model as load_count_model, run as run_count
23
  from inference_track import load_model as load_track_model, run as run_track
24
  from _utils.image_io import (
25
- standardize_image, inspect_image, image_size, array_nbytes, pixel_stats,
26
  RECOMMENDED_SIZE, WARN_SIZE, MAX_SIZE, MIN_SIZE, MAX_READ_BYTES, TIFF_EXTENSIONS,
27
  )
28
 
@@ -1063,16 +1064,16 @@ def track_video_handler(use_box_choice, first_frame_annot, zip_file_obj, overlay
1063
  if box_array:
1064
  bbox_info = f"\nπŸ”² Using bounding box: [{box_array[0][0]}, {box_array[0][1]}, {box_array[0][2]}, {box_array[0][3]}]"
1065
 
1066
- result_text = f"""βœ… Tracking completed!
1067
-
1068
- πŸ–ΌοΈ Processed frames: {len(valid_tif_files)}{bbox_info}
1069
-
1070
- πŸ“₯ Click the button below to download CTC-format results
1071
- The results include:
1072
- - res_track.txt (CTC-format tracking data)
1073
- - Other tracking-related files
1074
- - README.txt (Results description)
1075
- """
1076
 
1077
  if use_box_choice == "Yes" and box_array:
1078
  result_text += f"\nπŸ“¦ Using bounding box: {box_array}"
@@ -1110,34 +1111,70 @@ example_images_seg = [f for f in glob("example_imgs/seg/*")]
1110
  example_images_cnt = [f for f in glob("example_imgs/cnt/*")]
1111
  example_tracking_zips = [f for f in glob("example_imgs/tra/*.zip")]
1112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1113
  # ===== Gradio UI =====
1114
  CSS = """
1115
  /* ── Layout ──────────────────────────────────────────── */
1116
  .gradio-container {
1117
- max-width: 1380px !important;
1118
  margin: 0 auto !important;
1119
  font-family: 'Inter', 'Segoe UI', system-ui, sans-serif !important;
 
1120
  }
1121
 
1122
  /* ── Header markdown polish ───────────────────────────── */
1123
  .gradio-container .prose h1 {
1124
- font-size: 2rem !important;
1125
  font-weight: 700 !important;
1126
- color: #1e293b !important;
1127
- letter-spacing: -0.5px !important;
1128
- margin-bottom: 10px !important;
1129
  }
1130
  .gradio-container .prose h3 {
1131
  font-size: 1rem !important;
1132
  font-weight: 600 !important;
1133
- color: #0284c7 !important;
1134
  margin-top: 14px !important;
1135
  margin-bottom: 4px !important;
1136
  }
1137
  .gradio-container .prose p {
1138
  margin-top: 4px !important;
1139
  margin-bottom: 6px !important;
1140
- color: #475569 !important;
1141
  line-height: 1.7 !important;
1142
  }
1143
  .gradio-container .prose ul,
@@ -1146,79 +1183,145 @@ CSS = """
1146
  margin-bottom: 6px !important;
1147
  }
1148
  .gradio-container .prose li {
1149
- color: #475569 !important;
1150
  line-height: 1.7 !important;
1151
  }
1152
 
1153
- /* ── Top-level header section ─────────────────────────── */
1154
  .gradio-container > .gap > .prose:first-child {
1155
- background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 50%, #f0fdf4 100%) !important;
1156
- border: 1px solid #bae6fd !important;
1157
- border-radius: 16px !important;
1158
- padding: 28px 36px !important;
1159
- margin-bottom: 20px !important;
1160
- box-shadow: 0 4px 20px rgba(14,165,233,0.08) !important;
1161
  }
1162
 
1163
- /* ── Tabs ────────────────────────────────────────────── */
1164
- .tabs > .tab-nav {
1165
- border-bottom: 2px solid #e2e8f0 !important;
 
 
 
 
1166
  margin-bottom: 20px !important;
1167
- gap: 4px !important;
1168
  }
1169
- .tabs button {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1170
  font-size: 15px !important;
1171
- font-weight: 600 !important;
1172
- padding: 11px 24px !important;
1173
- border-radius: 8px 8px 0 0 !important;
1174
- color: #64748b !important;
1175
- transition: color 0.15s, background 0.15s !important;
1176
- }
1177
- .tabs button:hover {
1178
- color: #0ea5e9 !important;
1179
- background: #f0f9ff !important;
1180
- }
1181
- .tabs button.selected {
1182
- color: #0284c7 !important;
1183
- border-bottom: 3px solid #0284c7 !important;
1184
- background: transparent !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1185
  }
1186
 
1187
  /* ── Buttons ─────────────────────────────────────────── */
1188
  button.primary {
1189
- background: linear-gradient(135deg, #0284c7 0%, #0ea5e9 100%) !important;
1190
  border: none !important;
1191
- border-radius: 10px !important;
1192
  color: #fff !important;
1193
  font-weight: 600 !important;
1194
  font-size: 15px !important;
1195
- box-shadow: 0 3px 12px rgba(14,165,233,0.35) !important;
1196
- transition: transform 0.12s ease, box-shadow 0.15s ease !important;
1197
  }
1198
  button.primary:hover {
1199
- transform: translateY(-2px) !important;
1200
- box-shadow: 0 6px 20px rgba(14,165,233,0.45) !important;
1201
  }
1202
  button.secondary {
1203
- border-radius: 10px !important;
1204
  font-weight: 500 !important;
1205
- border: 1.5px solid #cbd5e1 !important;
1206
- color: #475569 !important;
1207
  transition: border-color 0.12s, color 0.12s, background 0.12s !important;
1208
  }
1209
  button.secondary:hover {
1210
- border-color: #94a3b8 !important;
1211
- color: #1e293b !important;
1212
- background: #f8fafc !important;
1213
  }
1214
 
1215
  /* ── Blocks and panels ───────────────────────────────── */
1216
- .gradio-container .block { border-radius: 14px !important; }
1217
  .gradio-container .gr-form,
1218
  .gradio-container .gr-box,
1219
  .gradio-container .gr-panel {
1220
- border-radius: 14px !important;
1221
- border-color: #e2e8f0 !important;
1222
  }
1223
 
1224
  /* ── Labels ──────────────────────────────────────────── */
@@ -1288,14 +1391,27 @@ label { font-weight: 500 !important; color: #374151 !important; }
1288
  display: flex !important;
1289
  align-items: center !important;
1290
  justify-content: center !important;
1291
- border-radius: 12px !important;
1292
- background: #f8fafc !important;
1293
  }
1294
  .uniform-height img, .uniform-height canvas {
1295
  max-height: 480px !important;
1296
  object-fit: contain !important;
1297
  }
1298
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1299
  /* ── Density map output ──────────────────────────────── */
1300
  #density_map_output { height: 480px !important; }
1301
  #density_map_output .image-container { height: 480px !important; }
@@ -1306,153 +1422,432 @@ label { font-weight: 500 !important; color: #374151 !important; }
1306
  object-fit: contain !important;
1307
  }
1308
 
1309
- /* ── Tab content description markdown ───────────────── */
1310
- .tabitem .prose h2 {
1311
- font-size: 1.3rem !important;
1312
- font-weight: 700 !important;
1313
- color: #1e293b !important;
1314
- margin-top: 0 !important;
1315
- margin-bottom: 10px !important;
1316
- padding-bottom: 8px !important;
1317
- border-bottom: 2px solid #e0f2fe !important;
1318
  }
1319
- .tabitem .prose:nth-child(2) {
1320
- background: #f8fafc !important;
1321
- border: 1px solid #e2e8f0 !important;
1322
- border-radius: 10px !important;
1323
- padding: 12px 18px !important;
1324
- margin-bottom: 16px !important;
1325
  }
1326
- .tabitem .prose:nth-child(2) p,
1327
- .tabitem .prose:nth-child(2) li {
1328
- font-size: 0.91rem !important;
1329
- color: #64748b !important;
1330
  }
1331
- .tabitem .prose:nth-child(2) strong {
1332
- color: #0f172a !important;
 
 
 
 
 
1333
  }
1334
 
1335
- /* ════════════════════════════════════════════════════════
1336
- DARK MODE (.dark is added to <html> by Gradio)
1337
- ════════════════════════════════════════════════════════ */
1338
-
1339
- /* ── Header text ─────────────────────────────────────── */
1340
- .dark .gradio-container .prose h1 {
1341
- color: #e2e8f0 !important;
1342
  }
1343
- .dark .gradio-container .prose h3 {
1344
- color: #38bdf8 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1345
  }
1346
- .dark .gradio-container .prose p,
1347
- .dark .gradio-container .prose li {
1348
- color: #94a3b8 !important;
 
 
 
 
 
 
1349
  }
1350
 
1351
- /* ── Top-level header card ───────────────────────────── */
1352
- .dark .gradio-container > .gap > .prose:first-child {
1353
- background: linear-gradient(135deg, #0c1a2e 0%, #0f2942 50%, #0d1f12 100%) !important;
1354
- border-color: #1e3a5f !important;
1355
- box-shadow: 0 4px 20px rgba(0,0,0,0.4) !important;
1356
  }
 
 
1357
 
1358
- /* ── Tabs ────────────────────────────────────────────── */
1359
- .dark .tabs > .tab-nav {
1360
- border-bottom-color: #334155 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1361
  }
1362
- .dark .tabs button {
1363
- color: #94a3b8 !important;
 
 
 
 
 
 
 
1364
  }
1365
- .dark .tabs button:hover {
1366
- color: #38bdf8 !important;
1367
- background: rgba(56,189,248,0.08) !important;
 
 
 
 
1368
  }
1369
- .dark .tabs button.selected {
1370
- color: #38bdf8 !important;
1371
- border-bottom-color: #38bdf8 !important;
 
 
 
 
 
 
 
1372
  }
 
1373
 
1374
- /* ── Buttons ─────────────────────────────────────────── */
1375
- .dark button.secondary {
1376
- border-color: #475569 !important;
1377
- color: #94a3b8 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1378
  background: transparent !important;
 
 
 
 
 
1379
  }
1380
- .dark button.secondary:hover {
1381
- border-color: #64748b !important;
1382
- color: #e2e8f0 !important;
1383
- background: rgba(255,255,255,0.05) !important;
 
 
 
 
 
 
 
1384
  }
1385
 
1386
- /* ── Blocks / panels ─────────────────────────────────── */
1387
- .dark .gradio-container .gr-form,
1388
- .dark .gradio-container .gr-box,
1389
- .dark .gradio-container .gr-panel {
1390
- border-color: #334155 !important;
 
 
 
1391
  }
1392
 
1393
- /* ── Labels ──────────────────────────────────────────── */
1394
- .dark label {
1395
- color: #cbd5e1 !important;
 
 
 
 
 
 
 
 
1396
  }
1397
- .dark .block-label,
1398
- .dark .block-label span {
1399
- color: var(--block-label-text-color) !important;
1400
- font-weight: var(--block-label-text-weight) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1401
  }
 
 
 
1402
 
1403
- /* ── Image output area ───────────────────────────────── */
1404
- .dark .uniform-height {
1405
- background: #1e293b !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1406
  }
1407
 
1408
- /* ── Tab content markdown ────────────────────────────── */
1409
- .dark .tabitem .prose h2 {
1410
- color: #e2e8f0 !important;
1411
- border-bottom-color: #1e3a5f !important;
 
 
 
 
 
 
1412
  }
1413
- .dark .tabitem .prose:nth-child(2) {
1414
- background: #1e293b !important;
1415
- border-color: #334155 !important;
 
 
 
 
 
1416
  }
1417
- .dark .tabitem .prose:nth-child(2) p,
1418
- .dark .tabitem .prose:nth-child(2) li {
1419
- color: #94a3b8 !important;
 
 
 
 
1420
  }
1421
- .dark .tabitem .prose:nth-child(2) strong {
1422
- color: #e2e8f0 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1423
  }
1424
  """
1425
 
 
 
 
 
 
 
 
 
 
 
 
 
1426
  with gr.Blocks(
1427
  title="Microscopy Analysis Suite",
1428
  theme=gr.themes.Soft(
1429
- primary_hue=gr.themes.colors.sky,
1430
- secondary_hue=gr.themes.colors.slate,
 
 
 
 
1431
  neutral_hue=gr.themes.colors.slate,
1432
  # A weight must be listed here to be usable; the default is (400, 600).
1433
  font=gr.themes.GoogleFont("Inter", weights=(400, 600, 700, 800)),
1434
  ),
1435
  css=CSS,
 
1436
  ) as demo:
1437
- gr.Markdown(
1438
- """
1439
- # πŸ”¬ MicroscopyMatching: Microscopy Image Analysis Suite
1440
-
1441
- ### Supporting three key tasks:
1442
- - 🎨 **Segmentation**: Instance segmentation of microscopic objects
1443
- - πŸ”’ **Counting**: Counting microscopic objects based on density maps
1444
- - 🎬 **Tracking**: Tracking microscopic objects in video sequences
1445
-
1446
- ### πŸ’‘ Technical Details:
1447
-
1448
- **MicroscopyMatching** - A general-purpose microscopy image analysis toolkit based on pre-trained Latent Diffusion Model
1449
-
1450
- ### πŸ“’ Note:
1451
-
1452
- This project is currently available with usage limits for research trial use and feedback collection. Please note that response speed may vary depending on GPU allocation queues. We plan to release a free public version in the future. We are actively improving the toolkit and greatly appreciate your feedback!
1453
-
1454
- """
1455
- )
1456
 
1457
 
1458
  # Must stay a callable: a plain value is deepcopied, so every session would
@@ -1469,136 +1864,113 @@ with gr.Blocks(
1469
 
1470
  with gr.Tabs():
1471
  # ===== Tab 1: Segmentation =====
1472
- with gr.Tab("🎨 Segmentation"):
1473
- gr.Markdown("## Instance Segmentation of Microscopic Objects")
1474
- gr.Markdown(
1475
- """
1476
- **Instructions:**
1477
- 1. Select an example image from the Example Image Gallery or upload your own image (**Please see the "Image Upload Requirements" section below before uploading**)
1478
- 2. (Optional) Specify a target object with a bounding box and select "Yes", or click "Run Segmentation" directly
1479
- 3. Click "Run Segmentation"
1480
- 4. View the segmentation results (you can adjust the overlay opacity by sliding the opacity bar below the visualization), download the original predicted mask (.tif format); if needed, click "Clear Selection" to choose a new image
1481
-
1482
- πŸ’‘ Want to reuse an image? Upload it under "βž• Upload New Example Image to Gallery" and click "βž• Add to Gallery". The uploaded image will appear at the front of the gallery, and can be loaded by clicking its thumbnail. Gallery additions last for the current session only and are cleared when you reload the page.
1483
-
1484
- 🀘 Tell us about your experience by rating and submitting feedback, which would greatly help us improve the framework!
1485
- """
1486
- )
1487
- # Must stay after the instructions markdown: the CSS targets it with
1488
- # .prose:nth-child(2).
1489
- with gr.Accordion("πŸ“‹ Image Upload Requirements (click to expand or fold)", open=False, elem_classes="req-accordion"):
1490
- gr.Markdown(
1491
- """
1492
- **Please do not upload images or data containing protected health information (PHI) or personally identifiable information (PII).**
1493
-
1494
- - **Formats:** Supports TIFF / OME-TIFF (8/16/32-bit, stacks, multi-channel), PNG, JPG, as well as various other microscopy image formats supported by [tifffiles](https://github.com/cgohlke/tifffile)
1495
- - **Maximum image size:** 4096 Γ— 4096 pixels
1496
- - **Channels:** Accepts RGB (3-channel), Grayscale (single-channel); for images with more than 3 channels, only the first three channels are used as RGB channels.
1497
- - **For image stacks:** First frame is loaded by default; you can pick another with the stack frame slider that appears after upload
1498
-
1499
- **Alternatively, you can use existing tools (e.g., ImageJ/Fiji) to convert your image to a 8-bit RGB/Grayscale PNG/JPG/TIFF file before uploading.**
1500
-
1501
- """
1502
- )
1503
 
1504
  with gr.Row():
1505
  with gr.Column(scale=1):
1506
  gr.Markdown("πŸ–ΌοΈ Upload Image (Optional: Provide a Bounding Box)", elem_classes="frame-heading")
1507
- annotator = BBoxAnnotator(
1508
- show_label=False,
1509
- categories=["cell"],
1510
- )
1511
- seg_frame_slider = gr.Slider(
1512
- minimum=1, maximum=1, step=1, value=1,
1513
- label="πŸ“š Stack frame (choose frame to use)",
1514
- visible=False,
1515
- )
1516
- seg_zplane_slider = gr.Slider(
1517
- minimum=1, maximum=1, step=1, value=1,
1518
- label="πŸ”¬ Z-plane (choose plane to use)",
1519
- visible=False,
1520
- )
 
 
1521
 
1522
  # Example Images Gallery
1523
  gr.Markdown("πŸ“ Example Image Gallery", elem_classes="frame-heading")
1524
- example_gallery = gr.Gallery(
1525
- show_label=True,
1526
- columns=len(example_images_seg),
1527
- rows=1,
1528
- height=120,
1529
- object_fit="cover",
1530
- show_download_button=False
1531
- )
1532
-
1533
-
1534
- with gr.Row():
1535
  use_box_radio = gr.Radio(
1536
  choices=["Yes", "No"],
1537
  value="No",
1538
- label="πŸ”² Specify Bounding Box?"
 
1539
  )
1540
  with gr.Row():
1541
  run_seg_btn = gr.Button("▢️ Run Segmentation", variant="primary", size="lg")
1542
  clear_btn = gr.Button("πŸ”„ Clear Selection", variant="secondary")
1543
 
 
 
 
 
 
 
 
 
 
 
 
 
1544
  # Upload Example Image
1545
  gr.Markdown("βž• Upload New Example Image to Gallery", elem_classes="frame-heading")
1546
- image_uploader = gr.File(
1547
- show_label=False,
1548
- file_types=["image"] + list(TIFF_EXTENSIONS),
1549
- type="filepath"
1550
- )
1551
- add_to_gallery_btn = gr.Button("βž• Add to Gallery", variant="secondary", size="sm")
1552
- add_gallery_status = gr.Markdown(visible=False)
1553
-
1554
-
1555
- with gr.Column(scale=2):
1556
- gr.Markdown("πŸ“Έ Segmentation Result", elem_classes="frame-heading")
1557
- seg_output = gr.Image(
1558
- type="pil",
1559
- show_label=False,
1560
- elem_classes="uniform-height"
1561
- )
1562
- seg_alpha_slider = gr.Slider(
1563
- minimum=0.0,
1564
- maximum=1.0,
1565
- step=0.05,
1566
- value=0.5,
1567
- label="πŸͺ„ Overlay Opacity"
1568
- )
1569
-
1570
- # Download Original Prediction
1571
- gr.Markdown("πŸ“₯ Download Original Prediction (.tif format)", elem_classes="frame-heading")
1572
- download_mask_btn = gr.File(
1573
- show_label=False,
1574
- visible=True,
1575
- height=40,
1576
- )
1577
-
1578
- # Satisfaction Rating
1579
- score_slider = gr.Slider(
1580
- minimum=1,
1581
- maximum=5,
1582
- step=1,
1583
- value=5,
1584
- label="🌟 Satisfaction Rating (1-5)"
1585
- )
1586
-
1587
- # Feedback Textbox
1588
- feedback_box = gr.Textbox(
1589
- placeholder="Please enter your feedback...",
1590
- lines=2,
1591
- label="πŸ’¬ Feedback"
1592
- )
1593
 
1594
- # Submit Button
1595
- submit_feedback_btn = gr.Button("πŸ’Ύ Submit Feedback", variant="secondary")
1596
 
1597
- feedback_status = gr.Textbox(
1598
- label="βœ… Submission Status",
1599
- lines=1,
1600
- visible=False
1601
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1602
 
1603
  annotator.upload(
1604
  fn=prepare_uploaded_image,
@@ -1612,6 +1984,20 @@ with gr.Blocks(
1612
  outputs=annotator
1613
  )
1614
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1615
  # click event for segmentation
1616
  run_seg_btn.click(
1617
  fn=segment_with_choice,
@@ -1711,58 +2097,32 @@ with gr.Blocks(
1711
  )
1712
 
1713
  # ===== Tab 2: Counting =====
1714
- with gr.Tab("πŸ”’ Counting"):
1715
- gr.Markdown("## Microscopy Object Counting Analysis")
1716
- gr.Markdown(
1717
- """
1718
- **Usage Instructions:**
1719
- 1. Select an example image from the Example Image Gallery or upload your own image (**Please see the "Image Upload Requirements" section below before uploading**)
1720
- 2. (Optional) Specify a target object with a bounding box and select "Yes", or click "Run Counting" directly
1721
- 3. Click "Run Counting"
1722
- 4. View the density map (you can adjust the density opacity by sliding the opacity bar below the visualization), download the original prediction (.npy format); if needed, click "Clear Selection" to choose a new image to run
1723
-
1724
- πŸ’‘ Want to reuse an image? Upload it under "βž• Upload New Example Image to Gallery" and click "βž• Add to Gallery". The uploaded image will appear at the front of the gallery, and can be loaded by clicking its thumbnail. Gallery additions last for the current session only and are cleared when you reload the page.
1725
-
1726
- 🀘 Tell us about your experience by rating and submitting feedback, which would greatly help us improve the framework!
1727
- """
1728
- )
1729
- # Must stay after the instructions markdown: the CSS targets it with
1730
- # .prose:nth-child(2).
1731
- with gr.Accordion("πŸ“‹ Image requirements", open=False, elem_classes="req-accordion"):
1732
- gr.Markdown(
1733
- """
1734
- **Please do not upload images or data containing protected health information (PHI) or personally identifiable information (PII).**
1735
-
1736
- - **Formats:** Supports TIFF / OME-TIFF (8/16/32-bit, stacks, multi-channel), PNG, JPG, as well as various other microscopy image formats supported by [tifffiles](https://github.com/cgohlke/tifffile)
1737
- - **Maximum image size:** 4096 Γ— 4096 pixels
1738
- - **Channels:** Accepts RGB (3-channel), Grayscale (single-channel); for images with more than 3 channels, only the first three channels are used as RGB channels.
1739
- - **For image stacks:** First frame is loaded by default; you can pick another with the stack frame slider that appears after upload
1740
-
1741
- **Alternatively, you can use existing tools (e.g., ImageJ/Fiji) to convert your image to a 8-bit RGB/Grayscale PNG/JPG/TIFF file before uploading.**
1742
- """
1743
- )
1744
 
1745
  with gr.Row():
1746
  with gr.Column(scale=1):
1747
  gr.Markdown("πŸ–ΌοΈ Upload Image (Optional: Provide a Bounding Box)", elem_classes="frame-heading")
1748
- count_annotator = BBoxAnnotator(
1749
- show_label=False,
1750
- categories=["cell"],
1751
- )
1752
- count_frame_slider = gr.Slider(
1753
- minimum=1, maximum=1, step=1, value=1,
1754
- label="πŸ“š Stack frame (choose frame to use)",
1755
- visible=False,
1756
- )
1757
- count_zplane_slider = gr.Slider(
1758
- minimum=1, maximum=1, step=1, value=1,
1759
- label="πŸ”¬ Z-plane (choose plane to use)",
1760
- visible=False,
1761
- )
 
 
1762
 
1763
  # Example gallery with "add" functionality
1764
  gr.Markdown("πŸ“ Example Image Gallery", elem_classes="frame-heading")
1765
- with gr.Row():
1766
  count_example_gallery = gr.Gallery(
1767
  show_label=False,
1768
  columns=len(example_images_cnt),
@@ -1772,80 +2132,86 @@ with gr.Blocks(
1772
  value=example_images_cnt.copy(), # Initialize with examples
1773
  show_download_button=False
1774
  )
1775
-
1776
-
1777
- with gr.Row():
1778
  count_use_box_radio = gr.Radio(
1779
  choices=["Yes", "No"],
1780
  value="No",
1781
- label="πŸ”² Specify Bounding Box?"
 
1782
  )
1783
 
1784
  with gr.Row():
1785
  count_btn = gr.Button("▢️ Run Counting", variant="primary", size="lg")
1786
  clear_btn = gr.Button("πŸ”„ Clear Selection", variant="secondary")
 
 
 
 
 
 
 
 
 
 
 
 
1787
 
1788
  # Add button to upload new examples
1789
  gr.Markdown("βž• Add Example Image to Gallery", elem_classes="frame-heading")
1790
- count_image_uploader = gr.File(
1791
- show_label=False,
1792
- file_types=["image"] + list(TIFF_EXTENSIONS),
1793
- type="filepath"
1794
- )
1795
- add_to_count_gallery_btn = gr.Button("βž• Add to Gallery", variant="secondary", size="sm")
1796
- count_add_status = gr.Markdown(visible=False)
 
1797
 
1798
 
1799
- with gr.Column(scale=2):
1800
- gr.Markdown("πŸ“Έ Density Map", elem_classes="frame-heading")
1801
- count_output = gr.Image(
1802
- show_label=False,
1803
- type="filepath",
1804
- elem_id="density_map_output"
1805
-
1806
- )
1807
- count_alpha_slider = gr.Slider(
1808
- minimum=0.0,
1809
- maximum=1.0,
1810
- step=0.05,
1811
- value=0.3,
1812
- label="πŸͺ„ Density Opacity"
1813
- )
1814
- count_status = gr.Textbox(
1815
- label="πŸ“Š Statistics",
1816
- lines=2
1817
- )
1818
- gr.Markdown("πŸ“₯ Download Original Prediction (.npy format)", elem_classes="frame-heading")
1819
- download_density_btn = gr.File(
1820
- show_label=False,
1821
- visible=True
1822
- )
1823
-
1824
- # Satisfaction rating
1825
- score_slider = gr.Slider(
1826
- minimum=1,
1827
- maximum=5,
1828
- step=1,
1829
- value=5,
1830
- label="🌟 Satisfaction Rating (1-5)"
1831
- )
1832
 
1833
- # Feedback textbox
1834
- feedback_box = gr.Textbox(
1835
- placeholder="Please enter your feedback...",
1836
- lines=2,
1837
- label="πŸ’¬ Feedback"
1838
- )
 
 
 
 
 
 
 
 
1839
 
1840
- # Submit button
1841
- submit_feedback_btn = gr.Button("πŸ’Ύ Submit Feedback", variant="secondary")
 
 
 
 
 
 
 
 
 
 
1842
 
1843
- feedback_status = gr.Textbox(
1844
- label="βœ… Submission Status",
1845
- lines=1,
1846
- visible=False
1847
- )
1848
-
1849
  # State for managing gallery images
1850
  count_user_examples = gr.State(example_images_cnt.copy())
1851
 
@@ -1892,6 +2258,18 @@ with gr.Blocks(
1892
  outputs=count_annotator
1893
  )
1894
 
 
 
 
 
 
 
 
 
 
 
 
 
1895
  # When user selects from gallery, load into annotator
1896
  def load_from_count_gallery(evt: gr.SelectData, all_imgs):
1897
  """Load a gallery image, reusing the upload path."""
@@ -1952,124 +2330,122 @@ with gr.Blocks(
1952
  )
1953
 
1954
  # ===== Tab 3: Tracking =====
1955
- with gr.Tab("🎬 Tracking"):
1956
- gr.Markdown("## Microscopy Object Video Tracking - Supports ZIP Upload")
1957
- gr.Markdown(
1958
- """
1959
- **Instructions:**
1960
- 1. Select a video from the example library or Upload your own video ZIP file. The ZIP should contain a sequence of TIF images named in chronological order (e.g., t000.tif, t001.tif...)
1961
- 2. (Optional) Specify a target object with a bounding box on the first frame and select "Yes", or click "Run Tracking" directly
1962
- 3. Click "Run Tracking"
1963
- 4. View the tracking results (you can adjust the overlay opacity by sliding the opacity bar below the visualization), download the CTC format results; if needed, click "Clear Selection" to choose a new ZIP file to run
1964
-
1965
- πŸ’‘ Want to reuse a video? Upload the ZIP under "βž• Upload New Example Image to Gallery" and click "βž• Add to Gallery". The uploaded video will appear at the front of the gallery, and can be loaded by clicking its thumbnail. Gallery additions last for the current session only and are cleared when you reload the page.
1966
-
1967
- 🀘 Tell us about your experience by rating and submitting feedback, which would greatly help us improve the framework!
1968
-
1969
- """
1970
- )
1971
 
1972
  with gr.Row():
1973
  with gr.Column(scale=1):
1974
- gr.Markdown("πŸ“¦ Upload Image Sequence in ZIP File", elem_classes="frame-heading")
1975
- track_zip_upload = gr.File(
1976
- show_label=False,
1977
- file_types=[".zip"]
1978
- )
 
1979
 
1980
  # First frame annotation for bounding box (heading + annotator
1981
  # share one column so they show/hide together)
1982
  with gr.Column(visible=False) as track_annot_group:
1983
  gr.Markdown("πŸ–ΌοΈ (Optional) First Frame Bounding Box Annotation", elem_classes="frame-heading")
1984
- track_first_frame_annotator = BBoxAnnotator(
1985
- show_label=False,
1986
- categories=["cell"],
1987
- )
 
 
1988
 
1989
  # Example ZIP gallery
1990
  gr.Markdown("πŸ“ Example Video Gallery (Click to Select)", elem_classes="frame-heading")
1991
- track_example_gallery = gr.Gallery(
1992
- show_label=False,
1993
- columns=10,
1994
- rows=1,
1995
- height=120,
1996
- object_fit="contain",
1997
- show_download_button=False
1998
- )
1999
-
2000
- with gr.Row():
 
2001
  track_use_box_radio = gr.Radio(
2002
  choices=["Yes", "No"],
2003
  value="No",
2004
- label="πŸ”² Specify Bounding Box?"
 
2005
  )
2006
 
2007
  with gr.Row():
2008
  track_btn = gr.Button("▢️ Run Tracking", variant="primary", size="lg")
2009
  clear_btn = gr.Button("πŸ”„ Clear Selection", variant="secondary")
2010
-
 
 
 
 
 
 
 
 
 
 
 
 
 
2011
  # Add to gallery button
2012
  gr.Markdown("βž• Add ZIP to Example Gallery", elem_classes="frame-heading")
2013
- track_gallery_upload = gr.File(
2014
- show_label=False,
2015
- file_types=[".zip"],
2016
- type="filepath"
2017
- )
 
2018
 
2019
- with gr.Column(scale=2):
2020
- gr.Markdown("πŸ“Έ Tracking Visualization", elem_classes="frame-heading")
2021
- track_first_frame_preview = gr.Image(
2022
- show_label=False,
2023
- type="filepath",
2024
- # height=400,
2025
- elem_classes="uniform-height",
2026
- interactive=False
2027
- )
2028
- track_alpha_slider = gr.Slider(
2029
- minimum=0.0,
2030
- maximum=1.0,
2031
- step=0.05,
2032
- value=0.3,
2033
- label="πŸͺ„ Overlay Opacity"
2034
- )
2035
-
2036
- track_output = gr.Textbox(
2037
- label="πŸ“Š Tracking Information",
2038
- lines=8,
2039
- interactive=False
2040
- )
2041
-
2042
- # heading + download share one column so they show/hide together
2043
- with gr.Column(visible=False) as track_dl_group:
2044
- gr.Markdown("πŸ“₯ Download Tracking Results (CTC Format)", elem_classes="frame-heading")
2045
- track_download = gr.File(
2046
  show_label=False,
 
 
 
 
 
 
 
 
 
 
 
 
2047
  )
2048
 
2049
- # Satisfaction rating
2050
- score_slider = gr.Slider(
2051
- minimum=1,
2052
- maximum=5,
2053
- step=1,
2054
- value=5,
2055
- label="🌟 Satisfaction Rating (1-5)"
2056
- )
2057
-
2058
- # Feedback textbox
2059
- feedback_box = gr.Textbox(
2060
- placeholder="Please enter your feedback...",
2061
- lines=2,
2062
- label="πŸ’¬ Feedback"
2063
- )
2064
-
2065
- # Submit button
2066
- submit_feedback_btn = gr.Button("πŸ’Ύ Submit Feedback", variant="secondary")
2067
 
2068
- feedback_status = gr.Textbox(
2069
- label="βœ… Submission Status",
2070
- lines=1,
2071
- visible=False
2072
- )
 
 
 
 
 
 
 
2073
 
2074
  # State for tracking examples
2075
  track_user_examples = gr.State(example_tracking_zips.copy())
@@ -2286,8 +2662,26 @@ with gr.Blocks(
2286
  inputs=[current_query_id, score_slider, feedback_box, annotator],
2287
  outputs=[feedback_status, feedback_status]
2288
  )
2289
-
2290
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2291
 
2292
  if __name__ == "__main__":
2293
  demo.queue().launch(
 
11
  from pathlib import Path
12
  import tempfile
13
  import zipfile
14
+ import html
15
  from skimage import measure
16
  from matplotlib import cm
17
  from glob import glob
 
23
  from inference_count import load_model as load_count_model, run as run_count
24
  from inference_track import load_model as load_track_model, run as run_track
25
  from _utils.image_io import (
26
+ standardize_image, inspect_image, image_size, array_nbytes, pixel_stats, bit_depth,
27
  RECOMMENDED_SIZE, WARN_SIZE, MAX_SIZE, MIN_SIZE, MAX_READ_BYTES, TIFF_EXTENSIONS,
28
  )
29
 
 
1064
  if box_array:
1065
  bbox_info = f"\nπŸ”² Using bounding box: [{box_array[0][0]}, {box_array[0][1]}, {box_array[0][2]}, {box_array[0][3]}]"
1066
 
1067
+ result_text = (
1068
+ f"βœ… Tracking completed!\n"
1069
+ f"\n"
1070
+ f"πŸ–ΌοΈ Processed frames: {len(valid_tif_files)}{bbox_info}\n"
1071
+ f"\n"
1072
+ f"πŸ“₯ Use the \"Download (.zip)\" button above to get the results, which include:\n"
1073
+ f"- res_track.txt (CTC-format tracking data)\n"
1074
+ f"- Other tracking-related files\n"
1075
+ f"- README.txt (Results description)"
1076
+ )
1077
 
1078
  if use_box_choice == "Yes" and box_array:
1079
  result_text += f"\nπŸ“¦ Using bounding box: {box_array}"
 
1111
  example_images_cnt = [f for f in glob("example_imgs/cnt/*")]
1112
  example_tracking_zips = [f for f in glob("example_imgs/tra/*.zip")]
1113
 
1114
+ _CHANNEL_NOTE = {1: "grayscale", 3: "RGB", 4: "RGBA"}
1115
+
1116
+
1117
+ def describe_image_info(raw_path, frame=1):
1118
+ """Metadata line under the result: file, size, channels, bit depth, frames.
1119
+
1120
+ Reads the *original* upload (not the standardized preview) so the numbers
1121
+ describe what the user actually provided. Metadata only - no pixel decode.
1122
+ """
1123
+ if not raw_path or not os.path.exists(raw_path):
1124
+ return gr.update(value="", visible=False)
1125
+
1126
+ info = inspect_image(raw_path)
1127
+ bits = bit_depth(raw_path)
1128
+
1129
+ fields = [f"<span class='ii-k'>File:</span> {html.escape(os.path.basename(raw_path))}"]
1130
+ if info.width and info.height:
1131
+ fields.append(f"<span class='ii-k'>Size:</span> {info.width} Γ— {info.height} px")
1132
+ note = _CHANNEL_NOTE.get(info.channels)
1133
+ fields.append(f"<span class='ii-k'>Channels:</span> {info.channels}"
1134
+ + (f" ({note})" if note else ""))
1135
+ if bits:
1136
+ fields.append(f"<span class='ii-k'>Bit depth:</span> {bits}-bit")
1137
+ if info.frames > 1:
1138
+ fields.append(f"<span class='ii-k'>Frames:</span> {int(frame)} / {info.frames}")
1139
+ if info.sub_frames > 1:
1140
+ fields.append(f"<span class='ii-k'>{info.sub_label.capitalize()}s:</span> {info.sub_frames}")
1141
+
1142
+ body = "".join(f"<span class='ii-f'>{f}</span>" for f in fields)
1143
+ return gr.update(
1144
+ value=f"<div class='ii-wrap'><span class='ii-title'>β“˜ Image information</span>{body}</div>",
1145
+ visible=True,
1146
+ )
1147
+
1148
+
1149
  # ===== Gradio UI =====
1150
  CSS = """
1151
  /* ── Layout ──────────────────────────────────────────── */
1152
  .gradio-container {
1153
+ max-width: 1320px !important;
1154
  margin: 0 auto !important;
1155
  font-family: 'Inter', 'Segoe UI', system-ui, sans-serif !important;
1156
+ background: #fff !important;
1157
  }
1158
 
1159
  /* ── Header markdown polish ───────────────────────────── */
1160
  .gradio-container .prose h1 {
1161
+ font-size: 1.4rem !important;
1162
  font-weight: 700 !important;
1163
+ color: #1c1c1a !important;
1164
+ letter-spacing: -0.01em !important;
1165
+ margin-bottom: 2px !important;
1166
  }
1167
  .gradio-container .prose h3 {
1168
  font-size: 1rem !important;
1169
  font-weight: 600 !important;
1170
+ color: #bf1e2e !important;
1171
  margin-top: 14px !important;
1172
  margin-bottom: 4px !important;
1173
  }
1174
  .gradio-container .prose p {
1175
  margin-top: 4px !important;
1176
  margin-bottom: 6px !important;
1177
+ color: #5c5a52 !important;
1178
  line-height: 1.7 !important;
1179
  }
1180
  .gradio-container .prose ul,
 
1183
  margin-bottom: 6px !important;
1184
  }
1185
  .gradio-container .prose li {
1186
+ color: #5c5a52 !important;
1187
  line-height: 1.7 !important;
1188
  }
1189
 
1190
+ /* ── Top-level header section: slim, no gradient card ─── */
1191
  .gradio-container > .gap > .prose:first-child {
1192
+ background: transparent !important;
1193
+ border: none !important;
1194
+ border-radius: 0 !important;
1195
+ padding: 18px 4px 10px !important;
1196
+ margin-bottom: 8px !important;
1197
+ box-shadow: none !important;
1198
  }
1199
 
1200
+ /* ── Mode selector: two-line task cards ──────────────── */
1201
+ /* Gradio 5.x markup: .tabs > .tab-wrapper > .tab-container[role=tablist] > button */
1202
+
1203
+ /* Undo Gradio's fixed-height, clipped, underlined nav strip */
1204
+ .tabs > .tab-wrapper {
1205
+ height: auto !important;
1206
+ padding-bottom: 0 !important;
1207
  margin-bottom: 20px !important;
 
1208
  }
1209
+ .tabs > .tab-wrapper > .tab-container {
1210
+ height: auto !important;
1211
+ overflow: visible !important;
1212
+ gap: 14px !important;
1213
+ align-items: stretch !important;
1214
+ }
1215
+ .tabs > .tab-wrapper > .tab-container::after { display: none !important; } /* bottom hairline */
1216
+ .tabs > .tab-wrapper > .overflow-menu { display: none !important; } /* "…" menu */
1217
+
1218
+ /* The card itself */
1219
+ .tabs > .tab-wrapper > .tab-container:not(.visually-hidden) > button {
1220
+ flex: 1 1 0 !important;
1221
+ display: grid !important;
1222
+ grid-template-columns: auto 1fr !important;
1223
+ grid-template-rows: auto auto !important;
1224
+ column-gap: 14px !important;
1225
+ row-gap: 2px !important;
1226
+ align-items: center !important;
1227
+ justify-items: start !important;
1228
+ text-align: left !important;
1229
+ white-space: normal !important;
1230
+ height: auto !important;
1231
+ padding: 16px 20px !important;
1232
+ border-radius: 10px !important;
1233
+ border: 1.5px solid #e7e5e1 !important;
1234
+ background: #fff !important;
1235
+ box-shadow: none !important;
1236
+ color: #1c1c1a !important;
1237
  font-size: 15px !important;
1238
+ font-weight: 700 !important;
1239
+ line-height: 1.3 !important;
1240
+ transition: border-color 0.12s, box-shadow 0.12s, color 0.12s !important;
1241
+ }
1242
+
1243
+ /* Left icon: spans both rows */
1244
+ .tabs > .tab-wrapper > .tab-container:not(.visually-hidden) > button::before {
1245
+ grid-column: 1 !important;
1246
+ grid-row: 1 / span 2 !important;
1247
+ font-size: 26px !important;
1248
+ line-height: 1 !important;
1249
+ align-self: center !important;
1250
+ }
1251
+
1252
+ /* Subtitle: row 2 of the text column.
1253
+ NOTE: also resets Gradio's .selected::after underline bar (position/size/bg/animation). */
1254
+ .tabs > .tab-wrapper > .tab-container:not(.visually-hidden) > button::after {
1255
+ grid-column: 2 !important;
1256
+ grid-row: 2 !important;
1257
+ position: static !important;
1258
+ width: auto !important;
1259
+ height: auto !important;
1260
+ background: none !important;
1261
+ animation: none !important;
1262
+ font-size: 13px !important;
1263
+ font-weight: 400 !important;
1264
+ color: #7a7873 !important;
1265
+ line-height: 1.35 !important;
1266
+ }
1267
+
1268
+ /* Per-tab icon + description */
1269
+ .tabs > .tab-wrapper > .tab-container:not(.visually-hidden) > button:nth-of-type(1)::before { content: "🎨"; }
1270
+ .tabs > .tab-wrapper > .tab-container:not(.visually-hidden) > button:nth-of-type(1)::after { content: "Instance segmentation of microscopic objects"; }
1271
+ .tabs > .tab-wrapper > .tab-container:not(.visually-hidden) > button:nth-of-type(2)::before { content: "πŸ”’"; }
1272
+ .tabs > .tab-wrapper > .tab-container:not(.visually-hidden) > button:nth-of-type(2)::after { content: "Microscopy Object Counting Analysis"; }
1273
+ .tabs > .tab-wrapper > .tab-container:not(.visually-hidden) > button:nth-of-type(3)::before { content: "🎬"; }
1274
+ .tabs > .tab-wrapper > .tab-container:not(.visually-hidden) > button:nth-of-type(3)::after { content: "Microscopy Object Video Tracking - Supports ZIP Upload"; }
1275
+
1276
+ /* States */
1277
+ .tabs > .tab-wrapper > .tab-container:not(.visually-hidden) > button:hover {
1278
+ border-color: #e6bdb8 !important;
1279
+ background: #fdf3f2 !important;
1280
+ }
1281
+ .tabs > .tab-wrapper > .tab-container:not(.visually-hidden) > button.selected {
1282
+ color: #bf1e2e !important;
1283
+ border-color: #bf1e2e !important;
1284
+ box-shadow: 0 0 0 1px #bf1e2e !important;
1285
+ background: #fff !important;
1286
+ }
1287
+ .tabs > .tab-wrapper > .tab-container:not(.visually-hidden) > button.selected::after {
1288
+ color: #7a7873 !important; /* description stays gray when selected */
1289
  }
1290
 
1291
  /* ── Buttons ─────────────────────────────────────────── */
1292
  button.primary {
1293
+ background: #bf1e2e !important;
1294
  border: none !important;
1295
+ border-radius: 8px !important;
1296
  color: #fff !important;
1297
  font-weight: 600 !important;
1298
  font-size: 15px !important;
1299
+ box-shadow: none !important;
1300
+ transition: background 0.12s ease !important;
1301
  }
1302
  button.primary:hover {
1303
+ background: #a61a28 !important;
 
1304
  }
1305
  button.secondary {
1306
+ border-radius: 8px !important;
1307
  font-weight: 500 !important;
1308
+ border: 1.5px solid #d8d5cd !important;
1309
+ color: #3a3a36 !important;
1310
  transition: border-color 0.12s, color 0.12s, background 0.12s !important;
1311
  }
1312
  button.secondary:hover {
1313
+ border-color: #bf1e2e !important;
1314
+ color: #bf1e2e !important;
1315
+ background: #fdf3f2 !important;
1316
  }
1317
 
1318
  /* ── Blocks and panels ───────────────────────────────── */
1319
+ .gradio-container .block { border-radius: 8px !important; }
1320
  .gradio-container .gr-form,
1321
  .gradio-container .gr-box,
1322
  .gradio-container .gr-panel {
1323
+ border-radius: 8px !important;
1324
+ border-color: #e7e5e1 !important;
1325
  }
1326
 
1327
  /* ── Labels ──────────────────────────────────────────── */
 
1391
  display: flex !important;
1392
  align-items: center !important;
1393
  justify-content: center !important;
1394
+ border-radius: 8px !important;
1395
+ background: #f0efeb !important;
1396
  }
1397
  .uniform-height img, .uniform-height canvas {
1398
  max-height: 480px !important;
1399
  object-fit: contain !important;
1400
  }
1401
 
1402
+ /* ── Input annotator: match the 480px result panel ───── */
1403
+ /* width/height auto keeps the <img> box equal to the rendered picture (no
1404
+ letterbox gaps), so the annotator's ResizeObserver still maps bbox
1405
+ coordinates onto the right rect after scaling. */
1406
+ .uniform-height-input img {
1407
+ max-height: 480px !important;
1408
+ width: auto !important;
1409
+ height: auto !important;
1410
+ max-width: 100% !important;
1411
+ object-fit: contain !important;
1412
+ margin: 0 auto !important;
1413
+ }
1414
+
1415
  /* ── Density map output ──────────────────────────────── */
1416
  #density_map_output { height: 480px !important; }
1417
  #density_map_output .image-container { height: 480px !important; }
 
1422
  object-fit: contain !important;
1423
  }
1424
 
1425
+ /* ── "How to use" collapsible instructions (was a permanent wall of text) ── */
1426
+ .howto-accordion {
1427
+ border: 1px solid #e7e5e1 !important;
1428
+ border-radius: 8px !important;
1429
+ margin-bottom: 14px !important;
 
 
 
 
1430
  }
1431
+ .howto-accordion .label-wrap {
1432
+ color: #bf1e2e !important;
 
 
 
 
1433
  }
1434
+ .howto-accordion .label-wrap span:not(.icon) {
1435
+ font-size: 0.9rem !important;
1436
+ font-weight: 700 !important;
1437
+ color: #bf1e2e !important;
1438
  }
1439
+ .howto-accordion .prose p,
1440
+ .howto-accordion .prose li {
1441
+ font-size: 0.87rem !important;
1442
+ color: #5c5a52 !important;
1443
+ }
1444
+ .howto-accordion .prose strong {
1445
+ color: #1c1c1a !important;
1446
  }
1447
 
1448
+ /* ── "How to use": top-right button + centered modal ─── */
1449
+ #header_row {
1450
+ align-items: center !important;
1451
+ justify-content: space-between !important;
1452
+ flex-wrap: nowrap !important;
 
 
1453
  }
1454
+ #header_title { flex: 1 1 auto !important; }
1455
+ #how_to_use_btn {
1456
+ flex: 0 0 auto !important;
1457
+ width: auto !important;
1458
+ min-width: 0 !important;
1459
+ align-self: center !important;
1460
+ background: #bf1e2e !important;
1461
+ color: #fff !important;
1462
+ font-weight: 600 !important;
1463
+ font-size: 13.5px !important;
1464
+ border: none !important;
1465
+ border-radius: 6px !important;
1466
+ padding: 9px 16px !important;
1467
+ box-shadow: none !important;
1468
+ }
1469
+ #how_to_use_btn:hover { background: #a61a28 !important; }
1470
+ #how_to_use_modal {
1471
+ position: fixed !important;
1472
+ top: 8% !important;
1473
+ left: 50% !important;
1474
+ transform: translateX(-50%) !important;
1475
+ width: min(600px, 92vw) !important;
1476
+ max-height: 82vh !important;
1477
+ overflow-y: auto !important;
1478
+ background: #fff !important;
1479
+ border: 1px solid #e7e5e1 !important;
1480
+ border-radius: 12px !important;
1481
+ box-shadow: 0 24px 70px rgba(0,0,0,0.28) !important;
1482
+ z-index: 1000 !important;
1483
+ padding: 22px 26px !important;
1484
  }
1485
+ #how_to_use_modal h3 { margin-top: 12px !important; }
1486
+ #close_how_to_use_btn {
1487
+ width: auto !important;
1488
+ min-width: 0 !important;
1489
+ margin-top: 8px !important;
1490
+ border: 1px solid #d8d5cd !important;
1491
+ border-radius: 6px !important;
1492
+ color: #3a3a36 !important;
1493
+ background: #fff !important;
1494
  }
1495
 
1496
+ /* ── Footer note ─────────────────────────────────────── */
1497
+ #footer_note {
1498
+ border-top: 1px solid #e7e5e1 !important;
1499
+ padding-top: 16px !important;
1500
+ margin-top: 24px !important;
1501
  }
1502
+ #footer_note p { font-size: 12.5px !important; color: #5c5a52 !important; line-height: 1.8 !important; }
1503
+ #footer_note strong { color: #1c1c1a !important; }
1504
 
1505
+ /* ── Requirements: collapsible panel (open by default) ── */
1506
+ .req-panel {
1507
+ border: 1.5px solid #e6bdb8 !important;
1508
+ border-radius: 10px !important;
1509
+ padding: 14px 16px !important;
1510
+ margin-bottom: 14px !important;
1511
+ background: #fdf3f2 !important;
1512
+ box-shadow: 0 1px 4px rgba(28, 28, 26, 0.07) !important;
1513
+ }
1514
+ .req-panel > *, .req-panel .block, .req-panel .form { background: transparent !important; border: none !important; }
1515
+ /* Accordion header: crimson, with a divider only while expanded */
1516
+ .req-panel .label-wrap {
1517
+ margin: 0 !important;
1518
+ padding: 0 !important;
1519
+ color: #bf1e2e !important;
1520
+ }
1521
+ .req-panel .label-wrap.open {
1522
+ margin-bottom: 10px !important;
1523
+ padding-bottom: 8px !important;
1524
+ border-bottom: 1.5px solid #f3d9d5 !important;
1525
+ }
1526
+ .req-panel .label-wrap span:not(.icon) {
1527
+ font-size: 0.92rem !important;
1528
+ font-weight: 800 !important;
1529
+ color: #bf1e2e !important;
1530
  }
1531
+ .req-panel .label-wrap .icon { color: #bf1e2e !important; }
1532
+ .req-warn {
1533
+ font-size: 12px !important;
1534
+ color: #8a5a1c !important;
1535
+ background: #faf3e6 !important;
1536
+ border: 1px solid #f0dfc0 !important;
1537
+ border-radius: 5px !important;
1538
+ padding: 8px 10px !important;
1539
+ margin-bottom: 8px !important;
1540
  }
1541
+ .req-row {
1542
+ display: flex !important;
1543
+ justify-content: space-between !important;
1544
+ gap: 16px !important;
1545
+ font-size: 12.5px !important;
1546
+ padding: 7px 0 !important;
1547
+ border-bottom: 1px solid #f0efeb !important;
1548
  }
1549
+ .req-row:last-of-type { border-bottom: none !important; }
1550
+ .req-label { color: #8b8981 !important; flex-shrink: 0 !important; }
1551
+ .req-val { color: #1c1c1a !important; font-weight: 500 !important; text-align: right !important; }
1552
+ .req-val a { color: #bf1e2e !important; }
1553
+ .req-val code {
1554
+ font-size: 12px !important;
1555
+ background: #f5f3f0 !important;
1556
+ padding: 1px 5px !important;
1557
+ border-radius: 4px !important;
1558
+ color: #1c1c1a !important;
1559
  }
1560
+ .req-note { font-size: 12px !important; color: #5c5a52 !important; margin-top: 10px !important; line-height: 1.5 !important; }
1561
 
1562
+ /* ── Result card ─────────────────────────────────────── */
1563
+ /* The row carries the -8px hug to the frame below, so the pill keeps a normal
1564
+ box and centre-aligns with the download button instead of sitting lower. */
1565
+ .result-header {
1566
+ align-items: center !important;
1567
+ gap: 12px !important;
1568
+ /* the download button makes this row taller than a bare pill, so it needs
1569
+ a deeper pull-down than .frame-heading's -8px to hug the card below */
1570
+ margin-bottom: -16px !important;
1571
+ }
1572
+ .result-header .frame-heading { margin-bottom: 0 !important; }
1573
+ .result-download {
1574
+ border: 1px solid #d9d7d2 !important;
1575
+ background: #fff !important;
1576
+ color: #1c1c1a !important;
1577
+ font-weight: 600 !important;
1578
+ border-radius: 8px !important;
1579
+ box-shadow: none !important;
1580
+ }
1581
+ .result-download:hover {
1582
+ border-color: #bf1e2e !important;
1583
+ color: #bf1e2e !important;
1584
+ background: #fdf3f2 !important;
1585
+ }
1586
+
1587
+ /* Opacity on one line. Gradio nests the label and the number box together in
1588
+ .head; display:contents lifts them into .wrap's flex row so the track can be
1589
+ ordered between them. */
1590
+ .opacity-row .wrap {
1591
+ display: flex !important;
1592
+ flex-direction: row !important;
1593
+ align-items: center !important;
1594
+ gap: 14px !important;
1595
+ }
1596
+ .opacity-row .head { display: contents !important; }
1597
+ .opacity-row .head label { order: 1 !important; flex: 0 0 auto !important; margin: 0 !important; }
1598
+ /* the theme gives block labels a tinted pill; plain text here */
1599
+ .opacity-row .head label,
1600
+ .opacity-row .head label span {
1601
  background: transparent !important;
1602
+ padding: 0 !important;
1603
+ color: #1c1c1a !important;
1604
+ font-weight: 600 !important;
1605
+ font-size: 14px !important;
1606
+ white-space: nowrap !important;
1607
  }
1608
+ .opacity-row .slider_input_container { order: 2 !important; flex: 1 1 auto !important; }
1609
+ .opacity-row .tab-like-container { order: 3 !important; flex: 0 0 auto !important; }
1610
+ .opacity-row .min_value, .opacity-row .max_value { display: none !important; }
1611
+
1612
+ /* ── Section cards: give each block its own frame ────── */
1613
+ .section-card {
1614
+ border: 1px solid #e7e5e1 !important;
1615
+ border-radius: 12px !important;
1616
+ background: #fff !important;
1617
+ padding: 16px !important;
1618
+ box-shadow: 0 1px 3px rgba(28, 28, 26, 0.04) !important;
1619
  }
1620
 
1621
+ /* No card here - it's one short control. But Gradio wraps lone inputs in a
1622
+ .form that paints its own fill, which shows as a grey band behind the row
1623
+ once the control is only one line tall. */
1624
+ .bbox-slot .form,
1625
+ .bbox-slot > .block {
1626
+ background: transparent !important;
1627
+ border: none !important;
1628
+ box-shadow: none !important;
1629
  }
1630
 
1631
+ /* Radio label + options on one line (Block = BlockTitle span + .wrap) */
1632
+ .inline-radio {
1633
+ display: flex !important;
1634
+ flex-direction: row !important;
1635
+ align-items: center !important;
1636
+ flex-wrap: wrap !important;
1637
+ gap: 12px !important;
1638
+ padding: 0 !important;
1639
+ border: none !important;
1640
+ background: transparent !important;
1641
+ box-shadow: none !important;
1642
  }
1643
+ .inline-radio > span { margin: 0 !important; flex: 0 0 auto !important; }
1644
+ .inline-radio .wrap { flex: 0 0 auto !important; margin: 0 !important; }
1645
+
1646
+ /* ── Image information line under the result ─────────── */
1647
+ /* gr.HTML puts elem_classes on BOTH the outer Block and the inner .prose div,
1648
+ so draw the box once on the inner one and strip the outer wrapper. */
1649
+ .block.img-info {
1650
+ border: none !important;
1651
+ background: transparent !important;
1652
+ padding: 0 !important;
1653
+ box-shadow: none !important;
1654
+ }
1655
+ .img-info.prose {
1656
+ border: 1px solid #e7e5e1 !important;
1657
+ border-radius: 10px !important;
1658
+ background: #fff !important;
1659
+ padding: 12px 14px !important;
1660
+ margin: 4px 0 8px !important;
1661
+ }
1662
+ .ii-wrap {
1663
+ display: flex !important;
1664
+ flex-wrap: wrap !important;
1665
+ align-items: center !important;
1666
+ gap: 6px 20px !important;
1667
+ font-size: 12.5px !important;
1668
+ color: #1c1c1a !important;
1669
+ line-height: 1.5 !important;
1670
  }
1671
+ .ii-title { color: #8b8981 !important; white-space: nowrap !important; }
1672
+ .ii-f { white-space: nowrap !important; }
1673
+ .ii-k { color: #5c5a52 !important; font-weight: 700 !important; margin-right: 3px !important; }
1674
 
1675
+ /* ── Feedback: one row (rating + comment + submit) ───── */
1676
+ .feedback-title p { font-size: 14px !important; font-weight: 700 !important; color: #1c1c1a !important; margin: 4px 0 8px !important; }
1677
+ .feedback-row {
1678
+ align-items: center !important;
1679
+ gap: 10px !important;
1680
+ /* wrap to a second line rather than crushing the comment box when the
1681
+ column gets narrow */
1682
+ flex-wrap: wrap !important;
1683
+ margin: 0 !important;
1684
+ }
1685
+ /* kill the per-child block padding/margins that were spreading the row out */
1686
+ .feedback-row > *,
1687
+ .feedback-row .block,
1688
+ .feedback-row .form,
1689
+ .feedback-row .wrap {
1690
+ margin: 0 !important;
1691
+ padding: 0 !important;
1692
+ border: none !important;
1693
+ background: transparent !important;
1694
+ min-width: 0 !important;
1695
+ }
1696
+ /* the comment box (2nd child) keeps a usable width; it takes the free space
1697
+ but never collapses β€” below its floor the row wraps instead */
1698
+ .feedback-row > *:nth-child(2) {
1699
+ flex: 1 1 200px !important;
1700
+ min-width: 170px !important;
1701
+ }
1702
+ /* the reset above must not let the button shrink below its own label */
1703
+ .feedback-row > *:last-child {
1704
+ flex: 0 0 auto !important;
1705
+ width: auto !important;
1706
+ min-width: max-content !important;
1707
+ }
1708
+ .feedback-row button {
1709
+ color: #bf1e2e !important;
1710
+ border: 1px solid #bf1e2e !important;
1711
+ background: #fff !important;
1712
+ font-weight: 700 !important;
1713
+ font-size: 14px !important;
1714
+ white-space: nowrap !important;
1715
+ padding: 9px 16px !important;
1716
+ border-radius: 8px !important;
1717
+ line-height: 1.2 !important;
1718
+ width: auto !important;
1719
+ min-width: max-content !important;
1720
+ }
1721
+ /* the input sits in a container=False textbox, so it has no frame of its own */
1722
+ .feedback-row textarea,
1723
+ .feedback-row input[type="text"] {
1724
+ border: 1px solid #ddd9d2 !important;
1725
+ border-radius: 8px !important;
1726
+ background: #fff !important;
1727
+ padding: 9px 12px !important;
1728
+ font-size: 14px !important;
1729
+ line-height: 1.3 !important;
1730
+ resize: none !important;
1731
+ overflow: hidden !important;
1732
  }
1733
 
1734
+ /* Star rating: single glyphs, reversed DOM + row-reverse so :checked fills left→right */
1735
+ .star-rating, .star-rating * { overflow: visible !important; max-height: none !important; }
1736
+ .star-rating { border: none !important; background: transparent !important; width: auto !important; min-width: max-content !important; flex: 0 0 auto !important; margin: 0 !important; }
1737
+ .star-rating > * { width: auto !important; min-width: max-content !important; }
1738
+ .star-rating .wrap {
1739
+ display: flex !important;
1740
+ flex-direction: row-reverse !important;
1741
+ justify-content: flex-end !important;
1742
+ gap: 2px !important;
1743
+ flex-wrap: nowrap !important;
1744
  }
1745
+ .star-rating label {
1746
+ border: none !important;
1747
+ background: transparent !important;
1748
+ box-shadow: none !important;
1749
+ padding: 0 !important;
1750
+ margin: 0 !important;
1751
+ cursor: pointer !important;
1752
+ line-height: 1 !important;
1753
  }
1754
+ .star-rating label input { position: absolute !important; opacity: 0 !important; width: 0 !important; height: 0 !important; pointer-events: none !important; margin: 0 !important; }
1755
+ .star-rating label span { font-size: 0 !important; color: transparent !important; }
1756
+ .star-rating label span::before {
1757
+ content: "β˜…";
1758
+ font-size: 19px !important;
1759
+ color: #dcd8d0 !important;
1760
+ transition: color .12s ease;
1761
  }
1762
+ /* hover preview: fill hovered + all visually-left (DOM-following) siblings */
1763
+ .star-rating label:hover span::before,
1764
+ .star-rating label:hover ~ label span::before { color: #ef8b93 !important; }
1765
+ /* selection: fill checked + all visually-left (DOM-following) siblings */
1766
+ .star-rating label:has(input:checked) span::before,
1767
+ .star-rating label:has(input:checked) ~ label span::before { color: #bf1e2e !important; }
1768
+ """
1769
+
1770
+ HOWTO_TITLE_SEG = "### β“˜ How to use (Segmentation)"
1771
+ HOWTO_TITLE_COUNT = "### β“˜ How to use (Counting)"
1772
+ HOWTO_TITLE_TRACK = "### β“˜ How to use (Tracking)"
1773
+
1774
+ HOWTO_SEG = """
1775
+ **Instructions:**
1776
+ 1. Select an example image from the Example Image Gallery or upload your own image (**Please see the "Image Upload Requirements" section before uploading**)
1777
+ 2. (Optional) Specify a target object with a bounding box and select "Yes", or click "Run Segmentation" directly
1778
+ 3. Click "Run Segmentation"
1779
+ 4. View the segmentation results (you can adjust the overlay opacity by sliding the opacity bar below the visualization), download the original predicted mask (.tif format); if needed, click "Clear Selection" to choose a new image
1780
+
1781
+ πŸ’‘ Want to reuse an image? Upload it under "βž• Upload New Example Image to Gallery" and click "βž• Add to Gallery". The uploaded image will appear at the front of the gallery, and can be loaded by clicking its thumbnail. Gallery additions last for the current session only and are cleared when you reload the page.
1782
+
1783
+ 🀘 Tell us about your experience by rating and submitting feedback, which would greatly help us improve the framework!
1784
+ """
1785
+
1786
+ HOWTO_COUNT = """
1787
+ **Instructions:**
1788
+ 1. Select an example image from the Example Image Gallery or upload your own image (**Please see the "Image Upload Requirements" section before uploading**)
1789
+ 2. (Optional) Specify a target object with a bounding box and select "Yes", or click "Run Counting" directly
1790
+ 3. Click "Run Counting"
1791
+ 4. View the density map (you can adjust the density opacity by sliding the opacity bar below the visualization), download the original prediction (.npy format); if needed, click "Clear Selection" to choose a new image to run
1792
+
1793
+ πŸ’‘ Want to reuse an image? Upload it under "βž• Upload New Example Image to Gallery" and click "βž• Add to Gallery". The uploaded image will appear at the front of the gallery, and can be loaded by clicking its thumbnail. Gallery additions last for the current session only and are cleared when you reload the page.
1794
+
1795
+ 🀘 Tell us about your experience by rating and submitting feedback, which would greatly help us improve the framework!
1796
+ """
1797
+
1798
+ # The whole stylesheet is written for a light background, so dark mode renders
1799
+ # badly. Gradio has no server-side switch: the frontend picks light/dark from
1800
+ # the `__theme` query param, else the OS preference. Pin the param, and also
1801
+ # strip the `dark` class directly so there's no flash before the reload.
1802
+ FORCE_LIGHT_JS = """
1803
+ function () {
1804
+ document.documentElement.classList.remove('dark');
1805
+ document.body.classList.remove('dark');
1806
+ const url = new URL(window.location);
1807
+ if (url.searchParams.get('__theme') !== 'light') {
1808
+ url.searchParams.set('__theme', 'light');
1809
+ window.location.replace(url.href);
1810
+ }
1811
  }
1812
  """
1813
 
1814
+ HOWTO_TRACK = """
1815
+ **Instructions:**
1816
+ 1. Select a video from the example library or Upload your own video ZIP file. The ZIP should contain a sequence of TIF images named in chronological order (e.g., t000.tif, t001.tif...)
1817
+ 2. (Optional) Specify a target object with a bounding box on the first frame and select "Yes", or click "Run Tracking" directly
1818
+ 3. Click "Run Tracking"
1819
+ 4. View the tracking results (you can adjust the overlay opacity by sliding the opacity bar below the visualization), download the CTC format results; if needed, click "Clear Selection" to choose a new ZIP file to run
1820
+
1821
+ πŸ’‘ Want to reuse a video? Upload the ZIP under "βž• Upload New Example Image to Gallery" and click "βž• Add to Gallery". The uploaded video will appear at the front of the gallery, and can be loaded by clicking its thumbnail. Gallery additions last for the current session only and are cleared when you reload the page.
1822
+
1823
+ 🀘 Tell us about your experience by rating and submitting feedback, which would greatly help us improve the framework!
1824
+ """
1825
+
1826
  with gr.Blocks(
1827
  title="Microscopy Analysis Suite",
1828
  theme=gr.themes.Soft(
1829
+ primary_hue=gr.themes.Color(c50="#fdeceb", c100="#fbd6d3", c200="#f5a8a3", c300="#ef7a73",
1830
+ c400="#c94a44", c500="#bf1e2e", c600="#a9192a", c700="#8f1424",
1831
+ c800="#75101e", c900="#5c0c17", c950="#42080f"),
1832
+ secondary_hue=gr.themes.Color(c50="#fdeceb", c100="#fbd6d3", c200="#f5a8a3", c300="#ef7a73",
1833
+ c400="#c94a44", c500="#bf1e2e", c600="#a9192a", c700="#8f1424",
1834
+ c800="#75101e", c900="#5c0c17", c950="#42080f"),
1835
  neutral_hue=gr.themes.colors.slate,
1836
  # A weight must be listed here to be usable; the default is (400, 600).
1837
  font=gr.themes.GoogleFont("Inter", weights=(400, 600, 700, 800)),
1838
  ),
1839
  css=CSS,
1840
+ js=FORCE_LIGHT_JS,
1841
  ) as demo:
1842
+ with gr.Row(elem_id="header_row"):
1843
+ gr.Markdown("# πŸ”¬ MicroscopyMatching: Microscopy Image Analysis Suite", elem_id="header_title")
1844
+ how_to_use_btn = gr.Button("β“˜ How to use", elem_id="how_to_use_btn", scale=0)
1845
+ with gr.Column(visible=False, elem_id="how_to_use_modal") as how_to_use_modal:
1846
+ how_to_use_title = gr.Markdown(HOWTO_TITLE_SEG, elem_id="how_to_use_title")
1847
+ howto_md = gr.Markdown(HOWTO_SEG)
1848
+ close_how_to_use_btn = gr.Button("Close", elem_id="close_how_to_use_btn", size="sm")
1849
+ how_to_use_btn.click(lambda: gr.update(visible=True), outputs=how_to_use_modal)
1850
+ close_how_to_use_btn.click(lambda: gr.update(visible=False), outputs=how_to_use_modal)
 
 
 
 
 
 
 
 
 
 
1851
 
1852
 
1853
  # Must stay a callable: a plain value is deepcopied, so every session would
 
1864
 
1865
  with gr.Tabs():
1866
  # ===== Tab 1: Segmentation =====
1867
+ with gr.Tab("Segment") as seg_tab:
1868
+ # gr.Markdown("## Instance Segmentation of Microscopic Objects")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1869
 
1870
  with gr.Row():
1871
  with gr.Column(scale=1):
1872
  gr.Markdown("πŸ–ΌοΈ Upload Image (Optional: Provide a Bounding Box)", elem_classes="frame-heading")
1873
+ with gr.Column(elem_classes="section-card"):
1874
+ annotator = BBoxAnnotator(
1875
+ show_label=False,
1876
+ categories=["cell"],
1877
+ elem_classes="uniform-height-input",
1878
+ )
1879
+ seg_frame_slider = gr.Slider(
1880
+ minimum=1, maximum=1, step=1, value=1,
1881
+ label="πŸ“š Stack frame (choose frame to use)",
1882
+ visible=False,
1883
+ )
1884
+ seg_zplane_slider = gr.Slider(
1885
+ minimum=1, maximum=1, step=1, value=1,
1886
+ label="πŸ”¬ Z-plane (choose plane to use)",
1887
+ visible=False,
1888
+ )
1889
 
1890
  # Example Images Gallery
1891
  gr.Markdown("πŸ“ Example Image Gallery", elem_classes="frame-heading")
1892
+ with gr.Column(elem_classes="section-card"):
1893
+ example_gallery = gr.Gallery(
1894
+ show_label=True,
1895
+ columns=len(example_images_seg),
1896
+ rows=1,
1897
+ height=120,
1898
+ object_fit="cover",
1899
+ show_download_button=False
1900
+ )
1901
+
1902
+ with gr.Column(elem_classes="bbox-slot"):
1903
  use_box_radio = gr.Radio(
1904
  choices=["Yes", "No"],
1905
  value="No",
1906
+ label="πŸ”² Specify Bounding Box?",
1907
+ elem_classes="inline-radio",
1908
  )
1909
  with gr.Row():
1910
  run_seg_btn = gr.Button("▢️ Run Segmentation", variant="primary", size="lg")
1911
  clear_btn = gr.Button("πŸ”„ Clear Selection", variant="secondary")
1912
 
1913
+ with gr.Accordion("πŸ“‹ Image Upload Requirements", open=True, elem_classes="req-panel"):
1914
+ gr.HTML(
1915
+ """
1916
+ <div class="req-warn">⚠️ Do not upload images or data containing protected health information (PHI) or personally identifiable information (PII).</div>
1917
+ <div class="req-row"><span class="req-label">Formats</span><span class="req-val">TIFF / OME-TIFF (8/16/32-bit, stacks, multi-channel), PNG, JPG, and other formats supported by <a href="https://github.com/cgohlke/tifffile" target="_blank">tifffile</a></span></div>
1918
+ <div class="req-row"><span class="req-label">Max size</span><span class="req-val">4096 Γ— 4096 px</span></div>
1919
+ <div class="req-row"><span class="req-label">Channels</span><span class="req-val">RGB (3-channel) or grayscale; channels beyond the first 3 are ignored</span></div>
1920
+ <div class="req-row"><span class="req-label">Stacks</span><span class="req-val">first frame loaded by default; frame slider appears after upload</span></div>
1921
+ <div class="req-note">Alternatively, convert to an 8-bit RGB/grayscale PNG/JPG/TIFF first (e.g. with ImageJ/Fiji).</div>
1922
+ """
1923
+ )
1924
+
1925
  # Upload Example Image
1926
  gr.Markdown("βž• Upload New Example Image to Gallery", elem_classes="frame-heading")
1927
+ with gr.Column(elem_classes="section-card"):
1928
+ image_uploader = gr.File(
1929
+ show_label=False,
1930
+ file_types=["image"] + list(TIFF_EXTENSIONS),
1931
+ type="filepath"
1932
+ )
1933
+ add_to_gallery_btn = gr.Button("βž• Add to Gallery", variant="secondary", size="sm")
1934
+ add_gallery_status = gr.Markdown(visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1935
 
 
 
1936
 
1937
+ with gr.Column(scale=1):
1938
+ with gr.Row(elem_classes="result-header"):
1939
+ gr.Markdown("πŸ“Έ Segmentation Result", elem_classes="frame-heading")
1940
+ download_mask_btn = gr.DownloadButton(
1941
+ "⬇️ Download (.tif)",
1942
+ size="sm",
1943
+ scale=0,
1944
+ elem_classes="result-download",
1945
+ )
1946
+ with gr.Column(elem_classes="section-card"):
1947
+ seg_output = gr.Image(
1948
+ type="pil",
1949
+ show_label=False,
1950
+ elem_classes="uniform-height"
1951
+ )
1952
+ seg_alpha_slider = gr.Slider(
1953
+ minimum=0.0,
1954
+ maximum=1.0,
1955
+ step=0.05,
1956
+ value=0.5,
1957
+ label="Overlay opacity",
1958
+ elem_classes="opacity-row",
1959
+ )
1960
+ seg_info_html = gr.HTML(visible=False, elem_classes="img-info")
1961
+
1962
+ # Feedback: rating + comment + submit on one row
1963
+ gr.Markdown("How was this result?", elem_classes="feedback-title")
1964
+ with gr.Row(elem_classes="feedback-row"):
1965
+ score_slider = gr.Radio(choices=[("5", 5), ("4", 4), ("3", 3), ("2", 2), ("1", 1)], value=5, show_label=False, container=False, scale=0, elem_classes="star-rating")
1966
+ feedback_box = gr.Textbox(placeholder="Tell us about your experience (optional)", lines=1, max_lines=1, show_label=False, container=False, scale=2)
1967
+ submit_feedback_btn = gr.Button("Submit feedback", variant="secondary", scale=0)
1968
+
1969
+ feedback_status = gr.Textbox(
1970
+ label="βœ… Submission Status",
1971
+ lines=1,
1972
+ visible=False
1973
+ )
1974
 
1975
  annotator.upload(
1976
  fn=prepare_uploaded_image,
 
1984
  outputs=annotator
1985
  )
1986
 
1987
+ # Image information line. Driven off the raw-path state so it covers
1988
+ # every way an image arrives (upload, gallery, clear); the frame
1989
+ # slider refreshes the "Frames: n / total" counter.
1990
+ seg_raw_state.change(
1991
+ fn=describe_image_info,
1992
+ inputs=[seg_raw_state, seg_frame_slider],
1993
+ outputs=seg_info_html
1994
+ )
1995
+ seg_frame_slider.release(
1996
+ fn=describe_image_info,
1997
+ inputs=[seg_raw_state, seg_frame_slider],
1998
+ outputs=seg_info_html
1999
+ )
2000
+
2001
  # click event for segmentation
2002
  run_seg_btn.click(
2003
  fn=segment_with_choice,
 
2097
  )
2098
 
2099
  # ===== Tab 2: Counting =====
2100
+ with gr.Tab("Count") as count_tab:
2101
+ # gr.Markdown("## Microscopy Object Counting Analysis")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2102
 
2103
  with gr.Row():
2104
  with gr.Column(scale=1):
2105
  gr.Markdown("πŸ–ΌοΈ Upload Image (Optional: Provide a Bounding Box)", elem_classes="frame-heading")
2106
+ with gr.Column(elem_classes="section-card"):
2107
+ count_annotator = BBoxAnnotator(
2108
+ show_label=False,
2109
+ categories=["cell"],
2110
+ elem_classes="uniform-height-input",
2111
+ )
2112
+ count_frame_slider = gr.Slider(
2113
+ minimum=1, maximum=1, step=1, value=1,
2114
+ label="πŸ“š Stack frame (choose frame to use)",
2115
+ visible=False,
2116
+ )
2117
+ count_zplane_slider = gr.Slider(
2118
+ minimum=1, maximum=1, step=1, value=1,
2119
+ label="πŸ”¬ Z-plane (choose plane to use)",
2120
+ visible=False,
2121
+ )
2122
 
2123
  # Example gallery with "add" functionality
2124
  gr.Markdown("πŸ“ Example Image Gallery", elem_classes="frame-heading")
2125
+ with gr.Column(elem_classes="section-card"):
2126
  count_example_gallery = gr.Gallery(
2127
  show_label=False,
2128
  columns=len(example_images_cnt),
 
2132
  value=example_images_cnt.copy(), # Initialize with examples
2133
  show_download_button=False
2134
  )
2135
+
2136
+ with gr.Column(elem_classes="bbox-slot"):
 
2137
  count_use_box_radio = gr.Radio(
2138
  choices=["Yes", "No"],
2139
  value="No",
2140
+ label="πŸ”² Specify Bounding Box?",
2141
+ elem_classes="inline-radio",
2142
  )
2143
 
2144
  with gr.Row():
2145
  count_btn = gr.Button("▢️ Run Counting", variant="primary", size="lg")
2146
  clear_btn = gr.Button("πŸ”„ Clear Selection", variant="secondary")
2147
+
2148
+ with gr.Accordion("πŸ“‹ Image Upload Requirements", open=True, elem_classes="req-panel"):
2149
+ gr.HTML(
2150
+ """
2151
+ <div class="req-warn">⚠️ Do not upload images or data containing protected health information (PHI) or personally identifiable information (PII).</div>
2152
+ <div class="req-row"><span class="req-label">Formats</span><span class="req-val">TIFF / OME-TIFF (8/16/32-bit, stacks, multi-channel), PNG, JPG, and other formats supported by <a href="https://github.com/cgohlke/tifffile" target="_blank">tifffile</a></span></div>
2153
+ <div class="req-row"><span class="req-label">Max size</span><span class="req-val">4096 Γ— 4096 px</span></div>
2154
+ <div class="req-row"><span class="req-label">Channels</span><span class="req-val">RGB (3-channel) or grayscale; channels beyond the first 3 are ignored</span></div>
2155
+ <div class="req-row"><span class="req-label">Stacks</span><span class="req-val">first frame loaded by default; frame slider appears after upload</span></div>
2156
+ <div class="req-note">Alternatively, convert to an 8-bit RGB/grayscale PNG/JPG/TIFF first (e.g. with ImageJ/Fiji).</div>
2157
+ """
2158
+ )
2159
 
2160
  # Add button to upload new examples
2161
  gr.Markdown("βž• Add Example Image to Gallery", elem_classes="frame-heading")
2162
+ with gr.Column(elem_classes="section-card"):
2163
+ count_image_uploader = gr.File(
2164
+ show_label=False,
2165
+ file_types=["image"] + list(TIFF_EXTENSIONS),
2166
+ type="filepath"
2167
+ )
2168
+ add_to_count_gallery_btn = gr.Button("βž• Add to Gallery", variant="secondary", size="sm")
2169
+ count_add_status = gr.Markdown(visible=False)
2170
 
2171
 
2172
+ with gr.Column(scale=1):
2173
+ with gr.Row(elem_classes="result-header"):
2174
+ gr.Markdown("πŸ“Έ Density Map", elem_classes="frame-heading")
2175
+ download_density_btn = gr.DownloadButton(
2176
+ "⬇️ Download (.npy)",
2177
+ size="sm",
2178
+ scale=0,
2179
+ elem_classes="result-download",
2180
+ )
2181
+ with gr.Column(elem_classes="section-card"):
2182
+ count_output = gr.Image(
2183
+ show_label=False,
2184
+ type="filepath",
2185
+ elem_id="density_map_output"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2186
 
2187
+ )
2188
+ count_alpha_slider = gr.Slider(
2189
+ minimum=0.0,
2190
+ maximum=1.0,
2191
+ step=0.05,
2192
+ value=0.3,
2193
+ label="Density opacity",
2194
+ elem_classes="opacity-row",
2195
+ )
2196
+ count_info_html = gr.HTML(visible=False, elem_classes="img-info")
2197
+ count_status = gr.Textbox(
2198
+ label="πŸ“Š Statistics",
2199
+ lines=2
2200
+ )
2201
 
2202
+ # Feedback: rating + comment + submit on one row
2203
+ gr.Markdown("How was this result?", elem_classes="feedback-title")
2204
+ with gr.Row(elem_classes="feedback-row"):
2205
+ score_slider = gr.Radio(choices=[("5", 5), ("4", 4), ("3", 3), ("2", 2), ("1", 1)], value=5, show_label=False, container=False, scale=0, elem_classes="star-rating")
2206
+ feedback_box = gr.Textbox(placeholder="Tell us about your experience (optional)", lines=1, max_lines=1, show_label=False, container=False, scale=2)
2207
+ submit_feedback_btn = gr.Button("Submit feedback", variant="secondary", scale=0)
2208
+
2209
+ feedback_status = gr.Textbox(
2210
+ label="βœ… Submission Status",
2211
+ lines=1,
2212
+ visible=False
2213
+ )
2214
 
 
 
 
 
 
 
2215
  # State for managing gallery images
2216
  count_user_examples = gr.State(example_images_cnt.copy())
2217
 
 
2258
  outputs=count_annotator
2259
  )
2260
 
2261
+ # Image information line (same pattern as the Segment tab).
2262
+ count_raw_state.change(
2263
+ fn=describe_image_info,
2264
+ inputs=[count_raw_state, count_frame_slider],
2265
+ outputs=count_info_html
2266
+ )
2267
+ count_frame_slider.release(
2268
+ fn=describe_image_info,
2269
+ inputs=[count_raw_state, count_frame_slider],
2270
+ outputs=count_info_html
2271
+ )
2272
+
2273
  # When user selects from gallery, load into annotator
2274
  def load_from_count_gallery(evt: gr.SelectData, all_imgs):
2275
  """Load a gallery image, reusing the upload path."""
 
2330
  )
2331
 
2332
  # ===== Tab 3: Tracking =====
2333
+ with gr.Tab("Track") as track_tab:
2334
+ # gr.Markdown("## Microscopy Object Video Tracking - Supports ZIP Upload")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2335
 
2336
  with gr.Row():
2337
  with gr.Column(scale=1):
2338
+ gr.Markdown("πŸ“¦ Upload Image Sequence in ZIP File (Optional: Provide a Bounding Box)", elem_classes="frame-heading")
2339
+ with gr.Column(elem_classes="section-card"):
2340
+ track_zip_upload = gr.File(
2341
+ show_label=False,
2342
+ file_types=[".zip"]
2343
+ )
2344
 
2345
  # First frame annotation for bounding box (heading + annotator
2346
  # share one column so they show/hide together)
2347
  with gr.Column(visible=False) as track_annot_group:
2348
  gr.Markdown("πŸ–ΌοΈ (Optional) First Frame Bounding Box Annotation", elem_classes="frame-heading")
2349
+ with gr.Column(elem_classes="section-card"):
2350
+ track_first_frame_annotator = BBoxAnnotator(
2351
+ show_label=False,
2352
+ categories=["cell"],
2353
+ elem_classes="uniform-height-input",
2354
+ )
2355
 
2356
  # Example ZIP gallery
2357
  gr.Markdown("πŸ“ Example Video Gallery (Click to Select)", elem_classes="frame-heading")
2358
+ with gr.Column(elem_classes="section-card"):
2359
+ track_example_gallery = gr.Gallery(
2360
+ show_label=False,
2361
+ columns=10,
2362
+ rows=1,
2363
+ height=120,
2364
+ object_fit="contain",
2365
+ show_download_button=False
2366
+ )
2367
+
2368
+ with gr.Column(elem_classes="bbox-slot"):
2369
  track_use_box_radio = gr.Radio(
2370
  choices=["Yes", "No"],
2371
  value="No",
2372
+ label="πŸ”² Specify Bounding Box?",
2373
+ elem_classes="inline-radio",
2374
  )
2375
 
2376
  with gr.Row():
2377
  track_btn = gr.Button("▢️ Run Tracking", variant="primary", size="lg")
2378
  clear_btn = gr.Button("πŸ”„ Clear Selection", variant="secondary")
2379
+
2380
+ with gr.Accordion("πŸ“‹ Upload Requirements", open=True, elem_classes="req-panel"):
2381
+ gr.HTML(
2382
+ """
2383
+ <div class="req-warn">⚠️ Do not upload images or data containing protected health information (PHI) or personally identifiable information (PII).</div>
2384
+ <div class="req-row"><span class="req-label">Format</span><span class="req-val">A <code>.zip</code> containing a sequence of <code>.tif</code> / <code>.tiff</code> frames</span></div>
2385
+ <div class="req-row"><span class="req-label">Frame order</span><span class="req-val">Sorted naturally by filename; use consistent names such as frame_001.tif, frame_002.tif, …</span></div>
2386
+ <div class="req-row"><span class="req-label">Image size</span><span class="req-val">Every frame must share the same width and height</span></div>
2387
+ <div class="req-row"><span class="req-label">Channels</span><span class="req-val">Grayscale and RGB TIFF frames; beyond 3 channels only the first three are used</span></div>
2388
+ <div class="req-row"><span class="req-label">Bit depth</span><span class="req-val">8-bit and 16-bit TIFFs</span></div>
2389
+ <div class="req-note">You can use ImageJ/Fiji or a similar tool to export your video as a numbered TIFF image sequence, then compress the frames into a ZIP before uploading.</div>
2390
+ """
2391
+ )
2392
+
2393
  # Add to gallery button
2394
  gr.Markdown("βž• Add ZIP to Example Gallery", elem_classes="frame-heading")
2395
+ with gr.Column(elem_classes="section-card"):
2396
+ track_gallery_upload = gr.File(
2397
+ show_label=False,
2398
+ file_types=[".zip"],
2399
+ type="filepath"
2400
+ )
2401
 
2402
+ with gr.Column(scale=1):
2403
+ with gr.Row(elem_classes="result-header"):
2404
+ gr.Markdown("πŸ“Έ Tracking Visualization", elem_classes="frame-heading")
2405
+ # keep the wrapper so the existing show/hide wiring still
2406
+ # targets a container, with the button inside it
2407
+ with gr.Column(visible=False, scale=0, min_width=170) as track_dl_group:
2408
+ track_download = gr.DownloadButton(
2409
+ "⬇️ Download (.zip)",
2410
+ size="sm",
2411
+ elem_classes="result-download",
2412
+ )
2413
+ with gr.Column(elem_classes="section-card"):
2414
+ track_first_frame_preview = gr.Image(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2415
  show_label=False,
2416
+ type="filepath",
2417
+ # height=400,
2418
+ elem_classes="uniform-height",
2419
+ interactive=False
2420
+ )
2421
+ track_alpha_slider = gr.Slider(
2422
+ minimum=0.0,
2423
+ maximum=1.0,
2424
+ step=0.05,
2425
+ value=0.3,
2426
+ label="Overlay opacity",
2427
+ elem_classes="opacity-row",
2428
  )
2429
 
2430
+ track_output = gr.Textbox(
2431
+ label="πŸ“Š Tracking Information",
2432
+ lines=9,
2433
+ max_lines=20,
2434
+ interactive=False
2435
+ )
 
 
 
 
 
 
 
 
 
 
 
 
2436
 
2437
+ # Feedback: rating + comment + submit on one row
2438
+ gr.Markdown("How was this result?", elem_classes="feedback-title")
2439
+ with gr.Row(elem_classes="feedback-row"):
2440
+ score_slider = gr.Radio(choices=[("5", 5), ("4", 4), ("3", 3), ("2", 2), ("1", 1)], value=5, show_label=False, container=False, scale=0, elem_classes="star-rating")
2441
+ feedback_box = gr.Textbox(placeholder="Tell us about your experience (optional)", lines=1, max_lines=1, show_label=False, container=False, scale=2)
2442
+ submit_feedback_btn = gr.Button("Submit feedback", variant="secondary", scale=0)
2443
+
2444
+ feedback_status = gr.Textbox(
2445
+ label="βœ… Submission Status",
2446
+ lines=1,
2447
+ visible=False
2448
+ )
2449
 
2450
  # State for tracking examples
2451
  track_user_examples = gr.State(example_tracking_zips.copy())
 
2662
  inputs=[current_query_id, score_slider, feedback_box, annotator],
2663
  outputs=[feedback_status, feedback_status]
2664
  )
2665
+
2666
+ seg_tab.select(lambda: (HOWTO_TITLE_SEG, HOWTO_SEG), outputs=[how_to_use_title, howto_md])
2667
+ count_tab.select(lambda: (HOWTO_TITLE_COUNT, HOWTO_COUNT), outputs=[how_to_use_title, howto_md])
2668
+ track_tab.select(lambda: (HOWTO_TITLE_TRACK, HOWTO_TRACK), outputs=[how_to_use_title, howto_md])
2669
+
2670
+ gr.Markdown(
2671
+ """
2672
+ **Supporting three key tasks:**
2673
+
2674
+ 🎨 **Segmentation**: Instance segmentation of microscopic objects
2675
+
2676
+ πŸ”’ **Counting**: Counting microscopic objects based on density maps
2677
+
2678
+ 🎬 **Tracking**: Tracking microscopic objects in video sequences
2679
+
2680
+ πŸ“’ **Note:** This project is currently available with usage limits for research trial use and feedback collection. Response speed may vary depending on GPU allocation queues. We plan to release a free public version in the future. We are actively improving the toolkit and greatly appreciate your feedback!
2681
+ """,
2682
+ elem_id="footer_note"
2683
+ )
2684
+
2685
 
2686
  if __name__ == "__main__":
2687
  demo.queue().launch(
tracking_one.py CHANGED
@@ -938,6 +938,16 @@ class TrackingModule(pl.LightningModule):
938
 
939
  self.eval()
940
  imgs, imgs_raw, images_stable, tra_imgs, imgs_01, height, width = load_track_images(file_dir)
 
 
 
 
 
 
 
 
 
 
941
  imgs_stable = torch.from_numpy(images_stable).float().to(self.device)
942
  imgs_enc = torch.from_numpy(imgs).float().to(self.device)
943
 
 
938
 
939
  self.eval()
940
  imgs, imgs_raw, images_stable, tra_imgs, imgs_01, height, width = load_track_images(file_dir)
941
+ if boxes is not None:
942
+ boxes = torch.as_tensor(boxes, dtype=torch.float32)
943
+ if boxes.ndim == 2:
944
+ boxes = boxes.unsqueeze(0)
945
+ boxes = boxes * torch.tensor(
946
+ [512.0 / width, 512.0 / height, 512.0 / width, 512.0 / height],
947
+ dtype=torch.float32,
948
+ )
949
+ boxes[..., 0::2].clamp_(0, 512)
950
+ boxes[..., 1::2].clamp_(0, 512)
951
  imgs_stable = torch.from_numpy(images_stable).float().to(self.device)
952
  imgs_enc = torch.from_numpy(imgs).float().to(self.device)
953