arrayview 0.47.0__tar.gz → 0.47.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 (115) hide show
  1. {arrayview-0.47.0 → arrayview-0.47.1}/PKG-INFO +1 -1
  2. {arrayview-0.47.0 → arrayview-0.47.1}/pyproject.toml +1 -1
  3. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_io.py +41 -0
  4. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_launcher.py +6 -0
  5. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_routes_loading.py +13 -0
  6. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_session.py +6 -0
  7. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_api.py +20 -0
  8. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_nifti_meta.py +23 -0
  9. {arrayview-0.47.0 → arrayview-0.47.1}/.gitignore +0 -0
  10. {arrayview-0.47.0 → arrayview-0.47.1}/LICENSE +0 -0
  11. {arrayview-0.47.0 → arrayview-0.47.1}/README.md +0 -0
  12. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/ARCHITECTURE.md +0 -0
  13. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/__init__.py +0 -0
  14. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/__main__.py +0 -0
  15. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_analysis.py +0 -0
  16. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_app.py +0 -0
  17. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_bootstrap_app.py +0 -0
  18. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_config.py +0 -0
  19. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_diagnostics.py +0 -0
  20. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_dicom.py +0 -0
  21. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_diff.py +0 -0
  22. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_extra_ports.py +0 -0
  23. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_icon.png +0 -0
  24. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_imaging.py +0 -0
  25. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_instance_registry.py +0 -0
  26. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_launch_plan.py +0 -0
  27. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_launch_trace.py +0 -0
  28. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_lifecycle.py +0 -0
  29. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_overlays.py +0 -0
  30. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_platform.py +0 -0
  31. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_render.py +0 -0
  32. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_routes_analysis.py +0 -0
  33. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_routes_drop.py +0 -0
  34. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_routes_export.py +0 -0
  35. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_routes_persistence.py +0 -0
  36. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_routes_preferences.py +0 -0
  37. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_routes_preload.py +0 -0
  38. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_routes_query.py +0 -0
  39. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_routes_rendering.py +0 -0
  40. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_routes_segmentation.py +0 -0
  41. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_routes_state.py +0 -0
  42. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_routes_vectorfield.py +0 -0
  43. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_routes_websocket.py +0 -0
  44. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_segmentation.py +0 -0
  45. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_server.py +0 -0
  46. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_session_spec.py +0 -0
  47. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_shell.html +0 -0
  48. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_source_safety.py +0 -0
  49. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_synthetic_mri.py +0 -0
  50. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_torch.py +0 -0
  51. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_tutorial.py +0 -0
  52. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_vectorfield.py +0 -0
  53. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_viewer.html +0 -0
  54. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_vscode.py +0 -0
  55. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_vscode_browser.py +0 -0
  56. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_vscode_extension.py +0 -0
  57. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_vscode_signal.py +0 -0
  58. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/_watch.py +0 -0
  59. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/arrayview-opener.vsix +0 -0
  60. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/gsap.min.js +0 -0
  61. {arrayview-0.47.0 → arrayview-0.47.1}/src/arrayview/todo.md +0 -0
  62. {arrayview-0.47.0 → arrayview-0.47.1}/tests/capture_patient_loading.py +0 -0
  63. {arrayview-0.47.0 → arrayview-0.47.1}/tests/capture_v_animation.py +0 -0
  64. {arrayview-0.47.0 → arrayview-0.47.1}/tests/conftest.py +0 -0
  65. {arrayview-0.47.0 → arrayview-0.47.1}/tests/lifecycle_matrix.py +0 -0
  66. {arrayview-0.47.0 → arrayview-0.47.1}/tests/make_vectorfield_test_arrays.py +0 -0
  67. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_1d_arrays.py +0 -0
  68. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_backend_shared.py +0 -0
  69. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_bootstrap_app.py +0 -0
  70. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_browser.py +0 -0
  71. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_cli.py +0 -0
  72. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_colorbar_hover_highlight.py +0 -0
  73. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_config.py +0 -0
  74. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_dicom.py +0 -0
  75. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_display_opener_results.py +0 -0
  76. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_doctor_commands.py +0 -0
  77. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_drop_import.py +0 -0
  78. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_instance_registry.py +0 -0
  79. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_invocation_contract_matrix.py +0 -0
  80. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_io.py +0 -0
  81. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_large_arrays.py +0 -0
  82. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_launch_blackbox.py +0 -0
  83. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_launch_diagnostics.py +0 -0
  84. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_launch_execution_ownership.py +0 -0
  85. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_launch_plan.py +0 -0
  86. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_launch_trace.py +0 -0
  87. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_lifecycle_contract.py +0 -0
  88. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_loading_server.py +0 -0
  89. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_mode_consistency.py +0 -0
  90. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_mode_matrix.py +0 -0
  91. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_mode_roundtrip.py +0 -0
  92. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_nifti_series.py +0 -0
  93. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_preferences_ui.py +0 -0
  94. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_public_invocation_regressions.py +0 -0
  95. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_render_failure_reporting.py +0 -0
  96. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_render_progress.py +0 -0
  97. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_rgb_pixel_art.py +0 -0
  98. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_roi_hud.py +0 -0
  99. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_safe_server_stop.py +0 -0
  100. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_server_identity_status.py +0 -0
  101. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_session_spec.py +0 -0
  102. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_source_safety.py +0 -0
  103. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_spawn_ownership.py +0 -0
  104. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_torch.py +0 -0
  105. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_tunnel_warm_path.py +0 -0
  106. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_tutorial.py +0 -0
  107. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_tutorial_browser.py +0 -0
  108. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_value_formatting.py +0 -0
  109. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_view_handle_lifecycle.py +0 -0
  110. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_view_launch_execution.py +0 -0
  111. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_vscode_ack_protocol.py +0 -0
  112. {arrayview-0.47.0 → arrayview-0.47.1}/tests/test_window_level_preview.py +0 -0
  113. {arrayview-0.47.0 → arrayview-0.47.1}/tests/ui_audit.py +0 -0
  114. {arrayview-0.47.0 → arrayview-0.47.1}/tests/v_anim_frames/.gitkeep +0 -0
  115. {arrayview-0.47.0 → arrayview-0.47.1}/tests/visual_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arrayview
