Source code for stimupy.stimuli.benarys

import warnings

import numpy as np

from stimupy.components.shapes import cross, rectangle, triangle
from stimupy.utils import resolution

__all__ = [
    "cross_generalized",
    "cross_rectangles",
    "cross_triangles",
    "todorovic_generalized",
    "todorovic_rectangles",
    "todorovic_triangles",
]


[docs]def cross_generalized( visual_size=None, ppd=None, shape=None, cross_thickness=None, target_size=None, target_type="r", target_rotation=0.0, target_x=None, target_y=None, intensity_background=1.0, intensity_cross=0.0, intensity_target=0.5, ): """Benary's Cross Illusion Parameters ---------- visual_size : Sequence[Number, Number], Number, or None (default) visual size [height, width] of grating, 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 grating, in pixels cross_thickness : float width of the cross bars in degrees visual angle target_size : (float, float) size of all target(s) in degrees visual angle target_type : tuple of strings type of targets to use; option: r (rectangle), t (triangle); as many targets as types target_rotation : tuple of floats, or float tuple with rotation of targets in deg, counterclockwise, as many targets as rotations target_x : tuple of floats tuple with x coordinates of targets in degrees, as many targets as coordinates target_y : tuple of floats tuple with y coordinates of targets in degrees, as many targets as coordinates intensity_background : float intensity value for background intensity_cross : float intensity value for cross intensity_target : float intensity value for target 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 ---------- Benary, W. (1924). Beobachtungen zu einem Experiment über Helligkeitskontrast. Psychologische Forschung, 5, 131-142. https://doi.org/10.1007/BF00402398 """ if cross_thickness is None: raise ValueError( "cross_generalized() missing argument 'cross_thickness' which is not 'None'" ) # Create cross cross_stim = cross( visual_size=visual_size, ppd=ppd, shape=shape, cross_size=visual_size, cross_arm_ratios=1.0, cross_thickness=cross_thickness, intensity_background=intensity_background, intensity_cross=intensity_cross, ) # Add targets stim = add_targets( cross_stim["img"], np.unique(ppd), target_size, target_type, target_rotation, target_x, target_y, intensity_target, ) del cross_stim["img"] return {**stim, **cross_stim}
[docs]def cross_rectangles( visual_size=None, ppd=None, shape=None, cross_thickness=None, target_size=None, intensity_background=1.0, intensity_cross=0.0, intensity_target=0.5, ): """ Benary's Cross stimulus with two rectangular targets with default placement Parameters ---------- visual_size : Sequence[Number, Number], Number, or None (default) visual size [height, width] of grating, 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 grating, in pixels cross_thickness : float width of the cross bars in degrees visual angle target_size : (float, float) size of all target(s) in degrees visual angle intensity_background : float intensity value for background intensity_cross : float intensity value for cross intensity_target : float intensity value for target 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 ---------- Benary, W. (1924). Beobachtungen zu einem Experiment über Helligkeitskontrast. Psychologische Forschung, 5, 131-142. https://doi.org/10.1007/BF00402398 """ if cross_thickness is None: raise ValueError( "cross_rectangles() missing argument 'cross_thickness' which is not 'None'" ) # Resolve resolution shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) if len(np.unique(ppd)) > 1: raise ValueError("ppd should be equal in x and y direction") if target_size is None: raise ValueError("target_size cannot be None") if isinstance(target_size, (float, int)): target_size = (target_size, target_size) if target_size[0] > cross_thickness: raise ValueError("Target size is larger than cross thickness") # Calculate target placement for classical Benarys cross theight, twidth = resolution.shape_from_visual_size_ppd(target_size, ppd) cheight, cwidth = resolution.shape_from_visual_size_ppd(cross_thickness, ppd) target_x = ( (shape[1] / 2 - cwidth / 2 - twidth) / ppd[1], (shape[1] - twidth) / ppd[1], ) target_y = ( (shape[0] / 2 - cheight / 2 - theight) / ppd[0], (shape[0] / 2 - cheight / 2) / ppd[0], ) stim = cross_generalized( visual_size=visual_size, ppd=ppd, shape=shape, cross_thickness=cross_thickness, target_size=target_size, target_type=("r",) * 2, target_rotation=0.0, target_x=np.round(np.array(target_x) * ppd) / ppd, target_y=np.round(np.array(target_y) * ppd) / ppd, intensity_background=intensity_background, intensity_cross=intensity_cross, intensity_target=intensity_target, ) return stim
[docs]def cross_triangles( visual_size=None, ppd=None, shape=None, cross_thickness=None, target_size=None, intensity_background=1.0, intensity_cross=0.0, intensity_target=0.5, ): """ Benary's Cross stimulus with two triangular targets with default placement Parameters ---------- visual_size : Sequence[Number, Number], Number, or None (default) visual size [height, width] of grating, 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 grating, in pixels cross_thickness : float width of the cross bars in degrees visual angle target_size : float size of adjacent and opposite of target triangle in degrees visual angle intensity_background : float intensity value for background intensity_cross : float intensity value for cross intensity_target : float intensity value for target 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 ---------- Benary, W. (1924). Beobachtungen zu einem Experiment über Helligkeitskontrast. Psychologische Forschung, 5, 131-142. https://doi.org/10.1007/BF00402398 """ if cross_thickness is None: raise ValueError( "cross_triangles() missing argument 'cross_thickness' which is not 'None'" ) # Resolve resolution shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) if len(np.unique(ppd)) > 1: raise ValueError("ppd should be equal in x and y direction") if target_size is None: raise ValueError("target_size cannot be None") if isinstance(target_size, (float, int)): target_size = (target_size, target_size) if target_size[0] != target_size[1]: raise ValueError("target needs to have the same height and width") if np.sqrt(target_size[0] ** 2 * 2.0) / 2.0 > cross_thickness: raise ValueError("Target size is larger than cross thickness") # Calculate target placement for classical Benarys cross theight, twidth = resolution.shape_from_visual_size_ppd(target_size, ppd) cheight, cwidth = resolution.shape_from_visual_size_ppd(cross_thickness, ppd) target_x = ( (shape[1] / 2 - cwidth / 2 - twidth) / ppd[1], (shape[1] / 2 + cwidth / 2) / ppd[1], ) target_y = ( (shape[0] / 2 - cheight / 2 - theight) / ppd[0], (shape[0] / 2 - cheight / 2) / ppd[0], ) stim = cross_generalized( visual_size=visual_size, ppd=ppd, shape=shape, cross_thickness=cross_thickness, target_size=target_size, target_type=("t",) * 2, target_rotation=(90.0, 45.0), target_x=np.round(np.array(target_x) * ppd) / ppd, target_y=np.round(np.array(target_y) * ppd) / ppd, intensity_background=intensity_background, intensity_cross=intensity_cross, intensity_target=intensity_target, ) return stim
[docs]def todorovic_generalized( visual_size=None, ppd=None, shape=None, L_width=None, target_size=None, target_type="r", target_rotation=0.0, target_x=None, target_y=None, intensity_background=1.0, intensity_cross=0.0, intensity_target=0.5, ): """ Todorovic Benary's Cross Illusion Parameters ---------- visual_size : Sequence[Number, Number], Number, or None (default) visual size [height, width] of grating, 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 grating, in pixels L_width : float width of L in degree visual angle target_size : (float, float) size of all target(s) in degrees visual angle target_type : tuple of strings type of targets to use; option: r (rectangle), t (triangle); as many targets as types target_rotation : tuple of floats, or float tuple with rotation of targets in deg, counterclockwise, as many targets as rotations target_x : tuple of floats tuple with x coordinates of targets in degrees, as many targets as coordinates target_y : tuple of floats tuple with y coordinates of targets in degrees, as many targets as coordinates intensity_background : float intensity value for background intensity_cross : float intensity value for cross intensity_target : float intensity value for target 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 ---------- Benary, W. (1924). Beobachtungen zu einem Experiment über Helligkeitskontrast. Psychologische Forschung, 5, 131-142. https://doi.org/10.1007/BF00402398 """ if L_width is None: raise ValueError("todorovic_generalized() missing argument 'L_width' which is not 'None'") # Resolve resolution shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) if len(np.unique(ppd)) > 1: raise ValueError("ppd should be equal in x and y direction") if L_width > visual_size[1] / 2: raise ValueError("L_width cannot be larger than stimulus_width / 2") L_size = (visual_size[0] / 2, visual_size[0] / 2, L_width, visual_size[1] - L_width) top, bottom, left, right = resolution.lengths_from_visual_angles_ppd(L_size, np.unique(ppd)) width, height = left + right, top + bottom # Create stimulus without targets mask = np.zeros(shape).astype(int) mask[:, 0:left] = 1 mask[height - bottom : :, 0 : width - left] = 1 img = np.where(mask, intensity_cross, intensity_background) # Add targets stim = add_targets( img, np.unique(ppd), target_size, target_type, target_rotation, target_x, target_y, intensity_target, ) # Add missing parameter information stim["visual_size"] = visual_size stim["ppd"] = ppd stim["shape"] = shape stim["L_width"] = L_width stim["intensity_background"] = intensity_background stim["intensity_cross"] = intensity_cross stim["L_mask"] = mask return stim
[docs]def todorovic_rectangles( visual_size=None, ppd=None, shape=None, L_width=None, target_size=None, intensity_background=1.0, intensity_cross=0.0, intensity_target=0.5, ): """ Todorovic version of Benary's Cross stimulus with two rectangular targets and default placement Parameters ---------- visual_size : Sequence[Number, Number], Number, or None (default) visual size [height, width] of grating, 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 grating, in pixels L_width : float width of L in degree visual angle target_size : (float, float) size of all target(s) in degrees visual angle intensity_background : float intensity value for background intensity_cross : float intensity value for cross intensity_target : float intensity value for target 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 ---------- Benary, W. (1924). Beobachtungen zu einem Experiment über Helligkeitskontrast. Psychologische Forschung, 5, 131-142. https://doi.org/10.1007/BF00402398 """ if L_width is None: raise ValueError("todorovic_rectangles() missing argument 'L_width' which is not 'None'") # Resolve resolution shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) if len(np.unique(ppd)) > 1: raise ValueError("ppd should be equal in x and y direction") if target_size is None: raise ValueError("target_size cannot be None") if isinstance(target_size, (float, int)): target_size = (target_size, target_size) # Calculate target placement for classical Todorovic-Benary target_x = (L_width, visual_size[1] - L_width - target_size[1]) target_y = (visual_size[0] / 2.0 - target_size[0], visual_size[0] / 2.0) stim = todorovic_generalized( visual_size=visual_size, ppd=ppd, L_width=L_width, target_size=target_size, target_type=("r",) * 2, target_rotation=0.0, target_x=np.round(np.array(target_x) * ppd) / ppd, target_y=np.round(np.array(target_y) * ppd) / ppd, intensity_background=intensity_background, intensity_cross=intensity_cross, intensity_target=intensity_target, ) return stim
[docs]def todorovic_triangles( visual_size=None, ppd=None, shape=None, L_width=None, target_size=None, intensity_background=1.0, intensity_cross=0.0, intensity_target=0.5, ): """ Todorovic version of Benary's Cross stimulus with two triangular targets and default placement Parameters ---------- visual_size : Sequence[Number, Number], Number, or None (default) visual size [height, width] of grating, 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 grating, in pixels L_width : float width of L in degree visual angle target_size : float size of adjacent and opposite of target triangle in degrees visual angle intensity_background : float intensity value for background intensity_cross : float intensity value for cross intensity_target : float intensity value for target 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 ---------- Benary, W. (1924). Beobachtungen zu einem Experiment über Helligkeitskontrast. Psychologische Forschung, 5, 131-142. https://doi.org/10.1007/BF00402398 """ if L_width is None: raise ValueError("todorovic_triangles() missing argument 'L_width' which is not 'None'") # Resolve resolution shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) if len(np.unique(ppd)) > 1: raise ValueError("ppd should be equal in x and y direction") if target_size is None: raise ValueError("target_size cannot be None") if isinstance(target_size, (float, int)): target_size = (target_size, target_size) else: raise ValueError("target_size should be a single float") # Calculate target placement for classical Todorovic-Benary target_x = (L_width, visual_size[1] - L_width - target_size[1]) target_y = (visual_size[0] / 2.0 - target_size[0], visual_size[0] / 2.0) stim = todorovic_generalized( visual_size=visual_size, ppd=ppd, L_width=L_width, target_size=target_size, target_type=("t",) * 2, target_rotation=(0.0, 180.0), target_x=np.round(np.array(target_x) * ppd) / ppd, target_y=np.round(np.array(target_y) * ppd) / ppd, intensity_background=intensity_background, intensity_cross=intensity_cross, intensity_target=intensity_target, ) return stim
def add_targets( img, ppd, target_size, target_type, target_rotation, target_x, target_y, intensity_target, ): """Add targets to Benary-like stimulus Parameters ---------- img : 2d numpy array image to which targets will be added ppd : int pixels per degree (visual angle) target_size : (float, float) size of all target(s) in degrees visual angle target_type : tuple of strings type of targets to use; option: r (rectangle), t (triangle); as many targets as types target_rotation : tuple of floats, or float tuple with rotation of targets in deg, counterclockwise, as many targets as rotations target_x : tuple of floats tuple with x coordinates of targets in degrees, as many targets as coordinates target_y : tuple of floats tuple with y coordinates of targets in degrees, as many targets as coordinates intensity_target : float intensity value for target 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 """ mask = np.zeros(img.shape) # If any target information is missing, dont add targets if ( (target_size is None) or (target_type is None) or (target_rotation is None) or (target_x is None) or (target_y is None) ): warnings.warn("Target information is missing - no target added") stim = { "target_size": None, "target_type": None, "target_rotation": None, "target_x": None, "target_y": None, "intensity_target": None, } # Re-format all target information and add targets else: if isinstance(target_size, (float, int)): target_size = (target_size, target_size) if isinstance(target_type, (str)): target_type = [target_type] if isinstance(target_rotation, (float, int)): target_rotation = [target_rotation] if isinstance(target_x, (float, int)): target_x = [target_x] if isinstance(target_y, (float, int)): target_y = [target_y] if np.min(target_x) < 0: raise ValueError("Leftmost target does not fit into image") if np.min(target_y) < 0: raise ValueError("Topmost target does not fit into image") n_targets = np.max( np.array([len(target_type), len(target_rotation), len(target_x), len(target_y)]) ) if len(target_type) == 1: target_type = target_type * n_targets if len(target_rotation) == 1: target_rotation = target_rotation * n_targets if len(target_x) == 1: target_x = target_x * n_targets if len(target_y) == 1: target_y = target_y * n_targets if any(len(lst) != n_targets for lst in [target_rotation, target_x, target_y]): raise Exception( "target_type, target_rotation, target_x and target_y need the same length." ) theight, twidth = resolution.lengths_from_visual_angles_ppd(target_size, ppd) ty = resolution.lengths_from_visual_angles_ppd(target_y, ppd) tx = resolution.lengths_from_visual_angles_ppd(target_x, ppd) if isinstance(ty, (int, float)): ty = (ty,) tx = (tx,) if (twidth + np.array(tx)).max() > img.shape[1]: raise ValueError("Rightmost target does not fit in image.") if (theight + np.array(ty)).max() > img.shape[0]: raise ValueError("Lowest target does not fit in image.") # Add targets: for i in range(len(target_x)): if target_type[i] == "r": target = rectangle( shape=[theight * 2, twidth * 2], ppd=ppd, rectangle_size=(theight / ppd, twidth / ppd), intensity_rectangle=intensity_target, intensity_background=0, rotation=target_rotation[i], ) mpatch = target["rectangle_mask"][~np.all(target["rectangle_mask"] == 0, axis=1)] tpatch = target["img"][~np.all(target["rectangle_mask"] == 0, axis=1)] elif target_type[i] == "t": target = triangle( shape=[theight * 3, twidth * 3], ppd=ppd, triangle_size=(theight / ppd, twidth / ppd), intensity_background=0, intensity_triangle=intensity_target, rotation=target_rotation[i], include_corners=True, ) mpatch = target["triangle_mask"][~np.all(target["triangle_mask"] == 0, axis=1)] tpatch = target["img"][~np.all(target["triangle_mask"] == 0, axis=1)] else: raise Exception("You can only use r or t as shapes") # Remove zero-rows and -columns tpatch = tpatch[:, ~np.all(mpatch == 0, axis=0)] mpatch = mpatch[:, ~np.all(mpatch == 0, axis=0)] theight_, twidth_ = tpatch.shape if ty[i] + theight_ > img.shape[0] or tx[i] + twidth_ > img.shape[1]: raise ValueError("At least one target does not fully fit into stimulus") # Only change the target parts of the image: mlarge = np.zeros(img.shape) mlarge[ty[i] : ty[i] + theight_, tx[i] : tx[i] + twidth_] = mpatch tlarge = np.zeros(img.shape) tlarge[ty[i] : ty[i] + theight_, tx[i] : tx[i] + twidth_] = tpatch img = np.where(mlarge, tlarge, img) mask = np.where(mlarge, i + 1, mask) stim = { "target_size": target_size, "target_type": target_type, "target_rotation": target_rotation, "target_x": target_x, "target_y": target_y, "intensity_target": intensity_target, } stim["img"] = img stim["target_mask"] = mask.astype(int) 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, "ppd": 30, } default_params.update(kwargs) params_benary = { "target_size": 1, "cross_thickness": 2, } params_todo = { "target_size": 1, "L_width": 2, } target_pos = { "target_x": (3, 6, 3, 6), "target_y": (4, 6, 6, 4), } # fmt: off stimuli = { "benarys_cross_general": cross_generalized(**default_params, **params_benary, **target_pos), "benarys_cross_rectangles": cross_rectangles(**default_params, **params_benary), "benarys_cross_triangles": cross_triangles(**default_params, **params_benary), "benarys_todorovic_general": todorovic_generalized(**default_params, **params_todo, **target_pos), "benarys_todorovic_rectangles": todorovic_rectangles(**default_params, **params_todo), "benarys_todorovic_triangles": todorovic_triangles(**default_params, **params_todo), } # fmt: on return stimuli if __name__ == "__main__": from stimupy.utils import plot_stimuli stims = overview() plot_stimuli(stims, mask=False, save=None)