VisionLanguageGroup commited on
Commit
28081ca
Β·
1 Parent(s): cd351de
Files changed (2) hide show
  1. _utils/image_io.py +14 -39
  2. app.py +53 -65
_utils/image_io.py CHANGED
@@ -1,28 +1,8 @@
1
  """Image standardization.
2
 
3
- Microscopy TIFFs come in many flavors: 16-bit or float pixels, single or
4
- multi channel, and multi-page Z / time stacks. ``standardize_image`` collapses
5
- any of those variants to a single canonical 8-bit RGB PNG used for BOTH the
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
- # Size policy. The model resizes any input to 512x512 (see segmentation.py), so a
41
- # larger image costs memory/time and *loses* detail rather than adding any.
42
- RECOMMENDED_SIZE = 512 # cropping to about this preserves the most detail
43
- WARN_SIZE = 3072 # above this, advise the user to crop
44
- MAX_SIZE = 4096 # above this, refuse: reading the pixels risks an OOM
45
- # Refuse files whose full array would not comfortably fit in memory. Measured on
46
- # the uncompressed array (shape x dtype) rather than the file size on disk, since
47
- # compression makes disk size a poor proxy for what we actually allocate.
48
- MAX_READ_BYTES = 300 * 1024 ** 2 # 300 MiB
49
-
50
- # Axis roles, per tifffile's `series.axes` naming.
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
- # One stem for the record and its image so the two pair up. query_id alone is
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 # still record the rest of the feedback
113
 
114
  feedback_data = {
115
  "query_id": query_id,
116
  "feedback_type": feedback_type,
117
  "feedback_text": feedback_text,
118
- # Path inside the dataset repo. This used to be the local temp path,
119
- # which was meaningless once the session ended.
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 tifffile nor PIL could parse the header, so this is not a
351
- # format we can read. Say so instead of failing silently downstream.
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
- # Guard before any pixels are read. On refusal clear the annotator so the
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
- # Only speak up when something non-obvious happened: a dimension had to be
434
- # guessed, it is a stack, or channels are being dropped. A plain RGB or
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
- # Weights must be loaded to be usable: GoogleFont defaults to (400, 600),
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 be a callable: gr.State deepcopies a plain value, so str(uuid4())
1411
- # would be evaluated once at construction and every session would share the
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
- # Raw uploaded path, kept so a different stack frame can be re-extracted
1419
- # (the annotator only holds the already-flattened preview PNG).
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
- # Reference material - collapsed by default. Must stay AFTER the
1441
- # instructions markdown: the CSS styles it via .prose:nth-child(2).
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
- # Standardize to an 8-bit PNG so TIFFs render as gallery
1600
- # thumbnails; prepend so it's immediately visible at the front.
 
 
 
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
- # click event for Gallery selection (gallery entries are single-frame)
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 standardize_image(item), item, gr.update(visible=False, value=1)
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
- # Reference material - collapsed by default. Must stay AFTER the
1674
- # instructions markdown: the CSS styles it via .prose:nth-child(2).
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
- # Standardize to an 8-bit PNG so TIFFs render as gallery
1803
- # thumbnails; prepend so it's immediately visible at the front.
 
 
 
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 (single-frame)
1832
  def load_from_count_gallery(evt: gr.SelectData, all_imgs):
1833
- """Load selected image from gallery into annotator"""
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 standardize_image(selected_img), selected_img, gr.update(visible=False, value=1)
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
  )