Module iaf.io.readers

Custom image readers.

Classes

class HuygensHDF5Reader (filename: Union[pathlib.Path, str] = None)

Huygens HDF5 Reader.

from iaf.io.readers import HuygensHDF5Reader

# Initialize the reader
reader = HuygensHDF5Reader("file.h5")

# To load a specific (timepoint, channel) image or stack:
stack = reader.load(timepoint=0, channel=0)

Stacks are lazily loaded only on access.

The reader exposes metadata information via properties:

Property Explanation
reader.channel_names Tuple of names for each of the acquisition channels.
reader.filename Full file name of the opened file.
reader.num_channels Number of channels.
reader.num_planes Number of planes (z levels).
reader.num_timepoints Number of time points.
reader.voxel_sizes Voxel sizes in units (nm) (x, y, z).
reader.metadata Dictionary of microscopy parameters.
reader.hdf5_dataset HDF5 dataset for advanced use.

Constructor.

Parameters

filename : Union[Path, str]
Full path to the file to open
Expand source code
class HuygensHDF5Reader:
    """Huygens HDF5 Reader.

    ```
    from iaf.io.readers import HuygensHDF5Reader

    # Initialize the reader
    reader = HuygensHDF5Reader("file.h5")

    # To load a specific (timepoint, channel) image or stack:
    stack = reader.load(timepoint=0, channel=0)
    ```

    Stacks are lazily loaded only on access.

    The reader exposes metadata information via properties:

    | Property                | Explanation                                                   |
    | ----------------------- | ------------------------------------------------------------- |
    | `reader.channel_names`  | Tuple of names for each of the acquisition channels.          |
    | `reader.filename`       | Full file name of the opened file.                            |
    | `reader.num_channels`   | Number of channels.                                           |
    | `reader.num_planes`     | Number of planes (z levels).                                  |
    | `reader.num_timepoints` | Number of time points.                                        |
    | `reader.voxel_sizes`    | Voxel sizes in units (nm) `(x, y, z)`.                        |
    | `reader.metadata`       | Dictionary of microscopy parameters.                          |
    | `reader.hdf5_dataset`   | HDF5 dataset for advanced use.                                |
    """

    def __init__(self, filename: Union[Path, str] = None):
        """Constructor.

        Parameters
        ----------

        filename: Union[Path, str]
            Full path to the file to open
        """

        self._file = None

        # Make sure a filename was passed
        if filename is None:
            raise ValueError("Please specify a file name!")

        self._filename: Path = Path(filename).absolute()
        if not self._filename.exists():
            raise IOError(f"File {filename} does not exist.")

        if not self._filename.suffix.lower() == ".h5":
            raise ValueError(f"The _file {self._filename} is not an .h5 file.")

        # Initialize _geometry
        self._num_timepoints: int = 1
        self._num_channels: int = 1
        self._num_planes: int = 1
        self._sizeX: int = 0
        self._sizeY: int = 0
        self._sizeZ: int = 0
        self._channels_names: tuple = ()

        # Metadata dictionary
        self._metadata = {}

        # Try opening file
        self._file = h5py.File(self._filename, "r")

        # Parse the geometry of the file and sets the appropriate bundle_ and iter_axis
        self._parse_geometry()

        # Parse metadata
        self._parse_metadata()

        # Parse voxel sizes
        self._voxel_sizes = (0.0, 0.0, 0.0)
        self._parse_voxel_sizes()

        # Parse channel names
        self._parse_channel_names()

    def _parse_geometry(self):
        """
        Parse geometry of IMS file and set properties.
        """

        if self._file is None:
            return

        # Get dataset
        image_dataset = self._file.get(list(self._file.keys())[0])["ImageData"]["Image"]
        dimensions = image_dataset.shape

        self._num_channels = dimensions[0]
        self._num_timepoints = dimensions[1]
        self._num_planes = dimensions[2]
        self._sizeX = dimensions[4]
        self._sizeY = dimensions[3]
        self._sizeZ = dimensions[2]

    def _parse_voxel_sizes(self):
        """
        Parse metadata of H5 file and extracts pixel size.
        """

        if self._file is None:
            return

        # Get dataset
        image_data = self._file.get(list(self._file.keys())[0])["ImageData"]

        # Store the pixel size
        self._voxel_sizes = (
            float(image_data["DimensionScaleX"][()]),
            float(image_data["DimensionScaleY"][()]),
            float(image_data["DimensionScaleZ"][()]),
        )

    def _parse_channel_names(self):
        """
        Parse the channel names.
        """

        if self._file is None:
            return

        # Get physical data group
        physical_data_group = self._file.get(list(self._file.keys())[0])["PhysicalData"]

        # Extract channel descriptions
        channel_names_list = []
        for c in range(len(physical_data_group["ChannelDescription"])):
            name = str(physical_data_group["ChannelDescription"][c], encoding="utf-8")
            channel_names_list.append(name)
        self._channels_names = tuple(channel_names_list)

    def _parse_metadata(self):
        """Extract the metadata."""

        if self._file is None:
            return

        # Get physical data group
        physical_data_group = self._file.get(list(self._file.keys())[0])["PhysicalData"]

        # Clear the dictionary
        self._metadata.clear()
        for key in physical_data_group.keys():
            if key.lower().endswith("enumeration"):
                continue
            # Initialize data -- it will be overwritten
            data = ""
            try:
                data = physical_data_group[key][()]
                if type(data) is bytes:
                    data = data.decode("utf-8")
                elif type(data) is np.ndarray:
                    if data.dtype == object:
                        data = data.astype(str)
                    else:
                        data = tuple(data.tolist())
                else:
                    print(f"Unsupported entry {key}: ({type(data)})")
                    continue
            except Exception:
                print(f"{key}: could not extract data!")

            # Store the processed entry
            self._metadata[key] = data

    def load(self, timepoint: int = 0, channel: int = 0):
        """Load specific timepoint and channel.

        Parameters
        ----------

        timepoint: int
            Index of the timepoint to load, 0-based. (Optional, default 0).

        channel: int
            Index of the channel to load, 0-based. (Optional, default 0).

        Returns
        -------

        dataset: Union[None, DataSet, np.ndarray]
            Dataset at the requested `timepoint` and `channel`.
        """

        if self._file is None:
            return

        # Get dataset
        image_dataset = self._file.get(list(self._file.keys())[0])["ImageData"]["Image"]

        # Check that the requested timepoint and channel are compatible with the dataset
        if timepoint > image_dataset.shape[0] - 1:
            raise ValueError("Timepoint out of bounds!")
        if channel > image_dataset.shape[1] - 1:
            raise ValueError("Channel out of bounds!")

        # Extract and return stack
        stack = image_dataset[channel, timepoint, :]

        # If z is 1, we drop to two dimensions
        if stack.shape[0] == 1:
            stack = stack.squeeze()

        return stack

    def __repr__(self):
        """Return human-readable representation of the file."""
        if self._file is None:
            return "HuygensHDF5Reader()"
        return (
            f'HuygensHDF5Reader("{Path(self._filename).name}")\n'
            f"  - Dimensions: ("
            f"t={self.num_timepoints}, "
            f"c={self.num_channels}, "
            f"z={self.num_planes}, "
            f"y={self.size[0]}, "
            f"x={self.size[1]})\n"
            f"  - Voxel size: ("
            f"x={1e6 * self.voxel_sizes[0]:.4f}µm, "
            f"y={1e6 * self.voxel_sizes[1]:.4f}µm, "
            f"z={1e6 * self.voxel_sizes[2]:.4f}µm)\n"
        )

    def __str__(self):
        """Return human-readable representation of the file."""
        return self.__repr__()

    @property
    def filename(self) -> str:
        """Return the file name with full path.

        Returns
        -------

        filename: str
            Full file name, or empty string if not set.
        """
        if self._file is None:
            return ""
        return str(self._filename)

    @property
    def channel_names(self) -> tuple:
        """Return the names of the channels.

        Returns
        -------

        channel_names: tuple
            Names of the channels
        """
        if self._file is None:
            return ()
        return self._channels_names

    @property
    def num_timepoints(self) -> int:
        """Return the number of timepoints.

        Returns
        -------

        num_timepoints: int
            Number of timepoints per series in the file. If no file is open, `num_timepoints` is 0.
        """
        if self._file is None:
            return 0
        return self._num_timepoints

    @property
    def num_channels(self) -> int:
        """Return the number of channels.

        Returns
        -------

        num_channels: int
            Number of channels per series in the file. If no file is open, `num_channels` is 0.
        """
        if self._file is None:
            return 0
        return self._num_channels

    @property
    def num_planes(self) -> int:
        """Return the number of planes.

        Returns
        -------

        num_planes: int
            Number of planes per series in the file. If no file is open, `num_planes` is 0.
        """
        if self._file is None:
            return 0
        return self._num_planes

    @property
    def size(self) -> tuple:
        """Return the XYZ size of the dataset.

        Returns
        -------

        size: tuple
            Size `(x, y, z)`
        """
        return self._sizeX, self._sizeY, self._sizeZ

    @property
    def voxel_sizes(self) -> tuple:
        """Return the global voxel sizes for the acquisition.

        Returns
        -------

        voxel_sizes: tuple
            Voxel sizes `(vx, vy, vz)`
        """
        return self._voxel_sizes

    @property
    def metadata(self) -> dict:
        """Return the metadata dictionary.

        Returns
        -------

        metadata: dict
            Metadata dictionary.
        """
        if self._file is None:
            return {}
        return self._metadata

    @property
    def hdf5_dataset(self) -> Union[Dataset, None]:
        """Return the underlying HDF5 dataset.

        Returns
        -------

        hdf5_dataset: h5py.Dataset
            HD5 dataset if a file is open, or None otherwise.
        """
        if self._file is None:
            return None
        # Get HDF5 dataset
        try:
            dataset = self._file.get(list(self._file.keys())[0])["ImageData"]["Image"]
        except Exception:
            return None
        return dataset

    def __del__(self):
        """Destructor."""
        if self._file is not None:
            try:
                self._file.close()
            except Exception as _:
                pass

