Source code for timagetk.components.metadata

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
#  Copyright (c) 2022 Univ. Lyon, ENS de Lyon, UCB Lyon 1, CNRS, INRAe, Inria
#  All rights reserved.
#  This file is part of the TimageTK library, and is released under the "GPLv3"
#  license. Please see the LICENSE.md file that should have been included as
#  part of this package.
# ------------------------------------------------------------------------------

"""Metadata functionnalities.

Use the function to add parameters to metadata when performing image modifications.

Metadata specifications:

 - Use 'timagetk' as the metadata main root key to store information
 - Use 'class' key for class information
 - Use a counter to sort operation orders
 - Under each number save a dictionary with details of the modification or
   about algorithm applied, such as its name and their parameters or the name of
   the transformation (or even the transformation if linear!).

Examples
--------
>>> import numpy as np
>>> from timagetk.algorithms.resample import isometric_resampling
>>> from timagetk import SpatialImage
>>> test_array = np.ones((5,5,10), dtype=np.uint8)
>>> image_1 = SpatialImage(test_array, voxelsize=[1., 1., 2.])
>>> image_iso = isometric_resampling(image_1)
>>> image_iso.metadata['timagetk']
{(1, 'resample_isotropic'):
    {'params': {},
    'called':
        {(1, 'resample'):
            {'params':
                {'image': ['', ''],
                'option': 'gray',
                'voxelsize': [1.0, 1.0, 1.0]},
            'called':
                {(1, 'apply_trsf'):
                    {'dtype': None,
                    'image': ['', ''],
                    'param_str_1': '-linear -template-dim 5 5 20 -template-voxel 1.0 1.0 1.0',
                    'param_str_2': ' -resize -interpolation linear',
                    'template_img': ['', ''],
                    'trsf': None}
                }
            }
        }
    }
}

"""

import os
import sys

import numpy as np

from timagetk.bin.logger import get_logger
from timagetk.components.trsf import Trsf
from timagetk.util import clean_type
from timagetk.util import compute_extent

log = get_logger(__name__)


