Source code for stimupy.stimuli.checkerboards

import warnings

import numpy as np

from stimupy.components import draw_regions, waves
from stimupy.utils import resolution
from stimupy.utils.contrast_conversions import transparency

__all__ = [
    "checkerboard",
    "contrast_contrast",
]


def mask_from_idx(checkerboard_stim, check_idc):
    """Create binary mask for specified check indidces

    Parameters
    ----------
    checkerboard_stim : dict
        stimulus dictionary of checkerboard,
    check_idc : Sequence[(Number, Number),...]
        target indices (row, column of checkerboard) of checks to create mask for

    Returns
    -------
    numpy.ndarray
        mask, as binary 2D numpy.ndarray with 1 for all pixels belonging to
        specified check(s), and 0 everywhere else

    Raises
    ------
    ValueError
        Check index is invalid given the board shape
    """
    board_shape = checkerboard_stim["board_shape"]
    mask = np.zeros(checkerboard_stim["shape"])
    for i, coords in enumerate(check_idc):
        if coords[0] < 0 or coords[0] > coords[0] or coords[1] < 0 or coords[1] > board_shape[1]:
            raise ValueError(f"Cannot provide mask for check {coords} outside board {board_shape}")
        m1 = np.where(checkerboard_stim["col_mask"] == coords[1] + 1, 1, 0)
        m2 = np.where(checkerboard_stim["row_mask"] == coords[0] + 1, 1, 0)
        mask = np.where(m1 + m2 == 2, i + 1, mask)

        if len(np.unique(mask)) == 1:
            raise ValueError(
                f"Cannot provide mask for check {coords} outside board because of rotation"
            )
    return mask


def extend_target_idx(target_index, offsets=[(-1, 0), (0, -1), (0, 0), (0, 1), (1, 0)]):
    """Extend target indices, to not just the specified check(s) but also surrounding

    Parameters
    ----------
    target_index : (Number, Number)
        target indices (row, column of checkerboard)
    offsets : list, optional
        relative indices of neighboring checks to include in target,
        by default [(-1, 0), (0, -1), (0, 0), (0, 1), (1, 0)]

    Returns
    -------
    List[Tuple(Number, Number),...]
        List of all target indices
    """
    extended_idc = []
    for offset in offsets:
        new_idx = (target_index[0] + offset[0], target_index[1] + offset[1])
        extended_idc.append(new_idx)
    return extended_idc


def add_targets(checkerboard_stim, target_indices=(), extend_targets=False, intensity_target=0.5):
    """Add targets to a checkerboard stimulus

    Parameters
    ----------
    checkerboard_stim : dict
        stimulus dictionary of checkerboard,
        needs to contain at least "img" and "board_shape"
    target_indices : Sequence[(Number, Number),...]
        target indices (row, column of checkerboard)
    extend_targets : bool, optional
        if true, extends the targets by 1 check in all 4 directions, by default False
    intensity_target : float, or Sequence[float, ...], optional
        intensity value for each target, by default 0.5.
        Can specify as many intensities as number of target_indices;
        If fewer intensities are passed than target_indices, cycles through intensities

    Returns
    -------
    dict[str, Any]
        dict with the updated stimulus (key: "img"),
        mask with integer index for each target (key: "target_mask"),
        and additional keys containing stimulus parameters
    """
    if target_indices is None:
        target_indices = ()
    target_mask = np.zeros(checkerboard_stim["shape"])

    # Define target mask
    for i, target in enumerate(target_indices):
        if extend_targets:
            target_idc = extend_target_idx(target)
        else:
            target_idc = [target]
        for target_idx in target_idc:
            target_mask += mask_from_idx(checkerboard_stim, (target_idx,)) * (i + 1)
    checkerboard_stim["target_mask"] = target_mask.astype(int)

    # Draw targets
    checkerboard_stim["img"] = np.where(
        target_mask,
        draw_regions(mask=target_mask, intensities=intensity_target, intensity_background=0.0),
        checkerboard_stim["img"],
    )

    return checkerboard_stim


