Example #1
0
    def shift(self, shift_cols, shift_rows):
        """Shift the image foreground

        This function shifts the center of mass (CoM) of the image by the
        specified number of rows and columns.

        Parameters
        ----------
        shift_cols : float
            Number of columns by which to shift the CoM.
            Positive: to the right, negative: to the left
        shift_rows : float
            Number of rows by which to shift the CoM.
            Positive: downward, negative: upward

        Returns
        -------
        stim : `ImageStimulus`
            A copy of the stimulus object containing the shifted image

        """
        img = self.data.reshape(self.img_shape)
        tf = SimilarityTransform(translation=[shift_cols, shift_rows])
        img = img_warp(img, tf.inverse)
        return ImageStimulus(img,
                             electrodes=self.electrodes,
                             metadata=self.metadata)
Example #2
0
    def center(self, loc=None):
        """Center the image foreground

        This function shifts the center of mass (CoM) to the image center.

        Parameters
        ----------
        loc : (col, row), optional
            The pixel location at which to center the CoM. By default, shifts
            the CoM to the image center.

        Returns
        -------
        stim : `ImageStimulus`
            A copy of the stimulus object containing the centered image

        """
        # Calculate center of mass:
        img = self.data.reshape(self.img_shape)
        m = img_moments(img, order=1)
        # No area found:
        if np.isclose(m[0, 0], 0):
            return img
        # Center location:
        if loc is None:
            loc = np.array(self.img_shape[::-1]) / 2.0 - 0.5
        # Shift the image by -centroid, +image center:
        transl = (loc[0] - m[0, 1] / m[0, 0], loc[1] - m[1, 0] / m[0, 0])
        tf_shift = SimilarityTransform(translation=transl)
        img = img_warp(img, tf_shift.inverse)
        return ImageStimulus(img,
                             electrodes=self.electrodes,
                             metadata=self.metadata)
Example #3
0
    def scale(self, scaling_factor):
        """Scale the image foreground

        This function scales the image foreground (excluding black pixels)
        by a factor.

        Parameters
        ----------
        scaling_factor : float
            Factory by which to scale the image

        Returns
        -------
        stim : `ImageStimulus`
            A copy of the stimulus object containing the scaled image

        """
        if scaling_factor <= 0:
            raise ValueError("Scaling factor must be greater than zero")
        # Calculate center of mass:
        img = self.data.reshape(self.img_shape)
        m = img_moments(img, order=1)
        # No area found:
        if np.isclose(m[0, 0], 0):
            return img
        # Shift the phosphene to (0, 0):
        center_mass = np.array([m[0, 1] / m[0, 0], m[1, 0] / m[0, 0]])
        tf_shift = SimilarityTransform(translation=-center_mass)
        # Scale the phosphene:
        tf_scale = SimilarityTransform(scale=scaling_factor)
        # Shift the phosphene back to where it was:
        tf_shift_inv = SimilarityTransform(translation=center_mass)
        # Combine all three transforms:
        tf = tf_shift + tf_scale + tf_shift_inv
        img = img_warp(img, tf.inverse)
        return ImageStimulus(img,
                             electrodes=self.electrodes,
                             metadata=self.metadata)