Instance variables

prop channel_names : tuple

Return the names of the channels.

Returns

channel_names : tuple
Names of the channels
Expand source code
@property
def channel_names(self) -> tuple:
    """Return the names of the channels.

    Returns
    -------

    channel_names: tuple
        Names of the channels
    """
    if self._file is None:
        return ()
    return self._channels_names
prop filename : str

Return the file name with full path.

Returns

filename : str
Full file name, or empty string if not set.
Expand source code
@property
def filename(self) -> str:
    """Return the file name with full path.

    Returns
    -------

    filename: str
        Full file name, or empty string if not set.
    """
    if self._file is None:
        return ""
    return str(self._filename)
prop hdf5_dataset : Optional[h5py._hl.dataset.Dataset]

Return the underlying HDF5 dataset.

Returns

hdf5_dataset : h5py.Dataset
HD5 dataset if a file is open, or None otherwise.
Expand source code
@property
def hdf5_dataset(self) -> Union[Dataset, None]:
    """Return the underlying HDF5 dataset.

    Returns
    -------

    hdf5_dataset: h5py.Dataset
        HD5 dataset if a file is open, or None otherwise.
    """
    if self._file is None:
        return None
    # Get HDF5 dataset
    try:
        dataset = self._file.get(list(self._file.keys())[0])["ImageData"]["Image"]
    except Exception:
        return None
    return dataset
