class VisualSpellerVE(MainloopFeedback): """ Visual Speller with six circles like the classical HexOSpell. """ # Triggers: look in Marker END_LEVEL1, END_LEVEL2 = 244, 245 # end of hex levels COPYSPELLING_FINISHED = 246 STIMULUS = [[11, 12, 13, 14, 15, 16], [21, 22, 23, 24, 25, 26]] RESPONSE = [[51, 52, 53, 54, 55, 56], [61, 62, 63, 64, 65, 66]] TARGET_ADD = 20 ERROR_ADD = 100 COUNTDOWN_STIMULI = 239 ERROR_POTENTIAL = 96 # send if error potential is classified def init(self): """ initialize parameters """ self.log_filename = "VisualSpellerVE.log" self.geometry = [0, 0, 1280, 800] ## size self.letterbox_size = (60, 60) self.osc_size = 40 self.font_size_phrase = 60 # the spelled phrase at the top self.font_size_current_letter = 80 # the spelled phrase at the top self.font_size_countdown = 150 # number during countdown self.desired_phrase = "" ## colors: self.bg_color = (0.0, 0.0, 0.0) self.phrase_color = (0.2, 0.0, 1.0) self.current_letter_color = (1.0, 0.0, 0.0) self.countdown_color = (0.2, 0.0, 1.0) self.osc_color = (1, 1, 1) self.letter_set = [ ["A", "B", "C", "D", "E"], ["F", "G", "H", "I", "J"], ["K", "L", "M", "N", "O"], ["P", "Q", "R", "S", "T"], ["U", "V", "W", "X", "Y"], ["Z", "_", ".", ",", "<"], ] self.fullscreen = False self.use_oscillator = True self.offline = True self.copy_spelling = True # in copy-spelling mode, selection of the target symbol is forced self.debug = False self.nCountdown = 5 self.nr_sequences = 6 self.randomize_sequence = True # set to False to present a fixed stimulus sequence self.min_dist = 2 # Min number of intermediate flashes bef. a flash is repeated twice self.stimulus_duration = 0.083 # 5 frames @60 Hz = 83ms flash self.interstimulus_duration = 0.1 self.animation_time = 1 self.wait_before_classify = 1.0 self.feedback_duration = 1.0 self.feedback_ErrP_duration = 1.0 self.wait_after_start = 0.0 # Countdown options self.do_animation = True self.synchronized_countdown = True if self.synchronized_countdown: self.do_animation = False self.countdown_level1 = True self.countdown_level2 = True self.countdown_shapes = {"circle": FilledCircle, "hexagon": FilledHexagon} self.countdown_shape_select = "hexagon" self.countdown_shape_color = (0.7, 0.7, 0.7) self.countdown_shape_on = True self.countdown_blinking_nr = 5 # number of pre-sequence stimuli(1 sec is approx. 5 frames at 60 Hz) self.wait_after_early_stopping = 3 # sec self.abort_trial = False self.output_per_stimulus = True self.use_ErrP_detection = False self.serialtrigger = False self.serialport = serialport.SerialPort(13) self.send_parallel_bak = self.send_parallel if self.debug: msg = "!!! YOU'RE IN DEBUG MODE! CLASSIFICATION WILL BE RANDOM OR KEYBOARD CONTROLLED !!!" self.logger.warning(msg) def pre_mainloop(self): ## logging assert len(self.log_filename) != 0 # 'log_filename' must not be empty string! logger.setLevel(logging.ERROR) handler = logging.FileHandler(self.log_filename, "w") handler.setLevel(logging.INFO) formatter = logging.Formatter("%(asctime)s: %(message)s") handler.setFormatter(formatter) self.logger.setLevel(logging.INFO) self.logger.addHandler(handler) self._nr_elements = 6 self._idx_backdoor = 5 self._init_classifier_output() self._classified_element = -1 self._classified_letter = -1 for s in self.desired_phrase: assert s in [l for ls in self.letter_set for l in ls] # invalid letters in desired phrase! self._spelled_phrase = "" self._spelled_letters = "" self._desired_letters = self.desired_phrase self._copyspelling_finished = False # if self.offline: # self.__idle() # In offline mode: set the first to-be-spelled letter self._spellerHeight = self.geometry[3] - self.letterbox_size[1] self._centerPos = (self.geometry[2] / 2.0, self._spellerHeight / 2.0) self._nr_letters = 0 for i in xrange(len(self.letter_set)): self._nr_letters += len(self.letter_set[i]) self._current_level = 1 # Index of current level self._current_sequence = 0 # Index of current sequence self._current_stimulus = 0 # Index of current stimlus self._current_countdown = self.nCountdown self.random = random.Random(clock()) self._debug_classified = None ## init states: self._state_countdown = True if not self.countdown_level1: self._state_countdown = False self._state_trial = True else: # self._state_countdown = not self.offline self._state_trial = False self._state_classify = False self._state_feedback = False self._state_abort = False ## init containers for VE elements: self._ve_elements = [] ## oscillator state: if not self.use_oscillator: self.osc_color = self.bg_color self.osc_size = 0 ## call subclass-specific pre_mainloop: self.prepare_mainloop() ## build screen elements: self.__init_screen() if self.offline: self.__idle() if self.abort_trial: """ Start listener for abort_trial event eg. """ ## delay after play (might be useful for filters...) pygame.time.wait(int(self.wait_after_start * 1000)) self.logger.info("waiting %d seconds after play." % self.wait_after_start) ## send start trigger: self.send_parallel(marker.RUN_START) self.logger.info("[TRIGGER] %d" % marker.RUN_START) ## error potential classifier: self._ErrP_classifier = None def on_interaction_event(self, data): self.logger.debug("interaction event") serial = data.get("serialtrigger", None) if serial is None: return if serial: self.logger.debug("using serial port") self.send_parallel = self.serialport.send else: self.logger.debug("using parallel port") self.send_parallel = self.send_parallel_bak def post_mainloop(self): """ Sends end marker to parallel port. """ if self.abort_trial: """ Stop listener for abort_trial event """ pass pygame.time.wait(500) self.send_parallel(marker.RUN_END) self.logger.info("[TRIGGER] %d" % marker.RUN_END) pygame.time.wait(500) self._presentation.set(quit=True) self._screen.close() def __init_screen(self): ## create screen: if not self.fullscreen: os.environ["SDL_VIDEO_WINDOW_POS"] = "%d, %d" % (self.geometry[0], self.geometry[1]) self._screen = Screen( size=(self.geometry[2], self.geometry[3]), fullscreen=self.fullscreen, bgcolor=self.bg_color, sync_swap=True ) ## create letter box on top: self._ve_letterbox = Target2D( position=(self._centerPos[0], self.geometry[3] * (1 - 0.01) - self.letterbox_size[1] / 2.0), size=(self.letterbox_size[0], self.letterbox_size[1]), color=self.phrase_color, ) self._ve_innerbox = Target2D( position=(self._centerPos[0], self.geometry[3] * (1 - 0.01) - self.letterbox_size[1] / 2.0), size=(self.letterbox_size[0] - 6, self.letterbox_size[1] - 6), color=self.bg_color, ) self._current_letter_position = ( self._centerPos[0], self.geometry[3] * (1 - 0.015) - self.letterbox_size[1] / 2.0, ) self._ve_current_letter = Text( position=self._current_letter_position, text=(len(self._desired_letters[:1]) == 0 and " " or self._desired_letters[:1]), font_size=self.font_size_current_letter, color=self.current_letter_color, anchor="center", ) self._ve_desired_letters = Text( position=(self._centerPos[0] + 5 + self.letterbox_size[0] / 2.0, self._current_letter_position[1]), text=(len(self._desired_letters[1:]) == 0 and " " or self._desired_letters[1:]), font_size=self.font_size_phrase, color=self.phrase_color, anchor="left", ) self._ve_spelled_phrase = Text( position=(self._centerPos[0] - 5 - self.letterbox_size[0] / 2.0, self._current_letter_position[1]), text=(len(self._spelled_phrase) == 0 and " " or self._spelled_phrase), font_size=self.font_size_phrase, color=self.phrase_color, anchor="right", ) # if we're in free spelling mode, we hide all text fields but # the _ve_spelled_phrase. we also need a multiline # _ve_spelled_phrase instead of the single lined one if self.offline == self.copy_spelling == False: self._spelled_phrase = " " self._ve_spelled_phrase = WrappedText( position=(0, self._current_letter_position[1]), text=(len(self._spelled_phrase) == 0 and " " or self._spelled_phrase), font_size=self.font_size_phrase, color=self.phrase_color, size=(float(self.geometry[2]), float(self.geometry[3])), ) for i in self._ve_letterbox, self._ve_innerbox, self._ve_current_letter, self._ve_desired_letters: i.set(on=False) ## add word box to elementlist: self._ve_elements.extend( [ self._ve_letterbox, self._ve_innerbox, self._ve_current_letter, self._ve_desired_letters, self._ve_spelled_phrase, ] ) ## create countdown: self._ve_countdown = Text( position=self._centerPos, text=" ", font_size=self.font_size_countdown, color=self.countdown_color, anchor="center", on=False, ) ## create countdown shapes self._ve_countdown_shape = self.countdown_shapes[self.countdown_shape_select]( radius=90, position=self._centerPos, color=self.countdown_shape_color, on=False ) ## create oscillator circle: self._ve_oscillator = FilledCircle( position=(self.osc_size / 2 + 10, self.osc_size / 2 + 10), radius=self.osc_size / 2, color=self.osc_color, on=False, ) ## create shapes and letters: self.init_screen_elements() ## add remaining elements to element list: self._ve_elements.extend([self._ve_countdown_shape, self._ve_countdown, self._ve_oscillator]) ## add elements to viewport: self._viewport = Viewport(screen=self._screen, stimuli=self._ve_elements) self._presentation = Presentation( viewports=[self._viewport], handle_event_callbacks=[(pygame.KEYDOWN, self.keyboard_input), (pygame.QUIT, self.__stop)], ) def play_tick(self): """ called every loop, if in play mode. """ self.pre_play_tick() if self._state_countdown: self.pre__countdown() self.__countdown() self.post__countdown() elif self._state_trial: self.pre__trial() self.__trial() self.post__trial() elif self._state_classify: self.pre__classify() self.__classify() self.post__classify() elif self._state_feedback: self.pre__feedback() self.__feedback() self.post__feedback() elif self._state_abort: self.__abort() self.post__abort() else: self.pre__idle() self.__idle() self.post__idle() self.post_play_tick() def __stop(self, *args): self.on_stop() def __idle(self): if self.offline and len(self._desired_letters) > 0: # add new letter: for e in xrange(len(self.letter_set)): for l in xrange(len(self.letter_set[e])): if self._desired_letters[0] == self.letter_set[e][l]: self._classified_element = e self._classified_letter = l if self.countdown_level1: self._state_countdown = True else: self._state_trial = True else: ## otherwise just wait until a new letter is sent: self._presentation.set(go_duration=(0.1, "seconds")) self._presentation.go() def __countdown(self): def blink(): i = 0 while i < self.countdown_blinking_nr: self._ve_countdown_shape.set(on=True) self._ve_countdown.set(text="%d" % self._current_countdown, on=True) self.send_parallel(self.COUNTDOWN_STIMULI) self.logger.info("[TRIGGER] %d" % self.COUNTDOWN_STIMULI) self._presentation.set(go_duration=(self.stimulus_duration, "seconds")) self._presentation.go() self._ve_countdown_shape.set(on=False) self._ve_countdown.set(on=False) self._presentation.set(go_duration=(self.interstimulus_duration, "seconds")) self._presentation.go() i = i + 1 if self._current_countdown == self.nCountdown: self.send_parallel(marker.COUNTDOWN_START) self.logger.info("[TRIGGER] %d" % marker.COUNTDOWN_START) self.set_countdown_screen() self._ve_countdown.set(on=True) self._ve_countdown_shape.set(on=self.countdown_shape_on) self._presentation.set(go_duration=(1, "seconds")) self._ve_countdown.set(text="%d" % self._current_countdown) self._presentation.go() self._current_countdown = (self._current_countdown - 1) % self.nCountdown if self.synchronized_countdown and self._current_countdown == 1: self.set_synchronized_countdown_screen() blink() self._current_countdown = self.nCountdown self.set_standard_screen() self._state_countdown = False self._state_trial = True self._ve_countdown.set(on=False) self._ve_countdown_shape.set(on=False) self._ve_countdown.set(color=self.countdown_color) if self._current_countdown == 0: # Executed only if self.synchronized_countdown = False self._current_countdown = self.nCountdown self.set_standard_screen() pygame.time.wait(10) self._state_countdown = False self._state_trial = True self._ve_countdown_shape.set(on=False) self._ve_countdown.set(on=False) self._ve_countdown.set(color=self.countdown_color) def __trial(self): if self._current_sequence == 0 and self._current_stimulus == 0: # level 1 animation when there is no countdown if self._current_level == 1 and not self.countdown_level1: if self.do_animation: self.set_countdown_screen() self.set_standard_screen() # generate random sequences: if self.randomize_sequence: self.flash_sequence = [] for _ in range(self.nr_sequences): random_flash_sequence( self, set=range(self._nr_elements), min_dist=self.min_dist, seq_len=self._nr_elements ) # or else use fixed sequence: else: self.flash_sequence = range(self._nr_elements) if self.randomize_sequence: currentStimulus = self.flash_sequence[self._current_sequence * self._nr_elements + self._current_stimulus] else: currentStimulus = self.flash_sequence[self._current_stimulus] # set stimulus: self.stimulus(currentStimulus, True) # self._ve_oscillator.set(on=True) if self.abort_trial and self.abort_trial_check(): # restart trial on abort_trial event: self._state_trial = False self._state_abort = True return # check if current stimulus is target and then send trigger: target_add = 0 if len(self._desired_letters) > 0: if self._current_level == 1: if self._desired_letters[:1] in self.letter_set[currentStimulus]: # current stimulus is target group: target_add = self.TARGET_ADD else: if currentStimulus == self._idx_backdoor: # current stimulus is backdoor: if not self._desired_letters[:1] in self.letter_set[self._classified_element]: # we are in the wrong group. backdoor is target: target_add = self.TARGET_ADD else: # current stimulus is no backdoor: if self._desired_letters[:1] == self.letter_set[self._classified_element][currentStimulus]: # current stimulus is target symbol: target_add = self.TARGET_ADD self.send_parallel(self.STIMULUS[self._current_level - 1][currentStimulus] + target_add) self.logger.info("[TRIGGER] %d" % (self.STIMULUS[self._current_level - 1][currentStimulus] + target_add)) # present stimulus: self._presentation.set(go_duration=(self.stimulus_duration, "seconds")) self._presentation.go() # reset to normal: self._ve_oscillator.set(on=False) self.stimulus(currentStimulus, False) # present interstimulus: self._presentation.set(go_duration=(self.interstimulus_duration, "seconds")) self._presentation.go() if self.debug: self.on_control_event({"cl_output": (self.random.random(), currentStimulus + 1)}) ## TODO: check here for classification !!!! if self.output_per_stimulus: # increase self._current_stimulus = (self._current_stimulus + 1) % self._nr_elements if self._current_stimulus == 0: self._current_sequence = (self._current_sequence + 1) % self.nr_sequences # check for end of trial: if self._current_sequence == 0 and self._current_stimulus == 0: # send trigger: if self._current_level == 1: self.send_parallel(self.END_LEVEL1) self.logger.info("[TRIGGER] %d" % self.END_LEVEL1) else: self.send_parallel(self.END_LEVEL2) self.logger.info("[TRIGGER] %d" % self.END_LEVEL2) # decide how to continue: self._state_trial = False self._state_classify = True else: # increase self._current_stimulus = (self._current_stimulus + 1) % self._nr_elements if self._current_stimulus == 0: self._current_sequence = (self._current_sequence + 1) % self.nr_sequences if self.check_classification(self._current_sequence + 1): self._state_trial = False self._state_classify = True pygame.time.wait(self.wait_after_early_stopping * 1000) if self._current_sequence == 0 and self._current_stimulus == 0: # send trigger: if self._current_level == 1: self.send_parallel(self.END_LEVEL1) self.logger.info("[TRIGGER] %d" % self.END_LEVEL1) else: self.send_parallel(self.END_LEVEL2) self.logger.info("[TRIGGER] %d" % self.END_LEVEL2) # decide how to continue: self._state_trial = False self._state_classify = True def check_classification(self, nr): # print self._classifier_output means = [None] * self._nr_elements minimum = maxint classified = None for ii in range(self._nr_elements): means[ii] = sum(self._classifier_output[ii]) / nr if means[ii] < minimum: minimum = means[ii] classified = ii + 1 print "\n**** Class: %d (mean=%f)\n" % (classified, means[classified - 1]) return classified def __classify(self): ## wait until all classifier outputs are received: self._presentation.set(go_duration=(self.wait_before_classify, "seconds")) self._presentation.go() if self.offline: if self._current_level == 1: classified = self._classified_element else: classified = self._classified_letter elif not self._debug_classified == None: classified = self._debug_classified self._debug_classified = None else: if self.output_per_stimulus: nClassified = sum([len(self._classifier_output[i]) for i in xrange(self._nr_elements)]) if nClassified < self._nr_elements * self.nr_sequences: pygame.time.wait(20) print "not enough classifier-outputs received! (something may be wrong)" return ## classify and set output: means = [None] * self._nr_elements minimum = maxint classified = None for ii in range(self._nr_elements): means[ii] = sum(self._classifier_output[ii]) / self.nr_sequences if means[ii] < minimum: minimum = means[ii] classified = ii print "\n**** Class: %d (mean=%f)\n" % (classified + 1, means[classified]) else: means = [None] * self._nr_elements minimum = maxint classified = None for ii in range(self._nr_elements): means[ii] = sum(self._classifier_output[ii]) / self.nr_sequences if means[ii] < minimum: minimum = means[ii] classified = ii print "\n**** Class: %d (mean=%f)\n" % (classified + 1, means[classified]) ## Reset classifier output to empty lists self._init_classifier_output() error_add = 0 ## evaluate classification: if self._current_level == 1: self._classified_element = classified if len(self._desired_letters) > 0 and not self._desired_letters[:1] in self.letter_set[classified]: # wrong group selected: error_add = self.ERROR_ADD else: self._classified_letter = classified if self._classified_letter == self._idx_backdoor: ## backdoor classified: if ( len(self._desired_letters) > 0 and self._desired_letters[:1] in self.letter_set[self._classified_element] ): # backdoor selection wrong: error_add = self.ERROR_ADD else: ## no backdoor classified: spelled_letter = self.letter_set[self._classified_element][self._classified_letter] if len(self._desired_letters) > 0 and spelled_letter != self._desired_letters[:1]: # wrong letter spelled: error_add = self.ERROR_ADD ## send response trigger: self.send_parallel(self.RESPONSE[self._current_level - 1][classified] + error_add) self.logger.info("[TRIGGER] %d" % (self.RESPONSE[self._current_level - 1][classified] + error_add)) self._state_classify = False self._state_feedback = True def __feedback(self): self._state_feedback = False ## call subclass method: self.feedback() ## check ErrP classification: if self.use_ErrP_detection: t = 0 while self._ErrP_classifier is None and t < 1000: t += 50 pygame.time.wait(50) if self._ErrP_classifier is None: print "no ErrP classifier received! " if self._ErrP_classifier: self.send_parallel(self.ERROR_POTENTIAL) self.logger.info("[TRIGGER] %d" % (self.ERROR_POTENTIAL)) ## call subclass method: if not self.countdown_level2: self.switch_level() ## update phrases: if ( (self._current_level == 2) and ( # only update, if we are at the end of level 2, self._classified_letter != self._idx_backdoor or self.copy_spelling ) and ( # if copyspelling off, we have no backdoor selected self._ErrP_classifier is None or not self._ErrP_classifier ) ): # no ErrP was detected (or ErrP detection is off) spelled_letter = "" if self.copy_spelling: ## in copy spelling we force the desired letter to be spelled if len(self._desired_letters) > 0: spelled_letter = self._desired_letters[:1] else: print "??? moved beyond desired phrase in copy spelling ???" else: spelled_letter = self.letter_set[self._classified_element][self._classified_letter] ## update desired phrase: if len(self._desired_letters) > 0: if spelled_letter == self._desired_letters[:1]: # correct letter spelled: self._desired_letters = self._desired_letters[1:] # remove first letter else: # wrong letter spelled: if spelled_letter == "<": self._desired_letters = self._spelled_phrase[-1:] + self._desired_letters else: self._desired_letters = "<" + self._desired_letters if len(self._desired_letters) == 0: self._copyspelling_finished = True ## update spelled phrase: self._spelled_letters += spelled_letter if spelled_letter == "<": self._spelled_phrase = self._spelled_phrase[:-1] else: self._spelled_phrase += spelled_letter ## update screen phrases: self.logger.info("Current Phrase:") self.logger.info(self._spelled_phrase) self._ve_spelled_phrase.set(text=(len(self._spelled_phrase) == 0 and " " or self._spelled_phrase)) self._ve_current_letter.set(text=(len(self._desired_letters[:1]) == 0 and " " or self._desired_letters[:1])) self._ve_desired_letters.set( text=(len(self._desired_letters[1:]) == 0 and " " or self._desired_letters[1:]) ) if self.use_ErrP_detection and self._ErrP_classifier: self._state_trial = True else: if self._current_level == 1: # continue with level2 trial: if self.countdown_level2: self._state_countdown = True else: self._state_trial = True elif not self.offline: # start countdown if self.countdown_level1: self._state_countdown = True else: self._state_trial = True # set new level: self._current_level = 3 - self._current_level ## reset ErrP_classifier: self._ErrP_classifier = None # check copyspelling: if self._copyspelling_finished: self._copyspelling_finished = False self.on_control_event({"print": 0}) # print desired phrase self.on_control_event({"print": 1}) # print spelled phrase self.on_control_event({"print": 2}) # print all spelled letters self.send_parallel(self.COPYSPELLING_FINISHED) self.logger.info("[TRIGGER] %d" % (self.COPYSPELLING_FINISHED)) pygame.time.wait(50) def __abort(self): # play warning sound def sine_array_onecycle(hz, peak, sample_rate): length = sample_rate / float(hz) omega = NP.pi * 2 / length xvalues = NP.arange(int(length)) * omega return peak * NP.sin(xvalues) def sine_array(hz, peak, samples_rate): return NP.resize(sine_array_onecycle(hz, peak, sample_rate), (sample_rate,)) sample_rate = 44100 pygame.mixer.init(sample_rate, -16, 2) # 44.1kHz, 16-bit signed, stereo f = sine_array(8000, 1, sample_rate) f = NP.array(zip(f, f)) sound = pygame.sndarray.make_sound(f) channel = sound.play(-1) channel.set_volume(0.2, 0.2) pygame.time.delay(1000) sound.stop() if self._current_level == 1 and self.countdown_level1: self._state_countdown = True elif self._current_level == 2 and self.countdown_level2: self._state_countdown = True else: self._state_trial = True self._init_classifier_output() def _init_classifier_output(self): ## Empty lists self._classifier_output = [list() for _ in xrange(self._nr_elements)] def abort_trial_check(self): """ Check if event is an abort trial event """ return False def keyboard_input(self, event): if event.key == pygame.K_ESCAPE: self.on_stop() elif event.key == pygame.K_KP_ENTER: self.on_control_event({"print": 0}) # print desired phrase self.on_control_event({"print": 1}) # print spelled phrase self.on_control_event({"print": 2}) # print all spelled letters elif event.key == pygame.K_DELETE: # The DELETE key empties the spelled text shown this works # only in free spelling mode (i.e. offline and copy_spelling # are set to False) if self.offline == self.copy_spelling == False: self.logger.info("Clearing Text.") self._spelled_phrase = " " elif self.debug: if ( (event.key >= pygame.K_a and event.key <= pygame.K_z) or (event.key == pygame.K_LESS) or (event.key == pygame.K_PERIOD) or (event.key == pygame.K_COMMA) ): self.on_control_event({"new_letter": chr(event.key).upper()}) elif event.key == pygame.K_MINUS: self.on_control_event({"new_letter": chr(pygame.K_UNDERSCORE)}) elif event.key == pygame.K_BACKSPACE: self.on_control_event({"new_letter": chr(pygame.K_LESS)}) elif event.key == pygame.K_SPACE: self.on_control_event({"new_letter": chr(pygame.K_UNDERSCORE)}) elif event.key == pygame.K_UP and self.use_ErrP_detection: self.on_control_event({"cl_output": (1, 7)}) elif event.key == pygame.K_DOWN and self.use_ErrP_detection: self.on_control_event({"cl_output": (0, 7)}) if not self.offline: if event.key >= pygame.K_0 and event.key <= pygame.K_5: self._debug_classified = int(chr(event.key)) elif event.key >= pygame.K_KP0 and event.key <= pygame.K_KP5: self._debug_classified = int(chr(event.key - 208)) def on_control_event(self, data): self.logger.info("[CONTROL_EVENT] %s" % str(data)) if data.has_key(u"cl_output"): # classification output was sent: score_data = data[u"cl_output"] cl_out = score_data[0] iSubstim = int(score_data[1]) # evt auch "Subtrial" if iSubstim in range(1, 7): self._classifier_output[iSubstim - 1].append(cl_out) elif self.use_ErrP_detection: self._ErrP_classifier = cl_out elif data.has_key("new_letter"): # get new letter to spell: self._desired_letters += data["new_letter"] self._ve_current_letter.set(text=(len(self._desired_letters[:1]) == 0 and " " or self._desired_letters[:1])) self._ve_desired_letters.set( text=(len(self._desired_letters[1:]) == 0 and " " or self._desired_letters[1:]) ) elif data.has_key(u"print"): if data[u"print"] == 0: self.logger.info("[DESIRED_PHRASE] %s" % self.desired_phrase) elif data[u"print"] == 1: self.logger.info("[SPELLED_PHRASE] %s" % self._spelled_phrase) elif data[u"print"] == 2: self.logger.info("[SPELLED_LETTERS] %s" % self._spelled_letters) """ ========================== == METHODS TO OVERLOAD: == ========================== """ def init_screen_elements(self): """ overwrite this function in subclass. """ pass def prepare_mainloop(self): """ overwrite this function in subclass. """ pass def set_countdown_screen(self): """ set screen how it should look during countdown. overwrite this function in subclass. """ pass def set_standard_screen(self): """ set screen elements to standard state. overwrite this function in subclass. """ pass def set_synchronized_countdown_screen(self): """ set screen elements to for the synchronized countdown. overwrite this function in subclass. """ pass def stimulus(self, i_element, on=True): """ turn on/off the stimulus elements and turn off/on the normal elements. overwrite this function in subclass. """ pass def feedback(self): """ set screen how it should look during feedback presentation. overwrite this function in subclass. """ pass def switch_level(self): """ overwrite this function in subclass. """ pass def pre_play_tick(self): pass def post_play_tick(self): pass def pre__countdown(self): pass def post__countdown(self): pass def pre__trial(self): pass def post__trial(self): pass def pre__classify(self): pass def post__classify(self): pass def pre__feedback(self): pass def post__feedback(self): pass def pre__idle(self): pass def post__idle(self): pass def post__abort(self): pass
class Simulation: def __init__(self, screen): self.state = SimulationState() self.state.v = 15 self.screen = screen self.center = screen.size[0] / 2.0 self.doneSetup = False self.quit = False def init_state(self): self.state.th = 0 self.state.x = 0 self.state.z = 0 def wait_for_key(self, t): #event = pygame.event.poll() #while event != pygame.NOEVENT: # if event == pygame.KEYDOWN: # self.askForNext.quit = True # event = pygame.event.poll() pygame.event.pump() if any(pygame.key.get_pressed()): print "Got key" self.askForNext.parameters.quit = True self.askForNext.parameters.enter_go_loop = True def update(self, t): # Estimate the current frame rate try: dt = self.frame_timer.get_average_ifi_sec() except RuntimeError: dt = 0.01 # Map the pointer position to angular velocity of +/- 90 degrees/s curr_pos = pygame.mouse.get_pos() self.pos_ring.add(curr_pos[0]) pos = self.pos_ring.head() center = self.center self.state.th = self.state.th + dt * (-(math.pi) / 2.0 * (pos - center) / center) # Update steering wheel self.wheel.set(angle=-90.0 * (curr_pos[0] - center) / center) th = self.state.th x = self.state.x z = self.state.z self.outf.write("%f\t%u\t%u\t%u\t%f\t%f\t%f\n" % (t, curr_pos[0], curr_pos[1], pos, th, x, z)) # this is a left handed camera transform, the right handed ones that are # built in to visionegg were not working for me. # Translate, then rotate about the y-axis by our current heading angle viewXfrm = numpy.matrix([[math.cos(th), 0.0, math.sin(th), 0.0], [0.0, 1.0, 0.0, 0.0], [-math.sin(th), 0.0, math.cos(th), 0.0], [ -x * math.cos(th) + z * math.sin(th), 0.0, -x * math.sin(th) - z * math.cos(th), 1.0 ]]) # Make a step in the direction of current heading self.state.x = x + self.state.v * dt * math.sin(-th) self.state.z = z - self.state.v * dt * math.cos(-th) self.camera_matrix.parameters.matrix = viewXfrm def doSim(self, trial, road, duration, tau, doEyetrack): # Measure sample rate in order to calculate delay buffer sample_rate = self.screen.measure_refresh_rate(2.0) print "Sample rate: " + str(sample_rate) #sample_rate = 60 self.doEyetrack = doEyetrack self.pos_ring = RingBuffer(self.center, int(math.floor(tau * sample_rate)) + 1) print("Ring Buffer:: size: " + str(self.pos_ring.size)) if doEyetrack: import pylink from EyeLinkCoreGraphicsVE import EyeLinkCoreGraphicsVE self.tracker = pylink.EyeLink() if self.tracker == None: print "Error: Eyelink is not connected" sys.exit() genv = EyeLinkCoreGraphicsVE(self.screen, self.tracker) pylink.openGraphicsEx(genv) #Opens the EDF file. edfFileName = "TRIAL" + str(trial) + ".EDF" self.tracker.openDataFile(edfFileName) pylink.flushGetkeyQueue() self.tracker.sendCommand("screen_pixel_coords = 0 0 %d %d" % (VisionEgg.config.VISIONEGG_SCREEN_W, VisionEgg.config.VISIONEGG_SCREEN_H)) tracker_software_ver = 0 eyelink_ver = self.tracker.getTrackerVersion() if eyelink_ver == 3: tvstr = self.tracker.getTrackerVersionString() vindex = tvstr.find("EYELINK CL") tracker_software_ver = int( float(tvstr[(vindex + len("EYELINK CL")):].strip())) if eyelink_ver >= 2: self.tracker.sendCommand("select_parser_configuration 0") if eyelink_ver == 2: #turn off scenelink camera stuff self.tracker.sendCommand("scene_camera_gazemap = NO") else: self.tracker.sendCommand("saccade_velocity_threshold = 35") self.tracker.sendCommand( "saccade_acceleration_threshold = 9500") # set EDF file contents self.tracker.sendCommand( "file_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON" ) if tracker_software_ver >= 4: self.tracker.sendCommand( "file_sample_data = LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS,HTARGET" ) else: self.tracker.sendCommand( "file_sample_data = LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS") # set link data (used for gaze cursor) self.tracker.sendCommand( "link_event_filter = LEFT,RIGHT,FIXATION,SACCADE,BLINK,BUTTON") if tracker_software_ver >= 4: self.tracker.sendCommand( "link_sample_data = LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS,HTARGET" ) else: self.tracker.sendCommand( "link_sample_data = LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS") if not self.doneSetup: self.tracker.doTrackerSetup() self.doneSetup = True else: while 1: try: error = self.tracker.doDriftCorrect( self.screen.size[0] / 2, self.screen.size[1] / 2, 1, 1) if error != 27: # ?? from example break else: self.tracker.doTrackerSetup() except: break self.screen.parameters.bgcolor = 106.0 / 255.0, 147.0 / 255.0, 0.0 # Load road data from file and create an image roadArray = numpy.loadtxt('road' + str(road) + '.txt') # Convert to a Path roadPath = ImagePath.Path( map(lambda xy: (xy[0], xy[1]), roadArray.tolist())) # Use Path to create a plot of the road im = Image.new("RGB", (2000, 100), (50, 50, 50)) draw = ImageDraw.Draw(im) # draw each side of the road separately draw.line(roadPath[:4000], fill=(200, 200, 200)) draw.line(roadPath[4000:], fill=(200, 200, 200)) del draw # Lay out a road texture in the x-z plane roadTexture = Texture(im) del im eye_height = 2.5 vertices = [(-10, -eye_height, 0), (-10, -eye_height, -1000), (10, -eye_height, 0), (10, -eye_height, -1000)] rect = TextureStimulus3D(texture=roadTexture, lowerleft=vertices[0], lowerright=vertices[1], upperleft=vertices[2], upperright=vertices[3]) # We will use these later for our camera transforms self.camera_matrix = ModelView() self.frame_timer = FrameTimer() self.outf = open( 'steersim-' + str(trial) + '-' + str(road) + '-out.txt', 'wb') # Vewport for the road viewport3D = Viewport( screen=self.screen, projection=SimplePerspectiveProjection(fov_x=75.2), camera_matrix=self.camera_matrix, stimuli=[rect]) # Construct a sky sky_l = 0 sky_r = self.screen.size[0] sky_t = self.screen.size[1] sky_b = self.screen.size[1] / 2 sky_vertices = [(sky_l, sky_t, 0), (sky_r, sky_t, 0), (sky_r, sky_b, 0), (sky_l, sky_b, 0)] sky = Rectangle3D(color=(144.0 / 255.0, 190.0 / 255.0, 1.0), vertex1=sky_vertices[0], vertex2=sky_vertices[1], vertex3=sky_vertices[2], vertex4=sky_vertices[3]) wheelTexture = Texture('wheel.png') self.wheel = TextureStimulus(texture=wheelTexture, internal_format=gl.GL_RGBA, position=(self.center, -75), anchor='center') # display the sky in its own viewport viewport2D = Viewport(screen=self.screen) viewport2D.parameters.stimuli = [sky, self.wheel] self.init_state() askText = Text(text='Press a key to start', anchor='center', position=(self.center, self.screen.size[1] / 2)) splash = Viewport(screen=self.screen) splash.parameters.stimuli = [askText] self.askForNext = Presentation(go_duration=(0.5, 'seconds'), viewports=[splash]) self.askForNext.add_controller( None, None, FunctionController(during_go_func=self.wait_for_key)) self.askForNext.parameters.enter_go_loop = True self.askForNext.run_forever() self.simPres = Presentation(go_duration=(duration, 'seconds'), viewports=[viewport3D, viewport2D], handle_event_callbacks=[ (pygame.KEYDOWN, self.check_keypress) ]) self.simPres.add_controller( None, None, FunctionController(during_go_func=self.update)) if doEyetrack: startTime = pylink.currentTime() self.tracker.sendMessage("SYNCTIME %d" % (pylink.currentTime() - startTime)) error = self.tracker.startRecording(1, 1, 1, 1) self.tracker.sendMessage("PRES %d START" % (trial)) self.simPres.go() if doEyetrack: self.tracker.sendMessage("PRES %d END" % (trial)) self.tracker.stopRecording() # File transfer and cleanup! self.tracker.setOfflineMode() pylink.msecDelay(500) #Close the file and transfer it to Display PC self.tracker.closeDataFile() self.tracker.receiveDataFile(edfFileName, edfFileName) self.outf.close() if self.quit: raise SystemExit def check_keypress(self, event): if event.key == pygame.K_q: self.quit = True self.simPres.set(go_duration=(0.0, 'seconds')) elif event.key == pygame.K_n: self.simPres.set(go_duration=(0.0, 'seconds')) elif event.key == pygame.K_UP: self.state.v += 1 elif event.key == pygame.K_DOWN: self.state.v -= 1
class SimpleVisionEgg: keyboard_controller = None trigger_controller = None screen = None presentation = None keys = None presses = None releases = None def __init__(self): """We break up initialization a bit as we need to go back and forth with some information. In this case, we need screen size before specifying the stimuli""" # pasted in from where it used to be at the beginning of the script # used to be outside of any methods... VisionEgg.start_default_logging() VisionEgg.watch_exceptions() # get screen size for setting fullscreen resolution # comment this block out if you don't want to use full-screen. screen = pygame.display.set_mode((0,0)) WIDTH, HEIGHT = screen.get_size() pygame.quit() VisionEgg.config.VISIONEGG_SCREEN_W = WIDTH VisionEgg.config.VISIONEGG_SCREEN_H = HEIGHT self.screen = get_default_screen() self.keys = [] self.presses = [] self.releases = [] def set_stimuli(self, stimuli, trigger=None, kb_controller=False): """Now that we have our stimuli, we initialize everything we can""" viewport = Viewport(screen=self.screen, size=self.screen.size, stimuli=stimuli) # We disable "check_events" so that we don't lose "instantaneous" key # presses and can check these in our Response classes self.presentation = Presentation(viewports=[viewport], check_events=False) if trigger: trigger_controller = KeyboardTriggerInController(trigger) self.presentation.add_controller(self.presentation, 'trigger_go_if_armed', trigger_controller) self.presentation.set(trigger_go_if_armed=0) if kb_controller: self.keyboard_controller = KeyboardResponseController() self.presentation.add_controller(None, None, self.keyboard_controller) def set_functions(self, update=None, pause_update=None): """Interface for cognac.StimulusController or similar""" self.presentation.add_controller(None, None, FunctionController(during_go_func=update, between_go_func=pause_update, return_type=NoneType) ) def go(self, go_duration=('forever',)): self.presentation.parameters.go_duration = go_duration self.presentation.go() def pause(self): self.presentation.parameters.go_duration = (0, 'frames') def get_new_response(self, t, min_interval=2.0 / 60, releases=False): """(key, press) = get_new_response(self, t, min_interval=2.0 / 60) DEPRECATED! Use this function to get responses from the keyboard controller in real time. Returns (None, None) if no new response is available. Maintains three instance variables - keys, presses and releases, which you can also access directly (but they won't be updated during loops where you don't call this function) This function makes a number of assumptions and is a little brittle right now. By not hard-coding the min_interval and maybe using key presses and release events directly, we'd have a much better function. But I don't really care right now. DJC """ raise DeprecationWarning("please use pygame directly, as in" + "StimController.Response") # Note - this is deprecated anyway, but it'd probably make more sense to # use the keyboard_controller.get_responses() to simply get the keys # that are down _right_now_ press_time = self.keyboard_controller.get_time_last_response_since_go() key = self.keyboard_controller.get_last_response_since_go() # Our first response! if len(self.keys) == 0: if key: self.keys.append(key) self.presses.append(press_time) self.releases.append(None) if releases: return (key, None) else: return (key, press_time) else: return (None, None) # We haven't seen a key press for min_interval if t >= press_time + min_interval and not self.releases[-1]: # This is only approximate! self.releases[-1] = t if releases: return (self.keys[-1], t) else: return (None, None) # We've seen a release, or we see a new key if (self.releases[-1] and press_time > self.releases[-1]) or \ key != self.keys[-1]: if not self.releases[-1]: self.releases[-1] = press_time self.keys.append(key) self.presses.append(press_time) self.releases.append(None) if releases: return (key, None) else: return (key, press_time) return (None, None) def get_responses(self, timeToSubtract=0, min_interval=2.0/60): """ Use this function to post-process the results of a KeyboardController VisionEgg's keyboard libraries records a keypress and timestamp every time something is down. So if a key is held down for 100 ms, there will be an entry in the keylist for every sample during that 100ms. This is a bit much; I'd rather just save onsets and offsets for every key. This function evaluates that. """ ''' If we're using the FORP, this isn't necessary, as events have no duration; they are represented as instantaneous keypresses. -- John ''' response = self.keyboard_controller.get_responses_since_go() responseTime = self.keyboard_controller.get_time_responses_since_go() # If I've only got one item in my response list, then it's silly to worry about onset/offset. Just keep it. if len(response) < 2: return (response,responseTime) # Save the first response, as by definition that's the first onset: goodResp = [response[0]] goodRespTime = [responseTime[0]-timeToSubtract] # Now step through every other item in the response list to check for unique-ness. for i in range(1,len(responseTime)): if (not(response[i] == response[i-1]) or \ (responseTime[i] - responseTime[i-1] > \ min_interval)): # ie, if something changed, or we have a long gap: offsetResp = [] # we might want to save an offset for item in response[i-1]: # loop through last item's data if (responseTime[i] - responseTime[i-1] < \ min_interval) and \ not(item in response[i]): # Bit clunky. Basically, holding down a key while pressing another creates # a unique response. So if you only let up one of those keys, code the # offset just for that key. offsetResp.append(item+'_Off') else: # it's been long enough that everything that was on should be called off. offsetResp.append(item+'_Off') if len(offsetResp) > 0: # If there's offset stuff to worry about, save it. goodResp.append(offsetResp) goodRespTime.append(responseTime[i-1]-timeToSubtract) # Save the new (onset) response. goodResp.append(response[i]) goodRespTime.append(responseTime[i]-timeToSubtract) # The final event should be an offset for whatever was down. offsetResp = [] for item in response[-1]: offsetResp.append(item+'_Off') goodResp.append(offsetResp) #goodResp.append(response[-1]+'_Off') goodRespTime.append(responseTime[-1]-timeToSubtract) return (goodResp, goodRespTime)
class VisionEggView(object): """ This class handles VisionEgg internals and the creation of common/standard stimuli like centered words, a fixation cross or a countdown. Inherit this and pass the type to VisionEggFeedback for customization. """ def __init__(self): self.__init_attributes() def __init_attributes(self): """ Setup internal attributes. """ self._logger = logging.getLogger('VisionEggView') self._logger.addHandler(logging.FileHandler('log')) self._screen_acquired = False self._viewports = [] def set_trigger_function(self, trigger): self._trigger = trigger def set_event_handlers(self, event_handlers): """ Set pygame/VisionEgg event handler function. """ self._event_handlers = event_handlers def set_iterator_semaphore(self, flag): """ Specify the object to be used as semaphore for iterators. See L{Switcherator} for more. """ self._iter = lambda it: Switcherator(flag, it) def update_parameters(self, **kwargs): """ Apply new parameters set from pyff. """ for k, v in kwargs.iteritems(): setattr(self, '_' + k, v) if self._screen_acquired: self.reinit() def acquire(self): """ Allow L{update_parameters} initialize VisionEgg. """ self._screen_acquired = True def reinit(self): """ Initialize VisionEgg objects. """ self.__init_screen() self.__init_presentation() self.__init_viewports() self.init() self.__init_text() def init(self): """ Overload this for additional custom VisionEgg initialization. """ pass def __init_screen(self): """ Create the VisionEgg Screen object using the pyff configuration parameters 'fullscreen' and 'geometry' and the font and background colors according to parameters 'font_color_name' and 'bg_color'. """ params = {'fullscreen': self._fullscreen, 'sync_swap': True} if not self._fullscreen: os.environ['SDL_VIDEO_WINDOW_POS'] = '%d, %d' % (self._geometry[0], self._geometry[1]) params['size'] = self._geometry[2:] self.screen = Screen(**params) self._set_bg_color() self._set_font_color() def __init_presentation(self): """ Provide a standard presentation object. """ self.presentation = Presentation( handle_event_callbacks=self._event_handlers) def __init_viewports(self): """ Provide a standard viewport. """ self._standard_viewport = Viewport(screen=self.screen) self.add_viewport(self._standard_viewport) def __init_text(self): """ Provide a text in the screen center for standard stimuli and fixation cross etc. """ sz = self.screen.size self._center_text = self.add_color_word(position=(sz[0] / 2., sz[1] / 2.), font_size=self._font_size) def add_viewport(self, viewport): """ Add an additional custom viewport object to the list of viewports. """ self._viewports.append(viewport) self.presentation.set(viewports=self._viewports) def clear_stimuli(self): """ Remove all existing stimuli in the standard viewport. """ self.set_stimuli() def add_stimuli(self, *stimuli): """ Add additional custom stimulus objects to the list of stimuli. TextList instances need their own Viewport, as they consist of multiple stimulus objects that are deleted everytime they change, and so the Viewport needs to have a reference to the containing TextList, otherwise they get lost. """ text_lists = filter(lambda s: isinstance(s, TextList), stimuli) if text_lists: for text in text_lists: self.add_viewport(Viewport(screen=self.screen, stimuli=text)) stimuli = filter(lambda s: not isinstance(s, TextList), stimuli) stimuli = self._standard_viewport.parameters.stimuli + list(stimuli) if stimuli: self.set_stimuli(*stimuli) def set_stimuli(self, *stimuli): """ Set the list of stimulus objects. """ self._standard_viewport.set(stimuli=list(stimuli)) def add_text_stimulus(self, text, font_size=None, **kw): if not kw.has_key('anchor'): kw['anchor'] = 'center' font_size = font_size or self._font_size txt = VisionEgg.Text.Text(text=text, font_size=font_size, **kw) self.add_stimuli(txt) return txt def add_color_word(self, text='', font_size=None, **kw): font_size = font_size or self._font_size txt = ColorWord(text=text, symbol_size=font_size, **kw) self.add_stimuli(txt) return txt def add_image_stimulus(self, **kw): if not kw.has_key('anchor'): kw['anchor'] = 'center' img = TextureStimulus(**kw) self.add_stimuli(img) return img def _create_color(self, name): try: if isinstance(name, tuple): return Color(*name).normalize() else: return Color(str(name)).normalize() except ValueError: self._logger.warn('No such pygame.Color: %s' % str(name)) def _set_font_color(self): """ Set the standard font color by pygame name. """ self._font_color = (self._create_color(self._font_color_name) or Color(1, 1, 1, 255).normalize()) def _set_bg_color(self): """ Set the standard background color by pygame name. """ c = (self._create_color(self._bg_color) or Color(0, 0, 0, 255).normalize()) self.screen.set(bgcolor=c) def present_frames(self, num_frames): """ Launch the presentation main loop for a given number of frames. """ self.presentation.set(go_duration=(num_frames, 'frames')) self.presentation.go() def present(self, sec): self.presentation.set(go_duration=(sec, 'seconds')) self.presentation.go() def update(self): """ Repaint the canvas for one frame to update changed stimuli. """ self.present_frames(1) def center_word(self, text, color=None): """ Set the standard word in the screen center. """ self._center_text.set(text=text) self._center_text.set(colors=color or (self._font_color for l in self._center_text)) def clear_center_word(self): """ Remove the center word from the screen. """ self.center_word('') self.update() def present_center_word(self, text, seconds, color=None): self.center_word(text, color) self.present(seconds) self.clear_center_word() def ask(self, question=True): """ Loop indefinitely until answered() is called. If question is True, a question mark is shown in the center. """ if question: self.center_word('?') self.presentation.run_forever() self.presentation.set(quit=False) self.clear_center_word() def answered(self): """ Abort the current presentation (usually the question mark) after subject input. For thread safety, the screen shouldn't be changed here. """ self.presentation.set(quit=True) def show_fixation_cross(self): """ Display the pyff parameter 'fixation_cross_symbol' for the period of time given by pyff parameter 'fixation_cross_time'. """ self.center_word(self._fixation_cross_symbol) self._trigger(marker.FIXATION_START, wait=True) self.present(self._fixation_cross_time) self._trigger(marker.FIXATION_END, wait=True) def countdown(self): """ Display a countdown according to pyff parameters 'countdown_start' and 'countdown_symbol_duration'. """ self._trigger(marker.COUNTDOWN_START, wait=True) for i in self._iter(reversed(xrange(self._countdown_start + 1))): self.center_word(str(i)) self.present(self._countdown_symbol_duration) self._trigger(marker.COUNTDOWN_END, wait=True) self.clear_center_word() def close(self): """ Shut down the screen. """ self._screen_acquired = False self.screen.close() def quit(self): """ Stop the presentation. """ self.presentation.set(quit=True) self.presentation.set(go_duration=(1, 'frames'))
class VisualSpellerVE(MainloopFeedback): ''' Visual Speller with six circles like the classical HexOSpell. ''' # Triggers: look in Marker END_LEVEL1, END_LEVEL2 = 244, 245 # end of hex levels COPYSPELLING_FINISHED = 246 STIMULUS = [[11, 12, 13, 14, 15, 16], [21, 22, 23, 24, 25, 26]] RESPONSE = [[51, 52, 53, 54, 55, 56], [61, 62, 63, 64, 65, 66]] TARGET_ADD = 20 ERROR_ADD = 100 COUNTDOWN_STIMULI = 239 ERROR_POTENTIAL = 96 # send if error potential is classified def init(self): ''' initialize parameters ''' self.log_filename = 'VisualSpellerVE.log' self.geometry = [0, 0, 1280, 800] ## size self.letterbox_size = (60, 60) self.osc_size = 40 self.font_size_phrase = 60 # the spelled phrase at the top self.font_size_current_letter = 80 # the spelled phrase at the top self.font_size_countdown = 150 # number during countdown self.desired_phrase = "" ## colors: self.bg_color = (0., 0., 0.) self.phrase_color = (0.2, 0.0, 1.0) self.current_letter_color = (1.0, 0.0, 0.0) self.countdown_color = (0.2, 0.0, 1.0) self.osc_color = (1, 1, 1) self.letter_set = [['A','B','C','D','E'], \ ['F','G','H','I','J'], \ ['K','L','M','N','O'], \ ['P','Q','R','S','T'], \ ['U','V','W','X','Y'], \ ['Z','_','.',',','<']] self.fullscreen = False self.use_oscillator = True self.offline = True self.copy_spelling = True # in copy-spelling mode, selection of the target symbol is forced self.debug = False self.nCountdown = 5 self.nr_sequences = 6 self.randomize_sequence = True # set to False to present a fixed stimulus sequence self.min_dist = 2 # Min number of intermediate flashes bef. a flash is repeated twice self.stimulus_duration = 0.083 # 5 frames @60 Hz = 83ms flash self.interstimulus_duration = 0.1 self.animation_time = 1 self.wait_before_classify = 1. self.feedback_duration = 1. self.feedback_ErrP_duration = 1.0 self.wait_after_start = 0. # Countdown options self.do_animation = True self.synchronized_countdown = True if (self.synchronized_countdown): self.do_animation = False self.countdown_level1 = True self.countdown_level2 = True self.countdown_shapes = { 'circle': FilledCircle, 'hexagon': FilledHexagon } self.countdown_shape_select = 'hexagon' self.countdown_shape_color = (0.7, 0.7, 0.7) self.countdown_shape_on = True self.countdown_blinking_nr = 5 #number of pre-sequence stimuli(1 sec is approx. 5 frames at 60 Hz) self.wait_after_early_stopping = 3 #sec self.abort_trial = False self.output_per_stimulus = True self.use_ErrP_detection = False self.serialtrigger = False # FIXME: this should be fixed properly try: self.serialport = serialport.SerialPort(13) except: self.serialport = None self.send_parallel_bak = self.send_parallel if self.debug: msg = "!!! YOU\'RE IN DEBUG MODE! CLASSIFICATION WILL BE RANDOM OR KEYBOARD CONTROLLED !!!" self.logger.warning(msg) def pre_mainloop(self): ## logging assert (len(self.log_filename) != 0 ) # 'log_filename' must not be empty string! logger.setLevel(logging.ERROR) handler = logging.FileHandler(self.log_filename, 'w') handler.setLevel(logging.INFO) formatter = logging.Formatter("%(asctime)s: %(message)s") handler.setFormatter(formatter) self.logger.setLevel(logging.INFO) self.logger.addHandler(handler) self._nr_elements = 6 self._idx_backdoor = 5 self._init_classifier_output() self._classified_element = -1 self._classified_letter = -1 for s in self.desired_phrase: assert s in [l for ls in self.letter_set for l in ls] # invalid letters in desired phrase! self._spelled_phrase = "" self._spelled_letters = "" self._desired_letters = self.desired_phrase self._copyspelling_finished = False # if self.offline: # self.__idle() # In offline mode: set the first to-be-spelled letter self._spellerHeight = self.geometry[3] - self.letterbox_size[1] self._centerPos = (self.geometry[2] / 2., self._spellerHeight / 2.) self._nr_letters = 0 for i in xrange(len(self.letter_set)): self._nr_letters += len(self.letter_set[i]) self._current_level = 1 # Index of current level self._current_sequence = 0 # Index of current sequence self._current_stimulus = 0 # Index of current stimlus self._current_countdown = self.nCountdown self.random = random.Random(clock()) self._debug_classified = None ## init states: self._state_countdown = True if not self.countdown_level1: self._state_countdown = False self._state_trial = True else: #self._state_countdown = not self.offline self._state_trial = False self._state_classify = False self._state_feedback = False self._state_abort = False ## init containers for VE elements: self._ve_elements = [] ## oscillator state: if not self.use_oscillator: self.osc_color = self.bg_color self.osc_size = 0 ## call subclass-specific pre_mainloop: self.prepare_mainloop() ## build screen elements: self.__init_screen() if self.offline: self.__idle() if self.abort_trial: ''' Start listener for abort_trial event eg. ''' ## delay after play (might be useful for filters...) pygame.time.wait(int(self.wait_after_start * 1000)) self.logger.info("waiting %d seconds after play." % self.wait_after_start) ## send start trigger: self.send_parallel(marker.RUN_START) self.logger.info("[TRIGGER] %d" % marker.RUN_START) ## error potential classifier: self._ErrP_classifier = None def on_interaction_event(self, data): self.logger.debug('interaction event') serial = data.get('serialtrigger', None) if serial is None: return if serial: self.logger.debug('using serial port') self.send_parallel = self.serialport.send else: self.logger.debug('using parallel port') self.send_parallel = self.send_parallel_bak def post_mainloop(self): """ Sends end marker to parallel port. """ if self.abort_trial: ''' Stop listener for abort_trial event ''' pass pygame.time.wait(500) self.send_parallel(marker.RUN_END) self.logger.info("[TRIGGER] %d" % marker.RUN_END) pygame.time.wait(500) self._presentation.set(quit=True) self._screen.close() def __init_screen(self): ## create screen: if not self.fullscreen: os.environ['SDL_VIDEO_WINDOW_POS'] = '%d, %d' % (self.geometry[0], self.geometry[1]) self._screen = Screen(size=(self.geometry[2], self.geometry[3]), fullscreen=self.fullscreen, bgcolor=self.bg_color, sync_swap=True) ## create letter box on top: self._ve_letterbox = Target2D(position=(self._centerPos[0], self.geometry[3] * (1 - 0.01) - self.letterbox_size[1] / 2.), size=(self.letterbox_size[0], self.letterbox_size[1]), color=self.phrase_color) self._ve_innerbox = Target2D(position=(self._centerPos[0], self.geometry[3] * (1 - 0.01) - self.letterbox_size[1] / 2.), size=(self.letterbox_size[0] - 6, self.letterbox_size[1] - 6), color=self.bg_color) self._current_letter_position = (self._centerPos[0], self.geometry[3] * (1 - 0.015) - self.letterbox_size[1] / 2.) self._ve_current_letter = Text( position=self._current_letter_position, text=(len(self._desired_letters[:1]) == 0 and " " or self._desired_letters[:1]), font_size=self.font_size_current_letter, color=self.current_letter_color, anchor='center') self._ve_desired_letters = Text( position=(self._centerPos[0] + 5 + self.letterbox_size[0] / 2., self._current_letter_position[1]), text=(len(self._desired_letters[1:]) == 0 and " " or self._desired_letters[1:]), font_size=self.font_size_phrase, color=self.phrase_color, anchor='left') self._ve_spelled_phrase = Text( position=(self._centerPos[0] - 5 - self.letterbox_size[0] / 2., self._current_letter_position[1]), text=(len(self._spelled_phrase) == 0 and " " or self._spelled_phrase), font_size=self.font_size_phrase, color=self.phrase_color, anchor='right') # if we're in free spelling mode, we hide all text fields but # the _ve_spelled_phrase. we also need a multiline # _ve_spelled_phrase instead of the single lined one if self.offline == self.copy_spelling == False: self._spelled_phrase = " " self._ve_spelled_phrase = WrappedText( position=(0, self._current_letter_position[1]), text=(len(self._spelled_phrase) == 0 and " " or self._spelled_phrase), font_size=self.font_size_phrase, color=self.phrase_color, size=(float(self.geometry[2]), float(self.geometry[3]))) for i in self._ve_letterbox, self._ve_innerbox, self._ve_current_letter, self._ve_desired_letters: i.set(on=False) ## add word box to elementlist: self._ve_elements.extend([ self._ve_letterbox, self._ve_innerbox, self._ve_current_letter, self._ve_desired_letters, self._ve_spelled_phrase ]) ## create countdown: self._ve_countdown = Text(position=self._centerPos, text=" ", font_size=self.font_size_countdown, color=self.countdown_color, anchor='center', on=False) ## create countdown shapes self._ve_countdown_shape = self.countdown_shapes[ self.countdown_shape_select](radius=90, position=self._centerPos, color=self.countdown_shape_color, on=False) ## create oscillator circle: self._ve_oscillator = FilledCircle(position=(self.osc_size / 2 + 10, self.osc_size / 2 + 10), radius=self.osc_size / 2, color=self.osc_color, on=False) ## create shapes and letters: self.init_screen_elements() ## add remaining elements to element list: self._ve_elements.extend([ self._ve_countdown_shape, self._ve_countdown, self._ve_oscillator ]) ## add elements to viewport: self._viewport = Viewport(screen=self._screen, stimuli=self._ve_elements) self._presentation = Presentation(viewports=[self._viewport], handle_event_callbacks=[ (pygame.KEYDOWN, self.keyboard_input), (pygame.QUIT, self.__stop) ]) def play_tick(self): """ called every loop, if in play mode. """ self.pre_play_tick() if self._state_countdown: self.pre__countdown() self.__countdown() self.post__countdown() elif self._state_trial: self.pre__trial() self.__trial() self.post__trial() elif self._state_classify: self.pre__classify() self.__classify() self.post__classify() elif self._state_feedback: self.pre__feedback() self.__feedback() self.post__feedback() elif self._state_abort: self.__abort() self.post__abort() else: self.pre__idle() self.__idle() self.post__idle() self.post_play_tick() def __stop(self, *args): self.on_stop() def __idle(self): if self.offline and len(self._desired_letters) > 0: # add new letter: for e in xrange(len(self.letter_set)): for l in xrange(len(self.letter_set[e])): if self._desired_letters[0] == self.letter_set[e][l]: self._classified_element = e self._classified_letter = l if self.countdown_level1: self._state_countdown = True else: self._state_trial = True else: ## otherwise just wait until a new letter is sent: self._presentation.set(go_duration=(0.1, 'seconds')) self._presentation.go() def __countdown(self): def blink(): i = 0 while (i < self.countdown_blinking_nr): self._ve_countdown_shape.set(on=True) self._ve_countdown.set(text="%d" % self._current_countdown, on=True) self.send_parallel(self.COUNTDOWN_STIMULI) self.logger.info("[TRIGGER] %d" % self.COUNTDOWN_STIMULI) self._presentation.set(go_duration=(self.stimulus_duration, 'seconds')) self._presentation.go() self._ve_countdown_shape.set(on=False) self._ve_countdown.set(on=False) self._presentation.set( go_duration=(self.interstimulus_duration, 'seconds')) self._presentation.go() i = i + 1 if self._current_countdown == self.nCountdown: self.send_parallel(marker.COUNTDOWN_START) self.logger.info("[TRIGGER] %d" % marker.COUNTDOWN_START) self.set_countdown_screen() self._ve_countdown.set(on=True) self._ve_countdown_shape.set(on=self.countdown_shape_on) self._presentation.set(go_duration=(1, 'seconds')) self._ve_countdown.set(text="%d" % self._current_countdown) self._presentation.go() self._current_countdown = (self._current_countdown - 1) % self.nCountdown if self.synchronized_countdown and self._current_countdown == 1: self.set_synchronized_countdown_screen() blink() self._current_countdown = self.nCountdown self.set_standard_screen() self._state_countdown = False self._state_trial = True self._ve_countdown.set(on=False) self._ve_countdown_shape.set(on=False) self._ve_countdown.set(color=self.countdown_color) if self._current_countdown == 0: # Executed only if self.synchronized_countdown = False self._current_countdown = self.nCountdown self.set_standard_screen() pygame.time.wait(10) self._state_countdown = False self._state_trial = True self._ve_countdown_shape.set(on=False) self._ve_countdown.set(on=False) self._ve_countdown.set(color=self.countdown_color) def __trial(self): if self._current_sequence == 0 and self._current_stimulus == 0: #level 1 animation when there is no countdown if self._current_level == 1 and not self.countdown_level1: if self.do_animation: self.set_countdown_screen() self.set_standard_screen() # generate random sequences: if self.randomize_sequence: self.flash_sequence = [] for _ in range(self.nr_sequences): random_flash_sequence(self, set=range(self._nr_elements), min_dist=self.min_dist, seq_len=self._nr_elements) # or else use fixed sequence: else: self.flash_sequence = range(self._nr_elements) if self.randomize_sequence: currentStimulus = self.flash_sequence[self._current_sequence * self._nr_elements + self._current_stimulus] else: currentStimulus = self.flash_sequence[self._current_stimulus] # set stimulus: self.stimulus(currentStimulus, True) #self._ve_oscillator.set(on=True) if self.abort_trial and self.abort_trial_check(): # restart trial on abort_trial event: self._state_trial = False self._state_abort = True return # check if current stimulus is target and then send trigger: target_add = 0 if len(self._desired_letters) > 0: if self._current_level == 1: if self._desired_letters[:1] in self.letter_set[ currentStimulus]: # current stimulus is target group: target_add = self.TARGET_ADD else: if currentStimulus == self._idx_backdoor: # current stimulus is backdoor: if not self._desired_letters[:1] in self.letter_set[ self._classified_element]: # we are in the wrong group. backdoor is target: target_add = self.TARGET_ADD else: # current stimulus is no backdoor: if self._desired_letters[:1] == self.letter_set[ self._classified_element][currentStimulus]: # current stimulus is target symbol: target_add = self.TARGET_ADD self.send_parallel(self.STIMULUS[self._current_level - 1][currentStimulus] + target_add) self.logger.info( "[TRIGGER] %d" % (self.STIMULUS[self._current_level - 1][currentStimulus] + target_add)) # present stimulus: self._presentation.set(go_duration=(self.stimulus_duration, 'seconds')) self._presentation.go() # reset to normal: self._ve_oscillator.set(on=False) self.stimulus(currentStimulus, False) # present interstimulus: self._presentation.set(go_duration=(self.interstimulus_duration, 'seconds')) self._presentation.go() if self.debug: self.on_control_event( {'cl_output': (self.random.random(), currentStimulus + 1)}) ## TODO: check here for classification !!!! if self.output_per_stimulus: # increase self._current_stimulus = (self._current_stimulus + 1) % self._nr_elements if self._current_stimulus == 0: self._current_sequence = (self._current_sequence + 1) % self.nr_sequences # check for end of trial: if self._current_sequence == 0 and self._current_stimulus == 0: # send trigger: if self._current_level == 1: self.send_parallel(self.END_LEVEL1) self.logger.info("[TRIGGER] %d" % self.END_LEVEL1) else: self.send_parallel(self.END_LEVEL2) self.logger.info("[TRIGGER] %d" % self.END_LEVEL2) # decide how to continue: self._state_trial = False self._state_classify = True else: # increase self._current_stimulus = (self._current_stimulus + 1) % self._nr_elements if self._current_stimulus == 0: self._current_sequence = (self._current_sequence + 1) % self.nr_sequences if self.check_classification(self._current_sequence + 1): self._state_trial = False self._state_classify = True pygame.time.wait(self.wait_after_early_stopping * 1000) if self._current_sequence == 0 and self._current_stimulus == 0: # send trigger: if self._current_level == 1: self.send_parallel(self.END_LEVEL1) self.logger.info("[TRIGGER] %d" % self.END_LEVEL1) else: self.send_parallel(self.END_LEVEL2) self.logger.info("[TRIGGER] %d" % self.END_LEVEL2) # decide how to continue: self._state_trial = False self._state_classify = True def check_classification(self, nr): #print self._classifier_output means = [None] * self._nr_elements minimum = maxint classified = None for ii in range(self._nr_elements): means[ii] = sum(self._classifier_output[ii]) / nr if means[ii] < minimum: minimum = means[ii] classified = ii + 1 print "\n**** Class: %d (mean=%f)\n" % (classified, means[classified - 1]) return classified def __classify(self): ## wait until all classifier outputs are received: self._presentation.set(go_duration=(self.wait_before_classify, 'seconds')) self._presentation.go() if self.offline: if self._current_level == 1: classified = self._classified_element else: classified = self._classified_letter elif not self._debug_classified == None: classified = self._debug_classified self._debug_classified = None else: if self.output_per_stimulus: nClassified = sum([ len(self._classifier_output[i]) for i in xrange(self._nr_elements) ]) if nClassified < self._nr_elements * self.nr_sequences: pygame.time.wait(20) print 'not enough classifier-outputs received! (something may be wrong)' return ## classify and set output: means = [None] * self._nr_elements minimum = maxint classified = None for ii in range(self._nr_elements): means[ii] = sum( self._classifier_output[ii]) / self.nr_sequences if means[ii] < minimum: minimum = means[ii] classified = ii print "\n**** Class: %d (mean=%f)\n" % (classified + 1, means[classified]) else: means = [None] * self._nr_elements minimum = maxint classified = None for ii in range(self._nr_elements): means[ii] = sum( self._classifier_output[ii]) / self.nr_sequences if means[ii] < minimum: minimum = means[ii] classified = ii print "\n**** Class: %d (mean=%f)\n" % (classified + 1, means[classified]) ## Reset classifier output to empty lists self._init_classifier_output() error_add = 0 ## evaluate classification: if self._current_level == 1: self._classified_element = classified if len(self._desired_letters ) > 0 and not self._desired_letters[:1] in self.letter_set[ classified]: # wrong group selected: error_add = self.ERROR_ADD else: self._classified_letter = classified if self._classified_letter == self._idx_backdoor: ## backdoor classified: if len(self._desired_letters ) > 0 and self._desired_letters[:1] in self.letter_set[ self._classified_element]: # backdoor selection wrong: error_add = self.ERROR_ADD else: ## no backdoor classified: spelled_letter = self.letter_set[self._classified_element][ self._classified_letter] if len(self._desired_letters ) > 0 and spelled_letter != self._desired_letters[:1]: # wrong letter spelled: error_add = self.ERROR_ADD ## send response trigger: self.send_parallel(self.RESPONSE[self._current_level - 1][classified] + error_add) self.logger.info( "[TRIGGER] %d" % (self.RESPONSE[self._current_level - 1][classified] + error_add)) self._state_classify = False self._state_feedback = True def __feedback(self): self._state_feedback = False ## call subclass method: self.feedback() ## check ErrP classification: if self.use_ErrP_detection: t = 0 while self._ErrP_classifier is None and t < 1000: t += 50 pygame.time.wait(50) if self._ErrP_classifier is None: print "no ErrP classifier received! " if self._ErrP_classifier: self.send_parallel(self.ERROR_POTENTIAL) self.logger.info("[TRIGGER] %d" % (self.ERROR_POTENTIAL)) ## call subclass method: if not self.countdown_level2: self.switch_level() ## update phrases: if ((self._current_level == 2) and # only update, if we are at the end of level 2, (self._classified_letter != self._idx_backdoor or self.copy_spelling) and # if copyspelling off, we have no backdoor selected (self._ErrP_classifier is None or not self._ErrP_classifier) ): # no ErrP was detected (or ErrP detection is off) spelled_letter = "" if self.copy_spelling: ## in copy spelling we force the desired letter to be spelled if len(self._desired_letters) > 0: spelled_letter = self._desired_letters[:1] else: print "??? moved beyond desired phrase in copy spelling ???" else: spelled_letter = self.letter_set[self._classified_element][ self._classified_letter] ## update desired phrase: if len(self._desired_letters) > 0: if spelled_letter == self._desired_letters[:1]: # correct letter spelled: self._desired_letters = self._desired_letters[ 1:] # remove first letter else: # wrong letter spelled: if spelled_letter == "<": self._desired_letters = self._spelled_phrase[ -1:] + self._desired_letters else: self._desired_letters = "<" + self._desired_letters if len(self._desired_letters) == 0: self._copyspelling_finished = True ## update spelled phrase: self._spelled_letters += spelled_letter if spelled_letter == "<": self._spelled_phrase = self._spelled_phrase[:-1] else: self._spelled_phrase += spelled_letter ## update screen phrases: self.logger.info('Current Phrase:') self.logger.info(self._spelled_phrase) self._ve_spelled_phrase.set( text=(len(self._spelled_phrase) == 0 and " " or self._spelled_phrase)) self._ve_current_letter.set( text=(len(self._desired_letters[:1]) == 0 and " " or self._desired_letters[:1])) self._ve_desired_letters.set( text=(len(self._desired_letters[1:]) == 0 and " " or self._desired_letters[1:])) if self.use_ErrP_detection and self._ErrP_classifier: self._state_trial = True else: if self._current_level == 1: # continue with level2 trial: if self.countdown_level2: self._state_countdown = True else: self._state_trial = True elif not self.offline: # start countdown if self.countdown_level1: self._state_countdown = True else: self._state_trial = True # set new level: self._current_level = 3 - self._current_level ## reset ErrP_classifier: self._ErrP_classifier = None # check copyspelling: if self._copyspelling_finished: self._copyspelling_finished = False self.on_control_event({'print': 0}) # print desired phrase self.on_control_event({'print': 1}) # print spelled phrase self.on_control_event({'print': 2}) # print all spelled letters self.send_parallel(self.COPYSPELLING_FINISHED) self.logger.info("[TRIGGER] %d" % (self.COPYSPELLING_FINISHED)) pygame.time.wait(50) def __abort(self): # play warning sound def sine_array_onecycle(hz, peak, sample_rate): length = sample_rate / float(hz) omega = NP.pi * 2 / length xvalues = NP.arange(int(length)) * omega return (peak * NP.sin(xvalues)) def sine_array(hz, peak, samples_rate): return NP.resize(sine_array_onecycle(hz, peak, sample_rate), (sample_rate, )) sample_rate = 44100 pygame.mixer.init(sample_rate, -16, 2) # 44.1kHz, 16-bit signed, stereo f = sine_array(8000, 1, sample_rate) f = NP.array(zip(f, f)) sound = pygame.sndarray.make_sound(f) channel = sound.play(-1) channel.set_volume(0.2, 0.2) pygame.time.delay(1000) sound.stop() if self._current_level == 1 and self.countdown_level1: self._state_countdown = True elif self._current_level == 2 and self.countdown_level2: self._state_countdown = True else: self._state_trial = True self._init_classifier_output() def _init_classifier_output(self): ## Empty lists self._classifier_output = [list() for _ in xrange(self._nr_elements)] def abort_trial_check(self): ''' Check if event is an abort trial event ''' return False def keyboard_input(self, event): if event.key == pygame.K_ESCAPE: self.on_stop() elif event.key == pygame.K_KP_ENTER: self.on_control_event({'print': 0}) # print desired phrase self.on_control_event({'print': 1}) # print spelled phrase self.on_control_event({'print': 2}) # print all spelled letters elif event.key == pygame.K_DELETE: # The DELETE key empties the spelled text shown this works # only in free spelling mode (i.e. offline and copy_spelling # are set to False) if self.offline == self.copy_spelling == False: self.logger.info('Clearing Text.') self._spelled_phrase = " " elif self.debug: if ((event.key >= pygame.K_a and event.key <= pygame.K_z) or (event.key == pygame.K_LESS) or (event.key == pygame.K_PERIOD) or (event.key == pygame.K_COMMA)): self.on_control_event({'new_letter': chr(event.key).upper()}) elif event.key == pygame.K_MINUS: self.on_control_event({'new_letter': chr(pygame.K_UNDERSCORE)}) elif event.key == pygame.K_BACKSPACE: self.on_control_event({'new_letter': chr(pygame.K_LESS)}) elif event.key == pygame.K_SPACE: self.on_control_event({'new_letter': chr(pygame.K_UNDERSCORE)}) elif event.key == pygame.K_UP and self.use_ErrP_detection: self.on_control_event({'cl_output': (1, 7)}) elif event.key == pygame.K_DOWN and self.use_ErrP_detection: self.on_control_event({'cl_output': (0, 7)}) if not self.offline: if (event.key >= pygame.K_0 and event.key <= pygame.K_5): self._debug_classified = int(chr(event.key)) elif (event.key >= pygame.K_KP0 and event.key <= pygame.K_KP5): self._debug_classified = int(chr(event.key - 208)) def on_control_event(self, data): self.logger.info("[CONTROL_EVENT] %s" % str(data)) if data.has_key(u'cl_output'): # classification output was sent: score_data = data[u'cl_output'] cl_out = score_data[0] iSubstim = int(score_data[1]) # evt auch "Subtrial" if iSubstim in range(1, 7): self._classifier_output[iSubstim - 1].append(cl_out) elif self.use_ErrP_detection: self._ErrP_classifier = cl_out elif data.has_key('new_letter'): # get new letter to spell: self._desired_letters += data['new_letter'] self._ve_current_letter.set( text=(len(self._desired_letters[:1]) == 0 and " " or self._desired_letters[:1])) self._ve_desired_letters.set( text=(len(self._desired_letters[1:]) == 0 and " " or self._desired_letters[1:])) elif data.has_key(u'print'): if data[u'print'] == 0: self.logger.info("[DESIRED_PHRASE] %s" % self.desired_phrase) elif data[u'print'] == 1: self.logger.info("[SPELLED_PHRASE] %s" % self._spelled_phrase) elif data[u'print'] == 2: self.logger.info("[SPELLED_LETTERS] %s" % self._spelled_letters) ''' ========================== == METHODS TO OVERLOAD: == ========================== ''' def init_screen_elements(self): ''' overwrite this function in subclass. ''' pass def prepare_mainloop(self): ''' overwrite this function in subclass. ''' pass def set_countdown_screen(self): ''' set screen how it should look during countdown. overwrite this function in subclass. ''' pass def set_standard_screen(self): ''' set screen elements to standard state. overwrite this function in subclass. ''' pass def set_synchronized_countdown_screen(self): ''' set screen elements to for the synchronized countdown. overwrite this function in subclass. ''' pass def stimulus(self, i_element, on=True): ''' turn on/off the stimulus elements and turn off/on the normal elements. overwrite this function in subclass. ''' pass def feedback(self): ''' set screen how it should look during feedback presentation. overwrite this function in subclass. ''' pass def switch_level(self): ''' overwrite this function in subclass. ''' pass def pre_play_tick(self): pass def post_play_tick(self): pass def pre__countdown(self): pass def post__countdown(self): pass def pre__trial(self): pass def post__trial(self): pass def pre__classify(self): pass def post__classify(self): pass def pre__feedback(self): pass def post__feedback(self): pass def pre__idle(self): pass def post__idle(self): pass def post__abort(self): pass
class VisionEggView(object): """ This class handles VisionEgg internals and the creation of common/standard stimuli like centered words, a fixation cross or a countdown. Inherit this and pass the type to VisionEggFeedback for customization. """ def __init__(self): self.__init_attributes() def __init_attributes(self): """ Setup internal attributes. """ self._logger = logging.getLogger('VisionEggView') self._logger.addHandler(logging.FileHandler('log')) self._screen_acquired = False self._viewports = [] def set_trigger_function(self, trigger): self._trigger = trigger def set_event_handlers(self, event_handlers): """ Set pygame/VisionEgg event handler function. """ self._event_handlers = event_handlers def set_iterator_semaphore(self, flag): """ Specify the object to be used as semaphore for iterators. See L{Switcherator} for more. """ self._iter = lambda it: Switcherator(flag, it) def update_parameters(self, **kwargs): """ Apply new parameters set from pyff. """ for k, v in kwargs.iteritems(): setattr(self, '_' + k, v) if self._screen_acquired: self.reinit() def acquire(self): """ Allow L{update_parameters} initialize VisionEgg. """ self._screen_acquired = True def reinit(self): """ Initialize VisionEgg objects. """ self.__init_screen() self.__init_presentation() self.__init_viewports() self.init() self.__init_text() def init(self): """ Overload this for additional custom VisionEgg initialization. """ pass def __init_screen(self): """ Create the VisionEgg Screen object using the pyff configuration parameters 'fullscreen' and 'geometry' and the font and background colors according to parameters 'font_color_name' and 'bg_color'. """ params = { 'fullscreen': self._fullscreen, 'sync_swap': True } if not self._fullscreen: os.environ['SDL_VIDEO_WINDOW_POS'] = '%d, %d' % (self._geometry[0], self._geometry[1]) params['size'] = self._geometry[2:] self.screen = Screen(**params) self._set_bg_color() self._set_font_color() def __init_presentation(self): """ Provide a standard presentation object. """ self.presentation = Presentation(handle_event_callbacks= self._event_handlers) def __init_viewports(self): """ Provide a standard viewport. """ self._standard_viewport = Viewport(screen=self.screen) self.add_viewport(self._standard_viewport) def __init_text(self): """ Provide a text in the screen center for standard stimuli and fixation cross etc. """ sz = self.screen.size self._center_text = self.add_color_word(position=(sz[0] / 2., sz[1] / 2.), font_size=self._font_size) def add_viewport(self, viewport): """ Add an additional custom viewport object to the list of viewports. """ self._viewports.append(viewport) self.presentation.set(viewports=self._viewports) def clear_stimuli(self): """ Remove all existing stimuli in the standard viewport. """ self.set_stimuli() def add_stimuli(self, *stimuli): """ Add additional custom stimulus objects to the list of stimuli. TextList instances need their own Viewport, as they consist of multiple stimulus objects that are deleted everytime they change, and so the Viewport needs to have a reference to the containing TextList, otherwise they get lost. """ text_lists = filter(lambda s: isinstance(s, TextList), stimuli) if text_lists: for text in text_lists: self.add_viewport(Viewport(screen=self.screen, stimuli=text)) stimuli = filter(lambda s: not isinstance(s, TextList), stimuli) stimuli = self._standard_viewport.parameters.stimuli + list(stimuli) if stimuli: self.set_stimuli(*stimuli) def set_stimuli(self, *stimuli): """ Set the list of stimulus objects. """ self._standard_viewport.set(stimuli=list(stimuli)) def add_text_stimulus(self, text, font_size=None, **kw): if not kw.has_key('anchor'): kw['anchor'] = 'center' font_size = font_size or self._font_size txt = VisionEgg.Text.Text(text=text, font_size=font_size, **kw) self.add_stimuli(txt) return txt def add_color_word(self, text='', font_size=None, **kw): font_size = font_size or self._font_size txt = ColorWord(text=text, symbol_size=font_size, **kw) self.add_stimuli(txt) return txt def add_image_stimulus(self, **kw): if not kw.has_key('anchor'): kw['anchor'] = 'center' img = TextureStimulus(**kw) self.add_stimuli(img) return img def _create_color(self, name): try: if isinstance(name, tuple): return Color(*name).normalize() else: return Color(str(name)).normalize() except ValueError: self._logger.warn('No such pygame.Color: %s' % str(name)) def _set_font_color(self): """ Set the standard font color by pygame name. """ self._font_color = (self._create_color(self._font_color_name) or Color(1, 1, 1, 255).normalize()) def _set_bg_color(self): """ Set the standard background color by pygame name. """ c = (self._create_color(self._bg_color) or Color(0, 0, 0, 255).normalize()) self.screen.set(bgcolor=c) def present_frames(self, num_frames): """ Launch the presentation main loop for a given number of frames. """ self.presentation.set(go_duration=(num_frames, 'frames')) self.presentation.go() def present(self, sec): self.presentation.set(go_duration=(sec, 'seconds')) self.presentation.go() def update(self): """ Repaint the canvas for one frame to update changed stimuli. """ self.present_frames(1) def center_word(self, text, color=None): """ Set the standard word in the screen center. """ self._center_text.set(text=text) self._center_text.set(colors=color or (self._font_color for l in self._center_text)) def clear_center_word(self): """ Remove the center word from the screen. """ self.center_word('') self.update() def present_center_word(self, text, seconds, color=None): self.center_word(text, color) self.present(seconds) self.clear_center_word() def ask(self, question=True): """ Loop indefinitely until answered() is called. If question is True, a question mark is shown in the center. """ if question: self.center_word('?') self.presentation.run_forever() self.presentation.set(quit=False) self.clear_center_word() def answered(self): """ Abort the current presentation (usually the question mark) after subject input. For thread safety, the screen shouldn't be changed here. """ self.presentation.set(quit=True) def show_fixation_cross(self): """ Display the pyff parameter 'fixation_cross_symbol' for the period of time given by pyff parameter 'fixation_cross_time'. """ self.center_word(self._fixation_cross_symbol) self._trigger(marker.FIXATION_START, wait=True) self.present(self._fixation_cross_time) self._trigger(marker.FIXATION_END, wait=True) def countdown(self): """ Display a countdown according to pyff parameters 'countdown_start' and 'countdown_symbol_duration'. """ self._trigger(marker.COUNTDOWN_START, wait=True) for i in self._iter(reversed(xrange(self._countdown_start + 1))): self.center_word(str(i)) self.present(self._countdown_symbol_duration) self._trigger(marker.COUNTDOWN_END, wait=True) self.clear_center_word() def close(self): """ Shut down the screen. """ self._screen_acquired = False self.screen.close() def quit(self): """ Stop the presentation. """ self.presentation.set(quit=True) self.presentation.set(go_duration=(1, 'frames'))