Source code for ClearMap.IO.TIF

# -*- coding: utf-8 -*-
"""
TIF module provides interface to read and write tif image files.

Note
----
This modules relies on the tifffile library.
"""
__author__    = 'Christoph Kirst <christoph.kirst.ck@gmail.com>'
__license__   = 'GPLv3 - GNU General Pulic License v3 (see LICENSE.txt)'
__copyright__ = 'Copyright © 2020 by Christoph Kirst'
__webpage__   = 'http://idisco.info'
__download__  = 'http://www.github.com/ChristophKirst/ClearMap2'

import numpy as np
import tifffile as tif

import ClearMap.IO.Source as src
import ClearMap.IO.Slice as slc

from ClearMap.Utils.Lazy import lazyattr


###############################################################################
### Source class
###############################################################################

[docs] class Source(src.Source): """Class to handle a tif file source Note ---- Its assumed that the image data is stored in a series of the tif file. """ def __init__(self, location, series = 0, multi_file = False): try: self._tif = tif.TiffFile(location, multifile=multi_file) except TypeError: # TODO: filter with message self._tif = tif.TiffFile(location, _multifile=multi_file) # Versions >= 2020.9.30 self._series = series self.multi_file = multi_file @property def name(self): return "Tif-Source" @lazyattr def series(self): return self._tif.series[self._series] @property def shape(self): return shape_from_tif(self.tif_shape) @property def tif_shape(self): if self._tif._multifile: return self._tif.series[self._series].shape else: s = self._tif.pages[0].shape l = len(self._tif.pages) if l > 1: s = (l,) + s return s @property def dtype(self): return self._tif.pages[0].dtype @property def location(self): return self._tif._fh.path @location.setter def location(self, value): if value != self.location: self._tif = tif.TiffFile(value, multifile = False) @property def array(self, processes = None): array = self._tif.asarray(maxworkers=processes) return array_from_tif(array) @property def element_strides(self): """The strides of the array elements. Returns ------- strides : tuple Strides of the array elements. Note ---- The strides of the elements module itemsize instead of bytes. """ memmap = self.as_memmap() return tuple(s // memmap.itemsize for s in memmap.strides) def __getitem__(self, slicing, processes=None): ndim = self.ndim if ndim >= 3: slicing = slc.unpack_slicing(slicing, ndim) slicing_z = slicing[-1] if isinstance(slicing_z, (int, np.int64)): slicing_z = int(slicing_z) array = self._tif.asarray(key=slicing_z, maxworkers=processes) array = array_from_tif(array) slicing_xy = (Ellipsis,) + slicing[-3:-1] if len(array.shape) > len(self._tif.pages[0].shape): slicing_xy = slicing_xy + (slice(None),) return array[slicing_xy] else: array = self._tif.asarray(maxworkers=processes) array = array_from_tif(array) return array.__getitem__(slicing) def __setitem__(self, *args): memmap = self.as_memmap() memmap.__setitem__(*args)
[docs] def metadata(self, info = None): """Returns metadata from this tif file. Arguments --------- source : str or Source The filename or data source. info : list or all Optional list of keywords, if all return full tif metadata, if None return default set info. Returns ------- metadata : dict Dictionary with the meta data. """ md = None for t in self._tif.flags: if hasattr(self._tif, t + '_metadata'): md = getattr(self._tif, t + '_metadata') if md is not None: break if md is None: md = {} if info is all: return md elif info is None: info = ['shape', 'resolution', 'overlap'] elif isinstance(info, str): info = [info] info = {k : None for k in info} def update_info(info, name, keys, mdict, astype, include_keys=False): value = [] for k in keys: try: v = mdict for kk in k.split('.'): v = v.get(kk, None) if v is None: break if include_keys and v is not None: info[k] = v value.append(astype(v)) except Exception: pass if len(value) > 0: info[name] = tuple(value) #get info mdp = md.get('Image', {}).get('Pixels', {}) keys = info.keys() if 'shape' in keys: #info['shape'] = self.shape; order = mdp.get('DimensionOrder', None) if order is None: order = ''.join([d for d in 'XYZTC' if 'Size' + d in mdp.keys()]) info['order'] = order skeys = ['Size' + d for d in order] update_info(info, 'shape', skeys, mdp, int) if 'description' in keys: info['description'] = self._tif.pages[0].description if 'resolution' in keys: rkeys = ['PhysicalSizeX', 'PhysicalSizeY', 'PhysicalSizeZ'] update_info(info, 'resolution', rkeys, mdp, float) if 'overlap' in keys: mdc = md.get('CustomAttributes', {}).get('PropArray',{}) okeys = ['xyz-Table_X_Overlap.Value', 'xyz-Table_Y_Overlap.Value'] update_info(info, 'overlap', okeys, mdc, float) return info
[docs] def as_memmap(self): try : return array_from_tif(tif.memmap(self.location)) except: raise ValueError('The tif file %s cannot be memmaped!' % self.location)
[docs] def as_virtual(self): return VirtualSource(source = self)
[docs] def as_real(self): return self
[docs] def as_buffer(self): return self.as_memmap()
### Formatting def __str__(self): try: name = self.name name = '%s' % name if name is not None else '' except: name ='' try: shape = self.shape shape ='%r' % ((shape,)) if shape is not None else '' except: shape = '' try: dtype = self.dtype dtype = '[%s]' % dtype if dtype is not None else '' except: dtype = '' try: order = self.order order = '|%s|' % order if order is not None else '' except: order = '' try: location = self.location location = '%s' % location if location is not None else '' if len(location) > 100: location = location[:50] + '...' + location[-50:] if len(location) > 0: location = '{%s}' % location except: location = '' return name + shape + dtype + order + location
[docs] class VirtualSource(src.VirtualSource): def __init__(self, source = None, shape = None, dtype = None, order = None, location = None, name = None): super(VirtualSource, self).__init__(source=source, shape=shape, dtype=dtype, order=order, location=location, name=name) if isinstance(source, Source): self.multi_file = source.multi_file self.series = source._series @property def name(self): return 'Virtual-Tif-Source'
[docs] def as_virtual(self): return self
[docs] def as_real(self): return Source(location=self.location, series=self.series, multi_file=self.multi_file)
[docs] def as_buffer(self): return self.as_real().as_buffer()
############################################################################### ### IO functionality ###############################################################################
[docs] def is_tif(source): """Checks if this source a TIF source""" if isinstance(source, Source): return True if isinstance(source, str): try: Source(source) except: # FIXME: too broad return False return True return False
[docs] def read(source, slicing = None, sink = None, **args): """Read data from a tif file. Arguments --------- source : str or TIF class The name of the tif file or a TIF class. slicing : slice, Slice or None An optional sub-slice to consider. Returns ------- data : array The image data in the tif file. """ if not isinstance(source, Source): source = Source(source) if slicing is None: return source.array else: return source.__getitem__(slicing)
[docs] def write(sink, data, **args): """Write data to a tif file Arguments --------- sink : str The name of the tif file. Returns ------- sink : str The name of the tif file. """ tif.imsave(sink, array_to_tif(data), **args) return sink
[docs] def create(location = None, shape = None, dtype = None, mode = None, as_source = True, **kwargs): """Create a memory map. Arguments --------- location : str The filename of the memory mapped array. shape : tuple or None The shape of the memory map to create. dtype : dtype The data type of the memory map. mode : 'r', 'w', 'w+', None The mode to open the memory map. as_source : bool If True, return as Source class. Returns ------- memmap : np.memmap The memory map. Note ---- By default memmaps are initialized as fortran contiguous if order is None. """ if shape is None: raise ValueError('Shape for new tif file must be given!') shape = shape_to_tif(shape) mode = 'r+' if mode == 'w+' or mode is None else mode dtype = 'float64' if dtype is None else dtype memmap = tif.memmap(filename=location, shape=shape, dtype=dtype, mode=mode) if as_source: return Source(location) else: return memmap
################################################################################ #### Array axes order ################################################################################
[docs] def shape_from_tif(shape): ndim = len(shape) shape = shape[:max(0, ndim-3)] + shape[-3:][::-1] # FIXME: rewrite for color and use in array_from_tiff too return shape
[docs] def shape_to_tif(shape): return shape_from_tif(shape)
[docs] def array_from_tif(array): ndim = array.ndim n_colors = min(array.shape) # valid if rgb if ndim == 4 and n_colors in (3, 4): # Put color last for RGB images col_idx = array.shape.index(n_colors) new_axes = list(range(ndim)) new_axes.pop(col_idx) new_axes = new_axes[::-1] new_axes.append(col_idx) array = np.transpose(array, new_axes) else: axes = list(range(ndim)) axes = axes[:max(0, ndim-3)] + axes[-3:][::-1] array = array.transpose(axes) return array
[docs] def array_to_tif(array): return array_from_tif(array)
################################################################################ #### Meta data ################################################################################ # # # #def change_OME_meta_data_string(description, info = None): # """Changes the meta data in an ome image descriptor # # Arguments: # description (str): xml ome image description # info (dict): dictionary of entries to try to change # # Returns: # str: modified xml image descriptor # """ # if not isinstance(info, dict): # return description; # # try: # xml = etree.fromstring(description); # except: # raise RuntimeError('could not parse ome xml description!'); # # keys = info.keys(); # # if 'overlap' in keys: # try: # #get the overlap # overlap = info['overlap']; # ex = [x for x in xml.iter('{*}xyz-Table_X_Overlap')][0]; # ey = [x for x in xml.iter('{*}xyz-Table_Y_Overlap')][0]; # ex.attrib['Value'] = str(overlap[0]); # ey.attrib['Value'] = str(overlap[1]); # except: # raise RuntimeWarning('could not change overlap in ome image description'); # # #add other meta data keys here # # return etree.tostring(xml, pretty_print = False); ################################################################################ #### Tests ################################################################################ def _test(): import ClearMap.Tests.Files as tfs import ClearMap.IO.TIF as tif filename = tfs.filename('tif_2d') t = tif.Source(location = filename) print(t) filename = tfs.filename('tif_2d_color') t = tif.Source(location = filename) print(t) d = tif.read(filename) print(d.shape) v = t.as_virtual() print(v) q = v.as_real() print(q)