arrayview 0.50.1__tar.gz → 0.51.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. {arrayview-0.50.1 → arrayview-0.51.1}/PKG-INFO +3 -1
  2. {arrayview-0.50.1 → arrayview-0.51.1}/README.md +2 -0
  3. {arrayview-0.50.1 → arrayview-0.51.1}/pyproject.toml +1 -1
  4. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_io.py +6 -4
  5. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_launcher.py +39 -2
  6. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_routes_loading.py +2 -0
  7. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_routes_query.py +15 -1
  8. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_viewer.html +3045 -850
  9. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_api.py +46 -0
  10. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_browser.py +634 -29
  11. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_cli.py +50 -6
  12. arrayview-0.51.1/tests/test_glides.py +166 -0
  13. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_mode_roundtrip.py +5 -1
  14. arrayview-0.51.1/tests/test_pane_names.py +512 -0
  15. {arrayview-0.50.1 → arrayview-0.51.1}/.gitignore +0 -0
  16. {arrayview-0.50.1 → arrayview-0.51.1}/LICENSE +0 -0
  17. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/ARCHITECTURE.md +0 -0
  18. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/__init__.py +0 -0
  19. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/__main__.py +0 -0
  20. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_analysis.py +0 -0
  21. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_app.py +0 -0
  22. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_bootstrap_app.py +0 -0
  23. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_config.py +0 -0
  24. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_diagnostics.py +0 -0
  25. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_dicom.py +0 -0
  26. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_diff.py +0 -0
  27. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_extra_ports.py +0 -0
  28. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_icon.png +0 -0
  29. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_imaging.py +0 -0
  30. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_instance_registry.py +0 -0
  31. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_launch_plan.py +0 -0
  32. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_launch_trace.py +0 -0
  33. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_lifecycle.py +0 -0
  34. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_overlays.py +0 -0
  35. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_platform.py +0 -0
  36. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_render.py +0 -0
  37. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_routes_analysis.py +0 -0
  38. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_routes_drop.py +0 -0
  39. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_routes_export.py +0 -0
  40. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_routes_persistence.py +0 -0
  41. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_routes_preferences.py +0 -0
  42. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_routes_preload.py +0 -0
  43. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_routes_rendering.py +0 -0
  44. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_routes_segmentation.py +0 -0
  45. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_routes_state.py +0 -0
  46. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_routes_vectorfield.py +0 -0
  47. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_routes_websocket.py +0 -0
  48. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_segmentation.py +0 -0
  49. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_server.py +0 -0
  50. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_session.py +0 -0
  51. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_session_spec.py +0 -0
  52. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_shell.html +0 -0
  53. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_source_safety.py +0 -0
  54. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_synthetic_mri.py +0 -0
  55. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_torch.py +0 -0
  56. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_tutorial.py +0 -0
  57. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_vectorfield.py +0 -0
  58. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_view_memory.py +0 -0
  59. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_vscode.py +0 -0
  60. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_vscode_browser.py +0 -0
  61. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_vscode_extension.py +0 -0
  62. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_vscode_signal.py +0 -0
  63. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/_watch.py +0 -0
  64. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/arrayview-opener.vsix +0 -0
  65. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/gsap.min.js +0 -0
  66. {arrayview-0.50.1 → arrayview-0.51.1}/src/arrayview/todo.md +0 -0
  67. {arrayview-0.50.1 → arrayview-0.51.1}/tests/capture_patient_loading.py +0 -0
  68. {arrayview-0.50.1 → arrayview-0.51.1}/tests/capture_v_animation.py +0 -0
  69. {arrayview-0.50.1 → arrayview-0.51.1}/tests/conftest.py +0 -0
  70. {arrayview-0.50.1 → arrayview-0.51.1}/tests/make_vectorfield_test_arrays.py +0 -0
  71. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_1d_arrays.py +0 -0
  72. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_backend_shared.py +0 -0
  73. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_config.py +0 -0
  74. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_dicom.py +0 -0
  75. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_drop_import.py +0 -0
  76. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_instance_registry.py +0 -0
  77. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_io.py +0 -0
  78. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_large_arrays.py +0 -0
  79. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_launch_blackbox.py +0 -0
  80. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_nifti_meta.py +0 -0
  81. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_nifti_series.py +0 -0
  82. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_rgb_pixel_art.py +0 -0
  83. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_safe_server_stop.py +0 -0
  84. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_source_safety.py +0 -0
  85. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_torch.py +0 -0
  86. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_tutorial.py +0 -0
  87. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_view_memory.py +0 -0
  88. {arrayview-0.50.1 → arrayview-0.51.1}/tests/test_vsix_bundle.py +0 -0
  89. {arrayview-0.50.1 → arrayview-0.51.1}/tests/ui_audit.py +0 -0
  90. {arrayview-0.50.1 → arrayview-0.51.1}/tests/v_anim_frames/.gitkeep +0 -0
  91. {arrayview-0.50.1 → arrayview-0.51.1}/tests/visual_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arrayview
