Skip to content
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

feature/add-scene-selection-widget #25

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2bd22cc
Open QListWidget if len(img.scenes) > 1
psobolewskiPhD Sep 12, 2021
2d71dfe
Refactor meta into a function
psobolewskiPhD Sep 12, 2021
6a619e6
Initial try at getting LayerData from list
psobolewskiPhD Sep 12, 2021
a7ecb91
Return [(None,)] and call add_image
psobolewskiPhD Sep 12, 2021
45041db
Ensure meta["scale"] is used
psobolewskiPhD Sep 12, 2021
8e4b238
Add comments
psobolewskiPhD Sep 12, 2021
7681037
Try Flake8
psobolewskiPhD Sep 15, 2021
78edab7
Use napari.current_viewer()
psobolewskiPhD Sep 15, 2021
a69c9a6
Add & use scene indexes
psobolewskiPhD Sep 15, 2021
cd8911c
Fix ImgLayer name
psobolewskiPhD Sep 15, 2021
aaa4b77
Fix lines and return none
psobolewskiPhD Sep 15, 2021
1f7a28f
Clean-up & comments
psobolewskiPhD Sep 15, 2021
1b3fa28
change to napari[all] and v4.11
psobolewskiPhD Sep 15, 2021
196888b
Fix scale in LIF test
psobolewskiPhD Sep 15, 2021
308fc3a
Add pytest-qt to test_req
psobolewskiPhD Sep 15, 2021
a22b1cd
Test for multi-scene widget
psobolewskiPhD Sep 15, 2021
f9d5d6c
Fix nr of widgets check
psobolewskiPhD Sep 15, 2021
99ea946
Fix flake8 import error?
psobolewskiPhD Sep 15, 2021
1c21e5d
Fix flake8 imports take 2 (isort)
psobolewskiPhD Sep 15, 2021
a241491
AICSImageIO~4.1.0
psobolewskiPhD Sep 15, 2021
75ae94e
Don't add a list of widgets!!
psobolewskiPhD Sep 15, 2021
ed026a0
Test img layer from widget
psobolewskiPhD Sep 15, 2021
e285842
Fix (some) linting
psobolewskiPhD Sep 16, 2021
63f564d
Fix linting
psobolewskiPhD Sep 16, 2021
ff81a0c
Linting—for real this time?
psobolewskiPhD Sep 16, 2021
dacc272
Linting!
psobolewskiPhD Sep 16, 2021
d194b8f
Fix typing to use napari.types and TYPE_CHECKING
psobolewskiPhD Sep 17, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 108 additions & 55 deletions napari_aicsimageio/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@
# -*- coding: utf-8 -*-

from functools import partial
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from typing import TYPE_CHECKING, Any, Dict, List, Optional

import napari
import xarray as xr
from aicsimageio import AICSImage, exceptions, types
from aicsimageio import AICSImage, exceptions
from aicsimageio.dimensions import DimensionNames
from qtpy.QtWidgets import QListWidget, QListWidgetItem

###############################################################################

LayerData = Union[Tuple[types.ArrayLike, Dict[str, Any], str]]
PathLike = Union[str, List[str]]
ReaderFunction = Callable[[PathLike], List[LayerData]]

###############################################################################
if TYPE_CHECKING:
from napari.types import LayerData, PathLike, ReaderFunction


