From 992dbab8030e80dc27ef5b56f91aa057195247b7 Mon Sep 17 00:00:00 2001 From: lynnschmittwilken Date: Fri, 13 Jan 2023 18:08:33 +0100 Subject: [PATCH] added tests for modelfest and updated many component-masks for that; updated RHS-json with new bullseyes --- stimuli/components/circular.py | 2 +- stimuli/components/edges.py | 4 +- stimuli/components/gaussians.py | 2 + stimuli/components/lines.py | 1 + stimuli/papers/__init__.py | 2 +- stimuli/papers/carney1999.py | 115 ++++++++++++------ stimuli/papers/carney1999_noise.tif | Bin 0 -> 56498 bytes tests/papers/RHS2007.json | 8 ++ tests/papers/carney1999.json | 174 ++++++++++++++++++++++++++++ tests/papers/test_carney1999.py | 20 ++++ 10 files changed, 286 insertions(+), 42 deletions(-) create mode 100644 stimuli/papers/carney1999_noise.tif create mode 100644 tests/papers/carney1999.json create mode 100644 tests/papers/test_carney1999.py diff --git a/stimuli/components/circular.py b/stimuli/components/circular.py index c03717c5..ee8b16bf 100644 --- a/stimuli/components/circular.py +++ b/stimuli/components/circular.py @@ -510,7 +510,7 @@ def bessel( stim = { "img": img, - "mask": None, + "mask": np.zeros(shape).astype(int), "visual_size": base["visual_size"], "ppd": base["ppd"], "shape": base["shape"], diff --git a/stimuli/components/edges.py b/stimuli/components/edges.py index 5b306bf3..d004d43e 100644 --- a/stimuli/components/edges.py +++ b/stimuli/components/edges.py @@ -52,7 +52,7 @@ def step_edge( img = np.where(base["rotated"] < base["rotated"].mean(), img, intensity_edges[1]) mask = np.ones(shape) mask = np.where(base["rotated"] < base["rotated"].mean(), mask, 2) - + stim = { "img": img, "mask": mask.astype(int), @@ -117,9 +117,7 @@ def gaussian_edge( img = stim["img"] - intensity_background img = img * window["img"] + intensity_background - mask = stim["mask"] * window["img"] stim["img"] = img - stim["mask"] = mask.astype(int) stim["sigma"] = sigma stim["intensity_background"] = intensity_background return stim diff --git a/stimuli/components/gaussians.py b/stimuli/components/gaussians.py index ebc0bc64..98bb5177 100644 --- a/stimuli/components/gaussians.py +++ b/stimuli/components/gaussians.py @@ -1,5 +1,6 @@ import numpy as np from stimuli.components import image_base +from stimuli.components.shapes import disc __all__ = [ @@ -71,6 +72,7 @@ def gaussian( stim = { "img": gaussian, + "mask": np.zeros(shape).astype(int), "sigma": sigma, "rotation": rotation, "visual_size": base["visual_size"], diff --git a/stimuli/components/lines.py b/stimuli/components/lines.py index 91972baa..2eae3ee0 100644 --- a/stimuli/components/lines.py +++ b/stimuli/components/lines.py @@ -200,6 +200,7 @@ def dipole( ) stim1["img"] = stim1["img"] + stim2["img"] + stim1["mask"] = (stim1["mask"] + stim2["mask"]*2).astype(int) return stim1 diff --git a/stimuli/papers/__init__.py b/stimuli/papers/__init__.py index 4dd65144..22e6e8c9 100644 --- a/stimuli/papers/__init__.py +++ b/stimuli/papers/__init__.py @@ -1 +1 @@ -__all__ = ["RHS2007", "domijan2015", "murray2020"] +__all__ = ["RHS2007", "domijan2015", "murray2020", "carney1999"] diff --git a/stimuli/papers/carney1999.py b/stimuli/papers/carney1999.py index 7231f6b3..3dbd0762 100644 --- a/stimuli/papers/carney1999.py +++ b/stimuli/papers/carney1999.py @@ -32,6 +32,7 @@ import numpy as np import pandas as pd +from pathlib import Path from stimuli.components import grating, gaussians, shapes, checkerboard, lines from stimuli.components.edges import gaussian_edge @@ -91,7 +92,7 @@ mean_lum = 0.5 -df = pd.read_csv("carney1999_data.csv", header=None) +df = pd.read_csv(Path(__file__).parents[0] / "carney1999_data.csv", header=None) participants = df[0] @@ -1140,14 +1141,13 @@ def Subthreshold22(ppd=PPD): stim1 = grating.gabor(**params, frequency=2) stim2 = grating.gabor(**params, frequency=2*np.sqrt(2)) - stim1 = { - "img": stim1["img"]/2 + stim2["img"]/2, - "mask2": stim2["mask"], - "edges2": stim2["edges"], - "frequency2": stim2["frequency"], - "bar_width2": stim2["bar_width"], - "n_bars2": stim2["n_bars"], - } + stim1["img"] = stim1["img"]/2 + stim2["img"]/2 + stim1["mask"] = stim1["mask"].astype(int) + stim1["mask2"] = stim2["mask"].astype(int) + stim1["edges2"] = stim2["edges"] + stim1["frequency2"] = stim2["frequency"] + stim1["bar_width2"] = stim2["bar_width"] + stim1["n_bars2"] = stim2["n_bars"] v = 85 experimental_data = { @@ -1196,14 +1196,12 @@ def Subthreshold23(ppd=PPD): stim1 = grating.gabor(**params, frequency=2) stim2 = grating.gabor(**params, frequency=4) - stim1 = { - "img": stim1["img"]/2 + stim2["img"]/2, - "mask2": stim2["mask"], - "edges2": stim2["edges"], - "frequency2": stim2["frequency"], - "bar_width2": stim2["bar_width"], - "n_bars2": stim2["n_bars"], - } + stim1["img"] = stim1["img"]/2 + stim2["img"]/2 + stim1["mask2"] = stim2["mask"].astype(int) + stim1["edges2"] = stim2["edges"] + stim1["frequency2"] = stim2["frequency"] + stim1["bar_width2"] = stim2["bar_width"] + stim1["n_bars2"] = stim2["n_bars"] v = 89 experimental_data = { @@ -1252,14 +1250,12 @@ def Subthreshold24(ppd=PPD): stim1 = grating.gabor(**params, frequency=4) stim2 = grating.gabor(**params, frequency=4*np.sqrt(2)) - stim1 = { - "img": stim1["img"]/2 + stim2["img"]/2, - "mask2": stim2["mask"], - "edges2": stim2["edges"], - "frequency2": stim2["frequency"], - "bar_width2": stim2["bar_width"], - "n_bars2": stim2["n_bars"], - } + stim1["img"] = stim1["img"]/2 + stim2["img"]/2 + stim1["mask2"] = stim2["mask"].astype(int) + stim1["edges2"] = stim2["edges"] + stim1["frequency2"] = stim2["frequency"] + stim1["bar_width2"] = stim2["bar_width"] + stim1["n_bars2"] = stim2["n_bars"] v = 93 experimental_data = { @@ -1308,14 +1304,12 @@ def Subthreshold25(ppd=PPD): stim1 = grating.gabor(**params, frequency=4) stim2 = grating.gabor(**params, frequency=8) - stim1 = { - "img": stim1["img"]/2 + stim2["img"]/2, - "mask2": stim2["mask"], - "edges2": stim2["edges"], - "frequency2": stim2["frequency"], - "bar_width2": stim2["bar_width"], - "n_bars2": stim2["n_bars"], - } + stim1["img"] = stim1["img"]/2 + stim2["img"]/2 + stim1["mask2"] = stim2["mask"].astype(int) + stim1["edges2"] = stim2["edges"] + stim1["frequency2"] = stim2["frequency"] + stim1["bar_width2"] = stim2["bar_width"] + stim1["n_bars2"] = stim2["n_bars"] v = 97 experimental_data = { @@ -1759,7 +1753,7 @@ def GaborString34(ppd=PPD): return {**stim1, "experimental_data": experimental_data} -def Noise35(ppd=PPD): +def Noise35_random(ppd=PPD): """Noise35 - binary noise x Gaussian, Carney et al (1999) Gaussian window: sy=sx=0.5 deg @@ -1800,6 +1794,52 @@ def Noise35(ppd=PPD): return {**stim, "experimental_data": experimental_data} +def Noise35(ppd=PPD): + """Noise35 - binary noise x Gaussian, Carney et al (1999) + Gaussian window: sy=sx=0.5 deg + + Parameters + ---------- + ppd : Sequence[Number, Number], Number, or None + pixels per degree [vertical, horizontal] + + Returns + ------- + dict[str, Any] + dict with the stimulus (key: "img") and additional keys containing + stimulus parameters + + References + ----------- + Carney, T., Klein, S. A., Tyler, C. W., Silverstein, A. D., Beutter, B., Levi, D., + ... & Eckstein, M. P. (1999). Development of an image/threshold database + for designing and testing human vision models. Proceedings of SPIE, 3644, + 542-551. https://doi.org/10.1117/12.348473 + """ + # Read natural image from Modelfest + img = read_tif(Path(__file__).parents[0] / "carney1999_noise.tif") + img = img / img.max() + + stim = { + "img": img, + "mask": np.zeros(img.shape).astype(int), + "visual_size": (256/PPD, 256/PPD), + "shape": img.shape, + "ppd": PPD, + "intensity_range": (img.min(), img.max()), + } + + v = 169 + experimental_data = { + "participants": participants, + "thresholds1": df[v], + "thresholds2": df[v+1], + "thresholds3": df[v+2], + "thresholds4": df[v+3], + } + return {**stim, "experimental_data": experimental_data} + + def Orientation36(ppd=PPD): """Orientation36 - oriented Gabor, Carney et al (1999) Frequency: 4 cpd @@ -2157,11 +2197,12 @@ def NaturalScene43(ppd=PPD): """ # Read natural image from Modelfest - img = read_tif("carney1999_natural_scene.tif") + img = read_tif(Path(__file__).parents[0] / "carney1999_natural_scene.tif") img = img / img.max() stim = { "img": img, + "mask": np.zeros(img.shape).astype(int), "visual_size": (256/PPD, 256/PPD), "shape": img.shape, "ppd": PPD, @@ -2203,7 +2244,7 @@ def compare(o1, s1, filename): def compare_all(): for stim_name in __all__: func = globals()[stim_name] - o1 = read_tif("./modelfest/" + stim_name + ".tif") + o1 = read_tif(str(Path(__file__).parents[0]) + "/modelfest/" + stim_name + ".tif") s1 = func()["img"] compare(o1, s1, stim_name + ".png") @@ -2212,5 +2253,5 @@ def compare_all(): from stimuli.utils import plot_stimuli stims = gen_all(skip=True) - plot_stimuli(stims, mask=False) + plot_stimuli(stims, mask=True) # compare_all() diff --git a/stimuli/papers/carney1999_noise.tif b/stimuli/papers/carney1999_noise.tif new file mode 100644 index 0000000000000000000000000000000000000000..e5d3c5d515558b7da6be0212be80e63be225ec63 GIT binary patch literal 56498 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