import glob
import json
import os
import h5py
import numpy as np
from .. import options
from .braindata import BrainData, VertexData, VolumeData
default_cmap = options.config.get("basic", "default_cmap")
# register_cmap is deprecated in matplotlib > 3.7.0 and replaced by colormaps.register
try:
from matplotlib import colormaps as cm
def register_cmap(cmap):
return cm.register(cmap)
except ImportError:
from matplotlib.cm import register_cmap
def normalize(data):
if isinstance(data, tuple):
if len(data) == 3:
if data[0].dtype == np.uint8:
return VolumeRGB(data[0][...,0], data[0][...,1], data[0][...,2], *data[1:])
return Volume(*data)
elif len(data) == 2:
return Vertex(*data)
else:
raise TypeError("Invalid input for Dataview")
elif isinstance(data, Dataview):
return data
else:
raise TypeError("Invalid input for Dataview")
def _from_hdf_data(h5, name, xfmname=None, subject=None, **kwargs):
"""Decodes a __hash named node from an HDF file into the
constituent Vertex or Volume object"""
dnode = h5.get("/data/%s"%name)
if dnode is None:
dnode = h5.get(name)
attrs = {k: u(v) for (k, v) in dnode.attrs.items()}
if subject is None:
subject = attrs['subject']
#support old style xfmname saving as attribute
if xfmname is None and 'xfmname' in attrs:
xfmname = attrs['xfmname']
mask = None
if 'mask' in attrs:
if attrs['mask'].startswith("__"):
mask = h5['/subjects/%s/transforms/%s/masks/%s' %
(attrs['subject'], xfmname, attrs['mask'])].value
else:
mask = attrs['mask']
#support old style RGB volumes
if dnode.dtype == np.uint8 and dnode.shape[-1] in (3, 4):
alpha = None
if dnode.shape[-1] == 4:
alpha = dnode[..., 3]
if xfmname is None:
return VertexRGB(dnode[...,0], dnode[...,1], dnode[...,2], subject,
alpha=alpha, **kwargs)
return VolumeRGB(dnode[...,0], dnode[...,1], dnode[...,2], subject, xfmname,
alpha=alpha, mask=mask, **kwargs)
if xfmname is None:
return Vertex(dnode, subject, **kwargs)
return Volume(dnode, subject, xfmname, mask=mask, **kwargs)
def _from_hdf_view(h5, data, xfmname=None, vmin=None, vmax=None, subject=None, **kwargs):
if isinstance(data, str):
return _from_hdf_data(h5, data, xfmname=xfmname, vmin=vmin, vmax=vmax, subject=subject, **kwargs)
if len(data) == 2:
dim1 = _from_hdf_data(h5, data[0], xfmname=xfmname[0], subject=subject)
dim2 = _from_hdf_data(h5, data[1], xfmname=xfmname[1], subject=subject)
cls = Vertex2D if isinstance(dim1, Vertex) else Volume2D
return cls(dim1, dim2, vmin=vmin[0], vmin2=vmin[1],
vmax=vmax[0], vmax2=vmax[1], subject=subject, **kwargs)
elif len(data) == 4:
red, green, blue = [_from_hdf_data(h5, d, xfmname=xfmname, subject=subject) for d in data[:3]]
alpha = None
if data[3] is not None:
alpha = _from_hdf_data(h5, data[3], xfmname=xfmname, subject=subject)
cls = VertexRGB if isinstance(red, Vertex) else VolumeRGB
return cls(red, green, blue, alpha=alpha, subject=subject, **kwargs)
else:
raise ValueError("Invalid Dataview specification")
class Dataview(object):
def __init__(self, cmap=None, vmin=None, vmax=None, description="", state=None, **kwargs):
if self.__class__ == Dataview:
raise TypeError('Cannot directly instantiate Dataview objects')
self.cmap = cmap if cmap is not None else default_cmap
self.vmin = vmin
self.vmax = vmax
self.state = state
self.attrs = kwargs
if 'priority' not in self.attrs:
self.attrs['priority'] = 1
self.description = description
def copy(self, *args, **kwargs):
kwargs.update(self.attrs)
return self.__class__(*args,
cmap=self.cmap,
vmin=self.vmin,
vmax=self.vmax,
description=self.description,
state=self.state,
**kwargs)
@property
def priority(self):
return self.attrs['priority']
@priority.setter
def priority(self, value):
self.attrs['priority'] = value
def to_json(self, simple=False):
if simple:
return dict()
desc = self.description
if hasattr(desc, 'decode'):
desc = desc.decode()
sdict = dict(
state=self.state,
attrs=self.attrs.copy(),
desc=desc)
try:
sdict.update(dict(
cmap=[self.cmap],
vmin=[self.vmin if self.vmin is not None else np.percentile(np.nan_to_num(self.data), 1)],
vmax=[self.vmax if self.vmax is not None else np.percentile(np.nan_to_num(self.data), 99)]
))
except AttributeError:
pass
return sdict
@staticmethod
def from_hdf(node, subject=None):
data = json.loads(u(node[0]))
desc = node[1]
try:
cmap = json.loads(u(node[2]))
except:
cmap = u(node[2])
vmin = json.loads(u(node[3]))
vmax = json.loads(u(node[4]))
state = json.loads(u(node[5]))
attrs = json.loads(u(node[6]))
try:
xfmname = json.loads(u(node[7]))
except ValueError:
xfmname = None
if not isinstance(vmin, list):
vmin = [vmin]
if not isinstance(vmax, list):
vmax = [vmax]
if not isinstance(cmap, list):
cmap = [cmap]
if len(data) == 1:
xfm = None if xfmname is None else xfmname[0]
return _from_hdf_view(node.file, data[0], xfmname=xfm, cmap=cmap[0], description=desc,
vmin=vmin[0], vmax=vmax[0], state=state, subject=subject, **attrs)
else:
views = [_from_hdf_view(node.file, d, xfmname=x, subject=subject) for d, x in zip(data, xfmname)]
raise NotImplementedError
def _write_hdf(self, h5, name="data", data=None, xfmname=None):
views = h5.require_group("/views")
view = views.require_dataset(name, (8,), h5py.special_dtype(vlen=str))
view[0] = json.dumps(data)
view[1] = self.description
try:
view[2] = json.dumps([self.cmap])
view[3] = json.dumps([self.vmin])
view[4] = json.dumps([self.vmax])
except AttributeError:
#For VolumeRGB/Vertex, there is no cmap/vmin/vmax
view[2] = "null"
view[3:5] = "null"
view[5] = json.dumps(self.state)
view[6] = json.dumps(self.attrs)
view[7] = json.dumps(xfmname)
return view
def get_cmapdict(self):
"""Returns a dictionary with cmap information."""
from matplotlib import colors
from matplotlib import pyplot as plt
try:
# plt.get_cmap accepts:
# - matplotlib colormap names
# - pycortex colormap names previously registered in matplotlib
# - matplotlib.colors.Colormap instances
cmap = plt.get_cmap(self.cmap)
except ValueError:
# unknown colormap, test whether it's in pycortex colormaps
cmapdir = options.config.get('webgl', 'colormaps')
colormaps = glob.glob(os.path.join(cmapdir, "*.png"))
colormaps = dict(((os.path.split(c)[1][:-4], c) for c in colormaps))
if self.cmap not in colormaps:
raise ValueError('Unkown color map %s' % self.cmap)
I = plt.imread(colormaps[self.cmap])
name = self.cmap if isinstance(self.cmap, str) else self.cmap.name
cmap = colors.ListedColormap(np.squeeze(I), name=name)
# Register colormap to matplotlib to avoid loading it again
register_cmap(cmap)
return dict(cmap=cmap, vmin=self.vmin, vmax=self.vmax)
@property
def raw(self):
from matplotlib import cm, colors
cmap = self.get_cmapdict()['cmap']
# Normalize colors according to vmin, vmax
norm = colors.Normalize(self.vmin, self.vmax)
cmapper = cm.ScalarMappable(norm=norm, cmap=cmap)
color_data = cmapper.to_rgba(self.data.flatten()).reshape(self.data.shape+(4,))
# rollaxis puts the last color dimension first, to allow output of separate channels: r,g,b,a = dataset.raw
color_data = (np.clip(color_data, 0, 1) * 255).astype(np.uint8)
return np.rollaxis(color_data, -1)
class Multiview(Dataview):
def __init__(self, views, description=""):
for view in views:
if not isinstance(view, Dataview):
raise TypeError("Must be a View object!")
raise NotImplementedError
self.views = views
def uniques(self, collapse=False):
for view in self.views:
for sv in view.uniques(collapse=collapse):
yield sv
[docs]
class Volume(VolumeData, Dataview):
"""
Encapsulates a 3D volume or 4D volumetric movie. Includes information on how
the volume should be colormapped for display purposes.
Parameters
----------
data : ndarray
The data. Can be 3D with shape (z,y,x), 1D with shape (v,) for masked data,
4D with shape (t,z,y,x), or 2D with shape (t,v). For masked data, if the
size of the given array matches any of the existing masks in the database,
that mask will automatically be loaded. If it does not, an error will be
raised.
subject : str
Subject identifier. Must exist in the pycortex database.
xfmname : str
Transform name. Must exist in the pycortex database.
mask : ndarray, optional
Binary 3D array with shape (z,y,x) showing which voxels are selected.
If masked data is given, the mask will automatically be loaded if it
exists in the pycortex database.
cmap : str or matplotlib colormap, optional
Colormap (or colormap name) to use. If not given defaults to matplotlib
default colormap.
vmin : float, optional
Minimum value in colormap. If not given, defaults to the 1st percentile
of the data.
vmax : float, optional
Maximum value in colormap. If not given defaults to the 99th percentile
of the data.
description : str, optional
String describing this dataset. Displayed in webgl viewer.
**kwargs
All additional arguments in kwargs are passed to the VolumeData and Dataview
"""
[docs]
def __init__(self, data, subject, xfmname, mask=None,
cmap=None, vmin=None, vmax=None, description="", **kwargs):
super(Volume, self).__init__(data, subject, xfmname, mask=mask,
cmap=cmap, vmin=vmin, vmax=vmax,
description=description, **kwargs)
# set vmin and vmax
self.vmin = self.vmin if self.vmin is not None else \
np.percentile(np.nan_to_num(self.data), 1)
self.vmax = self.vmax if self.vmax is not None else \
np.percentile(np.nan_to_num(self.data), 99)
def _write_hdf(self, h5, name="data"):
datanode = VolumeData._write_hdf(self, h5)
viewnode = Dataview._write_hdf(self, h5, name=name,
data=[self.name],
xfmname=[self.xfmname])
return viewnode
@property
def raw(self):
r, g, b, a = super(Volume, self).raw
return VolumeRGB(r, g, b, self.subject, self.xfmname, a,
description=self.description, state=self.state,
**self.attrs)
[docs]
class Vertex(VertexData, Dataview):
"""
Encapsulates a 1D vertex map or 2D vertex movie. Includes information on how
the data should be colormapped for display purposes.
Parameters
----------
data : ndarray
The data. Can be 1D with shape (v,), or 2D with shape (t,v). Here, v can
be the number of vertices in both hemispheres, or the number of vertices
in either one of the hemispheres. In that case, the data for the other
hemisphere will be filled with zeros.
subject : str
Subject identifier. Must exist in the pycortex database.
cmap : str or matplotlib colormap, optional
Colormap (or colormap name) to use. If not given defaults to matplotlib
default colormap.
vmin : float, optional
Minimum value in colormap. If not given, defaults to the 1st percentile
of the data.
vmax : float, optional
Maximum value in colormap. If not given defaults to the 99th percentile
of the data.
description : str, optional
String describing this dataset. Displayed in webgl viewer.
**kwargs
All additional arguments in kwargs are passed to the VolumeData and Dataview
"""
[docs]
def __init__(self, data, subject, cmap=None, vmin=None, vmax=None, description="", **kwargs):
super(Vertex, self).__init__(data, subject, cmap=cmap, vmin=vmin, vmax=vmax,
description=description, **kwargs)
# set vmin and vmax
self.vmin = self.vmin if self.vmin is not None else \
np.percentile(np.nan_to_num(self.data), 1)
self.vmax = self.vmax if self.vmax is not None else \
np.percentile(np.nan_to_num(self.data), 99)
def _write_hdf(self, h5, name="data"):
datanode = VertexData._write_hdf(self, h5)
viewnode = Dataview._write_hdf(self, h5, name=name, data=[self.name])
return viewnode
@property
def raw(self):
r, g, b, a = super(Vertex, self).raw
return VertexRGB(r, g, b, self.subject, a,
description=self.description, state=self.state,
**self.attrs)
[docs]
def map(self, target_subj, surface_type='fiducial',
hemi='both', fs_subj=None, **kwargs):
"""Map this data from this surface to another surface
Calls `cortex.freesurfer.vertex_to_vertex()` with this
vertex object as the first argument.
NOTE: Requires either previous computation of mapping matrices
(with `cortex.db.get_mri_surf2surf_matrix`) or active
freesurfer environment.
Parameters
----------
target_subj : str
freesurfer subject to which to map
Other Parameters
----------------
kwargs map to `cortex.freesurfer.vertex_to_vertex()`
"""
# Input check
if hemi not in ['lh', 'rh', 'both']:
raise ValueError("`hemi` kwarg must be 'lh', 'rh', or 'both'")
# lazy load
from ..database import db
mats = db.get_mri_surf2surf_matrix(self.subject, surface_type,
hemi='both', target_subj=target_subj, fs_subj=fs_subj,
**kwargs)
new_data = [mats[0].dot(self.left), mats[1].dot(self.right)]
if hemi == 'both':
new_data = np.hstack(new_data)
elif hemi == 'lh':
new_data = np.hstack([new_data[0], np.nan * np.zeros(new_data[1].shape)])
elif hemi == 'rh':
new_data = np.hstack([np.nan * np.zeros(new_data[0].shape), new_data[1]])
vx = Vertex(new_data, target_subj, vmin=self.vmin, vmax=self.vmax, cmap=self.cmap)
return vx
def u(s, encoding='utf8'):
try:
return s.decode(encoding)
except AttributeError:
return s
from .viewRGB import Colors, VertexRGB, VolumeRGB
from .view2D import Vertex2D, Volume2D