prop metadata : dict

Return the metadata dictionary.

Returns

metadata : dict
Metadata dictionary.
Expand source code
@property
def metadata(self) -> dict:
    """Return the metadata dictionary.

    Returns
    -------

    metadata: dict
        Metadata dictionary.
    """
    if self._file is None:
        return {}
    return self._metadata
prop num_channels : int

Return the number of channels.

Returns

num_channels : int
Number of channels per series in the file. If no file is open, num_channels is 0.
Expand source code
@property
def num_channels(self) -> int:
    """Return the number of channels.

    Returns
    -------

    num_channels: int
        Number of channels per series in the file. If no file is open, `num_channels` is 0.
    """
    if self._file is None:
        return 0
    return self._num_channels
prop num_planes : int

Return the number of planes.

Returns

num_planes : int
Number of planes per series in the file. If no file is open, num_planes is 0.
Expand source code
@property
def num_planes(self) -> int:
    """Return the number of planes.

    Returns
    -------

    num_planes: int
        Number of planes per series in the file. If no file is open, `num_planes` is 0.
    """
    if self._file is None:
        return 0
    return self._num_planes
prop num_timepoints : int

Return the number of timepoints.

Returns

num_timepoints : int
Number of timepoints per series in the file. If no file is open, num_timepoints is 0.
Expand source code
@property
def num_timepoints(self) -> int:
    """Return the number of timepoints.

    Returns
    -------

    num_timepoints: int
        Number of timepoints per series in the file. If no file is open, `num_timepoints` is 0.
    """
    if self._file is None:
        return 0
    return self._num_timepoints
prop size : tuple

Return the XYZ size of the dataset.

Returns

size : tuple
Size (x, y, z)
Expand source code
@property
def size(self) -> tuple:
    """Return the XYZ size of the dataset.

    Returns
    -------

    size: tuple
        Size `(x, y, z)`
    """
    return self._sizeX, self._sizeY, self._sizeZ
prop voxel_sizes : tuple

Return the global voxel sizes for the acquisition.

Returns

voxel_sizes : tuple
Voxel sizes (vx, vy, vz)
Expand source code
@property
def voxel_sizes(self) -> tuple:
    """Return the global voxel sizes for the acquisition.

    Returns
    -------

    voxel_sizes: tuple
        Voxel sizes `(vx, vy, vz)`
    """
    return self._voxel_sizes

Methods

def load(self, timepoint: int = 0, channel: int = 0)

Load specific timepoint and channel.

Parameters

timepoint : int
Index of the timepoint to load, 0-based. (Optional, default 0).
channel : int
Index of the channel to load, 0-based. (Optional, default 0).

Returns

dataset : Union[None, DataSet, np.ndarray]
Dataset at the requested timepoint and channel.
class ImarisReader (filename: Union[pathlib.Path, str] = None)

Imaris Reader.

Examples:

from iaf.io.readers import ImarisReader

# Initialize the reader
reader = ImarisReader("file.ims")

# To load a specific (timepoint, channel) image or stack:
stack = reader.load(timepoint=0, channel=0)

Stacks are lazily loaded only on access.

The reader exposes metadata information via properties:

Property Explanation
reader.channel_names Tuple of names for each of the acquisition channels.
reader.filename Full file name of the opened file.
reader.num_channels Number of channels.
reader.num_planes Number of planes (z levels).
reader.num_timepoints Number of time points.
reader.voxel_sizes Voxel sizes in units (um) (x, y, z).
reader.extends Extends in units (um) (minX, maxX, minY, maxY, minZ, maxZ).

Constructor.

Parameters

