Source code for nivlink.screen

import numpy as np

def _ellipse_in_shape(shape, center, radii, rotation=0.):
    """Generate coordinates of points within ellipse bounded by shape.
    
    Parameters
    ----------
    shape :  iterable of ints
        Shape of the input image.  Must be length 2.
    center : iterable of floats
        (row, column) position of center inside the given shape.
    radii : iterable of floats
        Size of two half axes (for row and column)
    rotation : float, optional
        Rotation of the ellipse defined by the above, in radians
        in range (-PI, PI), in contra clockwise direction,
        with respect to the column-axis.
    
    Returns
    -------
    rows : iterable of ints
        Row coordinates representing values within the ellipse.
    cols : iterable of ints
        Corresponding column coordinates representing values within the ellipse.
    """
    # https://github.com/scikit-image/scikit-image/blob/master/skimage/draw/draw.py
    r_lim, c_lim = np.ogrid[0:float(shape[0]), 0:float(shape[1])]
    r_org, c_org = center
    r_rad, c_rad = radii
    rotation %= np.pi
    sin_alpha, cos_alpha = np.sin(rotation), np.cos(rotation)
    r, c = (r_lim - r_org), (c_lim - c_org)
    distances = ((r * cos_alpha + c * sin_alpha) / r_rad) ** 2 \
                + ((r * sin_alpha - c * cos_alpha) / c_rad) ** 2
    return np.nonzero(distances < 1)

def _ellipse(x, y, x_radius, y_radius, shape=None, rotation=0.):
    """Generate coordinates of pixels within ellipse.
    
    Parameters
    ----------
    x, y : int
        Centre coordinate of ellipse.
    x_radius, y_radius : int
        Axes along the x- and y-dimensions. ``(x/x_radius)**2 + (y/y_radius)**2 = 1``.
    shape : tuple, optional
        Image shape which is used to determine the maximum extent of output pixel
        coordinates. This is useful for ellipses which exceed the image size.
        By default the full extent of the ellipse are used.
    rotation : float, optional (default 0.)
        Set the ellipse rotation (rotation) in range (-PI, PI)
        in contra clock wise direction, so PI/2 degree means swap ellipse axis
    
    Returns
    -------
    xx, yy : ndarray of int
        Pixel coordinates of ellipse. May be used to directly index into an array, 
        e.g. ``img[rr, cc] = 1``.
    
    Notes
    -----
    The ellipse equation::
        ((x * cos(alpha) + y * sin(alpha)) / x_radius) ** 2 +
        ((x * sin(alpha) - y * cos(alpha)) / y_radius) ** 2 = 1
    Note that the positions of `ellipse` without specified `shape` can have
    also, negative values, as this is correct on the plane. On the other hand
    using these ellipse positions for an image afterwards may lead to appearing
    on the other side of image, because ``image[-1, -1] = image[end-1, end-1]``
    """
    # https://github.com/scikit-image/scikit-image/blob/master/skimage/draw/draw.py
    center = np.array([x, y])
    radii = np.array([x_radius, y_radius])
    
    # allow just rotation with in range +/- 180 degree
    rotation %= np.pi

    # compute rotated radii by given rotation
    y_radius_rot = abs(y_radius * np.cos(rotation)) \
                   + x_radius * np.sin(rotation)
    x_radius_rot = y_radius * np.sin(rotation) \
                   + abs(x_radius * np.cos(rotation))
    # The upper_left and lower_right corners of the smallest rectangle
    # containing the ellipse.
    radii_rot = np.array([y_radius_rot, x_radius_rot])
    upper_left = np.ceil(center - radii_rot).astype(int)
    lower_right = np.floor(center + radii_rot).astype(int)

    if shape is not None:
        # Constrain upper_left and lower_right by shape boundary.
        upper_left = np.maximum(upper_left, np.array([0, 0]))
        lower_right = np.minimum(lower_right, np.array(shape[:2]) - 1)

    shifted_center = center - upper_left
    bounding_shape = lower_right - upper_left + 1

    rr, cc = _ellipse_in_shape(bounding_shape, shifted_center, radii, rotation)
    rr.flags.writeable = True
    cc.flags.writeable = True
    rr += upper_left[0]
    cc += upper_left[1]
    return rr, cc

