diff --git a/.travis.yml b/.travis.yml index 367a105045..fe2cdf0b97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,11 +33,11 @@ matrix: # Absolute minimum dependencies - python: 2.7 env: - - DEPENDS="numpy==1.7.1" + - DEPENDS="numpy==1.8" # Absolute minimum dependencies - python: 2.7 env: - - DEPENDS="numpy==1.7.1" + - DEPENDS="numpy==1.8" - CHECK_TYPE="import" # Absolute minimum dependencies plus oldest MPL # Check these against: @@ -46,11 +46,11 @@ matrix: # requirements.txt - python: 2.7 env: - - DEPENDS="numpy==1.7.1 matplotlib==1.3.1" + - DEPENDS="numpy==1.8 matplotlib==1.3.1" # Minimum pydicom dependency - python: 2.7 env: - - DEPENDS="numpy==1.7.1 pydicom==0.9.9 pillow==2.6" + - DEPENDS="numpy==1.8 pydicom==0.9.9 pillow==2.6" # pydicom master branch - python: 3.5 env: diff --git a/doc/source/api.rst b/doc/source/api.rst index 1ae1bb416c..0f3cf1de26 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -23,6 +23,7 @@ File Formats analyze spm2analyze spm99analyze + cifti2 gifti freesurfer minc1 diff --git a/doc/source/installation.rst b/doc/source/installation.rst index ec942bd043..c853de9619 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -87,7 +87,7 @@ Requirements .travis.yml * Python_ 2.7, or >= 3.4 -* NumPy_ 1.7.1 or greater +* NumPy_ 1.8 or greater * Six_ 1.3 or greater * SciPy_ (optional, for full SPM-ANALYZE support) * PyDICOM_ 0.9.9 or greater (optional, for DICOM support) diff --git a/nibabel/cifti2/__init__.py b/nibabel/cifti2/__init__.py index 3025a6f991..9dc6dd68b8 100644 --- a/nibabel/cifti2/__init__.py +++ b/nibabel/cifti2/__init__.py @@ -6,7 +6,7 @@ # copyright and license terms. # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -"""CIfTI format IO +"""CIFTI-2 format IO .. currentmodule:: nibabel.cifti2 @@ -14,6 +14,7 @@ :toctree: ../generated cifti2 + cifti2_axes """ from .parse_cifti2 import Cifti2Extension @@ -25,3 +26,4 @@ Cifti2TransformationMatrixVoxelIndicesIJKtoXYZ, Cifti2Vertices, Cifti2Volume, CIFTI_BRAIN_STRUCTURES, Cifti2HeaderError, CIFTI_MODEL_TYPES, load, save) +from .cifti2_axes import (Axis, BrainModelAxis, ParcelsAxis, SeriesAxis, LabelAxis, ScalarAxis) \ No newline at end of file diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index 67dab1d0c2..8a4f12e767 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -6,18 +6,15 @@ # copyright and license terms. # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -''' Read / write access to CIfTI2 image format +''' Read / write access to CIFTI-2 image format Format of the NIFTI2 container format described here: http://www.nitrc.org/forum/message.php?msg_id=3738 -Definition of the CIFTI2 header format and file extensions attached to this -email: +Definition of the CIFTI-2 header format and file extensions can be found at: - http://www.nitrc.org/forum/forum.php?thread_id=4380&forum_id=1955 - -Filename is ``CIFTI-2_Main_FINAL_1March2014.pdf``. + http://www.nitrc.org/projects/cifti ''' from __future__ import division, print_function, absolute_import import re @@ -42,7 +39,7 @@ def _float_01(val): class Cifti2HeaderError(Exception): - """ Error in CIFTI2 header + """ Error in CIFTI-2 header """ @@ -178,7 +175,7 @@ def _to_xml_element(self): class Cifti2LabelTable(xml.XmlSerializable, MutableMapping): - """ CIFTI2 label table: a sequence of ``Cifti2Label``s + """ CIFTI-2 label table: a sequence of ``Cifti2Label``s * Description - Used by NamedMap when IndicesMapToDataType is "CIFTI_INDEX_TYPE_LABELS" in order to associate names and display colors @@ -236,7 +233,7 @@ def _to_xml_element(self): class Cifti2Label(xml.XmlSerializable): - """ CIFTI2 label: association of integer key with a name and RGBA values + """ CIFTI-2 label: association of integer key with a name and RGBA values For all color components, value is floating point with range 0.0 to 1.0. @@ -314,7 +311,7 @@ def _to_xml_element(self): class Cifti2NamedMap(xml.XmlSerializable): - """CIFTI2 named map: association of name and optional data with a map index + """CIFTI-2 named map: association of name and optional data with a map index Associates a name, optional metadata, and possibly a LabelTable with an index in a map. @@ -432,7 +429,7 @@ def _to_xml_element(self): class Cifti2VoxelIndicesIJK(xml.XmlSerializable, MutableSequence): - """CIFTI2 VoxelIndicesIJK: Set of voxel indices contained in a structure + """CIFTI-2 VoxelIndicesIJK: Set of voxel indices contained in a structure * Description - Identifies the voxels that model a brain structure, or participate in a parcel. Note that when this is a child of BrainModel, @@ -514,7 +511,7 @@ def _to_xml_element(self): class Cifti2Vertices(xml.XmlSerializable, MutableSequence): - """CIFTI2 vertices - association of brain structure and a list of vertices + """CIFTI-2 vertices - association of brain structure and a list of vertices * Description - Contains a BrainStructure type and a list of vertex indices within a Parcel. @@ -580,7 +577,7 @@ def _to_xml_element(self): class Cifti2Parcel(xml.XmlSerializable): - """CIFTI2 parcel: association of a name with vertices and/or voxels + """CIFTI-2 parcel: association of a name with vertices and/or voxels * Description - Associates a name, plus vertices and/or voxels, with an index. @@ -695,7 +692,7 @@ def _to_xml_element(self): class Cifti2Volume(xml.XmlSerializable): - """CIFTI2 volume: information about a volume for mappings that use voxels + """CIFTI-2 volume: information about a volume for mappings that use voxels * Description - Provides information about the volume for any mappings that use voxels. @@ -738,7 +735,7 @@ def _to_xml_element(self): class Cifti2VertexIndices(xml.XmlSerializable, MutableSequence): - """CIFTI2 vertex indices: vertex indices for an associated brain model + """CIFTI-2 vertex indices: vertex indices for an associated brain model The vertex indices (which are independent for each surface, and zero-based) that are used in this brain model[.] The parent @@ -1081,7 +1078,7 @@ def _to_xml_element(self): class Cifti2Matrix(xml.XmlSerializable, MutableSequence): - """ CIFTI2 Matrix object + """ CIFTI-2 Matrix object This is a list-like container where the elements are instances of :class:`Cifti2MatrixIndicesMap`. @@ -1213,7 +1210,7 @@ def _to_xml_element(self): class Cifti2Header(FileBasedHeader, xml.XmlSerializable): - ''' Class for CIFTI2 header extension ''' + ''' Class for CIFTI-2 header extension ''' def __init__(self, matrix=None, version="2.0"): FileBasedHeader.__init__(self) @@ -1268,9 +1265,43 @@ def get_index_map(self, index): ''' return self.matrix.get_index_map(index) + def get_axis(self, index): + ''' + Generates the Cifti2 axis for a given dimension + + Parameters + ---------- + index : int + Dimension for which we want to obtain the mapping. + + Returns + ------- + axis : :class:`.cifti2_axes.Axis` + ''' + from . import cifti2_axes + return cifti2_axes.from_index_mapping(self.matrix.get_index_map(index)) + + @classmethod + def from_axes(cls, axes): + ''' + Creates a new Cifti2 header based on the Cifti2 axes + + Parameters + ---------- + axes : tuple of :class`.cifti2_axes.Axis` + sequence of Cifti2 axes describing each row/column of the matrix to be stored + + Returns + ------- + header : Cifti2Header + new header describing the rows/columns in a format consistent with Cifti2 + ''' + from . import cifti2_axes + return cifti2_axes.to_header(axes) + class Cifti2Image(DataobjImage): - """ Class for single file CIFTI2 format image + """ Class for single file CIFTI-2 format image """ header_class = Cifti2Header valid_exts = Nifti2Image.valid_exts @@ -1297,8 +1328,10 @@ def __init__(self, Object containing image data. It should be some object that returns an array from ``np.asanyarray``. It should have a ``shape`` attribute or property. - header : Cifti2Header instance - Header with data for / from XML part of CIFTI2 format. + header : Cifti2Header instance or sequence of :class:`cifti2_axes.Axis` + Header with data for / from XML part of CIFTI-2 format. + Alternatively a sequence of cifti2_axes.Axis objects can be provided + describing each dimension of the array. nifti_header : None or mapping or NIfTI2 header instance, optional Metadata for NIfTI2 component of this format. extra : None or mapping @@ -1306,6 +1339,8 @@ def __init__(self, file_map : mapping, optional Mapping giving file information for this image format. ''' + if not isinstance(header, Cifti2Header) and header: + header = Cifti2Header.from_axes(header) super(Cifti2Image, self).__init__(dataobj, header=header, extra=extra, file_map=file_map) self._nifti_header = Nifti2Header.from_header(nifti_header) @@ -1321,7 +1356,7 @@ def nifti_header(self): @classmethod def from_file_map(klass, file_map): - """ Load a CIFTI2 image from a file_map + """ Load a CIFTI-2 image from a file_map Parameters ---------- @@ -1341,7 +1376,7 @@ def from_file_map(klass, file_map): cifti_header = item.get_content() break else: - raise ValueError('NIfTI2 header does not contain a CIFTI2 ' + raise ValueError('NIfTI2 header does not contain a CIFTI-2 ' 'extension') # Construct cifti image. @@ -1400,7 +1435,7 @@ def to_file_map(self, file_map=None): img.to_file_map(file_map or self.file_map) def update_headers(self): - ''' Harmonize CIFTI2 and NIfTI headers with image data + ''' Harmonize CIFTI-2 and NIfTI headers with image data >>> import numpy as np >>> data = np.zeros((2,3,4)) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py new file mode 100644 index 0000000000..30decec3d1 --- /dev/null +++ b/nibabel/cifti2/cifti2_axes.py @@ -0,0 +1,1464 @@ +""" +Defines :class:`Axis` objects to create, read, and manipulate CIFTI-2 files + +These axes provide an alternative interface to the information in the CIFTI-2 header. +Each type of CIFTI-2 axes describing the rows/columns in a CIFTI-2 matrix is given a unique class: + +* :class:`BrainModelAxis`: each row/column is a voxel or vertex +* :class:`ParcelsAxis`: each row/column is a group of voxels and/or vertices +* :class:`ScalarAxis`: each row/column has a unique name (with optional meta-data) +* :class:`LabelAxis`: each row/column has a unique name and label table (with optional meta-data) +* :class:`SeriesAxis`: each row/column is a timepoint, which increases monotonically + +All of these classes are derived from the Axis class. + +After loading a CIFTI-2 file a tuple of axes describing the rows and columns can be obtained +from the :meth:`.cifti2.Cifti2Header.get_axis` method on the header object +(e.g. ``nibabel.load().header.get_axis()``). Inversely, a new +:class:`.cifti2.Cifti2Header` object can be created from existing Axis objects +using the :meth:`.cifti2.Cifti2Header.from_axes` factory method. + +CIFTI-2 Axis objects of the same type can be concatenated using the '+'-operator. +Numpy indexing also works on axes +(except for SeriesAxis objects, which have to remain monotonically increasing or decreasing). + +Creating new CIFTI-2 axes +----------------------- +New Axis objects can be constructed by providing a description for what is contained +in each row/column of the described tensor. For each Axis sub-class this descriptor is: + +* :class:`BrainModelAxis`: a CIFTI-2 structure name and a voxel or vertex index +* :class:`ParcelsAxis`: a name and a sequence of voxel and vertex indices +* :class:`ScalarAxis`: a name and optionally a dict of meta-data +* :class:`LabelAxis`: a name, dict of label index to name and colour, + and optionally a dict of meta-data +* :class:`SeriesAxis`: the time-point of each row/column is set by setting the start, stop, size, + and unit of the time-series + +Several helper functions exist to create new :class:`BrainModelAxis` axes: + +* :meth:`BrainModelAxis.from_mask` creates a new BrainModelAxis volume covering the + non-zero values of a mask +* :meth:`BrainModelAxis.from_surface` creates a new BrainModelAxis surface covering the provided + indices of a surface + +A :class:`ParcelsAxis` axis can be created from a sequence of :class:`BrainModelAxis` axes using +:meth:`ParcelsAxis.from_brain_models`. + +Examples +-------- +We can create brain models covering the left cortex and left thalamus using: + +>>> from nibabel import cifti2 +>>> import numpy as np +>>> bm_cortex = cifti2.BrainModelAxis.from_mask([True, False, True, True], +... name='cortex_left') +>>> bm_thal = cifti2.BrainModelAxis.from_mask(np.ones((2, 2, 2)), affine=np.eye(4), +... name='thalamus_left') + +In this very simple case ``bm_cortex`` describes a left cortical surface skipping the second +out of four vertices. ``bm_thal`` contains all voxels in a 2x2x2 volume. + +Brain structure names automatically get converted to valid CIFTI-2 indentifiers using +:meth:`BrainModelAxis.to_cifti_brain_structure_name`. +A 1-dimensional mask will be automatically interpreted as a surface element and a 3-dimensional +mask as a volume element. + +These can be concatenated in a single brain model covering the left cortex and thalamus by +simply adding them together + +>>> bm_full = bm_cortex + bm_thal + +Brain models covering the full HCP grayordinate space can be constructed by adding all the +volumetric and surface brain models together like this (or by reading one from an already +existing HCP file). + +Getting a specific brain region from the full brain model is as simple as: + +>>> assert bm_full[bm_full.name == 'CIFTI_STRUCTURE_CORTEX_LEFT'] == bm_cortex +>>> assert bm_full[bm_full.name == 'CIFTI_STRUCTURE_THALAMUS_LEFT'] == bm_thal + +You can also iterate over all brain structures in a brain model: + +>>> for idx, (name, slc, bm) in enumerate(bm_full.iter_structures()): +... print((str(name), slc)) +... assert bm == bm_full[slc] +... assert bm == bm_cortex if idx == 0 else bm_thal +('CIFTI_STRUCTURE_CORTEX_LEFT', slice(0, 3, None)) +('CIFTI_STRUCTURE_THALAMUS_LEFT', slice(3, None, None)) + +In this case there will be two iterations, namely: +('CIFTI_STRUCTURE_CORTEX_LEFT', slice(0, ), bm_cortex) +and +('CIFTI_STRUCTURE_THALAMUS_LEFT', slice(, None), bm_thal) + +ParcelsAxis can be constructed from selections of these brain models: + +>>> parcel = cifti2.ParcelsAxis.from_brain_models([ +... ('surface_parcel', bm_cortex[:2]), # contains first 2 cortical vertices +... ('volume_parcel', bm_thal), # contains thalamus +... ('combined_parcel', bm_full[[1, 8, 10]]), # contains selected voxels/vertices +... ]) + +Time series are represented by their starting time (typically 0), step size +(i.e. sampling time or TR), and number of elements: + +>>> series = cifti2.SeriesAxis(start=0, step=100, size=5000) + +So a header for fMRI data with a TR of 100 ms covering the left cortex and thalamus with +5000 timepoints could be created with + +>>> type(cifti2.Cifti2Header.from_axes((series, bm_cortex + bm_thal))) + + +Similarly the curvature and cortical thickness on the left cortex could be stored using a header +like: + +>>> type(cifti2.Cifti2Header.from_axes((cifti2.ScalarAxis(['curvature', 'thickness']), +... bm_cortex))) + +""" +import numpy as np +from . import cifti2 +from six import string_types, add_metaclass, integer_types +from operator import xor +import abc + + +def from_index_mapping(mim): + """ + Parses the MatrixIndicesMap to find the appropriate CIFTI-2 axis describing the rows or columns + + Parameters + ---------- + mim : :class:`.cifti2.Cifti2MatrixIndicesMap` + + Returns + ------- + axis : subclass of :class:`Axis` + """ + return_type = {'CIFTI_INDEX_TYPE_SCALARS': ScalarAxis, + 'CIFTI_INDEX_TYPE_LABELS': LabelAxis, + 'CIFTI_INDEX_TYPE_SERIES': SeriesAxis, + 'CIFTI_INDEX_TYPE_BRAIN_MODELS': BrainModelAxis, + 'CIFTI_INDEX_TYPE_PARCELS': ParcelsAxis} + return return_type[mim.indices_map_to_data_type].from_index_mapping(mim) + + +def to_header(axes): + """ + Converts the axes describing the rows/columns of a CIFTI-2 vector/matrix to a Cifti2Header + + Parameters + ---------- + axes : iterable of :py:class:`Axis` objects + one or more axes describing each dimension in turn + + Returns + ------- + header : :class:`.cifti2.Cifti2Header` + """ + axes = tuple(axes) + mims_all = [] + matrix = cifti2.Cifti2Matrix() + for dim, ax in enumerate(axes): + if ax in axes[:dim]: + dim_prev = axes.index(ax) + mims_all[dim_prev].applies_to_matrix_dimension.append(dim) + mims_all.append(mims_all[dim_prev]) + else: + mim = ax.to_mapping(dim) + mims_all.append(mim) + matrix.append(mim) + return cifti2.Cifti2Header(matrix) + + +@add_metaclass(abc.ABCMeta) +class Axis(object): + """ + Abstract class for any object describing the rows or columns of a CIFTI-2 vector/matrix + + Mainly used for type checking. + + Base class for the following concrete CIFTI-2 axes: + + * :class:`BrainModelAxis`: each row/column is a voxel or vertex + * :class:`ParcelsAxis`: each row/column is a group of voxels and/or vertices + * :class:`ScalarAxis`: each row/column has a unique name with optional meta-data + * :class:`LabelAxis`: each row/column has a unique name and label table with optional meta-data + * :class:`SeriesAxis`: each row/column is a timepoint, which increases monotonically + """ + + @property + def size(self): + return len(self) + + @abc.abstractmethod + def __len__(self): + pass + + @abc.abstractmethod + def __eq__(self, other): + """ + Compares whether two Axes are equal + + Parameters + ---------- + other : Axis + other axis to compare to + + Returns + ------- + False if the axes don't have the same type or if their content differs + """ + pass + + @abc.abstractmethod + def __add__(self, other): + """ + Concatenates two Axes of the same type + + Parameters + ---------- + other : Axis + axis to be appended to the current one + + Returns + ------- + Axis of the same subtype as self and other + """ + pass + + @abc.abstractmethod + def __getitem__(self, item): + """ + Extracts definition of single row/column or new Axis describing a subset of the rows/columns + """ + pass + + +class BrainModelAxis(Axis): + """ + Each row/column in the CIFTI-2 vector/matrix represents a single vertex or voxel + + This Axis describes which vertex/voxel is represented by each row/column. + """ + + def __init__(self, name, voxel=None, vertex=None, affine=None, + volume_shape=None, nvertices=None): + """ + New BrainModelAxis axes can be constructed by passing on the greyordinate brain-structure + names and voxel/vertex indices to the constructor or by one of the + factory methods: + + - :py:meth:`~BrainModelAxis.from_mask`: creates surface or volumetric BrainModelAxis axis + from respectively 1D or 3D masks + - :py:meth:`~BrainModelAxis.from_surface`: creates a surface BrainModelAxis axis + + The resulting BrainModelAxis axes can be concatenated by adding them together. + + Parameters + ---------- + name : array_like + brain structure name or (N, ) string array with the brain structure names + voxel : array_like, optional + (N, 3) array with the voxel indices (can be omitted for CIFTI-2 files only + covering the surface) + vertex : array_like, optional + (N, ) array with the vertex indices (can be omitted for volumetric CIFTI-2 files) + affine : array_like, optional + (4, 4) array mapping voxel indices to mm space (not needed for CIFTI-2 files only + covering the surface) + volume_shape : tuple of three integers, optional + shape of the volume in which the voxels were defined (not needed for CIFTI-2 files only + covering the surface) + nvertices : dict from string to integer, optional + maps names of surface elements to integers (not needed for volumetric CIFTI-2 files) + """ + if voxel is None: + if vertex is None: + raise ValueError("At least one of voxel or vertex indices should be defined") + nelements = len(vertex) + self.voxel = np.full((nelements, 3), fill_value=-1, dtype=int) + else: + nelements = len(voxel) + self.voxel = np.asanyarray(voxel, dtype=int) + + if vertex is None: + self.vertex = np.full(nelements, fill_value=-1, dtype=int) + else: + self.vertex = np.asanyarray(vertex, dtype=int) + + if isinstance(name, string_types): + name = [self.to_cifti_brain_structure_name(name)] * self.vertex.size + self.name = np.asanyarray(name, dtype='U') + + if nvertices is None: + self.nvertices = {} + else: + self.nvertices = {self.to_cifti_brain_structure_name(name): number + for name, number in nvertices.items()} + + for name in list(self.nvertices.keys()): + if name not in self.name: + del self.nvertices[name] + + surface_mask = self.surface_mask + if surface_mask.all(): + self.affine = None + self.volume_shape = None + else: + if affine is None or volume_shape is None: + raise ValueError("Affine and volume shape should be defined " + "for BrainModelAxis containing voxels") + self.affine = np.asanyarray(affine) + self.volume_shape = volume_shape + + if np.any(self.vertex[surface_mask] < 0): + raise ValueError('Undefined vertex indices found for surface elements') + if np.any(self.voxel[~surface_mask] < 0): + raise ValueError('Undefined voxel indices found for volumetric elements') + + for check_name in ('name', 'voxel', 'vertex'): + shape = (self.size, 3) if check_name == 'voxel' else (self.size, ) + if getattr(self, check_name).shape != shape: + raise ValueError("Input {} has incorrect shape ({}) for BrainModelAxis axis".format( + check_name, getattr(self, check_name).shape)) + + @classmethod + def from_mask(cls, mask, name='other', affine=None): + """ + Creates a new BrainModelAxis axis describing the provided mask + + Parameters + ---------- + mask : array_like + all non-zero voxels will be included in the BrainModelAxis axis + should be (Nx, Ny, Nz) array for volume mask or (Nvertex, ) array for surface mask + name : str, optional + Name of the brain structure (e.g. 'CortexRight', 'thalamus_left' or 'brain_stem') + affine : array_like, optional + (4, 4) array with the voxel to mm transformation (defaults to identity matrix) + Argument will be ignored for surface masks + + Returns + ------- + BrainModelAxis which covers the provided mask + """ + if affine is None: + affine = np.eye(4) + else: + affine = np.asanyarray(affine) + if affine.shape != (4, 4): + raise ValueError("Affine transformation should be a 4x4 array or None, not %r" % affine) + + mask = np.asanyarray(mask) + if mask.ndim == 1: + return cls.from_surface(np.where(mask != 0)[0], mask.size, name=name) + elif mask.ndim == 3: + voxels = np.array(np.where(mask != 0)).T + return cls(name, voxel=voxels, affine=affine, volume_shape=mask.shape) + else: + raise ValueError("Mask should be either 1-dimensional (for surfaces) or " + "3-dimensional (for volumes), not %i-dimensional" % mask.ndim) + + @classmethod + def from_surface(cls, vertices, nvertex, name='Other'): + """ + Creates a new BrainModelAxis axis describing the vertices on a surface + + Parameters + ---------- + vertices : array_like + indices of the vertices on the surface + nvertex : int + total number of vertices on the surface + name : str + Name of the brain structure (e.g. 'CortexLeft' or 'CortexRight') + + Returns + ------- + BrainModelAxis which covers (part of) the surface + """ + cifti_name = cls.to_cifti_brain_structure_name(name) + return cls(cifti_name, vertex=vertices, + nvertices={cifti_name: nvertex}) + + @classmethod + def from_index_mapping(cls, mim): + """ + Creates a new BrainModel axis based on a CIFTI-2 dataset + + Parameters + ---------- + mim : :class:`.cifti2.Cifti2MatrixIndicesMap` + + Returns + ------- + BrainModelAxis + """ + nbm = sum(bm.index_count for bm in mim.brain_models) + voxel = np.full((nbm, 3), fill_value=-1, dtype=int) + vertex = np.full(nbm, fill_value=-1, dtype=int) + name = [] + + nvertices = {} + affine, shape = None, None + for bm in mim.brain_models: + index_end = bm.index_offset + bm.index_count + is_surface = bm.model_type == 'CIFTI_MODEL_TYPE_SURFACE' + name.extend([bm.brain_structure] * bm.index_count) + if is_surface: + vertex[bm.index_offset: index_end] = bm.vertex_indices + nvertices[bm.brain_structure] = bm.surface_number_of_vertices + else: + voxel[bm.index_offset: index_end, :] = bm.voxel_indices_ijk + if affine is None: + shape = mim.volume.volume_dimensions + affine = mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix + return cls(name, voxel, vertex, affine, shape, nvertices) + + def to_mapping(self, dim): + """ + Converts the brain model axis to a MatrixIndicesMap for storage in CIFTI-2 format + + Parameters + ---------- + dim : int + which dimension of the CIFTI-2 vector/matrix is described by this dataset (zero-based) + + Returns + ------- + :class:`.cifti2.Cifti2MatrixIndicesMap` + """ + mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_BRAIN_MODELS') + for name, to_slice, bm in self.iter_structures(): + is_surface = name in self.nvertices.keys() + if is_surface: + voxels = None + vertices = cifti2.Cifti2VertexIndices(bm.vertex) + nvertex = self.nvertices[name] + else: + voxels = cifti2.Cifti2VoxelIndicesIJK(bm.voxel) + vertices = None + nvertex = None + if mim.volume is None: + affine = cifti2.Cifti2TransformationMatrixVoxelIndicesIJKtoXYZ(-3, self.affine) + mim.volume = cifti2.Cifti2Volume(self.volume_shape, affine) + cifti_bm = cifti2.Cifti2BrainModel( + to_slice.start, len(bm), + 'CIFTI_MODEL_TYPE_SURFACE' if is_surface else 'CIFTI_MODEL_TYPE_VOXELS', + name, nvertex, voxels, vertices + ) + mim.append(cifti_bm) + return mim + + def iter_structures(self): + """ + Iterates over all brain structures in the order that they appear along the axis + + Yields + ------ + tuple with 3 elements: + - CIFTI-2 brain structure name + - slice to select the data associated with the brain structure from the tensor + - brain model covering that specific brain structure + """ + idx_start = 0 + start_name = self.name[idx_start] + for idx_current, name in enumerate(self.name): + if start_name != name: + yield start_name, slice(idx_start, idx_current), self[idx_start: idx_current] + idx_start = idx_current + start_name = self.name[idx_start] + yield start_name, slice(idx_start, None), self[idx_start:] + + @staticmethod + def to_cifti_brain_structure_name(name): + """ + Attempts to convert the name of an anatomical region in a format recognized by CIFTI-2 + + This function returns: + + - the name if it is in the CIFTI-2 format already + - if the name is a tuple the first element is assumed to be the structure name while + the second is assumed to be the hemisphere (left, right or both). The latter will default + to both. + - names like left_cortex, cortex_left, LeftCortex, or CortexLeft will be converted to + CIFTI_STRUCTURE_CORTEX_LEFT + + see :py:func:`nibabel.cifti2.tests.test_name` for examples of + which conversions are possible + + Parameters + ---------- + name: iterable of 2-element tuples of integer and string + input name of an anatomical region + + Returns + ------- + CIFTI-2 compatible name + + Raises + ------ + ValueError: raised if the input name does not match a known anatomical structure in CIFTI-2 + """ + if name in cifti2.CIFTI_BRAIN_STRUCTURES: + return name + if not isinstance(name, string_types): + if len(name) == 1: + structure = name[0] + orientation = 'both' + else: + structure, orientation = name + if structure.lower() in ('left', 'right', 'both'): + orientation, structure = name + else: + orient_names = ('left', 'right', 'both') + for poss_orient in orient_names: + idx = len(poss_orient) + if poss_orient == name.lower()[:idx]: + orientation = poss_orient + if name[idx] in '_ ': + structure = name[idx + 1:] + else: + structure = name[idx:] + break + if poss_orient == name.lower()[-idx:]: + orientation = poss_orient + if name[-idx - 1] in '_ ': + structure = name[:-idx - 1] + else: + structure = name[:-idx] + break + else: + orientation = 'both' + structure = name + if orientation.lower() == 'both': + proposed_name = 'CIFTI_STRUCTURE_%s' % structure.upper() + else: + proposed_name = 'CIFTI_STRUCTURE_%s_%s' % (structure.upper(), orientation.upper()) + if proposed_name not in cifti2.CIFTI_BRAIN_STRUCTURES: + raise ValueError('%s was interpreted as %s, which is not a valid CIFTI brain structure' + % (name, proposed_name)) + return proposed_name + + @property + def surface_mask(self): + """ + (N, ) boolean array which is true for any element on the surface + """ + return np.vectorize(lambda name: name in self.nvertices.keys())(self.name) + + @property + def volume_mask(self): + """ + (N, ) boolean array which is true for any element on the surface + """ + return np.vectorize(lambda name: name not in self.nvertices.keys())(self.name) + + _affine = None + + @property + def affine(self): + """ + Affine of the volumetric image in which the greyordinate voxels were defined + """ + return self._affine + + @affine.setter + def affine(self, value): + if value is not None: + value = np.asanyarray(value) + if value.shape != (4, 4): + raise ValueError('Affine transformation should be a 4x4 array') + self._affine = value + + _volume_shape = None + + @property + def volume_shape(self): + """ + Shape of the volumetric image in which the greyordinate voxels were defined + """ + return self._volume_shape + + @volume_shape.setter + def volume_shape(self, value): + if value is not None: + value = tuple(value) + if len(value) != 3: + raise ValueError("Volume shape should be a tuple of length 3") + if not all(isinstance(v, integer_types) for v in value): + raise ValueError("All elements of the volume shape should be integers") + self._volume_shape = value + + _name = None + + @property + def name(self): + """The brain structure to which the voxel/vertices of belong + """ + return self._name + + @name.setter + def name(self, values): + self._name = np.array([self.to_cifti_brain_structure_name(name) for name in values]) + + def __len__(self): + return self.name.size + + def __eq__(self, other): + if not isinstance(other, BrainModelAxis) or len(self) != len(other): + return False + if xor(self.affine is None, other.affine is None): + return False + return ( + (self.affine is None or + np.allclose(self.affine, other.affine) and + self.volume_shape == other.volume_shape) and + self.nvertices == other.nvertices and + np.array_equal(self.name, other.name) and + np.array_equal(self.voxel[self.volume_mask], other.voxel[other.volume_mask]) and + np.array_equal(self.vertex[self.surface_mask], other.vertex[other.surface_mask]) + ) + + def __add__(self, other): + """ + Concatenates two BrainModels + + Parameters + ---------- + other : BrainModelAxis + brain model to be appended to the current one + + Returns + ------- + BrainModelAxis + """ + if not isinstance(other, BrainModelAxis): + return NotImplemented + if self.affine is None: + affine, shape = other.affine, other.volume_shape + else: + affine, shape = self.affine, self.volume_shape + if other.affine is not None and ( + not np.allclose(other.affine, affine) or + other.volume_shape != shape + ): + raise ValueError("Trying to concatenate two BrainModels defined " + "in a different brain volume") + + nvertices = dict(self.nvertices) + for name, value in other.nvertices.items(): + if name in nvertices.keys() and nvertices[name] != value: + raise ValueError("Trying to concatenate two BrainModels with inconsistent " + "number of vertices for %s" % name) + nvertices[name] = value + return self.__class__( + np.append(self.name, other.name), + np.concatenate((self.voxel, other.voxel), 0), + np.append(self.vertex, other.vertex), + affine, shape, nvertices + ) + + def __getitem__(self, item): + """ + Extracts part of the brain structure + + Parameters + ---------- + item : anything that can index a 1D array + + Returns + ------- + If `item` is an integer returns a tuple with 3 elements: + - boolean, which is True if it is a surface element + - vertex index if it is a surface element, otherwise array with 3 voxel indices + - structure.BrainStructure object describing the brain structure the element was taken from + + Otherwise returns a new BrainModelAxis + """ + if isinstance(item, integer_types): + return self.get_element(item) + if isinstance(item, string_types): + raise IndexError("Can not index an Axis with a string (except for ParcelsAxis)") + return self.__class__(self.name[item], self.voxel[item], self.vertex[item], + self.affine, self.volume_shape, self.nvertices) + + def get_element(self, index): + """ + Describes a single element from the axis + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + tuple with 3 elements + - str, 'CIFTI_MODEL_TYPE_SURFACE' for vertex or 'CIFTI_MODEL_TYPE_VOXELS' for voxel + - vertex index if it is a surface element, otherwise array with 3 voxel indices + - structure.BrainStructure object describing the brain structure the element was taken from + """ + element_type = 'CIFTI_MODEL_TYPE_' + ( + 'SURFACE' if self.name[index] in self.nvertices.keys() else 'VOXELS' + ) + struct = self.vertex if 'SURFACE' in element_type else self.voxel + return element_type, struct[index], self.name[index] + + +class ParcelsAxis(Axis): + """ + Each row/column in the CIFTI-2 vector/matrix represents a parcel of voxels/vertices + + This Axis describes which parcel is represented by each row/column. + + Individual parcels can be accessed based on their name, using + ``parcel = parcel_axis[name]`` + """ + + def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvertices=None): + """ + Use of this constructor is not recommended. New ParcelsAxis axes can be constructed more + easily from a sequence of BrainModelAxis axes using + :py:meth:`~ParcelsAxis.from_brain_models` + + Parameters + ---------- + name : array_like + (N, ) string array with the parcel names + voxels : array_like + (N, ) object array each containing a sequence of voxels. + For each parcel the voxels are represented by a (M, 3) index array + vertices : array_like + (N, ) object array each containing a sequence of vertices. + For each parcel the vertices are represented by a mapping from brain structure name to + (M, ) index array + affine : array_like, optional + (4, 4) array mapping voxel indices to mm space (not needed for CIFTI-2 files only + covering the surface) + volume_shape : tuple of three integers, optional + shape of the volume in which the voxels were defined (not needed for CIFTI-2 files only + covering the surface) + nvertices : dict from string to integer, optional + maps names of surface elements to integers (not needed for volumetric CIFTI-2 files) + """ + self.name = np.asanyarray(name, dtype='U') + as_array = np.asanyarray(voxels) + if as_array.ndim == 1: + voxels = as_array.astype('object') + else: + voxels = np.empty(len(voxels), dtype='object') + for idx in range(len(voxels)): + voxels[idx] = as_array[idx] + self.voxels = np.asanyarray(voxels, dtype='object') + self.vertices = np.asanyarray(vertices, dtype='object') + self.affine = np.asanyarray(affine) if affine is not None else None + self.volume_shape = volume_shape + if nvertices is None: + self.nvertices = {} + else: + self.nvertices = {BrainModelAxis.to_cifti_brain_structure_name(name): number + for name, number in nvertices.items()} + + for check_name in ('name', 'voxels', 'vertices'): + if getattr(self, check_name).shape != (self.size, ): + raise ValueError("Input {} has incorrect shape ({}) for Parcel axis".format( + check_name, getattr(self, check_name).shape)) + + @classmethod + def from_brain_models(cls, named_brain_models): + """ + Creates a Parcel axis from a list of BrainModelAxis axes with names + + Parameters + ---------- + named_brain_models : iterable of 2-element tuples of string and BrainModelAxis + list of (parcel name, brain model representation) pairs defining each parcel + + Returns + ------- + ParcelsAxis + """ + nparcels = len(named_brain_models) + affine = None + volume_shape = None + all_names = [] + all_voxels = np.zeros(nparcels, dtype='object') + all_vertices = np.zeros(nparcels, dtype='object') + nvertices = {} + for idx_parcel, (parcel_name, bm) in enumerate(named_brain_models): + all_names.append(parcel_name) + + voxels = bm.voxel[bm.volume_mask] + if voxels.shape[0] != 0: + if affine is None: + affine = bm.affine + volume_shape = bm.volume_shape + elif not np.allclose(affine, bm.affine) or (volume_shape != bm.volume_shape): + raise ValueError("Can not combine brain models defined in different " + "volumes into a single Parcel axis") + all_voxels[idx_parcel] = voxels + + vertices = {} + for name, _, bm_part in bm.iter_structures(): + if name in bm.nvertices.keys(): + if name in nvertices.keys() and nvertices[name] != bm.nvertices[name]: + raise ValueError("Got multiple conflicting number of " + "vertices for surface structure %s" % name) + nvertices[name] = bm.nvertices[name] + vertices[name] = bm_part.vertex + all_vertices[idx_parcel] = vertices + return ParcelsAxis(all_names, all_voxels, all_vertices, affine, volume_shape, nvertices) + + @classmethod + def from_index_mapping(cls, mim): + """ + Creates a new Parcels axis based on a CIFTI-2 dataset + + Parameters + ---------- + mim : :class:`cifti2.Cifti2MatrixIndicesMap` + + Returns + ------- + ParcelsAxis + """ + nparcels = len(list(mim.parcels)) + all_names = [] + all_voxels = np.zeros(nparcels, dtype='object') + all_vertices = np.zeros(nparcels, dtype='object') + + volume_shape = None if mim.volume is None else mim.volume.volume_dimensions + affine = None + if mim.volume is not None: + affine = mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix + nvertices = {} + for surface in mim.surfaces: + nvertices[surface.brain_structure] = surface.surface_number_of_vertices + for idx_parcel, parcel in enumerate(mim.parcels): + nvoxels = 0 if parcel.voxel_indices_ijk is None else len(parcel.voxel_indices_ijk) + voxels = np.zeros((nvoxels, 3), dtype='i4') + if nvoxels != 0: + voxels[:] = parcel.voxel_indices_ijk + vertices = {} + for vertex in parcel.vertices: + name = vertex.brain_structure + vertices[vertex.brain_structure] = np.array(vertex) + if name not in nvertices.keys(): + raise ValueError("Number of vertices for surface structure %s not defined" % + name) + all_voxels[idx_parcel] = voxels + all_vertices[idx_parcel] = vertices + all_names.append(parcel.name) + return cls(all_names, all_voxels, all_vertices, affine, volume_shape, nvertices) + + def to_mapping(self, dim): + """ + Converts the Parcel to a MatrixIndicesMap for storage in CIFTI-2 format + + Parameters + ---------- + dim : int + which dimension of the CIFTI-2 vector/matrix is described by this dataset (zero-based) + + Returns + ------- + :class:`cifti2.Cifti2MatrixIndicesMap` + """ + mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_PARCELS') + if self.affine is not None: + affine = cifti2.Cifti2TransformationMatrixVoxelIndicesIJKtoXYZ(-3, matrix=self.affine) + mim.volume = cifti2.Cifti2Volume(self.volume_shape, affine) + for name, nvertex in self.nvertices.items(): + mim.append(cifti2.Cifti2Surface(name, nvertex)) + for name, voxels, vertices in zip(self.name, self.voxels, self.vertices): + cifti_voxels = cifti2.Cifti2VoxelIndicesIJK(voxels) + element = cifti2.Cifti2Parcel(name, cifti_voxels) + for name_vertex, idx_vertices in vertices.items(): + element.vertices.append(cifti2.Cifti2Vertices(name_vertex, idx_vertices)) + mim.append(element) + return mim + + _affine = None + + @property + def affine(self): + """ + Affine of the volumetric image in which the greyordinate voxels were defined + """ + return self._affine + + @affine.setter + def affine(self, value): + if value is not None: + value = np.asanyarray(value) + if value.shape != (4, 4): + raise ValueError('Affine transformation should be a 4x4 array') + self._affine = value + + _volume_shape = None + + @property + def volume_shape(self): + """ + Shape of the volumetric image in which the greyordinate voxels were defined + """ + return self._volume_shape + + @volume_shape.setter + def volume_shape(self, value): + if value is not None: + value = tuple(value) + if len(value) != 3: + raise ValueError("Volume shape should be a tuple of length 3") + if not all(isinstance(v, integer_types) for v in value): + raise ValueError("All elements of the volume shape should be integers") + self._volume_shape = value + + def __len__(self): + return self.name.size + + def __eq__(self, other): + if (self.__class__ != other.__class__ or len(self) != len(other) or + not np.array_equal(self.name, other.name) or self.nvertices != other.nvertices or + any(not np.array_equal(vox1, vox2) + for vox1, vox2 in zip(self.voxels, other.voxels))): + return False + if self.affine is not None: + if ( + other.affine is None or + not np.allclose(self.affine, other.affine) or + self.volume_shape != other.volume_shape + ): + return False + elif other.affine is not None: + return False + for vert1, vert2 in zip(self.vertices, other.vertices): + if len(vert1) != len(vert2): + return False + for name in vert1.keys(): + if name not in vert2 or not np.array_equal(vert1[name], vert2[name]): + return False + return True + + def __add__(self, other): + """ + Concatenates two Parcels + + Parameters + ---------- + other : ParcelsAxis + parcel to be appended to the current one + + Returns + ------- + Parcel + """ + if not isinstance(other, ParcelsAxis): + return NotImplemented + if self.affine is None: + affine, shape = other.affine, other.volume_shape + else: + affine, shape = self.affine, self.volume_shape + if other.affine is not None and (not np.allclose(other.affine, affine) or + other.volume_shape != shape): + raise ValueError("Trying to concatenate two ParcelsAxis defined " + "in a different brain volume") + nvertices = dict(self.nvertices) + for name, value in other.nvertices.items(): + if name in nvertices.keys() and nvertices[name] != value: + raise ValueError("Trying to concatenate two ParcelsAxis with inconsistent " + "number of vertices for %s" + % name) + nvertices[name] = value + return self.__class__( + np.append(self.name, other.name), + np.append(self.voxels, other.voxels), + np.append(self.vertices, other.vertices), + affine, shape, nvertices + ) + + def __getitem__(self, item): + """ + Extracts subset of the axes based on the type of ``item``: + + - `int`: 3-element tuple of (parcel name, parcel voxels, parcel vertices) + - `string`: 2-element tuple of (parcel voxels, parcel vertices + - other object that can index 1D arrays: new Parcel axis + """ + if isinstance(item, string_types): + idx = np.where(self.name == item)[0] + if len(idx) == 0: + raise IndexError("Parcel %s not found" % item) + if len(idx) > 1: + raise IndexError("Multiple parcels with name %s found" % item) + return self.voxels[idx[0]], self.vertices[idx[0]] + if isinstance(item, integer_types): + return self.get_element(item) + return self.__class__(self.name[item], self.voxels[item], self.vertices[item], + self.affine, self.volume_shape, self.nvertices) + + def get_element(self, index): + """ + Describes a single element from the axis + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + tuple with 3 elements + - unicode name of the parcel + - (M, 3) int array with voxel indices + - dict from string to (K, ) int array with vertex indices + for a specific surface brain structure + """ + return self.name[index], self.voxels[index], self.vertices[index] + + +class ScalarAxis(Axis): + """ + Along this axis of the CIFTI-2 vector/matrix each row/column has been given + a unique name and optionally metadata + """ + + def __init__(self, name, meta=None): + """ + Parameters + ---------- + name : array_like + (N, ) string array with the parcel names + meta : array_like + (N, ) object array with a dictionary of metadata for each row/column. + Defaults to empty dictionary + """ + self.name = np.asanyarray(name, dtype='U') + if meta is None: + meta = [{} for _ in range(self.name.size)] + self.meta = np.asanyarray(meta, dtype='object') + + for check_name in ('name', 'meta'): + if getattr(self, check_name).shape != (self.size, ): + raise ValueError("Input {} has incorrect shape ({}) for ScalarAxis axis".format( + check_name, getattr(self, check_name).shape)) + + @classmethod + def from_index_mapping(cls, mim): + """ + Creates a new Scalar axis based on a CIFTI-2 dataset + + Parameters + ---------- + mim : :class:`.cifti2.Cifti2MatrixIndicesMap` + + Returns + ------- + ScalarAxis + """ + names = [nm.map_name for nm in mim.named_maps] + meta = [{} if nm.metadata is None else dict(nm.metadata) for nm in mim.named_maps] + return cls(names, meta) + + def to_mapping(self, dim): + """ + Converts the hcp_labels to a MatrixIndicesMap for storage in CIFTI-2 format + + Parameters + ---------- + dim : int + which dimension of the CIFTI-2 vector/matrix is described by this dataset (zero-based) + + Returns + ------- + :class:`.cifti2.Cifti2MatrixIndicesMap` + """ + mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_SCALARS') + for name, meta in zip(self.name, self.meta): + meta = None if len(meta) == 0 else meta + named_map = cifti2.Cifti2NamedMap(name, cifti2.Cifti2MetaData(meta)) + mim.append(named_map) + return mim + + def __len__(self): + return self.name.size + + def __eq__(self, other): + """ + Compares two Scalars + + Parameters + ---------- + other : ScalarAxis + scalar axis to be compared + + Returns + ------- + bool : False if type, length or content do not match + """ + if not isinstance(other, ScalarAxis) or self.size != other.size: + return False + return np.array_equal(self.name, other.name) and np.array_equal(self.meta, other.meta) + + def __add__(self, other): + """ + Concatenates two Scalars + + Parameters + ---------- + other : ScalarAxis + scalar axis to be appended to the current one + + Returns + ------- + ScalarAxis + """ + if not isinstance(other, ScalarAxis): + return NotImplemented + return ScalarAxis( + np.append(self.name, other.name), + np.append(self.meta, other.meta), + ) + + def __getitem__(self, item): + if isinstance(item, integer_types): + return self.get_element(item) + return self.__class__(self.name[item], self.meta[item]) + + def get_element(self, index): + """ + Describes a single element from the axis + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + tuple with 2 elements + - unicode name of the row/column + - dictionary with the element metadata + """ + return self.name[index], self.meta[index] + + +class LabelAxis(Axis): + """ + Defines CIFTI-2 axis for label array. + + Along this axis of the CIFTI-2 vector/matrix each row/column has been given a unique name, + label table, and optionally metadata + """ + + def __init__(self, name, label, meta=None): + """ + Parameters + ---------- + name : array_like + (N, ) string array with the parcel names + label : array_like + single dictionary or (N, ) object array with dictionaries mapping + from integers to (name, (R, G, B, A)), where name is a string and R, G, B, and A are + floats between 0 and 1 giving the colour and alpha (i.e., transparency) + meta : array_like, optional + (N, ) object array with a dictionary of metadata for each row/column + """ + self.name = np.asanyarray(name, dtype='U') + if isinstance(label, dict): + label = [label.copy() for _ in range(self.name.size)] + self.label = np.asanyarray(label, dtype='object') + if meta is None: + meta = [{} for _ in range(self.name.size)] + self.meta = np.asanyarray(meta, dtype='object') + + for check_name in ('name', 'meta', 'label'): + if getattr(self, check_name).shape != (self.size, ): + raise ValueError("Input {} has incorrect shape ({}) for LabelAxis axis".format( + check_name, getattr(self, check_name).shape)) + + @classmethod + def from_index_mapping(cls, mim): + """ + Creates a new Label axis based on a CIFTI-2 dataset + + Parameters + ---------- + mim : :class:`.cifti2.Cifti2MatrixIndicesMap` + + Returns + ------- + LabelAxis + """ + tables = [{key: (value.label, value.rgba) for key, value in nm.label_table.items()} + for nm in mim.named_maps] + rest = ScalarAxis.from_index_mapping(mim) + return LabelAxis(rest.name, tables, rest.meta) + + def to_mapping(self, dim): + """ + Converts the hcp_labels to a MatrixIndicesMap for storage in CIFTI-2 format + + Parameters + ---------- + dim : int + which dimension of the CIFTI-2 vector/matrix is described by this dataset (zero-based) + + Returns + ------- + :class:`.cifti2.Cifti2MatrixIndicesMap` + """ + mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_LABELS') + for name, label, meta in zip(self.name, self.label, self.meta): + label_table = cifti2.Cifti2LabelTable() + for key, value in label.items(): + label_table[key] = (value[0],) + tuple(value[1]) + if len(meta) == 0: + meta = None + named_map = cifti2.Cifti2NamedMap(name, cifti2.Cifti2MetaData(meta), + label_table) + mim.append(named_map) + return mim + + def __len__(self): + return self.name.size + + def __eq__(self, other): + """ + Compares two Labels + + Parameters + ---------- + other : LabelAxis + label axis to be compared + + Returns + ------- + bool : False if type, length or content do not match + """ + if not isinstance(other, LabelAxis) or self.size != other.size: + return False + return ( + np.array_equal(self.name, other.name) and + np.array_equal(self.meta, other.meta) and + np.array_equal(self.label, other.label) + ) + + def __add__(self, other): + """ + Concatenates two Labels + + Parameters + ---------- + other : LabelAxis + label axis to be appended to the current one + + Returns + ------- + LabelAxis + """ + if not isinstance(other, LabelAxis): + return NotImplemented + return LabelAxis( + np.append(self.name, other.name), + np.append(self.label, other.label), + np.append(self.meta, other.meta), + ) + + def __getitem__(self, item): + if isinstance(item, integer_types): + return self.get_element(item) + return self.__class__(self.name[item], self.label[item], self.meta[item]) + + def get_element(self, index): + """ + Describes a single element from the axis + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + tuple with 2 elements + - unicode name of the row/column + - dictionary with the label table + - dictionary with the element metadata + """ + return self.name[index], self.label[index], self.meta[index] + + +class SeriesAxis(Axis): + """ + Along this axis of the CIFTI-2 vector/matrix the rows/columns increase monotonously in time + + This Axis describes the time point of each row/column. + """ + size = None + + def __init__(self, start, step, size, unit="SECOND"): + """ + Creates a new SeriesAxis axis + + Parameters + ---------- + start : float + starting time point + step : float + sampling time (TR) + size : int + number of time points + unit : str + Unit of the step size (one of 'second', 'hertz', 'meter', or 'radian') + """ + self.unit = unit + self.start = start + self.step = step + self.size = size + + @property + def time(self): + return np.arange(self.size) * self.step + self.start + + @classmethod + def from_index_mapping(cls, mim): + """ + Creates a new SeriesAxis axis based on a CIFTI-2 dataset + + Parameters + ---------- + mim : :class:`.cifti2.Cifti2MatrixIndicesMap` + + Returns + ------- + SeriesAxis + """ + start = mim.series_start * 10 ** mim.series_exponent + step = mim.series_step * 10 ** mim.series_exponent + return cls(start, step, mim.number_of_series_points, mim.series_unit) + + def to_mapping(self, dim): + """ + Converts the SeriesAxis to a MatrixIndicesMap for storage in CIFTI-2 format + + Parameters + ---------- + dim : int + which dimension of the CIFTI-2 vector/matrix is described by this dataset (zero-based) + + Returns + ------- + :class:`cifti2.Cifti2MatrixIndicesMap` + """ + mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_SERIES') + mim.series_exponent = 0 + mim.series_start = self.start + mim.series_step = self.step + mim.number_of_series_points = self.size + mim.series_unit = self.unit + return mim + + _unit = None + + @property + def unit(self): + return self._unit + + @unit.setter + def unit(self, value): + if value.upper() not in ("SECOND", "HERTZ", "METER", "RADIAN"): + raise ValueError("SeriesAxis unit should be one of " + + "('second', 'hertz', 'meter', or 'radian'") + self._unit = value.upper() + + def __len__(self): + return self.size + + def __eq__(self, other): + """ + True if start, step, size, and unit are the same. + """ + return ( + isinstance(other, SeriesAxis) and + self.start == other.start and + self.step == other.step and + self.size == other.size and + self.unit == other.unit + ) + + def __add__(self, other): + """ + Concatenates two SeriesAxis + + Parameters + ---------- + other : SeriesAxis + Time SeriesAxis to append at the end of the current time SeriesAxis. + Note that the starting time of the other time SeriesAxis is ignored. + + Returns + ------- + SeriesAxis + New time SeriesAxis with the concatenation of the two + + Raises + ------ + ValueError + raised if the repetition time of the two time SeriesAxis is different + """ + if isinstance(other, SeriesAxis): + if other.step != self.step: + raise ValueError('Can only concatenate SeriesAxis with the same step size') + if other.unit != self.unit: + raise ValueError('Can only concatenate SeriesAxis with the same unit') + return SeriesAxis(self.start, self.step, self.size + other.size, self.unit) + return NotImplemented + + def __getitem__(self, item): + if isinstance(item, slice): + step = 1 if item.step is None else item.step + idx_start = ((self.size - 1 if step < 0 else 0) + if item.start is None else + (item.start if item.start >= 0 else self.size + item.start)) + idx_end = ((-1 if step < 0 else self.size) + if item.stop is None else + (item.stop if item.stop >= 0 else self.size + item.stop)) + if idx_start > self.size and step < 0: + idx_start = self.size - 1 + if idx_end > self.size: + idx_end = self.size + nelements = (idx_end - idx_start) // step + if nelements < 0: + nelements = 0 + return SeriesAxis(idx_start * self.step + self.start, self.step * step, + nelements, self.unit) + elif isinstance(item, integer_types): + return self.get_element(item) + raise IndexError('SeriesAxis can only be indexed with integers or slices ' + 'without breaking the regular structure') + + def get_element(self, index): + """ + Gives the time point of a specific row/column + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + float + """ + original_index = index + if index < 0: + index = self.size + index + if index >= self.size or index < 0: + raise IndexError("index %i is out of range for SeriesAxis with size %i" % + (original_index, self.size)) + return self.start + self.step * index diff --git a/nibabel/cifti2/parse_cifti2.py b/nibabel/cifti2/parse_cifti2.py index f0df76ac7d..608636a446 100644 --- a/nibabel/cifti2/parse_cifti2.py +++ b/nibabel/cifti2/parse_cifti2.py @@ -94,7 +94,7 @@ def may_contain_header(klass, binaryblock): @staticmethod def _chk_qfac(hdr, fix=False): - # Allow qfac of 0 without complaint for CIFTI2 + # Allow qfac of 0 without complaint for CIFTI-2 rep = Report(HeaderDataError) if hdr['pixdim'][0] in (-1, 0, 1): return hdr, rep @@ -127,7 +127,7 @@ class _Cifti2AsNiftiImage(Nifti2Image): class Cifti2Parser(xml.XmlParser): - '''Class to parse an XML string into a CIFTI2 header object''' + '''Class to parse an XML string into a CIFTI-2 header object''' def __init__(self, encoding=None, buffer_size=3500000, verbose=0): super(Cifti2Parser, self).__init__(encoding=encoding, buffer_size=buffer_size, @@ -164,7 +164,7 @@ def StartElementHandler(self, name, attrs): parent = self.struct_state[-1] if not isinstance(parent, Cifti2Header): raise Cifti2HeaderError( - 'Matrix element can only be a child of the CIFTI2 Header element' + 'Matrix element can only be a child of the CIFTI-2 Header element' ) parent.matrix = matrix self.struct_state.append(matrix) @@ -175,7 +175,8 @@ def StartElementHandler(self, name, attrs): parent = self.struct_state[-1] if not isinstance(parent, (Cifti2Matrix, Cifti2NamedMap)): raise Cifti2HeaderError( - 'MetaData element can only be a child of the CIFTI2 Matrix or NamedMap elements' + 'MetaData element can only be a child of the CIFTI-2 Matrix ' + 'or NamedMap elements' ) self.struct_state.append(meta) @@ -207,7 +208,7 @@ def StartElementHandler(self, name, attrs): matrix = self.struct_state[-1] if not isinstance(matrix, Cifti2Matrix): raise Cifti2HeaderError( - 'MatrixIndicesMap element can only be a child of the CIFTI2 Matrix element' + 'MatrixIndicesMap element can only be a child of the CIFTI-2 Matrix element' ) matrix.append(mim) self.struct_state.append(mim) @@ -218,7 +219,7 @@ def StartElementHandler(self, name, attrs): mim = self.struct_state[-1] if not isinstance(mim, Cifti2MatrixIndicesMap): raise Cifti2HeaderError( - 'NamedMap element can only be a child of the CIFTI2 MatrixIndicesMap element' + 'NamedMap element can only be a child of the CIFTI-2 MatrixIndicesMap element' ) self.struct_state.append(named_map) mim.append(named_map) @@ -234,7 +235,7 @@ def StartElementHandler(self, name, attrs): lata = Cifti2LabelTable() if not isinstance(named_map, Cifti2NamedMap): raise Cifti2HeaderError( - 'LabelTable element can only be a child of the CIFTI2 NamedMap element' + 'LabelTable element can only be a child of the CIFTI-2 NamedMap element' ) self.fsm_state.append('LabelTable') self.struct_state.append(lata) @@ -244,7 +245,7 @@ def StartElementHandler(self, name, attrs): lata = self.struct_state[-1] if not isinstance(lata, Cifti2LabelTable): raise Cifti2HeaderError( - 'Label element can only be a child of the CIFTI2 LabelTable element' + 'Label element can only be a child of the CIFTI-2 LabelTable element' ) label = Cifti2Label() label.key = int(attrs["Key"]) @@ -260,7 +261,7 @@ def StartElementHandler(self, name, attrs): named_map = self.struct_state[-1] if not isinstance(named_map, Cifti2NamedMap): raise Cifti2HeaderError( - 'MapName element can only be a child of the CIFTI2 NamedMap element' + 'MapName element can only be a child of the CIFTI-2 NamedMap element' ) self.fsm_state.append('MapName') @@ -271,7 +272,7 @@ def StartElementHandler(self, name, attrs): mim = self.struct_state[-1] if not isinstance(mim, Cifti2MatrixIndicesMap): raise Cifti2HeaderError( - 'Surface element can only be a child of the CIFTI2 MatrixIndicesMap element' + 'Surface element can only be a child of the CIFTI-2 MatrixIndicesMap element' ) if mim.indices_map_to_data_type != "CIFTI_INDEX_TYPE_PARCELS": raise Cifti2HeaderError( @@ -287,7 +288,7 @@ def StartElementHandler(self, name, attrs): mim = self.struct_state[-1] if not isinstance(mim, Cifti2MatrixIndicesMap): raise Cifti2HeaderError( - 'Parcel element can only be a child of the CIFTI2 MatrixIndicesMap element' + 'Parcel element can only be a child of the CIFTI-2 MatrixIndicesMap element' ) parcel.name = attrs["Name"] mim.append(parcel) @@ -299,7 +300,7 @@ def StartElementHandler(self, name, attrs): parcel = self.struct_state[-1] if not isinstance(parcel, Cifti2Parcel): raise Cifti2HeaderError( - 'Vertices element can only be a child of the CIFTI2 Parcel element' + 'Vertices element can only be a child of the CIFTI-2 Parcel element' ) vertices.brain_structure = attrs["BrainStructure"] if vertices.brain_structure not in CIFTI_BRAIN_STRUCTURES: @@ -315,7 +316,7 @@ def StartElementHandler(self, name, attrs): parent = self.struct_state[-1] if not isinstance(parent, (Cifti2Parcel, Cifti2BrainModel)): raise Cifti2HeaderError( - 'VoxelIndicesIJK element can only be a child of the CIFTI2 ' + 'VoxelIndicesIJK element can only be a child of the CIFTI-2 ' 'Parcel or BrainModel elements' ) parent.voxel_indices_ijk = Cifti2VoxelIndicesIJK() @@ -325,7 +326,7 @@ def StartElementHandler(self, name, attrs): mim = self.struct_state[-1] if not isinstance(mim, Cifti2MatrixIndicesMap): raise Cifti2HeaderError( - 'Volume element can only be a child of the CIFTI2 MatrixIndicesMap element' + 'Volume element can only be a child of the CIFTI-2 MatrixIndicesMap element' ) dimensions = tuple([int(val) for val in attrs["VolumeDimensions"].split(',')]) @@ -339,7 +340,7 @@ def StartElementHandler(self, name, attrs): if not isinstance(volume, Cifti2Volume): raise Cifti2HeaderError( 'TransformationMatrixVoxelIndicesIJKtoXYZ element can only be a child ' - 'of the CIFTI2 Volume element' + 'of the CIFTI-2 Volume element' ) transform = Cifti2TransformationMatrixVoxelIndicesIJKtoXYZ() transform.meter_exponent = int(attrs["MeterExponent"]) @@ -354,7 +355,7 @@ def StartElementHandler(self, name, attrs): if not isinstance(mim, Cifti2MatrixIndicesMap): raise Cifti2HeaderError( 'BrainModel element can only be a child ' - 'of the CIFTI2 MatrixIndicesMap element' + 'of the CIFTI-2 MatrixIndicesMap element' ) if mim.indices_map_to_data_type != "CIFTI_INDEX_TYPE_BRAIN_MODELS": raise Cifti2HeaderError( @@ -386,7 +387,7 @@ def StartElementHandler(self, name, attrs): if not isinstance(model, Cifti2BrainModel): raise Cifti2HeaderError( 'VertexIndices element can only be a child ' - 'of the CIFTI2 BrainModel element' + 'of the CIFTI-2 BrainModel element' ) self.fsm_state.append('VertexIndices') model.vertex_indices = index diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py new file mode 100644 index 0000000000..56457187a2 --- /dev/null +++ b/nibabel/cifti2/tests/test_axes.py @@ -0,0 +1,644 @@ +import numpy as np +from nose.tools import assert_raises +from .test_cifti2io_axes import check_rewrite +import nibabel.cifti2.cifti2_axes as axes +from copy import deepcopy + + +rand_affine = np.random.randn(4, 4) +vol_shape = (5, 10, 3) +use_label = {0: ('something', (0.2, 0.4, 0.1, 0.5)), 1: ('even better', (0.3, 0.8, 0.43, 0.9))} + + +def get_brain_models(): + """ + Generates a set of practice BrainModelAxis axes + + Yields + ------ + BrainModelAxis axis + """ + mask = np.zeros(vol_shape) + mask[0, 1, 2] = 1 + mask[0, 4, 2] = True + mask[0, 4, 0] = True + yield axes.BrainModelAxis.from_mask(mask, 'ThalamusRight', rand_affine) + mask[0, 0, 0] = True + yield axes.BrainModelAxis.from_mask(mask, affine=rand_affine) + + yield axes.BrainModelAxis.from_surface([0, 5, 10], 15, 'CortexLeft') + yield axes.BrainModelAxis.from_surface([0, 5, 10, 13], 15) + + surface_mask = np.zeros(15, dtype='bool') + surface_mask[[2, 9, 14]] = True + yield axes.BrainModelAxis.from_mask(surface_mask, name='CortexRight') + + +def get_parcels(): + """ + Generates a practice Parcel axis out of all practice brain models + + Returns + ------- + Parcel axis + """ + bml = list(get_brain_models()) + return axes.ParcelsAxis.from_brain_models([('mixed', bml[0] + bml[2]), ('volume', bml[1]), ('surface', bml[3])]) + + +def get_scalar(): + """ + Generates a practice ScalarAxis axis with names ('one', 'two', 'three') + + Returns + ------- + ScalarAxis axis + """ + return axes.ScalarAxis(['one', 'two', 'three']) + + +def get_label(): + """ + Generates a practice LabelAxis axis with names ('one', 'two', 'three') and two labels + + Returns + ------- + LabelAxis axis + """ + return axes.LabelAxis(['one', 'two', 'three'], use_label) + + +def get_series(): + """ + Generates a set of 4 practice SeriesAxis axes with different starting times/lengths/time steps and units + + Yields + ------ + SeriesAxis axis + """ + yield axes.SeriesAxis(3, 10, 4) + yield axes.SeriesAxis(8, 10, 3) + yield axes.SeriesAxis(3, 2, 4) + yield axes.SeriesAxis(5, 10, 5, "HERTZ") + + +def get_axes(): + """ + Iterates through all of the practice axes defined in the functions above + + Yields + ------ + Cifti2 axis + """ + yield get_parcels() + yield get_scalar() + yield get_label() + for elem in get_brain_models(): + yield elem + for elem in get_series(): + yield elem + + +def test_brain_models(): + """ + Tests the introspection and creation of CIFTI-2 BrainModelAxis axes + """ + bml = list(get_brain_models()) + assert len(bml[0]) == 3 + assert (bml[0].vertex == -1).all() + assert (bml[0].voxel == [[0, 1, 2], [0, 4, 0], [0, 4, 2]]).all() + assert bml[0][1][0] == 'CIFTI_MODEL_TYPE_VOXELS' + assert (bml[0][1][1] == [0, 4, 0]).all() + assert bml[0][1][2] == axes.BrainModelAxis.to_cifti_brain_structure_name('thalamus_right') + assert len(bml[1]) == 4 + assert (bml[1].vertex == -1).all() + assert (bml[1].voxel == [[0, 0, 0], [0, 1, 2], [0, 4, 0], [0, 4, 2]]).all() + assert len(bml[2]) == 3 + assert (bml[2].voxel == -1).all() + assert (bml[2].vertex == [0, 5, 10]).all() + assert bml[2][1] == ('CIFTI_MODEL_TYPE_SURFACE', 5, 'CIFTI_STRUCTURE_CORTEX_LEFT') + assert len(bml[3]) == 4 + assert (bml[3].voxel == -1).all() + assert (bml[3].vertex == [0, 5, 10, 13]).all() + assert bml[4][1] == ('CIFTI_MODEL_TYPE_SURFACE', 9, 'CIFTI_STRUCTURE_CORTEX_RIGHT') + assert len(bml[4]) == 3 + assert (bml[4].voxel == -1).all() + assert (bml[4].vertex == [2, 9, 14]).all() + + for bm, label, is_surface in zip(bml, ['ThalamusRight', 'Other', 'cortex_left', 'Other'], + (False, False, True, True)): + assert np.all(bm.surface_mask == ~bm.volume_mask) + structures = list(bm.iter_structures()) + assert len(structures) == 1 + name = structures[0][0] + assert name == axes.BrainModelAxis.to_cifti_brain_structure_name(label) + if is_surface: + assert bm.nvertices[name] == 15 + else: + assert name not in bm.nvertices + assert (bm.affine == rand_affine).all() + assert bm.volume_shape == vol_shape + + bmt = bml[0] + bml[1] + bml[2] + assert len(bmt) == 10 + structures = list(bmt.iter_structures()) + assert len(structures) == 3 + for bm, (name, _, bm_split) in zip(bml[:3], structures): + assert bm == bm_split + assert (bm_split.name == name).all() + assert bm == bmt[bmt.name == bm.name[0]] + assert bm == bmt[np.where(bmt.name == bm.name[0])] + + bmt = bmt + bml[2] + assert len(bmt) == 13 + structures = list(bmt.iter_structures()) + assert len(structures) == 3 + assert len(structures[-1][2]) == 6 + + # break brain model + bmt.affine = np.eye(4) + with assert_raises(ValueError): + bmt.affine = np.eye(3) + with assert_raises(ValueError): + bmt.affine = np.eye(4).flatten() + + bmt.volume_shape = (5, 3, 1) + with assert_raises(ValueError): + bmt.volume_shape = (5., 3, 1) + with assert_raises(ValueError): + bmt.volume_shape = (5, 3, 1, 4) + + with assert_raises(IndexError): + bmt['thalamus_left'] + + # Test the constructor + bm_vox = axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) + assert np.all(bm_vox.name == ['CIFTI_STRUCTURE_THALAMUS_LEFT'] * 5) + assert np.array_equal(bm_vox.vertex, np.full(5, -1)) + assert np.array_equal(bm_vox.voxel, np.full((5, 3), 1)) + with assert_raises(ValueError): + # no volume shape + axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4)) + with assert_raises(ValueError): + # no affine + axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), volume_shape=(2, 3, 4)) + with assert_raises(ValueError): + # incorrect name + axes.BrainModelAxis('random_name', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) + with assert_raises(ValueError): + # negative voxel indices + axes.BrainModelAxis('thalamus_left', voxel=-np.ones((5, 3), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) + with assert_raises(ValueError): + # no voxels or vertices + axes.BrainModelAxis('thalamus_left', affine=np.eye(4), volume_shape=(2, 3, 4)) + with assert_raises(ValueError): + # incorrect voxel shape + axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 2), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) + + bm_vertex = axes.BrainModelAxis('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 20}) + assert np.array_equal(bm_vertex.name, ['CIFTI_STRUCTURE_CORTEX_LEFT'] * 5) + assert np.array_equal(bm_vertex.vertex, np.full(5, 1)) + assert np.array_equal(bm_vertex.voxel, np.full((5, 3), -1)) + with assert_raises(ValueError): + axes.BrainModelAxis('cortex_left', vertex=np.ones(5, dtype=int)) + with assert_raises(ValueError): + axes.BrainModelAxis('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_right': 20}) + with assert_raises(ValueError): + axes.BrainModelAxis('cortex_left', vertex=-np.ones(5, dtype=int), nvertices={'cortex_left': 20}) + + # test from_mask errors + with assert_raises(ValueError): + # affine should be 4x4 matrix + axes.BrainModelAxis.from_mask(np.arange(5) > 2, affine=np.ones(5)) + with assert_raises(ValueError): + # only 1D or 3D masks accepted + axes.BrainModelAxis.from_mask(np.ones((5, 3))) + + # tests error in adding together or combining as ParcelsAxis + bm_vox = axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), + affine=np.eye(4), volume_shape=(2, 3, 4)) + bm_vox + bm_vox + assert (bm_vertex + bm_vox)[:bm_vertex.size] == bm_vertex + assert (bm_vox + bm_vertex)[:bm_vox.size] == bm_vox + for bm_added in (bm_vox + bm_vertex, bm_vertex + bm_vox): + assert bm_added.nvertices == bm_vertex.nvertices + assert np.all(bm_added.affine == bm_vox.affine) + assert bm_added.volume_shape == bm_vox.volume_shape + + axes.ParcelsAxis.from_brain_models([('a', bm_vox), ('b', bm_vox)]) + with assert_raises(Exception): + bm_vox + get_label() + + bm_other_shape = axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), + affine=np.eye(4), volume_shape=(4, 3, 4)) + with assert_raises(ValueError): + bm_vox + bm_other_shape + with assert_raises(ValueError): + axes.ParcelsAxis.from_brain_models([('a', bm_vox), ('b', bm_other_shape)]) + bm_other_affine = axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), + affine=np.eye(4) * 2, volume_shape=(2, 3, 4)) + with assert_raises(ValueError): + bm_vox + bm_other_affine + with assert_raises(ValueError): + axes.ParcelsAxis.from_brain_models([('a', bm_vox), ('b', bm_other_affine)]) + + bm_vertex = axes.BrainModelAxis('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 20}) + bm_other_number = axes.BrainModelAxis('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 30}) + with assert_raises(ValueError): + bm_vertex + bm_other_number + with assert_raises(ValueError): + axes.ParcelsAxis.from_brain_models([('a', bm_vertex), ('b', bm_other_number)]) + + # test equalities + bm_vox = axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), + affine=np.eye(4), volume_shape=(2, 3, 4)) + bm_other = deepcopy(bm_vox) + assert bm_vox == bm_other + bm_other.voxel[1, 0] = 0 + assert bm_vox != bm_other + + bm_other = deepcopy(bm_vox) + bm_other.vertex[1] = 10 + assert bm_vox == bm_other, 'vertices are ignored in volumetric BrainModelAxis' + + bm_other = deepcopy(bm_vox) + bm_other.name[1] = 'BRAIN_STRUCTURE_OTHER' + assert bm_vox != bm_other + + bm_other = deepcopy(bm_vox) + bm_other.affine[0, 0] = 10 + assert bm_vox != bm_other + + bm_other = deepcopy(bm_vox) + bm_other.affine = None + assert bm_vox != bm_other + assert bm_other != bm_vox + + bm_other = deepcopy(bm_vox) + bm_other.volume_shape = (10, 3, 4) + assert bm_vox != bm_other + + bm_vertex = axes.BrainModelAxis('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 20}) + bm_other = deepcopy(bm_vertex) + assert bm_vertex == bm_other + bm_other.voxel[1, 0] = 0 + assert bm_vertex == bm_other, 'voxels are ignored in surface BrainModelAxis' + + bm_other = deepcopy(bm_vertex) + bm_other.vertex[1] = 10 + assert bm_vertex != bm_other + + bm_other = deepcopy(bm_vertex) + bm_other.name[1] = 'BRAIN_STRUCTURE_CORTEX_RIGHT' + assert bm_vertex != bm_other + + bm_other = deepcopy(bm_vertex) + bm_other.nvertices['BRAIN_STRUCTURE_CORTEX_LEFT'] = 50 + assert bm_vertex != bm_other + + bm_other = deepcopy(bm_vertex) + bm_other.nvertices['BRAIN_STRUCTURE_CORTEX_RIGHT'] = 20 + assert bm_vertex != bm_other + + assert bm_vox != get_parcels() + assert bm_vertex != get_parcels() + + +def test_parcels(): + """ + Test the introspection and creation of CIFTI-2 Parcel axes + """ + prc = get_parcels() + assert isinstance(prc, axes.ParcelsAxis) + assert prc[0] == ('mixed', ) + prc['mixed'] + assert prc['mixed'][0].shape == (3, 3) + assert len(prc['mixed'][1]) == 1 + assert prc['mixed'][1]['CIFTI_STRUCTURE_CORTEX_LEFT'].shape == (3, ) + + assert prc[1] == ('volume', ) + prc['volume'] + assert prc['volume'][0].shape == (4, 3) + assert len(prc['volume'][1]) == 0 + + assert prc[2] == ('surface', ) + prc['surface'] + assert prc['surface'][0].shape == (0, 3) + assert len(prc['surface'][1]) == 1 + assert prc['surface'][1]['CIFTI_STRUCTURE_OTHER'].shape == (4, ) + + prc2 = prc + prc + assert len(prc2) == 6 + assert (prc2.affine == prc.affine).all() + assert (prc2.nvertices == prc.nvertices) + assert (prc2.volume_shape == prc.volume_shape) + assert prc2[:3] == prc + assert prc2[3:] == prc + + assert prc2[3:]['mixed'][0].shape == (3, 3) + assert len(prc2[3:]['mixed'][1]) == 1 + assert prc2[3:]['mixed'][1]['CIFTI_STRUCTURE_CORTEX_LEFT'].shape == (3, ) + + with assert_raises(IndexError): + prc['non_existent'] + + prc['surface'] + with assert_raises(IndexError): + # parcel exists twice + prc2['surface'] + + # break parcels + prc.affine = np.eye(4) + with assert_raises(ValueError): + prc.affine = np.eye(3) + with assert_raises(ValueError): + prc.affine = np.eye(4).flatten() + + prc.volume_shape = (5, 3, 1) + with assert_raises(ValueError): + prc.volume_shape = (5., 3, 1) + with assert_raises(ValueError): + prc.volume_shape = (5, 3, 1, 4) + + # break adding of parcels + with assert_raises(Exception): + prc + get_label() + + prc = get_parcels() + other_prc = get_parcels() + prc + other_prc + + other_prc = get_parcels() + other_prc.affine = np.eye(4) * 2 + with assert_raises(ValueError): + prc + other_prc + + other_prc = get_parcels() + other_prc.volume_shape = (20, 3, 4) + with assert_raises(ValueError): + prc + other_prc + + # test parcel equalities + prc = get_parcels() + assert prc != get_scalar() + + prc_other = deepcopy(prc) + assert prc == prc_other + assert prc != prc_other[:2] + assert prc == prc_other[:] + prc_other.affine[0, 0] = 10 + assert prc != prc_other + + prc_other = deepcopy(prc) + prc_other.affine = None + assert prc != prc_other + assert prc_other != prc + assert (prc + prc_other).affine is not None + assert (prc_other + prc).affine is not None + + prc_other = deepcopy(prc) + prc_other.volume_shape = (10, 3, 4) + assert prc != prc_other + with assert_raises(ValueError): + prc + prc_other + + prc_other = deepcopy(prc) + prc_other.nvertices['CIFTI_STRUCTURE_CORTEX_LEFT'] = 80 + assert prc != prc_other + with assert_raises(ValueError): + prc + prc_other + + prc_other = deepcopy(prc) + prc_other.voxels[0] = np.ones((2, 3), dtype='i4') + assert prc != prc_other + + prc_other = deepcopy(prc) + prc_other.voxels[0] = prc_other.voxels * 2 + assert prc != prc_other + + prc_other = deepcopy(prc) + prc_other.vertices[0]['CIFTI_STRUCTURE_CORTEX_LEFT'] = np.ones((8, ), dtype='i4') + assert prc != prc_other + + prc_other = deepcopy(prc) + prc_other.vertices[0]['CIFTI_STRUCTURE_CORTEX_LEFT'] *= 2 + assert prc != prc_other + + prc_other = deepcopy(prc) + prc_other.name[0] = 'new_name' + assert prc != prc_other + + # test direct initialisation + axes.ParcelsAxis( + voxels=[np.ones((3, 2), dtype=int)], + vertices=[{}], + name=['single_voxel'], + affine=np.eye(4), + volume_shape=(2, 3, 4), + ) + + with assert_raises(ValueError): + axes.ParcelsAxis( + voxels=[np.ones((3, 2), dtype=int)], + vertices=[{}], + name=[['single_voxel']], # wrong shape name array + affine=np.eye(4), + volume_shape=(2, 3, 4), + ) + + +def test_scalar(): + """ + Test the introspection and creation of CIFTI-2 ScalarAxis axes + """ + sc = get_scalar() + assert len(sc) == 3 + assert isinstance(sc, axes.ScalarAxis) + assert (sc.name == ['one', 'two', 'three']).all() + assert (sc.meta == [{}] * 3).all() + assert sc[1] == ('two', {}) + sc2 = sc + sc + assert len(sc2) == 6 + assert (sc2.name == ['one', 'two', 'three', 'one', 'two', 'three']).all() + assert (sc2.meta == [{}] * 6).all() + assert sc2[:3] == sc + assert sc2[3:] == sc + + sc.meta[1]['a'] = 3 + assert 'a' not in sc.meta + + # test equalities + assert sc != get_label() + with assert_raises(Exception): + sc + get_label() + + sc_other = deepcopy(sc) + assert sc == sc_other + assert sc != sc_other[:2] + assert sc == sc_other[:] + sc_other.name[0] = 'new_name' + assert sc != sc_other + + sc_other = deepcopy(sc) + sc_other.meta[0]['new_key'] = 'new_entry' + assert sc != sc_other + sc.meta[0]['new_key'] = 'new_entry' + assert sc == sc_other + + # test constructor + assert axes.ScalarAxis(['scalar_name'], [{}]) == axes.ScalarAxis(['scalar_name']) + + with assert_raises(ValueError): + axes.ScalarAxis([['scalar_name']]) # wrong shape + + with assert_raises(ValueError): + axes.ScalarAxis(['scalar_name'], [{}, {}]) # wrong size + + +def test_label(): + """ + Test the introspection and creation of CIFTI-2 ScalarAxis axes + """ + lab = get_label() + assert len(lab) == 3 + assert isinstance(lab, axes.LabelAxis) + assert (lab.name == ['one', 'two', 'three']).all() + assert (lab.meta == [{}] * 3).all() + assert (lab.label == [use_label] * 3).all() + assert lab[1] == ('two', use_label, {}) + lab2 = lab + lab + assert len(lab2) == 6 + assert (lab2.name == ['one', 'two', 'three', 'one', 'two', 'three']).all() + assert (lab2.meta == [{}] * 6).all() + assert (lab2.label == [use_label] * 6).all() + assert lab2[:3] == lab + assert lab2[3:] == lab + + # test equalities + lab = get_label() + assert lab != get_scalar() + with assert_raises(Exception): + lab + get_scalar() + + other_lab = deepcopy(lab) + assert lab != other_lab[:2] + assert lab == other_lab[:] + other_lab.name[0] = 'new_name' + assert lab != other_lab + + other_lab = deepcopy(lab) + other_lab.meta[0]['new_key'] = 'new_item' + assert 'new_key' not in other_lab.meta[1] + assert lab != other_lab + lab.meta[0]['new_key'] = 'new_item' + assert lab == other_lab + + other_lab = deepcopy(lab) + other_lab.label[0][20] = ('new_label', (0, 0, 0, 1)) + assert lab != other_lab + assert 20 not in other_lab.label[1] + lab.label[0][20] = ('new_label', (0, 0, 0, 1)) + assert lab == other_lab + + # test constructor + assert axes.LabelAxis(['scalar_name'], [{}], [{}]) == axes.LabelAxis(['scalar_name'], [{}]) + + with assert_raises(ValueError): + axes.LabelAxis([['scalar_name']], [{}]) # wrong shape + + with assert_raises(ValueError): + axes.LabelAxis(['scalar_name'], [{}, {}]) # wrong size + + +def test_series(): + """ + Test the introspection and creation of CIFTI-2 SeriesAxis axes + """ + sr = list(get_series()) + assert sr[0].unit == 'SECOND' + assert sr[1].unit == 'SECOND' + assert sr[2].unit == 'SECOND' + assert sr[3].unit == 'HERTZ' + sr[0].unit = 'hertz' + assert sr[0].unit == 'HERTZ' + with assert_raises(ValueError): + sr[0].unit = 'non_existent' + + sr = list(get_series()) + assert (sr[0].time == np.arange(4) * 10 + 3).all() + assert (sr[1].time == np.arange(3) * 10 + 8).all() + assert (sr[2].time == np.arange(4) * 2 + 3).all() + assert ((sr[0] + sr[1]).time == np.arange(7) * 10 + 3).all() + assert ((sr[1] + sr[0]).time == np.arange(7) * 10 + 8).all() + assert ((sr[1] + sr[0] + sr[0]).time == np.arange(11) * 10 + 8).all() + assert sr[1][2] == 28 + assert sr[1][-2] == sr[1].time[-2] + assert_raises(ValueError, lambda: sr[0] + sr[2]) + assert_raises(ValueError, lambda: sr[2] + sr[1]) + assert_raises(ValueError, lambda: sr[0] + sr[3]) + assert_raises(ValueError, lambda: sr[3] + sr[1]) + assert_raises(ValueError, lambda: sr[3] + sr[2]) + + # test slicing + assert (sr[0][1:3].time == sr[0].time[1:3]).all() + assert (sr[0][1:].time == sr[0].time[1:]).all() + assert (sr[0][:-2].time == sr[0].time[:-2]).all() + assert (sr[0][1:-1].time == sr[0].time[1:-1]).all() + assert (sr[0][1:-1:2].time == sr[0].time[1:-1:2]).all() + assert (sr[0][::2].time == sr[0].time[::2]).all() + assert (sr[0][:10:2].time == sr[0].time[::2]).all() + assert (sr[0][10:].time == sr[0].time[10:]).all() + assert (sr[0][10:12].time == sr[0].time[10:12]).all() + assert (sr[0][10::-1].time == sr[0].time[10::-1]).all() + assert (sr[0][3:1:-1].time == sr[0].time[3:1:-1]).all() + assert (sr[0][1:3:-1].time == sr[0].time[1:3:-1]).all() + + with assert_raises(IndexError): + assert sr[0][[0, 1]] + with assert_raises(IndexError): + assert sr[0][20] + with assert_raises(IndexError): + assert sr[0][-20] + + # test_equalities + sr = next(get_series()) + with assert_raises(Exception): + sr + get_scalar() + assert sr != sr[:2] + assert sr == sr[:] + + for key, value in ( + ('start', 20), + ('step', 7), + ('size', 14), + ('unit', 'HERTZ'), + ): + sr_other = deepcopy(sr) + assert sr == sr_other + setattr(sr_other, key, value) + assert sr != sr_other + + +def test_writing(): + """ + Tests the writing and reading back in of custom created CIFTI-2 axes + """ + for ax1 in get_axes(): + for ax2 in get_axes(): + arr = np.random.randn(len(ax1), len(ax2)) + check_rewrite(arr, (ax1, ax2)) + + +def test_common_interface(): + """ + Tests the common interface for all custom created CIFTI-2 axes + """ + for axis1, axis2 in zip(get_axes(), get_axes()): + assert axis1 == axis2 + concatenated = axis1 + axis2 + assert axis1 != concatenated + assert axis1 == concatenated[:axis1.size] + if isinstance(axis1, axes.SeriesAxis): + assert axis2 != concatenated[axis1.size:] + else: + assert axis2 == concatenated[axis1.size:] + + assert len(axis1) == axis1.size + diff --git a/nibabel/cifti2/tests/test_cifti2.py b/nibabel/cifti2/tests/test_cifti2.py index ce71b92bcc..6054c126b0 100644 --- a/nibabel/cifti2/tests/test_cifti2.py +++ b/nibabel/cifti2/tests/test_cifti2.py @@ -1,4 +1,4 @@ -""" Testing CIFTI2 objects +""" Testing CIFTI-2 objects """ import collections from xml.etree import ElementTree diff --git a/nibabel/cifti2/tests/test_cifti2io_axes.py b/nibabel/cifti2/tests/test_cifti2io_axes.py new file mode 100644 index 0000000000..4089395b78 --- /dev/null +++ b/nibabel/cifti2/tests/test_cifti2io_axes.py @@ -0,0 +1,180 @@ +from nibabel.cifti2 import cifti2_axes, cifti2 +from nibabel.tests.nibabel_data import get_nibabel_data, needs_nibabel_data +import nibabel as nib +import os +import numpy as np +import tempfile + +test_directory = os.path.join(get_nibabel_data(), 'nitest-cifti2') + +hcp_labels = ['CortexLeft', 'CortexRight', 'AccumbensLeft', 'AccumbensRight', 'AmygdalaLeft', 'AmygdalaRight', + 'brain_stem', 'CaudateLeft', 'CaudateRight', 'CerebellumLeft', 'CerebellumRight', + 'Diencephalon_ventral_left', 'Diencephalon_ventral_right', 'HippocampusLeft', 'HippocampusRight', + 'PallidumLeft', 'PallidumRight', 'PutamenLeft', 'PutamenRight', 'ThalamusLeft', 'ThalamusRight'] + +hcp_n_elements = [29696, 29716, 135, 140, 315, 332, 3472, 728, 755, 8709, 9144, 706, + 712, 764, 795, 297, 260, 1060, 1010, 1288, 1248] + +hcp_affine = np.array([[ -2., 0., 0., 90.], + [ 0., 2., 0., -126.], + [ 0., 0., 2., -72.], + [ 0., 0., 0., 1.]]) + + +def check_hcp_grayordinates(brain_model): + """Checks that a BrainModelAxis matches the expected 32k HCP grayordinates + """ + assert isinstance(brain_model, cifti2_axes.BrainModelAxis) + structures = list(brain_model.iter_structures()) + assert len(structures) == len(hcp_labels) + idx_start = 0 + for idx, (name, _, bm), label, nel in zip(range(len(structures)), structures, hcp_labels, hcp_n_elements): + if idx < 2: + assert name in bm.nvertices.keys() + assert (bm.voxel == -1).all() + assert (bm.vertex != -1).any() + assert bm.nvertices[name] == 32492 + else: + assert name not in bm.nvertices.keys() + assert (bm.voxel != -1).any() + assert (bm.vertex == -1).all() + assert (bm.affine == hcp_affine).all() + assert bm.volume_shape == (91, 109, 91) + assert name == cifti2_axes.BrainModelAxis.to_cifti_brain_structure_name(label) + assert len(bm) == nel + assert (bm.name == brain_model.name[idx_start:idx_start + nel]).all() + assert (bm.voxel == brain_model.voxel[idx_start:idx_start + nel]).all() + assert (bm.vertex == brain_model.vertex[idx_start:idx_start + nel]).all() + idx_start += nel + assert idx_start == len(brain_model) + + assert (brain_model.vertex[:5] == np.arange(5)).all() + assert structures[0][2].vertex[-1] == 32491 + assert structures[1][2].vertex[0] == 0 + assert structures[1][2].vertex[-1] == 32491 + assert structures[-1][2].name[-1] == brain_model.name[-1] + assert (structures[-1][2].voxel[-1] == brain_model.voxel[-1]).all() + assert structures[-1][2].vertex[-1] == brain_model.vertex[-1] + assert (brain_model.voxel[-1] == [38, 55, 46]).all() + assert (brain_model.voxel[70000] == [56, 22, 19]).all() + + +def check_Conte69(brain_model): + """Checks that the BrainModelAxis matches the expected Conte69 surface coordinates + """ + assert isinstance(brain_model, cifti2_axes.BrainModelAxis) + structures = list(brain_model.iter_structures()) + assert len(structures) == 2 + assert structures[0][0] == 'CIFTI_STRUCTURE_CORTEX_LEFT' + assert structures[0][2].surface_mask.all() + assert structures[1][0] == 'CIFTI_STRUCTURE_CORTEX_RIGHT' + assert structures[1][2].surface_mask.all() + assert (brain_model.voxel == -1).all() + + assert (brain_model.vertex[:5] == np.arange(5)).all() + assert structures[0][2].vertex[-1] == 32491 + assert structures[1][2].vertex[0] == 0 + assert structures[1][2].vertex[-1] == 32491 + + +def check_rewrite(arr, axes, extension='.nii'): + """ + Checks wheter writing the Cifti2 array to disc and reading it back in gives the same object + + Parameters + ---------- + arr : array + N-dimensional array of data + axes : Sequence[cifti2_axes.Axis] + sequence of length N with the meaning of the rows/columns along each dimension + extension : str + custom extension to use + """ + (fd, name) = tempfile.mkstemp(extension) + cifti2.Cifti2Image(arr, header=axes).to_filename(name) + img = nib.load(name) + arr2 = img.get_data() + assert (arr == arr2).all() + for idx in range(len(img.shape)): + assert (axes[idx] == img.header.get_axis(idx)) + return img + + +@needs_nibabel_data('nitest-cifti2') +def test_read_ones(): + img = nib.load(os.path.join(test_directory, 'ones.dscalar.nii')) + arr = img.get_data() + axes = [img.header.get_axis(dim) for dim in range(2)] + assert (arr == 1).all() + assert isinstance(axes[0], cifti2_axes.ScalarAxis) + assert len(axes[0]) == 1 + assert axes[0].name[0] == 'ones' + assert axes[0].meta[0] == {} + check_hcp_grayordinates(axes[1]) + img = check_rewrite(arr, axes) + check_hcp_grayordinates(img.header.get_axis(1)) + + +@needs_nibabel_data('nitest-cifti2') +def test_read_conte69_dscalar(): + img = nib.load(os.path.join(test_directory, 'Conte69.MyelinAndCorrThickness.32k_fs_LR.dscalar.nii')) + arr = img.get_data() + axes = [img.header.get_axis(dim) for dim in range(2)] + assert isinstance(axes[0], cifti2_axes.ScalarAxis) + assert len(axes[0]) == 2 + assert axes[0].name[0] == 'MyelinMap_BC_decurv' + assert axes[0].name[1] == 'corrThickness' + assert axes[0].meta[0] == {'PaletteColorMapping': '\n MODE_AUTO_SCALE_PERCENTAGE\n 98.000000 2.000000 2.000000 98.000000\n -100.000000 0.000000 0.000000 100.000000\n ROY-BIG-BL\n true\n true\n false\n true\n THRESHOLD_TEST_SHOW_OUTSIDE\n THRESHOLD_TYPE_OFF\n false\n -1.000000 1.000000\n -1.000000 1.000000\n -1.000000 1.000000\n \n PALETTE_THRESHOLD_RANGE_MODE_MAP\n'} + check_Conte69(axes[1]) + check_rewrite(arr, axes) + + +@needs_nibabel_data('nitest-cifti2') +def test_read_conte69_dtseries(): + img = nib.load(os.path.join(test_directory, 'Conte69.MyelinAndCorrThickness.32k_fs_LR.dtseries.nii')) + arr = img.get_data() + axes = [img.header.get_axis(dim) for dim in range(2)] + assert isinstance(axes[0], cifti2_axes.SeriesAxis) + assert len(axes[0]) == 2 + assert axes[0].start == 0 + assert axes[0].step == 1 + assert axes[0].size == arr.shape[0] + assert (axes[0].time == [0, 1]).all() + check_Conte69(axes[1]) + check_rewrite(arr, axes) + + +@needs_nibabel_data('nitest-cifti2') +def test_read_conte69_dlabel(): + img = nib.load(os.path.join(test_directory, 'Conte69.parcellations_VGD11b.32k_fs_LR.dlabel.nii')) + arr = img.get_data() + axes = [img.header.get_axis(dim) for dim in range(2)] + assert isinstance(axes[0], cifti2_axes.LabelAxis) + assert len(axes[0]) == 3 + assert (axes[0].name == ['Composite Parcellation-lh (FRB08_OFP03_retinotopic)', + 'Brodmann lh (from colin.R via pals_R-to-fs_LR)', 'MEDIAL WALL lh (fs_LR)']).all() + assert axes[0].label[1][70] == ('19_B05', (1.0, 0.867, 0.467, 1.0)) + assert (axes[0].meta == [{}] * 3).all() + check_Conte69(axes[1]) + check_rewrite(arr, axes) + + +@needs_nibabel_data('nitest-cifti2') +def test_read_conte69_ptseries(): + img = nib.load(os.path.join(test_directory, 'Conte69.MyelinAndCorrThickness.32k_fs_LR.ptseries.nii')) + arr = img.get_data() + axes = [img.header.get_axis(dim) for dim in range(2)] + assert isinstance(axes[0], cifti2_axes.SeriesAxis) + assert len(axes[0]) == 2 + assert axes[0].start == 0 + assert axes[0].step == 1 + assert axes[0].size == arr.shape[0] + assert (axes[0].time == [0, 1]).all() + + assert len(axes[1]) == 54 + voxels, vertices = axes[1]['ER_FRB08'] + assert voxels.shape == (0, 3) + assert len(vertices) == 2 + assert vertices['CIFTI_STRUCTURE_CORTEX_LEFT'].shape == (206 // 2, ) + assert vertices['CIFTI_STRUCTURE_CORTEX_RIGHT'].shape == (206 // 2, ) + check_rewrite(arr, axes) diff --git a/nibabel/cifti2/tests/test_cifti2io.py b/nibabel/cifti2/tests/test_cifti2io_header.py similarity index 99% rename from nibabel/cifti2/tests/test_cifti2io.py rename to nibabel/cifti2/tests/test_cifti2io_header.py index 521e112847..e4970625a4 100644 --- a/nibabel/cifti2/tests/test_cifti2io.py +++ b/nibabel/cifti2/tests/test_cifti2io_header.py @@ -43,7 +43,7 @@ def test_read_nifti2(): - # Error trying to read a CIFTI2 image from a NIfTI2-only image. + # Error trying to read a CIFTI-2 image from a NIfTI2-only image. filemap = ci.Cifti2Image.make_file_map() for k in filemap: filemap[k].fileobj = io.open(NIFTI2_DATA) diff --git a/nibabel/cifti2/tests/test_name.py b/nibabel/cifti2/tests/test_name.py new file mode 100644 index 0000000000..6b53d46523 --- /dev/null +++ b/nibabel/cifti2/tests/test_name.py @@ -0,0 +1,19 @@ +from nibabel.cifti2 import cifti2_axes + +equivalents = [('CIFTI_STRUCTURE_CORTEX_LEFT', ('CortexLeft', 'LeftCortex', 'left_cortex', 'Left Cortex', + 'Cortex_Left', 'cortex left', 'CORTEX_LEFT', 'LEFT CORTEX', + ('cortex', 'left'), ('CORTEX', 'Left'), ('LEFT', 'coRTEX'))), + ('CIFTI_STRUCTURE_CORTEX', ('Cortex', 'CortexBOTH', 'Cortex_both', 'both cortex', + 'BOTH_CORTEX', 'cortex', 'CORTEX', ('cortex', ), + ('COrtex', 'Both'), ('both', 'cortex')))] + + +def test_name_conversion(): + """ + Tests the automatic name conversion to a format recognized by CIFTI-2 + """ + func = cifti2_axes.BrainModelAxis.to_cifti_brain_structure_name + for base_name, input_names in equivalents: + assert base_name == func(base_name) + for name in input_names: + assert base_name == func(name) \ No newline at end of file diff --git a/nibabel/cifti2/tests/test_new_cifti2.py b/nibabel/cifti2/tests/test_new_cifti2.py index 9ead1e3088..01bc742a22 100644 --- a/nibabel/cifti2/tests/test_new_cifti2.py +++ b/nibabel/cifti2/tests/test_new_cifti2.py @@ -1,4 +1,4 @@ -"""Tests the generation of new CIFTI2 files from scratch +"""Tests the generation of new CIFTI-2 files from scratch Contains a series of functions to create and check each of the 5 CIFTI index types (i.e. BRAIN_MODELS, PARCELS, SCALARS, LABELS, and SERIES). diff --git a/nibabel/info.py b/nibabel/info.py index 56892c6efa..8035940b37 100644 --- a/nibabel/info.py +++ b/nibabel/info.py @@ -186,7 +186,7 @@ def cmp_pkg_version(version_str, pkg_version_str=__version__): # doc/source/installation.rst # requirements.txt # .travis.yml -NUMPY_MIN_VERSION = '1.7.1' +NUMPY_MIN_VERSION = '1.8' PYDICOM_MIN_VERSION = '0.9.9' SIX_MIN_VERSION = '1.3' diff --git a/requirements.txt b/requirements.txt index 061fa37bef..6299333665 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,4 @@ # doc/source/installation.rst six>=1.3 -numpy>=1.7.1 +numpy>=1.8