def _get_full_image_data(img: AICSImage, in_memory: bool) -> Optional[xr.DataArray]:
Expand All @@ -41,9 +38,89 @@ def _get_full_image_data(img: AICSImage, in_memory: bool) -> Optional[xr.DataArr
return None


# Function to handle multi-scene files.
def _get_scenes(img: AICSImage, in_memory: bool) -> None:

# Create the list widget and populate with the ids & scenes in the file
list_widget = QListWidget()
for i, scene in enumerate(img.scenes):
list_widget.addItem(f"{i} :: {scene}")
viewer = napari.current_viewer()
viewer.window.add_dock_widget(list_widget, area="right", name="Scene Selector")

# Function to create image layer from a scene selected in the list widget
def open_scene(item: QListWidgetItem) -> None:
scene_text = item.text()

# Use scene indexes to cover for duplicate names
scene_index = int(scene_text.split(" :: ")[0])
img.set_scene(scene_index)
if DimensionNames.MosaicTile in img.reader.dims.order:
try:
if in_memory:
data = img.reader.mosaic_xarray_data
else:
data = img.reader.mosaic_xarray_dask_data

# Catch reader does not support tile stitching
except NotImplementedError:
print(
"AICSImageIO: Mosaic tile stitching "
"not yet supported for this file format reader."
)
else:
if in_memory:
data = img.reader.xarray_data
else:
data = img.reader.xarray_dask_data
meta = _get_meta(data, img)
viewer.add_image(data, name=scene_text, metadata=meta, scale=meta["scale"])

list_widget.currentItemChanged.connect(open_scene)


# Function to get Metadata to provide with data
def _get_meta(data: xr.DataArray, img: AICSImage) -> Dict[str, Any]:
meta = {}
if DimensionNames.Channel in data.dims:

# Construct basic metadata
meta["name"] = data.coords[DimensionNames.Channel].data.tolist()
meta["channel_axis"] = data.dims.index(DimensionNames.Channel)

# Not multi-channel, use current scene as image name
else:
meta["name"] = img.reader.current_scene

# Handle samples / RGB
if DimensionNames.Samples in img.reader.dims.order:
meta["rgb"] = True

# Handle scales
scale: List[float] = []
for dim in img.reader.dims.order:
if dim in [
DimensionNames.SpatialX,
DimensionNames.SpatialY,
DimensionNames.SpatialZ,
]:
scale_val = getattr(img.physical_pixel_sizes, dim)
if scale_val is not None:
scale.append(scale_val)

# Apply scales
if len(scale) > 0:
meta["scale"] = tuple(scale)

# Apply all other metadata
meta["metadata"] = {"ome_types": img.metadata}

return meta


def reader_function(
path: PathLike, in_memory: bool, scene_name: Optional[str] = None
) -> Optional[List[LayerData]]:
path: "PathLike", in_memory: bool, scene_name: Optional[str] = None
) -> Optional[List["LayerData"]]:
"""
Given a single path return a list of LayerData tuples.
"""
Expand All @@ -57,56 +134,32 @@ def reader_function(

# Open file and get data
img = AICSImage(path)
print(
f"AICSImageIO: Image contains {len(img.scenes)} scenes. "
f"napari-aicsimageio currently only supports loading first scene, "
f"will load scene: '{img.current_scene}'."
)

data = _get_full_image_data(img, in_memory=in_memory)
# Check for multiple scenes
if len(img.scenes) > 1:
print(
f"AICSImageIO: Image contains {len(img.scenes)} scenes. "
f"Supporting more than the first scene is experimental. "
f"Select a scene from the list widget. There may be dragons!"
)
# Launch the list widget
_get_scenes(img, in_memory=in_memory)

# Catch None data
if data is None:
return None
# Return an empty LayerData list; ImgLayers will be handled via the widget.
# HT Jonas Windhager
return [(None,)]
psobolewskiPhD marked this conversation as resolved.
Show resolved Hide resolved
else:
# Metadata to provide with data
meta = {}
if DimensionNames.Channel in data.dims:
# Construct basic metadata
meta["name"] = data.coords[DimensionNames.Channel].data.tolist()
meta["channel_axis"] = data.dims.index(DimensionNames.Channel)

# Not multi-channel, use current scene as image name
else:
meta["name"] = img.reader.current_scene

# Handle samples / RGB
if DimensionNames.Samples in img.reader.dims.order:
meta["rgb"] = True

# Handle scales
scale: List[float] = []
for dim in img.reader.dims.order:
if dim in [
DimensionNames.SpatialX,
DimensionNames.SpatialY,
DimensionNames.SpatialZ,
]:
scale_val = getattr(img.physical_pixel_sizes, dim)
if scale_val is not None:
scale.append(scale_val)
data = _get_full_image_data(img, in_memory=in_memory)

# Apply scales
if len(scale) > 0:
meta["scale"] = tuple(scale)

# Apply all other metadata
meta["metadata"] = {"ome_types": img.metadata}

return [(data.data, meta, "image")]
# Catch None data
if data is None:
return None
else:
meta = _get_meta(data, img)
return [(data.data, meta, "image")]


def get_reader(path: PathLike, in_memory: bool) -> Optional[ReaderFunction]:
def get_reader(path: "PathLike", in_memory: bool) -> Optional["ReaderFunction"]:
"""
Given a single path or list of paths, return the appropriate aicsimageio reader.
"""
Expand Down
3 changes: 2 additions & 1 deletion napari_aicsimageio/in_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from typing import Optional

from napari.types import PathLike, ReaderFunction
from napari_plugin_engine import napari_hook_implementation

from . import core
Expand All @@ -11,5 +12,5 @@


@napari_hook_implementation
def napari_get_reader(path: core.PathLike) -> Optional[core.ReaderFunction]:
def napari_get_reader(path: PathLike) -> Optional[ReaderFunction]:
return core.get_reader(path, in_memory=True)
3 changes: 2 additions & 1 deletion napari_aicsimageio/out_of_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from typing import Optional

from napari.types import PathLike, ReaderFunction
from napari_plugin_engine import napari_hook_implementation

from . import core
Expand All @@ -11,5 +12,5 @@


@napari_hook_implementation
def napari_get_reader(path: core.PathLike) -> Optional[core.ReaderFunction]:
def napari_get_reader(path: PathLike) -> Optional[ReaderFunction]:
return core.get_reader(path, in_memory=False)
63 changes: 61 additions & 2 deletions napari_aicsimageio/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
# -*- coding: utf-8 -*-

from pathlib import Path
from typing import Any, Dict, Tuple
from typing import Any, Callable, Dict, Tuple

import dask.array as da
import napari
import numpy as np
import pytest

Expand Down Expand Up @@ -57,7 +58,7 @@
{
"name": ["Gray", "Red", "Green", "Cyan"],
"channel_axis": 1,
"scale": (4.984719055966396, 4.984719055966396),
"scale": (0.20061311154598827, 0.20061311154598827),
},
),
],
Expand Down Expand Up @@ -94,3 +95,61 @@ def test_reader(
# Check meta
meta.pop("metadata", None)
assert meta == expected_meta # type: ignore


SINGLESCENE_FILE = "s_1_t_1_c_1_z_1.czi"
MULTISCENE_FILE = "s_3_t_1_c_3_z_5.czi"


@pytest.mark.parametrize(
"in_memory, expected_dtype",
[
(True, np.ndarray),
(False, da.core.Array),
],
)
@pytest.mark.parametrize(
"filename, nr_widgets, expected_shape",
[
(SINGLESCENE_FILE, 0, (1, 325, 475)),
(MULTISCENE_FILE, 1, (3, 5, 325, 475)),
],
)
def test_for_multiscene_widget(
make_napari_viewer: Callable[..., napari.Viewer],
resources_dir: Path,
filename: str,
in_memory: bool,
nr_widgets: int,
expected_dtype: type,
expected_shape: Tuple[int, ...],
) -> None:
# Make a viewer
viewer = make_napari_viewer()
assert len(viewer.layers) == 0
assert len(viewer.window._dock_widgets) == 0

# Resolve filename to filepath
if isinstance(filename, str):
path = str(resources_dir / filename)

# Get reader
reader = core.get_reader(path, in_memory)

if reader is not None:
# Call reader on path
reader(path)

# Check for list widget
assert len(viewer.window._dock_widgets) == nr_widgets

if len(viewer.window._dock_widgets) != 0:
assert list(viewer.window._dock_widgets.keys())[0] == "Scene Selector"
viewer.window._dock_widgets["Scene Selector"].widget().setCurrentRow(1)
data = viewer.layers[0].data
assert isinstance(data.data, expected_dtype) # type: ignore
assert data.shape == expected_shape # type: ignore
else:
data, meta, _ = reader(path)[0]
assert isinstance(data, expected_dtype) # type: ignore
assert data.shape == expected_shape # type: ignore
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"mypy>=0.800",
"psutil>=5.7.0",
"pytest>=5.4.3",
"pytest-qt",
"pytest-cov>=2.9.0",
"pytest-raises>=0.11",
"quilt3~=3.4.0",
Expand All @@ -40,9 +41,9 @@
]

requirements = [
"aicsimageio[all]~=4.0.2",
"aicsimageio[all]~=4.1.0",
"fsspec[http]", # no version pin, we pull from aicsimageio
"napari~=0.4.10",
"napari[all]~=0.4.11",
"napari_plugin_engine~=0.1.4",
]

Expand Down