[docs]class ScreenInfo(object): """Container for visual stimuli information. Parameters ---------- xdim : int Screen size along horizontal axis (in pixels). ydim : int Screen size along vertical axis (in pixels). sfreq : float Sampling rate of eyetracker. n_screens: int Number different screens corresponding to different AoI distributions. Defauls to 1. Attributes ---------- labels : array List of unique AoIs. indices : array, shape (xdim, ydim) Look-up table matching pixels to AoIs. """ def __init__(self, xdim, ydim, sfreq, n_screens=1): self.sfreq = sfreq self.xdim = xdim self.ydim = ydim self.n_screens = n_screens self.labels = () self.indices = np.zeros((xdim,ydim,n_screens)) def _update_aoi(self): """Convenience function for updating AoI indices.""" values, indices = np.unique(self.indices, return_inverse=True) if np.all(values): indices += 1 self.indices = indices.reshape(self.xdim, self.ydim, self.n_screens) self.labels = tuple(range(1,int(self.indices.max())+1))
[docs] def add_rectangle_aoi(self, xmin, xmax, ymin, ymax, screen_id=1): """Add rectangle area of interest to screen. Parameters ---------- xmin, ymin : int or float Coordinates of top-left corner of AoI. Accepts absolute or fractional [0-1] position. xmax, ymax : int or float Coordinates of bottom-right corner of AoI. screen_id: int Which screen to add AoI to. Defaults to 1. Returns ------- None `indices` and `labels` modified in place. """ isfrac = lambda v: True if v < 1 and v > 0 else False xmin, xmax = [int(self.xdim * x) if isfrac(x) else int(x) for x in [xmin,xmax]] ymin, ymax = [int(self.ydim * y) if isfrac(y) else int(y) for y in [ymin,ymax]] self.indices[xmin:xmax,ymin:ymax,screen_id - 1] = self.indices.max() + 1 self._update_aoi()
[docs] def add_ellipsoid_aoi(self, x, y, x_radius, y_radius, rotation=0., screen_id=1, mask=None): """Generate coordinates of pixels within ellipse. Parameters ---------- x, y : int Center coordinate of ellipse. x_radius, y_radius : int Axes along the x- and y-dimensions. rotation : float Set the ellipse rotation (rotation) in range :math:`[-\pi, \pi]` in contra-clockwise direction, so :math:`\pi / 2` degree means swap ellipse axis. screen_id: int Which screen to add AoI to. Defaults to 1. mask: int Screen-sized array of 0s and 1s used to mask out parts of the display. Defaults to none. Returns ------- None `indices` and `labels` modified in place. """ # https://github.com/scikit-image/scikit-image/blob/master/skimage/draw/draw.py xx, yy = _ellipse(x, y, x_radius, y_radius, shape=(self.xdim,self.ydim), rotation=rotation) if mask is not None: # Flatten mask indices. (xx_mask,yy_mask) = mask.nonzero() mi = np.ravel_multi_index(np.array([xx_mask,yy_mask]),(self.xdim, self.ydim)) # Flatten ellipse indices. ei = np.ravel_multi_index(np.array([xx,yy]),(self.xdim, self.ydim)) # Intersect indices. idx = np.intersect1d(mi,ei) # Unravel into 2D again. [xxf,yyf] = np.unravel_index(idx,(self.xdim, self.ydim)) else: xxf = xx yyf = yy self.indices[xxf,yyf,screen_id - 1] = self.indices.max() + 1 self._update_aoi()
[docs] def plot_aoi(self, screen_id, height=3, ticks=False, cmap=None): """Plot areas of interest. Parameters ---------- screen_id: int Set of AoIs to plot. height : float Height of figure (in inches). ticks : bool Include axis ticks. cmap : matplotlib.cm object Colormap. Defaults to ListedColorMap. Returns ------- fig, ax : plt.figure Figure and axis of plot. Notes ----- Requires matplotlib. """ import matplotlib.pyplot as plt import matplotlib.cm as cm import matplotlib as matplotlib from matplotlib.colors import ListedColormap from mpl_toolkits.axes_grid1 import make_axes_locatable ## Initialize plot. ratio = float(self.xdim) / float(self.ydim) fig, ax = plt.subplots(1,1,figsize=(ratio*height, height)) divider = make_axes_locatable(ax) cax = divider.append_axes("right", size="3%", pad=0.05) ## Initialize colormap. if cmap is None: # Collect hex values from standard colormap. cmap = cm.get_cmap('tab20', 20) colors = [] for i in range(cmap.N): rgb = cmap(i)[:3] # will return rgba, we take only first 3 so we get rgb colors.append(matplotlib.colors.rgb2hex(rgb)) colors = colors[:len(self.labels)] # Add black. if np.any(self.indices==0): colors = np.insert(colors, 0, 'k') # Construct new colormap. cmap = ListedColormap(colors) ## Plotting. cbar = ax.imshow(self.indices[:,:,screen_id-1].T, cmap=cmap, aspect='auto', vmin=0, vmax=len(self.labels)) fig.colorbar(cbar, cax, ticks=np.arange(len(cmap.colors))) if not ticks: ax.set(xticks=[], yticks=[]) return fig, ax