Skip to content

Commit

Permalink
feature/scene-widget-additions (#27)
Browse files Browse the repository at this point in the history
* Open QListWidget if len(img.scenes) > 1

* Refactor meta into a function

* Initial try at getting LayerData from list

* Return [(None,)] and call add_image

* Ensure meta["scale"] is used

* Add comments

* Try Flake8

* Use napari.current_viewer()

* Add & use scene indexes

* Fix ImgLayer name

* Fix lines and return none

* Clean-up & comments

* change to napari[all] and v4.11

* Fix scale in LIF test

* Add pytest-qt to test_req

* Test for multi-scene widget

* Fix nr of widgets check

* Fix flake8 import error?

* Fix flake8 imports take 2 (isort)

* AICSImageIO~4.1.0

* Don't add a list of widgets!!

* Test img layer from widget

* Fix (some) linting

* Fix linting

* Linting—for real this time?

* Linting!

* Fix typing to use napari.types and TYPE_CHECKING

* Add qt support libs to gh workflos

* Change napari all to pyqt5 and pyside

* Add functionality for clearing layers on scene select

* Change to with and run for xvfb runner

* Change to napari install all

* Fix / update tests to remove n widgets check

* Check n number of layers instead of scene shape

* Don't test layers

* Add scene selector gif

* Add scene selection feature and gif to README

* Loosen aicsimageio dep pin

* Add checkbox for unpack channels and change defaults

* Change shape check back to multi-channel

* Wrong shape

* Update scene selection feature gif

* Add scene index to layer names

* Set layer name to scene label when not unpking ch

* Show scene name in layer

Co-authored-by: Peter Sobolewski <[email protected]>
  • Loading branch information
Jackson Maxfield Brown and psobolewskiPhD authored Sep 18, 2021
1 parent 8f298c6 commit ccdc11b
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 90 deletions.
9 changes: 7 additions & 2 deletions .github/workflows/build-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,17 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install .[test]
# Install qt support libs
- uses: tlambert03/setup-qt-libs@v1

- name: Download Test Resources
run: |
python scripts/download_test_resources.py
- name: Test with pytest
run: |
pytest --cov-report xml --cov=napari_aicsimageio napari_aicsimageio/tests/
uses: GabrielBB/xvfb-action@v1
with:
run: pytest --cov-report xml --cov=napari_aicsimageio napari_aicsimageio/tests/
- name: Upload codecov
uses: codecov/codecov-action@v1

Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/test-and-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,17 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install .[test]
# Install qt support libs
- uses: tlambert03/setup-qt-libs@v1

- name: Download Test Resources
run: |
python scripts/download_test_resources.py
- name: Test with pytest
run: |
pytest --cov-report xml --cov=napari_aicsimageio napari_aicsimageio/tests/
uses: GabrielBB/xvfb-action@v1
with:
run: pytest --cov-report xml --cov=napari_aicsimageio napari_aicsimageio/tests/
- name: Upload codecov
uses: codecov/codecov-action@v1

Expand Down
39 changes: 23 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ AICSImageIO bindings for napari
---

## Features
* Supports reading metadata and imaging data for:
* `CZI`
* `OME-TIFF`
* `TIFF`
* Any formats supported by [aicsimageio](https://github.com/AllenCellModeling/aicsimageio)
* Any additional format supported by [imageio](https://github.com/imageio/imageio)

- Supports reading metadata and imaging data for:
- `CZI`
- `OME-TIFF`
- `TIFF`
- Any formats supported by [aicsimageio](https://github.com/AllenCellModeling/aicsimageio)
- Any additional format supported by [imageio](https://github.com/imageio/imageio)

## Installation

**Stable Release:** `pip install napari-aicsimageio`<br>
**Development Head:** `pip install git+https://github.com/AllenCellModeling/napari-aicsimageio.git`

Expand All @@ -24,9 +26,10 @@ AICSImageIO bindings for napari
![screenshot of plugin sorter showing that napari-aicsimageio-in-memory should be placed above napari-aicsimageio-out-of-memory](https://raw.githubusercontent.com/AllenCellModeling/napari-aicsimageio/main/images/plugin-sorter.png)

There are two variants of this plugin that are added during installation:
* `aicsimageio-in-memory`, which reads an image fully into memory
* `aicsimageio-out-of-memory`, which delays reading ZYX chunks until required.
This allows for incredible large files to be read and displayed.

- `aicsimageio-in-memory`, which reads an image fully into memory
- `aicsimageio-out-of-memory`, which delays reading ZYX chunks until required.
This allows for incredible large files to be read and displayed.

## Examples of Features

Expand All @@ -41,21 +44,25 @@ napari viewer thanks to `ome-types`.

![screenshot of an OME-TIFF image view, multi-channel, z-stack, with metadata viewer](https://raw.githubusercontent.com/AllenCellModeling/napari-aicsimageio/main/images/ome-tiff-with-metadata-viewer.png)

#### Mosaic CZI Reading
#### Mosaic Reading

When reading CZI images, if the image is a mosaic tiled image, `napari-aicsimageio`
When reading CZI or LIF images, if the image is a mosaic tiled image, `napari-aicsimageio`
will return the reconstructed image:

![screenshot of a reconstructed / restitched mosaic tile CZI](https://raw.githubusercontent.com/AllenCellModeling/napari-aicsimageio/main/images/tiled-czi.png)
![screenshot of a reconstructed / restitched mosaic tile LIF](https://raw.githubusercontent.com/AllenCellModeling/napari-aicsimageio/main/images/tiled-lif.png)

#### Multi-Scene Selection

#### Mosaic LIF Reading
**Experimental**

When reading LIF images, if the image is a mosaic tiled image, `napari-aicsimageio`
will return the reconstructed image:
When reading a multi-scene file, a widget will be added to the napari viewer to manage
scene selection (clearing the viewer each time you change scene or adding the
scene content to the viewer) and a list of all scenes in the file.

![screenshot of a reconstructed / restitched mosaic tile LIF](https://raw.githubusercontent.com/AllenCellModeling/napari-aicsimageio/main/images/tiled-lif.png)
![gif of drag and drop file to scene selection and management](https://raw.githubusercontent.com/AllenCellModeling/napari-aicsimageio/main/images/scene-selection.gif)

## Development

See [CONTRIBUTING.md](CONTRIBUTING.md) for information related to developing the code.

For additional file format support, contributed directly to
Expand Down
Binary file added images/scene-selection.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
231 changes: 170 additions & 61 deletions napari_aicsimageio/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,45 @@
# -*- coding: utf-8 -*-

from functools import partial
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from pathlib import Path
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 (
QCheckBox,
QGroupBox,
QListWidget,
QListWidgetItem,
QVBoxLayout,
)

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

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

LayerData = Union[Tuple[types.ArrayLike, Dict[str, Any], str]]
PathLike = Union[str, List[str]]
ReaderFunction = Callable[[PathLike], List[LayerData]]
AICSIMAGEIO_CHOICES = "AICSImageIO Scene Management"
CLEAR_LAYERS_ON_SELECT = "Clear All Layers on New Scene Selection"
UNPACK_CHANNELS_TO_LAYERS = "Unpack Channels as Layers"

SCENE_LABEL_DELIMITER = " :: "

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


def _get_full_image_data(img: AICSImage, in_memory: bool) -> Optional[xr.DataArray]:
def _get_full_image_data(
img: AICSImage,
in_memory: bool,
) -> xr.DataArray:
if DimensionNames.MosaicTile in img.reader.dims.order:
try:
if in_memory:
return img.reader.mosaic_xarray_data
else:
return img.reader.mosaic_xarray_dask_data

return img.reader.mosaic_xarray_dask_data

# Catch reader does not support tile stitching
except NotImplementedError:
Expand All @@ -32,18 +49,139 @@ def _get_full_image_data(img: AICSImage, in_memory: bool) -> Optional[xr.DataArr
"not yet supported for this file format reader."
)

if in_memory:
return img.reader.xarray_data

return img.reader.xarray_dask_data


# Function to get Metadata to provide with data
def _get_meta(data: xr.DataArray, img: AICSImage) -> Dict[str, Any]:
meta: Dict[str, Any] = {}
if DimensionNames.Channel in data.dims:
# Construct basic metadata
channels_with_scene_index = [
f"{img.current_scene_index}{SCENE_LABEL_DELIMITER}"
f"{img.current_scene}{SCENE_LABEL_DELIMITER}{channel_name}"
for channel_name in data.coords[DimensionNames.Channel].data.tolist()
]
meta["name"] = channels_with_scene_index
meta["channel_axis"] = data.dims.index(DimensionNames.Channel)

# Not multi-channel, use current scene as image name
else:
if in_memory:
return img.reader.xarray_data
else:
return img.reader.xarray_dask_data
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 _widget_is_checked(widget_name: str) -> bool:
# Get napari viewer from current process
viewer = napari.current_viewer()

# Get scene management widget
scene_manager_choices_widget = viewer.window._dock_widgets[AICSIMAGEIO_CHOICES]
for child in scene_manager_choices_widget.widget().children():
if isinstance(child, QCheckBox):
if child.text() == widget_name:
return child.isChecked()

return False


# Function to handle multi-scene files.
def _get_scenes(path: "PathLike", img: AICSImage, in_memory: bool) -> None:
# Get napari viewer from current process
viewer = napari.current_viewer()

# Add a checkbox widget if not present
if AICSIMAGEIO_CHOICES not in viewer.window._dock_widgets:
# Create a checkbox widget to set "Clear On Scene Select" or not
scene_clear_checkbox = QCheckBox(CLEAR_LAYERS_ON_SELECT)
scene_clear_checkbox.setChecked(False)

# Create a checkbox widget to set "Unpack Channels" or not
channel_unpack_checkbox = QCheckBox(UNPACK_CHANNELS_TO_LAYERS)
channel_unpack_checkbox.setChecked(False)

# Add all scene management state to a single box
scene_manager_group = QGroupBox()
scene_manager_group_layout = QVBoxLayout()
scene_manager_group_layout.addWidget(scene_clear_checkbox)
scene_manager_group_layout.addWidget(channel_unpack_checkbox)
scene_manager_group.setLayout(scene_manager_group_layout)
scene_manager_group.setFixedHeight(100)

viewer.window.add_dock_widget(
scene_manager_group,
area="right",
name=AICSIMAGEIO_CHOICES,
)

# 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_LABEL_DELIMITER}{scene}")

# Add this files scenes widget to viewer
viewer.window.add_dock_widget(
list_widget,
area="right",
name=f"{Path(path).name}{SCENE_LABEL_DELIMITER}Scenes",
)

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

return None
# Use scene indexes to cover for duplicate names
scene_index = int(scene_text.split(SCENE_LABEL_DELIMITER)[0])

# Update scene on image and get data
img.set_scene(scene_index)
data = _get_full_image_data(img=img, in_memory=in_memory)

def reader_function(
path: PathLike, in_memory: bool, scene_name: Optional[str] = None
) -> Optional[List[LayerData]]:
# Get metadata and add to image
meta = _get_meta(data, img)

# Optionally clear layers
if _widget_is_checked(CLEAR_LAYERS_ON_SELECT):
viewer.layers.clear()

# Optionally remove channel axis
if not _widget_is_checked(UNPACK_CHANNELS_TO_LAYERS):
meta["name"] = scene_text
meta.pop("channel_axis", None)

viewer.add_image(data, **meta)

list_widget.currentItemChanged.connect(open_scene)


def reader_function(path: "PathLike", in_memory: bool) -> Optional[List["LayerData"]]:
"""
Given a single path return a list of LayerData tuples.
"""
Expand All @@ -57,56 +195,27 @@ 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(path=path, img=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,)]
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)

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

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

data = _get_full_image_data(img, in_memory=in_memory)
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 All @@ -123,7 +232,7 @@ def get_reader(path: PathLike, in_memory: bool) -> Optional[ReaderFunction]:

# The above line didn't error so we know we have a supported reader
# Return a partial function with in_memory determined
return partial(reader_function, in_memory=in_memory) # type: ignore
return partial(reader_function, in_memory=in_memory)

# No supported reader, return None
except exceptions.UnsupportedFileFormatError:
Expand Down
Loading

0 comments on commit ccdc11b

Please sign in to comment.