[docs]def checkerboard( visual_size=None, ppd=None, shape=None, frequency=None, board_shape=None, check_visual_size=None, target_indices=(), extend_targets=False, period="ignore", rotation=0.0, intensity_checks=(0.0, 1.0), intensity_target=0.5, round_phase_width=True, ): """Checkerboard assimilation effect High-contrast checkerboard, with intermediate targets embedded in it. Target brightness assimilates to direct surround, rather than contrast with it. These kinds of checkerboard displays are described by De Valois & De Valois (1988), and the brightness effect of it by Blakeslee & McCourt (2004). Parameters ---------- visual_size : Sequence[Number, Number], Number, or None (default) visual size of the total board [height, width] 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] in pixels frequency : Sequence[Number, Number], Number, or None (default) frequency of checkerboard in [y, x] in cpd board_shape : Sequence[Number, Number], Number, or None (default) number of checks in [height, width] of checkerboard check_visual_size : Sequence[Number, Number], Number, or None (default) visual size of a single check [height, width] in degrees target_indices : Sequence[(Number, Number),...], optional target indices (row, column of checkerboard), by default None extend_targets : bool, optional if true, extends the targets by 1 check in all 4 directions, by default False period : "even", "odd", "either" or "ignore" (default) ensure whether the grating has "even" number of phases, "odd" number of phases, either or whether not to round the number of phases ("ignore") rotation : float, optional rotation (in degrees), counterclockwise, by default 0.0 (horizontal) intensity_checks : Sequence[float, float] intensity values of checks, by default (0.0, 1.0) round_phase_width : Bool if True, round width of bars given resolution (default: True) Returns ------- dict[str, Any] dict with the stimulus (key: "img"), mask with integer index for each target (key: "target_mask"), and additional keys containing stimulus parameters References ---------- Blakeslee, B., & McCourt, M. E. (2004). A unified theory of brightness contrast and assimilation incorporating oriented multiscale spatial filtering and contrast normalization. Vision Research, 44(21), 2483-2503. https://doi.org/10/fmcx5p De Valois, R. L., & De Valois, K. K. (1988). Spatial Vision. Oxford University Press. """ lst = [visual_size, ppd, shape, frequency, board_shape, check_visual_size] if len([x for x in lst if x is not None]) < 3: raise ValueError( "'checkerboard()' needs 3 non-None arguments for resolving from 'visual_size', " "'ppd', 'shape', 'frequency', 'board_shape', 'check_visual_size'" ) if isinstance(frequency, (float, int)) or frequency is None: frequency = (frequency, frequency) if isinstance(board_shape, (float, int)) or board_shape is None: board_shape = (board_shape, board_shape) if isinstance(check_visual_size, (float, int)) or check_visual_size is None: check_visual_size = (check_visual_size, check_visual_size) create_twice = visual_size is None and shape is None # Create checkerboard by treating it as a plaid sw1 = waves.square( visual_size=visual_size, ppd=ppd, shape=shape, frequency=frequency[0], n_phases=board_shape[0], phase_width=check_visual_size[0], period=period, rotation=rotation, phase_shift=0, intensities=intensity_checks, origin="corner", round_phase_width=round_phase_width, distance_metric="oblique", ) sw2 = waves.square( visual_size=visual_size, ppd=ppd, shape=shape, frequency=frequency[1], n_phases=board_shape[1], phase_width=check_visual_size[1], period=period, rotation=rotation - 90, phase_shift=0, intensities=intensity_checks, origin="corner", round_phase_width=round_phase_width, distance_metric="oblique", ) # If neither a visual_size nor a shape was given, each square wave # grating is always a square. An easy solution is to just recreate # both gratings with the resolved parameters if create_twice: warnings.filterwarnings("ignore") sw1 = waves.square( visual_size=(sw1["visual_size"][0], sw2["visual_size"][1]), ppd=sw1["ppd"], shape=None, frequency=frequency[0], n_phases=board_shape[0], phase_width=check_visual_size[0], period=period, rotation=rotation, phase_shift=0, intensities=intensity_checks, origin="corner", round_phase_width=round_phase_width, distance_metric="oblique", ) sw2 = waves.square( visual_size=(sw1["visual_size"][0], sw2["visual_size"][1]), ppd=sw1["ppd"], shape=None, frequency=frequency[1], n_phases=board_shape[1], phase_width=check_visual_size[1], period=period, rotation=rotation - 90, phase_shift=0, intensities=intensity_checks, origin="corner", round_phase_width=round_phase_width, distance_metric="oblique", ) warnings.filterwarnings("default") # Add the two square-wave gratings into a checkerboard img = sw1["img"] + sw2["img"] img = np.where( img == intensity_checks[0] + intensity_checks[1], intensity_checks[1], intensity_checks[0] ) # Create a mask with target indices for each check mask = sw1["grating_mask"] + sw2["grating_mask"] * sw1["grating_mask"].max() * 10 unique_vals = np.unique(mask) for v in range(len(unique_vals)): mask[mask == unique_vals[v]] = v + 1 stim = { "img": img, "checker_mask": mask.astype(int), "col_mask": sw1["grating_mask"], "row_mask": sw2["grating_mask"], "visual_size": sw1["visual_size"], "ppd": sw1["ppd"], "shape": sw1["shape"], "frequency": (sw2["frequency"], sw1["frequency"]), "board_shape": (sw2["n_phases"], sw1["n_phases"]), "check_visual_size": (sw2["phase_width"], sw1["phase_width"]), "period": period, "rotation": rotation, "intensity_checks": intensity_checks, "target_indices": target_indices, "extend_targets": extend_targets, "round_phase_width": round_phase_width, } # Add targets stim = add_targets( stim, target_indices=target_indices, extend_targets=extend_targets, intensity_target=intensity_target, ) return stim
[docs]def contrast_contrast( visual_size=None, ppd=None, shape=None, frequency=None, board_shape=None, check_visual_size=None, target_shape=None, period="ignore", rotation=0.0, intensity_checks=(0.0, 1.0), tau=0.5, alpha=None, round_phase_width=True, ): """Contrast-contrast effect on checkerboard with square transparency layer Checkerboard version of the contrast-contrast illusion (Chubb, Sperling, Solomon, 1989), as used by Domijan (2015). Parameters ---------- shape : Sequence[Number, Number], Number, or None (default) shape [height, width] in pixels ppd : Sequence[Number, Number], Number, or None (default) pixels per degree [vertical, horizontal] visual_size : Sequence[Number, Number], Number, or None (default) visual size of the total board [height, width] in degrees board_shape : Sequence[Number, Number], Number, or None (default) number of checks in [height, width] of checkerboard check_visual_size : Sequence[Number, Number], Number, or None (default) visual size of a single check [height, width] in degrees targets_shape : Sequence[Number, Number], Number, or None (default) number of checks with transparency in y, x direction intensity_low : float, optional intensity value of the dark checks, by default 0.0 intensity_high : float, optional intensity value of the light checks, by default 1.0 tau : Number tau of transparency (i.e. value of transparent medium), default 0.5 alpha : Number or None (default) alpha of transparency (i.e. how transparent the medium is) round_phase_width : Bool if True, round width of bars given resolution (default: True) Returns ------- dict[str, Any] dict with the stimulus (key: "img"), mask with integer index for each target (key: "target_mask"), and additional keys containing stimulus parameters References ---------- Chubb, C., Sperling, G., & Solomon, J. A. (1989). Texture interactions determine perceived contrast. Proc. Natl. Acad. Sci. USA, 5. https://doi.org/10.1073/pnas.86.23.9631 Domijan, D. (2015). A Neurocomputational account of the role of contour facilitation in brightness perception. Frontiers in Human Neuroscience, 9(February), 1-16. https://doi.org/10/gh62x2 """ if target_shape is None: raise ValueError("contrast_contrast() missing argument 'target_shape' which is not 'None'") if alpha is None: raise ValueError("contrast_contrast() missing argument 'alpha' which is not 'None'") if isinstance(target_shape, (int, float)): target_shape = (target_shape, target_shape) # Set up basic checkerboard stim = checkerboard( visual_size=visual_size, ppd=ppd, shape=shape, frequency=frequency, board_shape=board_shape, check_visual_size=check_visual_size, period=period, rotation=rotation, intensity_checks=intensity_checks, round_phase_width=round_phase_width, ) img = stim["img"] # Determine target locations check_size_px = resolution.shape_from_visual_size_ppd( visual_size=stim["check_visual_size"], ppd=stim["ppd"] ) target_idx = np.zeros(img.shape, dtype=bool) tposy = (img.shape[0] - target_shape[0] * check_size_px.height) // 2 tposx = (img.shape[1] - target_shape[1] * check_size_px.width) // 2 target_idx[ tposy : tposy + target_shape[0] * check_size_px[0], tposx : tposx + target_shape[1] * check_size_px[1], ] = True # Construct mask for target region mask = np.zeros(img.shape) mask[target_idx] = 1 # Apply transparency to target locations img = transparency(img=img, mask=mask, alpha=alpha, tau=tau) stim["img"] = img stim["target_mask"] = mask.astype(int) stim["target_shape"] = target_shape stim["alpha"] = alpha stim["tau"] = tau stim["round_phase_width"] = round_phase_width return stim
def overview(**kwargs): """Generate example stimuli from this module Returns ------- stims : dict dict with all stimuli containing individual stimulus dicts. """ default_params = {"visual_size": (10, 10), "ppd": 30} default_params.update(kwargs) # fmt: off stimuli = { "checkerboard": checkerboard(**default_params, check_visual_size=(1, 1)), "checkerboard_from_frequency": checkerboard(**default_params, frequency=1, rotation=45), "checkerboard_with_targets": checkerboard(**default_params, check_visual_size=(1, 1), target_indices=[(3, 2), (5, 5)]), "checkerboard_contrast_contrast": contrast_contrast(**default_params, check_visual_size=(1, 1), target_shape=4, alpha=0.2), } # fmt: on return stimuli if __name__ == "__main__": from stimupy.utils import plot_stimuli stims = overview() plot_stimuli(stims, mask=False, save=None)