From 31ab942e59088c015b7473aa957c4847eff20452 Mon Sep 17 00:00:00 2001 From: lynnschmittwilken Date: Thu, 29 Sep 2022 13:37:12 +0200 Subject: [PATCH] partially updated benarys cross and changed default ppd for domijan2015 - producing the correct stims --- stimuli/illusions/__init__.py | 2 +- stimuli/illusions/benary_cross.py | 134 ++++++++++++++++++++++++++++-- stimuli/papers/RHS2007.py | 4 +- stimuli/papers/domijan2015.py | 42 ++++------ 4 files changed, 144 insertions(+), 38 deletions(-) diff --git a/stimuli/illusions/__init__.py b/stimuli/illusions/__init__.py index b7bfb4ac..073056a9 100644 --- a/stimuli/illusions/__init__.py +++ b/stimuli/illusions/__init__.py @@ -1,4 +1,4 @@ -from .benary_cross import benarys_cross +from .benary_cross import * from .bullseye import bullseye_stimulus from .checkerboards import * from .circular import * diff --git a/stimuli/illusions/benary_cross.py b/stimuli/illusions/benary_cross.py index 907b5f57..74db5497 100644 --- a/stimuli/illusions/benary_cross.py +++ b/stimuli/illusions/benary_cross.py @@ -4,9 +4,9 @@ from stimuli.utils import degrees_to_pixels -def benarys_cross( - ppd=10.0, - cross_size=(8.0, 8.0, 8.0, 8.0), +def benarys_cross_generalized( + shape=(21., 21.), + ppd=18.0, cross_thickness=5.0, target_size=(2.0, 2.0), target_type=("r", "r"), @@ -22,10 +22,10 @@ def benarys_cross( Parameters ---------- + shape : float or (float, float) + size of the stimulus in degrees of visual angle (height, width) ppd : int pixels per degree (visual angle) - cross_size : (float, float, float, float) - size of the cross' arms in degrees visual angle in form (top, bottom, left, right) cross_thickness : float width of the cross bars in degrees visual angle target_size : (float, float) @@ -49,6 +49,10 @@ def benarys_cross( ------- A stimulus dictionary with the stimulus ['img'] and target mask ['mask'] """ + if isinstance(shape, (float, int)): + shape = (shape, shape) + if isinstance(target_size, (float, int)): + target_size = (target_size, target_size) if any( len(lst) != len(target_type) for lst in [target_ori, target_posx, target_posy] @@ -58,6 +62,8 @@ def benarys_cross( " same length." ) + cross_size = np.array((shape[0], shape[0], shape[1], shape[1])) - cross_thickness + cross_size = cross_size / 2. target_y_px, target_x_px = degrees_to_pixels(target_size, ppd) tposy = degrees_to_pixels(target_posy, ppd) tposx = degrees_to_pixels(target_posx, ppd) @@ -66,9 +72,9 @@ def benarys_cross( mask = np.zeros((img.shape[0], img.shape[1])) if (target_x_px + np.array(tposx)).max() > img.shape[1]: - raise Exception("Leftmost target does not fit in image.") + raise ValueError("Rightmost target does not fit in image.") if (target_y_px + np.array(tposy)).max() > img.shape[0]: - raise Exception("Lowest target does not fit in image.") + raise ValueError("Lowest target does not fit in image.") # Add targets: for i in range(len(target_posx)): @@ -113,9 +119,119 @@ def benarys_cross( tposy[i] : tposy[i] + tpatch.shape[0], tposx[i] : tposx[i] + tpatch.shape[1], ] = mpatch + + # Make sure that stimulus size is as requested + img = img[0:int(shape[0]*ppd), 0:int(shape[1]*ppd)] + mask = mask[0:int(shape[0]*ppd), 0:int(shape[1]*ppd)] return {"img": img, "mask": mask} +def benarys_cross_rectangles( + shape=(21., 21.), + ppd=18, + cross_thickness=5.0, + target_size=(2.0, 4.0), + vback=1.0, + vcross=0.0, + vtarget=0.5, +): + """ + Benary's Cross stimulus with two rectangular targets with default placement + + Parameters + ---------- + shape : float or (float, float) + size of the stimulus in degrees of visual angle (height, width) + ppd : int + pixels per degree (visual angle) + cross_thickness : float + width of the cross bars in degrees visual angle + target_size : (float, float) + size of all target(s) in degrees visual angle + vback : float + background value + vcross : float + cross value + vtarget : float + target value + + Returns + ------- + A stimulus dictionary with the stimulus ['img'] and target mask ['mask'] + """ + + if isinstance(shape, (float, int)): + shape = (shape, shape) + if isinstance(target_size, (float, int)): + target_size = (target_size, target_size) + if target_size[0] > cross_thickness: + raise ValueError("Target size is larger than cross thickness") + + # Calculate parameters for classical Benarys cross with two targets + target_type = ("r",) * 2 + target_ori = (0., 0.) + target_posx = ((shape[1]-cross_thickness)/2. - target_size[1], shape[1]-target_size[1]) + target_posx = np.floor(np.array(target_posx) * ppd) / ppd + target_posy = ((shape[0]-cross_thickness)/2. - target_size[0], (shape[0]-cross_thickness)/2.) + target_posy = np.floor(np.array(target_posy) * ppd) / ppd + stim = benarys_cross_generalized(shape, ppd, cross_thickness, target_size, target_type, + target_ori, target_posx, target_posy, vback, vcross, vtarget) + return stim + + +def benarys_cross_triangles( + shape=(21., 21.), + ppd=18, + cross_thickness=5.0, + target_size=(2.0, 4.0), + vback=1.0, + vcross=0.0, + vtarget=0.5, +): + """ + Benary's Cross stimulus with two rectangular targets with default placement + + Parameters + ---------- + shape : float or (float, float) + size of the stimulus in degrees of visual angle (height, width) + ppd : int + pixels per degree (visual angle) + cross_thickness : float + width of the cross bars in degrees visual angle + target_size : (float, float) + size of all target(s) in degrees visual angle + vback : float + background value + vcross : float + cross value + vtarget : float + target value + + Returns + ------- + A stimulus dictionary with the stimulus ['img'] and target mask ['mask'] + """ + + if isinstance(shape, (float, int)): + shape = (shape, shape) + if isinstance(target_size, (float, int)): + target_size = (target_size, target_size) + if target_size[0] > cross_thickness: + raise ValueError("Target size is larger than cross thickness") + + # Calculate parameters for classical Benarys cross with two targets + target_type = ("t",) * 2 + target_ori = (0., 0.) + target_posx = ((shape[1]-cross_thickness)/2. - target_size[1], shape[1]-target_size[1]) + target_posx = np.floor(np.array(target_posx) * ppd) / ppd + target_posy = ((shape[0]-cross_thickness)/2. - target_size[0], (shape[0]-cross_thickness)/2.) + target_posy = np.floor(np.array(target_posy) * ppd) / ppd + stim = benarys_cross_generalized(shape, ppd, cross_thickness, target_size, target_type, + target_ori, target_posx, target_posy, vback, vcross, vtarget) + return stim + + def todorovic_benary( ppd=10.0, L_size=(8.0, 8.0, 2.0, 14.0), @@ -242,7 +358,9 @@ def todorovic_benary( from stimuli.utils import plot_stimuli stims = { - "Benary's cross": benarys_cross(), + "Benary's cross - generalized": benarys_cross_generalized(), + "Benary's cross - rectangles": benarys_cross_rectangles(), + "Benary's cross - triangles": benarys_cross_triangles(), "Todorovic' version of Benary's cross": todorovic_benary(), } ax = plot_stimuli(stims) diff --git a/stimuli/papers/RHS2007.py b/stimuli/papers/RHS2007.py index 53258994..016fcd93 100644 --- a/stimuli/papers/RHS2007.py +++ b/stimuli/papers/RHS2007.py @@ -766,9 +766,9 @@ def benary_cross(ppd=PPD, pad=True): vback = 1.0 vtarget = 0.5 padding = (0.0, 0.0, 4.0, 4.0) - stim = illusions.benary_cross.benarys_cross( + stim = illusions.benary_cross.benarys_cross_generalized( + shape=(13, 23), ppd=PPD, - cross_size=(4.5, 4.5, 9.5, 9.5), cross_thickness=4.0, target_type=("t", "t"), target_ori=(45.0, 0.0), diff --git a/stimuli/papers/domijan2015.py b/stimuli/papers/domijan2015.py index 854785fd..a77d8414 100644 --- a/stimuli/papers/domijan2015.py +++ b/stimuli/papers/domijan2015.py @@ -20,7 +20,7 @@ "white_yazdanbakhsh", ] -PPD = 1 # default: 1 +PPD = 10 # default: 10 HEIGHT_DEG = None # default: None PAD = True @@ -54,20 +54,20 @@ def check_requirements(original_size_px, height_px, height_deg, ppd): raise ValueError('You need to define two out of ppd, height_px and height_deg') if height_px is not None and ppd is not None: - conversion_fac = height_px / original_size_px / ppd + conversion_fac = height_px / original_size_px * PPD / ppd if height_deg is not None and ppd is not None: - conversion_fac = height_deg / original_size_px + conversion_fac = height_deg / original_size_px * PPD if height_px is not None and height_deg is not None and ppd is not None: ppd_calc = int(np.round(height_px / height_deg)) assert ppd_calc == ppd - conversion_fac = height_px / original_size_px / ppd + conversion_fac = height_px / original_size_px * PPD / ppd if height_px is not None and height_deg is not None and ppd is None: ppd = int(np.round(height_px / height_deg)) - conversion_fac = height_px / original_size_px / ppd - return height_px, height_deg, ppd, conversion_fac + conversion_fac = height_px / original_size_px * PPD / ppd + return height_px, height_deg, ppd, conversion_fac / PPD def dungeon(height_px=110, ppd=PPD, height_deg=HEIGHT_DEG): @@ -285,7 +285,7 @@ def simultaneous_brightness_contrast(height_px=100, ppd=PPD, height_deg=HEIGHT_D target_size = np.array((21, 21)) * conversion_fac target_pos = np.array((39, 39)) * conversion_fac - stim1 = illusions.sbc.simultaneous_contrast_general( + stim1 = illusions.sbc.simultaneous_contrast_generalized( shape=im_size, ppd=ppd, target_size=target_size, @@ -293,7 +293,7 @@ def simultaneous_brightness_contrast(height_px=100, ppd=PPD, height_deg=HEIGHT_D vback=1., vtarget=0.5, ) - stim2 = illusions.sbc.simultaneous_contrast_general( + stim2 = illusions.sbc.simultaneous_contrast_generalized( shape=im_size, ppd=ppd, target_size=target_size, @@ -340,26 +340,19 @@ def white(height_px=80, ppd=PPD, height_deg=HEIGHT_DEG, pad=PAD): def benary(height_px=100, ppd=PPD, height_deg=HEIGHT_DEG): height_px, height_deg, ppd, conversion_fac = check_requirements(100, height_px, height_deg, ppd) - cross_size = np.array((30,)*4) * conversion_fac - target_size = np.array((11.1, 11.1)) * conversion_fac # TODO: fix rounding problem for different ppds - target_posx = np.array((19, 70)) * conversion_fac - target_posy = np.array((19, 30)) * conversion_fac + target_size = np.array((11, 11)) * conversion_fac - stim = illusions.benary_cross.benarys_cross( + stim = illusions.benary_cross.benarys_cross_rectangles( + shape=81*conversion_fac, ppd=ppd, - cross_size=cross_size, cross_thickness=21*conversion_fac, - target_type=("r", "r"), - target_ori=(0.0, 0.0), target_size=target_size, - target_posx=target_posx, - target_posy=target_posy, vback=1., vcross=0., vtarget=0.5, ) - padding = np.array((9., 10., 9., 10.)) * conversion_fac + padding = np.array((9, 10., 9, 10.)) * conversion_fac stim["img"] = pad_img(stim["img"], padding, ppd, val=1.) stim["mask"] = pad_img(stim["mask"], padding, ppd, val=0) stim["original_range"] = (1, 9) @@ -367,6 +360,7 @@ def benary(height_px=100, ppd=PPD, height_deg=HEIGHT_DEG): def todorovic(height_px=100, ppd=PPD, height_deg=HEIGHT_DEG): + # Note: Compared to original, targets are misplaced by one pixel height_px, height_deg, ppd, conversion_fac = check_requirements(100, height_px, height_deg, ppd) shape = np.array((100, 100)) * conversion_fac target_size = np.array((41, 41)) * conversion_fac @@ -374,7 +368,7 @@ def todorovic(height_px=100, ppd=PPD, height_deg=HEIGHT_DEG): covers_offset = np.array((20, 20)) * conversion_fac stim1 = illusions.todorovic.todorovic_rectangle( - shape=shape-1, + shape=shape, ppd=ppd, target_size=target_size, covers_size=covers_size, @@ -384,7 +378,7 @@ def todorovic(height_px=100, ppd=PPD, height_deg=HEIGHT_DEG): vcovers=1., ) stim2 = illusions.todorovic.todorovic_rectangle( - shape=shape-1, + shape=shape, ppd=ppd, target_size=target_size, covers_size=covers_size, @@ -394,12 +388,6 @@ def todorovic(height_px=100, ppd=PPD, height_deg=HEIGHT_DEG): vcovers=0., ) - # In original stimulus, the targets are not placed perfectly centered - stim1['img'] = pad_img_to_shape(stim1['img'], shape, val=0.) - stim1['mask'] = pad_img_to_shape(stim1['mask'], shape, val=0) - stim2['img'] = pad_img_to_shape(stim2['img'], shape, val=1.) - stim2['mask'] = pad_img_to_shape(stim2['mask'], shape, val=0) - # Increase target index of right stimulus half mask2 = stim2["mask"] + 1 mask2[mask2 == 1] = 0