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)
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)
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)
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
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