import os
import json
from typing import Optional, Union
import warnings
import numpy as np
import numpy.typing as npt
from .. import options
from .views import Dataview, Volume, Vertex, VolumeRGB, VertexRGB
from .braindata import BrainData, VolumeData, VertexData
default_cmap2D = options.config.get("basic", "default_cmap2D")
class Dataview2D(Dataview):
"""Abstract base class for 2-dimensional data views.
"""
dim1: Dataview
dim2: Dataview
def __init__(self, description: str="", cmap: Optional[str]=None,
vmin: Optional[float]=None, vmax: Optional[float]=None,
vmin2: Optional[float]=None, vmax2: Optional[float]=None, state=None, **kwargs):
self.cmap = cmap or default_cmap2D
self.vmin = vmin
self.vmax = vmax
self.vmin2 = vmin if vmin2 is None else vmin2
self.vmax2 = vmax if vmax2 is None else vmax2
self.state = state
self.attrs = kwargs
if 'priority' not in self.attrs:
self.attrs['priority'] = 1
self.description = description
def uniques(self, collapse=False):
yield self.dim1
yield self.dim2
def _write_hdf(self, h5, name="data"):
self._cls._write_hdf(self.dim1, h5)
self._cls._write_hdf(self.dim2, h5)
viewnode = Dataview._write_hdf(self, h5, name=name)
viewnode[0] = json.dumps([[self.dim1.name, self.dim2.name]])
viewnode[3] = json.dumps([[self.vmin, self.vmin2]])
viewnode[4] = json.dumps([[self.vmax, self.vmax2]])
return viewnode
def to_json(self, simple=False):
sdict = dict(data=[[self.dim1.name, self.dim2.name]],
state=self.state,
attrs=self.attrs,
desc=self.description,
cmap=[self.cmap] )
d1js = self.dim1.to_json()
d2js = self.dim2.to_json()
sdict.update(dict(
vmin = [[self.vmin or d1js['vmin'][0], self.vmin2 or d2js['vmin'][0]]],
vmax = [[self.vmax or d1js['vmax'][0], self.vmax2 or d2js['vmax'][0]]],
))
if "xfm" in d1js:
sdict['xfm'] = [[d1js['xfm'][0], d2js['xfm'][0]]]
return sdict
def _to_raw(self, data1, data2):
from matplotlib import pyplot as plt
from matplotlib.colors import Normalize
cmapdir = options.config.get("webgl", "colormaps")
cmap = plt.imread(os.path.join(cmapdir, "%s.png"%self.cmap))
_warn_non_perceptually_uniform_colormap(self.cmap)
norm1 = Normalize(self.vmin, self.vmax)
norm2 = Normalize(self.vmin2, self.vmax2)
d1 = np.clip(norm1(data1), 0, 1)
d2 = np.clip(1 - norm2(data2), 0, 1)
dim1 = np.round(d1 * (cmap.shape[1]-1))
# Nans in data seemed to cause weird interaction with conversion to uint32
dim1 = np.nan_to_num(dim1).astype(np.uint32)
dim2 = np.round(d2 * (cmap.shape[0]-1))
dim2 = np.nan_to_num(dim2).astype(np.uint32)
colored = cmap[dim2.ravel(), dim1.ravel()]
# map r, g, b, a values between 0 and 255 to avoid problems with
# VolumeRGB when plotting flatmaps with quickflat
colored = (colored * 255).astype(np.uint8)
r, g, b, a = colored.T
r.shape = dim1.shape
g.shape = dim1.shape
b.shape = dim1.shape
a.shape = dim1.shape
# Preserve nan values as alpha = 0
aidx = np.logical_or(np.isnan(data1), np.isnan(data2))
a[aidx] = 0
# Code from main, to handle alpha input, prob better here but not tested.
# # Possibly move this above setting nans to alpha = 0;
# # Possibly multiply specified alpha by alpha in colormap??
# if 'alpha' in self.attrs:
# # Over-write alpha from colormap / nans with alpha arg if provided.
# # Question: Might it be important tokeep alpha as an attr?
# a = self.attrs.pop('alpha')
return r, g, b, a
@property
def subject(self):
return self.dim1.subject
[docs]
class Volume2D(Dataview2D):
"""
Contains two 3D volumes for simultaneous visualization. Includes information
on how the volumes should be jointly colormapped.
Parameters
----------
dim1 : ndarray or Volume
The first volume. Can be a 1D or 3D array (see Volume for details), or
a Volume.
dim2 : ndarray or Volume
The second volume. Can be a 1D or 3D array (see Volume for details), or
a Volume.
subject : str, optional
Subject identifier. Must exist in the pycortex database. If not given,
dim1 must be a Volume from which the subject can be extracted.
xfmname : str, optional
Transform name. Must exist in the pycortex database. If not given,
dim1 must be a Volume from which the subject can be extracted.
description : str, optional
String describing this dataset. Displayed in webgl viewer.
cmap : str, optional
Colormap (or colormap name) to use. If not given defaults to the
`default_cmap2d` in your pycortex options.cfg file.
vmin : float, optional
Minimum value in colormap for dim1. If not given defaults to TODO:WHAT
vmax : float, optional
Maximum value in colormap for dim1. If not given defaults to TODO:WHAT
vmin2 : float, optional
Minimum value in colormap for dim2. If not given defaults to TODO:WHAT
vmax2 : float, optional
Maximum value in colormap for dim2. If not given defaults to TODO:WHAT
**kwargs
All additional arguments in kwargs are passed to the VolumeData and Dataview
"""
_cls = VolumeData
dim1: Volume
dim2: Volume
[docs]
def __init__(self, dim1: Union[npt.NDArray, Volume], dim2: Union[npt.NDArray, Volume], subject: Optional[str]=None, xfmname: Optional[str]=None, description: str="", cmap: Optional[str]=None,
vmin: Optional[float]=None, vmax: Optional[float]=None, vmin2: Optional[float]=None, vmax2: Optional[float]=None, **kwargs):
if isinstance(dim1, self._cls):
if subject is not None or xfmname is not None:
raise TypeError("Subject and xfmname cannot be specified with Volumes")
if not isinstance(dim2, self._cls) or dim2.subject != dim1.subject:
raise TypeError("Invalid data for second dimension")
self.dim1 = dim1
self.dim2 = dim2
else:
if isinstance(dim2, self._cls):
raise TypeError("If dim2 is a Volume, dim1 must be a Volume as well")
if subject is None or xfmname is None:
raise TypeError("Subject and xfmname must be specified with raw data")
self.dim1 = Volume(dim1, subject, xfmname, vmin=vmin, vmax=vmax)
self.dim2 = Volume(dim2, subject, xfmname, vmin=vmin2, vmax=vmax2)
vmin = self.dim1.vmin if vmin is None else vmin
vmin2 = self.dim2.vmin if vmin2 is None else vmin2
vmax = self.dim1.vmax if vmax is None else vmax
vmax2 = self.dim2.vmax if vmax2 is None else vmax2
super(Volume2D, self).__init__(description=description, cmap=cmap, vmin=vmin,
vmax=vmax, vmin2=vmin2, vmax2=vmax2, **kwargs)
def __repr__(self):
return "<2D volumetric data for (%s, %s)>"%(self.dim1.subject, self.dim1.xfmname)
def _write_hdf(self, h5, name="data"):
viewnode = super(Volume2D, self)._write_hdf(h5, name)
viewnode[7] = json.dumps([[self.dim1.xfmname, self.dim2.xfmname]])
return viewnode
@property
def raw(self):
"""VolumeRGB object containing the colormapped data from this object.
"""
if self.dim1.xfmname != self.dim2.xfmname:
raise ValueError("Both Volumes must have same xfmname to generate single raw volume")
if ((self.dim1.linear and self.dim2.linear) and
(self.dim1.mask.shape == self.dim2.mask.shape) and
np.all(self.dim1.mask == self.dim2.mask)):
r, g, b, a = self._to_raw(self.dim1.data, self.dim2.data)
else:
r, g, b, a = self._to_raw(self.dim1.volume, self.dim2.volume)
# Allow manual override of alpha channel
kws = dict(subject=self.dim1.subject, xfmname=self.dim1.xfmname,
state=self.state, description=self.description, **self.attrs)
if not 'alpha' in self.attrs:
kws['alpha'] = a
return VolumeRGB(r, g, b, **kws)
@property
def xfmname(self):
return self.dim1.xfmname
[docs]
class Vertex2D(Dataview2D):
"""
Contains two vertex maps for simultaneous visualization. Includes information
on how the maps should be jointly colormapped.
Parameters
----------
dim1 : ndarray or Vertex
The first vertex map. Can be a 1D array (see Vertex for details), or
a Vertex.
dim2 : ndarray or Vertex
The second vertex map. Can be a 1D array (see Vertex for details), or
a Vertex.
subject : str, optional
Subject identifier. Must exist in the pycortex database. If not given,
dim1 must be a Vertex from which the subject can be extracted.
description : str, optional
String describing this dataset. Displayed in webgl viewer.
cmap : str, optional
Colormap (or colormap name) to use. If not given defaults to the
`default_cmap2d` in your pycortex options.cfg file.
vmin : float, optional
Minimum value in colormap for dim1. If not given defaults to TODO:WHAT
vmax : float, optional
Maximum value in colormap for dim1. If not given defaults to TODO:WHAT
vmin2 : float, optional
Minimum value in colormap for dim2. If not given defaults to TODO:WHAT
vmax2 : float, optional
Maximum value in colormap for dim2. If not given defaults to TODO:WHAT
**kwargs
All additional arguments in kwargs are passed to the VolumeData and Dataview
"""
_cls = VertexData
blend_curvature = _cls.blend_curvature # hacky inheritance
dim1: Vertex
dim2: Vertex
[docs]
def __init__(self, dim1: Union[npt.NDArray, Vertex], dim2: Union[npt.NDArray, Vertex], subject: Optional[str]=None, description: str="", cmap: Optional[str]=None,
vmin: Optional[float]=None, vmax: Optional[float]=None, vmin2: Optional[float]=None, vmax2: Optional[float]=None, **kwargs):
if isinstance(dim1, VertexData):
if subject is not None:
raise TypeError("Subject cannot be specified with Vertex")
if not isinstance(dim2, VertexData) or dim2.subject != dim1.subject:
raise TypeError("Invalid data for second dimension")
self.dim1 = dim1
self.dim2 = dim2
else:
if isinstance(dim2, self._cls):
raise TypeError("If dim2 is a Vertex, dim1 must be a Vertex as well")
if subject is None:
raise TypeError("Subject must be specified with raw data")
self.dim1 = Vertex(dim1, subject, vmin=vmin, vmax=vmax)
self.dim2 = Vertex(dim2, subject, vmin=vmin2, vmax=vmax2)
vmin = self.dim1.vmin if vmin is None else vmin
vmin2 = self.dim2.vmin if vmin2 is None else vmin2
vmax = self.dim1.vmax if vmax is None else vmax
vmax2 = self.dim2.vmax if vmax2 is None else vmax2
super(Vertex2D, self).__init__(description=description, cmap=cmap,
vmin=vmin, vmax=vmax, vmin2=vmin2,
vmax2=vmax2, **kwargs)
def __repr__(self):
return "<2D vertex data for (%s)>"%self.dim1.subject
@property
def raw(self):
"""VertexRGB object containing the colormapped data from this object.
"""
r, g, b, a = self._to_raw(self.dim1.data, self.dim2.data)
# Allow manual override of alpha channel
kws = dict(subject=self.dim1.subject)
if not 'alpha' in self.attrs:
kws['alpha'] = a
return VertexRGB(r, g, b, **kws)
@property
def vertices(self):
return self.raw.vertices
def _warn_non_perceptually_uniform_colormap(cmap):
mapping = {
"BuOr_2D": "PU_BuOr_covar",
"RdBu_covar": "PU_RdBu_covar",
"RdBu_covar2": "PU_BuOr_covar",
"RdBu_covar_alpha": "PU_RdBu_covar_alpha",
"RdGn_covar": "PU_RdGn_covar",
"hot_alpha": "fire_alpha",
}
if cmap in mapping:
warnings.warn("Colormap %r is not perceptually uniform. Consider using"
" %r instead." % (cmap, mapping[cmap]), UserWarning)