Newer
Older
from abc import ABC, abstractmethod
import os
from pathlib import Path

Christopher Randolph Rhodes
committed
from typing import Dict

Christopher Randolph Rhodes
committed
import numpy as np

Christopher Randolph Rhodes
committed
import czifile
import tifffile

Christopher Randolph Rhodes
committed
class GenericImageDataAccessor(ABC):
@abstractmethod
def __init__(self):

Christopher Randolph Rhodes
committed
"""
Abstract base class that exposes an interfaces for image data, irrespective of whether it is instantiated

Christopher Randolph Rhodes
committed
from file I/O or other means. Enforces X, Y, C, Z dimensions in that order.

Christopher Randolph Rhodes
committed
"""

Christopher Randolph Rhodes
committed
@property
def chroma(self):
return self.shape_dict['C']

Christopher Randolph Rhodes
committed
@staticmethod
def conform_data(data):

Christopher Randolph Rhodes
committed
if len(data.shape) > 4 or (0 in data.shape):

Christopher Randolph Rhodes
committed
raise DataShapeError(f'Cannot handle image with dimensions other than X, Y, C, and Z: {data.shape}')
ones = [1 for i in range(0, 4 - len(data.shape))]
return data.reshape(*data.shape, *ones)

Christopher Randolph Rhodes
committed
def is_3d(self):
return True if self.shape_dict['Z'] > 1 else False
if self._data.dtype == 'bool':
return True
elif self._data.dtype == 'uint8':
unique = np.unique(self._data)
if unique.shape[0] == 2 and np.all(unique == [0, 255]):
return True
return False
def get_one_channel_data (self, channel: int):

Christopher Randolph Rhodes
committed
c = int(channel)
return InMemoryDataAccessor(self.data[:, :, c:(c+1), :])

Christopher Randolph Rhodes
committed
@property
def pixel_scale_in_micrometers(self):
return {}
@property
def dtype(self):
return self.data.dtype
@property
def hw(self):
"""
Get data height and width as a tuple
:return: tuple of (Y, X) dimensions
"""
return self.shape_dict['Y'], self.shape_dict['X']
@property
def nz(self):
return self.shape_dict['Z']

Christopher Randolph Rhodes
committed
def data(self):

Christopher Randolph Rhodes
committed
"""
Return data as 4d with axes in order of Y, X, C, Z
:return: np.ndarray
"""

Christopher Randolph Rhodes
committed

Christopher Randolph Rhodes
committed
@property
def shape(self):
return self._data.shape

Christopher Randolph Rhodes
committed
@property
def shape_dict(self):

Christopher Randolph Rhodes
committed
return dict(zip(('Y', 'X', 'C', 'Z'), self.data.shape))

Christopher Randolph Rhodes
committed

Christopher Randolph Rhodes
committed
class InMemoryDataAccessor(GenericImageDataAccessor):
def __init__(self, data):

Christopher Randolph Rhodes
committed
self._data = self.conform_data(data)

Christopher Randolph Rhodes
committed

Christopher Randolph Rhodes
committed
class GenericImageFileAccessor(GenericImageDataAccessor): # image data is loaded from a file
def __init__(self, fpath: Path):

Christopher Randolph Rhodes
committed
"""
Interface for image data that originates in an image file
:param fpath: absolute path to image file
:param kwargs: variable-length keyword arguments
"""
if not os.path.exists(fpath):
raise FileAccessorError(f'Could not find file at {fpath}')
class TifSingleSeriesFileAccessor(GenericImageFileAccessor):
def __init__(self, fpath: Path):
super().__init__(fpath)
try:
tf = tifffile.TiffFile(fpath)
self.tf = tf
except Exception:
FileAccessorError(f'Unable to access data in {fpath}')
if len(tf.series) != 1:
raise DataShapeError(f'Expect only one series in {fpath}')
se = tf.series[0]
order = ['Y', 'X', 'C', 'Z']
axs = [a for a in se.axes if a in order]
da = se.asarray()
if 'C' not in axs:
axs.append('C')
da = np.expand_dims(da, len(da.shape))
da,
[axs.index(k) for k in order],
[0, 1, 2, 3]
)
self._data = self.conform_data(yxcz.reshape(yxcz.shape[0:4]))
def __del__(self):
self.tf.close()
class PngFileAccessor(GenericImageFileAccessor):
def __init__(self, fpath: Path):
super().__init__(fpath)
try:
arr = imread(fpath)
except Exception:
FileAccessorError(f'Unable to access data in {fpath}')
if len(arr.shape) == 3: # rgb
self._data = np.expand_dims(arr, 3)
else: # mono
self._data = np.expand_dims(arr, (2, 3))
class CziImageFileAccessor(GenericImageFileAccessor):

