Source code for stimupy.stimuli.todorovics

import itertools

import numpy as np

from stimupy.components.shapes import cross as cross_shape
from stimupy.components.shapes import rectangle as rectangle_shape
from stimupy.utils import make_two_sided, pad_dict_to_shape, resolution

__all__ = [
    "rectangle_generalized",
    "rectangle",
    "rectangle_two_sided",
    "cross_generalized",
    "cross",
    "cross_two_sided",
    "equal",
    "equal_two_sided",
]


[docs]def rectangle_generalized( visual_size=None, ppd=None, shape=None, target_size=None, target_position=None, covers_size=None, covers_x=None, covers_y=None, intensity_background=0.0, intensity_target=0.5, intensity_covers=1.0, ): """Rectangular target and rectangular covers added with flexible number of covers and flexible target and cover 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 target_size : float or (float, float) size of the target in degrees of visual angle (height, width) target_position : float or (float, float) coordinates where to place the target covers_size : float or (float, float) size of the covers in degrees of visual angle (height, width) covers_x : tuple of floats x coordinates of covers; as many covers as there are coordinates covers_y : tuple of floats y coordinates of covers; as many covers as there are coordinates intensity_background : float intensity value for background intensity_target : float intensity value for target intensity_covers : Sequence[Number, ...] or Number intensity value for covers Returns ------- dict[str, Any] dict with the stimulus (key: "img"), mask with integer index for the target (key: "target_mask"), and additional keys containing stimulus parameters References ---------- Blakeslee, B., & McCourt, M. E. (1999). A multiscale spatial filtering account of the White effect, simultaneous brightness contrast and grating induction. Vision Research, 39, 4361-4377. Pessoa, L., Baratoff, G., Neumann, H., & Todorovic, D. (1998). Lightness and junctions: variations on White's display. Investigative Ophthalmology and Visual Science (Supplement), 39, S159. Todorovic, D. (1997). Lightness and junctions. Perception, 26, 379-395. """ if target_size is None: raise ValueError( "rectangle_generalized() missing argument 'target_size' which is not 'None'" ) if covers_size is None: raise ValueError( "rectangle_generalized() missing argument 'covers_size' which is not 'None'" ) if covers_x is None: raise ValueError("rectangle_generalized() missing argument 'covers_x' which is not 'None'") if covers_y is None: raise ValueError("rectangle_generalized() missing argument 'covers_y' 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 isinstance(covers_size, (float, int)): covers_size = (covers_size, covers_size) if len(covers_x) != len(covers_y): raise ValueError("Need as many x- as y-coordinates") # Create image with square stim = rectangle_shape( visual_size=visual_size, ppd=ppd, rectangle_size=target_size, rectangle_position=target_position, intensity_background=intensity_background, intensity_rectangle=intensity_target, ) img = stim["img"] mask = stim["rectangle_mask"] # Add covers cheight, cwidth = resolution.lengths_from_visual_angles_ppd( covers_size, np.unique(ppd), round=False ) cx = resolution.lengths_from_visual_angles_ppd(covers_x, np.unique(ppd), round=False) cy = resolution.lengths_from_visual_angles_ppd(covers_y, np.unique(ppd), round=False) cheight = int(np.round(cheight)) cwidth = int(np.round(cwidth)) cx = np.round(cx).astype(int) cy = np.round(cy).astype(int) if isinstance(intensity_covers, (float, int)): int_cov = [ intensity_covers, ] else: int_cov = list(intensity_covers) int_cov = itertools.cycle(int_cov) for i in range(len(covers_x)): img[cy[i] : cy[i] + cheight, cx[i] : cx[i] + cwidth] = next(int_cov) mask[cy[i] : cy[i] + cheight, cx[i] : cx[i] + cwidth] = 0 if cy[i] + cheight > shape[0] or cx[i] + cwidth > shape[1]: raise ValueError("Covers do not fully fit into stimulus") stim["img"] = img stim["target_mask"] = mask.astype(int) stim["visual_size"] = visual_size stim["ppd"] = ppd stim["shape"] = shape stim["target_size"] = stim["rectangle_size"] stim["target_position"] = stim["rectangle_position"] stim["intensity_target"] = stim["intensity_rectangle"] stim["covers_size"] = covers_size stim["covers_x"] = covers_x stim["covers_y"] = covers_y stim["intensity_covers"] = intensity_covers del stim["rectangle_size"] del stim["rectangle_position"] del stim["intensity_rectangle"] return stim
[docs]def rectangle( visual_size=None, ppd=None, shape=None, target_size=None, target_position=None, covers_size=None, covers_offset=None, intensity_background=0.0, intensity_target=0.5, intensity_covers=1.0, ): """Rectangular target in the center and four rectangular covers added symmetrically around target center 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 target_size : float or (float, float) size of the target in degrees of visual angle (height, width) target_position : float or (float, float) coordinates where to place the target covers_size : float or (float, float) size of covers in degrees of visual angle (height, width) covers_offset : float or (float, float) distance from cover center to target center in (y, x) intensity_background : float intensity value for background intensity_target : float intensity value for target intensity_covers : Sequence[Number, ...] or Number intensity value for covers Returns ------- dict[str, Any] dict with the stimulus (key: "img"), mask with integer index for the target (key: "target_mask"), and additional keys containing stimulus parameters References ---------- Blakeslee, B., & McCourt, M. E. (1999). A multiscale spatial filtering account of the White effect, simultaneous brightness contrast and grating induction. Vision Research, 39, 4361-4377. Pessoa, L., Baratoff, G., Neumann, H., & Todorovic, D. (1998). Lightness and junctions: variations on White's display. Investigative Ophthalmology and Visual Science (Supplement), 39, S159. Todorovic, D. (1997). Lightness and junctions. Perception, 26, 379-395. """ if target_size is None: raise ValueError("rectangle() missing argument 'target_size' which is not 'None'") if covers_size is None: raise ValueError("rectangle() missing argument 'covers_size' which is not 'None'") if covers_offset is None: raise ValueError("rectangle() missing argument 'covers_offset' 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 isinstance(covers_size, (float, int)): covers_size = (covers_size, covers_size) if isinstance(covers_offset, (float, int)): covers_offset = (covers_offset, covers_offset) # Calculate placement of covers for generalized function: cy, cx = np.array(visual_size) / 2 y1 = cy - covers_offset[0] - covers_size[0] / 2 x1 = cx - covers_offset[1] - covers_size[1] / 2 y2 = cy + covers_offset[0] - covers_size[0] / 2 x2 = cx + covers_offset[1] - covers_size[1] / 2 stim = rectangle_generalized( visual_size=visual_size, ppd=ppd, target_size=target_size, target_position=target_position, covers_size=covers_size, covers_x=(x1, x2, x2, x1), covers_y=(y1, y2, y1, y2), intensity_background=intensity_background, intensity_target=intensity_target, intensity_covers=intensity_covers, ) return stim
rectangle_two_sided = make_two_sided( rectangle, two_sided_params=( "target_size", "covers_size", "covers_offset", "intensity_background", "intensity_target", "intensity_covers", ), )
[docs]def cross_generalized( visual_size=None, ppd=None, shape=None, cross_size=None, cross_thickness=None, covers_size=None, covers_x=None, covers_y=None, intensity_background=0.0, intensity_target=0.5, intensity_covers=1.0, ): """Cross target and rectangular covers added with flexible number of covers and flexible cover 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_size : float or (float, float) size of target cross in visual angle cross_thickness : float thickness of target cross in visual angle covers_size : float or (float, float) size of covers in degrees visual angle (height, width) covers_x : tuple of floats x coordinates of covers; as many covers as there are coordinates covers_y : tuple of floats y coordinates of covers; as many covers as there are coordinates intensity_background : float intensity value for background intensity_target : float intensity value for target intensity_covers : Sequence[Number, ...] or Number intensity value for covers Returns ------- dict[str, Any] dict with the stimulus (key: "img"), mask with integer index for the target (key: "target_mask"), and additional keys containing stimulus parameters References ---------- Blakeslee, B., & McCourt, M. E. (1999). A multiscale spatial filtering account of the White effect, simultaneous brightness contrast and grating induction. Vision Research, 39, 4361-4377. Pessoa, L., Baratoff, G., Neumann, H., & Todorovic, D. (1998). Lightness and junctions: variations on White's display. Investigative Ophthalmology and Visual Science (Supplement), 39, S159. Todorovic, D. (1997). Lightness and junctions. Perception, 26, 379-395. """ if cross_size is None: raise ValueError("cross_generalized() missing argument 'cross_size' which is not 'None'") if cross_thickness is None: raise ValueError( "cross_generalized() missing argument 'cross_thickness' which is not 'None'" ) if covers_size is None: raise ValueError("cross_generalized() missing argument 'covers_size' which is not 'None'") if covers_x is None: raise ValueError("cross_generalized() missing argument 'covers_x' which is not 'None'") if covers_y is None: raise ValueError("cross_generalized() missing argument 'covers_y' which is not 'None'") # Resolve resolution shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) ppd = np.unique(ppd)[0] if not isinstance(ppd, (float, int)): raise ValueError("ppd should be equal in x and y direction") if isinstance(covers_x, (float, int)): covers_x = (covers_x,) if isinstance(covers_y, (float, int)): covers_y = (covers_y,) if len(covers_x) != len(covers_y): raise ValueError("Need as many x- as y-coordinates") if isinstance(covers_size, (float, int)): covers_size = (covers_size, covers_size) if isinstance(cross_size, (float, int)): cross_size = (cross_size, cross_size) if cross_size[0] < cross_thickness or cross_size[1] < cross_thickness: raise ValueError("cross_size needs to be larger than cross_thickness") stim = cross_shape( visual_size=cross_size, ppd=ppd, cross_size=cross_size, cross_arm_ratios=1, cross_thickness=cross_thickness, intensity_background=intensity_background, intensity_cross=intensity_target, ) stim = pad_dict_to_shape(stim, shape=shape, pad_value=intensity_background) img = stim["img"] mask = stim["cross_mask"] cheight, cwidth = resolution.lengths_from_visual_angles_ppd( covers_size, np.unique(ppd), round=False ) cx = resolution.lengths_from_visual_angles_ppd(covers_x, np.unique(ppd), round=False) cy = resolution.lengths_from_visual_angles_ppd(covers_y, np.unique(ppd), round=False) cheight = int(np.round(cheight)) cwidth = int(np.round(cwidth)) cx = np.round(cx).astype(int) cy = np.round(cy).astype(int) if isinstance(intensity_covers, (float, int)): int_cov = [ intensity_covers, ] else: int_cov = list(intensity_covers) int_cov = itertools.cycle(int_cov) for i in range(len(covers_x)): img[cy[i] : cy[i] + cheight, cx[i] : cx[i] + cwidth] = next(int_cov) mask[cy[i] : cy[i] + cheight, cx[i] : cx[i] + cwidth] = 0 if cy[i] + cheight > shape[0] or cx[i] + cwidth > shape[1]: raise ValueError("Covers do not fully fit into stimulus") stim["img"] = img stim["target_mask"] = mask.astype(int) stim["target_size"] = stim["cross_size"] stim["intensity_target"] = intensity_target stim["covers_size"] = covers_size stim["covers_x"] = covers_x stim["covers_y"] = covers_y stim["intensity_covers"] = intensity_covers stim["visual_size"] = visual_size stim["ppd"] = ppd stim["shape"] = shape del (stim["cross_size"], stim["intensity_cross"], stim["cross_mask"]) return stim
[docs]def cross( visual_size=None, ppd=None, shape=None, cross_size=None, cross_thickness=None, covers_size=None, intensity_background=0.0, intensity_target=0.5, intensity_covers=1.0, ): """Cross target and four rectangular covers added at inner cross corners 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_size : float or (float, float) size of target cross in visual angle cross_thickness : float thickness of target cross in visual angle covers_size : float or (float, float) size of covers in degrees of visual angle (height, width) intensity_background : float intensity value for background intensity_target : float intensity value for target intensity_covers : Sequence[Number, ...] or Number intensity value for covers Returns ------- dict[str, Any] dict with the stimulus (key: "img"), mask with integer index for the target (key: "target_mask"), and additional keys containing stimulus parameters References ---------- Blakeslee, B., & McCourt, M. E. (1999). A multiscale spatial filtering account of the White effect, simultaneous brightness contrast and grating induction. Vision Research, 39, 4361-4377. Pessoa, L., Baratoff, G., Neumann, H., & Todorovic, D. (1998). Lightness and junctions: variations on White's display. Investigative Ophthalmology and Visual Science (Supplement), 39, S159. Todorovic, D. (1997). Lightness and junctions. Perception, 26, 379-395. """ if cross_size is None: raise ValueError("cross() missing argument 'cross_size' which is not 'None'") if cross_thickness is None: raise ValueError("cross() missing argument 'cross_thickness' which is not 'None'") if covers_size is None: raise ValueError("cross() missing argument 'covers_size' 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 isinstance(covers_size, (float, int)): covers_size = (covers_size, covers_size) # Calculate placement of covers for generalized function: ppd = np.unique(ppd)[0] cy = np.floor(shape[0] / 2) / ppd cx = np.floor(shape[1] / 2) / ppd ct = np.ceil(cross_thickness * ppd) ct = (ct + (ct % 2)) / ppd ct_half = ct / 2 y1 = cy - cross_thickness / 2 - covers_size[0] x1 = cx - cross_thickness / 2 - covers_size[1] y2 = cy + ct_half x2 = cx + ct_half stim = cross_generalized( visual_size=visual_size, ppd=ppd, cross_size=cross_size, cross_thickness=ct, covers_size=covers_size, covers_x=(x1, x2, x2, x1), covers_y=(y1, y2, y1, y2), intensity_background=intensity_background, intensity_target=intensity_target, intensity_covers=intensity_covers, ) return stim
cross_two_sided = make_two_sided( cross, two_sided_params=( "cross_size", "cross_thickness", "covers_size", "intensity_background", "intensity_target", "intensity_covers", ), )
[docs]def equal( visual_size=None, ppd=None, shape=None, cross_size=None, cross_thickness=None, intensity_background=0.0, intensity_target=0.5, intensity_covers=1.0, ): """Cross target and four rectangular covers added at inner cross corners 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_size : float or (float, float) size of target cross in visual angle cross_thickness : float thickness of target cross in visual angle intensity_background : float intensity value for background intensity_target : float intensity value for target intensity_covers : float intensity value for covers Returns ------- dict[str, Any] dict with the stimulus (key: "img"), mask with integer index for the target (key: "target_mask"), and additional keys containing stimulus parameters References ---------- Blakeslee, B., & McCourt, M. E. (1999). A multiscale spatial filtering account of the White effect, simultaneous brightness contrast and grating induction. Vision Research, 39, 4361-4377. Pessoa, L., Baratoff, G., Neumann, H., & Todorovic, D. (1998). Lightness and junctions: variations on White's display. Investigative Ophthalmology and Visual Science (Supplement), 39, S159. Todorovic, D. (1997). Lightness and junctions. Perception, 26, 379-395. """ if cross_size is None: raise ValueError("equal() missing argument 'cross_size' which is not 'None'") if cross_thickness is None: raise ValueError("equal() missing argument 'cross_thickness' which is not 'None'") if isinstance(cross_size, (float, int)): cross_size = (cross_size, cross_size) # 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") # Calculate placement of covers for generalized function: ppd = np.unique(ppd)[0] c1 = (np.ceil(cross_size[0] / 2 * ppd - cross_thickness / 2 * ppd + 1)) / ppd c2 = (np.ceil(cross_size[1] / 2 * ppd - cross_thickness / 2 * ppd + 1)) / ppd covers_size = (c1, c2) # covers_size = ((cross_size[0] - cross_thickness) / 2, (cross_size[1] - cross_thickness) / 2) stim = cross( visual_size=visual_size, ppd=ppd, shape=shape, cross_size=cross_size, cross_thickness=cross_thickness, covers_size=covers_size, intensity_background=intensity_background, intensity_target=intensity_target, intensity_covers=intensity_covers, ) window = rectangle_shape( visual_size=visual_size, ppd=ppd, shape=shape, rectangle_size=cross_size, ) stim["img"] = np.where(window["rectangle_mask"], stim["img"], intensity_background) stim["target_mask"] = np.where(window["rectangle_mask"], stim["target_mask"], 0).astype(int) return stim
equal_two_sided = make_two_sided( equal, two_sided_params=( "cross_size", "cross_thickness", "intensity_background", "intensity_target", "intensity_covers", ), ) 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) rectangle_params = { "target_size": 3, "covers_size": 1.5, } cross_params = { "cross_size": 4, "cross_thickness": 1.5, } # fmt: off stimuli = { "todorovic_rectangle": rectangle(**default_params, **rectangle_params, covers_offset=1.5), "todorovic_rectangle_general": rectangle_generalized(**default_params, **rectangle_params, target_position=3.5, covers_x=(2, 6), covers_y=(2, 6)), "todorovic_rectangle_2sided": rectangle_two_sided(**default_params, **rectangle_params, covers_offset=1, intensity_background=(0.0, 1.0), intensity_covers=(1.0, 0.0)), "todorovic_cross": cross(**default_params, **cross_params, covers_size=2), "todorovic_cross_general": cross_generalized(**default_params, **cross_params, covers_size=2, covers_x=(2, 6), covers_y=(2, 6)), "todorovic_cross_2sided": cross_two_sided(**default_params, **cross_params, covers_size=1, intensity_background=(0.0, 1.0), intensity_covers=(1.0, 0.0)), "todorovic_equal": equal(**default_params, **cross_params,), "todorovic_equal_2sided": equal_two_sided(**default_params, **cross_params, intensity_background=(0.0, 1.0), intensity_covers=(1.0, 0.0)), } # fmt: on return stimuli if __name__ == "__main__": from stimupy.utils import plot_stimuli stims = overview() plot_stimuli(stims, mask=False, save=None)