diff --git a/README.md b/README.md
index ae9d2b644..078b802e1 100644
--- a/README.md
+++ b/README.md
@@ -19,10 +19,6 @@ A Python library for reading and writing image data with specific support for ha
* `TIFF`
* Any additional format supported by `imageio`
-### Disclaimer:
-This package is under heavy revision in preparation for version 3.0.0 release. The quick start below is representative
-of how to interact with the package under 3.0.0 and not under the current stable release.
-
## Quick Start
```python
from aicsimageio import AICSImage, imread
@@ -58,7 +54,6 @@ im.metadata # returns whichever metadata parser best suits the file format
# Subsets or transposes of the image data can be requested:
im.get_image_data(out_orientation="ZYX") # returns a 3d data block containing only the ZYX dimensions
-
```
## Notes
@@ -66,6 +61,31 @@ im.get_image_data(out_orientation="ZYX") # returns a 3d data block containing o
or `Scene`, `Time`, `Channel`, `Z`, `Y`, and `X`.
* Each file format may use a different metadata parser it is dependent on the reader's implementation.
+## Experimental
+We include an experimental series reader which can force multiple images to act like a single `numpy.ndarray`.
+Image dimension consistency is checked during reading of any image required to complete a slice operation.
+All images must have consistent shape and whichever dimension is chosen to be used as the series dimension must be of
+size one (1) for each image in the series.
+
+```python
+from aicsimageio import AICSSeries
+
+# Combine a series of images and make them act like a single numpy ndarray
+series = AICSSeries(
+ [
+ "img1.tiff",
+ "img2.tiff",
+ "img3.tiff",
+ "img4.tiff"
+ ],
+ series_dim="T"
+)
+
+# Get data out
+series[0, :, :, :, :, :] # returns a 5D ndarray of TCZYX
+series[:, 3, 3, :, :, :] # returns a 4D ndarray of SZYX
+```
+
## Installation
**Stable Release:** `pip install aicsimageio`
**Development Head:** `pip install git+https://github.com/AllenCellModeling/aicsimageio.git`
diff --git a/aicsimageio/__init__.py b/aicsimageio/__init__.py
index 9f1672fe0..052fbaa24 100644
--- a/aicsimageio/__init__.py
+++ b/aicsimageio/__init__.py
@@ -1,5 +1,6 @@
from .aics_image import AICSImage # noqa: F401
from .aics_image import imread # noqa: F401
+from .aics_series import AICSSeries # noqa: F401
# Do not edit this string manually, always use bumpversion
# Details in CONTRIBUTING.md
diff --git a/aicsimageio/aics_image.py b/aicsimageio/aics_image.py
index 8d5899b8a..927315a7b 100644
--- a/aicsimageio/aics_image.py
+++ b/aicsimageio/aics_image.py
@@ -1,16 +1,19 @@
import logging
-import typing
-from typing import Optional, Type
+from typing import Optional, Type, Union
import numpy as np
from . import constants, transforms, types
-from .exceptions import InvalidDimensionOrderingError, UnsupportedFileFormatError
-from .readers import CziReader, DefaultReader, NdArrayReader, OmeTiffReader, TiffReader
+from .exceptions import (InvalidDimensionOrderingError,
+ UnsupportedFileFormatError)
+from .readers import (CziReader, DefaultReader, NdArrayReader, OmeTiffReader,
+ TiffReader)
from .readers.reader import Reader
log = logging.getLogger(__name__)
+###############################################################################
+
class AICSImage:
"""
@@ -85,7 +88,7 @@ class AICSImage:
def __init__(
self,
- data: typing.Union[types.FileLike, np.ndarray],
+ data: Union[types.FileLike, np.ndarray],
known_dims: Optional[str] = None,
**kwargs,
):
@@ -155,7 +158,7 @@ def data(self):
)
return self._data
- def size(self, dims: str = "STCZYX"):
+ def size(self, dims: str = constants.DEFAULT_DIMENSION_ORDER):
"""
Parameters
----------
@@ -166,7 +169,7 @@ def size(self, dims: str = "STCZYX"):
Returns a tuple with the requested dimensions filled in
"""
dims = dims.upper()
- if not (all(d in "STCZYX" for d in dims)):
+ if not (all(d in constants.DEFAULT_DIMENSION_ORDER for d in dims)):
raise InvalidDimensionOrderingError(f"Invalid dimensions requested: {dims}")
if not (all(d in self.dims for d in dims)):
raise InvalidDimensionOrderingError(f"Invalid dimensions requested: {dims}")
diff --git a/aicsimageio/aics_series.py b/aicsimageio/aics_series.py
new file mode 100644
index 000000000..e2cd3be1a
--- /dev/null
+++ b/aicsimageio/aics_series.py
@@ -0,0 +1,388 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import logging
+from pathlib import Path
+from typing import Iterable, List, Optional, Tuple, Union
+
+import numpy as np
+
+from . import AICSImage, constants, exceptions, types
+
+log = logging.getLogger(__name__)
+
+###############################################################################
+
+
+class AICSSeries:
+ """
+ AICSSeries takes an ordered iterable of image data types (files / bytestreams) of consistent dimensions and forces
+ them to act like they are a single image. The data for each image in the series is lazy loaded as are the images
+ themselves. This is beneficial when the images stacked together may be too large to fit in memory.
+
+ Examples
+ --------
+ series = AICSSeries(["img1.tiff", "img2.tiff"], "T")
+ series[0, :, :, 3, :, :] # returns a 4D array of TCYX
+
+ series = AICSImage("directory_full_of_tiffs/", "Z")
+ series[0, 0, 0, 3, :, :] # returns a 2D array of YX
+
+ Notes
+ -----
+ * Because each image is lazy loaded, we validate on every image read. If the image has different dimensions or
+ sizes from the prior read image, an InvalidDimensionOrderingError or InconsitentDataShapeException is raised.
+ * This class may be a temporary stop gap until chunked image reading is added to the core `AICSImage` class and if
+ and when that occurs, this class will be deprecated.
+ * No metadata is checked for ensuring that the data in each image is consistent across them all. Example being,
+ channels in different indicies in different files across the series.
+ """
+
+ def __init__(
+ self,
+ images: Union[types.PathLike, Iterable[types.PathLike]],
+ series_dim: str
+ ):
+ """
+ Constructor for AICSSeries class intended for providing an interface for image reading of a list of path like
+ references.
+
+ Parameters
+ ----------
+ images: Union[types.PathLike, Iterable[types.PathLike]]
+ Either a pathlike value to a directory with images or the ordered iterable set of pathlike objects to read
+ from for the series.
+ series_dim: str
+ Which dimension to override with the iterable images.
+ """
+ # Check if provided a directory, if so, get the directory contents
+ if isinstance(images, (str, Path)):
+ # Expand and handle path
+ images = Path(images).expanduser().resolve(strict=True)
+
+ # Ensure that it is actually a directory and not a single file
+ if not images.is_dir():
+ raise NotADirectoryError(
+ f"Provided a single file: {images}. Did you mean to use `AICSImage`?"
+ )
+
+ # Set images to the contents of the directory
+ images = sorted(images.iterdir())
+
+ # Drop any files that start with `.`
+ # TODO: Is this actually a valid operation to do for the user or way to specific?
+ # TODO: Different option, find the most common suffix and use it as the base?
+ predrop_count = len(images)
+ images = [img for img in images if img.name[0] != "."]
+ log.debug(f"Dropped {predrop_count - len(images)} files that began with a `.`.")
+
+ # At this point, images should have either been converted to a list of files if provided a directory
+ # Or was given an iterable of file like
+ if not isinstance(images, Iterable):
+ raise TypeError(
+ f"AICSSeries requires either a path to a directory or an iterable of files."
+ )
+
+ # Check that the iterable is more than one image
+ if len(images) <= 1:
+ raise ValueError(f"Provided a single file: {images}. Did you mean to use `AICSImage`?")
+
+ # Now check that everything in the iterable is file like
+ # This will also raise an error if any of the file like paths was not found
+ images = [Path(img).expanduser().resolve(strict=True) for img in images]
+
+ # Ensure that the series dim provided is part of the normal set and that only one dim is provided
+ series_dim = series_dim.upper()
+ if (
+ not isinstance(series_dim, str)
+ or series_dim not in constants.DEFAULT_DIMENSION_ORDER
+ or len(series_dim) != 1
+ ):
+ raise ValueError(
+ f"The series dimension must be a single character from the "
+ f"standard {constants.DEFAULT_DIMENSION_ORDER}. Received: '{series_dim}'."
+ )
+
+ # All easy checks are now complete, store the image list and the series dim
+ self._images = images
+ self._series_dim = series_dim
+
+ # Set the initial state of the series for future validation
+ # _shape is for ndarray shape consistency
+ # _size is for fast access after read
+ self._shape = None
+ self._size = None
+
+ def validate_series(self):
+ """
+ Runs the validation conditions against every image provided to the series.
+
+ Raises an InvalidDimensionOrderingError or InconsitentDataShapeException if any images fail validation.
+ """
+ for img_path in self.images:
+ with AICSImage(img_path) as img:
+ # Set self._shape if not already set
+ if self._shape is None:
+ self._shape = img.size()
+
+ # Ensure data consistency
+ self._ensure_valid_data_shape(img.size(), self.operating_index, self._shape)
+
+ # Clean up memory
+ del img
+
+ @property
+ def series_dim(self) -> str:
+ """
+ Returns
+ -------
+ series_dim: str
+ Which dimension is supposed to act like a normal array axis but is actually reading a series of images.
+ """
+ return self._series_dim
+
+ @property
+ def operating_index(self) -> int:
+ """
+ Returns
+ -------
+ operating_index: int
+ The dimension index of the series dimension.
+ """
+ return constants.DEFAULT_DIMENSION_ORDER.index(self.series_dim)
+
+ @property
+ def images(self) -> List[Path]:
+ """
+ Returns
+ -------
+ images: List[Path]:
+ The list of filepaths to images to be used for the series.
+ """
+ return self._images
+
+ @staticmethod
+ def _ensure_valid_data_shape(
+ data_shape: Tuple[int],
+ operating_index: int,
+ prior_shape: Optional[Tuple[int]] = None
+ ):
+ """
+ Used to ensure that the data shape is `1` in whichever index is the operating index for the series.
+ Additionally, if provided a prior shape, will check for shape consistenting between the pair.
+
+ Raises an InvalidDimensionOrderingError or InconsitentDataShapeException.
+
+ Parameters
+ ----------
+ data_shape: Tuple[int]
+ A tuple of six dimension sizes produced by `AICSImage.size()` for an image in the series.
+ operating_index: int
+ The operating index for the series. This is constants.DEFAULT_DIMENSION_ORDER.index(self.series_dim).
+ prior_shape: Optional[Tuple[int]] = None
+ An optional prior shape to ensure that the provided data shape is consistent with the prior.
+ """
+ if data_shape[operating_index] != 1:
+ raise exceptions.InvalidDimensionOrderingError(
+ f"The read data shape is invalid for the current operating series dimension. Read shape: {data_shape}."
+ )
+
+ if prior_shape:
+ if data_shape != prior_shape:
+ raise exceptions.InconsitentDataShapeException(
+ f"The read data shape is inconsitent with the prior data shape. "
+ f"Read shape: {data_shape}, prior shape: {prior_shape}"
+ )
+
+ def size(self, dims: str = constants.DEFAULT_DIMENSION_ORDER) -> Tuple[int]:
+ """
+ Parameters
+ ----------
+ dims: str
+ A string containing a list of dimensions being requested. The default is to return the six standard dims.
+
+ Returns
+ -------
+ dims: Tuple[int]
+ Returns a tuple with the requested dimensions filled in
+ """
+ # Only run if shape has never been retrieved before
+ if self._size is None:
+ with AICSImage(self.images[0]) as img:
+ # Get size
+ shape = img.size()
+
+ # Check data shape
+ self._ensure_valid_data_shape(shape, self.operating_index)
+
+ # Store shape for future checks
+ self._shape = shape
+ log.debug(f"Will hold all images to shape of: {self._shape}")
+
+ # Replace the retrieved size at operating index with the length of the series
+ size = []
+ for i, val in enumerate(shape):
+ if i == self.operating_index:
+ size.append(len(self.images))
+ else:
+ size.append(val)
+
+ # Set shape state
+ self._size = tuple(size)
+
+ return tuple([self._size[constants.DEFAULT_DIMENSION_ORDER.index(c)] for c in dims])
+
+ @property
+ def size_x(self) -> int:
+ """
+ Returns
+ -------
+ size_x: int
+ The size of the x dimension.
+ """
+ return self.size("X")[0]
+
+ @property
+ def size_y(self) -> int:
+ """
+ Returns
+ -------
+ size_y: int
+ The size of the x dimension.
+ """
+ return self.size("Y")[0]
+
+ @property
+ def size_z(self) -> int:
+ """
+ Returns
+ -------
+ size_z: int
+ The size of the x dimension.
+ """
+ return self.size("Z")[0]
+
+ @property
+ def size_c(self) -> int:
+ """
+ Returns
+ -------
+ size_c: int
+ The size of the x dimension.
+ """
+ return self.size("C")[0]
+
+ @property
+ def size_t(self) -> int:
+ """
+ Returns
+ -------
+ size_t: int
+ The size of the x dimension.
+ """
+ return self.size("T")[0]
+
+ @property
+ def size_s(self) -> int:
+ """
+ Returns
+ -------
+ size_s: int
+ The size of the x dimension.
+ """
+ return self.size("S")[0]
+
+ def __getitem__(self, selections: Tuple[Union[slice, int]]) -> np.ndarray:
+ """
+ Apply slice operations to the image series like a normal numpy.ndarray.
+ """
+ # Easy check to make sure that length of selections is at most the length of dims (6)
+ if len(selections) > len(constants.DEFAULT_DIMENSION_ORDER):
+ raise IndexError(f"More operations provided than dimensions available.")
+
+ # To maintain consistent behavior with numpy ndarray slicing behavior, if there are less operations than dims
+ # pad the selections with slice(None, None, None)
+ # Ex: series[0, 1, ] should pad to series[0, 1, :, :, :, :]
+ if len(selections) < len(constants.DEFAULT_DIMENSION_ORDER):
+ formatted_selections = [op for op in selections]
+ while len(formatted_selections) < len(constants.DEFAULT_DIMENSION_ORDER):
+ formatted_selections.append(slice(None, None, None))
+
+ # Cast to tuple and save as selections
+ selections = tuple(formatted_selections)
+
+ # Final check that every operation is either an integer or a slice
+ for op in selections:
+ if not isinstance(op, (int, slice)):
+ raise TypeError(
+ f"Operations on __getitem__ must be a single value or a slice to get from the data. Received: {op}."
+ )
+
+ # Get the operations required for the operating index
+ to_read = self.images[selections[self.operating_index]]
+
+ # Always convert to a list
+ if isinstance(to_read, Path):
+ to_read = [to_read]
+
+ # Get the other operations required on each image by selection all operations except the one occuring on the
+ # operating index, additionally, keep track of what we expect the dims to be out of these operations
+ ops = []
+ expected_dims = []
+ for i, op in enumerate(selections):
+ if i != self.operating_index:
+ ops.append(op)
+
+ if isinstance(op, slice):
+ expected_dims.append(constants.DEFAULT_DIMENSION_ORDER[i])
+
+ # Convert to Tuple
+ ops = tuple(ops)
+
+ # Read and apply operations across series
+ read_data = []
+ for img_to_read in to_read:
+ log.debug(f"Reading {img_to_read}...")
+ with AICSImage(img_to_read) as img:
+ # Set self._shape if not already set
+ if self._shape is None:
+ self._shape = img.size()
+
+ # Ensure data consistency
+ self._ensure_valid_data_shape(img.size(), self.operating_index, self._shape)
+
+ # Read and append
+ read_data.append(
+ # This will get us the other five dimensions
+ img.get_image_data(
+ constants.DEFAULT_DIMENSION_ORDER.replace(self.series_dim, ""),
+ copy=True
+ )[ops]
+ )
+
+ # Clean up after reading to save on memory
+ # TODO: Fix aicsimageio to do this clean up for us? I am not sure why it is being held onto
+ del img
+
+ # Stack data on series dim axis if expected and return
+ if self.series_dim in expected_dims:
+ data = np.stack(read_data, axis=expected_dims.index(self.series_dim))
+
+ # Otherwise we know it is just a single image because operating axis didn't matter so pull that image data out
+ else:
+ data = read_data[0]
+
+ # Clean up read data to save on memory
+ del read_data
+
+ return data
+
+ def __str__(self) -> str:
+ return (
+ f""
+ )
+
+ def __repr__(self) -> str:
+ return str(self)
diff --git a/aicsimageio/exceptions.py b/aicsimageio/exceptions.py
index 379af1fd3..45b93cf62 100644
--- a/aicsimageio/exceptions.py
+++ b/aicsimageio/exceptions.py
@@ -42,3 +42,16 @@ class ConflictingArgumentsError(Exception):
This exception is returned when 2 arguments to the same function are in conflict.
"""
pass
+
+
+class InconsitentDataShapeException(Exception):
+ """
+ Intended to be thrown when data shapes between multiple arrays aren't equal.
+ """
+
+ def __init__(self, message: str, **kwargs):
+ super().__init__(**kwargs)
+ self.message = message
+
+ def __str__(self):
+ return self.message
diff --git a/aicsimageio/tests/resources/series_data_invalid/example.png b/aicsimageio/tests/resources/series_data_invalid/example.png
new file mode 100644
index 000000000..04e55cbb5
Binary files /dev/null and b/aicsimageio/tests/resources/series_data_invalid/example.png differ
diff --git a/aicsimageio/tests/resources/series_data_invalid/s_1_t_1_c_10_z_1.ome.tiff b/aicsimageio/tests/resources/series_data_invalid/s_1_t_1_c_10_z_1.ome.tiff
new file mode 100644
index 000000000..f7a36c40d
Binary files /dev/null and b/aicsimageio/tests/resources/series_data_invalid/s_1_t_1_c_10_z_1.ome.tiff differ
diff --git a/aicsimageio/tests/resources/series_data_invalid/s_1_t_1_c_1_z_1.ome.tiff b/aicsimageio/tests/resources/series_data_invalid/s_1_t_1_c_1_z_1.ome.tiff
new file mode 100644
index 000000000..5db33ed9f
Binary files /dev/null and b/aicsimageio/tests/resources/series_data_invalid/s_1_t_1_c_1_z_1.ome.tiff differ
diff --git a/aicsimageio/tests/resources/series_data_valid/s_1_t_1_c_1_z_1_(0).ome.tiff b/aicsimageio/tests/resources/series_data_valid/s_1_t_1_c_1_z_1_(0).ome.tiff
new file mode 100644
index 000000000..5db33ed9f
Binary files /dev/null and b/aicsimageio/tests/resources/series_data_valid/s_1_t_1_c_1_z_1_(0).ome.tiff differ
diff --git a/aicsimageio/tests/resources/series_data_valid/s_1_t_1_c_1_z_1_(1).ome.tiff b/aicsimageio/tests/resources/series_data_valid/s_1_t_1_c_1_z_1_(1).ome.tiff
new file mode 100644
index 000000000..5db33ed9f
Binary files /dev/null and b/aicsimageio/tests/resources/series_data_valid/s_1_t_1_c_1_z_1_(1).ome.tiff differ
diff --git a/aicsimageio/tests/resources/series_data_valid/s_1_t_1_c_1_z_1_(2).ome.tiff b/aicsimageio/tests/resources/series_data_valid/s_1_t_1_c_1_z_1_(2).ome.tiff
new file mode 100644
index 000000000..5db33ed9f
Binary files /dev/null and b/aicsimageio/tests/resources/series_data_valid/s_1_t_1_c_1_z_1_(2).ome.tiff differ
diff --git a/aicsimageio/tests/test_aics_series.py b/aicsimageio/tests/test_aics_series.py
new file mode 100644
index 000000000..99295ae4b
--- /dev/null
+++ b/aicsimageio/tests/test_aics_series.py
@@ -0,0 +1,349 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from pathlib import Path
+
+import numpy as np
+import pytest
+
+from aicsimageio import AICSImage, AICSSeries, constants, exceptions
+
+###############################################################################
+
+DATA_DIR = Path(__file__).parent / "resources"
+
+###############################################################################
+
+
+@pytest.mark.parametrize("images, series_dim, expected_images", [
+ (
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"],
+ "T",
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"]
+ ),
+ (
+ DATA_DIR / "series_data_valid",
+ "T",
+ [
+ DATA_DIR / "series_data_valid" / "s_1_t_1_c_1_z_1_(0).ome.tiff",
+ DATA_DIR / "series_data_valid" / "s_1_t_1_c_1_z_1_(1).ome.tiff",
+ DATA_DIR / "series_data_valid" / "s_1_t_1_c_1_z_1_(2).ome.tiff",
+ ]
+ ),
+ # Doesn't fail because no checks are done on init
+ (
+ DATA_DIR / "series_data_invalid",
+ "T",
+ [
+ DATA_DIR / "series_data_invalid" / "example.png",
+ DATA_DIR / "series_data_invalid" / "s_1_t_1_c_10_z_1.ome.tiff",
+ DATA_DIR / "series_data_invalid" / "s_1_t_1_c_1_z_1.ome.tiff",
+ ]
+ ),
+ pytest.param(
+ DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff",
+ None,
+ None,
+ marks=pytest.mark.raises(exception=NotADirectoryError)
+ ),
+ pytest.param(1, None, None, marks=pytest.mark.raises(exception=TypeError)),
+ pytest.param([DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"], None, None, marks=pytest.mark.raises(exception=ValueError)),
+ pytest.param(DATA_DIR / "series_data_valid", 1, None, marks=pytest.mark.raises(exception=AttributeError)),
+ pytest.param(DATA_DIR / "series_data_valid", "B", None, marks=pytest.mark.raises(exception=ValueError)),
+ pytest.param(DATA_DIR / "series_data_valid", "ABC", None, marks=pytest.mark.raises(exception=ValueError))
+])
+def test_aics_series_init(images, series_dim, expected_images):
+ series = AICSSeries(images, series_dim)
+ for i, img_path in enumerate(series.images):
+ assert img_path == expected_images[i]
+
+
+@pytest.mark.parametrize("data_shape, operating_index, prior_shape", [
+ ((2, 1, 4, 60, 480, 480), 1, None),
+ ((2, 2, 2, 1, 100, 100), 3, None),
+ pytest.param((2, 2, 2), 0, None, marks=pytest.mark.raises(exceptions=exceptions.InvalidDimensionOrderingError)),
+ ((2, 1, 4, 60, 480, 480), 1, (2, 1, 4, 60, 480, 480)),
+ pytest.param(
+ (2, 1, 2, 1, 100, 100),
+ 1,
+ (1, 1, 3),
+ marks=pytest.mark.raises(exceptions=exceptions.InconsitentDataShapeException)
+ )
+])
+def test_valid_data_shape(data_shape, operating_index, prior_shape):
+ AICSSeries._ensure_valid_data_shape(data_shape, operating_index, prior_shape)
+
+
+@pytest.mark.parametrize("images, series_dim, dims, expected_size", [
+ (
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"],
+ "T",
+ constants.DEFAULT_DIMENSION_ORDER,
+ (1, 2, 1, 1, 325, 475)
+ ),
+ (
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"],
+ "T",
+ "CZYX",
+ (1, 1, 325, 475)
+ ),
+ pytest.param(
+ [DATA_DIR / "s_3_t_1_c_3_z_5.ome.tiff", DATA_DIR / "s_3_t_1_c_3_z_5.ome.tiff"],
+ "Z",
+ None,
+ None,
+ marks=pytest.mark.raises(exception=exceptions.InvalidDimensionOrderingError)
+ )
+])
+def test_size(images, series_dim, dims, expected_size):
+ series = AICSSeries(images, series_dim)
+ assert series.size(dims) == expected_size
+
+
+@pytest.mark.parametrize("images, series_dim, expected_sizes", [
+ (
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"],
+ "T",
+ (1, 2, 1, 1, 325, 475)
+ ),
+ (
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"],
+ "S",
+ (2, 1, 1, 1, 325, 475)
+ )
+])
+def test_single_dim_size_properties(images, series_dim, expected_sizes):
+ series = AICSSeries(images, series_dim)
+ assert series.size_s == expected_sizes[0]
+ assert series.size_t == expected_sizes[1]
+ assert series.size_c == expected_sizes[2]
+ assert series.size_z == expected_sizes[3]
+ assert series.size_y == expected_sizes[4]
+ assert series.size_x == expected_sizes[5]
+
+
+@pytest.mark.parametrize("images, series_dim", [
+ ([DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"], "T"),
+ (DATA_DIR / "series_data_valid", "T"),
+ # Fails because different size C dimensions
+ pytest.param(
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_10_z_1.ome.tiff"],
+ "C",
+ marks=pytest.mark.raises(exception=exceptions.InvalidDimensionOrderingError)
+ ),
+ # Fails because different size C dimensions
+ pytest.param(
+ DATA_DIR / "series_data_invalid",
+ "C",
+ marks=pytest.mark.raises(exception=exceptions.InvalidDimensionOrderingError)
+ ),
+ # Fails because different image shapes
+ pytest.param(
+ DATA_DIR / "series_data_invalid",
+ "T",
+ marks=pytest.mark.raises(exception=exceptions.InconsitentDataShapeException)
+ )
+])
+def test_validate_series(images, series_dim):
+ series = AICSSeries(images, series_dim)
+ series.validate_series()
+
+
+@pytest.mark.parametrize("images, series_dim, selections, expected_shape", [
+ (
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"],
+ "T",
+ (0, 0, 0, 0, slice(None, None, None), slice(None, None, None)),
+ (325, 475)
+ ),
+ (
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"],
+ "T",
+ (0, slice(None, None, None), 0, 0, slice(None, None, None), slice(None, None, None)),
+ (2, 325, 475)
+ ),
+ (
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"],
+ "T",
+ (0,),
+ (2, 1, 1, 325, 475)
+ ),
+ (
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"],
+ "T",
+ (0, 1, 0, ),
+ (1, 325, 475)
+ ),
+ (
+ [DATA_DIR / "s_1_t_1_c_10_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_10_z_1.ome.tiff"],
+ "T",
+ (0, 1, ),
+ (10, 1, 1736, 1776,)
+ ),
+ (
+ DATA_DIR / "series_data_valid",
+ "T",
+ (0, slice(None, None, None), 0, 0, ),
+ (3, 325, 475)
+ ),
+ # Fails because images found in directory have mixed shapes
+ pytest.param(
+ DATA_DIR / "series_data_invalid",
+ "T",
+ (0, slice(None, None, None), 0, 0, ),
+ None,
+ marks=pytest.mark.raises(exception=exceptions.InconsitentDataShapeException)
+ ),
+ # Fails because too many operations provided
+ pytest.param(
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"],
+ "T",
+ (0, 1, 0, 0, 0, 0, 0, 0),
+ None,
+ marks=pytest.mark.raises(exception=IndexError)
+ ),
+ # Fails because "hello" isn't a valid slice operation
+ pytest.param(
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"],
+ "T",
+ (0, 1, 0, "hello", ),
+ None,
+ marks=pytest.mark.raises(exception=TypeError)
+ ),
+ # Fails because requested index is out of range (for list of images)
+ pytest.param(
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"],
+ "T",
+ (0, 10, ),
+ None,
+ marks=pytest.mark.raises(exception=IndexError)
+ ),
+ # Fails because requested index is out of range (for actual data array)
+ pytest.param(
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff"],
+ "C",
+ (0, 10, ),
+ None,
+ marks=pytest.mark.raises(exception=IndexError)
+ ),
+ # Fails because different overall shape changes
+ pytest.param(
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_10_z_1.ome.tiff"],
+ "T",
+ (0, ),
+ None,
+ marks=pytest.mark.raises(exception=exceptions.InconsitentDataShapeException)
+ ),
+ # Fails because size of c changes
+ pytest.param(
+ [DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff", DATA_DIR / "s_1_t_1_c_10_z_1.ome.tiff"],
+ "C",
+ (0, ),
+ None,
+ marks=pytest.mark.raises(exception=exceptions.InvalidDimensionOrderingError)
+ )
+])
+def test_getitem_fast_checks(images, series_dim, selections, expected_shape):
+ series = AICSSeries(images, series_dim)
+ assert series[selections].shape == expected_shape
+
+
+@pytest.mark.parametrize("image, selections", [
+ (
+ DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff",
+ (
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None)
+ )
+ ),
+ (
+ DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff",
+ (
+ 0,
+ 0,
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None)
+ )
+ ),
+ (
+ DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff",
+ (
+ 0,
+ slice(None, None, None),
+ 0,
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None)
+ )
+ ),
+ (
+ DATA_DIR / "s_1_t_1_c_1_z_1.ome.tiff",
+ (
+ slice(None, None, None),
+ slice(None, None, None),
+ 0,
+ 0,
+ slice(None, None, None),
+ slice(None, None, None)
+ )
+ ),
+ (
+ DATA_DIR / "s_1_t_1_c_10_z_1.ome.tiff",
+ (
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None)
+ )
+ ),
+ (
+ DATA_DIR / "s_1_t_1_c_10_z_1.ome.tiff",
+ (
+ 0,
+ 0,
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None)
+ )
+ ),
+ (
+ DATA_DIR / "s_1_t_1_c_10_z_1.ome.tiff",
+ (
+ 0,
+ slice(None, None, None),
+ 0,
+ slice(None, None, None),
+ slice(None, None, None),
+ slice(None, None, None)
+ )
+ ),
+ (
+ DATA_DIR / "s_1_t_1_c_10_z_1.ome.tiff",
+ (
+ slice(None, None, None),
+ slice(None, None, None),
+ 0,
+ 0,
+ slice(None, None, None),
+ slice(None, None, None)
+ )
+ ),
+])
+def test_getitem_deep_equal(image, selections):
+ with AICSImage(image) as img:
+ data = img.get_image_data("SCZYX")
+
+ stacked = np.stack([data, data], axis=1)
+ expected = stacked[selections]
+
+ series = AICSSeries([image, image], "T")
+ assert np.array_equal(series[selections], expected)