import warnings
import numpy as np
from stimupy.components.gaussians import gaussian
from stimupy.components.shapes import parallelogram, rectangle
from stimupy.stimuli.waves import sine_linear as sinewave
from stimupy.stimuli.waves import square_linear as squarewave
from stimupy.utils import pad_dict_to_shape, pad_dict_to_visual_size, resolution, strip_dict
from stimupy.utils.filters import convolve
__all__ = [
"sinewave",
"squarewave",
"on_uniform",
"on_grating",
"on_grating_masked",
"phase_shifted",
"grating_induction",
"grating_induction_blur",
]
[docs]def on_grating_masked(
small_grating_params,
large_grating_params,
mask_size=None,
mask_rotation=None,
):
"""Small grating, with a parallelogram-like shape, on a larger grating
Parameters
----------
small_grating_params : dict
kwargs to generate small grating
large_grating_params : dict
kwargs to generate larger grating
mask_size : Sequence[Number, Number, Number], Sequence[Number, Number], Number or None (default)
size (height, width, depth) of parallelogram-like mask in degrees visual angle
mask_rotation: float
rotation of the mask in degree
Returns
-------
dict[str, Any]
dict with the stimulus (key: "img"),
mask with integer index for each bar (key: "target_mask"),
and additional keys containing stimulus parameters
References
----------
White, M. (1981).
The effect of the nature of the surround on the perceived lightness
of grey bars within square-wave test grating.
Perception, 10, 215-230.
https://doi.org/10.1068/p100215
"""
# Create gratings
small_grating = squarewave(**small_grating_params)
large_grating = squarewave(**large_grating_params)
if small_grating["ppd"] != large_grating["ppd"]:
raise ValueError("Gratings must have same ppd")
if mask_size is None:
mask_size = small_grating["visual_size"]
if mask_rotation is None:
mask_rotation = 0
window = parallelogram(
ppd=small_grating["ppd"],
shape=large_grating["shape"],
parallelogram_size=mask_size,
rotation=mask_rotation,
)["img"]
small_grating = pad_dict_to_shape(small_grating, large_grating["shape"])
window = window * (small_grating["pad_mask"] - 1)
img = np.where(window, small_grating["img"], large_grating["img"])
mask = np.where(window, small_grating["target_mask"], 0)
stim = {
"img": img,
"visual_size": large_grating["visual_size"],
"ppd": large_grating["ppd"],
"target_mask": mask.astype(int),
"small_grating_mask": small_grating["grating_mask"],
"large_grating_mask": large_grating["grating_mask"],
"small_grating_params": strip_dict(small_grating, squarewave),
"large_grating_params": strip_dict(large_grating, squarewave),
"mask_size": mask_size,
"mask_rotation": mask_rotation,
}
return stim
[docs]def on_grating(
small_grating_params,
large_grating_params,
):
"""Small grating on a larger grating
Parameters
----------
small_grating_params : dict
kwargs to generate small grating
large_grating_params : dict
kwargs to generate larger grating
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
----------
White, M. (1981).
The effect of the nature of the surround on the perceived lightness
of grey bars within square-wave test grating.
Perception, 10, 215-230.
https://doi.org/10.1068/p100215
"""
stim = on_grating_masked(
small_grating_params,
large_grating_params,
)
return stim
def phase_shifted(
visual_size=None,
ppd=None,
shape=None,
frequency=None,
n_bars=None,
bar_width=None,
period="ignore",
distance_metric="horizontal",
phase_shift=0,
intensity_bars=(0.0, 1.0),
target_size=None,
target_phase_shift=0,
intensity_target=0.5,
origin="corner",
):
if target_size is None:
raise ValueError(
"counterphase_induction() missing argument 'target_size' which is not 'None'"
)
if distance_metric == "horizontal":
rotation = 0
elif distance_metric == "vertical":
rotation = 90
else:
raise ValueError("distance_metric must be horizontal or vertical")
# Spatial square-wave grating
stim = squarewave(
visual_size=visual_size,
ppd=ppd,
shape=shape,
frequency=frequency,
n_bars=n_bars,
bar_width=bar_width,
rotation=rotation,
phase_shift=phase_shift,
period=period,
intensity_bars=intensity_bars,
origin=origin,
round_phase_width=True,
)
stim_target = squarewave(
visual_size=target_size,
ppd=stim["ppd"],
bar_width=stim["bar_width"],
rotation=rotation,
phase_shift=0,
period=period,
intensity_bars=(intensity_target, 0),
origin=origin,
round_phase_width=True,
)
stim_target = pad_dict_to_shape(stim_target, stim["shape"], 0)
cycle_px = stim_target["bar_width"] * stim_target["ppd"][0] * 2
# Translate phase information into pixels
target_phasea = np.abs(target_phase_shift)
target_phasea = target_phasea % 360
target_amount = target_phasea / 360.0
target_shift = target_amount * cycle_px
target_shifti = int(np.round(target_shift))
target_phasei = target_shifti / cycle_px * 360
if target_shift != int(target_shift):
s = np.sign(target_phase_shift)
warnings.warn(f"Rounding phase; {target_phase_shift} -> {s * target_phasei}")
# Shift targets by specified phase
cy, cx = stim["shape"]
if target_phase_shift < 0:
if distance_metric == "horizontal":
stim_target["img"][:, 0 : cx - target_shifti] = stim_target["img"][:, target_shifti::]
stim_target["grating_mask"][:, 0 : cx - target_shifti] = stim_target["grating_mask"][
:, target_shifti::
]
elif distance_metric == "vertical":
stim_target["img"][0 : cx - target_shifti, :] = stim_target["img"][target_shifti::, :]
stim_target["grating_mask"][0 : cx - target_shifti, :] = stim_target["grating_mask"][
target_shifti::, :
]
else:
if distance_metric == "horizontal":
stim_target["img"][:, target_shifti::] = stim_target["img"][:, 0 : cx - target_shifti]
stim_target["grating_mask"][:, target_shifti::] = stim_target["grating_mask"][
:, 0 : cx - target_shifti
]
elif distance_metric == "vertical":
stim_target["img"][target_shifti::, :] = stim_target["img"][0 : cx - target_shifti, :]
stim_target["grating_mask"][target_shifti::, :] = stim_target["grating_mask"][
0 : cx - target_shifti, :
]
# Add targets on grating
mask_temp = np.ones(stim["shape"])
mask_temp[stim_target["img"] == intensity_target] = 0
img = stim["img"] * mask_temp + stim_target["img"]
# Create target mask
mask = np.where(stim_target["img"] == intensity_target, stim_target["grating_mask"], 0)
unique_vals = np.unique(mask)
for v in range(len(unique_vals)):
mask[mask == unique_vals[v]] = v
stim["img"] = img
stim["target_mask"] = mask.astype(int)
stim["target_phase_shift"] = target_phasei
stim["target_size"] = stim_target["visual_size"]
stim["intensity_target"] = intensity_target
return stim
[docs]def grating_induction(
visual_size=None,
ppd=None,
shape=None,
frequency=None,
n_bars=None,
bar_width=None,
period="ignore",
rotation=0.0,
phase_shift=0,
intensities=(0.0, 1.0),
target_width=None,
intensity_target=0.5,
origin="corner",
):
"""Grating induction illusion using a sinewave grating
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
frequency : Number, or None (default)
spatial frequency of grating, in cycles per degree visual angle
n_bars : int, or None (default)
number of bars in the grating
bar_width : Number, or None (default)
width of a single bar, in degrees visual angle
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)
phase_shift : float
phase shift of grating in degrees
intensity_bars : Sequence[float, ...]
intensity value for each bar, by default (1.0, 0.0).
Can specify as many intensities as n_bars;
If fewer intensities are passed than n_bars, cycles through intensities
target_width : float
width of target stripe in degrees visual angle
intensities : Sequence[float, float] or None (default)
min and max intensity of sinewave
origin : "corner", "mean" or "center"
if "corner": set origin to upper left corner (default)
if "mean": set origin to hypothetical image center
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 target (key: "target_mask"),
and additional keys containing stimulus parameters
References
----------
McCourt, M. E. (1982).
A spatial frequency dependent grating-induction effect.
Vision Research, 22, 119-134.
https://doi.org/10.1016/0042-6989(82)90173-0
"""
if target_width is None:
raise ValueError("induction() missing argument 'target_width' which is not 'None'")
# Draw grating
stim = sinewave(
shape=shape,
visual_size=visual_size,
ppd=ppd,
frequency=frequency,
n_bars=n_bars,
bar_width=bar_width,
period=period,
rotation=rotation,
phase_shift=phase_shift,
intensities=intensities,
origin=origin,
)
# Identify target region
rectangle_size = (target_width, stim["visual_size"].width)
target_mask = rectangle(
rectangle_size=rectangle_size,
ppd=stim["ppd"],
visual_size=stim["visual_size"],
intensity_background=0,
intensity_rectangle=1,
)
# Superimpose
stim["img"] = np.where(target_mask["rectangle_mask"], intensity_target, stim["img"])
stim["target_mask"] = np.where(target_mask["rectangle_mask"], stim["grating_mask"], 0)
return stim
[docs]def grating_induction_blur(
visual_size=None,
ppd=None,
shape=None,
frequency=None,
n_bars=None,
bar_width=None,
period="ignore",
rotation=0.0,
phase_shift=0,
intensity_bars=(0.0, 1.0),
target_width=None,
sigma=None,
intensity_target=0.5,
origin="corner",
):
"""Grating induction illusion using a blurred square-wave grating
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
frequency : Number, or None (default)
spatial frequency of grating, in cycles per degree visual angle
n_bars : int, or None (default)
number of bars in the grating
bar_width : Number, or None (default)
width of a single bar, in degrees visual angle
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)
phase_shift : float
phase shift of grating in degrees
intensity_bars : Sequence[float, ...]
intensity value for each bar, by default (1.0, 0.0).
Can specify as many intensities as n_bars;
If fewer intensities are passed than n_bars, cycles through intensities
target_width : float
width of target stripe in degrees visual angle
target_blur : float
amount of Gaussian blur to blur square-wave grating (default: 0)
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 (default)
if "mean": set origin to hypothetical image center
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 target (key: "target_mask"),
and additional keys containing stimulus parameters
References
----------
McCourt, M. E. (1982).
A spatial frequency dependent grating-induction effect.
Vision Research, 22, 119-134.
https://doi.org/10.1016/0042-6989(82)90173-0
"""
if target_width is None:
raise ValueError("induction_blur() missing argument 'target_width' which is not 'None'")
# Draw grating
stim = squarewave(
shape=shape,
visual_size=visual_size,
ppd=ppd,
frequency=frequency,
n_bars=n_bars,
bar_width=bar_width,
period=period,
rotation=rotation,
phase_shift=phase_shift,
intensity_bars=intensity_bars,
origin=origin,
round_phase_width=True,
)
gauss = gaussian(
visual_size=stim["visual_size"],
ppd=stim["ppd"],
shape=stim["shape"],
sigma=sigma,
origin="center",
)["img"]
stim["img"] = convolve(stim["img"], gauss / np.sum(gauss), "same", padding=True)
# Identify target region
rectangle_size = (target_width, stim["visual_size"].width)
target_mask = rectangle(
rectangle_size=rectangle_size,
ppd=stim["ppd"],
visual_size=stim["visual_size"],
intensity_background=0,
intensity_rectangle=1,
)
# Superimpose
stim["img"] = np.where(target_mask["rectangle_mask"], intensity_target, stim["img"])
stim["target_mask"] = np.where(target_mask["rectangle_mask"], stim["grating_mask"], 0)
return stim
def overview(**kwargs):
"""Generate example stimuli from this module
Returns
-------
stims : dict
dict with all stimuli containing individual stimulus dicts.
"""
params = {
"ppd": 40,
"n_bars": 8,
"bar_width": 1.0,
}
small_grating = {
"ppd": 40,
"bar_width": 1.0,
"n_bars": 7,
"intensity_bars": (0.2, 0.8),
"target_indices": (0, 2, 4, 6),
"rotation": 90,
}
large_grating = {
"ppd": 40,
"bar_width": 1.0,
"n_bars": 21,
}
# fmt: off
stimuli = {
"grating_on_uniform": on_uniform(**params, visual_size=20, grating_size=5, target_indices=2),
"grating_on_grating": on_grating(large_grating_params=large_grating, small_grating_params=small_grating),
"grating_on_grating-masked": on_grating_masked(
large_grating_params=large_grating,
small_grating_params=small_grating,
mask_size=(5, 5, 2),
),
"grating_phase_shifted": phase_shifted(
**params, target_size=4, target_phase_shift=90
),
"grating_induction": grating_induction(**params, target_width=0.5),
"grating_induction_blurred-squarewave": grating_induction_blur(**params, target_width=0.5, sigma=0.1),
}
# fmt: on
return stimuli
if __name__ == "__main__":
from stimupy.utils import plot_stimuli
stims = overview()
plot_stimuli(stims, mask=False, save=None)