3
- Version: 0.47.0
3
+ Version: 0.47.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.47.0"
7
+ version = "0.47.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"
@@ -1863,3 +1863,44 @@ def _peek_file_shape(fpath: str, ext: str):
1863
1863
  except Exception:
1864
1864
  pass
1865
1865
  return None
1866
+
1867
+
1868
+ def nifti_grid_meta(filepath):
1869
+ """Return a NIfTI file's native-order spatial metadata (header only), or None."""
1870
+ if not (filepath.endswith(".nii") or filepath.endswith(".nii.gz")):
1871
+ return None
1872
+ return _nifti_header_with_meta(filepath, native=True)[1]
1873
+
1874
+
1875
+ def align_overlay_to_base(ov_path, ov_data, base_meta):
1876
+ """Resample a NIfTI overlay onto the base volume's voxel grid.
1877
+
1878
+ Only used when the overlay's spatial shape differs from the base, which
1879
+ otherwise leaves the overlay undisplayable. Both files' affines place the
1880
+ voxels in world space; label data uses nearest-neighbour so mask values
1881
+ stay exact. Returns ``ov_data`` unchanged when alignment does not apply.
1882
+ """
1883
+ if not base_meta or not (ov_path.endswith(".nii") or ov_path.endswith(".nii.gz")):
1884
+ return ov_data
1885
+ base_shape = base_meta.get("spatial_shape")
1886
+ base_affine = base_meta.get("data_affine")
1887
+ if base_shape is None or base_affine is None:
1888
+ return ov_data
1889
+ ov_shape = tuple(int(v) for v in ov_data.shape)
1890
+ if len(ov_shape) != 3 or ov_shape == tuple(base_shape):
1891
+ return ov_data
1892
+ from scipy.ndimage import affine_transform
1893
+
1894
+ ov_affine = np.asarray(_nib().load(ov_path).affine, dtype=np.float64)
1895
+ base_to_ov = np.linalg.inv(ov_affine) @ np.asarray(base_affine, dtype=np.float64)
1896
+ src = np.asarray(ov_data)
1897
+ is_label = np.issubdtype(src.dtype, np.integer) or src.dtype == bool
1898
+ return affine_transform(
1899
+ src.astype(np.float32) if not is_label else src,
1900
+ base_to_ov[:3, :3],
1901
+ offset=base_to_ov[:3, 3],
1902
+ output_shape=tuple(base_shape),
1903
+ order=0 if is_label else 1,
1904
+ mode="constant",
1905
+ cval=0,
1906
+ )
@@ -1314,6 +1314,7 @@ def _load_session_from_filepath(
1314
1314
  watch: bool = False,
1315
1315
  initial_view: dict | None = None,
1316
1316
  code_source: dict | None = None,
1317
+ align_to: str | None = None,
1317
1318
  ) -> dict:
