Spaces:
Running on Zero
Running on Zero
Commit Β·
28081ca
1
Parent(s): cd351de
clean up
Browse files- _utils/image_io.py +14 -39
- app.py +53 -65
_utils/image_io.py
CHANGED
|
@@ -1,28 +1,8 @@
|
|
| 1 |
"""Image standardization.
|
| 2 |
|
| 3 |
-
|
| 4 |
-
multi
|
| 5 |
-
|
| 6 |
-
on-screen preview and the model, so what you see is what gets segmented.
|
| 7 |
-
|
| 8 |
-
Which axis means what is read from the file's own metadata (``series.axes``
|
| 9 |
-
from tifffile: 'C'=channel, 'S'=RGB samples, 'Z'/'T'=stack, 'Y'/'X'=spatial)
|
| 10 |
-
rather than guessed from the array shape -- guessing cannot tell a 3-channel
|
| 11 |
-
(C, Y, X) image apart from a 3-slice (Z, Y, X) stack. Only when a file names
|
| 12 |
-
no axes at all (tifffile reports 'Q' = unknown) do we fall back to the dominant
|
| 13 |
-
convention: the first unknown axis of size 2-4 is the channel axis.
|
| 14 |
-
|
| 15 |
-
Reduction policy:
|
| 16 |
-
|
| 17 |
-
* multi-page / Z / time stacks -> one frame (default: the first)
|
| 18 |
-
* more than 3 channels -> first 3 channels (as R, G, B)
|
| 19 |
-
* intensity -> 0-255 -> 1st-99th percentile auto-contrast for
|
| 20 |
-
16-bit/float input (outlier-robust: a hot /
|
| 21 |
-
saturated pixel would make plain min/max
|
| 22 |
-
scaling collapse the real signal to black)
|
| 23 |
-
|
| 24 |
-
Standard 8-bit inputs (PNG/JPG/8-bit TIFF) are passed through unchanged in
|
| 25 |
-
value; only channel layout is normalized.
|
| 26 |
"""
|
| 27 |
|
| 28 |
import os
|
|
@@ -37,21 +17,19 @@ try:
|
|
| 37 |
except ImportError: # pragma: no cover - tifffile is a project dependency
|
| 38 |
tifffile = None
|
| 39 |
|
| 40 |
-
#
|
| 41 |
-
#
|
| 42 |
-
RECOMMENDED_SIZE = 512
|
| 43 |
-
WARN_SIZE = 3072
|
| 44 |
-
MAX_SIZE = 4096
|
| 45 |
-
#
|
| 46 |
-
#
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
#
|
| 51 |
-
_SPATIAL = ("Y", "X")
|
| 52 |
_CHANNEL = ("C", "S") # C = separate channel planes, S = interleaved RGB samples
|
| 53 |
_UNKNOWN = ("Q", "I") # file named no axis; only these may be *guessed* as channels
|
| 54 |
-
# Anything else (Z, T, ...) is a stack axis and is indexed by frame.
|
| 55 |
|
| 56 |
|
| 57 |
def _read_array(path):
|
|
@@ -145,7 +123,6 @@ def _reduce_to_hwc(arr, axes, frame=0):
|
|
| 145 |
"""
|
| 146 |
axes = list(axes)
|
| 147 |
|
| 148 |
-
# Drop size-1 axes, keeping arr and axes in sync.
|
| 149 |
for i in range(len(axes) - 1, -1, -1):
|
| 150 |
if arr.shape[i] == 1:
|
| 151 |
arr = arr.reshape(arr.shape[:i] + arr.shape[i + 1:])
|
|
@@ -153,7 +130,6 @@ def _reduce_to_hwc(arr, axes, frame=0):
|
|
| 153 |
|
| 154 |
_, planned, _ = _plan_axes(arr.shape, axes)
|
| 155 |
|
| 156 |
-
# Apply the plan to the array: infer the channel axis, then move it last.
|
| 157 |
if "C" in planned and not any(a in _CHANNEL for a in axes):
|
| 158 |
for i, (a, d) in enumerate(zip(axes, arr.shape)):
|
| 159 |
if a in _UNKNOWN and d in (2, 3, 4):
|
|
@@ -164,7 +140,6 @@ def _reduce_to_hwc(arr, axes, frame=0):
|
|
| 164 |
arr = np.moveaxis(arr, ci, -1)
|
| 165 |
axes.append(axes.pop(ci))
|
| 166 |
|
| 167 |
-
# Index the leading stack axes: first by `frame`, deeper by 0.
|
| 168 |
target = 3 if ci is not None else 2
|
| 169 |
first = True
|
| 170 |
while len(axes) > target:
|
|
|
|
| 1 |
"""Image standardization.
|
| 2 |
|
| 3 |
+
``standardize_image`` collapses any microscopy TIFF variant (16-bit/float,
|
| 4 |
+
multi-channel, multi-page Z/time stacks) to one canonical 8-bit RGB PNG, used
|
| 5 |
+
for both the preview and the model.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
import os
|
|
|
|
| 17 |
except ImportError: # pragma: no cover - tifffile is a project dependency
|
| 18 |
tifffile = None
|
| 19 |
|
| 20 |
+
# The model resizes any input to 512x512, so a larger image loses detail rather
|
| 21 |
+
# than adding any.
|
| 22 |
+
RECOMMENDED_SIZE = 512
|
| 23 |
+
WARN_SIZE = 3072
|
| 24 |
+
MAX_SIZE = 4096
|
| 25 |
+
# Measured on the uncompressed array, not the file size on disk: compression
|
| 26 |
+
# makes disk size a poor proxy for what opening the file actually allocates.
|
| 27 |
+
MAX_READ_BYTES = 300 * 1024 ** 2
|
| 28 |
+
|
| 29 |
+
# Axis roles, per tifffile's `series.axes` naming. Anything else (Z, T, ...) is a
|
| 30 |
+
# stack axis, indexed by frame.
|
|
|
|
| 31 |
_CHANNEL = ("C", "S") # C = separate channel planes, S = interleaved RGB samples
|
| 32 |
_UNKNOWN = ("Q", "I") # file named no axis; only these may be *guessed* as channels
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
def _read_array(path):
|
|
|
|
| 123 |
"""
|
| 124 |
axes = list(axes)
|
| 125 |
|
|
|
|
| 126 |
for i in range(len(axes) - 1, -1, -1):
|
| 127 |
if arr.shape[i] == 1:
|
| 128 |
arr = arr.reshape(arr.shape[:i] + arr.shape[i + 1:])
|
|
|
|
| 130 |
|
| 131 |
_, planned, _ = _plan_axes(arr.shape, axes)
|
| 132 |
|
|
|
|
| 133 |
if "C" in planned and not any(a in _CHANNEL for a in axes):
|
| 134 |
for i, (a, d) in enumerate(zip(axes, arr.shape)):
|
| 135 |
if a in _UNKNOWN and d in (2, 3, 4):
|
|
|
|
| 140 |
arr = np.moveaxis(arr, ci, -1)
|
| 141 |
axes.append(axes.pop(ci))
|
| 142 |
|
|
|
|
| 143 |
target = 3 if ci is not None else 2
|
| 144 |
first = True
|
| 145 |
while len(axes) > target:
|
app.py
CHANGED
|
@@ -31,12 +31,10 @@ DATASET_REPO = "VisionLanguageGroup/feedback"
|
|
| 31 |
|
| 32 |
|
| 33 |
print("===== clearing cache =====")
|
| 34 |
-
# cache_path = os.path.expanduser("~/.cache/")
|
| 35 |
cache_path = os.path.expanduser("~/.cache/huggingface/gradio")
|
| 36 |
if os.path.exists(cache_path):
|
| 37 |
try:
|
| 38 |
shutil.rmtree(cache_path)
|
| 39 |
-
# print("β
Deleted ~/.cache/")
|
| 40 |
print("β
Deleted ~/.cache/huggingface/gradio")
|
| 41 |
except:
|
| 42 |
pass
|
|
@@ -87,15 +85,12 @@ def save_feedback_to_hf(query_id, feedback_type, feedback_text=None, img_path=No
|
|
| 87 |
save_feedback(query_id, feedback_type, feedback_text, img_path, bboxes)
|
| 88 |
return
|
| 89 |
|
| 90 |
-
#
|
| 91 |
-
# not enough - a single session can submit feedback more than once.
|
| 92 |
stem = f"feedback_{query_id}_{int(time.time())}"
|
| 93 |
|
| 94 |
try:
|
| 95 |
api = HfApi()
|
| 96 |
|
| 97 |
-
# Upload the standardized PNG: the exact image the model saw (first
|
| 98 |
-
# frame, <=3 channels, auto-contrasted), not the raw upload.
|
| 99 |
image_in_repo = None
|
| 100 |
if img_path and os.path.exists(img_path):
|
| 101 |
try:
|
|
@@ -109,16 +104,14 @@ def save_feedback_to_hf(query_id, feedback_type, feedback_text=None, img_path=No
|
|
| 109 |
)
|
| 110 |
except Exception as e:
|
| 111 |
print(f"β οΈ Failed to upload image: {e}")
|
| 112 |
-
image_in_repo = None
|
| 113 |
|
| 114 |
feedback_data = {
|
| 115 |
"query_id": query_id,
|
| 116 |
"feedback_type": feedback_type,
|
| 117 |
"feedback_text": feedback_text,
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
"image_path": image_in_repo,
|
| 121 |
-
"bboxes": str(bboxes), # 转为ε符串
|
| 122 |
"datetime": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 123 |
"timestamp": time.time()
|
| 124 |
}
|
|
@@ -320,6 +313,22 @@ def cleanup_tracking_cache(track_vis_cache):
|
|
| 320 |
pass
|
| 321 |
|
| 322 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
def _annot_path(annot_value):
|
| 324 |
"""Extract the image path from a BBoxAnnotator value (path or (path, boxes))."""
|
| 325 |
if not annot_value:
|
|
@@ -347,8 +356,8 @@ def check_image(img_path):
|
|
| 347 |
"""
|
| 348 |
w, h = image_size(img_path)
|
| 349 |
if w <= 0 or h <= 0:
|
| 350 |
-
# Neither
|
| 351 |
-
#
|
| 352 |
ext = os.path.splitext(img_path)[1].lower() or "(no extension)"
|
| 353 |
return (f"Could not read {ext} as an image. Supported formats: "
|
| 354 |
f"TIFF / OME-TIFF (8/16/32-bit, stacks, multi-channel), PNG, JPG, and other formats suported by tiffile. "
|
|
@@ -420,8 +429,7 @@ def prepare_uploaded_image(annot_value):
|
|
| 420 |
if not img_path:
|
| 421 |
return annot_value, None, gr.update(visible=False, value=1)
|
| 422 |
|
| 423 |
-
#
|
| 424 |
-
# oversize file cannot be sent to inference.
|
| 425 |
rejected = check_image(img_path)
|
| 426 |
if rejected:
|
| 427 |
gr.Warning(rejected, duration=None, title="β Cannot use this file")
|
|
@@ -430,9 +438,8 @@ def prepare_uploaded_image(annot_value):
|
|
| 430 |
info = inspect_image(img_path)
|
| 431 |
display = standardize_image(img_path, frame=0)
|
| 432 |
|
| 433 |
-
#
|
| 434 |
-
#
|
| 435 |
-
# grayscale image needs no explanation.
|
| 436 |
if info.guessed or info.frames > 1 or info.channels > 3:
|
| 437 |
gr.Info(_describe_read(info), duration=None, title="π Image Loading Info")
|
| 438 |
|
|
@@ -1379,9 +1386,7 @@ with gr.Blocks(
|
|
| 1379 |
primary_hue=gr.themes.colors.sky,
|
| 1380 |
secondary_hue=gr.themes.colors.slate,
|
| 1381 |
neutral_hue=gr.themes.colors.slate,
|
| 1382 |
-
#
|
| 1383 |
-
# so any font-weight above 600 silently falls back. Inter tops out at 900;
|
| 1384 |
-
# anything higher is dropped by Google Fonts without an error.
|
| 1385 |
font=gr.themes.GoogleFont("Inter", weights=(400, 600, 700, 800)),
|
| 1386 |
),
|
| 1387 |
css=CSS,
|
|
@@ -1406,17 +1411,16 @@ with gr.Blocks(
|
|
| 1406 |
"""
|
| 1407 |
)
|
| 1408 |
|
| 1409 |
-
|
| 1410 |
-
# Must
|
| 1411 |
-
#
|
| 1412 |
-
# same id. A callable is re-run on each app load, giving one id per session.
|
| 1413 |
current_query_id = gr.State(lambda: str(uuid.uuid4()))
|
| 1414 |
user_uploaded_examples = gr.State(example_images_seg.copy())
|
| 1415 |
seg_vis_state = gr.State({})
|
| 1416 |
count_vis_state = gr.State({})
|
| 1417 |
track_vis_state = gr.State({})
|
| 1418 |
-
#
|
| 1419 |
-
#
|
| 1420 |
seg_raw_state = gr.State(None)
|
| 1421 |
count_raw_state = gr.State(None)
|
| 1422 |
|
|
@@ -1437,8 +1441,8 @@ with gr.Blocks(
|
|
| 1437 |
π€ Tell us about your experience by rating and submitting feedback, which would greatly help us improve the framework!
|
| 1438 |
"""
|
| 1439 |
)
|
| 1440 |
-
#
|
| 1441 |
-
#
|
| 1442 |
with gr.Accordion("π Image Upload Requirements (click to expand or fold)", open=False, elem_classes="req-accordion"):
|
| 1443 |
gr.Markdown(
|
| 1444 |
"""
|
|
@@ -1548,13 +1552,11 @@ with gr.Blocks(
|
|
| 1548 |
visible=False
|
| 1549 |
)
|
| 1550 |
|
| 1551 |
-
# standardize on upload (preview) + configure the stack frame slider
|
| 1552 |
annotator.upload(
|
| 1553 |
fn=prepare_uploaded_image,
|
| 1554 |
inputs=annotator,
|
| 1555 |
outputs=[annotator, seg_raw_state, seg_frame_slider]
|
| 1556 |
)
|
| 1557 |
-
# re-render the preview when the user picks a different stack frame
|
| 1558 |
seg_frame_slider.release(
|
| 1559 |
fn=select_frame,
|
| 1560 |
inputs=[seg_raw_state, seg_frame_slider],
|
|
@@ -1596,9 +1598,13 @@ with gr.Blocks(
|
|
| 1596 |
if rejected:
|
| 1597 |
return (current_imgs, current_imgs,
|
| 1598 |
gr.update(value=f"β {rejected}", visible=True), None)
|
| 1599 |
-
|
| 1600 |
-
|
|
|
|
|
|
|
|
|
|
| 1601 |
std_path = standardize_image(img_path)
|
|
|
|
| 1602 |
if std_path not in current_imgs:
|
| 1603 |
current_imgs.insert(0, std_path)
|
| 1604 |
return (current_imgs, current_imgs,
|
|
@@ -1610,11 +1616,11 @@ with gr.Blocks(
|
|
| 1610 |
outputs=[user_uploaded_examples, example_gallery, add_gallery_status, image_uploader]
|
| 1611 |
)
|
| 1612 |
|
| 1613 |
-
#
|
| 1614 |
def load_from_gallery(evt: gr.SelectData, all_imgs):
|
| 1615 |
if evt.index is not None and evt.index < len(all_imgs):
|
| 1616 |
item = all_imgs[evt.index]
|
| 1617 |
-
return
|
| 1618 |
return None, None, gr.update(visible=False, value=1)
|
| 1619 |
|
| 1620 |
example_gallery.select(
|
|
@@ -1670,8 +1676,8 @@ with gr.Blocks(
|
|
| 1670 |
π€ Tell us about your experience by rating and submitting feedback, which would greatly help us improve the framework!
|
| 1671 |
"""
|
| 1672 |
)
|
| 1673 |
-
#
|
| 1674 |
-
#
|
| 1675 |
with gr.Accordion("π Image requirements", open=False, elem_classes="req-accordion"):
|
| 1676 |
gr.Markdown(
|
| 1677 |
"""
|
|
@@ -1799,9 +1805,13 @@ with gr.Blocks(
|
|
| 1799 |
if rejected:
|
| 1800 |
return (current_imgs, current_imgs,
|
| 1801 |
gr.update(value=f"β {rejected}", visible=True), None)
|
| 1802 |
-
|
| 1803 |
-
|
|
|
|
|
|
|
|
|
|
| 1804 |
std_path = standardize_image(new_img_file)
|
|
|
|
| 1805 |
if std_path not in current_imgs:
|
| 1806 |
current_imgs.insert(0, std_path)
|
| 1807 |
print(f"β
Added image to gallery: {std_path}")
|
|
@@ -1815,26 +1825,24 @@ with gr.Blocks(
|
|
| 1815 |
outputs=[count_user_examples, count_example_gallery, count_add_status, count_image_uploader]
|
| 1816 |
)
|
| 1817 |
|
| 1818 |
-
# standardize on upload (preview) + configure the stack frame slider
|
| 1819 |
count_annotator.upload(
|
| 1820 |
fn=prepare_uploaded_image,
|
| 1821 |
inputs=count_annotator,
|
| 1822 |
outputs=[count_annotator, count_raw_state, count_frame_slider]
|
| 1823 |
)
|
| 1824 |
-
# re-render the preview when the user picks a different stack frame
|
| 1825 |
count_frame_slider.release(
|
| 1826 |
fn=select_frame,
|
| 1827 |
inputs=[count_raw_state, count_frame_slider],
|
| 1828 |
outputs=count_annotator
|
| 1829 |
)
|
| 1830 |
|
| 1831 |
-
# When user selects from gallery, load into annotator
|
| 1832 |
def load_from_count_gallery(evt: gr.SelectData, all_imgs):
|
| 1833 |
-
"""Load
|
| 1834 |
if evt.index is not None and evt.index < len(all_imgs):
|
| 1835 |
selected_img = all_imgs[evt.index]
|
| 1836 |
print(f"πΈ Loading image from gallery: {selected_img}")
|
| 1837 |
-
return
|
| 1838 |
return None, None, gr.update(visible=False, value=1)
|
| 1839 |
|
| 1840 |
count_example_gallery.select(
|
|
@@ -1867,14 +1875,7 @@ with gr.Blocks(
|
|
| 1867 |
try:
|
| 1868 |
img_path = annot_val[0] if annot_val and len(annot_val) > 0 else None
|
| 1869 |
bboxes = annot_val[1] if annot_val and len(annot_val) > 1 else []
|
| 1870 |
-
|
| 1871 |
-
# save_feedback(
|
| 1872 |
-
# query_id=query_id,
|
| 1873 |
-
# feedback_type=f"score_{int(score)}",
|
| 1874 |
-
# feedback_text=comment,
|
| 1875 |
-
# img_path=img_path,
|
| 1876 |
-
# bboxes=bboxes
|
| 1877 |
-
# )
|
| 1878 |
|
| 1879 |
save_feedback_to_hf(
|
| 1880 |
query_id=query_id,
|
|
@@ -2211,14 +2212,6 @@ with gr.Blocks(
|
|
| 2211 |
try:
|
| 2212 |
img_path = annot_val[0] if annot_val and len(annot_val) > 0 else None
|
| 2213 |
bboxes = annot_val[1] if annot_val and len(annot_val) > 1 else []
|
| 2214 |
-
|
| 2215 |
-
# save_feedback(
|
| 2216 |
-
# query_id=query_id,
|
| 2217 |
-
# feedback_type=f"score_{int(score)}",
|
| 2218 |
-
# feedback_text=comment,
|
| 2219 |
-
# img_path=img_path,
|
| 2220 |
-
# bboxes=bboxes
|
| 2221 |
-
# )
|
| 2222 |
|
| 2223 |
save_feedback_to_hf(
|
| 2224 |
query_id=query_id,
|
|
@@ -2246,9 +2239,4 @@ if __name__ == "__main__":
|
|
| 2246 |
share=False,
|
| 2247 |
ssr_mode=False,
|
| 2248 |
show_error=True,
|
| 2249 |
-
# Deliberately no max_file_size: a transport-layer 413 is silent in the
|
| 2250 |
-
# annotator (we never get to run, so we cannot show a message), and file
|
| 2251 |
-
# size is a poor proxy anyway - a 1344x1024 time-lapse can be 256MB while
|
| 2252 |
-
# a 4096x4096 RGB is only 50MB. Size is checked in check_image()
|
| 2253 |
-
# instead, where a real message can be shown.
|
| 2254 |
)
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
print("===== clearing cache =====")
|
|
|
|
| 34 |
cache_path = os.path.expanduser("~/.cache/huggingface/gradio")
|
| 35 |
if os.path.exists(cache_path):
|
| 36 |
try:
|
| 37 |
shutil.rmtree(cache_path)
|
|
|
|
| 38 |
print("β
Deleted ~/.cache/huggingface/gradio")
|
| 39 |
except:
|
| 40 |
pass
|
|
|
|
| 85 |
save_feedback(query_id, feedback_type, feedback_text, img_path, bboxes)
|
| 86 |
return
|
| 87 |
|
| 88 |
+
# Shared by the record and its image so the two pair up; query_id alone is not unique (a session can submit more than once).
|
|
|
|
| 89 |
stem = f"feedback_{query_id}_{int(time.time())}"
|
| 90 |
|
| 91 |
try:
|
| 92 |
api = HfApi()
|
| 93 |
|
|
|
|
|
|
|
| 94 |
image_in_repo = None
|
| 95 |
if img_path and os.path.exists(img_path):
|
| 96 |
try:
|
|
|
|
| 104 |
)
|
| 105 |
except Exception as e:
|
| 106 |
print(f"β οΈ Failed to upload image: {e}")
|
| 107 |
+
image_in_repo = None
|
| 108 |
|
| 109 |
feedback_data = {
|
| 110 |
"query_id": query_id,
|
| 111 |
"feedback_type": feedback_type,
|
| 112 |
"feedback_text": feedback_text,
|
| 113 |
+
"image_path": image_in_repo,
|
| 114 |
+
"bboxes": str(bboxes),
|
|
|
|
|
|
|
| 115 |
"datetime": time.strftime("%Y-%m-%d %H:%M:%S"),
|
| 116 |
"timestamp": time.time()
|
| 117 |
}
|
|
|
|
| 313 |
pass
|
| 314 |
|
| 315 |
|
| 316 |
+
# Per session. Refused rather than evicting the oldest, since there is no way to
|
| 317 |
+
# remove a single entry.
|
| 318 |
+
MAX_GALLERY_UPLOADS = 10
|
| 319 |
+
|
| 320 |
+
_GALLERY_RAW_SOURCE = {}
|
| 321 |
+
_RAW_SOURCE_CAP = 500
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def _remember_gallery_source(thumb_path, raw_path):
|
| 325 |
+
"""Map a gallery thumbnail back to the original file it was made from.
|
| 326 |
+
"""
|
| 327 |
+
_GALLERY_RAW_SOURCE[thumb_path] = raw_path
|
| 328 |
+
while len(_GALLERY_RAW_SOURCE) > _RAW_SOURCE_CAP: # dicts keep insertion order
|
| 329 |
+
_GALLERY_RAW_SOURCE.pop(next(iter(_GALLERY_RAW_SOURCE)))
|
| 330 |
+
|
| 331 |
+
|
| 332 |
def _annot_path(annot_value):
|
| 333 |
"""Extract the image path from a BBoxAnnotator value (path or (path, boxes))."""
|
| 334 |
if not annot_value:
|
|
|
|
| 356 |
"""
|
| 357 |
w, h = image_size(img_path)
|
| 358 |
if w <= 0 or h <= 0:
|
| 359 |
+
# Neither reader could parse the header, so say so rather than failing
|
| 360 |
+
# silently further down.
|
| 361 |
ext = os.path.splitext(img_path)[1].lower() or "(no extension)"
|
| 362 |
return (f"Could not read {ext} as an image. Supported formats: "
|
| 363 |
f"TIFF / OME-TIFF (8/16/32-bit, stacks, multi-channel), PNG, JPG, and other formats suported by tiffile. "
|
|
|
|
| 429 |
if not img_path:
|
| 430 |
return annot_value, None, gr.update(visible=False, value=1)
|
| 431 |
|
| 432 |
+
# Clear the annotator on refusal, so a rejected file cannot reach inference.
|
|
|
|
| 433 |
rejected = check_image(img_path)
|
| 434 |
if rejected:
|
| 435 |
gr.Warning(rejected, duration=None, title="β Cannot use this file")
|
|
|
|
| 438 |
info = inspect_image(img_path)
|
| 439 |
display = standardize_image(img_path, frame=0)
|
| 440 |
|
| 441 |
+
# Stay quiet unless something non-obvious happened; a plain RGB or grayscale
|
| 442 |
+
# image needs no explanation.
|
|
|
|
| 443 |
if info.guessed or info.frames > 1 or info.channels > 3:
|
| 444 |
gr.Info(_describe_read(info), duration=None, title="π Image Loading Info")
|
| 445 |
|
|
|
|
| 1386 |
primary_hue=gr.themes.colors.sky,
|
| 1387 |
secondary_hue=gr.themes.colors.slate,
|
| 1388 |
neutral_hue=gr.themes.colors.slate,
|
| 1389 |
+
# A weight must be listed here to be usable; the default is (400, 600).
|
|
|
|
|
|
|
| 1390 |
font=gr.themes.GoogleFont("Inter", weights=(400, 600, 700, 800)),
|
| 1391 |
),
|
| 1392 |
css=CSS,
|
|
|
|
| 1411 |
"""
|
| 1412 |
)
|
| 1413 |
|
| 1414 |
+
|
| 1415 |
+
# Must stay a callable: a plain value is deepcopied, so every session would
|
| 1416 |
+
# share one id.
|
|
|
|
| 1417 |
current_query_id = gr.State(lambda: str(uuid.uuid4()))
|
| 1418 |
user_uploaded_examples = gr.State(example_images_seg.copy())
|
| 1419 |
seg_vis_state = gr.State({})
|
| 1420 |
count_vis_state = gr.State({})
|
| 1421 |
track_vis_state = gr.State({})
|
| 1422 |
+
# The annotator only holds the flattened preview, so keep the original for
|
| 1423 |
+
# re-extracting other stack frames.
|
| 1424 |
seg_raw_state = gr.State(None)
|
| 1425 |
count_raw_state = gr.State(None)
|
| 1426 |
|
|
|
|
| 1441 |
π€ Tell us about your experience by rating and submitting feedback, which would greatly help us improve the framework!
|
| 1442 |
"""
|
| 1443 |
)
|
| 1444 |
+
# Must stay after the instructions markdown: the CSS targets it with
|
| 1445 |
+
# .prose:nth-child(2).
|
| 1446 |
with gr.Accordion("π Image Upload Requirements (click to expand or fold)", open=False, elem_classes="req-accordion"):
|
| 1447 |
gr.Markdown(
|
| 1448 |
"""
|
|
|
|
| 1552 |
visible=False
|
| 1553 |
)
|
| 1554 |
|
|
|
|
| 1555 |
annotator.upload(
|
| 1556 |
fn=prepare_uploaded_image,
|
| 1557 |
inputs=annotator,
|
| 1558 |
outputs=[annotator, seg_raw_state, seg_frame_slider]
|
| 1559 |
)
|
|
|
|
| 1560 |
seg_frame_slider.release(
|
| 1561 |
fn=select_frame,
|
| 1562 |
inputs=[seg_raw_state, seg_frame_slider],
|
|
|
|
| 1598 |
if rejected:
|
| 1599 |
return (current_imgs, current_imgs,
|
| 1600 |
gr.update(value=f"β {rejected}", visible=True), None)
|
| 1601 |
+
if len(current_imgs) - len(example_images_seg) >= MAX_GALLERY_UPLOADS:
|
| 1602 |
+
return (current_imgs, current_imgs,
|
| 1603 |
+
gr.update(value=f"β οΈ Gallery upload limit reached "
|
| 1604 |
+
f"({MAX_GALLERY_UPLOADS} images). Reload the page "
|
| 1605 |
+
f"to start over.", visible=True), None)
|
| 1606 |
std_path = standardize_image(img_path)
|
| 1607 |
+
_remember_gallery_source(std_path, img_path)
|
| 1608 |
if std_path not in current_imgs:
|
| 1609 |
current_imgs.insert(0, std_path)
|
| 1610 |
return (current_imgs, current_imgs,
|
|
|
|
| 1616 |
outputs=[user_uploaded_examples, example_gallery, add_gallery_status, image_uploader]
|
| 1617 |
)
|
| 1618 |
|
| 1619 |
+
# Reuse the upload path so the toast and frame slider behave the same.
|
| 1620 |
def load_from_gallery(evt: gr.SelectData, all_imgs):
|
| 1621 |
if evt.index is not None and evt.index < len(all_imgs):
|
| 1622 |
item = all_imgs[evt.index]
|
| 1623 |
+
return prepare_uploaded_image(_GALLERY_RAW_SOURCE.get(item, item))
|
| 1624 |
return None, None, gr.update(visible=False, value=1)
|
| 1625 |
|
| 1626 |
example_gallery.select(
|
|
|
|
| 1676 |
π€ Tell us about your experience by rating and submitting feedback, which would greatly help us improve the framework!
|
| 1677 |
"""
|
| 1678 |
)
|
| 1679 |
+
# Must stay after the instructions markdown: the CSS targets it with
|
| 1680 |
+
# .prose:nth-child(2).
|
| 1681 |
with gr.Accordion("π Image requirements", open=False, elem_classes="req-accordion"):
|
| 1682 |
gr.Markdown(
|
| 1683 |
"""
|
|
|
|
| 1805 |
if rejected:
|
| 1806 |
return (current_imgs, current_imgs,
|
| 1807 |
gr.update(value=f"β {rejected}", visible=True), None)
|
| 1808 |
+
if len(current_imgs) - len(example_images_cnt) >= MAX_GALLERY_UPLOADS:
|
| 1809 |
+
return (current_imgs, current_imgs,
|
| 1810 |
+
gr.update(value=f"β οΈ Gallery upload limit reached "
|
| 1811 |
+
f"({MAX_GALLERY_UPLOADS} images). Reload the page "
|
| 1812 |
+
f"to start over.", visible=True), None)
|
| 1813 |
std_path = standardize_image(new_img_file)
|
| 1814 |
+
_remember_gallery_source(std_path, new_img_file)
|
| 1815 |
if std_path not in current_imgs:
|
| 1816 |
current_imgs.insert(0, std_path)
|
| 1817 |
print(f"β
Added image to gallery: {std_path}")
|
|
|
|
| 1825 |
outputs=[count_user_examples, count_example_gallery, count_add_status, count_image_uploader]
|
| 1826 |
)
|
| 1827 |
|
|
|
|
| 1828 |
count_annotator.upload(
|
| 1829 |
fn=prepare_uploaded_image,
|
| 1830 |
inputs=count_annotator,
|
| 1831 |
outputs=[count_annotator, count_raw_state, count_frame_slider]
|
| 1832 |
)
|
|
|
|
| 1833 |
count_frame_slider.release(
|
| 1834 |
fn=select_frame,
|
| 1835 |
inputs=[count_raw_state, count_frame_slider],
|
| 1836 |
outputs=count_annotator
|
| 1837 |
)
|
| 1838 |
|
| 1839 |
+
# When user selects from gallery, load into annotator
|
| 1840 |
def load_from_count_gallery(evt: gr.SelectData, all_imgs):
|
| 1841 |
+
"""Load a gallery image, reusing the upload path."""
|
| 1842 |
if evt.index is not None and evt.index < len(all_imgs):
|
| 1843 |
selected_img = all_imgs[evt.index]
|
| 1844 |
print(f"πΈ Loading image from gallery: {selected_img}")
|
| 1845 |
+
return prepare_uploaded_image(_GALLERY_RAW_SOURCE.get(selected_img, selected_img))
|
| 1846 |
return None, None, gr.update(visible=False, value=1)
|
| 1847 |
|
| 1848 |
count_example_gallery.select(
|
|
|
|
| 1875 |
try:
|
| 1876 |
img_path = annot_val[0] if annot_val and len(annot_val) > 0 else None
|
| 1877 |
bboxes = annot_val[1] if annot_val and len(annot_val) > 1 else []
|
| 1878 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1879 |
|
| 1880 |
save_feedback_to_hf(
|
| 1881 |
query_id=query_id,
|
|
|
|
| 2212 |
try:
|
| 2213 |
img_path = annot_val[0] if annot_val and len(annot_val) > 0 else None
|
| 2214 |
bboxes = annot_val[1] if annot_val and len(annot_val) > 1 else []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2215 |
|
| 2216 |
save_feedback_to_hf(
|
| 2217 |
query_id=query_id,
|
|
|
|
| 2239 |
share=False,
|
| 2240 |
ssr_mode=False,
|
| 2241 |
show_error=True,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2242 |
)
|