Source code for stimupy.components

import itertools
import logging

import numpy as np

from stimupy.components import *  # noqa: F403 # angulars, edges, frames, gaussians, lines, radials, shapes, waves
from stimupy.components import texts
from stimupy.utils import resolution

# Get module level logger
logger = logging.getLogger("stimupy.components")

__all__ = [
    "overview",
    "plot_overview",
    "image_base",
    "draw_regions",
    "mask_regions",
    "combine_masks",
    "overview",
    "angulars",  # noqa: F405
    "radials",  # noqa: F405
    "edges",  # noqa: F405
    "frames",  # noqa: F405
    "gaussians",  # noqa: F405
    "lines",  # noqa: F405
    "shapes",  # noqa: F405
    "texts",  # noqa: F405
    "waves",  # noqa: F405
]


[docs] def image_base(visual_size=None, shape=None, ppd=None, rotation=0.0, origin="mean"): """Create coordinate-arrays to serve as image base for drawing Parameters ---------- visual_size : Sequence[Number, Number], Number, or None (default) visual size [height, width] of image, in degrees ppd : Sequence[Number, Number], Number, or None (default) pixels per degree [vertical, horizontal] shape : Sequence[Number, Number], Number, or None (default) shape [height, width] of image, in pixels rotation : float, optional rotation (in degrees) from 3 o'clock, counterclockwise, by default 0.0 origin : "corner", "mean" or "center" if "corner": set origin to upper left corner if "mean": set origin to hypothetical image center (default) if "center": set origin to real center (closest existing value to mean) Returns ------- dict[str, Any] dict with keys: "visual_size", "ppd" : resolved from input arguments, "x", "y" : single axes "horizontal", "vertical" : numpy.ndarray of shape, with distance from origin, in deg. visual angle, at each pixel "oblique", "oblique_y" : numpy.ndarray of shape, with oblique distances from origin, in deg. visual angle, at each pixel "radial" : numpyn.ndarray of shape, with radius from origin, in deg. visual angle, at each pixel "angular" : numpy.ndarray of shape, with angle relative to 3 o'clock, in rad, at each pixel "rectilinear" : numpy.ndarray of shape, with rectilinear/cityblock/Manhattan distance from origin, in deg. visual angle, at each pixel """ # Resolve resolution shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) # Get single axes x, y = resolution.visual_size_to_axes(visual_size=visual_size, shape=shape, origin=origin) # Linear distance image bases xx, yy = np.meshgrid(x, y) # Rotate to get obliques alpha = [np.cos(np.deg2rad(-rotation)), np.sin(np.deg2rad(-rotation))] beta = [np.cos(np.deg2rad(rotation)), np.sin(np.deg2rad(rotation))] oblique_x = alpha[0] * xx + alpha[1] * yy oblique_y = beta[1] * xx + beta[0] * yy if origin == "corner": oblique_x = oblique_x - oblique_x.min() oblique_y = oblique_y - oblique_y.min() # Rectilinear distance (frames) rectilinear = np.maximum(np.abs(oblique_x), np.abs(oblique_y)) # Radial distance radial = np.sqrt(xx**2 + yy**2) # Angular distance angular = np.arctan2(xx, yy) angular -= np.deg2rad(rotation + 90) angular %= 2 * np.pi return { "visual_size": visual_size, "ppd": ppd, "shape": shape, "rotation": rotation, "x": x, "y": y, "horizontal": xx, "vertical": yy, "oblique": oblique_x, "oblique_y": oblique_y, "rectilinear": rectilinear, "radial": radial, "angular": angular, }
[docs] def mask_regions( distance_metric, edges, shape=None, visual_size=None, ppd=None, rotation=0.0, origin=None, ): """Generate mask for regions in image Regions are defined by `edges` along a `distance_metric`. Regions will be masked consecutively, from `origin` outwards, such that each `edge` is the upper-limit of a region. Parameters ---------- distance_metric : any of keys in stimupy.components.image_base() which distance metric to mask over edges : Sequence[Number] upper-limit of each consecutive region visual_size : Sequence[Number, Number], Number, or None (default) visual size [height, width] of image, in degrees ppd : Sequence[Number, Number], Number, or None (default) pixels per degree [vertical, horizontal] shape : Sequence[Number, Number], Number, or None (default) shape [height, width] of image, in pixels rotation : float, optional rotation (in degrees) from 3 o'clock, counterclockwise, by default 0.0 origin : "corner", "mean" or "center" if "corner": set origin to upper left corner if "mean": set origin to hypothetical image center (default) if "center": set origin to real center (closest existing value to mean) Returns ------- dict[str, Any] mask with integer index for each angular segment (key: "mask"), and additional keys containing stimulus parameters """ # Set up coordinates base = image_base( shape=shape, visual_size=visual_size, ppd=ppd, rotation=rotation, origin=origin ) distances = base[distance_metric] distances = np.round(distances, 8) if isinstance(edges, (int, float)): edges = (edges,) # Mark elements with integer idx-value mask = np.zeros(base["shape"], dtype=int) for idx, edge in zip(reversed(range(len(edges))), reversed(edges)): mask[distances <= edge] = int(idx + 1) # Assemble output return { "mask": mask, "edges": edges, "distance_metric": distance_metric, "rotation": base["rotation"], "shape": base["shape"], "visual_size": base["visual_size"], "ppd": base["ppd"], "distances": distances, "origin": origin, }
[docs] def combine_masks(*masks): """Combines several masks into a singular mask Increments mask-indices, such that the resulting mask contains consecutive integer indices. Masks are combined in order. Parameters ---------- mask_1, mask_2, ... : numpy.ndarray Masks to be combined Returns ------- numpy.ndarray Combined mask, where integer indices are in order of the input masks. Raises ------ ValueError if masks do not all have the same shape (in pixels) ValueError if multiple masks index the same pixel """ # Initialize combined_mask = np.zeros_like(masks[0]) for mask in masks: # Check that masks have the same shape if not mask.shape == combined_mask.shape: raise ValueError("Not all masks have the same shape") # Check that masks don't overlap if (combined_mask & mask).any(): raise ValueError("Masks overlap") # Combine: increase `mask`-idc by adding the current highest idx in combined_mask combined_mask = np.where(mask, mask + combined_mask.max(), combined_mask) return combined_mask
[docs] def draw_regions(mask, intensities, intensity_background=0.5): """Draw regions defined by mask, with given intensities Parameters ---------- mask : numpy.ndarray image-array with integer-indices for each region to draw intensities : Sequence[float, ...] intensity value for each masked region. Can specify as many intensities as number of masked regions; If fewer intensities are passed than masked regions, cycles through intensities intensity_background : float, optional intensity value of background, by default 0.5 Returns ------- numpy.ndarray image-array, same shape as mask, with intensity assigned to each masked region """ # Create background img = np.ones(mask.shape) * intensity_background # Get mask indices mask_idcs = np.unique(mask[mask > 0]) if isinstance(intensities, (float, int)): intensities = (intensities,) # Assign intensities to masked regions ints = [*itertools.islice(itertools.cycle(intensities), len(mask_idcs))] for frame_idx, intensity in zip(mask_idcs, ints): img = np.where(mask == frame_idx, intensity, img) return img
[docs] def overview(skip=False): """Generate example stimuli from this module Returns ------- dict[str, dict] Dict mapping names to individual stimulus dicts """ stimuli = {} for stimmodule_name in __all__: if stimmodule_name in [ "overview", "plot_overview", "draw_regions", "image_base", "mask_regions", "combine_masks", ]: continue logger.info(f"Generating stimuli from {stimmodule_name}") # Get a reference to the actual module stimmodule = globals()[stimmodule_name] try: stims = stimmodule.overview() # Accumulate stimuli.update(stims) except NotImplementedError as e: if not skip: raise e # Skip stimuli that aren't implemented logger.info("-- not implemented") pass return stimuli
[docs] def plot_overview(mask=False, save=None, units="deg"): """Plot overview of examples in this module (and submodules) Parameters ---------- mask : bool or str, optional If True, plot mask on top of stimulus image (default: False). If string is provided, plot this key from stimulus dictionary as mask save : None or str, optional If None (default), do not save the plot. If string is provided, save plot under this name. units : "px", "deg" (default), or str what units to put on the axes, by default degrees visual angle ("deg"). If a str other than "deg"(/"degrees") or "px"(/"pix"/"pixels") is passed, it must be the key to a tuple in stim """ from stimupy.utils import plot_stimuli stims = overview(skip=True) plot_stimuli(stims, mask=mask, units=units, save=save)
if __name__ == "__main__": # Log to console at INFO level logger.setLevel(logging.INFO) logger.addHandler(logging.StreamHandler()) plot_overview()