Example #4
0
def plot_argus_phosphenes(data,
                          argus=None,
                          scale=1.0,
                          axon_map=None,
                          show_fovea=True,
                          ax=None):
    """Plots phosphenes centered over the corresponding electrodes

    .. versionadded:: 0.7

    Parameters
    ----------
    data : pd.DataFrame
        The Beyeler2019 dataset, a subset thereof, or a DataFrame with
        identical organization (i.e., must contain columns 'subject', 'image',
        'xrange', and 'yrange').
    argus : :py:class:`~pulse2percept.implants.ArgusI` or :py:class:`~pulse2percept.implants.ArgusII`
        Either an Argus I or Argus II implant. If None, the data is expected to
        contain additional columns: "implant_type_str", "implant_x",
        "implant_y", and "implant_rot", which together define the type and
        position of the implant. "implant_type_str" must be either "ArgusI" or
        "ArgusII".
    scale : float
        Scaling factor to apply to the phosphenes
    axon_map : :py:class:`~pulse2percept.models.AxonMapModel`
        An instance of the axon map model to use for visualization.
    show_fovea : bool
        Whether to indicate the location of the fovea with a square
    ax : axis
        Matplotlib axis
    """
    if not isinstance(data, pd.DataFrame):
        raise TypeError(f'"data" must be a Pandas DataFrame, not '
                        f'{type(data)}.')
    req_cols = ['subject', 'electrode', 'image', 'xrange', 'yrange']
    if not all(col in data.columns for col in req_cols):
        raise ValueError(f'"data" must have columns {req_cols}.')
    if len(data) == 0:
        raise ValueError('"data" is empty.')
    if len(data.subject.unique()) > 1:
        raise ValueError('"data" cannot contain data from more than one '
                         'subject.')
    if axon_map is not None and not isinstance(axon_map, AxonMapModel):
        raise TypeError(f'"axon_map" must be an AxonMapModel instance, not '
                        f'{type(axon_map)}.')
    if argus is None:
        # Implant not given, must first be created from data columns:
        try:
            specs = data.iloc[0]
            implant_type = ArgusI if specs.implant_type_str == 'ArgusI' else ArgusII
            argus = implant_type(x=specs['implant_x'],
                                 y=specs['implant_y'],
                                 rot=specs['implant_rot'])
        except (KeyError, AttributeError):
            raise ValueError('If "argus" is not given, "data" must contain '
                             'columns "implant_type_str", "implant_x", '
                             '"implant_y", and "implant_rot".')
    if not isinstance(argus, (ArgusI, ArgusII)):
        raise TypeError(f'"argus" must be an Argus I or Argus II implant, '
                        f'not {type(argus)}.')
    if isinstance(argus, ArgusI):
        px_argus = PX_ARGUS1
        img_argus = imread(PATH_ARGUS1)
    else:
        px_argus = PX_ARGUS2
        img_argus = imread(PATH_ARGUS2)
    if ax is None:
        ax = plt.gca()
    alpha_bg = 0.5  # alpha value for the array in the background
    thresh_fg = 0.95  # Grayscale value above which to mask the drawings

    # To simulate an implant in a left eye, flip the image left-right (along
    # with the electrode x-coordinates):
    if argus.eye == 'LE':
        img_argus = np.fliplr(img_argus)
        px_argus[:, 0] = img_argus.shape[1] - px_argus[:, 0] - 1

    # Add some padding to the output image so the array is not cut off:
    pad = 2000  # microns
    x_el = [e.x for e in argus.electrode_objects]
    y_el = [e.y for e in argus.electrode_objects]
    x_min, y_min = Watson2014Map.ret2dva(
        np.min(x_el) - pad,
        np.min(y_el) - pad)
    x_max, y_max = Watson2014Map.ret2dva(
        np.max(x_el) + pad,
        np.max(y_el) + pad)

    # Coordinate transform from degrees of visual angle to output, and from
    # image coordinates to output image:
    pts_in = []
    pts_dva = []
    pts_out = []
    try:
        out_shape = data.img_shape.values[0]
    except AttributeError:
        # Dataset does not have 'img_shape' column:
        try:
            out_shape = data.image.values[0].shape
        except IndexError:
            out_shape = (768, 1024)
    for xy, e in zip(px_argus, argus.electrode_objects):
        x_dva, y_dva = Watson2014Map.ret2dva(e.x, e.y)
        x_out = (x_dva - x_min) / (x_max - x_min) * (out_shape[1] - 1)
        y_out = (y_dva - y_min) / (y_max - y_min) * (out_shape[0] - 1)
        pts_in.append(xy)
        pts_dva.append([x_dva, y_dva])
        pts_out.append([x_out, y_out])
    pts_in = np.array(pts_in)
    pts_dva = np.array(pts_dva)
    pts_out = np.array(pts_out)
    dva2out = img_transform('similarity', pts_dva, pts_out)
    argus2out = img_transform('similarity', pts_in, pts_out)

    # Top left, top right, bottom left, bottom right corners:
    x_range = data.xrange.values[0]
    y_range = data.yrange.values[0]
    pts_dva = [[x_range[0], y_range[0]], [x_range[0], y_range[1]],
               [x_range[1], y_range[0]], [x_range[1], y_range[1]]]

    # Calculate average drawings, but don't binarize:
    all_imgs = np.zeros(out_shape)
    num_imgs = data.groupby('electrode')['image'].count()
    for _, row in data.iterrows():
        e_pos = Watson2014Map.ret2dva(argus[row['electrode']].x,
                                      argus[row['electrode']].y)
        align_center = dva2out(e_pos)[0]
        img_drawing = scale_image(row['image'], scale)
        img_drawing = center_image(img_drawing, loc=align_center)
        # We normalize by the number of phosphenes per electrode, so that if
        # all phosphenes are the same, their brightness would add up to 1:
        all_imgs += 1.0 / num_imgs[row['electrode']] * img_drawing
    all_imgs = 1 - all_imgs

    # Draw array schematic with specific alpha level:
    img_arr = img_warp(img_argus,
                       argus2out.inverse,
                       cval=1.0,
                       output_shape=out_shape)
    img_arr[:, :, 3] = alpha_bg

    # Replace pixels where drawings are dark enough, set alpha=1:
    rr, cc = np.unravel_index(
        np.where(all_imgs.ravel() < thresh_fg)[0], all_imgs.shape)
    for channel in range(3):
        img_arr[rr, cc, channel] = all_imgs[rr, cc]
    img_arr[rr, cc, 3] = 1

    ax.imshow(img_arr, cmap='gray', zorder=ZORDER['background'])

    if show_fovea:
        fovea = dva2out([0, 0])[0]
        ax.scatter(*fovea,
                   s=100,
                   marker='s',
                   c='w',
                   edgecolors='k',
                   zorder=ZORDER['foreground'])

    if axon_map is not None:
        axon_bundles = axon_map.grow_axon_bundles(n_bundles=100, prune=False)
        # Draw axon pathways:
        for bundle in axon_bundles:
            # Flip y upside down for dva:
            bundle = Watson2014Map.ret2dva(bundle[:, 0], -bundle[:, 1])
            bundle = np.array(bundle).T
            # Set segments outside the drawing window to NaN:
            x_idx = np.logical_or(bundle[:, 0] < x_min, bundle[:, 0] > x_max)
            bundle[x_idx, 0] = np.nan
            y_idx = np.logical_or(bundle[:, 1] < y_min, bundle[:, 1] > y_max)
            bundle[y_idx, 1] = np.nan
            bundle = dva2out(bundle)
            ax.plot(bundle[:, 0],
                    bundle[:, 1],
                    c=(0.6, 0.6, 0.6),
                    linewidth=2,
                    zorder=ZORDER['background'])

    return ax