Christopher Randolph Rhodes
committed
"""
Image that is stored in a Zeiss .CZI file; may be multi-channel, and/or a z-stack,
but not a time series or multiposition acquisition.
"""
def __init__(self, fpath: Path):
try:
cf = czifile.CziFile(fpath)

Christopher Randolph Rhodes
committed
self.czifile = cf
except Exception:
raise FileAccessorError(f'Unable to access CZI data in {fpath}')
sd = {ch: cf.shape[cf.axes.index(ch)] for ch in cf.axes}

Christopher Randolph Rhodes
committed
if (sd.get('S') and (sd['S'] > 1)) or (sd.get('T') and (sd['T'] > 1)):

Christopher Randolph Rhodes
committed
raise DataShapeError(f'Cannot handle image with multiple positions or time points: {sd}')

Christopher Randolph Rhodes
committed
idx = {k: sd[k] for k in ['Y', 'X', 'C', 'Z']}
yxcz = np.moveaxis(
cf.asarray(),
[cf.axes.index(ch) for ch in idx],
[0, 1, 2, 3]
)
self._data = self.conform_data(yxcz.reshape(yxcz.shape[0:4]))

Christopher Randolph Rhodes
committed
def __del__(self):
self.czifile.close()
@property
def pixel_scale_in_micrometers(self):
scale_meta = self.czifile.metadata(raw=False)['ImageDocument']['Metadata']['Scaling']['Items']['Distance']
sc = {}
for m in scale_meta:
if m['DefaultUnitFormat'].encode() == b'\xc2\xb5m' and m['Id'] in self.shape_dict.keys(): # literal mu-m
sc[m['Id']] = m['Value'] * 1e6
return sc
def write_accessor_data_to_file(fpath: Path, accessor: GenericImageDataAccessor, mkdir=True) -> bool:
if mkdir:
fpath.parent.mkdir(parents=True, exist_ok=True)

Christopher Randolph Rhodes
committed
try:

Christopher Randolph Rhodes
committed
zcyx= np.moveaxis(
accessor.data, # yxcz
[3, 2, 0, 1],

Christopher Randolph Rhodes
committed
[0, 1, 2, 3]
)
if accessor.is_mask():
tifffile.imwrite(fpath, zcyx.astype('uint8'), imagej=True)
else:
tifffile.imwrite(fpath, zcyx, imagej=True)

Christopher Randolph Rhodes
committed
except:
raise FileWriteError(f'Unable to write data to file')
return True
def generate_file_accessor(fpath):
if str(fpath).upper().endswith('.TIF') or str(fpath).upper().endswith('.TIFF'):
return TifSingleSeriesFileAccessor(fpath)
elif str(fpath).upper().endswith('.CZI'):
return CziImageFileAccessor(fpath)
elif str(fpath).upper().endswith('.PNG'):
return PngFileAccessor(fpath)
else:
raise FileAccessorError(f'Could not match a file accessor with {fpath}')
class Error(Exception):
class FileAccessorError(Error):
class FileNotFoundError(Error):

Christopher Randolph Rhodes
committed
class DataShapeError(Error):
class FileWriteError(Error):

Christopher Randolph Rhodes
committed
pass
class InvalidAxisKey(Error):
pass
class InvalidDataShape(Error):