filename : Union[Path, str]
Full path to the file to open
Expand source code
class ImarisReader:
    """Imaris Reader.

    Examples:

    ```
    from iaf.io.readers import ImarisReader

    # Initialize the reader
    reader = ImarisReader("file.ims")

    # To load a specific (timepoint, channel) image or stack:
    stack = reader.load(timepoint=0, channel=0)
    ```

    Stacks are lazily loaded only on access.

    The reader exposes metadata information via properties:

    | Property                | Explanation                                                   |
    | ----------------------- | ------------------------------------------------------------- |
    | `reader.channel_names`  | Tuple of names for each of the acquisition channels.          |
    | `reader.filename`       | Full file name of the opened file.                            |
    | `reader.num_channels`   | Number of channels.                                           |
    | `reader.num_planes`     | Number of planes (z levels).                                  |
    | `reader.num_timepoints` | Number of time points.                                        |
    | `reader.voxel_sizes`    | Voxel sizes in units (um) `(x, y, z)`.                        |
    | `reader.extends`        | Extends in units (um) `(minX, maxX, minY, maxY, minZ, maxZ)`. |
    """

    def __init__(self, filename: Union[Path, str] = None):
        """Constructor.

        Parameters
        ----------

        filename: Union[Path, str]
            Full path to the file to open
        """

        self._file = None

        # Make sure a filename was passed
        if filename is None:
            raise ValueError("Please specify a file name!")

        self._filename: Path = Path(filename).absolute()
        if not self._filename.exists():
            raise IOError(f"File {filename} does not exist.")

        if not self._filename.suffix.lower() == ".ims":
            raise ValueError(f"The _file {self._filename} is not an .ims file.")

        # Initialize _geometry
        self._num_timepoints: int = 1
        self._num_channels: int = 1
        self._num_planes: int = 1
        self._sizeX: int = 0
        self._sizeY: int = 0
        self._sizeZ: int = 0
        self._channels_names: tuple = ()
        self._extents: tuple = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)

        # Try opening file
        self._file = h5py.File(self._filename, "r")

        # Parse the geometry of the file and sets the appropriate bundle_ and iter_axis
        self._parse_geometry()

        # Parse voxel sizes
        self._voxel_sizes = (0.0, 0.0, 0.0)
        self._parse_voxel_sizes()

        # Parse channel names
        self._parse_channel_names()

    def _parse_geometry(self):
        """
        Parse geometry of IMS file and set properties.
        """
        self._num_timepoints = len(self._file["DataSet"]["ResolutionLevel 0"].keys())
        self._num_channels = len(
            self._file["DataSet"]["ResolutionLevel 0"]["TimePoint 0"].keys()
        )
        self._num_planes = self._file["DataSet"]["ResolutionLevel 0"]["TimePoint 0"][
            "Channel 0"
        ]["Data"].shape[0]
        self._sizeX = int(
            str(self._file["DataSetInfo"]["Image"].attrs["X"], encoding="utf-8")
        )
        self._sizeY = int(
            str(self._file["DataSetInfo"]["Image"].attrs["Y"], encoding="utf-8")
        )
        self._sizeZ = int(
            str(self._file["DataSetInfo"]["Image"].attrs["Z"], encoding="utf-8")
        )

    def _parse_voxel_sizes(self):
        """
        Parse metadata of IMS file and extracts pixel size and z step.
        """

        # Extract the extends
        extmin_x = float(
            str(self._file["DataSetInfo"]["Image"].attrs["ExtMin0"], encoding="utf-8")
        )
        extmax_x = float(
            str(self._file["DataSetInfo"]["Image"].attrs["ExtMax0"], encoding="utf-8")
        )
        extmin_y = float(
            str(self._file["DataSetInfo"]["Image"].attrs["ExtMin1"], encoding="utf-8")
        )
        extmax_y = float(
            str(self._file["DataSetInfo"]["Image"].attrs["ExtMax1"], encoding="utf-8")
        )
        extmin_z = float(
            str(self._file["DataSetInfo"]["Image"].attrs["ExtMin2"], encoding="utf-8")
        )
        extmax_z = float(
            str(self._file["DataSetInfo"]["Image"].attrs["ExtMax2"], encoding="utf-8")
        )

        # Set the extends property
        self._extends: tuple = (
            extmin_x,
            extmax_x,
            extmin_y,
            extmax_y,
            extmin_z,
            extmax_z,
        )

        # Calculate and store the voxel sizes
        voxel_x = (extmax_x - extmin_x) / self._sizeX
        voxel_y = (extmax_y - extmin_y) / self._sizeY
        voxel_z = (extmax_z - extmin_z) / self._sizeZ
        self._voxel_sizes = (voxel_x, voxel_y, voxel_z)

    def _parse_channel_names(self):
        """
        Parse the channel names.
        """
        channel_names_list = []
        for i in range(self.num_channels):
            name = str(
                self._file["DataSetInfo"][f"Channel {i}"].attrs["Name"],
                encoding="utf-8",
            )
            channel_names_list.append(name)
        self._channels_names = tuple(channel_names_list)

    def load(self, timepoint: int = 0, channel: int = 0, resolution_level: int = 0):
        """Load specific timepoint and channel.

        Parameters
        ----------

        timepoint: int
            Index of the timepoint to load, 0-based. (Optional, default 0).

        channel: int
            Index of the channel to load, 0-based. (Optional, default 0).

        resolution_level: int
            Resolution level to load, with 0 being full resolution. (Optional, default 0).

        Returns
        -------

        dataset: Union[None, DataSet, np.ndarray]
            Dataset at the requested `timepoint` and `channel`.
        """

        # Build keys from indices
        resolution_level_key = f"ResolutionLevel {resolution_level}"
        timepoint_key = f"TimePoint {timepoint}"
        channel_key = f"Channel {channel}"

        try:
            # Get the dataset
            dataset = self._file["DataSet"][resolution_level_key][timepoint_key][
                channel_key
            ]["Data"]

            # Because of chunk size, the dataset size may not be exactly the same as the underlying
            # stack size. We extract the relevant subset.
            dataset = dataset[: self._sizeZ, : self._sizeY, : self._sizeX]

            # If z is 1, we drop to two dimensions
            if self._sizeZ == 1:
                dataset = dataset.squeeze()

        except Exception:
            dataset = None

        return dataset

    def __repr__(self):
        """Return human-readable representation of the file."""
        if self._file is None:
            return "ImarisReader()"
        return (
            f'ImarisReader("{Path(self._filename).name}")\n'
            f"  - Dimensions: ("
            f"t={self.num_timepoints}, "
            f"c={self.num_channels}, "
            f"z={self.num_planes}, "
            f"y={self.size[0]}, "
            f"x={self.size[1]})\n"
            f"  - Voxel size: ("
            f"x={self.voxel_sizes[0]:.4f}µm, "
            f"y={self.voxel_sizes[1]:.4f}µm, "
            f"z={self.voxel_sizes[2]:.4f}µm)\n"
        )

    def __str__(self):
        """Return human-readable representation of the file."""
        return self.__repr__()

    @property
    def filename(self) -> str:
        """Return the file name with full path.

        Returns
        -------

        filename: str
            Full file name, or empty string if not set.
        """
        if self._file is None:
            return ""
        return str(self._filename)

    @property
    def extends(self) -> tuple:
        """Return the dataset extends.

        Returns
        -------

        extends: tuple
            Dataset extends:  `(extMinX, extMaxX, extMinY, extMaxY, extMinZ, extMaxZ)`
        """
        if self._file is None:
            return ()
        return self._extends

    @property
    def channel_names(self) -> tuple:
        """Return the names of the channels.

        Returns
        -------

        channel_names: tuple
            Names of the channels
        """
        if self._file is None:
            return ()
        return self._channels_names

    @property
    def num_timepoints(self) -> int:
        """Return the number of timepoints.

        Returns
        -------

        num_timepoints: int
            Number of timepoints per series in the file. If no file is open, `num_timepoints` is 0.
        """
        if self._file is None:
            return 0
        return self._num_timepoints

    @property
    def num_channels(self) -> int:
        """Return the number of channels.

        Returns
        -------

        num_channels: int
            Number of channels per series in the file. If no file is open, `num_channels` is 0.
        """
        if self._file is None:
            return 0
        return self._num_channels

    @property
    def num_planes(self) -> int:
        """Return the number of planes.

        Returns
        -------

        num_planes: int
            Number of planes per series in the file. If no file is open, `num_planes` is 0.
        """
        if self._file is None:
            return 0
        return self._num_planes

    @property
    def size(self) -> tuple:
        """Return the XYZ size of the dataset.

        Returns
        -------

        size: tuple
            Size `(x, y, z)`
        """
        return self._sizeX, self._sizeY, self._sizeZ

    @property
    def voxel_sizes(self) -> tuple:
        """Return the global voxel sizes for the acquisition.

        Returns
        -------

        voxel_sizes: tuple
            Voxel sizes `(vx, vy, vz)`
        """
        return self._voxel_sizes

    def __del__(self):
        """Destructor."""
        if self._file is not None:
            try:
                self._file.close()
            except Exception as _:
                pass

