forked from jamesmcm/StimScripts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tub_stimuli.py
275 lines (243 loc) · 10.5 KB
/
tub_stimuli.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#!/usr/bin/env python
"""
(c) 2012 TU Berlin
Copy of the generating functions for several visual stimuli from TU Berlin
vision package.
Module collecting different functions to create visual stimuli.
"""
import numpy as np
from utils import degrees_to_pixels
def cornsweet(size, ppd, contrast, ramp_width=3, exponent=2.75,
mean_lum=.5):
"""
Create a matrix containing a rectangular Cornsweet edge stimulus.
The 2D luminance profile of the stimulus is defined as
L = L_mean +/- (1 - X / w) ** a * L_mean * C/2 for the ramp and
L = L_mean for the area beyond the ramp.
X is the distance to the edge, w is the width of the ramp, a is a variable
determining the steepness of the ramp, and C is the contrast at the edge,
defined as C = (L_max-L_min) / L_mean.
Parameters
----------
size : tuple of 2 numbers
the size of the matrix in degrees of visual angle
ppd : number
the number of pixels in one degree of visual angle
contrast : number in [0,1]
the contrast at the Cornsweet edge, defined as
(max_luminance - min_luminance) / mean_luminance
ramp_width : number (optional)
the width of the luminance ramp in degrees of visual angle.
Default is 3.
exponent : number (optional)
Determines the steepness of the ramp. Default is 2.75. An
exponent value of 0 leads to a stimulus with uniform flanks.
mean_lum : number
The mean luminance of the stimulus, i.e. the value outside of
the ramp area.
Returns
-------
stim : 2D ndarray
References
----------
The formula and default values are taken from Boyaci, H., Fang, F., Murray,
S.O., Kersten, D. (2007). Responses to Lightness Variations in Early Human
Visual Cortex. Current Biology 17, 989-993.
"""
# compute size as the closest even number of pixel corresponding to the
# size given in degrees of visual angle.
size = np.round(np.tan(np.radians(np.array(size) / 2.)) /
np.tan(np.radians(.5)) * ppd / 2) * 2
stim = np.ones(size) * mean_lum
dist = np.arange(size[1] / 2 )
dist = np.degrees(np.arctan(dist / 2. / ppd * 2 * np.tan(np.radians(.5))))\
* 2
dist /= ramp_width
dist[dist > 1] = 1
profile = (1 - dist) ** exponent * mean_lum * contrast / 2
stim[:, 0:size[1]/2] += profile[::-1]
stim[:, size[1]/2:] -= profile
return stim
def todorovic(coc, vert_rep, horz_rep):
"""
Create a checkerboard illusion by appropriately aligning COC stimuli, in
the way demonstrated by Todorovic (1987).
Parameters
----------
coc : ndarray
The base Craig-O'Brien-Cornsweet stimulus, created with cornsweet().
It should have a small ramp-width compared to its size, moderate
contrast, and be square.
horz_rep : int
number of horizontal repetitions of the cornsweet stimulus.
vert_rep : int
number of vertical repetitions.
Returns
-------
stim: 2D ndarray
References
----------
Todorovic, D. (1987). The Craik-O'Brien-Cornsweet effect: new
varieties and their theoretical implications. Perception & psychophysics,
42(6), 545-60, Plate 4.
"""
stim = np.tile(np.hstack((coc, np.fliplr(coc))), (1, horz_rep / 2))
if horz_rep % 2 != 0:
stim = np.hstack((stim, stim[:, 0:coc.shape[1]]))
stim = np.tile(np.vstack((stim, np.roll(stim, coc.shape[1], 1))),
(vert_rep / 2, 1))
if vert_rep % 2 != 0:
stim = np.vstack((stim, stim[0:coc.shape[0], :]))
return stim
def square_wave(shape, ppd, contrast, frequency, mean_lum=.5, period='ignore',
start='high'):
"""
Create a horizontal square wave of given spatial frequency.
Parameters
----------
shape : tuple of 2 numbers
The shape of the stimulus in degrees of visual angle. (y,x)
ppd : number
the number of pixels in one degree of visual angle
contrast : number in [0,1]
the contrast of the grating, defined as
(max_luminance - min_luminance) / mean_luminance
frequency : number
the spatial frequency of the wave in cycles per degree
mean_lum : number
the mean luminance of the grating, i.e. (max_lum + min_lum) / 2.
The average luminance of the actual stimulus can differ slightly
from this value if the stimulus is not an integer of cycles big.
period : string in ['ignore', 'full', 'half'] (optional)
specifies if the period of the wave is taken into account when
determining exact stimulus dimensions.
'ignore' simply converts degrees to pixesl
'full' rounds down to garuantee a full period
'half' adds a half period to the size 'full' would yield.
Default is 'ignore'.
start : string in ['high', 'low'] (optional)
specifies if the wave starts with a high or low value. Default is
'high'.
Returns
-------
stim : 2D ndarray
the square wave stimulus
"""
if not period in ['ignore', 'full', 'half']:
raise TypeError('size not understood: %s' % period)
if not start in ['high', 'low']:
raise TypeError('start value not understood: %s' % start)
if frequency > ppd / 2:
raise ValueError('The frequency is limited to 1/2 cycle per pixel.')
shape = degrees_to_pixels(np.array(shape), ppd).astype(int)
pixels_per_cycle = int(degrees_to_pixels(1. / frequency / 2, ppd) + .5) * 2
if period is 'full':
shape[1] = shape[1] / pixels_per_cycle * pixels_per_cycle
elif period is 'half':
shape[1] = shape[1] / pixels_per_cycle * pixels_per_cycle + \
pixels_per_cycle / 2
diff = type(mean_lum)(contrast * mean_lum)
high = mean_lum + diff
low = mean_lum - diff
stim = np.ones(shape) * (low if start is 'high' else high)
index = [i + j for i in range(pixels_per_cycle / 2)
for j in range(0, shape[1], pixels_per_cycle)
if i + j < shape[1]]
stim[:, index] = low if start is 'low' else high
return stim
def whites_illusion_bmcc(shape, ppd, contrast, frequency, mean_lum=.5,
start='high'):
"""
Create a version of White's illusion on a square wave, in the style used by
Blakeslee and McCourt (1999).
Parameters
----------
shape : tuple of 2 numbers
The shape of the stimulus in degrees of visual angle. (y,x)
ppd : number
the number of pixels in one degree of visual angle
contrast : number in [0,1]
the contrast of the grating, defined as
(max_luminance - min_luminance) / mean_luminance
frequency : number
the spatial frequency of the wave in cycles per degree
mean_lum : number
the mean luminance of the grating, i.e. (max_lum + min_lum) / 2.
The average luminance of the actual stimulus can differ slightly
from this value if the stimulus is not an integer of cycles big.
start : string in ['high', 'low'] (optional)
specifies if the wave starts with a high or low value. Default is
'high'.
Returns
-------
stim : 2D ndarray
the stimulus
References
----------
Blakeslee B, McCourt ME (1999). A multiscale spatial filtering account of
the White effect, simultaneous brightness contrast and grating induction.
Vision research 39(26):4361-77.
"""
stim = square_wave(shape, ppd, contrast, frequency, mean_lum, 'full',
start)
half_cycle = int(degrees_to_pixels(1. / frequency / 2, ppd) + .5)
stim[stim.shape[0] / 3: stim.shape[0] / 3 * 2,
stim.shape[1] / 2 - 2 * half_cycle:
stim.shape[1] / 2 - half_cycle] = mean_lum
stim[stim.shape[0] / 3: stim.shape[0] / 3 * 2,
stim.shape[1] / 2 + half_cycle:
stim.shape[1] / 2 + 2 * half_cycle] = mean_lum
return stim
def whites_illusion_gil(shape, ppd, contrast, frequency, mean_lum=.5,
start='low'):
"""
Create a version of White's illusion on a square wave, in the style used by
Gilchrist (2006, p. 281)
Parameters
----------
shape : tuple of 2 numbers
The shape of the stimulus in degrees of visual angle. (y,x)
ppd : number
the number of pixels in one degree of visual angle
contrast : number in [0,1]
the contrast of the grating, defined as
(max_luminance - min_luminance) / mean_luminance
frequency : number
the spatial frequency of the wave in cycles per degree
mean_lum : number
the mean luminance of the grating, i.e. (max_lum + min_lum) / 2.
The average luminance of the actual stimulus can differ slightly
from this value if the stimulus is not an integer of cycles big.
start : string in ['high', 'low'] (optional)
specifies if the wave starts with a high or low value. Default is
'high'.
Returns
-------
stim : 2D ndarray
the stimulus
References
----------
Gilchrist A (2006). Seeing Black and White. New York, New York, USA: Oxford
University Press.
"""
stim = square_wave(shape, ppd, contrast, frequency, mean_lum, 'half',
start)
half_cycle = int(degrees_to_pixels(1. / frequency / 2, ppd) + .5)
on_dark_idx = [i for i in range(int(half_cycle * 2.5),
int(stim.shape[1] - half_cycle * .5))
if stim[0, i] < mean_lum]
on_light_idx = [i for i in range(int(half_cycle * 1.5),
int(stim.shape[1] - half_cycle * 1.5))
if stim[0, i] > mean_lum]
stim[stim.shape[0] / 5: stim.shape[0] / 5 * 2, on_light_idx] = mean_lum
stim[stim.shape[0] / 5 * 3: stim.shape[0] / 5 * 4, on_dark_idx] = mean_lum
# randomize border cutoff
max_cut = stim.shape[0] / 10
bg = stim[0, half_cycle]
for start_idx in range(0 if start is 'low' else half_cycle,
stim.shape[1] - half_cycle, 2 * half_cycle):
stim[0 : np.random.randint(max_cut),
start_idx : start_idx + half_cycle] = bg
stim[stim.shape[0] - np.random.randint(max_cut):,
start_idx : start_idx + half_cycle] = bg
return stim