From eca182a771f670742c036475ef7a750b10ce1475 Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Tue, 21 Mar 2023 21:43:46 +0100 Subject: [PATCH] Split `circulars` into `bullseyes`, `rings` --- stimupy/__init__.py | 4 + .../{illusions/circulars.py => bullseyes.py} | 166 ++++-------------- stimupy/illusions/__init__.py | 7 +- stimupy/rings.py | 153 ++++++++++++++++ 4 files changed, 190 insertions(+), 140 deletions(-) rename stimupy/{illusions/circulars.py => bullseyes.py} (62%) create mode 100644 stimupy/rings.py diff --git a/stimupy/__init__.py b/stimupy/__init__.py index fdec45d6..764a9001 100644 --- a/stimupy/__init__.py +++ b/stimupy/__init__.py @@ -2,6 +2,7 @@ from . import ( benarys, + bullseyes, checkerboards, components, cornsweets, @@ -17,6 +18,7 @@ noises, plaids, ponzos, + rings, sbcs, todorovics, utils, @@ -27,6 +29,7 @@ __stimuli__ = [ "benarys", + "bullseyes", "checkerboards", "cornsweets", "cubes", @@ -39,6 +42,7 @@ "mueller_lyers", "plaids", "ponzos", + "rings", "sbcs", "todorovics", "waves", diff --git a/stimupy/illusions/circulars.py b/stimupy/bullseyes.py similarity index 62% rename from stimupy/illusions/circulars.py rename to stimupy/bullseyes.py index d3fd7050..231ee501 100644 --- a/stimupy/illusions/circulars.py +++ b/stimupy/bullseyes.py @@ -1,130 +1,10 @@ -import numpy as np - from stimupy.utils import resolution, stack_dicts -from stimupy.waves import square_radial as rings - -__all__ = ["rings", "two_sided_rings", "bullseye", "two_sided_bullseye"] - - -def two_sided_rings( - visual_size=None, - ppd=None, - shape=None, - frequency=None, - n_rings=None, - ring_width=None, - phase_shift=0, - target_indices=None, - intensity_target=0.5, - intensity_rings=(1.0, 0.0), - intensity_background=0.5, - origin="mean", -): - """Two-sided circular grating, with one or more target rings - - Specification of the number of rings, and their width can be done in two ways: - a ring_width (in degrees) and n_rings, and/or by specifying the spatial frequency - of a circular grating (in cycles per degree) - - The total shape (in pixels) and visual size (in degrees) has to match the - specification of the rings and their widths. - Thus, not all 6 parameters (visual_size, ppd, shape, frequency, ring_width, n_rings) - have to be specified, as long as both the resolution, and the distribution of rings, - can be resolved. - - Note: all rings in a grating have the same width -- if more control is required - see disc_and_rings - - 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 circular grating, in cycles per degree - n_rings : int, or None (default) - number of rings - ring_width : Number, or None (default) - width of a single ring, in degrees - phase_shift : float - phase shift of grating in degrees - target_indices : int or Sequence[int, ] (optional) - indices of target discs. If not specified, use middle ring (round down) - intensity_target : float (optional) - intensity value of target ring(s), by default 0.5 - intensity_rings : Sequence[Number, ...] - intensity value for each ring, from inside to out, by default [1,0] - If fewer intensities are passed than number of radii, cycles through intensities - intensity_background : float (optional) - intensity value of background, 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 target (key: "target_mask"), - and additional keys containing stimulus parameters +from stimupy.waves import square_radial - References - ---------- - Hong, S. W., and Shevell, S. K. (2004). - Brightness contrast and assimilation from patterned inducing backgrounds. - Vision Research, 44, 35-43. - https://doi.org/10.1016/j.visres.2003.07.010 - Howe, P. D. L. (2005). - White's effect: - removing the junctions but preserving the strength of the illusion. - Perception, 34, 557-564. - https://doi.org/10.1068/p5414 - """ +__all__ = ["circular", "circular_two_sided"] - # Resolve resolution - shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) - stim1 = rings( - visual_size=(visual_size[0], visual_size[1] / 2), - ppd=ppd, - frequency=frequency, - n_rings=n_rings, - ring_width=ring_width, - phase_shift=phase_shift, - target_indices=target_indices, - intensity_target=intensity_target, - intensity_rings=intensity_rings, - intensity_background=intensity_background, - origin=origin, - clip=True, - ) - - stim2 = rings( - visual_size=(visual_size[0], visual_size[1] / 2), - ppd=ppd, - frequency=frequency, - n_rings=n_rings, - ring_width=ring_width, - phase_shift=phase_shift, - target_indices=target_indices, - intensity_target=intensity_target, - intensity_rings=intensity_rings[::-1], - intensity_background=intensity_background, - origin=origin, - clip=True, - ) - - stim = stack_dicts(stim1, stim2) - stim["shape"] = shape - stim["visual_size"] = visual_size - return stim - - -def bullseye( +def circular( visual_size=None, ppd=None, shape=None, @@ -209,7 +89,7 @@ def bullseye( Perception, 34, 557-564. https://doi.org/10.1068/p5414 """ - stim = rings( + stim = square_radial( visual_size=visual_size, ppd=ppd, shape=shape, @@ -227,7 +107,7 @@ def bullseye( return stim -def two_sided_bullseye( +def circular_two_sided( visual_size=None, ppd=None, shape=None, @@ -313,7 +193,7 @@ def two_sided_bullseye( # Resolve resolution shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) - stim1 = bullseye( + stim1 = circular( visual_size=(visual_size[0], visual_size[1] / 2), ppd=ppd, frequency=frequency, @@ -327,7 +207,7 @@ def two_sided_bullseye( clip=True, ) - stim2 = bullseye( + stim2 = circular( visual_size=(visual_size[0], visual_size[1] / 2), ppd=ppd, frequency=frequency, @@ -347,14 +227,32 @@ def two_sided_bullseye( return stim -if __name__ == "__main__": - from stimupy.utils import plot_stimuli +def overview(**kwargs): + """Generate example stimuli from this module - stims = { - "rings": rings(visual_size=(8, 8), ppd=32, frequency=1.0, clip=True), - "two_sided_rings": two_sided_rings(visual_size=(8, 8), ppd=32, frequency=1.0), - "bullseye": bullseye(visual_size=(8, 8), ppd=32, frequency=1.0), - "two_sided_bullseye": two_sided_bullseye(visual_size=(8, 16), ppd=32, frequency=1.0), + Returns + ------- + stims : dict + dict with all stimuli containing individual stimulus dicts. + """ + default_params = { + "visual_size": (10, 10), + "ppd": 30, } + default_params.update(kwargs) + + # fmt: off + stimuli = { + "circular": circular(**default_params, frequency=1.0, clip=True), + "circular, two sided": circular_two_sided(**default_params, frequency=1.0), + } + # fmt: on + + return stimuli + + +if __name__ == "__main__": + from stimupy.utils import plot_stimuli + stims = overview() plot_stimuli(stims, mask=True, save=None) diff --git a/stimupy/illusions/__init__.py b/stimupy/illusions/__init__.py index 28528954..6dd08524 100644 --- a/stimupy/illusions/__init__.py +++ b/stimupy/illusions/__init__.py @@ -1,4 +1,4 @@ -from . import angulars, circulars, frames +from . import angulars, frames def create_overview(): @@ -26,11 +26,6 @@ def create_overview(): stims = { # Angular "pinwheel": angulars.pinwheel(**p, n_segments=8, target_width=1, target_indices=3), - # Circular - "circular_rings": circulars.rings(**p, frequency=1.0), - "circular_rings_two_sided": circulars.two_sided_rings(**p, frequency=1.0), - "circular_bullseye": circulars.bullseye(**p, frequency=1.0), - "circular_bullseye_two_sided": circulars.two_sided_bullseye(**p, frequency=1.0), # Frames "frames": frames.rings(**p, frequency=0.5, target_indices=3), "frames_general": frames.rings_generalized(**p, radii=(1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5), target_indices=3), diff --git a/stimupy/rings.py b/stimupy/rings.py new file mode 100644 index 00000000..6bd1ac54 --- /dev/null +++ b/stimupy/rings.py @@ -0,0 +1,153 @@ +from stimupy.utils import resolution, stack_dicts +from stimupy.waves import square_radial as circular + +__all__ = ["circular", "circular_two_sided"] + + +def circular_two_sided( + visual_size=None, + ppd=None, + shape=None, + frequency=None, + n_rings=None, + ring_width=None, + phase_shift=0, + target_indices=None, + intensity_target=0.5, + intensity_rings=(1.0, 0.0), + intensity_background=0.5, + origin="mean", +): + """Two-sided circular grating, with one or more target rings + + Specification of the number of rings, and their width can be done in two ways: + a ring_width (in degrees) and n_rings, and/or by specifying the spatial frequency + of a circular grating (in cycles per degree) + + The total shape (in pixels) and visual size (in degrees) has to match the + specification of the rings and their widths. + Thus, not all 6 parameters (visual_size, ppd, shape, frequency, ring_width, n_rings) + have to be specified, as long as both the resolution, and the distribution of rings, + can be resolved. + + Note: all rings in a grating have the same width -- if more control is required + see disc_and_rings + + 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 circular grating, in cycles per degree + n_rings : int, or None (default) + number of rings + ring_width : Number, or None (default) + width of a single ring, in degrees + phase_shift : float + phase shift of grating in degrees + target_indices : int or Sequence[int, ] (optional) + indices of target discs. If not specified, use middle ring (round down) + intensity_target : float (optional) + intensity value of target ring(s), by default 0.5 + intensity_rings : Sequence[Number, ...] + intensity value for each ring, from inside to out, by default [1,0] + If fewer intensities are passed than number of radii, cycles through intensities + intensity_background : float (optional) + intensity value of background, 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 target (key: "target_mask"), + and additional keys containing stimulus parameters + + References + ---------- + Hong, S. W., and Shevell, S. K. (2004). + Brightness contrast and assimilation from patterned inducing backgrounds. + Vision Research, 44, 35-43. + https://doi.org/10.1016/j.visres.2003.07.010 + Howe, P. D. L. (2005). + White's effect: + removing the junctions but preserving the strength of the illusion. + Perception, 34, 557-564. + https://doi.org/10.1068/p5414 + """ + + # Resolve resolution + shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) + + stim1 = circular( + visual_size=(visual_size[0], visual_size[1] / 2), + ppd=ppd, + frequency=frequency, + n_rings=n_rings, + ring_width=ring_width, + phase_shift=phase_shift, + target_indices=target_indices, + intensity_target=intensity_target, + intensity_rings=intensity_rings, + intensity_background=intensity_background, + origin=origin, + clip=True, + ) + + stim2 = circular( + visual_size=(visual_size[0], visual_size[1] / 2), + ppd=ppd, + frequency=frequency, + n_rings=n_rings, + ring_width=ring_width, + phase_shift=phase_shift, + target_indices=target_indices, + intensity_target=intensity_target, + intensity_rings=intensity_rings[::-1], + intensity_background=intensity_background, + origin=origin, + clip=True, + ) + + stim = stack_dicts(stim1, stim2) + stim["shape"] = shape + stim["visual_size"] = visual_size + 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, 10), + "ppd": 30, + } + default_params.update(kwargs) + + # fmt: off + stimuli = { + "circular": circular(**default_params, frequency=1.0, clip=True), + "circular, two sided": circular_two_sided(**default_params, frequency=1.0), + } + # fmt: on + + return stimuli + + +if __name__ == "__main__": + from stimupy.utils import plot_stimuli + + stims = overview() + plot_stimuli(stims, mask=True, save=None)