Instance variables

prop channel_names : tuple

Return the names of the channels.

Returns

channel_names : tuple
Names of the channels
Expand source code
@property
def channel_names(self) -> tuple:
    """Return the names of the channels.

    Returns
    -------

    channel_names: tuple
        Names of the channels
    """
    if self._file is None:
        return ()
    return self._channels_names
prop extends : tuple

Return the dataset extends.

Returns

extends : tuple
Dataset extends: (extMinX, extMaxX, extMinY, extMaxY, extMinZ, extMaxZ)
Expand source code
@property
def extends(self) -> tuple:
    """Return the dataset extends.

    Returns
    -------

    extends: tuple
        Dataset extends:  `(extMinX, extMaxX, extMinY, extMaxY, extMinZ, extMaxZ)`
    """
    if self._file is None:
        return ()
    return self._extends
prop filename : str

Return the file name with full path.

Returns

filename : str
Full file name, or empty string if not set.
Expand source code
@property
def filename(self) -> str:
    """Return the file name with full path.

    Returns
    -------

    filename: str
        Full file name, or empty string if not set.
    """
    if self._file is None:
        return ""
    return str(self._filename)
prop num_channels : int

Return the number of channels.

Returns

num_channels : int
Number of channels per series in the file. If no file is open, num_channels is 0.
Expand source code
@property
def num_channels(self) -> int:
    """Return the number of channels.

    Returns
    -------

    num_channels: int
        Number of channels per series in the file. If no file is open, `num_channels` is 0.
    """
    if self._file is None:
        return 0
    return self._num_channels
prop num_planes : int

Return the number of planes.

Returns

num_planes : int
Number of planes per series in the file. If no file is open, num_planes is 0.
Expand source code
@property
def num_planes(self) -> int:
    """Return the number of planes.

    Returns
    -------

    num_planes: int
        Number of planes per series in the file. If no file is open, `num_planes` is 0.
    """
    if self._file is None:
        return 0
    return self._num_planes
prop num_timepoints : int

Return the number of timepoints.

Returns

num_timepoints : int
Number of timepoints per series in the file. If no file is open, num_timepoints is 0.
Expand source code
@property
def num_timepoints(self) -> int:
    """Return the number of timepoints.

    Returns
    -------

    num_timepoints: int
        Number of timepoints per series in the file. If no file is open, `num_timepoints` is 0.
    """
    if self._file is None:
        return 0
    return self._num_timepoints
prop size : tuple

Return the XYZ size of the dataset.

Returns

size : tuple
Size (x, y, z)
Expand source code
@property
def size(self) -> tuple:
    """Return the XYZ size of the dataset.

    Returns
    -------

    size: tuple
        Size `(x, y, z)`
    """
    return self._sizeX, self._sizeY, self._sizeZ
prop voxel_sizes : tuple

Return the global voxel sizes for the acquisition.

Returns

voxel_sizes : tuple
Voxel sizes (vx, vy, vz)
Expand source code
@property
def voxel_sizes(self) -> tuple:
    """Return the global voxel sizes for the acquisition.

    Returns
    -------

    voxel_sizes: tuple
        Voxel sizes `(vx, vy, vz)`
    """
    return self._voxel_sizes

Methods

def load(self, timepoint: int = 0, channel: int = 0, resolution_level: int = 0)

Load specific timepoint and channel.

Parameters

timepoint : int
Index of the timepoint to load, 0-based. (Optional, default 0).
channel : int
Index of the channel to load, 0-based. (Optional, default 0).
resolution_level : int
Resolution level to load, with 0 being full resolution. (Optional, default 0).