[docs] def get_func_name(level=1): """Get the name of the function it is embedded in. Parameters ---------- level : int, optional Stack level, set to 1 to get the name of the function where ``get_func_name`` is embedded in, or more to go higher in the stack. Returns ------- str Name of the function """ # https://stackoverflow.com/questions/251464/how-to-get-a-function-name-as-a-string-in-python func_name = sys._getframe(level).f_code.co_name if func_name.startswith('<') and func_name.endswith('>'): return None else: return sys._getframe(level).f_code.co_name
[docs] def get_func_params(level=1): """Get the parameters of the function it is embedded in. Parameters ---------- level : int, optional Stack level, set to 1 to get the name of the function where ``get_func_name`` is embedded in, or more to go higher in the stack. Returns ------- dict Dictionary with the name of the parameters and their values at given ``level``. """ # https://stackoverflow.com/questions/251464/how-to-get-a-function-name-as-a-string-in-python frame = sys._getframe(level) var_names = get_func_params_names_from_frame(frame) # - Make a dictionary of name and values: params = {name: get_func_param_value_from_frame(frame, name) for name in var_names if name not in ['verbose']} return params
[docs] def get_func_params_names_from_frame(frame): """Get the parameters' name of the function in the given ``frame``. Parameters ---------- frame Returns ------- str Name of the function """ # https://stackoverflow.com/questions/251464/how-to-get-a-function-name-as-a-string-in-python fc = frame.f_code return fc.co_varnames[:fc.co_argcount]
[docs] def get_func_param_value_from_frame(my_frame, v_name): """Get the value of a variable in the given ``frame``. Notes ----- If a ``SpatialImage`` instance is found, we return the metadata 'filepath'/'filename' instead of the whole object! If a ``Trsf`` is found, and it is linear, we return the transformation matrix as a numpy array. Parameters ---------- my_frame : frame Frame with a variable ``v_name`` v_name : str Name of a variable in the given frame ``my_frame`` Returns ------- any Value of the variable """ from timagetk.components.spatial_image import SpatialImage # https://stackoverflow.com/questions/251464/how-to-get-a-function-name-as-a-string-in-python val = my_frame.f_locals.get(v_name) # - When a SpatialImage is given, do not return the whole object but its 'filename' & 'filepath': if isinstance(val, SpatialImage): stype = clean_type(val) fun_name = my_frame.f_code.co_name # - Try to get the path: try: filepath = val.filepath except AttributeError: msg = "Could not find a 'filepath' in {} given to ``{}``!" log.warning(msg.format(stype, fun_name)) filepath = "unknown" # - Try to get the name: try: filename = val.filename except AttributeError: msg = "Could not find a 'filename' in {} given to ``{}``!" log.warning(msg.format(stype, fun_name)) filename = "unknown" # - Join the path and name: val = { 'path': os.path.join(filepath, filename), 'titk_md': val.metadata.get('timagetk', None), } # - When a Trsf is given and it is linear, return the matrix as an array: if isinstance(val, Trsf) and val.is_linear(): val = val.copy_to_array() return val
[docs] def get_last_id(md_dict): """Get the last id in the metadata dictionary. Parameters ---------- md_dict : dict Metadata dictionary Returns ------- int Last id if found else 1 """ ids = md_dict.keys() try: ids.remove('class') except ValueError: warn_msg = "Initializing from a metadata dictionary without 'class' entry in 'timagetk' metadata!" log.debug(warn_msg) if ids == []: msg = "Initialising from a 'timagetk' metadata dict with no keys!" log.debug(msg) return 0 lid = sorted([i[0] for i in ids])[-1] if lid == 0: return 0 else: return lid
[docs] def single_fn_md(lid, fname, fpars): """Metadata format when using low level-function. Use it when no higher level function is known. Parameters ---------- lid : int Last id fname : str Name of the function fpars : dict Parameter dictionary associated to function call Returns ------- dict {(lid+1, fname): fpars} """ return {(lid + 1, fname): fpars}
[docs] def called_fn_md(lid, hfname, fname, fpars): """Metadata format when using function calling others sub-processes. Parameters ---------- lid : int Last id hfname : str Name of the function of higher level calling the function fname : str Name of the function fpars : dict Parameter dictionary associated to function call Returns ------- dict {(lid+1, hfname): {'params': {}, 'called': {(1, fname): fpars}}} """ return {(lid + 1, hfname): {'params': {}, 'called': {(1, fname): fpars}}}
[docs] def add2md(image, extra_params=None, from_level=2): """Add parameters to 'timagetk' metadata attribute of given image. Parameters ---------- image : timagetk.SpatialImage or timagetk.LabelledImage or timagetk.TissueImage2D or timagetk.TissueImage3D Image with a metadata attribute extra_params : dict, optional Dictionary of additional parameter to save to the metadata. from_level : int ??? Returns ------- image : timagetk.SpatialImage or timagetk.LabelledImage or timagetk.TissueImage2D or timagetk.TissueImage3D Image with updated metadata dictionary """ # - Get the metadata dictionary: try: md_dict = image.metadata['timagetk'] except KeyError: # TODO: change this to error later warn_msg = "No 'timagetk' entry in '{}' instance metadata!" log.debug(warn_msg.format(clean_type(image))) md_dict = {} image.metadata.update({'timagetk': md_dict}) except AttributeError: raise TypeError("No metadata attribute is found!") # - Get the function name: fun_name = get_func_name(level=from_level) log.debug("Found function name: '{}'".format(fun_name)) # - Create new metadata from function name and used parameters: fun_param = get_func_params(level=from_level) log.debug("Found function parameters: '{}'".format(fun_param)) if extra_params is not None: # - Do not save 'verbose' kwarg: try: extra_params.pop('verbose') except KeyError: pass # - Update the function parameters dict: fun_param.update(extra_params) # - Find the last id: lid = get_last_id(md_dict) log.debug("Got last id: {}".format(lid)) higher_fun_name = get_func_name(level=from_level + 1) md = {} if higher_fun_name is None: log.debug( "No higher level function calling '{}'!".format(higher_fun_name)) if (lid, fun_name) in md_dict and md_dict[(lid, fun_name)]['params'] == {}: msg = "Updating 'params' for ({}, {})." log.debug(msg.format(lid, fun_name)) image.metadata['timagetk'][(lid, fun_name)][ 'params'] = fun_param # update params else: # - Low-level function case: md = single_fn_md(lid, fun_name, fun_param) else: if (lid, higher_fun_name) in md_dict: msg = "Appending a new sub-process '{}' called by '{}'." log.debug(msg.format(fun_name, higher_fun_name)) # - Case where same ``higher_fun_name`` call multiple sub-processes: assert md_dict[(lid, higher_fun_name)]['params'] == {} # Need to know the last id in already 'called' processes to update properly: called_md = md_dict[(lid, higher_fun_name)]['called'] clid = get_last_id(called_md) md = called_md.update(single_fn_md(clid, fun_name, fun_param)) elif (lid, fun_name) in md_dict: # - Case where a higher level function already called other processes that are registered if md_dict[(lid, fun_name)]['params'] == {}: msg = "Updating 'params' for ({}, {})." log.debug(msg.format(lid, fun_name)) image.metadata['timagetk'][(lid, fun_name)][ 'params'] = fun_param # update params msg = "Moving '{}' as called by '{}'!" log.debug(msg.format(fun_name, higher_fun_name)) sub_md = md_dict.pop((lid, fun_name)) md = {(lid, higher_fun_name): {'params': {}, 'called': {(1, fun_name): sub_md}}} else: msg = "Registering a new sub-process '{}' called by '{}'." log.debug(msg.format(fun_name, higher_fun_name)) # - Case where a new process is registered: md = called_fn_md(lid, higher_fun_name, fun_name, fun_param) if md != {} and md is not None: log.debug("Updating metadata with following dict:\n{}".format(md)) # - Update the 'timagetk' metadata: image._metadata['timagetk'].update(md) log.debug("New metadata:\n{}\n".format(image.metadata['timagetk'])) return image
[docs] def sort_ops(md): """Return a list of sorted operations from metadata root. Parameters ---------- md : dict Metadata dictionary. Returns ------- list List of sorted operations from metadata root """ ops_tuple = md.keys() # Remove 'class' key, not referring to list of operations: if 'class' in ops_tuple: ops_tuple.remove('class') # TODO: should test that key are len-2 tuples? -> more generic filtering! if len(ops_tuple) == 0: return None elif len(ops_tuple) == 1: return [ops_tuple[0][1]] else: ops_id = [op[0] for op in ops_tuple] ops_name = [op[1] for op in ops_tuple] return ops_name[np.argsort(ops_id)]
[docs] def get_params_called(md, op_id, op_name): """Get 'params' and 'called' values in metadata dictionary. Parameters ---------- md : dict Metadata dictionary. op_id : int Sorting id op_name : str Name of the algorithm or function Returns ------- dict Dictionary of function parameters None or dict Dictionary of called sub-functions """ try: assert (op_id, op_name) in md except AssertionError: msg = "Unknown metadata entry '({}, {})'." raise KeyError(msg.format(op_id, op_name)) if md[(op_id, op_name)].has_key('params'): params = md[(op_id, op_name)]['params'] called = md[(op_id, op_name)]['called'] else: params = md[(op_id, op_name)] called = None return params, called
[docs] def md_str(op_id, op_max, op_name, params, called_md, indent_lvl, indent=' '): """ Parameters ---------- op_id op_max op_name params called_md indent_lvl indent Returns ------- """ s = "" ind = indent * indent_lvl if op_max == 1: s += "{}# {}\n".format(ind, op_name) else: s += "{}#{}. {}\n".format(ind, op_id, op_name) s_p = "{}*params:\n{}\n" if params == {}: s += s_p.format(ind + indent, ind + indent * 2 + 'EMPTY') else: s += s_p.format(ind + indent, print_params(params, indent_lvl)) if called_md is not None: s += "{}*called:\n".format(ind + indent) return s
[docs] class Metadata(object): """Basic metadata class. Notes ----- Take a dictionary as input and make object attributes out of it. """
[docs] def __init__(self, md=None): """Metadata constructor. Notes ----- Uni-dimensional numpy arrays are changed to list. Other type are unchanged. See example. Parameters ---------- md : dict, optional Metadata dictionary Examples -------- >>> import numpy as np >>> from timagetk.components.metadata import Metadata >>> md = {'origin': [0, 0], 'shape': (10, 10), 'voxelsize':np.array([0.2, 0.2])} >>> md = Metadata(md) >>> md.origin [0, 0] >>> md.shape (10, 10) >>> md.get_dict() {'origin': [0, 0], 'shape': (10, 10), 'voxelsize': [0.2, 0.2]} """ # - Declare hidden attribute: self._dict = {} # - Initialize the object if metadata is given: if md is not None: self._test_input_md(md) self._lazy_loading(md)
def _lazy_loading(self, md): """Set the attributes.""" for attr_name, attr_val in md.items(): # - uni-dimensional arrays to list: if isinstance(attr_val, np.ndarray) and attr_val.ndim == 1: attr_val = attr_val.tolist() # - Set the object attribute: setattr(self, attr_name, attr_val) # - Set the dict key and value self._dict[attr_name] = attr_val @staticmethod def _test_input_md(md): """Check the input is a dictionary.""" try: assert isinstance(md, dict) except AssertionError: raise TypeError("Metadata class input should be a dictionary!") @staticmethod def _empty_md(default_tags): """Initialise empty metadata dictionary. Parameters ---------- default_tags : list of str List of tag names Returns ------- dict The empty metadata dictionary """ n_tags = len(default_tags) return dict(zip(default_tags, [None] * n_tags)) def __getitem__(self, item): return self._dict[item] def __setitem__(self, key, value): self.update({key: value}) def pop(self, key): return self._dict.pop(key, None)
[docs] def get_dict(self): """Return the metadata dictionary.""" return dict(self._dict.items())
[docs] def get(self, k, default): """Get a metadata value.""" try: return self._dict[k] except KeyError: return default
[docs] def update(self, md): """Update the metadata. Parameters ---------- md : dict, optional Metadata dictionary to use for updating metadata attribute """ self._test_input_md(md) for k, v in md.items(): self._dict[k] = v setattr(self, k, v)
#: List of tags associated to the metadata of an image. IMAGE_MD_TAGS = [ 'shape', 'ndim', 'dtype', 'axes_order', 'origin', 'voxelsize', 'unit', 'acquisition_date'] #: Dictionary mapping the type of each metadata tag. IMAGE_MD_TYPES = { 'shape': 'list', 'ndim': 'int', 'dtype': 'str', 'axes_order': 'dict', 'origin': 'list', 'voxelsize': 'list', 'unit': 'str', 'extent': 'list', 'acquisition_date': 'str'}
[docs] class ImageMetadata(Metadata): """Metadata associated to ``SpatialImage`` attributes. With a ``numpy.ndarray`` get: 'shape', 'ndim', & 'dtype'. Also, you must provide 'origin' and 'voxelsize' as keyword arguments, otherwise they will be set to their default values: ``DEFAULT_ORIG_2D`` & ``DEFAULT_VXS_2D`` or ``DEFAULT_ORIG_3D`` & ``DEFAULT_VXS_3D`` according to dimensionality. With a ``SpatialImage`` instance or any other which inherit it, also get: 'voxelsize' & 'origin'. Attributes ---------- shape : list of int List of integers defining the shape of the image, *i.e.* its size in voxel ndim : int Number of dimension of the image, should be 2 or 3 dtype : numpy.dtype Bit depth and size of the image origin : list of int Coordinates of the origin of the image voxelsize : list of float Size of the voxel in each direction of the image unit : str, optional The size unit, in International System of Units (meters), associated to the image. Notes ----- Defined attributes for ``ImageMetadata`` are in ``IMAGE_MD_TAGS``. Attribute 'extent' is computed by ``timagetk.util.compute_extent()``. It is possible to update a metadata dictionary with the known tags (and 'extent') using ``self.update_metadata()``. """
[docs] def __init__(self, image, **kwargs): """ImageMetadata constructor. Parameters ---------- image : numpy.ndarray or timagetk.SpatialImage Image to use to define metadata image_md : dict Dictionary of metadata, may contain 'voxelsize' and/or 'origin' if using an array instead of a SpatialImage Other Parameters ---------------- axes_order : list, optional Physical axes ordering, defaults to ``YX`` in 2D or ``ZYX`` in 3D. origin : list, optional Coordinates of the origin in the image, defaults to ``[0, 0]`` in 2D or ``[0, 0, 0]`` in 3D. voxelsize : list, optional Image voxelsize, defaults to ``[1.0, 1.0]`` in 2D or ``[1.0, 1.0, 1.0]`` in 3D. unit : int or float, optional The size unit, in International System of Units (meters), associated to the image. See Also -------- timagetk.components.spatial_image.DEFAULT_AXES_2D timagetk.components.spatial_image.DEFAULT_AXES_3D timagetk.components.spatial_image.DEFAULT_VXS_2D timagetk.components.spatial_image.DEFAULT_VXS_3D timagetk.components.spatial_image.DEFAULT_ORIG_2D timagetk.components.spatial_image.DEFAULT_ORIG_3D timagetk.components.spatial_image.DEFAULT_SIZE_UNIT Notes ----- If image is an array, you must provide 'origin' and 'voxelsize' as keyword arguments, otherwise they will be set to their default values. Examples -------- >>> import numpy as np >>> from timagetk.components.metadata import ImageMetadata >>> # - Create a random array: >>> img = np.random.random_sample((15, 15)) >>> # - Do NOT specify 'voxelsize' & 'origin': >>> img_md = ImageMetadata(img) >>> # - Take a look at the obtained metadata: >>> img_md.get_dict() {'shape': (15, 15), 'ndim': 2, 'dtype': dtype('float64'), 'axes_order': 'YX', 'voxelsize': [1.0, 1.0], 'unit': 1e-06, 'origin': [0, 0], 'extent': [14.0, 14.0]} >>> # - Specify 'voxelsize' & 'unit': >>> md = {'voxelsize': [0.5, 0.5], 'unit': 1e-06} >>> img_md = ImageMetadata(img, **md) >>> img_md.get_dict() {'shape': (15, 15), 'ndim': 2, 'dtype': dtype('float64'), 'axes_order': 'YX', 'voxelsize': [0.5, 0.5], 'unit': 1e-06, 'origin': [0, 0], 'extent': [7.0, 7.0]} >>> # - Use a SpatialImage as input: >>> import numpy as np >>> from timagetk import SpatialImage >>> from timagetk.components.metadata import ImageMetadata >>> # - Create a random array: >>> img = np.random.random_sample((15, 15)) >>> img = SpatialImage(img) >>> # - Do NOT specify 'voxelsize' & 'origin': >>> img_md = ImageMetadata(img) """ from timagetk.components.spatial_image import SpatialImage # - Initialize Metadata class: Metadata.__init__(self, None) # - Declare attributes: self.ndim = None # int self.dtype = None # numpy.dtype self.shape = None # tuple(int) self.axes_order = None # list of str self.origin = None # list of int self.voxelsize = None # list of float self.extent = None # list of float self.unit = None # list of float self.acquisition_date = kwargs.get('acquisition_date', None) # - Define attribute values from input type: if isinstance(image, SpatialImage): self.get_from_image(image) elif isinstance(image, np.ndarray): axo = kwargs.get('axes_order', None) vxs = kwargs.get('voxelsize', None) ori = kwargs.get('origin', None) unit = kwargs.get('unit', None) self.get_from_array(image, voxelsize=vxs, origin=ori, axes_order=axo, unit=unit) else: msg = "Unknown input `image` type ({})!" raise TypeError(msg.format(clean_type(image)))
def _compute_extent(self): """Compute the extent of the image. Returns ------- list Extent of the image, *i.e.* its size in real units. See Also -------- timagetk.util.compute_extent """ return compute_extent(self.voxelsize, self.shape) def _get_default(self, param, default_value_2d, default_value_3d): """Set default value for origin or voxelsize. Parameters ---------- param : list or None List parameter to return the default value default_value_2d : list Length-2 array for 2D image case default_value_3d : list Length-3 array for 2D image case Returns ------- param The correct parameter value """ if param is None or param == []: if self.ndim == 2: param = default_value_2d elif self.ndim == 3: param = default_value_3d else: raise ValueError("Weird dimensionality: {}".format(self.ndim)) else: try: assert len(param) == self.ndim except AssertionError: msg = "Given parameter ({}) is {} than array dimensionality ({})!" if len(param) > self.ndim: raise ValueError(msg.format(param, 'bigger', self.ndim)) else: raise ValueError(msg.format(param, 'smaller', self.ndim)) return param
[docs] def get_from_array(self, array, voxelsize=None, origin=None, axes_order=None, unit=None): """Get image metadata values from an array. Parameters ---------- array : numpy.ndarray Array to use to get metadata voxelsize : list, optional Array voxelsize, if ``None`` (default) use the default values (either DEFAULT_VXS_2D or DEFAULT_VXS_3D) origin : list, optional Array origin, if ``None`` (default) use the default values (either DEFAULT_ORIG_2D or DEFAULT_ORIG_3D) Notes ----- Only 'shape', 'ndim' & 'type' information are accessible from a NumPy array! Directly update the metadata dictionary and attributes. Examples -------- >>> import numpy as np >>> from timagetk.components.metadata import ImageMetadata >>> # - Create a random array: >>> img = np.random.random_sample((15, 15)) >>> # - Do NOT specify 'voxelsize' & 'origin': >>> img_md = ImageMetadata(img) >>> # - Take a look at the obtained metadata: >>> img_md.get_dict() {'shape': (15, 15), 'ndim': 2, 'dtype': dtype('float64'), 'voxelsize': [1.0, 1.0], 'unit': 1e-06, 'origin': [0, 0], 'extent': [14.0, 14.0]} """ from timagetk.components.spatial_image import DEFAULT_AXES_2D from timagetk.components.spatial_image import DEFAULT_AXES_3D from timagetk.components.spatial_image import DEFAULT_ORIG_2D from timagetk.components.spatial_image import DEFAULT_ORIG_3D from timagetk.components.spatial_image import DEFAULT_SIZE_UNIT from timagetk.components.spatial_image import DEFAULT_VXS_2D from timagetk.components.spatial_image import DEFAULT_VXS_3D self.update({'shape': array.shape}) self.update({'ndim': array.ndim}) self.update({'dtype': array.dtype}) defval = self._get_default(axes_order, DEFAULT_AXES_2D, DEFAULT_AXES_3D) self.update({'axes_order': defval}) defval = self._get_default(voxelsize, DEFAULT_VXS_2D, DEFAULT_VXS_3D) self.update({'voxelsize': defval}) defval = unit if unit is not None else DEFAULT_SIZE_UNIT self.update({'unit': defval}) defval = self._get_default(origin, DEFAULT_ORIG_2D, DEFAULT_ORIG_3D) self.update({'origin': defval}) # - Update the Metadata to get 'extent' with 'self.get_dict()': self.update({'extent': self._compute_extent()}) return
[docs] def get_from_image(self, image): """Get image metadata values from a `SpatialImage` instance. Parameters ---------- image : timagetk.SpatialImage Image that contains the attributes to update the metadata. Notes ----- Directly update the metadata dictionary and attributes. Examples -------- >>> from timagetk.components.metadata import ImageMetadata >>> from timagetk.array_util import random_spatial_image >>> # Initialize a random (uint8) 3D SpatialImage: >>> img = random_spatial_image((3, 5, 5), voxelsize=[1., 0.5, 0.5]) >>> img_md = ImageMetadata(img) >>> # - Take a look at the obtained metadata: >>> img_md.get_dict() {'shape': (15, 15), 'ndim': 2, 'dtype': dtype('uint8'), 'voxelsize': [1.0, 1.0], 'unit': 1e-06, 'origin': [0, 0], 'extent': [14.0, 14.0]} """ from timagetk.components.spatial_image import DEFAULT_ORIG_2D from timagetk.components.spatial_image import DEFAULT_ORIG_3D from timagetk.components.spatial_image import DEFAULT_VXS_2D from timagetk.components.spatial_image import DEFAULT_VXS_3D self.update({'shape': image.shape}) self.update({'ndim': image.ndim}) self.update({'dtype': image.dtype}) self.update({'unit': image.unit}) self.update({'axes_order': image.axes_order}) val = self._get_default(image.voxelsize, DEFAULT_VXS_2D, DEFAULT_VXS_3D) self.update({'voxelsize': list(map(float, val))}) val = self._get_default(image.origin, DEFAULT_ORIG_2D, DEFAULT_ORIG_3D) self.update({'origin': val}) # - Update the Metadata to get 'extent' with 'self.get_dict()': self.update({'extent': self._compute_extent()}) return
[docs] def update_metadata(self, metadata): """Update a metadata dictionary with its basics image information. Parameters ---------- metadata : dict A metadata dictionary to compare to the object attributes Returns ------- dict A verified metadata dictionary """ # --- Update metadata with object value: # -------------------------------------- for attr in IMAGE_MD_TAGS + ['extent']: attr_val = getattr(self, attr) try: if IMAGE_MD_TYPES[attr] == 'list': np.testing.assert_array_almost_equal(metadata[attr], attr_val, decimal=6) else: assert metadata[attr] == attr_val except KeyError: # Case where attribute is not defined in metadata: add it metadata[attr] = attr_val except AssertionError: # Case where attribute and metadata value differ: if attr_val is not None: # update metadata msg = "Metadata '{}' {} do not match the image {} {}." log.debug(msg.format(attr, metadata[attr], attr, attr_val)) metadata[attr] = attr_val log.debug("--> updated!") return metadata
[docs] def similar_metadata(self, md): """Compare this ImageMetadata values to another one. Parameters ---------- md : dict or ImageMetadata Dictionary or image metadata to compare to self Returns ------- bool ``True`` if all metadata values are equal, else ``False``. Examples -------- >>> from timagetk.array_util import dummy_spatial_image_2D >>> im1 = dummy_spatial_image_2D([0.5, 0.5]) >>> im2 = dummy_spatial_image_2D([0.6, 0.6]) >>> im1.metadata_image.similar_metadata(im2.metadata_image) """ if isinstance(md, ImageMetadata): md = md.get_dict() return all(self._dict[tag] == md[tag] for tag in IMAGE_MD_TAGS)
[docs] class ProcessMetadata(object): """ """
[docs] def __init__(self, image): """ProcessMetadata constructor. Parameters ---------- image : timagetk.SpatialImage or timagetk.LabelledImage or timagetk.TissueImage2D or timagetk.TissueImage3D Image to attach processing metadata to. """ self._dict = {} # - Save the image class: self.img_class = clean_type(image)
[docs] def get_dict(self): """ Returns ------- """ pass
[docs] def __str__(self): return self._print_md()
def _print_md(self, indent_lvl=0): """Formatter for metadata printing. Parameters ---------- indent_lvl : int, optional Level of indentation to use Returns ------- str """ md = self.get_dict() s = "" sorted_ops = sort_ops(md) if sorted_ops is None: return s op_id = 0 lid = get_last_id(md) for op_name in sorted_ops: op_id += 1 params, called = get_params_called(md, op_id, op_name) s += md_str(op_id, lid, op_name, params, called, indent_lvl) if called is not None: s += print_md(called, indent_lvl + 1) s = s[:-1] # remove the last '\n' return s
def _check_fname_def(sp_img, filename, filepath): """Hidden function verifying 'filename' & 'filepath' definition in SpatialImage metadata. Parameters ---------- sp_img : timagetk.SpatialImage Image with metadata filename : str String giving the name of the file (with its extension) filepath : str String giving the path to the file Returns ------- timagetk.SpatialImage Image and updated metadata """ # -- Check 'filename' & 'filepath' definition in metadata: try: # Try to load the 'filename' metadata: md_filepath = sp_img.metadata['filepath'] md_filename = sp_img.metadata['filename'] except KeyError: # If not defined in metadata, use path splitted `fname`: sp_img._metadata.update({'filepath': filepath}) sp_img._metadata.update({'filename': filename}) # sp_img.metadata['filepath'] = filepath # sp_img.metadata['filename'] = filename else: # If defined, compare given `fname` and metadata: (md_filepath, md_filename) = os.path.split(md_filename) # -- Check the given filepath and the one found in metadata (if any) if md_filepath == '': md_filepath = filepath elif md_filepath != filepath: log.warning("Metadata 'filepath' differ from the one given to the reader!") log.warning("Updated metadata 'filepath'!") md_filepath = filepath elif md_filepath == filepath: pass else: log.error("Got filepath: {}".format(filepath)) raise ValueError("Undefined 'filepath' condition!") # -- Check the given filename and the one found in metadata (if any) if md_filename == '': md_filename = filename elif md_filename != filename: log.warning("Metadata 'filename' differ from the one given to the reader!") log.warning("Updated metadata 'filename'!") elif md_filename == filename: pass else: log.error("Got filename: {}".format(filename)) raise ValueError("Undefined 'filename' condition!") # Update metadata keys: sp_img._metadata.update({'filepath': filepath}) sp_img._metadata.update({'filename': filename}) sp_img.filepath = sp_img.metadata['filepath'] sp_img.filename = sp_img.metadata['filename'] return sp_img def _check_class_def(sp_img): """Check timagetk class definition in metadata. Parameters ---------- sp_img : timagetk.SpatialImage Image with metadata Returns ------- timagetk.SpatialImage Image with updated metadata """ from timagetk.components.labelled_image import LabelledImage from timagetk import TissueImage2D from timagetk import TissueImage3D # -- Use 'class' definition in 'timagetk' metadata to return correct instance type: try: md_class = sp_img.metadata['timagetk']['class'] except KeyError: warn_msg = "Initializing from an image without 'class' entry in 'timagetk' metadata!" log.debug(warn_msg) sp_img.metadata.update({'timagetk': {'class': 'SpatialImage'}}) log.debug("Updated to 'SpatialImage' entry.") else: if md_class == 'LabelledImage': sp_img = LabelledImage(sp_img) elif md_class == 'TissueImage2D': sp_img = TissueImage2D(sp_img) elif md_class == 'TissueImage3D': sp_img = TissueImage3D(sp_img) else: sp_img.metadata.update({'timagetk': {'class': 'SpatialImage'}}) return sp_img def _check_physical_ppty(metadata): """Make sure physical properties are coherent. Parameters ---------- metadata : dict Metadata dictionary, should contain 'shape', 'extent' & 'voxelsize' info Raises ------ AssertionError If saved and computed 'extent' values are not the same """ from timagetk.components.spatial_image import compute_extent sh = metadata['shape'] vxs = metadata['voxelsize'] ext = metadata['extent'] ext2 = compute_extent(vxs, sh) try: np.testing.assert_array_equal(ext, ext2) except AssertionError: msg = "Metadata 'extent' is wrong: should be '{}', but read '{}'!" raise ValueError(msg.format(ext2, ext)) return # def check_metadata(array, origin, voxelsize, metadata=None): # """Build the metadata dictionary for basics image array infos. # # If ``metadata`` is ``None``, build the metadata dictionary based on the # array properties and other attributes. # If ``metadata`` is NOT ``None``, make sure it can match the array properties # and other attribute. # For a list of properties and attribute, see Notes. # # Notes # ----- # Raises ``ValueError`` if missmatch for: # # * voxelsize # * origin # * extent # # Update the metadata value for: # # * shape # * dim # * type # # Parameters # ---------- # array : timagetk.SpatialImage # a SpatialImage to use for metadata definition # metadata : dict, optional # a metadata dictionary to compare to the object variables # # Returns # ------- # metadata : dict # a verified metadata dictionary # """ # if not metadata: # metadata = {} # # # -- Check object property or attribute values against those in metadata: # # # --- Update metadata with object value: # # -------------------------------------- # try: # assert metadata['shape'] == array.shape # except KeyError: # metadata['shape'] = array.shape # except AssertionError: # msg = "Metadata 'shape' {} do not match the array shape {}." # log.warning(msg.format(metadata['shape'], array.shape)) # metadata['shape'] = array.shape # log.warning("--> updated!") # # try: # assert metadata['dim'] == array.ndim # except KeyError: # metadata['dim'] = array.ndim # except AssertionError: # msg = "Metadata 'dim' {} do not match the array shape {}." # log.warning(msg.format(metadata['dim'], array.ndim)) # metadata['dim'] = array.ndim # log.warning("--> updated!") # # try: # assert metadata['dtype'] == str(array.dtype) # except KeyError: # metadata['dtype'] = str(array.dtype) # except AssertionError: # log.warning( # "Metadata 'dtype' ({}) do not match the array dtype ({}).".format( # metadata['dtype'], array.dtype)) # metadata['dtype'] = str(array.dtype) # log.warning("--> updated!") # # # --- Raise ValueError exception: # # ------------------------------------- # try: # assert np.alltrue(metadata['voxelsize'] == array._voxelsize) # except KeyError: # metadata['voxelsize'] = array._voxelsize # except AssertionError: # raise ValueError( # "Metadata 'voxelsize' does not match the object voxelsize!") # # try: # assert np.alltrue(metadata['origin'] == array._origin) # except KeyError: # metadata['origin'] = array._origin # except AssertionError: # msg = "Metadata 'origin' {} does not match the object origin {}!" # raise ValueError(msg.format(metadata['origin'], array._origin)) # # try: # assert np.alltrue(metadata['extent'] == array._extent) # except KeyError: # metadata['extent'] = array._extent # except AssertionError: # msg = "Metadata 'extent' {} does not match the array extent {}!" # raise ValueError(msg.format(metadata['extent'], array._extent)) # # return metadata