Spaces:
Running on Zero
Running on Zero
Commit Β·
f129ffb
1
Parent(s): 5c4d9d8
update on ui
Browse files- _utils/image_io.py +20 -0
- app.py +869 -475
- 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 =
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 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:
|
| 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:
|
| 1125 |
font-weight: 700 !important;
|
| 1126 |
-
color: #
|
| 1127 |
-
letter-spacing: -0.
|
| 1128 |
-
margin-bottom:
|
| 1129 |
}
|
| 1130 |
.gradio-container .prose h3 {
|
| 1131 |
font-size: 1rem !important;
|
| 1132 |
font-weight: 600 !important;
|
| 1133 |
-
color: #
|
| 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: #
|
| 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: #
|
| 1150 |
line-height: 1.7 !important;
|
| 1151 |
}
|
| 1152 |
|
| 1153 |
-
/* ββ Top-level header section βββ
|
| 1154 |
.gradio-container > .gap > .prose:first-child {
|
| 1155 |
-
background:
|
| 1156 |
-
border:
|
| 1157 |
-
border-radius:
|
| 1158 |
-
padding:
|
| 1159 |
-
margin-bottom:
|
| 1160 |
-
box-shadow:
|
| 1161 |
}
|
| 1162 |
|
| 1163 |
-
/* ββ
|
| 1164 |
-
.tabs > .tab-
|
| 1165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1166 |
margin-bottom: 20px !important;
|
| 1167 |
-
gap: 4px !important;
|
| 1168 |
}
|
| 1169 |
-
.tabs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1170 |
font-size: 15px !important;
|
| 1171 |
-
font-weight:
|
| 1172 |
-
|
| 1173 |
-
border-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
.tabs button:
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1185 |
}
|
| 1186 |
|
| 1187 |
/* ββ Buttons βββββββββββββββββββββββββββββββββββββββββββ */
|
| 1188 |
button.primary {
|
| 1189 |
-
background:
|
| 1190 |
border: none !important;
|
| 1191 |
-
border-radius:
|
| 1192 |
color: #fff !important;
|
| 1193 |
font-weight: 600 !important;
|
| 1194 |
font-size: 15px !important;
|
| 1195 |
-
box-shadow:
|
| 1196 |
-
transition:
|
| 1197 |
}
|
| 1198 |
button.primary:hover {
|
| 1199 |
-
|
| 1200 |
-
box-shadow: 0 6px 20px rgba(14,165,233,0.45) !important;
|
| 1201 |
}
|
| 1202 |
button.secondary {
|
| 1203 |
-
border-radius:
|
| 1204 |
font-weight: 500 !important;
|
| 1205 |
-
border: 1.5px solid #
|
| 1206 |
-
color: #
|
| 1207 |
transition: border-color 0.12s, color 0.12s, background 0.12s !important;
|
| 1208 |
}
|
| 1209 |
button.secondary:hover {
|
| 1210 |
-
border-color: #
|
| 1211 |
-
color: #
|
| 1212 |
-
background: #
|
| 1213 |
}
|
| 1214 |
|
| 1215 |
/* ββ Blocks and panels βββββββββββββββββββββββββββββββββ */
|
| 1216 |
-
.gradio-container .block { border-radius:
|
| 1217 |
.gradio-container .gr-form,
|
| 1218 |
.gradio-container .gr-box,
|
| 1219 |
.gradio-container .gr-panel {
|
| 1220 |
-
border-radius:
|
| 1221 |
-
border-color: #
|
| 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:
|
| 1292 |
-
background: #
|
| 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 |
-
/* ββ
|
| 1310 |
-
.
|
| 1311 |
-
|
| 1312 |
-
|
| 1313 |
-
|
| 1314 |
-
margin-top: 0 !important;
|
| 1315 |
-
margin-bottom: 10px !important;
|
| 1316 |
-
padding-bottom: 8px !important;
|
| 1317 |
-
border-bottom: 2px solid #e0f2fe !important;
|
| 1318 |
}
|
| 1319 |
-
.
|
| 1320 |
-
|
| 1321 |
-
border: 1px solid #e2e8f0 !important;
|
| 1322 |
-
border-radius: 10px !important;
|
| 1323 |
-
padding: 12px 18px !important;
|
| 1324 |
-
margin-bottom: 16px !important;
|
| 1325 |
}
|
| 1326 |
-
.
|
| 1327 |
-
|
| 1328 |
-
font-
|
| 1329 |
-
color: #
|
| 1330 |
}
|
| 1331 |
-
.
|
| 1332 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1333 |
}
|
| 1334 |
|
| 1335 |
-
/*
|
| 1336 |
-
|
| 1337 |
-
|
| 1338 |
-
|
| 1339 |
-
|
| 1340 |
-
.dark .gradio-container .prose h1 {
|
| 1341 |
-
color: #e2e8f0 !important;
|
| 1342 |
}
|
| 1343 |
-
|
| 1344 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1345 |
}
|
| 1346 |
-
|
| 1347 |
-
|
| 1348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1349 |
}
|
| 1350 |
|
| 1351 |
-
/* ββ
|
| 1352 |
-
|
| 1353 |
-
|
| 1354 |
-
|
| 1355 |
-
|
| 1356 |
}
|
|
|
|
|
|
|
| 1357 |
|
| 1358 |
-
/* ββ
|
| 1359 |
-
.
|
| 1360 |
-
border
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1361 |
}
|
| 1362 |
-
.
|
| 1363 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1364 |
}
|
| 1365 |
-
.
|
| 1366 |
-
|
| 1367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1368 |
}
|
| 1369 |
-
.
|
| 1370 |
-
|
| 1371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1372 |
}
|
|
|
|
| 1373 |
|
| 1374 |
-
/* ββ
|
| 1375 |
-
|
| 1376 |
-
|
| 1377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1378 |
background: transparent !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1379 |
}
|
| 1380 |
-
.
|
| 1381 |
-
|
| 1382 |
-
|
| 1383 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1384 |
}
|
| 1385 |
|
| 1386 |
-
/*
|
| 1387 |
-
.
|
| 1388 |
-
|
| 1389 |
-
.
|
| 1390 |
-
|
|
|
|
|
|
|
|
|
|
| 1391 |
}
|
| 1392 |
|
| 1393 |
-
/*
|
| 1394 |
-
.
|
| 1395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1396 |
}
|
| 1397 |
-
.
|
| 1398 |
-
.
|
| 1399 |
-
|
| 1400 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1401 |
}
|
|
|
|
|
|
|
|
|
|
| 1402 |
|
| 1403 |
-
/* ββ
|
| 1404 |
-
.
|
| 1405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1406 |
}
|
| 1407 |
|
| 1408 |
-
/*
|
| 1409 |
-
.
|
| 1410 |
-
|
| 1411 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1412 |
}
|
| 1413 |
-
.
|
| 1414 |
-
|
| 1415 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1416 |
}
|
| 1417 |
-
.
|
| 1418 |
-
.
|
| 1419 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1420 |
}
|
| 1421 |
-
|
| 1422 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1423 |
}
|
| 1424 |
"""
|
| 1425 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1426 |
with gr.Blocks(
|
| 1427 |
title="Microscopy Analysis Suite",
|
| 1428 |
theme=gr.themes.Soft(
|
| 1429 |
-
primary_hue=gr.themes.
|
| 1430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 1438 |
-
"""
|
| 1439 |
-
|
| 1440 |
-
|
| 1441 |
-
|
| 1442 |
-
|
| 1443 |
-
|
| 1444 |
-
|
| 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("
|
| 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 |
-
|
| 1508 |
-
|
| 1509 |
-
|
| 1510 |
-
|
| 1511 |
-
|
| 1512 |
-
|
| 1513 |
-
|
| 1514 |
-
|
| 1515 |
-
|
| 1516 |
-
|
| 1517 |
-
|
| 1518 |
-
|
| 1519 |
-
|
| 1520 |
-
|
|
|
|
|
|
|
| 1521 |
|
| 1522 |
# Example Images Gallery
|
| 1523 |
gr.Markdown("π Example Image Gallery", elem_classes="frame-heading")
|
| 1524 |
-
|
| 1525 |
-
|
| 1526 |
-
|
| 1527 |
-
|
| 1528 |
-
|
| 1529 |
-
|
| 1530 |
-
|
| 1531 |
-
|
| 1532 |
-
|
| 1533 |
-
|
| 1534 |
-
with gr.
|
| 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 |
-
|
| 1547 |
-
|
| 1548 |
-
|
| 1549 |
-
|
| 1550 |
-
|
| 1551 |
-
|
| 1552 |
-
|
| 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 |
-
|
| 1598 |
-
|
| 1599 |
-
|
| 1600 |
-
|
| 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("
|
| 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 |
-
|
| 1749 |
-
|
| 1750 |
-
|
| 1751 |
-
|
| 1752 |
-
|
| 1753 |
-
|
| 1754 |
-
|
| 1755 |
-
|
| 1756 |
-
|
| 1757 |
-
|
| 1758 |
-
|
| 1759 |
-
|
| 1760 |
-
|
| 1761 |
-
|
|
|
|
|
|
|
| 1762 |
|
| 1763 |
# Example gallery with "add" functionality
|
| 1764 |
gr.Markdown("π Example Image Gallery", elem_classes="frame-heading")
|
| 1765 |
-
with gr.
|
| 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 |
-
|
| 1791 |
-
|
| 1792 |
-
|
| 1793 |
-
|
| 1794 |
-
|
| 1795 |
-
|
| 1796 |
-
|
|
|
|
| 1797 |
|
| 1798 |
|
| 1799 |
-
with gr.Column(scale=
|
| 1800 |
-
gr.
|
| 1801 |
-
|
| 1802 |
-
|
| 1803 |
-
|
| 1804 |
-
|
| 1805 |
-
|
| 1806 |
-
|
| 1807 |
-
|
| 1808 |
-
|
| 1809 |
-
|
| 1810 |
-
|
| 1811 |
-
|
| 1812 |
-
|
| 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 |
-
|
| 1834 |
-
|
| 1835 |
-
|
| 1836 |
-
|
| 1837 |
-
|
| 1838 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1839 |
|
| 1840 |
-
|
| 1841 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("
|
| 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 |
-
|
| 1976 |
-
|
| 1977 |
-
|
| 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 |
-
|
| 1985 |
-
|
| 1986 |
-
|
| 1987 |
-
|
|
|
|
|
|
|
| 1988 |
|
| 1989 |
# Example ZIP gallery
|
| 1990 |
gr.Markdown("π Example Video Gallery (Click to Select)", elem_classes="frame-heading")
|
| 1991 |
-
|
| 1992 |
-
|
| 1993 |
-
|
| 1994 |
-
|
| 1995 |
-
|
| 1996 |
-
|
| 1997 |
-
|
| 1998 |
-
|
| 1999 |
-
|
| 2000 |
-
|
|
|
|
| 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 |
-
|
| 2014 |
-
|
| 2015 |
-
|
| 2016 |
-
|
| 2017 |
-
|
|
|
|
| 2018 |
|
| 2019 |
-
with gr.Column(scale=
|
| 2020 |
-
gr.
|
| 2021 |
-
|
| 2022 |
-
|
| 2023 |
-
|
| 2024 |
-
|
| 2025 |
-
|
| 2026 |
-
|
| 2027 |
-
|
| 2028 |
-
|
| 2029 |
-
|
| 2030 |
-
|
| 2031 |
-
|
| 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 |
-
|
| 2050 |
-
|
| 2051 |
-
|
| 2052 |
-
|
| 2053 |
-
|
| 2054 |
-
|
| 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 |
-
|
| 2069 |
-
|
| 2070 |
-
|
| 2071 |
-
|
| 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 |
|