From 17058e9c2576c46a94df3d86d55f3056047576a2 Mon Sep 17 00:00:00 2001 From: lynnschmittwilken Date: Mon, 9 Jan 2023 12:09:06 +0100 Subject: [PATCH] closes #89; general component Mondrian, updated related code --- stimuli/components/mondrians.py | 143 +++++++++++++++++++++++++ stimuli/illusions/mondrians.py | 182 +++++++++++++++++--------------- stimuli/papers/RHS2007.py | 7 +- 3 files changed, 244 insertions(+), 88 deletions(-) create mode 100644 stimuli/components/mondrians.py diff --git a/stimuli/components/mondrians.py b/stimuli/components/mondrians.py new file mode 100644 index 00000000..39315a0e --- /dev/null +++ b/stimuli/components/mondrians.py @@ -0,0 +1,143 @@ +import numpy as np + +from stimuli.components.shapes import parallelogram +from stimuli.utils import resolution, degrees_to_pixels + +__all__ = [ + "mondrians", +] + + +def mondrians( + visual_size=None, + ppd=None, + shape=None, + mondrian_positions=None, + mondrian_sizes=None, + mondrian_intensities=None, + intensity_background=0.5, + ): + + # 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") + + img = np.ones(shape) * intensity_background + mask = np.zeros(shape) + + n_mondrians = len(mondrian_positions) + + if isinstance(mondrian_intensities, (float, int)): + mondrian_intensities = (mondrian_intensities,) * n_mondrians + + if isinstance(mondrian_sizes, (float, int)): + mondrian_sizes = ((mondrian_sizes, mondrian_sizes), ) * n_mondrians + + if any(len(lst) != n_mondrians for lst in [mondrian_positions, mondrian_sizes, mondrian_intensities]): + raise Exception("There need to be as many mondrian_positions as there are " + "mondrian_sizes and mondrian_intensities.") + + mondrian_positions_px = [] + mondrian_shapes = [] + + for m in range(n_mondrians): + try: + if len(mondrian_positions[m]) != 2: + raise ValueError("Mondrian position tuples should be (ypos, xpos)") + except Exception: + raise ValueError("Mondrian position tuples should be (ypos, xpos)") + + ypos, xpos = degrees_to_pixels(mondrian_positions[m], ppd[0]) + individual_shapes = degrees_to_pixels(mondrian_sizes[m], ppd[0]) + + try: + if len(individual_shapes) == 2: + depth = 0 + individual_shapes = individual_shapes + [depth,] + elif len(individual_shapes) == 3: + depth = mondrian_sizes[m][2] + else: + raise ValueError("Mondrian size tuples should be (height, width) for " + "rectangles or (height, width, depth) for parallelograms") + except Exception: + raise ValueError("Mondrian size tuples should be (height, width) for" + "rectangles or (height, width, depth) for parallelograms") + + if depth < 0: + xpos += int(depth*ppd[0]) + mondrian_positions_px.append(tuple([ypos, xpos])) + mondrian_shapes.append(tuple(individual_shapes)) + + # Create parallelogram + patch = parallelogram( + visual_size=(mondrian_sizes[m][0], mondrian_sizes[m][1]+np.abs(depth)), + ppd=ppd, + parallelogram_depth=depth, + intensity_background=intensity_background, + intensity_parallelogram=mondrian_intensities[m], + ) + + # Place it into Mondrian mosaic + yshape, xshape = patch["img"].shape + if ypos < 0 or xpos < 0: + raise ValueError("There are no negative position coordinates") + if (ypos+yshape > shape[0]) or (xpos+xshape > shape[1]): + raise ValueError("Not all Mondrians fit into the stimulus") + mask_large = np.zeros(shape) + mask_large[ypos:ypos+yshape, xpos:xpos+xshape] = patch["mask"] + + img[mask_large == 1] = mondrian_intensities[m] + mask[mask_large == 1] = m+1 + + stim = { + "img": img, + "mondrian_mask": mask.astype(int), + "ppd": ppd, + "visual_size": visual_size, + "shape": shape, + "mondrian_positions": tuple(mondrian_positions), + "mondrian_positions_px": tuple(mondrian_positions_px), + "mondrian_sizes": tuple(mondrian_sizes), + "mondrian_shapes": tuple(mondrian_shapes), + "mondrian_intensities": tuple(mondrian_intensities), + "intensity_background": intensity_background, + } + return stim + + +if __name__ == "__main__": + from stimuli.utils.plotting import plot_stimuli + + p1 = { + "mondrian_positions": ((0,0), (0,4), (1,3), (4,4), (5,1)), + "mondrian_sizes": 3, + "mondrian_intensities": np.random.rand(5), + } + + p2 = { + "mondrian_positions": ((0,0), (8,4), (1,6), (4,4), (5,1)), + "mondrian_sizes": ((3,4,1), (2,2,0), (5,4,-1), (3,4,1), (5,2,0)), + "mondrian_intensities": np.random.rand(5), + } + + p3 = { + "mondrian_positions": ((0,0), (0, 2)), + "mondrian_sizes": ((2,2,0), (2,2,0)), + "mondrian_intensities": (0.2, 0.8), + } + + p4 = { + "mondrian_positions": ((0,0), (0, 2)), + "mondrian_sizes": ((2,2,1), (2,2,1)), + "mondrian_intensities": (0.2, 0.8), + } + + stims = { + "mondrians1": mondrians(visual_size=8, ppd=10, **p1), + "mondrians2": mondrians(visual_size=10, ppd=10, **p2), + "mondrians3": mondrians(visual_size=(2, 6), ppd=10, **p3), + "mondrians4": mondrians(visual_size=(2, 6), ppd=10, **p4), + } + + plot_stimuli(stims) \ No newline at end of file diff --git a/stimuli/illusions/mondrians.py b/stimuli/illusions/mondrians.py index 0228ca7e..d698b9b3 100644 --- a/stimuli/illusions/mondrians.py +++ b/stimuli/illusions/mondrians.py @@ -1,7 +1,7 @@ import numpy as np -from stimuli.components.shapes import parallelogram -from stimuli.utils import degrees_to_pixels +from stimuli.components.mondrians import mondrians +from stimuli.utils import resolution, degrees_to_pixels __all__ = [ "corrugated_mondrians", @@ -9,12 +9,12 @@ def corrugated_mondrians( + visual_size=None, ppd=None, - width=None, - heights=None, - depths=None, + shape=None, + mondrian_depths=None, + mondrian_intensities=None, target_indices=None, - intensities=None, intensity_background=0.5, ): """ @@ -22,21 +22,21 @@ def corrugated_mondrians( Parameters ---------- - ppd : int - pixels per degree (visual angle) - width : float - width of rectangles in degree visual angle - heights : float or tuple of floats - height of rectangles; if single float, all rectangles have the same height - depths : float or tuple of floats - depth of rectangles; as many depths as there are rows + 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 + mondrian_depths : float or tuple of floats + depth of parallelograms (ie mondrians) per row + mondrian_intensities : nested tuples + intensities of mondrians; as many tuples as there are rows and as many + numbers in each tuple as there are columns target_indices : nested tuples - indices of targets; as many tuples as there are targets each with (x, y) indices - intensities : nested tuples - intensities of indiidual rectangles; as many tuples as there are rows and as many numbers in each - tuple as there are columns + indices of targets; as many tuples as there are targets with (y, x) indices intensity_background : float - value for background + intensity value for background Returns ------- @@ -48,80 +48,81 @@ def corrugated_mondrians( Science, 262(5142), 2042–2044. https://doi.org/10.1126/science.8266102 """ - if isinstance(heights, (float, int)): - heights = [heights] * len(depths) - - if any(len(lst) != len(heights) for lst in [depths, intensities]): - raise Exception("heights, depths, and intensities need the same length.") - - widths_px = degrees_to_pixels(width, ppd) - heights_px = degrees_to_pixels(heights, ppd) - depths_px = degrees_to_pixels(depths, ppd) - - nrows = len(depths) - ncols = len(intensities[0]) - height = int(np.array(heights_px).sum()) - width_ = int(widths_px * ncols + np.abs(np.array(depths_px)).sum()) - img = np.ones([height, width_]) * intensity_background - mask = np.zeros([height, width_]) - mval = 1 - + # 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") + nrows = len(mondrian_intensities) + ncols = len(mondrian_intensities[0]) + + if isinstance(mondrian_depths, (float, int)): + mondrian_depths = (mondrian_depths,) * nrows + + if len(mondrian_depths) != nrows: + raise ValueError("Unclear number of Mondrians in y-direction, check elements " + "in mondrian_intensities and mondrian_depths") + + height, width = visual_size + mdepths_px = degrees_to_pixels(mondrian_depths, ppd[0]) + max_depth = np.abs(np.array(mdepths_px)).max() + sum_depth = np.array(mdepths_px).sum() + red_depth = np.maximum(max_depth, sum_depth) + mheight_px, mwidth_px = int(shape[0] / nrows), int((shape[1]-red_depth) / ncols) + # Initial y coordinates yst = 0 - yen = int(heights_px[0]) # Calculate initial x coordinates - xstarts = np.cumsum(np.hstack([0, depths_px])) - temp = np.hstack([depths_px, 0]) + xstarts = np.cumsum(np.hstack([0, mdepths_px])) + temp = np.hstack([mdepths_px, 0]) temp[temp > 0] = 0.0 xstarts += temp xstarts += np.abs(xstarts.min()) - + + sizes = [] + poses = [] + ints = [] + tlist = [] + target_counter = 1 + for r in range(nrows): xst = xstarts[r] - xen = xst + int(widths_px + np.abs(depths_px[r])) + if mondrian_depths[r] < 0: + xst -= int(mondrian_depths[r]*ppd[0]) for c in range(ncols): - stim = parallelogram( - visual_size=(heights[r], width + abs(depths[r])), - ppd=ppd, - parallelogram_depth=depths[r], - intensity_background=0.0, - intensity_parallelogram=intensities[r][c] - intensity_background, - ) - - img[yst:yen, xst:xen] += stim["img"] - - if (r, c) in target_indices: - mask[yst:yen, xst:xen] += stim["mask"] * mval - mval += 1 - - xst += widths_px - xen += widths_px - yst += heights_px[r] - yen += heights_px[r] - - # Find and delete all irrelevant columns: - idx = np.argwhere(np.all(img == intensity_background, axis=0)) - img = np.delete(img, idx, axis=1) - mask = np.delete(mask, idx, axis=1) - - if len(np.unique(img[mask != 0])) > 1: + msize = (mheight_px/ppd[0], mwidth_px/ppd[1], mondrian_depths[r]) + mpos = (yst/ppd[0], xst/ppd[1]) + mint = mondrian_intensities[r][c] + + sizes.append(msize) + poses.append(mpos) + ints.append(mint) + + if (target_indices is not None) and (r, c) in target_indices: + tlist.append(target_counter) + + xst += mwidth_px + target_counter += 1 + yst += mheight_px + + stim = mondrians( + visual_size=visual_size, + ppd=ppd, + shape=shape, + mondrian_positions=poses, + mondrian_sizes=sizes, + mondrian_intensities=ints, + intensity_background=intensity_background, + ) + target_mask = np.zeros(shape) + for t in range(len(tlist)): + target_mask[stim["mondrian_mask"] == tlist[t]] = t+1 + stim["mask"] = target_mask.astype(int) + stim["target_indices"] = target_indices + + if len(np.unique(stim["img"][target_mask != 0])) > 1: raise Exception("targets are not equiluminant.") - - stim = { - "img": img, - "mask": mask.astype(int), - "ppd": ppd, - "visual_size": np.array(img.shape) / ppd, - "shape": img.shape, - "width": width, - "heights": heights, - "depths": depths, - "intensity_background": intensity_background, - "intensities": intensities, - "target_indices": target_indices, - } return stim @@ -129,7 +130,7 @@ def corrugated_mondrians( from stimuli.utils import plot_stim params = { - "ppd": 10, + "ppd": 20, "width": 2.0, "heights": 2.0, "depths": (0.0, 1.0, 0.0, -1.0), @@ -141,6 +142,19 @@ def corrugated_mondrians( (0.0, 0.4, 0.0, 0.4), ), } + + p2 = { + "visual_size": 10, + "ppd": 20, + "mondrian_depths": (1, 0, -1, 0), + "mondrian_intensities": ( + (0.4, 0.75, 0.4, 0.75), + (0.75, 0.4, 0.75, 1.0), + (0.4, 0.75, 0.4, 0.75), + (0.0, 0.4, 0.0, 0.4), + ), + "target_indices": ((1, 1), (3, 1)), + } - stim = corrugated_mondrians(**params) - plot_stim(stim, stim_name="Corrugated mondrians", mask=True, save=None) + stim = corrugated_mondrians(**p2) + plot_stim(stim, stim_name="Corrugated mondrians", mask=False, save=None) diff --git a/stimuli/papers/RHS2007.py b/stimuli/papers/RHS2007.py index e2f4847c..81427fe6 100644 --- a/stimuli/papers/RHS2007.py +++ b/stimuli/papers/RHS2007.py @@ -1412,12 +1412,11 @@ def corrugated_mondrian(ppd=PPD, pad=True): (v3, v2, v3, v2, v3), ) params = { + "visual_size": (5*2, 5*2+1), "ppd": ppd, - "width": 2.0, - "heights": 2.0, - "depths": (0.0, -1.0, 0.0, 1.0, 0.0), + "mondrian_depths": (0.0, -1.0, 0.0, 1.0, 0.0), + "mondrian_intensities": values, "target_indices": ((1, 2), (3, 2)), - "intensities": values, "intensity_background": 0.5, }