-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvisual_oddball.py
223 lines (195 loc) · 6.62 KB
/
visual_oddball.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
# Triangle-based P300 Speller
# Written in PsychoPy (use conda environment)
# Paradigm inspired by Li et al., 2019
# https://www.frontiersin.org/articles/10.3389/fnhum.2018.00520/full
#
# Wrote this in a couple of hours, but should suffice
# Photosensor circle in lower-right corner turns on for
# duration of task-relevant stimuli.
# An LSL marker is sent on the first frame of stimulus onset.
# Currently triangle point down is standard and point up is target
# '0' marker is standard and '1' marker is oddball/target
# Size of triangle roughly calculated to be 4 visual degrees like paper
# (took some rough measurements for my setup)
# Make sure you check out the link for that.
#
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# !!! MAKE SURE refresh_rate IS SET TO YOUR MONITOR'S REFRESH RATE !!!
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#
# Created........: 13Apr2021 [ollie-d]
# Last Modified..: 13Apr2021 [ollie-d]
from math import atan2, degrees
import psychopy.visual
import psychopy.event
import time
import numpy as np
import pylsl
import random
# Global variables
win = None # Global variable for window (Initialized in main)
mrkstream = None # Global variable for LSL marker stream (Initialized in main)
photosensor = None # Global variable for photosensor (Initialized in main)
triangle = None # Global variable for stimulus (Initialized in main)
fixation = None # Global variable for fixation cross (Initialized in main)
bg_color = [-1, -1, -1]
win_w = 1920
win_h = 1080
refresh_rate = 144. # Monitor refresh rate (CRITICAL FOR TIMING)
#========================================================
# High Level Functions
#========================================================
def Paradigm():
global refresh_rate
global win
global photosensor
global fixation
global triangle
global mrkstream
global bg_color
# Compute sequence of stimuli
sequence = CreateSequence(120, 30)
# Iterate through sequence and perform:
# 250ms bold fixation
# 500ms normal fixation
# 500ms stimulus presentation
# 1000ms black screen
for i, s in enumerate(sequence):
# 250ms Bold fixation cross
fixation.lineWidth = 1
fixation.lineColor = [1, 1, 1]
SetStimulus(fixation, 'on')
for frame in range(MsToFrames(250, refresh_rate)):
fixation.draw()
win.flip()
# 500ms Normal fixation cross
fixation.lineColor = bg_color
for frame in range(MsToFrames(500, refresh_rate)):
fixation.draw()
win.flip()
# 500ms Stimulus presentation (w/ fixation)
RotateTriangle(triangle, 180) # <-- Standard (S)
mrk = pylsl.vectorstr(['0'])
if s == 'T':
RotateTriangle(triangle, 0) # <-- Target (T)
mrk = pylsl.vectorstr(['1'])
SetStimulus(photosensor, 'on')
for frame in range(MsToFrames(500, refresh_rate)):
# Send LSL marker on first frame
if frame == 0:
mrkstream.push_sample(mrk);
photosensor.draw()
triangle.draw()
fixation.draw()
win.flip()
# 1000ms darkness
for frame in range(MsToFrames(1000, refresh_rate)):
win.flip()
#========================================================
# Low Level Functions
#========================================================
def CreateSequence(s, t):
# s is num standards
# t is num targets
# Sequence will be created of len(s+t)
# TT trials are possible (need to add code to prevent them)
seq = []
seq.append(['S' for x in range(s)])
seq.append(['T' for x in range(t)])
seq = listFlatten(seq)
random.seed()
random.shuffle(seq) # shuffles in-place
return seq
def InitTriangle(size=50):
return psychopy.visual.Polygon(
win=win,
edges=3,
units='pix',
radius=size,
lineWidth=3,
lineColor=[1, 1, 1],
fillColor=bg_color,
pos=[0, 0],
ori=0,
name='off'
)
def InitFixation(size=50):
return psychopy.visual.ShapeStim(
win=win,
units='pix',
size = size,
fillColor=[1, 1, 1],
lineColor=[1, 1, 1],
lineWidth = 1,
vertices = 'cross',
name = 'off', # Used to determine state
pos = [0, 0]
)
def InitPhotosensor(size=50):
# Create a circle in the lower right-hand corner
# Will be size pixels large
# Initiate as color of bg (off)
return psychopy.visual.Circle(
win=win,
units="pix",
radius=size,
fillColor=bg_color,
lineColor=bg_color,
lineWidth = 1,
edges = 32,
name = 'off', # Used to determine state
pos = ((win_w / 2) - size, -((win_h / 2) - size))
)
def SetStimulus(stim, c):
# c is state
# Make sure it's either on or off
c = c.lower();
if c != 'on' and c != 'off':
print('Invalid setting'); sys.stdout.flush()
return
if c == 'on':
stim.name = 'on';
stim.color = (1, 1, 1);
if c == 'off':
stim.name = 'off';
stim.color = bg_color;
def RotateTriangle(tri, a):
# Sets rotation to a, does not rotate by a
tri.ori = a
def MsToFrames(ms, fs):
dt = 1000 / fs;
return np.round(ms / dt).astype(int);
def DegToPix(h, d, r, deg):
# Source: https://osdoc.cogsci.nl/3.2/visualangle/
deg_per_px = degrees(atan2(.5*h, d)) / (.5*r)
size_in_px = deg / deg_per_px
return size_in_px
def listFlatten(df):
t = []
for i in range(len(df)):
for j in range(len(df[i])):
t.append(df[i][j])
return t
def CreateMrkStream():
info = pylsl.stream_info('P300_Markers', 'Markers', 1, 0, pylsl.cf_string, 'unsampledStream');
outlet = pylsl.stream_outlet(info, 1, 1)
return outlet;
if __name__ == "__main__":
# Create PsychoPy window
win = psychopy.visual.Window(
screen = 0,
size=[win_w, win_h],
units="pix",
fullscr=True,
color=bg_color,
gammaErrorPolicy = "ignore"
);
# Initialize LSL marker stream
mrkstream = CreateMrkStream();
time.sleep(5)
# Initialize photosensor
photosensor = InitPhotosensor(50)
fixation = InitFixation(30)
triangle = InitTriangle(np.round(DegToPix(20.3, 48.26, 1080, 4))) # ~181
# Run through paradigm
Paradigm()