Example #5
0
def plot_argus_phosphenes(X,
                          argus,
                          scale=1.0,
                          axon_map=None,
                          show_fovea=True,
                          ax=None):
    """Plots phosphenes centered over the corresponding electrodes

    .. versionadded:: 0.7

    Parameters
    ----------
    X : pd.DataFrame
    argus : :py:class:`~pulse2percept.implants.ArgusI` or :py:class:`~pulse2percept.implants.ArgusII`
        Either an Argus I or Argus II implant
    scale : float
        Scaling factor to apply to the phosphenes
    show_fovea : bool
        Whether to indicate the location of the fovea with a square
    ax : axis
        Matplotlib axis
    """
    if not isinstance(X, pd.DataFrame):
        raise TypeError('"X" must be a Pandas DataFrame, not %s.' % type(X))
    req_cols = ['subject', 'electrode', 'image', 'img_x_dva', 'img_y_dva']
    if not all(col in X.columns for col in req_cols):
        raise ValueError('"X" must have columns %s.' % req_cols)
    if len(X) == 0:
        raise ValueError('"X" is empty.')
    if len(X.subject.unique()) > 1:
        raise ValueError('"X" cannot contain data from more than one subject.')
    if not isinstance(argus, (ArgusI, ArgusII)):
        raise TypeError('"argus" must be an Argus I or Argus II implant, not '
                        '%s.' % type(argus))
    if ax is None:
        ax = plt.gca()
    alpha_bg = 0.5  # alpha value for the array in the background
    thresh_fg = 0.95  # Grayscale value above which to mask the drawings

    if isinstance(argus, ArgusI):
        px_argus = PX_ARGUS1
        img_argus = imread(PATH_ARGUS1)
    else:
        px_argus = PX_ARGUS2
        img_argus = imread(PATH_ARGUS2)

    # To simulate an implant in a left eye, flip the image left-right (along
    # with the electrode x-coordinates):
    if argus.eye == 'LE':
        img_argus = np.fliplr(img_argus)
        px_argus[:, 0] = img_argus.shape[1] - px_argus[:, 0] - 1

    # Add some padding to the output image so the array is not cut off:
    pad = 2000  # microns
    x_el = [e.x for e in argus.values()]
    y_el = [e.y for e in argus.values()]
    x_min = Watson2014Transform.ret2dva(np.min(x_el) - pad)
    x_max = Watson2014Transform.ret2dva(np.max(x_el) + pad)
    y_min = Watson2014Transform.ret2dva(np.min(y_el) - pad)
    y_max = Watson2014Transform.ret2dva(np.max(y_el) + pad)

    # Coordinate transform from degrees of visual angle to output, and from
    # image coordinates to output image:
    pts_in = []
    pts_dva = []
    pts_out = []
    try:
        out_shape = X.img_shape.values[0]
    except AttributeError:
        # Dataset does not have 'img_shape' column:
        try:
            out_shape = X.image.values[0].shape
        except IndexError:
            out_shape = (768, 1024)
    for xy, e in zip(px_argus, argus.values()):
        x_dva, y_dva = Watson2014Transform.ret2dva([e.x, e.y])
        x_out = (x_dva - x_min) / (x_max - x_min) * (out_shape[1] - 1)
        y_out = (y_dva - y_min) / (y_max - y_min) * (out_shape[0] - 1)
        pts_in.append(xy)
        pts_dva.append([x_dva, y_dva])
        pts_out.append([x_out, y_out])
    pts_in = np.array(pts_in)
    pts_dva = np.array(pts_dva)
    pts_out = np.array(pts_out)
    dva2out = img_transform('similarity', pts_dva, pts_out)
    argus2out = img_transform('similarity', pts_in, pts_out)

    # Top left, top right, bottom left, bottom right corners:
    x_range = X.img_x_dva
    y_range = X.img_y_dva
    pts_dva = [[x_range[0], y_range[0]], [x_range[0], y_range[1]],
               [x_range[1], y_range[0]], [x_range[1], y_range[1]]]

    # Calculate average drawings, but don't binarize:
    all_imgs = np.zeros(out_shape)
    num_imgs = X.groupby('electrode')['image'].count()
    for _, row in X.iterrows():
        e_pos = Watson2014Transform.ret2dva(
            (argus[row['electrode']].x, argus[row['electrode']].y))
        align_center = dva2out(e_pos)[0]
        img_drawing = scale_phosphene(row['image'], scale)
        img_drawing = center_phosphene(img_drawing, loc=align_center)
        # We normalize by the number of phosphenes per electrode, so that if
        # all phosphenes are the same, their brightness would add up to 1:
        all_imgs += 1.0 / num_imgs[row['electrode']] * img_drawing
    all_imgs = 1 - all_imgs

    # Draw array schematic with specific alpha level:
    img_arr = img_warp(img_argus,
                       argus2out.inverse,
                       cval=1.0,
                       output_shape=out_shape)
    img_arr[:, :, 3] = alpha_bg

    # Replace pixels where drawings are dark enough, set alpha=1:
    rr, cc = np.unravel_index(
        np.where(all_imgs.ravel() < thresh_fg)[0], all_imgs.shape)
    for channel in range(3):
        img_arr[rr, cc, channel] = all_imgs[rr, cc]
    img_arr[rr, cc, 3] = 1

    ax.imshow(img_arr, cmap='gray', zorder=1)

    if show_fovea:
        fovea = dva2out([0, 0])[0]
        ax.scatter(*fovea, s=100, marker='s', c='w', edgecolors='k', zorder=99)

    if axon_map is not None:
        axon_bundles = axon_map.grow_axon_bundles(n_bundles=100, prune=False)
        # Draw axon pathways:
        for bundle in axon_bundles:
            # Flip y upside down for dva:
            bundle = Watson2014Transform.ret2dva(bundle) * [1, -1]
            # Trim segments outside the drawing window:
            idx = np.logical_and(
                np.logical_and(bundle[:, 0] >= x_min, bundle[:, 0] <= x_max),
                np.logical_and(bundle[:, 1] >= y_min, bundle[:, 1] <= y_max))
            bundle = dva2out(bundle)
            ax.plot(bundle[idx, 0],
                    bundle[idx, 1],
                    c=(0.6, 0.6, 0.6),
                    linewidth=2,
                    zorder=1)

    return ax