1318
1319
  requested_sid = uuid.uuid4().hex
1319
1320
  payload = {
@@ -1329,6 +1330,8 @@ def _load_session_from_filepath(
1329
1330
  }
1330
1331
  if source_staging_dir:
1331
1332
  payload["source_staging_dir"] = source_staging_dir
1333
+ if align_to:
1334
+ payload["align_to"] = align_to
1332
1335
  if watch:
1333
1336
  payload["watch"] = True
1334
1337
  if initial_view:
@@ -1686,6 +1689,7 @@ def _register_cli_session_with_existing_server_impl(
1686
1689
  ov_name,
1687
1690
  expected_server_id=expected_server_id,
1688
1691
  source_staging_dir=(source_staging_dirs or {}).get(ov_path),
1692
+ align_to=os.path.abspath(base_file),
1689
1693
  )
1690
1694
  if "error" in ov_result:
1691
1695
  raise RuntimeError(
@@ -4803,6 +4807,7 @@ def _serve_daemon(
4803
4807
  array_choice_placeholder,
4804
4808
  load_data,
4805
4809
  load_data_with_meta,
4810
+ align_overlay_to_base,
4806
4811
  load_dir_collection,
4807
4812
  list_array_keys,
4808
4813
  )
@@ -4928,6 +4933,7 @@ def _serve_daemon(
4928
4933
  ):
4929
4934
  try:
4930
4935
  ov_data = load_data(ov_path)
4936
+ ov_data = align_overlay_to_base(ov_path, ov_data, spatial_meta)
4931
4937
  ov_session = _session_mod.Session(
4932
4938
  ov_data, filepath=ov_path, name=ov_name
4933
4939
  )
@@ -277,6 +277,7 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
277
277
  and os.path.abspath(existing.filepath) == abs_path
278
278
  and current_signature is not None
279
279
  and getattr(existing, "file_signature", None) == current_signature
280
+ and getattr(existing, "aligned_to", None) == body.get("align_to")
280
281
  ):
281
282
  if acquire_session_leases([existing.sid]):
282
283
  return {"sid": existing.sid, "name": existing.name, "notified": False}
@@ -458,6 +459,17 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
458
459
  key=_key,
459
460
  select=body.get("select"),
460
461
  )
462
+ if body.get("align_to"):
463
+ from ._io import align_overlay_to_base, nifti_grid_meta
464
+
465
+ base_meta = await asyncio.to_thread(
466
+ nifti_grid_meta, str(body["align_to"])
467
+ )
468
+ aligned = await asyncio.to_thread(
469
+ align_overlay_to_base, filepath, data, base_meta
470
+ )
471
+ if aligned is not data:
472
+ data, spatial_meta = aligned, base_meta
461
473
  except Exception as e:
462
474
  if staging_dir:
463
475
  cleanup_staging_directory(staging_dir)
@@ -479,6 +491,7 @@ def register_loading_routes(app, *, notify_shells, setup_rgb) -> None:
479
491
  str(value) for value in body.get("related_sids", [])
480
492
  ]
481
493
  apply_launch_view(session, body)
