import numpy as np
from stimupy.components.shapes import disc, rectangle
from stimupy.stimuli import bullseyes
from stimupy.utils import make_two_sided, pad_by_visual_size, pad_to_shape, resolution
__all__ = [
"generalized",
"basic",
"square",
"circular",
"with_dots",
"dotted",
"basic_two_sided",
"square_two_sided",
"circular_two_sided",
"with_dots_two_sided",
"dotted_two_sided",
]
[docs]def generalized(
visual_size=None,
ppd=None,
shape=None,
target_size=None,
target_position=None,
intensity_background=0.0,
intensity_target=0.5,
rotation=0.0,
):
"""Simultaneous contrast stimulus with free target placement
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
target_size : float or (float, float)
size [height, width] of the target, in degrees visual angle
target_position : float or (float, float)
position of the target in degree visual angle (height, width);
if None, place target centrally
intensity_background : float, optional
intensity value for background, by default 0.0
intensity_target : float, optional
intensity value for target, by default 0.5
rotation : float, optional
rotation (in degrees), counterclockwise, by default 0.0 (horizontal)
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
----------
Chevreul, M. (1855).
The principle of harmony and contrast of colors.
"""
if target_size is None:
raise ValueError("generalized() missing argument 'target_size' which is not 'None'")
stim = rectangle(
visual_size=visual_size,
ppd=ppd,
shape=shape,
rectangle_size=target_size,
rectangle_position=target_position,
intensity_background=intensity_background,
intensity_rectangle=intensity_target,
rotation=rotation,
)
stim["target_mask"] = stim.pop("rectangle_mask")
stim["target_size"] = stim.pop("rectangle_size")
stim["target_position"] = stim.pop("rectangle_position")
stim["intensity_target"] = stim.pop("intensity_rectangle")
return stim
[docs]def basic(
visual_size=None,
ppd=None,
shape=None,
target_size=None,
intensity_background=0.0,
intensity_target=0.5,
):
"""Simultaneous contrast stimulus with central target
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
target_size : float or (float, float)
size [height, width] of the target, in degrees visual angle
intensity_background : float, optional
intensity value for background, by default 0.0
intensity_target : float, optional
intensity value for target, by default 0.5
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
----------
Chevreul, M. (1855).
The principle of harmony and contrast of colors.
"""
if target_size is None:
raise ValueError("basic() missing argument 'target_size' which is not 'None'")
stim = generalized(
visual_size=visual_size,
ppd=ppd,
shape=shape,
target_size=target_size,
target_position=None,
intensity_background=intensity_background,
intensity_target=intensity_target,
)
return stim
[docs]def square(
target_radius,
visual_size=None,
ppd=None,
shape=None,
surround_radius=None,
rotation=0.0,
intensity_surround=0.0,
intensity_background=0.5,
intensity_target=0.5,
origin="mean",
):
"""Simultaneous contrast stimulus with square target and surround field
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
target_radius : float
radius of target, in degrees visual angle
surround_radius : float
radius of surround context field, in degrees visual angle
rotation : float, optional
rotation (in degrees), counterclockwise, by default 0.0 (horizontal)
intensity_surrond : float, optional
intensity of surround context field, by default 0.0
intensity_background : float, optional
intensity value of background, by default 0.5
intensity_target : float, or Sequence[float, ...], optional
intensity value for each target, by default 0.5.
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 the stimulus (key: "img"),
mask with integer index for each frame (key: "target_mask"),
and additional keys containing stimulus parameters
"""
stim = bullseyes.rectangular_generalized(
radii=(target_radius, surround_radius),
visual_size=visual_size,
ppd=ppd,
shape=shape,
rotation=rotation,
intensity_frames=(0.0, intensity_surround),
intensity_background=intensity_background,
intensity_target=intensity_target,
origin=origin,
)
return stim
[docs]def circular(
target_radius,
visual_size=None,
ppd=None,
shape=None,
surround_radius=None,
intensity_surround=0.0,
intensity_background=0.5,
intensity_target=0.5,
origin="mean",
):
"""Simultaneous contrast stimulus with circular target and surround field
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
target_radius : float
radius of target, in degrees visual angle
surround_radius : float
radius of surround context field, in degrees visual angle
rotation : float, optional
rotation (in degrees), counterclockwise, by default 0.0 (horizontal)
intensity_surrond : float, optional
intensity of surround context field, by default 0.0
intensity_background : float (optional)
intensity value of background, by default 0.5
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
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 the stimulus (key: "img"),
mask with integer index for each frame (key: "target_mask"),
and additional keys containing stimulus parameters
"""
stim = bullseyes.circular_generalized(
radii=(target_radius, surround_radius),
visual_size=visual_size,
ppd=ppd,
shape=shape,
intensity_rings=(0.0, intensity_surround),
intensity_background=intensity_background,
intensity_target=intensity_target,
origin=origin,
)
return stim
[docs]def with_dots(
visual_size=None,
ppd=None,
shape=None,
n_dots=None,
dot_radius=None,
distance=None,
target_shape=None,
intensity_background=0.0,
intensity_dots=1.0,
intensity_target=0.5,
):
"""Simultaneous contrast stimulus with dots
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
n_dots : int or (int, int)
stimulus size defined as the number of dots in y and x-directions
dot_radius : float
radius of dots
distance : float
distance between dots in degree visual angle
target_shape : int or (int, int)
target shape defined as the number of dots that fit into the target
intensity_background : float
intensity value for background
intensity_dots : float
intensity value for dots
intensity_target : float
intensity value for target
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
----------
Bressan, P., & Kramer, P. (2008).
Gating of remote effects on lightness.
Journal of Vision, 8(2), 16-16.
https://doi.org/10.1167/8.2.16
"""
if n_dots is None:
raise ValueError("with_dots() missing argument 'n_dots' which is not 'None'")
if dot_radius is None:
raise ValueError("with_dots() missing argument 'dot_radius' which is not 'None'")
if distance is None:
raise ValueError("with_dots() missing argument 'distance' which is not 'None'")
if target_shape is None:
raise ValueError("with_dots() missing argument 'target_shape' which is not 'None'")
# n_dots = number of dots vertical, horizontal, analogous to degrees
n_dots = resolution.validate_visual_size(n_dots)
if shape is None and visual_size is None:
visual_size = (
n_dots[0] * dot_radius * 2 + n_dots[0] * distance,
n_dots[1] * dot_radius * 2 + n_dots[1] * distance,
)
# 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")
# target shape = is in number of dots
target_shape = resolution.validate_visual_size(target_shape)
# Figure out pixels_per_dot
padding = (distance / 2.0,)
patch = disc(
radius=dot_radius,
ppd=ppd,
intensity_background=0.0,
intensity_disc=1,
)["img"]
patch = pad_by_visual_size(img=patch, padding=padding, ppd=ppd, pad_value=0.0)
pixels_per_dot = patch.shape
patch = np.tile(patch, (int(n_dots[0]), int(n_dots[1])))
# Target shape = target n_dots * pixels_per_dot
rect_shape = resolution.shape_from_visual_size_ppd(
visual_size=target_shape, ppd=pixels_per_dot
)
rect_visual_size = resolution.visual_size_from_shape_ppd(shape=rect_shape, ppd=ppd)
try:
patch = pad_to_shape(patch, shape, 0)
except Exception:
raise ValueError("visual_size or shape_argument are too small. Advice: set to None")
# Create the sbc in the background:
img_shape = patch.shape
sbc = rectangle(
shape=img_shape,
ppd=ppd,
rectangle_size=rect_visual_size,
intensity_background=intensity_background,
intensity_rectangle=intensity_target,
)
img = np.where(patch == 1, intensity_dots, sbc["img"])
mask = np.where(patch == 1, 0, sbc["rectangle_mask"])
stim = {
"img": img,
"target_mask": mask.astype(int),
"shape": shape,
"visual_size": visual_size,
"ppd": ppd,
"n_dots": n_dots,
"dot_radius": dot_radius,
"distance": distance,
"target_shape": target_shape,
"intensity_background": intensity_background,
"intensity_dots": intensity_dots,
"intensity_target": intensity_target,
}
return stim
[docs]def dotted(
visual_size=None,
ppd=None,
shape=None,
n_dots=None,
dot_radius=None,
distance=None,
target_shape=None,
intensity_background=0.0,
intensity_dots=1.0,
intensity_target=0.5,
):
"""Dotted simultaneous contrast
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
n_dots : int or (int, int)
stimulus size defined as the number of dots in y and x-directions
dot_radius : float
radius of dots
distance : float
distance between dots in degree visual angle
target_shape : int or (int, int)
target shape defined as the number of dots that fit into the target
intensity_background : float
intensity value for background
intensity_dots : float
intensity value for dots
intensity_target : float
intensity value for target
Returns
-------
dict[str, Any]
dict with the stimulus (key: "img"),
mask with integer index for the targets (key: "target_mask"),
and additional keys containing stimulus parameters
References
----------
Bressan, P., & Kramer, P. (2008).
Gating of remote effects on lightness.
Journal of Vision, 8(2), 16-16.
https://doi.org/10.1167/8.2.16
"""
if n_dots is None:
raise ValueError("dotted() missing argument 'n_dots' which is not 'None'")
if dot_radius is None:
raise ValueError("dotted() missing argument 'dot_radius' which is not 'None'")
if distance is None:
raise ValueError("dotted() missing argument 'distance' which is not 'None'")
if target_shape is None:
raise ValueError("dotted() missing argument 'target_shape' which is not 'None'")
# n_dots = number of dots vertical, horizontal, analogous to degrees
n_dots = resolution.validate_visual_size(n_dots)
if shape is None and visual_size is None:
visual_size = (
n_dots[0] * dot_radius * 2 + n_dots[0] * distance,
n_dots[1] * dot_radius * 2 + n_dots[1] * distance,
)
# 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")
# target shape = is in number of dots
target_shape = resolution.validate_visual_size(target_shape)
# Figure out pixels_per_dot
padding = (distance / 2.0,)
patch = disc(
radius=dot_radius,
ppd=ppd,
intensity_background=0.0,
intensity_disc=1.0,
)["img"]
patch = pad_by_visual_size(img=patch, padding=padding, ppd=ppd, pad_value=0.0)
pixels_per_dot = patch.shape
patch = np.tile(patch, (int(n_dots[0]), int(n_dots[1])))
# Target shape = target n_dots * pixels_per_dot
rect_shape = resolution.shape_from_visual_size_ppd(
visual_size=target_shape, ppd=pixels_per_dot
)
rect_visual_size = resolution.visual_size_from_shape_ppd(shape=rect_shape, ppd=ppd)
try:
patch = pad_to_shape(patch, shape, 0)
except Exception:
raise ValueError("visual_size or shape_argument are too small. Advice: set to None")
# Create the sbc and img:
img_shape = patch.shape
sbc = rectangle(
shape=img_shape,
ppd=ppd,
rectangle_size=rect_visual_size,
intensity_background=intensity_background,
intensity_rectangle=intensity_target,
)
img = np.where(patch, intensity_dots, intensity_background)
img = np.where(patch + sbc["rectangle_mask"] == 2, intensity_target, img)
mask = np.where(patch + sbc["rectangle_mask"] == 2, 1, 0)
stim = {
"img": img,
"target_mask": mask.astype(int),
"shape": shape,
"visual_size": visual_size,
"ppd": ppd,
"n_dots": n_dots,
"dot_radius": dot_radius,
"distance": distance,
"target_shape": target_shape,
"intensity_background": intensity_background,
"intensity_dots": intensity_dots,
"intensity_target": intensity_target,
}
return stim
generalized_two_sided = make_two_sided(
generalized,
two_sided_params=(
"target_size",
"target_position",
"rotation",
"intensity_target",
"intensity_background",
),
)
basic_two_sided = make_two_sided(
basic, two_sided_params=("target_size", "intensity_target", "intensity_background")
)
square_two_sided = make_two_sided(
square,
two_sided_params=(
"target_radius",
"surround_radius",
"rotation",
"intensity_target",
"intensity_surround",
"intensity_background",
),
)
circular_two_sided = make_two_sided(
circular,
two_sided_params=(
"target_radius",
"surround_radius",
"intensity_target",
"intensity_surround",
"intensity_background",
),
)
with_dots_two_sided = make_two_sided(
with_dots, two_sided_params=("intensity_dots", "intensity_background", "intensity_target")
)
dotted_two_sided = make_two_sided(
dotted, two_sided_params=("intensity_target", "intensity_background", "intensity_dots")
)
def overview(**kwargs):
"""Generate example stimuli from this module
Returns
-------
stims : dict
dict with all stimuli containing individual stimulus dicts.
"""
default_params = {
"ppd": 30,
}
default_params.update(kwargs)
dot_params = {
"n_dots": 5,
"dot_radius": 2,
"distance": 0.05,
"target_shape": 3,
}
# fmt: off
stimuli = {
"sbc_generalized": generalized(**default_params, visual_size=10, target_size=(3, 4), target_position=(1, 2)),
"sbc_basic": basic(**default_params, visual_size=10, target_size=3),
"sbc_square": square(**default_params, visual_size=10, target_radius=1, surround_radius=2),
"sbc_circular": circular(**default_params, visual_size=10, target_radius=1, surround_radius=2),
"sbc_with_dots": with_dots(**default_params, **dot_params),
"sbc_dotted": dotted(**default_params, **dot_params),
"sbc_2sided": basic_two_sided(**default_params, visual_size=10, target_size=2, intensity_background=(0.0, 1.0)),
"sbc_square_2sided": square_two_sided(**default_params, visual_size=10, target_radius=1, surround_radius=2, intensity_surround=(1.0, 0.0)),
"sbc_circular_2sided": circular_two_sided(**default_params, visual_size=10, target_radius=1, surround_radius=2, intensity_surround=(1.0, 0.0)),
"sbc_with_dots_2sided": with_dots_two_sided(**default_params, **dot_params, intensity_background=(0.0, 1.0), intensity_dots=(1.0, 0.0)),
"sbc_dotted_2sided": dotted_two_sided(**default_params, **dot_params, intensity_background=(0.0, 1.0), intensity_dots=(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)