3
- Version: 0.50.1
3
+ Version: 0.51.1
4
4
  Summary: Fast multi-dimensional array viewer
5
5
  Project-URL: Home, https://github.com/oscarvanderheide/arrayview
6
6
  Project-URL: Source, https://github.com/oscarvanderheide/arrayview
@@ -39,6 +39,8 @@ Description-Content-Type: text/markdown
39
39
 
40
40
  _This is my array viewer. There are many like it, but this one is mine._
41
41
 
42
+ <img src="docs/teaser.gif" alt="ArrayView: scrolling a volume, switching colormaps, three planes, and a difference view" width="800">
43
+
42
44
  Arrayview lets you scroll through multi-dimensional arrays.
43
45
 
44
46
  Open it from the shell, from Python, Julia, or Matlab, or inside a Jupyter notebook. Use it locally or over SSH.
@@ -2,6 +2,8 @@
2
2
 
3
3
  _This is my array viewer. There are many like it, but this one is mine._
4
4
 
5
+ <img src="docs/teaser.gif" alt="ArrayView: scrolling a volume, switching colormaps, three planes, and a difference view" width="800">
6
+
5
7
  Arrayview lets you scroll through multi-dimensional arrays.
6
8
 
7
9
  Open it from the shell, from Python, Julia, or Matlab, or inside a Jupyter notebook. Use it locally or over SSH.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.50.1"
7
+ version = "0.51.1"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -1452,7 +1452,7 @@ def _collection_pattern_paths(pattern):
1452
1452
  return paths
1453
1453
 
1454
1454
 
1455
- def _explicit_case_files(patterns):
1455
+ def _explicit_case_files(patterns, keep_order=False):
1456
1456
  """Plain file paths given as several patterns, or None.
1457
1457
 
1458
1458
  An unquoted ``--match dir/*/maps.nii`` reaches us already expanded by the
@@ -1467,7 +1467,7 @@ def _explicit_case_files(patterns):
1467
1467
  paths = [os.path.abspath(p) for p in patterns if _is_supported_collection_path(p)]
1468
1468
  if len(paths) != len(patterns) or len(set(paths)) != len(paths):
1469
1469
  return None
1470
- return sorted(paths)
1470
+ return paths if keep_order else sorted(paths)
1471
1471
 
1472
1472
 
1473
1473
  def _distinguishing_path_parts(paths):
@@ -1720,6 +1720,7 @@ def load_dir_collection(
1720
1720
  stack="auto",
1721
1721
  scan_progress=None,
1722
1722
  exclude_cases=None,
1723
+ keep_order=False,
1723
1724
  ):
1724
1725
  """Load recursive collection patterns as aligned lazy image/overlay stacks.
1725
1726
 
