# Parent class for python wrapper to libczi file for accessing Zeiss czi image and metadata.
import io
import multiprocessing
from pathlib import Path
from typing import BinaryIO, Tuple, Union
import numpy as np
import xml.etree.ElementTree as ET
from . import types
[docs]
class CziFile(object):
"""Zeiss CZI file object.
Args:
| czi_filename (str): Filename of czifile to access.
Kwargs:
| verbose (bool): Print information and times during czi file access.
.. note::
Utilizes compiled wrapper to libCZI for accessing the CZI file.
"""
# xxx - likely this is a Zeiss bug,
# units for the scale in the xml file are not correct (says microns, given in meters)
# scale_units = 1e6
# Dims as defined in libCZI
#
# Z = 1 # The Z-dimension.
# C = 2 # The C-dimension ("channel").
# T = 3 # The T-dimension ("time").
# R = 4 # The R-dimension ("rotation").
# S = 5 # The S-dimension ("scene").
# I = 6 # The I-dimension ("illumination").
# H = 7 # The H-dimension ("phase").
# V = 8 # The V-dimension ("view").
# aicspylibczi extended dims -- These may be returned but can not be used to request subblocks from the file
# A # The A-dimension ("samples" - RGB images).
# X # The X-dimension
# Y # The Y-dimension
####
ZISRAW_DIMS = {"Z", "C", "T", "R", "S", "I", "H", "V", "B"}
def __init__(
self,
czi_filename: types.FileLike,
verbose: bool = False,
):
# Convert to BytesIO (bytestream)
self._bytes = self.convert_to_buffer(czi_filename)
self.czifile_verbose = verbose
import _aicspylibczi
self.czilib = _aicspylibczi
self.reader = self.czilib.Reader(self._bytes)
self.meta_root = None
@property
def shape_is_consistent(self):
"""
Query if the file shape is consistent across scenes.
Returns
-------
bool
true if consistent, false the scenes have different dimension shapes
"""
return self.reader.has_consistent_shape()
@property
def dims(self):
"""
Get the dimensions present the binary data (not the metadata)
M, Y, X, A are included for completeness but can not be used as constraints.
Returns
-------
str
A string containing Dimensions letters present, ie "BSTZYX"
Note
----
Dimensions defined in libCZI -
V - view
H - phase
I - illumination
S - scene
R - rotation
T - time
C - channel
Z - z plane (height)
Dimensions added by aicspylibczi -
M - mosaic tile, mosaic images only
Y - image height
X - image width
A - samples, BGR/RGB images only
"""
return self.reader.read_dims_string()
[docs]
def get_dims_shape(self):
"""
Get the dimensions for the opened file from the binary data (not the metadata)
Returns
-------
list[dict]
A list of dictionaries containing Dimension / depth. If the shape is consistent across Scenes then
the list will have only one Dictionary. If the shape is inconsistent the the list will have a dictionary
for each Scene. A consistently shaped file with 3 scenes, 7 time-points
and 4 Z slices containing images of (h,w) = (325, 475) would return
[
{'S': (0, 3), 'T': (0,7), 'X': (0, 475), 'Y': (0, 325), 'Z': (0, 4)}
].
The result for a similarly shaped file but with different number of time-points per scene would yield
[
{'S': (0, 1), 'T': (0,8), 'X': (0, 475), 'Y': (0, 325), 'Z': (0, 4)},
{'S': (1, 2), 'T': (0,6), 'X': (0, 475), 'Y': (0, 325), 'Z': (0, 4)},
{'S': (2, 3), 'T': (0,7), 'X': (0, 475), 'Y': (0, 325), 'Z': (0, 4)}
]
For the same initial file but with an RGB pixel type the result would be
[
{'S': (0, 3), 'T': (0,7), 'X': (0, 475), 'Y': (0, 325), 'Z': (0, 4), 'A': (0,3)}
].
"""
return self.reader.read_dims()
@property
def pixel_type(self):
"""
The pixelType of the images. If the pixelType is different in the different subblocks it returns Invalid.
Returns
-------
A string containing the name of the type of each pixel. If inconsistent it returns invalid.
"""
return self.reader.pixel_type
[docs]
def get_tile_bounding_box(self, **kwargs):
"""
Get a single tile (subblock) bounding box (pyramid=0) for the specified dimensions.
For non-mosaic files.
Parameters
----------
kwargs
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").
R = 4 # The R-dimension ("rotation").
S = 5 # The S-dimension ("scene").
I = 6 # The I-dimension ("illumination").
H = 7 # The H-dimension ("phase").
V = 8 # The V-dimension ("view").
Returns
-------
bbox
A Bounding Box, bbox, of type BBox.
bbox.x = The x coordinate of the upper left corner of the bounding box.
bbox.y = The y coordinate of the upper left corner of the bounding box.
bbox.w = The width of the bounding box.
bbox.h = The height of the bounding box.
"""
plane_constraints = self._get_coords_from_kwargs(kwargs)
dims, bbox = self.reader.read_tile_bounding_box(plane_constraints)
return bbox
[docs]
def get_scene_bounding_box(self, index: int = 0):
"""
Get the bounding box (pyramid=0) for the specified scene. For non-mosaic files.
This should be equivalent to the results from get_tile_bounding_box but requiring only one arguments.
Parameters
----------
index
the scene index, if omitted it defaults to Zero
Returns
-------
bbox
A Bounding Box, bbox, of type BBox.
bbox.x = The x coordinate of the upper left corner of the bounding box.
bbox.y = The y coordinate of the upper left corner of the bounding box.
bbox.w = The width of the bounding box.
bbox.h = The height of the bounding box.
"""
bbox = self.reader.read_scene_bounding_box(index)
return bbox
[docs]
def get_all_tile_bounding_boxes(self, **kwargs):
"""
Get one or more tiles (subblocks) bounding boxes (pyramid=0) for the specified dimensions.
For non-mosaic files.
Parameters
----------
kwargs
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").
R = 4 # The R-dimension ("rotation").
S = 5 # The S-dimension ("scene").
I = 6 # The I-dimension ("illumination").
H = 7 # The H-dimension ("phase").
V = 8 # The V-dimension ("view").
Returns
-------
dict[tile_info, bbox]
A dictionary with keys of type TileInfo and values of type BBox.
For a key, ie tile, of type Tile:
tile.dimension_coordinates = A dictionary of Dimensions for the tile
For a value, ie bbox, of type BBox:
bbox.x = The x coordinate of the upper left corner of the bounding box.
bbox.y = The y coordinate of the upper left corner of the bounding box.
bbox.w = The width of the bounding box.
bbox.h = The height of the bounding box.
"""
plane_constraints = self._get_coords_from_kwargs(kwargs)
return self.reader.read_all_tile_bounding_boxes(plane_constraints)
[docs]
def get_all_scene_bounding_boxes(self):
"""
Get one or more tiles (subblocks) bounding boxes (pyramid=0) for the specified dimensions.
For non-mosaic files.
Returns
-------
dict[int, bbox]
A dictionary with keys of type Int and values of type BBox.
The integer key is the Scene Index.
For a value, ie bbox, of type BBox:
bbox.x = The x coordinate of the upper left corner of the bounding box.
bbox.y = The y coordinate of the upper left corner of the bounding box.
bbox.w = The width of the bounding box.
bbox.h = The height of the bounding box.
"""
return self.reader.read_all_scene_bounding_boxes()
[docs]
def get_mosaic_tile_bounding_box(self, **kwargs):
"""
Get a single tile (subblock) bounding box (pyramid=0) for the specified dimensions.
For mosaic files.
Parameters
----------
kwargs
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").
R = 4 # The R-dimension ("rotation").
S = 5 # The S-dimension ("scene").
I = 6 # The I-dimension ("illumination").
H = 7 # The H-dimension ("phase").
V = 8 # The V-dimension ("view").
M = 10 # The M_index, this is only valid for Mosaic files!
Returns
-------
bbox
A Bounding Box, bbox, of type BBox.
bbox.x = The x coordinate of the upper left corner of the bounding box.
bbox.y = The y coordinate of the upper left corner of the bounding box.
bbox.w = The width of the bounding box.
bbox.h = The height of the bounding box.
"""
plane_constraints = self._get_coords_from_kwargs(kwargs)
m_index = self._get_m_index_from_kwargs(kwargs)
ssorter, bbox = self.reader.read_mosaic_tile_bounding_box(
plane_constraints, m_index
)
return bbox
[docs]
def get_mosaic_scene_bounding_box(self, index: int = 0):
"""
Get the bounding box (pyramid=0) for the specified scene. For mosaic files.
This is not equivalent to the results from get_mosaic_tile_bounding_box.
Parameters
----------
index
the scene index, if omitted it defaults to Zero
Returns
-------
bbox
A Bounding Box, bbox, of type BBox.
bbox.x = The x coordinate of the upper left corner of the bounding box.
bbox.y = The y coordinate of the upper left corner of the bounding box.
bbox.w = The width of the bounding box.
bbox.h = The height of the bounding box.
"""
return self.reader.read_mosaic_scene_bounding_box(index)
[docs]
def get_all_mosaic_tile_bounding_boxes(self, **kwargs):
"""
Get one or more tiles (subblocks) bounding boxes (pyramid=0) for the specified dimensions.
For mosaic files.
Parameters
----------
kwargs
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").
R = 4 # The R-dimension ("rotation").
S = 5 # The S-dimension ("scene").
I = 6 # The I-dimension ("illumination").
H = 7 # The H-dimension ("phase").
V = 8 # The V-dimension ("view").
Returns
-------
dict[tile_info, bbox]
A dictionary with keys of type TileInfo and values of type BBox.
For a key, ie tle, of type Tile:
tle.dimension_coordinates = A dictionary of Dimensions for the tile
For a value, ie bbox, of type BBox:
bbox.x = The x coordinate of the upper left corner of the bounding box.
bbox.y = The y coordinate of the upper left corner of the bounding box.
bbox.w = The width of the bounding box.
bbox.h = The height of the bounding box.
"""
plane_constraints = self._get_coords_from_kwargs(kwargs)
# no m_index parameter
return self.reader.read_all_mosaic_tile_bounding_boxes(plane_constraints)
[docs]
def get_all_mosaic_scene_bounding_boxes(self):
"""
Get the scene (subblocks) bounding boxes (pyramid=0) for the specified dimensions.
For mosaic files.
Returns
-------
dict[int, bbox]
A dictionary with keys of type Int and values of type BBox.
The integer key is the Scene Index.
For a value, ie bbox, of type BBox:
bbox.x = The x coordinate of the upper left corner of the bounding box.
bbox.y = The y coordinate of the upper left corner of the bounding box.
bbox.w = The width of the bounding box.
bbox.h = The height of the bounding box.
"""
return self.reader.read_all_mosaic_scene_bounding_boxes()
[docs]
def get_mosaic_bounding_box(self):
"""
Get the bounding box for the entire mosaic image.
Returns
-------
bbox
A Bounding Box, bbox, of type BBox.
bbox.x = The x coordinate of the upper left corner of the bounding box.
bbox.y = The y coordinate of the upper left corner of the bounding box.
bbox.w = The width of the bounding box.
bbox.h = The height of the bounding box.
"""
return self.reader.read_mosaic_bounding_box()
@property
def size(self):
"""
This returns the Size of each dimension in the dims string. So if S had valid indexes of [0, 1, 2, 3, 4]
the returned tuple would have a value of 5 in the same position as the S occurs in the dims string.
Returns
-------
tuple
a tuple of dimension sizes. If the data has inconsistent shape the list will only contain -1 values and
the user needs to use dims_shape() to get the indexes.
"""
return tuple(self.reader.read_dims_sizes())
[docs]
def is_mosaic(self):
"""
Test if the loaded file is a mosaic file
Returns
-------
bool
True | False ie is this a mosaic file
"""
return self.reader.is_mosaic()
[docs]
@staticmethod
def convert_to_buffer(file: types.FileLike) -> Union[BinaryIO, np.ndarray]:
if isinstance(file, (str, Path)):
# This will both fully expand and enforce that the filepath exists
f = Path(file).expanduser().resolve(strict=True)
# This will check if the above enforced filepath is a directory
if f.is_dir():
raise IsADirectoryError(f)
return open(f, "rb")
# Convert bytes
elif isinstance(file, bytes):
return io.BytesIO(file)
# Set bytes
elif isinstance(file, (io.BytesIO, io.BufferedReader, io.IOBase, np.ndarray)):
return file
# Raise
else:
raise TypeError(
f"Reader only accepts types: [str, pathlib.Path, bytes, io.BytesIO, io.IOBase], received: {type(file)}"
)
@property
def meta(self):
"""
Extract the metadata block from the czi file.
Returns
-------
xml.etree.ElementTree.Element
The root element of the metadata tree
"""
if self.meta_root is None:
meta_str = self.reader.read_meta()
self.meta_root = ET.fromstring(meta_str)
return self.meta_root
[docs]
def read_image(self, **kwargs):
"""
Read the subblocks in the CZI file and for any subblocks that match all the constraints in kwargs return
that data. This allows you to select channels/scenes/time-points/Z-slices etc. Note if passed a BGR image
then the dims of the object will returned by this function and the implicit BGR will be expanded into an
A dimension. A is samples per pixel and will only be present for BGR images. This is logically more consistent
than mixing the samples into the channels as was done before aicspylibczi-3.0.0.
Parameters
----------
**kwargs
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").
R = 4 # The R-dimension ("rotation").
S = 5 # The S-dimension ("scene").
I = 6 # The I-dimension ("illumination").
H = 7 # The H-dimension ("phase").
V = 8 # The V-dimension ("view").
M = 10 # The M_index, this is only valid for Mosaic files!
Specify the number of cores to use for multithreading with cores.
cores = 3 # use 3 cores for threaded reading of the image.
Returns
-------
(numpy.ndarray, [Dimension, Size])
a tuple of (numpy.ndarray, a list of (Dimension, size)) the second element of the tuple is to make
sure the numpy.ndarray is interpretable. An example of the list is
[('S', 1), ('T', 1), ('C', 2), ('Z', 25), ('Y', 1024), ('X', 1024)]
so if you probed the numpy.ndarray with .shape you would get (1, 1, 2, 25, 1024, 1024).
Notes
-----
The M Dimension is a representation of the m_index used inside libCZI. Unfortunately this can be sparsely
packed for a given selection which causes problems when indexing memory. Consequently the M Dimension may
not match the m_index that is being used in libCZI or displayed in Zeiss' Zen software.
"""
plane_constraints = self._get_coords_from_kwargs(kwargs)
m_index = self._get_m_index_from_kwargs(kwargs)
cores = self._get_cores_from_kwargs(kwargs)
image, shape = self.reader.read_selected(plane_constraints, m_index, cores)
return image, shape
[docs]
def read_mosaic(
self,
region: Tuple = None,
scale_factor: float = 1.0,
background_color: Tuple = None,
**kwargs,
):
"""
Reads a mosaic file and returns an image corresponding to the specified dimensions. If the file is more than
a 2D sheet of pixels, meaning only one channel, z-slice, time-index, etc then the kwargs must specify the
dimension with more than one possible value.
**Example:** Read in the C=1 channel of a mosaic file at 1/10th the size
czi = CziFile(filename)
img = czi.read_mosaic(scale_factor=0.1, C=1)
Parameters
----------
region
A bounding box specifying the extraction box (x0, y0, width, height) specified in pixels
scale_factor
The amount to scale the data by, 0.1 would mean an image 1/10 the height and width of native, if you
get distortions it seems to be due to a bug in Zeiss's libCZI I'm trying to track it down but for now
if you use scale_factor=1.0 it should work properly.
background_color
Background color used when pixel is outside of a sublock. If omitted, it defaults to black
(r,g,b)=(0.0,0.0,0.0). Each color component is a float value between 0.0 and 1.0.
kwargs
The keywords below allow you to specify the dimension plane that constrains the 2D data. If the
constraints are underspecified the function will fail. ::
Z = 1 # The Z-dimension.
C = 2 # The C-dimension ("channel").
T = 3 # The T-dimension ("time").
R = 4 # The R-dimension ("rotation").
S = 5 # The S-dimension ("scene").
I = 6 # The I-dimension ("illumination").
H = 7 # The H-dimension ("phase").
V = 8 # The V-dimension ("view").
Returns
-------
numpy.ndarray
(1, height, width)
"""
plane_constraints = self._get_coords_from_kwargs(kwargs)
if region is None:
region = self.czilib.BBox()
region.w = -1
region.h = -1
else:
assert len(region) == 4
tmp = self.czilib.BBox()
tmp.x = region[0]
tmp.y = region[1]
tmp.w = region[2]
tmp.h = region[3]
region = tmp
if background_color is None:
background_color = self.czilib.RgbFloat()
background_color.r = 0.0
background_color.g = 0.0
background_color.b = 0.0
else:
assert len(background_color) == 3
tmp = self.czilib.RgbFloat()
tmp.r = background_color[0]
tmp.g = background_color[1]
tmp.b = background_color[2]
background_color = tmp
img = self.reader.read_mosaic(
plane_constraints, scale_factor, region, background_color
)
return img
def _get_coords_from_kwargs(self, kwargs):
plane_constraints = self.czilib.DimCoord()
[
plane_constraints.set_dim(k, v)
for (k, v) in kwargs.items()
if k in CziFile.ZISRAW_DIMS
]
return plane_constraints
def _get_m_index_from_kwargs(self, kwargs):
m_index = -1
if "M" in kwargs:
if not self.is_mosaic():
raise self.czilib.PylibCZI_CDimCoordinatesOverspecifiedException(
"M Dimension is specified but the file is not a mosaic file!"
)
m_index = kwargs.get("M")
return m_index
@staticmethod
def _get_cores_from_kwargs(kwargs):
cores = multiprocessing.cpu_count() - 1
if "cores" in kwargs:
cores = kwargs.get("cores")
return cores