Source code for mdt.visualization.maps.base

import warnings
from copy import copy, deepcopy
import numbers
import matplotlib.font_manager
import nibabel
import numpy as np
import yaml

import mdt
import mdt.visualization.layouts
from mdt.lib.nifti import load_nifti, NiftiInfoDecorated
from mdt.visualization.dict_conversion import StringConversion, \
    SimpleClassConversion, IntConversion, SimpleListConversion, BooleanConversion, \
    ConvertDictElements, ConvertDynamicFromModule, FloatConversion, WhiteListConversion, ConvertListElements
from mdt.visualization.layouts import Rectangular
from mdt.visualization.maps.utils import get_shortest_unique_names, find_all_nifti_files

__author__ = 'Robbert Harms'
__date__ = "2016-09-02"
__maintainer__ = "Robbert Harms"
__email__ = "robbert@xkls.nl"


[docs]class ConvertibleConfig: """Base class for convertible configuration containers. Objects implementing this interface return a conversion specification that describes how the implementing class can be converted to and from a simple dictionary containing only primitives and simple data structures. """
[docs] @classmethod def get_conversion_info(cls): """Get the conversion specification information for this class. Returns: mdt.visualization.dict_conversion.ConversionSpecification: the conversion specification """ raise NotImplementedError()
[docs]class SimpleConvertibleConfig: """Offers an simplified implementation of convertible configs. In addition to slightly simplifying the creation of the conversion specification, it also implements object equality checking (__eq__ and__ne__) as this is commonly used in the configuration objects, and it offers a default representation (__repr__) implementation based on the conversion specification. """
[docs] @classmethod def get_conversion_info(cls): return SimpleClassConversion(cls, cls._get_attribute_conversions())
@classmethod def _get_attribute_conversions(cls): """Simplifies the creation of the conversion info by returning a dictionary of conversion specifications. Returns: dict: a dictionary with for every attribute of the class a conversion specification (instance of :class:`~mdt.visualization.dict_conversion.ConversionSpecification`) describing how to transform every attribute of the class. """ raise NotImplementedError() def __repr__(self): return str(self.get_conversion_info().to_dict(self)) def __eq__(self, other): if not isinstance(other, self.__class__): return NotImplemented for key, value in self.__dict__.items(): if value != getattr(other, key) or (value is not None and getattr(other, key) is None) \ or (value is None and getattr(other, key) is not None): return False return True def __ne__(self, other): return not self.__eq__(other)
[docs]class DataInfo: """Information about the maps we are viewing."""
[docs] def get_map_names(self): """Get a list of the names of all the maps in this data info object. Returns: list: the list of map names """ raise NotImplementedError()
[docs] def get_map_data(self, map_name): """Get the data for the indicated map. Args: map_name (str): the name of the map we want the data of Returns: ndarray: the data of the given map """ raise NotImplementedError()
[docs] def get_single_map_info(self, map_name): """Get an information object for a single map. Args: map_name (str): the name of the map we want information about Returns: SingleMapInfo: information object about that map """ raise NotImplementedError()
[docs] def get_file_path(self, map_name): """Get the file name of the given map Returns: None if the map could not be found on dir, else a string with the file path. """ raise NotImplementedError()
[docs] def get_file_paths(self): """Get the file paths to each of the maps. If one of the maps does not have a file path, None is returned. Returns: dict: mapping map names to the file paths for each of the maps in this information container. """ raise NotImplementedError()
[docs] def get_max_dimension(self, map_names=None): """Get the minimum of the maximum dimension index over the maps Args: map_names (list of str): if given we will only scan the given list of maps Returns: int: either, 0, 1, 2 as the maximum dimension index in the maps. """ raise NotImplementedError()
[docs] def get_max_slice_index(self, dimension, map_names=None): """Get the maximum slice index in the given map on the given dimension. Args: dimension (int): the dimension we want the slice index of (maximum 3) map_names (list of str): if given we will only scan the given list of maps Returns: int: the minimum of the maximum slice indices over the given maps in the given dimension. """ raise NotImplementedError()
[docs] def get_max_volume_index(self, map_names=None): """Get the maximum volume index in the given maps. In contrast to the max dimension and max slice index functions, this gives the maximum over all the images. This since handling different volumes is implemented in the viewer. Args: map_names (list of str): if given we will only scan the given list of maps Returns: int: the maximum volume index in the given list of maps. Starts from 0. """ raise NotImplementedError()
[docs] def get_index_first_non_zero_slice(self, dimension, map_names=None): """Get the index of the first non zero slice in the maps. Args: dimension (int): the dimension to search in map_names (list of str): if given we will only scan the given list of maps Returns: int: the slice index with the first non zero values. """ raise NotImplementedError()
[docs] def slice_has_data(self, dimension, slice_index, map_names=None): """Check if at least one of the maps has non zero numbers on the given slice. Args: dimension (int): the dimension to search in slice_index (int): the index of the slice in the given dimension map_names (list of str): if given we will only scan the given list of maps Returns: bool: true if at least on of the maps has data in the given slice """ raise NotImplementedError()
[docs] def get_max_x_index(self, dimension, rotate=0, map_names=None): """Get the maximum x index supported over the images. In essence this gets the lowest x index found. Args: dimension (int): the dimension to search in rotate (int): the rotation factor by which we rotate the slices within the given dimension map_names (list of str): if given we will only scan the given list of maps Returns: int: the maximum x-index found. """ raise NotImplementedError()
[docs] def get_max_y_index(self, dimension, rotate=0, map_names=None): """Get the maximum y index supported over the images. In essence this gets the lowest y index found. Args: dimension (int): the dimension to search in rotate (int): the rotation factor by which we rotate the slices within the given dimension map_names (list of str): if given we will only scan the given list of maps Returns: int: the maximum y-index found. """ raise NotImplementedError()
[docs] def get_bounding_box(self, dimension, slice_index, volume_index, rotate, map_names=None): """Get the bounding box of the images. Args: dimension (int): the dimension to search in slice_index (int): the slice index in that dimension volume_index (int): the current volume index rotate (int): the angle by which to rotate the image before getting the bounding box map_names (list of str): if given we will only scan the given list of maps Returns: tuple of :class:`Point2d`: two points designating the upper left corner and the lower right corner of the bounding box. """ raise NotImplementedError()
[docs]class SimpleDataInfo(DataInfo): def __init__(self, maps): """A container for basic information about the volume maps we are viewing. Args: maps (dict): the dictionary with the maps to view, these maps can either be arrays with values, nibabel proxy images or SingleMapInfo objects. """ self._input_maps = maps
[docs] @classmethod def from_paths(cls, paths): """Load all the nifti files from the given paths. For paths that are directories we load all the elements inside that directory (but without recursion). Args: list of str: the list of paths to load (directories and files) Returns: SimpleDataInfo: the simple data info """ nifti_files = find_all_nifti_files(paths) return cls(_load_data_info(nifti_files))
[docs] def get_updated(self, updates=None, removals=None): """Get a new simple data info object that includes the given new maps. In the case of double map names the old maps are overwritten. Args: updates (dict): the dictionary with the maps to view, these maps can either be arrays with values, nibabel proxy images or SingleMapInfo objects. removals (list): a list of maps to remove Returns: SimpleDataInfo: a new updated data info object """ new_maps = copy(self._input_maps) if updates: new_maps.update(updates) if removals: for map_name in removals: if map_name in new_maps: del new_maps[map_name] return SimpleDataInfo(new_maps)
[docs] def get_map_names(self): return list(self._input_maps.keys())
[docs] def get_map_data(self, map_name): return self.get_single_map_info(map_name).data
[docs] def get_single_map_info(self, map_name): value = self._input_maps[map_name] if not isinstance(value, SingleMapInfo): return SingleMapInfo(value) return value
[docs] def get_file_path(self, map_name): return self.get_single_map_info(map_name).file_path
[docs] def get_file_paths(self): return {map_name: self.get_file_path(map_name) for map_name in self._input_maps.keys()}
[docs] def get_max_dimension(self, map_names=None): """Get the minimum of the maximum dimension index over the maps Args: map_names (list of str): if given we will only scan the given list of maps Returns: int: either, 0, 1, 2 as the maximum dimension index in the maps. """ map_names = map_names or self._input_maps.keys() if not map_names: raise ValueError('No maps to search in.') return min(self.get_single_map_info(map_name).max_dimension() for map_name in map_names)
[docs] def get_max_slice_index(self, dimension, map_names=None): """Get the maximum slice index in the given map on the given dimension. Args: dimension (int): the dimension we want the slice index of (maximum 3) map_names (list of str): if given we will only scan the given list of maps Returns: int: the minimum of the maximum slice indices over the given maps in the given dimension. """ map_names = map_names or self._input_maps.keys() max_dimension = self.get_max_dimension(map_names) if not map_names: raise ValueError('No maps to search in.') if dimension > max_dimension: raise ValueError('Dimension can not exceed {}.'.format(max_dimension)) return min(self.get_single_map_info(map_name).max_slice_index(dimension) for map_name in map_names)
[docs] def get_max_volume_index(self, map_names=None): """Get the maximum volume index in the given maps. In contrast to the max dimension and max slice index functions, this gives the maximum over all the images. This since handling different volumes is implemented in the viewer. Args: map_names (list of str): if given we will only scan the given list of maps Returns: int: the maximum volume index in the given list of maps. Starts from 0. """ map_names = map_names or self._input_maps.keys() if not map_names: raise ValueError('No maps to search in.') return max(self.get_single_map_info(map_name).max_volume_index() for map_name in map_names)
[docs] def get_index_first_non_zero_slice(self, dimension, map_names=None): """Get the index of the first non zero slice in the maps. Args: dimension (int): the dimension to search in map_names (list of str): if given we will only scan the given list of maps Returns: int: the slice index with the first non zero values. """ map_names = map_names or self._input_maps.keys() if not map_names: raise ValueError('No maps to search in.') for map_name in map_names: index = self.get_single_map_info(map_name).get_index_first_non_zero_slice(dimension) if index is not None: return index return 0
[docs] def slice_has_data(self, dimension, slice_index, map_names=None): """Check if at least one of the maps has non zero numbers on the given slice. Args: dimension (int): the dimension to search in slice_index (int): the index of the slice in the given dimension map_names (list of str): if given we will only scan the given list of maps Returns: bool: true if at least on of the maps has data in the given slice """ map_names = map_names or self._input_maps.keys() if not map_names: raise ValueError('No maps to search in.') for map_name in map_names: if self.get_single_map_info(map_name).slice_has_data(dimension, slice_index): return True return False
[docs] def get_max_x_index(self, dimension, rotate=0, map_names=None): """Get the maximum x index supported over the images. In essence this gets the lowest x index found. Args: dimension (int): the dimension to search in rotate (int): the rotation factor by which we rotate the slices within the given dimension map_names (list of str): if given we will only scan the given list of maps Returns: int: the maximum x-index found. """ map_names = map_names or self._input_maps.keys() if not map_names: raise ValueError('No maps to search in.') return min(self.get_single_map_info(map_name).get_max_x_index(dimension, rotate) for map_name in map_names)
[docs] def get_max_y_index(self, dimension, rotate=0, map_names=None): """Get the maximum y index supported over the images. In essence this gets the lowest y index found. Args: dimension (int): the dimension to search in rotate (int): the rotation factor by which we rotate the slices within the given dimension map_names (list of str): if given we will only scan the given list of maps Returns: int: the maximum y-index found. """ map_names = map_names or self._input_maps.keys() if not map_names: raise ValueError('No maps to search in.') return min(self.get_single_map_info(map_name).get_max_y_index(dimension, rotate) for map_name in map_names)
[docs] def get_bounding_box(self, dimension, slice_index, volume_index, rotate, map_names=None): """Get the bounding box of the images. Args: dimension (int): the dimension to search in slice_index (int): the slice index in that dimension volume_index (int): the current volume index rotate (int): the angle by which to rotate the image before getting the bounding box map_names (list of str): if given we will only scan the given list of maps Returns: tuple of Point: two point designating first the upper left corner and second the lower right corner of the bounding box. """ map_names = map_names or self._input_maps.keys() if not map_names: raise ValueError('No maps to search in.') bounding_boxes = [self.get_single_map_info(map_name).get_bounding_box(dimension, slice_index, volume_index, rotate) for map_name in map_names] p0x = min([bbox[0].x for bbox in bounding_boxes]) p0y = min([bbox[0].y for bbox in bounding_boxes]) p1x = max([bbox[1].x for bbox in bounding_boxes]) p1y = max([bbox[1].y for bbox in bounding_boxes]) return Point2d(p0x, p0y), Point2d(p1x, p1y)
[docs]class SingleMapInfo: def __init__(self, data, file_path=None): """Holds information about a single map. Args: data (ndarray or :class:`nibabel.spatialimages.SpatialImage`): the value of the map or the proxy to it file_path (str): optionally, the file path with the location of this map. If not set we try to retreive it from the data if the data is of subclass :class:`mdt.lib.nifti.NiftiInfoDecorated`. """ self._data = data self._shape = self._data.shape self._file_path = file_path if self._file_path is None: if isinstance(data, NiftiInfoDecorated): self._file_path = data.nifti_info.filepath
[docs] @classmethod def from_file(cls, nifti_path): return cls(load_nifti(nifti_path), nifti_path)
@property def shape(self): return deepcopy(self._shape) @property def data(self): if isinstance(self._data, nibabel.spatialimages.SpatialImage): return self._data.get_data() return self._data @property def file_path(self): return self._file_path
[docs] def min(self, mask=None): """Get the minimum value in this data. If a mask is provided we get the minimum value within the given mask. Args: mask (ndarray): the mask, we only include elements for which the mask > 0 Returns: float: the minimum value """ if mask is not None: return mdt.create_roi(self.data, mask).min() return self.data.min()
[docs] def max(self, mask=None): """Get the maximum value in this data. If a mask is provided we get the maximum value within the given mask. Args: mask (ndarray): the mask, we only include elements for which the mask > 0 Returns: float: the maximum value """ if mask is not None: return mdt.create_roi(self.data, mask).max() return self.data.max()
[docs] def min_max(self, mask=None): """Get the minimum and maximum value in this data. If a mask is provided we get the min and max value within the given mask. Infinities and NaN's are ignored by this algorithm. Args: mask (ndarray): the mask, we only include elements for which the mask > 0 Returns: tuple: (min, max) the minimum and maximum values """ if mask is not None: roi = mdt.create_roi(self.data, mask) return np.nanmin(roi), np.nanmax(roi) return np.nanmin(self.data), np.nanmax(self.data)
[docs] def has_nan(self): """Check if this data has any NaNs in it. Returns: boolean: True if there are NaN's anywhere in the data, false otherwise. """ return np.isnan(self.data).any()
[docs] def max_dimension(self): """Get the maximum dimension index in this map. The maximum value returned by this method is 2 and the minimum is 0. Returns: int: in the range 0, 1, 2 """ return min(len(self.shape), 3) - 1
[docs] def max_slice_index(self, dimension): """Get the maximum slice index on the given dimension. Args: dimension (int): the dimension we want the slice index of (maximum 3) Returns: int: the maximum slice index in the given dimension. """ return self.shape[dimension] - 1
[docs] def slice_has_data(self, dimension, slice_index): """Check if this map has non zero values in the given slice index. Args: dimension (int): the dimension we want the slice index of (maximum 3) slice_index (int): the slice index to look in Returns: int: the maximum slice index in the given dimension. """ slice_indexing = [slice(None)] * (self.max_dimension() + 1) slice_indexing[dimension] = slice_index return np.count_nonzero(self.data[slice_indexing])
[docs] def max_volume_index(self): """Get the maximum volume index in this map. The minimum is 0. Returns: int: the maximum volume index. """ if len(self.shape) > 3: return self.shape[3] - 1 return 0
[docs] def get_index_first_non_zero_slice(self, dimension): """Get the index of the first non zero slice in this map. Args: dimension (int): the dimension to search in Returns: int: the slice index with the first non zero values. """ slice_index = [slice(None)] * (self.max_dimension() + 1) if dimension > len(slice_index) - 1: raise ValueError('The given dimension {} is not supported.'.format(dimension)) for index in range(self.shape[dimension]): slice_index[dimension] = index if np.count_nonzero(self.data[slice_index]) > 0: return index return 0
[docs] def get_max_x_index(self, dimension, rotate=0): """Get the maximum x index. Args: dimension (int): the dimension to search in rotate (int): the value by which to rotate the slices in the given dimension Returns: int: the maximum x index """ shape = list(self.shape)[0:3] if len(shape) > 2: del shape[dimension] if rotate // 90 % 2 == 0: return max(0, shape[1] - 1) return max(0, shape[0] - 1)
[docs] def get_max_y_index(self, dimension, rotate=0): """Get the maximum y index. Args: dimension (int): the dimension to search in rotate (int): the value by which to rotate the slices in the given dimension Returns: int: the maximum y index """ shape = list(self.shape)[0:3] if len(shape) > 2: del shape[dimension] if rotate // 90 % 2 == 0: return max(0, shape[0] - 1) return max(0, shape[1] - 1)
[docs] def get_size_in_dimension(self, dimension, rotate=0): """Get the shape of the 2d view on the data in the given dimension. This basically returns a pair of (max_x, max_y). Args: dimension (int): the dimension to search in rotate (int): the value by which to rotate the slices in the given dimension Returns: tuple: (max_x, max_y) """ return self.get_max_x_index(dimension, rotate), self.get_max_y_index(dimension, rotate)
[docs] def get_bounding_box(self, dimension, slice_index, volume_index, rotate): """Get the bounding box of this map when displayed using the given indicing. This only works if the edges of the images are exactly zero, that is, it only works with masked datasets. Args: dimension (int): the dimension to search in slice_index (int): the slice index in that dimension volume_index (int): the current volume index rotate (int): the angle by which to rotate the image before getting the bounding box Returns: tuple of Point: two point designating first the upper left corner and second the lower right corner of the bounding box. """ def bbox(image): rows = np.any(image, axis=1) cols = np.any(image, axis=0) rows_where = np.where(rows) if np.size(rows_where): row_min, row_max = np.where(rows)[0][[0, -1]] column_min, column_max = np.where(cols)[0][[0, -1]] return row_min, row_max, column_min, column_max return 0, image.shape[0]-1, 0, image.shape[1]-1 if len(self.shape) == 2: image = self.data else: slice_indexing = [slice(None)] * (self.max_dimension() + 1) slice_indexing[dimension] = slice_index image = self.data[slice_indexing] if len(image.shape) > 2: if image.shape[2] > 1: image = image[..., volume_index] else: image = image[..., 0] if rotate: image = np.rot90(image, rotate // 90) row_min, row_max, column_min, column_max = bbox(image) return Point2d(column_min, row_min), Point2d(column_max, row_max)
[docs]class Zoom(SimpleConvertibleConfig): def __init__(self, p0, p1): """Container for zooming a map between the two given points. Args: p0 (Point2d): the lower left corner of the zoomed area p1 (Point2d): the upper right corner of the zoomed area """ self.p0 = p0 self.p1 = p1 if p0.x > p1.x or p0.y > p1.y: raise ValueError('The lower left point ({}, {}) should be smaller than the upper right point ({}, {})'. format(p0.x, p0.y, p1.x, p1.y)) if p0.x < 0 or p0.y < 0 or p1.x < 0 or p1.y < 0: raise ValueError('The zoom box ({}, {}), ({}, {}) can not ' 'be negative in any way.'.format(p0.x, p0.y, p1.x, p1.y)) if self.p0 is None or self.p1 is None: raise ValueError('One of the zoom points is None.')
[docs] @classmethod def from_coords(cls, x0, y0, x1, y1): return cls(Point2d(x0, y0), Point2d(x1, y1))
[docs] @classmethod def no_zoom(cls): return cls(Point2d(0, 0), Point2d(0, 0))
@classmethod def _get_attribute_conversions(cls): point_converter = Point2d.get_conversion_info() return {'p0': point_converter, 'p1': point_converter}
[docs] def get_rotated(self, rotation, x_dimension, y_dimension): """Return a new Zoom instance rotated with the given factor. This rotates the zoom box in the same way as the image is rotated. Args: rotation (int): the rotation by which to rotate in steps of 90 degrees x_dimension (int): the dimension of the image in the x coordinate y_dimension (int): the dimension of the image in the y coordinate Returns: Zoom: the rotated instance """ dimensions = [x_dimension, y_dimension] p0 = self.p0 p1 = self.p1 nmr_90_rotations = rotation % 360 // 90 for _ in range(nmr_90_rotations): dimensions = np.roll(dimensions, 1) new_p0 = Point2d(np.min([dimensions[0] - p0.y, dimensions[0] - p1.y]), np.min([p0.x, p1.x])) new_p1 = Point2d(np.max([dimensions[0] - p0.y, dimensions[0] - p1.y]), np.max([p0.x, p1.x])) p0 = new_p0 p1 = new_p1 if p0.x >= dimensions[0] - 1 or p0.x < 0: p0 = p0.get_updated(x=0) if p0.y >= dimensions[1] - 1 or p0.y < 0: p0 = p0.get_updated(y=0) if p1.x >= dimensions[0] - 1: p1 = p1.get_updated(x=dimensions[0] - 1) if p1.y >= dimensions[1] - 1: p1 = p1.get_updated(y=dimensions[1] - 1) return Zoom(p0, p1)
[docs] def apply(self, data): """Apply the zoom to the given 2d array and return the new array. Args: data (ndarray): the data to zoom in on """ correct = self.p0.x < data.shape[1] and self.p1.x < data.shape[1] \ and self.p0.y < data.shape[0] and self.p1.y < data.shape[0] \ and self.p0.x < self.p1.x and self.p0.y < self.p1.y if correct: return data[self.p0.y:self.p1.y, self.p0.x:self.p1.x] return data
[docs]class ColorbarSettings(SimpleConvertibleConfig): def __init__(self, visible=None, nmr_ticks=None, location=None, power_limits=None, round_precision=None): """Container for all colorbar related settings. Args: visible (boolean): if the colorbar is to be shown nmr_ticks (int): the number of ticks location (str): the location of the colorbar, one of 'right', 'left', 'top' or 'bottom' power_limits (tuple): size thresholds for scientific notation. The default is (-3, 4) which uses scientific notation for numbers less than 1e-3 or greater than 1e4. round_precision (int): how much digits (precision) after the decimal point. """ self.nmr_ticks = nmr_ticks self.location = location self.visible = visible self.power_limits = power_limits self.round_precision = round_precision if self.location is not None and self.location not in ['left', 'right', 'bottom', 'top']: raise ValueError("The colorbar location is '{}' which is not " "one of 'left', 'bottom', 'right', 'top'.".format(str(self.location))) if self.nmr_ticks is not None: self.nmr_ticks = int(self.nmr_ticks) if self.nmr_ticks <= 0: raise ValueError("The number of ticks in the colorbar needs to be a positive integer.") if self.visible is not None: self.visible = bool(self.visible) if self.power_limits is not None: if not isinstance(self.power_limits, (tuple, list)): raise ValueError('The power limits should be a tuple or list.') if len(self.power_limits) != 2: raise ValueError('The power limits should hold ' 'exactly two elements, {} given.'.format(len(self.power_limits))) if not all(isinstance(el, numbers.Integral) for el in self.power_limits): raise ValueError('The power limits should be integers.') if self.power_limits[0] > self.power_limits[1]: raise ValueError('The lower power limit should be lower than the upper power limit.') if self.round_precision is not None: self.round_precision = int(self.round_precision)
[docs] @staticmethod def get_default(): return ColorbarSettings(visible=True, nmr_ticks=4, location='right', power_limits=(-3, 4), round_precision=3)
@classmethod def _get_attribute_conversions(cls): return { 'nmr_ticks': IntConversion(), 'location': StringConversion(), 'visible': BooleanConversion(), 'power_limits': SimpleListConversion(), 'round_precision': IntConversion() }
[docs] def get_preferred(self, attr, other_settings=None): """Get the preferred value for the requested attribute. Other settings is a list of other colorbar settings object that are asked for the preferred value (in turn), if the value of this object is None. As a fallback, this will always use the default colorbar settings ``ColorbarSettings.get_default()`` as a final default. Args: attr (str): the attribute requested other_settings (list of ColorbarSettings): other settings to try. Returns: object: the value of the requested object """ if getattr(self, attr) is not None: return getattr(self, attr) if not other_settings: return getattr(self.get_default(), attr) for other_setting in other_settings: return other_setting.get_preferred(attr, other_settings[1:])
[docs]class VoxelAnnotation(SimpleConvertibleConfig): valid_text_locations = ['upper left', 'top left', 'upper right', 'top right', 'bottom right', 'lower right', 'bottom left', 'lower left', 'north', 'south', 'east', 'west', 'top', 'bottom', 'left', 'right'] def __init__(self, voxel_index, font_size=None, text_template=None, marker_size=1, text_location='upper left', text_distance=0.05, arrow_width=1): """Container for all voxel highlighting settings. Args: voxel_index (tuple): a tuple with the voxel index location font_size (int): the size of the annotation text text_template (str): the text template, can use the placeholders ``{voxel_index}`` and ``{value}``. marker_size (float): the rectangular size of the voxel marker text_location (str): the location of the text. Valid items are: ``upper left``, ``top left``, ``upper right``, ``top right``, ``bottom right``, ``lower right``, ``bottom left``, ``lower left``, ``north``, ``south``, ``east``, ``west``, ``top``, ``bottom``, ``left`` , ``right``. text_distance (float): the distance of the textbox to the marker in relative coordinates (0, 1). arrow_width (float): the width of the arrow """ self.voxel_index = voxel_index self.font_size = font_size self.text_template = text_template or "{voxel_index}\n{value:.3g}" self.marker_size = marker_size self.text_location = text_location self.text_distance = text_distance self.arrow_width = arrow_width if text_location not in self.valid_text_locations: raise ValueError('The given text location "{}" is not ' 'in the list of valid text locations: {}'.format(self.text_location, self.valid_text_locations)) if not isinstance(self.marker_size, numbers.Real): raise ValueError('The marker size should be a real number, {} given.'.format(self.marker_size)) if not isinstance(self.text_distance, numbers.Real): raise ValueError('The text distance should be a real number, {} given.'.format(self.text_distance)) if not isinstance(self.arrow_width, numbers.Real): raise ValueError('The arrow width should be a real number, {} given.'.format(self.arrow_width)) self.text_template.format(voxel_index=(0, 0, 0), value=0) # validates the template @classmethod def _get_attribute_conversions(cls): return { 'voxel_index': SimpleListConversion(), 'font_size': IntConversion(), 'text_template': StringConversion(allow_null=False), 'marker_size': FloatConversion(), 'text_location': StringConversion(allow_null=False), 'text_distance': FloatConversion(allow_null=False), 'arrow_width': FloatConversion(allow_null=False) }
[docs] def validate(self, data_info): self.text_template.format(voxel_index=(0, 0, 0), value=0) if len(self.voxel_index) != 3: raise ValueError('The location of the annotation should consist of (x, y, z) ' 'coordinates, {} given.'.format(self.voxel_index)) for dim, pos in enumerate(self.voxel_index): try: max_pos = data_info.get_max_slice_index(dimension=dim) except ValueError: max_pos = None if max_pos is not None: if pos > max_pos: raise ValueError('The location of a highlighted voxel in ' 'the {} dimension is larger than {}, {} given.'.format(dim, max_pos, pos)) if pos < 0: raise ValueError('The location of a highlighted voxel in ' 'the {} dimension is negative, {} given.'.format(dim, pos))
[docs]class Point2d(SimpleConvertibleConfig): def __init__(self, x, y): """Container for a single point""" self.x = x self.y = y
[docs] def get_updated(self, **kwargs): """Get a new :class:`Point2d` with updated arguments. Args: **kwargs (dict): the new keyword values, when given these take precedence over the current ones. Returns: Point2d: a new scale with updated values. """ new_values = dict(x=self.x, y=self.y) new_values.update(**kwargs) return Point2d(**new_values)
@classmethod def _get_attribute_conversions(cls): return {'x': IntConversion(allow_null=False), 'y': IntConversion(allow_null=False)}
[docs] def rotate90(self, nmr_rotations): """Rotate this point around a 90 degree angle Args: nmr_rotations (int): the number of 90 degree rotations, can be negative Returns: Point2d: the rotated point """ def rotate_coordinate(x, y, nmr_rotations): rotation_matrix = np.array([[0, -1], [1, 0]]) rx, ry = x, y for rotation in range(1, nmr_rotations + 1): rx, ry = rotation_matrix.dot([rx, ry]) return rx, ry return Point2d(*rotate_coordinate(self.x, self.y, nmr_rotations))
[docs]class Clipping(SimpleConvertibleConfig): def __init__(self, vmin=0, vmax=0, use_min=False, use_max=False): """Container for the map clipping information""" self.vmin = vmin self.vmax = vmax self.use_min = use_min self.use_max = use_max if use_min and use_max and vmin > vmax: raise ValueError('The minimum clipping ({}) can not be larger than the maximum clipping({})'.format( vmin, vmax))
[docs] def apply(self, data): """Apply the clipping to the given 2d array and return the new array. Args: data (ndarray): the data to clip """ if self.use_max or self.use_min: clipping_min = data.min() if self.use_min: clipping_min = self.vmin clipping_max = data.max() if self.use_max: clipping_max = self.vmax return np.clip(data, clipping_min, clipping_max) return data
[docs] def visible_changes(self, old_clipping): """Checks if there are any visible changes between this clipping and the other. This method can implement knowledge that allows the visualization routine to check if it would need to update the plot or not. It expects that the clipping you wish to use is the one on which this method is called. Args: old_clipping (Clipping): the previous clipping Returns: bool: if the differences between this clipping and the other would result in visible differences. """ if self.use_min != old_clipping.use_min or self.use_max != old_clipping.use_max: return True def visible_changes_in_min(): if self.vmin == old_clipping.vmin: return False else: return self.use_min def visible_changes_in_max(): if self.vmax == old_clipping.vmax: return False else: return self.use_max return visible_changes_in_max() or visible_changes_in_min()
[docs] def get_updated(self, **kwargs): """Get a new Clipping object with updated arguments. Args: **kwargs (dict): the new keyword values, when given these take precedence over the current ones. Returns: Clipping: a new scale with updated values. """ new_values = dict(vmin=self.vmin, vmax=self.vmax, use_min=self.use_min, use_max=self.use_max) new_values.update(**kwargs) return Clipping(**new_values)
@classmethod def _get_attribute_conversions(cls): return {'vmax': FloatConversion(allow_null=False), 'vmin': FloatConversion(allow_null=False), 'use_min': BooleanConversion(allow_null=False), 'use_max': BooleanConversion(allow_null=False)}
[docs]class Scale(SimpleConvertibleConfig): def __init__(self, vmin=0, vmax=0, use_min=False, use_max=False): """Container the map scaling information""" self.vmin = vmin self.vmax = vmax self.use_min = use_min self.use_max = use_max if use_min and use_max and vmin > vmax: raise ValueError('The minimum scale ({}) can not be larger than the maximum scale ({})'.format(vmin, vmax)) @classmethod def _get_attribute_conversions(cls): return {'vmax': FloatConversion(allow_null=False), 'vmin': FloatConversion(allow_null=False), 'use_min': BooleanConversion(allow_null=False), 'use_max': BooleanConversion(allow_null=False)}
[docs] def visible_changes(self, old_scale): """Checks if there are any visible changes between this scale and the other. This method can implement knowledge that allows the visualization routine to check if it would need to update the plot or not. It expects that the scale you wish to use is the one on which this method is called. Args: old_scale (Scale): the previous scale Returns: bool: if the differences between this scale and the other would result in visible differences. """ if self.use_min != old_scale.use_min or self.use_max != old_scale.use_max: return True def visible_changes_in_min(): if self.vmin == old_scale.vmin: return False else: return self.use_min def visible_changes_in_max(): if self.vmax == old_scale.vmax: return False else: return self.use_max return visible_changes_in_max() or visible_changes_in_min()
[docs] def get_updated(self, **kwargs): """Get a new Scale object with updated arguments. Args: **kwargs (dict): the new keyword values, when given these take precedence over the current ones. Returns: Scale: a new scale with updated values. """ new_values = dict(vmin=self.vmin, vmax=self.vmax, use_min=self.use_min, use_max=self.use_max) new_values.update(**kwargs) return Scale(**new_values)
[docs]class Font(SimpleConvertibleConfig): def __init__(self, family='sans-serif', size=14): """Information about the font to use Args: family: the name of the font to use size: the size of the font (> 0). """ self.family = family self.size = size if family not in self.font_names(): raise ValueError("The given font \"{}\" is not recognized.".format(family)) if size < 1: raise ValueError("The size ({}) can not be smaller than 1".format(str(size)))
[docs] def get_updated(self, **kwargs): """Get a new Font object with updated arguments. Args: **kwargs (dict): the new keyword values, when given these take precedence over the current ones. Returns: Font: a new Font with updated values. """ new_values = dict(family=self.family, size=self.size) new_values.update(**kwargs) return Font(**new_values)
@property def name(self): return self.family
[docs] @classmethod def font_names(cls): """Get the name of supported fonts Returns: list of str: the name of the supported fonts and font families. """ with warnings.catch_warnings(): warnings.simplefilter("ignore") fonts = matplotlib.font_manager.get_fontconfig_fonts() names = [] for font_name in fonts: try: names.append(matplotlib.font_manager.FontProperties(fname=font_name).get_name()) except RuntimeError: pass return list(sorted(['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace'])) + list(sorted(names))
@classmethod def _get_attribute_conversions(cls): return {'family': StringConversion(), 'size': IntConversion()}
[docs]class MapPlotConfig(SimpleConvertibleConfig): def __init__(self, dimension=2, slice_index=0, volume_index=0, rotate=90, colormap='hot', colormap_masked_color=None, maps_to_show=None, font=None, grid_layout=None, show_axis=False, show_titles=True, zoom=None, map_plot_options=None, interpolation='bilinear', flipud=None, title=None, title_spacing=None, mask_name=None, colorbar_settings=None, annotations=None): """Container for all plot related settings. Args: dimension (int): the dimension we are viewing slice_index (int): the slice in the dimension we are viewing volume_index (int): in the case of multiple volumes (4th dimension) which index we are in. rotate (int): the rotation factor, multiple of 90. By default we rotate 90 degrees to show most in-vivo datasets in a natural way. colormap (str): the name of the colormap to use colormap_masked_color (str): the color for masked, NaN and infinity values. maps_to_show (list of str): the names of the maps to show font (int): the font settings grid_layout (GridLayout): the layout of the grid show_axis (bool): if we show the axis or not show_titles (boolean): the global setting for enabling/disabling the plot titles zoom (Zoom): the zoom setting for all the plots map_plot_options (dict of SingleMapConfig): per map the map specific plot options interpolation (str): one of the available interpolations flipud (boolean): if True we flip the image upside down title (str): the title to this plot title_spacing (float): the spacing between the top of the plots and the title mask_name (str): the name of the mask to apply to the maps prior to display annotations (list of VoxelAnnotation): the voxel annotations colorbar_settings (ColorbarSettings): all colorbar related settings """ super().__init__() default_values = self.get_default_values() self.dimension = dimension self.slice_index = slice_index self.volume_index = volume_index self.rotate = rotate self.colormap = colormap self.colormap_masked_color = colormap_masked_color self.maps_to_show = maps_to_show or default_values['maps_to_show'] self.zoom = zoom or default_values['zoom'] self.font = font or default_values['font'] self.show_axis = bool(show_axis) self.show_titles = bool(show_titles) self.grid_layout = grid_layout or default_values['grid_layout'] self.interpolation = interpolation or default_values['bilinear'] self.flipud = flipud if self.flipud is None: self.flipud = default_values['flipud'] self.map_plot_options = map_plot_options or {} self.title = title self.title_spacing = title_spacing self.mask_name = mask_name self.annotations = annotations or [] self.colorbar_settings = colorbar_settings or default_values['colorbar_settings'] if interpolation not in self.get_available_interpolations(): raise ValueError('The given interpolation ({}) is not supported.'.format(interpolation)) if self.colormap not in self.get_available_colormaps(): raise ValueError('The given colormap ({}) is not supported.'.format(self.colormap)) if self.rotate not in [0, 90, 180, 270]: raise ValueError('The given rotation ({}) is not supported, use 90 ' 'degree angles within 360.'.format(self.rotate)) if self.dimension is None: raise ValueError('The dimension can not be None.') if self.slice_index is None: raise ValueError('The slice index can not be None.') if self.volume_index is None: raise ValueError('The volume index can not be None.') if self.rotate is None: raise ValueError('The rotation can not be None.') if self.dimension < 0: raise ValueError('The dimension can not be smaller than 0, {} given.'.format(self.dimension))
[docs] @classmethod def get_available_interpolations(cls): return _get_available_interpolations()
[docs] @classmethod def get_available_colormaps(cls): return _get_available_colormaps()
[docs] @staticmethod def get_default_values(): return { 'dimension': 2, 'slice_index': 0, 'volume_index': 0, 'rotate': 90, 'colormap': 'hot', 'colormap_masked_color': None, 'maps_to_show': '', 'zoom': Zoom.no_zoom(), 'font': Font(), 'show_axis': False, 'show_titles': True, 'grid_layout': Rectangular(), 'interpolation': 'bilinear', 'flipud': False, 'map_plot_options': {}, 'title': None, 'title_spacing': None, 'mask_name': None, 'annotations': [], 'colorbar_settings': ColorbarSettings.get_default() }
@classmethod def _get_attribute_conversions(cls): return {'dimension': IntConversion(), 'slice_index': IntConversion(), 'volume_index': IntConversion(), 'rotate': IntConversion(), 'colormap': StringConversion(), 'colormap_masked_color': StringConversion(), 'maps_to_show': SimpleListConversion(), 'zoom': Zoom.get_conversion_info(), 'font': Font.get_conversion_info(), 'show_axis': BooleanConversion(), 'show_titles': BooleanConversion(), 'map_plot_options': ConvertDictElements(SingleMapConfig.get_conversion_info()), 'grid_layout': ConvertDynamicFromModule(mdt.visualization.layouts), 'interpolation': WhiteListConversion(cls.get_available_interpolations(), 'bilinear'), 'flipud': BooleanConversion(allow_null=False), 'title': StringConversion(), 'title_spacing': FloatConversion(), 'mask_name': StringConversion(), 'annotations': ConvertListElements(VoxelAnnotation.get_conversion_info()), 'colorbar_settings': ColorbarSettings.get_conversion_info() }
[docs] @classmethod def from_yaml(cls, text): return cls.get_conversion_info().from_dict(yaml.safe_load(text))
[docs] @classmethod def from_dict(cls, config_dict): return cls.get_conversion_info().from_dict(config_dict)
[docs] def to_dict(self, non_default_only=False): """Export this configuration to a dictionary Args: non_default_only (boolean): if True, we will only export those options that are not set to their default. Returns: dict: dict representation of the data """ data = self.get_conversion_info().to_dict(self) default_values = self.get_default_values() if non_default_only: for key, value in list(data['map_plot_options'].items()): map_config = SingleMapConfig.get_conversion_info().from_dict(value) if map_config == SingleMapConfig(): del data['map_plot_options'][key] else: data['map_plot_options'][key] = map_config.to_dict(non_default_only=non_default_only) for key, default in default_values.items(): data_value = self._get_attribute_conversions()[key].from_dict(data[key]) if data_value == default: del data[key] return data
[docs] def to_yaml(self, non_default_only=False): """Convert this configuration to a YAML string. Args: non_default_only (boolean): if True, we will only export those options that are not set to their default. Returns: str: a YAML representation of this configuration. """ return yaml.safe_dump(self.to_dict(non_default_only=non_default_only))
[docs] def visible_changes(self, old_config): """Checks if there are any visible changes between this configuration and the other. This method can implement knowledge that allows the visualization routine to check if it would need to update the plot or not. It expects that the configuration you wish to display is the one on which this method is called. Args: old_config (MapPlotConfig): the previous configuration Returns: bool: if the differences between this configuration and the other would result in visible differences. """ def visible_difference_in_map_plot_options(): if not len(self.map_plot_options) and len(old_config.map_plot_options): return True for key in set(self.map_plot_options.keys()): if key in self.maps_to_show: if key not in old_config.map_plot_options: return True if self.map_plot_options[key].visible_changes(old_config.map_plot_options[key]): return True return False if any(getattr(self, key) != getattr(old_config, key) for key in filter(lambda key: key != 'map_plot_options', self.__dict__)): return True return visible_difference_in_map_plot_options()
[docs] def create_valid(self, data_info): """Creates a new configuration object with valid values. Args: data_info (mdt.visualization.maps.base.DataInfo): the data information Returns: MapPlotConfig: a valid map plot configuration """ config = deepcopy(self) if data_info.get_map_names(): max_dim = data_info.get_max_dimension() if max_dim < config.dimension: config.dimension = max_dim config.maps_to_show = list(filter(lambda k: k in data_info.get_map_names(), config.maps_to_show)) if config.mask_name not in data_info.get_map_names(): config.mask_name = None for name in config.maps_to_show: if name not in config.map_plot_options: config.map_plot_options[name] = SingleMapConfig() for name in list(config.map_plot_options): if name not in config.maps_to_show: del config.map_plot_options[name] return config
[docs] def validate(self, data_info): """Check if this configuration is valid given the provided data. Args: data_info (mdt.visualization.maps.base.DataInfo): the data information Raises: Exception: can raise multiple sorts of exceptions if this config is not valid given the data. """ if data_info.get_map_names(): self._validate_maps_to_show(data_info) self._validate_dimension(data_info) for key in self.__dict__: if hasattr(self, '_validate_' + key): getattr(self, '_validate_' + key)(data_info) return self
def _validate_maps_to_show(self, data_info): if any(map(lambda k: k not in data_info.get_map_names(), self.maps_to_show)): raise ValueError('One or more of the given maps to show is not in the data: {}'. format(set(self.maps_to_show).difference(set(data_info.get_map_names())))) def _validate_dimension(self, data_info): if self.maps_to_show: max_dim = data_info.get_max_dimension(map_names=self.maps_to_show) if self.dimension is None or self.dimension > max_dim: raise ValueError('The dimension ({}) can not be higher than {}.'.format(self.dimension, max_dim)) def _validate_slice_index(self, data_info): if self.maps_to_show: max_slice_index = data_info.get_max_slice_index(self.dimension, map_names=self.maps_to_show) if self.slice_index is None or self.slice_index > max_slice_index or self.slice_index < 0: raise ValueError('The slice index ({}) can not be higher than ' '{} or lower than 0.'.format(self.slice_index, max_slice_index)) def _validate_volume_index(self, data_info): if self.maps_to_show: max_volume_index = data_info.get_max_volume_index(map_names=self.maps_to_show) if self.volume_index > max_volume_index or self.volume_index < 0: raise ValueError('The volume index ({}) can not be higher than ' '{} or lower than 0.'.format(self.volume_index, max_volume_index)) def _validate_zoom(self, data_info): if self.maps_to_show: max_x = data_info.get_max_x_index(self.dimension, self.rotate, map_names=self.maps_to_show) max_y = data_info.get_max_y_index(self.dimension, self.rotate, map_names=self.maps_to_show) if self.zoom.p1.x > max_x: raise ValueError('The zoom maximum x ({}) can not be larger than {}'.format(self.zoom.p1.x, max_x)) if self.zoom.p1.y > max_y: raise ValueError('The zoom maximum y ({}) can not be larger than {}'.format(self.zoom.p1.y, max_y)) def _validate_mask_name(self, data_info): if self.mask_name: if self.mask_name not in data_info.get_map_names(): raise ValueError('The given global mask is not found in the list of maps.') def _validate_map_plot_options(self, data_info): def remove_not_in_data(): for key in list(self.map_plot_options): if key not in data_info.get_map_names(): del self.map_plot_options[key] def get_validated(): validated = {} for key, value in self.map_plot_options.items(): if value is not None: validated[key] = value.validate(data_info) self.map_plot_options = validated def remove_empty(): empty_config = SingleMapConfig() empty_removed = {} for key, value in self.map_plot_options.items(): if value != empty_config: empty_removed[key] = value self.map_plot_options = empty_removed def create_placeholders(): for map_name in self.maps_to_show: if map_name not in self.map_plot_options: self.map_plot_options[map_name] = SingleMapConfig() remove_not_in_data() get_validated() remove_empty() create_placeholders() def _validate_annotations(self, data_info): if self.annotations is None: self.annotations = [] for annotation in self.annotations: annotation.validate(data_info)
[docs]class SingleMapConfig(SimpleConvertibleConfig): def __init__(self, title=None, scale=None, clipping=None, colormap=None, colormap_masked_color=None, colorbar_label=None, show_title=None, title_spacing=None, mask_name=None, interpret_as_colormap=False, colormap_weight_map=None, colormap_order=None, colorbar_settings=None): """Creates the configuration for a single map plot. Args: title (str): the title of this plot, can contain latex using the matplotlib latex syntax scale (Scale): the scaling for the values in this map clipping (Clipping): the clipping to apply to the values prior to plotting colormap (str): the matplotlib colormap to use colormap_masked_color (str): the color for masked, NaN and infinity values. colorbar_label (str): the label for the colorbar show_title (boolean): if we want to show the title or not title_spacing (float): the spacing between the top of the plots and the title mask_name (str): the name of the mask used to mask the data prior to visualization interpret_as_colormap (boolean): if this is set to True and the referring map is a 4d volume with a vector of length 3 on the last dimension, then we can interpret this map as a colormap. This means that the elements of the last dimensions are used as (R, G, B) scalar values. colormap_weight_map (str): the name of another map to use as a scaling factor for this map. This is only used when ``interpret_as_colormap`` is set to True. This scales this map with the specified weight map. colormap_order (str): only used if ``interpret_as_colormap`` is used. This defines the order of the RGB components of the data. Valid strings are permutations of the letters RGB. colorbar_settings (ColorbarSettings): all colorbar related settings """ super().__init__() default_values = self.get_default_values() self.title = title self.title_spacing = title_spacing self.scale = scale or default_values['scale'] self.clipping = clipping or default_values['clipping'] self.colormap = colormap self.colormap_masked_color = colormap_masked_color self.colorbar_label = colorbar_label self.show_title = bool(show_title) if show_title is not None else default_values['show_title'] self.mask_name = mask_name self.interpret_as_colormap = bool(interpret_as_colormap) self.colormap_weight_map = colormap_weight_map self.colormap_order = colormap_order self.colorbar_settings = colorbar_settings or default_values['colorbar_settings'] if self.colormap is not None and self.colormap not in self.get_available_colormaps(): raise ValueError('The given colormap ({}) is not supported.'.format(self.colormap)) if colormap_order: if len(colormap_order) > 3 or not all(color in colormap_order.lower() for color in 'rgb'): raise ValueError('Incorrect colormap order specification, ' 'only permutations of "rgb" are allowed.'.format(colormap_order))
[docs] @staticmethod def get_default_values(): return { 'title': None, 'title_spacing': None, 'scale': Scale(), 'clipping': Clipping(), 'colormap': None, 'colormap_masked_color': None, 'colorbar_label': None, 'show_title': None, 'mask_name': None, 'interpret_as_colormap': False, 'colormap_weight_map': None, 'colormap_order': None, 'colorbar_settings': ColorbarSettings() }
@classmethod def _get_attribute_conversions(cls): return {'title': StringConversion(), 'scale': Scale.get_conversion_info(), 'clipping': Clipping.get_conversion_info(), 'colormap': StringConversion(), 'colormap_masked_color': StringConversion(), 'colorbar_label': StringConversion(), 'title_spacing': FloatConversion(), 'mask_name': StringConversion(), 'show_title': BooleanConversion(), 'interpret_as_colormap': BooleanConversion(allow_null=False), 'colormap_weight_map': StringConversion(), 'colormap_order': StringConversion(), 'colorbar_settings': ColorbarSettings.get_conversion_info()}
[docs] @classmethod def get_available_colormaps(cls): return _get_available_colormaps()
[docs] @classmethod def from_yaml(cls, text): return cls.get_conversion_info().from_dict(yaml.safe_load(text))
[docs] @classmethod def from_dict(cls, config_dict): return cls.get_conversion_info().from_dict(config_dict)
[docs] def to_dict(self, non_default_only=False): """Export this configuration to a dictionary Args: non_default_only (boolean): if True, we will only export those options that are not set to their default. Returns: dict: dict representation of the data """ data = self.get_conversion_info().to_dict(self) default_values = self.get_default_values() if non_default_only: for key, default in default_values.items(): data_value = self._get_attribute_conversions()[key].from_dict(data[key]) if data_value == default: del data[key] return data
[docs] def to_yaml(self, non_default_only=False): """Convert this configuration to a YAML string. Args: non_default_only (boolean): if True, we will only export those options that are not set to their default. Returns: str: a YAML representation of this configuration. """ return yaml.safe_dump(self.to_dict(non_default_only=non_default_only))
[docs] def visible_changes(self, old_config): """Checks if there are any visible changes between this configuration and the other. This method can implement knowledge that allows the visualization routine to check if it would need to update the plot or not. It expects that the configuration you wish to display is the one on which this method is called. Args: old_config (SingleMapConfig): the previous configuration Returns: bool: if the differences between this configuration and the other would result in visible differences. """ def filtered_attributes(): filtered = ['scale', 'clipping'] return [key for key in self.__dict__ if key not in filtered] def visible_changes_in_scale(): return self.scale.visible_changes(old_config.scale) def visible_changes_in_clipping(): return self.clipping.visible_changes(old_config.clipping) if any(getattr(self, key) != getattr(old_config, key) for key in filtered_attributes()): return True return visible_changes_in_clipping() or visible_changes_in_scale()
[docs] def validate(self, data_info): for key in self.__dict__: if hasattr(self, '_validate_' + key): getattr(self, '_validate_' + key)(data_info) return self
def _validate_mask_name(self, data_info): if self.mask_name is not None and self.mask_name not in data_info.get_map_names(): raise ValueError('The given mask name "{}" does not exist.'.format(self.mask_name)) def _validate_colormap_weight_map(self, data_info): if self.colormap_weight_map is not None and self.colormap_weight_map not in data_info.get_map_names(): raise ValueError('The given colormap weight map "{}" does not exist.'.format(self.colormap_weight_map))
def _get_available_interpolations(): """The available interpolations for either the general map plot config or the map specifics. Do not call these for outside use, rather, consult the class method of the specific config you want to change. Returns: list of str: the list of available interpolations. """ return ['none', 'nearest', 'bilinear', 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos'] def _get_available_colormaps(): """The available colormaps for either the general map plot config or the map specifics. Do not call these for outside use, rather, consult the class method of the specific config you want to change. Returns: list of str: the list of available colormaps. """ return sorted(matplotlib.cm.datad) def _load_data_info(nifti_files): """Load the data info for all the nifti files. In the case of conflicting simplified map names we name the maps to the shortest unique names. Returns: dict[str, SingleMapInfo]: the dictionary with the single map information """ data_info = {} for ind, map_name in enumerate(get_shortest_unique_names(nifti_files)): data_info[map_name] = SingleMapInfo.from_file(nifti_files[ind]) return data_info