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 print_md(metadata, indent_lvl=0):
"""
Parameters
----------
indent_lvl
metadata
Returns
-------
"""
try:
md = metadata['timagetk']
except KeyError:
md = metadata
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
[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]
def print_params(params, indent_lvl, p_indent='--', indent=' '):
"""
Parameters
----------
p_indent
indent
params
indent_lvl
Returns
-------
str
Formated parameters
Examples
--------
>>> import numpy as np
>>> from timagetk import SpatialImage
>>> from timagetk.algorithms.resample import isometric_resampling
>>> from timagetk.components.metadata import print_md
>>> test_array = np.ones((5,5,10), dtype=np.uint8)
>>> img = SpatialImage(test_array, voxelsize=[1., 1., 2.])
>>> output_image = isometric_resampling(img, voxelsize=0.4)
>>> print(print_md(output_image.metadata))
>>> from timagetk.synthetic_data.labelled_image import example_layered_sphere_labelled_image
>>> from timagetk.algorithms.linearfilter import gaussian_filter
>>> from timagetk.algorithms.regionalext import regional_extrema
>>> from timagetk.algorithms.connexe import connected_components
>>> from timagetk.tasks.segmentation import watershed_segmentation
>>> from timagetk.components.metadata import print_md
>>> input_image = example_layered_sphere_labelled_image
>>> smooth_image = gaussian_filter(input_image, sigma=2.0)
>>> regext_image = regional_extrema(smooth_image, h=5, method='min')
>>> seeds_image = connected_components(regext_image,low_threshold=1,high_threshold=3,method='connected_components')
>>> segmented_image = watershed_segmentation(smooth_image, seeds_image, control='first', method='seeded_watershed')
>>> segmented_image.metadata['timagetk']
>>> print(print_md(segmented_image.metadata))
"""
s = ""
ind = indent * indent_lvl + indent
for k in params.keys():
val = params.get(k, None)
if val == {}:
v = "EMPTY"
elif val is None:
v = "None"
elif k == 'titk_md':
v = ""
# - case when a SpatialImage instance has been found and metadata have been returned:
titk_md = params.get(k, {})
assert isinstance(titk_md, dict)
# Get 'class' metadata:
c = {'class': titk_md.get('class', None)}
if c['class'] is not None:
titk_md.pop('class')
v += "\n" + print_params(c, indent_lvl, '*-')
# Get SpatialImage metadata:
if titk_md == {}:
v += " with no metadata!"
else:
v += "\n" + print_md(titk_md, indent_lvl + 2)
elif isinstance(val, dict):
v = "\n" + print_params(val, indent_lvl + 1, '*-')
else:
v = "{}".format(val)
s += "{}{}{}: {}\n".format(ind, p_indent, k, v)
if s.endswith('\n'):
s = s[:-1] # remove the last '\n'
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(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)
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