#!/usr/bin/env python
# -*- coding: utf-8 -*-
import xml.etree.ElementTree as ET
from copy import copy
from math import floor
from typing import Any, Dict, Hashable, List, Optional, Tuple, Union
import dask.array as da
import numpy as np
import xarray as xr
from dask import delayed
from fsspec.spec import AbstractFileSystem
from .. import constants, exceptions, transforms, types
from ..dimensions import (
DEFAULT_CHUNK_DIMS,
DEFAULT_DIMENSION_ORDER_LIST,
DEFAULT_DIMENSION_ORDER_LIST_WITH_MOSAIC_TILES,
REQUIRED_CHUNK_DIMS,
DimensionNames,
)
from ..utils import io_utils
from .reader import Reader
try:
from readlif.reader import LifFile
except ImportError:
raise ImportError(
"readlif is required for this reader. "
"Install with `pip install 'readlif>=0.6.4'`"
)
###############################################################################
[docs]class LifReader(Reader):
"""
Wraps the readlif API to provide the same aicsimageio Reader API but for
volumetric LIF images.
Parameters
----------
image: types.PathLike
Path to image file to construct Reader for.
chunk_dims: Union[str, List[str]]
Which dimensions to create chunks for.
Default: DEFAULT_CHUNK_DIMS
Note: Dimensions.SpatialY, Dimensions.SpatialX, and DimensionNames.Samples,
will always be added to the list if not present during dask array
construction.
fs_kwargs: Dict[str, Any]
Any specific keyword arguments to pass down to the fsspec created filesystem.
Default: {}
Notes
-----
To use this reader, install with: `pip install readlif>=0.6.4`.
readlif is licensed under GPLv3 and is not included in this package.
"""
@staticmethod
def _is_supported_image(fs: AbstractFileSystem, path: str, **kwargs: Any) -> bool:
try:
with fs.open(path) as open_resource:
LifFile(open_resource)
return True
except ValueError:
return False
def __init__(
self,
image: types.PathLike,
chunk_dims: Union[str, List[str]] = DEFAULT_CHUNK_DIMS,
fs_kwargs: Dict[str, Any] = {},
is_x_flipped: bool = False,
is_y_flipped: bool = False,
is_x_and_y_swapped: bool = True,
):
# Expand details of provided image
self._fs, self._path = io_utils.pathlike_to_fs(
image,
enforce_exists=True,
fs_kwargs=fs_kwargs,
)
# Store params
if isinstance(chunk_dims, str):
chunk_dims = list(chunk_dims)
self.chunk_dims = chunk_dims
# If either of these are True, the respective dimension will be
# flipped along the other axis.
# Ex. if `is_x_flipped = True` in a 4x4 tiled space, coordinate (2, 3)
# will be (1, 3)
# Ex. where both are true in a 4x4 tiled space, coordinate (1, 0)
# will be (2, 3)
# from the mosaic_position will be swapped such that field_x represents y
# and field_y represents x.
self.is_x_flipped = is_x_flipped
self.is_y_flipped = is_y_flipped
# If `is_x_and_y_swapped` is True, the field_x and field_y given
# from the mosaic_position will be swapped such that field_x represents y
# and field_y represents x.
self.is_x_and_y_swapped = is_x_and_y_swapped
# Delayed storage
self._scene_short_info: Dict[str, Any] = {}
self._px_sizes: Optional[types.PhysicalPixelSizes] = None
# Enforce valid image
if not self._is_supported_image(self._fs, self._path):
raise exceptions.UnsupportedFileFormatError(
self.__class__.__name__, self._path
)
@property
def scenes(self) -> Tuple[str, ...]:
if self._scenes is None:
with self._fs.open(self._path) as open_resource:
lif = LifFile(open_resource)
scene_names = [image["name"] for image in lif.image_list]
self._scenes = tuple(scene_names)
return self._scenes
@staticmethod
def _get_image_data(
fs: AbstractFileSystem,
path: str,
scene: int,
retrieve_dims: List[str],
retrieve_indices: List[Optional[int]],
) -> np.ndarray:
"""
Open a file for reading, construct a Zarr store, select data, and compute to
numpy.
Parameters
----------
fs: AbstractFileSystem
The file system to use for reading.
path: str
The path to file to read.
scene: int
The scene index to pull the chunk from.
retrieve_dims: List[str]
The order of the retrieve indicies operations
retrieve_indices: List[Optional[int]],
The image index operations to retrieve.
If None, retrieve the whole dimension.
Returns
-------
chunk: np.ndarray
The image chunk as a numpy array.
"""
MISSING_DIM_SENTINAL_VALUE = -1
# Open and select the target image
with fs.open(path) as open_resource:
selected_scene = LifFile(open_resource).get_image(scene)
# Create the fill array shape
# Drop the YX as we will be pulling the individual YX planes
retrieve_shape: List[int] = []
use_selected_or_np_map: Dict[str, int] = {}
for dim, index_op in zip(retrieve_dims, retrieve_indices):
if dim not in [DimensionNames.SpatialY, DimensionNames.SpatialX]:
# Handle slices
if index_op is None:
# Store the dim for later to inform to use the np index
use_selected_or_np_map[dim] = MISSING_DIM_SENTINAL_VALUE
if dim == DimensionNames.MosaicTile:
retrieve_shape.append(selected_scene.n_mosaic)
elif dim == DimensionNames.Time:
retrieve_shape.append(selected_scene.nt)
elif dim == DimensionNames.Channel:
retrieve_shape.append(selected_scene.channels)
elif dim == DimensionNames.SpatialZ:
retrieve_shape.append(selected_scene.nz)
# Handle non-chunk dimensions (specific indices / ints)
else:
# Store the dim for later to inform to use the provided index
use_selected_or_np_map[dim] = index_op
retrieve_shape.append(1)
# Create list of planes that we will add each plane to, later we reshape
# Create empty arr with the desired shape to enumerate over the np index
planes: List[np.ndarray] = []
np_array_for_indices = np.empty(tuple(retrieve_shape), dtype=object)
for np_index, _ in np.ndenumerate(np_array_for_indices):
# Get each plane's index selection operations
# If the dimension is None, use the enumerated np index
# If the dimension is not None, use the provided value
plane_indices: Dict[str, int] = {}
# Handle MosaicTile
if DimensionNames.MosaicTile in use_selected_or_np_map:
if (
use_selected_or_np_map[DimensionNames.MosaicTile]
== MISSING_DIM_SENTINAL_VALUE
):
plane_indices["m"] = np_index[
retrieve_dims.index(DimensionNames.MosaicTile)
]
else:
plane_indices["m"] = use_selected_or_np_map[
DimensionNames.MosaicTile
]
# Handle Time
if (
use_selected_or_np_map[DimensionNames.Time]
== MISSING_DIM_SENTINAL_VALUE
):
plane_indices["t"] = np_index[
retrieve_dims.index(DimensionNames.Time)
]
else:
plane_indices["t"] = use_selected_or_np_map[DimensionNames.Time]
# Handle Channels
if (
use_selected_or_np_map[DimensionNames.Channel]
== MISSING_DIM_SENTINAL_VALUE
):
plane_indices["c"] = np_index[
retrieve_dims.index(DimensionNames.Channel)
]
else:
plane_indices["c"] = use_selected_or_np_map[DimensionNames.Channel]
# Handle SpatialZ
if (
use_selected_or_np_map[DimensionNames.SpatialZ]
== MISSING_DIM_SENTINAL_VALUE
):
plane_indices["z"] = np_index[
retrieve_dims.index(DimensionNames.SpatialZ)
]
else:
plane_indices["z"] = use_selected_or_np_map[DimensionNames.SpatialZ]
# Append the retrieved plane as a numpy array
planes.append(np.asarray(selected_scene.get_frame(**plane_indices)))
# Stack and reshape to get rid of the array of arrays
scene_dims = selected_scene.info["dims"]
retrieved_chunk = np.stack(planes).reshape(
np_array_for_indices.shape + (scene_dims.y, scene_dims.x)
)
# Remove extra dimensions if they were not requested
remove_dim_ops_list: List[Union[int, slice]] = []
for index in retrieve_indices:
if isinstance(index, int):
remove_dim_ops_list.append(0)
else:
remove_dim_ops_list.append(slice(None, None, None))
# Remove extra dimensions by using dim ops
retrieved_chunk = retrieved_chunk[tuple(remove_dim_ops_list)]
return retrieved_chunk
def _create_dask_array(
self, lif: LifFile, selected_scene_dims: List[str]
) -> xr.DataArray:
"""
Creates a delayed dask array for the file.
Parameters
----------
lif: LifFile
An open LifFile for processing.
selected_scene_dims: List[str]
The dimensions for the scene to create the dask array for
Returns
-------
image_data: da.Array
The fully constructed and fully delayed image as a Dask Array object.
"""
# Always add the plane dimensions if not present already
for dim in REQUIRED_CHUNK_DIMS:
if dim not in self.chunk_dims:
self.chunk_dims.append(dim)
# Safety measure / "feature"
self.chunk_dims = [d.upper() for d in self.chunk_dims]
# Construct the delayed dask array
selected_scene = lif.get_image(self.current_scene_index)
selected_scene_shape: List[int] = []
for dim in selected_scene_dims:
if dim == DimensionNames.MosaicTile:
selected_scene_shape.append(selected_scene.n_mosaic)
elif dim == DimensionNames.Time:
selected_scene_shape.append(selected_scene.nt)
elif dim == DimensionNames.Channel:
selected_scene_shape.append(selected_scene.channels)
elif dim == DimensionNames.SpatialZ:
selected_scene_shape.append(selected_scene.nz)
elif dim == DimensionNames.SpatialY:
selected_scene_shape.append(selected_scene.info["dims"].y)
elif dim == DimensionNames.SpatialX:
selected_scene_shape.append(selected_scene.info["dims"].x)
# Get sample for dtype
sample_plane = np.asarray(selected_scene.get_frame())
# Constuct the chunk and non-chunk shapes one dim at a time
# We also collect the chunk and non-chunk dimension order so that
# we can swap the dimensions after we block out the array
non_chunk_dim_order = []
non_chunk_shape = []
chunk_dim_order = []
chunk_shape = []
for dim, size in zip(selected_scene_dims, selected_scene_shape):
if dim in self.chunk_dims:
chunk_dim_order.append(dim)
chunk_shape.append(size)
else:
non_chunk_dim_order.append(dim)
non_chunk_shape.append(size)
# Fill out the rest of the blocked shape with dimension sizes of 1 to
# match the length of the sample chunk
# When dask.block happens it fills the dimensions from inner-most to
# outer-most with the chunks as long as the dimension is size 1
blocked_dim_order = non_chunk_dim_order + chunk_dim_order
blocked_shape = tuple(non_chunk_shape) + ((1,) * len(chunk_shape))
# Make ndarray for lazy arrays to fill
lazy_arrays: np.ndarray = np.ndarray(blocked_shape, dtype=object)
for np_index, _ in np.ndenumerate(lazy_arrays):
# All dimensions get their normal index except for chunk dims
# which get None, which tell the get data func to pull the whole dim
retrieve_indices = np_index[: len(non_chunk_shape)] + (
(None,) * len(chunk_shape)
)
# Fill the numpy array with the delayed arrays
lazy_arrays[np_index] = da.from_delayed(
delayed(LifReader._get_image_data)(
fs=self._fs,
path=self._path,
scene=self.current_scene_index,
retrieve_dims=blocked_dim_order,
retrieve_indices=retrieve_indices,
),
shape=chunk_shape,
dtype=sample_plane.dtype,
)
# Convert the numpy array of lazy readers into a dask array
image_data = da.block(lazy_arrays.tolist())
# Because we have set certain dimensions to be chunked and others not
# we will need to transpose back to original dimension ordering
# Example, if the original dimension ordering was "TZYX" and we
# chunked by "T", "Y", and "X"
# we created an array with dimensions ordering "ZTYX"
transpose_indices = []
for i, d in enumerate(selected_scene_dims):
new_index = blocked_dim_order.index(d)
if new_index != i:
transpose_indices.append(new_index)
else:
transpose_indices.append(i)
# Only run if the transpose is actually required
image_data = da.transpose(image_data, tuple(transpose_indices))
return image_data
@staticmethod
def _get_coords_and_physical_px_sizes(
xml: ET.Element, image_short_info: Dict[str, Any], scene_index: int
) -> Tuple[Dict[str, Any], types.PhysicalPixelSizes]:
# Create coord dict
coords: Dict[str, Any] = {}
# Get all images
img_sets = xml.findall(".//Image")
# Select the current scene
img = img_sets[scene_index]
# Construct channel list
scene_channel_list = []
channels = img.findall(".//ChannelDescription")
channel_details = img.findall(".//WideFieldChannelInfo")
for i, channel in enumerate(channels):
if len(channels) <= len(channel_details):
channel_detail = channel_details[i]
scene_channel_list.append(
(
f"{channel_detail.attrib['LUT']}"
f"--{channel_detail.attrib['ContrastingMethodName']}"
f"--{channel_detail.attrib['FluoCubeName']}"
)
)
else:
scene_channel_list.append(f"{channel.attrib['LUTName']}")
# Attach channel names to coords
coords[DimensionNames.Channel] = scene_channel_list
# Unpack short info scales
scale_x, scale_y, scale_z, scale_t = image_short_info["scale"]
# Scales from readlif are returned as px/µm
# We want to return as µm/px
scale_x = 1 / scale_x if scale_x is not None else None
scale_y = 1 / scale_y if scale_y is not None else None
scale_z = 1 / scale_z if scale_z is not None else None
# Handle Spatial Dimensions
if scale_z is not None:
coords[DimensionNames.SpatialZ] = Reader._generate_coord_array(
0, image_short_info["dims"].z, scale_z
)
if scale_y is not None:
coords[DimensionNames.SpatialY] = Reader._generate_coord_array(
0, image_short_info["dims"].y, scale_y
)
if scale_x is not None:
coords[DimensionNames.SpatialX] = Reader._generate_coord_array(
0, image_short_info["dims"].x, scale_x
)
# Time
if scale_t is not None:
coords[DimensionNames.Time] = Reader._generate_coord_array(
0, image_short_info["dims"].t, scale_t
)
# Create physical pixal sizes
px_sizes = types.PhysicalPixelSizes(scale_z, scale_y, scale_x)
return coords, px_sizes
def _read_delayed(self) -> xr.DataArray:
"""
Construct the delayed xarray DataArray object for the image.
Returns
-------
image: xr.DataArray
The fully constructed and fully delayed image as a DataArray object.
Metadata is attached in some cases as coords, dims, and attrs.
Raises
------
exceptions.UnsupportedFileFormatError
The file could not be read or is not supported.
"""
with self._fs.open(self._path) as open_resource:
lif = LifFile(open_resource)
selected_scene = lif.get_image(self.current_scene_index)
self._scene_short_info = selected_scene.info
# Check for mosaic tiles
tile_positions = self._scene_short_info["mosaic_position"]
# If there are tiles in the image use mosaic dims
if len(tile_positions) > 0:
dims = DEFAULT_DIMENSION_ORDER_LIST_WITH_MOSAIC_TILES
# Otherwise use standard dims
else:
dims = DEFAULT_DIMENSION_ORDER_LIST
# Get image data
image_data = self._create_dask_array(lif, dims)
# Get metadata
meta = lif.xml_root
# Create coordinate planes
coords, px_sizes = self._get_coords_and_physical_px_sizes(
xml=meta,
image_short_info=self._scene_short_info,
scene_index=self.current_scene_index,
)
# Store pixel sizes
self._px_sizes = px_sizes
return xr.DataArray(
image_data,
dims=dims,
coords=coords,
attrs={constants.METADATA_UNPROCESSED: meta},
)
def _read_immediate(self) -> xr.DataArray:
"""
Construct the in-memory xarray DataArray object for the image.
Returns
-------
image: xr.DataArray
The fully constructed and fully read into memory image as a DataArray
object. Metadata is attached in some cases as coords, dims, and attrs.
Raises
------
exceptions.UnsupportedFileFormatError
The file could not be read or is not supported.
"""
with self._fs.open(self._path) as open_resource:
lif = LifFile(open_resource)
selected_scene = lif.get_image(self.current_scene_index)
self._scene_short_info = selected_scene.info
# Check for mosaic tiles
tile_positions = self._scene_short_info["mosaic_position"]
# If there are tiles in the image use mosaic dims
if len(tile_positions) > 0:
dims = DEFAULT_DIMENSION_ORDER_LIST_WITH_MOSAIC_TILES
# Otherwise use standard dims
else:
dims = DEFAULT_DIMENSION_ORDER_LIST
# Get image data
image_data = self._get_image_data(
fs=self._fs,
path=self._path,
scene=self.current_scene_index,
retrieve_dims=dims,
retrieve_indices=[None] * len(dims), # Get all planes
)
# Get metadata
meta = lif.xml_root
# Create coordinate planes
coords, px_sizes = self._get_coords_and_physical_px_sizes(
xml=meta,
image_short_info=self._scene_short_info,
scene_index=self.current_scene_index,
)
# Store pixel sizes
self._px_sizes = px_sizes
return xr.DataArray(
image_data,
dims=dims,
coords=coords,
attrs={constants.METADATA_UNPROCESSED: meta},
)
def _stitch_tiles(
self,
data: types.ArrayLike,
dims: str,
mosaic_position: List[Tuple[int, int, float, float]],
) -> types.ArrayLike:
"""
This uses the mosaic_position of the LIF file to index into the data array,
retrieve the tile, transform it, and then recreate the XY plane of the tiles
before eventually combining them back together into one array (representing
the stitched mosaic image).
This stitching expects LIF files to have an extra pixel of overlap between tiles
and will shave off those pixels.
The X and Y coordinates may need to be flipped or swapped, this information is
stored in the LIF file.
Returns
-------
mosaic image: types.ArrayLike
The previously seperate tiles as one stitched image
"""
# Prefill a 2D list representing the XY plane
number_of_rows, number_of_columns = self._get_yx_tile_count()
xy_plane = np.zeros((number_of_rows, number_of_columns), dtype=object)
# Iterate over each mosaic_position coordinate using the relative
# field position (XY coordinate) given to retrieve each tile from
# the data array, transform it, and put back into a 2D (XY) array
for tile_index, tile_position, *_ in enumerate(mosaic_position):
# Get tile by getting all data for specific M
tile = transforms.reshape_data(
data,
given_dims=dims,
return_dims=dims.replace(DimensionNames.MosaicTile, ""),
M=tile_index,
)
column_index, row_index, *_ = tile_position
if self.is_x_and_y_swapped:
column_index, row_index = row_index, column_index
# LIF image stitching has a 1 pixel overlap;
# Drop the first pixel unless this is the last tile for that dimension
is_last_row = row_index + 1 >= number_of_rows
is_last_column = column_index + 1 >= number_of_columns
if not is_last_row:
tile = tile[:, :, :, 1:, :]
if not is_last_column:
tile = tile[:, :, :, :, 1:]
# LIF tiles are packed starting from bottom right so
# the origin (0, 0) needs to be the bottom right of the grid
# i.e. the end of the array hence the negative indexing
xy_plane[-(row_index + 1), -(column_index + 1)] = tile
# LIF files can have their X or Y coordinates flipped or even
# swapped, this information is stored in their metadata.
if self.is_x_flipped:
xy_plane = np.fliplr(xy_plane)
if self.is_y_flipped:
xy_plane = np.flipud(xy_plane)
# Concatenate plane into singular mosaic image
rows = [np.concatenate(row_as_tiles, axis=-1) for row_as_tiles in xy_plane]
return np.concatenate(rows, axis=-2)
def _construct_mosaic_xarray(self, data: types.ArrayLike) -> xr.DataArray:
# Get max of mosaic positions from lif
with self._fs.open(self._path) as open_resource:
lif = LifFile(open_resource)
selected_scene = lif.get_image(self.current_scene_index)
# Stitch
stitched = self._stitch_tiles(
data=data,
dims=self.dims.order,
mosaic_position=selected_scene.mosaic_position,
)
# Copy metadata
dims = [
d for d in self.xarray_dask_data.dims if d is not DimensionNames.MosaicTile
]
coords: Dict[Hashable, Any] = {
d: v
for d, v in self.xarray_dask_data.coords.items()
if d
not in [
DimensionNames.MosaicTile,
DimensionNames.SpatialY,
DimensionNames.SpatialX,
]
}
# Add expanded Y and X coords
scale_x, scale_y, _, _ = selected_scene.info["scale"]
scale_x = 1 / scale_x if scale_x is not None else None
scale_y = 1 / scale_y if scale_y is not None else None
if scale_y is not None:
coords[DimensionNames.SpatialY] = Reader._generate_coord_array(
0, stitched.shape[-2], scale_y
)
if scale_x is not None:
coords[DimensionNames.SpatialX] = Reader._generate_coord_array(
0, stitched.shape[-1], scale_x
)
attrs = copy(self.xarray_dask_data.attrs)
return xr.DataArray(
data=stitched,
dims=dims,
coords=coords,
attrs=attrs,
)
def _get_yx_tile_count(self) -> Tuple[int, int]:
"""
Get the number of tiles along the Y and X axis respectively.
Ex. Y = 3, X = 4 would mean the YX plane looks something like:
- - - -
- - - -
- - - -
while Y = 3, X = 2 would be:
_ _
_ _
_ _
Returns
-------
Y dimension length: int
The number of tiles along the Y axis.
X dimension length: int
The number of tiles along the X axis.
"""
# Determine the length of the x dimension (i.e. number of columns in XY plane)
x_dim_length = 1
for x, *_ in self._scene_short_info["mosaic_position"]:
if x + 1 > x_dim_length:
x_dim_length = x + 1
# The length of the mosaic_position array == X * Y so
# Y = (X * Y) / X
y_dim_length = floor(
len(self._scene_short_info["mosaic_position"]) / x_dim_length
)
if self.is_x_and_y_swapped:
y_dim_length, x_dim_length = x_dim_length, y_dim_length
return y_dim_length, x_dim_length
def _get_stitched_dask_mosaic(self) -> xr.DataArray:
return self._construct_mosaic_xarray(self.dask_data)
def _get_stitched_mosaic(self) -> xr.DataArray:
return self._construct_mosaic_xarray(self.data)
@property
def physical_pixel_sizes(self) -> types.PhysicalPixelSizes:
"""
Returns
-------
sizes: PhysicalPixelSizes
Using available metadata, the floats representing physical pixel sizes for
dimensions Z, Y, and X.
Notes
-----
We currently do not handle unit attachment to these values. Please see the file
metadata for unit information.
"""
if self._px_sizes is None:
# We get pixel sizes as a part of array construct
# so simply run array construct
self.dask_data
if self._px_sizes is None:
raise ValueError("Pixel sizes weren't created as a part of image reading")
return self._px_sizes
[docs] def get_mosaic_tile_position(
self, mosaic_tile_index: int, **kwargs: int
) -> Tuple[int, int]:
"""
Get the absolute position of the top left point for a single mosaic tile.
Not equivalent to readlif's notion of mosaic_position.
Parameters
----------
mosaic_tile_index: int
The index for the mosaic tile to retrieve position information for.
kwargs: int
The keywords below allow you to specify the dimensions that you wish
to match. If you under-specify the constraints you can easily
end up with a massive image stack.
Z = 1 # The Z-dimension.
C = 2 # The C-dimension ("channel").
T = 3 # The T-dimension ("time").
Returns
-------
top: int
The Y coordinate for the tile position.
left: int
The X coordinate for the tile position.
Raises
------
UnexpectedShapeError
The image has no mosaic dimension available.
IndexError
No matching mosaic tile index found.
"""
if DimensionNames.MosaicTile not in self.dims.order:
raise exceptions.UnexpectedShapeError("No mosaic dimension in image.")
if kwargs:
raise NotImplementedError(
"Selecting mosaic positions by dimensions is not supporting "
+ "by LifReader. Retrieve a specific mosaic position via the "
+ "mosaic tile index (M) by using .get_mosaic_tile_position() instead."
)
# LIFs are packed from bottom right to top left
# To counter: subtract 1 + M from list index to get from back of list
index_x, index_y, _, _ = self._scene_short_info["mosaic_position"][
-(mosaic_tile_index + 1)
]
y_dim_length, x_dim_length = self._get_yx_tile_count()
if self.is_x_and_y_swapped:
index_x, index_y = index_y, index_x
if self.is_x_flipped:
index_x = x_dim_length - index_x
if self.is_y_flipped:
index_y = y_dim_length - index_y
# Formula: (Dim position * Tile dim length) - Dim position
# where the "- Dim position" is to account for shaving a pixel off
# of each tile to account for overlap
return (
(index_y * self.dims.Y) - index_y,
(index_x * self.dims.X) - index_x,
)
[docs] def get_mosaic_tile_positions(self, **kwargs: int) -> List[Tuple[int, int]]:
"""
Get the absolute positions of the top left points for each mosaic tile
matching the specified dimensions and current scene.
Parameters
----------
kwargs: int
The keywords below allow you to specify the dimensions that you wish
to match. If you under-specify the constraints you can easily
end up with a massive image stack.
Z = 1 # The Z-dimension.
C = 2 # The C-dimension ("channel").
T = 3 # The T-dimension ("time").
Returns
-------
mosaic_tile_positions: List[Tuple[int, int]]
List of the Y and X coordinate for the tile positions.
Raises
------
UnexpectedShapeError
The image has no mosaic dimension available.
NotImplementedError
This reader does not support indexing tiles by dimensions other than M
"""
if DimensionNames.MosaicTile not in self.dims.order:
raise exceptions.UnexpectedShapeError("No mosaic dimension in image.")
if kwargs:
raise NotImplementedError(
"Selecting mosaic positions by dimensions is not supporting "
+ "by LifReader. Retrieve a specific mosaic position via the "
+ "mosaic tile index (M) by using .get_mosaic_tile_position() instead."
)
mosaic_positions: List[Tuple[int, int, float, float]] = self._scene_short_info[
"mosaic_position"
]
# LIFs are packed from bottom right to top left
# To counter: read the positions in reverse
adjusted_mosaic_positions: List[Tuple[int, int]] = []
y_dim_length, x_dim_length = self._get_yx_tile_count()
for x, y, *_ in reversed(mosaic_positions):
if self.is_x_and_y_swapped:
x, y = y, x
if self.is_x_flipped:
x = x_dim_length - x
if self.is_y_flipped:
y = y_dim_length - y
# Formula: (Dim position * Tile dim length) - Dim position
# where the "- Dim position" is to account for shaving a pixel off
# of each tile to account for overlap
adjusted_mosaic_positions.append(
(
(y * self.dims.Y) - y,
(x * self.dims.X) - x,
)
)
return adjusted_mosaic_positions