Skip to content

Commit 08d1e62

Browse files
authored
nImage.get_layer_data_tuple for a ready-for-napari approach (#37)
# Description This PR was built out of the need to support reading data that can be added as an image and/or labels layer. I thought more closely about things, and realized that I can collect the information more reliably as a layer data tuple, and take advantage of napari's machinery for such endeavour. This now makes `nImage` serve effectively one function besides the `BioImage` objects it already inherits: `layer_data_tuples = nImage(...).get_layer_data_tuples`. Everything else is private methods to help achieve this. Now, various things can be passed to this method, including channel_kwargs for overrides, but otherwise sensible defaults. The only disadvantage of this approach is that I had to remove the ease-of-use `channel_axis` kwarg to `add_image`, but I think I finally understand the napari discussions for why that should be removed -- it just doesn't translate well to other Layer Data Tuples. So now, I fully handle channel splitting myself, and make that the only way to process channel images. No more can channels be not split. ## Copilot Summary This pull request refactors the way multichannel images are handled and displayed in napari, improving channel splitting and metadata handling, and updating related tests and plugin configuration. The main changes include splitting image channels into separate napari layers, introducing colormap utilities, updating tests to reflect the new layer structure, and removing unused writer plugin definitions. **Image Layer Handling and Colormap Utilities** * Added a new module `src/ndevio/_colormap_utils.py` to provide colormap cycles and a utility for assigning colormaps to channels, supporting napari's conventions for single, dual, and multi-channel images. * Refactored image reading logic in `src/ndevio/_napari_reader.py` and related widget code to split channels into separate napari layers, returning lists of `LayerDataTuple` objects. This enables more accurate visualization and metadata per channel. [[1]](diffhunk://#diff-a1495b29f6e07c8278a427f1ad3f4b1033da9353d263c050f0c8d9ac1b2aeebdL98-L112) [[2]](diffhunk://#diff-a1495b29f6e07c8278a427f1ad3f4b1033da9353d263c050f0c8d9ac1b2aeebdL126-L162) [[3]](diffhunk://#diff-95cabfe66da31c6e66e52a789bb410721325aa105aacbc8a75b79194a9974351L124-R136) **Testing Updates** * Updated tests in `tests/test_napari_reader.py` and `tests/test_nimage.py` to validate that multichannel images are split into separate layers, and to check metadata and layer tuple structure accordingly. Test names and assertions were revised to match the new logic. [[1]](diffhunk://#diff-0e2609b36d787eac366ca4e6829e2509c539d1615bb1a700973c91176a6879e6R37-R39) [[2]](diffhunk://#diff-0e2609b36d787eac366ca4e6829e2509c539d1615bb1a700973c91176a6879e6L48-R63) [[3]](diffhunk://#diff-0e2609b36d787eac366ca4e6829e2509c539d1615bb1a700973c91176a6879e6R73) [[4]](diffhunk://#diff-0e2609b36d787eac366ca4e6829e2509c539d1615bb1a700973c91176a6879e6L83-R95) [[5]](diffhunk://#diff-6717c11ecd6a087070b7996a7e347caff169ea4db335e1edde84909b605ffbe1L35-R35) [[6]](diffhunk://#diff-6717c11ecd6a087070b7996a7e347caff169ea4db335e1edde84909b605ffbe1L133-R166) [[7]](diffhunk://#diff-6717c11ecd6a087070b7996a7e347caff169ea4db335e1edde84909b605ffbe1L177) [[8]](diffhunk://#diff-6717c11ecd6a087070b7996a7e347caff169ea4db335e1edde84909b605ffbe1L187-R213) [[9]](diffhunk://#diff-6717c11ecd6a087070b7996a7e347caff169ea4db335e1edde84909b605ffbe1L217) [[10]](diffhunk://#diff-6717c11ecd6a087070b7996a7e347caff169ea4db335e1edde84909b605ffbe1L229-R247) [[11]](diffhunk://#diff-6717c11ecd6a087070b7996a7e347caff169ea4db335e1edde84909b605ffbe1L250-R263) [[12]](diffhunk://#diff-6717c11ecd6a087070b7996a7e347caff169ea4db335e1edde84909b605ffbe1L269-R278) **Plugin Configuration** * Removed writer plugin definitions from `src/ndevio/napari.yaml` to clean up unused functionality and focus the plugin on reading and visualization. [[1]](diffhunk://#diff-37781c656dd096700e29a43b6a5fb914d4e9f1632516709c538774e9fb0e5149L12-L17) [[2]](diffhunk://#diff-37781c656dd096700e29a43b6a5fb914d4e9f1632516709c538774e9fb0e5149L62-L68)
1 parent 841d84e commit 08d1e62

File tree

9 files changed

+741
-216
lines changed

9 files changed

+741
-216
lines changed

pixi.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ndevio/_colormap_utils.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Colormap utilities for ndevio.
2+
3+
This module provides colormap cycles for
4+
multichannel image display.
5+
"""
6+
7+
SINGLE_CHANNEL_COLORMAP = "gray"
8+
9+
TWO_CHANNEL_CYCLE = ["magenta", "green"]
10+
11+
MULTI_CHANNEL_CYCLE = ["cyan", "magenta", "yellow", "blue", "green", "red"]
12+
13+
RGB = ["red", "green", "blue"]
14+
15+
16+
def get_colormap_for_channel(channel_idx: int, n_channels: int) -> str:
17+
"""
18+
Get colormap for a channel based on napari's defaults.
19+
20+
- 1 channel → gray
21+
- 2 channels → magenta, green (TWO_CHANNEL_CYCLE)
22+
- 3+ channels → cycles through MULTI_CHANNEL_CYCLE (CMYBGR)
23+
24+
Parameters
25+
----------
26+
channel_idx : int
27+
Index of the channel (0-based).
28+
n_channels : int
29+
Total number of channels in the image.
30+
31+
Returns
32+
-------
33+
str
34+
Colormap name.
35+
36+
"""
37+
if n_channels == 1:
38+
return SINGLE_CHANNEL_COLORMAP
39+
elif n_channels == 2:
40+
return TWO_CHANNEL_CYCLE[channel_idx % len(TWO_CHANNEL_CYCLE)]
41+
else:
42+
return MULTI_CHANNEL_CYCLE[channel_idx % len(MULTI_CHANNEL_CYCLE)]
43+
44+
45+
__all__ = [
46+
"SINGLE_CHANNEL_COLORMAP",
47+
"TWO_CHANNEL_CYCLE",
48+
"MULTI_CHANNEL_CYCLE",
49+
"RGB",
50+
"get_colormap_for_channel",
51+
]

src/ndevio/_napari_reader.py

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .nimage import determine_reader_plugin, nImage
1212

1313
if TYPE_CHECKING:
14-
from napari.types import LayerData, PathLike, ReaderFunction
14+
from napari.types import LayerDataTuple, PathLike, ReaderFunction
1515

1616
logger = logging.getLogger(__name__)
1717

@@ -95,21 +95,18 @@ def napari_reader_function(
9595
in_memory: bool | None = None,
9696
open_first_scene_only: bool = False,
9797
open_all_scenes: bool = False,
98-
layer_type: str = "image",
99-
) -> list[LayerData] | None:
98+
) -> list[LayerDataTuple] | None:
10099
"""
101100
Read a file using the given reader function.
102101
103102
Parameters
104103
----------
105104
path : PathLike
106105
Path to the file to be read
107-
reader : None
108-
Bioio Reader function to be used to read the file, by default None.
106+
reader : Callable
107+
Bioio Reader class to be used to read the file.
109108
in_memory : bool, optional
110109
Whether to read the file in memory, by default None.
111-
layer_type : str, optional
112-
Type of layer to be created in napari, by default 'image'.
113110
open_first_scene_only : bool, optional
114111
Whether to ignore multi-scene files and just open the first scene,
115112
by default False.
@@ -123,41 +120,23 @@ def napari_reader_function(
123120
List containing image data, metadata, and layer type
124121
125122
"""
126-
if isinstance(path, list):
127-
logger.info("Bioio: Expected a single path, got a list of paths.")
128-
return None
129-
130123
img = nImage(path, reader=reader)
131-
in_memory = (
132-
img._determine_in_memory(path) if in_memory is None else in_memory
133-
)
134-
# TODO: Guess layer type here (check channel names for labels?)
135-
logger.info("Bioio: Reading in-memory: %s", in_memory)
124+
logger.info("Bioio: Reading file with %d scenes", len(img.scenes))
136125

137126
# open first scene only
138127
if len(img.scenes) == 1 or open_first_scene_only:
139-
img_data = img.get_napari_image_data(in_memory=in_memory)
140-
img_meta = img.get_napari_metadata(path)
141-
return [(img_data.data, img_meta, layer_type)]
128+
return img.get_layer_data_tuples(in_memory=in_memory)
142129

143-
# TODO: USE settings for open first or all scenes to set the nubmer of iterations of a for loop
144-
# check napari reader settings stuff
145130
# open all scenes as layers
146-
if len(img.scenes) > 1 and open_all_scenes:
131+
if open_all_scenes:
147132
layer_list = []
148133
for scene in img.scenes:
149134
img.set_scene(scene)
150-
img_data = img.get_napari_image_data(in_memory=in_memory)
151-
img_meta = img.get_napari_metadata(path)
152-
layer_list.append((img_data.data, img_meta, layer_type))
135+
layer_list.extend(img.get_layer_data_tuples(in_memory=in_memory))
153136
return layer_list
154137

155-
# open scene widget
156-
if len(img.scenes) > 1 and not open_all_scenes:
157-
_open_scene_container(path=path, img=img, in_memory=in_memory)
158-
return [(None,)]
159-
160-
logger.warning("Bioio: Error reading file")
138+
# else: open scene widget
139+
_open_scene_container(path=path, img=img, in_memory=in_memory)
161140
return [(None,)]
162141

163142

src/ndevio/napari.yaml

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,6 @@ contributions:
99
- id: ndevio.get_reader
1010
python_name: ndevio._napari_reader:napari_get_reader
1111
title: Open file with ndevio
12-
- id: ndevio.write_multiple
13-
python_name: ndevio._writer:write_multiple
14-
title: Save multi-layer data with ndevio
15-
- id: ndevio.write_single_image
16-
python_name: ndevio._writer:write_single_image
17-
title: Save image data with ndevio
1812
- id: ndevio.make_plugin_installer_widget
1913
python_name: ndevio.widgets:PluginInstallerWidget
2014
title: Install BioIO Reader Plugins
@@ -59,13 +53,6 @@ contributions:
5953
'*.wlz', '*.wmf', '*.wmv', '*.wpi', '*.xbm', '*.xdce', '*.xml', '*.xpm', '*.xqd',
6054
'*.xqf', '*.xv', '*.xys', '*.zfp', '*.zfr', '*.zip', '*.zpo', '*.zvi'
6155
]
62-
writers:
63-
- command: ndevio.write_multiple
64-
layer_types: ['image*','labels*']
65-
filename_extensions: []
66-
- command: ndevio.write_single_image
67-
layer_types: ['image']
68-
filename_extensions: ['.npy']
6956
widgets:
7057
- command: ndevio.make_plugin_installer_widget
7158
display_name: Install BioIO Reader Plugins

src/ndevio/ndev_settings.yaml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ ndevio_reader:
1818
default: Open Scene Widget
1919
tooltip: How to handle files with multiple scenes
2020
value: Open Scene Widget
21-
unpack_channels_as_layers:
22-
default: true
23-
tooltip: Whether to unpack multi-channel images as separate layers
24-
value: true
2521
clear_layers_on_new_scene:
2622
default: false
2723
tooltip: Whether to clear the viewer when selecting a new scene

0 commit comments

Comments
 (0)