Returns

dataset : Union[None, DataSet, np.ndarray]
Dataset at the requested timepoint and channel.
class NikonND2Reader (filename: Union[pathlib.Path, str] = None)

Nikon ND2 reader that internally uses nd2reader by Ruben Verweij.

Examples:

from iaf.io.readers import NikonND2Reader

# Initialize the reader
reader = NikonND2Reader("file.nd2")

# To iterate over all series (or timepoints):
for img in reader:
    pass

# To access a specific series (or timepoint):
stack = reader[0]

Series (or timepoints) are lazily loaded only on access.

The reader exposes metadata information via properties:

Property Explanation
reader.channel_names Tuple of names for each of the acquisition channels.
reader.filename Full file name of the opened file.
reader.geometry Geometry for each series.
reader.iter_axis Axis over which the iteration occurs (one of "v" or "t").
reader.metadata Processed file metadata (dictionary).
reader.num_channels Number of channels.
reader.num_planes Number of planes (z levels).
reader.num_series Number of series (acquisitions) in the file.
reader.num_timepoints Number of time points.
reader.voxel_sizes Voxel sizes in units (um) (x, y, z).

If a file contains several series, iter_axis will be "v": that is, the iterator will return one series at a time. If there is only one series, however, iter_axis will point to the next dimension, that is "t". In this case, the iterator will return one time point at a time. No more granularity is supported. In all cases, the geometry property will indicate the dimensionality of the array returned by the iterator (e.g., "czyx").

Constructor.

Parameters

