import copy
import numpy as np
from stimupy.components import lines
from stimupy.utils import make_two_sided, resolution
__all__ = [
"mueller_lyer",
"two_sided",
]
[docs]def mueller_lyer(
visual_size=None,
ppd=None,
shape=None,
outer_lines_length=None,
outer_lines_angle=45,
target_length=None,
line_width=0,
intensity_outer_lines=1.0,
intensity_target=0.5,
intensity_background=0.0,
):
"""Mueller-Lyer's (1896) 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
outer_lines_length : Number
length of outer lines in degrees visual angle
outer_lines_angle : Number (optional)
angle of outer lines in degrees, by default 45. Must be between -180 and 180 degrees.
target_length : Number
length of target line in degrees visual angle
line_width : Number (optional)
line width in degrees visual angle; if 0 (default), line width is 1 px
intensity_outer_lines : Number (optional)
intensity value of outer lines, by default 0.01
intensity_target : Number (optional)
intensity value of target line, by default 0.5
intensity_background : Number (optional)
intensity value of background, by default 0.0
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
----------
Mueller-Lyer, F. (1896).
Zur Lehre von den optischen Taeuschungen.
Ueber Kontrast und Konfluxion.
Zeitschrift fuer Psychologie und Physiologie der Sinnesorgane, IX, 1-16.
"""
if outer_lines_length is None:
raise ValueError(
"mueller_lyer() missing argument 'outer_lines_length' which is not 'None'"
)
if target_length is None:
raise ValueError("mueller_lyer() missing argument 'target_length' which is not 'None'")
# Resolve resolution
shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
if outer_lines_angle > 180:
outer_lines_angle -= 360
if outer_lines_angle < -180 or outer_lines_angle > 180:
raise ValueError("outer_lines_angle should be between -180 and 180 deg")
angle1 = copy.deepcopy(outer_lines_angle) + 90
angle2 = -angle1 - 180
angle4 = copy.deepcopy(outer_lines_angle) - 90
angle3 = -angle4 - 180
target_line = lines.line(
visual_size=visual_size,
ppd=ppd,
shape=shape,
line_position=None,
line_length=target_length,
line_width=line_width,
rotation=90,
intensity_line=intensity_target - intensity_background,
intensity_background=0,
origin="center",
)
oline1 = lines.line(
visual_size=visual_size,
ppd=ppd,
shape=shape,
line_position=(0, -target_length / 2),
line_length=outer_lines_length,
line_width=line_width,
rotation=angle1,
intensity_line=intensity_outer_lines - intensity_background,
intensity_background=0,
origin="center",
)
oline2 = lines.line(
visual_size=visual_size,
ppd=ppd,
shape=shape,
line_position=(0, -target_length / 2),
line_length=outer_lines_length,
line_width=line_width,
rotation=angle2,
intensity_line=intensity_outer_lines - intensity_background,
intensity_background=0,
origin="center",
)
oline3 = lines.line(
visual_size=visual_size,
ppd=ppd,
shape=shape,
line_position=(0, target_length / 2),
line_length=outer_lines_length,
line_width=line_width,
rotation=angle3,
intensity_line=intensity_outer_lines - intensity_background,
intensity_background=0,
origin="center",
)
oline4 = lines.line(
visual_size=visual_size,
ppd=ppd,
shape=shape,
line_position=(0, target_length / 2),
line_length=outer_lines_length,
line_width=line_width,
rotation=angle4,
intensity_line=intensity_outer_lines - intensity_background,
intensity_background=0,
origin="center",
)
# Add outer lines together
olines = oline1["img"] + oline2["img"] + oline3["img"] + oline4["img"]
omasks1 = oline1["line_mask"] * 2 + oline2["line_mask"] * 3
omasks2 = oline3["line_mask"] * 4 + oline4["line_mask"] * 5
target_line["img"] += olines
target_line["img"] = np.where(
target_line["img"] > intensity_outer_lines, intensity_outer_lines, target_line["img"]
)
target_line["line_mask"] += omasks1
target_line["line_mask"] = np.where(target_line["line_mask"] > 3, 3, target_line["line_mask"])
target_line["line_mask"] += omasks2
target_line["line_mask"] = np.where(target_line["line_mask"] > 5, 5, target_line["line_mask"])
target_line["target_mask"] = np.where(target_line["line_mask"] == 1, 1, 0).astype(int)
target_line["outer_lines_length"] = outer_lines_length
target_line["outer_lines_angle"] = outer_lines_angle
target_line["target_length"] = target_length
target_line["line_width"] = line_width
target_line["intensity_outer_lines"] = intensity_outer_lines
target_line["intensity_target"] = intensity_target
target_line["intensity_background"] = intensity_background
return target_line
two_sided = make_two_sided(
mueller_lyer,
two_sided_params=(
"outer_lines_length",
"outer_lines_angle",
"target_length",
"line_width",
"intensity_outer_lines",
"intensity_target",
),
)
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)
stim_params = {
"outer_lines_length": 1,
"target_length": 3,
"line_width": 0.1,
}
# fmt: off
stimuli = {
"mueller_lyer": mueller_lyer(**default_params, **stim_params, outer_lines_angle=45),
"mueller_lyer_2sided": two_sided(**default_params, **stim_params, outer_lines_angle=(30, 135)),
}
# fmt: on
return stimuli
if __name__ == "__main__":
from stimupy.utils import plot_stimuli
stims = overview()
plot_stimuli(stims, mask=False, save=None)