-
Notifications
You must be signed in to change notification settings - Fork 1
[Breaking] Add multiscale data support #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -125,7 +125,8 @@ class nImage(BioImage): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Class-level type hints for instance attributes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: str | None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _is_remote: bool | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _layer_data: xr.DataArray | None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _reference_xarray: xr.DataArray | None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _layer_data: list | None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init__( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -159,6 +160,7 @@ def __init__( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Instance state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._reference_xarray = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._layer_data = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(image, str | Path): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import fsspec | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -180,17 +182,16 @@ def __init__( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._is_remote = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def layer_data(self) -> xr.DataArray: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Image data as xarray DataArray for napari layer creation. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def reference_xarray(self) -> xr.DataArray: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Image data as xarray DataArray for metadata determination. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Lazily loads data on first access. Uses in-memory or dask array | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Lazily loads xarray on first access. Uses in-memory or dask array | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| based on file size (determined automatically). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| xr.DataArray | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Squeezed image data. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Squeezed xarray for napari dimensions. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Notes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ----- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -199,13 +200,70 @@ def layer_data(self) -> xr.DataArray: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (the default). No special mosaic handling needed here. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self._layer_data is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self._reference_xarray is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Ensure we're at the highest-res level for metadata consistency | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| current_res = self.current_resolution_level | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.set_resolution_level(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self._is_remote or not determine_in_memory(self.path): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._layer_data = self.xarray_dask_data.squeeze() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._reference_xarray = self.xarray_dask_data.squeeze() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._layer_data = self.xarray_data.squeeze() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._reference_xarray = self.xarray_data.squeeze() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.set_resolution_level(current_res) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self._reference_xarray | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+203
to
+212
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def layer_data(self) -> list: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Image data arrays shaped for napari, one per resolution level. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns a list of arrays ordered from highest to lowest resolution. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| For single-resolution images the list has one element. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| napari automatically treats multi-element lists as multiscale data. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Multiscale images are always dask-backed for memory efficiency. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Single-resolution images use numpy or dask based on file size. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| list[ArrayLike] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Squeezed image arrays (C dim retained for multichannel split | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| in :meth:`get_layer_data_tuples`). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self._layer_data is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._layer_data = self._build_layer_data() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self._layer_data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _build_layer_data(self) -> list: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Build the list of arrays for all resolution levels.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| current_res = self.current_resolution_level | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| levels = self.resolution_levels | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| multiscale = len(levels) > 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Determine which dims to keep from level 0's squeezed metadata. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Using isel instead of squeeze ensures all levels have | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # consistent ndim (lower levels may have extra singleton spatial dims | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # that squeeze would incorrectly remove). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref = self.reference_xarray | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| keep_dims = set(ref.dims) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arrays: list = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for level in levels: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.set_resolution_level(level) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| multiscale | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| or self._is_remote | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| or not determine_in_memory(self.path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| xr_data = self.xarray_dask_data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| xr_data = self.xarray_data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| indexer = {d: 0 for d in xr_data.dims if d not in keep_dims} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| indexer = {d: 0 for d in xr_data.dims if d not in keep_dims} | |
| # Any dimension present in this level but not in the reference | |
| # xarray is assumed to be singleton and is indexed at 0 to keep | |
| # all levels with consistent ndim. Validate this assumption so | |
| # that non-singleton extra dimensions don't get silently dropped. | |
| non_ref_dims = [d for d in xr_data.dims if d not in keep_dims] | |
| for dim in non_ref_dims: | |
| size = xr_data.sizes.get(dim) | |
| if size is not None and size != 1: | |
| raise ValueError( | |
| f"Non-singleton dimension {dim!r} (size {size}) is " | |
| f"present in resolution level {level} but not in " | |
| f"reference dims {tuple(ref.dims)}; indexing with 0 " | |
| "would silently drop data." | |
| ) | |
| indexer = {d: 0 for d in non_ref_dims} |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The resolution level restoration at line 264 won't execute if an exception occurs within the loop. Consider using a try-finally block to ensure the resolution level is always restored, even if an error occurs during data loading or indexing operations.
| for level in levels: | |
| self.set_resolution_level(level) | |
| if ( | |
| multiscale | |
| or self._is_remote | |
| or not determine_in_memory(self.path) | |
| ): | |
| xr_data = self.xarray_dask_data | |
| else: | |
| xr_data = self.xarray_data | |
| indexer = {d: 0 for d in xr_data.dims if d not in keep_dims} | |
| arrays.append(xr_data.isel(indexer).data) | |
| self.set_resolution_level(current_res) | |
| return arrays | |
| try: | |
| for level in levels: | |
| self.set_resolution_level(level) | |
| if ( | |
| multiscale | |
| or self._is_remote | |
| or not determine_in_memory(self.path) | |
| ): | |
| xr_data = self.xarray_dask_data | |
| else: | |
| xr_data = self.xarray_data | |
| indexer = {d: 0 for d in xr_data.dims if d not in keep_dims} | |
| arrays.append(xr_data.isel(indexer).data) | |
| return arrays | |
| finally: | |
| self.set_resolution_level(current_res) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -29,9 +29,9 @@ def test_nImage_init(resources_dir: Path): | |||||
| # Shape is (T, C, Z, Y, X) = (1, 2, 60, 66, 85) | ||||||
| assert img.data.shape == (1, 2, 60, 66, 85) | ||||||
| # layer_data should not be loaded until accessed | ||||||
| assert img._layer_data is None | ||||||
| assert img._reference_xarray is None | ||||||
| # Accessing the property triggers lazy loading | ||||||
| assert img.layer_data is not None | ||||||
| assert img.reference_xarray is not None | ||||||
|
|
||||||
|
|
||||||
| def test_nImage_zarr(resources_dir: Path): | ||||||
|
|
@@ -50,7 +50,7 @@ def test_nImage_remote_zarr(): | |||||
| assert img.path == remote_zarr | ||||||
| assert img._is_remote | ||||||
| # original shape is (1, 2, 1, 512, 512) but layer_data is squeezed | ||||||
|
||||||
| # original shape is (1, 2, 1, 512, 512) but layer_data is squeezed | |
| # original shape is (1, 2, 1, 512, 512) but reference_xarray is squeezed |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is now slightly misleading. The test is accessing reference_xarray (not layer_data), so the comment should be updated to say "reference_xarray will be squeezed" for clarity. While reference_xarray is indeed squeezed, the comment as written suggests the test is about layer_data.
| # layer_data will be squeezed | |
| # reference_xarray will be squeezed |
Copilot
AI
Feb 19, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While the test verifies single-resolution images return a list with one element, there's no test coverage for actual multiscale images with multiple resolution levels. Consider adding a test that loads a multiscale image (e.g., a zarr with multiple pyramid levels) to verify that the list contains multiple arrays with decreasing resolutions and that dimensions are handled correctly across levels.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type annotation should be more specific. Consider changing
list | Nonetolist[ArrayLike] | Noneto match the return type of _build_layer_data and provide better type safety.