filename : Union[Path, str]
Full path to the file to open
Expand source code
class NikonND2Reader:
    """Nikon ND2 reader that internally uses [nd2reader](https://open-science-tools.github.io/nd2reader/) by
     Ruben Verweij.

    Examples:

    ```
    from iaf.io.readers import NikonND2Reader

    # Initialize the reader
    reader = NikonND2Reader("file.nd2")

    # To iterate over all series (or timepoints):
    for img in reader:
        pass

    # To access a specific series (or timepoint):
    stack = reader[0]
    ```

    Series (or timepoints) are lazily loaded only on access.

    The reader exposes metadata information via properties:

    | Property                | Explanation                                                   |
    | ----------------------- | ------------------------------------------------------------- |
    | `reader.channel_names`  | Tuple of names for each of the acquisition channels.          |
    | `reader.filename`       | Full file name of the opened file.                            |
    | `reader.geometry`       | Geometry for each series.                                     |
    | `reader.iter_axis`      | Axis over which the iteration occurs (one of `"v"` or `"t"`). |
    | `reader.metadata`       | Processed file metadata (dictionary).                         |
    | `reader.num_channels`   | Number of channels.                                           |
    | `reader.num_planes`     | Number of planes (z levels).                                  |
    | `reader.num_series`     | Number of series (acquisitions) in the file.                  |
    | `reader.num_timepoints` | Number of time points.                                        |
    | `reader.voxel_sizes`    | Voxel sizes in units (um) `(x, y, z)`.                        |

    If a file contains several series, `iter_axis` will be `"v"`: that is, the iterator will return one
    series at a time. If there is only one series, however, `iter_axis` will point to the next dimension,
    that is `"t"`. In this case, the iterator will return one time point at a time. No more granularity is
    supported. In all cases, the `geometry` property will indicate the dimensionality of the array returned by
    the iterator (e.g., `"czyx"`).
    """

    def __init__(self, filename: Union[Path, str] = None):
        """Constructor.

        Parameters
        ----------

        filename: Union[Path, str]
            Full path to the file to open
        """

        self._file: Optional[ND2Reader] = None

        # Make sure a filename was passed
        if filename is None:
            raise ValueError("Please specify a file name!")

        self._filename: Path = Path(filename).absolute()
        if not self._filename.exists():
            raise IOError(f"File {filename} does not exist.")

        if not self._filename.suffix.lower() == ".nd2":
            raise ValueError(f"The _file {self._filename} is not an .nd2 file.")

        # Initialize _geometry
        self._num_series: int = 1
        self._num_timepoints: int = 1
        self._num_channels: int = 1
        self._num_planes: int = 1
        self._geometry: str = ""
        self._iter_axis: str = ""

        # Open the file
        self._file = ND2Reader(str(self._filename))

        # Set the counter for the iterator
        self._current_index = 0

        # Parse the geometry of the file and sets the appropriate bundle_ and iter_axis
        self._parse_geometry()

        # Parse voxel sizes
        self._voxel_sizes = (0.0, 0.0, 0.0)
        self._parse_voxel_sizes()

    def __repr__(self):
        """Return human-readable representation of the file."""
        if self._file is None:
            return "NikonND2Reader()"
        return (
            f'NikonND2Reader("{Path(self._filename).name}")\n'
            f"  - Dimensions: ("
            f"v={self.num_series}, "
            f"t={self.num_timepoints}, "
            f"c={self.num_channels}, "
            f"z={self.num_planes}, "
            f"y={self.metadata['height']}, "
            f"x={self.metadata['width']})\n"
            f"  - Voxel size: ("
            f"x={self.voxel_sizes[0]:.4f}µm, "
            f"y={self.voxel_sizes[1]:.4f}µm, "
            f"z={self.voxel_sizes[2]:.4f}µm)\n"
            f'  - Series geometry: "{self.geometry}"'
        )

    def __str__(self):
        """Return human-readable representation of the file."""
        return self.__repr__()

    @property
    def filename(self) -> str:
        """Return the file name with full path.

        Returns
        -------

        filename: str
            Full file name, or empty string if not set.
        """
        if self._file is None:
            return ""
        return str(self._filename)

    @property
    def metadata(self) -> dict:
        """Return the processed file metadata.

        Returns
        -------

        matadata: dict
            Metadata dictionary. If no file is open, `metadata` is {}.
        """
        if self._file is None:
            return {}
        return self._file.metadata

    @property
    def raw_metadata(self) -> Union[None, dict]:
        """Return the raw file metadata.

        Returns
        -------

        raw_metadata: object of type [`RawMetadata`](https://open-science-tools.github.io/nd2reader/nd2reader.html#module-nd2reader.raw_metadata).
            If no file is open, `raw_metadata` is None.
        """
        if self._file is None:
            return None
        return self._file.parser._raw_metadata

    @property
    def num_series(self) -> int:
        """Return the number of series.

        Returns
        -------

        num_series: int
            Number of series in the file. If no file is open, `num_series` is 0.
        """
        if self._file is None:
            return 0
        return self._num_series

    @property
    def num_timepoints(self) -> int:
        """Return the number of timepoints.

        Returns
        -------

        num_timepoints: int
            Number of timepoints per series in the file. If no file is open, `num_timepoints` is 0.
        """
        if self._file is None:
            return 0
        return self._num_timepoints

    @property
    def num_channels(self) -> int:
        """Return the number of channels.

        Returns
        -------

        num_channels: int
            Number of channels per series in the file. If no file is open, `num_channels` is 0.
        """
        if self._file is None:
            return 0
        return self._num_channels

    @property
    def num_planes(self) -> int:
        """Return the number of planes.

        Returns
        -------

        num_planes: int
            Number of planes per series in the file. If no file is open, `num_planes` is 0.
        """
        if self._file is None:
            return 0
        return self._num_planes

    @property
    def geometry(self) -> str:
        """Return the geometry of the individual image or stack.

        Returns
        -------

        geometry: str
            Geometry of the individual image or stack. If no file is open, `geometry` is "".
        """
        if self._file is None:
            return ""
        return self._geometry

    @property
    def iter_axis(self) -> str:
        """Return the axis used for iterating over individual images or stacks.

        Returns
        -------

        iter_axis: str
            Axis used for iterating over individual images of stacks. If no file is open, `iter_axis` is "".
        """
        if self._file is None:
            return ""
        return self._iter_axis

    @property
    def voxel_sizes(self) -> tuple:
        """Return the global voxel sizes for the acquisition.

        Returns
        -------

        voxel_sizes: tuple
            Voxel sizes `(vx, vy, vz)`
        """
        return self._voxel_sizes

    @property
    def channel_names(self) -> tuple:
        """Return the names of the channels.

        Returns
        -------

        channel_names: tuple
            Voxel sizes (x, y, z)
        """
        if self._file is None:
            return ()
        return tuple(self._file.metadata["channels"])

    def __del__(self):
        """Destructor."""
        if self._file is not None:
            try:
                self._file.close()
            except Exception as _:
                pass

    def __getitem__(self, series: int) -> Frame:
        """Allows accessing the reader with the `[]` notation to get the next image/stack.

        Parameters
        ----------

        series: int
            Index of the series to be retrieved.

        Returns
        -------

        frame: Frame
            Requested image or stack. Please notice that the type of the data is ALWAYS `float64`!
        """

        if self._file is None:
            raise ValueError("Please load a file first.")

        if series < 0 or series > (self._num_series - 1):
            raise ValueError(f"Index value {series} is out of bounds.")

        # Get and return the image
        img = self._file[series]
        return img

    def __iter__(self):
        """Return the iterator.

        Returns
        -------

            iterator
        """
        self._current_index = 0
        return self

    def __next__(self):
        if self._current_index < self._num_series:
            img = self.__getitem__(self._current_index)
            self._current_index += 1
            return img
        else:
            raise StopIteration

    def _parse_geometry(self) -> None:
        """
        Parse geometry of ND2 file and sets `bundle_axis` and `_iter_axis` properties of ND2Reader.
        """

        self._geometry = "yx"
        if "z" in self._file.sizes and self._file.sizes["z"] > 1:
            self._num_planes = self._file.sizes["z"]
            self._geometry = "z" + self._geometry
        if "c" in self._file.sizes and self._file.sizes["c"] > 1:
            self._num_channels = self._file.sizes["c"]
            self._geometry = "c" + self._geometry
        if "t" in self._file.sizes and self._file.sizes["t"] > 1:
            self._num_timepoints = self._file.sizes["t"]
            self._geometry = "t" + self._geometry

        self._file.bundle_axes = self._geometry

        # Axis to iterate upon
        if "z" in self._file.sizes:
            self._file.iter_axes = ["z"]
        if "c" in self._file.sizes:
            self._file.iter_axes = ["c"]
        if "t" in self._file.sizes:
            self._file.iter_axes = ["t"]
        if "v" in self._file.sizes:
            self._num_series = self._file.sizes["v"]
            self._file.iter_axes = ["v"]

        # Set the main axis for iteration
        self._iter_axis = self._file.iter_axes[0]

    def _parse_voxel_sizes(self) -> None:
        """
        Parse metadata of ND2 file and extracts pixel size and z step.
        """

        # Build the array step by step
        voxel_sizes = [0.0, 0.0, 0.0]

        if "pixel_microns" in self._file.metadata:
            p = self._file.metadata["pixel_microns"]
            voxel_sizes[0] = p
            voxel_sizes[1] = p

        # If there is only one plane, we leave the z step to 0.0
        z_coords = None
        if self.num_planes > 1:
            if (
                "z_coordinates" in self._file.metadata
                and self._file.metadata["z_coordinates"] is not None
            ):
                z_coords = np.array(self._file.metadata["z_coordinates"])
            elif (
                hasattr(self._file._parser._raw_metadata, "z_data")
                and self._file.parser._raw_metadata.z_data is not None
            ):
                z_coords = np.array(self._file.parser._raw_metadata.z_data)
            else:
                print("Could not read z coordinates!")

        if z_coords is not None:
            z_steps = np.zeros(self.num_series * self.num_timepoints)
            for i, z in enumerate(range(0, len(z_coords), self.num_planes)):
                z_range = z_coords[z : z + self.num_planes]
                z_steps[i] = np.mean(np.diff(z_range))

            voxel_sizes[2] = z_steps.mean()

        # Set the voxel sizes
        self._voxel_sizes = tuple(voxel_sizes)