494
+ session.aligned_to = body.get("align_to")
482
495
  if staging_dir:
483
496
  session._source_staging_dirs = [str(staging_dir)]
484
497
  if not dir_patterns:
@@ -765,6 +765,12 @@ def upgrade_memmap_in_background(session) -> bool:
765
765
  from ._io import _nifti_display_array
766
766
 
767
767
  loaded = _nifti_display_array(data)
768
+ # An uncompressed proxy whose dtype needs no conversion comes
769
+ # back as a view of the file map, so nothing was read yet and
770
+ # every new orientation would still fault from disk. Copy in
771
+ # file order to make it one sequential read.
772
+ if not loaded.flags.owndata:
773
+ loaded = np.array(loaded, copy=True, order="K")
768
774
  except Exception:
769
775
  return # keep serving from the lazy source
770
776
  # A /reload or a released session must not be clobbered by a read that
@@ -7844,3 +7844,23 @@ class TestViewDisplayRouting:
7844
7844
  finally:
7845
7845
  for sid in set(session_mod.SESSIONS) - before_sids:
7846
7846
  session_mod.SESSIONS.pop(sid, None)
7847
+
7848
+
7849
+ def test_load_aligns_nifti_overlay_to_requested_base_grid(client, tmp_path):
7850
+ nib = pytest.importorskip("nibabel")
7851
+ import arrayview._session as session_mod
7852
+
7853
+ affine = np.diag([1.0, 1.0, 2.0, 1.0])
7854
+ base = tmp_path / "base.nii.gz"
7855
+ nib.save(nib.Nifti1Image(np.zeros((6, 6, 5), np.float32), affine), str(base))
7856
+ mask = tmp_path / "mask.nii.gz"
7857
+ nib.save(nib.Nifti1Image(np.ones((6, 6, 4), np.uint8), affine), str(mask))
7858
+
7859
+ plain = client.post("/load", json={"filepath": str(mask)}).json()["sid"]
7860
+ aligned = client.post(
7861
+ "/load", json={"filepath": str(mask), "align_to": str(base)}
7862
+ ).json()["sid"]
7863
+
7864
+ assert aligned != plain
7865
+ assert session_mod.SESSIONS[plain].shape == (6, 6, 4)
7866
+ assert session_mod.SESSIONS[aligned].shape == (6, 6, 5)
@@ -172,3 +172,26 @@ def test_explicit_ras_conversion_reorients_native_volume(client, tmp_path):
172
172
  reverted = client.post(f"/resample_ras/{sid}", json={"enabled": False})
173
173
  reverted.raise_for_status()
174
174
  assert reverted.json()["shape"] == [3, 4, 5]
175
+
176
+
177
+ def test_overlay_on_different_grid_is_resampled_onto_base(tmp_path):
178
+ from arrayview._io import align_overlay_to_base, load_data, load_data_with_meta
179
+
180
+ base_affine = np.diag([1.0, 1.0, 2.0, 1.0])
181
+ nib.save(nib.Nifti1Image(np.zeros((8, 8, 6), np.float32), base_affine), tmp_path / "base.nii.gz")
182
+ # Mask has one slice fewer and starts one base slice (2 mm) higher.
183
+ mask = np.zeros((8, 8, 5), np.uint8)
184
+ mask[2:4, 3:5, 1] = 1
185
+ mask_affine = base_affine.copy()
186
+ mask_affine[2, 3] = 2.0
187
+ nib.save(nib.Nifti1Image(mask, mask_affine), tmp_path / "mask.nii.gz")
188
+
189
+ _base, meta = load_data_with_meta(str(tmp_path / "base.nii.gz"))
190
+ path = str(tmp_path / "mask.nii.gz")
191
+ aligned = align_overlay_to_base(path, load_data(path), meta)
192
+
193
+ assert aligned.shape == (8, 8, 6)
194
+ assert aligned.dtype == np.uint8
195
+ expected = np.zeros((8, 8, 6), np.uint8)
196
+ expected[2:4, 3:5, 2] = 1
197
+ np.testing.assert_array_equal(aligned, expected)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes