Source code for cortex.dataset.viewRGB

from __future__ import annotations

import colorsys
from typing import Optional, TypeVar, Union
import warnings

import numpy as np
import numpy.typing as npt

from .views import Dataview, Volume, Vertex
from .braindata import BrainData, VolumeData, VertexData, _hash
from ..database import db

from .. import options

default_cmap = options.config.get("basic", "default_cmap")


ColorDtype = TypeVar("ColorDtype", int, float)
Color = tuple[ColorDtype, ColorDtype, ColorDtype]  # RGB color


class Colors:
    """
    Set of known colors
    """

    RoseRed: Color[int] = (237, 35, 96)
    LimeGreen: Color[int] = (141, 198, 63)
    SkyBlue: Color[int] = (0, 176, 218)
    DodgerBlue: Color[int] = (30, 144, 255)
    Red: Color[int] = (255, 000, 000)
    Green: Color[int] = (000, 255, 000)
    Blue: Color[int] = (000, 000, 255)


def RGB2HSV(color: Color | npt.NDArray) -> Color[float]:
    """
    Converts RGB to HS
    Parameters
    ----------
    color : tuple<uint8, uint8, uint8>
        RGB color value

    Returns
    -------
    tuple<int, float, float>
        HSV values. Hue in degrees, saturation and value on [0, 1]

    """
    hue, saturation, value = colorsys.rgb_to_hsv(
        color[0] / 255.0, color[1] / 255.0, color[2] / 255.0
    )
    hue *= 360
    return (int(hue), saturation, value)


def HSV2RGB(color: Color[float] | npt.NDArray) -> Color[int]:
    """
    Converts HSV to RGB

    Parameters
    ----------
    color : tuple<int, float, float>
        HSV values. Hue in degrees, saturation and value on [0, 1]

    Returns
    -------
    tuple<uint8, uint8, uint8>
        RGB color value
    """
    r, g, b = colorsys.hsv_to_rgb(color[0] / 360.0, color[1], color[2])
    return (int(r * 255), int(g * 255), int(b * 255))


class DataviewRGB(Dataview):
    """Abstract base class for RGB data views."""

    _cls = BrainData
    red: Dataview
    green: Dataview
    blue: Dataview

    def __init__(
        self, subject=None, alpha=None, description="", state=None, priority=1
    ):
        self.alpha = alpha
        self.subject = self.red.subject
        self.movie = self.red.movie
        self.description = description
        self.state = state
        self.attrs = dict(priority=priority)

        # If movie, make sure each channel has the same number of time points
        if self.red.movie:
            if (
                not self.red.data.shape[0]
                == self.green.data.shape[0]
                == self.blue.data.shape[0]
            ):
                raise ValueError(
                    "For movie data, all three channels have to be the same length"
                )

    def uniques(self, collapse=False):
        if collapse:
            yield self
        else:
            yield self.red
            yield self.green
            yield self.blue
            if self.alpha is not None:
                yield self.alpha

    def _write_hdf(self, h5, name="data", xfmname=None):
        self._cls._write_hdf(self.red, h5)
        self._cls._write_hdf(self.green, h5)
        self._cls._write_hdf(self.blue, h5)

        alpha = None
        if self.alpha is not None:
            self._cls._write_hdf(self.alpha, h5)
            alpha = self.alpha.name

        data = [self.red.name, self.green.name, self.blue.name, alpha]
        viewnode = Dataview._write_hdf(
            self, h5, name=name, data=[data], xfmname=xfmname
        )

        return viewnode

    def to_json(self, simple=False):
        sdict = super(DataviewRGB, self).to_json(simple=simple)

        if simple:
            sdict["name"] = self.name
            sdict["subject"] = self.subject
            sdict["min"] = 0
            sdict["max"] = 255
        else:
            sdict["data"] = [self.name]
            sdict["cmap"] = [default_cmap]
            sdict["vmin"] = [0]
            sdict["vmax"] = [255]
        return sdict

    def get_cmapdict(self):
        return dict()

    @staticmethod
    def color_voxels(
        channel1,
        channel2,
        channel3,
        channel1color,
        channel2color,
        channel3Color,
        value_max,
        saturation_max,
        vmin,
        vmax,
        autorange,
        alpha=None,
    ):
        """
        Colors voxels in 3 color dimensions but not necessarily canonical red, green, and blue
        Parameters
        ----------
        channel1 : ndarray or Volume or Vertex
            voxel values for first channel
        channel2 : ndarray or Volume or Vertex
            voxel values for second channel
        channel3 : ndarray or Volume or Vertex
            voxel values for third channel
        channel1color : tuple<uint8, uint8, uint8>
            color in RGB for first channel
        channel2color : tuple<uint8, uint8, uint8>
            color in RGB for second channel
        channel3Color : tuple<uint8, uint8, uint8>
            color in RGB for third channel
        value_max : float, optional
            Maximum HSV value for voxel colors. If not given, will be the value of
            the average of the three channel colors.
        saturation_max : float [0, 1]
            Maximum HSV saturation for voxel colors.
        vmin : float or tuple of float, optional
            Lower bound(s) that map to 0 in each color channel. If a single float, the same lower bound
            is used for all three channels. If a tuple of three floats, each channel
            uses its respective value. If None, the lower bound is auto-determined
            based on ``autorange``.
        vmax : float or tuple of float, optional
            Upper bound(s) that map to 255 in each color channel. If a single float, the same upper bound
            is used for all three channels. If a tuple of three floats, each channel
            uses its respective value. If None, the upper bound is auto-determined
            based on ``autorange``.
        autorange : 'shared' or 'individual'
            How to auto-determine bounds when vmin or vmax is None. 'shared' computes
            the 1st and 99th percentile across all three channels combined. 'individual'
            computes per-channel 1st and 99th percentiles. Overridden when vmin and
            vmax are both provided.
        alpha : ndarray or Volume or Vertex, optional
            Alpha values for each voxel. If None, alpha is set to 1 for all voxels.

        Returns
        -------
        red : ndarray of channel1.shape
            uint8 array of red values
        green : ndarray of channel1.shape
            uint8 array of green values
        blue : ndarray of channel1.shape
            uint8 array of blue values
        alpha : ndarray
            If alpha=None, uint8 array of alpha values with alpha=1 for every voxel.
            Otherwise, the same alpha values that were passed in. Additionally,
            voxels with NaNs will have an alpha value of 0.
        """
        # normalize each channel to [0, 1]
        data1 = (
            channel1.data
            if isinstance(channel1, (VolumeData, VertexData))
            else channel1
        )
        data1 = data1.astype(float)
        data2 = (
            channel2.data
            if isinstance(channel2, (VolumeData, VertexData))
            else channel2
        )
        data2 = data2.astype(float)
        data3 = (
            channel3.data
            if isinstance(channel3, (VolumeData, VertexData))
            else channel3
        )
        data3 = data3.astype(float)

        if (data1.shape != data2.shape) or (data2.shape != data3.shape):
            raise ValueError("Volumes are of different shapes")

        # Create an alpha mask now, before casting nans to 0
        # Voxels with at least one channel equal to NaN will be masked out.
        mask = np.isnan(np.array([data1, data2, data3])).any(axis=0)
        # Now convert to NaNs to num for all channels
        data1 = np.nan_to_num(data1)
        data2 = np.nan_to_num(data2)
        data3 = np.nan_to_num(data3)

        # Expand vmin/vmax to per-channel lists
        if isinstance(vmin, (int, float)):
            channel_vmins = [float(vmin), float(vmin), float(vmin)]
        elif vmin is not None:
            channel_vmins = [float(v) for v in vmin]
        else:
            channel_vmins = [None, None, None]

        if isinstance(vmax, (int, float)):
            channel_vmaxs = [float(vmax), float(vmax), float(vmax)]
        elif vmax is not None:
            channel_vmaxs = [float(v) for v in vmax]
        else:
            channel_vmaxs = [None, None, None]

        # Auto-determine any None bounds
        needs_auto_min = any(v is None for v in channel_vmins)
        needs_auto_max = any(v is None for v in channel_vmaxs)

        if (needs_auto_min or needs_auto_max):
            if autorange == 'shared':
                all_data = np.concatenate([data1.ravel(), data2.ravel(), data3.ravel()])
                shared_min = np.percentile(all_data, 1)
                shared_max = np.percentile(all_data, 99)
                channel_vmins = [shared_min if v is None else v for v in channel_vmins]
                channel_vmaxs = [shared_max if v is None else v for v in channel_vmaxs]
            elif autorange == 'individual':
                for i, data in enumerate([data1, data2, data3]):
                    if channel_vmins[i] is None:
                        channel_vmins[i] = np.percentile(data.ravel(), 1)
                    if channel_vmaxs[i] is None:
                        channel_vmaxs[i] = np.percentile(data.ravel(), 99)
            else:
                raise ValueError('autorange must be \'shared\' or \'individual\'')

        normalized = []
        for channel, (data, channel_min, channel_max) in enumerate(
            zip([data1, data2, data3], channel_vmins, channel_vmaxs), start=1
        ):
            channel_range = channel_max - channel_min
            if channel_range == 0:
                warnings.warn(
                    "Channel {} has no dynamic range (vmin == vmax) and will be zeroed out".format(channel)
                )
                normalized.append(np.zeros_like(data))
            else:
                normalized.append((data - channel_min) / channel_range)
        data1, data2, data3 = normalized
        data1 = np.clip(data1, 0, 1)
        data2 = np.clip(data2, 0, 1)
        data3 = np.clip(data3, 0, 1)

        channel1color = np.array(channel1color)
        channel2color = np.array(channel2color)
        channel3Color = np.array(channel3Color)

        averageColor = (channel1color + channel2color + channel3Color) / 3

        if value_max is None:
            _, _, value = RGB2HSV(averageColor)
            value_max = value

        red = np.zeros_like(data1, np.uint8)
        green = np.zeros_like(data1, np.uint8)
        blue = np.zeros_like(data1, np.uint8)
        for i in range(data1.size):
            this_color = (
                data1.flat[i] * channel1color
                + data2.flat[i] * channel2color
                + data3.flat[i] * channel3Color
            )
            this_color /= 3.0
            if (value_max != 1.0) or (saturation_max != 1.0):
                hue, saturation, value = RGB2HSV(this_color)
                saturation /= saturation_max
                value /= value_max
                if saturation > 1:
                    saturation = 1.0
                if value > 1:
                    value = 1.0
                this_color = HSV2RGB([hue, saturation, value])
            red.flat[i] = this_color[0]
            green.flat[i] = this_color[1]
            blue.flat[i] = this_color[2]

        # Now make an alpha volume
        if alpha is None:
            alpha = np.ones_like(red, np.uint8) * 255
        alpha[mask] = 0

        return red, green, blue, alpha


[docs] class VolumeRGB(DataviewRGB): """ Contains RGB (or RGBA) colors for each voxel in a volumetric dataset. Includes information about the subject and transform for the data. Three data channels are mapped into a 3D color set. By default the data channels are mapped on to red, green, and blue. They can also be mapped to be different colors as specified, and then linearly combined. Each data channel is represented as a separate Volume object (these can either be supplied explicitly as Volume objects or implicitly as numpy arrays). By default, each channel's range is determined independently from the data. Use ``vmin``/``vmax`` to specify explicit bounds, or ``autorange`` to control how bounds are auto-determined. Parameters ---------- channel1 : ndarray or Volume Array or Volume for the first data channel for each voxel. Can be a 1D or 3D array (see Volume for details), or a Volume. channel2 : ndarray or Volume Array or Volume for the second data channel for each voxel. Can be a 1D or 3D array (see Volume for details), or a Volume. channel3 : ndarray or Volume Array or Volume for the third data channel for or each voxel. 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, red 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, red must be a Volume from which the subject can be extracted. alpha : ndarray or Volume, optional Array or Volume that represents the alpha component of the color for each voxel. Can be a 1D or 3D array (see Volume for details), or a Volume. If None, all voxels will be assumed to have alpha=1.0. description : str, optional String describing this dataset. Displayed in webgl viewer. state : optional TODO: describe what this is channel1color : tuple<uint8, uint8, uint8> RGB color to use for the first data channel channel2color : tuple<uint8, uint8, uint8> RGB color to use for the second data channel channel3color : tuple<uint8, uint8, uint8> RGB color to use for the third data channel max_color_value : float [0, 1], optional Maximum HSV value for voxel colors. If not given, will be the value of the average of the three channel colors. max_color_saturation: float [0, 1] Maximum HSV saturation for voxel colors. vmin : float or tuple of float, optional Lower bound(s) that map to 0 in each color channel. If a single float, the same lower bound is used for all three channels. If a tuple of three floats, each channel uses its respective value. If None, the lower bound is auto-determined based on ``autorange``. vmax : float or tuple of float, optional Upper bound(s) that map to 255 in each color channel. If a single float, the same upper bound is used for all three channels. If a tuple of three floats, each channel uses its respective value. If None, the upper bound is auto-determined based on ``autorange``. autorange : 'shared' or 'individual' How to auto-determine bounds when vmin or vmax is None. 'shared' computes the 1st and 99th percentile across all three channels combined. 'individual' computes per-channel 1st and 99th percentiles. Overridden when vmin and vmax are both provided. Default is 'individual'. priority : int, optional Priority for display ordering. Default is 1. """ _cls = VolumeData red: Volume green: Volume blue: Volume _alpha: Optional[Union[npt.NDArray, Volume]]
[docs] def __init__( self, channel1: Union[npt.NDArray, Volume], channel2: Union[npt.NDArray, Volume], channel3: Union[npt.NDArray, Volume], subject: Optional[str] = None, xfmname: Optional[str] = None, alpha: Optional[Union[npt.NDArray, Volume]] = None, description: str = "", state=None, channel1color: Color = Colors.Red, channel2color: Color = Colors.Green, channel3color: Color = Colors.Blue, max_color_value: Optional[float] = None, max_color_saturation: float = 1.0, vmin: Optional[Union[float, tuple]] = None, vmax: Optional[Union[float, tuple]] = None, autorange: str = 'individual', priority: int = 1, ): channel1color = tuple(channel1color) channel2color = tuple(channel2color) channel3color = tuple(channel3color) if isinstance(channel1, VolumeData): if ( not isinstance(channel2, VolumeData) or channel1.subject != channel2.subject ): raise TypeError( "Data channel 2 is not a VolumeData object or is from a different subject" ) if ( not isinstance(channel3, VolumeData) or channel1.subject != channel3.subject ): raise TypeError( "Data channel 3 is not a VolumeData object or is from a different subject" ) if (subject is not None) and (channel1.subject != subject): raise ValueError( "Subject in VolumeData objects is different than specified subject" ) if ( (channel1color == Colors.Red) and (channel2color == Colors.Green) and (channel3color == Colors.Blue) and vmin is None and vmax is None and autorange == 'individual' ): # R/G/B basis can be directly passed through self.red = channel1 self.green = channel2 self.blue = channel3 self.alpha = alpha else: # need to remap colors red, green, blue, alpha = DataviewRGB.color_voxels( channel1, channel2, channel3, channel1color, channel2color, channel3color, max_color_value, max_color_saturation, vmin, vmax, autorange, alpha=alpha, ) self.red = Volume(red, channel1.subject, channel1.xfmname) self.green = Volume(green, channel1.subject, channel1.xfmname) self.blue = Volume(blue, channel1.subject, channel1.xfmname) self.alpha = alpha else: if subject is None or xfmname is None: raise TypeError("Subject and xfmname are required") if not isinstance(channel2, np.ndarray) or not isinstance( channel3, np.ndarray ): raise TypeError( "Data channels must be numpy arrays if channel1 is a numpy array" ) if ( (channel1color == Colors.Red) and (channel2color == Colors.Green) and (channel3color == Colors.Blue) and vmin is None and vmax is None and autorange == 'individual' ): # R/G/B basis can be directly passed through self.red = Volume(channel1, subject, xfmname) self.green = Volume(channel2, subject, xfmname) self.blue = Volume(channel3, subject, xfmname) self.alpha = alpha else: # need to remap colors red, green, blue, alpha = DataviewRGB.color_voxels( channel1, channel2, channel3, channel1color, channel2color, channel3color, max_color_value, max_color_saturation, vmin, vmax, autorange, alpha=alpha, ) self.red = Volume(red, subject, xfmname) self.green = Volume(green, subject, xfmname) self.blue = Volume(blue, subject, xfmname) self.alpha = alpha if ( self.red.xfmname == self.green.xfmname == self.blue.xfmname == self.alpha.xfmname ): self.xfmname = self.red.xfmname else: raise ValueError("Cannot handle different transforms per volume") super(VolumeRGB, self).__init__( subject, alpha, description=description, state=state, priority=priority )
@property def alpha(self): """Compute alpha transparency""" alpha = self._alpha if alpha is None: alpha = np.ones(self.red.volume.shape) alpha = Volume(alpha, self.red.subject, self.red.xfmname, vmin=0, vmax=1) if not isinstance(alpha, Volume): if alpha.dtype != np.uint8 and (alpha.min() < 0 or alpha.max() > 1): warnings.warn( "Some alpha values are outside the range of [0, 1]. " "Consider passing a Volume object as alpha with explicit vmin, vmax " "keyword arguments.", Warning, ) alpha = Volume(alpha, self.red.subject, self.red.xfmname, vmin=0, vmax=1) rgb = np.array([self.red.volume, self.green.volume, self.blue.volume]) mask = np.isnan(rgb).any(axis=0) alpha.volume[mask] = alpha.vmin return alpha @alpha.setter def alpha(self, alpha: Optional[Union[npt.NDArray, Volume]]): self._alpha = alpha
[docs] def to_json(self, simple=False): sdict = super(VolumeRGB, self).to_json(simple=simple) if simple: sdict["shape"] = self.red.shape else: sdict["xfm"] = [ list( np.array( db.get_xfm(self.subject, self.xfmname, "coord").xfm ).ravel() ) ] return sdict
@property def volume(self): """5-dimensional volume (t, z, y, x, rgba) with data that has been mapped into 8-bit unsigned integers that correspond to colors. """ volume = [] for dv in (self.red, self.green, self.blue, self.alpha): if dv.volume.dtype != np.uint8: vol = dv.volume.astype("float32", copy=True) if dv.vmin is None: if vol.min() < 0: vol -= vol.min() else: vol -= dv.vmin if dv.vmax is None: if vol.max() > 1: vol /= vol.max() else: vol /= dv.vmax - dv.vmin vol = (np.clip(vol, 0, 1) * 255).astype(np.uint8) else: vol = dv.volume.copy() volume.append(vol) return np.array(volume).transpose([1, 2, 3, 4, 0]) def __repr__(self): return "<RGB volumetric data for (%s, %s)>" % ( self.red.subject, self.red.xfmname, ) def __hash__(self): return hash(_hash(self.volume)) @property def name(self): return "__%s" % _hash(self.volume)[:16] def _write_hdf(self, h5, name="data"): return super(VolumeRGB, self)._write_hdf(h5, name=name, xfmname=[self.xfmname]) @property def raw(self): return self
[docs] class VertexRGB(DataviewRGB): """ Contains RGB (or RGBA) colors for each vertex in a surface dataset. Includes information about the subject. Three data channels are mapped into a 3D color set. By default the data channels are mapped on to red, green, and blue. They can also be mapped to be different colors as specified, and then linearly combined. Each color channel is represented as a separate Vertex object (these can either be supplied explicitly as Vertex objects or implicitly as np arrays). By default, each channel's range is determined independently from the data. Use ``vmin``/``vmax`` to specify explicit bounds, or ``autorange`` to control how bounds are auto-determined. Parameters ---------- red : ndarray or Vertex Array or Vertex that represents the first data channel for each vertex. Can be a 1D array (see Vertex for details), or a Vertex. green : ndarray or Vertex Array or Vertex that represents the second data channel for each vertex. Can be a 1D array (see Vertex for details), or a Vertex. blue : ndarray or Vertex Array or Vertex that represents the third data channel for each vertex. 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, red must be a Vertex from which the subject can be extracted. alpha : ndarray or Vertex, optional Array or Vertex that represents the alpha component of the color for each vertex. Can be a 1D array (see Vertex for details), or a Vertex. If None, all vertices will be assumed to have alpha=1.0. description : str, optional String describing this dataset. Displayed in webgl viewer. state : optional TODO: describe what this is channel1color : tuple<uint8, uint8, uint8> RGB color to use for the first data channel channel2color : tuple<uint8, uint8, uint8> RGB color to use for the second data channel channel3color : tuple<uint8, uint8, uint8> RGB color to use for the third data channel max_color_value : float [0, 1], optional Maximum HSV value for voxel colors. If not given, will be the value of the average of the three channel colors. max_color_saturation: float [0, 1] Maximum HSV saturation for voxel colors. vmin : float or tuple of float, optional Lower bound(s) that map to 0 in each color channel. If a single float, the same lower bound is used for all three channels. If a tuple of three floats, each channel uses its respective value. If None, the lower bound is auto-determined based on ``autorange``. vmax : float or tuple of float, optional Upper bound(s) that map to 255 in each color channel. If a single float, the same upper bound is used for all three channels. If a tuple of three floats, each channel uses its respective value. If None, the upper bound is auto-determined based on ``autorange``. autorange : 'shared' or 'individual' How to auto-determine bounds when vmin or vmax is None. 'shared' computes the 1st and 99th percentile across all three channels combined. 'individual' computes per-channel 1st and 99th percentiles. Overridden when vmin and vmax are both provided. Default is 'individual'. priority : int, optional Priority for display ordering. Default is 1. """ _cls = VertexData blend_curvature = _cls.blend_curvature # hacky inheritance red: Vertex green: Vertex blue: Vertex _alpha: Optional[Union[npt.NDArray, Vertex]]
[docs] def __init__( self, red: Union[npt.NDArray, Vertex], green: Union[npt.NDArray, Vertex], blue: Union[npt.NDArray, Vertex], subject: Optional[str] = None, alpha: Optional[Union[npt.NDArray, Vertex]] = None, description: str = "", state=None, channel1color=Colors.Red, channel2color=Colors.Green, channel3color=Colors.Blue, max_color_value=None, max_color_saturation=1.0, vmin=None, vmax=None, autorange='individual', priority=1, ): channel1color = tuple(channel1color) channel2color = tuple(channel2color) channel3color = tuple(channel3color) if isinstance(red, VertexData): if not isinstance(green, VertexData) or red.subject != green.subject: raise TypeError("Invalid data for green channel") if not isinstance(blue, VertexData) or red.subject != blue.subject: raise TypeError("Invalid data for blue channel") if (subject is not None) and (red.subject != subject): raise ValueError( "Subject in VertexData objects is different than specified subject" ) if ( (channel1color == Colors.Red) and (channel2color == Colors.Green) and (channel3color == Colors.Blue) and vmin is None and vmax is None and autorange == 'individual' ): # R/G/B basis can be directly passed through self.red = red self.green = green self.blue = blue self.alpha = alpha else: # need to remap colors r, g, b, alpha = DataviewRGB.color_voxels( red, green, blue, channel1color, channel2color, channel3color, max_color_value, max_color_saturation, vmin, vmax, autorange, alpha=alpha, ) self.red = Vertex(r, red.subject) self.green = Vertex(g, red.subject) self.blue = Vertex(b, red.subject) self.alpha = alpha else: if subject is None: raise TypeError("Subject name is required") if not isinstance(green, np.ndarray) or not isinstance(blue, np.ndarray): raise TypeError( "Data channels must be numpy arrays if red is a numpy array" ) if ( (channel1color == Colors.Red) and (channel2color == Colors.Green) and (channel3color == Colors.Blue) and vmin is None and vmax is None and autorange == 'individual' ): # R/G/B basis can be directly passed through self.red = Vertex(red, subject) self.green = Vertex(green, subject) self.blue = Vertex(blue, subject) self.alpha = alpha else: # need to remap colors r, g, b, alpha = DataviewRGB.color_voxels( red, green, blue, channel1color, channel2color, channel3color, max_color_value, max_color_saturation, vmin, vmax, autorange, alpha=alpha, ) self.red = Vertex(r, subject) self.green = Vertex(g, subject) self.blue = Vertex(b, subject) self.alpha = alpha super(VertexRGB, self).__init__( subject, alpha, description=description, state=state, priority=priority )
@property def alpha(self): """Compute alpha transparency""" alpha = self._alpha if alpha is None: alpha = np.ones(self.red.vertices.shape[1]) alpha = Vertex(alpha, self.red.subject, vmin=0, vmax=1) if not isinstance(alpha, Vertex): if alpha.dtype != np.uint8 and (alpha.min() < 0 or alpha.max() > 1): warnings.warn( "Some alpha values are outside the range of [0, 1]. " "Consider passing a Vertex object as alpha with explicit vmin, vmax " "keyword arguments.", Warning, ) alpha = Vertex(alpha, self.red.subject, vmin=0, vmax=1) rgb = np.array([self.red.data, self.green.data, self.blue.data]) mask = np.isnan(rgb).any(axis=0) alpha.data[mask] = alpha.vmin return alpha @alpha.setter def alpha(self, alpha: Optional[Union[npt.NDArray, Vertex]]): self._alpha = alpha @property def vertices(self): """3-dimensional volume (t, v, rgba) with data that has been mapped into 8-bit unsigned integers that correspond to colors. """ verts = [] for dv in (self.red, self.green, self.blue, self.alpha): if dv.vertices.dtype != np.uint8: vert = dv.vertices.astype("float32", copy=True) if dv.vmin is None: if vert.min() < 0: vert -= vert.min() else: vert -= dv.vmin if dv.vmax is None: if vert.max() > 1: vert /= vert.max() else: vert /= dv.vmax - dv.vmin vert = (np.clip(vert, 0, 1) * 255).astype(np.uint8) else: vert = dv.vertices.copy() verts.append(vert) return np.array(verts).transpose([1, 2, 0])
[docs] def to_json(self, simple=False): sdict = super(VertexRGB, self).to_json(simple=simple) if simple: sdict.update(dict(split=self.red.llen, frames=self.vertices.shape[0])) return sdict
@property def left(self): return self.vertices[:, : self.red.llen] @property def right(self): return self.vertices[:, self.red.llen :] def __repr__(self): return "<RGB vertex data for (%s)>" % (self.subject) def __hash__(self): return hash(_hash(self.vertices)) @property def name(self): return "__%s" % _hash(self.vertices)[:16] @property def raw(self): return self