diff --git a/demo/illusions/checkerboard.ipynb b/demo/illusions/checkerboard.ipynb index 07f978ef..fe83beb1 100644 --- a/demo/illusions/checkerboard.ipynb +++ b/demo/illusions/checkerboard.ipynb @@ -30,15 +30,17 @@ "outputs": [], "source": [ "params = {\n", - " \"shape\": None,\n", - " \"ppd\": 32,\n", " \"visual_size\": None,\n", + " \"ppd\": 32,\n", + " \"shape\": None,\n", + " \"frequency\": None,\n", " \"board_shape\": (5, 8),\n", " \"check_visual_size\": 2.0,\n", " \"targets\": ((2, 1), (2, 6)),\n", " \"extend_targets\": False,\n", - " \"intensity_low\": 0.0,\n", - " \"intensity_high\": 1.0,\n", + " \"period\": \"ignore\",\n", + " \"rotation\": 0,\n", + " \"intensity_checks\": (0.0, 1.0),\n", " \"intensity_target\": 0.5,\n", "}\n", "stim = checkerboard(**params)\n", @@ -120,16 +122,18 @@ " intensity_low = intensity_range[1]\n", " targets = [(target_row, target1_col), (target_row, target2_col)]\n", " stim = checkerboard(\n", - " shape,\n", - " ppd,\n", - " visual_size,\n", - " board_shape,\n", - " check_visual_size,\n", - " targets,\n", - " extend_targets,\n", - " intensity_low,\n", - " intensity_high,\n", - " intensity_target,\n", + " visual_size=visual_size,\n", + " ppd=ppd,\n", + " shape=shape,\n", + " frequency=None,\n", + " board_shape=board_shape,\n", + " check_visual_size=check_visual_size,\n", + " targets=targets,\n", + " extend_targets=extend_targets,\n", + " period=\"ignore\",\n", + " rotation=0,\n", + " intensity_checks=(intensity_low, intensity_high),\n", + " intensity_target=intensity_target,\n", " )\n", " plot_stim(stim)\n", "\n", @@ -252,7 +256,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3.10.5 64-bit ('stimuli-dev')", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -266,9 +270,50 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.10.6" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false }, - "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "d79f930315d22092267204fc095ece7b80c939bb23fde6f7397d8c6352112825" diff --git a/stimuli/components/__init__.py b/stimuli/components/__init__.py index 986f827c..92408bef 100644 --- a/stimuli/components/__init__.py +++ b/stimuli/components/__init__.py @@ -318,8 +318,11 @@ def resolve_grating_params( f"Grating frequency ({frequency}) should not exceed Nyquist limit {ppd/2} (ppd/2)" ) - # Accumulate edges of phases + # Accumulate edges of phases (rounding to avoid accumulation of + # floating point imprecisions) edges = [*itertools.accumulate(itertools.repeat(phase_width, int(np.ceil(n_phases))))] + edges = np.round(np.array(edges), 8) + edges = list(edges) return { "length": length, diff --git a/stimuli/components/checkerboard.py b/stimuli/components/checkerboard.py index 71fa82fe..3c66933a 100644 --- a/stimuli/components/checkerboard.py +++ b/stimuli/components/checkerboard.py @@ -1,186 +1,141 @@ import numpy as np -from stimuli.utils import resolution +import warnings +from stimuli.components.grating import square_wave __all__ = [ "checkerboard", ] -def resolve_params( - ppd=None, - shape=None, - visual_size=None, - board_shape=None, - check_visual_size=None, -): - """Resolves (if possible) the various size parameters of the checkerboard - - Checkerboard component takes the regular resolution parameters(shape, ppd, - visual_size). In addition, there has to be an additional specification of the size - of the checkerboard. This can be done in two ways: either through a board_shape - (height and width in integer number of checks), and/or by specifying the size the - visual size (in degrees) of a single check. - - The total shape (in pixels) and visual size (in degrees) has to match the - specification of the board shape (in checks) and check size (in degrees). Thus, - not all 5 parameters have to be specified, as long as the both the resolution - and the checkerboard size can be resolved. - - Note: all checks in a single board have the same size and shape. - - - Parameters - ---------- - ppd : Sequence[Number, Number], Number, or None (default) - pixels per degree [vertical, horizontal] - shape : Sequence[Number, Number], Number, or None (default) - shape [height, width] in pixels - visual_size : Sequence[Number, Number], Number, or None (default) - visual size of the total board [height, width] in degrees - board_shape : Sequence[Number, Number], Number, or None (default) - number of checks in [height, width] of checkerboard - check_visual_size : Sequence[Number, Number], Number, or None (default) - visual size of a single check [height, width] in degrees - - Returns - ------- - dict - dictionary with all five resolution & size parameters resolved. - - Raises - ------ - ResolutionError - if the total resolution (ppd, shape, visual_size) cannot be resolved - ResolutionError - if the board_shape and/or check_visual_size cannot be resolved - ValueError - if the (resolved) ppd does not allow for drawing (resolved) check_visual_size - - See also: - --------- - stimuli.components.checkerboard.checkerboard : - to draw the actual checkerboard - """ - - # Try to resolve resolution - try: - shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) - except ValueError: - ppd = resolution.validate_ppd(ppd) - shape = resolution.validate_shape(shape) - visual_size = resolution.validate_visual_size(visual_size) - - # Put intput check_visual_size and board_shape into canonical form - check_visual_size = resolution.validate_visual_size(check_visual_size) - board_shape = resolution.validate_shape(board_shape) - - # Try to resolve board dimensions - # The logic here is that inverting check_visual_size, which expresses - # degrees per check, gives "checks per degree", which functions analogous to ppd - # Thus we can resolve the board dimensions also as a "resolution", - # and then just invert the resolved "checks_per_degree" back to check_visual_size - checks_per_degree = ( - 1 / check_visual_size.height if check_visual_size.height is not None else None, - 1 / check_visual_size.width if check_visual_size.width is not None else None, - ) - board_shape, visual_size, checks_per_degree = resolution.resolve( - shape=board_shape, - visual_size=visual_size, - ppd=checks_per_degree, - ) - check_visual_size = resolution.validate_visual_size( - (1 / checks_per_degree.vertical, 1 / checks_per_degree.horizontal) - ) - - # Now resolve ppd - shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) - - # Is check_shape possible? - if (check_visual_size.height * ppd.vertical) % 1 or ( - check_visual_size.width * ppd.horizontal - ) % 1: - raise ValueError(f"Cannot produce checks of {check_visual_size} with resolution of {ppd}") - - return { - "shape": shape, - "visual_size": visual_size, - "ppd": ppd, - "check_visual_size": check_visual_size, - "board_shape": board_shape, - } - - def checkerboard( + visual_size=None, ppd=None, shape=None, - visual_size=None, + frequency=None, board_shape=None, check_visual_size=None, - intensity_low=0.0, - intensity_high=1.0, + period="ignore", + rotation=0, + intensity_checks=(1.0, 0.0), ): """Draws a checkerboard with given specifications Parameters ---------- + visual_size : Sequence[Number, Number], Number, or None (default) + visual size of the total board [height, width] 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] in pixels - visual_size : Sequence[Number, Number], Number, or None (default) - visual size of the total board [height, width] in degrees + frequency : Sequence[Number, Number], Number, or None (default) + frequency of checkerboard in [y, x] in cpd board_shape : Sequence[Number, Number], Number, or None (default) number of checks in [height, width] of checkerboard check_visual_size : Sequence[Number, Number], Number, or None (default) visual size of a single check [height, width] in degrees - intensity_low : float, optional - intensity value of the dark checks, by default 0.0 - intensity_high : float, optional - intensity value of the light checks, by default 1.0 + 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 + rotation of grating in degrees (default: 0 = horizontal) + intensity_checks : Sequence[float, float] + intensity values of checks, by default (1.0, 0.0) Returns - ------- - dict - Stimulus dictionary, with all the (resolved) parameters and - 'img' : stimulus image as 2D numpy.ndarray - - Raises - ------ - ValueError - if checkerboard does not fit into specified/resolved shape - - See also - -------- - stimuli.components.checkerboard.resolve_params : - how the size & resolution parameters can be resolved + ---------- + dict[str, Any] + dict with the stimulus (key: "img"), + mask with integer index for each target (key: "mask"), + and additional keys containing stimulus parameters """ - - params = resolve_params( + if isinstance(frequency, (float, int)) or frequency is None: + frequency = (frequency, frequency) + if isinstance(board_shape, (float, int)) or board_shape is None: + board_shape = (board_shape, board_shape) + if isinstance(check_visual_size, (float, int)) or check_visual_size is None: + check_visual_size = (check_visual_size, check_visual_size) + + create_twice = visual_size is None and shape is None + + # Create checkerboard by treating it as a plaid + sw1 = square_wave( + visual_size=visual_size, ppd=ppd, shape=shape, + frequency=frequency[0], + n_bars=board_shape[0], + bar_width=check_visual_size[0], + period=period, + rotation=rotation, + intensity_bars=intensity_checks, + ) + + sw2 = square_wave( visual_size=visual_size, - board_shape=board_shape, - check_visual_size=check_visual_size, - ) - board_shape = params["board_shape"] - check_visual_size = params["check_visual_size"] - ppc = resolution.shape_from_visual_size_ppd(check_visual_size, params["ppd"]) - - # Build board - img = np.ndarray(board_shape) - for i, j in np.ndindex(img.shape): - img[i, j] = intensity_low if i % 2 == j % 2 else intensity_high - - # Resize by pix per check - img = img.repeat(ppc.height, axis=0).repeat(ppc.width, axis=1) - if shape is not None and shape != (None, None): - if img.shape != shape: - raise ValueError("Could not fit board in {shape}") - - # Collect stim + params - stim = params - stim["img"] = img - stim["intensity_low"] = intensity_low - stim["intensity_high"] = intensity_high - + ppd=ppd, + shape=shape, + frequency=frequency[1], + n_bars=board_shape[1], + bar_width=check_visual_size[1], + period=period, + rotation=rotation+90, + intensity_bars=intensity_checks, + ) + + # If neither a visual_size nor a shape was given, each square wave + # grating is always a square. An easy solution is to just recreate + # both gratings with the resolved parameters + if create_twice: + warnings.filterwarnings("ignore") + sw1 = square_wave( + visual_size=(sw1["visual_size"][0], sw2["visual_size"][1]), + ppd=sw1["ppd"], + shape=None, + frequency=frequency[0], + n_bars=board_shape[0], + bar_width=check_visual_size[0], + period=period, + rotation=rotation, + intensity_bars=intensity_checks, + ) + + sw2 = square_wave( + visual_size=(sw1["visual_size"][0], sw2["visual_size"][1]), + ppd=sw1["ppd"], + shape=None, + frequency=frequency[1], + n_bars=board_shape[1], + bar_width=check_visual_size[1], + period=period, + rotation=rotation+90, + intensity_bars=intensity_checks, + ) + warnings.filterwarnings("default") + + # Add the two square-wave gratings into a checkerboard + img = sw1["img"] + sw2["img"] + img = np.where(img == intensity_checks[0]+intensity_checks[1], intensity_checks[1], intensity_checks[0]) + + # Create a mask with target indices for each check + mask = sw1["mask"] + sw2["mask"]*sw1["mask"].max()*10 + unique_vals = np.unique(mask) + for v in range(len(unique_vals)): + mask[mask == unique_vals[v]] = v+1 + + stim = { + "img": img, + "mask": mask.astype(int), + "visual_size": sw1["visual_size"], + "ppd": sw1["ppd"], + "shape": sw1["shape"], + "frequency": (sw2["frequency"], sw1["frequency"]), + "board_shape": (sw2["n_bars"], sw1["n_bars"]), + "check_visual_size": (sw2["bar_width"], sw1["bar_width"]), + "period": period, + "rotation": rotation, + "intensity_checks": intensity_checks, + "edges": (sw1["edges"], sw2["edges"]), + } return stim diff --git a/stimuli/components/grating.py b/stimuli/components/grating.py index dfbf4ec6..54e3f5a2 100644 --- a/stimuli/components/grating.py +++ b/stimuli/components/grating.py @@ -53,9 +53,9 @@ def mask_bars( def square_wave( - shape=None, visual_size=None, ppd=None, + shape=None, frequency=None, n_bars=None, bar_width=None, @@ -67,12 +67,12 @@ def square_wave( Parameters ---------- - shape : Sequence[Number, Number], Number, or None (default) - shape [height, width] of image, in pixels 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) diff --git a/stimuli/illusions/checkerboards.py b/stimuli/illusions/checkerboards.py index ce905125..f0027a48 100644 --- a/stimuli/illusions/checkerboards.py +++ b/stimuli/illusions/checkerboards.py @@ -121,15 +121,17 @@ def add_targets(checkerboard_stim, targets, extend_targets=False, intensity_targ def checkerboard( - shape=None, - ppd=None, visual_size=None, + ppd=None, + shape=None, + frequency=None, board_shape=None, check_visual_size=None, targets=None, extend_targets=False, - intensity_low=0.0, - intensity_high=1.0, + period="ignore", + rotation=0, + intensity_checks=(0.0, 1.0), intensity_target=0.5, ): """Checkerboard assimilation effect @@ -142,12 +144,14 @@ def checkerboard( Parameters ---------- - shape : Sequence[Number, Number], Number, or None (default) - shape [height, width] in pixels - ppd : Sequence[Number, Number], Number, or None (default) - pixels per degree [vertical, horizontal] visual_size : Sequence[Number, Number], Number, or None (default) visual size of the total board [height, width] 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] in pixels + frequency : Sequence[Number, Number], Number, or None (default) + frequency of checkerboard in [y, x] in cpd board_shape : Sequence[Number, Number], Number, or None (default) number of checks in [height, width] of checkerboard check_visual_size : Sequence[Number, Number], Number, or None (default) @@ -156,12 +160,14 @@ def checkerboard( target indices (row, column of checkerboard), by default None extend_targets : bool, optional if true, extends the targets by 1 check in all 4 directions, by default False - intensity_low : float, optional - intensity value of the dark checks (top left corner), by default 0.0 - intensity_high : float, optional - intensity value of the light checks, by default 1.0 - intensity_target : float, optional - intensity value of the target checks, by default 0.5 + 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 + rotation of grating in degrees (default: 0 = horizontal) + intensity_checks : Sequence[float, float] + intensity values of checks, by default (0.0, 1.0) Returns ------- @@ -186,13 +192,15 @@ def checkerboard( # Set up basic checkerboard stim = board( + visual_size=visual_size, ppd=ppd, shape=shape, - visual_size=visual_size, + frequency=frequency, board_shape=board_shape, check_visual_size=check_visual_size, - intensity_low=intensity_low, - intensity_high=intensity_high, + period=period, + rotation=rotation, + intensity_checks=intensity_checks, ) # Add targets @@ -209,14 +217,16 @@ def checkerboard( def contrast_contrast( - shape=None, - ppd=None, visual_size=None, + ppd=None, + shape=None, + frequency=None, board_shape=None, check_visual_size=None, target_shape=(2, 3), - intensity_low=0.0, - intensity_high=1.0, + period="ignore", + rotation=0, + intensity_checks=(0.0, 1.0), tau=0.5, alpha=0.2, ): @@ -270,13 +280,15 @@ def contrast_contrast( # Set up basic checkerboard stim = board( + visual_size=visual_size, ppd=ppd, shape=shape, - visual_size=visual_size, + frequency=frequency, board_shape=board_shape, check_visual_size=check_visual_size, - intensity_low=intensity_low, - intensity_high=intensity_high, + period=period, + rotation=rotation, + intensity_checks=intensity_checks, ) img = stim["img"] diff --git a/stimuli/papers/RHS2007.py b/stimuli/papers/RHS2007.py index 81427fe6..c7debc85 100644 --- a/stimuli/papers/RHS2007.py +++ b/stimuli/papers/RHS2007.py @@ -1260,8 +1260,7 @@ def checkerboard_016(ppd=PPD, pad=True): "check_visual_size": (5 / 32, 5 / 32), "targets": ((target_row, 16), (target_row, 85)), "extend_targets": False, - "intensity_low": v3, - "intensity_high": v1, + "intensity_checks": (v3, v1), "intensity_target": v2, } stim = illusions.checkerboards.checkerboard(**params) @@ -1311,8 +1310,7 @@ def checkerboard_094(ppd=PPD, pad=True): "check_visual_size": (30 / 32, 30 / 32), "targets": ((target_row, 6), (target_row, 17)), "extend_targets": False, - "intensity_low": v1, - "intensity_high": v3, + "intensity_checks": (v1, v3), "intensity_target": v2, } stim = illusions.checkerboards.checkerboard(**params) @@ -1362,8 +1360,7 @@ def checkerboard_21(ppd=PPD, pad=True): "check_visual_size": (67 / 32, 67 / 32), "targets": ((target_row, 2), (target_row, 7)), "extend_targets": False, - "intensity_low": v1, - "intensity_high": v3, + "intensity_checks": (v1, v3), "intensity_target": v2, } stim = illusions.checkerboards.checkerboard(**params) diff --git a/stimuli/papers/domijan2015.py b/stimuli/papers/domijan2015.py index 9116831f..14e7f956 100644 --- a/stimuli/papers/domijan2015.py +++ b/stimuli/papers/domijan2015.py @@ -824,8 +824,7 @@ def checkerboard_contrast_contrast( "target_shape": (4, 4), "tau": 0.5, "alpha": 0.5, - "intensity_low": v1, - "intensity_high": v3, + "intensity_checks": (v1, v3), } # Large checkerboard, embedded target region @@ -914,9 +913,8 @@ def checkerboard( "check_visual_size": (1.0 * visual_resize, 1.0 * visual_resize), "targets": [(3, 2), (5, 5)], "extend_targets": False, - "intensity_low": 0, - "intensity_high": 1, - "intensity_target": 0.5, + "intensity_checks": (v1, v3), + "intensity_target": v2, } stim = illusions.checkerboards.checkerboard(**params) @@ -986,9 +984,8 @@ def checkerboard_extended( "check_visual_size": (1.0 * visual_resize, 1.0 * visual_resize), "targets": [(3, 2), (5, 5)], "extend_targets": True, - "intensity_low": 0, - "intensity_high": 1, - "intensity_target": 0.5, + "intensity_checks": (v1, v3), + "intensity_target": v2, } stim = illusions.checkerboards.checkerboard(**params) @@ -1257,4 +1254,4 @@ def white_howe(visual_size=VSIZES["white_howe"], ppd=PPD, shape=SHAPES["white_ho from stimuli.utils import plot_stimuli stims = gen_all(skip=True) - plot_stimuli(stims, mask=False) + plot_stimuli(stims, mask=True) diff --git a/stimuli/papers/murray2020.py b/stimuli/papers/murray2020.py index c592601e..e701c71b 100644 --- a/stimuli/papers/murray2020.py +++ b/stimuli/papers/murray2020.py @@ -497,8 +497,7 @@ def checkassim(ppd=PPD, pad=PAD): "check_visual_size": 1 / PPD, "targets": ((3, 6), (3, 3)), "extend_targets": False, - "intensity_low": 17.5, - "intensity_high": 70.0, + "intensity_checks": (17.5, 70.0), "intensity_target": 35.0, } stim = illusions.checkerboards.checkerboard(