Instance variables

prop channel_names : tuple

Return the names of the channels.

Returns

channel_names : tuple
Voxel sizes (x, y, z)
Expand source code
@property
def channel_names(self) -> tuple:
    """Return the names of the channels.

    Returns
    -------

    channel_names: tuple
        Voxel sizes (x, y, z)
    """
    if self._file is None:
        return ()
    return tuple(self._file.metadata["channels"])
prop filename : str

Return the file name with full path.

Returns

filename : str
Full file name, or empty string if not set.
Expand source code
@property
def filename(self) -> str:
    """Return the file name with full path.

    Returns
    -------

    filename: str
        Full file name, or empty string if not set.
    """
    if self._file is None:
        return ""
    return str(self._filename)
prop geometry : str

Return the geometry of the individual image or stack.

Returns

geometry : str
Geometry of the individual image or stack. If no file is open, geometry is "".
Expand source code
@property
def geometry(self) -> str:
    """Return the geometry of the individual image or stack.

    Returns
    -------

    geometry: str
        Geometry of the individual image or stack. If no file is open, `geometry` is "".
    """
    if self._file is None:
        return ""
    return self._geometry
prop iter_axis : str

Return the axis used for iterating over individual images or stacks.

Returns

iter_axis : str
Axis used for iterating over individual images of stacks. If no file is open, iter_axis is "".
Expand source code
@property
def iter_axis(self) -> str:
    """Return the axis used for iterating over individual images or stacks.

    Returns
    -------

    iter_axis: str
        Axis used for iterating over individual images of stacks. If no file is open, `iter_axis` is "".
    """
    if self._file is None:
        return ""
    return self._iter_axis
prop metadata : dict

Return the processed file metadata.

Returns

matadata : dict
Metadata dictionary. If no file is open, metadata is {}.
Expand source code
@property
def metadata(self) -> dict:
    """Return the processed file metadata.

    Returns
    -------

    matadata: dict
        Metadata dictionary. If no file is open, `metadata` is {}.
    """
    if self._file is None:
        return {}
    return self._file.metadata
prop num_channels : int

Return the number of channels.

Returns

num_channels : int
Number of channels per series in the file. If no file is open, num_channels is 0.
Expand source code
@property
def num_channels(self) -> int:
    """Return the number of channels.

    Returns
    -------

    num_channels: int
        Number of channels per series in the file. If no file is open, `num_channels` is 0.
    """
    if self._file is None:
        return 0
    return self._num_channels
prop num_planes : int

Return the number of planes.

Returns

num_planes : int
Number of planes per series in the file. If no file is open, num_planes is 0.
Expand source code
@property
def num_planes(self) -> int:
    """Return the number of planes.

    Returns
    -------

    num_planes: int
        Number of planes per series in the file. If no file is open, `num_planes` is 0.
    """
    if self._file is None:
        return 0
    return self._num_planes
prop num_series : int

Return the number of series.

Returns

num_series : int
Number of series in the file. If no file is open, num_series is 0.
Expand source code
@property
def num_series(self) -> int:
    """Return the number of series.

    Returns
    -------

    num_series: int
        Number of series in the file. If no file is open, `num_series` is 0.
    """
    if self._file is None:
        return 0
    return self._num_series
prop num_timepoints : int

Return the number of timepoints.

Returns

num_timepoints : int
Number of timepoints per series in the file. If no file is open, num_timepoints is 0.
Expand source code
@property
def num_timepoints(self) -> int:
    """Return the number of timepoints.

    Returns
    -------

    num_timepoints: int
        Number of timepoints per series in the file. If no file is open, `num_timepoints` is 0.
    """
    if self._file is None:
        return 0
    return self._num_timepoints
prop raw_metadata : Optional[None]

Return the raw file metadata.

Returns

raw_metadata: object of type RawMetadata. If no file is open, raw_metadata is None.

Expand source code
@property
def raw_metadata(self) -> Union[None, dict]:
    """Return the raw file metadata.

    Returns
    -------

    raw_metadata: object of type [`RawMetadata`](https://open-science-tools.github.io/nd2reader/nd2reader.html#module-nd2reader.raw_metadata).
        If no file is open, `raw_metadata` is None.
    """
    if self._file is None:
        return None
    return self._file.parser._raw_metadata
prop voxel_sizes : tuple

Return the global voxel sizes for the acquisition.

Returns

voxel_sizes : tuple
Voxel sizes (vx, vy, vz)
Expand source code
@property
def voxel_sizes(self) -> tuple:
    """Return the global voxel sizes for the acquisition.

    Returns
    -------

    voxel_sizes: tuple
        Voxel sizes `(vx, vy, vz)`
    """
    return self._voxel_sizes