@@ -1730,7 +1731,8 @@ def load_dir_collection(
1730
1731
  *case_regex*, files are paired by the regex's named ``case`` group instead.
1731
1732
  *exclude_cases* removes known case ids before any image or overlay data is
1732
1733
  opened. It is used by the interactive CLI after the user accepts a partial
1733
- overlay match.
1734
+ overlay match. *keep_order* stacks an explicit file list in the order
1735
+ given instead of sorted.
1734
1736
  """
1735
1737
  if not base_patterns:
1736
1738
  raise ValueError("--match requires at least one positional image pattern.")
@@ -1753,7 +1755,7 @@ def load_dir_collection(
1753
1755
  raise ValueError("; ".join(missing))
1754
1756
  base_matrix = [[by_case[case] for by_case in base_maps] for case in case_ids]
1755
1757
  else:
1756
- explicit_cases = _explicit_case_files(base_patterns)
1758
+ explicit_cases = _explicit_case_files(base_patterns, keep_order=keep_order)
1757
1759
  base_lists = (
1758
1760
  [explicit_cases]
1759
1761
  if explicit_cases is not None
@@ -1323,6 +1323,7 @@ def _load_session_from_filepath(
1323
1323
  dir_exclude_cases: list[str] | None = None,
1324
1324
  collection_load: str = "lazy",
1325
1325
  collection_stack: str = "auto",
1326
+ collection_keep_order: bool = False,
1326
1327
  background: bool = False,
1327
1328
  expected_server_id: str | None = None,
1328
1329
  native_request_id: str | None = None,
@@ -1365,6 +1366,7 @@ def _load_session_from_filepath(
1365
1366
  payload["dir_exclude_cases"] = dir_exclude_cases or []
1366
1367
  payload["load"] = collection_load
1367
1368
  payload["stack"] = collection_stack
1369
+ payload["keep_order"] = collection_keep_order
1368
1370
  if notify and compare_sids:
1369
1371
  payload["compare_sid"] = compare_sids[0]
1370
1372
  payload["compare_sids"] = _join_query_values(compare_sids)
@@ -1686,6 +1688,7 @@ def _register_cli_session_with_existing_server_impl(
1686
1688
  dir_exclude_cases: list[str] | None = None,
1687
1689
  collection_load: str = "lazy",
1688
1690
  collection_stack: str = "auto",
1691
+ collection_keep_order: bool = False,
1689
1692
  overlay_names: list[str] | None = None,
1690
1693
  background_base: bool = False,
1691
1694
  expected_server_id: str | None = None,
@@ -1741,6 +1744,7 @@ def _register_cli_session_with_existing_server_impl(
1741
1744
  dir_exclude_cases=dir_exclude_cases,
1742
1745
  collection_load=collection_load,
1743
1746
  collection_stack=collection_stack,
1747
+ collection_keep_order=collection_keep_order,
1744
1748
  background=background_base,
1745
1749
  expected_server_id=expected_server_id,
1746
1750
  native_request_id=native_request_id,
@@ -1838,6 +1842,7 @@ def _handle_cli_existing_server(
1838
1842
  dir_exclude_cases: list[str] | None = None,
1839
1843
  collection_load: str = "lazy",
1840
1844
  collection_stack: str = "auto",
1845
+ collection_keep_order: bool = False,
1841
1846
  overlay_names: list[str] | None = None,
1842
1847
  cleanup_dir: str | None = None,
1843
1848
  source_staging_dirs: dict[str, str] | None = None,
@@ -1866,6 +1871,7 @@ def _handle_cli_existing_server(
1866
1871
  dir_exclude_cases=dir_exclude_cases,
1867
1872
  collection_load=collection_load,
1868
1873
  collection_stack=collection_stack,
1874
+ collection_keep_order=collection_keep_order,
1869
1875
  overlay_names=overlay_names,
1870
1876
  background_base=(
1871
1877
  is_remote
@@ -1954,6 +1960,7 @@ def _handle_cli_existing_server(
1954
1960
  dir_exclude_cases=dir_exclude_cases,
1955
1961
  collection_load=collection_load,
1956
1962
  collection_stack=collection_stack,
1963
+ collection_keep_order=collection_keep_order,
1957
1964
  overlay_names=overlay_names,
1958
1965
  source_staging_dirs=source_staging_dirs,
1959
1966
  )
@@ -2084,6 +2091,7 @@ def _handle_cli_spawned_daemon(
2084
2091
  dir_exclude_cases: list[str] | None = None,
2085
2092
  collection_load: str = "lazy",
2086
2093
  collection_stack: str = "auto",
2094
+ collection_keep_order: bool = False,
2087
2095
  overlay_names: list[str] | None = None,
2088
2096
  cleanup_dir: str | None = None,
2089
2097
  source_staging_dirs: dict[str, str] | None = None,
@@ -2147,6 +2155,7 @@ def _handle_cli_spawned_daemon(
2147
2155
  dir_exclude_cases=dir_exclude_cases,
2148
2156
  collection_load=collection_load,
2149
2157
  collection_stack=collection_stack,
2158
+ collection_keep_order=collection_keep_order,
2150
2159
  overlay_names=resolved_overlay_names,
2151
2160
  cleanup_dir=cleanup_dir,
2152
2161
  source_staging_dirs=source_staging_dirs,
@@ -2202,6 +2211,7 @@ def _handle_cli_spawned_daemon(
2202
2211
  f" dir_exclude_cases={repr(dir_exclude_cases)},"
2203
2212
  f" collection_load={repr(collection_load)},"
2204
2213
  f" collection_stack={repr(collection_stack)},"
2214
+ f" collection_keep_order={repr(collection_keep_order)},"
2205
2215
  f" cleanup_dir={repr(cleanup_dir)},"
2206
2216
  f" source_staging_dirs={repr(source_staging_dirs or {})},"
2207
2217
  f" watch={watch},"
@@ -4729,6 +4739,7 @@ def _serve_daemon(
4729
4739
  dir_exclude_cases: list[str] | None = None,
4730
4740
  collection_load: str = "lazy",
4731
4741
  collection_stack: str = "auto",
4742
+ collection_keep_order: bool = False,
4732
4743
  cleanup_dir: str | None = None,
4733
4744
  source_staging_dirs: dict[str, str] | None = None,
4734
4745
  watch: bool = False,
@@ -4868,6 +4879,7 @@ def _serve_daemon(
4868
4879
  exclude_cases=dir_exclude_cases,
4869
4880
  load=collection_load,
4870
4881
  stack=collection_stack,
4882
+ keep_order=collection_keep_order,
4871
4883
  )
4872
4884
  collection_spatial_ndim = len(_summary["spatial_shape"])
4873
4885
  elif (filepath.endswith(".npz") or filepath.endswith(".mat")) and not cleanup:
@@ -6052,6 +6064,27 @@ def arrayview():
6052
6064
  "stacking: a directory of arrays is stacked either way. It selects "
6053
6065
  "which files form the collection."
6054
6066
  )
6067
+ if (
6068
+ args.files
6069
+ and len(args.files) > 4
6070
+ and not args.stack_mode
6071
+ and not (
6072
+ args.compare
6073
+ or args.overlay
6074
+ or args.vectorfield
6075
+ or args.watch
6076
+ or args.rgb
6077
+ or args.relay
6078
+ )
6079
+ ):
6080
+ # Too many for a side-by-side compare: stack them along a new last
6081
+ # axis, one case per file, as --match does for explicit file lists.
6082
+ args.stack_mode = True
6083
+ args.keep_file_order = True
6084
+ print(
6085
+ f"[ArrayView] {len(args.files)} files: opened as one stack; "
6086
+ "step through the last dimension to switch files."
6087
+ )
6055
6088
  if args.dry_run and not args.stack_mode:
6056
6089
  parser.error("--dry-run requires --match.")
6057
6090
  excluded_cases = set()
@@ -6098,9 +6131,10 @@ def arrayview():
6098
6131
  dims_override = (0, 1)
6099
6132
  if not args.serve and not args.kill and not args.files:
6100
6133
  _configure_tutorial_args(args)
6101
- if args.files and len(args.files) > 6 and not args.stack_mode:
6134
+ if args.files and len(args.files) > 4 and not args.stack_mode:
6102
6135
  parser.error(
6103
- "At most six FILE arguments are supported; concat arrays first for larger compare sets."
6136
+ "More than four FILE arguments open as one stack, which does not "
6137
+ "support --compare, --overlay, --vectorfield, --watch, --rgb, or --relay."
6104
6138
  )
6105
6139
  if args.compare and len(args.files) > 1 and not args.stack_mode:
6106
6140
  parser.error("Use either positional compare files or --compare, not both.")
@@ -6310,6 +6344,7 @@ def arrayview():
6310
6344
  stack=args.stack_policy or "auto",
6311
6345
  scan_progress=scan_progress.update,
6312
6346
  exclude_cases=excluded_cases,
6347
+ keep_order=getattr(args, "keep_file_order", False),
6313
6348
  )
6314
6349
  finally:
6315
6350
  scan_progress.finish()
@@ -6683,6 +6718,7 @@ def arrayview():
6683
6718
  dir_exclude_cases=sorted(excluded_cases),
6684
6719
  collection_load=args.load,
6685
6720
  collection_stack=args.stack_policy or "auto",
6721
+ collection_keep_order=getattr(args, "keep_file_order", False),
6686
6722
  cleanup_dir=getattr(args, "_tutorial_cleanup_dir", None),
6687
6723
  source_staging_dirs=(
6688
6724
  source_staging_dirs if not args.stack_mode else None
@@ -6723,6 +6759,7 @@ def arrayview():
6723
6759
  dir_exclude_cases=sorted(excluded_cases),
6724
6760
  collection_load=args.load,
6725
6761
  collection_stack=args.stack_policy or "auto",
6762
+ collection_keep_order=getattr(args, "keep_file_order", False),
6726
6763
  source_staging_dirs=(
6727
6764
  source_staging_dirs if not args.stack_mode else None
6728
6765
  ),
@@ -227,6 +227,7 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
227
227
  tuple(str(case) for case in body.get("dir_exclude_cases", [])),
228
228
  body.get("load", "lazy"),
229
229
  body.get("stack", "auto"),
230
+ bool(body.get("keep_order", False)),
230
231
  )
231
232
  for existing in [] if requested_sid else SESSIONS.values():
232
233
  if getattr(existing, "collection_identity", None) == collection_identity:
@@ -440,6 +441,7 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
440
441
  exclude_cases=body.get("dir_exclude_cases", []),
441
442
  load=body.get("load", "lazy"),
442
443
  stack=body.get("stack", "auto"),
444
+ keep_order=bool(body.get("keep_order", False)),
443
445
  )
444
446
  elif filepath.endswith(".npz") or filepath.endswith(".mat"):
445
447
  _array_keys = await asyncio.to_thread(list_array_keys, filepath)
@@ -429,7 +429,7 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
429
429
  info["overlay_labels"] = []
430
430
  try:
431
431
  if np.issubdtype(session.data.dtype, np.integer):
432
- labels = np.unique(session.data)
432
+ labels = np.unique(_overlay_label_sample(session))
433
433
  labels = labels[labels > 0]
434
434
  if labels.size <= 16:
435
435
  info["overlay_labels"] = [int(value) for value in labels]
@@ -531,6 +531,18 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
531
531
  )
532
532
 
533
533
 
534
+ def _overlay_label_sample(session):
535
+ """Overlay values to read label ids from: the whole array, or only the
536
+ first case of a collection (reading every case would stall opening; the
537
+ viewer adds labels found in other cases as they are visited)."""
538
+ spatial = getattr(session, "collection_spatial_ndim", None)
539
+ shape = tuple(session.shape)
540
+ if spatial is None or not (0 < spatial < len(shape)):
541
+ return np.asarray(session.data)
542
+ index = (slice(None),) * spatial + (0,) * (len(shape) - spatial)
543
+ return np.asarray(session.data[index])
544
+
545
+
534
546
  def _overlay_slice_counts(ov_session, base_shape, dim_x, dim_y, axis, idx_list):
535
547
  """Count overlay pixels per label on every slice along ``axis``."""
536
548
  n = int(base_shape[axis])
@@ -555,6 +567,8 @@ def _overlay_slice_counts(ov_session, base_shape, dim_x, dim_y, axis, idx_list):
555
567
  return empty
556
568
  kept = [a for a in base_axes if a in (dim_x, dim_y, axis)]
557
569
  if axis in kept:
570
+ # Ragged collections return this case's own size, not the max-shape facade.
571
+ n = int(vol.shape[kept.index(axis)])
558
572
  vol = np.moveaxis(vol, kept.index(axis), 0).reshape(n, -1)
559
573
  else:
560
574
  # The overlay has no such axis: it looks the same on every slice.