Beispiel #1
0
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)
Beispiel #4
0
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'))
Beispiel #5
0
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
Beispiel #6
0
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'))